Repository: git-town/git-town

Issue:
bug: `gh` connector exits 1 and fails to open pr in browser during CreateProposal if "Continue in browser" is chosen
Using the `gh` connector, if I select "Continue in browser" to create the PR in a browser, CreateProposal opens the browser for user input, and then immediately tries to open the PR with `gh pr view --web`. This fails because the PR doesn't exist yet (user is still making the PR in the browser).

Specifically, I think it's [this line](https://github.com/git-town/git-town/blob/main/internal/forge/gh/connector.go#L46) in the gh connector that fails.

I only use the `gh` connector personally, but looking over other CreateProposal implementations, it seems like this is the only one that tries to do this. I'm thinking it could make sense to offload the `gh pr view --web` call to another opcode.

I'd guess that some users' workflows are purely cli and don't want the browser open anyhow, so maybe browser viewing (or not viewing) can be put behind another config option.

### Example:

<img width="972" height="327" alt="Image" src="https://github.com/user-attachments/assets/fe830a8f-35dd-4595-8eac-fc9673f9261b" />

### Steps to reproduce

1. Use a github repo, using the `gh` connector
2. Create a new feature branch (or switch to an existing one without a PR attached)
3. `git town propose`
4. When the `gh` call to create a PR is being made, choose the "Continue in browser" option
5. error

### Command output

<details>
<summary>click to expand 👈 </summary>

```shell
$ git town propose -v

(verbose) git version
git version 2.50.1 (Apple Git-155)

(verbose) git rev-parse --show-toplevel
/Users/jeff.wang/code/github/git-town

(verbose) git config -lz --global
user.name
Jeff W

user.email
233818084+j2fw@users.noreply.github.com

url.git@github.com:.insteadof
https://github.com/


(verbose) git config -lz --local
core.repositoryformatversion
0

core.filemode
true

core.bare
false

core.logallrefupdates
true

core.ignorecase
true

core.precomposeunicode
true

remote.origin.url
git@github.com:j2fw/git-town.git

remote.origin.fetch
+refs/heads/*:refs/remotes/origin/*

remote.origin.gh-resolved
base

branch.main.remote
origin

branch.main.merge
refs/heads/main

branch.main.vscode-merge-base
origin/main

git-town.github-connector
gh

git-town.main-branch
main

git-town.proposals-show-lineage
true

git-town-branch.jw/prs-cmd.parent
jw/b

branch.jw/prs-cmd.remote
origin

branch.jw/prs-cmd.merge
refs/heads/jw/prs-cmd

branch.jw/prs-cmd.vscode-merge-base
origin/main

git-town-branch.jw/a.parent
main

branch.jw/a.remote
origin

branch.jw/a.merge
refs/heads/jw/a

git-town-branch.jw/b.parent
jw/a

branch.jw/b.remote
origin

branch.jw/b.merge
refs/heads/jw/b

git-town-branch.jw/propose-with-lineage.parent
main

branch.jw/propose-with-lineage.remote
origin

branch.jw/propose-with-lineage.merge
refs/heads/jw/propose-with-lineage

remote.upstream.url
git@github.com:git-town/git-town.git

remote.upstream.fetch
+refs/heads/*:refs/remotes/upstream/*

branch.jw/add-lineage-to-proposed-body.remote
origin

branch.jw/add-lineage-to-proposed-body.merge
refs/heads/jw/add-lineage-to-proposed-body

git-town-branch.jw/propose-with-lineage-in-body.parent
main

branch.jw/propose-with-lineage-in-body.remote
origin

branch.jw/propose-with-lineage-in-body.merge
refs/heads/jw/propose-with-lineage-in-body

git-town-branch.jw/docs-link-fix.parent
main

branch.jw/docs-link-fix.remote
origin

branch.jw/docs-link-fix.merge
refs/heads/jw/docs-link-fix

branch.testing-links.remote
origin

branch.testing-links.merge
refs/heads/testing-links

git-town-branch.try-browser-flow.parent
jw/propose-with-lineage-in-body

branch.try-browser-flow.remote
origin

branch.try-browser-flow.merge
refs/heads/try-browser-flow

git-town-branch.try-browser-flow-2.parent
try-browser-flow

branch.try-browser-flow-2.remote
origin

branch.try-browser-flow-2.merge
refs/heads/try-browser-flow-2


(verbose) git config -lz
credential.helper
osxkeychain

init.defaultbranch
main

user.name
Jeff W

user.email
233818084+j2fw@users.noreply.github.com

url.git@github.com:.insteadof
https://github.com/

core.repositoryformatversion
0

core.filemode
true

core.bare
false

core.logallrefupdates
true

core.ignorecase
true

core.precomposeunicode
true

remote.origin.url
git@github.com:j2fw/git-town.git

remote.origin.fetch
+refs/heads/*:refs/remotes/origin/*

remote.origin.gh-resolved
base

branch.main.remote
origin

branch.main.merge
refs/heads/main

branch.main.vscode-merge-base
origin/main

git-town.github-connector
gh

git-town.main-branch
main

git-town.proposals-show-lineage
true

git-town-branch.jw/prs-cmd.parent
jw/b

branch.jw/prs-cmd.remote
origin

branch.jw/prs-cmd.merge
refs/heads/jw/prs-cmd

branch.jw/prs-cmd.vscode-merge-base
origin/main

git-town-branch.jw/a.parent
main

branch.jw/a.remote
origin

branch.jw/a.merge
refs/heads/jw/a

git-town-branch.jw/b.parent
jw/a

branch.jw/b.remote
origin

branch.jw/b.merge
refs/heads/jw/b

git-town-branch.jw/propose-with-lineage.parent
main

branch.jw/propose-with-lineage.remote
origin

branch.jw/propose-with-lineage.merge
refs/heads/jw/propose-with-lineage

remote.upstream.url
git@github.com:git-town/git-town.git

remote.upstream.fetch
+refs/heads/*:refs/remotes/upstream/*

branch.jw/add-lineage-to-proposed-body.remote
origin

branch.jw/add-lineage-to-proposed-body.merge
refs/heads/jw/add-lineage-to-proposed-body

git-town-branch.jw/propose-with-lineage-in-body.parent
main

branch.jw/propose-with-lineage-in-body.remote
origin

branch.jw/propose-with-lineage-in-body.merge
refs/heads/jw/propose-with-lineage-in-body

git-town-branch.jw/docs-link-fix.parent
main

branch.jw/docs-link-fix.remote
origin

branch.jw/docs-link-fix.merge
refs/heads/jw/docs-link-fix

branch.testing-links.remote
origin

branch.testing-links.merge
refs/heads/testing-links

git-town-branch.try-browser-flow.parent
jw/propose-with-lineage-in-body

branch.try-browser-flow.remote
origin

branch.try-browser-flow.merge
refs/heads/try-browser-flow

git-town-branch.try-browser-flow-2.parent
try-browser-flow

branch.try-browser-flow-2.remote
origin

branch.try-browser-flow-2.merge
refs/heads/try-browser-flow-2

Git Town 22.1.0
OS: darwin
Darwin porcini.local 24.6.0 Darwin Kernel Version 24.6.0: Mon Aug 11 21:15:09 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T6041 arm64

(verbose) git for-each-ref "--format=refname:%(refname) branchname:%(refname:lstrip=2) sha:%(objectname) head:%(if)%(HEAD)%(then)Y%(else)N%(end) worktree:%(if)%(worktreepath)%(then)Y%(else)N%(end) symref:%(if)%(symref)%(then)Y%(else)N%(end) upstream:%(upstream:lstrip=2) track:%(upstream:track,nobracket)" --sort=refname refs/heads/ refs/remotes/
refname:refs/heads/jw/a branchname:jw/a sha:a1bc3549927b11ac52cfadf7914e0314d77516cf head:N worktree:N symref:N upstream:origin/jw/a track:
refname:refs/heads/jw/add-lineage-to-proposed-body branchname:jw/add-lineage-to-proposed-body sha:6cc58fa559dcb4a527d37e6b907fe6f313fb0445 head:N worktree:N symref:N upstream:origin/jw/add-lineage-to-proposed-body track:gone
refname:refs/heads/jw/b branchname:jw/b sha:a1bc3549927b11ac52cfadf7914e0314d77516cf head:N worktree:N symref:N upstream:origin/jw/b track:
refname:refs/heads/jw/docs-link-fix branchname:jw/docs-link-fix sha:fc41984a6e40e4f34d01f2f04f43adc3c27da2cd head:N worktree:N symref:N upstream:origin/jw/docs-link-fix track:
refname:refs/heads/jw/propose-with-lineage branchname:jw/propose-with-lineage sha:6cc58fa559dcb4a527d37e6b907fe6f313fb0445 head:N worktree:N symref:N upstream:origin/jw/propose-with-lineage track:
refname:refs/heads/jw/propose-with-lineage-in-body branchname:jw/propose-with-lineage-in-body sha:06adcbb140ea92faf63174d1093d27ef6ca3bcfc head:N worktree:N symref:N upstream:origin/jw/propose-with-lineage-in-body track:
refname:refs/heads/jw/prs-cmd branchname:jw/prs-cmd sha:009747e72c445cc639d6f257233a58876e353e51 head:N worktree:N symref:N upstream:origin/jw/prs-cmd track:
refname:refs/heads/main branchname:main sha:973f35dde3d95f51ad20548df246d09b390a2983 head:N worktree:N symref:N upstream:origin/main track:
refname:refs/heads/testing-links branchname:testing-links sha:6958a60545161e2d2a45f7ff030049db20189ee4 head:N worktree:N symref:N upstream:origin/testing-links track:
refname:refs/heads/try-browser-flow branchname:try-browser-flow sha:fde034a18a3624f18934d6f80af633e95524394b head:Y worktree:Y symref:N upstream:origin/try-browser-flow track:
refname:refs/heads/try-browser-flow-2 branchname:try-browser-flow-2 sha:827469cf1dcb8f354655eb18c0cbc6d94c391a91 head:N worktree:N symref:N upstream:origin/try-browser-flow-2 track:
refname:refs/remotes/origin/HEAD branchname:origin/HEAD sha:973f35dde3d95f51ad20548df246d09b390a2983 head:N worktree:N symref:Y upstream: track:
refname:refs/remotes/origin/jw/a branchname:origin/jw/a sha:a1bc3549927b11ac52cfadf7914e0314d77516cf head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/b branchname:origin/jw/b sha:a1bc3549927b11ac52cfadf7914e0314d77516cf head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/docs-link-fix branchname:origin/jw/docs-link-fix sha:fc41984a6e40e4f34d01f2f04f43adc3c27da2cd head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/propose-with-lineage branchname:origin/jw/propose-with-lineage sha:6cc58fa559dcb4a527d37e6b907fe6f313fb0445 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/propose-with-lineage-in-body branchname:origin/jw/propose-with-lineage-in-body sha:06adcbb140ea92faf63174d1093d27ef6ca3bcfc head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/prs-cmd branchname:origin/jw/prs-cmd sha:009747e72c445cc639d6f257233a58876e353e51 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/main branchname:origin/main sha:973f35dde3d95f51ad20548df246d09b390a2983 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/testing-links branchname:origin/testing-links sha:6958a60545161e2d2a45f7ff030049db20189ee4 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/try-browser-flow branchname:origin/try-browser-flow sha:fde034a18a3624f18934d6f80af633e95524394b head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/try-browser-flow-2 branchname:origin/try-browser-flow-2 sha:827469cf1dcb8f354655eb18c0cbc6d94c391a91 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/upstream/main branchname:upstream/main sha:973f35dde3d95f51ad20548df246d09b390a2983 head:N worktree:N symref:N upstream: track:

(verbose) git status -z --ignore-submodules

(verbose) git rev-parse --verify -q MERGE_HEAD

(verbose) git rev-parse --absolute-git-dir
/Users/jeff.wang/code/github/git-town/.git

(verbose) git remote get-url origin
git@github.com:j2fw/git-town.git
Handle unfinished command: discard

(verbose) git remote
origin
upstream

[try-browser-flow] git fetch --prune --tags

(verbose) git stash list
stash@{0}: On jw/docs-link-fix: Git Town WIP
stash@{1}: WIP on jw/prs-cmd: 009747e72 Merge remote-tracking branch 'origin/jw/prs-cmd' into jw/prs-cmd

(verbose) git for-each-ref "--format=refname:%(refname) branchname:%(refname:lstrip=2) sha:%(objectname) head:%(if)%(HEAD)%(then)Y%(else)N%(end) worktree:%(if)%(worktreepath)%(then)Y%(else)N%(end) symref:%(if)%(symref)%(then)Y%(else)N%(end) upstream:%(upstream:lstrip=2) track:%(upstream:track,nobracket)" --sort=refname refs/heads/ refs/remotes/
refname:refs/heads/jw/a branchname:jw/a sha:a1bc3549927b11ac52cfadf7914e0314d77516cf head:N worktree:N symref:N upstream:origin/jw/a track:
refname:refs/heads/jw/add-lineage-to-proposed-body branchname:jw/add-lineage-to-proposed-body sha:6cc58fa559dcb4a527d37e6b907fe6f313fb0445 head:N worktree:N symref:N upstream:origin/jw/add-lineage-to-proposed-body track:gone
refname:refs/heads/jw/b branchname:jw/b sha:a1bc3549927b11ac52cfadf7914e0314d77516cf head:N worktree:N symref:N upstream:origin/jw/b track:
refname:refs/heads/jw/docs-link-fix branchname:jw/docs-link-fix sha:fc41984a6e40e4f34d01f2f04f43adc3c27da2cd head:N worktree:N symref:N upstream:origin/jw/docs-link-fix track:
refname:refs/heads/jw/propose-with-lineage branchname:jw/propose-with-lineage sha:6cc58fa559dcb4a527d37e6b907fe6f313fb0445 head:N worktree:N symref:N upstream:origin/jw/propose-with-lineage track:
refname:refs/heads/jw/propose-with-lineage-in-body branchname:jw/propose-with-lineage-in-body sha:06adcbb140ea92faf63174d1093d27ef6ca3bcfc head:N worktree:N symref:N upstream:origin/jw/propose-with-lineage-in-body track:
refname:refs/heads/jw/prs-cmd branchname:jw/prs-cmd sha:009747e72c445cc639d6f257233a58876e353e51 head:N worktree:N symref:N upstream:origin/jw/prs-cmd track:
refname:refs/heads/main branchname:main sha:973f35dde3d95f51ad20548df246d09b390a2983 head:N worktree:N symref:N upstream:origin/main track:
refname:refs/heads/testing-links branchname:testing-links sha:6958a60545161e2d2a45f7ff030049db20189ee4 head:N worktree:N symref:N upstream:origin/testing-links track:
refname:refs/heads/try-browser-flow branchname:try-browser-flow sha:fde034a18a3624f18934d6f80af633e95524394b head:Y worktree:Y symref:N upstream:origin/try-browser-flow track:
refname:refs/heads/try-browser-flow-2 branchname:try-browser-flow-2 sha:827469cf1dcb8f354655eb18c0cbc6d94c391a91 head:N worktree:N symref:N upstream:origin/try-browser-flow-2 track:
refname:refs/remotes/origin/HEAD branchname:origin/HEAD sha:973f35dde3d95f51ad20548df246d09b390a2983 head:N worktree:N symref:Y upstream: track:
refname:refs/remotes/origin/jw/a branchname:origin/jw/a sha:a1bc3549927b11ac52cfadf7914e0314d77516cf head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/b branchname:origin/jw/b sha:a1bc3549927b11ac52cfadf7914e0314d77516cf head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/docs-link-fix branchname:origin/jw/docs-link-fix sha:fc41984a6e40e4f34d01f2f04f43adc3c27da2cd head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/propose-with-lineage branchname:origin/jw/propose-with-lineage sha:6cc58fa559dcb4a527d37e6b907fe6f313fb0445 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/propose-with-lineage-in-body branchname:origin/jw/propose-with-lineage-in-body sha:06adcbb140ea92faf63174d1093d27ef6ca3bcfc head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/prs-cmd branchname:origin/jw/prs-cmd sha:009747e72c445cc639d6f257233a58876e353e51 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/main branchname:origin/main sha:973f35dde3d95f51ad20548df246d09b390a2983 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/testing-links branchname:origin/testing-links sha:6958a60545161e2d2a45f7ff030049db20189ee4 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/try-browser-flow branchname:origin/try-browser-flow sha:fde034a18a3624f18934d6f80af633e95524394b head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/try-browser-flow-2 branchname:origin/try-browser-flow-2 sha:827469cf1dcb8f354655eb18c0cbc6d94c391a91 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/upstream/main branchname:upstream/main sha:973f35dde3d95f51ad20548df246d09b390a2983 head:N worktree:N symref:N upstream: track:

(verbose) git rev-parse --verify --abbrev-ref @{-1}
jw/propose-with-lineage-in-body

(verbose) gh pr list --head=try-browser-flow --base=jw/propose-with-lineage-in-body --json=number,title,body,mergeable,headRefName,baseRefName,url
[]

(verbose) git log main..jw/propose-with-lineage-in-body --format=%s --reverse
proposal_create: insert lineage into proposalBody if possible
Merge branch 'main' into jw/propose-with-lineage
check if we actually find a proposal
Merge branch 'main' into jw/propose-with-lineage-in-body
proposal_create: add TODO for lineage construction error

(verbose) git log jw/propose-with-lineage-in-body..try-browser-flow --format=%s --reverse
proposal_create: add TODO for lineage construction error
Merge branch 'jw/propose-with-lineage-in-body' into try-browser-flow
Merge branch 'jw/propose-with-lineage-in-body' into try-browser-flow

[try-browser-flow] git checkout jw/propose-with-lineage-in-body
Switched to branch 'jw/propose-with-lineage-in-body'
Your branch is up to date with 'origin/jw/propose-with-lineage-in-body'.

(verbose) git rev-parse jw/propose-with-lineage-in-body origin/jw/propose-with-lineage-in-body
06adcbb140ea92faf63174d1093d27ef6ca3bcfc
06adcbb140ea92faf63174d1093d27ef6ca3bcfc

(verbose) git rev-parse --verify -q refs/heads/jw/propose-with-lineage-in-body
06adcbb140ea92faf63174d1093d27ef6ca3bcfc

(verbose) git rev-parse jw/propose-with-lineage-in-body origin/jw/propose-with-lineage-in-body
06adcbb140ea92faf63174d1093d27ef6ca3bcfc
06adcbb140ea92faf63174d1093d27ef6ca3bcfc

[jw/propose-with-lineage-in-body] git checkout try-browser-flow
Switched to branch 'try-browser-flow'
Your branch is up to date with 'origin/try-browser-flow'.

(verbose) git log --no-merges --format=%H refs/heads/jw/propose-with-lineage-in-body ^refs/heads/try-browser-flow

(verbose) git rev-parse try-browser-flow origin/try-browser-flow
fde034a18a3624f18934d6f80af633e95524394b
fde034a18a3624f18934d6f80af633e95524394b

(verbose) git rev-parse --verify -q refs/heads/try-browser-flow
fde034a18a3624f18934d6f80af633e95524394b

(verbose) git rev-parse try-browser-flow origin/try-browser-flow
fde034a18a3624f18934d6f80af633e95524394b
fde034a18a3624f18934d6f80af633e95524394b

(verbose) git rev-parse --abbrev-ref --symbolic-full-name @{u}
origin/try-browser-flow

(verbose) git rev-parse --verify -q refs/heads/jw/propose-with-lineage-in-body
06adcbb140ea92faf63174d1093d27ef6ca3bcfc

[try-browser-flow] gh pr create --head=try-browser-flow --base=jw/propose-with-lineage-in-body

Creating pull request for try-browser-flow into jw/propose-with-lineage-in-body in j2fw/git-town

? Title (required) proposal_create: add TODO for lineage construction error
? Body <Received>
? What's next? Continue in browser
Opening https://github.com/j2fw/git-town/compare/jw/propose-with-lineage-in-body...try-browser-flow in your browser.

[try-browser-flow] gh pr view --web
no pull requests found for branch "try-browser-flow"

(verbose) git for-each-ref "--format=refname:%(refname) branchname:%(refname:lstrip=2) sha:%(objectname) head:%(if)%(HEAD)%(then)Y%(else)N%(end) worktree:%(if)%(worktreepath)%(then)Y%(else)N%(end) symref:%(if)%(symref)%(then)Y%(else)N%(end) upstream:%(upstream:lstrip=2) track:%(upstream:track,nobracket)" --sort=refname refs/heads/ refs/remotes/
refname:refs/heads/jw/a branchname:jw/a sha:a1bc3549927b11ac52cfadf7914e0314d77516cf head:N worktree:N symref:N upstream:origin/jw/a track:
refname:refs/heads/jw/add-lineage-to-proposed-body branchname:jw/add-lineage-to-proposed-body sha:6cc58fa559dcb4a527d37e6b907fe6f313fb0445 head:N worktree:N symref:N upstream:origin/jw/add-lineage-to-proposed-body track:gone
refname:refs/heads/jw/b branchname:jw/b sha:a1bc3549927b11ac52cfadf7914e0314d77516cf head:N worktree:N symref:N upstream:origin/jw/b track:
refname:refs/heads/jw/docs-link-fix branchname:jw/docs-link-fix sha:fc41984a6e40e4f34d01f2f04f43adc3c27da2cd head:N worktree:N symref:N upstream:origin/jw/docs-link-fix track:
refname:refs/heads/jw/propose-with-lineage branchname:jw/propose-with-lineage sha:6cc58fa559dcb4a527d37e6b907fe6f313fb0445 head:N worktree:N symref:N upstream:origin/jw/propose-with-lineage track:
refname:refs/heads/jw/propose-with-lineage-in-body branchname:jw/propose-with-lineage-in-body sha:06adcbb140ea92faf63174d1093d27ef6ca3bcfc head:N worktree:N symref:N upstream:origin/jw/propose-with-lineage-in-body track:
refname:refs/heads/jw/prs-cmd branchname:jw/prs-cmd sha:009747e72c445cc639d6f257233a58876e353e51 head:N worktree:N symref:N upstream:origin/jw/prs-cmd track:
refname:refs/heads/main branchname:main sha:973f35dde3d95f51ad20548df246d09b390a2983 head:N worktree:N symref:N upstream:origin/main track:
refname:refs/heads/testing-links branchname:testing-links sha:6958a60545161e2d2a45f7ff030049db20189ee4 head:N worktree:N symref:N upstream:origin/testing-links track:
refname:refs/heads/try-browser-flow branchname:try-browser-flow sha:fde034a18a3624f18934d6f80af633e95524394b head:Y worktree:Y symref:N upstream:origin/try-browser-flow track:
refname:refs/heads/try-browser-flow-2 branchname:try-browser-flow-2 sha:827469cf1dcb8f354655eb18c0cbc6d94c391a91 head:N worktree:N symref:N upstream:origin/try-browser-flow-2 track:
refname:refs/remotes/origin/HEAD branchname:origin/HEAD sha:973f35dde3d95f51ad20548df246d09b390a2983 head:N worktree:N symref:Y upstream: track:
refname:refs/remotes/origin/jw/a branchname:origin/jw/a sha:a1bc3549927b11ac52cfadf7914e0314d77516cf head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/b branchname:origin/jw/b sha:a1bc3549927b11ac52cfadf7914e0314d77516cf head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/docs-link-fix branchname:origin/jw/docs-link-fix sha:fc41984a6e40e4f34d01f2f04f43adc3c27da2cd head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/propose-with-lineage branchname:origin/jw/propose-with-lineage sha:6cc58fa559dcb4a527d37e6b907fe6f313fb0445 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/propose-with-lineage-in-body branchname:origin/jw/propose-with-lineage-in-body sha:06adcbb140ea92faf63174d1093d27ef6ca3bcfc head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/jw/prs-cmd branchname:origin/jw/prs-cmd sha:009747e72c445cc639d6f257233a58876e353e51 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/main branchname:origin/main sha:973f35dde3d95f51ad20548df246d09b390a2983 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/testing-links branchname:origin/testing-links sha:6958a60545161e2d2a45f7ff030049db20189ee4 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/try-browser-flow branchname:origin/try-browser-flow sha:fde034a18a3624f18934d6f80af633e95524394b head:N worktree:N symref:N upstream: track:
refname:refs/remotes/origin/try-browser-flow-2 branchname:origin/try-browser-flow-2 sha:827469cf1dcb8f354655eb18c0cbc6d94c391a91 head:N worktree:N symref:N upstream: track:
refname:refs/remotes/upstream/main branchname:upstream/main sha:973f35dde3d95f51ad20548df246d09b390a2983 head:N worktree:N symref:N upstream: track:

(verbose) git config -lz --global
user.name
Jeff W

user.email
233818084+j2fw@users.noreply.github.com

url.git@github.com:.insteadof
https://github.com/


(verbose) git config -lz --local
core.repositoryformatversion
0

core.filemode
true

core.bare
false

core.logallrefupdates
true

core.ignorecase
true

core.precomposeunicode
true

remote.origin.url
git@github.com:j2fw/git-town.git

remote.origin.fetch
+refs/heads/*:refs/remotes/origin/*

remote.origin.gh-resolved
base

branch.main.remote
origin

branch.main.merge
refs/heads/main

branch.main.vscode-merge-base
origin/main

git-town.github-connector
gh

git-town.main-branch
main

git-town.proposals-show-lineage
true

git-town-branch.jw/prs-cmd.parent
jw/b

branch.jw/prs-cmd.remote
origin

branch.jw/prs-cmd.merge
refs/heads/jw/prs-cmd

branch.jw/prs-cmd.vscode-merge-base
origin/main

git-town-branch.jw/a.parent
main

branch.jw/a.remote
origin

branch.jw/a.merge
refs/heads/jw/a

git-town-branch.jw/b.parent
jw/a

branch.jw/b.remote
origin

branch.jw/b.merge
refs/heads/jw/b

git-town-branch.jw/propose-with-lineage.parent
main

branch.jw/propose-with-lineage.remote
origin

branch.jw/propose-with-lineage.merge
refs/heads/jw/propose-with-lineage

remote.upstream.url
git@github.com:git-town/git-town.git

remote.upstream.fetch
+refs/heads/*:refs/remotes/upstream/*

branch.jw/add-lineage-to-proposed-body.remote
origin

branch.jw/add-lineage-to-proposed-body.merge
refs/heads/jw/add-lineage-to-proposed-body

git-town-branch.jw/propose-with-lineage-in-body.parent
main

branch.jw/propose-with-lineage-in-body.remote
origin

branch.jw/propose-with-lineage-in-body.merge
refs/heads/jw/propose-with-lineage-in-body

git-town-branch.jw/docs-link-fix.parent
main

branch.jw/docs-link-fix.remote
origin

branch.jw/docs-link-fix.merge
refs/heads/jw/docs-link-fix

branch.testing-links.remote
origin

branch.testing-links.merge
refs/heads/testing-links

git-town-branch.try-browser-flow.parent
jw/propose-with-lineage-in-body

branch.try-browser-flow.remote
origin

branch.try-browser-flow.merge
refs/heads/try-browser-flow

git-town-branch.try-browser-flow-2.parent
try-browser-flow

branch.try-browser-flow-2.remote
origin

branch.try-browser-flow-2.merge
refs/heads/try-browser-flow-2


(verbose) git stash list
stash@{0}: On jw/docs-link-fix: Git Town WIP
stash@{1}: WIP on jw/prs-cmd: 009747e72 Merge remote-tracking branch 'origin/jw/prs-cmd' into jw/prs-cmd

(verbose) git status -z --ignore-submodules

(verbose) git rev-parse --verify -q MERGE_HEAD

(verbose) git rev-parse --absolute-git-dir
/Users/jeff.wang/code/github/git-town/.git

Ran 38 shell commands.
Error: exit status 1

To continue after having resolved conflicts, run "git town continue".
To go back to where you started, run "git town undo".
```

</details>

### Additional information

_No response_

Discussion:
@kevgo: Thanks for bringing this up! Currently, all forge connectors open a browser window so users can finalize their proposal on the web. Even if the proposal has already been created via the CLI, users often want to tag reviewers, add labels or milestones, or just proofread.

You've found an edge case where Git Town assumes the connector has already created the proposal, but it hasn't yet. The likely fix is that the `CreateProposal` method of the `gh` connector checks whether a proposal exists. If not, assume the user chose "Continue in browser" or aborted, and skip opening another browser window.

> I'd guess that some users' workflows are purely CLI and don't want the browser open anyhow, so maybe browser viewing (or not viewing) can be put behind another config option.

Good point. So far, nobody has raised that concern, but if it comes up, we could add a `propose.open-browser` setting to allow disabling browser launches for the `gh` and `glab` connectors.
@kevgo: This is now available in [v22.2](https://github.com/git-town/git-town/releases/tag/v22.2.0).

Solution patch:
diff --git a/.cursor/BUGBOT.md b/.cursor/BUGBOT.md
new file mode 100644
index 000000000..f839b3852
--- /dev/null
+++ b/.cursor/BUGBOT.md
@@ -0,0 +1 @@
+Ignore all files in the "vendor" folder.
diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml
index cae110f75..645175a87 100644
--- a/.github/workflows/unit.yml
+++ b/.github/workflows/unit.yml
@@ -14,3 +14,4 @@ jobs:
         with:
           go-version-file: go.mod
       - run: make unit-race
+      - run: make unit-text-runner
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
index 9d363c86a..32bcaeab3 100644
--- a/.github/workflows/windows.yml
+++ b/.github/workflows/windows.yml
@@ -13,4 +13,4 @@ jobs:
       - uses: actions/setup-go@v5
         with:
           go-version-file: go.mod
-      - run: make smokewin
+      - run: make cukesmokewin
diff --git a/.gitignore b/.gitignore
index e3a0d0b69..2b7b0cd80 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ tools/depth*
 tools/gofumpt*
 tools/golangci_lint*
 tools/iterate_map/iterate_map*
+tools/lint_cached_connectors/lint_cached_connectors*
 tools/lint_steps/lint_steps
 tools/messages_sorted/messages_sorted
 tools/messy_output/messy_output
diff --git a/.golangci.yml b/.golangci.yml
index 354198c49..e2b927381 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -3,6 +3,7 @@ linters:
   default: all
   disable:
     - cyclop # we keep a tab on function length ourselves
+    - contextcheck # we aren't using contexts to cancel operations yet, might be interesting in the future
     - depguard # we review our dependencies in pull requests
     - dogsled # nothing we can do about this one
     - dupl # too many false positives
@@ -25,6 +26,8 @@ linters:
     - nakedret # not useful
     - nestif # we keep complexity under check with code reviews
     - nilnil # in our code returning nil, nil makes sense
+    - noctx # we aren't using contexts to cancel operations yet, might be interesting in the future
+    - noinlineerr # inline errors are okay
     - nonamedreturns # they are useful in too many situations
     - nlreturn # this forces unnecessary empty lines in function bodies
     - staticcheck # we run this separately
@@ -32,8 +35,14 @@ linters:
     - varnamelen # not really useful
     - wrapcheck # too many false positives for return values defined in internal packages
     - wsl # this linter creates too many false positives, our policy is to not have any empty lines in code blocks
+    - wsl_v5 # this linter creates too many false positives, our policy is to not have any empty lines in code blocks
+
+    - godoclint # TODO: enable
 
   settings:
+    embeddedstructfieldcheck:
+      empty-line: false
+      forbid-mutex: true
     funcorder:
       # Checks that constructors are placed after the structure declaration.
       # Enabling this is incompatible with alphavet.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index df000c138..6aaad0d6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,33 @@
 # Git Town Changelog
 
-## 22.2.0 (unreleased)
+## 22.2.0 (2025-10-31)
 
 #### New Features
 
-- the new display-types configuration setting allows configuring which branch types Git Town shows in lists of branches. This affects the [branch](https://www.git-town.com/commands/branch.html), [switch](https://www.git-town.com/commands/switch.html), [config](https://www.git-town.com/commands/config.html), [down](https://www.git-town.com/commands/down.html), [up](https://www.git-town.com/commands/up.html) commands and all internal dialogs that ask to select a branch or parent branch.
+- Git Town now supports a non-hidden config file, i.e. `git-town.toml`, in addition to the existing config file names ([#5614](https://github.com/git-town/git-town/issues/5614)).
+- The new [order](https://www.git-town.com/preferences/order.html) configuration setting allows displaying lists of branches in reverse order, for example in [switch](https://www.git-town.com/commands/switch.html), [branch](https://www.git-town.com/commands/branch.html), [up](https://www.git-town.com/commands/up.html), and [down](https://www.git-town.com/commands/down.html), [config](https://www.git-town.com/commands/config.html). This is useful if you prefix your branches with the creation date like `YYYYMM` and want to see the newest branches on top ([#5615](https://github.com/git-town/git-town/issues/5615)).
+- The new [display-types](https://www.git-town.com/preferences/display-types.html) configuration setting allows configuring which branch types Git Town shows in lists of branches. This affects the [branch](https://www.git-town.com/commands/branch.html), [switch](https://www.git-town.com/commands/switch.html), [config](https://www.git-town.com/commands/config.html), [down](https://www.git-town.com/commands/down.html), and [up](https://www.git-town.com/commands/up.html) commands and all internal dialogs that ask to select a branch or parent branch ([#5659](https://github.com/git-town/git-town/issues/5659)).
+- switch: you can now toggle to show all branches on and off using the `a` hotkey ([#5624](https://github.com/git-town/git-town/issues/5624)).
+- setup assistant: now runs in the presence of invalid configuration data and overwrites it ([#5598](https://github.com/git-town/git-town/issues/5598)).
+- Git Town now uses the Git user name also from the environment variables `GIT_AUTHOR_EMAIL`, `GIT_COMMITTER_EMAIL`, `GIT_AUTHOR_NAME`, and `GIT_COMMITTER_NAME` if they aren't set in Git metadata `user.email` and `user.name` ([#5668](https://github.com/git-town/git-town/issues/5668)).
+
+#### Bug Fixes
+
+- compress: works if a folder has the same name as a branch ([#5594](https://github.com/git-town/git-town/issues/5594)).
+- ship: works if a folder has the same name as a branch ([#5657](https://github.com/git-town/git-town/issues/5657)).
+- set-parent: only changes commits of feature or prototype branches ([#5605](https://github.com/git-town/git-town/issues/5605)).
+- propose: correctly passes on a `--body` tag without content ([#5496](https://github.com/git-town/git-town/issues/5496)).
+- propose: the `gh` connector no longer crashes if the user chooses `Continue in browser` ([#5681](https://github.com/git-town/git-town/issues/5681)).
+- sync: pruning a freshly created branch no longer crashes ([#5660](https://github.com/git-town/git-town/issues/5660)).
+- hack: now always sets the correct branch type ([#5609](https://github.com/git-town/git-town/issues/5609)).
+- hack: `--beam` from a branch without parent now asks for the branch parent ([#5121](https://github.com/git-town/git-town/issues/5121)).
+- init: no longer crashes if if there are no remotes ([#5608](https://github.com/git-town/git-town/issues/5608)).
+- switch: can now combine `--all` and `--display-types` ([#5648](https://github.com/git-town/git-town/issues/5648)).
+- dead links on the "Contributing" tab on GitHub are fixed ([#5679](https://github.com/git-town/git-town/pull/5679)).
+
+#### Contributors
+
+Shoutout to @AmitJoki, @GamerGirlandCo, @Varedis, @caccavale, @derekspelledcorrectly, @j2fw, @james-harlyy, @kevgo, @kevinmichaelchen, @mrmarufpro, @nebbles, @ruudk, @stephenwade, @thattomperson, @tranhl, @tugrulates, @yaadata for contributing code, feedback, ideas, and bug fixes to 58 shipped pull requests and 24 resolved issues!
 
 ## 22.1.0 (2025-10-13)
 
diff --git a/CLAUDE.md b/CLAUDE.md
index 04f1c5090..43c994c2d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,101 +1 @@
-# Git Town
-
-Git Town is a CLI tool written in Go that provides additional Git commands for
-automating branch management, synchronization, and cleanup.
-
-## Development Guidelines
-
-- you can change any file in the current folder and its subfolders
-- never change files outside the Git repository
-- never create new Git branches
-- never make Git commits
-- I will review the changes you make and then commit them on my own.
-
-## Automated testing
-
-To run all unit tests for the project, use this command:
-
-```bash
-make unit
-```
-
-To run a single Cucumber test, also called end-to-end test, use this command:
-
-```bash
-make install
-go test -- <test path>
-```
-
-### Linters
-
-Please execute the linters after making changes to verify the correctness of
-your changes. To run them, use the following command:
-
-```bash
-make lint
-```
-
-### End-to-End Tests
-
-End-to-end tests are defined in the "features" directory. They take a while to
-execute, so only run them to verify that everything still works after you are
-done making changes. To run all end-to-end tests:
-
-```bash
-make cuke
-```
-
-## Key Architectural Components
-
-#### VM-Based Execution Framework
-
-Git Town uses an interpreter that executes self-modifying code consisting of
-Git-related opcodes:
-
-- Commands inspect Git repo state and generate a program of opcodes
-- The interpreter (`internal/vm/`) executes these programs
-- Programs can modify themselves at runtime based on repo state
-- Runstate is persisted to disk for resume capability
-
-#### Subsystem Organization
-
-The codebase is organized into orthogonal subsystems with `*domain` packages:
-
-- `internal/config/` - Configuration management
-- `internal/git/` - Git operations and domain types
-- `internal/forge/` - Integration with GitHub, GitLab, etc.
-- `internal/vm/` - Virtual machine and opcodes
-- `internal/cli/` - Command-line interface components
-- `internal/cmd/` - Source code for the subcommands of the CLI app
-- `internal/gohacks/` - Helper functions that make programming in Go more
-  ergonomic
-- `internal/messages` - strings shown in the UI
-- `internal/setup` - the setup assistant, a visual workflow letting the user
-  configure the application
-- `internal/state` - manages persistent state between Git Town invocations
-- `internal/subshell` - implements calling CLI applications in a subshell
-- `internal/undo` - code used for the undo functionality, it calculates the
-  difference between snapshots of the Git repository and determines the Git
-  commands to move the repository from one snapshot to another
-
-#### Important Directories
-
-- `internal/` - Core application code
-- `pkg/` - Public packages
-- `features/` - End-to-end tests (Cucumber/Godog)
-- `tools/` - Custom linters and development tools
-- `website/` - Documentation website (mdBook)
-
-### Code Style Notes
-
-- Use descriptive naming over brevity
-- Method receivers use `self` instead of short abbreviations
-- Extensive use of domain-specific types in `*domain` packages
-
-## Common Development Tasks
-
-### Debugging End-to-End Tests
-
-- Add the `@this` tag to a specific scenario and then run `make cukethis` to
-  execute only the tagged scenario
-- Add the `@debug` tag to a scenario to see CLI output and Git commands
+@AGENTS.md
diff --git a/Makefile b/Makefile
index 9a3c64e47..35bfccd93 100644
--- a/Makefile
+++ b/Makefile
@@ -1,20 +1,31 @@
-RTA_VERSION = 0.24.0  # run-that-app version to use
+RTA_VERSION = 0.24.2  # run-that-app version to use
 
 # internal data and state
 .DEFAULT_GOAL := help
-RELEASE_VERSION := "22.1.0"
+RELEASE_VERSION := "22.2.0"
 GO_TEST_ARGS = LANG=C GOGC=off BROWSER=
 
 contest: tools/rta@${RTA_VERSION}  # run the Contest server
 	@tools/rta contest
 
-cuke: install  # runs all end-to-end tests in a way that looks nice during development
+cuke: install  # runs all end-to-end tests with nice output
 	@env $(GO_TEST_ARGS) messyoutput=0 go test -v
 	@env $(GO_TEST_ARGS) messyoutput=1 go test -v
 
-cukeall: install  # runs all end-to-end tests
+cukeall: install  # runs all end-to-end tests on CI
 	@env $(GO_TEST_ARGS) go test -v
 
+cuke-prof: install  # creates a flamegraph for the end-to-end tests
+	env $(GO_TEST_ARGS) go test . -v -cpuprofile=godog.out
+	@rm git-town.test
+	@echo Please open https://www.speedscope.app and load the file godog.out
+
+cukesmoke: install  # run the smoke E2E tests
+	@env $(GO_TEST_ARGS) smoke=1 go test . -v -count=1
+
+cukesmokewin: install  # runs the smoke E2E tests on Windows
+	@env smoke=1 go test . -v -count=1
+
 cukethis: install  # runs the end-to-end tests that have a @this tag
 	@env $(GO_TEST_ARGS) cukethis=1 go test . -v -count=1
 
@@ -22,10 +33,13 @@ cukethiswin:  # runs the end-to-end tests that have a @this tag on Windows
 	go install -ldflags "-X github.com/git-town/git-town/v22/internal/cmd.version=-dev -X github.com/git-town/git-town/v22/internal/cmd.buildDate=1/2/3"
 	powershell -Command '$$env:cukethis=1 ; go test . -v -count=1'
 
-cuke-prof: install  # creates a flamegraph for the end-to-end tests
-	env $(GO_TEST_ARGS) go test . -v -cpuprofile=godog.out
-	@rm git-town.test
-	@echo Please open https://www.speedscope.app and load the file godog.out
+cuke-update: install  # updates the E2E tests based on the actual behavior of Git Town
+	@env $(GO_TEST_ARGS) cukeupdate=1 go test . -v -count=1
+	make --no-print-directory fix
+
+cuke-update-this: install  # updates the E2E tests that have a @this tag
+	@env $(GO_TEST_ARGS) cukeupdate=1 cukethis=1 go test . -v -count=1
+	make --no-print-directory fix
 
 cukeverbose: install  # run all tests in "verbose.feature" files
 	@env $(GO_TEST_ARGS) verbose=1 go test . -v -count=1
@@ -33,11 +47,14 @@ cukeverbose: install  # run all tests in "verbose.feature" files
 cukewin: install  # runs all end-to-end tests on Windows
 	go test . -v -count=1
 
-dependencies: tools/rta@${RTA_VERSION}  # prints the dependencies between the internal Go packages as a tree
+dependencies: tools/rta@${RTA_VERSION}  # prints the dependencies between the internal Go packages
 	@tools/rta depth . | grep git-town
 
-docs: install tools/node_modules  # tests the documentation
-	@tools/rta node tools/node_modules/.bin/text-runner --offline
+docs: install node_modules  # tests the documentation
+	@tools/rta node node_modules/.bin/text-runner --offline
+
+export-config-schema:  # exports the JSON-Schema for the configuration file
+	@go run tools/generate_jsonschema.go > docs/git-town.schema.json
 
 fix: tools/rta@${RTA_VERSION}  # runs all linters and auto-fixes
 	make --no-print-directory fix-optioncompare-in-tests
@@ -47,7 +64,7 @@ fix: tools/rta@${RTA_VERSION}  # runs all linters and auto-fixes
 	tools/rta gofumpt -l -w .
 	tools/rta dprint fmt
 	tools/rta dprint fmt --config dprint-changelog.json
-	tools/rta shfmt -f . | grep -v tools/node_modules | grep -v '^vendor/' | xargs tools/rta shfmt --write
+	tools/rta shfmt -f . | grep -v node_modules | grep -v '^vendor/' | xargs tools/rta shfmt --write
 	tools/rta ghokin fmt replace features/
 	tools/generate_opcodes_all.sh
 	tools/rta cucumber-sort format
@@ -58,10 +75,11 @@ help:  # prints all available targets
 install:  # builds for the current platform
 	@go install -ldflags="-s -w"
 
-lint: tools/node_modules tools/rta@${RTA_VERSION}  # lints the main codebase concurrently
+lint: node_modules tools/rta@${RTA_VERSION}  # lints the main codebase concurrently
 	make --no-print-directory lint-smoke
 	make --no-print-directory alphavet
 	make --no-print-directory deadcode
+	make --no-print-directory lint-cached-connectors
 	make --no-print-directory lint-iterate-map
 	make --no-print-directory lint-messages-sorted
 	make --no-print-directory lint-messy-output
@@ -75,10 +93,10 @@ lint: tools/node_modules tools/rta@${RTA_VERSION}  # lints the main codebase con
 	tools/rta actionlint
 	tools/rta --from-source staticcheck ./...
 	tools/ensure_no_files_with_dashes.sh
-	tools/rta shfmt -f . | grep -v 'tools/node_modules' | grep -v '^vendor/' | xargs tools/rta --optional shellcheck
+	tools/rta shfmt -f . | grep -v 'node_modules' | grep -v '^vendor/' | xargs tools/rta --optional shellcheck
 	tools/rta golangci-lint cache clean
 	tools/rta golangci-lint run
-	tools/rta node tools/node_modules/.bin/gherkin-lint
+	tools/rta node node_modules/.bin/gherkin-lint
 	tools/rta cucumber-sort check
 
 lint-all: lint tools/rta@${RTA_VERSION}  # runs all linters
@@ -88,6 +106,8 @@ lint-all: lint tools/rta@${RTA_VERSION}  # runs all linters
 	@(cd tools/format_self && make test)
 	@echo lint tools/format_unittests
 	@(cd tools/format_unittests && make test)
+	@echo lint tools/lint_cached_connectors
+	@(cd tools/lint_cached_connectors && make test)
 	@echo lint tools/lint_steps
 	@(cd tools/lint_steps && make test)
 	@echo lint tools/messages_sorted
@@ -119,6 +139,9 @@ keep-sorted: tools/rta@${RTA_VERSION}
 	tools/rta --install ripgrep
 	tools/rta keep-sorted $(shell tools/rta ripgrep -l 'keep-sorted end' ./ --glob '!Makefile')
 
+lint-cached-connectors:
+	@(cd tools/lint_cached_connectors && go build) && ./tools/lint_cached_connectors/lint_cached_connectors
+
 lint-iterate-map:
 	@(cd tools/iterate_map && go build) && ./tools/iterate_map/iterate_map
 
@@ -134,7 +157,7 @@ lint-print-config:
 lint-optioncompare:
 	@(cd tools/optioncompare && go build) && ./tools/optioncompare/optioncompare github.com/git-town/git-town/v22/...
 
-lint-smoke: tools/rta@${RTA_VERSION}  # runs only the essential linters to get quick feedback after refactoring
+lint-smoke: tools/rta@${RTA_VERSION}  # runs only the essential linters
 	@tools/rta exhaustruct -test=false "-i=github.com/git-town/git-town.*" github.com/git-town/git-town/...
 # @tools/rta ireturn --reject="github.com/git-town/git-town/v22/pkg/prelude.Option" github.com/git-town/git-town/...
 
@@ -147,15 +170,9 @@ lint-tests-sorted:
 lint-use-equal:
 	@(cd tools/use_equal && go build) && ./tools/use_equal/use_equal
 
-smoke: install  # run the smoke tests
-	@env $(GO_TEST_ARGS) smoke=1 go test . -v -count=1
-
-smokewin: install  # runs the Windows smoke tests
-	@env smoke=1 go test . -v -count=1
-
 stats: tools/rta@${RTA_VERSION}  # shows code statistics
 	@find . -type f \
-		| grep -v './tools/node_modules' \
+		| grep -v './node_modules' \
 		| grep -v '\./vendor/' \
 		| grep -v '\./.git/' \
 		| grep -v './website/book' \
@@ -183,6 +200,7 @@ UNIT_TEST_DIRS = \
 	./pkg/... \
 	./tools/format_self/... \
 	./tools/format_unittests/... \
+	./tools/lint_cached_connectors/... \
 	./tools/lint_steps/... \
 	./tools/messages_sorted/... \
 	./tools/messy_output/... \
@@ -195,6 +213,10 @@ unit: install  # runs only the unit tests for changed code
 
 unit-all: install  # runs all the unit tests
 	env GOGC=off go test -count=1 -shuffle=on -timeout=60s $(UNIT_TEST_DIRS)
+	make --no-print-directory unit-text-runner
+
+unit-text-runner: tools/rta@${RTA_VERSION} node_modules
+	@tools/rta npm run unit
 
 unit-race: install  # runs all the unit tests with race detector
 	env GOGC=off go test -count=1 -timeout 60s -race $(UNIT_TEST_DIRS)
@@ -205,9 +227,9 @@ update: tools/rta@${RTA_VERSION}  # updates all dependencies
 	(cd tools/optioncompare && go get -u ./...)
 	go mod tidy
 	go work vendor
-	rm -rf tools/node_modules package-lock.json
-	cd tools && ./rta npx -y npm-check-updates -u
-	cd tools && ./rta npm install
+	rm -rf node_modules package-lock.json
+	tools/rta npx -y npm-check-updates -u
+	tools/rta npm install
 	tools/rta --update
 	tools/rta dprint config update
 	tools/rta dprint config update --config dprint-changelog.json
@@ -240,6 +262,7 @@ deadcode: tools/rta@${RTA_VERSION}
 		| grep -v pkg/prelude/ptr.go \
 		| grep -v Paniced \
 		| grep -v Set.Add \
+		| grep -v Set.Contains \
 		| grep -v UseCustomMessageOr \
 		| grep -v UseDefaultMessage \
 		|| true
@@ -251,8 +274,8 @@ tools/rta@${RTA_VERSION}:
 	@mv tools/rta tools/rta@${RTA_VERSION}
 	@ln -s rta@${RTA_VERSION} tools/rta
 
-tools/node_modules: tools/package-lock.json tools/rta@${RTA_VERSION}
-	test -f tools/node_modules/.yarn-integrity && rm -rf tools/node_modules || true  # remove node_modules if installed with Yarn (TODO: remove after 2025-01-26)
+node_modules: package-lock.json tools/rta@${RTA_VERSION}
 	@echo "Installing Node based tools"
-	cd tools && ./rta npm ci
-	@touch tools/node_modules  # update timestamp of the node_modules folder so that Make doesn't re-install it on every command
+	tools/rta npm ci
+	@touch package-lock.json  # update timestamp so that Make doesn't re-install it on every command
+	@touch node_modules  # update timestamp so that Make doesn't re-install it on every command
diff --git a/agents.md b/agents.md
new file mode 100644
index 000000000..1fc2a8d08
--- /dev/null
+++ b/agents.md
@@ -0,0 +1,103 @@
+# Git Town
+
+Git Town is a CLI tool written in Go that provides additional Git commands for
+automating branch management, synchronization, and cleanup.
+
+## Development Guidelines
+
+- you can change any file in the current folder and its subfolders
+- never change files outside the Git repository
+- never create new Git branches
+- never make Git commits
+- I will review the changes you make and then commit them on my own.
+
+## Automated testing
+
+To run all unit tests for the project, use this command:
+
+```bash
+make unit
+```
+
+To run a single Cucumber test, also called end-to-end test, use this command:
+
+```bash
+make install
+go test -- <test path>
+```
+
+### Linters
+
+Please execute the linters after making changes to verify the correctness of
+your changes. To run them, use the following command:
+
+```bash
+make lint
+```
+
+### End-to-End Tests
+
+End-to-end tests are defined in the "features" directory. They take a while to
+execute, so only run them to verify that everything still works after you are
+done making changes. To run all end-to-end tests:
+
+```bash
+make cuke
+```
+
+## Key Architectural Components
+
+#### VM-Based Execution Framework
+
+Git Town uses an interpreter that executes self-modifying code consisting of
+Git-related opcodes:
+
+- Commands inspect Git repo state and generate a program of opcodes
+- The interpreter (`internal/vm/`) executes these programs
+- Programs can modify themselves at runtime based on repo state
+- Runstate is persisted to disk for resume capability
+
+#### Subsystem Organization
+
+The codebase is organized into orthogonal subsystems with `*domain` packages:
+
+- `internal/config/` - Configuration management
+- `internal/git/` - Git operations and domain types
+- `internal/forge/` - Integration with GitHub, GitLab, etc.
+- `internal/vm/` - Virtual machine and opcodes
+- `internal/cli/` - Command-line interface components
+- `internal/cmd/` - Source code for the subcommands of the CLI app
+- `internal/gohacks/` - Helper functions that make programming in Go more
+  ergonomic
+- `internal/messages` - strings shown in the UI
+- `internal/setup` - the setup assistant, a visual workflow letting the user
+  configure the application
+- `internal/state` - manages persistent state between Git Town invocations
+- `internal/subshell` - implements calling CLI applications in a subshell
+- `internal/undo` - code used for the undo functionality, it calculates the
+  difference between snapshots of the Git repository and determines the Git
+  commands to move the repository from one snapshot to another
+
+#### Important Directories
+
+- `internal/` - Core application code
+- `pkg/` - Public packages
+- `features/` - End-to-end tests (Cucumber/Godog)
+- `tools/` - Custom linters and development tools
+- `website/` - Documentation website (mdBook)
+
+### Code Style
+
+- Write idiomatic Go except for the conditions listed below
+- Use descriptive naming over brevity
+- Method receivers use `self` instead of short abbreviations
+- Use domain-specific types defined in the respective `*domain` packages if
+  applicable over the built-in basic type.
+
+## Common Development Tasks
+
+### Debugging End-to-End Tests
+
+- Add the `@this` tag to a specific scenario and then run `make cukethis` to
+  execute only the tagged scenario
+- Add the `@debug` tag to a Cucumber scenario to see CLI output and Git commands
diff --git a/contest.json b/contest.json
index 295f153d0..cfed1e610 100644
--- a/contest.json
+++ b/contest.json
@@ -27,7 +27,7 @@
     {
       "type": "test-file",
       "files": "**/*.md",
-      "run": "bash -c 'cd tools && ./rta npm exec text-runner ../{{file}}'"
+      "run": "make docs"
     },
 
     {
diff --git a/cucumber-sort.json b/cucumber-sort.json
index f257b10fd..a04895658 100644
--- a/cucumber-sort.json
+++ b/cucumber-sort.json
@@ -3,10 +3,14 @@
     "features/**/*.feature"
   ],
   "exclude": [
+    "features/append/branch_prefix/config_file.feature",
     "features/detach/merge_conflict/dependent_changes.feature",
     "features/end_to_end/ship_prototype_branch_and_sync.feature",
+    "features/hack/branch_prefix/config_file.feature",
     "features/observe/give_existing_branch/multiple_branches.feature",
     "features/prepend/beam/stack/dependent_changes.feature",
+    "features/prepend/branch_prefix/config_file.feature",
+    "features/rename/branch_prefix/config_file.feature",
     "features/set_parent/hoist/rebase/dependent_changes.feature",
     "features/ship/always_merge/current_branch/additional_commits/before_undo/online.feature",
     "features/ship/always_merge/current_branch/ship_delete_tracking_branch/disabled.feature",
@@ -61,6 +65,7 @@
     "features/walk/failing_script/failing_script.feature",
     "features/walk/stack/no_args.feature",
     "features/walk/undo/manual.feature",
+    "features/walk/worktree/no_args.feature",
     "vendor/**/*.feature"
   ],
   "record": true,
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 8a26c1cdf..793bc3c6e 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -100,6 +100,12 @@ go test src/cmd/root_test.go -v -run TestIsAcceptableGitVersion
 
 ## End-to-end tests
 
+Git Town uses an extensive end-to-end test suite that models a wide variety of
+situations in which Git Town can be used. This helps ensure Git Town behaves
+correct in these situations and debug Git Town when it doesn't.
+
+### Run all E2E tests
+
 Run the end-to-end tests during development (nicer output):
 
 <a type="make/command" dir="..">
@@ -120,6 +126,8 @@ make cukeall
 
 </a>
 
+### Run a subset of E2E tests
+
 Run all tests in the `features/append` folder or file:
 
 ```
@@ -146,17 +154,28 @@ Run all files named `es.feature`:
 find . -name es.feature | xargs go test --
 ```
 
+### Stubbing out forges in E2E tests
+
 Certain tests require that the Git remote points to an actual GitHub, Gitea,
-GitLab, Bitbucket, or Forgejo address. This causes `git push` operations in this
-test to also go to GitHub. To prevent this, set an environment variable
-`GIT_TOWN_REMOTE` with the desired value of the
-[development remote](../website/src/preferences/dev-remote.md), and Git Town
-will use that value instead of what is configured in the repo.
+GitLab, Bitbucket, or Forgejo address. An example is using this value to
+determine the forge type. Using such a setting would cause `git push` operations
+in this test to also go to GitHub. We want to avoid this, because it's slow,
+brittle, and stresses the external forges.
+
+To prevent this, keep the actual setting pointed to the local remote. Set an
+environment variable `GIT_TOWN_REMOTE` with the desired value of the
+[development remote](../website/src/preferences/dev-remote.md). Git Town will
+use that value for internal calculations, instead of what is configured in the
+repo.
+
+### Running on Windows
 
 If Cucumber tests produce garbled output on Windows, try running them inside Git
 Bash. See [this issue](https://github.com/cucumber/godog/issues/129) for
 details.
 
+### Pausing end-to-end tests
+
 To pause an end-to-end test so that you have time to inspect the status of the
 Git repository created by the test, add the step `And inspect the repo`. The
 test runner will pause and print the path of the test workspace. You can `cd`
@@ -164,7 +183,14 @@ into that path in a separate terminal window and inspect the repos there. The
 developer's repo is in the `repo` folder. The remote repo (that would normally
 be on GitHub) is in the `origin` folder.
 
-To see all commit SHAs of the repo, add the `And inspect the commits` step.
+### Finding the right SHAs
+
+Some end-to-end tests contain placeholders for commit SHAs. To find the correct
+placeholder, add the step `And inspect the commits`. This displays all SHAs in
+the repo, together with the placeholder to be used.
+
+Possible placeholders are defined in
+[here](../internal/test/handlebars/expand.go).
 
 ## Inspecting variables
 
@@ -188,7 +214,7 @@ spew.Dump(variable)
 pretty.LDiff(t, var1, var2)
 ```
 
-## Debug end-to-end tests
+### Debug an E2E test on the CLI
 
 To see the CLI output of the shell commands in a Cucumber test, as well as the
 Git commands that the Git Town test suite runs under the hood, add a tag
@@ -213,6 +239,8 @@ To manually inspect the local and remote Git repositories used in an end-to-end
 test, insert the step `And inspect the repo`. This step will make Cucumber print
 the path of the workspace and wait until you hit ENTER.
 
+### Debug an E2E test in VSCode
+
 Debug a Godog Cucumber feature in [VSCode](https://code.visualstudio.com):
 
 - open `main_test.go`
@@ -220,10 +248,9 @@ Debug a Godog Cucumber feature in [VSCode](https://code.visualstudio.com):
 - set a breakpoint in your test code
 - run the `debug a test` configuration in the debugger
 
-## Triangulate a hanging end-to-end test
+### Locate a hanging end-to-end test
 
-End-to-end tests sometimes hang due to Git Town waiting for input that the test
-doesn't enter. To find the hanging test you can do a binary search by executing
+End-to-end tests sometimes hang. To find the hanging test, you can execute
 subsets of tests using `go test -- features/<path>` where path is either a
 subfolder or file inside the "features" folder.
 
@@ -231,7 +258,32 @@ Alternatively, open `main_test.go`, change `Format` to `pretty` and
 `Concurrency` to 1, and run the entire test suite. The detailed output will give
 you hints at which test fails.
 
-## Configure the Cucumber IDE extension
+### Automatically update E2E test files
+
+Our E2E tests act as
+[golden files](https://en.wikipedia.org/wiki/Characterization_test): they embody
+the expected output that all future test runs must match. When we intentionally
+change Git Town's behavior, we also need to update possibly hundreds of E2E
+tests.
+
+To streamline this, run:
+
+<a type="make/command" dir="..">
+
+```bash
+make cuke-update
+```
+
+</a>
+
+When running in this mode, and an E2E test fails, the test runner updates the
+`.feature` file, rather than failing the test. You must then inspect the test
+files to verify whether the updated tests are correct.
+
+If needed, run <code type="make/command" dir="..">make fix</code> to format the
+recorded Gherkin code.
+
+### Configure the Cucumber IDE extension
 
 To configure the official
 [Cucumber extension](https://marketplace.visualstudio.com/items/?itemName=CucumberOpen.cucumber-official)
diff --git a/docs/RELEASE.md b/docs/RELEASE.md
index a1db3becd..686a2ec8b 100644
--- a/docs/RELEASE.md
+++ b/docs/RELEASE.md
@@ -10,8 +10,8 @@ This guide is for maintainers who make releases of Git Town.
     the README file
   - verify that the website content reflects all the changes made
   - search-and-replace the old version with the new version
-    - triple-digits: `22.1.0`
-    - double-digits: `22.1`
+    - triple-digits: `22.2.0`
+    - double-digits: `22.2`
     - its best to do this manually so that you don't change unrelated version
       numbers in CHANGELOG.md, lockfiles, and data files
   - if bumping the major version:
@@ -24,7 +24,7 @@ This guide is for maintainers who make releases of Git Town.
 - in a new branch:
   - update the changelog with links to the website
   - ship the branch
-- `git sync --all && git checkout main && git tag v22.1.0 && git push --tags`
+- `git sync --all && git checkout main && git tag v22.2.0 && git push --tags`
 - wait for CI to finish the release:
   https://github.com/git-town/git-town/actions
 - edit the new unpublished release:
diff --git a/docs/git-town.schema.json b/docs/git-town.schema.json
new file mode 100644
index 000000000..15be87586
--- /dev/null
+++ b/docs/git-town.schema.json
@@ -0,0 +1,208 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://www.git-town.com/git-town.toml",
+  "$ref": "#/$defs/Data",
+  "$defs": {
+    "Branches": {
+      "properties": {
+        "ContributionRegex": {
+          "type": "string"
+        },
+        "DefaultType": {
+          "type": "string"
+        },
+        "DisplayTypes": {
+          "type": "string"
+        },
+        "FeatureRegex": {
+          "type": "string"
+        },
+        "Main": {
+          "type": "string"
+        },
+        "ObservedRegex": {
+          "type": "string"
+        },
+        "Order": {
+          "type": "string"
+        },
+        "PerennialRegex": {
+          "type": "string"
+        },
+        "Perennials": {
+          "items": {
+            "type": "string"
+          },
+          "type": "array"
+        },
+        "UnknownType": {
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "type": "object"
+    },
+    "Create": {
+      "properties": {
+        "NewBranchType": {
+          "type": "string"
+        },
+        "PushNewbranches": {
+          "type": "boolean"
+        },
+        "ShareNewBranches": {
+          "type": "string"
+        },
+        "Stash": {
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false,
+      "type": "object"
+    },
+    "Data": {
+      "properties": {
+        "Branches": {
+          "$ref": "#/$defs/Branches"
+        },
+        "Create": {
+          "$ref": "#/$defs/Create"
+        },
+        "CreatePrototypeBranches": {
+          "type": "boolean"
+        },
+        "Hosting": {
+          "$ref": "#/$defs/Hosting"
+        },
+        "Propose": {
+          "$ref": "#/$defs/Propose"
+        },
+        "PushHook": {
+          "type": "boolean"
+        },
+        "PushNewbranches": {
+          "type": "boolean"
+        },
+        "Ship": {
+          "$ref": "#/$defs/Ship"
+        },
+        "ShipDeleteTrackingBranch": {
+          "type": "boolean"
+        },
+        "ShipStrategy": {
+          "type": "string"
+        },
+        "Sync": {
+          "$ref": "#/$defs/Sync"
+        },
+        "SyncStrategy": {
+          "$ref": "#/$defs/SyncStrategy"
+        },
+        "SyncTags": {
+          "type": "boolean"
+        },
+        "SyncUpstream": {
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false,
+      "type": "object"
+    },
+    "Hosting": {
+      "properties": {
+        "DevRemote": {
+          "type": "string"
+        },
+        "ForgeType": {
+          "type": "string"
+        },
+        "GitHubConnectorType": {
+          "type": "string"
+        },
+        "GitLabConnectorType": {
+          "type": "string"
+        },
+        "OriginHostname": {
+          "type": "string"
+        },
+        "Platform": {
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "type": "object"
+    },
+    "Propose": {
+      "properties": {
+        "Lineage": {
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "type": "object"
+    },
+    "Ship": {
+      "properties": {
+        "DeleteTrackingBranch": {
+          "type": "boolean"
+        },
+        "Strategy": {
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "type": "object"
+    },
+    "Sync": {
+      "properties": {
+        "AutoResolve": {
+          "type": "boolean"
+        },
+        "AutoSync": {
+          "type": "boolean"
+        },
+        "Detached": {
+          "type": "boolean"
+        },
+        "FeatureStrategy": {
+          "type": "string"
+        },
+        "PerennialStrategy": {
+          "type": "string"
+        },
+        "PrototypeStrategy": {
+          "type": "string"
+        },
+        "PushBranches": {
+          "type": "boolean"
+        },
+        "PushHook": {
+          "type": "boolean"
+        },
+        "Tags": {
+          "type": "boolean"
+        },
+        "Upstream": {
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false,
+      "type": "object"
+    },
+    "SyncStrategy": {
+      "properties": {
+        "FeatureBranches": {
+          "type": "string"
+        },
+        "PerennialBranches": {
+          "type": "string"
+        },
+        "PrototypeBranches": {
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "type": "object"
+    }
+  }
+}
diff --git a/dprint-changelog.json b/dprint-changelog.json
index df269fcbd..34667fd65 100644
--- a/dprint-changelog.json
+++ b/dprint-changelog.json
@@ -6,6 +6,6 @@
     "textWrap": "never"
   },
   "plugins": [
-    "https://plugins.dprint.dev/markdown-0.18.0.wasm"
+    "https://plugins.dprint.dev/markdown-0.20.0.wasm"
   ]
 }
diff --git a/dprint.json b/dprint.json
index 52e99971e..596b5607f 100644
--- a/dprint.json
+++ b/dprint.json
@@ -3,13 +3,16 @@
   "markdown": {
     "textWrap": "always"
   },
+  "typescript": {
+    "semiColons": "asi"
+  },
   "includes": ["**/*.{js,ts,json,md}"],
   "excludes": ["CHANGELOG.md", "vendor/**", "tools/node_modules/**", "website/book/**"],
   "plugins": [
-    "https://plugins.dprint.dev/json-0.20.0.wasm",
+    "https://plugins.dprint.dev/json-0.21.0.wasm",
     "https://plugins.dprint.dev/markdown-0.20.0.wasm",
     "https://plugins.dprint.dev/toml-0.7.0.wasm",
     "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm",
-    "https://plugins.dprint.dev/typescript-0.95.11.wasm"
+    "https://plugins.dprint.dev/typescript-0.95.12.wasm"
   ]
 }
diff --git a/features/append/branch_prefix/config_file.feature b/features/append/branch_prefix/config_file.feature
new file mode 100644
index 000000000..ed21ad231
--- /dev/null
+++ b/features/append/branch_prefix/config_file.feature
@@ -0,0 +1,46 @@
+Feature: append with configured branch-prefix via config file
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the committed configuration file:
+      """
+      [create]
+      branch-prefix = "kg-"
+      """
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town append <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                      |
+      | feature-1 | git fetch --prune --tags     |
+      |           | git checkout -b kg-feature-2 |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        feature-1
+          kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town append feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/append/branch_prefix/env_var.feature b/features/append/branch_prefix/env_var.feature
new file mode 100644
index 000000000..271a6df96
--- /dev/null
+++ b/features/append/branch_prefix/env_var.feature
@@ -0,0 +1,43 @@
+Feature: append with configured branch-prefix via environment variable
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town append <BRANCH_NAME>" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                      |
+      | feature-1 | git fetch --prune --tags     |
+      |           | git checkout -b kg-feature-2 |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        feature-1
+          kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I run "git-town append feature-2" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/append/branch_prefix/git_metadata.feature b/features/append/branch_prefix/git_metadata.feature
new file mode 100644
index 000000000..2f80749b1
--- /dev/null
+++ b/features/append/branch_prefix/git_metadata.feature
@@ -0,0 +1,42 @@
+Feature: append with configured branch-prefix via Git metadata
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And Git setting "git-town.branch-prefix" is "kg-"
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town append <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                      |
+      | feature-1 | git fetch --prune --tags     |
+      |           | git checkout -b kg-feature-2 |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        feature-1
+          kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town append feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/append/sync_strategy/compress/compressible.feature b/features/append/sync_strategy/compress/compressible.feature
index db19bfcc0..18a0ee151 100644
--- a/features/append/sync_strategy/compress/compressible.feature
+++ b/features/append/sync_strategy/compress/compressible.feature
@@ -18,7 +18,7 @@ Feature: append a new feature branch in a clean workspace using the "compress" s
     Then Git Town runs the commands
       | BRANCH   | COMMAND                           |
       | existing | git fetch --prune --tags          |
-      |          | git reset --soft main             |
+      |          | git reset --soft main --          |
       |          | git commit -m "existing commit 1" |
       |          | git push --force-with-lease       |
       |          | git checkout -b new               |
diff --git a/features/append/sync_strategy/compress/update_on_parent.feature b/features/append/sync_strategy/compress/update_on_parent.feature
index 48a7acef0..d87daa4e5 100644
--- a/features/append/sync_strategy/compress/update_on_parent.feature
+++ b/features/append/sync_strategy/compress/update_on_parent.feature
@@ -19,7 +19,7 @@ Feature: append a new feature branch in a clean workspace using the "compress" s
       | BRANCH  | COMMAND                            |
       | feature | git fetch --prune --tags           |
       |         | git merge --no-edit --ff main      |
-      |         | git reset --soft main              |
+      |         | git reset --soft main --           |
       |         | git commit -m "already compressed" |
       |         | git push --force-with-lease        |
       |         | git checkout -b new                |
diff --git a/features/append/sync_strategy/compress/update_on_tracking.feature b/features/append/sync_strategy/compress/update_on_tracking.feature
index 6b1a2e840..f4315d721 100644
--- a/features/append/sync_strategy/compress/update_on_tracking.feature
+++ b/features/append/sync_strategy/compress/update_on_tracking.feature
@@ -19,7 +19,7 @@ Feature: append a new feature branch in a clean workspace using the "compress" s
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "already compressed"      |
       |         | git push --force-with-lease             |
       |         | git checkout -b new                     |
diff --git a/features/compress/already_compressed/one_commit.feature b/features/compress/already_compressed/one_commit.feature
index 1f32ec2d3..9cc19da9f 100644
--- a/features/compress/already_compressed/one_commit.feature
+++ b/features/compress/already_compressed/one_commit.feature
@@ -16,7 +16,7 @@ Feature: does not compress already compressed branches
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                         |
       | feature | git fetch --prune --tags                        |
-      |         | git reset --soft main                           |
+      |         | git reset --soft main --                        |
       |         | git commit -m "commit 1"                        |
       |         | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/branch_types/contributing_branch/on_child_branch.feature b/features/compress/branch_types/contributing_branch/on_child_branch.feature
index 126ed0eed..e40d635db 100644
--- a/features/compress/branch_types/contributing_branch/on_child_branch.feature
+++ b/features/compress/branch_types/contributing_branch/on_child_branch.feature
@@ -23,7 +23,7 @@ Feature: does not compress contribution branches in the stack
     Then Git Town runs the commands
       | BRANCH | COMMAND                                         |
       | child  | git fetch --prune --tags                        |
-      |        | git reset --soft contribution                   |
+      |        | git reset --soft contribution --                |
       |        | git commit -m "child 1"                         |
       |        | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/branch_types/observed_branch/on_child_branch.feature b/features/compress/branch_types/observed_branch/on_child_branch.feature
index f7d45d460..45a265ba0 100644
--- a/features/compress/branch_types/observed_branch/on_child_branch.feature
+++ b/features/compress/branch_types/observed_branch/on_child_branch.feature
@@ -23,7 +23,7 @@ Feature: does not compress observed branches in the stack
     Then Git Town runs the commands
       | BRANCH | COMMAND                                         |
       | child  | git fetch --prune --tags                        |
-      |        | git reset --soft observed                       |
+      |        | git reset --soft observed --                    |
       |        | git commit -m "child 1"                         |
       |        | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/branch_types/observed_branch/on_observed_branch.feature b/features/compress/branch_types/observed_branch/on_observed_branch.feature
index 6540d43a0..fa3241b06 100644
--- a/features/compress/branch_types/observed_branch/on_observed_branch.feature
+++ b/features/compress/branch_types/observed_branch/on_observed_branch.feature
@@ -24,7 +24,7 @@ Feature: does not compress an active observed branch
       | BRANCH   | COMMAND                                         |
       | observed | git fetch --prune --tags                        |
       |          | git checkout child                              |
-      | child    | git reset --soft observed                       |
+      | child    | git reset --soft observed --                    |
       |          | git commit -m "child 1"                         |
       |          | git push --force-with-lease --force-if-includes |
       |          | git checkout observed                           |
diff --git a/features/compress/branch_types/parked/on_child_branch.feature b/features/compress/branch_types/parked/on_child_branch.feature
index b5f0bb000..33025c95e 100644
--- a/features/compress/branch_types/parked/on_child_branch.feature
+++ b/features/compress/branch_types/parked/on_child_branch.feature
@@ -23,7 +23,7 @@ Feature: does not compress non-active parked branches in the stack
     Then Git Town runs the commands
       | BRANCH | COMMAND                                         |
       | child  | git fetch --prune --tags                        |
-      |        | git reset --soft parked                         |
+      |        | git reset --soft parked --                      |
       |        | git commit -m "child 1"                         |
       |        | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/branch_types/parked/on_parked_branch.feature b/features/compress/branch_types/parked/on_parked_branch.feature
index aaa62a7f2..93b0bce0d 100644
--- a/features/compress/branch_types/parked/on_parked_branch.feature
+++ b/features/compress/branch_types/parked/on_parked_branch.feature
@@ -23,11 +23,11 @@ Feature: compresses active parked branches
     Then Git Town runs the commands
       | BRANCH | COMMAND                                         |
       | parked | git fetch --prune --tags                        |
-      |        | git reset --soft main                           |
+      |        | git reset --soft main --                        |
       |        | git commit -m "parked 1"                        |
       |        | git push --force-with-lease --force-if-includes |
       |        | git checkout child                              |
-      | child  | git reset --soft parked                         |
+      | child  | git reset --soft parked --                      |
       |        | git commit -m "child 1"                         |
       |        | git push --force-with-lease --force-if-includes |
       |        | git checkout parked                             |
diff --git a/features/compress/branch_types/parked/top_level.feature b/features/compress/branch_types/parked/top_level.feature
index 60afa6ec0..9ebe1643d 100644
--- a/features/compress/branch_types/parked/top_level.feature
+++ b/features/compress/branch_types/parked/top_level.feature
@@ -17,7 +17,7 @@ Feature: compress the commits on a parked branch
     Then Git Town runs the commands
       | BRANCH | COMMAND                                         |
       | parked | git fetch --prune --tags                        |
-      |        | git reset --soft main                           |
+      |        | git reset --soft main --                        |
       |        | git commit -m "commit 1"                        |
       |        | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/branch_types/prototype_branch/on_child_branch.feature b/features/compress/branch_types/prototype_branch/on_child_branch.feature
index 64d97fc54..2515c0887 100644
--- a/features/compress/branch_types/prototype_branch/on_child_branch.feature
+++ b/features/compress/branch_types/prototype_branch/on_child_branch.feature
@@ -24,11 +24,11 @@ Feature: does not compress non-active prototype branches in the stack
       | BRANCH    | COMMAND                                         |
       | child     | git fetch --prune --tags                        |
       |           | git checkout prototype                          |
-      | prototype | git reset --soft main                           |
+      | prototype | git reset --soft main --                        |
       |           | git commit -m "prototype 1"                     |
       |           | git push --force-with-lease --force-if-includes |
       |           | git checkout child                              |
-      | child     | git reset --soft prototype                      |
+      | child     | git reset --soft prototype --                   |
       |           | git commit -m "child 1"                         |
       |           | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/branch_types/prototype_branch/on_prototype_branch.feature b/features/compress/branch_types/prototype_branch/on_prototype_branch.feature
index 4fafc3264..67bc48d37 100644
--- a/features/compress/branch_types/prototype_branch/on_prototype_branch.feature
+++ b/features/compress/branch_types/prototype_branch/on_prototype_branch.feature
@@ -23,11 +23,11 @@ Feature: compresses active prototype branches
     Then Git Town runs the commands
       | BRANCH    | COMMAND                                         |
       | prototype | git fetch --prune --tags                        |
-      |           | git reset --soft main                           |
+      |           | git reset --soft main --                        |
       |           | git commit -m "prototype 1"                     |
       |           | git push --force-with-lease --force-if-includes |
       |           | git checkout child                              |
-      | child     | git reset --soft prototype                      |
+      | child     | git reset --soft prototype --                   |
       |           | git commit -m "child 1"                         |
       |           | git push --force-with-lease --force-if-includes |
       |           | git checkout prototype                          |
diff --git a/features/compress/branch_types/prototype_branch/top_level.feature b/features/compress/branch_types/prototype_branch/top_level.feature
index 3d5c9b67d..1d21c5ac6 100644
--- a/features/compress/branch_types/prototype_branch/top_level.feature
+++ b/features/compress/branch_types/prototype_branch/top_level.feature
@@ -17,7 +17,7 @@ Feature: compress the commits on a prototype branch
     Then Git Town runs the commands
       | BRANCH    | COMMAND                                         |
       | prototype | git fetch --prune --tags                        |
-      |           | git reset --soft main                           |
+      |           | git reset --soft main --                        |
       |           | git commit -m "commit 1"                        |
       |           | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/color.ui/always.feature b/features/compress/color.ui/always.feature
index e0dbb7d2e..4acd46764 100644
--- a/features/compress/color.ui/always.feature
+++ b/features/compress/color.ui/always.feature
@@ -17,7 +17,7 @@ Feature: compress keeps the full commit message of the first commit
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                         |
       | feature | git fetch --prune --tags                        |
-      |         | git reset --soft main                           |
+      |         | git reset --soft main --                        |
       |         | git commit -m "commit 1                         |
       |         | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/commit_message/multiple_lines.feature b/features/compress/commit_message/multiple_lines.feature
index 18d4c7db8..4451bcd7d 100644
--- a/features/compress/commit_message/multiple_lines.feature
+++ b/features/compress/commit_message/multiple_lines.feature
@@ -16,7 +16,7 @@ Feature: compress keeps the full commit message of the first commit
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                         |
       | feature | git fetch --prune --tags                        |
-      |         | git reset --soft main                           |
+      |         | git reset --soft main --                        |
       |         | git commit -m "commit 1                         |
       |         | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/commit_message/override.feature b/features/compress/commit_message/override.feature
index bfc577b1c..70f99ae4b 100644
--- a/features/compress/commit_message/override.feature
+++ b/features/compress/commit_message/override.feature
@@ -17,7 +17,7 @@ Feature: compress the commits on a feature branch
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                         |
       | feature | git fetch --prune --tags                        |
-      |         | git reset --soft main                           |
+      |         | git reset --soft main --                        |
       |         | git commit -m compressed                        |
       |         | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/dry_run/dry_run.feature b/features/compress/dry_run/dry_run.feature
index 703ef0ab1..89ff4ecc9 100644
--- a/features/compress/dry_run/dry_run.feature
+++ b/features/compress/dry_run/dry_run.feature
@@ -17,7 +17,7 @@ Feature: dry-run compressing the commits on a feature branch
     Then Git Town runs the commands
       | BRANCH  | COMMAND                  |
       | feature | git fetch --prune --tags |
-      |         | git reset --soft main    |
+      |         | git reset --soft main -- |
       |         | git commit -m "commit 1" |
     And all branches are now synchronized
     And file "file_1" still has content "content 1"
diff --git a/features/compress/folder_same_name_as_branch/folder_has_same_name_as_branch.feature b/features/compress/folder_same_name_as_branch/folder_has_same_name_as_branch.feature
new file mode 100644
index 000000000..eb94a212d
--- /dev/null
+++ b/features/compress/folder_same_name_as_branch/folder_has_same_name_as_branch.feature
@@ -0,0 +1,37 @@
+Feature: compress the branch that has the same name as a folder
+
+  Background:
+    Given a Git repo with origin
+    And the commits
+      | BRANCH | LOCATION | MESSAGE  | FILE NAME |
+      | main   | local    | commit 1 | main      |
+    And the branches
+      | NAME    | TYPE    | PARENT | LOCATIONS |
+      | feature | feature | main   | local     |
+    And the commits
+      | BRANCH  | LOCATION | MESSAGE  | FILE NAME | FILE CONTENT |
+      | feature | local    | commit 1 | file_1    | content 1    |
+      |         |          | commit 2 | file_2    | content 2    |
+    And the current branch is "feature"
+    When I run "git-town compress"
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH  | COMMAND                  |
+      | feature | git fetch --prune --tags |
+      |         | git reset --soft main -- |
+      |         | git commit -m "commit 1" |
+    And these commits exist now
+      | BRANCH  | LOCATION | MESSAGE  |
+      | main    | local    | commit 1 |
+      | feature | local    | commit 1 |
+    And file "file_1" still has content "content 1"
+    And file "file_2" still has content "content 2"
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH  | COMMAND                               |
+      | feature | git reset --hard {{ sha 'commit 2' }} |
+    And the initial branches and lineage exist now
+    And the initial commits exist now
diff --git a/features/compress/git_history/previous_branch_in_other_worktree.feature b/features/compress/git_history/previous_branch_in_other_worktree.feature
index 33c261867..25b087403 100644
--- a/features/compress/git_history/previous_branch_in_other_worktree.feature
+++ b/features/compress/git_history/previous_branch_in_other_worktree.feature
@@ -18,7 +18,7 @@ Feature: compress a branch when the previous branch is active in another worktre
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                         |
       | current | git fetch --prune --tags                        |
-      |         | git reset --soft main                           |
+      |         | git reset --soft main --                        |
       |         | git commit -m "commit 1"                        |
       |         | git push --force-with-lease --force-if-includes |
     And the previous Git branch is now "current"
diff --git a/features/compress/language/es.feature b/features/compress/language/es.feature
index 87e462ba4..8166add02 100644
--- a/features/compress/language/es.feature
+++ b/features/compress/language/es.feature
@@ -18,7 +18,7 @@ Feature: compress the commits in Spanish
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                         |
       | feature | git fetch --prune --tags                        |
-      |         | git reset --soft main                           |
+      |         | git reset --soft main --                        |
       |         | git commit -m "commit 1"                        |
       |         | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/merge_commit/with.feature b/features/compress/merge_commit/with.feature
index 3db7c64e8..ebaade97c 100644
--- a/features/compress/merge_commit/with.feature
+++ b/features/compress/merge_commit/with.feature
@@ -18,7 +18,7 @@ Feature: compress a branch with a merge commit
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                         |
       | feature | git fetch --prune --tags                        |
-      |         | git reset --soft main                           |
+      |         | git reset --soft main --                        |
       |         | git commit -m "commit 1"                        |
       |         | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/no_verify/enabled.feature b/features/compress/no_verify/enabled.feature
index bdbfa9317..59549a099 100644
--- a/features/compress/no_verify/enabled.feature
+++ b/features/compress/no_verify/enabled.feature
@@ -16,7 +16,7 @@ Feature: compress while disabling the pre-commit hook
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                         |
       | feature | git fetch --prune --tags                        |
-      |         | git reset --soft main                           |
+      |         | git reset --soft main --                        |
       |         | git commit -m "commit 1" --no-verify            |
       |         | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/stack/at_stack_leaf.feature b/features/compress/stack/at_stack_leaf.feature
index 5d1afd642..f1ac82c18 100644
--- a/features/compress/stack/at_stack_leaf.feature
+++ b/features/compress/stack/at_stack_leaf.feature
@@ -34,15 +34,15 @@ Feature: compress the commits on an entire stack when at the stack root
       | BRANCH | COMMAND                                         |
       | gamma  | git fetch --prune --tags                        |
       |        | git checkout alpha                              |
-      | alpha  | git reset --soft main                           |
+      | alpha  | git reset --soft main --                        |
       |        | git commit -m "alpha 1"                         |
       |        | git push --force-with-lease --force-if-includes |
       |        | git checkout beta                               |
-      | beta   | git reset --soft alpha                          |
+      | beta   | git reset --soft alpha --                       |
       |        | git commit -m "beta 1"                          |
       |        | git push --force-with-lease --force-if-includes |
       |        | git checkout gamma                              |
-      | gamma  | git reset --soft beta                           |
+      | gamma  | git reset --soft beta --                        |
       |        | git commit -m "gamma 1"                         |
       |        | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/stack/at_stack_root.feature b/features/compress/stack/at_stack_root.feature
index 2217939d6..6c8a0d67e 100644
--- a/features/compress/stack/at_stack_root.feature
+++ b/features/compress/stack/at_stack_root.feature
@@ -33,15 +33,15 @@ Feature: compress the commits on an entire stack when at the stack root
     Then Git Town runs the commands
       | BRANCH | COMMAND                                         |
       | alpha  | git fetch --prune --tags                        |
-      |        | git reset --soft main                           |
+      |        | git reset --soft main --                        |
       |        | git commit -m "alpha 1"                         |
       |        | git push --force-with-lease --force-if-includes |
       |        | git checkout beta                               |
-      | beta   | git reset --soft alpha                          |
+      | beta   | git reset --soft alpha --                       |
       |        | git commit -m "beta 1"                          |
       |        | git push --force-with-lease --force-if-includes |
       |        | git checkout gamma                              |
-      | gamma  | git reset --soft beta                           |
+      | gamma  | git reset --soft beta --                        |
       |        | git commit -m "gamma 1"                         |
       |        | git push --force-with-lease --force-if-includes |
       |        | git checkout alpha                              |
diff --git a/features/compress/tracking_branch/exists/multiple_commits.feature b/features/compress/tracking_branch/exists/multiple_commits.feature
index aec63abed..e2c8db918 100644
--- a/features/compress/tracking_branch/exists/multiple_commits.feature
+++ b/features/compress/tracking_branch/exists/multiple_commits.feature
@@ -17,7 +17,7 @@ Feature: compress the commits on a feature branch
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                         |
       | feature | git fetch --prune --tags                        |
-      |         | git reset --soft main                           |
+      |         | git reset --soft main --                        |
       |         | git commit -m "commit 1"                        |
       |         | git push --force-with-lease --force-if-includes |
     And all branches are now synchronized
diff --git a/features/compress/tracking_branch/exists/offline.feature b/features/compress/tracking_branch/exists/offline.feature
index b5bb664ce..f11ba6dcc 100644
--- a/features/compress/tracking_branch/exists/offline.feature
+++ b/features/compress/tracking_branch/exists/offline.feature
@@ -16,7 +16,7 @@ Feature: compress the commits in offline mode
   Scenario: result
     Then Git Town runs the commands
       | BRANCH  | COMMAND                  |
-      | feature | git reset --soft main    |
+      | feature | git reset --soft main -- |
       |         | git commit -m "commit 1" |
     And these commits exist now
       | BRANCH  | LOCATION | MESSAGE  |
diff --git a/features/compress/tracking_branch/none/local_branch.feature b/features/compress/tracking_branch/none/local_branch.feature
index c26bf7559..971ee9116 100644
--- a/features/compress/tracking_branch/none/local_branch.feature
+++ b/features/compress/tracking_branch/none/local_branch.feature
@@ -17,7 +17,7 @@ Feature: compress the commits on a local feature branch
     Then Git Town runs the commands
       | BRANCH  | COMMAND                  |
       | feature | git fetch --prune --tags |
-      |         | git reset --soft main    |
+      |         | git reset --soft main -- |
       |         | git commit -m "commit 1" |
     And all branches are now synchronized
     And these commits exist now
diff --git a/features/compress/uncommitted_changes/untracked_file.feature b/features/compress/uncommitted_changes/untracked_file.feature
index 3a7f6c4e0..e11bc71e4 100644
--- a/features/compress/uncommitted_changes/untracked_file.feature
+++ b/features/compress/uncommitted_changes/untracked_file.feature
@@ -26,11 +26,11 @@ Feature: compress the commits on an entire stack when at the stack root
       | alpha  | git fetch --prune --tags                        |
       |        | git add -A                                      |
       |        | git stash -m "Git Town WIP"                     |
-      |        | git reset --soft main                           |
+      |        | git reset --soft main --                        |
       |        | git commit -m "alpha 1"                         |
       |        | git push --force-with-lease --force-if-includes |
       |        | git checkout beta                               |
-      | beta   | git reset --soft alpha                          |
+      | beta   | git reset --soft alpha --                       |
       |        | git commit -m "beta 1"                          |
       |        | git push --force-with-lease --force-if-includes |
       |        | git checkout alpha                              |
diff --git a/features/compress/verbose/verbose.feature b/features/compress/verbose/verbose.feature
index f7d89df47..363e1431a 100644
--- a/features/compress/verbose/verbose.feature
+++ b/features/compress/verbose/verbose.feature
@@ -34,7 +34,7 @@ Feature: compress the commits on a feature branch verbosely
       |         | backend  | git log "--format=%H %s" main..feature                                                                                                                                                                                                                                                                                                           |
       |         | backend  | git show --no-patch --format=%B {{ sha-initial 'commit 1' }}                                                                                                                                                                                                                                                                                     |
       |         | backend  | git log --no-merges --format=%H refs/heads/main ^refs/heads/feature                                                                                                                                                                                                                                                                              |
-      |         | frontend | git reset --soft main                                                                                                                                                                                                                                                                                                                            |
+      |         | frontend | git reset --soft main --                                                                                                                                                                                                                                                                                                                         |
       |         | frontend | git commit -m "commit 1"                                                                                                                                                                                                                                                                                                                         |
       |         | backend  | git rev-parse feature origin/feature                                                                                                                                                                                                                                                                                                             |
       |         | frontend | git push --force-with-lease --force-if-includes                                                                                                                                                                                                                                                                                                  |
diff --git a/features/config/color.ui/always.feature b/features/config/color.ui/always.feature
index 68467e47d..cab8dc9e9 100644
--- a/features/config/color.ui/always.feature
+++ b/features/config/color.ui/always.feature
@@ -21,6 +21,7 @@ Feature: show the configuration
     Given Git setting "git-town.perennial-branches" is "qa staging"
     And Git setting "git-town.perennial-regex" is "^release-"
     And Git setting "git-town.auto-sync" is "false"
+    And Git setting "git-town.browser" is "firefox"
     And Git setting "git-town.contribution-regex" is "^renovate/"
     And Git setting "git-town.display-types" is "all"
     And Git setting "git-town.observed-regex" is "^dependabot/"
@@ -55,11 +56,13 @@ Feature: show the configuration
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: no
 
       Hosting:
+        browser: firefox
         development remote: origin
         forge type: (not set)
         origin hostname: (not set)
@@ -72,6 +75,9 @@ Feature: show the configuration
         GitLab connector type: api
         GitLab token: (not set)
 
+      Propose:
+        lineage: none
+
       Ship:
         delete tracking branch: yes
         ship strategy: squash-merge
@@ -108,9 +114,13 @@ Feature: show the configuration
       stash = false
 
       [hosting]
+      browser = "firefox"
       forge-type = "github"
       origin-hostname = "github.com"
 
+      [propose]
+      lineage = "cli"
+
       [ship]
       delete-tracking-branch = true
       strategy = "squash-merge"
@@ -149,11 +159,13 @@ Feature: show the configuration
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: push
         stash uncommitted changes: no
 
       Hosting:
+        browser: firefox
         development remote: origin
         forge type: github
         origin hostname: github.com
@@ -166,6 +178,9 @@ Feature: show the configuration
         GitLab connector type: (not set)
         GitLab token: (not set)
 
+      Propose:
+        lineage: cli
+
       Ship:
         delete tracking branch: yes
         ship strategy: squash-merge
@@ -188,6 +203,7 @@ Feature: show the configuration
     Given the main branch is "git-main"
     And Git setting "git-town.perennial-branches" is "git-perennial-1 git-perennial-2"
     And Git setting "git-town.auto-sync" is "false"
+    And Git setting "git-town.browser" is "firefox"
     And Git setting "git-town.contribution-regex" is "^git-contribution-regex"
     And Git setting "git-town.display-types" is "all"
     And Git setting "git-town.observed-regex" is "^git-observed-regex"
@@ -224,6 +240,7 @@ Feature: show the configuration
       stash = true
 
       [hosting]
+      browser = "chrome"
       forge-type = "github"
       origin-hostname = "github.com"
 
@@ -265,11 +282,13 @@ Feature: show the configuration
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: no
 
       Hosting:
+        browser: firefox
         development remote: origin
         forge type: github
         origin hostname: github.com
@@ -282,6 +301,9 @@ Feature: show the configuration
         GitLab connector type: (not set)
         GitLab token: (not set)
 
+      Propose:
+        lineage: none
+
       Ship:
         delete tracking branch: no
         ship strategy: squash-merge
@@ -333,11 +355,13 @@ Feature: show the configuration
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
 
       Hosting:
+        browser: (not set)
         development remote: origin
         forge type: (not set)
         origin hostname: (not set)
@@ -350,6 +374,9 @@ Feature: show the configuration
         GitLab connector type: (not set)
         GitLab token: (not set)
 
+      Propose:
+        lineage: none
+
       Ship:
         delete tracking branch: yes
         ship strategy: api
@@ -407,11 +434,13 @@ Feature: show the configuration
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
 
       Hosting:
+        browser: (not set)
         development remote: origin
         forge type: (not set)
         origin hostname: (not set)
@@ -424,6 +453,9 @@ Feature: show the configuration
         GitLab connector type: (not set)
         GitLab token: (not set)
 
+      Propose:
+        lineage: none
+
       Ship:
         delete tracking branch: yes
         ship strategy: api
diff --git a/features/config/remove/invalid_data.feature b/features/config/remove/invalid_data.feature
new file mode 100644
index 000000000..25b2b31d5
--- /dev/null
+++ b/features/config/remove/invalid_data.feature
@@ -0,0 +1,48 @@
+Feature: reset invalid configuration
+
+  Scenario: sync-feature-strategy is invalid
+    Given a Git repo with origin
+    And the main branch is "main"
+    And local Git setting "git-town.feature-regex" is "(feat"
+    And local Git setting "git-town.perennial-regex" is "(per"
+    And local Git setting "git-town.contribution-regex" is "(cont"
+    And local Git setting "git-town.observed-regex" is "(obs"
+    And local Git setting "git-town.new-branch-type" is "zonk"
+    And local Git setting "git-town.unknown-branch-type" is "zonk"
+    And local Git setting "git-town.sync-feature-strategy" is "--help"
+    And local Git setting "git-town.sync-perennial-strategy" is "zonk"
+    And local Git setting "git-town.sync-prototype-strategy" is "zonk"
+    And local Git setting "git-town.sync-upstream" is "zonk"
+    And local Git setting "git-town.auto-sync" is "zonk"
+    And local Git setting "git-town.sync-tags" is "zonk"
+    And local Git setting "git-town.detached" is "zonk"
+    And local Git setting "git-town.stash" is "zonk"
+    And local Git setting "git-town.share-new-branches" is "zonk"
+    And local Git setting "git-town.push-branches" is "zonk"
+    And local Git setting "git-town.push-hook" is "zonk"
+    And local Git setting "git-town.ship-strategy" is "zonk"
+    And local Git setting "git-town.ship-delete-tracking-branch" is "zonk"
+    And local Git setting "git-town.order" is "zonk"
+    When I run "git-town config remove"
+    Then Git Town runs no commands
+    And Git Town is no longer configured
+    And local Git setting "git-town.feature-regex" now doesn't exist
+    And local Git setting "git-town.perennial-regex" now doesn't exist
+    And local Git setting "git-town.contribution-regex" now doesn't exist
+    And local Git setting "git-town.observed-regex" now doesn't exist
+    And local Git setting "git-town.new-branch-type" now doesn't exist
+    And local Git setting "git-town.unknown-branch-type" now doesn't exist
+    And local Git setting "git-town.sync-feature-strategy" now doesn't exist
+    And local Git setting "git-town.sync-perennial-strategy" now doesn't exist
+    And local Git setting "git-town.sync-prototype-strategy" now doesn't exist
+    And local Git setting "git-town.sync-upstream" now doesn't exist
+    And local Git setting "git-town.auto-sync" now doesn't exist
+    And local Git setting "git-town.sync-tags" now doesn't exist
+    And local Git setting "git-town.detached" now doesn't exist
+    And local Git setting "git-town.stash" now doesn't exist
+    And local Git setting "git-town.share-new-branches" now doesn't exist
+    And local Git setting "git-town.push-branches" now doesn't exist
+    And local Git setting "git-town.push-hook" now doesn't exist
+    And local Git setting "git-town.ship-strategy" now doesn't exist
+    And local Git setting "git-town.ship-delete-tracking-branch" now doesn't exist
+    And local Git setting "git-town.order" now doesn't exist
diff --git a/features/config/view/alternative_config_file.feature b/features/config/view/alternative_config_file.feature
index 2cae91274..b18367ded 100644
--- a/features/config/view/alternative_config_file.feature
+++ b/features/config/view/alternative_config_file.feature
@@ -32,11 +32,13 @@ Feature: show the configuration when using an alternative config file
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
 
       Hosting:
+        browser: (not set)
         development remote: origin
         forge type: (not set)
         origin hostname: (not set)
@@ -49,6 +51,9 @@ Feature: show the configuration when using an alternative config file
         GitLab connector type: (not set)
         GitLab token: (not set)
 
+      Propose:
+        lineage: none
+
       Ship:
         delete tracking branch: yes
         ship strategy: api
diff --git a/features/config/view/config_file.feature b/features/config/view/config_file.feature
index 2d3a5cb9f..9caa2f17f 100644
--- a/features/config/view/config_file.feature
+++ b/features/config/view/config_file.feature
@@ -30,10 +30,12 @@ Feature: display information from config file
       display-types = "all"
 
       [create]
+      branch-prefix = "acme-"
       share-new-branches = "push"
       stash = false
 
       [hosting]
+      browser = "chrome"
       forge-type = "github"
       github-connector = "gh"
       origin-hostname = "github.com"
@@ -77,11 +79,13 @@ Feature: display information from config file
         git user email: email@example.com
 
       Create:
+        branch prefix: acme-
         new branch type: (not set)
         share new branches: push
         stash uncommitted changes: no
 
       Hosting:
+        browser: chrome
         development remote: origin
         forge type: github
         origin hostname: github.com
@@ -94,6 +98,9 @@ Feature: display information from config file
         GitLab connector type: (not set)
         GitLab token: (not set)
 
+      Propose:
+        lineage: none
+
       Ship:
         delete tracking branch: yes
         ship strategy: squash-merge
diff --git a/features/config/view/detached_head.feature b/features/config/view/detached_head.feature
index dc1d32cc1..0efa81977 100644
--- a/features/config/view/detached_head.feature
+++ b/features/config/view/detached_head.feature
@@ -14,6 +14,8 @@ Feature: display configuration from Git metadata in detached head state
       |        | local    | commit 2 |
     And Git setting "git-town.perennial-branches" is "qa staging"
     And Git setting "git-town.auto-sync" is "false"
+    And Git setting "git-town.branch-prefix" is "acme-"
+    And Git setting "git-town.browser" is "firefox"
     And Git setting "git-town.display-types" is "all"
     And Git setting "git-town.perennial-regex" is "^release-"
     And Git setting "git-town.contribution-regex" is "^renovate/"
@@ -23,6 +25,7 @@ Feature: display configuration from Git metadata in detached head state
     And Git setting "git-town.unknown-branch-type" is "observed"
     And Git setting "git-town.auto-resolve" is "false"
     And Git setting "git-town.detached" is "true"
+    And Git setting "git-town.proposals-show-lineage" is "cli"
     And Git setting "git-town.stash" is "false"
     And the current branch is "branch"
     And I ran "git checkout HEAD^"
@@ -52,11 +55,13 @@ Feature: display configuration from Git metadata in detached head state
         git user email: email@example.com
 
       Create:
+        branch prefix: acme-
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: no
 
       Hosting:
+        browser: firefox
         development remote: origin
         forge type: (not set)
         origin hostname: (not set)
@@ -69,6 +74,9 @@ Feature: display configuration from Git metadata in detached head state
         GitLab connector type: (not set)
         GitLab token: (not set)
 
+      Propose:
+        lineage: cli
+
       Ship:
         delete tracking branch: yes
         ship strategy: squash-merge
diff --git a/features/config/view/environment_variables.feature b/features/config/view/environment_variables.feature
index b7c9ba0f3..d0137cc82 100644
--- a/features/config/view/environment_variables.feature
+++ b/features/config/view/environment_variables.feature
@@ -16,6 +16,8 @@ Feature: display configuration defined in environment variables
       | GIT_TOWN_AUTO_SYNC                   | false              |
       | GIT_TOWN_BITBUCKET_APP_PASSWORD      | bitbucket-password |
       | GIT_TOWN_BITBUCKET_USERNAME          | bitbucket-user     |
+      | GIT_TOWN_BRANCH_PREFIX               | acme-              |
+      | BROWSER                              | firefox            |
       | GIT_TOWN_FORGEJO_TOKEN               | forgejo-token      |
       | GIT_TOWN_CONTRIBUTION_REGEX          | ^renovate/         |
       | GIT_TOWN_DETACHED                    | true               |
@@ -32,6 +34,7 @@ Feature: display configuration defined in environment variables
       | GIT_TOWN_NEW_BRANCH_TYPE             | prototype          |
       | GIT_TOWN_OBSERVED_REGEX              | ^dependabot/       |
       | GIT_TOWN_ORDER                       | desc               |
+      | GIT_TOWN_PROPOSALS_SHOW_LINEAGE      | cli                |
       | GIT_TOWN_ORIGIN_HOSTNAME             | codeforge          |
       | GIT_TOWN_OFFLINE                     | 1                  |
       | GIT_TOWN_PERENNIAL_BRANCHES          | qa staging         |
@@ -71,11 +74,13 @@ Feature: display configuration defined in environment variables
         git user email: email@example.com
 
       Create:
+        branch prefix: acme-
         new branch type: prototype
         share new branches: push
         stash uncommitted changes: no
 
       Hosting:
+        browser: firefox
         development remote: my-fork
         forge type: gitlab
         origin hostname: codeforge
@@ -88,6 +93,9 @@ Feature: display configuration defined in environment variables
         GitLab connector type: glab
         GitLab token: gitlab-token
 
+      Propose:
+        lineage: cli
+
       Ship:
         delete tracking branch: no
         ship strategy: fast-forward
diff --git a/features/config/view/git_metadata.feature b/features/config/view/git_metadata.feature
index 850bb275b..8267f0942 100644
--- a/features/config/view/git_metadata.feature
+++ b/features/config/view/git_metadata.feature
@@ -19,6 +19,8 @@ Feature: display configuration from Git metadata
     Given Git setting "git-town.perennial-branches" is "qa staging"
     And Git setting "git-town.perennial-regex" is "^release-"
     And Git setting "git-town.auto-sync" is "false"
+    And Git setting "git-town.branch-prefix" is "acme-"
+    And Git setting "git-town.browser" is "firefox"
     And Git setting "git-town.contribution-regex" is "^renovate/"
     And Git setting "git-town.display-types" is "all"
     And Git setting "git-town.observed-regex" is "^dependabot/"
@@ -28,6 +30,7 @@ Feature: display configuration from Git metadata
     And Git setting "git-town.auto-resolve" is "false"
     And Git setting "git-town.detached" is "true"
     And Git setting "git-town.stash" is "false"
+    And Git setting "git-town.proposals-show-lineage" is "cli"
     When I run "git-town config"
     Then Git Town prints:
       """
@@ -52,11 +55,13 @@ Feature: display configuration from Git metadata
         git user email: email@example.com
 
       Create:
+        branch prefix: acme-
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: no
 
       Hosting:
+        browser: firefox
         development remote: origin
         forge type: (not set)
         origin hostname: (not set)
@@ -69,6 +74,9 @@ Feature: display configuration from Git metadata
         GitLab connector type: (not set)
         GitLab token: (not set)
 
+      Propose:
+        lineage: cli
+
       Ship:
         delete tracking branch: yes
         ship strategy: squash-merge
@@ -119,11 +127,13 @@ Feature: display configuration from Git metadata
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
 
       Hosting:
+        browser: (not set)
         development remote: origin
         forge type: (not set)
         origin hostname: (not set)
@@ -136,6 +146,9 @@ Feature: display configuration from Git metadata
         GitLab connector type: (not set)
         GitLab token: (not set)
 
+      Propose:
+        lineage: none
+
       Ship:
         delete tracking branch: yes
         ship strategy: api
diff --git a/features/config/view/in_subfolder.feature b/features/config/view/in_subfolder.feature
index 08568990d..90eb70f41 100644
--- a/features/config/view/in_subfolder.feature
+++ b/features/config/view/in_subfolder.feature
@@ -33,11 +33,13 @@ Feature: show the configuration from a subfolder
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
 
       Hosting:
+        browser: (not set)
         development remote: origin
         forge type: (not set)
         origin hostname: (not set)
@@ -50,6 +52,9 @@ Feature: show the configuration from a subfolder
         GitLab connector type: (not set)
         GitLab token: (not set)
 
+      Propose:
+        lineage: none
+
       Ship:
         delete tracking branch: yes
         ship strategy: api
diff --git a/features/config/view/invalid_git_metadata.feature b/features/config/view/invalid_git_metadata.feature
new file mode 100644
index 000000000..faefccc45
--- /dev/null
+++ b/features/config/view/invalid_git_metadata.feature
@@ -0,0 +1,111 @@
+Feature: display invalid Git metadata
+
+  Scenario: invalid sync-feature-strategy
+    Given a Git repo with origin
+    And local Git setting "git-town.branch-prefix" is ""
+    And local Git setting "git-town.feature-regex" is "(feat"
+    And local Git setting "git-town.perennial-regex" is "(per"
+    And local Git setting "git-town.proposals-show-lineage" is "zonk"
+    And local Git setting "git-town.contribution-regex" is "(cont"
+    And local Git setting "git-town.observed-regex" is "(obs"
+    And local Git setting "git-town.new-branch-type" is "zonk"
+    And local Git setting "git-town.unknown-branch-type" is "zonk"
+    And local Git setting "git-town.sync-feature-strategy" is "--help"
+    And local Git setting "git-town.sync-perennial-strategy" is "zonk"
+    And local Git setting "git-town.sync-prototype-strategy" is "zonk"
+    And local Git setting "git-town.sync-upstream" is "zonk"
+    And local Git setting "git-town.auto-sync" is "zonk"
+    And local Git setting "git-town.sync-tags" is "zonk"
+    And local Git setting "git-town.detached" is "zonk"
+    And local Git setting "git-town.stash" is "zonk"
+    And local Git setting "git-town.share-new-branches" is "zonk"
+    And local Git setting "git-town.push-branches" is "zonk"
+    And local Git setting "git-town.push-hook" is "zonk"
+    And local Git setting "git-town.ship-strategy" is "zonk"
+    And local Git setting "git-town.ship-delete-tracking-branch" is "zonk"
+    And local Git setting "git-town.order" is "zonk"
+    When I run "git-town config"
+    Then Git Town prints:
+      """
+      Ignoring invalid value for "git-town.auto-sync": "zonk"
+      Ignoring invalid value for "git-town.contribution-regex": "(cont"
+      Ignoring invalid value for "git-town.detached": "zonk"
+      Ignoring invalid value for "git-town.feature-regex": "(feat"
+      Ignoring invalid value for "git-town.new-branch-type": "zonk"
+      Ignoring invalid value for "git-town.observed-regex": "(obs"
+      Ignoring invalid value for "git-town.order": "zonk"
+      Ignoring invalid value for "git-town.perennial-regex": "(per"
+      Ignoring invalid value for "git-town.proposals-show-lineage": "zonk"
+      Ignoring invalid value for "git-town.push-branches": "zonk"
+      Ignoring invalid value for "git-town.push-hook": "zonk"
+      Ignoring invalid value for "git-town.share-new-branches": "zonk"
+      Ignoring invalid value for "git-town.ship-delete-tracking-branch": "zonk"
+      Ignoring invalid value for "git-town.ship-strategy": "zonk"
+      Ignoring invalid value for "git-town.stash": "zonk"
+      Ignoring invalid value for "git-town.sync-feature-strategy": "--help"
+      Ignoring invalid value for "git-town.sync-perennial-strategy": "zonk"
+      Ignoring invalid value for "git-town.sync-prototype-strategy": "zonk"
+      Ignoring invalid value for "git-town.sync-tags": "zonk"
+      Ignoring invalid value for "git-town.sync-upstream": "zonk"
+      Ignoring invalid value for "git-town.unknown-branch-type": "zonk"
+
+      Branches:
+        contribution branches: (none)
+        contribution regex: (not set)
+        feature regex: (not set)
+        main branch: main
+        observed branches: (none)
+        observed regex: (not set)
+        parked branches: (none)
+        perennial branches: (none)
+        perennial regex: (not set)
+        prototype branches: (none)
+        unknown branch type: feature
+        order: asc
+        display types: all branch types except "feature" and "main"
+
+      Configuration:
+        offline: no
+        git user name: user
+        git user email: email@example.com
+
+      Create:
+        branch prefix: (not set)
+        new branch type: (not set)
+        share new branches: no
+        stash uncommitted changes: yes
+
+      Hosting:
+        browser: (not set)
+        development remote: origin
+        forge type: (not set)
+        origin hostname: (not set)
+        Bitbucket username: (not set)
+        Bitbucket app password: (not set)
+        Forgejo token: (not set)
+        Gitea token: (not set)
+        GitHub connector type: (not set)
+        GitHub token: (not set)
+        GitLab connector type: (not set)
+        GitLab token: (not set)
+
+      Propose:
+        lineage: none
+
+      Ship:
+        delete tracking branch: yes
+        ship strategy: api
+
+      Sync:
+        auto-resolve phantom conflicts: yes
+        auto-sync: yes
+        run detached: no
+        run pre-push hook: yes
+        feature sync strategy: merge
+        perennial sync strategy: rebase
+        prototype sync strategy: merge
+        push branches: yes
+        sync tags: yes
+        sync with upstream: yes
+        auto-resolve phantom conflicts: yes
+      """
diff --git a/features/config/view/lang/es.feature b/features/config/view/lang/es.feature
index ee629d416..3b1bfcdab 100644
--- a/features/config/view/lang/es.feature
+++ b/features/config/view/lang/es.feature
@@ -48,11 +48,13 @@ Feature: show the configuration in Spanish
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
 
       Hosting:
+        browser: (not set)
         development remote: origin
         forge type: (not set)
         origin hostname: (not set)
@@ -65,6 +67,9 @@ Feature: show the configuration in Spanish
         GitLab connector type: (not set)
         GitLab token: (not set)
 
+      Propose:
+        lineage: none
+
       Ship:
         delete tracking branch: yes
         ship strategy: squash-merge
diff --git a/features/config/view/multiple_config_sources.feature b/features/config/view/multiple_config_sources.feature
index 64767f6e1..216d22280 100644
--- a/features/config/view/multiple_config_sources.feature
+++ b/features/config/view/multiple_config_sources.feature
@@ -20,12 +20,15 @@ Feature: show the configuration
     Given the main branch is "git-main"
     And Git setting "git-town.perennial-branches" is "git-perennial-1 git-perennial-2"
     And Git setting "git-town.auto-sync" is "false"
+    And Git setting "git-town.branch-prefix" is "git-"
+    And Git setting "git-town.browser" is "firefox"
     And Git setting "git-town.contribution-regex" is "^git-contribution-regex"
     And Git setting "git-town.detached" is "true"
     And Git setting "git-town.display-types" is "no"
     And Git setting "git-town.observed-regex" is "^git-observed-regex"
     And Git setting "git-town.perennial-regex" is "^git-perennial-"
     And Git setting "git-town.feature-regex" is "git-feature-.*"
+    And Git setting "git-town.proposals-show-lineage" is "cli"
     And Git setting "git-town.github-connector" is "api"
     And Git setting "git-town.order" is "desc"
     And Git setting "git-town.share-new-branches" is "no"
@@ -52,10 +55,12 @@ Feature: show the configuration
       display-types = "all"
 
       [create]
+      branch-prefix = "file-"
       share-new-branches = "push"
       stash = true
 
       [hosting]
+      browser = "chrome"
       forge-type = "github"
       github-connector = "gh"
       origin-hostname = "github.com"
@@ -97,11 +102,13 @@ Feature: show the configuration
         git user email: email@example.com
 
       Create:
+        branch prefix: git-
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: no
 
       Hosting:
+        browser: firefox
         development remote: origin
         forge type: github
         origin hostname: github.com
@@ -114,6 +121,9 @@ Feature: show the configuration
         GitLab connector type: (not set)
         GitLab token: (not set)
 
+      Propose:
+        lineage: cli
+
       Ship:
         delete tracking branch: no
         ship strategy: squash-merge
diff --git a/features/config/view/no_config.feature b/features/config/view/no_config.feature
index 33bf09037..8f59e3c0d 100644
--- a/features/config/view/no_config.feature
+++ b/features/config/view/no_config.feature
@@ -35,11 +35,13 @@ Feature: no configuration data
         git user email: email@example.com
 
       Create:
+        branch prefix: (not set)
         new branch type: (not set)
         share new branches: no
         stash uncommitted changes: yes
 
       Hosting:
+        browser: (not set)
         development remote: origin
         forge type: (not set)
         origin hostname: (not set)
@@ -52,6 +54,9 @@ Feature: no configuration data
         GitLab connector type: (not set)
         GitLab token: (not set)
 
+      Propose:
+        lineage: none
+
       Ship:
         delete tracking branch: yes
         ship strategy: api
diff --git a/features/hack/beam/unknown_parent.feature b/features/hack/beam/unknown_parent.feature
new file mode 100644
index 000000000..00d572fe4
--- /dev/null
+++ b/features/hack/beam/unknown_parent.feature
@@ -0,0 +1,72 @@
+@messyoutput
+Feature: beam commits onto a new feature branch when the parent branch is unknown
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME     | TYPE   | PARENT | LOCATIONS     |
+      | existing | (none) |        | local, origin |
+    And the commits
+      | BRANCH   | LOCATION | MESSAGE     |
+      | main     | origin   | main commit |
+      | existing | local    | commit 1    |
+      | existing | local    | commit 2    |
+    And the current branch is "existing"
+    When I run "git-town hack new --beam" and enter into the dialog:
+      | DIALOG                       | KEYS             |
+      | parent branch for "existing" | enter            |
+      | commits to beam              | down space enter |
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH   | COMMAND                                                                                                 |
+      | existing | git checkout -b new main                                                                                |
+      | new      | git cherry-pick {{ sha-initial 'commit 2' }}                                                            |
+      |          | git checkout existing                                                                                   |
+      | existing | git -c rebase.updateRefs=false rebase --onto {{ sha-initial 'commit 2' }}^ {{ sha-initial 'commit 2' }} |
+      |          | git push --force-with-lease --force-if-includes                                                         |
+      |          | git checkout new                                                                                        |
+    And no rebase is now in progress
+    And this lineage exists now
+      """
+      main
+        existing
+        new
+      """
+    And these commits exist now
+      | BRANCH   | LOCATION      | MESSAGE     |
+      | main     | origin        | main commit |
+      | existing | local, origin | commit 1    |
+      | new      | local         | commit 2    |
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH   | COMMAND                                                                |
+      | new      | git checkout existing                                                  |
+      | existing | git reset --hard {{ sha-initial 'commit 2' }}                          |
+      |          | git push --force-with-lease origin {{ sha 'initial commit' }}:existing |
+      |          | git branch -D new                                                      |
+    And the initial branches and lineage exist now
+    And the initial commits exist now
+
+  Scenario: amend the beamed commit
+    And I amend this commit
+      | BRANCH | LOCATION | MESSAGE   | FILE NAME | FILE CONTENT    |
+      | new    | local    | commit 2b | file_2    | amended content |
+    And the current branch is "new"
+    When I run "git-town sync"
+    Then Git Town runs the commands
+      | BRANCH | COMMAND                                           |
+      | new    | git fetch --prune --tags                          |
+      |        | git checkout main                                 |
+      | main   | git -c rebase.updateRefs=false rebase origin/main |
+      |        | git checkout new                                  |
+      | new    | git merge --no-edit --ff main                     |
+      |        | git push -u origin new                            |
+    And these commits exist now
+      | BRANCH   | LOCATION      | MESSAGE                      |
+      | main     | local, origin | main commit                  |
+      | existing | local, origin | commit 1                     |
+      | new      | local, origin | commit 2b                    |
+      |          |               | Merge branch 'main' into new |
diff --git a/features/hack/beam/worktree_with_main.feature b/features/hack/beam/worktree_with_main.feature
new file mode 100644
index 000000000..20680d4e3
--- /dev/null
+++ b/features/hack/beam/worktree_with_main.feature
@@ -0,0 +1,56 @@
+@messyoutput
+Feature: beam commits from a branch on the same worktree as main
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME       | TYPE    | PARENT | LOCATIONS     |
+      | existing-1 | (none)  |        | local, origin |
+      | existing-2 | feature | main   | local, origin |
+    And the commits
+      | BRANCH     | LOCATION | MESSAGE     |
+      | main       | origin   | main commit |
+      | existing-1 | local    | commit 1a   |
+      | existing-1 | local    | commit 1b   |
+      | existing-2 | local    | commit 2    |
+    And the current branch is "existing-1"
+    And branch "existing-2" is active in another worktree
+    When I run "git-town hack new --beam" and enter into the dialog:
+      | DIALOG                         | KEYS             |
+      | parent branch for "existing-1" | enter            |
+      | commits to beam                | down space enter |
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH     | COMMAND                                                                                                   |
+      | existing-1 | git checkout -b new main                                                                                  |
+      | new        | git cherry-pick {{ sha-initial 'commit 1b' }}                                                             |
+      |            | git checkout existing-1                                                                                   |
+      | existing-1 | git -c rebase.updateRefs=false rebase --onto {{ sha-initial 'commit 1b' }}^ {{ sha-initial 'commit 1b' }} |
+      |            | git push --force-with-lease --force-if-includes                                                           |
+      |            | git checkout new                                                                                          |
+    And no rebase is now in progress
+    And this lineage exists now
+      """
+      main
+        existing-1
+        existing-2
+        new
+      """
+    And these commits exist now
+      | BRANCH     | LOCATION                | MESSAGE     |
+      | main       | origin                  | main commit |
+      | existing-1 | local, origin, worktree | commit 1a   |
+      | existing-2 | worktree                | commit 2    |
+      | new        | local                   | commit 1b   |
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH     | COMMAND                                                                  |
+      | new        | git checkout existing-1                                                  |
+      | existing-1 | git reset --hard {{ sha-initial 'commit 1b' }}                           |
+      |            | git push --force-with-lease origin {{ sha 'initial commit' }}:existing-1 |
+      |            | git branch -D new                                                        |
+    And the initial branches and lineage exist now
+    And the initial commits exist now
diff --git a/features/hack/beam/worktree_without_main.feature b/features/hack/beam/worktree_without_main.feature
new file mode 100644
index 000000000..783ab860c
--- /dev/null
+++ b/features/hack/beam/worktree_without_main.feature
@@ -0,0 +1,52 @@
+@messyoutput
+Feature: beam commits from a branch on a worktree different from main
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME       | TYPE   | PARENT | LOCATIONS     |
+      | existing-1 | (none) |        | local, origin |
+    And the commits
+      | BRANCH     | LOCATION | MESSAGE     |
+      | main       | origin   | main commit |
+      | existing-1 | local    | commit 1a   |
+      | existing-1 | local    | commit 1b   |
+    And the current branch is "existing-1"
+    And branch "main" is active in another worktree
+    When I run "git-town hack new --beam" and enter into the dialog:
+      | DIALOG                         | KEYS             |
+      | parent branch for "existing-1" | enter            |
+      | commits to beam                | down space enter |
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH     | COMMAND                                                                                                   |
+      | existing-1 | git checkout -b new main                                                                                  |
+      | new        | git cherry-pick {{ sha-initial 'commit 1b' }}                                                             |
+      |            | git checkout existing-1                                                                                   |
+      | existing-1 | git -c rebase.updateRefs=false rebase --onto {{ sha-initial 'commit 1b' }}^ {{ sha-initial 'commit 1b' }} |
+      |            | git push --force-with-lease --force-if-includes                                                           |
+      |            | git checkout new                                                                                          |
+    And no rebase is now in progress
+    And this lineage exists now
+      """
+      main
+        existing-1
+        new
+      """
+    And these commits exist now
+      | BRANCH     | LOCATION                | MESSAGE     |
+      | main       | origin                  | main commit |
+      | existing-1 | local, origin, worktree | commit 1a   |
+      | new        | local                   | commit 1b   |
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH     | COMMAND                                                                  |
+      | new        | git checkout existing-1                                                  |
+      | existing-1 | git reset --hard {{ sha-initial 'commit 1b' }}                           |
+      |            | git push --force-with-lease origin {{ sha 'initial commit' }}:existing-1 |
+      |            | git branch -D new                                                        |
+    And the initial branches and lineage exist now
+    And the initial commits exist now
diff --git a/features/hack/branch_prefix/config_file.feature b/features/hack/branch_prefix/config_file.feature
new file mode 100644
index 000000000..9391afe97
--- /dev/null
+++ b/features/hack/branch_prefix/config_file.feature
@@ -0,0 +1,47 @@
+Feature: hack with configured branch-prefix via config file
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the committed configuration file:
+      """
+      [create]
+      branch-prefix = "kg-"
+      """
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town hack <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                      |
+      | feature-1 | git fetch --prune --tags     |
+      |           | git checkout main            |
+      | main      | git checkout -b kg-feature-2 |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        feature-1
+        kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town hack feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/hack/branch_prefix/env_var.feature b/features/hack/branch_prefix/env_var.feature
new file mode 100644
index 000000000..6126fd2ba
--- /dev/null
+++ b/features/hack/branch_prefix/env_var.feature
@@ -0,0 +1,44 @@
+Feature: hack with configured branch-prefix via environment variable
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town hack <BRANCH_NAME>" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                      |
+      | feature-1 | git fetch --prune --tags     |
+      |           | git checkout main            |
+      | main      | git checkout -b kg-feature-2 |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        feature-1
+        kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I run "git-town hack feature-2" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/hack/branch_prefix/git_metadata.feature b/features/hack/branch_prefix/git_metadata.feature
new file mode 100644
index 000000000..8a7db473a
--- /dev/null
+++ b/features/hack/branch_prefix/git_metadata.feature
@@ -0,0 +1,43 @@
+Feature: hack with configured branch-prefix via Git metadata
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And Git setting "git-town.branch-prefix" is "kg-"
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town hack <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                      |
+      | feature-1 | git fetch --prune --tags     |
+      |           | git checkout main            |
+      | main      | git checkout -b kg-feature-2 |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        feature-1
+        kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town hack feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/init/default_values/long.feature b/features/init/default_values/long.feature
index d4b671a6c..32005b3a0 100644
--- a/features/init/default_values/long.feature
+++ b/features/init/default_values/long.feature
@@ -22,6 +22,7 @@ Feature: Accepting all default values leads to a working setup
       | feature regex               | enter      |
       | contribution regex          | enter      |
       | observed regex              | enter      |
+      | branch prefix               | enter      |
       | new branch type             | enter      |
       | unknown branch type         | enter      |
       | sync feature strategy       | enter      |
@@ -38,17 +39,19 @@ Feature: Accepting all default values leads to a working setup
       | ship-strategy               | enter      |
       | ship-delete-tracking branch | enter      |
       | order                       | enter      |
+      | proposals show lineage      | enter      |
       | config storage              | enter      |
 
   Scenario: result
     Then Git Town runs the commands
       | COMMAND                                              |
+      | git config git-town.main-branch main                 |
       | git config git-town.auto-sync true                   |
       | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
-      | git config git-town.main-branch main                 |
       | git config git-town.unknown-branch-type feature      |
       | git config git-town.order asc                        |
+      | git config git-town.proposals-show-lineage none      |
       | git config git-town.push-branches true               |
       | git config git-town.push-hook true                   |
       | git config git-town.share-new-branches no            |
@@ -83,6 +86,8 @@ Feature: Accepting all default values leads to a working setup
     And local Git setting "git-town.forge-type" still doesn't exist
     And local Git setting "git-town.github-token" still doesn't exist
     And local Git setting "git-town.hosting-origin-hostname" still doesn't exist
+    And local Git setting "git-town.branch-prefix" still doesn't exist
+    And local Git setting "git-town.proposals-show-lineage" still doesn't exist
     And local Git setting "git-town.feature-regex" now doesn't exist
     And local Git setting "git-town.contribution-regex" now doesn't exist
     And local Git setting "git-town.observed-regex" now doesn't exist
diff --git a/features/init/existing_configuration/change_basic.feature b/features/init/existing_configuration/change_basic.feature
new file mode 100644
index 000000000..f6f55a9cb
--- /dev/null
+++ b/features/init/existing_configuration/change_basic.feature
@@ -0,0 +1,121 @@
+@messyoutput
+Feature: don't change existing extended information when changing basic information
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME       | TYPE      | LOCATIONS     |
+      | qa         | perennial | local, origin |
+      | production | (none)    | local, origin |
+    And the main branch is "main"
+    And local Git setting "git-town.new-branch-type" is "parked"
+    And local Git setting "git-town.share-new-branches" is "no"
+    And local Git setting "git-town.order" is "desc"
+    And local Git setting "git-town.push-branches" is "false"
+    And local Git setting "git-town.push-hook" is "false"
+    And local Git setting "git-town.auto-sync" is "false"
+    And local Git setting "git-town.sync-tags" is "false"
+    And local Git setting "git-town.detached" is "false"
+    And local Git setting "git-town.sync-feature-strategy" is "rebase"
+    And local Git setting "git-town.sync-perennial-strategy" is "rebase"
+    And local Git setting "git-town.sync-prototype-strategy" is "rebase"
+    And local Git setting "git-town.sync-upstream" is "false"
+    And local Git setting "git-town.ship-delete-tracking-branch" is "false"
+    And local Git setting "git-town.perennial-regex" is "per"
+    And local Git setting "git-town.feature-regex" is "feat"
+    And local Git setting "git-town.contribution-regex" is "cont"
+    And local Git setting "git-town.observed-regex" is "obs"
+    And local Git setting "git-town.unknown-branch-type" is "observed"
+    And local Git setting "git-town.share-new-branches" is "push"
+    And local Git setting "git-town.push-branches" is "true"
+    And local Git setting "git-town.push-hook" is "true"
+    And local Git setting "git-town.ship-strategy" is "fast-forward"
+    And local Git setting "git-town.ship-delete-tracking-branch" is "true"
+    And local Git setting "git-town.proposals-show-lineage" is "cli"
+    When I run "git-town init" and enter into the dialogs:
+      | DIALOG                | KEYS                   |
+      | welcome               | enter                  |
+      | aliases               | enter                  |
+      | main branch           | enter                  |
+      | perennial branches    | space down space enter |
+      | origin hostname       | c o d e enter          |
+      | forge type            | up up enter            |
+      | github connector type | enter                  |
+      | github token          | g h - t o k enter      |
+      | token scope           | enter                  |
+      | enter all             | enter                  |
+      | config storage        | enter                  |
+
+  Scenario: result
+    Then Git Town runs the commands
+      | COMMAND                                                |
+      | git config git-town.github-token gh-tok                |
+      | git config git-town.perennial-branches "production qa" |
+      | git config git-town.hosting-origin-hostname code       |
+      | git config git-town.forge-type github                  |
+      | git config git-town.github-connector api               |
+    And local Git setting "git-town.auto-sync" is still "false"
+    And local Git setting "git-town.detached" is still "false"
+    And local Git setting "git-town.perennial-branches" is now "production qa"
+    And local Git setting "git-town.new-branch-type" is still "parked"
+    And local Git setting "git-town.forge-type" is now "github"
+    And local Git setting "git-town.github-token" is now "gh-tok"
+    And local Git setting "git-town.hosting-origin-hostname" is now "code"
+    And local Git setting "git-town.sync-feature-strategy" is still "rebase"
+    And local Git setting "git-town.sync-perennial-strategy" is still "rebase"
+    And local Git setting "git-town.sync-prototype-strategy" is still "rebase"
+    And local Git setting "git-town.sync-upstream" is still "false"
+    And local Git setting "git-town.sync-tags" is still "false"
+    And local Git setting "git-town.perennial-regex" is still "per"
+    And local Git setting "git-town.feature-regex" is still "feat"
+    And local Git setting "git-town.contribution-regex" is still "cont"
+    And local Git setting "git-town.observed-regex" is still "obs"
+    And local Git setting "git-town.unknown-branch-type" is still "observed"
+    And local Git setting "git-town.share-new-branches" is still "push"
+    And local Git setting "git-town.push-branches" is still "true"
+    And local Git setting "git-town.push-hook" is still "true"
+    And local Git setting "git-town.ship-strategy" is still "fast-forward"
+    And local Git setting "git-town.ship-delete-tracking-branch" is still "true"
+    And local Git setting "git-town.proposals-show-lineage" is still "cli"
+    And local Git setting "git-town.stash" still doesn't exist
+    And local Git setting "git-town.dev-remote" still doesn't exist
+    And the main branch is now "main"
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs no commands
+    And global Git setting "alias.append" still doesn't exist
+    And global Git setting "alias.diff-parent" still doesn't exist
+    And global Git setting "alias.hack" still doesn't exist
+    And global Git setting "alias.delete" still doesn't exist
+    And global Git setting "alias.prepend" still doesn't exist
+    And global Git setting "alias.propose" still doesn't exist
+    And global Git setting "alias.rename" still doesn't exist
+    And global Git setting "alias.repo" still doesn't exist
+    And global Git setting "alias.set-parent" still doesn't exist
+    And global Git setting "alias.ship" still doesn't exist
+    And global Git setting "alias.sync" still doesn't exist
+    And local Git setting "git-town.auto-sync" is still "false"
+    And local Git setting "git-town.detached" is still "false"
+    And local Git setting "git-town.new-branch-type" is still "parked"
+    And local Git setting "git-town.share-new-branches" is still "push"
+    And local Git setting "git-town.push-branches" is still "true"
+    And local Git setting "git-town.push-hook" is still "true"
+    And local Git setting "git-town.ship-delete-tracking-branch" is still "true"
+    And local Git setting "git-town.sync-feature-strategy" is still "rebase"
+    And local Git setting "git-town.sync-perennial-strategy" is still "rebase"
+    And local Git setting "git-town.sync-prototype-strategy" is still "rebase"
+    And local Git setting "git-town.sync-upstream" is still "false"
+    And local Git setting "git-town.perennial-regex" is still "per"
+    And local Git setting "git-town.feature-regex" is still "feat"
+    And local Git setting "git-town.contribution-regex" is still "cont"
+    And local Git setting "git-town.observed-regex" is still "obs"
+    And local Git setting "git-town.unknown-branch-type" is still "observed"
+    And local Git setting "git-town.ship-strategy" is still "fast-forward"
+    And local Git setting "git-town.proposals-show-lineage" is still "cli"
+    And local Git setting "git-town.hosting-origin-hostname" now doesn't exist
+    And local Git setting "git-town.forge-type" now doesn't exist
+    And local Git setting "git-town.github-token" now doesn't exist
+    And local Git setting "git-town.stash" still doesn't exist
+    And the main branch is now "main"
+    And the perennial branches are now "qa"
diff --git a/features/init/existing_configuration/change_git_metadata.feature b/features/init/existing_configuration/change_git_metadata.feature
index adf983a17..678b0a88a 100644
--- a/features/init/existing_configuration/change_git_metadata.feature
+++ b/features/init/existing_configuration/change_git_metadata.feature
@@ -33,6 +33,7 @@ Feature: change existing information in Git metadata
       | feature regex               | f e a t enter          |
       | contribution regex          | c o n t enter          |
       | observed regex              | o b s enter            |
+      | branch prefix               | k g - enter            |
       | new branch type             | down enter             |
       | unknown branch type         | down enter             |
       | sync feature strategy       | down enter             |
@@ -49,6 +50,7 @@ Feature: change existing information in Git metadata
       | ship strategy               | down down enter        |
       | ship delete tracking branch | down enter             |
       | order                       | up enter               |
+      | proposals show lineage      | down enter             |
       | config storage              | enter                  |
 
   Scenario: result
@@ -72,19 +74,21 @@ Feature: change existing information in Git metadata
       | git config --global alias.sync "town sync"               |
       | git config --global alias.up "town up"                   |
       | git config git-town.github-token gh-tok                  |
+      | git config git-town.perennial-branches "production qa"   |
+      | git config git-town.hosting-origin-hostname code         |
+      | git config git-town.forge-type github                    |
+      | git config git-town.github-connector api                 |
       | git config git-town.auto-sync true                       |
+      | git config git-town.branch-prefix kg-                    |
       | git config git-town.detached true                        |
       | git config git-town.new-branch-type prototype            |
-      | git config git-town.forge-type github                    |
-      | git config git-town.github-connector api                 |
-      | git config git-town.hosting-origin-hostname code         |
-      | git config git-town.perennial-branches "production qa"   |
       | git config git-town.perennial-regex per                  |
       | git config git-town.unknown-branch-type observed         |
       | git config git-town.feature-regex feat                   |
       | git config git-town.contribution-regex cont              |
       | git config git-town.observed-regex obs                   |
       | git config git-town.order asc                            |
+      | git config git-town.proposals-show-lineage cli           |
       | git config git-town.push-branches true                   |
       | git config git-town.push-hook true                       |
       | git config git-town.share-new-branches push              |
@@ -128,6 +132,7 @@ Feature: change existing information in Git metadata
     And local Git setting "git-town.push-hook" is now "true"
     And local Git setting "git-town.ship-strategy" is now "fast-forward"
     And local Git setting "git-town.ship-delete-tracking-branch" is now "true"
+    And local Git setting "git-town.proposals-show-lineage" is now "cli"
     And local Git setting "git-town.dev-remote" still doesn't exist
     And the main branch is now "main"
 
@@ -165,5 +170,6 @@ Feature: change existing information in Git metadata
     And local Git setting "git-town.observed-regex" now doesn't exist
     And local Git setting "git-town.unknown-branch-type" now doesn't exist
     And local Git setting "git-town.ship-strategy" now doesn't exist
+    And local Git setting "git-town.proposals-show-lineage" now doesn't exist
     And the main branch is now "main"
     And the perennial branches are now "qa"
diff --git a/features/init/existing_configuration/config_file_complete.feature b/features/init/existing_configuration/config_file_complete.feature
index b91182356..32c7c9213 100644
--- a/features/init/existing_configuration/config_file_complete.feature
+++ b/features/init/existing_configuration/config_file_complete.feature
@@ -16,6 +16,7 @@ Feature: don't ask for information already provided by the config file
       order = "desc"
 
       [create]
+      branch-prefix = "acme-"
       new-branch-type = "feature"
       share-new-branches = "propose"
       stash = true
@@ -25,6 +26,9 @@ Feature: don't ask for information already provided by the config file
       origin-hostname = "github.com"
       forge-type = "github"
 
+      [propose]
+      lineage = "none"
+
       [ship]
       delete-tracking-branch = true
       strategy = "api"
diff --git a/features/init/existing_configuration/config_file_partial.feature b/features/init/existing_configuration/config_file_partial.feature
index 57edab2ea..0a3caa4e7 100644
--- a/features/init/existing_configuration/config_file_partial.feature
+++ b/features/init/existing_configuration/config_file_partial.feature
@@ -40,6 +40,7 @@ Feature: ask for information not provided by the config file
       | feature regex           | f e a t enter         |
       | contribution regex      | c o n t enter         |
       | observed regex          | o b s enter           |
+      | branch prefix           | a c m e - enter       |
       | new branch type         | enter                 |
       | unknown branch type     | enter                 |
       | sync feature strategy   | enter                 |
@@ -51,19 +52,22 @@ Feature: ask for information not provided by the config file
       | push branches           | enter                 |
       | push hook               | enter                 |
       | ship strategy           | enter                 |
+      | proposals show lineage  | enter                 |
       | config storage          | enter                 |
     Then Git Town runs the commands
       | COMMAND                                             |
       | git config git-town.github-token gh-token           |
+      | git config git-town.perennial-branches branch-1     |
+      | git config git-town.github-connector api            |
+      | git config git-town.branch-prefix acme-             |
       | git config git-town.detached false                  |
       | git config git-town.new-branch-type feature         |
-      | git config git-town.github-connector api            |
-      | git config git-town.perennial-branches branch-1     |
       | git config git-town.perennial-regex peren           |
       | git config git-town.unknown-branch-type feature     |
       | git config git-town.feature-regex feat              |
       | git config git-town.contribution-regex cont         |
       | git config git-town.observed-regex obs              |
+      | git config git-town.proposals-show-lineage none     |
       | git config git-town.push-branches true              |
       | git config git-town.push-hook true                  |
       | git config git-town.share-new-branches no           |
diff --git a/features/init/existing_configuration/config_file_update.feature b/features/init/existing_configuration/config_file_update.feature
new file mode 100644
index 000000000..a96bc9dc1
--- /dev/null
+++ b/features/init/existing_configuration/config_file_update.feature
@@ -0,0 +1,108 @@
+@messyoutput
+Feature: don't ask for information already provided by the config file
+
+  Scenario:
+    Given a Git repo with origin
+    And the committed configuration file:
+      """
+      [branches]
+      main = "main"
+      contribution-regex = "contribute-"
+      feature-regex = "feat-"
+      observed-regex = "observed-"
+      perennial-regex = "release-"
+      perennials = ["staging"]
+      unknown-type = "observed"
+      order = "desc"
+
+      [create]
+      branch-prefix = "acme-"
+      new-branch-type = "feature"
+      share-new-branches = "propose"
+      stash = true
+
+      [hosting]
+      dev-remote = "something"
+      origin-hostname = "github.com"
+      forge-type = "github"
+
+      [propose]
+      lineage = "none"
+
+      [ship]
+      delete-tracking-branch = true
+      strategy = "api"
+
+      [sync]
+      auto-sync = false
+      detached = false
+      feature-strategy = "merge"
+      perennial-strategy = "rebase"
+      push-branches = false
+      push-hook = true
+      tags = true
+      upstream = true
+
+      [sync-strategy]
+      feature-branches = "rebase"
+      prototype-branches = "merge"
+      perennial-branches = "ff-only"
+      """
+    And Git Town is not configured
+    When I run "git-town init" and enter into the dialogs:
+      | DIALOG                | KEYS              |
+      | welcome               | enter             |
+      | aliases               | enter             |
+      | perennial branches    | enter             |
+      | github connector type | enter             |
+      | github token          | g h - t o k enter |
+      | token scope           | enter             |
+      | enter all             | down enter        |
+      | config storage        | down enter        |
+    Then Git Town runs the commands
+      | COMMAND                                 |
+      | git config git-town.github-token gh-tok |
+    And the configuration file is now:
+      """
+      # See https://www.git-town.com/configuration-file for details
+
+      [branches]
+      contribution-regex = "contribute-"
+      feature-regex = "feat-"
+      main = "main"
+      observed-regex = "observed-"
+      order = "desc"
+      perennials = ["staging"]
+      perennial-regex = "release-"
+      unknown-branch-type = "observed"
+
+      [create]
+      branch-prefix = "acme-"
+      new-branch-type = "feature"
+      share-new-branches = "propose"
+      stash = true
+
+      [hosting]
+      dev-remote = "something"
+      forge-type = "github"
+      github-connector-type = "api"
+      origin-hostname = "github.com"
+
+      [propose]
+      lineage = "none"
+
+      [ship]
+      delete-tracking-branch = true
+      strategy = "api"
+
+      [sync]
+      auto-sync = false
+      detached = false
+      feature-strategy = "merge"
+      perennial-strategy = "rebase"
+      prototype-strategy = "merge"
+      push-branches = false
+      push-hook = true
+      tags = true
+      upstream = true
+      """
diff --git a/features/init/existing_configuration/git_global.feature b/features/init/existing_configuration/git_global.feature
index 145cd8d06..fd8b9c42e 100644
--- a/features/init/existing_configuration/git_global.feature
+++ b/features/init/existing_configuration/git_global.feature
@@ -8,6 +8,7 @@ Feature: setup a new repo when I have configured some things in global Git metad
       | branch-1 | feature | main   | local     |
     And Git Town is not configured
     And global Git setting "git-town.auto-sync" is "false"
+    And global Git setting "git-town.branch-prefix" is "kg-"
     And global Git setting "git-town.feature-regex" is "^feat-"
     And global Git setting "git-town.contribution-regex" is "^cont-"
     And global Git setting "git-town.observed-regex" is "^obs-"
@@ -27,6 +28,7 @@ Feature: setup a new repo when I have configured some things in global Git metad
     And global Git setting "git-town.sync-feature-strategy" is "rebase"
     And global Git setting "git-town.sync-perennial-strategy" is "ff-only"
     And global Git setting "git-town.sync-prototype-strategy" is "compress"
+    And global Git setting "git-town.proposals-show-lineage" is "cli"
     And global Git setting "git-town.sync-tags" is "false"
     And global Git setting "git-town.sync-upstream" is "false"
     And global Git setting "git-town.detached" is "false"
@@ -44,6 +46,7 @@ Feature: setup a new repo when I have configured some things in global Git metad
       | feature regex               | enter       |
       | contribution regex          | enter       |
       | observed regex              | enter       |
+      | branch prefix               | enter       |
       | new branch type             | enter       |
       | unknown branch type         | enter       |
       | sync feature strategy       | enter       |
@@ -60,6 +63,7 @@ Feature: setup a new repo when I have configured some things in global Git metad
       | ship strategy               | enter       |
       | ship delete tracking branch | enter       |
       | order                       | enter       |
+      | proposals show lineage      | enter       |
       | config storage              | enter       |
     Then Git Town runs the commands
       | COMMAND                                         |
diff --git a/features/init/existing_configuration/invalid_config_data.feature b/features/init/existing_configuration/invalid_config_data.feature
new file mode 100644
index 000000000..88f9284d7
--- /dev/null
+++ b/features/init/existing_configuration/invalid_config_data.feature
@@ -0,0 +1,163 @@
+@messyoutput
+Feature: Fix invalid configuration data
+
+  Background:
+    Given a Git repo with origin
+    And Git Town is not configured
+    And local Git setting "init.defaultbranch" is "main"
+    And local Git setting "git-town.feature-regex" is "(feat"
+    And local Git setting "git-town.perennial-regex" is "(per"
+    And local Git setting "git-town.contribution-regex" is "(cont"
+    And local Git setting "git-town.observed-regex" is "(obs"
+    And local Git setting "git-town.new-branch-type" is "zonk"
+    And local Git setting "git-town.branch-prefix" is "xx"
+    And local Git setting "git-town.unknown-branch-type" is "zonk"
+    And local Git setting "git-town.sync-feature-strategy" is "--help"
+    And local Git setting "git-town.sync-perennial-strategy" is "zonk"
+    And local Git setting "git-town.sync-prototype-strategy" is "zonk"
+    And local Git setting "git-town.sync-upstream" is "zonk"
+    And local Git setting "git-town.auto-sync" is "zonk"
+    And local Git setting "git-town.sync-tags" is "zonk"
+    And local Git setting "git-town.detached" is "zonk"
+    And local Git setting "git-town.stash" is "zonk"
+    And local Git setting "git-town.share-new-branches" is "zonk"
+    And local Git setting "git-town.push-branches" is "zonk"
+    And local Git setting "git-town.push-hook" is "zonk"
+    And local Git setting "git-town.ship-strategy" is "zonk"
+    And local Git setting "git-town.ship-delete-tracking-branch" is "zonk"
+    And local Git setting "git-town.order" is "zonk"
+    When I run "git-town init" and enter into the dialogs:
+      | DIALOG                      | KEYS                          |
+      | welcome                     | enter                         |
+      | aliases                     | enter                         |
+      | main branch                 | enter                         |
+      | origin hostname             | enter                         |
+      | forge type                  | enter                         |
+      | enter all                   | down enter                    |
+      | perennial regex             | p e r enter                   |
+      | feature regex               | f e a t enter                 |
+      | contribution regex          | c o n t enter                 |
+      | observed regex              | o b s enter                   |
+      | branch prefix               | backspace backspace a b enter |
+      | new branch type             | down enter                    |
+      | unknown branch type         | down enter                    |
+      | sync feature strategy       | down enter                    |
+      | sync perennial strategy     | down enter                    |
+      | sync prototype strategy     | down enter                    |
+      | sync upstream               | down enter                    |
+      | auto-sync                   | down enter                    |
+      | sync-tags                   | down enter                    |
+      | detached                    | down enter                    |
+      | stash                       | down enter                    |
+      | share-new-branches          | down enter                    |
+      | push-branches               | down enter                    |
+      | push-hook                   | down enter                    |
+      | ship-strategy               | down enter                    |
+      | ship-delete-tracking branch | down enter                    |
+      | order                       | down enter                    |
+      | proposals show lineage      | enter                         |
+      | config storage              | enter                         |
+
+  Scenario: result
+    Then Git Town runs the commands
+      | COMMAND                                               |
+      | git config git-town.main-branch main                  |
+      | git config git-town.auto-sync false                   |
+      | git config git-town.branch-prefix ab                  |
+      | git config git-town.detached true                     |
+      | git config git-town.new-branch-type parked            |
+      | git config git-town.perennial-regex per               |
+      | git config git-town.unknown-branch-type observed      |
+      | git config git-town.feature-regex feat                |
+      | git config git-town.contribution-regex cont           |
+      | git config git-town.observed-regex obs                |
+      | git config git-town.order desc                        |
+      | git config git-town.proposals-show-lineage none       |
+      | git config git-town.push-branches false               |
+      | git config git-town.push-hook false                   |
+      | git config git-town.share-new-branches push           |
+      | git config git-town.ship-strategy always-merge        |
+      | git config git-town.ship-delete-tracking-branch false |
+      | git config git-town.stash false                       |
+      | git config git-town.sync-feature-strategy rebase      |
+      | git config git-town.sync-perennial-strategy rebase    |
+      | git config git-town.sync-prototype-strategy rebase    |
+      | git config git-town.sync-upstream false               |
+      | git config git-town.sync-tags false                   |
+    And Git Town prints:
+      """
+      Ignoring invalid value for "git-town.auto-sync": "zonk"
+      Ignoring invalid value for "git-town.contribution-regex": "(cont"
+      Ignoring invalid value for "git-town.detached": "zonk"
+      Ignoring invalid value for "git-town.feature-regex": "(feat"
+      Ignoring invalid value for "git-town.new-branch-type": "zonk"
+      Ignoring invalid value for "git-town.observed-regex": "(obs"
+      Ignoring invalid value for "git-town.order": "zonk"
+      Ignoring invalid value for "git-town.perennial-regex": "(per"
+      Ignoring invalid value for "git-town.push-branches": "zonk"
+      Ignoring invalid value for "git-town.push-hook": "zonk"
+      Ignoring invalid value for "git-town.share-new-branches": "zonk"
+      Ignoring invalid value for "git-town.ship-delete-tracking-branch": "zonk"
+      Ignoring invalid value for "git-town.ship-strategy": "zonk"
+      Ignoring invalid value for "git-town.stash": "zonk"
+      Ignoring invalid value for "git-town.sync-feature-strategy": "--help"
+      Ignoring invalid value for "git-town.sync-perennial-strategy": "zonk"
+      Ignoring invalid value for "git-town.sync-prototype-strategy": "zonk"
+      Ignoring invalid value for "git-town.sync-tags": "zonk"
+      Ignoring invalid value for "git-town.sync-upstream": "zonk"
+      Ignoring invalid value for "git-town.unknown-branch-type": "zonk"
+      """
+    And local Git setting "git-town.feature-regex" is now "feat"
+    And local Git setting "git-town.perennial-regex" is now "per"
+    And local Git setting "git-town.contribution-regex" is now "cont"
+    And local Git setting "git-town.observed-regex" is now "obs"
+    And local Git setting "git-town.new-branch-type" is now "parked"
+    And local Git setting "git-town.unknown-branch-type" is now "observed"
+    And local Git setting "git-town.sync-feature-strategy" is now "rebase"
+    And local Git setting "git-town.sync-perennial-strategy" is now "rebase"
+    And local Git setting "git-town.sync-prototype-strategy" is now "rebase"
+    And local Git setting "git-town.sync-upstream" is now "false"
+    And local Git setting "git-town.auto-sync" is now "false"
+    And local Git setting "git-town.sync-tags" is now "false"
+    And local Git setting "git-town.detached" is now "true"
+    And local Git setting "git-town.stash" is now "false"
+    And local Git setting "git-town.share-new-branches" is now "push"
+    And local Git setting "git-town.push-branches" is now "false"
+    And local Git setting "git-town.push-hook" is now "false"
+    And local Git setting "git-town.ship-strategy" is now "always-merge"
+    And local Git setting "git-town.ship-delete-tracking-branch" is now "false"
+    And local Git setting "git-town.order" is now "desc"
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then global Git setting "alias.append" now doesn't exist
+    And global Git setting "alias.diff-parent" now doesn't exist
+    And global Git setting "alias.hack" now doesn't exist
+    And global Git setting "alias.delete" now doesn't exist
+    And global Git setting "alias.prepend" now doesn't exist
+    And global Git setting "alias.propose" now doesn't exist
+    And global Git setting "alias.rename" now doesn't exist
+    And global Git setting "alias.repo" now doesn't exist
+    And global Git setting "alias.set-parent" now doesn't exist
+    And global Git setting "alias.ship" now doesn't exist
+    And global Git setting "alias.sync" now doesn't exist
+    And local Git setting "git-town.sync-feature-strategy" is now "--help"
+    And local Git setting "git-town.new-branch-type" is now "zonk"
+    And local Git setting "git-town.feature-regex" is now "(feat"
+    And local Git setting "git-town.contribution-regex" is now "(cont"
+    And local Git setting "git-town.observed-regex" is now "(obs"
+    And local Git setting "git-town.stash" is now "zonk"
+    And local Git setting "git-town.sync-perennial-strategy" is now "zonk"
+    And local Git setting "git-town.sync-upstream" is now "zonk"
+    And local Git setting "git-town.sync-tags" is now "zonk"
+    And local Git setting "git-town.perennial-regex" is now "(per"
+    And local Git setting "git-town.share-new-branches" is now "zonk"
+    And local Git setting "git-town.push-hook" is now "zonk"
+    And local Git setting "git-town.ship-strategy" is now "zonk"
+    And local Git setting "git-town.ship-delete-tracking-branch" is now "zonk"
+    And local Git setting "git-town.dev-remote" now doesn't exist
+    And local Git setting "git-town.main-branch" now doesn't exist
+    And local Git setting "git-town.perennial-branches" now doesn't exist
+    And local Git setting "git-town.forge-type" now doesn't exist
+    And local Git setting "git-town.github-token" now doesn't exist
+    And local Git setting "git-town.hosting-origin-hostname" now doesn't exist
diff --git a/features/init/existing_configuration/migrate_git_to_file.feature b/features/init/existing_configuration/migrate_git_to_file.feature
index 052c4f241..f0a5939ac 100644
--- a/features/init/existing_configuration/migrate_git_to_file.feature
+++ b/features/init/existing_configuration/migrate_git_to_file.feature
@@ -16,6 +16,7 @@ Feature: migrate existing configuration in Git metadata to a config file
     And local Git setting "git-town.push-hook" is "true"
     And local Git setting "git-town.new-branch-type" is "prototype"
     And local Git setting "git-town.order" is "desc"
+    And local Git setting "git-town.branch-prefix" is "acme-"
     And local Git setting "git-town.ship-strategy" is "squash-merge"
     And local Git setting "git-town.ship-delete-tracking-branch" is "false"
     And local Git setting "git-town.stash" is "false"
@@ -23,6 +24,7 @@ Feature: migrate existing configuration in Git metadata to a config file
     And local Git setting "git-town.sync-perennial-strategy" is "rebase"
     And local Git setting "git-town.sync-upstream" is "true"
     And local Git setting "git-town.sync-tags" is "false"
+    And local Git setting "git-town.proposals-show-lineage" is "cli"
     And local Git setting "git-town.unknown-branch-type" is "observed"
     When I run "git-town init" and enter into the dialogs:
       | DIALOG                      | KEYS       |
@@ -38,6 +40,7 @@ Feature: migrate existing configuration in Git metadata to a config file
       | feature regex               | enter      |
       | contribution regex          | enter      |
       | observed regex              | enter      |
+      | branch prefix               | enter      |
       | new branch type             | enter      |
       | unknown branch type         | enter      |
       | sync feature strategy       | enter      |
@@ -54,12 +57,14 @@ Feature: migrate existing configuration in Git metadata to a config file
       | ship strategy               | enter      |
       | ship delete tracking branch | enter      |
       | order                       | enter      |
+      | proposals show lineage      | enter      |
       | config storage              | down enter |
 
   Scenario: result
     Then Git Town runs the commands
       | COMMAND                                                 |
       | git config --unset git-town.auto-sync                   |
+      | git config --unset git-town.branch-prefix               |
       | git config --unset git-town.contribution-regex          |
       | git config --unset git-town.dev-remote                  |
       | git config --unset git-town.feature-regex               |
@@ -70,6 +75,7 @@ Feature: migrate existing configuration in Git metadata to a config file
       | git config --unset git-town.perennial-branches          |
       | git config --unset git-town.perennial-regex             |
       | git config --unset git-town.share-new-branches          |
+      | git config --unset git-town.proposals-show-lineage      |
       | git config --unset git-town.push-branches               |
       | git config --unset git-town.push-hook                   |
       | git config --unset git-town.ship-strategy               |
@@ -99,18 +105,24 @@ Feature: migrate existing configuration in Git metadata to a config file
     And local Git setting "git-town.new-branch-type" now doesn't exist
     And local Git setting "git-town.ship-strategy" now doesn't exist
     And local Git setting "git-town.ship-delete-tracking-branch" now doesn't exist
+    And local Git setting "git-town.proposals-show-lineage" now doesn't exist
     And local Git setting "git-town.stash" now doesn't exist
     And the configuration file is now:
       """
       # See https://www.git-town.com/configuration-file for details
 
       [branches]
+      contribution-regex = "coworker-.*"
+      feature-regex = "user-.*"
       main = "main"
+      observed-regex = "other-.*"
       order = "desc"
       perennials = ["qa"]
       perennial-regex = "release-.*"
+      unknown-branch-type = "observed"
 
       [create]
+      branch-prefix = "acme-"
       new-branch-type = "prototype"
       share-new-branches = "no"
       stash = false
@@ -118,12 +130,16 @@ Feature: migrate existing configuration in Git metadata to a config file
       [hosting]
       dev-remote = "fork"
 
+      [propose]
+      lineage = "cli"
+
       [ship]
       delete-tracking-branch = false
       strategy = "squash-merge"
 
       [sync]
       auto-sync = false
+      detached = false
       feature-strategy = "merge"
       perennial-strategy = "rebase"
       prototype-strategy = "merge"
@@ -147,6 +163,7 @@ Feature: migrate existing configuration in Git metadata to a config file
     And local Git setting "git-town.order" is now "desc"
     And local Git setting "git-town.unknown-branch-type" is now "observed"
     And local Git setting "git-town.share-new-branches" is now "no"
+    And local Git setting "git-town.proposals-show-lineage" is now "cli"
     And local Git setting "git-town.push-branches" is now "true"
     And local Git setting "git-town.push-hook" is now "true"
     And local Git setting "git-town.ship-strategy" is now "squash-merge"
diff --git a/features/init/existing_configuration/remove_git_metadata.feature b/features/init/existing_configuration/remove_git_metadata.feature
index 27e385f81..5b5b7ad3e 100644
--- a/features/init/existing_configuration/remove_git_metadata.feature
+++ b/features/init/existing_configuration/remove_git_metadata.feature
@@ -25,6 +25,7 @@ Feature: remove existing configuration in Git metadata
     And global Git setting "alias.ship" is "town ship"
     And global Git setting "alias.sync" is "town sync"
     And local Git setting "git-town.auto-sync" is "false"
+    And local Git setting "git-town.branch-prefix" is "kg-"
     And local Git setting "git-town.contribution-regex" is "other.*"
     And local Git setting "git-town.detached" is "true"
     And local Git setting "git-town.dev-remote" is "fork"
@@ -37,6 +38,7 @@ Feature: remove existing configuration in Git metadata
     And local Git setting "git-town.perennial-branches" is "qa"
     And local Git setting "git-town.perennial-regex" is "qa.*"
     And local Git setting "git-town.push-branches" is "false"
+    And local Git setting "git-town.proposals-show-lineage" is "none"
     And local Git setting "git-town.push-hook" is "false"
     And local Git setting "git-town.share-new-branches" is "push"
     And local Git setting "git-town.ship-delete-tracking-branch" is "false"
@@ -61,6 +63,7 @@ Feature: remove existing configuration in Git metadata
       | feature regex               | backspace backspace backspace backspace backspace backspace enter           |                     |
       | contribution regex          | backspace backspace backspace backspace backspace backspace backspace enter |                     |
       | observed regex              | backspace backspace backspace backspace backspace enter                     |                     |
+      | branch prefix               | backspace backspace backspace enter                                         |                     |
       | new branch type             | up enter                                                                    |                     |
       | unknown branch type         | up enter                                                                    |                     |
       | sync feature strategy       | up enter                                                                    |                     |
@@ -77,6 +80,7 @@ Feature: remove existing configuration in Git metadata
       | ship strategy               | down enter                                                                  |                     |
       | ship delete tracking branch | down enter                                                                  | disable             |
       | order                       | up enter                                                                    |                     |
+      | proposals show lineage      | down enter                                                                  |                     |
       | config storage              | enter                                                                       | git metadata        |
 
   Scenario: result
@@ -97,18 +101,20 @@ Feature: remove existing configuration in Git metadata
       | git config --global --unset alias.set-parent         |
       | git config --global --unset alias.ship               |
       | git config --global --unset alias.sync               |
+      | git config git-town.perennial-branches ""            |
+      | git config --unset git-town.hosting-origin-hostname  |
+      | git config --unset git-town.forge-type               |
       | git config git-town.auto-sync true                   |
+      | git config --unset git-town.branch-prefix            |
       | git config git-town.detached false                   |
       | git config git-town.new-branch-type feature          |
-      | git config --unset git-town.forge-type               |
-      | git config --unset git-town.hosting-origin-hostname  |
-      | git config git-town.perennial-branches ""            |
       | git config --unset git-town.perennial-regex          |
       | git config git-town.unknown-branch-type feature      |
       | git config --unset git-town.feature-regex            |
       | git config --unset git-town.contribution-regex       |
       | git config --unset git-town.observed-regex           |
       | git config git-town.order asc                        |
+      | git config git-town.proposals-show-lineage cli       |
       | git config git-town.push-branches true               |
       | git config git-town.push-hook true                   |
       | git config git-town.share-new-branches no            |
@@ -135,6 +141,7 @@ Feature: remove existing configuration in Git metadata
     And local Git setting "git-town.dev-remote" is now "fork"
     And local Git setting "git-town.new-branch-type" is now "feature"
     And local Git setting "git-town.order" is now "asc"
+    And local Git setting "git-town.proposals-show-lineage" is now "cli"
     And local Git setting "git-town.push-branches" is now "true"
     And local Git setting "git-town.push-hook" is now "true"
     And local Git setting "git-town.share-new-branches" is now "no"
@@ -170,6 +177,7 @@ Feature: remove existing configuration in Git metadata
     And global Git setting "alias.ship" is now "town ship"
     And global Git setting "alias.sync" is now "town sync"
     And local Git setting "git-town.auto-sync" is now "false"
+    And local Git setting "git-town.branch-prefix" is now "kg-"
     And local Git setting "git-town.contribution-regex" is now "other.*"
     And local Git setting "git-town.dev-remote" is now "fork"
     And local Git setting "git-town.feature-regex" is now "user.*"
@@ -180,6 +188,7 @@ Feature: remove existing configuration in Git metadata
     And local Git setting "git-town.order" is now "desc"
     And local Git setting "git-town.perennial-regex" is now "qa.*"
     And local Git setting "git-town.push-branches" is now "false"
+    And local Git setting "git-town.proposals-show-lineage" is now "none"
     And local Git setting "git-town.push-hook" is now "false"
     And local Git setting "git-town.share-new-branches" is now "push"
     And local Git setting "git-town.ship-delete-tracking-branch" is now "false"
diff --git a/features/merge/color.ui/always.feature b/features/merge/color.ui/always.feature
index 63b06f9f8..eead658e9 100644
--- a/features/merge/color.ui/always.feature
+++ b/features/merge/color.ui/always.feature
@@ -24,9 +24,9 @@ Feature: merging a branch in a stack that is fully in sync
       | beta   | git fetch --prune --tags                        |
       |        | git checkout alpha                              |
       | alpha  | git reset --hard {{ sha 'beta commit' }}        |
+      |        | git push origin :beta                           |
       |        | git branch -D beta                              |
       |        | git push --force-with-lease --force-if-includes |
-      |        | git push origin :beta                           |
     And this lineage exists now
       """
       main
diff --git a/features/merge/dry_run/enabled.feature b/features/merge/dry_run/enabled.feature
index 2c20cd704..807887125 100644
--- a/features/merge/dry_run/enabled.feature
+++ b/features/merge/dry_run/enabled.feature
@@ -23,8 +23,8 @@ Feature: dry-run merging branches
       | beta   | git fetch --prune --tags                 |
       |        | git checkout alpha                       |
       | alpha  | git reset --hard {{ sha 'beta commit' }} |
-      |        | git branch -D beta                       |
       |        | git push origin :beta                    |
+      |        | git branch -D beta                       |
     And the initial branches exist now
     And the initial commits exist now
 
diff --git a/features/merge/push_hook/disabled.feature b/features/merge/push_hook/disabled.feature
index 4dc66f7ae..1b0d18b05 100644
--- a/features/merge/push_hook/disabled.feature
+++ b/features/merge/push_hook/disabled.feature
@@ -24,9 +24,9 @@ Feature: merging a branch with disabled push-hook
       | beta   | git fetch --prune --tags                                    |
       |        | git checkout alpha                                          |
       | alpha  | git reset --hard {{ sha 'beta commit' }}                    |
+      |        | git push origin :beta                                       |
       |        | git branch -D beta                                          |
       |        | git push --force-with-lease --force-if-includes --no-verify |
-      |        | git push origin :beta                                       |
     And this lineage exists now
       """
       main
diff --git a/features/merge/sync/in_sync.feature b/features/merge/sync/in_sync.feature
index a4204dd8c..c40419a14 100644
--- a/features/merge/sync/in_sync.feature
+++ b/features/merge/sync/in_sync.feature
@@ -23,9 +23,9 @@ Feature: merging a branch in a stack that is fully in sync
       | beta   | git fetch --prune --tags                        |
       |        | git checkout alpha                              |
       | alpha  | git reset --hard {{ sha 'beta commit' }}        |
+      |        | git push origin :beta                           |
       |        | git branch -D beta                              |
       |        | git push --force-with-lease --force-if-includes |
-      |        | git push origin :beta                           |
     And this lineage exists now
       """
       main
diff --git a/features/merge/uncommitted_changes/with.feature b/features/merge/uncommitted_changes/with.feature
index 8b5d2c43a..4c4965c35 100644
--- a/features/merge/uncommitted_changes/with.feature
+++ b/features/merge/uncommitted_changes/with.feature
@@ -26,9 +26,9 @@ Feature: merging a branch with uncommitted changes
       |        | git stash -m "Git Town WIP"                     |
       |        | git checkout alpha                              |
       | alpha  | git reset --hard {{ sha 'beta commit' }}        |
+      |        | git push origin :beta                           |
       |        | git branch -D beta                              |
       |        | git push --force-with-lease --force-if-includes |
-      |        | git push origin :beta                           |
       |        | git stash pop                                   |
       |        | git restore --staged .                          |
     And this lineage exists now
diff --git a/features/merge/verbose/enabled.feature b/features/merge/verbose/enabled.feature
index 38d25f172..78cac6ad5 100644
--- a/features/merge/verbose/enabled.feature
+++ b/features/merge/verbose/enabled.feature
@@ -40,10 +40,10 @@ Feature: merging a branch verbosely
       |        | git checkout alpha                                                                                                                                                                                                                                                                                                                               |
       | alpha  | git reset --hard {{ sha 'beta commit' }}                                                                                                                                                                                                                                                                                                         |
       |        | git config --unset git-town-branch.beta.parent                                                                                                                                                                                                                                                                                                   |
+      |        | git push origin :beta                                                                                                                                                                                                                                                                                                                            |
       |        | git branch -D beta                                                                                                                                                                                                                                                                                                                               |
       |        | git rev-parse alpha origin/alpha                                                                                                                                                                                                                                                                                                                 |
       |        | git push --force-with-lease --force-if-includes                                                                                                                                                                                                                                                                                                  |
-      |        | git push origin :beta                                                                                                                                                                                                                                                                                                                            |
       |        | git config --unset git-town-branch.beta.branchtype                                                                                                                                                                                                                                                                                               |
       |        | git rev-parse --verify -q refs/heads/beta                                                                                                                                                                                                                                                                                                        |
       |        | git for-each-ref "--format=refname:%(refname) branchname:%(refname:lstrip=2) sha:%(objectname) head:%(if)%(HEAD)%(then)Y%(else)N%(end) worktree:%(if)%(worktreepath)%(then)Y%(else)N%(end) symref:%(if)%(symref)%(then)Y%(else)N%(end) upstream:%(upstream:lstrip=2) track:%(upstream:track,nobracket)" --sort=refname refs/heads/ refs/remotes/ |
diff --git a/features/prepend/beam/compress.feature b/features/prepend/beam/compress.feature
index 6747b6f61..b733aead3 100644
--- a/features/prepend/beam/compress.feature
+++ b/features/prepend/beam/compress.feature
@@ -53,7 +53,7 @@ Feature: prepend a branch to a feature branch using the "compress" sync strategy
     Then Git Town runs the commands
       | BRANCH | COMMAND                   |
       | parent | git fetch --prune --tags  |
-      |        | git reset --soft main     |
+      |        | git reset --soft main --  |
       |        | git commit -m "commit 1"  |
       |        | git push -u origin parent |
     And these commits exist now
@@ -76,12 +76,12 @@ Feature: prepend a branch to a feature branch using the "compress" sync strategy
       | BRANCH | COMMAND                         |
       | old    | git fetch --prune --tags        |
       |        | git checkout parent             |
-      | parent | git reset --soft main           |
+      | parent | git reset --soft main --        |
       |        | git commit -m "commit 1"        |
       |        | git push -u origin parent       |
       |        | git checkout old                |
       | old    | git merge --no-edit --ff parent |
-      |        | git reset --soft parent         |
+      |        | git reset --soft parent --      |
       |        | git commit -m "commit 1"        |
       |        | git push --force-with-lease     |
     And these commits exist now
diff --git a/features/prepend/branch_prefix/config_file.feature b/features/prepend/branch_prefix/config_file.feature
new file mode 100644
index 000000000..ea566bfda
--- /dev/null
+++ b/features/prepend/branch_prefix/config_file.feature
@@ -0,0 +1,46 @@
+Feature: prepend with configured branch-prefix via config file
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the committed configuration file:
+      """
+      [create]
+      branch-prefix = "kg-"
+      """
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town prepend <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                           |
+      | feature-1 | git fetch --prune --tags          |
+      |           | git checkout -b kg-feature-2 main |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        kg-feature-2
+          feature-1
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town prepend feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/prepend/branch_prefix/env_var.feature b/features/prepend/branch_prefix/env_var.feature
new file mode 100644
index 000000000..b9f8a698f
--- /dev/null
+++ b/features/prepend/branch_prefix/env_var.feature
@@ -0,0 +1,43 @@
+Feature: prepend with configured branch-prefix via environment variable
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town prepend <BRANCH_NAME>" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                           |
+      | feature-1 | git fetch --prune --tags          |
+      |           | git checkout -b kg-feature-2 main |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        kg-feature-2
+          feature-1
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I run "git-town prepend feature-2" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/prepend/branch_prefix/git_metadata.feature b/features/prepend/branch_prefix/git_metadata.feature
new file mode 100644
index 000000000..fe37553e0
--- /dev/null
+++ b/features/prepend/branch_prefix/git_metadata.feature
@@ -0,0 +1,42 @@
+Feature: prepend with configured branch-prefix via Git metadata
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And Git setting "git-town.branch-prefix" is "kg-"
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town prepend <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                           |
+      | feature-1 | git fetch --prune --tags          |
+      |           | git checkout -b kg-feature-2 main |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        kg-feature-2
+          feature-1
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town prepend feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                    |
+      | kg-feature-2 | git checkout feature-1     |
+      | feature-1    | git branch -D kg-feature-2 |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/prepend/sync_strategy/compress/compressible.feature b/features/prepend/sync_strategy/compress/compressible.feature
index bb2903b08..1c810a81e 100644
--- a/features/prepend/sync_strategy/compress/compressible.feature
+++ b/features/prepend/sync_strategy/compress/compressible.feature
@@ -22,12 +22,12 @@ Feature: prepend a branch to a feature branch with compressible commits in a cle
       | BRANCH   | COMMAND                            |
       | branch-2 | git fetch --prune --tags           |
       |          | git checkout branch-1              |
-      | branch-1 | git reset --soft main              |
+      | branch-1 | git reset --soft main --           |
       |          | git commit -m "branch-1 commit 1"  |
       |          | git push --force-with-lease        |
       |          | git checkout branch-2              |
       | branch-2 | git merge --no-edit --ff branch-1  |
-      |          | git reset --soft branch-1          |
+      |          | git reset --soft branch-1 --       |
       |          | git commit -m "branch-2 commit 1"  |
       |          | git push --force-with-lease        |
       |          | git checkout -b branch-1a branch-1 |
diff --git a/features/prepend/sync_strategy/compress/not_compressible.feature b/features/prepend/sync_strategy/compress/not_compressible.feature
index dc30a7f0d..b300a80e2 100644
--- a/features/prepend/sync_strategy/compress/not_compressible.feature
+++ b/features/prepend/sync_strategy/compress/not_compressible.feature
@@ -22,7 +22,7 @@ Feature: prepend a branch to a feature branch that is already compressed in a cl
       |          | git checkout branch-1              |
       | branch-1 | git checkout branch-2              |
       | branch-2 | git merge --no-edit --ff branch-1  |
-      |          | git reset --soft branch-1          |
+      |          | git reset --soft branch-1 --       |
       |          | git commit -m "branch-2 commit"    |
       |          | git push --force-with-lease        |
       |          | git checkout -b branch-1a branch-1 |
diff --git a/features/prepend/sync_strategy/compress/update_on_main.feature b/features/prepend/sync_strategy/compress/update_on_main.feature
index 514d9821f..2f28843ab 100644
--- a/features/prepend/sync_strategy/compress/update_on_main.feature
+++ b/features/prepend/sync_strategy/compress/update_on_main.feature
@@ -22,12 +22,12 @@ Feature: prepend a branch to a feature branch in a clean workspace using the "co
       | branch-2 | git fetch --prune --tags           |
       |          | git checkout branch-1              |
       | branch-1 | git merge --no-edit --ff main      |
-      |          | git reset --soft main              |
+      |          | git reset --soft main --           |
       |          | git commit -m "branch-1 commit"    |
       |          | git push --force-with-lease        |
       |          | git checkout branch-2              |
       | branch-2 | git merge --no-edit --ff branch-1  |
-      |          | git reset --soft branch-1          |
+      |          | git reset --soft branch-1 --       |
       |          | git commit -m "branch-2 commit"    |
       |          | git push --force-with-lease        |
       |          | git checkout -b branch-1a branch-1 |
diff --git a/features/prepend/sync_strategy/compress/update_on_tracking.feature b/features/prepend/sync_strategy/compress/update_on_tracking.feature
index 87aafd7e1..9373dc47e 100644
--- a/features/prepend/sync_strategy/compress/update_on_tracking.feature
+++ b/features/prepend/sync_strategy/compress/update_on_tracking.feature
@@ -24,7 +24,7 @@ Feature: prepend a branch to a feature branch with remote updates in a clean wor
       | branch-1 | git checkout branch-2                    |
       | branch-2 | git merge --no-edit --ff branch-1        |
       |          | git merge --no-edit --ff origin/branch-2 |
-      |          | git reset --soft branch-1                |
+      |          | git reset --soft branch-1 --             |
       |          | git commit -m "branch-2 commit"          |
       |          | git push --force-with-lease              |
       |          | git checkout -b branch-1a branch-1       |
diff --git a/features/propose/sync_strategy/compress.feature b/features/propose/sync_strategy/compress.feature
index 547a87503..90e38da57 100644
--- a/features/propose/sync_strategy/compress.feature
+++ b/features/propose/sync_strategy/compress.feature
@@ -23,7 +23,7 @@ Feature: proposing using the "compress" sync strategy
       | existing | git fetch --prune --tags                                            |
       |          | Looking for proposal online ... ok                                  |
       |          | git merge --no-edit --ff origin/existing                            |
-      |          | git reset --soft main                                               |
+      |          | git reset --soft main --                                            |
       |          | git commit -m "local existing commit 1"                             |
       |          | git push --force-with-lease                                         |
       |          | open https://github.com/git-town/git-town/compare/existing?expand=1 |
diff --git a/features/rename/branch_prefix/config_file.feature b/features/rename/branch_prefix/config_file.feature
new file mode 100644
index 000000000..595a21e10
--- /dev/null
+++ b/features/rename/branch_prefix/config_file.feature
@@ -0,0 +1,51 @@
+Feature: rename with configured branch-prefix via config file
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the committed configuration file:
+      """
+      [create]
+      branch-prefix = "kg-"
+      """
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town rename <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                                  |
+      | feature-1    | git fetch --prune --tags                 |
+      |              | git branch --move feature-1 kg-feature-2 |
+      |              | git checkout kg-feature-2                |
+      | kg-feature-2 | git push -u origin kg-feature-2          |
+      |              | git push origin :feature-1               |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town rename feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                                                |
+      | kg-feature-2 | git branch feature-1 {{ sha 'persisted config file' }} |
+      |              | git push -u origin feature-1                           |
+      |              | git checkout feature-1                                 |
+      | feature-1    | git branch -D kg-feature-2                             |
+      |              | git push origin :kg-feature-2                          |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/rename/branch_prefix/env_var.feature b/features/rename/branch_prefix/env_var.feature
new file mode 100644
index 000000000..ab791b4ea
--- /dev/null
+++ b/features/rename/branch_prefix/env_var.feature
@@ -0,0 +1,48 @@
+Feature: rename with configured branch-prefix via environment variable
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town rename <BRANCH_NAME>" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                                  |
+      | feature-1    | git fetch --prune --tags                 |
+      |              | git branch --move feature-1 kg-feature-2 |
+      |              | git checkout kg-feature-2                |
+      | kg-feature-2 | git push -u origin kg-feature-2          |
+      |              | git push origin :feature-1               |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I run "git-town rename feature-2" with these environment variables
+      | GIT_TOWN_BRANCH_PREFIX | kg- |
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                                         |
+      | kg-feature-2 | git branch feature-1 {{ sha 'initial commit' }} |
+      |              | git push -u origin feature-1                    |
+      |              | git checkout feature-1                          |
+      | feature-1    | git branch -D kg-feature-2                      |
+      |              | git push origin :kg-feature-2                   |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/rename/branch_prefix/git_metadata.feature b/features/rename/branch_prefix/git_metadata.feature
new file mode 100644
index 000000000..d2410d35f
--- /dev/null
+++ b/features/rename/branch_prefix/git_metadata.feature
@@ -0,0 +1,47 @@
+Feature: rename with configured branch-prefix via Git metadata
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And Git setting "git-town.branch-prefix" is "kg-"
+    And the current branch is "feature-1"
+
+  Scenario Outline:
+    When I run "git-town rename <BRANCH_NAME>"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                                  |
+      | feature-1    | git fetch --prune --tags                 |
+      |              | git branch --move feature-1 kg-feature-2 |
+      |              | git checkout kg-feature-2                |
+      | kg-feature-2 | git push -u origin kg-feature-2          |
+      |              | git push origin :feature-1               |
+    And the current branch is now "kg-feature-2"
+    And this lineage exists now
+      """
+      main
+        kg-feature-2
+      """
+
+    Examples:
+      | BRANCH_NAME  |
+      | feature-2    |
+      | kg-feature-2 |
+
+  Scenario: undo
+    Given I ran "git-town rename feature-2"
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH       | COMMAND                                         |
+      | kg-feature-2 | git branch feature-1 {{ sha 'initial commit' }} |
+      |              | git push -u origin feature-1                    |
+      |              | git checkout feature-1                          |
+      | feature-1    | git branch -D kg-feature-2                      |
+      |              | git push origin :kg-feature-2                   |
+    And the current branch is now "feature-1"
+    And this lineage exists now
+      """
+      main
+        feature-1
+      """
diff --git a/features/ship/unknown/unknown_ship_strategy.feature b/features/ship/unknown/unknown_ship_strategy.feature
index 6262f22c5..bcdc24adc 100644
--- a/features/ship/unknown/unknown_ship_strategy.feature
+++ b/features/ship/unknown/unknown_ship_strategy.feature
@@ -18,7 +18,7 @@ Feature: unknown ship strategy
     Then Git Town runs no commands
     And Git Town prints the error:
       """
-      unknown ship strategy: "zonk"
+      unknown ship strategy in "git-town.ship-strategy": "zonk"
       """
 
   Scenario: undo
@@ -26,7 +26,7 @@ Feature: unknown ship strategy
     Then Git Town runs no commands
     And Git Town prints the error:
       """
-      unknown ship strategy: "zonk"
+      unknown ship strategy in "git-town.ship-strategy": "zonk"
       """
     And the initial branches and lineage exist now
     And the initial commits exist now
diff --git a/features/switch/stash/cli_flag.feature b/features/switch/stash/cli_flag.feature
new file mode 100644
index 000000000..8f40754f6
--- /dev/null
+++ b/features/switch/stash/cli_flag.feature
@@ -0,0 +1,30 @@
+@messyoutput
+Feature: enable stashing via CLI flag
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME    | TYPE    | PARENT | LOCATIONS |
+      | local-1 | feature | main   | local     |
+      | local-2 | feature | main   | local     |
+    And the current branch is "local-1"
+
+  Scenario: with open changes
+    And an uncommitted file
+    When I run "git-town switch --stash" and enter into the dialogs:
+      | DIALOG        | KEYS       |
+      | switch-branch | down enter |
+    Then Git Town runs the commands
+      | BRANCH  | COMMAND                     |
+      | local-1 | git add -A                  |
+      |         | git stash -m "Git Town WIP" |
+      |         | git checkout local-2        |
+      | local-2 | git stash pop               |
+
+  Scenario: without open changes
+    When I run "git-town switch --stash" and enter into the dialogs:
+      | DIALOG        | KEYS       |
+      | switch-branch | down enter |
+    Then Git Town runs the commands
+      | BRANCH  | COMMAND              |
+      | local-1 | git checkout local-2 |
diff --git a/features/sync/all_branches/compress_sync_strategy/stack/ancestor_with_multiple_descendents_shipped.feature b/features/sync/all_branches/compress_sync_strategy/stack/ancestor_with_multiple_descendents_shipped.feature
index f1749cb94..396b9b192 100644
--- a/features/sync/all_branches/compress_sync_strategy/stack/ancestor_with_multiple_descendents_shipped.feature
+++ b/features/sync/all_branches/compress_sync_strategy/stack/ancestor_with_multiple_descendents_shipped.feature
@@ -34,12 +34,12 @@ Feature: shipped branch with multiple descendents
       |            | git branch -D feature-1                           |
       |            | git checkout feature-1a                           |
       | feature-1a | git merge --no-edit --ff main                     |
-      |            | git reset --soft main                             |
+      |            | git reset --soft main --                          |
       |            | git commit -m "feature-1a commit"                 |
       |            | git push --force-with-lease                       |
       |            | git checkout feature-1b                           |
       | feature-1b | git merge --no-edit --ff main                     |
-      |            | git reset --soft main                             |
+      |            | git reset --soft main --                          |
       |            | git commit -m "feature-1b commit"                 |
       |            | git push --force-with-lease                       |
       |            | git push --tags                                   |
diff --git a/features/sync/all_branches/merge_sync_strategy/conflicts/feature_branch_main_branch_conflict/shipped_changes_conflict_with_existing_feature_branch.feature b/features/sync/all_branches/merge_sync_strategy/conflicts/feature_branch_main_branch_conflict/shipped_changes_conflict_with_existing_feature_branch.feature
index 7e97f05eb..04cf1a794 100644
--- a/features/sync/all_branches/merge_sync_strategy/conflicts/feature_branch_main_branch_conflict/shipped_changes_conflict_with_existing_feature_branch.feature
+++ b/features/sync/all_branches/merge_sync_strategy/conflicts/feature_branch_main_branch_conflict/shipped_changes_conflict_with_existing_feature_branch.feature
@@ -32,8 +32,7 @@ Feature: shipped changes conflict with multiple existing feature branches
       | BRANCH | COMMAND                       |
       | alpha  | git commit --no-edit          |
       |        | git push                      |
-      |        | git checkout main             |
-      | main   | git branch -D beta            |
+      |        | git branch -D beta            |
       |        | git checkout gamma            |
       | gamma  | git merge --no-edit --ff main |
     And Git Town prints the error:
diff --git a/features/sync/all_branches/merge_sync_strategy/preserve_git_history/preserve_git_history.feature b/features/sync/all_branches/merge_sync_strategy/preserve_git_history/preserve_git_history.feature
index 606e5a4a8..151e3eb7d 100644
--- a/features/sync/all_branches/merge_sync_strategy/preserve_git_history/preserve_git_history.feature
+++ b/features/sync/all_branches/merge_sync_strategy/preserve_git_history/preserve_git_history.feature
@@ -18,7 +18,7 @@ Feature: preserve the previous Git branch
     Given origin deletes the "previous" branch
     When I run "git-town sync --all"
     Then the current branch is still "current"
-    And the previous Git branch is now "main"
+    And there is now no previous Git branch
 
   Scenario: both branches deleted
     Given origin deletes the "previous" branch
diff --git a/features/sync/all_branches/merge_sync_strategy/prune/cli_flag.feature b/features/sync/all_branches/merge_sync_strategy/prune/cli_flag.feature
index bce3458bb..ba8a129e9 100644
--- a/features/sync/all_branches/merge_sync_strategy/prune/cli_flag.feature
+++ b/features/sync/all_branches/merge_sync_strategy/prune/cli_flag.feature
@@ -26,17 +26,15 @@ Feature: prune enabled via CLI
       |           | git checkout feature-1                            |
       | feature-1 | git merge --no-edit --ff main                     |
       |           | git merge --no-edit --ff origin/feature-1         |
-      |           | git checkout main                                 |
-      | main      | git push origin :feature-1                        |
-      |           | git branch -D feature-1                           |
       |           | git checkout feature-2                            |
-      | feature-2 | git merge --no-edit --ff main                     |
+      | feature-2 | git push origin :feature-1                        |
+      |           | git branch -D feature-1                           |
+      |           | git merge --no-edit --ff main                     |
       |           | git merge --no-edit --ff origin/feature-2         |
-      |           | git checkout main                                 |
-      | main      | git push origin :feature-2                        |
-      |           | git branch -D feature-2                           |
       |           | git checkout feature-3                            |
-      | feature-3 | git merge --no-edit --ff main                     |
+      | feature-3 | git push origin :feature-2                        |
+      |           | git branch -D feature-2                           |
+      |           | git merge --no-edit --ff main                     |
       |           | git push                                          |
       |           | git push --tags                                   |
     And this lineage exists now
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/color.ui/always.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/color.ui/always.feature
index 9bf5e42e0..327ff8449 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/color.ui/always.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/color.ui/always.feature
@@ -29,7 +29,7 @@ Feature: one person making a series of commits and syncs in between
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And all branches are now synchronized
@@ -47,7 +47,7 @@ Feature: one person making a series of commits and syncs in between
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And all branches are now synchronized
@@ -66,7 +66,7 @@ Feature: one person making a series of commits and syncs in between
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And all branches are now synchronized
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/compressible/compressible.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/compressible/compressible.feature
index 7784a886d..fa937bfce 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/compressible/compressible.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/compressible/compressible.feature
@@ -22,12 +22,12 @@ Feature: sync a feature branch with multiple commits using the "compress" sync s
       | BRANCH | COMMAND                        |
       | beta   | git fetch --prune --tags       |
       |        | git checkout alpha             |
-      | alpha  | git reset --soft main          |
+      | alpha  | git reset --soft main --       |
       |        | git commit -m "alpha commit 1" |
       |        | git push --force-with-lease    |
       |        | git checkout beta              |
       | beta   | git merge --no-edit --ff alpha |
-      |        | git reset --soft alpha         |
+      |        | git reset --soft alpha --      |
       |        | git commit -m "beta commit 1"  |
       |        | git push --force-with-lease    |
     And these commits exist now
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/conflicts/conflict_feature_branch_tracking_branch.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/conflicts/conflict_feature_branch_tracking_branch.feature
index 755246c1b..0ac432a7c 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/conflicts/conflict_feature_branch_tracking_branch.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/conflicts/conflict_feature_branch_tracking_branch.feature
@@ -47,7 +47,7 @@ Feature: handle conflicts between the current feature branch and its tracking br
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                  |
       | feature | git commit --no-edit                     |
-      |         | git reset --soft main                    |
+      |         | git reset --soft main --                 |
       |         | git commit -m "conflicting local commit" |
       |         | git push --force-with-lease              |
     And no merge is now in progress
@@ -62,6 +62,6 @@ Feature: handle conflicts between the current feature branch and its tracking br
     And I run "git-town continue"
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                  |
-      | feature | git reset --soft main                    |
+      | feature | git reset --soft main --                 |
       |         | git commit -m "conflicting local commit" |
       |         | git push --force-with-lease              |
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/conflicts/conflict_feature_vs_main.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/conflicts/conflict_feature_vs_main.feature
index e0dcb5fb5..d38d378ac 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/conflicts/conflict_feature_vs_main.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/conflicts/conflict_feature_vs_main.feature
@@ -58,7 +58,7 @@ Feature: while syncing using the "compress" strategy, handle conflicts between t
       | BRANCH  | COMMAND                                    |
       | feature | git commit --no-edit                       |
       |         | git merge --no-edit --ff origin/feature    |
-      |         | git reset --soft main                      |
+      |         | git reset --soft main --                   |
       |         | git commit -m "conflicting feature commit" |
       |         | git push --force-with-lease                |
     And no merge is now in progress
@@ -76,7 +76,7 @@ Feature: while syncing using the "compress" strategy, handle conflicts between t
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                    |
       | feature | git merge --no-edit --ff origin/feature    |
-      |         | git reset --soft main                      |
+      |         | git reset --soft main --                   |
       |         | git commit -m "conflicting feature commit" |
       |         | git push --force-with-lease                |
     And no merge is now in progress
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/detached/enabled.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/detached/enabled.feature
index 60d4a7cb7..fee125cfc 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/detached/enabled.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/detached/enabled.feature
@@ -25,13 +25,13 @@ Feature: detached sync a grandchild feature branch using the "compress" strategy
       | beta   | git fetch --prune --tags              |
       |        | git checkout alpha                    |
       | alpha  | git merge --no-edit --ff origin/alpha |
-      |        | git reset --soft main                 |
+      |        | git reset --soft main --              |
       |        | git commit -m "local alpha commit"    |
       |        | git push --force-with-lease           |
       |        | git checkout beta                     |
       | beta   | git merge --no-edit --ff alpha        |
       |        | git merge --no-edit --ff origin/beta  |
-      |        | git reset --soft alpha                |
+      |        | git reset --soft alpha --             |
       |        | git commit -m "local beta commit"     |
       |        | git push --force-with-lease           |
     And these commits exist now
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/happy_path.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/happy_path.feature
index ba5dc1481..7bb0431e7 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/happy_path.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/happy_path.feature
@@ -29,7 +29,7 @@ Feature: one person making a series of commits and syncs in between
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And all branches are now synchronized
@@ -47,7 +47,7 @@ Feature: one person making a series of commits and syncs in between
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And all branches are now synchronized
@@ -65,7 +65,7 @@ Feature: one person making a series of commits and syncs in between
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And all branches are now synchronized
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/offline/happy_path.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/offline/happy_path.feature
index 8547d432d..c0cead6c0 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/offline/happy_path.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/offline/happy_path.feature
@@ -26,7 +26,7 @@ Feature: sync the current feature branch using the "compress" strategy in offlin
       |         | git checkout feature                              |
       | feature | git merge --no-edit --ff main                     |
       |         | git merge --no-edit --ff origin/feature           |
-      |         | git reset --soft main                             |
+      |         | git reset --soft main --                          |
       |         | git commit -m "local feature commit 1"            |
     And these commits exist now
       | BRANCH  | LOCATION | MESSAGE                |
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/push_branches/cli.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/push_branches/cli.feature
index 1c6c54ff5..4b6b5c241 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/push_branches/cli.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/push_branches/cli.feature
@@ -26,7 +26,7 @@ Feature: disable pushing through the CLI
       |         | git checkout feature                              |
       | feature | git merge --no-edit --ff main                     |
       |         | git merge --no-edit --ff origin/feature           |
-      |         | git reset --soft main                             |
+      |         | git reset --soft main --                          |
       |         | git commit -m "local feature commit 1"            |
     And these commits exist now
       | BRANCH  | LOCATION      | MESSAGE                |
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/push_branches/config_file.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/push_branches/config_file.feature
index bcd16609f..565c7a682 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/push_branches/config_file.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/push_branches/config_file.feature
@@ -34,7 +34,7 @@ Feature: disable pushing through the config file
       |         | git checkout feature                              |
       | feature | git merge --no-edit --ff main                     |
       |         | git merge --no-edit --ff origin/feature           |
-      |         | git reset --soft main                             |
+      |         | git reset --soft main --                          |
       |         | git commit -m "local feature commit 1"            |
     And these commits exist now
       | BRANCH  | LOCATION      | MESSAGE                |
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/push_branches/git_config.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/push_branches/git_config.feature
index 6c508949c..4fe92509c 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/push_branches/git_config.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/push_branches/git_config.feature
@@ -27,7 +27,7 @@ Feature: disable pushing through Git metadata
       |         | git checkout feature                              |
       | feature | git merge --no-edit --ff main                     |
       |         | git merge --no-edit --ff origin/feature           |
-      |         | git reset --soft main                             |
+      |         | git reset --soft main --                          |
       |         | git commit -m "local feature commit 1"            |
     And these commits exist now
       | BRANCH  | LOCATION      | MESSAGE                |
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/tracking_branch/without/without.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/tracking_branch/without/without.feature
index 58badd13e..2ea86671d 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/tracking_branch/without/without.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/tracking_branch/without/without.feature
@@ -24,7 +24,7 @@ Feature: sync the current feature branch without a tracking branch using the "co
       |         | git push                                          |
       |         | git checkout feature                              |
       | feature | git merge --no-edit --ff main                     |
-      |         | git reset --soft main                             |
+      |         | git reset --soft main --                          |
       |         | git commit -m "local feature commit 1"            |
       |         | git push -u origin feature                        |
     And the branches are now
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/two_collaborators/conflicting_changes_parallel.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/two_collaborators/conflicting_changes_parallel.feature
index 54136c46c..4c43ad28e 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/two_collaborators/conflicting_changes_parallel.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/two_collaborators/conflicting_changes_parallel.feature
@@ -36,7 +36,7 @@ Feature: two people using the "compress" strategy make concurrent conflicting ch
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "my first commit"         |
       |         | git push --force-with-lease             |
     And these commits exist now
@@ -63,7 +63,7 @@ Feature: two people using the "compress" strategy make concurrent conflicting ch
     Then Git Town runs the commands
       | BRANCH  | COMMAND                               |
       | feature | git commit --no-edit                  |
-      |         | git reset --soft main                 |
+      |         | git reset --soft main --              |
       |         | git commit -m "coworker first commit" |
       |         | git push --force-with-lease           |
     And all branches are now synchronized
@@ -91,7 +91,7 @@ Feature: two people using the "compress" strategy make concurrent conflicting ch
     Then Git Town runs the commands
       | BRANCH  | COMMAND                         |
       | feature | git commit --no-edit            |
-      |         | git reset --soft main           |
+      |         | git reset --soft main --        |
       |         | git commit -m "my first commit" |
       |         | git push --force-with-lease     |
     And all branches are now synchronized
@@ -119,7 +119,7 @@ Feature: two people using the "compress" strategy make concurrent conflicting ch
     Then Git Town runs the commands
       | BRANCH  | COMMAND                               |
       | feature | git commit --no-edit                  |
-      |         | git reset --soft main                 |
+      |         | git reset --soft main --              |
       |         | git commit -m "coworker first commit" |
       |         | git push --force-with-lease           |
     And all branches are now synchronized
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/two_collaborators/conflicting_changes_serial.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/two_collaborators/conflicting_changes_serial.feature
index 643fcb10e..86cc3b69e 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/two_collaborators/conflicting_changes_serial.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/two_collaborators/conflicting_changes_serial.feature
@@ -35,7 +35,7 @@ Feature: two people make alternating conflicting changes to the same branch usin
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And these commits exist now
@@ -63,7 +63,7 @@ Feature: two people make alternating conflicting changes to the same branch usin
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And these commits exist now
@@ -88,7 +88,7 @@ Feature: two people make alternating conflicting changes to the same branch usin
     Then Git Town runs the commands
       | BRANCH  | COMMAND                     |
       | feature | git commit --no-edit        |
-      |         | git reset --soft main       |
+      |         | git reset --soft main --    |
       |         | git commit -m "the feature" |
       |         | git push --force-with-lease |
     And these commits exist now
@@ -105,7 +105,7 @@ Feature: two people make alternating conflicting changes to the same branch usin
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And these commits exist now
@@ -130,7 +130,7 @@ Feature: two people make alternating conflicting changes to the same branch usin
     Then Git Town runs the commands
       | BRANCH  | COMMAND                     |
       | feature | git commit --no-edit        |
-      |         | git reset --soft main       |
+      |         | git reset --soft main --    |
       |         | git commit -m "the feature" |
       |         | git push --force-with-lease |
     And these commits exist now
@@ -147,7 +147,7 @@ Feature: two people make alternating conflicting changes to the same branch usin
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And these commits exist now
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/two_collaborators/nonconflicting_changes_serial.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/two_collaborators/nonconflicting_changes_serial.feature
index be2cceeb6..94b233ca2 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/two_collaborators/nonconflicting_changes_serial.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/two_collaborators/nonconflicting_changes_serial.feature
@@ -35,7 +35,7 @@ Feature: two people make alternating non-conflicting changes to the same branch
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And these commits exist now
@@ -63,7 +63,7 @@ Feature: two people make alternating non-conflicting changes to the same branch
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And these commits exist now
@@ -88,7 +88,7 @@ Feature: two people make alternating non-conflicting changes to the same branch
     Then Git Town runs the commands
       | BRANCH  | COMMAND                     |
       | feature | git commit --no-edit        |
-      |         | git reset --soft main       |
+      |         | git reset --soft main --    |
       |         | git commit -m "the feature" |
       |         | git push --force-with-lease |
     And these commits exist now
@@ -105,7 +105,7 @@ Feature: two people make alternating non-conflicting changes to the same branch
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And these commits exist now
@@ -130,7 +130,7 @@ Feature: two people make alternating non-conflicting changes to the same branch
     Then Git Town runs the commands
       | BRANCH  | COMMAND                     |
       | feature | git commit --no-edit        |
-      |         | git reset --soft main       |
+      |         | git reset --soft main --    |
       |         | git commit -m "the feature" |
       |         | git push --force-with-lease |
     And these commits exist now
@@ -147,7 +147,7 @@ Feature: two people make alternating non-conflicting changes to the same branch
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
       |         | git merge --no-edit --ff origin/feature |
-      |         | git reset --soft main                   |
+      |         | git reset --soft main --                |
       |         | git commit -m "the feature"             |
       |         | git push --force-with-lease             |
     And these commits exist now
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/updates/main_branch.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/updates/main_branch.feature
index ecea64afd..dff6989d1 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/updates/main_branch.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/updates/main_branch.feature
@@ -23,7 +23,7 @@ Feature: sync a feature branch with new commits on the main branch in detached m
       |        | git checkout alpha             |
       | alpha  | git checkout beta              |
       | beta   | git merge --no-edit --ff alpha |
-      |        | git reset --soft alpha         |
+      |        | git reset --soft alpha --      |
       |        | git commit -m "beta commit"    |
       |        | git push --force-with-lease    |
     And the initial commits exist now
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/updates/tracking_branch.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/updates/tracking_branch.feature
index 6eab0b707..86152fee3 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/updates/tracking_branch.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/updates/tracking_branch.feature
@@ -27,7 +27,7 @@ Feature: sync a feature branch with new commits on the tracking branch
       |        | git checkout alpha                   |
       | alpha  | git checkout beta                    |
       | beta   | git merge --no-edit --ff origin/beta |
-      |        | git reset --soft alpha               |
+      |        | git reset --soft alpha --            |
       |        | git commit -m "beta commit"          |
       |        | git push --force-with-lease          |
     And these commits exist now
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/upstream/happy_path.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/upstream/happy_path.feature
index 3c8662530..402023e1e 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/upstream/happy_path.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/upstream/happy_path.feature
@@ -26,7 +26,7 @@ Feature: "compress" sync with upstream repo
       |         | git checkout feature                                |
       | feature | git merge --no-edit --ff main                       |
       |         | git merge --no-edit --ff origin/feature             |
-      |         | git reset --soft main                               |
+      |         | git reset --soft main --                            |
       |         | git commit -m "local commit"                        |
       |         | git push --force-with-lease                         |
     And all branches are now synchronized
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/worktree/main_in_other_worktree.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/worktree/main_in_other_worktree.feature
new file mode 100644
index 000000000..d1b27c084
--- /dev/null
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/worktree/main_in_other_worktree.feature
@@ -0,0 +1,41 @@
+Feature: sync a branch when main is active in another worktree and has updates
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME    | TYPE    | PARENT | LOCATIONS     |
+      | feature | feature | main   | local, origin |
+    And the commits
+      | BRANCH  | LOCATION | MESSAGE               |
+      | main    | local    | local main commit     |
+      |         | origin   | origin main commit    |
+      | feature | local    | local feature commit  |
+      |         | origin   | origin feature commit |
+    And Git setting "git-town.sync-feature-strategy" is "compress"
+    And the current branch is "feature"
+    And branch "main" is active in another worktree
+    When I run "git-town sync"
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH  | COMMAND                                 |
+      | feature | git fetch --prune --tags                |
+      |         | git merge --no-edit --ff main           |
+      |         | git merge --no-edit --ff origin/feature |
+      |         | git reset --soft main --                |
+      |         | git commit -m "local feature commit"    |
+      |         | git push --force-with-lease             |
+    And these commits exist now
+      | BRANCH  | LOCATION      | MESSAGE              |
+      | main    | origin        | origin main commit   |
+      |         | worktree      | local main commit    |
+      | feature | local, origin | local feature commit |
+      |         | origin        | local main commit    |
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH  | COMMAND                                                                                |
+      | feature | git reset --hard {{ sha 'local feature commit' }}                                      |
+      |         | git push --force-with-lease origin {{ sha-in-origin 'origin feature commit' }}:feature |
+    And the initial commits exist now
diff --git a/features/sync/current_branch/feature_branch/compress_sync_strategy/worktree/previous_branch_in_other_worktree.feature b/features/sync/current_branch/feature_branch/compress_sync_strategy/worktree/previous_branch_in_other_worktree.feature
index 80219cd35..59421617f 100644
--- a/features/sync/current_branch/feature_branch/compress_sync_strategy/worktree/previous_branch_in_other_worktree.feature
+++ b/features/sync/current_branch/feature_branch/compress_sync_strategy/worktree/previous_branch_in_other_worktree.feature
@@ -19,7 +19,7 @@ Feature: sync while the previous branch is checked out in another worktree
     Then Git Town runs the commands
       | BRANCH  | COMMAND                    |
       | current | git fetch --prune --tags   |
-      |         | git reset --soft main      |
+      |         | git reset --soft main --   |
       |         | git commit -m "current 1"  |
       |         | git push -u origin current |
     And the previous Git branch is still "previous"
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/prune/cli_flag.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/prune/cli_flag.feature
index 6a12baa05..46d86961b 100644
--- a/features/sync/current_branch/feature_branch/merge_sync_strategy/prune/cli_flag.feature
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/prune/cli_flag.feature
@@ -24,10 +24,9 @@ Feature: prune enabled via CLI
       |           | git checkout feature-1                            |
       | feature-1 | git merge --no-edit --ff main                     |
       |           | git merge --no-edit --ff origin/feature-1         |
-      |           | git checkout main                                 |
-      | main      | git push origin :feature-1                        |
-      |           | git branch -D feature-1                           |
       |           | git checkout feature-2                            |
+      | feature-2 | git push origin :feature-1                        |
+      |           | git branch -D feature-1                           |
     And this lineage exists now
       """
       main
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/prune/remote_deleted.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/prune/remote_deleted.feature
new file mode 100644
index 000000000..81a0c6f3f
--- /dev/null
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/prune/remote_deleted.feature
@@ -0,0 +1,33 @@
+Feature: prune a branch with deleted remote
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME    | TYPE    | PARENT | LOCATIONS     |
+      | feature | feature | main   | local, origin |
+    And the commits
+      | BRANCH  | LOCATION      | MESSAGE        |
+      | feature | local, origin | feature commit |
+    And origin deletes the "feature" branch
+    And the current branch is "feature"
+    When I run "git-town sync --prune"
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH  | COMMAND                  |
+      | feature | git fetch --prune --tags |
+      |         | git checkout main        |
+      | main    | git branch -D feature    |
+    And the branches are now
+      | REPOSITORY    | BRANCHES |
+      | local, origin | main     |
+    And all branches are now synchronized
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH | COMMAND                                       |
+      | main   | git branch feature {{ sha 'feature commit' }} |
+      |        | git checkout feature                          |
+    And the initial branches and lineage exist now
+    And the initial commits exist now
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/tracking_branch/with/conflict_feature_branch_with_update_vs_main_branch.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/tracking_branch/with/conflict_feature_branch_with_update_vs_main_branch.feature
index ab6c46939..a3076d399 100644
--- a/features/sync/current_branch/feature_branch/merge_sync_strategy/tracking_branch/with/conflict_feature_branch_with_update_vs_main_branch.feature
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/tracking_branch/with/conflict_feature_branch_with_update_vs_main_branch.feature
@@ -107,7 +107,7 @@ Feature: handle conflicts between the current feature branch and the main branch
       |         | git merge --no-edit --ff origin/feature         |
       |         | git push                                        |
       |         | git fetch --prune --tags                        |
-      |         | git reset --soft main                           |
+      |         | git reset --soft main --                        |
       |         | git commit -m "conflicting feature commit"      |
       |         | git push --force-with-lease --force-if-includes |
     And Git Town prints:
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/tracking_branch/without/conflict_feature_vs_main.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/tracking_branch/without/conflict_feature_vs_main.feature
index 3568bffe3..1ac6178ba 100644
--- a/features/sync/current_branch/feature_branch/merge_sync_strategy/tracking_branch/without/conflict_feature_vs_main.feature
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/tracking_branch/without/conflict_feature_vs_main.feature
@@ -122,7 +122,7 @@ Feature: handle conflicts between the current feature branch and the main branch
       |         | git merge --no-edit --ff origin/feature         |
       |         | git push                                        |
       |         | git fetch --prune --tags                        |
-      |         | git reset --soft main                           |
+      |         | git reset --soft main --                        |
       |         | git commit -m "conflicting feature commit"      |
       |         | git push --force-with-lease --force-if-includes |
     And Git Town prints:
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/current_branch_remote_gone_and_main_in_other_worktree.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/current_branch_remote_gone_and_main_in_other_worktree.feature
new file mode 100644
index 000000000..aaea821dc
--- /dev/null
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/current_branch_remote_gone_and_main_in_other_worktree.feature
@@ -0,0 +1,28 @@
+Feature: sync a branch whose branch is gone while main is active in another worktree
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+      | feature-2 | feature | main   | local, origin |
+    And origin deletes the "feature-1" branch
+    And the current branch is "feature-1"
+    And branch "main" is active in another worktree
+    When I run "git-town sync"
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                  |
+      | feature-1 | git fetch --prune --tags |
+      |           | git checkout feature-2   |
+      | feature-2 | git branch -D feature-1  |
+    And the initial commits exist now
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                                         |
+      | feature-2 | git branch feature-1 {{ sha 'initial commit' }} |
+      |           | git checkout feature-1                          |
+    And the initial commits exist now
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/current_branch_remote_gone_no_other_branch_in_worktree.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/current_branch_remote_gone_no_other_branch_in_worktree.feature
new file mode 100644
index 000000000..e3699cc10
--- /dev/null
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/current_branch_remote_gone_no_other_branch_in_worktree.feature
@@ -0,0 +1,26 @@
+Feature: sync a branch whose tracking branch is gone while no other branch is available in the current worktree
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME      | TYPE    | PARENT | LOCATIONS     |
+      | feature-1 | feature | main   | local, origin |
+    And origin deletes the "feature-1" branch
+    And the current branch is "feature-1"
+    And branch "main" is active in another worktree
+    When I run "git-town sync"
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH    | COMMAND                  |
+      | feature-1 | git fetch --prune --tags |
+    And Git Town prints the error:
+      """
+      there is no other branch to switch to
+      """
+    And the initial commits exist now
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs no commands
+    And the initial commits exist now
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/feature_branch_in_other_worktree.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/feature_branch_in_other_worktree.feature
index 63c545fea..fcd1d22a6 100644
--- a/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/feature_branch_in_other_worktree.feature
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/feature_branch_in_other_worktree.feature
@@ -20,7 +20,7 @@ Feature: Sync a feature branch that is in another worktree than the main branch
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                 |
       | feature | git fetch --prune --tags                |
-      |         | git merge --no-edit --ff origin/main    |
+      |         | git merge --no-edit --ff main           |
       |         | git merge --no-edit --ff origin/feature |
       |         | git push                                |
     And the current branch in the other worktree is still "feature"
@@ -29,10 +29,10 @@ Feature: Sync a feature branch that is in another worktree than the main branch
       | main    | local            | local main commit                                          |
       |         | origin           | origin main commit                                         |
       | feature | origin, worktree | local feature commit                                       |
-      |         |                  | Merge remote-tracking branch 'origin/main' into feature    |
+      |         | origin           | local main commit                                          |
+      |         | origin, worktree | Merge branch 'main' into feature                           |
       |         |                  | origin feature commit                                      |
       |         |                  | Merge remote-tracking branch 'origin/feature' into feature |
-      |         | worktree         | origin main commit                                         |
 
   Scenario: undo
     When I run "git-town undo" in the other worktree
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/main_in_other_worktree.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/main_in_other_worktree.feature
new file mode 100644
index 000000000..fcf5ec9f0
--- /dev/null
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/main_in_other_worktree.feature
@@ -0,0 +1,41 @@
+Feature: sync a branch when main is active in another worktree and has updates
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME    | TYPE    | PARENT | LOCATIONS     |
+      | feature | feature | main   | local, origin |
+    And the commits
+      | BRANCH  | LOCATION | MESSAGE               |
+      | main    | local    | local main commit     |
+      |         | origin   | origin main commit    |
+      | feature | local    | local feature commit  |
+      |         | origin   | origin feature commit |
+    And the current branch is "feature"
+    And branch "main" is active in another worktree
+    When I run "git-town sync"
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH  | COMMAND                                 |
+      | feature | git fetch --prune --tags                |
+      |         | git merge --no-edit --ff main           |
+      |         | git merge --no-edit --ff origin/feature |
+      |         | git push                                |
+    And these commits exist now
+      | BRANCH  | LOCATION      | MESSAGE                                                    |
+      | main    | origin        | origin main commit                                         |
+      |         | worktree      | local main commit                                          |
+      | feature | local, origin | local feature commit                                       |
+      |         |               | Merge branch 'main' into feature                           |
+      |         |               | origin feature commit                                      |
+      |         |               | Merge remote-tracking branch 'origin/feature' into feature |
+      |         | origin        | local main commit                                          |
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH  | COMMAND                                                                                |
+      | feature | git reset --hard {{ sha 'local feature commit' }}                                      |
+      |         | git push --force-with-lease origin {{ sha-in-origin 'origin feature commit' }}:feature |
+    And the initial commits exist now
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/parent_in_other_worktree.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/parent_in_other_worktree.feature
index 39adb137f..5b704a1b3 100644
--- a/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/parent_in_other_worktree.feature
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/parent_in_other_worktree.feature
@@ -26,20 +26,20 @@ Feature: sync a branch whose parent is active in another worktree
       | main   | git -c rebase.updateRefs=false rebase origin/main |
       |        | git push                                          |
       |        | git checkout child                                |
-      | child  | git merge --no-edit --ff origin/parent            |
+      | child  | git merge --no-edit --ff parent                   |
       |        | git merge --no-edit --ff origin/child             |
       |        | git push                                          |
     And these commits exist now
-      | BRANCH | LOCATION                | MESSAGE                                                 |
-      | main   | local, origin, worktree | origin main commit                                      |
-      |        |                         | local main commit                                       |
-      | parent | origin                  | origin parent commit                                    |
-      |        | worktree                | local parent commit                                     |
-      | child  | local, origin           | local child commit                                      |
-      |        | local                   | origin parent commit                                    |
-      |        | local, origin           | Merge remote-tracking branch 'origin/parent' into child |
-      |        |                         | origin child commit                                     |
-      |        |                         | Merge remote-tracking branch 'origin/child' into child  |
+      | BRANCH | LOCATION                | MESSAGE                                                |
+      | main   | local, origin, worktree | origin main commit                                     |
+      |        |                         | local main commit                                      |
+      | parent | origin                  | origin parent commit                                   |
+      |        | worktree                | local parent commit                                    |
+      | child  | local, origin           | local child commit                                     |
+      |        |                         | Merge branch 'parent' into child                       |
+      |        |                         | origin child commit                                    |
+      |        |                         | Merge remote-tracking branch 'origin/child' into child |
+      |        | origin                  | local parent commit                                    |
 
   Scenario: undo
     When I run "git-town undo"
diff --git a/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/sync_from_linked_worktree.feature b/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/sync_from_linked_worktree.feature
index 240f60d7a..7e880b417 100644
--- a/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/sync_from_linked_worktree.feature
+++ b/features/sync/current_branch/feature_branch/merge_sync_strategy/worktree/sync_from_linked_worktree.feature
@@ -26,20 +26,20 @@ Feature: sync a branch whose parent is active in another worktree
       | main   | git -c rebase.updateRefs=false rebase origin/main |
       |        | git push                                          |
       |        | git checkout child                                |
-      | child  | git merge --no-edit --ff origin/parent            |
+      | child  | git merge --no-edit --ff parent                   |
       |        | git merge --no-edit --ff origin/child             |
       |        | git push                                          |
     And these commits exist now
-      | BRANCH | LOCATION                | MESSAGE                                                 |
-      | main   | local, origin, worktree | origin main commit                                      |
-      |        |                         | local main commit                                       |
-      | parent | local                   | local parent commit                                     |
-      |        | origin                  | origin parent commit                                    |
-      | child  | origin, worktree        | local child commit                                      |
-      |        |                         | Merge remote-tracking branch 'origin/parent' into child |
-      |        |                         | origin child commit                                     |
-      |        |                         | Merge remote-tracking branch 'origin/child' into child  |
-      |        | worktree                | origin parent commit                                    |
+      | BRANCH | LOCATION                | MESSAGE                                                |
+      | main   | local, origin, worktree | origin main commit                                     |
+      |        |                         | local main commit                                      |
+      | parent | local                   | local parent commit                                    |
+      |        | origin                  | origin parent commit                                   |
+      | child  | origin, worktree        | local child commit                                     |
+      |        | origin                  | local parent commit                                    |
+      |        | origin, worktree        | Merge branch 'parent' into child                       |
+      |        |                         | origin child commit                                    |
+      |        |                         | Merge remote-tracking branch 'origin/child' into child |
 
   Scenario: undo in other worktree
     When I run "git-town undo" in the other worktree
diff --git a/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/conflict_in_linked_worktree.feature b/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/conflict_in_linked_worktree.feature
index 563842839..7f36d478f 100644
--- a/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/conflict_in_linked_worktree.feature
+++ b/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/conflict_in_linked_worktree.feature
@@ -6,9 +6,9 @@ Feature: sync a branch in a "linked worktree" that has a merge conflict
       | NAME    | TYPE    | PARENT | LOCATIONS     |
       | feature | feature | main   | local, origin |
     And the commits
-      | BRANCH  | LOCATION | MESSAGE                    | FILE NAME        | FILE CONTENT    |
-      | main    | origin   | conflicting main commit    | conflicting_file | main content    |
-      | feature | local    | conflicting feature commit | conflicting_file | feature content |
+      | BRANCH  | LOCATION      | MESSAGE                    | FILE NAME        | FILE CONTENT    |
+      | main    | local, origin | conflicting main commit    | conflicting_file | main content    |
+      | feature | local         | conflicting feature commit | conflicting_file | feature content |
     And Git setting "git-town.sync-feature-strategy" is "rebase"
     And the current branch is "main"
     And branch "feature" is active in another worktree
@@ -16,10 +16,10 @@ Feature: sync a branch in a "linked worktree" that has a merge conflict
 
   Scenario: result
     Then Git Town runs the commands
-      | BRANCH  | COMMAND                                           |
-      | feature | git fetch --prune --tags                          |
-      |         | git push --force-with-lease --force-if-includes   |
-      |         | git -c rebase.updateRefs=false rebase origin/main |
+      | BRANCH  | COMMAND                                         |
+      | feature | git fetch --prune --tags                        |
+      |         | git push --force-with-lease --force-if-includes |
+      |         | git -c rebase.updateRefs=false rebase main      |
     And Git Town prints the error:
       """
       To continue after having resolved conflicts, run "git town continue".
@@ -53,6 +53,5 @@ Feature: sync a branch in a "linked worktree" that has a merge conflict
       | feature | git push --force-with-lease --force-if-includes |
     And these commits exist now
       | BRANCH  | LOCATION         | MESSAGE                 | FILE NAME        | FILE CONTENT     |
-      | main    | origin           | conflicting main commit | conflicting_file | main content     |
+      | main    | local, origin    | conflicting main commit | conflicting_file | main content     |
       | feature | origin, worktree | resolved commit         | conflicting_file | resolved content |
-      |         | worktree         | conflicting main commit | conflicting_file | main content     |
diff --git a/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/feature_branch_in_other_worktree.feature b/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/feature_branch_in_other_worktree.feature
index 6f83297b8..066e1da8d 100644
--- a/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/feature_branch_in_other_worktree.feature
+++ b/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/feature_branch_in_other_worktree.feature
@@ -22,16 +22,16 @@ Feature: Sync a feature branch that is in another worktree than the main branch
       | feature | git fetch --prune --tags                             |
       |         | git push --force-with-lease --force-if-includes      |
       |         | git -c rebase.updateRefs=false rebase origin/feature |
-      |         | git -c rebase.updateRefs=false rebase origin/main    |
+      |         | git -c rebase.updateRefs=false rebase main           |
       |         | git push --force-with-lease --force-if-includes      |
     And the current branch in the other worktree is still "feature"
     And these commits exist now
       | BRANCH  | LOCATION         | MESSAGE               |
       | main    | local            | local main commit     |
       |         | origin           | origin main commit    |
-      | feature | origin, worktree | origin feature commit |
+      | feature | origin           | local main commit     |
+      |         | origin, worktree | origin feature commit |
       |         |                  | local feature commit  |
-      |         | worktree         | origin main commit    |
 
   Scenario: undo
     When I run "git-town undo" in the other worktree
diff --git a/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/happy_path.feature b/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/happy_path.feature
index df9de6c50..005a6813d 100644
--- a/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/happy_path.feature
+++ b/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/happy_path.feature
@@ -21,25 +21,25 @@ Feature: sync a branch whose parent is active in another worktree
 
   Scenario: result
     Then Git Town runs the commands
-      | BRANCH | COMMAND                                                                               |
-      | child  | git fetch --prune --tags                                                              |
-      |        | git checkout main                                                                     |
-      | main   | git -c rebase.updateRefs=false rebase origin/main                                     |
-      |        | git push                                                                              |
-      |        | git checkout child                                                                    |
-      | child  | git push --force-with-lease --force-if-includes                                       |
-      |        | git -c rebase.updateRefs=false rebase origin/child                                    |
-      |        | git -c rebase.updateRefs=false rebase --onto origin/parent {{ sha 'initial commit' }} |
-      |        | git push --force-with-lease --force-if-includes                                       |
+      | BRANCH | COMMAND                                                                        |
+      | child  | git fetch --prune --tags                                                       |
+      |        | git checkout main                                                              |
+      | main   | git -c rebase.updateRefs=false rebase origin/main                              |
+      |        | git push                                                                       |
+      |        | git checkout child                                                             |
+      | child  | git push --force-with-lease --force-if-includes                                |
+      |        | git -c rebase.updateRefs=false rebase origin/child                             |
+      |        | git -c rebase.updateRefs=false rebase --onto parent {{ sha 'initial commit' }} |
+      |        | git push --force-with-lease --force-if-includes                                |
     And these commits exist now
       | BRANCH | LOCATION                | MESSAGE              |
       | main   | local, origin, worktree | origin main commit   |
       |        |                         | local main commit    |
       | parent | origin                  | origin parent commit |
       |        | worktree                | local parent commit  |
-      | child  | local                   | origin parent commit |
-      |        | local, origin           | origin child commit  |
+      | child  | local, origin           | origin child commit  |
       |        |                         | local child commit   |
+      |        | origin                  | local parent commit  |
 
   Scenario: undo
     When I run "git-town undo"
diff --git a/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/main_in_other_worktree.feature b/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/main_in_other_worktree.feature
new file mode 100644
index 000000000..2a39d667a
--- /dev/null
+++ b/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/main_in_other_worktree.feature
@@ -0,0 +1,41 @@
+Feature: sync a branch when main is active in another worktree and has updates
+
+  Background:
+    Given a Git repo with origin
+    And the branches
+      | NAME    | TYPE    | PARENT | LOCATIONS     |
+      | feature | feature | main   | local, origin |
+    And the commits
+      | BRANCH  | LOCATION | MESSAGE               |
+      | main    | local    | local main commit     |
+      |         | origin   | origin main commit    |
+      | feature | local    | local feature commit  |
+      |         | origin   | origin feature commit |
+    And Git setting "git-town.sync-feature-strategy" is "rebase"
+    And the current branch is "feature"
+    And branch "main" is active in another worktree
+    When I run "git-town sync"
+
+  Scenario: result
+    Then Git Town runs the commands
+      | BRANCH  | COMMAND                                                                      |
+      | feature | git fetch --prune --tags                                                     |
+      |         | git push --force-with-lease --force-if-includes                              |
+      |         | git -c rebase.updateRefs=false rebase origin/feature                         |
+      |         | git -c rebase.updateRefs=false rebase --onto main {{ sha 'initial commit' }} |
+      |         | git push --force-with-lease --force-if-includes                              |
+    And these commits exist now
+      | BRANCH  | LOCATION      | MESSAGE               |
+      | main    | origin        | origin main commit    |
+      |         | worktree      | local main commit     |
+      | feature | local, origin | origin feature commit |
+      |         |               | local feature commit  |
+      |         | origin        | local main commit     |
+
+  Scenario: undo
+    When I run "git-town undo"
+    Then Git Town runs the commands
+      | BRANCH  | COMMAND                                                                                |
+      | feature | git reset --hard {{ sha 'local feature commit' }}                                      |
+      |         | git push --force-with-lease origin {{ sha-in-origin 'origin feature commit' }}:feature |
+    And the initial commits exist now
diff --git a/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/parent_in_other_worktree.feature b/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/parent_in_other_worktree.feature
index cd0bc0ff5..9ff8bc356 100644
--- a/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/parent_in_other_worktree.feature
+++ b/features/sync/current_branch/feature_branch/rebase_sync_strategy/worktree/parent_in_other_worktree.feature
@@ -21,25 +21,25 @@ Feature: sync a branch whose parent is active in another worktree
 
   Scenario: result
     Then Git Town runs the commands
-      | BRANCH | COMMAND                                                                               |
-      | child  | git fetch --prune --tags                                                              |
-      |        | git checkout main                                                                     |
-      | main   | git -c rebase.updateRefs=false rebase origin/main                                     |
-      |        | git push                                                                              |
-      |        | git checkout child                                                                    |
-      | child  | git push --force-with-lease --force-if-includes                                       |
-      |        | git -c rebase.updateRefs=false rebase origin/child                                    |
-      |        | git -c rebase.updateRefs=false rebase --onto origin/parent {{ sha 'initial commit' }} |
-      |        | git push --force-with-lease --force-if-includes                                       |
+      | BRANCH | COMMAND                                                                        |
+      | child  | git fetch --prune --tags                                                       |
+      |        | git checkout main                                                              |
+      | main   | git -c rebase.updateRefs=false rebase origin/main                              |
+      |        | git push                                                                       |
+      |        | git checkout child                                                             |
+      | child  | git push --force-with-lease --force-if-includes                                |
+      |        | git -c rebase.updateRefs=false rebase origin/child                             |
+      |        | git -c rebase.updateRefs=false rebase --onto parent {{ sha 'initial commit' }} |
+      |        | git push --force-with-lease --force-if-includes                                |
     And these commits exist now
       | BRANCH | LOCATION                | MESSAGE              |
       | main   | local, origin, worktree | origin main commit   |
       |        |                         | local main commit    |
       | parent | origin                  | origin parent commit |
       |        | worktree                | local parent commit  |
-      | child  | local                   | origin parent commit |
-      |        | local, origin           | origin child commit  |
+      | child  | local, origin           | origin child commit  |
       |        |                         | local child commit   |
+      |        | origin                  | local parent commit  |
 
   Scenario: undo
     When I run "git-town undo"
diff --git a/features/sync/current_branch/perennial_branch/tracking/deleted.feature b/features/sync/current_branch/perennial_branch/tracking/deleted.feature
index 58681a7b4..49a8d2f81 100644
--- a/features/sync/current_branch/perennial_branch/tracking/deleted.feature
+++ b/features/sync/current_branch/perennial_branch/tracking/deleted.feature
@@ -15,9 +15,10 @@ Feature: sync perennial branch that was deleted at the remote
     Then Git Town runs the commands
       | BRANCH    | COMMAND                  |
       | perennial | git fetch --prune --tags |
+      |           | git checkout feature-1   |
+      | feature-1 | git branch -D perennial  |
       |           | git checkout main        |
-      | main      | git branch -D perennial  |
-      |           | git push --tags          |
+      | main      | git push --tags          |
     And Git Town prints:
       """
       deleted branch "perennial"
diff --git a/features/sync/stack/compress_sync_strategy/branches_gone/ancestors_missing_all.feature b/features/sync/stack/compress_sync_strategy/branches_gone/ancestors_missing_all.feature
index 662726f0c..c6c6ef297 100644
--- a/features/sync/stack/compress_sync_strategy/branches_gone/ancestors_missing_all.feature
+++ b/features/sync/stack/compress_sync_strategy/branches_gone/ancestors_missing_all.feature
@@ -29,7 +29,7 @@ Feature: stacked changes where all ancestor branches aren't local
       |        | git merge --no-edit --ff origin/alpha |
       |        | git merge --no-edit --ff origin/main  |
       |        | git merge --no-edit --ff origin/gamma |
-      |        | git reset --soft origin/beta          |
+      |        | git reset --soft origin/beta --       |
       |        | git commit -m "local gamma commit"    |
       |        | git push --force-with-lease           |
     And all branches are now synchronized
diff --git a/features/sync/stack/compress_sync_strategy/branches_gone/ancestors_missing_some.feature b/features/sync/stack/compress_sync_strategy/branches_gone/ancestors_missing_some.feature
index cc0122049..f0527a2c6 100644
--- a/features/sync/stack/compress_sync_strategy/branches_gone/ancestors_missing_some.feature
+++ b/features/sync/stack/compress_sync_strategy/branches_gone/ancestors_missing_some.feature
@@ -28,14 +28,14 @@ Feature: stacked changes where an ancestor branch isn't local
       |        | git checkout alpha                    |
       | alpha  | git merge --no-edit --ff origin/main  |
       |        | git merge --no-edit --ff origin/alpha |
-      |        | git reset --soft origin/main          |
+      |        | git reset --soft origin/main --       |
       |        | git commit -m "local alpha commit"    |
       |        | git push --force-with-lease           |
       |        | git checkout gamma                    |
       | gamma  | git merge --no-edit --ff origin/beta  |
       |        | git merge --no-edit --ff alpha        |
       |        | git merge --no-edit --ff origin/gamma |
-      |        | git reset --soft origin/beta          |
+      |        | git reset --soft origin/beta --       |
       |        | git commit -m "local gamma commit"    |
       |        | git push --force-with-lease           |
     And all branches are now synchronized
diff --git a/features/sync/stack/compress_sync_strategy/branches_gone/dependent_ancestor_shipped.feature b/features/sync/stack/compress_sync_strategy/branches_gone/dependent_ancestor_shipped.feature
index 91fe6abbe..95da05ee0 100644
--- a/features/sync/stack/compress_sync_strategy/branches_gone/dependent_ancestor_shipped.feature
+++ b/features/sync/stack/compress_sync_strategy/branches_gone/dependent_ancestor_shipped.feature
@@ -31,7 +31,7 @@ Feature: shipped the head branch of a synced stack with dependent changes
       |        | git checkout --ours file                          |
       |        | git add file                                      |
       |        | git commit --no-edit                              |
-      |        | git reset --soft main                             |
+      |        | git reset --soft main --                          |
       |        | git commit -m "beta commit"                       |
       |        | git push --force-with-lease                       |
     And all branches are now synchronized
diff --git a/features/sync/stack/compress_sync_strategy/branches_gone/dependent_ancestor_shipped_and_related_change_on_main.feature b/features/sync/stack/compress_sync_strategy/branches_gone/dependent_ancestor_shipped_and_related_change_on_main.feature
index 2112c0004..ef17dd91b 100644
--- a/features/sync/stack/compress_sync_strategy/branches_gone/dependent_ancestor_shipped_and_related_change_on_main.feature
+++ b/features/sync/stack/compress_sync_strategy/branches_gone/dependent_ancestor_shipped_and_related_change_on_main.feature
@@ -54,7 +54,7 @@ Feature: shipped the head branch of a synced stack with dependent changes that c
     Then Git Town runs the commands
       | BRANCH | COMMAND                     |
       | beta   | git commit --no-edit        |
-      |        | git reset --soft main       |
+      |        | git reset --soft main --    |
       |        | git commit -m "beta commit" |
       |        | git push --force-with-lease |
     And all branches are now synchronized
diff --git a/features/sync/stack/compress_sync_strategy/branches_gone/dependent_ancestor_shipped_and_unrelated_change_on_main.feature b/features/sync/stack/compress_sync_strategy/branches_gone/dependent_ancestor_shipped_and_unrelated_change_on_main.feature
index b4792a9bb..6bb001c14 100644
--- a/features/sync/stack/compress_sync_strategy/branches_gone/dependent_ancestor_shipped_and_unrelated_change_on_main.feature
+++ b/features/sync/stack/compress_sync_strategy/branches_gone/dependent_ancestor_shipped_and_unrelated_change_on_main.feature
@@ -33,7 +33,7 @@ Feature: shipped the head branch of a synced stack with dependent changes while
       |        | git checkout --ours file                          |
       |        | git add file                                      |
       |        | git commit --no-edit                              |
-      |        | git reset --soft main                             |
+      |        | git reset --soft main --                          |
       |        | git commit -m "beta commit"                       |
       |        | git push --force-with-lease                       |
     And all branches are now synchronized
diff --git a/features/sync/stack/compress_sync_strategy/branches_gone/independent_ancestors_shipped.feature b/features/sync/stack/compress_sync_strategy/branches_gone/independent_ancestors_shipped.feature
index e9f738e4f..93789cc98 100644
--- a/features/sync/stack/compress_sync_strategy/branches_gone/independent_ancestors_shipped.feature
+++ b/features/sync/stack/compress_sync_strategy/branches_gone/independent_ancestors_shipped.feature
@@ -30,7 +30,7 @@ Feature: sync a stack that contains shipped parent branches using the "compress"
       |           | git branch -D feature-2                           |
       |           | git checkout feature-3                            |
       | feature-3 | git merge --no-edit --ff main                     |
-      |           | git reset --soft main                             |
+      |           | git reset --soft main --                          |
       |           | git commit -m "feature-3 commit A"                |
       |           | git push --force-with-lease                       |
     And Git Town prints:
diff --git a/features/sync/stack/compress_sync_strategy/branches_gone/parent_shipped.feature b/features/sync/stack/compress_sync_strategy/branches_gone/parent_shipped.feature
index bf85466fb..7abaa74da 100644
--- a/features/sync/stack/compress_sync_strategy/branches_gone/parent_shipped.feature
+++ b/features/sync/stack/compress_sync_strategy/branches_gone/parent_shipped.feature
@@ -26,7 +26,7 @@ Feature: using the "compress" strategy, sync a branch whose parent was shipped
       |        | git branch -D parent                              |
       |        | git checkout child                                |
       | child  | git merge --no-edit --ff main                     |
-      |        | git reset --soft main                             |
+      |        | git reset --soft main --                          |
       |        | git commit -m "child commit 1"                    |
       |        | git push --force-with-lease                       |
     And Git Town prints:
diff --git a/features/sync/stack/compress_sync_strategy/happy_path.feature b/features/sync/stack/compress_sync_strategy/happy_path.feature
index b8ae56f5a..a47c7886b 100644
--- a/features/sync/stack/compress_sync_strategy/happy_path.feature
+++ b/features/sync/stack/compress_sync_strategy/happy_path.feature
@@ -29,13 +29,13 @@ Feature: sync a grandchild feature branch using the "compress" strategy
       |        | git checkout parent                               |
       | parent | git merge --no-edit --ff main                     |
       |        | git merge --no-edit --ff origin/parent            |
-      |        | git reset --soft main                             |
+      |        | git reset --soft main --                          |
       |        | git commit -m "local parent commit"               |
       |        | git push --force-with-lease                       |
       |        | git checkout child                                |
       | child  | git merge --no-edit --ff parent                   |
       |        | git merge --no-edit --ff origin/child             |
-      |        | git reset --soft parent                           |
+      |        | git reset --soft parent --                        |
       |        | git commit -m "local child commit"                |
       |        | git push --force-with-lease                       |
     And all branches are now synchronized
diff --git a/features/version/version.feature b/features/version/version.feature
index 5bc571413..d69f68f06 100644
--- a/features/version/version.feature
+++ b/features/version/version.feature
@@ -6,5 +6,5 @@ Feature: show the version of the current Git Town installation
     When I run "git-town --version"
     Then Git Town prints:
       """
-      Git Town 22.1.0
+      Git Town 22.2.0
       """
diff --git a/features/walk/worktree/no_args.feature b/features/walk/worktree/no_args.feature
new file mode 100644
index 000000000..a76f7a490
--- /dev/null
+++ b/features/walk/worktree/no_args.feature
@@ -0,0 +1,35 @@
+Feature: walk each branch of a stack without arguments when some branches are checked out in another worktree
+
+  Scenario: action
+    Given a Git repo with origin
+    And the branches
+      | NAME     | TYPE    | PARENT   | LOCATIONS |
+      | branch-1 | feature | main     | local     |
+      | branch-2 | feature | branch-1 | local     |
+      | branch-3 | feature | branch-2 | local     |
+    And the current branch is "branch-2"
+    And branch "branch-1" is active in another worktree
+    When I run "git-town walk --stack"
+    Then Git Town runs no commands
+    And Git Town prints:
+      """
+      Run "git town continue" to go to the next branch.
+      """
+    And the current branch is still "branch-2"
+    When I run "git-town continue"
+    Then Git Town runs the commands
+      | BRANCH   | COMMAND               |
+      | branch-2 | git checkout branch-3 |
+    And Git Town prints:
+      """
+      Run "git town continue" to go to the next branch.
+      """
+    And the current branch is now "branch-3"
+    When I run "git-town continue"
+    Then Git Town runs the commands
+      | BRANCH   | COMMAND               |
+      | branch-3 | git checkout branch-2 |
+    And Git Town prints:
+      """
+      Branch walk done.
+      """
diff --git a/features/walk/worktree/with_args.feature b/features/walk/worktree/with_args.feature
new file mode 100644
index 000000000..f77b822d3
--- /dev/null
+++ b/features/walk/worktree/with_args.feature
@@ -0,0 +1,22 @@
+Feature: walk each branch of a stack with arguments when some branches are checked out in another worktree
+
+  Scenario: iterate the full stack
+    Given a Git repo with origin
+    And the branches
+      | NAME     | TYPE    | PARENT   | LOCATIONS |
+      | branch-1 | feature | main     | local     |
+      | branch-2 | feature | branch-1 | local     |
+      | branch-3 | feature | branch-2 | local     |
+    And the current branch is "branch-2"
+    And branch "branch-1" is active in another worktree
+    When I run "git-town walk --stack echo hello"
+    Then Git Town runs the commands
+      | BRANCH   | COMMAND               |
+      | branch-2 | echo hello            |
+      |          | git checkout branch-3 |
+      | branch-3 | echo hello            |
+      |          | git checkout branch-2 |
+    And Git Town prints:
+      """
+      Branch walk done.
+      """
diff --git a/go.mod b/go.mod
index 22489404d..c041b9572 100644
--- a/go.mod
+++ b/go.mod
@@ -1,42 +1,48 @@
 module github.com/git-town/git-town/v22
 
-go 1.24.6
+go 1.24.9
 
 require (
-	code.gitea.io/sdk/gitea v0.21.0
-	codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.1.0
+	code.gitea.io/sdk/gitea v0.22.1
+	codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0
 	github.com/BurntSushi/toml v1.5.0
 	github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
-	github.com/carlmjohnson/requests v0.24.3
+	github.com/carlmjohnson/requests v0.25.1
 	github.com/charmbracelet/bubbles v0.21.0
-	github.com/charmbracelet/bubbletea v1.3.5
-	github.com/cucumber/godog v0.15.0
+	github.com/charmbracelet/bubbletea v1.3.10
+	github.com/cucumber/godog v0.15.1
 	github.com/cucumber/messages/go/v21 v21.0.1
 	github.com/davecgh/go-spew v1.1.1
 	github.com/dustin/go-humanize v1.0.1
 	github.com/google/go-cmp v0.7.0
 	github.com/google/go-github/v58 v58.0.0
+	github.com/invopop/jsonschema v0.13.0
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
 	github.com/kr/pretty v0.3.1
-	github.com/ktrysmt/go-bitbucket v0.9.86
+	github.com/ktrysmt/go-bitbucket v0.9.87
 	github.com/muesli/termenv v0.16.0
 	github.com/sergi/go-diff v1.4.0
-	github.com/shoenig/test v1.12.1
-	github.com/spf13/cobra v1.9.1
-	github.com/spf13/pflag v1.0.6
-	gitlab.com/gitlab-org/api/client-go v0.130.1
-	golang.org/x/oauth2 v0.30.0
+	github.com/shoenig/test v1.12.2
+	github.com/spf13/cobra v1.10.1
+	github.com/spf13/pflag v1.0.10
+	gitlab.com/gitlab-org/api/client-go v0.159.0
+	golang.org/x/oauth2 v0.32.0
 )
 
 require (
 	github.com/42wim/httpsig v1.2.3 // indirect
 	github.com/atotto/clipboard v0.1.4 // indirect
 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
-	github.com/charmbracelet/colorprofile v0.3.1 // indirect
+	github.com/bahlo/generic-list-go v0.2.0 // indirect
+	github.com/buger/jsonparser v1.1.1 // indirect
+	github.com/charmbracelet/colorprofile v0.3.3 // indirect
 	github.com/charmbracelet/lipgloss v1.1.0 // indirect
-	github.com/charmbracelet/x/ansi v0.9.2 // indirect
+	github.com/charmbracelet/x/ansi v0.10.3 // indirect
 	github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
-	github.com/charmbracelet/x/term v0.2.1 // indirect
+	github.com/charmbracelet/x/term v0.2.2 // indirect
+	github.com/clipperhouse/displaywidth v0.4.1 // indirect
+	github.com/clipperhouse/stringish v0.1.1 // indirect
+	github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
 	github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect
 	github.com/davidmz/go-pageant v1.0.2 // indirect
 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
@@ -46,25 +52,28 @@ require (
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
 	github.com/hashicorp/go-memdb v1.3.5 // indirect
-	github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
+	github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
 	github.com/hashicorp/go-version v1.7.0 // indirect
 	github.com/hashicorp/golang-lru v1.0.2 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/kr/text v0.2.0 // indirect
-	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+	github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-localereader v0.0.1 // indirect
-	github.com/mattn/go-runewidth v0.0.16 // indirect
+	github.com/mattn/go-runewidth v0.0.19 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
 	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/rogpeppe/go-internal v1.12.0 // indirect
+	github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
-	golang.org/x/crypto v0.39.0 // indirect
-	golang.org/x/net v0.41.0 // indirect
-	golang.org/x/sync v0.15.0 // indirect
-	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/text v0.26.0 // indirect
-	golang.org/x/time v0.12.0 // indirect
+	golang.org/x/crypto v0.43.0 // indirect
+	golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
+	golang.org/x/net v0.46.0 // indirect
+	golang.org/x/sys v0.37.0 // indirect
+	golang.org/x/text v0.30.0 // indirect
+	golang.org/x/time v0.14.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 61a95b069..be22e65bd 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,7 @@
-code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
-code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
-codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.1.0 h1:BsxZzd3nPPMyv8r6W964OL7XgTpsXvxO8f0vBTWpPok=
-codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.1.0/go.mod h1:2i9GsyawlJtVMO5pTS/Om5uo2O3JN/eCjGWy5v15NGg=
+code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA=
+code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
+codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 h1:HTCWpzyWQOHDWt3LzI6/d2jvUDsw/vgGRWm/8BTvcqI=
+codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0/go.mod h1:ZglEEDj+qkxYUb+SQIeqGtFxQrbaMYqIOgahNKb7uxs=
 github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
 github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
 github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
@@ -12,29 +12,39 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
 github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
-github.com/carlmjohnson/requests v0.24.3 h1:LYcM/jVIVPkioigMjEAnBACXl2vb42TVqiC8EYNoaXQ=
-github.com/carlmjohnson/requests v0.24.3/go.mod h1:duYA/jDnyZ6f3xbcF5PpZ9N8clgopubP2nK5i6MVMhU=
+github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
+github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
+github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
+github.com/carlmjohnson/requests v0.25.1 h1:17zNRLecxtAjhtdEIV+F+wrYfe+AGZUjWJtpndcOUYA=
+github.com/carlmjohnson/requests v0.25.1/go.mod h1:z3UEf8IE4sZxZ78spW6/tLdqBkfCu1Fn4RaYMnZ8SRM=
 github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
 github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
-github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
-github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
-github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
-github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
+github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
+github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
+github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
+github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
 github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
 github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
-github.com/charmbracelet/x/ansi v0.9.2 h1:92AGsQmNTRMzuzHEYfCdjQeUzTrgE1vfO5/7fEVoXdY=
-github.com/charmbracelet/x/ansi v0.9.2/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
+github.com/charmbracelet/x/ansi v0.10.3 h1:5Rt4C4qzYbla134jKfSE+xoeLG1o26z/H7cQ39dCKPU=
+github.com/charmbracelet/x/ansi v0.10.3/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
 github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
 github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
-github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
-github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
+github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
+github.com/clipperhouse/displaywidth v0.4.1 h1:uVw9V8UDfnggg3K2U84VWY1YLQ/x2aKSCtkRyYozfoU=
+github.com/clipperhouse/displaywidth v0.4.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
+github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
+github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
+github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
+github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI=
 github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0=
-github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo=
-github.com/cucumber/godog v0.15.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces=
+github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI=
+github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8=
 github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI=
 github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s=
 github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs=
@@ -72,8 +82,8 @@ github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjh
 github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
 github.com/hashicorp/go-memdb v1.3.5 h1:b3taDMxCBCBVgyRrS1AZVHO14ubMYZB++QpNhBg+Nyo=
 github.com/hashicorp/go-memdb v1.3.5/go.mod h1:8IVKKBkVe+fxFgdFOYxzQQNjz+sWCyHCdIC/+5+Vy1Y=
-github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
-github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
+github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
+github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
 github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
 github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
@@ -85,6 +95,9 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP
 github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
+github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -95,18 +108,20 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/ktrysmt/go-bitbucket v0.9.86 h1:co80t9wS4kKgRLsvDgi+3wQPiLY30u10ehcTxgXFvzw=
-github.com/ktrysmt/go-bitbucket v0.9.86/go.mod h1:/lsYmiQrBHNPTnPKF0Q+safIS7peInQOGbJXu0xPRho=
-github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
-github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/ktrysmt/go-bitbucket v0.9.87 h1:eR7E4ndyKpO2+HdBwUlsC5K/40nEDpjMLEWsPLY97oQ=
+github.com/ktrysmt/go-bitbucket v0.9.87/go.mod h1:slSdGm9Vh3L2ZOU1r7Fu2B9rPJvsflYgneRCoPA83eY=
+github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
+github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
 github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
-github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
-github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
+github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
@@ -118,7 +133,6 @@ github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
@@ -127,14 +141,16 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
 github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
-github.com/shoenig/test v1.12.1 h1:mLHfnMv7gmhhP44WrvT+nKSxKkPDiNkIuHGdIGI9RLU=
-github.com/shoenig/test v1.12.1/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
+github.com/shoenig/test v1.12.2 h1:ZVT8NeIUwGWpZcKaepPmFMoNQ3sVpxvqUh/MAqwFiJI=
+github.com/shoenig/test v1.12.2/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
 github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
-github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
-github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
+github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
-github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -143,47 +159,48 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
+github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
-gitlab.com/gitlab-org/api/client-go v0.130.1 h1:1xF5C5Zq3sFeNg3PzS2z63oqrxifne3n/OnbI7nptRc=
-gitlab.com/gitlab-org/api/client-go v0.130.1/go.mod h1:ZhSxLAWadqP6J9lMh40IAZOlOxBLPRh7yFOXR/bMJWM=
+gitlab.com/gitlab-org/api/client-go v0.159.0 h1:ibKeribio/OCsrsUz7pkgIN4E7HWDyrw/lDR6P2R7lU=
+gitlab.com/gitlab-org/api/client-go v0.159.0/go.mod h1:D0DHF7ILUfFo/JcoGMAEndiKMm8SiP/WjyJ4OfXxCKw=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
-golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
-golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
-golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
-golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
+golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
+golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
+golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
-golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
-golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
-golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
-golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
-golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
+golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
+golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
+golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
-golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
-golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
+golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
+golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
-golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
-golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
-golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/go.work b/go.work
index e36b31df7..bb64ab32e 100644
--- a/go.work
+++ b/go.work
@@ -1,10 +1,11 @@
-go 1.24.6
+go 1.24.9
 
 use (
 	.
 	./tools/format_self
 	./tools/format_unittests
 	./tools/iterate_map
+	./tools/lint_cached_connectors
 	./tools/lint_steps
 	./tools/messages_sorted
 	./tools/messy_output
diff --git a/internal/browser/env.go b/internal/browser/env.go
deleted file mode 100644
index e09ee76fd..000000000
--- a/internal/browser/env.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package browser
-
-// name of the environment variable that defines the browser to use
-const EnvVarName = "BROWSER"
-
-// if EnvVarName content is this value, pretend that you cannot open the browser
-const EnvVarNone = "(none)"
diff --git a/internal/browser/open.go b/internal/browser/open.go
index 95143f81d..244a8869e 100644
--- a/internal/browser/open.go
+++ b/internal/browser/open.go
@@ -3,10 +3,10 @@ package browser
 
 import (
 	"fmt"
-	"os"
 	"os/exec"
 	"runtime"
 
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/messages"
 	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
@@ -14,8 +14,8 @@ import (
 
 // Open opens a new window/tab in the default browser with the given URL.
 // If no browser is found, it prints the URL.
-func Open(url string, frontend subshelldomain.Runner) {
-	command, hasCommand := OpenBrowserCommand().Get()
+func Open(url string, frontend subshelldomain.Runner, config Option[configdomain.Browser]) {
+	command, hasCommand := OpenBrowserCommand(config).Get()
 	if !hasCommand {
 		fmt.Printf(messages.BrowserOpen, url)
 		return
@@ -26,7 +26,7 @@ func Open(url string, frontend subshelldomain.Runner) {
 }
 
 // OpenBrowserCommand provides the console command to open the default browser.
-func OpenBrowserCommand() Option[string] {
+func OpenBrowserCommand(config Option[configdomain.Browser]) Option[string] {
 	if runtime.GOOS == "windows" {
 		// NOTE: the "explorer" command cannot handle special characters like "?" and "=".
 		//       In particular, "?" can be escaped via "\", but "=" cannot.
@@ -34,11 +34,11 @@ func OpenBrowserCommand() Option[string] {
 		return Some("start")
 	}
 	openBrowserCommands := make([]string, 0, 11)
-	if browser := os.Getenv(EnvVarName); browser != "" {
-		if browser == EnvVarNone {
+	if browser, hasBrowser := config.Get(); hasBrowser {
+		if browser.NoBrowser() {
 			return None[string]()
 		}
-		openBrowserCommands = append(openBrowserCommands, browser)
+		openBrowserCommands = append(openBrowserCommands, browser.String())
 	}
 	openBrowserCommands = append(openBrowserCommands,
 		"wsl-open",           // for Windows Subsystem for Linux, see https://github.com/git-town/git-town/issues/1344
diff --git a/internal/cli/dialog/branch_prefix.go b/internal/cli/dialog/branch_prefix.go
new file mode 100644
index 000000000..d16b8dfc2
--- /dev/null
+++ b/internal/cli/dialog/branch_prefix.go
@@ -0,0 +1,46 @@
+package dialog
+
+import (
+	"cmp"
+	"fmt"
+
+	"github.com/git-town/git-town/v22/internal/cli/dialog/dialogcomponents"
+	"github.com/git-town/git-town/v22/internal/cli/dialog/dialogdomain"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
+	"github.com/git-town/git-town/v22/internal/messages"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+const (
+	branchPrefixTitle = `Branch prefix`
+	BranchPrefixHelp  = `
+When set, Git Town will automatically
+add this prefix to branches it creates or renames.
+
+For example, if set to "feature-",
+running "git town hack example"
+will create "feature-example".
+
+If you're not sure what to enter here,
+it's safe to leave it blank.
+
+`
+)
+
+func BranchPrefix(args Args[configdomain.BranchPrefix]) (Option[configdomain.BranchPrefix], dialogdomain.Exit, error) {
+	input, exit, errInput := dialogcomponents.TextField(dialogcomponents.TextFieldArgs{
+		DialogName:    "branch-prefix",
+		ExistingValue: args.Local.Or(args.Global).StringOr(""),
+		Help:          BranchPrefixHelp,
+		Inputs:        args.Inputs,
+		Prompt:        messages.BranchPrefixPrompt,
+		Title:         branchPrefixTitle,
+	})
+	newValue, errNewValue := configdomain.ParseBranchPrefix(input, "dialog")
+	if args.Global.Equal(newValue) {
+		// the user has entered the global value --> keep using the global value, don't store the local value
+		newValue = None[configdomain.BranchPrefix]()
+	}
+	fmt.Printf(messages.BranchPrefixResult, dialogcomponents.FormattedOption(newValue, args.Global.IsSome(), exit))
+	return newValue, exit, cmp.Or(errInput, errNewValue)
+}
diff --git a/internal/cli/dialog/commits_to_beam.go b/internal/cli/dialog/commits_to_beam.go
index c815bd1fa..bb5f13f93 100644
--- a/internal/cli/dialog/commits_to_beam.go
+++ b/internal/cli/dialog/commits_to_beam.go
@@ -14,7 +14,7 @@ import (
 
 const commitsToBeamTitle = `Select the commits to beam into branch %q`
 
-// lets the user select commits to beam to the target branch
+// CommitsToBeam lets the user select commits to beam to the target branch.
 func CommitsToBeam(commits []gitdomain.Commit, targetBranch gitdomain.LocalBranchName, git git.Commands, querier subshelldomain.Querier, inputs dialogcomponents.Inputs) (gitdomain.Commits, dialogdomain.Exit, error) {
 	if len(commits) == 0 {
 		return gitdomain.Commits{}, false, nil
diff --git a/internal/cli/dialog/contribution_regex.go b/internal/cli/dialog/contribution_regex.go
index d4675a24c..76cc8843f 100644
--- a/internal/cli/dialog/contribution_regex.go
+++ b/internal/cli/dialog/contribution_regex.go
@@ -35,7 +35,7 @@ func ContributionRegex(args Args[configdomain.ContributionRegex]) (Option[config
 		Prompt:        messages.ContributionRegexPrompt,
 		Title:         contributionRegexTitle,
 	})
-	newValue, errNewValue := configdomain.ParseContributionRegex(input)
+	newValue, errNewValue := configdomain.ParseContributionRegex(input, "dialog")
 	if args.Global.Equal(newValue) {
 		// the user has entered the global value --> keep using the global value, don't store the local value
 		newValue = None[configdomain.ContributionRegex]()
diff --git a/internal/cli/dialog/dialogcolors/dialog_colors.go b/internal/cli/dialog/dialogcolors/dialog_colors.go
index f57772cd6..1791585f7 100644
--- a/internal/cli/dialog/dialogcolors/dialog_colors.go
+++ b/internal/cli/dialog/dialogcolors/dialog_colors.go
@@ -5,7 +5,7 @@ import (
 	"github.com/muesli/termenv"
 )
 
-// Typical colors used in BubbleTea dialogs.
+// DialogColors defines the colors used in dialogs.
 type DialogColors struct {
 	EntryNumber termenv.Style // color for the number of entries
 	Help        termenv.Style // color of help text
diff --git a/internal/cli/dialog/dialogcomponents/checklist.go b/internal/cli/dialog/dialogcomponents/checklist.go
index e71a1fb76..9b0f6735c 100644
--- a/internal/cli/dialog/dialogcomponents/checklist.go
+++ b/internal/cli/dialog/dialogcomponents/checklist.go
@@ -10,7 +10,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/gohacks/slice"
 )
 
-// lets the user select zero, one, or many of the given entries
+// CheckList lets the user select zero, one, or many of the given entries.
 func CheckList[S any](entries list.Entries[S], selections []int, title, help string, inputs Inputs, dialogName string) (selected []S, exit dialogdomain.Exit, err error) {
 	cursor := entries.FirstEnabled()
 	program := tea.NewProgram(CheckListModel[S]{
@@ -32,7 +32,7 @@ type CheckListModel[S any] struct {
 	title      string // title to display before the help text
 }
 
-// provides all checked list entries
+// CheckedEntries provides all checked list entries.
 func (self CheckListModel[S]) CheckedEntries() []S {
 	result := []S{}
 	for e, entry := range self.Entries {
@@ -43,13 +43,13 @@ func (self CheckListModel[S]) CheckedEntries() []S {
 	return result
 }
 
-// unchecks the currently selected list entry
+// DisableCurrentEntry unchecks the currently selected list entry.
 func (self CheckListModel[S]) DisableCurrentEntry() CheckListModel[S] {
 	self.Selections = slice.Remove(self.Selections, self.Cursor)
 	return self
 }
 
-// checks the currently selected list entry
+// EnableCurrentEntry checks the currently selected list entry.
 func (self CheckListModel[S]) EnableCurrentEntry() CheckListModel[S] {
 	self.Selections = slice.AppendAllMissing(self.Selections, self.Cursor)
 	return self
@@ -59,12 +59,12 @@ func (self CheckListModel[S]) Init() tea.Cmd {
 	return nil
 }
 
-// indicates whether the row with the given number is checked or not
+// IsRowChecked indicates whether the row with the given number is checked or not.
 func (self CheckListModel[S]) IsRowChecked(row int) bool {
 	return slices.Contains(self.Selections, row)
 }
 
-// unchecks the currently selected list entry if it is checked,
+// ToggleCurrentEntry unchecks the currently selected list entry if it is checked,
 // and checks it if it is unchecked
 func (self CheckListModel[S]) ToggleCurrentEntry() CheckListModel[S] {
 	if self.IsRowChecked(self.Cursor) {
diff --git a/internal/cli/dialog/dialogcomponents/colors.go b/internal/cli/dialog/dialogcomponents/colors.go
index 040a42cce..e989a6065 100644
--- a/internal/cli/dialog/dialogcomponents/colors.go
+++ b/internal/cli/dialog/dialogcomponents/colors.go
@@ -22,7 +22,7 @@ func FormattedOption[T fmt.Stringer](userInput Option[T], hasGlobal bool, exit d
 	return colors.Green().Styled("(not provided)")
 }
 
-// FormattedToken provides the given API token in a printable format.
+// FormattedSecret provides the given API token in a printable format.
 func FormattedSecret(secret string, exit dialogdomain.Exit) string {
 	if exit {
 		return colors.Red().Styled("(aborted)")
diff --git a/internal/cli/dialog/dialogcomponents/input.go b/internal/cli/dialog/dialogcomponents/input.go
index 559b051ef..c051a8708 100644
--- a/internal/cli/dialog/dialogcomponents/input.go
+++ b/internal/cli/dialog/dialogcomponents/input.go
@@ -24,7 +24,7 @@ func ParseInput(envData string) Input {
 	if !has {
 		panic(fmt.Sprintf("found test input without step name: %q", envData))
 	}
-	for _, input := range strings.Split(keys, "|") {
+	for input := range strings.SplitSeq(keys, "|") {
 		if len(input) > 0 {
 			messages = append(messages, recognizeInput(input))
 		}
diff --git a/internal/cli/dialog/dialogcomponents/list/entries.go b/internal/cli/dialog/dialogcomponents/list/entries.go
index 78f967e2c..d28df0c46 100644
--- a/internal/cli/dialog/dialogcomponents/list/entries.go
+++ b/internal/cli/dialog/dialogcomponents/list/entries.go
@@ -9,7 +9,7 @@ import (
 // Entries provides methods for a collection of Entry instances.
 type Entries[S any] []Entry[S]
 
-// creates an Entries instance containing the given records
+// NewEntries creates an Entries instance containing the given records.
 func NewEntries[S fmt.Stringer](records ...S) Entries[S] {
 	result := make([]Entry[S], len(records))
 	for r, record := range records {
@@ -22,7 +22,7 @@ func NewEntries[S fmt.Stringer](records ...S) Entries[S] {
 	return result
 }
 
-// indicates whether all entries in this list are disabled
+// AllDisabled indicates whether all entries in this list are disabled.
 func (self Entries[S]) AllDisabled() bool {
 	for _, entry := range self {
 		if !entry.Disabled {
@@ -32,7 +32,7 @@ func (self Entries[S]) AllDisabled() bool {
 	return true
 }
 
-// provides the index of the first entry that is not disabled
+// FirstEnabled provides the index of the first entry that is not disabled.
 func (self Entries[S]) FirstEnabled() int {
 	for e, entry := range self {
 		if !entry.Disabled {
@@ -42,7 +42,7 @@ func (self Entries[S]) FirstEnabled() int {
 	return 0
 }
 
-// provides the position of the given needle in this list
+// IndexOf provides the position of the given needle in this list.
 func (self Entries[S]) IndexOf(needle S) int {
 	for e, entry := range self {
 		if equal.Equal(entry.Data, needle) {
@@ -52,7 +52,7 @@ func (self Entries[S]) IndexOf(needle S) int {
 	return 0
 }
 
-// provides the position of the given needle in this list
+// IndexOfFunc provides the position of the given needle in this list.
 func (self Entries[S]) IndexOfFunc(needle S, equalFn func(a, b S) bool) int {
 	for e, entry := range self {
 		if equalFn(entry.Data, needle) {
diff --git a/internal/cli/dialog/dialogcomponents/radiolist.go b/internal/cli/dialog/dialogcomponents/radiolist.go
index d0bea4318..9bc901874 100644
--- a/internal/cli/dialog/dialogcomponents/radiolist.go
+++ b/internal/cli/dialog/dialogcomponents/radiolist.go
@@ -9,7 +9,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/gohacks/slice"
 )
 
-// how many elements to display in the dialog
+// WindowSize defines the maximum number of elements to display at one time in the dialog.
 const WindowSize = 9
 
 // RadioList lets the user select one of the given entries.
diff --git a/internal/cli/dialog/feature_regex.go b/internal/cli/dialog/feature_regex.go
index eca77e135..eb407a9ff 100644
--- a/internal/cli/dialog/feature_regex.go
+++ b/internal/cli/dialog/feature_regex.go
@@ -35,7 +35,7 @@ func FeatureRegex(args Args[configdomain.FeatureRegex]) (Option[configdomain.Fea
 		Prompt:        messages.FeatureRegexPrompt,
 		Title:         featureRegexTitle,
 	})
-	newValue, errNewValue := configdomain.ParseFeatureRegex(input)
+	newValue, errNewValue := configdomain.ParseFeatureRegex(input, "dialog")
 	if args.Global.Equal(newValue) {
 		// the user has entered the global value --> keep using the global value, don't store the local value
 		newValue = None[configdomain.FeatureRegex]()
diff --git a/internal/cli/dialog/lineage.go b/internal/cli/dialog/lineage.go
index 60955027f..511379809 100644
--- a/internal/cli/dialog/lineage.go
+++ b/internal/cli/dialog/lineage.go
@@ -44,8 +44,9 @@ func Lineage(args LineageArgs) (additionalLineage configdomain.Lineage, addition
 		// look for parent in proposals
 		if connector, hasConnector := args.Connector.Get(); hasConnector {
 			if proposalSearcher, canSearchProposals := connector.(forgedomain.ProposalSearcher); canSearchProposals {
-				proposalOpt, _ := proposalSearcher.SearchProposal(branchToVerify)
-				if proposal, hasProposal := proposalOpt.Get(); hasProposal {
+				proposals, _ := proposalSearcher.SearchProposals(branchToVerify)
+				if len(proposals) == 1 {
+					proposal := proposals[0]
 					parent := proposal.Data.Data().Target
 					additionalLineage = additionalLineage.Set(branchToVerify, parent)
 					branchesToVerify = append(branchesToVerify, parent)
diff --git a/internal/cli/dialog/observed_regex.go b/internal/cli/dialog/observed_regex.go
index fe3a0c988..07d115998 100644
--- a/internal/cli/dialog/observed_regex.go
+++ b/internal/cli/dialog/observed_regex.go
@@ -35,7 +35,7 @@ func ObservedRegex(args Args[configdomain.ObservedRegex]) (Option[configdomain.O
 		Prompt:        messages.ObservedRegexPrompt,
 		Title:         observedRegexTitle,
 	})
-	newValue, errNewValue := configdomain.ParseObservedRegex(input)
+	newValue, errNewValue := configdomain.ParseObservedRegex(input, "dialog")
 	if args.Global.Equal(newValue) {
 		// the user has entered the global value --> keep using the global value, don't store the local value
 		newValue = None[configdomain.ObservedRegex]()
diff --git a/internal/cli/dialog/perennial_regex.go b/internal/cli/dialog/perennial_regex.go
index b44d3d179..96e037d97 100644
--- a/internal/cli/dialog/perennial_regex.go
+++ b/internal/cli/dialog/perennial_regex.go
@@ -34,7 +34,7 @@ func PerennialRegex(args Args[configdomain.PerennialRegex]) (Option[configdomain
 		Prompt:        messages.PerennialRegexPrompt,
 		Title:         perennialRegexTitle,
 	})
-	newValue, errNewValue := configdomain.ParsePerennialRegex(input)
+	newValue, errNewValue := configdomain.ParsePerennialRegex(input, "dialog")
 	if args.Global.Equal(newValue) {
 		// the user has entered the global value --> keep using the global value, don't store the local value
 		newValue = None[configdomain.PerennialRegex]()
diff --git a/internal/cli/dialog/proposals_show_lineage.go b/internal/cli/dialog/proposals_show_lineage.go
new file mode 100644
index 000000000..61b5bbcca
--- /dev/null
+++ b/internal/cli/dialog/proposals_show_lineage.go
@@ -0,0 +1,50 @@
+package dialog
+
+import (
+	"fmt"
+
+	"github.com/git-town/git-town/v22/internal/cli/dialog/dialogcomponents"
+	"github.com/git-town/git-town/v22/internal/cli/dialog/dialogcomponents/list"
+	"github.com/git-town/git-town/v22/internal/cli/dialog/dialogdomain"
+	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
+	"github.com/git-town/git-town/v22/internal/messages"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+const (
+	proposalsShowLineageTitle = `Proposals Show Lineage`
+	proposalsShowLineageHelp  = `
+	How should Git Town share stack lineage in proposals?
+
+	See https://www.git-town.com/how-to/github-actions-breadcrumb.html for details.
+`
+)
+
+func ProposalsShowLineage(args Args[forgedomain.ProposalsShowLineage]) (Option[forgedomain.ProposalsShowLineage], dialogdomain.Exit, error) {
+	entries := list.Entries[Option[forgedomain.ProposalsShowLineage]]{}
+	if global, hasGlobal := args.Global.Get(); hasGlobal {
+		entries = append(entries, list.Entry[Option[forgedomain.ProposalsShowLineage]]{
+			Data: None[forgedomain.ProposalsShowLineage](),
+			Text: fmt.Sprintf(messages.DialogUseGlobalValue, global),
+		})
+	}
+
+	entries = append(entries, list.Entries[Option[forgedomain.ProposalsShowLineage]]{
+		{
+			Data: Some(forgedomain.ProposalsShowLineageNone),
+			Text: "no stack lineage in proposals",
+		},
+		{
+			Data: Some(forgedomain.ProposalsShowLineageCLI),
+			Text: "Git Town CLI embeds the stack lineage into proposals",
+		},
+		{
+			Data: Some(forgedomain.ProposalsShowLineageCI),
+			Text: "CI server embeds the stack lineage into proposals",
+		},
+	}...)
+	defaultPos := entries.IndexOf(args.Local)
+	selection, exit, err := dialogcomponents.RadioList(entries, defaultPos, proposalsShowLineageTitle, proposalsShowLineageHelp, args.Inputs, "proposals-show-lineage")
+	fmt.Printf(messages.ProposalsLineage, dialogcomponents.FormattedOption(selection, args.Global.IsSome(), exit))
+	return selection, exit, err
+}
diff --git a/internal/cli/dialog/token_scope.go b/internal/cli/dialog/token_scope.go
index a56393cd8..24221f926 100644
--- a/internal/cli/dialog/token_scope.go
+++ b/internal/cli/dialog/token_scope.go
@@ -20,7 +20,7 @@ or for all Git repos on this machine?
 `
 )
 
-// GitHubToken lets the user enter the GitHub API token.
+// TokenScope lets the user enter the GitHub API token.
 func TokenScope(oldValue configdomain.ConfigScope, inputs dialogcomponents.Inputs) (configdomain.ConfigScope, dialogdomain.Exit, error) {
 	entries := list.Entries[configdomain.ConfigScope]{
 		{
diff --git a/internal/cli/dialog/welcome.go b/internal/cli/dialog/welcome.go
index 01a778a08..1e05c1e8a 100644
--- a/internal/cli/dialog/welcome.go
+++ b/internal/cli/dialog/welcome.go
@@ -29,7 +29,6 @@ When you're ready, press ENTER or O to continue.
 `
 )
 
-// MainBranch lets the user select a new main branch for this repo.
 func Welcome(inputs dialogcomponents.Inputs) (dialogdomain.Exit, error) {
 	return dialogcomponents.TextDisplay(welcomeTitle, welcomeText, inputs, "welcome")
 }
diff --git a/internal/cli/flags/all.go b/internal/cli/flags/all.go
index 5c65ec7ba..ef4ec7dd0 100644
--- a/internal/cli/flags/all.go
+++ b/internal/cli/flags/all.go
@@ -7,7 +7,7 @@ import (
 
 const allLong = "all"
 
-// type-safe access to the CLI arguments of type configdomain.SyncAllBranches
+// All provides type-safe access to the CLI arguments of type configdomain.SyncAllBranches.
 func All(desc string) (AddFunc, ReadAllFlagFunc) {
 	addFlag := func(cmd *cobra.Command) {
 		cmd.Flags().BoolP(allLong, "a", false, desc)
diff --git a/internal/cli/flags/proposal_body.go b/internal/cli/flags/proposal_body.go
index d2dbc2e85..8ceaf5339 100644
--- a/internal/cli/flags/proposal_body.go
+++ b/internal/cli/flags/proposal_body.go
@@ -16,7 +16,14 @@ func ProposalBody(short string) (AddFunc, ReadProposalBodyFlagFunc) {
 		cmd.Flags().StringP(bodyLong, short, "", "provide a body for the proposal")
 	}
 	readFlag := func(cmd *cobra.Command) (Option[gitdomain.ProposalBody], error) {
-		return readStringOptFlag[gitdomain.ProposalBody](cmd.Flags(), bodyLong)
+		if !cmd.Flags().Changed(bodyLong) {
+			return None[gitdomain.ProposalBody](), nil
+		}
+		value, err := cmd.Flags().GetString(bodyLong)
+		if err != nil {
+			return None[gitdomain.ProposalBody](), err
+		}
+		return Some(gitdomain.ProposalBody(value)), nil
 	}
 	return addFlag, readFlag
 }
diff --git a/internal/cli/flags/ship_strategy.go b/internal/cli/flags/ship_strategy.go
index fe36e7c7b..c6cafbea7 100644
--- a/internal/cli/flags/ship_strategy.go
+++ b/internal/cli/flags/ship_strategy.go
@@ -18,7 +18,7 @@ func ShipStrategy() (AddFunc, ReadShipStrategyFunc) {
 		if err != nil {
 			return None[configdomain.ShipStrategy](), err
 		}
-		return configdomain.ParseShipStrategy(value)
+		return configdomain.ParseShipStrategy(value, "--strategy CLI flag")
 	}
 	return addFlag, readFlag
 }
diff --git a/internal/cli/flags/stack.go b/internal/cli/flags/stack.go
index 31bea23d4..ea2363d04 100644
--- a/internal/cli/flags/stack.go
+++ b/internal/cli/flags/stack.go
@@ -7,7 +7,7 @@ import (
 
 const stackLong = "stack"
 
-// type-safe access to the CLI arguments of type configdomain.FullStack
+// Stack provides type-safe access to the CLI arguments of type configdomain.FullStack.
 func Stack(description string) (AddFunc, ReadStackFlagFunc) {
 	addFlag := func(cmd *cobra.Command) {
 		cmd.Flags().BoolP(stackLong, "s", false, description)
@@ -18,5 +18,4 @@ func Stack(description string) (AddFunc, ReadStackFlagFunc) {
 	return addFlag, readFlag
 }
 
-// ReadStackFlagFunc is the type signature for the function that reads the "stack" flag from the args to the given Cobra command.
 type ReadStackFlagFunc func(*cobra.Command) (configdomain.FullStack, error)
diff --git a/internal/cli/flags/stash.go b/internal/cli/flags/stash.go
index 50151cd06..c3a4df18b 100644
--- a/internal/cli/flags/stash.go
+++ b/internal/cli/flags/stash.go
@@ -8,7 +8,7 @@ import (
 
 const stashLong = "stash"
 
-// type-safe access to the CLI arguments of type configdomain.Stash
+// Stash provides type-safe access to the CLI arguments of type configdomain.Stash.
 func Stash() (AddFunc, ReadStashFlagFunc) {
 	addFlag := func(cmd *cobra.Command) {
 		cmd.Flags().Bool(stashLong, false, "stash uncommitted changes when creating branches")
@@ -20,5 +20,4 @@ func Stash() (AddFunc, ReadStashFlagFunc) {
 	return addFlag, readFlag
 }
 
-// ReadPrototypeFlagFunc is the type signature for the function that reads the "prototype" flag from the args to the given Cobra command.
 type ReadStashFlagFunc func(*cobra.Command) (Option[configdomain.Stash], error)
diff --git a/internal/cli/flags/sync.go b/internal/cli/flags/sync.go
index 39e2b0c6a..336994943 100644
--- a/internal/cli/flags/sync.go
+++ b/internal/cli/flags/sync.go
@@ -8,7 +8,7 @@ import (
 
 const syncLong = "sync"
 
-// type-safe access to the CLI arguments of type configdomain.AutoSync
+// Sync provides type-safe access to the CLI arguments of type configdomain.AutoSync.
 func Sync() (AddFunc, ReadSyncFlagFunc) {
 	addFlag := func(cmd *cobra.Command) {
 		cmd.Flags().Bool(syncLong, true, "sync branches")
@@ -20,5 +20,4 @@ func Sync() (AddFunc, ReadSyncFlagFunc) {
 	return addFlag, readFlag
 }
 
-// ReadPrototypeFlagFunc is the type signature for the function that reads the "prototype" flag from the args to the given Cobra command.
 type ReadSyncFlagFunc func(*cobra.Command) (Option[configdomain.AutoSync], error)
diff --git a/internal/cli/flags/type.go b/internal/cli/flags/type.go
index 2341ef61c..e02dfba1a 100644
--- a/internal/cli/flags/type.go
+++ b/internal/cli/flags/type.go
@@ -13,7 +13,7 @@ const (
 	typeShort = "t"
 )
 
-// type-safe access to the CLI arguments of type configdomain.BranchType
+// BranchType provides type-safe access to the CLI arguments of type configdomain.BranchType.
 func BranchType() (AddFunc, ReadTypeFlagFunc) {
 	addFlag := func(cmd *cobra.Command) {
 		cmd.PersistentFlags().StringP(typeLong, typeShort, "", "limit the list of branches to switch to the given branch type(s)")
@@ -23,16 +23,16 @@ func BranchType() (AddFunc, ReadTypeFlagFunc) {
 		if err != nil {
 			return []configdomain.BranchType{}, err
 		}
-		return ParseBranchTypes(value)
+		return ParseBranchTypes(value, "--type flag")
 	}
 	return addFlag, readFlag
 }
 
-func ParseBranchTypes(text string) ([]configdomain.BranchType, error) {
+func ParseBranchTypes(text string, source string) ([]configdomain.BranchType, error) {
 	branchTypeNames := SplitBranchTypeNames(text)
 	result := make([]configdomain.BranchType, 0, len(branchTypeNames))
 	for _, branchTypeName := range branchTypeNames {
-		branchTypeOpt, err := configdomain.ParseBranchType(branchTypeName)
+		branchTypeOpt, err := configdomain.ParseBranchType(branchTypeName, source)
 		if err != nil {
 			return result, err
 		}
diff --git a/internal/cli/flags/verbose.go b/internal/cli/flags/verbose.go
index f24b29274..8d94c97bc 100644
--- a/internal/cli/flags/verbose.go
+++ b/internal/cli/flags/verbose.go
@@ -11,7 +11,7 @@ const (
 	verboseShort = "v"
 )
 
-// type-safe access to the CLI arguments of type configdomain.Verbose
+// Verbose provides type-safe access to the CLI arguments of type configdomain.Verbose.
 func Verbose() (AddFunc, ReadVerboseFlagFunc) {
 	addFlag := func(cmd *cobra.Command) {
 		cmd.Flags().BoolP(verboseLong, verboseShort, false, "display all Git commands run under the hood")
diff --git a/internal/cli/print/entry.go b/internal/cli/print/entry.go
index bcef00293..c3c5241d2 100644
--- a/internal/cli/print/entry.go
+++ b/internal/cli/print/entry.go
@@ -3,5 +3,9 @@ package print
 import "fmt"
 
 func Entry(label, value string) {
-	fmt.Printf("  %s: %s\n", label, value)
+	if value == "" {
+		fmt.Printf("  %s: \"\"\n", label)
+	} else {
+		fmt.Printf("  %s: %s\n", label, value)
+	}
 }
diff --git a/internal/cli/print/logger.go b/internal/cli/print/logger.go
index 294be5a0b..8ec078eb1 100644
--- a/internal/cli/print/logger.go
+++ b/internal/cli/print/logger.go
@@ -13,6 +13,14 @@ func (self Logger) Failed(failure string) {
 	self.Log(colors.BoldRed().Styled(fmt.Sprintf("%v\n", failure)))
 }
 
+func (self Logger) Finished(err error) {
+	if err != nil {
+		self.Failed(err.Error())
+	} else {
+		self.Ok()
+	}
+}
+
 func (self Logger) Log(text string) {
 	fmt.Println(text)
 }
@@ -21,7 +29,7 @@ func (self Logger) Ok() {
 	self.Success("ok")
 }
 
-func (self Logger) Start(template string, data ...interface{}) {
+func (self Logger) Start(template string, data ...any) {
 	fmt.Println()
 	fmt.Print(colors.Bold().Styled(fmt.Sprintf(template, data...)))
 }
diff --git a/internal/cmd/append.go b/internal/cmd/append.go
index 40abfbf51..f60247fe0 100644
--- a/internal/cmd/append.go
+++ b/internal/cmd/append.go
@@ -151,6 +151,7 @@ func executeAppend(args executeAppendArgs) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        args.cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -253,6 +254,7 @@ func determineAppendData(args determineAppendDataArgs, repo execute.OpenRepoResu
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
@@ -310,11 +312,15 @@ func determineAppendData(args determineAppendDataArgs, repo execute.OpenRepoResu
 	if err != nil {
 		return data, configdomain.ProgramFlowExit, err
 	}
-	if branchesSnapshot.Branches.HasLocalBranch(args.targetBranch) {
-		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchAlreadyExistsLocally, args.targetBranch)
+	targetBranch := args.targetBranch
+	if prefix, hasPrefix := config.BranchPrefix.Get(); hasPrefix {
+		targetBranch = prefix.Apply(targetBranch)
 	}
-	if branchesSnapshot.Branches.HasMatchingTrackingBranchFor(args.targetBranch, repo.UnvalidatedConfig.NormalConfig.DevRemote) {
-		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchAlreadyExistsRemotely, args.targetBranch)
+	if branchesSnapshot.Branches.HasLocalBranch(targetBranch) {
+		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchAlreadyExistsLocally, targetBranch)
+	}
+	if branchesSnapshot.Branches.HasMatchingTrackingBranchFor(targetBranch, repo.UnvalidatedConfig.NormalConfig.DevRemote) {
+		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchAlreadyExistsRemotely, targetBranch)
 	}
 	initialBranch, hasInitialBranch := branchesSnapshot.Active.Get()
 	if !hasInitialBranch {
@@ -361,7 +367,7 @@ func determineAppendData(args determineAppendDataArgs, repo execute.OpenRepoResu
 		if err != nil {
 			return data, configdomain.ProgramFlowExit, err
 		}
-		commitsToBeam, exit, err = dialog.CommitsToBeam(commitsInBranch, args.targetBranch, repo.Git, repo.Backend, inputs)
+		commitsToBeam, exit, err = dialog.CommitsToBeam(commitsInBranch, targetBranch, repo.Git, repo.Backend, inputs)
 		if err != nil || exit {
 			return data, configdomain.ProgramFlowExit, err
 		}
@@ -392,7 +398,7 @@ func determineAppendData(args determineAppendDataArgs, repo execute.OpenRepoResu
 		prototype:                 args.prototype,
 		remotes:                   remotes,
 		stashSize:                 stashSize,
-		targetBranch:              args.targetBranch,
+		targetBranch:              targetBranch,
 	}, configdomain.ProgramFlowContinue, nil
 }
 
diff --git a/internal/cmd/branch.go b/internal/cmd/branch.go
index 8e40dc29e..51b951e3c 100644
--- a/internal/cmd/branch.go
+++ b/internal/cmd/branch.go
@@ -68,6 +68,7 @@ func executeBranch(cliConfig configdomain.PartialConfig) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
diff --git a/internal/cmd/compress.go b/internal/cmd/compress.go
index 958b9a0f9..ae9a03fc1 100644
--- a/internal/cmd/compress.go
+++ b/internal/cmd/compress.go
@@ -106,6 +106,7 @@ func executeCompress(cliConfig configdomain.PartialConfig, message Option[gitdom
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -197,6 +198,7 @@ func determineCompressData(repo execute.OpenRepoResult, message Option[gitdomain
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
diff --git a/internal/cmd/config/get_parent.go b/internal/cmd/config/get_parent.go
index f5836bdff..82117c0a6 100644
--- a/internal/cmd/config/get_parent.go
+++ b/internal/cmd/config/get_parent.go
@@ -49,6 +49,7 @@ func getParentCommand() *cobra.Command {
 func executeGetParent(args []string, cliConfig configdomain.PartialConfig) error {
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: false,
 		PrintCommands:    false,
 		ValidateGitRepo:  true,
diff --git a/internal/cmd/config/remove.go b/internal/cmd/config/remove.go
index 95621fb16..5faffbe79 100644
--- a/internal/cmd/config/remove.go
+++ b/internal/cmd/config/remove.go
@@ -51,6 +51,7 @@ func removeConfigCommand() *cobra.Command {
 func executeRemoveConfig(cliConfig configdomain.PartialConfig) error {
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    true,
 		PrintBranchNames: false,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
diff --git a/internal/cmd/config/root.go b/internal/cmd/config/root.go
index 005b93a6e..f6ff74bea 100644
--- a/internal/cmd/config/root.go
+++ b/internal/cmd/config/root.go
@@ -58,6 +58,7 @@ func RootCmd() *cobra.Command {
 func executeDisplayConfig(cliConfig configdomain.PartialConfig) error {
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    true,
 		PrintBranchNames: false,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -93,11 +94,13 @@ func printConfig(config config.UnvalidatedConfig) {
 	print.Entry("git user email", format.OptionalStringerSetting(config.NormalConfig.GitUserEmail))
 	fmt.Println()
 	print.Header("Create")
+	print.Entry("branch prefix", format.OptionalStringerSetting(config.NormalConfig.BranchPrefix))
 	print.Entry("new branch type", format.OptionalStringerSetting(config.NormalConfig.NewBranchType))
 	print.Entry("share new branches", config.NormalConfig.ShareNewBranches.String())
 	print.Entry("stash uncommitted changes", format.Bool(config.NormalConfig.Stash.ShouldStash()))
 	fmt.Println()
 	print.Header("Hosting")
+	print.Entry("browser", format.OptionalStringerSetting(config.NormalConfig.Browser))
 	print.Entry("development remote", config.NormalConfig.DevRemote.String())
 	print.Entry("forge type", format.OptionalStringerSetting(config.NormalConfig.ForgeType))
 	print.Entry("origin hostname", format.OptionalStringerSetting(config.NormalConfig.HostingOriginHostname))
@@ -110,6 +113,9 @@ func printConfig(config config.UnvalidatedConfig) {
 	print.Entry("GitLab connector type", format.OptionalStringerSetting(config.NormalConfig.GitLabConnectorType))
 	print.Entry("GitLab token", format.OptionalStringerSetting(config.NormalConfig.GitLabToken))
 	fmt.Println()
+	print.Header("Propose")
+	print.Entry("lineage", format.StringsSetting(config.NormalConfig.ProposalsShowLineage.String()))
+	fmt.Println()
 	print.Header("Ship")
 	print.Entry("delete tracking branch", format.Bool(config.NormalConfig.ShipDeleteTrackingBranch.ShouldDeleteTrackingBranch()))
 	print.Entry("ship strategy", config.NormalConfig.ShipStrategy.String())
diff --git a/internal/cmd/continue.go b/internal/cmd/continue.go
index e2a137bde..618cbcb17 100644
--- a/internal/cmd/continue.go
+++ b/internal/cmd/continue.go
@@ -63,6 +63,7 @@ func executeContinue(cliConfig configdomain.PartialConfig) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -117,6 +118,7 @@ func determineContinueData(repo execute.OpenRepoResult) (data continueData, flow
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
diff --git a/internal/cmd/contribute.go b/internal/cmd/contribute.go
index cb59b16d8..240cc8359 100644
--- a/internal/cmd/contribute.go
+++ b/internal/cmd/contribute.go
@@ -71,6 +71,7 @@ func contributeCmd() *cobra.Command {
 func executeContribute(args []string, cliConfig configdomain.PartialConfig) error {
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: false,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
diff --git a/internal/cmd/delete.go b/internal/cmd/delete.go
index b9396ae98..5b89a86b6 100644
--- a/internal/cmd/delete.go
+++ b/internal/cmd/delete.go
@@ -100,6 +100,7 @@ func executeDelete(args []string, cliConfig configdomain.PartialConfig) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -187,6 +188,7 @@ func determineDeleteData(args []string, repo execute.OpenRepoResult) (data delet
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
@@ -352,35 +354,26 @@ func deleteFeatureBranch(prog, finalUndoProgram Mutable[program.Program], data d
 	}
 	deleteLocalBranch(prog, finalUndoProgram, data)
 	if data.config.NormalConfig.ProposalsShowLineage == forgedomain.ProposalsShowLineageCLI {
-		if connector, hasConnector := data.connector.Get(); hasConnector {
-			if proposalFinder, hasProposalFinder := connector.(forgedomain.ProposalFinder); hasProposalFinder {
-				tree, err := forge.NewProposalStackLineageTree(forge.ProposalStackLineageArgs{
-					Connector:                proposalFinder,
+		_ = sync.AddStackLineageUpdateOpcodes(
+			sync.AddStackLineageUpdateOpcodesArgs{
+				Current:   data.initialBranch,
+				FullStack: true,
+				Program:   prog,
+				ProposalStackLineageArgs: forge.ProposalStackLineageArgs{
+					Connector:                forgedomain.ProposalFinderFromConnector(data.connector),
 					CurrentBranch:            data.initialBranch,
 					Lineage:                  data.config.NormalConfig.Lineage,
 					MainAndPerennialBranches: data.config.MainAndPerennials(),
 					Order:                    data.config.NormalConfig.Order,
-				})
-				if err != nil {
-					fmt.Printf("failed to update proposal stack lineage: %s\n", err.Error())
-				} else {
-					for branch, proposal := range tree.BranchToProposal { // okay to iterate the map in random order
-						// Do not update the proposal of the deleted branch.
-						// At this point, a forge (like github) would close
-						// the proposal because there is no longer a remote
-						// branch.
-						if branch == data.initialBranch {
-							continue
-						}
-						prog.Value.Add(&opcodes.ProposalUpdateLineage{
-							Current:         branch,
-							CurrentProposal: proposal,
-							LineageTree:     MutableSome(tree),
-						})
-					}
-				}
-			}
-		}
+				},
+				ProposalStackLineageTree: None[*forge.ProposalStackLineageTree](),
+				// Do not update the proposal of the deleted branch.
+				// At this point, a forge (like github) would close
+				// the proposal because there is no longer a remote
+				// branch.
+				SkipUpdateForProposalsWithBaseBranch: gitdomain.LocalBranchNames{data.initialBranch},
+			},
+		)
 	}
 }
 
diff --git a/internal/cmd/detach.go b/internal/cmd/detach.go
index 53e99e220..458660732 100644
--- a/internal/cmd/detach.go
+++ b/internal/cmd/detach.go
@@ -104,6 +104,7 @@ func executeDetach(cliConfig configdomain.PartialConfig) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -163,22 +164,23 @@ Start:
 }
 
 type detachData struct {
-	branchInfosLastRun  Option[gitdomain.BranchInfos]
-	branchToDetachInfo  gitdomain.BranchInfo
-	branchToDetachName  gitdomain.LocalBranchName
-	branchToDetachType  configdomain.BranchType
-	branchesSnapshot    gitdomain.BranchesSnapshot
-	children            []detachChildBranch
-	config              config.ValidatedConfig
-	connector           Option[forgedomain.Connector]
-	descendents         []detachChildBranch
-	hasOpenChanges      bool
-	initialBranch       gitdomain.LocalBranchName
-	inputs              dialogcomponents.Inputs
-	nonExistingBranches gitdomain.LocalBranchNames // branches that are listed in the lineage information, but don't exist in the repo, neither locally nor remotely
-	parentBranch        gitdomain.LocalBranchName
-	previousBranch      Option[gitdomain.LocalBranchName]
-	stashSize           gitdomain.StashSize
+	branchInfosLastRun     Option[gitdomain.BranchInfos]
+	branchToDetachInfo     gitdomain.BranchInfo
+	branchToDetachName     gitdomain.LocalBranchName
+	branchToDetachProposal Option[forgedomain.Proposal]
+	branchToDetachType     configdomain.BranchType
+	branchesSnapshot       gitdomain.BranchesSnapshot
+	children               []detachChildBranch
+	config                 config.ValidatedConfig
+	connector              Option[forgedomain.Connector]
+	descendents            []detachChildBranch
+	hasOpenChanges         bool
+	initialBranch          gitdomain.LocalBranchName
+	inputs                 dialogcomponents.Inputs
+	nonExistingBranches    gitdomain.LocalBranchNames // branches that are listed in the lineage information, but don't exist in the repo, neither locally nor remotely
+	parentBranch           gitdomain.LocalBranchName
+	previousBranch         Option[gitdomain.LocalBranchName]
+	stashSize              gitdomain.StashSize
 }
 
 type detachChildBranch struct {
@@ -198,6 +200,7 @@ func determineDetachData(repo execute.OpenRepoResult) (data detachData, flow con
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
@@ -293,16 +296,27 @@ func determineDetachData(repo execute.OpenRepoResult) (data detachData, flow con
 	if branchHasMergeCommits {
 		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchContainsMergeCommits, branchNameToDetach)
 	}
+
+	connectorProposalFinder := None[forgedomain.ProposalFinder]()
+	branchToDetachProposal := None[forgedomain.Proposal]()
+	if connector, hasConnector := connector.Get(); hasConnector {
+		if proposalFinder, canFindProposals := connector.(forgedomain.ProposalFinder); canFindProposals {
+			connectorProposalFinder = Some(proposalFinder)
+			branchToDetachProposal, err = proposalFinder.FindProposal(branchNameToDetach, parentBranch)
+			if err != nil {
+				return data, configdomain.ProgramFlowExit, err
+			}
+		}
+	}
 	childBranches := validatedConfig.NormalConfig.Lineage.Children(branchNameToDetach, validatedConfig.NormalConfig.Order)
 	children := make([]detachChildBranch, len(childBranches))
+	proposalFinder, hasProposalFinder := connectorProposalFinder.Get()
 	for c, childBranch := range childBranches {
 		proposal := None[forgedomain.Proposal]()
-		if connector, hasConnector := connector.Get(); hasConnector {
-			if proposalFinder, canFindProposals := connector.(forgedomain.ProposalFinder); canFindProposals {
-				proposal, err = proposalFinder.FindProposal(childBranch, initialBranch)
-				if err != nil {
-					return data, configdomain.ProgramFlowExit, err
-				}
+		if hasProposalFinder {
+			proposal, err = proposalFinder.FindProposal(childBranch, initialBranch)
+			if err != nil {
+				return data, configdomain.ProgramFlowExit, err
 			}
 		}
 		childInfo, has := branchesSnapshot.Branches.FindByLocalName(childBranch).Get()
@@ -331,22 +345,23 @@ func determineDetachData(repo execute.OpenRepoResult) (data detachData, flow con
 	lineageBranches := validatedConfig.NormalConfig.Lineage.BranchNames()
 	_, nonExistingBranches := branchesSnapshot.Branches.Select(repo.UnvalidatedConfig.NormalConfig.DevRemote, lineageBranches...)
 	return detachData{
-		branchInfosLastRun:  branchInfosLastRun,
-		branchToDetachInfo:  *branchToDetachInfo,
-		branchToDetachName:  branchNameToDetach,
-		branchToDetachType:  branchTypeToDetach,
-		branchesSnapshot:    branchesSnapshot,
-		children:            children,
-		config:              validatedConfig,
-		connector:           connector,
-		descendents:         descendents,
-		hasOpenChanges:      repoStatus.OpenChanges,
-		initialBranch:       initialBranch,
-		inputs:              inputs,
-		nonExistingBranches: nonExistingBranches,
-		parentBranch:        parentBranch,
-		previousBranch:      previousBranchOpt,
-		stashSize:           stashSize,
+		branchInfosLastRun:     branchInfosLastRun,
+		branchToDetachInfo:     *branchToDetachInfo,
+		branchToDetachName:     branchNameToDetach,
+		branchToDetachProposal: branchToDetachProposal,
+		branchToDetachType:     branchTypeToDetach,
+		branchesSnapshot:       branchesSnapshot,
+		children:               children,
+		config:                 validatedConfig,
+		connector:              connector,
+		descendents:            descendents,
+		hasOpenChanges:         repoStatus.OpenChanges,
+		initialBranch:          initialBranch,
+		inputs:                 inputs,
+		nonExistingBranches:    nonExistingBranches,
+		parentBranch:           parentBranch,
+		previousBranch:         previousBranchOpt,
+		stashSize:              stashSize,
 	}, configdomain.ProgramFlowContinue, nil
 }
 
@@ -394,15 +409,50 @@ func detachProgram(repo execute.OpenRepoResult, data detachData, finalMessages s
 				Parent: data.config.ValidatedConfigData.MainBranch,
 			},
 		)
-		for _, child := range data.config.NormalConfig.Lineage.Children(data.branchToDetachName, data.config.NormalConfig.Order) {
+	}
+	if proposal, hasProposal := data.branchToDetachProposal.Get(); hasProposal {
+		prog.Value.Add(
+			&opcodes.ProposalUpdateTarget{
+				NewBranch: data.config.ValidatedConfigData.MainBranch,
+				OldBranch: data.parentBranch,
+				Proposal:  proposal,
+			},
+		)
+	}
+	for _, child := range data.children {
+		prog.Value.Add(
+			&opcodes.LineageParentSet{
+				Branch: child.name,
+				Parent: data.parentBranch,
+			},
+		)
+
+		if proposal, hasProposal := child.proposal.Get(); hasProposal {
 			prog.Value.Add(
-				&opcodes.LineageParentSet{
-					Branch: child,
-					Parent: data.parentBranch,
+				&opcodes.ProposalUpdateTarget{
+					NewBranch: data.parentBranch,
+					OldBranch: data.branchToDetachName,
+					Proposal:  proposal,
 				},
 			)
 		}
 	}
+	if data.config.NormalConfig.ProposalsShowLineage == forgedomain.ProposalsShowLineageCLI {
+		_ = sync.AddStackLineageUpdateOpcodes(sync.AddStackLineageUpdateOpcodesArgs{
+			Current:   data.initialBranch,
+			FullStack: true,
+			Program:   prog,
+			ProposalStackLineageArgs: forge.ProposalStackLineageArgs{
+				Connector:                forgedomain.ProposalFinderFromConnector(data.connector),
+				CurrentBranch:            data.initialBranch,
+				Lineage:                  data.config.NormalConfig.Lineage,
+				MainAndPerennialBranches: data.config.MainAndPerennials(),
+				Order:                    data.config.NormalConfig.Order,
+			},
+			ProposalStackLineageTree:             None[*forge.ProposalStackLineageTree](),
+			SkipUpdateForProposalsWithBaseBranch: gitdomain.NewLocalBranchNames(),
+		})
+	}
 	cmdhelpers.Wrap(prog, cmdhelpers.WrapOptions{
 		DryRun:                   data.config.NormalConfig.DryRun,
 		InitialStashSize:         data.stashSize,
diff --git a/internal/cmd/diff_parent.go b/internal/cmd/diff_parent.go
index a26742847..6fe896dc4 100644
--- a/internal/cmd/diff_parent.go
+++ b/internal/cmd/diff_parent.go
@@ -64,6 +64,7 @@ func executeDiffParent(args []string, cliConfig configdomain.PartialConfig) erro
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -108,6 +109,7 @@ func determineDiffParentData(args []string, repo execute.OpenRepoResult) (data d
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
diff --git a/internal/cmd/down.go b/internal/cmd/down.go
index 28f7c1d4a..07dba1835 100644
--- a/internal/cmd/down.go
+++ b/internal/cmd/down.go
@@ -75,6 +75,7 @@ func executeDown(args executeDownArgs) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        args.cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
diff --git a/internal/cmd/feature.go b/internal/cmd/feature.go
index 04199a166..f64a171ab 100644
--- a/internal/cmd/feature.go
+++ b/internal/cmd/feature.go
@@ -63,6 +63,7 @@ func featureCmd() *cobra.Command {
 func executeFeature(args []string, cliConfig configdomain.PartialConfig) error {
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: false,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
diff --git a/internal/cmd/hack.go b/internal/cmd/hack.go
index db0b9fe24..7dd8f1e31 100644
--- a/internal/cmd/hack.go
+++ b/internal/cmd/hack.go
@@ -143,6 +143,7 @@ func executeHack(args hackArgs) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        args.cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -215,6 +216,7 @@ func determineHackData(args hackArgs, repo execute.OpenRepoResult) (data appendF
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
@@ -301,6 +303,9 @@ func determineHackData(args hackArgs, repo execute.OpenRepoResult) (data appendF
 		return data, configdomain.ProgramFlowExit, errors.New(messages.HackTooManyArguments)
 	}
 	targetBranch := targetBranches[0]
+	if prefix, hasPrefix := validatedConfig.NormalConfig.BranchPrefix.Get(); hasPrefix {
+		targetBranch = prefix.Apply(targetBranch)
+	}
 	if branchesSnapshot.Branches.HasLocalBranch(targetBranch) {
 		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchAlreadyExistsLocally, targetBranch)
 	}
diff --git a/internal/cmd/init.go b/internal/cmd/init.go
index 616ea3b68..03df1c2b9 100644
--- a/internal/cmd/init.go
+++ b/internal/cmd/init.go
@@ -58,6 +58,7 @@ func executeConfigSetup(cliConfig configdomain.PartialConfig) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    true,
 		PrintBranchNames: false,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -77,11 +78,11 @@ Start:
 	case configdomain.ProgramFlowRestart:
 		goto Start
 	}
-	userInput, exit, err := setup.Enter(data)
+	userInput, exit, enterAll, err := setup.Enter(data)
 	if err != nil || exit {
 		return err
 	}
-	if err = setup.Save(userInput, repo.UnvalidatedConfig, data, repo.Frontend); err != nil {
+	if err = setup.Save(userInput, repo.UnvalidatedConfig, data, enterAll, repo.Frontend); err != nil {
 		return err
 	}
 	return configinterpreter.Finished(configinterpreter.FinishedArgs{
diff --git a/internal/cmd/merge.go b/internal/cmd/merge.go
index cc9bb2ba2..bb6979749 100644
--- a/internal/cmd/merge.go
+++ b/internal/cmd/merge.go
@@ -6,10 +6,14 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/spf13/cobra"
+
 	"github.com/git-town/git-town/v22/internal/cli/dialog/dialogcomponents"
 	"github.com/git-town/git-town/v22/internal/cli/flags"
 	"github.com/git-town/git-town/v22/internal/cli/print"
 	"github.com/git-town/git-town/v22/internal/cmd/cmdhelpers"
+	"github.com/git-town/git-town/v22/internal/cmd/ship"
+	"github.com/git-town/git-town/v22/internal/cmd/sync"
 	"github.com/git-town/git-town/v22/internal/config"
 	"github.com/git-town/git-town/v22/internal/config/cliconfig"
 	"github.com/git-town/git-town/v22/internal/config/configdomain"
@@ -25,7 +29,6 @@ import (
 	"github.com/git-town/git-town/v22/internal/vm/optimizer"
 	"github.com/git-town/git-town/v22/internal/vm/program"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
-	"github.com/spf13/cobra"
 )
 
 const (
@@ -100,6 +103,7 @@ func executeMerge(cliConfig configdomain.PartialConfig) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -141,7 +145,7 @@ Start:
 		Backend:                 repo.Backend,
 		CommandsCounter:         repo.CommandsCounter,
 		Config:                  data.config,
-		Connector:               None[forgedomain.Connector](),
+		Connector:               data.connector,
 		FinalMessages:           repo.FinalMessages,
 		Frontend:                repo.Frontend,
 		Git:                     repo.Git,
@@ -158,21 +162,24 @@ Start:
 }
 
 type mergeData struct {
-	branchInfosLastRun Option[gitdomain.BranchInfos]
-	branchesSnapshot   gitdomain.BranchesSnapshot
-	config             config.ValidatedConfig
-	hasOpenChanges     bool
-	initialBranch      gitdomain.LocalBranchName
-	initialBranchInfo  gitdomain.BranchInfo
-	initialBranchSHA   gitdomain.SHA
-	initialBranchType  configdomain.BranchType
-	inputs             dialogcomponents.Inputs
-	parentBranch       gitdomain.LocalBranchName
-	parentBranchInfo   gitdomain.BranchInfo
-	parentBranchSHA    gitdomain.SHA
-	parentBranchType   configdomain.BranchType
-	previousBranch     Option[gitdomain.LocalBranchName]
-	stashSize          gitdomain.StashSize
+	branchInfosLastRun       Option[gitdomain.BranchInfos]
+	branchesSnapshot         gitdomain.BranchesSnapshot
+	childBranches            gitdomain.LocalBranchNames
+	config                   config.ValidatedConfig
+	connector                Option[forgedomain.Connector]
+	hasOpenChanges           bool
+	initialBranch            gitdomain.LocalBranchName
+	initialBranchInfo        gitdomain.BranchInfo
+	initialBranchSHA         gitdomain.SHA
+	initialBranchType        configdomain.BranchType
+	inputs                   dialogcomponents.Inputs
+	parentBranch             gitdomain.LocalBranchName
+	parentBranchInfo         gitdomain.BranchInfo
+	parentBranchSHA          gitdomain.SHA
+	parentBranchType         configdomain.BranchType
+	previousBranch           Option[gitdomain.LocalBranchName]
+	proposalsOfChildBranches []forgedomain.Proposal
+	stashSize                gitdomain.StashSize
 }
 
 func determineMergeData(repo execute.OpenRepoResult) (data mergeData, flow configdomain.ProgramFlow, err error) {
@@ -186,6 +193,7 @@ func determineMergeData(repo execute.OpenRepoResult) (data mergeData, flow confi
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
@@ -283,39 +291,62 @@ func determineMergeData(repo execute.OpenRepoResult) (data mergeData, flow confi
 	}
 	initialBranchType := validatedConfig.BranchType(initialBranch)
 	parentBranchType := validatedConfig.BranchType(parentBranch)
+
+	childBranches := validatedConfig.NormalConfig.Lineage.Children(initialBranch, validatedConfig.NormalConfig.Order)
+	proposalsOfChildBranches := ship.LoadProposalsOfChildBranches(ship.LoadProposalsOfChildBranchesArgs{
+		ConnectorOpt:               connector,
+		Lineage:                    validatedConfig.NormalConfig.Lineage,
+		Offline:                    repo.IsOffline,
+		OldBranch:                  initialBranch,
+		OldBranchHasTrackingBranch: branchesSnapshot.Branches.FindByLocalName(initialBranch).IsSome(),
+		Order:                      validatedConfig.NormalConfig.Order,
+	})
 	return mergeData{
-		branchInfosLastRun: branchInfosLastRun,
-		branchesSnapshot:   branchesSnapshot,
-		config:             validatedConfig,
-		hasOpenChanges:     repoStatus.OpenChanges,
-		initialBranch:      initialBranch,
-		initialBranchInfo:  *initialBranchInfo,
-		initialBranchSHA:   initialBranchSHA,
-		initialBranchType:  initialBranchType,
-		inputs:             inputs,
-		parentBranch:       parentBranch,
-		parentBranchInfo:   *parentBranchInfo,
-		parentBranchSHA:    parentBranchSHA,
-		parentBranchType:   parentBranchType,
-		previousBranch:     previousBranch,
-		stashSize:          stashSize,
+		branchInfosLastRun:       branchInfosLastRun,
+		branchesSnapshot:         branchesSnapshot,
+		childBranches:            childBranches,
+		config:                   validatedConfig,
+		connector:                connector,
+		hasOpenChanges:           repoStatus.OpenChanges,
+		initialBranch:            initialBranch,
+		initialBranchInfo:        *initialBranchInfo,
+		initialBranchSHA:         initialBranchSHA,
+		initialBranchType:        initialBranchType,
+		inputs:                   inputs,
+		parentBranch:             parentBranch,
+		parentBranchInfo:         *parentBranchInfo,
+		parentBranchSHA:          parentBranchSHA,
+		parentBranchType:         parentBranchType,
+		previousBranch:           previousBranch,
+		proposalsOfChildBranches: proposalsOfChildBranches,
+		stashSize:                stashSize,
 	}, configdomain.ProgramFlowContinue, err
 }
 
 func mergeProgram(repo execute.OpenRepoResult, data mergeData) program.Program {
 	prog := NewMutable(&program.Program{})
-	// there is no point in updating proposals:
-	// If the parent branch has a proposal, it doesn't need to change.
-	// The child branch proposal will get closed because the child branch gets deleted,
-	// and that's correct because it was from the child branch into the parent branch,
-	// and that doesn't make sense anymore because both branches are one now.
+	ship.UpdateChildBranchProposalsToGrandParent(prog.Value, data.proposalsOfChildBranches)
 	prog.Value.Add(&opcodes.Checkout{Branch: data.parentBranch})
 	if data.initialBranchSHA != data.parentBranchSHA {
-		prog.Value.Add(&opcodes.BranchLocalSetToSHA{SHA: data.initialBranchSHA})
+		prog.Value.Add(&opcodes.BranchCurrentResetToSHA{SHA: data.initialBranchSHA})
+	}
+	for _, child := range data.childBranches {
+		prog.Value.Add(&opcodes.LineageParentSetToGrandParent{Branch: child})
 	}
 	prog.Value.Add(&opcodes.LineageParentRemove{
 		Branch: data.initialBranch,
 	})
+	// We need to delete the remote branch before updating the parent branch.
+	// This order will make sure that if there is a related proposal where
+	// the base is the branch being merged and the target is the parent branch
+	// then that proposal gets marked "closed" rather than "merged" by forges
+	// like GitHub.
+	initialTrackingBranch, initialHasTrackingBranch := data.initialBranchInfo.RemoteName.Get()
+	if initialHasTrackingBranch && repo.IsOffline.IsOnline() {
+		prog.Value.Add(&opcodes.BranchTrackingDelete{
+			Branch: initialTrackingBranch,
+		})
+	}
 	prog.Value.Add(&opcodes.BranchLocalDelete{
 		Branch: data.initialBranch,
 	})
@@ -325,18 +356,28 @@ func mergeProgram(repo execute.OpenRepoResult, data mergeData) program.Program {
 			ForceIfIncludes: true,
 		})
 	}
-	initialTrackingBranch, initialHasTrackingBranch := data.initialBranchInfo.RemoteName.Get()
-	if initialHasTrackingBranch && repo.IsOffline.IsOnline() {
-		prog.Value.Add(&opcodes.BranchTrackingDelete{
-			Branch: initialTrackingBranch,
-		})
-	}
 	if _, hasOverride := data.config.NormalConfig.BranchTypeOverrides[data.initialBranch]; hasOverride {
 		prog.Value.Add(&opcodes.BranchTypeOverrideRemove{
 			Branch: data.initialBranch,
 		})
 	}
 	previousBranchCandidates := []Option[gitdomain.LocalBranchName]{data.previousBranch}
+	if data.config.NormalConfig.ProposalsShowLineage == forgedomain.ProposalsShowLineageCLI {
+		_ = sync.AddStackLineageUpdateOpcodes(sync.AddStackLineageUpdateOpcodesArgs{
+			Current:   data.initialBranch,
+			FullStack: true,
+			Program:   prog,
+			ProposalStackLineageArgs: forge.ProposalStackLineageArgs{
+				Connector:                forgedomain.ProposalFinderFromConnector(data.connector),
+				CurrentBranch:            data.initialBranch,
+				Lineage:                  data.config.NormalConfig.Lineage,
+				MainAndPerennialBranches: data.config.MainAndPerennials(),
+				Order:                    data.config.NormalConfig.Order,
+			},
+			ProposalStackLineageTree:             None[*forge.ProposalStackLineageTree](),
+			SkipUpdateForProposalsWithBaseBranch: gitdomain.LocalBranchNames{data.initialBranch},
+		})
+	}
 	cmdhelpers.Wrap(prog, cmdhelpers.WrapOptions{
 		DryRun:                   data.config.NormalConfig.DryRun,
 		InitialStashSize:         data.stashSize,
diff --git a/internal/cmd/observe.go b/internal/cmd/observe.go
index 68ad27ba4..0cdd1a07a 100644
--- a/internal/cmd/observe.go
+++ b/internal/cmd/observe.go
@@ -70,6 +70,7 @@ func observeCmd() *cobra.Command {
 func executeObserve(args []string, cliConfig configdomain.PartialConfig) error {
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: false,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
diff --git a/internal/cmd/offline.go b/internal/cmd/offline.go
index debba2356..d87140631 100644
--- a/internal/cmd/offline.go
+++ b/internal/cmd/offline.go
@@ -60,6 +60,7 @@ func offlineCmd() *cobra.Command {
 func executeOffline(args []string, cliConfig configdomain.PartialConfig) error {
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: false,
 		PrintCommands:    true,
 		ValidateGitRepo:  false,
diff --git a/internal/cmd/park.go b/internal/cmd/park.go
index 33d799641..03be18720 100644
--- a/internal/cmd/park.go
+++ b/internal/cmd/park.go
@@ -64,6 +64,7 @@ func parkCmd() *cobra.Command {
 func executePark(args []string, cliConfig configdomain.PartialConfig) error {
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: false,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
diff --git a/internal/cmd/prepend.go b/internal/cmd/prepend.go
index 2b19aaec3..ca9350ff1 100644
--- a/internal/cmd/prepend.go
+++ b/internal/cmd/prepend.go
@@ -166,6 +166,7 @@ func executePrepend(args prependArgs) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        args.cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -265,6 +266,7 @@ func determinePrependData(args prependArgs, repo execute.OpenRepoResult) (data p
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
@@ -323,6 +325,9 @@ func determinePrependData(args prependArgs, repo execute.OpenRepoResult) (data p
 		return data, configdomain.ProgramFlowExit, err
 	}
 	targetBranch := gitdomain.NewLocalBranchName(args.argv[0])
+	if prefix, hasPrefix := config.BranchPrefix.Get(); hasPrefix {
+		targetBranch = prefix.Apply(targetBranch)
+	}
 	if branchesSnapshot.Branches.HasLocalBranch(targetBranch) {
 		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchAlreadyExistsLocally, targetBranch)
 	}
@@ -509,30 +514,23 @@ func prependProgram(repo execute.OpenRepoResult, data prependData, finalMessages
 			PreviousBranchCandidates: previousBranchCandidates,
 		})
 	}
-
 	if data.config.NormalConfig.ProposalsShowLineage == forgedomain.ProposalsShowLineageCLI {
-		if hasConnector {
-			if proposalFinder, hasProposalFinder := connector.(forgedomain.ProposalFinder); hasProposalFinder {
-				tree, err := forge.NewProposalStackLineageTree(forge.ProposalStackLineageArgs{
-					Connector:                proposalFinder,
+		_ = sync.AddStackLineageUpdateOpcodes(
+			sync.AddStackLineageUpdateOpcodesArgs{
+				Current:   data.initialBranch,
+				FullStack: true,
+				Program:   prog,
+				ProposalStackLineageArgs: forge.ProposalStackLineageArgs{
+					Connector:                forgedomain.ProposalFinderFromConnector(data.connector),
 					CurrentBranch:            data.initialBranch,
 					Lineage:                  data.config.NormalConfig.Lineage,
 					MainAndPerennialBranches: data.config.MainAndPerennials(),
 					Order:                    data.config.NormalConfig.Order,
-				})
-				if err != nil {
-					fmt.Printf("failed to update proposal stack lineage: %s\n", err.Error())
-				} else {
-					for branch, proposal := range tree.BranchToProposal { // okay to iterate the map in random order
-						prog.Value.Add(&opcodes.ProposalUpdateLineage{
-							Current:         branch,
-							CurrentProposal: proposal,
-							LineageTree:     MutableSome(tree),
-						})
-					}
-				}
-			}
-		}
+				},
+				ProposalStackLineageTree:             None[*forge.ProposalStackLineageTree](),
+				SkipUpdateForProposalsWithBaseBranch: gitdomain.NewLocalBranchNames(),
+			},
+		)
 	}
 	return optimizer.Optimize(prog.Immutable())
 }
diff --git a/internal/cmd/propose.go b/internal/cmd/propose.go
index 5a41e9eaa..5615141f2 100644
--- a/internal/cmd/propose.go
+++ b/internal/cmd/propose.go
@@ -120,6 +120,7 @@ func executePropose(args proposeArgs) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        args.cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -223,6 +224,7 @@ func determineProposeData(repo execute.OpenRepoResult, args proposeArgs) (data p
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
@@ -393,7 +395,7 @@ func proposeProgram(repo execute.OpenRepoResult, data proposeData) program.Progr
 		case configdomain.BranchTypeContributionBranch, configdomain.BranchTypeMainBranch, configdomain.BranchTypeObservedBranch, configdomain.BranchTypePerennialBranch:
 			continue
 		}
-		prog.Value.Add(&opcodes.PushCurrentBranchIfLocal{
+		prog.Value.Add(&opcodes.BranchTrackingCreateIfNeeded{
 			CurrentBranch: branchToPropose.name,
 		})
 		previousBranchCandidates := []Option[gitdomain.LocalBranchName]{data.previousBranch}
diff --git a/internal/cmd/prototype.go b/internal/cmd/prototype.go
index 964829dd3..965a60a32 100644
--- a/internal/cmd/prototype.go
+++ b/internal/cmd/prototype.go
@@ -67,6 +67,7 @@ func prototypeCmd() *cobra.Command {
 func executePrototype(args []string, cliConfig configdomain.PartialConfig) error {
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
diff --git a/internal/cmd/rename.go b/internal/cmd/rename.go
index c8583fad2..58f3542f6 100644
--- a/internal/cmd/rename.go
+++ b/internal/cmd/rename.go
@@ -79,6 +79,7 @@ func executeRename(args []string, cliConfig configdomain.PartialConfig, force co
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -162,6 +163,7 @@ func determineRenameData(args []string, force configdomain.Force, repo execute.O
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
@@ -217,6 +219,9 @@ func determineRenameData(args []string, force configdomain.Force, repo execute.O
 		oldBranchName = gitdomain.NewLocalBranchName(args[0])
 		newBranchName = gitdomain.NewLocalBranchName(args[1])
 	}
+	if prefix, hasPrefix := config.BranchPrefix.Get(); hasPrefix {
+		newBranchName = prefix.Apply(newBranchName)
+	}
 	oldBranch, hasOldBranch := branchesSnapshot.Branches.FindByLocalName(oldBranchName).Get()
 	if !hasOldBranch {
 		return data, configdomain.ProgramFlowExit, fmt.Errorf(messages.BranchDoesntExist, oldBranchName)
diff --git a/internal/cmd/repo.go b/internal/cmd/repo.go
index b96fe601d..bab9fb710 100644
--- a/internal/cmd/repo.go
+++ b/internal/cmd/repo.go
@@ -64,6 +64,7 @@ func repoCommand() *cobra.Command {
 func executeRepo(args []string, cliConfig configdomain.PartialConfig) error {
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -97,6 +98,7 @@ func determineRepoData(args []string, repo execute.OpenRepoResult) (data repoDat
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
diff --git a/internal/cmd/runlog.go b/internal/cmd/runlog.go
index 5ca2f889f..d4193318e 100644
--- a/internal/cmd/runlog.go
+++ b/internal/cmd/runlog.go
@@ -64,6 +64,7 @@ func runLogCommand() *cobra.Command {
 func executeRunLog(cliConfig configdomain.PartialConfig) error {
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    true,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
diff --git a/internal/cmd/set_parent.go b/internal/cmd/set_parent.go
index 623622cde..4019236fc 100644
--- a/internal/cmd/set_parent.go
+++ b/internal/cmd/set_parent.go
@@ -4,6 +4,7 @@ import (
 	"cmp"
 	"errors"
 	"fmt"
+	"maps"
 	"os"
 	"regexp"
 
@@ -16,6 +17,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/cli/print"
 	"github.com/git-town/git-town/v22/internal/cmd/cmdhelpers"
 	"github.com/git-town/git-town/v22/internal/cmd/ship"
+	"github.com/git-town/git-town/v22/internal/cmd/sync"
 	"github.com/git-town/git-town/v22/internal/config"
 	"github.com/git-town/git-town/v22/internal/config/cliconfig"
 	"github.com/git-town/git-town/v22/internal/config/configdomain"
@@ -102,6 +104,7 @@ func executeSetParent(args []string, cliConfig configdomain.PartialConfig, noPar
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -247,6 +250,7 @@ func determineSetParentData(repo execute.OpenRepoResult) (data setParentData, fl
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
@@ -472,63 +476,75 @@ func updateProposalLineage(prog *program.Program, newParentOpt Option[gitdomain.
 	if data.config.NormalConfig.ProposalsShowLineage != forgedomain.ProposalsShowLineageCLI {
 		return
 	}
-	connector, hasConnector := data.connector.Get()
-	if !hasConnector {
-		return
-	}
-	proposalFinder, canFindProposals := connector.(forgedomain.ProposalFinder)
-	if !canFindProposals {
-		return
-	}
-	// Update the stack belonging to the initialBranch
-	tree, err := forge.NewProposalStackLineageTree(forge.ProposalStackLineageArgs{
-		Connector:                proposalFinder,
-		CurrentBranch:            data.initialBranch,
-		Lineage:                  data.config.NormalConfig.Lineage,
-		MainAndPerennialBranches: data.config.MainAndPerennials(),
-		Order:                    data.config.NormalConfig.Order,
-	})
-	if err != nil {
-		fmt.Printf("failed to update proposal stack lineage: %s\n", err.Error())
-		return
-	}
+
+	proposalStackTree := sync.AddStackLineageUpdateOpcodes(
+		sync.AddStackLineageUpdateOpcodesArgs{
+			Current:   data.initialBranch,
+			FullStack: true,
+			Program:   NewMutable(prog),
+			ProposalStackLineageArgs: forge.ProposalStackLineageArgs{
+				Connector:                forgedomain.ProposalFinderFromConnector(data.connector),
+				CurrentBranch:            data.initialBranch,
+				Lineage:                  data.config.NormalConfig.Lineage,
+				MainAndPerennialBranches: data.config.MainAndPerennials(),
+				Order:                    data.config.NormalConfig.Order,
+			},
+			ProposalStackLineageTree:             None[*forge.ProposalStackLineageTree](),
+			SkipUpdateForProposalsWithBaseBranch: gitdomain.NewLocalBranchNames(),
+		},
+	)
+
 	branchPropsalsUpdated := set.New[gitdomain.LocalBranchName]()
-	for branch, proposal := range tree.BranchToProposal { // okay to iterate the map in random order
-		prog.Add(&opcodes.ProposalUpdateLineage{
-			Current:         branch,
-			CurrentProposal: proposal,
-			LineageTree:     MutableSome(tree),
-		})
-		branchPropsalsUpdated.Add(branch)
+	// if the tree is a Some type, we know it will trigger program runs to update
+	// proposals belonging to the base branches iterated on below
+	if tree, hasProposalStackTree := proposalStackTree.Get(); hasProposalStackTree {
+		for b := range maps.Keys(tree.BranchToProposal) {
+			branchPropsalsUpdated.Add(b)
+		}
 	}
 
 	// If we are moving to a parent that is part of a completely different stack,
 	// update the lineage of all members of this other stack
+	_ = sync.AddStackLineageUpdateOpcodes(
+		sync.AddStackLineageUpdateOpcodesArgs{
+			Current:   data.initialBranch,
+			FullStack: true,
+			Program:   NewMutable(prog),
+			ProposalStackLineageArgs: forge.ProposalStackLineageArgs{
+				Connector:                forgedomain.ProposalFinderFromConnector(data.connector),
+				CurrentBranch:            data.initialBranch,
+				Lineage:                  data.config.NormalConfig.Lineage,
+				MainAndPerennialBranches: data.config.MainAndPerennials(),
+				Order:                    data.config.NormalConfig.Order,
+			},
+			ProposalStackLineageTree: proposalStackTree,
+			// Do not update the same proposal more than once because we updated
+			// it in a previous step
+			SkipUpdateForProposalsWithBaseBranch: branchPropsalsUpdated.Values(),
+		},
+	)
+
 	newParent, hasNewParent := newParentOpt.Get()
 	if hasNewParent {
-		err = tree.Rebuild(forge.ProposalStackLineageArgs{
-			Connector:                proposalFinder,
-			CurrentBranch:            newParent,
-			Lineage:                  data.config.NormalConfig.Lineage,
-			MainAndPerennialBranches: data.config.MainAndPerennials(),
-			Order:                    data.config.NormalConfig.Order,
-		})
-
-		if err == nil {
-			for branch, proposal := range tree.BranchToProposal { // okay to iterate the map in random order
+		// If we are moving to a parent that is part of a completely different stack,
+		// update the lineage of all members of this other stack
+		_ = sync.AddStackLineageUpdateOpcodes(
+			sync.AddStackLineageUpdateOpcodesArgs{
+				Current:   data.initialBranch,
+				FullStack: true,
+				Program:   NewMutable(prog),
+				ProposalStackLineageArgs: forge.ProposalStackLineageArgs{
+					Connector:                forgedomain.ProposalFinderFromConnector(data.connector),
+					CurrentBranch:            newParent,
+					Lineage:                  data.config.NormalConfig.Lineage,
+					MainAndPerennialBranches: data.config.MainAndPerennials(),
+					Order:                    data.config.NormalConfig.Order,
+				},
+				ProposalStackLineageTree: proposalStackTree,
 				// Do not update the same proposal more than once because we updated
 				// it in a previous step
-				if _, ok := branchPropsalsUpdated[branch]; !ok {
-					prog.Add(&opcodes.ProposalUpdateLineage{
-						Current:         branch,
-						CurrentProposal: proposal,
-						LineageTree:     MutableSome(tree),
-					})
-					branchPropsalsUpdated.Add(branch)
-				}
-			}
-		} else {
-			fmt.Printf("failed to update proposal stack lineage for new parent: %s\n", err.Error())
-		}
+				SkipUpdateForProposalsWithBaseBranch: branchPropsalsUpdated.Values(),
+			},
+		)
 	}
 }
diff --git a/internal/cmd/ship/cmd.go b/internal/cmd/ship/cmd.go
index 06817b12a..832fe9b9d 100644
--- a/internal/cmd/ship/cmd.go
+++ b/internal/cmd/ship/cmd.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/git-town/git-town/v22/internal/cli/flags"
 	"github.com/git-town/git-town/v22/internal/cmd/cmdhelpers"
+	"github.com/git-town/git-town/v22/internal/cmd/sync"
 	"github.com/git-town/git-town/v22/internal/config/cliconfig"
 	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/execute"
@@ -112,6 +113,7 @@ func executeShip(args executeShipArgs) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        args.cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -173,34 +175,25 @@ Start:
 		shipProgramSquashMerge(prog, repo, sharedData, squashMergeData, message)
 	}
 	if sharedData.config.NormalConfig.ProposalsShowLineage == forgedomain.ProposalsShowLineageCLI {
-		if connector, hasConnector := sharedData.connector.Get(); hasConnector {
-			if proposalFinder, hasProposalFinder := connector.(forgedomain.ProposalFinder); hasProposalFinder {
-				tree, err := forge.NewProposalStackLineageTree(forge.ProposalStackLineageArgs{
-					Connector:                proposalFinder,
+		_ = sync.AddStackLineageUpdateOpcodes(
+			sync.AddStackLineageUpdateOpcodesArgs{
+				Current:   sharedData.initialBranch,
+				FullStack: true,
+				Program:   prog,
+				ProposalStackLineageArgs: forge.ProposalStackLineageArgs{
+					Connector:                forgedomain.ProposalFinderFromConnector(sharedData.connector),
 					CurrentBranch:            sharedData.initialBranch,
 					Lineage:                  sharedData.config.NormalConfig.Lineage,
 					MainAndPerennialBranches: sharedData.config.MainAndPerennials(),
 					Order:                    sharedData.config.NormalConfig.Order,
-				})
-				if err != nil {
-					fmt.Printf("failed to update proposal stack lineage: %s\n", err.Error())
-				} else {
-					for branch, proposal := range tree.BranchToProposal { // okay to iterate the map in random order
-						// Proposal has been shipped and its stack lineage
-						// information shouldn't need to be updated because
-						// proposal is not in a review state.
-						if branch == sharedData.initialBranch {
-							continue
-						}
-						prog.Value.Add(&opcodes.ProposalUpdateLineage{
-							Current:         branch,
-							CurrentProposal: proposal,
-							LineageTree:     MutableSome(tree),
-						})
-					}
-				}
-			}
-		}
+				},
+				ProposalStackLineageTree: None[*forge.ProposalStackLineageTree](),
+				// Proposal has been shipped and its stack lineage
+				// information shouldn't need to be updated because
+				// proposal is not in a review state.
+				SkipUpdateForProposalsWithBaseBranch: gitdomain.LocalBranchNames{sharedData.initialBranch},
+			},
+		)
 	}
 	optimizedProgram := optimizer.Optimize(prog.Immutable())
 	runState := runstate.RunState{
diff --git a/internal/cmd/ship/shared.go b/internal/cmd/ship/shared.go
index 4346c778e..50546ebbb 100644
--- a/internal/cmd/ship/shared.go
+++ b/internal/cmd/ship/shared.go
@@ -49,6 +49,7 @@ func determineSharedShipData(args []string, repo execute.OpenRepoResult, shipStr
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
diff --git a/internal/cmd/skip.go b/internal/cmd/skip.go
index 7aa084932..8650d0fc0 100644
--- a/internal/cmd/skip.go
+++ b/internal/cmd/skip.go
@@ -58,6 +58,7 @@ func executeSkip(cliConfig configdomain.PartialConfig) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -76,6 +77,7 @@ Start:
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
diff --git a/internal/cmd/status/root.go b/internal/cmd/status/root.go
index e7f063be8..a5c079fb9 100644
--- a/internal/cmd/status/root.go
+++ b/internal/cmd/status/root.go
@@ -60,6 +60,7 @@ func RootCommand() *cobra.Command {
 func executeStatus(cliConfig configdomain.PartialConfig, pending configdomain.Pending) error {
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    true,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
diff --git a/internal/cmd/status/show.go b/internal/cmd/status/show.go
index cab22e831..5d3c48a11 100644
--- a/internal/cmd/status/show.go
+++ b/internal/cmd/status/show.go
@@ -48,6 +48,7 @@ func showRunstateCommand() *cobra.Command {
 func executeStatusShow(cliConfig configdomain.PartialConfig) error {
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    true,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
diff --git a/internal/cmd/swap/cmd.go b/internal/cmd/swap/cmd.go
index 0d5e867e0..7e745a722 100644
--- a/internal/cmd/swap/cmd.go
+++ b/internal/cmd/swap/cmd.go
@@ -10,6 +10,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/cli/flags"
 	"github.com/git-town/git-town/v22/internal/cli/print"
 	"github.com/git-town/git-town/v22/internal/cmd/cmdhelpers"
+	"github.com/git-town/git-town/v22/internal/cmd/sync"
 	"github.com/git-town/git-town/v22/internal/config"
 	"github.com/git-town/git-town/v22/internal/config/cliconfig"
 	"github.com/git-town/git-town/v22/internal/config/configdomain"
@@ -101,6 +102,7 @@ func executeSwap(cliConfig configdomain.PartialConfig) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -199,6 +201,7 @@ func determineSwapData(repo execute.OpenRepoResult) (data swapData, flow configd
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
@@ -399,17 +402,24 @@ func swapProgram(repo execute.OpenRepoResult, data swapData, finalMessages strin
 			program:     prog,
 		})
 	}
-	connector, hasConnector := data.connector.Get()
-	if data.config.NormalConfig.ProposalsShowLineage == forgedomain.ProposalsShowLineageCLI && hasConnector {
-		if proposalFinder, canFindProposals := connector.(forgedomain.ProposalFinder); canFindProposals {
-			swapUpdateProposalStackLineagesProgram(prog, forge.ProposalStackLineageArgs{
-				Connector:                proposalFinder,
-				CurrentBranch:            data.currentBranchName,
-				Lineage:                  data.config.NormalConfig.Lineage,
-				MainAndPerennialBranches: data.config.MainAndPerennials(),
-				Order:                    data.config.NormalConfig.Order,
-			})
-		}
+	if data.config.NormalConfig.ProposalsShowLineage == forgedomain.ProposalsShowLineageCLI {
+		_ = sync.AddStackLineageUpdateOpcodes(
+			sync.AddStackLineageUpdateOpcodesArgs{
+				Current:   data.initialBranch,
+				FullStack: true,
+				Program:   prog,
+				ProposalStackLineageArgs: forge.ProposalStackLineageArgs{
+					Connector:                forgedomain.ProposalFinderFromConnector(data.connector),
+					CurrentBranch:            data.initialBranch,
+					Lineage:                  data.config.NormalConfig.Lineage,
+					MainAndPerennialBranches: data.config.MainAndPerennials(),
+					Order:                    data.config.NormalConfig.Order,
+				},
+
+				ProposalStackLineageTree:             None[*forge.ProposalStackLineageTree](),
+				SkipUpdateForProposalsWithBaseBranch: gitdomain.NewLocalBranchNames(),
+			},
+		)
 	}
 	cmdhelpers.Wrap(prog, cmdhelpers.WrapOptions{
 		DryRun:                   data.config.NormalConfig.DryRun,
diff --git a/internal/cmd/swap/swap_update_proposal_stack_lineages_program.go b/internal/cmd/swap/swap_update_proposal_stack_lineages_program.go
deleted file mode 100644
index 94de71c1e..000000000
--- a/internal/cmd/swap/swap_update_proposal_stack_lineages_program.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package swap
-
-import (
-	"fmt"
-
-	"github.com/git-town/git-town/v22/internal/forge"
-	"github.com/git-town/git-town/v22/internal/gohacks/mapstools"
-	"github.com/git-town/git-town/v22/internal/vm/opcodes"
-	"github.com/git-town/git-town/v22/internal/vm/program"
-	. "github.com/git-town/git-town/v22/pkg/prelude"
-)
-
-func swapUpdateProposalStackLineagesProgram(program Mutable[program.Program], proposalStackLineageArgs forge.ProposalStackLineageArgs) {
-	// TODO: the proposal stack lineage builder executes its own GetProposalFn calls. Some of
-	// these calls are duplicative because similar calls are completed upstream. A separate
-	// enhancement is needed to reduce duplicative GetProposalFn calls through the forge
-	// connectors.
-	//
-	// NOTE: NewProposalStackLineageBuilder method stores all the proposals found in the stack lineage
-	// in map for fast retrieve through the GetProposals used below.
-	tree, err := forge.NewProposalStackLineageTree(proposalStackLineageArgs)
-	if err != nil {
-		fmt.Printf("failed to update proposal stack lineage: %s\n", err.Error())
-		return
-	}
-	for branch, proposal := range mapstools.SortedKeyValues(tree.BranchToProposal) {
-		program.Value.Add(&opcodes.ProposalUpdateLineage{
-			Current:         branch,
-			CurrentProposal: proposal,
-			LineageTree:     MutableSome(tree),
-		})
-	}
-}
diff --git a/internal/cmd/switch.go b/internal/cmd/switch.go
index 5f8a89002..7341bfd38 100644
--- a/internal/cmd/switch.go
+++ b/internal/cmd/switch.go
@@ -30,6 +30,7 @@ func switchCmd() *cobra.Command {
 	addDisplayTypesFlag, readDisplayTypesFlag := flags.Displaytypes()
 	addMergeFlag, readMergeFlag := flags.Merge()
 	addOrderFlag, readOrderFlag := flags.Order()
+	addStashFlag, readStashFlag := flags.Stash()
 	addTypeFlag, readTypeFlag := flags.BranchType()
 	addVerboseFlag, readVerboseFlag := flags.Verbose()
 	cmd := cobra.Command{
@@ -44,8 +45,9 @@ func switchCmd() *cobra.Command {
 			displayTypes, errDisplayTypes := readDisplayTypesFlag(cmd)
 			merge, errMerge := readMergeFlag(cmd)
 			order, errOrder := readOrderFlag(cmd)
+			stash, errStash := readStashFlag(cmd)
 			verbose, errVerbose := readVerboseFlag(cmd)
-			if err := cmp.Or(errBranchTypes, errAllBranches, errDisplayTypes, errMerge, errOrder, errVerbose); err != nil {
+			if err := cmp.Or(errBranchTypes, errAllBranches, errDisplayTypes, errMerge, errOrder, errStash, errVerbose); err != nil {
 				return err
 			}
 			cliConfig := cliconfig.New(cliconfig.NewArgs{
@@ -56,7 +58,7 @@ func switchCmd() *cobra.Command {
 				DryRun:       None[configdomain.DryRun](),
 				Order:        order,
 				PushBranches: None[configdomain.PushBranches](),
-				Stash:        None[configdomain.Stash](),
+				Stash:        stash,
 				Verbose:      verbose,
 			})
 			return executeSwitch(executeSwitchArgs{
@@ -72,6 +74,7 @@ func switchCmd() *cobra.Command {
 	addDisplayTypesFlag(&cmd)
 	addMergeFlag(&cmd)
 	addOrderFlag(&cmd)
+	addStashFlag(&cmd)
 	addTypeFlag(&cmd)
 	addVerboseFlag(&cmd)
 	return &cmd
@@ -89,6 +92,7 @@ func executeSwitch(args executeSwitchArgs) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        args.cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -152,8 +156,7 @@ Start:
 	if branchToCheckout == data.initialBranch {
 		return nil
 	}
-	err = repo.Git.CheckoutBranch(repo.Frontend, branchToCheckout, args.merge)
-	if err != nil {
+	if err := performSwitch(branchToCheckout, args.cliConfig.Stash.GetOr(false), data.hasOpenChanges, args.merge, repo); err != nil {
 		exitCode := 1
 		var exitErr *exec.ExitError
 		if errors.As(err, &exitErr) {
@@ -167,6 +170,7 @@ Start:
 type switchData struct {
 	branchesSnapshot   gitdomain.BranchesSnapshot
 	config             config.UnvalidatedConfig
+	hasOpenChanges     bool
 	initialBranch      gitdomain.LocalBranchName
 	inputs             dialogcomponents.Inputs
 	lineage            configdomain.Lineage
@@ -174,6 +178,22 @@ type switchData struct {
 	uncommittedChanges bool
 }
 
+func performSwitch(branchToCheckout gitdomain.LocalBranchName, stash configdomain.Stash, hasOpenChanges bool, merge configdomain.SwitchUsingMerge, repo execute.OpenRepoResult) error {
+	if stash.ShouldStash() && hasOpenChanges {
+		return switchWithStash(branchToCheckout, merge, repo)
+	}
+	return repo.Git.CheckoutBranch(repo.Frontend, branchToCheckout, merge)
+}
+
+func switchWithStash(branchToCheckout gitdomain.LocalBranchName, merge configdomain.SwitchUsingMerge, repo execute.OpenRepoResult) error {
+	if err := repo.Git.Stash(repo.Frontend); err != nil {
+		return err
+	}
+	errCheckout := repo.Git.CheckoutBranch(repo.Frontend, branchToCheckout, merge)
+	errPop := repo.Git.PopStash(repo.Frontend)
+	return cmp.Or(errCheckout, errPop)
+}
+
 func determineSwitchData(args []string, repo execute.OpenRepoResult) (data switchData, flow configdomain.ProgramFlow, err error) {
 	inputs := dialogcomponents.LoadInputs(os.Environ())
 	repoStatus, err := repo.Git.RepoStatus(repo.Backend)
@@ -216,6 +236,7 @@ func determineSwitchData(args []string, repo execute.OpenRepoResult) (data switc
 	return switchData{
 		branchesSnapshot:   branchesSnapshot,
 		config:             repo.UnvalidatedConfig,
+		hasOpenChanges:     repoStatus.OpenChanges,
 		initialBranch:      initialBranch,
 		inputs:             inputs,
 		lineage:            repo.UnvalidatedConfig.NormalConfig.Lineage,
diff --git a/internal/cmd/sync/cmd.go b/internal/cmd/sync/cmd.go
index 068b97391..c133d3ad0 100644
--- a/internal/cmd/sync/cmd.go
+++ b/internal/cmd/sync/cmd.go
@@ -118,6 +118,7 @@ func executeSync(args executeSyncArgs) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        args.cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -170,27 +171,23 @@ Start:
 	if data.remotes.HasRemote(data.config.NormalConfig.DevRemote) && data.shouldPushTags && data.config.NormalConfig.Offline.IsOnline() {
 		runProgram.Value.Add(&opcodes.PushTags{})
 	}
-
-	connector, hasConnector := data.connector.Get()
-	if hasConnector {
-		if proposalFinder, canFindProposals := connector.(forgedomain.ProposalFinder); canFindProposals {
-			if data.config.NormalConfig.ProposalsShowLineage == forgedomain.ProposalsShowLineageCLI {
-				BranchProposalsProgram(
-					BranchProposalsProgramArgs{
-						Current:   data.initialBranch,
-						FullStack: args.stack,
-						Program:   runProgram,
-						ProposalStackLineageArgs: forge.ProposalStackLineageArgs{
-							Connector:                proposalFinder,
-							CurrentBranch:            data.initialBranch,
-							Lineage:                  data.config.NormalConfig.Lineage,
-							MainAndPerennialBranches: data.config.MainAndPerennials(),
-							Order:                    data.config.NormalConfig.Order,
-						},
-					},
-				)
-			}
-		}
+	if data.config.NormalConfig.ProposalsShowLineage == forgedomain.ProposalsShowLineageCLI {
+		_ = AddStackLineageUpdateOpcodes(
+			AddStackLineageUpdateOpcodesArgs{
+				Current:   data.initialBranch,
+				FullStack: args.stack,
+				Program:   runProgram,
+				ProposalStackLineageArgs: forge.ProposalStackLineageArgs{
+					Connector:                forgedomain.ProposalFinderFromConnector(data.connector),
+					CurrentBranch:            data.initialBranch,
+					Lineage:                  data.config.NormalConfig.Lineage,
+					MainAndPerennialBranches: data.config.MainAndPerennials(),
+					Order:                    data.config.NormalConfig.Order,
+				},
+				ProposalStackLineageTree:             None[*forge.ProposalStackLineageTree](),
+				SkipUpdateForProposalsWithBaseBranch: gitdomain.NewLocalBranchNames(),
+			},
+		)
 	}
 
 	cmdhelpers.Wrap(runProgram, cmdhelpers.WrapOptions{
@@ -273,6 +270,7 @@ func determineSyncData(repo execute.OpenRepoResult, args determineSyncDataArgs)
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
diff --git a/internal/cmd/sync/sync_deleted_branch.go b/internal/cmd/sync/sync_deleted_branch.go
index 2d185ea1a..3644cd010 100644
--- a/internal/cmd/sync/sync_deleted_branch.go
+++ b/internal/cmd/sync/sync_deleted_branch.go
@@ -69,7 +69,7 @@ func syncDeletedFeatureBranchProgram(branch gitdomain.LocalBranchName, initialPa
 // deletes the given local branch as part of syncing it
 func syncDeleteLocalBranchProgram(branch gitdomain.LocalBranchName, args BranchProgramArgs) {
 	args.Program.Value.Add(
-		&opcodes.CheckoutParentOrMain{
+		&opcodes.CheckoutDescendentOrOtherIfNeeded{
 			Branch: branch,
 		},
 		&opcodes.BranchLocalDeleteContent{
diff --git a/internal/cmd/sync/sync_proposals.go b/internal/cmd/sync/sync_proposals.go
index bfb56d7b7..476e07733 100644
--- a/internal/cmd/sync/sync_proposals.go
+++ b/internal/cmd/sync/sync_proposals.go
@@ -12,39 +12,53 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-type BranchProposalsProgramArgs struct {
-	Current                  gitdomain.LocalBranchName
-	FullStack                configdomain.FullStack
-	Program                  Mutable[program.Program]
-	ProposalStackLineageArgs forge.ProposalStackLineageArgs
+type AddStackLineageUpdateOpcodesArgs struct {
+	Current                              gitdomain.LocalBranchName
+	FullStack                            configdomain.FullStack
+	Program                              Mutable[program.Program]
+	ProposalStackLineageArgs             forge.ProposalStackLineageArgs
+	ProposalStackLineageTree             Option[*forge.ProposalStackLineageTree]
+	SkipUpdateForProposalsWithBaseBranch gitdomain.LocalBranchNames
 }
 
-// BranchProposalsProgram syncs all given proposals.
-func BranchProposalsProgram(args BranchProposalsProgramArgs) {
+// AddStackLineageUpdateOpcodes syncs all given proposals.
+// Returns the stack lineage tree if its needed to recall this function.
+func AddStackLineageUpdateOpcodes(args AddStackLineageUpdateOpcodesArgs) Option[*forge.ProposalStackLineageTree] {
 	// TODO: there are now multiple places that load and use proposals for branches.
 	// To avoid double-loading the same proposal data in one run,
 	// extract an object that caches the already known proposals,
 	// i.e. which branch has which proposal,
 	// and loads missing proposal info on demand.
-	tree, err := forge.NewProposalStackLineageTree(args.ProposalStackLineageArgs)
+	tree, hasTree := args.ProposalStackLineageTree.Get()
+	var err error
+	if hasTree {
+		err = tree.Rebuild(args.ProposalStackLineageArgs)
+	} else {
+		tree, err = forge.NewProposalStackLineageTree(args.ProposalStackLineageArgs)
+	}
 	if err != nil {
 		fmt.Printf("failed to update proposal stack lineage: %s\n", err.Error())
-		return
+		return None[*forge.ProposalStackLineageTree]()
 	}
 
 	if args.FullStack.Enabled() {
 		for branch, proposal := range mapstools.SortedKeyValues(tree.BranchToProposal) {
+			if args.SkipUpdateForProposalsWithBaseBranch.Contains(branch) {
+				continue
+			}
 			args.Program.Value.Add(&opcodes.ProposalUpdateLineage{
 				Current:         branch,
 				CurrentProposal: proposal,
 				LineageTree:     MutableSome(tree),
 			})
 		}
-	} else {
+	} else if !args.SkipUpdateForProposalsWithBaseBranch.Contains(args.Current) {
 		args.Program.Value.Add(&opcodes.ProposalUpdateLineage{
 			Current:         args.Current,
 			CurrentProposal: tree.BranchToProposal[args.Current],
 			LineageTree:     MutableSome(tree),
 		})
 	}
+
+	return Some(tree)
 }
diff --git a/internal/cmd/undo.go b/internal/cmd/undo.go
index 42e631703..52ac53756 100644
--- a/internal/cmd/undo.go
+++ b/internal/cmd/undo.go
@@ -60,6 +60,7 @@ func executeUndo(cliConfig configdomain.PartialConfig) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -124,6 +125,7 @@ func determineUndoData(repo execute.OpenRepoResult) (data undoData, flow configd
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
diff --git a/internal/cmd/up.go b/internal/cmd/up.go
index f60e9320c..33ea48d32 100644
--- a/internal/cmd/up.go
+++ b/internal/cmd/up.go
@@ -77,6 +77,7 @@ func executeUp(args executeUpArgs) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        args.cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
diff --git a/internal/cmd/walk.go b/internal/cmd/walk.go
index 81915195d..0bf7ad941 100644
--- a/internal/cmd/walk.go
+++ b/internal/cmd/walk.go
@@ -123,6 +123,7 @@ func executeWalk(args executeWalkArgs) error {
 Start:
 	repo, err := execute.OpenRepo(execute.OpenRepoArgs{
 		CliConfig:        args.cliConfig,
+		IgnoreUnknown:    false,
 		PrintBranchNames: true,
 		PrintCommands:    true,
 		ValidateGitRepo:  true,
@@ -207,6 +208,7 @@ func determineWalkData(repo execute.OpenRepoResult, all configdomain.AllBranches
 		Backend:              repo.Backend,
 		BitbucketAppPassword: config.BitbucketAppPassword,
 		BitbucketUsername:    config.BitbucketUsername,
+		Browser:              config.Browser,
 		ForgeType:            config.ForgeType,
 		ForgejoToken:         config.ForgejoToken,
 		Frontend:             repo.Frontend,
@@ -286,10 +288,12 @@ func determineWalkData(repo execute.OpenRepoResult, all configdomain.AllBranches
 	case stack.Enabled():
 		branchesToWalk = validatedConfig.NormalConfig.Lineage.BranchLineageWithoutRoot(initialBranch, perennialBranchNames, validatedConfig.NormalConfig.Order)
 	}
+	branchesInOtherWorktrees := branchesSnapshot.Branches.BranchesInOtherWorktrees()
+	branchesInCurrentWorktree := branchesToWalk.Remove(branchesInOtherWorktrees...)
 	return walkData{
 		branchInfosLastRun: branchInfosLastRun,
 		branchesSnapshot:   branchesSnapshot,
-		branchesToWalk:     branchesToWalk,
+		branchesToWalk:     branchesInCurrentWorktree,
 		config:             validatedConfig,
 		connector:          connector,
 		hasOpenChanges:     repoStatus.OpenChanges,
diff --git a/internal/config/branches_to_mark.go b/internal/config/branches_to_mark.go
index 035f217de..3a35c614d 100644
--- a/internal/config/branches_to_mark.go
+++ b/internal/config/branches_to_mark.go
@@ -9,7 +9,7 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// provides the branches to make contribution, observed, parked, or prototype
+// BranchesToMark provides the branches to make contribution, observed, parked, or prototype.
 func BranchesToMark(args []string, branchesSnapshot gitdomain.BranchesSnapshot, config UnvalidatedConfig) (branchesToMark configdomain.BranchesAndTypes, branchToCheckout Option[gitdomain.LocalBranchName], err error) {
 	branchesToMark = configdomain.BranchesAndTypes{}
 	switch len(args) {
diff --git a/internal/config/cliconfig/cli_config.go b/internal/config/cliconfig/cli_config.go
index 72fb3c3e7..d5f2ed4ec 100644
--- a/internal/config/cliconfig/cli_config.go
+++ b/internal/config/cliconfig/cli_config.go
@@ -26,7 +26,9 @@ func New(args NewArgs) configdomain.PartialConfig {
 		AutoSync:                 args.AutoSync,
 		BitbucketAppPassword:     None[forgedomain.BitbucketAppPassword](),
 		BitbucketUsername:        None[forgedomain.BitbucketUsername](),
+		BranchPrefix:             None[configdomain.BranchPrefix](),
 		BranchTypeOverrides:      configdomain.BranchTypeOverrides{},
+		Browser:                  None[configdomain.Browser](),
 		ForgejoToken:             None[forgedomain.ForgejoToken](),
 		ContributionRegex:        None[configdomain.ContributionRegex](),
 		Detached:                 args.Detached,
diff --git a/internal/config/configdomain/alias_key.go b/internal/config/configdomain/alias_key.go
index b4006e344..02c77f2dd 100644
--- a/internal/config/configdomain/alias_key.go
+++ b/internal/config/configdomain/alias_key.go
@@ -6,10 +6,10 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// A key used for storing aliases in the Git configuration
+// AliasKey is a key used for storing aliases in the Git configuration.
 type AliasKey Key
 
-// tries to convert this Key into an AliasKey
+// NewAliasKey tries to convert this Key into an AliasKey.
 func NewAliasKey(key Key) Option[AliasKey] {
 	if strings.HasPrefix(key.String(), AliasKeyPrefix) {
 		return Some(AliasKey(key))
@@ -17,13 +17,13 @@ func NewAliasKey(key Key) Option[AliasKey] {
 	return None[AliasKey]()
 }
 
-// provides the AliasableCommand matching this AliasKey
+// AliasableCommand provides the AliasableCommand matching this AliasKey.
 func (self AliasKey) AliasableCommand() AliasableCommand {
 	commandName := strings.TrimPrefix(self.String(), AliasKeyPrefix)
 	return AliasableCommand(commandName)
 }
 
-// provides the generic Key that this AliasKey represents
+// Key provides the generic Key that this AliasKey represents.
 func (self AliasKey) Key() Key {
 	return Key(self)
 }
diff --git a/internal/config/configdomain/aliasable_command.go b/internal/config/configdomain/aliasable_command.go
index a15c65a4a..4d9268754 100644
--- a/internal/config/configdomain/aliasable_command.go
+++ b/internal/config/configdomain/aliasable_command.go
@@ -3,7 +3,7 @@ package configdomain
 // AliasableCommand defines Git Town commands that can shortened via Git aliases.
 type AliasableCommand string
 
-// provides the key that configures this aliasable command in the Git config
+// Key provides the key that configures this aliasable command in the Git config.
 func (self AliasableCommand) Key() AliasKey {
 	return AliasKey(AliasKeyPrefix + self.String())
 }
diff --git a/internal/config/configdomain/aliasable_commands.go b/internal/config/configdomain/aliasable_commands.go
index cde5c053e..edda915ad 100644
--- a/internal/config/configdomain/aliasable_commands.go
+++ b/internal/config/configdomain/aliasable_commands.go
@@ -4,7 +4,7 @@ import . "github.com/git-town/git-town/v22/pkg/prelude"
 
 type AliasableCommands []AliasableCommand
 
-// provides the AliasKey matching the given key name
+// LookupKey provides the AliasKey matching the given key name.
 func (self AliasableCommands) LookupKey(name string) Option[AliasKey] {
 	for _, aliasableCommand := range self {
 		keyOfCommand := aliasableCommand.Key()
diff --git a/internal/config/configdomain/auto_resolve.go b/internal/config/configdomain/auto_resolve.go
index 01a6a420b..da4206628 100644
--- a/internal/config/configdomain/auto_resolve.go
+++ b/internal/config/configdomain/auto_resolve.go
@@ -1,6 +1,6 @@
 package configdomain
 
-// indicates whether a Git Town command should not auto-resolve phantom merge conflicts
+// AutoResolve indicates whether a Git Town command should not auto-resolve phantom merge conflicts.
 type AutoResolve bool
 
 func (self AutoResolve) NoAutoResolve() bool {
diff --git a/internal/config/configdomain/auto_sync.go b/internal/config/configdomain/auto_sync.go
index f45a831fd..7d6428dbd 100644
--- a/internal/config/configdomain/auto_sync.go
+++ b/internal/config/configdomain/auto_sync.go
@@ -2,8 +2,8 @@ package configdomain
 
 import "strconv"
 
-// indicates whether a Git Town command should sync branches
-// before performing its actual functionality
+// AutoSync indicates whether a Git Town command should sync branches
+// before performing its actual functionality.
 type AutoSync bool
 
 func (self AutoSync) ShouldSync() bool {
diff --git a/internal/config/configdomain/beam.go b/internal/config/configdomain/beam.go
index 830d88e63..5279da50e 100644
--- a/internal/config/configdomain/beam.go
+++ b/internal/config/configdomain/beam.go
@@ -1,6 +1,6 @@
 package configdomain
 
-// indicates whether a Git Town command should create a prototype branch
+// Beam indicates whether a Git Town command should beam commits to the new branch.
 type Beam bool
 
 func (self Beam) ShouldBeam() bool {
diff --git a/internal/config/configdomain/begin_config_snapshot.go b/internal/config/configdomain/begin_config_snapshot.go
index 1ac088d86..a2a414250 100644
--- a/internal/config/configdomain/begin_config_snapshot.go
+++ b/internal/config/configdomain/begin_config_snapshot.go
@@ -2,14 +2,14 @@ package configdomain
 
 import "fmt"
 
-// ConfigSnapshot is a snapshot of the entire Git configuration at a particular point in time.
+// BeginConfigSnapshot is a snapshot of the entire Git configuration before a Git Town command starts.
 type BeginConfigSnapshot struct {
 	Global   SingleSnapshot
 	Local    SingleSnapshot
 	Unscoped SingleSnapshot
 }
 
-// looks up one of the contained SingleSnapshots by scope
+// ByScope looks up one of the contained SingleSnapshots by scope.
 func (self BeginConfigSnapshot) ByScope(scope ConfigScope) SingleSnapshot {
 	switch scope {
 	case ConfigScopeGlobal:
diff --git a/internal/config/configdomain/branch_prefix.go b/internal/config/configdomain/branch_prefix.go
new file mode 100644
index 000000000..b43ab0848
--- /dev/null
+++ b/internal/config/configdomain/branch_prefix.go
@@ -0,0 +1,28 @@
+package configdomain
+
+import (
+	"strings"
+
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+// BranchPrefix is a prefix that gets automatically added to branches created by Git Town.
+type BranchPrefix string
+
+// Apply provides the given branch name with this prefix applied.
+func (self BranchPrefix) Apply(branch gitdomain.LocalBranchName) gitdomain.LocalBranchName {
+	if strings.HasPrefix(branch.String(), self.String()) {
+		return branch
+	}
+	return gitdomain.NewLocalBranchName(self.String() + branch.String())
+}
+
+func (self BranchPrefix) String() string { return string(self) }
+
+func ParseBranchPrefix(value, _ string) (Option[BranchPrefix], error) {
+	if value == "" {
+		return None[BranchPrefix](), nil
+	}
+	return Some(BranchPrefix(value)), nil
+}
diff --git a/internal/config/configdomain/branch_specific_key.go b/internal/config/configdomain/branch_specific_key.go
index bf98a0e74..5e800aea4 100644
--- a/internal/config/configdomain/branch_specific_key.go
+++ b/internal/config/configdomain/branch_specific_key.go
@@ -4,7 +4,7 @@ import (
 	"strings"
 )
 
-// a Git config key that contains a branch specific value
+// BranchSpecificKey is a Git config key that contains a branch specific value.
 type BranchSpecificKey struct {
 	Key
 }
diff --git a/internal/config/configdomain/branch_to_sync.go b/internal/config/configdomain/branch_to_sync.go
index b774cc519..28b979d84 100644
--- a/internal/config/configdomain/branch_to_sync.go
+++ b/internal/config/configdomain/branch_to_sync.go
@@ -5,7 +5,7 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// all the information needed to sync a branch
+// BranchToSync contains all the information needed to sync a branch.
 type BranchToSync struct {
 	BranchInfo         gitdomain.BranchInfo
 	FirstCommitMessage Option[gitdomain.CommitMessage] // commit message of the first commit on this branch
diff --git a/internal/config/configdomain/branch_type.go b/internal/config/configdomain/branch_type.go
index b9b32479a..f75ff9634 100644
--- a/internal/config/configdomain/branch_type.go
+++ b/internal/config/configdomain/branch_type.go
@@ -32,7 +32,7 @@ func AllBranchTypes() []BranchType {
 	}
 }
 
-func ParseBranchType(text string) (Option[BranchType], error) {
+func ParseBranchType(text string, source string) (Option[BranchType], error) {
 	if len(text) == 0 || text == messages.DialogResultNone {
 		return None[BranchType](), nil
 	}
@@ -41,7 +41,7 @@ func ParseBranchType(text string) (Option[BranchType], error) {
 			return Some(branchType), nil
 		}
 	}
-	return None[BranchType](), fmt.Errorf(messages.DialogResultUnknownBranchType, text)
+	return None[BranchType](), fmt.Errorf(messages.DialogResultUnknownBranchType, source, text)
 }
 
 func (self BranchType) MustKnowParent() bool {
diff --git a/internal/config/configdomain/branch_type_override_key.go b/internal/config/configdomain/branch_type_override_key.go
index ef52e6971..f092fb0a1 100644
--- a/internal/config/configdomain/branch_type_override_key.go
+++ b/internal/config/configdomain/branch_type_override_key.go
@@ -9,13 +9,13 @@ import (
 
 const BranchTypeSuffix = ".branchtype"
 
-// a Key that contains a BranchTypeOverrides entry,
-// for example "git-town-branch.foo.branchtype"
+// BranchTypeOverrideKey is a Key that contains a BranchTypeOverrides entry,
+// for example "git-town-branch.foo.branchtype".
 type BranchTypeOverrideKey struct {
 	BranchSpecificKey
 }
 
-// provides the name of the child branch encoded in this LineageKey
+// Branch provides the name of the child branch encoded in this LineageKey.
 func (self BranchTypeOverrideKey) Branch() gitdomain.LocalBranchName {
 	text := strings.TrimSuffix(strings.TrimPrefix(self.String(), BranchSpecificKeyPrefix), BranchTypeSuffix)
 	return gitdomain.NewLocalBranchName(text)
diff --git a/internal/config/configdomain/branch_type_overrides.go b/internal/config/configdomain/branch_type_overrides.go
index aea77e152..8f826ce16 100644
--- a/internal/config/configdomain/branch_type_overrides.go
+++ b/internal/config/configdomain/branch_type_overrides.go
@@ -10,7 +10,7 @@ import (
 // These are stored in Git metadata like this: "git-town-branch.<name>.branchtype".
 type BranchTypeOverrides map[gitdomain.LocalBranchName]BranchType
 
-// adds the given BranchTypeOverrides to this BranchTypeOverrides
+// Concat adds the given BranchTypeOverrides to this BranchTypeOverrides.
 func (self BranchTypeOverrides) Concat(other BranchTypeOverrides) BranchTypeOverrides {
 	result := make(BranchTypeOverrides, len(self)+len(other))
 	maps.Copy(result, self)
diff --git a/internal/config/configdomain/browser.go b/internal/config/configdomain/browser.go
new file mode 100644
index 000000000..7150227ec
--- /dev/null
+++ b/internal/config/configdomain/browser.go
@@ -0,0 +1,24 @@
+package configdomain
+
+import (
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+type Browser string
+
+func (self Browser) NoBrowser() bool {
+	return self == NoBrowser
+}
+
+func (self Browser) String() string {
+	return string(self)
+}
+
+func ParseBrowser(value, _ string) (Option[Browser], error) {
+	if value == "" {
+		return None[Browser](), nil
+	}
+	return Some(Browser(value)), nil
+}
+
+const NoBrowser = "(none)"
diff --git a/internal/config/configdomain/commit.go b/internal/config/configdomain/commit.go
index 515b30e87..dca6a7da9 100644
--- a/internal/config/configdomain/commit.go
+++ b/internal/config/configdomain/commit.go
@@ -1,6 +1,6 @@
 package configdomain
 
-// when creating a new branch, whether to commit the currently staged changes into that new branch
+// Commit indicates whether to commit the currently staged changes into a new branch.
 type Commit bool
 
 func (self Commit) ShouldCommit() bool {
diff --git a/internal/config/configdomain/commit_hook.go b/internal/config/configdomain/commit_hook.go
index 825992e26..6274c23a7 100644
--- a/internal/config/configdomain/commit_hook.go
+++ b/internal/config/configdomain/commit_hook.go
@@ -4,7 +4,7 @@ import (
 	"strconv"
 )
 
-// PushHook contains the push-hook configuration setting.
+// CommitHook indicates whether commit-hooks are enabled.
 type CommitHook bool
 
 const (
diff --git a/internal/config/configdomain/config_scope.go b/internal/config/configdomain/config_scope.go
index 782676482..cdee5b235 100644
--- a/internal/config/configdomain/config_scope.go
+++ b/internal/config/configdomain/config_scope.go
@@ -7,7 +7,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/messages"
 )
 
-// defines the type of Git configuration used
+// ConfigScope defines the type of Git configuration used.
 type ConfigScope string
 
 const (
diff --git a/internal/config/configdomain/contribution_regex.go b/internal/config/configdomain/contribution_regex.go
index d84cc6ba0..edc0b78f1 100644
--- a/internal/config/configdomain/contribution_regex.go
+++ b/internal/config/configdomain/contribution_regex.go
@@ -1,16 +1,21 @@
 package configdomain
 
-import . "github.com/git-town/git-town/v22/pkg/prelude"
+import (
+	"fmt"
 
-// configuration setting that allows defining branches matching this regular expression as contribution branches
+	"github.com/git-town/git-town/v22/internal/messages"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+// ContributionRegex is a configuration setting that allows defining branches matching this regular expression as contribution branches.
 type ContributionRegex struct {
 	VerifiedRegex
 }
 
-func ParseContributionRegex(value string) (Option[ContributionRegex], error) {
+func ParseContributionRegex(value string, source string) (Option[ContributionRegex], error) {
 	verifiedRegexOpt, err := ParseRegex(value)
 	if err != nil {
-		return None[ContributionRegex](), err
+		return None[ContributionRegex](), fmt.Errorf(messages.CannotParse, source, err)
 	}
 	if verifiedRegex, hasVerifiedRegex := verifiedRegexOpt.Get(); hasVerifiedRegex {
 		return Some(ContributionRegex{VerifiedRegex: verifiedRegex}), nil
diff --git a/internal/config/configdomain/detached.go b/internal/config/configdomain/detached.go
index ebc26d146..e963261a4 100644
--- a/internal/config/configdomain/detached.go
+++ b/internal/config/configdomain/detached.go
@@ -2,7 +2,7 @@ package configdomain
 
 import "strconv"
 
-// indicates whether a Git Town command should not update the root branch of the stack
+// Detached indicates whether a Git Town command should not update the root branch of the stack.
 type Detached bool
 
 func (self Detached) ShouldWorkDetached() bool {
diff --git a/internal/config/configdomain/display_types.go b/internal/config/configdomain/display_types.go
index a029c10cf..05513ff74 100644
--- a/internal/config/configdomain/display_types.go
+++ b/internal/config/configdomain/display_types.go
@@ -10,7 +10,7 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// whether to display branch types in the CLI output
+// DisplayTypes specifies whether to display branch types in the CLI output.
 type DisplayTypes struct {
 	BranchTypes []BranchType // the branch types for which the user has specified exceptions
 	Quantifier  Quantifier   // whether to include or exclude the listed branches
@@ -25,7 +25,7 @@ const (
 	QuantifierOnly = ""    // display only the specified branches
 )
 
-// provides the serialized version to be used in configuration data
+// Serialize provides the serialized version to be used in configuration data.
 func (self DisplayTypes) Serialize(delimiter string) string {
 	switch self.Quantifier {
 	case QuantifierAll:
@@ -41,7 +41,7 @@ func (self DisplayTypes) Serialize(delimiter string) string {
 	panic("unhandled DisplayType quantifier: " + self.Quantifier)
 }
 
-// indicates whether Git Town should display the given branch type
+// ShouldDisplayType indicates whether Git Town should display the given branch type.
 func (self DisplayTypes) ShouldDisplayType(branchType BranchType) bool {
 	switch self.Quantifier {
 	case QuantifierAll:
@@ -57,7 +57,7 @@ func (self DisplayTypes) ShouldDisplayType(branchType BranchType) bool {
 	panic("unhandled DisplayType state: " + self.String())
 }
 
-// provides a human-readable version of this DisplayTypes
+// String provides a human-readable version of this DisplayTypes.
 func (self DisplayTypes) String() string {
 	switch self.Quantifier {
 	case QuantifierAll:
@@ -95,7 +95,7 @@ func ParseDisplayTypes(text, source string) (Option[DisplayTypes], error) {
 	}
 	branchTypes := make([]BranchType, len(parts))
 	for p, part := range parts {
-		branchTypeOpt, err := ParseBranchType(part)
+		branchTypeOpt, err := ParseBranchType(part, source)
 		if err != nil {
 			return None[DisplayTypes](), err
 		}
diff --git a/internal/config/configdomain/dry_run.go b/internal/config/configdomain/dry_run.go
index 36aa03e74..8e8d67a48 100644
--- a/internal/config/configdomain/dry_run.go
+++ b/internal/config/configdomain/dry_run.go
@@ -1,4 +1,4 @@
 package configdomain
 
-// indicates whether a Git Town command should execute the commands or only display them
+// DryRun indicates whether a Git Town command should execute the commands or only display them.
 type DryRun bool
diff --git a/internal/config/configdomain/end_config_snapshot.go b/internal/config/configdomain/end_config_snapshot.go
index 709faa02d..892319fba 100644
--- a/internal/config/configdomain/end_config_snapshot.go
+++ b/internal/config/configdomain/end_config_snapshot.go
@@ -1,6 +1,6 @@
 package configdomain
 
-// ConfigSnapshot is a snapshot of the entire Git configuration at a particular point in time.
+// EndConfigSnapshot is a snapshot of the entire Git configuration after a Git Town command finished.
 type EndConfigSnapshot struct {
 	Global SingleSnapshot
 	Local  SingleSnapshot
diff --git a/internal/config/configdomain/feature_regex.go b/internal/config/configdomain/feature_regex.go
index 7e644ce06..8bfe40847 100644
--- a/internal/config/configdomain/feature_regex.go
+++ b/internal/config/configdomain/feature_regex.go
@@ -1,13 +1,21 @@
 package configdomain
 
-import . "github.com/git-town/git-town/v22/pkg/prelude"
+import (
+	"fmt"
+
+	"github.com/git-town/git-town/v22/internal/messages"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
 
 type FeatureRegex struct {
 	VerifiedRegex
 }
 
-func ParseFeatureRegex(value string) (Option[FeatureRegex], error) {
+func ParseFeatureRegex(value string, source string) (Option[FeatureRegex], error) {
 	verifiedRegexOpt, err := ParseRegex(value)
+	if err != nil {
+		return None[FeatureRegex](), fmt.Errorf(messages.CannotParse, source, err)
+	}
 	if verifiedRegex, hasVerifiedRegex := verifiedRegexOpt.Get(); hasVerifiedRegex {
 		return Some(FeatureRegex{VerifiedRegex: verifiedRegex}), err
 	}
diff --git a/internal/config/configdomain/force.go b/internal/config/configdomain/force.go
index 869049f1c..0f92a2768 100644
--- a/internal/config/configdomain/force.go
+++ b/internal/config/configdomain/force.go
@@ -1,4 +1,4 @@
 package configdomain
 
-// indicates whether a Git Town command should execute the commands despite not all safety conditions in place
+// Force indicates whether a Git Town command should execute the commands despite not all safety conditions in place.
 type Force bool
diff --git a/internal/config/configdomain/full_stack.go b/internal/config/configdomain/full_stack.go
index 68bd97770..3c0924269 100644
--- a/internal/config/configdomain/full_stack.go
+++ b/internal/config/configdomain/full_stack.go
@@ -1,8 +1,9 @@
 package configdomain
 
-// indicates whether to perform an activity on all branches in the current stack
+// FullStack indicates whether to perform an activity on all branches in the current stack.
 type FullStack bool
 
+// Enabled indicates whether the full stack is enabled.
 func (self FullStack) Enabled() bool {
 	return bool(self)
 }
diff --git a/internal/config/configdomain/key.go b/internal/config/configdomain/key.go
index 9d0d5b6b8..3cadaf32f 100644
--- a/internal/config/configdomain/key.go
+++ b/internal/config/configdomain/key.go
@@ -58,6 +58,8 @@ const (
 	KeyAutoSync                            = Key("git-town.auto-sync")
 	KeyBitbucketAppPassword                = Key("git-town.bitbucket-app-password")
 	KeyBitbucketUsername                   = Key("git-town.bitbucket-username")
+	KeyBranchPrefix                        = Key("git-town.branch-prefix")
+	KeyBrowser                             = Key("git-town.browser")
 	KeyContributionRegex                   = Key("git-town.contribution-regex")
 	KeyDeprecatedCodebergToken             = Key("git-town.codeberg-token")
 	KeyDeprecatedCodeHostingDriver         = Key("git-town.code-hosting-driver")
@@ -122,6 +124,8 @@ var keys = []Key{
 	KeyAutoSync,
 	KeyBitbucketAppPassword,
 	KeyBitbucketUsername,
+	KeyBranchPrefix,
+	KeyBrowser,
 	KeyContributionRegex,
 	KeyDeprecatedAliasKill,
 	KeyDeprecatedAliasRenameBranch,
diff --git a/internal/config/configdomain/lineage.go b/internal/config/configdomain/lineage.go
index 2a3161771..a13bfc7c6 100644
--- a/internal/config/configdomain/lineage.go
+++ b/internal/config/configdomain/lineage.go
@@ -103,7 +103,7 @@ func (self Lineage) BranchesAndAncestors(branchNames gitdomain.LocalBranchNames,
 	return self.OrderHierarchically(result, order)
 }
 
-// provides all branches for which the parent is known
+// BranchesWithParents provides all branches for which the parent is known.
 func (self Lineage) BranchesWithParents() gitdomain.LocalBranchNames {
 	var result gitdomain.LocalBranchNames = slices.Collect(maps.Keys(self.data))
 	slice.NaturalSort(result)
@@ -184,7 +184,7 @@ func (self Lineage) Len() int {
 	return len(self.data)
 }
 
-// provides a new Lineage that consists of entries from both this and the given Lineage
+// Merge provides a new Lineage that consists of entries from both this and the given Lineage
 func (self Lineage) Merge(other Lineage) Lineage {
 	return Lineage{
 		data: mapstools.Merge(self.data, other.data),
@@ -247,7 +247,7 @@ func (self Lineage) Set(branch, parent gitdomain.LocalBranchName) Lineage {
 	return self
 }
 
-// provides the first branch from the given whitelist that is an ancestor of the given branch
+// YoungestAncestorWithin provides the first branch from the given whitelist that is an ancestor of the given branch
 func (self Lineage) YoungestAncestorWithin(branch gitdomain.LocalBranchName, whitelist gitdomain.LocalBranchNames) Option[gitdomain.LocalBranchName] {
 	for {
 		parent, hasParent := self.Parent(branch).Get()
diff --git a/internal/config/configdomain/lineage_key.go b/internal/config/configdomain/lineage_key.go
index 981a9631f..063fe16f8 100644
--- a/internal/config/configdomain/lineage_key.go
+++ b/internal/config/configdomain/lineage_key.go
@@ -7,7 +7,7 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// a Key that contains a lineage entry
+// LineageKey is a Key that contains a lineage entry.
 type LineageKey struct {
 	BranchSpecificKey
 }
@@ -20,7 +20,7 @@ func NewLineageKey(key Key) LineageKey {
 	}
 }
 
-// CheckLineage indicates using the returned option whether this key is a lineage key.
+// ParseLineageKey indicates using the returned option whether this key is a lineage key.
 func ParseLineageKey(key Key) Option[LineageKey] {
 	if isLineageKey(key.String()) {
 		return Some(NewLineageKey(key))
@@ -28,7 +28,7 @@ func ParseLineageKey(key Key) Option[LineageKey] {
 	return None[LineageKey]()
 }
 
-// provides the name of the child branch encoded in this LineageKey
+// ChildBranch provides the name of the child branch encoded in this LineageKey.
 func (self LineageKey) ChildBranch() gitdomain.LocalBranchName {
 	text := strings.TrimSuffix(strings.TrimPrefix(self.String(), BranchSpecificKeyPrefix), LineageKeySuffix)
 	return gitdomain.NewLocalBranchName(text)
diff --git a/internal/config/configdomain/new_branch_type.go b/internal/config/configdomain/new_branch_type.go
index c3fa54133..9ecc76c03 100644
--- a/internal/config/configdomain/new_branch_type.go
+++ b/internal/config/configdomain/new_branch_type.go
@@ -2,7 +2,7 @@ package configdomain
 
 import . "github.com/git-town/git-town/v22/pkg/prelude"
 
-// the type that branches created with hack, append, and prepend have
+// NewBranchType is the type that branches created with hack, append, and prepend have.
 type NewBranchType BranchType
 
 func (self NewBranchType) BranchType() BranchType {
diff --git a/internal/config/configdomain/no_parent.go b/internal/config/configdomain/no_parent.go
index e3efa9e8c..4407c3150 100644
--- a/internal/config/configdomain/no_parent.go
+++ b/internal/config/configdomain/no_parent.go
@@ -1,4 +1,5 @@
 package configdomain
 
-// indicates whether to set no parent
+// NoParent indicates whether to set nothing as the parent of a branch,
+// i.e. create a perennial branch.
 type NoParent bool
diff --git a/internal/config/configdomain/observed_regex.go b/internal/config/configdomain/observed_regex.go
index e8ff6750c..e4a25ad70 100644
--- a/internal/config/configdomain/observed_regex.go
+++ b/internal/config/configdomain/observed_regex.go
@@ -1,16 +1,21 @@
 package configdomain
 
-import . "github.com/git-town/git-town/v22/pkg/prelude"
+import (
+	"fmt"
 
-// configuration setting that allows defining branches matching this regular expression as observed branches
+	"github.com/git-town/git-town/v22/internal/messages"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+// ObservedRegex allows marking branches as observed branches
 type ObservedRegex struct {
 	VerifiedRegex
 }
 
-func ParseObservedRegex(value string) (Option[ObservedRegex], error) {
+func ParseObservedRegex(value string, source string) (Option[ObservedRegex], error) {
 	verifiedRegexOpt, err := ParseRegex(value)
 	if err != nil {
-		return None[ObservedRegex](), err
+		return None[ObservedRegex](), fmt.Errorf(messages.ObservedRegexCannotParse, value, source, err)
 	}
 	if verifiedRegex, hasVerifiedRegex := verifiedRegexOpt.Get(); hasVerifiedRegex {
 		return Some(ObservedRegex{VerifiedRegex: verifiedRegex}), nil
diff --git a/internal/config/configdomain/order.go b/internal/config/configdomain/order.go
index 43e1c9b05..7bdb4eb8f 100644
--- a/internal/config/configdomain/order.go
+++ b/internal/config/configdomain/order.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/git-town/git-town/v22/internal/messages"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
@@ -24,7 +25,7 @@ func (self Order) String() string {
 	return string(self)
 }
 
-func ParseOrder(value string, source Key) (Option[Order], error) {
+func ParseOrder(value string, source string) (Option[Order], error) {
 	switch strings.ToLower(value) {
 	case "":
 		return None[Order](), nil
@@ -33,6 +34,6 @@ func ParseOrder(value string, source Key) (Option[Order], error) {
 	case "d", "de", "des", "desc":
 		return Some(OrderDesc), nil
 	default:
-		return None[Order](), fmt.Errorf("invalid value for %s: %q", source, value)
+		return None[Order](), fmt.Errorf(messages.OrderInvalid, source, value)
 	}
 }
diff --git a/internal/config/configdomain/partial_config.go b/internal/config/configdomain/partial_config.go
index fac2e4636..5794006d1 100644
--- a/internal/config/configdomain/partial_config.go
+++ b/internal/config/configdomain/partial_config.go
@@ -21,7 +21,9 @@ type PartialConfig struct {
 	AutoSync                 Option[AutoSync]
 	BitbucketAppPassword     Option[forgedomain.BitbucketAppPassword]
 	BitbucketUsername        Option[forgedomain.BitbucketUsername]
+	BranchPrefix             Option[BranchPrefix]
 	BranchTypeOverrides      BranchTypeOverrides
+	Browser                  Option[Browser]
 	ContributionRegex        Option[ContributionRegex]
 	Detached                 Option[Detached]
 	DevRemote                Option[gitdomain.Remote]
@@ -69,7 +71,8 @@ func EmptyPartialConfig() PartialConfig {
 	} //exhaustruct:ignore
 }
 
-// Merges the given PartialConfig into this configuration object.
+// Merge combines the data of the given PartialConfig with this PartialConfig,
+// favoring the data of the given PartialConfig.
 func (self PartialConfig) Merge(other PartialConfig) PartialConfig {
 	return PartialConfig{
 		Aliases:                  mapstools.Merge(other.Aliases, self.Aliases),
@@ -77,7 +80,9 @@ func (self PartialConfig) Merge(other PartialConfig) PartialConfig {
 		AutoSync:                 other.AutoSync.Or(self.AutoSync),
 		BitbucketAppPassword:     other.BitbucketAppPassword.Or(self.BitbucketAppPassword),
 		BitbucketUsername:        other.BitbucketUsername.Or(self.BitbucketUsername),
+		BranchPrefix:             other.BranchPrefix.Or(self.BranchPrefix),
 		BranchTypeOverrides:      other.BranchTypeOverrides.Concat(self.BranchTypeOverrides),
+		Browser:                  other.Browser.Or(self.Browser),
 		ContributionRegex:        other.ContributionRegex.Or(self.ContributionRegex),
 		Detached:                 other.Detached.Or(self.Detached),
 		DevRemote:                other.DevRemote.Or(self.DevRemote),
diff --git a/internal/config/configdomain/pending.go b/internal/config/configdomain/pending.go
index c91291af1..804e563d5 100644
--- a/internal/config/configdomain/pending.go
+++ b/internal/config/configdomain/pending.go
@@ -1,4 +1,4 @@
 package configdomain
 
-// indicates whether "git town status" should print only the pending command
+// Pending indicates whether "git town status" should print only the pending command
 type Pending bool
diff --git a/internal/config/configdomain/perennial_regex.go b/internal/config/configdomain/perennial_regex.go
index 4f0b6ae3b..aa6bc8ac7 100644
--- a/internal/config/configdomain/perennial_regex.go
+++ b/internal/config/configdomain/perennial_regex.go
@@ -1,13 +1,21 @@
 package configdomain
 
-import . "github.com/git-town/git-town/v22/pkg/prelude"
+import (
+	"fmt"
+
+	"github.com/git-town/git-town/v22/internal/messages"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
 
 type PerennialRegex struct {
 	VerifiedRegex
 }
 
-func ParsePerennialRegex(value string) (Option[PerennialRegex], error) {
+func ParsePerennialRegex(value string, source string) (Option[PerennialRegex], error) {
 	verifiedRegexOpt, err := ParseRegex(value)
+	if err != nil {
+		return None[PerennialRegex](), fmt.Errorf(messages.CannotParse, source, err)
+	}
 	if verifiedRegex, hasVerifiedRegex := verifiedRegexOpt.Get(); hasVerifiedRegex {
 		return Some(PerennialRegex{VerifiedRegex: verifiedRegex}), err
 	}
diff --git a/internal/config/configdomain/propose.go b/internal/config/configdomain/propose.go
index ad6ac4b52..6578705b6 100644
--- a/internal/config/configdomain/propose.go
+++ b/internal/config/configdomain/propose.go
@@ -1,6 +1,6 @@
 package configdomain
 
-// indicates whether a Git Town command should propose the branch it creates
+// Propose indicates whether a Git Town command should propose the branch it creates
 type Propose bool
 
 func (self Propose) ShouldPropose() bool {
diff --git a/internal/config/configdomain/prototype.go b/internal/config/configdomain/prototype.go
index 8a7aabf67..7f9dc008b 100644
--- a/internal/config/configdomain/prototype.go
+++ b/internal/config/configdomain/prototype.go
@@ -1,4 +1,4 @@
 package configdomain
 
-// indicates whether a Git Town command should create a prototype branch
+// Prototype indicates whether a Git Town command should create a prototype branch
 type Prototype bool
diff --git a/internal/config/configdomain/prune.go b/internal/config/configdomain/prune.go
index 069041a2a..e56dbb001 100644
--- a/internal/config/configdomain/prune.go
+++ b/internal/config/configdomain/prune.go
@@ -1,4 +1,5 @@
 package configdomain
 
-// indicates whether a Git Town command should remove empty branches
+// Prune indicates whether a Git Town command should remove empty branches
+// while syncing.
 type Prune bool
diff --git a/internal/config/configdomain/push_branches.go b/internal/config/configdomain/push_branches.go
index 9a55c3fe6..e24dda11c 100644
--- a/internal/config/configdomain/push_branches.go
+++ b/internal/config/configdomain/push_branches.go
@@ -2,7 +2,7 @@ package configdomain
 
 import "strconv"
 
-// indicates whether Git Town commands should push local commits to the respective tracking branch
+// PushBranches indicates whether Git Town commands should push local commits to the respective tracking branch
 type PushBranches bool
 
 func (self PushBranches) ShouldPush() bool {
diff --git a/internal/config/configdomain/share_new_branches.go b/internal/config/configdomain/share_new_branches.go
index 37dc44500..8df5bb7e8 100644
--- a/internal/config/configdomain/share_new_branches.go
+++ b/internal/config/configdomain/share_new_branches.go
@@ -3,6 +3,7 @@ package configdomain
 import (
 	"fmt"
 
+	"github.com/git-town/git-town/v22/internal/gohacks"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
@@ -10,12 +11,9 @@ import (
 type ShareNewBranches string
 
 const (
-	// don't share new branches
-	ShareNewBranchesNone ShareNewBranches = "no"
-	// push new branches to the dev remote
-	ShareNewBranchesPush ShareNewBranches = "push"
-	// propose new branches
-	ShareNewBranchesPropose ShareNewBranches = "propose"
+	ShareNewBranchesNone    ShareNewBranches = "no"      // don't share new branches
+	ShareNewBranchesPush    ShareNewBranches = "push"    // push new branches to the dev remote
+	ShareNewBranchesPropose ShareNewBranches = "propose" // propose new branches
 )
 
 var ShareNewBranchValues = []ShareNewBranches{
@@ -28,10 +26,14 @@ func (self ShareNewBranches) String() string {
 	return string(self)
 }
 
-func ParseShareNewBranches(value string, source Key) (Option[ShareNewBranches], error) {
+func ParseShareNewBranches(value string, source string) (Option[ShareNewBranches], error) {
 	if value == "" {
 		return None[ShareNewBranches](), nil
 	}
+	parsed, err := gohacks.ParseBool[bool](value, source)
+	if err == nil && !parsed {
+		return Some(ShareNewBranchesNone), nil
+	}
 	for _, option := range ShareNewBranchValues {
 		if value == option.String() {
 			return Some(option), nil
diff --git a/internal/config/configdomain/ship_into_nonperennial_parent.go b/internal/config/configdomain/ship_into_nonperennial_parent.go
index 348ecff0b..16340a289 100644
--- a/internal/config/configdomain/ship_into_nonperennial_parent.go
+++ b/internal/config/configdomain/ship_into_nonperennial_parent.go
@@ -1,4 +1,4 @@
 package configdomain
 
-// indicates whether to sync all branches or only the current branch
+// ShipIntoNonperennialParent indicates whether to allow shipping into another feature branch.
 type ShipIntoNonperennialParent bool
diff --git a/internal/config/configdomain/ship_strategy.go b/internal/config/configdomain/ship_strategy.go
index 1de0f51f3..4c37269ca 100644
--- a/internal/config/configdomain/ship_strategy.go
+++ b/internal/config/configdomain/ship_strategy.go
@@ -21,7 +21,7 @@ func (self ShipStrategy) String() string {
 	return string(self)
 }
 
-func ParseShipStrategy(text string) (Option[ShipStrategy], error) {
+func ParseShipStrategy(text string, source string) (Option[ShipStrategy], error) {
 	text = strings.TrimSpace(text)
 	if text == "" {
 		return None[ShipStrategy](), nil
@@ -32,7 +32,7 @@ func ParseShipStrategy(text string) (Option[ShipStrategy], error) {
 			return Some(shipStrategy), nil
 		}
 	}
-	return None[ShipStrategy](), fmt.Errorf(messages.ConfigShipStrategyUnknown, text)
+	return None[ShipStrategy](), fmt.Errorf(messages.ConfigShipStrategyUnknown, source, text)
 }
 
 func ShipStrategies() []ShipStrategy {
diff --git a/internal/config/configdomain/single_snapshot.go b/internal/config/configdomain/single_snapshot.go
index 827f53bce..da0e631c7 100644
--- a/internal/config/configdomain/single_snapshot.go
+++ b/internal/config/configdomain/single_snapshot.go
@@ -5,7 +5,7 @@ import "github.com/git-town/git-town/v22/internal/gohacks/mapstools"
 // SingleSnapshot contains all of the local or global Git metadata config settings.
 type SingleSnapshot map[Key]string
 
-// provides all the keys that describe aliases for Git Town commands
+// AliasEntries provides all the keys that describe aliases for Git Town commands
 func (self SingleSnapshot) AliasEntries() map[AliasKey]string {
 	result := map[AliasKey]string{}
 	for key, value := range self { // okay to iterate the map in random order here because we assign to a new map
@@ -25,7 +25,7 @@ func (self SingleSnapshot) Aliases() Aliases {
 	return result
 }
 
-// provides all the keys that describe lineage entries
+// LineageEntries provides all the keys that describe lineage entries
 func (self SingleSnapshot) LineageEntries() map[LineageKey]string {
 	result := map[LineageKey]string{}
 	for key, value := range self { // okay to iterate the map in random order here because we assign to a new map
diff --git a/internal/config/configdomain/stash.go b/internal/config/configdomain/stash.go
index 02c8d2ffd..d1d5c6aa6 100644
--- a/internal/config/configdomain/stash.go
+++ b/internal/config/configdomain/stash.go
@@ -2,7 +2,7 @@ package configdomain
 
 import "strconv"
 
-// indicates whether Git Town should stash open changes before creating a new branch
+// Stash indicates whether Git Town should stash open changes before creating a new branch
 type Stash bool
 
 func (self Stash) ShouldStash() bool {
diff --git a/internal/config/configdomain/switch_using_merge.go b/internal/config/configdomain/switch_using_merge.go
index 49ad9edc3..db892d0f2 100644
--- a/internal/config/configdomain/switch_using_merge.go
+++ b/internal/config/configdomain/switch_using_merge.go
@@ -1,6 +1,6 @@
 package configdomain
 
-// indicates whether to switch to another branch using Git's --merge flag
+// SwitchUsingMerge indicates whether to switch to another branch using Git's --merge flag
 type SwitchUsingMerge bool
 
 func (self SwitchUsingMerge) Enabled() bool {
diff --git a/internal/config/configdomain/sync_all_branches.go b/internal/config/configdomain/sync_all_branches.go
index dbb641ab6..c76c321a0 100644
--- a/internal/config/configdomain/sync_all_branches.go
+++ b/internal/config/configdomain/sync_all_branches.go
@@ -1,6 +1,6 @@
 package configdomain
 
-// indicates whether to sync all branches or only the current branch
+// AllBranches indicates whether to sync all branches or only the current branch
 type AllBranches bool
 
 func (self AllBranches) Enabled() bool {
diff --git a/internal/config/configdomain/sync_feature_strategy.go b/internal/config/configdomain/sync_feature_strategy.go
index 873adf0b4..e584e0a7e 100644
--- a/internal/config/configdomain/sync_feature_strategy.go
+++ b/internal/config/configdomain/sync_feature_strategy.go
@@ -1,6 +1,9 @@
 package configdomain
 
 import (
+	"fmt"
+
+	"github.com/git-town/git-town/v22/internal/messages"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
@@ -21,8 +24,11 @@ const (
 	SyncFeatureStrategyCompress = SyncFeatureStrategy(SyncStrategyCompress)
 )
 
-func ParseSyncFeatureStrategy(text string) (Option[SyncFeatureStrategy], error) {
-	syncStrategyOpt, err := ParseSyncStrategy(text)
+func ParseSyncFeatureStrategy(value string, source string) (Option[SyncFeatureStrategy], error) {
+	syncStrategyOpt, err := ParseSyncStrategy(value)
+	if err != nil {
+		return None[SyncFeatureStrategy](), fmt.Errorf(messages.CannotParse, source, err)
+	}
 	if syncStrategy, has := syncStrategyOpt.Get(); has {
 		return Some(SyncFeatureStrategy(syncStrategy)), err
 	}
diff --git a/internal/config/configdomain/sync_perennial_strategy.go b/internal/config/configdomain/sync_perennial_strategy.go
index 96588e677..75bb031b6 100644
--- a/internal/config/configdomain/sync_perennial_strategy.go
+++ b/internal/config/configdomain/sync_perennial_strategy.go
@@ -1,6 +1,11 @@
 package configdomain
 
-import . "github.com/git-town/git-town/v22/pkg/prelude"
+import (
+	"fmt"
+
+	"github.com/git-town/git-town/v22/internal/messages"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
 
 // SyncPerennialStrategy defines legal values for the "sync-perennial-strategy" configuration setting.
 type SyncPerennialStrategy SyncStrategy
@@ -19,8 +24,11 @@ const (
 	SyncPerennialStrategyFFOnly = SyncPerennialStrategy(SyncStrategyFFOnly)
 )
 
-func ParseSyncPerennialStrategy(text string) (Option[SyncPerennialStrategy], error) {
-	syncStrategyOpt, err := ParseSyncStrategy(text)
+func ParseSyncPerennialStrategy(value string, source string) (Option[SyncPerennialStrategy], error) {
+	syncStrategyOpt, err := ParseSyncStrategy(value)
+	if err != nil {
+		return None[SyncPerennialStrategy](), fmt.Errorf(messages.CannotParse, source, err)
+	}
 	if syncStrategy, has := syncStrategyOpt.Get(); has {
 		return Some(SyncPerennialStrategy(syncStrategy)), err
 	}
diff --git a/internal/config/configdomain/sync_prototype_strategy.go b/internal/config/configdomain/sync_prototype_strategy.go
index f338e8b23..2ae08a18f 100644
--- a/internal/config/configdomain/sync_prototype_strategy.go
+++ b/internal/config/configdomain/sync_prototype_strategy.go
@@ -1,6 +1,11 @@
 package configdomain
 
-import . "github.com/git-town/git-town/v22/pkg/prelude"
+import (
+	"fmt"
+
+	"github.com/git-town/git-town/v22/internal/messages"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
 
 // SyncPrototypeStrategy defines legal values for the "sync-prototype-strategy" configuration setting.
 type SyncPrototypeStrategy SyncStrategy
@@ -23,8 +28,11 @@ func NewSyncPrototypeStrategyFromSyncFeatureStrategy(syncFeatureStrategy SyncFea
 	return SyncPrototypeStrategy(syncFeatureStrategy)
 }
 
-func ParseSyncPrototypeStrategy(text string) (Option[SyncPrototypeStrategy], error) {
-	syncStrategyOpt, err := ParseSyncStrategy(text)
+func ParseSyncPrototypeStrategy(value string, source string) (Option[SyncPrototypeStrategy], error) {
+	syncStrategyOpt, err := ParseSyncStrategy(value)
+	if err != nil {
+		return None[SyncPrototypeStrategy](), fmt.Errorf(messages.CannotParse, source, err)
+	}
 	if syncStrategy, has := syncStrategyOpt.Get(); has {
 		return Some(SyncPrototypeStrategy(syncStrategy)), err
 	}
diff --git a/internal/config/configdomain/sync_strategy.go b/internal/config/configdomain/sync_strategy.go
index 25f01d3d6..0086e3165 100644
--- a/internal/config/configdomain/sync_strategy.go
+++ b/internal/config/configdomain/sync_strategy.go
@@ -34,7 +34,7 @@ func ParseSyncStrategy(text string) (Option[SyncStrategy], error) {
 	return None[SyncStrategy](), fmt.Errorf(messages.ConfigSyncStrategyUnknown, text)
 }
 
-// provides all valid sync strategies
+// SyncStrategies provides all valid sync strategies
 func SyncStrategies() []SyncStrategy {
 	return []SyncStrategy{
 		SyncStrategyMerge,
diff --git a/internal/config/configdomain/unknown_branch_type.go b/internal/config/configdomain/unknown_branch_type.go
index 63b0e4635..b600f2308 100644
--- a/internal/config/configdomain/unknown_branch_type.go
+++ b/internal/config/configdomain/unknown_branch_type.go
@@ -2,7 +2,7 @@ package configdomain
 
 import . "github.com/git-town/git-town/v22/pkg/prelude"
 
-// the type that branches downloaded from the dev remote have
+// UnknownBranchType is the type that branches downloaded from the dev remote have by default.
 type UnknownBranchType BranchType
 
 func (self UnknownBranchType) BranchType() BranchType {
diff --git a/internal/config/configdomain/unvalidated_config_data.go b/internal/config/configdomain/unvalidated_config_data.go
index 3fd5bdb2b..058183745 100644
--- a/internal/config/configdomain/unvalidated_config_data.go
+++ b/internal/config/configdomain/unvalidated_config_data.go
@@ -23,7 +23,9 @@ func (self *UnvalidatedConfigData) IsMainBranch(branch gitdomain.LocalBranchName
 	return false
 }
 
-// indicates the branch type of the given branch, if it can determine it
+// PartialBranchType provides the type of the given branch,
+// based solely on the incomplete branch type information in this UnvalidatedConfigData.
+// For correct branch types, use ValidatedConfig.BranchType.
 func (self *UnvalidatedConfigData) PartialBranchType(branch gitdomain.LocalBranchName) Option[BranchType] {
 	if self.IsMainBranch(branch) {
 		return Some(BranchTypeMainBranch)
diff --git a/internal/config/configdomain/verbose.go b/internal/config/configdomain/verbose.go
index a1716fad0..6993a83dc 100644
--- a/internal/config/configdomain/verbose.go
+++ b/internal/config/configdomain/verbose.go
@@ -1,4 +1,4 @@
 package configdomain
 
-// indicates whether a Git Town command should display all Git commands it executes
+// Verbose indicates whether a Git Town command should display all Git commands it executes
 type Verbose bool
diff --git a/internal/config/configdomain/verified_regex.go b/internal/config/configdomain/verified_regex.go
index 4d5d56917..bc5c0edec 100644
--- a/internal/config/configdomain/verified_regex.go
+++ b/internal/config/configdomain/verified_regex.go
@@ -7,7 +7,8 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// a regular expression in the Git Town configuration
+// VerifiedRegex is a regular expression in the Git Town configuration
+// that is known to be valid, and for which the original source is available.
 type VerifiedRegex struct {
 	regex *regexp.Regexp
 	text  string
diff --git a/internal/config/configfile/data.go b/internal/config/configfile/data.go
index 30d3d9a60..8cbb3647f 100644
--- a/internal/config/configfile/data.go
+++ b/internal/config/configfile/data.go
@@ -36,6 +36,7 @@ func (self Branches) IsEmpty() bool {
 }
 
 type Create struct {
+	BranchPrefix     *string `toml:"branch-prefix"`
 	NewBranchType    *string `toml:"new-branch-type"`
 	PushNewbranches  *bool   `toml:"push-new-branches"`
 	ShareNewBranches *string `toml:"share-new-branches"`
@@ -43,6 +44,7 @@ type Create struct {
 }
 
 type Hosting struct {
+	Browser             *string `toml:"browser"`
 	DevRemote           *string `toml:"dev-remote"`
 	ForgeType           *string `toml:"forge-type"`
 	GitHubConnectorType *string `toml:"github-connector"`
diff --git a/internal/config/configfile/load.go b/internal/config/configfile/load.go
index e895881b5..6e9d0ad95 100644
--- a/internal/config/configfile/load.go
+++ b/internal/config/configfile/load.go
@@ -44,38 +44,42 @@ func Load(rootDir gitdomain.RepoRootDir, fileName string, finalMessages stringsl
 
 // Validate converts the given low-level configfile data into high-level config data.
 func Validate(data Data, finalMessages stringslice.Collector) (configdomain.PartialConfig, error) {
-	// keep-sorted start
-	var autoResolve Option[configdomain.AutoResolve]
-	var autoSync Option[configdomain.AutoSync]
-	var contributionRegex Option[configdomain.ContributionRegex]
-	var detached Option[configdomain.Detached]
-	var devRemote Option[gitdomain.Remote]
-	var displayTypes Option[configdomain.DisplayTypes]
-	var featureRegex Option[configdomain.FeatureRegex]
-	var forgeType Option[forgedomain.ForgeType]
-	var githubConnectorType Option[forgedomain.GitHubConnectorType]
-	var gitlabConnectorType Option[forgedomain.GitLabConnectorType]
-	var hostingOriginHostname Option[configdomain.HostingOriginHostname]
-	var mainBranch Option[gitdomain.LocalBranchName]
-	var newBranchType Option[configdomain.NewBranchType]
-	var observedRegex Option[configdomain.ObservedRegex]
-	var order Option[configdomain.Order]
-	var perennialBranches gitdomain.LocalBranchNames
-	var perennialRegex Option[configdomain.PerennialRegex]
-	var proposalsShowLineage Option[forgedomain.ProposalsShowLineage]
-	var pushBranches Option[configdomain.PushBranches]
-	var pushHook Option[configdomain.PushHook]
-	var shareNewBranches Option[configdomain.ShareNewBranches]
-	var shipDeleteTrackingBranch Option[configdomain.ShipDeleteTrackingBranch]
-	var shipStrategy Option[configdomain.ShipStrategy]
-	var stash Option[configdomain.Stash]
-	var syncFeatureStrategy Option[configdomain.SyncFeatureStrategy]
-	var syncPerennialStrategy Option[configdomain.SyncPerennialStrategy]
-	var syncPrototypeStrategy Option[configdomain.SyncPrototypeStrategy]
-	var syncTags Option[configdomain.SyncTags]
-	var syncUpstream Option[configdomain.SyncUpstream]
-	var unknownBranchType Option[configdomain.UnknownBranchType]
-	// keep-sorted end
+	var (
+		// keep-sorted start
+		autoResolve              Option[configdomain.AutoResolve]
+		autoSync                 Option[configdomain.AutoSync]
+		branchPrefix             Option[configdomain.BranchPrefix]
+		browser                  Option[configdomain.Browser]
+		contributionRegex        Option[configdomain.ContributionRegex]
+		detached                 Option[configdomain.Detached]
+		devRemote                Option[gitdomain.Remote]
+		displayTypes             Option[configdomain.DisplayTypes]
+		featureRegex             Option[configdomain.FeatureRegex]
+		forgeType                Option[forgedomain.ForgeType]
+		githubConnectorType      Option[forgedomain.GitHubConnectorType]
+		gitlabConnectorType      Option[forgedomain.GitLabConnectorType]
+		hostingOriginHostname    Option[configdomain.HostingOriginHostname]
+		mainBranch               Option[gitdomain.LocalBranchName]
+		newBranchType            Option[configdomain.NewBranchType]
+		observedRegex            Option[configdomain.ObservedRegex]
+		order                    Option[configdomain.Order]
+		perennialBranches        gitdomain.LocalBranchNames
+		perennialRegex           Option[configdomain.PerennialRegex]
+		proposalsShowLineage     Option[forgedomain.ProposalsShowLineage]
+		pushBranches             Option[configdomain.PushBranches]
+		pushHook                 Option[configdomain.PushHook]
+		shareNewBranches         Option[configdomain.ShareNewBranches]
+		shipDeleteTrackingBranch Option[configdomain.ShipDeleteTrackingBranch]
+		shipStrategy             Option[configdomain.ShipStrategy]
+		stash                    Option[configdomain.Stash]
+		syncFeatureStrategy      Option[configdomain.SyncFeatureStrategy]
+		syncPerennialStrategy    Option[configdomain.SyncPerennialStrategy]
+		syncPrototypeStrategy    Option[configdomain.SyncPrototypeStrategy]
+		syncTags                 Option[configdomain.SyncTags]
+		syncUpstream             Option[configdomain.SyncUpstream]
+		unknownBranchType        Option[configdomain.UnknownBranchType]
+		// keep-sorted end
+	)
 	var err error
 	// load legacy definitions first, so that the proper definitions loaded later override them
 	if data.CreatePrototypeBranches != nil {
@@ -109,16 +113,16 @@ func Validate(data Data, finalMessages stringslice.Collector) (configdomain.Part
 		}
 		perennialBranches = gitdomain.NewLocalBranchNames(data.Branches.Perennials...)
 		if data.Branches.PerennialRegex != nil {
-			perennialRegex, err = configdomain.ParsePerennialRegex(*data.Branches.PerennialRegex)
+			perennialRegex, err = configdomain.ParsePerennialRegex(*data.Branches.PerennialRegex, messages.ConfigFile)
 			ec.Check(err)
 		}
 		if data.Branches.DefaultType != nil {
-			branchType, err := configdomain.ParseBranchType(*data.Branches.DefaultType)
+			branchType, err := configdomain.ParseBranchType(*data.Branches.DefaultType, messages.ConfigFile)
 			ec.Check(err)
 			unknownBranchType = configdomain.UnknownBranchTypeOpt(branchType)
 		}
 		if data.Branches.DisplayTypes != nil {
-			displayTypes, err = configdomain.ParseDisplayTypes(*data.Branches.DisplayTypes, "config file")
+			displayTypes, err = configdomain.ParseDisplayTypes(*data.Branches.DisplayTypes, messages.ConfigFile)
 			ec.Check(err)
 		}
 		if data.Branches.FeatureRegex != nil {
@@ -143,18 +147,22 @@ func Validate(data Data, finalMessages stringslice.Collector) (configdomain.Part
 			}
 		}
 		if data.Branches.Order != nil {
-			order, err = configdomain.ParseOrder(*data.Branches.Order, "config file")
+			order, err = configdomain.ParseOrder(*data.Branches.Order, messages.ConfigFile)
 			ec.Check(err)
 		}
 		if data.Branches.UnknownType != nil {
-			branchType, err := configdomain.ParseBranchType(*data.Branches.UnknownType)
+			branchType, err := configdomain.ParseBranchType(*data.Branches.UnknownType, messages.ConfigFile)
 			ec.Check(err)
 			unknownBranchType = configdomain.UnknownBranchTypeOpt(branchType)
 		}
 	}
 	if data.Create != nil {
+		if data.Create.BranchPrefix != nil {
+			branchPrefix, err = configdomain.ParseBranchPrefix(*data.Create.BranchPrefix, messages.ConfigFile)
+			ec.Check(err)
+		}
 		if data.Create.NewBranchType != nil {
-			branchType, err := configdomain.ParseBranchType(*data.Create.NewBranchType)
+			branchType, err := configdomain.ParseBranchType(*data.Create.NewBranchType, messages.ConfigFile)
 			ec.Check(err)
 			newBranchType = configdomain.NewBranchTypeOpt(branchType)
 		}
@@ -163,7 +171,7 @@ func Validate(data Data, finalMessages stringslice.Collector) (configdomain.Part
 			finalMessages.Add(messages.PushNewBranchesDeprecation)
 		}
 		if data.Create.ShareNewBranches != nil {
-			shareNewBranches, err = configdomain.ParseShareNewBranches(*data.Create.ShareNewBranches, configdomain.KeyShareNewBranches)
+			shareNewBranches, err = configdomain.ParseShareNewBranches(*data.Create.ShareNewBranches, configdomain.KeyShareNewBranches.String())
 			ec.Check(err)
 		}
 		if data.Create.Stash != nil {
@@ -171,23 +179,27 @@ func Validate(data Data, finalMessages stringslice.Collector) (configdomain.Part
 		}
 	}
 	if data.Hosting != nil {
+		if data.Hosting.Browser != nil {
+			browser, err = configdomain.ParseBrowser(*data.Hosting.Browser, messages.ConfigFile)
+			ec.Check(err)
+		}
 		if data.Hosting.Platform != nil {
-			forgeType, err = forgedomain.ParseForgeType(*data.Hosting.Platform)
+			forgeType, err = forgedomain.ParseForgeType(*data.Hosting.Platform, messages.ConfigFile)
 			ec.Check(err)
 		}
 		if data.Hosting.DevRemote != nil {
 			devRemote = gitdomain.NewRemote(*data.Hosting.DevRemote)
 		}
 		if data.Hosting.ForgeType != nil {
-			forgeType, err = forgedomain.ParseForgeType(*data.Hosting.ForgeType)
+			forgeType, err = forgedomain.ParseForgeType(*data.Hosting.ForgeType, messages.ConfigFile)
 			ec.Check(err)
 		}
 		if data.Hosting.GitHubConnectorType != nil {
-			githubConnectorType, err = forgedomain.ParseGitHubConnectorType(*data.Hosting.GitHubConnectorType)
+			githubConnectorType, err = forgedomain.ParseGitHubConnectorType(*data.Hosting.GitHubConnectorType, messages.ConfigFile)
 			ec.Check(err)
 		}
 		if data.Hosting.GitLabConnectorType != nil {
-			gitlabConnectorType, err = forgedomain.ParseGitLabConnectorType(*data.Hosting.GitLabConnectorType)
+			gitlabConnectorType, err = forgedomain.ParseGitLabConnectorType(*data.Hosting.GitLabConnectorType, messages.ConfigFile)
 			ec.Check(err)
 		}
 		if data.Hosting.OriginHostname != nil {
@@ -196,7 +208,7 @@ func Validate(data Data, finalMessages stringslice.Collector) (configdomain.Part
 	}
 	if data.Propose != nil {
 		if data.Propose.Lineage != nil {
-			proposalsShowLineage, err = forgedomain.ParseProposalsShowLineage(*data.Propose.Lineage)
+			proposalsShowLineage, err = forgedomain.ParseProposalsShowLineage(*data.Propose.Lineage, messages.ConfigFile)
 			ec.Check(err)
 		}
 	}
@@ -210,15 +222,15 @@ func Validate(data Data, finalMessages stringslice.Collector) (configdomain.Part
 	}
 	if data.SyncStrategy != nil {
 		if data.SyncStrategy.FeatureBranches != nil {
-			syncFeatureStrategy, err = configdomain.ParseSyncFeatureStrategy(*data.SyncStrategy.FeatureBranches)
+			syncFeatureStrategy, err = configdomain.ParseSyncFeatureStrategy(*data.SyncStrategy.FeatureBranches, messages.ConfigFile)
 			ec.Check(err)
 		}
 		if data.SyncStrategy.PerennialBranches != nil {
-			syncPerennialStrategy, err = configdomain.ParseSyncPerennialStrategy(*data.SyncStrategy.PerennialBranches)
+			syncPerennialStrategy, err = configdomain.ParseSyncPerennialStrategy(*data.SyncStrategy.PerennialBranches, messages.ConfigFile)
 			ec.Check(err)
 		}
 		if data.SyncStrategy.PrototypeBranches != nil {
-			syncPrototypeStrategy, err = configdomain.ParseSyncPrototypeStrategy(*data.SyncStrategy.PrototypeBranches)
+			syncPrototypeStrategy, err = configdomain.ParseSyncPrototypeStrategy(*data.SyncStrategy.PrototypeBranches, messages.ConfigFile)
 			ec.Check(err)
 		}
 	}
@@ -233,15 +245,15 @@ func Validate(data Data, finalMessages stringslice.Collector) (configdomain.Part
 			detached = Some(configdomain.Detached(*data.Sync.Detached))
 		}
 		if data.Sync.FeatureStrategy != nil {
-			syncFeatureStrategy, err = configdomain.ParseSyncFeatureStrategy(*data.Sync.FeatureStrategy)
+			syncFeatureStrategy, err = configdomain.ParseSyncFeatureStrategy(*data.Sync.FeatureStrategy, messages.ConfigFile)
 			ec.Check(err)
 		}
 		if data.Sync.PerennialStrategy != nil {
-			syncPerennialStrategy, err = configdomain.ParseSyncPerennialStrategy(*data.Sync.PerennialStrategy)
+			syncPerennialStrategy, err = configdomain.ParseSyncPerennialStrategy(*data.Sync.PerennialStrategy, messages.ConfigFile)
 			ec.Check(err)
 		}
 		if data.Sync.PrototypeStrategy != nil {
-			syncPrototypeStrategy, err = configdomain.ParseSyncPrototypeStrategy(*data.Sync.PrototypeStrategy)
+			syncPrototypeStrategy, err = configdomain.ParseSyncPrototypeStrategy(*data.Sync.PrototypeStrategy, messages.ConfigFile)
 			ec.Check(err)
 		}
 		if data.Sync.PushBranches != nil {
@@ -262,7 +274,9 @@ func Validate(data Data, finalMessages stringslice.Collector) (configdomain.Part
 		AutoSync:                 autoSync,
 		BitbucketAppPassword:     None[forgedomain.BitbucketAppPassword](),
 		BitbucketUsername:        None[forgedomain.BitbucketUsername](),
+		BranchPrefix:             branchPrefix,
 		BranchTypeOverrides:      configdomain.BranchTypeOverrides{},
+		Browser:                  browser,
 		ForgejoToken:             None[forgedomain.ForgejoToken](),
 		ContributionRegex:        contributionRegex,
 		Detached:                 detached,
diff --git a/internal/config/configfile/save.go b/internal/config/configfile/save.go
index d2b646738..1f10be923 100644
--- a/internal/config/configfile/save.go
+++ b/internal/config/configfile/save.go
@@ -1,6 +1,7 @@
 package configfile
 
 import (
+	"cmp"
 	"fmt"
 	"os"
 	"strings"
@@ -20,16 +21,47 @@ func RenderTOML(data configdomain.PartialConfig) string {
 	result := strings.Builder{}
 	result.WriteString("# See https://www.git-town.com/configuration-file for details\n")
 
-	main, hasMain := data.MainBranch.Get()
+	// keep-sorted start
+	contributionRegex, hasContributionRegex := data.ContributionRegex.Get()
 	displayTypes, hasDisplayTypes := data.DisplayTypes.Get()
+	featureRegex, hasFeatureRegex := data.FeatureRegex.Get()
 	hasPerennialBranches := len(data.PerennialBranches) > 0
+	main, hasMain := data.MainBranch.Get()
+	observedRegex, hasObservedRegex := data.ObservedRegex.Get()
 	order, hasOrder := data.Order.Get()
 	perennialRegex, hasPerennialRegex := data.PerennialRegex.Get()
-	if hasMain || hasPerennialBranches || hasPerennialRegex {
+	unknownBranchType, hasUnknownBranchType := data.UnknownBranchType.Get()
+	// keep-sorted end
+	if cmp.Or(
+		// keep-sorted start
+		hasContributionRegex,
+		hasDisplayTypes,
+		hasFeatureRegex,
+		hasMain,
+		hasObservedRegex,
+		hasOrder,
+		hasPerennialBranches,
+		hasPerennialRegex,
+		hasUnknownBranchType,
+		// keep-sorted end
+	) {
 		result.WriteString("\n[branches]\n")
+		// keep-sorted start block=yes
+		if hasContributionRegex {
+			result.WriteString(fmt.Sprintf("contribution-regex = %q\n", contributionRegex))
+		}
+		if hasDisplayTypes {
+			result.WriteString(fmt.Sprintf("display-types = %q\n", displayTypes.Serialize(" ")))
+		}
+		if hasFeatureRegex {
+			result.WriteString(fmt.Sprintf("feature-regex = %q\n", featureRegex))
+		}
 		if hasMain {
 			result.WriteString(fmt.Sprintf("main = %q\n", main))
 		}
+		if hasObservedRegex {
+			result.WriteString(fmt.Sprintf("observed-regex = %q\n", observedRegex))
+		}
 		if hasOrder {
 			result.WriteString(fmt.Sprintf("order = %q\n", order))
 		}
@@ -39,16 +71,22 @@ func RenderTOML(data configdomain.PartialConfig) string {
 		if hasPerennialRegex {
 			result.WriteString(fmt.Sprintf("perennial-regex = %q\n", perennialRegex))
 		}
-		if hasDisplayTypes {
-			result.WriteString(fmt.Sprintf("display-types = %q\n", displayTypes.Serialize(" ")))
+		if hasUnknownBranchType {
+			result.WriteString(fmt.Sprintf("unknown-branch-type = %q\n", unknownBranchType))
 		}
+		// keep-sorted end
 	}
 
+	branchPrefix, hasBranchPrefix := data.BranchPrefix.Get()
 	newBranchType, hasNewBranchType := data.NewBranchType.Get()
 	shareNewBranches, hasShareNewBranches := data.ShareNewBranches.Get()
 	stash, hasStash := data.Stash.Get()
-	if hasNewBranchType || hasShareNewBranches {
+	if cmp.Or(hasBranchPrefix, hasNewBranchType, hasShareNewBranches, hasStash) {
 		result.WriteString("\n[create]\n")
+		// keep-sorted start block=yes
+		if hasBranchPrefix {
+			result.WriteString(fmt.Sprintf("branch-prefix = %q\n", branchPrefix))
+		}
 		if hasNewBranchType {
 			result.WriteString(fmt.Sprintf("new-branch-type = %q\n", newBranchType))
 		}
@@ -58,22 +96,48 @@ func RenderTOML(data configdomain.PartialConfig) string {
 		if hasStash {
 			result.WriteString(fmt.Sprintf("stash = %s\n", stash))
 		}
+		// keep-sorted end
 	}
 
+	// keep-sorted start
+	browser, hasBrowser := data.Browser.Get()
 	devRemote, hasDevRemote := data.DevRemote.Get()
 	forgeType, hasForgeType := data.ForgeType.Get()
+	githubConnectorType, hasGitHubConnectorType := data.GitHubConnectorType.Get()
+	gitlabConnectorType, hasGitLabConnectorType := data.GitLabConnectorType.Get()
 	originHostName, hasOriginHostName := data.HostingOriginHostname.Get()
-	if hasDevRemote || hasForgeType || hasOriginHostName {
+	// keep-sorted end
+	if cmp.Or(
+		// keep-sorted start
+		hasBrowser,
+		hasDevRemote,
+		hasForgeType,
+		hasGitHubConnectorType,
+		hasGitLabConnectorType,
+		hasOriginHostName,
+		// keep-sorted end
+	) {
 		result.WriteString("\n[hosting]\n")
+		// keep-sorted start block=yes
+		if hasBrowser {
+			result.WriteString(fmt.Sprintf("browser = %q\n", browser))
+		}
 		if hasDevRemote {
 			result.WriteString(fmt.Sprintf("dev-remote = %q\n", devRemote))
 		}
 		if hasForgeType {
 			result.WriteString(fmt.Sprintf("forge-type = %q\n", forgeType))
 		}
+		if hasGitHubConnectorType {
+			result.WriteString(fmt.Sprintf("github-connector-type = %q\n", githubConnectorType))
+		}
+		if hasGitLabConnectorType {
+			result.WriteString(fmt.Sprintf("gitlab-connector-type = %q\n", gitlabConnectorType))
+		}
 		if hasOriginHostName {
 			result.WriteString(fmt.Sprintf("origin-hostname = %q\n", originHostName))
 		}
+		// keep-sorted end
 	}
 
 	proposalShowLineage, hasProposalShowLineage := data.ProposalsShowLineage.Get()
@@ -84,7 +148,7 @@ func RenderTOML(data configdomain.PartialConfig) string {
 
 	deleteTrackingBranch, hasDeleteTrackingBranch := data.ShipDeleteTrackingBranch.Get()
 	shipStrategy, hasShipStrategy := data.ShipStrategy.Get()
-	if hasDeleteTrackingBranch || hasShipStrategy {
+	if cmp.Or(hasDeleteTrackingBranch, hasShipStrategy) {
 		result.WriteString("\n[ship]\n")
 		if hasDeleteTrackingBranch {
 			result.WriteString(fmt.Sprintf("delete-tracking-branch = %t\n", deleteTrackingBranch))
@@ -94,30 +158,50 @@ func RenderTOML(data configdomain.PartialConfig) string {
 		}
 	}
 
+	// keep-sorted start
 	autoResolve, hasAutoResolve := data.AutoResolve.Get()
 	autoSync, hasAutoSync := data.AutoSync.Get()
-	syncFeatureStrategy, hasSyncFeatureStrategy := data.SyncFeatureStrategy.Get()
-	syncPerennialStrategy, hasSyncPerennialStrategy := data.SyncPerennialStrategy.Get()
-	syncPrototypeStrategy, hasSyncPrototypeStrategy := data.SyncPrototypeStrategy.Get()
+	detached, hasDetached := data.Detached.Get()
 	pushBranches, hasPushBranches := data.PushBranches.Get()
 	pushHook, hasPushHook := data.PushHook.Get()
-	syncTags, hasSyncTags := data.SyncTags.Get()
-	syncUpstream, hasSyncUpstream := data.SyncUpstream.Get()
-	if hasAutoResolve || hasSyncFeatureStrategy || hasSyncPerennialStrategy || hasSyncPrototypeStrategy || hasPushHook || hasSyncTags || hasSyncUpstream {
+	syncFeatureStrategy, hasFeatureStrategy := data.SyncFeatureStrategy.Get()
+	syncPerennialStrategy, hasPerennialStrategy := data.SyncPerennialStrategy.Get()
+	syncPrototypeStrategy, hasPrototypeStrategy := data.SyncPrototypeStrategy.Get()
+	syncTags, hasTags := data.SyncTags.Get()
+	syncUpstream, hasUpstream := data.SyncUpstream.Get()
+	// keep-sorted end
+	if cmp.Or(
+		// keep-sorted start
+		hasAutoResolve,
+		hasAutoSync,
+		hasDetached,
+		hasFeatureStrategy,
+		hasPerennialStrategy,
+		hasPrototypeStrategy,
+		hasPushBranches,
+		hasPushHook,
+		hasTags,
+		hasUpstream,
+		// keep-sorted end
+	) {
 		result.WriteString("\n[sync]\n")
+		// keep-sorted start block=yes
 		if hasAutoResolve {
 			result.WriteString(fmt.Sprintf("auto-resolve = %t\n", autoResolve))
 		}
 		if hasAutoSync {
 			result.WriteString(fmt.Sprintf("auto-sync = %t\n", autoSync))
 		}
-		if hasSyncFeatureStrategy {
+		if hasDetached {
+			result.WriteString(fmt.Sprintf("detached = %t\n", detached))
+		}
+		if hasFeatureStrategy {
 			result.WriteString(fmt.Sprintf("feature-strategy = %q\n", syncFeatureStrategy))
 		}
-		if hasSyncPerennialStrategy {
+		if hasPerennialStrategy {
 			result.WriteString(fmt.Sprintf("perennial-strategy = %q\n", syncPerennialStrategy))
 		}
-		if hasSyncPrototypeStrategy {
+		if hasPrototypeStrategy {
 			result.WriteString(fmt.Sprintf("prototype-strategy = %q\n", syncPrototypeStrategy))
 		}
 		if hasPushBranches {
@@ -126,12 +210,13 @@ func RenderTOML(data configdomain.PartialConfig) string {
 		if hasPushHook {
 			result.WriteString(fmt.Sprintf("push-hook = %t\n", pushHook))
 		}
-		if hasSyncTags {
+		if hasTags {
 			result.WriteString(fmt.Sprintf("tags = %t\n", syncTags))
 		}
-		if hasSyncUpstream {
+		if hasUpstream {
 			result.WriteString(fmt.Sprintf("upstream = %t\n", syncUpstream))
 		}
+		// keep-sorted end
 	}
 	return result.String()
 }
diff --git a/internal/config/envconfig/load.go b/internal/config/envconfig/load.go
index 4727ca157..0b0b4423a 100644
--- a/internal/config/envconfig/load.go
+++ b/internal/config/envconfig/load.go
@@ -15,6 +15,8 @@ const (
 	autoSync                 = "GIT_TOWN_AUTO_SYNC"
 	bitbucketAppPassword     = "GIT_TOWN_BITBUCKET_APP_PASSWORD"
 	bitbucketUserName        = "GIT_TOWN_BITBUCKET_USERNAME"
+	branchPrefix             = "GIT_TOWN_BRANCH_PREFIX"
+	Browser                  = "BROWSER"
 	forgejoToken             = "GIT_TOWN_FORGEJO_TOKEN"
 	contributionRegex        = "GIT_TOWN_CONTRIBUTION_REGEX"
 	detached                 = "GIT_TOWN_DETACHED"
@@ -57,44 +59,48 @@ const (
 )
 
 func Load(env EnvVars) (configdomain.PartialConfig, error) {
-	autoResolve, errAutoResolve := gohacks.ParseBoolOpt[configdomain.AutoResolve](env.Get(autoResolve), autoResolve)
-	autoSync, errAutoSync := gohacks.ParseBoolOpt[configdomain.AutoSync](env.Get(autoSync), autoSync)
-	contributionRegex, errContribRegex := configdomain.ParseContributionRegex(env.Get(contributionRegex))
-	detached, errDetached := gohacks.ParseBoolOpt[configdomain.Detached](env.Get(detached), detached)
-	displayTypesOpt, errDisplayTypes := configdomain.ParseDisplayTypes(env.Get(displayTypes), displayTypes)
-	dryRun, errDryRun := gohacks.ParseBoolOpt[configdomain.DryRun](env.Get(dryRun), dryRun)
-	featureRegex, errFeatureRegex := configdomain.ParseFeatureRegex(env.Get(featureRegex))
-	forgeType, errForgeType := forgedomain.ParseForgeType(env.Get(forgeType))
+	autoResolve, errAutoResolve := load(env, autoResolve, gohacks.ParseBoolOpt[configdomain.AutoResolve])
+	autoSync, errAutoSync := load(env, autoSync, gohacks.ParseBoolOpt[configdomain.AutoSync])
+	branchPrefix, errBranchPrefix := load(env, branchPrefix, configdomain.ParseBranchPrefix)
+	browser, errBrowser := load(env, Browser, configdomain.ParseBrowser)
+	contributionRegex, errContribRegex := load(env, contributionRegex, configdomain.ParseContributionRegex)
+	detached, errDetached := load(env, detached, gohacks.ParseBoolOpt[configdomain.Detached])
+	displayTypesOpt, errDisplayTypes := load(env, displayTypes, configdomain.ParseDisplayTypes)
+	dryRun, errDryRun := load(env, dryRun, gohacks.ParseBoolOpt[configdomain.DryRun])
+	featureRegex, errFeatureRegex := load(env, featureRegex, configdomain.ParseFeatureRegex)
+	forgeType, errForgeType := load(env, forgeType, forgedomain.ParseForgeType)
 	gitAuthorEmailValue := NewOption(gitdomain.GitUserEmail(env.Get(gitAuthorEmail)))
 	gitCommitterEmailValue := NewOption(gitdomain.GitUserEmail(env.Get(gitCommitterEmail)))
 	gitUserEmail := gitAuthorEmailValue.Or(gitCommitterEmailValue)
 	gitAuthorNameValue := NewOption(gitdomain.GitUserName(env.Get(gitAuthorName)))
 	gitCommitterNameValue := NewOption(gitdomain.GitUserName(env.Get(gitCommitterName)))
 	gitUserName := gitAuthorNameValue.Or(gitCommitterNameValue)
-	githubConnectorType, errGitHubConnectorType := forgedomain.ParseGitHubConnectorType(env.Get(githubConnectorType))
-	gitlabConnectorType, errGitLabConnectorType := forgedomain.ParseGitLabConnectorType(env.Get(gitlabConnectorType))
-	newBranchType, errNewBranchType := configdomain.ParseBranchType(env.Get(newBranchType))
-	observedRegex, errObservedRegex := configdomain.ParseObservedRegex(env.Get(observedRegex))
+	githubConnectorType, errGitHubConnectorType := load(env, githubConnectorType, forgedomain.ParseGitHubConnectorType)
+	gitlabConnectorType, errGitLabConnectorType := load(env, gitlabConnectorType, forgedomain.ParseGitLabConnectorType)
+	newBranchType, errNewBranchType := load(env, newBranchType, configdomain.ParseBranchType)
+	observedRegex, errObservedRegex := load(env, observedRegex, configdomain.ParseObservedRegex)
 	order, errOrder := configdomain.ParseOrder(env.Get(order), order)
-	offline, errOffline := gohacks.ParseBoolOpt[configdomain.Offline](env.Get(offline), offline)
-	perennialRegex, errPerennialRegex := configdomain.ParsePerennialRegex(env.Get(perennialRegex))
-	proposalsShowLineage, errProposalsShowLineage := forgedomain.ParseProposalsShowLineage(env.Get(proposalsShowLineage))
-	pushBranches, errPushBranches := gohacks.ParseBoolOpt[configdomain.PushBranches](env.Get(pushBranches), pushBranches)
-	pushHook, errPushHook := gohacks.ParseBoolOpt[configdomain.PushHook](env.Get(pushHook), pushHook)
-	shareNewBranches, errShareNewBranches := configdomain.ParseShareNewBranches(env.Get(shareNewBranches), shareNewBranches)
-	shipDeleteTrackingBranch, errShipDeleteTrackingBranch := gohacks.ParseBoolOpt[configdomain.ShipDeleteTrackingBranch](env.Get(shipDeleteTrackingBranch), shipDeleteTrackingBranch)
-	shipStrategy, errShipStrategy := configdomain.ParseShipStrategy(env.Get(shipStrategy))
-	stash, errStash := gohacks.ParseBoolOpt[configdomain.Stash](env.Get(stash), stash)
-	syncFeatureStrategy, errSyncFeatureStrategy := configdomain.ParseSyncFeatureStrategy(env.Get(syncFeatureStrategy))
-	syncPerennialStrategy, errSyncPerennialStrategy := configdomain.ParseSyncPerennialStrategy(env.Get(syncPerennialStrategy))
-	syncPrototypeStrategy, errSyncPrototypeStrategy := configdomain.ParseSyncPrototypeStrategy(env.Get(syncPrototypeStrategy))
-	syncTags, errSyncTags := gohacks.ParseBoolOpt[configdomain.SyncTags](env.Get(syncTags), syncTags)
-	syncUpstream, errSyncUpstream := gohacks.ParseBoolOpt[configdomain.SyncUpstream](env.Get(syncUpstream), syncUpstream)
-	unknownBranchType, errUnknownBranchType := configdomain.ParseBranchType(env.Get(unknownBranchType))
-	verbose, errVerbose := gohacks.ParseBoolOpt[configdomain.Verbose](env.Get(verbose), verbose)
+	offline, errOffline := load(env, offline, gohacks.ParseBoolOpt[configdomain.Offline])
+	perennialRegex, errPerennialRegex := load(env, perennialRegex, configdomain.ParsePerennialRegex)
+	proposalsShowLineage, errProposalsShowLineage := load(env, proposalsShowLineage, forgedomain.ParseProposalsShowLineage)
+	pushBranches, errPushBranches := load(env, pushBranches, gohacks.ParseBoolOpt[configdomain.PushBranches])
+	pushHook, errPushHook := load(env, pushHook, gohacks.ParseBoolOpt[configdomain.PushHook])
+	shareNewBranches, errShareNewBranches := load(env, shareNewBranches, configdomain.ParseShareNewBranches)
+	shipDeleteTrackingBranch, errShipDeleteTrackingBranch := load(env, shipDeleteTrackingBranch, gohacks.ParseBoolOpt[configdomain.ShipDeleteTrackingBranch])
+	shipStrategy, errShipStrategy := load(env, shipStrategy, configdomain.ParseShipStrategy)
+	stash, errStash := load(env, stash, gohacks.ParseBoolOpt[configdomain.Stash])
+	syncFeatureStrategy, errSyncFeatureStrategy := load(env, syncFeatureStrategy, configdomain.ParseSyncFeatureStrategy)
+	syncPerennialStrategy, errSyncPerennialStrategy := load(env, syncPerennialStrategy, configdomain.ParseSyncPerennialStrategy)
+	syncPrototypeStrategy, errSyncPrototypeStrategy := load(env, syncPrototypeStrategy, configdomain.ParseSyncPrototypeStrategy)
+	syncTags, errSyncTags := load(env, syncTags, gohacks.ParseBoolOpt[configdomain.SyncTags])
+	syncUpstream, errSyncUpstream := load(env, syncUpstream, gohacks.ParseBoolOpt[configdomain.SyncUpstream])
+	unknownBranchType, errUnknownBranchType := load(env, unknownBranchType, configdomain.ParseBranchType)
+	verbose, errVerbose := load(env, verbose, gohacks.ParseBoolOpt[configdomain.Verbose])
 	err := cmp.Or(
 		errAutoResolve,
 		errAutoSync,
+		errBranchPrefix,
+		errBrowser,
 		errContribRegex,
 		errDetached,
 		errDisplayTypes,
@@ -129,7 +135,9 @@ func Load(env EnvVars) (configdomain.PartialConfig, error) {
 		AutoSync:                 autoSync,
 		BitbucketAppPassword:     forgedomain.ParseBitbucketAppPassword(env.Get(bitbucketAppPassword)),
 		BitbucketUsername:        forgedomain.ParseBitbucketUsername(env.Get(bitbucketUserName)),
+		BranchPrefix:             branchPrefix,
 		BranchTypeOverrides:      configdomain.BranchTypeOverrides{}, // not loaded from env vars
+		Browser:                  browser,
 		ForgejoToken:             forgedomain.ParseForgejoToken(env.Get(forgejoToken)),
 		ContributionRegex:        contributionRegex,
 		Detached:                 detached,
@@ -170,3 +178,7 @@ func Load(env EnvVars) (configdomain.PartialConfig, error) {
 		Verbose:                  verbose,
 	}, err
 }
+
+func load[T any](env EnvVars, varName string, parser func(string, string) (T, error)) (T, error) { //nolint:ireturn
+	return parser(env.Get(varName), varName)
+}
diff --git a/internal/config/gitconfig/high_level.go b/internal/config/gitconfig/high_level.go
index 25597ad9e..ec1719335 100644
--- a/internal/config/gitconfig/high_level.go
+++ b/internal/config/gitconfig/high_level.go
@@ -49,6 +49,10 @@ func RemoveBitbucketUsername(runner subshelldomain.Runner) error {
 	return RemoveConfigValue(runner, configdomain.ConfigScopeLocal, configdomain.KeyBitbucketUsername)
 }
 
+func RemoveBranchPrefix(runner subshelldomain.Runner) error {
+	return RemoveConfigValue(runner, configdomain.ConfigScopeLocal, configdomain.KeyBranchPrefix)
+}
+
 func RemoveBranchTypeOverride(runner subshelldomain.Runner, branch gitdomain.LocalBranchName) error {
 	key := configdomain.NewBranchTypeOverrideKeyForBranch(branch)
 	return RemoveConfigValue(runner, configdomain.ConfigScopeLocal, key.Key)
@@ -130,6 +134,10 @@ func RemovePerennialRegex(runner subshelldomain.Runner) error {
 	return RemoveConfigValue(runner, configdomain.ConfigScopeLocal, configdomain.KeyPerennialRegex)
 }
 
+func RemoveProposalsShowLineage(runner subshelldomain.Runner) error {
+	return RemoveConfigValue(runner, configdomain.ConfigScopeLocal, configdomain.KeyProposalsShowLineage)
+}
+
 func RemovePushBranches(runner subshelldomain.Runner) error {
 	return RemoveConfigValue(runner, configdomain.ConfigScopeLocal, configdomain.KeyPushBranches)
 }
@@ -194,6 +202,10 @@ func SetBitbucketUsername(runner subshelldomain.Runner, value forgedomain.Bitbuc
 	return SetConfigValue(runner, scope, configdomain.KeyBitbucketUsername, value.String())
 }
 
+func SetBranchPrefix(runner subshelldomain.Runner, value configdomain.BranchPrefix, scope configdomain.ConfigScope) error {
+	return SetConfigValue(runner, scope, configdomain.KeyBranchPrefix, value.String())
+}
+
 func SetBranchTypeOverride(runner subshelldomain.Runner, branchType configdomain.BranchType, branches ...gitdomain.LocalBranchName) error {
 	for _, branch := range branches {
 		if err := SetConfigValue(runner, configdomain.ConfigScopeLocal, configdomain.NewBranchTypeOverrideKeyForBranch(branch).Key, branchType.String()); err != nil {
@@ -283,6 +295,10 @@ func SetPerennialRegex(runner subshelldomain.Runner, value configdomain.Perennia
 	return SetConfigValue(runner, scope, configdomain.KeyPerennialRegex, value.String())
 }
 
+func SetProposalsShowLineage(runner subshelldomain.Runner, value forgedomain.ProposalsShowLineage, scope configdomain.ConfigScope) error {
+	return SetConfigValue(runner, scope, configdomain.KeyProposalsShowLineage, value.String())
+}
+
 func SetPushBranches(runner subshelldomain.Runner, value configdomain.PushBranches, scope configdomain.ConfigScope) error {
 	return SetConfigValue(runner, scope, configdomain.KeyPushBranches, strconv.FormatBool(bool(value)))
 }
diff --git a/internal/config/gitconfig/low_level.go b/internal/config/gitconfig/low_level.go
index b2c1409e2..e738c8bb8 100644
--- a/internal/config/gitconfig/low_level.go
+++ b/internal/config/gitconfig/low_level.go
@@ -27,7 +27,7 @@ func LoadSnapshot(backend subshelldomain.RunnerQuerier, scopeOpt Option[configdo
 	if err != nil || output == "" {
 		return snapshot, nil //nolint:nilerr  // Git returns an error if there is no global Git config, assume empty config in this case
 	}
-	for _, line := range strings.Split(output, "\x00") {
+	for line := range strings.SplitSeq(output, "\x00") {
 		if len(line) == 0 {
 			continue
 		}
@@ -57,7 +57,7 @@ func LoadSnapshot(backend subshelldomain.RunnerQuerier, scopeOpt Option[configdo
 			}
 			for branchList, branchType := range mapstools.SortedKeyValues(configdomain.ObsoleteBranchLists) {
 				if configKey == branchList {
-					for _, branch := range strings.Split(value, " ") {
+					for branch := range strings.SplitSeq(value, " ") {
 						branchTypeKey := configdomain.Key(configdomain.BranchSpecificKeyPrefix + branch + configdomain.BranchTypeSuffix)
 						snapshot[branchTypeKey] = branchType.String()
 						_ = SetConfigValue(backend, configdomain.ConfigScopeLocal, branchTypeKey, branchType.String())
diff --git a/internal/config/normal_config.go b/internal/config/normal_config.go
index ed55d2e9a..6b66020bd 100644
--- a/internal/config/normal_config.go
+++ b/internal/config/normal_config.go
@@ -34,7 +34,9 @@ type NormalConfig struct {
 	AutoSync                 configdomain.AutoSync
 	BitbucketAppPassword     Option[forgedomain.BitbucketAppPassword]
 	BitbucketUsername        Option[forgedomain.BitbucketUsername]
+	BranchPrefix             Option[configdomain.BranchPrefix]
 	BranchTypeOverrides      configdomain.BranchTypeOverrides
+	Browser                  Option[configdomain.Browser]
 	ContributionRegex        Option[configdomain.ContributionRegex]
 	Detached                 configdomain.Detached
 	DevRemote                gitdomain.Remote
@@ -91,7 +93,7 @@ func (self *NormalConfig) DevURL(querier subshelldomain.Querier) Option[giturl.P
 	return self.RemoteURL(querier, self.DevRemote)
 }
 
-// provides a new NormalConfig that contains data from the given PartialConfig,
+// OverwriteWith provides a new NormalConfig that contains data from the given PartialConfig,
 // backfilled with data from this NormalConfig where missing
 func (self *NormalConfig) OverwriteWith(other configdomain.PartialConfig) NormalConfig {
 	return NormalConfig{
@@ -100,7 +102,9 @@ func (self *NormalConfig) OverwriteWith(other configdomain.PartialConfig) Normal
 		AutoSync:                 other.AutoSync.GetOr(self.AutoSync),
 		BitbucketAppPassword:     other.BitbucketAppPassword.Or(self.BitbucketAppPassword),
 		BitbucketUsername:        other.BitbucketUsername.Or(self.BitbucketUsername),
+		BranchPrefix:             other.BranchPrefix.Or(self.BranchPrefix),
 		BranchTypeOverrides:      other.BranchTypeOverrides.Concat(self.BranchTypeOverrides),
+		Browser:                  other.Browser.Or(self.Browser),
 		ContributionRegex:        other.ContributionRegex.Or(self.ContributionRegex),
 		Detached:                 other.Detached.GetOr(self.Detached),
 		DevRemote:                other.DevRemote.GetOr(self.DevRemote),
@@ -249,7 +253,9 @@ func DefaultNormalConfig() NormalConfig {
 		AutoSync:             true,
 		BitbucketAppPassword: None[forgedomain.BitbucketAppPassword](),
 		BitbucketUsername:    None[forgedomain.BitbucketUsername](),
+		BranchPrefix:         None[configdomain.BranchPrefix](),
 		BranchTypeOverrides:  configdomain.BranchTypeOverrides{},
+		Browser:              None[configdomain.Browser](),
 		ContributionRegex:    None[configdomain.ContributionRegex](),
 		Detached:             false,
 		DevRemote:            gitdomain.RemoteOrigin,
@@ -301,7 +307,9 @@ func NewNormalConfigFromPartial(partial configdomain.PartialConfig, defaults Nor
 		AutoSync:                 partial.AutoSync.GetOr(defaults.AutoSync),
 		BitbucketAppPassword:     partial.BitbucketAppPassword,
 		BitbucketUsername:        partial.BitbucketUsername,
+		BranchPrefix:             partial.BranchPrefix,
 		BranchTypeOverrides:      partial.BranchTypeOverrides,
+		Browser:                  partial.Browser.Or(defaults.Browser),
 		ContributionRegex:        partial.ContributionRegex,
 		Detached:                 partial.Detached.GetOr(defaults.Detached),
 		DevRemote:                partial.DevRemote.GetOr(defaults.DevRemote),
diff --git a/internal/config/parse_snapshot.go b/internal/config/parse_snapshot.go
index 581262d5f..a9b9a1753 100644
--- a/internal/config/parse_snapshot.go
+++ b/internal/config/parse_snapshot.go
@@ -16,8 +16,8 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// provides the branch type overrides stored in the given Git metadata snapshot
-func NewBranchTypeOverridesInSnapshot(snapshot configdomain.SingleSnapshot, runner subshelldomain.Runner) (configdomain.BranchTypeOverrides, error) {
+// NewBranchTypeOverridesInSnapshot provides the branch type overrides stored in the given Git metadata snapshot.
+func NewBranchTypeOverridesInSnapshot(snapshot configdomain.SingleSnapshot, ignoreUnknown bool, runner subshelldomain.Runner) (configdomain.BranchTypeOverrides, error) {
 	result := configdomain.BranchTypeOverrides{}
 	for key, value := range snapshot { // okay to iterate the map in random order because we assign to a new map
 		key, isBranchTypeKey := configdomain.ParseBranchTypeOverrideKey(key).Get()
@@ -38,9 +38,13 @@ func NewBranchTypeOverridesInSnapshot(snapshot configdomain.SingleSnapshot, runn
 			_ = gitconfig.RemoveConfigValue(runner, configdomain.ConfigScopeLocal, key.Key)
 			continue
 		}
-		branchTypeOpt, err := configdomain.ParseBranchType(value)
+		branchTypeOpt, err := configdomain.ParseBranchType(value, key.String())
 		if err != nil {
-			return result, err
+			if ignoreUnknown {
+				fmt.Printf("Ignoring unknown branch type override for %q: %s\n", branch, value)
+			} else {
+				return result, err
+			}
 		}
 		if branchType, hasBranchType := branchTypeOpt.Get(); hasBranchType {
 			result[branch] = branchType
@@ -76,43 +80,46 @@ func NewLineageFromSnapshot(snapshot configdomain.SingleSnapshot, updateOutdated
 	return result, nil
 }
 
-func NewPartialConfigFromSnapshot(snapshot configdomain.SingleSnapshot, updateOutdated bool, runner subshelldomain.Runner) (configdomain.PartialConfig, error) {
-	aliases := snapshot.Aliases()
-	autoResolve, errAutoResolve := gohacks.ParseBoolOpt[configdomain.AutoResolve](snapshot[configdomain.KeyAutoResolve], configdomain.KeyAutoResolve.String())
-	autoSync, errAutoSync := gohacks.ParseBoolOpt[configdomain.AutoSync](snapshot[configdomain.KeyAutoSync], configdomain.KeyAutoSync.String())
-	branchTypeOverrides, errBranchTypeOverride := NewBranchTypeOverridesInSnapshot(snapshot, runner)
-	contributionRegex, errContributionRegex := configdomain.ParseContributionRegex(snapshot[configdomain.KeyContributionRegex])
-	detached, errDetached := gohacks.ParseBoolOpt[configdomain.Detached](snapshot[configdomain.KeyDetached], configdomain.KeyDetached.String())
-	displayTypes, errDisplayTypes := configdomain.ParseDisplayTypes(snapshot[configdomain.KeyDisplayTypes], configdomain.KeyDisplayTypes.String())
-	featureRegex, errFeatureRegex := configdomain.ParseFeatureRegex(snapshot[configdomain.KeyFeatureRegex])
-	forgeType, errForgeType := forgedomain.ParseForgeType(snapshot[configdomain.KeyForgeType])
-	githubConnectorType, errGitHubConnectorType := forgedomain.ParseGitHubConnectorType(snapshot[configdomain.KeyGitHubConnectorType])
-	gitlabConnectorType, errGitLabConnectorType := forgedomain.ParseGitLabConnectorType(snapshot[configdomain.KeyGitLabConnectorType])
+func NewPartialConfigFromSnapshot(snapshot configdomain.SingleSnapshot, updateOutdated bool, ignoreUnknown bool, runner subshelldomain.Runner) (configdomain.PartialConfig, error) {
+	autoResolve, errAutoResolve := load(snapshot, configdomain.KeyAutoResolve, gohacks.ParseBoolOpt[configdomain.AutoResolve], ignoreUnknown)
+	autoSync, errAutoSync := load(snapshot, configdomain.KeyAutoSync, gohacks.ParseBoolOpt[configdomain.AutoSync], ignoreUnknown)
+	branchPrefix, errBranchPrefix := load(snapshot, configdomain.KeyBranchPrefix, configdomain.ParseBranchPrefix, ignoreUnknown)
+	branchTypeOverrides, errBranchTypeOverride := NewBranchTypeOverridesInSnapshot(snapshot, ignoreUnknown, runner)
+	browser, errBrowser := load(snapshot, configdomain.KeyBrowser, configdomain.ParseBrowser, ignoreUnknown)
+	contributionRegex, errContributionRegex := load(snapshot, configdomain.KeyContributionRegex, configdomain.ParseContributionRegex, ignoreUnknown)
+	detached, errDetached := load(snapshot, configdomain.KeyDetached, gohacks.ParseBoolOpt[configdomain.Detached], ignoreUnknown)
+	displayTypes, errDisplayTypes := load(snapshot, configdomain.KeyDisplayTypes, configdomain.ParseDisplayTypes, ignoreUnknown)
+	featureRegex, errFeatureRegex := load(snapshot, configdomain.KeyFeatureRegex, configdomain.ParseFeatureRegex, ignoreUnknown)
+	forgeType, errForgeType := load(snapshot, configdomain.KeyForgeType, forgedomain.ParseForgeType, ignoreUnknown)
+	githubConnectorType, errGitHubConnectorType := load(snapshot, configdomain.KeyGitHubConnectorType, forgedomain.ParseGitHubConnectorType, ignoreUnknown)
+	gitlabConnectorType, errGitLabConnectorType := load(snapshot, configdomain.KeyGitLabConnectorType, forgedomain.ParseGitLabConnectorType, ignoreUnknown)
 	lineage, errLineage := NewLineageFromSnapshot(snapshot, updateOutdated, runner)
-	newBranchTypeValue, errNewBranchType := configdomain.ParseBranchType(snapshot[configdomain.KeyNewBranchType])
+	newBranchTypeValue, errNewBranchType := load(snapshot, configdomain.KeyNewBranchType, configdomain.ParseBranchType, ignoreUnknown)
 	newBranchType := configdomain.NewBranchTypeOpt(newBranchTypeValue)
-	observedRegex, errObservedRegex := configdomain.ParseObservedRegex(snapshot[configdomain.KeyObservedRegex])
-	order, errOrder := configdomain.ParseOrder(snapshot[configdomain.KeyOrder], configdomain.KeyOrder)
-	offline, errOffline := gohacks.ParseBoolOpt[configdomain.Offline](snapshot[configdomain.KeyOffline], configdomain.KeyOffline.String())
-	perennialRegex, errPerennialRegex := configdomain.ParsePerennialRegex(snapshot[configdomain.KeyPerennialRegex])
-	proposalsShowLineage, errProposalsShowLineage := forgedomain.ParseProposalsShowLineage(snapshot[configdomain.KeyProposalsShowLineage])
-	pushBranches, errPushBranches := gohacks.ParseBoolOpt[configdomain.PushBranches](snapshot[configdomain.KeyPushBranches], configdomain.KeyPushBranches.String())
-	pushHook, errPushHook := gohacks.ParseBoolOpt[configdomain.PushHook](snapshot[configdomain.KeyPushHook], configdomain.KeyPushHook.String())
-	shareNewBranches, errShareNewBranches := configdomain.ParseShareNewBranches(snapshot[configdomain.KeyShareNewBranches], configdomain.KeyShareNewBranches)
-	shipDeleteTrackingBranch, errShipDeleteTrackingBranch := gohacks.ParseBoolOpt[configdomain.ShipDeleteTrackingBranch](snapshot[configdomain.KeyShipDeleteTrackingBranch], configdomain.KeyShipDeleteTrackingBranch.String())
-	shipStrategy, errShipStrategy := configdomain.ParseShipStrategy(snapshot[configdomain.KeyShipStrategy])
-	stash, errStash := gohacks.ParseBoolOpt[configdomain.Stash](snapshot[configdomain.KeyStash], configdomain.KeyStash.String())
-	syncFeatureStrategy, errSyncFeatureStrategy := configdomain.ParseSyncFeatureStrategy(snapshot[configdomain.KeySyncFeatureStrategy])
-	syncPerennialStrategy, errSyncPerennialStrategy := configdomain.ParseSyncPerennialStrategy(snapshot[configdomain.KeySyncPerennialStrategy])
-	syncPrototypeStrategy, errSyncPrototypeStrategy := configdomain.ParseSyncPrototypeStrategy(snapshot[configdomain.KeySyncPrototypeStrategy])
-	syncTags, errSyncTags := gohacks.ParseBoolOpt[configdomain.SyncTags](snapshot[configdomain.KeySyncTags], configdomain.KeySyncTags.String())
-	syncUpstream, errSyncUpstream := gohacks.ParseBoolOpt[configdomain.SyncUpstream](snapshot[configdomain.KeySyncUpstream], configdomain.KeySyncUpstream.String())
-	unknownBranchTypeValue, errUnknownBranchType := configdomain.ParseBranchType(snapshot[configdomain.KeyUnknownBranchType])
+	observedRegex, errObservedRegex := load(snapshot, configdomain.KeyObservedRegex, configdomain.ParseObservedRegex, ignoreUnknown)
+	order, errOrder := load(snapshot, configdomain.KeyOrder, configdomain.ParseOrder, ignoreUnknown)
+	offline, errOffline := load(snapshot, configdomain.KeyOffline, gohacks.ParseBoolOpt[configdomain.Offline], ignoreUnknown)
+	perennialRegex, errPerennialRegex := load(snapshot, configdomain.KeyPerennialRegex, configdomain.ParsePerennialRegex, ignoreUnknown)
+	proposalsShowLineage, errProposalsShowLineage := load(snapshot, configdomain.KeyProposalsShowLineage, forgedomain.ParseProposalsShowLineage, ignoreUnknown)
+	pushBranches, errPushBranches := load(snapshot, configdomain.KeyPushBranches, gohacks.ParseBoolOpt[configdomain.PushBranches], ignoreUnknown)
+	pushHook, errPushHook := load(snapshot, configdomain.KeyPushHook, gohacks.ParseBoolOpt[configdomain.PushHook], ignoreUnknown)
+	shareNewBranches, errShareNewBranches := load(snapshot, configdomain.KeyShareNewBranches, configdomain.ParseShareNewBranches, ignoreUnknown)
+	shipDeleteTrackingBranch, errShipDeleteTrackingBranch := load(snapshot, configdomain.KeyShipDeleteTrackingBranch, gohacks.ParseBoolOpt[configdomain.ShipDeleteTrackingBranch], ignoreUnknown)
+	shipStrategy, errShipStrategy := load(snapshot, configdomain.KeyShipStrategy, configdomain.ParseShipStrategy, ignoreUnknown)
+	stash, errStash := load(snapshot, configdomain.KeyStash, gohacks.ParseBoolOpt[configdomain.Stash], ignoreUnknown)
+	syncFeatureStrategy, errSyncFeatureStrategy := load(snapshot, configdomain.KeySyncFeatureStrategy, configdomain.ParseSyncFeatureStrategy, ignoreUnknown)
+	syncPerennialStrategy, errSyncPerennialStrategy := load(snapshot, configdomain.KeySyncPerennialStrategy, configdomain.ParseSyncPerennialStrategy, ignoreUnknown)
+	syncPrototypeStrategy, errSyncPrototypeStrategy := load(snapshot, configdomain.KeySyncPrototypeStrategy, configdomain.ParseSyncPrototypeStrategy, ignoreUnknown)
+	syncTags, errSyncTags := load(snapshot, configdomain.KeySyncTags, gohacks.ParseBoolOpt[configdomain.SyncTags], ignoreUnknown)
+	syncUpstream, errSyncUpstream := load(snapshot, configdomain.KeySyncUpstream, gohacks.ParseBoolOpt[configdomain.SyncUpstream], ignoreUnknown)
+	unknownBranchTypeValue, errUnknownBranchType := load(snapshot, configdomain.KeyUnknownBranchType, configdomain.ParseBranchType, ignoreUnknown)
 	unknownBranchType := configdomain.UnknownBranchTypeOpt(unknownBranchTypeValue)
 	err := cmp.Or(
 		errAutoResolve,
 		errAutoSync,
+		errBranchPrefix,
 		errBranchTypeOverride,
+		errBrowser,
 		errContributionRegex,
 		errDetached,
 		errDisplayTypes,
@@ -141,12 +148,14 @@ func NewPartialConfigFromSnapshot(snapshot configdomain.SingleSnapshot, updateOu
 		errUnknownBranchType,
 	)
 	return configdomain.PartialConfig{
-		Aliases:                  aliases,
+		Aliases:                  snapshot.Aliases(),
 		AutoResolve:              autoResolve,
 		AutoSync:                 autoSync,
 		BitbucketAppPassword:     forgedomain.ParseBitbucketAppPassword(snapshot[configdomain.KeyBitbucketAppPassword]),
 		BitbucketUsername:        forgedomain.ParseBitbucketUsername(snapshot[configdomain.KeyBitbucketUsername]),
+		BranchPrefix:             branchPrefix,
 		BranchTypeOverrides:      branchTypeOverrides,
+		Browser:                  browser,
 		ForgejoToken:             forgedomain.ParseForgejoToken(snapshot[configdomain.KeyForgejoToken]),
 		ContributionRegex:        contributionRegex,
 		Detached:                 detached,
@@ -187,3 +196,22 @@ func NewPartialConfigFromSnapshot(snapshot configdomain.SingleSnapshot, updateOu
 		Verbose:                  None[configdomain.Verbose](),
 	}, err
 }
+
+func load[T any](snapshot configdomain.SingleSnapshot, key configdomain.Key, parseFunc func(string, string) (T, error), ignoreUnknown bool) (T, error) { //nolint:ireturn
+	valueStr, has := snapshot[key]
+	if !has {
+		var zero T
+		return zero, nil
+	}
+	value, err := parseFunc(valueStr, key.String())
+	if err != nil {
+		if ignoreUnknown {
+			fmt.Printf("Ignoring invalid value for %q: %q\n", key, valueStr)
+			var zero T
+			return zero, nil
+		}
+		var zero T
+		return zero, err
+	}
+	return value, nil
+}
diff --git a/internal/config/unvalidated_config.go b/internal/config/unvalidated_config.go
index a7cbf1c72..923206b53 100644
--- a/internal/config/unvalidated_config.go
+++ b/internal/config/unvalidated_config.go
@@ -46,7 +46,7 @@ func (self *UnvalidatedConfig) Reload(backend subshelldomain.RunnerQuerier) conf
 	globalSnapshot, _ := gitconfig.LoadSnapshot(backend, Some(configdomain.ConfigScopeGlobal), configdomain.UpdateOutdatedNo) // we ignore the Git cache here because reloading a config in the middle of a Git Town command doesn't change the cached initial state of the repo
 	localSnapshot, _ := gitconfig.LoadSnapshot(backend, Some(configdomain.ConfigScopeLocal), configdomain.UpdateOutdatedNo)   // we ignore the Git cache here because reloading a config in the middle of a Git Town command doesn't change the cached initial state of the repo
 	unscopedSnapshot, _ := gitconfig.LoadSnapshot(backend, None[configdomain.ConfigScope](), configdomain.UpdateOutdatedNo)   // we ignore the Git cache here because reloading a config in the middle of a Git Town command doesn't change the cached initial state of the repo
-	unscopedGitConfig, _ := NewPartialConfigFromSnapshot(unscopedSnapshot, false, nil)
+	unscopedGitConfig, _ := NewPartialConfigFromSnapshot(unscopedSnapshot, false, false, nil)
 	envConfig, _ := envconfig.Load(envconfig.NewEnvVars(os.Environ()))
 	unvalidatedConfig, normalConfig := mergeConfigs(mergeConfigsArgs{
 		cli:      configdomain.EmptyPartialConfig(),
diff --git a/internal/config/validated_config.go b/internal/config/validated_config.go
index c5c66bf53..ba21e4c03 100644
--- a/internal/config/validated_config.go
+++ b/internal/config/validated_config.go
@@ -8,7 +8,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
 )
 
-// Config provides type-safe access to Git Town configuration settings
+// ValidatedConfig provides type-safe access to Git Town configuration settings
 // stored in the local and global Git configuration.
 type ValidatedConfig struct {
 	NormalConfig        NormalConfig
@@ -44,7 +44,7 @@ func (self *ValidatedConfig) BranchesOfType(branches gitdomain.LocalBranchNames,
 	return result
 }
 
-// removes the given branch from the lineage, and updates its children
+// CleanupBranchFromLineage removes the given branch from the lineage, and updates its children.
 func (self *ValidatedConfig) CleanupBranchFromLineage(runner subshelldomain.Runner, branch gitdomain.LocalBranchName, order configdomain.Order) {
 	parent, hasParent := self.NormalConfig.Lineage.Parent(branch).Get()
 	children := self.NormalConfig.Lineage.Children(branch, order)
@@ -95,7 +95,7 @@ func (self *ValidatedConfig) RemoveDeletedBranchesFromLineage(branchInfos gitdom
 	}
 }
 
-// provides this collection without the perennial branch at the root
+// RemovePerennials provides this collection without the perennial branch at the root.
 func (self *ValidatedConfig) RemovePerennials(stack gitdomain.LocalBranchNames) gitdomain.LocalBranchNames {
 	if len(stack) == 0 {
 		return stack
diff --git a/internal/config/version.go b/internal/config/version.go
index 06787746b..3d53a6b71 100644
--- a/internal/config/version.go
+++ b/internal/config/version.go
@@ -1,4 +1,4 @@
 package config
 
-// the current Git Town version
-const GitTownVersion = "22.1.0"
+// GitTownVersion provides the current Git Town version.
+const GitTownVersion = "22.2.0"
diff --git a/internal/execute/open_repo.go b/internal/execute/open_repo.go
index 0e8779b52..d9b07424a 100644
--- a/internal/execute/open_repo.go
+++ b/internal/execute/open_repo.go
@@ -1,6 +1,7 @@
 package execute
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"os"
@@ -56,7 +57,7 @@ func OpenRepo(args OpenRepoArgs) (OpenRepoResult, error) {
 	if err != nil {
 		return emptyOpenRepoResult(), err
 	}
-	globalConfig, err := config.NewPartialConfigFromSnapshot(globalSnapshot, true, backendRunner)
+	globalConfig, err := config.NewPartialConfigFromSnapshot(globalSnapshot, true, args.IgnoreUnknown, backendRunner)
 	if err != nil {
 		return emptyOpenRepoResult(), err
 	}
@@ -64,7 +65,7 @@ func OpenRepo(args OpenRepoArgs) (OpenRepoResult, error) {
 	if err != nil {
 		return emptyOpenRepoResult(), err
 	}
-	localConfig, err := config.NewPartialConfigFromSnapshot(localSnapshot, true, backendRunner)
+	localConfig, err := config.NewPartialConfigFromSnapshot(localSnapshot, true, args.IgnoreUnknown, backendRunner)
 	if err != nil {
 		return emptyOpenRepoResult(), err
 	}
@@ -72,7 +73,7 @@ func OpenRepo(args OpenRepoArgs) (OpenRepoResult, error) {
 	if err != nil {
 		return emptyOpenRepoResult(), err
 	}
-	unscopedConfig, err := config.NewPartialConfigFromSnapshot(unscopedSnapshot, true, backendRunner)
+	unscopedConfig, err := config.NewPartialConfigFromSnapshot(unscopedSnapshot, true, args.IgnoreUnknown, backendRunner)
 	if err != nil {
 		return emptyOpenRepoResult(), err
 	}
@@ -123,9 +124,9 @@ func OpenRepo(args OpenRepoArgs) (OpenRepoResult, error) {
 		fmt.Println("OS:", runtime.GOOS)
 		var cmd *exec.Cmd
 		if runtime.GOOS == "windows" {
-			cmd = exec.Command("cmd", "/c", "ver")
+			cmd = exec.CommandContext(context.Background(), "cmd", "/c", "ver")
 		} else {
-			cmd = exec.Command("uname", "-a")
+			cmd = exec.CommandContext(context.Background(), "uname", "-a")
 		}
 		cmd.Stdout = os.Stdout
 		cmd.Stderr = os.Stderr
@@ -160,6 +161,7 @@ func OpenRepo(args OpenRepoArgs) (OpenRepoResult, error) {
 
 type OpenRepoArgs struct {
 	CliConfig        configdomain.PartialConfig
+	IgnoreUnknown    bool
 	PrintBranchNames bool
 	PrintCommands    bool
 	ValidateGitRepo  bool
diff --git a/internal/forge/azuredevops/new.go b/internal/forge/azuredevops/new.go
index 18e89ad6d..1a397130f 100644
--- a/internal/forge/azuredevops/new.go
+++ b/internal/forge/azuredevops/new.go
@@ -1,6 +1,7 @@
 package azuredevops
 
 import (
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/git/giturl"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
@@ -12,6 +13,7 @@ func Detect(remoteURL giturl.Parts) bool {
 }
 
 type NewConnectorArgs struct {
+	Browser          Option[configdomain.Browser]
 	ProposalOverride Option[forgedomain.ProposalOverride]
 	RemoteURL        giturl.Parts
 }
@@ -24,5 +26,6 @@ func NewConnector(args NewConnectorArgs) WebConnector {
 			Organization: args.RemoteURL.Org,
 			Repository:   args.RemoteURL.Repo,
 		},
+		browser: args.Browser,
 	}
 }
diff --git a/internal/forge/azuredevops/web_connector.go b/internal/forge/azuredevops/web_connector.go
index 8b23e0450..a9ded60a8 100644
--- a/internal/forge/azuredevops/web_connector.go
+++ b/internal/forge/azuredevops/web_connector.go
@@ -5,8 +5,10 @@ import (
 	"net/url"
 
 	"github.com/git-town/git-town/v22/internal/browser"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
 var (
@@ -17,15 +19,16 @@ var (
 // WebConnector provides connectivity to Azure DevOps through the web browser.
 type WebConnector struct {
 	forgedomain.HostedRepoInfo
+	browser Option[configdomain.Browser]
 }
 
 func (self WebConnector) BrowseRepository(runner subshelldomain.Runner) error {
-	browser.Open(self.RepositoryURL(), runner)
+	browser.Open(self.RepositoryURL(), runner, self.browser)
 	return nil
 }
 
 func (self WebConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
-	browser.Open(self.NewProposalURL(data), data.FrontendRunner)
+	browser.Open(self.NewProposalURL(data), data.FrontendRunner, self.browser)
 	return nil
 }
 
diff --git a/internal/forge/bitbucketcloud/api_connector.go b/internal/forge/bitbucketcloud/api_connector.go
index 0abd2d88b..669e95dd1 100644
--- a/internal/forge/bitbucketcloud/api_connector.go
+++ b/internal/forge/bitbucketcloud/api_connector.go
@@ -40,7 +40,7 @@ func (self APIConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 		Owner:    self.Organization,
 		RepoSlug: self.Repository,
 		Query:    query,
-		States:   []string{"open"},
+		States:   []string{"open", "new"},
 	})
 	if err != nil {
 		self.log.Failed(err.Error())
@@ -50,56 +50,49 @@ func (self APIConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 		self.log.Success("none")
 		return None[forgedomain.Proposal](), nil
 	}
-	result2, ok := result1.(map[string]interface{})
+	result2, ok := result1.(map[string]any)
 	if !ok {
 		self.log.Failed(messages.APIUnexpectedResultDataStructure)
 		return None[forgedomain.Proposal](), nil
 	}
-	size1, has := result2["size"]
+	proposals1, has := result2["values"]
 	if !has {
 		self.log.Failed(messages.APIUnexpectedResultDataStructure)
 		return None[forgedomain.Proposal](), nil
 	}
-	size2, ok := size1.(float64)
+	proposals2, ok := proposals1.([]any)
 	if !ok {
 		self.log.Failed(messages.APIUnexpectedResultDataStructure)
 		return None[forgedomain.Proposal](), nil
 	}
-	size := int(size2)
-	if size == 0 {
-		self.log.Success("none")
-		return None[forgedomain.Proposal](), nil
-	}
-	if size > 1 {
-		self.log.Failed(fmt.Sprintf(messages.ProposalMultipleFromToFound, size, branch, target))
-		return None[forgedomain.Proposal](), nil
-	}
-	proposal1, has := result2["values"]
-	if !has {
-		self.log.Failed(messages.APIUnexpectedResultDataStructure)
-		return None[forgedomain.Proposal](), nil
-	}
-	proposal2, ok := proposal1.([]interface{})
-	if !ok {
-		self.log.Failed(messages.APIUnexpectedResultDataStructure)
-		return None[forgedomain.Proposal](), nil
-	}
-	if len(proposal2) == 0 {
-		self.log.Failed(messages.APIUnexpectedResultDataStructure)
-		return None[forgedomain.Proposal](), nil
-	}
-	proposal3, ok := proposal2[0].(map[string]interface{})
-	if !ok {
-		self.log.Failed(messages.APIUnexpectedResultDataStructure)
-		return None[forgedomain.Proposal](), nil
+	result := []forgedomain.Proposal{}
+	for _, proposal1 := range proposals2 {
+		proposal2, ok := proposal1.(map[string]any)
+		if !ok {
+			self.log.Failed(messages.APIUnexpectedResultDataStructure)
+			return None[forgedomain.Proposal](), nil
+		}
+		proposal3, err := parsePullRequest(proposal2)
+		if err != nil {
+			self.log.Failed(err.Error())
+			return None[forgedomain.Proposal](), nil
+		}
+		if !proposal3.Active {
+			continue
+		}
+		self.log.Success(fmt.Sprintf("#%d ", proposal3.Number))
+		proposal := forgedomain.Proposal{Data: proposal3, ForgeType: forgedomain.ForgeTypeBitbucket}
+		result = append(result, proposal)
 	}
-	proposal4, err := parsePullRequest(proposal3)
-	if err != nil {
-		self.log.Failed(err.Error())
+	switch len(result) {
+	case 0:
+		self.log.Success("none")
 		return None[forgedomain.Proposal](), nil
+	case 1:
+		return Some(result[0]), nil
+	default:
+		return None[forgedomain.Proposal](), fmt.Errorf(messages.ProposalMultipleFromToFound, len(result), branch, target)
 	}
-	self.log.Success(fmt.Sprintf("#%d", proposal4.Number))
-	return Some(forgedomain.Proposal{Data: proposal4, ForgeType: forgedomain.ForgeTypeBitbucket}), nil
 }
 
 // ============================================================================
@@ -108,60 +101,56 @@ func (self APIConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 
 var _ forgedomain.ProposalSearcher = apiConnector // type check
 
-func (self APIConnector) SearchProposal(branch gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+func (self APIConnector) SearchProposals(branch gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
 	self.log.Start(messages.APIParentBranchLookupStart, branch.String())
 	response1, err := self.client.Value.Repositories.PullRequests.Gets(&bitbucket.PullRequestsOptions{
 		Owner:    self.Organization,
 		RepoSlug: self.Repository,
 		Query:    fmt.Sprintf("source.branch.name = %q", branch),
-		States:   []string{"open"},
+		States:   []string{"open", "new"},
 	})
 	if err != nil {
 		self.log.Failed(err.Error())
-		return None[forgedomain.Proposal](), err
+		return []forgedomain.Proposal{}, err
 	}
-	response2, ok := response1.(map[string]interface{})
+	response2, ok := response1.(map[string]any)
 	if !ok {
 		self.log.Failed(messages.APIUnexpectedResultDataStructure)
-		return None[forgedomain.Proposal](), nil
+		return []forgedomain.Proposal{}, nil
 	}
-	size1, has := response2["size"]
+	proposals1, has := response2["values"]
 	if !has {
 		self.log.Failed(messages.APIUnexpectedResultDataStructure)
-		return None[forgedomain.Proposal](), nil
+		return []forgedomain.Proposal{}, nil
 	}
-	size2, ok := size1.(float64)
+	proposals2, ok := proposals1.([]any)
 	if !ok {
 		self.log.Failed(messages.APIUnexpectedResultDataStructure)
-		return None[forgedomain.Proposal](), nil
-	}
-	size3 := int(size2)
-	if size3 == 0 {
-		self.log.Success("none")
-		return None[forgedomain.Proposal](), nil
-	}
-	if size3 > 1 {
-		self.log.Failed(fmt.Sprintf(messages.ProposalMultipleFromFound, size3, branch))
-		return None[forgedomain.Proposal](), nil
-	}
-	values1, has := response2["values"]
-	if !has {
-		self.log.Failed(messages.APIUnexpectedResultDataStructure)
-		return None[forgedomain.Proposal](), nil
+		return []forgedomain.Proposal{}, nil
 	}
-	values2, ok := values1.([]interface{})
-	if !ok {
-		self.log.Failed(messages.APIUnexpectedResultDataStructure)
-		return None[forgedomain.Proposal](), nil
+	result := []forgedomain.Proposal{}
+	for _, proposal1 := range proposals2 {
+		proposal2, ok := proposal1.(map[string]any)
+		if !ok {
+			self.log.Failed(messages.APIUnexpectedResultDataStructure)
+			return []forgedomain.Proposal{}, nil
+		}
+		proposal3, err := parsePullRequest(proposal2)
+		if err != nil {
+			self.log.Failed(err.Error())
+			return []forgedomain.Proposal{}, nil
+		}
+		if !proposal3.Active {
+			continue
+		}
+		self.log.Success(fmt.Sprintf("#%d ", proposal3.Number))
+		proposal := forgedomain.Proposal{Data: proposal3, ForgeType: forgedomain.ForgeTypeBitbucket}
+		result = append(result, proposal)
 	}
-	proposal1 := values2[0].(map[string]interface{})
-	proposal2, err := parsePullRequest(proposal1)
-	if err != nil {
-		self.log.Failed(err.Error())
-		return None[forgedomain.Proposal](), nil
+	if len(result) == 0 {
+		self.log.Success("none")
 	}
-	self.log.Success(proposal2.Target.String())
-	return Some(forgedomain.Proposal{Data: proposal2, ForgeType: forgedomain.ForgeTypeBitbucket}), nil
+	return result, nil
 }
 
 // ============================================================================
@@ -181,12 +170,8 @@ func (self APIConnector) SquashMergeProposal(number int, message gitdomain.Commi
 		RepoSlug: self.Repository,
 		Message:  message.String(),
 	})
-	if err != nil {
-		self.log.Failed(err.Error())
-		return err
-	}
-	self.log.Ok()
-	return nil
+	self.log.Finished(err)
+	return err
 }
 
 // ============================================================================
@@ -209,12 +194,8 @@ func (self APIConnector) UpdateProposalBody(proposalData forgedomain.ProposalInt
 		Draft:             data.Draft,
 		CloseSourceBranch: data.CloseSourceBranch,
 	})
-	if err != nil {
-		self.log.Failed(err.Error())
-		return err
-	}
-	self.log.Ok()
-	return nil
+	self.log.Finished(err)
+	return err
 }
 
 // ============================================================================
@@ -237,12 +218,8 @@ func (self APIConnector) UpdateProposalSource(proposalData forgedomain.ProposalI
 		Draft:             data.Draft,
 		CloseSourceBranch: data.CloseSourceBranch,
 	})
-	if err != nil {
-		self.log.Failed(err.Error())
-		return err
-	}
-	self.log.Ok()
-	return nil
+	self.log.Finished(err)
+	return err
 }
 
 // ============================================================================
@@ -265,12 +242,8 @@ func (self APIConnector) UpdateProposalTarget(proposalData forgedomain.ProposalI
 		Draft:             data.Draft,
 		CloseSourceBranch: data.CloseSourceBranch,
 	})
-	if err != nil {
-		self.log.Failed(err.Error())
-		return err
-	}
-	self.log.Ok()
-	return nil
+	self.log.Finished(err)
+	return err
 }
 
 // ============================================================================
diff --git a/internal/forge/bitbucketcloud/cached_api_connector.go b/internal/forge/bitbucketcloud/cached_api_connector.go
new file mode 100644
index 000000000..88d6775d2
--- /dev/null
+++ b/internal/forge/bitbucketcloud/cached_api_connector.go
@@ -0,0 +1,120 @@
+package bitbucketcloud
+
+import (
+	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+// type checks
+var (
+	cachedAPIConnector CachedAPIConnector
+	_                  forgedomain.Connector = &cachedAPIConnector
+)
+
+// CachedAPIConnector provides access to the Bitbucket Cloud API while caching proposal information.
+type CachedAPIConnector struct {
+	api   APIConnector
+	cache forgedomain.APICache
+}
+
+func (self *CachedAPIConnector) BrowseRepository(runner subshelldomain.Runner) error {
+	return self.api.BrowseRepository(runner)
+}
+
+func (self *CachedAPIConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
+	return self.api.CreateProposal(data)
+}
+
+func (self *CachedAPIConnector) DefaultProposalMessage(proposalData forgedomain.ProposalData) string {
+	return self.api.DefaultProposalMessage(proposalData)
+}
+
+// ============================================================================
+// find proposals
+// ============================================================================
+
+var _ forgedomain.ProposalFinder = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) FindProposal(source, target gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+	if cachedProposal, has := self.cache.Lookup(source, target); has {
+		return cachedProposal, nil
+	}
+	loadedProposal, err := self.api.FindProposal(source, target)
+	if err == nil {
+		self.cache.RegisterLookupResult(source, target, loadedProposal)
+	}
+	return loadedProposal, err
+}
+
+// ============================================================================
+// search proposals
+// ============================================================================
+
+var _ forgedomain.ProposalSearcher = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) SearchProposals(source gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
+	if cachedSearchResult, has := self.cache.LookupSearch(source); has {
+		return cachedSearchResult, nil
+	}
+	loadedSearchResult, err := self.api.SearchProposals(source)
+	if err == nil {
+		self.cache.RegisterSearchResult(source, loadedSearchResult)
+	}
+	return loadedSearchResult, err
+}
+
+// ============================================================================
+// squash-merge proposals
+// ============================================================================
+
+var _ forgedomain.ProposalMerger = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) SquashMergeProposal(proposalNumber int, message gitdomain.CommitMessage) error {
+	self.cache.Clear()
+	return self.api.SquashMergeProposal(proposalNumber, message)
+}
+
+// ============================================================================
+// update proposal body
+// ============================================================================
+
+var _ forgedomain.ProposalBodyUpdater = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) UpdateProposalBody(proposalData forgedomain.ProposalInterface, newBody string) error {
+	self.cache.Clear()
+	return self.api.UpdateProposalBody(proposalData, newBody)
+}
+
+// ============================================================================
+// udpate proposal source
+// ============================================================================
+
+var _ forgedomain.ProposalSourceUpdater = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) UpdateProposalSource(proposalData forgedomain.ProposalInterface, source gitdomain.LocalBranchName) error {
+	self.cache.Clear()
+	return self.api.UpdateProposalSource(proposalData, source)
+}
+
+// ============================================================================
+// update proposal target
+// ============================================================================
+
+var _ forgedomain.ProposalTargetUpdater = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) UpdateProposalTarget(proposalData forgedomain.ProposalInterface, target gitdomain.LocalBranchName) error {
+	self.cache.Clear()
+	return self.api.UpdateProposalTarget(proposalData, target)
+}
+
+// ============================================================================
+// verify credentials
+// ============================================================================
+
+var _ forgedomain.CredentialVerifier = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) VerifyCredentials() forgedomain.VerifyCredentialsResult {
+	return self.api.VerifyCredentials()
+}
diff --git a/internal/forge/bitbucketcloud/new.go b/internal/forge/bitbucketcloud/new.go
index da375968a..f727705b7 100644
--- a/internal/forge/bitbucketcloud/new.go
+++ b/internal/forge/bitbucketcloud/new.go
@@ -2,6 +2,7 @@ package bitbucketcloud
 
 import (
 	"github.com/git-town/git-town/v22/internal/cli/print"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/git/giturl"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
@@ -15,6 +16,7 @@ func Detect(remoteURL giturl.Parts) bool {
 
 type NewConnectorArgs struct {
 	AppPassword      Option[forgedomain.BitbucketAppPassword]
+	Browser          Option[configdomain.Browser]
 	Log              print.Logger
 	ProposalOverride Option[forgedomain.ProposalOverride]
 	RemoteURL        giturl.Parts
@@ -29,6 +31,7 @@ func NewConnector(args NewConnectorArgs) forgedomain.Connector { //nolint: iretu
 			Organization: args.RemoteURL.Org,
 			Repository:   args.RemoteURL.Repo,
 		},
+		browser: args.Browser,
 	}
 	if proposalURLOverride, hasProposalOverride := args.ProposalOverride.Get(); hasProposalOverride {
 		return TestConnector{
@@ -40,11 +43,15 @@ func NewConnector(args NewConnectorArgs) forgedomain.Connector { //nolint: iretu
 	userName, hasUserName := args.UserName.Get()
 	appPassword, hasAppPassword := args.AppPassword.Get()
 	if hasUserName && hasAppPassword {
-		return APIConnector{
+		apiConnector := APIConnector{
 			WebConnector: webConnector,
 			client:       NewMutable(bitbucket.NewBasicAuth(userName.String(), appPassword.String())),
 			log:          args.Log,
 		}
+		return &CachedAPIConnector{
+			api:   apiConnector,
+			cache: forgedomain.APICache{},
+		}
 	}
 	return webConnector
 }
diff --git a/internal/forge/bitbucketcloud/parser.go b/internal/forge/bitbucketcloud/parser.go
index c1bc8d28d..d1ecc7047 100644
--- a/internal/forge/bitbucketcloud/parser.go
+++ b/internal/forge/bitbucketcloud/parser.go
@@ -2,6 +2,7 @@ package bitbucketcloud
 
 import (
 	"errors"
+	"strings"
 
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/git/gitdomain"
@@ -9,7 +10,7 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-func parsePullRequest(pullRequest map[string]interface{}) (result forgedomain.BitbucketCloudProposalData, err error) {
+func parsePullRequest(pullRequest map[string]any) (result forgedomain.BitbucketCloudProposalData, err error) {
 	id1, has := pullRequest["id"]
 	if !has {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
@@ -35,11 +36,21 @@ func parsePullRequest(pullRequest map[string]interface{}) (result forgedomain.Bi
 	if !ok {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
+	state1, has := pullRequest["state"]
+	if !has {
+		return result, errors.New(messages.APIUnexpectedResultDataStructure)
+	}
+	state2, ok := state1.(string)
+	if !ok {
+		return result, errors.New(messages.APIUnexpectedResultDataStructure)
+	}
+	state3 := strings.ToLower(state2)
+	isActive := state3 == "open" || state3 == "new"
 	destination1, has := pullRequest["destination"]
 	if !has {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
-	destination2, ok := destination1.(map[string]interface{})
+	destination2, ok := destination1.(map[string]any)
 	if !ok {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
@@ -47,7 +58,7 @@ func parsePullRequest(pullRequest map[string]interface{}) (result forgedomain.Bi
 	if !has {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
-	destination4, ok := destination3.(map[string]interface{})
+	destination4, ok := destination3.(map[string]any)
 	if !ok {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
@@ -59,11 +70,11 @@ func parsePullRequest(pullRequest map[string]interface{}) (result forgedomain.Bi
 	if !ok {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
-	source1 := pullRequest["source"]
+	source1, has := pullRequest["source"]
 	if !has {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
-	source2, ok := source1.(map[string]interface{})
+	source2, ok := source1.(map[string]any)
 	if !ok {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
@@ -71,7 +82,7 @@ func parsePullRequest(pullRequest map[string]interface{}) (result forgedomain.Bi
 	if !has {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
-	source4, ok := source3.(map[string]interface{})
+	source4, ok := source3.(map[string]any)
 	if !ok {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
@@ -87,7 +98,7 @@ func parsePullRequest(pullRequest map[string]interface{}) (result forgedomain.Bi
 	if !has {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
-	url2, ok := url1.(map[string]interface{})
+	url2, ok := url1.(map[string]any)
 	if !ok {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
@@ -95,7 +106,7 @@ func parsePullRequest(pullRequest map[string]interface{}) (result forgedomain.Bi
 	if !has {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
-	url4, ok := url3.(map[string]interface{})
+	url4, ok := url3.(map[string]any)
 	if !ok {
 		return result, errors.New(messages.APIUnexpectedResultDataStructure)
 	}
@@ -125,6 +136,7 @@ func parsePullRequest(pullRequest map[string]interface{}) (result forgedomain.Bi
 	}
 	return forgedomain.BitbucketCloudProposalData{
 		ProposalData: forgedomain.ProposalData{
+			Active:       isActive,
 			MergeWithAPI: false,
 			Number:       number,
 			Source:       gitdomain.NewLocalBranchName(source6),
diff --git a/internal/forge/bitbucketcloud/web_connector.go b/internal/forge/bitbucketcloud/web_connector.go
index 9da7a893c..6ed249dac 100644
--- a/internal/forge/bitbucketcloud/web_connector.go
+++ b/internal/forge/bitbucketcloud/web_connector.go
@@ -5,8 +5,10 @@ import (
 	"net/url"
 
 	"github.com/git-town/git-town/v22/internal/browser"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
 var (
@@ -17,15 +19,16 @@ var (
 // WebConnector provides connectivity to Bitbucket Cloud through the web browser.
 type WebConnector struct {
 	forgedomain.HostedRepoInfo
+	browser Option[configdomain.Browser]
 }
 
 func (self WebConnector) BrowseRepository(runner subshelldomain.Runner) error {
-	browser.Open(self.RepositoryURL(), runner)
+	browser.Open(self.RepositoryURL(), runner, self.browser)
 	return nil
 }
 
 func (self WebConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
-	browser.Open(self.NewProposalURL(data), data.FrontendRunner)
+	browser.Open(self.NewProposalURL(data), data.FrontendRunner, self.browser)
 	return nil
 }
 
diff --git a/internal/forge/bitbucketdatacenter/api_connector.go b/internal/forge/bitbucketdatacenter/api_connector.go
index 20f1c3e51..6dbf548c9 100644
--- a/internal/forge/bitbucketdatacenter/api_connector.go
+++ b/internal/forge/bitbucketdatacenter/api_connector.go
@@ -62,9 +62,10 @@ func (self APIConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 		self.log.Success("no PR found matching source and target branch")
 		return None[forgedomain.Proposal](), nil
 	}
-	proposal := parsePullRequest(*needle, self.RepositoryURL())
-	self.log.Success(fmt.Sprintf("#%d", proposal.Number))
-	return Some(forgedomain.Proposal{Data: proposal, ForgeType: forgedomain.ForgeTypeBitbucketDatacenter}), nil
+	proposalData := parsePullRequest(*needle, self.RepositoryURL())
+	self.log.Success(fmt.Sprintf("#%d", proposalData.Number))
+	proposal := forgedomain.Proposal{Data: proposalData, ForgeType: forgedomain.ForgeTypeBitbucketDatacenter}
+	return Some(proposal), nil
 }
 
 // ============================================================================
@@ -73,7 +74,7 @@ func (self APIConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 
 var _ forgedomain.ProposalSearcher = apiConnector // type check
 
-func (self APIConnector) SearchProposal(branch gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+func (self APIConnector) SearchProposals(branch gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
 	self.log.Start(messages.APIProposalLookupStart)
 	ctx := context.TODO()
 	fromRefID := fmt.Sprintf("refs/heads/%v", branch)
@@ -84,26 +85,21 @@ func (self APIConnector) SearchProposal(branch gitdomain.LocalBranchName) (Optio
 		Fetch(ctx)
 	if err != nil {
 		self.log.Failed(err.Error())
-		return None[forgedomain.Proposal](), err
-	}
-	if len(resp.Values) == 0 {
-		self.log.Success("none")
-		return None[forgedomain.Proposal](), nil
+		return []forgedomain.Proposal{}, err
 	}
-	var needle *PullRequest
+	result := []forgedomain.Proposal{}
 	for _, pr := range resp.Values {
 		if pr.FromRef.ID == fromRefID {
-			needle = &pr
-			break
+			proposalData := parsePullRequest(pr, self.RepositoryURL())
+			self.log.Success(fmt.Sprintf("#%d ", proposalData.Number))
+			proposal := forgedomain.Proposal{Data: proposalData, ForgeType: forgedomain.ForgeTypeBitbucketDatacenter}
+			result = append(result, proposal)
 		}
 	}
-	if needle == nil {
-		self.log.Success("no PR found matching source branch")
-		return None[forgedomain.Proposal](), nil
+	if len(result) == 0 {
+		self.log.Success("none")
 	}
-	proposal := parsePullRequest(*needle, self.RepositoryURL())
-	self.log.Success(fmt.Sprintf("#%d", proposal.Number))
-	return Some(forgedomain.Proposal{Data: proposal, ForgeType: forgedomain.ForgeTypeBitbucketDatacenter}), nil
+	return result, nil
 }
 
 func (self APIConnector) apiBaseURL() string {
diff --git a/internal/forge/bitbucketdatacenter/cached_api_connector.go b/internal/forge/bitbucketdatacenter/cached_api_connector.go
new file mode 100644
index 000000000..93b77326a
--- /dev/null
+++ b/internal/forge/bitbucketdatacenter/cached_api_connector.go
@@ -0,0 +1,66 @@
+package bitbucketdatacenter
+
+import (
+	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+// type checks
+var (
+	cachedAPIConnector CachedAPIConnector
+	_                  forgedomain.Connector = &cachedAPIConnector
+)
+
+// APIConnector provides access to the Bitbucket DataCenter API.
+type CachedAPIConnector struct {
+	api   APIConnector
+	cache forgedomain.APICache
+}
+
+func (self *CachedAPIConnector) BrowseRepository(runner subshelldomain.Runner) error {
+	return self.api.BrowseRepository(runner)
+}
+
+func (self *CachedAPIConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
+	return self.api.CreateProposal(data)
+}
+
+func (self *CachedAPIConnector) DefaultProposalMessage(proposalData forgedomain.ProposalData) string {
+	return self.api.DefaultProposalMessage(proposalData)
+}
+
+// ============================================================================
+// find proposals
+// ============================================================================
+
+var _ forgedomain.ProposalFinder = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) FindProposal(source, target gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+	if cachedProposal, has := self.cache.Lookup(source, target); has {
+		return cachedProposal, nil
+	}
+	loadedProposal, err := self.api.FindProposal(source, target)
+	if err == nil {
+		self.cache.RegisterLookupResult(source, target, loadedProposal)
+	}
+	return loadedProposal, err
+}
+
+// ============================================================================
+// search proposals
+// ============================================================================
+
+var _ forgedomain.ProposalSearcher = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) SearchProposals(source gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
+	if cachedSearchResult, has := self.cache.LookupSearch(source); has {
+		return cachedSearchResult, nil
+	}
+	loadedSearchResult, err := self.api.SearchProposals(source)
+	if err == nil {
+		self.cache.RegisterSearchResult(source, loadedSearchResult)
+	}
+	return loadedSearchResult, err
+}
diff --git a/internal/forge/bitbucketdatacenter/new.go b/internal/forge/bitbucketdatacenter/new.go
index 583af4643..515cb4c2a 100644
--- a/internal/forge/bitbucketdatacenter/new.go
+++ b/internal/forge/bitbucketdatacenter/new.go
@@ -2,6 +2,7 @@ package bitbucketdatacenter
 
 import (
 	"github.com/git-town/git-town/v22/internal/cli/print"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/git/giturl"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
@@ -20,6 +21,7 @@ func NewConnector(args NewConnectorArgs) forgedomain.Connector { //nolint:iretur
 			Organization: args.RemoteURL.Org,
 			Repository:   args.RemoteURL.Repo,
 		},
+		browser: args.Browser,
 	}
 	if proposalURLOverride, hasProposalOverride := args.ProposalOverride.Get(); hasProposalOverride {
 		return TestConnector{
@@ -29,18 +31,23 @@ func NewConnector(args NewConnectorArgs) forgedomain.Connector { //nolint:iretur
 		}
 	}
 	if args.UserName.IsSome() && args.AppPassword.IsSome() {
-		return APIConnector{
+		apiConnector := APIConnector{
 			WebConnector: webConnector,
 			log:          args.Log,
 			token:        args.AppPassword.GetOrZero().String(),
 			username:     args.UserName.GetOrZero().String(),
 		}
+		return &CachedAPIConnector{
+			api:   apiConnector,
+			cache: forgedomain.APICache{},
+		}
 	}
 	return webConnector
 }
 
 type NewConnectorArgs struct {
 	AppPassword      Option[forgedomain.BitbucketAppPassword]
+	Browser          Option[configdomain.Browser]
 	Log              print.Logger
 	ProposalOverride Option[forgedomain.ProposalOverride]
 	RemoteURL        giturl.Parts
diff --git a/internal/forge/bitbucketdatacenter/parser.go b/internal/forge/bitbucketdatacenter/parser.go
index d45804acd..2148c59cc 100644
--- a/internal/forge/bitbucketdatacenter/parser.go
+++ b/internal/forge/bitbucketdatacenter/parser.go
@@ -10,6 +10,7 @@ import (
 
 func parsePullRequest(pullRequest PullRequest, repoURL string) forgedomain.ProposalData {
 	return forgedomain.ProposalData{
+		Active:       !pullRequest.Closed,
 		MergeWithAPI: false,
 		Number:       pullRequest.ID,
 		Source:       gitdomain.NewLocalBranchName(pullRequest.FromRef.DisplayID),
diff --git a/internal/forge/bitbucketdatacenter/web_connector.go b/internal/forge/bitbucketdatacenter/web_connector.go
index cc9796728..5da4b083f 100644
--- a/internal/forge/bitbucketdatacenter/web_connector.go
+++ b/internal/forge/bitbucketdatacenter/web_connector.go
@@ -5,8 +5,10 @@ import (
 	"net/url"
 
 	"github.com/git-town/git-town/v22/internal/browser"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
 // type check
@@ -18,15 +20,16 @@ var (
 // WebConnector provides connectivity to Bitbucket DataCenter through the web browser.
 type WebConnector struct {
 	forgedomain.HostedRepoInfo
+	browser Option[configdomain.Browser]
 }
 
 func (self WebConnector) BrowseRepository(runner subshelldomain.Runner) error {
-	browser.Open(self.RepositoryURL(), runner)
+	browser.Open(self.RepositoryURL(), runner, self.browser)
 	return nil
 }
 
 func (self WebConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
-	browser.Open(self.NewProposalURL(data), data.FrontendRunner)
+	browser.Open(self.NewProposalURL(data), data.FrontendRunner, self.browser)
 	return nil
 }
 
diff --git a/internal/forge/forgedomain/api_cache.go b/internal/forge/forgedomain/api_cache.go
new file mode 100644
index 000000000..54a63eea6
--- /dev/null
+++ b/internal/forge/forgedomain/api_cache.go
@@ -0,0 +1,114 @@
+package forgedomain
+
+import (
+	"fmt"
+	"slices"
+
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+// APICache caches results of API calls to a forge.
+type APICache struct {
+	results []Result // the cached items
+}
+
+type Result any
+
+// the result of a find operation
+type lookupResult struct {
+	proposal Option[Proposal]
+	source   gitdomain.LocalBranchName
+	target   gitdomain.LocalBranchName
+}
+
+// the result of a search operation
+type searchResult struct {
+	proposals []Proposal
+	source    gitdomain.LocalBranchName
+}
+
+// Clear removes all cached results.
+func (self *APICache) Clear() {
+	self.results = []Result{}
+}
+
+// Lookup provides the proposal for the given source and target branch.
+// Since the absence of a proposal is a valid API result, the second return value is true if the cache is certain about the result.
+// If there is a cached proposal, returns (Some(proposal), true).
+// If it knows there is no proposal for the given source and target branch, returns (None, true).
+// If it has no records, returns (None, false).
+func (self *APICache) Lookup(source, target gitdomain.LocalBranchName) (proposal Option[Proposal], certain bool) {
+	for _, result := range self.results {
+		switch result := result.(type) {
+		case lookupResult:
+			if result.source == source && result.target == target {
+				return result.proposal, true
+			}
+		case searchResult:
+			if result.source == source {
+				for _, proposal := range result.proposals {
+					if proposal.Data.Data().Target == target {
+						return Some(proposal), true
+					}
+				}
+				// here we know that there was a proposal search for the source branch,
+				// and the target branch was not in the result --> we know there is no proposal
+				return None[Proposal](), true
+			}
+		default:
+			panic(fmt.Sprintf("unknown result type: %T", result))
+		}
+	}
+	// we didn't cache any API results for the given source and target branch
+	return None[Proposal](), false
+}
+
+// LookupSearch provides the cached search result for the given source branch.
+func (self *APICache) LookupSearch(source gitdomain.LocalBranchName) (proposals []Proposal, knows bool) {
+	for _, result := range self.results {
+		if searchResult, ok := result.(searchResult); ok {
+			if searchResult.source == source {
+				return searchResult.proposals, true
+			}
+		}
+	}
+	return []Proposal{}, false
+}
+
+// RegisterLookupResult registers the given result of a lookup operation.
+func (self *APICache) RegisterLookupResult(source, target gitdomain.LocalBranchName, proposal Option[Proposal]) {
+	self.removeLookupResult(source, target)
+	self.results = append(self.results, lookupResult{
+		proposal: proposal,
+		source:   source,
+		target:   target,
+	})
+}
+
+// RegisterSearchResult registers the given result of a search operation.
+func (self *APICache) RegisterSearchResult(source gitdomain.LocalBranchName, proposals []Proposal) {
+	self.removeSearchResult(source)
+	self.results = append(self.results, searchResult{
+		proposals: proposals,
+		source:    source,
+	})
+}
+
+func (self *APICache) removeLookupResult(source, target gitdomain.LocalBranchName) {
+	self.results = slices.DeleteFunc(self.results, func(result Result) bool {
+		if result, ok := result.(lookupResult); ok {
+			return result.source == source && result.target == target
+		}
+		return false
+	})
+}
+
+func (self *APICache) removeSearchResult(source gitdomain.LocalBranchName) {
+	self.results = slices.DeleteFunc(self.results, func(result Result) bool {
+		if result, ok := result.(searchResult); ok {
+			return result.source == source
+		}
+		return false
+	})
+}
diff --git a/internal/forge/forgedomain/connector.go b/internal/forge/forgedomain/connector.go
index 7338c4071..a748a7753 100644
--- a/internal/forge/forgedomain/connector.go
+++ b/internal/forge/forgedomain/connector.go
@@ -42,60 +42,42 @@ type VerifyCredentialsResult struct {
 	AuthorizationError  error          // error while verifying authorization, nil == user is authenticated
 }
 
-// ProposalUpdater describes methods that connectors need to implement
+// ProposalBodyUpdater describes methods that connectors need to implement
 // to enable Git Town to update proposals at the active forge.
 type ProposalBodyUpdater interface {
-	// If the connector instance supports loading proposals via the API,
-	// calling this function returns a function that you can call
-	// to update the body (description) of the given proposal to the given value.
-	// A None return value indicates that this connector does not support this feature (yet).
+	// Updates the body (description) of the given proposal to the given value.
 	UpdateProposalBody(proposal ProposalInterface, newBody string) error
 }
 
 // ProposalFinder describes methods that connectors need to implement
 // to enable Git Town to find proposals at the active forge.
 type ProposalFinder interface {
-	// If this connector instance supports loading proposals via the API,
-	// calling this function returns a function that you can call
-	// to load details about the proposal for the given branch into the given target branch.
-	// A None return value indicates that this connector does not support this feature (yet).
+	// Loads details about the proposal for the given branch into the given target branch.
 	FindProposal(branch, target gitdomain.LocalBranchName) (Option[Proposal], error)
 }
 
 // ProposalMerger describes methods that connectors need to implement
 // to enable Git Town to merge for proposals at the active forge.
 type ProposalMerger interface {
-	// If this connector instance supports loading proposals via the API,
-	// calling this function returns a function that you can call
-	// to merge the proposal with the given number using the given message.
-	// A None return value indicates that this connector does not support this feature (yet).
+	// Merges the proposal with the given number using the given message.
 	SquashMergeProposal(number int, message gitdomain.CommitMessage) error
 }
 
 // ProposalSearcher describes methods that connectors need to implement
 // to enable Git Town to search for proposals at the active forge.
 type ProposalSearcher interface {
-	// If this connector instance supports loading proposals via the API,
-	// calling this function returns a function that you can call
-	// to search for a proposal that has the given branch as its source branch.
-	// A None return value indicates that this connector does not support this feature (yet).
-	SearchProposal(branch gitdomain.LocalBranchName) (Option[Proposal], error)
+	// SearchProposals finds all active proposals that have the given branch as its source branch.
+	SearchProposals(branch gitdomain.LocalBranchName) ([]Proposal, error)
 }
 
 // ProposalSourceUpdater describes methods that connectors need to implement
 // to enable Git Town to update the source branch of proposals at the active forge.
 type ProposalSourceUpdater interface {
-	// If this connector instance supports loading proposals via the API,
-	// calling this function returns a function that you can call
-	// to update the source branch of the proposal with the given number.
-	// A None return value indicates that this connector does not support this feature (yet).
+	// Updates the source branch of the proposal with the given number.
 	UpdateProposalSource(proposal ProposalInterface, newSource gitdomain.LocalBranchName) error
 }
 
 type ProposalTargetUpdater interface {
-	// If this connector instance supports loading proposals via the API,
-	// calling this function returns a function that you can call
-	// to update the target branch of the proposal with the given number.
-	// A None return value indicates that this connector does not support this feature (yet).
+	// Updates the target branch of the proposal with the given number.
 	UpdateProposalTarget(proposal ProposalInterface, newTarget gitdomain.LocalBranchName) error
 }
diff --git a/internal/forge/forgedomain/forge_type.go b/internal/forge/forgedomain/forge_type.go
index b791e86b3..3c4b1ae31 100644
--- a/internal/forge/forgedomain/forge_type.go
+++ b/internal/forge/forgedomain/forge_type.go
@@ -24,7 +24,7 @@ const (
 )
 
 // ParseForgeType provides the ForgeType enum matching the given text.
-func ParseForgeType(name string) (Option[ForgeType], error) {
+func ParseForgeType(name string, source string) (Option[ForgeType], error) {
 	if name == "" {
 		return None[ForgeType](), nil
 	}
@@ -34,7 +34,7 @@ func ParseForgeType(name string) (Option[ForgeType], error) {
 			return Some(forgeType), nil
 		}
 	}
-	return None[ForgeType](), fmt.Errorf(messages.ForgeTypeUnknown, name)
+	return None[ForgeType](), fmt.Errorf(messages.ForgeTypeUnknown, source, name)
 }
 
 // forgeTypes provides all legal values for ForgeType
diff --git a/internal/forge/forgedomain/github_connector_type.go b/internal/forge/forgedomain/github_connector_type.go
index f6c9ab8e3..8be333317 100644
--- a/internal/forge/forgedomain/github_connector_type.go
+++ b/internal/forge/forgedomain/github_connector_type.go
@@ -27,7 +27,7 @@ func GitHubConnectorTypes() []GitHubConnectorType {
 	}
 }
 
-func ParseGitHubConnectorType(text string) (Option[GitHubConnectorType], error) {
+func ParseGitHubConnectorType(text string, source string) (Option[GitHubConnectorType], error) {
 	if text == "" {
 		return None[GitHubConnectorType](), nil
 	}
@@ -36,5 +36,5 @@ func ParseGitHubConnectorType(text string) (Option[GitHubConnectorType], error)
 			return Some(connectorType), nil
 		}
 	}
-	return None[GitHubConnectorType](), fmt.Errorf(messages.GitHubConnectorTypeUnknown, text)
+	return None[GitHubConnectorType](), fmt.Errorf(messages.GitHubConnectorTypeUnknown, source, text)
 }
diff --git a/internal/forge/forgedomain/gitlab_connector_type.go b/internal/forge/forgedomain/gitlab_connector_type.go
index eb03d26c2..63b6d3e35 100644
--- a/internal/forge/forgedomain/gitlab_connector_type.go
+++ b/internal/forge/forgedomain/gitlab_connector_type.go
@@ -27,7 +27,7 @@ func GitLabConnectorTypes() []GitLabConnectorType {
 	}
 }
 
-func ParseGitLabConnectorType(text string) (Option[GitLabConnectorType], error) {
+func ParseGitLabConnectorType(text string, source string) (Option[GitLabConnectorType], error) {
 	if text == "" {
 		return None[GitLabConnectorType](), nil
 	}
@@ -36,5 +36,5 @@ func ParseGitLabConnectorType(text string) (Option[GitLabConnectorType], error)
 			return Some(connectorType), nil
 		}
 	}
-	return None[GitLabConnectorType](), fmt.Errorf(messages.GitLabConnectorTypeUnknown, text)
+	return None[GitLabConnectorType](), fmt.Errorf(messages.GitLabConnectorTypeUnknown, source, text)
 }
diff --git a/internal/forge/forgedomain/proposal.go b/internal/forge/forgedomain/proposal.go
index 9dbb8f6d3..ddd2f9ea4 100644
--- a/internal/forge/forgedomain/proposal.go
+++ b/internal/forge/forgedomain/proposal.go
@@ -14,7 +14,7 @@ type Proposal struct {
 }
 
 func (self Proposal) MarshalJSON() ([]byte, error) {
-	return json.Marshal(map[string]interface{}{
+	return json.Marshal(map[string]any{
 		"data":       self.Data,
 		"forge-type": self.ForgeType.String(),
 	})
@@ -30,13 +30,13 @@ func (self *Proposal) UnmarshalJSON(b []byte) error {
 	if err := json.Unmarshal(mapping["forge-type"], &forgeTypeName); err != nil {
 		return err
 	}
-	forgeTypeOpt, err := ParseForgeType(forgeTypeName)
+	forgeTypeOpt, err := ParseForgeType(forgeTypeName, "JSON")
 	if err != nil {
 		return err
 	}
 	forgeType, hasForgeType := forgeTypeOpt.Get()
 	if !hasForgeType {
-		return fmt.Errorf(messages.ForgeTypeUnknown, forgeTypeName)
+		return fmt.Errorf(messages.ForgeTypeUnknown, "JSON", forgeTypeName)
 	}
 	switch forgeType {
 	case ForgeTypeBitbucket:
diff --git a/internal/forge/forgedomain/proposal_data.go b/internal/forge/forgedomain/proposal_data.go
index e3bef8c0a..1fca8ea65 100644
--- a/internal/forge/forgedomain/proposal_data.go
+++ b/internal/forge/forgedomain/proposal_data.go
@@ -6,6 +6,7 @@ import (
 )
 
 type ProposalData struct {
+	Active       bool // whether the proposal is open
 	Body         Option[string]
 	MergeWithAPI bool
 	Number       int
diff --git a/internal/forge/forgedomain/proposal_finder.go b/internal/forge/forgedomain/proposal_finder.go
new file mode 100644
index 000000000..a719bb859
--- /dev/null
+++ b/internal/forge/forgedomain/proposal_finder.go
@@ -0,0 +1,17 @@
+package forgedomain
+
+import (
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+func ProposalFinderFromConnector(connector Option[Connector]) Option[ProposalFinder] {
+	conn, hasConn := connector.Get()
+	if !hasConn {
+		return None[ProposalFinder]()
+	}
+	if proposalFinder, hasProposalFinder := conn.(ProposalFinder); hasProposalFinder {
+		return Some(proposalFinder)
+	}
+
+	return None[ProposalFinder]()
+}
diff --git a/internal/forge/forgedomain/proposal_override.go b/internal/forge/forgedomain/proposal_override.go
index ffbdf543a..5e73c9a58 100644
--- a/internal/forge/forgedomain/proposal_override.go
+++ b/internal/forge/forgedomain/proposal_override.go
@@ -7,13 +7,14 @@ import (
 )
 
 const (
-	// the key under which the proposal API lookup override gets stored in the environment variables
+	// OverrideKey defines the environment variable to simulate proposals at the forge during end-to-end testing.
 	OverrideKey = "GIT_TOWN_TEST_PROPOSAL"
 
-	// the content to use in the OverrideKey environment variable to simulate the API returning that no proposal exists
+	// OverrideNoProposal is the value to use in OverrideKey to simulate the absence of proposals at the forge.
 	OverrideNoProposal = "(no proposal)"
 )
 
+// ProposalOverride allows returning mock proposal data in tests.
 type ProposalOverride string
 
 func (self ProposalOverride) String() string {
diff --git a/internal/forge/forgedomain/proposals_show_lineage.go b/internal/forge/forgedomain/proposals_show_lineage.go
index c387c28cf..c2ef58e68 100644
--- a/internal/forge/forgedomain/proposals_show_lineage.go
+++ b/internal/forge/forgedomain/proposals_show_lineage.go
@@ -9,7 +9,7 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// indicates whether and how proposals should display the stack lineage of the respective branch
+// ProposalsShowLineage indicates whether and how proposals should display the stack lineage of the respective branch.
 type ProposalsShowLineage string
 
 const (
@@ -22,7 +22,7 @@ func (self ProposalsShowLineage) String() string {
 	return string(self)
 }
 
-func ParseProposalsShowLineage(value string) (Option[ProposalsShowLineage], error) {
+func ParseProposalsShowLineage(value string, source string) (Option[ProposalsShowLineage], error) {
 	switch strings.ToLower(strings.TrimSpace(value)) {
 	case "":
 		return None[ProposalsShowLineage](), nil
@@ -35,7 +35,7 @@ func ParseProposalsShowLineage(value string) (Option[ProposalsShowLineage], erro
 	}
 	parsedOpt, err := gohacks.ParseBoolOpt[bool](value, "proposals-show-lineage")
 	if err != nil {
-		return None[ProposalsShowLineage](), fmt.Errorf(messages.ProposalsShowLineageInvalid, value)
+		return None[ProposalsShowLineage](), fmt.Errorf(messages.ProposalsLineageInvalid, source, value)
 	}
 	if parsed, has := parsedOpt.Get(); has {
 		if parsed {
@@ -44,5 +44,5 @@ func ParseProposalsShowLineage(value string) (Option[ProposalsShowLineage], erro
 		}
 		return Some(ProposalsShowLineageNone), nil
 	}
-	return None[ProposalsShowLineage](), fmt.Errorf(messages.ProposalsShowLineageInvalid, value)
+	return None[ProposalsShowLineage](), fmt.Errorf(messages.ProposalsLineageInvalid, source, value)
 }
diff --git a/internal/forge/forgejo/api_connector.go b/internal/forge/forgejo/api_connector.go
index d55684484..dac1fdc52 100644
--- a/internal/forge/forgejo/api_connector.go
+++ b/internal/forge/forgejo/api_connector.go
@@ -53,16 +53,17 @@ func (self *APIConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 		return None[forgedomain.Proposal](), err
 	}
 	pullRequests := FilterPullRequests(openPullRequests, branch, target)
-	switch len(pullRequests) {
+	proposalDatas := parsePullRequests(pullRequests)
+	switch len(proposalDatas) {
 	case 0:
 		self.log.Success("none")
 		return None[forgedomain.Proposal](), nil
 	case 1:
-		proposal := parsePullRequest(pullRequests[0])
-		self.log.Success(proposal.Target.String())
-		return Some(forgedomain.Proposal{Data: proposal, ForgeType: forgedomain.ForgeTypeForgejo}), nil
+		proposal := proposalDatas[0]
+		self.log.Success(proposal.Data.Data().Target.String())
+		return Some(proposal), nil
 	default:
-		return None[forgedomain.Proposal](), fmt.Errorf(messages.ProposalMultipleFromToFound, len(pullRequests), branch, target)
+		return None[forgedomain.Proposal](), fmt.Errorf(messages.ProposalMultipleFromToFound, len(proposalDatas), branch, target)
 	}
 }
 
@@ -72,11 +73,11 @@ func (self *APIConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 
 var _ forgedomain.ProposalSearcher = &apiConnector // type check
 
-func (self *APIConnector) SearchProposal(branch gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+func (self *APIConnector) SearchProposals(branch gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
 	self.log.Start(messages.APIParentBranchLookupStart, branch.String())
 	client, err := self.getClient()
 	if err != nil {
-		return None[forgedomain.Proposal](), err
+		return []forgedomain.Proposal{}, err
 	}
 	openPullRequests, _, err := client.ListRepoPullRequests(self.Organization, self.Repository, forgejo.ListPullRequestsOptions{
 		ListOptions: forgejo.ListOptions{
@@ -86,21 +87,20 @@ func (self *APIConnector) SearchProposal(branch gitdomain.LocalBranchName) (Opti
 	})
 	if err != nil {
 		self.log.Failed(err.Error())
-		return None[forgedomain.Proposal](), err
+		return []forgedomain.Proposal{}, err
 	}
 	pullRequests := filterPullRequests2(openPullRequests, branch)
-	switch len(pullRequests) {
-	case 0:
+	result := make([]forgedomain.Proposal, len(pullRequests))
+	for p, pullRequest := range pullRequests {
+		proposalData := parsePullRequest(pullRequest)
+		self.log.Success(proposalData.Target.String())
+		proposal := forgedomain.Proposal{Data: proposalData, ForgeType: forgedomain.ForgeTypeForgejo}
+		result[p] = proposal
+	}
+	if len(result) == 0 {
 		self.log.Success("none")
-		return None[forgedomain.Proposal](), nil
-	case 1:
-		pullRequest := pullRequests[0]
-		proposal := parsePullRequest(pullRequest)
-		self.log.Success(proposal.Target.String())
-		return Some(forgedomain.Proposal{Data: proposal, ForgeType: forgedomain.ForgeTypeForgejo}), nil
-	default:
-		return None[forgedomain.Proposal](), fmt.Errorf(messages.ProposalMultipleFromFound, len(pullRequests), branch)
 	}
+	return result, nil
 }
 
 // ============================================================================
@@ -121,8 +121,8 @@ func (self *APIConnector) SquashMergeProposal(number int, message gitdomain.Comm
 	}
 	_, _, err = client.MergePullRequest(self.Organization, self.Repository, int64(number), forgejo.MergePullRequestOption{
 		Style:   forgejo.MergeStyleSquash,
-		Title:   commitMessageParts.Subject,
-		Message: commitMessageParts.Text,
+		Title:   commitMessageParts.Title.String(),
+		Message: commitMessageParts.Body,
 	})
 	if err != nil {
 		self.log.Failed(err.Error())
@@ -131,7 +131,7 @@ func (self *APIConnector) SquashMergeProposal(number int, message gitdomain.Comm
 	self.log.Ok()
 	self.log.Start(messages.APIProposalLookupStart)
 	_, _, err = client.GetPullRequest(self.Organization, self.Repository, int64(number))
-	self.log.Ok()
+	self.log.Finished(err)
 	return err
 }
 
@@ -151,12 +151,8 @@ func (self *APIConnector) UpdateProposalBody(proposalData forgedomain.ProposalIn
 	_, _, err = client.EditPullRequest(self.Organization, self.Repository, int64(data.Number), forgejo.EditPullRequestOption{
 		Body: newBody,
 	})
-	if err != nil {
-		self.log.Failed(err.Error())
-		return err
-	}
-	self.log.Ok()
-	return nil
+	self.log.Finished(err)
+	return err
 }
 
 // ============================================================================
@@ -176,12 +172,8 @@ func (self *APIConnector) UpdateProposalTarget(proposalData forgedomain.Proposal
 	_, _, err = client.EditPullRequest(self.Organization, self.Repository, int64(data.Number), forgejo.EditPullRequestOption{
 		Base: targetName,
 	})
-	if err != nil {
-		self.log.Failed(err.Error())
-		return err
-	}
-	self.log.Ok()
-	return nil
+	self.log.Finished(err)
+	return err
 }
 
 // ============================================================================
@@ -253,6 +245,7 @@ func filterPullRequests2(pullRequests []*forgejo.PullRequest, branch gitdomain.L
 
 func parsePullRequest(pullRequest *forgejo.PullRequest) forgedomain.ProposalData {
 	return forgedomain.ProposalData{
+		Active:       pullRequest.State == forgejo.StateOpen,
 		MergeWithAPI: pullRequest.Mergeable,
 		Number:       int(pullRequest.Index),
 		Source:       gitdomain.NewLocalBranchName(pullRequest.Head.Ref),
@@ -262,3 +255,13 @@ func parsePullRequest(pullRequest *forgejo.PullRequest) forgedomain.ProposalData
 		URL:          pullRequest.HTMLURL,
 	}
 }
+
+func parsePullRequests(pullRequests []*forgejo.PullRequest) []forgedomain.Proposal {
+	result := []forgedomain.Proposal{}
+	for _, pullRequest := range pullRequests {
+		proposalData := parsePullRequest(pullRequest)
+		proposal := forgedomain.Proposal{Data: proposalData, ForgeType: forgedomain.ForgeTypeForgejo}
+		result = append(result, proposal)
+	}
+	return result
+}
diff --git a/internal/forge/forgejo/cached_api_connector.go b/internal/forge/forgejo/cached_api_connector.go
new file mode 100644
index 000000000..251209677
--- /dev/null
+++ b/internal/forge/forgejo/cached_api_connector.go
@@ -0,0 +1,109 @@
+package forgejo
+
+import (
+	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+// type checks
+var (
+	cachedAPIConnector CachedAPIConnector
+	_                  forgedomain.Connector = &cachedAPIConnector
+)
+
+// CachedAPIConnector provides access to the Forgejo API with caching.
+type CachedAPIConnector struct {
+	api   *APIConnector
+	cache forgedomain.APICache
+}
+
+func (self *CachedAPIConnector) BrowseRepository(runner subshelldomain.Runner) error {
+	return self.api.BrowseRepository(runner)
+}
+
+func (self *CachedAPIConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
+	return self.api.CreateProposal(data)
+}
+
+func (self *CachedAPIConnector) DefaultProposalMessage(proposalData forgedomain.ProposalData) string {
+	return self.api.DefaultProposalMessage(proposalData)
+}
+
+// ============================================================================
+// find proposals
+// ============================================================================
+
+var _ forgedomain.ProposalFinder = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) FindProposal(source, target gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+	if cachedProposal, has := self.cache.Lookup(source, target); has {
+		return cachedProposal, nil
+	}
+	loadedProposal, err := self.api.FindProposal(source, target)
+	if err == nil {
+		self.cache.RegisterLookupResult(source, target, loadedProposal)
+	}
+	return loadedProposal, err
+}
+
+// ============================================================================
+// search proposals
+// ============================================================================
+
+var _ forgedomain.ProposalSearcher = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) SearchProposals(source gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
+	if cachedSearchResult, has := self.cache.LookupSearch(source); has {
+		return cachedSearchResult, nil
+	}
+	loadedSearchResult, err := self.api.SearchProposals(source)
+	if err == nil {
+		self.cache.RegisterSearchResult(source, loadedSearchResult)
+	}
+	return loadedSearchResult, err
+}
+
+// ============================================================================
+// squash-merge proposals
+// ============================================================================
+
+var _ forgedomain.ProposalMerger = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) SquashMergeProposal(number int, message gitdomain.CommitMessage) error {
+	self.cache.Clear()
+	return self.api.SquashMergeProposal(number, message)
+}
+
+// ============================================================================
+// update proposal body
+// ============================================================================
+
+var _ forgedomain.ProposalBodyUpdater = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) UpdateProposalBody(proposalData forgedomain.ProposalInterface, newBody string) error {
+	self.cache.Clear()
+	return self.api.UpdateProposalBody(proposalData, newBody)
+}
+
+// ============================================================================
+// update proposal target
+// ============================================================================
+
+var _ forgedomain.ProposalTargetUpdater = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) UpdateProposalTarget(proposalData forgedomain.ProposalInterface, target gitdomain.LocalBranchName) error {
+	self.cache.Clear()
+	return self.api.UpdateProposalTarget(proposalData, target)
+}
+
+// ============================================================================
+// verify credentials
+// ============================================================================
+
+var _ forgedomain.CredentialVerifier = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) VerifyCredentials() forgedomain.VerifyCredentialsResult {
+	return self.api.VerifyCredentials()
+}
diff --git a/internal/forge/forgejo/new.go b/internal/forge/forgejo/new.go
index 8b67e1330..b0554ff08 100644
--- a/internal/forge/forgejo/new.go
+++ b/internal/forge/forgejo/new.go
@@ -3,6 +3,7 @@ package forgejo
 import (
 	"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
 	"github.com/git-town/git-town/v22/internal/cli/print"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/git/giturl"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
@@ -15,6 +16,7 @@ func Detect(remoteURL giturl.Parts) bool {
 
 type NewConnectorArgs struct {
 	APIToken         Option[forgedomain.ForgejoToken]
+	Browser          Option[configdomain.Browser]
 	Log              print.Logger
 	ProposalOverride Option[forgedomain.ProposalOverride]
 	RemoteURL        giturl.Parts
@@ -28,6 +30,7 @@ func NewConnector(args NewConnectorArgs) forgedomain.Connector { //nolint:iretur
 			Organization: args.RemoteURL.Org,
 			Repository:   args.RemoteURL.Repo,
 		},
+		browser: args.Browser,
 	}
 	if proposalURLOverride, hasProposalOverride := args.ProposalOverride.Get(); hasProposalOverride {
 		return TestConnector{
@@ -37,13 +40,17 @@ func NewConnector(args NewConnectorArgs) forgedomain.Connector { //nolint:iretur
 		}
 	}
 	if args.APIToken.IsSome() {
-		return APIConnector{
+		apiConnector := APIConnector{
 			APIToken:     args.APIToken,
 			WebConnector: webConnector,
 			_client:      MutableNone[forgejo.Client](),
 			log:          args.Log,
 			remoteURL:    args.RemoteURL,
 		}
+		return &CachedAPIConnector{
+			api:   &apiConnector,
+			cache: forgedomain.APICache{},
+		}
 	}
 	return webConnector
 }
diff --git a/internal/forge/forgejo/web_connector.go b/internal/forge/forgejo/web_connector.go
index f6ad567a6..1fe2322a0 100644
--- a/internal/forge/forgejo/web_connector.go
+++ b/internal/forge/forgejo/web_connector.go
@@ -5,8 +5,10 @@ import (
 	"net/url"
 
 	"github.com/git-town/git-town/v22/internal/browser"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
 var (
@@ -17,15 +19,16 @@ var (
 // WebConnector provides connectivity to Forgejo through the web browser.
 type WebConnector struct {
 	forgedomain.HostedRepoInfo
+	browser Option[configdomain.Browser]
 }
 
 func (self WebConnector) BrowseRepository(runner subshelldomain.Runner) error {
-	browser.Open(self.RepositoryURL(), runner)
+	browser.Open(self.RepositoryURL(), runner, self.browser)
 	return nil
 }
 
 func (self WebConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
-	browser.Open(self.NewProposalURL(data), data.FrontendRunner)
+	browser.Open(self.NewProposalURL(data), data.FrontendRunner, self.browser)
 	return nil
 }
 
diff --git a/internal/forge/gh/cached_connector.go b/internal/forge/gh/cached_connector.go
new file mode 100644
index 000000000..12ed9b016
--- /dev/null
+++ b/internal/forge/gh/cached_connector.go
@@ -0,0 +1,117 @@
+package gh
+
+import (
+	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+// type checks
+var (
+	cachedConnector CachedConnector
+	_               forgedomain.Connector = &cachedConnector
+)
+
+// CachedConnector talks to the GitHub API through the "gh" executable with caching.
+type CachedConnector struct {
+	Cache     forgedomain.APICache
+	Connector Connector
+}
+
+// ============================================================================
+// browse the repo
+// ============================================================================
+
+func (self *CachedConnector) BrowseRepository(runner subshelldomain.Runner) error {
+	return self.Connector.BrowseRepository(runner)
+}
+
+// ============================================================================
+// create proposals
+// ============================================================================
+
+func (self *CachedConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
+	return self.Connector.CreateProposal(data)
+}
+
+func (self *CachedConnector) DefaultProposalMessage(data forgedomain.ProposalData) string {
+	return self.Connector.DefaultProposalMessage(data)
+}
+
+// ============================================================================
+// find proposals
+// ============================================================================
+
+var _ forgedomain.ProposalFinder = &cachedConnector // type-check
+
+func (self *CachedConnector) FindProposal(source, target gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+	if cachedProposal, has := self.Cache.Lookup(source, target); has {
+		return cachedProposal, nil
+	}
+	loadedProposal, err := self.Connector.FindProposal(source, target)
+	if err == nil {
+		self.Cache.RegisterLookupResult(source, target, loadedProposal)
+	}
+	return loadedProposal, err
+}
+
+// ============================================================================
+// search proposals
+// ============================================================================
+
+var _ forgedomain.ProposalSearcher = &cachedConnector // type-check
+
+func (self *CachedConnector) SearchProposals(source gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
+	if cachedSearchResult, has := self.Cache.LookupSearch(source); has {
+		return cachedSearchResult, nil
+	}
+	loadedSearchResult, err := self.Connector.SearchProposals(source)
+	if err == nil {
+		self.Cache.RegisterSearchResult(source, loadedSearchResult)
+	}
+	return loadedSearchResult, err
+}
+
+// ============================================================================
+// squash-merge proposals
+// ============================================================================
+
+var _ forgedomain.ProposalMerger = &cachedConnector // type-check
+
+func (self *CachedConnector) SquashMergeProposal(number int, message gitdomain.CommitMessage) error {
+	self.Cache.Clear()
+	return self.Connector.SquashMergeProposal(number, message)
+}
+
+// ============================================================================
+// update proposal body
+// ============================================================================
+
+var _ forgedomain.ProposalBodyUpdater = &cachedConnector // type-check
+
+func (self *CachedConnector) UpdateProposalBody(proposalData forgedomain.ProposalInterface, updatedBody string) error {
+	self.Cache.Clear()
+	return self.Connector.UpdateProposalBody(proposalData, updatedBody)
+}
+
+// ============================================================================
+// update proposal target
+// ============================================================================
+
+var _ forgedomain.ProposalTargetUpdater = &cachedConnector // type-check
+
+func (self *CachedConnector) UpdateProposalTarget(proposalData forgedomain.ProposalInterface, target gitdomain.LocalBranchName) error {
+	self.Cache.Clear()
+	return self.Connector.UpdateProposalTarget(proposalData, target)
+}
+
+// ============================================================================
+// verify credentials
+// ============================================================================
+
+var _ forgedomain.CredentialVerifier = &cachedConnector // type check
+
+func (self *CachedConnector) VerifyCredentials() forgedomain.VerifyCredentialsResult {
+	return self.Connector.VerifyCredentials()
+}
diff --git a/internal/forge/gh/connector.go b/internal/forge/gh/connector.go
index bfd1b6c59..e4b539a38 100644
--- a/internal/forge/gh/connector.go
+++ b/internal/forge/gh/connector.go
@@ -11,6 +11,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/forge/github"
 	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/gohacks/stringslice"
 	"github.com/git-town/git-town/v22/internal/messages"
 	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
@@ -28,10 +29,18 @@ type Connector struct {
 	Frontend subshelldomain.Runner
 }
 
+// ============================================================================
+// browse the repo
+// ============================================================================
+
 func (self Connector) BrowseRepository(runner subshelldomain.Runner) error {
 	return runner.Run("gh", "browse")
 }
 
+// ============================================================================
+// create proposals
+// ============================================================================
+
 func (self Connector) CreateProposal(data forgedomain.CreateProposalArgs) error {
 	args := []string{"pr", "create", "--head=" + data.Branch.String(), "--base=" + data.ParentBranch.String()}
 	if title, hasTitle := data.ProposalTitle.Get(); hasTitle {
@@ -43,6 +52,14 @@ func (self Connector) CreateProposal(data forgedomain.CreateProposalArgs) error
 	if err := self.Frontend.Run("gh", args...); err != nil {
 		return err
 	}
+	// check if the proposal exists
+	proposal, err := self.FindProposal(data.Branch, data.ParentBranch)
+	if err != nil {
+		return err
+	}
+	if proposal.IsNone() {
+		return nil
+	}
 	return self.Frontend.Run("gh", "pr", "view", "--web")
 }
 
@@ -61,7 +78,18 @@ func (self Connector) FindProposal(branch, target gitdomain.LocalBranchName) (Op
 	if err != nil {
 		return None[forgedomain.Proposal](), err
 	}
-	return ParseJSONOutput(out, branch)
+	proposals, err := ParseJSONOutput(out)
+	if err != nil {
+		return None[forgedomain.Proposal](), err
+	}
+	switch len(proposals) {
+	case 0:
+		return None[forgedomain.Proposal](), nil
+	case 1:
+		return Some(proposals[0]), nil
+	default:
+		return None[forgedomain.Proposal](), fmt.Errorf(messages.ProposalMultipleFromToFound, len(proposals), branch, target)
+	}
 }
 
 // ============================================================================
@@ -70,12 +98,12 @@ func (self Connector) FindProposal(branch, target gitdomain.LocalBranchName) (Op
 
 var _ forgedomain.ProposalSearcher = ghConnector // type-check
 
-func (self Connector) SearchProposal(branch gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
-	out, err := self.Backend.Query("gh", "--head="+branch.String(), "--json=number,title,body,mergeable,headRefName,baseRefName,url")
+func (self Connector) SearchProposals(branch gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
+	out, err := self.Backend.Query("gh", "pr", "list", "--head="+branch.String(), "--json=number,title,body,mergeable,headRefName,baseRefName,url")
 	if err != nil {
-		return None[forgedomain.Proposal](), err
+		return []forgedomain.Proposal{}, err
 	}
-	return ParseJSONOutput(out, branch)
+	return ParseJSONOutput(out)
 }
 
 // ============================================================================
@@ -132,7 +160,7 @@ func ParsePermissionsOutput(output string) forgedomain.VerifyCredentialsResult {
 		AuthenticationError: nil,
 		AuthorizationError:  nil,
 	}
-	lines := strings.Split(output, "\n")
+	lines := stringslice.NonEmptyLines(output)
 	regex := regexp.MustCompile(`Logged in to github.com account (\w+)`)
 	for _, line := range lines {
 		matches := regex.FindStringSubmatch(line)
diff --git a/internal/forge/gh/parser.go b/internal/forge/gh/parser.go
index f4d82dc2a..1002921a9 100644
--- a/internal/forge/gh/parser.go
+++ b/internal/forge/gh/parser.go
@@ -2,36 +2,35 @@ package gh
 
 import (
 	"encoding/json"
-	"fmt"
 
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/git/gitdomain"
-	"github.com/git-town/git-town/v22/internal/messages"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-func ParseJSONOutput(output string, branch gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+func ParseJSONOutput(output string) ([]forgedomain.Proposal, error) {
 	var parsed []jsonData
 	err := json.Unmarshal([]byte(output), &parsed)
 	if err != nil || len(parsed) == 0 {
-		return None[forgedomain.Proposal](), err
+		return []forgedomain.Proposal{}, err
 	}
-	if len(parsed) > 1 {
-		return None[forgedomain.Proposal](), fmt.Errorf(messages.ProposalMultipleFromFound, len(parsed), branch)
+	result := make([]forgedomain.Proposal, len(parsed))
+	for d, data := range parsed {
+		result[d] = forgedomain.Proposal{
+			Data: forgedomain.ProposalData{
+				Active:       data.State == "open",
+				Body:         NewOption(data.Body),
+				MergeWithAPI: data.Mergeable == "MERGEABLE",
+				Number:       data.Number,
+				Source:       gitdomain.NewLocalBranchName(data.HeadRefName),
+				Target:       gitdomain.NewLocalBranchName(data.BaseRefName),
+				Title:        data.Title,
+				URL:          data.URL,
+			},
+			ForgeType: forgedomain.ForgeTypeGitHub,
+		}
 	}
-	data := parsed[0]
-	return Some(forgedomain.Proposal{
-		Data: forgedomain.ProposalData{
-			Body:         NewOption(data.Body),
-			MergeWithAPI: data.Mergeable == "MERGEABLE",
-			Number:       data.Number,
-			Source:       gitdomain.NewLocalBranchName(data.HeadRefName),
-			Target:       gitdomain.NewLocalBranchName(data.BaseRefName),
-			Title:        data.Title,
-			URL:          data.URL,
-		},
-		ForgeType: forgedomain.ForgeTypeGitHub,
-	}), nil
+	return result, nil
 }
 
 // data returned by glab in JSON mode
@@ -41,6 +40,7 @@ type jsonData struct {
 	HeadRefName string `json:"headRefName"`
 	Mergeable   string `json:"mergeable"`
 	Number      int    `json:"number"`
+	State       string `json:"state"`
 	Title       string `json:"title"`
 	URL         string `json:"url"`
 }
diff --git a/internal/forge/gitea/api_connector.go b/internal/forge/gitea/api_connector.go
index a8d1e7f8e..39e56302b 100644
--- a/internal/forge/gitea/api_connector.go
+++ b/internal/forge/gitea/api_connector.go
@@ -74,10 +74,10 @@ func (self *AuthConnector) FindProposal(branch, target gitdomain.LocalBranchName
 
 var _ forgedomain.ProposalSearcher = &apiConnector // type check
 
-func (self *AuthConnector) SearchProposal(branch gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+func (self *AuthConnector) SearchProposals(branch gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
 	client, err := self.getClient()
 	if err != nil {
-		return None[forgedomain.Proposal](), err
+		return []forgedomain.Proposal{}, err
 	}
 	self.log.Start(messages.APIParentBranchLookupStart, branch.String())
 	openPullRequests, _, err := client.ListRepoPullRequests(self.Organization, self.Repository, gitea.ListPullRequestsOptions{
@@ -88,21 +88,20 @@ func (self *AuthConnector) SearchProposal(branch gitdomain.LocalBranchName) (Opt
 	})
 	if err != nil {
 		self.log.Failed(err.Error())
-		return None[forgedomain.Proposal](), err
+		return []forgedomain.Proposal{}, err
 	}
 	pullRequests := FilterPullRequests2(openPullRequests, branch)
-	switch len(pullRequests) {
-	case 0:
+	result := make([]forgedomain.Proposal, len(pullRequests))
+	for p, pullRequest := range pullRequests {
+		proposalData := parsePullRequest(pullRequest)
+		self.log.Success(proposalData.Target.String())
+		proposal := forgedomain.Proposal{Data: proposalData, ForgeType: forgedomain.ForgeTypeGitea}
+		result[p] = proposal
+	}
+	if len(result) == 0 {
 		self.log.Success("none")
-		return None[forgedomain.Proposal](), nil
-	case 1:
-		pullRequest := pullRequests[0]
-		proposal := parsePullRequest(pullRequest)
-		self.log.Success(proposal.Target.String())
-		return Some(forgedomain.Proposal{Data: proposal, ForgeType: forgedomain.ForgeTypeGitea}), nil
-	default:
-		return None[forgedomain.Proposal](), fmt.Errorf(messages.ProposalMultipleFromFound, len(pullRequests), branch)
 	}
+	return result, nil
 }
 
 // ============================================================================
@@ -123,8 +122,8 @@ func (self *AuthConnector) SquashMergeProposal(number int, message gitdomain.Com
 	self.log.Start(messages.ForgeGitHubMergingViaAPI, colors.BoldGreen().Styled(strconv.Itoa(number)))
 	_, _, err = client.MergePullRequest(self.Organization, self.Repository, int64(number), gitea.MergePullRequestOption{
 		Style:   gitea.MergeStyleSquash,
-		Title:   commitMessageParts.Subject,
-		Message: commitMessageParts.Text,
+		Title:   commitMessageParts.Title.String(),
+		Message: commitMessageParts.Body,
 	})
 	if err != nil {
 		self.log.Failed(err.Error())
@@ -133,7 +132,7 @@ func (self *AuthConnector) SquashMergeProposal(number int, message gitdomain.Com
 	self.log.Ok()
 	self.log.Start(messages.APIProposalLookupStart)
 	_, _, err = client.GetPullRequest(self.Organization, self.Repository, int64(number))
-	self.log.Ok()
+	self.log.Finished(err)
 	return err
 }
 
@@ -151,14 +150,10 @@ func (self *AuthConnector) UpdateProposalBody(proposalData forgedomain.ProposalI
 	data := proposalData.Data()
 	self.log.Start(messages.APIProposalUpdateBody, colors.BoldGreen().Styled("#"+strconv.Itoa(data.Number)))
 	_, _, err = client.EditPullRequest(self.Organization, self.Repository, int64(data.Number), gitea.EditPullRequestOption{
-		Body: updatedBody,
+		Body: &updatedBody,
 	})
-	if err != nil {
-		self.log.Failed(err.Error())
-		return err
-	}
-	self.log.Ok()
-	return nil
+	self.log.Finished(err)
+	return err
 }
 
 // ============================================================================
@@ -178,12 +173,8 @@ func (self *AuthConnector) UpdateProposalTarget(proposalData forgedomain.Proposa
 	_, _, err = client.EditPullRequest(self.Organization, self.Repository, int64(data.Number), gitea.EditPullRequestOption{
 		Base: targetName,
 	})
-	if err != nil {
-		self.log.Failed(err.Error())
-		return err
-	}
-	self.log.Ok()
-	return nil
+	self.log.Finished(err)
+	return err
 }
 
 // ============================================================================
diff --git a/internal/forge/gitea/cached_api_connector.go b/internal/forge/gitea/cached_api_connector.go
new file mode 100644
index 000000000..79baa50e7
--- /dev/null
+++ b/internal/forge/gitea/cached_api_connector.go
@@ -0,0 +1,109 @@
+package gitea
+
+import (
+	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+// type checks
+var (
+	cachedAPIConnector CachedAPIConnector
+	_                  forgedomain.Connector = &cachedAPIConnector
+)
+
+// CachedAPIConnector provides access to the Gitea API with caching.
+type CachedAPIConnector struct {
+	api   AuthConnector
+	cache forgedomain.APICache
+}
+
+func (self *CachedAPIConnector) BrowseRepository(runner subshelldomain.Runner) error {
+	return self.api.BrowseRepository(runner)
+}
+
+func (self *CachedAPIConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
+	return self.api.CreateProposal(data)
+}
+
+func (self *CachedAPIConnector) DefaultProposalMessage(proposalData forgedomain.ProposalData) string {
+	return self.api.DefaultProposalMessage(proposalData)
+}
+
+// ============================================================================
+// find proposals
+// ============================================================================
+
+var _ forgedomain.ProposalFinder = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) FindProposal(source, target gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+	if cachedProposal, has := self.cache.Lookup(source, target); has {
+		return cachedProposal, nil
+	}
+	loadedProposal, err := self.api.FindProposal(source, target)
+	if err == nil {
+		self.cache.RegisterLookupResult(source, target, loadedProposal)
+	}
+	return loadedProposal, err
+}
+
+// ============================================================================
+// search proposals
+// ============================================================================
+
+var _ forgedomain.ProposalSearcher = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) SearchProposals(source gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
+	if cachedSearchResult, has := self.cache.LookupSearch(source); has {
+		return cachedSearchResult, nil
+	}
+	loadedSearchResult, err := self.api.SearchProposals(source)
+	if err == nil {
+		self.cache.RegisterSearchResult(source, loadedSearchResult)
+	}
+	return loadedSearchResult, err
+}
+
+// ============================================================================
+// squash-merge proposals
+// ============================================================================
+
+var _ forgedomain.ProposalMerger = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) SquashMergeProposal(number int, message gitdomain.CommitMessage) error {
+	self.cache.Clear()
+	return self.api.SquashMergeProposal(number, message)
+}
+
+// ============================================================================
+// update proposal body
+// ============================================================================
+
+var _ forgedomain.ProposalBodyUpdater = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) UpdateProposalBody(proposalData forgedomain.ProposalInterface, newBody string) error {
+	self.cache.Clear()
+	return self.api.UpdateProposalBody(proposalData, newBody)
+}
+
+// ============================================================================
+// update proposal target
+// ============================================================================
+
+var _ forgedomain.ProposalTargetUpdater = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) UpdateProposalTarget(proposalData forgedomain.ProposalInterface, target gitdomain.LocalBranchName) error {
+	self.cache.Clear()
+	return self.api.UpdateProposalTarget(proposalData, target)
+}
+
+// ============================================================================
+// verify credentials
+// ============================================================================
+
+var _ forgedomain.CredentialVerifier = &cachedAPIConnector
+
+func (self *CachedAPIConnector) VerifyCredentials() forgedomain.VerifyCredentialsResult {
+	return self.api.VerifyCredentials()
+}
diff --git a/internal/forge/gitea/new.go b/internal/forge/gitea/new.go
index 700dc5e4c..cfca697f7 100644
--- a/internal/forge/gitea/new.go
+++ b/internal/forge/gitea/new.go
@@ -3,6 +3,7 @@ package gitea
 import (
 	"code.gitea.io/sdk/gitea"
 	"github.com/git-town/git-town/v22/internal/cli/print"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/git/giturl"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
@@ -15,6 +16,7 @@ func Detect(remoteURL giturl.Parts) bool {
 
 type NewConnectorArgs struct {
 	APIToken         Option[forgedomain.GiteaToken]
+	Browser          Option[configdomain.Browser]
 	Log              print.Logger
 	ProposalOverride Option[forgedomain.ProposalOverride]
 	RemoteURL        giturl.Parts
@@ -28,6 +30,7 @@ func NewConnector(args NewConnectorArgs) forgedomain.Connector { //nolint:iretur
 			Organization: args.RemoteURL.Org,
 			Repository:   args.RemoteURL.Repo,
 		},
+		browser: args.Browser,
 	}
 	if proposalURLOverride, hasProposalOverride := args.ProposalOverride.Get(); hasProposalOverride {
 		return TestConnector{
@@ -37,13 +40,17 @@ func NewConnector(args NewConnectorArgs) forgedomain.Connector { //nolint:iretur
 		}
 	}
 	if apiToken, hasAPIToken := args.APIToken.Get(); hasAPIToken {
-		return AuthConnector{
+		authConnector := AuthConnector{
 			APIToken:     apiToken,
 			RemoteURL:    args.RemoteURL,
 			WebConnector: webConnector,
 			_client:      MutableNone[gitea.Client](),
 			log:          args.Log,
 		}
+		return &CachedAPIConnector{
+			api:   authConnector,
+			cache: forgedomain.APICache{},
+		}
 	}
 	return webConnector
 }
diff --git a/internal/forge/gitea/parser.go b/internal/forge/gitea/parser.go
index 8d9436c68..03fc5ae81 100644
--- a/internal/forge/gitea/parser.go
+++ b/internal/forge/gitea/parser.go
@@ -29,6 +29,7 @@ func FilterPullRequests2(pullRequests []*gitea.PullRequest, branch gitdomain.Loc
 
 func parsePullRequest(pullRequest *gitea.PullRequest) forgedomain.ProposalData {
 	return forgedomain.ProposalData{
+		Active:       pullRequest.State == "open",
 		MergeWithAPI: pullRequest.Mergeable,
 		Number:       int(pullRequest.Index),
 		Source:       gitdomain.NewLocalBranchName(pullRequest.Head.Ref),
diff --git a/internal/forge/gitea/web_connector.go b/internal/forge/gitea/web_connector.go
index 31ba781ac..76206487e 100644
--- a/internal/forge/gitea/web_connector.go
+++ b/internal/forge/gitea/web_connector.go
@@ -5,8 +5,10 @@ import (
 	"net/url"
 
 	"github.com/git-town/git-town/v22/internal/browser"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
 var (
@@ -17,15 +19,16 @@ var (
 // WebConnector provides connectivity to gitea through the browser.
 type WebConnector struct {
 	forgedomain.HostedRepoInfo
+	browser Option[configdomain.Browser]
 }
 
 func (self WebConnector) BrowseRepository(runner subshelldomain.Runner) error {
-	browser.Open(self.RepositoryURL(), runner)
+	browser.Open(self.RepositoryURL(), runner, self.browser)
 	return nil
 }
 
 func (self WebConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
-	browser.Open(self.NewProposalURL(data), data.FrontendRunner)
+	browser.Open(self.NewProposalURL(data), data.FrontendRunner, self.browser)
 	return nil
 }
 
diff --git a/internal/forge/github/api_connector.go b/internal/forge/github/api_connector.go
index 34cf02f77..144d6f57e 100644
--- a/internal/forge/github/api_connector.go
+++ b/internal/forge/github/api_connector.go
@@ -52,9 +52,10 @@ func (self APIConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 	if len(pullRequests) > 1 {
 		return None[forgedomain.Proposal](), fmt.Errorf(messages.ProposalMultipleFromToFound, len(pullRequests), branch, target)
 	}
-	proposal := parsePullRequest(pullRequests[0])
-	self.log.Log(fmt.Sprintf("%s (%s)", colors.BoldGreen().Styled("#"+strconv.Itoa(proposal.Number)), proposal.Title))
-	return Some(forgedomain.Proposal{Data: proposal, ForgeType: forgedomain.ForgeTypeGitHub}), nil
+	proposalData := parsePullRequest(pullRequests[0])
+	self.log.Log(fmt.Sprintf("%s (%s)", colors.BoldGreen().Styled("#"+strconv.Itoa(proposalData.Number)), proposalData.Title))
+	proposal := forgedomain.Proposal{Data: proposalData, ForgeType: forgedomain.ForgeTypeGitHub}
+	return Some(proposal), nil
 }
 
 // ============================================================================
@@ -63,7 +64,7 @@ func (self APIConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 
 var _ forgedomain.ProposalSearcher = apiConnector // type check
 
-func (self APIConnector) SearchProposal(branch gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+func (self APIConnector) SearchProposals(branch gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
 	self.log.Start(messages.APIParentBranchLookupStart, branch.String())
 	pullRequests, _, err := self.client.Value.PullRequests.List(context.Background(), self.Organization, self.Repository, &github.PullRequestListOptions{
 		Head:  self.Organization + ":" + branch.String(),
@@ -71,18 +72,19 @@ func (self APIConnector) SearchProposal(branch gitdomain.LocalBranchName) (Optio
 	})
 	if err != nil {
 		self.log.Failed(err.Error())
-		return None[forgedomain.Proposal](), err
+		return []forgedomain.Proposal{}, err
+	}
+	result := make([]forgedomain.Proposal, len(pullRequests))
+	for p, pullRequest := range pullRequests {
+		proposalData := parsePullRequest(pullRequest)
+		self.log.Success(proposalData.Target.String())
+		proposal := forgedomain.Proposal{Data: proposalData, ForgeType: forgedomain.ForgeTypeGitHub}
+		result[p] = proposal
 	}
 	if len(pullRequests) == 0 {
 		self.log.Success("none")
-		return None[forgedomain.Proposal](), nil
 	}
-	if len(pullRequests) > 1 {
-		return None[forgedomain.Proposal](), fmt.Errorf(messages.ProposalMultipleFromFound, len(pullRequests), branch)
-	}
-	proposal := parsePullRequest(pullRequests[0])
-	self.log.Success(proposal.Target.String())
-	return Some(forgedomain.Proposal{Data: proposal, ForgeType: forgedomain.ForgeTypeGitHub}), nil
+	return result, nil
 }
 
 // ============================================================================
@@ -97,9 +99,9 @@ func (self APIConnector) SquashMergeProposal(number int, message gitdomain.Commi
 	}
 	self.log.Start(messages.ForgeGitHubMergingViaAPI, colors.BoldGreen().Styled("#"+strconv.Itoa(number)))
 	commitMessageParts := message.Parts()
-	_, _, err := self.client.Value.PullRequests.Merge(context.Background(), self.Organization, self.Repository, number, commitMessageParts.Text, &github.PullRequestOptions{
+	_, _, err := self.client.Value.PullRequests.Merge(context.Background(), self.Organization, self.Repository, number, commitMessageParts.Body, &github.PullRequestOptions{
 		MergeMethod: "squash",
-		CommitTitle: commitMessageParts.Subject,
+		CommitTitle: commitMessageParts.Title.String(),
 	})
 	if err != nil {
 		self.log.Ok()
@@ -119,12 +121,8 @@ func (self APIConnector) UpdateProposalBody(proposalData forgedomain.ProposalInt
 	_, _, err := self.client.Value.PullRequests.Edit(context.Background(), self.Organization, self.Repository, data.Number, &github.PullRequest{
 		Body: Ptr(updatedBody),
 	})
-	if err != nil {
-		self.log.Failed(err.Error())
-		return err
-	}
-	self.log.Ok()
-	return nil
+	self.log.Finished(err)
+	return err
 }
 
 // ============================================================================
@@ -142,12 +140,8 @@ func (self APIConnector) UpdateProposalTarget(proposalData forgedomain.ProposalI
 			Ref: &(targetName),
 		},
 	})
-	if err != nil {
-		self.log.Failed(err.Error())
-		return err
-	}
-	self.log.Ok()
-	return nil
+	self.log.Finished(err)
+	return err
 }
 
 // ============================================================================
diff --git a/internal/forge/github/cached_api_connector.go b/internal/forge/github/cached_api_connector.go
new file mode 100644
index 000000000..6352a4315
--- /dev/null
+++ b/internal/forge/github/cached_api_connector.go
@@ -0,0 +1,109 @@
+package github
+
+import (
+	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+// type checks
+var (
+	cachedAPIConnector CachedAPIConnector
+	_                  forgedomain.Connector = &cachedAPIConnector
+)
+
+// CachedAPIConnector provides access to the GitHub API with caching.
+type CachedAPIConnector struct {
+	api   APIConnector
+	cache forgedomain.APICache
+}
+
+func (self *CachedAPIConnector) BrowseRepository(runner subshelldomain.Runner) error {
+	return self.api.BrowseRepository(runner)
+}
+
+func (self *CachedAPIConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
+	return self.api.CreateProposal(data)
+}
+
+func (self *CachedAPIConnector) DefaultProposalMessage(proposalData forgedomain.ProposalData) string {
+	return self.api.DefaultProposalMessage(proposalData)
+}
+
+// ============================================================================
+// find proposals
+// ============================================================================
+
+var _ forgedomain.ProposalFinder = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) FindProposal(source, target gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+	if cachedProposal, has := self.cache.Lookup(source, target); has {
+		return cachedProposal, nil
+	}
+	loadedProposal, err := self.api.FindProposal(source, target)
+	if err == nil {
+		self.cache.RegisterLookupResult(source, target, loadedProposal)
+	}
+	return loadedProposal, err
+}
+
+// ============================================================================
+// search proposals
+// ============================================================================
+
+var _ forgedomain.ProposalSearcher = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) SearchProposals(source gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
+	if cachedSearchResult, has := self.cache.LookupSearch(source); has {
+		return cachedSearchResult, nil
+	}
+	loadedSearchResult, err := self.api.SearchProposals(source)
+	if err == nil {
+		self.cache.RegisterSearchResult(source, loadedSearchResult)
+	}
+	return loadedSearchResult, err
+}
+
+// ============================================================================
+// squash-merge proposals
+// ============================================================================
+
+var _ forgedomain.ProposalMerger = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) SquashMergeProposal(number int, message gitdomain.CommitMessage) error {
+	self.cache.Clear()
+	return self.api.SquashMergeProposal(number, message)
+}
+
+// ============================================================================
+// update proposal body
+// ============================================================================
+
+var _ forgedomain.ProposalBodyUpdater = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) UpdateProposalBody(proposalData forgedomain.ProposalInterface, newBody string) error {
+	self.cache.Clear()
+	return self.api.UpdateProposalBody(proposalData, newBody)
+}
+
+// ============================================================================
+// update proposal target
+// ============================================================================
+
+var _ forgedomain.ProposalTargetUpdater = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) UpdateProposalTarget(proposalData forgedomain.ProposalInterface, target gitdomain.LocalBranchName) error {
+	self.cache.Clear()
+	return self.api.UpdateProposalTarget(proposalData, target)
+}
+
+// ============================================================================
+// verify credentials
+// ============================================================================
+
+var _ forgedomain.CredentialVerifier = &cachedAPIConnector // type check
+
+func (self *CachedAPIConnector) VerifyCredentials() forgedomain.VerifyCredentialsResult {
+	return self.api.VerifyCredentials()
+}
diff --git a/internal/forge/github/new.go b/internal/forge/github/new.go
index d98c537d2..9809eb4a6 100644
--- a/internal/forge/github/new.go
+++ b/internal/forge/github/new.go
@@ -8,6 +8,7 @@ import (
 	"golang.org/x/oauth2"
 
 	"github.com/git-town/git-town/v22/internal/cli/print"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/git/gitdomain"
 	"github.com/git-town/git-town/v22/internal/git/giturl"
@@ -22,6 +23,7 @@ func Detect(remoteURL giturl.Parts) bool {
 
 type NewConnectorArgs struct {
 	APIToken         Option[forgedomain.GitHubToken]
+	Browser          Option[configdomain.Browser]
 	Log              print.Logger
 	ProposalOverride Option[forgedomain.ProposalOverride]
 	RemoteURL        giturl.Parts
@@ -34,6 +36,7 @@ func NewConnector(args NewConnectorArgs) (forgedomain.Connector, error) { //noli
 			Organization: args.RemoteURL.Org,
 			Repository:   args.RemoteURL.Repo,
 		},
+		browser: args.Browser,
 	}
 	if proposalURLOverride, hasProposalOverride := args.ProposalOverride.Get(); hasProposalOverride {
 		return TestConnector{
@@ -54,10 +57,14 @@ func NewConnector(args NewConnectorArgs) (forgedomain.Connector, error) { //noli
 				return webConnector, fmt.Errorf(messages.GitHubEnterpriseInitializeError, err)
 			}
 		}
-		return APIConnector{
+		apiConnector := APIConnector{
 			WebConnector: webConnector,
 			client:       NewMutable(githubClient),
 			log:          args.Log,
+		}
+		return &CachedAPIConnector{
+			api:   apiConnector,
+			cache: forgedomain.APICache{},
 		}, nil
 	}
 	return webConnector, nil
@@ -70,6 +77,7 @@ func RepositoryURL(hostNameWithStandardPort string, organization string, reposit
 // parsePullRequest extracts standardized proposal data from the given GitHub pull-request.
 func parsePullRequest(pullRequest *github.PullRequest) forgedomain.ProposalData {
 	return forgedomain.ProposalData{
+		Active:       pullRequest.GetState() == "open",
 		Body:         NewOption(pullRequest.GetBody()),
 		Number:       pullRequest.GetNumber(),
 		Source:       gitdomain.NewLocalBranchName(pullRequest.Head.GetRef()),
diff --git a/internal/forge/github/web_connector.go b/internal/forge/github/web_connector.go
index a32faa8fb..883a04d02 100644
--- a/internal/forge/github/web_connector.go
+++ b/internal/forge/github/web_connector.go
@@ -5,8 +5,10 @@ import (
 	"net/url"
 
 	"github.com/git-town/git-town/v22/internal/browser"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
 var (
@@ -17,15 +19,16 @@ var (
 // WebConnector provides connectivity to GitHub through the GitHub website.
 type WebConnector struct {
 	forgedomain.HostedRepoInfo
+	browser Option[configdomain.Browser]
 }
 
 func (self WebConnector) BrowseRepository(runner subshelldomain.Runner) error {
-	browser.Open(self.RepositoryURL(), runner)
+	browser.Open(self.RepositoryURL(), runner, self.browser)
 	return nil
 }
 
 func (self WebConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
-	browser.Open(self.NewProposalURL(data), data.FrontendRunner)
+	browser.Open(self.NewProposalURL(data), data.FrontendRunner, self.browser)
 	return nil
 }
 
diff --git a/internal/forge/gitlab/api_connector.go b/internal/forge/gitlab/api_connector.go
index 13c92edb6..49c96df05 100644
--- a/internal/forge/gitlab/api_connector.go
+++ b/internal/forge/gitlab/api_connector.go
@@ -64,7 +64,7 @@ func (self APIConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 
 var _ forgedomain.ProposalSearcher = apiConnector
 
-func (self APIConnector) SearchProposal(branch gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+func (self APIConnector) SearchProposals(branch gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
 	self.log.Start(messages.APIParentBranchLookupStart, branch.String())
 	opts := &gitlab.ListProjectMergeRequestsOptions{
 		State:        gitlab.Ptr("opened"),
@@ -73,19 +73,19 @@ func (self APIConnector) SearchProposal(branch gitdomain.LocalBranchName) (Optio
 	mergeRequests, _, err := self.client.MergeRequests.ListProjectMergeRequests(self.projectPath(), opts)
 	if err != nil {
 		self.log.Failed(err.Error())
-		return None[forgedomain.Proposal](), err
+		return []forgedomain.Proposal{}, err
 	}
-	switch len(mergeRequests) {
-	case 0:
+	result := make([]forgedomain.Proposal, len(mergeRequests))
+	for m, mergeRequest := range mergeRequests {
+		proposalData := parseMergeRequest(mergeRequest)
+		self.log.Success(proposalData.Target.String())
+		proposal := forgedomain.Proposal{Data: proposalData, ForgeType: forgedomain.ForgeTypeGitLab}
+		result[m] = proposal
+	}
+	if len(result) == 0 {
 		self.log.Success("none")
-		return None[forgedomain.Proposal](), nil
-	case 1:
-		proposal := parseMergeRequest(mergeRequests[0])
-		self.log.Success(proposal.Target.String())
-		return Some(forgedomain.Proposal{Data: proposal, ForgeType: forgedomain.ForgeTypeGitLab}), nil
-	default:
-		return None[forgedomain.Proposal](), fmt.Errorf(messages.ProposalMultipleFromFound, len(mergeRequests), branch)
 	}
+	return result, nil
 }
 
 // ============================================================================
@@ -106,12 +106,8 @@ func (self APIConnector) SquashMergeProposal(number int, message gitdomain.Commi
 		// the branch will be deleted by Git Town
 		ShouldRemoveSourceBranch: gitlab.Ptr(false),
 	})
-	if err != nil {
-		self.log.Failed(err.Error())
-		return err
-	}
-	self.log.Ok()
-	return nil
+	self.log.Finished(err)
+	return err
 }
 
 // ============================================================================
@@ -126,12 +122,8 @@ func (self APIConnector) UpdateProposalBody(proposalData forgedomain.ProposalInt
 	_, _, err := self.client.MergeRequests.UpdateMergeRequest(self.projectPath(), data.Number, &gitlab.UpdateMergeRequestOptions{
 		Description: Ptr(updatedDescription),
 	})
-	if err != nil {
-		self.log.Failed(err.Error())
-		return err
-	}
-	self.log.Ok()
-	return nil
+	self.log.Finished(err)
+	return err
 }
 
 // ============================================================================
@@ -146,12 +138,8 @@ func (self APIConnector) UpdateProposalTarget(proposalData forgedomain.ProposalI
 	_, _, err := self.client.MergeRequests.UpdateMergeRequest(self.projectPath(), data.Number, &gitlab.UpdateMergeRequestOptions{
 		TargetBranch: gitlab.Ptr(target.String()),
 	})
-	if err != nil {
-		self.log.Failed(err.Error())
-		return err
-	}
-	self.log.Ok()
-	return nil
+	self.log.Finished(err)
+	return err
 }
 
 // ============================================================================
diff --git a/internal/forge/gitlab/cached_api_connector.go b/internal/forge/gitlab/cached_api_connector.go
new file mode 100644
index 000000000..6610e16bc
--- /dev/null
+++ b/internal/forge/gitlab/cached_api_connector.go
@@ -0,0 +1,109 @@
+package gitlab
+
+import (
+	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+// type checks
+var (
+	cachedAPIConnector CachedAPIConnector
+	_                  forgedomain.Connector = &cachedAPIConnector
+)
+
+// CachedAPIConnector provides access to the GitLab API with caching.
+type CachedAPIConnector struct {
+	api   APIConnector
+	cache forgedomain.APICache
+}
+
+func (self *CachedAPIConnector) BrowseRepository(runner subshelldomain.Runner) error {
+	return self.api.BrowseRepository(runner)
+}
+
+func (self *CachedAPIConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
+	return self.api.CreateProposal(data)
+}
+
+func (self *CachedAPIConnector) DefaultProposalMessage(proposalData forgedomain.ProposalData) string {
+	return self.api.DefaultProposalMessage(proposalData)
+}
+
+// ============================================================================
+// find proposals
+// ============================================================================
+
+var _ forgedomain.ProposalFinder = &cachedAPIConnector
+
+func (self *CachedAPIConnector) FindProposal(source, target gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+	if cachedProposal, has := self.cache.Lookup(source, target); has {
+		return cachedProposal, nil
+	}
+	loadedProposal, err := self.api.FindProposal(source, target)
+	if err == nil {
+		self.cache.RegisterLookupResult(source, target, loadedProposal)
+	}
+	return loadedProposal, err
+}
+
+// ============================================================================
+// search proposals
+// ============================================================================
+
+var _ forgedomain.ProposalSearcher = &cachedAPIConnector
+
+func (self *CachedAPIConnector) SearchProposals(source gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
+	if cachedSearchResult, has := self.cache.LookupSearch(source); has {
+		return cachedSearchResult, nil
+	}
+	loadedSearchResult, err := self.api.SearchProposals(source)
+	if err == nil {
+		self.cache.RegisterSearchResult(source, loadedSearchResult)
+	}
+	return loadedSearchResult, err
+}
+
+// ============================================================================
+// squash-merge proposals
+// ============================================================================
+
+var _ forgedomain.ProposalMerger = &cachedAPIConnector
+
+func (self *CachedAPIConnector) SquashMergeProposal(number int, message gitdomain.CommitMessage) error {
+	self.cache.Clear()
+	return self.api.SquashMergeProposal(number, message)
+}
+
+// ============================================================================
+// update proposal body
+// ============================================================================
+
+var _ forgedomain.ProposalBodyUpdater = &cachedAPIConnector
+
+func (self *CachedAPIConnector) UpdateProposalBody(proposalData forgedomain.ProposalInterface, newBody string) error {
+	self.cache.Clear()
+	return self.api.UpdateProposalBody(proposalData, newBody)
+}
+
+// ============================================================================
+// update proposal target
+// ============================================================================
+
+var _ forgedomain.ProposalTargetUpdater = &cachedAPIConnector
+
+func (self *CachedAPIConnector) UpdateProposalTarget(proposalData forgedomain.ProposalInterface, target gitdomain.LocalBranchName) error {
+	self.cache.Clear()
+	return self.api.UpdateProposalTarget(proposalData, target)
+}
+
+// ============================================================================
+// verify credentials
+// ============================================================================
+
+var _ forgedomain.CredentialVerifier = &cachedAPIConnector
+
+func (self *CachedAPIConnector) VerifyCredentials() forgedomain.VerifyCredentialsResult {
+	return self.api.VerifyCredentials()
+}
diff --git a/internal/forge/gitlab/new.go b/internal/forge/gitlab/new.go
index e2f4219da..b18a0d426 100644
--- a/internal/forge/gitlab/new.go
+++ b/internal/forge/gitlab/new.go
@@ -2,6 +2,7 @@ package gitlab
 
 import (
 	"github.com/git-town/git-town/v22/internal/cli/print"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/git/giturl"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
@@ -15,6 +16,7 @@ func Detect(remoteURL giturl.Parts) bool {
 
 type NewConnectorArgs struct {
 	APIToken         Option[forgedomain.GitLabToken]
+	Browser          Option[configdomain.Browser]
 	Log              print.Logger
 	ProposalOverride Option[forgedomain.ProposalOverride]
 	RemoteURL        giturl.Parts
@@ -27,6 +29,7 @@ func NewConnector(args NewConnectorArgs) (forgedomain.Connector, error) { //noli
 			Organization: args.RemoteURL.Org,
 			Repository:   args.RemoteURL.Repo,
 		},
+		browser: args.Browser,
 	}
 	if proposalURLOverride, hasProposalOverride := args.ProposalOverride.Get(); hasProposalOverride {
 		return TestConnector{
@@ -40,10 +43,14 @@ func NewConnector(args NewConnectorArgs) (forgedomain.Connector, error) { //noli
 		if err != nil {
 			return webConnector, err
 		}
-		return APIConnector{
+		apiConnector := APIConnector{
 			WebConnector: webConnector,
 			client:       client,
 			log:          args.Log,
+		}
+		return &CachedAPIConnector{
+			api:   apiConnector,
+			cache: forgedomain.APICache{},
 		}, nil
 	}
 	return webConnector, nil
diff --git a/internal/forge/gitlab/parser.go b/internal/forge/gitlab/parser.go
index abb3b5b67..c43f9535b 100644
--- a/internal/forge/gitlab/parser.go
+++ b/internal/forge/gitlab/parser.go
@@ -9,6 +9,7 @@ import (
 
 func parseMergeRequest(mergeRequest *gitlab.BasicMergeRequest) forgedomain.ProposalData {
 	return forgedomain.ProposalData{
+		Active:       mergeRequest.State == "opened",
 		MergeWithAPI: true,
 		Number:       mergeRequest.IID,
 		Source:       gitdomain.NewLocalBranchName(mergeRequest.SourceBranch),
diff --git a/internal/forge/gitlab/web_connector.go b/internal/forge/gitlab/web_connector.go
index 80ecc479b..e3befa8f0 100644
--- a/internal/forge/gitlab/web_connector.go
+++ b/internal/forge/gitlab/web_connector.go
@@ -5,8 +5,10 @@ import (
 	"net/url"
 
 	"github.com/git-town/git-town/v22/internal/browser"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
 var (
@@ -17,16 +19,17 @@ var (
 // WebConnector provides connectivity to GitLab through the browser.
 type WebConnector struct {
 	forgedomain.HostedRepoInfo
+	browser Option[configdomain.Browser]
 }
 
 func (self WebConnector) BrowseRepository(runner subshelldomain.Runner) error {
-	browser.Open(self.RepositoryURL(), runner)
+	browser.Open(self.RepositoryURL(), runner, self.browser)
 	return nil
 }
 
 func (self WebConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
 	url := self.NewProposalURL(data)
-	browser.Open(url, data.FrontendRunner)
+	browser.Open(url, data.FrontendRunner, self.browser)
 	return nil
 }
 
diff --git a/internal/forge/glab/cached_connector.go b/internal/forge/glab/cached_connector.go
new file mode 100644
index 000000000..fdf71b2d4
--- /dev/null
+++ b/internal/forge/glab/cached_connector.go
@@ -0,0 +1,118 @@
+package glab
+
+import (
+	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+)
+
+// type checks
+var (
+	cachedConnector CachedConnector
+	_               forgedomain.Connector = &cachedConnector
+)
+
+// CachedConnector provides standardized connectivity for the given repository
+// via the glab CLI with caching.
+type CachedConnector struct {
+	Cache     forgedomain.APICache
+	Connector Connector
+}
+
+// ============================================================================
+// browse the repo
+// ============================================================================
+
+func (self *CachedConnector) BrowseRepository(runner subshelldomain.Runner) error {
+	return self.Connector.BrowseRepository(runner)
+}
+
+// ============================================================================
+// create proposals
+// ============================================================================
+
+func (self *CachedConnector) CreateProposal(data forgedomain.CreateProposalArgs) error {
+	return self.Connector.CreateProposal(data)
+}
+
+func (self *CachedConnector) DefaultProposalMessage(data forgedomain.ProposalData) string {
+	return self.Connector.DefaultProposalMessage(data)
+}
+
+// ============================================================================
+// find proposals
+// ============================================================================
+
+var _ forgedomain.ProposalFinder = &cachedConnector // type check
+
+func (self *CachedConnector) FindProposal(source, target gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+	if cachedProposal, has := self.Cache.Lookup(source, target); has {
+		return cachedProposal, nil
+	}
+	loadedProposal, err := self.Connector.FindProposal(source, target)
+	if err == nil {
+		self.Cache.RegisterLookupResult(source, target, loadedProposal)
+	}
+	return loadedProposal, err
+}
+
+// ============================================================================
+// search proposals
+// ============================================================================
+
+var _ forgedomain.ProposalSearcher = &cachedConnector // type check
+
+func (self *CachedConnector) SearchProposals(source gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
+	if cachedSearchResult, has := self.Cache.LookupSearch(source); has {
+		return cachedSearchResult, nil
+	}
+	loadedSearchResult, err := self.Connector.SearchProposals(source)
+	if err == nil {
+		self.Cache.RegisterSearchResult(source, loadedSearchResult)
+	}
+	return loadedSearchResult, err
+}
+
+// ============================================================================
+// squash-merge proposals
+// ============================================================================
+
+var _ forgedomain.ProposalMerger = &cachedConnector // type check
+
+func (self *CachedConnector) SquashMergeProposal(number int, message gitdomain.CommitMessage) error {
+	self.Cache.Clear()
+	return self.Connector.SquashMergeProposal(number, message)
+}
+
+// ============================================================================
+// update proposal body
+// ============================================================================
+
+var _ forgedomain.ProposalBodyUpdater = &cachedConnector // type check
+
+func (self *CachedConnector) UpdateProposalBody(proposalData forgedomain.ProposalInterface, updatedDescription string) error {
+	self.Cache.Clear()
+	return self.Connector.UpdateProposalBody(proposalData, updatedDescription)
+}
+
+// ============================================================================
+// update proposal target
+// ============================================================================
+
+var _ forgedomain.ProposalTargetUpdater = &cachedConnector // type check
+
+func (self *CachedConnector) UpdateProposalTarget(proposalData forgedomain.ProposalInterface, target gitdomain.LocalBranchName) error {
+	self.Cache.Clear()
+	return self.Connector.UpdateProposalTarget(proposalData, target)
+}
+
+// ============================================================================
+// verify credentials
+// ============================================================================
+
+var _ forgedomain.CredentialVerifier = &cachedConnector
+
+func (self *CachedConnector) VerifyCredentials() forgedomain.VerifyCredentialsResult {
+	return self.Connector.VerifyCredentials()
+}
diff --git a/internal/forge/glab/connector.go b/internal/forge/glab/connector.go
index 9fee534da..13c21af9a 100644
--- a/internal/forge/glab/connector.go
+++ b/internal/forge/glab/connector.go
@@ -1,11 +1,13 @@
 package glab
 
 import (
+	"fmt"
 	"strconv"
 
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/forge/gitlab"
 	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/messages"
 	"github.com/git-town/git-town/v22/internal/subshell/subshelldomain"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
@@ -23,10 +25,18 @@ type Connector struct {
 	Frontend subshelldomain.Runner
 }
 
+// ============================================================================
+// browse the repo
+// ============================================================================
+
 func (self Connector) BrowseRepository(runner subshelldomain.Runner) error {
 	return runner.Run("glab", "repo", "view", "--web")
 }
 
+// ============================================================================
+// create proposals
+// ============================================================================
+
 func (self Connector) CreateProposal(data forgedomain.CreateProposalArgs) error {
 	args := []string{"mr", "create", "--source-branch=" + data.Branch.String(), "--target-branch=" + data.ParentBranch.String()}
 	title, hasTitle := data.ProposalTitle.Get()
@@ -59,7 +69,18 @@ func (self Connector) FindProposal(branch, target gitdomain.LocalBranchName) (Op
 	if err != nil {
 		return None[forgedomain.Proposal](), err
 	}
-	return ParseJSONOutput(out, branch)
+	proposals, err := ParseJSONOutput(out)
+	if err != nil {
+		return None[forgedomain.Proposal](), err
+	}
+	switch len(proposals) {
+	case 0:
+		return None[forgedomain.Proposal](), nil
+	case 1:
+		return Some(proposals[0]), nil
+	default:
+		return None[forgedomain.Proposal](), fmt.Errorf(messages.ProposalMultipleFromFound, len(proposals), branch)
+	}
 }
 
 // ============================================================================
@@ -68,12 +89,12 @@ func (self Connector) FindProposal(branch, target gitdomain.LocalBranchName) (Op
 
 var _ forgedomain.ProposalSearcher = glabConnector // type check
 
-func (self Connector) SearchProposal(branch gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+func (self Connector) SearchProposals(branch gitdomain.LocalBranchName) ([]forgedomain.Proposal, error) {
 	out, err := self.Backend.Query("glab", "--source-branch="+branch.String(), "--output=json")
 	if err != nil {
-		return None[forgedomain.Proposal](), err
+		return []forgedomain.Proposal{}, err
 	}
-	return ParseJSONOutput(out, branch)
+	return ParseJSONOutput(out)
 }
 
 // ============================================================================
diff --git a/internal/forge/glab/parser.go b/internal/forge/glab/parser.go
index 2c6a18ccb..477ce63c0 100644
--- a/internal/forge/glab/parser.go
+++ b/internal/forge/glab/parser.go
@@ -3,26 +3,26 @@ package glab
 import (
 	"encoding/json"
 	"errors"
-	"fmt"
 	"regexp"
-	"strings"
 
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/gohacks/stringslice"
 	"github.com/git-town/git-town/v22/internal/messages"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-func ParseJSONOutput(output string, branch gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
+func ParseJSONOutput(output string) ([]forgedomain.Proposal, error) {
 	var parsed []jsonData
 	err := json.Unmarshal([]byte(output), &parsed)
 	if err != nil || len(parsed) == 0 {
-		return None[forgedomain.Proposal](), err
+		return []forgedomain.Proposal{}, err
 	}
-	if len(parsed) > 1 {
-		return None[forgedomain.Proposal](), fmt.Errorf(messages.ProposalMultipleFromFound, len(parsed), branch)
+	result := make([]forgedomain.Proposal, len(parsed))
+	for d, data := range parsed {
+		result[d] = createProposal(data)
 	}
-	return Some(createProposal(parsed[0])), nil
+	return result, nil
 }
 
 func ParsePermissionsOutput(output string) forgedomain.VerifyCredentialsResult {
@@ -31,7 +31,7 @@ func ParsePermissionsOutput(output string) forgedomain.VerifyCredentialsResult {
 		AuthenticationError: nil,
 		AuthorizationError:  nil,
 	}
-	lines := strings.Split(output, "\n")
+	lines := stringslice.NonEmptyLines(output)
 	regex := regexp.MustCompile(`Logged in to \S+ as (\S+) `)
 	for _, line := range lines {
 		matches := regex.FindStringSubmatch(line)
@@ -51,6 +51,7 @@ type jsonData struct {
 	Mergeable    string `json:"detailed_merge_status"` //nolint:tagliatelle
 	Number       int    `json:"iid"`                   //nolint:tagliatelle
 	SourceBranch string `json:"source_branch"`         //nolint:tagliatelle
+	State        string `json:"state"`                 //nolint:tagliatelle
 	TargetBranch string `json:"target_branch"`         //nolint:tagliatelle
 	Title        string `json:"title"`
 	URL          string `json:"web_url"` //nolint:tagliatelle
@@ -59,6 +60,7 @@ type jsonData struct {
 func createProposal(data jsonData) forgedomain.Proposal {
 	return forgedomain.Proposal{
 		Data: forgedomain.ProposalData{
+			Active:       data.State == "open",
 			Body:         NewOption(data.Description),
 			MergeWithAPI: data.Mergeable == "mergeable",
 			Number:       data.Number,
diff --git a/internal/forge/new_connector.go b/internal/forge/new_connector.go
index 40fff8642..768369854 100644
--- a/internal/forge/new_connector.go
+++ b/internal/forge/new_connector.go
@@ -2,6 +2,7 @@ package forge
 
 import (
 	"github.com/git-town/git-town/v22/internal/cli/print"
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/forge/azuredevops"
 	"github.com/git-town/git-town/v22/internal/forge/bitbucketcloud"
 	"github.com/git-town/git-town/v22/internal/forge/bitbucketdatacenter"
@@ -30,12 +31,14 @@ func NewConnector(args NewConnectorArgs) (Option[forgedomain.Connector], error)
 	switch forgeType {
 	case forgedomain.ForgeTypeAzureDevOps:
 		connector = azuredevops.NewConnector(azuredevops.NewConnectorArgs{
+			Browser:          args.Browser,
 			ProposalOverride: proposalOverride,
 			RemoteURL:        remoteURL,
 		})
 	case forgedomain.ForgeTypeBitbucket:
 		connector = bitbucketcloud.NewConnector(bitbucketcloud.NewConnectorArgs{
 			AppPassword:      args.BitbucketAppPassword,
+			Browser:          args.Browser,
 			Log:              args.Log,
 			ProposalOverride: proposalOverride,
 			RemoteURL:        remoteURL,
@@ -44,6 +47,7 @@ func NewConnector(args NewConnectorArgs) (Option[forgedomain.Connector], error)
 	case forgedomain.ForgeTypeBitbucketDatacenter:
 		connector = bitbucketdatacenter.NewConnector(bitbucketdatacenter.NewConnectorArgs{
 			AppPassword:      args.BitbucketAppPassword,
+			Browser:          args.Browser,
 			Log:              args.Log,
 			ProposalOverride: proposalOverride,
 			RemoteURL:        remoteURL,
@@ -52,6 +56,7 @@ func NewConnector(args NewConnectorArgs) (Option[forgedomain.Connector], error)
 	case forgedomain.ForgeTypeForgejo:
 		connector = forgejo.NewConnector(forgejo.NewConnectorArgs{
 			APIToken:         args.ForgejoToken,
+			Browser:          args.Browser,
 			Log:              args.Log,
 			ProposalOverride: proposalOverride,
 			RemoteURL:        remoteURL,
@@ -59,6 +64,7 @@ func NewConnector(args NewConnectorArgs) (Option[forgedomain.Connector], error)
 	case forgedomain.ForgeTypeGitea:
 		connector = gitea.NewConnector(gitea.NewConnectorArgs{
 			APIToken:         args.GiteaToken,
+			Browser:          args.Browser,
 			Log:              args.Log,
 			ProposalOverride: proposalOverride,
 			RemoteURL:        remoteURL,
@@ -69,20 +75,25 @@ func NewConnector(args NewConnectorArgs) (Option[forgedomain.Connector], error)
 			case forgedomain.GitHubConnectorTypeAPI:
 				connector, err = github.NewConnector(github.NewConnectorArgs{
 					APIToken:         args.GitHubToken,
+					Browser:          args.Browser,
 					Log:              args.Log,
 					ProposalOverride: proposalOverride,
 					RemoteURL:        remoteURL,
 				})
 			case forgedomain.GitHubConnectorTypeGh:
-				connector = gh.Connector{
-					Backend:  args.Backend,
-					Frontend: args.Frontend,
+				connector = &gh.CachedConnector{
+					Connector: gh.Connector{
+						Backend:  args.Backend,
+						Frontend: args.Frontend,
+					},
+					Cache: forgedomain.APICache{},
 				}
 			}
 		} else {
 			// no GitHubConnectorType specified --> use the API connector
 			connector, err = github.NewConnector(github.NewConnectorArgs{
 				APIToken:         args.GitHubToken,
+				Browser:          args.Browser,
 				Log:              args.Log,
 				ProposalOverride: proposalOverride,
 				RemoteURL:        remoteURL,
@@ -94,20 +105,25 @@ func NewConnector(args NewConnectorArgs) (Option[forgedomain.Connector], error)
 			case forgedomain.GitLabConnectorTypeAPI:
 				connector, err = gitlab.NewConnector(gitlab.NewConnectorArgs{
 					APIToken:         args.GitLabToken,
+					Browser:          args.Browser,
 					Log:              args.Log,
 					ProposalOverride: proposalOverride,
 					RemoteURL:        remoteURL,
 				})
 			case forgedomain.GitLabConnectorTypeGlab:
-				connector = glab.Connector{
-					Backend:  args.Backend,
-					Frontend: args.Frontend,
+				connector = &glab.CachedConnector{
+					Connector: glab.Connector{
+						Backend:  args.Backend,
+						Frontend: args.Frontend,
+					},
+					Cache: forgedomain.APICache{},
 				}
 			}
 		} else {
 			// no GitLabConnectorType specified --> use the API connector
 			connector, err = gitlab.NewConnector(gitlab.NewConnectorArgs{
 				APIToken:         args.GitLabToken,
+				Browser:          args.Browser,
 				Log:              args.Log,
 				ProposalOverride: proposalOverride,
 				RemoteURL:        remoteURL,
@@ -121,6 +137,7 @@ type NewConnectorArgs struct {
 	Backend              subshelldomain.Querier
 	BitbucketAppPassword Option[forgedomain.BitbucketAppPassword]
 	BitbucketUsername    Option[forgedomain.BitbucketUsername]
+	Browser              Option[configdomain.Browser]
 	ForgeType            Option[forgedomain.ForgeType]
 	ForgejoToken         Option[forgedomain.ForgejoToken]
 	Frontend             subshelldomain.Runner
diff --git a/internal/forge/proposal_stack_lineage_builder.go b/internal/forge/proposal_stack_lineage_builder.go
index 013b7cf2b..74d105080 100644
--- a/internal/forge/proposal_stack_lineage_builder.go
+++ b/internal/forge/proposal_stack_lineage_builder.go
@@ -13,7 +13,7 @@ import (
 const indentMarker = "-"
 
 type ProposalStackLineageArgs struct {
-	Connector                forgedomain.ProposalFinder
+	Connector                Option[forgedomain.ProposalFinder]
 	CurrentBranch            gitdomain.LocalBranchName
 	Lineage                  configdomain.Lineage
 	MainAndPerennialBranches gitdomain.LocalBranchNames
@@ -59,7 +59,7 @@ func (self *ProposalStackLineageBuilder) Build(args ProposalStackLineageArgs) st
 	var builder strings.Builder
 	builder.WriteString("\n-------------------------\n")
 	builder.WriteString(self.build(self.tree.Node, args, 0))
-	builder.WriteString("\nStack generated by [Git Town](https://github.com/git-town/git-town)\n")
+	builder.WriteString("\n<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>\n")
 	return builder.String()
 }
 
diff --git a/internal/forge/proposal_stack_lineage_tree.go b/internal/forge/proposal_stack_lineage_tree.go
index a1f5ad789..4454a44e5 100644
--- a/internal/forge/proposal_stack_lineage_tree.go
+++ b/internal/forge/proposal_stack_lineage_tree.go
@@ -92,7 +92,7 @@ func buildAncestorChain(
 	visited map[gitdomain.LocalBranchName]*ProposalStackLineageTreeNode,
 ) (gitdomain.LocalBranchNames, error) {
 	ancestors := args.Lineage.Ancestors(args.CurrentBranch)
-	descendants := gitdomain.NewLocalBranchNames(args.CurrentBranch.String())
+	descendants := gitdomain.LocalBranchNames{args.CurrentBranch}
 	previous := tree.Node
 
 	for _, ancestor := range ancestors {
@@ -150,9 +150,12 @@ func createAncestorNode(
 func findProposal(
 	childBranch gitdomain.LocalBranchName,
 	targetBranch gitdomain.LocalBranchName,
-	connector forgedomain.ProposalFinder,
+	connector Option[forgedomain.ProposalFinder],
 ) (Option[forgedomain.Proposal], error) {
-	return connector.FindProposal(childBranch, targetBranch)
+	if proposalFinder, hasProposalFinder := connector.Get(); hasProposalFinder {
+		return proposalFinder.FindProposal(childBranch, targetBranch)
+	}
+	return None[forgedomain.Proposal](), nil
 }
 
 func findRelevantChildren(
diff --git a/internal/git/blob.go b/internal/git/blob.go
index e8befc293..bae92c4b5 100644
--- a/internal/git/blob.go
+++ b/internal/git/blob.go
@@ -10,7 +10,7 @@ import (
 	"github.com/git-town/git-town/v22/pkg/colors"
 )
 
-// describes the content of a file blob in Git
+// Blob describes the content of a file blob in Git.
 type Blob struct {
 	FilePath   string        // relative path of the file in the repo
 	Permission string        // permissions, in the form "100755"
diff --git a/internal/git/commands.go b/internal/git/commands.go
index 94008d736..1b430d0db 100644
--- a/internal/git/commands.go
+++ b/internal/git/commands.go
@@ -95,6 +95,32 @@ func (self *Commands) BranchInSyncWithTracking(querier subshelldomain.Querier, b
 	return branchSHA == trackingSHA, nil
 }
 
+func (self *Commands) BranchesAvailableInCurrentWorktree(querier subshelldomain.Querier) (gitdomain.LocalBranchNames, error) {
+	branches, err := branchesQuery(querier)
+	if err != nil {
+		return gitdomain.LocalBranchNames{}, err
+	}
+	result := gitdomain.LocalBranchNames{}
+	for _, branch := range branches {
+		// Skip symbolic refs
+		if branch.Symref {
+			continue
+		}
+		// Only include local branches
+		if !isLocalRefName(branch.RefName) {
+			continue
+		}
+		// Include branches that are not checked out in other worktrees
+		// A branch is available in the current worktree if:
+		// - It's not checked out anywhere (!branch.Worktree), OR
+		// - It's checked out in the current worktree (branch.Head)
+		if !branch.Worktree || branch.Head {
+			result = append(result, branch.BranchName.LocalName())
+		}
+	}
+	return result, nil
+}
+
 func (self *Commands) BranchesSnapshot(querier subshelldomain.Querier) (gitdomain.BranchesSnapshot, error) {
 	branches, err := branchesQuery(querier)
 	if err != nil {
@@ -292,12 +318,9 @@ func (self *Commands) CommitsInFeatureBranch(querier subshelldomain.Querier, bra
 	if err != nil {
 		return gitdomain.Commits{}, err
 	}
-	lines := strings.Split(output, "\n")
+	lines := stringslice.NonEmptyLines(output)
 	result := make(gitdomain.Commits, 0, len(lines))
 	for _, line := range lines {
-		if len(line) == 0 {
-			continue
-		}
 		sha, message, ok := strings.Cut(line, " ")
 		if !ok {
 			continue
@@ -415,8 +438,7 @@ func (self *Commands) CurrentBranchDuringRebase(querier subshelldomain.Querier)
 			continue
 		}
 		refName := strings.TrimSpace(string(content))
-		if strings.HasPrefix(refName, "refs/heads/") {
-			branchName := strings.TrimPrefix(refName, "refs/heads/")
+		if branchName, isBranchName := strings.CutPrefix(refName, "refs/heads/"); isBranchName {
 			return Some(gitdomain.NewLocalBranchName(branchName)), nil
 		}
 		// rebase head name is not a branch name
@@ -494,7 +516,7 @@ func (self *Commands) FileConflicts(querier subshelldomain.Querier) (FileConflic
 	return ParseLsFilesUnmergedOutput(output)
 }
 
-// provides the commit message of the first commit in the branch with the given name
+// FirstCommitMessageInBranch provides the commit message of the first commit in the branch with the given name.
 func (self *Commands) FirstCommitMessageInBranch(runner subshelldomain.Querier, branch, parent gitdomain.BranchName) (Option[gitdomain.CommitMessage], error) {
 	output, err := runner.QueryTrim("git", "log", fmt.Sprintf("%s..%s", parent, branch), "--format=%s", "--reverse")
 	if err != nil {
@@ -507,7 +529,7 @@ func (self *Commands) FirstCommitMessageInBranch(runner subshelldomain.Querier,
 	return Some(gitdomain.CommitMessage(lines[0])), nil
 }
 
-// provides the first branch in the given list of branch names that actually exists
+// FirstExistingBranch provides the first branch in the given list of branch names that actually exists.
 func (self *Commands) FirstExistingBranch(runner subshelldomain.Runner, branches ...gitdomain.LocalBranchName) Option[gitdomain.LocalBranchName] {
 	for _, branch := range branches {
 		if self.BranchExists(runner, branch) {
@@ -576,7 +598,7 @@ func (self *Commands) MergeBranchNoEdit(runner subshelldomain.Runner, branch git
 	return runner.Run("git", "merge", "--no-edit", "--ff", branch.String())
 }
 
-// loads the information needed to determine which of the given file conflicts are phantom merge conflicts
+// MergeConflicts loads the information needed to determine which of the given file conflicts are phantom merge conflicts.
 func (self *Commands) MergeConflicts(querier subshelldomain.Querier, fileConflicts FileConflicts, parentLocation gitdomain.Location, rootBranch gitdomain.LocalBranchName) (MergeConflicts, error) {
 	result := make(MergeConflicts, len(fileConflicts))
 	for f, fileConflict := range fileConflicts {
@@ -695,7 +717,7 @@ func (self *Commands) Rebase(runner subshelldomain.Runner, target gitdomain.Bran
 	return runner.Run("git", "-c", "rebase.updateRefs=false", "rebase", target.String())
 }
 
-// Rebase initiates a Git rebase of the current branch onto the given branch.
+// RebaseOnto initiates a Git rebase of the current branch onto the given branch.
 func (self *Commands) RebaseOnto(runner subshelldomain.Runner, branchToRebaseOnto gitdomain.Location, commitsToRemove gitdomain.Location) error {
 	return runner.Run("git", "-c", "rebase.updateRefs=false", "rebase", "--onto", branchToRebaseOnto.String(), commitsToRemove.String())
 }
@@ -761,7 +783,7 @@ func (self *Commands) RepoStatus(backend subshelldomain.RunnerQuerier) (gitdomai
 }
 
 func (self *Commands) ResetBranch(runner subshelldomain.Runner, target gitdomain.BranchName) error {
-	return runner.Run("git", "reset", "--soft", target.String())
+	return runner.Run("git", "reset", "--soft", target.String(), "--")
 }
 
 func (self *Commands) ResetCurrentBranchToSHA(runner subshelldomain.Runner, sha gitdomain.SHA) error {
@@ -809,7 +831,7 @@ func (self *Commands) StageFiles(runner subshelldomain.Runner, names ...string)
 	return runner.Run("git", args...)
 }
 
-// determines the branch that is configured in Git as the default branch
+// StandardBranch determines the branch that is configured in Git as the default branch.
 func (self *Commands) StandardBranch(querier subshelldomain.Querier) Option[gitdomain.LocalBranchName] {
 	if defaultBranch, has := gitconfig.DefaultBranch(querier).Get(); has {
 		return Some(defaultBranch)
diff --git a/internal/git/file_conflict.go b/internal/git/file_conflict.go
index a9bad02b6..d6113a4d7 100644
--- a/internal/git/file_conflict.go
+++ b/internal/git/file_conflict.go
@@ -18,7 +18,7 @@ type FileConflict struct {
 	IncomingChange      Option[Blob] // info about the content of the file on the branch being merged in, None == file is being deleted here
 }
 
-// prints debug information
+// Debug prints debug information.
 func (self FileConflict) Debug(querier subshelldomain.Querier) {
 	base, hasBase := self.BaseChange.Get()
 	current, hasCurrent := self.CurrentBranchChange.Get()
diff --git a/internal/git/file_status.go b/internal/git/file_status.go
index e07630c1a..c2a7433fd 100644
--- a/internal/git/file_status.go
+++ b/internal/git/file_status.go
@@ -9,7 +9,7 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// information about each file reported by "git status -z"
+// FileStatus contains information about each file reported by "git status -z".
 type FileStatus struct {
 	OriginalPath Option[string] // if the file was renamed or copied, the old path of the file
 	Path         string         // the path to the file
diff --git a/internal/git/gitdomain/author.go b/internal/git/gitdomain/author.go
index 3ad9ea2ed..5a4cdbb1d 100644
--- a/internal/git/gitdomain/author.go
+++ b/internal/git/gitdomain/author.go
@@ -5,7 +5,7 @@ import . "github.com/git-town/git-town/v22/pkg/prelude"
 // Author represents the author of a commit in the format "name <email>"
 type Author string
 
-// implements fmt.Stringer
+// String implements the fmt.Stringer interface.
 func (self Author) String() string {
 	return string(self)
 }
diff --git a/internal/git/gitdomain/branch_info.go b/internal/git/gitdomain/branch_info.go
index f1d3b2263..5948c504e 100644
--- a/internal/git/gitdomain/branch_info.go
+++ b/internal/git/gitdomain/branch_info.go
@@ -20,11 +20,11 @@ type BranchInfo struct {
 	// RemoteSHA contains the SHA of the tracking branch before Git Town ran.
 	RemoteSHA Option[SHA]
 
-	// SyncStatus of the branch
+	// SyncStatus of the branch.
 	SyncStatus SyncStatus
 }
 
-// provides both the name and SHA of the local branch
+// GetLocal provides both the name and SHA of the local branch.
 func (self BranchInfo) GetLocal() (bool, LocalBranchName, SHA) {
 	name, hasName := self.LocalName.Get()
 	sha, hasSHA := self.LocalSHA.Get()
@@ -61,14 +61,14 @@ func (self BranchInfo) GetLocalOrRemoteSHA() SHA {
 	panic("BranchInfo has neither a local nor remote SHA")
 }
 
-// provides both the name and SHA of the remote branch
+// GetRemoteBranch provides both the name and SHA of the remote branch.
 func (self BranchInfo) GetRemoteBranch() (bool, RemoteBranchName, SHA) {
 	name, hasName := self.RemoteName.Get()
 	sha, hasSHA := self.RemoteSHA.Get()
 	return hasName && hasSHA, name, sha
 }
 
-// provides the SHAs of the local and remote branch
+// GetSHAs provides the SHAs of the local and remote branch.
 func (self BranchInfo) GetSHAs() (hasBothSHA bool, localSHA, remoteSHA SHA) {
 	local, hasLocal := self.LocalSHA.Get()
 	remote, hasRemote := self.RemoteSHA.Get()
@@ -118,7 +118,7 @@ func (self BranchInfo) IsLocalOnlyBranch() (bool, LocalBranchName) {
 	return true, branchName
 }
 
-// Indicates whether the branch described by this BranchInfo is omni
+// IsOmniBranch indicates whether the branch described by this BranchInfo is omni
 // and provides all relevant data around this scenario.
 // An omni branch has the same SHA locally and remotely.
 func (self BranchInfo) IsOmniBranch() (isOmni bool, branch LocalBranchName, sha SHA) {
@@ -129,7 +129,7 @@ func (self BranchInfo) IsOmniBranch() (isOmni bool, branch LocalBranchName, sha
 	return isOmni, branchName, localSHA
 }
 
-// provides the name of this branch as a local branch, independent of whether this branch is local or not
+// LocalBranchName provides the name of this branch as a local branch, independent of whether this branch is local or not.
 func (self BranchInfo) LocalBranchName() LocalBranchName {
 	if localName, hasLocalName := self.LocalName.Get(); hasLocalName {
 		return localName
diff --git a/internal/git/gitdomain/branch_infos.go b/internal/git/gitdomain/branch_infos.go
index f6de24bbc..bf404b81e 100644
--- a/internal/git/gitdomain/branch_infos.go
+++ b/internal/git/gitdomain/branch_infos.go
@@ -2,6 +2,7 @@ package gitdomain
 
 import (
 	"fmt"
+	"strings"
 
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
@@ -19,6 +20,16 @@ func (self BranchInfos) BranchIsActiveInAnotherWorktree(branch LocalBranchName)
 	return branchInfo.SyncStatus == SyncStatusOtherWorktree
 }
 
+func (self BranchInfos) BranchesInOtherWorktrees() LocalBranchNames {
+	result := LocalBranchNames{}
+	for _, bi := range self {
+		if bi.SyncStatus == SyncStatusOtherWorktree {
+			result = append(result, bi.GetLocalOrRemoteNameAsLocalName())
+		}
+	}
+	return result
+}
+
 // FindByLocalName provides the branch with the given name if one exists.
 func (self BranchInfos) FindByLocalName(branchName LocalBranchName) OptionalMutable[BranchInfo] {
 	for bi, branch := range self {
@@ -111,7 +122,7 @@ func (self BranchInfos) HasLocalBranches(branches LocalBranchNames) bool {
 	return true
 }
 
-// HasMatchingRemoteBranchFor indicates whether there is already a remote branch matching the given local branch.
+// HasMatchingTrackingBranchFor indicates whether there is already a remote branch matching the given local branch.
 func (self BranchInfos) HasMatchingTrackingBranchFor(localBranch LocalBranchName, devRemote Remote) bool {
 	return self.FindByRemoteName(localBranch.TrackingBranch(devRemote)).IsSome()
 }
@@ -188,11 +199,13 @@ func (self BranchInfos) Select(remote Remote, names ...LocalBranchName) (result
 }
 
 func (self BranchInfos) String() string {
-	result := "BranchInfos:\n"
+	result := strings.Builder{}
+	result.WriteString("BranchInfos:\n")
 	for _, bi := range self {
-		result += bi.String() + "\n"
+		result.WriteString(bi.String())
+		result.WriteRune('\n')
 	}
-	return result
+	return result.String()
 }
 
 func (self BranchInfos) UpdateLocalSHA(branch LocalBranchName, sha SHA) error {
diff --git a/internal/git/gitdomain/branch_name.go b/internal/git/gitdomain/branch_name.go
index adf6cdd67..5d1e7e5f9 100644
--- a/internal/git/gitdomain/branch_name.go
+++ b/internal/git/gitdomain/branch_name.go
@@ -45,7 +45,7 @@ func (self BranchName) RemoteName() RemoteBranchName {
 	return NewRemoteBranchName("origin/" + string(self))
 }
 
-// Implementation of the fmt.Stringer interface.
+// String implements the fmt.Stringer interface.
 func (self BranchName) String() string {
 	return string(self)
 }
diff --git a/internal/git/gitdomain/commit_message.go b/internal/git/gitdomain/commit_message.go
index d9da988ff..8df69fc1b 100644
--- a/internal/git/gitdomain/commit_message.go
+++ b/internal/git/gitdomain/commit_message.go
@@ -7,8 +7,8 @@ type CommitMessage string
 
 // CommitMessageParts describes the parts of a Git commit message.
 type CommitMessageParts struct {
-	Subject string // the first line of the commit message
-	Text    string // the commit message text minus the first line and empty lines separating it from the rest of the message
+	Body  string      // the commit message text minus the first line and empty lines separating it from the rest of the message
+	Title CommitTitle // the first line of the commit message
 }
 
 // Parts separates the parts of the given commit message.
@@ -18,8 +18,8 @@ func (self CommitMessage) Parts() CommitMessageParts {
 		body = body[1:]
 	}
 	return CommitMessageParts{
-		Subject: title,
-		Text:    body,
+		Body:  body,
+		Title: CommitTitle(title),
 	}
 }
 
diff --git a/internal/git/gitdomain/commit_message_file.go b/internal/git/gitdomain/commit_message_file.go
index a81bc69d8..e60929fb1 100644
--- a/internal/git/gitdomain/commit_message_file.go
+++ b/internal/git/gitdomain/commit_message_file.go
@@ -3,7 +3,7 @@ package gitdomain
 // CommitMessageFile is the name of the file from which to read the CommitMessage.
 type CommitMessageFile string
 
-// indicates whether the commit message should be read from STDIN
+// ShouldReadStdin indicates whether the commit message should be read from STDIN.
 func (self CommitMessageFile) ShouldReadStdin() bool {
 	return self == "-"
 }
diff --git a/internal/git/gitdomain/commit_title.go b/internal/git/gitdomain/commit_title.go
new file mode 100644
index 000000000..7c7af5608
--- /dev/null
+++ b/internal/git/gitdomain/commit_title.go
@@ -0,0 +1,8 @@
+package gitdomain
+
+// CommitTitle is the first line of a CommitMessage.
+type CommitTitle string
+
+func (self CommitTitle) String() string {
+	return string(self)
+}
diff --git a/internal/git/gitdomain/local_branch_name.go b/internal/git/gitdomain/local_branch_name.go
index 3297aca1a..481b40099 100644
--- a/internal/git/gitdomain/local_branch_name.go
+++ b/internal/git/gitdomain/local_branch_name.go
@@ -45,7 +45,7 @@ func (self LocalBranchName) RefName() string {
 	return "refs/heads/" + self.String()
 }
 
-// Implementation of the fmt.Stringer interface.
+// String implements the fmt.Stringer interface.
 func (self LocalBranchName) String() string { return string(self) }
 
 // TrackingBranch provides the name of the tracking branch for this local branch.
diff --git a/internal/git/gitdomain/local_branch_names.go b/internal/git/gitdomain/local_branch_names.go
index bb7dee78f..aa794d6dc 100644
--- a/internal/git/gitdomain/local_branch_names.go
+++ b/internal/git/gitdomain/local_branch_names.go
@@ -17,7 +17,7 @@ func NewLocalBranchNames(names ...string) LocalBranchNames {
 	return result
 }
 
-// ParseLocalBranchNamesRef constructs a LocalBranchNames instance
+// ParseLocalBranchNames constructs a LocalBranchNames instance
 // containing the branches listed in the given space-separated string.
 func ParseLocalBranchNames(names string) LocalBranchNames {
 	parts := strings.Split(names, " ")
@@ -51,19 +51,21 @@ func (self LocalBranchNames) Contains(branch LocalBranchName) bool {
 	return slices.Contains(self, branch)
 }
 
-// Hoist moves the given needle to the front of the list.
-func (self LocalBranchNames) Hoist(needle LocalBranchName) LocalBranchNames {
+// Hoist returns the given list with the given branches moved to the front,
+// in the order they are given.
+func (self LocalBranchNames) Hoist(branches ...LocalBranchName) LocalBranchNames {
 	result := make(LocalBranchNames, 0, len(self))
-	foundNeedle := false
-	for _, branch := range self {
-		if branch == needle {
-			foundNeedle = true
-		} else {
+	// Add the hoisted branches first, in the order given
+	for _, branch := range branches {
+		if slices.Contains(self, branch) {
 			result = append(result, branch)
 		}
 	}
-	if foundNeedle {
-		result = append(LocalBranchNames{needle}, result...)
+	// Add the remaining branches in their original order
+	for _, branch := range self {
+		if !slices.Contains(branches, branch) {
+			result = append(result, branch)
+		}
 	}
 	return result
 }
diff --git a/internal/git/gitdomain/location.go b/internal/git/gitdomain/location.go
index 7d07c0024..7b67772ba 100644
--- a/internal/git/gitdomain/location.go
+++ b/internal/git/gitdomain/location.go
@@ -14,7 +14,6 @@ func (self Location) IsRemoteBranchName() bool {
 	return strings.HasPrefix(self.String(), "origin/")
 }
 
-// Implementation of the fmt.Stringer interface.
 func (self Location) String() string {
 	return string(self)
 }
diff --git a/internal/git/gitdomain/proposal_body_file.go b/internal/git/gitdomain/proposal_body_file.go
index 1e4bc0e22..df577cf59 100644
--- a/internal/git/gitdomain/proposal_body_file.go
+++ b/internal/git/gitdomain/proposal_body_file.go
@@ -1,9 +1,9 @@
 package gitdomain
 
-// ProposalBody is the body of a proposal
+// ProposalBodyFile is the body of a proposal.
 type ProposalBodyFile string
 
-// indicates whether the body should be read from STDIN
+// ShouldReadStdin indicates whether the body should be read from STDIN.
 func (self ProposalBodyFile) ShouldReadStdin() bool {
 	return self == "-"
 }
diff --git a/internal/git/gitdomain/remote.go b/internal/git/gitdomain/remote.go
index 21b9ab779..a2cff3fb1 100644
--- a/internal/git/gitdomain/remote.go
+++ b/internal/git/gitdomain/remote.go
@@ -17,7 +17,6 @@ func NewRemote(id string) Option[Remote] {
 	return Some(Remote(id))
 }
 
-// Implementation of the fmt.Stringer interface.
 func (self Remote) String() string {
 	return string(self)
 }
diff --git a/internal/git/gitdomain/remote_branch_name.go b/internal/git/gitdomain/remote_branch_name.go
index 72be66641..899f7b038 100644
--- a/internal/git/gitdomain/remote_branch_name.go
+++ b/internal/git/gitdomain/remote_branch_name.go
@@ -55,7 +55,6 @@ func (self RemoteBranchName) Remote() Remote {
 	return remote
 }
 
-// Implementation of the fmt.Stringer interface.
 func (self RemoteBranchName) String() string {
 	return string(self)
 }
diff --git a/internal/git/gitdomain/sha.go b/internal/git/gitdomain/sha.go
index ffa18e17f..4b307358b 100644
--- a/internal/git/gitdomain/sha.go
+++ b/internal/git/gitdomain/sha.go
@@ -45,12 +45,11 @@ func (self SHA) Location() Location {
 	return Location(string(self))
 }
 
-// Implementation of the fmt.Stringer interface.
 func (self SHA) String() string {
 	return string(self)
 }
 
-// Truncates reduces the length of this SHA to the given length.
+// Truncate reduces the length of this SHA to the given length.
 // This is only for test code.
 // Please use git.Commands.ShortenSHA in production code.
 func (self SHA) Truncate(newLen int) SHA {
diff --git a/internal/git/merge_conflict.go b/internal/git/merge_conflict.go
index a1dd8ad4f..236a27f69 100644
--- a/internal/git/merge_conflict.go
+++ b/internal/git/merge_conflict.go
@@ -7,7 +7,7 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// Everything Git Town needs to know about a merge conflict to determine whether this is a phantom merge conflict.
+// MergeConflict contains everything Git Town needs to know about a merge conflict to determine whether this is a phantom merge conflict.
 type MergeConflict struct {
 	Current Option[Blob] // info about the file on the current branch
 	Parent  Option[Blob] // info about the file on the original parent
diff --git a/internal/git/phantom_conflict.go b/internal/git/phantom_conflict.go
index 0e5e54484..c67b632df 100644
--- a/internal/git/phantom_conflict.go
+++ b/internal/git/phantom_conflict.go
@@ -7,7 +7,7 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// describes a file within an unresolved merge conflict that experiences a phantom merge conflict
+// PhantomConflict describes a file within an unresolved merge conflict that experiences a phantom merge conflict.
 type PhantomConflict struct {
 	FilePath   string
 	Resolution gitdomain.ConflictResolution
diff --git a/internal/git/unmerged_stage.go b/internal/git/unmerged_stage.go
index a21f9c6f7..a98ddd5a6 100644
--- a/internal/git/unmerged_stage.go
+++ b/internal/git/unmerged_stage.go
@@ -1,6 +1,6 @@
 package git
 
-// describes the roles that a file can play in a merge conflict
+// UnmergedStage describes the roles that a file can play in a merge conflict.
 type UnmergedStage int
 
 const (
@@ -9,7 +9,7 @@ const (
 	UnmergedStageIncoming      UnmergedStage = 3 // the file on the branch getting merged in
 )
 
-// all possile UnmergedStages instances
+// UnmergedStages provides all possible UnmergedStage instances.
 var UnmergedStages = []UnmergedStage{
 	UnmergedStageBase,
 	UnmergedStageCurrentBranch,
diff --git a/internal/git/version.go b/internal/git/version.go
index f597ba59a..d75138cd2 100644
--- a/internal/git/version.go
+++ b/internal/git/version.go
@@ -1,17 +1,17 @@
 package git
 
-// the currently installed Git version
+// Version provides the currently installed Git version.
 type Version struct {
 	Major int
 	Minor int
 }
 
-// indicates whether the installed Git version works for Git Town
+// IsMinimumRequiredGitVersion indicates whether the installed Git version works for Git Town.
 func (self Version) IsMinimumRequiredGitVersion() bool {
 	return self.Major > 2 || (self.Major == 2 && self.Minor >= 30)
 }
 
-// an empty version, to be used only in testing
+// EmptyVersion provides an empty version, to be used only in testing.
 func EmptyVersion() Version {
 	return Version{} //exhaustruct:ignore
 }
diff --git a/internal/gohacks/cache/with_previous.go b/internal/gohacks/cache/with_previous.go
index bb6da8f92..e523e0a4e 100644
--- a/internal/gohacks/cache/with_previous.go
+++ b/internal/gohacks/cache/with_previous.go
@@ -25,7 +25,7 @@ func (self *WithPrevious[T]) Invalidate() {
 	self.value = None[T]()
 }
 
-// Sets a new current value.
+// Set assigns a new current value.
 func (self *WithPrevious[T]) Set(newValue T) {
 	self.previous = self.value
 	self.value = Some(newValue)
diff --git a/internal/gohacks/mapstools/merge.go b/internal/gohacks/mapstools/merge.go
index 77319596f..2353fd28d 100644
--- a/internal/gohacks/mapstools/merge.go
+++ b/internal/gohacks/mapstools/merge.go
@@ -2,7 +2,7 @@ package mapstools
 
 import "maps"
 
-// provides the given maps merged together
+// Merge provides the given maps merged together.
 func Merge[K comparable, V any](map1, map2 map[K]V) map[K]V {
 	result := maps.Clone(map1)
 	maps.Copy(result, map2)
diff --git a/internal/gohacks/slice/contains_any.go b/internal/gohacks/slice/contains_any.go
index 84c4e73e6..a4d04c77c 100644
--- a/internal/gohacks/slice/contains_any.go
+++ b/internal/gohacks/slice/contains_any.go
@@ -2,7 +2,7 @@ package slice
 
 import "slices"
 
-// indicates whether the given haystack contains any of the given needles
+// ContainsAny indicates whether the given haystack contains any of the given needles.
 func ContainsAny[C comparable](haystack []C, needles []C) bool {
 	for _, needle := range needles {
 		if slices.Contains(haystack, needle) {
diff --git a/internal/gohacks/slice/get_all.go b/internal/gohacks/slice/get_all.go
index e0983a46b..fb1eebe17 100644
--- a/internal/gohacks/slice/get_all.go
+++ b/internal/gohacks/slice/get_all.go
@@ -2,7 +2,7 @@ package slice
 
 import . "github.com/git-town/git-town/v22/pkg/prelude"
 
-// returns a copy of given slice where all options are unwrapped and None options are removed
+// GetAll returns a copy of given slice where all options are unwrapped and None options are removed.
 func GetAll[T any](slice []Option[T]) []T {
 	result := make([]T, 0, len(slice))
 	for _, elementOpt := range slice {
diff --git a/internal/gohacks/slice/natural_sort.go b/internal/gohacks/slice/natural_sort.go
index ef7f3f809..52c192589 100644
--- a/internal/gohacks/slice/natural_sort.go
+++ b/internal/gohacks/slice/natural_sort.go
@@ -7,7 +7,7 @@ import (
 	"unicode"
 )
 
-// sorts the given elements in natural sort order (https://en.wikipedia.org/wiki/Natural_sort_order)
+// NaturalSort sorts the given elements in natural sort order (https://en.wikipedia.org/wiki/Natural_sort_order).
 func NaturalSort[T fmt.Stringer](list []T) {
 	if len(list) < 2 {
 		return
diff --git a/internal/gohacks/slice/natural_sort_reverse.go b/internal/gohacks/slice/natural_sort_reverse.go
index 8ae4f81c9..19278d26d 100644
--- a/internal/gohacks/slice/natural_sort_reverse.go
+++ b/internal/gohacks/slice/natural_sort_reverse.go
@@ -5,7 +5,7 @@ import (
 	"slices"
 )
 
-// sorts the given elements in reverse natural sort order
+// NaturalSortReverse sorts the given elements in reverse natural sort order
 func NaturalSortReverse[T fmt.Stringer](list []T) {
 	if len(list) < 2 {
 		return
diff --git a/internal/gohacks/slice/stringify.go b/internal/gohacks/slice/stringify.go
index 4257484ee..91984916d 100644
--- a/internal/gohacks/slice/stringify.go
+++ b/internal/gohacks/slice/stringify.go
@@ -2,7 +2,7 @@ package slice
 
 import "fmt"
 
-// provides the string version of the given stringers
+// Stringify provides the string version of the given stringers
 func Stringify[T fmt.Stringer](elements []T) []string {
 	result := make([]string, len(elements))
 	for e, element := range elements {
diff --git a/internal/gohacks/strings.go b/internal/gohacks/strings.go
index 31aee994f..b6d189b9a 100644
--- a/internal/gohacks/strings.go
+++ b/internal/gohacks/strings.go
@@ -6,12 +6,26 @@ func EscapeNewLines(text string) string {
 	return strings.ReplaceAll(text, "\n", "\\n")
 }
 
-// provides the given text where each line is indented by the given amount
+// IndentLines provides the given text where each line is indented by the given amount of spaces.
 func IndentLines(text string, amount int) string {
 	lines := strings.Split(text, "\n")
 	result := make([]string, len(lines))
+	indent := strings.Repeat(" ", amount)
 	for l, line := range lines {
-		result[l] = strings.Repeat(" ", amount) + line
+		result[l] = indent + line
 	}
 	return strings.Join(result, "\n")
 }
+
+// LeadingWhitespace provides the leading whitespace in the given string.
+func LeadingWhitespace(line string) string {
+	result := strings.Builder{}
+	for _, r := range line {
+		if r == ' ' || r == '\t' {
+			result.WriteRune(r)
+		} else {
+			break
+		}
+	}
+	return result.String()
+}
diff --git a/internal/gohacks/stringslice/change_indent_non_empty.go b/internal/gohacks/stringslice/change_indent_non_empty.go
new file mode 100644
index 000000000..2c9e40704
--- /dev/null
+++ b/internal/gohacks/stringslice/change_indent_non_empty.go
@@ -0,0 +1,16 @@
+package stringslice
+
+import "strings"
+
+// ChangeIndentNonEmpty changes the indentation of each non-empty line to the given string.
+func ChangeIndentNonEmpty(lines []string, indentation string) []string {
+	result := make([]string, len(lines))
+	for i, line := range lines {
+		if strings.TrimSpace(line) != "" {
+			result[i] = indentation + strings.TrimLeft(line, " \t")
+		} else {
+			result[i] = line
+		}
+	}
+	return result
+}
diff --git a/internal/gohacks/stringslice/collector.go b/internal/gohacks/stringslice/collector.go
index 364337313..ec2e42346 100644
--- a/internal/gohacks/stringslice/collector.go
+++ b/internal/gohacks/stringslice/collector.go
@@ -1,7 +1,6 @@
 package stringslice
 
-// Collector accumulates string instances one at a time.
-// The zero value is an empty collection.
+// Collector accumulates strings.
 type Collector struct {
 	data *[]string
 }
@@ -13,12 +12,12 @@ func NewCollector() Collector {
 	}
 }
 
-// Add appends a string instance to this collector.
+// Add appends a string to this collector.
 func (self Collector) Add(text string) {
 	*self.data = append(*self.data, text)
 }
 
-// Result provides all accumulated string instances.
+// Result provides all accumulated strings.
 func (self Collector) Result() []string {
 	if self.data == nil {
 		return []string{}
diff --git a/internal/gohacks/stringslice/equal_ignore_whitespace.go b/internal/gohacks/stringslice/equal_ignore_whitespace.go
new file mode 100644
index 000000000..f8420786b
--- /dev/null
+++ b/internal/gohacks/stringslice/equal_ignore_whitespace.go
@@ -0,0 +1,17 @@
+package stringslice
+
+import "strings"
+
+// EqualIgnoreWhitespace indicates whether the given string slices match,
+// ignoring whitespace before and after each string.
+func EqualIgnoreWhitespace(strings1, strings2 []string) bool {
+	if len(strings1) != len(strings2) {
+		return false
+	}
+	for s, string1 := range strings1 {
+		if strings.TrimSpace(string1) != strings.TrimSpace(strings2[s]) {
+			return false
+		}
+	}
+	return true
+}
diff --git a/internal/gohacks/stringslice/locate_section.go b/internal/gohacks/stringslice/locate_section.go
new file mode 100644
index 000000000..06692ce77
--- /dev/null
+++ b/internal/gohacks/stringslice/locate_section.go
@@ -0,0 +1,12 @@
+package stringslice
+
+// LocateSection provides the line number where the given section starts within the given lines.
+func LocateSection(lines, section []string) (start int, found bool) {
+	sectionLength := len(section)
+	for i := 0; i <= len(lines)-sectionLength; i++ {
+		if EqualIgnoreWhitespace(lines[i:i+sectionLength], section) {
+			return i, true
+		}
+	}
+	return -1, false
+}
diff --git a/internal/gohacks/stringslice/non_empty_lines.go b/internal/gohacks/stringslice/non_empty_lines.go
new file mode 100644
index 000000000..9e2110ac6
--- /dev/null
+++ b/internal/gohacks/stringslice/non_empty_lines.go
@@ -0,0 +1,19 @@
+package stringslice
+
+import "strings"
+
+// NonEmptyLines splits the input by newlines and returns non-empty, trimmed lines.
+func NonEmptyLines(output string) []string {
+	if output == "" {
+		return []string{}
+	}
+	lines := strings.Split(output, "\n")
+	result := make([]string, 0, len(lines))
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if line != "" {
+			result = append(result, line)
+		}
+	}
+	return result
+}
diff --git a/internal/gohacks/stringslice/replace_regex.go b/internal/gohacks/stringslice/replace_regex.go
new file mode 100644
index 000000000..6e8939dc0
--- /dev/null
+++ b/internal/gohacks/stringslice/replace_regex.go
@@ -0,0 +1,12 @@
+package stringslice
+
+import "regexp"
+
+// ReplaceRegex replaces all matches of the given regex with the given replacement in the given strings.
+func ReplaceRegex(texts []string, regex *regexp.Regexp, replacement string) []string {
+	result := make([]string, len(texts))
+	for t, text := range texts {
+		result[t] = regex.ReplaceAllString(text, replacement)
+	}
+	return result
+}
diff --git a/internal/gohacks/stringslice/trim_empty_lines.go b/internal/gohacks/stringslice/trim_empty_lines.go
new file mode 100644
index 000000000..cc75b398d
--- /dev/null
+++ b/internal/gohacks/stringslice/trim_empty_lines.go
@@ -0,0 +1,18 @@
+package stringslice
+
+import "strings"
+
+// TrimEmptyLines removes leading and trailing empty lines from the given lines.
+func TrimEmptyLines(lines []string) []string {
+	result := make([]string, 0, len(lines))
+	for _, line := range lines {
+		if len(result) > 0 || strings.TrimSpace(line) != "" {
+			result = append(result, line)
+		}
+	}
+	// Trim trailing empty lines
+	for len(result) > 0 && strings.TrimSpace(result[len(result)-1]) == "" {
+		result = result[:len(result)-1]
+	}
+	return result
+}
diff --git a/internal/gohacks/type_name.go b/internal/gohacks/type_name.go
index 7d4dc05c1..d0ccbfc21 100644
--- a/internal/gohacks/type_name.go
+++ b/internal/gohacks/type_name.go
@@ -3,7 +3,7 @@ package gohacks
 import "reflect"
 
 // TypeName provides the name of the type of the given variable.
-func TypeName(myvar interface{}) string {
+func TypeName(myvar any) string {
 	if myvar == nil {
 		return "nil"
 	}
diff --git a/internal/gohacks/wrap_if_error.go b/internal/gohacks/wrap_if_error.go
index 0cf6a6b7d..49c4610ca 100644
--- a/internal/gohacks/wrap_if_error.go
+++ b/internal/gohacks/wrap_if_error.go
@@ -4,8 +4,7 @@ import (
 	"fmt"
 )
 
-// If the given error is nil, returns nil.
-// If the given error is not nil, returns it wrapped in the given error message.
+// WrapIfError wraps the given error in the given text.
 func WrapIfError(err error, text string, a ...any) error {
 	if err == nil {
 		return nil
diff --git a/internal/messages/en.go b/internal/messages/en.go
index 33b5fdc14..27f2763c8 100644
--- a/internal/messages/en.go
+++ b/internal/messages/en.go
@@ -47,13 +47,17 @@ const (
 	BranchIsNowPrototype            = "branch %q is now a prototype branch\n"
 	BranchLocalProblem              = "cannot determine whether the local branch %q exists: %w"
 	BranchLocalSHAProblem           = "cannot determine SHA of local branch %q: %w"
+	BranchNotAvailable              = "there is no other branch to switch to"
 	BranchNotInSyncWithParent       = `branch %q is not in sync with its parent, please run "git town sync" and try again`
 	BranchOtherWorktree             = `branch %q is active in another worktree`
 	BranchParentChanged             = "branch %q is now a child of %q"
+	BranchPrefixPrompt              = "Branch prefix: "
+	BranchPrefixResult              = "Branch prefix: %s\n"
 	BranchTypeCannotDetermine       = "cannot determine type of branch %q"
 	BrowserOpen                     = "Please open in a browser: %s\n"
 
 	CacheUnitialized                   = "using a cached value before initialization"
+	CannotParse                        = "cannot parse %q: %w"
 	CommandsRun                        = "Ran %d shell commands."
 	CommitMessageProblem               = "cannot determine last commit message: %w"
 	CommitsSelected                    = "%d commits selected"
@@ -69,6 +73,7 @@ const (
 	CompressParkedBranch               = "branch %q and should not compress it"
 	CompressUnsynced                   = "please sync branch %q before compressing it"
 	ConfigBranchTypeOverrideEmpty      = "removing empty branch type entry"
+	ConfigFile                         = "config file"
 	ConfigFileCannotRead               = "cannot read the configuration file %q: %w"
 	ConfigFileInvalidContent           = "the configuration file %q does not contain TOML-formatted content: %w"
 	ConfigLineageEmptyChild            = "removing empty lineage entry"
@@ -77,7 +82,7 @@ const (
 	ConfigNeeded                       = "Git Town needs to be configured\n\n"
 	ConfigRemoveError                  = "unexpected error while removing the 'git-town' section from the Git configuration: %w"
 	ConfigScopeUnknown                 = "unknown configuration scope"
-	ConfigShipStrategyUnknown          = "unknown ship strategy: %q"
+	ConfigShipStrategyUnknown          = "unknown ship strategy in %q: %q"
 	ConfigStorage                      = "Config storage: %s\n"
 	ConfigSyncStrategyUnknown          = "unknown sync strategy: %q"
 	ConflictDetectionProblem           = "cannot determine conflicts: %w"
@@ -118,7 +123,7 @@ Please upgrade to the new format: create.new-branch-type = "prototype"`
 	DevRemote                           = "Development remote: %s\n"
 	DialogResultAll                     = "(all)"
 	DialogResultNone                    = "(none)"
-	DialogResultUnknownBranchType       = "unknown branch type: %q"
+	DialogResultUnknownBranchType       = "unknown branch type defined in %q: %q"
 	DialogUnexpectedResponse            = "unexpected response: %s"
 	DialogUseGlobalValue                = "use global value (%s)"
 	DiffConflictWithMain                = "conflicts between your uncommmitted changes and the main branch"
@@ -150,20 +155,20 @@ Please upgrade to the new format: create.new-branch-type = "prototype"`
 	ForgeGitLabUpdateMRViaAPI    = "Updating target branch for MR !%d to %q ... "
 	ForgejoTokenPrompt           = "Forgejo API token: "
 	ForgejoTokenResult           = "Forgejo token: %s\n"
-	ForgeTypeUnknown             = "unknown forge type: %q"
+	ForgeTypeUnknown             = "unknown forge type defined in %q: %q"
 
 	GitAnotherProcessIsRunningRetry = "another git process seems to be running in this repository, retrying in 1 sec ..."
 	GitDirMissing                   = "cannot determine the '.git' directory: %w"
 	GiteaTokenPrompt                = "Gitea token: "
 	GiteaTokenResult                = "Gitea token: %s\n"
 	GitHubConnectorTypeResult       = "GitHub connector type: %s\n"
-	GitHubConnectorTypeUnknown      = "unknown GitHubConnectorType: %q"
+	GitHubConnectorTypeUnknown      = "unknown GitHubConnectorType defined in %q: %q"
 	GitHubEnterpriseInitializeError = "cannot initialize GitHub Enterprise client: %s"
 	GitHubTokenPrompt               = "GitHub API token: "
 	GitHubTokenResult               = "GitHub token: %s\n"
 	GitLabConnectorTypePrompt       = "GitLab connector type: "
 	GitLabConnectorTypeResult       = "GitLab connector type: %s\n"
-	GitLabConnectorTypeUnknown      = "unknown GitLabConnectorType: %q"
+	GitLabConnectorTypeUnknown      = "unknown GitLabConnectorType defined in %q: %q"
 	GitLabTokenPrompt               = "GitLab token: "
 	GitLabTokenResult               = "GitLab token: %s\n"
 	GitURLCannotParse               = "cannot parse Git URL %q"
@@ -203,12 +208,14 @@ Please upgrade to the new format: create.new-branch-type = "prototype"`
 	ObservedBranchCannotPropose = "cannot propose observed branches"
 	ObservedBranchCannotShip    = "cannot ship observed branches"
 	ObserveDetachedHead         = "please check out the branch to make observed"
+	ObservedRegexCannotParse    = "unknown observed regex value (%q) defined in %q: %w"
 	ObservedRegexPrompt         = "Observed regex: "
 	ObservedRegexResult         = "Observed regex: %s\n"
 	OfflineNotAllowed           = "this command requires an active internet connection"
 	OpcodeUnknown               = "unknown opcode: %q, run \"git town status reset\" to reset it"
 	OpenChangesProblem          = "cannot determine open changes: %w"
 	Order                       = "Order: %s\n"
+	OrderInvalid                = "invalid order defined in %s: %q"
 	OriginHostnamePrompt        = "Origin hostname override: "
 	OriginHostnameResult        = "Origin hostname: %s\n"
 
@@ -234,8 +241,9 @@ Please upgrade to the new format: create.new-branch-type = "prototype"`
 	ProposalMultipleFromToFound             = "found %d proposals from branch %q to branch %q"
 	ProposalNoNumberGiven                   = "no proposal number given"
 	ProposalNoParent                        = "branch %q has no parent and can therefore not be proposed"
+	ProposalsLineage                        = "Proposals display the stack lineage: %s\n"
+	ProposalsLineageInvalid                 = "invalid value for whether proposals should show the lineage in %q: %q. Valid values are: none, ci, cli"
 	ProposalSourceCannotUpdate              = "cannot update the proposal source branch on your forge"
-	ProposalsShowLineageInvalid             = "invalid value for whether proposals should show the lineage: %q. Valid values are: none, ci, cli"
 	ProposalTargetBranchUpdateProblem       = "cannot update the target branch of proposal %d on your forge"
 	ProposalURLProblem                      = "cannot determine proposal URL from %q to %q: %w"
 	ProposeDetached                         = "please check out the branch to propose"
diff --git a/internal/regexes/regexes.go b/internal/regexes/regexes.go
index d2921127b..f24f40c51 100644
--- a/internal/regexes/regexes.go
+++ b/internal/regexes/regexes.go
@@ -2,7 +2,7 @@ package regexes
 
 import "regexp"
 
-// allows filtering by one of multiple regular expressions
+// Regexes allows filtering by one of multiple regular expressions.
 type Regexes []*regexp.Regexp
 
 func NewRegexes(texts []string) (Regexes, error) {
@@ -17,7 +17,7 @@ func NewRegexes(texts []string) (Regexes, error) {
 	return result, nil
 }
 
-// indicates whether the given text matches any of the internal regexes
+// Matches indicates whether the given text matches any of the internal regexes.
 func (self Regexes) Matches(text string) bool {
 	if len(self) == 0 {
 		return true
diff --git a/internal/setup/enter.go b/internal/setup/enter.go
index 38be0f6ee..e958357b3 100644
--- a/internal/setup/enter.go
+++ b/internal/setup/enter.go
@@ -20,36 +20,36 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-func Enter(data Data) (UserInput, dialogdomain.Exit, error) {
+func Enter(data Data) (UserInput, dialogdomain.Exit, bool, error) {
 	var emptyResult UserInput
 	exit, err := dialog.Welcome(data.Inputs)
 	if err != nil || exit {
-		return emptyResult, exit, err
+		return emptyResult, exit, false, err
 	}
 	aliases, exit, err := dialog.Aliases(configdomain.AllAliasableCommands(), data.Config.NormalConfig.Aliases, data.Inputs)
 	if err != nil || exit {
-		return emptyResult, exit, err
+		return emptyResult, exit, false, err
 	}
 	mainBranchSetting, actualMainBranch, exit, err := enterMainBranch(data)
 	if err != nil || exit {
-		return emptyResult, exit, err
+		return emptyResult, exit, false, err
 	}
 	perennialBranches, exit, err := enterPerennialBranches(data, actualMainBranch)
 	if err != nil || exit {
-		return emptyResult, exit, err
+		return emptyResult, exit, false, err
 	}
 EnterForgeData:
 	devRemote, exit, err := enterDevRemote(data)
 	if err != nil || exit {
-		return emptyResult, exit, err
+		return emptyResult, exit, false, err
 	}
 	hostingOriginHostName, exit, err := enterOriginHostName(data)
 	if err != nil || exit {
-		return emptyResult, exit, err
+		return emptyResult, exit, false, err
 	}
 	enteredForgeType, exit, err := enterForgeType(data)
 	if err != nil || exit {
-		return emptyResult, exit, err
+		return emptyResult, exit, false, err
 	}
 	devURL := data.Config.NormalConfig.DevURL(data.Backend)
 	actualForgeType := determineForgeType(enteredForgeType.Or(data.Config.File.ForgeType), devURL)
@@ -68,7 +68,7 @@ EnterForgeData:
 		case forgedomain.ForgeTypeBitbucket, forgedomain.ForgeTypeBitbucketDatacenter:
 			bitbucketUsername, exit, err = enterBitbucketUserName(data)
 			if err != nil || exit {
-				return emptyResult, exit, err
+				return emptyResult, exit, false, err
 			}
 			bitbucketAppPassword, exit, err = enterBitbucketAppPassword(data)
 		case forgedomain.ForgeTypeForgejo:
@@ -78,7 +78,7 @@ EnterForgeData:
 		case forgedomain.ForgeTypeGitHub:
 			githubConnectorTypeOpt, exit, err = enterGitHubConnectorType(data)
 			if err != nil || exit {
-				return emptyResult, exit, err
+				return emptyResult, exit, false, err
 			}
 			if githubConnectorType, has := githubConnectorTypeOpt.Get(); has {
 				switch githubConnectorType {
@@ -90,7 +90,7 @@ EnterForgeData:
 		case forgedomain.ForgeTypeGitLab:
 			gitlabConnectorTypeOpt, exit, err = enterGitLabConnectorType(data)
 			if err != nil || exit {
-				return emptyResult, exit, err
+				return emptyResult, exit, false, err
 			}
 			if gitlabConnectorType, has := gitlabConnectorTypeOpt.Get(); has {
 				switch gitlabConnectorType {
@@ -101,7 +101,7 @@ EnterForgeData:
 			}
 		}
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 	}
 	repeat, exit, err := testForgeAuth(testForgeAuthArgs{
@@ -114,13 +114,13 @@ EnterForgeData:
 		giteaToken:           giteaToken.Or(data.Config.GitGlobal.GiteaToken),
 		githubConnectorType:  githubConnectorTypeOpt.Or(data.Config.GitGlobal.GitHubConnectorType),
 		githubToken:          githubToken.Or(data.Config.GitGlobal.GitHubToken),
-		gitlabConnectorType:  gitlabConnectorTypeOpt.Or(gitlabConnectorTypeOpt),
+		gitlabConnectorType:  gitlabConnectorTypeOpt.Or(data.Config.GitGlobal.GitLabConnectorType),
 		gitlabToken:          gitlabToken.Or(data.Config.GitGlobal.GitLabToken),
 		inputs:               data.Inputs,
 		remoteURL:            data.Config.NormalConfig.RemoteURL(data.Backend, devRemote.GetOr(config.DefaultNormalConfig().DevRemote)),
 	})
 	if err != nil || exit {
-		return emptyResult, exit, err
+		return emptyResult, exit, false, err
 	}
 	if repeat {
 		goto EnterForgeData
@@ -138,17 +138,18 @@ EnterForgeData:
 		inputs:               data.Inputs,
 	})
 	if err != nil || exit {
-		return emptyResult, exit, err
+		return emptyResult, exit, false, err
 	}
 	enterAll, exit, err := dialog.EnterAll(data.Inputs)
 	if err != nil || exit {
-		return emptyResult, exit, err
+		return emptyResult, exit, enterAll, err
 	}
 	autoSync := None[configdomain.AutoSync]()
 	perennialRegex := None[configdomain.PerennialRegex]()
 	featureRegex := None[configdomain.FeatureRegex]()
 	contributionRegex := None[configdomain.ContributionRegex]()
 	observedRegex := None[configdomain.ObservedRegex]()
+	branchPrefix := None[configdomain.BranchPrefix]()
 	order := None[configdomain.Order]()
 	newBranchType := None[configdomain.NewBranchType]()
 	unknownBranchType := None[configdomain.UnknownBranchType]()
@@ -164,91 +165,100 @@ EnterForgeData:
 	pushHook := None[configdomain.PushHook]()
 	shipStrategy := None[configdomain.ShipStrategy]()
 	shipDeleteTrackingBranch := None[configdomain.ShipDeleteTrackingBranch]()
+	proposalsShowLineage := None[forgedomain.ProposalsShowLineage]()
 	if enterAll {
 		perennialRegex, exit, err = enterPerennialRegex(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		featureRegex, exit, err = enterFeatureRegex(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		contributionRegex, exit, err = enterContributionRegex(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		observedRegex, exit, err = enterObservedRegex(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
+		}
+		branchPrefix, exit, err = enterBranchPrefix(data)
+		if err != nil || exit {
+			return emptyResult, exit, false, err
 		}
 		newBranchType, exit, err = enterNewBranchType(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		unknownBranchType, exit, err = enterUnknownBranchType(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		syncFeatureStrategy, exit, err = enterSyncFeatureStrategy(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		syncPerennialStrategy, exit, err = enterSyncPerennialStrategy(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		syncPrototypeStrategy, exit, err = enterSyncPrototypeStrategy(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		syncUpstream, exit, err = enterSyncUpstream(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		autoSync, exit, err = enterAutoSync(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		syncTags, exit, err = enterSyncTags(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		detached, exit, err = enterDetached(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		stash, exit, err = enterStash(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		shareNewBranches, exit, err = enterShareNewBranches(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		pushBranches, exit, err = enterPushBranches(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		pushHook, exit, err = enterPushHook(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		shipStrategy, exit, err = enterShipStrategy(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		shipDeleteTrackingBranch, exit, err = enterShipDeleteTrackingBranch(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
 		}
 		order, exit, err = enterOrder(data)
 		if err != nil || exit {
-			return emptyResult, exit, err
+			return emptyResult, exit, false, err
+		}
+		proposalsShowLineage, exit, err = enterProposalsShowLineage(data)
+		if err != nil || exit {
+			return emptyResult, exit, false, err
 		}
 	}
 	configStorage, exit, err := dialog.ConfigStorage(data.Inputs)
 	if err != nil || exit {
-		return emptyResult, exit, err
+		return emptyResult, exit, false, err
 	}
 	normalData := configdomain.PartialConfig{
 		Aliases:                  aliases,
@@ -256,7 +266,9 @@ EnterForgeData:
 		AutoSync:                 autoSync,
 		BitbucketAppPassword:     bitbucketAppPassword,
 		BitbucketUsername:        bitbucketUsername,
+		BranchPrefix:             branchPrefix,
 		BranchTypeOverrides:      configdomain.BranchTypeOverrides{}, // the setup assistant doesn't ask for this
+		Browser:                  None[configdomain.Browser](),
 		ForgejoToken:             forgejoToken,
 		ContributionRegex:        contributionRegex,
 		Detached:                 detached,
@@ -281,7 +293,7 @@ EnterForgeData:
 		Order:                    order,
 		PerennialBranches:        perennialBranches,
 		PerennialRegex:           perennialRegex,
-		ProposalsShowLineage:     None[forgedomain.ProposalsShowLineage](), // TODO: populate this in the setup assistant once https://github.com/git-town/git-town/issues/3003 is shipped
+		ProposalsShowLineage:     proposalsShowLineage,
 		PushBranches:             pushBranches,
 		PushHook:                 pushHook,
 		ShareNewBranches:         shareNewBranches,
@@ -299,7 +311,7 @@ EnterForgeData:
 	validatedData := configdomain.ValidatedConfigData{
 		MainBranch: actualMainBranch,
 	}
-	return UserInput{normalData, actualForgeType, tokenScope, configStorage, validatedData}, false, nil
+	return UserInput{normalData, actualForgeType, tokenScope, configStorage, validatedData}, false, enterAll, nil
 }
 
 // data entered by the user in the setup assistant
@@ -364,6 +376,17 @@ func enterBitbucketUserName(data Data) (Option[forgedomain.BitbucketUsername], d
 	})
 }
 
+func enterBranchPrefix(data Data) (Option[configdomain.BranchPrefix], dialogdomain.Exit, error) {
+	if data.Config.File.BranchPrefix.IsSome() {
+		return None[configdomain.BranchPrefix](), false, nil
+	}
+	return dialog.BranchPrefix(dialog.Args[configdomain.BranchPrefix]{
+		Global: data.Config.GitGlobal.BranchPrefix,
+		Inputs: data.Inputs,
+		Local:  data.Config.GitLocal.BranchPrefix,
+	})
+}
+
 func enterContributionRegex(data Data) (Option[configdomain.ContributionRegex], dialogdomain.Exit, error) {
 	if data.Config.File.ContributionRegex.IsSome() {
 		return None[configdomain.ContributionRegex](), false, nil
@@ -566,6 +589,17 @@ func enterPerennialRegex(data Data) (Option[configdomain.PerennialRegex], dialog
 	})
 }
 
+func enterProposalsShowLineage(data Data) (Option[forgedomain.ProposalsShowLineage], dialogdomain.Exit, error) {
+	if data.Config.File.ProposalsShowLineage.IsSome() {
+		return None[forgedomain.ProposalsShowLineage](), false, nil
+	}
+	return dialog.ProposalsShowLineage(dialog.Args[forgedomain.ProposalsShowLineage]{
+		Global: data.Config.GitGlobal.ProposalsShowLineage,
+		Inputs: data.Inputs,
+		Local:  data.Config.GitLocal.ProposalsShowLineage,
+	})
+}
+
 func enterPushBranches(data Data) (Option[configdomain.PushBranches], dialogdomain.Exit, error) {
 	if data.Config.File.PushBranches.IsSome() {
 		return None[configdomain.PushBranches](), false, nil
@@ -751,6 +785,7 @@ func testForgeAuth(args testForgeAuthArgs) (repeat bool, exit dialogdomain.Exit,
 		Backend:              args.backend,
 		BitbucketAppPassword: args.bitbucketAppPassword,
 		BitbucketUsername:    args.bitbucketUsername,
+		Browser:              None[configdomain.Browser](),
 		ForgeType:            args.forgeTypeOpt,
 		ForgejoToken:         args.forgejoToken,
 		Frontend:             args.backend,
diff --git a/internal/setup/save.go b/internal/setup/save.go
index 7ec936c9e..e95c0d262 100644
--- a/internal/setup/save.go
+++ b/internal/setup/save.go
@@ -15,7 +15,7 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-func Save(userInput UserInput, unvalidatedConfig config.UnvalidatedConfig, data Data, frontend subshelldomain.Runner) error {
+func Save(userInput UserInput, unvalidatedConfig config.UnvalidatedConfig, data Data, enterAll bool, frontend subshelldomain.Runner) error {
 	fc := gohacks.ErrorCollector{}
 	fc.Check(
 		saveAliases(userInput.Data.Aliases, unvalidatedConfig.GitGlobal.Aliases, frontend),
@@ -54,9 +54,9 @@ func Save(userInput UserInput, unvalidatedConfig config.UnvalidatedConfig, data
 	}
 	switch userInput.StorageLocation {
 	case dialog.ConfigStorageOptionFile:
-		return saveAllToFile(userInput, unvalidatedConfig.GitLocal, frontend)
+		return saveAllToFile(userInput, unvalidatedConfig.File, unvalidatedConfig.GitLocal, frontend)
 	case dialog.ConfigStorageOptionGit:
-		return saveAllToGit(userInput, unvalidatedConfig.GitLocal, unvalidatedConfig.File, data, frontend)
+		return saveAllToGit(userInput, unvalidatedConfig.GitLocal, unvalidatedConfig.File, data, enterAll, frontend)
 	}
 	return nil
 }
@@ -78,14 +78,18 @@ func saveAliases(valuesToWriteToGit configdomain.Aliases, valuesAlreadyInGit con
 	return nil
 }
 
-func saveAllToFile(userInput UserInput, gitConfig configdomain.PartialConfig, runner subshelldomain.Runner) error {
+func saveAllToFile(userInput UserInput, existingConfigFile configdomain.PartialConfig, gitConfig configdomain.PartialConfig, runner subshelldomain.Runner) error {
 	userInput.Data.MainBranch = Some(userInput.ValidatedConfig.MainBranch)
-	if err := configfile.Save(userInput.Data); err != nil {
+	configData := existingConfigFile.Merge(userInput.Data)
+	if err := configfile.Save(configData); err != nil {
 		return err
 	}
 	if gitConfig.AutoSync.IsSome() {
 		_ = gitconfig.RemoveAutoSync(runner)
 	}
+	if gitConfig.BranchPrefix.IsSome() {
+		_ = gitconfig.RemoveBranchPrefix(runner)
+	}
 	if gitConfig.ContributionRegex.IsSome() {
 		_ = gitconfig.RemoveContributionRegex(runner)
 	}
@@ -119,6 +123,9 @@ func saveAllToFile(userInput UserInput, gitConfig configdomain.PartialConfig, ru
 	if gitConfig.ShareNewBranches.IsSome() {
 		_ = gitconfig.RemoveShareNewBranches(runner)
 	}
+	if gitConfig.ProposalsShowLineage.IsSome() {
+		_ = gitconfig.RemoveProposalsShowLineage(runner)
+	}
 	if gitConfig.PushBranches.IsSome() {
 		_ = gitconfig.RemovePushBranches(runner)
 	}
@@ -158,22 +165,26 @@ func saveAllToFile(userInput UserInput, gitConfig configdomain.PartialConfig, ru
 	return saveFeatureRegex(userInput.Data.FeatureRegex, gitConfig.FeatureRegex, runner)
 }
 
-func saveAllToGit(userInput UserInput, existingGitConfig configdomain.PartialConfig, configFile configdomain.PartialConfig, data Data, frontend subshelldomain.Runner) error {
+func saveAllToGit(userInput UserInput, existingGitConfig configdomain.PartialConfig, configFile configdomain.PartialConfig, data Data, enterAll bool, frontend subshelldomain.Runner) error {
 	fc := gohacks.ErrorCollector{}
-	// TODO: sort this alphabetically
-	if configFile.AutoSync.IsNone() {
+
+	// BASIC CONFIGURATION
+	if configFile.MainBranch.IsNone() {
 		fc.Check(
-			saveAutoSync(userInput.Data.AutoSync, existingGitConfig.AutoSync, frontend),
+			saveMainBranch(userInput.Data.MainBranch, existingGitConfig.MainBranch, frontend),
 		)
 	}
-	if configFile.Detached.IsNone() {
+	fc.Check(
+		savePerennialBranches(userInput.Data.PerennialBranches, existingGitConfig.PerennialBranches, frontend),
+	)
+	if len(data.Remotes) > 1 && configFile.DevRemote.IsNone() {
 		fc.Check(
-			saveDetached(userInput.Data.Detached, existingGitConfig.Detached, frontend),
+			saveDevRemote(userInput.Data.DevRemote, existingGitConfig.DevRemote, frontend),
 		)
 	}
-	if configFile.NewBranchType.IsNone() {
+	if configFile.HostingOriginHostname.IsNone() {
 		fc.Check(
-			saveNewBranchType(userInput.Data.NewBranchType, existingGitConfig.NewBranchType, frontend),
+			saveOriginHostname(userInput.Data.HostingOriginHostname, existingGitConfig.HostingOriginHostname, frontend),
 		)
 	}
 	if configFile.ForgeType.IsNone() {
@@ -191,19 +202,33 @@ func saveAllToGit(userInput UserInput, existingGitConfig configdomain.PartialCon
 			saveGitLabConnectorType(userInput.Data.GitLabConnectorType, existingGitConfig.GitLabConnectorType, frontend),
 		)
 	}
-	if configFile.HostingOriginHostname.IsNone() {
+
+	if !enterAll {
+		return fc.Err
+	}
+
+	// EXTENDED CONFIGURATION
+	// TODO: sort this alphabetically
+	if configFile.AutoSync.IsNone() {
 		fc.Check(
-			saveOriginHostname(userInput.Data.HostingOriginHostname, existingGitConfig.HostingOriginHostname, frontend),
+			saveAutoSync(userInput.Data.AutoSync, existingGitConfig.AutoSync, frontend),
 		)
 	}
-	if configFile.MainBranch.IsNone() {
+	if configFile.BranchPrefix.IsNone() {
 		fc.Check(
-			saveMainBranch(userInput.Data.MainBranch, existingGitConfig.MainBranch, frontend),
+			saveBranchPrefix(userInput.Data.BranchPrefix, existingGitConfig.BranchPrefix, frontend),
+		)
+	}
+	if configFile.Detached.IsNone() {
+		fc.Check(
+			saveDetached(userInput.Data.Detached, existingGitConfig.Detached, frontend),
+		)
+	}
+	if configFile.NewBranchType.IsNone() {
+		fc.Check(
+			saveNewBranchType(userInput.Data.NewBranchType, existingGitConfig.NewBranchType, frontend),
 		)
 	}
-	fc.Check(
-		savePerennialBranches(userInput.Data.PerennialBranches, existingGitConfig.PerennialBranches, frontend),
-	)
 	if configFile.PerennialRegex.IsNone() {
 		fc.Check(
 			savePerennialRegex(userInput.Data.PerennialRegex, existingGitConfig.PerennialRegex, frontend),
@@ -214,11 +239,6 @@ func saveAllToGit(userInput UserInput, existingGitConfig configdomain.PartialCon
 			saveUnknownBranchType(userInput.Data.UnknownBranchType, existingGitConfig.UnknownBranchType, frontend),
 		)
 	}
-	if len(data.Remotes) > 1 && configFile.DevRemote.IsNone() {
-		fc.Check(
-			saveDevRemote(userInput.Data.DevRemote, existingGitConfig.DevRemote, frontend),
-		)
-	}
 	if configFile.FeatureRegex.IsNone() {
 		fc.Check(
 			saveFeatureRegex(userInput.Data.FeatureRegex, existingGitConfig.FeatureRegex, frontend),
@@ -239,6 +259,11 @@ func saveAllToGit(userInput UserInput, existingGitConfig configdomain.PartialCon
 			saveOrder(userInput.Data.Order, existingGitConfig.Order, frontend),
 		)
 	}
+	if configFile.ProposalsShowLineage.IsNone() {
+		fc.Check(
+			saveProposalsShowLineage(userInput.Data.ProposalsShowLineage, existingGitConfig.ProposalsShowLineage, frontend),
+		)
+	}
 	if configFile.PushBranches.IsNone() {
 		fc.Check(
 			savePushBranches(userInput.Data.PushBranches, existingGitConfig.PushBranches, frontend),
@@ -329,6 +354,17 @@ func saveBitbucketUsername(valueToWriteToGit Option[forgedomain.BitbucketUsernam
 	return gitconfig.RemoveBitbucketUsername(frontend)
 }
 
+func saveBranchPrefix(valueToWriteToGit Option[configdomain.BranchPrefix], valueAlreadyInGit Option[configdomain.BranchPrefix], runner subshelldomain.Runner) error {
+	if valueToWriteToGit.Equal(valueAlreadyInGit) {
+		return nil
+	}
+	if value, has := valueToWriteToGit.Get(); has {
+		return gitconfig.SetBranchPrefix(runner, value, configdomain.ConfigScopeLocal)
+	}
+	_ = gitconfig.RemoveBranchPrefix(runner)
+	return nil
+}
+
 func saveContributionRegex(valueToWriteToGit Option[configdomain.ContributionRegex], valueAlreadyInGit Option[configdomain.ContributionRegex], runner subshelldomain.Runner) error {
 	if valueToWriteToGit.Equal(valueAlreadyInGit) {
 		return nil
@@ -527,6 +563,16 @@ func savePerennialRegex(valueToWriteToGit Option[configdomain.PerennialRegex], v
 	return nil
 }
 
+func saveProposalsShowLineage(valueToWriteToGit Option[forgedomain.ProposalsShowLineage], valueAlreadyInGit Option[forgedomain.ProposalsShowLineage], runner subshelldomain.Runner) error {
+	if valueAlreadyInGit.Equal(valueToWriteToGit) {
+		return nil
+	}
+	if value, has := valueToWriteToGit.Get(); has {
+		return gitconfig.SetProposalsShowLineage(runner, value, configdomain.ConfigScopeLocal)
+	}
+	return gitconfig.RemoveProposalsShowLineage(runner)
+}
+
 func savePushBranches(valueToWriteToGit Option[configdomain.PushBranches], valueAlreadyInGit Option[configdomain.PushBranches], runner subshelldomain.Runner) error {
 	if valueAlreadyInGit.Equal(valueToWriteToGit) {
 		return nil
diff --git a/internal/subshell/backend_runner.go b/internal/subshell/backend_runner.go
index b3260120e..0ac1c3c24 100644
--- a/internal/subshell/backend_runner.go
+++ b/internal/subshell/backend_runner.go
@@ -2,6 +2,7 @@ package subshell
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"os"
 	"os/exec"
@@ -51,16 +52,16 @@ func (self BackendRunner) execute(env []string, executable string, args ...strin
 	if self.Verbose {
 		printHeader(env, executable, args...)
 	}
-	subProcess := exec.Command(executable, args...) // #nosec
-	subProcess.Env = append(subProcess.Environ(), env...)
-	if dir, has := self.Dir.Get(); has {
-		subProcess.Dir = dir
-	}
 	concurrentGitRetriesLeft := concurrentGitRetries
 	var outputText string
 	var outputBytes []byte
 	var err error
 	for {
+		subProcess := exec.CommandContext(context.Background(), executable, args...) // #nosec
+		subProcess.Env = append(subProcess.Environ(), env...)
+		if dir, has := self.Dir.Get(); has {
+			subProcess.Dir = dir
+		}
 		outputBytes, err = subProcess.CombinedOutput()
 		outputText = string(outputBytes)
 		if err == nil {
diff --git a/internal/subshell/frontend_runner.go b/internal/subshell/frontend_runner.go
index 97664a961..acd936bdd 100644
--- a/internal/subshell/frontend_runner.go
+++ b/internal/subshell/frontend_runner.go
@@ -2,6 +2,7 @@ package subshell
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"io"
 	"os"
@@ -94,7 +95,7 @@ func (self *FrontendRunner) execute(env []string, cmd string, args ...string) (e
 	}
 	concurrentGitRetriesLeft := concurrentGitRetries
 	for {
-		subProcess := exec.Command(cmd, args...)
+		subProcess := exec.CommandContext(context.Background(), cmd, args...)
 		subProcess.Env = append(subProcess.Environ(), env...)
 		var stderrBuffer bytes.Buffer // we only need to look at STDERR since that's where Git will print error messages
 		subProcess.Stderr = io.MultiWriter(os.Stderr, &stderrBuffer)
diff --git a/internal/undo/undostash/stash_diff.go b/internal/undo/undostash/stash_diff.go
index 7cb7d318b..691346297 100644
--- a/internal/undo/undostash/stash_diff.go
+++ b/internal/undo/undostash/stash_diff.go
@@ -15,9 +15,7 @@ type StashDiff struct {
 func NewStashDiff(before, after gitdomain.StashSize) StashDiff {
 	diff := int(after) - int(before)
 	// limit stashes to unpop to at most 1 because Git Town never creates more than 1 stash entry
-	if diff > 1 {
-		diff = 1
-	}
+	diff = min(diff, 1)
 	return StashDiff{
 		EntriesAdded: diff,
 	}
diff --git a/internal/validate/config.go b/internal/validate/config.go
index e10f6b85f..d234729d2 100644
--- a/internal/validate/config.go
+++ b/internal/validate/config.go
@@ -29,11 +29,11 @@ func Config(args ConfigArgs) (config.ValidatedConfig, dialogdomain.Exit, error)
 			Snapshot:      args.ConfigSnapshot,
 		}
 		var exit dialogdomain.Exit
-		userInput, exit, err := setup.Enter(setupData)
+		userInput, exit, enterAll, err := setup.Enter(setupData)
 		if err != nil || exit {
 			return config.EmptyValidatedConfig(), exit, err
 		}
-		err = setup.Save(userInput, args.Unvalidated.Immutable(), setupData, args.Frontend)
+		err = setup.Save(userInput, args.Unvalidated.Immutable(), setupData, enterAll, args.Frontend)
 		if err != nil {
 			return config.EmptyValidatedConfig(), exit, err
 		}
diff --git a/internal/validate/handle_unfinished_state.go b/internal/validate/handle_unfinished_state.go
index 791f2ac72..e00d41534 100644
--- a/internal/validate/handle_unfinished_state.go
+++ b/internal/validate/handle_unfinished_state.go
@@ -56,6 +56,7 @@ func HandleUnfinishedState(args UnfinishedStateArgs) (configdomain.ProgramFlow,
 			Backend:              args.Backend,
 			BitbucketAppPassword: normalConfig.BitbucketAppPassword,
 			BitbucketUsername:    normalConfig.BitbucketUsername,
+			Browser:              normalConfig.Browser,
 			ForgeType:            normalConfig.ForgeType,
 			ForgejoToken:         normalConfig.ForgejoToken,
 			Frontend:             args.Frontend,
diff --git a/internal/vm/opcodes/all.go b/internal/vm/opcodes/all.go
index 3223e0c4c..4f5c2c88b 100644
--- a/internal/vm/opcodes/all.go
+++ b/internal/vm/opcodes/all.go
@@ -19,12 +19,12 @@ func All() []shared.Opcode {
 		&BranchLocalDeleteContent{},
 		&BranchLocalDelete{},
 		&BranchLocalRename{},
-		&BranchLocalSetToSHA{},
 		&BranchRemoteCreate{},
 		&BranchRemoteSetToSHAIfNeeded{},
 		&BranchRemoteSetToSHA{},
 		&BranchReset{},
 		&BranchTrackingCreateIfLocalExists{},
+		&BranchTrackingCreateIfNeeded{},
 		&BranchTrackingCreate{},
 		&BranchTrackingDelete{},
 		&BranchTypeOverrideRemove{},
@@ -34,11 +34,12 @@ func All() []shared.Opcode {
 		&ChangesDiscard{},
 		&ChangesStage{},
 		&ChangesUnstageAll{},
+		&CheckoutDescendentOrOtherIfNeeded{},
+		&CheckoutDescendentOrOther{},
 		&CheckoutFirstExisting{},
 		&CheckoutHistoryPreserve{},
 		&CheckoutIfExists{},
 		&CheckoutIfNeeded{},
-		&CheckoutParentOrMain{},
 		&CheckoutUncached{},
 		&Checkout{},
 		&CherryPickAbort{},
@@ -88,7 +89,6 @@ func All() []shared.Opcode {
 		&PushCurrentBranchForceIfNeeded{},
 		&PushCurrentBranchForceIgnoreError{},
 		&PushCurrentBranchForce{},
-		&PushCurrentBranchIfLocal{},
 		&PushCurrentBranchIfNeeded{},
 		&PushCurrentBranch{},
 		&PushTags{},
diff --git a/internal/vm/opcodes/branch_current_reset_to_parent.go b/internal/vm/opcodes/branch_current_reset_to_parent.go
index 64ba3f0fc..427f96cf3 100644
--- a/internal/vm/opcodes/branch_current_reset_to_parent.go
+++ b/internal/vm/opcodes/branch_current_reset_to_parent.go
@@ -6,7 +6,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/vm/shared"
 )
 
-// ResetCurrentBranch resets all commits in the current branch.
+// BranchCurrentResetToParent resets all commits in the current branch to the parent.
 type BranchCurrentResetToParent struct {
 	CurrentBranch gitdomain.LocalBranchName
 }
diff --git a/internal/vm/opcodes/branch_current_reset_to_sha.go b/internal/vm/opcodes/branch_current_reset_to_sha.go
index 60dac9d02..b3f68cacc 100644
--- a/internal/vm/opcodes/branch_current_reset_to_sha.go
+++ b/internal/vm/opcodes/branch_current_reset_to_sha.go
@@ -8,9 +8,9 @@ import (
 // BranchCurrentResetToSHA undoes all commits on the current branch
 // all the way until the given SHA.
 type BranchCurrentResetToSHA struct {
-	SetToSHA gitdomain.SHA
+	SHA gitdomain.SHA
 }
 
 func (self *BranchCurrentResetToSHA) Run(args shared.RunArgs) error {
-	return args.Git.ResetCurrentBranchToSHA(args.Frontend, self.SetToSHA)
+	return args.Git.ResetCurrentBranchToSHA(args.Frontend, self.SHA)
 }
diff --git a/internal/vm/opcodes/branch_current_reset_to_sha_if_needed.go b/internal/vm/opcodes/branch_current_reset_to_sha_if_needed.go
index f6916200f..b8d67396c 100644
--- a/internal/vm/opcodes/branch_current_reset_to_sha_if_needed.go
+++ b/internal/vm/opcodes/branch_current_reset_to_sha_if_needed.go
@@ -32,7 +32,7 @@ func (self *BranchCurrentResetToSHAIfNeeded) Run(args shared.RunArgs) error {
 		return fmt.Errorf(messages.BranchHasWrongSHA, currentBranch.StringOr(currentSHA.Truncate(7).String()), self.SetToSHA, self.MustHaveSHA, currentSHA)
 	}
 	args.PrependOpcodes(&BranchCurrentResetToSHA{
-		SetToSHA: self.SetToSHA,
+		SHA: self.SetToSHA,
 	})
 	return nil
 }
diff --git a/internal/vm/opcodes/branch_delete_if_empty_at_runtime.go b/internal/vm/opcodes/branch_delete_if_empty_at_runtime.go
index 7ede5da4f..c9c3543ed 100644
--- a/internal/vm/opcodes/branch_delete_if_empty_at_runtime.go
+++ b/internal/vm/opcodes/branch_delete_if_empty_at_runtime.go
@@ -21,7 +21,7 @@ func (self *BranchDeleteIfEmptyAtRuntime) Run(args shared.RunArgs) error {
 	}
 	if !hasUnmergedChanges {
 		args.PrependOpcodes(
-			&CheckoutParentOrMain{
+			&CheckoutDescendentOrOther{
 				Branch: self.Branch,
 			},
 			&BranchTrackingDelete{
diff --git a/internal/vm/opcodes/branch_local_delete_content.go b/internal/vm/opcodes/branch_local_delete_content.go
index 6cc9cdc42..cf5e19c02 100644
--- a/internal/vm/opcodes/branch_local_delete_content.go
+++ b/internal/vm/opcodes/branch_local_delete_content.go
@@ -6,7 +6,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/vm/shared"
 )
 
-// deletes the given branch including all commits
+// BranchLocalDeleteContent deletes the given branch including all commits.
 type BranchLocalDeleteContent struct {
 	BranchToDelete     gitdomain.LocalBranchName
 	BranchToRebaseOnto gitdomain.LocalBranchName
diff --git a/internal/vm/opcodes/branch_local_rename.go b/internal/vm/opcodes/branch_local_rename.go
index 1cb1beed1..63751bba0 100644
--- a/internal/vm/opcodes/branch_local_rename.go
+++ b/internal/vm/opcodes/branch_local_rename.go
@@ -5,7 +5,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/vm/shared"
 )
 
-// CreateBranch creates a new branch but leaves the current branch unchanged.
+// BranchLocalRename renames a local branch.
 type BranchLocalRename struct {
 	NewName gitdomain.LocalBranchName
 	OldName gitdomain.LocalBranchName
diff --git a/internal/vm/opcodes/branch_local_set_to_sha.go b/internal/vm/opcodes/branch_local_set_to_sha.go
deleted file mode 100644
index 8a1f327c6..000000000
--- a/internal/vm/opcodes/branch_local_set_to_sha.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package opcodes
-
-import (
-	"github.com/git-town/git-town/v22/internal/git/gitdomain"
-	"github.com/git-town/git-town/v22/internal/vm/shared"
-)
-
-// BranchLocalSetToSHA sets the given local branch to the given SHA.
-type BranchLocalSetToSHA struct {
-	SHA gitdomain.SHA
-}
-
-func (self *BranchLocalSetToSHA) Run(args shared.RunArgs) error {
-	return args.Git.ResetCurrentBranchToSHA(args.Frontend, self.SHA)
-}
diff --git a/internal/vm/opcodes/push_current_branch_if_local.go b/internal/vm/opcodes/branch_tracking_create_if_needed.go
similarity index 63%
rename from internal/vm/opcodes/push_current_branch_if_local.go
rename to internal/vm/opcodes/branch_tracking_create_if_needed.go
index 9ff2ff94f..9474723c5 100644
--- a/internal/vm/opcodes/push_current_branch_if_local.go
+++ b/internal/vm/opcodes/branch_tracking_create_if_needed.go
@@ -5,12 +5,12 @@ import (
 	"github.com/git-town/git-town/v22/internal/vm/shared"
 )
 
-// PushCurrentBranch pushes the current branch to its existing tracking branch.
-type PushCurrentBranchIfLocal struct {
+// BranchTrackingCreateIfNeeded creates the tracking branch for the current branch if it doesn't exist.
+type BranchTrackingCreateIfNeeded struct {
 	CurrentBranch gitdomain.LocalBranchName
 }
 
-func (self *PushCurrentBranchIfLocal) Run(args shared.RunArgs) error {
+func (self *BranchTrackingCreateIfNeeded) Run(args shared.RunArgs) error {
 	hasTrackingBranch := args.Git.CurrentBranchHasTrackingBranch(args.Backend)
 	if !hasTrackingBranch {
 		args.PrependOpcodes(&BranchTrackingCreate{
diff --git a/internal/vm/opcodes/branch_type_override_remove.go b/internal/vm/opcodes/branch_type_override_remove.go
index 1de1110e8..d5159c172 100644
--- a/internal/vm/opcodes/branch_type_override_remove.go
+++ b/internal/vm/opcodes/branch_type_override_remove.go
@@ -6,7 +6,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/vm/shared"
 )
 
-// removes the branch with the given name from the contribution branches list in the Git config
+// BranchTypeOverrideRemove removes the branch with the given name from the contribution branches list in the Git config.
 type BranchTypeOverrideRemove struct {
 	Branch gitdomain.LocalBranchName
 }
diff --git a/internal/vm/opcodes/branch_type_override_set.go b/internal/vm/opcodes/branch_type_override_set.go
index 55b27f4f1..af207abbc 100644
--- a/internal/vm/opcodes/branch_type_override_set.go
+++ b/internal/vm/opcodes/branch_type_override_set.go
@@ -11,7 +11,7 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// registers the branch with the given name as a contribution branch in the Git config
+// BranchTypeOverrideSet registers the branch with the given name as a contribution branch in the Git config.
 type BranchTypeOverrideSet struct {
 	Branch     gitdomain.LocalBranchName
 	BranchType configdomain.BranchType
diff --git a/internal/vm/opcodes/branch_with_remote_gone_delete_if_empty_at_runtime.go b/internal/vm/opcodes/branch_with_remote_gone_delete_if_empty_at_runtime.go
index 9bdf521b4..af4820906 100644
--- a/internal/vm/opcodes/branch_with_remote_gone_delete_if_empty_at_runtime.go
+++ b/internal/vm/opcodes/branch_with_remote_gone_delete_if_empty_at_runtime.go
@@ -28,7 +28,7 @@ func (self *BranchWithRemoteGoneDeleteIfEmptyAtRuntime) Run(args shared.RunArgs)
 		})
 	} else {
 		args.PrependOpcodes(
-			&CheckoutParentOrMain{
+			&CheckoutDescendentOrOther{
 				Branch: self.Branch,
 			},
 			&BranchLocalDeleteContent{
diff --git a/internal/vm/opcodes/browser_open.go b/internal/vm/opcodes/browser_open.go
index 777a561fe..854db39d8 100644
--- a/internal/vm/opcodes/browser_open.go
+++ b/internal/vm/opcodes/browser_open.go
@@ -11,6 +11,6 @@ type BrowserOpen struct {
 }
 
 func (self *BrowserOpen) Run(args shared.RunArgs) error {
-	browser.Open(self.URL, args.Frontend)
+	browser.Open(self.URL, args.Frontend, args.Config.Value.NormalConfig.Browser)
 	return nil
 }
diff --git a/internal/vm/opcodes/checkout_descendent_or_other.go b/internal/vm/opcodes/checkout_descendent_or_other.go
new file mode 100644
index 000000000..53dc32150
--- /dev/null
+++ b/internal/vm/opcodes/checkout_descendent_or_other.go
@@ -0,0 +1,33 @@
+package opcodes
+
+import (
+	"errors"
+	"slices"
+
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/messages"
+	"github.com/git-town/git-town/v22/internal/vm/shared"
+)
+
+// CheckoutAncestorOrOther checks out the first available ancestor branch of the current branch
+// or any other branch available in the current worktree.
+type CheckoutDescendentOrOther struct {
+	Branch gitdomain.LocalBranchName
+}
+
+func (self *CheckoutDescendentOrOther) Run(args shared.RunArgs) error {
+	availableBranches, err := args.Git.BranchesAvailableInCurrentWorktree(args.Backend)
+	if err != nil {
+		return err
+	}
+	availableBranches = availableBranches.Remove(self.Branch)
+	descendents := args.Config.Value.NormalConfig.Lineage.Descendants(self.Branch, args.Config.Value.NormalConfig.Order)
+	ancestors := args.Config.Value.NormalConfig.Lineage.Ancestors(self.Branch)
+	slices.Reverse(ancestors)
+	branches := availableBranches.Hoist(ancestors...).Hoist(descendents...)
+	for _, branch := range branches {
+		args.PrependOpcodes(&CheckoutIfNeeded{Branch: branch})
+		return nil
+	}
+	return errors.New(messages.BranchNotAvailable)
+}
diff --git a/internal/vm/opcodes/checkout_descendent_or_other_if_needed.go b/internal/vm/opcodes/checkout_descendent_or_other_if_needed.go
new file mode 100644
index 000000000..4278b4917
--- /dev/null
+++ b/internal/vm/opcodes/checkout_descendent_or_other_if_needed.go
@@ -0,0 +1,30 @@
+package opcodes
+
+import (
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/vm/shared"
+)
+
+// CheckoutDescendentOrOtherIfNeeded checks out the first available ancestor branch of the current branch
+// or any other branch available in the current worktree
+// if the current branch is the same as the given branch.
+type CheckoutDescendentOrOtherIfNeeded struct {
+	Branch gitdomain.LocalBranchName
+}
+
+func (self *CheckoutDescendentOrOtherIfNeeded) Run(args shared.RunArgs) error {
+	currentBranchOpt, err := args.Git.CurrentBranch(args.Backend)
+	if err != nil {
+		return err
+	}
+	currentBranch, hasCurrentBranch := currentBranchOpt.Get()
+	if !hasCurrentBranch {
+		// Detached HEAD - not on the branch, so no need to checkout
+		return nil
+	}
+	if currentBranch != self.Branch {
+		return nil
+	}
+	args.PrependOpcodes(&CheckoutDescendentOrOther{Branch: self.Branch})
+	return nil
+}
diff --git a/internal/vm/opcodes/checkout_first_existing.go b/internal/vm/opcodes/checkout_first_existing.go
index 7fd74c8c8..bf9649d88 100644
--- a/internal/vm/opcodes/checkout_first_existing.go
+++ b/internal/vm/opcodes/checkout_first_existing.go
@@ -5,7 +5,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/vm/shared"
 )
 
-// CheckoutIfExists does the same as Checkout
+// CheckoutFirstExisting checks out the first existing branch,
 // but only if that branch actually exists.
 type CheckoutFirstExisting struct {
 	Branches   gitdomain.LocalBranchNames
diff --git a/internal/vm/opcodes/checkout_parent_or_main.go b/internal/vm/opcodes/checkout_parent_or_main.go
deleted file mode 100644
index 863157128..000000000
--- a/internal/vm/opcodes/checkout_parent_or_main.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package opcodes
-
-import (
-	"github.com/git-town/git-town/v22/internal/git/gitdomain"
-	"github.com/git-town/git-town/v22/internal/vm/shared"
-)
-
-// CheckoutParentOrMain checks out the parent branch of the current branch,
-// or the main branch if the current branch has no parent.
-type CheckoutParentOrMain struct {
-	Branch gitdomain.LocalBranchName
-}
-
-func (self *CheckoutParentOrMain) Run(args shared.RunArgs) error {
-	parent := args.Config.Value.NormalConfig.Lineage.Parent(self.Branch).GetOr(args.Config.Value.ValidatedConfigData.MainBranch)
-	args.PrependOpcodes(&CheckoutIfNeeded{Branch: parent})
-	return nil
-}
diff --git a/internal/vm/opcodes/checkout_uncached.go b/internal/vm/opcodes/checkout_uncached.go
index 4601ca961..4975bc0bf 100644
--- a/internal/vm/opcodes/checkout_uncached.go
+++ b/internal/vm/opcodes/checkout_uncached.go
@@ -5,7 +5,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/vm/shared"
 )
 
-// CheckoutIfNeeded checks out a new branch.
+// CheckoutUncached checks out a new branch ignoring the current-branch cache..
 type CheckoutUncached struct {
 	Branch gitdomain.LocalBranchName
 }
diff --git a/internal/vm/opcodes/cherry_pick.go b/internal/vm/opcodes/cherry_pick.go
index a0227b21a..6a66e9ea0 100644
--- a/internal/vm/opcodes/cherry_pick.go
+++ b/internal/vm/opcodes/cherry_pick.go
@@ -5,7 +5,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/vm/shared"
 )
 
-// Checkout checks out the given existing branch.
+// CherryPick cherry-picks the given commit.
 type CherryPick struct {
 	SHA gitdomain.SHA
 }
diff --git a/internal/vm/opcodes/cherry_pick_abort.go b/internal/vm/opcodes/cherry_pick_abort.go
index dc6d3c62d..3d303dc11 100644
--- a/internal/vm/opcodes/cherry_pick_abort.go
+++ b/internal/vm/opcodes/cherry_pick_abort.go
@@ -4,7 +4,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/vm/shared"
 )
 
-// Checkout checks out the given existing branch.
+// CherryPickAbort aborts a suspended cherry-pick operation.
 type CherryPickAbort struct{}
 
 func (self *CherryPickAbort) Run(args shared.RunArgs) error {
diff --git a/internal/vm/opcodes/cherry_pick_continue.go b/internal/vm/opcodes/cherry_pick_continue.go
index 5586152f8..e0d490591 100644
--- a/internal/vm/opcodes/cherry_pick_continue.go
+++ b/internal/vm/opcodes/cherry_pick_continue.go
@@ -4,7 +4,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/vm/shared"
 )
 
-// Checkout checks out the given existing branch.
+// CherryPickContinue continues a suspended cherry-pick operation.
 type CherryPickContinue struct{}
 
 func (self *CherryPickContinue) Run(args shared.RunArgs) error {
diff --git a/internal/vm/opcodes/commit_message_comment_out.go b/internal/vm/opcodes/commit_message_comment_out.go
index 9b8bfff2c..d8d289039 100644
--- a/internal/vm/opcodes/commit_message_comment_out.go
+++ b/internal/vm/opcodes/commit_message_comment_out.go
@@ -8,7 +8,7 @@ import (
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// comments out the currently active commit message
+// CommitMessageCommentOut comments out the currently active commit message.
 type CommitMessageCommentOut struct{}
 
 func (self *CommitMessageCommentOut) Run(args shared.RunArgs) error {
diff --git a/internal/vm/opcodes/commit_remove.go b/internal/vm/opcodes/commit_remove.go
index a3230de7d..30dfce1c0 100644
--- a/internal/vm/opcodes/commit_remove.go
+++ b/internal/vm/opcodes/commit_remove.go
@@ -5,7 +5,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/vm/shared"
 )
 
-// removes the commit with the given SHA from the given branch
+// CommitRemove removes the commit with the given SHA from the given branch.
 type CommitRemove struct {
 	SHA gitdomain.SHA
 }
diff --git a/internal/vm/opcodes/execute_shell_command.go b/internal/vm/opcodes/execute_shell_command.go
index 1f3258263..5a44761fe 100644
--- a/internal/vm/opcodes/execute_shell_command.go
+++ b/internal/vm/opcodes/execute_shell_command.go
@@ -2,8 +2,7 @@ package opcodes
 
 import "github.com/git-town/git-town/v22/internal/vm/shared"
 
-// FetchUpstream brings the Git history of the local repository
-// up to speed with activities that happened in the upstream remote.
+// ExecuteShellCommand executes a shell command.
 type ExecuteShellCommand struct {
 	Args       []string
 	Executable string
diff --git a/internal/vm/opcodes/proposal_create.go b/internal/vm/opcodes/proposal_create.go
index 820014789..d35772eaf 100644
--- a/internal/vm/opcodes/proposal_create.go
+++ b/internal/vm/opcodes/proposal_create.go
@@ -3,6 +3,7 @@ package opcodes
 import (
 	"fmt"
 
+	"github.com/git-town/git-town/v22/internal/forge"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/git/gitdomain"
 	"github.com/git-town/git-town/v22/internal/messages"
@@ -27,7 +28,8 @@ func (self *ProposalCreate) Run(args shared.RunArgs) error {
 	if !hasConnector {
 		return forgedomain.UnsupportedServiceError()
 	}
-	return connector.CreateProposal(forgedomain.CreateProposalArgs{
+
+	err := connector.CreateProposal(forgedomain.CreateProposalArgs{
 		Branch:         self.Branch,
 		FrontendRunner: args.Frontend,
 		MainBranch:     self.MainBranch,
@@ -35,4 +37,35 @@ func (self *ProposalCreate) Run(args shared.RunArgs) error {
 		ProposalBody:   self.ProposalBody,
 		ProposalTitle:  self.ProposalTitle,
 	})
+	if err != nil {
+		return err
+	}
+
+	if args.Config.Value.NormalConfig.ProposalsShowLineage == forgedomain.ProposalsShowLineageCLI {
+		if proposalFinder, canFindProposals := connector.(forgedomain.ProposalFinder); canFindProposals {
+			lineageTree, err := forge.NewProposalStackLineageTree(forge.ProposalStackLineageArgs{
+				Connector:                proposalFinder,
+				CurrentBranch:            self.Branch,
+				Lineage:                  args.Config.Value.NormalConfig.Lineage,
+				MainAndPerennialBranches: args.Config.Value.MainAndPerennials(),
+				Order:                    args.Config.Value.NormalConfig.Order,
+			})
+			if err != nil {
+				// TODO: make sure error message return from failing to construct lineage is consistent across all invocations
+				fmt.Printf("failed to construct proposal stack lineage: %s\n", err.Error())
+			}
+			proposalOpt, err := proposalFinder.FindProposal(self.Branch, parentBranch)
+			if err != nil {
+				return err
+			}
+			if proposalOpt.IsSome() {
+				args.PrependOpcodes(&ProposalUpdateLineage{
+					Current:         self.Branch,
+					CurrentProposal: proposalOpt,
+					LineageTree:     MutableSome(lineageTree),
+				})
+			}
+		}
+	}
+	return nil
 }
diff --git a/internal/vm/opcodes/proposal_update_lineage.go b/internal/vm/opcodes/proposal_update_lineage.go
index 7f13daf2c..e793c4b97 100644
--- a/internal/vm/opcodes/proposal_update_lineage.go
+++ b/internal/vm/opcodes/proposal_update_lineage.go
@@ -15,20 +15,12 @@ type ProposalUpdateLineage struct {
 }
 
 func (self *ProposalUpdateLineage) Run(args shared.RunArgs) error {
-	connector, hasConnector := args.Connector.Get()
-	if !hasConnector {
-		return nil
-	}
 	proposal, hasProposal := self.CurrentProposal.Get()
 	if !hasProposal {
 		return nil
 	}
-	proposalFinder, canFindProposals := connector.(forgedomain.ProposalFinder)
-	if !canFindProposals {
-		return nil
-	}
 	lineageArgs := forge.ProposalStackLineageArgs{
-		Connector:                proposalFinder,
+		Connector:                forgedomain.ProposalFinderFromConnector(args.Connector),
 		CurrentBranch:            self.Current,
 		Lineage:                  args.Config.Value.NormalConfig.Lineage,
 		MainAndPerennialBranches: args.Config.Value.MainAndPerennials(),
diff --git a/internal/vm/opcodes/rebase_ancestor_local.go b/internal/vm/opcodes/rebase_ancestor_local.go
index ec12c2329..b86b4ccf4 100644
--- a/internal/vm/opcodes/rebase_ancestor_local.go
+++ b/internal/vm/opcodes/rebase_ancestor_local.go
@@ -2,12 +2,11 @@ package opcodes
 
 import (
 	"github.com/git-town/git-town/v22/internal/git/gitdomain"
-	"github.com/git-town/git-town/v22/internal/messages"
 	"github.com/git-town/git-town/v22/internal/vm/shared"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
-// rebases a branch against a local ancestor branch
+// RebaseAncestorLocal rebases a branch against a local ancestor branch.
 type RebaseAncestorLocal struct {
 	Ancestor        gitdomain.LocalBranchName
 	Branch          gitdomain.LocalBranchName
@@ -15,20 +14,11 @@ type RebaseAncestorLocal struct {
 }
 
 func (self *RebaseAncestorLocal) Run(args shared.RunArgs) error {
-	branchInfos, hasBranchInfos := args.BranchInfos.Get()
-	if !hasBranchInfos {
-		panic(messages.BranchInfosNotProvided)
-	}
-	var branchToRebaseOnto gitdomain.BranchName
-	if branchInfos.BranchIsActiveInAnotherWorktree(self.Ancestor) {
-		branchToRebaseOnto = self.Ancestor.TrackingBranch(args.Config.Value.NormalConfig.DevRemote).BranchName()
-	} else {
-		branchToRebaseOnto = self.Ancestor.BranchName()
-	}
+	ancestorBranchName := self.Ancestor.BranchName()
 	commitsToRemove, hasCommitsToRemove := self.CommitsToRemove.Get()
 	ancestorSHA := None[gitdomain.SHA]()
 	if hasCommitsToRemove {
-		sha, err := args.Git.SHAForBranch(args.Backend, branchToRebaseOnto)
+		sha, err := args.Git.SHAForBranch(args.Backend, ancestorBranchName)
 		if err != nil {
 			return err
 		}
@@ -39,17 +29,17 @@ func (self *RebaseAncestorLocal) Run(args shared.RunArgs) error {
 		// This removes old versions of commits that were amended by the user.
 		// The new commits of the parent get added back during the rebase.
 		args.PrependOpcodes(&RebaseOnto{
-			BranchToRebaseOnto: branchToRebaseOnto,
+			BranchToRebaseOnto: ancestorBranchName,
 			CommitsToRemove:    commitsToRemove.Location(),
 		})
 	} else {
-		isInSync, err := args.Git.BranchInSyncWithParent(args.Backend, self.Branch, branchToRebaseOnto)
+		isInSync, err := args.Git.BranchInSyncWithParent(args.Backend, self.Branch, ancestorBranchName)
 		if err != nil {
 			return err
 		}
 		if !isInSync {
 			args.PrependOpcodes(&RebaseBranch{
-				Branch: branchToRebaseOnto,
+				Branch: ancestorBranchName,
 			})
 		}
 	}
diff --git a/internal/vm/opcodes/rebase_ancestor_remote.go b/internal/vm/opcodes/rebase_ancestor_remote.go
index fecbeb6bf..68804973f 100644
--- a/internal/vm/opcodes/rebase_ancestor_remote.go
+++ b/internal/vm/opcodes/rebase_ancestor_remote.go
@@ -5,7 +5,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/vm/shared"
 )
 
-// rebases a branch against its ancestor branch, which exists only remotely
+// RebaseAncestorRemote rebases a branch against its ancestor branch, which exists only remotely.
 type RebaseAncestorRemote struct {
 	Ancestor gitdomain.RemoteBranchName
 	Branch   gitdomain.LocalBranchName
diff --git a/internal/vm/opcodes/sync_feature_branch_merge.go b/internal/vm/opcodes/sync_feature_branch_merge.go
index c6a615c7f..70a5bee7c 100644
--- a/internal/vm/opcodes/sync_feature_branch_merge.go
+++ b/internal/vm/opcodes/sync_feature_branch_merge.go
@@ -34,20 +34,14 @@ func (self *SyncFeatureBranchMerge) Run(args shared.RunArgs) error {
 		if parentBranchInfo, hasParentInfo := branchInfos.FindLocalOrRemote(parent, args.Config.Value.NormalConfig.DevRemote).Get(); hasParentInfo {
 			parentIsLocal := parentBranchInfo.LocalName.IsSome()
 			if parentIsLocal {
-				var parentToMerge gitdomain.BranchName
-				if branchInfos.BranchIsActiveInAnotherWorktree(parent) {
-					parentToMerge = parent.TrackingBranch(args.Config.Value.NormalConfig.DevRemote).BranchName()
-				} else {
-					parentToMerge = parent.BranchName()
-				}
-				isInSync, err := args.Git.BranchInSyncWithParent(args.Backend, self.Branch, parentToMerge)
+				isInSync, err := args.Git.BranchInSyncWithParent(args.Backend, self.Branch, parent.BranchName())
 				if err != nil {
 					return err
 				}
 				if !isInSync {
 					program = append(program, &MergeParentResolvePhantomConflicts{
 						CurrentBranch:     self.Branch,
-						CurrentParent:     parentToMerge,
+						CurrentParent:     parent.BranchName(),
 						InitialParentName: self.InitialParentName,
 						InitialParentSHA:  self.InitialParentSHA,
 					})
diff --git a/internal/vm/program/json.go b/internal/vm/program/json.go
index 69ab70224..fe64826c9 100644
--- a/internal/vm/program/json.go
+++ b/internal/vm/program/json.go
@@ -17,7 +17,7 @@ type JSON struct {
 
 // MarshalJSON marshals the opcode to JSON.
 func (self *JSON) MarshalJSON() ([]byte, error) {
-	return json.Marshal(map[string]interface{}{
+	return json.Marshal(map[string]any{
 		"data": self.Opcode,
 		"type": gohacks.TypeName(self.Opcode),
 	})
diff --git a/internal/vm/shared/branches_in_opcode.go b/internal/vm/shared/branches_in_opcode.go
index 0c2454e3b..e4343d07b 100644
--- a/internal/vm/shared/branches_in_opcode.go
+++ b/internal/vm/shared/branches_in_opcode.go
@@ -13,19 +13,19 @@ func BranchesInOpcode(code Opcode) []gitdomain.BranchName {
 	for i := range valueOfSelf.NumField() {
 		field := valueOfSelf.Field(i)
 		fieldType := typeOfSelf.Field(i).Type
-		if fieldType == reflect.TypeOf((*gitdomain.BranchName)(nil)).Elem() {
+		if fieldType == reflect.TypeFor[gitdomain.BranchName]() {
 			branchName := field.Interface().(gitdomain.BranchName)
 			result = append(result, branchName)
 		}
-		if fieldType == reflect.TypeOf((*gitdomain.LocalBranchName)(nil)).Elem() {
+		if fieldType == reflect.TypeFor[gitdomain.LocalBranchName]() {
 			localBranchName := field.Interface().(gitdomain.LocalBranchName)
 			result = append(result, localBranchName.BranchName())
 		}
-		if fieldType == reflect.TypeOf((*gitdomain.LocalBranchNames)(nil)).Elem() {
+		if fieldType == reflect.TypeFor[gitdomain.LocalBranchNames]() {
 			localBranchNames := field.Interface().(gitdomain.LocalBranchNames)
 			result = append(result, localBranchNames.BranchNames()...)
 		}
-		if fieldType == reflect.TypeOf((*gitdomain.RemoteBranchName)(nil)).Elem() {
+		if fieldType == reflect.TypeFor[gitdomain.RemoteBranchName]() {
 			remoteBranchName := field.Interface().(gitdomain.RemoteBranchName)
 			result = append(result, remoteBranchName.BranchName())
 		}
diff --git a/internal/vm/shared/opcode.go b/internal/vm/shared/opcode.go
index f5f39b639..033f2e458 100644
--- a/internal/vm/shared/opcode.go
+++ b/internal/vm/shared/opcode.go
@@ -7,7 +7,7 @@ package shared
 // Opcodes implement the command pattern (https://en.wikipedia.org/wiki/Command_pattern)
 // and provide opcodes to continue and abort them.
 // Undoing an opcode is done via the undo package.
-type Opcode interface{}
+type Opcode any
 
 // Runnable marks an opcode that can execute subshell commands.
 type Runnable interface {
diff --git a/tools/package-lock.json b/package-lock.json
similarity index 84%
rename from tools/package-lock.json
rename to package-lock.json
index 1117c6e3d..19115816a 100644
--- a/tools/package-lock.json
+++ b/package-lock.json
@@ -4,30 +4,32 @@
   "requires": true,
   "packages": {
     "": {
-      "dependencies": {
+      "devDependencies": {
         "gherkin-lint": "4.2.4",
-        "text-runner": "7.1.2",
-        "textrun-make": "0.3.1"
+        "text-runner": "7.4.0",
+        "textrun-make": "0.6.0"
       }
     },
     "node_modules/@babel/code-frame": {
-      "version": "7.26.2",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
-      "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+      "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.27.1",
         "js-tokens": "^4.0.0",
-        "picocolors": "^1.0.0"
+        "picocolors": "^1.1.1"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.25.9",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
-      "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=6.9.0"
@@ -37,6 +39,7 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/@fengmk2/ps-tree/-/ps-tree-2.0.2.tgz",
       "integrity": "sha512-CN5YjfoIrNSxVQssQsvsvcehWDt5VYSEKamOsDHWrepOfTIOxBCIXULf1cscNpaz2NW6/GqM3FMddwm5ctxXow==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "event-stream": "^4.0.1"
@@ -48,10 +51,18 @@
         "node": ">= 18.19.0"
       }
     },
+    "node_modules/@keyv/serialize": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
+      "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
       "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@nodelib/fs.stat": "2.0.5",
@@ -65,6 +76,7 @@
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
       "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 8"
@@ -74,6 +86,7 @@
       "version": "1.2.8",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
       "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@nodelib/fs.scandir": "2.1.5",
@@ -87,30 +100,35 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
       "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+      "dev": true,
       "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/base64": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
       "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+      "dev": true,
       "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/codegen": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
       "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+      "dev": true,
       "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/eventemitter": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
       "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+      "dev": true,
       "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/fetch": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
       "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+      "dev": true,
       "license": "BSD-3-Clause",
       "dependencies": {
         "@protobufjs/aspromise": "^1.1.1",
@@ -121,42 +139,49 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
       "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+      "dev": true,
       "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/inquire": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
       "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+      "dev": true,
       "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/path": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
       "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+      "dev": true,
       "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/pool": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
       "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+      "dev": true,
       "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/utf8": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
       "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+      "dev": true,
       "license": "BSD-3-Clause"
     },
     "node_modules/@sec-ant/readable-stream": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
       "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/@sindresorhus/is": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.1.tgz",
-      "integrity": "sha512-QWLl2P+rsCJeofkDNIT3WFmb6NrRud1SUYW8dIhXK/46XFV8Q/g7Bsvib0Askb0reRLe+WYPeeE+l5cH7SlkuQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.1.tgz",
+      "integrity": "sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=18"
@@ -166,9 +191,10 @@
       }
     },
     "node_modules/@sindresorhus/merge-streams": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
-      "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
+      "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=18"
@@ -178,31 +204,30 @@
       }
     },
     "node_modules/@sindresorhus/slugify": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz",
-      "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-3.0.0.tgz",
+      "integrity": "sha512-SCrKh1zS96q+CuH5GumHcyQEVPsM4Ve8oE0E6tw7AAhGq50K8ojbTUOQnX/j9Mhcv/AXiIsbCfquovyGOo5fGw==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@sindresorhus/transliterate": "^1.0.0",
+        "@sindresorhus/transliterate": "^2.0.0",
         "escape-string-regexp": "^5.0.0"
       },
       "engines": {
-        "node": ">=12"
+        "node": ">=20"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/@sindresorhus/transliterate": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz",
-      "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-2.2.0.tgz",
+      "integrity": "sha512-aLtANYAx3qvFilasPhZke27+Cm7WawGGuGiOd2EAp0lg1NdWKfulslcql/Qi7lNQ9odkPjwRbwk9c4CYsTh+Rg==",
+      "dev": true,
       "license": "MIT",
-      "dependencies": {
-        "escape-string-regexp": "^5.0.0"
-      },
       "engines": {
-        "node": ">=12"
+        "node": ">=20"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
@@ -212,6 +237,7 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
       "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "defer-to-connect": "^2.0.1"
@@ -224,33 +250,38 @@
       "version": "4.0.4",
       "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
       "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/@types/long": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
       "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/@types/node": {
-      "version": "22.13.5",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz",
-      "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==",
+      "version": "24.10.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz",
+      "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
-        "undici-types": "~6.20.0"
+        "undici-types": "~7.16.0"
       }
     },
     "node_modules/@types/uuid": {
       "version": "3.4.13",
       "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.13.tgz",
       "integrity": "sha512-pAeZeUbLE4Z9Vi9wsWV2bYPTweEHeJJy0G4pEjOA/FSvy1Ad5U5Km8iDV6TKre1mjBiVNfAdVHKruP8bAh4Q5A==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/anchor-markdown-header": {
       "version": "0.7.0",
       "resolved": "https://registry.npmjs.org/anchor-markdown-header/-/anchor-markdown-header-0.7.0.tgz",
       "integrity": "sha512-OYUgo5tYN+WipGB2R3sZUvXLpTzqSnm0b3AXRqN9Rhi2Z+uCQsFSguDDgpoSdhDrnu/aCYHFGHosdL9hbaWSHA==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "emoji-regex": "~10.1.0",
@@ -261,12 +292,14 @@
       "version": "10.1.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.1.0.tgz",
       "integrity": "sha512-xAEnNCT3w2Tg6MA7ly6QqYJvEoY1tm9iIjJ3yMKK9JPlWuRHAMoe5iETwQnx3M9TVbFMfsrBgWKR+IsmswwNjg==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/ansi-regex": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -276,18 +309,21 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true,
       "license": "Python-2.0"
     },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0",
@@ -298,6 +334,7 @@
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
       "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "fill-range": "^7.1.1"
@@ -310,39 +347,59 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
       "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/cacheable-lookup": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
       "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=14.16"
       }
     },
     "node_modules/cacheable-request": {
-      "version": "12.0.1",
-      "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz",
-      "integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==",
+      "version": "13.0.13",
+      "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.13.tgz",
+      "integrity": "sha512-/5/6xDTN/AZHOn93MNaU7HUSzOL/YJWOwa5hV84ib1mPUtQB/ZTg5bn57UGfdNwXGhgQfcPpJDy+eGiNgGJI1w==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@types/http-cache-semantics": "^4.0.4",
         "get-stream": "^9.0.1",
-        "http-cache-semantics": "^4.1.1",
-        "keyv": "^4.5.4",
+        "http-cache-semantics": "^4.2.0",
+        "keyv": "^5.5.3",
         "mimic-response": "^4.0.0",
-        "normalize-url": "^8.0.1",
+        "normalize-url": "^8.1.0",
         "responselike": "^3.0.0"
       },
       "engines": {
         "node": ">=18"
       }
     },
+    "node_modules/cacheable-request/node_modules/responselike": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
+      "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lowercase-keys": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/cli-cursor": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
       "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "restore-cursor": "^5.0.0"
@@ -358,6 +415,7 @@
       "version": "3.12.0",
       "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
       "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "string-width": "^4.2.3"
@@ -366,16 +424,11 @@
         "node": ">=4"
       }
     },
-    "node_modules/colorette": {
-      "version": "2.0.20",
-      "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
-      "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
-      "license": "MIT"
-    },
     "node_modules/commander": {
       "version": "11.0.0",
       "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz",
       "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=16"
@@ -385,12 +438,14 @@
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/core-js": {
       "version": "3.33.1",
       "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.1.tgz",
       "integrity": "sha512-qVSq3s+d4+GsqN0teRCJtM6tdEEXyWxjzbhVrCHmBS5ZTM0FS2MOS0D13dUXAWDUN6a+lHI/N1hF9Ytz6iLl9Q==",
+      "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
       "funding": {
@@ -403,6 +458,7 @@
       "resolved": "https://registry.npmjs.org/cucumber-messages/-/cucumber-messages-8.0.0.tgz",
       "integrity": "sha512-lUnWRMjwA9+KhDec/5xRZV3Du67ISumHnVLywWQXyvzmc4P+Eqx8CoeQrBQoau3Pw1hs4kJLTDyV85hFBF00SQ==",
       "deprecated": "This package is now published under @cucumber/messages",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@types/uuid": "^3.4.6",
@@ -411,9 +467,10 @@
       }
     },
     "node_modules/decamelize": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz",
-      "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz",
+      "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
@@ -426,6 +483,7 @@
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
       "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "mimic-response": "^3.1.0"
@@ -441,6 +499,7 @@
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
       "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=10"
@@ -453,6 +512,7 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
       "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=10"
@@ -462,18 +522,21 @@
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
       "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/emoji-regex": {
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/end-child-processes": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/end-child-processes/-/end-child-processes-2.0.3.tgz",
       "integrity": "sha512-M3YAahyQkANA2VFPg5ywqlZF1vOey03sxpesRYz346STOawA5YBnAgbKmjcRHijiG73ELFMCrpDq5Xl0txrQeg==",
+      "dev": true,
       "license": "ISC",
       "dependencies": {
         "@fengmk2/ps-tree": "2.0.2"
@@ -483,6 +546,7 @@
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
       "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "dev": true,
       "license": "BSD-2-Clause",
       "engines": {
         "node": ">=0.12"
@@ -495,6 +559,7 @@
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
       "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=12"
@@ -507,6 +572,7 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz",
       "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "duplexer": "^0.1.1",
@@ -522,6 +588,7 @@
       "version": "3.3.3",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
       "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@nodelib/fs.stat": "^2.0.2",
@@ -538,6 +605,7 @@
       "version": "1.19.1",
       "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
       "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+      "dev": true,
       "license": "ISC",
       "dependencies": {
         "reusify": "^1.0.4"
@@ -547,6 +615,7 @@
       "version": "7.1.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
       "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "to-regex-range": "^5.0.1"
@@ -556,9 +625,10 @@
       }
     },
     "node_modules/form-data-encoder": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz",
-      "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz",
+      "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 18"
@@ -568,18 +638,21 @@
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
       "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
       "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true,
       "license": "ISC"
     },
     "node_modules/get-stream": {
       "version": "9.0.1",
       "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
       "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@sec-ant/readable-stream": "^0.4.1",
@@ -597,6 +670,7 @@
       "resolved": "https://registry.npmjs.org/gherkin/-/gherkin-9.0.0.tgz",
       "integrity": "sha512-6xoAepoxo5vhkBXjB4RCfVnSKHu5z9SqXIQVUyj+Jw8BQX8odATlee5otXgdN8llZvyvHokuvNiBeB3naEnnIQ==",
       "deprecated": "This package is now published under @cucumber/gherkin",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "commander": "^4.0.1",
@@ -611,6 +685,7 @@
       "version": "4.2.4",
       "resolved": "https://registry.npmjs.org/gherkin-lint/-/gherkin-lint-4.2.4.tgz",
       "integrity": "sha512-iM+ECIHOF6Wh94YIF1hSHA6JH9rzcgozlMLHA/uCzGtQiMjb/uL093eh1nTpfoJ/38veL7Jfh4yY2inu7uUoFA==",
+      "dev": true,
       "license": "ISC",
       "dependencies": {
         "commander": "11.0.0",
@@ -632,6 +707,7 @@
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
       "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 6"
@@ -642,6 +718,7 @@
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
       "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
       "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
       "license": "ISC",
       "dependencies": {
         "fs.realpath": "^1.0.0",
@@ -662,6 +739,7 @@
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
       "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
       "license": "ISC",
       "dependencies": {
         "is-glob": "^4.0.1"
@@ -671,41 +749,44 @@
       }
     },
     "node_modules/globby": {
-      "version": "14.1.0",
-      "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz",
-      "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==",
+      "version": "15.0.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-15.0.0.tgz",
+      "integrity": "sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@sindresorhus/merge-streams": "^2.1.0",
+        "@sindresorhus/merge-streams": "^4.0.0",
         "fast-glob": "^3.3.3",
-        "ignore": "^7.0.3",
+        "ignore": "^7.0.5",
         "path-type": "^6.0.0",
         "slash": "^5.1.0",
         "unicorn-magic": "^0.3.0"
       },
       "engines": {
-        "node": ">=18"
+        "node": ">=20"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/got": {
-      "version": "14.4.6",
-      "resolved": "https://registry.npmjs.org/got/-/got-14.4.6.tgz",
-      "integrity": "sha512-rnhwfM/PhMNJ1i17k3DuDqgj0cKx3IHxBKVv/WX1uDKqrhi2Gv3l7rhPThR/Cc6uU++dD97W9c8Y0qyw9x0jag==",
+      "version": "14.5.0",
+      "resolved": "https://registry.npmjs.org/got/-/got-14.5.0.tgz",
+      "integrity": "sha512-rZ8+ZMYz9yeTFmFtOiwf/Trcli8gdqgqAz0kmkEOWcT7C3BFG3Z+dLQu0y8je7RogG7Wp1obnu5GerwCaccftw==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@sindresorhus/is": "^7.0.1",
         "@szmarczak/http-timer": "^5.0.1",
         "cacheable-lookup": "^7.0.0",
-        "cacheable-request": "^12.0.1",
+        "cacheable-request": "^13.0.12",
         "decompress-response": "^6.0.0",
         "form-data-encoder": "^4.0.2",
         "http2-wrapper": "^2.2.1",
+        "keyv": "^5.5.3",
         "lowercase-keys": "^3.0.0",
         "p-cancelable": "^4.0.1",
-        "responselike": "^3.0.0",
+        "responselike": "^4.0.2",
         "type-fest": "^4.26.1"
       },
       "engines": {
@@ -716,15 +797,17 @@
       }
     },
     "node_modules/http-cache-semantics": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
-      "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+      "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
+      "dev": true,
       "license": "BSD-2-Clause"
     },
     "node_modules/http2-wrapper": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz",
       "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "quick-lru": "^5.1.1",
@@ -735,9 +818,10 @@
       }
     },
     "node_modules/humanize-string": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/humanize-string/-/humanize-string-3.0.0.tgz",
-      "integrity": "sha512-jhWD2GAZRMELz0IEIfqpEdi0M4CMQF1GpJpBYIopFN6wT+78STiujfQTKcKqZzOJgUkIgJSo2xFeHdsg922JZQ==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/humanize-string/-/humanize-string-3.1.0.tgz",
+      "integrity": "sha512-wEtOOR3sT8nZ7W0WZwEXo68z2EA0kpcpjaN/ZXxpps9PDzZdX8+TAj+XOGKx2WkagP9mnhWtFpfMNSd2WAhiIQ==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "decamelize": "^6.0.0"
@@ -750,9 +834,10 @@
       }
     },
     "node_modules/ignore": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz",
-      "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==",
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+      "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 4"
@@ -763,6 +848,7 @@
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
       "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+      "dev": true,
       "license": "ISC",
       "dependencies": {
         "once": "^1.3.0",
@@ -773,12 +859,14 @@
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true,
       "license": "ISC"
     },
     "node_modules/is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
       "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
@@ -788,6 +876,7 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
       "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -797,6 +886,7 @@
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
       "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "is-extglob": "^2.1.1"
@@ -809,6 +899,7 @@
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
       "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.12.0"
@@ -818,6 +909,7 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
       "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=18"
@@ -830,33 +922,31 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
       "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-      "license": "MIT"
-    },
-    "node_modules/json-buffer": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
-      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/jsonc-reader": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/jsonc-reader/-/jsonc-reader-0.3.0.tgz",
       "integrity": "sha512-VhccWXRd96QqRRo0OLqwpRdX7fVBktUQhVnVvlDYuy6gpie/+TR9kQHkcdUHkK1MnxrFHTWFZmr6Dna5WoTtxw==",
+      "dev": true,
       "license": "ISC"
     },
     "node_modules/keyv": {
-      "version": "4.5.4",
-      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
-      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz",
+      "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
-        "json-buffer": "3.0.1"
+        "@keyv/serialize": "^1.1.1"
       }
     },
     "node_modules/linkify-it": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
       "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "uc.micro": "^2.0.0"
@@ -866,18 +956,21 @@
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/long": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
       "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
+      "dev": true,
       "license": "Apache-2.0"
     },
     "node_modules/lowercase-keys": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
       "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
@@ -890,12 +983,14 @@
       "version": "0.0.7",
       "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz",
       "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/markdown-it": {
       "version": "14.1.0",
       "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
       "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "argparse": "^2.0.1",
@@ -913,12 +1008,14 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
       "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
       "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 8"
@@ -928,6 +1025,7 @@
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
       "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "braces": "^3.0.3",
@@ -941,6 +1039,7 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
       "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=18"
@@ -953,6 +1052,7 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
       "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
@@ -965,6 +1065,7 @@
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
       "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
       "license": "ISC",
       "dependencies": {
         "brace-expansion": "^1.1.7"
@@ -977,15 +1078,17 @@
       "version": "1.2.8",
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
       "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "dev": true,
       "license": "MIT",
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/normalize-url": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz",
-      "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==",
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz",
+      "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=14.16"
@@ -998,6 +1101,7 @@
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
       "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
       "license": "ISC",
       "dependencies": {
         "wrappy": "1"
@@ -1007,6 +1111,7 @@
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
       "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "mimic-function": "^5.0.0"
@@ -1022,27 +1127,43 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz",
       "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=14.16"
       }
     },
     "node_modules/parse5": {
-      "version": "7.2.1",
-      "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
-      "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
+      "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
-        "entities": "^4.5.0"
+        "entities": "^6.0.0"
       },
       "funding": {
         "url": "https://github.com/inikulin/parse5?sponsor=1"
       }
     },
+    "node_modules/parse5/node_modules/entities": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+      "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
     "node_modules/path-is-absolute": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
       "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
@@ -1052,6 +1173,7 @@
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz",
       "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=18"
@@ -1064,6 +1186,7 @@
       "version": "0.0.11",
       "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
       "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==",
+      "dev": true,
       "license": [
         "MIT",
         "Apache2"
@@ -1076,12 +1199,14 @@
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
       "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
       "license": "ISC"
     },
     "node_modules/picomatch": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
       "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8.6"
@@ -1094,6 +1219,7 @@
       "version": "6.11.4",
       "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
       "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==",
+      "dev": true,
       "hasInstallScript": true,
       "license": "BSD-3-Clause",
       "dependencies": {
@@ -1120,6 +1246,7 @@
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
       "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=6"
@@ -1129,6 +1256,7 @@
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
       "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -1149,6 +1277,7 @@
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
       "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=10"
@@ -1161,24 +1290,27 @@
       "version": "0.5.5",
       "resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.5.5.tgz",
       "integrity": "sha512-lMR8tOtDqazFT6W2bZidoXwkptMdF3pCxpri0AEokHg0sZlC2GdoLqnoaxsEj1o7/BtXV1MKtT3YviA1t7rW7g==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/resolve-alpn": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
       "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/responselike": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
-      "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==",
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz",
+      "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "lowercase-keys": "^3.0.0"
       },
       "engines": {
-        "node": ">=14.16"
+        "node": ">=20"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
@@ -1188,6 +1320,7 @@
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
       "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "onetime": "^7.0.0",
@@ -1204,6 +1337,7 @@
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
       "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "iojs": ">=1.0.0",
@@ -1214,6 +1348,7 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
       "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -1234,15 +1369,17 @@
       }
     },
     "node_modules/sax": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
-      "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
-      "license": "ISC"
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
+      "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
+      "dev": true,
+      "license": "BlueOak-1.0.0"
     },
     "node_modules/signal-exit": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
       "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
       "license": "ISC",
       "engines": {
         "node": ">=14"
@@ -1255,6 +1392,7 @@
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
       "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=14.16"
@@ -1267,6 +1405,7 @@
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
       "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true,
       "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.10.0"
@@ -1276,6 +1415,7 @@
       "version": "0.5.21",
       "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
       "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "buffer-from": "^1.0.0",
@@ -1286,6 +1426,7 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
       "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "through": "2"
@@ -1298,6 +1439,7 @@
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz",
       "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "duplexer": "~0.1.1",
@@ -1308,6 +1450,7 @@
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "emoji-regex": "^8.0.0",
@@ -1322,6 +1465,7 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ansi-regex": "^5.0.1"
@@ -1334,67 +1478,71 @@
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
       "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/text-runner": {
-      "version": "7.1.2",
-      "resolved": "https://registry.npmjs.org/text-runner/-/text-runner-7.1.2.tgz",
-      "integrity": "sha512-gIAPtq6tMksyxrZqjcBYl+rxjP30l2accwhf/Fb5FP/nLM5p9NAti66Z8/76yW3SxRgj8CDmmct1kFY60c9hOw==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/text-runner/-/text-runner-7.4.0.tgz",
+      "integrity": "sha512-KlwWEiDE3Skcq4dU7qm++ZuPRVclVrROW8JD1nmhSwEhOVbE+Asred366JTkMnEJHWNFRnZc3ZMvt+7R1WksdQ==",
+      "dev": true,
       "license": "ISC",
       "dependencies": {
-        "@babel/code-frame": "7.26.2",
+        "@babel/code-frame": "7.27.1",
         "cli-cursor": "5.0.0",
         "cli-progress": "3.12.0",
-        "colorette": "2.0.20",
         "end-child-processes": "2.0.3",
         "jsonc-reader": "0.3.0",
         "minimist": "1.2.8",
-        "text-runner-engine": "7.1.2"
+        "text-runner-engine": "7.4.0"
       },
       "bin": {
         "text-runner": "bin/text-runner"
       }
     },
     "node_modules/text-runner-engine": {
-      "version": "7.1.2",
-      "resolved": "https://registry.npmjs.org/text-runner-engine/-/text-runner-engine-7.1.2.tgz",
-      "integrity": "sha512-KswRdeaVnMpHgAE8b1yDwQY65bvaCA+DQ+rRpQZAZK6+2NVVd2QtJ78XsgCYRJACCNYdNTVPnR0mqWtJ8ekM8Q==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/text-runner-engine/-/text-runner-engine-7.4.0.tgz",
+      "integrity": "sha512-rRyU1qFwN7rBIfT5nPNkgtRpFcynmEzUs9NhOgJjHQxDIcBfHMT4JcCOe0Fh61iY/P2x53E60/D6BrUM7vVwng==",
+      "dev": true,
       "license": "ISC",
       "dependencies": {
-        "@sindresorhus/slugify": "2.2.1",
+        "@sindresorhus/slugify": "3.0.0",
         "anchor-markdown-header": "0.7.0",
-        "globby": "14.1.0",
-        "got": "14.4.6",
-        "humanize-string": "3.0.0",
+        "globby": "15.0.0",
+        "got": "14.5.0",
+        "humanize-string": "3.1.0",
         "is-glob": "4.0.3",
         "markdown-it": "14.1.0",
-        "parse5": "7.2.1",
+        "parse5": "8.0.0",
         "tmp-promise": "3.0.3"
       }
     },
     "node_modules/textrun-make": {
-      "version": "0.3.1",
-      "resolved": "https://registry.npmjs.org/textrun-make/-/textrun-make-0.3.1.tgz",
-      "integrity": "sha512-+Z+umrW8dG3pM38rLI8ZJOJQcqrfFe5JwNdF66fQ+3iayMzX65ywRBLpJrHe86MjoDJKjPAQJ/ctHMbBD+s8rQ==",
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/textrun-make/-/textrun-make-0.6.0.tgz",
+      "integrity": "sha512-9kjeZT7g+aXyEpVB88YDtpL4IQhPbBbuKX2gBE+19YvnAkg4eaQ1sP0htRNEqvGNNXEbfVlOUYzWnpx2vKAing==",
+      "dev": true,
       "license": "ISC",
       "dependencies": {
-        "colorette": "2.0.20",
-        "text-runner-engine": "7.1.2"
+        "text-runner-engine": "7.4.0"
       }
     },
     "node_modules/through": {
       "version": "2.3.8",
       "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
       "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/tmp": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
-      "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+      "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=14.14"
@@ -1404,6 +1552,7 @@
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
       "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "tmp": "^0.2.0"
@@ -1413,6 +1562,7 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "is-number": "^7.0.0"
@@ -1422,9 +1572,10 @@
       }
     },
     "node_modules/type-fest": {
-      "version": "4.37.0",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.37.0.tgz",
-      "integrity": "sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==",
+      "version": "4.41.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+      "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+      "dev": true,
       "license": "(MIT OR CC0-1.0)",
       "engines": {
         "node": ">=16"
@@ -1437,18 +1588,21 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
       "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/undici-types": {
-      "version": "6.20.0",
-      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
-      "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+      "version": "7.16.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+      "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/unicorn-magic": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
       "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=18"
@@ -1462,6 +1616,7 @@
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
       "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
       "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "dev": true,
       "license": "MIT",
       "bin": {
         "uuid": "bin/uuid"
@@ -1471,12 +1626,14 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true,
       "license": "ISC"
     },
     "node_modules/xml-js": {
       "version": "1.6.11",
       "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
       "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "sax": "^1.2.4"
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..cd6e536a0
--- /dev/null
+++ b/package.json
@@ -0,0 +1,11 @@
+{
+  "type": "module",
+  "devDependencies": {
+    "gherkin-lint": "4.2.4",
+    "text-runner": "7.4.0",
+    "textrun-make": "0.6.0"
+  },
+  "scripts": {
+    "unit": "tools/rta node --test text-runner-tests/*.test.ts"
+  }
+}
diff --git a/pkg/asserts/no_error.go b/pkg/asserts/no_error.go
index 33d920cbf..5ea3aa2dd 100644
--- a/pkg/asserts/no_error.go
+++ b/pkg/asserts/no_error.go
@@ -6,13 +6,15 @@ func NoError(err error) {
 	}
 }
 
-// transparently checks the error value returned by a fallible operation
+// NoError1 verifies the given result consisting of a value and error.
+// If the error is nil, it returns the value, otherwise it panics.
 func NoError1[T any](value T, err error) T { //nolint:ireturn
 	NoError(err)
 	return value
 }
 
-// transparently checks the error value returned by a fallible operation
+// NoError2 verifies the given result consisting of two values and error.
+// If the error is nil, it returns the values, otherwise it panics.
 func NoError2[A, B any](value1 A, value2 B, err error) (A, B) { //nolint:ireturn
 	NoError(err)
 	return value1, value2
diff --git a/pkg/prelude/mutable.go b/pkg/prelude/mutable.go
index c480a407f..311ea51a0 100644
--- a/pkg/prelude/mutable.go
+++ b/pkg/prelude/mutable.go
@@ -7,7 +7,7 @@ type Mutable[T any] struct {
 	Value *T
 }
 
-// provides an non-mutable copy of the contained mutable value
+// Immutable provides an non-mutable copy of the contained mutable value.
 func (self Mutable[T]) Immutable() T { //nolint:ireturn
 	return *self.Value
 }
diff --git a/pkg/set/set.go b/pkg/set/set.go
index d1f9ef91f..f2760109b 100644
--- a/pkg/set/set.go
+++ b/pkg/set/set.go
@@ -26,6 +26,11 @@ func (self Set[T]) AddSet(other Set[T]) {
 	self.Add(other.Values()...)
 }
 
+func (self Set[T]) Contains(value T) bool {
+	_, ok := self[value]
+	return ok
+}
+
 func (self Set[T]) Values() []T {
 	result := slices.Collect(maps.Keys(self))
 	slices.Sort(result)
diff --git a/run-that-app b/run-that-app
index c043d494d..29708e9ae 100644
--- a/run-that-app
+++ b/run-that-app
@@ -1,25 +1,25 @@
-actionlint 1.7.7
+actionlint 1.7.8
 alphavet 0.1.0
 contest 0.5.0
 cucumber-sort 0.1.0
-deadcode 0.28.0
+deadcode 0.38.0
 depth 1.2.1
-dprint 0.50.0
+dprint 0.50.2
 exhaustruct 3.3.1
-ghokin 3.8.0
-gofumpt 0.8.0
-golangci-lint 2.1.6
+ghokin 3.8.1
+gofumpt 0.9.2
+golangci-lint 2.6.0
 goreleaser 1.26.2
 govulncheck 1.1.4
 ireturn 0.4.0
 keep-sorted 0.7.1
 mdbook 0.4.51
 mdbook-linkcheck 0.7.7
-node 24.2.0
-npm 24.2.0
-npx 24.2.0
-ripgrep 14.1.1
-scc 3.5.0
-shellcheck system 0.10.0
-shfmt 3.11.0
+node 25.1.0
+npm 25.1.0
+npx 25.1.0
+ripgrep 15.1.0
+scc 3.6.0
+shellcheck system 0.11.0
+shfmt 3.12.0
 staticcheck 2025.1.1
diff --git a/text-runner/git-town-command.ts b/text-runner/git-town-command.ts
new file mode 100644
index 000000000..b51045448
--- /dev/null
+++ b/text-runner/git-town-command.ts
@@ -0,0 +1,234 @@
+import { deepEqual } from "node:assert/strict"
+import { exec } from "node:child_process"
+import { promisify } from "node:util"
+import * as textRunner from "text-runner"
+
+const execAsync = promisify(exec)
+
+/** verifies a MD page that describes a Git Town command */
+export async function gitTownCommand(action: textRunner.actions.Args) {
+  const doc = new Document(action.document)
+
+  // determine the Git Town command that this page describes
+  const summarySection = doc.summarySection()
+  const command = summarySection.command()
+
+  // get the actual arguments of this Git Town command
+  const actualArgs = await command.actualArgs()
+  const actualJSON = JSON.stringify(actualArgs, null, 2)
+
+  // get the arguments described by the command summary
+  const summaryArgs = summarySection.args()
+  const summaryJSON = JSON.stringify(summaryArgs, null, 2)
+
+  // ensure the summary documents the arguments correct
+  action.log(`ACTUAL:\n${actualJSON}`)
+  action.log(`SUMMARY SECTION:\n${summaryJSON}`)
+  deepEqual(summaryArgs, actualArgs)
+
+  // get the arguments described by the "## Options" section
+  const optionsArgs = doc.argsInOptions()
+  const optionsJSON = JSON.stringify(optionsArgs, null, 2)
+
+  // ensure the options section documents the arguments correct
+  action.log(`ACTUAL:\n${actualJSON}`)
+  action.log(`OPTIONS SECTION:\n${optionsJSON}`)
+  deepEqual(optionsArgs, actualArgs)
+}
+
+/** Document contains the AST for an entire webpage describing a Git Town command */
+class Document {
+  nodes: textRunner.ast.NodeList
+
+  constructor(nodes: textRunner.ast.NodeList) {
+    this.nodes = nodes
+  }
+
+  /** provides the text of the ```command-summary block at the beginning of this page */
+  summarySection(): SummarySection {
+    const fences = this.nodes.nodesOfTypes("fence")
+    if (fences.length === 0) {
+      throw new Error("no fenced blocks found")
+    }
+    // the first fenced block contains the summary
+    const summaryBlock = fences[0]
+    const summaryNodes = this.nodes.nodesFor(summaryBlock)
+    return new SummarySection(summaryNodes.text())
+  }
+
+  /** provides the arguments documented in the "## Options" section of this page */
+  argsInOptions(): string[][] {
+    let result: string[][] = []
+    let insideOptions = false
+    for (const node of this.nodes) {
+      if (isH2(node)) {
+        if (insideOptions) {
+          // here we run into the next h2 heading after options --> done parsing options
+          return result
+        }
+        if (this.isOptionsHeading(node)) {
+          insideOptions = true
+        }
+        continue
+      }
+      if (insideOptions) {
+        if (isFlagHeading(node)) {
+          const flagNodes = this.nodes.nodesFor(node)
+          result.push(texts(flagNodes))
+        }
+      }
+    }
+    return result
+  }
+
+  /** indicates whether the given node is the "## Options" heading */
+  isOptionsHeading(node: textRunner.ast.Node): boolean {
+    const nodes = this.nodes.nodesFor(node)
+    const text = nodes.text()
+    return text === "Options"
+  }
+}
+
+/** SummarySection contains the text of the ```command-summary block of a Document*/
+export class SummarySection {
+  text: string
+
+  constructor(text: string) {
+    this.text = text
+  }
+
+  /** provides the arguments that this summary section describes for its Git Town command */
+  args(): string[][] {
+    const args: string[][] = []
+    // Match all optional arguments in square brackets: [-p | --prototype] or [(-m | --message) <text>]
+    const matches = this.text.matchAll(/\[([^\]]+)\]/g)
+    for (const match of matches) {
+      let argText = match[1]
+
+      // Check if this contains grouped arguments in parentheses
+      const groupMatch = argText.match(/^\(([^)]+)\)(.*)/)
+      if (groupMatch) {
+        // Extract the content inside parentheses and any content after (like <message>)
+        argText = groupMatch[1] + groupMatch[2]
+      }
+
+      if (!argText.trim().startsWith("-")) {
+        // this element doesn't contain a flag (doesn't start with -)
+        continue
+      }
+      const normalizedArgText = argText.replace(/<.+?>/g, "string")
+      // Split by | to get the different variations of the flag
+      const variations = normalizedArgText.split("|").map((v) => v.trim())
+      args.push(variations)
+    }
+    return args
+  }
+
+  /** provides the name of the Git Town command described by this summary section */
+  command(): GitTownCommand {
+    const match = this.text.match(/^git town ([^<[(]+?)(?:\s+-|\s+<|\s+\[|\s+\(|$)/)
+    const commandName = match?.[1]?.trim() || ""
+    return new GitTownCommand(commandName)
+  }
+}
+
+/** GitTownCommand represents a specific Git Town command, like "append" or "sync" */
+class GitTownCommand {
+  name: string
+
+  constructor(name: string) {
+    this.name = name
+  }
+
+  /** provides the actual arguments that this Git Town command accepts, determined by calling it with --help and parsing the output */
+  async actualArgs(): Promise<string[][]> {
+    const result = await execAsync(`git town ${this.name} --help`)
+    const output = new HelpOutput(result.stdout)
+    return output.flags()
+  }
+}
+
+/** HelpOutput is the output of a Git Town command executed with "--help" */
+export class HelpOutput {
+  text: string
+
+  constructor(text: string) {
+    this.text = text
+  }
+
+  /** provides the content of the "Flags:" section of this help output as a list of flag variations */
+  flags(): string[][] {
+    const result: string[][] = []
+    const lines = this.text.split("\n")
+    let inFlagsSection = false
+    for (const line of lines) {
+      if (line.includes("Flags:")) {
+        inFlagsSection = true
+        continue
+      }
+      if (!inFlagsSection) {
+        continue
+      }
+      // Stop if we hit an empty line (end of flags section)
+      if (line.trim() === "") {
+        break
+      }
+      // Parse flag line - format: "  -b, --beam             description"
+      // The description starts after 2 or more spaces
+      const match = line.match(/^\s+(.+?)\s{2,}/)
+      if (match) {
+        const flagsPart = match[1].trim()
+        const flags = flagsPart.split(/,\s+/).map((flag) => {
+          // Remove default value notation like [="all"]
+          return flag.replace(/\[="[^"]*"\]/, "")
+        })
+        if (flags.length > 0) {
+          result.push(flags)
+        }
+      }
+    }
+    return result
+  }
+}
+
+function isFlagHeading(node: textRunner.ast.Node): boolean {
+  return node.type === "h4_open"
+}
+
+function isH2(node: textRunner.ast.Node): boolean {
+  return node.type === "h2_open"
+}
+
+function texts(nodes: textRunner.ast.NodeList): string[] {
+  let result: string[] = []
+  for (const node of nodes) {
+    if (node.type === "text") {
+      result.push(node.content)
+    }
+  }
+  return standardizeArgument(removeNegatedFlag(result))
+}
+
+export function removeNegatedFlag(flags: string[]): string[] {
+  if (flags.length < 2) {
+    return flags
+  }
+  return flags.filter(flag => !flag.startsWith("--no-"))
+}
+
+export function standardizeArgument(texts: string[]): string[] {
+  const result: string[] = []
+  for (const text of texts) {
+    if (text.startsWith("--")) {
+      const parts = text.split(" ")
+      if (parts.length > 1) {
+        parts[1] = "string"
+      }
+      result.push(parts.join(" "))
+    } else if (text.startsWith("-")) {
+      const parts = text.split(" ")
+      result.push(parts[0])
+    }
+  }
+  return result
+}
diff --git a/tools/.prettierignore b/tools/.prettierignore
deleted file mode 100644
index 6ca3b5de8..000000000
--- a/tools/.prettierignore
+++ /dev/null
@@ -1 +0,0 @@
-../vendor/
diff --git a/tools/ensure_no_files_with_dashes.sh b/tools/ensure_no_files_with_dashes.sh
index f3ab90efb..0cf01a37a 100755
--- a/tools/ensure_no_files_with_dashes.sh
+++ b/tools/ensure_no_files_with_dashes.sh
@@ -14,6 +14,7 @@ files_with_dashes=$(find . -name '*-*' |
 	grep -v './dprint-changelog.json' |
 	grep -v './cucumber-sort.json' |
 	grep -v './.gherkin-*' |
+	grep -v './text-runner' |
 	grep -v 'tools(/|\\)rta')
 if [ -n "$files_with_dashes" ]; then
 	count=$(echo "$files_with_dashes" | wc -l)
diff --git a/tools/format_self/format_self.go b/tools/format_self/format_self.go
index aff9bf9b5..660844ca0 100644
--- a/tools/format_self/format_self.go
+++ b/tools/format_self/format_self.go
@@ -67,9 +67,11 @@ func IsGoFile(path string) bool {
 // shouldIgnorePath indicates whether the file with the given path should be ignored (not formatted).
 func shouldIgnorePath(path string) bool {
 	return strings.HasPrefix(path, "vendor/") ||
-		path == "internal/config/configdomain/offline.go" ||
+		// keep-sorted start
 		path == "internal/cli/dialog/switch_branch.go" ||
+		path == "internal/config/configdomain/offline.go" ||
 		path == "internal/gohacks/slice/natural_sort.go" ||
 		path == "tools/tests_sorted/matcher/matcher.go" ||
+		// keep-sorted end
 		strings.HasPrefix(path, "tools/stats_release")
 }
diff --git a/tools/format_self/go.mod b/tools/format_self/go.mod
index 13c2a45be..79238d989 100644
--- a/tools/format_self/go.mod
+++ b/tools/format_self/go.mod
@@ -1,3 +1,3 @@
 module github.com/git-town/git-town/tools/format_self
 
-go 1.24.6
+go 1.24.9
diff --git a/tools/generate_jsonschema.go b/tools/generate_jsonschema.go
new file mode 100644
index 000000000..eddd64ab5
--- /dev/null
+++ b/tools/generate_jsonschema.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+
+	"github.com/git-town/git-town/v22/internal/config/configfile"
+	"github.com/invopop/jsonschema"
+)
+
+func main() {
+	reflector := new(jsonschema.Reflector)
+	reflector.RequiredFromJSONSchemaTags = true
+	schema := reflector.Reflect(&configfile.Data{}) //exhaustruct:ignore
+	schema.ID = "https://www.git-town.com/git-town.toml"
+	schemaJSON, err := json.MarshalIndent(schema, "", "  ")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Error marshaling JSON schema: %v\n", err)
+		os.Exit(1)
+	}
+	fmt.Println(string(schemaJSON))
+}
diff --git a/tools/iterate_map/go.mod b/tools/iterate_map/go.mod
index 585354003..1aff2ff2c 100644
--- a/tools/iterate_map/go.mod
+++ b/tools/iterate_map/go.mod
@@ -1,6 +1,6 @@
 module github.com/git-town/git-town/tools/iterate_map
 
-go 1.24.6
+go 1.24.9
 
 require golang.org/x/tools v0.37.0
 
diff --git a/tools/lint_cached_connectors/Makefile b/tools/lint_cached_connectors/Makefile
new file mode 100644
index 000000000..abc5a4875
--- /dev/null
+++ b/tools/lint_cached_connectors/Makefile
@@ -0,0 +1,18 @@
+.DEFAULT_GOAL := help
+
+fix:  # runs all linters and auto-fixes
+	../rta gofumpt -l -w .
+	go run ../format_self/format_self.go
+
+help:  # prints all available targets
+	@grep -h -E '^[a-zA-Z_-]+:.*?# .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?# "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
+
+lint:  # runs all linters
+	../rta golangci-lint cache clean
+	../rta golangci-lint run
+	../rta deadcode github.com/git-town/git-town/tools/lint_cached_connectors
+
+test: lint unit fix  # runs all tests
+
+unit:  # runs the unit tests
+	@env GOGC=off go test ./...
diff --git a/tools/lint_cached_connectors/README.md b/tools/lint_cached_connectors/README.md
new file mode 100644
index 000000000..dfbab7b10
--- /dev/null
+++ b/tools/lint_cached_connectors/README.md
@@ -0,0 +1,37 @@
+# Lint Cached Connectors
+
+This linter verifies that cached connectors in `internal/forge` implement the
+same interfaces as their uncached counterparts.
+
+## Purpose
+
+The codebase has pairs of connectors:
+
+- **Uncached connectors**: Direct API/CLI connectors (e.g., `APIConnector`,
+  `Connector`)
+- **Cached connectors**: Wrappers that add caching (e.g., `CachedAPIConnector`,
+  `CachedConnector`)
+
+Each cached connector should implement exactly the same interfaces as its
+uncached counterpart to ensure they are interchangeable.
+
+## How It Works
+
+The linter:
+
+1. Parses Go source files for connector pairs
+2. Extracts interface implementations from type check comments (e.g.,
+   `var _ forgedomain.ProposalFinder = apiConnector`)
+3. Compares interfaces between cached and uncached versions
+4. Reports any missing interface implementations
+
+## Connector Pairs Checked
+
+- `bitbucketcloud`: `APIConnector` / `CachedAPIConnector`
+- `bitbucketdatacenter`: `APIConnector` / `CachedAPIConnector`
+- `forgejo`: `APIConnector` / `CachedAPIConnector`
+- `gitea`: `APIConnector` / `CachedAPIConnector`
+- `github`: `APIConnector` / `CachedAPIConnector`
+- `gitlab`: `APIConnector` / `CachedAPIConnector`
+- `gh`: `Connector` / `CachedConnector`
+- `glab`: `Connector` / `CachedConnector`
diff --git a/tools/lint_cached_connectors/go.mod b/tools/lint_cached_connectors/go.mod
new file mode 100644
index 000000000..3663d2107
--- /dev/null
+++ b/tools/lint_cached_connectors/go.mod
@@ -0,0 +1,3 @@
+module github.com/git-town/git-town/tools/lint_cached_connectors
+
+go 1.23
diff --git a/tools/lint_cached_connectors/main.go b/tools/lint_cached_connectors/main.go
new file mode 100644
index 000000000..52bde832f
--- /dev/null
+++ b/tools/lint_cached_connectors/main.go
@@ -0,0 +1,270 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"go/ast"
+	"go/parser"
+	"go/token"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"github.com/git-town/git-town/v22/pkg/set"
+)
+
+// ConnectorPair represents a pair of cached and uncached connectors
+type ConnectorPair struct {
+	CachedFile   string // file path of the cached connector
+	CachedType   string // type name of the cached connector
+	Package      string // package name of the connector pair
+	UncachedFile string // file path of the uncached connector
+	UncachedType string // type name of the uncached connector
+}
+
+// InterfaceImplementation represents a type implementing an interface
+type InterfaceImplementation struct {
+	InterfaceName string         // name of the interface that is implemented
+	Position      token.Position // file, line, and column where the interface is implemented
+	TypeName      string         // type of the connector that implements the interface
+}
+
+func main() {
+	if err := check(); err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+}
+
+func check() error {
+	connectorPairs, err := connectorPairs("internal/forge")
+	if err != nil {
+		return fmt.Errorf("error discovering connector pairs: %w", err)
+	}
+	if len(connectorPairs) == 0 {
+		return errors.New("No connector pairs found")
+	}
+	for _, pair := range connectorPairs {
+		uncachedInterfaces, err := implementedInterfaces(pair.UncachedFile, pair.UncachedType)
+		if err != nil {
+			return fmt.Errorf("Error parsing %s: %w", pair.UncachedFile, err)
+		}
+		cachedInterfaces, err := implementedInterfaces(pair.CachedFile, pair.CachedType)
+		if err != nil {
+			return fmt.Errorf("Error parsing %s: %w", pair.CachedFile, err)
+		}
+		for _, missing := range missingInterfaces(uncachedInterfaces, cachedInterfaces) {
+			return fmt.Errorf(
+				"%s:1  %s needs to implement %s\nbecause %s implements it in %s:%d",
+				pair.CachedFile,
+				pair.CachedType,
+				missing.InterfaceName,
+				pair.UncachedType,
+				missing.Position.Filename,
+				missing.Position.Line,
+			)
+		}
+	}
+	return nil
+}
+
+// connectorPairs discovers all cached/uncached connector pairs in the given directory
+func connectorPairs(dir string) ([]ConnectorPair, error) {
+	var result []ConnectorPair
+	entries, err := os.ReadDir(dir)
+	if err != nil {
+		return nil, fmt.Errorf("reading %s: %w", dir, err)
+	}
+	for _, entry := range entries {
+		if !entry.IsDir() {
+			continue
+		}
+		pkgName := entry.Name()
+		pkgPath := filepath.Join(dir, pkgName)
+		globPattern := filepath.Join(pkgPath, "cached_*.go")
+		cachedFiles, err := filepath.Glob(globPattern)
+		if err != nil {
+			return nil, fmt.Errorf("globbing %s in %s: %w", globPattern, pkgPath, err)
+		}
+		for _, cachedFile := range cachedFiles {
+			uncachedFile := uncachedFilePath(cachedFile, pkgPath)
+			if _, err := os.Stat(uncachedFile); os.IsNotExist(err) {
+				return nil, fmt.Errorf("uncached file %s does not exist", uncachedFile)
+			}
+			cachedType, err := primaryTypeName(cachedFile)
+			if err != nil {
+				return nil, fmt.Errorf("extracting type from %s: %w", cachedFile, err)
+			}
+			uncachedType, err := primaryTypeName(uncachedFile)
+			if err != nil {
+				return nil, fmt.Errorf("extracting type from %s: %w", uncachedFile, err)
+			}
+			result = append(result, ConnectorPair{
+				CachedFile:   cachedFile,
+				CachedType:   cachedType,
+				Package:      pkgName,
+				UncachedFile: uncachedFile,
+				UncachedType: uncachedType,
+			})
+		}
+	}
+	sort.Slice(result, func(i, j int) bool {
+		return result[i].Package < result[j].Package
+	})
+	return result, nil
+}
+
+// uncachedFilePath extracts the uncached file path from a cached file path.
+// "cached_api_connector.go" -> "api_connector.go"
+// "cached_connector.go" -> "connector.go"
+func uncachedFilePath(cachedFile, pkgPath string) string {
+	baseName := filepath.Base(cachedFile)
+	uncachedName := strings.TrimPrefix(baseName, "cached_")
+	return filepath.Join(pkgPath, uncachedName)
+}
+
+// primaryTypeName parses a Go file and extracts the primary struct type name.
+// It looks for the first struct type declaration in the file.
+func primaryTypeName(filePath string) (string, error) {
+	fileSet := token.NewFileSet()
+	file, err := parser.ParseFile(fileSet, filePath, nil, 0)
+	if err != nil {
+		return "", err
+	}
+	for _, decl := range file.Decls {
+		genDecl, ok := decl.(*ast.GenDecl)
+		if !ok || genDecl.Tok != token.TYPE {
+			continue
+		}
+		for _, spec := range genDecl.Specs {
+			typeSpec, ok := spec.(*ast.TypeSpec)
+			if !ok {
+				continue
+			}
+			if _, ok := typeSpec.Type.(*ast.StructType); ok {
+				return typeSpec.Name.Name, nil
+			}
+		}
+	}
+	return "", fmt.Errorf("no struct type found in %s", filePath)
+}
+
+// implementedInterfaces parses a Go file and extracts all interfaces
+// that the given type name implements
+func implementedInterfaces(filePath, typeName string) ([]InterfaceImplementation, error) {
+	fileSet := token.NewFileSet()
+	file, err := parser.ParseFile(fileSet, filePath, nil, parser.ParseComments)
+	if err != nil {
+		return nil, err
+	}
+	var implementations []InterfaceImplementation
+
+	// First, find all variables of our target type
+	// Pattern: var myConnector MyConnectorType
+	typeVars := set.New[string]()
+	for _, decl := range file.Decls {
+		genDecl, ok := decl.(*ast.GenDecl)
+		if !ok || genDecl.Tok != token.VAR {
+			continue
+		}
+		for _, spec := range genDecl.Specs {
+			valueSpec, ok := spec.(*ast.ValueSpec)
+			if !ok || valueSpec.Type == nil {
+				continue
+			}
+			if getTypeName(valueSpec.Type) == typeName {
+				for _, name := range valueSpec.Names {
+					typeVars.Add(name.Name)
+				}
+			}
+		}
+	}
+
+	// Now find interface type checks
+	// Pattern: var _ InterfaceName = myConnectorVar or &myConnectorVar
+	for _, decl := range file.Decls {
+		genDecl, ok := decl.(*ast.GenDecl)
+		if !ok || genDecl.Tok != token.VAR {
+			continue
+		}
+		for _, spec := range genDecl.Specs {
+			valueSpec, ok := spec.(*ast.ValueSpec)
+			if !ok {
+				continue
+			}
+
+			// Check if this is a type assertion (var _ Interface = variable)
+			if len(valueSpec.Names) == 1 && valueSpec.Names[0].Name == "_" && valueSpec.Type != nil && len(valueSpec.Values) > 0 {
+				// Get the interface name
+				interfaceName := getTypeName(valueSpec.Type)
+
+				// Check if the value is a variable of our target type
+				// Handle both: varName and &varName
+				valueName := valueVarName(valueSpec.Values[0])
+				if typeVars.Contains(valueName) {
+					position := fileSet.Position(valueSpec.Pos())
+					implementations = append(implementations, InterfaceImplementation{
+						InterfaceName: interfaceName,
+						Position:      position,
+						TypeName:      typeName,
+					})
+				}
+			}
+		}
+	}
+	return implementations, nil
+}
+
+// getTypeName extracts the type name from an AST expression
+func getTypeName(expr ast.Expr) string {
+	switch t := expr.(type) {
+	case *ast.Ident:
+		return t.Name
+	case *ast.SelectorExpr:
+		// For qualified names like forgedomain.Connector
+		if x, ok := t.X.(*ast.Ident); ok {
+			return x.Name + "." + t.Sel.Name
+		}
+		return t.Sel.Name
+	case *ast.CompositeLit:
+		return getTypeName(t.Type)
+	default:
+		return ""
+	}
+}
+
+// valueVarName extracts the variable name from an expression, handling both
+// direct references (varName) and pointer references (&varName)
+func valueVarName(expr ast.Expr) string {
+	switch t := expr.(type) {
+	case *ast.Ident:
+		return t.Name
+	case *ast.UnaryExpr:
+		// Handle &varName
+		if t.Op == token.AND {
+			return valueVarName(t.X)
+		}
+		return ""
+	default:
+		return ""
+	}
+}
+
+// missingInterfaces returns interfaces that are in 'expected' but not in 'actual'
+func missingInterfaces(expected, actual []InterfaceImplementation) []InterfaceImplementation {
+	actualSet := make(map[string]bool)
+	for _, impl := range actual {
+		actualSet[impl.InterfaceName] = true
+	}
+	var missing []InterfaceImplementation
+	for _, impl := range expected {
+		if !actualSet[impl.InterfaceName] {
+			missing = append(missing, impl)
+		}
+	}
+	sort.Slice(missing, func(i, j int) bool {
+		return strings.Compare(missing[i].InterfaceName, missing[j].InterfaceName) < 0
+	})
+	return missing
+}
diff --git a/tools/lint_steps/go.mod b/tools/lint_steps/go.mod
index eb2c58661..80cc9c41e 100644
--- a/tools/lint_steps/go.mod
+++ b/tools/lint_steps/go.mod
@@ -1,3 +1,3 @@
 module github.com/git-town/git-town/tools/lint_steps
 
-go 1.24.6
+go 1.24.9
diff --git a/tools/lint_steps/lint_steps.go b/tools/lint_steps/lint_steps.go
index 9a5c0d695..3a0140438 100644
--- a/tools/lint_steps/lint_steps.go
+++ b/tools/lint_steps/lint_steps.go
@@ -33,6 +33,11 @@ func main() {
 		panic("no step definitions found")
 	}
 
+	unanchoredStepDefs := CheckStepRegexAnchors(existingStepDefs)
+	for _, unanchoredStepDef := range unanchoredStepDefs {
+		fmt.Printf("%s:%d step definition regex must start with ^ and end with $: %s\n", fileName, unanchoredStepDef.Line, unanchoredStepDef.Text)
+	}
+
 	unsortedStepDefs := AllUnsortedStepDefs(existingStepDefs)
 	for _, unsortedStepDef := range unsortedStepDefs {
 		fmt.Printf("%s:%d steps are not alphabetically sorted, expected here: %s\n", fileName, unsortedStepDef.Line, unsortedStepDef.Text)
@@ -42,7 +47,7 @@ func main() {
 	for _, unusedStepDef := range unusedStepDefs {
 		fmt.Printf("%s:%d unused step definition: %s\n", fileName, unusedStepDef.Line, unusedStepDef.Text)
 	}
-	if len(unsortedStepDefs) > 0 || len(unusedStepDefs) > 0 {
+	if len(unanchoredStepDefs) > 0 || len(unsortedStepDefs) > 0 || len(unusedStepDefs) > 0 {
 		os.Exit(1)
 	}
 }
diff --git a/tools/lint_steps/stepdef_regex.go b/tools/lint_steps/stepdef_regex.go
new file mode 100644
index 000000000..41da481c9
--- /dev/null
+++ b/tools/lint_steps/stepdef_regex.go
@@ -0,0 +1,15 @@
+package main
+
+import "strings"
+
+// CheckStepRegexAnchors verifies that all step definition regular expressions
+// start with "^" and end with "$" to ensure proper matching.
+func CheckStepRegexAnchors(stepDefs []StepDefinition) []StepDefinition {
+	var result []StepDefinition
+	for _, stepDef := range stepDefs {
+		if !strings.HasPrefix(stepDef.Text, "^") || !strings.HasSuffix(stepDef.Text, "$") {
+			result = append(result, stepDef)
+		}
+	}
+	return result
+}
diff --git a/tools/lint_steps/stepdef_usage.go b/tools/lint_steps/stepdef_usage.go
index 34ffda476..c3a080492 100644
--- a/tools/lint_steps/stepdef_usage.go
+++ b/tools/lint_steps/stepdef_usage.go
@@ -61,7 +61,7 @@ func FindUsedStepsIn(fileContent string) []string {
 		stepUsageRE = regexp.MustCompile(`^\s*(?:Given|When|Then|And) (.*)$`)
 	}
 	var result []string
-	for _, line := range strings.Split(fileContent, "\n") {
+	for line := range strings.SplitSeq(fileContent, "\n") {
 		matches := stepUsageRE.FindAllStringSubmatch(line, -1)
 		if len(matches) > 0 {
 			result = append(result, strings.TrimSpace(matches[0][1]))
@@ -72,12 +72,7 @@ func FindUsedStepsIn(fileContent string) []string {
 
 // indicates whether the given step definition is used anywhere in the given list of executed steps
 func IsStepDefUsed(definedStep StepRE, usedSteps []string) bool {
-	for _, usedStep := range usedSteps {
-		if definedStep.regex.MatchString(usedStep) {
-			return true
-		}
-	}
-	return false
+	return slices.ContainsFunc(usedSteps, definedStep.regex.MatchString)
 }
 
 // provides all steps that are executed in .feature files
diff --git a/tools/mdbook_preprocessor/mdbook_preprocessor.js b/tools/mdbook_preprocessor/mdbook_preprocessor.js
index 802590344..156ededcb 100644
--- a/tools/mdbook_preprocessor/mdbook_preprocessor.js
+++ b/tools/mdbook_preprocessor/mdbook_preprocessor.js
@@ -1,7 +1,7 @@
 // @ts-check
 /// <reference types="node" />
 
-import { extractCommand, tokenize } from "./util.js";
+import { extractCommand, tokenize } from "./util.js"
 
 /**
  * @typedef {import("./types").Book} Book
@@ -19,16 +19,16 @@ import { extractCommand, tokenize } from "./util.js";
  */
 
 if (process.argv.length > 2) {
-  handleSupports();
+  handleSupports()
 } else {
-  await handlePreprocess();
+  await handlePreprocess()
 }
 
 function handleSupports() {
   if (process.argv[2] === "supports" && process.argv[3] === "html") {
-    process.exit(0);
+    process.exit(0)
   }
-  process.exit(1);
+  process.exit(1)
 }
 
 /**
@@ -40,19 +40,19 @@ function handleSupports() {
  */
 async function handlePreprocess() {
   // Read from stdin
-  let stdin = "";
+  let stdin = ""
   for await (const chunk of process.stdin) {
-    stdin += chunk;
+    stdin += chunk
   }
 
   // We don't care about the context. Only process the book.
-  const [, book] = JSON.parse(stdin);
+  const [, book] = JSON.parse(stdin)
 
-  processBook(book);
+  processBook(book)
 
   // Write to stdout
-  const output = JSON.stringify(book);
-  process.stdout.write(output + "\n");
+  const output = JSON.stringify(book)
+  process.stdout.write(output + "\n")
 }
 
 /**
@@ -60,7 +60,7 @@ async function handlePreprocess() {
  */
 function processBook(book) {
   for (const bookItem of book.sections) {
-    processBookItem(bookItem);
+    processBookItem(bookItem)
   }
 }
 
@@ -70,15 +70,15 @@ function processBook(book) {
 function processBookItem(bookItem) {
   // bookItem is { "Chapter": Chapter }, "Separator", or { "PartTitle": string }
   if (bookItem === "Separator") {
-    return;
+    return
   }
   // bookItem is { "Chapter": Chapter } or { "PartTitle": string }
   if ("PartTitle" in bookItem) {
-    return;
+    return
   }
   // bookItem is { "Chapter": Chapter }
 
-  processChapter(bookItem.Chapter);
+  processChapter(bookItem.Chapter)
 }
 
 /**
@@ -86,10 +86,10 @@ function processBookItem(bookItem) {
  */
 function processChapter(chapter) {
   for (const subItem of chapter.sub_items) {
-    processBookItem(subItem);
+    processBookItem(subItem)
   }
 
-  chapter.content = processContent(chapter.content);
+  chapter.content = processContent(chapter.content)
 }
 
 /**
@@ -98,10 +98,10 @@ function processChapter(chapter) {
  */
 function processContent(content) {
   return content.replaceAll(/```command-summary\n([\s\S]*?)\n```/g, (_, code) => {
-    return processCommandSummary(code);
+    return processCommandSummary(code)
   }).replaceAll(/^( *)```wrap\n([\s\S]*?)\n\1```/gm, (_, indent, code) => {
-    return processCodeWrap(code, indent);
-  });
+    return processCodeWrap(code, indent)
+  })
 }
 
 /**
@@ -149,18 +149,18 @@ function processCommandSummary(code) {
     code
       .split("\n")
       .map(line => {
-        const tokens = tokenize(line);
-        const { command, otherTokens } = extractCommand(tokens);
+        const tokens = tokenize(line)
+        const { command, otherTokens } = extractCommand(tokens)
 
-        const indent = command.length + 1;
+        const indent = command.length + 1
         return `<div class="gt-command-summary" style="padding-left: ${indent}ch; text-indent: -${indent}ch"><span class="gt-command">${command}</span> ${
           otherTokens
             .map(token => `<span>${token.replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</span>`)
             .join(" ")
-        }</div>`;
+        }</div>`
       })
       .join("")
-  }</pre></code>`;
+  }</pre></code>`
 }
 
 /**
@@ -208,14 +208,14 @@ function processCodeWrap(code, indent) {
     code
       .split("\n")
       .map(line => {
-        const tokens = tokenize(line.slice(indent.length));
+        const tokens = tokenize(line.slice(indent.length))
 
         return `<div class="gt-code-wrap">${
           tokens
             .map(token => `<span>${token.replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</span>`)
             .join(" ")
-        }</div>`;
+        }</div>`
       })
       .join("")
-  }</pre></code>`;
+  }</pre></code>`
 }
diff --git a/tools/mdbook_preprocessor/types.d.ts b/tools/mdbook_preprocessor/types.d.ts
index 9afa93e87..d223f5ec0 100644
--- a/tools/mdbook_preprocessor/types.d.ts
+++ b/tools/mdbook_preprocessor/types.d.ts
@@ -1,24 +1,24 @@
 // https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html
 export type Book = {
-  sections: BookItem[];
-};
+  sections: BookItem[]
+}
 
 // https://docs.rs/mdbook/latest/mdbook/book/enum.BookItem.html
 export type BookItem =
   | { Chapter: Chapter }
   | "Separator"
-  | { PartTitle: string };
+  | { PartTitle: string }
 
 // https://docs.rs/mdbook/latest/mdbook/book/struct.Chapter.html
 export type Chapter = {
-  name: string;
-  content: string;
-  number?: SectionNumber;
-  sub_items: BookItem[];
-  path?: string;
-  source_path?: string;
-  parent_names: string[];
-};
+  name: string
+  content: string
+  number?: SectionNumber
+  sub_items: BookItem[]
+  path?: string
+  source_path?: string
+  parent_names: string[]
+}
 
 // https://docs.rs/mdbook/latest/mdbook/book/struct.SectionNumber.html
-export type SectionNumber = number[];
+export type SectionNumber = number[]
diff --git a/tools/mdbook_preprocessor/util.js b/tools/mdbook_preprocessor/util.js
index 4bfbe44ef..afd5f51eb 100644
--- a/tools/mdbook_preprocessor/util.js
+++ b/tools/mdbook_preprocessor/util.js
@@ -1,6 +1,6 @@
 // @ts-check
 
-const GROUP_CHARS = ["()", "<>", "[]"];
+const GROUP_CHARS = ["()", "<>", "[]"]
 
 /**
  * This function tokenizes a line of text into strings that should be kept
@@ -15,30 +15,30 @@ const GROUP_CHARS = ["()", "<>", "[]"];
  * @returns {string[]}
  */
 export function tokenize(line) {
-  const tokens = [];
-  let token = "";
-  let group = undefined;
+  const tokens = []
+  let token = ""
+  let group = undefined
   for (const char of line) {
     if (group) {
       if (char === group[1]) {
-        group = undefined;
+        group = undefined
       }
-      token += char;
+      token += char
     } else {
-      const nextGroup = GROUP_CHARS.find(group => group[0] === char);
+      const nextGroup = GROUP_CHARS.find(group => group[0] === char)
       if (nextGroup) {
-        group = nextGroup;
-        token += char;
+        group = nextGroup
+        token += char
       } else if (char === " ") {
-        tokens.push(token);
-        token = "";
+        tokens.push(token)
+        token = ""
       } else {
-        token += char;
+        token += char
       }
     }
   }
-  tokens.push(token);
-  return tokens;
+  tokens.push(token)
+  return tokens
 }
 
 /**
@@ -52,15 +52,15 @@ export function tokenize(line) {
  * @returns {{ command: string, otherTokens: string[] }}
  */
 export function extractCommand(tokens) {
-  const otherTokens = [...tokens];
+  const otherTokens = [...tokens]
 
-  const commandTokens = [];
+  const commandTokens = []
   while (otherTokens.length > 0 && otherTokens[0].match(/^[a-z]/i)) {
-    commandTokens.push(otherTokens[0]);
-    otherTokens.shift();
+    commandTokens.push(otherTokens[0])
+    otherTokens.shift()
   }
 
-  const command = commandTokens.join(" ");
+  const command = commandTokens.join(" ")
 
-  return { command, otherTokens };
+  return { command, otherTokens }
 }
diff --git a/tools/mdbook_preprocessor/util.test.js b/tools/mdbook_preprocessor/util.test.js
index 323e98b55..5c7816492 100644
--- a/tools/mdbook_preprocessor/util.test.js
+++ b/tools/mdbook_preprocessor/util.test.js
@@ -1,29 +1,29 @@
 // @ts-check
 /// <reference types="node" />
 
-import assert from "node:assert/strict";
-import { describe, it } from "node:test";
+import assert from "node:assert/strict"
+import { describe, it } from "node:test"
 
-import { extractCommand, tokenize } from "./util.js";
+import { extractCommand, tokenize } from "./util.js"
 
 describe("tokenize", () => {
   it("should split string by spaces", () => {
-    const have = "git town append";
-    const want = ["git", "town", "append"];
-    assert.deepEqual(tokenize(have), want);
-  });
+    const have = "git town append"
+    const want = ["git", "town", "append"]
+    assert.deepEqual(tokenize(have), want)
+  })
 
   it("should not split tokens wrapped by various brackets", () => {
     for (const [left, right] of [["(", ")"], ["<", ">"], ["[", "]"]]) {
-      const have = `git town append ${left}-p | --prototype${right}`;
-      const want = ["git", "town", "append", `${left}-p | --prototype${right}`];
-      assert.deepEqual(tokenize(have), want);
+      const have = `git town append ${left}-p | --prototype${right}`
+      const want = ["git", "town", "append", `${left}-p | --prototype${right}`]
+      assert.deepEqual(tokenize(have), want)
     }
-  });
+  })
 
   it("should not split tokens wrapped by nested brackets", () => {
-    const have = "git town hack [<branch name>]";
-    const want = ["git", "town", "hack", "[<branch name>]"];
-    assert.deepEqual(tokenize(have), want);
-  });
-});
+    const have = "git town hack [<branch name>]"
+    const want = ["git", "town", "hack", "[<branch name>]"]
+    assert.deepEqual(tokenize(have), want)
+  })
+})
diff --git a/tools/messages_sorted/go.mod b/tools/messages_sorted/go.mod
index 2dce1674a..bf3d05c84 100644
--- a/tools/messages_sorted/go.mod
+++ b/tools/messages_sorted/go.mod
@@ -1,3 +1,3 @@
 module github.com/git-town/git-town/tools/messages_sorted
 
-go 1.24.6
+go 1.24.9
diff --git a/tools/messages_sorted/messages_sorted.go b/tools/messages_sorted/messages_sorted.go
index 090f24213..7ce356063 100644
--- a/tools/messages_sorted/messages_sorted.go
+++ b/tools/messages_sorted/messages_sorted.go
@@ -104,10 +104,7 @@ func getDiffContextLines(filePath string, actualNames []string, expectedNames []
 		}
 
 		// Calculate context range
-		start := pos - contextSize
-		if start < 0 {
-			start = 0
-		}
+		start := max(pos-contextSize, 0)
 		end := pos + contextSize
 		if end >= len(actualNames) {
 			end = len(actualNames) - 1
diff --git a/tools/messy_output/go.mod b/tools/messy_output/go.mod
index b556766b9..95ec91ea2 100644
--- a/tools/messy_output/go.mod
+++ b/tools/messy_output/go.mod
@@ -1,3 +1,3 @@
 module github.com/git-town/git-town/tools/messy_output
 
-go 1.24.6
+go 1.24.9
diff --git a/tools/optioncompare/go.mod b/tools/optioncompare/go.mod
index b8e7d6b6d..7366499a5 100644
--- a/tools/optioncompare/go.mod
+++ b/tools/optioncompare/go.mod
@@ -1,10 +1,10 @@
 module github.com/git-town/git-town/tools/optioncompare
 
-go 1.24.6
+go 1.24.9
 
-require golang.org/x/tools v0.34.0
+require golang.org/x/tools v0.38.0
 
 require (
-	golang.org/x/mod v0.25.0 // indirect
-	golang.org/x/sync v0.15.0 // indirect
+	golang.org/x/mod v0.29.0 // indirect
+	golang.org/x/sync v0.17.0 // indirect
 )
diff --git a/tools/optioncompare/go.sum b/tools/optioncompare/go.sum
index 2f2916f90..30238e392 100644
--- a/tools/optioncompare/go.sum
+++ b/tools/optioncompare/go.sum
@@ -4,11 +4,17 @@ golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
 golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
 golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
 golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
+golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
 golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
 golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
 golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
 golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
 golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
 golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
+golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
+golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
diff --git a/tools/package.json b/tools/package.json
deleted file mode 100644
index 1aa8c7202..000000000
--- a/tools/package.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "type": "module",
-  "dependencies": {
-    "gherkin-lint": "4.2.4",
-    "text-runner": "7.1.2",
-    "textrun-make": "0.3.1"
-  }
-}
diff --git a/tools/print_config_exhaustive/go.mod b/tools/print_config_exhaustive/go.mod
index 1247bd537..e5474b332 100644
--- a/tools/print_config_exhaustive/go.mod
+++ b/tools/print_config_exhaustive/go.mod
@@ -1,3 +1,3 @@
 module github.com/git-town/git-town/tools/print_config_exhaustive
 
-go 1.24.6
+go 1.24.9
diff --git a/tools/print_config_exhaustive/lint.go b/tools/print_config_exhaustive/lint.go
index eed90b9fd..cbd70b380 100644
--- a/tools/print_config_exhaustive/lint.go
+++ b/tools/print_config_exhaustive/lint.go
@@ -18,7 +18,6 @@ var whiteList = []string{
 	"Aliases",
 	"BranchTypeOverrides",
 	"DryRun",
-	"ProposalsShowLineage", // TODO: remove once https://github.com/git-town/git-town/issues/3003 is shipped
 	"Verbose",
 }
 
@@ -39,7 +38,7 @@ func FindDefinedFields(text string) []string {
 		log.Fatalf("Error: Failed to find NormalConfig struct")
 	}
 	result := []string{}
-	for _, line := range strings.Split(strings.TrimSpace(match[1]), "\n") {
+	for line := range strings.SplitSeq(strings.TrimSpace(match[1]), "\n") {
 		trimmedLine := strings.TrimSpace(line)
 		if strings.HasPrefix(trimmedLine, "//") {
 			continue // skip comments
diff --git a/tools/stats_release/go.mod b/tools/stats_release/go.mod
index c3d7fddf4..6a67bccd7 100644
--- a/tools/stats_release/go.mod
+++ b/tools/stats_release/go.mod
@@ -1,3 +1,3 @@
 module github.com/git-town/git-town/tools/stats_release
 
-go 1.24.6
+go 1.24.9
diff --git a/tools/structs_sorted/go.mod b/tools/structs_sorted/go.mod
index 3c15343a4..ddc3a9d28 100644
--- a/tools/structs_sorted/go.mod
+++ b/tools/structs_sorted/go.mod
@@ -1,3 +1,3 @@
 module github.com/git-town/git-town/tools/structs_sorted
 
-go 1.24.6
+go 1.24.9
diff --git a/tools/tests_sorted/go.mod b/tools/tests_sorted/go.mod
index 2c9f4e307..99e836386 100644
--- a/tools/tests_sorted/go.mod
+++ b/tools/tests_sorted/go.mod
@@ -1,3 +1,3 @@
 module github.com/git-town/git-town/tools/tests_sorted
 
-go 1.24.6
+go 1.24.9
diff --git a/tools/use_equal/go.mod b/tools/use_equal/go.mod
index 54fef00ad..af9339713 100644
--- a/tools/use_equal/go.mod
+++ b/tools/use_equal/go.mod
@@ -1,3 +1,3 @@
 module github.com/git-town/git-town/tools/use_equal
 
-go 1.24.6
+go 1.24.9
diff --git a/vendor/code.gitea.io/sdk/gitea/admin_cron.go b/vendor/code.gitea.io/sdk/gitea/admin_cron.go
index 84316da2b..de05ccb38 100644
--- a/vendor/code.gitea.io/sdk/gitea/admin_cron.go
+++ b/vendor/code.gitea.io/sdk/gitea/admin_cron.go
@@ -42,6 +42,5 @@ func (c *Client) RunCronTasks(task string) (*Response, error) {
 	if err := escapeValidatePathSegments(&task); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("POST", fmt.Sprintf("/admin/cron/%s", task), jsonHeader, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("POST", fmt.Sprintf("/admin/cron/%s", task), jsonHeader, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/admin_user.go b/vendor/code.gitea.io/sdk/gitea/admin_user.go
index 172f0645c..e49dbbdc0 100644
--- a/vendor/code.gitea.io/sdk/gitea/admin_user.go
+++ b/vendor/code.gitea.io/sdk/gitea/admin_user.go
@@ -93,8 +93,7 @@ func (c *Client) AdminEditUser(user string, opt EditUserOption) (*Response, erro
 	if err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PATCH", fmt.Sprintf("/admin/users/%s", user), jsonHeader, bytes.NewReader(body))
-	return resp, err
+	return c.doRequestWithStatusHandle("PATCH", fmt.Sprintf("/admin/users/%s", user), jsonHeader, bytes.NewReader(body))
 }
 
 // AdminDeleteUser delete one user according name
@@ -102,8 +101,7 @@ func (c *Client) AdminDeleteUser(user string) (*Response, error) {
 	if err := escapeValidatePathSegments(&user); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/admin/users/%s", user), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/admin/users/%s", user), nil, nil)
 }
 
 // AdminCreateUserPublicKey adds a public key for the user
@@ -125,6 +123,5 @@ func (c *Client) AdminDeleteUserPublicKey(user string, keyID int) (*Response, er
 	if err := escapeValidatePathSegments(&user); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/admin/users/%s/keys/%d", user, keyID), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/admin/users/%s/keys/%d", user, keyID), nil, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/attachment.go b/vendor/code.gitea.io/sdk/gitea/attachment.go
index c19a82c85..89c3a503b 100644
--- a/vendor/code.gitea.io/sdk/gitea/attachment.go
+++ b/vendor/code.gitea.io/sdk/gitea/attachment.go
@@ -106,6 +106,5 @@ func (c *Client) DeleteReleaseAttachment(user, repo string, release, id int64) (
 	if err := escapeValidatePathSegments(&user, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/releases/%d/assets/%d", user, repo, release, id), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/releases/%d/assets/%d", user, repo, release, id), nil, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/client.go b/vendor/code.gitea.io/sdk/gitea/client.go
index f45961a96..5bf19c32e 100644
--- a/vendor/code.gitea.io/sdk/gitea/client.go
+++ b/vendor/code.gitea.io/sdk/gitea/client.go
@@ -317,7 +317,12 @@ func (c *Client) getWebResponse(method, path string, body io.Reader) ([]byte, *R
 		return nil, nil, err
 	}
 
-	defer resp.Body.Close()
+	defer func() {
+		if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
+			err = closeErr
+		}
+	}()
+
 	data, err := io.ReadAll(resp.Body)
 	if debug {
 		fmt.Printf("Response: %v\n\n", resp)
@@ -397,7 +402,12 @@ func statusCodeToErr(resp *Response) (body []byte, err error) {
 	//
 	// error: body will be read for details
 	//
-	defer resp.Body.Close()
+	defer func() {
+		if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
+			err = closeErr
+		}
+	}()
+
 	data, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return nil, fmt.Errorf("body read on HTTP error %d: %v", resp.StatusCode, err)
@@ -410,7 +420,7 @@ func statusCodeToErr(resp *Response) (body []byte, err error) {
 		// plain string, so we try to return a helpful error anyway
 		path := resp.Request.URL.Path
 		method := resp.Request.Method
-		return data, fmt.Errorf("Unknown API Error: %d\nRequest: '%s' with '%s' method and '%s' body", resp.StatusCode, path, method, string(data))
+		return data, fmt.Errorf("unknown API error: %d\nRequest: '%s' with '%s' method and '%s' body", resp.StatusCode, path, method, string(data))
 	}
 
 	if msg, ok := errMap["message"]; ok {
@@ -436,12 +446,36 @@ func (c *Client) getResponseReader(method, path string, header http.Header, body
 	return resp.Body, resp, nil
 }
 
+func (c *Client) doRequestWithStatusHandle(method, path string, header http.Header, body io.Reader) (*Response, error) {
+	resp, err := c.doRequest(method, path, header, body)
+	if err != nil {
+		return resp, err
+	}
+
+	// check for errors
+	if _, err = statusCodeToErr(resp); err != nil {
+		// resp.Body has already been closed in statusCodeToErr
+		return resp, err
+	}
+	defer func() {
+		if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
+			err = closeErr
+		}
+	}()
+
+	return resp, err
+}
+
 func (c *Client) getResponse(method, path string, header http.Header, body io.Reader) ([]byte, *Response, error) {
 	resp, err := c.doRequest(method, path, header, body)
 	if err != nil {
 		return nil, resp, err
 	}
-	defer resp.Body.Close()
+	defer func() {
+		if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
+			err = closeErr
+		}
+	}()
 
 	// check for errors
 	data, err := statusCodeToErr(resp)
@@ -471,7 +505,11 @@ func (c *Client) getStatusCode(method, path string, header http.Header, body io.
 	if err != nil {
 		return -1, resp, err
 	}
-	defer resp.Body.Close()
+	defer func() {
+		if closeErr := resp.Body.Close(); closeErr != nil && err == nil {
+			err = closeErr
+		}
+	}()
 
 	return resp.StatusCode, resp, nil
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/git_hook.go b/vendor/code.gitea.io/sdk/gitea/git_hook.go
index d8fbf71bd..6b1f042dc 100644
--- a/vendor/code.gitea.io/sdk/gitea/git_hook.go
+++ b/vendor/code.gitea.io/sdk/gitea/git_hook.go
@@ -57,8 +57,7 @@ func (c *Client) EditRepoGitHook(user, repo, id string, opt EditGitHookOption) (
 	if err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PATCH", fmt.Sprintf("/repos/%s/%s/hooks/git/%s", user, repo, id), jsonHeader, bytes.NewReader(body))
-	return resp, err
+	return c.doRequestWithStatusHandle("PATCH", fmt.Sprintf("/repos/%s/%s/hooks/git/%s", user, repo, id), jsonHeader, bytes.NewReader(body))
 }
 
 // DeleteRepoGitHook delete one Git hook from a repository
@@ -66,6 +65,5 @@ func (c *Client) DeleteRepoGitHook(user, repo, id string) (*Response, error) {
 	if err := escapeValidatePathSegments(&user, &repo, &id); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/hooks/git/%s", user, repo, id), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/hooks/git/%s", user, repo, id), nil, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/hook.go b/vendor/code.gitea.io/sdk/gitea/hook.go
index f91b60c37..a18635c14 100644
--- a/vendor/code.gitea.io/sdk/gitea/hook.go
+++ b/vendor/code.gitea.io/sdk/gitea/hook.go
@@ -191,8 +191,7 @@ func (c *Client) EditOrgHook(org string, id int64, opt EditHookOption) (*Respons
 	if err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PATCH", fmt.Sprintf("/orgs/%s/hooks/%d", org, id), jsonHeader, bytes.NewReader(body))
-	return resp, err
+	return c.doRequestWithStatusHandle("PATCH", fmt.Sprintf("/orgs/%s/hooks/%d", org, id), jsonHeader, bytes.NewReader(body))
 }
 
 // EditMyHook modify one hook of the authenticated user, with hook id and options
@@ -201,8 +200,7 @@ func (c *Client) EditMyHook(id int64, opt EditHookOption) (*Response, error) {
 	if err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PATCH", fmt.Sprintf("/user/hooks/%d", id), jsonHeader, bytes.NewReader(body))
-	return resp, err
+	return c.doRequestWithStatusHandle("PATCH", fmt.Sprintf("/user/hooks/%d", id), jsonHeader, bytes.NewReader(body))
 }
 
 // EditRepoHook modify one hook of a repository, with hook id and options
@@ -214,8 +212,7 @@ func (c *Client) EditRepoHook(user, repo string, id int64, opt EditHookOption) (
 	if err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PATCH", fmt.Sprintf("/repos/%s/%s/hooks/%d", user, repo, id), jsonHeader, bytes.NewReader(body))
-	return resp, err
+	return c.doRequestWithStatusHandle("PATCH", fmt.Sprintf("/repos/%s/%s/hooks/%d", user, repo, id), jsonHeader, bytes.NewReader(body))
 }
 
 // DeleteOrgHook delete one hook from an organization, with hook id
@@ -223,14 +220,12 @@ func (c *Client) DeleteOrgHook(org string, id int64) (*Response, error) {
 	if err := escapeValidatePathSegments(&org); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/orgs/%s/hooks/%d", org, id), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/orgs/%s/hooks/%d", org, id), nil, nil)
 }
 
 // DeleteMyHook delete one hook from the authenticated user, with hook id
 func (c *Client) DeleteMyHook(id int64) (*Response, error) {
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/user/hooks/%d", id), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/user/hooks/%d", id), nil, nil)
 }
 
 // DeleteRepoHook delete one hook from a repository, with hook id
@@ -238,6 +233,5 @@ func (c *Client) DeleteRepoHook(user, repo string, id int64) (*Response, error)
 	if err := escapeValidatePathSegments(&user, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/hooks/%d", user, repo, id), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/hooks/%d", user, repo, id), nil, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/httpsign.go b/vendor/code.gitea.io/sdk/gitea/httpsign.go
index 8aecb454f..9b76c6ce5 100644
--- a/vendor/code.gitea.io/sdk/gitea/httpsign.go
+++ b/vendor/code.gitea.io/sdk/gitea/httpsign.go
@@ -147,7 +147,7 @@ func (c *Client) SignRequest(r *http.Request) error {
 
 	if c.httpsigner.cert {
 		// add our certificate to the headers to sign
-		pubkey, _ := ssh.ParsePublicKey(c.httpsigner.Signer.PublicKey().Marshal())
+		pubkey, _ := ssh.ParsePublicKey(c.httpsigner.PublicKey().Marshal())
 		if cert, ok := pubkey.(*ssh.Certificate); ok {
 			certString := base64.RawStdEncoding.EncodeToString(cert.Marshal())
 			r.Header.Add("x-ssh-certificate", certString)
@@ -175,17 +175,26 @@ func (c *Client) SignRequest(r *http.Request) error {
 	}
 
 	// create a signer for the request and headers, the signature will be valid for 10 seconds
-	var (
-		signer httpsig.SSHSigner
-		err    error
-	)
+	var err error
 
 	// use legacyhttpsig to sign with RSA-SHA1 on older gitea releases
 	if err = c.checkServerVersionGreaterThanOrEqual(version1_23_0); err != nil {
-		signer, _, err = legacyhttpsig.NewSSHSigner(c.httpsigner.Signer, httpsig.DigestSha512, headersToSign, legacyhttpsig.Signature, 10)
-	} else {
-		signer, _, err = httpsig.NewSSHSigner(c.httpsigner.Signer, httpsig.DigestSha512, headersToSign, httpsig.Signature, 10)
+		// Legacy signer
+		legacySigner, _, err := legacyhttpsig.NewSSHSigner(c.httpsigner, httpsig.DigestSha512, headersToSign, legacyhttpsig.Signature, 10)
+		if err != nil {
+			return fmt.Errorf("legacy httpsig.NewSSHSigner failed: %s", err)
+		}
+
+		// sign the request, use the fingerprint if we don't have a certificate
+		keyID := "gitea"
+		if !c.httpsigner.cert {
+			keyID = ssh.FingerprintSHA256(c.httpsigner.PublicKey())
+		}
+
+		return legacySigner.SignRequest(keyID, r, contents)
 	}
+	// Modern signer
+	modernSigner, _, err := httpsig.NewSSHSigner(c.httpsigner, httpsig.DigestSha512, headersToSign, httpsig.Signature, 10)
 	if err != nil {
 		return fmt.Errorf("httpsig.NewSSHSigner failed: %s", err)
 	}
@@ -193,15 +202,10 @@ func (c *Client) SignRequest(r *http.Request) error {
 	// sign the request, use the fingerprint if we don't have a certificate
 	keyID := "gitea"
 	if !c.httpsigner.cert {
-		keyID = ssh.FingerprintSHA256(c.httpsigner.Signer.PublicKey())
+		keyID = ssh.FingerprintSHA256(c.httpsigner.PublicKey())
 	}
 
-	err = signer.SignRequest(keyID, r, contents)
-	if err != nil {
-		return fmt.Errorf("httpsig.Signrequest failed: %s", err)
-	}
-
-	return nil
+	return modernSigner.SignRequest(keyID, r, contents)
 }
 
 // findCertSigner returns the Signer containing a valid certificate
diff --git a/vendor/code.gitea.io/sdk/gitea/issue.go b/vendor/code.gitea.io/sdk/gitea/issue.go
index 603d23de2..5f5924fab 100644
--- a/vendor/code.gitea.io/sdk/gitea/issue.go
+++ b/vendor/code.gitea.io/sdk/gitea/issue.go
@@ -294,10 +294,9 @@ func (c *Client) DeleteIssue(user, repo string, id int64) (*Response, error) {
 	if err := escapeValidatePathSegments(&user, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE",
+	return c.doRequestWithStatusHandle("DELETE",
 		fmt.Sprintf("/repos/%s/%s/issues/%d", user, repo, id),
 		nil, nil)
-	return resp, err
 }
 
 func (c *Client) issueBackwardsCompatibility(issue *Issue) {
diff --git a/vendor/code.gitea.io/sdk/gitea/issue_comment.go b/vendor/code.gitea.io/sdk/gitea/issue_comment.go
index 8131a6edc..cafbc4a60 100644
--- a/vendor/code.gitea.io/sdk/gitea/issue_comment.go
+++ b/vendor/code.gitea.io/sdk/gitea/issue_comment.go
@@ -149,6 +149,5 @@ func (c *Client) DeleteIssueComment(owner, repo string, commentID int64) (*Respo
 	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/issues/comments/%d", owner, repo, commentID), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/issues/comments/%d", owner, repo, commentID), nil, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/issue_label.go b/vendor/code.gitea.io/sdk/gitea/issue_label.go
index f343ee5ef..1868cd069 100644
--- a/vendor/code.gitea.io/sdk/gitea/issue_label.go
+++ b/vendor/code.gitea.io/sdk/gitea/issue_label.go
@@ -8,145 +8,8 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"regexp"
-	"strings"
 )
 
-// Label a label to an issue or a pr
-type Label struct {
-	ID   int64  `json:"id"`
-	Name string `json:"name"`
-	// example: 00aabb
-	Color       string `json:"color"`
-	Description string `json:"description"`
-	URL         string `json:"url"`
-}
-
-// ListLabelsOptions options for listing repository's labels
-type ListLabelsOptions struct {
-	ListOptions
-}
-
-// ListRepoLabels list labels of one repository
-func (c *Client) ListRepoLabels(owner, repo string, opt ListLabelsOptions) ([]*Label, *Response, error) {
-	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
-		return nil, nil, err
-	}
-	opt.setDefaults()
-	labels := make([]*Label, 0, opt.PageSize)
-	resp, err := c.getParsedResponse("GET", fmt.Sprintf("/repos/%s/%s/labels?%s", owner, repo, opt.getURLQuery().Encode()), nil, nil, &labels)
-	return labels, resp, err
-}
-
-// GetRepoLabel get one label of repository by repo it
-func (c *Client) GetRepoLabel(owner, repo string, id int64) (*Label, *Response, error) {
-	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
-		return nil, nil, err
-	}
-	label := new(Label)
-	resp, err := c.getParsedResponse("GET", fmt.Sprintf("/repos/%s/%s/labels/%d", owner, repo, id), nil, nil, label)
-	return label, resp, err
-}
-
-// CreateLabelOption options for creating a label
-type CreateLabelOption struct {
-	Name string `json:"name"`
-	// example: #00aabb
-	Color       string `json:"color"`
-	Description string `json:"description"`
-}
-
-// Validate the CreateLabelOption struct
-func (opt CreateLabelOption) Validate() error {
-	aw, err := regexp.MatchString("^#?[0-9,a-f,A-F]{6}$", opt.Color)
-	if err != nil {
-		return err
-	}
-	if !aw {
-		return fmt.Errorf("invalid color format")
-	}
-	if len(strings.TrimSpace(opt.Name)) == 0 {
-		return fmt.Errorf("empty name not allowed")
-	}
-	return nil
-}
-
-// CreateLabel create one label of repository
-func (c *Client) CreateLabel(owner, repo string, opt CreateLabelOption) (*Label, *Response, error) {
-	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
-		return nil, nil, err
-	}
-	if err := opt.Validate(); err != nil {
-		return nil, nil, err
-	}
-	if len(opt.Color) == 6 {
-		if err := c.checkServerVersionGreaterThanOrEqual(version1_12_0); err != nil {
-			opt.Color = "#" + opt.Color
-		}
-	}
-	body, err := json.Marshal(&opt)
-	if err != nil {
-		return nil, nil, err
-	}
-	label := new(Label)
-	resp, err := c.getParsedResponse("POST",
-		fmt.Sprintf("/repos/%s/%s/labels", owner, repo),
-		jsonHeader, bytes.NewReader(body), label)
-	return label, resp, err
-}
-
-// EditLabelOption options for editing a label
-type EditLabelOption struct {
-	Name        *string `json:"name"`
-	Color       *string `json:"color"`
-	Description *string `json:"description"`
-}
-
-// Validate the EditLabelOption struct
-func (opt EditLabelOption) Validate() error {
-	if opt.Color != nil {
-		aw, err := regexp.MatchString("^#?[0-9,a-f,A-F]{6}$", *opt.Color)
-		if err != nil {
-			return err
-		}
-		if !aw {
-			return fmt.Errorf("invalid color format")
-		}
-	}
-	if opt.Name != nil {
-		if len(strings.TrimSpace(*opt.Name)) == 0 {
-			return fmt.Errorf("empty name not allowed")
-		}
-	}
-	return nil
-}
-
-// EditLabel modify one label with options
-func (c *Client) EditLabel(owner, repo string, id int64, opt EditLabelOption) (*Label, *Response, error) {
-	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
-		return nil, nil, err
-	}
-	if err := opt.Validate(); err != nil {
-		return nil, nil, err
-	}
-	body, err := json.Marshal(&opt)
-	if err != nil {
-		return nil, nil, err
-	}
-	label := new(Label)
-	resp, err := c.getParsedResponse("PATCH", fmt.Sprintf("/repos/%s/%s/labels/%d", owner, repo, id), jsonHeader, bytes.NewReader(body), label)
-	return label, resp, err
-}
-
-// DeleteLabel delete one label of repository by id
-func (c *Client) DeleteLabel(owner, repo string, id int64) (*Response, error) {
-	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
-		return nil, err
-	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/labels/%d", owner, repo, id), nil, nil)
-	return resp, err
-}
-
 // GetIssueLabels get labels of one issue via issue id
 func (c *Client) GetIssueLabels(owner, repo string, index int64, opts ListLabelsOptions) ([]*Label, *Response, error) {
 	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
@@ -197,8 +60,7 @@ func (c *Client) DeleteIssueLabel(owner, repo string, index, label int64) (*Resp
 	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/issues/%d/labels/%d", owner, repo, index, label), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/issues/%d/labels/%d", owner, repo, index, label), nil, nil)
 }
 
 // ClearIssueLabels delete all the labels of one issue.
@@ -206,6 +68,5 @@ func (c *Client) ClearIssueLabels(owner, repo string, index int64) (*Response, e
 	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/issues/%d/labels", owner, repo, index), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/issues/%d/labels", owner, repo, index), nil, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/issue_milestone.go b/vendor/code.gitea.io/sdk/gitea/issue_milestone.go
index 4e0d79a9f..87fd49369 100644
--- a/vendor/code.gitea.io/sdk/gitea/issue_milestone.go
+++ b/vendor/code.gitea.io/sdk/gitea/issue_milestone.go
@@ -192,8 +192,7 @@ func (c *Client) DeleteMilestone(owner, repo string, id int64) (*Response, error
 	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/milestones/%d", owner, repo, id), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/milestones/%d", owner, repo, id), nil, nil)
 }
 
 // DeleteMilestoneByName delete one milestone by name
@@ -209,8 +208,7 @@ func (c *Client) DeleteMilestoneByName(owner, repo, name string) (*Response, err
 	if err := escapeValidatePathSegments(&owner, &repo, &name); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/milestones/%s", owner, repo, name), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/milestones/%s", owner, repo, name), nil, nil)
 }
 
 // resolveMilestoneByName is a fallback method to find milestone id by name
diff --git a/vendor/code.gitea.io/sdk/gitea/issue_reaction.go b/vendor/code.gitea.io/sdk/gitea/issue_reaction.go
index b45c06664..6c79faffe 100644
--- a/vendor/code.gitea.io/sdk/gitea/issue_reaction.go
+++ b/vendor/code.gitea.io/sdk/gitea/issue_reaction.go
@@ -68,8 +68,7 @@ func (c *Client) DeleteIssueReaction(owner, repo string, index int64, reaction s
 	if err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/issues/%d/reactions", owner, repo, index), jsonHeader, bytes.NewReader(body))
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/issues/%d/reactions", owner, repo, index), jsonHeader, bytes.NewReader(body))
 }
 
 // PostIssueCommentReaction add a reaction to a comment of an issue
@@ -97,8 +96,7 @@ func (c *Client) DeleteIssueCommentReaction(owner, repo string, commentID int64,
 	if err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE",
+	return c.doRequestWithStatusHandle("DELETE",
 		fmt.Sprintf("/repos/%s/%s/issues/comments/%d/reactions", owner, repo, commentID),
 		jsonHeader, bytes.NewReader(body))
-	return resp, err
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/issue_stopwatch.go b/vendor/code.gitea.io/sdk/gitea/issue_stopwatch.go
index ebb0b8ae4..72f0d7a32 100644
--- a/vendor/code.gitea.io/sdk/gitea/issue_stopwatch.go
+++ b/vendor/code.gitea.io/sdk/gitea/issue_stopwatch.go
@@ -32,8 +32,7 @@ func (c *Client) DeleteIssueStopwatch(owner, repo string, index int64) (*Respons
 	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/issues/%d/stopwatch/delete", owner, repo, index), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/issues/%d/stopwatch/delete", owner, repo, index), nil, nil)
 }
 
 // StartIssueStopWatch starts a stopwatch for an existing issue for a given
@@ -42,8 +41,7 @@ func (c *Client) StartIssueStopWatch(owner, repo string, index int64) (*Response
 	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("POST", fmt.Sprintf("/repos/%s/%s/issues/%d/stopwatch/start", owner, repo, index), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("POST", fmt.Sprintf("/repos/%s/%s/issues/%d/stopwatch/start", owner, repo, index), nil, nil)
 }
 
 // StopIssueStopWatch stops an existing stopwatch for an issue in a given
@@ -52,6 +50,5 @@ func (c *Client) StopIssueStopWatch(owner, repo string, index int64) (*Response,
 	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("POST", fmt.Sprintf("/repos/%s/%s/issues/%d/stopwatch/stop", owner, repo, index), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("POST", fmt.Sprintf("/repos/%s/%s/issues/%d/stopwatch/stop", owner, repo, index), nil, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/issue_tracked_time.go b/vendor/code.gitea.io/sdk/gitea/issue_tracked_time.go
index c55851623..164c1f0ee 100644
--- a/vendor/code.gitea.io/sdk/gitea/issue_tracked_time.go
+++ b/vendor/code.gitea.io/sdk/gitea/issue_tracked_time.go
@@ -128,8 +128,7 @@ func (c *Client) ResetIssueTime(owner, repo string, index int64) (*Response, err
 	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/issues/%d/times", owner, repo, index), jsonHeader, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/issues/%d/times", owner, repo, index), jsonHeader, nil)
 }
 
 // DeleteTime delete a specific tracked time by id of a single issue for a given repository
@@ -137,6 +136,5 @@ func (c *Client) DeleteTime(owner, repo string, index, timeID int64) (*Response,
 	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/issues/%d/times/%d", owner, repo, index, timeID), jsonHeader, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/issues/%d/times/%d", owner, repo, index, timeID), jsonHeader, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/notifications.go b/vendor/code.gitea.io/sdk/gitea/notifications.go
index 640cc4dae..a75a41e9b 100644
--- a/vendor/code.gitea.io/sdk/gitea/notifications.go
+++ b/vendor/code.gitea.io/sdk/gitea/notifications.go
@@ -174,7 +174,7 @@ func (c *Client) ReadNotification(id int64, status ...NotifyStatus) (*Notificati
 		resp, err := c.getParsedResponse("PATCH", link, nil, nil, thread)
 		return thread, resp, err
 	}
-	_, resp, err := c.getResponse("PATCH", link, nil, nil)
+	resp, err := c.doRequestWithStatusHandle("PATCH", link, nil, nil)
 	return nil, resp, err
 }
 
@@ -210,7 +210,7 @@ func (c *Client) ReadNotifications(opt MarkNotificationOptions) ([]*Notification
 		resp, err := c.getParsedResponse("PUT", link.String(), nil, nil, &threads)
 		return threads, resp, err
 	}
-	_, resp, err := c.getResponse("PUT", link.String(), nil, nil)
+	resp, err := c.doRequestWithStatusHandle("PUT", link.String(), nil, nil)
 	return nil, resp, err
 }
 
@@ -252,6 +252,6 @@ func (c *Client) ReadRepoNotifications(owner, repo string, opt MarkNotificationO
 		resp, err := c.getParsedResponse("PUT", link.String(), nil, nil, &threads)
 		return threads, resp, err
 	}
-	_, resp, err := c.getResponse("PUT", link.String(), nil, nil)
+	resp, err := c.doRequestWithStatusHandle("PUT", link.String(), nil, nil)
 	return nil, resp, err
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/oauth2.go b/vendor/code.gitea.io/sdk/gitea/oauth2.go
index 86e256838..7dfa14cac 100644
--- a/vendor/code.gitea.io/sdk/gitea/oauth2.go
+++ b/vendor/code.gitea.io/sdk/gitea/oauth2.go
@@ -88,6 +88,6 @@ func (c *Client) DeleteOauth2(oauth2id int64) (*Response, error) {
 	if err := c.checkServerVersionGreaterThanOrEqual(version1_12_0); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/user/applications/oauth2/%d", oauth2id), nil, nil)
+	resp, err := c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/user/applications/oauth2/%d", oauth2id), nil, nil)
 	return resp, err
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/org.go b/vendor/code.gitea.io/sdk/gitea/org.go
index 9df4d5318..729b638c7 100644
--- a/vendor/code.gitea.io/sdk/gitea/org.go
+++ b/vendor/code.gitea.io/sdk/gitea/org.go
@@ -141,8 +141,7 @@ func (c *Client) EditOrg(orgname string, opt EditOrgOption) (*Response, error) {
 	if err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PATCH", fmt.Sprintf("/orgs/%s", orgname), jsonHeader, bytes.NewReader(body))
-	return resp, err
+	return c.doRequestWithStatusHandle("PATCH", fmt.Sprintf("/orgs/%s", orgname), jsonHeader, bytes.NewReader(body))
 }
 
 // DeleteOrg deletes an organization
@@ -150,6 +149,5 @@ func (c *Client) DeleteOrg(orgname string) (*Response, error) {
 	if err := escapeValidatePathSegments(&orgname); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/orgs/%s", orgname), jsonHeader, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/orgs/%s", orgname), jsonHeader, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/org_action.go b/vendor/code.gitea.io/sdk/gitea/org_action.go
index 3ffa3f22d..0dafa9144 100644
--- a/vendor/code.gitea.io/sdk/gitea/org_action.go
+++ b/vendor/code.gitea.io/sdk/gitea/org_action.go
@@ -31,10 +31,152 @@ func (c *Client) ListOrgActionSecret(org string, opt ListOrgActionSecretOption)
 	return secrets, resp, err
 }
 
+type OrgActionVariable struct {
+	OwnerID     int64  `json:"owner_id"`
+	RepoID      int64  `json:"repo_id"`
+	Name        string `json:"name"`
+	Data        string `json:"data"`
+	Description string `json:"description"`
+}
+
+// ListOrgActionVariableOption lists OrgActionVariable options
+type ListOrgActionVariableOption struct {
+	ListOptions
+}
+
+// ListOrgActionVariable lists an organization's action variables
+func (c *Client) ListOrgActionVariable(org string, opt ListOrgActionVariableOption) ([]*OrgActionVariable, *Response, error) {
+	if err := escapeValidatePathSegments(&org); err != nil {
+		return nil, nil, err
+	}
+	opt.setDefaults()
+	variables := make([]*OrgActionVariable, 0, opt.PageSize)
+
+	link, _ := url.Parse(fmt.Sprintf("/orgs/%s/actions/variables", org))
+	link.RawQuery = opt.getURLQuery().Encode()
+	resp, err := c.getParsedResponse("GET", link.String(), jsonHeader, nil, &variables)
+	return variables, resp, err
+}
+
+// GetOrgActionVariable gets a single organization's action variable by name
+func (c *Client) GetOrgActionVariable(org, name string) (*OrgActionVariable, *Response, error) {
+	if err := escapeValidatePathSegments(&org, &name); err != nil {
+		return nil, nil, err
+	}
+	var variable OrgActionVariable
+	resp, err := c.getParsedResponse("GET",
+		fmt.Sprintf("/orgs/%s/actions/variables/%s", org, name),
+		jsonHeader, nil, &variable)
+	if err != nil {
+		return nil, resp, err
+	}
+	return &variable, resp, nil
+}
+
+// CreateOrgActionVariableOption represents the options for creating an org action variable.
+type CreateOrgActionVariableOption struct {
+	Name        string `json:"name"`        // Name is the name of the variable.
+	Value       string `json:"value"`       // Value is the value of the variable.
+	Description string `json:"description"` // Description is the description of the variable.
+}
+
+// Validate checks if the CreateOrgActionVariableOption is valid.
+func (opt *CreateOrgActionVariableOption) Validate() error {
+	if len(opt.Name) == 0 {
+		return fmt.Errorf("name required")
+	}
+	if len(opt.Name) > 30 {
+		return fmt.Errorf("name too long")
+	}
+	if len(opt.Value) == 0 {
+		return fmt.Errorf("value required")
+	}
+	return nil
+}
+
+// CreateOrgActionVariable creates a variable for the specified organization in the Gitea Actions.
+func (c *Client) CreateOrgActionVariable(org string, opt CreateOrgActionVariableOption) (*Response, error) {
+	if err := escapeValidatePathSegments(&org); err != nil {
+		return nil, err
+	}
+	if err := (&opt).Validate(); err != nil {
+		return nil, err
+	}
+	body, err := json.Marshal(&opt)
+	if err != nil {
+		return nil, err
+	}
+
+	status, resp, err := c.getStatusCode("POST", fmt.Sprintf("/orgs/%s/actions/variables/%s", org, opt.Name), jsonHeader, bytes.NewReader(body))
+	if err != nil {
+		return nil, err
+	}
+
+	switch status {
+	case http.StatusCreated:
+		return resp, nil
+	case http.StatusNoContent:
+		return resp, nil
+	case http.StatusNotFound:
+		return resp, fmt.Errorf("forbidden")
+	case http.StatusBadRequest:
+		return resp, fmt.Errorf("bad request")
+	default:
+		return resp, fmt.Errorf("unexpected Status: %d", status)
+	}
+}
+
+// UpdateOrgActionVariableOption represents the options for updating an org action variable.
+type UpdateOrgActionVariableOption struct {
+	Value       string `json:"value"`       // Value is the new value of the variable.
+	Description string `json:"description"` // Description is the new description of the variable.
+}
+
+// Validate checks if the UpdateOrgActionVariableOption is valid.
+func (opt *UpdateOrgActionVariableOption) Validate() error {
+	if len(opt.Value) == 0 {
+		return fmt.Errorf("value required")
+	}
+	return nil
+}
+
+// UpdateOrgActionVariable updates a variable for the specified organization in the Gitea Actions.
+func (c *Client) UpdateOrgActionVariable(org, name string, opt UpdateOrgActionVariableOption) (*Response, error) {
+	if err := escapeValidatePathSegments(&org, &name); err != nil {
+		return nil, err
+	}
+	if err := (&opt).Validate(); err != nil {
+		return nil, err
+	}
+	body, err := json.Marshal(&opt)
+	if err != nil {
+		return nil, err
+	}
+
+	status, resp, err := c.getStatusCode("PUT", fmt.Sprintf("/orgs/%s/actions/variables/%s", org, name), jsonHeader, bytes.NewReader(body))
+	if err != nil {
+		return nil, err
+	}
+
+	switch status {
+	case http.StatusOK:
+		return resp, nil
+	case http.StatusNoContent:
+		return resp, nil
+	case http.StatusNotFound:
+		return resp, fmt.Errorf("forbidden")
+	case http.StatusBadRequest:
+		return resp, fmt.Errorf("bad request")
+	default:
+		return resp, fmt.Errorf("unexpected Status: %d", status)
+	}
+}
+
 // CreateSecretOption represents the options for creating a secret.
 type CreateSecretOption struct {
-	Name string `json:"name"` // Name is the name of the secret.
-	Data string `json:"data"` // Data is the data of the secret.
+	Name        string `json:"name"`        // Name is the name of the secret.
+	Data        string `json:"data"`        // Data is the data of the secret.
+	Description string `json:"description"` // Description is the description of the secret.
 }
 
 // Validate checks if the CreateSecretOption is valid.
diff --git a/vendor/code.gitea.io/sdk/gitea/org_label.go b/vendor/code.gitea.io/sdk/gitea/org_label.go
new file mode 100644
index 000000000..c524235e1
--- /dev/null
+++ b/vendor/code.gitea.io/sdk/gitea/org_label.go
@@ -0,0 +1,116 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package gitea
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"regexp"
+	"strings"
+)
+
+// ListOrgLabelsOptions options for listing organization labels
+type ListOrgLabelsOptions struct {
+	ListOptions
+}
+
+// ListOrgLabels returns the labels defined at the org level
+func (c *Client) ListOrgLabels(orgName string, opt ListOrgLabelsOptions) ([]*Label, *Response, error) {
+	if err := escapeValidatePathSegments(&orgName); err != nil {
+		return nil, nil, err
+	}
+	opt.setDefaults()
+	labels := make([]*Label, 0, opt.PageSize)
+	link, _ := url.Parse(fmt.Sprintf("/orgs/%s/labels", orgName))
+	link.RawQuery = opt.getURLQuery().Encode()
+	resp, err := c.getParsedResponse("GET", link.String(), jsonHeader, nil, &labels)
+	return labels, resp, err
+}
+
+type CreateOrgLabelOption struct {
+	// Name of the label
+	Name string `json:"name"`
+	// Color of the label in hex format without #
+	Color string `json:"color"`
+	// Description of the label
+	Description string `json:"description"`
+	// Whether this is an exclusive label
+	Exclusive bool `json:"exclusive"`
+}
+
+// Validate the CreateLabelOption struct
+func (opt CreateOrgLabelOption) Validate() error {
+	aw, err := regexp.MatchString("^#?[0-9,a-f,A-F]{6}$", opt.Color)
+	if err != nil {
+		return err
+	}
+	if !aw {
+		return fmt.Errorf("invalid color format")
+	}
+	if len(strings.TrimSpace(opt.Name)) == 0 {
+		return fmt.Errorf("empty name not allowed")
+	}
+	return nil
+}
+
+// CreateOrgLabel creates a new label under an organization
+func (c *Client) CreateOrgLabel(orgName string, opt CreateOrgLabelOption) (*Label, *Response, error) {
+	if err := escapeValidatePathSegments(&orgName); err != nil {
+		return nil, nil, err
+	}
+	body, err := json.Marshal(&opt)
+	if err != nil {
+		return nil, nil, err
+	}
+	label := new(Label)
+	resp, err := c.getParsedResponse("POST", fmt.Sprintf("/orgs/%s/labels", orgName), jsonHeader, bytes.NewReader(body), label)
+	return label, resp, err
+}
+
+// GetOrgLabel get one label of organization by org it
+func (c *Client) GetOrgLabel(orgName string, labelID int64) (*Label, *Response, error) {
+	if err := escapeValidatePathSegments(&orgName); err != nil {
+		return nil, nil, err
+	}
+	label := new(Label)
+	resp, err := c.getParsedResponse("GET", fmt.Sprintf("/orgs/%s/labels/%d", orgName, labelID), nil, nil, label)
+	return label, resp, err
+}
+
+type EditOrgLabelOption struct {
+	// New name of the label
+	Name *string `json:"name"`
+	// New color of the label in hex format without #
+	Color *string `json:"color"`
+	// New description of the label
+	Description *string `json:"description"`
+	// Whether this is an exclusive label
+	Exclusive *bool `json:"exclusive,omitempty"`
+}
+
+// EditOrgLabel edits an existing org-level label by ID
+func (c *Client) EditOrgLabel(orgName string, labelID int64, opt EditOrgLabelOption) (*Label, *Response, error) {
+	if err := escapeValidatePathSegments(&orgName); err != nil {
+		return nil, nil, err
+	}
+	body, err := json.Marshal(&opt)
+	if err != nil {
+		return nil, nil, err
+	}
+	label := new(Label)
+	resp, err := c.getParsedResponse("PATCH", fmt.Sprintf("/orgs/%s/labels/%d", orgName, labelID), jsonHeader, bytes.NewReader(body), label)
+	return label, resp, err
+}
+
+// DeleteOrgLabel deletes a org label by ID
+func (c *Client) DeleteOrgLabel(orgName string, labelID int64) (*Response, error) {
+	if err := escapeValidatePathSegments(&orgName); err != nil {
+		return nil, err
+	}
+	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/orgs/%s/labels/%d", orgName, labelID), jsonHeader, nil)
+	return resp, err
+}
diff --git a/vendor/code.gitea.io/sdk/gitea/org_member.go b/vendor/code.gitea.io/sdk/gitea/org_member.go
index 79dad4e87..91ac44a78 100644
--- a/vendor/code.gitea.io/sdk/gitea/org_member.go
+++ b/vendor/code.gitea.io/sdk/gitea/org_member.go
@@ -15,8 +15,7 @@ func (c *Client) DeleteOrgMembership(org, user string) (*Response, error) {
 	if err := escapeValidatePathSegments(&org, &user); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/orgs/%s/members/%s", org, user), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/orgs/%s/members/%s", org, user), nil, nil)
 }
 
 // ListOrgMembershipOption list OrgMembership options
diff --git a/vendor/code.gitea.io/sdk/gitea/org_team.go b/vendor/code.gitea.io/sdk/gitea/org_team.go
index d24574680..070b4f91b 100644
--- a/vendor/code.gitea.io/sdk/gitea/org_team.go
+++ b/vendor/code.gitea.io/sdk/gitea/org_team.go
@@ -202,14 +202,12 @@ func (c *Client) EditTeam(id int64, opt EditTeamOption) (*Response, error) {
 	if err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PATCH", fmt.Sprintf("/teams/%d", id), jsonHeader, bytes.NewReader(body))
-	return resp, err
+	return c.doRequestWithStatusHandle("PATCH", fmt.Sprintf("/teams/%d", id), jsonHeader, bytes.NewReader(body))
 }
 
 // DeleteTeam deletes a team of an organization
 func (c *Client) DeleteTeam(id int64) (*Response, error) {
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/teams/%d", id), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/teams/%d", id), nil, nil)
 }
 
 // ListTeamMembersOptions options for listing team's members
@@ -240,8 +238,7 @@ func (c *Client) AddTeamMember(id int64, user string) (*Response, error) {
 	if err := escapeValidatePathSegments(&user); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PUT", fmt.Sprintf("/teams/%d/members/%s", id, user), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("PUT", fmt.Sprintf("/teams/%d/members/%s", id, user), nil, nil)
 }
 
 // RemoveTeamMember removes a member from a team
@@ -249,8 +246,7 @@ func (c *Client) RemoveTeamMember(id int64, user string) (*Response, error) {
 	if err := escapeValidatePathSegments(&user); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/teams/%d/members/%s", id, user), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/teams/%d/members/%s", id, user), nil, nil)
 }
 
 // ListTeamRepositoriesOptions options for listing team's repositories
@@ -271,8 +267,7 @@ func (c *Client) AddTeamRepository(id int64, org, repo string) (*Response, error
 	if err := escapeValidatePathSegments(&org, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PUT", fmt.Sprintf("/teams/%d/repos/%s/%s", id, org, repo), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("PUT", fmt.Sprintf("/teams/%d/repos/%s/%s", id, org, repo), nil, nil)
 }
 
 // RemoveTeamRepository removes a repository from a team
@@ -280,6 +275,5 @@ func (c *Client) RemoveTeamRepository(id int64, org, repo string) (*Response, er
 	if err := escapeValidatePathSegments(&org, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/teams/%d/repos/%s/%s", id, org, repo), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/teams/%d/repos/%s/%s", id, org, repo), nil, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/package.go b/vendor/code.gitea.io/sdk/gitea/package.go
index 41ffdee64..c55933c07 100644
--- a/vendor/code.gitea.io/sdk/gitea/package.go
+++ b/vendor/code.gitea.io/sdk/gitea/package.go
@@ -78,8 +78,7 @@ func (c *Client) DeletePackage(owner, packageType, name, version string) (*Respo
 	if err := escapeValidatePathSegments(&owner, &packageType, &name, &version); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/packages/%s/%s/%s/%s", owner, packageType, name, version), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/packages/%s/%s/%s/%s", owner, packageType, name, version), nil, nil)
 }
 
 // ListPackageFiles lists the files within a package
diff --git a/vendor/code.gitea.io/sdk/gitea/pull.go b/vendor/code.gitea.io/sdk/gitea/pull.go
index 8ca816472..5c615869a 100644
--- a/vendor/code.gitea.io/sdk/gitea/pull.go
+++ b/vendor/code.gitea.io/sdk/gitea/pull.go
@@ -36,6 +36,7 @@ type PullRequest struct {
 	Assignee  *User      `json:"assignee"`
 	Assignees []*User    `json:"assignees"`
 	State     StateType  `json:"state"`
+	Draft     bool       `json:"draft"`
 	IsLocked  bool       `json:"is_locked"`
 	Comments  int        `json:"comments"`
 
@@ -149,15 +150,17 @@ func (c *Client) GetPullRequest(owner, repo string, index int64) (*PullRequest,
 
 // CreatePullRequestOption options when creating a pull request
 type CreatePullRequestOption struct {
-	Head      string     `json:"head"`
-	Base      string     `json:"base"`
-	Title     string     `json:"title"`
-	Body      string     `json:"body"`
-	Assignee  string     `json:"assignee"`
-	Assignees []string   `json:"assignees"`
-	Milestone int64      `json:"milestone"`
-	Labels    []int64    `json:"labels"`
-	Deadline  *time.Time `json:"due_date"`
+	Head          string     `json:"head"`
+	Base          string     `json:"base"`
+	Title         string     `json:"title"`
+	Body          string     `json:"body"`
+	Assignee      string     `json:"assignee"`
+	Assignees     []string   `json:"assignees"`
+	Reviewers     []string   `json:"reviewers"`
+	TeamReviewers []string   `json:"team_reviewers"`
+	Milestone     int64      `json:"milestone"`
+	Labels        []int64    `json:"labels"`
+	Deadline      *time.Time `json:"due_date"`
 }
 
 // CreatePullRequest create pull request with options
@@ -179,7 +182,7 @@ func (c *Client) CreatePullRequest(owner, repo string, opt CreatePullRequestOpti
 // EditPullRequestOption options when modify pull request
 type EditPullRequestOption struct {
 	Title               string     `json:"title"`
-	Body                string     `json:"body"`
+	Body                *string    `json:"body"`
 	Base                string     `json:"base"`
 	Assignee            string     `json:"assignee"`
 	Assignees           []string   `json:"assignees"`
diff --git a/vendor/code.gitea.io/sdk/gitea/pull_review.go b/vendor/code.gitea.io/sdk/gitea/pull_review.go
index 6d32c4f12..e5e39bd2e 100644
--- a/vendor/code.gitea.io/sdk/gitea/pull_review.go
+++ b/vendor/code.gitea.io/sdk/gitea/pull_review.go
@@ -202,8 +202,7 @@ func (c *Client) DeletePullReview(owner, repo string, index, id int64) (*Respons
 		return nil, err
 	}
 
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/pulls/%d/reviews/%d", owner, repo, index, id), jsonHeader, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/pulls/%d/reviews/%d", owner, repo, index, id), jsonHeader, nil)
 }
 
 // CreatePullReview create a review to an pull request
@@ -265,10 +264,9 @@ func (c *Client) CreateReviewRequests(owner, repo string, index int64, opt PullR
 		return nil, err
 	}
 
-	_, resp, err := c.getResponse("POST",
+	return c.doRequestWithStatusHandle("POST",
 		fmt.Sprintf("/repos/%s/%s/pulls/%d/requested_reviewers", owner, repo, index),
 		jsonHeader, bytes.NewReader(body))
-	return resp, err
 }
 
 // DeleteReviewRequests delete review requests to an pull request
@@ -284,10 +282,9 @@ func (c *Client) DeleteReviewRequests(owner, repo string, index int64, opt PullR
 		return nil, err
 	}
 
-	_, resp, err := c.getResponse("DELETE",
+	return c.doRequestWithStatusHandle("DELETE",
 		fmt.Sprintf("/repos/%s/%s/pulls/%d/requested_reviewers", owner, repo, index),
 		jsonHeader, bytes.NewReader(body))
-	return resp, err
 }
 
 // DismissPullReview dismiss a review for a pull request
@@ -303,10 +300,9 @@ func (c *Client) DismissPullReview(owner, repo string, index, id int64, opt Dism
 		return nil, err
 	}
 
-	_, resp, err := c.getResponse("POST",
+	return c.doRequestWithStatusHandle("POST",
 		fmt.Sprintf("/repos/%s/%s/pulls/%d/reviews/%d/dismissals", owner, repo, index, id),
 		jsonHeader, bytes.NewReader(body))
-	return resp, err
 }
 
 // UnDismissPullReview cancel to dismiss a review for a pull request
@@ -318,8 +314,7 @@ func (c *Client) UnDismissPullReview(owner, repo string, index, id int64) (*Resp
 		return nil, err
 	}
 
-	_, resp, err := c.getResponse("POST",
+	return c.doRequestWithStatusHandle("POST",
 		fmt.Sprintf("/repos/%s/%s/pulls/%d/reviews/%d/undismissals", owner, repo, index, id),
 		jsonHeader, nil)
-	return resp, err
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/release.go b/vendor/code.gitea.io/sdk/gitea/release.go
index f3707b704..d53be26c3 100644
--- a/vendor/code.gitea.io/sdk/gitea/release.go
+++ b/vendor/code.gitea.io/sdk/gitea/release.go
@@ -173,10 +173,9 @@ func (c *Client) DeleteRelease(user, repo string, id int64) (*Response, error) {
 	if err := escapeValidatePathSegments(&user, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE",
+	return c.doRequestWithStatusHandle("DELETE",
 		fmt.Sprintf("/repos/%s/%s/releases/%d", user, repo, id),
 		nil, nil)
-	return resp, err
 }
 
 // DeleteReleaseByTag deletes a release frm a repository by tag
@@ -187,10 +186,9 @@ func (c *Client) DeleteReleaseByTag(user, repo, tag string) (*Response, error) {
 	if err := c.checkServerVersionGreaterThanOrEqual(version1_14_0); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE",
+	return c.doRequestWithStatusHandle("DELETE",
 		fmt.Sprintf("/repos/%s/%s/releases/tags/%s", user, repo, tag),
 		nil, nil)
-	return resp, err
 }
 
 // fallbackGetReleaseByTag is fallback for old gitea installations ( < 1.13.0 )
diff --git a/vendor/code.gitea.io/sdk/gitea/repo.go b/vendor/code.gitea.io/sdk/gitea/repo.go
index b8dba5b2a..3f8e7a998 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo.go
@@ -259,10 +259,10 @@ func (opt *SearchRepoOptions) QueryEncode() string {
 
 	// Repo Attributes
 	if opt.IsPrivate != nil {
-		query.Add("is_private", fmt.Sprintf("%v", opt.IsPrivate))
+		query.Add("is_private", fmt.Sprintf("%t", *opt.IsPrivate))
 	}
 	if opt.IsArchived != nil {
-		query.Add("archived", fmt.Sprintf("%v", opt.IsArchived))
+		query.Add("archived", fmt.Sprintf("%t", *opt.IsArchived))
 	}
 	if opt.ExcludeTemplate {
 		query.Add("template", "false")
@@ -506,8 +506,7 @@ func (c *Client) DeleteRepo(owner, repo string) (*Response, error) {
 	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s", owner, repo), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s", owner, repo), nil, nil)
 }
 
 // MirrorSync adds a mirrored repository to the mirror sync queue.
@@ -515,8 +514,7 @@ func (c *Client) MirrorSync(owner, repo string) (*Response, error) {
 	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("POST", fmt.Sprintf("/repos/%s/%s/mirror-sync", owner, repo), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("POST", fmt.Sprintf("/repos/%s/%s/mirror-sync", owner, repo), nil, nil)
 }
 
 // GetRepoLanguages return language stats of a repo
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_action.go b/vendor/code.gitea.io/sdk/gitea/repo_action.go
index 65bccccb8..1764dbb4c 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo_action.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo_action.go
@@ -84,8 +84,7 @@ func (c *Client) DeleteRepoActionSecret(user, repo, secretName string) (*Respons
 		return nil, err
 	}
 
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/actions/secrets/%s", user, repo, secretName), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/actions/secrets/%s", user, repo, secretName), nil, nil)
 }
 
 // GetRepoActionVariable returns a repository variable in the Gitea Actions.
@@ -117,8 +116,7 @@ func (c *Client) CreateRepoActionVariable(user, repo, variableName, value string
 		return nil, err
 	}
 
-	_, resp, err := c.getResponse("POST", fmt.Sprintf("/repos/%s/%s/actions/variables/%s", user, repo, variableName), jsonHeader, bytes.NewReader(body))
-	return resp, err
+	return c.doRequestWithStatusHandle("POST", fmt.Sprintf("/repos/%s/%s/actions/variables/%s", user, repo, variableName), jsonHeader, bytes.NewReader(body))
 }
 
 // UpdateRepoActionVariable updates a repository variable in the Gitea Actions.
@@ -139,8 +137,7 @@ func (c *Client) UpdateRepoActionVariable(user, repo, variableName, value string
 		return nil, err
 	}
 
-	_, resp, err := c.getResponse("PUT", fmt.Sprintf("/repos/%s/%s/actions/variables/%s", user, repo, variableName), jsonHeader, bytes.NewReader(body))
-	return resp, err
+	return c.doRequestWithStatusHandle("PUT", fmt.Sprintf("/repos/%s/%s/actions/variables/%s", user, repo, variableName), jsonHeader, bytes.NewReader(body))
 }
 
 // DeleteRepoActionVariable deletes a repository variable in the Gitea Actions.
@@ -151,6 +148,5 @@ func (c *Client) DeleteRepoActionVariable(user, reponame, variableName string) (
 		return nil, err
 	}
 
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/actions/variables/%s", user, reponame, variableName), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/actions/variables/%s", user, reponame, variableName), nil, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_branch.go b/vendor/code.gitea.io/sdk/gitea/repo_branch.go
index 0b7e873c4..637ad5f2f 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo_branch.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo_branch.go
@@ -8,6 +8,7 @@ package gitea
 import (
 	"bytes"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"time"
 )
@@ -100,6 +101,36 @@ func (c *Client) DeleteRepoBranch(user, repo, branch string) (bool, *Response, e
 	return status == 204, resp, nil
 }
 
+type UpdateRepoBranchOption struct {
+	Name string `json:"name"`
+}
+
+func (opt UpdateRepoBranchOption) Validate() error {
+	if len(opt.Name) == 0 {
+		return errors.New("empty Name field")
+	}
+
+	return nil
+}
+
+func (c *Client) UpdateRepoBranch(user, repo, branch string, opt UpdateRepoBranchOption) (sucessful bool, resp *Response, err error) {
+	if err := escapeValidatePathSegments(&user, &repo, &branch); err != nil {
+		return false, nil, err
+	}
+	if err := c.checkServerVersionGreaterThanOrEqual(version1_23_0); err != nil {
+		return false, nil, err
+	}
+	if err := opt.Validate(); err != nil {
+		return false, nil, err
+	}
+	body, err := json.Marshal(&opt)
+	if err != nil {
+		return false, nil, err
+	}
+	status, resp, err := c.getStatusCode("PATCH", fmt.Sprintf("/repos/%s/%s/branches/%s", user, repo, branch), jsonHeader, bytes.NewReader(body))
+	return status == 204, resp, err
+}
+
 // CreateBranchOption options when creating a branch in a repository
 type CreateBranchOption struct {
 	// Name of the branch to create
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_branch_protection.go b/vendor/code.gitea.io/sdk/gitea/repo_branch_protection.go
index 3196d03bf..4f40dc360 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo_branch_protection.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo_branch_protection.go
@@ -168,6 +168,5 @@ func (c *Client) DeleteBranchProtection(owner, repo, name string) (*Response, er
 	if err := c.checkServerVersionGreaterThanOrEqual(version1_12_0); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/branch_protections/%s", owner, repo, name), jsonHeader, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/branch_protections/%s", owner, repo, name), jsonHeader, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_collaborator.go b/vendor/code.gitea.io/sdk/gitea/repo_collaborator.go
index 41e257655..2825a3eda 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo_collaborator.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo_collaborator.go
@@ -122,8 +122,7 @@ func (c *Client) AddCollaborator(user, repo, collaborator string, opt AddCollabo
 	if err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PUT", fmt.Sprintf("/repos/%s/%s/collaborators/%s", user, repo, collaborator), jsonHeader, bytes.NewReader(body))
-	return resp, err
+	return c.doRequestWithStatusHandle("PUT", fmt.Sprintf("/repos/%s/%s/collaborators/%s", user, repo, collaborator), jsonHeader, bytes.NewReader(body))
 }
 
 // DeleteCollaborator remove a collaborator from a repository
@@ -131,9 +130,8 @@ func (c *Client) DeleteCollaborator(user, repo, collaborator string) (*Response,
 	if err := escapeValidatePathSegments(&user, &repo, &collaborator); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE",
+	return c.doRequestWithStatusHandle("DELETE",
 		fmt.Sprintf("/repos/%s/%s/collaborators/%s", user, repo, collaborator), nil, nil)
-	return resp, err
 }
 
 // GetReviewers return all users that can be requested to review in this repo
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_file.go b/vendor/code.gitea.io/sdk/gitea/repo_file.go
index 9285be2aa..1c078d9d6 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo_file.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo_file.go
@@ -126,7 +126,11 @@ func (c *Client) GetFile(owner, repo, ref, filepath string, resolveLFS ...bool)
 	if reader == nil {
 		return nil, resp, err
 	}
-	defer reader.Close()
+	defer func() {
+		if closeErr := reader.Close(); closeErr != nil && err == nil {
+			err = closeErr
+		}
+	}()
 
 	data, err2 := io.ReadAll(reader)
 	if err2 != nil {
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_key.go b/vendor/code.gitea.io/sdk/gitea/repo_key.go
index ee2ff4084..b9964fb78 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo_key.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo_key.go
@@ -86,6 +86,5 @@ func (c *Client) DeleteDeployKey(owner, repo string, keyID int64) (*Response, er
 	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/keys/%d", owner, repo, keyID), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/keys/%d", owner, repo, keyID), nil, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_label.go b/vendor/code.gitea.io/sdk/gitea/repo_label.go
new file mode 100644
index 000000000..097928ade
--- /dev/null
+++ b/vendor/code.gitea.io/sdk/gitea/repo_label.go
@@ -0,0 +1,150 @@
+// Copyright 2016 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package gitea
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"regexp"
+	"strings"
+)
+
+// Label a label to an issue or a pr
+type Label struct {
+	ID   int64  `json:"id"`
+	Name string `json:"name"`
+	// example: 00aabb
+	Color       string `json:"color"`
+	Description string `json:"description"`
+	Exclusive   bool   `json:"exclusive"`
+	URL         string `json:"url"`
+}
+
+// ListLabelsOptions options for listing repository's labels
+type ListLabelsOptions struct {
+	ListOptions
+}
+
+// ListRepoLabels list labels of one repository
+func (c *Client) ListRepoLabels(owner, repo string, opt ListLabelsOptions) ([]*Label, *Response, error) {
+	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
+		return nil, nil, err
+	}
+	opt.setDefaults()
+	labels := make([]*Label, 0, opt.PageSize)
+	resp, err := c.getParsedResponse("GET", fmt.Sprintf("/repos/%s/%s/labels?%s", owner, repo, opt.getURLQuery().Encode()), nil, nil, &labels)
+	return labels, resp, err
+}
+
+// GetRepoLabel get one label of repository by repo it
+func (c *Client) GetRepoLabel(owner, repo string, id int64) (*Label, *Response, error) {
+	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
+		return nil, nil, err
+	}
+	label := new(Label)
+	resp, err := c.getParsedResponse("GET", fmt.Sprintf("/repos/%s/%s/labels/%d", owner, repo, id), nil, nil, label)
+	return label, resp, err
+}
+
+// CreateLabelOption options for creating a label
+type CreateLabelOption struct {
+	Name string `json:"name"`
+	// example: #00aabb
+	Color       string `json:"color"`
+	Description string `json:"description"`
+	Exclusive   bool   `json:"exclusive"`
+}
+
+// Validate the CreateLabelOption struct
+func (opt CreateLabelOption) Validate() error {
+	aw, err := regexp.MatchString("^#?[0-9,a-f,A-F]{6}$", opt.Color)
+	if err != nil {
+		return err
+	}
+	if !aw {
+		return fmt.Errorf("invalid color format")
+	}
+	if len(strings.TrimSpace(opt.Name)) == 0 {
+		return fmt.Errorf("empty name not allowed")
+	}
+	return nil
+}
+
+// CreateLabel create one label of repository
+func (c *Client) CreateLabel(owner, repo string, opt CreateLabelOption) (*Label, *Response, error) {
+	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
+		return nil, nil, err
+	}
+	if err := opt.Validate(); err != nil {
+		return nil, nil, err
+	}
+	if len(opt.Color) == 6 {
+		if err := c.checkServerVersionGreaterThanOrEqual(version1_12_0); err != nil {
+			opt.Color = "#" + opt.Color
+		}
+	}
+	body, err := json.Marshal(&opt)
+	if err != nil {
+		return nil, nil, err
+	}
+	label := new(Label)
+	resp, err := c.getParsedResponse("POST",
+		fmt.Sprintf("/repos/%s/%s/labels", owner, repo),
+		jsonHeader, bytes.NewReader(body), label)
+	return label, resp, err
+}
+
+// EditLabelOption options for editing a label
+type EditLabelOption struct {
+	Name        *string `json:"name"`
+	Color       *string `json:"color"`
+	Description *string `json:"description"`
+	Exclusive   *bool   `json:"exclusive"`
+}
+
+// Validate the EditLabelOption struct
+func (opt EditLabelOption) Validate() error {
+	if opt.Color != nil {
+		aw, err := regexp.MatchString("^#?[0-9,a-f,A-F]{6}$", *opt.Color)
+		if err != nil {
+			return err
+		}
+		if !aw {
+			return fmt.Errorf("invalid color format")
+		}
+	}
+	if opt.Name != nil {
+		if len(strings.TrimSpace(*opt.Name)) == 0 {
+			return fmt.Errorf("empty name not allowed")
+		}
+	}
+	return nil
+}
+
+// EditLabel modify one label with options
+func (c *Client) EditLabel(owner, repo string, id int64, opt EditLabelOption) (*Label, *Response, error) {
+	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
+		return nil, nil, err
+	}
+	if err := opt.Validate(); err != nil {
+		return nil, nil, err
+	}
+	body, err := json.Marshal(&opt)
+	if err != nil {
+		return nil, nil, err
+	}
+	label := new(Label)
+	resp, err := c.getParsedResponse("PATCH", fmt.Sprintf("/repos/%s/%s/labels/%d", owner, repo, id), jsonHeader, bytes.NewReader(body), label)
+	return label, resp, err
+}
+
+// DeleteLabel delete one label of repository by id
+func (c *Client) DeleteLabel(owner, repo string, id int64) (*Response, error) {
+	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
+		return nil, err
+	}
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/labels/%d", owner, repo, id), nil, nil)
+}
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_migrate.go b/vendor/code.gitea.io/sdk/gitea/repo_migrate.go
index 2de215360..73a04bc8f 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo_migrate.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo_migrate.go
@@ -55,15 +55,15 @@ type MigrateRepoOption struct {
 func (opt *MigrateRepoOption) Validate(c *Client) error {
 	// check user options
 	if len(opt.CloneAddr) == 0 {
-		return fmt.Errorf("CloneAddr required")
+		return fmt.Errorf("clone addr required")
 	}
 	if len(opt.RepoName) == 0 {
-		return fmt.Errorf("RepoName required")
+		return fmt.Errorf("repo name required")
 	} else if len(opt.RepoName) > 100 {
-		return fmt.Errorf("RepoName to long")
+		return fmt.Errorf("repo name too long")
 	}
 	if len(opt.Description) > 2048 {
-		return fmt.Errorf("Description to long")
+		return fmt.Errorf("description too long")
 	}
 	switch opt.Service {
 	case GitServiceGithub:
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_mirror.go b/vendor/code.gitea.io/sdk/gitea/repo_mirror.go
index df320940a..2ee806f10 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo_mirror.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo_mirror.go
@@ -43,3 +43,34 @@ func (c *Client) PushMirrors(user, repo string, opt CreatePushMirrorOption) (*Pu
 	resp, err := c.getParsedResponse("POST", fmt.Sprintf("/repos/%s/%s/push_mirrors", user, repo), jsonHeader, bytes.NewReader(body), &pm)
 	return pm, resp, err
 }
+
+// ListPushMirrors gets all push mirrors of a repository
+func (c *Client) ListPushMirrors(user, repo string, opt ListOptions) ([]*PushMirrorResponse, *Response, error) {
+	if err := escapeValidatePathSegments(&user, &repo); err != nil {
+		return nil, nil, err
+	}
+	opt.setDefaults()
+	pms := make([]*PushMirrorResponse, 0, opt.PageSize)
+	resp, err := c.getParsedResponse("GET",
+		fmt.Sprintf("/repos/%s/%s/push_mirrors?%s", user, repo, opt.getURLQuery().Encode()),
+		nil, nil, &pms)
+	return pms, resp, err
+}
+
+// GetPushMirrorByRemoteName get a push mirror of the repository by remote name
+func (c *Client) GetPushMirrorByRemoteName(user, repo, remoteName string) (*PushMirrorResponse, *Response, error) {
+	if err := escapeValidatePathSegments(&user, &repo, &remoteName); err != nil {
+		return nil, nil, err
+	}
+	pm := new(PushMirrorResponse)
+	resp, err := c.getParsedResponse("GET", fmt.Sprintf("/repos/%s/%s/push_mirrors/%s", user, repo, remoteName), nil, nil, &pm)
+	return pm, resp, err
+}
+
+// DeletePushMirror deletes a push mirror from a repository by remote name
+func (c *Client) DeletePushMirror(user, repo, remoteName string) (*Response, error) {
+	if err := escapeValidatePathSegments(&user, &repo, &remoteName); err != nil {
+		return nil, err
+	}
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/push_mirrors/%s", user, repo, remoteName), nil, nil)
+}
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_stars.go b/vendor/code.gitea.io/sdk/gitea/repo_stars.go
index 01243c250..45488eb67 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo_stars.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo_stars.go
@@ -47,7 +47,7 @@ func (c *Client) IsRepoStarring(user, repo string) (bool, *Response, error) {
 	if err := escapeValidatePathSegments(&user, &repo); err != nil {
 		return false, nil, err
 	}
-	_, resp, err := c.getResponse("GET", fmt.Sprintf("/user/starred/%s/%s", user, repo), jsonHeader, nil)
+	resp, err := c.doRequestWithStatusHandle("GET", fmt.Sprintf("/user/starred/%s/%s", user, repo), jsonHeader, nil)
 	if resp != nil {
 		switch resp.StatusCode {
 		case http.StatusNotFound:
@@ -66,7 +66,7 @@ func (c *Client) StarRepo(user, repo string) (*Response, error) {
 	if err := escapeValidatePathSegments(&user, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PUT", fmt.Sprintf("/user/starred/%s/%s", user, repo), jsonHeader, nil)
+	resp, err := c.doRequestWithStatusHandle("PUT", fmt.Sprintf("/user/starred/%s/%s", user, repo), jsonHeader, nil)
 	if resp != nil {
 		switch resp.StatusCode {
 		case http.StatusNoContent:
@@ -83,7 +83,7 @@ func (c *Client) UnStarRepo(user, repo string) (*Response, error) {
 	if err := escapeValidatePathSegments(&user, &repo); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/user/starred/%s/%s", user, repo), jsonHeader, nil)
+	resp, err := c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/user/starred/%s/%s", user, repo), jsonHeader, nil)
 	if resp != nil {
 		switch resp.StatusCode {
 		case http.StatusNoContent:
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_tag.go b/vendor/code.gitea.io/sdk/gitea/repo_tag.go
index 7317d3f39..c0dcfe81b 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo_tag.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo_tag.go
@@ -123,8 +123,7 @@ func (c *Client) DeleteTag(user, repo, tag string) (*Response, error) {
 	if err := c.checkServerVersionGreaterThanOrEqual(version1_14_0); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE",
+	return c.doRequestWithStatusHandle("DELETE",
 		fmt.Sprintf("/repos/%s/%s/tags/%s", user, repo, tag),
 		nil, nil)
-	return resp, err
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_tag_protection.go b/vendor/code.gitea.io/sdk/gitea/repo_tag_protection.go
index 02cd372b8..ec433ae92 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo_tag_protection.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo_tag_protection.go
@@ -120,6 +120,5 @@ func (c *Client) DeleteTagProtection(owner, repo string, id int64) (*Response, e
 		return nil, err
 	}
 
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/tag_protections/%d", owner, repo, id), jsonHeader, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/tag_protections/%d", owner, repo, id), jsonHeader, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_team.go b/vendor/code.gitea.io/sdk/gitea/repo_team.go
index b983d8748..36e0863f5 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo_team.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo_team.go
@@ -30,8 +30,7 @@ func (c *Client) AddRepoTeam(user, repo, team string) (*Response, error) {
 	if err := escapeValidatePathSegments(&user, &repo, &team); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PUT", fmt.Sprintf("/repos/%s/%s/teams/%s", user, repo, team), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("PUT", fmt.Sprintf("/repos/%s/%s/teams/%s", user, repo, team), nil, nil)
 }
 
 // RemoveRepoTeam delete a team from a repository
@@ -42,8 +41,7 @@ func (c *Client) RemoveRepoTeam(user, repo, team string) (*Response, error) {
 	if err := escapeValidatePathSegments(&user, &repo, &team); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/teams/%s", user, repo, team), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/teams/%s", user, repo, team), nil, nil)
 }
 
 // CheckRepoTeam check if team is assigned to repo by name and return it.
diff --git a/vendor/code.gitea.io/sdk/gitea/repo_topics.go b/vendor/code.gitea.io/sdk/gitea/repo_topics.go
index 92f2228cd..9a105f2b3 100644
--- a/vendor/code.gitea.io/sdk/gitea/repo_topics.go
+++ b/vendor/code.gitea.io/sdk/gitea/repo_topics.go
@@ -45,8 +45,7 @@ func (c *Client) SetRepoTopics(user, repo string, list []string) (*Response, err
 	if err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PUT", fmt.Sprintf("/repos/%s/%s/topics", user, repo), jsonHeader, bytes.NewReader(body))
-	return resp, err
+	return c.doRequestWithStatusHandle("PUT", fmt.Sprintf("/repos/%s/%s/topics", user, repo), jsonHeader, bytes.NewReader(body))
 }
 
 // AddRepoTopic adds a topic to a repo's topics list
@@ -54,8 +53,7 @@ func (c *Client) AddRepoTopic(user, repo, topic string) (*Response, error) {
 	if err := escapeValidatePathSegments(&user, &repo, &topic); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PUT", fmt.Sprintf("/repos/%s/%s/topics/%s", user, repo, topic), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("PUT", fmt.Sprintf("/repos/%s/%s/topics/%s", user, repo, topic), nil, nil)
 }
 
 // DeleteRepoTopic deletes a topic from repo's topics list
@@ -63,6 +61,5 @@ func (c *Client) DeleteRepoTopic(user, repo, topic string) (*Response, error) {
 	if err := escapeValidatePathSegments(&user, &repo, &topic); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/topics/%s", user, repo, topic), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/topics/%s", user, repo, topic), nil, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/secret.go b/vendor/code.gitea.io/sdk/gitea/secret.go
index d67713d8d..ea11ed06e 100644
--- a/vendor/code.gitea.io/sdk/gitea/secret.go
+++ b/vendor/code.gitea.io/sdk/gitea/secret.go
@@ -11,6 +11,8 @@ type Secret struct {
 	Name string `json:"name"`
 	// the secret's data
 	Data string `json:"data"`
+	// the secret's description
+	Description string `json:"description"`
 	// Date and Time of secret creation
 	Created time.Time `json:"created_at"`
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/user_app.go b/vendor/code.gitea.io/sdk/gitea/user_app.go
index 92a3a2601..bd5b1f888 100644
--- a/vendor/code.gitea.io/sdk/gitea/user_app.go
+++ b/vendor/code.gitea.io/sdk/gitea/user_app.go
@@ -11,6 +11,7 @@ import (
 	"fmt"
 	"net/url"
 	"reflect"
+	"time"
 )
 
 // AccessTokenScope represents the scope for an access token.
@@ -71,6 +72,8 @@ type AccessToken struct {
 	Token          string             `json:"sha1"`
 	TokenLastEight string             `json:"token_last_eight"`
 	Scopes         []AccessTokenScope `json:"scopes"`
+	Created        time.Time          `json:"created_at,omitempty"`
+	Updated        time.Time          `json:"last_used_at,omitempty"`
 }
 
 // ListAccessTokensOptions options for listing a users's access tokens
@@ -138,6 +141,5 @@ func (c *Client) DeleteAccessToken(value interface{}) (*Response, error) {
 		return nil, fmt.Errorf("only string and int64 supported")
 	}
 
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/users/%s/tokens/%s", url.PathEscape(username), url.PathEscape(token)), jsonHeader, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/users/%s/tokens/%s", url.PathEscape(username), url.PathEscape(token)), jsonHeader, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/user_email.go b/vendor/code.gitea.io/sdk/gitea/user_email.go
index 4962b082b..4c24a4246 100644
--- a/vendor/code.gitea.io/sdk/gitea/user_email.go
+++ b/vendor/code.gitea.io/sdk/gitea/user_email.go
@@ -59,6 +59,5 @@ func (c *Client) DeleteEmail(opt DeleteEmailOption) (*Response, error) {
 	if err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", "/user/emails", jsonHeader, bytes.NewReader(body))
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", "/user/emails", jsonHeader, bytes.NewReader(body))
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/user_follow.go b/vendor/code.gitea.io/sdk/gitea/user_follow.go
index 7bd340ca2..40072aa9a 100644
--- a/vendor/code.gitea.io/sdk/gitea/user_follow.go
+++ b/vendor/code.gitea.io/sdk/gitea/user_follow.go
@@ -60,7 +60,7 @@ func (c *Client) IsFollowing(target string) (bool, *Response) {
 		// ToDo return err
 		return false, nil
 	}
-	_, resp, err := c.getResponse("GET", fmt.Sprintf("/user/following/%s", target), nil, nil)
+	resp, err := c.doRequestWithStatusHandle("GET", fmt.Sprintf("/user/following/%s", target), nil, nil)
 	return err == nil, resp
 }
 
@@ -70,7 +70,7 @@ func (c *Client) IsUserFollowing(user, target string) (bool, *Response) {
 		// ToDo return err
 		return false, nil
 	}
-	_, resp, err := c.getResponse("GET", fmt.Sprintf("/users/%s/following/%s", user, target), nil, nil)
+	resp, err := c.doRequestWithStatusHandle("GET", fmt.Sprintf("/users/%s/following/%s", user, target), nil, nil)
 	return err == nil, resp
 }
 
@@ -79,7 +79,7 @@ func (c *Client) Follow(target string) (*Response, error) {
 	if err := escapeValidatePathSegments(&target); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("PUT", fmt.Sprintf("/user/following/%s", target), nil, nil)
+	resp, err := c.doRequestWithStatusHandle("PUT", fmt.Sprintf("/user/following/%s", target), nil, nil)
 	return resp, err
 }
 
@@ -88,6 +88,6 @@ func (c *Client) Unfollow(target string) (*Response, error) {
 	if err := escapeValidatePathSegments(&target); err != nil {
 		return nil, err
 	}
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/user/following/%s", target), nil, nil)
+	resp, err := c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/user/following/%s", target), nil, nil)
 	return resp, err
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/user_gpgkey.go b/vendor/code.gitea.io/sdk/gitea/user_gpgkey.go
index 6c1b9d10f..15c2794a6 100644
--- a/vendor/code.gitea.io/sdk/gitea/user_gpgkey.go
+++ b/vendor/code.gitea.io/sdk/gitea/user_gpgkey.go
@@ -84,6 +84,5 @@ func (c *Client) CreateGPGKey(opt CreateGPGKeyOption) (*GPGKey, *Response, error
 
 // DeleteGPGKey delete GPG key with key id
 func (c *Client) DeleteGPGKey(keyID int64) (*Response, error) {
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/user/gpg_keys/%d", keyID), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/user/gpg_keys/%d", keyID), nil, nil)
 }
diff --git a/vendor/code.gitea.io/sdk/gitea/user_key.go b/vendor/code.gitea.io/sdk/gitea/user_key.go
index 02795baef..d01627626 100644
--- a/vendor/code.gitea.io/sdk/gitea/user_key.go
+++ b/vendor/code.gitea.io/sdk/gitea/user_key.go
@@ -19,6 +19,7 @@ type PublicKey struct {
 	Title       string    `json:"title,omitempty"`
 	Fingerprint string    `json:"fingerprint,omitempty"`
 	Created     time.Time `json:"created_at,omitempty"`
+	Updated     time.Time `json:"last_used_at,omitempty"`
 	Owner       *User     `json:"user,omitempty"`
 	ReadOnly    bool      `json:"read_only,omitempty"`
 	KeyType     string    `json:"key_type,omitempty"`
@@ -78,6 +79,5 @@ func (c *Client) CreatePublicKey(opt CreateKeyOption) (*PublicKey, *Response, er
 
 // DeletePublicKey delete public key with key id
 func (c *Client) DeletePublicKey(keyID int64) (*Response, error) {
-	_, resp, err := c.getResponse("DELETE", fmt.Sprintf("/user/keys/%d", keyID), nil, nil)
-	return resp, err
+	return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/user/keys/%d", keyID), nil, nil)
 }
diff --git a/vendor/codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2/pull.go b/vendor/codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2/pull.go
index 4a3c3e716..3ddeabfe0 100644
--- a/vendor/codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2/pull.go
+++ b/vendor/codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2/pull.go
@@ -151,6 +151,21 @@ func (c *Client) GetPullRequest(owner, repo string, index int64) (*PullRequest,
 	return pr, resp, err
 }
 
+// GetPullRequestByBaseAndHead finds a pull request by base and head
+func (c *Client) GetPullRequestByBaseAndHead(owner, repo, base, head string) (*PullRequest, *Response, error) {
+	if err := escapeValidatePathSegments(&owner, &repo, &base, &head); err != nil {
+		return nil, nil, err
+	}
+	pr := new(PullRequest)
+	resp, err := c.getParsedResponse("GET", fmt.Sprintf("/repos/%s/%s/pulls/%s/%s", owner, repo, base, head), nil, nil, pr)
+	if c.checkServerVersionGreaterThanOrEqual(version1_14_0) != nil {
+		if err := fixPullHeadSha(c, pr); err != nil {
+			return pr, resp, err
+		}
+	}
+	return pr, resp, err
+}
+
 // CreatePullRequestOption options when creating a pull request
 type CreatePullRequestOption struct {
 	Head      string     `json:"head"`
diff --git a/vendor/github.com/bahlo/generic-list-go/LICENSE b/vendor/github.com/bahlo/generic-list-go/LICENSE
new file mode 100644
index 000000000..6a66aea5e
--- /dev/null
+++ b/vendor/github.com/bahlo/generic-list-go/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/bahlo/generic-list-go/README.md b/vendor/github.com/bahlo/generic-list-go/README.md
new file mode 100644
index 000000000..68bbce9fb
--- /dev/null
+++ b/vendor/github.com/bahlo/generic-list-go/README.md
@@ -0,0 +1,5 @@
+# generic-list-go [![CI](https://github.com/bahlo/generic-list-go/actions/workflows/ci.yml/badge.svg)](https://github.com/bahlo/generic-list-go/actions/workflows/ci.yml)
+
+Go [container/list](https://pkg.go.dev/container/list) but with generics.
+
+The code is based on `container/list` in `go1.18beta2`.
diff --git a/vendor/github.com/bahlo/generic-list-go/list.go b/vendor/github.com/bahlo/generic-list-go/list.go
new file mode 100644
index 000000000..a06a7c612
--- /dev/null
+++ b/vendor/github.com/bahlo/generic-list-go/list.go
@@ -0,0 +1,235 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package list implements a doubly linked list.
+//
+// To iterate over a list (where l is a *List):
+//	for e := l.Front(); e != nil; e = e.Next() {
+//		// do something with e.Value
+//	}
+//
+package list
+
+// Element is an element of a linked list.
+type Element[T any] struct {
+	// Next and previous pointers in the doubly-linked list of elements.
+	// To simplify the implementation, internally a list l is implemented
+	// as a ring, such that &l.root is both the next element of the last
+	// list element (l.Back()) and the previous element of the first list
+	// element (l.Front()).
+	next, prev *Element[T]
+
+	// The list to which this element belongs.
+	list *List[T]
+
+	// The value stored with this element.
+	Value T
+}
+
+// Next returns the next list element or nil.
+func (e *Element[T]) Next() *Element[T] {
+	if p := e.next; e.list != nil && p != &e.list.root {
+		return p
+	}
+	return nil
+}
+
+// Prev returns the previous list element or nil.
+func (e *Element[T]) Prev() *Element[T] {
+	if p := e.prev; e.list != nil && p != &e.list.root {
+		return p
+	}
+	return nil
+}
+
+// List represents a doubly linked list.
+// The zero value for List is an empty list ready to use.
+type List[T any] struct {
+	root Element[T] // sentinel list element, only &root, root.prev, and root.next are used
+	len  int        // current list length excluding (this) sentinel element
+}
+
+// Init initializes or clears list l.
+func (l *List[T]) Init() *List[T] {
+	l.root.next = &l.root
+	l.root.prev = &l.root
+	l.len = 0
+	return l
+}
+
+// New returns an initialized list.
+func New[T any]() *List[T] { return new(List[T]).Init() }
+
+// Len returns the number of elements of list l.
+// The complexity is O(1).
+func (l *List[T]) Len() int { return l.len }
+
+// Front returns the first element of list l or nil if the list is empty.
+func (l *List[T]) Front() *Element[T] {
+	if l.len == 0 {
+		return nil
+	}
+	return l.root.next
+}
+
+// Back returns the last element of list l or nil if the list is empty.
+func (l *List[T]) Back() *Element[T] {
+	if l.len == 0 {
+		return nil
+	}
+	return l.root.prev
+}
+
+// lazyInit lazily initializes a zero List value.
+func (l *List[T]) lazyInit() {
+	if l.root.next == nil {
+		l.Init()
+	}
+}
+
+// insert inserts e after at, increments l.len, and returns e.
+func (l *List[T]) insert(e, at *Element[T]) *Element[T] {
+	e.prev = at
+	e.next = at.next
+	e.prev.next = e
+	e.next.prev = e
+	e.list = l
+	l.len++
+	return e
+}
+
+// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
+func (l *List[T]) insertValue(v T, at *Element[T]) *Element[T] {
+	return l.insert(&Element[T]{Value: v}, at)
+}
+
+// remove removes e from its list, decrements l.len
+func (l *List[T]) remove(e *Element[T]) {
+	e.prev.next = e.next
+	e.next.prev = e.prev
+	e.next = nil // avoid memory leaks
+	e.prev = nil // avoid memory leaks
+	e.list = nil
+	l.len--
+}
+
+// move moves e to next to at.
+func (l *List[T]) move(e, at *Element[T]) {
+	if e == at {
+		return
+	}
+	e.prev.next = e.next
+	e.next.prev = e.prev
+
+	e.prev = at
+	e.next = at.next
+	e.prev.next = e
+	e.next.prev = e
+}
+
+// Remove removes e from l if e is an element of list l.
+// It returns the element value e.Value.
+// The element must not be nil.
+func (l *List[T]) Remove(e *Element[T]) T {
+	if e.list == l {
+		// if e.list == l, l must have been initialized when e was inserted
+		// in l or l == nil (e is a zero Element) and l.remove will crash
+		l.remove(e)
+	}
+	return e.Value
+}
+
+// PushFront inserts a new element e with value v at the front of list l and returns e.
+func (l *List[T]) PushFront(v T) *Element[T] {
+	l.lazyInit()
+	return l.insertValue(v, &l.root)
+}
+
+// PushBack inserts a new element e with value v at the back of list l and returns e.
+func (l *List[T]) PushBack(v T) *Element[T] {
+	l.lazyInit()
+	return l.insertValue(v, l.root.prev)
+}
+
+// InsertBefore inserts a new element e with value v immediately before mark and returns e.
+// If mark is not an element of l, the list is not modified.
+// The mark must not be nil.
+func (l *List[T]) InsertBefore(v T, mark *Element[T]) *Element[T] {
+	if mark.list != l {
+		return nil
+	}
+	// see comment in List.Remove about initialization of l
+	return l.insertValue(v, mark.prev)
+}
+
+// InsertAfter inserts a new element e with value v immediately after mark and returns e.
+// If mark is not an element of l, the list is not modified.
+// The mark must not be nil.
+func (l *List[T]) InsertAfter(v T, mark *Element[T]) *Element[T] {
+	if mark.list != l {
+		return nil
+	}
+	// see comment in List.Remove about initialization of l
+	return l.insertValue(v, mark)
+}
+
+// MoveToFront moves element e to the front of list l.
+// If e is not an element of l, the list is not modified.
+// The element must not be nil.
+func (l *List[T]) MoveToFront(e *Element[T]) {
+	if e.list != l || l.root.next == e {
+		return
+	}
+	// see comment in List.Remove about initialization of l
+	l.move(e, &l.root)
+}
+
+// MoveToBack moves element e to the back of list l.
+// If e is not an element of l, the list is not modified.
+// The element must not be nil.
+func (l *List[T]) MoveToBack(e *Element[T]) {
+	if e.list != l || l.root.prev == e {
+		return
+	}
+	// see comment in List.Remove about initialization of l
+	l.move(e, l.root.prev)
+}
+
+// MoveBefore moves element e to its new position before mark.
+// If e or mark is not an element of l, or e == mark, the list is not modified.
+// The element and mark must not be nil.
+func (l *List[T]) MoveBefore(e, mark *Element[T]) {
+	if e.list != l || e == mark || mark.list != l {
+		return
+	}
+	l.move(e, mark.prev)
+}
+
+// MoveAfter moves element e to its new position after mark.
+// If e or mark is not an element of l, or e == mark, the list is not modified.
+// The element and mark must not be nil.
+func (l *List[T]) MoveAfter(e, mark *Element[T]) {
+	if e.list != l || e == mark || mark.list != l {
+		return
+	}
+	l.move(e, mark)
+}
+
+// PushBackList inserts a copy of another list at the back of list l.
+// The lists l and other may be the same. They must not be nil.
+func (l *List[T]) PushBackList(other *List[T]) {
+	l.lazyInit()
+	for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
+		l.insertValue(e.Value, l.root.prev)
+	}
+}
+
+// PushFrontList inserts a copy of another list at the front of list l.
+// The lists l and other may be the same. They must not be nil.
+func (l *List[T]) PushFrontList(other *List[T]) {
+	l.lazyInit()
+	for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {
+		l.insertValue(e.Value, &l.root)
+	}
+}
diff --git a/vendor/github.com/buger/jsonparser/.gitignore b/vendor/github.com/buger/jsonparser/.gitignore
new file mode 100644
index 000000000..5598d8a56
--- /dev/null
+++ b/vendor/github.com/buger/jsonparser/.gitignore
@@ -0,0 +1,12 @@
+
+*.test
+
+*.out
+
+*.mprof
+
+.idea
+
+vendor/github.com/buger/goterm/
+prof.cpu
+prof.mem
diff --git a/vendor/github.com/buger/jsonparser/.travis.yml b/vendor/github.com/buger/jsonparser/.travis.yml
new file mode 100644
index 000000000..dbfb7cf98
--- /dev/null
+++ b/vendor/github.com/buger/jsonparser/.travis.yml
@@ -0,0 +1,11 @@
+language: go
+arch:
+    - amd64
+    - ppc64le
+go:
+    - 1.7.x
+    - 1.8.x
+    - 1.9.x
+    - 1.10.x
+    - 1.11.x
+script: go test -v ./.
diff --git a/vendor/github.com/buger/jsonparser/Dockerfile b/vendor/github.com/buger/jsonparser/Dockerfile
new file mode 100644
index 000000000..37fc9fd0b
--- /dev/null
+++ b/vendor/github.com/buger/jsonparser/Dockerfile
@@ -0,0 +1,12 @@
+FROM golang:1.6
+
+RUN go get github.com/Jeffail/gabs
+RUN go get github.com/bitly/go-simplejson
+RUN go get github.com/pquerna/ffjson
+RUN go get github.com/antonholmquist/jason
+RUN go get github.com/mreiferson/go-ujson
+RUN go get -tags=unsafe -u github.com/ugorji/go/codec
+RUN go get github.com/mailru/easyjson
+
+WORKDIR /go/src/github.com/buger/jsonparser
+ADD . /go/src/github.com/buger/jsonparser
\ No newline at end of file
diff --git a/vendor/github.com/buger/jsonparser/LICENSE b/vendor/github.com/buger/jsonparser/LICENSE
new file mode 100644
index 000000000..ac25aeb7d
--- /dev/null
+++ b/vendor/github.com/buger/jsonparser/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 Leonid Bugaev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/buger/jsonparser/Makefile b/vendor/github.com/buger/jsonparser/Makefile
new file mode 100644
index 000000000..e843368cf
--- /dev/null
+++ b/vendor/github.com/buger/jsonparser/Makefile
@@ -0,0 +1,36 @@
+SOURCE = parser.go
+CONTAINER = jsonparser
+SOURCE_PATH = /go/src/github.com/buger/jsonparser
+BENCHMARK = JsonParser
+BENCHTIME = 5s
+TEST = .
+DRUN = docker run -v `pwd`:$(SOURCE_PATH) -i -t $(CONTAINER)
+
+build:
+	docker build -t $(CONTAINER) .
+
+race:
+	$(DRUN) --env GORACE="halt_on_error=1" go test ./. $(ARGS) -v -race -timeout 15s
+
+bench:
+	$(DRUN) go test $(LDFLAGS) -test.benchmem -bench $(BENCHMARK) ./benchmark/ $(ARGS) -benchtime $(BENCHTIME) -v
+
+bench_local:
+	$(DRUN) go test $(LDFLAGS) -test.benchmem -bench . $(ARGS) -benchtime $(BENCHTIME) -v
+
+profile:
+	$(DRUN) go test $(LDFLAGS) -test.benchmem -bench $(BENCHMARK) ./benchmark/ $(ARGS) -memprofile mem.mprof -v
+	$(DRUN) go test $(LDFLAGS) -test.benchmem -bench $(BENCHMARK) ./benchmark/ $(ARGS) -cpuprofile cpu.out -v
+	$(DRUN) go test $(LDFLAGS) -test.benchmem -bench $(BENCHMARK) ./benchmark/ $(ARGS) -c
+
+test:
+	$(DRUN) go test $(LDFLAGS) ./ -run $(TEST) -timeout 10s $(ARGS) -v
+
+fmt:
+	$(DRUN) go fmt ./...
+
+vet:
+	$(DRUN) go vet ./.
+
+bash:
+	$(DRUN) /bin/bash
\ No newline at end of file
diff --git a/vendor/github.com/buger/jsonparser/README.md b/vendor/github.com/buger/jsonparser/README.md
new file mode 100644
index 000000000..d7e0ec397
--- /dev/null
+++ b/vendor/github.com/buger/jsonparser/README.md
@@ -0,0 +1,365 @@
+[![Go Report Card](https://goreportcard.com/badge/github.com/buger/jsonparser)](https://goreportcard.com/report/github.com/buger/jsonparser) ![License](https://img.shields.io/dub/l/vibe-d.svg)
+# Alternative JSON parser for Go (10x times faster standard library)
+
+It does not require you to know the structure of the payload (eg. create structs), and allows accessing fields by providing the path to them. It is up to **10 times faster** than standard `encoding/json` package (depending on payload size and usage), **allocates no memory**. See benchmarks below.
+
+## Rationale
+Originally I made this for a project that relies on a lot of 3rd party APIs that can be unpredictable and complex.
+I love simplicity and prefer to avoid external dependecies. `encoding/json` requires you to know exactly your data structures, or if you prefer to use `map[string]interface{}` instead, it will be very slow and hard to manage.
+I investigated what's on the market and found that most libraries are just wrappers around `encoding/json`, there is few options with own parsers (`ffjson`, `easyjson`), but they still requires you to create data structures.
+
+
+Goal of this project is to push JSON parser to the performance limits and not sacrifice with compliance and developer user experience.
+
+## Example
+For the given JSON our goal is to extract the user's full name, number of github followers and avatar.
+
+```go
+import "github.com/buger/jsonparser"
+
+...
+
+data := []byte(`{
+  "person": {
+    "name": {
+      "first": "Leonid",
+      "last": "Bugaev",
+      "fullName": "Leonid Bugaev"
+    },
+    "github": {
+      "handle": "buger",
+      "followers": 109
+    },
+    "avatars": [
+      { "url": "https://avatars1.githubusercontent.com/u/14009?v=3&s=460", "type": "thumbnail" }
+    ]
+  },
+  "company": {
+    "name": "Acme"
+  }
+}`)
+
+// You can specify key path by providing arguments to Get function
+jsonparser.Get(data, "person", "name", "fullName")
+
+// There is `GetInt` and `GetBoolean` helpers if you exactly know key data type
+jsonparser.GetInt(data, "person", "github", "followers")
+
+// When you try to get object, it will return you []byte slice pointer to data containing it
+// In `company` it will be `{"name": "Acme"}`
+jsonparser.Get(data, "company")
+
+// If the key doesn't exist it will throw an error
+var size int64
+if value, err := jsonparser.GetInt(data, "company", "size"); err == nil {
+  size = value
+}
+
+// You can use `ArrayEach` helper to iterate items [item1, item2 .... itemN]
+jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
+	fmt.Println(jsonparser.Get(value, "url"))
+}, "person", "avatars")
+
+// Or use can access fields by index!
+jsonparser.GetString(data, "person", "avatars", "[0]", "url")
+
+// You can use `ObjectEach` helper to iterate objects { "key1":object1, "key2":object2, .... "keyN":objectN }
+jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
+        fmt.Printf("Key: '%s'\n Value: '%s'\n Type: %s\n", string(key), string(value), dataType)
+	return nil
+}, "person", "name")
+
+// The most efficient way to extract multiple keys is `EachKey`
+
+paths := [][]string{
+  []string{"person", "name", "fullName"},
+  []string{"person", "avatars", "[0]", "url"},
+  []string{"company", "url"},
+}
+jsonparser.EachKey(data, func(idx int, value []byte, vt jsonparser.ValueType, err error){
+  switch idx {
+  case 0: // []string{"person", "name", "fullName"}
+    ...
+  case 1: // []string{"person", "avatars", "[0]", "url"}
+    ...
+  case 2: // []string{"company", "url"},
+    ...
+  }
+}, paths...)
+
+// For more information see docs below
+```
+
+## Need to speedup your app?
+
+I'm available for consulting and can help you push your app performance to the limits. Ping me at: leonsbox@gmail.com.
+
+## Reference
+
+Library API is really simple. You just need the `Get` method to perform any operation. The rest is just helpers around it.
+
+You also can view API at [godoc.org](https://godoc.org/github.com/buger/jsonparser)
+
+
+### **`Get`**
+```go
+func Get(data []byte, keys ...string) (value []byte, dataType jsonparser.ValueType, offset int, err error)
+```
+Receives data structure, and key path to extract value from.
+
+Returns:
+* `value` - Pointer to original data structure containing key value, or just empty slice if nothing found or error
+* `dataType` - 	Can be: `NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null`
+* `offset` - Offset from provided data structure where key value ends. Used mostly internally, for example for `ArrayEach` helper.
+* `err` - If the key is not found or any other parsing issue, it should return error. If key not found it also sets `dataType` to `NotExist`
+
+Accepts multiple keys to specify path to JSON value (in case of quering nested structures).
+If no keys are provided it will try to extract the closest JSON value (simple ones or object/array), useful for reading streams or arrays, see `ArrayEach` implementation.
+
+Note that keys can be an array indexes: `jsonparser.GetInt("person", "avatars", "[0]", "url")`, pretty cool, yeah?
+
+### **`GetString`**
+```go
+func GetString(data []byte, keys ...string) (val string, err error)
+```
+Returns strings properly handing escaped and unicode characters. Note that this will cause additional memory allocations.
+
+### **`GetUnsafeString`**
+If you need string in your app, and ready to sacrifice with support of escaped symbols in favor of speed. It returns string mapped to existing byte slice memory, without any allocations:
+```go
+s, _, := jsonparser.GetUnsafeString(data, "person", "name", "title")
+switch s {
+  case 'CEO':
+    ...
+  case 'Engineer'
+    ...
+  ...
+}
+```
+Note that `unsafe` here means that your string will exist until GC will free underlying byte slice, for most of cases it means that you can use this string only in current context, and should not pass it anywhere externally: through channels or any other way.
+
+
+### **`GetBoolean`**, **`GetInt`** and **`GetFloat`**
+```go
+func GetBoolean(data []byte, keys ...string) (val bool, err error)
+
+func GetFloat(data []byte, keys ...string) (val float64, err error)
+
+func GetInt(data []byte, keys ...string) (val int64, err error)
+```
+If you know the key type, you can use the helpers above.
+If key data type do not match, it will return error.
+
+### **`ArrayEach`**
+```go
+func ArrayEach(data []byte, cb func(value []byte, dataType jsonparser.ValueType, offset int, err error), keys ...string)
+```
+Needed for iterating arrays, accepts a callback function with the same return arguments as `Get`.
+
+### **`ObjectEach`**
+```go
+func ObjectEach(data []byte, callback func(key []byte, value []byte, dataType ValueType, offset int) error, keys ...string) (err error)
+```
+Needed for iterating object, accepts a callback function. Example:
+```go
+var handler func([]byte, []byte, jsonparser.ValueType, int) error
+handler = func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
+	//do stuff here
+}
+jsonparser.ObjectEach(myJson, handler)
+```
+
+
+### **`EachKey`**
+```go
+func EachKey(data []byte, cb func(idx int, value []byte, dataType jsonparser.ValueType, err error), paths ...[]string)
+```
+When you need to read multiple keys, and you do not afraid of low-level API `EachKey` is your friend. It read payload only single time, and calls callback function once path is found. For example when you call multiple times `Get`, it has to process payload multiple times, each time you call it. Depending on payload `EachKey` can be multiple times faster than `Get`. Path can use nested keys as well!
+
+```go
+paths := [][]string{
+	[]string{"uuid"},
+	[]string{"tz"},
+	[]string{"ua"},
+	[]string{"st"},
+}
+var data SmallPayload
+
+jsonparser.EachKey(smallFixture, func(idx int, value []byte, vt jsonparser.ValueType, err error){
+	switch idx {
+	case 0:
+		data.Uuid, _ = value
+	case 1:
+		v, _ := jsonparser.ParseInt(value)
+		data.Tz = int(v)
+	case 2:
+		data.Ua, _ = value
+	case 3:
+		v, _ := jsonparser.ParseInt(value)
+		data.St = int(v)
+	}
+}, paths...)
+```
+
+### **`Set`**
+```go
+func Set(data []byte, setValue []byte, keys ...string) (value []byte, err error)
+```
+Receives existing data structure, key path to set, and value to set at that key. *This functionality is experimental.*
+
+Returns:
+* `value` - Pointer to original data structure with updated or added key value.
+* `err` - If any parsing issue, it should return error.
+
+Accepts multiple keys to specify path to JSON value (in case of updating or creating  nested structures).
+
+Note that keys can be an array indexes: `jsonparser.Set(data, []byte("http://github.com"), "person", "avatars", "[0]", "url")`
+
+### **`Delete`**
+```go
+func Delete(data []byte, keys ...string) value []byte
+```
+Receives existing data structure, and key path to delete. *This functionality is experimental.*
+
+Returns:
+* `value` - Pointer to original data structure with key path deleted if it can be found. If there is no key path, then the whole data structure is deleted.
+
+Accepts multiple keys to specify path to JSON value (in case of updating or creating  nested structures).
+
+Note that keys can be an array indexes: `jsonparser.Delete(data, "person", "avatars", "[0]", "url")`
+
+
+## What makes it so fast?
+* It does not rely on `encoding/json`, `reflection` or `interface{}`, the only real package dependency is `bytes`.
+* Operates with JSON payload on byte level, providing you pointers to the original data structure: no memory allocation.
+* No automatic type conversions, by default everything is a []byte, but it provides you value type, so you can convert by yourself (there is few helpers included).
+* Does not parse full record, only keys you specified
+
+
+## Benchmarks
+
+There are 3 benchmark types, trying to simulate real-life usage for small, medium and large JSON payloads.
+For each metric, the lower value is better. Time/op is in nanoseconds. Values better than standard encoding/json marked as bold text.
+Benchmarks run on standard Linode 1024 box.
+
+Compared libraries:
+* https://golang.org/pkg/encoding/json
+* https://github.com/Jeffail/gabs
+* https://github.com/a8m/djson
+* https://github.com/bitly/go-simplejson
+* https://github.com/antonholmquist/jason
+* https://github.com/mreiferson/go-ujson
+* https://github.com/ugorji/go/codec
+* https://github.com/pquerna/ffjson
+* https://github.com/mailru/easyjson
+* https://github.com/buger/jsonparser
+
+#### TLDR
+If you want to skip next sections we have 2 winner: `jsonparser` and `easyjson`.
+`jsonparser` is up to 10 times faster than standard `encoding/json` package (depending on payload size and usage), and almost infinitely (literally) better in memory consumption because it operates with data on byte level, and provide direct slice pointers.
+`easyjson` wins in CPU in medium tests and frankly i'm impressed with this package: it is remarkable results considering that it is almost drop-in replacement for `encoding/json` (require some code generation).
+
+It's hard to fully compare `jsonparser` and `easyjson` (or `ffson`), they a true parsers and fully process record, unlike `jsonparser` which parse only keys you specified.
+
+If you searching for replacement of `encoding/json` while keeping structs, `easyjson` is an amazing choice. If you want to process dynamic JSON, have memory constrains, or more control over your data you should try `jsonparser`.
+
+`jsonparser` performance heavily depends on usage, and it works best when you do not need to process full record, only some keys. The more calls you need to make, the slower it will be, in contrast `easyjson` (or `ffjson`, `encoding/json`) parser record only 1 time, and then you can make as many calls as you want.
+
+With great power comes great responsibility! :)
+
+
+#### Small payload
+
+Each test processes 190 bytes of http log as a JSON record.
+It should read multiple fields.
+https://github.com/buger/jsonparser/blob/master/benchmark/benchmark_small_payload_test.go
+
+Library | time/op | bytes/op | allocs/op 
+ ------ | ------- | -------- | -------
+encoding/json struct | 7879 | 880 | 18 
+encoding/json interface{} | 8946 | 1521 | 38
+Jeffail/gabs | 10053 | 1649 | 46
+bitly/go-simplejson | 10128 | 2241 | 36 
+antonholmquist/jason | 27152 | 7237 | 101 
+github.com/ugorji/go/codec | 8806 | 2176 | 31 
+mreiferson/go-ujson | **7008** | **1409** | 37 
+a8m/djson | 3862 | 1249 | 30 
+pquerna/ffjson | **3769** | **624** | **15** 
+mailru/easyjson | **2002** | **192** | **9** 
+buger/jsonparser | **1367** | **0** | **0** 
+buger/jsonparser (EachKey API) | **809** | **0** | **0** 
+
+Winners are ffjson, easyjson and jsonparser, where jsonparser is up to 9.8x faster than encoding/json and 4.6x faster than ffjson, and slightly faster than easyjson.
+If you look at memory allocation, jsonparser has no rivals, as it makes no data copy and operates with raw []byte structures and pointers to it.
+
+#### Medium payload
+
+Each test processes a 2.4kb JSON record (based on Clearbit API).
+It should read multiple nested fields and 1 array.
+
+https://github.com/buger/jsonparser/blob/master/benchmark/benchmark_medium_payload_test.go
+
+| Library | time/op | bytes/op | allocs/op |
+| ------- | ------- | -------- | --------- |
+| encoding/json struct | 57749 | 1336 | 29 |
+| encoding/json interface{} | 79297 | 10627 | 215 |
+| Jeffail/gabs | 83807 | 11202 | 235 |
+| bitly/go-simplejson | 88187 | 17187 | 220 |
+| antonholmquist/jason | 94099 | 19013 | 247 |
+| github.com/ugorji/go/codec | 114719 | 6712 | 152 |
+| mreiferson/go-ujson | **56972** | 11547 | 270 |
+| a8m/djson | 28525 | 10196 | 198 | 
+| pquerna/ffjson | **20298** | **856** | **20** |
+| mailru/easyjson | **10512** | **336** | **12** |
+| buger/jsonparser | **15955** | **0** | **0** |
+| buger/jsonparser (EachKey API) | **8916** | **0** | **0** |
+
+The difference between ffjson and jsonparser in CPU usage is smaller, while the memory consumption difference is growing. On the other hand `easyjson` shows remarkable performance for medium payload.
+
+`gabs`, `go-simplejson` and `jason` are based on encoding/json and map[string]interface{} and actually only helpers for unstructured JSON, their performance correlate with `encoding/json interface{}`, and they will skip next round.
+`go-ujson` while have its own parser, shows same performance as `encoding/json`, also skips next round. Same situation with `ugorji/go/codec`, but it showed unexpectedly bad performance for complex payloads.
+
+
+#### Large payload
+
+Each test processes a 24kb JSON record (based on Discourse API)
+It should read 2 arrays, and for each item in array get a few fields.
+Basically it means processing a full JSON file.
+
+https://github.com/buger/jsonparser/blob/master/benchmark/benchmark_large_payload_test.go
+
+| Library | time/op | bytes/op | allocs/op |
+| --- | --- | --- | --- |
+| encoding/json struct | 748336 | 8272 | 307 |
+| encoding/json interface{} | 1224271 | 215425 | 3395 |
+| a8m/djson | 510082 | 213682 | 2845 |
+| pquerna/ffjson | **312271** | **7792** | **298** |
+| mailru/easyjson | **154186** | **6992** | **288** |
+| buger/jsonparser | **85308** | **0** | **0** |
+
+`jsonparser` now is a winner, but do not forget that it is way more lightweight parser than `ffson` or `easyjson`, and they have to parser all the data, while `jsonparser` parse only what you need. All `ffjson`, `easysjon` and `jsonparser` have their own parsing code, and does not depend on `encoding/json` or `interface{}`, thats one of the reasons why they are so fast. `easyjson` also use a bit of `unsafe` package to reduce memory consuption (in theory it can lead to some unexpected GC issue, but i did not tested enough)
+
+Also last benchmark did not included `EachKey` test, because in this particular case we need to read lot of Array values, and using `ArrayEach` is more efficient. 
+
+## Questions and support
+
+All bug-reports and suggestions should go though Github Issues.
+
+## Contributing
+
+1. Fork it
+2. Create your feature branch (git checkout -b my-new-feature)
+3. Commit your changes (git commit -am 'Added some feature')
+4. Push to the branch (git push origin my-new-feature)
+5. Create new Pull Request
+
+## Development
+
+All my development happens using Docker, and repo include some Make tasks to simplify development.
+
+* `make build` - builds docker image, usually can be called only once
+* `make test` - run tests
+* `make fmt` - run go fmt
+* `make bench` - run benchmarks (if you need to run only single benchmark modify `BENCHMARK` variable in make file)
+* `make profile` - runs benchmark and generate 3 files-  `cpu.out`, `mem.mprof` and `benchmark.test` binary, which can be used for `go tool pprof`
+* `make bash` - enter container (i use it for running `go tool pprof` above)
diff --git a/vendor/github.com/buger/jsonparser/bytes.go b/vendor/github.com/buger/jsonparser/bytes.go
new file mode 100644
index 000000000..0bb0ff395
--- /dev/null
+++ b/vendor/github.com/buger/jsonparser/bytes.go
@@ -0,0 +1,47 @@
+package jsonparser
+
+import (
+	bio "bytes"
+)
+
+// minInt64 '-9223372036854775808' is the smallest representable number in int64
+const minInt64 = `9223372036854775808`
+
+// About 2x faster then strconv.ParseInt because it only supports base 10, which is enough for JSON
+func parseInt(bytes []byte) (v int64, ok bool, overflow bool) {
+	if len(bytes) == 0 {
+		return 0, false, false
+	}
+
+	var neg bool = false
+	if bytes[0] == '-' {
+		neg = true
+		bytes = bytes[1:]
+	}
+
+	var b int64 = 0
+	for _, c := range bytes {
+		if c >= '0' && c <= '9' {
+			b = (10 * v) + int64(c-'0')
+		} else {
+			return 0, false, false
+		}
+		if overflow = (b < v); overflow {
+			break
+		}
+		v = b
+	}
+
+	if overflow {
+		if neg && bio.Equal(bytes, []byte(minInt64)) {
+			return b, true, false
+		}
+		return 0, false, true
+	}
+
+	if neg {
+		return -v, true, false
+	} else {
+		return v, true, false
+	}
+}
diff --git a/vendor/github.com/buger/jsonparser/bytes_safe.go b/vendor/github.com/buger/jsonparser/bytes_safe.go
new file mode 100644
index 000000000..ff16a4a19
--- /dev/null
+++ b/vendor/github.com/buger/jsonparser/bytes_safe.go
@@ -0,0 +1,25 @@
+// +build appengine appenginevm
+
+package jsonparser
+
+import (
+	"strconv"
+)
+
+// See fastbytes_unsafe.go for explanation on why *[]byte is used (signatures must be consistent with those in that file)
+
+func equalStr(b *[]byte, s string) bool {
+	return string(*b) == s
+}
+
+func parseFloat(b *[]byte) (float64, error) {
+	return strconv.ParseFloat(string(*b), 64)
+}
+
+func bytesToString(b *[]byte) string {
+	return string(*b)
+}
+
+func StringToBytes(s string) []byte {
+	return []byte(s)
+}
diff --git a/vendor/github.com/buger/jsonparser/bytes_unsafe.go b/vendor/github.com/buger/jsonparser/bytes_unsafe.go
new file mode 100644
index 000000000..589fea87e
--- /dev/null
+++ b/vendor/github.com/buger/jsonparser/bytes_unsafe.go
@@ -0,0 +1,44 @@
+// +build !appengine,!appenginevm
+
+package jsonparser
+
+import (
+	"reflect"
+	"strconv"
+	"unsafe"
+	"runtime"
+)
+
+//
+// The reason for using *[]byte rather than []byte in parameters is an optimization. As of Go 1.6,
+// the compiler cannot perfectly inline the function when using a non-pointer slice. That is,
+// the non-pointer []byte parameter version is slower than if its function body is manually
+// inlined, whereas the pointer []byte version is equally fast to the manually inlined
+// version. Instruction count in assembly taken from "go tool compile" confirms this difference.
+//
+// TODO: Remove hack after Go 1.7 release
+//
+func equalStr(b *[]byte, s string) bool {
+	return *(*string)(unsafe.Pointer(b)) == s
+}
+
+func parseFloat(b *[]byte) (float64, error) {
+	return strconv.ParseFloat(*(*string)(unsafe.Pointer(b)), 64)
+}
+
+// A hack until issue golang/go#2632 is fixed.
+// See: https://github.com/golang/go/issues/2632
+func bytesToString(b *[]byte) string {
+	return *(*string)(unsafe.Pointer(b))
+}
+
+func StringToBytes(s string) []byte {
+	b := make([]byte, 0, 0)
+	bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
+	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
+	bh.Data = sh.Data
+	bh.Cap = sh.Len
+	bh.Len = sh.Len
+	runtime.KeepAlive(s)
+	return b
+}
diff --git a/vendor/github.com/buger/jsonparser/escape.go b/vendor/github.com/buger/jsonparser/escape.go
new file mode 100644
index 000000000..49669b942
--- /dev/null
+++ b/vendor/github.com/buger/jsonparser/escape.go
@@ -0,0 +1,173 @@
+package jsonparser
+
+import (
+	"bytes"
+	"unicode/utf8"
+)
+
+// JSON Unicode stuff: see https://tools.ietf.org/html/rfc7159#section-7
+
+const supplementalPlanesOffset = 0x10000
+const highSurrogateOffset = 0xD800
+const lowSurrogateOffset = 0xDC00
+
+const basicMultilingualPlaneReservedOffset = 0xDFFF
+const basicMultilingualPlaneOffset = 0xFFFF
+
+func combineUTF16Surrogates(high, low rune) rune {
+	return supplementalPlanesOffset + (high-highSurrogateOffset)<<10 + (low - lowSurrogateOffset)
+}
+
+const badHex = -1
+
+func h2I(c byte) int {
+	switch {
+	case c >= '0' && c <= '9':
+		return int(c - '0')
+	case c >= 'A' && c <= 'F':
+		return int(c - 'A' + 10)
+	case c >= 'a' && c <= 'f':
+		return int(c - 'a' + 10)
+	}
+	return badHex
+}
+
+// decodeSingleUnicodeEscape decodes a single \uXXXX escape sequence. The prefix \u is assumed to be present and
+// is not checked.
+// In JSON, these escapes can either come alone or as part of "UTF16 surrogate pairs" that must be handled together.
+// This function only handles one; decodeUnicodeEscape handles this more complex case.
+func decodeSingleUnicodeEscape(in []byte) (rune, bool) {
+	// We need at least 6 characters total
+	if len(in) < 6 {
+		return utf8.RuneError, false
+	}
+
+	// Convert hex to decimal
+	h1, h2, h3, h4 := h2I(in[2]), h2I(in[3]), h2I(in[4]), h2I(in[5])
+	if h1 == badHex || h2 == badHex || h3 == badHex || h4 == badHex {
+		return utf8.RuneError, false
+	}
+
+	// Compose the hex digits
+	return rune(h1<<12 + h2<<8 + h3<<4 + h4), true
+}
+
+// isUTF16EncodedRune checks if a rune is in the range for non-BMP characters,
+// which is used to describe UTF16 chars.
+// Source: https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane
+func isUTF16EncodedRune(r rune) bool {
+	return highSurrogateOffset <= r && r <= basicMultilingualPlaneReservedOffset
+}
+
+func decodeUnicodeEscape(in []byte) (rune, int) {
+	if r, ok := decodeSingleUnicodeEscape(in); !ok {
+		// Invalid Unicode escape
+		return utf8.RuneError, -1
+	} else if r <= basicMultilingualPlaneOffset && !isUTF16EncodedRune(r) {
+		// Valid Unicode escape in Basic Multilingual Plane
+		return r, 6
+	} else if r2, ok := decodeSingleUnicodeEscape(in[6:]); !ok { // Note: previous decodeSingleUnicodeEscape success guarantees at least 6 bytes remain
+		// UTF16 "high surrogate" without manditory valid following Unicode escape for the "low surrogate"
+		return utf8.RuneError, -1
+	} else if r2 < lowSurrogateOffset {
+		// Invalid UTF16 "low surrogate"
+		return utf8.RuneError, -1
+	} else {
+		// Valid UTF16 surrogate pair
+		return combineUTF16Surrogates(r, r2), 12
+	}
+}
+
+// backslashCharEscapeTable: when '\X' is found for some byte X, it is to be replaced with backslashCharEscapeTable[X]
+var backslashCharEscapeTable = [...]byte{
+	'"':  '"',
+	'\\': '\\',
+	'/':  '/',
+	'b':  '\b',
+	'f':  '\f',
+	'n':  '\n',
+	'r':  '\r',
+	't':  '\t',
+}
+
+// unescapeToUTF8 unescapes the single escape sequence starting at 'in' into 'out' and returns
+// how many characters were consumed from 'in' and emitted into 'out'.
+// If a valid escape sequence does not appear as a prefix of 'in', (-1, -1) to signal the error.
+func unescapeToUTF8(in, out []byte) (inLen int, outLen int) {
+	if len(in) < 2 || in[0] != '\\' {
+		// Invalid escape due to insufficient characters for any escape or no initial backslash
+		return -1, -1
+	}
+
+	// https://tools.ietf.org/html/rfc7159#section-7
+	switch e := in[1]; e {
+	case '"', '\\', '/', 'b', 'f', 'n', 'r', 't':
+		// Valid basic 2-character escapes (use lookup table)
+		out[0] = backslashCharEscapeTable[e]
+		return 2, 1
+	case 'u':
+		// Unicode escape
+		if r, inLen := decodeUnicodeEscape(in); inLen == -1 {
+			// Invalid Unicode escape
+			return -1, -1
+		} else {
+			// Valid Unicode escape; re-encode as UTF8
+			outLen := utf8.EncodeRune(out, r)
+			return inLen, outLen
+		}
+	}
+
+	return -1, -1
+}
+
+// unescape unescapes the string contained in 'in' and returns it as a slice.
+// If 'in' contains no escaped characters:
+//   Returns 'in'.
+// Else, if 'out' is of sufficient capacity (guaranteed if cap(out) >= len(in)):
+//   'out' is used to build the unescaped string and is returned with no extra allocation
+// Else:
+//   A new slice is allocated and returned.
+func Unescape(in, out []byte) ([]byte, error) {
+	firstBackslash := bytes.IndexByte(in, '\\')
+	if firstBackslash == -1 {
+		return in, nil
+	}
+
+	// Get a buffer of sufficient size (allocate if needed)
+	if cap(out) < len(in) {
+		out = make([]byte, len(in))
+	} else {
+		out = out[0:len(in)]
+	}
+
+	// Copy the first sequence of unescaped bytes to the output and obtain a buffer pointer (subslice)
+	copy(out, in[:firstBackslash])
+	in = in[firstBackslash:]
+	buf := out[firstBackslash:]
+
+	for len(in) > 0 {
+		// Unescape the next escaped character
+		inLen, bufLen := unescapeToUTF8(in, buf)
+		if inLen == -1 {
+			return nil, MalformedStringEscapeError
+		}
+
+		in = in[inLen:]
+		buf = buf[bufLen:]
+
+		// Copy everything up until the next backslash
+		nextBackslash := bytes.IndexByte(in, '\\')
+		if nextBackslash == -1 {
+			copy(buf, in)
+			buf = buf[len(in):]
+			break
+		} else {
+			copy(buf, in[:nextBackslash])
+			buf = buf[nextBackslash:]
+			in = in[nextBackslash:]
+		}
+	}
+
+	// Trim the out buffer to the amount that was actually emitted
+	return out[:len(out)-len(buf)], nil
+}
diff --git a/vendor/github.com/buger/jsonparser/fuzz.go b/vendor/github.com/buger/jsonparser/fuzz.go
new file mode 100644
index 000000000..854bd11b2
--- /dev/null
+++ b/vendor/github.com/buger/jsonparser/fuzz.go
@@ -0,0 +1,117 @@
+package jsonparser
+
+func FuzzParseString(data []byte) int {
+	r, err := ParseString(data)
+	if err != nil || r == "" {
+		return 0
+	}
+	return 1
+}
+
+func FuzzEachKey(data []byte) int {
+	paths := [][]string{
+		{"name"},
+		{"order"},
+		{"nested", "a"},
+		{"nested", "b"},
+		{"nested2", "a"},
+		{"nested", "nested3", "b"},
+		{"arr", "[1]", "b"},
+		{"arrInt", "[3]"},
+		{"arrInt", "[5]"},
+		{"nested"},
+		{"arr", "["},
+		{"a\n", "b\n"},
+	}
+	EachKey(data, func(idx int, value []byte, vt ValueType, err error) {}, paths...)
+	return 1
+}
+
+func FuzzDelete(data []byte) int {
+	Delete(data, "test")
+	return 1
+}
+
+func FuzzSet(data []byte) int {
+	_, err := Set(data, []byte(`"new value"`), "test")
+	if err != nil {
+		return 0
+	}
+	return 1
+}
+
+func FuzzObjectEach(data []byte) int {
+	_ = ObjectEach(data, func(key, value []byte, valueType ValueType, off int) error {
+		return nil
+	})
+	return 1
+}
+
+func FuzzParseFloat(data []byte) int {
+	_, err := ParseFloat(data)
+	if err != nil {
+		return 0
+	}
+	return 1
+}
+
+func FuzzParseInt(data []byte) int {
+	_, err := ParseInt(data)
+	if err != nil {
+		return 0
+	}
+	return 1
+}
+
+func FuzzParseBool(data []byte) int {
+	_, err := ParseBoolean(data)
+	if err != nil {
+		return 0
+	}
+	return 1
+}
+
+func FuzzTokenStart(data []byte) int {
+	_ = tokenStart(data)
+	return 1
+}
+
+func FuzzGetString(data []byte) int {
+	_, err := GetString(data, "test")
+	if err != nil {
+		return 0
+	}
+	return 1
+}
+
+func FuzzGetFloat(data []byte) int {
+	_, err := GetFloat(data, "test")
+	if err != nil {
+		return 0
+	}
+	return 1
+}
+
+func FuzzGetInt(data []byte) int {
+	_, err := GetInt(data, "test")
+	if err != nil {
+		return 0
+	}
+	return 1
+}
+
+func FuzzGetBoolean(data []byte) int {
+	_, err := GetBoolean(data, "test")
+	if err != nil {
+		return 0
+	}
+	return 1
+}
+
+func FuzzGetUnsafeString(data []byte) int {
+	_, err := GetUnsafeString(data, "test")
+	if err != nil {
+		return 0
+	}
+	return 1
+}
diff --git a/vendor/github.com/buger/jsonparser/oss-fuzz-build.sh b/vendor/github.com/buger/jsonparser/oss-fuzz-build.sh
new file mode 100644
index 000000000..c573b0e2d
--- /dev/null
+++ b/vendor/github.com/buger/jsonparser/oss-fuzz-build.sh
@@ -0,0 +1,47 @@
+#!/bin/bash -eu
+
+git clone https://github.com/dvyukov/go-fuzz-corpus
+zip corpus.zip go-fuzz-corpus/json/corpus/*
+
+cp corpus.zip $OUT/fuzzparsestring_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzParseString fuzzparsestring
+
+cp corpus.zip $OUT/fuzzeachkey_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzEachKey fuzzeachkey
+
+cp corpus.zip $OUT/fuzzdelete_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzDelete fuzzdelete
+
+cp corpus.zip $OUT/fuzzset_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzSet fuzzset
+
+cp corpus.zip $OUT/fuzzobjecteach_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzObjectEach fuzzobjecteach
+
+cp corpus.zip $OUT/fuzzparsefloat_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzParseFloat fuzzparsefloat
+
+cp corpus.zip $OUT/fuzzparseint_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzParseInt fuzzparseint
+
+cp corpus.zip $OUT/fuzzparsebool_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzParseBool fuzzparsebool
+
+cp corpus.zip $OUT/fuzztokenstart_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzTokenStart fuzztokenstart
+
+cp corpus.zip $OUT/fuzzgetstring_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzGetString fuzzgetstring
+
+cp corpus.zip $OUT/fuzzgetfloat_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzGetFloat fuzzgetfloat
+
+cp corpus.zip $OUT/fuzzgetint_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzGetInt fuzzgetint
+
+cp corpus.zip $OUT/fuzzgetboolean_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzGetBoolean fuzzgetboolean
+
+cp corpus.zip $OUT/fuzzgetunsafestring_seed_corpus.zip
+compile_go_fuzzer github.com/buger/jsonparser FuzzGetUnsafeString fuzzgetunsafestring
+
diff --git a/vendor/github.com/buger/jsonparser/parser.go b/vendor/github.com/buger/jsonparser/parser.go
new file mode 100644
index 000000000..14b80bc48
--- /dev/null
+++ b/vendor/github.com/buger/jsonparser/parser.go
@@ -0,0 +1,1283 @@
+package jsonparser
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"strconv"
+)
+
+// Errors
+var (
+	KeyPathNotFoundError       = errors.New("Key path not found")
+	UnknownValueTypeError      = errors.New("Unknown value type")
+	MalformedJsonError         = errors.New("Malformed JSON error")
+	MalformedStringError       = errors.New("Value is string, but can't find closing '\"' symbol")
+	MalformedArrayError        = errors.New("Value is array, but can't find closing ']' symbol")
+	MalformedObjectError       = errors.New("Value looks like object, but can't find closing '}' symbol")
+	MalformedValueError        = errors.New("Value looks like Number/Boolean/None, but can't find its end: ',' or '}' symbol")
+	OverflowIntegerError       = errors.New("Value is number, but overflowed while parsing")
+	MalformedStringEscapeError = errors.New("Encountered an invalid escape sequence in a string")
+)
+
+// How much stack space to allocate for unescaping JSON strings; if a string longer
+// than this needs to be escaped, it will result in a heap allocation
+const unescapeStackBufSize = 64
+
+func tokenEnd(data []byte) int {
+	for i, c := range data {
+		switch c {
+		case ' ', '\n', '\r', '\t', ',', '}', ']':
+			return i
+		}
+	}
+
+	return len(data)
+}
+
+func findTokenStart(data []byte, token byte) int {
+	for i := len(data) - 1; i >= 0; i-- {
+		switch data[i] {
+		case token:
+			return i
+		case '[', '{':
+			return 0
+		}
+	}
+
+	return 0
+}
+
+func findKeyStart(data []byte, key string) (int, error) {
+	i := 0
+	ln := len(data)
+	if ln > 0 && (data[0] == '{' || data[0] == '[') {
+		i = 1
+	}
+	var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings
+
+	if ku, err := Unescape(StringToBytes(key), stackbuf[:]); err == nil {
+		key = bytesToString(&ku)
+	}
+
+	for i < ln {
+		switch data[i] {
+		case '"':
+			i++
+			keyBegin := i
+
+			strEnd, keyEscaped := stringEnd(data[i:])
+			if strEnd == -1 {
+				break
+			}
+			i += strEnd
+			keyEnd := i - 1
+
+			valueOffset := nextToken(data[i:])
+			if valueOffset == -1 {
+				break
+			}
+
+			i += valueOffset
+
+			// if string is a key, and key level match
+			k := data[keyBegin:keyEnd]
+			// for unescape: if there are no escape sequences, this is cheap; if there are, it is a
+			// bit more expensive, but causes no allocations unless len(key) > unescapeStackBufSize
+			if keyEscaped {
+				if ku, err := Unescape(k, stackbuf[:]); err != nil {
+					break
+				} else {
+					k = ku
+				}
+			}
+
+			if data[i] == ':' && len(key) == len(k) && bytesToString(&k) == key {
+				return keyBegin - 1, nil
+			}
+
+		case '[':
+			end := blockEnd(data[i:], data[i], ']')
+			if end != -1 {
+				i = i + end
+			}
+		case '{':
+			end := blockEnd(data[i:], data[i], '}')
+			if end != -1 {
+				i = i + end
+			}
+		}
+		i++
+	}
+
+	return -1, KeyPathNotFoundError
+}
+
+func tokenStart(data []byte) int {
+	for i := len(data) - 1; i >= 0; i-- {
+		switch data[i] {
+		case '\n', '\r', '\t', ',', '{', '[':
+			return i
+		}
+	}
+
+	return 0
+}
+
+// Find position of next character which is not whitespace
+func nextToken(data []byte) int {
+	for i, c := range data {
+		switch c {
+		case ' ', '\n', '\r', '\t':
+			continue
+		default:
+			return i
+		}
+	}
+
+	return -1
+}
+
+// Find position of last character which is not whitespace
+func lastToken(data []byte) int {
+	for i := len(data) - 1; i >= 0; i-- {
+		switch data[i] {
+		case ' ', '\n', '\r', '\t':
+			continue
+		default:
+			return i
+		}
+	}
+
+	return -1
+}
+
+// Tries to find the end of string
+// Support if string contains escaped quote symbols.
+func stringEnd(data []byte) (int, bool) {
+	escaped := false
+	for i, c := range data {
+		if c == '"' {
+			if !escaped {
+				return i + 1, false
+			} else {
+				j := i - 1
+				for {
+					if j < 0 || data[j] != '\\' {
+						return i + 1, true // even number of backslashes
+					}
+					j--
+					if j < 0 || data[j] != '\\' {
+						break // odd number of backslashes
+					}
+					j--
+
+				}
+			}
+		} else if c == '\\' {
+			escaped = true
+		}
+	}
+
+	return -1, escaped
+}
+
+// Find end of the data structure, array or object.
+// For array openSym and closeSym will be '[' and ']', for object '{' and '}'
+func blockEnd(data []byte, openSym byte, closeSym byte) int {
+	level := 0
+	i := 0
+	ln := len(data)
+
+	for i < ln {
+		switch data[i] {
+		case '"': // If inside string, skip it
+			se, _ := stringEnd(data[i+1:])
+			if se == -1 {
+				return -1
+			}
+			i += se
+		case openSym: // If open symbol, increase level
+			level++
+		case closeSym: // If close symbol, increase level
+			level--
+
+			// If we have returned to the original level, we're done
+			if level == 0 {
+				return i + 1
+			}
+		}
+		i++
+	}
+
+	return -1
+}
+
+func searchKeys(data []byte, keys ...string) int {
+	keyLevel := 0
+	level := 0
+	i := 0
+	ln := len(data)
+	lk := len(keys)
+	lastMatched := true
+
+	if lk == 0 {
+		return 0
+	}
+
+	var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings
+
+	for i < ln {
+		switch data[i] {
+		case '"':
+			i++
+			keyBegin := i
+
+			strEnd, keyEscaped := stringEnd(data[i:])
+			if strEnd == -1 {
+				return -1
+			}
+			i += strEnd
+			keyEnd := i - 1
+
+			valueOffset := nextToken(data[i:])
+			if valueOffset == -1 {
+				return -1
+			}
+
+			i += valueOffset
+
+			// if string is a key
+			if data[i] == ':' {
+				if level < 1 {
+					return -1
+				}
+
+				key := data[keyBegin:keyEnd]
+
+				// for unescape: if there are no escape sequences, this is cheap; if there are, it is a
+				// bit more expensive, but causes no allocations unless len(key) > unescapeStackBufSize
+				var keyUnesc []byte
+				if !keyEscaped {
+					keyUnesc = key
+				} else if ku, err := Unescape(key, stackbuf[:]); err != nil {
+					return -1
+				} else {
+					keyUnesc = ku
+				}
+
+				if level <= len(keys) {
+					if equalStr(&keyUnesc, keys[level-1]) {
+						lastMatched = true
+
+						// if key level match
+						if keyLevel == level-1 {
+							keyLevel++
+							// If we found all keys in path
+							if keyLevel == lk {
+								return i + 1
+							}
+						}
+					} else {
+						lastMatched = false
+					}
+				} else {
+					return -1
+				}
+			} else {
+				i--
+			}
+		case '{':
+
+			// in case parent key is matched then only we will increase the level otherwise can directly
+			// can move to the end of this block
+			if !lastMatched {
+				end := blockEnd(data[i:], '{', '}')
+				if end == -1 {
+					return -1
+				}
+				i += end - 1
+			} else {
+				level++
+			}
+		case '}':
+			level--
+			if level == keyLevel {
+				keyLevel--
+			}
+		case '[':
+			// If we want to get array element by index
+			if keyLevel == level && keys[level][0] == '[' {
+				var keyLen = len(keys[level])
+				if keyLen < 3 || keys[level][0] != '[' || keys[level][keyLen-1] != ']' {
+					return -1
+				}
+				aIdx, err := strconv.Atoi(keys[level][1 : keyLen-1])
+				if err != nil {
+					return -1
+				}
+				var curIdx int
+				var valueFound []byte
+				var valueOffset int
+				var curI = i
+				ArrayEach(data[i:], func(value []byte, dataType ValueType, offset int, err error) {
+					if curIdx == aIdx {
+						valueFound = value
+						valueOffset = offset
+						if dataType == String {
+							valueOffset = valueOffset - 2
+							valueFound = data[curI+valueOffset : curI+valueOffset+len(value)+2]
+						}
+					}
+					curIdx += 1
+				})
+
+				if valueFound == nil {
+					return -1
+				} else {
+					subIndex := searchKeys(valueFound, keys[level+1:]...)
+					if subIndex < 0 {
+						return -1
+					}
+					return i + valueOffset + subIndex
+				}
+			} else {
+				// Do not search for keys inside arrays
+				if arraySkip := blockEnd(data[i:], '[', ']'); arraySkip == -1 {
+					return -1
+				} else {
+					i += arraySkip - 1
+				}
+			}
+		case ':': // If encountered, JSON data is malformed
+			return -1
+		}
+
+		i++
+	}
+
+	return -1
+}
+
+func sameTree(p1, p2 []string) bool {
+	minLen := len(p1)
+	if len(p2) < minLen {
+		minLen = len(p2)
+	}
+
+	for pi_1, p_1 := range p1[:minLen] {
+		if p2[pi_1] != p_1 {
+			return false
+		}
+	}
+
+	return true
+}
+
+func EachKey(data []byte, cb func(int, []byte, ValueType, error), paths ...[]string) int {
+	var x struct{}
+	pathFlags := make([]bool, len(paths))
+	var level, pathsMatched, i int
+	ln := len(data)
+
+	var maxPath int
+	for _, p := range paths {
+		if len(p) > maxPath {
+			maxPath = len(p)
+		}
+	}
+
+	pathsBuf := make([]string, maxPath)
+
+	for i < ln {
+		switch data[i] {
+		case '"':
+			i++
+			keyBegin := i
+
+			strEnd, keyEscaped := stringEnd(data[i:])
+			if strEnd == -1 {
+				return -1
+			}
+			i += strEnd
+
+			keyEnd := i - 1
+
+			valueOffset := nextToken(data[i:])
+			if valueOffset == -1 {
+				return -1
+			}
+
+			i += valueOffset
+
+			// if string is a key, and key level match
+			if data[i] == ':' {
+				match := -1
+				key := data[keyBegin:keyEnd]
+
+				// for unescape: if there are no escape sequences, this is cheap; if there are, it is a
+				// bit more expensive, but causes no allocations unless len(key) > unescapeStackBufSize
+				var keyUnesc []byte
+				if !keyEscaped {
+					keyUnesc = key
+				} else {
+					var stackbuf [unescapeStackBufSize]byte
+					if ku, err := Unescape(key, stackbuf[:]); err != nil {
+						return -1
+					} else {
+						keyUnesc = ku
+					}
+				}
+
+				if maxPath >= level {
+					if level < 1 {
+						cb(-1, nil, Unknown, MalformedJsonError)
+						return -1
+					}
+
+					pathsBuf[level-1] = bytesToString(&keyUnesc)
+					for pi, p := range paths {
+						if len(p) != level || pathFlags[pi] || !equalStr(&keyUnesc, p[level-1]) || !sameTree(p, pathsBuf[:level]) {
+							continue
+						}
+
+						match = pi
+
+						pathsMatched++
+						pathFlags[pi] = true
+
+						v, dt, _, e := Get(data[i+1:])
+						cb(pi, v, dt, e)
+
+						if pathsMatched == len(paths) {
+							break
+						}
+					}
+					if pathsMatched == len(paths) {
+						return i
+					}
+				}
+
+				if match == -1 {
+					tokenOffset := nextToken(data[i+1:])
+					i += tokenOffset
+
+					if data[i] == '{' {
+						blockSkip := blockEnd(data[i:], '{', '}')
+						i += blockSkip + 1
+					}
+				}
+
+				if i < ln {
+					switch data[i] {
+					case '{', '}', '[', '"':
+						i--
+					}
+				}
+			} else {
+				i--
+			}
+		case '{':
+			level++
+		case '}':
+			level--
+		case '[':
+			var ok bool
+			arrIdxFlags := make(map[int]struct{})
+			pIdxFlags := make([]bool, len(paths))
+
+			if level < 0 {
+				cb(-1, nil, Unknown, MalformedJsonError)
+				return -1
+			}
+
+			for pi, p := range paths {
+				if len(p) < level+1 || pathFlags[pi] || p[level][0] != '[' || !sameTree(p, pathsBuf[:level]) {
+					continue
+				}
+				if len(p[level]) >= 2 {
+					aIdx, _ := strconv.Atoi(p[level][1 : len(p[level])-1])
+					arrIdxFlags[aIdx] = x
+					pIdxFlags[pi] = true
+				}
+			}
+
+			if len(arrIdxFlags) > 0 {
+				level++
+
+				var curIdx int
+				arrOff, _ := ArrayEach(data[i:], func(value []byte, dataType ValueType, offset int, err error) {
+					if _, ok = arrIdxFlags[curIdx]; ok {
+						for pi, p := range paths {
+							if pIdxFlags[pi] {
+								aIdx, _ := strconv.Atoi(p[level-1][1 : len(p[level-1])-1])
+
+								if curIdx == aIdx {
+									of := searchKeys(value, p[level:]...)
+
+									pathsMatched++
+									pathFlags[pi] = true
+
+									if of != -1 {
+										v, dt, _, e := Get(value[of:])
+										cb(pi, v, dt, e)
+									}
+								}
+							}
+						}
+					}
+
+					curIdx += 1
+				})
+
+				if pathsMatched == len(paths) {
+					return i
+				}
+
+				i += arrOff - 1
+			} else {
+				// Do not search for keys inside arrays
+				if arraySkip := blockEnd(data[i:], '[', ']'); arraySkip == -1 {
+					return -1
+				} else {
+					i += arraySkip - 1
+				}
+			}
+		case ']':
+			level--
+		}
+
+		i++
+	}
+
+	return -1
+}
+
+// Data types available in valid JSON data.
+type ValueType int
+
+const (
+	NotExist = ValueType(iota)
+	String
+	Number
+	Object
+	Array
+	Boolean
+	Null
+	Unknown
+)
+
+func (vt ValueType) String() string {
+	switch vt {
+	case NotExist:
+		return "non-existent"
+	case String:
+		return "string"
+	case Number:
+		return "number"
+	case Object:
+		return "object"
+	case Array:
+		return "array"
+	case Boolean:
+		return "boolean"
+	case Null:
+		return "null"
+	default:
+		return "unknown"
+	}
+}
+
+var (
+	trueLiteral  = []byte("true")
+	falseLiteral = []byte("false")
+	nullLiteral  = []byte("null")
+)
+
+func createInsertComponent(keys []string, setValue []byte, comma, object bool) []byte {
+	isIndex := string(keys[0][0]) == "["
+	offset := 0
+	lk := calcAllocateSpace(keys, setValue, comma, object)
+	buffer := make([]byte, lk, lk)
+	if comma {
+		offset += WriteToBuffer(buffer[offset:], ",")
+	}
+	if isIndex && !comma {
+		offset += WriteToBuffer(buffer[offset:], "[")
+	} else {
+		if object {
+			offset += WriteToBuffer(buffer[offset:], "{")
+		}
+		if !isIndex {
+			offset += WriteToBuffer(buffer[offset:], "\"")
+			offset += WriteToBuffer(buffer[offset:], keys[0])
+			offset += WriteToBuffer(buffer[offset:], "\":")
+		}
+	}
+
+	for i := 1; i < len(keys); i++ {
+		if string(keys[i][0]) == "[" {
+			offset += WriteToBuffer(buffer[offset:], "[")
+		} else {
+			offset += WriteToBuffer(buffer[offset:], "{\"")
+			offset += WriteToBuffer(buffer[offset:], keys[i])
+			offset += WriteToBuffer(buffer[offset:], "\":")
+		}
+	}
+	offset += WriteToBuffer(buffer[offset:], string(setValue))
+	for i := len(keys) - 1; i > 0; i-- {
+		if string(keys[i][0]) == "[" {
+			offset += WriteToBuffer(buffer[offset:], "]")
+		} else {
+			offset += WriteToBuffer(buffer[offset:], "}")
+		}
+	}
+	if isIndex && !comma {
+		offset += WriteToBuffer(buffer[offset:], "]")
+	}
+	if object && !isIndex {
+		offset += WriteToBuffer(buffer[offset:], "}")
+	}
+	return buffer
+}
+
+func calcAllocateSpace(keys []string, setValue []byte, comma, object bool) int {
+	isIndex := string(keys[0][0]) == "["
+	lk := 0
+	if comma {
+		// ,
+		lk += 1
+	}
+	if isIndex && !comma {
+		// []
+		lk += 2
+	} else {
+		if object {
+			// {
+			lk += 1
+		}
+		if !isIndex {
+			// "keys[0]"
+			lk += len(keys[0]) + 3
+		}
+	}
+
+
+	lk += len(setValue)
+	for i := 1; i < len(keys); i++ {
+		if string(keys[i][0]) == "[" {
+			// []
+			lk += 2
+		} else {
+			// {"keys[i]":setValue}
+			lk += len(keys[i]) + 5
+		}
+	}
+
+	if object && !isIndex {
+		// }
+		lk += 1
+	}
+
+	return lk
+}
+
+func WriteToBuffer(buffer []byte, str string) int {
+	copy(buffer, str)
+	return len(str)
+}
+
+/*
+
+Del - Receives existing data structure, path to delete.
+
+Returns:
+`data` - return modified data
+
+*/
+func Delete(data []byte, keys ...string) []byte {
+	lk := len(keys)
+	if lk == 0 {
+		return data[:0]
+	}
+
+	array := false
+	if len(keys[lk-1]) > 0 && string(keys[lk-1][0]) == "[" {
+		array = true
+	}
+
+	var startOffset, keyOffset int
+	endOffset := len(data)
+	var err error
+	if !array {
+		if len(keys) > 1 {
+			_, _, startOffset, endOffset, err = internalGet(data, keys[:lk-1]...)
+			if err == KeyPathNotFoundError {
+				// problem parsing the data
+				return data
+			}
+		}
+
+		keyOffset, err = findKeyStart(data[startOffset:endOffset], keys[lk-1])
+		if err == KeyPathNotFoundError {
+			// problem parsing the data
+			return data
+		}
+		keyOffset += startOffset
+		_, _, _, subEndOffset, _ := internalGet(data[startOffset:endOffset], keys[lk-1])
+		endOffset = startOffset + subEndOffset
+		tokEnd := tokenEnd(data[endOffset:])
+		tokStart := findTokenStart(data[:keyOffset], ","[0])
+
+		if data[endOffset+tokEnd] == ","[0] {
+			endOffset += tokEnd + 1
+		} else if data[endOffset+tokEnd] == " "[0] && len(data) > endOffset+tokEnd+1 && data[endOffset+tokEnd+1] == ","[0] {
+			endOffset += tokEnd + 2
+		} else if data[endOffset+tokEnd] == "}"[0] && data[tokStart] == ","[0] {
+			keyOffset = tokStart
+		}
+	} else {
+		_, _, keyOffset, endOffset, err = internalGet(data, keys...)
+		if err == KeyPathNotFoundError {
+			// problem parsing the data
+			return data
+		}
+
+		tokEnd := tokenEnd(data[endOffset:])
+		tokStart := findTokenStart(data[:keyOffset], ","[0])
+
+		if data[endOffset+tokEnd] == ","[0] {
+			endOffset += tokEnd + 1
+		} else if data[endOffset+tokEnd] == "]"[0] && data[tokStart] == ","[0] {
+			keyOffset = tokStart
+		}
+	}
+
+	// We need to remove remaining trailing comma if we delete las element in the object
+	prevTok := lastToken(data[:keyOffset])
+	remainedValue := data[endOffset:]
+
+	var newOffset int
+	if nextToken(remainedValue) > -1 && remainedValue[nextToken(remainedValue)] == '}' && data[prevTok] == ',' {
+		newOffset = prevTok
+	} else {
+		newOffset = prevTok + 1
+	}
+
+	// We have to make a copy here if we don't want to mangle the original data, because byte slices are
+	// accessed by reference and not by value
+	dataCopy := make([]byte, len(data))
+	copy(dataCopy, data)
+	data = append(dataCopy[:newOffset], dataCopy[endOffset:]...)
+
+	return data
+}
+
+/*
+
+Set - Receives existing data structure, path to set, and data to set at that key.
+
+Returns:
+`value` - modified byte array
+`err` - On any parsing error
+
+*/
+func Set(data []byte, setValue []byte, keys ...string) (value []byte, err error) {
+	// ensure keys are set
+	if len(keys) == 0 {
+		return nil, KeyPathNotFoundError
+	}
+
+	_, _, startOffset, endOffset, err := internalGet(data, keys...)
+	if err != nil {
+		if err != KeyPathNotFoundError {
+			// problem parsing the data
+			return nil, err
+		}
+		// full path doesnt exist
+		// does any subpath exist?
+		var depth int
+		for i := range keys {
+			_, _, start, end, sErr := internalGet(data, keys[:i+1]...)
+			if sErr != nil {
+				break
+			} else {
+				endOffset = end
+				startOffset = start
+				depth++
+			}
+		}
+		comma := true
+		object := false
+		if endOffset == -1 {
+			firstToken := nextToken(data)
+			// We can't set a top-level key if data isn't an object
+			if firstToken < 0 || data[firstToken] != '{' {
+				return nil, KeyPathNotFoundError
+			}
+			// Don't need a comma if the input is an empty object
+			secondToken := firstToken + 1 + nextToken(data[firstToken+1:])
+			if data[secondToken] == '}' {
+				comma = false
+			}
+			// Set the top level key at the end (accounting for any trailing whitespace)
+			// This assumes last token is valid like '}', could check and return error
+			endOffset = lastToken(data)
+		}
+		depthOffset := endOffset
+		if depth != 0 {
+			// if subpath is a non-empty object, add to it
+			// or if subpath is a non-empty array, add to it
+			if (data[startOffset] == '{' && data[startOffset+1+nextToken(data[startOffset+1:])] != '}') ||
+				(data[startOffset] == '[' && data[startOffset+1+nextToken(data[startOffset+1:])] == '{') && keys[depth:][0][0] == 91 {
+				depthOffset--
+				startOffset = depthOffset
+				// otherwise, over-write it with a new object
+			} else {
+				comma = false
+				object = true
+			}
+		} else {
+			startOffset = depthOffset
+		}
+		value = append(data[:startOffset], append(createInsertComponent(keys[depth:], setValue, comma, object), data[depthOffset:]...)...)
+	} else {
+		// path currently exists
+		startComponent := data[:startOffset]
+		endComponent := data[endOffset:]
+
+		value = make([]byte, len(startComponent)+len(endComponent)+len(setValue))
+		newEndOffset := startOffset + len(setValue)
+		copy(value[0:startOffset], startComponent)
+		copy(value[startOffset:newEndOffset], setValue)
+		copy(value[newEndOffset:], endComponent)
+	}
+	return value, nil
+}
+
+func getType(data []byte, offset int) ([]byte, ValueType, int, error) {
+	var dataType ValueType
+	endOffset := offset
+
+	// if string value
+	if data[offset] == '"' {
+		dataType = String
+		if idx, _ := stringEnd(data[offset+1:]); idx != -1 {
+			endOffset += idx + 1
+		} else {
+			return nil, dataType, offset, MalformedStringError
+		}
+	} else if data[offset] == '[' { // if array value
+		dataType = Array
+		// break label, for stopping nested loops
+		endOffset = blockEnd(data[offset:], '[', ']')
+
+		if endOffset == -1 {
+			return nil, dataType, offset, MalformedArrayError
+		}
+
+		endOffset += offset
+	} else if data[offset] == '{' { // if object value
+		dataType = Object
+		// break label, for stopping nested loops
+		endOffset = blockEnd(data[offset:], '{', '}')
+
+		if endOffset == -1 {
+			return nil, dataType, offset, MalformedObjectError
+		}
+
+		endOffset += offset
+	} else {
+		// Number, Boolean or None
+		end := tokenEnd(data[endOffset:])
+
+		if end == -1 {
+			return nil, dataType, offset, MalformedValueError
+		}
+
+		value := data[offset : endOffset+end]
+
+		switch data[offset] {
+		case 't', 'f': // true or false
+			if bytes.Equal(value, trueLiteral) || bytes.Equal(value, falseLiteral) {
+				dataType = Boolean
+			} else {
+				return nil, Unknown, offset, UnknownValueTypeError
+			}
+		case 'u', 'n': // undefined or null
+			if bytes.Equal(value, nullLiteral) {
+				dataType = Null
+			} else {
+				return nil, Unknown, offset, UnknownValueTypeError
+			}
+		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-':
+			dataType = Number
+		default:
+			return nil, Unknown, offset, UnknownValueTypeError
+		}
+
+		endOffset += end
+	}
+	return data[offset:endOffset], dataType, endOffset, nil
+}
+
+/*
+Get - Receives data structure, and key path to extract value from.
+
+Returns:
+`value` - Pointer to original data structure containing key value, or just empty slice if nothing found or error
+`dataType` -    Can be: `NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null`
+`offset` - Offset from provided data structure where key value ends. Used mostly internally, for example for `ArrayEach` helper.
+`err` - If key not found or any other parsing issue it should return error. If key not found it also sets `dataType` to `NotExist`
+
+Accept multiple keys to specify path to JSON value (in case of quering nested structures).
+If no keys provided it will try to extract closest JSON value (simple ones or object/array), useful for reading streams or arrays, see `ArrayEach` implementation.
+*/
+func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset int, err error) {
+	a, b, _, d, e := internalGet(data, keys...)
+	return a, b, d, e
+}
+
+func internalGet(data []byte, keys ...string) (value []byte, dataType ValueType, offset, endOffset int, err error) {
+	if len(keys) > 0 {
+		if offset = searchKeys(data, keys...); offset == -1 {
+			return nil, NotExist, -1, -1, KeyPathNotFoundError
+		}
+	}
+
+	// Go to closest value
+	nO := nextToken(data[offset:])
+	if nO == -1 {
+		return nil, NotExist, offset, -1, MalformedJsonError
+	}
+
+	offset += nO
+	value, dataType, endOffset, err = getType(data, offset)
+	if err != nil {
+		return value, dataType, offset, endOffset, err
+	}
+
+	// Strip quotes from string values
+	if dataType == String {
+		value = value[1 : len(value)-1]
+	}
+
+	return value[:len(value):len(value)], dataType, offset, endOffset, nil
+}
+
+// ArrayEach is used when iterating arrays, accepts a callback function with the same return arguments as `Get`.
+func ArrayEach(data []byte, cb func(value []byte, dataType ValueType, offset int, err error), keys ...string) (offset int, err error) {
+	if len(data) == 0 {
+		return -1, MalformedObjectError
+	}
+
+	nT := nextToken(data)
+	if nT == -1 {
+		return -1, MalformedJsonError
+	}
+
+	offset = nT + 1
+
+	if len(keys) > 0 {
+		if offset = searchKeys(data, keys...); offset == -1 {
+			return offset, KeyPathNotFoundError
+		}
+
+		// Go to closest value
+		nO := nextToken(data[offset:])
+		if nO == -1 {
+			return offset, MalformedJsonError
+		}
+
+		offset += nO
+
+		if data[offset] != '[' {
+			return offset, MalformedArrayError
+		}
+
+		offset++
+	}
+
+	nO := nextToken(data[offset:])
+	if nO == -1 {
+		return offset, MalformedJsonError
+	}
+
+	offset += nO
+
+	if data[offset] == ']' {
+		return offset, nil
+	}
+
+	for true {
+		v, t, o, e := Get(data[offset:])
+
+		if e != nil {
+			return offset, e
+		}
+
+		if o == 0 {
+			break
+		}
+
+		if t != NotExist {
+			cb(v, t, offset+o-len(v), e)
+		}
+
+		if e != nil {
+			break
+		}
+
+		offset += o
+
+		skipToToken := nextToken(data[offset:])
+		if skipToToken == -1 {
+			return offset, MalformedArrayError
+		}
+		offset += skipToToken
+
+		if data[offset] == ']' {
+			break
+		}
+
+		if data[offset] != ',' {
+			return offset, MalformedArrayError
+		}
+
+		offset++
+	}
+
+	return offset, nil
+}
+
+// ObjectEach iterates over the key-value pairs of a JSON object, invoking a given callback for each such entry
+func ObjectEach(data []byte, callback func(key []byte, value []byte, dataType ValueType, offset int) error, keys ...string) (err error) {
+	offset := 0
+
+	// Descend to the desired key, if requested
+	if len(keys) > 0 {
+		if off := searchKeys(data, keys...); off == -1 {
+			return KeyPathNotFoundError
+		} else {
+			offset = off
+		}
+	}
+
+	// Validate and skip past opening brace
+	if off := nextToken(data[offset:]); off == -1 {
+		return MalformedObjectError
+	} else if offset += off; data[offset] != '{' {
+		return MalformedObjectError
+	} else {
+		offset++
+	}
+
+	// Skip to the first token inside the object, or stop if we find the ending brace
+	if off := nextToken(data[offset:]); off == -1 {
+		return MalformedJsonError
+	} else if offset += off; data[offset] == '}' {
+		return nil
+	}
+
+	// Loop pre-condition: data[offset] points to what should be either the next entry's key, or the closing brace (if it's anything else, the JSON is malformed)
+	for offset < len(data) {
+		// Step 1: find the next key
+		var key []byte
+
+		// Check what the the next token is: start of string, end of object, or something else (error)
+		switch data[offset] {
+		case '"':
+			offset++ // accept as string and skip opening quote
+		case '}':
+			return nil // we found the end of the object; stop and return success
+		default:
+			return MalformedObjectError
+		}
+
+		// Find the end of the key string
+		var keyEscaped bool
+		if off, esc := stringEnd(data[offset:]); off == -1 {
+			return MalformedJsonError
+		} else {
+			key, keyEscaped = data[offset:offset+off-1], esc
+			offset += off
+		}
+
+		// Unescape the string if needed
+		if keyEscaped {
+			var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings
+			if keyUnescaped, err := Unescape(key, stackbuf[:]); err != nil {
+				return MalformedStringEscapeError
+			} else {
+				key = keyUnescaped
+			}
+		}
+
+		// Step 2: skip the colon
+		if off := nextToken(data[offset:]); off == -1 {
+			return MalformedJsonError
+		} else if offset += off; data[offset] != ':' {
+			return MalformedJsonError
+		} else {
+			offset++
+		}
+
+		// Step 3: find the associated value, then invoke the callback
+		if value, valueType, off, err := Get(data[offset:]); err != nil {
+			return err
+		} else if err := callback(key, value, valueType, offset+off); err != nil { // Invoke the callback here!
+			return err
+		} else {
+			offset += off
+		}
+
+		// Step 4: skip over the next comma to the following token, or stop if we hit the ending brace
+		if off := nextToken(data[offset:]); off == -1 {
+			return MalformedArrayError
+		} else {
+			offset += off
+			switch data[offset] {
+			case '}':
+				return nil // Stop if we hit the close brace
+			case ',':
+				offset++ // Ignore the comma
+			default:
+				return MalformedObjectError
+			}
+		}
+
+		// Skip to the next token after the comma
+		if off := nextToken(data[offset:]); off == -1 {
+			return MalformedArrayError
+		} else {
+			offset += off
+		}
+	}
+
+	return MalformedObjectError // we shouldn't get here; it's expected that we will return via finding the ending brace
+}
+
+// GetUnsafeString returns the value retrieved by `Get`, use creates string without memory allocation by mapping string to slice memory. It does not handle escape symbols.
+func GetUnsafeString(data []byte, keys ...string) (val string, err error) {
+	v, _, _, e := Get(data, keys...)
+
+	if e != nil {
+		return "", e
+	}
+
+	return bytesToString(&v), nil
+}
+
+// GetString returns the value retrieved by `Get`, cast to a string if possible, trying to properly handle escape and utf8 symbols
+// If key data type do not match, it will return an error.
+func GetString(data []byte, keys ...string) (val string, err error) {
+	v, t, _, e := Get(data, keys...)
+
+	if e != nil {
+		return "", e
+	}
+
+	if t != String {
+		return "", fmt.Errorf("Value is not a string: %s", string(v))
+	}
+
+	// If no escapes return raw content
+	if bytes.IndexByte(v, '\\') == -1 {
+		return string(v), nil
+	}
+
+	return ParseString(v)
+}
+
+// GetFloat returns the value retrieved by `Get`, cast to a float64 if possible.
+// The offset is the same as in `Get`.
+// If key data type do not match, it will return an error.
+func GetFloat(data []byte, keys ...string) (val float64, err error) {
+	v, t, _, e := Get(data, keys...)
+
+	if e != nil {
+		return 0, e
+	}
+
+	if t != Number {
+		return 0, fmt.Errorf("Value is not a number: %s", string(v))
+	}
+
+	return ParseFloat(v)
+}
+
+// GetInt returns the value retrieved by `Get`, cast to a int64 if possible.
+// If key data type do not match, it will return an error.
+func GetInt(data []byte, keys ...string) (val int64, err error) {
+	v, t, _, e := Get(data, keys...)
+
+	if e != nil {
+		return 0, e
+	}
+
+	if t != Number {
+		return 0, fmt.Errorf("Value is not a number: %s", string(v))
+	}
+
+	return ParseInt(v)
+}
+
+// GetBoolean returns the value retrieved by `Get`, cast to a bool if possible.
+// The offset is the same as in `Get`.
+// If key data type do not match, it will return error.
+func GetBoolean(data []byte, keys ...string) (val bool, err error) {
+	v, t, _, e := Get(data, keys...)
+
+	if e != nil {
+		return false, e
+	}
+
+	if t != Boolean {
+		return false, fmt.Errorf("Value is not a boolean: %s", string(v))
+	}
+
+	return ParseBoolean(v)
+}
+
+// ParseBoolean parses a Boolean ValueType into a Go bool (not particularly useful, but here for completeness)
+func ParseBoolean(b []byte) (bool, error) {
+	switch {
+	case bytes.Equal(b, trueLiteral):
+		return true, nil
+	case bytes.Equal(b, falseLiteral):
+		return false, nil
+	default:
+		return false, MalformedValueError
+	}
+}
+
+// ParseString parses a String ValueType into a Go string (the main parsing work is unescaping the JSON string)
+func ParseString(b []byte) (string, error) {
+	var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings
+	if bU, err := Unescape(b, stackbuf[:]); err != nil {
+		return "", MalformedValueError
+	} else {
+		return string(bU), nil
+	}
+}
+
+// ParseNumber parses a Number ValueType into a Go float64
+func ParseFloat(b []byte) (float64, error) {
+	if v, err := parseFloat(&b); err != nil {
+		return 0, MalformedValueError
+	} else {
+		return v, nil
+	}
+}
+
+// ParseInt parses a Number ValueType into a Go int64
+func ParseInt(b []byte) (int64, error) {
+	if v, ok, overflow := parseInt(b); !ok {
+		if overflow {
+			return 0, OverflowIntegerError
+		}
+		return 0, MalformedValueError
+	} else {
+		return v, nil
+	}
+}
diff --git a/vendor/github.com/carlmjohnson/requests/builder_core.go b/vendor/github.com/carlmjohnson/requests/builder_core.go
index 615bbe07d..dac6a7426 100644
--- a/vendor/github.com/carlmjohnson/requests/builder_core.go
+++ b/vendor/github.com/carlmjohnson/requests/builder_core.go
@@ -6,9 +6,9 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"slices"
 
 	"github.com/carlmjohnson/requests/internal/minitrue"
-	"github.com/carlmjohnson/requests/internal/slicex"
 )
 
 // Builder is a convenient way to build, send, and handle HTTP requests.
@@ -208,7 +208,7 @@ func (rb *Builder) Clone() *Builder {
 	rb2 := *rb
 	rb2.ub = *rb.ub.Clone()
 	rb2.rb = *rb.rb.Clone()
-	slicex.Clip(&rb2.validators)
+	rb2.validators = slices.Clip(rb2.validators)
 	return &rb2
 }
 
diff --git a/vendor/github.com/carlmjohnson/requests/core_req.go b/vendor/github.com/carlmjohnson/requests/core_req.go
index 6b8119c5d..d544a357c 100644
--- a/vendor/github.com/carlmjohnson/requests/core_req.go
+++ b/vendor/github.com/carlmjohnson/requests/core_req.go
@@ -6,9 +6,9 @@ import (
 	"io"
 	"net/http"
 	"net/url"
+	"slices"
 
 	"github.com/carlmjohnson/requests/internal/minitrue"
-	"github.com/carlmjohnson/requests/internal/slicex"
 )
 
 // nopCloser is like io.NopCloser(),
@@ -57,8 +57,8 @@ func (rb *requestBuilder) Body(src BodyGetter) {
 // Clone creates a new Builder suitable for independent mutation.
 func (rb *requestBuilder) Clone() *requestBuilder {
 	rb2 := *rb
-	slicex.Clip(&rb2.headers)
-	slicex.Clip(&rb2.cookies)
+	rb2.headers = slices.Clip(rb2.headers)
+	rb2.cookies = slices.Clip(rb2.cookies)
 	return &rb2
 }
 
diff --git a/vendor/github.com/carlmjohnson/requests/core_url.go b/vendor/github.com/carlmjohnson/requests/core_url.go
index 6cdde56c2..00d7db18b 100644
--- a/vendor/github.com/carlmjohnson/requests/core_url.go
+++ b/vendor/github.com/carlmjohnson/requests/core_url.go
@@ -3,8 +3,7 @@ package requests
 import (
 	"cmp"
 	"net/url"
-
-	"github.com/carlmjohnson/requests/internal/slicex"
+	"slices"
 )
 
 type multimap struct {
@@ -51,8 +50,8 @@ func (ub *urlBuilder) ParamOptional(key string, values ...string) {
 
 func (ub *urlBuilder) Clone() *urlBuilder {
 	ub2 := *ub
-	slicex.Clip(&ub2.paths)
-	slicex.Clip(&ub2.params)
+	ub2.paths = slices.Clip(ub2.paths)
+	ub2.params = slices.Clip(ub2.params)
 	return &ub2
 }
 
diff --git a/vendor/github.com/carlmjohnson/requests/internal/slicex/slicex.go b/vendor/github.com/carlmjohnson/requests/internal/slicex/slicex.go
deleted file mode 100644
index ee2453361..000000000
--- a/vendor/github.com/carlmjohnson/requests/internal/slicex/slicex.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package slicex
-
-func Clip[T any](sp *[]T) {
-	s := *sp
-	*sp = s[:len(s):len(s)]
-}
diff --git a/vendor/github.com/carlmjohnson/requests/validator.go b/vendor/github.com/carlmjohnson/requests/validator.go
index e973ca565..07701810d 100644
--- a/vendor/github.com/carlmjohnson/requests/validator.go
+++ b/vendor/github.com/carlmjohnson/requests/validator.go
@@ -5,8 +5,10 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"maps"
 	"mime"
 	"net/http"
+	"slices"
 )
 
 // DefaultValidator is the validator applied by Builder unless otherwise specified.
@@ -29,10 +31,8 @@ func (se *ResponseError) Error() string {
 // CheckStatus validates the response has an acceptable status code.
 func CheckStatus(acceptStatuses ...int) ResponseHandler {
 	return func(res *http.Response) error {
-		for _, code := range acceptStatuses {
-			if res.StatusCode == code {
-				return nil
-			}
+		if slices.Contains(acceptStatuses, res.StatusCode) {
+			return nil
 		}
 
 		return fmt.Errorf("%w: unexpected status: %d",
@@ -46,11 +46,7 @@ func HasStatusErr(err error, codes ...int) bool {
 		return false
 	}
 	if se := new(ResponseError); errors.As(err, &se) {
-		for _, code := range codes {
-			if se.StatusCode == code {
-				return true
-			}
-		}
+		return slices.Contains(codes, se.StatusCode)
 	}
 	return false
 }
@@ -63,10 +59,8 @@ func CheckContentType(cts ...string) ResponseHandler {
 			return fmt.Errorf("%w: problem matching Content-Type",
 				(*ResponseError)(res))
 		}
-		for _, ct := range cts {
-			if mt == ct {
-				return nil
-			}
+		if slices.Contains(cts, mt) {
+			return nil
 		}
 		return fmt.Errorf("%w: unexpected Content-Type: %s",
 			(*ResponseError)(res), mt)
@@ -101,9 +95,7 @@ func CheckPeek(n int, f func([]byte) error) ResponseHandler {
 // CopyHeaders copies the response headers to h.
 func CopyHeaders(h map[string][]string) ResponseHandler {
 	return func(res *http.Response) error {
-		for k, v := range res.Header {
-			h[k] = v
-		}
+		maps.Copy(h, res.Header)
 
 		return nil
 	}
diff --git a/vendor/github.com/charmbracelet/bubbletea/.golangci.yml b/vendor/github.com/charmbracelet/bubbletea/.golangci.yml
index be61d89ba..929cb0ac9 100644
--- a/vendor/github.com/charmbracelet/bubbletea/.golangci.yml
+++ b/vendor/github.com/charmbracelet/bubbletea/.golangci.yml
@@ -7,7 +7,6 @@ linters:
     - exhaustive
     - goconst
     - godot
-    - godox
     - gomoddirectives
     - goprintffuncname
     - gosec
@@ -27,6 +26,10 @@ linters:
     - whitespace
     - wrapcheck
   exclusions:
+    rules:
+      - text: '(slog|log)\.\w+'
+        linters:
+          - noctx
     generated: lax
     presets:
       - common-false-positives
diff --git a/vendor/github.com/charmbracelet/bubbletea/LICENSE b/vendor/github.com/charmbracelet/bubbletea/LICENSE
index 31d76c1c6..be3f0c19e 100644
--- a/vendor/github.com/charmbracelet/bubbletea/LICENSE
+++ b/vendor/github.com/charmbracelet/bubbletea/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2020-2023 Charmbracelet, Inc
+Copyright (c) 2020-2025 Charmbracelet, Inc
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/vendor/github.com/charmbracelet/bubbletea/README.md b/vendor/github.com/charmbracelet/bubbletea/README.md
index 8f5b1c47a..83f3da073 100644
--- a/vendor/github.com/charmbracelet/bubbletea/README.md
+++ b/vendor/github.com/charmbracelet/bubbletea/README.md
@@ -9,8 +9,7 @@
     <br>
     <a href="https://github.com/charmbracelet/bubbletea/releases"><img src="https://img.shields.io/github/release/charmbracelet/bubbletea.svg" alt="Latest Release"></a>
     <a href="https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc"><img src="https://godoc.org/github.com/charmbracelet/bubbletea?status.svg" alt="GoDoc"></a>
-    <a href="https://github.com/charmbracelet/bubbletea/actions"><img src="https://github.com/charmbracelet/bubbletea/actions/workflows/build.yml/badge.svg" alt="Build Status"></a>
-    <a href="https://www.phorm.ai/query?projectId=a0e324b6-b706-4546-b951-6671ea60c13f"><img src="https://stuff.charm.sh/misc/phorm-badge.svg" alt="phorm.ai"></a>
+    <a href="https://github.com/charmbracelet/bubbletea/actions"><img src="https://github.com/charmbracelet/bubbletea/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status"></a>
 </p>
 
 The fun, functional and stateful way to build terminal apps. A Go framework
@@ -396,6 +395,6 @@ of days past.
 
 Part of [Charm](https://charm.sh).
 
-<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
+<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-banner-next.jpg" width="400"></a>
 
 Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
diff --git a/vendor/github.com/charmbracelet/bubbletea/commands.go b/vendor/github.com/charmbracelet/bubbletea/commands.go
index bfa3b7045..ec9dac288 100644
--- a/vendor/github.com/charmbracelet/bubbletea/commands.go
+++ b/vendor/github.com/charmbracelet/bubbletea/commands.go
@@ -13,6 +13,27 @@ import (
 //		       return tea.Batch(someCommand, someOtherCommand)
 //	    }
 func Batch(cmds ...Cmd) Cmd {
+	return compactCmds[BatchMsg](cmds)
+}
+
+// BatchMsg is a message used to perform a bunch of commands concurrently with
+// no ordering guarantees. You can send a BatchMsg with Batch.
+type BatchMsg []Cmd
+
+// Sequence runs the given commands one at a time, in order. Contrast this with
+// Batch, which runs commands concurrently.
+func Sequence(cmds ...Cmd) Cmd {
+	return compactCmds[sequenceMsg](cmds)
+}
+
+// sequenceMsg is used internally to run the given commands in order.
+type sequenceMsg []Cmd
+
+// compactCmds ignores any nil commands in cmds, and returns the most direct
+// command possible. That is, considering the non-nil commands, if there are
+// none it returns nil, if there is exactly one it returns that command
+// directly, else it returns the non-nil commands as type T.
+func compactCmds[T ~[]Cmd](cmds []Cmd) Cmd {
 	var validCmds []Cmd //nolint:prealloc
 	for _, c := range cmds {
 		if c == nil {
@@ -27,26 +48,11 @@ func Batch(cmds ...Cmd) Cmd {
 		return validCmds[0]
 	default:
 		return func() Msg {
-			return BatchMsg(validCmds)
+			return T(validCmds)
 		}
 	}
 }
 
-// BatchMsg is a message used to perform a bunch of commands concurrently with
-// no ordering guarantees. You can send a BatchMsg with Batch.
-type BatchMsg []Cmd
-
-// Sequence runs the given commands one at a time, in order. Contrast this with
-// Batch, which runs commands concurrently.
-func Sequence(cmds ...Cmd) Cmd {
-	return func() Msg {
-		return sequenceMsg(cmds)
-	}
-}
-
-// sequenceMsg is used internally to run the given commands in order.
-type sequenceMsg []Cmd
-
 // Every is a command that ticks in sync with the system clock. So, if you
 // wanted to tick with the system clock every second, minute or hour you
 // could use this. It's also handy for having different things tick in sync.
diff --git a/vendor/github.com/charmbracelet/bubbletea/exec.go b/vendor/github.com/charmbracelet/bubbletea/exec.go
index 7a14d2a77..80484145f 100644
--- a/vendor/github.com/charmbracelet/bubbletea/exec.go
+++ b/vendor/github.com/charmbracelet/bubbletea/exec.go
@@ -114,6 +114,7 @@ func (p *Program) exec(c ExecCommand, fn ExecCallback) {
 
 	// Execute system command.
 	if err := c.Run(); err != nil {
+		p.renderer.resetLinesRendered()
 		_ = p.RestoreTerminal() // also try to restore the terminal.
 		if fn != nil {
 			go p.Send(fn(err))
@@ -121,6 +122,9 @@ func (p *Program) exec(c ExecCommand, fn ExecCallback) {
 		return
 	}
 
+	// Maintain the existing output from the command
+	p.renderer.resetLinesRendered()
+
 	// Have the program re-capture input.
 	err := p.RestoreTerminal()
 	if fn != nil {
diff --git a/vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go b/vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go
index 9af89428d..1e48f70c3 100644
--- a/vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go
+++ b/vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go
@@ -67,6 +67,8 @@ func newInputReader(r io.Reader, enableMouse bool) (cancelreader.CancelReader, e
 func (r *conInputReader) Cancel() bool {
 	r.setCanceled()
 
+	// Warning: These cancel methods do not reliably work on console input
+	// 			and should not be counted on.
 	return windows.CancelIoEx(r.conin, nil) == nil || windows.CancelIo(r.conin) == nil
 }
 
@@ -106,7 +108,7 @@ func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32,
 	return originalMode, nil
 }
 
-// cancelMixin represents a goroutine-safe cancelation status.
+// cancelMixin represents a goroutine-safe cancellation status.
 type cancelMixin struct {
 	unsafeCanceled bool
 	lock           sync.Mutex
diff --git a/vendor/github.com/charmbracelet/bubbletea/key_windows.go b/vendor/github.com/charmbracelet/bubbletea/key_windows.go
index c977fb842..648bd784c 100644
--- a/vendor/github.com/charmbracelet/bubbletea/key_windows.go
+++ b/vendor/github.com/charmbracelet/bubbletea/key_windows.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"fmt"
 	"io"
+	"time"
 
 	"github.com/erikgeiser/coninput"
 	localereader "github.com/mattn/go-localereader"
@@ -25,14 +26,10 @@ func readConInputs(ctx context.Context, msgsch chan<- Msg, con *conInputReader)
 	var ps coninput.ButtonState                 // keep track of previous mouse state
 	var ws coninput.WindowBufferSizeEventRecord // keep track of the last window size event
 	for {
-		events, err := coninput.ReadNConsoleInputs(con.conin, 16)
+		events, err := peekAndReadConsInput(con)
 		if err != nil {
-			if con.isCanceled() {
-				return cancelreader.ErrCanceled
-			}
-			return fmt.Errorf("read coninput events: %w", err)
+			return err
 		}
-
 		for _, event := range events {
 			var msgs []Msg
 			switch e := event.Unwrap().(type) {
@@ -94,6 +91,50 @@ func readConInputs(ctx context.Context, msgsch chan<- Msg, con *conInputReader)
 	}
 }
 
+// Peek for new input in a tight loop and then read the input.
+// windows.CancelIo* does not work reliably so peek first and only use the data if
+// the console input is not cancelled.
+func peekAndReadConsInput(con *conInputReader) ([]coninput.InputRecord, error) {
+	events, err := peekConsInput(con)
+	if err != nil {
+		return events, err
+	}
+	events, err = coninput.ReadNConsoleInputs(con.conin, intToUint32OrDie(len(events)))
+	if con.isCanceled() {
+		return events, cancelreader.ErrCanceled
+	}
+	if err != nil {
+		return events, fmt.Errorf("read coninput events: %w", err)
+	}
+	return events, nil
+}
+
+// Convert i to unit32 or panic if it cannot be converted. Check satisfies lint G115.
+func intToUint32OrDie(i int) uint32 {
+	if i < 0 {
+		panic("cannot convert numEvents " + fmt.Sprint(i) + " to uint32")
+	}
+	return uint32(i) //nolint:gosec
+}
+
+// Keeps peeking until there is data or the input is cancelled.
+func peekConsInput(con *conInputReader) ([]coninput.InputRecord, error) {
+	for {
+		events, err := coninput.PeekNConsoleInputs(con.conin, 16)
+		if con.isCanceled() {
+			return events, cancelreader.ErrCanceled
+		}
+		if err != nil {
+			return events, fmt.Errorf("peek coninput events: %w", err)
+		}
+		if len(events) > 0 {
+			return events, nil
+		}
+		// Sleep for a bit to avoid busy waiting.
+		time.Sleep(16 * time.Millisecond)
+	}
+}
+
 func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, action MouseAction) {
 	btn := p ^ s
 	action = MouseActionPress
@@ -117,16 +158,16 @@ func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, action Mou
 		return button, action
 	}
 
-	switch {
-	case btn == coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button
+	switch btn {
+	case coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button
 		button = MouseButtonLeft
-	case btn == coninput.RIGHTMOST_BUTTON_PRESSED: // right button
+	case coninput.RIGHTMOST_BUTTON_PRESSED: // right button
 		button = MouseButtonRight
-	case btn == coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button
+	case coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button
 		button = MouseButtonMiddle
-	case btn == coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward)
+	case coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward)
 		button = MouseButtonBackward
-	case btn == coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward)
+	case coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward)
 		button = MouseButtonForward
 	}
 
diff --git a/vendor/github.com/charmbracelet/bubbletea/nil_renderer.go b/vendor/github.com/charmbracelet/bubbletea/nil_renderer.go
index 0bc4a1720..1bc909b6d 100644
--- a/vendor/github.com/charmbracelet/bubbletea/nil_renderer.go
+++ b/vendor/github.com/charmbracelet/bubbletea/nil_renderer.go
@@ -26,3 +26,4 @@ func (n nilRenderer) setWindowTitle(_ string)    {}
 func (n nilRenderer) reportFocus() bool          { return false }
 func (n nilRenderer) enableReportFocus()         {}
 func (n nilRenderer) disableReportFocus()        {}
+func (n nilRenderer) resetLinesRendered()        {}
diff --git a/vendor/github.com/charmbracelet/bubbletea/renderer.go b/vendor/github.com/charmbracelet/bubbletea/renderer.go
index 9eb7943bc..5b6df66a7 100644
--- a/vendor/github.com/charmbracelet/bubbletea/renderer.go
+++ b/vendor/github.com/charmbracelet/bubbletea/renderer.go
@@ -79,6 +79,9 @@ type renderer interface {
 
 	// disableReportFocus stops reporting focus events to the program.
 	disableReportFocus()
+
+	// resetLinesRendered ensures exec output remains on screen on exit
+	resetLinesRendered()
 }
 
 // repaintMsg forces a full repaint.
diff --git a/vendor/github.com/charmbracelet/bubbletea/screen.go b/vendor/github.com/charmbracelet/bubbletea/screen.go
index dfec48f0b..02e1bfb0a 100644
--- a/vendor/github.com/charmbracelet/bubbletea/screen.go
+++ b/vendor/github.com/charmbracelet/bubbletea/screen.go
@@ -131,7 +131,7 @@ func EnableBracketedPaste() Msg {
 type enableBracketedPasteMsg struct{}
 
 // DisableBracketedPaste is a special command that tells the Bubble Tea program
-// to accept bracketed paste input.
+// to stop processing bracketed paste input.
 //
 // Note that bracketed paste will be automatically disabled when the
 // program quits.
diff --git a/vendor/github.com/charmbracelet/bubbletea/standard_renderer.go b/vendor/github.com/charmbracelet/bubbletea/standard_renderer.go
index 45e8c82d2..969a58c29 100644
--- a/vendor/github.com/charmbracelet/bubbletea/standard_renderer.go
+++ b/vendor/github.com/charmbracelet/bubbletea/standard_renderer.go
@@ -277,7 +277,7 @@ func (r *standardRenderer) flush() {
 		// using the full terminal window.
 		buf.WriteString(ansi.CursorPosition(0, len(newLines)))
 	} else {
-		buf.WriteString(ansi.CursorBackward(r.width))
+		buf.WriteByte('\r')
 	}
 
 	_, _ = r.out.Write(buf.Bytes())
@@ -545,6 +545,10 @@ func (r *standardRenderer) clearIgnoredLines() {
 	r.ignoreLines = nil
 }
 
+func (r *standardRenderer) resetLinesRendered() {
+	r.linesRendered = 0
+}
+
 // insertTop effectively scrolls up. It inserts lines at the top of a given
 // area designated to be a scrollable region, pushing everything else down.
 // This is roughly how ncurses does it.
diff --git a/vendor/github.com/charmbracelet/bubbletea/tea.go b/vendor/github.com/charmbracelet/bubbletea/tea.go
index c4866f220..db84343e3 100644
--- a/vendor/github.com/charmbracelet/bubbletea/tea.go
+++ b/vendor/github.com/charmbracelet/bubbletea/tea.go
@@ -24,7 +24,6 @@ import (
 
 	"github.com/charmbracelet/x/term"
 	"github.com/muesli/cancelreader"
-	"golang.org/x/sync/errgroup"
 )
 
 // ErrProgramPanic is returned by [Program.Run] when the program recovers from a panic.
@@ -73,7 +72,7 @@ const (
 	customInput
 )
 
-// String implements the stringer interface for [inputType]. It is inteded to
+// String implements the stringer interface for [inputType]. It is intended to
 // be used in testing.
 func (i inputType) String() string {
 	return [...]string{
@@ -220,7 +219,7 @@ func Suspend() Msg {
 // You can send this message with [Suspend()].
 type SuspendMsg struct{}
 
-// ResumeMsg can be listen to to do something once a program is resumed back
+// ResumeMsg can be listen to do something once a program is resumed back
 // from a suspend state.
 type ResumeMsg struct{}
 
@@ -472,42 +471,12 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
 				p.exec(msg.cmd, msg.fn)
 
 			case BatchMsg:
-				for _, cmd := range msg {
-					select {
-					case <-p.ctx.Done():
-						return model, nil
-					case cmds <- cmd:
-					}
-				}
+				go p.execBatchMsg(msg)
 				continue
 
 			case sequenceMsg:
-				go func() {
-					// Execute commands one at a time, in order.
-					for _, cmd := range msg {
-						if cmd == nil {
-							continue
-						}
-
-						msg := cmd()
-						if batchMsg, ok := msg.(BatchMsg); ok {
-							g, _ := errgroup.WithContext(p.ctx)
-							for _, cmd := range batchMsg {
-								cmd := cmd
-								g.Go(func() error {
-									p.Send(cmd())
-									return nil
-								})
-							}
-
-							//nolint:errcheck,gosec
-							g.Wait() // wait for all commands from batch msg to finish
-							continue
-						}
-
-						p.Send(msg)
-					}
-				}()
+				go p.execSequenceMsg(msg)
+				continue
 
 			case setWindowTitleMsg:
 				p.SetWindowTitle(string(msg))
@@ -535,6 +504,74 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
 	}
 }
 
+func (p *Program) execSequenceMsg(msg sequenceMsg) {
+	if !p.startupOptions.has(withoutCatchPanics) {
+		defer func() {
+			if r := recover(); r != nil {
+				p.recoverFromGoPanic(r)
+			}
+		}()
+	}
+
+	// Execute commands one at a time, in order.
+	for _, cmd := range msg {
+		if cmd == nil {
+			continue
+		}
+		msg := cmd()
+		switch msg := msg.(type) {
+		case BatchMsg:
+			p.execBatchMsg(msg)
+		case sequenceMsg:
+			p.execSequenceMsg(msg)
+		default:
+			p.Send(msg)
+		}
+	}
+}
+
+func (p *Program) execBatchMsg(msg BatchMsg) {
+	if !p.startupOptions.has(withoutCatchPanics) {
+		defer func() {
+			if r := recover(); r != nil {
+				p.recoverFromGoPanic(r)
+			}
+		}()
+	}
+
+	// Execute commands one at a time.
+	var wg sync.WaitGroup
+	for _, cmd := range msg {
+		if cmd == nil {
+			continue
+		}
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+
+			if !p.startupOptions.has(withoutCatchPanics) {
+				defer func() {
+					if r := recover(); r != nil {
+						p.recoverFromGoPanic(r)
+					}
+				}()
+			}
+
+			msg := cmd()
+			switch msg := msg.(type) {
+			case BatchMsg:
+				p.execBatchMsg(msg)
+			case sequenceMsg:
+				p.execSequenceMsg(msg)
+			default:
+				p.Send(msg)
+			}
+		}()
+	}
+
+	wg.Wait() // wait for all commands from batch msg to finish
+}
+
 // Run initializes the program and runs its event loops, blocking until it gets
 // terminated by either [Program.Quit], [Program.Kill], or its signal handler.
 // Returns the final model.
diff --git a/vendor/github.com/charmbracelet/bubbletea/tty_windows.go b/vendor/github.com/charmbracelet/bubbletea/tty_windows.go
index 154491a6a..211151f78 100644
--- a/vendor/github.com/charmbracelet/bubbletea/tty_windows.go
+++ b/vendor/github.com/charmbracelet/bubbletea/tty_windows.go
@@ -56,7 +56,7 @@ func (p *Program) initInput() (err error) {
 
 // Open the Windows equivalent of a TTY.
 func openInputTTY() (*os.File, error) {
-	f, err := os.OpenFile("CONIN$", os.O_RDWR, 0o644)
+	f, err := os.OpenFile("CONIN$", os.O_RDWR, 0o644) //nolint:gosec
 	if err != nil {
 		return nil, fmt.Errorf("error opening file: %w", err)
 	}
diff --git a/vendor/github.com/charmbracelet/colorprofile/.golangci.yml b/vendor/github.com/charmbracelet/colorprofile/.golangci.yml
index be61d89ba..929cb0ac9 100644
--- a/vendor/github.com/charmbracelet/colorprofile/.golangci.yml
+++ b/vendor/github.com/charmbracelet/colorprofile/.golangci.yml
@@ -7,7 +7,6 @@ linters:
     - exhaustive
     - goconst
     - godot
-    - godox
     - gomoddirectives
     - goprintffuncname
     - gosec
@@ -27,6 +26,10 @@ linters:
     - whitespace
     - wrapcheck
   exclusions:
+    rules:
+      - text: '(slog|log)\.\w+'
+        linters:
+          - noctx
     generated: lax
     presets:
       - common-false-positives
diff --git a/vendor/github.com/charmbracelet/colorprofile/README.md b/vendor/github.com/charmbracelet/colorprofile/README.md
index c72b2f4b5..2afffce07 100644
--- a/vendor/github.com/charmbracelet/colorprofile/README.md
+++ b/vendor/github.com/charmbracelet/colorprofile/README.md
@@ -82,6 +82,12 @@ w.Profile = colorprofile.NoTTY
 fmt.Fprintf(w, myFancyANSI) // not as fancy
 ```
 
+## Contributing
+
+See [contributing][contribute].
+
+[contribute]: https://github.com/charmbracelet/colorprofile/contribute
+
 ## Feedback
 
 We’d love to hear your thoughts on this project. Feel free to drop us a note!
diff --git a/vendor/github.com/charmbracelet/colorprofile/env.go b/vendor/github.com/charmbracelet/colorprofile/env.go
index 749f989e2..3c312c9fd 100644
--- a/vendor/github.com/charmbracelet/colorprofile/env.go
+++ b/vendor/github.com/charmbracelet/colorprofile/env.go
@@ -153,29 +153,24 @@ func envColorProfile(env environ) (p Profile) {
 		p = ANSI
 	}
 
-	parts := strings.Split(term, "-")
-	switch parts[0] {
-	case "alacritty",
-		"contour",
-		"foot",
-		"ghostty",
-		"kitty",
-		"rio",
-		"st",
-		"wezterm":
+	switch {
+	case strings.Contains(term, "alacritty"),
+		strings.Contains(term, "contour"),
+		strings.Contains(term, "foot"),
+		strings.Contains(term, "ghostty"),
+		strings.Contains(term, "kitty"),
+		strings.Contains(term, "rio"),
+		strings.Contains(term, "st"),
+		strings.Contains(term, "wezterm"):
 		return TrueColor
-	case "xterm":
-		if len(parts) > 1 {
-			switch parts[1] {
-			case "ghostty", "kitty":
-				// These terminals can be defined as xterm-TERMNAME
-				return TrueColor
-			}
-		}
-	case "tmux", "screen":
+	case strings.HasPrefix(term, "tmux"), strings.HasPrefix(term, "screen"):
 		if p < ANSI256 {
 			p = ANSI256
 		}
+	case strings.HasPrefix(term, "xterm"):
+		if p < ANSI {
+			p = ANSI
+		}
 	}
 
 	if isCloudShell, _ := strconv.ParseBool(env.get("GOOGLE_CLOUD_SHELL")); isCloudShell {
diff --git a/vendor/github.com/charmbracelet/colorprofile/profile.go b/vendor/github.com/charmbracelet/colorprofile/profile.go
index 2bcb37c6e..4eb77ca43 100644
--- a/vendor/github.com/charmbracelet/colorprofile/profile.go
+++ b/vendor/github.com/charmbracelet/colorprofile/profile.go
@@ -2,10 +2,9 @@ package colorprofile
 
 import (
 	"image/color"
-	"math"
+	"sync"
 
 	"github.com/charmbracelet/x/ansi"
-	"github.com/lucasb-eyer/go-colorful"
 )
 
 // Profile is a color profile: NoTTY, Ascii, ANSI, ANSI256, or TrueColor.
@@ -41,359 +40,61 @@ func (p Profile) String() string {
 	return "Unknown"
 }
 
+var (
+	cache = map[Profile]map[color.Color]color.Color{
+		ANSI256: {},
+		ANSI:    {},
+	}
+	mu sync.RWMutex
+)
+
 // Convert transforms a given Color to a Color supported within the Profile.
-func (p Profile) Convert(c color.Color) color.Color {
+func (p Profile) Convert(c color.Color) (cc color.Color) {
 	if p <= Ascii {
 		return nil
 	}
-
-	switch c := c.(type) {
-	case ansi.BasicColor:
-		return c
-
-	case ansi.ExtendedColor:
-		if p == ANSI {
-			return ansi256ToANSIColor(c)
-		}
+	if p == TrueColor {
+		// TrueColor is a passthrough.
 		return c
+	}
 
-	case ansi.TrueColor, color.Color:
-		h, ok := colorful.MakeColor(c)
-		if !ok {
-			return nil
+	// Do we have a cached color for this profile and color?
+	mu.RLock()
+	if c != nil && cache[p] != nil {
+		if cc, ok := cache[p][c]; ok {
+			mu.RUnlock()
+			return cc
 		}
-		if p != TrueColor {
-			ac := hexToANSI256Color(h)
-			if p == ANSI {
-				return ansi256ToANSIColor(ac)
+	}
+	mu.RUnlock()
+
+	// If we don't have a cached color, we need to convert it and cache it.
+	defer func() {
+		mu.Lock()
+		if cc != nil && cache[p] != nil {
+			if _, ok := cache[p][c]; !ok {
+				cache[p][c] = cc
 			}
-			return ac
 		}
-		return c
-	}
+		mu.Unlock()
+	}()
 
-	return c
-}
+	switch c.(type) {
+	case ansi.BasicColor:
+		return c
 
-func hexToANSI256Color(c colorful.Color) ansi.ExtendedColor {
-	v2ci := func(v float64) int {
-		if v < 48 {
-			return 0
-		}
-		if v < 115 {
-			return 1
+	case ansi.IndexedColor:
+		if p == ANSI {
+			return ansi.Convert16(c)
 		}
-		return int((v - 35) / 40)
-	}
-
-	// Calculate the nearest 0-based color index at 16..231
-	r := v2ci(c.R * 255.0) // 0..5 each
-	g := v2ci(c.G * 255.0)
-	b := v2ci(c.B * 255.0)
-	ci := 36*r + 6*g + b /* 0..215 */
-
-	// Calculate the represented colors back from the index
-	i2cv := [6]int{0, 0x5f, 0x87, 0xaf, 0xd7, 0xff}
-	cr := i2cv[r] // r/g/b, 0..255 each
-	cg := i2cv[g]
-	cb := i2cv[b]
-
-	// Calculate the nearest 0-based gray index at 232..255
-	var grayIdx int
-	average := (cr + cg + cb) / 3
-	if average > 238 {
-		grayIdx = 23
-	} else {
-		grayIdx = (average - 3) / 10 // 0..23
-	}
-	gv := 8 + 10*grayIdx // same value for r/g/b, 0..255
-
-	// Return the one which is nearer to the original input rgb value
-	c2 := colorful.Color{R: float64(cr) / 255.0, G: float64(cg) / 255.0, B: float64(cb) / 255.0}
-	g2 := colorful.Color{R: float64(gv) / 255.0, G: float64(gv) / 255.0, B: float64(gv) / 255.0}
-	colorDist := c.DistanceHSLuv(c2)
-	grayDist := c.DistanceHSLuv(g2)
-
-	if colorDist <= grayDist {
-		return ansi.ExtendedColor(16 + ci) //nolint:gosec
-	}
-	return ansi.ExtendedColor(232 + grayIdx) //nolint:gosec
-}
-
-func ansi256ToANSIColor(c ansi.ExtendedColor) ansi.BasicColor {
-	var r int
-	md := math.MaxFloat64
-
-	h, _ := colorful.Hex(ansiHex[c])
-	for i := 0; i <= 15; i++ {
-		hb, _ := colorful.Hex(ansiHex[i])
-		d := h.DistanceHSLuv(hb)
+		return c
 
-		if d < md {
-			md = d
-			r = i
+	default:
+		if p == ANSI256 {
+			return ansi.Convert256(c)
+		} else if p == ANSI {
+			return ansi.Convert16(c)
 		}
+		return c
 	}
-
-	return ansi.BasicColor(r) //nolint:gosec
-}
-
-// RGB values of ANSI colors (0-255).
-var ansiHex = []string{
-	"#000000",
-	"#800000",
-	"#008000",
-	"#808000",
-	"#000080",
-	"#800080",
-	"#008080",
-	"#c0c0c0",
-	"#808080",
-	"#ff0000",
-	"#00ff00",
-	"#ffff00",
-	"#0000ff",
-	"#ff00ff",
-	"#00ffff",
-	"#ffffff",
-	"#000000",
-	"#00005f",
-	"#000087",
-	"#0000af",
-	"#0000d7",
-	"#0000ff",
-	"#005f00",
-	"#005f5f",
-	"#005f87",
-	"#005faf",
-	"#005fd7",
-	"#005fff",
-	"#008700",
-	"#00875f",
-	"#008787",
-	"#0087af",
-	"#0087d7",
-	"#0087ff",
-	"#00af00",
-	"#00af5f",
-	"#00af87",
-	"#00afaf",
-	"#00afd7",
-	"#00afff",
-	"#00d700",
-	"#00d75f",
-	"#00d787",
-	"#00d7af",
-	"#00d7d7",
-	"#00d7ff",
-	"#00ff00",
-	"#00ff5f",
-	"#00ff87",
-	"#00ffaf",
-	"#00ffd7",
-	"#00ffff",
-	"#5f0000",
-	"#5f005f",
-	"#5f0087",
-	"#5f00af",
-	"#5f00d7",
-	"#5f00ff",
-	"#5f5f00",
-	"#5f5f5f",
-	"#5f5f87",
-	"#5f5faf",
-	"#5f5fd7",
-	"#5f5fff",
-	"#5f8700",
-	"#5f875f",
-	"#5f8787",
-	"#5f87af",
-	"#5f87d7",
-	"#5f87ff",
-	"#5faf00",
-	"#5faf5f",
-	"#5faf87",
-	"#5fafaf",
-	"#5fafd7",
-	"#5fafff",
-	"#5fd700",
-	"#5fd75f",
-	"#5fd787",
-	"#5fd7af",
-	"#5fd7d7",
-	"#5fd7ff",
-	"#5fff00",
-	"#5fff5f",
-	"#5fff87",
-	"#5fffaf",
-	"#5fffd7",
-	"#5fffff",
-	"#870000",
-	"#87005f",
-	"#870087",
-	"#8700af",
-	"#8700d7",
-	"#8700ff",
-	"#875f00",
-	"#875f5f",
-	"#875f87",
-	"#875faf",
-	"#875fd7",
-	"#875fff",
-	"#878700",
-	"#87875f",
-	"#878787",
-	"#8787af",
-	"#8787d7",
-	"#8787ff",
-	"#87af00",
-	"#87af5f",
-	"#87af87",
-	"#87afaf",
-	"#87afd7",
-	"#87afff",
-	"#87d700",
-	"#87d75f",
-	"#87d787",
-	"#87d7af",
-	"#87d7d7",
-	"#87d7ff",
-	"#87ff00",
-	"#87ff5f",
-	"#87ff87",
-	"#87ffaf",
-	"#87ffd7",
-	"#87ffff",
-	"#af0000",
-	"#af005f",
-	"#af0087",
-	"#af00af",
-	"#af00d7",
-	"#af00ff",
-	"#af5f00",
-	"#af5f5f",
-	"#af5f87",
-	"#af5faf",
-	"#af5fd7",
-	"#af5fff",
-	"#af8700",
-	"#af875f",
-	"#af8787",
-	"#af87af",
-	"#af87d7",
-	"#af87ff",
-	"#afaf00",
-	"#afaf5f",
-	"#afaf87",
-	"#afafaf",
-	"#afafd7",
-	"#afafff",
-	"#afd700",
-	"#afd75f",
-	"#afd787",
-	"#afd7af",
-	"#afd7d7",
-	"#afd7ff",
-	"#afff00",
-	"#afff5f",
-	"#afff87",
-	"#afffaf",
-	"#afffd7",
-	"#afffff",
-	"#d70000",
-	"#d7005f",
-	"#d70087",
-	"#d700af",
-	"#d700d7",
-	"#d700ff",
-	"#d75f00",
-	"#d75f5f",
-	"#d75f87",
-	"#d75faf",
-	"#d75fd7",
-	"#d75fff",
-	"#d78700",
-	"#d7875f",
-	"#d78787",
-	"#d787af",
-	"#d787d7",
-	"#d787ff",
-	"#d7af00",
-	"#d7af5f",
-	"#d7af87",
-	"#d7afaf",
-	"#d7afd7",
-	"#d7afff",
-	"#d7d700",
-	"#d7d75f",
-	"#d7d787",
-	"#d7d7af",
-	"#d7d7d7",
-	"#d7d7ff",
-	"#d7ff00",
-	"#d7ff5f",
-	"#d7ff87",
-	"#d7ffaf",
-	"#d7ffd7",
-	"#d7ffff",
-	"#ff0000",
-	"#ff005f",
-	"#ff0087",
-	"#ff00af",
-	"#ff00d7",
-	"#ff00ff",
-	"#ff5f00",
-	"#ff5f5f",
-	"#ff5f87",
-	"#ff5faf",
-	"#ff5fd7",
-	"#ff5fff",
-	"#ff8700",
-	"#ff875f",
-	"#ff8787",
-	"#ff87af",
-	"#ff87d7",
-	"#ff87ff",
-	"#ffaf00",
-	"#ffaf5f",
-	"#ffaf87",
-	"#ffafaf",
-	"#ffafd7",
-	"#ffafff",
-	"#ffd700",
-	"#ffd75f",
-	"#ffd787",
-	"#ffd7af",
-	"#ffd7d7",
-	"#ffd7ff",
-	"#ffff00",
-	"#ffff5f",
-	"#ffff87",
-	"#ffffaf",
-	"#ffffd7",
-	"#ffffff",
-	"#080808",
-	"#121212",
-	"#1c1c1c",
-	"#262626",
-	"#303030",
-	"#3a3a3a",
-	"#444444",
-	"#4e4e4e",
-	"#585858",
-	"#626262",
-	"#6c6c6c",
-	"#767676",
-	"#808080",
-	"#8a8a8a",
-	"#949494",
-	"#9e9e9e",
-	"#a8a8a8",
-	"#b2b2b2",
-	"#bcbcbc",
-	"#c6c6c6",
-	"#d0d0d0",
-	"#dadada",
-	"#e4e4e4",
-	"#eeeeee",
 }
diff --git a/vendor/github.com/charmbracelet/x/ansi/ansi.go b/vendor/github.com/charmbracelet/x/ansi/ansi.go
index 48d873c3f..d5a2f2519 100644
--- a/vendor/github.com/charmbracelet/x/ansi/ansi.go
+++ b/vendor/github.com/charmbracelet/x/ansi/ansi.go
@@ -7,5 +7,5 @@ import "io"
 //
 // This is a syntactic sugar over [io.WriteString].
 func Execute(w io.Writer, s string) (int, error) {
-	return io.WriteString(w, s)
+	return io.WriteString(w, s) //nolint:wrapcheck
 }
diff --git a/vendor/github.com/charmbracelet/x/ansi/background.go b/vendor/github.com/charmbracelet/x/ansi/background.go
index 2383cf09f..46f821425 100644
--- a/vendor/github.com/charmbracelet/x/ansi/background.go
+++ b/vendor/github.com/charmbracelet/x/ansi/background.go
@@ -3,63 +3,93 @@ package ansi
 import (
 	"fmt"
 	"image/color"
+
+	"github.com/lucasb-eyer/go-colorful"
 )
 
-// Colorizer is a [color.Color] interface that can be formatted as a string.
-type Colorizer interface {
-	color.Color
-	fmt.Stringer
-}
+// HexColor is a [color.Color] that can be formatted as a hex string.
+type HexColor string
 
-// HexColorizer is a [color.Color] that can be formatted as a hex string.
-type HexColorizer struct{ color.Color }
+// RGBA returns the RGBA values of the color.
+func (h HexColor) RGBA() (r, g, b, a uint32) {
+	hex := h.color()
+	if hex == nil {
+		return 0, 0, 0, 0
+	}
+	return hex.RGBA()
+}
 
-var _ Colorizer = HexColorizer{}
+// Hex returns the hex representation of the color. If the color is invalid, it
+// returns an empty string.
+func (h HexColor) Hex() string {
+	hex := h.color()
+	if hex == nil {
+		return ""
+	}
+	return hex.Hex()
+}
 
 // String returns the color as a hex string. If the color is nil, an empty
 // string is returned.
-func (h HexColorizer) String() string {
-	if h.Color == nil {
-		return ""
+func (h HexColor) String() string {
+	return h.Hex()
+}
+
+// color returns the underlying color of the HexColor.
+func (h HexColor) color() *colorful.Color {
+	hex, err := colorful.Hex(string(h))
+	if err != nil {
+		return nil
 	}
-	r, g, b, _ := h.RGBA()
-	// Get the lower 8 bits
-	r &= 0xff
-	g &= 0xff
-	b &= 0xff
-	return fmt.Sprintf("#%02x%02x%02x", uint8(r), uint8(g), uint8(b)) //nolint:gosec
+	return &hex
 }
 
-// XRGBColorizer is a [color.Color] that can be formatted as an XParseColor
+// XRGBColor is a [color.Color] that can be formatted as an XParseColor
 // rgb: string.
 //
 // See: https://linux.die.net/man/3/xparsecolor
-type XRGBColorizer struct{ color.Color }
+type XRGBColor struct {
+	color.Color
+}
 
-var _ Colorizer = XRGBColorizer{}
+// RGBA returns the RGBA values of the color.
+func (x XRGBColor) RGBA() (r, g, b, a uint32) {
+	if x.Color == nil {
+		return 0, 0, 0, 0
+	}
+	return x.Color.RGBA()
+}
 
 // String returns the color as an XParseColor rgb: string. If the color is nil,
 // an empty string is returned.
-func (x XRGBColorizer) String() string {
+func (x XRGBColor) String() string {
 	if x.Color == nil {
 		return ""
 	}
-	r, g, b, _ := x.RGBA()
+	r, g, b, _ := x.Color.RGBA()
 	// Get the lower 8 bits
 	return fmt.Sprintf("rgb:%04x/%04x/%04x", r, g, b)
 }
 
-// XRGBAColorizer is a [color.Color] that can be formatted as an XParseColor
+// XRGBAColor is a [color.Color] that can be formatted as an XParseColor
 // rgba: string.
 //
 // See: https://linux.die.net/man/3/xparsecolor
-type XRGBAColorizer struct{ color.Color }
+type XRGBAColor struct {
+	color.Color
+}
 
-var _ Colorizer = XRGBAColorizer{}
+// RGBA returns the RGBA values of the color.
+func (x XRGBAColor) RGBA() (r, g, b, a uint32) {
+	if x.Color == nil {
+		return 0, 0, 0, 0
+	}
+	return x.Color.RGBA()
+}
 
 // String returns the color as an XParseColor rgba: string. If the color is nil,
 // an empty string is returned.
-func (x XRGBAColorizer) String() string {
+func (x XRGBAColor) String() string {
 	if x.Color == nil {
 		return ""
 	}
@@ -74,19 +104,12 @@ func (x XRGBAColorizer) String() string {
 //	OSC 10 ; color ST
 //	OSC 10 ; color BEL
 //
-// Where color is the encoded color number.
+// Where color is the encoded color number. Most terminals support hex,
+// XParseColor rgb: and rgba: strings. You could use [HexColor], [XRGBColor],
+// or [XRGBAColor] to format the color.
 //
 // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
-func SetForegroundColor(c color.Color) string {
-	var s string
-	switch c := c.(type) {
-	case Colorizer:
-		s = c.String()
-	case fmt.Stringer:
-		s = c.String()
-	default:
-		s = HexColorizer{c}.String()
-	}
+func SetForegroundColor(s string) string {
 	return "\x1b]10;" + s + "\x07"
 }
 
@@ -108,19 +131,12 @@ const ResetForegroundColor = "\x1b]110\x07"
 //	OSC 11 ; color ST
 //	OSC 11 ; color BEL
 //
-// Where color is the encoded color number.
+// Where color is the encoded color number. Most terminals support hex,
+// XParseColor rgb: and rgba: strings. You could use [HexColor], [XRGBColor],
+// or [XRGBAColor] to format the color.
 //
 // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
-func SetBackgroundColor(c color.Color) string {
-	var s string
-	switch c := c.(type) {
-	case Colorizer:
-		s = c.String()
-	case fmt.Stringer:
-		s = c.String()
-	default:
-		s = HexColorizer{c}.String()
-	}
+func SetBackgroundColor(s string) string {
 	return "\x1b]11;" + s + "\x07"
 }
 
@@ -141,19 +157,12 @@ const ResetBackgroundColor = "\x1b]111\x07"
 //	OSC 12 ; color ST
 //	OSC 12 ; color BEL
 //
-// Where color is the encoded color number.
+// Where color is the encoded color number. Most terminals support hex,
+// XParseColor rgb: and rgba: strings. You could use [HexColor], [XRGBColor],
+// or [XRGBAColor] to format the color.
 //
 // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
-func SetCursorColor(c color.Color) string {
-	var s string
-	switch c := c.(type) {
-	case Colorizer:
-		s = c.String()
-	case fmt.Stringer:
-		s = c.String()
-	default:
-		s = HexColorizer{c}.String()
-	}
+func SetCursorColor(s string) string {
 	return "\x1b]12;" + s + "\x07"
 }
 
diff --git a/vendor/github.com/charmbracelet/x/ansi/charset.go b/vendor/github.com/charmbracelet/x/ansi/charset.go
index 50fff51fc..02edfe7ad 100644
--- a/vendor/github.com/charmbracelet/x/ansi/charset.go
+++ b/vendor/github.com/charmbracelet/x/ansi/charset.go
@@ -39,17 +39,17 @@ func SCS(gset byte, charset byte) string {
 	return SelectCharacterSet(gset, charset)
 }
 
-// Locking Shift 1 Right (LS1R) shifts G1 into GR character set.
+// LS1R (Locking Shift 1 Right) shifts G1 into GR character set.
 const LS1R = "\x1b~"
 
-// Locking Shift 2 (LS2) shifts G2 into GL character set.
+// LS2 (Locking Shift 2) shifts G2 into GL character set.
 const LS2 = "\x1bn"
 
-// Locking Shift 2 Right (LS2R) shifts G2 into GR character set.
+// LS2R (Locking Shift 2 Right) shifts G2 into GR character set.
 const LS2R = "\x1b}"
 
-// Locking Shift 3 (LS3) shifts G3 into GL character set.
+// LS3 (Locking Shift 3) shifts G3 into GL character set.
 const LS3 = "\x1bo"
 
-// Locking Shift 3 Right (LS3R) shifts G3 into GR character set.
+// LS3R (Locking Shift 3 Right) shifts G3 into GR character set.
 const LS3R = "\x1b|"
diff --git a/vendor/github.com/charmbracelet/x/ansi/ctrl.go b/vendor/github.com/charmbracelet/x/ansi/ctrl.go
index 8ca744cf2..64bcf1136 100644
--- a/vendor/github.com/charmbracelet/x/ansi/ctrl.go
+++ b/vendor/github.com/charmbracelet/x/ansi/ctrl.go
@@ -38,6 +38,25 @@ const RequestXTVersion = RequestNameVersion
 // If no attributes are given, or if the attribute is 0, this function returns
 // the request sequence. Otherwise, it returns the response sequence.
 //
+// Common attributes include:
+//   - 1	132 columns
+//   - 2	Printer port
+//   - 4	Sixel
+//   - 6	Selective erase
+//   - 7	Soft character set (DRCS)
+//   - 8	User-defined keys (UDKs)
+//   - 9	National replacement character sets (NRCS) (International terminal only)
+//   - 12	Yugoslavian (SCS)
+//   - 15	Technical character set
+//   - 18	Windowing capability
+//   - 21	Horizontal scrolling
+//   - 23	Greek
+//   - 24	Turkish
+//   - 42	ISO Latin-2 character set
+//   - 44	PCTerm
+//   - 45	Soft key map
+//   - 46	ASCII emulation
+//
 // See https://vt100.net/docs/vt510-rm/DA1.html
 func PrimaryDeviceAttributes(attrs ...int) string {
 	if len(attrs) == 0 {
diff --git a/vendor/github.com/charmbracelet/x/ansi/cursor.go b/vendor/github.com/charmbracelet/x/ansi/cursor.go
index 2a74c483c..4adf68964 100644
--- a/vendor/github.com/charmbracelet/x/ansi/cursor.go
+++ b/vendor/github.com/charmbracelet/x/ansi/cursor.go
@@ -358,8 +358,8 @@ func CHT(n int) string {
 	return CursorHorizontalForwardTab(n)
 }
 
-// EraseCharacter (ECH) returns a sequence for erasing n characters and moving
-// the cursor to the right. This doesn't affect other cell attributes.
+// EraseCharacter (ECH) returns a sequence for erasing n characters from the
+// screen. This doesn't affect other cell attributes.
 //
 // Default is 1.
 //
@@ -591,7 +591,7 @@ const ReverseIndex = "\x1bM"
 //
 // Default is 1.
 //
-//	CSI n `
+//	CSI n \`
 //
 // See: https://vt100.net/docs/vt510-rm/HPA.html
 func HorizontalPositionAbsolute(col int) string {
diff --git a/vendor/github.com/charmbracelet/x/ansi/graphics.go b/vendor/github.com/charmbracelet/x/ansi/graphics.go
index b5b1dc82f..d4a693b72 100644
--- a/vendor/github.com/charmbracelet/x/ansi/graphics.go
+++ b/vendor/github.com/charmbracelet/x/ansi/graphics.go
@@ -2,16 +2,8 @@ package ansi
 
 import (
 	"bytes"
-	"encoding/base64"
-	"errors"
-	"fmt"
-	"image"
-	"io"
-	"os"
 	"strconv"
 	"strings"
-
-	"github.com/charmbracelet/x/ansi/kitty"
 )
 
 // SixelGraphics returns a sequence that encodes the given sixel image payload to
@@ -68,170 +60,3 @@ func KittyGraphics(payload []byte, opts ...string) string {
 	buf.WriteString("\x1b\\")
 	return buf.String()
 }
-
-var (
-	// KittyGraphicsTempDir is the directory where temporary files are stored.
-	// This is used in [WriteKittyGraphics] along with [os.CreateTemp].
-	KittyGraphicsTempDir = ""
-
-	// KittyGraphicsTempPattern is the pattern used to create temporary files.
-	// This is used in [WriteKittyGraphics] along with [os.CreateTemp].
-	// The Kitty Graphics protocol requires the file path to contain the
-	// substring "tty-graphics-protocol".
-	KittyGraphicsTempPattern = "tty-graphics-protocol-*"
-)
-
-// EncodeKittyGraphics writes an image using the Kitty Graphics protocol with
-// the given options to w. It chunks the written data if o.Chunk is true.
-//
-// You can omit m and use nil when rendering an image from a file. In this
-// case, you must provide a file path in o.File and use o.Transmission =
-// [kitty.File]. You can also use o.Transmission = [kitty.TempFile] to write
-// the image to a temporary file. In that case, the file path is ignored, and
-// the image is written to a temporary file that is automatically deleted by
-// the terminal.
-//
-// See https://sw.kovidgoyal.net/kitty/graphics-protocol/
-func EncodeKittyGraphics(w io.Writer, m image.Image, o *kitty.Options) error {
-	if o == nil {
-		o = &kitty.Options{}
-	}
-
-	if o.Transmission == 0 && len(o.File) != 0 {
-		o.Transmission = kitty.File
-	}
-
-	var data bytes.Buffer // the data to be encoded into base64
-	e := &kitty.Encoder{
-		Compress: o.Compression == kitty.Zlib,
-		Format:   o.Format,
-	}
-
-	switch o.Transmission {
-	case kitty.Direct:
-		if err := e.Encode(&data, m); err != nil {
-			return fmt.Errorf("failed to encode direct image: %w", err)
-		}
-
-	case kitty.SharedMemory:
-		// TODO: Implement shared memory
-		return fmt.Errorf("shared memory transmission is not yet implemented")
-
-	case kitty.File:
-		if len(o.File) == 0 {
-			return kitty.ErrMissingFile
-		}
-
-		f, err := os.Open(o.File)
-		if err != nil {
-			return fmt.Errorf("failed to open file: %w", err)
-		}
-
-		defer f.Close() //nolint:errcheck
-
-		stat, err := f.Stat()
-		if err != nil {
-			return fmt.Errorf("failed to get file info: %w", err)
-		}
-
-		mode := stat.Mode()
-		if !mode.IsRegular() {
-			return fmt.Errorf("file is not a regular file")
-		}
-
-		// Write the file path to the buffer
-		if _, err := data.WriteString(f.Name()); err != nil {
-			return fmt.Errorf("failed to write file path to buffer: %w", err)
-		}
-
-	case kitty.TempFile:
-		f, err := os.CreateTemp(KittyGraphicsTempDir, KittyGraphicsTempPattern)
-		if err != nil {
-			return fmt.Errorf("failed to create file: %w", err)
-		}
-
-		defer f.Close() //nolint:errcheck
-
-		if err := e.Encode(f, m); err != nil {
-			return fmt.Errorf("failed to encode image to file: %w", err)
-		}
-
-		// Write the file path to the buffer
-		if _, err := data.WriteString(f.Name()); err != nil {
-			return fmt.Errorf("failed to write file path to buffer: %w", err)
-		}
-	}
-
-	// Encode image to base64
-	var payload bytes.Buffer // the base64 encoded image to be written to w
-	b64 := base64.NewEncoder(base64.StdEncoding, &payload)
-	if _, err := data.WriteTo(b64); err != nil {
-		return fmt.Errorf("failed to write base64 encoded image to payload: %w", err)
-	}
-	if err := b64.Close(); err != nil {
-		return err
-	}
-
-	// If not chunking, write all at once
-	if !o.Chunk {
-		_, err := io.WriteString(w, KittyGraphics(payload.Bytes(), o.Options()...))
-		return err
-	}
-
-	// Write in chunks
-	var (
-		err error
-		n   int
-	)
-	chunk := make([]byte, kitty.MaxChunkSize)
-	isFirstChunk := true
-
-	for {
-		// Stop if we read less than the chunk size [kitty.MaxChunkSize].
-		n, err = io.ReadFull(&payload, chunk)
-		if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) {
-			break
-		}
-		if err != nil {
-			return fmt.Errorf("failed to read chunk: %w", err)
-		}
-
-		opts := buildChunkOptions(o, isFirstChunk, false)
-		if _, err := io.WriteString(w, KittyGraphics(chunk[:n], opts...)); err != nil {
-			return err
-		}
-
-		isFirstChunk = false
-	}
-
-	// Write the last chunk
-	opts := buildChunkOptions(o, isFirstChunk, true)
-	_, err = io.WriteString(w, KittyGraphics(chunk[:n], opts...))
-	return err
-}
-
-// buildChunkOptions creates the options slice for a chunk
-func buildChunkOptions(o *kitty.Options, isFirstChunk, isLastChunk bool) []string {
-	var opts []string
-	if isFirstChunk {
-		opts = o.Options()
-	} else {
-		// These options are allowed in subsequent chunks
-		if o.Quite > 0 {
-			opts = append(opts, fmt.Sprintf("q=%d", o.Quite))
-		}
-		if o.Action == kitty.Frame {
-			opts = append(opts, "a=f")
-		}
-	}
-
-	if !isFirstChunk || !isLastChunk {
-		// We don't need to encode the (m=) option when we only have one chunk.
-		if isLastChunk {
-			opts = append(opts, "m=0")
-		} else {
-			opts = append(opts, "m=1")
-		}
-	}
-	return opts
-}
diff --git a/vendor/github.com/charmbracelet/x/ansi/inband.go b/vendor/github.com/charmbracelet/x/ansi/inband.go
new file mode 100644
index 000000000..cf29b2485
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/ansi/inband.go
@@ -0,0 +1,24 @@
+package ansi
+
+import "fmt"
+
+// InBandResize encodes an in-band terminal resize event sequence.
+//
+//	CSI 48 ; height_cells ; widht_cells ; height_pixels ; width_pixels t
+//
+// See https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83
+func InBandResize(heightCells, widthCells, heightPixels, widthPixels int) string {
+	if heightCells < 0 {
+		heightCells = 0
+	}
+	if widthCells < 0 {
+		widthCells = 0
+	}
+	if heightPixels < 0 {
+		heightPixels = 0
+	}
+	if widthPixels < 0 {
+		widthPixels = 0
+	}
+	return fmt.Sprintf("\x1b[48;%d;%d;%d;%dt", heightCells, widthCells, heightPixels, widthPixels)
+}
diff --git a/vendor/github.com/charmbracelet/x/ansi/kitty/decoder.go b/vendor/github.com/charmbracelet/x/ansi/kitty/decoder.go
deleted file mode 100644
index fbd08441f..000000000
--- a/vendor/github.com/charmbracelet/x/ansi/kitty/decoder.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package kitty
-
-import (
-	"compress/zlib"
-	"fmt"
-	"image"
-	"image/color"
-	"image/png"
-	"io"
-)
-
-// Decoder is a decoder for the Kitty graphics protocol. It supports decoding
-// images in the 24-bit [RGB], 32-bit [RGBA], and [PNG] formats. It can also
-// decompress data using zlib.
-// The default format is 32-bit [RGBA].
-type Decoder struct {
-	// Uses zlib decompression.
-	Decompress bool
-
-	// Can be one of [RGB], [RGBA], or [PNG].
-	Format int
-
-	// Width of the image in pixels. This can be omitted if the image is [PNG]
-	// formatted.
-	Width int
-
-	// Height of the image in pixels. This can be omitted if the image is [PNG]
-	// formatted.
-	Height int
-}
-
-// Decode decodes the image data from r in the specified format.
-func (d *Decoder) Decode(r io.Reader) (image.Image, error) {
-	if d.Decompress {
-		zr, err := zlib.NewReader(r)
-		if err != nil {
-			return nil, fmt.Errorf("failed to create zlib reader: %w", err)
-		}
-
-		defer zr.Close() //nolint:errcheck
-		r = zr
-	}
-
-	if d.Format == 0 {
-		d.Format = RGBA
-	}
-
-	switch d.Format {
-	case RGBA, RGB:
-		return d.decodeRGBA(r, d.Format == RGBA)
-
-	case PNG:
-		return png.Decode(r)
-
-	default:
-		return nil, fmt.Errorf("unsupported format: %d", d.Format)
-	}
-}
-
-// decodeRGBA decodes the image data in 32-bit RGBA or 24-bit RGB formats.
-func (d *Decoder) decodeRGBA(r io.Reader, alpha bool) (image.Image, error) {
-	m := image.NewRGBA(image.Rect(0, 0, d.Width, d.Height))
-
-	var buf []byte
-	if alpha {
-		buf = make([]byte, 4)
-	} else {
-		buf = make([]byte, 3)
-	}
-
-	for y := 0; y < d.Height; y++ {
-		for x := 0; x < d.Width; x++ {
-			if _, err := io.ReadFull(r, buf[:]); err != nil {
-				return nil, fmt.Errorf("failed to read pixel data: %w", err)
-			}
-			if alpha {
-				m.SetRGBA(x, y, color.RGBA{buf[0], buf[1], buf[2], buf[3]})
-			} else {
-				m.SetRGBA(x, y, color.RGBA{buf[0], buf[1], buf[2], 0xff})
-			}
-		}
-	}
-
-	return m, nil
-}
diff --git a/vendor/github.com/charmbracelet/x/ansi/kitty/encoder.go b/vendor/github.com/charmbracelet/x/ansi/kitty/encoder.go
deleted file mode 100644
index f668b9e31..000000000
--- a/vendor/github.com/charmbracelet/x/ansi/kitty/encoder.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package kitty
-
-import (
-	"compress/zlib"
-	"fmt"
-	"image"
-	"image/png"
-	"io"
-)
-
-// Encoder is an encoder for the Kitty graphics protocol. It supports encoding
-// images in the 24-bit [RGB], 32-bit [RGBA], and [PNG] formats, and
-// compressing the data using zlib.
-// The default format is 32-bit [RGBA].
-type Encoder struct {
-	// Uses zlib compression.
-	Compress bool
-
-	// Can be one of [RGBA], [RGB], or [PNG].
-	Format int
-}
-
-// Encode encodes the image data in the specified format and writes it to w.
-func (e *Encoder) Encode(w io.Writer, m image.Image) error {
-	if m == nil {
-		return nil
-	}
-
-	if e.Compress {
-		zw := zlib.NewWriter(w)
-		defer zw.Close() //nolint:errcheck
-		w = zw
-	}
-
-	if e.Format == 0 {
-		e.Format = RGBA
-	}
-
-	switch e.Format {
-	case RGBA, RGB:
-		bounds := m.Bounds()
-		for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
-			for x := bounds.Min.X; x < bounds.Max.X; x++ {
-				r, g, b, a := m.At(x, y).RGBA()
-				switch e.Format {
-				case RGBA:
-					w.Write([]byte{byte(r >> 8), byte(g >> 8), byte(b >> 8), byte(a >> 8)}) //nolint:errcheck
-				case RGB:
-					w.Write([]byte{byte(r >> 8), byte(g >> 8), byte(b >> 8)}) //nolint:errcheck
-				}
-			}
-		}
-
-	case PNG:
-		if err := png.Encode(w, m); err != nil {
-			return fmt.Errorf("failed to encode PNG: %w", err)
-		}
-
-	default:
-		return fmt.Errorf("unsupported format: %d", e.Format)
-	}
-
-	return nil
-}
diff --git a/vendor/github.com/charmbracelet/x/ansi/kitty/graphics.go b/vendor/github.com/charmbracelet/x/ansi/kitty/graphics.go
deleted file mode 100644
index 490e7a8a3..000000000
--- a/vendor/github.com/charmbracelet/x/ansi/kitty/graphics.go
+++ /dev/null
@@ -1,414 +0,0 @@
-package kitty
-
-import "errors"
-
-// ErrMissingFile is returned when the file path is missing.
-var ErrMissingFile = errors.New("missing file path")
-
-// MaxChunkSize is the maximum chunk size for the image data.
-const MaxChunkSize = 1024 * 4
-
-// Placeholder is a special Unicode character that can be used as a placeholder
-// for an image.
-const Placeholder = '\U0010EEEE'
-
-// Graphics image format.
-const (
-	// 32-bit RGBA format.
-	RGBA = 32
-
-	// 24-bit RGB format.
-	RGB = 24
-
-	// PNG format.
-	PNG = 100
-)
-
-// Compression types.
-const (
-	Zlib = 'z'
-)
-
-// Transmission types.
-const (
-	// The data transmitted directly in the escape sequence.
-	Direct = 'd'
-
-	// The data transmitted in a regular file.
-	File = 'f'
-
-	// A temporary file is used and deleted after transmission.
-	TempFile = 't'
-
-	// A shared memory object.
-	// For POSIX see https://pubs.opengroup.org/onlinepubs/9699919799/functions/shm_open.html
-	// For Windows see https://docs.microsoft.com/en-us/windows/win32/memory/creating-named-shared-memory
-	SharedMemory = 's'
-)
-
-// Action types.
-const (
-	// Transmit image data.
-	Transmit = 't'
-	// TransmitAndPut transmit image data and display (put) it.
-	TransmitAndPut = 'T'
-	// Query terminal for image info.
-	Query = 'q'
-	// Put (display) previously transmitted image.
-	Put = 'p'
-	// Delete image.
-	Delete = 'd'
-	// Frame transmits data for animation frames.
-	Frame = 'f'
-	// Animate controls animation.
-	Animate = 'a'
-	// Compose composes animation frames.
-	Compose = 'c'
-)
-
-// Delete types.
-const (
-	// Delete all placements visible on screen
-	DeleteAll = 'a'
-	// Delete all images with the specified id, specified using the i key. If
-	// you specify a p key for the placement id as well, then only the
-	// placement with the specified image id and placement id will be deleted.
-	DeleteID = 'i'
-	// Delete newest image with the specified number, specified using the I
-	// key. If you specify a p key for the placement id as well, then only the
-	// placement with the specified number and placement id will be deleted.
-	DeleteNumber = 'n'
-	// Delete all placements that intersect with the current cursor position.
-	DeleteCursor = 'c'
-	// Delete animation frames.
-	DeleteFrames = 'f'
-	// Delete all placements that intersect a specific cell, the cell is
-	// specified using the x and y keys
-	DeleteCell = 'p'
-	// Delete all placements that intersect a specific cell having a specific
-	// z-index. The cell and z-index is specified using the x, y and z keys.
-	DeleteCellZ = 'q'
-	// Delete all images whose id is greater than or equal to the value of the x
-	// key and less than or equal to the value of the y.
-	DeleteRange = 'r'
-	// Delete all placements that intersect the specified column, specified using
-	// the x key.
-	DeleteColumn = 'x'
-	// Delete all placements that intersect the specified row, specified using
-	// the y key.
-	DeleteRow = 'y'
-	// Delete all placements that have the specified z-index, specified using the
-	// z key.
-	DeleteZ = 'z'
-)
-
-// Diacritic returns the diacritic rune at the specified index. If the index is
-// out of bounds, the first diacritic rune is returned.
-func Diacritic(i int) rune {
-	if i < 0 || i >= len(diacritics) {
-		return diacritics[0]
-	}
-	return diacritics[i]
-}
-
-// From https://sw.kovidgoyal.net/kitty/_downloads/f0a0de9ec8d9ff4456206db8e0814937/rowcolumn-diacritics.txt
-// See https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders for further explanation.
-var diacritics = []rune{
-	'\u0305',
-	'\u030D',
-	'\u030E',
-	'\u0310',
-	'\u0312',
-	'\u033D',
-	'\u033E',
-	'\u033F',
-	'\u0346',
-	'\u034A',
-	'\u034B',
-	'\u034C',
-	'\u0350',
-	'\u0351',
-	'\u0352',
-	'\u0357',
-	'\u035B',
-	'\u0363',
-	'\u0364',
-	'\u0365',
-	'\u0366',
-	'\u0367',
-	'\u0368',
-	'\u0369',
-	'\u036A',
-	'\u036B',
-	'\u036C',
-	'\u036D',
-	'\u036E',
-	'\u036F',
-	'\u0483',
-	'\u0484',
-	'\u0485',
-	'\u0486',
-	'\u0487',
-	'\u0592',
-	'\u0593',
-	'\u0594',
-	'\u0595',
-	'\u0597',
-	'\u0598',
-	'\u0599',
-	'\u059C',
-	'\u059D',
-	'\u059E',
-	'\u059F',
-	'\u05A0',
-	'\u05A1',
-	'\u05A8',
-	'\u05A9',
-	'\u05AB',
-	'\u05AC',
-	'\u05AF',
-	'\u05C4',
-	'\u0610',
-	'\u0611',
-	'\u0612',
-	'\u0613',
-	'\u0614',
-	'\u0615',
-	'\u0616',
-	'\u0617',
-	'\u0657',
-	'\u0658',
-	'\u0659',
-	'\u065A',
-	'\u065B',
-	'\u065D',
-	'\u065E',
-	'\u06D6',
-	'\u06D7',
-	'\u06D8',
-	'\u06D9',
-	'\u06DA',
-	'\u06DB',
-	'\u06DC',
-	'\u06DF',
-	'\u06E0',
-	'\u06E1',
-	'\u06E2',
-	'\u06E4',
-	'\u06E7',
-	'\u06E8',
-	'\u06EB',
-	'\u06EC',
-	'\u0730',
-	'\u0732',
-	'\u0733',
-	'\u0735',
-	'\u0736',
-	'\u073A',
-	'\u073D',
-	'\u073F',
-	'\u0740',
-	'\u0741',
-	'\u0743',
-	'\u0745',
-	'\u0747',
-	'\u0749',
-	'\u074A',
-	'\u07EB',
-	'\u07EC',
-	'\u07ED',
-	'\u07EE',
-	'\u07EF',
-	'\u07F0',
-	'\u07F1',
-	'\u07F3',
-	'\u0816',
-	'\u0817',
-	'\u0818',
-	'\u0819',
-	'\u081B',
-	'\u081C',
-	'\u081D',
-	'\u081E',
-	'\u081F',
-	'\u0820',
-	'\u0821',
-	'\u0822',
-	'\u0823',
-	'\u0825',
-	'\u0826',
-	'\u0827',
-	'\u0829',
-	'\u082A',
-	'\u082B',
-	'\u082C',
-	'\u082D',
-	'\u0951',
-	'\u0953',
-	'\u0954',
-	'\u0F82',
-	'\u0F83',
-	'\u0F86',
-	'\u0F87',
-	'\u135D',
-	'\u135E',
-	'\u135F',
-	'\u17DD',
-	'\u193A',
-	'\u1A17',
-	'\u1A75',
-	'\u1A76',
-	'\u1A77',
-	'\u1A78',
-	'\u1A79',
-	'\u1A7A',
-	'\u1A7B',
-	'\u1A7C',
-	'\u1B6B',
-	'\u1B6D',
-	'\u1B6E',
-	'\u1B6F',
-	'\u1B70',
-	'\u1B71',
-	'\u1B72',
-	'\u1B73',
-	'\u1CD0',
-	'\u1CD1',
-	'\u1CD2',
-	'\u1CDA',
-	'\u1CDB',
-	'\u1CE0',
-	'\u1DC0',
-	'\u1DC1',
-	'\u1DC3',
-	'\u1DC4',
-	'\u1DC5',
-	'\u1DC6',
-	'\u1DC7',
-	'\u1DC8',
-	'\u1DC9',
-	'\u1DCB',
-	'\u1DCC',
-	'\u1DD1',
-	'\u1DD2',
-	'\u1DD3',
-	'\u1DD4',
-	'\u1DD5',
-	'\u1DD6',
-	'\u1DD7',
-	'\u1DD8',
-	'\u1DD9',
-	'\u1DDA',
-	'\u1DDB',
-	'\u1DDC',
-	'\u1DDD',
-	'\u1DDE',
-	'\u1DDF',
-	'\u1DE0',
-	'\u1DE1',
-	'\u1DE2',
-	'\u1DE3',
-	'\u1DE4',
-	'\u1DE5',
-	'\u1DE6',
-	'\u1DFE',
-	'\u20D0',
-	'\u20D1',
-	'\u20D4',
-	'\u20D5',
-	'\u20D6',
-	'\u20D7',
-	'\u20DB',
-	'\u20DC',
-	'\u20E1',
-	'\u20E7',
-	'\u20E9',
-	'\u20F0',
-	'\u2CEF',
-	'\u2CF0',
-	'\u2CF1',
-	'\u2DE0',
-	'\u2DE1',
-	'\u2DE2',
-	'\u2DE3',
-	'\u2DE4',
-	'\u2DE5',
-	'\u2DE6',
-	'\u2DE7',
-	'\u2DE8',
-	'\u2DE9',
-	'\u2DEA',
-	'\u2DEB',
-	'\u2DEC',
-	'\u2DED',
-	'\u2DEE',
-	'\u2DEF',
-	'\u2DF0',
-	'\u2DF1',
-	'\u2DF2',
-	'\u2DF3',
-	'\u2DF4',
-	'\u2DF5',
-	'\u2DF6',
-	'\u2DF7',
-	'\u2DF8',
-	'\u2DF9',
-	'\u2DFA',
-	'\u2DFB',
-	'\u2DFC',
-	'\u2DFD',
-	'\u2DFE',
-	'\u2DFF',
-	'\uA66F',
-	'\uA67C',
-	'\uA67D',
-	'\uA6F0',
-	'\uA6F1',
-	'\uA8E0',
-	'\uA8E1',
-	'\uA8E2',
-	'\uA8E3',
-	'\uA8E4',
-	'\uA8E5',
-	'\uA8E6',
-	'\uA8E7',
-	'\uA8E8',
-	'\uA8E9',
-	'\uA8EA',
-	'\uA8EB',
-	'\uA8EC',
-	'\uA8ED',
-	'\uA8EE',
-	'\uA8EF',
-	'\uA8F0',
-	'\uA8F1',
-	'\uAAB0',
-	'\uAAB2',
-	'\uAAB3',
-	'\uAAB7',
-	'\uAAB8',
-	'\uAABE',
-	'\uAABF',
-	'\uAAC1',
-	'\uFE20',
-	'\uFE21',
-	'\uFE22',
-	'\uFE23',
-	'\uFE24',
-	'\uFE25',
-	'\uFE26',
-	'\U00010A0F',
-	'\U00010A38',
-	'\U0001D185',
-	'\U0001D186',
-	'\U0001D187',
-	'\U0001D188',
-	'\U0001D189',
-	'\U0001D1AA',
-	'\U0001D1AB',
-	'\U0001D1AC',
-	'\U0001D1AD',
-	'\U0001D242',
-	'\U0001D243',
-	'\U0001D244',
-}
diff --git a/vendor/github.com/charmbracelet/x/ansi/kitty/options.go b/vendor/github.com/charmbracelet/x/ansi/kitty/options.go
deleted file mode 100644
index a8d907bdd..000000000
--- a/vendor/github.com/charmbracelet/x/ansi/kitty/options.go
+++ /dev/null
@@ -1,367 +0,0 @@
-package kitty
-
-import (
-	"encoding"
-	"fmt"
-	"strconv"
-	"strings"
-)
-
-var (
-	_ encoding.TextMarshaler   = Options{}
-	_ encoding.TextUnmarshaler = &Options{}
-)
-
-// Options represents a Kitty Graphics Protocol options.
-type Options struct {
-	// Common options.
-
-	// Action (a=t) is the action to be performed on the image. Can be one of
-	// [Transmit], [TransmitDisplay], [Query], [Put], [Delete], [Frame],
-	// [Animate], [Compose].
-	Action byte
-
-	// Quite mode (q=0) is the quiet mode. Can be either zero, one, or two
-	// where zero is the default, 1 suppresses OK responses, and 2 suppresses
-	// both OK and error responses.
-	Quite byte
-
-	// Transmission options.
-
-	// ID (i=) is the image ID. The ID is a unique identifier for the image.
-	// Must be a positive integer up to [math.MaxUint32].
-	ID int
-
-	// PlacementID (p=) is the placement ID. The placement ID is a unique
-	// identifier for the placement of the image. Must be a positive integer up
-	// to [math.MaxUint32].
-	PlacementID int
-
-	// Number (I=0) is the number of images to be transmitted.
-	Number int
-
-	// Format (f=32) is the image format. One of [RGBA], [RGB], [PNG].
-	Format int
-
-	// ImageWidth (s=0) is the transmitted image width.
-	ImageWidth int
-
-	// ImageHeight (v=0) is the transmitted image height.
-	ImageHeight int
-
-	// Compression (o=) is the image compression type. Can be [Zlib] or zero.
-	Compression byte
-
-	// Transmission (t=d) is the image transmission type. Can be [Direct], [File],
-	// [TempFile], or[SharedMemory].
-	Transmission byte
-
-	// File is the file path to be used when the transmission type is [File].
-	// If [Options.Transmission] is omitted i.e. zero and this is non-empty,
-	// the transmission type is set to [File].
-	File string
-
-	// Size (S=0) is the size to be read from the transmission medium.
-	Size int
-
-	// Offset (O=0) is the offset byte to start reading from the transmission
-	// medium.
-	Offset int
-
-	// Chunk (m=) whether the image is transmitted in chunks. Can be either
-	// zero or one. When true, the image is transmitted in chunks. Each chunk
-	// must be a multiple of 4, and up to [MaxChunkSize] bytes. Each chunk must
-	// have the m=1 option except for the last chunk which must have m=0.
-	Chunk bool
-
-	// Display options.
-
-	// X (x=0) is the pixel X coordinate of the image to start displaying.
-	X int
-
-	// Y (y=0) is the pixel Y coordinate of the image to start displaying.
-	Y int
-
-	// Z (z=0) is the Z coordinate of the image to display.
-	Z int
-
-	// Width (w=0) is the width of the image to display.
-	Width int
-
-	// Height (h=0) is the height of the image to display.
-	Height int
-
-	// OffsetX (X=0) is the OffsetX coordinate of the cursor cell to start
-	// displaying the image. OffsetX=0 is the leftmost cell. This must be
-	// smaller than the terminal cell width.
-	OffsetX int
-
-	// OffsetY (Y=0) is the OffsetY coordinate of the cursor cell to start
-	// displaying the image. OffsetY=0 is the topmost cell. This must be
-	// smaller than the terminal cell height.
-	OffsetY int
-
-	// Columns (c=0) is the number of columns to display the image. The image
-	// will be scaled to fit the number of columns.
-	Columns int
-
-	// Rows (r=0) is the number of rows to display the image. The image will be
-	// scaled to fit the number of rows.
-	Rows int
-
-	// VirtualPlacement (U=0) whether to use virtual placement. This is used
-	// with Unicode [Placeholder] to display images.
-	VirtualPlacement bool
-
-	// DoNotMoveCursor (C=0) whether to move the cursor after displaying the
-	// image.
-	DoNotMoveCursor bool
-
-	// ParentID (P=0) is the parent image ID. The parent ID is the ID of the
-	// image that is the parent of the current image. This is used with Unicode
-	// [Placeholder] to display images relative to the parent image.
-	ParentID int
-
-	// ParentPlacementID (Q=0) is the parent placement ID. The parent placement
-	// ID is the ID of the placement of the parent image. This is used with
-	// Unicode [Placeholder] to display images relative to the parent image.
-	ParentPlacementID int
-
-	// Delete options.
-
-	// Delete (d=a) is the delete action. Can be one of [DeleteAll],
-	// [DeleteID], [DeleteNumber], [DeleteCursor], [DeleteFrames],
-	// [DeleteCell], [DeleteCellZ], [DeleteRange], [DeleteColumn], [DeleteRow],
-	// [DeleteZ].
-	Delete byte
-
-	// DeleteResources indicates whether to delete the resources associated
-	// with the image.
-	DeleteResources bool
-}
-
-// Options returns the options as a slice of a key-value pairs.
-func (o *Options) Options() (opts []string) {
-	opts = []string{}
-	if o.Format == 0 {
-		o.Format = RGBA
-	}
-
-	if o.Action == 0 {
-		o.Action = Transmit
-	}
-
-	if o.Delete == 0 {
-		o.Delete = DeleteAll
-	}
-
-	if o.Transmission == 0 {
-		if len(o.File) > 0 {
-			o.Transmission = File
-		} else {
-			o.Transmission = Direct
-		}
-	}
-
-	if o.Format != RGBA {
-		opts = append(opts, fmt.Sprintf("f=%d", o.Format))
-	}
-
-	if o.Quite > 0 {
-		opts = append(opts, fmt.Sprintf("q=%d", o.Quite))
-	}
-
-	if o.ID > 0 {
-		opts = append(opts, fmt.Sprintf("i=%d", o.ID))
-	}
-
-	if o.PlacementID > 0 {
-		opts = append(opts, fmt.Sprintf("p=%d", o.PlacementID))
-	}
-
-	if o.Number > 0 {
-		opts = append(opts, fmt.Sprintf("I=%d", o.Number))
-	}
-
-	if o.ImageWidth > 0 {
-		opts = append(opts, fmt.Sprintf("s=%d", o.ImageWidth))
-	}
-
-	if o.ImageHeight > 0 {
-		opts = append(opts, fmt.Sprintf("v=%d", o.ImageHeight))
-	}
-
-	if o.Transmission != Direct {
-		opts = append(opts, fmt.Sprintf("t=%c", o.Transmission))
-	}
-
-	if o.Size > 0 {
-		opts = append(opts, fmt.Sprintf("S=%d", o.Size))
-	}
-
-	if o.Offset > 0 {
-		opts = append(opts, fmt.Sprintf("O=%d", o.Offset))
-	}
-
-	if o.Compression == Zlib {
-		opts = append(opts, fmt.Sprintf("o=%c", o.Compression))
-	}
-
-	if o.VirtualPlacement {
-		opts = append(opts, "U=1")
-	}
-
-	if o.DoNotMoveCursor {
-		opts = append(opts, "C=1")
-	}
-
-	if o.ParentID > 0 {
-		opts = append(opts, fmt.Sprintf("P=%d", o.ParentID))
-	}
-
-	if o.ParentPlacementID > 0 {
-		opts = append(opts, fmt.Sprintf("Q=%d", o.ParentPlacementID))
-	}
-
-	if o.X > 0 {
-		opts = append(opts, fmt.Sprintf("x=%d", o.X))
-	}
-
-	if o.Y > 0 {
-		opts = append(opts, fmt.Sprintf("y=%d", o.Y))
-	}
-
-	if o.Z > 0 {
-		opts = append(opts, fmt.Sprintf("z=%d", o.Z))
-	}
-
-	if o.Width > 0 {
-		opts = append(opts, fmt.Sprintf("w=%d", o.Width))
-	}
-
-	if o.Height > 0 {
-		opts = append(opts, fmt.Sprintf("h=%d", o.Height))
-	}
-
-	if o.OffsetX > 0 {
-		opts = append(opts, fmt.Sprintf("X=%d", o.OffsetX))
-	}
-
-	if o.OffsetY > 0 {
-		opts = append(opts, fmt.Sprintf("Y=%d", o.OffsetY))
-	}
-
-	if o.Columns > 0 {
-		opts = append(opts, fmt.Sprintf("c=%d", o.Columns))
-	}
-
-	if o.Rows > 0 {
-		opts = append(opts, fmt.Sprintf("r=%d", o.Rows))
-	}
-
-	if o.Delete != DeleteAll || o.DeleteResources {
-		da := o.Delete
-		if o.DeleteResources {
-			da = da - ' ' // to uppercase
-		}
-
-		opts = append(opts, fmt.Sprintf("d=%c", da))
-	}
-
-	if o.Action != Transmit {
-		opts = append(opts, fmt.Sprintf("a=%c", o.Action))
-	}
-
-	return
-}
-
-// String returns the string representation of the options.
-func (o Options) String() string {
-	return strings.Join(o.Options(), ",")
-}
-
-// MarshalText returns the string representation of the options.
-func (o Options) MarshalText() ([]byte, error) {
-	return []byte(o.String()), nil
-}
-
-// UnmarshalText parses the options from the given string.
-func (o *Options) UnmarshalText(text []byte) error {
-	opts := strings.Split(string(text), ",")
-	for _, opt := range opts {
-		ps := strings.SplitN(opt, "=", 2)
-		if len(ps) != 2 || len(ps[1]) == 0 {
-			continue
-		}
-
-		switch ps[0] {
-		case "a":
-			o.Action = ps[1][0]
-		case "o":
-			o.Compression = ps[1][0]
-		case "t":
-			o.Transmission = ps[1][0]
-		case "d":
-			d := ps[1][0]
-			if d >= 'A' && d <= 'Z' {
-				o.DeleteResources = true
-				d = d + ' ' // to lowercase
-			}
-			o.Delete = d
-		case "i", "q", "p", "I", "f", "s", "v", "S", "O", "m", "x", "y", "z", "w", "h", "X", "Y", "c", "r", "U", "P", "Q":
-			v, err := strconv.Atoi(ps[1])
-			if err != nil {
-				continue
-			}
-
-			switch ps[0] {
-			case "i":
-				o.ID = v
-			case "q":
-				o.Quite = byte(v)
-			case "p":
-				o.PlacementID = v
-			case "I":
-				o.Number = v
-			case "f":
-				o.Format = v
-			case "s":
-				o.ImageWidth = v
-			case "v":
-				o.ImageHeight = v
-			case "S":
-				o.Size = v
-			case "O":
-				o.Offset = v
-			case "m":
-				o.Chunk = v == 0 || v == 1
-			case "x":
-				o.X = v
-			case "y":
-				o.Y = v
-			case "z":
-				o.Z = v
-			case "w":
-				o.Width = v
-			case "h":
-				o.Height = v
-			case "X":
-				o.OffsetX = v
-			case "Y":
-				o.OffsetY = v
-			case "c":
-				o.Columns = v
-			case "r":
-				o.Rows = v
-			case "U":
-				o.VirtualPlacement = v == 1
-			case "P":
-				o.ParentID = v
-			case "Q":
-				o.ParentPlacementID = v
-			}
-		}
-	}
-
-	return nil
-}
diff --git a/vendor/github.com/charmbracelet/x/ansi/mode.go b/vendor/github.com/charmbracelet/x/ansi/mode.go
index b57b09cc7..efe62583e 100644
--- a/vendor/github.com/charmbracelet/x/ansi/mode.go
+++ b/vendor/github.com/charmbracelet/x/ansi/mode.go
@@ -48,7 +48,7 @@ type Mode interface {
 	Mode() int
 }
 
-// SetMode (SM) returns a sequence to set a mode.
+// SetMode (SM) or (DECSET) returns a sequence to set a mode.
 // The mode arguments are a list of modes to set.
 //
 // If one of the modes is a [DECMode], the function will returns two escape
@@ -72,7 +72,12 @@ func SM(modes ...Mode) string {
 	return SetMode(modes...)
 }
 
-// ResetMode (RM) returns a sequence to reset a mode.
+// DECSET is an alias for [SetMode].
+func DECSET(modes ...Mode) string {
+	return SetMode(modes...)
+}
+
+// ResetMode (RM) or (DECRST) returns a sequence to reset a mode.
 // The mode arguments are a list of modes to reset.
 //
 // If one of the modes is a [DECMode], the function will returns two escape
@@ -96,9 +101,14 @@ func RM(modes ...Mode) string {
 	return ResetMode(modes...)
 }
 
+// DECRST is an alias for [ResetMode].
+func DECRST(modes ...Mode) string {
+	return ResetMode(modes...)
+}
+
 func setMode(reset bool, modes ...Mode) (s string) {
 	if len(modes) == 0 {
-		return
+		return s
 	}
 
 	cmd := "h"
@@ -132,7 +142,7 @@ func setMode(reset bool, modes ...Mode) (s string) {
 	if len(dec) > 0 {
 		s += seq + "?" + strings.Join(dec, ";") + cmd
 	}
-	return
+	return s
 }
 
 // RequestMode (DECRQM) returns a sequence to request a mode from the terminal.
@@ -312,7 +322,7 @@ const (
 
 // Deprecated: use [SetCursorKeysMode] and [ResetCursorKeysMode] instead.
 const (
-	EnableCursorKeys  = "\x1b[?1h"
+	EnableCursorKeys  = "\x1b[?1h" //nolint:revive // grouped constants
 	DisableCursorKeys = "\x1b[?1l"
 )
 
@@ -563,8 +573,9 @@ const (
 
 // Deprecated: use [SetFocusEventMode], [ResetFocusEventMode], and
 // [RequestFocusEventMode] instead.
+// Focus reporting mode constants.
 const (
-	ReportFocusMode = DECMode(1004)
+	ReportFocusMode = DECMode(1004) //nolint:revive // grouped constants
 
 	EnableReportFocus  = "\x1b[?1004h"
 	DisableReportFocus = "\x1b[?1004l"
@@ -592,7 +603,7 @@ const (
 // Deprecated: use [SgrExtMouseMode] [SetSgrExtMouseMode],
 // [ResetSgrExtMouseMode], and [RequestSgrExtMouseMode] instead.
 const (
-	MouseSgrExtMode    = DECMode(1006)
+	MouseSgrExtMode    = DECMode(1006) //nolint:revive // grouped constants
 	EnableMouseSgrExt  = "\x1b[?1006h"
 	DisableMouseSgrExt = "\x1b[?1006l"
 	RequestMouseSgrExt = "\x1b[?1006$p"
@@ -708,7 +719,7 @@ const (
 // Deprecated: use [SetBracketedPasteMode], [ResetBracketedPasteMode], and
 // [RequestBracketedPasteMode] instead.
 const (
-	EnableBracketedPaste  = "\x1b[?2004h"
+	EnableBracketedPaste  = "\x1b[?2004h" //nolint:revive // grouped constants
 	DisableBracketedPaste = "\x1b[?2004l"
 	RequestBracketedPaste = "\x1b[?2004$p"
 )
@@ -725,6 +736,8 @@ const (
 	RequestSynchronizedOutputMode = "\x1b[?2026$p"
 )
 
+// Synchronized Output Mode. See [SynchronizedOutputMode].
+//
 // Deprecated: use [SynchronizedOutputMode], [SetSynchronizedOutputMode], and
 // [ResetSynchronizedOutputMode], and [RequestSynchronizedOutputMode] instead.
 const (
@@ -735,12 +748,28 @@ const (
 	RequestSyncdOutput = "\x1b[?2026$p"
 )
 
+// Unicode Core Mode is a mode that determines whether the terminal should use
+// Unicode grapheme clustering to calculate the width of glyphs for each
+// terminal cell.
+//
+// See: https://github.com/contour-terminal/terminal-unicode-core
+const (
+	UnicodeCoreMode = DECMode(2027)
+
+	SetUnicodeCoreMode     = "\x1b[?2027h"
+	ResetUnicodeCoreMode   = "\x1b[?2027l"
+	RequestUnicodeCoreMode = "\x1b[?2027$p"
+)
+
 // Grapheme Clustering Mode is a mode that determines whether the terminal
 // should look for grapheme clusters instead of single runes in the rendered
 // text. This makes the terminal properly render combining characters such as
 // emojis.
 //
 // See: https://github.com/contour-terminal/terminal-unicode-core
+//
+// Deprecated: use [GraphemeClusteringMode], [SetUnicodeCoreMode],
+// [ResetUnicodeCoreMode], and [RequestUnicodeCoreMode] instead.
 const (
 	GraphemeClusteringMode = DECMode(2027)
 
@@ -749,14 +778,54 @@ const (
 	RequestGraphemeClusteringMode = "\x1b[?2027$p"
 )
 
-// Deprecated: use [SetGraphemeClusteringMode], [ResetGraphemeClusteringMode], and
-// [RequestGraphemeClusteringMode] instead.
+// Grapheme Clustering Mode. See [GraphemeClusteringMode].
+//
+// Deprecated: use [SetUnicodeCoreMode], [ResetUnicodeCoreMode], and
+// [RequestUnicodeCoreMode] instead.
 const (
 	EnableGraphemeClustering  = "\x1b[?2027h"
 	DisableGraphemeClustering = "\x1b[?2027l"
 	RequestGraphemeClustering = "\x1b[?2027$p"
 )
 
+// LightDarkMode is a mode that enables reporting the operating system's color
+// scheme (light or dark) preference. It reports the color scheme as a [DSR]
+// and [LightDarkReport] escape sequences encoded as follows:
+//
+//	CSI ? 997 ; 1 n   for dark mode
+//	CSI ? 997 ; 2 n   for light mode
+//
+// The color preference can also be requested via the following [DSR] and
+// [RequestLightDarkReport] escape sequences:
+//
+//	CSI ? 996 n
+//
+// See: https://contour-terminal.org/vt-extensions/color-palette-update-notifications/
+const (
+	LightDarkMode = DECMode(2031)
+
+	SetLightDarkMode     = "\x1b[?2031h"
+	ResetLightDarkMode   = "\x1b[?2031l"
+	RequestLightDarkMode = "\x1b[?2031$p"
+)
+
+// InBandResizeMode is a mode that reports terminal resize events as escape
+// sequences. This is useful for systems that do not support [SIGWINCH] like
+// Windows.
+//
+// The terminal then sends the following encoding:
+//
+//	CSI 48 ; cellsHeight ; cellsWidth ; pixelHeight ; pixelWidth t
+//
+// See: https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83
+const (
+	InBandResizeMode = DECMode(2048)
+
+	SetInBandResizeMode     = "\x1b[?2048h"
+	ResetInBandResizeMode   = "\x1b[?2048l"
+	RequestInBandResizeMode = "\x1b[?2048$p"
+)
+
 // Win32Input is a mode that determines whether input is processed by the
 // Win32 console and Conpty.
 //
@@ -772,7 +841,7 @@ const (
 // Deprecated: use [SetWin32InputMode], [ResetWin32InputMode], and
 // [RequestWin32InputMode] instead.
 const (
-	EnableWin32Input  = "\x1b[?9001h"
+	EnableWin32Input  = "\x1b[?9001h" //nolint:revive // grouped constants
 	DisableWin32Input = "\x1b[?9001l"
 	RequestWin32Input = "\x1b[?9001$p"
 )
diff --git a/vendor/github.com/charmbracelet/x/ansi/mouse.go b/vendor/github.com/charmbracelet/x/ansi/mouse.go
index 95b0127fd..94b8b4858 100644
--- a/vendor/github.com/charmbracelet/x/ansi/mouse.go
+++ b/vendor/github.com/charmbracelet/x/ansi/mouse.go
@@ -134,7 +134,7 @@ func EncodeMouseButton(b MouseButton, motion, shift, alt, ctrl bool) (m byte) {
 		m |= bitMotion
 	}
 
-	return
+	return m
 }
 
 // x10Offset is the offset for X10 mouse events.
diff --git a/vendor/github.com/charmbracelet/x/ansi/notification.go b/vendor/github.com/charmbracelet/x/ansi/notification.go
index c712f3405..fd6221d24 100644
--- a/vendor/github.com/charmbracelet/x/ansi/notification.go
+++ b/vendor/github.com/charmbracelet/x/ansi/notification.go
@@ -1,5 +1,10 @@
 package ansi
 
+import (
+	"fmt"
+	"strings"
+)
+
 // Notify sends a desktop notification using iTerm's OSC 9.
 //
 //	OSC 9 ; Mc ST
@@ -11,3 +16,17 @@ package ansi
 func Notify(s string) string {
 	return "\x1b]9;" + s + "\x07"
 }
+
+// DesktopNotification sends a desktop notification based on the extensible OSC
+// 99 escape code.
+//
+//	OSC 99 ; <metadata> ; <payload> ST
+//	OSC 99 ; <metadata> ; <payload> BEL
+//
+// Where <metadata> is a colon-separated list of key-value pairs, and
+// <payload> is the notification body.
+//
+// See: https://sw.kovidgoyal.net/kitty/desktop-notifications/
+func DesktopNotification(payload string, metadata ...string) string {
+	return fmt.Sprintf("\x1b]99;%s;%s\x07", strings.Join(metadata, ":"), payload)
+}
diff --git a/vendor/github.com/charmbracelet/x/ansi/palette.go b/vendor/github.com/charmbracelet/x/ansi/palette.go
new file mode 100644
index 000000000..cc4519b81
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/ansi/palette.go
@@ -0,0 +1,34 @@
+package ansi
+
+import (
+	"fmt"
+	"image/color"
+)
+
+// SetPalette sets the palette color for the given index. The index is a 16
+// color index between 0 and 15. The color is a 24-bit RGB color.
+//
+//	OSC P n rrggbb BEL
+//
+// Where n is the color index in hex (0-f), and rrggbb is the color in
+// hexadecimal format (e.g., ff0000 for red).
+//
+// This sequence is specific to the Linux Console and may not work in other
+// terminal emulators.
+//
+// See https://man7.org/linux/man-pages/man4/console_codes.4.html
+func SetPalette(i int, c color.Color) string {
+	if c == nil || i < 0 || i > 15 {
+		return ""
+	}
+	r, g, b, _ := c.RGBA()
+	return fmt.Sprintf("\x1b]P%x%02x%02x%02x\x07", i, r>>8, g>>8, b>>8)
+}
+
+// ResetPalette resets the color palette to the default values.
+//
+// This sequence is specific to the Linux Console and may not work in other
+// terminal emulators.
+//
+// See https://man7.org/linux/man-pages/man4/console_codes.4.html
+const ResetPalette = "\x1b]R\x07"
diff --git a/vendor/github.com/charmbracelet/x/ansi/parser.go b/vendor/github.com/charmbracelet/x/ansi/parser.go
index 882e1ed7a..e770c15f9 100644
--- a/vendor/github.com/charmbracelet/x/ansi/parser.go
+++ b/vendor/github.com/charmbracelet/x/ansi/parser.go
@@ -150,7 +150,7 @@ func (p *Parser) StateName() string {
 // Parse parses the given dispatcher and byte buffer.
 // Deprecated: Loop over the buffer and call [Parser.Advance] instead.
 func (p *Parser) Parse(b []byte) {
-	for i := 0; i < len(b); i++ {
+	for i := range b {
 		p.Advance(b[i])
 	}
 }
@@ -245,7 +245,7 @@ func (p *Parser) parseStringCmd() {
 	if p.dataLen >= 0 {
 		datalen = p.dataLen
 	}
-	for i := 0; i < datalen; i++ {
+	for i := range datalen {
 		d := p.data[i]
 		if d < '0' || d > '9' {
 			break
diff --git a/vendor/github.com/charmbracelet/x/ansi/parser/const.go b/vendor/github.com/charmbracelet/x/ansi/parser/const.go
index d62dbf379..85c90869c 100644
--- a/vendor/github.com/charmbracelet/x/ansi/parser/const.go
+++ b/vendor/github.com/charmbracelet/x/ansi/parser/const.go
@@ -1,3 +1,4 @@
+// Package parser provides ANSI escape sequence parsing functionality.
 package parser
 
 // Action is a DEC ANSI parser action.
@@ -19,7 +20,7 @@ const (
 	IgnoreAction = NoneAction
 )
 
-// nolint: unused
+// ActionNames provides string names for parser actions.
 var ActionNames = []string{
 	"NoneAction",
 	"ClearAction",
@@ -58,7 +59,7 @@ const (
 	Utf8State
 )
 
-// nolint: unused
+// StateNames provides string names for parser states.
 var StateNames = []string{
 	"GroundState",
 	"CsiEntryState",
diff --git a/vendor/github.com/charmbracelet/x/ansi/parser/seq.go b/vendor/github.com/charmbracelet/x/ansi/parser/seq.go
index 29f491d1c..de7e15e68 100644
--- a/vendor/github.com/charmbracelet/x/ansi/parser/seq.go
+++ b/vendor/github.com/charmbracelet/x/ansi/parser/seq.go
@@ -78,7 +78,7 @@ func Subparams(params []int, i int) []int {
 	// Count the number of parameters before the given parameter index.
 	var count int
 	var j int
-	for j = 0; j < len(params); j++ {
+	for j = range params {
 		if count == i {
 			break
 		}
@@ -116,7 +116,7 @@ func Subparams(params []int, i int) []int {
 // sub-parameters.
 func Len(params []int) int {
 	var n int
-	for i := 0; i < len(params); i++ {
+	for i := range params {
 		if !HasMore(params, i) {
 			n++
 		}
@@ -128,7 +128,7 @@ func Len(params []int) int {
 // function for each parameter.
 // The function should return false to stop the iteration.
 func Range(params []int, fn func(i int, param int, hasMore bool) bool) {
-	for i := 0; i < len(params); i++ {
+	for i := range params {
 		if !fn(i, Param(params, i), HasMore(params, i)) {
 			break
 		}
diff --git a/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go b/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go
index 558a5eacc..ef46b7b6a 100644
--- a/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go
+++ b/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go
@@ -30,7 +30,7 @@ func NewTransitionTable(size int) TransitionTable {
 
 // SetDefault sets default transition.
 func (t TransitionTable) SetDefault(action Action, state State) {
-	for i := 0; i < len(t); i++ {
+	for i := range t {
 		t[i] = action<<TransitionActionShift | state
 	}
 }
@@ -63,7 +63,7 @@ func (t TransitionTable) Transition(state State, code byte) (State, Action) {
 	return value & TransitionStateMask, value >> TransitionActionShift
 }
 
-// byte range macro
+// byte range macro.
 func r(start, end byte) []byte {
 	var a []byte
 	for i := int(start); i <= int(end); i++ {
diff --git a/vendor/github.com/charmbracelet/x/ansi/parser_decode.go b/vendor/github.com/charmbracelet/x/ansi/parser_decode.go
index 3e5047394..096c86354 100644
--- a/vendor/github.com/charmbracelet/x/ansi/parser_decode.go
+++ b/vendor/github.com/charmbracelet/x/ansi/parser_decode.go
@@ -4,8 +4,9 @@ import (
 	"unicode/utf8"
 
 	"github.com/charmbracelet/x/ansi/parser"
+	"github.com/clipperhouse/displaywidth"
+	"github.com/clipperhouse/uax29/v2/graphemes"
 	"github.com/mattn/go-runewidth"
-	"github.com/rivo/uniseg"
 )
 
 // State represents the state of the ANSI escape sequence parser used by
@@ -176,10 +177,7 @@ func decodeSequence[T string | []byte](m Method, b T, state State, p *Parser) (s
 			}
 
 			if utf8.RuneStart(c) {
-				seq, _, width, _ = FirstGraphemeCluster(b, -1)
-				if m == WcWidth {
-					width = runewidth.StringWidth(string(seq))
-				}
+				seq, width = FirstGraphemeCluster(b, m)
 				i += len(seq)
 				return b[:i], width, i, NormalState
 			}
@@ -359,7 +357,7 @@ func parseOscCmd(p *Parser) {
 	if p == nil || p.cmd != parser.MissingCommand {
 		return
 	}
-	for j := 0; j < p.dataLen; j++ {
+	for j := range p.dataLen {
 		d := p.data[j]
 		if d < '0' || d > '9' {
 			break
@@ -434,17 +432,22 @@ func HasEscPrefix[T string | []byte](b T) bool {
 	return len(b) > 0 && b[0] == ESC
 }
 
-// FirstGraphemeCluster returns the first grapheme cluster in the given string or byte slice.
-// This is a syntactic sugar function that wraps
-// uniseg.FirstGraphemeClusterInString and uniseg.FirstGraphemeCluster.
-func FirstGraphemeCluster[T string | []byte](b T, state int) (T, T, int, int) {
+// FirstGraphemeCluster returns the first grapheme cluster in the given string
+// or byte slice, and its monospace display width.
+func FirstGraphemeCluster[T string | []byte](b T, m Method) (T, int) {
 	switch b := any(b).(type) {
 	case string:
-		cluster, rest, width, newState := uniseg.FirstGraphemeClusterInString(b, state)
-		return T(cluster), T(rest), width, newState
+		cluster := graphemes.FromString(b).First()
+		if m == WcWidth {
+			return T(cluster), runewidth.StringWidth(cluster)
+		}
+		return T(cluster), displaywidth.String(cluster)
 	case []byte:
-		cluster, rest, width, newState := uniseg.FirstGraphemeCluster(b, state)
-		return T(cluster), T(rest), width, newState
+		cluster := graphemes.FromBytes(b).First()
+		if m == WcWidth {
+			return T(cluster), runewidth.StringWidth(string(cluster))
+		}
+		return T(cluster), displaywidth.Bytes(cluster)
 	}
 	panic("unreachable")
 }
@@ -490,7 +493,7 @@ func Command(prefix, inter, final byte) (c int) {
 	c = int(final)
 	c |= int(prefix) << parser.PrefixShift
 	c |= int(inter) << parser.IntermedShift
-	return
+	return c
 }
 
 // Param represents a sequence parameter. Sequence parameters with
@@ -520,5 +523,5 @@ func Parameter(p int, hasMore bool) (s int) {
 	if hasMore {
 		s |= parser.HasMoreFlag
 	}
-	return
+	return s
 }
diff --git a/vendor/github.com/charmbracelet/x/ansi/passthrough.go b/vendor/github.com/charmbracelet/x/ansi/passthrough.go
index 14a745220..7ac7cef15 100644
--- a/vendor/github.com/charmbracelet/x/ansi/passthrough.go
+++ b/vendor/github.com/charmbracelet/x/ansi/passthrough.go
@@ -21,10 +21,7 @@ func ScreenPassthrough(seq string, limit int) string {
 	b.WriteString("\x1bP")
 	if limit > 0 {
 		for i := 0; i < len(seq); i += limit {
-			end := i + limit
-			if end > len(seq) {
-				end = len(seq)
-			}
+			end := min(i+limit, len(seq))
 			b.WriteString(seq[i:end])
 			if end < len(seq) {
 				b.WriteString("\x1b\\\x1bP")
@@ -52,7 +49,7 @@ func ScreenPassthrough(seq string, limit int) string {
 func TmuxPassthrough(seq string) string {
 	var b bytes.Buffer
 	b.WriteString("\x1bPtmux;")
-	for i := 0; i < len(seq); i++ {
+	for i := range len(seq) {
 		if seq[i] == ESC {
 			b.WriteByte(ESC)
 		}
diff --git a/vendor/github.com/charmbracelet/x/ansi/progress.go b/vendor/github.com/charmbracelet/x/ansi/progress.go
new file mode 100644
index 000000000..19441d1eb
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/ansi/progress.go
@@ -0,0 +1,49 @@
+package ansi
+
+import "strconv"
+
+// ResetProgressBar is a sequence that resets the progress bar to its default
+// state (hidden).
+//
+// OSC 9 ; 4 ; 0 BEL
+//
+// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
+const ResetProgressBar = "\x1b]9;4;0\x07"
+
+// SetProgressBar returns a sequence for setting the progress bar to a specific
+// percentage (0-100) in the "default" state.
+//
+// OSC 9 ; 4 ; 1 Percentage BEL
+//
+// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
+func SetProgressBar(percentage int) string {
+	return "\x1b]9;4;1;" + strconv.Itoa(min(max(0, percentage), 100)) + "\x07"
+}
+
+// SetErrorProgressBar returns a sequence for setting the progress bar to a
+// specific percentage (0-100) in the "Error" state..
+//
+// OSC 9 ; 4 ; 2 Percentage BEL
+//
+// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
+func SetErrorProgressBar(percentage int) string {
+	return "\x1b]9;4;2;" + strconv.Itoa(min(max(0, percentage), 100)) + "\x07"
+}
+
+// SetIndeterminateProgressBar is a sequence that sets the progress bar to the
+// indeterminate state.
+//
+// OSC 9 ; 4 ; 3 BEL
+//
+// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
+const SetIndeterminateProgressBar = "\x1b]9;4;3\x07"
+
+// SetWarningProgressBar is a sequence that sets the progress bar to the
+// "Warning" state.
+//
+// OSC 9 ; 4 ; 4 Percentage BEL
+//
+// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
+func SetWarningProgressBar(percentage int) string {
+	return "\x1b]9;4;4;" + strconv.Itoa(min(max(0, percentage), 100)) + "\x07"
+}
diff --git a/vendor/github.com/charmbracelet/x/ansi/screen.go b/vendor/github.com/charmbracelet/x/ansi/screen.go
index c76e4f0d6..e2027dff9 100644
--- a/vendor/github.com/charmbracelet/x/ansi/screen.go
+++ b/vendor/github.com/charmbracelet/x/ansi/screen.go
@@ -351,7 +351,7 @@ func DECRQPSR(n int) string {
 //
 // See: https://vt100.net/docs/vt510-rm/DECTABSR.html
 func TabStopReport(stops ...int) string {
-	var s []string
+	var s []string //nolint:prealloc
 	for _, v := range stops {
 		s = append(s, strconv.Itoa(v))
 	}
@@ -376,7 +376,7 @@ func DECTABSR(stops ...int) string {
 //
 // See: https://vt100.net/docs/vt510-rm/DECCIR.html
 func CursorInformationReport(values ...int) string {
-	var s []string
+	var s []string //nolint:prealloc
 	for _, v := range values {
 		s = append(s, strconv.Itoa(v))
 	}
@@ -395,7 +395,7 @@ func DECCIR(values ...int) string {
 //
 //	CSI Pn b
 //
-// See: ECMA-48 § 8.3.103
+// See: ECMA-48 § 8.3.103.
 func RepeatPreviousCharacter(n int) string {
 	var s string
 	if n > 1 {
diff --git a/vendor/github.com/charmbracelet/x/ansi/sgr.go b/vendor/github.com/charmbracelet/x/ansi/sgr.go
index c4bd2afad..5e6d05dfe 100644
--- a/vendor/github.com/charmbracelet/x/ansi/sgr.go
+++ b/vendor/github.com/charmbracelet/x/ansi/sgr.go
@@ -1,6 +1,6 @@
 package ansi
 
-// Select Graphic Rendition (SGR) is a command that sets display attributes.
+// SelectGraphicRendition (SGR) is a command that sets display attributes.
 //
 // Default is 0.
 //
diff --git a/vendor/github.com/charmbracelet/x/ansi/status.go b/vendor/github.com/charmbracelet/x/ansi/status.go
index 4337e189d..3adfb028e 100644
--- a/vendor/github.com/charmbracelet/x/ansi/status.go
+++ b/vendor/github.com/charmbracelet/x/ansi/status.go
@@ -11,10 +11,10 @@ type StatusReport interface {
 	StatusReport() int
 }
 
-// ANSIReport represents an ANSI terminal status report.
+// ANSIStatusReport represents an ANSI terminal status report.
 type ANSIStatusReport int //nolint:revive
 
-// Report returns the status report identifier.
+// StatusReport returns the status report identifier.
 func (s ANSIStatusReport) StatusReport() int {
 	return int(s)
 }
@@ -22,7 +22,7 @@ func (s ANSIStatusReport) StatusReport() int {
 // DECStatusReport represents a DEC terminal status report.
 type DECStatusReport int
 
-// Status returns the status report identifier.
+// StatusReport returns the status report identifier.
 func (s DECStatusReport) StatusReport() int {
 	return int(s)
 }
@@ -89,6 +89,16 @@ const RequestCursorPositionReport = "\x1b[6n"
 // See: https://vt100.net/docs/vt510-rm/DECXCPR.html
 const RequestExtendedCursorPositionReport = "\x1b[?6n"
 
+// RequestLightDarkReport is a control sequence that requests the terminal to
+// report its operating system light/dark color preference. Supported terminals
+// should respond with a [LightDarkReport] sequence as follows:
+//
+//	CSI ? 997 ; 1 n   for dark mode
+//	CSI ? 997 ; 2 n   for light mode
+//
+// See: https://contour-terminal.org/vt-extensions/color-palette-update-notifications/
+const RequestLightDarkReport = "\x1b[?996n"
+
 // CursorPositionReport (CPR) is a control sequence that reports the cursor's
 // position.
 //
@@ -142,3 +152,17 @@ func ExtendedCursorPositionReport(line, column, page int) string {
 func DECXCPR(line, column, page int) string {
 	return ExtendedCursorPositionReport(line, column, page)
 }
+
+// LightDarkReport is a control sequence that reports the terminal's operating
+// system light/dark color preference.
+//
+//	CSI ? 997 ; 1 n   for dark mode
+//	CSI ? 997 ; 2 n   for light mode
+//
+// See: https://contour-terminal.org/vt-extensions/color-palette-update-notifications/
+func LightDarkReport(dark bool) string {
+	if dark {
+		return "\x1b[?997;1n"
+	}
+	return "\x1b[?997;2n"
+}
diff --git a/vendor/github.com/charmbracelet/x/ansi/style.go b/vendor/github.com/charmbracelet/x/ansi/style.go
index c8663f32b..77048dce0 100644
--- a/vendor/github.com/charmbracelet/x/ansi/style.go
+++ b/vendor/github.com/charmbracelet/x/ansi/style.go
@@ -526,7 +526,7 @@ func underlineColorString(c Color) string {
 //  2. Support ignoring and omitting the color space id (second parameter) with respect to RGB colors
 //  3. Support ignoring and omitting the 6th parameter with respect to RGB and CMY colors
 //  4. Support reading RGBA colors
-func ReadStyleColor(params Params, co *color.Color) (n int) {
+func ReadStyleColor(params Params, co *color.Color) int {
 	if len(params) < 2 { // Need at least SGR type and color type
 		return 0
 	}
@@ -535,7 +535,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
 	s := params[0]
 	p := params[1]
 	colorType := p.Param(0)
-	n = 2
+	n := 2
 
 	paramsfn := func() (p1, p2, p3, p4 int) {
 		// Where should we start reading the color?
@@ -594,7 +594,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
 			B: uint8(b), //nolint:gosec
 			A: 0xff,
 		}
-		return
+		return n
 
 	case 3: // CMY direct color
 		if len(params) < 5 {
@@ -612,7 +612,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
 			Y: uint8(y), //nolint:gosec
 			K: 0,
 		}
-		return
+		return n
 
 	case 4: // CMYK direct color
 		if len(params) < 6 {
@@ -630,7 +630,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
 			Y: uint8(y), //nolint:gosec
 			K: uint8(k), //nolint:gosec
 		}
-		return
+		return n
 
 	case 5: // indexed color
 		if len(params) < 3 {
@@ -665,7 +665,7 @@ func ReadStyleColor(params Params, co *color.Color) (n int) {
 			B: uint8(b), //nolint:gosec
 			A: uint8(a), //nolint:gosec
 		}
-		return
+		return n
 
 	default:
 		return 0
diff --git a/vendor/github.com/charmbracelet/x/ansi/termcap.go b/vendor/github.com/charmbracelet/x/ansi/termcap.go
index 3c5c7da92..b59aa4203 100644
--- a/vendor/github.com/charmbracelet/x/ansi/termcap.go
+++ b/vendor/github.com/charmbracelet/x/ansi/termcap.go
@@ -5,7 +5,7 @@ import (
 	"strings"
 )
 
-// RequestTermcap (XTGETTCAP) requests Termcap/Terminfo strings.
+// XTGETTCAP (RequestTermcap) requests Termcap/Terminfo strings.
 //
 //	DCS + q <Pt> ST
 //
diff --git a/vendor/github.com/charmbracelet/x/ansi/truncate.go b/vendor/github.com/charmbracelet/x/ansi/truncate.go
index 3f541fa56..f95f82930 100644
--- a/vendor/github.com/charmbracelet/x/ansi/truncate.go
+++ b/vendor/github.com/charmbracelet/x/ansi/truncate.go
@@ -1,11 +1,11 @@
 package ansi
 
 import (
-	"bytes"
+	"strings"
 
 	"github.com/charmbracelet/x/ansi/parser"
-	"github.com/mattn/go-runewidth"
-	"github.com/rivo/uniseg"
+	"github.com/clipperhouse/displaywidth"
+	"github.com/clipperhouse/uax29/v2/graphemes"
 )
 
 // Cut the string, without adding any prefix or tail strings. This function is
@@ -74,12 +74,11 @@ func truncate(m Method, s string, length int, tail string) string {
 		return ""
 	}
 
-	var cluster []byte
-	var buf bytes.Buffer
+	var cluster string
+	var buf strings.Builder
 	curWidth := 0
 	ignoring := false
 	pstate := parser.GroundState // initial state
-	b := []byte(s)
 	i := 0
 
 	// Here we iterate over the bytes of the string and collect printable
@@ -88,16 +87,12 @@ func truncate(m Method, s string, length int, tail string) string {
 	//
 	// Once we reach the given length, we start ignoring characters and only
 	// collect ANSI escape codes until we reach the end of string.
-	for i < len(b) {
-		state, action := parser.Table.Transition(pstate, b[i])
+	for i < len(s) {
+		state, action := parser.Table.Transition(pstate, s[i])
 		if state == parser.Utf8State {
 			// This action happens when we transition to the Utf8State.
 			var width int
-			cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
-			if m == WcWidth {
-				width = runewidth.StringWidth(string(cluster))
-			}
-
+			cluster, width = FirstGraphemeCluster(s[i:], m)
 			// increment the index by the length of the cluster
 			i += len(cluster)
 			curWidth += width
@@ -118,7 +113,7 @@ func truncate(m Method, s string, length int, tail string) string {
 				continue
 			}
 
-			buf.Write(cluster)
+			buf.WriteString(cluster)
 
 			// Done collecting, now we're back in the ground state.
 			pstate = parser.GroundState
@@ -152,7 +147,7 @@ func truncate(m Method, s string, length int, tail string) string {
 			}
 			fallthrough
 		default:
-			buf.WriteByte(b[i])
+			buf.WriteByte(s[i])
 			i++
 		}
 
@@ -193,27 +188,23 @@ func truncateLeft(m Method, s string, n int, prefix string) string {
 		return s
 	}
 
-	var cluster []byte
-	var buf bytes.Buffer
+	var cluster string
+	var buf strings.Builder
 	curWidth := 0
 	ignoring := true
 	pstate := parser.GroundState
-	b := []byte(s)
 	i := 0
 
-	for i < len(b) {
+	for i < len(s) {
 		if !ignoring {
-			buf.Write(b[i:])
+			buf.WriteString(s[i:])
 			break
 		}
 
-		state, action := parser.Table.Transition(pstate, b[i])
+		state, action := parser.Table.Transition(pstate, s[i])
 		if state == parser.Utf8State {
 			var width int
-			cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
-			if m == WcWidth {
-				width = runewidth.StringWidth(string(cluster))
-			}
+			cluster, width = FirstGraphemeCluster(s[i:], m)
 
 			i += len(cluster)
 			curWidth += width
@@ -224,7 +215,7 @@ func truncateLeft(m Method, s string, n int, prefix string) string {
 			}
 
 			if curWidth > n {
-				buf.Write(cluster)
+				buf.WriteString(cluster)
 			}
 
 			if ignoring {
@@ -259,7 +250,7 @@ func truncateLeft(m Method, s string, n int, prefix string) string {
 			}
 			fallthrough
 		default:
-			buf.WriteByte(b[i])
+			buf.WriteByte(s[i])
 			i++
 		}
 
@@ -278,22 +269,22 @@ func truncateLeft(m Method, s string, n int, prefix string) string {
 // You can use this with [Truncate], [TruncateLeft], and [Cut].
 func ByteToGraphemeRange(str string, byteStart, byteStop int) (charStart, charStop int) {
 	bytePos, charPos := 0, 0
-	gr := uniseg.NewGraphemes(str)
+	gr := graphemes.FromString(str)
 	for byteStart > bytePos {
 		if !gr.Next() {
 			break
 		}
-		bytePos += len(gr.Str())
-		charPos += max(1, gr.Width())
+		bytePos += len(gr.Value())
+		charPos += max(1, displaywidth.String(gr.Value()))
 	}
 	charStart = charPos
 	for byteStop > bytePos {
 		if !gr.Next() {
 			break
 		}
-		bytePos += len(gr.Str())
-		charPos += max(1, gr.Width())
+		bytePos += len(gr.Value())
+		charPos += max(1, displaywidth.String(gr.Value()))
 	}
 	charStop = charPos
-	return
+	return charStart, charStop
 }
diff --git a/vendor/github.com/charmbracelet/x/ansi/urxvt.go b/vendor/github.com/charmbracelet/x/ansi/urxvt.go
new file mode 100644
index 000000000..b9e86281d
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/ansi/urxvt.go
@@ -0,0 +1,17 @@
+package ansi
+
+import (
+	"fmt"
+	"strings"
+)
+
+// URxvtExt returns an escape sequence for calling a URxvt perl extension with
+// the given name and parameters.
+//
+//	OSC 777 ; extension_name ; param1 ; param2 ; ... ST
+//	OSC 777 ; extension_name ; param1 ; param2 ; ... BEL
+//
+// See: https://man.archlinux.org/man/extra/rxvt-unicode/urxvt.7.en#XTerm_Operating_System_Commands
+func URxvtExt(extension string, params ...string) string {
+	return fmt.Sprintf("\x1b]777;%s;%s\x07", extension, strings.Join(params, ";"))
+}
diff --git a/vendor/github.com/charmbracelet/x/ansi/util.go b/vendor/github.com/charmbracelet/x/ansi/util.go
index 0f5cea787..103f452bb 100644
--- a/vendor/github.com/charmbracelet/x/ansi/util.go
+++ b/vendor/github.com/charmbracelet/x/ansi/util.go
@@ -90,17 +90,3 @@ func XParseColor(s string) color.Color {
 	}
 	return nil
 }
-
-type ordered interface {
-	~int | ~int8 | ~int16 | ~int32 | ~int64 |
-		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
-		~float32 | ~float64 |
-		~string
-}
-
-func max[T ordered](a, b T) T { //nolint:predeclared
-	if a > b {
-		return a
-	}
-	return b
-}
diff --git a/vendor/github.com/charmbracelet/x/ansi/width.go b/vendor/github.com/charmbracelet/x/ansi/width.go
index d0487d350..fd680cfdc 100644
--- a/vendor/github.com/charmbracelet/x/ansi/width.go
+++ b/vendor/github.com/charmbracelet/x/ansi/width.go
@@ -4,8 +4,6 @@ import (
 	"bytes"
 
 	"github.com/charmbracelet/x/ansi/parser"
-	"github.com/mattn/go-runewidth"
-	"github.com/rivo/uniseg"
 )
 
 // Strip removes ANSI escape codes from a string.
@@ -19,7 +17,7 @@ func Strip(s string) string {
 
 	// This implements a subset of the Parser to only collect runes and
 	// printable characters.
-	for i := 0; i < len(s); i++ {
+	for i := range len(s) {
 		if pstate == parser.Utf8State {
 			// During this state, collect rw bytes to form a valid rune in the
 			// buffer. After getting all the rune bytes into the buffer,
@@ -83,20 +81,16 @@ func stringWidth(m Method, s string) int {
 	}
 
 	var (
-		pstate  = parser.GroundState // initial state
-		cluster string
-		width   int
+		pstate = parser.GroundState // initial state
+		width  int
 	)
 
 	for i := 0; i < len(s); i++ {
 		state, action := parser.Table.Transition(pstate, s[i])
 		if state == parser.Utf8State {
-			var w int
-			cluster, _, w, _ = uniseg.FirstGraphemeClusterInString(s[i:], -1)
-			if m == WcWidth {
-				w = runewidth.StringWidth(cluster)
-			}
+			cluster, w := FirstGraphemeCluster(s[i:], m)
 			width += w
+
 			i += len(cluster) - 1
 			pstate = parser.GroundState
 			continue
diff --git a/vendor/github.com/charmbracelet/x/ansi/winop.go b/vendor/github.com/charmbracelet/x/ansi/winop.go
index 0238780d0..8487178b1 100644
--- a/vendor/github.com/charmbracelet/x/ansi/winop.go
+++ b/vendor/github.com/charmbracelet/x/ansi/winop.go
@@ -8,16 +8,22 @@ import (
 const (
 	// ResizeWindowWinOp is a window operation that resizes the terminal
 	// window.
+	//
+	// Deprecated: Use constant number directly with [WindowOp].
 	ResizeWindowWinOp = 4
 
 	// RequestWindowSizeWinOp is a window operation that requests a report of
 	// the size of the terminal window in pixels. The response is in the form:
 	//  CSI 4 ; height ; width t
+	//
+	// Deprecated: Use constant number directly with [WindowOp].
 	RequestWindowSizeWinOp = 14
 
 	// RequestCellSizeWinOp is a window operation that requests a report of
 	// the size of the terminal cell size in pixels. The response is in the form:
 	//  CSI 6 ; height ; width t
+	//
+	// Deprecated: Use constant number directly with [WindowOp].
 	RequestCellSizeWinOp = 16
 )
 
diff --git a/vendor/github.com/charmbracelet/x/ansi/wrap.go b/vendor/github.com/charmbracelet/x/ansi/wrap.go
index 6b9958008..530be6cae 100644
--- a/vendor/github.com/charmbracelet/x/ansi/wrap.go
+++ b/vendor/github.com/charmbracelet/x/ansi/wrap.go
@@ -2,15 +2,14 @@ package ansi
 
 import (
 	"bytes"
+	"strings"
 	"unicode"
 	"unicode/utf8"
 
 	"github.com/charmbracelet/x/ansi/parser"
-	"github.com/mattn/go-runewidth"
-	"github.com/rivo/uniseg"
 )
 
-// nbsp is a non-breaking space
+// nbsp is a non-breaking space.
 const nbsp = 0xA0
 
 // Hardwrap wraps a string or a block of text to a given line length, breaking
@@ -57,10 +56,7 @@ func hardwrap(m Method, s string, limit int, preserveSpace bool) string {
 		state, action := parser.Table.Transition(pstate, b[i])
 		if state == parser.Utf8State {
 			var width int
-			cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
-			if m == WcWidth {
-				width = runewidth.StringWidth(string(cluster))
-			}
+			cluster, width = FirstGraphemeCluster(b[i:], m)
 			i += len(cluster)
 
 			if curWidth+width > limit {
@@ -190,12 +186,9 @@ func wordwrap(m Method, s string, limit int, breakpoints string) string {
 	i := 0
 	for i < len(b) {
 		state, action := parser.Table.Transition(pstate, b[i])
-		if state == parser.Utf8State {
+		if state == parser.Utf8State { //nolint:nestif
 			var width int
-			cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
-			if m == WcWidth {
-				width = runewidth.StringWidth(string(cluster))
-			}
+			cluster, width = FirstGraphemeCluster(b[i:], m)
 			i += len(cluster)
 
 			r, _ := utf8.DecodeRune(cluster)
@@ -303,20 +296,24 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
 	}
 
 	var (
-		cluster  []byte
-		buf      bytes.Buffer
-		word     bytes.Buffer
-		space    bytes.Buffer
-		curWidth int                  // written width of the line
-		wordLen  int                  // word buffer len without ANSI escape codes
-		pstate   = parser.GroundState // initial state
-		b        = []byte(s)
+		cluster    string
+		buf        bytes.Buffer
+		word       bytes.Buffer
+		space      bytes.Buffer
+		spaceWidth int                  // width of the space buffer
+		curWidth   int                  // written width of the line
+		wordLen    int                  // word buffer len without ANSI escape codes
+		pstate     = parser.GroundState // initial state
 	)
 
 	addSpace := func() {
-		curWidth += space.Len()
+		if spaceWidth == 0 && space.Len() == 0 {
+			return
+		}
+		curWidth += spaceWidth
 		buf.Write(space.Bytes())
 		space.Reset()
+		spaceWidth = 0
 	}
 
 	addWord := func() {
@@ -335,32 +332,31 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
 		buf.WriteByte('\n')
 		curWidth = 0
 		space.Reset()
+		spaceWidth = 0
 	}
 
 	i := 0
-	for i < len(b) {
-		state, action := parser.Table.Transition(pstate, b[i])
-		if state == parser.Utf8State {
+	for i < len(s) {
+		state, action := parser.Table.Transition(pstate, s[i])
+		if state == parser.Utf8State { //nolint:nestif
 			var width int
-			cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
-			if m == WcWidth {
-				width = runewidth.StringWidth(string(cluster))
-			}
+			cluster, width = FirstGraphemeCluster(s[i:], m)
 			i += len(cluster)
 
-			r, _ := utf8.DecodeRune(cluster)
+			r, _ := utf8.DecodeRuneInString(cluster)
 			switch {
 			case r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp: // nbsp is a non-breaking space
 				addWord()
 				space.WriteRune(r)
-			case bytes.ContainsAny(cluster, breakpoints):
+				spaceWidth += width
+			case strings.ContainsAny(cluster, breakpoints):
 				addSpace()
 				if curWidth+wordLen+width > limit {
-					word.Write(cluster)
+					word.WriteString(cluster)
 					wordLen += width
 				} else {
 					addWord()
-					buf.Write(cluster)
+					buf.WriteString(cluster)
 					curWidth += width
 				}
 			default:
@@ -369,12 +365,17 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
 					addWord()
 				}
 
-				word.Write(cluster)
+				word.WriteString(cluster)
 				wordLen += width
 
-				if curWidth+wordLen+space.Len() > limit {
+				if curWidth+wordLen+spaceWidth > limit {
 					addNewline()
 				}
+
+				if wordLen == limit {
+					// Hardwrap the word if it's too long
+					addWord()
+				}
 			}
 
 			pstate = parser.GroundState
@@ -383,16 +384,17 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
 
 		switch action {
 		case parser.PrintAction, parser.ExecuteAction:
-			switch r := rune(b[i]); {
+			switch r := rune(s[i]); {
 			case r == '\n':
 				if wordLen == 0 {
-					if curWidth+space.Len() > limit {
+					if curWidth+spaceWidth > limit {
 						curWidth = 0
 					} else {
 						// preserve whitespaces
 						buf.Write(space.Bytes())
 					}
 					space.Reset()
+					spaceWidth = 0
 				}
 
 				addWord()
@@ -400,6 +402,7 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
 			case unicode.IsSpace(r):
 				addWord()
 				space.WriteRune(r)
+				spaceWidth++
 			case r == '-':
 				fallthrough
 			case runeContainsAny(r, breakpoints):
@@ -418,6 +421,7 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
 				if curWidth == limit {
 					addNewline()
 				}
+
 				word.WriteRune(r)
 				wordLen++
 
@@ -426,13 +430,13 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
 					addWord()
 				}
 
-				if curWidth+wordLen+space.Len() > limit {
+				if curWidth+wordLen+spaceWidth > limit {
 					addNewline()
 				}
 			}
 
 		default:
-			word.WriteByte(b[i])
+			word.WriteByte(s[i])
 		}
 
 		// We manage the UTF8 state separately manually above.
@@ -443,13 +447,14 @@ func wrap(m Method, s string, limit int, breakpoints string) string {
 	}
 
 	if wordLen == 0 {
-		if curWidth+space.Len() > limit {
+		if curWidth+spaceWidth > limit {
 			curWidth = 0
 		} else {
 			// preserve whitespaces
 			buf.Write(space.Bytes())
 		}
 		space.Reset()
+		spaceWidth = 0
 	}
 
 	addWord()
diff --git a/vendor/github.com/charmbracelet/x/term/term.go b/vendor/github.com/charmbracelet/x/term/term.go
index 58d6522ca..151e47582 100644
--- a/vendor/github.com/charmbracelet/x/term/term.go
+++ b/vendor/github.com/charmbracelet/x/term/term.go
@@ -1,3 +1,5 @@
+// Package term provides a platform-independent interfaces for interacting with
+// Terminal and TTY devices.
 package term
 
 // State contains platform-specific state of a terminal.
diff --git a/vendor/github.com/charmbracelet/x/term/term_plan9.go b/vendor/github.com/charmbracelet/x/term/term_plan9.go
new file mode 100644
index 000000000..eba3bdf7f
--- /dev/null
+++ b/vendor/github.com/charmbracelet/x/term/term_plan9.go
@@ -0,0 +1,118 @@
+package term
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+type state struct {
+	termName string
+	raw      bool
+	ctl      *os.File
+}
+
+// termName returns the name of the terminal or os.ErrNotExist if there is no terminal.
+func termName(fd uintptr) (string, error) {
+	ctl, err := os.ReadFile(filepath.Join("/fd", fmt.Sprintf("%dctl", fd)))
+	if err != nil {
+		return "", err
+	}
+	f := strings.Fields(string(ctl))
+	if len(f) == 0 {
+		return "", os.ErrNotExist
+	}
+	return f[len(f)-1], nil
+}
+
+func isTerminal(fd uintptr) bool {
+	ctl, err := os.ReadFile(filepath.Join("/fd", fmt.Sprintf("%dctl", fd)))
+	if err != nil {
+		return false
+	}
+	if strings.Contains(string(ctl), "/dev/cons") {
+		return true
+	}
+	return false
+}
+
+func makeRaw(fd uintptr) (*State, error) {
+	t, err := termName(fd)
+	if err != nil {
+		return nil, err
+	}
+	ctl, err := os.OpenFile(t, os.O_RDWR, 0)
+	if err != nil {
+		return nil, err
+	}
+	if _, err := ctl.Write([]byte("rawon")); err != nil {
+		return nil, err
+	}
+	return &State{state: state{termName: t, raw: true, ctl: ctl}}, nil
+}
+
+func getState(fd uintptr) (*State, error) {
+	t, err := termName(fd)
+	if err != nil {
+		return nil, err
+	}
+	ctl, err := os.OpenFile(t, os.O_RDWR, 0)
+	if err != nil {
+		return nil, err
+	}
+	return &State{state: state{termName: t, raw: false, ctl: ctl}}, nil
+
+}
+
+func restore(_ uintptr, state *State) error {
+	if _, err := state.ctl.Write([]byte("rawoff")); err != nil {
+		return err
+	}
+	return nil
+}
+
+// getSize returns the size. This will only work if you are running
+// under a window manager in Plan 9. Else, the only option
+// is to return a reasonable default.
+func getSize(fd uintptr) (int, int, error) {
+	w, h := 80, 40
+	b, err := os.ReadFile("/dev/wctl")
+	if err != nil {
+		return w, h, err
+	}
+	f := strings.Fields(string(b))
+	if len(f) != 4 {
+		return w, h, fmt.Errorf("%q only has %d of 4 needed fields:%w", f, len(f), os.ErrInvalid)
+	}
+	// The contents of wctl, as defined in the driver, are
+	// 4 12-char fields: upper left x, y; and lower-right x, y
+	var ulx, uly, lrx, lry int
+	if n, err := fmt.Sscanf(string(b[:48]), "%d%d%d%d", &ulx, &uly, &lrx, &lry); n != 4 || err != nil {
+		return w, h, fmt.Errorf("scanning %q:%d of 4 items scanned:%w", string(b[:48]), n, err)
+	}
+
+	w, h = lrx-lrx, lry-uly
+	return w, h, nil
+}
+
+func setState(_ uintptr, state *State) error {
+	raw := "rawoff"
+	if state.raw {
+		raw = "rawon"
+	}
+	if _, err := state.ctl.Write([]byte(raw)); err != nil {
+		return err
+	}
+	return nil
+}
+
+func readPassword(fd uintptr) ([]byte, error) {
+	f := os.NewFile(fd, "cons")
+	var b [128]byte
+	n, err := f.Read(b[:])
+	if err != nil {
+		return nil, err
+	}
+	return b[:n], nil
+}
diff --git a/vendor/github.com/charmbracelet/x/term/term_unix.go b/vendor/github.com/charmbracelet/x/term/term_unix.go
index 1459cb1be..77706d48e 100644
--- a/vendor/github.com/charmbracelet/x/term/term_unix.go
+++ b/vendor/github.com/charmbracelet/x/term/term_unix.go
@@ -19,7 +19,7 @@ func isTerminal(fd uintptr) bool {
 func makeRaw(fd uintptr) (*State, error) {
 	termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios)
 	if err != nil {
-		return nil, err
+		return nil, err //nolint:wrapcheck
 	}
 
 	oldState := State{state{Termios: *termios}}
@@ -34,7 +34,7 @@ func makeRaw(fd uintptr) (*State, error) {
 	termios.Cc[unix.VMIN] = 1
 	termios.Cc[unix.VTIME] = 0
 	if err := unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios); err != nil {
-		return nil, err
+		return nil, err //nolint:wrapcheck
 	}
 
 	return &oldState, nil
@@ -45,26 +45,26 @@ func setState(fd uintptr, state *State) error {
 	if state != nil {
 		termios = &state.Termios
 	}
-	return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios)
+	return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios) //nolint:wrapcheck
 }
 
 func getState(fd uintptr) (*State, error) {
 	termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios)
 	if err != nil {
-		return nil, err
+		return nil, err //nolint:wrapcheck
 	}
 
 	return &State{state{Termios: *termios}}, nil
 }
 
 func restore(fd uintptr, state *State) error {
-	return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &state.Termios)
+	return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &state.Termios) //nolint:wrapcheck
 }
 
 func getSize(fd uintptr) (width, height int, err error) {
 	ws, err := unix.IoctlGetWinsize(int(fd), unix.TIOCGWINSZ)
 	if err != nil {
-		return 0, 0, err
+		return 0, 0, err //nolint:wrapcheck
 	}
 	return int(ws.Col), int(ws.Row), nil
 }
@@ -73,13 +73,13 @@ func getSize(fd uintptr) (width, height int, err error) {
 type passwordReader int
 
 func (r passwordReader) Read(buf []byte) (int, error) {
-	return unix.Read(int(r), buf)
+	return unix.Read(int(r), buf) //nolint:wrapcheck
 }
 
 func readPassword(fd uintptr) ([]byte, error) {
 	termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios)
 	if err != nil {
-		return nil, err
+		return nil, err //nolint:wrapcheck
 	}
 
 	newState := *termios
@@ -87,10 +87,10 @@ func readPassword(fd uintptr) ([]byte, error) {
 	newState.Lflag |= unix.ICANON | unix.ISIG
 	newState.Iflag |= unix.ICRNL
 	if err := unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &newState); err != nil {
-		return nil, err
+		return nil, err //nolint:wrapcheck
 	}
 
-	defer unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios)
+	defer unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios) //nolint:errcheck
 
 	return readPasswordLine(passwordReader(fd))
 }
diff --git a/vendor/github.com/charmbracelet/x/term/term_windows.go b/vendor/github.com/charmbracelet/x/term/term_windows.go
index fe7afdec9..d94f68455 100644
--- a/vendor/github.com/charmbracelet/x/term/term_windows.go
+++ b/vendor/github.com/charmbracelet/x/term/term_windows.go
@@ -24,7 +24,7 @@ func makeRaw(fd uintptr) (*State, error) {
 	if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
 		return nil, err
 	}
-	raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT)
+	raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT)
 	raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT
 	if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil {
 		return nil, err
diff --git a/vendor/github.com/charmbracelet/x/term/util.go b/vendor/github.com/charmbracelet/x/term/util.go
index b7313418f..9c2b628bb 100644
--- a/vendor/github.com/charmbracelet/x/term/util.go
+++ b/vendor/github.com/charmbracelet/x/term/util.go
@@ -41,7 +41,7 @@ func readPasswordLine(reader io.Reader) ([]byte, error) {
 			if err == io.EOF && len(ret) > 0 {
 				return ret, nil
 			}
-			return ret, err
+			return ret, err //nolint:wrapcheck
 		}
 	}
 }
diff --git a/vendor/github.com/clipperhouse/displaywidth/.gitignore b/vendor/github.com/clipperhouse/displaywidth/.gitignore
new file mode 100644
index 000000000..e43b0f988
--- /dev/null
+++ b/vendor/github.com/clipperhouse/displaywidth/.gitignore
@@ -0,0 +1 @@
+.DS_Store
diff --git a/vendor/github.com/clipperhouse/displaywidth/AGENTS.md b/vendor/github.com/clipperhouse/displaywidth/AGENTS.md
new file mode 100644
index 000000000..853e2917d
--- /dev/null
+++ b/vendor/github.com/clipperhouse/displaywidth/AGENTS.md
@@ -0,0 +1,37 @@
+The goals and overview of this package can be found in the README.md file,
+start by reading that.
+
+The goal of this package is to determine the display (column) width of a
+string, UTF-8 bytes, or runes, as would happen in a monospace font, especially
+in a terminal.
+
+When troubleshooting, write Go unit tests instead of executing debug scripts.
+The tests can return whatever logs or output you need. If those tests are
+only for temporary troubleshooting, clean up the tests after the debugging is
+done.
+
+(Separate executable debugging scripts are messy, tend to have conflicting
+dependencies and are hard to cleanup.)
+
+If you make changes to the trie generation in internal/gen, it can be invoked
+by running `go generate` from the top package directory.
+
+## Pull Requests and branches
+
+For PRs (pull requests), you can use the gh CLI tool to retrieve details,
+or post comments. Then, compare the current branch with main. Reviewing a PR
+and reviewing a branch are about the same, but the PR may add context.
+
+Look for bugs. Think like GitHub Copilot or Cursor BugBot.
+
+Offer to post a brief summary of the review to the PR, via the gh CLI tool.
+
+## Comparisons to go-runewidth
+
+We originally attempted to make this package compatible with go-runewidth.
+However, we found that there were too many differences in the handling of
+certain characters and properties.
+
+We believe, preliminarily, that our choices are more correct and complete,
+by using more complete categories such as Unicode Cf (format) for zero-width
+and Mn (Nonspacing_Mark) for combining marks.
diff --git a/vendor/github.com/clipperhouse/displaywidth/LICENSE b/vendor/github.com/clipperhouse/displaywidth/LICENSE
new file mode 100644
index 000000000..4b8064eb3
--- /dev/null
+++ b/vendor/github.com/clipperhouse/displaywidth/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Matt Sherman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/clipperhouse/displaywidth/README.md b/vendor/github.com/clipperhouse/displaywidth/README.md
new file mode 100644
index 000000000..209bff798
--- /dev/null
+++ b/vendor/github.com/clipperhouse/displaywidth/README.md
@@ -0,0 +1,113 @@
+# displaywidth
+
+A high-performance Go package for measuring the monospace display width of strings, UTF-8 bytes, and runes.
+
+[![Documentation](https://pkg.go.dev/badge/github.com/clipperhouse/displaywidth.svg)](https://pkg.go.dev/github.com/clipperhouse/displaywidth)
+[![Test](https://github.com/clipperhouse/displaywidth/actions/workflows/gotest.yml/badge.svg)](https://github.com/clipperhouse/displaywidth/actions/workflows/gotest.yml)
+[![Fuzz](https://github.com/clipperhouse/displaywidth/actions/workflows/gofuzz.yml/badge.svg)](https://github.com/clipperhouse/displaywidth/actions/workflows/gofuzz.yml)
+## Install
+```bash
+go get github.com/clipperhouse/displaywidth
+```
+
+## Usage
+
+```go
+package main
+
+import (
+    "fmt"
+    "github.com/clipperhouse/displaywidth"
+)
+
+func main() {
+    width := displaywidth.String("Hello, 世界!")
+    fmt.Println(width)
+
+    width = displaywidth.Bytes([]byte("🌍"))
+    fmt.Println(width)
+
+    width = displaywidth.Rune('🌍')
+    fmt.Println(width)
+}
+```
+
+For most purposes, you should use the `String` or `Bytes` methods.
+
+### Options
+
+You can specify East Asian Width and Strict Emoji Neutral settings. If
+unspecified, the default is `EastAsianWidth: false, StrictEmojiNeutral: true`.
+
+```go
+options := displaywidth.Options{
+    EastAsianWidth:     true,
+    StrictEmojiNeutral: false,
+}
+
+width := options.String("Hello, 世界!")
+fmt.Println(width)
+```
+
+## Details
+
+This package implements the Unicode East Asian Width standard
+([UAX #11](https://www.unicode.org/reports/tr11/)), and handles
+[version selectors](https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)),
+and [regional indicator pairs](https://en.wikipedia.org/wiki/Regional_indicator_symbol)
+(flags). It operates on bytes without decoding runes for better performance.
+
+## Prior Art
+
+[mattn/go-runewidth](https://github.com/mattn/go-runewidth)
+
+[rivo/uniseg](https://github.com/rivo/uniseg)
+
+[x/text/width](https://pkg.go.dev/golang.org/x/text/width)
+
+[x/text/internal/triegen](https://pkg.go.dev/golang.org/x/text/internal/triegen)
+
+## Benchmarks
+
+```bash
+cd comparison
+go test -bench=. -benchmem
+```
+
+```
+goos: darwin
+goarch: arm64
+pkg: github.com/clipperhouse/displaywidth/comparison
+cpu: Apple M2
+BenchmarkStringDefault/clipperhouse/displaywidth-8           11124 ns/op	 151.66 MB/s	  0 B/op	   0 allocs/op
+BenchmarkStringDefault/mattn/go-runewidth-8                  14209 ns/op	 118.73 MB/s	  0 B/op	   0 allocs/op
+BenchmarkStringDefault/rivo/uniseg-8                         19287 ns/op	  87.47 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_EAW/clipperhouse/displaywidth-8              11134 ns/op	 151.52 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_EAW/mattn/go-runewidth-8                     23793 ns/op	  70.90 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_EAW/rivo/uniseg-8                            19593 ns/op	  86.10 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_StrictEmoji/clipperhouse/displaywidth-8      11124 ns/op	 151.65 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_StrictEmoji/mattn/go-runewidth-8             14310 ns/op	 117.89 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_StrictEmoji/rivo/uniseg-8                    19699 ns/op	  85.64 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_ASCII/clipperhouse/displaywidth-8             1107 ns/op	 115.64 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_ASCII/mattn/go-runewidth-8                    1174 ns/op	 109.04 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_ASCII/rivo/uniseg-8                           1582 ns/op	  80.93 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_Unicode/clipperhouse/displaywidth-8            959.7 ns/op	 138.59 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_Unicode/mattn/go-runewidth-8                  1422 ns/op	  93.53 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_Unicode/rivo/uniseg-8                         2032 ns/op	  65.47 MB/s	  0 B/op	   0 allocs/op
+BenchmarkStringWidth_Emoji/clipperhouse/displaywidth-8        3230 ns/op	 224.17 MB/s	  0 B/op	   0 allocs/op
+BenchmarkStringWidth_Emoji/mattn/go-runewidth-8               4826 ns/op	 150.03 MB/s	  0 B/op	   0 allocs/op
+BenchmarkStringWidth_Emoji/rivo/uniseg-8                      6582 ns/op	 109.99 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_Mixed/clipperhouse/displaywidth-8             4094 ns/op	 123.83 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_Mixed/mattn/go-runewidth-8                    4612 ns/op	 109.92 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_Mixed/rivo/uniseg-8                           6312 ns/op	  80.32 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_ControlChars/clipperhouse/displaywidth-8       346.7 ns/op	  95.19 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_ControlChars/mattn/go-runewidth-8              365.0 ns/op	  90.42 MB/s	  0 B/op	   0 allocs/op
+BenchmarkString_ControlChars/rivo/uniseg-8                     408.9 ns/op	  80.70 MB/s	  0 B/op	   0 allocs/op
+```
+
+I use a similar technique in [this grapheme cluster library](https://github.com/clipperhouse/uax29).
+
+## Compatibility
+
+`clipperhouse/displaywidth`, `mattn/go-runewidth`, and `rivo/uniseg` should give the
+same outputs for real-world text. See [comparison/README.md](comparison/README.md).
diff --git a/vendor/github.com/clipperhouse/displaywidth/gen.go b/vendor/github.com/clipperhouse/displaywidth/gen.go
new file mode 100644
index 000000000..52e1085bc
--- /dev/null
+++ b/vendor/github.com/clipperhouse/displaywidth/gen.go
@@ -0,0 +1,3 @@
+package displaywidth
+
+//go:generate go run -C internal/gen .
diff --git a/vendor/github.com/clipperhouse/displaywidth/trie.go b/vendor/github.com/clipperhouse/displaywidth/trie.go
new file mode 100644
index 000000000..9866a7aae
--- /dev/null
+++ b/vendor/github.com/clipperhouse/displaywidth/trie.go
@@ -0,0 +1,1903 @@
+// Code generated by internal/gen/main.go. DO NOT EDIT.
+
+package displaywidth
+
+import "github.com/clipperhouse/stringish"
+
+// property represents the properties of a character as bit flags
+// The underlying type is uint8 since we only use 7 bits for flags.
+type property uint8
+
+const (
+	// Always 2 wide
+	_East_Asian_Full_Wide property = 1 << iota
+	// Width depends on EastAsianWidth option
+	_East_Asian_Ambiguous
+	// Width depends on EastAsianWidth and StrictEmojiNeutral options
+	_Emoji
+	// Always 0 width, includes combining marks, control characters, non-printable, etc
+	_ZeroWidth
+	// VARIATION SELECTOR-15 (U+FE0E) requests text presentation (width 1); not in the trie, see [width]
+	_VS15
+	// VARIATION SELECTOR-16 (U+FE0F) requests emoji presentation (width 2); not in the trie, see [width]
+	_VS16
+	// Regional Indicator Pair (flag) grapheme cluster; not in the trie, see [width]
+	_RI_PAIR
+)
+
+// lookup returns the trie value for the first UTF-8 encoding in s and
+// the width in bytes of this encoding. The size will be 0 if s does not
+// hold enough bytes to complete the encoding. len(s) must be greater than 0.
+func lookup[T stringish.Interface](s T) (v uint8, sz int) {
+	c0 := s[0]
+	switch {
+	case c0 < 0x80: // is ASCII
+		return stringWidthValues[c0], 1
+	case c0 < 0xC2:
+		return 0, 1 // Illegal UTF-8: not a starter, not ASCII.
+	case c0 < 0xE0: // 2-byte UTF-8
+		if len(s) < 2 {
+			return 0, 0
+		}
+		i := stringWidthIndex[c0]
+		c1 := s[1]
+		if c1 < 0x80 || 0xC0 <= c1 {
+			return 0, 1 // Illegal UTF-8: not a continuation byte.
+		}
+		return lookupValue(uint32(i), c1), 2
+	case c0 < 0xF0: // 3-byte UTF-8
+		if len(s) < 3 {
+			return 0, 0
+		}
+		i := stringWidthIndex[c0]
+		c1 := s[1]
+		if c1 < 0x80 || 0xC0 <= c1 {
+			return 0, 1 // Illegal UTF-8: not a continuation byte.
+		}
+		o := uint32(i)<<6 + uint32(c1)
+		i = stringWidthIndex[o]
+		c2 := s[2]
+		if c2 < 0x80 || 0xC0 <= c2 {
+			return 0, 2 // Illegal UTF-8: not a continuation byte.
+		}
+		return lookupValue(uint32(i), c2), 3
+	case c0 < 0xF8: // 4-byte UTF-8
+		if len(s) < 4 {
+			return 0, 0
+		}
+		i := stringWidthIndex[c0]
+		c1 := s[1]
+		if c1 < 0x80 || 0xC0 <= c1 {
+			return 0, 1 // Illegal UTF-8: not a continuation byte.
+		}
+		o := uint32(i)<<6 + uint32(c1)
+		i = stringWidthIndex[o]
+		c2 := s[2]
+		if c2 < 0x80 || 0xC0 <= c2 {
+			return 0, 2 // Illegal UTF-8: not a continuation byte.
+		}
+		o = uint32(i)<<6 + uint32(c2)
+		i = stringWidthIndex[o]
+		c3 := s[3]
+		if c3 < 0x80 || 0xC0 <= c3 {
+			return 0, 3 // Illegal UTF-8: not a continuation byte.
+		}
+		return lookupValue(uint32(i), c3), 4
+	}
+	// Illegal rune
+	return 0, 1
+}
+
+// stringWidthTrie. Total size: 18432 bytes (18.00 KiB). Checksum: dea3fa00409f1ec3.
+// type stringWidthTrie struct { }
+
+// func newStringWidthTrie(i int) *stringWidthTrie {
+// 	return &stringWidthTrie{}
+// }
+
+// lookupValue determines the type of block n and looks up the value for b.
+func lookupValue(n uint32, b byte) uint8 {
+	switch {
+	default:
+		return uint8(stringWidthValues[n<<6+uint32(b)])
+	}
+}
+
+// stringWidthValues: 257 blocks, 16448 entries, 16448 bytes
+// The third block is the zero block.
+var stringWidthValues = [16448]uint8{
+	// Block 0x0, offset 0x0
+	// Block 0x1, offset 0x40
+	// Block 0x2, offset 0x80
+	// Block 0x3, offset 0xc0
+	0xc0: 0x0008, 0xc1: 0x0008, 0xc2: 0x0008, 0xc3: 0x0008, 0xc4: 0x0008, 0xc5: 0x0008,
+	0xc6: 0x0008, 0xc7: 0x0008, 0xc8: 0x0008, 0xc9: 0x0008, 0xca: 0x0008, 0xcb: 0x0008,
+	0xcc: 0x0008, 0xcd: 0x0008, 0xce: 0x0008, 0xcf: 0x0008, 0xd0: 0x0008, 0xd1: 0x0008,
+	0xd2: 0x0008, 0xd3: 0x0008, 0xd4: 0x0008, 0xd5: 0x0008, 0xd6: 0x0008, 0xd7: 0x0008,
+	0xd8: 0x0008, 0xd9: 0x0008, 0xda: 0x0008, 0xdb: 0x0008, 0xdc: 0x0008, 0xdd: 0x0008,
+	0xde: 0x0008, 0xdf: 0x0008, 0xe1: 0x0002,
+	0xe4: 0x0002, 0xe7: 0x0002, 0xe8: 0x0002,
+	0xea: 0x0002, 0xed: 0x000a, 0xee: 0x0002,
+	0xf0: 0x0002, 0xf1: 0x0002, 0xf2: 0x0002, 0xf3: 0x0002, 0xf4: 0x0002,
+	0xf6: 0x0002, 0xf7: 0x0002, 0xf8: 0x0002, 0xf9: 0x0002, 0xfa: 0x0002,
+	0xfc: 0x0002, 0xfd: 0x0002, 0xfe: 0x0002, 0xff: 0x0002,
+	// Block 0x4, offset 0x100
+	0x106: 0x0002,
+	0x110: 0x0002,
+	0x117: 0x0002,
+	0x118: 0x0002,
+	0x11e: 0x0002, 0x11f: 0x0002, 0x120: 0x0002, 0x121: 0x0002,
+	0x126: 0x0002, 0x128: 0x0002, 0x129: 0x0002,
+	0x12a: 0x0002, 0x12c: 0x0002, 0x12d: 0x0002,
+	0x130: 0x0002, 0x132: 0x0002, 0x133: 0x0002,
+	0x137: 0x0002, 0x138: 0x0002, 0x139: 0x0002, 0x13a: 0x0002,
+	0x13c: 0x0002, 0x13e: 0x0002,
+	// Block 0x5, offset 0x140
+	0x141: 0x0002,
+	0x151: 0x0002,
+	0x153: 0x0002,
+	0x15b: 0x0002,
+	0x166: 0x0002, 0x167: 0x0002,
+	0x16b: 0x0002,
+	0x171: 0x0002, 0x172: 0x0002, 0x173: 0x0002,
+	0x178: 0x0002,
+	0x17f: 0x0002,
+	// Block 0x6, offset 0x180
+	0x180: 0x0002, 0x181: 0x0002, 0x182: 0x0002, 0x184: 0x0002,
+	0x188: 0x0002, 0x189: 0x0002, 0x18a: 0x0002, 0x18b: 0x0002,
+	0x18d: 0x0002,
+	0x192: 0x0002, 0x193: 0x0002,
+	0x1a6: 0x0002, 0x1a7: 0x0002,
+	0x1ab: 0x0002,
+	// Block 0x7, offset 0x1c0
+	0x1ce: 0x0002, 0x1d0: 0x0002,
+	0x1d2: 0x0002, 0x1d4: 0x0002, 0x1d6: 0x0002,
+	0x1d8: 0x0002, 0x1da: 0x0002, 0x1dc: 0x0002,
+	// Block 0x8, offset 0x200
+	0x211: 0x0002,
+	0x221: 0x0002,
+	// Block 0x9, offset 0x240
+	0x244: 0x0002,
+	0x247: 0x0002, 0x249: 0x0002, 0x24a: 0x0002, 0x24b: 0x0002,
+	0x24d: 0x0002, 0x250: 0x0002,
+	0x258: 0x0002, 0x259: 0x0002, 0x25a: 0x0002, 0x25b: 0x0002, 0x25d: 0x0002,
+	0x25f: 0x0002,
+	// Block 0xa, offset 0x280
+	0x280: 0x000a, 0x281: 0x000a, 0x282: 0x000a, 0x283: 0x000a, 0x284: 0x000a, 0x285: 0x000a,
+	0x286: 0x000a, 0x287: 0x000a, 0x288: 0x000a, 0x289: 0x000a, 0x28a: 0x000a, 0x28b: 0x000a,
+	0x28c: 0x000a, 0x28d: 0x000a, 0x28e: 0x000a, 0x28f: 0x000a, 0x290: 0x000a, 0x291: 0x000a,
+	0x292: 0x000a, 0x293: 0x000a, 0x294: 0x000a, 0x295: 0x000a, 0x296: 0x000a, 0x297: 0x000a,
+	0x298: 0x000a, 0x299: 0x000a, 0x29a: 0x000a, 0x29b: 0x000a, 0x29c: 0x000a, 0x29d: 0x000a,
+	0x29e: 0x000a, 0x29f: 0x000a, 0x2a0: 0x000a, 0x2a1: 0x000a, 0x2a2: 0x000a, 0x2a3: 0x000a,
+	0x2a4: 0x000a, 0x2a5: 0x000a, 0x2a6: 0x000a, 0x2a7: 0x000a, 0x2a8: 0x000a, 0x2a9: 0x000a,
+	0x2aa: 0x000a, 0x2ab: 0x000a, 0x2ac: 0x000a, 0x2ad: 0x000a, 0x2ae: 0x000a, 0x2af: 0x000a,
+	0x2b0: 0x000a, 0x2b1: 0x000a, 0x2b2: 0x000a, 0x2b3: 0x000a, 0x2b4: 0x000a, 0x2b5: 0x000a,
+	0x2b6: 0x000a, 0x2b7: 0x000a, 0x2b8: 0x000a, 0x2b9: 0x000a, 0x2ba: 0x000a, 0x2bb: 0x000a,
+	0x2bc: 0x000a, 0x2bd: 0x000a, 0x2be: 0x000a, 0x2bf: 0x000a,
+	// Block 0xb, offset 0x2c0
+	0x2c0: 0x000a, 0x2c1: 0x000a, 0x2c2: 0x000a, 0x2c3: 0x000a, 0x2c4: 0x000a, 0x2c5: 0x000a,
+	0x2c6: 0x000a, 0x2c7: 0x000a, 0x2c8: 0x000a, 0x2c9: 0x000a, 0x2ca: 0x000a, 0x2cb: 0x000a,
+	0x2cc: 0x000a, 0x2cd: 0x000a, 0x2ce: 0x000a, 0x2cf: 0x000a, 0x2d0: 0x000a, 0x2d1: 0x000a,
+	0x2d2: 0x000a, 0x2d3: 0x000a, 0x2d4: 0x000a, 0x2d5: 0x000a, 0x2d6: 0x000a, 0x2d7: 0x000a,
+	0x2d8: 0x000a, 0x2d9: 0x000a, 0x2da: 0x000a, 0x2db: 0x000a, 0x2dc: 0x000a, 0x2dd: 0x000a,
+	0x2de: 0x000a, 0x2df: 0x000a, 0x2e0: 0x000a, 0x2e1: 0x000a, 0x2e2: 0x000a, 0x2e3: 0x000a,
+	0x2e4: 0x000a, 0x2e5: 0x000a, 0x2e6: 0x000a, 0x2e7: 0x000a, 0x2e8: 0x000a, 0x2e9: 0x000a,
+	0x2ea: 0x000a, 0x2eb: 0x000a, 0x2ec: 0x000a, 0x2ed: 0x000a, 0x2ee: 0x000a, 0x2ef: 0x000a,
+	// Block 0xc, offset 0x300
+	0x311: 0x0002,
+	0x312: 0x0002, 0x313: 0x0002, 0x314: 0x0002, 0x315: 0x0002, 0x316: 0x0002, 0x317: 0x0002,
+	0x318: 0x0002, 0x319: 0x0002, 0x31a: 0x0002, 0x31b: 0x0002, 0x31c: 0x0002, 0x31d: 0x0002,
+	0x31e: 0x0002, 0x31f: 0x0002, 0x320: 0x0002, 0x321: 0x0002, 0x323: 0x0002,
+	0x324: 0x0002, 0x325: 0x0002, 0x326: 0x0002, 0x327: 0x0002, 0x328: 0x0002, 0x329: 0x0002,
+	0x331: 0x0002, 0x332: 0x0002, 0x333: 0x0002, 0x334: 0x0002, 0x335: 0x0002,
+	0x336: 0x0002, 0x337: 0x0002, 0x338: 0x0002, 0x339: 0x0002, 0x33a: 0x0002, 0x33b: 0x0002,
+	0x33c: 0x0002, 0x33d: 0x0002, 0x33e: 0x0002, 0x33f: 0x0002,
+	// Block 0xd, offset 0x340
+	0x340: 0x0002, 0x341: 0x0002, 0x343: 0x0002, 0x344: 0x0002, 0x345: 0x0002,
+	0x346: 0x0002, 0x347: 0x0002, 0x348: 0x0002, 0x349: 0x0002,
+	// Block 0xe, offset 0x380
+	0x381: 0x0002,
+	0x390: 0x0002, 0x391: 0x0002,
+	0x392: 0x0002, 0x393: 0x0002, 0x394: 0x0002, 0x395: 0x0002, 0x396: 0x0002, 0x397: 0x0002,
+	0x398: 0x0002, 0x399: 0x0002, 0x39a: 0x0002, 0x39b: 0x0002, 0x39c: 0x0002, 0x39d: 0x0002,
+	0x39e: 0x0002, 0x39f: 0x0002, 0x3a0: 0x0002, 0x3a1: 0x0002, 0x3a2: 0x0002, 0x3a3: 0x0002,
+	0x3a4: 0x0002, 0x3a5: 0x0002, 0x3a6: 0x0002, 0x3a7: 0x0002, 0x3a8: 0x0002, 0x3a9: 0x0002,
+	0x3aa: 0x0002, 0x3ab: 0x0002, 0x3ac: 0x0002, 0x3ad: 0x0002, 0x3ae: 0x0002, 0x3af: 0x0002,
+	0x3b0: 0x0002, 0x3b1: 0x0002, 0x3b2: 0x0002, 0x3b3: 0x0002, 0x3b4: 0x0002, 0x3b5: 0x0002,
+	0x3b6: 0x0002, 0x3b7: 0x0002, 0x3b8: 0x0002, 0x3b9: 0x0002, 0x3ba: 0x0002, 0x3bb: 0x0002,
+	0x3bc: 0x0002, 0x3bd: 0x0002, 0x3be: 0x0002, 0x3bf: 0x0002,
+	// Block 0xf, offset 0x3c0
+	0x3c0: 0x0002, 0x3c1: 0x0002, 0x3c2: 0x0002, 0x3c3: 0x0002, 0x3c4: 0x0002, 0x3c5: 0x0002,
+	0x3c6: 0x0002, 0x3c7: 0x0002, 0x3c8: 0x0002, 0x3c9: 0x0002, 0x3ca: 0x0002, 0x3cb: 0x0002,
+	0x3cc: 0x0002, 0x3cd: 0x0002, 0x3ce: 0x0002, 0x3cf: 0x0002, 0x3d1: 0x0002,
+	// Block 0x10, offset 0x400
+	0x403: 0x0008, 0x404: 0x0008, 0x405: 0x0008,
+	0x406: 0x0008, 0x407: 0x0008, 0x408: 0x0008, 0x409: 0x0008,
+	// Block 0x11, offset 0x440
+	0x451: 0x0008,
+	0x452: 0x0008, 0x453: 0x0008, 0x454: 0x0008, 0x455: 0x0008, 0x456: 0x0008, 0x457: 0x0008,
+	0x458: 0x0008, 0x459: 0x0008, 0x45a: 0x0008, 0x45b: 0x0008, 0x45c: 0x0008, 0x45d: 0x0008,
+	0x45e: 0x0008, 0x45f: 0x0008, 0x460: 0x0008, 0x461: 0x0008, 0x462: 0x0008, 0x463: 0x0008,
+	0x464: 0x0008, 0x465: 0x0008, 0x466: 0x0008, 0x467: 0x0008, 0x468: 0x0008, 0x469: 0x0008,
+	0x46a: 0x0008, 0x46b: 0x0008, 0x46c: 0x0008, 0x46d: 0x0008, 0x46e: 0x0008, 0x46f: 0x0008,
+	0x470: 0x0008, 0x471: 0x0008, 0x472: 0x0008, 0x473: 0x0008, 0x474: 0x0008, 0x475: 0x0008,
+	0x476: 0x0008, 0x477: 0x0008, 0x478: 0x0008, 0x479: 0x0008, 0x47a: 0x0008, 0x47b: 0x0008,
+	0x47c: 0x0008, 0x47d: 0x0008, 0x47f: 0x0008,
+	// Block 0x12, offset 0x480
+	0x481: 0x0008, 0x482: 0x0008, 0x484: 0x0008, 0x485: 0x0008,
+	0x487: 0x0008,
+	// Block 0x13, offset 0x4c0
+	0x4c0: 0x0008, 0x4c1: 0x0008, 0x4c2: 0x0008, 0x4c3: 0x0008, 0x4c4: 0x0008, 0x4c5: 0x0008,
+	0x4d0: 0x0008, 0x4d1: 0x0008,
+	0x4d2: 0x0008, 0x4d3: 0x0008, 0x4d4: 0x0008, 0x4d5: 0x0008, 0x4d6: 0x0008, 0x4d7: 0x0008,
+	0x4d8: 0x0008, 0x4d9: 0x0008, 0x4da: 0x0008, 0x4dc: 0x0008,
+	// Block 0x14, offset 0x500
+	0x50b: 0x0008,
+	0x50c: 0x0008, 0x50d: 0x0008, 0x50e: 0x0008, 0x50f: 0x0008, 0x510: 0x0008, 0x511: 0x0008,
+	0x512: 0x0008, 0x513: 0x0008, 0x514: 0x0008, 0x515: 0x0008, 0x516: 0x0008, 0x517: 0x0008,
+	0x518: 0x0008, 0x519: 0x0008, 0x51a: 0x0008, 0x51b: 0x0008, 0x51c: 0x0008, 0x51d: 0x0008,
+	0x51e: 0x0008, 0x51f: 0x0008,
+	0x530: 0x0008,
+	// Block 0x15, offset 0x540
+	0x556: 0x0008, 0x557: 0x0008,
+	0x558: 0x0008, 0x559: 0x0008, 0x55a: 0x0008, 0x55b: 0x0008, 0x55c: 0x0008, 0x55d: 0x0008,
+	0x55f: 0x0008, 0x560: 0x0008, 0x561: 0x0008, 0x562: 0x0008, 0x563: 0x0008,
+	0x564: 0x0008, 0x567: 0x0008, 0x568: 0x0008,
+	0x56a: 0x0008, 0x56b: 0x0008, 0x56c: 0x0008, 0x56d: 0x0008,
+	// Block 0x16, offset 0x580
+	0x58f: 0x0008, 0x591: 0x0008,
+	0x5b0: 0x0008, 0x5b1: 0x0008, 0x5b2: 0x0008, 0x5b3: 0x0008, 0x5b4: 0x0008, 0x5b5: 0x0008,
+	0x5b6: 0x0008, 0x5b7: 0x0008, 0x5b8: 0x0008, 0x5b9: 0x0008, 0x5ba: 0x0008, 0x5bb: 0x0008,
+	0x5bc: 0x0008, 0x5bd: 0x0008, 0x5be: 0x0008, 0x5bf: 0x0008,
+	// Block 0x17, offset 0x5c0
+	0x5c0: 0x0008, 0x5c1: 0x0008, 0x5c2: 0x0008, 0x5c3: 0x0008, 0x5c4: 0x0008, 0x5c5: 0x0008,
+	0x5c6: 0x0008, 0x5c7: 0x0008, 0x5c8: 0x0008, 0x5c9: 0x0008, 0x5ca: 0x0008,
+	// Block 0x18, offset 0x600
+	0x626: 0x0008, 0x627: 0x0008, 0x628: 0x0008, 0x629: 0x0008,
+	0x62a: 0x0008, 0x62b: 0x0008, 0x62c: 0x0008, 0x62d: 0x0008, 0x62e: 0x0008, 0x62f: 0x0008,
+	0x630: 0x0008,
+	// Block 0x19, offset 0x640
+	0x66b: 0x0008, 0x66c: 0x0008, 0x66d: 0x0008, 0x66e: 0x0008, 0x66f: 0x0008,
+	0x670: 0x0008, 0x671: 0x0008, 0x672: 0x0008, 0x673: 0x0008,
+	0x67d: 0x0008,
+	// Block 0x1a, offset 0x680
+	0x696: 0x0008, 0x697: 0x0008,
+	0x698: 0x0008, 0x699: 0x0008, 0x69b: 0x0008, 0x69c: 0x0008, 0x69d: 0x0008,
+	0x69e: 0x0008, 0x69f: 0x0008, 0x6a0: 0x0008, 0x6a1: 0x0008, 0x6a2: 0x0008, 0x6a3: 0x0008,
+	0x6a5: 0x0008, 0x6a6: 0x0008, 0x6a7: 0x0008, 0x6a9: 0x0008,
+	0x6aa: 0x0008, 0x6ab: 0x0008, 0x6ac: 0x0008, 0x6ad: 0x0008,
+	// Block 0x1b, offset 0x6c0
+	0x6d9: 0x0008, 0x6da: 0x0008, 0x6db: 0x0008,
+	// Block 0x1c, offset 0x700
+	0x710: 0x0008, 0x711: 0x0008,
+	0x718: 0x0008, 0x719: 0x0008, 0x71a: 0x0008, 0x71b: 0x0008, 0x71c: 0x0008, 0x71d: 0x0008,
+	0x71e: 0x0008, 0x71f: 0x0008,
+	// Block 0x1d, offset 0x740
+	0x74a: 0x0008, 0x74b: 0x0008,
+	0x74c: 0x0008, 0x74d: 0x0008, 0x74e: 0x0008, 0x74f: 0x0008, 0x750: 0x0008, 0x751: 0x0008,
+	0x752: 0x0008, 0x753: 0x0008, 0x754: 0x0008, 0x755: 0x0008, 0x756: 0x0008, 0x757: 0x0008,
+	0x758: 0x0008, 0x759: 0x0008, 0x75a: 0x0008, 0x75b: 0x0008, 0x75c: 0x0008, 0x75d: 0x0008,
+	0x75e: 0x0008, 0x75f: 0x0008, 0x760: 0x0008, 0x761: 0x0008, 0x762: 0x0008, 0x763: 0x0008,
+	0x764: 0x0008, 0x765: 0x0008, 0x766: 0x0008, 0x767: 0x0008, 0x768: 0x0008, 0x769: 0x0008,
+	0x76a: 0x0008, 0x76b: 0x0008, 0x76c: 0x0008, 0x76d: 0x0008, 0x76e: 0x0008, 0x76f: 0x0008,
+	0x770: 0x0008, 0x771: 0x0008, 0x772: 0x0008, 0x773: 0x0008, 0x774: 0x0008, 0x775: 0x0008,
+	0x776: 0x0008, 0x777: 0x0008, 0x778: 0x0008, 0x779: 0x0008, 0x77a: 0x0008, 0x77b: 0x0008,
+	0x77c: 0x0008, 0x77d: 0x0008, 0x77e: 0x0008, 0x77f: 0x0008,
+	// Block 0x1e, offset 0x780
+	0x780: 0x0008, 0x781: 0x0008, 0x782: 0x0008,
+	0x7ba: 0x0008,
+	0x7bc: 0x0008,
+	// Block 0x1f, offset 0x7c0
+	0x7c1: 0x0008, 0x7c2: 0x0008, 0x7c3: 0x0008, 0x7c4: 0x0008, 0x7c5: 0x0008,
+	0x7c6: 0x0008, 0x7c7: 0x0008, 0x7c8: 0x0008,
+	0x7cd: 0x0008, 0x7d1: 0x0008,
+	0x7d2: 0x0008, 0x7d3: 0x0008, 0x7d4: 0x0008, 0x7d5: 0x0008, 0x7d6: 0x0008, 0x7d7: 0x0008,
+	0x7e2: 0x0008, 0x7e3: 0x0008,
+	// Block 0x20, offset 0x800
+	0x801: 0x0008,
+	0x83c: 0x0008,
+	// Block 0x21, offset 0x840
+	0x841: 0x0008, 0x842: 0x0008, 0x843: 0x0008, 0x844: 0x0008,
+	0x84d: 0x0008,
+	0x862: 0x0008, 0x863: 0x0008,
+	0x87e: 0x0008,
+	// Block 0x22, offset 0x880
+	0x881: 0x0008, 0x882: 0x0008,
+	0x8bc: 0x0008,
+	// Block 0x23, offset 0x8c0
+	0x8c1: 0x0008, 0x8c2: 0x0008,
+	0x8c7: 0x0008, 0x8c8: 0x0008, 0x8cb: 0x0008,
+	0x8cc: 0x0008, 0x8cd: 0x0008, 0x8d1: 0x0008,
+	0x8f0: 0x0008, 0x8f1: 0x0008, 0x8f5: 0x0008,
+	// Block 0x24, offset 0x900
+	0x901: 0x0008, 0x902: 0x0008, 0x903: 0x0008, 0x904: 0x0008, 0x905: 0x0008,
+	0x907: 0x0008, 0x908: 0x0008,
+	0x90d: 0x0008,
+	0x922: 0x0008, 0x923: 0x0008,
+	0x93a: 0x0008, 0x93b: 0x0008,
+	0x93c: 0x0008, 0x93d: 0x0008, 0x93e: 0x0008, 0x93f: 0x0008,
+	// Block 0x25, offset 0x940
+	0x941: 0x0008,
+	0x97c: 0x0008, 0x97f: 0x0008,
+	// Block 0x26, offset 0x980
+	0x981: 0x0008, 0x982: 0x0008, 0x983: 0x0008, 0x984: 0x0008,
+	0x98d: 0x0008,
+	0x995: 0x0008, 0x996: 0x0008,
+	0x9a2: 0x0008, 0x9a3: 0x0008,
+	// Block 0x27, offset 0x9c0
+	0x9c2: 0x0008,
+	// Block 0x28, offset 0xa00
+	0xa00: 0x0008,
+	0xa0d: 0x0008,
+	// Block 0x29, offset 0xa40
+	0xa40: 0x0008, 0xa44: 0x0008,
+	0xa7c: 0x0008, 0xa7e: 0x0008, 0xa7f: 0x0008,
+	// Block 0x2a, offset 0xa80
+	0xa80: 0x0008,
+	0xa86: 0x0008, 0xa87: 0x0008, 0xa88: 0x0008, 0xa8a: 0x0008, 0xa8b: 0x0008,
+	0xa8c: 0x0008, 0xa8d: 0x0008,
+	0xa95: 0x0008, 0xa96: 0x0008,
+	0xaa2: 0x0008, 0xaa3: 0x0008,
+	// Block 0x2b, offset 0xac0
+	0xac6: 0x0008,
+	0xacc: 0x0008, 0xacd: 0x0008,
+	0xae2: 0x0008, 0xae3: 0x0008,
+	// Block 0x2c, offset 0xb00
+	0xb00: 0x0008, 0xb01: 0x0008,
+	0xb3b: 0x0008,
+	0xb3c: 0x0008,
+	// Block 0x2d, offset 0xb40
+	0xb41: 0x0008, 0xb42: 0x0008, 0xb43: 0x0008, 0xb44: 0x0008,
+	0xb4d: 0x0008,
+	0xb62: 0x0008, 0xb63: 0x0008,
+	// Block 0x2e, offset 0xb80
+	0xb81: 0x0008,
+	// Block 0x2f, offset 0xbc0
+	0xbca: 0x0008,
+	0xbd2: 0x0008, 0xbd3: 0x0008, 0xbd4: 0x0008, 0xbd6: 0x0008,
+	// Block 0x30, offset 0xc00
+	0xc31: 0x0008, 0xc34: 0x0008, 0xc35: 0x0008,
+	0xc36: 0x0008, 0xc37: 0x0008, 0xc38: 0x0008, 0xc39: 0x0008, 0xc3a: 0x0008,
+	// Block 0x31, offset 0xc40
+	0xc47: 0x0008, 0xc48: 0x0008, 0xc49: 0x0008, 0xc4a: 0x0008, 0xc4b: 0x0008,
+	0xc4c: 0x0008, 0xc4d: 0x0008, 0xc4e: 0x0008,
+	// Block 0x32, offset 0xc80
+	0xcb1: 0x0008, 0xcb4: 0x0008, 0xcb5: 0x0008,
+	0xcb6: 0x0008, 0xcb7: 0x0008, 0xcb8: 0x0008, 0xcb9: 0x0008, 0xcba: 0x0008, 0xcbb: 0x0008,
+	0xcbc: 0x0008,
+	// Block 0x33, offset 0xcc0
+	0xcc8: 0x0008, 0xcc9: 0x0008, 0xcca: 0x0008, 0xccb: 0x0008,
+	0xccc: 0x0008, 0xccd: 0x0008, 0xcce: 0x0008,
+	// Block 0x34, offset 0xd00
+	0xd18: 0x0008, 0xd19: 0x0008,
+	0xd35: 0x0008,
+	0xd37: 0x0008, 0xd39: 0x0008,
+	// Block 0x35, offset 0xd40
+	0xd71: 0x0008, 0xd72: 0x0008, 0xd73: 0x0008, 0xd74: 0x0008, 0xd75: 0x0008,
+	0xd76: 0x0008, 0xd77: 0x0008, 0xd78: 0x0008, 0xd79: 0x0008, 0xd7a: 0x0008, 0xd7b: 0x0008,
+	0xd7c: 0x0008, 0xd7d: 0x0008, 0xd7e: 0x0008,
+	// Block 0x36, offset 0xd80
+	0xd80: 0x0008, 0xd81: 0x0008, 0xd82: 0x0008, 0xd83: 0x0008, 0xd84: 0x0008,
+	0xd86: 0x0008, 0xd87: 0x0008,
+	0xd8d: 0x0008, 0xd8e: 0x0008, 0xd8f: 0x0008, 0xd90: 0x0008, 0xd91: 0x0008,
+	0xd92: 0x0008, 0xd93: 0x0008, 0xd94: 0x0008, 0xd95: 0x0008, 0xd96: 0x0008, 0xd97: 0x0008,
+	0xd99: 0x0008, 0xd9a: 0x0008, 0xd9b: 0x0008, 0xd9c: 0x0008, 0xd9d: 0x0008,
+	0xd9e: 0x0008, 0xd9f: 0x0008, 0xda0: 0x0008, 0xda1: 0x0008, 0xda2: 0x0008, 0xda3: 0x0008,
+	0xda4: 0x0008, 0xda5: 0x0008, 0xda6: 0x0008, 0xda7: 0x0008, 0xda8: 0x0008, 0xda9: 0x0008,
+	0xdaa: 0x0008, 0xdab: 0x0008, 0xdac: 0x0008, 0xdad: 0x0008, 0xdae: 0x0008, 0xdaf: 0x0008,
+	0xdb0: 0x0008, 0xdb1: 0x0008, 0xdb2: 0x0008, 0xdb3: 0x0008, 0xdb4: 0x0008, 0xdb5: 0x0008,
+	0xdb6: 0x0008, 0xdb7: 0x0008, 0xdb8: 0x0008, 0xdb9: 0x0008, 0xdba: 0x0008, 0xdbb: 0x0008,
+	0xdbc: 0x0008,
+	// Block 0x37, offset 0xdc0
+	0xdc6: 0x0008,
+	// Block 0x38, offset 0xe00
+	0xe2d: 0x0008, 0xe2e: 0x0008, 0xe2f: 0x0008,
+	0xe30: 0x0008, 0xe32: 0x0008, 0xe33: 0x0008, 0xe34: 0x0008, 0xe35: 0x0008,
+	0xe36: 0x0008, 0xe37: 0x0008, 0xe39: 0x0008, 0xe3a: 0x0008,
+	0xe3d: 0x0008, 0xe3e: 0x0008,
+	// Block 0x39, offset 0xe40
+	0xe58: 0x0008, 0xe59: 0x0008,
+	0xe5e: 0x0008, 0xe5f: 0x0008, 0xe60: 0x0008,
+	0xe71: 0x0008, 0xe72: 0x0008, 0xe73: 0x0008, 0xe74: 0x0008,
+	// Block 0x3a, offset 0xe80
+	0xe82: 0x0008, 0xe85: 0x0008,
+	0xe86: 0x0008,
+	0xe8d: 0x0008,
+	0xe9d: 0x0008,
+	// Block 0x3b, offset 0xec0
+	0xec0: 0x0001, 0xec1: 0x0001, 0xec2: 0x0001, 0xec3: 0x0001, 0xec4: 0x0001, 0xec5: 0x0001,
+	0xec6: 0x0001, 0xec7: 0x0001, 0xec8: 0x0001, 0xec9: 0x0001, 0xeca: 0x0001, 0xecb: 0x0001,
+	0xecc: 0x0001, 0xecd: 0x0001, 0xece: 0x0001, 0xecf: 0x0001, 0xed0: 0x0001, 0xed1: 0x0001,
+	0xed2: 0x0001, 0xed3: 0x0001, 0xed4: 0x0001, 0xed5: 0x0001, 0xed6: 0x0001, 0xed7: 0x0001,
+	0xed8: 0x0001, 0xed9: 0x0001, 0xeda: 0x0001, 0xedb: 0x0001, 0xedc: 0x0001, 0xedd: 0x0001,
+	0xede: 0x0001, 0xedf: 0x0001, 0xee0: 0x0001, 0xee1: 0x0001, 0xee2: 0x0001, 0xee3: 0x0001,
+	0xee4: 0x0001, 0xee5: 0x0001, 0xee6: 0x0001, 0xee7: 0x0001, 0xee8: 0x0001, 0xee9: 0x0001,
+	0xeea: 0x0001, 0xeeb: 0x0001, 0xeec: 0x0001, 0xeed: 0x0001, 0xeee: 0x0001, 0xeef: 0x0001,
+	0xef0: 0x0001, 0xef1: 0x0001, 0xef2: 0x0001, 0xef3: 0x0001, 0xef4: 0x0001, 0xef5: 0x0001,
+	0xef6: 0x0001, 0xef7: 0x0001, 0xef8: 0x0001, 0xef9: 0x0001, 0xefa: 0x0001, 0xefb: 0x0001,
+	0xefc: 0x0001, 0xefd: 0x0001, 0xefe: 0x0001, 0xeff: 0x0001,
+	// Block 0x3c, offset 0xf00
+	0xf00: 0x0001, 0xf01: 0x0001, 0xf02: 0x0001, 0xf03: 0x0001, 0xf04: 0x0001, 0xf05: 0x0001,
+	0xf06: 0x0001, 0xf07: 0x0001, 0xf08: 0x0001, 0xf09: 0x0001, 0xf0a: 0x0001, 0xf0b: 0x0001,
+	0xf0c: 0x0001, 0xf0d: 0x0001, 0xf0e: 0x0001, 0xf0f: 0x0001, 0xf10: 0x0001, 0xf11: 0x0001,
+	0xf12: 0x0001, 0xf13: 0x0001, 0xf14: 0x0001, 0xf15: 0x0001, 0xf16: 0x0001, 0xf17: 0x0001,
+	0xf18: 0x0001, 0xf19: 0x0001, 0xf1a: 0x0001, 0xf1b: 0x0001, 0xf1c: 0x0001, 0xf1d: 0x0001,
+	0xf1e: 0x0001, 0xf1f: 0x0001,
+	// Block 0x3d, offset 0xf40
+	0xf5d: 0x0008,
+	0xf5e: 0x0008, 0xf5f: 0x0008,
+	// Block 0x3e, offset 0xf80
+	0xf92: 0x0008, 0xf93: 0x0008, 0xf94: 0x0008,
+	0xfb2: 0x0008, 0xfb3: 0x0008,
+	// Block 0x3f, offset 0xfc0
+	0xfd2: 0x0008, 0xfd3: 0x0008,
+	0xff2: 0x0008, 0xff3: 0x0008,
+	// Block 0x40, offset 0x1000
+	0x1034: 0x0008, 0x1035: 0x0008,
+	0x1037: 0x0008, 0x1038: 0x0008, 0x1039: 0x0008, 0x103a: 0x0008, 0x103b: 0x0008,
+	0x103c: 0x0008, 0x103d: 0x0008,
+	// Block 0x41, offset 0x1040
+	0x1046: 0x0008, 0x1049: 0x0008, 0x104a: 0x0008, 0x104b: 0x0008,
+	0x104c: 0x0008, 0x104d: 0x0008, 0x104e: 0x0008, 0x104f: 0x0008, 0x1050: 0x0008, 0x1051: 0x0008,
+	0x1052: 0x0008, 0x1053: 0x0008,
+	0x105d: 0x0008,
+	// Block 0x42, offset 0x1080
+	0x108b: 0x0008,
+	0x108c: 0x0008, 0x108d: 0x0008, 0x108e: 0x0008, 0x108f: 0x0008,
+	// Block 0x43, offset 0x10c0
+	0x10c5: 0x0008,
+	0x10c6: 0x0008,
+	0x10e9: 0x0008,
+	// Block 0x44, offset 0x1100
+	0x1120: 0x0008, 0x1121: 0x0008, 0x1122: 0x0008,
+	0x1127: 0x0008, 0x1128: 0x0008,
+	0x1132: 0x0008,
+	0x1139: 0x0008, 0x113a: 0x0008, 0x113b: 0x0008,
+	// Block 0x45, offset 0x1140
+	0x1157: 0x0008,
+	0x1158: 0x0008, 0x115b: 0x0008,
+	// Block 0x46, offset 0x1180
+	0x1196: 0x0008,
+	0x1198: 0x0008, 0x1199: 0x0008, 0x119a: 0x0008, 0x119b: 0x0008, 0x119c: 0x0008, 0x119d: 0x0008,
+	0x119e: 0x0008, 0x11a0: 0x0008, 0x11a2: 0x0008,
+	0x11a5: 0x0008, 0x11a6: 0x0008, 0x11a7: 0x0008, 0x11a8: 0x0008, 0x11a9: 0x0008,
+	0x11aa: 0x0008, 0x11ab: 0x0008, 0x11ac: 0x0008,
+	0x11b3: 0x0008, 0x11b4: 0x0008, 0x11b5: 0x0008,
+	0x11b6: 0x0008, 0x11b7: 0x0008, 0x11b8: 0x0008, 0x11b9: 0x0008, 0x11ba: 0x0008, 0x11bb: 0x0008,
+	0x11bc: 0x0008, 0x11bf: 0x0008,
+	// Block 0x47, offset 0x11c0
+	0x11f0: 0x0008, 0x11f1: 0x0008, 0x11f2: 0x0008, 0x11f3: 0x0008, 0x11f4: 0x0008, 0x11f5: 0x0008,
+	0x11f6: 0x0008, 0x11f7: 0x0008, 0x11f8: 0x0008, 0x11f9: 0x0008, 0x11fa: 0x0008, 0x11fb: 0x0008,
+	0x11fc: 0x0008, 0x11fd: 0x0008, 0x11fe: 0x0008, 0x11ff: 0x0008,
+	// Block 0x48, offset 0x1200
+	0x1200: 0x0008, 0x1201: 0x0008, 0x1202: 0x0008, 0x1203: 0x0008, 0x1204: 0x0008, 0x1205: 0x0008,
+	0x1206: 0x0008, 0x1207: 0x0008, 0x1208: 0x0008, 0x1209: 0x0008, 0x120a: 0x0008, 0x120b: 0x0008,
+	0x120c: 0x0008, 0x120d: 0x0008, 0x120e: 0x0008,
+	// Block 0x49, offset 0x1240
+	0x1240: 0x0008, 0x1241: 0x0008, 0x1242: 0x0008, 0x1243: 0x0008,
+	0x1274: 0x0008,
+	0x1276: 0x0008, 0x1277: 0x0008, 0x1278: 0x0008, 0x1279: 0x0008, 0x127a: 0x0008,
+	0x127c: 0x0008,
+	// Block 0x4a, offset 0x1280
+	0x1282: 0x0008,
+	0x12ab: 0x0008, 0x12ac: 0x0008, 0x12ad: 0x0008, 0x12ae: 0x0008, 0x12af: 0x0008,
+	0x12b0: 0x0008, 0x12b1: 0x0008, 0x12b2: 0x0008, 0x12b3: 0x0008,
+	// Block 0x4b, offset 0x12c0
+	0x12c0: 0x0008, 0x12c1: 0x0008,
+	0x12e2: 0x0008, 0x12e3: 0x0008,
+	0x12e4: 0x0008, 0x12e5: 0x0008, 0x12e8: 0x0008, 0x12e9: 0x0008,
+	0x12eb: 0x0008, 0x12ec: 0x0008, 0x12ed: 0x0008,
+	// Block 0x4c, offset 0x1300
+	0x1326: 0x0008, 0x1328: 0x0008, 0x1329: 0x0008,
+	0x132d: 0x0008, 0x132f: 0x0008,
+	0x1330: 0x0008, 0x1331: 0x0008,
+	// Block 0x4d, offset 0x1340
+	0x136c: 0x0008, 0x136d: 0x0008, 0x136e: 0x0008, 0x136f: 0x0008,
+	0x1370: 0x0008, 0x1371: 0x0008, 0x1372: 0x0008, 0x1373: 0x0008,
+	0x1376: 0x0008, 0x1377: 0x0008,
+	// Block 0x4e, offset 0x1380
+	0x1390: 0x0008, 0x1391: 0x0008,
+	0x1392: 0x0008, 0x1394: 0x0008, 0x1395: 0x0008, 0x1396: 0x0008, 0x1397: 0x0008,
+	0x1398: 0x0008, 0x1399: 0x0008, 0x139a: 0x0008, 0x139b: 0x0008, 0x139c: 0x0008, 0x139d: 0x0008,
+	0x139e: 0x0008, 0x139f: 0x0008, 0x13a0: 0x0008, 0x13a2: 0x0008, 0x13a3: 0x0008,
+	0x13a4: 0x0008, 0x13a5: 0x0008, 0x13a6: 0x0008, 0x13a7: 0x0008, 0x13a8: 0x0008,
+	0x13ad: 0x0008,
+	0x13b4: 0x0008,
+	0x13b8: 0x0008, 0x13b9: 0x0008,
+	// Block 0x4f, offset 0x13c0
+	0x13c0: 0x0008, 0x13c1: 0x0008, 0x13c2: 0x0008, 0x13c3: 0x0008, 0x13c4: 0x0008, 0x13c5: 0x0008,
+	0x13c6: 0x0008, 0x13c7: 0x0008, 0x13c8: 0x0008, 0x13c9: 0x0008, 0x13ca: 0x0008, 0x13cb: 0x0008,
+	0x13cc: 0x0008, 0x13cd: 0x0008, 0x13ce: 0x0008, 0x13cf: 0x0008, 0x13d0: 0x0008, 0x13d1: 0x0008,
+	0x13d2: 0x0008, 0x13d3: 0x0008, 0x13d4: 0x0008, 0x13d5: 0x0008, 0x13d6: 0x0008, 0x13d7: 0x0008,
+	0x13d8: 0x0008, 0x13d9: 0x0008, 0x13da: 0x0008, 0x13db: 0x0008, 0x13dc: 0x0008, 0x13dd: 0x0008,
+	0x13de: 0x0008, 0x13df: 0x0008, 0x13e0: 0x0008, 0x13e1: 0x0008, 0x13e2: 0x0008, 0x13e3: 0x0008,
+	0x13e4: 0x0008, 0x13e5: 0x0008, 0x13e6: 0x0008, 0x13e7: 0x0008, 0x13e8: 0x0008, 0x13e9: 0x0008,
+	0x13ea: 0x0008, 0x13eb: 0x0008, 0x13ec: 0x0008, 0x13ed: 0x0008, 0x13ee: 0x0008, 0x13ef: 0x0008,
+	0x13f0: 0x0008, 0x13f1: 0x0008, 0x13f2: 0x0008, 0x13f3: 0x0008, 0x13f4: 0x0008, 0x13f5: 0x0008,
+	0x13f6: 0x0008, 0x13f7: 0x0008, 0x13f8: 0x0008, 0x13f9: 0x0008, 0x13fa: 0x0008, 0x13fb: 0x0008,
+	0x13fc: 0x0008, 0x13fd: 0x0008, 0x13fe: 0x0008, 0x13ff: 0x0008,
+	// Block 0x50, offset 0x1400
+	0x140b: 0x0008,
+	0x140c: 0x0008, 0x140d: 0x0008, 0x140e: 0x0008, 0x140f: 0x0008, 0x1410: 0x0002,
+	0x1413: 0x0002, 0x1414: 0x0002, 0x1415: 0x0002, 0x1416: 0x0002,
+	0x1418: 0x0002, 0x1419: 0x0002, 0x141c: 0x0002, 0x141d: 0x0002,
+	0x1420: 0x0002, 0x1421: 0x0002, 0x1422: 0x0002,
+	0x1424: 0x0002, 0x1425: 0x0002, 0x1426: 0x0002, 0x1427: 0x0002, 0x1428: 0x0008, 0x1429: 0x0008,
+	0x142a: 0x0008, 0x142b: 0x0008, 0x142c: 0x0008, 0x142d: 0x0008, 0x142e: 0x0008,
+	0x1430: 0x0002, 0x1432: 0x0002, 0x1433: 0x0002, 0x1435: 0x0002,
+	0x143b: 0x0002,
+	0x143c: 0x0004, 0x143e: 0x0002,
+	// Block 0x51, offset 0x1440
+	0x1449: 0x0004,
+	0x1460: 0x0008, 0x1461: 0x0008, 0x1462: 0x0008, 0x1463: 0x0008,
+	0x1464: 0x0008, 0x1466: 0x0008, 0x1467: 0x0008, 0x1468: 0x0008, 0x1469: 0x0008,
+	0x146a: 0x0008, 0x146b: 0x0008, 0x146c: 0x0008, 0x146d: 0x0008, 0x146e: 0x0008, 0x146f: 0x0008,
+	0x1474: 0x0002,
+	0x147f: 0x0002,
+	// Block 0x52, offset 0x1480
+	0x1481: 0x0002, 0x1482: 0x0002, 0x1483: 0x0002, 0x1484: 0x0002,
+	0x14ac: 0x0002,
+	// Block 0x53, offset 0x14c0
+	0x14d0: 0x0008, 0x14d1: 0x0008,
+	0x14d2: 0x0008, 0x14d3: 0x0008, 0x14d4: 0x0008, 0x14d5: 0x0008, 0x14d6: 0x0008, 0x14d7: 0x0008,
+	0x14d8: 0x0008, 0x14d9: 0x0008, 0x14da: 0x0008, 0x14db: 0x0008, 0x14dc: 0x0008, 0x14dd: 0x0008,
+	0x14de: 0x0008, 0x14df: 0x0008, 0x14e0: 0x0008, 0x14e1: 0x0008, 0x14e2: 0x0008, 0x14e3: 0x0008,
+	0x14e4: 0x0008, 0x14e5: 0x0008, 0x14e6: 0x0008, 0x14e7: 0x0008, 0x14e8: 0x0008, 0x14e9: 0x0008,
+	0x14ea: 0x0008, 0x14eb: 0x0008, 0x14ec: 0x0008, 0x14ed: 0x0008, 0x14ee: 0x0008, 0x14ef: 0x0008,
+	0x14f0: 0x0008,
+	// Block 0x54, offset 0x1500
+	0x1503: 0x0002, 0x1505: 0x0002,
+	0x1509: 0x0002,
+	0x1513: 0x0002, 0x1516: 0x0002,
+	0x1521: 0x0002, 0x1522: 0x0006,
+	0x1526: 0x0002,
+	0x152b: 0x0002,
+	0x1539: 0x0004,
+	// Block 0x55, offset 0x1540
+	0x1553: 0x0002, 0x1554: 0x0002,
+	0x155b: 0x0002, 0x155c: 0x0002, 0x155d: 0x0002,
+	0x155e: 0x0002, 0x1560: 0x0002, 0x1561: 0x0002, 0x1562: 0x0002, 0x1563: 0x0002,
+	0x1564: 0x0002, 0x1565: 0x0002, 0x1566: 0x0002, 0x1567: 0x0002, 0x1568: 0x0002, 0x1569: 0x0002,
+	0x156a: 0x0002, 0x156b: 0x0002,
+	0x1570: 0x0002, 0x1571: 0x0002, 0x1572: 0x0002, 0x1573: 0x0002, 0x1574: 0x0002, 0x1575: 0x0002,
+	0x1576: 0x0002, 0x1577: 0x0002, 0x1578: 0x0002, 0x1579: 0x0002,
+	// Block 0x56, offset 0x1580
+	0x1589: 0x0002,
+	0x1590: 0x0002, 0x1591: 0x0002,
+	0x1592: 0x0002, 0x1593: 0x0002, 0x1594: 0x0006, 0x1595: 0x0006, 0x1596: 0x0006, 0x1597: 0x0006,
+	0x1598: 0x0006, 0x1599: 0x0006,
+	0x15a9: 0x0004,
+	0x15aa: 0x0004,
+	0x15b8: 0x0002, 0x15b9: 0x0002,
+	// Block 0x57, offset 0x15c0
+	0x15d2: 0x0002, 0x15d4: 0x0002,
+	0x15e7: 0x0002,
+	// Block 0x58, offset 0x1600
+	0x1600: 0x0002, 0x1602: 0x0002, 0x1603: 0x0002,
+	0x1607: 0x0002, 0x1608: 0x0002, 0x160b: 0x0002,
+	0x160f: 0x0002, 0x1611: 0x0002,
+	0x1615: 0x0002,
+	0x161a: 0x0002, 0x161d: 0x0002,
+	0x161e: 0x0002, 0x161f: 0x0002, 0x1620: 0x0002, 0x1623: 0x0002,
+	0x1625: 0x0002, 0x1627: 0x0002, 0x1628: 0x0002, 0x1629: 0x0002,
+	0x162a: 0x0002, 0x162b: 0x0002, 0x162c: 0x0002, 0x162e: 0x0002,
+	0x1634: 0x0002, 0x1635: 0x0002,
+	0x1636: 0x0002, 0x1637: 0x0002,
+	0x163c: 0x0002, 0x163d: 0x0002,
+	// Block 0x59, offset 0x1640
+	0x1648: 0x0002,
+	0x164c: 0x0002,
+	0x1652: 0x0002,
+	0x1660: 0x0002, 0x1661: 0x0002,
+	0x1664: 0x0002, 0x1665: 0x0002, 0x1666: 0x0002, 0x1667: 0x0002,
+	0x166a: 0x0002, 0x166b: 0x0002, 0x166e: 0x0002, 0x166f: 0x0002,
+	// Block 0x5a, offset 0x1680
+	0x1682: 0x0002, 0x1683: 0x0002,
+	0x1686: 0x0002, 0x1687: 0x0002,
+	0x1695: 0x0002,
+	0x1699: 0x0002,
+	0x16a5: 0x0002,
+	0x16bf: 0x0002,
+	// Block 0x5b, offset 0x16c0
+	0x16d2: 0x0002,
+	0x16da: 0x0005, 0x16db: 0x0005,
+	0x16e8: 0x0004, 0x16e9: 0x0001,
+	0x16ea: 0x0001,
+	// Block 0x5c, offset 0x1700
+	0x1708: 0x0004,
+	// Block 0x5d, offset 0x1740
+	0x174f: 0x0004,
+	0x1769: 0x0005,
+	0x176a: 0x0005, 0x176b: 0x0005, 0x176c: 0x0005, 0x176d: 0x0004, 0x176e: 0x0004, 0x176f: 0x0004,
+	0x1770: 0x0005, 0x1771: 0x0004, 0x1772: 0x0004, 0x1773: 0x0005,
+	0x1778: 0x0004, 0x1779: 0x0004, 0x177a: 0x0004,
+	// Block 0x5e, offset 0x1780
+	0x17a0: 0x0002, 0x17a1: 0x0002, 0x17a2: 0x0002, 0x17a3: 0x0002,
+	0x17a4: 0x0002, 0x17a5: 0x0002, 0x17a6: 0x0002, 0x17a7: 0x0002, 0x17a8: 0x0002, 0x17a9: 0x0002,
+	0x17aa: 0x0002, 0x17ab: 0x0002, 0x17ac: 0x0002, 0x17ad: 0x0002, 0x17ae: 0x0002, 0x17af: 0x0002,
+	0x17b0: 0x0002, 0x17b1: 0x0002, 0x17b2: 0x0002, 0x17b3: 0x0002, 0x17b4: 0x0002, 0x17b5: 0x0002,
+	0x17b6: 0x0002, 0x17b7: 0x0002, 0x17b8: 0x0002, 0x17b9: 0x0002, 0x17ba: 0x0002, 0x17bb: 0x0002,
+	0x17bc: 0x0002, 0x17bd: 0x0002, 0x17be: 0x0002, 0x17bf: 0x0002,
+	// Block 0x5f, offset 0x17c0
+	0x17c0: 0x0002, 0x17c1: 0x0002, 0x17c2: 0x0002, 0x17c3: 0x0002, 0x17c4: 0x0002, 0x17c5: 0x0002,
+	0x17c6: 0x0002, 0x17c7: 0x0002, 0x17c8: 0x0002, 0x17c9: 0x0002, 0x17ca: 0x0002, 0x17cb: 0x0002,
+	0x17cc: 0x0002, 0x17cd: 0x0002, 0x17ce: 0x0002, 0x17cf: 0x0002, 0x17d0: 0x0002, 0x17d1: 0x0002,
+	0x17d2: 0x0002, 0x17d3: 0x0002, 0x17d4: 0x0002, 0x17d5: 0x0002, 0x17d6: 0x0002, 0x17d7: 0x0002,
+	0x17d8: 0x0002, 0x17d9: 0x0002, 0x17da: 0x0002, 0x17db: 0x0002, 0x17dc: 0x0002, 0x17dd: 0x0002,
+	0x17de: 0x0002, 0x17df: 0x0002, 0x17e0: 0x0002, 0x17e1: 0x0002, 0x17e2: 0x0002, 0x17e3: 0x0002,
+	0x17e4: 0x0002, 0x17e5: 0x0002, 0x17e6: 0x0002, 0x17e7: 0x0002, 0x17e8: 0x0002, 0x17e9: 0x0002,
+	0x17ea: 0x0002, 0x17eb: 0x0002, 0x17ec: 0x0002, 0x17ed: 0x0002, 0x17ee: 0x0002, 0x17ef: 0x0002,
+	0x17f0: 0x0002, 0x17f1: 0x0002, 0x17f2: 0x0002, 0x17f3: 0x0002, 0x17f4: 0x0002, 0x17f5: 0x0002,
+	0x17f6: 0x0002, 0x17f7: 0x0002, 0x17f8: 0x0002, 0x17f9: 0x0002, 0x17fa: 0x0002, 0x17fb: 0x0002,
+	0x17fc: 0x0002, 0x17fd: 0x0002, 0x17fe: 0x0002, 0x17ff: 0x0002,
+	// Block 0x60, offset 0x1800
+	0x1800: 0x0002, 0x1801: 0x0002, 0x1802: 0x0006, 0x1803: 0x0002, 0x1804: 0x0002, 0x1805: 0x0002,
+	0x1806: 0x0002, 0x1807: 0x0002, 0x1808: 0x0002, 0x1809: 0x0002, 0x180a: 0x0002, 0x180b: 0x0002,
+	0x180c: 0x0002, 0x180d: 0x0002, 0x180e: 0x0002, 0x180f: 0x0002, 0x1810: 0x0002, 0x1811: 0x0002,
+	0x1812: 0x0002, 0x1813: 0x0002, 0x1814: 0x0002, 0x1815: 0x0002, 0x1816: 0x0002, 0x1817: 0x0002,
+	0x1818: 0x0002, 0x1819: 0x0002, 0x181a: 0x0002, 0x181b: 0x0002, 0x181c: 0x0002, 0x181d: 0x0002,
+	0x181e: 0x0002, 0x181f: 0x0002, 0x1820: 0x0002, 0x1821: 0x0002, 0x1822: 0x0002, 0x1823: 0x0002,
+	0x1824: 0x0002, 0x1825: 0x0002, 0x1826: 0x0002, 0x1827: 0x0002, 0x1828: 0x0002, 0x1829: 0x0002,
+	0x182b: 0x0002, 0x182c: 0x0002, 0x182d: 0x0002, 0x182e: 0x0002, 0x182f: 0x0002,
+	0x1830: 0x0002, 0x1831: 0x0002, 0x1832: 0x0002, 0x1833: 0x0002, 0x1834: 0x0002, 0x1835: 0x0002,
+	0x1836: 0x0002, 0x1837: 0x0002, 0x1838: 0x0002, 0x1839: 0x0002, 0x183a: 0x0002, 0x183b: 0x0002,
+	0x183c: 0x0002, 0x183d: 0x0002, 0x183e: 0x0002, 0x183f: 0x0002,
+	// Block 0x61, offset 0x1840
+	0x1840: 0x0002, 0x1841: 0x0002, 0x1842: 0x0002, 0x1843: 0x0002, 0x1844: 0x0002, 0x1845: 0x0002,
+	0x1846: 0x0002, 0x1847: 0x0002, 0x1848: 0x0002, 0x1849: 0x0002, 0x184a: 0x0002, 0x184b: 0x0002,
+	0x1850: 0x0002, 0x1851: 0x0002,
+	0x1852: 0x0002, 0x1853: 0x0002, 0x1854: 0x0002, 0x1855: 0x0002, 0x1856: 0x0002, 0x1857: 0x0002,
+	0x1858: 0x0002, 0x1859: 0x0002, 0x185a: 0x0002, 0x185b: 0x0002, 0x185c: 0x0002, 0x185d: 0x0002,
+	0x185e: 0x0002, 0x185f: 0x0002, 0x1860: 0x0002, 0x1861: 0x0002, 0x1862: 0x0002, 0x1863: 0x0002,
+	0x1864: 0x0002, 0x1865: 0x0002, 0x1866: 0x0002, 0x1867: 0x0002, 0x1868: 0x0002, 0x1869: 0x0002,
+	0x186a: 0x0002, 0x186b: 0x0002, 0x186c: 0x0002, 0x186d: 0x0002, 0x186e: 0x0002, 0x186f: 0x0002,
+	0x1870: 0x0002, 0x1871: 0x0002, 0x1872: 0x0002, 0x1873: 0x0002,
+	// Block 0x62, offset 0x1880
+	0x1880: 0x0002, 0x1881: 0x0002, 0x1882: 0x0002, 0x1883: 0x0002, 0x1884: 0x0002, 0x1885: 0x0002,
+	0x1886: 0x0002, 0x1887: 0x0002, 0x1888: 0x0002, 0x1889: 0x0002, 0x188a: 0x0002, 0x188b: 0x0002,
+	0x188c: 0x0002, 0x188d: 0x0002, 0x188e: 0x0002, 0x188f: 0x0002,
+	0x1892: 0x0002, 0x1893: 0x0002, 0x1894: 0x0002, 0x1895: 0x0002,
+	0x18a0: 0x0002, 0x18a1: 0x0002, 0x18a3: 0x0002,
+	0x18a4: 0x0002, 0x18a5: 0x0002, 0x18a6: 0x0002, 0x18a7: 0x0002, 0x18a8: 0x0002, 0x18a9: 0x0002,
+	0x18aa: 0x0004, 0x18ab: 0x0004,
+	0x18b2: 0x0002, 0x18b3: 0x0002,
+	0x18b6: 0x0006, 0x18b7: 0x0002,
+	0x18bc: 0x0002, 0x18bd: 0x0002,
+	// Block 0x63, offset 0x18c0
+	0x18c0: 0x0006, 0x18c1: 0x0002,
+	0x18c6: 0x0002, 0x18c7: 0x0002, 0x18c8: 0x0002, 0x18cb: 0x0002,
+	0x18ce: 0x0002, 0x18cf: 0x0002, 0x18d0: 0x0002, 0x18d1: 0x0002,
+	0x18e2: 0x0002, 0x18e3: 0x0002,
+	0x18e4: 0x0002, 0x18e5: 0x0002,
+	0x18ef: 0x0002,
+	0x18fb: 0x0004,
+	0x18fc: 0x0004, 0x18fd: 0x0005, 0x18fe: 0x0005,
+	// Block 0x64, offset 0x1900
+	0x1900: 0x0004, 0x1901: 0x0004, 0x1902: 0x0004, 0x1903: 0x0004, 0x1904: 0x0004, 0x1905: 0x0006,
+	0x1906: 0x0002, 0x1907: 0x0004, 0x1908: 0x0004, 0x1909: 0x0006, 0x190a: 0x0004, 0x190b: 0x0004,
+	0x190c: 0x0004, 0x190d: 0x0004, 0x190e: 0x0006, 0x190f: 0x0006, 0x1910: 0x0004, 0x1911: 0x0004,
+	0x1912: 0x0004, 0x1914: 0x0005, 0x1915: 0x0005, 0x1916: 0x0004, 0x1917: 0x0004,
+	0x1918: 0x0004, 0x1919: 0x0004, 0x191a: 0x0004, 0x191b: 0x0004, 0x191c: 0x0006, 0x191d: 0x0004,
+	0x191e: 0x0006, 0x191f: 0x0004, 0x1920: 0x0004, 0x1921: 0x0004, 0x1922: 0x0004, 0x1923: 0x0004,
+	0x1924: 0x0004, 0x1925: 0x0004, 0x1926: 0x0004, 0x1927: 0x0004, 0x1928: 0x0004, 0x1929: 0x0004,
+	0x192a: 0x0004, 0x192b: 0x0004, 0x192c: 0x0004, 0x192d: 0x0004, 0x192e: 0x0004, 0x192f: 0x0004,
+	0x1930: 0x0004, 0x1931: 0x0004, 0x1932: 0x0004, 0x1933: 0x0004, 0x1934: 0x0004, 0x1935: 0x0004,
+	0x1936: 0x0004, 0x1937: 0x0004, 0x1938: 0x0004, 0x1939: 0x0004, 0x193a: 0x0004, 0x193b: 0x0004,
+	0x193c: 0x0004, 0x193d: 0x0004, 0x193e: 0x0004, 0x193f: 0x0004,
+	// Block 0x65, offset 0x1940
+	0x1940: 0x0006, 0x1941: 0x0004, 0x1942: 0x0006, 0x1943: 0x0004, 0x1944: 0x0004, 0x1945: 0x0004,
+	0x1946: 0x0004, 0x1947: 0x0004, 0x1948: 0x0005, 0x1949: 0x0005, 0x194a: 0x0005, 0x194b: 0x0005,
+	0x194c: 0x0005, 0x194d: 0x0005, 0x194e: 0x0005, 0x194f: 0x0005, 0x1950: 0x0005, 0x1951: 0x0005,
+	0x1952: 0x0005, 0x1953: 0x0005, 0x1954: 0x0004, 0x1955: 0x0004, 0x1956: 0x0004, 0x1957: 0x0004,
+	0x1958: 0x0004, 0x1959: 0x0004, 0x195a: 0x0004, 0x195b: 0x0004, 0x195c: 0x0004, 0x195d: 0x0004,
+	0x195e: 0x0004, 0x195f: 0x0004, 0x1960: 0x0006, 0x1961: 0x0006, 0x1962: 0x0004, 0x1963: 0x0006,
+	0x1964: 0x0006, 0x1965: 0x0006, 0x1966: 0x0004, 0x1967: 0x0006, 0x1968: 0x0006, 0x1969: 0x0006,
+	0x196a: 0x0006, 0x196b: 0x0004, 0x196c: 0x0006, 0x196d: 0x0006, 0x196e: 0x0004, 0x196f: 0x0006,
+	0x1970: 0x0004, 0x1971: 0x0004, 0x1972: 0x0004, 0x1973: 0x0004, 0x1974: 0x0004, 0x1975: 0x0004,
+	0x1976: 0x0004, 0x1977: 0x0004, 0x1978: 0x0004, 0x1979: 0x0004, 0x197a: 0x0004, 0x197b: 0x0004,
+	0x197c: 0x0004, 0x197d: 0x0004, 0x197e: 0x0004, 0x197f: 0x0005,
+	// Block 0x66, offset 0x1980
+	0x1980: 0x0004, 0x1981: 0x0004, 0x1982: 0x0004, 0x1983: 0x0004, 0x1984: 0x0004, 0x1985: 0x0004,
+	0x1990: 0x0004, 0x1991: 0x0004,
+	0x1992: 0x0004, 0x1993: 0x0005, 0x1994: 0x0004, 0x1995: 0x0004, 0x1996: 0x0004, 0x1997: 0x0004,
+	0x1998: 0x0004, 0x1999: 0x0004, 0x199a: 0x0004, 0x199b: 0x0004, 0x199c: 0x0004, 0x199d: 0x0004,
+	0x199e: 0x0006, 0x199f: 0x0006, 0x19a0: 0x0004, 0x19a1: 0x0005, 0x19a2: 0x0004, 0x19a3: 0x0004,
+	0x19a4: 0x0004, 0x19a5: 0x0004, 0x19a6: 0x0004, 0x19a7: 0x0004, 0x19a8: 0x0004, 0x19a9: 0x0004,
+	0x19aa: 0x0005, 0x19ab: 0x0005, 0x19ac: 0x0004, 0x19ad: 0x0004, 0x19ae: 0x0004, 0x19af: 0x0004,
+	0x19b0: 0x0004, 0x19b1: 0x0004, 0x19b2: 0x0004, 0x19b3: 0x0004, 0x19b4: 0x0004, 0x19b5: 0x0004,
+	0x19b6: 0x0004, 0x19b7: 0x0004, 0x19b8: 0x0004, 0x19b9: 0x0004, 0x19ba: 0x0004, 0x19bb: 0x0004,
+	0x19bc: 0x0004, 0x19bd: 0x0005, 0x19be: 0x0005, 0x19bf: 0x0006,
+	// Block 0x67, offset 0x19c0
+	0x19c0: 0x0004, 0x19c1: 0x0004, 0x19c2: 0x0004, 0x19c3: 0x0004, 0x19c4: 0x0005, 0x19c5: 0x0005,
+	0x19c6: 0x0006, 0x19c7: 0x0006, 0x19c8: 0x0006, 0x19c9: 0x0006, 0x19ca: 0x0006, 0x19cb: 0x0006,
+	0x19cc: 0x0006, 0x19cd: 0x0006, 0x19ce: 0x0005, 0x19cf: 0x0006, 0x19d0: 0x0006, 0x19d1: 0x0006,
+	0x19d2: 0x0006, 0x19d3: 0x0006, 0x19d4: 0x0005, 0x19d5: 0x0006, 0x19d6: 0x0006, 0x19d7: 0x0006,
+	0x19d8: 0x0006, 0x19d9: 0x0006, 0x19da: 0x0006, 0x19db: 0x0006, 0x19dc: 0x0006, 0x19dd: 0x0006,
+	0x19de: 0x0006, 0x19df: 0x0006, 0x19e0: 0x0006, 0x19e1: 0x0006, 0x19e2: 0x0004, 0x19e3: 0x0006,
+	0x19e4: 0x0004, 0x19e5: 0x0004, 0x19e6: 0x0004, 0x19e7: 0x0004, 0x19e8: 0x0006, 0x19e9: 0x0006,
+	0x19ea: 0x0005, 0x19eb: 0x0006, 0x19ec: 0x0006, 0x19ed: 0x0006, 0x19ee: 0x0006, 0x19ef: 0x0006,
+	0x19f0: 0x0006, 0x19f1: 0x0006, 0x19f2: 0x0005, 0x19f3: 0x0005, 0x19f4: 0x0006, 0x19f5: 0x0005,
+	0x19f6: 0x0006, 0x19f7: 0x0006, 0x19f8: 0x0006, 0x19f9: 0x0006, 0x19fa: 0x0005, 0x19fb: 0x0006,
+	0x19fc: 0x0006, 0x19fd: 0x0005, 0x19fe: 0x0006, 0x19ff: 0x0006,
+	// Block 0x68, offset 0x1a00
+	0x1a00: 0x0004, 0x1a01: 0x0004, 0x1a02: 0x0004, 0x1a03: 0x0004, 0x1a04: 0x0004, 0x1a05: 0x0005,
+	0x1a08: 0x0004, 0x1a09: 0x0004, 0x1a0a: 0x0005, 0x1a0b: 0x0005,
+	0x1a0c: 0x0004, 0x1a0d: 0x0004, 0x1a0e: 0x0004, 0x1a0f: 0x0004, 0x1a10: 0x0004, 0x1a11: 0x0004,
+	0x1a12: 0x0004, 0x1a14: 0x0004, 0x1a16: 0x0004,
+	0x1a1d: 0x0004,
+	0x1a21: 0x0004,
+	0x1a28: 0x0005,
+	0x1a33: 0x0004, 0x1a34: 0x0004,
+	0x1a3d: 0x0002,
+	// Block 0x69, offset 0x1a40
+	0x1a44: 0x0004,
+	0x1a47: 0x0004,
+	0x1a4c: 0x0005, 0x1a4e: 0x0005,
+	0x1a53: 0x0005, 0x1a54: 0x0005, 0x1a55: 0x0005, 0x1a57: 0x0005,
+	0x1a63: 0x0004,
+	0x1a64: 0x0004, 0x1a65: 0x0004, 0x1a66: 0x0004, 0x1a67: 0x0004,
+	0x1a76: 0x0002, 0x1a77: 0x0002, 0x1a78: 0x0002, 0x1a79: 0x0002, 0x1a7a: 0x0002, 0x1a7b: 0x0002,
+	0x1a7c: 0x0002, 0x1a7d: 0x0002, 0x1a7e: 0x0002, 0x1a7f: 0x0002,
+	// Block 0x6a, offset 0x1a80
+	0x1a95: 0x0005, 0x1a96: 0x0005, 0x1a97: 0x0005,
+	0x1aa1: 0x0004,
+	0x1ab0: 0x0005,
+	0x1abf: 0x0005,
+	// Block 0x6b, offset 0x1ac0
+	0x1af4: 0x0004, 0x1af5: 0x0004,
+	// Block 0x6c, offset 0x1b00
+	0x1b05: 0x0004,
+	0x1b06: 0x0004, 0x1b07: 0x0004,
+	0x1b1b: 0x0005, 0x1b1c: 0x0005,
+	// Block 0x6d, offset 0x1b40
+	0x1b50: 0x0005,
+	0x1b55: 0x0005, 0x1b56: 0x0002, 0x1b57: 0x0002,
+	0x1b58: 0x0002, 0x1b59: 0x0002,
+	// Block 0x6e, offset 0x1b80
+	0x1baf: 0x0008,
+	0x1bb0: 0x0008, 0x1bb1: 0x0008,
+	// Block 0x6f, offset 0x1bc0
+	0x1bff: 0x0008,
+	// Block 0x70, offset 0x1c00
+	0x1c20: 0x0008, 0x1c21: 0x0008, 0x1c22: 0x0008, 0x1c23: 0x0008,
+	0x1c24: 0x0008, 0x1c25: 0x0008, 0x1c26: 0x0008, 0x1c27: 0x0008, 0x1c28: 0x0008, 0x1c29: 0x0008,
+	0x1c2a: 0x0008, 0x1c2b: 0x0008, 0x1c2c: 0x0008, 0x1c2d: 0x0008, 0x1c2e: 0x0008, 0x1c2f: 0x0008,
+	0x1c30: 0x0008, 0x1c31: 0x0008, 0x1c32: 0x0008, 0x1c33: 0x0008, 0x1c34: 0x0008, 0x1c35: 0x0008,
+	0x1c36: 0x0008, 0x1c37: 0x0008, 0x1c38: 0x0008, 0x1c39: 0x0008, 0x1c3a: 0x0008, 0x1c3b: 0x0008,
+	0x1c3c: 0x0008, 0x1c3d: 0x0008, 0x1c3e: 0x0008, 0x1c3f: 0x0008,
+	// Block 0x71, offset 0x1c40
+	0x1c40: 0x0001, 0x1c41: 0x0001, 0x1c42: 0x0001, 0x1c43: 0x0001, 0x1c44: 0x0001, 0x1c45: 0x0001,
+	0x1c46: 0x0001, 0x1c47: 0x0001, 0x1c48: 0x0001, 0x1c49: 0x0001, 0x1c4a: 0x0001, 0x1c4b: 0x0001,
+	0x1c4c: 0x0001, 0x1c4d: 0x0001, 0x1c4e: 0x0001, 0x1c4f: 0x0001, 0x1c50: 0x0001, 0x1c51: 0x0001,
+	0x1c52: 0x0001, 0x1c53: 0x0001, 0x1c54: 0x0001, 0x1c55: 0x0001, 0x1c56: 0x0001, 0x1c57: 0x0001,
+	0x1c58: 0x0001, 0x1c59: 0x0001, 0x1c5b: 0x0001, 0x1c5c: 0x0001, 0x1c5d: 0x0001,
+	0x1c5e: 0x0001, 0x1c5f: 0x0001, 0x1c60: 0x0001, 0x1c61: 0x0001, 0x1c62: 0x0001, 0x1c63: 0x0001,
+	0x1c64: 0x0001, 0x1c65: 0x0001, 0x1c66: 0x0001, 0x1c67: 0x0001, 0x1c68: 0x0001, 0x1c69: 0x0001,
+	0x1c6a: 0x0001, 0x1c6b: 0x0001, 0x1c6c: 0x0001, 0x1c6d: 0x0001, 0x1c6e: 0x0001, 0x1c6f: 0x0001,
+	0x1c70: 0x0001, 0x1c71: 0x0001, 0x1c72: 0x0001, 0x1c73: 0x0001, 0x1c74: 0x0001, 0x1c75: 0x0001,
+	0x1c76: 0x0001, 0x1c77: 0x0001, 0x1c78: 0x0001, 0x1c79: 0x0001, 0x1c7a: 0x0001, 0x1c7b: 0x0001,
+	0x1c7c: 0x0001, 0x1c7d: 0x0001, 0x1c7e: 0x0001, 0x1c7f: 0x0001,
+	// Block 0x72, offset 0x1c80
+	0x1c80: 0x0001, 0x1c81: 0x0001, 0x1c82: 0x0001, 0x1c83: 0x0001, 0x1c84: 0x0001, 0x1c85: 0x0001,
+	0x1c86: 0x0001, 0x1c87: 0x0001, 0x1c88: 0x0001, 0x1c89: 0x0001, 0x1c8a: 0x0001, 0x1c8b: 0x0001,
+	0x1c8c: 0x0001, 0x1c8d: 0x0001, 0x1c8e: 0x0001, 0x1c8f: 0x0001, 0x1c90: 0x0001, 0x1c91: 0x0001,
+	0x1c92: 0x0001, 0x1c93: 0x0001, 0x1c94: 0x0001, 0x1c95: 0x0001, 0x1c96: 0x0001, 0x1c97: 0x0001,
+	0x1c98: 0x0001, 0x1c99: 0x0001, 0x1c9a: 0x0001, 0x1c9b: 0x0001, 0x1c9c: 0x0001, 0x1c9d: 0x0001,
+	0x1c9e: 0x0001, 0x1c9f: 0x0001, 0x1ca0: 0x0001, 0x1ca1: 0x0001, 0x1ca2: 0x0001, 0x1ca3: 0x0001,
+	0x1ca4: 0x0001, 0x1ca5: 0x0001, 0x1ca6: 0x0001, 0x1ca7: 0x0001, 0x1ca8: 0x0001, 0x1ca9: 0x0001,
+	0x1caa: 0x0001, 0x1cab: 0x0001, 0x1cac: 0x0001, 0x1cad: 0x0001, 0x1cae: 0x0001, 0x1caf: 0x0001,
+	0x1cb0: 0x0001, 0x1cb1: 0x0001, 0x1cb2: 0x0001, 0x1cb3: 0x0001,
+	// Block 0x73, offset 0x1cc0
+	0x1cc0: 0x0001, 0x1cc1: 0x0001, 0x1cc2: 0x0001, 0x1cc3: 0x0001, 0x1cc4: 0x0001, 0x1cc5: 0x0001,
+	0x1cc6: 0x0001, 0x1cc7: 0x0001, 0x1cc8: 0x0001, 0x1cc9: 0x0001, 0x1cca: 0x0001, 0x1ccb: 0x0001,
+	0x1ccc: 0x0001, 0x1ccd: 0x0001, 0x1cce: 0x0001, 0x1ccf: 0x0001, 0x1cd0: 0x0001, 0x1cd1: 0x0001,
+	0x1cd2: 0x0001, 0x1cd3: 0x0001, 0x1cd4: 0x0001, 0x1cd5: 0x0001,
+	0x1cf0: 0x0001, 0x1cf1: 0x0001, 0x1cf2: 0x0001, 0x1cf3: 0x0001, 0x1cf4: 0x0001, 0x1cf5: 0x0001,
+	0x1cf6: 0x0001, 0x1cf7: 0x0001, 0x1cf8: 0x0001, 0x1cf9: 0x0001, 0x1cfa: 0x0001, 0x1cfb: 0x0001,
+	0x1cfc: 0x0001, 0x1cfd: 0x0001, 0x1cfe: 0x0001, 0x1cff: 0x0001,
+	// Block 0x74, offset 0x1d00
+	0x1d00: 0x0001, 0x1d01: 0x0001, 0x1d02: 0x0001, 0x1d03: 0x0001, 0x1d04: 0x0001, 0x1d05: 0x0001,
+	0x1d06: 0x0001, 0x1d07: 0x0001, 0x1d08: 0x0001, 0x1d09: 0x0001, 0x1d0a: 0x0001, 0x1d0b: 0x0001,
+	0x1d0c: 0x0001, 0x1d0d: 0x0001, 0x1d0e: 0x0001, 0x1d0f: 0x0001, 0x1d10: 0x0001, 0x1d11: 0x0001,
+	0x1d12: 0x0001, 0x1d13: 0x0001, 0x1d14: 0x0001, 0x1d15: 0x0001, 0x1d16: 0x0001, 0x1d17: 0x0001,
+	0x1d18: 0x0001, 0x1d19: 0x0001, 0x1d1a: 0x0001, 0x1d1b: 0x0001, 0x1d1c: 0x0001, 0x1d1d: 0x0001,
+	0x1d1e: 0x0001, 0x1d1f: 0x0001, 0x1d20: 0x0001, 0x1d21: 0x0001, 0x1d22: 0x0001, 0x1d23: 0x0001,
+	0x1d24: 0x0001, 0x1d25: 0x0001, 0x1d26: 0x0001, 0x1d27: 0x0001, 0x1d28: 0x0001, 0x1d29: 0x0001,
+	0x1d2a: 0x0009, 0x1d2b: 0x0009, 0x1d2c: 0x0009, 0x1d2d: 0x0009, 0x1d2e: 0x0001, 0x1d2f: 0x0001,
+	0x1d30: 0x0005, 0x1d31: 0x0001, 0x1d32: 0x0001, 0x1d33: 0x0001, 0x1d34: 0x0001, 0x1d35: 0x0001,
+	0x1d36: 0x0001, 0x1d37: 0x0001, 0x1d38: 0x0001, 0x1d39: 0x0001, 0x1d3a: 0x0001, 0x1d3b: 0x0001,
+	0x1d3c: 0x0001, 0x1d3d: 0x0005, 0x1d3e: 0x0001,
+	// Block 0x75, offset 0x1d40
+	0x1d41: 0x0001, 0x1d42: 0x0001, 0x1d43: 0x0001, 0x1d44: 0x0001, 0x1d45: 0x0001,
+	0x1d46: 0x0001, 0x1d47: 0x0001, 0x1d48: 0x0001, 0x1d49: 0x0001, 0x1d4a: 0x0001, 0x1d4b: 0x0001,
+	0x1d4c: 0x0001, 0x1d4d: 0x0001, 0x1d4e: 0x0001, 0x1d4f: 0x0001, 0x1d50: 0x0001, 0x1d51: 0x0001,
+	0x1d52: 0x0001, 0x1d53: 0x0001, 0x1d54: 0x0001, 0x1d55: 0x0001, 0x1d56: 0x0001, 0x1d57: 0x0001,
+	0x1d58: 0x0001, 0x1d59: 0x0001, 0x1d5a: 0x0001, 0x1d5b: 0x0001, 0x1d5c: 0x0001, 0x1d5d: 0x0001,
+	0x1d5e: 0x0001, 0x1d5f: 0x0001, 0x1d60: 0x0001, 0x1d61: 0x0001, 0x1d62: 0x0001, 0x1d63: 0x0001,
+	0x1d64: 0x0001, 0x1d65: 0x0001, 0x1d66: 0x0001, 0x1d67: 0x0001, 0x1d68: 0x0001, 0x1d69: 0x0001,
+	0x1d6a: 0x0001, 0x1d6b: 0x0001, 0x1d6c: 0x0001, 0x1d6d: 0x0001, 0x1d6e: 0x0001, 0x1d6f: 0x0001,
+	0x1d70: 0x0001, 0x1d71: 0x0001, 0x1d72: 0x0001, 0x1d73: 0x0001, 0x1d74: 0x0001, 0x1d75: 0x0001,
+	0x1d76: 0x0001, 0x1d77: 0x0001, 0x1d78: 0x0001, 0x1d79: 0x0001, 0x1d7a: 0x0001, 0x1d7b: 0x0001,
+	0x1d7c: 0x0001, 0x1d7d: 0x0001, 0x1d7e: 0x0001, 0x1d7f: 0x0001,
+	// Block 0x76, offset 0x1d80
+	0x1d80: 0x0001, 0x1d81: 0x0001, 0x1d82: 0x0001, 0x1d83: 0x0001, 0x1d84: 0x0001, 0x1d85: 0x0001,
+	0x1d86: 0x0001, 0x1d87: 0x0001, 0x1d88: 0x0001, 0x1d89: 0x0001, 0x1d8a: 0x0001, 0x1d8b: 0x0001,
+	0x1d8c: 0x0001, 0x1d8d: 0x0001, 0x1d8e: 0x0001, 0x1d8f: 0x0001, 0x1d90: 0x0001, 0x1d91: 0x0001,
+	0x1d92: 0x0001, 0x1d93: 0x0001, 0x1d94: 0x0001, 0x1d95: 0x0001, 0x1d96: 0x0001,
+	0x1d99: 0x0009, 0x1d9a: 0x0009, 0x1d9b: 0x0001, 0x1d9c: 0x0001, 0x1d9d: 0x0001,
+	0x1d9e: 0x0001, 0x1d9f: 0x0001, 0x1da0: 0x0001, 0x1da1: 0x0001, 0x1da2: 0x0001, 0x1da3: 0x0001,
+	0x1da4: 0x0001, 0x1da5: 0x0001, 0x1da6: 0x0001, 0x1da7: 0x0001, 0x1da8: 0x0001, 0x1da9: 0x0001,
+	0x1daa: 0x0001, 0x1dab: 0x0001, 0x1dac: 0x0001, 0x1dad: 0x0001, 0x1dae: 0x0001, 0x1daf: 0x0001,
+	0x1db0: 0x0001, 0x1db1: 0x0001, 0x1db2: 0x0001, 0x1db3: 0x0001, 0x1db4: 0x0001, 0x1db5: 0x0001,
+	0x1db6: 0x0001, 0x1db7: 0x0001, 0x1db8: 0x0001, 0x1db9: 0x0001, 0x1dba: 0x0001, 0x1dbb: 0x0001,
+	0x1dbc: 0x0001, 0x1dbd: 0x0001, 0x1dbe: 0x0001, 0x1dbf: 0x0001,
+	// Block 0x77, offset 0x1dc0
+	0x1dc5: 0x0001,
+	0x1dc6: 0x0001, 0x1dc7: 0x0001, 0x1dc8: 0x0001, 0x1dc9: 0x0001, 0x1dca: 0x0001, 0x1dcb: 0x0001,
+	0x1dcc: 0x0001, 0x1dcd: 0x0001, 0x1dce: 0x0001, 0x1dcf: 0x0001, 0x1dd0: 0x0001, 0x1dd1: 0x0001,
+	0x1dd2: 0x0001, 0x1dd3: 0x0001, 0x1dd4: 0x0001, 0x1dd5: 0x0001, 0x1dd6: 0x0001, 0x1dd7: 0x0001,
+	0x1dd8: 0x0001, 0x1dd9: 0x0001, 0x1dda: 0x0001, 0x1ddb: 0x0001, 0x1ddc: 0x0001, 0x1ddd: 0x0001,
+	0x1dde: 0x0001, 0x1ddf: 0x0001, 0x1de0: 0x0001, 0x1de1: 0x0001, 0x1de2: 0x0001, 0x1de3: 0x0001,
+	0x1de4: 0x0001, 0x1de5: 0x0001, 0x1de6: 0x0001, 0x1de7: 0x0001, 0x1de8: 0x0001, 0x1de9: 0x0001,
+	0x1dea: 0x0001, 0x1deb: 0x0001, 0x1dec: 0x0001, 0x1ded: 0x0001, 0x1dee: 0x0001, 0x1def: 0x0001,
+	0x1df1: 0x0001, 0x1df2: 0x0001, 0x1df3: 0x0001, 0x1df4: 0x0001, 0x1df5: 0x0001,
+	0x1df6: 0x0001, 0x1df7: 0x0001, 0x1df8: 0x0001, 0x1df9: 0x0001, 0x1dfa: 0x0001, 0x1dfb: 0x0001,
+	0x1dfc: 0x0001, 0x1dfd: 0x0001, 0x1dfe: 0x0001, 0x1dff: 0x0001,
+	// Block 0x78, offset 0x1e00
+	0x1e00: 0x0001, 0x1e01: 0x0001, 0x1e02: 0x0001, 0x1e03: 0x0001, 0x1e04: 0x0001, 0x1e05: 0x0001,
+	0x1e06: 0x0001, 0x1e07: 0x0001, 0x1e08: 0x0001, 0x1e09: 0x0001, 0x1e0a: 0x0001, 0x1e0b: 0x0001,
+	0x1e0c: 0x0001, 0x1e0d: 0x0001, 0x1e0e: 0x0001, 0x1e10: 0x0001, 0x1e11: 0x0001,
+	0x1e12: 0x0001, 0x1e13: 0x0001, 0x1e14: 0x0001, 0x1e15: 0x0001, 0x1e16: 0x0001, 0x1e17: 0x0001,
+	0x1e18: 0x0001, 0x1e19: 0x0001, 0x1e1a: 0x0001, 0x1e1b: 0x0001, 0x1e1c: 0x0001, 0x1e1d: 0x0001,
+	0x1e1e: 0x0001, 0x1e1f: 0x0001, 0x1e20: 0x0001, 0x1e21: 0x0001, 0x1e22: 0x0001, 0x1e23: 0x0001,
+	0x1e24: 0x0001, 0x1e25: 0x0001, 0x1e26: 0x0001, 0x1e27: 0x0001, 0x1e28: 0x0001, 0x1e29: 0x0001,
+	0x1e2a: 0x0001, 0x1e2b: 0x0001, 0x1e2c: 0x0001, 0x1e2d: 0x0001, 0x1e2e: 0x0001, 0x1e2f: 0x0001,
+	0x1e30: 0x0001, 0x1e31: 0x0001, 0x1e32: 0x0001, 0x1e33: 0x0001, 0x1e34: 0x0001, 0x1e35: 0x0001,
+	0x1e36: 0x0001, 0x1e37: 0x0001, 0x1e38: 0x0001, 0x1e39: 0x0001, 0x1e3a: 0x0001, 0x1e3b: 0x0001,
+	0x1e3c: 0x0001, 0x1e3d: 0x0001, 0x1e3e: 0x0001, 0x1e3f: 0x0001,
+	// Block 0x79, offset 0x1e40
+	0x1e40: 0x0001, 0x1e41: 0x0001, 0x1e42: 0x0001, 0x1e43: 0x0001, 0x1e44: 0x0001, 0x1e45: 0x0001,
+	0x1e46: 0x0001, 0x1e47: 0x0001, 0x1e48: 0x0001, 0x1e49: 0x0001, 0x1e4a: 0x0001, 0x1e4b: 0x0001,
+	0x1e4c: 0x0001, 0x1e4d: 0x0001, 0x1e4e: 0x0001, 0x1e4f: 0x0001, 0x1e50: 0x0001, 0x1e51: 0x0001,
+	0x1e52: 0x0001, 0x1e53: 0x0001, 0x1e54: 0x0001, 0x1e55: 0x0001, 0x1e56: 0x0001, 0x1e57: 0x0001,
+	0x1e58: 0x0001, 0x1e59: 0x0001, 0x1e5a: 0x0001, 0x1e5b: 0x0001, 0x1e5c: 0x0001, 0x1e5d: 0x0001,
+	0x1e5e: 0x0001, 0x1e5f: 0x0001, 0x1e60: 0x0001, 0x1e61: 0x0001, 0x1e62: 0x0001, 0x1e63: 0x0001,
+	0x1e6f: 0x0001,
+	0x1e70: 0x0001, 0x1e71: 0x0001, 0x1e72: 0x0001, 0x1e73: 0x0001, 0x1e74: 0x0001, 0x1e75: 0x0001,
+	0x1e76: 0x0001, 0x1e77: 0x0001, 0x1e78: 0x0001, 0x1e79: 0x0001, 0x1e7a: 0x0001, 0x1e7b: 0x0001,
+	0x1e7c: 0x0001, 0x1e7d: 0x0001, 0x1e7e: 0x0001, 0x1e7f: 0x0001,
+	// Block 0x7a, offset 0x1e80
+	0x1e80: 0x0001, 0x1e81: 0x0001, 0x1e82: 0x0001, 0x1e83: 0x0001, 0x1e84: 0x0001, 0x1e85: 0x0001,
+	0x1e86: 0x0001, 0x1e87: 0x0001, 0x1e88: 0x0001, 0x1e89: 0x0001, 0x1e8a: 0x0001, 0x1e8b: 0x0001,
+	0x1e8c: 0x0001, 0x1e8d: 0x0001, 0x1e8e: 0x0001, 0x1e8f: 0x0001, 0x1e90: 0x0001, 0x1e91: 0x0001,
+	0x1e92: 0x0001, 0x1e93: 0x0001, 0x1e94: 0x0001, 0x1e95: 0x0001, 0x1e96: 0x0001, 0x1e97: 0x0001,
+	0x1e98: 0x0001, 0x1e99: 0x0001, 0x1e9a: 0x0001, 0x1e9b: 0x0001, 0x1e9c: 0x0001, 0x1e9d: 0x0001,
+	0x1e9e: 0x0001, 0x1ea0: 0x0001, 0x1ea1: 0x0001, 0x1ea2: 0x0001, 0x1ea3: 0x0001,
+	0x1ea4: 0x0001, 0x1ea5: 0x0001, 0x1ea6: 0x0001, 0x1ea7: 0x0001, 0x1ea8: 0x0001, 0x1ea9: 0x0001,
+	0x1eaa: 0x0001, 0x1eab: 0x0001, 0x1eac: 0x0001, 0x1ead: 0x0001, 0x1eae: 0x0001, 0x1eaf: 0x0001,
+	0x1eb0: 0x0001, 0x1eb1: 0x0001, 0x1eb2: 0x0001, 0x1eb3: 0x0001, 0x1eb4: 0x0001, 0x1eb5: 0x0001,
+	0x1eb6: 0x0001, 0x1eb7: 0x0001, 0x1eb8: 0x0001, 0x1eb9: 0x0001, 0x1eba: 0x0001, 0x1ebb: 0x0001,
+	0x1ebc: 0x0001, 0x1ebd: 0x0001, 0x1ebe: 0x0001, 0x1ebf: 0x0001,
+	// Block 0x7b, offset 0x1ec0
+	0x1ec0: 0x0001, 0x1ec1: 0x0001, 0x1ec2: 0x0001, 0x1ec3: 0x0001, 0x1ec4: 0x0001, 0x1ec5: 0x0001,
+	0x1ec6: 0x0001, 0x1ec7: 0x0001, 0x1ec8: 0x0002, 0x1ec9: 0x0002, 0x1eca: 0x0002, 0x1ecb: 0x0002,
+	0x1ecc: 0x0002, 0x1ecd: 0x0002, 0x1ece: 0x0002, 0x1ecf: 0x0002, 0x1ed0: 0x0001, 0x1ed1: 0x0001,
+	0x1ed2: 0x0001, 0x1ed3: 0x0001, 0x1ed4: 0x0001, 0x1ed5: 0x0001, 0x1ed6: 0x0001, 0x1ed7: 0x0001,
+	0x1ed8: 0x0001, 0x1ed9: 0x0001, 0x1eda: 0x0001, 0x1edb: 0x0001, 0x1edc: 0x0001, 0x1edd: 0x0001,
+	0x1ede: 0x0001, 0x1edf: 0x0001, 0x1ee0: 0x0001, 0x1ee1: 0x0001, 0x1ee2: 0x0001, 0x1ee3: 0x0001,
+	0x1ee4: 0x0001, 0x1ee5: 0x0001, 0x1ee6: 0x0001, 0x1ee7: 0x0001, 0x1ee8: 0x0001, 0x1ee9: 0x0001,
+	0x1eea: 0x0001, 0x1eeb: 0x0001, 0x1eec: 0x0001, 0x1eed: 0x0001, 0x1eee: 0x0001, 0x1eef: 0x0001,
+	0x1ef0: 0x0001, 0x1ef1: 0x0001, 0x1ef2: 0x0001, 0x1ef3: 0x0001, 0x1ef4: 0x0001, 0x1ef5: 0x0001,
+	0x1ef6: 0x0001, 0x1ef7: 0x0001, 0x1ef8: 0x0001, 0x1ef9: 0x0001, 0x1efa: 0x0001, 0x1efb: 0x0001,
+	0x1efc: 0x0001, 0x1efd: 0x0001, 0x1efe: 0x0001, 0x1eff: 0x0001,
+	// Block 0x7c, offset 0x1f00
+	0x1f00: 0x0001, 0x1f01: 0x0001, 0x1f02: 0x0001, 0x1f03: 0x0001, 0x1f04: 0x0001, 0x1f05: 0x0001,
+	0x1f06: 0x0001, 0x1f07: 0x0001, 0x1f08: 0x0001, 0x1f09: 0x0001, 0x1f0a: 0x0001, 0x1f0b: 0x0001,
+	0x1f0c: 0x0001, 0x1f0d: 0x0001, 0x1f0e: 0x0001, 0x1f0f: 0x0001, 0x1f10: 0x0001, 0x1f11: 0x0001,
+	0x1f12: 0x0001, 0x1f13: 0x0001, 0x1f14: 0x0001, 0x1f15: 0x0001, 0x1f16: 0x0001, 0x1f17: 0x0005,
+	0x1f18: 0x0001, 0x1f19: 0x0005, 0x1f1a: 0x0001, 0x1f1b: 0x0001, 0x1f1c: 0x0001, 0x1f1d: 0x0001,
+	0x1f1e: 0x0001, 0x1f1f: 0x0001, 0x1f20: 0x0001, 0x1f21: 0x0001, 0x1f22: 0x0001, 0x1f23: 0x0001,
+	0x1f24: 0x0001, 0x1f25: 0x0001, 0x1f26: 0x0001, 0x1f27: 0x0001, 0x1f28: 0x0001, 0x1f29: 0x0001,
+	0x1f2a: 0x0001, 0x1f2b: 0x0001, 0x1f2c: 0x0001, 0x1f2d: 0x0001, 0x1f2e: 0x0001, 0x1f2f: 0x0001,
+	0x1f30: 0x0001, 0x1f31: 0x0001, 0x1f32: 0x0001, 0x1f33: 0x0001, 0x1f34: 0x0001, 0x1f35: 0x0001,
+	0x1f36: 0x0001, 0x1f37: 0x0001, 0x1f38: 0x0001, 0x1f39: 0x0001, 0x1f3a: 0x0001, 0x1f3b: 0x0001,
+	0x1f3c: 0x0001, 0x1f3d: 0x0001, 0x1f3e: 0x0001, 0x1f3f: 0x0001,
+	// Block 0x7d, offset 0x1f40
+	0x1f40: 0x0001, 0x1f41: 0x0001, 0x1f42: 0x0001, 0x1f43: 0x0001, 0x1f44: 0x0001, 0x1f45: 0x0001,
+	0x1f46: 0x0001, 0x1f47: 0x0001, 0x1f48: 0x0001, 0x1f49: 0x0001, 0x1f4a: 0x0001, 0x1f4b: 0x0001,
+	0x1f4c: 0x0001, 0x1f50: 0x0001, 0x1f51: 0x0001,
+	0x1f52: 0x0001, 0x1f53: 0x0001, 0x1f54: 0x0001, 0x1f55: 0x0001, 0x1f56: 0x0001, 0x1f57: 0x0001,
+	0x1f58: 0x0001, 0x1f59: 0x0001, 0x1f5a: 0x0001, 0x1f5b: 0x0001, 0x1f5c: 0x0001, 0x1f5d: 0x0001,
+	0x1f5e: 0x0001, 0x1f5f: 0x0001, 0x1f60: 0x0001, 0x1f61: 0x0001, 0x1f62: 0x0001, 0x1f63: 0x0001,
+	0x1f64: 0x0001, 0x1f65: 0x0001, 0x1f66: 0x0001, 0x1f67: 0x0001, 0x1f68: 0x0001, 0x1f69: 0x0001,
+	0x1f6a: 0x0001, 0x1f6b: 0x0001, 0x1f6c: 0x0001, 0x1f6d: 0x0001, 0x1f6e: 0x0001, 0x1f6f: 0x0001,
+	0x1f70: 0x0001, 0x1f71: 0x0001, 0x1f72: 0x0001, 0x1f73: 0x0001, 0x1f74: 0x0001, 0x1f75: 0x0001,
+	0x1f76: 0x0001, 0x1f77: 0x0001, 0x1f78: 0x0001, 0x1f79: 0x0001, 0x1f7a: 0x0001, 0x1f7b: 0x0001,
+	0x1f7c: 0x0001, 0x1f7d: 0x0001, 0x1f7e: 0x0001, 0x1f7f: 0x0001,
+	// Block 0x7e, offset 0x1f80
+	0x1f80: 0x0001, 0x1f81: 0x0001, 0x1f82: 0x0001, 0x1f83: 0x0001, 0x1f84: 0x0001, 0x1f85: 0x0001,
+	0x1f86: 0x0001,
+	// Block 0x7f, offset 0x1fc0
+	0x1fef: 0x0008,
+	0x1ff0: 0x0008, 0x1ff1: 0x0008, 0x1ff2: 0x0008, 0x1ff4: 0x0008, 0x1ff5: 0x0008,
+	0x1ff6: 0x0008, 0x1ff7: 0x0008, 0x1ff8: 0x0008, 0x1ff9: 0x0008, 0x1ffa: 0x0008, 0x1ffb: 0x0008,
+	0x1ffc: 0x0008, 0x1ffd: 0x0008,
+	// Block 0x80, offset 0x2000
+	0x201e: 0x0008, 0x201f: 0x0008,
+	// Block 0x81, offset 0x2040
+	0x2070: 0x0008, 0x2071: 0x0008,
+	// Block 0x82, offset 0x2080
+	0x2082: 0x0008,
+	0x2086: 0x0008, 0x208b: 0x0008,
+	0x20a5: 0x0008, 0x20a6: 0x0008,
+	0x20ac: 0x0008,
+	// Block 0x83, offset 0x20c0
+	0x20c4: 0x0008, 0x20c5: 0x0008,
+	0x20e0: 0x0008, 0x20e1: 0x0008, 0x20e2: 0x0008, 0x20e3: 0x0008,
+	0x20e4: 0x0008, 0x20e5: 0x0008, 0x20e6: 0x0008, 0x20e7: 0x0008, 0x20e8: 0x0008, 0x20e9: 0x0008,
+	0x20ea: 0x0008, 0x20eb: 0x0008, 0x20ec: 0x0008, 0x20ed: 0x0008, 0x20ee: 0x0008, 0x20ef: 0x0008,
+	0x20f0: 0x0008, 0x20f1: 0x0008,
+	0x20ff: 0x0008,
+	// Block 0x84, offset 0x2100
+	0x2126: 0x0008, 0x2127: 0x0008, 0x2128: 0x0008, 0x2129: 0x0008,
+	0x212a: 0x0008, 0x212b: 0x0008, 0x212c: 0x0008, 0x212d: 0x0008,
+	// Block 0x85, offset 0x2140
+	0x2147: 0x0008, 0x2148: 0x0008, 0x2149: 0x0008, 0x214a: 0x0008, 0x214b: 0x0008,
+	0x214c: 0x0008, 0x214d: 0x0008, 0x214e: 0x0008, 0x214f: 0x0008, 0x2150: 0x0008, 0x2151: 0x0008,
+	0x2160: 0x0001, 0x2161: 0x0001, 0x2162: 0x0001, 0x2163: 0x0001,
+	0x2164: 0x0001, 0x2165: 0x0001, 0x2166: 0x0001, 0x2167: 0x0001, 0x2168: 0x0001, 0x2169: 0x0001,
+	0x216a: 0x0001, 0x216b: 0x0001, 0x216c: 0x0001, 0x216d: 0x0001, 0x216e: 0x0001, 0x216f: 0x0001,
+	0x2170: 0x0001, 0x2171: 0x0001, 0x2172: 0x0001, 0x2173: 0x0001, 0x2174: 0x0001, 0x2175: 0x0001,
+	0x2176: 0x0001, 0x2177: 0x0001, 0x2178: 0x0001, 0x2179: 0x0001, 0x217a: 0x0001, 0x217b: 0x0001,
+	0x217c: 0x0001,
+	// Block 0x86, offset 0x2180
+	0x2180: 0x0008, 0x2181: 0x0008, 0x2182: 0x0008,
+	0x21b3: 0x0008,
+	0x21b6: 0x0008, 0x21b7: 0x0008, 0x21b8: 0x0008, 0x21b9: 0x0008,
+	0x21bc: 0x0008, 0x21bd: 0x0008,
+	// Block 0x87, offset 0x21c0
+	0x21e5: 0x0008,
+	// Block 0x88, offset 0x2200
+	0x2229: 0x0008,
+	0x222a: 0x0008, 0x222b: 0x0008, 0x222c: 0x0008, 0x222d: 0x0008, 0x222e: 0x0008,
+	0x2231: 0x0008, 0x2232: 0x0008, 0x2235: 0x0008,
+	0x2236: 0x0008,
+	// Block 0x89, offset 0x2240
+	0x2243: 0x0008,
+	0x224c: 0x0008,
+	0x227c: 0x0008,
+	// Block 0x8a, offset 0x2280
+	0x22b0: 0x0008, 0x22b2: 0x0008, 0x22b3: 0x0008, 0x22b4: 0x0008,
+	0x22b7: 0x0008, 0x22b8: 0x0008,
+	0x22be: 0x0008, 0x22bf: 0x0008,
+	// Block 0x8b, offset 0x22c0
+	0x22c1: 0x0008,
+	0x22ec: 0x0008, 0x22ed: 0x0008,
+	0x22f6: 0x0008,
+	// Block 0x8c, offset 0x2300
+	0x2325: 0x0008, 0x2328: 0x0008,
+	0x232d: 0x0008,
+	// Block 0x8d, offset 0x2340
+	0x2340: 0x0001, 0x2341: 0x0001, 0x2342: 0x0001, 0x2343: 0x0001, 0x2344: 0x0001, 0x2345: 0x0001,
+	0x2346: 0x0001, 0x2347: 0x0001, 0x2348: 0x0001, 0x2349: 0x0001, 0x234a: 0x0001, 0x234b: 0x0001,
+	0x234c: 0x0001, 0x234d: 0x0001, 0x234e: 0x0001, 0x234f: 0x0001, 0x2350: 0x0001, 0x2351: 0x0001,
+	0x2352: 0x0001, 0x2353: 0x0001, 0x2354: 0x0001, 0x2355: 0x0001, 0x2356: 0x0001, 0x2357: 0x0001,
+	0x2358: 0x0001, 0x2359: 0x0001, 0x235a: 0x0001, 0x235b: 0x0001, 0x235c: 0x0001, 0x235d: 0x0001,
+	0x235e: 0x0001, 0x235f: 0x0001, 0x2360: 0x0001, 0x2361: 0x0001, 0x2362: 0x0001, 0x2363: 0x0001,
+	// Block 0x8e, offset 0x2380
+	0x239e: 0x0008,
+	// Block 0x8f, offset 0x23c0
+	0x23c0: 0x000a, 0x23c1: 0x000a, 0x23c2: 0x000a, 0x23c3: 0x000a, 0x23c4: 0x000a, 0x23c5: 0x000a,
+	0x23c6: 0x000a, 0x23c7: 0x000a, 0x23c8: 0x000a, 0x23c9: 0x000a, 0x23ca: 0x000a, 0x23cb: 0x000a,
+	0x23cc: 0x000a, 0x23cd: 0x000a, 0x23ce: 0x000a, 0x23cf: 0x000a, 0x23d0: 0x0001, 0x23d1: 0x0001,
+	0x23d2: 0x0001, 0x23d3: 0x0001, 0x23d4: 0x0001, 0x23d5: 0x0001, 0x23d6: 0x0001, 0x23d7: 0x0001,
+	0x23d8: 0x0001, 0x23d9: 0x0001,
+	0x23e0: 0x0008, 0x23e1: 0x0008, 0x23e2: 0x0008, 0x23e3: 0x0008,
+	0x23e4: 0x0008, 0x23e5: 0x0008, 0x23e6: 0x0008, 0x23e7: 0x0008, 0x23e8: 0x0008, 0x23e9: 0x0008,
+	0x23ea: 0x0008, 0x23eb: 0x0008, 0x23ec: 0x0008, 0x23ed: 0x0008, 0x23ee: 0x0008, 0x23ef: 0x0008,
+	0x23f0: 0x0001, 0x23f1: 0x0001, 0x23f2: 0x0001, 0x23f3: 0x0001, 0x23f4: 0x0001, 0x23f5: 0x0001,
+	0x23f6: 0x0001, 0x23f7: 0x0001, 0x23f8: 0x0001, 0x23f9: 0x0001, 0x23fa: 0x0001, 0x23fb: 0x0001,
+	0x23fc: 0x0001, 0x23fd: 0x0001, 0x23fe: 0x0001, 0x23ff: 0x0001,
+	// Block 0x90, offset 0x2400
+	0x2400: 0x0001, 0x2401: 0x0001, 0x2402: 0x0001, 0x2403: 0x0001, 0x2404: 0x0001, 0x2405: 0x0001,
+	0x2406: 0x0001, 0x2407: 0x0001, 0x2408: 0x0001, 0x2409: 0x0001, 0x240a: 0x0001, 0x240b: 0x0001,
+	0x240c: 0x0001, 0x240d: 0x0001, 0x240e: 0x0001, 0x240f: 0x0001, 0x2410: 0x0001, 0x2411: 0x0001,
+	0x2412: 0x0001, 0x2414: 0x0001, 0x2415: 0x0001, 0x2416: 0x0001, 0x2417: 0x0001,
+	0x2418: 0x0001, 0x2419: 0x0001, 0x241a: 0x0001, 0x241b: 0x0001, 0x241c: 0x0001, 0x241d: 0x0001,
+	0x241e: 0x0001, 0x241f: 0x0001, 0x2420: 0x0001, 0x2421: 0x0001, 0x2422: 0x0001, 0x2423: 0x0001,
+	0x2424: 0x0001, 0x2425: 0x0001, 0x2426: 0x0001, 0x2428: 0x0001, 0x2429: 0x0001,
+	0x242a: 0x0001, 0x242b: 0x0001,
+	// Block 0x91, offset 0x2440
+	0x2440: 0x0001, 0x2441: 0x0001, 0x2442: 0x0001, 0x2443: 0x0001, 0x2444: 0x0001, 0x2445: 0x0001,
+	0x2446: 0x0001, 0x2447: 0x0001, 0x2448: 0x0001, 0x2449: 0x0001, 0x244a: 0x0001, 0x244b: 0x0001,
+	0x244c: 0x0001, 0x244d: 0x0001, 0x244e: 0x0001, 0x244f: 0x0001, 0x2450: 0x0001, 0x2451: 0x0001,
+	0x2452: 0x0001, 0x2453: 0x0001, 0x2454: 0x0001, 0x2455: 0x0001, 0x2456: 0x0001, 0x2457: 0x0001,
+	0x2458: 0x0001, 0x2459: 0x0001, 0x245a: 0x0001, 0x245b: 0x0001, 0x245c: 0x0001, 0x245d: 0x0001,
+	0x245e: 0x0001, 0x245f: 0x0001, 0x2460: 0x0001,
+	// Block 0x92, offset 0x2480
+	0x24a0: 0x0001, 0x24a1: 0x0001, 0x24a2: 0x0001, 0x24a3: 0x0001,
+	0x24a4: 0x0001, 0x24a5: 0x0001, 0x24a6: 0x0001,
+	0x24b9: 0x0008, 0x24ba: 0x0008, 0x24bb: 0x0008,
+	0x24be: 0x0008, 0x24bf: 0x0008,
+	// Block 0x93, offset 0x24c0
+	0x24fd: 0x0008,
+	// Block 0x94, offset 0x2500
+	0x2520: 0x0008,
+	// Block 0x95, offset 0x2540
+	0x2576: 0x0008, 0x2577: 0x0008, 0x2578: 0x0008, 0x2579: 0x0008, 0x257a: 0x0008,
+	// Block 0x96, offset 0x2580
+	0x2581: 0x0008, 0x2582: 0x0008, 0x2583: 0x0008, 0x2585: 0x0008,
+	0x2586: 0x0008,
+	0x258c: 0x0008, 0x258d: 0x0008, 0x258e: 0x0008, 0x258f: 0x0008,
+	0x25b8: 0x0008, 0x25b9: 0x0008, 0x25ba: 0x0008,
+	0x25bf: 0x0008,
+	// Block 0x97, offset 0x25c0
+	0x25e5: 0x0008, 0x25e6: 0x0008,
+	// Block 0x98, offset 0x2600
+	0x2624: 0x0008, 0x2625: 0x0008, 0x2626: 0x0008, 0x2627: 0x0008,
+	// Block 0x99, offset 0x2640
+	0x266b: 0x0008, 0x266c: 0x0008,
+	// Block 0x9a, offset 0x2680
+	0x26bd: 0x0008, 0x26be: 0x0008, 0x26bf: 0x0008,
+	// Block 0x9b, offset 0x26c0
+	0x26c6: 0x0008, 0x26c7: 0x0008, 0x26c8: 0x0008, 0x26c9: 0x0008, 0x26ca: 0x0008, 0x26cb: 0x0008,
+	0x26cc: 0x0008, 0x26cd: 0x0008, 0x26ce: 0x0008, 0x26cf: 0x0008, 0x26d0: 0x0008,
+	// Block 0x9c, offset 0x2700
+	0x2702: 0x0008, 0x2703: 0x0008, 0x2704: 0x0008, 0x2705: 0x0008,
+	// Block 0x9d, offset 0x2740
+	0x2741: 0x0008,
+	0x2778: 0x0008, 0x2779: 0x0008, 0x277a: 0x0008, 0x277b: 0x0008,
+	0x277c: 0x0008, 0x277d: 0x0008, 0x277e: 0x0008, 0x277f: 0x0008,
+	// Block 0x9e, offset 0x2780
+	0x2780: 0x0008, 0x2781: 0x0008, 0x2782: 0x0008, 0x2783: 0x0008, 0x2784: 0x0008, 0x2785: 0x0008,
+	0x2786: 0x0008,
+	0x27b0: 0x0008, 0x27b3: 0x0008, 0x27b4: 0x0008,
+	0x27bf: 0x0008,
+	// Block 0x9f, offset 0x27c0
+	0x27c0: 0x0008, 0x27c1: 0x0008,
+	0x27f3: 0x0008, 0x27f4: 0x0008, 0x27f5: 0x0008,
+	0x27f6: 0x0008, 0x27f9: 0x0008, 0x27fa: 0x0008,
+	0x27fd: 0x0008,
+	// Block 0xa0, offset 0x2800
+	0x2802: 0x0008,
+	0x280d: 0x0008,
+	// Block 0xa1, offset 0x2840
+	0x2840: 0x0008, 0x2841: 0x0008, 0x2842: 0x0008,
+	0x2867: 0x0008, 0x2868: 0x0008, 0x2869: 0x0008,
+	0x286a: 0x0008, 0x286b: 0x0008, 0x286d: 0x0008, 0x286e: 0x0008, 0x286f: 0x0008,
+	0x2870: 0x0008, 0x2871: 0x0008, 0x2872: 0x0008, 0x2873: 0x0008, 0x2874: 0x0008,
+	// Block 0xa2, offset 0x2880
+	0x28b3: 0x0008,
+	// Block 0xa3, offset 0x28c0
+	0x28c0: 0x0008, 0x28c1: 0x0008,
+	0x28f6: 0x0008, 0x28f7: 0x0008, 0x28f8: 0x0008, 0x28f9: 0x0008, 0x28fa: 0x0008, 0x28fb: 0x0008,
+	0x28fc: 0x0008, 0x28fd: 0x0008, 0x28fe: 0x0008,
+	// Block 0xa4, offset 0x2900
+	0x2909: 0x0008, 0x290a: 0x0008, 0x290b: 0x0008,
+	0x290c: 0x0008, 0x290f: 0x0008,
+	// Block 0xa5, offset 0x2940
+	0x296f: 0x0008,
+	0x2970: 0x0008, 0x2971: 0x0008, 0x2974: 0x0008,
+	0x2976: 0x0008, 0x2977: 0x0008,
+	0x297e: 0x0008,
+	// Block 0xa6, offset 0x2980
+	0x299f: 0x0008, 0x29a3: 0x0008,
+	0x29a4: 0x0008, 0x29a5: 0x0008, 0x29a6: 0x0008, 0x29a7: 0x0008, 0x29a8: 0x0008, 0x29a9: 0x0008,
+	0x29aa: 0x0008,
+	// Block 0xa7, offset 0x29c0
+	0x29c0: 0x0008,
+	0x29e6: 0x0008, 0x29e7: 0x0008, 0x29e8: 0x0008, 0x29e9: 0x0008,
+	0x29ea: 0x0008, 0x29eb: 0x0008, 0x29ec: 0x0008,
+	0x29f0: 0x0008, 0x29f1: 0x0008, 0x29f2: 0x0008, 0x29f3: 0x0008, 0x29f4: 0x0008,
+	// Block 0xa8, offset 0x2a00
+	0x2a38: 0x0008, 0x2a39: 0x0008, 0x2a3a: 0x0008, 0x2a3b: 0x0008,
+	0x2a3c: 0x0008, 0x2a3d: 0x0008, 0x2a3e: 0x0008, 0x2a3f: 0x0008,
+	// Block 0xa9, offset 0x2a40
+	0x2a42: 0x0008, 0x2a43: 0x0008, 0x2a44: 0x0008,
+	0x2a46: 0x0008,
+	0x2a5e: 0x0008,
+	// Block 0xaa, offset 0x2a80
+	0x2ab3: 0x0008, 0x2ab4: 0x0008, 0x2ab5: 0x0008,
+	0x2ab6: 0x0008, 0x2ab7: 0x0008, 0x2ab8: 0x0008, 0x2aba: 0x0008,
+	0x2abf: 0x0008,
+	// Block 0xab, offset 0x2ac0
+	0x2ac0: 0x0008, 0x2ac2: 0x0008, 0x2ac3: 0x0008,
+	// Block 0xac, offset 0x2b00
+	0x2b32: 0x0008, 0x2b33: 0x0008, 0x2b34: 0x0008, 0x2b35: 0x0008,
+	0x2b3c: 0x0008, 0x2b3d: 0x0008, 0x2b3f: 0x0008,
+	// Block 0xad, offset 0x2b40
+	0x2b40: 0x0008,
+	0x2b5c: 0x0008, 0x2b5d: 0x0008,
+	// Block 0xae, offset 0x2b80
+	0x2bb3: 0x0008, 0x2bb4: 0x0008, 0x2bb5: 0x0008,
+	0x2bb6: 0x0008, 0x2bb7: 0x0008, 0x2bb8: 0x0008, 0x2bb9: 0x0008, 0x2bba: 0x0008,
+	0x2bbd: 0x0008, 0x2bbf: 0x0008,
+	// Block 0xaf, offset 0x2bc0
+	0x2bc0: 0x0008,
+	// Block 0xb0, offset 0x2c00
+	0x2c2b: 0x0008, 0x2c2d: 0x0008,
+	0x2c30: 0x0008, 0x2c31: 0x0008, 0x2c32: 0x0008, 0x2c33: 0x0008, 0x2c34: 0x0008, 0x2c35: 0x0008,
+	0x2c37: 0x0008,
+	// Block 0xb1, offset 0x2c40
+	0x2c5d: 0x0008,
+	0x2c5e: 0x0008, 0x2c5f: 0x0008, 0x2c62: 0x0008, 0x2c63: 0x0008,
+	0x2c64: 0x0008, 0x2c65: 0x0008, 0x2c67: 0x0008, 0x2c68: 0x0008, 0x2c69: 0x0008,
+	0x2c6a: 0x0008, 0x2c6b: 0x0008,
+	// Block 0xb2, offset 0x2c80
+	0x2caf: 0x0008,
+	0x2cb0: 0x0008, 0x2cb1: 0x0008, 0x2cb2: 0x0008, 0x2cb3: 0x0008, 0x2cb4: 0x0008, 0x2cb5: 0x0008,
+	0x2cb6: 0x0008, 0x2cb7: 0x0008, 0x2cb9: 0x0008, 0x2cba: 0x0008,
+	// Block 0xb3, offset 0x2cc0
+	0x2cfb: 0x0008,
+	0x2cfc: 0x0008, 0x2cfe: 0x0008,
+	// Block 0xb4, offset 0x2d00
+	0x2d03: 0x0008,
+	// Block 0xb5, offset 0x2d40
+	0x2d54: 0x0008, 0x2d55: 0x0008, 0x2d56: 0x0008, 0x2d57: 0x0008,
+	0x2d5a: 0x0008, 0x2d5b: 0x0008,
+	0x2d60: 0x0008,
+	// Block 0xb6, offset 0x2d80
+	0x2d81: 0x0008, 0x2d82: 0x0008, 0x2d83: 0x0008, 0x2d84: 0x0008, 0x2d85: 0x0008,
+	0x2d86: 0x0008, 0x2d87: 0x0008, 0x2d88: 0x0008, 0x2d89: 0x0008, 0x2d8a: 0x0008,
+	0x2db3: 0x0008, 0x2db4: 0x0008, 0x2db5: 0x0008,
+	0x2db6: 0x0008, 0x2db7: 0x0008, 0x2db8: 0x0008, 0x2dbb: 0x0008,
+	0x2dbc: 0x0008, 0x2dbd: 0x0008, 0x2dbe: 0x0008,
+	// Block 0xb7, offset 0x2dc0
+	0x2dc7: 0x0008,
+	0x2dd1: 0x0008,
+	0x2dd2: 0x0008, 0x2dd3: 0x0008, 0x2dd4: 0x0008, 0x2dd5: 0x0008, 0x2dd6: 0x0008,
+	0x2dd9: 0x0008, 0x2dda: 0x0008, 0x2ddb: 0x0008,
+	// Block 0xb8, offset 0x2e00
+	0x2e0a: 0x0008, 0x2e0b: 0x0008,
+	0x2e0c: 0x0008, 0x2e0d: 0x0008, 0x2e0e: 0x0008, 0x2e0f: 0x0008, 0x2e10: 0x0008, 0x2e11: 0x0008,
+	0x2e12: 0x0008, 0x2e13: 0x0008, 0x2e14: 0x0008, 0x2e15: 0x0008, 0x2e16: 0x0008,
+	0x2e18: 0x0008, 0x2e19: 0x0008,
+	// Block 0xb9, offset 0x2e40
+	0x2e70: 0x0008, 0x2e71: 0x0008, 0x2e72: 0x0008, 0x2e73: 0x0008, 0x2e74: 0x0008, 0x2e75: 0x0008,
+	0x2e76: 0x0008, 0x2e78: 0x0008, 0x2e79: 0x0008, 0x2e7a: 0x0008, 0x2e7b: 0x0008,
+	0x2e7c: 0x0008, 0x2e7d: 0x0008, 0x2e7f: 0x0008,
+	// Block 0xba, offset 0x2e80
+	0x2e92: 0x0008, 0x2e93: 0x0008, 0x2e94: 0x0008, 0x2e95: 0x0008, 0x2e96: 0x0008, 0x2e97: 0x0008,
+	0x2e98: 0x0008, 0x2e99: 0x0008, 0x2e9a: 0x0008, 0x2e9b: 0x0008, 0x2e9c: 0x0008, 0x2e9d: 0x0008,
+	0x2e9e: 0x0008, 0x2e9f: 0x0008, 0x2ea0: 0x0008, 0x2ea1: 0x0008, 0x2ea2: 0x0008, 0x2ea3: 0x0008,
+	0x2ea4: 0x0008, 0x2ea5: 0x0008, 0x2ea6: 0x0008, 0x2ea7: 0x0008,
+	0x2eaa: 0x0008, 0x2eab: 0x0008, 0x2eac: 0x0008, 0x2ead: 0x0008, 0x2eae: 0x0008, 0x2eaf: 0x0008,
+	0x2eb0: 0x0008, 0x2eb2: 0x0008, 0x2eb3: 0x0008, 0x2eb5: 0x0008,
+	0x2eb6: 0x0008,
+	// Block 0xbb, offset 0x2ec0
+	0x2ef1: 0x0008, 0x2ef2: 0x0008, 0x2ef3: 0x0008, 0x2ef4: 0x0008, 0x2ef5: 0x0008,
+	0x2ef6: 0x0008, 0x2efa: 0x0008,
+	0x2efc: 0x0008, 0x2efd: 0x0008, 0x2eff: 0x0008,
+	// Block 0xbc, offset 0x2f00
+	0x2f00: 0x0008, 0x2f01: 0x0008, 0x2f02: 0x0008, 0x2f03: 0x0008, 0x2f04: 0x0008, 0x2f05: 0x0008,
+	0x2f07: 0x0008,
+	// Block 0xbd, offset 0x2f40
+	0x2f50: 0x0008, 0x2f51: 0x0008,
+	0x2f55: 0x0008, 0x2f57: 0x0008,
+	// Block 0xbe, offset 0x2f80
+	0x2fb3: 0x0008, 0x2fb4: 0x0008,
+	// Block 0xbf, offset 0x2fc0
+	0x2fc0: 0x0008, 0x2fc1: 0x0008,
+	0x2ff6: 0x0008, 0x2ff7: 0x0008, 0x2ff8: 0x0008, 0x2ff9: 0x0008, 0x2ffa: 0x0008,
+	// Block 0xc0, offset 0x3000
+	0x3000: 0x0008, 0x3002: 0x0008,
+	// Block 0xc1, offset 0x3040
+	0x3040: 0x0008,
+	0x3047: 0x0008, 0x3048: 0x0008, 0x3049: 0x0008, 0x304a: 0x0008, 0x304b: 0x0008,
+	0x304c: 0x0008, 0x304d: 0x0008, 0x304e: 0x0008, 0x304f: 0x0008, 0x3050: 0x0008, 0x3051: 0x0008,
+	0x3052: 0x0008, 0x3053: 0x0008, 0x3054: 0x0008, 0x3055: 0x0008,
+	// Block 0xc2, offset 0x3080
+	0x30b0: 0x0008, 0x30b1: 0x0008, 0x30b2: 0x0008, 0x30b3: 0x0008, 0x30b4: 0x0008,
+	// Block 0xc3, offset 0x30c0
+	0x30f0: 0x0008, 0x30f1: 0x0008, 0x30f2: 0x0008, 0x30f3: 0x0008, 0x30f4: 0x0008, 0x30f5: 0x0008,
+	0x30f6: 0x0008,
+	// Block 0xc4, offset 0x3100
+	0x310f: 0x0008,
+	// Block 0xc5, offset 0x3140
+	0x314f: 0x0008, 0x3150: 0x0008, 0x3151: 0x0008,
+	0x3152: 0x0008,
+	// Block 0xc6, offset 0x3180
+	0x31a0: 0x0001, 0x31a1: 0x0001, 0x31a2: 0x0001, 0x31a3: 0x0001,
+	0x31a4: 0x0009,
+	0x31b0: 0x0001, 0x31b1: 0x0001,
+	// Block 0xc7, offset 0x31c0
+	0x31c0: 0x0001, 0x31c1: 0x0001, 0x31c2: 0x0001, 0x31c3: 0x0001, 0x31c4: 0x0001, 0x31c5: 0x0001,
+	0x31c6: 0x0001, 0x31c7: 0x0001, 0x31c8: 0x0001, 0x31c9: 0x0001, 0x31ca: 0x0001, 0x31cb: 0x0001,
+	0x31cc: 0x0001, 0x31cd: 0x0001, 0x31ce: 0x0001, 0x31cf: 0x0001, 0x31d0: 0x0001, 0x31d1: 0x0001,
+	0x31d2: 0x0001, 0x31d3: 0x0001, 0x31d4: 0x0001, 0x31d5: 0x0001, 0x31d6: 0x0001, 0x31d7: 0x0001,
+	0x31d8: 0x0001, 0x31d9: 0x0001, 0x31da: 0x0001, 0x31db: 0x0001, 0x31dc: 0x0001, 0x31dd: 0x0001,
+	0x31de: 0x0001, 0x31df: 0x0001, 0x31e0: 0x0001, 0x31e1: 0x0001, 0x31e2: 0x0001, 0x31e3: 0x0001,
+	0x31e4: 0x0001, 0x31e5: 0x0001, 0x31e6: 0x0001, 0x31e7: 0x0001, 0x31e8: 0x0001, 0x31e9: 0x0001,
+	0x31ea: 0x0001, 0x31eb: 0x0001, 0x31ec: 0x0001, 0x31ed: 0x0001, 0x31ee: 0x0001, 0x31ef: 0x0001,
+	0x31f0: 0x0001, 0x31f1: 0x0001, 0x31f2: 0x0001, 0x31f3: 0x0001, 0x31f4: 0x0001, 0x31f5: 0x0001,
+	0x31f6: 0x0001, 0x31f7: 0x0001,
+	// Block 0xc8, offset 0x3200
+	0x3200: 0x0001, 0x3201: 0x0001, 0x3202: 0x0001, 0x3203: 0x0001, 0x3204: 0x0001, 0x3205: 0x0001,
+	0x3206: 0x0001, 0x3207: 0x0001, 0x3208: 0x0001, 0x3209: 0x0001, 0x320a: 0x0001, 0x320b: 0x0001,
+	0x320c: 0x0001, 0x320d: 0x0001, 0x320e: 0x0001, 0x320f: 0x0001, 0x3210: 0x0001, 0x3211: 0x0001,
+	0x3212: 0x0001, 0x3213: 0x0001, 0x3214: 0x0001, 0x3215: 0x0001,
+	// Block 0xc9, offset 0x3240
+	0x3240: 0x0001, 0x3241: 0x0001, 0x3242: 0x0001, 0x3243: 0x0001, 0x3244: 0x0001, 0x3245: 0x0001,
+	0x3246: 0x0001, 0x3247: 0x0001, 0x3248: 0x0001,
+	// Block 0xca, offset 0x3280
+	0x32b0: 0x0001, 0x32b1: 0x0001, 0x32b2: 0x0001, 0x32b3: 0x0001, 0x32b5: 0x0001,
+	0x32b6: 0x0001, 0x32b7: 0x0001, 0x32b8: 0x0001, 0x32b9: 0x0001, 0x32ba: 0x0001, 0x32bb: 0x0001,
+	0x32bd: 0x0001, 0x32be: 0x0001,
+	// Block 0xcb, offset 0x32c0
+	0x32c0: 0x0001, 0x32c1: 0x0001, 0x32c2: 0x0001, 0x32c3: 0x0001, 0x32c4: 0x0001, 0x32c5: 0x0001,
+	0x32c6: 0x0001, 0x32c7: 0x0001, 0x32c8: 0x0001, 0x32c9: 0x0001, 0x32ca: 0x0001, 0x32cb: 0x0001,
+	0x32cc: 0x0001, 0x32cd: 0x0001, 0x32ce: 0x0001, 0x32cf: 0x0001, 0x32d0: 0x0001, 0x32d1: 0x0001,
+	0x32d2: 0x0001, 0x32d3: 0x0001, 0x32d4: 0x0001, 0x32d5: 0x0001, 0x32d6: 0x0001, 0x32d7: 0x0001,
+	0x32d8: 0x0001, 0x32d9: 0x0001, 0x32da: 0x0001, 0x32db: 0x0001, 0x32dc: 0x0001, 0x32dd: 0x0001,
+	0x32de: 0x0001, 0x32df: 0x0001, 0x32e0: 0x0001, 0x32e1: 0x0001, 0x32e2: 0x0001,
+	0x32f2: 0x0001,
+	// Block 0xcc, offset 0x3300
+	0x3310: 0x0001, 0x3311: 0x0001,
+	0x3312: 0x0001, 0x3315: 0x0001,
+	0x3324: 0x0001, 0x3325: 0x0001, 0x3326: 0x0001, 0x3327: 0x0001,
+	0x3330: 0x0001, 0x3331: 0x0001, 0x3332: 0x0001, 0x3333: 0x0001, 0x3334: 0x0001, 0x3335: 0x0001,
+	0x3336: 0x0001, 0x3337: 0x0001, 0x3338: 0x0001, 0x3339: 0x0001, 0x333a: 0x0001, 0x333b: 0x0001,
+	0x333c: 0x0001, 0x333d: 0x0001, 0x333e: 0x0001, 0x333f: 0x0001,
+	// Block 0xcd, offset 0x3340
+	0x3340: 0x0001, 0x3341: 0x0001, 0x3342: 0x0001, 0x3343: 0x0001, 0x3344: 0x0001, 0x3345: 0x0001,
+	0x3346: 0x0001, 0x3347: 0x0001, 0x3348: 0x0001, 0x3349: 0x0001, 0x334a: 0x0001, 0x334b: 0x0001,
+	0x334c: 0x0001, 0x334d: 0x0001, 0x334e: 0x0001, 0x334f: 0x0001, 0x3350: 0x0001, 0x3351: 0x0001,
+	0x3352: 0x0001, 0x3353: 0x0001, 0x3354: 0x0001, 0x3355: 0x0001, 0x3356: 0x0001, 0x3357: 0x0001,
+	0x3358: 0x0001, 0x3359: 0x0001, 0x335a: 0x0001, 0x335b: 0x0001, 0x335c: 0x0001, 0x335d: 0x0001,
+	0x335e: 0x0001, 0x335f: 0x0001, 0x3360: 0x0001, 0x3361: 0x0001, 0x3362: 0x0001, 0x3363: 0x0001,
+	0x3364: 0x0001, 0x3365: 0x0001, 0x3366: 0x0001, 0x3367: 0x0001, 0x3368: 0x0001, 0x3369: 0x0001,
+	0x336a: 0x0001, 0x336b: 0x0001, 0x336c: 0x0001, 0x336d: 0x0001, 0x336e: 0x0001, 0x336f: 0x0001,
+	0x3370: 0x0001, 0x3371: 0x0001, 0x3372: 0x0001, 0x3373: 0x0001, 0x3374: 0x0001, 0x3375: 0x0001,
+	0x3376: 0x0001, 0x3377: 0x0001, 0x3378: 0x0001, 0x3379: 0x0001, 0x337a: 0x0001, 0x337b: 0x0001,
+	// Block 0xce, offset 0x3380
+	0x339d: 0x0008,
+	0x339e: 0x0008, 0x33a0: 0x0008, 0x33a1: 0x0008, 0x33a2: 0x0008, 0x33a3: 0x0008,
+	// Block 0xcf, offset 0x33c0
+	0x33c0: 0x0008, 0x33c1: 0x0008, 0x33c2: 0x0008, 0x33c3: 0x0008, 0x33c4: 0x0008, 0x33c5: 0x0008,
+	0x33c6: 0x0008, 0x33c7: 0x0008, 0x33c8: 0x0008, 0x33c9: 0x0008, 0x33ca: 0x0008, 0x33cb: 0x0008,
+	0x33cc: 0x0008, 0x33cd: 0x0008, 0x33ce: 0x0008, 0x33cf: 0x0008, 0x33d0: 0x0008, 0x33d1: 0x0008,
+	0x33d2: 0x0008, 0x33d3: 0x0008, 0x33d4: 0x0008, 0x33d5: 0x0008, 0x33d6: 0x0008, 0x33d7: 0x0008,
+	0x33d8: 0x0008, 0x33d9: 0x0008, 0x33da: 0x0008, 0x33db: 0x0008, 0x33dc: 0x0008, 0x33dd: 0x0008,
+	0x33de: 0x0008, 0x33df: 0x0008, 0x33e0: 0x0008, 0x33e1: 0x0008, 0x33e2: 0x0008, 0x33e3: 0x0008,
+	0x33e4: 0x0008, 0x33e5: 0x0008, 0x33e6: 0x0008, 0x33e7: 0x0008, 0x33e8: 0x0008, 0x33e9: 0x0008,
+	0x33ea: 0x0008, 0x33eb: 0x0008, 0x33ec: 0x0008, 0x33ed: 0x0008,
+	0x33f0: 0x0008, 0x33f1: 0x0008, 0x33f2: 0x0008, 0x33f3: 0x0008, 0x33f4: 0x0008, 0x33f5: 0x0008,
+	0x33f6: 0x0008, 0x33f7: 0x0008, 0x33f8: 0x0008, 0x33f9: 0x0008, 0x33fa: 0x0008, 0x33fb: 0x0008,
+	0x33fc: 0x0008, 0x33fd: 0x0008, 0x33fe: 0x0008, 0x33ff: 0x0008,
+	// Block 0xd0, offset 0x3400
+	0x3400: 0x0008, 0x3401: 0x0008, 0x3402: 0x0008, 0x3403: 0x0008, 0x3404: 0x0008, 0x3405: 0x0008,
+	0x3406: 0x0008,
+	// Block 0xd1, offset 0x3440
+	0x3467: 0x0008, 0x3468: 0x0008, 0x3469: 0x0008,
+	0x3473: 0x0008, 0x3474: 0x0008, 0x3475: 0x0008,
+	0x3476: 0x0008, 0x3477: 0x0008, 0x3478: 0x0008, 0x3479: 0x0008, 0x347a: 0x0008, 0x347b: 0x0008,
+	0x347c: 0x0008, 0x347d: 0x0008, 0x347e: 0x0008, 0x347f: 0x0008,
+	// Block 0xd2, offset 0x3480
+	0x3480: 0x0008, 0x3481: 0x0008, 0x3482: 0x0008, 0x3485: 0x0008,
+	0x3486: 0x0008, 0x3487: 0x0008, 0x3488: 0x0008, 0x3489: 0x0008, 0x348a: 0x0008, 0x348b: 0x0008,
+	0x34aa: 0x0008, 0x34ab: 0x0008, 0x34ac: 0x0008, 0x34ad: 0x0008,
+	// Block 0xd3, offset 0x34c0
+	0x34c2: 0x0008, 0x34c3: 0x0008, 0x34c4: 0x0008,
+	// Block 0xd4, offset 0x3500
+	0x3500: 0x0008, 0x3501: 0x0008, 0x3502: 0x0008, 0x3503: 0x0008, 0x3504: 0x0008, 0x3505: 0x0008,
+	0x3506: 0x0008, 0x3507: 0x0008, 0x3508: 0x0008, 0x3509: 0x0008, 0x350a: 0x0008, 0x350b: 0x0008,
+	0x350c: 0x0008, 0x350d: 0x0008, 0x350e: 0x0008, 0x350f: 0x0008, 0x3510: 0x0008, 0x3511: 0x0008,
+	0x3512: 0x0008, 0x3513: 0x0008, 0x3514: 0x0008, 0x3515: 0x0008, 0x3516: 0x0008, 0x3517: 0x0008,
+	0x3518: 0x0008, 0x3519: 0x0008, 0x351a: 0x0008, 0x351b: 0x0008, 0x351c: 0x0008, 0x351d: 0x0008,
+	0x351e: 0x0008, 0x351f: 0x0008, 0x3520: 0x0008, 0x3521: 0x0008, 0x3522: 0x0008, 0x3523: 0x0008,
+	0x3524: 0x0008, 0x3525: 0x0008, 0x3526: 0x0008, 0x3527: 0x0008, 0x3528: 0x0008, 0x3529: 0x0008,
+	0x352a: 0x0008, 0x352b: 0x0008, 0x352c: 0x0008, 0x352d: 0x0008, 0x352e: 0x0008, 0x352f: 0x0008,
+	0x3530: 0x0008, 0x3531: 0x0008, 0x3532: 0x0008, 0x3533: 0x0008, 0x3534: 0x0008, 0x3535: 0x0008,
+	0x3536: 0x0008, 0x353b: 0x0008,
+	0x353c: 0x0008, 0x353d: 0x0008, 0x353e: 0x0008, 0x353f: 0x0008,
+	// Block 0xd5, offset 0x3540
+	0x3540: 0x0008, 0x3541: 0x0008, 0x3542: 0x0008, 0x3543: 0x0008, 0x3544: 0x0008, 0x3545: 0x0008,
+	0x3546: 0x0008, 0x3547: 0x0008, 0x3548: 0x0008, 0x3549: 0x0008, 0x354a: 0x0008, 0x354b: 0x0008,
+	0x354c: 0x0008, 0x354d: 0x0008, 0x354e: 0x0008, 0x354f: 0x0008, 0x3550: 0x0008, 0x3551: 0x0008,
+	0x3552: 0x0008, 0x3553: 0x0008, 0x3554: 0x0008, 0x3555: 0x0008, 0x3556: 0x0008, 0x3557: 0x0008,
+	0x3558: 0x0008, 0x3559: 0x0008, 0x355a: 0x0008, 0x355b: 0x0008, 0x355c: 0x0008, 0x355d: 0x0008,
+	0x355e: 0x0008, 0x355f: 0x0008, 0x3560: 0x0008, 0x3561: 0x0008, 0x3562: 0x0008, 0x3563: 0x0008,
+	0x3564: 0x0008, 0x3565: 0x0008, 0x3566: 0x0008, 0x3567: 0x0008, 0x3568: 0x0008, 0x3569: 0x0008,
+	0x356a: 0x0008, 0x356b: 0x0008, 0x356c: 0x0008,
+	0x3575: 0x0008,
+	// Block 0xd6, offset 0x3580
+	0x3584: 0x0008,
+	0x359b: 0x0008, 0x359c: 0x0008, 0x359d: 0x0008,
+	0x359e: 0x0008, 0x359f: 0x0008, 0x35a1: 0x0008, 0x35a2: 0x0008, 0x35a3: 0x0008,
+	0x35a4: 0x0008, 0x35a5: 0x0008, 0x35a6: 0x0008, 0x35a7: 0x0008, 0x35a8: 0x0008, 0x35a9: 0x0008,
+	0x35aa: 0x0008, 0x35ab: 0x0008, 0x35ac: 0x0008, 0x35ad: 0x0008, 0x35ae: 0x0008, 0x35af: 0x0008,
+	// Block 0xd7, offset 0x35c0
+	0x35c0: 0x0008, 0x35c1: 0x0008, 0x35c2: 0x0008, 0x35c3: 0x0008, 0x35c4: 0x0008, 0x35c5: 0x0008,
+	0x35c6: 0x0008, 0x35c8: 0x0008, 0x35c9: 0x0008, 0x35ca: 0x0008, 0x35cb: 0x0008,
+	0x35cc: 0x0008, 0x35cd: 0x0008, 0x35ce: 0x0008, 0x35cf: 0x0008, 0x35d0: 0x0008, 0x35d1: 0x0008,
+	0x35d2: 0x0008, 0x35d3: 0x0008, 0x35d4: 0x0008, 0x35d5: 0x0008, 0x35d6: 0x0008, 0x35d7: 0x0008,
+	0x35d8: 0x0008, 0x35db: 0x0008, 0x35dc: 0x0008, 0x35dd: 0x0008,
+	0x35de: 0x0008, 0x35df: 0x0008, 0x35e0: 0x0008, 0x35e1: 0x0008, 0x35e3: 0x0008,
+	0x35e4: 0x0008, 0x35e6: 0x0008, 0x35e7: 0x0008, 0x35e8: 0x0008, 0x35e9: 0x0008,
+	0x35ea: 0x0008,
+	// Block 0xd8, offset 0x3600
+	0x362e: 0x0008,
+	// Block 0xd9, offset 0x3640
+	0x366c: 0x0008, 0x366d: 0x0008, 0x366e: 0x0008, 0x366f: 0x0008,
+	// Block 0xda, offset 0x3680
+	0x3690: 0x0008, 0x3691: 0x0008,
+	0x3692: 0x0008, 0x3693: 0x0008, 0x3694: 0x0008, 0x3695: 0x0008, 0x3696: 0x0008,
+	// Block 0xdb, offset 0x36c0
+	0x36c4: 0x0008, 0x36c5: 0x0008,
+	0x36c6: 0x0008, 0x36c7: 0x0008, 0x36c8: 0x0008, 0x36c9: 0x0008, 0x36ca: 0x0008,
+	// Block 0xdc, offset 0x3700
+	0x3700: 0x0004, 0x3701: 0x0004, 0x3702: 0x0004, 0x3703: 0x0004, 0x3704: 0x0005, 0x3705: 0x0004,
+	0x3706: 0x0004, 0x3707: 0x0004, 0x3708: 0x0004, 0x3709: 0x0004, 0x370a: 0x0004, 0x370b: 0x0004,
+	0x370c: 0x0004, 0x370d: 0x0004, 0x370e: 0x0004, 0x370f: 0x0004, 0x3710: 0x0004, 0x3711: 0x0004,
+	0x3712: 0x0004, 0x3713: 0x0004, 0x3714: 0x0004, 0x3715: 0x0004, 0x3716: 0x0004, 0x3717: 0x0004,
+	0x3718: 0x0004, 0x3719: 0x0004, 0x371a: 0x0004, 0x371b: 0x0004, 0x371c: 0x0004, 0x371d: 0x0004,
+	0x371e: 0x0004, 0x371f: 0x0004, 0x3720: 0x0004, 0x3721: 0x0004, 0x3722: 0x0004, 0x3723: 0x0004,
+	0x3724: 0x0004, 0x3725: 0x0004, 0x3726: 0x0004, 0x3727: 0x0004, 0x3728: 0x0004, 0x3729: 0x0004,
+	0x372a: 0x0004, 0x372b: 0x0004, 0x372c: 0x0004, 0x372d: 0x0004, 0x372e: 0x0004, 0x372f: 0x0004,
+	0x3730: 0x0004, 0x3731: 0x0004, 0x3732: 0x0004, 0x3733: 0x0004, 0x3734: 0x0004, 0x3735: 0x0004,
+	0x3736: 0x0004, 0x3737: 0x0004, 0x3738: 0x0004, 0x3739: 0x0004, 0x373a: 0x0004, 0x373b: 0x0004,
+	0x373c: 0x0004, 0x373d: 0x0004, 0x373e: 0x0004, 0x373f: 0x0004,
+	// Block 0xdd, offset 0x3740
+	0x3740: 0x0004, 0x3741: 0x0004, 0x3742: 0x0004, 0x3743: 0x0004, 0x3744: 0x0004, 0x3745: 0x0004,
+	0x3746: 0x0004, 0x3747: 0x0004, 0x3748: 0x0004, 0x3749: 0x0004, 0x374a: 0x0004, 0x374b: 0x0004,
+	0x374c: 0x0004, 0x374d: 0x0004, 0x374e: 0x0004, 0x374f: 0x0004, 0x3750: 0x0004, 0x3751: 0x0004,
+	0x3752: 0x0004, 0x3753: 0x0004, 0x3754: 0x0004, 0x3755: 0x0004, 0x3756: 0x0004, 0x3757: 0x0004,
+	0x3758: 0x0004, 0x3759: 0x0004, 0x375a: 0x0004, 0x375b: 0x0004, 0x375c: 0x0004, 0x375d: 0x0004,
+	0x375e: 0x0004, 0x375f: 0x0004, 0x3760: 0x0004, 0x3761: 0x0004, 0x3762: 0x0004, 0x3763: 0x0004,
+	0x3764: 0x0004, 0x3765: 0x0004, 0x3766: 0x0004, 0x3767: 0x0004, 0x3768: 0x0004, 0x3769: 0x0004,
+	0x376a: 0x0004, 0x376b: 0x0004, 0x376c: 0x0004, 0x376d: 0x0004, 0x376e: 0x0004, 0x376f: 0x0004,
+	0x3770: 0x0004, 0x3771: 0x0004, 0x3772: 0x0004, 0x3773: 0x0004, 0x3774: 0x0004, 0x3775: 0x0004,
+	0x3776: 0x0004, 0x3777: 0x0004, 0x3778: 0x0004, 0x3779: 0x0004, 0x377a: 0x0004, 0x377b: 0x0004,
+	0x377c: 0x0004, 0x377d: 0x0004, 0x377e: 0x0004, 0x377f: 0x0004,
+	// Block 0xde, offset 0x3780
+	0x3780: 0x0004, 0x3781: 0x0004, 0x3782: 0x0004, 0x3783: 0x0004, 0x3784: 0x0004, 0x3785: 0x0004,
+	0x3786: 0x0004, 0x3787: 0x0004, 0x3788: 0x0004, 0x3789: 0x0004, 0x378a: 0x0004, 0x378b: 0x0004,
+	0x378c: 0x0004, 0x378d: 0x0004, 0x378e: 0x0004, 0x378f: 0x0005, 0x3790: 0x0004, 0x3791: 0x0004,
+	0x3792: 0x0004, 0x3793: 0x0004, 0x3794: 0x0004, 0x3795: 0x0004, 0x3796: 0x0004, 0x3797: 0x0004,
+	0x3798: 0x0004, 0x3799: 0x0004, 0x379a: 0x0004, 0x379b: 0x0004, 0x379c: 0x0004, 0x379d: 0x0004,
+	0x379e: 0x0004, 0x379f: 0x0004, 0x37a0: 0x0004, 0x37a1: 0x0004, 0x37a2: 0x0004, 0x37a3: 0x0004,
+	0x37a4: 0x0004, 0x37a5: 0x0004, 0x37a6: 0x0004, 0x37a7: 0x0004, 0x37a8: 0x0004, 0x37a9: 0x0004,
+	0x37aa: 0x0004, 0x37ab: 0x0004, 0x37ac: 0x0004, 0x37ad: 0x0004, 0x37ae: 0x0004, 0x37af: 0x0004,
+	0x37b0: 0x0004, 0x37b1: 0x0004, 0x37b2: 0x0004, 0x37b3: 0x0004, 0x37b4: 0x0004, 0x37b5: 0x0004,
+	0x37b6: 0x0004, 0x37b7: 0x0004, 0x37b8: 0x0004, 0x37b9: 0x0004, 0x37ba: 0x0004, 0x37bb: 0x0004,
+	0x37bc: 0x0004, 0x37bd: 0x0004, 0x37be: 0x0004, 0x37bf: 0x0004,
+	// Block 0xdf, offset 0x37c0
+	0x37c0: 0x0002, 0x37c1: 0x0002, 0x37c2: 0x0002, 0x37c3: 0x0002, 0x37c4: 0x0002, 0x37c5: 0x0002,
+	0x37c6: 0x0002, 0x37c7: 0x0002, 0x37c8: 0x0002, 0x37c9: 0x0002, 0x37ca: 0x0002,
+	0x37cd: 0x0004, 0x37ce: 0x0004, 0x37cf: 0x0004, 0x37d0: 0x0002, 0x37d1: 0x0002,
+	0x37d2: 0x0002, 0x37d3: 0x0002, 0x37d4: 0x0002, 0x37d5: 0x0002, 0x37d6: 0x0002, 0x37d7: 0x0002,
+	0x37d8: 0x0002, 0x37d9: 0x0002, 0x37da: 0x0002, 0x37db: 0x0002, 0x37dc: 0x0002, 0x37dd: 0x0002,
+	0x37de: 0x0002, 0x37df: 0x0002, 0x37e0: 0x0002, 0x37e1: 0x0002, 0x37e2: 0x0002, 0x37e3: 0x0002,
+	0x37e4: 0x0002, 0x37e5: 0x0002, 0x37e6: 0x0002, 0x37e7: 0x0002, 0x37e8: 0x0002, 0x37e9: 0x0002,
+	0x37ea: 0x0002, 0x37eb: 0x0002, 0x37ec: 0x0002, 0x37ed: 0x0002, 0x37ef: 0x0004,
+	0x37f0: 0x0002, 0x37f1: 0x0002, 0x37f2: 0x0002, 0x37f3: 0x0002, 0x37f4: 0x0002, 0x37f5: 0x0002,
+	0x37f6: 0x0002, 0x37f7: 0x0002, 0x37f8: 0x0002, 0x37f9: 0x0002, 0x37fa: 0x0002, 0x37fb: 0x0002,
+	0x37fc: 0x0002, 0x37fd: 0x0002, 0x37fe: 0x0002, 0x37ff: 0x0002,
+	// Block 0xe0, offset 0x3800
+	0x3800: 0x0002, 0x3801: 0x0002, 0x3802: 0x0002, 0x3803: 0x0002, 0x3804: 0x0002, 0x3805: 0x0002,
+	0x3806: 0x0002, 0x3807: 0x0002, 0x3808: 0x0002, 0x3809: 0x0002, 0x380a: 0x0002, 0x380b: 0x0002,
+	0x380c: 0x0002, 0x380d: 0x0002, 0x380e: 0x0002, 0x380f: 0x0002, 0x3810: 0x0002, 0x3811: 0x0002,
+	0x3812: 0x0002, 0x3813: 0x0002, 0x3814: 0x0002, 0x3815: 0x0002, 0x3816: 0x0002, 0x3817: 0x0002,
+	0x3818: 0x0002, 0x3819: 0x0002, 0x381a: 0x0002, 0x381b: 0x0002, 0x381c: 0x0002, 0x381d: 0x0002,
+	0x381e: 0x0002, 0x381f: 0x0002, 0x3820: 0x0002, 0x3821: 0x0002, 0x3822: 0x0002, 0x3823: 0x0002,
+	0x3824: 0x0002, 0x3825: 0x0002, 0x3826: 0x0002, 0x3827: 0x0002, 0x3828: 0x0002, 0x3829: 0x0002,
+	0x382c: 0x0004, 0x382d: 0x0004, 0x382e: 0x0004, 0x382f: 0x0004,
+	0x3830: 0x0006, 0x3831: 0x0006, 0x3832: 0x0002, 0x3833: 0x0002, 0x3834: 0x0002, 0x3835: 0x0002,
+	0x3836: 0x0002, 0x3837: 0x0002, 0x3838: 0x0002, 0x3839: 0x0002, 0x383a: 0x0002, 0x383b: 0x0002,
+	0x383c: 0x0002, 0x383d: 0x0002, 0x383e: 0x0006, 0x383f: 0x0006,
+	// Block 0xe1, offset 0x3840
+	0x3840: 0x0002, 0x3841: 0x0002, 0x3842: 0x0002, 0x3843: 0x0002, 0x3844: 0x0002, 0x3845: 0x0002,
+	0x3846: 0x0002, 0x3847: 0x0002, 0x3848: 0x0002, 0x3849: 0x0002, 0x384a: 0x0002, 0x384b: 0x0002,
+	0x384c: 0x0002, 0x384d: 0x0002, 0x384e: 0x0005, 0x384f: 0x0002, 0x3850: 0x0002, 0x3851: 0x0005,
+	0x3852: 0x0005, 0x3853: 0x0005, 0x3854: 0x0005, 0x3855: 0x0005, 0x3856: 0x0005, 0x3857: 0x0005,
+	0x3858: 0x0005, 0x3859: 0x0005, 0x385a: 0x0005, 0x385b: 0x0002, 0x385c: 0x0002, 0x385d: 0x0002,
+	0x385e: 0x0002, 0x385f: 0x0002, 0x3860: 0x0002, 0x3861: 0x0002, 0x3862: 0x0002, 0x3863: 0x0002,
+	0x3864: 0x0002, 0x3865: 0x0002, 0x3866: 0x0002, 0x3867: 0x0002, 0x3868: 0x0002, 0x3869: 0x0002,
+	0x386a: 0x0002, 0x386b: 0x0002, 0x386c: 0x0002, 0x386d: 0x0004, 0x386e: 0x0004, 0x386f: 0x0004,
+	0x3870: 0x0004, 0x3871: 0x0004, 0x3872: 0x0004, 0x3873: 0x0004, 0x3874: 0x0004, 0x3875: 0x0004,
+	0x3876: 0x0004, 0x3877: 0x0004, 0x3878: 0x0004, 0x3879: 0x0004, 0x387a: 0x0004, 0x387b: 0x0004,
+	0x387c: 0x0004, 0x387d: 0x0004, 0x387e: 0x0004, 0x387f: 0x0004,
+	// Block 0xe2, offset 0x3880
+	0x3880: 0x0004, 0x3881: 0x0004, 0x3882: 0x0004, 0x3883: 0x0004, 0x3884: 0x0004, 0x3885: 0x0004,
+	0x3886: 0x0004, 0x3887: 0x0004, 0x3888: 0x0004, 0x3889: 0x0004, 0x388a: 0x0004, 0x388b: 0x0004,
+	0x388c: 0x0004, 0x388d: 0x0004, 0x388e: 0x0004, 0x388f: 0x0004, 0x3890: 0x0004, 0x3891: 0x0004,
+	0x3892: 0x0004, 0x3893: 0x0004, 0x3894: 0x0004, 0x3895: 0x0004, 0x3896: 0x0004, 0x3897: 0x0004,
+	0x3898: 0x0004, 0x3899: 0x0004, 0x389a: 0x0004, 0x389b: 0x0004, 0x389c: 0x0004, 0x389d: 0x0004,
+	0x389e: 0x0004, 0x389f: 0x0004, 0x38a0: 0x0004, 0x38a1: 0x0004, 0x38a2: 0x0004, 0x38a3: 0x0004,
+	0x38a4: 0x0004, 0x38a5: 0x0004,
+	// Block 0xe3, offset 0x38c0
+	0x38c0: 0x0001, 0x38c1: 0x0005, 0x38c2: 0x0005, 0x38c3: 0x0004, 0x38c4: 0x0004, 0x38c5: 0x0004,
+	0x38c6: 0x0004, 0x38c7: 0x0004, 0x38c8: 0x0004, 0x38c9: 0x0004, 0x38ca: 0x0004, 0x38cb: 0x0004,
+	0x38cc: 0x0004, 0x38cd: 0x0004, 0x38ce: 0x0004, 0x38cf: 0x0004, 0x38d0: 0x0001, 0x38d1: 0x0001,
+	0x38d2: 0x0001, 0x38d3: 0x0001, 0x38d4: 0x0001, 0x38d5: 0x0001, 0x38d6: 0x0001, 0x38d7: 0x0001,
+	0x38d8: 0x0001, 0x38d9: 0x0001, 0x38da: 0x0005, 0x38db: 0x0001, 0x38dc: 0x0001, 0x38dd: 0x0001,
+	0x38de: 0x0001, 0x38df: 0x0001, 0x38e0: 0x0001, 0x38e1: 0x0001, 0x38e2: 0x0001, 0x38e3: 0x0001,
+	0x38e4: 0x0001, 0x38e5: 0x0001, 0x38e6: 0x0001, 0x38e7: 0x0001, 0x38e8: 0x0001, 0x38e9: 0x0001,
+	0x38ea: 0x0001, 0x38eb: 0x0001, 0x38ec: 0x0001, 0x38ed: 0x0001, 0x38ee: 0x0001, 0x38ef: 0x0005,
+	0x38f0: 0x0001, 0x38f1: 0x0001, 0x38f2: 0x0005, 0x38f3: 0x0005, 0x38f4: 0x0005, 0x38f5: 0x0005,
+	0x38f6: 0x0005, 0x38f7: 0x0005, 0x38f8: 0x0005, 0x38f9: 0x0005, 0x38fa: 0x0005, 0x38fb: 0x0001,
+	0x38fc: 0x0004, 0x38fd: 0x0004, 0x38fe: 0x0004, 0x38ff: 0x0004,
+	// Block 0xe4, offset 0x3900
+	0x3900: 0x0001, 0x3901: 0x0001, 0x3902: 0x0001, 0x3903: 0x0001, 0x3904: 0x0001, 0x3905: 0x0001,
+	0x3906: 0x0001, 0x3907: 0x0001, 0x3908: 0x0001, 0x3909: 0x0004, 0x390a: 0x0004, 0x390b: 0x0004,
+	0x390c: 0x0004, 0x390d: 0x0004, 0x390e: 0x0004, 0x390f: 0x0004, 0x3910: 0x0005, 0x3911: 0x0005,
+	0x3912: 0x0004, 0x3913: 0x0004, 0x3914: 0x0004, 0x3915: 0x0004, 0x3916: 0x0004, 0x3917: 0x0004,
+	0x3918: 0x0004, 0x3919: 0x0004, 0x391a: 0x0004, 0x391b: 0x0004, 0x391c: 0x0004, 0x391d: 0x0004,
+	0x391e: 0x0004, 0x391f: 0x0004, 0x3920: 0x0005, 0x3921: 0x0005, 0x3922: 0x0005, 0x3923: 0x0005,
+	0x3924: 0x0005, 0x3925: 0x0005, 0x3926: 0x0004, 0x3927: 0x0004, 0x3928: 0x0004, 0x3929: 0x0004,
+	0x392a: 0x0004, 0x392b: 0x0004, 0x392c: 0x0004, 0x392d: 0x0004, 0x392e: 0x0004, 0x392f: 0x0004,
+	0x3930: 0x0004, 0x3931: 0x0004, 0x3932: 0x0004, 0x3933: 0x0004, 0x3934: 0x0004, 0x3935: 0x0004,
+	0x3936: 0x0004, 0x3937: 0x0004, 0x3938: 0x0004, 0x3939: 0x0004, 0x393a: 0x0004, 0x393b: 0x0004,
+	0x393c: 0x0004, 0x393d: 0x0004, 0x393e: 0x0004, 0x393f: 0x0004,
+	// Block 0xe5, offset 0x3940
+	0x3940: 0x0005, 0x3941: 0x0005, 0x3942: 0x0005, 0x3943: 0x0005, 0x3944: 0x0005, 0x3945: 0x0005,
+	0x3946: 0x0005, 0x3947: 0x0005, 0x3948: 0x0005, 0x3949: 0x0005, 0x394a: 0x0005, 0x394b: 0x0005,
+	0x394c: 0x0005, 0x394d: 0x0005, 0x394e: 0x0005, 0x394f: 0x0005, 0x3950: 0x0005, 0x3951: 0x0005,
+	0x3952: 0x0005, 0x3953: 0x0005, 0x3954: 0x0005, 0x3955: 0x0005, 0x3956: 0x0005, 0x3957: 0x0005,
+	0x3958: 0x0005, 0x3959: 0x0005, 0x395a: 0x0005, 0x395b: 0x0005, 0x395c: 0x0005, 0x395d: 0x0005,
+	0x395e: 0x0005, 0x395f: 0x0005, 0x3960: 0x0005, 0x3961: 0x0004, 0x3962: 0x0004, 0x3963: 0x0004,
+	0x3964: 0x0004, 0x3965: 0x0004, 0x3966: 0x0004, 0x3967: 0x0004, 0x3968: 0x0004, 0x3969: 0x0004,
+	0x396a: 0x0004, 0x396b: 0x0004, 0x396c: 0x0004, 0x396d: 0x0005, 0x396e: 0x0005, 0x396f: 0x0005,
+	0x3970: 0x0005, 0x3971: 0x0005, 0x3972: 0x0005, 0x3973: 0x0005, 0x3974: 0x0005, 0x3975: 0x0005,
+	0x3976: 0x0004, 0x3977: 0x0005, 0x3978: 0x0005, 0x3979: 0x0005, 0x397a: 0x0005, 0x397b: 0x0005,
+	0x397c: 0x0005, 0x397d: 0x0005, 0x397e: 0x0005, 0x397f: 0x0005,
+	// Block 0xe6, offset 0x3980
+	0x3980: 0x0005, 0x3981: 0x0005, 0x3982: 0x0005, 0x3983: 0x0005, 0x3984: 0x0005, 0x3985: 0x0005,
+	0x3986: 0x0005, 0x3987: 0x0005, 0x3988: 0x0005, 0x3989: 0x0005, 0x398a: 0x0005, 0x398b: 0x0005,
+	0x398c: 0x0005, 0x398d: 0x0005, 0x398e: 0x0005, 0x398f: 0x0005, 0x3990: 0x0005, 0x3991: 0x0005,
+	0x3992: 0x0005, 0x3993: 0x0005, 0x3994: 0x0005, 0x3995: 0x0005, 0x3996: 0x0005, 0x3997: 0x0005,
+	0x3998: 0x0005, 0x3999: 0x0005, 0x399a: 0x0005, 0x399b: 0x0005, 0x399c: 0x0005, 0x399d: 0x0005,
+	0x399e: 0x0005, 0x399f: 0x0005, 0x39a0: 0x0005, 0x39a1: 0x0005, 0x39a2: 0x0005, 0x39a3: 0x0005,
+	0x39a4: 0x0005, 0x39a5: 0x0005, 0x39a6: 0x0005, 0x39a7: 0x0005, 0x39a8: 0x0005, 0x39a9: 0x0005,
+	0x39aa: 0x0005, 0x39ab: 0x0005, 0x39ac: 0x0005, 0x39ad: 0x0005, 0x39ae: 0x0005, 0x39af: 0x0005,
+	0x39b0: 0x0005, 0x39b1: 0x0005, 0x39b2: 0x0005, 0x39b3: 0x0005, 0x39b4: 0x0005, 0x39b5: 0x0005,
+	0x39b6: 0x0005, 0x39b7: 0x0005, 0x39b8: 0x0005, 0x39b9: 0x0005, 0x39ba: 0x0005, 0x39bb: 0x0005,
+	0x39bc: 0x0005, 0x39bd: 0x0004, 0x39be: 0x0005, 0x39bf: 0x0005,
+	// Block 0xe7, offset 0x39c0
+	0x39c0: 0x0005, 0x39c1: 0x0005, 0x39c2: 0x0005, 0x39c3: 0x0005, 0x39c4: 0x0005, 0x39c5: 0x0005,
+	0x39c6: 0x0005, 0x39c7: 0x0005, 0x39c8: 0x0005, 0x39c9: 0x0005, 0x39ca: 0x0005, 0x39cb: 0x0005,
+	0x39cc: 0x0005, 0x39cd: 0x0005, 0x39ce: 0x0005, 0x39cf: 0x0005, 0x39d0: 0x0005, 0x39d1: 0x0005,
+	0x39d2: 0x0005, 0x39d3: 0x0005, 0x39d4: 0x0004, 0x39d5: 0x0004, 0x39d6: 0x0004, 0x39d7: 0x0004,
+	0x39d8: 0x0004, 0x39d9: 0x0004, 0x39da: 0x0004, 0x39db: 0x0004, 0x39dc: 0x0004, 0x39dd: 0x0004,
+	0x39de: 0x0004, 0x39df: 0x0004, 0x39e0: 0x0005, 0x39e1: 0x0005, 0x39e2: 0x0005, 0x39e3: 0x0005,
+	0x39e4: 0x0005, 0x39e5: 0x0005, 0x39e6: 0x0005, 0x39e7: 0x0005, 0x39e8: 0x0005, 0x39e9: 0x0005,
+	0x39ea: 0x0005, 0x39eb: 0x0005, 0x39ec: 0x0005, 0x39ed: 0x0005, 0x39ee: 0x0005, 0x39ef: 0x0005,
+	0x39f0: 0x0005, 0x39f1: 0x0005, 0x39f2: 0x0005, 0x39f3: 0x0005, 0x39f4: 0x0005, 0x39f5: 0x0005,
+	0x39f6: 0x0005, 0x39f7: 0x0005, 0x39f8: 0x0005, 0x39f9: 0x0005, 0x39fa: 0x0005, 0x39fb: 0x0005,
+	0x39fc: 0x0005, 0x39fd: 0x0005, 0x39fe: 0x0005, 0x39ff: 0x0005,
+	// Block 0xe8, offset 0x3a00
+	0x3a00: 0x0005, 0x3a01: 0x0005, 0x3a02: 0x0005, 0x3a03: 0x0005, 0x3a04: 0x0005, 0x3a05: 0x0005,
+	0x3a06: 0x0005, 0x3a07: 0x0005, 0x3a08: 0x0005, 0x3a09: 0x0005, 0x3a0a: 0x0005, 0x3a0b: 0x0004,
+	0x3a0c: 0x0004, 0x3a0d: 0x0004, 0x3a0e: 0x0004, 0x3a0f: 0x0005, 0x3a10: 0x0005, 0x3a11: 0x0005,
+	0x3a12: 0x0005, 0x3a13: 0x0005, 0x3a14: 0x0004, 0x3a15: 0x0004, 0x3a16: 0x0004, 0x3a17: 0x0004,
+	0x3a18: 0x0004, 0x3a19: 0x0004, 0x3a1a: 0x0004, 0x3a1b: 0x0004, 0x3a1c: 0x0004, 0x3a1d: 0x0004,
+	0x3a1e: 0x0004, 0x3a1f: 0x0004, 0x3a20: 0x0005, 0x3a21: 0x0005, 0x3a22: 0x0005, 0x3a23: 0x0005,
+	0x3a24: 0x0005, 0x3a25: 0x0005, 0x3a26: 0x0005, 0x3a27: 0x0005, 0x3a28: 0x0005, 0x3a29: 0x0005,
+	0x3a2a: 0x0005, 0x3a2b: 0x0005, 0x3a2c: 0x0005, 0x3a2d: 0x0005, 0x3a2e: 0x0005, 0x3a2f: 0x0005,
+	0x3a30: 0x0005, 0x3a31: 0x0004, 0x3a32: 0x0004, 0x3a33: 0x0004, 0x3a34: 0x0005, 0x3a35: 0x0004,
+	0x3a36: 0x0004, 0x3a37: 0x0004, 0x3a38: 0x0005, 0x3a39: 0x0005, 0x3a3a: 0x0005, 0x3a3b: 0x0001,
+	0x3a3c: 0x0001, 0x3a3d: 0x0001, 0x3a3e: 0x0001, 0x3a3f: 0x0001,
+	// Block 0xe9, offset 0x3a40
+	0x3a40: 0x0005, 0x3a41: 0x0005, 0x3a42: 0x0005, 0x3a43: 0x0005, 0x3a44: 0x0005, 0x3a45: 0x0005,
+	0x3a46: 0x0005, 0x3a47: 0x0005, 0x3a48: 0x0005, 0x3a49: 0x0005, 0x3a4a: 0x0005, 0x3a4b: 0x0005,
+	0x3a4c: 0x0005, 0x3a4d: 0x0005, 0x3a4e: 0x0005, 0x3a4f: 0x0005, 0x3a50: 0x0005, 0x3a51: 0x0005,
+	0x3a52: 0x0005, 0x3a53: 0x0005, 0x3a54: 0x0005, 0x3a55: 0x0005, 0x3a56: 0x0005, 0x3a57: 0x0005,
+	0x3a58: 0x0005, 0x3a59: 0x0005, 0x3a5a: 0x0005, 0x3a5b: 0x0005, 0x3a5c: 0x0005, 0x3a5d: 0x0005,
+	0x3a5e: 0x0005, 0x3a5f: 0x0005, 0x3a60: 0x0005, 0x3a61: 0x0005, 0x3a62: 0x0005, 0x3a63: 0x0005,
+	0x3a64: 0x0005, 0x3a65: 0x0005, 0x3a66: 0x0005, 0x3a67: 0x0005, 0x3a68: 0x0005, 0x3a69: 0x0005,
+	0x3a6a: 0x0005, 0x3a6b: 0x0005, 0x3a6c: 0x0005, 0x3a6d: 0x0005, 0x3a6e: 0x0005, 0x3a6f: 0x0005,
+	0x3a70: 0x0005, 0x3a71: 0x0005, 0x3a72: 0x0005, 0x3a73: 0x0005, 0x3a74: 0x0005, 0x3a75: 0x0005,
+	0x3a76: 0x0005, 0x3a77: 0x0005, 0x3a78: 0x0005, 0x3a79: 0x0005, 0x3a7a: 0x0005, 0x3a7b: 0x0005,
+	0x3a7c: 0x0005, 0x3a7d: 0x0005, 0x3a7e: 0x0005, 0x3a7f: 0x0004,
+	// Block 0xea, offset 0x3a80
+	0x3a80: 0x0005, 0x3a81: 0x0004, 0x3a82: 0x0005, 0x3a83: 0x0005, 0x3a84: 0x0005, 0x3a85: 0x0005,
+	0x3a86: 0x0005, 0x3a87: 0x0005, 0x3a88: 0x0005, 0x3a89: 0x0005, 0x3a8a: 0x0005, 0x3a8b: 0x0005,
+	0x3a8c: 0x0005, 0x3a8d: 0x0005, 0x3a8e: 0x0005, 0x3a8f: 0x0005, 0x3a90: 0x0005, 0x3a91: 0x0005,
+	0x3a92: 0x0005, 0x3a93: 0x0005, 0x3a94: 0x0005, 0x3a95: 0x0005, 0x3a96: 0x0005, 0x3a97: 0x0005,
+	0x3a98: 0x0005, 0x3a99: 0x0005, 0x3a9a: 0x0005, 0x3a9b: 0x0005, 0x3a9c: 0x0005, 0x3a9d: 0x0005,
+	0x3a9e: 0x0005, 0x3a9f: 0x0005, 0x3aa0: 0x0005, 0x3aa1: 0x0005, 0x3aa2: 0x0005, 0x3aa3: 0x0005,
+	0x3aa4: 0x0005, 0x3aa5: 0x0005, 0x3aa6: 0x0005, 0x3aa7: 0x0005, 0x3aa8: 0x0005, 0x3aa9: 0x0005,
+	0x3aaa: 0x0005, 0x3aab: 0x0005, 0x3aac: 0x0005, 0x3aad: 0x0005, 0x3aae: 0x0005, 0x3aaf: 0x0005,
+	0x3ab0: 0x0005, 0x3ab1: 0x0005, 0x3ab2: 0x0005, 0x3ab3: 0x0005, 0x3ab4: 0x0005, 0x3ab5: 0x0005,
+	0x3ab6: 0x0005, 0x3ab7: 0x0005, 0x3ab8: 0x0005, 0x3ab9: 0x0005, 0x3aba: 0x0005, 0x3abb: 0x0005,
+	0x3abc: 0x0005, 0x3abd: 0x0005, 0x3abe: 0x0005, 0x3abf: 0x0005,
+	// Block 0xeb, offset 0x3ac0
+	0x3ac0: 0x0005, 0x3ac1: 0x0005, 0x3ac2: 0x0005, 0x3ac3: 0x0005, 0x3ac4: 0x0005, 0x3ac5: 0x0005,
+	0x3ac6: 0x0005, 0x3ac7: 0x0005, 0x3ac8: 0x0005, 0x3ac9: 0x0005, 0x3aca: 0x0005, 0x3acb: 0x0005,
+	0x3acc: 0x0005, 0x3acd: 0x0005, 0x3ace: 0x0005, 0x3acf: 0x0005, 0x3ad0: 0x0005, 0x3ad1: 0x0005,
+	0x3ad2: 0x0005, 0x3ad3: 0x0005, 0x3ad4: 0x0005, 0x3ad5: 0x0005, 0x3ad6: 0x0005, 0x3ad7: 0x0005,
+	0x3ad8: 0x0005, 0x3ad9: 0x0005, 0x3ada: 0x0005, 0x3adb: 0x0005, 0x3adc: 0x0005, 0x3add: 0x0005,
+	0x3ade: 0x0005, 0x3adf: 0x0005, 0x3ae0: 0x0005, 0x3ae1: 0x0005, 0x3ae2: 0x0005, 0x3ae3: 0x0005,
+	0x3ae4: 0x0005, 0x3ae5: 0x0005, 0x3ae6: 0x0005, 0x3ae7: 0x0005, 0x3ae8: 0x0005, 0x3ae9: 0x0005,
+	0x3aea: 0x0005, 0x3aeb: 0x0005, 0x3aec: 0x0005, 0x3aed: 0x0005, 0x3aee: 0x0005, 0x3aef: 0x0005,
+	0x3af0: 0x0005, 0x3af1: 0x0005, 0x3af2: 0x0005, 0x3af3: 0x0005, 0x3af4: 0x0005, 0x3af5: 0x0005,
+	0x3af6: 0x0005, 0x3af7: 0x0005, 0x3af8: 0x0005, 0x3af9: 0x0005, 0x3afa: 0x0005, 0x3afb: 0x0005,
+	0x3afc: 0x0005, 0x3afd: 0x0005, 0x3afe: 0x0005, 0x3aff: 0x0005,
+	// Block 0xec, offset 0x3b00
+	0x3b00: 0x0005, 0x3b01: 0x0005, 0x3b02: 0x0005, 0x3b03: 0x0005, 0x3b04: 0x0005, 0x3b05: 0x0005,
+	0x3b06: 0x0005, 0x3b07: 0x0005, 0x3b08: 0x0005, 0x3b09: 0x0005, 0x3b0a: 0x0005, 0x3b0b: 0x0005,
+	0x3b0c: 0x0005, 0x3b0d: 0x0005, 0x3b0e: 0x0005, 0x3b0f: 0x0005, 0x3b10: 0x0005, 0x3b11: 0x0005,
+	0x3b12: 0x0005, 0x3b13: 0x0005, 0x3b14: 0x0005, 0x3b15: 0x0005, 0x3b16: 0x0005, 0x3b17: 0x0005,
+	0x3b18: 0x0005, 0x3b19: 0x0005, 0x3b1a: 0x0005, 0x3b1b: 0x0005, 0x3b1c: 0x0005, 0x3b1d: 0x0005,
+	0x3b1e: 0x0005, 0x3b1f: 0x0005, 0x3b20: 0x0005, 0x3b21: 0x0005, 0x3b22: 0x0005, 0x3b23: 0x0005,
+	0x3b24: 0x0005, 0x3b25: 0x0005, 0x3b26: 0x0005, 0x3b27: 0x0005, 0x3b28: 0x0005, 0x3b29: 0x0005,
+	0x3b2a: 0x0005, 0x3b2b: 0x0005, 0x3b2c: 0x0005, 0x3b2d: 0x0005, 0x3b2e: 0x0005, 0x3b2f: 0x0005,
+	0x3b30: 0x0005, 0x3b31: 0x0005, 0x3b32: 0x0005, 0x3b33: 0x0005, 0x3b34: 0x0005, 0x3b35: 0x0005,
+	0x3b36: 0x0005, 0x3b37: 0x0005, 0x3b38: 0x0005, 0x3b39: 0x0005, 0x3b3a: 0x0005, 0x3b3b: 0x0005,
+	0x3b3c: 0x0005, 0x3b3d: 0x0004, 0x3b3e: 0x0004, 0x3b3f: 0x0005,
+	// Block 0xed, offset 0x3b40
+	0x3b40: 0x0005, 0x3b41: 0x0005, 0x3b42: 0x0005, 0x3b43: 0x0005, 0x3b44: 0x0005, 0x3b45: 0x0005,
+	0x3b46: 0x0005, 0x3b47: 0x0005, 0x3b48: 0x0005, 0x3b49: 0x0005, 0x3b4a: 0x0005, 0x3b4b: 0x0005,
+	0x3b4c: 0x0005, 0x3b4d: 0x0005, 0x3b4e: 0x0005, 0x3b4f: 0x0005, 0x3b50: 0x0005, 0x3b51: 0x0005,
+	0x3b52: 0x0005, 0x3b53: 0x0005, 0x3b54: 0x0005, 0x3b55: 0x0005, 0x3b56: 0x0005, 0x3b57: 0x0005,
+	0x3b58: 0x0005, 0x3b59: 0x0005, 0x3b5a: 0x0005, 0x3b5b: 0x0005, 0x3b5c: 0x0005, 0x3b5d: 0x0005,
+	0x3b5e: 0x0005, 0x3b5f: 0x0005, 0x3b60: 0x0005, 0x3b61: 0x0005, 0x3b62: 0x0005, 0x3b63: 0x0005,
+	0x3b64: 0x0005, 0x3b65: 0x0005, 0x3b66: 0x0005, 0x3b67: 0x0005, 0x3b68: 0x0005, 0x3b69: 0x0005,
+	0x3b6a: 0x0005, 0x3b6b: 0x0005, 0x3b6c: 0x0005, 0x3b6d: 0x0005, 0x3b6e: 0x0005, 0x3b6f: 0x0005,
+	0x3b70: 0x0005, 0x3b71: 0x0005, 0x3b72: 0x0005, 0x3b73: 0x0005, 0x3b74: 0x0005, 0x3b75: 0x0005,
+	0x3b76: 0x0005, 0x3b77: 0x0005, 0x3b78: 0x0005, 0x3b79: 0x0005, 0x3b7a: 0x0005, 0x3b7b: 0x0005,
+	0x3b7c: 0x0005, 0x3b7d: 0x0005,
+	// Block 0xee, offset 0x3b80
+	0x3b86: 0x0004, 0x3b87: 0x0004, 0x3b88: 0x0004, 0x3b89: 0x0004, 0x3b8a: 0x0004, 0x3b8b: 0x0005,
+	0x3b8c: 0x0005, 0x3b8d: 0x0005, 0x3b8e: 0x0005, 0x3b8f: 0x0004, 0x3b90: 0x0005, 0x3b91: 0x0005,
+	0x3b92: 0x0005, 0x3b93: 0x0005, 0x3b94: 0x0005, 0x3b95: 0x0005, 0x3b96: 0x0005, 0x3b97: 0x0005,
+	0x3b98: 0x0005, 0x3b99: 0x0005, 0x3b9a: 0x0005, 0x3b9b: 0x0005, 0x3b9c: 0x0005, 0x3b9d: 0x0005,
+	0x3b9e: 0x0005, 0x3b9f: 0x0005, 0x3ba0: 0x0005, 0x3ba1: 0x0005, 0x3ba2: 0x0005, 0x3ba3: 0x0005,
+	0x3ba4: 0x0005, 0x3ba5: 0x0005, 0x3ba6: 0x0005, 0x3ba7: 0x0005, 0x3ba8: 0x0004, 0x3ba9: 0x0004,
+	0x3baa: 0x0004, 0x3bab: 0x0004, 0x3bac: 0x0004, 0x3bad: 0x0004, 0x3bae: 0x0004, 0x3baf: 0x0004,
+	0x3bb0: 0x0004, 0x3bb1: 0x0004, 0x3bb2: 0x0004, 0x3bb3: 0x0004, 0x3bb4: 0x0004, 0x3bb5: 0x0004,
+	0x3bb6: 0x0004, 0x3bb7: 0x0004, 0x3bb8: 0x0004, 0x3bb9: 0x0004, 0x3bba: 0x0005, 0x3bbb: 0x0004,
+	0x3bbc: 0x0004, 0x3bbd: 0x0004, 0x3bbe: 0x0004, 0x3bbf: 0x0004,
+	// Block 0xef, offset 0x3bc0
+	0x3bc0: 0x0004, 0x3bc1: 0x0004, 0x3bc2: 0x0004, 0x3bc3: 0x0004, 0x3bc4: 0x0004, 0x3bc5: 0x0004,
+	0x3bc6: 0x0004, 0x3bc7: 0x0004, 0x3bc8: 0x0004, 0x3bc9: 0x0004, 0x3bca: 0x0004, 0x3bcb: 0x0004,
+	0x3bcc: 0x0004, 0x3bcd: 0x0004, 0x3bce: 0x0004, 0x3bcf: 0x0004, 0x3bd0: 0x0004, 0x3bd1: 0x0004,
+	0x3bd2: 0x0004, 0x3bd3: 0x0004, 0x3bd4: 0x0004, 0x3bd5: 0x0005, 0x3bd6: 0x0005, 0x3bd7: 0x0004,
+	0x3bd8: 0x0004, 0x3bd9: 0x0004, 0x3bda: 0x0004, 0x3bdb: 0x0004, 0x3bdc: 0x0004, 0x3bdd: 0x0004,
+	0x3bde: 0x0004, 0x3bdf: 0x0004, 0x3be0: 0x0004, 0x3be1: 0x0004, 0x3be2: 0x0004, 0x3be3: 0x0004,
+	0x3be4: 0x0005, 0x3be5: 0x0004, 0x3be6: 0x0004, 0x3be7: 0x0004, 0x3be8: 0x0004, 0x3be9: 0x0004,
+	0x3bea: 0x0004, 0x3beb: 0x0004, 0x3bec: 0x0004, 0x3bed: 0x0004, 0x3bee: 0x0004, 0x3bef: 0x0004,
+	0x3bf0: 0x0004, 0x3bf1: 0x0004, 0x3bf2: 0x0004, 0x3bf3: 0x0004, 0x3bf4: 0x0004, 0x3bf5: 0x0004,
+	0x3bf6: 0x0004, 0x3bf7: 0x0004, 0x3bf8: 0x0004, 0x3bf9: 0x0004, 0x3bfa: 0x0004, 0x3bfb: 0x0004,
+	0x3bfc: 0x0004, 0x3bfd: 0x0004, 0x3bfe: 0x0004, 0x3bff: 0x0004,
+	// Block 0xf0, offset 0x3c00
+	0x3c00: 0x0004, 0x3c01: 0x0004, 0x3c02: 0x0004, 0x3c03: 0x0004, 0x3c04: 0x0004, 0x3c05: 0x0004,
+	0x3c06: 0x0004, 0x3c07: 0x0004, 0x3c08: 0x0004, 0x3c09: 0x0004, 0x3c0a: 0x0004, 0x3c0b: 0x0004,
+	0x3c0c: 0x0004, 0x3c0d: 0x0004, 0x3c0e: 0x0004, 0x3c0f: 0x0004, 0x3c10: 0x0004, 0x3c11: 0x0004,
+	0x3c12: 0x0004, 0x3c13: 0x0004, 0x3c14: 0x0004, 0x3c15: 0x0004, 0x3c16: 0x0004, 0x3c17: 0x0004,
+	0x3c18: 0x0004, 0x3c19: 0x0004, 0x3c1a: 0x0004, 0x3c1b: 0x0004, 0x3c1c: 0x0004, 0x3c1d: 0x0004,
+	0x3c1e: 0x0004, 0x3c1f: 0x0004, 0x3c20: 0x0004, 0x3c21: 0x0004, 0x3c22: 0x0004, 0x3c23: 0x0004,
+	0x3c24: 0x0004, 0x3c25: 0x0004, 0x3c26: 0x0004, 0x3c27: 0x0004, 0x3c28: 0x0004, 0x3c29: 0x0004,
+	0x3c2a: 0x0004, 0x3c2b: 0x0004, 0x3c2c: 0x0004, 0x3c2d: 0x0004, 0x3c2e: 0x0004, 0x3c2f: 0x0004,
+	0x3c30: 0x0004, 0x3c31: 0x0004, 0x3c32: 0x0004, 0x3c33: 0x0004, 0x3c34: 0x0004, 0x3c35: 0x0004,
+	0x3c36: 0x0004, 0x3c37: 0x0004, 0x3c38: 0x0004, 0x3c39: 0x0004, 0x3c3a: 0x0004, 0x3c3b: 0x0005,
+	0x3c3c: 0x0005, 0x3c3d: 0x0005, 0x3c3e: 0x0005, 0x3c3f: 0x0005,
+	// Block 0xf1, offset 0x3c40
+	0x3c40: 0x0005, 0x3c41: 0x0005, 0x3c42: 0x0005, 0x3c43: 0x0005, 0x3c44: 0x0005, 0x3c45: 0x0005,
+	0x3c46: 0x0005, 0x3c47: 0x0005, 0x3c48: 0x0005, 0x3c49: 0x0005, 0x3c4a: 0x0005, 0x3c4b: 0x0005,
+	0x3c4c: 0x0005, 0x3c4d: 0x0005, 0x3c4e: 0x0005, 0x3c4f: 0x0005,
+	// Block 0xf2, offset 0x3c80
+	0x3c80: 0x0005, 0x3c81: 0x0005, 0x3c82: 0x0005, 0x3c83: 0x0005, 0x3c84: 0x0005, 0x3c85: 0x0005,
+	0x3c86: 0x0004, 0x3c87: 0x0004, 0x3c88: 0x0004, 0x3c89: 0x0004, 0x3c8a: 0x0004, 0x3c8b: 0x0004,
+	0x3c8c: 0x0005, 0x3c8d: 0x0004, 0x3c8e: 0x0004, 0x3c8f: 0x0004, 0x3c90: 0x0005, 0x3c91: 0x0005,
+	0x3c92: 0x0005, 0x3c93: 0x0004, 0x3c94: 0x0004, 0x3c95: 0x0005, 0x3c96: 0x0005, 0x3c97: 0x0005,
+	0x3c98: 0x0004, 0x3c99: 0x0004, 0x3c9a: 0x0004, 0x3c9b: 0x0004, 0x3c9c: 0x0005, 0x3c9d: 0x0005,
+	0x3c9e: 0x0005, 0x3c9f: 0x0005, 0x3ca0: 0x0004, 0x3ca1: 0x0004, 0x3ca2: 0x0004, 0x3ca3: 0x0004,
+	0x3ca4: 0x0004, 0x3ca5: 0x0004, 0x3ca6: 0x0004, 0x3ca7: 0x0004, 0x3ca8: 0x0004, 0x3ca9: 0x0004,
+	0x3caa: 0x0004, 0x3cab: 0x0005, 0x3cac: 0x0005, 0x3cad: 0x0004, 0x3cae: 0x0004, 0x3caf: 0x0004,
+	0x3cb0: 0x0004, 0x3cb1: 0x0004, 0x3cb2: 0x0004, 0x3cb3: 0x0004, 0x3cb4: 0x0005, 0x3cb5: 0x0005,
+	0x3cb6: 0x0005, 0x3cb7: 0x0005, 0x3cb8: 0x0005, 0x3cb9: 0x0005, 0x3cba: 0x0005, 0x3cbb: 0x0005,
+	0x3cbc: 0x0005, 0x3cbd: 0x0004, 0x3cbe: 0x0004, 0x3cbf: 0x0004,
+	// Block 0xf3, offset 0x3cc0
+	0x3cf4: 0x0004, 0x3cf5: 0x0004,
+	0x3cf6: 0x0004, 0x3cf7: 0x0004, 0x3cf8: 0x0004, 0x3cf9: 0x0004, 0x3cfa: 0x0004, 0x3cfb: 0x0004,
+	0x3cfc: 0x0004, 0x3cfd: 0x0004, 0x3cfe: 0x0004, 0x3cff: 0x0004,
+	// Block 0xf4, offset 0x3d00
+	0x3d15: 0x0004, 0x3d16: 0x0004, 0x3d17: 0x0004,
+	0x3d18: 0x0004, 0x3d19: 0x0004, 0x3d1a: 0x0004, 0x3d1b: 0x0004, 0x3d1c: 0x0004, 0x3d1d: 0x0004,
+	0x3d1e: 0x0004, 0x3d1f: 0x0004, 0x3d20: 0x0005, 0x3d21: 0x0005, 0x3d22: 0x0005, 0x3d23: 0x0005,
+	0x3d24: 0x0005, 0x3d25: 0x0005, 0x3d26: 0x0005, 0x3d27: 0x0005, 0x3d28: 0x0005, 0x3d29: 0x0005,
+	0x3d2a: 0x0005, 0x3d2b: 0x0005, 0x3d2c: 0x0004, 0x3d2d: 0x0004, 0x3d2e: 0x0004, 0x3d2f: 0x0004,
+	0x3d30: 0x0005, 0x3d31: 0x0004, 0x3d32: 0x0004, 0x3d33: 0x0004, 0x3d34: 0x0004, 0x3d35: 0x0004,
+	0x3d36: 0x0004, 0x3d37: 0x0004, 0x3d38: 0x0004, 0x3d39: 0x0004, 0x3d3a: 0x0004, 0x3d3b: 0x0004,
+	0x3d3c: 0x0004, 0x3d3d: 0x0004, 0x3d3e: 0x0004, 0x3d3f: 0x0004,
+	// Block 0xf5, offset 0x3d40
+	0x3d4c: 0x0004, 0x3d4d: 0x0004, 0x3d4e: 0x0004, 0x3d4f: 0x0004,
+	// Block 0xf6, offset 0x3d80
+	0x3d88: 0x0004, 0x3d89: 0x0004, 0x3d8a: 0x0004, 0x3d8b: 0x0004,
+	0x3d8c: 0x0004, 0x3d8d: 0x0004, 0x3d8e: 0x0004, 0x3d8f: 0x0004,
+	0x3d9a: 0x0004, 0x3d9b: 0x0004, 0x3d9c: 0x0004, 0x3d9d: 0x0004,
+	0x3d9e: 0x0004, 0x3d9f: 0x0004,
+	// Block 0xf7, offset 0x3dc0
+	0x3dc8: 0x0004, 0x3dc9: 0x0004, 0x3dca: 0x0004, 0x3dcb: 0x0004,
+	0x3dcc: 0x0004, 0x3dcd: 0x0004, 0x3dce: 0x0004, 0x3dcf: 0x0004,
+	0x3dee: 0x0004, 0x3def: 0x0004,
+	0x3df0: 0x0004, 0x3df1: 0x0004, 0x3df2: 0x0004, 0x3df3: 0x0004, 0x3df4: 0x0004, 0x3df5: 0x0004,
+	0x3df6: 0x0004, 0x3df7: 0x0004, 0x3df8: 0x0004, 0x3df9: 0x0004, 0x3dfa: 0x0004, 0x3dfb: 0x0004,
+	0x3dfc: 0x0004, 0x3dfd: 0x0004, 0x3dfe: 0x0004, 0x3dff: 0x0004,
+	// Block 0xf8, offset 0x3e00
+	0x3e0c: 0x0005, 0x3e0d: 0x0005, 0x3e0e: 0x0005, 0x3e0f: 0x0005, 0x3e10: 0x0005, 0x3e11: 0x0005,
+	0x3e12: 0x0005, 0x3e13: 0x0005, 0x3e14: 0x0005, 0x3e15: 0x0005, 0x3e16: 0x0005, 0x3e17: 0x0005,
+	0x3e18: 0x0005, 0x3e19: 0x0005, 0x3e1a: 0x0005, 0x3e1b: 0x0005, 0x3e1c: 0x0005, 0x3e1d: 0x0005,
+	0x3e1e: 0x0005, 0x3e1f: 0x0005, 0x3e20: 0x0005, 0x3e21: 0x0005, 0x3e22: 0x0005, 0x3e23: 0x0005,
+	0x3e24: 0x0005, 0x3e25: 0x0005, 0x3e26: 0x0005, 0x3e27: 0x0005, 0x3e28: 0x0005, 0x3e29: 0x0005,
+	0x3e2a: 0x0005, 0x3e2b: 0x0005, 0x3e2c: 0x0005, 0x3e2d: 0x0005, 0x3e2e: 0x0005, 0x3e2f: 0x0005,
+	0x3e30: 0x0005, 0x3e31: 0x0005, 0x3e32: 0x0005, 0x3e33: 0x0005, 0x3e34: 0x0005, 0x3e35: 0x0005,
+	0x3e36: 0x0005, 0x3e37: 0x0005, 0x3e38: 0x0005, 0x3e39: 0x0005, 0x3e3a: 0x0005,
+	0x3e3c: 0x0005, 0x3e3d: 0x0005, 0x3e3e: 0x0005, 0x3e3f: 0x0005,
+	// Block 0xf9, offset 0x3e40
+	0x3e40: 0x0005, 0x3e41: 0x0005, 0x3e42: 0x0005, 0x3e43: 0x0005, 0x3e44: 0x0005, 0x3e45: 0x0005,
+	0x3e47: 0x0005, 0x3e48: 0x0005, 0x3e49: 0x0005, 0x3e4a: 0x0005, 0x3e4b: 0x0005,
+	0x3e4c: 0x0005, 0x3e4d: 0x0005, 0x3e4e: 0x0005, 0x3e4f: 0x0005, 0x3e50: 0x0005, 0x3e51: 0x0005,
+	0x3e52: 0x0005, 0x3e53: 0x0005, 0x3e54: 0x0005, 0x3e55: 0x0005, 0x3e56: 0x0005, 0x3e57: 0x0005,
+	0x3e58: 0x0005, 0x3e59: 0x0005, 0x3e5a: 0x0005, 0x3e5b: 0x0005, 0x3e5c: 0x0005, 0x3e5d: 0x0005,
+	0x3e5e: 0x0005, 0x3e5f: 0x0005, 0x3e60: 0x0005, 0x3e61: 0x0005, 0x3e62: 0x0005, 0x3e63: 0x0005,
+	0x3e64: 0x0005, 0x3e65: 0x0005, 0x3e66: 0x0005, 0x3e67: 0x0005, 0x3e68: 0x0005, 0x3e69: 0x0005,
+	0x3e6a: 0x0005, 0x3e6b: 0x0005, 0x3e6c: 0x0005, 0x3e6d: 0x0005, 0x3e6e: 0x0005, 0x3e6f: 0x0005,
+	0x3e70: 0x0005, 0x3e71: 0x0005, 0x3e72: 0x0005, 0x3e73: 0x0005, 0x3e74: 0x0005, 0x3e75: 0x0005,
+	0x3e76: 0x0005, 0x3e77: 0x0005, 0x3e78: 0x0005, 0x3e79: 0x0005, 0x3e7a: 0x0005, 0x3e7b: 0x0005,
+	0x3e7c: 0x0005, 0x3e7d: 0x0005, 0x3e7e: 0x0005, 0x3e7f: 0x0005,
+	// Block 0xfa, offset 0x3e80
+	0x3e80: 0x0004, 0x3e81: 0x0004, 0x3e82: 0x0004, 0x3e83: 0x0004, 0x3e84: 0x0004, 0x3e85: 0x0004,
+	0x3e86: 0x0004, 0x3e87: 0x0004, 0x3e88: 0x0004, 0x3e89: 0x0004, 0x3e8a: 0x0004, 0x3e8b: 0x0004,
+	0x3e8c: 0x0004, 0x3e8d: 0x0004, 0x3e8e: 0x0004, 0x3e8f: 0x0004, 0x3e90: 0x0004, 0x3e91: 0x0004,
+	0x3e92: 0x0004, 0x3e93: 0x0004, 0x3e94: 0x0004, 0x3e95: 0x0004, 0x3e96: 0x0004, 0x3e97: 0x0004,
+	0x3e98: 0x0004, 0x3e99: 0x0004, 0x3e9a: 0x0004, 0x3e9b: 0x0004, 0x3e9c: 0x0004, 0x3e9d: 0x0004,
+	0x3e9e: 0x0004, 0x3e9f: 0x0004, 0x3ea0: 0x0004, 0x3ea1: 0x0004, 0x3ea2: 0x0004, 0x3ea3: 0x0004,
+	0x3ea4: 0x0004, 0x3ea5: 0x0004, 0x3ea6: 0x0004, 0x3ea7: 0x0004, 0x3ea8: 0x0004, 0x3ea9: 0x0004,
+	0x3eaa: 0x0004, 0x3eab: 0x0004, 0x3eac: 0x0004, 0x3ead: 0x0004, 0x3eae: 0x0004, 0x3eaf: 0x0004,
+	0x3eb0: 0x0005, 0x3eb1: 0x0005, 0x3eb2: 0x0005, 0x3eb3: 0x0005, 0x3eb4: 0x0005, 0x3eb5: 0x0005,
+	0x3eb6: 0x0005, 0x3eb7: 0x0005, 0x3eb8: 0x0005, 0x3eb9: 0x0005, 0x3eba: 0x0005, 0x3ebb: 0x0005,
+	0x3ebc: 0x0005, 0x3ebd: 0x0004, 0x3ebe: 0x0004, 0x3ebf: 0x0004,
+	// Block 0xfb, offset 0x3ec0
+	0x3ec0: 0x0005, 0x3ec1: 0x0005, 0x3ec2: 0x0005, 0x3ec3: 0x0005, 0x3ec4: 0x0005, 0x3ec5: 0x0005,
+	0x3ec6: 0x0005, 0x3ec7: 0x0005, 0x3ec8: 0x0005, 0x3ec9: 0x0004, 0x3eca: 0x0004, 0x3ecb: 0x0004,
+	0x3ecc: 0x0004, 0x3ecd: 0x0004, 0x3ece: 0x0004, 0x3ecf: 0x0004, 0x3ed0: 0x0005, 0x3ed1: 0x0005,
+	0x3ed2: 0x0005, 0x3ed3: 0x0005, 0x3ed4: 0x0005, 0x3ed5: 0x0005, 0x3ed6: 0x0005, 0x3ed7: 0x0005,
+	0x3ed8: 0x0005, 0x3ed9: 0x0005, 0x3eda: 0x0005, 0x3edb: 0x0005, 0x3edc: 0x0005, 0x3edd: 0x0005,
+	0x3ede: 0x0005, 0x3edf: 0x0005, 0x3ee0: 0x0005, 0x3ee1: 0x0005, 0x3ee2: 0x0005, 0x3ee3: 0x0005,
+	0x3ee4: 0x0005, 0x3ee5: 0x0005, 0x3ee6: 0x0005, 0x3ee7: 0x0005, 0x3ee8: 0x0005, 0x3ee9: 0x0005,
+	0x3eea: 0x0005, 0x3eeb: 0x0005, 0x3eec: 0x0005, 0x3eed: 0x0005, 0x3eee: 0x0005, 0x3eef: 0x0005,
+	0x3ef0: 0x0005, 0x3ef1: 0x0005, 0x3ef2: 0x0005, 0x3ef3: 0x0005, 0x3ef4: 0x0005, 0x3ef5: 0x0005,
+	0x3ef6: 0x0005, 0x3ef7: 0x0005, 0x3ef8: 0x0005, 0x3ef9: 0x0005, 0x3efa: 0x0005, 0x3efb: 0x0005,
+	0x3efc: 0x0005, 0x3efd: 0x0005, 0x3efe: 0x0004, 0x3eff: 0x0005,
+	// Block 0xfc, offset 0x3f00
+	0x3f00: 0x0005, 0x3f01: 0x0005, 0x3f02: 0x0005, 0x3f03: 0x0005, 0x3f04: 0x0005, 0x3f05: 0x0005,
+	0x3f06: 0x0004, 0x3f07: 0x0004, 0x3f08: 0x0004, 0x3f09: 0x0004, 0x3f0a: 0x0004, 0x3f0b: 0x0004,
+	0x3f0c: 0x0004, 0x3f0d: 0x0004, 0x3f0e: 0x0005, 0x3f0f: 0x0005, 0x3f10: 0x0005, 0x3f11: 0x0005,
+	0x3f12: 0x0005, 0x3f13: 0x0005, 0x3f14: 0x0005, 0x3f15: 0x0005, 0x3f16: 0x0005, 0x3f17: 0x0005,
+	0x3f18: 0x0005, 0x3f19: 0x0005, 0x3f1a: 0x0005, 0x3f1b: 0x0005, 0x3f1c: 0x0004, 0x3f1d: 0x0004,
+	0x3f1e: 0x0004, 0x3f1f: 0x0004, 0x3f20: 0x0005, 0x3f21: 0x0005, 0x3f22: 0x0005, 0x3f23: 0x0005,
+	0x3f24: 0x0005, 0x3f25: 0x0005, 0x3f26: 0x0005, 0x3f27: 0x0005, 0x3f28: 0x0005, 0x3f29: 0x0004,
+	0x3f2a: 0x0004, 0x3f2b: 0x0004, 0x3f2c: 0x0004, 0x3f2d: 0x0004, 0x3f2e: 0x0004, 0x3f2f: 0x0004,
+	0x3f30: 0x0005, 0x3f31: 0x0005, 0x3f32: 0x0005, 0x3f33: 0x0005, 0x3f34: 0x0005, 0x3f35: 0x0005,
+	0x3f36: 0x0005, 0x3f37: 0x0005, 0x3f38: 0x0005, 0x3f39: 0x0004, 0x3f3a: 0x0004, 0x3f3b: 0x0004,
+	0x3f3c: 0x0004, 0x3f3d: 0x0004, 0x3f3e: 0x0004, 0x3f3f: 0x0004,
+	// Block 0xfd, offset 0x3f40
+	0x3f40: 0x0004, 0x3f41: 0x0004, 0x3f42: 0x0004, 0x3f43: 0x0004, 0x3f44: 0x0004, 0x3f45: 0x0004,
+	0x3f46: 0x0004, 0x3f47: 0x0004, 0x3f48: 0x0004, 0x3f49: 0x0004, 0x3f4a: 0x0004, 0x3f4b: 0x0004,
+	0x3f4c: 0x0004, 0x3f4d: 0x0004, 0x3f4e: 0x0004, 0x3f4f: 0x0004, 0x3f50: 0x0004, 0x3f51: 0x0004,
+	0x3f52: 0x0004, 0x3f53: 0x0004, 0x3f54: 0x0004, 0x3f55: 0x0004, 0x3f56: 0x0004, 0x3f57: 0x0004,
+	0x3f58: 0x0004, 0x3f59: 0x0004, 0x3f5a: 0x0004, 0x3f5b: 0x0004, 0x3f5c: 0x0004, 0x3f5d: 0x0004,
+	0x3f5e: 0x0004, 0x3f5f: 0x0004, 0x3f60: 0x0004, 0x3f61: 0x0004, 0x3f62: 0x0004, 0x3f63: 0x0004,
+	0x3f64: 0x0004, 0x3f65: 0x0004, 0x3f66: 0x0004, 0x3f67: 0x0004, 0x3f68: 0x0004, 0x3f69: 0x0004,
+	0x3f6a: 0x0004, 0x3f6b: 0x0004, 0x3f6c: 0x0004, 0x3f6d: 0x0004, 0x3f6e: 0x0004, 0x3f6f: 0x0004,
+	0x3f70: 0x0004, 0x3f71: 0x0004, 0x3f72: 0x0004, 0x3f73: 0x0004, 0x3f74: 0x0004, 0x3f75: 0x0004,
+	0x3f76: 0x0004, 0x3f77: 0x0004, 0x3f78: 0x0004, 0x3f79: 0x0004, 0x3f7a: 0x0004, 0x3f7b: 0x0004,
+	0x3f7c: 0x0004, 0x3f7d: 0x0004,
+	// Block 0xfe, offset 0x3f80
+	0x3f80: 0x0001, 0x3f81: 0x0001, 0x3f82: 0x0001, 0x3f83: 0x0001, 0x3f84: 0x0001, 0x3f85: 0x0001,
+	0x3f86: 0x0001, 0x3f87: 0x0001, 0x3f88: 0x0001, 0x3f89: 0x0001, 0x3f8a: 0x0001, 0x3f8b: 0x0001,
+	0x3f8c: 0x0001, 0x3f8d: 0x0001, 0x3f8e: 0x0001, 0x3f8f: 0x0001, 0x3f90: 0x0001, 0x3f91: 0x0001,
+	0x3f92: 0x0001, 0x3f93: 0x0001, 0x3f94: 0x0001, 0x3f95: 0x0001, 0x3f96: 0x0001, 0x3f97: 0x0001,
+	0x3f98: 0x0001, 0x3f99: 0x0001, 0x3f9a: 0x0001, 0x3f9b: 0x0001, 0x3f9c: 0x0001, 0x3f9d: 0x0001,
+	0x3f9e: 0x0001, 0x3f9f: 0x0001, 0x3fa0: 0x0001, 0x3fa1: 0x0001, 0x3fa2: 0x0001, 0x3fa3: 0x0001,
+	0x3fa4: 0x0001, 0x3fa5: 0x0001, 0x3fa6: 0x0001, 0x3fa7: 0x0001, 0x3fa8: 0x0001, 0x3fa9: 0x0001,
+	0x3faa: 0x0001, 0x3fab: 0x0001, 0x3fac: 0x0001, 0x3fad: 0x0001, 0x3fae: 0x0001, 0x3faf: 0x0001,
+	0x3fb0: 0x0001, 0x3fb1: 0x0001, 0x3fb2: 0x0001, 0x3fb3: 0x0001, 0x3fb4: 0x0001, 0x3fb5: 0x0001,
+	0x3fb6: 0x0001, 0x3fb7: 0x0001, 0x3fb8: 0x0001, 0x3fb9: 0x0001, 0x3fba: 0x0001, 0x3fbb: 0x0001,
+	0x3fbc: 0x0001, 0x3fbd: 0x0001,
+	// Block 0xff, offset 0x3fc0
+	0x3fc1: 0x0008,
+	0x3fe0: 0x0008, 0x3fe1: 0x0008, 0x3fe2: 0x0008, 0x3fe3: 0x0008,
+	0x3fe4: 0x0008, 0x3fe5: 0x0008, 0x3fe6: 0x0008, 0x3fe7: 0x0008, 0x3fe8: 0x0008, 0x3fe9: 0x0008,
+	0x3fea: 0x0008, 0x3feb: 0x0008, 0x3fec: 0x0008, 0x3fed: 0x0008, 0x3fee: 0x0008, 0x3fef: 0x0008,
+	0x3ff0: 0x0008, 0x3ff1: 0x0008, 0x3ff2: 0x0008, 0x3ff3: 0x0008, 0x3ff4: 0x0008, 0x3ff5: 0x0008,
+	0x3ff6: 0x0008, 0x3ff7: 0x0008, 0x3ff8: 0x0008, 0x3ff9: 0x0008, 0x3ffa: 0x0008, 0x3ffb: 0x0008,
+	0x3ffc: 0x0008, 0x3ffd: 0x0008, 0x3ffe: 0x0008, 0x3fff: 0x0008,
+	// Block 0x100, offset 0x4000
+	0x4000: 0x0002, 0x4001: 0x0002, 0x4002: 0x0002, 0x4003: 0x0002, 0x4004: 0x0002, 0x4005: 0x0002,
+	0x4006: 0x0002, 0x4007: 0x0002, 0x4008: 0x0002, 0x4009: 0x0002, 0x400a: 0x0002, 0x400b: 0x0002,
+	0x400c: 0x0002, 0x400d: 0x0002, 0x400e: 0x0002, 0x400f: 0x0002, 0x4010: 0x0002, 0x4011: 0x0002,
+	0x4012: 0x0002, 0x4013: 0x0002, 0x4014: 0x0002, 0x4015: 0x0002, 0x4016: 0x0002, 0x4017: 0x0002,
+	0x4018: 0x0002, 0x4019: 0x0002, 0x401a: 0x0002, 0x401b: 0x0002, 0x401c: 0x0002, 0x401d: 0x0002,
+	0x401e: 0x0002, 0x401f: 0x0002, 0x4020: 0x0002, 0x4021: 0x0002, 0x4022: 0x0002, 0x4023: 0x0002,
+	0x4024: 0x0002, 0x4025: 0x0002, 0x4026: 0x0002, 0x4027: 0x0002, 0x4028: 0x0002, 0x4029: 0x0002,
+	0x402a: 0x0002, 0x402b: 0x0002, 0x402c: 0x0002, 0x402d: 0x0002, 0x402e: 0x0002, 0x402f: 0x0002,
+	0x4030: 0x0002, 0x4031: 0x0002, 0x4032: 0x0002, 0x4033: 0x0002, 0x4034: 0x0002, 0x4035: 0x0002,
+	0x4036: 0x0002, 0x4037: 0x0002, 0x4038: 0x0002, 0x4039: 0x0002, 0x403a: 0x0002, 0x403b: 0x0002,
+	0x403c: 0x0002, 0x403d: 0x0002,
+}
+
+// stringWidthIndex: 31 blocks, 1984 entries, 1984 bytes
+// Block 0 is the zero block.
+var stringWidthIndex = [1984]uint8{
+	// Block 0x0, offset 0x0
+	// Block 0x1, offset 0x40
+	// Block 0x2, offset 0x80
+	// Block 0x3, offset 0xc0
+	0xc2: 0x01, 0xc3: 0x02, 0xc4: 0x03, 0xc5: 0x04, 0xc7: 0x05,
+	0xc9: 0x06, 0xcb: 0x07, 0xcc: 0x08, 0xcd: 0x09, 0xce: 0x0a, 0xcf: 0x0b,
+	0xd0: 0x0c, 0xd1: 0x0d, 0xd2: 0x0e, 0xd6: 0x0f, 0xd7: 0x10,
+	0xd8: 0x11, 0xd9: 0x12, 0xdb: 0x13, 0xdc: 0x14, 0xdd: 0x15, 0xde: 0x16, 0xdf: 0x17,
+	0xe0: 0x02, 0xe1: 0x03, 0xe2: 0x04, 0xe3: 0x05, 0xe4: 0x06, 0xe5: 0x07, 0xe6: 0x07, 0xe7: 0x07,
+	0xe8: 0x07, 0xe9: 0x07, 0xea: 0x08, 0xeb: 0x07, 0xec: 0x07, 0xed: 0x09, 0xee: 0x0a, 0xef: 0x0b,
+	0xf0: 0x18, 0xf3: 0x1b, 0xf4: 0x1c,
+	// Block 0x4, offset 0x100
+	0x120: 0x18, 0x121: 0x19, 0x122: 0x1a, 0x123: 0x1b, 0x124: 0x1c, 0x125: 0x1d, 0x126: 0x1e, 0x127: 0x1f,
+	0x128: 0x20, 0x129: 0x21, 0x12a: 0x20, 0x12b: 0x22, 0x12c: 0x23, 0x12d: 0x24, 0x12e: 0x25, 0x12f: 0x26,
+	0x130: 0x27, 0x131: 0x28, 0x132: 0x23, 0x133: 0x29, 0x134: 0x2a, 0x135: 0x2b, 0x136: 0x2c, 0x137: 0x2d,
+	0x138: 0x2e, 0x139: 0x2f, 0x13a: 0x30, 0x13b: 0x31, 0x13c: 0x32, 0x13d: 0x33, 0x13e: 0x34, 0x13f: 0x35,
+	// Block 0x5, offset 0x140
+	0x140: 0x36, 0x141: 0x37, 0x142: 0x38, 0x144: 0x39, 0x145: 0x3a,
+	0x14d: 0x3b,
+	0x15c: 0x3c, 0x15d: 0x3d, 0x15e: 0x3e, 0x15f: 0x3f,
+	0x160: 0x40, 0x162: 0x41, 0x164: 0x42,
+	0x168: 0x43, 0x169: 0x44, 0x16a: 0x45, 0x16b: 0x46, 0x16c: 0x47, 0x16d: 0x48, 0x16e: 0x49, 0x16f: 0x4a,
+	0x170: 0x4b, 0x173: 0x4c, 0x177: 0x4d,
+	// Block 0x6, offset 0x180
+	0x180: 0x4e, 0x181: 0x4f, 0x182: 0x50, 0x183: 0x51, 0x184: 0x52, 0x185: 0x53, 0x186: 0x54, 0x187: 0x55,
+	0x188: 0x56, 0x189: 0x57, 0x18a: 0x58, 0x18c: 0x59, 0x18e: 0x5a, 0x18f: 0x5b,
+	0x191: 0x5c, 0x192: 0x5d, 0x193: 0x5e, 0x194: 0x5d, 0x195: 0x5f, 0x196: 0x60, 0x197: 0x61,
+	0x198: 0x62, 0x199: 0x63, 0x19a: 0x64, 0x19b: 0x65, 0x19c: 0x66, 0x19d: 0x67, 0x19e: 0x68,
+	0x1a4: 0x69,
+	0x1ac: 0x6a, 0x1ad: 0x6b,
+	0x1b3: 0x6c, 0x1b5: 0x6d, 0x1b7: 0x6e,
+	0x1ba: 0x6f, 0x1bb: 0x70, 0x1bc: 0x39, 0x1bd: 0x39, 0x1be: 0x39, 0x1bf: 0x71,
+	// Block 0x7, offset 0x1c0
+	0x1c0: 0x72, 0x1c1: 0x73, 0x1c2: 0x74, 0x1c3: 0x39, 0x1c4: 0x75, 0x1c5: 0x39, 0x1c6: 0x76, 0x1c7: 0x77,
+	0x1c8: 0x78, 0x1c9: 0x79, 0x1ca: 0x7a, 0x1cb: 0x39, 0x1cc: 0x39, 0x1cd: 0x39, 0x1ce: 0x39, 0x1cf: 0x39,
+	0x1d0: 0x39, 0x1d1: 0x39, 0x1d2: 0x39, 0x1d3: 0x39, 0x1d4: 0x39, 0x1d5: 0x39, 0x1d6: 0x39, 0x1d7: 0x39,
+	0x1d8: 0x39, 0x1d9: 0x39, 0x1da: 0x39, 0x1db: 0x39, 0x1dc: 0x39, 0x1dd: 0x39, 0x1de: 0x39, 0x1df: 0x39,
+	0x1e0: 0x39, 0x1e1: 0x39, 0x1e2: 0x39, 0x1e3: 0x39, 0x1e4: 0x39, 0x1e5: 0x39, 0x1e6: 0x39, 0x1e7: 0x39,
+	0x1e8: 0x39, 0x1e9: 0x39, 0x1ea: 0x39, 0x1eb: 0x39, 0x1ec: 0x39, 0x1ed: 0x39, 0x1ee: 0x39, 0x1ef: 0x39,
+	0x1f0: 0x39, 0x1f1: 0x39, 0x1f2: 0x39, 0x1f3: 0x39, 0x1f4: 0x39, 0x1f5: 0x39, 0x1f6: 0x39, 0x1f7: 0x39,
+	0x1f8: 0x39, 0x1f9: 0x39, 0x1fa: 0x39, 0x1fb: 0x39, 0x1fc: 0x39, 0x1fd: 0x39, 0x1fe: 0x39, 0x1ff: 0x39,
+	// Block 0x8, offset 0x200
+	0x200: 0x39, 0x201: 0x39, 0x202: 0x39, 0x203: 0x39, 0x204: 0x39, 0x205: 0x39, 0x206: 0x39, 0x207: 0x39,
+	0x208: 0x39, 0x209: 0x39, 0x20a: 0x39, 0x20b: 0x39, 0x20c: 0x39, 0x20d: 0x39, 0x20e: 0x39, 0x20f: 0x39,
+	0x210: 0x39, 0x211: 0x39, 0x212: 0x39, 0x213: 0x39, 0x214: 0x39, 0x215: 0x39, 0x216: 0x39, 0x217: 0x39,
+	0x218: 0x39, 0x219: 0x39, 0x21a: 0x39, 0x21b: 0x39, 0x21c: 0x39, 0x21d: 0x39, 0x21e: 0x39, 0x21f: 0x39,
+	0x220: 0x39, 0x221: 0x39, 0x222: 0x39, 0x223: 0x39, 0x224: 0x39, 0x225: 0x39, 0x226: 0x39, 0x227: 0x39,
+	0x228: 0x39, 0x229: 0x39, 0x22a: 0x39, 0x22b: 0x39, 0x22c: 0x39, 0x22d: 0x39, 0x22e: 0x39, 0x22f: 0x39,
+	0x230: 0x39, 0x231: 0x39, 0x232: 0x39, 0x233: 0x39, 0x234: 0x39, 0x235: 0x39, 0x236: 0x39,
+	0x238: 0x39, 0x239: 0x39, 0x23a: 0x39, 0x23b: 0x39, 0x23c: 0x39, 0x23d: 0x39, 0x23e: 0x39, 0x23f: 0x39,
+	// Block 0x9, offset 0x240
+	0x240: 0x39, 0x241: 0x39, 0x242: 0x39, 0x243: 0x39, 0x244: 0x39, 0x245: 0x39, 0x246: 0x39, 0x247: 0x39,
+	0x248: 0x39, 0x249: 0x39, 0x24a: 0x39, 0x24b: 0x39, 0x24c: 0x39, 0x24d: 0x39, 0x24e: 0x39, 0x24f: 0x39,
+	0x250: 0x39, 0x251: 0x39, 0x252: 0x39, 0x253: 0x39, 0x254: 0x39, 0x255: 0x39, 0x256: 0x39, 0x257: 0x39,
+	0x258: 0x39, 0x259: 0x39, 0x25a: 0x39, 0x25b: 0x39, 0x25c: 0x39, 0x25d: 0x39, 0x25e: 0x39, 0x25f: 0x39,
+	0x260: 0x39, 0x261: 0x39, 0x262: 0x39, 0x263: 0x39, 0x264: 0x39, 0x265: 0x39, 0x266: 0x39, 0x267: 0x39,
+	0x268: 0x39, 0x269: 0x39, 0x26a: 0x39, 0x26b: 0x39, 0x26c: 0x39, 0x26d: 0x39, 0x26e: 0x39, 0x26f: 0x39,
+	0x270: 0x39, 0x271: 0x39, 0x272: 0x39, 0x273: 0x39, 0x274: 0x39, 0x275: 0x39, 0x276: 0x39, 0x277: 0x39,
+	0x278: 0x39, 0x279: 0x39, 0x27a: 0x39, 0x27b: 0x39, 0x27c: 0x39, 0x27d: 0x39, 0x27e: 0x39, 0x27f: 0x39,
+	// Block 0xa, offset 0x280
+	0x280: 0x39, 0x281: 0x39, 0x282: 0x39, 0x283: 0x39, 0x284: 0x39, 0x285: 0x39, 0x286: 0x39, 0x287: 0x39,
+	0x288: 0x39, 0x289: 0x39, 0x28a: 0x39, 0x28b: 0x39, 0x28c: 0x39, 0x28d: 0x39, 0x28e: 0x39, 0x28f: 0x39,
+	0x290: 0x39, 0x291: 0x39, 0x292: 0x7b, 0x293: 0x7c,
+	0x299: 0x7d, 0x29a: 0x7e, 0x29b: 0x7f,
+	0x2a0: 0x80, 0x2a3: 0x81, 0x2a4: 0x82, 0x2a5: 0x83, 0x2a6: 0x84, 0x2a7: 0x85,
+	0x2a8: 0x86, 0x2a9: 0x87, 0x2aa: 0x88, 0x2ab: 0x89, 0x2af: 0x8a,
+	0x2b0: 0x39, 0x2b1: 0x39, 0x2b2: 0x39, 0x2b3: 0x39, 0x2b4: 0x39, 0x2b5: 0x39, 0x2b6: 0x39, 0x2b7: 0x39,
+	0x2b8: 0x39, 0x2b9: 0x39, 0x2ba: 0x39, 0x2bb: 0x39, 0x2bc: 0x39, 0x2bd: 0x39, 0x2be: 0x39, 0x2bf: 0x39,
+	// Block 0xb, offset 0x2c0
+	0x2c0: 0x39, 0x2c1: 0x39, 0x2c2: 0x39, 0x2c3: 0x39, 0x2c4: 0x39, 0x2c5: 0x39, 0x2c6: 0x39, 0x2c7: 0x39,
+	0x2c8: 0x39, 0x2c9: 0x39, 0x2ca: 0x39, 0x2cb: 0x39, 0x2cc: 0x39, 0x2cd: 0x39, 0x2ce: 0x39, 0x2cf: 0x39,
+	0x2d0: 0x39, 0x2d1: 0x39, 0x2d2: 0x39, 0x2d3: 0x39, 0x2d4: 0x39, 0x2d5: 0x39, 0x2d6: 0x39, 0x2d7: 0x39,
+	0x2d8: 0x39, 0x2d9: 0x39, 0x2da: 0x39, 0x2db: 0x39, 0x2dc: 0x39, 0x2dd: 0x39, 0x2de: 0x8b,
+	// Block 0xc, offset 0x300
+	0x300: 0x5d, 0x301: 0x5d, 0x302: 0x5d, 0x303: 0x5d, 0x304: 0x5d, 0x305: 0x5d, 0x306: 0x5d, 0x307: 0x5d,
+	0x308: 0x5d, 0x309: 0x5d, 0x30a: 0x5d, 0x30b: 0x5d, 0x30c: 0x5d, 0x30d: 0x5d, 0x30e: 0x5d, 0x30f: 0x5d,
+	0x310: 0x5d, 0x311: 0x5d, 0x312: 0x5d, 0x313: 0x5d, 0x314: 0x5d, 0x315: 0x5d, 0x316: 0x5d, 0x317: 0x5d,
+	0x318: 0x5d, 0x319: 0x5d, 0x31a: 0x5d, 0x31b: 0x5d, 0x31c: 0x5d, 0x31d: 0x5d, 0x31e: 0x5d, 0x31f: 0x5d,
+	0x320: 0x5d, 0x321: 0x5d, 0x322: 0x5d, 0x323: 0x5d, 0x324: 0x5d, 0x325: 0x5d, 0x326: 0x5d, 0x327: 0x5d,
+	0x328: 0x5d, 0x329: 0x5d, 0x32a: 0x5d, 0x32b: 0x5d, 0x32c: 0x5d, 0x32d: 0x5d, 0x32e: 0x5d, 0x32f: 0x5d,
+	0x330: 0x5d, 0x331: 0x5d, 0x332: 0x5d, 0x333: 0x5d, 0x334: 0x5d, 0x335: 0x5d, 0x336: 0x5d, 0x337: 0x5d,
+	0x338: 0x5d, 0x339: 0x5d, 0x33a: 0x5d, 0x33b: 0x5d, 0x33c: 0x5d, 0x33d: 0x5d, 0x33e: 0x5d, 0x33f: 0x5d,
+	// Block 0xd, offset 0x340
+	0x340: 0x5d, 0x341: 0x5d, 0x342: 0x5d, 0x343: 0x5d, 0x344: 0x5d, 0x345: 0x5d, 0x346: 0x5d, 0x347: 0x5d,
+	0x348: 0x5d, 0x349: 0x5d, 0x34a: 0x5d, 0x34b: 0x5d, 0x34c: 0x5d, 0x34d: 0x5d, 0x34e: 0x5d, 0x34f: 0x5d,
+	0x350: 0x5d, 0x351: 0x5d, 0x352: 0x5d, 0x353: 0x5d, 0x354: 0x5d, 0x355: 0x5d, 0x356: 0x5d, 0x357: 0x5d,
+	0x358: 0x5d, 0x359: 0x5d, 0x35a: 0x5d, 0x35b: 0x5d, 0x35c: 0x5d, 0x35d: 0x5d, 0x35e: 0x5d, 0x35f: 0x5d,
+	0x360: 0x5d, 0x361: 0x5d, 0x362: 0x5d, 0x363: 0x5d, 0x364: 0x39, 0x365: 0x39, 0x366: 0x39, 0x367: 0x39,
+	0x368: 0x39, 0x369: 0x39, 0x36a: 0x39, 0x36b: 0x39, 0x36c: 0x8c,
+	0x378: 0x8d, 0x379: 0x8e, 0x37b: 0x6d, 0x37c: 0x73, 0x37d: 0x8f, 0x37f: 0x90,
+	// Block 0xe, offset 0x380
+	0x387: 0x91,
+	0x38b: 0x92, 0x38d: 0x93,
+	0x3a8: 0x94, 0x3ab: 0x95,
+	0x3b4: 0x96,
+	0x3ba: 0x97, 0x3bb: 0x98, 0x3bd: 0x99, 0x3be: 0x9a,
+	// Block 0xf, offset 0x3c0
+	0x3c0: 0x9b, 0x3c1: 0x9c, 0x3c2: 0x9d, 0x3c3: 0x9e, 0x3c4: 0x9f, 0x3c5: 0xa0, 0x3c6: 0xa1, 0x3c7: 0xa2,
+	0x3c8: 0xa3, 0x3c9: 0x2c, 0x3cb: 0xa4, 0x3cc: 0x2a, 0x3cd: 0xa5,
+	0x3d0: 0xa6, 0x3d1: 0xa7, 0x3d2: 0xa8, 0x3d3: 0xa9, 0x3d6: 0xaa, 0x3d7: 0xab,
+	0x3d8: 0xac, 0x3d9: 0xad, 0x3da: 0xae, 0x3dc: 0xaf,
+	0x3e0: 0xb0, 0x3e4: 0xb1, 0x3e5: 0xb2, 0x3e7: 0xb3,
+	0x3e8: 0xb4, 0x3e9: 0xb5, 0x3ea: 0xb6,
+	0x3f0: 0xb7, 0x3f2: 0xb8, 0x3f4: 0xb9, 0x3f5: 0xba, 0x3f6: 0xbb,
+	0x3fb: 0xbc, 0x3fc: 0xbd, 0x3fd: 0xbe,
+	// Block 0x10, offset 0x400
+	0x410: 0x45, 0x411: 0xbf,
+	// Block 0x11, offset 0x440
+	0x46b: 0xc0, 0x46c: 0xc1,
+	0x47d: 0xc2, 0x47e: 0xc3, 0x47f: 0xc4,
+	// Block 0x12, offset 0x480
+	0x480: 0x39, 0x481: 0x39, 0x482: 0x39, 0x483: 0x39, 0x484: 0x39, 0x485: 0x39, 0x486: 0x39, 0x487: 0x39,
+	0x488: 0x39, 0x489: 0x39, 0x48a: 0x39, 0x48b: 0x39, 0x48c: 0x39, 0x48d: 0x39, 0x48e: 0x39, 0x48f: 0x39,
+	0x490: 0x39, 0x491: 0x39, 0x492: 0x39, 0x493: 0x39, 0x494: 0x39, 0x495: 0x39, 0x496: 0x39, 0x497: 0x39,
+	0x498: 0x39, 0x499: 0x39, 0x49a: 0x39, 0x49b: 0x39, 0x49c: 0x39, 0x49d: 0x39, 0x49e: 0x39, 0x49f: 0xc5,
+	0x4a0: 0x39, 0x4a1: 0x39, 0x4a2: 0x39, 0x4a3: 0x39, 0x4a4: 0x39, 0x4a5: 0x39, 0x4a6: 0x39, 0x4a7: 0x39,
+	0x4a8: 0x39, 0x4a9: 0x39, 0x4aa: 0x39, 0x4ab: 0x39, 0x4ac: 0x39, 0x4ad: 0x39, 0x4ae: 0x39, 0x4af: 0x39,
+	0x4b0: 0x39, 0x4b1: 0x39, 0x4b2: 0x39, 0x4b3: 0xc6, 0x4b4: 0xc7,
+	// Block 0x13, offset 0x4c0
+	0x4ff: 0xc8,
+	// Block 0x14, offset 0x500
+	0x500: 0x39, 0x501: 0x39, 0x502: 0x39, 0x503: 0x39, 0x504: 0xc9, 0x505: 0xca, 0x506: 0x39, 0x507: 0x39,
+	0x508: 0x39, 0x509: 0x39, 0x50a: 0x39, 0x50b: 0xcb,
+	0x532: 0xcc,
+	// Block 0x15, offset 0x540
+	0x57c: 0xcd, 0x57d: 0xce,
+	// Block 0x16, offset 0x580
+	0x585: 0xcf, 0x586: 0xd0,
+	0x589: 0xd1,
+	0x5a8: 0xd2, 0x5a9: 0xd3, 0x5aa: 0xd4,
+	// Block 0x17, offset 0x5c0
+	0x5c0: 0xd5, 0x5c2: 0xc2, 0x5c4: 0xc1,
+	0x5ca: 0xd6, 0x5cb: 0xd7,
+	0x5d3: 0xd7,
+	0x5e3: 0xd8, 0x5e5: 0xd9,
+	// Block 0x18, offset 0x600
+	0x600: 0xda, 0x601: 0xdb, 0x602: 0xdb, 0x603: 0xdc, 0x604: 0xdd, 0x605: 0xde, 0x606: 0xdf, 0x607: 0xe0,
+	0x608: 0xe1, 0x609: 0xe2, 0x60a: 0xdb, 0x60b: 0xdb, 0x60c: 0xe3, 0x60d: 0xe4, 0x60e: 0xe5, 0x60f: 0xe6,
+	0x610: 0xe7, 0x611: 0xe8, 0x612: 0xe9, 0x613: 0xea, 0x614: 0xeb, 0x615: 0xec, 0x616: 0xed, 0x617: 0xee,
+	0x618: 0xe9, 0x619: 0xef, 0x61a: 0xe9, 0x61b: 0xf0, 0x61d: 0xf1, 0x61f: 0xf2,
+	0x620: 0xf3, 0x621: 0xf4, 0x622: 0xf5, 0x623: 0xdb, 0x624: 0xf6, 0x625: 0xf7, 0x626: 0xe9, 0x627: 0xe9,
+	0x628: 0xdb, 0x629: 0xf8, 0x62a: 0xf9, 0x62b: 0xfa,
+	0x630: 0xdb, 0x631: 0xdb, 0x632: 0xdb, 0x633: 0xdb, 0x634: 0xdb, 0x635: 0xdb, 0x636: 0xdb, 0x637: 0xdb,
+	0x638: 0xdb, 0x639: 0xdb, 0x63a: 0xdb, 0x63b: 0xdb, 0x63c: 0xdb, 0x63d: 0xdb, 0x63e: 0xdb, 0x63f: 0xfb,
+	// Block 0x19, offset 0x640
+	0x640: 0x39, 0x641: 0x39, 0x642: 0x39, 0x643: 0x39, 0x644: 0x39, 0x645: 0x39, 0x646: 0x39, 0x647: 0x39,
+	0x648: 0x39, 0x649: 0x39, 0x64a: 0x39, 0x64b: 0x39, 0x64c: 0x39, 0x64d: 0x39, 0x64e: 0x39, 0x64f: 0x39,
+	0x650: 0x39, 0x651: 0x39, 0x652: 0x39, 0x653: 0x39, 0x654: 0x39, 0x655: 0x39, 0x656: 0x39, 0x657: 0x39,
+	0x658: 0x39, 0x659: 0x39, 0x65a: 0x39, 0x65b: 0x39, 0x65c: 0x39, 0x65d: 0x39, 0x65e: 0x39, 0x65f: 0x39,
+	0x660: 0x39, 0x661: 0x39, 0x662: 0x39, 0x663: 0x39, 0x664: 0x39, 0x665: 0x39, 0x666: 0x39, 0x667: 0x39,
+	0x668: 0x39, 0x669: 0x39, 0x66a: 0x39, 0x66b: 0x39, 0x66c: 0x39, 0x66d: 0x39, 0x66e: 0x39, 0x66f: 0x39,
+	0x670: 0x39, 0x671: 0x39, 0x672: 0x39, 0x673: 0x39, 0x674: 0x39, 0x675: 0x39, 0x676: 0x39, 0x677: 0x39,
+	0x678: 0x39, 0x679: 0x39, 0x67a: 0x39, 0x67b: 0x39, 0x67c: 0x39, 0x67d: 0x39, 0x67e: 0x39, 0x67f: 0xfc,
+	// Block 0x1a, offset 0x680
+	0x690: 0x0c, 0x691: 0x0d, 0x693: 0x0e, 0x696: 0x0f, 0x697: 0x07,
+	0x698: 0x10, 0x69a: 0x11, 0x69b: 0x12, 0x69c: 0x13, 0x69d: 0x14, 0x69e: 0x15, 0x69f: 0x16,
+	0x6a0: 0x07, 0x6a1: 0x07, 0x6a2: 0x07, 0x6a3: 0x07, 0x6a4: 0x07, 0x6a5: 0x07, 0x6a6: 0x07, 0x6a7: 0x07,
+	0x6a8: 0x07, 0x6a9: 0x07, 0x6aa: 0x07, 0x6ab: 0x07, 0x6ac: 0x07, 0x6ad: 0x07, 0x6ae: 0x07, 0x6af: 0x17,
+	0x6b0: 0x07, 0x6b1: 0x07, 0x6b2: 0x07, 0x6b3: 0x07, 0x6b4: 0x07, 0x6b5: 0x07, 0x6b6: 0x07, 0x6b7: 0x07,
+	0x6b8: 0x07, 0x6b9: 0x07, 0x6ba: 0x07, 0x6bb: 0x07, 0x6bc: 0x07, 0x6bd: 0x07, 0x6be: 0x07, 0x6bf: 0x17,
+	// Block 0x1b, offset 0x6c0
+	0x6c0: 0xfd, 0x6c1: 0x4d, 0x6c4: 0x08, 0x6c5: 0x08, 0x6c6: 0x08, 0x6c7: 0x09,
+	// Block 0x1c, offset 0x700
+	0x700: 0x5d, 0x701: 0x5d, 0x702: 0x5d, 0x703: 0x5d, 0x704: 0x5d, 0x705: 0x5d, 0x706: 0x5d, 0x707: 0x5d,
+	0x708: 0x5d, 0x709: 0x5d, 0x70a: 0x5d, 0x70b: 0x5d, 0x70c: 0x5d, 0x70d: 0x5d, 0x70e: 0x5d, 0x70f: 0x5d,
+	0x710: 0x5d, 0x711: 0x5d, 0x712: 0x5d, 0x713: 0x5d, 0x714: 0x5d, 0x715: 0x5d, 0x716: 0x5d, 0x717: 0x5d,
+	0x718: 0x5d, 0x719: 0x5d, 0x71a: 0x5d, 0x71b: 0x5d, 0x71c: 0x5d, 0x71d: 0x5d, 0x71e: 0x5d, 0x71f: 0x5d,
+	0x720: 0x5d, 0x721: 0x5d, 0x722: 0x5d, 0x723: 0x5d, 0x724: 0x5d, 0x725: 0x5d, 0x726: 0x5d, 0x727: 0x5d,
+	0x728: 0x5d, 0x729: 0x5d, 0x72a: 0x5d, 0x72b: 0x5d, 0x72c: 0x5d, 0x72d: 0x5d, 0x72e: 0x5d, 0x72f: 0x5d,
+	0x730: 0x5d, 0x731: 0x5d, 0x732: 0x5d, 0x733: 0x5d, 0x734: 0x5d, 0x735: 0x5d, 0x736: 0x5d, 0x737: 0x5d,
+	0x738: 0x5d, 0x739: 0x5d, 0x73a: 0x5d, 0x73b: 0x5d, 0x73c: 0x5d, 0x73d: 0x5d, 0x73e: 0x5d, 0x73f: 0xfe,
+	// Block 0x1d, offset 0x740
+	0x760: 0x19,
+	0x770: 0x0a, 0x771: 0x0a, 0x772: 0x0a, 0x773: 0x0a, 0x774: 0x0a, 0x775: 0x0a, 0x776: 0x0a, 0x777: 0x0a,
+	0x778: 0x0a, 0x779: 0x0a, 0x77a: 0x0a, 0x77b: 0x0a, 0x77c: 0x0a, 0x77d: 0x0a, 0x77e: 0x0a, 0x77f: 0x1a,
+	// Block 0x1e, offset 0x780
+	0x780: 0x0a, 0x781: 0x0a, 0x782: 0x0a, 0x783: 0x0a, 0x784: 0x0a, 0x785: 0x0a, 0x786: 0x0a, 0x787: 0x0a,
+	0x788: 0x0a, 0x789: 0x0a, 0x78a: 0x0a, 0x78b: 0x0a, 0x78c: 0x0a, 0x78d: 0x0a, 0x78e: 0x0a, 0x78f: 0x1a,
+}
diff --git a/vendor/github.com/clipperhouse/displaywidth/width.go b/vendor/github.com/clipperhouse/displaywidth/width.go
new file mode 100644
index 000000000..0c7edb2fd
--- /dev/null
+++ b/vendor/github.com/clipperhouse/displaywidth/width.go
@@ -0,0 +1,224 @@
+package displaywidth
+
+import (
+	"unicode/utf8"
+
+	"github.com/clipperhouse/stringish"
+	"github.com/clipperhouse/uax29/v2/graphemes"
+)
+
+// String calculates the display width of a string
+// using the [DefaultOptions]
+func String(s string) int {
+	return DefaultOptions.String(s)
+}
+
+// Bytes calculates the display width of a []byte
+// using the [DefaultOptions]
+func Bytes(s []byte) int {
+	return DefaultOptions.Bytes(s)
+}
+
+func Rune(r rune) int {
+	return DefaultOptions.Rune(r)
+}
+
+type Options struct {
+	EastAsianWidth     bool
+	StrictEmojiNeutral bool
+}
+
+var DefaultOptions = Options{
+	EastAsianWidth:     false,
+	StrictEmojiNeutral: true,
+}
+
+// String calculates the display width of a string
+// for the given options
+func (options Options) String(s string) int {
+	if len(s) == 0 {
+		return 0
+	}
+
+	total := 0
+	g := graphemes.FromString(s)
+	for g.Next() {
+		// The first character in the grapheme cluster determines the width;
+		// we use lookupProperties which can consider immediate VS15/VS16.
+		props := lookupProperties(g.Value())
+		total += props.width(options)
+	}
+	return total
+}
+
+// BytesOptions calculates the display width of a []byte
+// for the given options
+func (options Options) Bytes(s []byte) int {
+	if len(s) == 0 {
+		return 0
+	}
+
+	total := 0
+	g := graphemes.FromBytes(s)
+	for g.Next() {
+		// The first character in the grapheme cluster determines the width;
+		// we use lookupProperties which can consider immediate VS15/VS16.
+		props := lookupProperties(g.Value())
+		total += props.width(options)
+	}
+	return total
+}
+
+func (options Options) Rune(r rune) int {
+	// Fast path for ASCII
+	if r < utf8.RuneSelf {
+		if isASCIIControl(byte(r)) {
+			// Control (0x00-0x1F) and DEL (0x7F)
+			return 0
+		}
+		// ASCII printable (0x20-0x7E)
+		return 1
+	}
+
+	// Surrogates (U+D800-U+DFFF) are invalid UTF-8 and have zero width
+	// Other packages might turn them into the replacement character (U+FFFD)
+	// in which case, we won't see it.
+	if r >= 0xD800 && r <= 0xDFFF {
+		return 0
+	}
+
+	// Stack-allocated to avoid heap allocation
+	var buf [4]byte // UTF-8 is at most 4 bytes
+	n := utf8.EncodeRune(buf[:], r)
+	// Skip the grapheme iterator and directly lookup properties
+	props := lookupProperties(buf[:n])
+	return props.width(options)
+}
+
+func isASCIIControl(b byte) bool {
+	return b < 0x20 || b == 0x7F
+}
+
+const defaultWidth = 1
+
+// is returns true if the property flag is set
+func (p property) is(flag property) bool {
+	return p&flag != 0
+}
+
+// lookupProperties returns the properties for the first character in a string
+func lookupProperties[T stringish.Interface](s T) property {
+	if len(s) == 0 {
+		return 0
+	}
+
+	b := s[0]
+	if isASCIIControl(b) {
+		return _ZeroWidth
+	}
+
+	l := len(s)
+
+	if b < utf8.RuneSelf { // Single-byte ASCII
+		// Check for variation selector after ASCII (e.g., keycap sequences like 1️⃣)
+		var p property
+		if l >= 4 {
+			// Create a subslice to help the compiler eliminate bounds checks
+			vs := s[1:4]
+			if vs[0] == 0xEF && vs[1] == 0xB8 {
+				switch vs[2] {
+				case 0x8E:
+					p |= _VS15
+				case 0x8F:
+					p |= _VS16
+				}
+			}
+		}
+		return p // ASCII characters are width 1 by default, or 2 with VS16
+	}
+
+	// Regional indicator pair (flag) - detect early before trie lookup.
+	// Formed by two Regional Indicator symbols (U+1F1E6–U+1F1FF),
+	// each encoded as F0 9F 87 A6–BF. Always width 2, no trie lookup needed.
+	if l >= 8 {
+		// Create a subslice to help the compiler eliminate bounds checks
+		ri := s[:8]
+		if ri[0] == 0xF0 &&
+			ri[1] == 0x9F &&
+			ri[2] == 0x87 {
+			b3 := ri[3]
+			if b3 >= 0xA6 && b3 <= 0xBF &&
+				ri[4] == 0xF0 &&
+				ri[5] == 0x9F &&
+				ri[6] == 0x87 {
+				b7 := ri[7]
+				if b7 >= 0xA6 && b7 <= 0xBF {
+					return _RI_PAIR
+				}
+			}
+		}
+	}
+
+	props, size := lookup(s)
+	p := property(props)
+
+	// Variation Selectors
+	if size > 0 && l >= size+3 {
+		// Create a subslice to help the compiler eliminate bounds checks
+		vs := s[size : size+3]
+		if vs[0] == 0xEF && vs[1] == 0xB8 {
+			switch vs[2] {
+			case 0x8E:
+				p |= _VS15
+			case 0x8F:
+				p |= _VS16
+			}
+		}
+	}
+
+	return p
+}
+
+// width determines the display width of a character based on its properties
+// and configuration options
+func (p property) width(options Options) int {
+	if p == 0 {
+		// Character not in trie, use default behavior
+		return defaultWidth
+	}
+
+	if p.is(_ZeroWidth) {
+		return 0
+	}
+
+	// Explicit presentation overrides from VS come first.
+	if p.is(_VS16) {
+		return 2
+	}
+	if p.is(_VS15) {
+		return 1
+	}
+
+	// Regional indicator pair (flag) grapheme cluster
+	// returns 1 under StrictEmojiNeutral=false, which
+	// is compatible with go-runewidth & uniseg.
+	if p.is(_RI_PAIR) && options.StrictEmojiNeutral {
+		return 2
+	}
+
+	if options.EastAsianWidth {
+		if p.is(_East_Asian_Ambiguous) {
+			return 2
+		}
+		if p.is(_East_Asian_Ambiguous|_Emoji) && !options.StrictEmojiNeutral {
+			return 2
+		}
+	}
+
+	if p.is(_East_Asian_Full_Wide) {
+		return 2
+	}
+
+	// Default width for all other characters
+	return defaultWidth
+}
diff --git a/vendor/github.com/clipperhouse/stringish/.gitignore b/vendor/github.com/clipperhouse/stringish/.gitignore
new file mode 100644
index 000000000..12fbfb739
--- /dev/null
+++ b/vendor/github.com/clipperhouse/stringish/.gitignore
@@ -0,0 +1,2 @@
+.DS_Store
+*.test
diff --git a/vendor/github.com/clipperhouse/stringish/LICENSE b/vendor/github.com/clipperhouse/stringish/LICENSE
new file mode 100644
index 000000000..4b8064eb3
--- /dev/null
+++ b/vendor/github.com/clipperhouse/stringish/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Matt Sherman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/clipperhouse/stringish/README.md b/vendor/github.com/clipperhouse/stringish/README.md
new file mode 100644
index 000000000..fa1f7cc67
--- /dev/null
+++ b/vendor/github.com/clipperhouse/stringish/README.md
@@ -0,0 +1,64 @@
+# stringish
+
+A small Go module that provides a generic type constraint for “string-like”
+data, and a utf8 package that works with both strings and byte slices
+without conversions.
+
+```go
+type Interface interface {
+	~[]byte | ~string
+}
+```
+
+[![Go Reference](https://pkg.go.dev/badge/github.com/clipperhouse/stringish/utf8.svg)](https://pkg.go.dev/github.com/clipperhouse/stringish/utf8)
+[![Test Status](https://github.com/clipperhouse/stringish/actions/workflows/gotest.yml/badge.svg)](https://github.com/clipperhouse/stringish/actions/workflows/gotest.yml)
+
+## Install
+
+```
+go get github.com/clipperhouse/stringish
+```
+
+## Examples
+
+```go
+import (
+    "github.com/clipperhouse/stringish"
+    "github.com/clipperhouse/stringish/utf8"
+)
+
+s := "Hello, 世界"
+r, size := utf8.DecodeRune(s)   // not DecodeRuneInString 🎉
+
+b := []byte("Hello, 世界")
+r, size = utf8.DecodeRune(b)    // same API!
+
+func MyFoo[T stringish.Interface](s T) T {
+    // pass a string or a []byte
+    // iterate, slice, transform, whatever
+}
+```
+
+## Motivation
+
+Sometimes we want APIs to accept `string` or `[]byte` without having to convert
+between those types. That conversion usually allocates!
+
+By implementing with `stringish.Interface`, we can have a single API, and
+single implementation for both types: one `Foo` instead of `Foo` and
+`FooString`.
+
+We have converted the
+[`unicode/utf8` package](https://github.com/clipperhouse/stringish/blob/main/utf8/utf8.go)
+as an example -- note the absence of`*InString` funcs. We might look at `x/text`
+next.
+
+## Used by
+
+- clipperhouse/uax29: [stringish trie](https://github.com/clipperhouse/uax29/blob/master/graphemes/trie.go#L27), [stringish iterator](https://github.com/clipperhouse/uax29/blob/master/internal/iterators/iterator.go#L9), [stringish SplitFunc](https://github.com/clipperhouse/uax29/blob/master/graphemes/splitfunc.go#L21)
+
+- [clipperhouse/displaywidth](https://github.com/clipperhouse/displaywidth)
+
+## Prior discussion
+
+- [Consideration of similar by the Go team](https://github.com/golang/go/issues/48643)
diff --git a/vendor/github.com/clipperhouse/stringish/interface.go b/vendor/github.com/clipperhouse/stringish/interface.go
new file mode 100644
index 000000000..adfeab61e
--- /dev/null
+++ b/vendor/github.com/clipperhouse/stringish/interface.go
@@ -0,0 +1,5 @@
+package stringish
+
+type Interface interface {
+	~[]byte | ~string
+}
diff --git a/vendor/github.com/clipperhouse/uax29/v2/LICENSE b/vendor/github.com/clipperhouse/uax29/v2/LICENSE
new file mode 100644
index 000000000..6ae86a9a1
--- /dev/null
+++ b/vendor/github.com/clipperhouse/uax29/v2/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Matt Sherman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/clipperhouse/uax29/v2/graphemes/README.md b/vendor/github.com/clipperhouse/uax29/v2/graphemes/README.md
new file mode 100644
index 000000000..dc14d11e2
--- /dev/null
+++ b/vendor/github.com/clipperhouse/uax29/v2/graphemes/README.md
@@ -0,0 +1,94 @@
+An implementation of grapheme cluster boundaries from [Unicode text segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (UAX 29), for Unicode version 15.0.0.
+
+[![Documentation](https://pkg.go.dev/badge/github.com/clipperhouse/uax29/v2/graphemes.svg)](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes)
+![Tests](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
+![Fuzz](https://github.com/clipperhouse/uax29/actions/workflows/gofuzz.yml/badge.svg)
+
+## Quick start
+
+```
+go get "github.com/clipperhouse/uax29/v2/graphemes"
+```
+
+```go
+import "github.com/clipperhouse/uax29/v2/graphemes"
+
+text := "Hello, 世界. Nice dog! 👍🐶"
+
+tokens := graphemes.FromString(text)
+
+for tokens.Next() {                     // Next() returns true until end of data
+	fmt.Println(tokens.Value())         // Do something with the current grapheme
+}
+```
+
+_A grapheme is a “single visible character”, which might be a simple as a single letter, or a complex emoji that consists of several Unicode code points._
+
+## Conformance
+
+We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-26.html#Tests29).
+
+![Tests](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
+![Fuzz](https://github.com/clipperhouse/uax29/actions/workflows/gofuzz.yml/badge.svg)
+
+## APIs
+
+### If you have a `string`
+
+```go
+text := "Hello, 世界. Nice dog! 👍🐶"
+
+tokens := graphemes.FromString(text)
+
+for tokens.Next() {                     // Next() returns true until end of data
+	fmt.Println(tokens.Value())         // Do something with the current grapheme
+}
+```
+
+### If you have an `io.Reader`
+
+`FromReader` embeds a [`bufio.Scanner`](https://pkg.go.dev/bufio#Scanner), so just use those methods.
+
+```go
+r := getYourReader()                        // from a file or network maybe
+tokens := graphemes.FromReader(r)
+
+for tokens.Scan() {                         // Scan() returns true until error or EOF
+	fmt.Println(tokens.Text())              // Do something with the current grapheme
+}
+
+if tokens.Err() != nil {                    // Check the error
+	log.Fatal(tokens.Err())
+}
+```
+
+### If you have a `[]byte`
+
+```go
+b := []byte("Hello, 世界. Nice dog! 👍🐶")
+
+tokens := graphemes.FromBytes(b)
+
+for tokens.Next() {                     // Next() returns true until end of data
+	fmt.Println(tokens.Value())         // Do something with the current grapheme
+}
+```
+
+### Benchmarks
+
+On a Mac M2 laptop, we see around 200MB/s, or around 100 million graphemes per second, and no allocations.
+
+```
+goos: darwin
+goarch: arm64
+pkg: github.com/clipperhouse/uax29/graphemes/comparative
+cpu: Apple M2
+BenchmarkGraphemes/clipperhouse/uax29-8    	    173805 ns/op	 201.16 MB/s      0 B/op	   0 allocs/op
+BenchmarkGraphemes/rivo/uniseg-8           	   2045128 ns/op	  17.10 MB/s      0 B/op	   0 allocs/op
+```
+
+### Invalid inputs
+
+Invalid UTF-8 input is considered undefined behavior. We test to ensure that bad inputs will not cause pathological outcomes, such as a panic or infinite loop. Callers should expect “garbage-in, garbage-out”.
+
+Your pipeline should probably include a call to [`utf8.Valid()`](https://pkg.go.dev/unicode/utf8#Valid).
diff --git a/vendor/github.com/clipperhouse/uax29/v2/graphemes/iterator.go b/vendor/github.com/clipperhouse/uax29/v2/graphemes/iterator.go
new file mode 100644
index 000000000..1eaaa534c
--- /dev/null
+++ b/vendor/github.com/clipperhouse/uax29/v2/graphemes/iterator.go
@@ -0,0 +1,31 @@
+package graphemes
+
+import (
+	"github.com/clipperhouse/stringish"
+	"github.com/clipperhouse/uax29/v2/internal/iterators"
+)
+
+type Iterator[T stringish.Interface] struct {
+	*iterators.Iterator[T]
+}
+
+var (
+	splitFuncString = splitFunc[string]
+	splitFuncBytes  = splitFunc[[]byte]
+)
+
+// FromString returns an iterator for the grapheme clusters in the input string.
+// Iterate while Next() is true, and access the grapheme via Value().
+func FromString(s string) Iterator[string] {
+	return Iterator[string]{
+		iterators.New(splitFuncString, s),
+	}
+}
+
+// FromBytes returns an iterator for the grapheme clusters in the input bytes.
+// Iterate while Next() is true, and access the grapheme via Value().
+func FromBytes(b []byte) Iterator[[]byte] {
+	return Iterator[[]byte]{
+		iterators.New(splitFuncBytes, b),
+	}
+}
diff --git a/vendor/github.com/clipperhouse/uax29/v2/graphemes/reader.go b/vendor/github.com/clipperhouse/uax29/v2/graphemes/reader.go
new file mode 100644
index 000000000..9aa006618
--- /dev/null
+++ b/vendor/github.com/clipperhouse/uax29/v2/graphemes/reader.go
@@ -0,0 +1,25 @@
+// Package graphemes implements Unicode grapheme cluster boundaries: https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
+package graphemes
+
+import (
+	"bufio"
+	"io"
+)
+
+type Scanner struct {
+	*bufio.Scanner
+}
+
+// FromReader returns a Scanner, to split graphemes per
+// https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
+//
+// It embeds a [bufio.Scanner], so you can use its methods.
+//
+// Iterate through graphemes by calling Scan() until false, then check Err().
+func FromReader(r io.Reader) *Scanner {
+	sc := bufio.NewScanner(r)
+	sc.Split(SplitFunc)
+	return &Scanner{
+		Scanner: sc,
+	}
+}
diff --git a/vendor/github.com/clipperhouse/uax29/v2/graphemes/splitfunc.go b/vendor/github.com/clipperhouse/uax29/v2/graphemes/splitfunc.go
new file mode 100644
index 000000000..cbe1ec9ef
--- /dev/null
+++ b/vendor/github.com/clipperhouse/uax29/v2/graphemes/splitfunc.go
@@ -0,0 +1,174 @@
+package graphemes
+
+import (
+	"bufio"
+
+	"github.com/clipperhouse/stringish"
+)
+
+// is determines if lookup intersects propert(ies)
+func (lookup property) is(properties property) bool {
+	return (lookup & properties) != 0
+}
+
+const _Ignore = _Extend
+
+// SplitFunc is a bufio.SplitFunc implementation of Unicode grapheme cluster segmentation, for use with bufio.Scanner.
+//
+// See https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
+var SplitFunc bufio.SplitFunc = splitFunc[[]byte]
+
+func splitFunc[T stringish.Interface](data T, atEOF bool) (advance int, token T, err error) {
+	var empty T
+	if len(data) == 0 {
+		return 0, empty, nil
+	}
+
+	// These vars are stateful across loop iterations
+	var pos int
+	var lastExIgnore property = 0     // "last excluding ignored categories"
+	var lastLastExIgnore property = 0 // "last one before that"
+	var regionalIndicatorCount int
+
+	// Rules are usually of the form Cat1 × Cat2; "current" refers to the first property
+	// to the right of the ×, from which we look back or forward
+
+	current, w := lookup(data[pos:])
+	if w == 0 {
+		if !atEOF {
+			// Rune extends past current data, request more
+			return 0, empty, nil
+		}
+		pos = len(data)
+		return pos, data[:pos], nil
+	}
+
+	// https://unicode.org/reports/tr29/#GB1
+	// Start of text always advances
+	pos += w
+
+	for {
+		eot := pos == len(data) // "end of text"
+
+		if eot {
+			if !atEOF {
+				// Token extends past current data, request more
+				return 0, empty, nil
+			}
+
+			// https://unicode.org/reports/tr29/#GB2
+			break
+		}
+
+		/*
+			We've switched the evaluation order of GB1↓ and GB2↑. It's ok:
+			because we've checked for len(data) at the top of this function,
+			sot and eot are mutually exclusive, order doesn't matter.
+		*/
+
+		// Rules are usually of the form Cat1 × Cat2; "current" refers to the first property
+		// to the right of the ×, from which we look back or forward
+
+		// Remember previous properties to avoid lookups/lookbacks
+		last := current
+		if !last.is(_Ignore) {
+			lastLastExIgnore = lastExIgnore
+			lastExIgnore = last
+		}
+
+		current, w = lookup(data[pos:])
+		if w == 0 {
+			if atEOF {
+				// Just return the bytes, we can't do anything with them
+				pos = len(data)
+				break
+			}
+			// Rune extends past current data, request more
+			return 0, empty, nil
+		}
+
+		// Optimization: no rule can possibly apply
+		if current|last == 0 { // i.e. both are zero
+			break
+		}
+
+		// https://unicode.org/reports/tr29/#GB3
+		if current.is(_LF) && last.is(_CR) {
+			pos += w
+			continue
+		}
+
+		// https://unicode.org/reports/tr29/#GB4
+		// https://unicode.org/reports/tr29/#GB5
+		if (current | last).is(_Control | _CR | _LF) {
+			break
+		}
+
+		// https://unicode.org/reports/tr29/#GB6
+		if current.is(_L|_V|_LV|_LVT) && last.is(_L) {
+			pos += w
+			continue
+		}
+
+		// https://unicode.org/reports/tr29/#GB7
+		if current.is(_V|_T) && last.is(_LV|_V) {
+			pos += w
+			continue
+		}
+
+		// https://unicode.org/reports/tr29/#GB8
+		if current.is(_T) && last.is(_LVT|_T) {
+			pos += w
+			continue
+		}
+
+		// https://unicode.org/reports/tr29/#GB9
+		if current.is(_Extend | _ZWJ) {
+			pos += w
+			continue
+		}
+
+		// https://unicode.org/reports/tr29/#GB9a
+		if current.is(_SpacingMark) {
+			pos += w
+			continue
+		}
+
+		// https://unicode.org/reports/tr29/#GB9b
+		if last.is(_Prepend) {
+			pos += w
+			continue
+		}
+
+		// https://unicode.org/reports/tr29/#GB9c
+		// TODO(clipperhouse):
+		// It appears to be added in Unicode 15.1.0:
+		// https://unicode.org/versions/Unicode15.1.0/#Migration
+		// This package currently supports Unicode 15.0.0, so
+		// out of scope for now
+
+		// https://unicode.org/reports/tr29/#GB11
+		if current.is(_ExtendedPictographic) && last.is(_ZWJ) && lastLastExIgnore.is(_ExtendedPictographic) {
+			pos += w
+			continue
+		}
+
+		// https://unicode.org/reports/tr29/#GB12
+		// https://unicode.org/reports/tr29/#GB13
+		if (current & last).is(_RegionalIndicator) {
+			regionalIndicatorCount++
+
+			odd := regionalIndicatorCount%2 == 1
+			if odd {
+				pos += w
+				continue
+			}
+		}
+
+		// If we fall through all the above rules, it's a grapheme cluster break
+		break
+	}
+
+	// Return token
+	return pos, data[:pos], nil
+}
diff --git a/vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go b/vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go
new file mode 100644
index 000000000..8aaabfacf
--- /dev/null
+++ b/vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go
@@ -0,0 +1,1409 @@
+package graphemes
+
+import "github.com/clipperhouse/stringish"
+
+// generated by github.com/clipperhouse/uax29/v2
+// from https://www.unicode.org/Public/15.0.0/ucd/auxiliary/GraphemeBreakProperty.txt
+
+type property uint16
+
+const (
+	_CR property = 1 << iota
+	_Control
+	_Extend
+	_ExtendedPictographic
+	_L
+	_LF
+	_LV
+	_LVT
+	_Prepend
+	_RegionalIndicator
+	_SpacingMark
+	_T
+	_V
+	_ZWJ
+)
+
+// lookup returns the trie value for the first UTF-8 encoding in s and
+// the width in bytes of this encoding. The size will be 0 if s does not
+// hold enough bytes to complete the encoding. len(s) must be greater than 0.
+func lookup[T stringish.Interface](s T) (v property, sz int) {
+	c0 := s[0]
+	switch {
+	case c0 < 0x80: // is ASCII
+		return graphemesValues[c0], 1
+	case c0 < 0xC2:
+		return 0, 1 // Illegal UTF-8: not a starter, not ASCII.
+	case c0 < 0xE0: // 2-byte UTF-8
+		if len(s) < 2 {
+			return 0, 0
+		}
+		i := graphemesIndex[c0]
+		c1 := s[1]
+		if c1 < 0x80 || 0xC0 <= c1 {
+			return 0, 1 // Illegal UTF-8: not a continuation byte.
+		}
+		return lookupValue(uint32(i), c1), 2
+	case c0 < 0xF0: // 3-byte UTF-8
+		if len(s) < 3 {
+			return 0, 0
+		}
+		i := graphemesIndex[c0]
+		c1 := s[1]
+		if c1 < 0x80 || 0xC0 <= c1 {
+			return 0, 1 // Illegal UTF-8: not a continuation byte.
+		}
+		o := uint32(i)<<6 + uint32(c1)
+		i = graphemesIndex[o]
+		c2 := s[2]
+		if c2 < 0x80 || 0xC0 <= c2 {
+			return 0, 2 // Illegal UTF-8: not a continuation byte.
+		}
+		return lookupValue(uint32(i), c2), 3
+	case c0 < 0xF8: // 4-byte UTF-8
+		if len(s) < 4 {
+			return 0, 0
+		}
+		i := graphemesIndex[c0]
+		c1 := s[1]
+		if c1 < 0x80 || 0xC0 <= c1 {
+			return 0, 1 // Illegal UTF-8: not a continuation byte.
+		}
+		o := uint32(i)<<6 + uint32(c1)
+		i = graphemesIndex[o]
+		c2 := s[2]
+		if c2 < 0x80 || 0xC0 <= c2 {
+			return 0, 2 // Illegal UTF-8: not a continuation byte.
+		}
+		o = uint32(i)<<6 + uint32(c2)
+		i = graphemesIndex[o]
+		c3 := s[3]
+		if c3 < 0x80 || 0xC0 <= c3 {
+			return 0, 3 // Illegal UTF-8: not a continuation byte.
+		}
+		return lookupValue(uint32(i), c3), 4
+	}
+	// Illegal rune
+	return 0, 1
+}
+
+// graphemesTrie. Total size: 29120 bytes (28.44 KiB). Checksum: 80ad0c5ab9375f7.
+// type graphemesTrie struct { }
+
+// func newGraphemesTrie(i int) *graphemesTrie {
+// 	return &graphemesTrie{}
+// }
+
+// lookupValue determines the type of block n and looks up the value for b.
+func lookupValue(n uint32, b byte) property {
+	switch {
+	default:
+		return property(graphemesValues[n<<6+uint32(b)])
+	}
+}
+
+// graphemesValues: 215 blocks, 13760 entries, 27520 bytes
+// The third block is the zero block.
+var graphemesValues = [13760]property{
+	// Block 0x0, offset 0x0
+	0x00: 0x0002, 0x01: 0x0002, 0x02: 0x0002, 0x03: 0x0002, 0x04: 0x0002, 0x05: 0x0002,
+	0x06: 0x0002, 0x07: 0x0002, 0x08: 0x0002, 0x09: 0x0002, 0x0a: 0x0020, 0x0b: 0x0002,
+	0x0c: 0x0002, 0x0d: 0x0001, 0x0e: 0x0002, 0x0f: 0x0002, 0x10: 0x0002, 0x11: 0x0002,
+	0x12: 0x0002, 0x13: 0x0002, 0x14: 0x0002, 0x15: 0x0002, 0x16: 0x0002, 0x17: 0x0002,
+	0x18: 0x0002, 0x19: 0x0002, 0x1a: 0x0002, 0x1b: 0x0002, 0x1c: 0x0002, 0x1d: 0x0002,
+	0x1e: 0x0002, 0x1f: 0x0002,
+	// Block 0x1, offset 0x40
+	0x7f: 0x0002,
+	// Block 0x2, offset 0x80
+	// Block 0x3, offset 0xc0
+	0xc0: 0x0002, 0xc1: 0x0002, 0xc2: 0x0002, 0xc3: 0x0002, 0xc4: 0x0002, 0xc5: 0x0002,
+	0xc6: 0x0002, 0xc7: 0x0002, 0xc8: 0x0002, 0xc9: 0x0002, 0xca: 0x0002, 0xcb: 0x0002,
+	0xcc: 0x0002, 0xcd: 0x0002, 0xce: 0x0002, 0xcf: 0x0002, 0xd0: 0x0002, 0xd1: 0x0002,
+	0xd2: 0x0002, 0xd3: 0x0002, 0xd4: 0x0002, 0xd5: 0x0002, 0xd6: 0x0002, 0xd7: 0x0002,
+	0xd8: 0x0002, 0xd9: 0x0002, 0xda: 0x0002, 0xdb: 0x0002, 0xdc: 0x0002, 0xdd: 0x0002,
+	0xde: 0x0002, 0xdf: 0x0002,
+	0xe9: 0x0008,
+	0xed: 0x0002, 0xee: 0x0008,
+	// Block 0x4, offset 0x100
+	0x100: 0x0004, 0x101: 0x0004, 0x102: 0x0004, 0x103: 0x0004, 0x104: 0x0004, 0x105: 0x0004,
+	0x106: 0x0004, 0x107: 0x0004, 0x108: 0x0004, 0x109: 0x0004, 0x10a: 0x0004, 0x10b: 0x0004,
+	0x10c: 0x0004, 0x10d: 0x0004, 0x10e: 0x0004, 0x10f: 0x0004, 0x110: 0x0004, 0x111: 0x0004,
+	0x112: 0x0004, 0x113: 0x0004, 0x114: 0x0004, 0x115: 0x0004, 0x116: 0x0004, 0x117: 0x0004,
+	0x118: 0x0004, 0x119: 0x0004, 0x11a: 0x0004, 0x11b: 0x0004, 0x11c: 0x0004, 0x11d: 0x0004,
+	0x11e: 0x0004, 0x11f: 0x0004, 0x120: 0x0004, 0x121: 0x0004, 0x122: 0x0004, 0x123: 0x0004,
+	0x124: 0x0004, 0x125: 0x0004, 0x126: 0x0004, 0x127: 0x0004, 0x128: 0x0004, 0x129: 0x0004,
+	0x12a: 0x0004, 0x12b: 0x0004, 0x12c: 0x0004, 0x12d: 0x0004, 0x12e: 0x0004, 0x12f: 0x0004,
+	0x130: 0x0004, 0x131: 0x0004, 0x132: 0x0004, 0x133: 0x0004, 0x134: 0x0004, 0x135: 0x0004,
+	0x136: 0x0004, 0x137: 0x0004, 0x138: 0x0004, 0x139: 0x0004, 0x13a: 0x0004, 0x13b: 0x0004,
+	0x13c: 0x0004, 0x13d: 0x0004, 0x13e: 0x0004, 0x13f: 0x0004,
+	// Block 0x5, offset 0x140
+	0x140: 0x0004, 0x141: 0x0004, 0x142: 0x0004, 0x143: 0x0004, 0x144: 0x0004, 0x145: 0x0004,
+	0x146: 0x0004, 0x147: 0x0004, 0x148: 0x0004, 0x149: 0x0004, 0x14a: 0x0004, 0x14b: 0x0004,
+	0x14c: 0x0004, 0x14d: 0x0004, 0x14e: 0x0004, 0x14f: 0x0004, 0x150: 0x0004, 0x151: 0x0004,
+	0x152: 0x0004, 0x153: 0x0004, 0x154: 0x0004, 0x155: 0x0004, 0x156: 0x0004, 0x157: 0x0004,
+	0x158: 0x0004, 0x159: 0x0004, 0x15a: 0x0004, 0x15b: 0x0004, 0x15c: 0x0004, 0x15d: 0x0004,
+	0x15e: 0x0004, 0x15f: 0x0004, 0x160: 0x0004, 0x161: 0x0004, 0x162: 0x0004, 0x163: 0x0004,
+	0x164: 0x0004, 0x165: 0x0004, 0x166: 0x0004, 0x167: 0x0004, 0x168: 0x0004, 0x169: 0x0004,
+	0x16a: 0x0004, 0x16b: 0x0004, 0x16c: 0x0004, 0x16d: 0x0004, 0x16e: 0x0004, 0x16f: 0x0004,
+	// Block 0x6, offset 0x180
+	0x183: 0x0004, 0x184: 0x0004, 0x185: 0x0004,
+	0x186: 0x0004, 0x187: 0x0004, 0x188: 0x0004, 0x189: 0x0004,
+	// Block 0x7, offset 0x1c0
+	0x1d1: 0x0004,
+	0x1d2: 0x0004, 0x1d3: 0x0004, 0x1d4: 0x0004, 0x1d5: 0x0004, 0x1d6: 0x0004, 0x1d7: 0x0004,
+	0x1d8: 0x0004, 0x1d9: 0x0004, 0x1da: 0x0004, 0x1db: 0x0004, 0x1dc: 0x0004, 0x1dd: 0x0004,
+	0x1de: 0x0004, 0x1df: 0x0004, 0x1e0: 0x0004, 0x1e1: 0x0004, 0x1e2: 0x0004, 0x1e3: 0x0004,
+	0x1e4: 0x0004, 0x1e5: 0x0004, 0x1e6: 0x0004, 0x1e7: 0x0004, 0x1e8: 0x0004, 0x1e9: 0x0004,
+	0x1ea: 0x0004, 0x1eb: 0x0004, 0x1ec: 0x0004, 0x1ed: 0x0004, 0x1ee: 0x0004, 0x1ef: 0x0004,
+	0x1f0: 0x0004, 0x1f1: 0x0004, 0x1f2: 0x0004, 0x1f3: 0x0004, 0x1f4: 0x0004, 0x1f5: 0x0004,
+	0x1f6: 0x0004, 0x1f7: 0x0004, 0x1f8: 0x0004, 0x1f9: 0x0004, 0x1fa: 0x0004, 0x1fb: 0x0004,
+	0x1fc: 0x0004, 0x1fd: 0x0004, 0x1ff: 0x0004,
+	// Block 0x8, offset 0x200
+	0x201: 0x0004, 0x202: 0x0004, 0x204: 0x0004, 0x205: 0x0004,
+	0x207: 0x0004,
+	// Block 0x9, offset 0x240
+	0x240: 0x0100, 0x241: 0x0100, 0x242: 0x0100, 0x243: 0x0100, 0x244: 0x0100, 0x245: 0x0100,
+	0x250: 0x0004, 0x251: 0x0004,
+	0x252: 0x0004, 0x253: 0x0004, 0x254: 0x0004, 0x255: 0x0004, 0x256: 0x0004, 0x257: 0x0004,
+	0x258: 0x0004, 0x259: 0x0004, 0x25a: 0x0004, 0x25c: 0x0002,
+	// Block 0xa, offset 0x280
+	0x28b: 0x0004,
+	0x28c: 0x0004, 0x28d: 0x0004, 0x28e: 0x0004, 0x28f: 0x0004, 0x290: 0x0004, 0x291: 0x0004,
+	0x292: 0x0004, 0x293: 0x0004, 0x294: 0x0004, 0x295: 0x0004, 0x296: 0x0004, 0x297: 0x0004,
+	0x298: 0x0004, 0x299: 0x0004, 0x29a: 0x0004, 0x29b: 0x0004, 0x29c: 0x0004, 0x29d: 0x0004,
+	0x29e: 0x0004, 0x29f: 0x0004,
+	0x2b0: 0x0004,
+	// Block 0xb, offset 0x2c0
+	0x2d6: 0x0004, 0x2d7: 0x0004,
+	0x2d8: 0x0004, 0x2d9: 0x0004, 0x2da: 0x0004, 0x2db: 0x0004, 0x2dc: 0x0004, 0x2dd: 0x0100,
+	0x2df: 0x0004, 0x2e0: 0x0004, 0x2e1: 0x0004, 0x2e2: 0x0004, 0x2e3: 0x0004,
+	0x2e4: 0x0004, 0x2e7: 0x0004, 0x2e8: 0x0004,
+	0x2ea: 0x0004, 0x2eb: 0x0004, 0x2ec: 0x0004, 0x2ed: 0x0004,
+	// Block 0xc, offset 0x300
+	0x30f: 0x0100, 0x311: 0x0004,
+	0x330: 0x0004, 0x331: 0x0004, 0x332: 0x0004, 0x333: 0x0004, 0x334: 0x0004, 0x335: 0x0004,
+	0x336: 0x0004, 0x337: 0x0004, 0x338: 0x0004, 0x339: 0x0004, 0x33a: 0x0004, 0x33b: 0x0004,
+	0x33c: 0x0004, 0x33d: 0x0004, 0x33e: 0x0004, 0x33f: 0x0004,
+	// Block 0xd, offset 0x340
+	0x340: 0x0004, 0x341: 0x0004, 0x342: 0x0004, 0x343: 0x0004, 0x344: 0x0004, 0x345: 0x0004,
+	0x346: 0x0004, 0x347: 0x0004, 0x348: 0x0004, 0x349: 0x0004, 0x34a: 0x0004,
+	// Block 0xe, offset 0x380
+	0x3a6: 0x0004, 0x3a7: 0x0004, 0x3a8: 0x0004, 0x3a9: 0x0004,
+	0x3aa: 0x0004, 0x3ab: 0x0004, 0x3ac: 0x0004, 0x3ad: 0x0004, 0x3ae: 0x0004, 0x3af: 0x0004,
+	0x3b0: 0x0004,
+	// Block 0xf, offset 0x3c0
+	0x3eb: 0x0004, 0x3ec: 0x0004, 0x3ed: 0x0004, 0x3ee: 0x0004, 0x3ef: 0x0004,
+	0x3f0: 0x0004, 0x3f1: 0x0004, 0x3f2: 0x0004, 0x3f3: 0x0004,
+	0x3fd: 0x0004,
+	// Block 0x10, offset 0x400
+	0x416: 0x0004, 0x417: 0x0004,
+	0x418: 0x0004, 0x419: 0x0004, 0x41b: 0x0004, 0x41c: 0x0004, 0x41d: 0x0004,
+	0x41e: 0x0004, 0x41f: 0x0004, 0x420: 0x0004, 0x421: 0x0004, 0x422: 0x0004, 0x423: 0x0004,
+	0x425: 0x0004, 0x426: 0x0004, 0x427: 0x0004, 0x429: 0x0004,
+	0x42a: 0x0004, 0x42b: 0x0004, 0x42c: 0x0004, 0x42d: 0x0004,
+	// Block 0x11, offset 0x440
+	0x459: 0x0004, 0x45a: 0x0004, 0x45b: 0x0004,
+	// Block 0x12, offset 0x480
+	0x490: 0x0100, 0x491: 0x0100,
+	0x498: 0x0004, 0x499: 0x0004, 0x49a: 0x0004, 0x49b: 0x0004, 0x49c: 0x0004, 0x49d: 0x0004,
+	0x49e: 0x0004, 0x49f: 0x0004,
+	// Block 0x13, offset 0x4c0
+	0x4ca: 0x0004, 0x4cb: 0x0004,
+	0x4cc: 0x0004, 0x4cd: 0x0004, 0x4ce: 0x0004, 0x4cf: 0x0004, 0x4d0: 0x0004, 0x4d1: 0x0004,
+	0x4d2: 0x0004, 0x4d3: 0x0004, 0x4d4: 0x0004, 0x4d5: 0x0004, 0x4d6: 0x0004, 0x4d7: 0x0004,
+	0x4d8: 0x0004, 0x4d9: 0x0004, 0x4da: 0x0004, 0x4db: 0x0004, 0x4dc: 0x0004, 0x4dd: 0x0004,
+	0x4de: 0x0004, 0x4df: 0x0004, 0x4e0: 0x0004, 0x4e1: 0x0004, 0x4e2: 0x0100, 0x4e3: 0x0004,
+	0x4e4: 0x0004, 0x4e5: 0x0004, 0x4e6: 0x0004, 0x4e7: 0x0004, 0x4e8: 0x0004, 0x4e9: 0x0004,
+	0x4ea: 0x0004, 0x4eb: 0x0004, 0x4ec: 0x0004, 0x4ed: 0x0004, 0x4ee: 0x0004, 0x4ef: 0x0004,
+	0x4f0: 0x0004, 0x4f1: 0x0004, 0x4f2: 0x0004, 0x4f3: 0x0004, 0x4f4: 0x0004, 0x4f5: 0x0004,
+	0x4f6: 0x0004, 0x4f7: 0x0004, 0x4f8: 0x0004, 0x4f9: 0x0004, 0x4fa: 0x0004, 0x4fb: 0x0004,
+	0x4fc: 0x0004, 0x4fd: 0x0004, 0x4fe: 0x0004, 0x4ff: 0x0004,
+	// Block 0x14, offset 0x500
+	0x500: 0x0004, 0x501: 0x0004, 0x502: 0x0004, 0x503: 0x0400,
+	0x53a: 0x0004, 0x53b: 0x0400,
+	0x53c: 0x0004, 0x53e: 0x0400, 0x53f: 0x0400,
+	// Block 0x15, offset 0x540
+	0x540: 0x0400, 0x541: 0x0004, 0x542: 0x0004, 0x543: 0x0004, 0x544: 0x0004, 0x545: 0x0004,
+	0x546: 0x0004, 0x547: 0x0004, 0x548: 0x0004, 0x549: 0x0400, 0x54a: 0x0400, 0x54b: 0x0400,
+	0x54c: 0x0400, 0x54d: 0x0004, 0x54e: 0x0400, 0x54f: 0x0400, 0x551: 0x0004,
+	0x552: 0x0004, 0x553: 0x0004, 0x554: 0x0004, 0x555: 0x0004, 0x556: 0x0004, 0x557: 0x0004,
+	0x562: 0x0004, 0x563: 0x0004,
+	// Block 0x16, offset 0x580
+	0x581: 0x0004, 0x582: 0x0400, 0x583: 0x0400,
+	0x5bc: 0x0004, 0x5be: 0x0004, 0x5bf: 0x0400,
+	// Block 0x17, offset 0x5c0
+	0x5c0: 0x0400, 0x5c1: 0x0004, 0x5c2: 0x0004, 0x5c3: 0x0004, 0x5c4: 0x0004,
+	0x5c7: 0x0400, 0x5c8: 0x0400, 0x5cb: 0x0400,
+	0x5cc: 0x0400, 0x5cd: 0x0004,
+	0x5d7: 0x0004,
+	0x5e2: 0x0004, 0x5e3: 0x0004,
+	0x5fe: 0x0004,
+	// Block 0x18, offset 0x600
+	0x601: 0x0004, 0x602: 0x0004, 0x603: 0x0400,
+	0x63c: 0x0004, 0x63e: 0x0400, 0x63f: 0x0400,
+	// Block 0x19, offset 0x640
+	0x640: 0x0400, 0x641: 0x0004, 0x642: 0x0004,
+	0x647: 0x0004, 0x648: 0x0004, 0x64b: 0x0004,
+	0x64c: 0x0004, 0x64d: 0x0004, 0x651: 0x0004,
+	0x670: 0x0004, 0x671: 0x0004, 0x675: 0x0004,
+	// Block 0x1a, offset 0x680
+	0x680: 0x0400, 0x681: 0x0004, 0x682: 0x0004, 0x683: 0x0004, 0x684: 0x0004, 0x685: 0x0004,
+	0x687: 0x0004, 0x688: 0x0004, 0x689: 0x0400, 0x68b: 0x0400,
+	0x68c: 0x0400, 0x68d: 0x0004,
+	0x6a2: 0x0004, 0x6a3: 0x0004,
+	0x6ba: 0x0004, 0x6bb: 0x0004,
+	0x6bc: 0x0004, 0x6bd: 0x0004, 0x6be: 0x0004, 0x6bf: 0x0004,
+	// Block 0x1b, offset 0x6c0
+	0x6c1: 0x0004, 0x6c2: 0x0400, 0x6c3: 0x0400,
+	0x6fc: 0x0004, 0x6fe: 0x0004, 0x6ff: 0x0004,
+	// Block 0x1c, offset 0x700
+	0x700: 0x0400, 0x701: 0x0004, 0x702: 0x0004, 0x703: 0x0004, 0x704: 0x0004,
+	0x707: 0x0400, 0x708: 0x0400, 0x70b: 0x0400,
+	0x70c: 0x0400, 0x70d: 0x0004,
+	0x715: 0x0004, 0x716: 0x0004, 0x717: 0x0004,
+	0x722: 0x0004, 0x723: 0x0004,
+	// Block 0x1d, offset 0x740
+	0x742: 0x0004,
+	0x77e: 0x0004, 0x77f: 0x0400,
+	// Block 0x1e, offset 0x780
+	0x780: 0x0004, 0x781: 0x0400, 0x782: 0x0400,
+	0x786: 0x0400, 0x787: 0x0400, 0x788: 0x0400, 0x78a: 0x0400, 0x78b: 0x0400,
+	0x78c: 0x0400, 0x78d: 0x0004,
+	0x797: 0x0004,
+	// Block 0x1f, offset 0x7c0
+	0x7c0: 0x0004, 0x7c1: 0x0400, 0x7c2: 0x0400, 0x7c3: 0x0400, 0x7c4: 0x0004,
+	0x7fc: 0x0004, 0x7fe: 0x0004, 0x7ff: 0x0004,
+	// Block 0x20, offset 0x800
+	0x800: 0x0004, 0x801: 0x0400, 0x802: 0x0400, 0x803: 0x0400, 0x804: 0x0400,
+	0x806: 0x0004, 0x807: 0x0004, 0x808: 0x0004, 0x80a: 0x0004, 0x80b: 0x0004,
+	0x80c: 0x0004, 0x80d: 0x0004,
+	0x815: 0x0004, 0x816: 0x0004,
+	0x822: 0x0004, 0x823: 0x0004,
+	// Block 0x21, offset 0x840
+	0x841: 0x0004, 0x842: 0x0400, 0x843: 0x0400,
+	0x87c: 0x0004, 0x87e: 0x0400, 0x87f: 0x0004,
+	// Block 0x22, offset 0x880
+	0x880: 0x0400, 0x881: 0x0400, 0x882: 0x0004, 0x883: 0x0400, 0x884: 0x0400,
+	0x886: 0x0004, 0x887: 0x0400, 0x888: 0x0400, 0x88a: 0x0400, 0x88b: 0x0400,
+	0x88c: 0x0004, 0x88d: 0x0004,
+	0x895: 0x0004, 0x896: 0x0004,
+	0x8a2: 0x0004, 0x8a3: 0x0004,
+	0x8b3: 0x0400,
+	// Block 0x23, offset 0x8c0
+	0x8c0: 0x0004, 0x8c1: 0x0004, 0x8c2: 0x0400, 0x8c3: 0x0400,
+	0x8fb: 0x0004,
+	0x8fc: 0x0004, 0x8fe: 0x0004, 0x8ff: 0x0400,
+	// Block 0x24, offset 0x900
+	0x900: 0x0400, 0x901: 0x0004, 0x902: 0x0004, 0x903: 0x0004, 0x904: 0x0004,
+	0x906: 0x0400, 0x907: 0x0400, 0x908: 0x0400, 0x90a: 0x0400, 0x90b: 0x0400,
+	0x90c: 0x0400, 0x90d: 0x0004, 0x90e: 0x0100,
+	0x917: 0x0004,
+	0x922: 0x0004, 0x923: 0x0004,
+	// Block 0x25, offset 0x940
+	0x941: 0x0004, 0x942: 0x0400, 0x943: 0x0400,
+	// Block 0x26, offset 0x980
+	0x98a: 0x0004,
+	0x98f: 0x0004, 0x990: 0x0400, 0x991: 0x0400,
+	0x992: 0x0004, 0x993: 0x0004, 0x994: 0x0004, 0x996: 0x0004,
+	0x998: 0x0400, 0x999: 0x0400, 0x99a: 0x0400, 0x99b: 0x0400, 0x99c: 0x0400, 0x99d: 0x0400,
+	0x99e: 0x0400, 0x99f: 0x0004,
+	0x9b2: 0x0400, 0x9b3: 0x0400,
+	// Block 0x27, offset 0x9c0
+	0x9f1: 0x0004, 0x9f3: 0x0400, 0x9f4: 0x0004, 0x9f5: 0x0004,
+	0x9f6: 0x0004, 0x9f7: 0x0004, 0x9f8: 0x0004, 0x9f9: 0x0004, 0x9fa: 0x0004,
+	// Block 0x28, offset 0xa00
+	0xa07: 0x0004, 0xa08: 0x0004, 0xa09: 0x0004, 0xa0a: 0x0004, 0xa0b: 0x0004,
+	0xa0c: 0x0004, 0xa0d: 0x0004, 0xa0e: 0x0004,
+	// Block 0x29, offset 0xa40
+	0xa71: 0x0004, 0xa73: 0x0400, 0xa74: 0x0004, 0xa75: 0x0004,
+	0xa76: 0x0004, 0xa77: 0x0004, 0xa78: 0x0004, 0xa79: 0x0004, 0xa7a: 0x0004, 0xa7b: 0x0004,
+	0xa7c: 0x0004,
+	// Block 0x2a, offset 0xa80
+	0xa88: 0x0004, 0xa89: 0x0004, 0xa8a: 0x0004, 0xa8b: 0x0004,
+	0xa8c: 0x0004, 0xa8d: 0x0004, 0xa8e: 0x0004,
+	// Block 0x2b, offset 0xac0
+	0xad8: 0x0004, 0xad9: 0x0004,
+	0xaf5: 0x0004,
+	0xaf7: 0x0004, 0xaf9: 0x0004,
+	0xafe: 0x0400, 0xaff: 0x0400,
+	// Block 0x2c, offset 0xb00
+	0xb31: 0x0004, 0xb32: 0x0004, 0xb33: 0x0004, 0xb34: 0x0004, 0xb35: 0x0004,
+	0xb36: 0x0004, 0xb37: 0x0004, 0xb38: 0x0004, 0xb39: 0x0004, 0xb3a: 0x0004, 0xb3b: 0x0004,
+	0xb3c: 0x0004, 0xb3d: 0x0004, 0xb3e: 0x0004, 0xb3f: 0x0400,
+	// Block 0x2d, offset 0xb40
+	0xb40: 0x0004, 0xb41: 0x0004, 0xb42: 0x0004, 0xb43: 0x0004, 0xb44: 0x0004,
+	0xb46: 0x0004, 0xb47: 0x0004,
+	0xb4d: 0x0004, 0xb4e: 0x0004, 0xb4f: 0x0004, 0xb50: 0x0004, 0xb51: 0x0004,
+	0xb52: 0x0004, 0xb53: 0x0004, 0xb54: 0x0004, 0xb55: 0x0004, 0xb56: 0x0004, 0xb57: 0x0004,
+	0xb59: 0x0004, 0xb5a: 0x0004, 0xb5b: 0x0004, 0xb5c: 0x0004, 0xb5d: 0x0004,
+	0xb5e: 0x0004, 0xb5f: 0x0004, 0xb60: 0x0004, 0xb61: 0x0004, 0xb62: 0x0004, 0xb63: 0x0004,
+	0xb64: 0x0004, 0xb65: 0x0004, 0xb66: 0x0004, 0xb67: 0x0004, 0xb68: 0x0004, 0xb69: 0x0004,
+	0xb6a: 0x0004, 0xb6b: 0x0004, 0xb6c: 0x0004, 0xb6d: 0x0004, 0xb6e: 0x0004, 0xb6f: 0x0004,
+	0xb70: 0x0004, 0xb71: 0x0004, 0xb72: 0x0004, 0xb73: 0x0004, 0xb74: 0x0004, 0xb75: 0x0004,
+	0xb76: 0x0004, 0xb77: 0x0004, 0xb78: 0x0004, 0xb79: 0x0004, 0xb7a: 0x0004, 0xb7b: 0x0004,
+	0xb7c: 0x0004,
+	// Block 0x2e, offset 0xb80
+	0xb86: 0x0004,
+	// Block 0x2f, offset 0xbc0
+	0xbed: 0x0004, 0xbee: 0x0004, 0xbef: 0x0004,
+	0xbf0: 0x0004, 0xbf1: 0x0400, 0xbf2: 0x0004, 0xbf3: 0x0004, 0xbf4: 0x0004, 0xbf5: 0x0004,
+	0xbf6: 0x0004, 0xbf7: 0x0004, 0xbf9: 0x0004, 0xbfa: 0x0004, 0xbfb: 0x0400,
+	0xbfc: 0x0400, 0xbfd: 0x0004, 0xbfe: 0x0004,
+	// Block 0x30, offset 0xc00
+	0xc16: 0x0400, 0xc17: 0x0400,
+	0xc18: 0x0004, 0xc19: 0x0004,
+	0xc1e: 0x0004, 0xc1f: 0x0004, 0xc20: 0x0004,
+	0xc31: 0x0004, 0xc32: 0x0004, 0xc33: 0x0004, 0xc34: 0x0004,
+	// Block 0x31, offset 0xc40
+	0xc42: 0x0004, 0xc44: 0x0400, 0xc45: 0x0004,
+	0xc46: 0x0004,
+	0xc4d: 0x0004,
+	0xc5d: 0x0004,
+	// Block 0x32, offset 0xc80
+	0xc80: 0x0010, 0xc81: 0x0010, 0xc82: 0x0010, 0xc83: 0x0010, 0xc84: 0x0010, 0xc85: 0x0010,
+	0xc86: 0x0010, 0xc87: 0x0010, 0xc88: 0x0010, 0xc89: 0x0010, 0xc8a: 0x0010, 0xc8b: 0x0010,
+	0xc8c: 0x0010, 0xc8d: 0x0010, 0xc8e: 0x0010, 0xc8f: 0x0010, 0xc90: 0x0010, 0xc91: 0x0010,
+	0xc92: 0x0010, 0xc93: 0x0010, 0xc94: 0x0010, 0xc95: 0x0010, 0xc96: 0x0010, 0xc97: 0x0010,
+	0xc98: 0x0010, 0xc99: 0x0010, 0xc9a: 0x0010, 0xc9b: 0x0010, 0xc9c: 0x0010, 0xc9d: 0x0010,
+	0xc9e: 0x0010, 0xc9f: 0x0010, 0xca0: 0x0010, 0xca1: 0x0010, 0xca2: 0x0010, 0xca3: 0x0010,
+	0xca4: 0x0010, 0xca5: 0x0010, 0xca6: 0x0010, 0xca7: 0x0010, 0xca8: 0x0010, 0xca9: 0x0010,
+	0xcaa: 0x0010, 0xcab: 0x0010, 0xcac: 0x0010, 0xcad: 0x0010, 0xcae: 0x0010, 0xcaf: 0x0010,
+	0xcb0: 0x0010, 0xcb1: 0x0010, 0xcb2: 0x0010, 0xcb3: 0x0010, 0xcb4: 0x0010, 0xcb5: 0x0010,
+	0xcb6: 0x0010, 0xcb7: 0x0010, 0xcb8: 0x0010, 0xcb9: 0x0010, 0xcba: 0x0010, 0xcbb: 0x0010,
+	0xcbc: 0x0010, 0xcbd: 0x0010, 0xcbe: 0x0010, 0xcbf: 0x0010,
+	// Block 0x33, offset 0xcc0
+	0xcc0: 0x0010, 0xcc1: 0x0010, 0xcc2: 0x0010, 0xcc3: 0x0010, 0xcc4: 0x0010, 0xcc5: 0x0010,
+	0xcc6: 0x0010, 0xcc7: 0x0010, 0xcc8: 0x0010, 0xcc9: 0x0010, 0xcca: 0x0010, 0xccb: 0x0010,
+	0xccc: 0x0010, 0xccd: 0x0010, 0xcce: 0x0010, 0xccf: 0x0010, 0xcd0: 0x0010, 0xcd1: 0x0010,
+	0xcd2: 0x0010, 0xcd3: 0x0010, 0xcd4: 0x0010, 0xcd5: 0x0010, 0xcd6: 0x0010, 0xcd7: 0x0010,
+	0xcd8: 0x0010, 0xcd9: 0x0010, 0xcda: 0x0010, 0xcdb: 0x0010, 0xcdc: 0x0010, 0xcdd: 0x0010,
+	0xcde: 0x0010, 0xcdf: 0x0010, 0xce0: 0x1000, 0xce1: 0x1000, 0xce2: 0x1000, 0xce3: 0x1000,
+	0xce4: 0x1000, 0xce5: 0x1000, 0xce6: 0x1000, 0xce7: 0x1000, 0xce8: 0x1000, 0xce9: 0x1000,
+	0xcea: 0x1000, 0xceb: 0x1000, 0xcec: 0x1000, 0xced: 0x1000, 0xcee: 0x1000, 0xcef: 0x1000,
+	0xcf0: 0x1000, 0xcf1: 0x1000, 0xcf2: 0x1000, 0xcf3: 0x1000, 0xcf4: 0x1000, 0xcf5: 0x1000,
+	0xcf6: 0x1000, 0xcf7: 0x1000, 0xcf8: 0x1000, 0xcf9: 0x1000, 0xcfa: 0x1000, 0xcfb: 0x1000,
+	0xcfc: 0x1000, 0xcfd: 0x1000, 0xcfe: 0x1000, 0xcff: 0x1000,
+	// Block 0x34, offset 0xd00
+	0xd00: 0x1000, 0xd01: 0x1000, 0xd02: 0x1000, 0xd03: 0x1000, 0xd04: 0x1000, 0xd05: 0x1000,
+	0xd06: 0x1000, 0xd07: 0x1000, 0xd08: 0x1000, 0xd09: 0x1000, 0xd0a: 0x1000, 0xd0b: 0x1000,
+	0xd0c: 0x1000, 0xd0d: 0x1000, 0xd0e: 0x1000, 0xd0f: 0x1000, 0xd10: 0x1000, 0xd11: 0x1000,
+	0xd12: 0x1000, 0xd13: 0x1000, 0xd14: 0x1000, 0xd15: 0x1000, 0xd16: 0x1000, 0xd17: 0x1000,
+	0xd18: 0x1000, 0xd19: 0x1000, 0xd1a: 0x1000, 0xd1b: 0x1000, 0xd1c: 0x1000, 0xd1d: 0x1000,
+	0xd1e: 0x1000, 0xd1f: 0x1000, 0xd20: 0x1000, 0xd21: 0x1000, 0xd22: 0x1000, 0xd23: 0x1000,
+	0xd24: 0x1000, 0xd25: 0x1000, 0xd26: 0x1000, 0xd27: 0x1000, 0xd28: 0x0800, 0xd29: 0x0800,
+	0xd2a: 0x0800, 0xd2b: 0x0800, 0xd2c: 0x0800, 0xd2d: 0x0800, 0xd2e: 0x0800, 0xd2f: 0x0800,
+	0xd30: 0x0800, 0xd31: 0x0800, 0xd32: 0x0800, 0xd33: 0x0800, 0xd34: 0x0800, 0xd35: 0x0800,
+	0xd36: 0x0800, 0xd37: 0x0800, 0xd38: 0x0800, 0xd39: 0x0800, 0xd3a: 0x0800, 0xd3b: 0x0800,
+	0xd3c: 0x0800, 0xd3d: 0x0800, 0xd3e: 0x0800, 0xd3f: 0x0800,
+	// Block 0x35, offset 0xd40
+	0xd40: 0x0800, 0xd41: 0x0800, 0xd42: 0x0800, 0xd43: 0x0800, 0xd44: 0x0800, 0xd45: 0x0800,
+	0xd46: 0x0800, 0xd47: 0x0800, 0xd48: 0x0800, 0xd49: 0x0800, 0xd4a: 0x0800, 0xd4b: 0x0800,
+	0xd4c: 0x0800, 0xd4d: 0x0800, 0xd4e: 0x0800, 0xd4f: 0x0800, 0xd50: 0x0800, 0xd51: 0x0800,
+	0xd52: 0x0800, 0xd53: 0x0800, 0xd54: 0x0800, 0xd55: 0x0800, 0xd56: 0x0800, 0xd57: 0x0800,
+	0xd58: 0x0800, 0xd59: 0x0800, 0xd5a: 0x0800, 0xd5b: 0x0800, 0xd5c: 0x0800, 0xd5d: 0x0800,
+	0xd5e: 0x0800, 0xd5f: 0x0800, 0xd60: 0x0800, 0xd61: 0x0800, 0xd62: 0x0800, 0xd63: 0x0800,
+	0xd64: 0x0800, 0xd65: 0x0800, 0xd66: 0x0800, 0xd67: 0x0800, 0xd68: 0x0800, 0xd69: 0x0800,
+	0xd6a: 0x0800, 0xd6b: 0x0800, 0xd6c: 0x0800, 0xd6d: 0x0800, 0xd6e: 0x0800, 0xd6f: 0x0800,
+	0xd70: 0x0800, 0xd71: 0x0800, 0xd72: 0x0800, 0xd73: 0x0800, 0xd74: 0x0800, 0xd75: 0x0800,
+	0xd76: 0x0800, 0xd77: 0x0800, 0xd78: 0x0800, 0xd79: 0x0800, 0xd7a: 0x0800, 0xd7b: 0x0800,
+	0xd7c: 0x0800, 0xd7d: 0x0800, 0xd7e: 0x0800, 0xd7f: 0x0800,
+	// Block 0x36, offset 0xd80
+	0xd9d: 0x0004,
+	0xd9e: 0x0004, 0xd9f: 0x0004,
+	// Block 0x37, offset 0xdc0
+	0xdd2: 0x0004, 0xdd3: 0x0004, 0xdd4: 0x0004, 0xdd5: 0x0400,
+	0xdf2: 0x0004, 0xdf3: 0x0004, 0xdf4: 0x0400,
+	// Block 0x38, offset 0xe00
+	0xe12: 0x0004, 0xe13: 0x0004,
+	0xe32: 0x0004, 0xe33: 0x0004,
+	// Block 0x39, offset 0xe40
+	0xe74: 0x0004, 0xe75: 0x0004,
+	0xe76: 0x0400, 0xe77: 0x0004, 0xe78: 0x0004, 0xe79: 0x0004, 0xe7a: 0x0004, 0xe7b: 0x0004,
+	0xe7c: 0x0004, 0xe7d: 0x0004, 0xe7e: 0x0400, 0xe7f: 0x0400,
+	// Block 0x3a, offset 0xe80
+	0xe80: 0x0400, 0xe81: 0x0400, 0xe82: 0x0400, 0xe83: 0x0400, 0xe84: 0x0400, 0xe85: 0x0400,
+	0xe86: 0x0004, 0xe87: 0x0400, 0xe88: 0x0400, 0xe89: 0x0004, 0xe8a: 0x0004, 0xe8b: 0x0004,
+	0xe8c: 0x0004, 0xe8d: 0x0004, 0xe8e: 0x0004, 0xe8f: 0x0004, 0xe90: 0x0004, 0xe91: 0x0004,
+	0xe92: 0x0004, 0xe93: 0x0004,
+	0xe9d: 0x0004,
+	// Block 0x3b, offset 0xec0
+	0xecb: 0x0004,
+	0xecc: 0x0004, 0xecd: 0x0004, 0xece: 0x0002, 0xecf: 0x0004,
+	// Block 0x3c, offset 0xf00
+	0xf05: 0x0004,
+	0xf06: 0x0004,
+	0xf29: 0x0004,
+	// Block 0x3d, offset 0xf40
+	0xf60: 0x0004, 0xf61: 0x0004, 0xf62: 0x0004, 0xf63: 0x0400,
+	0xf64: 0x0400, 0xf65: 0x0400, 0xf66: 0x0400, 0xf67: 0x0004, 0xf68: 0x0004, 0xf69: 0x0400,
+	0xf6a: 0x0400, 0xf6b: 0x0400,
+	0xf70: 0x0400, 0xf71: 0x0400, 0xf72: 0x0004, 0xf73: 0x0400, 0xf74: 0x0400, 0xf75: 0x0400,
+	0xf76: 0x0400, 0xf77: 0x0400, 0xf78: 0x0400, 0xf79: 0x0004, 0xf7a: 0x0004, 0xf7b: 0x0004,
+	// Block 0x3e, offset 0xf80
+	0xf97: 0x0004,
+	0xf98: 0x0004, 0xf99: 0x0400, 0xf9a: 0x0400, 0xf9b: 0x0004,
+	// Block 0x3f, offset 0xfc0
+	0xfd5: 0x0400, 0xfd6: 0x0004, 0xfd7: 0x0400,
+	0xfd8: 0x0004, 0xfd9: 0x0004, 0xfda: 0x0004, 0xfdb: 0x0004, 0xfdc: 0x0004, 0xfdd: 0x0004,
+	0xfde: 0x0004, 0xfe0: 0x0004, 0xfe2: 0x0004,
+	0xfe5: 0x0004, 0xfe6: 0x0004, 0xfe7: 0x0004, 0xfe8: 0x0004, 0xfe9: 0x0004,
+	0xfea: 0x0004, 0xfeb: 0x0004, 0xfec: 0x0004, 0xfed: 0x0400, 0xfee: 0x0400, 0xfef: 0x0400,
+	0xff0: 0x0400, 0xff1: 0x0400, 0xff2: 0x0400, 0xff3: 0x0004, 0xff4: 0x0004, 0xff5: 0x0004,
+	0xff6: 0x0004, 0xff7: 0x0004, 0xff8: 0x0004, 0xff9: 0x0004, 0xffa: 0x0004, 0xffb: 0x0004,
+	0xffc: 0x0004, 0xfff: 0x0004,
+	// Block 0x40, offset 0x1000
+	0x1030: 0x0004, 0x1031: 0x0004, 0x1032: 0x0004, 0x1033: 0x0004, 0x1034: 0x0004, 0x1035: 0x0004,
+	0x1036: 0x0004, 0x1037: 0x0004, 0x1038: 0x0004, 0x1039: 0x0004, 0x103a: 0x0004, 0x103b: 0x0004,
+	0x103c: 0x0004, 0x103d: 0x0004, 0x103e: 0x0004, 0x103f: 0x0004,
+	// Block 0x41, offset 0x1040
+	0x1040: 0x0004, 0x1041: 0x0004, 0x1042: 0x0004, 0x1043: 0x0004, 0x1044: 0x0004, 0x1045: 0x0004,
+	0x1046: 0x0004, 0x1047: 0x0004, 0x1048: 0x0004, 0x1049: 0x0004, 0x104a: 0x0004, 0x104b: 0x0004,
+	0x104c: 0x0004, 0x104d: 0x0004, 0x104e: 0x0004,
+	// Block 0x42, offset 0x1080
+	0x1080: 0x0004, 0x1081: 0x0004, 0x1082: 0x0004, 0x1083: 0x0004, 0x1084: 0x0400,
+	0x10b4: 0x0004, 0x10b5: 0x0004,
+	0x10b6: 0x0004, 0x10b7: 0x0004, 0x10b8: 0x0004, 0x10b9: 0x0004, 0x10ba: 0x0004, 0x10bb: 0x0400,
+	0x10bc: 0x0004, 0x10bd: 0x0400, 0x10be: 0x0400, 0x10bf: 0x0400,
+	// Block 0x43, offset 0x10c0
+	0x10c0: 0x0400, 0x10c1: 0x0400, 0x10c2: 0x0004, 0x10c3: 0x0400, 0x10c4: 0x0400,
+	0x10eb: 0x0004, 0x10ec: 0x0004, 0x10ed: 0x0004, 0x10ee: 0x0004, 0x10ef: 0x0004,
+	0x10f0: 0x0004, 0x10f1: 0x0004, 0x10f2: 0x0004, 0x10f3: 0x0004,
+	// Block 0x44, offset 0x1100
+	0x1100: 0x0004, 0x1101: 0x0004, 0x1102: 0x0400,
+	0x1121: 0x0400, 0x1122: 0x0004, 0x1123: 0x0004,
+	0x1124: 0x0004, 0x1125: 0x0004, 0x1126: 0x0400, 0x1127: 0x0400, 0x1128: 0x0004, 0x1129: 0x0004,
+	0x112a: 0x0400, 0x112b: 0x0004, 0x112c: 0x0004, 0x112d: 0x0004,
+	// Block 0x45, offset 0x1140
+	0x1166: 0x0004, 0x1167: 0x0400, 0x1168: 0x0004, 0x1169: 0x0004,
+	0x116a: 0x0400, 0x116b: 0x0400, 0x116c: 0x0400, 0x116d: 0x0004, 0x116e: 0x0400, 0x116f: 0x0004,
+	0x1170: 0x0004, 0x1171: 0x0004, 0x1172: 0x0400, 0x1173: 0x0400,
+	// Block 0x46, offset 0x1180
+	0x11a4: 0x0400, 0x11a5: 0x0400, 0x11a6: 0x0400, 0x11a7: 0x0400, 0x11a8: 0x0400, 0x11a9: 0x0400,
+	0x11aa: 0x0400, 0x11ab: 0x0400, 0x11ac: 0x0004, 0x11ad: 0x0004, 0x11ae: 0x0004, 0x11af: 0x0004,
+	0x11b0: 0x0004, 0x11b1: 0x0004, 0x11b2: 0x0004, 0x11b3: 0x0004, 0x11b4: 0x0400, 0x11b5: 0x0400,
+	0x11b6: 0x0004, 0x11b7: 0x0004,
+	// Block 0x47, offset 0x11c0
+	0x11d0: 0x0004, 0x11d1: 0x0004,
+	0x11d2: 0x0004, 0x11d4: 0x0004, 0x11d5: 0x0004, 0x11d6: 0x0004, 0x11d7: 0x0004,
+	0x11d8: 0x0004, 0x11d9: 0x0004, 0x11da: 0x0004, 0x11db: 0x0004, 0x11dc: 0x0004, 0x11dd: 0x0004,
+	0x11de: 0x0004, 0x11df: 0x0004, 0x11e0: 0x0004, 0x11e1: 0x0400, 0x11e2: 0x0004, 0x11e3: 0x0004,
+	0x11e4: 0x0004, 0x11e5: 0x0004, 0x11e6: 0x0004, 0x11e7: 0x0004, 0x11e8: 0x0004,
+	0x11ed: 0x0004,
+	0x11f4: 0x0004,
+	0x11f7: 0x0400, 0x11f8: 0x0004, 0x11f9: 0x0004,
+	// Block 0x48, offset 0x1200
+	0x120b: 0x0002,
+	0x120c: 0x0004, 0x120d: 0x2000, 0x120e: 0x0002, 0x120f: 0x0002,
+	0x1228: 0x0002, 0x1229: 0x0002,
+	0x122a: 0x0002, 0x122b: 0x0002, 0x122c: 0x0002, 0x122d: 0x0002, 0x122e: 0x0002,
+	0x123c: 0x0008,
+	// Block 0x49, offset 0x1240
+	0x1249: 0x0008,
+	0x1260: 0x0002, 0x1261: 0x0002, 0x1262: 0x0002, 0x1263: 0x0002,
+	0x1264: 0x0002, 0x1265: 0x0002, 0x1266: 0x0002, 0x1267: 0x0002, 0x1268: 0x0002, 0x1269: 0x0002,
+	0x126a: 0x0002, 0x126b: 0x0002, 0x126c: 0x0002, 0x126d: 0x0002, 0x126e: 0x0002, 0x126f: 0x0002,
+	// Block 0x4a, offset 0x1280
+	0x1290: 0x0004, 0x1291: 0x0004,
+	0x1292: 0x0004, 0x1293: 0x0004, 0x1294: 0x0004, 0x1295: 0x0004, 0x1296: 0x0004, 0x1297: 0x0004,
+	0x1298: 0x0004, 0x1299: 0x0004, 0x129a: 0x0004, 0x129b: 0x0004, 0x129c: 0x0004, 0x129d: 0x0004,
+	0x129e: 0x0004, 0x129f: 0x0004, 0x12a0: 0x0004, 0x12a1: 0x0004, 0x12a2: 0x0004, 0x12a3: 0x0004,
+	0x12a4: 0x0004, 0x12a5: 0x0004, 0x12a6: 0x0004, 0x12a7: 0x0004, 0x12a8: 0x0004, 0x12a9: 0x0004,
+	0x12aa: 0x0004, 0x12ab: 0x0004, 0x12ac: 0x0004, 0x12ad: 0x0004, 0x12ae: 0x0004, 0x12af: 0x0004,
+	0x12b0: 0x0004,
+	// Block 0x4b, offset 0x12c0
+	0x12e2: 0x0008,
+	0x12f9: 0x0008,
+	// Block 0x4c, offset 0x1300
+	0x1314: 0x0008, 0x1315: 0x0008, 0x1316: 0x0008, 0x1317: 0x0008,
+	0x1318: 0x0008, 0x1319: 0x0008,
+	0x1329: 0x0008,
+	0x132a: 0x0008,
+	// Block 0x4d, offset 0x1340
+	0x135a: 0x0008, 0x135b: 0x0008,
+	0x1368: 0x0008,
+	// Block 0x4e, offset 0x1380
+	0x1388: 0x0008,
+	// Block 0x4f, offset 0x13c0
+	0x13cf: 0x0008,
+	0x13e9: 0x0008,
+	0x13ea: 0x0008, 0x13eb: 0x0008, 0x13ec: 0x0008, 0x13ed: 0x0008, 0x13ee: 0x0008, 0x13ef: 0x0008,
+	0x13f0: 0x0008, 0x13f1: 0x0008, 0x13f2: 0x0008, 0x13f3: 0x0008,
+	0x13f8: 0x0008, 0x13f9: 0x0008, 0x13fa: 0x0008,
+	// Block 0x50, offset 0x1400
+	0x1402: 0x0008,
+	// Block 0x51, offset 0x1440
+	0x146a: 0x0008, 0x146b: 0x0008,
+	0x1476: 0x0008,
+	// Block 0x52, offset 0x1480
+	0x1480: 0x0008,
+	0x14bb: 0x0008,
+	0x14bc: 0x0008, 0x14bd: 0x0008, 0x14be: 0x0008,
+	// Block 0x53, offset 0x14c0
+	0x14c0: 0x0008, 0x14c1: 0x0008, 0x14c2: 0x0008, 0x14c3: 0x0008, 0x14c4: 0x0008, 0x14c5: 0x0008,
+	0x14c7: 0x0008, 0x14c8: 0x0008, 0x14c9: 0x0008, 0x14ca: 0x0008, 0x14cb: 0x0008,
+	0x14cc: 0x0008, 0x14cd: 0x0008, 0x14ce: 0x0008, 0x14cf: 0x0008, 0x14d0: 0x0008, 0x14d1: 0x0008,
+	0x14d2: 0x0008, 0x14d4: 0x0008, 0x14d5: 0x0008, 0x14d6: 0x0008, 0x14d7: 0x0008,
+	0x14d8: 0x0008, 0x14d9: 0x0008, 0x14da: 0x0008, 0x14db: 0x0008, 0x14dc: 0x0008, 0x14dd: 0x0008,
+	0x14de: 0x0008, 0x14df: 0x0008, 0x14e0: 0x0008, 0x14e1: 0x0008, 0x14e2: 0x0008, 0x14e3: 0x0008,
+	0x14e4: 0x0008, 0x14e5: 0x0008, 0x14e6: 0x0008, 0x14e7: 0x0008, 0x14e8: 0x0008, 0x14e9: 0x0008,
+	0x14ea: 0x0008, 0x14eb: 0x0008, 0x14ec: 0x0008, 0x14ed: 0x0008, 0x14ee: 0x0008, 0x14ef: 0x0008,
+	0x14f0: 0x0008, 0x14f1: 0x0008, 0x14f2: 0x0008, 0x14f3: 0x0008, 0x14f4: 0x0008, 0x14f5: 0x0008,
+	0x14f6: 0x0008, 0x14f7: 0x0008, 0x14f8: 0x0008, 0x14f9: 0x0008, 0x14fa: 0x0008, 0x14fb: 0x0008,
+	0x14fc: 0x0008, 0x14fd: 0x0008, 0x14fe: 0x0008, 0x14ff: 0x0008,
+	// Block 0x54, offset 0x1500
+	0x1500: 0x0008, 0x1501: 0x0008, 0x1502: 0x0008, 0x1503: 0x0008, 0x1504: 0x0008, 0x1505: 0x0008,
+	0x1506: 0x0008, 0x1507: 0x0008, 0x1508: 0x0008, 0x1509: 0x0008, 0x150a: 0x0008, 0x150b: 0x0008,
+	0x150c: 0x0008, 0x150d: 0x0008, 0x150e: 0x0008, 0x150f: 0x0008, 0x1510: 0x0008, 0x1511: 0x0008,
+	0x1512: 0x0008, 0x1513: 0x0008, 0x1514: 0x0008, 0x1515: 0x0008, 0x1516: 0x0008, 0x1517: 0x0008,
+	0x1518: 0x0008, 0x1519: 0x0008, 0x151a: 0x0008, 0x151b: 0x0008, 0x151c: 0x0008, 0x151d: 0x0008,
+	0x151e: 0x0008, 0x151f: 0x0008, 0x1520: 0x0008, 0x1521: 0x0008, 0x1522: 0x0008, 0x1523: 0x0008,
+	0x1524: 0x0008, 0x1525: 0x0008, 0x1526: 0x0008, 0x1527: 0x0008, 0x1528: 0x0008, 0x1529: 0x0008,
+	0x152a: 0x0008, 0x152b: 0x0008, 0x152c: 0x0008, 0x152d: 0x0008, 0x152e: 0x0008, 0x152f: 0x0008,
+	0x1530: 0x0008, 0x1531: 0x0008, 0x1532: 0x0008, 0x1533: 0x0008, 0x1534: 0x0008, 0x1535: 0x0008,
+	0x1536: 0x0008, 0x1537: 0x0008, 0x1538: 0x0008, 0x1539: 0x0008, 0x153a: 0x0008, 0x153b: 0x0008,
+	0x153c: 0x0008, 0x153d: 0x0008, 0x153e: 0x0008, 0x153f: 0x0008,
+	// Block 0x55, offset 0x1540
+	0x1540: 0x0008, 0x1541: 0x0008, 0x1542: 0x0008, 0x1543: 0x0008, 0x1544: 0x0008, 0x1545: 0x0008,
+	0x1550: 0x0008, 0x1551: 0x0008,
+	0x1552: 0x0008, 0x1553: 0x0008, 0x1554: 0x0008, 0x1555: 0x0008, 0x1556: 0x0008, 0x1557: 0x0008,
+	0x1558: 0x0008, 0x1559: 0x0008, 0x155a: 0x0008, 0x155b: 0x0008, 0x155c: 0x0008, 0x155d: 0x0008,
+	0x155e: 0x0008, 0x155f: 0x0008, 0x1560: 0x0008, 0x1561: 0x0008, 0x1562: 0x0008, 0x1563: 0x0008,
+	0x1564: 0x0008, 0x1565: 0x0008, 0x1566: 0x0008, 0x1567: 0x0008, 0x1568: 0x0008, 0x1569: 0x0008,
+	0x156a: 0x0008, 0x156b: 0x0008, 0x156c: 0x0008, 0x156d: 0x0008, 0x156e: 0x0008, 0x156f: 0x0008,
+	0x1570: 0x0008, 0x1571: 0x0008, 0x1572: 0x0008, 0x1573: 0x0008, 0x1574: 0x0008, 0x1575: 0x0008,
+	0x1576: 0x0008, 0x1577: 0x0008, 0x1578: 0x0008, 0x1579: 0x0008, 0x157a: 0x0008, 0x157b: 0x0008,
+	0x157c: 0x0008, 0x157d: 0x0008, 0x157e: 0x0008, 0x157f: 0x0008,
+	// Block 0x56, offset 0x1580
+	0x1580: 0x0008, 0x1581: 0x0008, 0x1582: 0x0008, 0x1583: 0x0008, 0x1584: 0x0008, 0x1585: 0x0008,
+	0x1588: 0x0008, 0x1589: 0x0008, 0x158a: 0x0008, 0x158b: 0x0008,
+	0x158c: 0x0008, 0x158d: 0x0008, 0x158e: 0x0008, 0x158f: 0x0008, 0x1590: 0x0008, 0x1591: 0x0008,
+	0x1592: 0x0008, 0x1594: 0x0008, 0x1596: 0x0008,
+	0x159d: 0x0008,
+	0x15a1: 0x0008,
+	0x15a8: 0x0008,
+	0x15b3: 0x0008, 0x15b4: 0x0008,
+	// Block 0x57, offset 0x15c0
+	0x15c4: 0x0008,
+	0x15c7: 0x0008,
+	0x15cc: 0x0008, 0x15ce: 0x0008,
+	0x15d3: 0x0008, 0x15d4: 0x0008, 0x15d5: 0x0008, 0x15d7: 0x0008,
+	0x15e3: 0x0008,
+	0x15e4: 0x0008, 0x15e5: 0x0008, 0x15e6: 0x0008, 0x15e7: 0x0008,
+	// Block 0x58, offset 0x1600
+	0x1615: 0x0008, 0x1616: 0x0008, 0x1617: 0x0008,
+	0x1621: 0x0008,
+	0x1630: 0x0008,
+	0x163f: 0x0008,
+	// Block 0x59, offset 0x1640
+	0x1674: 0x0008, 0x1675: 0x0008,
+	// Block 0x5a, offset 0x1680
+	0x1685: 0x0008,
+	0x1686: 0x0008, 0x1687: 0x0008,
+	0x169b: 0x0008, 0x169c: 0x0008,
+	// Block 0x5b, offset 0x16c0
+	0x16d0: 0x0008,
+	0x16d5: 0x0008,
+	// Block 0x5c, offset 0x1700
+	0x172f: 0x0004,
+	0x1730: 0x0004, 0x1731: 0x0004,
+	// Block 0x5d, offset 0x1740
+	0x177f: 0x0004,
+	// Block 0x5e, offset 0x1780
+	0x17a0: 0x0004, 0x17a1: 0x0004, 0x17a2: 0x0004, 0x17a3: 0x0004,
+	0x17a4: 0x0004, 0x17a5: 0x0004, 0x17a6: 0x0004, 0x17a7: 0x0004, 0x17a8: 0x0004, 0x17a9: 0x0004,
+	0x17aa: 0x0004, 0x17ab: 0x0004, 0x17ac: 0x0004, 0x17ad: 0x0004, 0x17ae: 0x0004, 0x17af: 0x0004,
+	0x17b0: 0x0004, 0x17b1: 0x0004, 0x17b2: 0x0004, 0x17b3: 0x0004, 0x17b4: 0x0004, 0x17b5: 0x0004,
+	0x17b6: 0x0004, 0x17b7: 0x0004, 0x17b8: 0x0004, 0x17b9: 0x0004, 0x17ba: 0x0004, 0x17bb: 0x0004,
+	0x17bc: 0x0004, 0x17bd: 0x0004, 0x17be: 0x0004, 0x17bf: 0x0004,
+	// Block 0x5f, offset 0x17c0
+	0x17ea: 0x0004, 0x17eb: 0x0004, 0x17ec: 0x0004, 0x17ed: 0x0004, 0x17ee: 0x0004, 0x17ef: 0x0004,
+	0x17f0: 0x0008,
+	0x17fd: 0x0008,
+	// Block 0x60, offset 0x1800
+	0x1819: 0x0004, 0x181a: 0x0004,
+	// Block 0x61, offset 0x1840
+	0x1857: 0x0008,
+	0x1859: 0x0008,
+	// Block 0x62, offset 0x1880
+	0x18af: 0x0004,
+	0x18b0: 0x0004, 0x18b1: 0x0004, 0x18b2: 0x0004, 0x18b4: 0x0004, 0x18b5: 0x0004,
+	0x18b6: 0x0004, 0x18b7: 0x0004, 0x18b8: 0x0004, 0x18b9: 0x0004, 0x18ba: 0x0004, 0x18bb: 0x0004,
+	0x18bc: 0x0004, 0x18bd: 0x0004,
+	// Block 0x63, offset 0x18c0
+	0x18de: 0x0004, 0x18df: 0x0004,
+	// Block 0x64, offset 0x1900
+	0x1930: 0x0004, 0x1931: 0x0004,
+	// Block 0x65, offset 0x1940
+	0x1942: 0x0004,
+	0x1946: 0x0004, 0x194b: 0x0004,
+	0x1963: 0x0400,
+	0x1964: 0x0400, 0x1965: 0x0004, 0x1966: 0x0004, 0x1967: 0x0400,
+	0x196c: 0x0004,
+	// Block 0x66, offset 0x1980
+	0x1980: 0x0400, 0x1981: 0x0400,
+	0x19b4: 0x0400, 0x19b5: 0x0400,
+	0x19b6: 0x0400, 0x19b7: 0x0400, 0x19b8: 0x0400, 0x19b9: 0x0400, 0x19ba: 0x0400, 0x19bb: 0x0400,
+	0x19bc: 0x0400, 0x19bd: 0x0400, 0x19be: 0x0400, 0x19bf: 0x0400,
+	// Block 0x67, offset 0x19c0
+	0x19c0: 0x0400, 0x19c1: 0x0400, 0x19c2: 0x0400, 0x19c3: 0x0400, 0x19c4: 0x0004, 0x19c5: 0x0004,
+	0x19e0: 0x0004, 0x19e1: 0x0004, 0x19e2: 0x0004, 0x19e3: 0x0004,
+	0x19e4: 0x0004, 0x19e5: 0x0004, 0x19e6: 0x0004, 0x19e7: 0x0004, 0x19e8: 0x0004, 0x19e9: 0x0004,
+	0x19ea: 0x0004, 0x19eb: 0x0004, 0x19ec: 0x0004, 0x19ed: 0x0004, 0x19ee: 0x0004, 0x19ef: 0x0004,
+	0x19f0: 0x0004, 0x19f1: 0x0004,
+	0x19ff: 0x0004,
+	// Block 0x68, offset 0x1a00
+	0x1a26: 0x0004, 0x1a27: 0x0004, 0x1a28: 0x0004, 0x1a29: 0x0004,
+	0x1a2a: 0x0004, 0x1a2b: 0x0004, 0x1a2c: 0x0004, 0x1a2d: 0x0004,
+	// Block 0x69, offset 0x1a40
+	0x1a47: 0x0004, 0x1a48: 0x0004, 0x1a49: 0x0004, 0x1a4a: 0x0004, 0x1a4b: 0x0004,
+	0x1a4c: 0x0004, 0x1a4d: 0x0004, 0x1a4e: 0x0004, 0x1a4f: 0x0004, 0x1a50: 0x0004, 0x1a51: 0x0004,
+	0x1a52: 0x0400, 0x1a53: 0x0400,
+	0x1a60: 0x0010, 0x1a61: 0x0010, 0x1a62: 0x0010, 0x1a63: 0x0010,
+	0x1a64: 0x0010, 0x1a65: 0x0010, 0x1a66: 0x0010, 0x1a67: 0x0010, 0x1a68: 0x0010, 0x1a69: 0x0010,
+	0x1a6a: 0x0010, 0x1a6b: 0x0010, 0x1a6c: 0x0010, 0x1a6d: 0x0010, 0x1a6e: 0x0010, 0x1a6f: 0x0010,
+	0x1a70: 0x0010, 0x1a71: 0x0010, 0x1a72: 0x0010, 0x1a73: 0x0010, 0x1a74: 0x0010, 0x1a75: 0x0010,
+	0x1a76: 0x0010, 0x1a77: 0x0010, 0x1a78: 0x0010, 0x1a79: 0x0010, 0x1a7a: 0x0010, 0x1a7b: 0x0010,
+	0x1a7c: 0x0010,
+	// Block 0x6a, offset 0x1a80
+	0x1a80: 0x0004, 0x1a81: 0x0004, 0x1a82: 0x0004, 0x1a83: 0x0400,
+	0x1ab3: 0x0004, 0x1ab4: 0x0400, 0x1ab5: 0x0400,
+	0x1ab6: 0x0004, 0x1ab7: 0x0004, 0x1ab8: 0x0004, 0x1ab9: 0x0004, 0x1aba: 0x0400, 0x1abb: 0x0400,
+	0x1abc: 0x0004, 0x1abd: 0x0004, 0x1abe: 0x0400, 0x1abf: 0x0400,
+	// Block 0x6b, offset 0x1ac0
+	0x1ac0: 0x0400,
+	0x1ae5: 0x0004,
+	// Block 0x6c, offset 0x1b00
+	0x1b29: 0x0004,
+	0x1b2a: 0x0004, 0x1b2b: 0x0004, 0x1b2c: 0x0004, 0x1b2d: 0x0004, 0x1b2e: 0x0004, 0x1b2f: 0x0400,
+	0x1b30: 0x0400, 0x1b31: 0x0004, 0x1b32: 0x0004, 0x1b33: 0x0400, 0x1b34: 0x0400, 0x1b35: 0x0004,
+	0x1b36: 0x0004,
+	// Block 0x6d, offset 0x1b40
+	0x1b43: 0x0004,
+	0x1b4c: 0x0004, 0x1b4d: 0x0400,
+	0x1b7c: 0x0004,
+	// Block 0x6e, offset 0x1b80
+	0x1bb0: 0x0004, 0x1bb2: 0x0004, 0x1bb3: 0x0004, 0x1bb4: 0x0004,
+	0x1bb7: 0x0004, 0x1bb8: 0x0004,
+	0x1bbe: 0x0004, 0x1bbf: 0x0004,
+	// Block 0x6f, offset 0x1bc0
+	0x1bc1: 0x0004,
+	0x1beb: 0x0400, 0x1bec: 0x0004, 0x1bed: 0x0004, 0x1bee: 0x0400, 0x1bef: 0x0400,
+	0x1bf5: 0x0400,
+	0x1bf6: 0x0004,
+	// Block 0x70, offset 0x1c00
+	0x1c23: 0x0400,
+	0x1c24: 0x0400, 0x1c25: 0x0004, 0x1c26: 0x0400, 0x1c27: 0x0400, 0x1c28: 0x0004, 0x1c29: 0x0400,
+	0x1c2a: 0x0400, 0x1c2c: 0x0400, 0x1c2d: 0x0004,
+	// Block 0x71, offset 0x1c40
+	0x1c40: 0x0040, 0x1c41: 0x0080, 0x1c42: 0x0080, 0x1c43: 0x0080, 0x1c44: 0x0080, 0x1c45: 0x0080,
+	0x1c46: 0x0080, 0x1c47: 0x0080, 0x1c48: 0x0080, 0x1c49: 0x0080, 0x1c4a: 0x0080, 0x1c4b: 0x0080,
+	0x1c4c: 0x0080, 0x1c4d: 0x0080, 0x1c4e: 0x0080, 0x1c4f: 0x0080, 0x1c50: 0x0080, 0x1c51: 0x0080,
+	0x1c52: 0x0080, 0x1c53: 0x0080, 0x1c54: 0x0080, 0x1c55: 0x0080, 0x1c56: 0x0080, 0x1c57: 0x0080,
+	0x1c58: 0x0080, 0x1c59: 0x0080, 0x1c5a: 0x0080, 0x1c5b: 0x0080, 0x1c5c: 0x0040, 0x1c5d: 0x0080,
+	0x1c5e: 0x0080, 0x1c5f: 0x0080, 0x1c60: 0x0080, 0x1c61: 0x0080, 0x1c62: 0x0080, 0x1c63: 0x0080,
+	0x1c64: 0x0080, 0x1c65: 0x0080, 0x1c66: 0x0080, 0x1c67: 0x0080, 0x1c68: 0x0080, 0x1c69: 0x0080,
+	0x1c6a: 0x0080, 0x1c6b: 0x0080, 0x1c6c: 0x0080, 0x1c6d: 0x0080, 0x1c6e: 0x0080, 0x1c6f: 0x0080,
+	0x1c70: 0x0080, 0x1c71: 0x0080, 0x1c72: 0x0080, 0x1c73: 0x0080, 0x1c74: 0x0080, 0x1c75: 0x0080,
+	0x1c76: 0x0080, 0x1c77: 0x0080, 0x1c78: 0x0040, 0x1c79: 0x0080, 0x1c7a: 0x0080, 0x1c7b: 0x0080,
+	0x1c7c: 0x0080, 0x1c7d: 0x0080, 0x1c7e: 0x0080, 0x1c7f: 0x0080,
+	// Block 0x72, offset 0x1c80
+	0x1c80: 0x0080, 0x1c81: 0x0080, 0x1c82: 0x0080, 0x1c83: 0x0080, 0x1c84: 0x0080, 0x1c85: 0x0080,
+	0x1c86: 0x0080, 0x1c87: 0x0080, 0x1c88: 0x0080, 0x1c89: 0x0080, 0x1c8a: 0x0080, 0x1c8b: 0x0080,
+	0x1c8c: 0x0080, 0x1c8d: 0x0080, 0x1c8e: 0x0080, 0x1c8f: 0x0080, 0x1c90: 0x0080, 0x1c91: 0x0080,
+	0x1c92: 0x0080, 0x1c93: 0x0080, 0x1c94: 0x0040, 0x1c95: 0x0080, 0x1c96: 0x0080, 0x1c97: 0x0080,
+	0x1c98: 0x0080, 0x1c99: 0x0080, 0x1c9a: 0x0080, 0x1c9b: 0x0080, 0x1c9c: 0x0080, 0x1c9d: 0x0080,
+	0x1c9e: 0x0080, 0x1c9f: 0x0080, 0x1ca0: 0x0080, 0x1ca1: 0x0080, 0x1ca2: 0x0080, 0x1ca3: 0x0080,
+	0x1ca4: 0x0080, 0x1ca5: 0x0080, 0x1ca6: 0x0080, 0x1ca7: 0x0080, 0x1ca8: 0x0080, 0x1ca9: 0x0080,
+	0x1caa: 0x0080, 0x1cab: 0x0080, 0x1cac: 0x0080, 0x1cad: 0x0080, 0x1cae: 0x0080, 0x1caf: 0x0080,
+	0x1cb0: 0x0040, 0x1cb1: 0x0080, 0x1cb2: 0x0080, 0x1cb3: 0x0080, 0x1cb4: 0x0080, 0x1cb5: 0x0080,
+	0x1cb6: 0x0080, 0x1cb7: 0x0080, 0x1cb8: 0x0080, 0x1cb9: 0x0080, 0x1cba: 0x0080, 0x1cbb: 0x0080,
+	0x1cbc: 0x0080, 0x1cbd: 0x0080, 0x1cbe: 0x0080, 0x1cbf: 0x0080,
+	// Block 0x73, offset 0x1cc0
+	0x1cc0: 0x0080, 0x1cc1: 0x0080, 0x1cc2: 0x0080, 0x1cc3: 0x0080, 0x1cc4: 0x0080, 0x1cc5: 0x0080,
+	0x1cc6: 0x0080, 0x1cc7: 0x0080, 0x1cc8: 0x0080, 0x1cc9: 0x0080, 0x1cca: 0x0080, 0x1ccb: 0x0080,
+	0x1ccc: 0x0040, 0x1ccd: 0x0080, 0x1cce: 0x0080, 0x1ccf: 0x0080, 0x1cd0: 0x0080, 0x1cd1: 0x0080,
+	0x1cd2: 0x0080, 0x1cd3: 0x0080, 0x1cd4: 0x0080, 0x1cd5: 0x0080, 0x1cd6: 0x0080, 0x1cd7: 0x0080,
+	0x1cd8: 0x0080, 0x1cd9: 0x0080, 0x1cda: 0x0080, 0x1cdb: 0x0080, 0x1cdc: 0x0080, 0x1cdd: 0x0080,
+	0x1cde: 0x0080, 0x1cdf: 0x0080, 0x1ce0: 0x0080, 0x1ce1: 0x0080, 0x1ce2: 0x0080, 0x1ce3: 0x0080,
+	0x1ce4: 0x0080, 0x1ce5: 0x0080, 0x1ce6: 0x0080, 0x1ce7: 0x0080, 0x1ce8: 0x0040, 0x1ce9: 0x0080,
+	0x1cea: 0x0080, 0x1ceb: 0x0080, 0x1cec: 0x0080, 0x1ced: 0x0080, 0x1cee: 0x0080, 0x1cef: 0x0080,
+	0x1cf0: 0x0080, 0x1cf1: 0x0080, 0x1cf2: 0x0080, 0x1cf3: 0x0080, 0x1cf4: 0x0080, 0x1cf5: 0x0080,
+	0x1cf6: 0x0080, 0x1cf7: 0x0080, 0x1cf8: 0x0080, 0x1cf9: 0x0080, 0x1cfa: 0x0080, 0x1cfb: 0x0080,
+	0x1cfc: 0x0080, 0x1cfd: 0x0080, 0x1cfe: 0x0080, 0x1cff: 0x0080,
+	// Block 0x74, offset 0x1d00
+	0x1d00: 0x0080, 0x1d01: 0x0080, 0x1d02: 0x0080, 0x1d03: 0x0080, 0x1d04: 0x0040, 0x1d05: 0x0080,
+	0x1d06: 0x0080, 0x1d07: 0x0080, 0x1d08: 0x0080, 0x1d09: 0x0080, 0x1d0a: 0x0080, 0x1d0b: 0x0080,
+	0x1d0c: 0x0080, 0x1d0d: 0x0080, 0x1d0e: 0x0080, 0x1d0f: 0x0080, 0x1d10: 0x0080, 0x1d11: 0x0080,
+	0x1d12: 0x0080, 0x1d13: 0x0080, 0x1d14: 0x0080, 0x1d15: 0x0080, 0x1d16: 0x0080, 0x1d17: 0x0080,
+	0x1d18: 0x0080, 0x1d19: 0x0080, 0x1d1a: 0x0080, 0x1d1b: 0x0080, 0x1d1c: 0x0080, 0x1d1d: 0x0080,
+	0x1d1e: 0x0080, 0x1d1f: 0x0080, 0x1d20: 0x0040, 0x1d21: 0x0080, 0x1d22: 0x0080, 0x1d23: 0x0080,
+	0x1d24: 0x0080, 0x1d25: 0x0080, 0x1d26: 0x0080, 0x1d27: 0x0080, 0x1d28: 0x0080, 0x1d29: 0x0080,
+	0x1d2a: 0x0080, 0x1d2b: 0x0080, 0x1d2c: 0x0080, 0x1d2d: 0x0080, 0x1d2e: 0x0080, 0x1d2f: 0x0080,
+	0x1d30: 0x0080, 0x1d31: 0x0080, 0x1d32: 0x0080, 0x1d33: 0x0080, 0x1d34: 0x0080, 0x1d35: 0x0080,
+	0x1d36: 0x0080, 0x1d37: 0x0080, 0x1d38: 0x0080, 0x1d39: 0x0080, 0x1d3a: 0x0080, 0x1d3b: 0x0080,
+	0x1d3c: 0x0040, 0x1d3d: 0x0080, 0x1d3e: 0x0080, 0x1d3f: 0x0080,
+	// Block 0x75, offset 0x1d40
+	0x1d40: 0x0080, 0x1d41: 0x0080, 0x1d42: 0x0080, 0x1d43: 0x0080, 0x1d44: 0x0080, 0x1d45: 0x0080,
+	0x1d46: 0x0080, 0x1d47: 0x0080, 0x1d48: 0x0080, 0x1d49: 0x0080, 0x1d4a: 0x0080, 0x1d4b: 0x0080,
+	0x1d4c: 0x0080, 0x1d4d: 0x0080, 0x1d4e: 0x0080, 0x1d4f: 0x0080, 0x1d50: 0x0080, 0x1d51: 0x0080,
+	0x1d52: 0x0080, 0x1d53: 0x0080, 0x1d54: 0x0080, 0x1d55: 0x0080, 0x1d56: 0x0080, 0x1d57: 0x0080,
+	0x1d58: 0x0040, 0x1d59: 0x0080, 0x1d5a: 0x0080, 0x1d5b: 0x0080, 0x1d5c: 0x0080, 0x1d5d: 0x0080,
+	0x1d5e: 0x0080, 0x1d5f: 0x0080, 0x1d60: 0x0080, 0x1d61: 0x0080, 0x1d62: 0x0080, 0x1d63: 0x0080,
+	0x1d64: 0x0080, 0x1d65: 0x0080, 0x1d66: 0x0080, 0x1d67: 0x0080, 0x1d68: 0x0080, 0x1d69: 0x0080,
+	0x1d6a: 0x0080, 0x1d6b: 0x0080, 0x1d6c: 0x0080, 0x1d6d: 0x0080, 0x1d6e: 0x0080, 0x1d6f: 0x0080,
+	0x1d70: 0x0080, 0x1d71: 0x0080, 0x1d72: 0x0080, 0x1d73: 0x0080, 0x1d74: 0x0040, 0x1d75: 0x0080,
+	0x1d76: 0x0080, 0x1d77: 0x0080, 0x1d78: 0x0080, 0x1d79: 0x0080, 0x1d7a: 0x0080, 0x1d7b: 0x0080,
+	0x1d7c: 0x0080, 0x1d7d: 0x0080, 0x1d7e: 0x0080, 0x1d7f: 0x0080,
+	// Block 0x76, offset 0x1d80
+	0x1d80: 0x0080, 0x1d81: 0x0080, 0x1d82: 0x0080, 0x1d83: 0x0080, 0x1d84: 0x0080, 0x1d85: 0x0080,
+	0x1d86: 0x0080, 0x1d87: 0x0080, 0x1d88: 0x0080, 0x1d89: 0x0080, 0x1d8a: 0x0080, 0x1d8b: 0x0080,
+	0x1d8c: 0x0080, 0x1d8d: 0x0080, 0x1d8e: 0x0080, 0x1d8f: 0x0080, 0x1d90: 0x0040, 0x1d91: 0x0080,
+	0x1d92: 0x0080, 0x1d93: 0x0080, 0x1d94: 0x0080, 0x1d95: 0x0080, 0x1d96: 0x0080, 0x1d97: 0x0080,
+	0x1d98: 0x0080, 0x1d99: 0x0080, 0x1d9a: 0x0080, 0x1d9b: 0x0080, 0x1d9c: 0x0080, 0x1d9d: 0x0080,
+	0x1d9e: 0x0080, 0x1d9f: 0x0080, 0x1da0: 0x0080, 0x1da1: 0x0080, 0x1da2: 0x0080, 0x1da3: 0x0080,
+	0x1da4: 0x0080, 0x1da5: 0x0080, 0x1da6: 0x0080, 0x1da7: 0x0080, 0x1da8: 0x0080, 0x1da9: 0x0080,
+	0x1daa: 0x0080, 0x1dab: 0x0080, 0x1dac: 0x0040, 0x1dad: 0x0080, 0x1dae: 0x0080, 0x1daf: 0x0080,
+	0x1db0: 0x0080, 0x1db1: 0x0080, 0x1db2: 0x0080, 0x1db3: 0x0080, 0x1db4: 0x0080, 0x1db5: 0x0080,
+	0x1db6: 0x0080, 0x1db7: 0x0080, 0x1db8: 0x0080, 0x1db9: 0x0080, 0x1dba: 0x0080, 0x1dbb: 0x0080,
+	0x1dbc: 0x0080, 0x1dbd: 0x0080, 0x1dbe: 0x0080, 0x1dbf: 0x0080,
+	// Block 0x77, offset 0x1dc0
+	0x1dc0: 0x0080, 0x1dc1: 0x0080, 0x1dc2: 0x0080, 0x1dc3: 0x0080, 0x1dc4: 0x0080, 0x1dc5: 0x0080,
+	0x1dc6: 0x0080, 0x1dc7: 0x0080, 0x1dc8: 0x0040, 0x1dc9: 0x0080, 0x1dca: 0x0080, 0x1dcb: 0x0080,
+	0x1dcc: 0x0080, 0x1dcd: 0x0080, 0x1dce: 0x0080, 0x1dcf: 0x0080, 0x1dd0: 0x0080, 0x1dd1: 0x0080,
+	0x1dd2: 0x0080, 0x1dd3: 0x0080, 0x1dd4: 0x0080, 0x1dd5: 0x0080, 0x1dd6: 0x0080, 0x1dd7: 0x0080,
+	0x1dd8: 0x0080, 0x1dd9: 0x0080, 0x1dda: 0x0080, 0x1ddb: 0x0080, 0x1ddc: 0x0080, 0x1ddd: 0x0080,
+	0x1dde: 0x0080, 0x1ddf: 0x0080, 0x1de0: 0x0080, 0x1de1: 0x0080, 0x1de2: 0x0080, 0x1de3: 0x0080,
+	0x1de4: 0x0040, 0x1de5: 0x0080, 0x1de6: 0x0080, 0x1de7: 0x0080, 0x1de8: 0x0080, 0x1de9: 0x0080,
+	0x1dea: 0x0080, 0x1deb: 0x0080, 0x1dec: 0x0080, 0x1ded: 0x0080, 0x1dee: 0x0080, 0x1def: 0x0080,
+	0x1df0: 0x0080, 0x1df1: 0x0080, 0x1df2: 0x0080, 0x1df3: 0x0080, 0x1df4: 0x0080, 0x1df5: 0x0080,
+	0x1df6: 0x0080, 0x1df7: 0x0080, 0x1df8: 0x0080, 0x1df9: 0x0080, 0x1dfa: 0x0080, 0x1dfb: 0x0080,
+	0x1dfc: 0x0080, 0x1dfd: 0x0080, 0x1dfe: 0x0080, 0x1dff: 0x0080,
+	// Block 0x78, offset 0x1e00
+	0x1e00: 0x0080, 0x1e01: 0x0080, 0x1e02: 0x0080, 0x1e03: 0x0080, 0x1e04: 0x0080, 0x1e05: 0x0080,
+	0x1e06: 0x0080, 0x1e07: 0x0080, 0x1e08: 0x0040, 0x1e09: 0x0080, 0x1e0a: 0x0080, 0x1e0b: 0x0080,
+	0x1e0c: 0x0080, 0x1e0d: 0x0080, 0x1e0e: 0x0080, 0x1e0f: 0x0080, 0x1e10: 0x0080, 0x1e11: 0x0080,
+	0x1e12: 0x0080, 0x1e13: 0x0080, 0x1e14: 0x0080, 0x1e15: 0x0080, 0x1e16: 0x0080, 0x1e17: 0x0080,
+	0x1e18: 0x0080, 0x1e19: 0x0080, 0x1e1a: 0x0080, 0x1e1b: 0x0080, 0x1e1c: 0x0080, 0x1e1d: 0x0080,
+	0x1e1e: 0x0080, 0x1e1f: 0x0080, 0x1e20: 0x0080, 0x1e21: 0x0080, 0x1e22: 0x0080, 0x1e23: 0x0080,
+	0x1e30: 0x1000, 0x1e31: 0x1000, 0x1e32: 0x1000, 0x1e33: 0x1000, 0x1e34: 0x1000, 0x1e35: 0x1000,
+	0x1e36: 0x1000, 0x1e37: 0x1000, 0x1e38: 0x1000, 0x1e39: 0x1000, 0x1e3a: 0x1000, 0x1e3b: 0x1000,
+	0x1e3c: 0x1000, 0x1e3d: 0x1000, 0x1e3e: 0x1000, 0x1e3f: 0x1000,
+	// Block 0x79, offset 0x1e40
+	0x1e40: 0x1000, 0x1e41: 0x1000, 0x1e42: 0x1000, 0x1e43: 0x1000, 0x1e44: 0x1000, 0x1e45: 0x1000,
+	0x1e46: 0x1000, 0x1e4b: 0x0800,
+	0x1e4c: 0x0800, 0x1e4d: 0x0800, 0x1e4e: 0x0800, 0x1e4f: 0x0800, 0x1e50: 0x0800, 0x1e51: 0x0800,
+	0x1e52: 0x0800, 0x1e53: 0x0800, 0x1e54: 0x0800, 0x1e55: 0x0800, 0x1e56: 0x0800, 0x1e57: 0x0800,
+	0x1e58: 0x0800, 0x1e59: 0x0800, 0x1e5a: 0x0800, 0x1e5b: 0x0800, 0x1e5c: 0x0800, 0x1e5d: 0x0800,
+	0x1e5e: 0x0800, 0x1e5f: 0x0800, 0x1e60: 0x0800, 0x1e61: 0x0800, 0x1e62: 0x0800, 0x1e63: 0x0800,
+	0x1e64: 0x0800, 0x1e65: 0x0800, 0x1e66: 0x0800, 0x1e67: 0x0800, 0x1e68: 0x0800, 0x1e69: 0x0800,
+	0x1e6a: 0x0800, 0x1e6b: 0x0800, 0x1e6c: 0x0800, 0x1e6d: 0x0800, 0x1e6e: 0x0800, 0x1e6f: 0x0800,
+	0x1e70: 0x0800, 0x1e71: 0x0800, 0x1e72: 0x0800, 0x1e73: 0x0800, 0x1e74: 0x0800, 0x1e75: 0x0800,
+	0x1e76: 0x0800, 0x1e77: 0x0800, 0x1e78: 0x0800, 0x1e79: 0x0800, 0x1e7a: 0x0800, 0x1e7b: 0x0800,
+	// Block 0x7a, offset 0x1e80
+	0x1e9e: 0x0004,
+	// Block 0x7b, offset 0x1ec0
+	0x1ec0: 0x0004, 0x1ec1: 0x0004, 0x1ec2: 0x0004, 0x1ec3: 0x0004, 0x1ec4: 0x0004, 0x1ec5: 0x0004,
+	0x1ec6: 0x0004, 0x1ec7: 0x0004, 0x1ec8: 0x0004, 0x1ec9: 0x0004, 0x1eca: 0x0004, 0x1ecb: 0x0004,
+	0x1ecc: 0x0004, 0x1ecd: 0x0004, 0x1ece: 0x0004, 0x1ecf: 0x0004,
+	0x1ee0: 0x0004, 0x1ee1: 0x0004, 0x1ee2: 0x0004, 0x1ee3: 0x0004,
+	0x1ee4: 0x0004, 0x1ee5: 0x0004, 0x1ee6: 0x0004, 0x1ee7: 0x0004, 0x1ee8: 0x0004, 0x1ee9: 0x0004,
+	0x1eea: 0x0004, 0x1eeb: 0x0004, 0x1eec: 0x0004, 0x1eed: 0x0004, 0x1eee: 0x0004, 0x1eef: 0x0004,
+	// Block 0x7c, offset 0x1f00
+	0x1f3f: 0x0002,
+	// Block 0x7d, offset 0x1f40
+	0x1f70: 0x0002, 0x1f71: 0x0002, 0x1f72: 0x0002, 0x1f73: 0x0002, 0x1f74: 0x0002, 0x1f75: 0x0002,
+	0x1f76: 0x0002, 0x1f77: 0x0002, 0x1f78: 0x0002, 0x1f79: 0x0002, 0x1f7a: 0x0002, 0x1f7b: 0x0002,
+	// Block 0x7e, offset 0x1f80
+	0x1fbd: 0x0004,
+	// Block 0x7f, offset 0x1fc0
+	0x1fe0: 0x0004,
+	// Block 0x80, offset 0x2000
+	0x2036: 0x0004, 0x2037: 0x0004, 0x2038: 0x0004, 0x2039: 0x0004, 0x203a: 0x0004,
+	// Block 0x81, offset 0x2040
+	0x2041: 0x0004, 0x2042: 0x0004, 0x2043: 0x0004, 0x2045: 0x0004,
+	0x2046: 0x0004,
+	0x204c: 0x0004, 0x204d: 0x0004, 0x204e: 0x0004, 0x204f: 0x0004,
+	0x2078: 0x0004, 0x2079: 0x0004, 0x207a: 0x0004,
+	0x207f: 0x0004,
+	// Block 0x82, offset 0x2080
+	0x20a5: 0x0004, 0x20a6: 0x0004,
+	// Block 0x83, offset 0x20c0
+	0x20e4: 0x0004, 0x20e5: 0x0004, 0x20e6: 0x0004, 0x20e7: 0x0004,
+	// Block 0x84, offset 0x2100
+	0x212b: 0x0004, 0x212c: 0x0004,
+	// Block 0x85, offset 0x2140
+	0x217d: 0x0004, 0x217e: 0x0004, 0x217f: 0x0004,
+	// Block 0x86, offset 0x2180
+	0x2186: 0x0004, 0x2187: 0x0004, 0x2188: 0x0004, 0x2189: 0x0004, 0x218a: 0x0004, 0x218b: 0x0004,
+	0x218c: 0x0004, 0x218d: 0x0004, 0x218e: 0x0004, 0x218f: 0x0004, 0x2190: 0x0004,
+	// Block 0x87, offset 0x21c0
+	0x21c2: 0x0004, 0x21c3: 0x0004, 0x21c4: 0x0004, 0x21c5: 0x0004,
+	// Block 0x88, offset 0x2200
+	0x2200: 0x0400, 0x2201: 0x0004, 0x2202: 0x0400,
+	0x2238: 0x0004, 0x2239: 0x0004, 0x223a: 0x0004, 0x223b: 0x0004,
+	0x223c: 0x0004, 0x223d: 0x0004, 0x223e: 0x0004, 0x223f: 0x0004,
+	// Block 0x89, offset 0x2240
+	0x2240: 0x0004, 0x2241: 0x0004, 0x2242: 0x0004, 0x2243: 0x0004, 0x2244: 0x0004, 0x2245: 0x0004,
+	0x2246: 0x0004,
+	0x2270: 0x0004, 0x2273: 0x0004, 0x2274: 0x0004,
+	0x227f: 0x0004,
+	// Block 0x8a, offset 0x2280
+	0x2280: 0x0004, 0x2281: 0x0004, 0x2282: 0x0400,
+	0x22b0: 0x0400, 0x22b1: 0x0400, 0x22b2: 0x0400, 0x22b3: 0x0004, 0x22b4: 0x0004, 0x22b5: 0x0004,
+	0x22b6: 0x0004, 0x22b7: 0x0400, 0x22b8: 0x0400, 0x22b9: 0x0004, 0x22ba: 0x0004,
+	0x22bd: 0x0100,
+	// Block 0x8b, offset 0x22c0
+	0x22c2: 0x0004,
+	0x22cd: 0x0100,
+	// Block 0x8c, offset 0x2300
+	0x2300: 0x0004, 0x2301: 0x0004, 0x2302: 0x0004,
+	0x2327: 0x0004, 0x2328: 0x0004, 0x2329: 0x0004,
+	0x232a: 0x0004, 0x232b: 0x0004, 0x232c: 0x0400, 0x232d: 0x0004, 0x232e: 0x0004, 0x232f: 0x0004,
+	0x2330: 0x0004, 0x2331: 0x0004, 0x2332: 0x0004, 0x2333: 0x0004, 0x2334: 0x0004,
+	// Block 0x8d, offset 0x2340
+	0x2345: 0x0400,
+	0x2346: 0x0400,
+	0x2373: 0x0004,
+	// Block 0x8e, offset 0x2380
+	0x2380: 0x0004, 0x2381: 0x0004, 0x2382: 0x0400,
+	0x23b3: 0x0400, 0x23b4: 0x0400, 0x23b5: 0x0400,
+	0x23b6: 0x0004, 0x23b7: 0x0004, 0x23b8: 0x0004, 0x23b9: 0x0004, 0x23ba: 0x0004, 0x23bb: 0x0004,
+	0x23bc: 0x0004, 0x23bd: 0x0004, 0x23be: 0x0004, 0x23bf: 0x0400,
+	// Block 0x8f, offset 0x23c0
+	0x23c0: 0x0400, 0x23c2: 0x0100, 0x23c3: 0x0100,
+	0x23c9: 0x0004, 0x23ca: 0x0004, 0x23cb: 0x0004,
+	0x23cc: 0x0004, 0x23ce: 0x0400, 0x23cf: 0x0004,
+	// Block 0x90, offset 0x2400
+	0x242c: 0x0400, 0x242d: 0x0400, 0x242e: 0x0400, 0x242f: 0x0004,
+	0x2430: 0x0004, 0x2431: 0x0004, 0x2432: 0x0400, 0x2433: 0x0400, 0x2434: 0x0004, 0x2435: 0x0400,
+	0x2436: 0x0004, 0x2437: 0x0004,
+	0x243e: 0x0004,
+	// Block 0x91, offset 0x2440
+	0x2441: 0x0004,
+	// Block 0x92, offset 0x2480
+	0x249f: 0x0004, 0x24a0: 0x0400, 0x24a1: 0x0400, 0x24a2: 0x0400, 0x24a3: 0x0004,
+	0x24a4: 0x0004, 0x24a5: 0x0004, 0x24a6: 0x0004, 0x24a7: 0x0004, 0x24a8: 0x0004, 0x24a9: 0x0004,
+	0x24aa: 0x0004,
+	// Block 0x93, offset 0x24c0
+	0x24c0: 0x0004, 0x24c1: 0x0400, 0x24c2: 0x0400, 0x24c3: 0x0400, 0x24c4: 0x0400,
+	0x24c7: 0x0400, 0x24c8: 0x0400, 0x24cb: 0x0400,
+	0x24cc: 0x0400, 0x24cd: 0x0400,
+	0x24d7: 0x0004,
+	0x24e2: 0x0400, 0x24e3: 0x0400,
+	0x24e6: 0x0004, 0x24e7: 0x0004, 0x24e8: 0x0004, 0x24e9: 0x0004,
+	0x24ea: 0x0004, 0x24eb: 0x0004, 0x24ec: 0x0004,
+	0x24f0: 0x0004, 0x24f1: 0x0004, 0x24f2: 0x0004, 0x24f3: 0x0004, 0x24f4: 0x0004,
+	// Block 0x94, offset 0x2500
+	0x2535: 0x0400,
+	0x2536: 0x0400, 0x2537: 0x0400, 0x2538: 0x0004, 0x2539: 0x0004, 0x253a: 0x0004, 0x253b: 0x0004,
+	0x253c: 0x0004, 0x253d: 0x0004, 0x253e: 0x0004, 0x253f: 0x0004,
+	// Block 0x95, offset 0x2540
+	0x2540: 0x0400, 0x2541: 0x0400, 0x2542: 0x0004, 0x2543: 0x0004, 0x2544: 0x0004, 0x2545: 0x0400,
+	0x2546: 0x0004,
+	0x255e: 0x0004,
+	// Block 0x96, offset 0x2580
+	0x25b0: 0x0004, 0x25b1: 0x0400, 0x25b2: 0x0400, 0x25b3: 0x0004, 0x25b4: 0x0004, 0x25b5: 0x0004,
+	0x25b6: 0x0004, 0x25b7: 0x0004, 0x25b8: 0x0004, 0x25b9: 0x0400, 0x25ba: 0x0004, 0x25bb: 0x0400,
+	0x25bc: 0x0400, 0x25bd: 0x0004, 0x25be: 0x0400, 0x25bf: 0x0004,
+	// Block 0x97, offset 0x25c0
+	0x25c0: 0x0004, 0x25c1: 0x0400, 0x25c2: 0x0004, 0x25c3: 0x0004,
+	// Block 0x98, offset 0x2600
+	0x262f: 0x0004,
+	0x2630: 0x0400, 0x2631: 0x0400, 0x2632: 0x0004, 0x2633: 0x0004, 0x2634: 0x0004, 0x2635: 0x0004,
+	0x2638: 0x0400, 0x2639: 0x0400, 0x263a: 0x0400, 0x263b: 0x0400,
+	0x263c: 0x0004, 0x263d: 0x0004, 0x263e: 0x0400, 0x263f: 0x0004,
+	// Block 0x99, offset 0x2640
+	0x2640: 0x0004,
+	0x265c: 0x0004, 0x265d: 0x0004,
+	// Block 0x9a, offset 0x2680
+	0x26b0: 0x0400, 0x26b1: 0x0400, 0x26b2: 0x0400, 0x26b3: 0x0004, 0x26b4: 0x0004, 0x26b5: 0x0004,
+	0x26b6: 0x0004, 0x26b7: 0x0004, 0x26b8: 0x0004, 0x26b9: 0x0004, 0x26ba: 0x0004, 0x26bb: 0x0400,
+	0x26bc: 0x0400, 0x26bd: 0x0004, 0x26be: 0x0400, 0x26bf: 0x0004,
+	// Block 0x9b, offset 0x26c0
+	0x26c0: 0x0004,
+	// Block 0x9c, offset 0x2700
+	0x272b: 0x0004, 0x272c: 0x0400, 0x272d: 0x0004, 0x272e: 0x0400, 0x272f: 0x0400,
+	0x2730: 0x0004, 0x2731: 0x0004, 0x2732: 0x0004, 0x2733: 0x0004, 0x2734: 0x0004, 0x2735: 0x0004,
+	0x2736: 0x0400, 0x2737: 0x0004,
+	// Block 0x9d, offset 0x2740
+	0x275d: 0x0004,
+	0x275e: 0x0004, 0x275f: 0x0004, 0x2762: 0x0004, 0x2763: 0x0004,
+	0x2764: 0x0004, 0x2765: 0x0004, 0x2766: 0x0400, 0x2767: 0x0004, 0x2768: 0x0004, 0x2769: 0x0004,
+	0x276a: 0x0004, 0x276b: 0x0004,
+	// Block 0x9e, offset 0x2780
+	0x27ac: 0x0400, 0x27ad: 0x0400, 0x27ae: 0x0400, 0x27af: 0x0004,
+	0x27b0: 0x0004, 0x27b1: 0x0004, 0x27b2: 0x0004, 0x27b3: 0x0004, 0x27b4: 0x0004, 0x27b5: 0x0004,
+	0x27b6: 0x0004, 0x27b7: 0x0004, 0x27b8: 0x0400, 0x27b9: 0x0004, 0x27ba: 0x0004,
+	// Block 0x9f, offset 0x27c0
+	0x27f0: 0x0004, 0x27f1: 0x0400, 0x27f2: 0x0400, 0x27f3: 0x0400, 0x27f4: 0x0400, 0x27f5: 0x0400,
+	0x27f7: 0x0400, 0x27f8: 0x0400, 0x27fb: 0x0004,
+	0x27fc: 0x0004, 0x27fd: 0x0400, 0x27fe: 0x0004, 0x27ff: 0x0100,
+	// Block 0xa0, offset 0x2800
+	0x2800: 0x0400, 0x2801: 0x0100, 0x2802: 0x0400, 0x2803: 0x0004,
+	// Block 0xa1, offset 0x2840
+	0x2851: 0x0400,
+	0x2852: 0x0400, 0x2853: 0x0400, 0x2854: 0x0004, 0x2855: 0x0004, 0x2856: 0x0004, 0x2857: 0x0004,
+	0x285a: 0x0004, 0x285b: 0x0004, 0x285c: 0x0400, 0x285d: 0x0400,
+	0x285e: 0x0400, 0x285f: 0x0400, 0x2860: 0x0004,
+	0x2864: 0x0400,
+	// Block 0xa2, offset 0x2880
+	0x2881: 0x0004, 0x2882: 0x0004, 0x2883: 0x0004, 0x2884: 0x0004, 0x2885: 0x0004,
+	0x2886: 0x0004, 0x2887: 0x0004, 0x2888: 0x0004, 0x2889: 0x0004, 0x288a: 0x0004,
+	0x28b3: 0x0004, 0x28b4: 0x0004, 0x28b5: 0x0004,
+	0x28b6: 0x0004, 0x28b7: 0x0004, 0x28b8: 0x0004, 0x28b9: 0x0400, 0x28ba: 0x0100, 0x28bb: 0x0004,
+	0x28bc: 0x0004, 0x28bd: 0x0004, 0x28be: 0x0004,
+	// Block 0xa3, offset 0x28c0
+	0x28c7: 0x0004,
+	0x28d1: 0x0004,
+	0x28d2: 0x0004, 0x28d3: 0x0004, 0x28d4: 0x0004, 0x28d5: 0x0004, 0x28d6: 0x0004, 0x28d7: 0x0400,
+	0x28d8: 0x0400, 0x28d9: 0x0004, 0x28da: 0x0004, 0x28db: 0x0004,
+	// Block 0xa4, offset 0x2900
+	0x2904: 0x0100, 0x2905: 0x0100,
+	0x2906: 0x0100, 0x2907: 0x0100, 0x2908: 0x0100, 0x2909: 0x0100, 0x290a: 0x0004, 0x290b: 0x0004,
+	0x290c: 0x0004, 0x290d: 0x0004, 0x290e: 0x0004, 0x290f: 0x0004, 0x2910: 0x0004, 0x2911: 0x0004,
+	0x2912: 0x0004, 0x2913: 0x0004, 0x2914: 0x0004, 0x2915: 0x0004, 0x2916: 0x0004, 0x2917: 0x0400,
+	0x2918: 0x0004, 0x2919: 0x0004,
+	// Block 0xa5, offset 0x2940
+	0x296f: 0x0400,
+	0x2970: 0x0004, 0x2971: 0x0004, 0x2972: 0x0004, 0x2973: 0x0004, 0x2974: 0x0004, 0x2975: 0x0004,
+	0x2976: 0x0004, 0x2978: 0x0004, 0x2979: 0x0004, 0x297a: 0x0004, 0x297b: 0x0004,
+	0x297c: 0x0004, 0x297d: 0x0004, 0x297e: 0x0400, 0x297f: 0x0004,
+	// Block 0xa6, offset 0x2980
+	0x2992: 0x0004, 0x2993: 0x0004, 0x2994: 0x0004, 0x2995: 0x0004, 0x2996: 0x0004, 0x2997: 0x0004,
+	0x2998: 0x0004, 0x2999: 0x0004, 0x299a: 0x0004, 0x299b: 0x0004, 0x299c: 0x0004, 0x299d: 0x0004,
+	0x299e: 0x0004, 0x299f: 0x0004, 0x29a0: 0x0004, 0x29a1: 0x0004, 0x29a2: 0x0004, 0x29a3: 0x0004,
+	0x29a4: 0x0004, 0x29a5: 0x0004, 0x29a6: 0x0004, 0x29a7: 0x0004, 0x29a9: 0x0400,
+	0x29aa: 0x0004, 0x29ab: 0x0004, 0x29ac: 0x0004, 0x29ad: 0x0004, 0x29ae: 0x0004, 0x29af: 0x0004,
+	0x29b0: 0x0004, 0x29b1: 0x0400, 0x29b2: 0x0004, 0x29b3: 0x0004, 0x29b4: 0x0400, 0x29b5: 0x0004,
+	0x29b6: 0x0004,
+	// Block 0xa7, offset 0x29c0
+	0x29f1: 0x0004, 0x29f2: 0x0004, 0x29f3: 0x0004, 0x29f4: 0x0004, 0x29f5: 0x0004,
+	0x29f6: 0x0004, 0x29fa: 0x0004,
+	0x29fc: 0x0004, 0x29fd: 0x0004, 0x29ff: 0x0004,
+	// Block 0xa8, offset 0x2a00
+	0x2a00: 0x0004, 0x2a01: 0x0004, 0x2a02: 0x0004, 0x2a03: 0x0004, 0x2a04: 0x0004, 0x2a05: 0x0004,
+	0x2a06: 0x0100, 0x2a07: 0x0004,
+	// Block 0xa9, offset 0x2a40
+	0x2a4a: 0x0400, 0x2a4b: 0x0400,
+	0x2a4c: 0x0400, 0x2a4d: 0x0400, 0x2a4e: 0x0400, 0x2a50: 0x0004, 0x2a51: 0x0004,
+	0x2a53: 0x0400, 0x2a54: 0x0400, 0x2a55: 0x0004, 0x2a56: 0x0400, 0x2a57: 0x0004,
+	// Block 0xaa, offset 0x2a80
+	0x2ab3: 0x0004, 0x2ab4: 0x0004, 0x2ab5: 0x0400,
+	0x2ab6: 0x0400,
+	// Block 0xab, offset 0x2ac0
+	0x2ac0: 0x0004, 0x2ac1: 0x0004, 0x2ac2: 0x0100, 0x2ac3: 0x0400,
+	0x2af4: 0x0400, 0x2af5: 0x0400,
+	0x2af6: 0x0004, 0x2af7: 0x0004, 0x2af8: 0x0004, 0x2af9: 0x0004, 0x2afa: 0x0004,
+	0x2afe: 0x0400, 0x2aff: 0x0400,
+	// Block 0xac, offset 0x2b00
+	0x2b00: 0x0004, 0x2b01: 0x0400, 0x2b02: 0x0004,
+	// Block 0xad, offset 0x2b40
+	0x2b70: 0x0002, 0x2b71: 0x0002, 0x2b72: 0x0002, 0x2b73: 0x0002, 0x2b74: 0x0002, 0x2b75: 0x0002,
+	0x2b76: 0x0002, 0x2b77: 0x0002, 0x2b78: 0x0002, 0x2b79: 0x0002, 0x2b7a: 0x0002, 0x2b7b: 0x0002,
+	0x2b7c: 0x0002, 0x2b7d: 0x0002, 0x2b7e: 0x0002, 0x2b7f: 0x0002,
+	// Block 0xae, offset 0x2b80
+	0x2b80: 0x0004,
+	0x2b87: 0x0004, 0x2b88: 0x0004, 0x2b89: 0x0004, 0x2b8a: 0x0004, 0x2b8b: 0x0004,
+	0x2b8c: 0x0004, 0x2b8d: 0x0004, 0x2b8e: 0x0004, 0x2b8f: 0x0004, 0x2b90: 0x0004, 0x2b91: 0x0004,
+	0x2b92: 0x0004, 0x2b93: 0x0004, 0x2b94: 0x0004, 0x2b95: 0x0004,
+	// Block 0xaf, offset 0x2bc0
+	0x2bf0: 0x0004, 0x2bf1: 0x0004, 0x2bf2: 0x0004, 0x2bf3: 0x0004, 0x2bf4: 0x0004,
+	// Block 0xb0, offset 0x2c00
+	0x2c30: 0x0004, 0x2c31: 0x0004, 0x2c32: 0x0004, 0x2c33: 0x0004, 0x2c34: 0x0004, 0x2c35: 0x0004,
+	0x2c36: 0x0004,
+	// Block 0xb1, offset 0x2c40
+	0x2c4f: 0x0004, 0x2c51: 0x0400,
+	0x2c52: 0x0400, 0x2c53: 0x0400, 0x2c54: 0x0400, 0x2c55: 0x0400, 0x2c56: 0x0400, 0x2c57: 0x0400,
+	0x2c58: 0x0400, 0x2c59: 0x0400, 0x2c5a: 0x0400, 0x2c5b: 0x0400, 0x2c5c: 0x0400, 0x2c5d: 0x0400,
+	0x2c5e: 0x0400, 0x2c5f: 0x0400, 0x2c60: 0x0400, 0x2c61: 0x0400, 0x2c62: 0x0400, 0x2c63: 0x0400,
+	0x2c64: 0x0400, 0x2c65: 0x0400, 0x2c66: 0x0400, 0x2c67: 0x0400, 0x2c68: 0x0400, 0x2c69: 0x0400,
+	0x2c6a: 0x0400, 0x2c6b: 0x0400, 0x2c6c: 0x0400, 0x2c6d: 0x0400, 0x2c6e: 0x0400, 0x2c6f: 0x0400,
+	0x2c70: 0x0400, 0x2c71: 0x0400, 0x2c72: 0x0400, 0x2c73: 0x0400, 0x2c74: 0x0400, 0x2c75: 0x0400,
+	0x2c76: 0x0400, 0x2c77: 0x0400, 0x2c78: 0x0400, 0x2c79: 0x0400, 0x2c7a: 0x0400, 0x2c7b: 0x0400,
+	0x2c7c: 0x0400, 0x2c7d: 0x0400, 0x2c7e: 0x0400, 0x2c7f: 0x0400,
+	// Block 0xb2, offset 0x2c80
+	0x2c80: 0x0400, 0x2c81: 0x0400, 0x2c82: 0x0400, 0x2c83: 0x0400, 0x2c84: 0x0400, 0x2c85: 0x0400,
+	0x2c86: 0x0400, 0x2c87: 0x0400,
+	0x2c8f: 0x0004, 0x2c90: 0x0004, 0x2c91: 0x0004,
+	0x2c92: 0x0004,
+	// Block 0xb3, offset 0x2cc0
+	0x2ce4: 0x0004,
+	0x2cf0: 0x0400, 0x2cf1: 0x0400,
+	// Block 0xb4, offset 0x2d00
+	0x2d1d: 0x0004,
+	0x2d1e: 0x0004, 0x2d20: 0x0002, 0x2d21: 0x0002, 0x2d22: 0x0002, 0x2d23: 0x0002,
+	// Block 0xb5, offset 0x2d40
+	0x2d40: 0x0004, 0x2d41: 0x0004, 0x2d42: 0x0004, 0x2d43: 0x0004, 0x2d44: 0x0004, 0x2d45: 0x0004,
+	0x2d46: 0x0004, 0x2d47: 0x0004, 0x2d48: 0x0004, 0x2d49: 0x0004, 0x2d4a: 0x0004, 0x2d4b: 0x0004,
+	0x2d4c: 0x0004, 0x2d4d: 0x0004, 0x2d4e: 0x0004, 0x2d4f: 0x0004, 0x2d50: 0x0004, 0x2d51: 0x0004,
+	0x2d52: 0x0004, 0x2d53: 0x0004, 0x2d54: 0x0004, 0x2d55: 0x0004, 0x2d56: 0x0004, 0x2d57: 0x0004,
+	0x2d58: 0x0004, 0x2d59: 0x0004, 0x2d5a: 0x0004, 0x2d5b: 0x0004, 0x2d5c: 0x0004, 0x2d5d: 0x0004,
+	0x2d5e: 0x0004, 0x2d5f: 0x0004, 0x2d60: 0x0004, 0x2d61: 0x0004, 0x2d62: 0x0004, 0x2d63: 0x0004,
+	0x2d64: 0x0004, 0x2d65: 0x0004, 0x2d66: 0x0004, 0x2d67: 0x0004, 0x2d68: 0x0004, 0x2d69: 0x0004,
+	0x2d6a: 0x0004, 0x2d6b: 0x0004, 0x2d6c: 0x0004, 0x2d6d: 0x0004,
+	0x2d70: 0x0004, 0x2d71: 0x0004, 0x2d72: 0x0004, 0x2d73: 0x0004, 0x2d74: 0x0004, 0x2d75: 0x0004,
+	0x2d76: 0x0004, 0x2d77: 0x0004, 0x2d78: 0x0004, 0x2d79: 0x0004, 0x2d7a: 0x0004, 0x2d7b: 0x0004,
+	0x2d7c: 0x0004, 0x2d7d: 0x0004, 0x2d7e: 0x0004, 0x2d7f: 0x0004,
+	// Block 0xb6, offset 0x2d80
+	0x2d80: 0x0004, 0x2d81: 0x0004, 0x2d82: 0x0004, 0x2d83: 0x0004, 0x2d84: 0x0004, 0x2d85: 0x0004,
+	0x2d86: 0x0004,
+	// Block 0xb7, offset 0x2dc0
+	0x2de5: 0x0004, 0x2de6: 0x0400, 0x2de7: 0x0004, 0x2de8: 0x0004, 0x2de9: 0x0004,
+	0x2ded: 0x0400, 0x2dee: 0x0004, 0x2def: 0x0004,
+	0x2df0: 0x0004, 0x2df1: 0x0004, 0x2df2: 0x0004, 0x2df3: 0x0002, 0x2df4: 0x0002, 0x2df5: 0x0002,
+	0x2df6: 0x0002, 0x2df7: 0x0002, 0x2df8: 0x0002, 0x2df9: 0x0002, 0x2dfa: 0x0002, 0x2dfb: 0x0004,
+	0x2dfc: 0x0004, 0x2dfd: 0x0004, 0x2dfe: 0x0004, 0x2dff: 0x0004,
+	// Block 0xb8, offset 0x2e00
+	0x2e00: 0x0004, 0x2e01: 0x0004, 0x2e02: 0x0004, 0x2e05: 0x0004,
+	0x2e06: 0x0004, 0x2e07: 0x0004, 0x2e08: 0x0004, 0x2e09: 0x0004, 0x2e0a: 0x0004, 0x2e0b: 0x0004,
+	0x2e2a: 0x0004, 0x2e2b: 0x0004, 0x2e2c: 0x0004, 0x2e2d: 0x0004,
+	// Block 0xb9, offset 0x2e40
+	0x2e42: 0x0004, 0x2e43: 0x0004, 0x2e44: 0x0004,
+	// Block 0xba, offset 0x2e80
+	0x2e80: 0x0004, 0x2e81: 0x0004, 0x2e82: 0x0004, 0x2e83: 0x0004, 0x2e84: 0x0004, 0x2e85: 0x0004,
+	0x2e86: 0x0004, 0x2e87: 0x0004, 0x2e88: 0x0004, 0x2e89: 0x0004, 0x2e8a: 0x0004, 0x2e8b: 0x0004,
+	0x2e8c: 0x0004, 0x2e8d: 0x0004, 0x2e8e: 0x0004, 0x2e8f: 0x0004, 0x2e90: 0x0004, 0x2e91: 0x0004,
+	0x2e92: 0x0004, 0x2e93: 0x0004, 0x2e94: 0x0004, 0x2e95: 0x0004, 0x2e96: 0x0004, 0x2e97: 0x0004,
+	0x2e98: 0x0004, 0x2e99: 0x0004, 0x2e9a: 0x0004, 0x2e9b: 0x0004, 0x2e9c: 0x0004, 0x2e9d: 0x0004,
+	0x2e9e: 0x0004, 0x2e9f: 0x0004, 0x2ea0: 0x0004, 0x2ea1: 0x0004, 0x2ea2: 0x0004, 0x2ea3: 0x0004,
+	0x2ea4: 0x0004, 0x2ea5: 0x0004, 0x2ea6: 0x0004, 0x2ea7: 0x0004, 0x2ea8: 0x0004, 0x2ea9: 0x0004,
+	0x2eaa: 0x0004, 0x2eab: 0x0004, 0x2eac: 0x0004, 0x2ead: 0x0004, 0x2eae: 0x0004, 0x2eaf: 0x0004,
+	0x2eb0: 0x0004, 0x2eb1: 0x0004, 0x2eb2: 0x0004, 0x2eb3: 0x0004, 0x2eb4: 0x0004, 0x2eb5: 0x0004,
+	0x2eb6: 0x0004, 0x2ebb: 0x0004,
+	0x2ebc: 0x0004, 0x2ebd: 0x0004, 0x2ebe: 0x0004, 0x2ebf: 0x0004,
+	// Block 0xbb, offset 0x2ec0
+	0x2ec0: 0x0004, 0x2ec1: 0x0004, 0x2ec2: 0x0004, 0x2ec3: 0x0004, 0x2ec4: 0x0004, 0x2ec5: 0x0004,
+	0x2ec6: 0x0004, 0x2ec7: 0x0004, 0x2ec8: 0x0004, 0x2ec9: 0x0004, 0x2eca: 0x0004, 0x2ecb: 0x0004,
+	0x2ecc: 0x0004, 0x2ecd: 0x0004, 0x2ece: 0x0004, 0x2ecf: 0x0004, 0x2ed0: 0x0004, 0x2ed1: 0x0004,
+	0x2ed2: 0x0004, 0x2ed3: 0x0004, 0x2ed4: 0x0004, 0x2ed5: 0x0004, 0x2ed6: 0x0004, 0x2ed7: 0x0004,
+	0x2ed8: 0x0004, 0x2ed9: 0x0004, 0x2eda: 0x0004, 0x2edb: 0x0004, 0x2edc: 0x0004, 0x2edd: 0x0004,
+	0x2ede: 0x0004, 0x2edf: 0x0004, 0x2ee0: 0x0004, 0x2ee1: 0x0004, 0x2ee2: 0x0004, 0x2ee3: 0x0004,
+	0x2ee4: 0x0004, 0x2ee5: 0x0004, 0x2ee6: 0x0004, 0x2ee7: 0x0004, 0x2ee8: 0x0004, 0x2ee9: 0x0004,
+	0x2eea: 0x0004, 0x2eeb: 0x0004, 0x2eec: 0x0004,
+	0x2ef5: 0x0004,
+	// Block 0xbc, offset 0x2f00
+	0x2f04: 0x0004,
+	0x2f1b: 0x0004, 0x2f1c: 0x0004, 0x2f1d: 0x0004,
+	0x2f1e: 0x0004, 0x2f1f: 0x0004, 0x2f21: 0x0004, 0x2f22: 0x0004, 0x2f23: 0x0004,
+	0x2f24: 0x0004, 0x2f25: 0x0004, 0x2f26: 0x0004, 0x2f27: 0x0004, 0x2f28: 0x0004, 0x2f29: 0x0004,
+	0x2f2a: 0x0004, 0x2f2b: 0x0004, 0x2f2c: 0x0004, 0x2f2d: 0x0004, 0x2f2e: 0x0004, 0x2f2f: 0x0004,
+	// Block 0xbd, offset 0x2f40
+	0x2f40: 0x0004, 0x2f41: 0x0004, 0x2f42: 0x0004, 0x2f43: 0x0004, 0x2f44: 0x0004, 0x2f45: 0x0004,
+	0x2f46: 0x0004, 0x2f48: 0x0004, 0x2f49: 0x0004, 0x2f4a: 0x0004, 0x2f4b: 0x0004,
+	0x2f4c: 0x0004, 0x2f4d: 0x0004, 0x2f4e: 0x0004, 0x2f4f: 0x0004, 0x2f50: 0x0004, 0x2f51: 0x0004,
+	0x2f52: 0x0004, 0x2f53: 0x0004, 0x2f54: 0x0004, 0x2f55: 0x0004, 0x2f56: 0x0004, 0x2f57: 0x0004,
+	0x2f58: 0x0004, 0x2f5b: 0x0004, 0x2f5c: 0x0004, 0x2f5d: 0x0004,
+	0x2f5e: 0x0004, 0x2f5f: 0x0004, 0x2f60: 0x0004, 0x2f61: 0x0004, 0x2f63: 0x0004,
+	0x2f64: 0x0004, 0x2f66: 0x0004, 0x2f67: 0x0004, 0x2f68: 0x0004, 0x2f69: 0x0004,
+	0x2f6a: 0x0004,
+	// Block 0xbe, offset 0x2f80
+	0x2f8f: 0x0004,
+	// Block 0xbf, offset 0x2fc0
+	0x2fee: 0x0004,
+	// Block 0xc0, offset 0x3000
+	0x302c: 0x0004, 0x302d: 0x0004, 0x302e: 0x0004, 0x302f: 0x0004,
+	// Block 0xc1, offset 0x3040
+	0x3050: 0x0004, 0x3051: 0x0004,
+	0x3052: 0x0004, 0x3053: 0x0004, 0x3054: 0x0004, 0x3055: 0x0004, 0x3056: 0x0004,
+	// Block 0xc2, offset 0x3080
+	0x3084: 0x0004, 0x3085: 0x0004,
+	0x3086: 0x0004, 0x3087: 0x0004, 0x3088: 0x0004, 0x3089: 0x0004, 0x308a: 0x0004,
+	// Block 0xc3, offset 0x30c0
+	0x30cd: 0x0008, 0x30ce: 0x0008, 0x30cf: 0x0008,
+	0x30ef: 0x0008,
+	// Block 0xc4, offset 0x3100
+	0x312c: 0x0008, 0x312d: 0x0008, 0x312e: 0x0008, 0x312f: 0x0008,
+	0x3130: 0x0008, 0x3131: 0x0008,
+	0x313e: 0x0008, 0x313f: 0x0008,
+	// Block 0xc5, offset 0x3140
+	0x314e: 0x0008, 0x3151: 0x0008,
+	0x3152: 0x0008, 0x3153: 0x0008, 0x3154: 0x0008, 0x3155: 0x0008, 0x3156: 0x0008, 0x3157: 0x0008,
+	0x3158: 0x0008, 0x3159: 0x0008, 0x315a: 0x0008,
+	0x316d: 0x0008, 0x316e: 0x0008, 0x316f: 0x0008,
+	0x3170: 0x0008, 0x3171: 0x0008, 0x3172: 0x0008, 0x3173: 0x0008, 0x3174: 0x0008, 0x3175: 0x0008,
+	0x3176: 0x0008, 0x3177: 0x0008, 0x3178: 0x0008, 0x3179: 0x0008, 0x317a: 0x0008, 0x317b: 0x0008,
+	0x317c: 0x0008, 0x317d: 0x0008, 0x317e: 0x0008, 0x317f: 0x0008,
+	// Block 0xc6, offset 0x3180
+	0x3180: 0x0008, 0x3181: 0x0008, 0x3182: 0x0008, 0x3183: 0x0008, 0x3184: 0x0008, 0x3185: 0x0008,
+	0x3186: 0x0008, 0x3187: 0x0008, 0x3188: 0x0008, 0x3189: 0x0008, 0x318a: 0x0008, 0x318b: 0x0008,
+	0x318c: 0x0008, 0x318d: 0x0008, 0x318e: 0x0008, 0x318f: 0x0008, 0x3190: 0x0008, 0x3191: 0x0008,
+	0x3192: 0x0008, 0x3193: 0x0008, 0x3194: 0x0008, 0x3195: 0x0008, 0x3196: 0x0008, 0x3197: 0x0008,
+	0x3198: 0x0008, 0x3199: 0x0008, 0x319a: 0x0008, 0x319b: 0x0008, 0x319c: 0x0008, 0x319d: 0x0008,
+	0x319e: 0x0008, 0x319f: 0x0008, 0x31a0: 0x0008, 0x31a1: 0x0008, 0x31a2: 0x0008, 0x31a3: 0x0008,
+	0x31a4: 0x0008, 0x31a5: 0x0008, 0x31a6: 0x0200, 0x31a7: 0x0200, 0x31a8: 0x0200, 0x31a9: 0x0200,
+	0x31aa: 0x0200, 0x31ab: 0x0200, 0x31ac: 0x0200, 0x31ad: 0x0200, 0x31ae: 0x0200, 0x31af: 0x0200,
+	0x31b0: 0x0200, 0x31b1: 0x0200, 0x31b2: 0x0200, 0x31b3: 0x0200, 0x31b4: 0x0200, 0x31b5: 0x0200,
+	0x31b6: 0x0200, 0x31b7: 0x0200, 0x31b8: 0x0200, 0x31b9: 0x0200, 0x31ba: 0x0200, 0x31bb: 0x0200,
+	0x31bc: 0x0200, 0x31bd: 0x0200, 0x31be: 0x0200, 0x31bf: 0x0200,
+	// Block 0xc7, offset 0x31c0
+	0x31c1: 0x0008, 0x31c2: 0x0008, 0x31c3: 0x0008, 0x31c4: 0x0008, 0x31c5: 0x0008,
+	0x31c6: 0x0008, 0x31c7: 0x0008, 0x31c8: 0x0008, 0x31c9: 0x0008, 0x31ca: 0x0008, 0x31cb: 0x0008,
+	0x31cc: 0x0008, 0x31cd: 0x0008, 0x31ce: 0x0008, 0x31cf: 0x0008,
+	0x31da: 0x0008,
+	0x31ef: 0x0008,
+	0x31f2: 0x0008, 0x31f3: 0x0008, 0x31f4: 0x0008, 0x31f5: 0x0008,
+	0x31f6: 0x0008, 0x31f7: 0x0008, 0x31f8: 0x0008, 0x31f9: 0x0008, 0x31fa: 0x0008,
+	0x31fc: 0x0008, 0x31fd: 0x0008, 0x31fe: 0x0008, 0x31ff: 0x0008,
+	// Block 0xc8, offset 0x3200
+	0x3209: 0x0008, 0x320a: 0x0008, 0x320b: 0x0008,
+	0x320c: 0x0008, 0x320d: 0x0008, 0x320e: 0x0008, 0x320f: 0x0008, 0x3210: 0x0008, 0x3211: 0x0008,
+	0x3212: 0x0008, 0x3213: 0x0008, 0x3214: 0x0008, 0x3215: 0x0008, 0x3216: 0x0008, 0x3217: 0x0008,
+	0x3218: 0x0008, 0x3219: 0x0008, 0x321a: 0x0008, 0x321b: 0x0008, 0x321c: 0x0008, 0x321d: 0x0008,
+	0x321e: 0x0008, 0x321f: 0x0008, 0x3220: 0x0008, 0x3221: 0x0008, 0x3222: 0x0008, 0x3223: 0x0008,
+	0x3224: 0x0008, 0x3225: 0x0008, 0x3226: 0x0008, 0x3227: 0x0008, 0x3228: 0x0008, 0x3229: 0x0008,
+	0x322a: 0x0008, 0x322b: 0x0008, 0x322c: 0x0008, 0x322d: 0x0008, 0x322e: 0x0008, 0x322f: 0x0008,
+	0x3230: 0x0008, 0x3231: 0x0008, 0x3232: 0x0008, 0x3233: 0x0008, 0x3234: 0x0008, 0x3235: 0x0008,
+	0x3236: 0x0008, 0x3237: 0x0008, 0x3238: 0x0008, 0x3239: 0x0008, 0x323a: 0x0008, 0x323b: 0x0008,
+	0x323c: 0x0008, 0x323d: 0x0008, 0x323e: 0x0008, 0x323f: 0x0008,
+	// Block 0xc9, offset 0x3240
+	0x3240: 0x0008, 0x3241: 0x0008, 0x3242: 0x0008, 0x3243: 0x0008, 0x3244: 0x0008, 0x3245: 0x0008,
+	0x3246: 0x0008, 0x3247: 0x0008, 0x3248: 0x0008, 0x3249: 0x0008, 0x324a: 0x0008, 0x324b: 0x0008,
+	0x324c: 0x0008, 0x324d: 0x0008, 0x324e: 0x0008, 0x324f: 0x0008, 0x3250: 0x0008, 0x3251: 0x0008,
+	0x3252: 0x0008, 0x3253: 0x0008, 0x3254: 0x0008, 0x3255: 0x0008, 0x3256: 0x0008, 0x3257: 0x0008,
+	0x3258: 0x0008, 0x3259: 0x0008, 0x325a: 0x0008, 0x325b: 0x0008, 0x325c: 0x0008, 0x325d: 0x0008,
+	0x325e: 0x0008, 0x325f: 0x0008, 0x3260: 0x0008, 0x3261: 0x0008, 0x3262: 0x0008, 0x3263: 0x0008,
+	0x3264: 0x0008, 0x3265: 0x0008, 0x3266: 0x0008, 0x3267: 0x0008, 0x3268: 0x0008, 0x3269: 0x0008,
+	0x326a: 0x0008, 0x326b: 0x0008, 0x326c: 0x0008, 0x326d: 0x0008, 0x326e: 0x0008, 0x326f: 0x0008,
+	0x3270: 0x0008, 0x3271: 0x0008, 0x3272: 0x0008, 0x3273: 0x0008, 0x3274: 0x0008, 0x3275: 0x0008,
+	0x3276: 0x0008, 0x3277: 0x0008, 0x3278: 0x0008, 0x3279: 0x0008, 0x327a: 0x0008, 0x327b: 0x0004,
+	0x327c: 0x0004, 0x327d: 0x0004, 0x327e: 0x0004, 0x327f: 0x0004,
+	// Block 0xca, offset 0x3280
+	0x3280: 0x0008, 0x3281: 0x0008, 0x3282: 0x0008, 0x3283: 0x0008, 0x3284: 0x0008, 0x3285: 0x0008,
+	0x3286: 0x0008, 0x3287: 0x0008, 0x3288: 0x0008, 0x3289: 0x0008, 0x328a: 0x0008, 0x328b: 0x0008,
+	0x328c: 0x0008, 0x328d: 0x0008, 0x328e: 0x0008, 0x328f: 0x0008, 0x3290: 0x0008, 0x3291: 0x0008,
+	0x3292: 0x0008, 0x3293: 0x0008, 0x3294: 0x0008, 0x3295: 0x0008, 0x3296: 0x0008, 0x3297: 0x0008,
+	0x3298: 0x0008, 0x3299: 0x0008, 0x329a: 0x0008, 0x329b: 0x0008, 0x329c: 0x0008, 0x329d: 0x0008,
+	0x329e: 0x0008, 0x329f: 0x0008, 0x32a0: 0x0008, 0x32a1: 0x0008, 0x32a2: 0x0008, 0x32a3: 0x0008,
+	0x32a4: 0x0008, 0x32a5: 0x0008, 0x32a6: 0x0008, 0x32a7: 0x0008, 0x32a8: 0x0008, 0x32a9: 0x0008,
+	0x32aa: 0x0008, 0x32ab: 0x0008, 0x32ac: 0x0008, 0x32ad: 0x0008, 0x32ae: 0x0008, 0x32af: 0x0008,
+	0x32b0: 0x0008, 0x32b1: 0x0008, 0x32b2: 0x0008, 0x32b3: 0x0008, 0x32b4: 0x0008, 0x32b5: 0x0008,
+	0x32b6: 0x0008, 0x32b7: 0x0008, 0x32b8: 0x0008, 0x32b9: 0x0008, 0x32ba: 0x0008, 0x32bb: 0x0008,
+	0x32bc: 0x0008, 0x32bd: 0x0008,
+	// Block 0xcb, offset 0x32c0
+	0x32c6: 0x0008, 0x32c7: 0x0008, 0x32c8: 0x0008, 0x32c9: 0x0008, 0x32ca: 0x0008, 0x32cb: 0x0008,
+	0x32cc: 0x0008, 0x32cd: 0x0008, 0x32ce: 0x0008, 0x32cf: 0x0008, 0x32d0: 0x0008, 0x32d1: 0x0008,
+	0x32d2: 0x0008, 0x32d3: 0x0008, 0x32d4: 0x0008, 0x32d5: 0x0008, 0x32d6: 0x0008, 0x32d7: 0x0008,
+	0x32d8: 0x0008, 0x32d9: 0x0008, 0x32da: 0x0008, 0x32db: 0x0008, 0x32dc: 0x0008, 0x32dd: 0x0008,
+	0x32de: 0x0008, 0x32df: 0x0008, 0x32e0: 0x0008, 0x32e1: 0x0008, 0x32e2: 0x0008, 0x32e3: 0x0008,
+	0x32e4: 0x0008, 0x32e5: 0x0008, 0x32e6: 0x0008, 0x32e7: 0x0008, 0x32e8: 0x0008, 0x32e9: 0x0008,
+	0x32ea: 0x0008, 0x32eb: 0x0008, 0x32ec: 0x0008, 0x32ed: 0x0008, 0x32ee: 0x0008, 0x32ef: 0x0008,
+	0x32f0: 0x0008, 0x32f1: 0x0008, 0x32f2: 0x0008, 0x32f3: 0x0008, 0x32f4: 0x0008, 0x32f5: 0x0008,
+	0x32f6: 0x0008, 0x32f7: 0x0008, 0x32f8: 0x0008, 0x32f9: 0x0008, 0x32fa: 0x0008, 0x32fb: 0x0008,
+	0x32fc: 0x0008, 0x32fd: 0x0008, 0x32fe: 0x0008, 0x32ff: 0x0008,
+	// Block 0xcc, offset 0x3300
+	0x3300: 0x0008, 0x3301: 0x0008, 0x3302: 0x0008, 0x3303: 0x0008, 0x3304: 0x0008, 0x3305: 0x0008,
+	0x3306: 0x0008, 0x3307: 0x0008, 0x3308: 0x0008, 0x3309: 0x0008, 0x330a: 0x0008, 0x330b: 0x0008,
+	0x330c: 0x0008, 0x330d: 0x0008, 0x330e: 0x0008, 0x330f: 0x0008,
+	// Block 0xcd, offset 0x3340
+	0x3374: 0x0008, 0x3375: 0x0008,
+	0x3376: 0x0008, 0x3377: 0x0008, 0x3378: 0x0008, 0x3379: 0x0008, 0x337a: 0x0008, 0x337b: 0x0008,
+	0x337c: 0x0008, 0x337d: 0x0008, 0x337e: 0x0008, 0x337f: 0x0008,
+	// Block 0xce, offset 0x3380
+	0x3395: 0x0008, 0x3396: 0x0008, 0x3397: 0x0008,
+	0x3398: 0x0008, 0x3399: 0x0008, 0x339a: 0x0008, 0x339b: 0x0008, 0x339c: 0x0008, 0x339d: 0x0008,
+	0x339e: 0x0008, 0x339f: 0x0008, 0x33a0: 0x0008, 0x33a1: 0x0008, 0x33a2: 0x0008, 0x33a3: 0x0008,
+	0x33a4: 0x0008, 0x33a5: 0x0008, 0x33a6: 0x0008, 0x33a7: 0x0008, 0x33a8: 0x0008, 0x33a9: 0x0008,
+	0x33aa: 0x0008, 0x33ab: 0x0008, 0x33ac: 0x0008, 0x33ad: 0x0008, 0x33ae: 0x0008, 0x33af: 0x0008,
+	0x33b0: 0x0008, 0x33b1: 0x0008, 0x33b2: 0x0008, 0x33b3: 0x0008, 0x33b4: 0x0008, 0x33b5: 0x0008,
+	0x33b6: 0x0008, 0x33b7: 0x0008, 0x33b8: 0x0008, 0x33b9: 0x0008, 0x33ba: 0x0008, 0x33bb: 0x0008,
+	0x33bc: 0x0008, 0x33bd: 0x0008, 0x33be: 0x0008, 0x33bf: 0x0008,
+	// Block 0xcf, offset 0x33c0
+	0x33cc: 0x0008, 0x33cd: 0x0008, 0x33ce: 0x0008, 0x33cf: 0x0008,
+	// Block 0xd0, offset 0x3400
+	0x3408: 0x0008, 0x3409: 0x0008, 0x340a: 0x0008, 0x340b: 0x0008,
+	0x340c: 0x0008, 0x340d: 0x0008, 0x340e: 0x0008, 0x340f: 0x0008,
+	0x341a: 0x0008, 0x341b: 0x0008, 0x341c: 0x0008, 0x341d: 0x0008,
+	0x341e: 0x0008, 0x341f: 0x0008,
+	// Block 0xd1, offset 0x3440
+	0x3448: 0x0008, 0x3449: 0x0008, 0x344a: 0x0008, 0x344b: 0x0008,
+	0x344c: 0x0008, 0x344d: 0x0008, 0x344e: 0x0008, 0x344f: 0x0008,
+	0x346e: 0x0008, 0x346f: 0x0008,
+	0x3470: 0x0008, 0x3471: 0x0008, 0x3472: 0x0008, 0x3473: 0x0008, 0x3474: 0x0008, 0x3475: 0x0008,
+	0x3476: 0x0008, 0x3477: 0x0008, 0x3478: 0x0008, 0x3479: 0x0008, 0x347a: 0x0008, 0x347b: 0x0008,
+	0x347c: 0x0008, 0x347d: 0x0008, 0x347e: 0x0008, 0x347f: 0x0008,
+	// Block 0xd2, offset 0x3480
+	0x348c: 0x0008, 0x348d: 0x0008, 0x348e: 0x0008, 0x348f: 0x0008, 0x3490: 0x0008, 0x3491: 0x0008,
+	0x3492: 0x0008, 0x3493: 0x0008, 0x3494: 0x0008, 0x3495: 0x0008, 0x3496: 0x0008, 0x3497: 0x0008,
+	0x3498: 0x0008, 0x3499: 0x0008, 0x349a: 0x0008, 0x349b: 0x0008, 0x349c: 0x0008, 0x349d: 0x0008,
+	0x349e: 0x0008, 0x349f: 0x0008, 0x34a0: 0x0008, 0x34a1: 0x0008, 0x34a2: 0x0008, 0x34a3: 0x0008,
+	0x34a4: 0x0008, 0x34a5: 0x0008, 0x34a6: 0x0008, 0x34a7: 0x0008, 0x34a8: 0x0008, 0x34a9: 0x0008,
+	0x34aa: 0x0008, 0x34ab: 0x0008, 0x34ac: 0x0008, 0x34ad: 0x0008, 0x34ae: 0x0008, 0x34af: 0x0008,
+	0x34b0: 0x0008, 0x34b1: 0x0008, 0x34b2: 0x0008, 0x34b3: 0x0008, 0x34b4: 0x0008, 0x34b5: 0x0008,
+	0x34b6: 0x0008, 0x34b7: 0x0008, 0x34b8: 0x0008, 0x34b9: 0x0008, 0x34ba: 0x0008,
+	0x34bc: 0x0008, 0x34bd: 0x0008, 0x34be: 0x0008, 0x34bf: 0x0008,
+	// Block 0xd3, offset 0x34c0
+	0x34c0: 0x0008, 0x34c1: 0x0008, 0x34c2: 0x0008, 0x34c3: 0x0008, 0x34c4: 0x0008, 0x34c5: 0x0008,
+	0x34c7: 0x0008, 0x34c8: 0x0008, 0x34c9: 0x0008, 0x34ca: 0x0008, 0x34cb: 0x0008,
+	0x34cc: 0x0008, 0x34cd: 0x0008, 0x34ce: 0x0008, 0x34cf: 0x0008, 0x34d0: 0x0008, 0x34d1: 0x0008,
+	0x34d2: 0x0008, 0x34d3: 0x0008, 0x34d4: 0x0008, 0x34d5: 0x0008, 0x34d6: 0x0008, 0x34d7: 0x0008,
+	0x34d8: 0x0008, 0x34d9: 0x0008, 0x34da: 0x0008, 0x34db: 0x0008, 0x34dc: 0x0008, 0x34dd: 0x0008,
+	0x34de: 0x0008, 0x34df: 0x0008, 0x34e0: 0x0008, 0x34e1: 0x0008, 0x34e2: 0x0008, 0x34e3: 0x0008,
+	0x34e4: 0x0008, 0x34e5: 0x0008, 0x34e6: 0x0008, 0x34e7: 0x0008, 0x34e8: 0x0008, 0x34e9: 0x0008,
+	0x34ea: 0x0008, 0x34eb: 0x0008, 0x34ec: 0x0008, 0x34ed: 0x0008, 0x34ee: 0x0008, 0x34ef: 0x0008,
+	0x34f0: 0x0008, 0x34f1: 0x0008, 0x34f2: 0x0008, 0x34f3: 0x0008, 0x34f4: 0x0008, 0x34f5: 0x0008,
+	0x34f6: 0x0008, 0x34f7: 0x0008, 0x34f8: 0x0008, 0x34f9: 0x0008, 0x34fa: 0x0008, 0x34fb: 0x0008,
+	0x34fc: 0x0008, 0x34fd: 0x0008, 0x34fe: 0x0008, 0x34ff: 0x0008,
+	// Block 0xd4, offset 0x3500
+	0x3500: 0x0002, 0x3501: 0x0002, 0x3502: 0x0002, 0x3503: 0x0002, 0x3504: 0x0002, 0x3505: 0x0002,
+	0x3506: 0x0002, 0x3507: 0x0002, 0x3508: 0x0002, 0x3509: 0x0002, 0x350a: 0x0002, 0x350b: 0x0002,
+	0x350c: 0x0002, 0x350d: 0x0002, 0x350e: 0x0002, 0x350f: 0x0002, 0x3510: 0x0002, 0x3511: 0x0002,
+	0x3512: 0x0002, 0x3513: 0x0002, 0x3514: 0x0002, 0x3515: 0x0002, 0x3516: 0x0002, 0x3517: 0x0002,
+	0x3518: 0x0002, 0x3519: 0x0002, 0x351a: 0x0002, 0x351b: 0x0002, 0x351c: 0x0002, 0x351d: 0x0002,
+	0x351e: 0x0002, 0x351f: 0x0002, 0x3520: 0x0004, 0x3521: 0x0004, 0x3522: 0x0004, 0x3523: 0x0004,
+	0x3524: 0x0004, 0x3525: 0x0004, 0x3526: 0x0004, 0x3527: 0x0004, 0x3528: 0x0004, 0x3529: 0x0004,
+	0x352a: 0x0004, 0x352b: 0x0004, 0x352c: 0x0004, 0x352d: 0x0004, 0x352e: 0x0004, 0x352f: 0x0004,
+	0x3530: 0x0004, 0x3531: 0x0004, 0x3532: 0x0004, 0x3533: 0x0004, 0x3534: 0x0004, 0x3535: 0x0004,
+	0x3536: 0x0004, 0x3537: 0x0004, 0x3538: 0x0004, 0x3539: 0x0004, 0x353a: 0x0004, 0x353b: 0x0004,
+	0x353c: 0x0004, 0x353d: 0x0004, 0x353e: 0x0004, 0x353f: 0x0004,
+	// Block 0xd5, offset 0x3540
+	0x3540: 0x0002, 0x3541: 0x0002, 0x3542: 0x0002, 0x3543: 0x0002, 0x3544: 0x0002, 0x3545: 0x0002,
+	0x3546: 0x0002, 0x3547: 0x0002, 0x3548: 0x0002, 0x3549: 0x0002, 0x354a: 0x0002, 0x354b: 0x0002,
+	0x354c: 0x0002, 0x354d: 0x0002, 0x354e: 0x0002, 0x354f: 0x0002, 0x3550: 0x0002, 0x3551: 0x0002,
+	0x3552: 0x0002, 0x3553: 0x0002, 0x3554: 0x0002, 0x3555: 0x0002, 0x3556: 0x0002, 0x3557: 0x0002,
+	0x3558: 0x0002, 0x3559: 0x0002, 0x355a: 0x0002, 0x355b: 0x0002, 0x355c: 0x0002, 0x355d: 0x0002,
+	0x355e: 0x0002, 0x355f: 0x0002, 0x3560: 0x0002, 0x3561: 0x0002, 0x3562: 0x0002, 0x3563: 0x0002,
+	0x3564: 0x0002, 0x3565: 0x0002, 0x3566: 0x0002, 0x3567: 0x0002, 0x3568: 0x0002, 0x3569: 0x0002,
+	0x356a: 0x0002, 0x356b: 0x0002, 0x356c: 0x0002, 0x356d: 0x0002, 0x356e: 0x0002, 0x356f: 0x0002,
+	0x3570: 0x0002, 0x3571: 0x0002, 0x3572: 0x0002, 0x3573: 0x0002, 0x3574: 0x0002, 0x3575: 0x0002,
+	0x3576: 0x0002, 0x3577: 0x0002, 0x3578: 0x0002, 0x3579: 0x0002, 0x357a: 0x0002, 0x357b: 0x0002,
+	0x357c: 0x0002, 0x357d: 0x0002, 0x357e: 0x0002, 0x357f: 0x0002,
+	// Block 0xd6, offset 0x3580
+	0x3580: 0x0004, 0x3581: 0x0004, 0x3582: 0x0004, 0x3583: 0x0004, 0x3584: 0x0004, 0x3585: 0x0004,
+	0x3586: 0x0004, 0x3587: 0x0004, 0x3588: 0x0004, 0x3589: 0x0004, 0x358a: 0x0004, 0x358b: 0x0004,
+	0x358c: 0x0004, 0x358d: 0x0004, 0x358e: 0x0004, 0x358f: 0x0004, 0x3590: 0x0004, 0x3591: 0x0004,
+	0x3592: 0x0004, 0x3593: 0x0004, 0x3594: 0x0004, 0x3595: 0x0004, 0x3596: 0x0004, 0x3597: 0x0004,
+	0x3598: 0x0004, 0x3599: 0x0004, 0x359a: 0x0004, 0x359b: 0x0004, 0x359c: 0x0004, 0x359d: 0x0004,
+	0x359e: 0x0004, 0x359f: 0x0004, 0x35a0: 0x0004, 0x35a1: 0x0004, 0x35a2: 0x0004, 0x35a3: 0x0004,
+	0x35a4: 0x0004, 0x35a5: 0x0004, 0x35a6: 0x0004, 0x35a7: 0x0004, 0x35a8: 0x0004, 0x35a9: 0x0004,
+	0x35aa: 0x0004, 0x35ab: 0x0004, 0x35ac: 0x0004, 0x35ad: 0x0004, 0x35ae: 0x0004, 0x35af: 0x0004,
+	0x35b0: 0x0002, 0x35b1: 0x0002, 0x35b2: 0x0002, 0x35b3: 0x0002, 0x35b4: 0x0002, 0x35b5: 0x0002,
+	0x35b6: 0x0002, 0x35b7: 0x0002, 0x35b8: 0x0002, 0x35b9: 0x0002, 0x35ba: 0x0002, 0x35bb: 0x0002,
+	0x35bc: 0x0002, 0x35bd: 0x0002, 0x35be: 0x0002, 0x35bf: 0x0002,
+}
+
+// graphemesIndex: 25 blocks, 1600 entries, 1600 bytes
+// Block 0 is the zero block.
+var graphemesIndex = [1600]property{
+	// Block 0x0, offset 0x0
+	// Block 0x1, offset 0x40
+	// Block 0x2, offset 0x80
+	// Block 0x3, offset 0xc0
+	0xc2: 0x01,
+	0xcc: 0x02, 0xcd: 0x03,
+	0xd2: 0x04, 0xd6: 0x05, 0xd7: 0x06,
+	0xd8: 0x07, 0xd9: 0x08, 0xdb: 0x09, 0xdc: 0x0a, 0xdd: 0x0b, 0xde: 0x0c, 0xdf: 0x0d,
+	0xe0: 0x02, 0xe1: 0x03, 0xe2: 0x04, 0xe3: 0x05,
+	0xea: 0x06, 0xeb: 0x07, 0xec: 0x08, 0xed: 0x09, 0xef: 0x0a,
+	0xf0: 0x14, 0xf3: 0x16,
+	// Block 0x4, offset 0x100
+	0x120: 0x0e, 0x121: 0x0f, 0x122: 0x10, 0x123: 0x11, 0x124: 0x12, 0x125: 0x13, 0x126: 0x14, 0x127: 0x15,
+	0x128: 0x16, 0x129: 0x17, 0x12a: 0x16, 0x12b: 0x18, 0x12c: 0x19, 0x12d: 0x1a, 0x12e: 0x1b, 0x12f: 0x1c,
+	0x130: 0x1d, 0x131: 0x1e, 0x132: 0x1f, 0x133: 0x20, 0x134: 0x21, 0x135: 0x22, 0x136: 0x23, 0x137: 0x24,
+	0x138: 0x25, 0x139: 0x26, 0x13a: 0x27, 0x13b: 0x28, 0x13c: 0x29, 0x13d: 0x2a, 0x13e: 0x2b, 0x13f: 0x2c,
+	// Block 0x5, offset 0x140
+	0x140: 0x2d, 0x141: 0x2e, 0x142: 0x2f, 0x144: 0x30, 0x145: 0x31, 0x146: 0x32, 0x147: 0x33,
+	0x14d: 0x34,
+	0x15c: 0x35, 0x15d: 0x36, 0x15e: 0x37, 0x15f: 0x38,
+	0x160: 0x39, 0x162: 0x3a, 0x164: 0x3b,
+	0x168: 0x3c, 0x169: 0x3d, 0x16a: 0x3e, 0x16b: 0x3f, 0x16c: 0x40, 0x16d: 0x41, 0x16e: 0x42, 0x16f: 0x43,
+	0x170: 0x44, 0x173: 0x45, 0x177: 0x02,
+	// Block 0x6, offset 0x180
+	0x180: 0x46, 0x181: 0x47, 0x183: 0x48, 0x184: 0x49, 0x186: 0x4a,
+	0x18c: 0x4b, 0x18e: 0x4c, 0x18f: 0x4d,
+	0x193: 0x4e, 0x196: 0x4f, 0x197: 0x50,
+	0x198: 0x51, 0x199: 0x52, 0x19a: 0x53, 0x19b: 0x52, 0x19c: 0x54, 0x19d: 0x55, 0x19e: 0x56,
+	0x1a4: 0x57,
+	0x1ac: 0x58, 0x1ad: 0x59,
+	0x1b3: 0x5a, 0x1b5: 0x5b, 0x1b7: 0x5c,
+	// Block 0x7, offset 0x1c0
+	0x1c0: 0x5d, 0x1c2: 0x5e,
+	0x1ca: 0x5f,
+	// Block 0x8, offset 0x200
+	0x219: 0x60, 0x21a: 0x61, 0x21b: 0x62,
+	0x220: 0x63, 0x222: 0x64, 0x223: 0x65, 0x224: 0x66, 0x225: 0x67, 0x226: 0x68, 0x227: 0x69,
+	0x228: 0x6a, 0x229: 0x6b, 0x22a: 0x6c, 0x22b: 0x6d, 0x22f: 0x6e,
+	0x230: 0x6f, 0x231: 0x70, 0x232: 0x71, 0x233: 0x72, 0x234: 0x73, 0x235: 0x74, 0x236: 0x75, 0x237: 0x6f,
+	0x238: 0x70, 0x239: 0x71, 0x23a: 0x72, 0x23b: 0x73, 0x23c: 0x74, 0x23d: 0x75, 0x23e: 0x6f, 0x23f: 0x70,
+	// Block 0x9, offset 0x240
+	0x240: 0x71, 0x241: 0x72, 0x242: 0x73, 0x243: 0x74, 0x244: 0x75, 0x245: 0x6f, 0x246: 0x70, 0x247: 0x71,
+	0x248: 0x72, 0x249: 0x73, 0x24a: 0x74, 0x24b: 0x75, 0x24c: 0x6f, 0x24d: 0x70, 0x24e: 0x71, 0x24f: 0x72,
+	0x250: 0x73, 0x251: 0x74, 0x252: 0x75, 0x253: 0x6f, 0x254: 0x70, 0x255: 0x71, 0x256: 0x72, 0x257: 0x73,
+	0x258: 0x74, 0x259: 0x75, 0x25a: 0x6f, 0x25b: 0x70, 0x25c: 0x71, 0x25d: 0x72, 0x25e: 0x73, 0x25f: 0x74,
+	0x260: 0x75, 0x261: 0x6f, 0x262: 0x70, 0x263: 0x71, 0x264: 0x72, 0x265: 0x73, 0x266: 0x74, 0x267: 0x75,
+	0x268: 0x6f, 0x269: 0x70, 0x26a: 0x71, 0x26b: 0x72, 0x26c: 0x73, 0x26d: 0x74, 0x26e: 0x75, 0x26f: 0x6f,
+	0x270: 0x70, 0x271: 0x71, 0x272: 0x72, 0x273: 0x73, 0x274: 0x74, 0x275: 0x75, 0x276: 0x6f, 0x277: 0x70,
+	0x278: 0x71, 0x279: 0x72, 0x27a: 0x73, 0x27b: 0x74, 0x27c: 0x75, 0x27d: 0x6f, 0x27e: 0x70, 0x27f: 0x71,
+	// Block 0xa, offset 0x280
+	0x280: 0x72, 0x281: 0x73, 0x282: 0x74, 0x283: 0x75, 0x284: 0x6f, 0x285: 0x70, 0x286: 0x71, 0x287: 0x72,
+	0x288: 0x73, 0x289: 0x74, 0x28a: 0x75, 0x28b: 0x6f, 0x28c: 0x70, 0x28d: 0x71, 0x28e: 0x72, 0x28f: 0x73,
+	0x290: 0x74, 0x291: 0x75, 0x292: 0x6f, 0x293: 0x70, 0x294: 0x71, 0x295: 0x72, 0x296: 0x73, 0x297: 0x74,
+	0x298: 0x75, 0x299: 0x6f, 0x29a: 0x70, 0x29b: 0x71, 0x29c: 0x72, 0x29d: 0x73, 0x29e: 0x74, 0x29f: 0x75,
+	0x2a0: 0x6f, 0x2a1: 0x70, 0x2a2: 0x71, 0x2a3: 0x72, 0x2a4: 0x73, 0x2a5: 0x74, 0x2a6: 0x75, 0x2a7: 0x6f,
+	0x2a8: 0x70, 0x2a9: 0x71, 0x2aa: 0x72, 0x2ab: 0x73, 0x2ac: 0x74, 0x2ad: 0x75, 0x2ae: 0x6f, 0x2af: 0x70,
+	0x2b0: 0x71, 0x2b1: 0x72, 0x2b2: 0x73, 0x2b3: 0x74, 0x2b4: 0x75, 0x2b5: 0x6f, 0x2b6: 0x70, 0x2b7: 0x71,
+	0x2b8: 0x72, 0x2b9: 0x73, 0x2ba: 0x74, 0x2bb: 0x75, 0x2bc: 0x6f, 0x2bd: 0x70, 0x2be: 0x71, 0x2bf: 0x72,
+	// Block 0xb, offset 0x2c0
+	0x2c0: 0x73, 0x2c1: 0x74, 0x2c2: 0x75, 0x2c3: 0x6f, 0x2c4: 0x70, 0x2c5: 0x71, 0x2c6: 0x72, 0x2c7: 0x73,
+	0x2c8: 0x74, 0x2c9: 0x75, 0x2ca: 0x6f, 0x2cb: 0x70, 0x2cc: 0x71, 0x2cd: 0x72, 0x2ce: 0x73, 0x2cf: 0x74,
+	0x2d0: 0x75, 0x2d1: 0x6f, 0x2d2: 0x70, 0x2d3: 0x71, 0x2d4: 0x72, 0x2d5: 0x73, 0x2d6: 0x74, 0x2d7: 0x75,
+	0x2d8: 0x6f, 0x2d9: 0x70, 0x2da: 0x71, 0x2db: 0x72, 0x2dc: 0x73, 0x2dd: 0x74, 0x2de: 0x76, 0x2df: 0x77,
+	// Block 0xc, offset 0x300
+	0x32c: 0x78,
+	0x338: 0x79, 0x33b: 0x7a, 0x33e: 0x61, 0x33f: 0x7b,
+	// Block 0xd, offset 0x340
+	0x347: 0x7c,
+	0x34b: 0x7d, 0x34d: 0x7e,
+	0x368: 0x7f, 0x36b: 0x80,
+	0x374: 0x81,
+	0x37a: 0x82, 0x37b: 0x83, 0x37d: 0x84, 0x37e: 0x85,
+	// Block 0xe, offset 0x380
+	0x380: 0x86, 0x381: 0x87, 0x382: 0x88, 0x383: 0x89, 0x384: 0x8a, 0x385: 0x8b, 0x386: 0x8c, 0x387: 0x8d,
+	0x388: 0x8e, 0x389: 0x8f, 0x38b: 0x90, 0x38c: 0x21, 0x38d: 0x91,
+	0x390: 0x92, 0x391: 0x93, 0x392: 0x94, 0x393: 0x95, 0x396: 0x96, 0x397: 0x97,
+	0x398: 0x98, 0x399: 0x99, 0x39a: 0x9a, 0x39c: 0x9b,
+	0x3a0: 0x9c, 0x3a4: 0x9d, 0x3a5: 0x9e, 0x3a7: 0x9f,
+	0x3a8: 0xa0, 0x3a9: 0xa1, 0x3aa: 0xa2,
+	0x3b0: 0xa3, 0x3b2: 0xa4, 0x3b4: 0xa5, 0x3b5: 0xa6, 0x3b6: 0xa7,
+	0x3bb: 0xa8, 0x3bc: 0xa9, 0x3bd: 0xaa,
+	// Block 0xf, offset 0x3c0
+	0x3d0: 0xab, 0x3d1: 0xac,
+	// Block 0x10, offset 0x400
+	0x42b: 0xad, 0x42c: 0xae,
+	0x43d: 0xaf, 0x43e: 0xb0, 0x43f: 0xb1,
+	// Block 0x11, offset 0x440
+	0x472: 0xb2,
+	// Block 0x12, offset 0x480
+	0x4bc: 0xb3, 0x4bd: 0xb4,
+	// Block 0x13, offset 0x4c0
+	0x4c5: 0xb5, 0x4c6: 0xb6,
+	0x4c9: 0xb7,
+	0x4e8: 0xb8, 0x4e9: 0xb9, 0x4ea: 0xba,
+	// Block 0x14, offset 0x500
+	0x500: 0xbb, 0x502: 0xbc, 0x504: 0xae,
+	0x50a: 0xbd, 0x50b: 0xbe,
+	0x513: 0xbe,
+	0x523: 0xbf, 0x525: 0xc0,
+	// Block 0x15, offset 0x540
+	0x540: 0x52, 0x541: 0x52, 0x542: 0x52, 0x543: 0x52, 0x544: 0xc1, 0x545: 0xc2, 0x546: 0xc3, 0x547: 0xc4,
+	0x548: 0xc5, 0x549: 0xc6, 0x54a: 0x52, 0x54b: 0x52, 0x54c: 0x52, 0x54d: 0x52, 0x54e: 0x52, 0x54f: 0xc7,
+	0x550: 0x52, 0x551: 0x52, 0x552: 0x52, 0x553: 0x52, 0x554: 0xc8, 0x555: 0xc9, 0x556: 0x52, 0x557: 0x52,
+	0x558: 0x52, 0x559: 0xca, 0x55a: 0x52, 0x55b: 0x52, 0x55d: 0xcb, 0x55f: 0xcc,
+	0x560: 0xcd, 0x561: 0xce, 0x562: 0xcf, 0x563: 0x52, 0x564: 0xd0, 0x565: 0xd1, 0x566: 0x52, 0x567: 0x52,
+	0x568: 0x52, 0x569: 0x52, 0x56a: 0x52, 0x56b: 0x52,
+	0x570: 0x52, 0x571: 0x52, 0x572: 0x52, 0x573: 0x52, 0x574: 0x52, 0x575: 0x52, 0x576: 0x52, 0x577: 0x52,
+	0x578: 0x52, 0x579: 0x52, 0x57a: 0x52, 0x57b: 0x52, 0x57c: 0x52, 0x57d: 0x52, 0x57e: 0x52, 0x57f: 0xc8,
+	// Block 0x16, offset 0x580
+	0x590: 0x0b, 0x591: 0x0c, 0x593: 0x0d, 0x596: 0x0e,
+	0x59b: 0x0f, 0x59c: 0x10, 0x59d: 0x11, 0x59e: 0x12, 0x59f: 0x13,
+	// Block 0x17, offset 0x5c0
+	0x5c0: 0xd2, 0x5c1: 0x02, 0x5c2: 0xd3, 0x5c3: 0xd3, 0x5c4: 0x02, 0x5c5: 0x02, 0x5c6: 0x02, 0x5c7: 0xd4,
+	0x5c8: 0xd3, 0x5c9: 0xd3, 0x5ca: 0xd3, 0x5cb: 0xd3, 0x5cc: 0xd3, 0x5cd: 0xd3, 0x5ce: 0xd3, 0x5cf: 0xd3,
+	0x5d0: 0xd3, 0x5d1: 0xd3, 0x5d2: 0xd3, 0x5d3: 0xd3, 0x5d4: 0xd3, 0x5d5: 0xd3, 0x5d6: 0xd3, 0x5d7: 0xd3,
+	0x5d8: 0xd3, 0x5d9: 0xd3, 0x5da: 0xd3, 0x5db: 0xd3, 0x5dc: 0xd3, 0x5dd: 0xd3, 0x5de: 0xd3, 0x5df: 0xd3,
+	0x5e0: 0xd3, 0x5e1: 0xd3, 0x5e2: 0xd3, 0x5e3: 0xd3, 0x5e4: 0xd3, 0x5e5: 0xd3, 0x5e6: 0xd3, 0x5e7: 0xd3,
+	0x5e8: 0xd3, 0x5e9: 0xd3, 0x5ea: 0xd3, 0x5eb: 0xd3, 0x5ec: 0xd3, 0x5ed: 0xd3, 0x5ee: 0xd3, 0x5ef: 0xd3,
+	0x5f0: 0xd3, 0x5f1: 0xd3, 0x5f2: 0xd3, 0x5f3: 0xd3, 0x5f4: 0xd3, 0x5f5: 0xd3, 0x5f6: 0xd3, 0x5f7: 0xd3,
+	0x5f8: 0xd3, 0x5f9: 0xd3, 0x5fa: 0xd3, 0x5fb: 0xd3, 0x5fc: 0xd3, 0x5fd: 0xd3, 0x5fe: 0xd3, 0x5ff: 0xd3,
+	// Block 0x18, offset 0x600
+	0x620: 0x15,
+}
diff --git a/vendor/github.com/clipperhouse/uax29/v2/internal/iterators/iterator.go b/vendor/github.com/clipperhouse/uax29/v2/internal/iterators/iterator.go
new file mode 100644
index 000000000..e21348638
--- /dev/null
+++ b/vendor/github.com/clipperhouse/uax29/v2/internal/iterators/iterator.go
@@ -0,0 +1,100 @@
+package iterators
+
+import "github.com/clipperhouse/stringish"
+
+type SplitFunc[T stringish.Interface] func(T, bool) (int, T, error)
+
+// Iterator is a generic iterator for words that are either []byte or string.
+// Iterate while Next() is true, and access the word via Value().
+type Iterator[T stringish.Interface] struct {
+	split SplitFunc[T]
+	data  T
+	start int
+	pos   int
+}
+
+// New creates a new Iterator for the given data and SplitFunc.
+func New[T stringish.Interface](split SplitFunc[T], data T) *Iterator[T] {
+	return &Iterator[T]{
+		split: split,
+		data:  data,
+	}
+}
+
+// SetText sets the text for the iterator to operate on, and resets all state.
+func (iter *Iterator[T]) SetText(data T) {
+	iter.data = data
+	iter.start = 0
+	iter.pos = 0
+}
+
+// Split sets the SplitFunc for the Iterator.
+func (iter *Iterator[T]) Split(split SplitFunc[T]) {
+	iter.split = split
+}
+
+// Next advances the iterator to the next token. It returns false when there
+// are no remaining tokens or an error occurred.
+func (iter *Iterator[T]) Next() bool {
+	if iter.pos == len(iter.data) {
+		return false
+	}
+	if iter.pos > len(iter.data) {
+		panic("SplitFunc advanced beyond the end of the data")
+	}
+
+	iter.start = iter.pos
+
+	advance, _, err := iter.split(iter.data[iter.pos:], true)
+	if err != nil {
+		panic(err)
+	}
+	if advance <= 0 {
+		panic("SplitFunc returned a zero or negative advance")
+	}
+
+	iter.pos += advance
+	if iter.pos > len(iter.data) {
+		panic("SplitFunc advanced beyond the end of the data")
+	}
+
+	return true
+}
+
+// Value returns the current token.
+func (iter *Iterator[T]) Value() T {
+	return iter.data[iter.start:iter.pos]
+}
+
+// Start returns the byte position of the current token in the original data.
+func (iter *Iterator[T]) Start() int {
+	return iter.start
+}
+
+// End returns the byte position after the current token in the original data.
+func (iter *Iterator[T]) End() int {
+	return iter.pos
+}
+
+// Reset resets the iterator to the beginning of the data.
+func (iter *Iterator[T]) Reset() {
+	iter.start = 0
+	iter.pos = 0
+}
+
+func (iter *Iterator[T]) First() T {
+	if len(iter.data) == 0 {
+		return iter.data
+	}
+	advance, _, err := iter.split(iter.data, true)
+	if err != nil {
+		panic(err)
+	}
+	if advance <= 0 {
+		panic("SplitFunc returned a zero or negative advance")
+	}
+	if advance > len(iter.data) {
+		panic("SplitFunc advanced beyond the end of the data")
+	}
+	return iter.data[:advance]
+}
diff --git a/vendor/github.com/cucumber/godog/CHANGELOG.md b/vendor/github.com/cucumber/godog/CHANGELOG.md
index 11558f103..21a323e0a 100644
--- a/vendor/github.com/cucumber/godog/CHANGELOG.md
+++ b/vendor/github.com/cucumber/godog/CHANGELOG.md
@@ -8,6 +8,26 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
 
 ## Unreleased
 
+## [v0.15.1]
+
+### Added
+- Step text is added to "step is undefined" error - ([669](https://github.com/cucumber/godog/pull/669) - [vearutop](https://github.com/vearutop))
+- Localisation support by @MegaGrindStone in https://github.com/cucumber/godog/pull/665
+- feat: support uint types by @chengxilo in https://github.com/cucumber/godog/pull/695
+
+### Changed
+- Replace deprecated `::set-output` - ([681](https://github.com/cucumber/godog/pull/681) - [nodeg](https://github.com/nodeg))
+
+### Fixed
+- fix(errors): fix(errors): Fix expected Step argument count for steps with `context.Context` ([679](https://github.com/cucumber/godog/pull/679) - [tigh-latte](https://github.com/tigh-latte))
+- fix(formatter): On concurrent execution, execute formatter at end of Scenario - ([645](https://github.com/cucumber/godog/pull/645) - [tigh-latte](https://github.com/tigh-latte))
+- Pretty printing results now prints the line where the step is declared instead of the line where the handler is declared. ([668](https://github.com/cucumber/godog/pull/668) - [spencerc](https://github.com/SpencerC))
+- Update honnef.co/go/tools/cmd/staticcheck version in Makefile by @RezaZareiii in https://github.com/cucumber/godog/pull/670
+- fix: verify dogT exists in the context before using it by @cakoolen in https://github.com/cucumber/godog/pull/692
+- fix: change bang to being in README by @nahomEagleLion in https://github.com/cucumber/godog/pull/687
+- Mark junit test cases as skipped if no pickle step results available by @mrsheepuk in https://github.com/cucumber/godog/pull/597
+- Print step declaration line instead of handler declaration line by @SpencerC in https://github.com/cucumber/godog/pull/668
+
 ## [v0.15.0]
 
 ### Added
@@ -39,7 +59,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
 
 ### Changed
 - Update test.yml ([583](https://github.com/cucumber/godog/pull/583) - [vearutop](https://github.com/vearutop))
-  
+
 ## [v0.13.0]
 ### Added
 - Support for reading feature files from an `fs.FS` ([550](https://github.com/cucumber/godog/pull/550) - [tigh-latte](https://github.com/tigh-latte))
@@ -241,6 +261,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
 ### Changed
 - Changed code references to DATA-DOG/godog to cucumber/godog to help get things building correctly. ([mxygem](https://github.com/mxygem))
 
+[v0.15.1]: https://github.com/cucumber/godog/compare/v0.15.0...v0.15.1
 [v0.15.0]: https://github.com/cucumber/godog/compare/v0.14.1...v0.15.0
 [v0.14.1]: https://github.com/cucumber/godog/compare/v0.14.0...v0.14.1
 [v0.14.0]: https://github.com/cucumber/godog/compare/v0.13.0...v0.14.0
diff --git a/vendor/github.com/cucumber/godog/Makefile b/vendor/github.com/cucumber/godog/Makefile
index aefccb132..06c95c4e0 100644
--- a/vendor/github.com/cucumber/godog/Makefile
+++ b/vendor/github.com/cucumber/godog/Makefile
@@ -23,8 +23,8 @@ check-go-version:
 test: check-go-version
 	@echo "running all tests"
 	@go fmt ./...
-	@go run honnef.co/go/tools/cmd/staticcheck@v0.4.7 github.com/cucumber/godog
-	@go run honnef.co/go/tools/cmd/staticcheck@v0.4.7 github.com/cucumber/godog/cmd/godog
+	@go run honnef.co/go/tools/cmd/staticcheck@v0.5.1 github.com/cucumber/godog
+	@go run honnef.co/go/tools/cmd/staticcheck@v0.5.1 github.com/cucumber/godog/cmd/godog
 	go vet ./...
 	go test -race ./...
 	go run ./cmd/godog -f progress -c 4
diff --git a/vendor/github.com/cucumber/godog/README.md b/vendor/github.com/cucumber/godog/README.md
index fbe765f5b..dceacf167 100644
--- a/vendor/github.com/cucumber/godog/README.md
+++ b/vendor/github.com/cucumber/godog/README.md
@@ -26,7 +26,7 @@ Godog merges specification and test documentation into one cohesive whole.
 ### Living documentation
 
 Because they're automatically tested by Godog, your specifications are
-always bang up-to-date.
+always being up-to-date.
 
 ### Focus on the customer
 
diff --git a/vendor/github.com/cucumber/godog/formatters/fmt.go b/vendor/github.com/cucumber/godog/formatters/fmt.go
index fec969613..973cf11b8 100644
--- a/vendor/github.com/cucumber/godog/formatters/fmt.go
+++ b/vendor/github.com/cucumber/godog/formatters/fmt.go
@@ -74,6 +74,12 @@ type Formatter interface {
 	Summary()
 }
 
+// FlushFormatter is a `Formatter` but can be flushed.
+type FlushFormatter interface {
+	Formatter
+	Flush()
+}
+
 // FormatterFunc builds a formatter with given
 // suite name and io.Writer to record output
 type FormatterFunc func(string, io.Writer) Formatter
diff --git a/vendor/github.com/cucumber/godog/internal/flags/options.go b/vendor/github.com/cucumber/godog/internal/flags/options.go
index 5ca40f910..40acea652 100644
--- a/vendor/github.com/cucumber/godog/internal/flags/options.go
+++ b/vendor/github.com/cucumber/godog/internal/flags/options.go
@@ -48,6 +48,9 @@ type Options struct {
 	// from feature files
 	Tags string
 
+	// Dialect to be used to parse feature files. If not set, default to "en".
+	Dialect string
+
 	// The formatter name
 	Format string
 
diff --git a/vendor/github.com/cucumber/godog/internal/formatters/fmt.go b/vendor/github.com/cucumber/godog/internal/formatters/fmt.go
index 3efe1f466..5530c0c24 100644
--- a/vendor/github.com/cucumber/godog/internal/formatters/fmt.go
+++ b/vendor/github.com/cucumber/godog/internal/formatters/fmt.go
@@ -78,9 +78,7 @@ func mustConvertStringToInt(s string) int {
 func DefinitionID(sd *models.StepDefinition) string {
 	ptr := sd.HandlerValue.Pointer()
 	f := runtime.FuncForPC(ptr)
-	file, line := f.FileLine(ptr)
-	dir := filepath.Dir(file)
-
+	dir := filepath.Dir(sd.File)
 	fn := strings.Replace(f.Name(), dir, "", -1)
 	var parts []string
 	for _, gr := range matchFuncDefRef.FindAllStringSubmatch(fn, -1) {
@@ -100,7 +98,7 @@ func DefinitionID(sd *models.StepDefinition) string {
 		fn = strings.Replace(fn, "..", ".", -1)
 	}
 
-	return fmt.Sprintf("%s:%d -> %s", filepath.Base(file), line, fn)
+	return fmt.Sprintf("%s:%d -> %s", filepath.Base(sd.File), sd.Line, fn)
 }
 
 var matchFuncDefRef = regexp.MustCompile(`\(([^\)]+)\)`)
diff --git a/vendor/github.com/cucumber/godog/internal/formatters/fmt_flushwrap.go b/vendor/github.com/cucumber/godog/internal/formatters/fmt_flushwrap.go
new file mode 100644
index 000000000..129b06210
--- /dev/null
+++ b/vendor/github.com/cucumber/godog/internal/formatters/fmt_flushwrap.go
@@ -0,0 +1,108 @@
+package formatters
+
+import (
+	"sync"
+
+	"github.com/cucumber/godog/formatters"
+	messages "github.com/cucumber/messages/go/v21"
+)
+
+// WrapOnFlush wrap a `formatters.Formatter` in a `formatters.FlushFormatter`, which only
+// executes when `Flush` is called
+func WrapOnFlush(fmt formatters.Formatter) formatters.FlushFormatter {
+	return &onFlushFormatter{
+		fmt: fmt,
+		fns: make([]func(), 0),
+		mu:  &sync.Mutex{},
+	}
+}
+
+type onFlushFormatter struct {
+	fmt formatters.Formatter
+	fns []func()
+	mu  *sync.Mutex
+}
+
+func (o *onFlushFormatter) Pickle(pickle *messages.Pickle) {
+	o.fns = append(o.fns, func() {
+		o.fmt.Pickle(pickle)
+	})
+}
+
+func (o *onFlushFormatter) Passed(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
+	o.fns = append(o.fns, func() {
+		o.fmt.Passed(pickle, step, definition)
+	})
+}
+
+// Ambiguous implements formatters.Formatter.
+func (o *onFlushFormatter) Ambiguous(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition, err error) {
+	o.fns = append(o.fns, func() {
+		o.fmt.Ambiguous(pickle, step, definition, err)
+	})
+}
+
+// Defined implements formatters.Formatter.
+func (o *onFlushFormatter) Defined(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
+	o.fns = append(o.fns, func() {
+		o.fmt.Defined(pickle, step, definition)
+	})
+}
+
+// Failed implements formatters.Formatter.
+func (o *onFlushFormatter) Failed(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition, err error) {
+	o.fns = append(o.fns, func() {
+		o.fmt.Failed(pickle, step, definition, err)
+	})
+}
+
+// Feature implements formatters.Formatter.
+func (o *onFlushFormatter) Feature(pickle *messages.GherkinDocument, p string, c []byte) {
+	o.fns = append(o.fns, func() {
+		o.fmt.Feature(pickle, p, c)
+	})
+}
+
+// Pending implements formatters.Formatter.
+func (o *onFlushFormatter) Pending(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
+	o.fns = append(o.fns, func() {
+		o.fmt.Pending(pickle, step, definition)
+	})
+}
+
+// Skipped implements formatters.Formatter.
+func (o *onFlushFormatter) Skipped(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
+	o.fns = append(o.fns, func() {
+		o.fmt.Skipped(pickle, step, definition)
+	})
+}
+
+// Summary implements formatters.Formatter.
+func (o *onFlushFormatter) Summary() {
+	o.fns = append(o.fns, func() {
+		o.fmt.Summary()
+	})
+}
+
+// TestRunStarted implements formatters.Formatter.
+func (o *onFlushFormatter) TestRunStarted() {
+	o.fns = append(o.fns, func() {
+		o.fmt.TestRunStarted()
+	})
+}
+
+// Undefined implements formatters.Formatter.
+func (o *onFlushFormatter) Undefined(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
+	o.fns = append(o.fns, func() {
+		o.fmt.Undefined(pickle, step, definition)
+	})
+}
+
+// Flush the logs.
+func (o *onFlushFormatter) Flush() {
+	o.mu.Lock()
+	defer o.mu.Unlock()
+	for _, fn := range o.fns {
+		fn()
+	}
+}
diff --git a/vendor/github.com/cucumber/godog/internal/formatters/fmt_junit.go b/vendor/github.com/cucumber/godog/internal/formatters/fmt_junit.go
index d7b251707..85acabe2e 100644
--- a/vendor/github.com/cucumber/godog/internal/formatters/fmt_junit.go
+++ b/vendor/github.com/cucumber/godog/internal/formatters/fmt_junit.go
@@ -10,6 +10,7 @@ import (
 	"time"
 
 	"github.com/cucumber/godog/formatters"
+	"github.com/cucumber/godog/internal/models"
 	"github.com/cucumber/godog/internal/utils"
 )
 
@@ -47,6 +48,40 @@ func junitTimeDuration(from, to time.Time) string {
 	return strconv.FormatFloat(to.Sub(from).Seconds(), 'f', -1, 64)
 }
 
+// getPickleResult deals with the fact that if there's no result due to 'StopOnFirstFailure' being
+// set, MustGetPickleResult panics.
+func (f *JUnit) getPickleResult(pickleID string) (res *models.PickleResult) {
+	defer func() {
+		if r := recover(); r != nil {
+			res = nil
+		}
+	}()
+	pr := f.Storage.MustGetPickleResult(pickleID)
+	res = &pr
+	return
+}
+
+func (f *JUnit) getPickleStepResult(stepID string) (res *models.PickleStepResult) {
+	defer func() {
+		if r := recover(); r != nil {
+			res = nil
+		}
+	}()
+	psr := f.Storage.MustGetPickleStepResult(stepID)
+	res = &psr
+	return
+}
+
+func (f *JUnit) getPickleStepResultsByPickleID(pickleID string) (res []models.PickleStepResult) {
+	defer func() {
+		if r := recover(); r != nil {
+			res = nil
+		}
+	}()
+	res = f.Storage.MustGetPickleStepResultsByPickleID(pickleID)
+	return
+}
+
 func (f *JUnit) buildJUNITPackageSuite() JunitPackageSuite {
 	features := f.Storage.MustGetFeatures()
 	sort.Sort(sortFeaturesByName(features))
@@ -79,33 +114,37 @@ func (f *JUnit) buildJUNITPackageSuite() JunitPackageSuite {
 		var outlineNo = make(map[string]int)
 		for idx, pickle := range pickles {
 			tc := junitTestCase{}
-
-			pickleResult := f.Storage.MustGetPickleResult(pickle.Id)
-
-			if idx == 0 {
-				firstPickleStartedAt = pickleResult.StartedAt
+			tc.Name = pickle.Name
+			if testcaseNames[tc.Name] > 1 {
+				outlineNo[tc.Name] = outlineNo[tc.Name] + 1
+				tc.Name += fmt.Sprintf(" #%d", outlineNo[tc.Name])
 			}
 
-			lastPickleFinishedAt = pickleResult.StartedAt
+			pickleResult := f.getPickleResult(pickle.Id)
+			if pickleResult == nil {
+				tc.Status = skipped.String()
+			} else {
+				if idx == 0 {
+					firstPickleStartedAt = pickleResult.StartedAt
+				}
+				lastPickleFinishedAt = pickleResult.StartedAt
+			}
 
 			if len(pickle.Steps) > 0 {
 				lastStep := pickle.Steps[len(pickle.Steps)-1]
-				lastPickleStepResult := f.Storage.MustGetPickleStepResult(lastStep.Id)
-				lastPickleFinishedAt = lastPickleStepResult.FinishedAt
+				if lastPickleStepResult := f.getPickleStepResult(lastStep.Id); lastPickleStepResult != nil {
+					lastPickleFinishedAt = lastPickleStepResult.FinishedAt
+				}
 			}
 
-			tc.Time = junitTimeDuration(pickleResult.StartedAt, lastPickleFinishedAt)
-
-			tc.Name = pickle.Name
-			if testcaseNames[tc.Name] > 1 {
-				outlineNo[tc.Name] = outlineNo[tc.Name] + 1
-				tc.Name += fmt.Sprintf(" #%d", outlineNo[tc.Name])
+			if pickleResult != nil {
+				tc.Time = junitTimeDuration(pickleResult.StartedAt, lastPickleFinishedAt)
 			}
 
 			ts.Tests++
 			suite.Tests++
 
-			pickleStepResults := f.Storage.MustGetPickleStepResultsByPickleID(pickle.Id)
+			pickleStepResults := f.getPickleStepResultsByPickleID(pickle.Id)
 			for _, stepResult := range pickleStepResults {
 				pickleStep := f.Storage.MustGetPickleStep(stepResult.PickleStepID)
 
diff --git a/vendor/github.com/cucumber/godog/internal/formatters/fmt_pretty.go b/vendor/github.com/cucumber/godog/internal/formatters/fmt_pretty.go
index 91dbc0cb1..76d733793 100644
--- a/vendor/github.com/cucumber/godog/internal/formatters/fmt_pretty.go
+++ b/vendor/github.com/cucumber/godog/internal/formatters/fmt_pretty.go
@@ -243,7 +243,7 @@ func (f *Pretty) Summary() {
 	f.Base.Summary()
 }
 
-func (f *Pretty) printOutlineExample(pickle *messages.Pickle, backgroundSteps int) {
+func (f *Pretty) printOutlineExample(pickle *messages.Pickle, step *messages.PickleStep, backgroundSteps int) {
 	var errorMsg string
 	var clr = green
 
@@ -255,7 +255,7 @@ func (f *Pretty) printOutlineExample(pickle *messages.Pickle, backgroundSteps in
 	printExampleHeader := exampleTable.TableBody[0].Id == exampleRow.Id
 	firstExamplesTable := astScenario.Examples[0].Location.Line == exampleTable.Location.Line
 
-	pickleStepResults := f.Storage.MustGetPickleStepResultsByPickleID(pickle.Id)
+	pickleStepResults := f.Storage.MustGetPickleStepResultsByPickleIDUntilStep(pickle.Id, step.Id)
 
 	firstExecutedScenarioStep := len(pickleStepResults) == backgroundSteps+1
 	if firstExamplesTable && printExampleHeader && firstExecutedScenarioStep {
@@ -419,7 +419,7 @@ func (f *Pretty) printStep(pickle *messages.Pickle, pickleStep *messages.PickleS
 	}
 
 	if !astBackgroundStep && len(astScenario.Examples) > 0 {
-		f.printOutlineExample(pickle, backgroundSteps)
+		f.printOutlineExample(pickle, pickleStep, backgroundSteps)
 		return
 	}
 
diff --git a/vendor/github.com/cucumber/godog/internal/models/stepdef.go b/vendor/github.com/cucumber/godog/internal/models/stepdef.go
index 90ffef548..7c2e973ac 100644
--- a/vendor/github.com/cucumber/godog/internal/models/stepdef.go
+++ b/vendor/github.com/cucumber/godog/internal/models/stepdef.go
@@ -27,6 +27,8 @@ type StepDefinition struct {
 
 	Args         []interface{}
 	HandlerValue reflect.Value
+	File         string
+	Line         int
 
 	// multistep related
 	Nested    bool
@@ -54,7 +56,7 @@ func (sd *StepDefinition) Run(ctx context.Context) (context.Context, interface{}
 	}
 
 	if len(sd.Args) < numIn {
-		return ctx, fmt.Errorf("%w: expected %d arguments, matched %d from step", ErrUnmatchedStepArgumentNumber, typ.NumIn(), len(sd.Args))
+		return ctx, fmt.Errorf("%w: expected %d arguments, matched %d from step", ErrUnmatchedStepArgumentNumber, numIn, len(sd.Args))
 	}
 
 	for i := 0; i < numIn; i++ {
@@ -110,6 +112,56 @@ func (sd *StepDefinition) Run(ctx context.Context) (context.Context, interface{}
 				return ctx, fmt.Errorf(`%w %d: "%s" to int8: %s`, ErrCannotConvert, i, s, err)
 			}
 			values = append(values, reflect.ValueOf(int8(v)))
+		case reflect.Uint:
+			s, err := sd.shouldBeString(i)
+			if err != nil {
+				return ctx, err
+			}
+			v, err := strconv.ParseUint(s, 10, 0)
+			if err != nil {
+				return ctx, fmt.Errorf(`%w %d: "%s" to uint: %s`, ErrCannotConvert, i, s, err)
+			}
+			values = append(values, reflect.ValueOf(uint(v)))
+		case reflect.Uint64:
+			s, err := sd.shouldBeString(i)
+			if err != nil {
+				return ctx, err
+			}
+			v, err := strconv.ParseUint(s, 10, 64)
+			if err != nil {
+				return ctx, fmt.Errorf(`%w %d: "%s" to uint64: %s`, ErrCannotConvert, i, s, err)
+			}
+			values = append(values, reflect.ValueOf(v))
+		case reflect.Uint32:
+			s, err := sd.shouldBeString(i)
+			if err != nil {
+				return ctx, err
+			}
+			v, err := strconv.ParseUint(s, 10, 32)
+			if err != nil {
+				return ctx, fmt.Errorf(`%w %d: "%s" to uint32: %s`, ErrCannotConvert, i, s, err)
+			}
+			values = append(values, reflect.ValueOf(uint32(v)))
+		case reflect.Uint16:
+			s, err := sd.shouldBeString(i)
+			if err != nil {
+				return ctx, err
+			}
+			v, err := strconv.ParseUint(s, 10, 16)
+			if err != nil {
+				return ctx, fmt.Errorf(`%w %d: "%s" to uint16: %s`, ErrCannotConvert, i, s, err)
+			}
+			values = append(values, reflect.ValueOf(uint16(v)))
+		case reflect.Uint8:
+			s, err := sd.shouldBeString(i)
+			if err != nil {
+				return ctx, err
+			}
+			v, err := strconv.ParseUint(s, 10, 8)
+			if err != nil {
+				return ctx, fmt.Errorf(`%w %d: "%s" to uint8: %s`, ErrCannotConvert, i, s, err)
+			}
+			values = append(values, reflect.ValueOf(uint8(v)))
 		case reflect.String:
 			s, err := sd.shouldBeString(i)
 			if err != nil {
diff --git a/vendor/github.com/cucumber/godog/internal/parser/parser.go b/vendor/github.com/cucumber/godog/internal/parser/parser.go
index 3db1b5260..f607000aa 100644
--- a/vendor/github.com/cucumber/godog/internal/parser/parser.go
+++ b/vendor/github.com/cucumber/godog/internal/parser/parser.go
@@ -33,7 +33,7 @@ func ExtractFeaturePathLine(p string) (string, int) {
 	return retPath, line
 }
 
-func parseFeatureFile(fsys fs.FS, path string, newIDFunc func() string) (*models.Feature, error) {
+func parseFeatureFile(fsys fs.FS, path, dialect string, newIDFunc func() string) (*models.Feature, error) {
 	reader, err := fsys.Open(path)
 	if err != nil {
 		return nil, err
@@ -42,7 +42,7 @@ func parseFeatureFile(fsys fs.FS, path string, newIDFunc func() string) (*models
 	defer reader.Close()
 
 	var buf bytes.Buffer
-	gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc)
+	gherkinDocument, err := gherkin.ParseGherkinDocumentForLanguage(io.TeeReader(reader, &buf), dialect, newIDFunc)
 	if err != nil {
 		return nil, fmt.Errorf("%s - %v", path, err)
 	}
@@ -54,11 +54,11 @@ func parseFeatureFile(fsys fs.FS, path string, newIDFunc func() string) (*models
 	return &f, nil
 }
 
-func parseBytes(path string, feature []byte, newIDFunc func() string) (*models.Feature, error) {
+func parseBytes(path string, feature []byte, dialect string, newIDFunc func() string) (*models.Feature, error) {
 	reader := bytes.NewReader(feature)
 
 	var buf bytes.Buffer
-	gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc)
+	gherkinDocument, err := gherkin.ParseGherkinDocumentForLanguage(io.TeeReader(reader, &buf), dialect, newIDFunc)
 	if err != nil {
 		return nil, fmt.Errorf("%s - %v", path, err)
 	}
@@ -70,7 +70,7 @@ func parseBytes(path string, feature []byte, newIDFunc func() string) (*models.F
 	return &f, nil
 }
 
-func parseFeatureDir(fsys fs.FS, dir string, newIDFunc func() string) ([]*models.Feature, error) {
+func parseFeatureDir(fsys fs.FS, dir, dialect string, newIDFunc func() string) ([]*models.Feature, error) {
 	var features []*models.Feature
 	return features, fs.WalkDir(fsys, dir, func(p string, f fs.DirEntry, err error) error {
 		if err != nil {
@@ -85,7 +85,7 @@ func parseFeatureDir(fsys fs.FS, dir string, newIDFunc func() string) ([]*models
 			return nil
 		}
 
-		feat, err := parseFeatureFile(fsys, p, newIDFunc)
+		feat, err := parseFeatureFile(fsys, p, dialect, newIDFunc)
 		if err != nil {
 			return err
 		}
@@ -95,7 +95,7 @@ func parseFeatureDir(fsys fs.FS, dir string, newIDFunc func() string) ([]*models
 	})
 }
 
-func parsePath(fsys fs.FS, path string, newIDFunc func() string) ([]*models.Feature, error) {
+func parsePath(fsys fs.FS, path, dialect string, newIDFunc func() string) ([]*models.Feature, error) {
 	var features []*models.Feature
 
 	path, line := ExtractFeaturePathLine(path)
@@ -114,10 +114,10 @@ func parsePath(fsys fs.FS, path string, newIDFunc func() string) ([]*models.Feat
 	}
 
 	if fi.IsDir() {
-		return parseFeatureDir(fsys, path, newIDFunc)
+		return parseFeatureDir(fsys, path, dialect, newIDFunc)
 	}
 
-	ft, err := parseFeatureFile(fsys, path, newIDFunc)
+	ft, err := parseFeatureFile(fsys, path, dialect, newIDFunc)
 	if err != nil {
 		return features, err
 	}
@@ -146,14 +146,18 @@ func parsePath(fsys fs.FS, path string, newIDFunc func() string) ([]*models.Feat
 }
 
 // ParseFeatures ...
-func ParseFeatures(fsys fs.FS, filter string, paths []string) ([]*models.Feature, error) {
+func ParseFeatures(fsys fs.FS, filter, dialect string, paths []string) ([]*models.Feature, error) {
 	var order int
 
+	if dialect == "" {
+		dialect = gherkin.DefaultDialect
+	}
+
 	featureIdxs := make(map[string]int)
 	uniqueFeatureURI := make(map[string]*models.Feature)
 	newIDFunc := (&messages.Incrementing{}).NewId
 	for _, path := range paths {
-		feats, err := parsePath(fsys, path, newIDFunc)
+		feats, err := parsePath(fsys, path, dialect, newIDFunc)
 
 		switch {
 		case os.IsNotExist(err):
@@ -189,14 +193,18 @@ func ParseFeatures(fsys fs.FS, filter string, paths []string) ([]*models.Feature
 
 type FeatureContent = flags.Feature
 
-func ParseFromBytes(filter string, featuresInputs []FeatureContent) ([]*models.Feature, error) {
+func ParseFromBytes(filter, dialect string, featuresInputs []FeatureContent) ([]*models.Feature, error) {
 	var order int
 
+	if dialect == "" {
+		dialect = gherkin.DefaultDialect
+	}
+
 	featureIdxs := make(map[string]int)
 	uniqueFeatureURI := make(map[string]*models.Feature)
 	newIDFunc := (&messages.Incrementing{}).NewId
 	for _, f := range featuresInputs {
-		ft, err := parseBytes(f.Name, f.Contents, newIDFunc)
+		ft, err := parseBytes(f.Name, f.Contents, dialect, newIDFunc)
 		if err != nil {
 			return nil, err
 		}
diff --git a/vendor/github.com/cucumber/godog/internal/storage/storage.go b/vendor/github.com/cucumber/godog/internal/storage/storage.go
index 3fce8a8d5..72b7e86f7 100644
--- a/vendor/github.com/cucumber/godog/internal/storage/storage.go
+++ b/vendor/github.com/cucumber/godog/internal/storage/storage.go
@@ -223,7 +223,7 @@ func (s *Storage) MustGetPickleStepResult(id string) models.PickleStepResult {
 	return v.(models.PickleStepResult)
 }
 
-// MustGetPickleStepResultsByPickleID will retrieve pickle strep results by pickle id and panic on error.
+// MustGetPickleStepResultsByPickleID will retrieve pickle step results by pickle id and panic on error.
 func (s *Storage) MustGetPickleStepResultsByPickleID(pickleID string) (psrs []models.PickleStepResult) {
 	it := s.mustGet(tablePickleStepResult, tablePickleStepResultIndexPickleID, pickleID)
 	for v := it.Next(); v != nil; v = it.Next() {
@@ -233,6 +233,21 @@ func (s *Storage) MustGetPickleStepResultsByPickleID(pickleID string) (psrs []mo
 	return psrs
 }
 
+// MustGetPickleStepResultsByPickleIDUntilStep will retrieve pickle step results by pickle id
+// from 0..stepID for that pickle.
+func (s *Storage) MustGetPickleStepResultsByPickleIDUntilStep(pickleID string, untilStepID string) (psrs []models.PickleStepResult) {
+	it := s.mustGet(tablePickleStepResult, tablePickleStepResultIndexPickleID, pickleID)
+	for v := it.Next(); v != nil; v = it.Next() {
+		psr := v.(models.PickleStepResult)
+		psrs = append(psrs, psr)
+		if psr.PickleStepID == untilStepID {
+			break
+		}
+	}
+
+	return psrs
+}
+
 // MustGetPickleStepResultsByStatus will retrieve pickle strep results by status and panic on error.
 func (s *Storage) MustGetPickleStepResultsByStatus(status models.StepResultStatus) (psrs []models.PickleStepResult) {
 	it := s.mustGet(tablePickleStepResult, tablePickleStepResultIndexStatus, status)
diff --git a/vendor/github.com/cucumber/godog/run.go b/vendor/github.com/cucumber/godog/run.go
index 405aaff9d..1231d0286 100644
--- a/vendor/github.com/cucumber/godog/run.go
+++ b/vendor/github.com/cucumber/godog/run.go
@@ -33,8 +33,10 @@ const (
 	exitOptionError
 )
 
-type testSuiteInitializer func(*TestSuiteContext)
-type scenarioInitializer func(*ScenarioContext)
+type (
+	testSuiteInitializer func(*TestSuiteContext)
+	scenarioInitializer  func(*ScenarioContext)
+)
 
 type runner struct {
 	randomSeed            int64
@@ -115,6 +117,13 @@ func (r *runner) concurrent(rate int) (failed bool) {
 
 				// Copy base suite.
 				suite := *testSuiteContext.suite
+				if rate > 1 {
+					// if running concurrently, only print at end of scenario to keep
+					// scenario logs segregated
+					ffmt := ifmt.WrapOnFlush(testSuiteContext.suite.fmt)
+					suite.fmt = ffmt
+					defer ffmt.Flush()
+				}
 
 				if r.scenarioInitializer != nil {
 					sc := ScenarioContext{suite: &suite}
@@ -238,7 +247,7 @@ func runWithOptions(suiteName string, runner runner, opt Options) int {
 	opt.FS = storage.FS{FS: opt.FS}
 
 	if len(opt.FeatureContents) > 0 {
-		features, err := parser.ParseFromBytes(opt.Tags, opt.FeatureContents)
+		features, err := parser.ParseFromBytes(opt.Tags, opt.Dialect, opt.FeatureContents)
 		if err != nil {
 			fmt.Fprintln(os.Stderr, err)
 			return exitOptionError
@@ -247,7 +256,7 @@ func runWithOptions(suiteName string, runner runner, opt Options) int {
 	}
 
 	if len(opt.Paths) > 0 {
-		features, err := parser.ParseFeatures(opt.FS, opt.Tags, opt.Paths)
+		features, err := parser.ParseFeatures(opt.FS, opt.Tags, opt.Dialect, opt.Paths)
 		if err != nil {
 			fmt.Fprintln(os.Stderr, err)
 			return exitOptionError
@@ -380,7 +389,7 @@ func (ts TestSuite) RetrieveFeatures() ([]*models.Feature, error) {
 		}
 	}
 
-	return parser.ParseFeatures(opt.FS, opt.Tags, opt.Paths)
+	return parser.ParseFeatures(opt.FS, opt.Tags, opt.Dialect, opt.Paths)
 }
 
 func getDefaultOptions() (*Options, error) {
diff --git a/vendor/github.com/cucumber/godog/suite.go b/vendor/github.com/cucumber/godog/suite.go
index 9a387299c..6ca1bf53d 100644
--- a/vendor/github.com/cucumber/godog/suite.go
+++ b/vendor/github.com/cucumber/godog/suite.go
@@ -159,7 +159,9 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
 
 		// Check for any calls to Fail on dogT
 		if err == nil {
-			err = getTestingT(ctx).isFailed()
+			if t := getTestingT(ctx); t != nil {
+				err = t.isFailed()
+			}
 		}
 
 		status := StepUndefined
@@ -258,6 +260,8 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
 				},
 				Args:         match.Args,
 				HandlerValue: match.HandlerValue,
+				File:         match.File,
+				Line:         match.Line,
 				Nested:       match.Nested,
 				Undefined:    undef,
 			}
@@ -270,7 +274,7 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
 		s.storage.MustInsertPickleStepResult(sr)
 
 		s.fmt.Undefined(pickle, step, match.GetInternalStepDefinition())
-		return ctx, ErrUndefined
+		return ctx, fmt.Errorf("%w: %s", ErrUndefined, step.Text)
 	}
 
 	if scenarioErr != nil {
@@ -461,7 +465,7 @@ func (s *suite) maybeSubSteps(ctx context.Context, result interface{}) (context.
 		}
 
 		if def == nil {
-			return ctx, ErrUndefined
+			return ctx, fmt.Errorf("%w: %s", ErrUndefined, text)
 		} else {
 			ctx, err = s.runSubStep(ctx, text, def)
 			if err != nil {
@@ -530,6 +534,8 @@ func (s *suite) matchStepTextAndType(text string, stepType messages.PickleStepTy
 				},
 				Args:         args,
 				HandlerValue: h.HandlerValue,
+				File:         h.File,
+				Line:         h.Line,
 				Nested:       h.Nested,
 			}
 
@@ -609,7 +615,7 @@ func (s *suite) runPickle(pickle *messages.Pickle) (err error) {
 		s.storage.MustInsertPickleResult(pr)
 
 		s.fmt.Pickle(pickle)
-		return ErrUndefined
+		return fmt.Errorf("%w: no steps in scenario", ErrUndefined)
 	}
 
 	// Before scenario hooks are called in context of first evaluated step
diff --git a/vendor/github.com/hashicorp/go-retryablehttp/.go-version b/vendor/github.com/hashicorp/go-retryablehttp/.go-version
index 6fee2fedb..a1b6e17d6 100644
--- a/vendor/github.com/hashicorp/go-retryablehttp/.go-version
+++ b/vendor/github.com/hashicorp/go-retryablehttp/.go-version
@@ -1 +1 @@
-1.22.2
+1.23
diff --git a/vendor/github.com/hashicorp/go-retryablehttp/.golangci.yml b/vendor/github.com/hashicorp/go-retryablehttp/.golangci.yml
new file mode 100644
index 000000000..4ff1a93b0
--- /dev/null
+++ b/vendor/github.com/hashicorp/go-retryablehttp/.golangci.yml
@@ -0,0 +1,11 @@
+# Copyright (c) HashiCorp, Inc.
+# SPDX-License-Identifier: MPL-2.0
+
+linters:
+  disable-all: true
+  enable:
+    - errcheck
+    - staticcheck
+    - gosimple
+    - govet
+output_format: colored-line-number
diff --git a/vendor/github.com/hashicorp/go-retryablehttp/CODEOWNERS b/vendor/github.com/hashicorp/go-retryablehttp/CODEOWNERS
index d6dd78a2d..85b44a129 100644
--- a/vendor/github.com/hashicorp/go-retryablehttp/CODEOWNERS
+++ b/vendor/github.com/hashicorp/go-retryablehttp/CODEOWNERS
@@ -1 +1,13 @@
-* @hashicorp/go-retryablehttp-maintainers
+# Each line is a file pattern followed by one or more owners.
+# More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
+
+# Default owner
+* @hashicorp/team-ip-compliance @hashicorp/go-retryablehttp-maintainers
+
+# Add override rules below. Each line is a file/folder pattern followed by one or more owners.
+# Being an owner means those groups or individuals will be added as reviewers to PRs affecting
+# those areas of the code.
+# Examples:
+# /docs/  @docs-team
+# *.js    @js-team
+# *.go    @go-team
diff --git a/vendor/github.com/hashicorp/go-retryablehttp/Makefile b/vendor/github.com/hashicorp/go-retryablehttp/Makefile
index 525524196..07b85a132 100644
--- a/vendor/github.com/hashicorp/go-retryablehttp/Makefile
+++ b/vendor/github.com/hashicorp/go-retryablehttp/Makefile
@@ -2,7 +2,7 @@ default: test
 
 test:
 	go vet ./...
-	go test -v -race ./...
+	go test -v -race ./... -coverprofile=coverage.out
 
 updatedeps:
 	go get -f -t -u ./...
diff --git a/vendor/github.com/hashicorp/go-retryablehttp/client.go b/vendor/github.com/hashicorp/go-retryablehttp/client.go
index efee53c40..91059f7de 100644
--- a/vendor/github.com/hashicorp/go-retryablehttp/client.go
+++ b/vendor/github.com/hashicorp/go-retryablehttp/client.go
@@ -638,6 +638,23 @@ func LinearJitterBackoff(min, max time.Duration, attemptNum int, resp *http.Resp
 	return time.Duration(jitterMin * int64(attemptNum))
 }
 
+// RateLimitLinearJitterBackoff wraps the retryablehttp.LinearJitterBackoff.
+// It first checks if the response status code is http.StatusTooManyRequests
+// (HTTP Code 429) or http.StatusServiceUnavailable (HTTP Code 503). If it is
+// and the response contains a Retry-After response header, it will wait the
+// amount of time specified by the header. Otherwise, this calls
+// LinearJitterBackoff.
+func RateLimitLinearJitterBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
+	if resp != nil {
+		if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable {
+			if sleep, ok := parseRetryAfterHeader(resp.Header["Retry-After"]); ok {
+				return sleep
+			}
+		}
+	}
+	return LinearJitterBackoff(min, max, attemptNum, resp)
+}
+
 // PassthroughErrorHandler is an ErrorHandler that directly passes through the
 // values from the net/http library for the final request. The body is not
 // closed.
@@ -870,11 +887,13 @@ func (c *Client) Head(url string) (*http.Response, error) {
 }
 
 // Post is a shortcut for doing a POST request without making a new client.
+// The bodyType parameter sets the "Content-Type" header of the request.
 func Post(url, bodyType string, body interface{}) (*http.Response, error) {
 	return defaultClient.Post(url, bodyType, body)
 }
 
 // Post is a convenience method for doing simple POST requests.
+// The bodyType parameter sets the "Content-Type" header of the request.
 func (c *Client) Post(url, bodyType string, body interface{}) (*http.Response, error) {
 	req, err := NewRequest("POST", url, body)
 	if err != nil {
diff --git a/vendor/github.com/invopop/jsonschema/.gitignore b/vendor/github.com/invopop/jsonschema/.gitignore
new file mode 100644
index 000000000..8ef0e14fc
--- /dev/null
+++ b/vendor/github.com/invopop/jsonschema/.gitignore
@@ -0,0 +1,2 @@
+vendor/
+.idea/
diff --git a/vendor/github.com/invopop/jsonschema/.golangci.yml b/vendor/github.com/invopop/jsonschema/.golangci.yml
new file mode 100644
index 000000000..b89b2e124
--- /dev/null
+++ b/vendor/github.com/invopop/jsonschema/.golangci.yml
@@ -0,0 +1,69 @@
+run:
+  tests: true
+  max-same-issues: 50
+
+output:
+  print-issued-lines: false
+
+linters:
+  enable:
+    - gocyclo
+    - gocritic
+    - goconst
+    - dupl
+    - unconvert
+    - goimports
+    - unused
+    - govet
+    - nakedret
+    - errcheck
+    - revive
+    - ineffassign
+    - goconst
+    - unparam
+    - gofmt
+
+linters-settings:
+  vet:
+    check-shadowing: true
+    use-installed-packages: true
+  dupl:
+    threshold: 100
+  goconst:
+    min-len: 8
+    min-occurrences: 3
+  gocyclo:
+    min-complexity: 20
+  gocritic:
+    disabled-checks:
+      - ifElseChain
+  gofmt:
+    rewrite-rules:
+      - pattern: "interface{}"
+        replacement: "any"
+      - pattern: "a[b:len(a)]"
+        replacement: "a[b:]"
+
+issues:
+  max-per-linter: 0
+  max-same: 0
+  exclude-dirs:
+    - resources
+    - old
+  exclude-files:
+    - cmd/protopkg/main.go
+  exclude-use-default: false
+  exclude:
+    # Captured by errcheck.
+    - "^(G104|G204):"
+    # Very commonly not checked.
+    - 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*Print(f|ln|)|os\.(Un)?Setenv). is not checked'
+    # Weird error only seen on Kochiku...
+    - "internal error: no range for"
+    - 'exported method `.*\.(MarshalJSON|UnmarshalJSON|URN|Payload|GoString|Close|Provides|Requires|ExcludeFromHash|MarshalText|UnmarshalText|Description|Check|Poll|Severity)` should have comment or be unexported'
+    - "composite literal uses unkeyed fields"
+    - 'declaration of "err" shadows declaration'
+    - "by other packages, and that stutters"
+    - "Potential file inclusion via variable"
+    - "at least one file in a package should have a package comment"
+    - "bad syntax for struct tag pair"
diff --git a/vendor/github.com/invopop/jsonschema/COPYING b/vendor/github.com/invopop/jsonschema/COPYING
new file mode 100644
index 000000000..2993ec085
--- /dev/null
+++ b/vendor/github.com/invopop/jsonschema/COPYING
@@ -0,0 +1,19 @@
+Copyright (C) 2014 Alec Thomas
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/invopop/jsonschema/README.md b/vendor/github.com/invopop/jsonschema/README.md
new file mode 100644
index 000000000..27b362e1d
--- /dev/null
+++ b/vendor/github.com/invopop/jsonschema/README.md
@@ -0,0 +1,374 @@
+# Go JSON Schema Reflection
+
+[![Lint](https://github.com/invopop/jsonschema/actions/workflows/lint.yaml/badge.svg)](https://github.com/invopop/jsonschema/actions/workflows/lint.yaml)
+[![Test Go](https://github.com/invopop/jsonschema/actions/workflows/test.yaml/badge.svg)](https://github.com/invopop/jsonschema/actions/workflows/test.yaml)
+[![Go Report Card](https://goreportcard.com/badge/github.com/invopop/jsonschema)](https://goreportcard.com/report/github.com/invopop/jsonschema)
+[![GoDoc](https://godoc.org/github.com/invopop/jsonschema?status.svg)](https://godoc.org/github.com/invopop/jsonschema)
+[![codecov](https://codecov.io/gh/invopop/jsonschema/graph/badge.svg?token=JMEB8W8GNZ)](https://codecov.io/gh/invopop/jsonschema)
+![Latest Tag](https://img.shields.io/github/v/tag/invopop/jsonschema)
+
+This package can be used to generate [JSON Schemas](http://json-schema.org/latest/json-schema-validation.html) from Go types through reflection.
+
+- Supports arbitrarily complex types, including `interface{}`, maps, slices, etc.
+- Supports json-schema features such as minLength, maxLength, pattern, format, etc.
+- Supports simple string and numeric enums.
+- Supports custom property fields via the `jsonschema_extras` struct tag.
+
+This repository is a fork of the original [jsonschema](https://github.com/alecthomas/jsonschema) by [@alecthomas](https://github.com/alecthomas). At [Invopop](https://invopop.com) we use jsonschema as a cornerstone in our [GOBL library](https://github.com/invopop/gobl), and wanted to be able to continue building and adding features without taking up Alec's time. There have been a few significant changes that probably mean this version is a not compatible with with Alec's:
+
+- The original was stuck on the draft-04 version of JSON Schema, we've now moved to the latest JSON Schema Draft 2020-12.
+- Schema IDs are added automatically from the current Go package's URL in order to be unique, and can be disabled with the `Anonymous` option.
+- Support for the `FullyQualifyTypeName` option has been removed. If you have conflicts, you should use multiple schema files with different IDs, set the `DoNotReference` option to true to hide definitions completely, or add your own naming strategy using the `Namer` property.
+- Support for `yaml` tags and related options has been dropped for the sake of simplification. There were a [few inconsistencies](https://github.com/invopop/jsonschema/pull/21) around this that have now been fixed.
+
+## Versions
+
+This project is still under v0 scheme, as per Go convention, breaking changes are likely. Please pin go modules to version tags or branches, and reach out if you think something can be improved.
+
+Go version >= 1.18 is required as generics are now being used.
+
+## Example
+
+The following Go type:
+
+```go
+type TestUser struct {
+  ID            int                    `json:"id"`
+  Name          string                 `json:"name" jsonschema:"title=the name,description=The name of a friend,example=joe,example=lucy,default=alex"`
+  Friends       []int                  `json:"friends,omitempty" jsonschema_description:"The list of IDs, omitted when empty"`
+  Tags          map[string]interface{} `json:"tags,omitempty" jsonschema_extras:"a=b,foo=bar,foo=bar1"`
+  BirthDate     time.Time              `json:"birth_date,omitempty" jsonschema:"oneof_required=date"`
+  YearOfBirth   string                 `json:"year_of_birth,omitempty" jsonschema:"oneof_required=year"`
+  Metadata      interface{}            `json:"metadata,omitempty" jsonschema:"oneof_type=string;array"`
+  FavColor      string                 `json:"fav_color,omitempty" jsonschema:"enum=red,enum=green,enum=blue"`
+}
+```
+
+Results in following JSON Schema:
+
+```go
+jsonschema.Reflect(&TestUser{})
+```
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://github.com/invopop/jsonschema_test/test-user",
+  "$ref": "#/$defs/TestUser",
+  "$defs": {
+    "TestUser": {
+      "oneOf": [
+        {
+          "required": ["birth_date"],
+          "title": "date"
+        },
+        {
+          "required": ["year_of_birth"],
+          "title": "year"
+        }
+      ],
+      "properties": {
+        "id": {
+          "type": "integer"
+        },
+        "name": {
+          "type": "string",
+          "title": "the name",
+          "description": "The name of a friend",
+          "default": "alex",
+          "examples": ["joe", "lucy"]
+        },
+        "friends": {
+          "items": {
+            "type": "integer"
+          },
+          "type": "array",
+          "description": "The list of IDs, omitted when empty"
+        },
+        "tags": {
+          "type": "object",
+          "a": "b",
+          "foo": ["bar", "bar1"]
+        },
+        "birth_date": {
+          "type": "string",
+          "format": "date-time"
+        },
+        "year_of_birth": {
+          "type": "string"
+        },
+        "metadata": {
+          "oneOf": [
+            {
+              "type": "string"
+            },
+            {
+              "type": "array"
+            }
+          ]
+        },
+        "fav_color": {
+          "type": "string",
+          "enum": ["red", "green", "blue"]
+        }
+      },
+      "additionalProperties": false,
+      "type": "object",
+      "required": ["id", "name"]
+    }
+  }
+}
+```
+
+## YAML
+
+Support for `yaml` tags has now been removed. If you feel very strongly about this, we've opened a discussion to hear your comments: https://github.com/invopop/jsonschema/discussions/28
+
+The recommended approach if you need to deal with YAML data is to first convert to JSON. The [invopop/yaml](https://github.com/invopop/yaml) library will make this trivial.
+
+## Configurable behaviour
+
+The behaviour of the schema generator can be altered with parameters when a `jsonschema.Reflector`
+instance is created.
+
+### ExpandedStruct
+
+If set to `true`, makes the top level struct not to reference itself in the definitions. But type passed should be a struct type.
+
+eg.
+
+```go
+type GrandfatherType struct {
+	FamilyName string `json:"family_name" jsonschema:"required"`
+}
+
+type SomeBaseType struct {
+	SomeBaseProperty int `json:"some_base_property"`
+	// The jsonschema required tag is nonsensical for private and ignored properties.
+	// Their presence here tests that the fields *will not* be required in the output
+	// schema, even if they are tagged required.
+	somePrivateBaseProperty            string `json:"i_am_private" jsonschema:"required"`
+	SomeIgnoredBaseProperty            string `json:"-" jsonschema:"required"`
+	SomeSchemaIgnoredProperty          string `jsonschema:"-,required"`
+	SomeUntaggedBaseProperty           bool   `jsonschema:"required"`
+	someUnexportedUntaggedBaseProperty bool
+	Grandfather                        GrandfatherType `json:"grand"`
+}
+```
+
+will output:
+
+```json
+{
+  "$schema": "http://json-schema.org/draft/2020-12/schema",
+  "required": ["some_base_property", "grand", "SomeUntaggedBaseProperty"],
+  "properties": {
+    "SomeUntaggedBaseProperty": {
+      "type": "boolean"
+    },
+    "grand": {
+      "$schema": "http://json-schema.org/draft/2020-12/schema",
+      "$ref": "#/definitions/GrandfatherType"
+    },
+    "some_base_property": {
+      "type": "integer"
+    }
+  },
+  "type": "object",
+  "$defs": {
+    "GrandfatherType": {
+      "required": ["family_name"],
+      "properties": {
+        "family_name": {
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "type": "object"
+    }
+  }
+}
+```
+
+### Using Go Comments
+
+Writing a good schema with descriptions inside tags can become cumbersome and tedious, especially if you already have some Go comments around your types and field definitions. If you'd like to take advantage of these existing comments, you can use the `AddGoComments(base, path string)` method that forms part of the reflector to parse your go files and automatically generate a dictionary of Go import paths, types, and fields, to individual comments. These will then be used automatically as description fields, and can be overridden with a manual definition if needed.
+
+Take a simplified example of a User struct which for the sake of simplicity we assume is defined inside this package:
+
+```go
+package main
+
+// User is used as a base to provide tests for comments.
+type User struct {
+	// Unique sequential identifier.
+	ID int `json:"id" jsonschema:"required"`
+	// Name of the user
+	Name string `json:"name"`
+}
+```
+
+To get the comments provided into your JSON schema, use a regular `Reflector` and add the go code using an import module URL and path. Fully qualified go module paths cannot be determined reliably by the `go/parser` library, so we need to introduce this manually:
+
+```go
+r := new(Reflector)
+if err := r.AddGoComments("github.com/invopop/jsonschema", "./"); err != nil {
+  // deal with error
+}
+s := r.Reflect(&User{})
+// output
+```
+
+Expect the results to be similar to:
+
+```json
+{
+  "$schema": "http://json-schema.org/draft/2020-12/schema",
+  "$ref": "#/$defs/User",
+  "$defs": {
+    "User": {
+      "required": ["id"],
+      "properties": {
+        "id": {
+          "type": "integer",
+          "description": "Unique sequential identifier."
+        },
+        "name": {
+          "type": "string",
+          "description": "Name of the user"
+        }
+      },
+      "additionalProperties": false,
+      "type": "object",
+      "description": "User is used as a base to provide tests for comments."
+    }
+  }
+}
+```
+
+### Custom Key Naming
+
+In some situations, the keys actually used to write files are different from Go structs'.
+
+This is often the case when writing a configuration file to YAML or JSON from a Go struct, or when returning a JSON response for a Web API: APIs typically use snake_case, while Go uses PascalCase.
+
+You can pass a `func(string) string` function to `Reflector`'s `KeyNamer` option to map Go field names to JSON key names and reflect the aforementioned transformations, without having to specify `json:"..."` on every struct field.
+
+For example, consider the following struct
+
+```go
+type User struct {
+  GivenName       string
+  PasswordSalted  []byte `json:"salted_password"`
+}
+```
+
+We can transform field names to snake_case in the generated JSON schema:
+
+```go
+r := new(jsonschema.Reflector)
+r.KeyNamer = strcase.SnakeCase // from package github.com/stoewer/go-strcase
+
+r.Reflect(&User{})
+```
+
+Will yield
+
+```diff
+  {
+    "$schema": "http://json-schema.org/draft/2020-12/schema",
+    "$ref": "#/$defs/User",
+    "$defs": {
+      "User": {
+        "properties": {
+-         "GivenName": {
++         "given_name": {
+            "type": "string"
+          },
+          "salted_password": {
+            "type": "string",
+            "contentEncoding": "base64"
+          }
+        },
+        "additionalProperties": false,
+        "type": "object",
+-       "required": ["GivenName", "salted_password"]
++       "required": ["given_name", "salted_password"]
+      }
+    }
+  }
+```
+
+As you can see, if a field name has a `json:""` tag set, the `key` argument to `KeyNamer` will have the value of that tag.
+
+### Custom Type Definitions
+
+Sometimes it can be useful to have custom JSON Marshal and Unmarshal methods in your structs that automatically convert for example a string into an object.
+
+This library will recognize and attempt to call four different methods that help you adjust schemas to your specific needs:
+
+- `JSONSchema() *Schema` - will prevent auto-generation of the schema so that you can provide your own definition.
+- `JSONSchemaExtend(schema *jsonschema.Schema)` - will be called _after_ the schema has been generated, allowing you to add or manipulate the fields easily.
+- `JSONSchemaAlias() any` - is called when reflecting the type of object and allows for an alternative to be used instead.
+- `JSONSchemaProperty(prop string) any` - will be called for every property inside a struct giving you the chance to provide an alternative object to convert into a schema.
+
+Note that all of these methods **must** be defined on a non-pointer object for them to be called.
+
+Take the following simplified example of a `CompactDate` that only includes the Year and Month:
+
+```go
+type CompactDate struct {
+	Year  int
+	Month int
+}
+
+func (d *CompactDate) UnmarshalJSON(data []byte) error {
+  if len(data) != 9 {
+    return errors.New("invalid compact date length")
+  }
+  var err error
+  d.Year, err = strconv.Atoi(string(data[1:5]))
+  if err != nil {
+    return err
+  }
+  d.Month, err = strconv.Atoi(string(data[7:8]))
+  if err != nil {
+    return err
+  }
+  return nil
+}
+
+func (d *CompactDate) MarshalJSON() ([]byte, error) {
+  buf := new(bytes.Buffer)
+  buf.WriteByte('"')
+  buf.WriteString(fmt.Sprintf("%d-%02d", d.Year, d.Month))
+  buf.WriteByte('"')
+  return buf.Bytes(), nil
+}
+
+func (CompactDate) JSONSchema() *Schema {
+	return &Schema{
+		Type:        "string",
+		Title:       "Compact Date",
+		Description: "Short date that only includes year and month",
+		Pattern:     "^[0-9]{4}-[0-1][0-9]$",
+	}
+}
+```
+
+The resulting schema generated for this struct would look like:
+
+```json
+{
+  "$schema": "http://json-schema.org/draft/2020-12/schema",
+  "$ref": "#/$defs/CompactDate",
+  "$defs": {
+    "CompactDate": {
+      "pattern": "^[0-9]{4}-[0-1][0-9]$",
+      "type": "string",
+      "title": "Compact Date",
+      "description": "Short date that only includes year and month"
+    }
+  }
+}
+```
diff --git a/vendor/github.com/invopop/jsonschema/id.go b/vendor/github.com/invopop/jsonschema/id.go
new file mode 100644
index 000000000..73fafb38d
--- /dev/null
+++ b/vendor/github.com/invopop/jsonschema/id.go
@@ -0,0 +1,76 @@
+package jsonschema
+
+import (
+	"errors"
+	"fmt"
+	"net/url"
+	"strings"
+)
+
+// ID represents a Schema ID type which should always be a URI.
+// See draft-bhutton-json-schema-00 section 8.2.1
+type ID string
+
+// EmptyID is used to explicitly define an ID with no value.
+const EmptyID ID = ""
+
+// Validate is used to check if the ID looks like a proper schema.
+// This is done by parsing the ID as a URL and checking it has all the
+// relevant parts.
+func (id ID) Validate() error {
+	u, err := url.Parse(id.String())
+	if err != nil {
+		return fmt.Errorf("invalid URL: %w", err)
+	}
+	if u.Hostname() == "" {
+		return errors.New("missing hostname")
+	}
+	if !strings.Contains(u.Hostname(), ".") {
+		return errors.New("hostname does not look valid")
+	}
+	if u.Path == "" {
+		return errors.New("path is expected")
+	}
+	if u.Scheme != "https" && u.Scheme != "http" {
+		return errors.New("unexpected schema")
+	}
+	return nil
+}
+
+// Anchor sets the anchor part of the schema URI.
+func (id ID) Anchor(name string) ID {
+	b := id.Base()
+	return ID(b.String() + "#" + name)
+}
+
+// Def adds or replaces a definition identifier.
+func (id ID) Def(name string) ID {
+	b := id.Base()
+	return ID(b.String() + "#/$defs/" + name)
+}
+
+// Add appends the provided path to the id, and removes any
+// anchor data that might be there.
+func (id ID) Add(path string) ID {
+	b := id.Base()
+	if !strings.HasPrefix(path, "/") {
+		path = "/" + path
+	}
+	return ID(b.String() + path)
+}
+
+// Base removes any anchor information from the schema
+func (id ID) Base() ID {
+	s := id.String()
+	i := strings.LastIndex(s, "#")
+	if i != -1 {
+		s = s[0:i]
+	}
+	s = strings.TrimRight(s, "/")
+	return ID(s)
+}
+
+// String provides string version of ID
+func (id ID) String() string {
+	return string(id)
+}
diff --git a/vendor/github.com/invopop/jsonschema/reflect.go b/vendor/github.com/invopop/jsonschema/reflect.go
new file mode 100644
index 000000000..73ce7e465
--- /dev/null
+++ b/vendor/github.com/invopop/jsonschema/reflect.go
@@ -0,0 +1,1148 @@
+// Package jsonschema uses reflection to generate JSON Schemas from Go types [1].
+//
+// If json tags are present on struct fields, they will be used to infer
+// property names and if a property is required (omitempty is present).
+//
+// [1] http://json-schema.org/latest/json-schema-validation.html
+package jsonschema
+
+import (
+	"bytes"
+	"encoding/json"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// customSchemaImpl is used to detect if the type provides it's own
+// custom Schema Type definition to use instead. Very useful for situations
+// where there are custom JSON Marshal and Unmarshal methods.
+type customSchemaImpl interface {
+	JSONSchema() *Schema
+}
+
+// Function to be run after the schema has been generated.
+// this will let you modify a schema afterwards
+type extendSchemaImpl interface {
+	JSONSchemaExtend(*Schema)
+}
+
+// If the object to be reflected defines a `JSONSchemaAlias` method, its type will
+// be used instead of the original type.
+type aliasSchemaImpl interface {
+	JSONSchemaAlias() any
+}
+
+// If an object to be reflected defines a `JSONSchemaPropertyAlias` method,
+// it will be called for each property to determine if another object
+// should be used for the contents.
+type propertyAliasSchemaImpl interface {
+	JSONSchemaProperty(prop string) any
+}
+
+var customAliasSchema = reflect.TypeOf((*aliasSchemaImpl)(nil)).Elem()
+var customPropertyAliasSchema = reflect.TypeOf((*propertyAliasSchemaImpl)(nil)).Elem()
+
+var customType = reflect.TypeOf((*customSchemaImpl)(nil)).Elem()
+var extendType = reflect.TypeOf((*extendSchemaImpl)(nil)).Elem()
+
+// customSchemaGetFieldDocString
+type customSchemaGetFieldDocString interface {
+	GetFieldDocString(fieldName string) string
+}
+
+type customGetFieldDocString func(fieldName string) string
+
+var customStructGetFieldDocString = reflect.TypeOf((*customSchemaGetFieldDocString)(nil)).Elem()
+
+// Reflect reflects to Schema from a value using the default Reflector
+func Reflect(v any) *Schema {
+	return ReflectFromType(reflect.TypeOf(v))
+}
+
+// ReflectFromType generates root schema using the default Reflector
+func ReflectFromType(t reflect.Type) *Schema {
+	r := &Reflector{}
+	return r.ReflectFromType(t)
+}
+
+// A Reflector reflects values into a Schema.
+type Reflector struct {
+	// BaseSchemaID defines the URI that will be used as a base to determine Schema
+	// IDs for models. For example, a base Schema ID of `https://invopop.com/schemas`
+	// when defined with a struct called `User{}`, will result in a schema with an
+	// ID set to `https://invopop.com/schemas/user`.
+	//
+	// If no `BaseSchemaID` is provided, we'll take the type's complete package path
+	// and use that as a base instead. Set `Anonymous` to try if you do not want to
+	// include a schema ID.
+	BaseSchemaID ID
+
+	// Anonymous when true will hide the auto-generated Schema ID and provide what is
+	// known as an "anonymous schema". As a rule, this is not recommended.
+	Anonymous bool
+
+	// AssignAnchor when true will use the original struct's name as an anchor inside
+	// every definition, including the root schema. These can be useful for having a
+	// reference to the original struct's name in CamelCase instead of the snake-case used
+	// by default for URI compatibility.
+	//
+	// Anchors do not appear to be widely used out in the wild, so at this time the
+	// anchors themselves will not be used inside generated schema.
+	AssignAnchor bool
+
+	// AllowAdditionalProperties will cause the Reflector to generate a schema
+	// without additionalProperties set to 'false' for all struct types. This means
+	// the presence of additional keys in JSON objects will not cause validation
+	// to fail. Note said additional keys will simply be dropped when the
+	// validated JSON is unmarshaled.
+	AllowAdditionalProperties bool
+
+	// RequiredFromJSONSchemaTags will cause the Reflector to generate a schema
+	// that requires any key tagged with `jsonschema:required`, overriding the
+	// default of requiring any key *not* tagged with `json:,omitempty`.
+	RequiredFromJSONSchemaTags bool
+
+	// Do not reference definitions. This will remove the top-level $defs map and
+	// instead cause the entire structure of types to be output in one tree. The
+	// list of type definitions (`$defs`) will not be included.
+	DoNotReference bool
+
+	// ExpandedStruct when true will include the reflected type's definition in the
+	// root as opposed to a definition with a reference.
+	ExpandedStruct bool
+
+	// FieldNameTag will change the tag used to get field names. json tags are used by default.
+	FieldNameTag string
+
+	// IgnoredTypes defines a slice of types that should be ignored in the schema,
+	// switching to just allowing additional properties instead.
+	IgnoredTypes []any
+
+	// Lookup allows a function to be defined that will provide a custom mapping of
+	// types to Schema IDs. This allows existing schema documents to be referenced
+	// by their ID instead of being embedded into the current schema definitions.
+	// Reflected types will never be pointers, only underlying elements.
+	Lookup func(reflect.Type) ID
+
+	// Mapper is a function that can be used to map custom Go types to jsonschema schemas.
+	Mapper func(reflect.Type) *Schema
+
+	// Namer allows customizing of type names. The default is to use the type's name
+	// provided by the reflect package.
+	Namer func(reflect.Type) string
+
+	// KeyNamer allows customizing of key names.
+	// The default is to use the key's name as is, or the json tag if present.
+	// If a json tag is present, KeyNamer will receive the tag's name as an argument, not the original key name.
+	KeyNamer func(string) string
+
+	// AdditionalFields allows adding structfields for a given type
+	AdditionalFields func(reflect.Type) []reflect.StructField
+
+	// LookupComment allows customizing comment lookup. Given a reflect.Type and optionally
+	// a field name, it should return the comment string associated with this type or field.
+	//
+	// If the field name is empty, it should return the type's comment; otherwise, the field's
+	// comment should be returned. If no comment is found, an empty string should be returned.
+	//
+	// When set, this function is called before the below CommentMap lookup mechanism. However,
+	// if it returns an empty string, the CommentMap is still consulted.
+	LookupComment func(reflect.Type, string) string
+
+	// CommentMap is a dictionary of fully qualified go types and fields to comment
+	// strings that will be used if a description has not already been provided in
+	// the tags. Types and fields are added to the package path using "." as a
+	// separator.
+	//
+	// Type descriptions should be defined like:
+	//
+	//   map[string]string{"github.com/invopop/jsonschema.Reflector": "A Reflector reflects values into a Schema."}
+	//
+	// And Fields defined as:
+	//
+	//   map[string]string{"github.com/invopop/jsonschema.Reflector.DoNotReference": "Do not reference definitions."}
+	//
+	// See also: AddGoComments, LookupComment
+	CommentMap map[string]string
+}
+
+// Reflect reflects to Schema from a value.
+func (r *Reflector) Reflect(v any) *Schema {
+	return r.ReflectFromType(reflect.TypeOf(v))
+}
+
+// ReflectFromType generates root schema
+func (r *Reflector) ReflectFromType(t reflect.Type) *Schema {
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem() // re-assign from pointer
+	}
+
+	name := r.typeName(t)
+
+	s := new(Schema)
+	definitions := Definitions{}
+	s.Definitions = definitions
+	bs := r.reflectTypeToSchemaWithID(definitions, t)
+	if r.ExpandedStruct {
+		*s = *definitions[name]
+		delete(definitions, name)
+	} else {
+		*s = *bs
+	}
+
+	// Attempt to set the schema ID
+	if !r.Anonymous && s.ID == EmptyID {
+		baseSchemaID := r.BaseSchemaID
+		if baseSchemaID == EmptyID {
+			id := ID("https://" + t.PkgPath())
+			if err := id.Validate(); err == nil {
+				// it's okay to silently ignore URL errors
+				baseSchemaID = id
+			}
+		}
+		if baseSchemaID != EmptyID {
+			s.ID = baseSchemaID.Add(ToSnakeCase(name))
+		}
+	}
+
+	s.Version = Version
+	if !r.DoNotReference {
+		s.Definitions = definitions
+	}
+
+	return s
+}
+
+// Available Go defined types for JSON Schema Validation.
+// RFC draft-wright-json-schema-validation-00, section 7.3
+var (
+	timeType = reflect.TypeOf(time.Time{}) // date-time RFC section 7.3.1
+	ipType   = reflect.TypeOf(net.IP{})    // ipv4 and ipv6 RFC section 7.3.4, 7.3.5
+	uriType  = reflect.TypeOf(url.URL{})   // uri RFC section 7.3.6
+)
+
+// Byte slices will be encoded as base64
+var byteSliceType = reflect.TypeOf([]byte(nil))
+
+// Except for json.RawMessage
+var rawMessageType = reflect.TypeOf(json.RawMessage{})
+
+// Go code generated from protobuf enum types should fulfil this interface.
+type protoEnum interface {
+	EnumDescriptor() ([]byte, []int)
+}
+
+var protoEnumType = reflect.TypeOf((*protoEnum)(nil)).Elem()
+
+// SetBaseSchemaID is a helper use to be able to set the reflectors base
+// schema ID from a string as opposed to then ID instance.
+func (r *Reflector) SetBaseSchemaID(id string) {
+	r.BaseSchemaID = ID(id)
+}
+
+func (r *Reflector) refOrReflectTypeToSchema(definitions Definitions, t reflect.Type) *Schema {
+	id := r.lookupID(t)
+	if id != EmptyID {
+		return &Schema{
+			Ref: id.String(),
+		}
+	}
+
+	// Already added to definitions?
+	if def := r.refDefinition(definitions, t); def != nil {
+		return def
+	}
+
+	return r.reflectTypeToSchemaWithID(definitions, t)
+}
+
+func (r *Reflector) reflectTypeToSchemaWithID(defs Definitions, t reflect.Type) *Schema {
+	s := r.reflectTypeToSchema(defs, t)
+	if s != nil {
+		if r.Lookup != nil {
+			id := r.Lookup(t)
+			if id != EmptyID {
+				s.ID = id
+			}
+		}
+	}
+	return s
+}
+
+func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) *Schema {
+	// only try to reflect non-pointers
+	if t.Kind() == reflect.Ptr {
+		return r.refOrReflectTypeToSchema(definitions, t.Elem())
+	}
+
+	// Check if the there is an alias method that provides an object
+	// that we should use instead of this one.
+	if t.Implements(customAliasSchema) {
+		v := reflect.New(t)
+		o := v.Interface().(aliasSchemaImpl)
+		t = reflect.TypeOf(o.JSONSchemaAlias())
+		return r.refOrReflectTypeToSchema(definitions, t)
+	}
+
+	// Do any pre-definitions exist?
+	if r.Mapper != nil {
+		if t := r.Mapper(t); t != nil {
+			return t
+		}
+	}
+	if rt := r.reflectCustomSchema(definitions, t); rt != nil {
+		return rt
+	}
+
+	// Prepare a base to which details can be added
+	st := new(Schema)
+
+	// jsonpb will marshal protobuf enum options as either strings or integers.
+	// It will unmarshal either.
+	if t.Implements(protoEnumType) {
+		st.OneOf = []*Schema{
+			{Type: "string"},
+			{Type: "integer"},
+		}
+		return st
+	}
+
+	// Defined format types for JSON Schema Validation
+	// RFC draft-wright-json-schema-validation-00, section 7.3
+	// TODO email RFC section 7.3.2, hostname RFC section 7.3.3, uriref RFC section 7.3.7
+	if t == ipType {
+		// TODO differentiate ipv4 and ipv6 RFC section 7.3.4, 7.3.5
+		st.Type = "string"
+		st.Format = "ipv4"
+		return st
+	}
+
+	switch t.Kind() {
+	case reflect.Struct:
+		r.reflectStruct(definitions, t, st)
+
+	case reflect.Slice, reflect.Array:
+		r.reflectSliceOrArray(definitions, t, st)
+
+	case reflect.Map:
+		r.reflectMap(definitions, t, st)
+
+	case reflect.Interface:
+		// empty
+
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		st.Type = "integer"
+
+	case reflect.Float32, reflect.Float64:
+		st.Type = "number"
+
+	case reflect.Bool:
+		st.Type = "boolean"
+
+	case reflect.String:
+		st.Type = "string"
+
+	default:
+		panic("unsupported type " + t.String())
+	}
+
+	r.reflectSchemaExtend(definitions, t, st)
+
+	// Always try to reference the definition which may have just been created
+	if def := r.refDefinition(definitions, t); def != nil {
+		return def
+	}
+
+	return st
+}
+
+func (r *Reflector) reflectCustomSchema(definitions Definitions, t reflect.Type) *Schema {
+	if t.Kind() == reflect.Ptr {
+		return r.reflectCustomSchema(definitions, t.Elem())
+	}
+
+	if t.Implements(customType) {
+		v := reflect.New(t)
+		o := v.Interface().(customSchemaImpl)
+		st := o.JSONSchema()
+		r.addDefinition(definitions, t, st)
+		if ref := r.refDefinition(definitions, t); ref != nil {
+			return ref
+		}
+		return st
+	}
+
+	return nil
+}
+
+func (r *Reflector) reflectSchemaExtend(definitions Definitions, t reflect.Type, s *Schema) *Schema {
+	if t.Implements(extendType) {
+		v := reflect.New(t)
+		o := v.Interface().(extendSchemaImpl)
+		o.JSONSchemaExtend(s)
+		if ref := r.refDefinition(definitions, t); ref != nil {
+			return ref
+		}
+	}
+
+	return s
+}
+
+func (r *Reflector) reflectSliceOrArray(definitions Definitions, t reflect.Type, st *Schema) {
+	if t == rawMessageType {
+		return
+	}
+
+	r.addDefinition(definitions, t, st)
+
+	if st.Description == "" {
+		st.Description = r.lookupComment(t, "")
+	}
+
+	if t.Kind() == reflect.Array {
+		l := uint64(t.Len())
+		st.MinItems = &l
+		st.MaxItems = &l
+	}
+	if t.Kind() == reflect.Slice && t.Elem() == byteSliceType.Elem() {
+		st.Type = "string"
+		// NOTE: ContentMediaType is not set here
+		st.ContentEncoding = "base64"
+	} else {
+		st.Type = "array"
+		st.Items = r.refOrReflectTypeToSchema(definitions, t.Elem())
+	}
+}
+
+func (r *Reflector) reflectMap(definitions Definitions, t reflect.Type, st *Schema) {
+	r.addDefinition(definitions, t, st)
+
+	st.Type = "object"
+	if st.Description == "" {
+		st.Description = r.lookupComment(t, "")
+	}
+
+	switch t.Key().Kind() {
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		st.PatternProperties = map[string]*Schema{
+			"^[0-9]+$": r.refOrReflectTypeToSchema(definitions, t.Elem()),
+		}
+		st.AdditionalProperties = FalseSchema
+		return
+	}
+	if t.Elem().Kind() != reflect.Interface {
+		st.AdditionalProperties = r.refOrReflectTypeToSchema(definitions, t.Elem())
+	}
+}
+
+// Reflects a struct to a JSON Schema type.
+func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type, s *Schema) {
+	// Handle special types
+	switch t {
+	case timeType: // date-time RFC section 7.3.1
+		s.Type = "string"
+		s.Format = "date-time"
+		return
+	case uriType: // uri RFC section 7.3.6
+		s.Type = "string"
+		s.Format = "uri"
+		return
+	}
+
+	r.addDefinition(definitions, t, s)
+	s.Type = "object"
+	s.Properties = NewProperties()
+	s.Description = r.lookupComment(t, "")
+	if r.AssignAnchor {
+		s.Anchor = t.Name()
+	}
+	if !r.AllowAdditionalProperties && s.AdditionalProperties == nil {
+		s.AdditionalProperties = FalseSchema
+	}
+
+	ignored := false
+	for _, it := range r.IgnoredTypes {
+		if reflect.TypeOf(it) == t {
+			ignored = true
+			break
+		}
+	}
+	if !ignored {
+		r.reflectStructFields(s, definitions, t)
+	}
+}
+
+func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t reflect.Type) {
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+	}
+	if t.Kind() != reflect.Struct {
+		return
+	}
+
+	var getFieldDocString customGetFieldDocString
+	if t.Implements(customStructGetFieldDocString) {
+		v := reflect.New(t)
+		o := v.Interface().(customSchemaGetFieldDocString)
+		getFieldDocString = o.GetFieldDocString
+	}
+
+	customPropertyMethod := func(string) any {
+		return nil
+	}
+	if t.Implements(customPropertyAliasSchema) {
+		v := reflect.New(t)
+		o := v.Interface().(propertyAliasSchemaImpl)
+		customPropertyMethod = o.JSONSchemaProperty
+	}
+
+	handleField := func(f reflect.StructField) {
+		name, shouldEmbed, required, nullable := r.reflectFieldName(f)
+		// if anonymous and exported type should be processed recursively
+		// current type should inherit properties of anonymous one
+		if name == "" {
+			if shouldEmbed {
+				r.reflectStructFields(st, definitions, f.Type)
+			}
+			return
+		}
+
+		// If a JSONSchemaAlias(prop string) method is defined, attempt to use
+		// the provided object's type instead of the field's type.
+		var property *Schema
+		if alias := customPropertyMethod(name); alias != nil {
+			property = r.refOrReflectTypeToSchema(definitions, reflect.TypeOf(alias))
+		} else {
+			property = r.refOrReflectTypeToSchema(definitions, f.Type)
+		}
+
+		property.structKeywordsFromTags(f, st, name)
+		if property.Description == "" {
+			property.Description = r.lookupComment(t, f.Name)
+		}
+		if getFieldDocString != nil {
+			property.Description = getFieldDocString(f.Name)
+		}
+
+		if nullable {
+			property = &Schema{
+				OneOf: []*Schema{
+					property,
+					{
+						Type: "null",
+					},
+				},
+			}
+		}
+
+		st.Properties.Set(name, property)
+		if required {
+			st.Required = appendUniqueString(st.Required, name)
+		}
+	}
+
+	for i := 0; i < t.NumField(); i++ {
+		f := t.Field(i)
+		handleField(f)
+	}
+	if r.AdditionalFields != nil {
+		if af := r.AdditionalFields(t); af != nil {
+			for _, sf := range af {
+				handleField(sf)
+			}
+		}
+	}
+}
+
+func appendUniqueString(base []string, value string) []string {
+	for _, v := range base {
+		if v == value {
+			return base
+		}
+	}
+	return append(base, value)
+}
+
+// addDefinition will append the provided schema. If needed, an ID and anchor will also be added.
+func (r *Reflector) addDefinition(definitions Definitions, t reflect.Type, s *Schema) {
+	name := r.typeName(t)
+	if name == "" {
+		return
+	}
+	definitions[name] = s
+}
+
+// refDefinition will provide a schema with a reference to an existing definition.
+func (r *Reflector) refDefinition(definitions Definitions, t reflect.Type) *Schema {
+	if r.DoNotReference {
+		return nil
+	}
+	name := r.typeName(t)
+	if name == "" {
+		return nil
+	}
+	if _, ok := definitions[name]; !ok {
+		return nil
+	}
+	return &Schema{
+		Ref: "#/$defs/" + name,
+	}
+}
+
+func (r *Reflector) lookupID(t reflect.Type) ID {
+	if r.Lookup != nil {
+		if t.Kind() == reflect.Ptr {
+			t = t.Elem()
+		}
+		return r.Lookup(t)
+
+	}
+	return EmptyID
+}
+
+func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, propertyName string) {
+	t.Description = f.Tag.Get("jsonschema_description")
+
+	tags := splitOnUnescapedCommas(f.Tag.Get("jsonschema"))
+	tags = t.genericKeywords(tags, parent, propertyName)
+
+	switch t.Type {
+	case "string":
+		t.stringKeywords(tags)
+	case "number":
+		t.numericalKeywords(tags)
+	case "integer":
+		t.numericalKeywords(tags)
+	case "array":
+		t.arrayKeywords(tags)
+	case "boolean":
+		t.booleanKeywords(tags)
+	}
+	extras := strings.Split(f.Tag.Get("jsonschema_extras"), ",")
+	t.extraKeywords(extras)
+}
+
+// read struct tags for generic keywords
+func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName string) []string { //nolint:gocyclo
+	unprocessed := make([]string, 0, len(tags))
+	for _, tag := range tags {
+		nameValue := strings.SplitN(tag, "=", 2)
+		if len(nameValue) == 2 {
+			name, val := nameValue[0], nameValue[1]
+			switch name {
+			case "title":
+				t.Title = val
+			case "description":
+				t.Description = val
+			case "type":
+				t.Type = val
+			case "anchor":
+				t.Anchor = val
+			case "oneof_required":
+				var typeFound *Schema
+				for i := range parent.OneOf {
+					if parent.OneOf[i].Title == nameValue[1] {
+						typeFound = parent.OneOf[i]
+					}
+				}
+				if typeFound == nil {
+					typeFound = &Schema{
+						Title:    nameValue[1],
+						Required: []string{},
+					}
+					parent.OneOf = append(parent.OneOf, typeFound)
+				}
+				typeFound.Required = append(typeFound.Required, propertyName)
+			case "anyof_required":
+				var typeFound *Schema
+				for i := range parent.AnyOf {
+					if parent.AnyOf[i].Title == nameValue[1] {
+						typeFound = parent.AnyOf[i]
+					}
+				}
+				if typeFound == nil {
+					typeFound = &Schema{
+						Title:    nameValue[1],
+						Required: []string{},
+					}
+					parent.AnyOf = append(parent.AnyOf, typeFound)
+				}
+				typeFound.Required = append(typeFound.Required, propertyName)
+			case "oneof_ref":
+				subSchema := t
+				if t.Items != nil {
+					subSchema = t.Items
+				}
+				if subSchema.OneOf == nil {
+					subSchema.OneOf = make([]*Schema, 0, 1)
+				}
+				subSchema.Ref = ""
+				refs := strings.Split(nameValue[1], ";")
+				for _, r := range refs {
+					subSchema.OneOf = append(subSchema.OneOf, &Schema{
+						Ref: r,
+					})
+				}
+			case "oneof_type":
+				if t.OneOf == nil {
+					t.OneOf = make([]*Schema, 0, 1)
+				}
+				t.Type = ""
+				types := strings.Split(nameValue[1], ";")
+				for _, ty := range types {
+					t.OneOf = append(t.OneOf, &Schema{
+						Type: ty,
+					})
+				}
+			case "anyof_ref":
+				subSchema := t
+				if t.Items != nil {
+					subSchema = t.Items
+				}
+				if subSchema.AnyOf == nil {
+					subSchema.AnyOf = make([]*Schema, 0, 1)
+				}
+				subSchema.Ref = ""
+				refs := strings.Split(nameValue[1], ";")
+				for _, r := range refs {
+					subSchema.AnyOf = append(subSchema.AnyOf, &Schema{
+						Ref: r,
+					})
+				}
+			case "anyof_type":
+				if t.AnyOf == nil {
+					t.AnyOf = make([]*Schema, 0, 1)
+				}
+				t.Type = ""
+				types := strings.Split(nameValue[1], ";")
+				for _, ty := range types {
+					t.AnyOf = append(t.AnyOf, &Schema{
+						Type: ty,
+					})
+				}
+			default:
+				unprocessed = append(unprocessed, tag)
+			}
+		}
+	}
+	return unprocessed
+}
+
+// read struct tags for boolean type keywords
+func (t *Schema) booleanKeywords(tags []string) {
+	for _, tag := range tags {
+		nameValue := strings.Split(tag, "=")
+		if len(nameValue) != 2 {
+			continue
+		}
+		name, val := nameValue[0], nameValue[1]
+		if name == "default" {
+			if val == "true" {
+				t.Default = true
+			} else if val == "false" {
+				t.Default = false
+			}
+		}
+	}
+}
+
+// read struct tags for string type keywords
+func (t *Schema) stringKeywords(tags []string) {
+	for _, tag := range tags {
+		nameValue := strings.SplitN(tag, "=", 2)
+		if len(nameValue) == 2 {
+			name, val := nameValue[0], nameValue[1]
+			switch name {
+			case "minLength":
+				t.MinLength = parseUint(val)
+			case "maxLength":
+				t.MaxLength = parseUint(val)
+			case "pattern":
+				t.Pattern = val
+			case "format":
+				t.Format = val
+			case "readOnly":
+				i, _ := strconv.ParseBool(val)
+				t.ReadOnly = i
+			case "writeOnly":
+				i, _ := strconv.ParseBool(val)
+				t.WriteOnly = i
+			case "default":
+				t.Default = val
+			case "example":
+				t.Examples = append(t.Examples, val)
+			case "enum":
+				t.Enum = append(t.Enum, val)
+			}
+		}
+	}
+}
+
+// read struct tags for numerical type keywords
+func (t *Schema) numericalKeywords(tags []string) {
+	for _, tag := range tags {
+		nameValue := strings.Split(tag, "=")
+		if len(nameValue) == 2 {
+			name, val := nameValue[0], nameValue[1]
+			switch name {
+			case "multipleOf":
+				t.MultipleOf, _ = toJSONNumber(val)
+			case "minimum":
+				t.Minimum, _ = toJSONNumber(val)
+			case "maximum":
+				t.Maximum, _ = toJSONNumber(val)
+			case "exclusiveMaximum":
+				t.ExclusiveMaximum, _ = toJSONNumber(val)
+			case "exclusiveMinimum":
+				t.ExclusiveMinimum, _ = toJSONNumber(val)
+			case "default":
+				if num, ok := toJSONNumber(val); ok {
+					t.Default = num
+				}
+			case "example":
+				if num, ok := toJSONNumber(val); ok {
+					t.Examples = append(t.Examples, num)
+				}
+			case "enum":
+				if num, ok := toJSONNumber(val); ok {
+					t.Enum = append(t.Enum, num)
+				}
+			}
+		}
+	}
+}
+
+// read struct tags for object type keywords
+// func (t *Type) objectKeywords(tags []string) {
+//     for _, tag := range tags{
+//         nameValue := strings.Split(tag, "=")
+//         name, val := nameValue[0], nameValue[1]
+//         switch name{
+//             case "dependencies":
+//                 t.Dependencies = val
+//                 break;
+//             case "patternProperties":
+//                 t.PatternProperties = val
+//                 break;
+//         }
+//     }
+// }
+
+// read struct tags for array type keywords
+func (t *Schema) arrayKeywords(tags []string) {
+	var defaultValues []any
+
+	unprocessed := make([]string, 0, len(tags))
+	for _, tag := range tags {
+		nameValue := strings.Split(tag, "=")
+		if len(nameValue) == 2 {
+			name, val := nameValue[0], nameValue[1]
+			switch name {
+			case "minItems":
+				t.MinItems = parseUint(val)
+			case "maxItems":
+				t.MaxItems = parseUint(val)
+			case "uniqueItems":
+				t.UniqueItems = true
+			case "default":
+				defaultValues = append(defaultValues, val)
+			case "format":
+				t.Items.Format = val
+			case "pattern":
+				t.Items.Pattern = val
+			default:
+				unprocessed = append(unprocessed, tag) // left for further processing by underlying type
+			}
+		}
+	}
+	if len(defaultValues) > 0 {
+		t.Default = defaultValues
+	}
+
+	if len(unprocessed) == 0 {
+		// we don't have anything else to process
+		return
+	}
+
+	switch t.Items.Type {
+	case "string":
+		t.Items.stringKeywords(unprocessed)
+	case "number":
+		t.Items.numericalKeywords(unprocessed)
+	case "integer":
+		t.Items.numericalKeywords(unprocessed)
+	case "array":
+		// explicitly don't support traversal for the [][]..., as it's unclear where the array tags belong
+	case "boolean":
+		t.Items.booleanKeywords(unprocessed)
+	}
+}
+
+func (t *Schema) extraKeywords(tags []string) {
+	for _, tag := range tags {
+		nameValue := strings.SplitN(tag, "=", 2)
+		if len(nameValue) == 2 {
+			t.setExtra(nameValue[0], nameValue[1])
+		}
+	}
+}
+
+func (t *Schema) setExtra(key, val string) {
+	if t.Extras == nil {
+		t.Extras = map[string]any{}
+	}
+	if existingVal, ok := t.Extras[key]; ok {
+		switch existingVal := existingVal.(type) {
+		case string:
+			t.Extras[key] = []string{existingVal, val}
+		case []string:
+			t.Extras[key] = append(existingVal, val)
+		case int:
+			t.Extras[key], _ = strconv.Atoi(val)
+		case bool:
+			t.Extras[key] = (val == "true" || val == "t")
+		}
+	} else {
+		switch key {
+		case "minimum":
+			t.Extras[key], _ = strconv.Atoi(val)
+		default:
+			var x any
+			if val == "true" {
+				x = true
+			} else if val == "false" {
+				x = false
+			} else {
+				x = val
+			}
+			t.Extras[key] = x
+		}
+	}
+}
+
+func requiredFromJSONTags(tags []string, val *bool) {
+	if ignoredByJSONTags(tags) {
+		return
+	}
+
+	for _, tag := range tags[1:] {
+		if tag == "omitempty" {
+			*val = false
+			return
+		}
+	}
+	*val = true
+}
+
+func requiredFromJSONSchemaTags(tags []string, val *bool) {
+	if ignoredByJSONSchemaTags(tags) {
+		return
+	}
+	for _, tag := range tags {
+		if tag == "required" {
+			*val = true
+		}
+	}
+}
+
+func nullableFromJSONSchemaTags(tags []string) bool {
+	if ignoredByJSONSchemaTags(tags) {
+		return false
+	}
+	for _, tag := range tags {
+		if tag == "nullable" {
+			return true
+		}
+	}
+	return false
+}
+
+func ignoredByJSONTags(tags []string) bool {
+	return tags[0] == "-"
+}
+
+func ignoredByJSONSchemaTags(tags []string) bool {
+	return tags[0] == "-"
+}
+
+func inlinedByJSONTags(tags []string) bool {
+	for _, tag := range tags[1:] {
+		if tag == "inline" {
+			return true
+		}
+	}
+	return false
+}
+
+// toJSONNumber converts string to *json.Number.
+// It'll aso return whether the number is valid.
+func toJSONNumber(s string) (json.Number, bool) {
+	num := json.Number(s)
+	if _, err := num.Int64(); err == nil {
+		return num, true
+	}
+	if _, err := num.Float64(); err == nil {
+		return num, true
+	}
+	return json.Number(""), false
+}
+
+func parseUint(num string) *uint64 {
+	val, err := strconv.ParseUint(num, 10, 64)
+	if err != nil {
+		return nil
+	}
+	return &val
+}
+
+func (r *Reflector) fieldNameTag() string {
+	if r.FieldNameTag != "" {
+		return r.FieldNameTag
+	}
+	return "json"
+}
+
+func (r *Reflector) reflectFieldName(f reflect.StructField) (string, bool, bool, bool) {
+	jsonTagString := f.Tag.Get(r.fieldNameTag())
+	jsonTags := strings.Split(jsonTagString, ",")
+
+	if ignoredByJSONTags(jsonTags) {
+		return "", false, false, false
+	}
+
+	schemaTags := strings.Split(f.Tag.Get("jsonschema"), ",")
+	if ignoredByJSONSchemaTags(schemaTags) {
+		return "", false, false, false
+	}
+
+	var required bool
+	if !r.RequiredFromJSONSchemaTags {
+		requiredFromJSONTags(jsonTags, &required)
+	}
+	requiredFromJSONSchemaTags(schemaTags, &required)
+
+	nullable := nullableFromJSONSchemaTags(schemaTags)
+
+	if f.Anonymous && jsonTags[0] == "" {
+		// As per JSON Marshal rules, anonymous structs are inherited
+		if f.Type.Kind() == reflect.Struct {
+			return "", true, false, false
+		}
+
+		// As per JSON Marshal rules, anonymous pointer to structs are inherited
+		if f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct {
+			return "", true, false, false
+		}
+	}
+
+	// As per JSON Marshal rules, inline nested structs that have `inline` tag.
+	if inlinedByJSONTags(jsonTags) {
+		return "", true, false, false
+	}
+
+	// Try to determine the name from the different combos
+	name := f.Name
+	if jsonTags[0] != "" {
+		name = jsonTags[0]
+	}
+	if !f.Anonymous && f.PkgPath != "" {
+		// field not anonymous and not export has no export name
+		name = ""
+	} else if r.KeyNamer != nil {
+		name = r.KeyNamer(name)
+	}
+
+	return name, false, required, nullable
+}
+
+// UnmarshalJSON is used to parse a schema object or boolean.
+func (t *Schema) UnmarshalJSON(data []byte) error {
+	if bytes.Equal(data, []byte("true")) {
+		*t = *TrueSchema
+		return nil
+	} else if bytes.Equal(data, []byte("false")) {
+		*t = *FalseSchema
+		return nil
+	}
+	type SchemaAlt Schema
+	aux := &struct {
+		*SchemaAlt
+	}{
+		SchemaAlt: (*SchemaAlt)(t),
+	}
+	return json.Unmarshal(data, aux)
+}
+
+// MarshalJSON is used to serialize a schema object or boolean.
+func (t *Schema) MarshalJSON() ([]byte, error) {
+	if t.boolean != nil {
+		if *t.boolean {
+			return []byte("true"), nil
+		}
+		return []byte("false"), nil
+	}
+	if reflect.DeepEqual(&Schema{}, t) {
+		// Don't bother returning empty schemas
+		return []byte("true"), nil
+	}
+	type SchemaAlt Schema
+	b, err := json.Marshal((*SchemaAlt)(t))
+	if err != nil {
+		return nil, err
+	}
+	if len(t.Extras) == 0 {
+		return b, nil
+	}
+	m, err := json.Marshal(t.Extras)
+	if err != nil {
+		return nil, err
+	}
+	if len(b) == 2 {
+		return m, nil
+	}
+	b[len(b)-1] = ','
+	return append(b, m[1:]...), nil
+}
+
+func (r *Reflector) typeName(t reflect.Type) string {
+	if r.Namer != nil {
+		if name := r.Namer(t); name != "" {
+			return name
+		}
+	}
+	return t.Name()
+}
+
+// Split on commas that are not preceded by `\`.
+// This way, we prevent splitting regexes
+func splitOnUnescapedCommas(tagString string) []string {
+	ret := make([]string, 0)
+	separated := strings.Split(tagString, ",")
+	ret = append(ret, separated[0])
+	i := 0
+	for _, nextTag := range separated[1:] {
+		if len(ret[i]) == 0 {
+			ret = append(ret, nextTag)
+			i++
+			continue
+		}
+
+		if ret[i][len(ret[i])-1] == '\\' {
+			ret[i] = ret[i][:len(ret[i])-1] + "," + nextTag
+		} else {
+			ret = append(ret, nextTag)
+			i++
+		}
+	}
+
+	return ret
+}
+
+func fullyQualifiedTypeName(t reflect.Type) string {
+	return t.PkgPath() + "." + t.Name()
+}
diff --git a/vendor/github.com/invopop/jsonschema/reflect_comments.go b/vendor/github.com/invopop/jsonschema/reflect_comments.go
new file mode 100644
index 000000000..ff374c75c
--- /dev/null
+++ b/vendor/github.com/invopop/jsonschema/reflect_comments.go
@@ -0,0 +1,146 @@
+package jsonschema
+
+import (
+	"fmt"
+	"io/fs"
+	gopath "path"
+	"path/filepath"
+	"reflect"
+	"strings"
+
+	"go/ast"
+	"go/doc"
+	"go/parser"
+	"go/token"
+)
+
+type commentOptions struct {
+	fullObjectText bool // use the first sentence only?
+}
+
+// CommentOption allows for special configuration options when preparing Go
+// source files for comment extraction.
+type CommentOption func(*commentOptions)
+
+// WithFullComment will configure the comment extraction to process to use an
+// object type's full comment text instead of just the synopsis.
+func WithFullComment() CommentOption {
+	return func(o *commentOptions) {
+		o.fullObjectText = true
+	}
+}
+
+// AddGoComments will update the reflectors comment map with all the comments
+// found in the provided source directories including sub-directories, in order to
+// generate a dictionary of comments associated with Types and Fields. The results
+// will be added to the `Reflect.CommentMap` ready to use with Schema "description"
+// fields.
+//
+// The `go/parser` library is used to extract all the comments and unfortunately doesn't
+// have a built-in way to determine the fully qualified name of a package. The `base`
+// parameter, the URL used to import that package, is thus required to be able to match
+// reflected types.
+//
+// When parsing type comments, by default we use the `go/doc`'s Synopsis method to extract
+// the first phrase only. Field comments, which tend to be much shorter, will include everything.
+// This behavior can be changed by using the `WithFullComment` option.
+func (r *Reflector) AddGoComments(base, path string, opts ...CommentOption) error {
+	if r.CommentMap == nil {
+		r.CommentMap = make(map[string]string)
+	}
+	co := new(commentOptions)
+	for _, opt := range opts {
+		opt(co)
+	}
+
+	return r.extractGoComments(base, path, r.CommentMap, co)
+}
+
+func (r *Reflector) extractGoComments(base, path string, commentMap map[string]string, opts *commentOptions) error {
+	fset := token.NewFileSet()
+	dict := make(map[string][]*ast.Package)
+	err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if info.IsDir() {
+			d, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
+			if err != nil {
+				return err
+			}
+			for _, v := range d {
+				// paths may have multiple packages, like for tests
+				k := gopath.Join(base, path)
+				dict[k] = append(dict[k], v)
+			}
+		}
+		return nil
+	})
+	if err != nil {
+		return err
+	}
+
+	for pkg, p := range dict {
+		for _, f := range p {
+			gtxt := ""
+			typ := ""
+			ast.Inspect(f, func(n ast.Node) bool {
+				switch x := n.(type) {
+				case *ast.TypeSpec:
+					typ = x.Name.String()
+					if !ast.IsExported(typ) {
+						typ = ""
+					} else {
+						txt := x.Doc.Text()
+						if txt == "" && gtxt != "" {
+							txt = gtxt
+							gtxt = ""
+						}
+						if !opts.fullObjectText {
+							txt = doc.Synopsis(txt)
+						}
+						commentMap[fmt.Sprintf("%s.%s", pkg, typ)] = strings.TrimSpace(txt)
+					}
+				case *ast.Field:
+					txt := x.Doc.Text()
+					if txt == "" {
+						txt = x.Comment.Text()
+					}
+					if typ != "" && txt != "" {
+						for _, n := range x.Names {
+							if ast.IsExported(n.String()) {
+								k := fmt.Sprintf("%s.%s.%s", pkg, typ, n)
+								commentMap[k] = strings.TrimSpace(txt)
+							}
+						}
+					}
+				case *ast.GenDecl:
+					// remember for the next type
+					gtxt = x.Doc.Text()
+				}
+				return true
+			})
+		}
+	}
+
+	return nil
+}
+
+func (r *Reflector) lookupComment(t reflect.Type, name string) string {
+	if r.LookupComment != nil {
+		if comment := r.LookupComment(t, name); comment != "" {
+			return comment
+		}
+	}
+
+	if r.CommentMap == nil {
+		return ""
+	}
+
+	n := fullyQualifiedTypeName(t)
+	if name != "" {
+		n = n + "." + name
+	}
+
+	return r.CommentMap[n]
+}
diff --git a/vendor/github.com/invopop/jsonschema/schema.go b/vendor/github.com/invopop/jsonschema/schema.go
new file mode 100644
index 000000000..2d914b8c8
--- /dev/null
+++ b/vendor/github.com/invopop/jsonschema/schema.go
@@ -0,0 +1,94 @@
+package jsonschema
+
+import (
+	"encoding/json"
+
+	orderedmap "github.com/wk8/go-ordered-map/v2"
+)
+
+// Version is the JSON Schema version.
+var Version = "https://json-schema.org/draft/2020-12/schema"
+
+// Schema represents a JSON Schema object type.
+// RFC draft-bhutton-json-schema-00 section 4.3
+type Schema struct {
+	// RFC draft-bhutton-json-schema-00
+	Version     string      `json:"$schema,omitempty"`     // section 8.1.1
+	ID          ID          `json:"$id,omitempty"`         // section 8.2.1
+	Anchor      string      `json:"$anchor,omitempty"`     // section 8.2.2
+	Ref         string      `json:"$ref,omitempty"`        // section 8.2.3.1
+	DynamicRef  string      `json:"$dynamicRef,omitempty"` // section 8.2.3.2
+	Definitions Definitions `json:"$defs,omitempty"`       // section 8.2.4
+	Comments    string      `json:"$comment,omitempty"`    // section 8.3
+	// RFC draft-bhutton-json-schema-00 section 10.2.1 (Sub-schemas with logic)
+	AllOf []*Schema `json:"allOf,omitempty"` // section 10.2.1.1
+	AnyOf []*Schema `json:"anyOf,omitempty"` // section 10.2.1.2
+	OneOf []*Schema `json:"oneOf,omitempty"` // section 10.2.1.3
+	Not   *Schema   `json:"not,omitempty"`   // section 10.2.1.4
+	// RFC draft-bhutton-json-schema-00 section 10.2.2 (Apply sub-schemas conditionally)
+	If               *Schema            `json:"if,omitempty"`               // section 10.2.2.1
+	Then             *Schema            `json:"then,omitempty"`             // section 10.2.2.2
+	Else             *Schema            `json:"else,omitempty"`             // section 10.2.2.3
+	DependentSchemas map[string]*Schema `json:"dependentSchemas,omitempty"` // section 10.2.2.4
+	// RFC draft-bhutton-json-schema-00 section 10.3.1 (arrays)
+	PrefixItems []*Schema `json:"prefixItems,omitempty"` // section 10.3.1.1
+	Items       *Schema   `json:"items,omitempty"`       // section 10.3.1.2  (replaces additionalItems)
+	Contains    *Schema   `json:"contains,omitempty"`    // section 10.3.1.3
+	// RFC draft-bhutton-json-schema-00 section 10.3.2 (sub-schemas)
+	Properties           *orderedmap.OrderedMap[string, *Schema] `json:"properties,omitempty"`           // section 10.3.2.1
+	PatternProperties    map[string]*Schema                      `json:"patternProperties,omitempty"`    // section 10.3.2.2
+	AdditionalProperties *Schema                                 `json:"additionalProperties,omitempty"` // section 10.3.2.3
+	PropertyNames        *Schema                                 `json:"propertyNames,omitempty"`        // section 10.3.2.4
+	// RFC draft-bhutton-json-schema-validation-00, section 6
+	Type              string              `json:"type,omitempty"`              // section 6.1.1
+	Enum              []any               `json:"enum,omitempty"`              // section 6.1.2
+	Const             any                 `json:"const,omitempty"`             // section 6.1.3
+	MultipleOf        json.Number         `json:"multipleOf,omitempty"`        // section 6.2.1
+	Maximum           json.Number         `json:"maximum,omitempty"`           // section 6.2.2
+	ExclusiveMaximum  json.Number         `json:"exclusiveMaximum,omitempty"`  // section 6.2.3
+	Minimum           json.Number         `json:"minimum,omitempty"`           // section 6.2.4
+	ExclusiveMinimum  json.Number         `json:"exclusiveMinimum,omitempty"`  // section 6.2.5
+	MaxLength         *uint64             `json:"maxLength,omitempty"`         // section 6.3.1
+	MinLength         *uint64             `json:"minLength,omitempty"`         // section 6.3.2
+	Pattern           string              `json:"pattern,omitempty"`           // section 6.3.3
+	MaxItems          *uint64             `json:"maxItems,omitempty"`          // section 6.4.1
+	MinItems          *uint64             `json:"minItems,omitempty"`          // section 6.4.2
+	UniqueItems       bool                `json:"uniqueItems,omitempty"`       // section 6.4.3
+	MaxContains       *uint64             `json:"maxContains,omitempty"`       // section 6.4.4
+	MinContains       *uint64             `json:"minContains,omitempty"`       // section 6.4.5
+	MaxProperties     *uint64             `json:"maxProperties,omitempty"`     // section 6.5.1
+	MinProperties     *uint64             `json:"minProperties,omitempty"`     // section 6.5.2
+	Required          []string            `json:"required,omitempty"`          // section 6.5.3
+	DependentRequired map[string][]string `json:"dependentRequired,omitempty"` // section 6.5.4
+	// RFC draft-bhutton-json-schema-validation-00, section 7
+	Format string `json:"format,omitempty"`
+	// RFC draft-bhutton-json-schema-validation-00, section 8
+	ContentEncoding  string  `json:"contentEncoding,omitempty"`  // section 8.3
+	ContentMediaType string  `json:"contentMediaType,omitempty"` // section 8.4
+	ContentSchema    *Schema `json:"contentSchema,omitempty"`    // section 8.5
+	// RFC draft-bhutton-json-schema-validation-00, section 9
+	Title       string `json:"title,omitempty"`       // section 9.1
+	Description string `json:"description,omitempty"` // section 9.1
+	Default     any    `json:"default,omitempty"`     // section 9.2
+	Deprecated  bool   `json:"deprecated,omitempty"`  // section 9.3
+	ReadOnly    bool   `json:"readOnly,omitempty"`    // section 9.4
+	WriteOnly   bool   `json:"writeOnly,omitempty"`   // section 9.4
+	Examples    []any  `json:"examples,omitempty"`    // section 9.5
+
+	Extras map[string]any `json:"-"`
+
+	// Special boolean representation of the Schema - section 4.3.2
+	boolean *bool
+}
+
+var (
+	// TrueSchema defines a schema with a true value
+	TrueSchema = &Schema{boolean: &[]bool{true}[0]}
+	// FalseSchema defines a schema with a false value
+	FalseSchema = &Schema{boolean: &[]bool{false}[0]}
+)
+
+// Definitions hold schema definitions.
+// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.26
+// RFC draft-wright-json-schema-validation-00, section 5.26
+type Definitions map[string]*Schema
diff --git a/vendor/github.com/invopop/jsonschema/utils.go b/vendor/github.com/invopop/jsonschema/utils.go
new file mode 100644
index 000000000..ed8edf741
--- /dev/null
+++ b/vendor/github.com/invopop/jsonschema/utils.go
@@ -0,0 +1,26 @@
+package jsonschema
+
+import (
+	"regexp"
+	"strings"
+
+	orderedmap "github.com/wk8/go-ordered-map/v2"
+)
+
+var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
+var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
+
+// ToSnakeCase converts the provided string into snake case using dashes.
+// This is useful for Schema IDs and definitions to be coherent with
+// common JSON Schema examples.
+func ToSnakeCase(str string) string {
+	snake := matchFirstCap.ReplaceAllString(str, "${1}-${2}")
+	snake = matchAllCap.ReplaceAllString(snake, "${1}-${2}")
+	return strings.ToLower(snake)
+}
+
+// NewProperties is a helper method to instantiate a new properties ordered
+// map.
+func NewProperties() *orderedmap.OrderedMap[string, *Schema] {
+	return orderedmap.New[string, *Schema]()
+}
diff --git a/vendor/github.com/ktrysmt/go-bitbucket/.gitignore b/vendor/github.com/ktrysmt/go-bitbucket/.gitignore
index 22d0d82f8..4a5caae53 100644
--- a/vendor/github.com/ktrysmt/go-bitbucket/.gitignore
+++ b/vendor/github.com/ktrysmt/go-bitbucket/.gitignore
@@ -1 +1,3 @@
 vendor
+.idea
+.env
\ No newline at end of file
diff --git a/vendor/github.com/ktrysmt/go-bitbucket/repository.go b/vendor/github.com/ktrysmt/go-bitbucket/repository.go
index 25fee782c..c8c8827b3 100644
--- a/vendor/github.com/ktrysmt/go-bitbucket/repository.go
+++ b/vendor/github.com/ktrysmt/go-bitbucket/repository.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"net/url"
 	"path"
@@ -440,11 +441,17 @@ func (r *Repository) ListRefs(rbo *RepositoryRefOptions) (*RepositoryRefs, error
 	return decodeRepositoryRefs(bodyString)
 }
 
+// ListBranches gets all branches in the Bitbucket repository and returns them as a RepositoryBranches.
+// You can pass query parameter to filter branches by name.
+//
+// Bitbucket API docs: https://developer.atlassian.com/cloud/bitbucket/rest/api-group-refs/#api-repositories-workspace-repo-slug-refs-get
 func (r *Repository) ListBranches(rbo *RepositoryBranchOptions) (*RepositoryBranches, error) {
-
 	params := url.Values{}
+
 	if rbo.Query != "" {
-		params.Add("q", rbo.Query)
+		// https://developer.atlassian.com/cloud/bitbucket/rest/intro/#operators
+		query := fmt.Sprintf("name ~ \"%s\"", rbo.Query)
+		params.Set("q", query)
 	}
 
 	if rbo.Sort != "" {
@@ -468,12 +475,12 @@ func (r *Repository) ListBranches(rbo *RepositoryBranchOptions) (*RepositoryBran
 	if err != nil {
 		return nil, err
 	}
-	bodyBytes, err := ioutil.ReadAll(response)
+	bodyBytes, err := io.ReadAll(response)
 	if err != nil {
 		return nil, err
 	}
-	bodyString := string(bodyBytes)
-	return decodeRepositoryBranches(bodyString)
+
+	return decodeRepositoryBranches(string(bodyBytes))
 }
 
 func (r *Repository) GetBranch(rbo *RepositoryBranchOptions) (*RepositoryBranch, error) {
diff --git a/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md b/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md
index 84f9c7b2c..a10d3fc8d 100644
--- a/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md
+++ b/vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md
@@ -6,8 +6,32 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 The format of this file is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 but only releases after v1.0.3 properly adhere to it.
 
+## [Unreleased]
+
+## [1.3.0] - 2025-09-08
+### Added
+- `BlendLinearRgb` (#50)
+- `DistanceRiemersma` (#52)
+- Introduce a function for sorting colors (#57)
+- YAML marshal/unmarshal support (#63)
+- Add support for OkLab and OkLch (#66)
+- Functions that use randomness now support specifying a custom source (#73)
+- Functions BlendOkLab and BlendOkLch (#70)
+
+## Changed
+- `Hex()` parsing is much faster (#78)
+
+### Fixed
+- Fix bug when doing HSV/HCL blending between a gray color and non-gray color (#60)
+- Docs for HSV/HSL were updated to note that hue 360 is not allowed (#71)
+
+### Deprecated
+- `DistanceLinearRGB` is deprecated for the name `DistanceLinearRgb` which is more in-line with the rest of the library
+
 
 ## [1.2.0] - 2021-01-27
+This is the same as the v1.1.0 tag.
+
 ### Added
 - HSLuv and HPLuv color spaces (#41, #51)
 - CIE LCh(uv) color space, called `LuvLCh` in code (#51)
diff --git a/vendor/github.com/lucasb-eyer/go-colorful/README.md b/vendor/github.com/lucasb-eyer/go-colorful/README.md
index 8b9bd4999..b3bb545cf 100644
--- a/vendor/github.com/lucasb-eyer/go-colorful/README.md
+++ b/vendor/github.com/lucasb-eyer/go-colorful/README.md
@@ -1,6 +1,7 @@
 go-colorful
 ===========
 
+[![Go Reference](https://pkg.go.dev/badge/github.com/lucasb-eyer/go-colorful.svg)](https://pkg.go.dev/github.com/lucasb-eyer/go-colorful)
 [![go reportcard](https://goreportcard.com/badge/github.com/lucasb-eyer/go-colorful)](https://goreportcard.com/report/github.com/lucasb-eyer/go-colorful)
 
 A library for playing with colors in Go. Supports Go 1.13 onwards.
@@ -29,10 +30,10 @@ Go-Colorful stores colors in RGB and provides methods from converting these to v
 - **CIE-xyY:** encodes chromacity in x and y and luminance in Y, all in [0..1]
 - **CIE-L\*a\*b\*:** A *perceptually uniform* color space, i.e. distances are meaningful. L\* in [0..1] and a\*, b\* almost in [-1..1].
 - **CIE-L\*u\*v\*:** Very similar to CIE-L\*a\*b\*, there is [no consensus](http://en.wikipedia.org/wiki/CIELUV#Historical_background) on which one is "better".
-- **CIE-L\*C\*h° (HCL):** This is generally the [most useful](http://vis4.net/blog/posts/avoid-equidistant-hsv-colors/) one; CIE-L\*a\*b\* space in polar coordinates, i.e. a *better* HSV. H° is in [0..360], C\* almost in [-1..1] and L\* as in CIE-L\*a\*b\*.
-- **CIE LCh(uv):** Called `LuvLCh` in code, this is a cylindrical transformation of the CIE-L\*u\*v\* color space. Like HCL above: H° is in [0..360], C\* almost in [-1..1] and L\* as in CIE-L\*u\*v\*.
+- **CIE-L\*C\*h° (HCL):** This is generally the [most useful](http://vis4.net/blog/posts/avoid-equidistant-hsv-colors/) one; CIE-L\*a\*b\* space in polar coordinates, i.e. a *better* HSV. H° is in [0..360], C\* almost in [0..1] and L\* as in CIE-L\*a\*b\*.
+- **CIE LCh(uv):** Called `LuvLCh` in code, this is a cylindrical transformation of the CIE-L\*u\*v\* color space. Like HCL above: H° is in [0..360], C\* almost in [0..1] and L\* as in CIE-L\*u\*v\*.
 - **HSLuv:** The better alternative to HSL, see [here](https://www.hsluv.org/) and [here](https://www.kuon.ch/post/2020-03-08-hsluv/). Hue in [0..360], Saturation and Luminance in [0..1].
-- **HPLuv:** A variant of HSLuv. The color space is smoother, but only pastel colors can be included. Because the valid colors are limited, it's easy to get invalid Saturation values way above 1.0, indicating the color can't be represented in HPLuv beccause it's not pastel.
+- **HPLuv:** A variant of HSLuv. The color space is smoother, but only pastel colors can be included. Because the valid colors are limited, it's easy to get invalid Saturation values way above 1.0, indicating the color can't be represented in HPLuv because it's not pastel.
 
 For the colorspaces where it makes sense (XYZ, Lab, Luv, HCl), the
 [D65](http://en.wikipedia.org/wiki/Illuminant_D65) is used as reference white
@@ -52,14 +53,6 @@ Nice, but what's it useful for?
 - Generating random colors under some constraints (e.g. colors of the same shade, or shades of one color.)
 - Generating gorgeous random palettes with distinct colors of a same temperature.
 
-What not (yet)?
-===============
-There are a few features which are currently missing and might be useful.
-I just haven't implemented them yet because I didn't have the need for it.
-Pull requests welcome.
-
-- Sorting colors (potentially using above mentioned distances)
-
 So which colorspace should I use?
 =================================
 It depends on what you want to do. I think the folks from *I want hue* are
@@ -139,7 +132,7 @@ alpha colors, this means the RGB values are lost (set to 0) and it's impossible
 to recover them. In such a case `MakeColor` will return `false` as its second value.
 
 ### Comparing colors
-In the RGB color space, the Euclidian distance between colors *doesn't* correspond
+In the RGB color space, the Euclidean distance between colors *doesn't* correspond
 to visual/perceptual distance. This means that two pairs of colors which have the
 same distance in RGB space can look much further apart. This is fixed by the
 CIE-L\*a\*b\*, CIE-L\*u\*v\* and CIE-L\*C\*h° color spaces.
@@ -208,7 +201,7 @@ What you see is that HSV is really bad: it adds some green, which is not present
 in the original colors at all! RGB is much better, but it stays light a little
 too long. LUV and LAB both hit the right lightness but LAB has a little more
 color. HCL works in the same vein as HSV (both cylindrical interpolations) but
-it does it right in that there is no green appearing and the lighthness changes
+it does it right in that there is no green appearing and the lightness changes
 in a linear manner.
 
 While this seems all good, you need to know one thing: When interpolating in any
@@ -316,11 +309,11 @@ generating this picture in `doc/colorgens/colorgens.go`.
 
 ### Getting random palettes
 As soon as you need to generate more than one random color, you probably want
-them to be distinguishible. Playing against an opponent which has almost the
+them to be distinguishable. Playing against an opponent which has almost the
 same blue as I do is not fun. This is where random palettes can help.
 
 These palettes are generated using an algorithm which ensures that all colors
-on the palette are as distinguishible as possible. Again, there is a `Fast`
+on the palette are as distinguishable as possible. Again, there is a `Fast`
 method which works in HSV and is less perceptually uniform and a non-`Fast`
 method which works in CIE spaces. For more theory on `SoftPalette`, check out
 [I want hue](http://tools.medialab.sciences-po.fr/iwanthue/theory.php). Yet
@@ -372,10 +365,18 @@ from top to bottom: `Warm`, `FastWarm`, `Happy`, `FastHappy`, `Soft`,
 Again, the code used for generating the above image is available as [doc/palettegens/palettegens.go](https://github.com/lucasb-eyer/go-colorful/blob/master/doc/palettegens/palettegens.go).
 
 ### Sorting colors
-TODO: Sort using dist fn.
+
+Sorting colors is not a well-defined operation.  For example, {dark blue, dark red, light blue, light red} is already sorted if darker colors should precede lighter colors but would need to be re-sorted as {dark red, light red, dark blue, light blue} if longer-wavelength colors should precede shorter-wavelength colors.
+
+Go-Colorful's `Sorted` function orders a list of colors so as to minimize the average distance between adjacent colors, including between the last and the first.  (`Sorted` does not necessarily find the true minimum, only a reasonably close approximation.)  The following picture, drawn by [doc/colorsort/colorsort.go](https://github.com/lucasb-eyer/go-colorful/blob/master/doc/colorsort/colorsort.go), illustrates `Sorted`'s behavior:
+
+![Sorting colors](doc/colorsort/colorsort.png)
+
+The first row represents the input: a slice of 512 randomly chosen colors.  The second row shows the colors sorted in CIE-L\*C\*h° space, ordered first by lightness (L), then by hue angle (h), and finally by chroma (C).  Note that distracting pinstripes permeate the colors.  Sorting using *any* color space and *any* ordering of the channels yields a similar pinstriped pattern.  The third row of the image was sorted using Go-Colorful's `Sorted` function.  Although the colors do not appear to be in any particular order, the sequence at least appears smoother than the one sorted by channel.
+
 
 ### Using linear RGB for computations
-There are two methods for transforming RGB<->Linear RGB: a fast and almost precise one,
+There are two methods for transforming RGB⟷Linear RGB: a fast and almost precise one,
 and a slow and precise one.
 
 ```go
@@ -472,9 +473,9 @@ Who?
 ====
 
 This library was developed by Lucas Beyer with contributions from
-Bastien Dejean (@baskerville), Phil Kulak (@pkulak) and Christian Muehlhaeuser (@muesli).
+Bastien Dejean (@baskerville), Phil Kulak (@pkulak), Christian Muehlhaeuser (@muesli), and Scott Pakin (@spakin).
 
-It is now maintained by makeworld (@makeworld-the-better-one).
+It is now maintained by makeworld (@makew0rld).
 
 
 ## License
diff --git a/vendor/github.com/lucasb-eyer/go-colorful/colorgens.go b/vendor/github.com/lucasb-eyer/go-colorful/colorgens.go
index 2e2e49e19..ac697d655 100644
--- a/vendor/github.com/lucasb-eyer/go-colorful/colorgens.go
+++ b/vendor/github.com/lucasb-eyer/go-colorful/colorgens.go
@@ -2,28 +2,32 @@
 
 package colorful
 
-import (
-	"math/rand"
-)
-
 // Creates a random dark, "warm" color through a restricted HSV space.
-func FastWarmColor() Color {
+func FastWarmColorWithRand(rand RandInterface) Color {
 	return Hsv(
 		rand.Float64()*360.0,
 		0.5+rand.Float64()*0.3,
 		0.3+rand.Float64()*0.3)
 }
 
+func FastWarmColor() Color {
+	return FastWarmColorWithRand(getDefaultGlobalRand())
+}
+
 // Creates a random dark, "warm" color through restricted HCL space.
 // This is slower than FastWarmColor but will likely give you colors which have
 // the same "warmness" if you run it many times.
-func WarmColor() (c Color) {
-	for c = randomWarm(); !c.IsValid(); c = randomWarm() {
+func WarmColorWithRand(rand RandInterface) (c Color) {
+	for c = randomWarmWithRand(rand); !c.IsValid(); c = randomWarmWithRand(rand) {
 	}
 	return
 }
 
-func randomWarm() Color {
+func WarmColor() (c Color) {
+	return WarmColorWithRand(getDefaultGlobalRand())
+}
+
+func randomWarmWithRand(rand RandInterface) Color {
 	return Hcl(
 		rand.Float64()*360.0,
 		0.1+rand.Float64()*0.3,
@@ -31,23 +35,31 @@ func randomWarm() Color {
 }
 
 // Creates a random bright, "pimpy" color through a restricted HSV space.
-func FastHappyColor() Color {
+func FastHappyColorWithRand(rand RandInterface) Color {
 	return Hsv(
 		rand.Float64()*360.0,
 		0.7+rand.Float64()*0.3,
 		0.6+rand.Float64()*0.3)
 }
 
+func FastHappyColor() Color {
+	return FastHappyColorWithRand(getDefaultGlobalRand())
+}
+
 // Creates a random bright, "pimpy" color through restricted HCL space.
 // This is slower than FastHappyColor but will likely give you colors which
 // have the same "brightness" if you run it many times.
-func HappyColor() (c Color) {
-	for c = randomPimp(); !c.IsValid(); c = randomPimp() {
+func HappyColorWithRand(rand RandInterface) (c Color) {
+	for c = randomPimpWithRand(rand); !c.IsValid(); c = randomPimpWithRand(rand) {
 	}
 	return
 }
 
-func randomPimp() Color {
+func HappyColor() (c Color) {
+	return HappyColorWithRand(getDefaultGlobalRand())
+}
+
+func randomPimpWithRand(rand RandInterface) Color {
 	return Hcl(
 		rand.Float64()*360.0,
 		0.5+rand.Float64()*0.3,
diff --git a/vendor/github.com/lucasb-eyer/go-colorful/colors.go b/vendor/github.com/lucasb-eyer/go-colorful/colors.go
index 0d5bffe5d..17441a8c6 100644
--- a/vendor/github.com/lucasb-eyer/go-colorful/colors.go
+++ b/vendor/github.com/lucasb-eyer/go-colorful/colors.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"image/color"
 	"math"
+	"strconv"
 )
 
 // A color is stored internally using sRGB (standard RGB) values in the range 0-1
@@ -94,15 +95,39 @@ func (c1 Color) DistanceRgb(c2 Color) float64 {
 	return math.Sqrt(sq(c1.R-c2.R) + sq(c1.G-c2.G) + sq(c1.B-c2.B))
 }
 
-// DistanceLinearRGB computes the distance between two colors in linear RGB
+// DistanceLinearRgb computes the distance between two colors in linear RGB
 // space. This is not useful for measuring how humans perceive color, but
 // might be useful for other things, like dithering.
-func (c1 Color) DistanceLinearRGB(c2 Color) float64 {
+func (c1 Color) DistanceLinearRgb(c2 Color) float64 {
 	r1, g1, b1 := c1.LinearRgb()
 	r2, g2, b2 := c2.LinearRgb()
 	return math.Sqrt(sq(r1-r2) + sq(g1-g2) + sq(b1-b2))
 }
 
+// DistanceLinearRGB is deprecated in favour of DistanceLinearRgb.
+// They do the exact same thing.
+func (c1 Color) DistanceLinearRGB(c2 Color) float64 {
+	return c1.DistanceLinearRgb(c2)
+}
+
+// DistanceRiemersma is a color distance algorithm developed by Thiadmer Riemersma.
+// It uses RGB coordinates, but he claims it has similar results to CIELUV.
+// This makes it both fast and accurate.
+//
+// Sources:
+//
+//	https://www.compuphase.com/cmetric.htm
+//	https://github.com/lucasb-eyer/go-colorful/issues/52
+func (c1 Color) DistanceRiemersma(c2 Color) float64 {
+	rAvg := (c1.R + c2.R) / 2.0
+	// Deltas
+	dR := c1.R - c2.R
+	dG := c1.G - c2.G
+	dB := c1.B - c2.B
+
+	return math.Sqrt((2+rAvg)*dR*dR + 4*dG*dG + (2+(1-rAvg))*dB*dB)
+}
+
 // Check for equality between colors within the tolerance Delta (1/255).
 func (c1 Color) AlmostEqualRgb(c2 Color) bool {
 	return math.Abs(c1.R-c2.R)+
@@ -112,9 +137,11 @@ func (c1 Color) AlmostEqualRgb(c2 Color) bool {
 
 // You don't really want to use this, do you? Go for BlendLab, BlendLuv or BlendHcl.
 func (c1 Color) BlendRgb(c2 Color, t float64) Color {
-	return Color{c1.R + t*(c2.R-c1.R),
+	return Color{
+		c1.R + t*(c2.R-c1.R),
 		c1.G + t*(c2.G-c1.G),
-		c1.B + t*(c2.B-c1.B)}
+		c1.B + t*(c2.B-c1.B),
+	}
 }
 
 // Utility used by Hxx color-spaces for interpolating between two angles in [0,360].
@@ -128,9 +155,9 @@ func interp_angle(a0, a1, t float64) float64 {
 /// HSV ///
 ///////////
 // From http://en.wikipedia.org/wiki/HSL_and_HSV
-// Note that h is in [0..360] and s,v in [0..1]
+// Note that h is in [0..359] and s,v in [0..1]
 
-// Hsv returns the Hue [0..360], Saturation and Value [0..1] of the color.
+// Hsv returns the Hue [0..359], Saturation and Value [0..1] of the color.
 func (col Color) Hsv() (h, s, v float64) {
 	min := math.Min(math.Min(col.R, col.G), col.B)
 	v = math.Max(math.Max(col.R, col.G), col.B)
@@ -160,7 +187,7 @@ func (col Color) Hsv() (h, s, v float64) {
 	return
 }
 
-// Hsv creates a new Color given a Hue in [0..360], a Saturation and a Value in [0..1]
+// Hsv creates a new Color given a Hue in [0..359], a Saturation and a Value in [0..1]
 func Hsv(H, S, V float64) Color {
 	Hp := H / 60.0
 	C := V * S
@@ -198,6 +225,13 @@ func (c1 Color) BlendHsv(c2 Color, t float64) Color {
 	h1, s1, v1 := c1.Hsv()
 	h2, s2, v2 := c2.Hsv()
 
+	// https://github.com/lucasb-eyer/go-colorful/pull/60
+	if s1 == 0 && s2 != 0 {
+		h1 = h2
+	} else if s2 == 0 && s1 != 0 {
+		h2 = h1
+	}
+
 	// We know that h are both in [0..360]
 	return Hsv(interp_angle(h1, h2, t), s1+t*(s2-s1), v1+t*(v2-v1))
 }
@@ -205,7 +239,7 @@ func (c1 Color) BlendHsv(c2 Color, t float64) Color {
 /// HSL ///
 ///////////
 
-// Hsl returns the Hue [0..360], Saturation [0..1], and Luminance (lightness) [0..1] of the color.
+// Hsl returns the Hue [0..359], Saturation [0..1], and Luminance (lightness) [0..1] of the color.
 func (col Color) Hsl() (h, s, l float64) {
 	min := math.Min(math.Min(col.R, col.G), col.B)
 	max := math.Max(math.Max(col.R, col.G), col.B)
@@ -240,7 +274,7 @@ func (col Color) Hsl() (h, s, l float64) {
 	return
 }
 
-// Hsl creates a new Color given a Hue in [0..360], a Saturation [0..1], and a Luminance (lightness) in [0..1]
+// Hsl creates a new Color given a Hue in [0..359], a Saturation [0..1], and a Luminance (lightness) in [0..1]
 func Hsl(h, s, l float64) Color {
 	if s == 0 {
 		return Color{l, l, l}
@@ -331,23 +365,46 @@ func (col Color) Hex() string {
 
 // Hex parses a "html" hex color-string, either in the 3 "#f0c" or 6 "#ff1034" digits form.
 func Hex(scol string) (Color, error) {
-	format := "#%02x%02x%02x"
-	factor := 1.0 / 255.0
-	if len(scol) == 4 {
-		format = "#%1x%1x%1x"
-		factor = 1.0 / 15.0
+	if scol == "" || scol[0] != '#' {
+		return Color{}, fmt.Errorf("color: %v is not a hex-color", scol)
+	}
+	var c Color
+	var err error
+	switch len(scol) {
+	case 4:
+		c, err = parseHexColor(scol[1:2], scol[2:3], scol[3:4], 4, 1.0/15.0)
+	case 7:
+		c, err = parseHexColor(scol[1:3], scol[3:5], scol[5:7], 8, 1.0/255.0)
+	default:
+		return Color{}, fmt.Errorf("color: %v is not a hex-color", scol)
 	}
-
-	var r, g, b uint8
-	n, err := fmt.Sscanf(scol, format, &r, &g, &b)
 	if err != nil {
+		return Color{}, fmt.Errorf("color: %v is not a hex-color: %w", scol, err)
+	}
+	return c, nil
+}
+
+func parseHexColor(r, g, b string, bits int, factor float64) (Color, error) {
+	var c Color
+	var v uint64
+	var err error
+
+	if v, err = strconv.ParseUint(r, 16, bits); err != nil {
 		return Color{}, err
 	}
-	if n != 3 {
-		return Color{}, fmt.Errorf("color: %v is not a hex-color", scol)
+	c.R = float64(v) * factor
+
+	if v, err = strconv.ParseUint(g, 16, bits); err != nil {
+		return Color{}, err
 	}
+	c.G = float64(v) * factor
 
-	return Color{float64(r) * factor, float64(g) * factor, float64(b) * factor}, nil
+	if v, err = strconv.ParseUint(b, 16, bits); err != nil {
+		return Color{}, err
+	}
+	c.B = float64(v) * factor
+
+	return c, err
 }
 
 /// Linear ///
@@ -377,7 +434,7 @@ func linearize_fast(v float64) float64 {
 	v2 := v1 * v1
 	v3 := v2 * v1
 	v4 := v2 * v2
-	//v5 := v3*v2
+	// v5 := v3*v2
 	return -0.248750514614486 + 0.925583310193438*v + 1.16740237321695*v2 + 0.280457026598666*v3 - 0.0757991963780179*v4 //+ 0.0437040411548932*v5
 }
 
@@ -450,6 +507,19 @@ func LinearRgbToXyz(r, g, b float64) (x, y, z float64) {
 	return
 }
 
+// BlendLinearRgb blends two colors in the Linear RGB color-space.
+// Unlike BlendRgb, this will not produce dark color around the center.
+// t == 0 results in c1, t == 1 results in c2
+func (c1 Color) BlendLinearRgb(c2 Color, t float64) Color {
+	r1, g1, b1 := c1.LinearRgb()
+	r2, g2, b2 := c2.LinearRgb()
+	return LinearRgb(
+		r1+t*(r2-r1),
+		g1+t*(g2-g1),
+		b1+t*(b2-b1),
+	)
+}
+
 /// XYZ ///
 ///////////
 // http://www.sjbrown.co.uk/2004/05/14/gamma-correct-rendering/
@@ -784,7 +854,7 @@ func LuvToXyz(l, u, v float64) (x, y, z float64) {
 }
 
 func LuvToXyzWhiteRef(l, u, v float64, wref [3]float64) (x, y, z float64) {
-	//y = wref[1] * lab_finv((l + 0.16) / 1.16)
+	// y = wref[1] * lab_finv((l + 0.16) / 1.16)
 	if l <= 0.08 {
 		y = wref[1] * l * 100.0 * 3.0 / 29.0 * 3.0 / 29.0 * 3.0 / 29.0
 	} else {
@@ -913,6 +983,13 @@ func (col1 Color) BlendHcl(col2 Color, t float64) Color {
 	h1, c1, l1 := col1.Hcl()
 	h2, c2, l2 := col2.Hcl()
 
+	// https://github.com/lucasb-eyer/go-colorful/pull/60
+	if c1 <= 0.00015 && c2 >= 0.00015 {
+		h1 = h2
+	} else if c2 <= 0.00015 && c1 >= 0.00015 {
+		h2 = h1
+	}
+
 	// We know that h are both in [0..360]
 	return Hcl(interp_angle(h1, h2, t), c1+t*(c2-c1), l1+t*(l2-l1)).Clamped()
 }
@@ -977,3 +1054,103 @@ func (col1 Color) BlendLuvLCh(col2 Color, t float64) Color {
 	// We know that h are both in [0..360]
 	return LuvLCh(l1+t*(l2-l1), c1+t*(c2-c1), interp_angle(h1, h2, t))
 }
+
+/// OkLab ///
+///////////
+
+func (col Color) OkLab() (l, a, b float64) {
+	return XyzToOkLab(col.Xyz())
+}
+
+func OkLab(l, a, b float64) Color {
+	return Xyz(OkLabToXyz(l, a, b))
+}
+
+func XyzToOkLab(x, y, z float64) (l, a, b float64) {
+	l_ := math.Cbrt(0.8189330101*x + 0.3618667424*y - 0.1288597137*z)
+	m_ := math.Cbrt(0.0329845436*x + 0.9293118715*y + 0.0361456387*z)
+	s_ := math.Cbrt(0.0482003018*x + 0.2643662691*y + 0.6338517070*z)
+	l = 0.2104542553*l_ + 0.7936177850*m_ - 0.0040720468*s_
+	a = 1.9779984951*l_ - 2.4285922050*m_ + 0.4505937099*s_
+	b = 0.0259040371*l_ + 0.7827717662*m_ - 0.8086757660*s_
+	return
+}
+
+func OkLabToXyz(l, a, b float64) (x, y, z float64) {
+	l_ := 0.9999999984505196*l + 0.39633779217376774*a + 0.2158037580607588*b
+	m_ := 1.0000000088817607*l - 0.10556134232365633*a - 0.0638541747717059*b
+	s_ := 1.0000000546724108*l - 0.08948418209496574*a - 1.2914855378640917*b
+
+	ll := math.Pow(l_, 3)
+	m := math.Pow(m_, 3)
+	s := math.Pow(s_, 3)
+
+	x = 1.2268798733741557*ll - 0.5578149965554813*m + 0.28139105017721594*s
+	y = -0.04057576262431372*ll + 1.1122868293970594*m - 0.07171106666151696*s
+	z = -0.07637294974672142*ll - 0.4214933239627916*m + 1.5869240244272422*s
+
+	return
+}
+
+// BlendOkLab blends two colors in the OkLab color-space, which should result in a better blend (even compared to BlendLab).
+func (c1 Color) BlendOkLab(c2 Color, t float64) Color {
+	l1, a1, b1 := c1.OkLab()
+	l2, a2, b2 := c2.OkLab()
+	return OkLab(l1+t*(l2-l1),
+		a1+t*(a2-a1),
+		b1+t*(b2-b1))
+}
+
+/// OkLch ///
+///////////
+
+func (col Color) OkLch() (l, c, h float64) {
+	return OkLabToOkLch(col.OkLab())
+}
+
+func OkLch(l, c, h float64) Color {
+	return Xyz(OkLchToXyz(l, c, h))
+}
+
+func XyzToOkLch(x, y, z float64) (float64, float64, float64) {
+	l, c, h := OkLabToOkLch(XyzToOkLab(x, y, z))
+	return l, c, h
+}
+
+func OkLchToXyz(l, c, h float64) (float64, float64, float64) {
+	x, y, z := OkLabToXyz(OkLchToOkLab(l, c, h))
+	return x, y, z
+}
+
+func OkLabToOkLch(l, a, b float64) (float64, float64, float64) {
+	c := math.Sqrt((a * a) + (b * b))
+	h := math.Atan2(b, a)
+	if h < 0 {
+		h += 2 * math.Pi
+	}
+
+	return l, c, h * 180 / math.Pi
+}
+
+func OkLchToOkLab(l, c, h float64) (float64, float64, float64) {
+	h *= math.Pi / 180
+	a := c * math.Cos(h)
+	b := c * math.Sin(h)
+	return l, a, b
+}
+
+// BlendOkLch blends two colors in the OkLch color-space, which should result in a better blend (even compared to BlendHcl).
+func (col1 Color) BlendOkLch(col2 Color, t float64) Color {
+	l1, c1, h1 := col1.OkLch()
+	l2, c2, h2 := col2.OkLch()
+
+	// https://github.com/lucasb-eyer/go-colorful/pull/60
+	if c1 <= 0.00015 && c2 >= 0.00015 {
+		h1 = h2
+	} else if c2 <= 0.00015 && c1 >= 0.00015 {
+		h2 = h1
+	}
+
+	// We know that h are both in [0..360]
+	return OkLch(l1+t*(l2-l1), c1+t*(c2-c1), interp_angle(h1, h2, t)).Clamped()
+}
diff --git a/vendor/github.com/lucasb-eyer/go-colorful/happy_palettegen.go b/vendor/github.com/lucasb-eyer/go-colorful/happy_palettegen.go
index bb66dfa4f..0cb9286cb 100644
--- a/vendor/github.com/lucasb-eyer/go-colorful/happy_palettegen.go
+++ b/vendor/github.com/lucasb-eyer/go-colorful/happy_palettegen.go
@@ -1,13 +1,9 @@
 package colorful
 
-import (
-	"math/rand"
-)
-
 // Uses the HSV color space to generate colors with similar S,V but distributed
 // evenly along their Hue. This is fast but not always pretty.
 // If you've got time to spare, use Lab (the non-fast below).
-func FastHappyPalette(colorsCount int) (colors []Color) {
+func FastHappyPaletteWithRand(colorsCount int, rand RandInterface) (colors []Color) {
 	colors = make([]Color, colorsCount)
 
 	for i := 0; i < colorsCount; i++ {
@@ -16,10 +12,18 @@ func FastHappyPalette(colorsCount int) (colors []Color) {
 	return
 }
 
-func HappyPalette(colorsCount int) ([]Color, error) {
+func FastHappyPalette(colorsCount int) (colors []Color) {
+	return FastHappyPaletteWithRand(colorsCount, getDefaultGlobalRand())
+}
+
+func HappyPaletteWithRand(colorsCount int, rand RandInterface) ([]Color, error) {
 	pimpy := func(l, a, b float64) bool {
 		_, c, _ := LabToHcl(l, a, b)
 		return 0.3 <= c && 0.4 <= l && l <= 0.8
 	}
-	return SoftPaletteEx(colorsCount, SoftPaletteSettings{pimpy, 50, true})
+	return SoftPaletteExWithRand(colorsCount, SoftPaletteSettings{pimpy, 50, true}, rand)
+}
+
+func HappyPalette(colorsCount int) ([]Color, error) {
+	return HappyPaletteWithRand(colorsCount, getDefaultGlobalRand())
 }
diff --git a/vendor/github.com/lucasb-eyer/go-colorful/hexcolor.go b/vendor/github.com/lucasb-eyer/go-colorful/hexcolor.go
index 76f31d8f9..ad8b06cc9 100644
--- a/vendor/github.com/lucasb-eyer/go-colorful/hexcolor.go
+++ b/vendor/github.com/lucasb-eyer/go-colorful/hexcolor.go
@@ -65,3 +65,23 @@ func (hc *HexColor) Decode(hexCode string) error {
 	*hc = HexColor(col)
 	return nil
 }
+
+func (hc HexColor) MarshalYAML() (interface{}, error) {
+	return Color(hc).Hex(), nil
+}
+
+func (hc *HexColor) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var hexCode string
+	if err := unmarshal(&hexCode); err != nil {
+		return err
+	}
+
+	var col, err = Hex(hexCode)
+	if err != nil {
+		return err
+	}
+
+	*hc = HexColor(col)
+
+	return nil
+}
diff --git a/vendor/github.com/lucasb-eyer/go-colorful/hsluv.go b/vendor/github.com/lucasb-eyer/go-colorful/hsluv.go
index d19fb6443..cc5148822 100644
--- a/vendor/github.com/lucasb-eyer/go-colorful/hsluv.go
+++ b/vendor/github.com/lucasb-eyer/go-colorful/hsluv.go
@@ -11,7 +11,8 @@ import "math"
 // comparing to the test values, this modified white reference is used internally.
 //
 // See this GitHub thread for details on these values:
-//     https://github.com/hsluv/hsluv/issues/79
+//
+//	https://github.com/hsluv/hsluv/issues/79
 var hSLuvD65 = [3]float64{0.95045592705167, 1.0, 1.089057750759878}
 
 func LuvLChToHSLuv(l, c, h float64) (float64, float64, float64) {
@@ -115,7 +116,7 @@ func (col Color) HPLuv() (h, s, l float64) {
 	return LuvLChToHPLuv(col.LuvLChWhiteRef(hSLuvD65))
 }
 
-// DistanceHSLuv calculates Euclidan distance in the HSLuv colorspace. No idea
+// DistanceHSLuv calculates Euclidean distance in the HSLuv colorspace. No idea
 // how useful this is.
 //
 // The Hue value is divided by 100 before the calculation, so that H, S, and L
diff --git a/vendor/github.com/lucasb-eyer/go-colorful/rand.go b/vendor/github.com/lucasb-eyer/go-colorful/rand.go
new file mode 100644
index 000000000..d3a2d5b50
--- /dev/null
+++ b/vendor/github.com/lucasb-eyer/go-colorful/rand.go
@@ -0,0 +1,22 @@
+package colorful
+
+import "math/rand"
+
+type RandInterface interface {
+	Float64() float64
+	Intn(n int) int
+}
+
+type defaultGlobalRand struct{}
+
+func (df defaultGlobalRand) Float64() float64 {
+	return rand.Float64()
+}
+
+func (df defaultGlobalRand) Intn(n int) int {
+	return rand.Intn(n)
+}
+
+func getDefaultGlobalRand() RandInterface {
+	return defaultGlobalRand{}
+}
diff --git a/vendor/github.com/lucasb-eyer/go-colorful/soft_palettegen.go b/vendor/github.com/lucasb-eyer/go-colorful/soft_palettegen.go
index 9f7bf6f7c..6d8aa137e 100644
--- a/vendor/github.com/lucasb-eyer/go-colorful/soft_palettegen.go
+++ b/vendor/github.com/lucasb-eyer/go-colorful/soft_palettegen.go
@@ -6,7 +6,6 @@ package colorful
 import (
 	"fmt"
 	"math"
-	"math/rand"
 )
 
 // The algorithm works in L*a*b* color space and converts to RGB in the end.
@@ -32,7 +31,7 @@ type SoftPaletteSettings struct {
 // as a new palette of distinctive colors. Falls back to K-medoid if the mean
 // happens to fall outside of the color-space, which can only happen if you
 // specify a CheckColor function.
-func SoftPaletteEx(colorsCount int, settings SoftPaletteSettings) ([]Color, error) {
+func SoftPaletteExWithRand(colorsCount int, settings SoftPaletteSettings, rand RandInterface) ([]Color, error) {
 
 	// Checks whether it's a valid RGB and also fulfills the potentially provided constraint.
 	check := func(col lab_t) bool {
@@ -79,7 +78,7 @@ func SoftPaletteEx(colorsCount int, settings SoftPaletteSettings) ([]Color, erro
 
 	// The actual k-means/medoid iterations
 	for i := 0; i < settings.Iterations; i++ {
-		// Reassing the samples to clusters, i.e. to their closest mean.
+		// Reassigning the samples to clusters, i.e. to their closest mean.
 		// By the way, also check if any sample is used as a medoid and if so, mark that.
 		for isample, sample := range samples {
 			samples_used[isample] = false
@@ -100,7 +99,7 @@ func SoftPaletteEx(colorsCount int, settings SoftPaletteSettings) ([]Color, erro
 
 		// Compute new means according to the samples.
 		for imean := range means {
-			// The new mean is the average of all samples belonging to it..
+			// The new mean is the average of all samples belonging to it.
 			nsamples := 0
 			newmean := lab_t{0.0, 0.0, 0.0}
 			for isample, sample := range samples {
@@ -148,9 +147,17 @@ func SoftPaletteEx(colorsCount int, settings SoftPaletteSettings) ([]Color, erro
 	return labs2cols(means), nil
 }
 
+func SoftPaletteEx(colorsCount int, settings SoftPaletteSettings) ([]Color, error) {
+	return SoftPaletteExWithRand(colorsCount, settings, getDefaultGlobalRand())
+}
+
 // A wrapper which uses common parameters.
+func SoftPaletteWithRand(colorsCount int, rand RandInterface) ([]Color, error) {
+	return SoftPaletteExWithRand(colorsCount, SoftPaletteSettings{nil, 50, false}, rand)
+}
+
 func SoftPalette(colorsCount int) ([]Color, error) {
-	return SoftPaletteEx(colorsCount, SoftPaletteSettings{nil, 50, false})
+	return SoftPaletteWithRand(colorsCount, getDefaultGlobalRand())
 }
 
 func in(haystack []lab_t, upto int, needle lab_t) bool {
diff --git a/vendor/github.com/lucasb-eyer/go-colorful/sort.go b/vendor/github.com/lucasb-eyer/go-colorful/sort.go
new file mode 100644
index 000000000..b1c1b6813
--- /dev/null
+++ b/vendor/github.com/lucasb-eyer/go-colorful/sort.go
@@ -0,0 +1,191 @@
+// This file provides functions for sorting colors.
+
+package colorful
+
+import (
+	"math"
+	"sort"
+)
+
+// An element represents a single element of a set.  It is used to
+// implement a disjoint-set forest.
+type element struct {
+	parent *element // Parent element
+	rank   int      // Rank (approximate depth) of the subtree with this element as root
+}
+
+// newElement creates a singleton set and returns its sole element.
+func newElement() *element {
+	s := &element{}
+	s.parent = s
+	return s
+}
+
+// find returns an arbitrary element of a set when invoked on any element of
+// the set, The important feature is that it returns the same value when
+// invoked on any element of the set.  Consequently, it can be used to test if
+// two elements belong to the same set.
+func (e *element) find() *element {
+	for e.parent != e {
+		e.parent = e.parent.parent
+		e = e.parent
+	}
+	return e
+}
+
+// union establishes the union of two sets when given an element from each set.
+// Afterwards, the original sets no longer exist as separate entities.
+func union(e1, e2 *element) {
+	// Ensure the two elements aren't already part of the same union.
+	e1Root := e1.find()
+	e2Root := e2.find()
+	if e1Root == e2Root {
+		return
+	}
+
+	// Create a union by making the shorter tree point to the root of the
+	// larger tree.
+	switch {
+	case e1Root.rank < e2Root.rank:
+		e1Root.parent = e2Root
+	case e1Root.rank > e2Root.rank:
+		e2Root.parent = e1Root
+	default:
+		e2Root.parent = e1Root
+		e1Root.rank++
+	}
+}
+
+// An edgeIdxs describes an edge in a graph or tree.  The vertices in the edge
+// are indexes into a list of Color values.
+type edgeIdxs [2]int
+
+// An edgeDistance is a map from an edge (pair of indices) to a distance
+// between the two vertices.
+type edgeDistance map[edgeIdxs]float64
+
+// allToAllDistancesCIEDE2000 computes the CIEDE2000 distance between each pair of
+// colors.  It returns a map from a pair of indices (u, v) with u < v to a
+// distance.
+func allToAllDistancesCIEDE2000(cs []Color) edgeDistance {
+	nc := len(cs)
+	m := make(edgeDistance, nc*nc)
+	for u := 0; u < nc-1; u++ {
+		for v := u + 1; v < nc; v++ {
+			m[edgeIdxs{u, v}] = cs[u].DistanceCIEDE2000(cs[v])
+		}
+	}
+	return m
+}
+
+// sortEdges sorts all edges in a distance map by increasing vertex distance.
+func sortEdges(m edgeDistance) []edgeIdxs {
+	es := make([]edgeIdxs, 0, len(m))
+	for uv := range m {
+		es = append(es, uv)
+	}
+	sort.Slice(es, func(i, j int) bool {
+		return m[es[i]] < m[es[j]]
+	})
+	return es
+}
+
+// minSpanTree computes a minimum spanning tree from a vertex count and a
+// distance-sorted edge list.  It returns the subset of edges that belong to
+// the tree, including both (u, v) and (v, u) for each edge.
+func minSpanTree(nc int, es []edgeIdxs) map[edgeIdxs]struct{} {
+	// Start with each vertex in its own set.
+	elts := make([]*element, nc)
+	for i := range elts {
+		elts[i] = newElement()
+	}
+
+	// Run Kruskal's algorithm to construct a minimal spanning tree.
+	mst := make(map[edgeIdxs]struct{}, nc)
+	for _, uv := range es {
+		u, v := uv[0], uv[1]
+		if elts[u].find() == elts[v].find() {
+			continue // Same set: edge would introduce a cycle.
+		}
+		mst[uv] = struct{}{}
+		mst[edgeIdxs{v, u}] = struct{}{}
+		union(elts[u], elts[v])
+	}
+	return mst
+}
+
+// traverseMST walks a minimum spanning tree in prefix order.
+func traverseMST(mst map[edgeIdxs]struct{}, root int) []int {
+	// Compute a list of neighbors for each vertex.
+	neighs := make(map[int][]int, len(mst))
+	for uv := range mst {
+		u, v := uv[0], uv[1]
+		neighs[u] = append(neighs[u], v)
+	}
+	for u, vs := range neighs {
+		sort.Ints(vs)
+		copy(neighs[u], vs)
+	}
+
+	// Walk the tree from a given vertex.
+	order := make([]int, 0, len(neighs))
+	visited := make(map[int]bool, len(neighs))
+	var walkFrom func(int)
+	walkFrom = func(r int) {
+		// Visit the starting vertex.
+		order = append(order, r)
+		visited[r] = true
+
+		// Recursively visit each child in turn.
+		for _, c := range neighs[r] {
+			if !visited[c] {
+				walkFrom(c)
+			}
+		}
+	}
+	walkFrom(root)
+	return order
+}
+
+// Sorted sorts a list of Color values.  Sorting is not a well-defined operation
+// for colors so the intention here primarily is to order colors so that the
+// transition from one to the next is fairly smooth.
+func Sorted(cs []Color) []Color {
+	// Do nothing in trivial cases.
+	newCs := make([]Color, len(cs))
+	if len(cs) < 2 {
+		copy(newCs, cs)
+		return newCs
+	}
+
+	// Compute the distance from each color to every other color.
+	dists := allToAllDistancesCIEDE2000(cs)
+
+	// Produce a list of edges in increasing order of the distance between
+	// their vertices.
+	edges := sortEdges(dists)
+
+	// Construct a minimum spanning tree from the list of edges.
+	mst := minSpanTree(len(cs), edges)
+
+	// Find the darkest color in the list.
+	var black Color
+	var dIdx int             // Index of darkest color
+	light := math.MaxFloat64 // Lightness of darkest color (distance from black)
+	for i, c := range cs {
+		d := black.DistanceCIEDE2000(c)
+		if d < light {
+			dIdx = i
+			light = d
+		}
+	}
+
+	// Traverse the tree starting from the darkest color.
+	idxs := traverseMST(mst, dIdx)
+
+	// Convert the index list to a list of colors, overwriting the input.
+	for i, idx := range idxs {
+		newCs[i] = cs[idx]
+	}
+	return newCs
+}
diff --git a/vendor/github.com/lucasb-eyer/go-colorful/warm_palettegen.go b/vendor/github.com/lucasb-eyer/go-colorful/warm_palettegen.go
index 00f42a5cc..d294fb415 100644
--- a/vendor/github.com/lucasb-eyer/go-colorful/warm_palettegen.go
+++ b/vendor/github.com/lucasb-eyer/go-colorful/warm_palettegen.go
@@ -1,13 +1,9 @@
 package colorful
 
-import (
-	"math/rand"
-)
-
 // Uses the HSV color space to generate colors with similar S,V but distributed
 // evenly along their Hue. This is fast but not always pretty.
 // If you've got time to spare, use Lab (the non-fast below).
-func FastWarmPalette(colorsCount int) (colors []Color) {
+func FastWarmPaletteWithRand(colorsCount int, rand RandInterface) (colors []Color) {
 	colors = make([]Color, colorsCount)
 
 	for i := 0; i < colorsCount; i++ {
@@ -16,10 +12,18 @@ func FastWarmPalette(colorsCount int) (colors []Color) {
 	return
 }
 
-func WarmPalette(colorsCount int) ([]Color, error) {
+func FastWarmPalette(colorsCount int) (colors []Color) {
+	return FastWarmPaletteWithRand(colorsCount, getDefaultGlobalRand())
+}
+
+func WarmPaletteWithRand(colorsCount int, rand RandInterface) ([]Color, error) {
 	warmy := func(l, a, b float64) bool {
 		_, c, _ := LabToHcl(l, a, b)
 		return 0.1 <= c && c <= 0.4 && 0.2 <= l && l <= 0.5
 	}
-	return SoftPaletteEx(colorsCount, SoftPaletteSettings{warmy, 50, true})
+	return SoftPaletteExWithRand(colorsCount, SoftPaletteSettings{warmy, 50, true}, rand)
+}
+
+func WarmPalette(colorsCount int) ([]Color, error) {
+	return WarmPaletteWithRand(colorsCount, getDefaultGlobalRand())
 }
diff --git a/vendor/github.com/mailru/easyjson/LICENSE b/vendor/github.com/mailru/easyjson/LICENSE
new file mode 100644
index 000000000..fbff658f7
--- /dev/null
+++ b/vendor/github.com/mailru/easyjson/LICENSE
@@ -0,0 +1,7 @@
+Copyright (c) 2016 Mail.Ru Group
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/github.com/mailru/easyjson/buffer/pool.go b/vendor/github.com/mailru/easyjson/buffer/pool.go
new file mode 100644
index 000000000..598a54af9
--- /dev/null
+++ b/vendor/github.com/mailru/easyjson/buffer/pool.go
@@ -0,0 +1,278 @@
+// Package buffer implements a buffer for serialization, consisting of a chain of []byte-s to
+// reduce copying and to allow reuse of individual chunks.
+package buffer
+
+import (
+	"io"
+	"net"
+	"sync"
+)
+
+// PoolConfig contains configuration for the allocation and reuse strategy.
+type PoolConfig struct {
+	StartSize  int // Minimum chunk size that is allocated.
+	PooledSize int // Minimum chunk size that is reused, reusing chunks too small will result in overhead.
+	MaxSize    int // Maximum chunk size that will be allocated.
+}
+
+var config = PoolConfig{
+	StartSize:  128,
+	PooledSize: 512,
+	MaxSize:    32768,
+}
+
+// Reuse pool: chunk size -> pool.
+var buffers = map[int]*sync.Pool{}
+
+func initBuffers() {
+	for l := config.PooledSize; l <= config.MaxSize; l *= 2 {
+		buffers[l] = new(sync.Pool)
+	}
+}
+
+func init() {
+	initBuffers()
+}
+
+// Init sets up a non-default pooling and allocation strategy. Should be run before serialization is done.
+func Init(cfg PoolConfig) {
+	config = cfg
+	initBuffers()
+}
+
+// putBuf puts a chunk to reuse pool if it can be reused.
+func putBuf(buf []byte) {
+	size := cap(buf)
+	if size < config.PooledSize {
+		return
+	}
+	if c := buffers[size]; c != nil {
+		c.Put(buf[:0])
+	}
+}
+
+// getBuf gets a chunk from reuse pool or creates a new one if reuse failed.
+func getBuf(size int) []byte {
+	if size >= config.PooledSize {
+		if c := buffers[size]; c != nil {
+			v := c.Get()
+			if v != nil {
+				return v.([]byte)
+			}
+		}
+	}
+	return make([]byte, 0, size)
+}
+
+// Buffer is a buffer optimized for serialization without extra copying.
+type Buffer struct {
+
+	// Buf is the current chunk that can be used for serialization.
+	Buf []byte
+
+	toPool []byte
+	bufs   [][]byte
+}
+
+// EnsureSpace makes sure that the current chunk contains at least s free bytes,
+// possibly creating a new chunk.
+func (b *Buffer) EnsureSpace(s int) {
+	if cap(b.Buf)-len(b.Buf) < s {
+		b.ensureSpaceSlow(s)
+	}
+}
+
+func (b *Buffer) ensureSpaceSlow(s int) {
+	l := len(b.Buf)
+	if l > 0 {
+		if cap(b.toPool) != cap(b.Buf) {
+			// Chunk was reallocated, toPool can be pooled.
+			putBuf(b.toPool)
+		}
+		if cap(b.bufs) == 0 {
+			b.bufs = make([][]byte, 0, 8)
+		}
+		b.bufs = append(b.bufs, b.Buf)
+		l = cap(b.toPool) * 2
+	} else {
+		l = config.StartSize
+	}
+
+	if l > config.MaxSize {
+		l = config.MaxSize
+	}
+	b.Buf = getBuf(l)
+	b.toPool = b.Buf
+}
+
+// AppendByte appends a single byte to buffer.
+func (b *Buffer) AppendByte(data byte) {
+	b.EnsureSpace(1)
+	b.Buf = append(b.Buf, data)
+}
+
+// AppendBytes appends a byte slice to buffer.
+func (b *Buffer) AppendBytes(data []byte) {
+	if len(data) <= cap(b.Buf)-len(b.Buf) {
+		b.Buf = append(b.Buf, data...) // fast path
+	} else {
+		b.appendBytesSlow(data)
+	}
+}
+
+func (b *Buffer) appendBytesSlow(data []byte) {
+	for len(data) > 0 {
+		b.EnsureSpace(1)
+
+		sz := cap(b.Buf) - len(b.Buf)
+		if sz > len(data) {
+			sz = len(data)
+		}
+
+		b.Buf = append(b.Buf, data[:sz]...)
+		data = data[sz:]
+	}
+}
+
+// AppendString appends a string to buffer.
+func (b *Buffer) AppendString(data string) {
+	if len(data) <= cap(b.Buf)-len(b.Buf) {
+		b.Buf = append(b.Buf, data...) // fast path
+	} else {
+		b.appendStringSlow(data)
+	}
+}
+
+func (b *Buffer) appendStringSlow(data string) {
+	for len(data) > 0 {
+		b.EnsureSpace(1)
+
+		sz := cap(b.Buf) - len(b.Buf)
+		if sz > len(data) {
+			sz = len(data)
+		}
+
+		b.Buf = append(b.Buf, data[:sz]...)
+		data = data[sz:]
+	}
+}
+
+// Size computes the size of a buffer by adding sizes of every chunk.
+func (b *Buffer) Size() int {
+	size := len(b.Buf)
+	for _, buf := range b.bufs {
+		size += len(buf)
+	}
+	return size
+}
+
+// DumpTo outputs the contents of a buffer to a writer and resets the buffer.
+func (b *Buffer) DumpTo(w io.Writer) (written int, err error) {
+	bufs := net.Buffers(b.bufs)
+	if len(b.Buf) > 0 {
+		bufs = append(bufs, b.Buf)
+	}
+	n, err := bufs.WriteTo(w)
+
+	for _, buf := range b.bufs {
+		putBuf(buf)
+	}
+	putBuf(b.toPool)
+
+	b.bufs = nil
+	b.Buf = nil
+	b.toPool = nil
+
+	return int(n), err
+}
+
+// BuildBytes creates a single byte slice with all the contents of the buffer. Data is
+// copied if it does not fit in a single chunk. You can optionally provide one byte
+// slice as argument that it will try to reuse.
+func (b *Buffer) BuildBytes(reuse ...[]byte) []byte {
+	if len(b.bufs) == 0 {
+		ret := b.Buf
+		b.toPool = nil
+		b.Buf = nil
+		return ret
+	}
+
+	var ret []byte
+	size := b.Size()
+
+	// If we got a buffer as argument and it is big enough, reuse it.
+	if len(reuse) == 1 && cap(reuse[0]) >= size {
+		ret = reuse[0][:0]
+	} else {
+		ret = make([]byte, 0, size)
+	}
+	for _, buf := range b.bufs {
+		ret = append(ret, buf...)
+		putBuf(buf)
+	}
+
+	ret = append(ret, b.Buf...)
+	putBuf(b.toPool)
+
+	b.bufs = nil
+	b.toPool = nil
+	b.Buf = nil
+
+	return ret
+}
+
+type readCloser struct {
+	offset int
+	bufs   [][]byte
+}
+
+func (r *readCloser) Read(p []byte) (n int, err error) {
+	for _, buf := range r.bufs {
+		// Copy as much as we can.
+		x := copy(p[n:], buf[r.offset:])
+		n += x // Increment how much we filled.
+
+		// Did we empty the whole buffer?
+		if r.offset+x == len(buf) {
+			// On to the next buffer.
+			r.offset = 0
+			r.bufs = r.bufs[1:]
+
+			// We can release this buffer.
+			putBuf(buf)
+		} else {
+			r.offset += x
+		}
+
+		if n == len(p) {
+			break
+		}
+	}
+	// No buffers left or nothing read?
+	if len(r.bufs) == 0 {
+		err = io.EOF
+	}
+	return
+}
+
+func (r *readCloser) Close() error {
+	// Release all remaining buffers.
+	for _, buf := range r.bufs {
+		putBuf(buf)
+	}
+	// In case Close gets called multiple times.
+	r.bufs = nil
+
+	return nil
+}
+
+// ReadCloser creates an io.ReadCloser with all the contents of the buffer.
+func (b *Buffer) ReadCloser() io.ReadCloser {
+	ret := &readCloser{0, append(b.bufs, b.Buf)}
+
+	b.bufs = nil
+	b.toPool = nil
+	b.Buf = nil
+
+	return ret
+}
diff --git a/vendor/github.com/mailru/easyjson/jwriter/writer.go b/vendor/github.com/mailru/easyjson/jwriter/writer.go
new file mode 100644
index 000000000..2c5b20105
--- /dev/null
+++ b/vendor/github.com/mailru/easyjson/jwriter/writer.go
@@ -0,0 +1,405 @@
+// Package jwriter contains a JSON writer.
+package jwriter
+
+import (
+	"io"
+	"strconv"
+	"unicode/utf8"
+
+	"github.com/mailru/easyjson/buffer"
+)
+
+// Flags describe various encoding options. The behavior may be actually implemented in the encoder, but
+// Flags field in Writer is used to set and pass them around.
+type Flags int
+
+const (
+	NilMapAsEmpty   Flags = 1 << iota // Encode nil map as '{}' rather than 'null'.
+	NilSliceAsEmpty                   // Encode nil slice as '[]' rather than 'null'.
+)
+
+// Writer is a JSON writer.
+type Writer struct {
+	Flags Flags
+
+	Error        error
+	Buffer       buffer.Buffer
+	NoEscapeHTML bool
+}
+
+// Size returns the size of the data that was written out.
+func (w *Writer) Size() int {
+	return w.Buffer.Size()
+}
+
+// DumpTo outputs the data to given io.Writer, resetting the buffer.
+func (w *Writer) DumpTo(out io.Writer) (written int, err error) {
+	return w.Buffer.DumpTo(out)
+}
+
+// BuildBytes returns writer data as a single byte slice. You can optionally provide one byte slice
+// as argument that it will try to reuse.
+func (w *Writer) BuildBytes(reuse ...[]byte) ([]byte, error) {
+	if w.Error != nil {
+		return nil, w.Error
+	}
+
+	return w.Buffer.BuildBytes(reuse...), nil
+}
+
+// ReadCloser returns an io.ReadCloser that can be used to read the data.
+// ReadCloser also resets the buffer.
+func (w *Writer) ReadCloser() (io.ReadCloser, error) {
+	if w.Error != nil {
+		return nil, w.Error
+	}
+
+	return w.Buffer.ReadCloser(), nil
+}
+
+// RawByte appends raw binary data to the buffer.
+func (w *Writer) RawByte(c byte) {
+	w.Buffer.AppendByte(c)
+}
+
+// RawByte appends raw binary data to the buffer.
+func (w *Writer) RawString(s string) {
+	w.Buffer.AppendString(s)
+}
+
+// Raw appends raw binary data to the buffer or sets the error if it is given. Useful for
+// calling with results of MarshalJSON-like functions.
+func (w *Writer) Raw(data []byte, err error) {
+	switch {
+	case w.Error != nil:
+		return
+	case err != nil:
+		w.Error = err
+	case len(data) > 0:
+		w.Buffer.AppendBytes(data)
+	default:
+		w.RawString("null")
+	}
+}
+
+// RawText encloses raw binary data in quotes and appends in to the buffer.
+// Useful for calling with results of MarshalText-like functions.
+func (w *Writer) RawText(data []byte, err error) {
+	switch {
+	case w.Error != nil:
+		return
+	case err != nil:
+		w.Error = err
+	case len(data) > 0:
+		w.String(string(data))
+	default:
+		w.RawString("null")
+	}
+}
+
+// Base64Bytes appends data to the buffer after base64 encoding it
+func (w *Writer) Base64Bytes(data []byte) {
+	if data == nil {
+		w.Buffer.AppendString("null")
+		return
+	}
+	w.Buffer.AppendByte('"')
+	w.base64(data)
+	w.Buffer.AppendByte('"')
+}
+
+func (w *Writer) Uint8(n uint8) {
+	w.Buffer.EnsureSpace(3)
+	w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
+}
+
+func (w *Writer) Uint16(n uint16) {
+	w.Buffer.EnsureSpace(5)
+	w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
+}
+
+func (w *Writer) Uint32(n uint32) {
+	w.Buffer.EnsureSpace(10)
+	w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
+}
+
+func (w *Writer) Uint(n uint) {
+	w.Buffer.EnsureSpace(20)
+	w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
+}
+
+func (w *Writer) Uint64(n uint64) {
+	w.Buffer.EnsureSpace(20)
+	w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, n, 10)
+}
+
+func (w *Writer) Int8(n int8) {
+	w.Buffer.EnsureSpace(4)
+	w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
+}
+
+func (w *Writer) Int16(n int16) {
+	w.Buffer.EnsureSpace(6)
+	w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
+}
+
+func (w *Writer) Int32(n int32) {
+	w.Buffer.EnsureSpace(11)
+	w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
+}
+
+func (w *Writer) Int(n int) {
+	w.Buffer.EnsureSpace(21)
+	w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
+}
+
+func (w *Writer) Int64(n int64) {
+	w.Buffer.EnsureSpace(21)
+	w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, n, 10)
+}
+
+func (w *Writer) Uint8Str(n uint8) {
+	w.Buffer.EnsureSpace(3)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+	w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+}
+
+func (w *Writer) Uint16Str(n uint16) {
+	w.Buffer.EnsureSpace(5)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+	w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+}
+
+func (w *Writer) Uint32Str(n uint32) {
+	w.Buffer.EnsureSpace(10)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+	w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+}
+
+func (w *Writer) UintStr(n uint) {
+	w.Buffer.EnsureSpace(20)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+	w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+}
+
+func (w *Writer) Uint64Str(n uint64) {
+	w.Buffer.EnsureSpace(20)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+	w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, n, 10)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+}
+
+func (w *Writer) UintptrStr(n uintptr) {
+	w.Buffer.EnsureSpace(20)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+	w.Buffer.Buf = strconv.AppendUint(w.Buffer.Buf, uint64(n), 10)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+}
+
+func (w *Writer) Int8Str(n int8) {
+	w.Buffer.EnsureSpace(4)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+	w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+}
+
+func (w *Writer) Int16Str(n int16) {
+	w.Buffer.EnsureSpace(6)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+	w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+}
+
+func (w *Writer) Int32Str(n int32) {
+	w.Buffer.EnsureSpace(11)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+	w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+}
+
+func (w *Writer) IntStr(n int) {
+	w.Buffer.EnsureSpace(21)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+	w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, int64(n), 10)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+}
+
+func (w *Writer) Int64Str(n int64) {
+	w.Buffer.EnsureSpace(21)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+	w.Buffer.Buf = strconv.AppendInt(w.Buffer.Buf, n, 10)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+}
+
+func (w *Writer) Float32(n float32) {
+	w.Buffer.EnsureSpace(20)
+	w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, float64(n), 'g', -1, 32)
+}
+
+func (w *Writer) Float32Str(n float32) {
+	w.Buffer.EnsureSpace(20)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+	w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, float64(n), 'g', -1, 32)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+}
+
+func (w *Writer) Float64(n float64) {
+	w.Buffer.EnsureSpace(20)
+	w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, n, 'g', -1, 64)
+}
+
+func (w *Writer) Float64Str(n float64) {
+	w.Buffer.EnsureSpace(20)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+	w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, float64(n), 'g', -1, 64)
+	w.Buffer.Buf = append(w.Buffer.Buf, '"')
+}
+
+func (w *Writer) Bool(v bool) {
+	w.Buffer.EnsureSpace(5)
+	if v {
+		w.Buffer.Buf = append(w.Buffer.Buf, "true"...)
+	} else {
+		w.Buffer.Buf = append(w.Buffer.Buf, "false"...)
+	}
+}
+
+const chars = "0123456789abcdef"
+
+func getTable(falseValues ...int) [128]bool {
+	table := [128]bool{}
+
+	for i := 0; i < 128; i++ {
+		table[i] = true
+	}
+
+	for _, v := range falseValues {
+		table[v] = false
+	}
+
+	return table
+}
+
+var (
+	htmlEscapeTable   = getTable(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, '"', '&', '<', '>', '\\')
+	htmlNoEscapeTable = getTable(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, '"', '\\')
+)
+
+func (w *Writer) String(s string) {
+	w.Buffer.AppendByte('"')
+
+	// Portions of the string that contain no escapes are appended as
+	// byte slices.
+
+	p := 0 // last non-escape symbol
+
+	escapeTable := &htmlEscapeTable
+	if w.NoEscapeHTML {
+		escapeTable = &htmlNoEscapeTable
+	}
+
+	for i := 0; i < len(s); {
+		c := s[i]
+
+		if c < utf8.RuneSelf {
+			if escapeTable[c] {
+				// single-width character, no escaping is required
+				i++
+				continue
+			}
+
+			w.Buffer.AppendString(s[p:i])
+			switch c {
+			case '\t':
+				w.Buffer.AppendString(`\t`)
+			case '\r':
+				w.Buffer.AppendString(`\r`)
+			case '\n':
+				w.Buffer.AppendString(`\n`)
+			case '\\':
+				w.Buffer.AppendString(`\\`)
+			case '"':
+				w.Buffer.AppendString(`\"`)
+			default:
+				w.Buffer.AppendString(`\u00`)
+				w.Buffer.AppendByte(chars[c>>4])
+				w.Buffer.AppendByte(chars[c&0xf])
+			}
+
+			i++
+			p = i
+			continue
+		}
+
+		// broken utf
+		runeValue, runeWidth := utf8.DecodeRuneInString(s[i:])
+		if runeValue == utf8.RuneError && runeWidth == 1 {
+			w.Buffer.AppendString(s[p:i])
+			w.Buffer.AppendString(`\ufffd`)
+			i++
+			p = i
+			continue
+		}
+
+		// jsonp stuff - tab separator and line separator
+		if runeValue == '\u2028' || runeValue == '\u2029' {
+			w.Buffer.AppendString(s[p:i])
+			w.Buffer.AppendString(`\u202`)
+			w.Buffer.AppendByte(chars[runeValue&0xf])
+			i += runeWidth
+			p = i
+			continue
+		}
+		i += runeWidth
+	}
+	w.Buffer.AppendString(s[p:])
+	w.Buffer.AppendByte('"')
+}
+
+const encode = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+const padChar = '='
+
+func (w *Writer) base64(in []byte) {
+
+	if len(in) == 0 {
+		return
+	}
+
+	w.Buffer.EnsureSpace(((len(in)-1)/3 + 1) * 4)
+
+	si := 0
+	n := (len(in) / 3) * 3
+
+	for si < n {
+		// Convert 3x 8bit source bytes into 4 bytes
+		val := uint(in[si+0])<<16 | uint(in[si+1])<<8 | uint(in[si+2])
+
+		w.Buffer.Buf = append(w.Buffer.Buf, encode[val>>18&0x3F], encode[val>>12&0x3F], encode[val>>6&0x3F], encode[val&0x3F])
+
+		si += 3
+	}
+
+	remain := len(in) - si
+	if remain == 0 {
+		return
+	}
+
+	// Add the remaining small block
+	val := uint(in[si+0]) << 16
+	if remain == 2 {
+		val |= uint(in[si+1]) << 8
+	}
+
+	w.Buffer.Buf = append(w.Buffer.Buf, encode[val>>18&0x3F], encode[val>>12&0x3F])
+
+	switch remain {
+	case 2:
+		w.Buffer.Buf = append(w.Buffer.Buf, encode[val>>6&0x3F], byte(padChar))
+	case 1:
+		w.Buffer.Buf = append(w.Buffer.Buf, byte(padChar), byte(padChar))
+	}
+}
diff --git a/vendor/github.com/mattn/go-runewidth/benchstat.txt b/vendor/github.com/mattn/go-runewidth/benchstat.txt
new file mode 100644
index 000000000..a9efdbde3
--- /dev/null
+++ b/vendor/github.com/mattn/go-runewidth/benchstat.txt
@@ -0,0 +1,43 @@
+goos: darwin
+goarch: arm64
+pkg: github.com/mattn/go-runewidth
+cpu: Apple M2
+                                   │   old.txt    │              new.txt               │
+                                   │    sec/op    │   sec/op     vs base               │
+String1WidthAll/regular-8            108.92m ± 0%   35.09m ± 3%  -67.78% (p=0.002 n=6)
+String1WidthAll/lut-8                 93.97m ± 0%   18.70m ± 0%  -80.10% (p=0.002 n=6)
+String1Width768/regular-8             60.62µ ± 1%   11.54µ ± 0%  -80.97% (p=0.002 n=6)
+String1Width768/lut-8                 60.66µ ± 1%   11.43µ ± 0%  -81.16% (p=0.002 n=6)
+String1WidthAllEastAsian/regular-8   115.13m ± 1%   40.79m ± 8%  -64.57% (p=0.002 n=6)
+String1WidthAllEastAsian/lut-8        93.65m ± 0%   18.70m ± 2%  -80.03% (p=0.002 n=6)
+String1Width768EastAsian/regular-8    75.32µ ± 0%   23.49µ ± 0%  -68.82% (p=0.002 n=6)
+String1Width768EastAsian/lut-8        60.76µ ± 0%   11.50µ ± 0%  -81.07% (p=0.002 n=6)
+geomean                               2.562m        604.5µ       -76.41%
+
+                                   │   old.txt    │                 new.txt                 │
+                                   │     B/op     │    B/op      vs base                    │
+String1WidthAll/regular-8            106.3Mi ± 0%    0.0Mi ± 0%  -100.00% (p=0.002 n=6)
+String1WidthAll/lut-8                106.3Mi ± 0%    0.0Mi ± 0%  -100.00% (p=0.002 n=6)
+String1Width768/regular-8            75.00Ki ± 0%   0.00Ki ± 0%  -100.00% (p=0.002 n=6)
+String1Width768/lut-8                75.00Ki ± 0%   0.00Ki ± 0%  -100.00% (p=0.002 n=6)
+String1WidthAllEastAsian/regular-8   106.3Mi ± 0%    0.0Mi ± 0%  -100.00% (p=0.002 n=6)
+String1WidthAllEastAsian/lut-8       106.3Mi ± 0%    0.0Mi ± 0%  -100.00% (p=0.002 n=6)
+String1Width768EastAsian/regular-8   75.00Ki ± 0%   0.00Ki ± 0%  -100.00% (p=0.002 n=6)
+String1Width768EastAsian/lut-8       75.00Ki ± 0%   0.00Ki ± 0%  -100.00% (p=0.002 n=6)
+geomean                              2.790Mi                     ?                      ¹ ²
+¹ summaries must be >0 to compute geomean
+² ratios must be >0 to compute geomean
+
+                                   │   old.txt   │                 new.txt                 │
+                                   │  allocs/op  │  allocs/op   vs base                    │
+String1WidthAll/regular-8            3.342M ± 0%   0.000M ± 0%  -100.00% (p=0.002 n=6)
+String1WidthAll/lut-8                3.342M ± 0%   0.000M ± 0%  -100.00% (p=0.002 n=6)
+String1Width768/regular-8            2.304k ± 0%   0.000k ± 0%  -100.00% (p=0.002 n=6)
+String1Width768/lut-8                2.304k ± 0%   0.000k ± 0%  -100.00% (p=0.002 n=6)
+String1WidthAllEastAsian/regular-8   3.342M ± 0%   0.000M ± 0%  -100.00% (p=0.002 n=6)
+String1WidthAllEastAsian/lut-8       3.342M ± 0%   0.000M ± 0%  -100.00% (p=0.002 n=6)
+String1Width768EastAsian/regular-8   2.304k ± 0%   0.000k ± 0%  -100.00% (p=0.002 n=6)
+String1Width768EastAsian/lut-8       2.304k ± 0%   0.000k ± 0%  -100.00% (p=0.002 n=6)
+geomean                              87.75k                     ?                      ¹ ²
+¹ summaries must be >0 to compute geomean
+² ratios must be >0 to compute geomean
diff --git a/vendor/github.com/mattn/go-runewidth/new.txt b/vendor/github.com/mattn/go-runewidth/new.txt
new file mode 100644
index 000000000..889071256
--- /dev/null
+++ b/vendor/github.com/mattn/go-runewidth/new.txt
@@ -0,0 +1,54 @@
+goos: darwin
+goarch: arm64
+pkg: github.com/mattn/go-runewidth
+cpu: Apple M2
+BenchmarkString1WidthAll/regular-8    	      33	  35033923 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAll/regular-8    	      33	  34965112 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAll/regular-8    	      33	  36307234 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAll/regular-8    	      33	  35007705 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAll/regular-8    	      33	  35154182 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAll/regular-8    	      34	  35155400 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAll/lut-8        	      63	  18688500 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAll/lut-8        	      63	  18712474 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAll/lut-8        	      63	  18700211 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAll/lut-8        	      62	  18694179 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAll/lut-8        	      62	  18708392 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAll/lut-8        	      63	  18770608 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768/regular-8    	  104137	     11526 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768/regular-8    	  103986	     11540 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768/regular-8    	  104079	     11552 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768/regular-8    	  103963	     11530 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768/regular-8    	  103714	     11538 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768/regular-8    	  104181	     11537 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768/lut-8        	  105150	     11420 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768/lut-8        	  104778	     11423 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768/lut-8        	  105069	     11422 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768/lut-8        	  105127	     11475 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768/lut-8        	  104742	     11433 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768/lut-8        	  105163	     11432 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAllEastAsian/regular-8         	      28	  40723347 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAllEastAsian/regular-8         	      28	  40790299 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAllEastAsian/regular-8         	      28	  40801338 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAllEastAsian/regular-8         	      28	  40798216 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAllEastAsian/regular-8         	      28	  44135253 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAllEastAsian/regular-8         	      28	  40779546 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAllEastAsian/lut-8             	      62	  18694165 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAllEastAsian/lut-8             	      62	  18685047 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAllEastAsian/lut-8             	      62	  18689273 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAllEastAsian/lut-8             	      62	  19150346 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAllEastAsian/lut-8             	      63	  19126154 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1WidthAllEastAsian/lut-8             	      62	  18712619 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768EastAsian/regular-8         	   50775	     23595 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768EastAsian/regular-8         	   51061	     23563 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768EastAsian/regular-8         	   51057	     23492 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768EastAsian/regular-8         	   51138	     23445 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768EastAsian/regular-8         	   51195	     23469 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768EastAsian/regular-8         	   51087	     23482 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768EastAsian/lut-8             	  104559	     11549 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768EastAsian/lut-8             	  104508	     11483 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768EastAsian/lut-8             	  104296	     11503 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768EastAsian/lut-8             	  104606	     11485 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768EastAsian/lut-8             	  104588	     11495 ns/op	       0 B/op	       0 allocs/op
+BenchmarkString1Width768EastAsian/lut-8             	  104602	     11518 ns/op	       0 B/op	       0 allocs/op
+PASS
+ok  	github.com/mattn/go-runewidth	64.455s
diff --git a/vendor/github.com/mattn/go-runewidth/old.txt b/vendor/github.com/mattn/go-runewidth/old.txt
new file mode 100644
index 000000000..5b9ac1646
--- /dev/null
+++ b/vendor/github.com/mattn/go-runewidth/old.txt
@@ -0,0 +1,54 @@
+goos: darwin
+goarch: arm64
+pkg: github.com/mattn/go-runewidth
+cpu: Apple M2
+BenchmarkString1WidthAll/regular-8    	      10	 108559258 ns/op	111412145 B/op	 3342342 allocs/op
+BenchmarkString1WidthAll/regular-8    	      10	 108968079 ns/op	111412364 B/op	 3342343 allocs/op
+BenchmarkString1WidthAll/regular-8    	      10	 108890338 ns/op	111412388 B/op	 3342344 allocs/op
+BenchmarkString1WidthAll/regular-8    	      10	 108940704 ns/op	111412584 B/op	 3342346 allocs/op
+BenchmarkString1WidthAll/regular-8    	      10	 108632796 ns/op	111412348 B/op	 3342343 allocs/op
+BenchmarkString1WidthAll/regular-8    	      10	 109354546 ns/op	111412777 B/op	 3342343 allocs/op
+BenchmarkString1WidthAll/lut-8        	      12	  93844406 ns/op	111412569 B/op	 3342345 allocs/op
+BenchmarkString1WidthAll/lut-8        	      12	  93991080 ns/op	111412512 B/op	 3342344 allocs/op
+BenchmarkString1WidthAll/lut-8        	      12	  93980632 ns/op	111412413 B/op	 3342343 allocs/op
+BenchmarkString1WidthAll/lut-8        	      12	  94004083 ns/op	111412396 B/op	 3342343 allocs/op
+BenchmarkString1WidthAll/lut-8        	      12	  93959795 ns/op	111412445 B/op	 3342343 allocs/op
+BenchmarkString1WidthAll/lut-8        	      12	  93846198 ns/op	111412556 B/op	 3342345 allocs/op
+BenchmarkString1Width768/regular-8    	   19785	     60696 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768/regular-8    	   19824	     60520 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768/regular-8    	   19832	     60547 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768/regular-8    	   19778	     60543 ns/op	   76800 B/op	    2304 allocs/op
+BenchmarkString1Width768/regular-8    	   19842	     61142 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768/regular-8    	   19780	     60696 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768/lut-8        	   19598	     61161 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768/lut-8        	   19731	     60707 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768/lut-8        	   19738	     60626 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768/lut-8        	   19764	     60670 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768/lut-8        	   19797	     60642 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768/lut-8        	   19738	     60608 ns/op	   76800 B/op	    2304 allocs/op
+BenchmarkString1WidthAllEastAsian/regular-8         	       9	 115080431 ns/op	111412458 B/op	 3342345 allocs/op
+BenchmarkString1WidthAllEastAsian/regular-8         	       9	 114908880 ns/op	111412476 B/op	 3342345 allocs/op
+BenchmarkString1WidthAllEastAsian/regular-8         	       9	 115077134 ns/op	111412540 B/op	 3342345 allocs/op
+BenchmarkString1WidthAllEastAsian/regular-8         	       9	 115175292 ns/op	111412467 B/op	 3342345 allocs/op
+BenchmarkString1WidthAllEastAsian/regular-8         	       9	 115792653 ns/op	111412362 B/op	 3342344 allocs/op
+BenchmarkString1WidthAllEastAsian/regular-8         	       9	 115255417 ns/op	111412572 B/op	 3342346 allocs/op
+BenchmarkString1WidthAllEastAsian/lut-8             	      12	  93761542 ns/op	111412538 B/op	 3342345 allocs/op
+BenchmarkString1WidthAllEastAsian/lut-8             	      12	  94089990 ns/op	111412440 B/op	 3342343 allocs/op
+BenchmarkString1WidthAllEastAsian/lut-8             	      12	  93721410 ns/op	111412514 B/op	 3342344 allocs/op
+BenchmarkString1WidthAllEastAsian/lut-8             	      12	  93572951 ns/op	111412329 B/op	 3342342 allocs/op
+BenchmarkString1WidthAllEastAsian/lut-8             	      12	  93536052 ns/op	111412206 B/op	 3342341 allocs/op
+BenchmarkString1WidthAllEastAsian/lut-8             	      12	  93532365 ns/op	111412412 B/op	 3342343 allocs/op
+BenchmarkString1Width768EastAsian/regular-8         	   15904	     75401 ns/op	   76800 B/op	    2304 allocs/op
+BenchmarkString1Width768EastAsian/regular-8         	   15932	     75449 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768EastAsian/regular-8         	   15944	     75181 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768EastAsian/regular-8         	   15963	     75311 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768EastAsian/regular-8         	   15879	     75292 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768EastAsian/regular-8         	   15955	     75334 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768EastAsian/lut-8             	   19692	     60692 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768EastAsian/lut-8             	   19712	     60699 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768EastAsian/lut-8             	   19741	     60819 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768EastAsian/lut-8             	   19771	     60653 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768EastAsian/lut-8             	   19737	     61027 ns/op	   76801 B/op	    2304 allocs/op
+BenchmarkString1Width768EastAsian/lut-8             	   19657	     60820 ns/op	   76801 B/op	    2304 allocs/op
+PASS
+ok  	github.com/mattn/go-runewidth	76.165s
diff --git a/vendor/github.com/mattn/go-runewidth/runewidth.go b/vendor/github.com/mattn/go-runewidth/runewidth.go
index 7dfbb3be9..0edabac39 100644
--- a/vendor/github.com/mattn/go-runewidth/runewidth.go
+++ b/vendor/github.com/mattn/go-runewidth/runewidth.go
@@ -4,7 +4,7 @@ import (
 	"os"
 	"strings"
 
-	"github.com/rivo/uniseg"
+	"github.com/clipperhouse/uax29/v2/graphemes"
 )
 
 //go:generate go run script/generate.go
@@ -64,6 +64,9 @@ func inTable(r rune, t table) bool {
 	if r < t[0].first {
 		return false
 	}
+	if r > t[len(t)-1].last {
+		return false
+	}
 
 	bot := 0
 	top := len(t) - 1
@@ -175,10 +178,10 @@ func (c *Condition) CreateLUT() {
 
 // StringWidth return width as you can see
 func (c *Condition) StringWidth(s string) (width int) {
-	g := uniseg.NewGraphemes(s)
+	g := graphemes.FromString(s)
 	for g.Next() {
 		var chWidth int
-		for _, r := range g.Runes() {
+		for _, r := range g.Value() {
 			chWidth = c.RuneWidth(r)
 			if chWidth > 0 {
 				break // Our best guess at this point is to use the width of the first non-zero-width rune.
@@ -197,17 +200,17 @@ func (c *Condition) Truncate(s string, w int, tail string) string {
 	w -= c.StringWidth(tail)
 	var width int
 	pos := len(s)
-	g := uniseg.NewGraphemes(s)
+	g := graphemes.FromString(s)
 	for g.Next() {
 		var chWidth int
-		for _, r := range g.Runes() {
+		for _, r := range g.Value() {
 			chWidth = c.RuneWidth(r)
 			if chWidth > 0 {
 				break // See StringWidth() for details.
 			}
 		}
 		if width+chWidth > w {
-			pos, _ = g.Positions()
+			pos = g.Start()
 			break
 		}
 		width += chWidth
@@ -224,10 +227,10 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
 	var width int
 	pos := len(s)
 
-	g := uniseg.NewGraphemes(s)
+	g := graphemes.FromString(s)
 	for g.Next() {
 		var chWidth int
-		for _, r := range g.Runes() {
+		for _, r := range g.Value() {
 			chWidth = c.RuneWidth(r)
 			if chWidth > 0 {
 				break // See StringWidth() for details.
@@ -236,10 +239,10 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
 
 		if width+chWidth > w {
 			if width < w {
-				_, pos = g.Positions()
+				pos = g.End()
 				prefix += strings.Repeat(" ", width+chWidth-w)
 			} else {
-				pos, _ = g.Positions()
+				pos = g.Start()
 			}
 
 			break
diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_windows.go b/vendor/github.com/mattn/go-runewidth/runewidth_windows.go
index 5f987a310..951500a24 100644
--- a/vendor/github.com/mattn/go-runewidth/runewidth_windows.go
+++ b/vendor/github.com/mattn/go-runewidth/runewidth_windows.go
@@ -4,6 +4,7 @@
 package runewidth
 
 import (
+	"os"
 	"syscall"
 )
 
@@ -14,6 +15,11 @@ var (
 
 // IsEastAsian return true if the current locale is CJK
 func IsEastAsian() bool {
+	if os.Getenv("WT_SESSION") != "" {
+		// Windows Terminal always not use East Asian Ambiguous Width(s).
+		return false
+	}
+
 	r1, _, _ := procGetConsoleOutputCP.Call()
 	if r1 == 0 {
 		return false
diff --git a/vendor/github.com/spf13/cobra/.golangci.yml b/vendor/github.com/spf13/cobra/.golangci.yml
index 2c8f4808c..6acf8ab1e 100644
--- a/vendor/github.com/spf13/cobra/.golangci.yml
+++ b/vendor/github.com/spf13/cobra/.golangci.yml
@@ -12,14 +12,20 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+version: "2"
+
 run:
-  deadline: 5m
+  timeout: 5m
+
+formatters:
+  enable:
+    - gofmt
+    - goimports
 
 linters:
-  disable-all: true
+  default: none
   enable:
     #- bodyclose
-    # - deadcode ! deprecated since v1.49.0; replaced by 'unused'
     #- depguard
     #- dogsled
     #- dupl
@@ -30,28 +36,24 @@ linters:
     - goconst
     - gocritic
     #- gocyclo
-    - gofmt
-    - goimports
-    #- gomnd
     #- goprintffuncname
     - gosec
-    - gosimple
     - govet
     - ineffassign
     #- lll
     - misspell
+    #- mnd
     #- nakedret
     #- noctx
     - nolintlint
     #- rowserrcheck
-    #- scopelint
     - staticcheck
-    #- structcheck ! deprecated since v1.49.0; replaced by 'unused'
-    - stylecheck
-    #- typecheck
     - unconvert
     #- unparam
     - unused
-    # - varcheck ! deprecated since v1.49.0; replaced by 'unused'
     #- whitespace
-  fast: false
+  exclusions:
+    presets:
+      - common-false-positives
+      - legacy
+      - std-error-handling
diff --git a/vendor/github.com/spf13/cobra/README.md b/vendor/github.com/spf13/cobra/README.md
index 71757151c..8416275f4 100644
--- a/vendor/github.com/spf13/cobra/README.md
+++ b/vendor/github.com/spf13/cobra/README.md
@@ -1,8 +1,14 @@
-
-![cobra logo](https://github.com/user-attachments/assets/cbc3adf8-0dff-46e9-a88d-5e2d971c169e)
+<div align="center">
+<a href="https://cobra.dev">
+<img width="512" height="535" alt="cobra-logo" src="https://github.com/user-attachments/assets/c8bf9aad-b5ae-41d3-8899-d83baec10af8" />
+</a>
+</div>
 
 Cobra is a library for creating powerful modern CLI applications.
 
+<a href="https://cobra.dev">Visit Cobra.dev for extensive documentation</a> 
+
+
 Cobra is used in many Go projects such as [Kubernetes](https://kubernetes.io/),
 [Hugo](https://gohugo.io), and [GitHub CLI](https://github.com/cli/cli) to
 name a few. [This list](site/content/projects_using_cobra.md) contains a more extensive list of projects using Cobra.
@@ -11,6 +17,20 @@ name a few. [This list](site/content/projects_using_cobra.md) contains a more ex
 [![Go Reference](https://pkg.go.dev/badge/github.com/spf13/cobra.svg)](https://pkg.go.dev/github.com/spf13/cobra)
 [![Go Report Card](https://goreportcard.com/badge/github.com/spf13/cobra)](https://goreportcard.com/report/github.com/spf13/cobra)
 [![Slack](https://img.shields.io/badge/Slack-cobra-brightgreen)](https://gophers.slack.com/archives/CD3LP1199)
+<hr>
+<div align="center" markdown="1">
+   <sup>Supported by:</sup>
+   <br>
+   <br>
+   <a href="https://www.warp.dev/cobra">
+      <img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/ab8dd143-b0fd-4904-bdc5-dd7ecac94eae">
+   </a>
+
+### [Warp, the AI terminal for devs](https://www.warp.dev/cobra)
+[Try Cobra in Warp today](https://www.warp.dev/cobra)<br>
+
+</div>
+<hr>
 
 # Overview
 
diff --git a/vendor/github.com/spf13/cobra/SECURITY.md b/vendor/github.com/spf13/cobra/SECURITY.md
new file mode 100644
index 000000000..54e60c28c
--- /dev/null
+++ b/vendor/github.com/spf13/cobra/SECURITY.md
@@ -0,0 +1,105 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+The `cobra` maintainers take security issues seriously and
+we appreciate your efforts to _**responsibly**_ disclose your findings.
+We will make every effort to swiftly respond and address concerns.
+
+To report a security vulnerability:
+
+1. **DO NOT** create a public GitHub issue for the vulnerability!
+2. **DO NOT** create a public GitHub Pull Request with a fix for the vulnerability!
+3. Send an email to `cobra-security@googlegroups.com`.
+4. Include the following details in your report:
+   - Description of the vulnerability
+   - Steps to reproduce
+   - Potential impact of the vulnerability (to your downstream project, to the Go ecosystem, etc.)
+   - Any potential mitigations you've already identified
+5. Allow up to 7 days for an initial response.
+   You should receive an acknowledgment of your report and an estimated timeline for a fix.
+6. (Optional) If you have a fix and would like to contribute your patch, please work
+   directly with the maintainers via `cobra-security@googlegroups.com` to
+   coordinate pushing the patch to GitHub, cutting a new release, and disclosing the change.
+
+## Response Process
+
+When a security vulnerability report is received, the `cobra` maintainers will:
+
+1. Confirm receipt of the vulnerability report within 7 days.
+2. Assess the report to determine if it constitutes a security vulnerability.
+3. If confirmed, assign the vulnerability a severity level and create a timeline for addressing it.
+4. Develop and test a fix.
+5. Patch the vulnerability and make a new GitHub release: the maintainers will coordinate disclosure with the reporter.
+6. Create a new GitHub Security Advisory to inform the broader Go ecosystem
+
+## Disclosure Policy
+
+The `cobra` maintainers follow a coordinated disclosure process:
+
+1. Security vulnerabilities will be addressed as quickly as possible.
+2. A CVE (Common Vulnerabilities and Exposures) identifier will be requested for significant vulnerabilities
+   that are within `cobra` itself.
+3. Once a fix is ready, the maintainers will:
+   - Release a new version containing the fix.
+   - Update the security advisory with details about the vulnerability.
+   - Credit the reporter (unless they wish to remain anonymous).
+   - Credit the fixer (unless they wish to remain anonymous, this may be the same as the reporter).
+   - Announce the vulnerability through appropriate channels
+     (GitHub Security Advisory, mailing lists, GitHub Releases, etc.)
+
+## Supported Versions
+
+Security fixes will typically only be released for the most recent major release.
+
+## Upstream Security Issues
+
+`cobra` generally will not accept vulnerability reports that originate in upstream
+dependencies. I.e., if there is a problem in Go code that `cobra` depends on,
+it is best to engage that project's maintainers and owners.
+
+This security policy primarily pertains only to `cobra` itself but if you believe you've
+identified a problem that originates in an upstream dependency and is being widely
+distributed by `cobra`, please follow the disclosure procedure above: the `cobra`
+maintainers will work with you to determine the severity and ecosystem impact.
+
+## Security Updates and CVEs
+
+Information about known security vulnerabilities and CVEs affecting `cobra` will
+be published as GitHub Security Advisories at
+https://github.com/spf13/cobra/security/advisories.
+
+All users are encouraged to watch the repository and upgrade promptly when
+security releases are published.
+
+## `cobra` Security Best Practices for Users
+
+When using `cobra` in your CLIs, the `cobra` maintainers recommend the following:
+
+1. Always use the latest version of `cobra`.
+2. [Use Go modules](https://go.dev/blog/using-go-modules) for dependency management.
+3. Always use the latest possible version of Go.
+
+## Security Best Practices for Contributors
+
+When contributing to `cobra`:
+
+1. Be mindful of security implications when adding new features or modifying existing ones.
+2. Be aware of `cobra`'s extremely large reach: it is used in nearly every Go CLI
+   (like Kubernetes, Docker, Prometheus, etc. etc.)
+3. Write tests that explicitly cover edge cases and potential issues.
+4. If you discover a security issue while working on `cobra`, please report it
+   following the process above rather than opening a public pull request or issue that
+   addresses the vulnerability.
+5. Take personal sec-ops seriously and secure your GitHub account: use [two-factor authentication](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa),
+   [sign your commits with a GPG or SSH key](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification),
+   etc.
+
+## Acknowledgments
+
+The `cobra` maintainers would like to thank all security researchers and
+community members who help keep cobra, its users, and the entire Go ecosystem secure through responsible disclosures!!
+
+---
+
+*This security policy is inspired by the [Open Web Application Security Project (OWASP)](https://owasp.org/) guidelines and security best practices.*
diff --git a/vendor/github.com/spf13/cobra/command.go b/vendor/github.com/spf13/cobra/command.go
index dbb2c298b..78088db69 100644
--- a/vendor/github.com/spf13/cobra/command.go
+++ b/vendor/github.com/spf13/cobra/command.go
@@ -39,7 +39,7 @@ const (
 )
 
 // FParseErrWhitelist configures Flag parse errors to be ignored
-type FParseErrWhitelist flag.ParseErrorsWhitelist
+type FParseErrWhitelist flag.ParseErrorsAllowlist
 
 // Group Structure to manage groups for commands
 type Group struct {
@@ -1296,6 +1296,11 @@ Simply type ` + c.DisplayName() + ` help [path to command] for full details.`,
 					c.Printf("Unknown help topic %#q\n", args)
 					CheckErr(c.Root().Usage())
 				} else {
+					// FLow the context down to be used in help text
+					if cmd.ctx == nil {
+						cmd.ctx = c.ctx
+					}
+
 					cmd.InitDefaultHelpFlag()    // make possible 'help' flag to be shown
 					cmd.InitDefaultVersionFlag() // make possible 'version' flag to be shown
 					CheckErr(cmd.Help())
@@ -1872,7 +1877,7 @@ func (c *Command) ParseFlags(args []string) error {
 	c.mergePersistentFlags()
 
 	// do it here after merging all flags and just before parse
-	c.Flags().ParseErrorsWhitelist = flag.ParseErrorsWhitelist(c.FParseErrWhitelist)
+	c.Flags().ParseErrorsAllowlist = flag.ParseErrorsAllowlist(c.FParseErrWhitelist)
 
 	err := c.Flags().Parse(args)
 	// Print warnings if they occurred (e.g. deprecated flag messages).
@@ -2020,7 +2025,7 @@ func defaultUsageFunc(w io.Writer, in interface{}) error {
 		fmt.Fprint(w, trimRightSpace(c.InheritedFlags().FlagUsages()))
 	}
 	if c.HasHelpSubCommands() {
-		fmt.Fprintf(w, "\n\nAdditional help topcis:")
+		fmt.Fprintf(w, "\n\nAdditional help topics:")
 		for _, subcmd := range c.Commands() {
 			if subcmd.IsAdditionalHelpTopicCommand() {
 				fmt.Fprintf(w, "\n  %s %s", rpad(subcmd.CommandPath(), subcmd.CommandPathPadding()), subcmd.Short)
diff --git a/vendor/github.com/spf13/cobra/completions.go b/vendor/github.com/spf13/cobra/completions.go
index a1752f763..d3607c2d2 100644
--- a/vendor/github.com/spf13/cobra/completions.go
+++ b/vendor/github.com/spf13/cobra/completions.go
@@ -115,6 +115,13 @@ type CompletionOptions struct {
 	DisableDescriptions bool
 	// HiddenDefaultCmd makes the default 'completion' command hidden
 	HiddenDefaultCmd bool
+	// DefaultShellCompDirective sets the ShellCompDirective that is returned
+	// if no special directive can be determined
+	DefaultShellCompDirective *ShellCompDirective
+}
+
+func (receiver *CompletionOptions) SetDefaultShellCompDirective(directive ShellCompDirective) {
+	receiver.DefaultShellCompDirective = &directive
 }
 
 // Completion is a string that can be used for completions
@@ -375,7 +382,7 @@ func (c *Command) getCompletions(args []string) (*Command, []Completion, ShellCo
 	// Error while attempting to parse flags
 	if flagErr != nil {
 		// If error type is flagCompError and we don't want flagCompletion we should ignore the error
-		if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) {
+		if _, ok := flagErr.(*flagCompError); !ok || flagCompletion {
 			return finalCmd, []Completion{}, ShellCompDirectiveDefault, flagErr
 		}
 	}
@@ -480,6 +487,14 @@ func (c *Command) getCompletions(args []string) (*Command, []Completion, ShellCo
 		}
 	} else {
 		directive = ShellCompDirectiveDefault
+		// check current and parent commands for a custom DefaultShellCompDirective
+		for cmd := finalCmd; cmd != nil; cmd = cmd.parent {
+			if cmd.CompletionOptions.DefaultShellCompDirective != nil {
+				directive = *cmd.CompletionOptions.DefaultShellCompDirective
+				break
+			}
+		}
+
 		if flag == nil {
 			foundLocalNonPersistentFlag := false
 			// If TraverseChildren is true on the root command we don't check for
@@ -773,7 +788,7 @@ See each sub-command's help for details on how to use the generated script.
 		// shell completion for it (prog __complete completion '')
 		subCmd, cmdArgs, err := c.Find(args)
 		if err != nil || subCmd.Name() != compCmdName &&
-			!(subCmd.Name() == ShellCompRequestCmd && len(cmdArgs) > 1 && cmdArgs[0] == compCmdName) {
+			(subCmd.Name() != ShellCompRequestCmd || len(cmdArgs) <= 1 || cmdArgs[0] != compCmdName) {
 			// The completion command is not being called or being completed so we remove it.
 			c.RemoveCommand(completionCmd)
 			return
diff --git a/vendor/github.com/spf13/pflag/README.md b/vendor/github.com/spf13/pflag/README.md
index 7eacc5bdb..388c4e5ea 100644
--- a/vendor/github.com/spf13/pflag/README.md
+++ b/vendor/github.com/spf13/pflag/README.md
@@ -284,6 +284,33 @@ func main() {
 }
 ```
 
+### Using pflag with go test
+`pflag` does not parse the shorthand versions of go test's built-in flags (i.e., those starting with `-test.`).
+For more context, see issues [#63](https://github.com/spf13/pflag/issues/63) and [#238](https://github.com/spf13/pflag/issues/238) for more details.
+
+For example, if you use pflag in your `TestMain` function and call `pflag.Parse()` after defining your custom flags, running a test like this:
+```bash
+go test /your/tests -run ^YourTest -v --your-test-pflags
+```
+will result in the `-v` flag being ignored. This happens because of the way pflag handles flag parsing, skipping over go test's built-in shorthand flags.
+To work around this, you can use the `ParseSkippedFlags` function, which ensures that go test's flags are parsed separately using the standard flag package.
+
+**Example**: You want to parse go test flags that are otherwise ignore by `pflag.Parse()`
+```go
+import (
+	goflag "flag"
+	flag "github.com/spf13/pflag"
+)
+
+var ip *int = flag.Int("flagname", 1234, "help message for flagname")
+
+func main() {
+	flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
+    flag.ParseSkippedFlags(os.Args[1:], goflag.CommandLine)
+	flag.Parse()
+}
+```
+
 ## More info
 
 You can see the full reference documentation of the pflag package
diff --git a/vendor/github.com/spf13/pflag/bool_func.go b/vendor/github.com/spf13/pflag/bool_func.go
new file mode 100644
index 000000000..83d77afa8
--- /dev/null
+++ b/vendor/github.com/spf13/pflag/bool_func.go
@@ -0,0 +1,40 @@
+package pflag
+
+// -- func Value
+type boolfuncValue func(string) error
+
+func (f boolfuncValue) Set(s string) error { return f(s) }
+
+func (f boolfuncValue) Type() string { return "boolfunc" }
+
+func (f boolfuncValue) String() string { return "" } // same behavior as stdlib 'flag' package
+
+func (f boolfuncValue) IsBoolFlag() bool { return true }
+
+// BoolFunc defines a func flag with specified name, callback function and usage string.
+//
+// The callback function will be called every time "--{name}" (or any form that matches the flag) is parsed
+// on the command line.
+func (f *FlagSet) BoolFunc(name string, usage string, fn func(string) error) {
+	f.BoolFuncP(name, "", usage, fn)
+}
+
+// BoolFuncP is like BoolFunc, but accepts a shorthand letter that can be used after a single dash.
+func (f *FlagSet) BoolFuncP(name, shorthand string, usage string, fn func(string) error) {
+	var val Value = boolfuncValue(fn)
+	flag := f.VarPF(val, name, shorthand, usage)
+	flag.NoOptDefVal = "true"
+}
+
+// BoolFunc defines a func flag with specified name, callback function and usage string.
+//
+// The callback function will be called every time "--{name}" (or any form that matches the flag) is parsed
+// on the command line.
+func BoolFunc(name string, usage string, fn func(string) error) {
+	CommandLine.BoolFuncP(name, "", usage, fn)
+}
+
+// BoolFuncP is like BoolFunc, but accepts a shorthand letter that can be used after a single dash.
+func BoolFuncP(name, shorthand string, usage string, fn func(string) error) {
+	CommandLine.BoolFuncP(name, shorthand, usage, fn)
+}
diff --git a/vendor/github.com/spf13/pflag/count.go b/vendor/github.com/spf13/pflag/count.go
index a0b2679f7..d49c0143c 100644
--- a/vendor/github.com/spf13/pflag/count.go
+++ b/vendor/github.com/spf13/pflag/count.go
@@ -85,7 +85,7 @@ func (f *FlagSet) CountP(name, shorthand string, usage string) *int {
 
 // Count defines a count flag with specified name, default value, and usage string.
 // The return value is the address of an int variable that stores the value of the flag.
-// A count flag will add 1 to its value evey time it is found on the command line
+// A count flag will add 1 to its value every time it is found on the command line
 func Count(name string, usage string) *int {
 	return CommandLine.CountP(name, "", usage)
 }
diff --git a/vendor/github.com/spf13/pflag/errors.go b/vendor/github.com/spf13/pflag/errors.go
new file mode 100644
index 000000000..ff11b66be
--- /dev/null
+++ b/vendor/github.com/spf13/pflag/errors.go
@@ -0,0 +1,149 @@
+package pflag
+
+import "fmt"
+
+// notExistErrorMessageType specifies which flavor of "flag does not exist"
+// is printed by NotExistError. This allows the related errors to be grouped
+// under a single NotExistError struct without making a breaking change to
+// the error message text.
+type notExistErrorMessageType int
+
+const (
+	flagNotExistMessage notExistErrorMessageType = iota
+	flagNotDefinedMessage
+	flagNoSuchFlagMessage
+	flagUnknownFlagMessage
+	flagUnknownShorthandFlagMessage
+)
+
+// NotExistError is the error returned when trying to access a flag that
+// does not exist in the FlagSet.
+type NotExistError struct {
+	name                string
+	specifiedShorthands string
+	messageType         notExistErrorMessageType
+}
+
+// Error implements error.
+func (e *NotExistError) Error() string {
+	switch e.messageType {
+	case flagNotExistMessage:
+		return fmt.Sprintf("flag %q does not exist", e.name)
+
+	case flagNotDefinedMessage:
+		return fmt.Sprintf("flag accessed but not defined: %s", e.name)
+
+	case flagNoSuchFlagMessage:
+		return fmt.Sprintf("no such flag -%v", e.name)
+
+	case flagUnknownFlagMessage:
+		return fmt.Sprintf("unknown flag: --%s", e.name)
+
+	case flagUnknownShorthandFlagMessage:
+		c := rune(e.name[0])
+		return fmt.Sprintf("unknown shorthand flag: %q in -%s", c, e.specifiedShorthands)
+	}
+
+	panic(fmt.Errorf("unknown flagNotExistErrorMessageType: %v", e.messageType))
+}
+
+// GetSpecifiedName returns the name of the flag (without dashes) as it
+// appeared in the parsed arguments.
+func (e *NotExistError) GetSpecifiedName() string {
+	return e.name
+}
+
+// GetSpecifiedShortnames returns the group of shorthand arguments
+// (without dashes) that the flag appeared within. If the flag was not in a
+// shorthand group, this will return an empty string.
+func (e *NotExistError) GetSpecifiedShortnames() string {
+	return e.specifiedShorthands
+}
+
+// ValueRequiredError is the error returned when a flag needs an argument but
+// no argument was provided.
+type ValueRequiredError struct {
+	flag                *Flag
+	specifiedName       string
+	specifiedShorthands string
+}
+
+// Error implements error.
+func (e *ValueRequiredError) Error() string {
+	if len(e.specifiedShorthands) > 0 {
+		c := rune(e.specifiedName[0])
+		return fmt.Sprintf("flag needs an argument: %q in -%s", c, e.specifiedShorthands)
+	}
+
+	return fmt.Sprintf("flag needs an argument: --%s", e.specifiedName)
+}
+
+// GetFlag returns the flag for which the error occurred.
+func (e *ValueRequiredError) GetFlag() *Flag {
+	return e.flag
+}
+
+// GetSpecifiedName returns the name of the flag (without dashes) as it
+// appeared in the parsed arguments.
+func (e *ValueRequiredError) GetSpecifiedName() string {
+	return e.specifiedName
+}
+
+// GetSpecifiedShortnames returns the group of shorthand arguments
+// (without dashes) that the flag appeared within. If the flag was not in a
+// shorthand group, this will return an empty string.
+func (e *ValueRequiredError) GetSpecifiedShortnames() string {
+	return e.specifiedShorthands
+}
+
+// InvalidValueError is the error returned when an invalid value is used
+// for a flag.
+type InvalidValueError struct {
+	flag  *Flag
+	value string
+	cause error
+}
+
+// Error implements error.
+func (e *InvalidValueError) Error() string {
+	flag := e.flag
+	var flagName string
+	if flag.Shorthand != "" && flag.ShorthandDeprecated == "" {
+		flagName = fmt.Sprintf("-%s, --%s", flag.Shorthand, flag.Name)
+	} else {
+		flagName = fmt.Sprintf("--%s", flag.Name)
+	}
+	return fmt.Sprintf("invalid argument %q for %q flag: %v", e.value, flagName, e.cause)
+}
+
+// Unwrap implements errors.Unwrap.
+func (e *InvalidValueError) Unwrap() error {
+	return e.cause
+}
+
+// GetFlag returns the flag for which the error occurred.
+func (e *InvalidValueError) GetFlag() *Flag {
+	return e.flag
+}
+
+// GetValue returns the invalid value that was provided.
+func (e *InvalidValueError) GetValue() string {
+	return e.value
+}
+
+// InvalidSyntaxError is the error returned when a bad flag name is passed on
+// the command line.
+type InvalidSyntaxError struct {
+	specifiedFlag string
+}
+
+// Error implements error.
+func (e *InvalidSyntaxError) Error() string {
+	return fmt.Sprintf("bad flag syntax: %s", e.specifiedFlag)
+}
+
+// GetSpecifiedName returns the exact flag (with dashes) as it
+// appeared in the parsed arguments.
+func (e *InvalidSyntaxError) GetSpecifiedFlag() string {
+	return e.specifiedFlag
+}
diff --git a/vendor/github.com/spf13/pflag/flag.go b/vendor/github.com/spf13/pflag/flag.go
index 7c058de37..2fd3c5759 100644
--- a/vendor/github.com/spf13/pflag/flag.go
+++ b/vendor/github.com/spf13/pflag/flag.go
@@ -27,23 +27,32 @@ unaffected.
 Define flags using flag.String(), Bool(), Int(), etc.
 
 This declares an integer flag, -flagname, stored in the pointer ip, with type *int.
+
 	var ip = flag.Int("flagname", 1234, "help message for flagname")
+
 If you like, you can bind the flag to a variable using the Var() functions.
+
 	var flagvar int
 	func init() {
 		flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")
 	}
+
 Or you can create custom flags that satisfy the Value interface (with
 pointer receivers) and couple them to flag parsing by
+
 	flag.Var(&flagVal, "name", "help message for flagname")
+
 For such flags, the default value is just the initial value of the variable.
 
 After all flags are defined, call
+
 	flag.Parse()
+
 to parse the command line into the defined flags.
 
 Flags may then be used directly. If you're using the flags themselves,
 they are all pointers; if you bind to variables, they're values.
+
 	fmt.Println("ip has value ", *ip)
 	fmt.Println("flagvar has value ", flagvar)
 
@@ -54,22 +63,26 @@ The arguments are indexed from 0 through flag.NArg()-1.
 The pflag package also defines some new functions that are not in flag,
 that give one-letter shorthands for flags. You can use these by appending
 'P' to the name of any function that defines a flag.
+
 	var ip = flag.IntP("flagname", "f", 1234, "help message")
 	var flagvar bool
 	func init() {
 		flag.BoolVarP(&flagvar, "boolname", "b", true, "help message")
 	}
 	flag.VarP(&flagval, "varname", "v", "help message")
+
 Shorthand letters can be used with single dashes on the command line.
 Boolean shorthand flags can be combined with other shorthand flags.
 
 Command line flag syntax:
+
 	--flag    // boolean flags only
 	--flag=x
 
 Unlike the flag package, a single dash before an option means something
 different than a double dash. Single dashes signify a series of shorthand
 letters for flags. All but the last shorthand letter must be boolean flags.
+
 	// boolean flags
 	-f
 	-abc
@@ -124,12 +137,17 @@ const (
 	PanicOnError
 )
 
-// ParseErrorsWhitelist defines the parsing errors that can be ignored
-type ParseErrorsWhitelist struct {
+// ParseErrorsAllowlist defines the parsing errors that can be ignored
+type ParseErrorsAllowlist struct {
 	// UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags
 	UnknownFlags bool
 }
 
+// ParseErrorsWhitelist defines the parsing errors that can be ignored.
+//
+// Deprecated: use [ParseErrorsAllowlist] instead. This type will be removed in a future release.
+type ParseErrorsWhitelist = ParseErrorsAllowlist
+
 // NormalizedName is a flag name that has been normalized according to rules
 // for the FlagSet (e.g. making '-' and '_' equivalent).
 type NormalizedName string
@@ -145,8 +163,13 @@ type FlagSet struct {
 	// help/usage messages.
 	SortFlags bool
 
-	// ParseErrorsWhitelist is used to configure a whitelist of errors
-	ParseErrorsWhitelist ParseErrorsWhitelist
+	// ParseErrorsAllowlist is used to configure an allowlist of errors
+	ParseErrorsAllowlist ParseErrorsAllowlist
+
+	// ParseErrorsAllowlist is used to configure an allowlist of errors.
+	//
+	// Deprecated: use [FlagSet.ParseErrorsAllowlist] instead. This field will be removed in a future release.
+	ParseErrorsWhitelist ParseErrorsAllowlist
 
 	name              string
 	parsed            bool
@@ -381,7 +404,7 @@ func (f *FlagSet) lookup(name NormalizedName) *Flag {
 func (f *FlagSet) getFlagType(name string, ftype string, convFunc func(sval string) (interface{}, error)) (interface{}, error) {
 	flag := f.Lookup(name)
 	if flag == nil {
-		err := fmt.Errorf("flag accessed but not defined: %s", name)
+		err := &NotExistError{name: name, messageType: flagNotDefinedMessage}
 		return nil, err
 	}
 
@@ -411,7 +434,7 @@ func (f *FlagSet) ArgsLenAtDash() int {
 func (f *FlagSet) MarkDeprecated(name string, usageMessage string) error {
 	flag := f.Lookup(name)
 	if flag == nil {
-		return fmt.Errorf("flag %q does not exist", name)
+		return &NotExistError{name: name, messageType: flagNotExistMessage}
 	}
 	if usageMessage == "" {
 		return fmt.Errorf("deprecated message for flag %q must be set", name)
@@ -427,7 +450,7 @@ func (f *FlagSet) MarkDeprecated(name string, usageMessage string) error {
 func (f *FlagSet) MarkShorthandDeprecated(name string, usageMessage string) error {
 	flag := f.Lookup(name)
 	if flag == nil {
-		return fmt.Errorf("flag %q does not exist", name)
+		return &NotExistError{name: name, messageType: flagNotExistMessage}
 	}
 	if usageMessage == "" {
 		return fmt.Errorf("deprecated message for flag %q must be set", name)
@@ -441,7 +464,7 @@ func (f *FlagSet) MarkShorthandDeprecated(name string, usageMessage string) erro
 func (f *FlagSet) MarkHidden(name string) error {
 	flag := f.Lookup(name)
 	if flag == nil {
-		return fmt.Errorf("flag %q does not exist", name)
+		return &NotExistError{name: name, messageType: flagNotExistMessage}
 	}
 	flag.Hidden = true
 	return nil
@@ -464,18 +487,16 @@ func (f *FlagSet) Set(name, value string) error {
 	normalName := f.normalizeFlagName(name)
 	flag, ok := f.formal[normalName]
 	if !ok {
-		return fmt.Errorf("no such flag -%v", name)
+		return &NotExistError{name: name, messageType: flagNoSuchFlagMessage}
 	}
 
 	err := flag.Value.Set(value)
 	if err != nil {
-		var flagName string
-		if flag.Shorthand != "" && flag.ShorthandDeprecated == "" {
-			flagName = fmt.Sprintf("-%s, --%s", flag.Shorthand, flag.Name)
-		} else {
-			flagName = fmt.Sprintf("--%s", flag.Name)
+		return &InvalidValueError{
+			flag:  flag,
+			value: value,
+			cause: err,
 		}
-		return fmt.Errorf("invalid argument %q for %q flag: %v", value, flagName, err)
 	}
 
 	if !flag.Changed {
@@ -501,7 +522,7 @@ func (f *FlagSet) SetAnnotation(name, key string, values []string) error {
 	normalName := f.normalizeFlagName(name)
 	flag, ok := f.formal[normalName]
 	if !ok {
-		return fmt.Errorf("no such flag -%v", name)
+		return &NotExistError{name: name, messageType: flagNoSuchFlagMessage}
 	}
 	if flag.Annotations == nil {
 		flag.Annotations = map[string][]string{}
@@ -538,7 +559,7 @@ func (f *FlagSet) PrintDefaults() {
 func (f *Flag) defaultIsZeroValue() bool {
 	switch f.Value.(type) {
 	case boolFlag:
-		return f.DefValue == "false"
+		return f.DefValue == "false" || f.DefValue == ""
 	case *durationValue:
 		// Beginning in Go 1.7, duration zero values are "0s"
 		return f.DefValue == "0" || f.DefValue == "0s"
@@ -551,7 +572,7 @@ func (f *Flag) defaultIsZeroValue() bool {
 	case *intSliceValue, *stringSliceValue, *stringArrayValue:
 		return f.DefValue == "[]"
 	default:
-		switch f.Value.String() {
+		switch f.DefValue {
 		case "false":
 			return true
 		case "<nil>":
@@ -588,8 +609,10 @@ func UnquoteUsage(flag *Flag) (name string, usage string) {
 
 	name = flag.Value.Type()
 	switch name {
-	case "bool":
+	case "bool", "boolfunc":
 		name = ""
+	case "func":
+		name = "value"
 	case "float64":
 		name = "float"
 	case "int64":
@@ -707,7 +730,7 @@ func (f *FlagSet) FlagUsagesWrapped(cols int) string {
 			switch flag.Value.Type() {
 			case "string":
 				line += fmt.Sprintf("[=\"%s\"]", flag.NoOptDefVal)
-			case "bool":
+			case "bool", "boolfunc":
 				if flag.NoOptDefVal != "true" {
 					line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
 				}
@@ -911,12 +934,10 @@ func VarP(value Value, name, shorthand, usage string) {
 	CommandLine.VarP(value, name, shorthand, usage)
 }
 
-// failf prints to standard error a formatted error and usage message and
+// fail prints an error message and usage message to standard error and
 // returns the error.
-func (f *FlagSet) failf(format string, a ...interface{}) error {
-	err := fmt.Errorf(format, a...)
+func (f *FlagSet) fail(err error) error {
 	if f.errorHandling != ContinueOnError {
-		fmt.Fprintln(f.Output(), err)
 		f.usage()
 	}
 	return err
@@ -934,9 +955,9 @@ func (f *FlagSet) usage() {
 	}
 }
 
-//--unknown (args will be empty)
-//--unknown --next-flag ... (args will be --next-flag ...)
-//--unknown arg ... (args will be arg ...)
+// --unknown (args will be empty)
+// --unknown --next-flag ... (args will be --next-flag ...)
+// --unknown arg ... (args will be arg ...)
 func stripUnknownFlagValue(args []string) []string {
 	if len(args) == 0 {
 		//--unknown
@@ -960,7 +981,7 @@ func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []strin
 	a = args
 	name := s[2:]
 	if len(name) == 0 || name[0] == '-' || name[0] == '=' {
-		err = f.failf("bad flag syntax: %s", s)
+		err = f.fail(&InvalidSyntaxError{specifiedFlag: s})
 		return
 	}
 
@@ -974,6 +995,8 @@ func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []strin
 			f.usage()
 			return a, ErrHelp
 		case f.ParseErrorsWhitelist.UnknownFlags:
+			fallthrough
+		case f.ParseErrorsAllowlist.UnknownFlags:
 			// --unknown=unknownval arg ...
 			// we do not want to lose arg in this case
 			if len(split) >= 2 {
@@ -982,7 +1005,7 @@ func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []strin
 
 			return stripUnknownFlagValue(a), nil
 		default:
-			err = f.failf("unknown flag: --%s", name)
+			err = f.fail(&NotExistError{name: name, messageType: flagUnknownFlagMessage})
 			return
 		}
 	}
@@ -1000,13 +1023,16 @@ func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []strin
 		a = a[1:]
 	} else {
 		// '--flag' (arg was required)
-		err = f.failf("flag needs an argument: %s", s)
+		err = f.fail(&ValueRequiredError{
+			flag:          flag,
+			specifiedName: name,
+		})
 		return
 	}
 
 	err = fn(flag, value)
 	if err != nil {
-		f.failf(err.Error())
+		f.fail(err)
 	}
 	return
 }
@@ -1014,7 +1040,7 @@ func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []strin
 func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parseFunc) (outShorts string, outArgs []string, err error) {
 	outArgs = args
 
-	if strings.HasPrefix(shorthands, "test.") {
+	if isGotestShorthandFlag(shorthands) {
 		return
 	}
 
@@ -1029,6 +1055,8 @@ func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parse
 			err = ErrHelp
 			return
 		case f.ParseErrorsWhitelist.UnknownFlags:
+			fallthrough
+		case f.ParseErrorsAllowlist.UnknownFlags:
 			// '-f=arg arg ...'
 			// we do not want to lose arg in this case
 			if len(shorthands) > 2 && shorthands[1] == '=' {
@@ -1039,7 +1067,11 @@ func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parse
 			outArgs = stripUnknownFlagValue(outArgs)
 			return
 		default:
-			err = f.failf("unknown shorthand flag: %q in -%s", c, shorthands)
+			err = f.fail(&NotExistError{
+				name:                string(c),
+				specifiedShorthands: shorthands,
+				messageType:         flagUnknownShorthandFlagMessage,
+			})
 			return
 		}
 	}
@@ -1062,7 +1094,11 @@ func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parse
 		outArgs = args[1:]
 	} else {
 		// '-f' (arg was required)
-		err = f.failf("flag needs an argument: %q in -%s", c, shorthands)
+		err = f.fail(&ValueRequiredError{
+			flag:                flag,
+			specifiedName:       string(c),
+			specifiedShorthands: shorthands,
+		})
 		return
 	}
 
@@ -1072,7 +1108,7 @@ func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parse
 
 	err = fn(flag, value)
 	if err != nil {
-		f.failf(err.Error())
+		f.fail(err)
 	}
 	return
 }
@@ -1135,12 +1171,12 @@ func (f *FlagSet) Parse(arguments []string) error {
 	}
 	f.parsed = true
 
-	if len(arguments) < 0 {
+	f.args = make([]string, 0, len(arguments))
+
+	if len(arguments) == 0 {
 		return nil
 	}
 
-	f.args = make([]string, 0, len(arguments))
-
 	set := func(flag *Flag, value string) error {
 		return f.Set(flag.Name, value)
 	}
@@ -1151,7 +1187,10 @@ func (f *FlagSet) Parse(arguments []string) error {
 		case ContinueOnError:
 			return err
 		case ExitOnError:
-			fmt.Println(err)
+			if err == ErrHelp {
+				os.Exit(0)
+			}
+			fmt.Fprintln(f.Output(), err)
 			os.Exit(2)
 		case PanicOnError:
 			panic(err)
@@ -1177,6 +1216,10 @@ func (f *FlagSet) ParseAll(arguments []string, fn func(flag *Flag, value string)
 		case ContinueOnError:
 			return err
 		case ExitOnError:
+			if err == ErrHelp {
+				os.Exit(0)
+			}
+			fmt.Fprintln(f.Output(), err)
 			os.Exit(2)
 		case PanicOnError:
 			panic(err)
diff --git a/vendor/github.com/spf13/pflag/func.go b/vendor/github.com/spf13/pflag/func.go
new file mode 100644
index 000000000..9f4d88f27
--- /dev/null
+++ b/vendor/github.com/spf13/pflag/func.go
@@ -0,0 +1,37 @@
+package pflag
+
+// -- func Value
+type funcValue func(string) error
+
+func (f funcValue) Set(s string) error { return f(s) }
+
+func (f funcValue) Type() string { return "func" }
+
+func (f funcValue) String() string { return "" } // same behavior as stdlib 'flag' package
+
+// Func defines a func flag with specified name, callback function and usage string.
+//
+// The callback function will be called every time "--{name}={value}" (or equivalent) is
+// parsed on the command line, with "{value}" as an argument.
+func (f *FlagSet) Func(name string, usage string, fn func(string) error) {
+	f.FuncP(name, "", usage, fn)
+}
+
+// FuncP is like Func, but accepts a shorthand letter that can be used after a single dash.
+func (f *FlagSet) FuncP(name string, shorthand string, usage string, fn func(string) error) {
+	var val Value = funcValue(fn)
+	f.VarP(val, name, shorthand, usage)
+}
+
+// Func defines a func flag with specified name, callback function and usage string.
+//
+// The callback function will be called every time "--{name}={value}" (or equivalent) is
+// parsed on the command line, with "{value}" as an argument.
+func Func(name string, usage string, fn func(string) error) {
+	CommandLine.FuncP(name, "", usage, fn)
+}
+
+// FuncP is like Func, but accepts a shorthand letter that can be used after a single dash.
+func FuncP(name, shorthand string, usage string, fn func(string) error) {
+	CommandLine.FuncP(name, shorthand, usage, fn)
+}
diff --git a/vendor/github.com/spf13/pflag/golangflag.go b/vendor/github.com/spf13/pflag/golangflag.go
index d3dd72b7f..e62eab538 100644
--- a/vendor/github.com/spf13/pflag/golangflag.go
+++ b/vendor/github.com/spf13/pflag/golangflag.go
@@ -8,8 +8,18 @@ import (
 	goflag "flag"
 	"reflect"
 	"strings"
+	"time"
 )
 
+// go test flags prefixes
+func isGotestFlag(flag string) bool {
+	return strings.HasPrefix(flag, "-test.")
+}
+
+func isGotestShorthandFlag(flag string) bool {
+	return strings.HasPrefix(flag, "test.")
+}
+
 // flagValueWrapper implements pflag.Value around a flag.Value.  The main
 // difference here is the addition of the Type method that returns a string
 // name of the type.  As this is generally unknown, we approximate that with
@@ -103,3 +113,49 @@ func (f *FlagSet) AddGoFlagSet(newSet *goflag.FlagSet) {
 	}
 	f.addedGoFlagSets = append(f.addedGoFlagSets, newSet)
 }
+
+// CopyToGoFlagSet will add all current flags to the given Go flag set.
+// Deprecation remarks get copied into the usage description.
+// Whenever possible, a flag gets added for which Go flags shows
+// a proper type in the help message.
+func (f *FlagSet) CopyToGoFlagSet(newSet *goflag.FlagSet) {
+	f.VisitAll(func(flag *Flag) {
+		usage := flag.Usage
+		if flag.Deprecated != "" {
+			usage += " (DEPRECATED: " + flag.Deprecated + ")"
+		}
+
+		switch value := flag.Value.(type) {
+		case *stringValue:
+			newSet.StringVar((*string)(value), flag.Name, flag.DefValue, usage)
+		case *intValue:
+			newSet.IntVar((*int)(value), flag.Name, *(*int)(value), usage)
+		case *int64Value:
+			newSet.Int64Var((*int64)(value), flag.Name, *(*int64)(value), usage)
+		case *uintValue:
+			newSet.UintVar((*uint)(value), flag.Name, *(*uint)(value), usage)
+		case *uint64Value:
+			newSet.Uint64Var((*uint64)(value), flag.Name, *(*uint64)(value), usage)
+		case *durationValue:
+			newSet.DurationVar((*time.Duration)(value), flag.Name, *(*time.Duration)(value), usage)
+		case *float64Value:
+			newSet.Float64Var((*float64)(value), flag.Name, *(*float64)(value), usage)
+		default:
+			newSet.Var(flag.Value, flag.Name, usage)
+		}
+	})
+}
+
+// ParseSkippedFlags explicitly Parses go test flags (i.e. the one starting with '-test.') with goflag.Parse(),
+// since by default those are skipped by pflag.Parse().
+// Typical usage example: `ParseGoTestFlags(os.Args[1:], goflag.CommandLine)`
+func ParseSkippedFlags(osArgs []string, goFlagSet *goflag.FlagSet) error {
+	var skippedFlags []string
+	for _, f := range osArgs {
+		if isGotestFlag(f) {
+			skippedFlags = append(skippedFlags, f)
+		}
+	}
+	return goFlagSet.Parse(skippedFlags)
+}
+
diff --git a/vendor/github.com/spf13/pflag/ipnet_slice.go b/vendor/github.com/spf13/pflag/ipnet_slice.go
index 6b541aa87..c6e89da18 100644
--- a/vendor/github.com/spf13/pflag/ipnet_slice.go
+++ b/vendor/github.com/spf13/pflag/ipnet_slice.go
@@ -73,7 +73,7 @@ func (s *ipNetSliceValue) String() string {
 
 func ipNetSliceConv(val string) (interface{}, error) {
 	val = strings.Trim(val, "[]")
-	// Emtpy string would cause a slice with one (empty) entry
+	// Empty string would cause a slice with one (empty) entry
 	if len(val) == 0 {
 		return []net.IPNet{}, nil
 	}
diff --git a/vendor/github.com/spf13/pflag/string_to_string.go b/vendor/github.com/spf13/pflag/string_to_string.go
index 890a01afc..1d1e3bf91 100644
--- a/vendor/github.com/spf13/pflag/string_to_string.go
+++ b/vendor/github.com/spf13/pflag/string_to_string.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"encoding/csv"
 	"fmt"
+	"sort"
 	"strings"
 )
 
@@ -62,8 +63,15 @@ func (s *stringToStringValue) Type() string {
 }
 
 func (s *stringToStringValue) String() string {
+	keys := make([]string, 0, len(*s.value))
+	for k := range *s.value {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+
 	records := make([]string, 0, len(*s.value)>>1)
-	for k, v := range *s.value {
+	for _, k := range keys {
+		v := (*s.value)[k]
 		records = append(records, k+"="+v)
 	}
 
diff --git a/vendor/github.com/spf13/pflag/text.go b/vendor/github.com/spf13/pflag/text.go
new file mode 100644
index 000000000..886d5a3d8
--- /dev/null
+++ b/vendor/github.com/spf13/pflag/text.go
@@ -0,0 +1,81 @@
+package pflag
+
+import (
+	"encoding"
+	"fmt"
+	"reflect"
+)
+
+// following is copied from go 1.23.4 flag.go
+type textValue struct{ p encoding.TextUnmarshaler }
+
+func newTextValue(val encoding.TextMarshaler, p encoding.TextUnmarshaler) textValue {
+	ptrVal := reflect.ValueOf(p)
+	if ptrVal.Kind() != reflect.Ptr {
+		panic("variable value type must be a pointer")
+	}
+	defVal := reflect.ValueOf(val)
+	if defVal.Kind() == reflect.Ptr {
+		defVal = defVal.Elem()
+	}
+	if defVal.Type() != ptrVal.Type().Elem() {
+		panic(fmt.Sprintf("default type does not match variable type: %v != %v", defVal.Type(), ptrVal.Type().Elem()))
+	}
+	ptrVal.Elem().Set(defVal)
+	return textValue{p}
+}
+
+func (v textValue) Set(s string) error {
+	return v.p.UnmarshalText([]byte(s))
+}
+
+func (v textValue) Get() interface{} {
+	return v.p
+}
+
+func (v textValue) String() string {
+	if m, ok := v.p.(encoding.TextMarshaler); ok {
+		if b, err := m.MarshalText(); err == nil {
+			return string(b)
+		}
+	}
+	return ""
+}
+
+//end of copy
+
+func (v textValue) Type() string {
+	return reflect.ValueOf(v.p).Type().Name()
+}
+
+// GetText set out, which implements encoding.UnmarshalText, to the value of a flag with given name
+func (f *FlagSet) GetText(name string, out encoding.TextUnmarshaler) error {
+	flag := f.Lookup(name)
+	if flag == nil {
+		return fmt.Errorf("flag accessed but not defined: %s", name)
+	}
+	if flag.Value.Type() != reflect.TypeOf(out).Name() {
+		return fmt.Errorf("trying to get %s value of flag of type %s", reflect.TypeOf(out).Name(), flag.Value.Type())
+	}
+	return out.UnmarshalText([]byte(flag.Value.String()))
+}
+
+// TextVar defines a flag with a specified name, default value, and usage string. The argument p must be a pointer to a variable that will hold the value of the flag, and p must implement encoding.TextUnmarshaler. If the flag is used, the flag value will be passed to p's UnmarshalText method. The type of the default value must be the same as the type of p.
+func (f *FlagSet) TextVar(p encoding.TextUnmarshaler, name string, value encoding.TextMarshaler, usage string) {
+	f.VarP(newTextValue(value, p), name, "", usage)
+}
+
+// TextVarP is like TextVar, but accepts a shorthand letter that can be used after a single dash.
+func (f *FlagSet) TextVarP(p encoding.TextUnmarshaler, name, shorthand string, value encoding.TextMarshaler, usage string) {
+	f.VarP(newTextValue(value, p), name, shorthand, usage)
+}
+
+// TextVar defines a flag with a specified name, default value, and usage string. The argument p must be a pointer to a variable that will hold the value of the flag, and p must implement encoding.TextUnmarshaler. If the flag is used, the flag value will be passed to p's UnmarshalText method. The type of the default value must be the same as the type of p.
+func TextVar(p encoding.TextUnmarshaler, name string, value encoding.TextMarshaler, usage string) {
+	CommandLine.VarP(newTextValue(value, p), name, "", usage)
+}
+
+// TextVarP is like TextVar, but accepts a shorthand letter that can be used after a single dash.
+func TextVarP(p encoding.TextUnmarshaler, name, shorthand string, value encoding.TextMarshaler, usage string) {
+	CommandLine.VarP(newTextValue(value, p), name, shorthand, usage)
+}
diff --git a/vendor/github.com/spf13/pflag/time.go b/vendor/github.com/spf13/pflag/time.go
new file mode 100644
index 000000000..3dee42479
--- /dev/null
+++ b/vendor/github.com/spf13/pflag/time.go
@@ -0,0 +1,124 @@
+package pflag
+
+import (
+	"fmt"
+	"strings"
+	"time"
+)
+
+// TimeValue adapts time.Time for use as a flag.
+type timeValue struct {
+	*time.Time
+	formats []string
+}
+
+func newTimeValue(val time.Time, p *time.Time, formats []string) *timeValue {
+	*p = val
+	return &timeValue{
+		Time:    p,
+		formats: formats,
+	}
+}
+
+// Set time.Time value from string based on accepted formats.
+func (d *timeValue) Set(s string) error {
+	s = strings.TrimSpace(s)
+	for _, f := range d.formats {
+		v, err := time.Parse(f, s)
+		if err != nil {
+			continue
+		}
+		*d.Time = v
+		return nil
+	}
+
+	formatsString := ""
+	for i, f := range d.formats {
+		if i > 0 {
+			formatsString += ", "
+		}
+		formatsString += fmt.Sprintf("`%s`", f)
+	}
+
+	return fmt.Errorf("invalid time format `%s` must be one of: %s", s, formatsString)
+}
+
+// Type name for time.Time flags.
+func (d *timeValue) Type() string {
+	return "time"
+}
+
+func (d *timeValue) String() string {
+	if d.Time.IsZero() {
+		return ""
+	} else {
+		return d.Time.Format(time.RFC3339Nano)
+	}
+}
+
+// GetTime return the time value of a flag with the given name
+func (f *FlagSet) GetTime(name string) (time.Time, error) {
+	flag := f.Lookup(name)
+	if flag == nil {
+		err := fmt.Errorf("flag accessed but not defined: %s", name)
+		return time.Time{}, err
+	}
+
+	if flag.Value.Type() != "time" {
+		err := fmt.Errorf("trying to get %s value of flag of type %s", "time", flag.Value.Type())
+		return time.Time{}, err
+	}
+
+	val, ok := flag.Value.(*timeValue)
+	if !ok {
+		return time.Time{}, fmt.Errorf("value %s is not a time", flag.Value)
+	}
+
+	return *val.Time, nil
+}
+
+// TimeVar defines a time.Time flag with specified name, default value, and usage string.
+// The argument p points to a time.Time variable in which to store the value of the flag.
+func (f *FlagSet) TimeVar(p *time.Time, name string, value time.Time, formats []string, usage string) {
+	f.TimeVarP(p, name, "", value, formats, usage)
+}
+
+// TimeVarP is like TimeVar, but accepts a shorthand letter that can be used after a single dash.
+func (f *FlagSet) TimeVarP(p *time.Time, name, shorthand string, value time.Time, formats []string, usage string) {
+	f.VarP(newTimeValue(value, p, formats), name, shorthand, usage)
+}
+
+// TimeVar defines a time.Time flag with specified name, default value, and usage string.
+// The argument p points to a time.Time variable in which to store the value of the flag.
+func TimeVar(p *time.Time, name string, value time.Time, formats []string, usage string) {
+	CommandLine.TimeVarP(p, name, "", value, formats, usage)
+}
+
+// TimeVarP is like TimeVar, but accepts a shorthand letter that can be used after a single dash.
+func TimeVarP(p *time.Time, name, shorthand string, value time.Time, formats []string, usage string) {
+	CommandLine.VarP(newTimeValue(value, p, formats), name, shorthand, usage)
+}
+
+// Time defines a time.Time flag with specified name, default value, and usage string.
+// The return value is the address of a time.Time variable that stores the value of the flag.
+func (f *FlagSet) Time(name string, value time.Time, formats []string, usage string) *time.Time {
+	return f.TimeP(name, "", value, formats, usage)
+}
+
+// TimeP is like Time, but accepts a shorthand letter that can be used after a single dash.
+func (f *FlagSet) TimeP(name, shorthand string, value time.Time, formats []string, usage string) *time.Time {
+	p := new(time.Time)
+	f.TimeVarP(p, name, shorthand, value, formats, usage)
+	return p
+}
+
+// Time defines a time.Time flag with specified name, default value, and usage string.
+// The return value is the address of a time.Time variable that stores the value of the flag.
+func Time(name string, value time.Time, formats []string, usage string) *time.Time {
+	return CommandLine.TimeP(name, "", value, formats, usage)
+}
+
+// TimeP is like Time, but accepts a shorthand letter that can be used after a single dash.
+func TimeP(name, shorthand string, value time.Time, formats []string, usage string) *time.Time {
+	return CommandLine.TimeP(name, shorthand, value, formats, usage)
+}
diff --git a/vendor/github.com/wk8/go-ordered-map/v2/.gitignore b/vendor/github.com/wk8/go-ordered-map/v2/.gitignore
new file mode 100644
index 000000000..57872d0f1
--- /dev/null
+++ b/vendor/github.com/wk8/go-ordered-map/v2/.gitignore
@@ -0,0 +1 @@
+/vendor/
diff --git a/vendor/github.com/wk8/go-ordered-map/v2/.golangci.yml b/vendor/github.com/wk8/go-ordered-map/v2/.golangci.yml
new file mode 100644
index 000000000..2417df10d
--- /dev/null
+++ b/vendor/github.com/wk8/go-ordered-map/v2/.golangci.yml
@@ -0,0 +1,80 @@
+run:
+  tests: false
+
+linters: 
+  disable-all: true
+  enable:
+    - asciicheck
+    - bidichk
+    - bodyclose
+    - containedctx
+    - contextcheck
+    - decorder
+    - depguard
+    - dogsled
+    - dupl
+    - durationcheck
+    - errcheck
+    - errchkjson
+    # FIXME: commented out as it crashes with 1.18 for now
+    # - errname
+    - errorlint
+    - exportloopref
+    - forbidigo
+    - funlen
+    - gci
+    - gochecknoglobals
+    - gochecknoinits
+    - gocognit
+    - goconst
+    - gocritic
+    - gocyclo
+    - godox
+    - gofmt
+    - gofumpt
+    - goheader
+    - goimports
+    - gomnd
+    - gomoddirectives
+    - gomodguard
+    - goprintffuncname
+    - gosec
+    - gosimple
+    - govet
+    - grouper
+    - ifshort
+    - importas
+    - ineffassign
+    - lll
+    - maintidx
+    - makezero
+    - misspell
+    - nakedret
+    - nilerr
+    - nilnil
+    - noctx
+    - nolintlint
+    - paralleltest
+    - prealloc
+    - predeclared
+    - promlinter
+    # FIXME: doesn't support 1.18 yet
+    # - revive
+    - rowserrcheck
+    - sqlclosecheck
+    - staticcheck
+    - structcheck
+    - stylecheck
+    - tagliatelle
+    - tenv
+    - testpackage
+    - thelper
+    - tparallel
+    - typecheck
+    - unconvert
+    - unparam
+    - unused
+    - varcheck
+    - varnamelen
+    - wastedassign
+    - whitespace
diff --git a/vendor/github.com/wk8/go-ordered-map/v2/CHANGELOG.md b/vendor/github.com/wk8/go-ordered-map/v2/CHANGELOG.md
new file mode 100644
index 000000000..f27126f84
--- /dev/null
+++ b/vendor/github.com/wk8/go-ordered-map/v2/CHANGELOG.md
@@ -0,0 +1,38 @@
+# Changelog
+
+[comment]: # (Changes since last release go here)
+
+## 2.1.8 - Jun 27th 2023
+
+* Added support for YAML serialization/deserialization
+
+## 2.1.7 - Apr 13th 2023
+
+* Renamed test_utils.go to utils_test.go
+
+## 2.1.6 - Feb 15th 2023
+
+* Added `GetAndMoveToBack()` and `GetAndMoveToFront()` methods
+
+## 2.1.5 - Dec 13th 2022
+
+* Added `Value()` method
+
+## 2.1.4 - Dec 12th 2022
+
+* Fixed a bug with UTF-8 special characters in JSON keys
+
+## 2.1.3 - Dec 11th 2022
+
+* Added support for JSON marshalling/unmarshalling of wrapper of primitive types
+
+## 2.1.2 - Dec 10th 2022
+* Allowing to pass options to `New`, to give a capacity hint, or initial data
+* Allowing to deserialize nested ordered maps from JSON without having to explicitly instantiate them
+* Added the `AddPairs` method
+
+## 2.1.1 - Dec 9th 2022
+* Fixing a bug with JSON marshalling
+
+## 2.1.0 - Dec 7th 2022
+* Added support for JSON serialization/deserialization
diff --git a/vendor/github.com/wk8/go-ordered-map/v2/LICENSE b/vendor/github.com/wk8/go-ordered-map/v2/LICENSE
new file mode 100644
index 000000000..8dada3eda
--- /dev/null
+++ b/vendor/github.com/wk8/go-ordered-map/v2/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/vendor/github.com/wk8/go-ordered-map/v2/Makefile b/vendor/github.com/wk8/go-ordered-map/v2/Makefile
new file mode 100644
index 000000000..6e0e18a1b
--- /dev/null
+++ b/vendor/github.com/wk8/go-ordered-map/v2/Makefile
@@ -0,0 +1,32 @@
+.DEFAULT_GOAL := all
+
+.PHONY: all
+all: test_with_fuzz lint
+
+# the TEST_FLAGS env var can be set to eg run only specific tests
+TEST_COMMAND = go test -v -count=1 -race -cover $(TEST_FLAGS)
+
+.PHONY: test
+test:
+	$(TEST_COMMAND)
+
+.PHONY: bench
+bench:
+	go test -bench=.
+
+FUZZ_TIME ?= 10s
+
+# see https://github.com/golang/go/issues/46312
+# and https://stackoverflow.com/a/72673487/4867444
+# if we end up having more fuzz tests
+.PHONY: test_with_fuzz
+test_with_fuzz:
+	$(TEST_COMMAND) -fuzz=FuzzRoundTripJSON -fuzztime=$(FUZZ_TIME)
+	$(TEST_COMMAND) -fuzz=FuzzRoundTripYAML -fuzztime=$(FUZZ_TIME)
+
+.PHONY: fuzz
+fuzz: test_with_fuzz
+
+.PHONY: lint
+lint:
+	golangci-lint run
diff --git a/vendor/github.com/wk8/go-ordered-map/v2/README.md b/vendor/github.com/wk8/go-ordered-map/v2/README.md
new file mode 100644
index 000000000..b02894443
--- /dev/null
+++ b/vendor/github.com/wk8/go-ordered-map/v2/README.md
@@ -0,0 +1,154 @@
+[![Go Reference](https://pkg.go.dev/badge/github.com/wk8/go-ordered-map/v2.svg)](https://pkg.go.dev/github.com/wk8/go-ordered-map/v2)
+[![Build Status](https://circleci.com/gh/wk8/go-ordered-map.svg?style=svg)](https://app.circleci.com/pipelines/github/wk8/go-ordered-map)
+
+# Golang Ordered Maps
+
+Same as regular maps, but also remembers the order in which keys were inserted, akin to [Python's `collections.OrderedDict`s](https://docs.python.org/3.7/library/collections.html#ordereddict-objects).
+
+It offers the following features:
+* optimal runtime performance (all operations are constant time)
+* optimal memory usage (only one copy of values, no unnecessary memory allocation)
+* allows iterating from newest or oldest keys indifferently, without memory copy, allowing to `break` the iteration, and in time linear to the number of keys iterated over rather than the total length of the ordered map
+* supports any generic types for both keys and values. If you're running go < 1.18, you can use [version 1](https://github.com/wk8/go-ordered-map/tree/v1) that takes and returns generic `interface{}`s instead of using generics
+* idiomatic API, akin to that of [`container/list`](https://golang.org/pkg/container/list)
+* support for JSON and YAML marshalling
+
+## Documentation
+
+[The full documentation is available on pkg.go.dev](https://pkg.go.dev/github.com/wk8/go-ordered-map/v2).
+
+## Installation
+```bash
+go get -u github.com/wk8/go-ordered-map/v2
+```
+
+Or use your favorite golang vendoring tool!
+
+## Supported go versions
+
+Go >= 1.18 is required to use version >= 2 of this library, as it uses generics.
+
+If you're running go < 1.18, you can use [version 1](https://github.com/wk8/go-ordered-map/tree/v1) instead.
+
+## Example / usage
+
+```go
+package main
+
+import (
+	"fmt"
+
+	"github.com/wk8/go-ordered-map/v2"
+)
+
+func main() {
+	om := orderedmap.New[string, string]()
+
+	om.Set("foo", "bar")
+	om.Set("bar", "baz")
+	om.Set("coucou", "toi")
+
+	fmt.Println(om.Get("foo"))          // => "bar", true
+	fmt.Println(om.Get("i dont exist")) // => "", false
+
+	// iterating pairs from oldest to newest:
+	for pair := om.Oldest(); pair != nil; pair = pair.Next() {
+		fmt.Printf("%s => %s\n", pair.Key, pair.Value)
+	} // prints:
+	// foo => bar
+	// bar => baz
+	// coucou => toi
+
+	// iterating over the 2 newest pairs:
+	i := 0
+	for pair := om.Newest(); pair != nil; pair = pair.Prev() {
+		fmt.Printf("%s => %s\n", pair.Key, pair.Value)
+		i++
+		if i >= 2 {
+			break
+		}
+	} // prints:
+	// coucou => toi
+	// bar => baz
+}
+```
+
+An `OrderedMap`'s keys must implement `comparable`, and its values can be anything, for example:
+
+```go
+type myStruct struct {
+	payload string
+}
+
+func main() {
+	om := orderedmap.New[int, *myStruct]()
+
+	om.Set(12, &myStruct{"foo"})
+	om.Set(1, &myStruct{"bar"})
+
+	value, present := om.Get(12)
+	if !present {
+		panic("should be there!")
+	}
+	fmt.Println(value.payload) // => foo
+
+	for pair := om.Oldest(); pair != nil; pair = pair.Next() {
+		fmt.Printf("%d => %s\n", pair.Key, pair.Value.payload)
+	} // prints:
+	// 12 => foo
+	// 1 => bar
+}
+```
+
+Also worth noting that you can provision ordered maps with a capacity hint, as you would do by passing an optional hint to `make(map[K]V, capacity`):
+```go
+om := orderedmap.New[int, *myStruct](28)
+```
+
+You can also pass in some initial data to store in the map:
+```go
+om := orderedmap.New[int, string](orderedmap.WithInitialData[int, string](
+	orderedmap.Pair[int, string]{
+		Key:   12,
+		Value: "foo",
+	},
+	orderedmap.Pair[int, string]{
+		Key:   28,
+		Value: "bar",
+	},
+))
+```
+
+`OrderedMap`s also support JSON serialization/deserialization, and preserves order:
+
+```go
+// serialization
+data, err := json.Marshal(om)
+...
+
+// deserialization
+om := orderedmap.New[string, string]() // or orderedmap.New[int, any](), or any type you expect
+err := json.Unmarshal(data, &om)
+...
+```
+
+Similarly, it also supports YAML serialization/deserialization using the yaml.v3 package, which also preserves order:
+
+```go
+// serialization
+data, err := yaml.Marshal(om)
+...
+
+// deserialization
+om := orderedmap.New[string, string]() // or orderedmap.New[int, any](), or any type you expect
+err := yaml.Unmarshal(data, &om)
+...
+```
+
+## Alternatives
+
+There are several other ordered map golang implementations out there, but I believe that at the time of writing none of them offer the same functionality as this library; more specifically:
+* [iancoleman/orderedmap](https://github.com/iancoleman/orderedmap) only accepts `string` keys, its `Delete` operations are linear
+* [cevaris/ordered_map](https://github.com/cevaris/ordered_map) uses a channel for iterations, and leaks goroutines if the iteration is interrupted before fully traversing the map
+* [mantyr/iterator](https://github.com/mantyr/iterator) also uses a channel for iterations, and its `Delete` operations are linear
+* [samdolan/go-ordered-map](https://github.com/samdolan/go-ordered-map) adds unnecessary locking (users should add their own locking instead if they need it), its `Delete` and `Get` operations are linear, iterations trigger a linear memory allocation
diff --git a/vendor/github.com/wk8/go-ordered-map/v2/json.go b/vendor/github.com/wk8/go-ordered-map/v2/json.go
new file mode 100644
index 000000000..a545b536b
--- /dev/null
+++ b/vendor/github.com/wk8/go-ordered-map/v2/json.go
@@ -0,0 +1,182 @@
+package orderedmap
+
+import (
+	"bytes"
+	"encoding"
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"unicode/utf8"
+
+	"github.com/buger/jsonparser"
+	"github.com/mailru/easyjson/jwriter"
+)
+
+var (
+	_ json.Marshaler   = &OrderedMap[int, any]{}
+	_ json.Unmarshaler = &OrderedMap[int, any]{}
+)
+
+// MarshalJSON implements the json.Marshaler interface.
+func (om *OrderedMap[K, V]) MarshalJSON() ([]byte, error) { //nolint:funlen
+	if om == nil || om.list == nil {
+		return []byte("null"), nil
+	}
+
+	writer := jwriter.Writer{}
+	writer.RawByte('{')
+
+	for pair, firstIteration := om.Oldest(), true; pair != nil; pair = pair.Next() {
+		if firstIteration {
+			firstIteration = false
+		} else {
+			writer.RawByte(',')
+		}
+
+		switch key := any(pair.Key).(type) {
+		case string:
+			writer.String(key)
+		case encoding.TextMarshaler:
+			writer.RawByte('"')
+			writer.Raw(key.MarshalText())
+			writer.RawByte('"')
+		case int:
+			writer.IntStr(key)
+		case int8:
+			writer.Int8Str(key)
+		case int16:
+			writer.Int16Str(key)
+		case int32:
+			writer.Int32Str(key)
+		case int64:
+			writer.Int64Str(key)
+		case uint:
+			writer.UintStr(key)
+		case uint8:
+			writer.Uint8Str(key)
+		case uint16:
+			writer.Uint16Str(key)
+		case uint32:
+			writer.Uint32Str(key)
+		case uint64:
+			writer.Uint64Str(key)
+		default:
+
+			// this switch takes care of wrapper types around primitive types, such as
+			// type myType string
+			switch keyValue := reflect.ValueOf(key); keyValue.Type().Kind() {
+			case reflect.String:
+				writer.String(keyValue.String())
+			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+				writer.Int64Str(keyValue.Int())
+			case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+				writer.Uint64Str(keyValue.Uint())
+			default:
+				return nil, fmt.Errorf("unsupported key type: %T", key)
+			}
+		}
+
+		writer.RawByte(':')
+		// the error is checked at the end of the function
+		writer.Raw(json.Marshal(pair.Value)) //nolint:errchkjson
+	}
+
+	writer.RawByte('}')
+
+	return dumpWriter(&writer)
+}
+
+func dumpWriter(writer *jwriter.Writer) ([]byte, error) {
+	if writer.Error != nil {
+		return nil, writer.Error
+	}
+
+	var buf bytes.Buffer
+	buf.Grow(writer.Size())
+	if _, err := writer.DumpTo(&buf); err != nil {
+		return nil, err
+	}
+
+	return buf.Bytes(), nil
+}
+
+// UnmarshalJSON implements the json.Unmarshaler interface.
+func (om *OrderedMap[K, V]) UnmarshalJSON(data []byte) error {
+	if om.list == nil {
+		om.initialize(0)
+	}
+
+	return jsonparser.ObjectEach(
+		data,
+		func(keyData []byte, valueData []byte, dataType jsonparser.ValueType, offset int) error {
+			if dataType == jsonparser.String {
+				// jsonparser removes the enclosing quotes; we need to restore them to make a valid JSON
+				valueData = data[offset-len(valueData)-2 : offset]
+			}
+
+			var key K
+			var value V
+
+			switch typedKey := any(&key).(type) {
+			case *string:
+				s, err := decodeUTF8(keyData)
+				if err != nil {
+					return err
+				}
+				*typedKey = s
+			case encoding.TextUnmarshaler:
+				if err := typedKey.UnmarshalText(keyData); err != nil {
+					return err
+				}
+			case *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64:
+				if err := json.Unmarshal(keyData, typedKey); err != nil {
+					return err
+				}
+			default:
+				// this switch takes care of wrapper types around primitive types, such as
+				// type myType string
+				switch reflect.TypeOf(key).Kind() {
+				case reflect.String:
+					s, err := decodeUTF8(keyData)
+					if err != nil {
+						return err
+					}
+
+					convertedKeyData := reflect.ValueOf(s).Convert(reflect.TypeOf(key))
+					reflect.ValueOf(&key).Elem().Set(convertedKeyData)
+				case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+					reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+					if err := json.Unmarshal(keyData, &key); err != nil {
+						return err
+					}
+				default:
+					return fmt.Errorf("unsupported key type: %T", key)
+				}
+			}
+
+			if err := json.Unmarshal(valueData, &value); err != nil {
+				return err
+			}
+
+			om.Set(key, value)
+			return nil
+		})
+}
+
+func decodeUTF8(input []byte) (string, error) {
+	remaining, offset := input, 0
+	runes := make([]rune, 0, len(remaining))
+
+	for len(remaining) > 0 {
+		r, size := utf8.DecodeRune(remaining)
+		if r == utf8.RuneError && size <= 1 {
+			return "", fmt.Errorf("not a valid UTF-8 string (at position %d): %s", offset, string(input))
+		}
+
+		runes = append(runes, r)
+		remaining = remaining[size:]
+		offset += size
+	}
+
+	return string(runes), nil
+}
diff --git a/vendor/github.com/wk8/go-ordered-map/v2/orderedmap.go b/vendor/github.com/wk8/go-ordered-map/v2/orderedmap.go
new file mode 100644
index 000000000..064714191
--- /dev/null
+++ b/vendor/github.com/wk8/go-ordered-map/v2/orderedmap.go
@@ -0,0 +1,296 @@
+// Package orderedmap implements an ordered map, i.e. a map that also keeps track of
+// the order in which keys were inserted.
+//
+// All operations are constant-time.
+//
+// Github repo: https://github.com/wk8/go-ordered-map
+//
+package orderedmap
+
+import (
+	"fmt"
+
+	list "github.com/bahlo/generic-list-go"
+)
+
+type Pair[K comparable, V any] struct {
+	Key   K
+	Value V
+
+	element *list.Element[*Pair[K, V]]
+}
+
+type OrderedMap[K comparable, V any] struct {
+	pairs map[K]*Pair[K, V]
+	list  *list.List[*Pair[K, V]]
+}
+
+type initConfig[K comparable, V any] struct {
+	capacity    int
+	initialData []Pair[K, V]
+}
+
+type InitOption[K comparable, V any] func(config *initConfig[K, V])
+
+// WithCapacity allows giving a capacity hint for the map, akin to the standard make(map[K]V, capacity).
+func WithCapacity[K comparable, V any](capacity int) InitOption[K, V] {
+	return func(c *initConfig[K, V]) {
+		c.capacity = capacity
+	}
+}
+
+// WithInitialData allows passing in initial data for the map.
+func WithInitialData[K comparable, V any](initialData ...Pair[K, V]) InitOption[K, V] {
+	return func(c *initConfig[K, V]) {
+		c.initialData = initialData
+		if c.capacity < len(initialData) {
+			c.capacity = len(initialData)
+		}
+	}
+}
+
+// New creates a new OrderedMap.
+// options can either be one or several InitOption[K, V], or a single integer,
+// which is then interpreted as a capacity hint, à la make(map[K]V, capacity).
+func New[K comparable, V any](options ...any) *OrderedMap[K, V] { //nolint:varnamelen
+	orderedMap := &OrderedMap[K, V]{}
+
+	var config initConfig[K, V]
+	for _, untypedOption := range options {
+		switch option := untypedOption.(type) {
+		case int:
+			if len(options) != 1 {
+				invalidOption()
+			}
+			config.capacity = option
+
+		case InitOption[K, V]:
+			option(&config)
+
+		default:
+			invalidOption()
+		}
+	}
+
+	orderedMap.initialize(config.capacity)
+	orderedMap.AddPairs(config.initialData...)
+
+	return orderedMap
+}
+
+const invalidOptionMessage = `when using orderedmap.New[K,V]() with options, either provide one or several InitOption[K, V]; or a single integer which is then interpreted as a capacity hint, à la make(map[K]V, capacity).` //nolint:lll
+
+func invalidOption() { panic(invalidOptionMessage) }
+
+func (om *OrderedMap[K, V]) initialize(capacity int) {
+	om.pairs = make(map[K]*Pair[K, V], capacity)
+	om.list = list.New[*Pair[K, V]]()
+}
+
+// Get looks for the given key, and returns the value associated with it,
+// or V's nil value if not found. The boolean it returns says whether the key is present in the map.
+func (om *OrderedMap[K, V]) Get(key K) (val V, present bool) {
+	if pair, present := om.pairs[key]; present {
+		return pair.Value, true
+	}
+
+	return
+}
+
+// Load is an alias for Get, mostly to present an API similar to `sync.Map`'s.
+func (om *OrderedMap[K, V]) Load(key K) (V, bool) {
+	return om.Get(key)
+}
+
+// Value returns the value associated with the given key or the zero value.
+func (om *OrderedMap[K, V]) Value(key K) (val V) {
+	if pair, present := om.pairs[key]; present {
+		val = pair.Value
+	}
+	return
+}
+
+// GetPair looks for the given key, and returns the pair associated with it,
+// or nil if not found. The Pair struct can then be used to iterate over the ordered map
+// from that point, either forward or backward.
+func (om *OrderedMap[K, V]) GetPair(key K) *Pair[K, V] {
+	return om.pairs[key]
+}
+
+// Set sets the key-value pair, and returns what `Get` would have returned
+// on that key prior to the call to `Set`.
+func (om *OrderedMap[K, V]) Set(key K, value V) (val V, present bool) {
+	if pair, present := om.pairs[key]; present {
+		oldValue := pair.Value
+		pair.Value = value
+		return oldValue, true
+	}
+
+	pair := &Pair[K, V]{
+		Key:   key,
+		Value: value,
+	}
+	pair.element = om.list.PushBack(pair)
+	om.pairs[key] = pair
+
+	return
+}
+
+// AddPairs allows setting multiple pairs at a time. It's equivalent to calling
+// Set on each pair sequentially.
+func (om *OrderedMap[K, V]) AddPairs(pairs ...Pair[K, V]) {
+	for _, pair := range pairs {
+		om.Set(pair.Key, pair.Value)
+	}
+}
+
+// Store is an alias for Set, mostly to present an API similar to `sync.Map`'s.
+func (om *OrderedMap[K, V]) Store(key K, value V) (V, bool) {
+	return om.Set(key, value)
+}
+
+// Delete removes the key-value pair, and returns what `Get` would have returned
+// on that key prior to the call to `Delete`.
+func (om *OrderedMap[K, V]) Delete(key K) (val V, present bool) {
+	if pair, present := om.pairs[key]; present {
+		om.list.Remove(pair.element)
+		delete(om.pairs, key)
+		return pair.Value, true
+	}
+	return
+}
+
+// Len returns the length of the ordered map.
+func (om *OrderedMap[K, V]) Len() int {
+	if om == nil || om.pairs == nil {
+		return 0
+	}
+	return len(om.pairs)
+}
+
+// Oldest returns a pointer to the oldest pair. It's meant to be used to iterate on the ordered map's
+// pairs from the oldest to the newest, e.g.:
+// for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() { fmt.Printf("%v => %v\n", pair.Key, pair.Value) }
+func (om *OrderedMap[K, V]) Oldest() *Pair[K, V] {
+	if om == nil || om.list == nil {
+		return nil
+	}
+	return listElementToPair(om.list.Front())
+}
+
+// Newest returns a pointer to the newest pair. It's meant to be used to iterate on the ordered map's
+// pairs from the newest to the oldest, e.g.:
+// for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() { fmt.Printf("%v => %v\n", pair.Key, pair.Value) }
+func (om *OrderedMap[K, V]) Newest() *Pair[K, V] {
+	if om == nil || om.list == nil {
+		return nil
+	}
+	return listElementToPair(om.list.Back())
+}
+
+// Next returns a pointer to the next pair.
+func (p *Pair[K, V]) Next() *Pair[K, V] {
+	return listElementToPair(p.element.Next())
+}
+
+// Prev returns a pointer to the previous pair.
+func (p *Pair[K, V]) Prev() *Pair[K, V] {
+	return listElementToPair(p.element.Prev())
+}
+
+func listElementToPair[K comparable, V any](element *list.Element[*Pair[K, V]]) *Pair[K, V] {
+	if element == nil {
+		return nil
+	}
+	return element.Value
+}
+
+// KeyNotFoundError may be returned by functions in this package when they're called with keys that are not present
+// in the map.
+type KeyNotFoundError[K comparable] struct {
+	MissingKey K
+}
+
+func (e *KeyNotFoundError[K]) Error() string {
+	return fmt.Sprintf("missing key: %v", e.MissingKey)
+}
+
+// MoveAfter moves the value associated with key to its new position after the one associated with markKey.
+// Returns an error iff key or markKey are not present in the map. If an error is returned,
+// it will be a KeyNotFoundError.
+func (om *OrderedMap[K, V]) MoveAfter(key, markKey K) error {
+	elements, err := om.getElements(key, markKey)
+	if err != nil {
+		return err
+	}
+	om.list.MoveAfter(elements[0], elements[1])
+	return nil
+}
+
+// MoveBefore moves the value associated with key to its new position before the one associated with markKey.
+// Returns an error iff key or markKey are not present in the map. If an error is returned,
+// it will be a KeyNotFoundError.
+func (om *OrderedMap[K, V]) MoveBefore(key, markKey K) error {
+	elements, err := om.getElements(key, markKey)
+	if err != nil {
+		return err
+	}
+	om.list.MoveBefore(elements[0], elements[1])
+	return nil
+}
+
+func (om *OrderedMap[K, V]) getElements(keys ...K) ([]*list.Element[*Pair[K, V]], error) {
+	elements := make([]*list.Element[*Pair[K, V]], len(keys))
+	for i, k := range keys {
+		pair, present := om.pairs[k]
+		if !present {
+			return nil, &KeyNotFoundError[K]{k}
+		}
+		elements[i] = pair.element
+	}
+	return elements, nil
+}
+
+// MoveToBack moves the value associated with key to the back of the ordered map,
+// i.e. makes it the newest pair in the map.
+// Returns an error iff key is not present in the map. If an error is returned,
+// it will be a KeyNotFoundError.
+func (om *OrderedMap[K, V]) MoveToBack(key K) error {
+	_, err := om.GetAndMoveToBack(key)
+	return err
+}
+
+// MoveToFront moves the value associated with key to the front of the ordered map,
+// i.e. makes it the oldest pair in the map.
+// Returns an error iff key is not present in the map. If an error is returned,
+// it will be a KeyNotFoundError.
+func (om *OrderedMap[K, V]) MoveToFront(key K) error {
+	_, err := om.GetAndMoveToFront(key)
+	return err
+}
+
+// GetAndMoveToBack combines Get and MoveToBack in the same call. If an error is returned,
+// it will be a KeyNotFoundError.
+func (om *OrderedMap[K, V]) GetAndMoveToBack(key K) (val V, err error) {
+	if pair, present := om.pairs[key]; present {
+		val = pair.Value
+		om.list.MoveToBack(pair.element)
+	} else {
+		err = &KeyNotFoundError[K]{key}
+	}
+
+	return
+}
+
+// GetAndMoveToFront combines Get and MoveToFront in the same call. If an error is returned,
+// it will be a KeyNotFoundError.
+func (om *OrderedMap[K, V]) GetAndMoveToFront(key K) (val V, err error) {
+	if pair, present := om.pairs[key]; present {
+		val = pair.Value
+		om.list.MoveToFront(pair.element)
+	} else {
+		err = &KeyNotFoundError[K]{key}
+	}
+
+	return
+}
diff --git a/vendor/github.com/wk8/go-ordered-map/v2/yaml.go b/vendor/github.com/wk8/go-ordered-map/v2/yaml.go
new file mode 100644
index 000000000..602247128
--- /dev/null
+++ b/vendor/github.com/wk8/go-ordered-map/v2/yaml.go
@@ -0,0 +1,71 @@
+package orderedmap
+
+import (
+	"fmt"
+
+	"gopkg.in/yaml.v3"
+)
+
+var (
+	_ yaml.Marshaler   = &OrderedMap[int, any]{}
+	_ yaml.Unmarshaler = &OrderedMap[int, any]{}
+)
+
+// MarshalYAML implements the yaml.Marshaler interface.
+func (om *OrderedMap[K, V]) MarshalYAML() (interface{}, error) {
+	if om == nil {
+		return []byte("null"), nil
+	}
+
+	node := yaml.Node{
+		Kind: yaml.MappingNode,
+	}
+
+	for pair := om.Oldest(); pair != nil; pair = pair.Next() {
+		key, value := pair.Key, pair.Value
+
+		keyNode := &yaml.Node{}
+
+		// serialize key to yaml, then deserialize it back into the node
+		// this is a hack to get the correct tag for the key
+		if err := keyNode.Encode(key); err != nil {
+			return nil, err
+		}
+
+		valueNode := &yaml.Node{}
+		if err := valueNode.Encode(value); err != nil {
+			return nil, err
+		}
+
+		node.Content = append(node.Content, keyNode, valueNode)
+	}
+
+	return &node, nil
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface.
+func (om *OrderedMap[K, V]) UnmarshalYAML(value *yaml.Node) error {
+	if value.Kind != yaml.MappingNode {
+		return fmt.Errorf("pipeline must contain YAML mapping, has %v", value.Kind)
+	}
+
+	if om.list == nil {
+		om.initialize(0)
+	}
+
+	for index := 0; index < len(value.Content); index += 2 {
+		var key K
+		var val V
+
+		if err := value.Content[index].Decode(&key); err != nil {
+			return err
+		}
+		if err := value.Content[index+1].Decode(&val); err != nil {
+			return err
+		}
+
+		om.Set(key, val)
+	}
+
+	return nil
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/.gitignore b/vendor/gitlab.com/gitlab-org/api/client-go/.gitignore
index d28ad4186..79e862f1f 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/.gitignore
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/.gitignore
@@ -33,6 +33,9 @@ _testmain.go
 vendor
 .go/
 .golangci-lint/
+scripts/commitlint/node_modules
 
 # reports
-gl-code-quality-report.json
\ No newline at end of file
+gl-code-quality-report.json
+
+.mise/
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/.gitlab-ci.yml b/vendor/gitlab.com/gitlab-org/api/client-go/.gitlab-ci.yml
index 7b5da83ef..85a9d932e 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/.gitlab-ci.yml
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/.gitlab-ci.yml
@@ -7,6 +7,7 @@ workflow:
 
 include:
   - template: Jobs/SAST.gitlab-ci.yml
+  - component: ${CI_SERVER_FQDN}/components/dependency-scanning/main@0.8.0
   - component: ${CI_SERVER_FQDN}/gitlab-org/components/danger-review/danger-review@2.0.0
     inputs:
       job_stage: lint
@@ -18,13 +19,31 @@ include:
   # groups. However, there is no easy way to conditionally set tags or even variables without
   # jeopardizing existing (complex) workflow:rules or job:rules. Thus, we resort to
   # this nasty conditionally include hack.
-  - local: '.gitlab/ci/gitlab-go-runner-tags.gitlab-ci.yml'
+  - local: ".gitlab/ci/gitlab-go-runner-tags.gitlab-ci.yml"
     rules:
       - if: $CI_PROJECT_ROOT_NAMESPACE == 'gitlab-org' || $CI_PROJECT_ROOT_NAMESPACE == 'gitlab-community'
-  - local: '.gitlab/ci/community-go-runner-tags.gitlab-ci.yml'
+  - local: ".gitlab/ci/community-go-runner-tags.gitlab-ci.yml"
     rules:
       - if: $CI_PROJECT_ROOT_NAMESPACE != 'gitlab-org' && $CI_PROJECT_ROOT_NAMESPACE != 'gitlab-community'
 
+  # Add Work Type Classification labels to merge requests,
+  # see https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/blob/main/autolabels.md
+  - project: "gitlab-com/gl-infra/common-ci-tasks"
+    ref: v2.93.0
+    file: "autolabels.yml"
+    inputs:
+      stage: lint
+
+  # Analyze commits to determine whether to cut a release
+  # see https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/blob/main/semantic-release.md
+  - project: "gitlab-com/gl-infra/common-ci-tasks"
+    ref: v2.93.0
+    file: "semantic-release.yml"
+    inputs:
+      stage: deploy
+      validate_stage: lint
+      vault: access_tokens/${VAULT_SECRETS_PATH}/semantic-release/token@ci
+
 stages:
   - lint
   - test
@@ -34,8 +53,8 @@ stages:
   parallel:
     matrix:
       - GOLANG_IMAGE_VERSION:
-        - '1.23'
-        - '1.24'
+          - "1.23"
+          - "1.24"
 
 .go:base:
   extends:
@@ -44,28 +63,43 @@ stages:
   variables:
     GOPATH: $CI_PROJECT_DIR/.go
     GOLANGCI_LINT_CACHE: $CI_PROJECT_DIR/.golangci-lint
+    MISE_VERSION: v2025.6.1
+    MISE_DATA_DIR: $CI_PROJECT_DIR/.mise/mise-data
   before_script:
     - mkdir -p "${GOPATH}" "${GOLANGCI_LINT_CACHE}"
     - export PATH="${GOPATH}/bin:$PATH"
+    # Install mise
+    - curl --retry 6 https://mise.run | MISE_VERSION=${MISE_VERSION} MISE_INSTALL_PATH=/usr/local/bin/mise sh
+    - eval "$(mise activate bash --shims)"
+    - mise install
   cache:
-    paths:
-      - $GOPATH/pkg/mod/
-      - $GOLANGCI_LINT_CACHE/
-    key:
-      files:
-        - go.sum
+    - key:
+        files:
+          - go.sum
+      paths:
+        - $GOPATH/pkg/mod/
+        - $GOLANGCI_LINT_CACHE/
+    - key:
+        prefix: mise-
+        files: [".tool-versions"]
+      paths:
+        - $MISE_DATA_DIR
+
   # We only need to run Go-related jobs when actual Go files changed
   # or when running either on the default branch or for a tag.
   rules:
-    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+    - if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
     - if: $CI_COMMIT_TAG
     - changes:
-        - '**/*.go'
+        - "**/*.go"
         - testdata/**
         - go.mod
         - go.sum
         - .gitlab-ci.yml
         - .gitlab/ci/*.yml
+        - buf.yaml
+        - buf.gen.yaml
+        - buf.lock
 
 golangci-lint:
   extends:
@@ -73,8 +107,8 @@ golangci-lint:
   stage: lint
   needs: []
   variables:
-    REPORT_FILENAME: 'gl-code-quality-report.json'
-  image: golangci/golangci-lint:v2.1.6
+    REPORT_FILENAME: "gl-code-quality-report.json"
+  image: golangci/golangci-lint:v2.6.1
   script:
     - golangci-lint run
   artifacts:
@@ -83,12 +117,21 @@ golangci-lint:
     paths: [$REPORT_FILENAME]
     when: always
 
+buf-lint:
+  extends:
+    - .go:base
+  stage: lint
+  needs: []
+  script:
+    - buf format --diff --exit-code
+    - buf lint
+
 verify-generated-code:
   extends:
     - .go:base
   stage: lint
   needs: []
-  image: golang:1.24-bookworm
+  image: golang:1.25-bookworm
   script:
     - make generate
     - |
@@ -101,17 +144,6 @@ verify-generated-code:
         exit 1;
       }
 
-commitlint:
-  stage: lint
-  needs: []
-  rules:
-    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
-  image:
-    name: commitlint/commitlint:19.8.1
-    entrypoint: [""]
-  script:
-    - commitlint --from ${CI_MERGE_REQUEST_DIFF_BASE_SHA} --to ${CI_COMMIT_SHA}
-
 tests:unit:
   extends:
     - .go:base
@@ -121,8 +153,8 @@ tests:unit:
   image: golang:$GOLANG_IMAGE_VERSION
   variables:
     # configure tooling versions
-    GOTESTSUM_VERSION: 'v1.12.0'
-    GOCOVER_COBERTURA_VERSION: 'v1.2.1-0.20240107185409-0818f3538137'
+    GOTESTSUM_VERSION: "v1.12.0"
+    GOCOVER_COBERTURA_VERSION: "v1.2.1-0.20240107185409-0818f3538137"
 
     # configure artifact files
     JUNIT_FILENAME: tests.xml
@@ -146,21 +178,6 @@ tests:unit:
         coverage_format: cobertura
     when: always
 
-release:
-  image: node:24-bookworm-slim
-  stage: deploy
-  before_script:
-    - apt-get update && apt-get install -y --no-install-recommends git-core ca-certificates
-    - npm install -g semantic-release @semantic-release/gitlab @semantic-release/git @semantic-release/changelog
-  script:
-    - semantic-release
-  rules:
-    - if: '$CI_SERVER_HOST != "gitlab.com" || $CI_PROJECT_PATH != "gitlab-org/api/client-go"'
-      when: never
-    - if: $CI_PIPELINE_SOURCE == "schedule"
-      when: never
-    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
-
 # Update rules on SAST to ensure the jobs show up in the pipeline
 # this prevents forks that don't have `ultimate` from skipping SAST scans
 # since gitlab-advaced-sast replaces semgrep.
@@ -168,3 +185,12 @@ semgrep-sast:
   needs: []
   rules:
     - when: always
+
+# Update rules on dependency scanning so that it only runs when on the 
+# canonical project or the community fork. This ensures that when people
+# use personal forks, it doesn't fail stating the forks are unlicensed.
+dependency-scanning:
+  rules:
+    - if: $CI_PROJECT_ID == "65271576" # Canonical repository
+    - if: $CI_PROJECT_ID == "65275361" # Community Fork
+    - when: never
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/.golangci.yml b/vendor/gitlab.com/gitlab-org/api/client-go/.golangci.yml
index c2eb5c9b0..de06c9da7 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/.golangci.yml
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/.golangci.yml
@@ -19,14 +19,21 @@ linters:
   enable:
     - asciicheck
     - dogsled
+    - dupword
     - errorlint
     - goconst
+    - godoclint
     - misspell
     - nakedret
     - nolintlint
     - revive
+    - staticcheck
+    - testifylint
     - unconvert
+    - usestdlibvars
     - whitespace
+    - tparallel
+    - paralleltest
   disable:
     - errcheck
   settings:
@@ -38,18 +45,80 @@ linters:
       enable-all-rules: false
       rules:
         - name: deep-exit
-  
+        - name: get-return
+        - name: redundant-import-alias
+        - name: time-date
+        - name: var-naming
+    staticcheck:
+      checks:
+        - ST1020
+        - ST1021
+        - ST1022
+    testifylint:
+      disable-all: true
+      enable:
+        - blank-import
+        - bool-compare
+        - compares
+        - contains
+        - empty
+        - encoded-compare
+        - equal-values
+        - error-is-as
+        - error-nil
+        - expected-actual
+        - formatter
+        - go-require
+        - len
+        - negative-positive
+        - nil-compare
+        - regexp
+        - useless-assert
+    usestdlibvars:
+      http-method: false
+      http-status-code: false
+      time-date-month: true
+
   # List of regexps of issue texts to exclude.
   exclusions:
     generated: lax
     presets:
-      - comments
       - common-false-positives
       - legacy
       - std-error-handling
     rules:
       - path: (.+)\.go$
         text: ^.*, make it a constant$
+      - source: "// Deprecated:"
+        linters:
+          - staticcheck
+      - source: "// GitLab API docs:"
+        linters:
+          - staticcheck
+      - text: "var-naming: struct field PipelineId should be PipelineID"
+        path: commits\.go
+        linters:
+          - revive
+      - text: "var-naming: struct field SelectiveSyncNamespaceIds should be SelectiveSyncNamespaceIDs"
+        path: geo_nodes\.go
+        linters:
+          - revive
+      - text: "var-naming: struct field RefsUrl should be RefsURL"
+        path: import\.go
+        linters:
+          - revive
+      - text: "var-naming: struct field BitbucketServerUrl should be BitbucketServerURL"
+        path: import\.go
+        linters:
+          - revive
+      - text: "var-naming: struct field CrlUrl should be CrlURL"
+        path: tags\.go
+        linters:
+          - revive
+      - text: "var-naming: type ServicePingNonSqlMetrics should be ServicePingNonSQLMetrics"
+        path: usage_data\.go
+        linters:
+          - revive
     paths:
       - third_party$
       - builtin$
@@ -57,7 +126,7 @@ linters:
 issues:
   # Maximum issues count per one linter (set to 0 to disable)
   max-issues-per-linter: 0
-  # Maximum count of issues with the same text (set to 0 to disable)  
+  # Maximum count of issues with the same text (set to 0 to disable)
   max-same-issues: 0
 formatters:
   enable:
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/.releaserc.json b/vendor/gitlab.com/gitlab-org/api/client-go/.releaserc.json
new file mode 100644
index 000000000..ab9aa899c
--- /dev/null
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/.releaserc.json
@@ -0,0 +1,16 @@
+{
+  "branches": ["main"],
+  "plugins": [
+    "@gitlab/semantic-release-merge-request-analyzer",
+    "@semantic-release/release-notes-generator",
+    "@semantic-release/changelog",
+    "@semantic-release/gitlab",
+    [
+      "@semantic-release/git",
+      {
+        "assets": ["CHANGELOG.md"],
+        "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
+      }
+    ]
+  ]
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/.tool-versions b/vendor/gitlab.com/gitlab-org/api/client-go/.tool-versions
index ce09ad71e..ea4eec6f2 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/.tool-versions
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/.tool-versions
@@ -1,2 +1,5 @@
-golang 1.23
-golangci-lint 2.1.6
+golang        1.23
+golangci-lint 2.5.0
+gofumpt       0.8.0
+buf           1.55.1
+node          24.8.0
\ No newline at end of file
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/AGENTS.md b/vendor/gitlab.com/gitlab-org/api/client-go/AGENTS.md
new file mode 100644
index 000000000..aa1a06239
--- /dev/null
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/AGENTS.md
@@ -0,0 +1,384 @@
+# AI Agent Guidelines for GitLab client-go
+
+This document provides comprehensive guidelines for AI agents working with the GitLab client-go repository. It covers development practices, testing requirements, code formatting, API alignment, and code generation procedures.
+
+## Repository Overview
+
+The GitLab client-go is a Go client library for the GitLab API, enabling Go programs to interact with GitLab in a simple and uniform way. The repository follows strict Go best practices and maintains close alignment with GitLab's official API documentation.
+
+## Development Workflow
+
+### Prerequisites
+
+When asked to modify code, read CONTRIBUTING.md and README.md for examples and formatting instructions. Where the 
+instructions in CONTRIBUTING.md and README.md conflict with information in AGENTS.md, prefer the instructions in 
+CONTRIBUTING.md and README.md over the instructions in AGENTS.md
+
+When asked to perform analysis on the codebase instead of changing code, skipping the read of CONTRIBUTING.md and README.md
+is allowed and preferred, since understanding contributing guidelines is not required to perform analysis.
+
+### Required Tools
+
+- **Go** - Use the version specified in go.mod
+- **gofumpt** - Code formatter
+- **golangci-lint** - Linting tool
+- **buf** - Protocol buffer tools for code generation
+- **gomock** - Mock generation
+
+### Running Tests
+
+```bash
+# Run all tests with race detection
+make test
+
+# Run the complete reviewable process (includes tests)
+make reviewable
+```
+
+### Test Patterns
+
+- All tests use the `testing` package with `testify/assert`
+- Tests are parallelized using `t.Parallel()`
+- Mock HTTP handlers are used for API testing
+- Test data is stored in `testdata/` directory
+- Each service method should have corresponding test coverage
+  - **CRITICAL** - When fixing bugs or creating new features, ensure new test scenarios are added to cover the new logic.
+- When writing a test, write Gherkin comments in-line with the test to make the tests easier to read. This means adding GIVEN/WHEN/THEN comments in tests.
+
+
+### Test Structure Example
+
+```go
+func TestGetUser(t *testing.T) {
+    t.Parallel()
+    mux, client := setup(t)
+
+    path := "/api/v4/users/1"
+    mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
+        testMethod(t, r, http.MethodGet)
+        mustWriteHTTPResponse(t, w, "testdata/get_user.json")
+    })
+
+    user, _, err := client.Users.GetUser(1, GetUsersOptions{})
+    assert.NoError(t, err)
+    // ... assertions
+}
+```
+
+## Code Formatting and Linting
+
+### Formatting
+
+The project uses `gofumpt` for code formatting:
+
+```bash
+# Format all Go files
+make fmt
+```
+
+**Formatting Rules:**
+- Line width for comments: < 80 characters
+- Line width for code: < 100 characters (where sensible)
+- Use `gofumpt` for consistent formatting
+- Follow Go best practices
+
+### Linting
+
+```bash
+# Run all linters
+make lint
+```
+
+**Linting Configuration:**
+- Uses `golangci-lint` with custom configuration in `.golangci.yml`
+- Enabled linters: asciicheck, dogsled, dupword, errorlint, goconst, misspell, 
+  nakedret, nolintlint, revive, staticcheck, testifylint, unconvert, 
+  usestdlibvars, whitespace
+- Excludes generated files and examples directory
+
+## Mock generation
+
+This repository uses gomock to generate testing structs, which are in the `testing/` folder. These need to be kept up-to-date with function signatures to that the Service implementations match the interfaces that have generated mocks.
+
+### Available Generation Commands
+
+```bash
+# Generate all code (protobuf, mocks, testing client)
+make generate
+
+# Clean generated files
+make clean
+```
+
+### Generation Scripts
+
+1. **`scripts/generate_testing_client.sh`** - Generates testing client with mocks
+2. **`scripts/generate_mock_api.sh`** - Generates mock interfaces for all services
+3. **`scripts/generate_service_interface_map.sh`** - Generates service interface mapping
+
+### When to Regenerate
+
+- After adding new service interfaces
+- After modifying existing interfaces
+- Before committing changes
+- When mock generation fails
+
+## Function Comment Formatting
+
+### Required Comment Structure
+
+Every public function, type, and method must have properly formatted comments:
+
+```go
+// FunctionName performs a specific action with the given parameters.
+//
+// GitLab API docs: https://docs.gitlab.com/api/endpoint/
+func (s *ServiceName) FunctionName(param Type, opt *Options, options ...RequestOptionFunc) (*ReturnType, *Response, error) {
+    // Implementation
+}
+```
+
+### Comment Guidelines
+
+1. **Function Comments:**
+   - Start with function name (no "The" or "This function")
+   - Use present tense ("performs", "returns", "creates")
+   - Keep under 80 characters per line
+   - Include GitLab API documentation link
+
+2. **Type Comments:**
+   - Start with type name
+   - Describe the purpose and usage
+   - Include GitLab API documentation link
+
+3. **Struct Field Comments:**
+   - Use `json:"field_name"` tags
+   - Include `url:"field_name,omitempty"` for query parameters
+   - Document complex fields
+
+### GitLab API Documentation Alignment
+
+**CRITICAL: All code must align with GitLab's official API documentation.**
+
+#### API Documentation References
+
+Every function must reference the corresponding GitLab API documentation:
+
+```go
+// GitLab API docs: https://docs.gitlab.com/api/users/
+// GitLab API docs: https://docs.gitlab.com/api/projects/#list-all-projects
+// GitLab API docs: https://docs.gitlab.com/api/commits/#get-the-diff-of-a-commit
+```
+
+#### Field Ordering
+
+Struct fields and methods should be ordered to match the GitLab API documentation:
+
+```go
+type CreateProjectOptions struct {
+    Name                     *string `url:"name,omitempty" json:"name,omitempty"`
+    Description              *string `url:"description,omitempty" json:"description,omitempty"`
+    Visibility               *VisibilityValue `url:"visibility,omitempty" json:"visibility,omitempty"`
+    // ... other fields in API documentation order
+}
+```
+
+#### Parameter Validation
+
+- Use `any` type for project/group IDs to support both int64 and string. 
+- Implement proper parameter parsing with `parseID()` function
+- Validate required parameters before making API calls
+
+## Code Structure and Patterns
+
+### Service Structure
+
+Each GitLab API service follows this pattern:
+
+```go
+type (
+    ServiceNameInterface interface {
+        MethodName(opt *MethodOptions, options ...RequestOptionFunc) (*ReturnType, *Response, error)
+        // ... other methods
+    }
+
+    // ServiceName handles communication with the service related methods
+    // of the GitLab API.
+    //
+    // GitLab API docs: https://docs.gitlab.com/api/service/
+    ServiceName struct {
+        client *Client
+    }
+)
+
+var _ ServiceNameInterface = (*ServiceName)(nil)
+```
+
+### Request Options Pattern
+
+All API methods should accept `options ...RequestOptionFunc`:
+
+```go
+func (s *ServiceName) MethodName(opt *MethodOptions, options ...RequestOptionFunc) (*ReturnType, *Response, error) {
+    project, err := parseID(pid)
+    if err != nil {
+        return nil, nil, err
+    }
+    
+    u := fmt.Sprintf("projects/%s/endpoint", PathEscape(project))
+    
+    req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
+    if err != nil {
+        return nil, nil, err
+    }
+    
+    var result *ReturnType
+    resp, err := s.client.Do(req, &result)
+    if err != nil {
+        return nil, resp, err
+    }
+    
+    return result, resp, nil
+}
+```
+
+### Error Handling
+
+- Always return `(*Type, *Response, error)` tuple
+- Use `PathEscape()` for URL path parameters
+- Use `url.PathEscape()` for query parameters
+- Handle `parseID()` errors for project/group IDs
+
+### Type Usage
+
+- Do not use `interface{}`, use the `any` alias instead!
+- Do not use `int`, use `int64` instead! This applies to both slices and maps.
+
+## Pre-commit Checklist
+
+**CRITICAL: Tests MUST be run for every build or code modification.**
+**CRITICAL: Linting MUST pass for every build or code modification.**
+**CRITICAL: Mock generation should be run any time function signatures change**
+
+You can accomplish all three of these by running `make reviewable`, which will do:
+
+1. `make setup` - Install dependencies
+2. `make generate` - Generate required code
+3. `make fmt` - Format code
+4. `make lint` - Run linters
+5. `make test` - Run tests
+
+## Code Generation Guidelines
+
+### When Adding New Services
+
+1. Create the service file (e.g., `new_service.go`)
+2. Define the interface and struct following the established pattern
+3. Implement all methods with proper error handling
+4. Add comprehensive tests in `new_service_test.go`
+5. Run `make generate` to update mocks and testing client
+6. Ensure all tests pass with `make test`
+
+### Mock Generation
+
+The repository uses `gomock` for generating mocks:
+
+```bash
+# Generate mocks for all interfaces
+make generate
+```
+
+Mocks are automatically generated in the `testing/` package and should not be manually edited.
+
+## File Organization
+
+### Service Files
+
+- One service per file (e.g., `users.go`, `projects.go`)
+- Corresponding test file (e.g., `users_test.go`)
+- Interface definition at the top of the file
+- Service struct and implementation below
+
+### Generated Files
+
+- `testing/*_mock.go` - Generated mock files
+- `testing/*_generated.go` - Generated testing client files
+- `*_generated_test.go` - Generated test files
+
+**Never edit generated files manually.**
+
+## Common Patterns and Best Practices
+
+### Pointer Usage
+
+Use pointers for optional fields in structs:
+
+```go
+type CreateUserOptions struct {
+    Name     *string `url:"name,omitempty" json:"name,omitempty"`
+    Email    *string `url:"email,omitempty" json:"email,omitempty"`
+    Username *string `url:"username,omitempty" json:"username,omitempty"`
+}
+```
+
+### Time Handling
+
+Use `*time.Time` for time fields and `ISOTime` for custom time types that only support year-month-day formatting:
+
+```go
+type User struct {
+    CreatedAt *time.Time `json:"created_at"`
+    LastActivityOn *ISOTime `json:"last_activity_on"`
+}
+```
+
+### Response Handling
+
+Always return the full response for pagination and metadata:
+
+```go
+users, resp, err := client.Users.ListUsers(&gitlab.ListUsersOptions{})
+if err != nil {
+    return err
+}
+
+// Access pagination info
+fmt.Printf("Total pages: %d\n", resp.TotalPages)
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Tests failing after changes:**
+   - Run `make generate` to update mocks
+   - Check for linting errors with `make lint`
+   - Ensure all imports are correct
+
+2. **Linting errors:**
+   - Run `make fmt` to fix formatting issues
+   - Check `.golangci.yml` for specific rule configurations
+   - Address any static analysis warnings
+
+3. **Generation failures:**
+   - Ensure all interfaces are properly defined
+   - Check that service files follow the correct pattern
+   - Verify that all required tools are installed
+
+### Getting Help
+
+- Check existing issues in the [issue tracker](https://gitlab.com/gitlab-org/api/client-go/-/issues)
+- Review the [contributing guide](CONTRIBUTING.md)
+- Examine similar implementations in the codebase
+- Refer to [GitLab API documentation](https://docs.gitlab.com/ee/api/)
+
+## Summary
+
+When working with this repository:
+
+1. **Always run tests** - `make test` is mandatory
+2. **Follow formatting rules** - Use `gofumpt` and respect line limits
+3. **Align with GitLab API docs** - Every function must reference official documentation
+4. **Generate code when needed** - Run `make generate` after interface changes
+5. **Use proper commenting** - Include GitLab API links and follow format guidelines
+6. **Maintain consistency** - Follow established patterns and conventions
\ No newline at end of file
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/CHANGELOG.md b/vendor/gitlab.com/gitlab-org/api/client-go/CHANGELOG.md
index 1f5e48928..807bbe251 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/CHANGELOG.md
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/CHANGELOG.md
@@ -1,3 +1,452 @@
+## 0.159.0
+
+### 🚀 Features
+
+- feat(integrations): add group integration API endpoints for Jira ([!2563](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2563)) by [Harsh Rai](https://gitlab.com/harshrai654)
+
+### 🔄 Other Changes
+
+- chore(deps): update golangci/golangci-lint docker tag to v2.6.1 ([!2564](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2564)) by [GitLab Dependency Bot](https://gitlab.com/gitlab-dependency-update-bot)
+
+
+
+# [0.159.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.158.0...v0.159.0) (2025-11-04)
+
+
+### Features
+
+* **integrations:** add group integration API endpoints for Jira ([09e18ee](https://gitlab.com/gitlab-org/api/client-go/commit/09e18ee598bb7805ac8221f6a05426b1785f9011))
+
+## 0.158.0
+
+### 🚀 Features
+
+- Add support to send variables for GraphQL queries ([!2562](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2562)) by [rafasf](https://gitlab.com/rafasf)
+
+### 🔄 Other Changes
+
+- chore(deps): update module cel.dev/expr to v0.25.0 ([!2560](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2560)) by [GitLab Dependency Bot](https://gitlab.com/gitlab-dependency-update-bot)
+- chore(no-release): standardize GitLab name capitalization ([!2551](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2551)) by [Zubeen](https://gitlab.com/syedzubeen)
+- chore(deps): update golangci/golangci-lint docker tag to v2.6.0 ([!2558](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2558)) by [GitLab Dependency Bot](https://gitlab.com/gitlab-dependency-update-bot)
+- refactor: moved comments to interface 2 ([!2557](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2557)) by [Zubeen](https://gitlab.com/syedzubeen)
+- refactor: moved comments to interface ([!2556](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2556)) by [Zubeen](https://gitlab.com/syedzubeen)
+- refactor(test): avoid panic in tests with goroutines ([!2553](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2553)) by [Oleksandr Redko](https://gitlab.com/alexandear)
+
+
+
+# [0.158.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.157.1...v0.158.0) (2025-11-03)
+
+## 0.157.1
+
+### 🐛 Bug Fixes
+
+- fix(protected_packages): fix invalid types ([!2554](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2554)) by [Ruwen Schwedewsky](https://gitlab.com/RuwenSchwedewskySinch)
+
+### 🔄 Other Changes
+
+- chore: Update review instructions for mentioning GitLab ([!2552](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2552)) by [Zubeen](https://gitlab.com/syedzubeen)
+- Implement do function to reduce boilerplate ([!2550](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2550)) by [Timo Furrer](https://gitlab.com/timofurrer)
+- refactor(test): migrate to testify assertions 4 ([!2548](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2548)) by [Zubeen](https://gitlab.com/syedzubeen)
+- refactor(test): migrate to testify assertions 2 ([!2546](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2546)) by [Zubeen](https://gitlab.com/syedzubeen)
+- refactor(test): migrate to testify assertions ([!2545](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2545)) by [Zubeen](https://gitlab.com/syedzubeen)
+- refactor(test): migrate to testify assertions 5 ([!2549](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2549)) by [Zubeen](https://gitlab.com/syedzubeen)
+- test: add unit tests for cluster agents and deployments ([!2499](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2499)) by [Zubeen](https://gitlab.com/syedzubeen)
+- refactor(test): migrate to testify assertions 3 ([!2547](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2547)) by [Zubeen](https://gitlab.com/syedzubeen)
+- Fix: Helper Functions for Code Refactoring ([!2544](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2544)) by [Yashesvinee V](https://gitlab.com/yashes7516)
+- test: adds UT for formatPackageURL ([!2527](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2527)) by [Zubeen](https://gitlab.com/syedzubeen)
+- test: adds UT for getEpicLinks ([!2526](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2526)) by [Zubeen](https://gitlab.com/syedzubeen)
+- test: add test for ApproveOrRejectProjectDeployment ([!2498](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2498)) by [Zubeen](https://gitlab.com/syedzubeen)
+- test: adds UTs for packages ([!2529](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2529)) by [Zubeen](https://gitlab.com/syedzubeen)
+
+
+
+## [0.157.1](https://gitlab.com/gitlab-org/api/client-go/compare/v0.157.0...v0.157.1) (2025-10-28)
+
+
+### Bug Fixes
+
+* **no-release:** Helper Functions for Code Refactoring ([6feffea](https://gitlab.com/gitlab-org/api/client-go/commit/6feffea6696a8e333fd0811eee8501e58ba743e3))
+* **protected_packages:** fix invalid types ([c09943b](https://gitlab.com/gitlab-org/api/client-go/commit/c09943b0dde510dca32a2544a9c0f75f85943d96))
+
+## 0.157.0
+
+### 🚀 Features
+
+- Add merge requests commit api ([!2539](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2539)) by [Ilya Savitsky](https://gitlab.com/ipsavitsky234)
+
+### 🔄 Other Changes
+
+- test: adds missing UTs for notifications ([!2528](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2528)) by [Zubeen](https://gitlab.com/syedzubeen)
+- chore: Update review instructions ([!2537](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2537)) by [Patrick Rice](https://gitlab.com/PatrickRice)
+- chore(no-release): Fix godoc comments; enable godoclint ([!2535](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2535)) by [Oleksandr Redko](https://gitlab.com/alexandear)
+
+
+
+# [0.157.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.156.0...v0.157.0) (2025-10-13)
+
+## 0.156.0
+
+### 🚀 Features
+
+- feat(api): add support for test report summary api ([!2487](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2487)) by [Daniela Filipe Bento](https://gitlab.com/danifbento)
+
+
+
+# [0.156.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.155.0...v0.156.0) (2025-10-10)
+
+
+### Features
+
+* **api:** add support for test report summary api ([8a0c6dd](https://gitlab.com/gitlab-org/api/client-go/commit/8a0c6dde10a4c9c034274a439eaa060dc6e40995))
+
+## 0.155.0
+
+### 🚀 Features
+
+- feat(group_relations_export): Added Group Relations API integration ([!2508](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2508)) by [Jose Gabriel Companioni Benitez](https://gitlab.com/elC0mpa)
+
+### 🔄 Other Changes
+
+- chore: use local protoc plugin with buf ([!2536](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2536)) by [Timo Furrer](https://gitlab.com/timofurrer)
+- chore(no-release): Change generated file comment ([!2532](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2532)) by [Oleksandr Redko](https://gitlab.com/alexandear)
+- docs(no-release): Fix the comment for EnvVarGitLabContext ([!2533](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2533)) by [Oleksandr Redko](https://gitlab.com/alexandear)
+- feat(client_options): Added unit tests ([!2510](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2510)) by [Jose Gabriel Companioni Benitez](https://gitlab.com/elC0mpa)
+
+
+
+# [0.155.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.154.0...v0.155.0) (2025-10-09)
+
+
+### Features
+
+* **client_options:** Added unit tests ([c148031](https://gitlab.com/gitlab-org/api/client-go/commit/c14803189aa47a0cc9e64e9b455b93e6d4c4e4b9))
+* **group_relations_export:** Added Group Relations API integration ([956e039](https://gitlab.com/gitlab-org/api/client-go/commit/956e03950d6bc03c56fa1ea4c5d6e06bfd0b264f))
+
+## 0.154.0
+
+### 🚀 Features
+
+- feat(protected_packages): Add api integration ([!2520](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2520)) by [Jose Gabriel Companioni Benitez](https://gitlab.com/elC0mpa)
+
+
+
+# [0.154.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.153.0...v0.154.0) (2025-10-08)
+
+
+### Features
+
+* **protected_packages:** Add api integration ([2de15c7](https://gitlab.com/gitlab-org/api/client-go/commit/2de15c7875e232b0b0b1e5e5bb8e184cd11d0774))
+
+## 0.153.0
+
+### 🚀 Features
+
+- feat(project_Statistics): Added api integration ([!2512](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2512)) by [Jose Gabriel Companioni Benitez](https://gitlab.com/elC0mpa)
+
+### 🔄 Other Changes
+
+- refactor: moved comments to interface ([!2509](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2509)) by [ajey muthiah](https://gitlab.com/ajeymuthiah)
+- chore(no-release): Helper Functions for Code Refactoring ([!2503](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2503)) by [Yashesvinee V](https://gitlab.com/yashes7516)
+- Add t.Parallel() to all tests and enable linters ([!2513](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2513)) by [Timo Furrer](https://gitlab.com/timofurrer)
+- ci: Remove the `commitlint` job. ([!2511](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2511)) by [Florian Forster](https://gitlab.com/fforster)
+- refactor: moved comments to interface ([!2507](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2507)) by [ajey muthiah](https://gitlab.com/ajeymuthiah)
+
+
+
+# [0.153.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.152.0...v0.153.0) (2025-10-08)
+
+
+### Features
+
+* **project_Statistics:** Added api integration ([75b5a03](https://gitlab.com/gitlab-org/api/client-go/commit/75b5a03010a39d5353c975a558fda0b6f00cb697))
+
+## 0.152.0
+
+### 🚀 Features
+
+- feat(api): add api support for listing users who starred a project ([!2486](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2486)) by [ajey muthiah](https://gitlab.com/ajeymuthiah)
+
+### 🔄 Other Changes
+
+- chore(no-release): Update Duo Review Instructions ([!2502](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2502)) by [Patrick Rice](https://gitlab.com/PatrickRice)
+- feat(model_registry_api): Added api integration ([!2501](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2501)) by [Jose Gabriel Companioni Benitez](https://gitlab.com/elC0mpa)
+- feat(no-release): Add AGENTS.md file ([!2479](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2479)) by [Patrick Rice](https://gitlab.com/PatrickRice)
+- chore(no-release): Disable dependency scanning on personal forks ([!2500](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2500)) by [Patrick Rice](https://gitlab.com/PatrickRice)
+
+
+
+# [0.152.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.151.0...v0.152.0) (2025-10-06)
+
+
+### Features
+
+* **api:** add api support for listing users who starred a project ([0cdb4ce](https://gitlab.com/gitlab-org/api/client-go/commit/0cdb4ce5399b43e47bf120a90b16d00c022e194c))
+* **model_registry_api:** Added api integration ([065dd63](https://gitlab.com/gitlab-org/api/client-go/commit/065dd639bc8bd0f44cab4d92dbe3ea7f134b913f))
+* **no-release:** Add AGENTS.md file ([b9febab](https://gitlab.com/gitlab-org/api/client-go/commit/b9febab3181c3f87edd1fd99b5e596f76bc8b7cc))
+
+## 0.151.0
+
+### 🚀 Features
+
+- feat(api): add api support for delete enterprise user ([!2492](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2492)) by [ajey muthiah](https://gitlab.com/ajeymuthiah)
+
+### 🔄 Other Changes
+
+- docs(no-release): Make it easier to find the docs on issues ([!2497](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2497)) by [Heidi Berry](https://gitlab.com/heidi.berry)
+
+
+
+# [0.151.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.150.0...v0.151.0) (2025-10-04)
+
+
+### Features
+
+* **api:** add api support for delete enterprise user ([36ca8ab](https://gitlab.com/gitlab-org/api/client-go/commit/36ca8ab7672c352a073d59dacae3d763d4089abb))
+
+## 0.150.0
+
+### 🚀 Features
+
+- feat: add Project Aliases API support ([!2493](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2493)) by [Yashesvinee V](https://gitlab.com/yashes7516)
+
+### 🔄 Other Changes
+
+- chore(deps): update module buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go to v1.36.10-20250912141014-52f32327d4b0.1 ([!2495](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2495)) by [GitLab Dependency Bot](https://gitlab.com/gitlab-dependency-update-bot)
+- chore(deps): update module github.com/danieljoos/wincred to v1.2.3 ([!2494](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2494)) by [GitLab Dependency Bot](https://gitlab.com/gitlab-dependency-update-bot)
+
+
+
+# [0.150.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.149.0...v0.150.0) (2025-10-03)
+
+
+### Features
+
+* add Project Aliases API support ([4ece88e](https://gitlab.com/gitlab-org/api/client-go/commit/4ece88e6a8cfa0f53e68184b2905d4c2fb6e857a))
+
+## 0.149.0
+
+### 🚀 Features
+
+- feat(no-release): Add dependency scanning ([!2480](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2480)) by [Patrick Rice](https://gitlab.com/PatrickRice)
+
+### 🔄 Other Changes
+
+- ci(semantic-release): migrate to `@gitlab/semantic-release-merge-request-analyzer` ([!2490](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2490)) by [Florian Forster](https://gitlab.com/fforster)
+- ci: add the `autolabels` job ([!2489](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2489)) by [Florian Forster](https://gitlab.com/fforster)
+- chore(deps): update module google.golang.org/protobuf to v1.36.10 ([!2488](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2488)) by [GitLab Dependency Bot](https://gitlab.com/gitlab-dependency-update-bot)
+- refactor(no-release): added tests for delete project hook method ([!2482](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2482)) by [Jose Gabriel Companioni Benitez](https://gitlab.com/elC0mpa)
+- docs(no-release): Add guide for adding new APIs and issue templates ([!2478](https://gitlab.com/gitlab-org/api/client-go/-/merge_requests/2478)) by [Heidi Berry](https://gitlab.com/heidi.berry)
+
+
+
+# [0.149.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.148.1...v0.149.0) (2025-10-02)
+
+
+### Features
+
+* **no-release:** Add dependency scanning ([8b0ee10](https://gitlab.com/gitlab-org/api/client-go/commit/8b0ee10acb8adceb5d34be2165b7d587b1e42e49))
+
+## [0.148.1](https://gitlab.com/gitlab-org/api/client-go/compare/v0.148.0...v0.148.1) (2025-09-26)
+
+
+### Bug Fixes
+
+* label unmarshaling for `BasicMergeRequest` list operations ([e80c453](https://gitlab.com/gitlab-org/api/client-go/commit/e80c453aa6a5a265ec8748ae3f3f761a70f4470e))
+
+# [0.148.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.147.1...v0.148.0) (2025-09-23)
+
+
+### Features
+
+* **ResourceGroup:** add `newest_ready_first` to resource group `process_mode` ([fc8f743](https://gitlab.com/gitlab-org/api/client-go/commit/fc8f7431da4ca8594723105473687e8f1378df2b))
+
+## [0.147.1](https://gitlab.com/gitlab-org/api/client-go/compare/v0.147.0...v0.147.1) (2025-09-22)
+
+
+### Bug Fixes
+
+* **client:** use default retry policy from retryablehttp ([2a72511](https://gitlab.com/gitlab-org/api/client-go/commit/2a725113118608712f668b159ca2dab11f4e588e))
+
+# [0.147.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.146.0...v0.147.0) (2025-09-22)
+
+
+### Features
+
+* **Project:** add resource_group_default_process_mode ([7804faf](https://gitlab.com/gitlab-org/api/client-go/commit/7804fafa18cc15fec8a0886a081bf3311d72eb1f))
+
+# [0.146.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.145.0...v0.146.0) (2025-09-18)
+
+
+### Features
+
+* **pipelines:** Add compile-time type-safe pipeline inputs support ([4b30e60](https://gitlab.com/gitlab-org/api/client-go/commit/4b30e60260e4f06e7684352693aac49abd748579)), closes [gitlab-org/api/client-go#2154](https://gitlab.com/gitlab-org/api/client-go/issues/2154)
+* **PipelinesService:** Add support for pipeline inputs with type validation ([ab3056f](https://gitlab.com/gitlab-org/api/client-go/commit/ab3056f403ec0268e14b312de3f5b51b115ad97a)), closes [gitlab-org/api/client-go#2154](https://gitlab.com/gitlab-org/api/client-go/issues/2154)
+* **PipelineTriggersService:** Add support for pipeline inputs to trigger API ([9ad770e](https://gitlab.com/gitlab-org/api/client-go/commit/9ad770e49e59b2a41c665dfc4781f3b56650e813)), closes [gitlab-org/api/client-go#2154](https://gitlab.com/gitlab-org/api/client-go/issues/2154)
+
+# [0.145.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.144.1...v0.145.0) (2025-09-15)
+
+
+### Features
+
+* Add missing created_by field to ProjectMembers and GroupMembers ([5348e01](https://gitlab.com/gitlab-org/api/client-go/commit/5348e01913c358c53bdd3da46b069713273d6802))
+
+## [0.144.1](https://gitlab.com/gitlab-org/api/client-go/compare/v0.144.0...v0.144.1) (2025-09-13)
+
+# [0.144.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.143.3...v0.144.0) (2025-09-12)
+
+
+### Features
+
+* **client:** add http.RoundTripper Middleware Configuration Option to Client ([88f9d10](https://gitlab.com/gitlab-org/api/client-go/commit/88f9d1055acbd5e060ab13947b856ccc3a03da6f))
+
+## [0.143.3](https://gitlab.com/gitlab-org/api/client-go/compare/v0.143.2...v0.143.3) (2025-09-10)
+
+## [0.143.2](https://gitlab.com/gitlab-org/api/client-go/compare/v0.143.1...v0.143.2) (2025-09-09)
+
+## [0.143.1](https://gitlab.com/gitlab-org/api/client-go/compare/v0.143.0...v0.143.1) (2025-09-08)
+
+# [0.143.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.142.6...v0.143.0) (2025-09-08)
+
+
+### Features
+
+* **users:** Add support for PublicEmail to ListUsers ([74a3b6a](https://gitlab.com/gitlab-org/api/client-go/commit/74a3b6a7dd1340faa70ec1246b5b99394c56f90b))
+
+## [0.142.6](https://gitlab.com/gitlab-org/api/client-go/compare/v0.142.5...v0.142.6) (2025-09-02)
+
+## [0.142.5](https://gitlab.com/gitlab-org/api/client-go/compare/v0.142.4...v0.142.5) (2025-08-30)
+
+## [0.142.4](https://gitlab.com/gitlab-org/api/client-go/compare/v0.142.3...v0.142.4) (2025-08-28)
+
+## [0.142.3](https://gitlab.com/gitlab-org/api/client-go/compare/v0.142.2...v0.142.3) (2025-08-28)
+
+## [0.142.2](https://gitlab.com/gitlab-org/api/client-go/compare/v0.142.1...v0.142.2) (2025-08-26)
+
+## [0.142.1](https://gitlab.com/gitlab-org/api/client-go/compare/v0.142.0...v0.142.1) (2025-08-25)
+
+# [0.142.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.141.2...v0.142.0) (2025-08-21)
+
+
+### Features
+
+* **tokens:** add expiration filters and sorting options to ListPersonalAccessTokens ([0a9f797](https://gitlab.com/gitlab-org/api/client-go/commit/0a9f79790ac87c7f7b8e32e9cdea27fbc613728b))
+
+## [0.141.2](https://gitlab.com/gitlab-org/api/client-go/compare/v0.141.1...v0.141.2) (2025-08-20)
+
+## [0.141.1](https://gitlab.com/gitlab-org/api/client-go/compare/v0.141.0...v0.141.1) (2025-08-18)
+
+# [0.141.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.140.0...v0.141.0) (2025-08-18)
+
+
+### Features
+
+* **config:** support custom headers for instances ([76b0e82](https://gitlab.com/gitlab-org/api/client-go/commit/76b0e82ab57b21b7da915117fb37ac2bf56506e8))
+
+# [0.140.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.139.2...v0.140.0) (2025-08-18)
+
+
+### Features
+
+* **client:** add support for cookie jars ([4b525e3](https://gitlab.com/gitlab-org/api/client-go/commit/4b525e3f14741176ea8cbf4e7ae988b87455f4d0))
+
+## [0.139.2](https://gitlab.com/gitlab-org/api/client-go/compare/v0.139.1...v0.139.2) (2025-08-14)
+
+## [0.139.1](https://gitlab.com/gitlab-org/api/client-go/compare/v0.139.0...v0.139.1) (2025-08-14)
+
+# [0.139.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.138.0...v0.139.0) (2025-08-13)
+
+
+### Features
+
+* **terraform:** improve Terraform States service ([e08128b](https://gitlab.com/gitlab-org/api/client-go/commit/e08128bf87011455db06dc946e77b2a16ee36948))
+
+# [0.138.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.137.0...v0.138.0) (2025-08-12)
+
+
+### Bug Fixes
+
+* deprecate ListProjectInvidedGroupOptions due to a typo ([322496a](https://gitlab.com/gitlab-org/api/client-go/commit/322496a8a4c3fd7393b4b2c2b427c42fff243861))
+* Update config package name to v1beta1 ([f958e6b](https://gitlab.com/gitlab-org/api/client-go/commit/f958e6bd2935fddf4867d9992908e87288e89c20))
+
+
+### Features
+
+* add support for field "Created at" for Tags ([f363d57](https://gitlab.com/gitlab-org/api/client-go/commit/f363d57853f2e05c848e88946269c936f0b6bf76))
+* **app settings:** Add support for CanCreateOrganization ([1db661d](https://gitlab.com/gitlab-org/api/client-go/commit/1db661de26e0d3a78134c6bd1d31fb24d9a60677))
+* **hooks:** Add support for project webhook url variables ([efabed5](https://gitlab.com/gitlab-org/api/client-go/commit/efabed57d83eefe565aa2dbbb943d94212ec6167))
+* update datadog integration with new fields and API endpoints ([660ef31](https://gitlab.com/gitlab-org/api/client-go/commit/660ef31daf884bde545cfaa88432ac5ec7e3bfe7))
+* update external status checks to return the status check object ([2d78e8c](https://gitlab.com/gitlab-org/api/client-go/commit/2d78e8cc43971c4395c980672de7263c10401900))
+
+# [0.137.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.136.0...v0.137.0) (2025-07-21)
+
+
+### Features
+
+* **integrations:** add group harbor integration ([220e4cb](https://gitlab.com/gitlab-org/api/client-go/commit/220e4cb524d9303d36384043f29f96f43e4d9387))
+
+# [0.136.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.135.0...v0.136.0) (2025-07-21)
+
+
+### Features
+
+* **client:** add NewRequestToURL function for calls to absolute URLs ([524b571](https://gitlab.com/gitlab-org/api/client-go/commit/524b571339b7704e0e346a5a64f367265b96125f))
+* **projects:** Add support for RestoreProject ([b33e888](https://gitlab.com/gitlab-org/api/client-go/commit/b33e8882ad6611b1ac19222d0abdbfc477846ea1))
+
+# [0.135.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.134.0...v0.135.0) (2025-07-21)
+
+
+### Features
+
+* **config:** implement extensions API ([257f745](https://gitlab.com/gitlab-org/api/client-go/commit/257f74599727b6a006ba65b1c3efd7ff5fc7b86c))
+* **config:** initial push of the ability to use a config file for auth ([575c0cc](https://gitlab.com/gitlab-org/api/client-go/commit/575c0cc6a1de48582ea9b3b19cef021dc3f1397a))
+* **integrations:** add group integration for microsoft teams ([da0b1dd](https://gitlab.com/gitlab-org/api/client-go/commit/da0b1dd5b86fd6a41d7c043621611d0687fc628f))
+* **merge-requests:** add auto_merge, deprecate old field, for merging a request ([9119eb0](https://gitlab.com/gitlab-org/api/client-go/commit/9119eb0e6662f136e589cdee74aaa410136ca664))
+
+# [0.134.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.133.1...v0.134.0) (2025-07-07)
+
+
+### Features
+
+* **oauth:** implement OAuth2 helper package ([a44e8eb](https://gitlab.com/gitlab-org/api/client-go/commit/a44e8eb7743ff8d948f396b9849a82a7d7d6d6c4))
+
+## [0.133.1](https://gitlab.com/gitlab-org/api/client-go/compare/v0.133.0...v0.133.1) (2025-07-07)
+
+
+### Bug Fixes
+
+* deprecate ProjectReposityStorage due to a typo ([38a9652](https://gitlab.com/gitlab-org/api/client-go/commit/38a965279a4c570fd4db4f08503a63c4e7177439))
+
+# [0.133.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.132.0...v0.133.0) (2025-07-03)
+
+
+### Features
+
+* **testing:** allow to specify client options when creating test client ([9377147](https://gitlab.com/gitlab-org/api/client-go/commit/93771470166ce7c9097328b5e49f75a381c1720b))
+
+# [0.132.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.131.0...v0.132.0) (2025-07-02)
+
+
+### Bug Fixes
+
+* **no-release:** fix body-max-line-length ([f5d6d05](https://gitlab.com/gitlab-org/api/client-go/commit/f5d6d05d5781cd4fc31fa647ed94d486a1f6fa72))
+
+
+### Features
+
+* add missing ref_protected property from PushWebhookEventType ([15d0224](https://gitlab.com/gitlab-org/api/client-go/commit/15d0224575e7a5415783466afffe6c6b7aaf5dec))
+* add WithUserAgent client option ([3e8b80c](https://gitlab.com/gitlab-org/api/client-go/commit/3e8b80cd40b3d4ad54cb050ebd1b6e11b848869a))
+* export various auth sources ([281e408](https://gitlab.com/gitlab-org/api/client-go/commit/281e4083beed2b88b035dddcb562982d4c412143))
+* **serviceaccounts:** bring group service accounts in line with API ([a08974f](https://gitlab.com/gitlab-org/api/client-go/commit/a08974f284c043d4039495ed4b8f24ebeb256cdc))
+* **serviceaccounts:** bring group service accounts in line with API ([fb582a4](https://gitlab.com/gitlab-org/api/client-go/commit/fb582a4bb523443984851bc1d4b0fb699cfa2a9f))
+
+# [0.131.0](https://gitlab.com/gitlab-org/api/client-go/compare/v0.130.1...v0.131.0) (2025-07-01)
+
+
+### Features
+
+* add ScanAndCollect for pagination ([cbac9ae](https://gitlab.com/gitlab-org/api/client-go/commit/cbac9aed9bb3c7f8d175585a6d38baa3f2a7fbe1))
+* add support for optional query params to get commit statuses ([e1b29ad](https://gitlab.com/gitlab-org/api/client-go/commit/e1b29adfd37db39aae4e1547f336b71d67efcdb8))
+
 ## [0.130.1](https://gitlab.com/gitlab-org/api/client-go/compare/v0.130.0...v0.130.1) (2025-06-11)
 
 
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/CONTRIBUTING.md b/vendor/gitlab.com/gitlab-org/api/client-go/CONTRIBUTING.md
index fb7d5c273..25c74f5bb 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/CONTRIBUTING.md
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/CONTRIBUTING.md
@@ -43,16 +43,24 @@ New `struct` fields or methods should be placed (as much as possible) in the sam
 order as the ordering used in the public API docs. The idea is that this makes it
 easier to find things.
 
-### Setting up your local development environment to contribute
+## Setting up your local development environment to contribute
 
 1. Install dependencies:
+
    ```sh
    make setup
    ```
+
 1. Make your changes on your feature branch in the community fork or your personal fork
 1. Run the reviewable command, which tests, lints and formats the code:
+
    ```sh
    make reviewable
    ```
+
 1. Push your feature branch upstream
 1. Open up your merge request
+
+## Development Guides
+
+For more detailed advice on adding new features, please see our dedicated [guide for adding new APIs or Endpoints](./docs/guides/AddingAPISupport.md).
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/Makefile b/vendor/gitlab.com/gitlab-org/api/client-go/Makefile
index 510bcbf63..1ae79c886 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/Makefile
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/Makefile
@@ -8,24 +8,22 @@ help: ## Display this help
 
 reviewable: setup generate fmt lint test ## Run before committing.
 
-fmt: install-gofumpt ## Format code
+fmt: ## Format code
+	@buf format -w
 	@gofumpt -l -w *.go testing/*.go examples/*.go
 
-lint: install-golangci-lint ## Run linter
+lint: ## Run linter
 	@golangci-lint run
+	@buf format --exit-code
+	@buf lint
 
 .PHONY: setup
-setup: install-golangci-lint install-gofumpt ## Setup your local environment
+setup: ## Setup your local environment
 	go mod tidy
 
-install-golangci-lint:
-	@go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
-
-install-gofumpt:
-	@go install mvdan.cc/gofumpt@latest
-
 .PHONY: generate
-generate: install-gofumpt ## Generate files
+generate: ## Generate files
+	buf generate # install from .tool-versions
 	./scripts/generate_testing_client.sh
 	./scripts/generate_service_interface_map.sh
 	./scripts/generate_mock_api.sh
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/README.md b/vendor/gitlab.com/gitlab-org/api/client-go/README.md
index 33e0ce068..d8dc8d1a0 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/README.md
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/README.md
@@ -92,6 +92,139 @@ func main() {
 }
 ```
 
+#### Use OAuth2 helper package
+
+The following example demonstrates how to use the `gitlab.com/gitlab-org/api/client-go/oauth2` package:
+
+```go
+package main
+
+import (
+	"context"
+	"fmt"
+	"os/exec"
+
+	gitlab "gitlab.com/gitlab-org/api/client-go"
+	"gitlab.com/gitlab-org/api/client-go/gitlaboauth2"
+)
+
+func main() {
+	ctx := context.Background()
+	// Authorize with GitLab.com and OAuth2
+	clientID := "aaa"
+	redirectURL := "http://localhost:9999/auth/redirect"
+	scopes := []string{"read_api"}
+	config := gitlaboauth2.NewOAuth2Config("", clientID, redirectURL, scopes)
+
+	server := gitlaboauth2.NewCallbackServer(config, ":9999", func(url string) error {
+		return exec.Command("open", url).Start()
+	})
+
+	token, err := server.GetToken(ctx)
+	if err != nil {
+		panic(err)
+	}
+
+	client, err := gitlab.NewAuthSourceClient(gitlab.OAuthTokenSource{TokenSource: config.TokenSource(ctx, token)})
+	if err != nil {
+		panic(err)
+	}
+
+	user, _, err := client.Users.CurrentUser()
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Printf("Current user: %s\n", user.Username)
+}
+```
+
+#### Use the `config` package (experimental)
+
+The `config` package defines a configuration file format (YAML) to configure GitLab instances
+and their associated authentication methods combined in contexts (similar to what you might know from Kubernetes).
+
+The configuration is located in the users config directory (e.g. XDG config dir), in `gitlab/config.yaml`.
+
+A basic example for an OAuth flow for GitLab.com that stores the credentials in the systems keyring, looks like this:
+
+```yaml
+version: gitlab.com/config/v1beta1
+
+instances:
+    - name: gitlab-com
+      server: https://gitlab.com
+
+auths:
+    - name: oauth-keyring
+      auth-info:
+        oauth2:
+            access-token-source:
+                keyring:
+                    service: client-go
+                    user: access-token
+            refresh-token-source:
+                keyring:
+                    service: client-go
+                    user: refresh-token
+contexts:
+    - name: gitlab-com-keyring
+      instance: gitlab-com
+      auth: oauth-keyring
+
+current-context: gitlab-com-keyring
+```
+
+An application with `client-go` is able to effortlessly create a new client using that configuration:
+
+```go
+package main
+
+import (
+	"fmt"
+	"log"
+
+	"gitlab.com/gitlab-org/api/client-go"
+	"gitlab.com/gitlab-org/api/client-go/config"
+)
+
+func main() {
+	// Create a config with default location (~/.config/gitlab/config.yaml)
+	cfg := config.New(
+		config.WithOAuth2Settings(config.OAuth2Settings{
+			AuthorizationFlowEnabled: true,
+			CallbackServerListenAddr: ":7171",
+			Browser: func(url string) error {
+				fmt.Printf("Open: %s\n", url)
+				return nil
+			},
+			ClientID:    "<your-client-id>",
+			RedirectURL: "http://localhost:7171/auth/redirect",
+			Scopes:      []string{"read_api"},
+		}),
+	)
+
+	// Load the configuration
+	if err := cfg.Load(); err != nil {
+		log.Printf("Failed to load config: %v", err)
+		return
+	}
+
+	client, err := cfg.NewClient(gitlab.WithUserAgent("my-app"))
+	if err != nil {
+		log.Fatalf("Failed to create client: %v", err)
+	}
+
+	// Use the client
+	user, _, err := client.Users.CurrentUser()
+	if err != nil {
+		log.Fatalf("Failed to get current user: %v", err)
+	}
+
+	fmt.Printf("Authenticated as: %s (%s)\n", user.Name, user.Username)
+}
+```
+
 For complete usage of go-gitlab, see the full [package docs](https://godoc.org/gitlab.com/gitlab-org/api/client-go).
 
 ## Installation
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/access_requests.go b/vendor/gitlab.com/gitlab-org/api/client-go/access_requests.go
index 1c2ef31c7..a942862ae 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/access_requests.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/access_requests.go
@@ -24,13 +24,56 @@ import (
 
 type (
 	AccessRequestsServiceInterface interface {
+		// ListProjectAccessRequests gets a list of access requests
+		// viewable by the authenticated user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/access_requests/#list-access-requests-for-a-group-or-project
 		ListProjectAccessRequests(pid any, opt *ListAccessRequestsOptions, options ...RequestOptionFunc) ([]*AccessRequest, *Response, error)
+
+		// ListGroupAccessRequests gets a list of access requests
+		// viewable by the authenticated user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/access_requests/#list-access-requests-for-a-group-or-project
 		ListGroupAccessRequests(gid any, opt *ListAccessRequestsOptions, options ...RequestOptionFunc) ([]*AccessRequest, *Response, error)
+
+		// RequestProjectAccess requests access for the authenticated user
+		// to a group or project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/access_requests/#request-access-to-a-group-or-project
 		RequestProjectAccess(pid any, options ...RequestOptionFunc) (*AccessRequest, *Response, error)
+
+		// RequestGroupAccess requests access for the authenticated user
+		// to a group or project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/access_requests/#request-access-to-a-group-or-project
 		RequestGroupAccess(gid any, options ...RequestOptionFunc) (*AccessRequest, *Response, error)
+
+		// ApproveProjectAccessRequest approves an access request for the given user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/access_requests/#approve-an-access-request
 		ApproveProjectAccessRequest(pid any, user int, opt *ApproveAccessRequestOptions, options ...RequestOptionFunc) (*AccessRequest, *Response, error)
+
+		// ApproveGroupAccessRequest approves an access request for the given user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/access_requests/#approve-an-access-request
 		ApproveGroupAccessRequest(gid any, user int, opt *ApproveAccessRequestOptions, options ...RequestOptionFunc) (*AccessRequest, *Response, error)
+
+		// DenyProjectAccessRequest denies an access request for the given user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/access_requests/#deny-an-access-request
 		DenyProjectAccessRequest(pid any, user int, options ...RequestOptionFunc) (*Response, error)
+
+		// DenyGroupAccessRequest denies an access request for the given user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/access_requests/#deny-an-access-request
 		DenyGroupAccessRequest(gid any, user int, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -66,11 +109,6 @@ type AccessRequest struct {
 // https://docs.gitlab.com/api/access_requests/#list-access-requests-for-a-group-or-project
 type ListAccessRequestsOptions ListOptions
 
-// ListProjectAccessRequests gets a list of access requests
-// viewable by the authenticated user.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/access_requests/#list-access-requests-for-a-group-or-project
 func (s *AccessRequestsService) ListProjectAccessRequests(pid any, opt *ListAccessRequestsOptions, options ...RequestOptionFunc) ([]*AccessRequest, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -92,11 +130,6 @@ func (s *AccessRequestsService) ListProjectAccessRequests(pid any, opt *ListAcce
 	return ars, resp, nil
 }
 
-// ListGroupAccessRequests gets a list of access requests
-// viewable by the authenticated user.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/access_requests/#list-access-requests-for-a-group-or-project
 func (s *AccessRequestsService) ListGroupAccessRequests(gid any, opt *ListAccessRequestsOptions, options ...RequestOptionFunc) ([]*AccessRequest, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -118,11 +151,6 @@ func (s *AccessRequestsService) ListGroupAccessRequests(gid any, opt *ListAccess
 	return ars, resp, nil
 }
 
-// RequestProjectAccess requests access for the authenticated user
-// to a group or project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/access_requests/#request-access-to-a-group-or-project
 func (s *AccessRequestsService) RequestProjectAccess(pid any, options ...RequestOptionFunc) (*AccessRequest, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -144,11 +172,6 @@ func (s *AccessRequestsService) RequestProjectAccess(pid any, options ...Request
 	return ar, resp, nil
 }
 
-// RequestGroupAccess requests access for the authenticated user
-// to a group or project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/access_requests/#request-access-to-a-group-or-project
 func (s *AccessRequestsService) RequestGroupAccess(gid any, options ...RequestOptionFunc) (*AccessRequest, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -179,10 +202,6 @@ type ApproveAccessRequestOptions struct {
 	AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"`
 }
 
-// ApproveProjectAccessRequest approves an access request for the given user.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/access_requests/#approve-an-access-request
 func (s *AccessRequestsService) ApproveProjectAccessRequest(pid any, user int, opt *ApproveAccessRequestOptions, options ...RequestOptionFunc) (*AccessRequest, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -204,10 +223,6 @@ func (s *AccessRequestsService) ApproveProjectAccessRequest(pid any, user int, o
 	return ar, resp, nil
 }
 
-// ApproveGroupAccessRequest approves an access request for the given user.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/access_requests/#approve-an-access-request
 func (s *AccessRequestsService) ApproveGroupAccessRequest(gid any, user int, opt *ApproveAccessRequestOptions, options ...RequestOptionFunc) (*AccessRequest, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -229,10 +244,6 @@ func (s *AccessRequestsService) ApproveGroupAccessRequest(gid any, user int, opt
 	return ar, resp, nil
 }
 
-// DenyProjectAccessRequest denies an access request for the given user.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/access_requests/#deny-an-access-request
 func (s *AccessRequestsService) DenyProjectAccessRequest(pid any, user int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -248,10 +259,6 @@ func (s *AccessRequestsService) DenyProjectAccessRequest(pid any, user int, opti
 	return s.client.Do(req, nil)
 }
 
-// DenyGroupAccessRequest denies an access request for the given user.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/access_requests/#deny-an-access-request
 func (s *AccessRequestsService) DenyGroupAccessRequest(gid any, user int, options ...RequestOptionFunc) (*Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/alert_management.go b/vendor/gitlab.com/gitlab-org/api/client-go/alert_management.go
index b87f15c55..681c985da 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/alert_management.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/alert_management.go
@@ -23,9 +23,28 @@ import (
 
 type (
 	AlertManagementServiceInterface interface {
+		// UploadMetricImage uploads a metric image to a project alert.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/alert_management_alerts/#upload-metric-image
 		UploadMetricImage(pid any, alertIID int, content io.Reader, filename string, opt *UploadMetricImageOptions, options ...RequestOptionFunc) (*MetricImage, *Response, error)
+
+		// ListMetricImages lists all the metric images for a project alert.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/alert_management_alerts/#list-metric-images
 		ListMetricImages(pid any, alertIID int, opt *ListMetricImagesOptions, options ...RequestOptionFunc) ([]*MetricImage, *Response, error)
+
+		// UpdateMetricImage updates a metric image for a project alert.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/alert_management_alerts/#update-metric-image
 		UpdateMetricImage(pid any, alertIID int, id int, opt *UpdateMetricImageOptions, options ...RequestOptionFunc) (*MetricImage, *Response, error)
+
+		// DeleteMetricImage deletes a metric image for a project alert.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/alert_management_alerts/#delete-metric-image
 		DeleteMetricImage(pid any, alertIID int, id int, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -63,10 +82,6 @@ type UploadMetricImageOptions struct {
 	URLText *string `url:"url_text,omitempty" json:"url_text,omitempty"`
 }
 
-// UploadMetricImageOptions uploads a metric image to a project alert.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/alert_management_alerts/#upload-metric-image
 func (s *AlertManagementService) UploadMetricImage(pid any, alertIID int, content io.Reader, filename string, opt *UploadMetricImageOptions, options ...RequestOptionFunc) (*MetricImage, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -96,10 +111,6 @@ type ListMetricImagesOptions struct {
 	ListOptions
 }
 
-// ListMetricImages lists all the metric images for a project alert.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/alert_management_alerts/#list-metric-images
 func (s *AlertManagementService) ListMetricImages(pid any, alertIID int, opt *ListMetricImagesOptions, options ...RequestOptionFunc) ([]*MetricImage, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -130,10 +141,6 @@ type UpdateMetricImageOptions struct {
 	URLText *string `url:"url_text,omitempty" json:"url_text,omitempty"`
 }
 
-// UpdateMetricImage updates a metric image for a project alert.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/alert_management_alerts/#update-metric-image
 func (s *AlertManagementService) UpdateMetricImage(pid any, alertIID int, id int, opt *UpdateMetricImageOptions, options ...RequestOptionFunc) (*MetricImage, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -155,10 +162,6 @@ func (s *AlertManagementService) UpdateMetricImage(pid any, alertIID int, id int
 	return mi, resp, nil
 }
 
-// DeleteMetricImage deletes a metric image for a project alert.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/alert_management_alerts/#delete-metric-image
 func (s *AlertManagementService) DeleteMetricImage(pid any, alertIID int, id int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/appearance.go b/vendor/gitlab.com/gitlab-org/api/client-go/appearance.go
index 6b4724e50..876a2eb6f 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/appearance.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/appearance.go
@@ -20,13 +20,22 @@ import "net/http"
 
 type (
 	AppearanceServiceInterface interface {
+		// GetAppearance gets the current appearance configuration of the GitLab instance.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/appearance/#get-details-on-current-application-appearance
 		GetAppearance(options ...RequestOptionFunc) (*Appearance, *Response, error)
+
+		// ChangeAppearance changes the appearance configuration.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/appearance/#update-application-appearance
 		ChangeAppearance(opt *ChangeAppearanceOptions, options ...RequestOptionFunc) (*Appearance, *Response, error)
 	}
 
-	// AppearanceService handles communication with appearance of the Gitlab API.
+	// AppearanceService handles communication with appearance of the GitLab API.
 	//
-	// Gitlab API docs: https://docs.gitlab.com/api/appearance/
+	// GitLab API docs: https://docs.gitlab.com/api/appearance/
 	AppearanceService struct {
 		client *Client
 	}
@@ -36,7 +45,7 @@ var _ AppearanceServiceInterface = (*AppearanceService)(nil)
 
 // Appearance represents a GitLab appearance.
 //
-// Gitlab API docs: https://docs.gitlab.com/api/appearance/
+// GitLab API docs: https://docs.gitlab.com/api/appearance/
 type Appearance struct {
 	Title                       string `json:"title"`
 	Description                 string `json:"description"`
@@ -57,10 +66,6 @@ type Appearance struct {
 	EmailHeaderAndFooterEnabled bool   `json:"email_header_and_footer_enabled"`
 }
 
-// GetAppearance gets the current appearance configuration of the GitLab instance.
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/appearance/#get-details-on-current-application-appearance
 func (s *AppearanceService) GetAppearance(options ...RequestOptionFunc) (*Appearance, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodGet, "application/appearance", nil, options)
 	if err != nil {
@@ -101,10 +106,6 @@ type ChangeAppearanceOptions struct {
 	URL                         *string `url:"url,omitempty" json:"url,omitempty"`
 }
 
-// ChangeAppearance changes the appearance configuration.
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/appearance/#update-application-appearance
 func (s *AppearanceService) ChangeAppearance(opt *ChangeAppearanceOptions, options ...RequestOptionFunc) (*Appearance, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodPut, "application/appearance", opt, options)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/application_statistics.go b/vendor/gitlab.com/gitlab-org/api/client-go/application_statistics.go
index 5442852c5..df3f9d61a 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/application_statistics.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/application_statistics.go
@@ -18,6 +18,10 @@ import "net/http"
 
 type (
 	ApplicationStatisticsServiceInterface interface {
+		// GetApplicationStatistics gets details on the current application statistics.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/statistics/#get-details-on-current-application-statistics
 		GetApplicationStatistics(options ...RequestOptionFunc) (*ApplicationStatistics, *Response, error)
 	}
 
@@ -49,10 +53,6 @@ type ApplicationStatistics struct {
 	ActiveUsers   int `url:"active_users" json:"active_users"`
 }
 
-// GetApplicationStatistics gets details on the current application statistics.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/statistics/#get-details-on-current-application-statistics
 func (s *ApplicationStatisticsService) GetApplicationStatistics(options ...RequestOptionFunc) (*ApplicationStatistics, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodGet, "application/statistics", nil, options)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/applications.go b/vendor/gitlab.com/gitlab-org/api/client-go/applications.go
index 06dae54cb..2a5008be7 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/applications.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/applications.go
@@ -23,15 +23,29 @@ import (
 
 type (
 	ApplicationsServiceInterface interface {
+		// CreateApplication creates a new application owned by the authenticated user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/applications/#create-an-application
 		CreateApplication(opt *CreateApplicationOptions, options ...RequestOptionFunc) (*Application, *Response, error)
+
+		// ListApplications get a list of administrables applications by the authenticated user
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/applications/#list-all-applications
 		ListApplications(opt *ListApplicationsOptions, options ...RequestOptionFunc) ([]*Application, *Response, error)
+
+		// DeleteApplication removes a specific application.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/applications/#delete-an-application
 		DeleteApplication(application int, options ...RequestOptionFunc) (*Response, error)
 	}
 
 	// ApplicationsService handles communication with administrables applications
-	// of the Gitlab API.
+	// of the GitLab API.
 	//
-	// Gitlab API docs: https://docs.gitlab.com/api/applications/
+	// GitLab API docs: https://docs.gitlab.com/api/applications/
 	ApplicationsService struct {
 		client *Client
 	}
@@ -60,9 +74,6 @@ type CreateApplicationOptions struct {
 	Confidential *bool   `url:"confidential,omitempty" json:"confidential,omitempty"`
 }
 
-// CreateApplication creates a new application owned by the authenticated user.
-//
-// Gitlab API docs: https://docs.gitlab.com/api/applications/#create-an-application
 func (s *ApplicationsService) CreateApplication(opt *CreateApplicationOptions, options ...RequestOptionFunc) (*Application, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodPost, "applications", opt, options)
 	if err != nil {
@@ -82,9 +93,6 @@ func (s *ApplicationsService) CreateApplication(opt *CreateApplicationOptions, o
 // ListApplications() options.
 type ListApplicationsOptions ListOptions
 
-// ListApplications get a list of administrables applications by the authenticated user
-//
-// Gitlab API docs : https://docs.gitlab.com/api/applications/#list-all-applications
 func (s *ApplicationsService) ListApplications(opt *ListApplicationsOptions, options ...RequestOptionFunc) ([]*Application, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodGet, "applications", opt, options)
 	if err != nil {
@@ -100,10 +108,6 @@ func (s *ApplicationsService) ListApplications(opt *ListApplicationsOptions, opt
 	return as, resp, nil
 }
 
-// DeleteApplication removes a specific application.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/applications/#delete-an-application
 func (s *ApplicationsService) DeleteApplication(application int, options ...RequestOptionFunc) (*Response, error) {
 	u := fmt.Sprintf("applications/%d", application)
 
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/audit_events.go b/vendor/gitlab.com/gitlab-org/api/client-go/audit_events.go
index 3a61d8a4f..6815bff77 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/audit_events.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/audit_events.go
@@ -8,11 +8,41 @@ import (
 
 type (
 	AuditEventsServiceInterface interface {
+		// ListInstanceAuditEvents gets a list of audit events for instance.
+		// Authentication as Administrator is required.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/audit_events/#retrieve-all-instance-audit-events
 		ListInstanceAuditEvents(opt *ListAuditEventsOptions, options ...RequestOptionFunc) ([]*AuditEvent, *Response, error)
+
+		// GetInstanceAuditEvent gets a specific instance audit event.
+		// Authentication as Administrator is required.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/audit_events/#retrieve-single-instance-audit-event
 		GetInstanceAuditEvent(event int, options ...RequestOptionFunc) (*AuditEvent, *Response, error)
+
+		// ListGroupAuditEvents gets a list of audit events for the specified group
+		// viewable by the authenticated user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/audit_events/#retrieve-all-group-audit-events
 		ListGroupAuditEvents(gid any, opt *ListAuditEventsOptions, options ...RequestOptionFunc) ([]*AuditEvent, *Response, error)
+
+		// GetGroupAuditEvent gets a specific group audit event.
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/audit_events/#retrieve-a-specific-group-audit-event
 		GetGroupAuditEvent(gid any, event int, options ...RequestOptionFunc) (*AuditEvent, *Response, error)
+
+		// ListProjectAuditEvents gets a list of audit events for the specified project
+		// viewable by the authenticated user.
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/audit_events/#retrieve-all-project-audit-events
 		ListProjectAuditEvents(pid any, opt *ListAuditEventsOptions, options ...RequestOptionFunc) ([]*AuditEvent, *Response, error)
+
+		// GetProjectAuditEvent gets a specific project audit event.
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/audit_events/#retrieve-a-specific-project-audit-event
 		GetProjectAuditEvent(pid any, event int, options ...RequestOptionFunc) (*AuditEvent, *Response, error)
 	}
 
@@ -77,10 +107,6 @@ type ListAuditEventsOptions struct {
 	CreatedBefore *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
 }
 
-// ListInstanceAuditEvents gets a list of audit events for instance.
-// Authentication as Administrator is required.
-//
-// GitLab API docs: https://docs.gitlab.com/api/audit_events/#retrieve-all-instance-audit-events
 func (s *AuditEventsService) ListInstanceAuditEvents(opt *ListAuditEventsOptions, options ...RequestOptionFunc) ([]*AuditEvent, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodGet, "audit_events", opt, options)
 	if err != nil {
@@ -96,10 +122,6 @@ func (s *AuditEventsService) ListInstanceAuditEvents(opt *ListAuditEventsOptions
 	return aes, resp, nil
 }
 
-// GetInstanceAuditEvent gets a specific instance audit event.
-// Authentication as Administrator is required.
-//
-// GitLab API docs: https://docs.gitlab.com/api/audit_events/#retrieve-single-instance-audit-event
 func (s *AuditEventsService) GetInstanceAuditEvent(event int, options ...RequestOptionFunc) (*AuditEvent, *Response, error) {
 	u := fmt.Sprintf("audit_events/%d", event)
 
@@ -117,10 +139,6 @@ func (s *AuditEventsService) GetInstanceAuditEvent(event int, options ...Request
 	return ae, resp, nil
 }
 
-// ListGroupAuditEvents gets a list of audit events for the specified group
-// viewable by the authenticated user.
-//
-// GitLab API docs: https://docs.gitlab.com/api/audit_events/#retrieve-all-group-audit-events
 func (s *AuditEventsService) ListGroupAuditEvents(gid any, opt *ListAuditEventsOptions, options ...RequestOptionFunc) ([]*AuditEvent, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -142,9 +160,6 @@ func (s *AuditEventsService) ListGroupAuditEvents(gid any, opt *ListAuditEventsO
 	return aes, resp, nil
 }
 
-// GetGroupAuditEvent gets a specific group audit event.
-//
-// GitLab API docs: https://docs.gitlab.com/api/audit_events/#retrieve-a-specific-group-audit-event
 func (s *AuditEventsService) GetGroupAuditEvent(gid any, event int, options ...RequestOptionFunc) (*AuditEvent, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -166,10 +181,6 @@ func (s *AuditEventsService) GetGroupAuditEvent(gid any, event int, options ...R
 	return ae, resp, nil
 }
 
-// ListProjectAuditEvents gets a list of audit events for the specified project
-// viewable by the authenticated user.
-//
-// GitLab API docs: https://docs.gitlab.com/api/audit_events/#retrieve-all-project-audit-events
 func (s *AuditEventsService) ListProjectAuditEvents(pid any, opt *ListAuditEventsOptions, options ...RequestOptionFunc) ([]*AuditEvent, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -191,10 +202,6 @@ func (s *AuditEventsService) ListProjectAuditEvents(pid any, opt *ListAuditEvent
 	return aes, resp, nil
 }
 
-// GetProjectAuditEvent gets a specific project audit event.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/audit_events/#retrieve-a-specific-project-audit-event
 func (s *AuditEventsService) GetProjectAuditEvent(pid any, event int, options ...RequestOptionFunc) (*AuditEvent, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/avatar.go b/vendor/gitlab.com/gitlab-org/api/client-go/avatar.go
index e0bf2d6f7..7536936fa 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/avatar.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/avatar.go
@@ -22,6 +22,10 @@ import (
 
 type (
 	AvatarRequestsServiceInterface interface {
+		// GetAvatar gets the avatar URL for a user with the given email address.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/avatar/#get-details-on-an-account-avatar
 		GetAvatar(opt *GetAvatarOptions, options ...RequestOptionFunc) (*Avatar, *Response, error)
 	}
 
@@ -52,10 +56,6 @@ type GetAvatarOptions struct {
 	Size  *int    `url:"size,omitempty" json:"size,omitempty"`
 }
 
-// GetAvatar gets the avatar URL for a user with the given email address.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/avatar/#get-details-on-an-account-avatar
 func (s *AvatarRequestsService) GetAvatar(opt *GetAvatarOptions, options ...RequestOptionFunc) (*Avatar, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodGet, "avatar", opt, options)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/award_emojis.go b/vendor/gitlab.com/gitlab-org/api/client-go/award_emojis.go
index 525ec4475..948b7f2dd 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/award_emojis.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/award_emojis.go
@@ -24,29 +24,154 @@ import (
 
 type (
 	AwardEmojiServiceInterface interface {
+		// ListMergeRequestAwardEmoji gets a list of all award emoji on the merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#list-an-awardables-emoji-reactions
 		ListMergeRequestAwardEmoji(pid any, mergeRequestIID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error)
+
+		// ListIssueAwardEmoji gets a list of all award emoji on the issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#list-an-awardables-emoji-reactions
 		ListIssueAwardEmoji(pid any, issueIID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error)
+
+		// ListSnippetAwardEmoji gets a list of all award emoji on the snippet.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#list-an-awardables-emoji-reactions
 		ListSnippetAwardEmoji(pid any, snippetID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error)
+
+		// GetMergeRequestAwardEmoji get an award emoji from merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#get-single-emoji-reaction
 		GetMergeRequestAwardEmoji(pid any, mergeRequestIID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error)
+
+		// GetIssueAwardEmoji get an award emoji from issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#get-single-emoji-reaction
 		GetIssueAwardEmoji(pid any, issueIID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error)
+
+		// GetSnippetAwardEmoji get an award emoji from snippet.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#get-single-emoji-reaction
 		GetSnippetAwardEmoji(pid any, snippetID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error)
+
+		// CreateMergeRequestAwardEmoji get an award emoji from merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#add-a-new-emoji-reaction
 		CreateMergeRequestAwardEmoji(pid any, mergeRequestIID int, opt *CreateAwardEmojiOptions, options ...RequestOptionFunc) (*AwardEmoji, *Response, error)
+
+		// CreateIssueAwardEmoji get an award emoji from issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#add-a-new-emoji-reaction
 		CreateIssueAwardEmoji(pid any, issueIID int, opt *CreateAwardEmojiOptions, options ...RequestOptionFunc) (*AwardEmoji, *Response, error)
+
+		// CreateSnippetAwardEmoji get an award emoji from snippet.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#add-a-new-emoji-reaction
 		CreateSnippetAwardEmoji(pid any, snippetID int, opt *CreateAwardEmojiOptions, options ...RequestOptionFunc) (*AwardEmoji, *Response, error)
+
+		// DeleteIssueAwardEmoji delete award emoji on an issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#delete-an-emoji-reaction
 		DeleteIssueAwardEmoji(pid any, issueIID, awardID int, options ...RequestOptionFunc) (*Response, error)
+
+		// DeleteMergeRequestAwardEmoji delete award emoji on a merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#delete-an-emoji-reaction
 		DeleteMergeRequestAwardEmoji(pid any, mergeRequestIID, awardID int, options ...RequestOptionFunc) (*Response, error)
+
+		// DeleteSnippetAwardEmoji delete award emoji on a snippet.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#delete-an-emoji-reaction
 		DeleteSnippetAwardEmoji(pid any, snippetID, awardID int, options ...RequestOptionFunc) (*Response, error)
+
+		// ListIssuesAwardEmojiOnNote gets a list of all award emoji on a note from the
+		// issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#list-a-comments-emoji-reactions
 		ListIssuesAwardEmojiOnNote(pid any, issueID, noteID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error)
+
+		// ListMergeRequestAwardEmojiOnNote gets a list of all award emoji on a note
+		// from the merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#list-a-comments-emoji-reactions
 		ListMergeRequestAwardEmojiOnNote(pid any, mergeRequestIID, noteID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error)
+
+		// ListSnippetAwardEmojiOnNote gets a list of all award emoji on a note from the
+		// snippet.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#list-a-comments-emoji-reactions
 		ListSnippetAwardEmojiOnNote(pid any, snippetIID, noteID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error)
+
+		// GetIssuesAwardEmojiOnNote gets an award emoji on a note from an issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#get-an-emoji-reaction-for-a-comment
 		GetIssuesAwardEmojiOnNote(pid any, issueID, noteID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error)
+
+		// GetMergeRequestAwardEmojiOnNote gets an award emoji on a note from a
+		// merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#get-an-emoji-reaction-for-a-comment
 		GetMergeRequestAwardEmojiOnNote(pid any, mergeRequestIID, noteID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error)
+
+		// GetSnippetAwardEmojiOnNote gets an award emoji on a note from a snippet.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#get-an-emoji-reaction-for-a-comment
 		GetSnippetAwardEmojiOnNote(pid any, snippetIID, noteID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error)
+
+		// CreateIssuesAwardEmojiOnNote gets an award emoji on a note from an issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#add-a-new-emoji-reaction-to-a-comment
 		CreateIssuesAwardEmojiOnNote(pid any, issueID, noteID int, opt *CreateAwardEmojiOptions, options ...RequestOptionFunc) (*AwardEmoji, *Response, error)
+
+		// CreateMergeRequestAwardEmojiOnNote gets an award emoji on a note from a
+		// merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#add-a-new-emoji-reaction-to-a-comment
 		CreateMergeRequestAwardEmojiOnNote(pid any, mergeRequestIID, noteID int, opt *CreateAwardEmojiOptions, options ...RequestOptionFunc) (*AwardEmoji, *Response, error)
+
+		// CreateSnippetAwardEmojiOnNote gets an award emoji on a note from a snippet.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#add-a-new-emoji-reaction-to-a-comment
 		CreateSnippetAwardEmojiOnNote(pid any, snippetIID, noteID int, opt *CreateAwardEmojiOptions, options ...RequestOptionFunc) (*AwardEmoji, *Response, error)
+
+		// DeleteIssuesAwardEmojiOnNote deletes an award emoji on a note from an issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#delete-an-emoji-reaction-from-a-comment
 		DeleteIssuesAwardEmojiOnNote(pid any, issueID, noteID, awardID int, options ...RequestOptionFunc) (*Response, error)
+
+		// DeleteMergeRequestAwardEmojiOnNote deletes an award emoji on a note from a
+		// merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#delete-an-emoji-reaction-from-a-comment
 		DeleteMergeRequestAwardEmojiOnNote(pid any, mergeRequestIID, noteID, awardID int, options ...RequestOptionFunc) (*Response, error)
+
+		// DeleteSnippetAwardEmojiOnNote deletes an award emoji on a note from a snippet.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/emoji_reactions/#delete-an-emoji-reaction-from-a-comment
 		DeleteSnippetAwardEmojiOnNote(pid any, snippetIID, noteID, awardID int, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -94,26 +219,14 @@ const (
 // https://docs.gitlab.com/api/emoji_reactions/
 type ListAwardEmojiOptions ListOptions
 
-// ListMergeRequestAwardEmoji gets a list of all award emoji on the merge request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#list-an-awardables-emoji-reactions
 func (s *AwardEmojiService) ListMergeRequestAwardEmoji(pid any, mergeRequestIID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error) {
 	return s.listAwardEmoji(pid, awardMergeRequest, mergeRequestIID, opt, options...)
 }
 
-// ListIssueAwardEmoji gets a list of all award emoji on the issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#list-an-awardables-emoji-reactions
 func (s *AwardEmojiService) ListIssueAwardEmoji(pid any, issueIID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error) {
 	return s.listAwardEmoji(pid, awardIssue, issueIID, opt, options...)
 }
 
-// ListSnippetAwardEmoji gets a list of all award emoji on the snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#list-an-awardables-emoji-reactions
 func (s *AwardEmojiService) ListSnippetAwardEmoji(pid any, snippetID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error) {
 	return s.listAwardEmoji(pid, awardSnippets, snippetID, opt, options...)
 }
@@ -143,26 +256,14 @@ func (s *AwardEmojiService) listAwardEmoji(pid any, resource string, resourceID
 	return as, resp, nil
 }
 
-// GetMergeRequestAwardEmoji get an award emoji from merge request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#get-single-emoji-reaction
 func (s *AwardEmojiService) GetMergeRequestAwardEmoji(pid any, mergeRequestIID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
 	return s.getAwardEmoji(pid, awardMergeRequest, mergeRequestIID, awardID, options...)
 }
 
-// GetIssueAwardEmoji get an award emoji from issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#get-single-emoji-reaction
 func (s *AwardEmojiService) GetIssueAwardEmoji(pid any, issueIID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
 	return s.getAwardEmoji(pid, awardIssue, issueIID, awardID, options...)
 }
 
-// GetSnippetAwardEmoji get an award emoji from snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#get-single-emoji-reaction
 func (s *AwardEmojiService) GetSnippetAwardEmoji(pid any, snippetID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
 	return s.getAwardEmoji(pid, awardSnippets, snippetID, awardID, options...)
 }
@@ -202,26 +303,14 @@ type CreateAwardEmojiOptions struct {
 	Name string `json:"name"`
 }
 
-// CreateMergeRequestAwardEmoji get an award emoji from merge request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#add-a-new-emoji-reaction
 func (s *AwardEmojiService) CreateMergeRequestAwardEmoji(pid any, mergeRequestIID int, opt *CreateAwardEmojiOptions, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
 	return s.createAwardEmoji(pid, awardMergeRequest, mergeRequestIID, opt, options...)
 }
 
-// CreateIssueAwardEmoji get an award emoji from issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#add-a-new-emoji-reaction
 func (s *AwardEmojiService) CreateIssueAwardEmoji(pid any, issueIID int, opt *CreateAwardEmojiOptions, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
 	return s.createAwardEmoji(pid, awardIssue, issueIID, opt, options...)
 }
 
-// CreateSnippetAwardEmoji get an award emoji from snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#add-a-new-emoji-reaction
 func (s *AwardEmojiService) CreateSnippetAwardEmoji(pid any, snippetID int, opt *CreateAwardEmojiOptions, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
 	return s.createAwardEmoji(pid, awardSnippets, snippetID, opt, options...)
 }
@@ -251,26 +340,14 @@ func (s *AwardEmojiService) createAwardEmoji(pid any, resource string, resourceI
 	return a, resp, nil
 }
 
-// DeleteIssueAwardEmoji delete award emoji on an issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#delete-an-emoji-reaction
 func (s *AwardEmojiService) DeleteIssueAwardEmoji(pid any, issueIID, awardID int, options ...RequestOptionFunc) (*Response, error) {
 	return s.deleteAwardEmoji(pid, awardIssue, issueIID, awardID, options...)
 }
 
-// DeleteMergeRequestAwardEmoji delete award emoji on a merge request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#delete-an-emoji-reaction
 func (s *AwardEmojiService) DeleteMergeRequestAwardEmoji(pid any, mergeRequestIID, awardID int, options ...RequestOptionFunc) (*Response, error) {
 	return s.deleteAwardEmoji(pid, awardMergeRequest, mergeRequestIID, awardID, options...)
 }
 
-// DeleteSnippetAwardEmoji delete award emoji on a snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#delete-an-emoji-reaction
 func (s *AwardEmojiService) DeleteSnippetAwardEmoji(pid any, snippetID, awardID int, options ...RequestOptionFunc) (*Response, error) {
 	return s.deleteAwardEmoji(pid, awardSnippets, snippetID, awardID, options...)
 }
@@ -294,40 +371,25 @@ func (s *AwardEmojiService) deleteAwardEmoji(pid any, resource string, resourceI
 	return s.client.Do(req, nil)
 }
 
-// ListIssuesAwardEmojiOnNote gets a list of all award emoji on a note from the
-// issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#list-a-comments-emoji-reactions
 func (s *AwardEmojiService) ListIssuesAwardEmojiOnNote(pid any, issueID, noteID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error) {
 	return s.listAwardEmojiOnNote(pid, awardIssue, issueID, noteID, opt, options...)
 }
 
-// ListMergeRequestAwardEmojiOnNote gets a list of all award emoji on a note
-// from the merge request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#list-a-comments-emoji-reactions
 func (s *AwardEmojiService) ListMergeRequestAwardEmojiOnNote(pid any, mergeRequestIID, noteID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error) {
 	return s.listAwardEmojiOnNote(pid, awardMergeRequest, mergeRequestIID, noteID, opt, options...)
 }
 
-// ListSnippetAwardEmojiOnNote gets a list of all award emoji on a note from the
-// snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#list-a-comments-emoji-reactions
 func (s *AwardEmojiService) ListSnippetAwardEmojiOnNote(pid any, snippetIID, noteID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error) {
 	return s.listAwardEmojiOnNote(pid, awardSnippets, snippetIID, noteID, opt, options...)
 }
 
-func (s *AwardEmojiService) listAwardEmojiOnNote(pid any, resources string, ressourceID, noteID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error) {
+func (s *AwardEmojiService) listAwardEmojiOnNote(pid any, resources string, resourceID, noteID int, opt *ListAwardEmojiOptions, options ...RequestOptionFunc) ([]*AwardEmoji, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
 		return nil, nil, err
 	}
 	u := fmt.Sprintf("projects/%s/%s/%d/notes/%d/award_emoji", PathEscape(project), resources,
-		ressourceID, noteID)
+		resourceID, noteID)
 
 	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
 	if err != nil {
@@ -343,40 +405,27 @@ func (s *AwardEmojiService) listAwardEmojiOnNote(pid any, resources string, ress
 	return as, resp, nil
 }
 
-// GetIssuesAwardEmojiOnNote gets an award emoji on a note from an issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#get-an-emoji-reaction-for-a-comment
 func (s *AwardEmojiService) GetIssuesAwardEmojiOnNote(pid any, issueID, noteID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
 	return s.getSingleNoteAwardEmoji(pid, awardIssue, issueID, noteID, awardID, options...)
 }
 
-// GetMergeRequestAwardEmojiOnNote gets an award emoji on a note from a
-// merge request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#get-an-emoji-reaction-for-a-comment
 func (s *AwardEmojiService) GetMergeRequestAwardEmojiOnNote(pid any, mergeRequestIID, noteID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
 	return s.getSingleNoteAwardEmoji(pid, awardMergeRequest, mergeRequestIID, noteID, awardID,
 		options...)
 }
 
-// GetSnippetAwardEmojiOnNote gets an award emoji on a note from a snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#get-an-emoji-reaction-for-a-comment
 func (s *AwardEmojiService) GetSnippetAwardEmojiOnNote(pid any, snippetIID, noteID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
 	return s.getSingleNoteAwardEmoji(pid, awardSnippets, snippetIID, noteID, awardID, options...)
 }
 
-func (s *AwardEmojiService) getSingleNoteAwardEmoji(pid any, ressource string, resourceID, noteID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
+func (s *AwardEmojiService) getSingleNoteAwardEmoji(pid any, resource string, resourceID, noteID, awardID int, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
 		return nil, nil, err
 	}
 	u := fmt.Sprintf("projects/%s/%s/%d/notes/%d/award_emoji/%d",
 		PathEscape(project),
-		ressource,
+		resource,
 		resourceID,
 		noteID,
 		awardID,
@@ -396,27 +445,14 @@ func (s *AwardEmojiService) getSingleNoteAwardEmoji(pid any, ressource string, r
 	return a, resp, nil
 }
 
-// CreateIssuesAwardEmojiOnNote gets an award emoji on a note from an issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#add-a-new-emoji-reaction-to-a-comment
 func (s *AwardEmojiService) CreateIssuesAwardEmojiOnNote(pid any, issueID, noteID int, opt *CreateAwardEmojiOptions, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
 	return s.createAwardEmojiOnNote(pid, awardIssue, issueID, noteID, opt, options...)
 }
 
-// CreateMergeRequestAwardEmojiOnNote gets an award emoji on a note from a
-// merge request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#add-a-new-emoji-reaction-to-a-comment
 func (s *AwardEmojiService) CreateMergeRequestAwardEmojiOnNote(pid any, mergeRequestIID, noteID int, opt *CreateAwardEmojiOptions, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
 	return s.createAwardEmojiOnNote(pid, awardMergeRequest, mergeRequestIID, noteID, opt, options...)
 }
 
-// CreateSnippetAwardEmojiOnNote gets an award emoji on a note from a snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#add-a-new-emoji-reaction-to-a-comment
 func (s *AwardEmojiService) CreateSnippetAwardEmojiOnNote(pid any, snippetIID, noteID int, opt *CreateAwardEmojiOptions, options ...RequestOptionFunc) (*AwardEmoji, *Response, error) {
 	return s.createAwardEmojiOnNote(pid, awardSnippets, snippetIID, noteID, opt, options...)
 }
@@ -451,28 +487,15 @@ func (s *AwardEmojiService) createAwardEmojiOnNote(pid any, resource string, res
 	return a, resp, nil
 }
 
-// DeleteIssuesAwardEmojiOnNote deletes an award emoji on a note from an issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#delete-an-emoji-reaction-from-a-comment
 func (s *AwardEmojiService) DeleteIssuesAwardEmojiOnNote(pid any, issueID, noteID, awardID int, options ...RequestOptionFunc) (*Response, error) {
 	return s.deleteAwardEmojiOnNote(pid, awardIssue, issueID, noteID, awardID, options...)
 }
 
-// DeleteMergeRequestAwardEmojiOnNote deletes an award emoji on a note from a
-// merge request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#delete-an-emoji-reaction-from-a-comment
 func (s *AwardEmojiService) DeleteMergeRequestAwardEmojiOnNote(pid any, mergeRequestIID, noteID, awardID int, options ...RequestOptionFunc) (*Response, error) {
 	return s.deleteAwardEmojiOnNote(pid, awardMergeRequest, mergeRequestIID, noteID, awardID,
 		options...)
 }
 
-// DeleteSnippetAwardEmojiOnNote deletes an award emoji on a note from a snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/emoji_reactions/#delete-an-emoji-reaction-from-a-comment
 func (s *AwardEmojiService) DeleteSnippetAwardEmojiOnNote(pid any, snippetIID, noteID, awardID int, options ...RequestOptionFunc) (*Response, error) {
 	return s.deleteAwardEmojiOnNote(pid, awardSnippets, snippetIID, noteID, awardID, options...)
 }
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/boards.go b/vendor/gitlab.com/gitlab-org/api/client-go/boards.go
index 1d1558d02..488dccc74 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/boards.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/boards.go
@@ -23,15 +23,66 @@ import (
 
 type (
 	IssueBoardsServiceInterface interface {
+		// CreateIssueBoard creates a new issue board.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/boards/#create-an-issue-board
 		CreateIssueBoard(pid any, opt *CreateIssueBoardOptions, options ...RequestOptionFunc) (*IssueBoard, *Response, error)
+
+		// UpdateIssueBoard update an issue board.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/boards/#update-an-issue-board
 		UpdateIssueBoard(pid any, board int, opt *UpdateIssueBoardOptions, options ...RequestOptionFunc) (*IssueBoard, *Response, error)
+
+		// DeleteIssueBoard deletes an issue board.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/boards/#delete-an-issue-board
 		DeleteIssueBoard(pid any, board int, options ...RequestOptionFunc) (*Response, error)
+
+		// ListIssueBoards gets a list of all issue boards in a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/boards/#list-project-issue-boards
 		ListIssueBoards(pid any, opt *ListIssueBoardsOptions, options ...RequestOptionFunc) ([]*IssueBoard, *Response, error)
+
+		// GetIssueBoard gets a single issue board of a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/boards/#show-a-single-issue-board
 		GetIssueBoard(pid any, board int, options ...RequestOptionFunc) (*IssueBoard, *Response, error)
+
+		// GetIssueBoardLists gets a list of the issue board's lists. Does not include
+		// backlog and closed lists.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/boards/#list-board-lists-in-a-project-issue-board
 		GetIssueBoardLists(pid any, board int, opt *GetIssueBoardListsOptions, options ...RequestOptionFunc) ([]*BoardList, *Response, error)
+
+		// GetIssueBoardList gets a single issue board list.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/boards/#show-a-single-board-list
 		GetIssueBoardList(pid any, board, list int, options ...RequestOptionFunc) (*BoardList, *Response, error)
+
+		// CreateIssueBoardList creates a new issue board list.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/boards/#create-a-board-list
 		CreateIssueBoardList(pid any, board int, opt *CreateIssueBoardListOptions, options ...RequestOptionFunc) (*BoardList, *Response, error)
+
+		// UpdateIssueBoardList updates the position of an existing issue board list.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/boards/#reorder-a-list-in-a-board
 		UpdateIssueBoardList(pid any, board, list int, opt *UpdateIssueBoardListOptions, options ...RequestOptionFunc) (*BoardList, *Response, error)
+
+		// DeleteIssueBoardList soft deletes an issue board list. Only for admins and
+		// project owners.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/boards/#delete-a-board-list-from-a-board
 		DeleteIssueBoardList(pid any, board, list int, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -100,9 +151,6 @@ type CreateIssueBoardOptions struct {
 	Name *string `url:"name,omitempty" json:"name,omitempty"`
 }
 
-// CreateIssueBoard creates a new issue board.
-//
-// GitLab API docs: https://docs.gitlab.com/api/boards/#create-an-issue-board
 func (s *IssueBoardsService) CreateIssueBoard(pid any, opt *CreateIssueBoardOptions, options ...RequestOptionFunc) (*IssueBoard, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -135,9 +183,6 @@ type UpdateIssueBoardOptions struct {
 	Weight      *int          `url:"weight,omitempty" json:"weight,omitempty"`
 }
 
-// UpdateIssueBoard update an issue board.
-//
-// GitLab API docs: https://docs.gitlab.com/api/boards/#update-an-issue-board
 func (s *IssueBoardsService) UpdateIssueBoard(pid any, board int, opt *UpdateIssueBoardOptions, options ...RequestOptionFunc) (*IssueBoard, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -159,9 +204,6 @@ func (s *IssueBoardsService) UpdateIssueBoard(pid any, board int, opt *UpdateIss
 	return is, resp, nil
 }
 
-// DeleteIssueBoard deletes an issue board.
-//
-// GitLab API docs: https://docs.gitlab.com/api/boards/#delete-an-issue-board
 func (s *IssueBoardsService) DeleteIssueBoard(pid any, board int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -182,9 +224,6 @@ func (s *IssueBoardsService) DeleteIssueBoard(pid any, board int, options ...Req
 // GitLab API docs: https://docs.gitlab.com/api/boards/#list-project-issue-boards
 type ListIssueBoardsOptions ListOptions
 
-// ListIssueBoards gets a list of all issue boards in a project.
-//
-// GitLab API docs: https://docs.gitlab.com/api/boards/#list-project-issue-boards
 func (s *IssueBoardsService) ListIssueBoards(pid any, opt *ListIssueBoardsOptions, options ...RequestOptionFunc) ([]*IssueBoard, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -206,9 +245,6 @@ func (s *IssueBoardsService) ListIssueBoards(pid any, opt *ListIssueBoardsOption
 	return is, resp, nil
 }
 
-// GetIssueBoard gets a single issue board of a project.
-//
-// GitLab API docs: https://docs.gitlab.com/api/boards/#show-a-single-issue-board
 func (s *IssueBoardsService) GetIssueBoard(pid any, board int, options ...RequestOptionFunc) (*IssueBoard, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -235,10 +271,6 @@ func (s *IssueBoardsService) GetIssueBoard(pid any, board int, options ...Reques
 // GitLab API docs: https://docs.gitlab.com/api/boards/#list-board-lists-in-a-project-issue-board
 type GetIssueBoardListsOptions ListOptions
 
-// GetIssueBoardLists gets a list of the issue board's lists. Does not include
-// backlog and closed lists.
-//
-// GitLab API docs: https://docs.gitlab.com/api/boards/#list-board-lists-in-a-project-issue-board
 func (s *IssueBoardsService) GetIssueBoardLists(pid any, board int, opt *GetIssueBoardListsOptions, options ...RequestOptionFunc) ([]*BoardList, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -260,9 +292,6 @@ func (s *IssueBoardsService) GetIssueBoardLists(pid any, board int, opt *GetIssu
 	return bl, resp, nil
 }
 
-// GetIssueBoardList gets a single issue board list.
-//
-// GitLab API docs: https://docs.gitlab.com/api/boards/#show-a-single-board-list
 func (s *IssueBoardsService) GetIssueBoardList(pid any, board, list int, options ...RequestOptionFunc) (*BoardList, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -299,9 +328,6 @@ type CreateIssueBoardListOptions struct {
 	IterationID *int `url:"iteration_id,omitempty" json:"iteration_id,omitempty"`
 }
 
-// CreateIssueBoardList creates a new issue board list.
-//
-// GitLab API docs: https://docs.gitlab.com/api/boards/#create-a-board-list
 func (s *IssueBoardsService) CreateIssueBoardList(pid any, board int, opt *CreateIssueBoardListOptions, options ...RequestOptionFunc) (*BoardList, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -331,9 +357,6 @@ type UpdateIssueBoardListOptions struct {
 	Position *int `url:"position" json:"position"`
 }
 
-// UpdateIssueBoardList updates the position of an existing issue board list.
-//
-// GitLab API docs: https://docs.gitlab.com/api/boards/#reorder-a-list-in-a-board
 func (s *IssueBoardsService) UpdateIssueBoardList(pid any, board, list int, opt *UpdateIssueBoardListOptions, options ...RequestOptionFunc) (*BoardList, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -359,11 +382,6 @@ func (s *IssueBoardsService) UpdateIssueBoardList(pid any, board, list int, opt
 	return bl, resp, nil
 }
 
-// DeleteIssueBoardList soft deletes an issue board list. Only for admins and
-// project owners.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/boards/#delete-a-board-list-from-a-board
 func (s *IssueBoardsService) DeleteIssueBoardList(pid any, board, list int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/branches.go b/vendor/gitlab.com/gitlab-org/api/client-go/branches.go
index 1c753b17f..340d267ff 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/branches.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/branches.go
@@ -24,10 +24,34 @@ import (
 
 type (
 	BranchesServiceInterface interface {
+		// ListBranches gets a list of repository branches from a project, sorted by name alphabetically.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/branches/#list-repository-branches
 		ListBranches(pid any, opts *ListBranchesOptions, options ...RequestOptionFunc) ([]*Branch, *Response, error)
+
+		// GetBranch gets a single project repository branch.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/branches/#get-single-repository-branch
 		GetBranch(pid any, branch string, options ...RequestOptionFunc) (*Branch, *Response, error)
+
+		// CreateBranch creates branch from commit SHA or existing branch.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/branches/#create-repository-branch
 		CreateBranch(pid any, opt *CreateBranchOptions, options ...RequestOptionFunc) (*Branch, *Response, error)
+
+		// DeleteBranch deletes an existing branch.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/branches/#delete-repository-branch
 		DeleteBranch(pid any, branch string, options ...RequestOptionFunc) (*Response, error)
+
+		// DeleteMergedBranches deletes all branches that are merged into the project's default branch.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/branches/#delete-merged-branches
 		DeleteMergedBranches(pid any, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -71,11 +95,6 @@ type ListBranchesOptions struct {
 	Regex  *string `url:"regex,omitempty" json:"regex,omitempty"`
 }
 
-// ListBranches gets a list of repository branches from a project, sorted by
-// name alphabetically.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/branches/#list-repository-branches
 func (s *BranchesService) ListBranches(pid any, opts *ListBranchesOptions, options ...RequestOptionFunc) ([]*Branch, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -97,10 +116,6 @@ func (s *BranchesService) ListBranches(pid any, opts *ListBranchesOptions, optio
 	return b, resp, nil
 }
 
-// GetBranch gets a single project repository branch.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/branches/#get-single-repository-branch
 func (s *BranchesService) GetBranch(pid any, branch string, options ...RequestOptionFunc) (*Branch, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -131,10 +146,6 @@ type CreateBranchOptions struct {
 	Ref    *string `url:"ref,omitempty" json:"ref,omitempty"`
 }
 
-// CreateBranch creates branch from commit SHA or existing branch.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/branches/#create-repository-branch
 func (s *BranchesService) CreateBranch(pid any, opt *CreateBranchOptions, options ...RequestOptionFunc) (*Branch, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -156,10 +167,6 @@ func (s *BranchesService) CreateBranch(pid any, opt *CreateBranchOptions, option
 	return b, resp, nil
 }
 
-// DeleteBranch deletes an existing branch.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/branches/#delete-repository-branch
 func (s *BranchesService) DeleteBranch(pid any, branch string, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -175,10 +182,6 @@ func (s *BranchesService) DeleteBranch(pid any, branch string, options ...Reques
 	return s.client.Do(req, nil)
 }
 
-// DeleteMergedBranches deletes all branches that are merged into the project's default branch.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/branches/#delete-merged-branches
 func (s *BranchesService) DeleteMergedBranches(pid any, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/broadcast_messages.go b/vendor/gitlab.com/gitlab-org/api/client-go/broadcast_messages.go
index 2e4220086..920a48884 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/broadcast_messages.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/broadcast_messages.go
@@ -24,10 +24,34 @@ import (
 
 type (
 	BroadcastMessagesServiceInterface interface {
+		// ListBroadcastMessages gets a list of all broadcasted messages.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/broadcast_messages/#get-all-broadcast-messages
 		ListBroadcastMessages(opt *ListBroadcastMessagesOptions, options ...RequestOptionFunc) ([]*BroadcastMessage, *Response, error)
+
+		// GetBroadcastMessage gets a single broadcast message.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/broadcast_messages/#get-a-specific-broadcast-message
 		GetBroadcastMessage(broadcast int, options ...RequestOptionFunc) (*BroadcastMessage, *Response, error)
+
+		// CreateBroadcastMessage creates a message to broadcast.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/broadcast_messages/#create-a-broadcast-message
 		CreateBroadcastMessage(opt *CreateBroadcastMessageOptions, options ...RequestOptionFunc) (*BroadcastMessage, *Response, error)
+
+		// UpdateBroadcastMessage updates a broadcasted message.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/broadcast_messages/#update-a-broadcast-message
 		UpdateBroadcastMessage(broadcast int, opt *UpdateBroadcastMessageOptions, options ...RequestOptionFunc) (*BroadcastMessage, *Response, error)
+
+		// DeleteBroadcastMessage deletes a broadcasted message.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/broadcast_messages/#delete-a-broadcast-message
 		DeleteBroadcastMessage(broadcast int, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -67,10 +91,6 @@ type BroadcastMessage struct {
 // https://docs.gitlab.com/api/broadcast_messages/#get-all-broadcast-messages
 type ListBroadcastMessagesOptions ListOptions
 
-// ListBroadcastMessages gets a list of all broadcasted messages.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/broadcast_messages/#get-all-broadcast-messages
 func (s *BroadcastMessagesService) ListBroadcastMessages(opt *ListBroadcastMessagesOptions, options ...RequestOptionFunc) ([]*BroadcastMessage, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodGet, "broadcast_messages", opt, options)
 	if err != nil {
@@ -86,10 +106,6 @@ func (s *BroadcastMessagesService) ListBroadcastMessages(opt *ListBroadcastMessa
 	return bs, resp, nil
 }
 
-// GetBroadcastMessage gets a single broadcast message.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/broadcast_messages/#get-a-specific-broadcast-message
 func (s *BroadcastMessagesService) GetBroadcastMessage(broadcast int, options ...RequestOptionFunc) (*BroadcastMessage, *Response, error) {
 	u := fmt.Sprintf("broadcast_messages/%d", broadcast)
 
@@ -124,10 +140,6 @@ type CreateBroadcastMessageOptions struct {
 	Theme              *string            `url:"theme,omitempty" json:"theme,omitempty"`
 }
 
-// CreateBroadcastMessage creates a message to broadcast.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/broadcast_messages/#create-a-broadcast-message
 func (s *BroadcastMessagesService) CreateBroadcastMessage(opt *CreateBroadcastMessageOptions, options ...RequestOptionFunc) (*BroadcastMessage, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodPost, "broadcast_messages", opt, options)
 	if err != nil {
@@ -160,10 +172,6 @@ type UpdateBroadcastMessageOptions struct {
 	Theme              *string            `url:"theme,omitempty" json:"theme,omitempty"`
 }
 
-// UpdateBroadcastMessage update a broadcasted message.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/broadcast_messages/#update-a-broadcast-message
 func (s *BroadcastMessagesService) UpdateBroadcastMessage(broadcast int, opt *UpdateBroadcastMessageOptions, options ...RequestOptionFunc) (*BroadcastMessage, *Response, error) {
 	u := fmt.Sprintf("broadcast_messages/%d", broadcast)
 
@@ -181,10 +189,6 @@ func (s *BroadcastMessagesService) UpdateBroadcastMessage(broadcast int, opt *Up
 	return b, resp, nil
 }
 
-// DeleteBroadcastMessage deletes a broadcasted message.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/broadcast_messages/#delete-a-broadcast-message
 func (s *BroadcastMessagesService) DeleteBroadcastMessage(broadcast int, options ...RequestOptionFunc) (*Response, error) {
 	u := fmt.Sprintf("broadcast_messages/%d", broadcast)
 
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/buf.gen.yaml b/vendor/gitlab.com/gitlab-org/api/client-go/buf.gen.yaml
new file mode 100644
index 000000000..dd0e5916d
--- /dev/null
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/buf.gen.yaml
@@ -0,0 +1,6 @@
+version: v2
+plugins:
+  - local: ["go", "run", "google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.10"]
+    out: .
+    opt:
+      - paths=source_relative
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/buf.lock b/vendor/gitlab.com/gitlab-org/api/client-go/buf.lock
new file mode 100644
index 000000000..2327d76b5
--- /dev/null
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/buf.lock
@@ -0,0 +1,6 @@
+# Generated by buf. DO NOT EDIT.
+version: v2
+deps:
+  - name: buf.build/bufbuild/protovalidate
+    commit: d22d418d82d84932ba4ba554ce4208ca
+    digest: b5:8b454cb411e14f0024bdb58ac2689aed2321e81617b61ef8a22c8147a7a9757eeb049ebfb899a699d37051bdb3251e172ba1c1ae2f5005d7c3f47a202109136a
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/buf.yaml b/vendor/gitlab.com/gitlab-org/api/client-go/buf.yaml
new file mode 100644
index 000000000..b1c4db332
--- /dev/null
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/buf.yaml
@@ -0,0 +1,12 @@
+version: v2
+
+modules:
+  - path: .
+    excludes:
+      # these are mostly cache directories in CI
+      - .go
+      - .golangci-lint
+      - .mise
+
+deps:
+  - buf.build/bufbuild/protovalidate:v0.14.0
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/bulk_imports.go b/vendor/gitlab.com/gitlab-org/api/client-go/bulk_imports.go
index 70464498e..73ac24341 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/bulk_imports.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/bulk_imports.go
@@ -61,9 +61,6 @@ type BulkImportStartMigrationResponse struct {
 	HasFailures bool      `json:"has_failures"`
 }
 
-// StartMigration starts a migration.
-//
-// GitLab API docs: https://docs.gitlab.com/api/bulk_imports/#start-a-new-group-or-project-migration
 func (b *BulkImportsService) StartMigration(startMigrationOptions *BulkImportStartMigrationOptions, options ...RequestOptionFunc) (*BulkImportStartMigrationResponse, *Response, error) {
 	request, err := b.client.NewRequest(http.MethodPost, "bulk_imports", startMigrationOptions, options)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/ci_yml_templates.go b/vendor/gitlab.com/gitlab-org/api/client-go/ci_yml_templates.go
index 168ca1fc4..f29df826d 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/ci_yml_templates.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/ci_yml_templates.go
@@ -23,7 +23,16 @@ import (
 
 type (
 	CIYMLTemplatesServiceInterface interface {
+		// ListAllTemplates get all GitLab CI YML templates.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/templates/gitlab_ci_ymls/#list-gitlab-ci-yaml-templates
 		ListAllTemplates(opt *ListCIYMLTemplatesOptions, options ...RequestOptionFunc) ([]*CIYMLTemplateListItem, *Response, error)
+
+		// GetTemplate get a single GitLab CI YML template.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/templates/gitlab_ci_ymls/#single-gitlab-ci-yaml-template
 		GetTemplate(key string, options ...RequestOptionFunc) (*CIYMLTemplate, *Response, error)
 	}
 
@@ -63,10 +72,6 @@ type CIYMLTemplateListItem struct {
 // https://docs.gitlab.com/api/templates/gitlab_ci_ymls/#list-gitlab-ci-yaml-templates
 type ListCIYMLTemplatesOptions ListOptions
 
-// ListAllTemplates get all GitLab CI YML templates.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/templates/gitlab_ci_ymls/#list-gitlab-ci-yaml-templates
 func (s *CIYMLTemplatesService) ListAllTemplates(opt *ListCIYMLTemplatesOptions, options ...RequestOptionFunc) ([]*CIYMLTemplateListItem, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodGet, "templates/gitlab_ci_ymls", opt, options)
 	if err != nil {
@@ -82,10 +87,6 @@ func (s *CIYMLTemplatesService) ListAllTemplates(opt *ListCIYMLTemplatesOptions,
 	return cts, resp, nil
 }
 
-// GetTemplate get a single GitLab CI YML template.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/templates/gitlab_ci_ymls/#single-gitlab-ci-yaml-template
 func (s *CIYMLTemplatesService) GetTemplate(key string, options ...RequestOptionFunc) (*CIYMLTemplate, *Response, error) {
 	u := fmt.Sprintf("templates/gitlab_ci_ymls/%s", PathEscape(key))
 
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/client_options.go b/vendor/gitlab.com/gitlab-org/api/client-go/client_options.go
index 2ff7bab9b..57a7f2024 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/client_options.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/client_options.go
@@ -17,6 +17,7 @@
 package gitlab
 
 import (
+	"fmt"
 	"net/http"
 	"time"
 
@@ -140,3 +141,31 @@ func WithRequestOptions(options ...RequestOptionFunc) ClientOptionFunc {
 		return nil
 	}
 }
+
+// WithUserAgent can be used to configure a custom user agent.
+func WithUserAgent(userAgent string) ClientOptionFunc {
+	return func(c *Client) error {
+		c.UserAgent = userAgent
+		return nil
+	}
+}
+
+// WithCookieJar can be used to configure a cookie jar.
+func WithCookieJar(jar http.CookieJar) ClientOptionFunc {
+	return func(c *Client) error {
+		c.jar = jar
+		return nil
+	}
+}
+
+// WithInterceptor registers an Interceptor in the client's http request call pipeline.
+// It returns a ClientOptionFunc that adds the interceptor to the client.
+func WithInterceptor(i Interceptor) ClientOptionFunc {
+	return func(c *Client) error {
+		if i == nil {
+			return fmt.Errorf("interceptor cannot be nil")
+		}
+		c.interceptors = append(c.interceptors, i)
+		return nil
+	}
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/cluster_agents.go b/vendor/gitlab.com/gitlab-org/api/client-go/cluster_agents.go
index 88855174d..20ba1f158 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/cluster_agents.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/cluster_agents.go
@@ -24,13 +24,52 @@ import (
 
 type (
 	ClusterAgentsServiceInterface interface {
+		// ListAgents returns a list of agents registered for the project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/cluster_agents/#list-the-agents-for-a-project
 		ListAgents(pid any, opt *ListAgentsOptions, options ...RequestOptionFunc) ([]*Agent, *Response, error)
+
+		// GetAgent gets a single agent details.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/cluster_agents/#get-details-about-an-agent
 		GetAgent(pid any, id int, options ...RequestOptionFunc) (*Agent, *Response, error)
+
+		// RegisterAgent registers an agent to the project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/cluster_agents/#register-an-agent-with-a-project
 		RegisterAgent(pid any, opt *RegisterAgentOptions, options ...RequestOptionFunc) (*Agent, *Response, error)
+
+		// DeleteAgent deletes an existing agent registration.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/cluster_agents/#delete-a-registered-agent
 		DeleteAgent(pid any, id int, options ...RequestOptionFunc) (*Response, error)
+
+		// ListAgentTokens returns a list of tokens for an agent.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/cluster_agents/#list-tokens-for-an-agent
 		ListAgentTokens(pid any, aid int, opt *ListAgentTokensOptions, options ...RequestOptionFunc) ([]*AgentToken, *Response, error)
+
+		// GetAgentToken gets a single agent token.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/cluster_agents/#get-a-single-agent-token
 		GetAgentToken(pid any, aid int, id int, options ...RequestOptionFunc) (*AgentToken, *Response, error)
+
+		// CreateAgentToken creates a new token for an agent.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/cluster_agents/#create-an-agent-token
 		CreateAgentToken(pid any, aid int, opt *CreateAgentTokenOptions, options ...RequestOptionFunc) (*AgentToken, *Response, error)
+
+		// RevokeAgentToken revokes an agent token.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/cluster_agents/#revoke-an-agent-token
 		RevokeAgentToken(pid any, aid int, id int, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -96,54 +135,29 @@ func (a AgentToken) String() string {
 // https://docs.gitlab.com/api/cluster_agents/#list-the-agents-for-a-project
 type ListAgentsOptions ListOptions
 
-// ListAgents returns a list of agents registered for the project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/cluster_agents/#list-the-agents-for-a-project
 func (s *ClusterAgentsService) ListAgents(pid any, opt *ListAgentsOptions, options ...RequestOptionFunc) ([]*Agent, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
 		return nil, nil, err
 	}
-	uri := fmt.Sprintf("projects/%s/cluster_agents", PathEscape(project))
-
-	req, err := s.client.NewRequest(http.MethodGet, uri, opt, options)
-	if err != nil {
-		return nil, nil, err
-	}
 
-	var as []*Agent
-	resp, err := s.client.Do(req, &as)
-	if err != nil {
-		return nil, resp, err
-	}
-
-	return as, resp, nil
+	return do[[]*Agent](s.client,
+		withPath("projects/%s/cluster_agents", project),
+		withAPIOpts(opt),
+		withRequestOpts(options...),
+	)
 }
 
-// GetAgent gets a single agent details.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/cluster_agents/#get-details-about-an-agent
 func (s *ClusterAgentsService) GetAgent(pid any, id int, options ...RequestOptionFunc) (*Agent, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
 		return nil, nil, err
 	}
-	uri := fmt.Sprintf("projects/%s/cluster_agents/%d", PathEscape(project), id)
-
-	req, err := s.client.NewRequest(http.MethodGet, uri, nil, options)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	a := new(Agent)
-	resp, err := s.client.Do(req, a)
-	if err != nil {
-		return nil, resp, err
-	}
 
-	return a, resp, nil
+	return do[*Agent](s.client,
+		withPath("projects/%s/cluster_agents/%d", project, id),
+		withRequestOpts(options...),
+	)
 }
 
 // RegisterAgentOptions represents the available RegisterAgent()
@@ -155,48 +169,32 @@ type RegisterAgentOptions struct {
 	Name *string `url:"name,omitempty" json:"name,omitempty"`
 }
 
-// RegisterAgent registers an agent to the project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/cluster_agents/#register-an-agent-with-a-project
 func (s *ClusterAgentsService) RegisterAgent(pid any, opt *RegisterAgentOptions, options ...RequestOptionFunc) (*Agent, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
 		return nil, nil, err
 	}
-	uri := fmt.Sprintf("projects/%s/cluster_agents", PathEscape(project))
-
-	req, err := s.client.NewRequest(http.MethodPost, uri, opt, options)
-	if err != nil {
-		return nil, nil, err
-	}
 
-	a := new(Agent)
-	resp, err := s.client.Do(req, a)
-	if err != nil {
-		return nil, resp, err
-	}
-
-	return a, resp, nil
+	return do[*Agent](s.client,
+		withMethod(http.MethodPost),
+		withPath("projects/%s/cluster_agents", project),
+		withAPIOpts(opt),
+		withRequestOpts(options...),
+	)
 }
 
-// DeleteAgent deletes an existing agent registration.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/cluster_agents/#delete-a-registered-agent
 func (s *ClusterAgentsService) DeleteAgent(pid any, id int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
 		return nil, err
 	}
-	uri := fmt.Sprintf("projects/%s/cluster_agents/%d", PathEscape(project), id)
-
-	req, err := s.client.NewRequest(http.MethodDelete, uri, nil, options)
-	if err != nil {
-		return nil, err
-	}
 
-	return s.client.Do(req, nil)
+	_, resp, err := do[none](s.client,
+		withMethod(http.MethodDelete),
+		withPath("projects/%s/cluster_agents/%d", project, id),
+		withRequestOpts(options...),
+	)
+	return resp, err
 }
 
 // ListAgentTokensOptions represents the available ListAgentTokens() options.
@@ -205,10 +203,6 @@ func (s *ClusterAgentsService) DeleteAgent(pid any, id int, options ...RequestOp
 // https://docs.gitlab.com/api/cluster_agents/#list-tokens-for-an-agent
 type ListAgentTokensOptions ListOptions
 
-// ListAgentTokens returns a list of tokens for an agent.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/cluster_agents/#list-tokens-for-an-agent
 func (s *ClusterAgentsService) ListAgentTokens(pid any, aid int, opt *ListAgentTokensOptions, options ...RequestOptionFunc) ([]*AgentToken, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -230,10 +224,6 @@ func (s *ClusterAgentsService) ListAgentTokens(pid any, aid int, opt *ListAgentT
 	return ats, resp, nil
 }
 
-// GetAgentToken gets a single agent token.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/cluster_agents/#get-a-single-agent-token
 func (s *ClusterAgentsService) GetAgentToken(pid any, aid int, id int, options ...RequestOptionFunc) (*AgentToken, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -264,10 +254,6 @@ type CreateAgentTokenOptions struct {
 	Description *string `url:"description,omitempty" json:"description,omitempty"`
 }
 
-// CreateAgentToken creates a new token for an agent.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/cluster_agents/#create-an-agent-token
 func (s *ClusterAgentsService) CreateAgentToken(pid any, aid int, opt *CreateAgentTokenOptions, options ...RequestOptionFunc) (*AgentToken, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -289,10 +275,6 @@ func (s *ClusterAgentsService) CreateAgentToken(pid any, aid int, opt *CreateAge
 	return at, resp, nil
 }
 
-// RevokeAgentToken revokes an agent token.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/cluster_agents/#revoke-an-agent-token
 func (s *ClusterAgentsService) RevokeAgentToken(pid any, aid int, id int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/commitlint.config.mjs b/vendor/gitlab.com/gitlab-org/api/client-go/commitlint.config.mjs
deleted file mode 100644
index 2291173ef..000000000
--- a/vendor/gitlab.com/gitlab-org/api/client-go/commitlint.config.mjs
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
-  extends: ['@commitlint/config-conventional']
-};
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/commits.go b/vendor/gitlab.com/gitlab-org/api/client-go/commits.go
index d73031f05..112ace3e3 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/commits.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/commits.go
@@ -29,18 +29,85 @@ import (
 // GitLab API docs: https://docs.gitlab.com/api/commits/
 type (
 	CommitsServiceInterface interface {
+		// ListCommits gets a list of repository commits in a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/commits/#list-repository-commits
 		ListCommits(pid any, opt *ListCommitsOptions, options ...RequestOptionFunc) ([]*Commit, *Response, error)
+
+		// GetCommitRefs gets all references (from branches or tags) a commit is pushed to.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/commits/#get-references-a-commit-is-pushed-to
 		GetCommitRefs(pid any, sha string, opt *GetCommitRefsOptions, options ...RequestOptionFunc) ([]*CommitRef, *Response, error)
+
+		// GetCommit gets a specific commit identified by the commit hash or name of a
+		// branch or tag.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/commits/#get-a-single-commit
 		GetCommit(pid any, sha string, opt *GetCommitOptions, options ...RequestOptionFunc) (*Commit, *Response, error)
+
+		// CreateCommit creates a commit with multiple files and actions.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/commits/#create-a-commit-with-multiple-files-and-actions
 		CreateCommit(pid any, opt *CreateCommitOptions, options ...RequestOptionFunc) (*Commit, *Response, error)
+
+		// GetCommitDiff gets the diff of a commit in a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/commits/#get-the-diff-of-a-commit
 		GetCommitDiff(pid any, sha string, opt *GetCommitDiffOptions, options ...RequestOptionFunc) ([]*Diff, *Response, error)
+
+		// GetCommitComments gets the comments of a commit in a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/commits/#get-the-comments-of-a-commit
 		GetCommitComments(pid any, sha string, opt *GetCommitCommentsOptions, options ...RequestOptionFunc) ([]*CommitComment, *Response, error)
+
+		// PostCommitComment adds a comment to a commit. Optionally you can post
+		// comments on a specific line of a commit. Therefore both path, line_new and
+		// line_old are required.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/commits/#post-comment-to-commit
 		PostCommitComment(pid any, sha string, opt *PostCommitCommentOptions, options ...RequestOptionFunc) (*CommitComment, *Response, error)
+
+		// GetCommitStatuses gets the statuses of a commit in a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/commits/#list-the-statuses-of-a-commit
 		GetCommitStatuses(pid any, sha string, opt *GetCommitStatusesOptions, options ...RequestOptionFunc) ([]*CommitStatus, *Response, error)
+
+		// SetCommitStatus sets the status of a commit in a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/commits/#set-the-pipeline-status-of-a-commit
 		SetCommitStatus(pid any, sha string, opt *SetCommitStatusOptions, options ...RequestOptionFunc) (*CommitStatus, *Response, error)
+
+		// ListMergeRequestsByCommit gets merge request associated with a commit.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/commits/#list-merge-requests-associated-with-a-commit
 		ListMergeRequestsByCommit(pid any, sha string, options ...RequestOptionFunc) ([]*BasicMergeRequest, *Response, error)
+
+		// CherryPickCommit cherry picks a commit to a given branch.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/commits/#cherry-pick-a-commit
 		CherryPickCommit(pid any, sha string, opt *CherryPickCommitOptions, options ...RequestOptionFunc) (*Commit, *Response, error)
+
+		// RevertCommit reverts a commit in a given branch.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/commits/#revert-a-commit
 		RevertCommit(pid any, sha string, opt *RevertCommitOptions, options ...RequestOptionFunc) (*Commit, *Response, error)
+
+		// GetGPGSignature gets a GPG signature of a commit.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/commits/#get-signature-of-a-commit
 		GetGPGSignature(pid any, sha string, options ...RequestOptionFunc) (*GPGSignature, *Response, error)
 	}
 
@@ -109,9 +176,6 @@ type ListCommitsOptions struct {
 	Trailers    *bool      `url:"trailers,omitempty" json:"trailers,omitempty"`
 }
 
-// ListCommits gets a list of repository commits in a project.
-//
-// GitLab API docs: https://docs.gitlab.com/api/commits/#list-repository-commits
 func (s *CommitsService) ListCommits(pid any, opt *ListCommitsOptions, options ...RequestOptionFunc) ([]*Commit, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -151,10 +215,6 @@ type GetCommitRefsOptions struct {
 	Type *string `url:"type,omitempty" json:"type,omitempty"`
 }
 
-// GetCommitRefs gets all references (from branches or tags) a commit is pushed to
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/commits/#get-references-a-commit-is-pushed-to
 func (s *CommitsService) GetCommitRefs(pid any, sha string, opt *GetCommitRefsOptions, options ...RequestOptionFunc) ([]*CommitRef, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -184,10 +244,6 @@ type GetCommitOptions struct {
 	Stats *bool `url:"stats,omitempty" json:"stats,omitempty"`
 }
 
-// GetCommit gets a specific commit identified by the commit hash or name of a
-// branch or tag.
-//
-// GitLab API docs: https://docs.gitlab.com/api/commits/#get-a-single-commit
 func (s *CommitsService) GetCommit(pid any, sha string, opt *GetCommitOptions, options ...RequestOptionFunc) (*Commit, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -242,9 +298,6 @@ type CommitActionOptions struct {
 	ExecuteFilemode *bool            `url:"execute_filemode,omitempty" json:"execute_filemode,omitempty"`
 }
 
-// CreateCommit creates a commit with multiple files and actions.
-//
-// GitLab API docs: https://docs.gitlab.com/api/commits/#create-a-commit-with-multiple-files-and-actions
 func (s *CommitsService) CreateCommit(pid any, opt *CreateCommitOptions, options ...RequestOptionFunc) (*Commit, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -293,10 +346,6 @@ type GetCommitDiffOptions struct {
 	Unidiff *bool `url:"unidiff,omitempty" json:"unidiff,omitempty"`
 }
 
-// GetCommitDiff gets the diff of a commit in a project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/commits/#get-the-diff-of-a-commit
 func (s *CommitsService) GetCommitDiff(pid any, sha string, opt *GetCommitDiffOptions, options ...RequestOptionFunc) ([]*Diff, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -350,10 +399,6 @@ func (c CommitComment) String() string {
 // https://docs.gitlab.com/api/commits/#get-the-comments-of-a-commit
 type GetCommitCommentsOptions ListOptions
 
-// GetCommitComments gets the comments of a commit in a project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/commits/#get-the-comments-of-a-commit
 func (s *CommitsService) GetCommitComments(pid any, sha string, opt *GetCommitCommentsOptions, options ...RequestOptionFunc) ([]*CommitComment, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -387,12 +432,6 @@ type PostCommitCommentOptions struct {
 	LineType *string `url:"line_type" json:"line_type"`
 }
 
-// PostCommitComment adds a comment to a commit. Optionally you can post
-// comments on a specific line of a commit. Therefor both path, line_new and
-// line_old are required.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/commits/#post-comment-to-commit
 func (s *CommitsService) PostCommitComment(pid any, sha string, opt *PostCommitCommentOptions, options ...RequestOptionFunc) (*CommitComment, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -419,10 +458,11 @@ func (s *CommitsService) PostCommitComment(pid any, sha string, opt *PostCommitC
 // GitLab API docs: https://docs.gitlab.com/api/commits/#list-the-statuses-of-a-commit
 type GetCommitStatusesOptions struct {
 	ListOptions
-	Ref   *string `url:"ref,omitempty" json:"ref,omitempty"`
-	Stage *string `url:"stage,omitempty" json:"stage,omitempty"`
-	Name  *string `url:"name,omitempty" json:"name,omitempty"`
-	All   *bool   `url:"all,omitempty" json:"all,omitempty"`
+	Ref        *string `url:"ref,omitempty" json:"ref,omitempty"`
+	Stage      *string `url:"stage,omitempty" json:"stage,omitempty"`
+	Name       *string `url:"name,omitempty" json:"name,omitempty"`
+	PipelineID *int    `url:"pipeline_id,omitempty" json:"pipeline_id,omitempty"`
+	All        *bool   `url:"all,omitempty" json:"all,omitempty"`
 }
 
 // CommitStatus represents a GitLab commit status.
@@ -445,9 +485,6 @@ type CommitStatus struct {
 	TargetURL    string     `json:"target_url"`
 }
 
-// GetCommitStatuses gets the statuses of a commit in a project.
-//
-// GitLab API docs: https://docs.gitlab.com/api/commits/#list-the-statuses-of-a-commit
 func (s *CommitsService) GetCommitStatuses(pid any, sha string, opt *GetCommitStatusesOptions, options ...RequestOptionFunc) ([]*CommitStatus, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -483,9 +520,6 @@ type SetCommitStatusOptions struct {
 	PipelineID  *int            `url:"pipeline_id,omitempty" json:"pipeline_id,omitempty"`
 }
 
-// SetCommitStatus sets the status of a commit in a project.
-//
-// GitLab API docs: https://docs.gitlab.com/api/commits/#set-the-pipeline-status-of-a-commit
 func (s *CommitsService) SetCommitStatus(pid any, sha string, opt *SetCommitStatusOptions, options ...RequestOptionFunc) (*CommitStatus, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -507,10 +541,6 @@ func (s *CommitsService) SetCommitStatus(pid any, sha string, opt *SetCommitStat
 	return cs, resp, nil
 }
 
-// ListMergeRequestsByCommit gets merge request associated with a commit.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/commits/#list-merge-requests-associated-with-a-commit
 func (s *CommitsService) ListMergeRequestsByCommit(pid any, sha string, options ...RequestOptionFunc) ([]*BasicMergeRequest, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -541,9 +571,6 @@ type CherryPickCommitOptions struct {
 	Message *string `url:"message,omitempty" json:"message,omitempty"`
 }
 
-// CherryPickCommit cherry picks a commit to a given branch.
-//
-// GitLab API docs: https://docs.gitlab.com/api/commits/#cherry-pick-a-commit
 func (s *CommitsService) CherryPickCommit(pid any, sha string, opt *CherryPickCommitOptions, options ...RequestOptionFunc) (*Commit, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -572,9 +599,6 @@ type RevertCommitOptions struct {
 	Branch *string `url:"branch,omitempty" json:"branch,omitempty"`
 }
 
-// RevertCommit reverts a commit in a given branch.
-//
-// GitLab API docs: https://docs.gitlab.com/api/commits/#revert-a-commit
 func (s *CommitsService) RevertCommit(pid any, sha string, opt *RevertCommitOptions, options ...RequestOptionFunc) (*Commit, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -596,7 +620,7 @@ func (s *CommitsService) RevertCommit(pid any, sha string, opt *RevertCommitOpti
 	return c, resp, nil
 }
 
-// GPGSignature represents a Gitlab commit's GPG Signature.
+// GPGSignature represents a GitLab commit's GPG Signature.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/commits/#get-signature-of-a-commit
@@ -609,9 +633,6 @@ type GPGSignature struct {
 	KeySubkeyID        int    `json:"gpg_key_subkey_id"`
 }
 
-// GetGPGSignature gets a GPG signature of a commit.
-//
-// GitLab API docs: https://docs.gitlab.com/api/commits/#get-signature-of-a-commit
 func (s *CommitsService) GetGPGSignature(pid any, sha string, options ...RequestOptionFunc) (*GPGSignature, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/container_registry.go b/vendor/gitlab.com/gitlab-org/api/client-go/container_registry.go
index 4e473ee0e..3eb727ff5 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/container_registry.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/container_registry.go
@@ -24,13 +24,52 @@ import (
 
 type (
 	ContainerRegistryServiceInterface interface {
+		// ListProjectRegistryRepositories gets a list of registry repositories in a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/container_registry/#within-a-project
 		ListProjectRegistryRepositories(pid any, opt *ListRegistryRepositoriesOptions, options ...RequestOptionFunc) ([]*RegistryRepository, *Response, error)
+
+		// ListGroupRegistryRepositories gets a list of registry repositories in a group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/container_registry/#within-a-group
 		ListGroupRegistryRepositories(gid any, opt *ListRegistryRepositoriesOptions, options ...RequestOptionFunc) ([]*RegistryRepository, *Response, error)
+
+		// GetSingleRegistryRepository gets the details of single registry repository.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/container_registry/#get-details-of-a-single-repository
 		GetSingleRegistryRepository(pid any, opt *GetSingleRegistryRepositoryOptions, options ...RequestOptionFunc) (*RegistryRepository, *Response, error)
+
+		// DeleteRegistryRepository deletes a repository in a registry.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/container_registry/#delete-registry-repository
 		DeleteRegistryRepository(pid any, repository int, options ...RequestOptionFunc) (*Response, error)
+
+		// ListRegistryRepositoryTags gets a list of tags for given registry repository.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/container_registry/#list-registry-repository-tags
 		ListRegistryRepositoryTags(pid any, repository int, opt *ListRegistryRepositoryTagsOptions, options ...RequestOptionFunc) ([]*RegistryRepositoryTag, *Response, error)
+
+		// GetRegistryRepositoryTagDetail get details of a registry repository tag.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/container_registry/#get-details-of-a-registry-repository-tag
 		GetRegistryRepositoryTagDetail(pid any, repository int, tagName string, options ...RequestOptionFunc) (*RegistryRepositoryTag, *Response, error)
+
+		// DeleteRegistryRepositoryTag deletes a registry repository tag.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/container_registry/#delete-a-registry-repository-tag
 		DeleteRegistryRepositoryTag(pid any, repository int, tagName string, options ...RequestOptionFunc) (*Response, error)
+
+		// DeleteRegistryRepositoryTags deletes repository tags in bulk based on given criteria.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/container_registry/#delete-registry-repository-tags-in-bulk
 		DeleteRegistryRepositoryTags(pid any, repository int, opt *DeleteRegistryRepositoryTagsOptions, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -96,10 +135,6 @@ type ListRegistryRepositoriesOptions struct {
 	TagsCount *bool `url:"tags_count,omitempty" json:"tags_count,omitempty"`
 }
 
-// ListProjectRegistryRepositories gets a list of registry repositories in a project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/container_registry/#within-a-project
 func (s *ContainerRegistryService) ListProjectRegistryRepositories(pid any, opt *ListRegistryRepositoriesOptions, options ...RequestOptionFunc) ([]*RegistryRepository, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -121,10 +156,6 @@ func (s *ContainerRegistryService) ListProjectRegistryRepositories(pid any, opt
 	return repos, resp, nil
 }
 
-// ListGroupRegistryRepositories gets a list of registry repositories in a group.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/container_registry/#within-a-group
 func (s *ContainerRegistryService) ListGroupRegistryRepositories(gid any, opt *ListRegistryRepositoriesOptions, options ...RequestOptionFunc) ([]*RegistryRepository, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -156,10 +187,6 @@ type GetSingleRegistryRepositoryOptions struct {
 	TagsCount *bool `url:"tags_count,omitempty" json:"tags_count,omitempty"`
 }
 
-// GetSingleRegistryRepository gets the details of single registry repository.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/container_registry/#get-details-of-a-single-repository
 func (s *ContainerRegistryService) GetSingleRegistryRepository(pid any, opt *GetSingleRegistryRepositoryOptions, options ...RequestOptionFunc) (*RegistryRepository, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -181,10 +208,6 @@ func (s *ContainerRegistryService) GetSingleRegistryRepository(pid any, opt *Get
 	return repo, resp, nil
 }
 
-// DeleteRegistryRepository deletes a repository in a registry.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/container_registry/#delete-registry-repository
 func (s *ContainerRegistryService) DeleteRegistryRepository(pid any, repository int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -207,10 +230,6 @@ func (s *ContainerRegistryService) DeleteRegistryRepository(pid any, repository
 // https://docs.gitlab.com/api/container_registry/#list-registry-repository-tags
 type ListRegistryRepositoryTagsOptions ListOptions
 
-// ListRegistryRepositoryTags gets a list of tags for given registry repository.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/container_registry/#list-registry-repository-tags
 func (s *ContainerRegistryService) ListRegistryRepositoryTags(pid any, repository int, opt *ListRegistryRepositoryTagsOptions, options ...RequestOptionFunc) ([]*RegistryRepositoryTag, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -235,10 +254,6 @@ func (s *ContainerRegistryService) ListRegistryRepositoryTags(pid any, repositor
 	return tags, resp, nil
 }
 
-// GetRegistryRepositoryTagDetail get details of a registry repository tag
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/container_registry/#get-details-of-a-registry-repository-tag
 func (s *ContainerRegistryService) GetRegistryRepositoryTagDetail(pid any, repository int, tagName string, options ...RequestOptionFunc) (*RegistryRepositoryTag, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -264,10 +279,6 @@ func (s *ContainerRegistryService) GetRegistryRepositoryTagDetail(pid any, repos
 	return tag, resp, nil
 }
 
-// DeleteRegistryRepositoryTag deletes a registry repository tag.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/container_registry/#delete-a-registry-repository-tag
 func (s *ContainerRegistryService) DeleteRegistryRepositoryTag(pid any, repository int, tagName string, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -302,11 +313,6 @@ type DeleteRegistryRepositoryTagsOptions struct {
 	NameRegexp *string `url:"name_regex,omitempty" json:"name_regex,omitempty"`
 }
 
-// DeleteRegistryRepositoryTags deletes repository tags in bulk based on
-// given criteria.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/container_registry/#delete-registry-repository-tags-in-bulk
 func (s *ContainerRegistryService) DeleteRegistryRepositoryTags(pid any, repository int, opt *DeleteRegistryRepositoryTagsOptions, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/container_registry_protection_rules.go b/vendor/gitlab.com/gitlab-org/api/client-go/container_registry_protection_rules.go
index 5871f3983..f7c0f2eb0 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/container_registry_protection_rules.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/container_registry_protection_rules.go
@@ -21,9 +21,32 @@ import (
 
 type (
 	ContainerRegistryProtectionRulesServiceInterface interface {
+		// ListContainerRegistryProtectionRules gets a list of container repository
+		// protection rules from a project’s container registry.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/container_repository_protection_rules/#list-container-repository-protection-rules
 		ListContainerRegistryProtectionRules(pid any, options ...RequestOptionFunc) ([]*ContainerRegistryProtectionRule, *Response, error)
+
+		// CreateContainerRegistryProtectionRule creates a container repository
+		// protection rule for a project’s container registry.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/container_repository_protection_rules/#create-a-container-repository-protection-rule
 		CreateContainerRegistryProtectionRule(pid any, opt *CreateContainerRegistryProtectionRuleOptions, options ...RequestOptionFunc) (*ContainerRegistryProtectionRule, *Response, error)
+
+		// UpdateContainerRegistryProtectionRule updates a container repository protection
+		// rule for a project’s container registry.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/container_repository_protection_rules/#update-a-container-repository-protection-rule
 		UpdateContainerRegistryProtectionRule(pid any, ruleID int, opt *UpdateContainerRegistryProtectionRuleOptions, options ...RequestOptionFunc) (*ContainerRegistryProtectionRule, *Response, error)
+
+		// DeleteContainerRegistryProtectionRule deletes a container repository protection
+		// rule from a project’s container registry.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/container_repository_protection_rules/#delete-a-container-repository-protection-rule
 		DeleteContainerRegistryProtectionRule(pid any, ruleID int, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -57,11 +80,6 @@ func (s ContainerRegistryProtectionRule) String() string {
 	return Stringify(s)
 }
 
-// ListContainerRegistryProtectionRules gets a list of container repository
-// protection rules from a project’s container registry.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/container_repository_protection_rules/#list-container-repository-protection-rules
 func (s *ContainerRegistryProtectionRulesService) ListContainerRegistryProtectionRules(pid any, options ...RequestOptionFunc) ([]*ContainerRegistryProtectionRule, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -94,11 +112,6 @@ type CreateContainerRegistryProtectionRuleOptions struct {
 	MinimumAccessLevelForDelete *ProtectionRuleAccessLevel `url:"minimum_access_level_for_delete,omitempty" json:"minimum_access_level_for_delete,omitempty"`
 }
 
-// CreateContainerRegistryProtectionRule creates a container repository
-// protection rule for a project’s container registry.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/container_repository_protection_rules/#create-a-container-repository-protection-rule
 func (s *ContainerRegistryProtectionRulesService) CreateContainerRegistryProtectionRule(pid any, opt *CreateContainerRegistryProtectionRuleOptions, options ...RequestOptionFunc) (*ContainerRegistryProtectionRule, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -131,11 +144,6 @@ type UpdateContainerRegistryProtectionRuleOptions struct {
 	MinimumAccessLevelForDelete *ProtectionRuleAccessLevel `url:"minimum_access_level_for_delete,omitempty" json:"minimum_access_level_for_delete,omitempty"`
 }
 
-// UpdateContainerRegistryProtectionRule updates a container repository protection
-// rule for a project’s container registry.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/container_repository_protection_rules/#update-a-container-repository-protection-rule
 func (s *ContainerRegistryProtectionRulesService) UpdateContainerRegistryProtectionRule(pid any, ruleID int, opt *UpdateContainerRegistryProtectionRuleOptions, options ...RequestOptionFunc) (*ContainerRegistryProtectionRule, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -157,11 +165,6 @@ func (s *ContainerRegistryProtectionRulesService) UpdateContainerRegistryProtect
 	return rule, resp, nil
 }
 
-// DeleteContainerRegistryProtectionRule deletes a container repository protection
-// rule from a project’s container registry.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/container_repository_protection_rules/#delete-a-container-repository-protection-rule
 func (s *ContainerRegistryProtectionRulesService) DeleteContainerRegistryProtectionRule(pid any, ruleID int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/custom_attributes.go b/vendor/gitlab.com/gitlab-org/api/client-go/custom_attributes.go
index 92e3babce..e365aa044 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/custom_attributes.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/custom_attributes.go
@@ -23,17 +23,76 @@ import (
 
 type (
 	CustomAttributesServiceInterface interface {
+		// ListCustomUserAttributes lists the custom attributes of the specified user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/custom_attributes/#list-custom-attributes
 		ListCustomUserAttributes(user int, options ...RequestOptionFunc) ([]*CustomAttribute, *Response, error)
+
+		// ListCustomGroupAttributes lists the custom attributes of the specified group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/custom_attributes/#list-custom-attributes
 		ListCustomGroupAttributes(group int, options ...RequestOptionFunc) ([]*CustomAttribute, *Response, error)
+
+		// ListCustomProjectAttributes lists the custom attributes of the specified project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/custom_attributes/#list-custom-attributes
 		ListCustomProjectAttributes(project int, options ...RequestOptionFunc) ([]*CustomAttribute, *Response, error)
+
+		// GetCustomUserAttribute returns the user attribute with a specific key.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/custom_attributes/#single-custom-attribute
 		GetCustomUserAttribute(user int, key string, options ...RequestOptionFunc) (*CustomAttribute, *Response, error)
+
+		// GetCustomGroupAttribute returns the group attribute with a specific key.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/custom_attributes/#single-custom-attribute
 		GetCustomGroupAttribute(group int, key string, options ...RequestOptionFunc) (*CustomAttribute, *Response, error)
+
+		// GetCustomProjectAttribute returns the project attribute with a specific key.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/custom_attributes/#single-custom-attribute
 		GetCustomProjectAttribute(project int, key string, options ...RequestOptionFunc) (*CustomAttribute, *Response, error)
+
+		// SetCustomUserAttribute sets the custom attributes of the specified user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/custom_attributes/#set-custom-attribute
 		SetCustomUserAttribute(user int, c CustomAttribute, options ...RequestOptionFunc) (*CustomAttribute, *Response, error)
+
+		// SetCustomGroupAttribute sets the custom attributes of the specified group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/custom_attributes/#set-custom-attribute
 		SetCustomGroupAttribute(group int, c CustomAttribute, options ...RequestOptionFunc) (*CustomAttribute, *Response, error)
+
+		// SetCustomProjectAttribute sets the custom attributes of the specified project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/custom_attributes/#set-custom-attribute
 		SetCustomProjectAttribute(project int, c CustomAttribute, options ...RequestOptionFunc) (*CustomAttribute, *Response, error)
+
+		// DeleteCustomUserAttribute removes the custom attribute of the specified user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/custom_attributes/#delete-custom-attribute
 		DeleteCustomUserAttribute(user int, key string, options ...RequestOptionFunc) (*Response, error)
+
+		// DeleteCustomGroupAttribute removes the custom attribute of the specified group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/custom_attributes/#delete-custom-attribute
 		DeleteCustomGroupAttribute(group int, key string, options ...RequestOptionFunc) (*Response, error)
+
+		// DeleteCustomProjectAttribute removes the custom attribute of the specified project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/custom_attributes/#delete-custom-attribute
 		DeleteCustomProjectAttribute(project int, key string, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -56,26 +115,14 @@ type CustomAttribute struct {
 	Value string `json:"value"`
 }
 
-// ListCustomUserAttributes lists the custom attributes of the specified user.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/custom_attributes/#list-custom-attributes
 func (s *CustomAttributesService) ListCustomUserAttributes(user int, options ...RequestOptionFunc) ([]*CustomAttribute, *Response, error) {
 	return s.listCustomAttributes("users", user, options...)
 }
 
-// ListCustomGroupAttributes lists the custom attributes of the specified group.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/custom_attributes/#list-custom-attributes
 func (s *CustomAttributesService) ListCustomGroupAttributes(group int, options ...RequestOptionFunc) ([]*CustomAttribute, *Response, error) {
 	return s.listCustomAttributes("groups", group, options...)
 }
 
-// ListCustomProjectAttributes lists the custom attributes of the specified project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/custom_attributes/#list-custom-attributes
 func (s *CustomAttributesService) ListCustomProjectAttributes(project int, options ...RequestOptionFunc) ([]*CustomAttribute, *Response, error) {
 	return s.listCustomAttributes("projects", project, options...)
 }
@@ -95,26 +142,14 @@ func (s *CustomAttributesService) listCustomAttributes(resource string, id int,
 	return cas, resp, nil
 }
 
-// GetCustomUserAttribute returns the user attribute with a specific key.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/custom_attributes/#single-custom-attribute
 func (s *CustomAttributesService) GetCustomUserAttribute(user int, key string, options ...RequestOptionFunc) (*CustomAttribute, *Response, error) {
 	return s.getCustomAttribute("users", user, key, options...)
 }
 
-// GetCustomGroupAttribute returns the group attribute with a specific key.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/custom_attributes/#single-custom-attribute
 func (s *CustomAttributesService) GetCustomGroupAttribute(group int, key string, options ...RequestOptionFunc) (*CustomAttribute, *Response, error) {
 	return s.getCustomAttribute("groups", group, key, options...)
 }
 
-// GetCustomProjectAttribute returns the project attribute with a specific key.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/custom_attributes/#single-custom-attribute
 func (s *CustomAttributesService) GetCustomProjectAttribute(project int, key string, options ...RequestOptionFunc) (*CustomAttribute, *Response, error) {
 	return s.getCustomAttribute("projects", project, key, options...)
 }
@@ -134,26 +169,14 @@ func (s *CustomAttributesService) getCustomAttribute(resource string, id int, ke
 	return ca, resp, nil
 }
 
-// SetCustomUserAttribute sets the custom attributes of the specified user.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/custom_attributes/#set-custom-attribute
 func (s *CustomAttributesService) SetCustomUserAttribute(user int, c CustomAttribute, options ...RequestOptionFunc) (*CustomAttribute, *Response, error) {
 	return s.setCustomAttribute("users", user, c, options...)
 }
 
-// SetCustomGroupAttribute sets the custom attributes of the specified group.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/custom_attributes/#set-custom-attribute
 func (s *CustomAttributesService) SetCustomGroupAttribute(group int, c CustomAttribute, options ...RequestOptionFunc) (*CustomAttribute, *Response, error) {
 	return s.setCustomAttribute("groups", group, c, options...)
 }
 
-// SetCustomProjectAttribute sets the custom attributes of the specified project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/custom_attributes/#set-custom-attribute
 func (s *CustomAttributesService) SetCustomProjectAttribute(project int, c CustomAttribute, options ...RequestOptionFunc) (*CustomAttribute, *Response, error) {
 	return s.setCustomAttribute("projects", project, c, options...)
 }
@@ -173,26 +196,14 @@ func (s *CustomAttributesService) setCustomAttribute(resource string, id int, c
 	return ca, resp, nil
 }
 
-// DeleteCustomUserAttribute removes the custom attribute of the specified user.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/custom_attributes/#delete-custom-attribute
 func (s *CustomAttributesService) DeleteCustomUserAttribute(user int, key string, options ...RequestOptionFunc) (*Response, error) {
 	return s.deleteCustomAttribute("users", user, key, options...)
 }
 
-// DeleteCustomGroupAttribute removes the custom attribute of the specified group.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/custom_attributes/#delete-custom-attribute
 func (s *CustomAttributesService) DeleteCustomGroupAttribute(group int, key string, options ...RequestOptionFunc) (*Response, error) {
 	return s.deleteCustomAttribute("groups", group, key, options...)
 }
 
-// DeleteCustomProjectAttribute removes the custom attribute of the specified project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/custom_attributes/#delete-custom-attribute
 func (s *CustomAttributesService) DeleteCustomProjectAttribute(project int, key string, options ...RequestOptionFunc) (*Response, error) {
 	return s.deleteCustomAttribute("projects", project, key, options...)
 }
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/database_migrations.go b/vendor/gitlab.com/gitlab-org/api/client-go/database_migrations.go
index 007b1b401..0b5490e80 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/database_migrations.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/database_migrations.go
@@ -21,6 +21,12 @@ import (
 
 type (
 	DatabaseMigrationsServiceInterface interface {
+		// MarkMigrationAsSuccessful marks pending migrations as successfully executed
+		// to prevent them from being executed by the db:migrate tasks. Use this API to
+		// skip failing migrations after they are determined to be safe to skip.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/database_migrations/#mark-a-migration-as-successful
 		MarkMigrationAsSuccessful(version int, opt *MarkMigrationAsSuccessfulOptions, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -44,12 +50,6 @@ type MarkMigrationAsSuccessfulOptions struct {
 	Database string `url:"database,omitempty" json:"database,omitempty"`
 }
 
-// MarkMigrationAsSuccessful markd pending migrations as successfully executed
-// to prevent them from being executed by the db:migrate tasks. Use this API to
-// skip failing migrations after they are determined to be safe to skip.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/database_migrations/#mark-a-migration-as-successful
 func (s *DatabaseMigrationsService) MarkMigrationAsSuccessful(version int, opt *MarkMigrationAsSuccessfulOptions, options ...RequestOptionFunc) (*Response, error) {
 	u := fmt.Sprintf("admin/migrations/%d/mark", version)
 
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/dependencies.go b/vendor/gitlab.com/gitlab-org/api/client-go/dependencies.go
index e4b85381e..36dacc96d 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/dependencies.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/dependencies.go
@@ -21,6 +21,12 @@ import (
 
 type (
 	DependenciesServiceInterface interface {
+		// ListProjectDependencies Get a list of project dependencies. This API partially
+		// mirroring Dependency List feature. This list can be generated only for languages
+		// and package managers supported by Gemnasium.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/dependencies/#list-project-dependencies
 		ListProjectDependencies(pid any, opt *ListProjectDependenciesOptions, options ...RequestOptionFunc) ([]*Dependency, *Response, error)
 	}
 
@@ -75,12 +81,6 @@ type ListProjectDependenciesOptions struct {
 	PackageManager []*DependencyPackageManagerValue `url:"package_manager,omitempty" json:"package_manager,omitempty"`
 }
 
-// ListProjectDependencies Get a list of project dependencies. This API partially
-// mirroring Dependency List feature. This list can be generated only for languages
-// and package managers supported by Gemnasium.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/dependencies/#list-project-dependencies
 func (s *DependenciesService) ListProjectDependencies(pid any, opt *ListProjectDependenciesOptions, options ...RequestOptionFunc) ([]*Dependency, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/dependency_list_export.go b/vendor/gitlab.com/gitlab-org/api/client-go/dependency_list_export.go
index ed475fd46..675e6af10 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/dependency_list_export.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/dependency_list_export.go
@@ -9,8 +9,37 @@ import (
 
 type (
 	DependencyListExportServiceInterface interface {
+		// CreateDependencyListExport creates a new CycloneDX JSON export for all the project dependencies
+		// detected in a pipeline.
+		//
+		// If an authenticated user does not have permission to read_dependency, this request returns a 403
+		// Forbidden status code.
+		//
+		// SBOM exports can be only accessed by the export’s author.
+		//
+		// GitLab docs:
+		// https://docs.gitlab.com/api/dependency_list_export/#create-a-dependency-list-export
 		CreateDependencyListExport(pipelineID int, opt *CreateDependencyListExportOptions, options ...RequestOptionFunc) (*DependencyListExport, *Response, error)
+
+		// GetDependencyListExport gets metadata about a single dependency list export.
+		//
+		// GitLab docs:
+		// https://docs.gitlab.com/api/dependency_list_export/#get-single-dependency-list-export
 		GetDependencyListExport(id int, options ...RequestOptionFunc) (*DependencyListExport, *Response, error)
+
+		// DownloadDependencyListExport downloads a single dependency list export.
+		//
+		// The github.com/CycloneDX/cyclonedx-go package can be used to parse the data from the returned io.Reader.
+		//
+		//	sbom := new(cdx.BOM)
+		//	decoder := cdx.NewBOMDecoder(reader, cdx.BOMFileFormatJSON)
+		//
+		//	if err = decoder.Decode(sbom); err != nil {
+		//		panic(err)
+		//	}
+		//
+		// GitLab docs:
+		// https://docs.gitlab.com/api/dependency_list_export/#download-dependency-list-export
 		DownloadDependencyListExport(id int, options ...RequestOptionFunc) (io.Reader, *Response, error)
 	}
 
@@ -47,16 +76,6 @@ type DependencyListExport struct {
 
 const defaultExportType = "sbom"
 
-// CreateDependencyListExport creates a new CycloneDX JSON export for all the project dependencies
-// detected in a pipeline.
-//
-// If an authenticated user does not have permission to read_dependency, this request returns a 403
-// Forbidden status code.
-//
-// SBOM exports can be only accessed by the export’s author.
-//
-// GitLab docs:
-// https://docs.gitlab.com/api/dependency_list_export/#create-a-dependency-list-export
 func (s *DependencyListExportService) CreateDependencyListExport(pipelineID int, opt *CreateDependencyListExportOptions, options ...RequestOptionFunc) (*DependencyListExport, *Response, error) {
 	// POST /pipelines/:id/dependency_list_exports
 	createExportPath := fmt.Sprintf("pipelines/%d/dependency_list_exports", pipelineID)
@@ -82,10 +101,6 @@ func (s *DependencyListExportService) CreateDependencyListExport(pipelineID int,
 	return export, resp, nil
 }
 
-// GetDependencyListExport gets metadata about a single dependency list export.
-//
-// GitLab docs:
-// https://docs.gitlab.com/api/dependency_list_export/#get-single-dependency-list-export
 func (s *DependencyListExportService) GetDependencyListExport(id int, options ...RequestOptionFunc) (*DependencyListExport, *Response, error) {
 	// GET /dependency_list_exports/:id
 	getExportPath := fmt.Sprintf("dependency_list_exports/%d", id)
@@ -104,19 +119,6 @@ func (s *DependencyListExportService) GetDependencyListExport(id int, options ..
 	return export, resp, nil
 }
 
-// DownloadDependencyListExport downloads a single dependency list export.
-//
-// The github.com/CycloneDX/cyclonedx-go package can be used to parse the data from the returned io.Reader.
-//
-//	sbom := new(cdx.BOM)
-//	decoder := cdx.NewBOMDecoder(reader, cdx.BOMFileFormatJSON)
-//
-//	if err = decoder.Decode(sbom); err != nil {
-//		panic(err)
-//	}
-//
-// GitLab docs:
-// https://docs.gitlab.com/api/dependency_list_export/#download-dependency-list-export
 func (s *DependencyListExportService) DownloadDependencyListExport(id int, options ...RequestOptionFunc) (io.Reader, *Response, error) {
 	// GET /dependency_list_exports/:id/download
 	downloadExportPath := fmt.Sprintf("dependency_list_exports/%d/download", id)
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/dependency_proxy.go b/vendor/gitlab.com/gitlab-org/api/client-go/dependency_proxy.go
index a475cb5fd..c210e8e98 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/dependency_proxy.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/dependency_proxy.go
@@ -21,6 +21,11 @@ import (
 
 type (
 	DependencyProxyServiceInterface interface {
+		// PurgeGroupDependencyProxy schedules for deletion the cached manifests and blobs
+		// for a group. This endpoint requires the Owner role for the group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/dependency_proxy/#purge-the-dependency-proxy-for-a-group
 		PurgeGroupDependencyProxy(gid any, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -35,11 +40,6 @@ type (
 
 var _ DependencyProxyServiceInterface = (*DependencyProxyService)(nil)
 
-// PurgeGroupDependencyProxy schedules for deletion the cached manifests and blobs
-// for a group. This endpoint requires the Owner role for the group.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/dependency_proxy/#purge-the-dependency-proxy-for-a-group
 func (s *DependencyProxyService) PurgeGroupDependencyProxy(gid any, options ...RequestOptionFunc) (*Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/deploy_keys.go b/vendor/gitlab.com/gitlab-org/api/client-go/deploy_keys.go
index 57d24568f..2d6af5b81 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/deploy_keys.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/deploy_keys.go
@@ -24,14 +24,61 @@ import (
 
 type (
 	DeployKeysServiceInterface interface {
+		// ListAllDeployKeys gets a list of all deploy keys.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_keys/#list-all-deploy-keys
 		ListAllDeployKeys(opt *ListInstanceDeployKeysOptions, options ...RequestOptionFunc) ([]*InstanceDeployKey, *Response, error)
+
+		// AddInstanceDeployKey creates a deploy key for the GitLab instance.
+		// Requires administrator access.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_keys/#add-deploy-key
 		AddInstanceDeployKey(opt *AddInstanceDeployKeyOptions, options ...RequestOptionFunc) (*InstanceDeployKey, *Response, error)
+
+		// ListProjectDeployKeys gets a list of a project's deploy keys.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_keys/#list-deploy-keys-for-project
 		ListProjectDeployKeys(pid any, opt *ListProjectDeployKeysOptions, options ...RequestOptionFunc) ([]*ProjectDeployKey, *Response, error)
+
+		// ListUserProjectDeployKeys gets a list of a user's deploy keys.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_keys/#list-project-deploy-keys-for-user
 		ListUserProjectDeployKeys(uid any, opt *ListUserProjectDeployKeysOptions, options ...RequestOptionFunc) ([]*ProjectDeployKey, *Response, error)
+
+		// GetDeployKey gets a single deploy key.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_keys/#get-a-single-deploy-key
 		GetDeployKey(pid any, deployKey int, options ...RequestOptionFunc) (*ProjectDeployKey, *Response, error)
+
+		// AddDeployKey creates a new deploy key for a project. If the deploy key already
+		// exists in another project, it will be joined to the project but only if
+		// the original one is accessible by the same user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_keys/#add-deploy-key-for-a-project
 		AddDeployKey(pid any, opt *AddDeployKeyOptions, options ...RequestOptionFunc) (*ProjectDeployKey, *Response, error)
+
+		// DeleteDeployKey deletes a deploy key from a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_keys/#delete-deploy-key
 		DeleteDeployKey(pid any, deployKey int, options ...RequestOptionFunc) (*Response, error)
+
+		// EnableDeployKey enables a deploy key.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_keys/#enable-a-deploy-key
 		EnableDeployKey(pid any, deployKey int, options ...RequestOptionFunc) (*ProjectDeployKey, *Response, error)
+
+		// UpdateDeployKey updates a deploy key for a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_keys/#update-deploy-key
 		UpdateDeployKey(pid any, deployKey int, opt *UpdateDeployKeyOptions, options ...RequestOptionFunc) (*ProjectDeployKey, *Response, error)
 	}
 
@@ -95,7 +142,7 @@ func (k ProjectDeployKey) String() string {
 	return Stringify(k)
 }
 
-// ListProjectDeployKeysOptions represents the available ListAllDeployKeys()
+// ListInstanceDeployKeysOptions represents the available ListAllDeployKeys()
 // options.
 //
 // GitLab API docs:
@@ -105,10 +152,6 @@ type ListInstanceDeployKeysOptions struct {
 	Public *bool `url:"public,omitempty" json:"public,omitempty"`
 }
 
-// ListAllDeployKeys gets a list of all deploy keys
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_keys/#list-all-deploy-keys
 func (s *DeployKeysService) ListAllDeployKeys(opt *ListInstanceDeployKeysOptions, options ...RequestOptionFunc) ([]*InstanceDeployKey, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodGet, "deploy_keys", opt, options)
 	if err != nil {
@@ -135,11 +178,6 @@ type AddInstanceDeployKeyOptions struct {
 	ExpiresAt *time.Time `url:"expires_at,omitempty" json:"expires_at,omitempty"`
 }
 
-// AddInstanceDeployKey creates a deploy key for the GitLab instance.
-// Requires administrator access.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_keys/#add-deploy-key
 func (s *DeployKeysService) AddInstanceDeployKey(opt *AddInstanceDeployKeyOptions, options ...RequestOptionFunc) (*InstanceDeployKey, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodPost, "deploy_keys", opt, options)
 	if err != nil {
@@ -162,10 +200,6 @@ func (s *DeployKeysService) AddInstanceDeployKey(opt *AddInstanceDeployKeyOption
 // https://docs.gitlab.com/api/deploy_keys/#list-deploy-keys-for-project
 type ListProjectDeployKeysOptions ListOptions
 
-// ListProjectDeployKeys gets a list of a project's deploy keys
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_keys/#list-deploy-keys-for-project
 func (s *DeployKeysService) ListProjectDeployKeys(pid any, opt *ListProjectDeployKeysOptions, options ...RequestOptionFunc) ([]*ProjectDeployKey, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -194,10 +228,6 @@ func (s *DeployKeysService) ListProjectDeployKeys(pid any, opt *ListProjectDeplo
 // https://docs.gitlab.com/api/deploy_keys/#list-project-deploy-keys-for-user
 type ListUserProjectDeployKeysOptions ListOptions
 
-// ListUserProjectDeployKeys gets a list of a user's deploy keys
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_keys/#list-project-deploy-keys-for-user
 func (s *DeployKeysService) ListUserProjectDeployKeys(uid any, opt *ListUserProjectDeployKeysOptions, options ...RequestOptionFunc) ([]*ProjectDeployKey, *Response, error) {
 	user, err := parseID(uid)
 	if err != nil {
@@ -219,10 +249,6 @@ func (s *DeployKeysService) ListUserProjectDeployKeys(uid any, opt *ListUserProj
 	return ks, resp, nil
 }
 
-// GetDeployKey gets a single deploy key.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_keys/#get-a-single-deploy-key
 func (s *DeployKeysService) GetDeployKey(pid any, deployKey int, options ...RequestOptionFunc) (*ProjectDeployKey, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -255,12 +281,6 @@ type AddDeployKeyOptions struct {
 	ExpiresAt *time.Time `url:"expires_at,omitempty" json:"expires_at,omitempty"`
 }
 
-// AddDeployKey creates a new deploy key for a project. If deploy key already
-// exists in another project - it will be joined to project but only if
-// original one is accessible by the same user.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_keys/#add-deploy-key-for-a-project
 func (s *DeployKeysService) AddDeployKey(pid any, opt *AddDeployKeyOptions, options ...RequestOptionFunc) (*ProjectDeployKey, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -282,10 +302,6 @@ func (s *DeployKeysService) AddDeployKey(pid any, opt *AddDeployKeyOptions, opti
 	return k, resp, nil
 }
 
-// DeleteDeployKey deletes a deploy key from a project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_keys/#delete-deploy-key
 func (s *DeployKeysService) DeleteDeployKey(pid any, deployKey int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -301,10 +317,6 @@ func (s *DeployKeysService) DeleteDeployKey(pid any, deployKey int, options ...R
 	return s.client.Do(req, nil)
 }
 
-// EnableDeployKey enables a deploy key.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_keys/#enable-a-deploy-key
 func (s *DeployKeysService) EnableDeployKey(pid any, deployKey int, options ...RequestOptionFunc) (*ProjectDeployKey, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -335,10 +347,6 @@ type UpdateDeployKeyOptions struct {
 	CanPush *bool   `url:"can_push,omitempty" json:"can_push,omitempty"`
 }
 
-// UpdateDeployKey updates a deploy key for a project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_keys/#update-deploy-key
 func (s *DeployKeysService) UpdateDeployKey(pid any, deployKey int, opt *UpdateDeployKeyOptions, options ...RequestOptionFunc) (*ProjectDeployKey, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/deploy_tokens.go b/vendor/gitlab.com/gitlab-org/api/client-go/deploy_tokens.go
index f7128a69f..0af3cd910 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/deploy_tokens.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/deploy_tokens.go
@@ -24,14 +24,58 @@ import (
 
 type (
 	DeployTokensServiceInterface interface {
+		// ListAllDeployTokens gets a list of all deploy tokens.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_tokens/#list-all-deploy-tokens
 		ListAllDeployTokens(options ...RequestOptionFunc) ([]*DeployToken, *Response, error)
+
+		// ListProjectDeployTokens gets a list of a project's deploy tokens.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_tokens/#list-project-deploy-tokens
 		ListProjectDeployTokens(pid any, opt *ListProjectDeployTokensOptions, options ...RequestOptionFunc) ([]*DeployToken, *Response, error)
+
+		// GetProjectDeployToken gets a single deploy token.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_tokens/#get-a-project-deploy-token
 		GetProjectDeployToken(pid any, deployToken int, options ...RequestOptionFunc) (*DeployToken, *Response, error)
+
+		// CreateProjectDeployToken creates a new deploy token for a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_tokens/#create-a-project-deploy-token
 		CreateProjectDeployToken(pid any, opt *CreateProjectDeployTokenOptions, options ...RequestOptionFunc) (*DeployToken, *Response, error)
+
+		// DeleteProjectDeployToken removes a deploy token from the project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_tokens/#delete-a-project-deploy-token
 		DeleteProjectDeployToken(pid any, deployToken int, options ...RequestOptionFunc) (*Response, error)
+
+		// ListGroupDeployTokens gets a list of a group’s deploy tokens.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_tokens/#list-group-deploy-tokens
 		ListGroupDeployTokens(gid any, opt *ListGroupDeployTokensOptions, options ...RequestOptionFunc) ([]*DeployToken, *Response, error)
+
+		// GetGroupDeployToken gets a single deploy token.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_tokens/#get-a-group-deploy-token
 		GetGroupDeployToken(gid any, deployToken int, options ...RequestOptionFunc) (*DeployToken, *Response, error)
+
+		// CreateGroupDeployToken creates a new deploy token for a group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_tokens/#create-a-group-deploy-token
 		CreateGroupDeployToken(gid any, opt *CreateGroupDeployTokenOptions, options ...RequestOptionFunc) (*DeployToken, *Response, error)
+
+		// DeleteGroupDeployToken removes a deploy token from the group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deploy_tokens/#delete-a-group-deploy-token
 		DeleteGroupDeployToken(gid any, deployToken int, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -60,10 +104,6 @@ func (k DeployToken) String() string {
 	return Stringify(k)
 }
 
-// ListAllDeployTokens gets a list of all deploy tokens.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_tokens/#list-all-deploy-tokens
 func (s *DeployTokensService) ListAllDeployTokens(options ...RequestOptionFunc) ([]*DeployToken, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodGet, "deploy_tokens", nil, options)
 	if err != nil {
@@ -86,10 +126,6 @@ func (s *DeployTokensService) ListAllDeployTokens(options ...RequestOptionFunc)
 // https://docs.gitlab.com/api/deploy_tokens/#list-project-deploy-tokens
 type ListProjectDeployTokensOptions ListOptions
 
-// ListProjectDeployTokens gets a list of a project's deploy tokens.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_tokens/#list-project-deploy-tokens
 func (s *DeployTokensService) ListProjectDeployTokens(pid any, opt *ListProjectDeployTokensOptions, options ...RequestOptionFunc) ([]*DeployToken, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -111,10 +147,6 @@ func (s *DeployTokensService) ListProjectDeployTokens(pid any, opt *ListProjectD
 	return ts, resp, nil
 }
 
-// GetProjectDeployToken gets a single deploy token.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_tokens/#get-a-project-deploy-token
 func (s *DeployTokensService) GetProjectDeployToken(pid any, deployToken int, options ...RequestOptionFunc) (*DeployToken, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -147,10 +179,6 @@ type CreateProjectDeployTokenOptions struct {
 	Scopes    *[]string  `url:"scopes,omitempty" json:"scopes,omitempty"`
 }
 
-// CreateProjectDeployToken creates a new deploy token for a project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_tokens/#create-a-project-deploy-token
 func (s *DeployTokensService) CreateProjectDeployToken(pid any, opt *CreateProjectDeployTokenOptions, options ...RequestOptionFunc) (*DeployToken, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -172,10 +200,6 @@ func (s *DeployTokensService) CreateProjectDeployToken(pid any, opt *CreateProje
 	return t, resp, nil
 }
 
-// DeleteProjectDeployToken removes a deploy token from the project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_tokens/#delete-a-project-deploy-token
 func (s *DeployTokensService) DeleteProjectDeployToken(pid any, deployToken int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -198,10 +222,6 @@ func (s *DeployTokensService) DeleteProjectDeployToken(pid any, deployToken int,
 // https://docs.gitlab.com/api/deploy_tokens/#list-group-deploy-tokens
 type ListGroupDeployTokensOptions ListOptions
 
-// ListGroupDeployTokens gets a list of a group’s deploy tokens.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_tokens/#list-group-deploy-tokens
 func (s *DeployTokensService) ListGroupDeployTokens(gid any, opt *ListGroupDeployTokensOptions, options ...RequestOptionFunc) ([]*DeployToken, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -223,10 +243,6 @@ func (s *DeployTokensService) ListGroupDeployTokens(gid any, opt *ListGroupDeplo
 	return ts, resp, nil
 }
 
-// GetGroupDeployToken gets a single deploy token.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_tokens/#get-a-group-deploy-token
 func (s *DeployTokensService) GetGroupDeployToken(gid any, deployToken int, options ...RequestOptionFunc) (*DeployToken, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -259,10 +275,6 @@ type CreateGroupDeployTokenOptions struct {
 	Scopes    *[]string  `url:"scopes,omitempty" json:"scopes,omitempty"`
 }
 
-// CreateGroupDeployToken creates a new deploy token for a group.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_tokens/#create-a-group-deploy-token
 func (s *DeployTokensService) CreateGroupDeployToken(gid any, opt *CreateGroupDeployTokenOptions, options ...RequestOptionFunc) (*DeployToken, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -284,10 +296,6 @@ func (s *DeployTokensService) CreateGroupDeployToken(gid any, opt *CreateGroupDe
 	return t, resp, nil
 }
 
-// DeleteGroupDeployToken removes a deploy token from the group.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deploy_tokens/#delete-a-group-deploy-token
 func (s *DeployTokensService) DeleteGroupDeployToken(gid any, deployToken int, options ...RequestOptionFunc) (*Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/deployments.go b/vendor/gitlab.com/gitlab-org/api/client-go/deployments.go
index 7368477c9..86d48ee5c 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/deployments.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/deployments.go
@@ -24,11 +24,40 @@ import (
 type (
 	// DeploymentsServiceInterface defines all the API methods for the DeploymentsService
 	DeploymentsServiceInterface interface {
+		// ListProjectDeployments gets a list of deployments in a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deployments/#list-project-deployments
 		ListProjectDeployments(pid any, opts *ListProjectDeploymentsOptions, options ...RequestOptionFunc) ([]*Deployment, *Response, error)
+
+		// GetProjectDeployment gets a specific deployment for a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deployments/#get-a-specific-deployment
 		GetProjectDeployment(pid any, deployment int, options ...RequestOptionFunc) (*Deployment, *Response, error)
+
+		// CreateProjectDeployment creates a project deployment.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deployments/#create-a-deployment
 		CreateProjectDeployment(pid any, opt *CreateProjectDeploymentOptions, options ...RequestOptionFunc) (*Deployment, *Response, error)
+
+		// UpdateProjectDeployment updates a project deployment.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deployments/#update-a-deployment
 		UpdateProjectDeployment(pid any, deployment int, opt *UpdateProjectDeploymentOptions, options ...RequestOptionFunc) (*Deployment, *Response, error)
+
+		// ApproveOrRejectProjectDeployment approves or rejects a blocked deployment.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deployments/#approve-or-reject-a-blocked-deployment
 		ApproveOrRejectProjectDeployment(pid any, deployment int, opt *ApproveOrRejectProjectDeploymentOptions, options ...RequestOptionFunc) (*Response, error)
+
+		// DeleteProjectDeployment deletes a specific deployment.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deployments/#delete-a-specific-deployment
 		DeleteProjectDeployment(pid any, deployment int, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -43,7 +72,7 @@ type (
 
 var _ DeploymentsServiceInterface = (*DeploymentsService)(nil)
 
-// Deployment represents the Gitlab deployment
+// Deployment represents the GitLab deployment
 type Deployment struct {
 	ID          int          `json:"id"`
 	IID         int          `json:"iid"`
@@ -91,19 +120,15 @@ type ListProjectDeploymentsOptions struct {
 	Environment *string `url:"environment,omitempty" json:"environment,omitempty"`
 	Status      *string `url:"status,omitempty" json:"status,omitempty"`
 
-	// Only for Gitlab versions less than 14
+	// Only for GitLab versions less than 14
 	UpdatedAfter  *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"`
 	UpdatedBefore *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"`
 
-	// Only for Gitlab 14 or higher
+	// Only for GitLab 14 or higher
 	FinishedAfter  *time.Time `url:"finished_after,omitempty" json:"finished_after,omitempty"`
 	FinishedBefore *time.Time `url:"finished_before,omitempty" json:"finished_before,omitempty"`
 }
 
-// ListProjectDeployments gets a list of deployments in a project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deployments/#list-project-deployments
 func (s *DeploymentsService) ListProjectDeployments(pid any, opts *ListProjectDeploymentsOptions, options ...RequestOptionFunc) ([]*Deployment, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -125,10 +150,6 @@ func (s *DeploymentsService) ListProjectDeployments(pid any, opts *ListProjectDe
 	return ds, resp, nil
 }
 
-// GetProjectDeployment get a deployment for a project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deployments/#get-a-specific-deployment
 func (s *DeploymentsService) GetProjectDeployment(pid any, deployment int, options ...RequestOptionFunc) (*Deployment, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -163,10 +184,6 @@ type CreateProjectDeploymentOptions struct {
 	Status      *DeploymentStatusValue `url:"status,omitempty" json:"status,omitempty"`
 }
 
-// CreateProjectDeployment creates a project deployment.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deployments/#create-a-deployment
 func (s *DeploymentsService) CreateProjectDeployment(pid any, opt *CreateProjectDeploymentOptions, options ...RequestOptionFunc) (*Deployment, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -197,10 +214,6 @@ type UpdateProjectDeploymentOptions struct {
 	Status *DeploymentStatusValue `url:"status,omitempty" json:"status,omitempty"`
 }
 
-// UpdateProjectDeployment updates a project deployment.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deployments/#update-a-deployment
 func (s *DeploymentsService) UpdateProjectDeployment(pid any, deployment int, opt *UpdateProjectDeploymentOptions, options ...RequestOptionFunc) (*Deployment, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -233,10 +246,6 @@ type ApproveOrRejectProjectDeploymentOptions struct {
 	RepresentedAs *string                   `url:"represented_as,omitempty" json:"represented_as,omitempty"`
 }
 
-// ApproveOrRejectProjectDeployment approve or reject a blocked deployment.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deployments/#approve-or-reject-a-blocked-deployment
 func (s *DeploymentsService) ApproveOrRejectProjectDeployment(pid any, deployment int,
 	opt *ApproveOrRejectProjectDeploymentOptions, options ...RequestOptionFunc,
 ) (*Response, error) {
@@ -254,10 +263,6 @@ func (s *DeploymentsService) ApproveOrRejectProjectDeployment(pid any, deploymen
 	return s.client.Do(req, nil)
 }
 
-// DeleteProjectDeployment delete a project deployment.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deployments/#delete-a-specific-deployment
 func (s *DeploymentsService) DeleteProjectDeployment(pid any, deployment int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/deployments_merge_requests.go b/vendor/gitlab.com/gitlab-org/api/client-go/deployments_merge_requests.go
index a2b0e717c..ca5aa8ce1 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/deployments_merge_requests.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/deployments_merge_requests.go
@@ -11,6 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+
 package gitlab
 
 import (
@@ -21,6 +22,10 @@ import (
 type (
 	// DeploymentMergeRequestsServiceInterface defines all the API methods for the DeploymentMergeRequestsService
 	DeploymentMergeRequestsServiceInterface interface {
+		// ListDeploymentMergeRequests get the merge requests associated with deployment.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/deployments/#list-of-merge-requests-associated-with-a-deployment
 		ListDeploymentMergeRequests(pid any, deployment int, opts *ListMergeRequestsOptions, options ...RequestOptionFunc) ([]*MergeRequest, *Response, error)
 	}
 
@@ -36,10 +41,6 @@ type (
 
 var _ DeploymentMergeRequestsServiceInterface = (*DeploymentMergeRequestsService)(nil)
 
-// ListDeploymentMergeRequests get the merge requests associated with deployment.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/deployments/#list-of-merge-requests-associated-with-a-deployment
 func (s *DeploymentMergeRequestsService) ListDeploymentMergeRequests(pid any, deployment int, opts *ListMergeRequestsOptions, options ...RequestOptionFunc) ([]*MergeRequest, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/discussions.go b/vendor/gitlab.com/gitlab-org/api/client-go/discussions.go
index f73a865ad..a6cc6224b 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/discussions.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/discussions.go
@@ -25,36 +25,189 @@ import (
 type (
 	// DiscussionsServiceInterface defines all the API methods for the DiscussionsService
 	DiscussionsServiceInterface interface {
+		// ListIssueDiscussions gets a list of all discussions for a single issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#list-project-issue-discussion-items
 		ListIssueDiscussions(pid any, issue int, opt *ListIssueDiscussionsOptions, options ...RequestOptionFunc) ([]*Discussion, *Response, error)
+
+		// GetIssueDiscussion returns a single discussion for a specific project issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#get-single-issue-discussion-item
 		GetIssueDiscussion(pid any, issue int, discussion string, options ...RequestOptionFunc) (*Discussion, *Response, error)
+
+		// CreateIssueDiscussion creates a new discussion to a single project issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#create-new-issue-thread
 		CreateIssueDiscussion(pid any, issue int, opt *CreateIssueDiscussionOptions, options ...RequestOptionFunc) (*Discussion, *Response, error)
+
+		// AddIssueDiscussionNote creates a new note in an existing discussion of an issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#add-note-to-existing-issue-thread
 		AddIssueDiscussionNote(pid any, issue int, discussion string, opt *AddIssueDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error)
+
+		// UpdateIssueDiscussionNote modifies an existing note in a discussion of an issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#modify-existing-issue-thread-note
 		UpdateIssueDiscussionNote(pid any, issue int, discussion string, note int, opt *UpdateIssueDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error)
+
+		// DeleteIssueDiscussionNote deletes a note from a discussion of an issue.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#delete-an-issue-thread-note
 		DeleteIssueDiscussionNote(pid any, issue int, discussion string, note int, options ...RequestOptionFunc) (*Response, error)
+
+		// ListSnippetDiscussions gets all discussions for a snippet.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#list-project-snippet-discussion-items
 		ListSnippetDiscussions(pid any, snippet int, opt *ListSnippetDiscussionsOptions, options ...RequestOptionFunc) ([]*Discussion, *Response, error)
+
+		// GetSnippetDiscussion returns a single discussion for a snippet.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#get-single-snippet-discussion-item
 		GetSnippetDiscussion(pid any, snippet int, discussion string, options ...RequestOptionFunc) (*Discussion, *Response, error)
+
+		// CreateSnippetDiscussion creates a new discussion for a snippet.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#create-new-snippet-thread
 		CreateSnippetDiscussion(pid any, snippet int, opt *CreateSnippetDiscussionOptions, options ...RequestOptionFunc) (*Discussion, *Response, error)
+
+		// AddSnippetDiscussionNote adds a new note to a snippet discussion.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#add-note-to-existing-snippet-thread
 		AddSnippetDiscussionNote(pid any, snippet int, discussion string, opt *AddSnippetDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error)
+
+		// UpdateSnippetDiscussionNote modifies an existing note in a snippet discussion.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#modify-existing-snippet-thread-note
 		UpdateSnippetDiscussionNote(pid any, snippet int, discussion string, note int, opt *UpdateSnippetDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error)
+
+		// DeleteSnippetDiscussionNote deletes a note from a snippet discussion.
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/discussions/#delete-a-snippet-thread-note
 		DeleteSnippetDiscussionNote(pid any, snippet int, discussion string, note int, options ...RequestOptionFunc) (*Response, error)
+
+		// ListGroupEpicDiscussions gets all discussions for a group epic.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#list-group-epic-discussion-items
 		ListGroupEpicDiscussions(gid any, epic int, opt *ListGroupEpicDiscussionsOptions, options ...RequestOptionFunc) ([]*Discussion, *Response, error)
+
+		// GetEpicDiscussion returns a single discussion for a group epic.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#get-single-epic-discussion-item
 		GetEpicDiscussion(gid any, epic int, discussion string, options ...RequestOptionFunc) (*Discussion, *Response, error)
+
+		// CreateEpicDiscussion creates a new discussion for a group epic.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#create-new-epic-thread
 		CreateEpicDiscussion(gid any, epic int, opt *CreateEpicDiscussionOptions, options ...RequestOptionFunc) (*Discussion, *Response, error)
+
+		// AddEpicDiscussionNote adds a new note to an epic discussion.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#add-note-to-existing-epic-thread
 		AddEpicDiscussionNote(gid any, epic int, discussion string, opt *AddEpicDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error)
+
+		// UpdateEpicDiscussionNote modifies an existing note in an epic discussion.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#modify-existing-epic-thread-note
 		UpdateEpicDiscussionNote(gid any, epic int, discussion string, note int, opt *UpdateEpicDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error)
+
+		// DeleteEpicDiscussionNote deletes a note from an epic discussion.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#delete-an-epic-thread-note
 		DeleteEpicDiscussionNote(gid any, epic int, discussion string, note int, options ...RequestOptionFunc) (*Response, error)
+
+		// ListMergeRequestDiscussions gets all discussions for a merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#list-project-merge-request-discussion-items
 		ListMergeRequestDiscussions(pid any, mergeRequest int, opt *ListMergeRequestDiscussionsOptions, options ...RequestOptionFunc) ([]*Discussion, *Response, error)
+
+		// GetMergeRequestDiscussion returns a single discussion for a merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#get-single-merge-request-discussion-item
 		GetMergeRequestDiscussion(pid any, mergeRequest int, discussion string, options ...RequestOptionFunc) (*Discussion, *Response, error)
+
+		// CreateMergeRequestDiscussion creates a new discussion for a merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#create-new-merge-request-thread
 		CreateMergeRequestDiscussion(pid any, mergeRequest int, opt *CreateMergeRequestDiscussionOptions, options ...RequestOptionFunc) (*Discussion, *Response, error)
+
+		// ResolveMergeRequestDiscussion resolves or unresolves a merge request discussion.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#resolve-a-merge-request-thread
 		ResolveMergeRequestDiscussion(pid any, mergeRequest int, discussion string, opt *ResolveMergeRequestDiscussionOptions, options ...RequestOptionFunc) (*Discussion, *Response, error)
+
+		// AddMergeRequestDiscussionNote adds a new note to a merge request discussion.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#add-note-to-existing-merge-request-thread
 		AddMergeRequestDiscussionNote(pid any, mergeRequest int, discussion string, opt *AddMergeRequestDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error)
+
+		// UpdateMergeRequestDiscussionNote modifies an existing note in a merge request discussion.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#modify-an-existing-merge-request-thread-note
 		UpdateMergeRequestDiscussionNote(pid any, mergeRequest int, discussion string, note int, opt *UpdateMergeRequestDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error)
+
+		// DeleteMergeRequestDiscussionNote deletes a note from a merge request discussion.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#delete-a-merge-request-thread-note
 		DeleteMergeRequestDiscussionNote(pid any, mergeRequest int, discussion string, note int, options ...RequestOptionFunc) (*Response, error)
+
+		// ListCommitDiscussions gets all discussions for a commit.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#list-project-commit-discussion-items
 		ListCommitDiscussions(pid any, commit string, opt *ListCommitDiscussionsOptions, options ...RequestOptionFunc) ([]*Discussion, *Response, error)
+
+		// GetCommitDiscussion returns a single discussion for a commit.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#get-single-commit-discussion-item
 		GetCommitDiscussion(pid any, commit string, discussion string, options ...RequestOptionFunc) (*Discussion, *Response, error)
+
+		// CreateCommitDiscussion creates a new discussion for a commit.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#create-new-commit-thread
 		CreateCommitDiscussion(pid any, commit string, opt *CreateCommitDiscussionOptions, options ...RequestOptionFunc) (*Discussion, *Response, error)
+
+		// AddCommitDiscussionNote adds a new note to a commit discussion.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#add-note-to-existing-commit-thread
 		AddCommitDiscussionNote(pid any, commit string, discussion string, opt *AddCommitDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error)
+
+		// UpdateCommitDiscussionNote modifies an existing note in a commit discussion.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#modify-an-existing-commit-thread-note
 		UpdateCommitDiscussionNote(pid any, commit string, discussion string, note int, opt *UpdateCommitDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error)
+
+		// DeleteCommitDiscussionNote deletes a note from a commit discussion.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/discussions/#delete-a-commit-thread-note
 		DeleteCommitDiscussionNote(pid any, commit string, discussion string, note int, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -89,11 +242,6 @@ func (d Discussion) String() string {
 // https://docs.gitlab.com/api/discussions/#list-project-issue-discussion-items
 type ListIssueDiscussionsOptions ListOptions
 
-// ListIssueDiscussions gets a list of all discussions for a single
-// issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#list-project-issue-discussion-items
 func (s *DiscussionsService) ListIssueDiscussions(pid any, issue int, opt *ListIssueDiscussionsOptions, options ...RequestOptionFunc) ([]*Discussion, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -115,10 +263,6 @@ func (s *DiscussionsService) ListIssueDiscussions(pid any, issue int, opt *ListI
 	return ds, resp, nil
 }
 
-// GetIssueDiscussion returns a single discussion for a specific project issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#get-single-issue-discussion-item
 func (s *DiscussionsService) GetIssueDiscussion(pid any, issue int, discussion string, options ...RequestOptionFunc) (*Discussion, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -154,10 +298,6 @@ type CreateIssueDiscussionOptions struct {
 	CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
 }
 
-// CreateIssueDiscussion creates a new discussion to a single project issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#create-new-issue-thread
 func (s *DiscussionsService) CreateIssueDiscussion(pid any, issue int, opt *CreateIssueDiscussionOptions, options ...RequestOptionFunc) (*Discussion, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -189,10 +329,6 @@ type AddIssueDiscussionNoteOptions struct {
 	CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
 }
 
-// AddIssueDiscussionNote creates a new discussion to a single project issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#add-note-to-existing-issue-thread
 func (s *DiscussionsService) AddIssueDiscussionNote(pid any, issue int, discussion string, opt *AddIssueDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -228,10 +364,6 @@ type UpdateIssueDiscussionNoteOptions struct {
 	CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
 }
 
-// UpdateIssueDiscussionNote modifies existing discussion of an issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#modify-existing-issue-thread-note
 func (s *DiscussionsService) UpdateIssueDiscussionNote(pid any, issue int, discussion string, note int, opt *UpdateIssueDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -258,10 +390,6 @@ func (s *DiscussionsService) UpdateIssueDiscussionNote(pid any, issue int, discu
 	return n, resp, nil
 }
 
-// DeleteIssueDiscussionNote deletes an existing discussion of an issue.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#delete-an-issue-thread-note
 func (s *DiscussionsService) DeleteIssueDiscussionNote(pid any, issue int, discussion string, note int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -289,11 +417,6 @@ func (s *DiscussionsService) DeleteIssueDiscussionNote(pid any, issue int, discu
 // https://docs.gitlab.com/api/discussions/#list-project-snippet-discussion-items
 type ListSnippetDiscussionsOptions ListOptions
 
-// ListSnippetDiscussions gets a list of all discussions for a single
-// snippet. Snippet discussions are comments users can post to a snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#list-project-snippet-discussion-items
 func (s *DiscussionsService) ListSnippetDiscussions(pid any, snippet int, opt *ListSnippetDiscussionsOptions, options ...RequestOptionFunc) ([]*Discussion, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -315,10 +438,6 @@ func (s *DiscussionsService) ListSnippetDiscussions(pid any, snippet int, opt *L
 	return ds, resp, nil
 }
 
-// GetSnippetDiscussion returns a single discussion for a given snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#get-single-snippet-discussion-item
 func (s *DiscussionsService) GetSnippetDiscussion(pid any, snippet int, discussion string, options ...RequestOptionFunc) (*Discussion, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -354,11 +473,6 @@ type CreateSnippetDiscussionOptions struct {
 	CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
 }
 
-// CreateSnippetDiscussion creates a new discussion for a single snippet.
-// Snippet discussions are comments users can post to a snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#create-new-snippet-thread
 func (s *DiscussionsService) CreateSnippetDiscussion(pid any, snippet int, opt *CreateSnippetDiscussionOptions, options ...RequestOptionFunc) (*Discussion, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -390,11 +504,6 @@ type AddSnippetDiscussionNoteOptions struct {
 	CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
 }
 
-// AddSnippetDiscussionNote creates a new discussion to a single project
-// snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#add-note-to-existing-snippet-thread
 func (s *DiscussionsService) AddSnippetDiscussionNote(pid any, snippet int, discussion string, opt *AddSnippetDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -430,10 +539,6 @@ type UpdateSnippetDiscussionNoteOptions struct {
 	CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
 }
 
-// UpdateSnippetDiscussionNote modifies existing discussion of a snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#modify-existing-snippet-thread-note
 func (s *DiscussionsService) UpdateSnippetDiscussionNote(pid any, snippet int, discussion string, note int, opt *UpdateSnippetDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -460,10 +565,6 @@ func (s *DiscussionsService) UpdateSnippetDiscussionNote(pid any, snippet int, d
 	return n, resp, nil
 }
 
-// DeleteSnippetDiscussionNote deletes an existing discussion of a snippet.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#delete-a-snippet-thread-note
 func (s *DiscussionsService) DeleteSnippetDiscussionNote(pid any, snippet int, discussion string, note int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -491,11 +592,6 @@ func (s *DiscussionsService) DeleteSnippetDiscussionNote(pid any, snippet int, d
 // https://docs.gitlab.com/api/discussions/#list-group-epic-discussion-items
 type ListGroupEpicDiscussionsOptions ListOptions
 
-// ListGroupEpicDiscussions gets a list of all discussions for a single
-// epic. Epic discussions are comments users can post to a epic.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#list-group-epic-discussion-items
 func (s *DiscussionsService) ListGroupEpicDiscussions(gid any, epic int, opt *ListGroupEpicDiscussionsOptions, options ...RequestOptionFunc) ([]*Discussion, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -520,10 +616,6 @@ func (s *DiscussionsService) ListGroupEpicDiscussions(gid any, epic int, opt *Li
 	return ds, resp, nil
 }
 
-// GetEpicDiscussion returns a single discussion for a given epic.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#get-single-epic-discussion-item
 func (s *DiscussionsService) GetEpicDiscussion(gid any, epic int, discussion string, options ...RequestOptionFunc) (*Discussion, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -559,11 +651,6 @@ type CreateEpicDiscussionOptions struct {
 	CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
 }
 
-// CreateEpicDiscussion creates a new discussion for a single epic. Epic
-// discussions are comments users can post to a epic.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#create-new-epic-thread
 func (s *DiscussionsService) CreateEpicDiscussion(gid any, epic int, opt *CreateEpicDiscussionOptions, options ...RequestOptionFunc) (*Discussion, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -598,10 +685,6 @@ type AddEpicDiscussionNoteOptions struct {
 	CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
 }
 
-// AddEpicDiscussionNote creates a new discussion to a single project epic.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#add-note-to-existing-epic-thread
 func (s *DiscussionsService) AddEpicDiscussionNote(gid any, epic int, discussion string, opt *AddEpicDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -637,10 +720,6 @@ type UpdateEpicDiscussionNoteOptions struct {
 	CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
 }
 
-// UpdateEpicDiscussionNote modifies existing discussion of an epic.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#modify-existing-epic-thread-note
 func (s *DiscussionsService) UpdateEpicDiscussionNote(gid any, epic int, discussion string, note int, opt *UpdateEpicDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -667,10 +746,6 @@ func (s *DiscussionsService) UpdateEpicDiscussionNote(gid any, epic int, discuss
 	return n, resp, nil
 }
 
-// DeleteEpicDiscussionNote deletes an existing discussion of a epic.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#delete-an-epic-thread-note
 func (s *DiscussionsService) DeleteEpicDiscussionNote(gid any, epic int, discussion string, note int, options ...RequestOptionFunc) (*Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -698,11 +773,6 @@ func (s *DiscussionsService) DeleteEpicDiscussionNote(gid any, epic int, discuss
 // https://docs.gitlab.com/api/discussions/#list-project-merge-request-discussion-items
 type ListMergeRequestDiscussionsOptions ListOptions
 
-// ListMergeRequestDiscussions gets a list of all discussions for a single
-// merge request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#list-project-merge-request-discussion-items
 func (s *DiscussionsService) ListMergeRequestDiscussions(pid any, mergeRequest int, opt *ListMergeRequestDiscussionsOptions, options ...RequestOptionFunc) ([]*Discussion, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -727,11 +797,6 @@ func (s *DiscussionsService) ListMergeRequestDiscussions(pid any, mergeRequest i
 	return ds, resp, nil
 }
 
-// GetMergeRequestDiscussion returns a single discussion for a given merge
-// request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#get-single-merge-request-discussion-item
 func (s *DiscussionsService) GetMergeRequestDiscussion(pid any, mergeRequest int, discussion string, options ...RequestOptionFunc) (*Discussion, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -800,11 +865,6 @@ type LinePositionOptions struct {
 	NewLine  *int    `url:"new_line,omitempty" json:"new_line,omitempty"`
 }
 
-// CreateMergeRequestDiscussion creates a new discussion for a single merge
-// request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#create-new-merge-request-thread
 func (s *DiscussionsService) CreateMergeRequestDiscussion(pid any, mergeRequest int, opt *CreateMergeRequestDiscussionOptions, options ...RequestOptionFunc) (*Discussion, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -838,11 +898,6 @@ type ResolveMergeRequestDiscussionOptions struct {
 	Resolved *bool `url:"resolved,omitempty" json:"resolved,omitempty"`
 }
 
-// ResolveMergeRequestDiscussion resolves/unresolves whole discussion of a merge
-// request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#resolve-a-merge-request-thread
 func (s *DiscussionsService) ResolveMergeRequestDiscussion(pid any, mergeRequest int, discussion string, opt *ResolveMergeRequestDiscussionOptions, options ...RequestOptionFunc) (*Discussion, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -878,11 +933,6 @@ type AddMergeRequestDiscussionNoteOptions struct {
 	CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
 }
 
-// AddMergeRequestDiscussionNote creates a new discussion to a single project
-// merge request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#add-note-to-existing-merge-request-thread
 func (s *DiscussionsService) AddMergeRequestDiscussionNote(pid any, mergeRequest int, discussion string, opt *AddMergeRequestDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -919,11 +969,6 @@ type UpdateMergeRequestDiscussionNoteOptions struct {
 	Resolved  *bool      `url:"resolved,omitempty" json:"resolved,omitempty"`
 }
 
-// UpdateMergeRequestDiscussionNote modifies existing discussion of a merge
-// request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#modify-an-existing-merge-request-thread-note
 func (s *DiscussionsService) UpdateMergeRequestDiscussionNote(pid any, mergeRequest int, discussion string, note int, opt *UpdateMergeRequestDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -950,11 +995,6 @@ func (s *DiscussionsService) UpdateMergeRequestDiscussionNote(pid any, mergeRequ
 	return n, resp, nil
 }
 
-// DeleteMergeRequestDiscussionNote deletes an existing discussion of a merge
-// request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#delete-a-merge-request-thread-note
 func (s *DiscussionsService) DeleteMergeRequestDiscussionNote(pid any, mergeRequest int, discussion string, note int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -982,11 +1022,6 @@ func (s *DiscussionsService) DeleteMergeRequestDiscussionNote(pid any, mergeRequ
 // https://docs.gitlab.com/api/discussions/#list-project-commit-discussion-items
 type ListCommitDiscussionsOptions ListOptions
 
-// ListCommitDiscussions gets a list of all discussions for a single
-// commit.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#list-project-commit-discussion-items
 func (s *DiscussionsService) ListCommitDiscussions(pid any, commit string, opt *ListCommitDiscussionsOptions, options ...RequestOptionFunc) ([]*Discussion, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -1011,11 +1046,6 @@ func (s *DiscussionsService) ListCommitDiscussions(pid any, commit string, opt *
 	return ds, resp, nil
 }
 
-// GetCommitDiscussion returns a single discussion for a specific project
-// commit.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#get-single-commit-discussion-item
 func (s *DiscussionsService) GetCommitDiscussion(pid any, commit string, discussion string, options ...RequestOptionFunc) (*Discussion, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -1052,10 +1082,6 @@ type CreateCommitDiscussionOptions struct {
 	Position  *NotePosition `url:"position,omitempty" json:"position,omitempty"`
 }
 
-// CreateCommitDiscussion creates a new discussion to a single project commit.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#create-new-commit-thread
 func (s *DiscussionsService) CreateCommitDiscussion(pid any, commit string, opt *CreateCommitDiscussionOptions, options ...RequestOptionFunc) (*Discussion, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -1090,10 +1116,6 @@ type AddCommitDiscussionNoteOptions struct {
 	CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
 }
 
-// AddCommitDiscussionNote creates a new discussion to a single project commit.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#add-note-to-existing-commit-thread
 func (s *DiscussionsService) AddCommitDiscussionNote(pid any, commit string, discussion string, opt *AddCommitDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -1129,10 +1151,6 @@ type UpdateCommitDiscussionNoteOptions struct {
 	CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
 }
 
-// UpdateCommitDiscussionNote modifies existing discussion of a commit.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#modify-an-existing-commit-thread-note
 func (s *DiscussionsService) UpdateCommitDiscussionNote(pid any, commit string, discussion string, note int, opt *UpdateCommitDiscussionNoteOptions, options ...RequestOptionFunc) (*Note, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -1159,10 +1177,6 @@ func (s *DiscussionsService) UpdateCommitDiscussionNote(pid any, commit string,
 	return n, resp, nil
 }
 
-// DeleteCommitDiscussionNote deletes an existing discussion of an commit.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/discussions/#delete-a-commit-thread-note
 func (s *DiscussionsService) DeleteCommitDiscussionNote(pid any, commit string, discussion string, note int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/dockerfile_templates.go b/vendor/gitlab.com/gitlab-org/api/client-go/dockerfile_templates.go
index 0885b3b88..045b00a56 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/dockerfile_templates.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/dockerfile_templates.go
@@ -25,7 +25,16 @@ import (
 type (
 	// DockerfileTemplatesServiceInterface defines all the API methods for the DockerfileTemplatesService
 	DockerfileTemplatesServiceInterface interface {
+		// ListTemplates get a list of available Dockerfile templates.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/templates/dockerfiles/#list-dockerfile-templates
 		ListTemplates(opt *ListDockerfileTemplatesOptions, options ...RequestOptionFunc) ([]*DockerfileTemplateListItem, *Response, error)
+
+		// GetTemplate get a single Dockerfile template.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/templates/dockerfiles/#single-dockerfile-template
 		GetTemplate(key string, options ...RequestOptionFunc) (*DockerfileTemplate, *Response, error)
 	}
 
@@ -62,10 +71,6 @@ type DockerfileTemplateListItem struct {
 // https://docs.gitlab.com/api/templates/dockerfiles/#list-dockerfile-templates
 type ListDockerfileTemplatesOptions ListOptions
 
-// ListTemplates get a list of available Dockerfile templates.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/templates/dockerfiles/#list-dockerfile-templates
 func (s *DockerfileTemplatesService) ListTemplates(opt *ListDockerfileTemplatesOptions, options ...RequestOptionFunc) ([]*DockerfileTemplateListItem, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodGet, "templates/dockerfiles", opt, options)
 	if err != nil {
@@ -81,10 +86,6 @@ func (s *DockerfileTemplatesService) ListTemplates(opt *ListDockerfileTemplatesO
 	return gs, resp, nil
 }
 
-// GetTemplate get a single Dockerfile template.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/templates/dockerfiles/#single-dockerfile-template
 func (s *DockerfileTemplatesService) GetTemplate(key string, options ...RequestOptionFunc) (*DockerfileTemplate, *Response, error) {
 	u := fmt.Sprintf("templates/dockerfiles/%s", url.PathEscape(key))
 
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/dora_metrics.go b/vendor/gitlab.com/gitlab-org/api/client-go/dora_metrics.go
index 21b4d0eba..8f8382410 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/dora_metrics.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/dora_metrics.go
@@ -24,14 +24,23 @@ import (
 type (
 	// DORAMetricsServiceInterface defines all the API methods for the DORAMetricsService
 	DORAMetricsServiceInterface interface {
+		// GetProjectDORAMetrics gets the DORA metrics for a project.
+		//
+		// GitLab API Docs:
+		// https://docs.gitlab.com/api/dora/metrics/#get-project-level-dora-metrics
 		GetProjectDORAMetrics(pid any, opt GetDORAMetricsOptions, options ...RequestOptionFunc) ([]DORAMetric, *Response, error)
+
+		// GetGroupDORAMetrics gets the DORA metrics for a group.
+		//
+		// GitLab API Docs:
+		// https://docs.gitlab.com/api/dora/metrics/#get-group-level-dora-metrics
 		GetGroupDORAMetrics(gid any, opt GetDORAMetricsOptions, options ...RequestOptionFunc) ([]DORAMetric, *Response, error)
 	}
 
 	// DORAMetricsService handles communication with the DORA metrics related methods
 	// of the GitLab API.
 	//
-	// Gitlab API docs: https://docs.gitlab.com/api/dora/metrics/
+	// GitLab API docs: https://docs.gitlab.com/api/dora/metrics/
 	DORAMetricsService struct {
 		client *Client
 	}
@@ -41,13 +50,13 @@ var _ DORAMetricsServiceInterface = (*DORAMetricsService)(nil)
 
 // DORAMetric represents a single DORA metric data point.
 //
-// Gitlab API docs: https://docs.gitlab.com/api/dora/metrics/
+// GitLab API docs: https://docs.gitlab.com/api/dora/metrics/
 type DORAMetric struct {
 	Date  string  `json:"date"`
 	Value float64 `json:"value"`
 }
 
-// Gets a string representation of a DORAMetric data point
+// String gets a string representation of a DORAMetric data point
 //
 // GitLab API docs: https://docs.gitlab.com/api/dora/metrics/
 func (m DORAMetric) String() string {
@@ -66,10 +75,6 @@ type GetDORAMetricsOptions struct {
 	StartDate        *ISOTime            `url:"start_date,omitempty" json:"start_date,omitempty"`
 }
 
-// GetProjectDORAMetrics gets the DORA metrics for a project.
-//
-// GitLab API Docs:
-// https://docs.gitlab.com/api/dora/metrics/#get-project-level-dora-metrics
 func (s *DORAMetricsService) GetProjectDORAMetrics(pid any, opt GetDORAMetricsOptions, options ...RequestOptionFunc) ([]DORAMetric, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -91,10 +96,6 @@ func (s *DORAMetricsService) GetProjectDORAMetrics(pid any, opt GetDORAMetricsOp
 	return metrics, resp, err
 }
 
-// GetGroupDORAMetrics gets the DORA metrics for a group.
-//
-// GitLab API Docs:
-// https://docs.gitlab.com/api/dora/metrics/#get-group-level-dora-metrics
 func (s *DORAMetricsService) GetGroupDORAMetrics(gid any, opt GetDORAMetricsOptions, options ...RequestOptionFunc) ([]DORAMetric, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/draft_notes.go b/vendor/gitlab.com/gitlab-org/api/client-go/draft_notes.go
index 452096562..9ab6c50cb 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/draft_notes.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/draft_notes.go
@@ -24,12 +24,46 @@ import (
 type (
 	// DraftNotesServiceInterface defines all the API methods for the DraftNotesService
 	DraftNotesServiceInterface interface {
+		// ListDraftNotes gets a list of all draft notes for a merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/draft_notes/#list-all-merge-request-draft-notes
 		ListDraftNotes(pid any, mergeRequest int, opt *ListDraftNotesOptions, options ...RequestOptionFunc) ([]*DraftNote, *Response, error)
+
+		// GetDraftNote gets a single draft note for a merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/draft_notes/#get-a-single-draft-note
 		GetDraftNote(pid any, mergeRequest int, note int, options ...RequestOptionFunc) (*DraftNote, *Response, error)
+
+		// CreateDraftNote creates a draft note for a merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/draft_notes/#create-a-draft-note
 		CreateDraftNote(pid any, mergeRequest int, opt *CreateDraftNoteOptions, options ...RequestOptionFunc) (*DraftNote, *Response, error)
+
+		// UpdateDraftNote updates a draft note for a merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/draft_notes/#update-a-draft-note
 		UpdateDraftNote(pid any, mergeRequest int, note int, opt *UpdateDraftNoteOptions, options ...RequestOptionFunc) (*DraftNote, *Response, error)
+
+		// DeleteDraftNote deletes a single draft note for a merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/draft_notes/#delete-a-draft-note
 		DeleteDraftNote(pid any, mergeRequest int, note int, options ...RequestOptionFunc) (*Response, error)
+
+		// PublishDraftNote publishes a single draft note for a merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/draft_notes/#publish-a-draft-note
 		PublishDraftNote(pid any, mergeRequest int, note int, options ...RequestOptionFunc) (*Response, error)
+
+		// PublishAllDraftNotes publishes all draft notes for a merge request that belong to the user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/draft_notes/#publish-a-draft-note
 		PublishAllDraftNotes(pid any, mergeRequest int, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -68,10 +102,6 @@ type ListDraftNotesOptions struct {
 	Sort    *string `url:"sort,omitempty" json:"sort,omitempty"`
 }
 
-// ListDraftNotes gets a list of all draft notes for a merge request.
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/draft_notes/#list-all-merge-request-draft-notes
 func (s *DraftNotesService) ListDraftNotes(pid any, mergeRequest int, opt *ListDraftNotesOptions, options ...RequestOptionFunc) ([]*DraftNote, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -93,10 +123,6 @@ func (s *DraftNotesService) ListDraftNotes(pid any, mergeRequest int, opt *ListD
 	return n, resp, nil
 }
 
-// GetDraftNote gets a single draft note for a merge request.
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/draft_notes/#get-a-single-draft-note
 func (s *DraftNotesService) GetDraftNote(pid any, mergeRequest int, note int, options ...RequestOptionFunc) (*DraftNote, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -121,7 +147,7 @@ func (s *DraftNotesService) GetDraftNote(pid any, mergeRequest int, note int, op
 // CreateDraftNoteOptions represents the available CreateDraftNote()
 // options.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/draft_notes/#create-a-draft-note
 type CreateDraftNoteOptions struct {
 	Note                  *string          `url:"note" json:"note"`
@@ -131,10 +157,6 @@ type CreateDraftNoteOptions struct {
 	Position              *PositionOptions `url:"position,omitempty" json:"position,omitempty"`
 }
 
-// CreateDraftNote creates a draft note for a merge request.
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/draft_notes/#create-a-draft-note
 func (s *DraftNotesService) CreateDraftNote(pid any, mergeRequest int, opt *CreateDraftNoteOptions, options ...RequestOptionFunc) (*DraftNote, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -159,16 +181,13 @@ func (s *DraftNotesService) CreateDraftNote(pid any, mergeRequest int, opt *Crea
 // UpdateDraftNoteOptions represents the available UpdateDraftNote()
 // options.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/draft_notes/#create-a-draft-note
 type UpdateDraftNoteOptions struct {
 	Note     *string          `url:"note,omitempty" json:"note,omitempty"`
 	Position *PositionOptions `url:"position,omitempty" json:"position,omitempty"`
 }
 
-// UpdateDraftNote updates a draft note for a merge request.
-//
-// Gitlab API docs: https://docs.gitlab.com/api/draft_notes/#create-a-draft-note
 func (s *DraftNotesService) UpdateDraftNote(pid any, mergeRequest int, note int, opt *UpdateDraftNoteOptions, options ...RequestOptionFunc) (*DraftNote, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -190,10 +209,6 @@ func (s *DraftNotesService) UpdateDraftNote(pid any, mergeRequest int, note int,
 	return n, resp, nil
 }
 
-// DeleteDraftNote deletes a single draft note for a merge request.
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/draft_notes/#delete-a-draft-note
 func (s *DraftNotesService) DeleteDraftNote(pid any, mergeRequest int, note int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -209,10 +224,6 @@ func (s *DraftNotesService) DeleteDraftNote(pid any, mergeRequest int, note int,
 	return s.client.Do(req, nil)
 }
 
-// PublishDraftNote publishes a single draft note for a merge request.
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/draft_notes/#publish-a-draft-note
 func (s *DraftNotesService) PublishDraftNote(pid any, mergeRequest int, note int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -228,10 +239,6 @@ func (s *DraftNotesService) PublishDraftNote(pid any, mergeRequest int, note int
 	return s.client.Do(req, nil)
 }
 
-// PublishAllDraftNotes publishes all draft notes for a merge request that belong to the user.
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/draft_notes/#publish-a-draft-note
 func (s *DraftNotesService) PublishAllDraftNotes(pid any, mergeRequest int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/enterprise_users.go b/vendor/gitlab.com/gitlab-org/api/client-go/enterprise_users.go
index e50b5c511..03771bc0a 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/enterprise_users.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/enterprise_users.go
@@ -22,9 +22,30 @@ import (
 
 type (
 	EnterpriseUsersServiceInterface interface {
+		// ListEnterpriseUsers lists all enterprise users for a given top-level group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_enterprise_users/#list-all-enterprise-users
 		ListEnterpriseUsers(gid any, opt *ListEnterpriseUsersOptions, options ...RequestOptionFunc) ([]*User, *Response, error)
+
+		// GetEnterpriseUser gets details on a specified enterprise user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_enterprise_users/#get-details-on-an-enterprise-user
 		GetEnterpriseUser(gid any, uid int, options ...RequestOptionFunc) (*User, *Response, error)
+
+		// Disable2FAForEnterpriseUser disables two-factor authentication (2FA) for a
+		// specified enterprise user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_enterprise_users/#disable-two-factor-authentication-for-an-enterprise-user
 		Disable2FAForEnterpriseUser(gid any, uid int, options ...RequestOptionFunc) (*Response, error)
+
+		// DeleteEnterpriseUser deletes an specified enterprise user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_enterprise_users/#delete-an-enterprise-user
+		DeleteEnterpriseUser(gid any, uid int, deleteOptions *DeleteEnterpriseUserOptions, options ...RequestOptionFunc) (*Response, error)
 	}
 
 	// EnterpriseUsersService handles communication with the enterprise users
@@ -54,10 +75,6 @@ type ListEnterpriseUsersOptions struct {
 	TwoFactor     string     `url:"two_factor,omitempty" json:"two_factor,omitempty"`
 }
 
-// ListEnterpriseUsers lists all enterprise users for a given top-level group.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/group_enterprise_users/#list-all-enterprise-users
 func (s *EnterpriseUsersService) ListEnterpriseUsers(gid any, opt *ListEnterpriseUsersOptions, options ...RequestOptionFunc) ([]*User, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -79,10 +96,6 @@ func (s *EnterpriseUsersService) ListEnterpriseUsers(gid any, opt *ListEnterpris
 	return users, resp, nil
 }
 
-// GetEnterpriseUser gets details on a specified enterprise user.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/group_enterprise_users/#get-details-on-an-enterprise-user
 func (s *EnterpriseUsersService) GetEnterpriseUser(gid any, uid int, options ...RequestOptionFunc) (*User, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -104,11 +117,6 @@ func (s *EnterpriseUsersService) GetEnterpriseUser(gid any, uid int, options ...
 	return user, resp, nil
 }
 
-// Disable2FAForEnterpriseUser disables two-factor authentication (2FA) for a
-// specified enterprise user.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/group_enterprise_users/#disable-two-factor-authentication-for-an-enterprise-user
 func (s *EnterpriseUsersService) Disable2FAForEnterpriseUser(gid any, uid int, options ...RequestOptionFunc) (*Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -123,3 +131,26 @@ func (s *EnterpriseUsersService) Disable2FAForEnterpriseUser(gid any, uid int, o
 
 	return s.client.Do(req, nil)
 }
+
+// DeleteEnterpriseUserOptions represents the available DeleteEnterpriseUser options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_enterprise_users/#delete-an-enterprise-user
+type DeleteEnterpriseUserOptions struct {
+	HardDelete *bool `url:"hard_delete,omitempty" json:"hard_delete,omitempty"`
+}
+
+func (s *EnterpriseUsersService) DeleteEnterpriseUser(gid any, uid int, opt *DeleteEnterpriseUserOptions, options ...RequestOptionFunc) (*Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("groups/%s/enterprise_users/%d", PathEscape(group), uid)
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, opt, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/environments.go b/vendor/gitlab.com/gitlab-org/api/client-go/environments.go
index 9d8f9fedc..7d5936c2c 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/environments.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/environments.go
@@ -25,11 +25,43 @@ import (
 type (
 	// EnvironmentsServiceInterface defines all the API methods for the EnvironmentsService
 	EnvironmentsServiceInterface interface {
+		// ListEnvironments gets a list of environments from a project, sorted by name
+		// alphabetically.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/environments/#list-environments
 		ListEnvironments(pid any, opts *ListEnvironmentsOptions, options ...RequestOptionFunc) ([]*Environment, *Response, error)
+
+		// GetEnvironment gets a specific environment from a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/environments/#get-a-specific-environment
 		GetEnvironment(pid any, environment int, options ...RequestOptionFunc) (*Environment, *Response, error)
+
+		// CreateEnvironment adds an environment to a project. This method is idempotent
+		// and can be called multiple times with the same parameters. Creating an environment
+		// that already exists does not affect the existing association.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/environments/#create-a-new-environment
 		CreateEnvironment(pid any, opt *CreateEnvironmentOptions, options ...RequestOptionFunc) (*Environment, *Response, error)
+
+		// EditEnvironment updates a project team environment to a specified access level.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/environments/#update-an-existing-environment
 		EditEnvironment(pid any, environment int, opt *EditEnvironmentOptions, options ...RequestOptionFunc) (*Environment, *Response, error)
+
+		// DeleteEnvironment removes an environment from a project team.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/environments/#delete-an-environment
 		DeleteEnvironment(pid any, environment int, options ...RequestOptionFunc) (*Response, error)
+
+		// StopEnvironment stops an environment within a specific project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/environments/#stop-an-environment
 		StopEnvironment(pid any, environmentID int, opt *StopEnvironmentOptions, options ...RequestOptionFunc) (*Environment, *Response, error)
 	}
 
@@ -81,11 +113,6 @@ type ListEnvironmentsOptions struct {
 	States *string `url:"states,omitempty" json:"states,omitempty"`
 }
 
-// ListEnvironments gets a list of environments from a project, sorted by name
-// alphabetically.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/environments/#list-environments
 func (s *EnvironmentsService) ListEnvironments(pid any, opts *ListEnvironmentsOptions, options ...RequestOptionFunc) ([]*Environment, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -107,10 +134,6 @@ func (s *EnvironmentsService) ListEnvironments(pid any, opts *ListEnvironmentsOp
 	return envs, resp, nil
 }
 
-// GetEnvironment gets a specific environment from a project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/environments/#get-a-specific-environment
 func (s *EnvironmentsService) GetEnvironment(pid any, environment int, options ...RequestOptionFunc) (*Environment, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -147,12 +170,6 @@ type CreateEnvironmentOptions struct {
 	AutoStopSetting     *string `url:"auto_stop_setting,omitempty" json:"auto_stop_setting,omitempty"`
 }
 
-// CreateEnvironment adds an environment to a project. This method is idempotent
-// and can be called multiple times with the same parameters. Creating an environment
-// that already exists does not affect the existing association.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/environments/#create-a-new-environment
 func (s *EnvironmentsService) CreateEnvironment(pid any, opt *CreateEnvironmentOptions, options ...RequestOptionFunc) (*Environment, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -189,10 +206,6 @@ type EditEnvironmentOptions struct {
 	AutoStopSetting     *string `url:"auto_stop_setting,omitempty" json:"auto_stop_setting,omitempty"`
 }
 
-// EditEnvironment updates a project team environment to a specified access level..
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/environments/#update-an-existing-environment
 func (s *EnvironmentsService) EditEnvironment(pid any, environment int, opt *EditEnvironmentOptions, options ...RequestOptionFunc) (*Environment, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -214,10 +227,6 @@ func (s *EnvironmentsService) EditEnvironment(pid any, environment int, opt *Edi
 	return env, resp, nil
 }
 
-// DeleteEnvironment removes an environment from a project team.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/environments/#delete-an-environment
 func (s *EnvironmentsService) DeleteEnvironment(pid any, environment int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -241,10 +250,6 @@ type StopEnvironmentOptions struct {
 	Force *bool `url:"force,omitempty" json:"force,omitempty"`
 }
 
-// StopEnvironment stops an environment within a specific project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/environments/#stop-an-environment
 func (s *EnvironmentsService) StopEnvironment(pid any, environmentID int, opt *StopEnvironmentOptions, options ...RequestOptionFunc) (*Environment, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/epic_issues.go b/vendor/gitlab.com/gitlab-org/api/client-go/epic_issues.go
index 625d86c11..7325a71c6 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/epic_issues.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/epic_issues.go
@@ -26,12 +26,32 @@ type (
 	// Will be removed in v5 of the API, use Work Items API instead
 	EpicIssuesServiceInterface interface {
 		// Will be removed in v5 of the API, use Work Items API instead
+		// ListEpicIssues get a list of epic issues.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/epic_issues/#list-issues-for-an-epic
 		ListEpicIssues(gid any, epic int, opt *ListOptions, options ...RequestOptionFunc) ([]*Issue, *Response, error)
+
+		// AssignEpicIssue assigns an existing issue to an epic.
 		// Will be removed in v5 of the API, use Work Items API instead
+		//
+		// GitLab API Docs:
+		// https://docs.gitlab.com/api/epic_issues/#assign-an-issue-to-the-epic
 		AssignEpicIssue(gid any, epic, issue int, options ...RequestOptionFunc) (*EpicIssueAssignment, *Response, error)
+
+		// RemoveEpicIssue removes an issue from an epic.
 		// Will be removed in v5 of the API, use Work Items API instead
+		//
+		// GitLab API Docs:
+		// https://docs.gitlab.com/api/epic_issues/#remove-an-issue-from-the-epic
 		RemoveEpicIssue(gid any, epic, epicIssue int, options ...RequestOptionFunc) (*EpicIssueAssignment, *Response, error)
+
+		// UpdateEpicIssueAssignmentOptions describes the UpdateEpicIssueAssignment()
+		// options.
 		// Will be removed in v5 of the API, use Work Items API instead
+		//
+		// GitLab API Docs:
+		// https://docs.gitlab.com/api/epic_issues/#update-epic---issue-association
 		UpdateEpicIssueAssignment(gid any, epic, epicIssue int, opt *UpdateEpicIssueAssignmentOptions, options ...RequestOptionFunc) ([]*Issue, *Response, error)
 	}
 
@@ -49,7 +69,7 @@ type (
 var _ EpicIssuesServiceInterface = (*EpicIssuesService)(nil)
 
 // EpicIssueAssignment contains both the epic and issue objects returned from
-// Gitlab with the assignment ID.
+// GitLab with the assignment ID.
 // Will be removed in v5 of the API, use Work Items API instead
 //
 // GitLab API docs: https://docs.gitlab.com/api/epic_issues/
@@ -59,11 +79,6 @@ type EpicIssueAssignment struct {
 	Issue *Issue `json:"issue"`
 }
 
-// ListEpicIssues get a list of epic issues.
-// Will be removed in v5 of the API, use Work Items API instead
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/epic_issues/#list-issues-for-an-epic
 func (s *EpicIssuesService) ListEpicIssues(gid any, epic int, opt *ListOptions, options ...RequestOptionFunc) ([]*Issue, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -85,11 +100,6 @@ func (s *EpicIssuesService) ListEpicIssues(gid any, epic int, opt *ListOptions,
 	return is, resp, nil
 }
 
-// AssignEpicIssue assigns an existing issue to an epic.
-// Will be removed in v5 of the API, use Work Items API instead
-//
-// Gitlab API Docs:
-// https://docs.gitlab.com/api/epic_issues/#assign-an-issue-to-the-epic
 func (s *EpicIssuesService) AssignEpicIssue(gid any, epic, issue int, options ...RequestOptionFunc) (*EpicIssueAssignment, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -111,11 +121,6 @@ func (s *EpicIssuesService) AssignEpicIssue(gid any, epic, issue int, options ..
 	return a, resp, nil
 }
 
-// RemoveEpicIssue removes an issue from an epic.
-// Will be removed in v5 of the API, use Work Items API instead
-//
-// Gitlab API Docs:
-// https://docs.gitlab.com/api/epic_issues/#remove-an-issue-from-the-epic
 func (s *EpicIssuesService) RemoveEpicIssue(gid any, epic, epicIssue int, options ...RequestOptionFunc) (*EpicIssueAssignment, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -137,12 +142,6 @@ func (s *EpicIssuesService) RemoveEpicIssue(gid any, epic, epicIssue int, option
 	return a, resp, nil
 }
 
-// UpdateEpicIssueAssignmentOptions describes the UpdateEpicIssueAssignment()
-// options.
-// Will be removed in v5 of the API, use Work Items API instead
-//
-// Gitlab API Docs:
-// https://docs.gitlab.com/api/epic_issues/#update-epic---issue-association
 type UpdateEpicIssueAssignmentOptions struct {
 	*ListOptions
 	MoveBeforeID *int `url:"move_before_id,omitempty" json:"move_before_id,omitempty"`
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/epics.go b/vendor/gitlab.com/gitlab-org/api/client-go/epics.go
index df28cc032..ca194b587 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/epics.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/epics.go
@@ -26,17 +26,42 @@ type (
 	// EpicsServiceInterface defines all the API methods for the EpicsService
 	// Will be removed in v5 of the API, use Work Items API instead
 	EpicsServiceInterface interface {
+		// ListGroupEpics gets a list of group epics. This function accepts pagination
+		// parameters page and per_page to return the list of group epics.
 		// Will be removed in v5 of the API, use Work Items API instead
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/epics/#list-epics-for-a-group
 		ListGroupEpics(gid any, opt *ListGroupEpicsOptions, options ...RequestOptionFunc) ([]*Epic, *Response, error)
+
+		// GetEpic gets a single group epic.
 		// Will be removed in v5 of the API, use Work Items API instead
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/epics/#single-epic
 		GetEpic(gid any, epic int, options ...RequestOptionFunc) (*Epic, *Response, error)
+
+		// GetEpicLinks gets all child epics of an epic.
 		// Will be removed in v5 of the API, use Work Items API instead
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/epic_links/
 		GetEpicLinks(gid any, epic int, options ...RequestOptionFunc) ([]*Epic, *Response, error)
+
+		// CreateEpic creates a new group epic.
 		// Will be removed in v5 of the API, use Work Items API instead
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/epics/#new-epic
 		CreateEpic(gid any, opt *CreateEpicOptions, options ...RequestOptionFunc) (*Epic, *Response, error)
+
+		// UpdateEpic updates an existing group epic. This function is also used
+		// to mark an epic as closed.
 		// Will be removed in v5 of the API, use Work Items API instead
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/epics/#update-epic
 		UpdateEpic(gid any, epic int, opt *UpdateEpicOptions, options ...RequestOptionFunc) (*Epic, *Response, error)
+
+		// DeleteEpic deletes a single group epic.
 		// Will be removed in v5 of the API, use Work Items API instead
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/epics/#delete-epic
 		DeleteEpic(gid any, epic int, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -97,6 +122,8 @@ type Epic struct {
 	URL                     string      `json:"url"`
 }
 
+// String gets a string representation of an Epic.
+//
 // Will be removed in v5 of the API, use Work Items API instead
 func (e Epic) String() string {
 	return Stringify(e)
@@ -124,11 +151,6 @@ type ListGroupEpicsOptions struct {
 	MyReactionEmoji         *string       `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"`
 }
 
-// ListGroupEpics gets a list of group epics. This function accepts pagination
-// parameters page and per_page to return the list of group epics.
-// Will be removed in v5 of the API, use Work Items API instead
-//
-// GitLab API docs: https://docs.gitlab.com/api/epics/#list-epics-for-a-group
 func (s *EpicsService) ListGroupEpics(gid any, opt *ListGroupEpicsOptions, options ...RequestOptionFunc) ([]*Epic, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -150,10 +172,6 @@ func (s *EpicsService) ListGroupEpics(gid any, opt *ListGroupEpicsOptions, optio
 	return es, resp, nil
 }
 
-// GetEpic gets a single group epic.
-// Will be removed in v5 of the API, use Work Items API instead
-//
-// GitLab API docs: https://docs.gitlab.com/api/epics/#single-epic
 func (s *EpicsService) GetEpic(gid any, epic int, options ...RequestOptionFunc) (*Epic, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -175,10 +193,6 @@ func (s *EpicsService) GetEpic(gid any, epic int, options ...RequestOptionFunc)
 	return e, resp, nil
 }
 
-// GetEpicLinks gets all child epics of an epic.
-// Will be removed in v5 of the API, use Work Items API instead
-//
-// GitLab API docs: https://docs.gitlab.com/api/epic_links/
 func (s *EpicsService) GetEpicLinks(gid any, epic int, options ...RequestOptionFunc) ([]*Epic, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -218,10 +232,6 @@ type CreateEpicOptions struct {
 	ParentID         *int          `url:"parent_id,omitempty" json:"parent_id,omitempty"`
 }
 
-// CreateEpic creates a new group epic.
-// Will be removed in v5 of the API, use Work Items API instead
-//
-// GitLab API docs: https://docs.gitlab.com/api/epics/#new-epic
 func (s *EpicsService) CreateEpic(gid any, opt *CreateEpicOptions, options ...RequestOptionFunc) (*Epic, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -264,11 +274,6 @@ type UpdateEpicOptions struct {
 	Color            *string       `url:"color,omitempty" json:"color,omitempty"`
 }
 
-// UpdateEpic updates an existing group epic. This function is also used
-// to mark an epic as closed.
-// Will be removed in v5 of the API, use Work Items API instead
-//
-// GitLab API docs: https://docs.gitlab.com/api/epics/#update-epic
 func (s *EpicsService) UpdateEpic(gid any, epic int, opt *UpdateEpicOptions, options ...RequestOptionFunc) (*Epic, *Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
@@ -290,10 +295,6 @@ func (s *EpicsService) UpdateEpic(gid any, epic int, opt *UpdateEpicOptions, opt
 	return e, resp, nil
 }
 
-// DeleteEpic deletes a single group epic.
-// Will be removed in v5 of the API, use Work Items API instead
-//
-// GitLab API docs: https://docs.gitlab.com/api/epics/#delete-epic
 func (s *EpicsService) DeleteEpic(gid any, epic int, options ...RequestOptionFunc) (*Response, error) {
 	group, err := parseID(gid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/error_tracking.go b/vendor/gitlab.com/gitlab-org/api/client-go/error_tracking.go
index 14de8ea88..a53adc37c 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/error_tracking.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/error_tracking.go
@@ -24,10 +24,35 @@ import (
 type (
 	// ErrorTrackingServiceInterface defines all the API methods for the ErrorTrackingService
 	ErrorTrackingServiceInterface interface {
+		// GetErrorTrackingSettings gets error tracking settings.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/error_tracking/#get-error-tracking-settings
 		GetErrorTrackingSettings(pid any, options ...RequestOptionFunc) (*ErrorTrackingSettings, *Response, error)
+
+		// EnableDisableErrorTracking allows you to enable or disable the error tracking
+		// settings for a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/error_tracking/#enable-or-disable-the-error-tracking-project-settings
 		EnableDisableErrorTracking(pid any, opt *EnableDisableErrorTrackingOptions, options ...RequestOptionFunc) (*ErrorTrackingSettings, *Response, error)
+
+		// ListClientKeys lists error tracking project client keys.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/error_tracking/#list-project-client-keys
 		ListClientKeys(pid any, opt *ListClientKeysOptions, options ...RequestOptionFunc) ([]*ErrorTrackingClientKey, *Response, error)
+
+		// CreateClientKey creates a new client key for a project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/error_tracking/#create-a-client-key
 		CreateClientKey(pid any, options ...RequestOptionFunc) (*ErrorTrackingClientKey, *Response, error)
+
+		// DeleteClientKey removes a client key from the project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/error_tracking/#delete-a-client-key
 		DeleteClientKey(pid any, keyID int, options ...RequestOptionFunc) (*Response, error)
 	}
 
@@ -72,10 +97,6 @@ func (p ErrorTrackingSettings) String() string {
 	return Stringify(p)
 }
 
-// GetErrorTrackingSettings gets error tracking settings.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/error_tracking/#get-error-tracking-settings
 func (s *ErrorTrackingService) GetErrorTrackingSettings(pid any, options ...RequestOptionFunc) (*ErrorTrackingSettings, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -107,11 +128,6 @@ type EnableDisableErrorTrackingOptions struct {
 	Integrated *bool `url:"integrated,omitempty" json:"integrated,omitempty"`
 }
 
-// EnableDisableErrorTracking allows you to enable or disable the error tracking
-// settings for a project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/error_tracking/#enable-or-disable-the-error-tracking-project-settings
 func (s *ErrorTrackingService) EnableDisableErrorTracking(pid any, opt *EnableDisableErrorTrackingOptions, options ...RequestOptionFunc) (*ErrorTrackingSettings, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -139,10 +155,6 @@ func (s *ErrorTrackingService) EnableDisableErrorTracking(pid any, opt *EnableDi
 // https://docs.gitlab.com/api/error_tracking/#list-project-client-keys
 type ListClientKeysOptions ListOptions
 
-// ListClientKeys lists error tracking project client keys.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/error_tracking/#list-project-client-keys
 func (s *ErrorTrackingService) ListClientKeys(pid any, opt *ListClientKeysOptions, options ...RequestOptionFunc) ([]*ErrorTrackingClientKey, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -164,10 +176,6 @@ func (s *ErrorTrackingService) ListClientKeys(pid any, opt *ListClientKeysOption
 	return cks, resp, nil
 }
 
-// CreateClientKey creates a new client key for a project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/error_tracking/#create-a-client-key
 func (s *ErrorTrackingService) CreateClientKey(pid any, options ...RequestOptionFunc) (*ErrorTrackingClientKey, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -189,10 +197,6 @@ func (s *ErrorTrackingService) CreateClientKey(pid any, options ...RequestOption
 	return ck, resp, nil
 }
 
-// DeleteClientKey removes a client key from the project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/error_tracking/#delete-a-client-key
 func (s *ErrorTrackingService) DeleteClientKey(pid any, keyID int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/event_parsing.go b/vendor/gitlab.com/gitlab-org/api/client-go/event_parsing.go
index cae15b4bc..577972e9a 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/event_parsing.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/event_parsing.go
@@ -22,7 +22,7 @@ import (
 	"net/http"
 )
 
-// EventType represents a Gitlab event type.
+// EventType represents a GitLab event type.
 type EventType string
 
 // List of available event types.
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/event_webhook_types.go b/vendor/gitlab.com/gitlab-org/api/client-go/event_webhook_types.go
index 4c9eaad45..57983cf73 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/event_webhook_types.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/event_webhook_types.go
@@ -336,8 +336,8 @@ type IssueEvent struct {
 		AssigneeID          int      `json:"assignee_id"`
 		AuthorID            int      `json:"author_id"`
 		ProjectID           int      `json:"project_id"`
-		CreatedAt           string   `json:"created_at"` // Should be *time.Time (see Gitlab issue #21468)
-		UpdatedAt           string   `json:"updated_at"` // Should be *time.Time (see Gitlab issue #21468)
+		CreatedAt           string   `json:"created_at"` // Should be *time.Time (see GitLab issue #21468)
+		UpdatedAt           string   `json:"updated_at"` // Should be *time.Time (see GitLab issue #21468)
 		UpdatedByID         int      `json:"updated_by_id"`
 		LastEditedAt        string   `json:"last_edited_at"`
 		LastEditedByID      int      `json:"last_edited_by_id"`
@@ -645,8 +645,8 @@ type MergeEvent struct {
 		AssigneeIDs              []int        `json:"assignee_ids"`
 		ReviewerIDs              []int        `json:"reviewer_ids"`
 		Title                    string       `json:"title"`
-		CreatedAt                string       `json:"created_at"` // Should be *time.Time (see Gitlab issue #21468)
-		UpdatedAt                string       `json:"updated_at"` // Should be *time.Time (see Gitlab issue #21468)
+		CreatedAt                string       `json:"created_at"` // Should be *time.Time (see GitLab issue #21468)
+		UpdatedAt                string       `json:"updated_at"` // Should be *time.Time (see GitLab issue #21468)
 		StCommits                []*Commit    `json:"st_commits"`
 		StDiffs                  []*Diff      `json:"st_diffs"`
 		LastEditedAt             string       `json:"last_edited_at"`
@@ -986,6 +986,7 @@ type PushEvent struct {
 	Before       string `json:"before"`
 	After        string `json:"after"`
 	Ref          string `json:"ref"`
+	RefProtected bool   `json:"ref_protected"`
 	CheckoutSHA  string `json:"checkout_sha"`
 	UserID       int    `json:"user_id"`
 	UserName     string `json:"user_name"`
@@ -1034,11 +1035,11 @@ type PushEvent struct {
 // https://docs.gitlab.com/user/project/integrations/webhook_events/#release-events
 type ReleaseEvent struct {
 	ID          int    `json:"id"`
-	CreatedAt   string `json:"created_at"` // Should be *time.Time (see Gitlab issue #21468)
+	CreatedAt   string `json:"created_at"` // Should be *time.Time (see GitLab issue #21468)
 	Description string `json:"description"`
 	Name        string `json:"name"`
 	Tag         string `json:"tag"`
-	ReleasedAt  string `json:"released_at"` // Should be *time.Time (see Gitlab issue #21468)
+	ReleasedAt  string `json:"released_at"` // Should be *time.Time (see GitLab issue #21468)
 	ObjectKind  string `json:"object_kind"`
 	Project     struct {
 		ID                int     `json:"id"`
@@ -1078,7 +1079,7 @@ type ReleaseEvent struct {
 		ID        string `json:"id"`
 		Message   string `json:"message"`
 		Title     string `json:"title"`
-		Timestamp string `json:"timestamp"` // Should be *time.Time (see Gitlab issue #21468)
+		Timestamp string `json:"timestamp"` // Should be *time.Time (see GitLab issue #21468)
 		URL       string `json:"url"`
 		Author    struct {
 			Name  string `json:"name"`
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/events.go b/vendor/gitlab.com/gitlab-org/api/client-go/events.go
index a11d77cb2..2e8f130d7 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/events.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/events.go
@@ -25,7 +25,17 @@ import (
 type (
 	// EventsServiceInterface defines all the API methods for the EventsService
 	EventsServiceInterface interface {
+		// ListCurrentUserContributionEvents  retrieves all events
+		// for the currently authenticated user.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/events/#list-all-events
 		ListCurrentUserContributionEvents(opt *ListContributionEventsOptions, options ...RequestOptionFunc) ([]*ContributionEvent, *Response, error)
+
+		// ListProjectVisibleEvents gets the events for the specified project.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/events/#list-a-projects-visible-events
 		ListProjectVisibleEvents(pid any, opt *ListProjectVisibleEventsOptions, options ...RequestOptionFunc) ([]*ProjectEvent, *Response, error)
 	}
 
@@ -78,7 +88,7 @@ type ContributionEvent struct {
 
 // ListContributionEventsOptions represents the options for GetUserContributionEvents
 //
-// GitLap API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/events/#get-user-contribution-events
 type ListContributionEventsOptions struct {
 	ListOptions
@@ -89,11 +99,6 @@ type ListContributionEventsOptions struct {
 	Sort       *string               `url:"sort,omitempty" json:"sort,omitempty"`
 }
 
-// ListUserContributionEvents retrieves user contribution events
-// for the specified user, sorted from newest to oldest.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/events/#get-user-contribution-events
 func (s *UsersService) ListUserContributionEvents(uid any, opt *ListContributionEventsOptions, options ...RequestOptionFunc) ([]*ContributionEvent, *Response, error) {
 	user, err := parseID(uid)
 	if err != nil {
@@ -115,9 +120,6 @@ func (s *UsersService) ListUserContributionEvents(uid any, opt *ListContribution
 	return cs, resp, nil
 }
 
-// ListCurrentUserContributionEvents gets a list currently authenticated user's events
-//
-// GitLab API docs: https://docs.gitlab.com/api/events/#list-currently-authenticated-users-events
 func (s *EventsService) ListCurrentUserContributionEvents(opt *ListContributionEventsOptions, options ...RequestOptionFunc) ([]*ContributionEvent, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodGet, "events", opt, options)
 	if err != nil {
@@ -215,10 +217,6 @@ type ListProjectVisibleEventsOptions struct {
 	Sort       *string               `url:"sort,omitempty" json:"sort,omitempty"`
 }
 
-// ListProjectVisibleEvents gets the events for the specified project.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/events/#list-a-projects-visible-events
 func (s *EventsService) ListProjectVisibleEvents(pid any, opt *ListProjectVisibleEventsOptions, options ...RequestOptionFunc) ([]*ProjectEvent, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/external_status_checks.go b/vendor/gitlab.com/gitlab-org/api/client-go/external_status_checks.go
index caddb0815..0678a4878 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/external_status_checks.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/external_status_checks.go
@@ -9,13 +9,98 @@ import (
 type (
 	// ExternalStatusChecksServiceInterface defines all the API methods for the ExternalStatusChecksService
 	ExternalStatusChecksServiceInterface interface {
-		ListMergeStatusChecks(pid any, mr int, opt *ListOptions, options ...RequestOptionFunc) ([]*MergeStatusCheck, *Response, error)
-		SetExternalStatusCheckStatus(pid any, mergeRequest int, opt *SetExternalStatusCheckStatusOptions, options ...RequestOptionFunc) (*Response, error)
-		ListProjectStatusChecks(pid any, opt *ListOptions, options ...RequestOptionFunc) ([]*ProjectStatusCheck, *Response, error)
+		// CreateExternalStatusCheck creates an external status check.
+		// Deprecated: to be removed in 1.0; use CreateProjectExternalStatusCheck instead
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#create-external-status-check-service
 		CreateExternalStatusCheck(pid any, opt *CreateExternalStatusCheckOptions, options ...RequestOptionFunc) (*Response, error)
+
+		// DeleteExternalStatusCheck deletes an external status check.
+		// Deprecated: to be removed in 1.0; use DeleteProjectExternalStatusCheck instead
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#delete-external-status-check-service
 		DeleteExternalStatusCheck(pid any, check int, options ...RequestOptionFunc) (*Response, error)
+
+		// UpdateExternalStatusCheck updates an external status check.
+		// Deprecated: to be removed in 1.0; use UpdateProjectExternalStatusCheck instead
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#update-external-status-check-service
 		UpdateExternalStatusCheck(pid any, check int, opt *UpdateExternalStatusCheckOptions, options ...RequestOptionFunc) (*Response, error)
+
+		// ListMergeStatusChecks lists the external status checks that apply to it
+		// and their status for a single merge request.
+		// Deprecated: to be removed in 1.0; use ListProjectMergeRequestExternalStatusChecks instead
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#list-status-checks-for-a-merge-request
+		ListMergeStatusChecks(pid any, mr int, opt *ListOptions, options ...RequestOptionFunc) ([]*MergeStatusCheck, *Response, error)
+
+		// ListProjectStatusChecks lists the project external status checks.
+		// Deprecated: to be removed in 1.0; use ListProjectExternalStatusChecks instead
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#get-project-external-status-check-services
+		ListProjectStatusChecks(pid any, opt *ListOptions, options ...RequestOptionFunc) ([]*ProjectStatusCheck, *Response, error)
+
+		// RetryFailedStatusCheckForAMergeRequest retries the specified failed external status check.
+		// Deprecated: to be removed in 1.0; use RetryFailedExternalStatusCheckForProjectMergeRequest instead
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#retry-failed-status-check-for-a-merge-request
 		RetryFailedStatusCheckForAMergeRequest(pid any, mergeRequest int, externalStatusCheck int, options ...RequestOptionFunc) (*Response, error)
+
+		// SetExternalStatusCheckStatus sets the status of an external status check.
+		// Deprecated: to be removed in 1.0; use SetProjectMergeRequestExternalStatusCheckStatus instead
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#set-status-of-an-external-status-check
+		SetExternalStatusCheckStatus(pid any, mergeRequest int, opt *SetExternalStatusCheckStatusOptions, options ...RequestOptionFunc) (*Response, error)
+
+		// ListProjectMergeRequestExternalStatusChecks lists the external status checks that apply to it
+		// and their status for a single merge request.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#list-status-checks-for-a-merge-request
+		ListProjectMergeRequestExternalStatusChecks(pid any, mr int, opt *ListProjectMergeRequestExternalStatusChecksOptions, options ...RequestOptionFunc) ([]*MergeStatusCheck, *Response, error)
+
+		// ListProjectExternalStatusChecks lists the project external status checks.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#get-project-external-status-check-services
+		ListProjectExternalStatusChecks(pid any, opt *ListProjectExternalStatusChecksOptions, options ...RequestOptionFunc) ([]*ProjectStatusCheck, *Response, error)
+
+		// RetryFailedExternalStatusCheckForProjectMergeRequest retries the specified failed external status check.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#retry-failed-status-check-for-a-merge-request
+		RetryFailedExternalStatusCheckForProjectMergeRequest(pid any, mergeRequest int, externalStatusCheck int, opt *RetryFailedExternalStatusCheckForProjectMergeRequestOptions, options ...RequestOptionFunc) (*Response, error)
+
+		// CreateProjectExternalStatusCheck creates an external status check.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#create-external-status-check-service
+		CreateProjectExternalStatusCheck(pid any, opt *CreateProjectExternalStatusCheckOptions, options ...RequestOptionFunc) (*ProjectStatusCheck, *Response, error)
+
+		// UpdateProjectExternalStatusCheck updates an external status check.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#update-external-status-check-service
+		UpdateProjectExternalStatusCheck(pid any, check int, opt *UpdateProjectExternalStatusCheckOptions, options ...RequestOptionFunc) (*ProjectStatusCheck, *Response, error)
+
+		// DeleteProjectExternalStatusCheck deletes an external status check.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#delete-external-status-check-service
+		DeleteProjectExternalStatusCheck(pid any, check int, opt *DeleteProjectExternalStatusCheckOptions, options ...RequestOptionFunc) (*Response, error)
+
+		// SetProjectMergeRequestExternalStatusCheckStatus sets the status of an external status check.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/status_checks/#set-status-of-an-external-status-check
+		SetProjectMergeRequestExternalStatusCheckStatus(pid any, mergeRequest int, opt *SetProjectMergeRequestExternalStatusCheckStatusOptions, options ...RequestOptionFunc) (*Response, error)
 	}
 
 	// ExternalStatusChecksService handles communication with the external
@@ -41,6 +126,7 @@ type ProjectStatusCheck struct {
 	Name              string                       `json:"name"`
 	ProjectID         int                          `json:"project_id"`
 	ExternalURL       string                       `json:"external_url"`
+	HMAC              bool                         `json:"hmac"`
 	ProtectedBranches []StatusCheckProtectedBranch `json:"protected_branches"`
 }
 
@@ -53,11 +139,6 @@ type StatusCheckProtectedBranch struct {
 	CodeOwnerApprovalRequired bool       `json:"code_owner_approval_required"`
 }
 
-// ListMergeStatusChecks lists the external status checks that apply to it
-// and their status for a single merge request.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/status_checks/#list-status-checks-for-a-merge-request
 func (s *ExternalStatusChecksService) ListMergeStatusChecks(pid any, mr int, opt *ListOptions, options ...RequestOptionFunc) ([]*MergeStatusCheck, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -81,6 +162,7 @@ func (s *ExternalStatusChecksService) ListMergeStatusChecks(pid any, mr int, opt
 
 // SetExternalStatusCheckStatusOptions represents the available
 // SetExternalStatusCheckStatus() options.
+// Deprecated: to be removed in 1.0; use SetProjectMergeRequestExternalStatusCheckStatusOptions instead
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/status_checks/#set-status-of-an-external-status-check
@@ -90,10 +172,6 @@ type SetExternalStatusCheckStatusOptions struct {
 	Status                *string `url:"status,omitempty" json:"status,omitempty"`
 }
 
-// SetExternalStatusCheckStatus sets the status of an external status check.
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/status_checks/#set-status-of-an-external-status-check
 func (s *ExternalStatusChecksService) SetExternalStatusCheckStatus(pid any, mergeRequest int, opt *SetExternalStatusCheckStatusOptions, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -109,10 +187,6 @@ func (s *ExternalStatusChecksService) SetExternalStatusCheckStatus(pid any, merg
 	return s.client.Do(req, nil)
 }
 
-// ListProjectStatusChecks lists the project external status checks.
-//
-// GitLab API docs:
-// https://docs.gitlab.com/api/status_checks/#get-project-external-status-check-services
 func (s *ExternalStatusChecksService) ListProjectStatusChecks(pid any, opt *ListOptions, options ...RequestOptionFunc) ([]*ProjectStatusCheck, *Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -136,6 +210,7 @@ func (s *ExternalStatusChecksService) ListProjectStatusChecks(pid any, opt *List
 
 // CreateExternalStatusCheckOptions represents the available
 // CreateExternalStatusCheck() options.
+// Deprecated: to be removed in 1.0; use CreateProjectExternalStatusCheckOptions instead
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/status_checks/#create-external-status-check-service
@@ -145,10 +220,6 @@ type CreateExternalStatusCheckOptions struct {
 	ProtectedBranchIDs *[]int  `url:"protected_branch_ids,omitempty" json:"protected_branch_ids,omitempty"`
 }
 
-// CreateExternalStatusCheck creates an external status check.
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/status_checks/#create-external-status-check-service
 func (s *ExternalStatusChecksService) CreateExternalStatusCheck(pid any, opt *CreateExternalStatusCheckOptions, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -164,10 +235,6 @@ func (s *ExternalStatusChecksService) CreateExternalStatusCheck(pid any, opt *Cr
 	return s.client.Do(req, nil)
 }
 
-// DeleteExternalStatusCheck deletes an external status check.
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/status_checks/#delete-external-status-check-service
 func (s *ExternalStatusChecksService) DeleteExternalStatusCheck(pid any, check int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -185,6 +252,7 @@ func (s *ExternalStatusChecksService) DeleteExternalStatusCheck(pid any, check i
 
 // UpdateExternalStatusCheckOptions represents the available
 // UpdateExternalStatusCheck() options.
+// Deprecated: to be removed in 1.0; use UpdateProjectExternalStatusCheckOptions instead
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/status_checks/#update-external-status-check-service
@@ -194,10 +262,6 @@ type UpdateExternalStatusCheckOptions struct {
 	ProtectedBranchIDs *[]int  `url:"protected_branch_ids,omitempty" json:"protected_branch_ids,omitempty"`
 }
 
-// UpdateExternalStatusCheck updates an external status check.
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/status_checks/#update-external-status-check-service
 func (s *ExternalStatusChecksService) UpdateExternalStatusCheck(pid any, check int, opt *UpdateExternalStatusCheckOptions, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -213,10 +277,6 @@ func (s *ExternalStatusChecksService) UpdateExternalStatusCheck(pid any, check i
 	return s.client.Do(req, nil)
 }
 
-// UpdateExternalStatusCheck updates an external status check.
-//
-// Gitlab API docs:
-// https://docs.gitlab.com/api/status_checks/#retry-failed-status-check-for-a-merge-request
 func (s *ExternalStatusChecksService) RetryFailedStatusCheckForAMergeRequest(pid any, mergeRequest int, externalStatusCheck int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
 	if err != nil {
@@ -231,3 +291,199 @@ func (s *ExternalStatusChecksService) RetryFailedStatusCheckForAMergeRequest(pid
 
 	return s.client.Do(req, nil)
 }
+
+// ListProjectMergeRequestExternalStatusChecksOptions represents the available
+// ListProjectMergeRequestExternalStatusChecks() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/status_checks/#list-status-checks-for-a-merge-request
+type ListProjectMergeRequestExternalStatusChecksOptions struct {
+	ListOptions
+}
+
+func (s *ExternalStatusChecksService) ListProjectMergeRequestExternalStatusChecks(pid any, mr int, opt *ListProjectMergeRequestExternalStatusChecksOptions, options ...RequestOptionFunc) ([]*MergeStatusCheck, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/merge_requests/%d/status_checks", PathEscape(project), mr)
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var mscs []*MergeStatusCheck
+	resp, err := s.client.Do(req, &mscs)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return mscs, resp, nil
+}
+
+// ListProjectExternalStatusChecksOptions represents the available
+// ListProjectExternalStatusChecks() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/status_checks/#get-project-external-status-check-services
+type ListProjectExternalStatusChecksOptions struct {
+	ListOptions
+}
+
+func (s *ExternalStatusChecksService) ListProjectExternalStatusChecks(pid any, opt *ListProjectExternalStatusChecksOptions, options ...RequestOptionFunc) ([]*ProjectStatusCheck, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/external_status_checks", PathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var pscs []*ProjectStatusCheck
+	resp, err := s.client.Do(req, &pscs)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pscs, resp, nil
+}
+
+// CreateProjectExternalStatusCheckOptions represents the available
+// CreateProjectExternalStatusCheck() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/status_checks/#create-external-status-check-service
+type CreateProjectExternalStatusCheckOptions struct {
+	Name               *string `url:"name,omitempty" json:"name,omitempty"`
+	ExternalURL        *string `url:"external_url,omitempty" json:"external_url,omitempty"`
+	SharedSecret       *string `url:"shared_secret,omitempty" json:"shared_secret,omitempty"`
+	ProtectedBranchIDs *[]int  `url:"protected_branch_ids,omitempty" json:"protected_branch_ids,omitempty"`
+}
+
+func (s *ExternalStatusChecksService) CreateProjectExternalStatusCheck(pid any, opt *CreateProjectExternalStatusCheckOptions, options ...RequestOptionFunc) (*ProjectStatusCheck, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/external_status_checks", PathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	statusCheck := new(ProjectStatusCheck)
+	resp, err := s.client.Do(req, statusCheck)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return statusCheck, resp, nil
+}
+
+// DeleteProjectExternalStatusCheckOptions represents the available
+// DeleteProjectExternalStatusCheck() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/status_checks/#delete-external-status-check-service
+type DeleteProjectExternalStatusCheckOptions struct{}
+
+func (s *ExternalStatusChecksService) DeleteProjectExternalStatusCheck(pid any, check int, opt *DeleteProjectExternalStatusCheckOptions, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/external_status_checks/%d", PathEscape(project), check)
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, opt, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
+// UpdateProjectExternalStatusCheckOptions represents the available
+// UpdateProjectExternalStatusCheck() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/status_checks/#update-external-status-check-service
+type UpdateProjectExternalStatusCheckOptions struct {
+	Name               *string `url:"name,omitempty" json:"name,omitempty"`
+	ExternalURL        *string `url:"external_url,omitempty" json:"external_url,omitempty"`
+	SharedSecret       *string `url:"shared_secret,omitempty" json:"shared_secret,omitempty"`
+	ProtectedBranchIDs *[]int  `url:"protected_branch_ids,omitempty" json:"protected_branch_ids,omitempty"`
+}
+
+func (s *ExternalStatusChecksService) UpdateProjectExternalStatusCheck(pid any, check int, opt *UpdateProjectExternalStatusCheckOptions, options ...RequestOptionFunc) (*ProjectStatusCheck, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/external_status_checks/%d", PathEscape(project), check)
+
+	req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	statusCheck := new(ProjectStatusCheck)
+	resp, err := s.client.Do(req, statusCheck)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return statusCheck, resp, nil
+}
+
+// RetryFailedExternalStatusCheckForProjectMergeRequestOptions represents the available
+// RetryFailedExternalStatusCheckForProjectMergeRequest() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/status_checks/#retry-failed-status-check-for-a-merge-request
+type RetryFailedExternalStatusCheckForProjectMergeRequestOptions struct{}
+
+func (s *ExternalStatusChecksService) RetryFailedExternalStatusCheckForProjectMergeRequest(pid any, mergeRequest int, externalStatusCheck int, opt *RetryFailedExternalStatusCheckForProjectMergeRequestOptions, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/merge_requests/%d/status_checks/%d/retry", PathEscape(project), mergeRequest, externalStatusCheck)
+
+	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
+// SetProjectMergeRequestExternalStatusCheckStatusOptions represents the available
+// SetProjectMergeRequestExternalStatusCheckStatus() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/status_checks/#set-status-of-an-external-status-check
+type SetProjectMergeRequestExternalStatusCheckStatusOptions struct {
+	SHA                   *string `url:"sha,omitempty" json:"sha,omitempty"`
+	ExternalStatusCheckID *int    `url:"external_status_check_id,omitempty" json:"external_status_check_id,omitempty"`
+	Status                *string `url:"status,omitempty" json:"status,omitempty"`
+}
+
+func (s *ExternalStatusChecksService) SetProjectMergeRequestExternalStatusCheckStatus(pid any, mergeRequest int, opt *SetProjectMergeRequestExternalStatusCheckStatusOptions, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/merge_requests/%d/status_check_responses", PathEscape(project), mergeRequest)
+
+	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/feature_flag_user_lists.go b/vendor/gitlab.com/gitlab-org/api/client-go/feature_flag_user_lists.go
index 5555a2831..6208158c6 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/feature_flag_user_lists.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/feature_flag_user_lists.go
@@ -29,7 +29,7 @@ type (
 		DeleteFeatureFlagUserList(pid any, iid int, options ...RequestOptionFunc) (*Response, error)
 	}
 
-	// FeatureFlagUserListService handles communication with the feature flag
+	// FeatureFlagUserListsService handles communication with the feature flag
 	// user list related methods of the GitLab API.
 	//
 	// GitLab API docs: https://docs.gitlab.com/api/feature_flag_user_lists/
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/freeze_periods.go b/vendor/gitlab.com/gitlab-org/api/client-go/freeze_periods.go
index cf6d3f854..8662ac4df 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/freeze_periods.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/freeze_periods.go
@@ -67,7 +67,7 @@ type FreezePeriod struct {
 // https://docs.gitlab.com/api/freeze_periods/#list-freeze-periods
 type ListFreezePeriodsOptions ListOptions
 
-// ListFreezePeriods gets a list of project project freeze periods.
+// ListFreezePeriods gets a list of project freeze periods.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/freeze_periods/#list-freeze-periods
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/geo_sites.go b/vendor/gitlab.com/gitlab-org/api/client-go/geo_sites.go
index 53cdb895e..bd0f9f2b3 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/geo_sites.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/geo_sites.go
@@ -457,7 +457,7 @@ type GeoSiteStatus struct {
 	Links                                           GeoSiteStatusLink `json:"_links"`
 }
 
-// GeoSiteStatus represents the status of Geo Site.
+// GeoSiteStatusLink represents the links for a GitLab Geo Site status.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/geo_sites/#retrieve-status-about-all-geo-sites
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/gitlab.go b/vendor/gitlab.com/gitlab-org/api/client-go/gitlab.go
index fe912715f..d4b4d1571 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/gitlab.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/gitlab.go
@@ -28,6 +28,7 @@ import (
 	"math"
 	"math/rand"
 	"mime/multipart"
+	"net"
 	"net/http"
 	"net/url"
 	"sort"
@@ -51,6 +52,9 @@ const (
 
 	headerRateLimit = "RateLimit-Limit"
 	headerRateReset = "RateLimit-Reset"
+
+	AccessTokenHeaderName = "PRIVATE-TOKEN"
+	JobTokenHeaderName    = "JOB-TOKEN"
 )
 
 // AuthType represents an authentication type within GitLab.
@@ -97,9 +101,16 @@ type Client struct {
 	// once.
 	authSourceInit sync.Once
 
+	// jar is the cookie jar to use with the HTTP client
+	jar http.CookieJar
+
 	// Default request options applied to every request.
 	defaultRequestOptions []RequestOptionFunc
 
+	// interceptors contain the stack of *http.Client round tripper builder func
+	// which are used to decorate the http.Client#Transport value.
+	interceptors []Interceptor
+
 	// User agent used when communicating with the GitLab API.
 	UserAgent string
 
@@ -157,6 +168,7 @@ type Client struct {
 	GroupCluster                     GroupClustersServiceInterface
 	GroupEpicBoards                  GroupEpicBoardsServiceInterface
 	GroupImportExport                GroupImportExportServiceInterface
+	Integrations                     IntegrationsServiceInterface
 	GroupIssueBoards                 GroupIssueBoardsServiceInterface
 	GroupIterations                  GroupIterationsServiceInterface
 	GroupLabels                      GroupLabelsServiceInterface
@@ -164,6 +176,7 @@ type Client struct {
 	GroupMembers                     GroupMembersServiceInterface
 	GroupMilestones                  GroupMilestonesServiceInterface
 	GroupProtectedEnvironments       GroupProtectedEnvironmentsServiceInterface
+	GroupRelationsExport             GroupRelationsExportServiceInterface
 	GroupReleases                    GroupReleasesServiceInterface
 	GroupRepositoryStorageMove       GroupRepositoryStorageMoveServiceInterface
 	GroupSCIM                        GroupSCIMServiceInterface
@@ -189,10 +202,12 @@ type Client struct {
 	MemberRolesService               MemberRolesServiceInterface
 	MergeRequestApprovals            MergeRequestApprovalsServiceInterface
 	MergeRequestApprovalSettings     MergeRequestApprovalSettingsServiceInterface
+	MergeRequestContextCommits       MergeRequestContextCommitsServiceInterface
 	MergeRequests                    MergeRequestsServiceInterface
 	MergeTrains                      MergeTrainsServiceInterface
 	Metadata                         MetadataServiceInterface
 	Milestones                       MilestonesServiceInterface
+	ModelRegistry                    ModelRegistryServiceInterface
 	Namespaces                       NamespacesServiceInterface
 	Notes                            NotesServiceInterface
 	NotificationSettings             NotificationSettingsServiceInterface
@@ -205,6 +220,7 @@ type Client struct {
 	Pipelines                        PipelinesServiceInterface
 	PlanLimits                       PlanLimitsServiceInterface
 	ProjectAccessTokens              ProjectAccessTokensServiceInterface
+	ProjectAliases                   ProjectAliasesServiceInterface
 	ProjectBadges                    ProjectBadgesServiceInterface
 	ProjectCluster                   ProjectClustersServiceInterface
 	ProjectFeatureFlags              ProjectFeatureFlagServiceInterface
@@ -216,12 +232,14 @@ type Client struct {
 	ProjectRepositoryStorageMove     ProjectRepositoryStorageMoveServiceInterface
 	ProjectSecuritySettings          ProjectSecuritySettingsServiceInterface
 	ProjectSnippets                  ProjectSnippetsServiceInterface
+	ProjectStatistics                ProjectStatisticsServiceInterface
 	ProjectTemplates                 ProjectTemplatesServiceInterface
 	ProjectVariables                 ProjectVariablesServiceInterface
 	ProjectVulnerabilities           ProjectVulnerabilitiesServiceInterface
 	Projects                         ProjectsServiceInterface
 	ProtectedBranches                ProtectedBranchesServiceInterface
 	ProtectedEnvironments            ProtectedEnvironmentsServiceInterface
+	ProtectedPackages                ProtectedPackagesServiceInterface
 	ProtectedTags                    ProtectedTagsServiceInterface
 	ReleaseLinks                     ReleaseLinksServiceInterface
 	Releases                         ReleasesServiceInterface
@@ -254,6 +272,32 @@ type Client struct {
 	Wikis                            WikisServiceInterface
 }
 
+// Interceptor is used to build a *http.Client request pipeline,
+//
+// It receives the next RoundTripper in the chain and returns a new one that
+// will be used for the request.
+//
+// This next RoundTripper might or might not be the actual transporter,
+// which actually does the request call,
+// but it is safe to assume that calling the next will result in the expected HTTP call.
+//
+// Example:
+//
+//	// Simple logger interceptor.
+//	logger := func(next http.RoundTripper) http.RoundTripper {
+//	    return roundtripperFunc(func(req *http.Request) (*http.Response, error) {
+//	        fmt.Printf("Request: %s %s\n", req.Method, req.URL)
+//	        resp, err := next.RoundTrip(req)
+//	        if err == nil {
+//	            fmt.Printf("Response status: %d\n", resp.StatusCode)
+//	        }
+//	        return resp, err
+//	    })
+//	}
+//
+// The Interceptor type lets you add such middlewares to a client by chaining them.
+type Interceptor func(next http.RoundTripper) http.RoundTripper
+
 // ListOptions specifies the optional parameters to various List methods that
 // support pagination.
 type ListOptions struct {
@@ -279,11 +323,7 @@ type RateLimiter interface {
 // NewClient returns a new GitLab API client. To use API methods which require
 // authentication, provide a valid private or personal token.
 func NewClient(token string, options ...ClientOptionFunc) (*Client, error) {
-	as := staticAuthSource{
-		token:    token,
-		authType: PrivateToken,
-	}
-
+	as := AccessTokenAuthSource{Token: token}
 	return NewAuthSourceClient(as, options...)
 }
 
@@ -300,9 +340,9 @@ func NewClient(token string, options ...ClientOptionFunc) (*Client, error) {
 //
 // Deprecated: GitLab recommends against using this authentication method.
 func NewBasicAuthClient(username, password string, options ...ClientOptionFunc) (*Client, error) {
-	as := &passwordCredentialsAuthSource{
-		username: username,
-		password: password,
+	as := &PasswordCredentialsAuthSource{
+		Username: username,
+		Password: password,
 	}
 
 	return NewAuthSourceClient(as, options...)
@@ -311,11 +351,7 @@ func NewBasicAuthClient(username, password string, options ...ClientOptionFunc)
 // NewJobClient returns a new GitLab API client. To use API methods which require
 // authentication, provide a valid job token.
 func NewJobClient(token string, options ...ClientOptionFunc) (*Client, error) {
-	as := staticAuthSource{
-		token:    token,
-		authType: JobToken,
-	}
-
+	as := JobTokenAuthSource{Token: token}
 	return NewAuthSourceClient(as, options...)
 }
 
@@ -335,7 +371,7 @@ func NewOAuthClient(token string, options ...ClientOptionFunc) (*Client, error)
 	return NewAuthSourceClient(as, options...)
 }
 
-// NewAuthSourceClient returns a new GitLab API client that uses the AuthSouce for authentication.
+// NewAuthSourceClient returns a new GitLab API client that uses the AuthSource for authentication.
 func NewAuthSourceClient(as AuthSource, options ...ClientOptionFunc) (*Client, error) {
 	c := &Client{
 		UserAgent:  userAgent,
@@ -366,6 +402,20 @@ func NewAuthSourceClient(as AuthSource, options ...ClientOptionFunc) (*Client, e
 		}
 	}
 
+	decorateHTTPClientTransportWithInterceptors(c)
+
+	// Wire up the cookie jar.
+	// The ClientOptionFunc can't do it directly,
+	// because the user may also specify HTTPClient
+	// and we want the order of passing those to matter
+	// as little as possible.
+	if c.jar != nil {
+		if c.client.HTTPClient.Jar != nil {
+			return nil, errors.New("conflicting option functions when creating client. The provided HTTP Client already has a Jar configured, therefore you can't use gitlab.WithCookieJar")
+		}
+		c.client.HTTPClient.Jar = c.jar
+	}
+
 	// If no custom limiter was set using a client option, configure
 	// the default rate limiter with values that implicitly disable
 	// rate limiting until an initial HTTP call is done and we can
@@ -431,6 +481,7 @@ func NewAuthSourceClient(as AuthSource, options ...ClientOptionFunc) (*Client, e
 	c.GroupCluster = &GroupClustersService{client: c}
 	c.GroupEpicBoards = &GroupEpicBoardsService{client: c}
 	c.GroupImportExport = &GroupImportExportService{client: c}
+	c.Integrations = &IntegrationsService{client: c}
 	c.GroupIssueBoards = &GroupIssueBoardsService{client: c}
 	c.GroupIterations = &GroupIterationsService{client: c}
 	c.GroupLabels = &GroupLabelsService{client: c}
@@ -438,6 +489,7 @@ func NewAuthSourceClient(as AuthSource, options ...ClientOptionFunc) (*Client, e
 	c.GroupMembers = &GroupMembersService{client: c}
 	c.GroupMilestones = &GroupMilestonesService{client: c}
 	c.GroupProtectedEnvironments = &GroupProtectedEnvironmentsService{client: c}
+	c.GroupRelationsExport = &GroupRelationsExportService{client: c}
 	c.GroupReleases = &GroupReleasesService{client: c}
 	c.GroupRepositoryStorageMove = &GroupRepositoryStorageMoveService{client: c}
 	c.GroupSCIM = &GroupSCIMService{client: c}
@@ -463,10 +515,12 @@ func NewAuthSourceClient(as AuthSource, options ...ClientOptionFunc) (*Client, e
 	c.MemberRolesService = &MemberRolesService{client: c}
 	c.MergeRequestApprovals = &MergeRequestApprovalsService{client: c}
 	c.MergeRequestApprovalSettings = &MergeRequestApprovalSettingsService{client: c}
+	c.MergeRequestContextCommits = &MergeRequestContextCommitsService{client: c}
 	c.MergeRequests = &MergeRequestsService{client: c, timeStats: timeStats}
 	c.MergeTrains = &MergeTrainsService{client: c}
 	c.Metadata = &MetadataService{client: c}
 	c.Milestones = &MilestonesService{client: c}
+	c.ModelRegistry = &ModelRegistryService{client: c}
 	c.Namespaces = &NamespacesService{client: c}
 	c.Notes = &NotesService{client: c}
 	c.NotificationSettings = &NotificationSettingsService{client: c}
@@ -479,6 +533,7 @@ func NewAuthSourceClient(as AuthSource, options ...ClientOptionFunc) (*Client, e
 	c.Pipelines = &PipelinesService{client: c}
 	c.PlanLimits = &PlanLimitsService{client: c}
 	c.ProjectAccessTokens = &ProjectAccessTokensService{client: c}
+	c.ProjectAliases = &ProjectAliasesService{client: c}
 	c.ProjectBadges = &ProjectBadgesService{client: c}
 	c.ProjectCluster = &ProjectClustersService{client: c}
 	c.ProjectFeatureFlags = &ProjectFeatureFlagService{client: c}
@@ -490,12 +545,14 @@ func NewAuthSourceClient(as AuthSource, options ...ClientOptionFunc) (*Client, e
 	c.ProjectRepositoryStorageMove = &ProjectRepositoryStorageMoveService{client: c}
 	c.ProjectSecuritySettings = &ProjectSecuritySettingsService{client: c}
 	c.ProjectSnippets = &ProjectSnippetsService{client: c}
+	c.ProjectStatistics = &ProjectStatisticsService{client: c}
 	c.ProjectTemplates = &ProjectTemplatesService{client: c}
 	c.ProjectVariables = &ProjectVariablesService{client: c}
 	c.ProjectVulnerabilities = &ProjectVulnerabilitiesService{client: c}
 	c.Projects = &ProjectsService{client: c}
 	c.ProtectedBranches = &ProtectedBranchesService{client: c}
 	c.ProtectedEnvironments = &ProtectedEnvironmentsService{client: c}
+	c.ProtectedPackages = &ProtectedPackagesService{client: c}
 	c.ProtectedTags = &ProtectedTagsService{client: c}
 	c.ReleaseLinks = &ReleaseLinksService{client: c}
 	c.Releases = &ReleasesService{client: c}
@@ -530,18 +587,119 @@ func NewAuthSourceClient(as AuthSource, options ...ClientOptionFunc) (*Client, e
 	return c, nil
 }
 
+func decorateHTTPClientTransportWithInterceptors(c *Client) {
+	if len(c.interceptors) == 0 {
+		return
+	}
+	c.client.HTTPClient.Transport = chainInterceptors(c.client.HTTPClient.Transport, c.interceptors...)
+}
+
+func chainInterceptors(rt http.RoundTripper, interceptors ...Interceptor) http.RoundTripper {
+	for i := len(interceptors) - 1; i >= 0; i-- {
+		rt = interceptors[i](rt)
+	}
+	return rt
+}
+
+func (c *Client) HTTPClient() *http.Client {
+	return c.client.HTTPClient
+}
+
 // retryHTTPCheck provides a callback for Client.CheckRetry which
-// will retry both rate limit (429) and server (>= 500) errors.
+// respects default retries and retries what is safe to retry.
 func (c *Client) retryHTTPCheck(ctx context.Context, resp *http.Response, err error) (bool, error) {
+	// do not retry if retries are disabled completely
+	if c.disableRetries {
+		return false, nil
+	}
+
+	// do not retry on context.Canceled or context.DeadlineExceeded
 	if ctx.Err() != nil {
 		return false, ctx.Err()
 	}
+
 	if err != nil {
-		return false, err
+		// We should be able to retry requests with assumed idempotent HTTP methods.
+		// In a future iteration we might want to annotate requests that are idempotent
+		// to further improve this logic here.
+		if resp != nil && resp.Request != nil {
+			switch resp.Request.Method {
+			case http.MethodConnect, http.MethodOptions, http.MethodTrace, http.MethodHead, http.MethodGet:
+				return true, nil
+			}
+		}
+
+		// Only retry errors that we know happened before writing to the wire
+		var urlErr *url.Error
+		var netOpErr *net.OpError
+		var dnsErr *net.DNSError
+		// see Go src/net/http/transport.go
+		var potentialTLSHandshakeErr interface {
+			Timeout() bool
+			Temporary() bool
+		}
+
+		switch {
+		// DNS errors are safe - they happen before any connection
+		case errors.As(err, &dnsErr):
+			// NXDOMAIN should not be retried
+			if dnsErr.IsNotFound {
+				return false, err
+			}
+			// Other DNS errors are safe to retry
+			return true, nil
+
+		// Direct net.OpError for dial operations
+		case errors.As(err, &netOpErr):
+			// from the comments in the implementation of net.OpError.Temporary
+			// it seems that it's safe to retry temporary OpErrors.
+			if netOpErr.Temporary() {
+				return true, nil
+			}
+
+			if strings.EqualFold(netOpErr.Op, "dial") {
+				return true, nil
+			}
+
+		// Connection refused errors are safe - they happen at TCP establishment
+		case errors.As(err, &urlErr):
+			if strings.Contains(urlErr.Error(), "connection refused") {
+				return true, nil
+			}
+
+		// TLS handshake errors are safe if we can identify them
+		case errors.As(err, &potentialTLSHandshakeErr):
+			// Check if this is a TLS handshake timeout specifically
+			if strings.Contains(err.Error(), "net/http: TLS handshake timeout") {
+				return true, nil
+			}
+		}
+
+		// we are conservative here and do not want to retry any "unknown" errors, because
+		// they could have happened when the connection was already established and the request
+		// partially fulfilled. We don't have the insights here if the request
+		// was idempotent or not, so we reject a retry.
+		return false, nil
+	}
+
+	// 429 Too Many Requests is recoverable. Sometimes the server puts
+	// a Retry-After response header to indicate when the server is
+	// available to start processing request from client.
+	if resp.StatusCode == http.StatusTooManyRequests {
+		return true, nil
 	}
-	if !c.disableRetries && (resp.StatusCode == 429 || resp.StatusCode >= 500) {
+
+	// Check the response code. We retry on 500-range responses to allow
+	// the server time to recover, as 500's are typically not permanent
+	// errors and may relate to outages on the server side. This will catch
+	// invalid response codes as well, like 0 and 999.
+	// Status code 0 is especially important for AWS ALB:
+	// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-troubleshooting.html#response-code-000
+	if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != http.StatusNotImplemented) {
+		// return true, fmt.Errorf("unexpected HTTP status %s", resp.Status)
 		return true, nil
 	}
+
 	return false, nil
 }
 
@@ -668,6 +826,14 @@ func (c *Client) NewRequest(method, path string, opt any, options []RequestOptio
 	u.RawPath = c.baseURL.Path + path
 	u.Path = c.baseURL.Path + unescaped
 
+	return c.NewRequestToURL(method, &u, opt, options)
+}
+
+func (c *Client) NewRequestToURL(method string, u *url.URL, opt any, options []RequestOptionFunc) (*retryablehttp.Request, error) {
+	if u.Scheme != c.baseURL.Scheme || u.Host != c.baseURL.Host {
+		return nil, fmt.Errorf("client only allows requests to URLs matching the clients configured base URL. Got %q, base URL is %q", u.String(), c.baseURL.String())
+	}
+
 	// Create a request specific headers map.
 	reqHeaders := make(http.Header)
 	reqHeaders.Set("Accept", "application/json")
@@ -682,10 +848,11 @@ func (c *Client) NewRequest(method, path string, opt any, options []RequestOptio
 		reqHeaders.Set("Content-Type", "application/json")
 
 		if opt != nil {
-			body, err = json.Marshal(opt)
+			b, err := json.Marshal(opt)
 			if err != nil {
 				return nil, err
 			}
+			body = b
 		}
 	case opt != nil:
 		q, err := query.Values(opt)
@@ -981,7 +1148,7 @@ func parseID(id any) (string, error) {
 	}
 }
 
-// Helper function to escape a project identifier.
+// PathEscape is a helper function to escape a project identifier.
 func PathEscape(s string) string {
 	return strings.ReplaceAll(url.PathEscape(s), ".", "%2E")
 }
@@ -1010,6 +1177,10 @@ func (e *ErrorResponse) Error() string {
 	}
 }
 
+func (e *ErrorResponse) HasStatusCode(statusCode int) bool {
+	return e != nil && e.Response != nil && e.Response.StatusCode == statusCode
+}
+
 // CheckResponse checks the API response for errors, and returns them if present.
 func CheckResponse(r *http.Response) error {
 	switch r.StatusCode {
@@ -1080,6 +1251,15 @@ func parseError(raw any) string {
 	}
 }
 
+func HasStatusCode(err error, statusCode int) bool {
+	var errResponse *ErrorResponse
+	if !errors.As(err, &errResponse) {
+		return false
+	}
+
+	return errResponse.HasStatusCode(statusCode)
+}
+
 // newRetryableHTTPClientWithRetryCheck returns a `retryablehttp.Client` clone of itself with the given CheckRetry function
 func (c *Client) newRetryableHTTPClientWithRetryCheck(cr retryablehttp.CheckRetry) *retryablehttp.Client {
 	return &retryablehttp.Client{
@@ -1125,48 +1305,52 @@ func (as OAuthTokenSource) Header(_ context.Context) (string, string, error) {
 	return "Authorization", "Bearer " + t.AccessToken, nil
 }
 
-// staticAuthSource implements the AuthSource interface for static tokens.
-type staticAuthSource struct {
-	token    string
-	authType AuthType
+// JobTokenAuthSource used as an AuthSource for CI Job Tokens
+type JobTokenAuthSource struct {
+	Token string
 }
 
-func (staticAuthSource) Init(context.Context, *Client) error {
+func (JobTokenAuthSource) Init(context.Context, *Client) error {
 	return nil
 }
 
-func (as staticAuthSource) Header(_ context.Context) (string, string, error) {
-	switch as.authType {
-	case PrivateToken:
-		return "PRIVATE-TOKEN", as.token, nil
+func (s JobTokenAuthSource) Header(_ context.Context) (string, string, error) {
+	return JobTokenHeaderName, s.Token, nil
+}
 
-	case JobToken:
-		return "JOB-TOKEN", as.token, nil
+// AccessTokenAuthSource used as an AuthSource for various access tokens, like Personal-, Project- and Group- Access Tokens.
+// Can be used for all tokens that authorize with the Private-Token header.
+type AccessTokenAuthSource struct {
+	Token string
+}
 
-	default:
-		return "", "", fmt.Errorf("invalid auth type: %v", as.authType)
-	}
+func (AccessTokenAuthSource) Init(context.Context, *Client) error {
+	return nil
+}
+
+func (s AccessTokenAuthSource) Header(_ context.Context) (string, string, error) {
+	return AccessTokenHeaderName, s.Token, nil
 }
 
-// passwordTokenSource implements the AuthSource interface for the OAuth 2.0
+// PasswordCredentialsAuthSource implements the AuthSource interface for the OAuth 2.0
 // resource owner password credentials flow.
-type passwordCredentialsAuthSource struct {
-	username string
-	password string
+type PasswordCredentialsAuthSource struct {
+	Username string
+	Password string
 
 	AuthSource
 }
 
-func (as *passwordCredentialsAuthSource) Init(ctx context.Context, client *Client) error {
+func (as *PasswordCredentialsAuthSource) Init(ctx context.Context, client *Client) error {
 	ctx = context.WithValue(ctx, oauth2.HTTPClient, client.client.HTTPClient)
 
 	config := &oauth2.Config{
 		Endpoint: client.endpoint(),
 	}
 
-	pct, err := config.PasswordCredentialsToken(ctx, as.username, as.password)
+	pct, err := config.PasswordCredentialsToken(ctx, as.Username, as.Password)
 	if err != nil {
-		return fmt.Errorf("PasswordCredentialsToken(%q, ******): %w", as.username, err)
+		return fmt.Errorf("PasswordCredentialsToken(%q, ******): %w", as.Username, err)
 	}
 
 	as.AuthSource = OAuthTokenSource{
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/graphql.go b/vendor/gitlab.com/gitlab-org/api/client-go/graphql.go
index 41c64d069..18fd6c9a3 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/graphql.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/graphql.go
@@ -22,7 +22,8 @@ type (
 	}
 
 	GraphQLQuery struct {
-		Query string `json:"query"`
+		Query     string         `json:"query"`
+		Variables map[string]any `json:"variables,omitempty"`
 	}
 
 	GenericGraphQLErrors struct {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/group_hooks.go b/vendor/gitlab.com/gitlab-org/api/client-go/group_hooks.go
index cd7117c84..096e5ca0d 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/group_hooks.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/group_hooks.go
@@ -227,7 +227,7 @@ type EditGroupHookOptions struct {
 
 // EditGroupHook edits a hook for a specified group.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/group_webhooks/#edit-group-hook
 func (s *GroupsService) EditGroupHook(gid any, hook int, opt *EditGroupHookOptions, options ...RequestOptionFunc) (*GroupHook, *Response, error) {
 	group, err := parseID(gid)
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/group_iterations.go b/vendor/gitlab.com/gitlab-org/api/client-go/group_iterations.go
index a0f7e337f..0a5e235e1 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/group_iterations.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/group_iterations.go
@@ -27,7 +27,7 @@ type (
 		ListGroupIterations(gid any, opt *ListGroupIterationsOptions, options ...RequestOptionFunc) ([]*GroupIteration, *Response, error)
 	}
 
-	// IterationsAPI handles communication with the iterations related methods
+	// GroupIterationsService handles communication with the iterations related methods
 	// of the GitLab API
 	//
 	// GitLab API docs: https://docs.gitlab.com/api/group_iterations/
@@ -38,7 +38,7 @@ type (
 
 var _ GroupIterationsServiceInterface = (*GroupIterationsService)(nil)
 
-// GroupInteration represents a GitLab iteration.
+// GroupIteration represents a GitLab iteration.
 //
 // GitLab API docs: https://docs.gitlab.com/api/group_iterations/
 type GroupIteration struct {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/group_members.go b/vendor/gitlab.com/gitlab-org/api/client-go/group_members.go
index 7ba88838c..14f835e95 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/group_members.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/group_members.go
@@ -55,6 +55,7 @@ type GroupMember struct {
 	AvatarURL         string                   `json:"avatar_url"`
 	WebURL            string                   `json:"web_url"`
 	CreatedAt         *time.Time               `json:"created_at"`
+	CreatedBy         *MemberCreatedBy         `json:"created_by"`
 	ExpiresAt         *ISOTime                 `json:"expires_at"`
 	AccessLevel       AccessLevelValue         `json:"access_level"`
 	Email             string                   `json:"email,omitempty"`
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/group_milestones.go b/vendor/gitlab.com/gitlab-org/api/client-go/group_milestones.go
index e9fc97aa8..8ae487a12 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/group_milestones.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/group_milestones.go
@@ -295,7 +295,7 @@ func (s *GroupMilestonesService) GetGroupMilestoneMergeRequests(gid any, milesto
 	return mr, resp, nil
 }
 
-// BurndownChartEvent reprensents a burnout chart event
+// BurndownChartEvent represents a burnout chart event.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/group_milestones/#get-all-burndown-chart-events-for-a-single-milestone
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/group_protected_environments.go b/vendor/gitlab.com/gitlab-org/api/client-go/group_protected_environments.go
index aea180371..211201164 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/group_protected_environments.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/group_protected_environments.go
@@ -53,7 +53,7 @@ type GroupProtectedEnvironment struct {
 	ApprovalRules         []*GroupEnvironmentApprovalRule      `json:"approval_rules"`
 }
 
-// GroupEnvironmentAccessDescription represents the access decription for a
+// GroupEnvironmentAccessDescription represents the access description for a
 // group-level protected environment.
 //
 // GitLab API docs:
@@ -153,7 +153,7 @@ type ProtectGroupEnvironmentOptions struct {
 	ApprovalRules         *[]*GroupEnvironmentApprovalRuleOptions `url:"approval_rules,omitempty" json:"approval_rules,omitempty"`
 }
 
-// GroupEnvironmentAccessOptions represents the options for an access decription
+// GroupEnvironmentAccessOptions represents the options for an access description
 // for a group-level protected environment.
 //
 // GitLab API docs:
@@ -217,7 +217,7 @@ type UpdateGroupProtectedEnvironmentOptions struct {
 }
 
 // UpdateGroupEnvironmentAccessOptions represents the options for updates to the
-// access decription for a group-level protected environment.
+// access description for a group-level protected environment.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/group_protected_environments/#update-a-protected-environment
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/group_relations_export.go b/vendor/gitlab.com/gitlab-org/api/client-go/group_relations_export.go
new file mode 100644
index 000000000..38863ba2c
--- /dev/null
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/group_relations_export.go
@@ -0,0 +1,145 @@
+package gitlab
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"time"
+)
+
+// GroupRelationsScheduleExportOptions represents the available ScheduleExport() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_relations_export/#schedule-new-export
+type GroupRelationsScheduleExportOptions struct {
+	Batched *bool `url:"batched,omitempty" json:"batched,omitempty"`
+}
+
+// ListGroupRelationsStatusOptions represents the available ListExportStatus() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_relations_export/#export-status
+type ListGroupRelationsStatusOptions struct {
+	ListOptions
+
+	Relation *string `url:"relation,omitempty" json:"relation,omitempty"`
+}
+
+// GroupRelationsDownloadOptions represents the available ExportDownload() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_relations_export/#download-exported-relations
+type GroupRelationsDownloadOptions struct {
+	Relation    *string `url:"relation,omitempty" json:"relation,omitempty"`
+	Batched     *bool   `url:"batched,omitempty" json:"batched,omitempty"`
+	BatchNumber *int64  `url:"batch_number,omitempty" json:"batch_number,omitempty"`
+}
+
+type GroupRelationStatus struct {
+	Relation     string    `json:"relation"`
+	Status       int64     `json:"status"`
+	Error        string    `json:"error"`
+	UpdatedAt    time.Time `json:"updated_at"`
+	Batched      bool      `json:"batched"`
+	BatchesCount int64     `json:"batches_count"`
+	Batches      []Batch   `json:"batches,omitempty"`
+}
+
+type Batch struct {
+	Status       int64     `json:"status"`
+	BatchNumber  int64     `json:"batch_number"`
+	ObjectsCount int64     `json:"objects_count"`
+	Error        string    `json:"error"`
+	UpdatedAt    time.Time `json:"updated_at"`
+}
+
+type (
+	GroupRelationsExportServiceInterface interface {
+		// ScheduleExport schedules a new export of group relations.
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/group_relations_export/#schedule-new-export
+		ScheduleExport(gid any, opt *GroupRelationsScheduleExportOptions, options ...RequestOptionFunc) (*Response, error)
+		// ListExportStatus gets the status of group relations export.
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/group_relations_export/#export-status
+		ListExportStatus(gid any, opt *ListGroupRelationsStatusOptions, options ...RequestOptionFunc) ([]*GroupRelationStatus, *Response, error)
+		// ExportDownload downloads the exported group relations.
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/group_relations_export/#download-exported-relations
+		ExportDownload(gid any, opt *GroupRelationsDownloadOptions, options ...RequestOptionFunc) (*bytes.Reader, *Response, error)
+	}
+
+	// GroupRelationsExportService handles communication with the group relations export related methods
+	// of the GitLab API.
+	//
+	// GitLab API docs: https://docs.gitlab.com/api/group_relations_export
+	GroupRelationsExportService struct {
+		client *Client
+	}
+)
+
+var _ GroupRelationsExportServiceInterface = (*GroupRelationsExportService)(nil)
+
+func (s *GroupRelationsExportService) ScheduleExport(gid any, opt *GroupRelationsScheduleExportOptions, options ...RequestOptionFunc) (*Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, err
+	}
+
+	u := fmt.Sprintf("groups/%s/export_relations",
+		PathEscape(group),
+	)
+
+	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
+func (s *GroupRelationsExportService) ListExportStatus(gid any, opt *ListGroupRelationsStatusOptions, options ...RequestOptionFunc) ([]*GroupRelationStatus, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	u := fmt.Sprintf("groups/%s/export_relations/status", PathEscape(group))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var groups []*GroupRelationStatus
+	resp, err := s.client.Do(req, &groups)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return groups, resp, nil
+}
+
+func (s *GroupRelationsExportService) ExportDownload(gid any, opt *GroupRelationsDownloadOptions, options ...RequestOptionFunc) (*bytes.Reader, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	u := fmt.Sprintf("groups/%s/export_relations/download",
+		PathEscape(group),
+	)
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	groupRelationsExportDownload := new(bytes.Buffer)
+	resp, err := s.client.Do(req, groupRelationsExportDownload)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return bytes.NewReader(groupRelationsExportDownload.Bytes()), resp, err
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/group_security_settings.go b/vendor/gitlab.com/gitlab-org/api/client-go/group_security_settings.go
index c93a31157..58de5ec27 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/group_security_settings.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/group_security_settings.go
@@ -28,7 +28,7 @@ type (
 	// GroupSecuritySettingsService handles communication with the Group Security Settings
 	// related methods of the GitLab API.
 	//
-	// Gitlab API docs:
+	// GitLab API docs:
 	// https://docs.gitlab.com/api/group_security_settings/
 	GroupSecuritySettingsService struct {
 		client *Client
@@ -39,14 +39,14 @@ var _ GroupSecuritySettingsServiceInterface = (*GroupSecuritySettingsService)(ni
 
 // GroupSecuritySettings represents the group security settings data.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/group_security_settings/
 type GroupSecuritySettings struct {
 	SecretPushProtectionEnabled bool     `json:"secret_push_protection_enabled"`
 	Errors                      []string `json:"errors"`
 }
 
-// Gets a string representation of the GroupSecuritySettings data.
+// String gets a string representation of the GroupSecuritySettings data.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/group_security_settings/
@@ -54,7 +54,7 @@ func (s GroupSecuritySettings) String() string {
 	return Stringify(s)
 }
 
-// GetGroupSecuritySettingsOptions represent the request options for updating
+// UpdateGroupSecuritySettingsOptions represent the request options for updating
 // the group security settings.
 //
 // GitLab API docs:
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/group_serviceaccounts.go b/vendor/gitlab.com/gitlab-org/api/client-go/group_serviceaccounts.go
index 01fba66ac..8021b56de 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/group_serviceaccounts.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/group_serviceaccounts.go
@@ -19,6 +19,7 @@ package gitlab
 import (
 	"fmt"
 	"net/http"
+	"time"
 )
 
 // GroupServiceAccount represents a GitLab service account user.
@@ -29,6 +30,7 @@ type GroupServiceAccount struct {
 	ID       int    `json:"id"`
 	Name     string `json:"name"`
 	UserName string `json:"username"`
+	Email    string `json:"email"`
 }
 
 // ListServiceAccountsOptions represents the available ListServiceAccounts() options.
@@ -41,7 +43,7 @@ type ListServiceAccountsOptions struct {
 	Sort    *string `url:"sort,omitempty" json:"sort,omitempty"`
 }
 
-// ListServiceAccounts gets a list of service acxcounts.
+// ListServiceAccounts gets a list of service accounts.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/group_service_accounts/#list-all-service-account-users
@@ -73,9 +75,10 @@ func (s *GroupsService) ListServiceAccounts(gid any, opt *ListServiceAccountsOpt
 type CreateServiceAccountOptions struct {
 	Name     *string `url:"name,omitempty" json:"name,omitempty"`
 	Username *string `url:"username,omitempty" json:"username,omitempty"`
+	Email    *string `url:"email,omitempty" json:"email,omitempty"`
 }
 
-// Creates a service account user.
+// CreateServiceAccount creates a service account user.
 //
 // This API endpoint works on top-level groups only. It does not work on subgroups.
 //
@@ -102,6 +105,42 @@ func (s *GroupsService) CreateServiceAccount(gid any, opt *CreateServiceAccountO
 	return sa, resp, nil
 }
 
+// UpdateServiceAccountOptions represents the available UpdateServiceAccount() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_service_accounts/#update-a-service-account-user
+type UpdateServiceAccountOptions struct {
+	Name     *string `url:"name,omitempty" json:"name,omitempty"`
+	Username *string `url:"username,omitempty" json:"username,omitempty"`
+}
+
+// UpdateServiceAccount updates a service account user.
+//
+// This API endpoint works on top-level groups only. It does not work on subgroups.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_service_accounts/#update-a-service-account-user
+func (s *GroupsService) UpdateServiceAccount(gid any, serviceAccount int, opt *UpdateServiceAccountOptions, options ...RequestOptionFunc) (*GroupServiceAccount, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/service_accounts/%d", PathEscape(group), serviceAccount)
+
+	req, err := s.client.NewRequest(http.MethodPatch, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	sa := new(GroupServiceAccount)
+	resp, err := s.client.Do(req, sa)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return sa, resp, nil
+}
+
 // DeleteServiceAccountOptions represents the available DeleteServiceAccount() options.
 //
 // GitLab API docs:
@@ -131,15 +170,62 @@ func (s *GroupsService) DeleteServiceAccount(gid any, serviceAccount int, opt *D
 	return s.client.Do(req, nil)
 }
 
+// ListServiceAccountPersonalAccessTokensOptions represents the available
+// ListServiceAccountPersonalAccessTokens() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_service_accounts/#list-all-personal-access-tokens-for-a-service-account-user
+type ListServiceAccountPersonalAccessTokensOptions struct {
+	ListOptions
+	CreatedAfter   *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"`
+	CreatedBefore  *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
+	ExpiresAfter   *ISOTime   `url:"expires_after,omitempty" json:"expires_after,omitempty"`
+	ExpiresBefore  *ISOTime   `url:"expires_before,omitempty" json:"expires_before,omitempty"`
+	LastUsedAfter  *time.Time `url:"last_used_after,omitempty" json:"last_used_after,omitempty"`
+	LastUsedBefore *time.Time `url:"last_used_before,omitempty" json:"last_used_before,omitempty"`
+	Revoked        *bool      `url:"revoked,omitempty" json:"revoked,omitempty"`
+	UserID         *int       `url:"user_id,omitempty" json:"user_id,omitempty"`
+	Search         *string    `url:"search,omitempty" json:"search,omitempty"`
+	Sort           *string    `url:"sort,omitempty" json:"sort,omitempty"`
+	State          *string    `url:"state,omitempty" json:"state,omitempty"`
+}
+
+// ListServiceAccountPersonalAccessTokens gets a list of personal access tokens for a
+// service account user for a group.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_service_accounts/#list-all-personal-access-tokens-for-a-service-account-user
+func (s *GroupsService) ListServiceAccountPersonalAccessTokens(gid any, serviceAccount int, opt *ListServiceAccountPersonalAccessTokensOptions, options ...RequestOptionFunc) ([]*PersonalAccessToken, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/service_accounts/%d/personal_access_tokens", PathEscape(group), serviceAccount)
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var pats []*PersonalAccessToken
+	resp, err := s.client.Do(req, &pats)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pats, resp, nil
+}
+
 // CreateServiceAccountPersonalAccessTokenOptions represents the available
 // CreateServiceAccountPersonalAccessToken() options.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/group_service_accounts/#create-a-personal-access-token-for-a-service-account-user
 type CreateServiceAccountPersonalAccessTokenOptions struct {
-	Name      *string   `url:"name,omitempty" json:"name,omitempty"`
-	Scopes    *[]string `url:"scopes,omitempty" json:"scopes,omitempty"`
-	ExpiresAt *ISOTime  `url:"expires_at,omitempty" json:"expires_at,omitempty"`
+	Name        *string   `url:"name,omitempty" json:"name,omitempty"`
+	Description *string   `url:"description,omitempty" json:"description,omitempty"`
+	Scopes      *[]string `url:"scopes,omitempty" json:"scopes,omitempty"`
+	ExpiresAt   *ISOTime  `url:"expires_at,omitempty" json:"expires_at,omitempty"`
 }
 
 // CreateServiceAccountPersonalAccessToken add a new Personal Access Token for a
@@ -168,6 +254,26 @@ func (s *GroupsService) CreateServiceAccountPersonalAccessToken(gid any, service
 	return pat, resp, nil
 }
 
+// RevokeServiceAccountPersonalAccessToken revokes a personal access token for an
+// existing service account user in a given top-level group.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_service_accounts/#revoke-a-personal-access-token-for-a-service-account-user
+func (s *GroupsService) RevokeServiceAccountPersonalAccessToken(gid any, serviceAccount, token int, options ...RequestOptionFunc) (*Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("groups/%s/service_accounts/%d/personal_access_tokens/%d", PathEscape(group), serviceAccount, token)
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
 // RotateServiceAccountPersonalAccessTokenOptions represents the available RotateServiceAccountPersonalAccessToken()
 // options.
 //
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/group_ssh_certificates.go b/vendor/gitlab.com/gitlab-org/api/client-go/group_ssh_certificates.go
index 27f9dddbd..62408d1c8 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/group_ssh_certificates.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/group_ssh_certificates.go
@@ -38,7 +38,7 @@ type GroupSSHCertificate struct {
 // ListGroupSSHCertificates gets a list of SSH certificates for a specified
 // group.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/group_ssh_certificates/#get-all-ssh-certificates-for-a-particular-group
 func (s *GroupSSHCertificatesService) ListGroupSSHCertificates(gid any, options ...RequestOptionFunc) ([]*GroupSSHCertificate, *Response, error) {
 	group, err := parseID(gid)
@@ -71,9 +71,9 @@ type CreateGroupSSHCertificateOptions struct {
 	Title *string `url:"title,omitempty" json:"title,omitempty"`
 }
 
-// CreateMemberRole creates a new member role for a specified group.
+// CreateGroupSSHCertificate creates a new SSH certificate in the group.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/group_ssh_certificates/#create-ssh-certificate
 func (s *GroupSSHCertificatesService) CreateGroupSSHCertificate(gid any, opt *CreateGroupSSHCertificateOptions, options ...RequestOptionFunc) (*GroupSSHCertificate, *Response, error) {
 	group, err := parseID(gid)
@@ -98,7 +98,7 @@ func (s *GroupSSHCertificatesService) CreateGroupSSHCertificate(gid any, opt *Cr
 
 // DeleteGroupSSHCertificate deletes a SSH certificate from a specified group.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/group_ssh_certificates/#delete-group-ssh-certificate
 func (s *GroupSSHCertificatesService) DeleteGroupSSHCertificate(gid any, cert int, options ...RequestOptionFunc) (*Response, error) {
 	group, err := parseID(gid)
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/group_wikis.go b/vendor/gitlab.com/gitlab-org/api/client-go/group_wikis.go
index b04fd9983..3ed22e498 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/group_wikis.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/group_wikis.go
@@ -32,7 +32,7 @@ type (
 	}
 
 	// GroupWikisService handles communication with the group wikis related methods of
-	// the Gitlab API.
+	// the GitLab API.
 	//
 	// GitLab API docs: https://docs.gitlab.com/api/group_wikis/
 	GroupWikisService struct {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/groups.go b/vendor/gitlab.com/gitlab-org/api/client-go/groups.go
index 0212d6946..a45313428 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/groups.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/groups.go
@@ -79,8 +79,11 @@ type (
 		// group_serviceaccounts.go
 		ListServiceAccounts(gid any, opt *ListServiceAccountsOptions, options ...RequestOptionFunc) ([]*GroupServiceAccount, *Response, error)
 		CreateServiceAccount(gid any, opt *CreateServiceAccountOptions, options ...RequestOptionFunc) (*GroupServiceAccount, *Response, error)
+		UpdateServiceAccount(gid any, serviceAccount int, opt *UpdateServiceAccountOptions, options ...RequestOptionFunc) (*GroupServiceAccount, *Response, error)
 		DeleteServiceAccount(gid any, serviceAccount int, opt *DeleteServiceAccountOptions, options ...RequestOptionFunc) (*Response, error)
+		ListServiceAccountPersonalAccessTokens(gid any, serviceAccount int, opt *ListServiceAccountPersonalAccessTokensOptions, options ...RequestOptionFunc) ([]*PersonalAccessToken, *Response, error)
 		CreateServiceAccountPersonalAccessToken(gid any, serviceAccount int, opt *CreateServiceAccountPersonalAccessTokenOptions, options ...RequestOptionFunc) (*PersonalAccessToken, *Response, error)
+		RevokeServiceAccountPersonalAccessToken(gid any, serviceAccount, token int, options ...RequestOptionFunc) (*Response, error)
 		RotateServiceAccountPersonalAccessToken(gid any, serviceAccount, token int, opt *RotateServiceAccountPersonalAccessTokenOptions, options ...RequestOptionFunc) (*PersonalAccessToken, *Response, error)
 
 		// group_members.go
@@ -776,7 +779,7 @@ func (s *GroupsService) DeleteGroup(gid any, opt *DeleteGroupOptions, options ..
 
 // RestoreGroup restores a previously deleted group
 //
-// GitLap API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/groups/#restore-a-group-marked-for-deletion
 func (s *GroupsService) RestoreGroup(gid any, options ...RequestOptionFunc) (*Group, *Response, error) {
 	group, err := parseID(gid)
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/import.go b/vendor/gitlab.com/gitlab-org/api/client-go/import.go
index 693fc598c..019c4b726 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/import.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/import.go
@@ -82,7 +82,7 @@ type ImportRepositoryFromGitHubOptions struct {
 	TimeoutStrategy *string `url:"timeout_strategy,omitempty" json:"timeout_strategy,omitempty"`
 }
 
-// Import a repository from GitHub.
+// ImportRepositoryFromGitHub imports a repository from GitHub.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/import/#import-repository-from-github
@@ -130,7 +130,7 @@ type CancelGitHubProjectImportOptions struct {
 	ProjectID *int `url:"project_id,omitempty" json:"project_id,omitempty"`
 }
 
-// Cancel an import of a repository from GitHub.
+// CancelGitHubProjectImport cancels an import of a repository from GitHub.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/import/#cancel-github-project-import
@@ -158,7 +158,7 @@ type ImportGitHubGistsIntoGitLabSnippetsOptions struct {
 	PersonalAccessToken *string `url:"personal_access_token,omitempty" json:"personal_access_token,omitempty"`
 }
 
-// Import personal GitHub Gists into personal GitLab Snippets.
+// ImportGitHubGistsIntoGitLabSnippets imports personal GitHub Gists into personal GitLab Snippets.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/import/#import-github-gists-into-gitlab-snippets
@@ -203,7 +203,7 @@ type ImportRepositoryFromBitbucketServerOptions struct {
 	TimeoutStrategy         *string `url:"timeout_strategy,omitempty" json:"timeout_strategy,omitempty"`
 }
 
-// Import a repository from Bitbucket Server.
+// ImportRepositoryFromBitbucketServer imports a repository from Bitbucket Server.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/import/#import-repository-from-bitbucket-server
@@ -258,7 +258,7 @@ type ImportRepositoryFromBitbucketCloudOptions struct {
 	NewName              *string `url:"new_name,omitempty" json:"new_name,omitempty"`
 }
 
-// Import a repository from Bitbucket Cloud.
+// ImportRepositoryFromBitbucketCloud imports a repository from Bitbucket Cloud.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/import/#import-repository-from-bitbucket-cloud
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/integrations.go b/vendor/gitlab.com/gitlab-org/api/client-go/integrations.go
new file mode 100644
index 000000000..d9fdd1315
--- /dev/null
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/integrations.go
@@ -0,0 +1,281 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import (
+	"net/http"
+	"time"
+)
+
+type (
+	IntegrationsServiceInterface interface {
+		// ListActiveGroupIntegrations gets a list of all active group integrations.
+		// The vulnerability_events field is only available for GitLab Enterprise Edition.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_integrations/#list-all-active-integrations
+		ListActiveGroupIntegrations(gid any, opt *ListActiveIntegrationsOptions, options ...RequestOptionFunc) ([]*Integration, *Response, error)
+
+		// SetUpGroupHarbor sets up the Harbor integration for a group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_integrations/#set-up-harbor
+		SetUpGroupHarbor(gid any, opt *SetUpHarborOptions, options ...RequestOptionFunc) (*Integration, *Response, error)
+
+		// DisableGroupHarbor disables the Harbor integration for a group.
+		// Integration settings are reset.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_integrations/#disable-harbor
+		DisableGroupHarbor(gid any, options ...RequestOptionFunc) (*Response, error)
+
+		// GetGroupHarborSettings gets the Harbor integration for a group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_integrations/#get-harbor-settings
+		GetGroupHarborSettings(gid any, options ...RequestOptionFunc) (*Integration, *Response, error)
+
+		// SetGroupMicrosoftTeamsNotifications sets up Microsoft Teams notifications for a group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_integrations/#set-up-microsoft-teams-notifications
+		SetGroupMicrosoftTeamsNotifications(gid any, opt *SetMicrosoftTeamsNotificationsOptions, options ...RequestOptionFunc) (*Integration, *Response, error)
+
+		// DisableGroupMicrosoftTeamsNotifications disables Microsoft Teams notifications
+		// for a group. Integration settings are reset.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_integrations/#disable-microsoft-teams-notifications
+		DisableGroupMicrosoftTeamsNotifications(gid any, options ...RequestOptionFunc) (*Response, error)
+
+		// GetGroupMicrosoftTeamsNotifications gets the Microsoft Teams notifications for a group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_integrations/#get-microsoft-teams-notifications-settings
+		GetGroupMicrosoftTeamsNotifications(gid any, options ...RequestOptionFunc) (*Integration, *Response, error)
+
+		// SetUpGroupJira sets up the Jira integration for a group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_integrations/#set-up-jira
+		SetUpGroupJira(gid any, opt *SetUpJiraOptions, options ...RequestOptionFunc) (*Integration, *Response, error)
+
+		// DisableGroupJira disables the Jira integration for a group.
+		// Integration settings are reset.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_integrations/#disable-jira
+		DisableGroupJira(gid any, options ...RequestOptionFunc) (*Response, error)
+
+		// GetGroupJiraSettings gets the Jira integration for a group.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/group_integrations/#get-jira-settings
+		GetGroupJiraSettings(gid any, options ...RequestOptionFunc) (*Integration, *Response, error)
+	}
+
+	// IntegrationsService handles communication with the group
+	// integrations related methods of the GitLab API.
+	//
+	// GitLab API docs: https://docs.gitlab.com/ee/api/group_integrations.html
+	IntegrationsService struct {
+		client *Client
+	}
+)
+
+var _ IntegrationsServiceInterface = (*IntegrationsService)(nil)
+
+// Integration represents a GitLab group or project integration.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_integrations/
+// https://docs.gitlab.com/api/project_integrations/
+type Integration struct {
+	ID                             int        `json:"id"`
+	Title                          string     `json:"title"`
+	Slug                           string     `json:"slug"`
+	CreatedAt                      *time.Time `json:"created_at"`
+	UpdatedAt                      *time.Time `json:"updated_at"`
+	Active                         bool       `json:"active"`
+	AlertEvents                    bool       `json:"alert_events"`
+	CommitEvents                   bool       `json:"commit_events"`
+	ConfidentialIssuesEvents       bool       `json:"confidential_issues_events"`
+	ConfidentialNoteEvents         bool       `json:"confidential_note_events"`
+	DeploymentEvents               bool       `json:"deployment_events"`
+	GroupConfidentialMentionEvents bool       `json:"group_confidential_mention_events"`
+	GroupMentionEvents             bool       `json:"group_mention_events"`
+	IncidentEvents                 bool       `json:"incident_events"`
+	IssuesEvents                   bool       `json:"issues_events"`
+	JobEvents                      bool       `json:"job_events"`
+	MergeRequestsEvents            bool       `json:"merge_requests_events"`
+	NoteEvents                     bool       `json:"note_events"`
+	PipelineEvents                 bool       `json:"pipeline_events"`
+	PushEvents                     bool       `json:"push_events"`
+	TagPushEvents                  bool       `json:"tag_push_events"`
+	VulnerabilityEvents            bool       `json:"vulnerability_events"`
+	WikiPageEvents                 bool       `json:"wiki_page_events"`
+	CommentOnEventEnabled          bool       `json:"comment_on_event_enabled"`
+	Inherited                      bool       `json:"inherited"`
+}
+
+// ListActiveIntegrationsOptions represents the available
+// ListActiveIntegrations() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_integrations/#list-all-active-integrations
+type ListActiveIntegrationsOptions struct {
+	ListOptions
+}
+
+func (s *IntegrationsService) ListActiveGroupIntegrations(gid any, opt *ListActiveIntegrationsOptions, options ...RequestOptionFunc) ([]*Integration, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	return do[[]*Integration](s.client, withPath("groups/%s/integrations", group), withMethod(http.MethodGet), withAPIOpts(opt), withRequestOpts(options...))
+}
+
+// SetUpHarborOptions represents the available SetUpGroupHarbor()
+// options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_integrations/#set-up-harbor
+type SetUpHarborOptions struct {
+	URL                  *string `url:"url,omitempty" json:"url,omitempty"`
+	ProjectName          *string `url:"project_name,omitempty" json:"project_name,omitempty"`
+	Username             *string `url:"username,omitempty" json:"username,omitempty"`
+	Password             *string `url:"password,omitempty" json:"password,omitempty"`
+	UseInheritedSettings *bool   `url:"use_inherited_settings,omitempty" json:"use_inherited_settings,omitempty"`
+}
+
+func (s *IntegrationsService) SetUpGroupHarbor(gid any, opt *SetUpHarborOptions, options ...RequestOptionFunc) (*Integration, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	return do[*Integration](s.client, withPath("groups/%s/integrations/harbor", group), withMethod(http.MethodPut), withAPIOpts(opt), withRequestOpts(options...))
+}
+
+func (s *IntegrationsService) DisableGroupHarbor(gid any, options ...RequestOptionFunc) (*Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, err
+	}
+	_, resp, err := do[none](s.client, withPath("groups/%s/integrations/harbor", group), withMethod(http.MethodDelete), withRequestOpts(options...))
+	return resp, err
+}
+
+func (s *IntegrationsService) GetGroupHarborSettings(gid any, options ...RequestOptionFunc) (*Integration, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	return do[*Integration](s.client, withPath("groups/%s/integrations/harbor", group), withMethod(http.MethodGet), withRequestOpts(options...))
+}
+
+// SetMicrosoftTeamsNotificationsOptions represents the available
+// SetMicrosoftTeamsNotifications() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_integrations/#set-up-microsoft-teams-notifications
+type SetMicrosoftTeamsNotificationsOptions struct {
+	Targets                   *string `url:"targets,omitempty"`
+	Webhook                   *string `url:"webhook,omitempty"`
+	NotifyOnlyBrokenPipelines *bool   `url:"notify_only_broken_pipelines,omitempty"`
+	NotifyOnlyDefaultBranch   *bool   `url:"notify_only_default_branch,omitempty"`
+	BranchesToBeNotified      *string `url:"branches_to_be_notified,omitempty"`
+	PushEvents                *bool   `url:"push_events,omitempty"`
+	IssuesEvents              *bool   `url:"issues_events,omitempty"`
+	ConfidentialIssuesEvents  *bool   `url:"confidential_issues_events,omitempty"`
+	MergeRequestsEvents       *bool   `url:"merge_requests_events,omitempty"`
+	TagPushEvents             *bool   `url:"tag_push_events,omitempty"`
+	NoteEvents                *bool   `url:"note_events,omitempty"`
+	ConfidentialNoteEvents    *bool   `url:"confidential_note_events,omitempty"`
+	PipelineEvents            *bool   `url:"pipeline_events,omitempty"`
+	WikiPageEvents            *bool   `url:"wiki_page_events,omitempty"`
+	UseInheritedSettings      *bool   `url:"use_inherited_settings,omitempty"`
+}
+
+func (s *IntegrationsService) SetGroupMicrosoftTeamsNotifications(gid any, opt *SetMicrosoftTeamsNotificationsOptions, options ...RequestOptionFunc) (*Integration, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	return do[*Integration](s.client, withPath("groups/%s/integrations/microsoft_teams", group), withMethod(http.MethodPut), withAPIOpts(opt), withRequestOpts(options...))
+}
+
+func (s *IntegrationsService) DisableGroupMicrosoftTeamsNotifications(gid any, options ...RequestOptionFunc) (*Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, err
+	}
+	_, resp, err := do[none](s.client, withPath("groups/%s/integrations/microsoft_teams", group), withMethod(http.MethodDelete), withRequestOpts(options...))
+	return resp, err
+}
+
+func (s *IntegrationsService) GetGroupMicrosoftTeamsNotifications(gid any, options ...RequestOptionFunc) (*Integration, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	return do[*Integration](s.client, withPath("groups/%s/integrations/microsoft_teams", group), withMethod(http.MethodGet), withRequestOpts(options...))
+}
+
+// SetUpJiraOptions represents the available SetUpJira() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/group_integrations/#set-up-jira
+type SetUpJiraOptions struct {
+	URL                          *string   `url:"url,omitempty" json:"url,omitempty"`
+	APIURL                       *string   `url:"api_url,omitempty" json:"api_url,omitempty"`
+	Username                     *string   `url:"username,omitempty" json:"username,omitempty"`
+	Password                     *string   `url:"password,omitempty" json:"password,omitempty"`
+	Active                       *bool     `url:"active,omitempty" json:"active,omitempty"`
+	JiraAuthType                 *int64    `url:"jira_auth_type,omitempty" json:"jira_auth_type,omitempty"`
+	JiraIssuePrefix              *string   `url:"jira_issue_prefix,omitempty" json:"jira_issue_prefix,omitempty"`
+	JiraIssueRegex               *string   `url:"jira_issue_regex,omitempty" json:"jira_issue_regex,omitempty"`
+	JiraIssueTransitionAutomatic *bool     `url:"jira_issue_transition_automatic,omitempty" json:"jira_issue_transition_automatic,omitempty"`
+	JiraIssueTransitionID        *string   `url:"jira_issue_transition_id,omitempty" json:"jira_issue_transition_id,omitempty"`
+	CommitEvents                 *bool     `url:"commit_events,omitempty" json:"commit_events,omitempty"`
+	MergeRequestsEvents          *bool     `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"`
+	CommentOnEventEnabled        *bool     `url:"comment_on_event_enabled,omitempty" json:"comment_on_event_enabled,omitempty"`
+	IssuesEnabled                *bool     `url:"issues_enabled,omitempty" json:"issues_enabled,omitempty"`
+	ProjectKeys                  *[]string `url:"project_keys,omitempty" json:"project_keys,omitempty"`
+	UseInheritedSettings         *bool     `url:"use_inherited_settings,omitempty" json:"use_inherited_settings,omitempty"`
+}
+
+func (s *IntegrationsService) SetUpGroupJira(gid any, opt *SetUpJiraOptions, options ...RequestOptionFunc) (*Integration, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	return do[*Integration](s.client, withPath("groups/%s/integrations/jira", group), withMethod(http.MethodPut), withAPIOpts(opt), withRequestOpts(options...))
+}
+
+func (s *IntegrationsService) DisableGroupJira(gid any, options ...RequestOptionFunc) (*Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, err
+	}
+	_, resp, err := do[none](s.client, withPath("groups/%s/integrations/jira", group), withMethod(http.MethodDelete), withRequestOpts(options...))
+	return resp, err
+}
+
+func (s *IntegrationsService) GetGroupJiraSettings(gid any, options ...RequestOptionFunc) (*Integration, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	return do[*Integration](s.client, withPath("groups/%s/integrations/jira", group), withMethod(http.MethodGet), withRequestOpts(options...))
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/issues.go b/vendor/gitlab.com/gitlab-org/api/client-go/issues.go
index 102047053..cd2564e50 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/issues.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/issues.go
@@ -282,9 +282,9 @@ type ListGroupIssuesOptions struct {
 	AuthorUsername    *string       `url:"author_username,omitempty" json:"author_username,omitempty"`
 	NotAuthorUsername *string       `url:"not[author_username],omitempty" json:"not[author_username],omitempty"`
 
-	// AssigneeID is defined as an int the the documentation, however, the field
+	// AssigneeID is defined as an int in the documentation, however, the field
 	// must be able to accept Assignee IDs and the words 'None' and 'Any'.  Use
-	// *AssigneIDValue instead of *int.
+	// *AssigneeIDValue instead of *int.
 	AssigneeID          *AssigneeIDValue `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
 	NotAssigneeID       *int             `url:"not[assignee_id],omitempty" json:"not[assignee_id],omitempty"`
 	AssigneeUsername    *string          `url:"assignee_username,omitempty" json:"assignee_username,omitempty"`
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/job_token_scope.go b/vendor/gitlab.com/gitlab-org/api/client-go/job_token_scope.go
index 1421f5913..93239844d 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/job_token_scope.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/job_token_scope.go
@@ -11,6 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+
 package gitlab
 
 import (
@@ -144,7 +145,7 @@ func (j *JobTokenScopeService) GetProjectJobTokenInboundAllowList(pid any, opt *
 	return ps, resp, nil
 }
 
-// AddProjectToJobScopeAllowListOptions represents the available
+// JobTokenInboundAllowOptions represents the available
 // AddProjectToJobScopeAllowList() options.
 //
 // GitLab API docs:
@@ -216,7 +217,7 @@ type GetJobTokenAllowlistGroupsOptions struct {
 	ListOptions
 }
 
-// GetJobTokenAllowListGroups fetches the CI/CD job token allowlist groups
+// GetJobTokenAllowlistGroups fetches the CI/CD job token allowlist groups
 // (job token scopes) of a project.
 //
 // GitLab API docs:
@@ -251,7 +252,7 @@ type AddGroupToJobTokenAllowlistOptions struct {
 	TargetGroupID *int `url:"target_group_id,omitempty" json:"target_group_id,omitempty"`
 }
 
-// AddProjectToJobScopeGroupsAllowList adds a new group to a project's job token
+// AddGroupToJobTokenAllowlist adds a new group to a project's job token
 // inbound groups allow list.
 //
 // GitLab API docs:
@@ -277,7 +278,7 @@ func (j *JobTokenScopeService) AddGroupToJobTokenAllowlist(pid any, opt *AddGrou
 	return jt, resp, nil
 }
 
-// RemoveGroupFromJopTokenAllowlist removes a group from a project's job
+// RemoveGroupFromJobTokenAllowlist removes a group from a project's job
 // token inbound groups allow list.
 //
 // GitLab API docs:
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/jobs.go b/vendor/gitlab.com/gitlab-org/api/client-go/jobs.go
index 2b6ba1acc..94aef9540 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/jobs.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/jobs.go
@@ -366,10 +366,9 @@ func (s *JobsService) DownloadSingleArtifactsFile(pid any, jobID int, artifactPa
 	return bytes.NewReader(artifactBuf.Bytes()), resp, err
 }
 
-// DownloadSingleArtifactsFile download a single artifact file for a specific
-// job of the latest successful pipeline for the given reference name from
-// inside the job’s artifacts archive. The file is extracted from the archive
-// and streamed to the client.
+// DownloadSingleArtifactsFileByTagOrBranch downloads a single file from
+// a job’s artifacts in the latest successful pipeline using the reference name.
+// The file is extracted from the archive and streamed to the client.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/job_artifacts/#download-a-single-artifact-file-from-specific-tag-or-branch
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/markdown.go b/vendor/gitlab.com/gitlab-org/api/client-go/markdown.go
index 8e2e30f65..af02391eb 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/markdown.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/markdown.go
@@ -20,14 +20,14 @@ var _ MarkdownServiceInterface = (*MarkdownService)(nil)
 
 // Markdown represents a markdown document.
 //
-// Gitlab API docs: https://docs.gitlab.com/api/markdown/
+// GitLab API docs: https://docs.gitlab.com/api/markdown/
 type Markdown struct {
 	HTML string `json:"html"`
 }
 
 // RenderOptions represents the available Render() options.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/markdown/#render-an-arbitrary-markdown-document
 type RenderOptions struct {
 	Text                    *string `url:"text,omitempty" json:"text,omitempty"`
@@ -37,7 +37,7 @@ type RenderOptions struct {
 
 // Render an arbitrary markdown document.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/markdown/#render-an-arbitrary-markdown-document
 func (s *MarkdownService) Render(opt *RenderOptions, options ...RequestOptionFunc) (*Markdown, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodPost, "markdown", opt, options)
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/markdown_uploads.go b/vendor/gitlab.com/gitlab-org/api/client-go/markdown_uploads.go
index 37a6831c5..13b662394 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/markdown_uploads.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/markdown_uploads.go
@@ -25,7 +25,7 @@ import (
 
 // MarkdownUpload represents a single markdown upload.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/project_markdown_uploads/
 // https://docs.gitlab.com/api/group_markdown_uploads/
 type MarkdownUpload struct {
@@ -36,7 +36,7 @@ type MarkdownUpload struct {
 	UploadedBy *User      `json:"uploaded_by"`
 }
 
-// Gets a string representation of a MarkdownUpload.
+// String gets a string representation of a MarkdownUpload.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/project_markdown_uploads/
@@ -47,7 +47,7 @@ func (m MarkdownUpload) String() string {
 
 // MarkdownUploadedFile represents a single markdown uploaded file.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/project_markdown_uploads/
 type MarkdownUploadedFile struct {
 	ID       int    `json:"id"`
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/member_roles.go b/vendor/gitlab.com/gitlab-org/api/client-go/member_roles.go
index e9fede214..1b1d7daab 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/member_roles.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/member_roles.go
@@ -62,7 +62,7 @@ type MemberRole struct {
 // ListInstanceMemberRoles gets all member roles in an instance.
 // Authentication as Administrator is required.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/member_roles/#get-all-instance-member-roles
 func (s *MemberRolesService) ListInstanceMemberRoles(options ...RequestOptionFunc) ([]*MemberRole, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodGet, "member_roles", nil, options)
@@ -113,7 +113,7 @@ type CreateMemberRoleOptions struct {
 
 // CreateInstanceMemberRole creates an instance-wide member role.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/member_roles/#create-a-instance-member-role
 func (s *MemberRolesService) CreateInstanceMemberRole(opt *CreateMemberRoleOptions, options ...RequestOptionFunc) (*MemberRole, *Response, error) {
 	req, err := s.client.NewRequest(http.MethodPost, "member_roles", opt, options)
@@ -132,7 +132,7 @@ func (s *MemberRolesService) CreateInstanceMemberRole(opt *CreateMemberRoleOptio
 
 // DeleteInstanceMemberRole deletes a member role from a specified group.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/member_roles/#delete-an-instance-member-role
 func (s *MemberRolesService) DeleteInstanceMemberRole(memberRoleID int, options ...RequestOptionFunc) (*Response, error) {
 	u := fmt.Sprintf("member_roles/%d", memberRoleID)
@@ -147,7 +147,7 @@ func (s *MemberRolesService) DeleteInstanceMemberRole(memberRoleID int, options
 
 // ListMemberRoles gets a list of member roles for a specified group.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/member_roles/#get-all-group-member-roles
 func (s *MemberRolesService) ListMemberRoles(gid any, options ...RequestOptionFunc) ([]*MemberRole, *Response, error) {
 	group, err := parseID(gid)
@@ -172,7 +172,7 @@ func (s *MemberRolesService) ListMemberRoles(gid any, options ...RequestOptionFu
 
 // CreateMemberRole creates a new member role for a specified group.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/member_roles/#add-a-member-role-to-a-group
 func (s *MemberRolesService) CreateMemberRole(gid any, opt *CreateMemberRoleOptions, options ...RequestOptionFunc) (*MemberRole, *Response, error) {
 	group, err := parseID(gid)
@@ -197,7 +197,7 @@ func (s *MemberRolesService) CreateMemberRole(gid any, opt *CreateMemberRoleOpti
 
 // DeleteMemberRole deletes a member role from a specified group.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/member_roles/#remove-member-role-of-a-group
 func (s *MemberRolesService) DeleteMemberRole(gid any, memberRole int, options ...RequestOptionFunc) (*Response, error) {
 	group, err := parseID(gid)
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/merge_request_context_commits.go b/vendor/gitlab.com/gitlab-org/api/client-go/merge_request_context_commits.go
new file mode 100644
index 000000000..08819fcd7
--- /dev/null
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/merge_request_context_commits.go
@@ -0,0 +1,116 @@
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+)
+
+type (
+	// MergeRequestContextCommitsServiceInterface handles communication with the
+	// merge request context commits related methods of the GitLab API.
+	MergeRequestContextCommitsServiceInterface interface {
+		// ListMergeRequestContextCommits gets a list of merge request context commits.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/merge_request_context_commits/#list-mr-context-commits
+		ListMergeRequestContextCommits(pid any, mergeRequest int64, options ...RequestOptionFunc) ([]*Commit, *Response, error)
+		// CreateMergeRequestContextCommits creates a list of merge request context
+		// commits.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/merge_request_context_commits/#create-mr-context-commits
+		CreateMergeRequestContextCommits(pid any, mergeRequest int64, opt *CreateMergeRequestContextCommitsOptions, options ...RequestOptionFunc) ([]*Commit, *Response, error)
+		// DeleteMergeRequestContextCommits deletes a list of merge request context
+		// commits.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/merge_request_context_commits/#delete-mr-context-commits
+		DeleteMergeRequestContextCommits(pid any, mergeRequest int64, opt *DeleteMergeRequestContextCommitsOptions, options ...RequestOptionFunc) (*Response, error)
+	}
+
+	// MergeRequestContextCommitsService handles communication with the merge
+	// request context commits related methods of the GitLab API.
+	//
+	// GitLab API docs:
+	// https://docs.gitlab.com/api/merge_request_context_commits/
+	MergeRequestContextCommitsService struct {
+		client *Client
+	}
+)
+
+var _ MergeRequestContextCommitsServiceInterface = (*MergeRequestContextCommitsService)(nil)
+
+func (s *MergeRequestContextCommitsService) ListMergeRequestContextCommits(pid any, mergeRequest int64, options ...RequestOptionFunc) ([]*Commit, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/merge_requests/%d/context_commits", PathEscape(project), mergeRequest)
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var c []*Commit
+	resp, err := s.client.Do(req, &c)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return c, resp, nil
+}
+
+// CreateMergeRequestContextCommitsOptions represents the available
+// CreateMergeRequestContextCommits() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/merge_request_context_commits/#create-mr-context-commits
+type CreateMergeRequestContextCommitsOptions struct {
+	Commits *[]string `url:"commits,omitempty" json:"commits,omitempty"`
+}
+
+func (s *MergeRequestContextCommitsService) CreateMergeRequestContextCommits(pid any, mergeRequest int64, opt *CreateMergeRequestContextCommitsOptions, options ...RequestOptionFunc) ([]*Commit, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/merge_requests/%d/context_commits", PathEscape(project), mergeRequest)
+
+	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var c []*Commit
+	resp, err := s.client.Do(req, &c)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return c, resp, nil
+}
+
+// DeleteMergeRequestContextCommitsOptions represents the available
+// DeleteMergeRequestContextCommits() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/merge_request_context_commits/#delete-mr-context-commits
+type DeleteMergeRequestContextCommitsOptions struct {
+	Commits *[]string `url:"commits,omitempty" json:"commits,omitempty"`
+}
+
+func (s *MergeRequestContextCommitsService) DeleteMergeRequestContextCommits(pid any, mergeRequest int64, opt *DeleteMergeRequestContextCommitsOptions, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/merge_requests/%d/context_commits", PathEscape(project), mergeRequest)
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, opt, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/merge_requests.go b/vendor/gitlab.com/gitlab-org/api/client-go/merge_requests.go
index 08578fcd8..ab2d6c443 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/merge_requests.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/merge_requests.go
@@ -204,9 +204,9 @@ func (m *MergeRequest) UnmarshalJSON(data []byte) error {
 	return json.Unmarshal(data, (*alias)(m))
 }
 
-// MergeRequestDiff represents Gitlab merge request diff.
+// MergeRequestDiff represents GitLab merge request diff.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/merge_requests/#list-merge-request-diffs
 type MergeRequestDiff struct {
 	OldPath       string `json:"old_path"`
@@ -220,9 +220,9 @@ type MergeRequestDiff struct {
 	GeneratedFile bool   `json:"generated_file"`
 }
 
-// MergeRequestDiffVersion represents Gitlab merge request version.
+// MergeRequestDiffVersion represents GitLab merge request version.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/merge_requests/#get-merge-request-diff-versions
 type MergeRequestDiffVersion struct {
 	ID             int        `json:"id"`
@@ -293,13 +293,13 @@ func (s *MergeRequestsService) ListMergeRequests(opt *ListMergeRequestsOptions,
 		return nil, nil, err
 	}
 
-	var m []*BasicMergeRequest
-	resp, err := s.client.Do(req, &m)
+	var mrs []*MergeRequest
+	resp, err := s.client.Do(req, &mrs)
 	if err != nil {
 		return nil, resp, err
 	}
 
-	return m, resp, nil
+	return toBasic(mrs), resp, nil
 }
 
 // ListProjectMergeRequestsOptions represents the available ListMergeRequests()
@@ -356,13 +356,13 @@ func (s *MergeRequestsService) ListProjectMergeRequests(pid any, opt *ListProjec
 		return nil, nil, err
 	}
 
-	var m []*BasicMergeRequest
-	resp, err := s.client.Do(req, &m)
+	var mrs []*MergeRequest
+	resp, err := s.client.Do(req, &mrs)
 	if err != nil {
 		return nil, resp, err
 	}
 
-	return m, resp, nil
+	return toBasic(mrs), resp, nil
 }
 
 // ListGroupMergeRequestsOptions represents the available ListGroupMergeRequests()
@@ -419,13 +419,13 @@ func (s *MergeRequestsService) ListGroupMergeRequests(gid any, opt *ListGroupMer
 		return nil, nil, err
 	}
 
-	var m []*BasicMergeRequest
-	resp, err := s.client.Do(req, &m)
+	var mrs []*MergeRequest
+	resp, err := s.client.Do(req, &mrs)
 	if err != nil {
 		return nil, resp, err
 	}
 
-	return m, resp, nil
+	return toBasic(mrs), resp, nil
 }
 
 // GetMergeRequestsOptions represents the available GetMergeRequests()
@@ -930,12 +930,15 @@ func (s *MergeRequestsService) DeleteMergeRequest(pid any, mergeRequest int, opt
 // GitLab API docs:
 // https://docs.gitlab.com/api/merge_requests/#merge-a-merge-request
 type AcceptMergeRequestOptions struct {
-	MergeCommitMessage        *string `url:"merge_commit_message,omitempty" json:"merge_commit_message,omitempty"`
-	SquashCommitMessage       *string `url:"squash_commit_message,omitempty" json:"squash_commit_message,omitempty"`
-	Squash                    *bool   `url:"squash,omitempty" json:"squash,omitempty"`
-	ShouldRemoveSourceBranch  *bool   `url:"should_remove_source_branch,omitempty" json:"should_remove_source_branch,omitempty"`
-	MergeWhenPipelineSucceeds *bool   `url:"merge_when_pipeline_succeeds,omitempty" json:"merge_when_pipeline_succeeds,omitempty"`
-	SHA                       *string `url:"sha,omitempty" json:"sha,omitempty"`
+	AutoMerge                *bool   `url:"auto_merge,omitempty" json:"auto_merge,omitempty"`
+	MergeCommitMessage       *string `url:"merge_commit_message,omitempty" json:"merge_commit_message,omitempty"`
+	SquashCommitMessage      *string `url:"squash_commit_message,omitempty" json:"squash_commit_message,omitempty"`
+	Squash                   *bool   `url:"squash,omitempty" json:"squash,omitempty"`
+	ShouldRemoveSourceBranch *bool   `url:"should_remove_source_branch,omitempty" json:"should_remove_source_branch,omitempty"`
+	SHA                      *string `url:"sha,omitempty" json:"sha,omitempty"`
+
+	// Deprecated: use AutoMerge instead
+	MergeWhenPipelineSucceeds *bool `url:"merge_when_pipeline_succeeds,omitempty" json:"merge_when_pipeline_succeeds,omitempty"`
 }
 
 // AcceptMergeRequest merges changes submitted with MR using this API. If merge
@@ -1372,3 +1375,13 @@ func (s *MergeRequestsService) GetMergeRequestDependencies(pid any, mergeRequest
 
 	return mrd, resp, err
 }
+
+func toBasic(mrs []*MergeRequest) []*BasicMergeRequest {
+	ret := make([]*BasicMergeRequest, len(mrs))
+
+	for i, mr := range mrs {
+		ret[i] = &mr.BasicMergeRequest
+	}
+
+	return ret
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/merge_trains.go b/vendor/gitlab.com/gitlab-org/api/client-go/merge_trains.go
index bfeb65801..89b37d33c 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/merge_trains.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/merge_trains.go
@@ -25,7 +25,7 @@ type (
 
 var _ MergeTrainsServiceInterface = (*MergeTrainsService)(nil)
 
-// MergeTrain represents a Gitlab merge train.
+// MergeTrain represents a GitLab merge train.
 //
 // GitLab API docs: https://docs.gitlab.com/api/merge_trains/
 type MergeTrain struct {
@@ -41,7 +41,7 @@ type MergeTrain struct {
 	Duration     int                     `json:"duration"`
 }
 
-// MergeTrainMergeRequest represents a Gitlab merge request inside merge train.
+// MergeTrainMergeRequest represents a GitLab merge request inside merge train.
 //
 // GitLab API docs: https://docs.gitlab.com/api/merge_trains/
 type MergeTrainMergeRequest struct {
@@ -58,7 +58,7 @@ type MergeTrainMergeRequest struct {
 
 // ListMergeTrainsOptions represents the available ListMergeTrain() options.
 //
-// Gitab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/merge_trains/#list-merge-trains-for-a-project
 type ListMergeTrainsOptions struct {
 	ListOptions
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/model_registry.go b/vendor/gitlab.com/gitlab-org/api/client-go/model_registry.go
new file mode 100644
index 000000000..9611e885a
--- /dev/null
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/model_registry.go
@@ -0,0 +1,61 @@
+package gitlab
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"net/url"
+)
+
+type (
+	ModelRegistryServiceInterface interface {
+		DownloadMachineLearningModelPackage(pid, modelVersionID any, path string, filename string, options ...RequestOptionFunc) (*bytes.Reader, *Response, error)
+	}
+
+	// ModelRegistryService handles communication with the model registry related methods
+	// of the GitLab API.
+	//
+	// GitLab API docs: https://docs.gitlab.com/api/model_registry/
+	ModelRegistryService struct {
+		client *Client
+	}
+)
+
+var _ ModelRegistryServiceInterface = (*ModelRegistryService)(nil)
+
+// DownloadMachineLearningModelPackage downloads a machine learning model package file.
+//
+// GitLab API docs: https://docs.gitlab.com/api/model_registry/#download-a-model-package-file
+func (s *ModelRegistryService) DownloadMachineLearningModelPackage(pid, modelVersionID any, path string, filename string, options ...RequestOptionFunc) (*bytes.Reader, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// The modelVersionID can be an int or a string like "candidate:5",
+	// so we convert it to a string for the URL.
+	mvid, err := parseID(modelVersionID)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	u := fmt.Sprintf("projects/%s/packages/ml_models/%s/files/%s/%s",
+		PathEscape(project),
+		url.PathEscape(mvid),
+		url.PathEscape(path),
+		url.PathEscape(filename),
+	)
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	registryDownload := new(bytes.Buffer)
+	resp, err := s.client.Do(req, registryDownload)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return bytes.NewReader(registryDownload.Bytes()), resp, err
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/notifications.go b/vendor/gitlab.com/gitlab-org/api/client-go/notifications.go
index 539600f3c..fab7089c8 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/notifications.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/notifications.go
@@ -43,7 +43,7 @@ type (
 
 var _ NotificationSettingsServiceInterface = (*NotificationSettingsService)(nil)
 
-// NotificationSettings represents the Gitlab notification setting.
+// NotificationSettings represents the GitLab notification setting.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/notification_settings/#valid-notification-levels
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/pages.go b/vendor/gitlab.com/gitlab-org/api/client-go/pages.go
index 379844054..baffb2559 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/pages.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/pages.go
@@ -105,7 +105,7 @@ func (s *PagesService) GetPages(gid any, options ...RequestOptionFunc) (*Pages,
 	return p, resp, nil
 }
 
-// UpdatePages represents the available UpdatePages() options.
+// UpdatePagesOptions represents the available UpdatePages() options.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/pages/#update-pages-settings-for-a-project
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/pages_domains.go b/vendor/gitlab.com/gitlab-org/api/client-go/pages_domains.go
index c6b5d79eb..de18e4830 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/pages_domains.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/pages_domains.go
@@ -209,7 +209,7 @@ func (s *PagesDomainsService) UpdatePagesDomain(pid any, domain string, opt *Upd
 	return pd, resp, nil
 }
 
-// DeletePagesDomain deletes an existing prject pages domain.
+// DeletePagesDomain deletes an existing project pages domain.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/pages_domains/#delete-pages-domain
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/pagination.go b/vendor/gitlab.com/gitlab-org/api/client-go/pagination.go
index cc7a1e708..8214e84e3 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/pagination.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/pagination.go
@@ -6,6 +6,7 @@ package gitlab
 import (
 	"fmt"
 	"iter"
+	"slices"
 )
 
 type PaginationOptionFunc = RequestOptionFunc
@@ -157,3 +158,24 @@ func Must[T any](it iter.Seq2[T, error]) iter.Seq[T] {
 		}
 	}
 }
+
+// ScanAndCollect is a convenience function that collects all results and returns them as slice as well as an error if one happens.
+//
+//	opts := &ListProjectsOptions{}
+//	projects, err := ScanAndCollect(func(p PaginationOptionFunc) ([]*Project, *Response, error) {
+//		return c.Projects.ListProjects(opts, p)
+//	}))
+//	if err != nil {
+//		// handle the error
+//	}
+//	// do something with projects
+//
+// Attention: This API is experimental and may be subject to breaking changes to improve the API in the future.
+func ScanAndCollect[T any](f func(p PaginationOptionFunc) ([]T, *Response, error)) ([]T, error) {
+	it, hasErr := Scan(f)
+	allItems := slices.Collect(it)
+	if err := hasErr(); err != nil {
+		return nil, err
+	}
+	return allItems, nil
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/personal_access_tokens.go b/vendor/gitlab.com/gitlab-org/api/client-go/personal_access_tokens.go
index 0dd5da953..9dcbe6872 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/personal_access_tokens.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/personal_access_tokens.go
@@ -84,10 +84,13 @@ type ListPersonalAccessTokensOptions struct {
 	ListOptions
 	CreatedAfter   *ISOTime `url:"created_after,omitempty" json:"created_after,omitempty"`
 	CreatedBefore  *ISOTime `url:"created_before,omitempty" json:"created_before,omitempty"`
+	ExpiresAfter   *ISOTime `url:"expires_after,omitempty" json:"expires_after,omitempty"`
+	ExpiresBefore  *ISOTime `url:"expires_before,omitempty" json:"expires_before,omitempty"`
 	LastUsedAfter  *ISOTime `url:"last_used_after,omitempty" json:"last_used_after,omitempty"`
 	LastUsedBefore *ISOTime `url:"last_used_before,omitempty" json:"last_used_before,omitempty"`
 	Revoked        *bool    `url:"revoked,omitempty" json:"revoked,omitempty"`
 	Search         *string  `url:"search,omitempty" json:"search,omitempty"`
+	Sort           *string  `url:"sort,omitempty" json:"sort,omitempty"`
 	State          *string  `url:"state,omitempty" json:"state,omitempty"`
 	UserID         *int     `url:"user_id,omitempty" json:"user_id,omitempty"`
 }
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/pipeline_schedules.go b/vendor/gitlab.com/gitlab-org/api/client-go/pipeline_schedules.go
index 2e2f156dd..e258215f2 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/pipeline_schedules.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/pipeline_schedules.go
@@ -295,7 +295,7 @@ func (s *PipelineSchedulesService) DeletePipelineSchedule(pid any, schedule int,
 
 // RunPipelineSchedule triggers a new scheduled pipeline to run immediately.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/pipeline_schedules/#run-a-scheduled-pipeline-immediately
 func (s *PipelineSchedulesService) RunPipelineSchedule(pid any, schedule int, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/pipeline_triggers.go b/vendor/gitlab.com/gitlab-org/api/client-go/pipeline_triggers.go
index deff0c89f..97835de39 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/pipeline_triggers.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/pipeline_triggers.go
@@ -207,6 +207,10 @@ type RunPipelineTriggerOptions struct {
 	Ref       *string           `url:"ref" json:"ref"`
 	Token     *string           `url:"token" json:"token"`
 	Variables map[string]string `url:"variables,omitempty" json:"variables,omitempty"`
+
+	// Inputs contains pipeline input parameters.
+	// See PipelineInputsOption for supported types and usage.
+	Inputs PipelineInputsOption `url:"inputs,omitempty" json:"inputs,omitempty"`
 }
 
 // RunPipelineTrigger starts a trigger from a project.
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/pipelines.go b/vendor/gitlab.com/gitlab-org/api/client-go/pipelines.go
index 88cf702ad..93b889150 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/pipelines.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/pipelines.go
@@ -17,9 +17,12 @@
 package gitlab
 
 import (
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"time"
+
+	"golang.org/x/exp/constraints"
 )
 
 type PipelineSource string
@@ -50,6 +53,7 @@ type (
 		GetPipeline(pid any, pipeline int, options ...RequestOptionFunc) (*Pipeline, *Response, error)
 		GetPipelineVariables(pid any, pipeline int, options ...RequestOptionFunc) ([]*PipelineVariable, *Response, error)
 		GetPipelineTestReport(pid any, pipeline int, options ...RequestOptionFunc) (*PipelineTestReport, *Response, error)
+		GetPipelineTestReportSummary(pid any, pipeline int, options ...RequestOptionFunc) (*PipelineTestReportSummary, *Response, error)
 		GetLatestPipeline(pid any, opt *GetLatestPipelineOptions, options ...RequestOptionFunc) (*Pipeline, *Response, error)
 		CreatePipeline(pid any, opt *CreatePipelineOptions, options ...RequestOptionFunc) (*Pipeline, *Response, error)
 		RetryPipelineBuild(pid any, pipeline int, options ...RequestOptionFunc) (*Pipeline, *Response, error)
@@ -169,6 +173,37 @@ type PipelineTestCases struct {
 	RecentFailures *RecentFailures `json:"recent_failures"`
 }
 
+// PipelineTestReportSummary contains a summary report of a test run
+type PipelineTestReportSummary struct {
+	Total      PipelineTotalSummary       `json:"total"`
+	TestSuites []PipelineTestSuiteSummary `json:"test_suites"`
+}
+
+// PipelineTotalSummary contains a total summary of a test run
+type PipelineTotalSummary struct {
+	// Documentation examples only show whole numbers, but the test specs for GitLab show decimals, so `float64` is the better attribute here.
+	Time       float64 `json:"time"`
+	Count      int     `json:"count"`
+	Success    int     `json:"success"`
+	Failed     int     `json:"failed"`
+	Skipped    int     `json:"skipped"`
+	Error      int     `json:"error"`
+	SuiteError *string `json:"suite_error"`
+}
+
+// PipelineTestSuiteSummary contains a test suite summary of a test run
+type PipelineTestSuiteSummary struct {
+	Name         string  `json:"name"`
+	TotalTime    float64 `json:"total_time"`
+	TotalCount   int     `json:"total_count"`
+	SuccessCount int     `json:"success_count"`
+	FailedCount  int     `json:"failed_count"`
+	SkippedCount int     `json:"skipped_count"`
+	ErrorCount   int     `json:"error_count"`
+	BuildIDs     []int   `json:"build_ids"`
+	SuiteError   *string `json:"suite_error"`
+}
+
 // RecentFailures contains failures count for the project's default branch.
 type RecentFailures struct {
 	Count      int    `json:"count"`
@@ -222,7 +257,7 @@ type ListProjectPipelinesOptions struct {
 	CreatedBefore *time.Time       `url:"created_before,omitempty" json:"created_before,omitempty"`
 }
 
-// ListProjectPipelines gets a list of project piplines.
+// ListProjectPipelines gets a list of project pipelines.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/pipelines/#list-project-pipelines
@@ -322,6 +357,31 @@ func (s *PipelinesService) GetPipelineTestReport(pid any, pipeline int, options
 	return p, resp, nil
 }
 
+// GetPipelineTestReportSummary gets the test report summary of a single project pipeline.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/pipelines/#get-a-test-report-summary-for-a-pipeline
+func (s *PipelinesService) GetPipelineTestReportSummary(pid any, pipeline int, options ...RequestOptionFunc) (*PipelineTestReportSummary, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/pipelines/%d/test_report_summary", PathEscape(project), pipeline)
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	p := new(PipelineTestReportSummary)
+	resp, err := s.client.Do(req, p)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return p, resp, nil
+}
+
 // GetLatestPipelineOptions represents the available GetLatestPipeline() options.
 //
 // GitLab API docs:
@@ -362,9 +422,13 @@ func (s *PipelinesService) GetLatestPipeline(pid any, opt *GetLatestPipelineOpti
 type CreatePipelineOptions struct {
 	Ref       *string                     `url:"ref" json:"ref"`
 	Variables *[]*PipelineVariableOptions `url:"variables,omitempty" json:"variables,omitempty"`
+
+	// Inputs contains pipeline input parameters.
+	// See PipelineInputsOption for supported types and usage.
+	Inputs PipelineInputsOption `url:"inputs,omitempty" json:"inputs,omitempty"`
 }
 
-// PipelineVariable represents a pipeline variable.
+// PipelineVariableOptions represents a pipeline variable option.
 //
 // GitLab API docs: https://docs.gitlab.com/api/pipelines/#create-a-new-pipeline
 type PipelineVariableOptions struct {
@@ -373,6 +437,67 @@ type PipelineVariableOptions struct {
 	VariableType *VariableTypeValue `url:"variable_type,omitempty" json:"variable_type,omitempty"`
 }
 
+// PipelineInputsOption represents pipeline input parameters with type-safe values.
+// Each value must be wrapped using NewPipelineInputValue() to ensure compile-time type safety.
+//
+// Supported value types:
+//   - string
+//   - integers (int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64)
+//   - floats (float32, float64)
+//   - bool
+//   - []string
+//
+// Example:
+//
+//	inputs := PipelineInputsOption{
+//	    "environment": NewPipelineInputValue("production"),
+//	    "replicas":    NewPipelineInputValue(3),
+//	    "debug":       NewPipelineInputValue(false),
+//	    "regions":     NewPipelineInputValue([]string{"us-east", "eu-west"}),
+//	}
+//
+// GitLab API docs:
+// - https://docs.gitlab.com/api/pipelines/#create-a-new-pipeline
+// - https://docs.gitlab.com/api/pipeline_triggers/#trigger-a-pipeline-with-a-token
+type PipelineInputsOption map[string]PipelineInputValueInterface
+
+// PipelineInputValueInterface is implemented by PipelineInputValue[T] for supported pipeline input types.
+// Use NewPipelineInputValue() to create instances - do not implement this interface directly.
+//
+// See PipelineInputsOption for supported types and usage examples.
+type PipelineInputValueInterface interface {
+	pipelineInputValue()
+}
+
+// PipelineInputValueType is a type constraint for valid pipeline input value types.
+// This constraint ensures only supported GitLab pipeline input types can be used.
+type PipelineInputValueType interface {
+	~string | constraints.Integer | constraints.Float | ~bool | []string
+}
+
+// PipelineInputValue wraps a pipeline input value with compile-time type safety.
+// Use NewPipelineInputValue() to create instances of this type.
+type PipelineInputValue[T PipelineInputValueType] struct {
+	Value T
+}
+
+// MarshalJSON implements the json.Marshaler interface.
+func (v PipelineInputValue[T]) MarshalJSON() ([]byte, error) {
+	return json.Marshal(v.Value)
+}
+
+// pipelineInputValue implements PipelineInputValueInterface.
+func (PipelineInputValue[T]) pipelineInputValue() {}
+
+// NewPipelineInputValue wraps a value for use in pipeline inputs.
+// Similar to Ptr(), this ensures type safety at compile time.
+// Supported types: string, integers, floats, bool, []string
+func NewPipelineInputValue[T PipelineInputValueType](value T) PipelineInputValue[T] {
+	return PipelineInputValue[T]{
+		Value: value,
+	}
+}
+
 // CreatePipeline creates a new project pipeline.
 //
 // GitLab API docs:
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/plan_limits.go b/vendor/gitlab.com/gitlab-org/api/client-go/plan_limits.go
index 4d4986057..53e9c9fd5 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/plan_limits.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/plan_limits.go
@@ -58,7 +58,7 @@ type GetCurrentPlanLimitsOptions struct {
 	PlanName *string `url:"plan_name,omitempty" json:"plan_name,omitempty"`
 }
 
-// List the current limits of a plan on the GitLab instance.
+// GetCurrentPlanLimits lists the current limits of a plan on the GitLab instance.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/plan_limits/#get-current-plan-limits
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/project_aliases.go b/vendor/gitlab.com/gitlab-org/api/client-go/project_aliases.go
new file mode 100644
index 000000000..a1af481ca
--- /dev/null
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/project_aliases.go
@@ -0,0 +1,121 @@
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+)
+
+type (
+	ProjectAliasesServiceInterface interface {
+		ListProjectAliases(options ...RequestOptionFunc) ([]*ProjectAlias, *Response, error)
+		GetProjectAlias(name string, options ...RequestOptionFunc) (*ProjectAlias, *Response, error)
+		CreateProjectAlias(opt *CreateProjectAliasOptions, options ...RequestOptionFunc) (*ProjectAlias, *Response, error)
+		DeleteProjectAlias(name string, options ...RequestOptionFunc) (*Response, error)
+	}
+
+	// ProjectAliasesService handles communication with the project aliases related methods of the GitLab API.
+	//
+	// GitLab API docs: https://docs.gitlab.com/api/project_aliases/
+	ProjectAliasesService struct {
+		client *Client
+	}
+)
+
+var _ ProjectAliasesServiceInterface = (*ProjectAliasesService)(nil)
+
+// ProjectAlias represents a GitLab project alias.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_aliases/
+type ProjectAlias struct {
+	ID        int    `json:"id"`
+	ProjectID int    `json:"project_id"`
+	Name      string `json:"name"`
+}
+
+// CreateProjectAliasOptions represents the options for creating a project alias.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_aliases/#create-a-project-alias
+type CreateProjectAliasOptions struct {
+	Name      *string `json:"name" url:"name,omitempty"`
+	ProjectID int     `json:"project_id" url:"project_id,omitempty"`
+}
+
+// ListProjectAliases gets a list of all project aliases.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_aliases/#list-all-project-aliases
+func (s *ProjectAliasesService) ListProjectAliases(options ...RequestOptionFunc) ([]*ProjectAlias, *Response, error) {
+	u := "project_aliases"
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var aliases []*ProjectAlias
+	resp, err := s.client.Do(req, &aliases)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return aliases, resp, nil
+}
+
+// GetProjectAlias gets details of a project alias.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_aliases/#get-project-alias-details
+func (s *ProjectAliasesService) GetProjectAlias(name string, options ...RequestOptionFunc) (*ProjectAlias, *Response, error) {
+	u := fmt.Sprintf("project_aliases/%s", PathEscape(name))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	aliasObj := new(ProjectAlias)
+	resp, err := s.client.Do(req, aliasObj)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return aliasObj, resp, nil
+}
+
+// CreateProjectAlias creates a new project alias.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_aliases/#create-a-project-alias
+func (s *ProjectAliasesService) CreateProjectAlias(opt *CreateProjectAliasOptions, options ...RequestOptionFunc) (*ProjectAlias, *Response, error) {
+	u := "project_aliases"
+
+	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	alias := new(ProjectAlias)
+	resp, err := s.client.Do(req, alias)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return alias, resp, nil
+}
+
+// DeleteProjectAlias deletes a project alias.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_aliases/#delete-a-project-alias
+func (s *ProjectAliasesService) DeleteProjectAlias(name string, options ...RequestOptionFunc) (*Response, error) {
+	u := fmt.Sprintf("project_aliases/%s", PathEscape(name))
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/project_feature_flags.go b/vendor/gitlab.com/gitlab-org/api/client-go/project_feature_flags.go
index 53491bccf..223ea5960 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/project_feature_flags.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/project_feature_flags.go
@@ -66,8 +66,8 @@ type ProjectFeatureFlagStrategyParameter struct {
 	UserIDs    string `json:"userIds,omitempty"`
 	Percentage string `json:"percentage,omitempty"`
 
-	// Following fields aren't documented in Gitlab API docs,
-	// but are present in Gitlab API since 13.5.
+	// Following fields aren't documented in GitLab API docs,
+	// but are present in GitLab API since 13.5.
 	// Docs: https://docs.getunleash.io/reference/activation-strategies#gradual-rollout
 	Rollout    string `json:"rollout,omitempty"`
 	Stickiness string `json:"stickiness,omitempty"`
@@ -139,7 +139,7 @@ func (s *ProjectFeatureFlagService) GetProjectFeatureFlag(pid any, name string,
 // CreateProjectFeatureFlagOptions represents the available
 // CreateProjectFeatureFlag() options.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/feature_flags/#create-a-feature-flag
 type CreateProjectFeatureFlagOptions struct {
 	Name        *string                        `url:"name,omitempty" json:"name,omitempty"`
@@ -152,7 +152,7 @@ type CreateProjectFeatureFlagOptions struct {
 // FeatureFlagStrategyOptions represents the available feature flag strategy
 // options.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/feature_flags/#create-a-feature-flag
 type FeatureFlagStrategyOptions struct {
 	ID         *int                                 `url:"id,omitempty" json:"id,omitempty"`
@@ -164,7 +164,7 @@ type FeatureFlagStrategyOptions struct {
 // ProjectFeatureFlagScopeOptions represents the available feature flag scope
 // options.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/feature_flags/#create-a-feature-flag
 type ProjectFeatureFlagScopeOptions struct {
 	ID               *int    `url:"id,omitempty" json:"id,omitempty"`
@@ -173,7 +173,7 @@ type ProjectFeatureFlagScopeOptions struct {
 
 // CreateProjectFeatureFlag creates a feature flag
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/feature_flags/#create-a-feature-flag
 func (s *ProjectFeatureFlagService) CreateProjectFeatureFlag(pid any, opt *CreateProjectFeatureFlagOptions, options ...RequestOptionFunc) (*ProjectFeatureFlag, *Response, error) {
 	project, err := parseID(pid)
@@ -201,7 +201,7 @@ func (s *ProjectFeatureFlagService) CreateProjectFeatureFlag(pid any, opt *Creat
 // UpdateProjectFeatureFlagOptions represents the available
 // UpdateProjectFeatureFlag() options.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/feature_flags/#update-a-feature-flag
 type UpdateProjectFeatureFlagOptions struct {
 	Name        *string                        `url:"name,omitempty" json:"name,omitempty"`
@@ -212,7 +212,7 @@ type UpdateProjectFeatureFlagOptions struct {
 
 // UpdateProjectFeatureFlag updates a feature flag
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/feature_flags/#update-a-feature-flag
 func (s *ProjectFeatureFlagService) UpdateProjectFeatureFlag(pid any, name string, opt *UpdateProjectFeatureFlagOptions, options ...RequestOptionFunc) (*ProjectFeatureFlag, *Response, error) {
 	group, err := parseID(pid)
@@ -240,7 +240,7 @@ func (s *ProjectFeatureFlagService) UpdateProjectFeatureFlag(pid any, name strin
 
 // DeleteProjectFeatureFlag deletes a feature flag
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/feature_flags/#delete-a-feature-flag
 func (s *ProjectFeatureFlagService) DeleteProjectFeatureFlag(pid any, name string, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/project_import_export.go b/vendor/gitlab.com/gitlab-org/api/client-go/project_import_export.go
index eab7e3edf..105ec930c 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/project_import_export.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/project_import_export.go
@@ -184,7 +184,7 @@ type ImportFileOptions struct {
 	OverrideParams *CreateProjectOptions `url:"override_params,omitempty" json:"override_params,omitempty"`
 }
 
-// Import a project from an archive file.
+// ImportFromFile imports a project from an archive file.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/project_import_export/#import-a-file
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/project_markdown_uploads.go b/vendor/gitlab.com/gitlab-org/api/client-go/project_markdown_uploads.go
index b7d0ea307..88a39320b 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/project_markdown_uploads.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/project_markdown_uploads.go
@@ -32,7 +32,7 @@ type (
 		DeleteProjectMarkdownUploadBySecretAndFilename(pid any, secret string, filename string, options ...RequestOptionFunc) (*Response, error)
 	}
 
-	// MarkdownUploadsService handles communication with the project
+	// ProjectMarkdownUploadsService handles communication with the project
 	// markdown uploads related methods of the GitLab API.
 	//
 	// GitLab API docs:
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/project_members.go b/vendor/gitlab.com/gitlab-org/api/client-go/project_members.go
index fa4bbcef7..e12a2fa6f 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/project_members.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/project_members.go
@@ -55,6 +55,7 @@ type ProjectMember struct {
 	Name        string           `json:"name"`
 	State       string           `json:"state"`
 	CreatedAt   *time.Time       `json:"created_at"`
+	CreatedBy   *MemberCreatedBy `json:"created_by"`
 	ExpiresAt   *ISOTime         `json:"expires_at"`
 	AccessLevel AccessLevelValue `json:"access_level"`
 	WebURL      string           `json:"web_url"`
@@ -62,6 +63,15 @@ type ProjectMember struct {
 	MemberRole  *MemberRole      `json:"member_role"`
 }
 
+type MemberCreatedBy struct {
+	ID        int    `json:"id"`
+	Username  string `json:"username"`
+	Name      string `json:"name"`
+	State     string `json:"state"`
+	AvatarURL string `json:"avatar_url"`
+	WebURL    string `json:"web_url"`
+}
+
 // ListProjectMembersOptions represents the available ListProjectMembers() and
 // ListAllProjectMembers() options.
 //
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/project_security_settings.go b/vendor/gitlab.com/gitlab-org/api/client-go/project_security_settings.go
index 028d62724..52eb64d1d 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/project_security_settings.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/project_security_settings.go
@@ -30,7 +30,7 @@ type (
 	// ProjectSecuritySettingsService handles communication with the Project Security Settings
 	// related methods of the GitLab API.
 	//
-	// Gitlab API docs:
+	// GitLab API docs:
 	// https://docs.gitlab.com/api/project_security_settings/
 	ProjectSecuritySettingsService struct {
 		client *Client
@@ -41,7 +41,7 @@ var _ ProjectSecuritySettingsServiceInterface = (*ProjectSecuritySettingsService
 
 // ProjectSecuritySettings represents the project security settings data.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/project_security_settings/
 type ProjectSecuritySettings struct {
 	ProjectID                           int64      `json:"project_id"`
@@ -56,7 +56,7 @@ type ProjectSecuritySettings struct {
 	SecretPushProtectionEnabled         bool       `json:"secret_push_protection_enabled"`
 }
 
-// Gets a string representation of the ProjectSecuritySettings data.
+// String gets a string representation of the ProjectSecuritySettings data.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/project_security_settings/
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/project_statistics.go b/vendor/gitlab.com/gitlab-org/api/client-go/project_statistics.go
new file mode 100644
index 000000000..1ff9edf98
--- /dev/null
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/project_statistics.go
@@ -0,0 +1,66 @@
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+)
+
+type (
+	ProjectStatisticsServiceInterface interface {
+		// Last30DaysStatistics gets the project statistics for the last 30 days.
+		//
+		// GitLab API docs: https://docs.gitlab.com/api/project_statistics/#get-the-statistics-of-the-last-30-days
+		Last30DaysStatistics(pid any, options ...RequestOptionFunc) (*ProjectStatistics, *Response, error)
+	}
+
+	// ProjectStatisticsService handles communication with the project statistics related methods
+	// of the GitLab API.
+	//
+	// GitLab API docs: https://docs.gitlab.com/api/project_statistics
+	ProjectStatisticsService struct {
+		client *Client
+	}
+)
+
+// ProjectStatistics represents the Project Statistics.
+//
+// GitLab API docs: https://docs.gitlab.com/api/project_statistics
+type ProjectStatistics struct {
+	Fetches FetchStats `json:"fetches"`
+}
+
+type FetchStats struct {
+	Total int64      `json:"total"`
+	Days  []DayStats `json:"days"`
+}
+
+type DayStats struct {
+	Count int64  `json:"count"`
+	Date  string `json:"date"`
+}
+
+var _ ProjectStatisticsServiceInterface = (*ProjectStatisticsService)(nil)
+
+func (s *ProjectStatisticsService) Last30DaysStatistics(pid any, options ...RequestOptionFunc) (*ProjectStatistics, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	u := fmt.Sprintf("projects/%s/statistics",
+		PathEscape(project),
+	)
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	projectStats := new(ProjectStatistics)
+	resp, err := s.client.Do(req, projectStats)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return projectStats, resp, err
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/project_vulnerabilities.go b/vendor/gitlab.com/gitlab-org/api/client-go/project_vulnerabilities.go
index 9b939e0bf..aa85f43ba 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/project_vulnerabilities.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/project_vulnerabilities.go
@@ -44,7 +44,7 @@ type (
 // Deprecated: use GraphQL Query.vulnerabilities instead
 var _ ProjectVulnerabilitiesServiceInterface = (*ProjectVulnerabilitiesService)(nil)
 
-// Project represents a GitLab project vulnerability.
+// ProjectVulnerability represents a GitLab project vulnerability.
 // Deprecated: use GraphQL Query.vulnerabilities instead
 //
 // GitLab API docs: https://docs.gitlab.com/api/project_vulnerabilities/
@@ -74,7 +74,7 @@ type ProjectVulnerability struct {
 	UpdatedByID             int        `json:"updated_by_id"`
 }
 
-// Project represents a GitLab project vulnerability finding.
+// Finding represents a GitLab project vulnerability finding.
 // Deprecated: use GraphQL Query.vulnerabilities instead
 //
 // GitLab API docs: https://docs.gitlab.com/api/project_vulnerabilities/
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/projects.go b/vendor/gitlab.com/gitlab-org/api/client-go/projects.go
index 4becdbb3c..7d1045b27 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/projects.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/projects.go
@@ -50,6 +50,7 @@ type (
 		UnstarProject(pid any, options ...RequestOptionFunc) (*Project, *Response, error)
 		ArchiveProject(pid any, options ...RequestOptionFunc) (*Project, *Response, error)
 		UnarchiveProject(pid any, options ...RequestOptionFunc) (*Project, *Response, error)
+		RestoreProject(pid any, options ...RequestOptionFunc) (*Project, *Response, error)
 		DeleteProject(pid any, opt *DeleteProjectOptions, options ...RequestOptionFunc) (*Response, error)
 		ShareProjectWithGroup(pid any, opt *ShareWithGroupOptions, options ...RequestOptionFunc) (*Response, error)
 		DeleteSharedProjectFromGroup(pid any, groupID int, options ...RequestOptionFunc) (*Response, error)
@@ -61,6 +62,8 @@ type (
 		TriggerTestProjectHook(pid any, hook int, event ProjectHookEvent, options ...RequestOptionFunc) (*Response, error)
 		SetProjectCustomHeader(pid any, hook int, key string, opt *SetHookCustomHeaderOptions, options ...RequestOptionFunc) (*Response, error)
 		DeleteProjectCustomHeader(pid any, hook int, key string, options ...RequestOptionFunc) (*Response, error)
+		SetProjectWebhookURLVariable(pid any, hook int, key string, opt *SetProjectWebhookURLVariableOptions, options ...RequestOptionFunc) (*Response, error)
+		DeleteProjectWebhookURLVariable(pid any, hook int, key string, options ...RequestOptionFunc) (*Response, error)
 		CreateProjectForkRelation(pid any, fork int, options ...RequestOptionFunc) (*ProjectForkRelation, *Response, error)
 		DeleteProjectForkRelation(pid any, options ...RequestOptionFunc) (*Response, error)
 		UploadAvatar(pid any, avatar io.Reader, filename string, options ...RequestOptionFunc) (*Project, *Response, error)
@@ -83,6 +86,7 @@ type (
 		TransferProject(pid any, opt *TransferProjectOptions, options ...RequestOptionFunc) (*Project, *Response, error)
 		StartHousekeepingProject(pid any, options ...RequestOptionFunc) (*Response, error)
 		GetRepositoryStorage(pid any, options ...RequestOptionFunc) (*ProjectReposityStorage, *Response, error)
+		ListProjectStarrers(pid any, opts *ListProjectStarrersOptions, options ...RequestOptionFunc) ([]*ProjectStarrer, *Response, error)
 	}
 
 	// ProjectsService handles communication with the repositories related methods
@@ -133,6 +137,7 @@ type Project struct {
 	License                                   *ProjectLicense            `json:"license"`
 	SharedRunnersEnabled                      bool                       `json:"shared_runners_enabled"`
 	GroupRunnersEnabled                       bool                       `json:"group_runners_enabled"`
+	ResourceGroupDefaultProcessMode           ResourceGroupProcessMode   `json:"resource_group_default_process_mode"`
 	RunnerTokenExpirationInterval             int                        `json:"runner_token_expiration_interval"`
 	ForksCount                                int                        `json:"forks_count"`
 	StarCount                                 int                        `json:"star_count"`
@@ -627,7 +632,7 @@ func (s *ProjectsService) ListProjectsGroups(pid any, opt *ListProjectGroupOptio
 
 // ProjectLanguages is a map of strings because the response is arbitrary
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/projects/#list-programming-languages-used
 type ProjectLanguages map[string]float32
 
@@ -755,6 +760,7 @@ type CreateProjectOptions struct {
 	SecurityAndComplianceAccessLevel          *AccessControlValue                  `url:"security_and_compliance_access_level,omitempty" json:"security_and_compliance_access_level,omitempty"`
 	SharedRunnersEnabled                      *bool                                `url:"shared_runners_enabled,omitempty" json:"shared_runners_enabled,omitempty"`
 	GroupRunnersEnabled                       *bool                                `url:"group_runners_enabled,omitempty" json:"group_runners_enabled,omitempty"`
+	ResourceGroupDefaultProcessMode           *ResourceGroupProcessMode            `url:"resource_group_default_process_mode,omitempty" json:"resource_group_default_process_mode,omitempty"`
 	ShowDefaultAwardEmojis                    *bool                                `url:"show_default_award_emojis,omitempty" json:"show_default_award_emojis,omitempty"`
 	SnippetsAccessLevel                       *AccessControlValue                  `url:"snippets_access_level,omitempty" json:"snippets_access_level,omitempty"`
 	SquashCommitTemplate                      *string                              `url:"squash_commit_template,omitempty" json:"squash_commit_template,omitempty"`
@@ -1001,6 +1007,7 @@ type EditProjectOptions struct {
 	ServiceDeskEnabled                        *bool                                        `url:"service_desk_enabled,omitempty" json:"service_desk_enabled,omitempty"`
 	SharedRunnersEnabled                      *bool                                        `url:"shared_runners_enabled,omitempty" json:"shared_runners_enabled,omitempty"`
 	GroupRunnersEnabled                       *bool                                        `url:"group_runners_enabled,omitempty" json:"group_runners_enabled,omitempty"`
+	ResourceGroupDefaultProcessMode           *ResourceGroupProcessMode                    `url:"resource_group_default_process_mode,omitempty" json:"resource_group_default_process_mode,omitempty"`
 	ShowDefaultAwardEmojis                    *bool                                        `url:"show_default_award_emojis,omitempty" json:"show_default_award_emojis,omitempty"`
 	SnippetsAccessLevel                       *AccessControlValue                          `url:"snippets_access_level,omitempty" json:"snippets_access_level,omitempty"`
 	SquashCommitTemplate                      *string                                      `url:"squash_commit_template,omitempty" json:"squash_commit_template,omitempty"`
@@ -1147,12 +1154,12 @@ func (s *ProjectsService) StarProject(pid any, options ...RequestOptionFunc) (*P
 	return p, resp, nil
 }
 
-// ListProjectInvidedGroupOptions represents the available
+// ListProjectInvitedGroupOptions represents the available
 // ListProjectsInvitedGroups() options.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/projects/#list-a-projects-invited-groups
-type ListProjectInvidedGroupOptions struct {
+type ListProjectInvitedGroupOptions struct {
 	ListOptions
 	Search               *string           `url:"search,omitempty" json:"search,omitempty"`
 	MinAccessLevel       *AccessLevelValue `url:"min_access_level,omitempty" json:"min_access_level,omitempty"`
@@ -1160,6 +1167,11 @@ type ListProjectInvidedGroupOptions struct {
 	WithCustomAttributes *bool             `url:"with_custom_attributes,omitempty" json:"with_custom_attributes,omitempty"`
 }
 
+// ListProjectInvidedGroupOptions is kept for backwards compatibility.
+//
+// Deprecated: use ListProjectInvitedGroupOptions instead. The ListProjectInvidedGroupOptions type will be removed in the next release.
+type ListProjectInvidedGroupOptions = ListProjectInvitedGroupOptions
+
 // ListProjectsInvitedGroups lists invited groups of a project
 //
 // GitLab API docs:
@@ -1262,6 +1274,31 @@ func (s *ProjectsService) UnarchiveProject(pid any, options ...RequestOptionFunc
 	return p, resp, nil
 }
 
+// RestoreProject restores a project that is marked for deletion.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/projects/#restore-a-project-marked-for-deletion
+func (s *ProjectsService) RestoreProject(pid any, options ...RequestOptionFunc) (*Project, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/restore", PathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodPost, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	p := new(Project)
+	resp, err := s.client.Do(req, p)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return p, resp, nil
+}
+
 // DeleteProjectOptions represents the available DeleteProject() options.
 //
 // GitLab API docs:
@@ -1346,6 +1383,12 @@ type HookCustomHeader struct {
 	Value string `json:"value"`
 }
 
+// HookURLVariable represents a project or group hook URL variable
+type HookURLVariable struct {
+	Key   string `json:"key"`
+	Value string `json:"value"`
+}
+
 // ProjectHook represents a project hook.
 //
 // GitLab API docs:
@@ -1355,7 +1398,6 @@ type ProjectHook struct {
 	URL                       string              `json:"url"`
 	Name                      string              `json:"name"`
 	Description               string              `json:"description"`
-	ConfidentialNoteEvents    bool                `json:"confidential_note_events"`
 	ProjectID                 int                 `json:"project_id"`
 	PushEvents                bool                `json:"push_events"`
 	PushEventsBranchFilter    string              `json:"push_events_branch_filter"`
@@ -1364,13 +1406,19 @@ type ProjectHook struct {
 	MergeRequestsEvents       bool                `json:"merge_requests_events"`
 	TagPushEvents             bool                `json:"tag_push_events"`
 	NoteEvents                bool                `json:"note_events"`
+	ConfidentialNoteEvents    bool                `json:"confidential_note_events"`
 	JobEvents                 bool                `json:"job_events"`
 	PipelineEvents            bool                `json:"pipeline_events"`
 	WikiPageEvents            bool                `json:"wiki_page_events"`
 	DeploymentEvents          bool                `json:"deployment_events"`
 	ReleasesEvents            bool                `json:"releases_events"`
+	MilestoneEvents           bool                `json:"milestone_events"`
+	FeatureFlagEvents         bool                `json:"feature_flag_events"`
 	EnableSSLVerification     bool                `json:"enable_ssl_verification"`
+	RepositoryUpdateEvents    bool                `json:"repository_update_events"`
 	AlertStatus               string              `json:"alert_status"`
+	DisabledUntil             *time.Time          `json:"disabled_until"`
+	URLVariables              []HookURLVariable   `json:"url_variables"`
 	CreatedAt                 *time.Time          `json:"created_at"`
 	ResourceAccessTokenEvents bool                `json:"resource_access_token_events"`
 	CustomWebhookTemplate     string              `json:"custom_webhook_template"`
@@ -1633,6 +1681,53 @@ func (s *ProjectsService) DeleteProjectCustomHeader(pid any, hook int, key strin
 	return s.client.Do(req, nil)
 }
 
+// SetProjectWebhookURLVariableOptions represents the available
+// SetProjectWebhookURLVariable() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_webhooks/#set-a-url-variable
+type SetProjectWebhookURLVariableOptions struct {
+	Value *string `json:"value,omitempty"`
+}
+
+// SetProjectWebhookURLVariable creates or updates a project webhook URL variable.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_webhooks/#set-a-url-variable
+func (s *ProjectsService) SetProjectWebhookURLVariable(pid any, hook int, key string, opt *SetProjectWebhookURLVariableOptions, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/hooks/%d/url_variables/%s", PathEscape(project), hook, PathEscape(key))
+
+	req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
+// DeleteProjectWebhookURLVariable deletes a project webhook URL variable.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_webhooks/#delete-a-url-variable
+func (s *ProjectsService) DeleteProjectWebhookURLVariable(pid any, hook int, key string, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/hooks/%d/url_variables/%s", PathEscape(project), hook, PathEscape(key))
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
 // ProjectForkRelation represents a project fork relationship.
 //
 // GitLab API docs:
@@ -2327,17 +2422,22 @@ func (s *ProjectsService) StartHousekeepingProject(pid any, options ...RequestOp
 	return s.client.Do(req, nil)
 }
 
-// GetRepositoryStorage Get the path to repository storage.
+// ProjectRepositoryStorage represents the repository storage information for a project.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/projects/#get-the-path-to-repository-storage
-type ProjectReposityStorage struct {
+type ProjectRepositoryStorage struct {
 	ProjectID         int        `json:"project_id"`
 	DiskPath          string     `json:"disk_path"`
 	CreatedAt         *time.Time `json:"created_at"`
 	RepositoryStorage string     `json:"repository_storage"`
 }
 
+// ProjectReposityStorage is kept for backwards compatibility.
+//
+// Deprecated: use ProjectRepositoryStorage instead. The ProjectReposityStorage type will be removed in the next release.
+type ProjectReposityStorage = ProjectRepositoryStorage
+
 // GetRepositoryStorage Get the path to repository storage.
 //
 // GitLab API docs:
@@ -2362,3 +2462,47 @@ func (s *ProjectsService) GetRepositoryStorage(pid any, options ...RequestOption
 
 	return prs, resp, nil
 }
+
+// ProjectStarrer represents a user who starred a project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_starring/#list-users-who-starred-a-project
+type ProjectStarrer struct {
+	StarredSince time.Time   `json:"starred_since"`
+	User         ProjectUser `json:"user"`
+}
+
+// ListProjectStarrersOptions represents the available ListProjectStarrers() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_starring/#list-users-who-starred-a-project
+type ListProjectStarrersOptions struct {
+	ListOptions
+	Search *string `url:"search,omitempty" json:"search,omitempty"`
+}
+
+// ListProjectStarrers gets users who starred a project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_starring/#list-users-who-starred-a-project
+func (s *ProjectsService) ListProjectStarrers(pid any, opts *ListProjectStarrersOptions, options ...RequestOptionFunc) ([]*ProjectStarrer, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	u := fmt.Sprintf("projects/%s/starrers", PathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opts, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var starrers []*ProjectStarrer
+	resp, err := s.client.Do(req, &starrers)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return starrers, resp, nil
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/protected_branches.go b/vendor/gitlab.com/gitlab-org/api/client-go/protected_branches.go
index d3c1aaa76..0a6fdc83a 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/protected_branches.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/protected_branches.go
@@ -223,7 +223,7 @@ type UpdateProtectedBranchOptions struct {
 
 // UpdateProtectedBranch updates a protected branch.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/protected_branches/#update-a-protected-branch
 func (s *ProtectedBranchesService) UpdateProtectedBranch(pid any, branch string, opt *UpdateProtectedBranchOptions, options ...RequestOptionFunc) (*ProtectedBranch, *Response, error) {
 	project, err := parseID(pid)
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/protected_environments.go b/vendor/gitlab.com/gitlab-org/api/client-go/protected_environments.go
index 0135b1ee1..17a440d1a 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/protected_environments.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/protected_environments.go
@@ -53,7 +53,7 @@ type ProtectedEnvironment struct {
 	ApprovalRules         []*EnvironmentApprovalRule      `json:"approval_rules"`
 }
 
-// EnvironmentAccessDescription represents the access decription for a protected
+// EnvironmentAccessDescription represents the access description for a protected
 // environment.
 //
 // GitLab API docs:
@@ -153,7 +153,7 @@ type ProtectRepositoryEnvironmentsOptions struct {
 	ApprovalRules         *[]*EnvironmentApprovalRuleOptions `url:"approval_rules,omitempty" json:"approval_rules,omitempty"`
 }
 
-// EnvironmentAccessOptions represents the options for an access decription for
+// EnvironmentAccessOptions represents the options for an access description for
 // a protected environment.
 //
 // GitLab API docs:
@@ -218,7 +218,7 @@ type UpdateProtectedEnvironmentsOptions struct {
 }
 
 // UpdateEnvironmentAccessOptions represents the options for updates to an
-// access decription for a protected environment.
+// access description for a protected environment.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/protected_environments/#update-a-protected-environment
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/protected_packages.go b/vendor/gitlab.com/gitlab-org/api/client-go/protected_packages.go
new file mode 100644
index 000000000..890765e68
--- /dev/null
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/protected_packages.go
@@ -0,0 +1,165 @@
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+)
+
+type (
+	ProtectedPackagesServiceInterface interface {
+		// ListPackageProtectionRules gets a list of project package protection rules.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/project_packages_protection_rules/#list-package-protection-rules
+		ListPackageProtectionRules(pid any, opt *ListPackageProtectionRulesOptions, options ...RequestOptionFunc) ([]*PackageProtectionRule, *Response, error)
+		// CreatePackageProtectionRules creates a new package protection rules.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/project_packages_protection_rules/#create-a-package-protection-rule
+		CreatePackageProtectionRules(pid any, opt *CreatePackageProtectionRulesOptions, options ...RequestOptionFunc) (*PackageProtectionRule, *Response, error)
+		// UpdatePackageProtectionRules updates an existing package protection rule.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/project_packages_protection_rules/#update-a-package-protection-rule
+		UpdatePackageProtectionRules(pid any, packageProtectionRule int64, opt *UpdatePackageProtectionRulesOptions, options ...RequestOptionFunc) (*PackageProtectionRule, *Response, error)
+		// DeletePackageProtectionRules deletes an existing package protection rules.
+		//
+		// GitLab API docs:
+		// https://docs.gitlab.com/api/project_packages_protection_rules/#delete-a-package-protection-rule
+		DeletePackageProtectionRules(pid any, packageProtectionRule int64, options ...RequestOptionFunc) (*Response, error)
+	}
+
+	// ProtectedPackagesService handles communication with the protected packages related methods
+	// of the GitLab API.
+	//
+	// GitLab API docs:
+	// https://docs.gitlab.com/api/project_packages_protection_rules/
+	ProtectedPackagesService struct {
+		client *Client
+	}
+)
+
+var _ ProtectedPackagesServiceInterface = (*ProtectedPackagesService)(nil)
+
+// PackageProtectionRule represents a GitLab package protection rule.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_packages_protection_rules
+type PackageProtectionRule struct {
+	ID                          int64  `json:"id"`
+	ProjectID                   int64  `json:"project_id"`
+	PackageNamePattern          string `json:"package_name_pattern"`
+	PackageType                 string `json:"package_type"`
+	MinimumAccessLevelForDelete string `json:"minimum_access_level_for_delete"`
+	MinimumAccessLevelForPush   string `json:"minimum_access_level_for_push"`
+}
+
+// ListPackageProtectionRulesOptions represents the available ListPackageProtectionRules() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_packages_protection_rules/#list-package-protection-rules
+type ListPackageProtectionRulesOptions struct {
+	ListOptions
+}
+
+// CreatePackageProtectionRulesOptions represents the available CreatePackageProtectionRules() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_packages_protection_rules/#create-a-package-protection-rule
+type CreatePackageProtectionRulesOptions struct {
+	PackageNamePattern          *string `url:"package_name_pattern" json:"package_name_pattern"`
+	PackageType                 *string `url:"package_type" json:"package_type"`
+	MinimumAccessLevelForDelete *string `url:"minimum_access_level_for_delete" json:"minimum_access_level_for_delete"`
+	MinimumAccessLevelForPush   *string `url:"minimum_access_level_for_push" json:"minimum_access_level_for_push"`
+}
+
+// UpdatePackageProtectionRulesOptions represents the available
+// UpdatePackageProtectionRules() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/api/project_packages_protection_rules/#update-a-package-protection-rule
+type UpdatePackageProtectionRulesOptions struct {
+	PackageNamePattern          *string `url:"package_name_pattern" json:"package_name_pattern"`
+	PackageType                 *string `url:"package_type" json:"package_type"`
+	MinimumAccessLevelForDelete *string `url:"minimum_access_level_for_delete" json:"minimum_access_level_for_delete"`
+	MinimumAccessLevelForPush   *string `url:"minimum_access_level_for_push" json:"minimum_access_level_for_push"`
+}
+
+func (s *ProtectedPackagesService) ListPackageProtectionRules(pid any, opts *ListPackageProtectionRulesOptions, options ...RequestOptionFunc) ([]*PackageProtectionRule, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	u := fmt.Sprintf("projects/%s/packages/protection/rules", PathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opts, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var b []*PackageProtectionRule
+	resp, err := s.client.Do(req, &b)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return b, resp, nil
+}
+
+func (s *ProtectedPackagesService) CreatePackageProtectionRules(pid any, opt *CreatePackageProtectionRulesOptions, options ...RequestOptionFunc) (*PackageProtectionRule, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/packages/protection/rules", PathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	p := new(PackageProtectionRule)
+	resp, err := s.client.Do(req, p)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return p, resp, nil
+}
+
+func (s *ProtectedPackagesService) DeletePackageProtectionRules(pid any, packageProtectionRule int64, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/packages/protection/rules/%d", PathEscape(project), packageProtectionRule)
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
+func (s *ProtectedPackagesService) UpdatePackageProtectionRules(pid any, packageProtectionRule int64, opt *UpdatePackageProtectionRulesOptions, options ...RequestOptionFunc) (*PackageProtectionRule, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/packages/protection/rules/%d", PathEscape(project), packageProtectionRule)
+
+	req, err := s.client.NewRequest(http.MethodPatch, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	p := new(PackageProtectionRule)
+	resp, err := s.client.Do(req, p)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return p, resp, nil
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/protected_tags.go b/vendor/gitlab.com/gitlab-org/api/client-go/protected_tags.go
index e9adfe248..2d776eed9 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/protected_tags.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/protected_tags.go
@@ -50,7 +50,7 @@ type ProtectedTag struct {
 	CreateAccessLevels []*TagAccessDescription `json:"create_access_levels"`
 }
 
-// TagAccessDescription reperesents the access decription for a protected tag.
+// TagAccessDescription represents the access description for a protected tag.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/protected_tags/
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/release.config.mjs b/vendor/gitlab.com/gitlab-org/api/client-go/release.config.mjs
deleted file mode 100644
index ef6774efd..000000000
--- a/vendor/gitlab.com/gitlab-org/api/client-go/release.config.mjs
+++ /dev/null
@@ -1,13 +0,0 @@
-export default {
-  branches: ["main"],
-  plugins: [
-    "@semantic-release/commit-analyzer",
-    "@semantic-release/release-notes-generator",
-    "@semantic-release/changelog",
-    "@semantic-release/gitlab",
-    ["@semantic-release/git", {
-      assets: ["CHANGELOG.md"],
-      message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
-    }]
-  ]
-};
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/releases.go b/vendor/gitlab.com/gitlab-org/api/client-go/releases.go
index 1d335e314..8810690db 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/releases.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/releases.go
@@ -138,7 +138,7 @@ type ListReleasesOptions struct {
 	IncludeHTMLDescription *bool   `url:"include_html_description,omitempty" json:"include_html_description,omitempty"`
 }
 
-// ListReleases gets a pagenated of releases accessible by the authenticated user.
+// ListReleases gets a paginated list of releases accessible by the authenticated user.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/releases/#list-releases
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/repositories.go b/vendor/gitlab.com/gitlab-org/api/client-go/repositories.go
index 50da7ceb1..e364db7d0 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/repositories.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/repositories.go
@@ -269,7 +269,7 @@ func (s *RepositoriesService) Compare(pid any, opt *CompareOptions, options ...R
 	return c, resp, nil
 }
 
-// Contributor represents a GitLap contributor.
+// Contributor represents a GitLab contributor.
 //
 // GitLab API docs: https://docs.gitlab.com/api/repositories/#contributors
 type Contributor struct {
@@ -369,7 +369,7 @@ type AddChangelogOptions struct {
 
 // AddChangelog generates changelog data based on commits in a repository.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/repositories/#add-changelog-data-to-a-changelog-file
 func (s *RepositoriesService) AddChangelog(pid any, opt *AddChangelogOptions, options ...RequestOptionFunc) (*Response, error) {
 	project, err := parseID(pid)
@@ -415,7 +415,7 @@ type GenerateChangelogDataOptions struct {
 // GenerateChangelogData generates changelog data based on commits in a
 // repository, without committing them to a changelog file.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/repositories/#generate-changelog-data
 func (s *RepositoriesService) GenerateChangelogData(pid any, opt GenerateChangelogDataOptions, options ...RequestOptionFunc) (*ChangelogData, *Response, error) {
 	project, err := parseID(pid)
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/request_handler.go b/vendor/gitlab.com/gitlab-org/api/client-go/request_handler.go
new file mode 100644
index 000000000..6ead6434c
--- /dev/null
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/request_handler.go
@@ -0,0 +1,130 @@
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+)
+
+type doConfig struct {
+	method      string
+	path        string
+	apiOpts     any
+	requestOpts []RequestOptionFunc
+}
+
+type doOption func(c *doConfig)
+
+func withMethod(method string) doOption {
+	return func(c *doConfig) {
+		c.method = method
+	}
+}
+
+func withPath(path string, args ...any) doOption {
+	return func(c *doConfig) {
+		as := make([]any, len(args))
+		for i, a := range args {
+			switch v := a.(type) {
+			case string:
+				as[i] = PathEscape(v)
+			default:
+				as[i] = v
+			}
+		}
+		c.path = fmt.Sprintf(path, as...)
+	}
+}
+
+func withAPIOpts(o any) doOption {
+	return func(c *doConfig) {
+		c.apiOpts = o
+	}
+}
+
+func withRequestOpts(o ...RequestOptionFunc) doOption {
+	return func(c *doConfig) {
+		c.requestOpts = o
+	}
+}
+
+// none is a sentinel type to signal that a request performed with do does not return a value.
+type none struct{}
+
+// do constructs an API requests, performs it and processes the response.
+//
+// Use the opts to configure the request.
+// If the response body shouldn't be handled, use the none sentinel type
+// and ignore the first return argument.
+//
+// Example:
+//
+// // Get Request to return single *Agent:
+// return do[*Agent](s.client,
+//
+//	withPath("projects/%s/cluster_agents/%d", project, id),
+//	withRequestOpts(options...),
+//
+// )
+//
+// // Get Request to return multiple []*Agents
+// return do[[]*Agent](s.client,
+//
+//	withPath("projects/%s/cluster_agents", project),
+//	withRequestOpts(options...),
+//
+// )
+//
+// // Post Request to create Agent and return *Agents
+// return do[*Agent](s.client,
+//
+//	withMethod(http.MethodPost),
+//	withPath("projects/%s/cluster_agents", project),
+//	withAPIOpts(opt),
+//	withRequestOpts(options...),
+//
+// )
+//
+// // Delete Request that returns nothing:
+// _, resp, err := do[none](s.client,
+//
+//	withMethod(http.MethodDelete),
+//	withPath("projects/%s/cluster_agents/%d", project, id),
+//	withRequestOpts(options...),
+//
+// )
+func do[T any](client *Client, opts ...doOption) (T, *Response, error) {
+	// default config
+	config := &doConfig{
+		method:  http.MethodGet,
+		apiOpts: nil,
+	}
+
+	// apply options to config
+	for _, f := range opts {
+		f(config)
+	}
+
+	req, err := client.NewRequest(config.method, config.path, config.apiOpts, config.requestOpts)
+	if err != nil {
+		var z T
+		return z, nil, err
+	}
+
+	var (
+		as   T
+		resp *Response
+	)
+	if reflect.TypeOf(as) == reflect.TypeOf(none{}) {
+		resp, err = client.Do(req, nil)
+	} else {
+		resp, err = client.Do(req, &as)
+	}
+
+	if err != nil {
+		var z T
+		return z, resp, err
+	}
+
+	return as, resp, nil
+}
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/request_options.go b/vendor/gitlab.com/gitlab-org/api/client-go/request_options.go
index 16f699c08..166c1d92c 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/request_options.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/request_options.go
@@ -39,10 +39,10 @@ func WithContext(ctx context.Context) RequestOptionFunc {
 
 // copyContextValues copy some context key and values in old context
 func copyContextValues(oldCtx context.Context, newCtx context.Context) context.Context {
-	cheryRetry := checkRetryFromContext(oldCtx)
+	checkRetry := checkRetryFromContext(oldCtx)
 
-	if cheryRetry != nil {
-		newCtx = contextWithCheckRetry(newCtx, cheryRetry)
+	if checkRetry != nil {
+		newCtx = contextWithCheckRetry(newCtx, checkRetry)
 	}
 
 	return newCtx
@@ -72,12 +72,12 @@ func WithHeaders(headers map[string]string) RequestOptionFunc {
 // query parameter in the request with its corresponding response parameter.
 func WithKeysetPaginationParameters(nextLink string) RequestOptionFunc {
 	return func(req *retryablehttp.Request) error {
-		nextUrl, err := url.Parse(nextLink)
+		nextURL, err := url.Parse(nextLink)
 		if err != nil {
 			return err
 		}
 		q := req.URL.Query()
-		for k, values := range nextUrl.Query() {
+		for k, values := range nextURL.Query() {
 			q.Del(k)
 			for _, v := range values {
 				q.Add(k, v)
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/resource_group.go b/vendor/gitlab.com/gitlab-org/api/client-go/resource_group.go
index 7471c2959..6b851143f 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/resource_group.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/resource_group.go
@@ -42,7 +42,7 @@ type (
 
 var _ ResourceGroupServiceInterface = (*ResourceGroupService)(nil)
 
-// ResourceGrouop represents a GitLab Project Resource Group.
+// ResourceGroup represents a GitLab Project Resource Group.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/resource_groups/
@@ -54,7 +54,7 @@ type ResourceGroup struct {
 	UpdatedAt   *time.Time `json:"updated_at"`
 }
 
-// Gets a string representation of a ResourceGroup
+// String gets a string representation of a ResourceGroup
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/resource_groups/
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/secure_files.go b/vendor/gitlab.com/gitlab-org/api/client-go/secure_files.go
index 761beff90..13fc386cc 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/secure_files.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/secure_files.go
@@ -90,7 +90,7 @@ type SecureFileSubject struct {
 	UID string `json:"UID"`
 }
 
-// Gets a string representation of a SecureFile.
+// String gets a string representation of a SecureFile.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/secure_files/
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/services.go b/vendor/gitlab.com/gitlab-org/api/client-go/services.go
index 7ce79c4ea..c41b6e049 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/services.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/services.go
@@ -21,7 +21,6 @@ import (
 	"fmt"
 	"net/http"
 	"strconv"
-	"time"
 )
 
 type (
@@ -103,33 +102,7 @@ var _ ServicesServiceInterface = (*ServicesService)(nil)
 // Service represents a GitLab service.
 //
 // GitLab API docs: https://docs.gitlab.com/api/project_integrations/
-type Service struct {
-	ID                             int        `json:"id"`
-	Title                          string     `json:"title"`
-	Slug                           string     `json:"slug"`
-	CreatedAt                      *time.Time `json:"created_at"`
-	UpdatedAt                      *time.Time `json:"updated_at"`
-	Active                         bool       `json:"active"`
-	AlertEvents                    bool       `json:"alert_events"`
-	CommitEvents                   bool       `json:"commit_events"`
-	ConfidentialIssuesEvents       bool       `json:"confidential_issues_events"`
-	ConfidentialNoteEvents         bool       `json:"confidential_note_events"`
-	DeploymentEvents               bool       `json:"deployment_events"`
-	GroupConfidentialMentionEvents bool       `json:"group_confidential_mention_events"`
-	GroupMentionEvents             bool       `json:"group_mention_events"`
-	IncidentEvents                 bool       `json:"incident_events"`
-	IssuesEvents                   bool       `json:"issues_events"`
-	JobEvents                      bool       `json:"job_events"`
-	MergeRequestsEvents            bool       `json:"merge_requests_events"`
-	NoteEvents                     bool       `json:"note_events"`
-	PipelineEvents                 bool       `json:"pipeline_events"`
-	PushEvents                     bool       `json:"push_events"`
-	TagPushEvents                  bool       `json:"tag_push_events"`
-	VulnerabilityEvents            bool       `json:"vulnerability_events"`
-	WikiPageEvents                 bool       `json:"wiki_page_events"`
-	CommentOnEventEnabled          bool       `json:"comment_on_event_enabled"`
-	Inherited                      bool       `json:"inherited"`
-}
+type Service = Integration
 
 // ListServices gets a list of all active services.
 //
@@ -268,12 +241,14 @@ type DataDogService struct {
 // GitLab API docs:
 // https://docs.gitlab.com/api/project_integrations/#datadog
 type DataDogServiceProperties struct {
-	APIURL             string `url:"api_url,omitempty" json:"api_url,omitempty"`
-	DataDogEnv         string `url:"datadog_env,omitempty" json:"datadog_env,omitempty"`
-	DataDogService     string `url:"datadog_service,omitempty" json:"datadog_service,omitempty"`
-	DataDogSite        string `url:"datadog_site,omitempty" json:"datadog_site,omitempty"`
-	DataDogTags        string `url:"datadog_tags,omitempty" json:"datadog_tags,omitempty"`
-	ArchiveTraceEvents bool   `url:"archive_trace_events,omitempty" json:"archive_trace_events,omitempty"`
+	APIURL               string `url:"api_url,omitempty" json:"api_url,omitempty"`
+	DataDogEnv           string `url:"datadog_env,omitempty" json:"datadog_env,omitempty"`
+	DataDogService       string `url:"datadog_service,omitempty" json:"datadog_service,omitempty"`
+	DataDogSite          string `url:"datadog_site,omitempty" json:"datadog_site,omitempty"`
+	DataDogTags          string `url:"datadog_tags,omitempty" json:"datadog_tags,omitempty"`
+	ArchiveTraceEvents   bool   `url:"archive_trace_events,omitempty" json:"archive_trace_events,omitempty"`
+	DataDogCIVisibility  bool   `url:"datadog_ci_visibility,omitempty" json:"datadog_ci_visibility,omitempty"`
+	UseInheritedSettings bool   `url:"use_inherited_settings,omitempty" json:"use_inherited_settings,omitempty"`
 }
 
 // GetDataDogService gets DataDog service settings for a project.
@@ -285,7 +260,7 @@ func (s *ServicesService) GetDataDogService(pid any, options ...RequestOptionFun
 	if err != nil {
 		return nil, nil, err
 	}
-	u := fmt.Sprintf("projects/%s/services/datadog", PathEscape(project))
+	u := fmt.Sprintf("projects/%s/integrations/datadog", PathEscape(project))
 
 	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
 	if err != nil {
@@ -307,13 +282,15 @@ func (s *ServicesService) GetDataDogService(pid any, options ...RequestOptionFun
 // GitLab API docs:
 // https://docs.gitlab.com/api/project_integrations/#set-up-datadog
 type SetDataDogServiceOptions struct {
-	APIKey             *string `url:"api_key,omitempty" json:"api_key,omitempty"`
-	APIURL             *string `url:"api_url,omitempty" json:"api_url,omitempty"`
-	DataDogEnv         *string `url:"datadog_env,omitempty" json:"datadog_env,omitempty"`
-	DataDogService     *string `url:"datadog_service,omitempty" json:"datadog_service,omitempty"`
-	DataDogSite        *string `url:"datadog_site,omitempty" json:"datadog_site,omitempty"`
-	DataDogTags        *string `url:"datadog_tags,omitempty" json:"datadog_tags,omitempty"`
-	ArchiveTraceEvents *bool   `url:"archive_trace_events,omitempty" json:"archive_trace_events,omitempty"`
+	APIKey               *string `url:"api_key,omitempty" json:"api_key,omitempty"`
+	APIURL               *string `url:"api_url,omitempty" json:"api_url,omitempty"`
+	DataDogEnv           *string `url:"datadog_env,omitempty" json:"datadog_env,omitempty"`
+	DataDogService       *string `url:"datadog_service,omitempty" json:"datadog_service,omitempty"`
+	DataDogSite          *string `url:"datadog_site,omitempty" json:"datadog_site,omitempty"`
+	DataDogTags          *string `url:"datadog_tags,omitempty" json:"datadog_tags,omitempty"`
+	ArchiveTraceEvents   *bool   `url:"archive_trace_events,omitempty" json:"archive_trace_events,omitempty"`
+	DataDogCIVisibility  *bool   `url:"datadog_ci_visibility,omitempty" json:"datadog_ci_visibility,omitempty"`
+	UseInheritedSettings *bool   `url:"use_inherited_settings,omitempty" json:"use_inherited_settings,omitempty"`
 }
 
 // SetDataDogService sets DataDog service settings for a project.
@@ -325,7 +302,7 @@ func (s *ServicesService) SetDataDogService(pid any, opt *SetDataDogServiceOptio
 	if err != nil {
 		return nil, nil, err
 	}
-	u := fmt.Sprintf("projects/%s/services/datadog", PathEscape(project))
+	u := fmt.Sprintf("projects/%s/integrations/datadog", PathEscape(project))
 
 	req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
 	if err != nil {
@@ -350,7 +327,7 @@ func (s *ServicesService) DeleteDataDogService(pid any, options ...RequestOption
 	if err != nil {
 		return nil, err
 	}
-	u := fmt.Sprintf("projects/%s/services/datadog", PathEscape(project))
+	u := fmt.Sprintf("projects/%s/integrations/datadog", PathEscape(project))
 
 	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
 	if err != nil {
@@ -932,13 +909,7 @@ func (s *ServicesService) GetHarborService(pid any, options ...RequestOptionFunc
 //
 // GitLab API docs:
 // https://docs.gitlab.com/api/project_integrations/#set-up-harbor
-type SetHarborServiceOptions struct {
-	URL                  *string `url:"url,omitempty" json:"url,omitempty"`
-	ProjectName          *string `url:"project_name,omitempty" json:"project_name,omitempty"`
-	Username             *string `url:"username,omitempty" json:"username,omitempty"`
-	Password             *string `url:"password,omitempty" json:"password,omitempty"`
-	UseInheritedSettings *bool   `url:"use_inherited_settings,omitempty" json:"use_inherited_settings,omitempty"`
-}
+type SetHarborServiceOptions = SetUpHarborOptions
 
 // SetHarborService sets Harbor service for a project.
 //
@@ -2178,7 +2149,7 @@ func (s *ServicesService) DeleteSlackSlashCommandsService(pid any, options ...Re
 
 // TelegramService represents Telegram service settings.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/project_integrations/#telegram
 type TelegramService struct {
 	Service
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/settings.go b/vendor/gitlab.com/gitlab-org/api/client-go/settings.go
index 2f04c29b0..9e7981695 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/settings.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/settings.go
@@ -486,6 +486,7 @@ type Settings struct {
 	UserEmailLookupLimit int `json:"user_email_lookup_limit"`
 }
 
+// UnmarshalJSON implements the json.Unmarshaler interface.
 // Settings requires a custom unmarshaller in order to properly unmarshal
 // `container_registry_import_created_before` which is either a time.Time or
 // an empty string if no value is set.
@@ -570,6 +571,7 @@ type UpdateSettingsOptions struct {
 	BulkImportEnabled                                     *bool                                   `url:"bulk_import_enabled,omitempty" json:"bulk_import_enabled,omitempty"`
 	BulkImportMaxDownloadFileSize                         *int                                    `url:"bulk_import_max_download_file_size,omitempty" json:"bulk_import_max_download_file_size,omitempty"`
 	CanCreateGroup                                        *bool                                   `url:"can_create_group,omitempty" json:"can_create_group,omitempty"`
+	CanCreateOrganization                                 *bool                                   `url:"can_create_organization,omitempty" json:"can_create_organization,omitempty"`
 	CheckNamespacePlan                                    *bool                                   `url:"check_namespace_plan,omitempty" json:"check_namespace_plan,omitempty"`
 	CIJobLiveTraceEnabled                                 *bool                                   `url:"ci_job_live_trace_enabled,omitempty" json:"ci_job_live_trace_enabled,omitempty"`
 	CIMaxIncludes                                         *int                                    `url:"ci_max_includes,omitempty" json:"ci_max_includes,omitempty"`
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/strings.go b/vendor/gitlab.com/gitlab-org/api/client-go/strings.go
index def37b255..d8aaa7f52 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/strings.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/strings.go
@@ -23,7 +23,7 @@ import (
 )
 
 // Stringify attempts to create a reasonable string representation of types in
-// the Gitlab library.  It does things like resolve pointers to their values
+// the GitLab library.  It does things like resolve pointers to their values
 // and omits struct fields with nil values.
 func Stringify(message any) string {
 	var buf bytes.Buffer
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/system_hooks.go b/vendor/gitlab.com/gitlab-org/api/client-go/system_hooks.go
index 7fe26a461..3f778ec0f 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/system_hooks.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/system_hooks.go
@@ -42,7 +42,7 @@ type (
 
 var _ SystemHooksServiceInterface = (*SystemHooksService)(nil)
 
-// Hook represents a GitLap system hook.
+// Hook represents a GitLab system hook.
 //
 // GitLab API docs: https://docs.gitlab.com/api/system_hooks/
 type Hook struct {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/tags.go b/vendor/gitlab.com/gitlab-org/api/client-go/tags.go
index 43b0e37a2..aec5f3d6b 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/tags.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/tags.go
@@ -21,6 +21,7 @@ import (
 	"math/big"
 	"net/http"
 	"net/url"
+	"time"
 )
 
 type (
@@ -53,6 +54,7 @@ type Tag struct {
 	Message   string       `json:"message"`
 	Protected bool         `json:"protected"`
 	Target    string       `json:"target"`
+	CreatedAt *time.Time   `json:"created_at"`
 }
 
 // X509Signature represents a GitLab Tag Signature object.
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/terraform_states.go b/vendor/gitlab.com/gitlab-org/api/client-go/terraform_states.go
index 0c4bea406..6ca64d25e 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/terraform_states.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/terraform_states.go
@@ -10,11 +10,12 @@ import (
 
 type (
 	TerraformStatesServiceInterface interface {
-		List(projectFullPath string) ([]TerraformState, *Response, error)
-		Get(projectFullPath string, name string) (*TerraformState, *Response, error)
+		List(projectFullPath string, options ...RequestOptionFunc) ([]TerraformState, *Response, error)
+		Get(projectFullPath string, name string, options ...RequestOptionFunc) (*TerraformState, *Response, error)
 		Download(pid any, name string, serial uint64, options ...RequestOptionFunc) (io.Reader, *Response, error)
 		DownloadLatest(pid any, name string, options ...RequestOptionFunc) (io.Reader, *Response, error)
 		Delete(pid any, name string, options ...RequestOptionFunc) (*Response, error)
+		DeleteVersion(pid any, name string, serial uint64, options ...RequestOptionFunc) (*Response, error)
 		Lock(pid any, name string, options ...RequestOptionFunc) (*Response, error)
 		Unlock(pid any, name string, options ...RequestOptionFunc) (*Response, error)
 	}
@@ -29,6 +30,8 @@ type (
 
 var _ TerraformStatesServiceInterface = (*TerraformStatesService)(nil)
 
+// TerraformState represents a Terraform state.
+//
 // GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#terraformstate
 type TerraformState struct {
 	Name          string                `json:"name"`
@@ -39,6 +42,8 @@ type TerraformState struct {
 	LockedAt      time.Time             `json:"lockedAt"`
 }
 
+// TerraformStateVersion represents a Terraform state version.
+//
 // GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#terraformstateversion
 type TerraformStateVersion struct {
 	Serial       uint64    `json:"serial"`
@@ -48,7 +53,7 @@ type TerraformStateVersion struct {
 }
 
 // List returns all Terraform states
-func (s *TerraformStatesService) List(projectFullPath string) ([]TerraformState, *Response, error) {
+func (s *TerraformStatesService) List(projectFullPath string, options ...RequestOptionFunc) ([]TerraformState, *Response, error) {
 	query := GraphQLQuery{
 		Query: fmt.Sprintf(`
 			query {
@@ -82,7 +87,7 @@ func (s *TerraformStatesService) List(projectFullPath string) ([]TerraformState,
 			} `json:"project"`
 		} `json:"data"`
 	}
-	resp, err := s.client.GraphQL.Do(query, &response)
+	resp, err := s.client.GraphQL.Do(query, &response, options...)
 	if err != nil {
 		return nil, resp, err
 	}
@@ -94,7 +99,7 @@ func (s *TerraformStatesService) List(projectFullPath string) ([]TerraformState,
 }
 
 // Get returns a single Terraform state
-func (s *TerraformStatesService) Get(projectFullPath string, name string) (*TerraformState, *Response, error) {
+func (s *TerraformStatesService) Get(projectFullPath string, name string, options ...RequestOptionFunc) (*TerraformState, *Response, error) {
 	query := GraphQLQuery{
 		Query: fmt.Sprintf(`
 			query {
@@ -124,7 +129,7 @@ func (s *TerraformStatesService) Get(projectFullPath string, name string) (*Terr
 			} `json:"project"`
 		} `json:"data"`
 	}
-	resp, err := s.client.GraphQL.Do(query, &response)
+	resp, err := s.client.GraphQL.Do(query, &response, options...)
 	if err != nil {
 		return nil, resp, err
 	}
@@ -195,6 +200,24 @@ func (s *TerraformStatesService) Delete(pid any, name string, options ...Request
 	return s.client.Do(req, nil)
 }
 
+// DeleteVersion deletes a single Terraform state version
+//
+// GitLab API docs: https://docs.gitlab.com/user/infrastructure/iac/terraform_state/
+func (s *TerraformStatesService) DeleteVersion(pid any, name string, serial uint64, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	uri := fmt.Sprintf("projects/%s/terraform/state/%s/versions/%d", PathEscape(project), PathEscape(name), serial)
+
+	req, err := s.client.NewRequest(http.MethodDelete, uri, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
 // Lock locks a single Terraform state
 //
 // GitLab API docs: https://docs.gitlab.com/user/infrastructure/iac/terraform_state/
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/todos.go b/vendor/gitlab.com/gitlab-org/api/client-go/todos.go
index 7711e7250..643a456fe 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/todos.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/todos.go
@@ -30,7 +30,7 @@ type (
 	}
 
 	// TodosService handles communication with the todos related methods of
-	// the Gitlab API.
+	// the GitLab API.
 	//
 	// GitLab API docs: https://docs.gitlab.com/api/todos/
 	TodosService struct {
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/types.go b/vendor/gitlab.com/gitlab-org/api/client-go/types.go
index 78c71830c..ebfb67d1c 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/types.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/types.go
@@ -212,7 +212,7 @@ type AvailabilityValue string
 
 // List of available availability values.
 //
-// Undocummented, see code at:
+// Undocumented, see code at:
 // https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/app/models/user_status.rb#L22
 const (
 	NotSet AvailabilityValue = "not_set"
@@ -299,7 +299,7 @@ const (
 	ProtectionRuleAccessLevelAdmin      ProtectionRuleAccessLevel = "admin"
 )
 
-// DeploymentApprovalStatus represents a Gitlab deployment approval status.
+// DeploymentApprovalStatus represents a GitLab deployment approval status.
 type DeploymentApprovalStatus string
 
 // These constants represent all valid deployment approval statuses.
@@ -308,7 +308,7 @@ const (
 	DeploymentApprovalStatusRejected DeploymentApprovalStatus = "rejected"
 )
 
-// DeploymentStatusValue represents a Gitlab deployment status.
+// DeploymentStatusValue represents a GitLab deployment status.
 type DeploymentStatusValue string
 
 // These constants represent all valid deployment statuses.
@@ -458,7 +458,7 @@ func (t ISOTime) MarshalJSON() ([]byte, error) {
 	}
 
 	if y := time.Time(t).Year(); y < 0 || y >= 10000 {
-		// ISO 8901 uses 4 digits for the years.
+		// ISO 8601 uses 4 digits for the years.
 		return nil, errors.New("json: ISOTime year outside of range [0,9999]")
 	}
 
@@ -718,9 +718,10 @@ type ResourceGroupProcessMode string
 // GitLab API docs:
 // https://docs.gitlab.com/ci/resource_groups/#process-modes
 const (
-	Unordered   ResourceGroupProcessMode = "unordered"
-	OldestFirst ResourceGroupProcessMode = "oldest_first"
-	NewestFirst ResourceGroupProcessMode = "newest_first"
+	Unordered        ResourceGroupProcessMode = "unordered"
+	OldestFirst      ResourceGroupProcessMode = "oldest_first"
+	NewestFirst      ResourceGroupProcessMode = "newest_first"
+	NewestReadyFirst ResourceGroupProcessMode = "newest_ready_first"
 )
 
 // SharedRunnersSettingValue determines whether shared runners are enabled for a
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/users.go b/vendor/gitlab.com/gitlab-org/api/client-go/users.go
index e790aa4c1..bdbdd9899 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/users.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/users.go
@@ -114,6 +114,8 @@ var (
 	ErrUserRejectPrevented           = errors.New("cannot reject a user if not authenticated as administrator")
 	ErrUserTwoFactorNotEnabled       = errors.New("cannot disable two factor authentication if not enabled")
 	ErrUserUnblockPrevented          = errors.New("cannot unblock a user that is blocked by LDAP synchronization")
+
+	errUnexpectedResultCode = errors.New("received unexpected result code")
 )
 
 // BasicUser included in other service responses (such as merge requests, pipelines, etc).
@@ -169,6 +171,7 @@ type User struct {
 	AvatarURL                      string             `json:"avatar_url"`
 	CanCreateGroup                 bool               `json:"can_create_group"`
 	CanCreateProject               bool               `json:"can_create_project"`
+	CanCreateOrganization          bool               `json:"can_create_organization"`
 	ProjectsLimit                  int                `json:"projects_limit"`
 	CurrentSignInAt                *time.Time         `json:"current_sign_in_at"`
 	CurrentSignInIP                *net.IP            `json:"current_sign_in_ip"`
@@ -217,13 +220,14 @@ func (a *UserAvatar) MarshalJSON() ([]byte, error) {
 // GitLab API docs: https://docs.gitlab.com/api/users/#list-users
 type ListUsersOptions struct {
 	ListOptions
-	Active          *bool `url:"active,omitempty" json:"active,omitempty"`
-	Blocked         *bool `url:"blocked,omitempty" json:"blocked,omitempty"`
-	Humans          *bool `url:"humans,omitempty" json:"humans,omitempty"`
-	ExcludeInternal *bool `url:"exclude_internal,omitempty" json:"exclude_internal,omitempty"`
-	ExcludeActive   *bool `url:"exclude_active,omitempty" json:"exclude_active,omitempty"`
-	ExcludeExternal *bool `url:"exclude_external,omitempty" json:"exclude_external,omitempty"`
-	ExcludeHumans   *bool `url:"exclude_humans,omitempty" json:"exclude_humans,omitempty"`
+	Active          *bool   `url:"active,omitempty" json:"active,omitempty"`
+	Blocked         *bool   `url:"blocked,omitempty" json:"blocked,omitempty"`
+	Humans          *bool   `url:"humans,omitempty" json:"humans,omitempty"`
+	ExcludeInternal *bool   `url:"exclude_internal,omitempty" json:"exclude_internal,omitempty"`
+	ExcludeActive   *bool   `url:"exclude_active,omitempty" json:"exclude_active,omitempty"`
+	ExcludeExternal *bool   `url:"exclude_external,omitempty" json:"exclude_external,omitempty"`
+	ExcludeHumans   *bool   `url:"exclude_humans,omitempty" json:"exclude_humans,omitempty"`
+	PublicEmail     *string `url:"public_email,omitempty" json:"public_email,omitempty"`
 
 	// The options below are only available for admins.
 	Search               *string    `url:"search,omitempty" json:"search,omitempty"`
@@ -545,7 +549,7 @@ func (s *UsersService) SetUserStatus(opt *UserStatusOptions, options ...RequestO
 
 // UserAssociationsCount represents the user associations count.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/users/#get-a-count-of-a-users-projects-groups-issues-and-merge-requests
 type UserAssociationsCount struct {
 	GroupsCount        int `json:"groups_count"`
@@ -556,7 +560,7 @@ type UserAssociationsCount struct {
 
 // GetUserAssociationsCount gets a list of a specified user associations.
 //
-// Gitlab API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/users/#get-a-count-of-a-users-projects-groups-issues-and-merge-requests
 func (s *UsersService) GetUserAssociationsCount(user int, options ...RequestOptionFunc) (*UserAssociationsCount, *Response, error) {
 	u := fmt.Sprintf("users/%d/associations_count", user)
@@ -1111,7 +1115,7 @@ func (s *UsersService) BlockUser(user int, options ...RequestOptionFunc) error {
 	case 404:
 		return ErrUserNotFound
 	default:
-		return fmt.Errorf("received unexpected result code: %d", resp.StatusCode)
+		return fmt.Errorf("%w: %d", errUnexpectedResultCode, resp.StatusCode)
 	}
 }
 
@@ -1139,7 +1143,7 @@ func (s *UsersService) UnblockUser(user int, options ...RequestOptionFunc) error
 	case 404:
 		return ErrUserNotFound
 	default:
-		return fmt.Errorf("received unexpected result code: %d", resp.StatusCode)
+		return fmt.Errorf("%w: %d", errUnexpectedResultCode, resp.StatusCode)
 	}
 }
 
@@ -1165,7 +1169,7 @@ func (s *UsersService) BanUser(user int, options ...RequestOptionFunc) error {
 	case 404:
 		return ErrUserNotFound
 	default:
-		return fmt.Errorf("received unexpected result code: %d", resp.StatusCode)
+		return fmt.Errorf("%w: %d", errUnexpectedResultCode, resp.StatusCode)
 	}
 }
 
@@ -1191,7 +1195,7 @@ func (s *UsersService) UnbanUser(user int, options ...RequestOptionFunc) error {
 	case 404:
 		return ErrUserNotFound
 	default:
-		return fmt.Errorf("received unexpected result code: %d", resp.StatusCode)
+		return fmt.Errorf("%w: %d", errUnexpectedResultCode, resp.StatusCode)
 	}
 }
 
@@ -1219,7 +1223,7 @@ func (s *UsersService) DeactivateUser(user int, options ...RequestOptionFunc) er
 	case 404:
 		return ErrUserNotFound
 	default:
-		return fmt.Errorf("received unexpected result code: %d", resp.StatusCode)
+		return fmt.Errorf("%w: %d", errUnexpectedResultCode, resp.StatusCode)
 	}
 }
 
@@ -1247,7 +1251,7 @@ func (s *UsersService) ActivateUser(user int, options ...RequestOptionFunc) erro
 	case 404:
 		return ErrUserNotFound
 	default:
-		return fmt.Errorf("received unexpected result code: %d", resp.StatusCode)
+		return fmt.Errorf("%w: %d", errUnexpectedResultCode, resp.StatusCode)
 	}
 }
 
@@ -1275,7 +1279,7 @@ func (s *UsersService) ApproveUser(user int, options ...RequestOptionFunc) error
 	case 404:
 		return ErrUserNotFound
 	default:
-		return fmt.Errorf("received unexpected result code: %d", resp.StatusCode)
+		return fmt.Errorf("%w: %d", errUnexpectedResultCode, resp.StatusCode)
 	}
 }
 
@@ -1305,7 +1309,7 @@ func (s *UsersService) RejectUser(user int, options ...RequestOptionFunc) error
 	case 409:
 		return ErrUserConflict
 	default:
-		return fmt.Errorf("received unexpected result code: %d", resp.StatusCode)
+		return fmt.Errorf("%w: %d", errUnexpectedResultCode, resp.StatusCode)
 	}
 }
 
@@ -1501,7 +1505,7 @@ type UserActivity struct {
 
 // GetUserActivitiesOptions represents the options for GetUserActivities
 //
-// GitLap API docs:
+// GitLab API docs:
 // https://docs.gitlab.com/api/users/#list-a-users-activity
 type GetUserActivitiesOptions struct {
 	ListOptions
@@ -1595,7 +1599,7 @@ func (s *UsersService) DisableTwoFactor(user int, options ...RequestOptionFunc)
 	case 404:
 		return ErrUserNotFound
 	default:
-		return fmt.Errorf("received unexpected result code: %d", resp.StatusCode)
+		return fmt.Errorf("%w: %d", errUnexpectedResultCode, resp.StatusCode)
 	}
 }
 
diff --git a/vendor/gitlab.com/gitlab-org/api/client-go/wikis.go b/vendor/gitlab.com/gitlab-org/api/client-go/wikis.go
index e17b2041c..1c14f4210 100644
--- a/vendor/gitlab.com/gitlab-org/api/client-go/wikis.go
+++ b/vendor/gitlab.com/gitlab-org/api/client-go/wikis.go
@@ -33,7 +33,7 @@ type (
 	}
 
 	// WikisService handles communication with the wikis related methods of
-	// the Gitlab API.
+	// the GitLab API.
 	//
 	// GitLab API docs:
 	// https://docs.gitlab.com/api/wikis/
diff --git a/vendor/golang.org/x/crypto/curve25519/curve25519.go b/vendor/golang.org/x/crypto/curve25519/curve25519.go
index 8ff087df4..048faef3a 100644
--- a/vendor/golang.org/x/crypto/curve25519/curve25519.go
+++ b/vendor/golang.org/x/crypto/curve25519/curve25519.go
@@ -3,11 +3,14 @@
 // license that can be found in the LICENSE file.
 
 // Package curve25519 provides an implementation of the X25519 function, which
-// performs scalar multiplication on the elliptic curve known as Curve25519.
-// See RFC 7748.
+// performs scalar multiplication on the elliptic curve known as Curve25519
+// according to [RFC 7748].
 //
-// This package is a wrapper for the X25519 implementation
-// in the crypto/ecdh package.
+// The curve25519 package is a wrapper for the X25519 implementation in the
+// crypto/ecdh package. It is [frozen] and is not accepting new features.
+//
+// [RFC 7748]: https://datatracker.ietf.org/doc/html/rfc7748
+// [frozen]: https://go.dev/wiki/Frozen
 package curve25519
 
 import "crypto/ecdh"
diff --git a/vendor/golang.org/x/crypto/ed25519/ed25519.go b/vendor/golang.org/x/crypto/ed25519/ed25519.go
index 59b3a95a7..df453dcce 100644
--- a/vendor/golang.org/x/crypto/ed25519/ed25519.go
+++ b/vendor/golang.org/x/crypto/ed25519/ed25519.go
@@ -2,16 +2,19 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package ed25519 implements the Ed25519 signature algorithm. See
-// https://ed25519.cr.yp.to/.
+// Package ed25519 implements the Ed25519 signature algorithm.
 //
 // These functions are also compatible with the “Ed25519” function defined in
-// RFC 8032. However, unlike RFC 8032's formulation, this package's private key
+// [RFC 8032]. However, unlike RFC 8032's formulation, this package's private key
 // representation includes a public key suffix to make multiple signing
 // operations with the same key more efficient. This package refers to the RFC
 // 8032 private key as the “seed”.
 //
-// This package is a wrapper around the standard library crypto/ed25519 package.
+// The ed25519 package is a wrapper for the Ed25519 implementation in the
+// crypto/ed25519 package. It is [frozen] and is not accepting new features.
+//
+// [RFC 8032]: https://datatracker.ietf.org/doc/html/rfc8032
+// [frozen]: https://go.dev/wiki/Frozen
 package ed25519
 
 import (
diff --git a/vendor/golang.org/x/crypto/ssh/agent/client.go b/vendor/golang.org/x/crypto/ssh/agent/client.go
index 37525e1a1..b357e18b0 100644
--- a/vendor/golang.org/x/crypto/ssh/agent/client.go
+++ b/vendor/golang.org/x/crypto/ssh/agent/client.go
@@ -430,8 +430,9 @@ func (c *client) List() ([]*Key, error) {
 		return keys, nil
 	case *failureAgentMsg:
 		return nil, errors.New("agent: failed to list keys")
+	default:
+		return nil, fmt.Errorf("agent: failed to list keys, unexpected message type %T", msg)
 	}
-	panic("unreachable")
 }
 
 // Sign has the agent sign the data using a protocol 2 key as defined
@@ -462,8 +463,9 @@ func (c *client) SignWithFlags(key ssh.PublicKey, data []byte, flags SignatureFl
 		return &sig, nil
 	case *failureAgentMsg:
 		return nil, errors.New("agent: failed to sign challenge")
+	default:
+		return nil, fmt.Errorf("agent: failed to sign challenge, unexpected message type %T", msg)
 	}
-	panic("unreachable")
 }
 
 // unmarshal parses an agent message in packet, returning the parsed
diff --git a/vendor/golang.org/x/crypto/ssh/cipher.go b/vendor/golang.org/x/crypto/ssh/cipher.go
index 6a5b582aa..7554ed57a 100644
--- a/vendor/golang.org/x/crypto/ssh/cipher.go
+++ b/vendor/golang.org/x/crypto/ssh/cipher.go
@@ -8,6 +8,7 @@ import (
 	"crypto/aes"
 	"crypto/cipher"
 	"crypto/des"
+	"crypto/fips140"
 	"crypto/rc4"
 	"crypto/subtle"
 	"encoding/binary"
@@ -15,6 +16,7 @@ import (
 	"fmt"
 	"hash"
 	"io"
+	"slices"
 
 	"golang.org/x/crypto/chacha20"
 	"golang.org/x/crypto/internal/poly1305"
@@ -93,41 +95,41 @@ func streamCipherMode(skip int, createFunc func(key, iv []byte) (cipher.Stream,
 }
 
 // cipherModes documents properties of supported ciphers. Ciphers not included
-// are not supported and will not be negotiated, even if explicitly requested in
-// ClientConfig.Crypto.Ciphers.
-var cipherModes = map[string]*cipherMode{
-	// Ciphers from RFC 4344, which introduced many CTR-based ciphers. Algorithms
-	// are defined in the order specified in the RFC.
-	CipherAES128CTR: {16, aes.BlockSize, streamCipherMode(0, newAESCTR)},
-	CipherAES192CTR: {24, aes.BlockSize, streamCipherMode(0, newAESCTR)},
-	CipherAES256CTR: {32, aes.BlockSize, streamCipherMode(0, newAESCTR)},
-
-	// Ciphers from RFC 4345, which introduces security-improved arcfour ciphers.
-	// They are defined in the order specified in the RFC.
-	InsecureCipherRC4128: {16, 0, streamCipherMode(1536, newRC4)},
-	InsecureCipherRC4256: {32, 0, streamCipherMode(1536, newRC4)},
-
-	// Cipher defined in RFC 4253, which describes SSH Transport Layer Protocol.
-	// Note that this cipher is not safe, as stated in RFC 4253: "Arcfour (and
-	// RC4) has problems with weak keys, and should be used with caution."
-	// RFC 4345 introduces improved versions of Arcfour.
-	InsecureCipherRC4: {16, 0, streamCipherMode(0, newRC4)},
-
-	// AEAD ciphers
-	CipherAES128GCM:        {16, 12, newGCMCipher},
-	CipherAES256GCM:        {32, 12, newGCMCipher},
-	CipherChaCha20Poly1305: {64, 0, newChaCha20Cipher},
-
+// are not supported and will not be negotiated, even if explicitly configured.
+// When FIPS mode is enabled, only FIPS-approved algorithms are included.
+var cipherModes = map[string]*cipherMode{}
+
+func init() {
+	cipherModes[CipherAES128CTR] = &cipherMode{16, aes.BlockSize, streamCipherMode(0, newAESCTR)}
+	cipherModes[CipherAES192CTR] = &cipherMode{24, aes.BlockSize, streamCipherMode(0, newAESCTR)}
+	cipherModes[CipherAES256CTR] = &cipherMode{32, aes.BlockSize, streamCipherMode(0, newAESCTR)}
+	//  Use of GCM with arbitrary IVs is not allowed in FIPS 140-only mode,
+	// we'll wire it up to NewGCMForSSH in Go 1.26.
+	//
+	// For now it means we'll work with fips140=on but not fips140=only.
+	cipherModes[CipherAES128GCM] = &cipherMode{16, 12, newGCMCipher}
+	cipherModes[CipherAES256GCM] = &cipherMode{32, 12, newGCMCipher}
+
+	if fips140.Enabled() {
+		defaultCiphers = slices.DeleteFunc(defaultCiphers, func(algo string) bool {
+			_, ok := cipherModes[algo]
+			return !ok
+		})
+		return
+	}
+
+	cipherModes[CipherChaCha20Poly1305] = &cipherMode{64, 0, newChaCha20Cipher}
+	// Insecure ciphers not included in the default configuration.
+	cipherModes[InsecureCipherRC4128] = &cipherMode{16, 0, streamCipherMode(1536, newRC4)}
+	cipherModes[InsecureCipherRC4256] = &cipherMode{32, 0, streamCipherMode(1536, newRC4)}
+	cipherModes[InsecureCipherRC4] = &cipherMode{16, 0, streamCipherMode(0, newRC4)}
 	// CBC mode is insecure and so is not included in the default config.
 	// (See https://www.ieee-security.org/TC/SP2013/papers/4977a526.pdf). If absolutely
 	// needed, it's possible to specify a custom Config to enable it.
 	// You should expect that an active attacker can recover plaintext if
 	// you do.
-	InsecureCipherAES128CBC: {16, aes.BlockSize, newAESCBCCipher},
-
-	// 3des-cbc is insecure and is not included in the default
-	// config.
-	InsecureCipherTripleDESCBC: {24, des.BlockSize, newTripleDESCBCCipher},
+	cipherModes[InsecureCipherAES128CBC] = &cipherMode{16, aes.BlockSize, newAESCBCCipher}
+	cipherModes[InsecureCipherTripleDESCBC] = &cipherMode{24, des.BlockSize, newTripleDESCBCCipher}
 }
 
 // prefixLen is the length of the packet prefix that contains the packet length
diff --git a/vendor/golang.org/x/crypto/ssh/client_auth.go b/vendor/golang.org/x/crypto/ssh/client_auth.go
index c12818fdc..3127e4990 100644
--- a/vendor/golang.org/x/crypto/ssh/client_auth.go
+++ b/vendor/golang.org/x/crypto/ssh/client_auth.go
@@ -9,6 +9,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"slices"
 	"strings"
 )
 
@@ -83,7 +84,7 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
 			// success
 			return nil
 		} else if ok == authFailure {
-			if m := auth.method(); !contains(tried, m) {
+			if m := auth.method(); !slices.Contains(tried, m) {
 				tried = append(tried, m)
 			}
 		}
@@ -97,7 +98,7 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
 	findNext:
 		for _, a := range config.Auth {
 			candidateMethod := a.method()
-			if contains(tried, candidateMethod) {
+			if slices.Contains(tried, candidateMethod) {
 				continue
 			}
 			for _, meth := range methods {
@@ -117,15 +118,6 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
 	return fmt.Errorf("ssh: unable to authenticate, attempted methods %v, no supported methods remain", tried)
 }
 
-func contains(list []string, e string) bool {
-	for _, s := range list {
-		if s == e {
-			return true
-		}
-	}
-	return false
-}
-
 // An AuthMethod represents an instance of an RFC 4252 authentication method.
 type AuthMethod interface {
 	// auth authenticates user over transport t.
@@ -255,7 +247,7 @@ func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (MultiA
 		// Fallback to use if there is no "server-sig-algs" extension or a
 		// common algorithm cannot be found. We use the public key format if the
 		// MultiAlgorithmSigner supports it, otherwise we return an error.
-		if !contains(as.Algorithms(), underlyingAlgo(keyFormat)) {
+		if !slices.Contains(as.Algorithms(), underlyingAlgo(keyFormat)) {
 			return "", fmt.Errorf("ssh: no common public key signature algorithm, server only supports %q for key type %q, signer only supports %v",
 				underlyingAlgo(keyFormat), keyFormat, as.Algorithms())
 		}
@@ -284,7 +276,7 @@ func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (MultiA
 	// Filter algorithms based on those supported by MultiAlgorithmSigner.
 	var keyAlgos []string
 	for _, algo := range algorithmsForKeyFormat(keyFormat) {
-		if contains(as.Algorithms(), underlyingAlgo(algo)) {
+		if slices.Contains(as.Algorithms(), underlyingAlgo(algo)) {
 			keyAlgos = append(keyAlgos, algo)
 		}
 	}
@@ -334,7 +326,7 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
 		// the key try to use the obtained algorithm as if "server-sig-algs" had
 		// not been implemented if supported from the algorithm signer.
 		if !ok && idx < origSignersLen && isRSACert(algo) && algo != CertAlgoRSAv01 {
-			if contains(as.Algorithms(), KeyAlgoRSA) {
+			if slices.Contains(as.Algorithms(), KeyAlgoRSA) {
 				// We retry using the compat algorithm after all signers have
 				// been tried normally.
 				signers = append(signers, &multiAlgorithmSigner{
@@ -385,7 +377,7 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
 		// contain the "publickey" method, do not attempt to authenticate with any
 		// other keys.  According to RFC 4252 Section 7, the latter can occur when
 		// additional authentication methods are required.
-		if success == authSuccess || !contains(methods, cb.method()) {
+		if success == authSuccess || !slices.Contains(methods, cb.method()) {
 			return success, methods, err
 		}
 	}
@@ -434,7 +426,7 @@ func confirmKeyAck(key PublicKey, c packetConn) (bool, error) {
 			// servers send the key type instead. OpenSSH allows any algorithm
 			// that matches the public key, so we do the same.
 			// https://github.com/openssh/openssh-portable/blob/86bdd385/sshconnect2.c#L709
-			if !contains(algorithmsForKeyFormat(key.Type()), msg.Algo) {
+			if !slices.Contains(algorithmsForKeyFormat(key.Type()), msg.Algo) {
 				return false, nil
 			}
 			if !bytes.Equal(msg.PubKey, pubKey) {
diff --git a/vendor/golang.org/x/crypto/ssh/common.go b/vendor/golang.org/x/crypto/ssh/common.go
index 8bfad16c4..2e44e9c9e 100644
--- a/vendor/golang.org/x/crypto/ssh/common.go
+++ b/vendor/golang.org/x/crypto/ssh/common.go
@@ -6,6 +6,7 @@ package ssh
 
 import (
 	"crypto"
+	"crypto/fips140"
 	"crypto/rand"
 	"fmt"
 	"io"
@@ -256,6 +257,40 @@ type Algorithms struct {
 	PublicKeyAuths []string
 }
 
+func init() {
+	if fips140.Enabled() {
+		defaultHostKeyAlgos = slices.DeleteFunc(defaultHostKeyAlgos, func(algo string) bool {
+			_, err := hashFunc(underlyingAlgo(algo))
+			return err != nil
+		})
+		defaultPubKeyAuthAlgos = slices.DeleteFunc(defaultPubKeyAuthAlgos, func(algo string) bool {
+			_, err := hashFunc(underlyingAlgo(algo))
+			return err != nil
+		})
+	}
+}
+
+func hashFunc(format string) (crypto.Hash, error) {
+	switch format {
+	case KeyAlgoRSASHA256, KeyAlgoECDSA256, KeyAlgoSKED25519, KeyAlgoSKECDSA256:
+		return crypto.SHA256, nil
+	case KeyAlgoECDSA384:
+		return crypto.SHA384, nil
+	case KeyAlgoRSASHA512, KeyAlgoECDSA521:
+		return crypto.SHA512, nil
+	case KeyAlgoED25519:
+		// KeyAlgoED25519 doesn't pre-hash.
+		return 0, nil
+	case KeyAlgoRSA, InsecureKeyAlgoDSA:
+		if fips140.Enabled() {
+			return 0, fmt.Errorf("ssh: hash algorithm for format %q not allowed in FIPS 140 mode", format)
+		}
+		return crypto.SHA1, nil
+	default:
+		return 0, fmt.Errorf("ssh: hash algorithm for format %q not mapped", format)
+	}
+}
+
 // SupportedAlgorithms returns algorithms currently implemented by this package,
 // excluding those with security issues, which are returned by
 // InsecureAlgorithms. The algorithms listed here are in preference order.
@@ -283,21 +318,6 @@ func InsecureAlgorithms() Algorithms {
 
 var supportedCompressions = []string{compressionNone}
 
-// hashFuncs keeps the mapping of supported signature algorithms to their
-// respective hashes needed for signing and verification.
-var hashFuncs = map[string]crypto.Hash{
-	KeyAlgoRSA:         crypto.SHA1,
-	KeyAlgoRSASHA256:   crypto.SHA256,
-	KeyAlgoRSASHA512:   crypto.SHA512,
-	InsecureKeyAlgoDSA: crypto.SHA1,
-	KeyAlgoECDSA256:    crypto.SHA256,
-	KeyAlgoECDSA384:    crypto.SHA384,
-	KeyAlgoECDSA521:    crypto.SHA512,
-	// KeyAlgoED25519 doesn't pre-hash.
-	KeyAlgoSKECDSA256: crypto.SHA256,
-	KeyAlgoSKED25519:  crypto.SHA256,
-}
-
 // algorithmsForKeyFormat returns the supported signature algorithms for a given
 // public key format (PublicKey.Type), in order of preference. See RFC 8332,
 // Section 2. See also the note in sendKexInit on backwards compatibility.
@@ -312,11 +332,40 @@ func algorithmsForKeyFormat(keyFormat string) []string {
 	}
 }
 
+// keyFormatForAlgorithm returns the key format corresponding to the given
+// signature algorithm. It returns an empty string if the signature algorithm is
+// invalid or unsupported.
+func keyFormatForAlgorithm(sigAlgo string) string {
+	switch sigAlgo {
+	case KeyAlgoRSA, KeyAlgoRSASHA256, KeyAlgoRSASHA512:
+		return KeyAlgoRSA
+	case CertAlgoRSAv01, CertAlgoRSASHA256v01, CertAlgoRSASHA512v01:
+		return CertAlgoRSAv01
+	case KeyAlgoED25519,
+		KeyAlgoSKED25519,
+		KeyAlgoSKECDSA256,
+		KeyAlgoECDSA256,
+		KeyAlgoECDSA384,
+		KeyAlgoECDSA521,
+		InsecureKeyAlgoDSA,
+		InsecureCertAlgoDSAv01,
+		CertAlgoECDSA256v01,
+		CertAlgoECDSA384v01,
+		CertAlgoECDSA521v01,
+		CertAlgoSKECDSA256v01,
+		CertAlgoED25519v01,
+		CertAlgoSKED25519v01:
+		return sigAlgo
+	default:
+		return ""
+	}
+}
+
 // isRSA returns whether algo is a supported RSA algorithm, including certificate
 // algorithms.
 func isRSA(algo string) bool {
 	algos := algorithmsForKeyFormat(KeyAlgoRSA)
-	return contains(algos, underlyingAlgo(algo))
+	return slices.Contains(algos, underlyingAlgo(algo))
 }
 
 func isRSACert(algo string) bool {
@@ -515,7 +564,7 @@ func (c *Config) SetDefaults() {
 		if kexAlgoMap[k] != nil {
 			// Ignore the KEX if we have no kexAlgoMap definition.
 			kexs = append(kexs, k)
-			if k == KeyExchangeCurve25519 && !contains(c.KeyExchanges, keyExchangeCurve25519LibSSH) {
+			if k == KeyExchangeCurve25519 && !slices.Contains(c.KeyExchanges, keyExchangeCurve25519LibSSH) {
 				kexs = append(kexs, keyExchangeCurve25519LibSSH)
 			}
 		}
diff --git a/vendor/golang.org/x/crypto/ssh/doc.go b/vendor/golang.org/x/crypto/ssh/doc.go
index 04ccce346..5b4de9eff 100644
--- a/vendor/golang.org/x/crypto/ssh/doc.go
+++ b/vendor/golang.org/x/crypto/ssh/doc.go
@@ -17,8 +17,18 @@ References:
 	[PROTOCOL.certkeys]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD
 	[SSH-PARAMETERS]:    http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1
 	[SSH-CERTS]:	https://datatracker.ietf.org/doc/html/draft-miller-ssh-cert-01
+	[FIPS 140-3 mode]: https://go.dev/doc/security/fips140
 
 This package does not fall under the stability promise of the Go language itself,
 so its API may be changed when pressing needs arise.
+
+# FIPS 140-3 mode
+
+When the program is in [FIPS 140-3 mode], this package behaves as if only SP
+800-140C and SP 800-140D approved cipher suites, signature algorithms,
+certificate public key types and sizes, and key exchange and derivation
+algorithms were implemented. Others are silently ignored and not negotiated, or
+rejected. This set may depend on the algorithms supported by the FIPS 140-3 Go
+Cryptographic Module selected with GOFIPS140, and may change across Go versions.
 */
 package ssh
diff --git a/vendor/golang.org/x/crypto/ssh/handshake.go b/vendor/golang.org/x/crypto/ssh/handshake.go
index a90bfe331..4be3cbb6d 100644
--- a/vendor/golang.org/x/crypto/ssh/handshake.go
+++ b/vendor/golang.org/x/crypto/ssh/handshake.go
@@ -10,6 +10,7 @@ import (
 	"io"
 	"log"
 	"net"
+	"slices"
 	"strings"
 	"sync"
 )
@@ -527,7 +528,7 @@ func (t *handshakeTransport) sendKexInit() error {
 			switch s := k.(type) {
 			case MultiAlgorithmSigner:
 				for _, algo := range algorithmsForKeyFormat(keyFormat) {
-					if contains(s.Algorithms(), underlyingAlgo(algo)) {
+					if slices.Contains(s.Algorithms(), underlyingAlgo(algo)) {
 						msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, algo)
 					}
 				}
@@ -679,7 +680,7 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
 		return err
 	}
 
-	if t.sessionID == nil && ((isClient && contains(serverInit.KexAlgos, kexStrictServer)) || (!isClient && contains(clientInit.KexAlgos, kexStrictClient))) {
+	if t.sessionID == nil && ((isClient && slices.Contains(serverInit.KexAlgos, kexStrictServer)) || (!isClient && slices.Contains(clientInit.KexAlgos, kexStrictClient))) {
 		t.strictMode = true
 		if err := t.conn.setStrictMode(); err != nil {
 			return err
@@ -736,7 +737,7 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
 	// On the server side, after the first SSH_MSG_NEWKEYS, send a SSH_MSG_EXT_INFO
 	// message with the server-sig-algs extension if the client supports it. See
 	// RFC 8308, Sections 2.4 and 3.1, and [PROTOCOL], Section 1.9.
-	if !isClient && firstKeyExchange && contains(clientInit.KexAlgos, "ext-info-c") {
+	if !isClient && firstKeyExchange && slices.Contains(clientInit.KexAlgos, "ext-info-c") {
 		supportedPubKeyAuthAlgosList := strings.Join(t.publicKeyAuthAlgorithms, ",")
 		extInfo := &extInfoMsg{
 			NumExtensions: 2,
@@ -790,7 +791,7 @@ func (a algorithmSignerWrapper) SignWithAlgorithm(rand io.Reader, data []byte, a
 func pickHostKey(hostKeys []Signer, algo string) AlgorithmSigner {
 	for _, k := range hostKeys {
 		if s, ok := k.(MultiAlgorithmSigner); ok {
-			if !contains(s.Algorithms(), underlyingAlgo(algo)) {
+			if !slices.Contains(s.Algorithms(), underlyingAlgo(algo)) {
 				continue
 			}
 		}
diff --git a/vendor/golang.org/x/crypto/ssh/kex.go b/vendor/golang.org/x/crypto/ssh/kex.go
index 78aaf0310..5f7fdd851 100644
--- a/vendor/golang.org/x/crypto/ssh/kex.go
+++ b/vendor/golang.org/x/crypto/ssh/kex.go
@@ -8,12 +8,14 @@ import (
 	"crypto"
 	"crypto/ecdsa"
 	"crypto/elliptic"
+	"crypto/fips140"
 	"crypto/rand"
 	"encoding/binary"
 	"errors"
 	"fmt"
 	"io"
 	"math/big"
+	"slices"
 
 	"golang.org/x/crypto/curve25519"
 )
@@ -395,9 +397,27 @@ func ecHash(curve elliptic.Curve) crypto.Hash {
 	return crypto.SHA512
 }
 
+// kexAlgoMap defines the supported KEXs. KEXs not included are not supported
+// and will not be negotiated, even if explicitly configured. When FIPS mode is
+// enabled, only FIPS-approved algorithms are included.
 var kexAlgoMap = map[string]kexAlgorithm{}
 
 func init() {
+	// mlkem768x25519-sha256 we'll work with fips140=on but not fips140=only
+	// until Go 1.26.
+	kexAlgoMap[KeyExchangeMLKEM768X25519] = &mlkem768WithCurve25519sha256{}
+	kexAlgoMap[KeyExchangeECDHP521] = &ecdh{elliptic.P521()}
+	kexAlgoMap[KeyExchangeECDHP384] = &ecdh{elliptic.P384()}
+	kexAlgoMap[KeyExchangeECDHP256] = &ecdh{elliptic.P256()}
+
+	if fips140.Enabled() {
+		defaultKexAlgos = slices.DeleteFunc(defaultKexAlgos, func(algo string) bool {
+			_, ok := kexAlgoMap[algo]
+			return !ok
+		})
+		return
+	}
+
 	p, _ := new(big.Int).SetString(oakleyGroup2, 16)
 	kexAlgoMap[InsecureKeyExchangeDH1SHA1] = &dhGroup{
 		g:        new(big.Int).SetInt64(2),
@@ -431,14 +451,10 @@ func init() {
 		hashFunc: crypto.SHA512,
 	}
 
-	kexAlgoMap[KeyExchangeECDHP521] = &ecdh{elliptic.P521()}
-	kexAlgoMap[KeyExchangeECDHP384] = &ecdh{elliptic.P384()}
-	kexAlgoMap[KeyExchangeECDHP256] = &ecdh{elliptic.P256()}
 	kexAlgoMap[KeyExchangeCurve25519] = &curve25519sha256{}
 	kexAlgoMap[keyExchangeCurve25519LibSSH] = &curve25519sha256{}
 	kexAlgoMap[InsecureKeyExchangeDHGEXSHA1] = &dhGEXSHA{hashFunc: crypto.SHA1}
 	kexAlgoMap[KeyExchangeDHGEXSHA256] = &dhGEXSHA{hashFunc: crypto.SHA256}
-	kexAlgoMap[KeyExchangeMLKEM768X25519] = &mlkem768WithCurve25519sha256{}
 }
 
 // curve25519sha256 implements the curve25519-sha256 (formerly known as
diff --git a/vendor/golang.org/x/crypto/ssh/keys.go b/vendor/golang.org/x/crypto/ssh/keys.go
index a28c0de50..a035956fc 100644
--- a/vendor/golang.org/x/crypto/ssh/keys.go
+++ b/vendor/golang.org/x/crypto/ssh/keys.go
@@ -27,6 +27,7 @@ import (
 	"fmt"
 	"io"
 	"math/big"
+	"slices"
 	"strings"
 
 	"golang.org/x/crypto/ssh/internal/bcrypt_pbkdf"
@@ -89,6 +90,11 @@ func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, err err
 		}
 		return cert, nil, nil
 	}
+	if keyFormat := keyFormatForAlgorithm(algo); keyFormat != "" {
+		return nil, nil, fmt.Errorf("ssh: signature algorithm %q isn't a key format; key is malformed and should be re-encoded with type %q",
+			algo, keyFormat)
+	}
+
 	return nil, nil, fmt.Errorf("ssh: unknown key algorithm: %v", algo)
 }
 
@@ -191,9 +197,10 @@ func ParseKnownHosts(in []byte) (marker string, hosts []string, pubKey PublicKey
 	return "", nil, nil, "", nil, io.EOF
 }
 
-// ParseAuthorizedKey parses a public key from an authorized_keys
-// file used in OpenSSH according to the sshd(8) manual page.
+// ParseAuthorizedKey parses a public key from an authorized_keys file used in
+// OpenSSH according to the sshd(8) manual page. Invalid lines are ignored.
 func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) {
+	var lastErr error
 	for len(in) > 0 {
 		end := bytes.IndexByte(in, '\n')
 		if end != -1 {
@@ -222,6 +229,8 @@ func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []str
 
 		if out, comment, err = parseAuthorizedKey(in[i:]); err == nil {
 			return out, comment, options, rest, nil
+		} else {
+			lastErr = err
 		}
 
 		// No key type recognised. Maybe there's an options field at
@@ -264,12 +273,18 @@ func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []str
 		if out, comment, err = parseAuthorizedKey(in[i:]); err == nil {
 			options = candidateOptions
 			return out, comment, options, rest, nil
+		} else {
+			lastErr = err
 		}
 
 		in = rest
 		continue
 	}
 
+	if lastErr != nil {
+		return nil, "", nil, nil, fmt.Errorf("ssh: no key found; last parsing error for ignored line: %w", lastErr)
+	}
+
 	return nil, "", nil, nil, errors.New("ssh: no key found")
 }
 
@@ -395,11 +410,11 @@ func NewSignerWithAlgorithms(signer AlgorithmSigner, algorithms []string) (Multi
 	}
 
 	for _, algo := range algorithms {
-		if !contains(supportedAlgos, algo) {
+		if !slices.Contains(supportedAlgos, algo) {
 			return nil, fmt.Errorf("ssh: algorithm %q is not supported for key type %q",
 				algo, signer.PublicKey().Type())
 		}
-		if !contains(signerAlgos, algo) {
+		if !slices.Contains(signerAlgos, algo) {
 			return nil, fmt.Errorf("ssh: algorithm %q is restricted for the provided signer", algo)
 		}
 	}
@@ -486,10 +501,13 @@ func (r *rsaPublicKey) Marshal() []byte {
 
 func (r *rsaPublicKey) Verify(data []byte, sig *Signature) error {
 	supportedAlgos := algorithmsForKeyFormat(r.Type())
-	if !contains(supportedAlgos, sig.Format) {
+	if !slices.Contains(supportedAlgos, sig.Format) {
 		return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, r.Type())
 	}
-	hash := hashFuncs[sig.Format]
+	hash, err := hashFunc(sig.Format)
+	if err != nil {
+		return err
+	}
 	h := hash.New()
 	h.Write(data)
 	digest := h.Sum(nil)
@@ -606,7 +624,11 @@ func (k *dsaPublicKey) Verify(data []byte, sig *Signature) error {
 	if sig.Format != k.Type() {
 		return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, k.Type())
 	}
-	h := hashFuncs[sig.Format].New()
+	hash, err := hashFunc(sig.Format)
+	if err != nil {
+		return err
+	}
+	h := hash.New()
 	h.Write(data)
 	digest := h.Sum(nil)
 
@@ -651,7 +673,11 @@ func (k *dsaPrivateKey) SignWithAlgorithm(rand io.Reader, data []byte, algorithm
 		return nil, fmt.Errorf("ssh: unsupported signature algorithm %s", algorithm)
 	}
 
-	h := hashFuncs[k.PublicKey().Type()].New()
+	hash, err := hashFunc(k.PublicKey().Type())
+	if err != nil {
+		return nil, err
+	}
+	h := hash.New()
 	h.Write(data)
 	digest := h.Sum(nil)
 	r, s, err := dsa.Sign(rand, k.PrivateKey, digest)
@@ -801,8 +827,11 @@ func (k *ecdsaPublicKey) Verify(data []byte, sig *Signature) error {
 	if sig.Format != k.Type() {
 		return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, k.Type())
 	}
-
-	h := hashFuncs[sig.Format].New()
+	hash, err := hashFunc(sig.Format)
+	if err != nil {
+		return err
+	}
+	h := hash.New()
 	h.Write(data)
 	digest := h.Sum(nil)
 
@@ -905,8 +934,11 @@ func (k *skECDSAPublicKey) Verify(data []byte, sig *Signature) error {
 	if sig.Format != k.Type() {
 		return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, k.Type())
 	}
-
-	h := hashFuncs[sig.Format].New()
+	hash, err := hashFunc(sig.Format)
+	if err != nil {
+		return err
+	}
+	h := hash.New()
 	h.Write([]byte(k.application))
 	appDigest := h.Sum(nil)
 
@@ -1009,7 +1041,11 @@ func (k *skEd25519PublicKey) Verify(data []byte, sig *Signature) error {
 		return fmt.Errorf("invalid size %d for Ed25519 public key", l)
 	}
 
-	h := hashFuncs[sig.Format].New()
+	hash, err := hashFunc(sig.Format)
+	if err != nil {
+		return err
+	}
+	h := hash.New()
 	h.Write([]byte(k.application))
 	appDigest := h.Sum(nil)
 
@@ -1112,11 +1148,14 @@ func (s *wrappedSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm
 		algorithm = s.pubKey.Type()
 	}
 
-	if !contains(s.Algorithms(), algorithm) {
+	if !slices.Contains(s.Algorithms(), algorithm) {
 		return nil, fmt.Errorf("ssh: unsupported signature algorithm %q for key format %q", algorithm, s.pubKey.Type())
 	}
 
-	hashFunc := hashFuncs[algorithm]
+	hashFunc, err := hashFunc(algorithm)
+	if err != nil {
+		return nil, err
+	}
 	var digest []byte
 	if hashFunc != 0 {
 		h := hashFunc.New()
diff --git a/vendor/golang.org/x/crypto/ssh/mac.go b/vendor/golang.org/x/crypto/ssh/mac.go
index de2639d57..87d626fbb 100644
--- a/vendor/golang.org/x/crypto/ssh/mac.go
+++ b/vendor/golang.org/x/crypto/ssh/mac.go
@@ -7,11 +7,13 @@ package ssh
 // Message authentication support
 
 import (
+	"crypto/fips140"
 	"crypto/hmac"
 	"crypto/sha1"
 	"crypto/sha256"
 	"crypto/sha512"
 	"hash"
+	"slices"
 )
 
 type macMode struct {
@@ -46,23 +48,37 @@ func (t truncatingMAC) Size() int {
 
 func (t truncatingMAC) BlockSize() int { return t.hmac.BlockSize() }
 
-var macModes = map[string]*macMode{
-	HMACSHA512ETM: {64, true, func(key []byte) hash.Hash {
+// macModes defines the supported MACs. MACs not included are not supported
+// and will not be negotiated, even if explicitly configured. When FIPS mode is
+// enabled, only FIPS-approved algorithms are included.
+var macModes = map[string]*macMode{}
+
+func init() {
+	macModes[HMACSHA512ETM] = &macMode{64, true, func(key []byte) hash.Hash {
 		return hmac.New(sha512.New, key)
-	}},
-	HMACSHA256ETM: {32, true, func(key []byte) hash.Hash {
+	}}
+	macModes[HMACSHA256ETM] = &macMode{32, true, func(key []byte) hash.Hash {
 		return hmac.New(sha256.New, key)
-	}},
-	HMACSHA512: {64, false, func(key []byte) hash.Hash {
+	}}
+	macModes[HMACSHA512] = &macMode{64, false, func(key []byte) hash.Hash {
 		return hmac.New(sha512.New, key)
-	}},
-	HMACSHA256: {32, false, func(key []byte) hash.Hash {
+	}}
+	macModes[HMACSHA256] = &macMode{32, false, func(key []byte) hash.Hash {
 		return hmac.New(sha256.New, key)
-	}},
-	HMACSHA1: {20, false, func(key []byte) hash.Hash {
+	}}
+
+	if fips140.Enabled() {
+		defaultMACs = slices.DeleteFunc(defaultMACs, func(algo string) bool {
+			_, ok := macModes[algo]
+			return !ok
+		})
+		return
+	}
+
+	macModes[HMACSHA1] = &macMode{20, false, func(key []byte) hash.Hash {
 		return hmac.New(sha1.New, key)
-	}},
-	InsecureHMACSHA196: {20, false, func(key []byte) hash.Hash {
+	}}
+	macModes[InsecureHMACSHA196] = &macMode{20, false, func(key []byte) hash.Hash {
 		return truncatingMAC{12, hmac.New(sha1.New, key)}
-	}},
+	}}
 }
diff --git a/vendor/golang.org/x/crypto/ssh/server.go b/vendor/golang.org/x/crypto/ssh/server.go
index 98679ba5b..064dcbaf5 100644
--- a/vendor/golang.org/x/crypto/ssh/server.go
+++ b/vendor/golang.org/x/crypto/ssh/server.go
@@ -10,6 +10,7 @@ import (
 	"fmt"
 	"io"
 	"net"
+	"slices"
 	"strings"
 )
 
@@ -43,6 +44,9 @@ type Permissions struct {
 	// pass data from the authentication callbacks to the server
 	// application layer.
 	Extensions map[string]string
+
+	// ExtraData allows to store user defined data.
+	ExtraData map[any]any
 }
 
 type GSSAPIWithMICConfig struct {
@@ -126,6 +130,21 @@ type ServerConfig struct {
 	// Permissions.Extensions entry.
 	PublicKeyCallback func(conn ConnMetadata, key PublicKey) (*Permissions, error)
 
+	// VerifiedPublicKeyCallback, if non-nil, is called after a client
+	// successfully confirms having control over a key that was previously
+	// approved by PublicKeyCallback. The permissions object passed to the
+	// callback is the one returned by PublicKeyCallback for the given public
+	// key and its ownership is transferred to the callback. The returned
+	// Permissions object can be the same object, optionally modified, or a
+	// completely new object. If VerifiedPublicKeyCallback is non-nil,
+	// PublicKeyCallback is not allowed to return a PartialSuccessError, which
+	// can instead be returned by VerifiedPublicKeyCallback.
+	//
+	// VerifiedPublicKeyCallback does not affect which authentication methods
+	// are included in the list of methods that can be attempted by the client.
+	VerifiedPublicKeyCallback func(conn ConnMetadata, key PublicKey, permissions *Permissions,
+		signatureAlgorithm string) (*Permissions, error)
+
 	// KeyboardInteractiveCallback, if non-nil, is called when
 	// keyboard-interactive authentication is selected (RFC
 	// 4256). The client object's Challenge function should be
@@ -246,7 +265,7 @@ func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewCha
 		fullConf.PublicKeyAuthAlgorithms = defaultPubKeyAuthAlgos
 	} else {
 		for _, algo := range fullConf.PublicKeyAuthAlgorithms {
-			if !contains(SupportedAlgorithms().PublicKeyAuths, algo) && !contains(InsecureAlgorithms().PublicKeyAuths, algo) {
+			if !slices.Contains(SupportedAlgorithms().PublicKeyAuths, algo) && !slices.Contains(InsecureAlgorithms().PublicKeyAuths, algo) {
 				c.Close()
 				return nil, nil, nil, fmt.Errorf("ssh: unsupported public key authentication algorithm %s", algo)
 			}
@@ -631,7 +650,7 @@ userAuthLoop:
 				return nil, parseError(msgUserAuthRequest)
 			}
 			algo := string(algoBytes)
-			if !contains(config.PublicKeyAuthAlgorithms, underlyingAlgo(algo)) {
+			if !slices.Contains(config.PublicKeyAuthAlgorithms, underlyingAlgo(algo)) {
 				authErr = fmt.Errorf("ssh: algorithm %q not accepted", algo)
 				break
 			}
@@ -652,6 +671,9 @@ userAuthLoop:
 				candidate.pubKeyData = pubKeyData
 				candidate.perms, candidate.result = authConfig.PublicKeyCallback(s, pubKey)
 				_, isPartialSuccessError := candidate.result.(*PartialSuccessError)
+				if isPartialSuccessError && config.VerifiedPublicKeyCallback != nil {
+					return nil, errors.New("ssh: invalid library usage: PublicKeyCallback must not return partial success when VerifiedPublicKeyCallback is defined")
+				}
 
 				if (candidate.result == nil || isPartialSuccessError) &&
 					candidate.perms != nil &&
@@ -695,7 +717,7 @@ userAuthLoop:
 				// ssh-rsa-cert-v01@openssh.com algorithm with ssh-rsa public
 				// key type. The algorithm and public key type must be
 				// consistent: both must be certificate algorithms, or neither.
-				if !contains(algorithmsForKeyFormat(pubKey.Type()), algo) {
+				if !slices.Contains(algorithmsForKeyFormat(pubKey.Type()), algo) {
 					authErr = fmt.Errorf("ssh: public key type %q not compatible with selected algorithm %q",
 						pubKey.Type(), algo)
 					break
@@ -705,7 +727,7 @@ userAuthLoop:
 				// algorithm name that corresponds to algo with
 				// sig.Format.  This is usually the same, but
 				// for certs, the names differ.
-				if !contains(config.PublicKeyAuthAlgorithms, sig.Format) {
+				if !slices.Contains(config.PublicKeyAuthAlgorithms, sig.Format) {
 					authErr = fmt.Errorf("ssh: algorithm %q not accepted", sig.Format)
 					break
 				}
@@ -722,6 +744,12 @@ userAuthLoop:
 
 				authErr = candidate.result
 				perms = candidate.perms
+				if authErr == nil && config.VerifiedPublicKeyCallback != nil {
+					// Only call VerifiedPublicKeyCallback after the key has been accepted
+					// and successfully verified. If authErr is non-nil, the key is not
+					// considered verified and the callback must not run.
+					perms, authErr = config.VerifiedPublicKeyCallback(s, pubKey, perms, algo)
+				}
 			}
 		case "gssapi-with-mic":
 			if authConfig.GSSAPIWithMICConfig == nil {
diff --git a/vendor/golang.org/x/crypto/ssh/transport.go b/vendor/golang.org/x/crypto/ssh/transport.go
index 663619845..fa3dd6a42 100644
--- a/vendor/golang.org/x/crypto/ssh/transport.go
+++ b/vendor/golang.org/x/crypto/ssh/transport.go
@@ -8,6 +8,7 @@ import (
 	"bufio"
 	"bytes"
 	"errors"
+	"fmt"
 	"io"
 	"log"
 )
@@ -254,6 +255,9 @@ var (
 // (to setup server->client keys) or clientKeys (for client->server keys).
 func newPacketCipher(d direction, algs DirectionAlgorithms, kex *kexResult) (packetCipher, error) {
 	cipherMode := cipherModes[algs.Cipher]
+	if cipherMode == nil {
+		return nil, fmt.Errorf("ssh: unsupported cipher %v", algs.Cipher)
+	}
 
 	iv := make([]byte, cipherMode.ivSize)
 	key := make([]byte, cipherMode.keySize)
diff --git a/vendor/golang.org/x/exp/LICENSE b/vendor/golang.org/x/exp/LICENSE
new file mode 100644
index 000000000..2a7cf70da
--- /dev/null
+++ b/vendor/golang.org/x/exp/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2009 The Go Authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google LLC nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/golang.org/x/exp/PATENTS b/vendor/golang.org/x/exp/PATENTS
new file mode 100644
index 000000000..733099041
--- /dev/null
+++ b/vendor/golang.org/x/exp/PATENTS
@@ -0,0 +1,22 @@
+Additional IP Rights Grant (Patents)
+
+"This implementation" means the copyrightable works distributed by
+Google as part of the Go project.
+
+Google hereby grants to You a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this section)
+patent license to make, have made, use, offer to sell, sell, import,
+transfer and otherwise run, modify and propagate the contents of this
+implementation of Go, where such license applies only to those patent
+claims, both currently owned or controlled by Google and acquired in
+the future, licensable by Google that are necessarily infringed by this
+implementation of Go.  This grant does not include claims that would be
+infringed only as a consequence of further modification of this
+implementation.  If you or your agent or exclusive licensee institute or
+order or agree to the institution of patent litigation against any
+entity (including a cross-claim or counterclaim in a lawsuit) alleging
+that this implementation of Go or any code incorporated within this
+implementation of Go constitutes direct or contributory patent
+infringement, or inducement of patent infringement, then any patent
+rights granted to you under this License for this implementation of Go
+shall terminate as of the date such litigation is filed.
diff --git a/vendor/golang.org/x/exp/constraints/constraints.go b/vendor/golang.org/x/exp/constraints/constraints.go
new file mode 100644
index 000000000..9d260bab1
--- /dev/null
+++ b/vendor/golang.org/x/exp/constraints/constraints.go
@@ -0,0 +1,54 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package constraints defines a set of useful constraints to be used
+// with type parameters.
+package constraints
+
+import "cmp"
+
+// Signed is a constraint that permits any signed integer type.
+// If future releases of Go add new predeclared signed integer types,
+// this constraint will be modified to include them.
+type Signed interface {
+	~int | ~int8 | ~int16 | ~int32 | ~int64
+}
+
+// Unsigned is a constraint that permits any unsigned integer type.
+// If future releases of Go add new predeclared unsigned integer types,
+// this constraint will be modified to include them.
+type Unsigned interface {
+	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
+}
+
+// Integer is a constraint that permits any integer type.
+// If future releases of Go add new predeclared integer types,
+// this constraint will be modified to include them.
+type Integer interface {
+	Signed | Unsigned
+}
+
+// Float is a constraint that permits any floating-point type.
+// If future releases of Go add new predeclared floating-point types,
+// this constraint will be modified to include them.
+type Float interface {
+	~float32 | ~float64
+}
+
+// Complex is a constraint that permits any complex numeric type.
+// If future releases of Go add new predeclared complex numeric types,
+// this constraint will be modified to include them.
+type Complex interface {
+	~complex64 | ~complex128
+}
+
+// Ordered is a constraint that permits any ordered type: any type
+// that supports the operators < <= >= >.
+// If future releases of Go add new ordered types,
+// this constraint will be modified to include them.
+//
+// This type is redundant since Go 1.21 introduced [cmp.Ordered].
+//
+//go:fix inline
+type Ordered = cmp.Ordered
diff --git a/vendor/golang.org/x/net/html/escape.go b/vendor/golang.org/x/net/html/escape.go
index 04c6bec21..12f227370 100644
--- a/vendor/golang.org/x/net/html/escape.go
+++ b/vendor/golang.org/x/net/html/escape.go
@@ -299,7 +299,7 @@ func escape(w writer, s string) error {
 		case '\r':
 			esc = "&#13;"
 		default:
-			panic("unrecognized escape character")
+			panic("html: unrecognized escape character")
 		}
 		s = s[i+1:]
 		if _, err := w.WriteString(esc); err != nil {
diff --git a/vendor/golang.org/x/net/html/parse.go b/vendor/golang.org/x/net/html/parse.go
index 518ee4c94..88fc0056a 100644
--- a/vendor/golang.org/x/net/html/parse.go
+++ b/vendor/golang.org/x/net/html/parse.go
@@ -136,7 +136,7 @@ func (p *parser) indexOfElementInScope(s scope, matchTags ...a.Atom) int {
 					return -1
 				}
 			default:
-				panic("unreachable")
+				panic(fmt.Sprintf("html: internal error: indexOfElementInScope unknown scope: %d", s))
 			}
 		}
 		switch s {
@@ -179,7 +179,7 @@ func (p *parser) clearStackToContext(s scope) {
 				return
 			}
 		default:
-			panic("unreachable")
+			panic(fmt.Sprintf("html: internal error: clearStackToContext unknown scope: %d", s))
 		}
 	}
 }
@@ -231,7 +231,14 @@ func (p *parser) addChild(n *Node) {
 	}
 
 	if n.Type == ElementNode {
-		p.oe = append(p.oe, n)
+		p.insertOpenElement(n)
+	}
+}
+
+func (p *parser) insertOpenElement(n *Node) {
+	p.oe = append(p.oe, n)
+	if len(p.oe) > 512 {
+		panic("html: open stack of elements exceeds 512 nodes")
 	}
 }
 
@@ -810,7 +817,7 @@ func afterHeadIM(p *parser) bool {
 			p.im = inFramesetIM
 			return true
 		case a.Base, a.Basefont, a.Bgsound, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Template, a.Title:
-			p.oe = append(p.oe, p.head)
+			p.insertOpenElement(p.head)
 			defer p.oe.remove(p.head)
 			return inHeadIM(p)
 		case a.Head:
@@ -1678,7 +1685,7 @@ func inTableBodyIM(p *parser) bool {
 	return inTableIM(p)
 }
 
-// Section 12.2.6.4.14.
+// Section 13.2.6.4.14.
 func inRowIM(p *parser) bool {
 	switch p.tok.Type {
 	case StartTagToken:
@@ -1690,7 +1697,9 @@ func inRowIM(p *parser) bool {
 			p.im = inCellIM
 			return true
 		case a.Caption, a.Col, a.Colgroup, a.Tbody, a.Tfoot, a.Thead, a.Tr:
-			if p.popUntil(tableScope, a.Tr) {
+			if p.elementInScope(tableScope, a.Tr) {
+				p.clearStackToContext(tableRowScope)
+				p.oe.pop()
 				p.im = inTableBodyIM
 				return false
 			}
@@ -1700,22 +1709,28 @@ func inRowIM(p *parser) bool {
 	case EndTagToken:
 		switch p.tok.DataAtom {
 		case a.Tr:
-			if p.popUntil(tableScope, a.Tr) {
+			if p.elementInScope(tableScope, a.Tr) {
+				p.clearStackToContext(tableRowScope)
+				p.oe.pop()
 				p.im = inTableBodyIM
 				return true
 			}
 			// Ignore the token.
 			return true
 		case a.Table:
-			if p.popUntil(tableScope, a.Tr) {
+			if p.elementInScope(tableScope, a.Tr) {
+				p.clearStackToContext(tableRowScope)
+				p.oe.pop()
 				p.im = inTableBodyIM
 				return false
 			}
 			// Ignore the token.
 			return true
 		case a.Tbody, a.Tfoot, a.Thead:
-			if p.elementInScope(tableScope, p.tok.DataAtom) {
-				p.parseImpliedToken(EndTagToken, a.Tr, a.Tr.String())
+			if p.elementInScope(tableScope, p.tok.DataAtom) && p.elementInScope(tableScope, a.Tr) {
+				p.clearStackToContext(tableRowScope)
+				p.oe.pop()
+				p.im = inTableBodyIM
 				return false
 			}
 			// Ignore the token.
@@ -2222,16 +2237,20 @@ func parseForeignContent(p *parser) bool {
 			p.acknowledgeSelfClosingTag()
 		}
 	case EndTagToken:
+		if strings.EqualFold(p.oe[len(p.oe)-1].Data, p.tok.Data) {
+			p.oe = p.oe[:len(p.oe)-1]
+			return true
+		}
 		for i := len(p.oe) - 1; i >= 0; i-- {
-			if p.oe[i].Namespace == "" {
-				return p.im(p)
-			}
 			if strings.EqualFold(p.oe[i].Data, p.tok.Data) {
 				p.oe = p.oe[:i]
+				return true
+			}
+			if i > 0 && p.oe[i-1].Namespace == "" {
 				break
 			}
 		}
-		return true
+		return p.im(p)
 	default:
 		// Ignore the token.
 	}
@@ -2312,9 +2331,13 @@ func (p *parser) parseCurrentToken() {
 	}
 }
 
-func (p *parser) parse() error {
+func (p *parser) parse() (err error) {
+	defer func() {
+		if panicErr := recover(); panicErr != nil {
+			err = fmt.Errorf("%s", panicErr)
+		}
+	}()
 	// Iterate until EOF. Any other error will cause an early return.
-	var err error
 	for err != io.EOF {
 		// CDATA sections are allowed only in foreign content.
 		n := p.oe.top()
@@ -2343,6 +2366,8 @@ func (p *parser) parse() error {
 // <tag>s. Conversely, explicit <tag>s in r's data can be silently dropped,
 // with no corresponding node in the resulting tree.
 //
+// Parse will reject HTML that is nested deeper than 512 elements.
+//
 // The input is assumed to be UTF-8 encoded.
 func Parse(r io.Reader) (*Node, error) {
 	return ParseWithOptions(r)
diff --git a/vendor/golang.org/x/net/html/render.go b/vendor/golang.org/x/net/html/render.go
index e8c123345..0157d89e1 100644
--- a/vendor/golang.org/x/net/html/render.go
+++ b/vendor/golang.org/x/net/html/render.go
@@ -184,7 +184,7 @@ func render1(w writer, n *Node) error {
 		return err
 	}
 
-	// Add initial newline where there is danger of a newline beging ignored.
+	// Add initial newline where there is danger of a newline being ignored.
 	if c := n.FirstChild; c != nil && c.Type == TextNode && strings.HasPrefix(c.Data, "\n") {
 		switch n.Data {
 		case "pre", "listing", "textarea":
diff --git a/vendor/golang.org/x/oauth2/oauth2.go b/vendor/golang.org/x/oauth2/oauth2.go
index de34feb84..3e3b63069 100644
--- a/vendor/golang.org/x/oauth2/oauth2.go
+++ b/vendor/golang.org/x/oauth2/oauth2.go
@@ -9,7 +9,6 @@
 package oauth2 // import "golang.org/x/oauth2"
 
 import (
-	"bytes"
 	"context"
 	"errors"
 	"net/http"
@@ -158,7 +157,7 @@ func SetAuthURLParam(key, value string) AuthCodeOption {
 // PKCE), https://www.oauth.com/oauth2-servers/pkce/ and
 // https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-09.html#name-cross-site-request-forgery (describing both approaches)
 func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
-	var buf bytes.Buffer
+	var buf strings.Builder
 	buf.WriteString(c.Endpoint.AuthURL)
 	v := url.Values{
 		"response_type": {"code"},
diff --git a/vendor/golang.org/x/sys/unix/affinity_linux.go b/vendor/golang.org/x/sys/unix/affinity_linux.go
index 3c7a6d6e2..3ea470387 100644
--- a/vendor/golang.org/x/sys/unix/affinity_linux.go
+++ b/vendor/golang.org/x/sys/unix/affinity_linux.go
@@ -41,6 +41,15 @@ func (s *CPUSet) Zero() {
 	clear(s[:])
 }
 
+// Fill adds all possible CPU bits to the set s. On Linux, [SchedSetaffinity]
+// will silently ignore any invalid CPU bits in [CPUSet] so this is an
+// efficient way of resetting the CPU affinity of a process.
+func (s *CPUSet) Fill() {
+	for i := range s {
+		s[i] = ^cpuMask(0)
+	}
+}
+
 func cpuBitsIndex(cpu int) int {
 	return cpu / _NCPUBITS
 }
diff --git a/vendor/golang.org/x/sys/unix/fdset.go b/vendor/golang.org/x/sys/unix/fdset.go
index 9e83d18cd..62ed12645 100644
--- a/vendor/golang.org/x/sys/unix/fdset.go
+++ b/vendor/golang.org/x/sys/unix/fdset.go
@@ -23,7 +23,5 @@ func (fds *FdSet) IsSet(fd int) bool {
 
 // Zero clears the set fds.
 func (fds *FdSet) Zero() {
-	for i := range fds.Bits {
-		fds.Bits[i] = 0
-	}
+	clear(fds.Bits[:])
 }
diff --git a/vendor/golang.org/x/sys/unix/ifreq_linux.go b/vendor/golang.org/x/sys/unix/ifreq_linux.go
index 848840ae4..309f5a2b0 100644
--- a/vendor/golang.org/x/sys/unix/ifreq_linux.go
+++ b/vendor/golang.org/x/sys/unix/ifreq_linux.go
@@ -111,9 +111,7 @@ func (ifr *Ifreq) SetUint32(v uint32) {
 // clear zeroes the ifreq's union field to prevent trailing garbage data from
 // being sent to the kernel if an ifreq is reused.
 func (ifr *Ifreq) clear() {
-	for i := range ifr.raw.Ifru {
-		ifr.raw.Ifru[i] = 0
-	}
+	clear(ifr.raw.Ifru[:])
 }
 
 // TODO(mdlayher): export as IfreqData? For now we can provide helpers such as
diff --git a/vendor/golang.org/x/sys/unix/mkall.sh b/vendor/golang.org/x/sys/unix/mkall.sh
index e6f31d374..d0ed61191 100644
--- a/vendor/golang.org/x/sys/unix/mkall.sh
+++ b/vendor/golang.org/x/sys/unix/mkall.sh
@@ -49,6 +49,7 @@ esac
 if [[ "$GOOS" = "linux" ]]; then
 	# Use the Docker-based build system
 	# Files generated through docker (use $cmd so you can Ctl-C the build or run)
+	set -e
 	$cmd docker build --tag generate:$GOOS $GOOS
 	$cmd docker run --interactive --tty --volume $(cd -- "$(dirname -- "$0")/.." && pwd):/build generate:$GOOS
 	exit
diff --git a/vendor/golang.org/x/sys/unix/syscall_linux.go b/vendor/golang.org/x/sys/unix/syscall_linux.go
index 4958a6570..9439af961 100644
--- a/vendor/golang.org/x/sys/unix/syscall_linux.go
+++ b/vendor/golang.org/x/sys/unix/syscall_linux.go
@@ -801,9 +801,7 @@ func (sa *SockaddrPPPoE) sockaddr() (unsafe.Pointer, _Socklen, error) {
 	// one. The kernel expects SID to be in network byte order.
 	binary.BigEndian.PutUint16(sa.raw[6:8], sa.SID)
 	copy(sa.raw[8:14], sa.Remote)
-	for i := 14; i < 14+IFNAMSIZ; i++ {
-		sa.raw[i] = 0
-	}
+	clear(sa.raw[14 : 14+IFNAMSIZ])
 	copy(sa.raw[14:], sa.Dev)
 	return unsafe.Pointer(&sa.raw), SizeofSockaddrPPPoX, nil
 }
diff --git a/vendor/golang.org/x/sys/unix/syscall_netbsd.go b/vendor/golang.org/x/sys/unix/syscall_netbsd.go
index 88162099a..34a467697 100644
--- a/vendor/golang.org/x/sys/unix/syscall_netbsd.go
+++ b/vendor/golang.org/x/sys/unix/syscall_netbsd.go
@@ -248,6 +248,23 @@ func Statvfs(path string, buf *Statvfs_t) (err error) {
 	return Statvfs1(path, buf, ST_WAIT)
 }
 
+func Getvfsstat(buf []Statvfs_t, flags int) (n int, err error) {
+	var (
+		_p0     unsafe.Pointer
+		bufsize uintptr
+	)
+	if len(buf) > 0 {
+		_p0 = unsafe.Pointer(&buf[0])
+		bufsize = unsafe.Sizeof(Statvfs_t{}) * uintptr(len(buf))
+	}
+	r0, _, e1 := Syscall(SYS_GETVFSSTAT, uintptr(_p0), bufsize, uintptr(flags))
+	n = int(r0)
+	if e1 != 0 {
+		err = e1
+	}
+	return
+}
+
 /*
  * Exposed directly
  */
diff --git a/vendor/golang.org/x/sys/windows/syscall_windows.go b/vendor/golang.org/x/sys/windows/syscall_windows.go
index 640f6b153..bd5133730 100644
--- a/vendor/golang.org/x/sys/windows/syscall_windows.go
+++ b/vendor/golang.org/x/sys/windows/syscall_windows.go
@@ -321,6 +321,8 @@ func NewCallbackCDecl(fn interface{}) uintptr {
 //sys	SetConsoleOutputCP(cp uint32) (err error) = kernel32.SetConsoleOutputCP
 //sys	WriteConsole(console Handle, buf *uint16, towrite uint32, written *uint32, reserved *byte) (err error) = kernel32.WriteConsoleW
 //sys	ReadConsole(console Handle, buf *uint16, toread uint32, read *uint32, inputControl *byte) (err error) = kernel32.ReadConsoleW
+//sys	GetNumberOfConsoleInputEvents(console Handle, numevents *uint32) (err error) = kernel32.GetNumberOfConsoleInputEvents
+//sys	FlushConsoleInputBuffer(console Handle) (err error) = kernel32.FlushConsoleInputBuffer
 //sys	resizePseudoConsole(pconsole Handle, size uint32) (hr error) = kernel32.ResizePseudoConsole
 //sys	CreateToolhelp32Snapshot(flags uint32, processId uint32) (handle Handle, err error) [failretval==InvalidHandle] = kernel32.CreateToolhelp32Snapshot
 //sys	Module32First(snapshot Handle, moduleEntry *ModuleEntry32) (err error) = kernel32.Module32FirstW
diff --git a/vendor/golang.org/x/sys/windows/types_windows.go b/vendor/golang.org/x/sys/windows/types_windows.go
index 993a2297d..358be3c7f 100644
--- a/vendor/golang.org/x/sys/windows/types_windows.go
+++ b/vendor/golang.org/x/sys/windows/types_windows.go
@@ -65,6 +65,22 @@ var signals = [...]string{
 	15: "terminated",
 }
 
+// File flags for [os.OpenFile]. The O_ prefix is used to indicate
+// that these flags are specific to the OpenFile function.
+const (
+	O_FILE_FLAG_OPEN_NO_RECALL     = FILE_FLAG_OPEN_NO_RECALL
+	O_FILE_FLAG_OPEN_REPARSE_POINT = FILE_FLAG_OPEN_REPARSE_POINT
+	O_FILE_FLAG_SESSION_AWARE      = FILE_FLAG_SESSION_AWARE
+	O_FILE_FLAG_POSIX_SEMANTICS    = FILE_FLAG_POSIX_SEMANTICS
+	O_FILE_FLAG_BACKUP_SEMANTICS   = FILE_FLAG_BACKUP_SEMANTICS
+	O_FILE_FLAG_DELETE_ON_CLOSE    = FILE_FLAG_DELETE_ON_CLOSE
+	O_FILE_FLAG_SEQUENTIAL_SCAN    = FILE_FLAG_SEQUENTIAL_SCAN
+	O_FILE_FLAG_RANDOM_ACCESS      = FILE_FLAG_RANDOM_ACCESS
+	O_FILE_FLAG_NO_BUFFERING       = FILE_FLAG_NO_BUFFERING
+	O_FILE_FLAG_OVERLAPPED         = FILE_FLAG_OVERLAPPED
+	O_FILE_FLAG_WRITE_THROUGH      = FILE_FLAG_WRITE_THROUGH
+)
+
 const (
 	FILE_READ_DATA        = 0x00000001
 	FILE_READ_ATTRIBUTES  = 0x00000080
diff --git a/vendor/golang.org/x/sys/windows/zsyscall_windows.go b/vendor/golang.org/x/sys/windows/zsyscall_windows.go
index 641a5f4b7..426151a01 100644
--- a/vendor/golang.org/x/sys/windows/zsyscall_windows.go
+++ b/vendor/golang.org/x/sys/windows/zsyscall_windows.go
@@ -238,6 +238,7 @@ var (
 	procFindResourceW                                        = modkernel32.NewProc("FindResourceW")
 	procFindVolumeClose                                      = modkernel32.NewProc("FindVolumeClose")
 	procFindVolumeMountPointClose                            = modkernel32.NewProc("FindVolumeMountPointClose")
+	procFlushConsoleInputBuffer                              = modkernel32.NewProc("FlushConsoleInputBuffer")
 	procFlushFileBuffers                                     = modkernel32.NewProc("FlushFileBuffers")
 	procFlushViewOfFile                                      = modkernel32.NewProc("FlushViewOfFile")
 	procFormatMessageW                                       = modkernel32.NewProc("FormatMessageW")
@@ -284,6 +285,7 @@ var (
 	procGetNamedPipeHandleStateW                             = modkernel32.NewProc("GetNamedPipeHandleStateW")
 	procGetNamedPipeInfo                                     = modkernel32.NewProc("GetNamedPipeInfo")
 	procGetNamedPipeServerProcessId                          = modkernel32.NewProc("GetNamedPipeServerProcessId")
+	procGetNumberOfConsoleInputEvents                        = modkernel32.NewProc("GetNumberOfConsoleInputEvents")
 	procGetOverlappedResult                                  = modkernel32.NewProc("GetOverlappedResult")
 	procGetPriorityClass                                     = modkernel32.NewProc("GetPriorityClass")
 	procGetProcAddress                                       = modkernel32.NewProc("GetProcAddress")
@@ -2111,6 +2113,14 @@ func FindVolumeMountPointClose(findVolumeMountPoint Handle) (err error) {
 	return
 }
 
+func FlushConsoleInputBuffer(console Handle) (err error) {
+	r1, _, e1 := syscall.SyscallN(procFlushConsoleInputBuffer.Addr(), uintptr(console))
+	if r1 == 0 {
+		err = errnoErr(e1)
+	}
+	return
+}
+
 func FlushFileBuffers(handle Handle) (err error) {
 	r1, _, e1 := syscall.SyscallN(procFlushFileBuffers.Addr(), uintptr(handle))
 	if r1 == 0 {
@@ -2481,6 +2491,14 @@ func GetNamedPipeServerProcessId(pipe Handle, serverProcessID *uint32) (err erro
 	return
 }
 
+func GetNumberOfConsoleInputEvents(console Handle, numevents *uint32) (err error) {
+	r1, _, e1 := syscall.SyscallN(procGetNumberOfConsoleInputEvents.Addr(), uintptr(console), uintptr(unsafe.Pointer(numevents)))
+	if r1 == 0 {
+		err = errnoErr(e1)
+	}
+	return
+}
+
 func GetOverlappedResult(handle Handle, overlapped *Overlapped, done *uint32, wait bool) (err error) {
 	var _p0 uint32
 	if wait {
diff --git a/vendor/golang.org/x/time/rate/rate.go b/vendor/golang.org/x/time/rate/rate.go
index 794b2e32b..563270c15 100644
--- a/vendor/golang.org/x/time/rate/rate.go
+++ b/vendor/golang.org/x/time/rate/rate.go
@@ -195,7 +195,7 @@ func (r *Reservation) CancelAt(t time.Time) {
 	// update state
 	r.lim.last = t
 	r.lim.tokens = tokens
-	if r.timeToAct == r.lim.lastEvent {
+	if r.timeToAct.Equal(r.lim.lastEvent) {
 		prevEvent := r.timeToAct.Add(r.limit.durationFromTokens(float64(-r.tokens)))
 		if !prevEvent.Before(t) {
 			r.lim.lastEvent = prevEvent
diff --git a/vendor/golang.org/x/tools/go/analysis/checker/checker.go b/vendor/golang.org/x/tools/go/analysis/checker/checker.go
index f0d28ffe8..455be3fda 100644
--- a/vendor/golang.org/x/tools/go/analysis/checker/checker.go
+++ b/vendor/golang.org/x/tools/go/analysis/checker/checker.go
@@ -128,7 +128,6 @@ type Action struct {
 	pass         *analysis.Pass
 	objectFacts  map[objectFactKey]analysis.Fact
 	packageFacts map[packageFactKey]analysis.Fact
-	inputs       map[*analysis.Analyzer]any
 }
 
 func (act *Action) String() string {
@@ -295,7 +294,6 @@ func (act *Action) execOnce() {
 			// in-memory outputs of prerequisite analyzers
 			// become inputs to this analysis pass.
 			inputs[dep.Analyzer] = dep.Result
-
 		} else if dep.Analyzer == act.Analyzer { // (always true)
 			// Same analysis, different package (vertical edge):
 			// serialized facts produced by prerequisite analysis
diff --git a/vendor/golang.org/x/tools/go/analysis/unitchecker/unitchecker.go b/vendor/golang.org/x/tools/go/analysis/unitchecker/unitchecker.go
index 550391624..7b805b882 100644
--- a/vendor/golang.org/x/tools/go/analysis/unitchecker/unitchecker.go
+++ b/vendor/golang.org/x/tools/go/analysis/unitchecker/unitchecker.go
@@ -73,7 +73,9 @@ type Config struct {
 	PackageVetx               map[string]string // maps package path to file of fact information
 	VetxOnly                  bool              // run analysis only for facts, not diagnostics
 	VetxOutput                string            // where to write file of fact information
-	SucceedOnTypecheckFailure bool
+	Stdout                    string            // write stdout (e.g. JSON, unified diff) to this file
+	SucceedOnTypecheckFailure bool              // obsolete awful hack; see #18395 and below
+	WarnDiagnostics           bool              // printing diagnostics should not cause a non-zero exit
 }
 
 // Main is the main function of a vet-like analysis tool that must be
@@ -142,6 +144,15 @@ func Run(configFile string, analyzers []*analysis.Analyzer) {
 		log.Fatal(err)
 	}
 
+	// Redirect stdout to a file as requested.
+	if cfg.Stdout != "" {
+		f, err := os.Create(cfg.Stdout)
+		if err != nil {
+			log.Fatal(err)
+		}
+		os.Stdout = f
+	}
+
 	fset := token.NewFileSet()
 	results, err := run(fset, cfg, analyzers)
 	if err != nil {
@@ -152,7 +163,7 @@ func Run(configFile string, analyzers []*analysis.Analyzer) {
 
 	// In VetxOnly mode, the analysis is run only for facts.
 	if !cfg.VetxOnly {
-		code = processResults(fset, cfg.ID, results)
+		code = processResults(fset, cfg.ID, results, cfg.WarnDiagnostics)
 	}
 
 	os.Exit(code)
@@ -176,7 +187,7 @@ func readConfig(filename string) (*Config, error) {
 	return cfg, nil
 }
 
-func processResults(fset *token.FileSet, id string, results []result) (exit int) {
+func processResults(fset *token.FileSet, id string, results []result, warnDiagnostics bool) (exit int) {
 	if analysisflags.Fix {
 		// Don't print the diagnostics,
 		// but apply all fixes from the root actions.
@@ -225,7 +236,9 @@ func processResults(fset *token.FileSet, id string, results []result) (exit int)
 		for _, res := range results {
 			for _, diag := range res.diagnostics {
 				analysisflags.PrintPlain(os.Stderr, fset, analysisflags.Context, diag)
-				exit = 1
+				if !warnDiagnostics {
+					exit = 1
+				}
 			}
 		}
 	}
diff --git a/vendor/golang.org/x/tools/go/packages/golist.go b/vendor/golang.org/x/tools/go/packages/golist.go
index 89f89dd2d..680a70ca8 100644
--- a/vendor/golang.org/x/tools/go/packages/golist.go
+++ b/vendor/golang.org/x/tools/go/packages/golist.go
@@ -364,12 +364,6 @@ type jsonPackage struct {
 	DepsErrors []*packagesinternal.PackageError
 }
 
-type jsonPackageError struct {
-	ImportStack []string
-	Pos         string
-	Err         string
-}
-
 func otherFiles(p *jsonPackage) [][]string {
 	return [][]string{p.CFiles, p.CXXFiles, p.MFiles, p.HFiles, p.FFiles, p.SFiles, p.SwigFiles, p.SwigCXXFiles, p.SysoFiles}
 }
diff --git a/vendor/golang.org/x/tools/internal/analysisinternal/analysis.go b/vendor/golang.org/x/tools/internal/analysisinternal/analysis.go
index e48dc3f33..2b4a8ebb6 100644
--- a/vendor/golang.org/x/tools/internal/analysisinternal/analysis.go
+++ b/vendor/golang.org/x/tools/internal/analysisinternal/analysis.go
@@ -7,96 +7,23 @@
 package analysisinternal
 
 import (
-	"bytes"
 	"cmp"
 	"fmt"
 	"go/ast"
-	"go/printer"
-	"go/scanner"
 	"go/token"
 	"go/types"
-	"iter"
-	pathpkg "path"
 	"slices"
 	"strings"
 
 	"golang.org/x/tools/go/analysis"
-	"golang.org/x/tools/go/ast/inspector"
-	"golang.org/x/tools/internal/moreiters"
-	"golang.org/x/tools/internal/typesinternal"
 )
 
-// Deprecated: this heuristic is ill-defined.
-// TODO(adonovan): move to sole use in gopls/internal/cache.
-func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos {
-	// Get the end position for the type error.
-	file := fset.File(start)
-	if file == nil {
-		return start
-	}
-	if offset := file.PositionFor(start, false).Offset; offset > len(src) {
-		return start
-	} else {
-		src = src[offset:]
-	}
-
-	// Attempt to find a reasonable end position for the type error.
-	//
-	// TODO(rfindley): the heuristic implemented here is unclear. It looks like
-	// it seeks the end of the primary operand starting at start, but that is not
-	// quite implemented (for example, given a func literal this heuristic will
-	// return the range of the func keyword).
-	//
-	// We should formalize this heuristic, or deprecate it by finally proposing
-	// to add end position to all type checker errors.
-	//
-	// Nevertheless, ensure that the end position at least spans the current
-	// token at the cursor (this was golang/go#69505).
-	end := start
-	{
-		var s scanner.Scanner
-		fset := token.NewFileSet()
-		f := fset.AddFile("", fset.Base(), len(src))
-		s.Init(f, src, nil /* no error handler */, scanner.ScanComments)
-		pos, tok, lit := s.Scan()
-		if tok != token.SEMICOLON && token.Pos(f.Base()) <= pos && pos <= token.Pos(f.Base()+f.Size()) {
-			off := file.Offset(pos) + len(lit)
-			src = src[off:]
-			end += token.Pos(off)
-		}
-	}
-
-	// Look for bytes that might terminate the current operand. See note above:
-	// this is imprecise.
-	if width := bytes.IndexAny(src, " \n,():;[]+-*/"); width > 0 {
-		end += token.Pos(width)
-	}
-	return end
-}
-
-// WalkASTWithParent walks the AST rooted at n. The semantics are
-// similar to ast.Inspect except it does not call f(nil).
-func WalkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) {
-	var ancestors []ast.Node
-	ast.Inspect(n, func(n ast.Node) (recurse bool) {
-		if n == nil {
-			ancestors = ancestors[:len(ancestors)-1]
-			return false
-		}
-
-		var parent ast.Node
-		if len(ancestors) > 0 {
-			parent = ancestors[len(ancestors)-1]
-		}
-		ancestors = append(ancestors, n)
-		return f(n, parent)
-	})
-}
-
 // MatchingIdents finds the names of all identifiers in 'node' that match any of the given types.
 // 'pos' represents the position at which the identifiers may be inserted. 'pos' must be within
 // the scope of each of identifier we select. Otherwise, we will insert a variable at 'pos' that
 // is unrecognized.
+//
+// TODO(adonovan): this is only used by gopls/internal/analysis/fill{returns,struct}. Move closer.
 func MatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *types.Info, pkg *types.Package) map[types.Type][]string {
 
 	// Initialize matches to contain the variable types we are searching for.
@@ -212,185 +139,6 @@ func CheckReadable(pass *analysis.Pass, filename string) error {
 	return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename)
 }
 
-// AddImport checks whether this file already imports pkgpath and
-// that import is in scope at pos. If so, it returns the name under
-// which it was imported and a zero edit. Otherwise, it adds a new
-// import of pkgpath, using a name derived from the preferred name,
-// and returns the chosen name, a prefix to be concatenated with member
-// to form a qualified name, and the edit for the new import.
-//
-// In the special case that pkgpath is dot-imported then member, the
-// identifier for which the import is being added, is consulted. If
-// member is not shadowed at pos, AddImport returns (".", "", nil).
-// (AddImport accepts the caller's implicit claim that the imported
-// package declares member.)
-//
-// It does not mutate its arguments.
-func AddImport(info *types.Info, file *ast.File, preferredName, pkgpath, member string, pos token.Pos) (name, prefix string, newImport []analysis.TextEdit) {
-	// Find innermost enclosing lexical block.
-	scope := info.Scopes[file].Innermost(pos)
-	if scope == nil {
-		panic("no enclosing lexical block")
-	}
-
-	// Is there an existing import of this package?
-	// If so, are we in its scope? (not shadowed)
-	for _, spec := range file.Imports {
-		pkgname := info.PkgNameOf(spec)
-		if pkgname != nil && pkgname.Imported().Path() == pkgpath {
-			name = pkgname.Name()
-			if name == "." {
-				// The scope of ident must be the file scope.
-				if s, _ := scope.LookupParent(member, pos); s == info.Scopes[file] {
-					return name, "", nil
-				}
-			} else if _, obj := scope.LookupParent(name, pos); obj == pkgname {
-				return name, name + ".", nil
-			}
-		}
-	}
-
-	// We must add a new import.
-	// Ensure we have a fresh name.
-	newName := FreshName(scope, pos, preferredName)
-
-	// Create a new import declaration either before the first existing
-	// declaration (which must exist), including its comments; or
-	// inside the declaration, if it is an import group.
-	//
-	// Use a renaming import whenever the preferred name is not
-	// available, or the chosen name does not match the last
-	// segment of its path.
-	newText := fmt.Sprintf("%q", pkgpath)
-	if newName != preferredName || newName != pathpkg.Base(pkgpath) {
-		newText = fmt.Sprintf("%s %q", newName, pkgpath)
-	}
-	decl0 := file.Decls[0]
-	var before ast.Node = decl0
-	switch decl0 := decl0.(type) {
-	case *ast.GenDecl:
-		if decl0.Doc != nil {
-			before = decl0.Doc
-		}
-	case *ast.FuncDecl:
-		if decl0.Doc != nil {
-			before = decl0.Doc
-		}
-	}
-	// If the first decl is an import group, add this new import at the end.
-	if gd, ok := before.(*ast.GenDecl); ok && gd.Tok == token.IMPORT && gd.Rparen.IsValid() {
-		pos = gd.Rparen
-		// if it's a std lib, we should append it at the beginning of import group.
-		// otherwise we may see the std package is put at the last behind a 3rd module which doesn't follow our convention.
-		// besides, gofmt doesn't help in this case.
-		if IsStdPackage(pkgpath) && len(gd.Specs) != 0 {
-			pos = gd.Specs[0].Pos()
-			newText += "\n\t"
-		} else {
-			newText = "\t" + newText + "\n"
-		}
-	} else {
-		pos = before.Pos()
-		newText = "import " + newText + "\n\n"
-	}
-	return newName, newName + ".", []analysis.TextEdit{{
-		Pos:     pos,
-		End:     pos,
-		NewText: []byte(newText),
-	}}
-}
-
-// FreshName returns the name of an identifier that is undefined
-// at the specified position, based on the preferred name.
-func FreshName(scope *types.Scope, pos token.Pos, preferred string) string {
-	newName := preferred
-	for i := 0; ; i++ {
-		if _, obj := scope.LookupParent(newName, pos); obj == nil {
-			break // fresh
-		}
-		newName = fmt.Sprintf("%s%d", preferred, i)
-	}
-	return newName
-}
-
-// Format returns a string representation of the node n.
-func Format(fset *token.FileSet, n ast.Node) string {
-	var buf strings.Builder
-	printer.Fprint(&buf, fset, n) // ignore errors
-	return buf.String()
-}
-
-// Imports returns true if path is imported by pkg.
-func Imports(pkg *types.Package, path string) bool {
-	for _, imp := range pkg.Imports() {
-		if imp.Path() == path {
-			return true
-		}
-	}
-	return false
-}
-
-// IsTypeNamed reports whether t is (or is an alias for) a
-// package-level defined type with the given package path and one of
-// the given names. It returns false if t is nil.
-//
-// This function avoids allocating the concatenation of "pkg.Name",
-// which is important for the performance of syntax matching.
-func IsTypeNamed(t types.Type, pkgPath string, names ...string) bool {
-	if named, ok := types.Unalias(t).(*types.Named); ok {
-		tname := named.Obj()
-		return tname != nil &&
-			typesinternal.IsPackageLevel(tname) &&
-			tname.Pkg().Path() == pkgPath &&
-			slices.Contains(names, tname.Name())
-	}
-	return false
-}
-
-// IsPointerToNamed reports whether t is (or is an alias for) a pointer to a
-// package-level defined type with the given package path and one of the given
-// names. It returns false if t is not a pointer type.
-func IsPointerToNamed(t types.Type, pkgPath string, names ...string) bool {
-	r := typesinternal.Unpointer(t)
-	if r == t {
-		return false
-	}
-	return IsTypeNamed(r, pkgPath, names...)
-}
-
-// IsFunctionNamed reports whether obj is a package-level function
-// defined in the given package and has one of the given names.
-// It returns false if obj is nil.
-//
-// This function avoids allocating the concatenation of "pkg.Name",
-// which is important for the performance of syntax matching.
-func IsFunctionNamed(obj types.Object, pkgPath string, names ...string) bool {
-	f, ok := obj.(*types.Func)
-	return ok &&
-		typesinternal.IsPackageLevel(obj) &&
-		f.Pkg().Path() == pkgPath &&
-		f.Type().(*types.Signature).Recv() == nil &&
-		slices.Contains(names, f.Name())
-}
-
-// IsMethodNamed reports whether obj is a method defined on a
-// package-level type with the given package and type name, and has
-// one of the given names. It returns false if obj is nil.
-//
-// This function avoids allocating the concatenation of "pkg.TypeName.Name",
-// which is important for the performance of syntax matching.
-func IsMethodNamed(obj types.Object, pkgPath string, typeName string, names ...string) bool {
-	if fn, ok := obj.(*types.Func); ok {
-		if recv := fn.Type().(*types.Signature).Recv(); recv != nil {
-			_, T := typesinternal.ReceiverNamed(recv)
-			return T != nil &&
-				IsTypeNamed(T, pkgPath, typeName) &&
-				slices.Contains(names, fn.Name())
-		}
-	}
-	return false
-}
-
 // ValidateFixes validates the set of fixes for a single diagnostic.
 // Any error indicates a bug in the originating analyzer.
 //
@@ -493,6 +241,20 @@ func validateFix(fset *token.FileSet, fix *analysis.SuggestedFix) error {
 	return nil
 }
 
+// Range returns an [analysis.Range] for the specified start and end positions.
+func Range(pos, end token.Pos) analysis.Range {
+	return tokenRange{pos, end}
+}
+
+// tokenRange is an implementation of the [analysis.Range] interface.
+type tokenRange struct{ StartPos, EndPos token.Pos }
+
+func (r tokenRange) Pos() token.Pos { return r.StartPos }
+func (r tokenRange) End() token.Pos { return r.EndPos }
+
+// TODO(adonovan): the import-related functions below don't depend on
+// analysis (or even on go/types or go/ast). Move somewhere more logical.
+
 // CanImport reports whether one package is allowed to import another.
 //
 // TODO(adonovan): allow customization of the accessibility relation
@@ -520,133 +282,6 @@ func CanImport(from, to string) bool {
 	return true
 }
 
-// DeleteStmt returns the edits to remove the [ast.Stmt] identified by
-// curStmt, if it is contained within a BlockStmt, CaseClause,
-// CommClause, or is the STMT in switch STMT; ... {...}. It returns nil otherwise.
-func DeleteStmt(fset *token.FileSet, curStmt inspector.Cursor) []analysis.TextEdit {
-	stmt := curStmt.Node().(ast.Stmt)
-	// if the stmt is on a line by itself delete the whole line
-	// otherwise just delete the statement.
-
-	// this logic would be a lot simpler with the file contents, and somewhat simpler
-	// if the cursors included the comments.
-
-	tokFile := fset.File(stmt.Pos())
-	lineOf := tokFile.Line
-	stmtStartLine, stmtEndLine := lineOf(stmt.Pos()), lineOf(stmt.End())
-
-	var from, to token.Pos
-	// bounds of adjacent syntax/comments on same line, if any
-	limits := func(left, right token.Pos) {
-		if lineOf(left) == stmtStartLine {
-			from = left
-		}
-		if lineOf(right) == stmtEndLine {
-			to = right
-		}
-	}
-	// TODO(pjw): there are other places a statement might be removed:
-	// IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
-	// (removing the blocks requires more rewriting than this routine would do)
-	// CommCase   = "case" ( SendStmt | RecvStmt ) | "default" .
-	// (removing the stmt requires more rewriting, and it's unclear what the user means)
-	switch parent := curStmt.Parent().Node().(type) {
-	case *ast.SwitchStmt:
-		limits(parent.Switch, parent.Body.Lbrace)
-	case *ast.TypeSwitchStmt:
-		limits(parent.Switch, parent.Body.Lbrace)
-		if parent.Assign == stmt {
-			return nil // don't let the user break the type switch
-		}
-	case *ast.BlockStmt:
-		limits(parent.Lbrace, parent.Rbrace)
-	case *ast.CommClause:
-		limits(parent.Colon, curStmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
-		if parent.Comm == stmt {
-			return nil // maybe the user meant to remove the entire CommClause?
-		}
-	case *ast.CaseClause:
-		limits(parent.Colon, curStmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
-	case *ast.ForStmt:
-		limits(parent.For, parent.Body.Lbrace)
-
-	default:
-		return nil // not one of ours
-	}
-
-	if prev, found := curStmt.PrevSibling(); found && lineOf(prev.Node().End()) == stmtStartLine {
-		from = prev.Node().End() // preceding statement ends on same line
-	}
-	if next, found := curStmt.NextSibling(); found && lineOf(next.Node().Pos()) == stmtEndLine {
-		to = next.Node().Pos() // following statement begins on same line
-	}
-	// and now for the comments
-Outer:
-	for _, cg := range enclosingFile(curStmt).Comments {
-		for _, co := range cg.List {
-			if lineOf(co.End()) < stmtStartLine {
-				continue
-			} else if lineOf(co.Pos()) > stmtEndLine {
-				break Outer // no more are possible
-			}
-			if lineOf(co.End()) == stmtStartLine && co.End() < stmt.Pos() {
-				if !from.IsValid() || co.End() > from {
-					from = co.End()
-					continue // maybe there are more
-				}
-			}
-			if lineOf(co.Pos()) == stmtEndLine && co.Pos() > stmt.End() {
-				if !to.IsValid() || co.Pos() < to {
-					to = co.Pos()
-					continue // maybe there are more
-				}
-			}
-		}
-	}
-	// if either from or to is valid, just remove the statement
-	// otherwise remove the line
-	edit := analysis.TextEdit{Pos: stmt.Pos(), End: stmt.End()}
-	if from.IsValid() || to.IsValid() {
-		// remove just the statement.
-		// we can't tell if there is a ; or whitespace right after the statement
-		// ideally we'd like to remove the former and leave the latter
-		// (if gofmt has run, there likely won't be a ;)
-		// In type switches we know there's a semicolon somewhere after the statement,
-		// but the extra work for this special case is not worth it, as gofmt will fix it.
-		return []analysis.TextEdit{edit}
-	}
-	// remove the whole line
-	for lineOf(edit.Pos) == stmtStartLine {
-		edit.Pos--
-	}
-	edit.Pos++ // get back tostmtStartLine
-	for lineOf(edit.End) == stmtEndLine {
-		edit.End++
-	}
-	return []analysis.TextEdit{edit}
-}
-
-// Comments returns an iterator over the comments overlapping the specified interval.
-func Comments(file *ast.File, start, end token.Pos) iter.Seq[*ast.Comment] {
-	// TODO(adonovan): optimize use binary O(log n) instead of linear O(n) search.
-	return func(yield func(*ast.Comment) bool) {
-		for _, cg := range file.Comments {
-			for _, co := range cg.List {
-				if co.Pos() > end {
-					return
-				}
-				if co.End() < start {
-					continue
-				}
-
-				if !yield(co) {
-					return
-				}
-			}
-		}
-	}
-}
-
 // IsStdPackage reports whether the specified package path belongs to a
 // package in the standard library (including internal dependencies).
 func IsStdPackage(path string) bool {
@@ -658,20 +293,3 @@ func IsStdPackage(path string) bool {
 	}
 	return !strings.Contains(path[:slash], ".") && path != "testdata"
 }
-
-// Range returns an [analysis.Range] for the specified start and end positions.
-func Range(pos, end token.Pos) analysis.Range {
-	return tokenRange{pos, end}
-}
-
-// tokenRange is an implementation of the [analysis.Range] interface.
-type tokenRange struct{ StartPos, EndPos token.Pos }
-
-func (r tokenRange) Pos() token.Pos { return r.StartPos }
-func (r tokenRange) End() token.Pos { return r.EndPos }
-
-// enclosingFile returns the syntax tree for the file enclosing c.
-func enclosingFile(c inspector.Cursor) *ast.File {
-	c, _ = moreiters.First(c.Enclosing((*ast.File)(nil)))
-	return c.Node().(*ast.File)
-}
diff --git a/vendor/golang.org/x/tools/internal/analysisinternal/extractdoc.go b/vendor/golang.org/x/tools/internal/analysisinternal/extractdoc.go
index 39507723d..bfb5900f1 100644
--- a/vendor/golang.org/x/tools/internal/analysisinternal/extractdoc.go
+++ b/vendor/golang.org/x/tools/internal/analysisinternal/extractdoc.go
@@ -97,7 +97,7 @@ func ExtractDoc(content, name string) (string, error) {
 	if f.Doc == nil {
 		return "", fmt.Errorf("Go source file has no package doc comment")
 	}
-	for _, section := range strings.Split(f.Doc.Text(), "\n# ") {
+	for section := range strings.SplitSeq(f.Doc.Text(), "\n# ") {
 		if body := strings.TrimPrefix(section, "Analyzer "+name); body != section &&
 			body != "" &&
 			body[0] == '\r' || body[0] == '\n' {
diff --git a/vendor/golang.org/x/tools/internal/diff/lcs/old.go b/vendor/golang.org/x/tools/internal/diff/lcs/old.go
index 4c346706a..7b7c5cc67 100644
--- a/vendor/golang.org/x/tools/internal/diff/lcs/old.go
+++ b/vendor/golang.org/x/tools/internal/diff/lcs/old.go
@@ -378,10 +378,7 @@ func (e *editGraph) twoDone(df, db int) (int, bool) {
 		return 0, false // diagonals cannot overlap
 	}
 	kmin := max(-df, -db+e.delta)
-	kmax := db + e.delta
-	if df < kmax {
-		kmax = df
-	}
+	kmax := min(df, db+e.delta)
 	for k := kmin; k <= kmax; k += 2 {
 		x := e.vf.get(df, k)
 		u := e.vb.get(db, k-e.delta)
diff --git a/vendor/golang.org/x/tools/internal/diff/lcs/sequence.go b/vendor/golang.org/x/tools/internal/diff/lcs/sequence.go
index 2d72d2630..811bb216e 100644
--- a/vendor/golang.org/x/tools/internal/diff/lcs/sequence.go
+++ b/vendor/golang.org/x/tools/internal/diff/lcs/sequence.go
@@ -103,11 +103,3 @@ func commonSuffixLenString(a, b string) int {
 	}
 	return i
 }
-
-func min(x, y int) int {
-	if x < y {
-		return x
-	} else {
-		return y
-	}
-}
diff --git a/vendor/golang.org/x/tools/internal/event/core/event.go b/vendor/golang.org/x/tools/internal/event/core/event.go
index a6cf0e64a..ade5d1e79 100644
--- a/vendor/golang.org/x/tools/internal/event/core/event.go
+++ b/vendor/golang.org/x/tools/internal/event/core/event.go
@@ -28,11 +28,6 @@ type Event struct {
 	dynamic []label.Label  // dynamically sized storage for remaining labels
 }
 
-// eventLabelMap implements label.Map for a the labels of an Event.
-type eventLabelMap struct {
-	event Event
-}
-
 func (ev Event) At() time.Time { return ev.at }
 
 func (ev Event) Format(f fmt.State, r rune) {
diff --git a/vendor/golang.org/x/tools/internal/gcimporter/iexport.go b/vendor/golang.org/x/tools/internal/gcimporter/iexport.go
index 780873e3a..4a4357d2b 100644
--- a/vendor/golang.org/x/tools/internal/gcimporter/iexport.go
+++ b/vendor/golang.org/x/tools/internal/gcimporter/iexport.go
@@ -569,7 +569,6 @@ func (p *iexporter) exportName(obj types.Object) (res string) {
 
 type iexporter struct {
 	fset    *token.FileSet
-	out     *bytes.Buffer
 	version int
 
 	shallow    bool                // don't put types from other packages in the index
diff --git a/vendor/golang.org/x/tools/internal/moreiters/iters.go b/vendor/golang.org/x/tools/internal/moreiters/iters.go
deleted file mode 100644
index 69c76ccb9..000000000
--- a/vendor/golang.org/x/tools/internal/moreiters/iters.go
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright 2025 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package moreiters
-
-import "iter"
-
-// First returns the first value of seq and true.
-// If seq is empty, it returns the zero value of T and false.
-func First[T any](seq iter.Seq[T]) (z T, ok bool) {
-	for t := range seq {
-		return t, true
-	}
-	return z, false
-}
-
-// Contains reports whether x is an element of the sequence seq.
-func Contains[T comparable](seq iter.Seq[T], x T) bool {
-	for cand := range seq {
-		if cand == x {
-			return true
-		}
-	}
-	return false
-}
-
-// Every reports whether every pred(t) for t in seq returns true,
-// stopping at the first false element.
-func Every[T any](seq iter.Seq[T], pred func(T) bool) bool {
-	for t := range seq {
-		if !pred(t) {
-			return false
-		}
-	}
-	return true
-}
-
-// Any reports whether any pred(t) for t in seq returns true.
-func Any[T any](seq iter.Seq[T], pred func(T) bool) bool {
-	for t := range seq {
-		if pred(t) {
-			return true
-		}
-	}
-	return false
-}
diff --git a/vendor/golang.org/x/tools/internal/typesinternal/fx.go b/vendor/golang.org/x/tools/internal/typesinternal/fx.go
new file mode 100644
index 000000000..93acff217
--- /dev/null
+++ b/vendor/golang.org/x/tools/internal/typesinternal/fx.go
@@ -0,0 +1,49 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package typesinternal
+
+import (
+	"go/ast"
+	"go/token"
+	"go/types"
+)
+
+// NoEffects reports whether the expression has no side effects, i.e., it
+// does not modify the memory state. This function is conservative: it may
+// return false even when the expression has no effect.
+func NoEffects(info *types.Info, expr ast.Expr) bool {
+	noEffects := true
+	ast.Inspect(expr, func(n ast.Node) bool {
+		switch v := n.(type) {
+		case nil, *ast.Ident, *ast.BasicLit, *ast.BinaryExpr, *ast.ParenExpr,
+			*ast.SelectorExpr, *ast.IndexExpr, *ast.SliceExpr, *ast.TypeAssertExpr,
+			*ast.StarExpr, *ast.CompositeLit, *ast.ArrayType, *ast.StructType,
+			*ast.MapType, *ast.InterfaceType, *ast.KeyValueExpr:
+			// No effect
+		case *ast.UnaryExpr:
+			// Channel send <-ch has effects
+			if v.Op == token.ARROW {
+				noEffects = false
+			}
+		case *ast.CallExpr:
+			// Type conversion has no effects
+			if !info.Types[v.Fun].IsType() {
+				// TODO(adonovan): Add a case for built-in functions without side
+				// effects (by using callsPureBuiltin from tools/internal/refactor/inline)
+
+				noEffects = false
+			}
+		case *ast.FuncLit:
+			// A FuncLit has no effects, but do not descend into it.
+			return false
+		default:
+			// All other expressions have effects
+			noEffects = false
+		}
+
+		return noEffects
+	})
+	return noEffects
+}
diff --git a/vendor/golang.org/x/tools/internal/typesinternal/isnamed.go b/vendor/golang.org/x/tools/internal/typesinternal/isnamed.go
new file mode 100644
index 000000000..f2affec4f
--- /dev/null
+++ b/vendor/golang.org/x/tools/internal/typesinternal/isnamed.go
@@ -0,0 +1,71 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package typesinternal
+
+import (
+	"go/types"
+	"slices"
+)
+
+// IsTypeNamed reports whether t is (or is an alias for) a
+// package-level defined type with the given package path and one of
+// the given names. It returns false if t is nil.
+//
+// This function avoids allocating the concatenation of "pkg.Name",
+// which is important for the performance of syntax matching.
+func IsTypeNamed(t types.Type, pkgPath string, names ...string) bool {
+	if named, ok := types.Unalias(t).(*types.Named); ok {
+		tname := named.Obj()
+		return tname != nil &&
+			IsPackageLevel(tname) &&
+			tname.Pkg().Path() == pkgPath &&
+			slices.Contains(names, tname.Name())
+	}
+	return false
+}
+
+// IsPointerToNamed reports whether t is (or is an alias for) a pointer to a
+// package-level defined type with the given package path and one of the given
+// names. It returns false if t is not a pointer type.
+func IsPointerToNamed(t types.Type, pkgPath string, names ...string) bool {
+	r := Unpointer(t)
+	if r == t {
+		return false
+	}
+	return IsTypeNamed(r, pkgPath, names...)
+}
+
+// IsFunctionNamed reports whether obj is a package-level function
+// defined in the given package and has one of the given names.
+// It returns false if obj is nil.
+//
+// This function avoids allocating the concatenation of "pkg.Name",
+// which is important for the performance of syntax matching.
+func IsFunctionNamed(obj types.Object, pkgPath string, names ...string) bool {
+	f, ok := obj.(*types.Func)
+	return ok &&
+		IsPackageLevel(obj) &&
+		f.Pkg().Path() == pkgPath &&
+		f.Type().(*types.Signature).Recv() == nil &&
+		slices.Contains(names, f.Name())
+}
+
+// IsMethodNamed reports whether obj is a method defined on a
+// package-level type with the given package and type name, and has
+// one of the given names. It returns false if obj is nil.
+//
+// This function avoids allocating the concatenation of "pkg.TypeName.Name",
+// which is important for the performance of syntax matching.
+func IsMethodNamed(obj types.Object, pkgPath string, typeName string, names ...string) bool {
+	if fn, ok := obj.(*types.Func); ok {
+		if recv := fn.Type().(*types.Signature).Recv(); recv != nil {
+			_, T := ReceiverNamed(recv)
+			return T != nil &&
+				IsTypeNamed(T, pkgPath, typeName) &&
+				slices.Contains(names, fn.Name())
+		}
+	}
+	return false
+}
diff --git a/vendor/golang.org/x/tools/internal/typesinternal/qualifier.go b/vendor/golang.org/x/tools/internal/typesinternal/qualifier.go
index b64f714eb..64f47919f 100644
--- a/vendor/golang.org/x/tools/internal/typesinternal/qualifier.go
+++ b/vendor/golang.org/x/tools/internal/typesinternal/qualifier.go
@@ -15,6 +15,14 @@ import (
 // file.
 // If the same package is imported multiple times, the last appearance is
 // recorded.
+//
+// TODO(adonovan): this function ignores the effect of shadowing. It
+// should accept a [token.Pos] and a [types.Info] and compute only the
+// set of imports that are not shadowed at that point, analogous to
+// [analysisinternal.AddImport]. It could also compute (as a side
+// effect) the set of additional imports required to ensure that there
+// is an accessible import for each necessary package, making it
+// converge even more closely with AddImport.
 func FileQualifier(f *ast.File, pkg *types.Package) types.Qualifier {
 	// Construct mapping of import paths to their defined names.
 	// It is only necessary to look at renaming imports.
diff --git a/vendor/golang.org/x/tools/internal/typesinternal/types.go b/vendor/golang.org/x/tools/internal/typesinternal/types.go
index a5cd7e8db..fef74a785 100644
--- a/vendor/golang.org/x/tools/internal/typesinternal/types.go
+++ b/vendor/golang.org/x/tools/internal/typesinternal/types.go
@@ -2,8 +2,20 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package typesinternal provides access to internal go/types APIs that are not
-// yet exported.
+// Package typesinternal provides helpful operators for dealing with
+// go/types:
+//
+//   - operators for querying typed syntax trees (e.g. [Imports], [IsFunctionNamed]);
+//   - functions for converting types to strings or syntax (e.g. [TypeExpr], FileQualifier]);
+//   - helpers for working with the [go/types] API (e.g. [NewTypesInfo]);
+//   - access to internal go/types APIs that are not yet
+//     exported (e.g. [SetUsesCgo], [ErrorCodeStartEnd], [VarKind]); and
+//   - common algorithms related to types (e.g. [TooNewStdSymbols]).
+//
+// See also:
+//   - [golang.org/x/tools/internal/astutil], for operations on untyped syntax;
+//   - [golang.org/x/tools/internal/analysisinernal], for helpers for analyzers;
+//   - [golang.org/x/tools/internal/refactor], for operators to compute text edits.
 package typesinternal
 
 import (
@@ -13,6 +25,7 @@ import (
 	"reflect"
 	"unsafe"
 
+	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/internal/aliases"
 )
 
@@ -60,6 +73,9 @@ func ErrorCodeStartEnd(err types.Error) (code ErrorCode, start, end token.Pos, o
 // which is often excessive.)
 //
 // If pkg is nil, it is equivalent to [*types.Package.Name].
+//
+// TODO(adonovan): all uses of this with TypeString should be
+// eliminated when https://go.dev/issues/75604 is resolved.
 func NameRelativeTo(pkg *types.Package) types.Qualifier {
 	return func(other *types.Package) string {
 		if pkg != nil && pkg == other {
@@ -153,3 +169,31 @@ func NewTypesInfo() *types.Info {
 		FileVersions: map[*ast.File]string{},
 	}
 }
+
+// EnclosingScope returns the innermost block logically enclosing the cursor.
+func EnclosingScope(info *types.Info, cur inspector.Cursor) *types.Scope {
+	for cur := range cur.Enclosing() {
+		n := cur.Node()
+		// A function's Scope is associated with its FuncType.
+		switch f := n.(type) {
+		case *ast.FuncDecl:
+			n = f.Type
+		case *ast.FuncLit:
+			n = f.Type
+		}
+		if b := info.Scopes[n]; b != nil {
+			return b
+		}
+	}
+	panic("no Scope for *ast.File")
+}
+
+// Imports reports whether path is imported by pkg.
+func Imports(pkg *types.Package, path string) bool {
+	for _, imp := range pkg.Imports() {
+		if imp.Path() == path {
+			return true
+		}
+	}
+	return false
+}
diff --git a/vendor/golang.org/x/tools/internal/typesinternal/zerovalue.go b/vendor/golang.org/x/tools/internal/typesinternal/zerovalue.go
index d272949c1..453bba2ad 100644
--- a/vendor/golang.org/x/tools/internal/typesinternal/zerovalue.go
+++ b/vendor/golang.org/x/tools/internal/typesinternal/zerovalue.go
@@ -204,23 +204,12 @@ func ZeroExpr(t types.Type, qual types.Qualifier) (_ ast.Expr, isValid bool) {
 	}
 }
 
-// IsZeroExpr uses simple syntactic heuristics to report whether expr
-// is a obvious zero value, such as 0, "", nil, or false.
-// It cannot do better without type information.
-func IsZeroExpr(expr ast.Expr) bool {
-	switch e := expr.(type) {
-	case *ast.BasicLit:
-		return e.Value == "0" || e.Value == `""`
-	case *ast.Ident:
-		return e.Name == "nil" || e.Name == "false"
-	default:
-		return false
-	}
-}
-
 // TypeExpr returns syntax for the specified type. References to named types
 // are qualified by an appropriate (optional) qualifier function.
 // It may panic for types such as Tuple or Union.
+//
+// See also https://go.dev/issues/75604, which will provide a robust
+// Type-to-valid-Go-syntax formatter.
 func TypeExpr(t types.Type, qual types.Qualifier) ast.Expr {
 	switch t := t.(type) {
 	case *types.Basic:
diff --git a/vendor/gopkg.in/yaml.v3/LICENSE b/vendor/gopkg.in/yaml.v3/LICENSE
new file mode 100644
index 000000000..2683e4bb1
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/LICENSE
@@ -0,0 +1,50 @@
+
+This project is covered by two different licenses: MIT and Apache.
+
+#### MIT License ####
+
+The following files were ported to Go from C files of libyaml, and thus
+are still covered by their original MIT license, with the additional
+copyright staring in 2011 when the project was ported over:
+
+    apic.go emitterc.go parserc.go readerc.go scannerc.go
+    writerc.go yamlh.go yamlprivateh.go
+
+Copyright (c) 2006-2010 Kirill Simonov
+Copyright (c) 2006-2011 Kirill Simonov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+### Apache License ###
+
+All the remaining project files are covered by the Apache license:
+
+Copyright (c) 2011-2019 Canonical Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/vendor/gopkg.in/yaml.v3/NOTICE b/vendor/gopkg.in/yaml.v3/NOTICE
new file mode 100644
index 000000000..866d74a7a
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/NOTICE
@@ -0,0 +1,13 @@
+Copyright 2011-2016 Canonical Ltd.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/vendor/gopkg.in/yaml.v3/README.md b/vendor/gopkg.in/yaml.v3/README.md
new file mode 100644
index 000000000..08eb1babd
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/README.md
@@ -0,0 +1,150 @@
+# YAML support for the Go language
+
+Introduction
+------------
+
+The yaml package enables Go programs to comfortably encode and decode YAML
+values. It was developed within [Canonical](https://www.canonical.com) as
+part of the [juju](https://juju.ubuntu.com) project, and is based on a
+pure Go port of the well-known [libyaml](http://pyyaml.org/wiki/LibYAML)
+C library to parse and generate YAML data quickly and reliably.
+
+Compatibility
+-------------
+
+The yaml package supports most of YAML 1.2, but preserves some behavior
+from 1.1 for backwards compatibility.
+
+Specifically, as of v3 of the yaml package:
+
+ - YAML 1.1 bools (_yes/no, on/off_) are supported as long as they are being
+   decoded into a typed bool value. Otherwise they behave as a string. Booleans
+   in YAML 1.2 are _true/false_ only.
+ - Octals encode and decode as _0777_ per YAML 1.1, rather than _0o777_
+   as specified in YAML 1.2, because most parsers still use the old format.
+   Octals in the  _0o777_ format are supported though, so new files work.
+ - Does not support base-60 floats. These are gone from YAML 1.2, and were
+   actually never supported by this package as it's clearly a poor choice.
+
+and offers backwards
+compatibility with YAML 1.1 in some cases.
+1.2, including support for
+anchors, tags, map merging, etc. Multi-document unmarshalling is not yet
+implemented, and base-60 floats from YAML 1.1 are purposefully not
+supported since they're a poor design and are gone in YAML 1.2.
+
+Installation and usage
+----------------------
+
+The import path for the package is *gopkg.in/yaml.v3*.
+
+To install it, run:
+
+    go get gopkg.in/yaml.v3
+
+API documentation
+-----------------
+
+If opened in a browser, the import path itself leads to the API documentation:
+
+  - [https://gopkg.in/yaml.v3](https://gopkg.in/yaml.v3)
+
+API stability
+-------------
+
+The package API for yaml v3 will remain stable as described in [gopkg.in](https://gopkg.in).
+
+
+License
+-------
+
+The yaml package is licensed under the MIT and Apache License 2.0 licenses.
+Please see the LICENSE file for details.
+
+
+Example
+-------
+
+```Go
+package main
+
+import (
+        "fmt"
+        "log"
+
+        "gopkg.in/yaml.v3"
+)
+
+var data = `
+a: Easy!
+b:
+  c: 2
+  d: [3, 4]
+`
+
+// Note: struct fields must be public in order for unmarshal to
+// correctly populate the data.
+type T struct {
+        A string
+        B struct {
+                RenamedC int   `yaml:"c"`
+                D        []int `yaml:",flow"`
+        }
+}
+
+func main() {
+        t := T{}
+    
+        err := yaml.Unmarshal([]byte(data), &t)
+        if err != nil {
+                log.Fatalf("error: %v", err)
+        }
+        fmt.Printf("--- t:\n%v\n\n", t)
+    
+        d, err := yaml.Marshal(&t)
+        if err != nil {
+                log.Fatalf("error: %v", err)
+        }
+        fmt.Printf("--- t dump:\n%s\n\n", string(d))
+    
+        m := make(map[interface{}]interface{})
+    
+        err = yaml.Unmarshal([]byte(data), &m)
+        if err != nil {
+                log.Fatalf("error: %v", err)
+        }
+        fmt.Printf("--- m:\n%v\n\n", m)
+    
+        d, err = yaml.Marshal(&m)
+        if err != nil {
+                log.Fatalf("error: %v", err)
+        }
+        fmt.Printf("--- m dump:\n%s\n\n", string(d))
+}
+```
+
+This example will generate the following output:
+
+```
+--- t:
+{Easy! {2 [3 4]}}
+
+--- t dump:
+a: Easy!
+b:
+  c: 2
+  d: [3, 4]
+
+
+--- m:
+map[a:Easy! b:map[c:2 d:[3 4]]]
+
+--- m dump:
+a: Easy!
+b:
+  c: 2
+  d:
+  - 3
+  - 4
+```
+
diff --git a/vendor/gopkg.in/yaml.v3/apic.go b/vendor/gopkg.in/yaml.v3/apic.go
new file mode 100644
index 000000000..ae7d049f1
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/apic.go
@@ -0,0 +1,747 @@
+// 
+// Copyright (c) 2011-2019 Canonical Ltd
+// Copyright (c) 2006-2010 Kirill Simonov
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package yaml
+
+import (
+	"io"
+)
+
+func yaml_insert_token(parser *yaml_parser_t, pos int, token *yaml_token_t) {
+	//fmt.Println("yaml_insert_token", "pos:", pos, "typ:", token.typ, "head:", parser.tokens_head, "len:", len(parser.tokens))
+
+	// Check if we can move the queue at the beginning of the buffer.
+	if parser.tokens_head > 0 && len(parser.tokens) == cap(parser.tokens) {
+		if parser.tokens_head != len(parser.tokens) {
+			copy(parser.tokens, parser.tokens[parser.tokens_head:])
+		}
+		parser.tokens = parser.tokens[:len(parser.tokens)-parser.tokens_head]
+		parser.tokens_head = 0
+	}
+	parser.tokens = append(parser.tokens, *token)
+	if pos < 0 {
+		return
+	}
+	copy(parser.tokens[parser.tokens_head+pos+1:], parser.tokens[parser.tokens_head+pos:])
+	parser.tokens[parser.tokens_head+pos] = *token
+}
+
+// Create a new parser object.
+func yaml_parser_initialize(parser *yaml_parser_t) bool {
+	*parser = yaml_parser_t{
+		raw_buffer: make([]byte, 0, input_raw_buffer_size),
+		buffer:     make([]byte, 0, input_buffer_size),
+	}
+	return true
+}
+
+// Destroy a parser object.
+func yaml_parser_delete(parser *yaml_parser_t) {
+	*parser = yaml_parser_t{}
+}
+
+// String read handler.
+func yaml_string_read_handler(parser *yaml_parser_t, buffer []byte) (n int, err error) {
+	if parser.input_pos == len(parser.input) {
+		return 0, io.EOF
+	}
+	n = copy(buffer, parser.input[parser.input_pos:])
+	parser.input_pos += n
+	return n, nil
+}
+
+// Reader read handler.
+func yaml_reader_read_handler(parser *yaml_parser_t, buffer []byte) (n int, err error) {
+	return parser.input_reader.Read(buffer)
+}
+
+// Set a string input.
+func yaml_parser_set_input_string(parser *yaml_parser_t, input []byte) {
+	if parser.read_handler != nil {
+		panic("must set the input source only once")
+	}
+	parser.read_handler = yaml_string_read_handler
+	parser.input = input
+	parser.input_pos = 0
+}
+
+// Set a file input.
+func yaml_parser_set_input_reader(parser *yaml_parser_t, r io.Reader) {
+	if parser.read_handler != nil {
+		panic("must set the input source only once")
+	}
+	parser.read_handler = yaml_reader_read_handler
+	parser.input_reader = r
+}
+
+// Set the source encoding.
+func yaml_parser_set_encoding(parser *yaml_parser_t, encoding yaml_encoding_t) {
+	if parser.encoding != yaml_ANY_ENCODING {
+		panic("must set the encoding only once")
+	}
+	parser.encoding = encoding
+}
+
+// Create a new emitter object.
+func yaml_emitter_initialize(emitter *yaml_emitter_t) {
+	*emitter = yaml_emitter_t{
+		buffer:     make([]byte, output_buffer_size),
+		raw_buffer: make([]byte, 0, output_raw_buffer_size),
+		states:     make([]yaml_emitter_state_t, 0, initial_stack_size),
+		events:     make([]yaml_event_t, 0, initial_queue_size),
+		best_width: -1,
+	}
+}
+
+// Destroy an emitter object.
+func yaml_emitter_delete(emitter *yaml_emitter_t) {
+	*emitter = yaml_emitter_t{}
+}
+
+// String write handler.
+func yaml_string_write_handler(emitter *yaml_emitter_t, buffer []byte) error {
+	*emitter.output_buffer = append(*emitter.output_buffer, buffer...)
+	return nil
+}
+
+// yaml_writer_write_handler uses emitter.output_writer to write the
+// emitted text.
+func yaml_writer_write_handler(emitter *yaml_emitter_t, buffer []byte) error {
+	_, err := emitter.output_writer.Write(buffer)
+	return err
+}
+
+// Set a string output.
+func yaml_emitter_set_output_string(emitter *yaml_emitter_t, output_buffer *[]byte) {
+	if emitter.write_handler != nil {
+		panic("must set the output target only once")
+	}
+	emitter.write_handler = yaml_string_write_handler
+	emitter.output_buffer = output_buffer
+}
+
+// Set a file output.
+func yaml_emitter_set_output_writer(emitter *yaml_emitter_t, w io.Writer) {
+	if emitter.write_handler != nil {
+		panic("must set the output target only once")
+	}
+	emitter.write_handler = yaml_writer_write_handler
+	emitter.output_writer = w
+}
+
+// Set the output encoding.
+func yaml_emitter_set_encoding(emitter *yaml_emitter_t, encoding yaml_encoding_t) {
+	if emitter.encoding != yaml_ANY_ENCODING {
+		panic("must set the output encoding only once")
+	}
+	emitter.encoding = encoding
+}
+
+// Set the canonical output style.
+func yaml_emitter_set_canonical(emitter *yaml_emitter_t, canonical bool) {
+	emitter.canonical = canonical
+}
+
+// Set the indentation increment.
+func yaml_emitter_set_indent(emitter *yaml_emitter_t, indent int) {
+	if indent < 2 || indent > 9 {
+		indent = 2
+	}
+	emitter.best_indent = indent
+}
+
+// Set the preferred line width.
+func yaml_emitter_set_width(emitter *yaml_emitter_t, width int) {
+	if width < 0 {
+		width = -1
+	}
+	emitter.best_width = width
+}
+
+// Set if unescaped non-ASCII characters are allowed.
+func yaml_emitter_set_unicode(emitter *yaml_emitter_t, unicode bool) {
+	emitter.unicode = unicode
+}
+
+// Set the preferred line break character.
+func yaml_emitter_set_break(emitter *yaml_emitter_t, line_break yaml_break_t) {
+	emitter.line_break = line_break
+}
+
+///*
+// * Destroy a token object.
+// */
+//
+//YAML_DECLARE(void)
+//yaml_token_delete(yaml_token_t *token)
+//{
+//    assert(token);  // Non-NULL token object expected.
+//
+//    switch (token.type)
+//    {
+//        case YAML_TAG_DIRECTIVE_TOKEN:
+//            yaml_free(token.data.tag_directive.handle);
+//            yaml_free(token.data.tag_directive.prefix);
+//            break;
+//
+//        case YAML_ALIAS_TOKEN:
+//            yaml_free(token.data.alias.value);
+//            break;
+//
+//        case YAML_ANCHOR_TOKEN:
+//            yaml_free(token.data.anchor.value);
+//            break;
+//
+//        case YAML_TAG_TOKEN:
+//            yaml_free(token.data.tag.handle);
+//            yaml_free(token.data.tag.suffix);
+//            break;
+//
+//        case YAML_SCALAR_TOKEN:
+//            yaml_free(token.data.scalar.value);
+//            break;
+//
+//        default:
+//            break;
+//    }
+//
+//    memset(token, 0, sizeof(yaml_token_t));
+//}
+//
+///*
+// * Check if a string is a valid UTF-8 sequence.
+// *
+// * Check 'reader.c' for more details on UTF-8 encoding.
+// */
+//
+//static int
+//yaml_check_utf8(yaml_char_t *start, size_t length)
+//{
+//    yaml_char_t *end = start+length;
+//    yaml_char_t *pointer = start;
+//
+//    while (pointer < end) {
+//        unsigned char octet;
+//        unsigned int width;
+//        unsigned int value;
+//        size_t k;
+//
+//        octet = pointer[0];
+//        width = (octet & 0x80) == 0x00 ? 1 :
+//                (octet & 0xE0) == 0xC0 ? 2 :
+//                (octet & 0xF0) == 0xE0 ? 3 :
+//                (octet & 0xF8) == 0xF0 ? 4 : 0;
+//        value = (octet & 0x80) == 0x00 ? octet & 0x7F :
+//                (octet & 0xE0) == 0xC0 ? octet & 0x1F :
+//                (octet & 0xF0) == 0xE0 ? octet & 0x0F :
+//                (octet & 0xF8) == 0xF0 ? octet & 0x07 : 0;
+//        if (!width) return 0;
+//        if (pointer+width > end) return 0;
+//        for (k = 1; k < width; k ++) {
+//            octet = pointer[k];
+//            if ((octet & 0xC0) != 0x80) return 0;
+//            value = (value << 6) + (octet & 0x3F);
+//        }
+//        if (!((width == 1) ||
+//            (width == 2 && value >= 0x80) ||
+//            (width == 3 && value >= 0x800) ||
+//            (width == 4 && value >= 0x10000))) return 0;
+//
+//        pointer += width;
+//    }
+//
+//    return 1;
+//}
+//
+
+// Create STREAM-START.
+func yaml_stream_start_event_initialize(event *yaml_event_t, encoding yaml_encoding_t) {
+	*event = yaml_event_t{
+		typ:      yaml_STREAM_START_EVENT,
+		encoding: encoding,
+	}
+}
+
+// Create STREAM-END.
+func yaml_stream_end_event_initialize(event *yaml_event_t) {
+	*event = yaml_event_t{
+		typ: yaml_STREAM_END_EVENT,
+	}
+}
+
+// Create DOCUMENT-START.
+func yaml_document_start_event_initialize(
+	event *yaml_event_t,
+	version_directive *yaml_version_directive_t,
+	tag_directives []yaml_tag_directive_t,
+	implicit bool,
+) {
+	*event = yaml_event_t{
+		typ:               yaml_DOCUMENT_START_EVENT,
+		version_directive: version_directive,
+		tag_directives:    tag_directives,
+		implicit:          implicit,
+	}
+}
+
+// Create DOCUMENT-END.
+func yaml_document_end_event_initialize(event *yaml_event_t, implicit bool) {
+	*event = yaml_event_t{
+		typ:      yaml_DOCUMENT_END_EVENT,
+		implicit: implicit,
+	}
+}
+
+// Create ALIAS.
+func yaml_alias_event_initialize(event *yaml_event_t, anchor []byte) bool {
+	*event = yaml_event_t{
+		typ:    yaml_ALIAS_EVENT,
+		anchor: anchor,
+	}
+	return true
+}
+
+// Create SCALAR.
+func yaml_scalar_event_initialize(event *yaml_event_t, anchor, tag, value []byte, plain_implicit, quoted_implicit bool, style yaml_scalar_style_t) bool {
+	*event = yaml_event_t{
+		typ:             yaml_SCALAR_EVENT,
+		anchor:          anchor,
+		tag:             tag,
+		value:           value,
+		implicit:        plain_implicit,
+		quoted_implicit: quoted_implicit,
+		style:           yaml_style_t(style),
+	}
+	return true
+}
+
+// Create SEQUENCE-START.
+func yaml_sequence_start_event_initialize(event *yaml_event_t, anchor, tag []byte, implicit bool, style yaml_sequence_style_t) bool {
+	*event = yaml_event_t{
+		typ:      yaml_SEQUENCE_START_EVENT,
+		anchor:   anchor,
+		tag:      tag,
+		implicit: implicit,
+		style:    yaml_style_t(style),
+	}
+	return true
+}
+
+// Create SEQUENCE-END.
+func yaml_sequence_end_event_initialize(event *yaml_event_t) bool {
+	*event = yaml_event_t{
+		typ: yaml_SEQUENCE_END_EVENT,
+	}
+	return true
+}
+
+// Create MAPPING-START.
+func yaml_mapping_start_event_initialize(event *yaml_event_t, anchor, tag []byte, implicit bool, style yaml_mapping_style_t) {
+	*event = yaml_event_t{
+		typ:      yaml_MAPPING_START_EVENT,
+		anchor:   anchor,
+		tag:      tag,
+		implicit: implicit,
+		style:    yaml_style_t(style),
+	}
+}
+
+// Create MAPPING-END.
+func yaml_mapping_end_event_initialize(event *yaml_event_t) {
+	*event = yaml_event_t{
+		typ: yaml_MAPPING_END_EVENT,
+	}
+}
+
+// Destroy an event object.
+func yaml_event_delete(event *yaml_event_t) {
+	*event = yaml_event_t{}
+}
+
+///*
+// * Create a document object.
+// */
+//
+//YAML_DECLARE(int)
+//yaml_document_initialize(document *yaml_document_t,
+//        version_directive *yaml_version_directive_t,
+//        tag_directives_start *yaml_tag_directive_t,
+//        tag_directives_end *yaml_tag_directive_t,
+//        start_implicit int, end_implicit int)
+//{
+//    struct {
+//        error yaml_error_type_t
+//    } context
+//    struct {
+//        start *yaml_node_t
+//        end *yaml_node_t
+//        top *yaml_node_t
+//    } nodes = { NULL, NULL, NULL }
+//    version_directive_copy *yaml_version_directive_t = NULL
+//    struct {
+//        start *yaml_tag_directive_t
+//        end *yaml_tag_directive_t
+//        top *yaml_tag_directive_t
+//    } tag_directives_copy = { NULL, NULL, NULL }
+//    value yaml_tag_directive_t = { NULL, NULL }
+//    mark yaml_mark_t = { 0, 0, 0 }
+//
+//    assert(document) // Non-NULL document object is expected.
+//    assert((tag_directives_start && tag_directives_end) ||
+//            (tag_directives_start == tag_directives_end))
+//                            // Valid tag directives are expected.
+//
+//    if (!STACK_INIT(&context, nodes, INITIAL_STACK_SIZE)) goto error
+//
+//    if (version_directive) {
+//        version_directive_copy = yaml_malloc(sizeof(yaml_version_directive_t))
+//        if (!version_directive_copy) goto error
+//        version_directive_copy.major = version_directive.major
+//        version_directive_copy.minor = version_directive.minor
+//    }
+//
+//    if (tag_directives_start != tag_directives_end) {
+//        tag_directive *yaml_tag_directive_t
+//        if (!STACK_INIT(&context, tag_directives_copy, INITIAL_STACK_SIZE))
+//            goto error
+//        for (tag_directive = tag_directives_start
+//                tag_directive != tag_directives_end; tag_directive ++) {
+//            assert(tag_directive.handle)
+//            assert(tag_directive.prefix)
+//            if (!yaml_check_utf8(tag_directive.handle,
+//                        strlen((char *)tag_directive.handle)))
+//                goto error
+//            if (!yaml_check_utf8(tag_directive.prefix,
+//                        strlen((char *)tag_directive.prefix)))
+//                goto error
+//            value.handle = yaml_strdup(tag_directive.handle)
+//            value.prefix = yaml_strdup(tag_directive.prefix)
+//            if (!value.handle || !value.prefix) goto error
+//            if (!PUSH(&context, tag_directives_copy, value))
+//                goto error
+//            value.handle = NULL
+//            value.prefix = NULL
+//        }
+//    }
+//
+//    DOCUMENT_INIT(*document, nodes.start, nodes.end, version_directive_copy,
+//            tag_directives_copy.start, tag_directives_copy.top,
+//            start_implicit, end_implicit, mark, mark)
+//
+//    return 1
+//
+//error:
+//    STACK_DEL(&context, nodes)
+//    yaml_free(version_directive_copy)
+//    while (!STACK_EMPTY(&context, tag_directives_copy)) {
+//        value yaml_tag_directive_t = POP(&context, tag_directives_copy)
+//        yaml_free(value.handle)
+//        yaml_free(value.prefix)
+//    }
+//    STACK_DEL(&context, tag_directives_copy)
+//    yaml_free(value.handle)
+//    yaml_free(value.prefix)
+//
+//    return 0
+//}
+//
+///*
+// * Destroy a document object.
+// */
+//
+//YAML_DECLARE(void)
+//yaml_document_delete(document *yaml_document_t)
+//{
+//    struct {
+//        error yaml_error_type_t
+//    } context
+//    tag_directive *yaml_tag_directive_t
+//
+//    context.error = YAML_NO_ERROR // Eliminate a compiler warning.
+//
+//    assert(document) // Non-NULL document object is expected.
+//
+//    while (!STACK_EMPTY(&context, document.nodes)) {
+//        node yaml_node_t = POP(&context, document.nodes)
+//        yaml_free(node.tag)
+//        switch (node.type) {
+//            case YAML_SCALAR_NODE:
+//                yaml_free(node.data.scalar.value)
+//                break
+//            case YAML_SEQUENCE_NODE:
+//                STACK_DEL(&context, node.data.sequence.items)
+//                break
+//            case YAML_MAPPING_NODE:
+//                STACK_DEL(&context, node.data.mapping.pairs)
+//                break
+//            default:
+//                assert(0) // Should not happen.
+//        }
+//    }
+//    STACK_DEL(&context, document.nodes)
+//
+//    yaml_free(document.version_directive)
+//    for (tag_directive = document.tag_directives.start
+//            tag_directive != document.tag_directives.end
+//            tag_directive++) {
+//        yaml_free(tag_directive.handle)
+//        yaml_free(tag_directive.prefix)
+//    }
+//    yaml_free(document.tag_directives.start)
+//
+//    memset(document, 0, sizeof(yaml_document_t))
+//}
+//
+///**
+// * Get a document node.
+// */
+//
+//YAML_DECLARE(yaml_node_t *)
+//yaml_document_get_node(document *yaml_document_t, index int)
+//{
+//    assert(document) // Non-NULL document object is expected.
+//
+//    if (index > 0 && document.nodes.start + index <= document.nodes.top) {
+//        return document.nodes.start + index - 1
+//    }
+//    return NULL
+//}
+//
+///**
+// * Get the root object.
+// */
+//
+//YAML_DECLARE(yaml_node_t *)
+//yaml_document_get_root_node(document *yaml_document_t)
+//{
+//    assert(document) // Non-NULL document object is expected.
+//
+//    if (document.nodes.top != document.nodes.start) {
+//        return document.nodes.start
+//    }
+//    return NULL
+//}
+//
+///*
+// * Add a scalar node to a document.
+// */
+//
+//YAML_DECLARE(int)
+//yaml_document_add_scalar(document *yaml_document_t,
+//        tag *yaml_char_t, value *yaml_char_t, length int,
+//        style yaml_scalar_style_t)
+//{
+//    struct {
+//        error yaml_error_type_t
+//    } context
+//    mark yaml_mark_t = { 0, 0, 0 }
+//    tag_copy *yaml_char_t = NULL
+//    value_copy *yaml_char_t = NULL
+//    node yaml_node_t
+//
+//    assert(document) // Non-NULL document object is expected.
+//    assert(value) // Non-NULL value is expected.
+//
+//    if (!tag) {
+//        tag = (yaml_char_t *)YAML_DEFAULT_SCALAR_TAG
+//    }
+//
+//    if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error
+//    tag_copy = yaml_strdup(tag)
+//    if (!tag_copy) goto error
+//
+//    if (length < 0) {
+//        length = strlen((char *)value)
+//    }
+//
+//    if (!yaml_check_utf8(value, length)) goto error
+//    value_copy = yaml_malloc(length+1)
+//    if (!value_copy) goto error
+//    memcpy(value_copy, value, length)
+//    value_copy[length] = '\0'
+//
+//    SCALAR_NODE_INIT(node, tag_copy, value_copy, length, style, mark, mark)
+//    if (!PUSH(&context, document.nodes, node)) goto error
+//
+//    return document.nodes.top - document.nodes.start
+//
+//error:
+//    yaml_free(tag_copy)
+//    yaml_free(value_copy)
+//
+//    return 0
+//}
+//
+///*
+// * Add a sequence node to a document.
+// */
+//
+//YAML_DECLARE(int)
+//yaml_document_add_sequence(document *yaml_document_t,
+//        tag *yaml_char_t, style yaml_sequence_style_t)
+//{
+//    struct {
+//        error yaml_error_type_t
+//    } context
+//    mark yaml_mark_t = { 0, 0, 0 }
+//    tag_copy *yaml_char_t = NULL
+//    struct {
+//        start *yaml_node_item_t
+//        end *yaml_node_item_t
+//        top *yaml_node_item_t
+//    } items = { NULL, NULL, NULL }
+//    node yaml_node_t
+//
+//    assert(document) // Non-NULL document object is expected.
+//
+//    if (!tag) {
+//        tag = (yaml_char_t *)YAML_DEFAULT_SEQUENCE_TAG
+//    }
+//
+//    if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error
+//    tag_copy = yaml_strdup(tag)
+//    if (!tag_copy) goto error
+//
+//    if (!STACK_INIT(&context, items, INITIAL_STACK_SIZE)) goto error
+//
+//    SEQUENCE_NODE_INIT(node, tag_copy, items.start, items.end,
+//            style, mark, mark)
+//    if (!PUSH(&context, document.nodes, node)) goto error
+//
+//    return document.nodes.top - document.nodes.start
+//
+//error:
+//    STACK_DEL(&context, items)
+//    yaml_free(tag_copy)
+//
+//    return 0
+//}
+//
+///*
+// * Add a mapping node to a document.
+// */
+//
+//YAML_DECLARE(int)
+//yaml_document_add_mapping(document *yaml_document_t,
+//        tag *yaml_char_t, style yaml_mapping_style_t)
+//{
+//    struct {
+//        error yaml_error_type_t
+//    } context
+//    mark yaml_mark_t = { 0, 0, 0 }
+//    tag_copy *yaml_char_t = NULL
+//    struct {
+//        start *yaml_node_pair_t
+//        end *yaml_node_pair_t
+//        top *yaml_node_pair_t
+//    } pairs = { NULL, NULL, NULL }
+//    node yaml_node_t
+//
+//    assert(document) // Non-NULL document object is expected.
+//
+//    if (!tag) {
+//        tag = (yaml_char_t *)YAML_DEFAULT_MAPPING_TAG
+//    }
+//
+//    if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error
+//    tag_copy = yaml_strdup(tag)
+//    if (!tag_copy) goto error
+//
+//    if (!STACK_INIT(&context, pairs, INITIAL_STACK_SIZE)) goto error
+//
+//    MAPPING_NODE_INIT(node, tag_copy, pairs.start, pairs.end,
+//            style, mark, mark)
+//    if (!PUSH(&context, document.nodes, node)) goto error
+//
+//    return document.nodes.top - document.nodes.start
+//
+//error:
+//    STACK_DEL(&context, pairs)
+//    yaml_free(tag_copy)
+//
+//    return 0
+//}
+//
+///*
+// * Append an item to a sequence node.
+// */
+//
+//YAML_DECLARE(int)
+//yaml_document_append_sequence_item(document *yaml_document_t,
+//        sequence int, item int)
+//{
+//    struct {
+//        error yaml_error_type_t
+//    } context
+//
+//    assert(document) // Non-NULL document is required.
+//    assert(sequence > 0
+//            && document.nodes.start + sequence <= document.nodes.top)
+//                            // Valid sequence id is required.
+//    assert(document.nodes.start[sequence-1].type == YAML_SEQUENCE_NODE)
+//                            // A sequence node is required.
+//    assert(item > 0 && document.nodes.start + item <= document.nodes.top)
+//                            // Valid item id is required.
+//
+//    if (!PUSH(&context,
+//                document.nodes.start[sequence-1].data.sequence.items, item))
+//        return 0
+//
+//    return 1
+//}
+//
+///*
+// * Append a pair of a key and a value to a mapping node.
+// */
+//
+//YAML_DECLARE(int)
+//yaml_document_append_mapping_pair(document *yaml_document_t,
+//        mapping int, key int, value int)
+//{
+//    struct {
+//        error yaml_error_type_t
+//    } context
+//
+//    pair yaml_node_pair_t
+//
+//    assert(document) // Non-NULL document is required.
+//    assert(mapping > 0
+//            && document.nodes.start + mapping <= document.nodes.top)
+//                            // Valid mapping id is required.
+//    assert(document.nodes.start[mapping-1].type == YAML_MAPPING_NODE)
+//                            // A mapping node is required.
+//    assert(key > 0 && document.nodes.start + key <= document.nodes.top)
+//                            // Valid key id is required.
+//    assert(value > 0 && document.nodes.start + value <= document.nodes.top)
+//                            // Valid value id is required.
+//
+//    pair.key = key
+//    pair.value = value
+//
+//    if (!PUSH(&context,
+//                document.nodes.start[mapping-1].data.mapping.pairs, pair))
+//        return 0
+//
+//    return 1
+//}
+//
+//
diff --git a/vendor/gopkg.in/yaml.v3/decode.go b/vendor/gopkg.in/yaml.v3/decode.go
new file mode 100644
index 000000000..0173b6982
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/decode.go
@@ -0,0 +1,1000 @@
+//
+// Copyright (c) 2011-2019 Canonical Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package yaml
+
+import (
+	"encoding"
+	"encoding/base64"
+	"fmt"
+	"io"
+	"math"
+	"reflect"
+	"strconv"
+	"time"
+)
+
+// ----------------------------------------------------------------------------
+// Parser, produces a node tree out of a libyaml event stream.
+
+type parser struct {
+	parser   yaml_parser_t
+	event    yaml_event_t
+	doc      *Node
+	anchors  map[string]*Node
+	doneInit bool
+	textless bool
+}
+
+func newParser(b []byte) *parser {
+	p := parser{}
+	if !yaml_parser_initialize(&p.parser) {
+		panic("failed to initialize YAML emitter")
+	}
+	if len(b) == 0 {
+		b = []byte{'\n'}
+	}
+	yaml_parser_set_input_string(&p.parser, b)
+	return &p
+}
+
+func newParserFromReader(r io.Reader) *parser {
+	p := parser{}
+	if !yaml_parser_initialize(&p.parser) {
+		panic("failed to initialize YAML emitter")
+	}
+	yaml_parser_set_input_reader(&p.parser, r)
+	return &p
+}
+
+func (p *parser) init() {
+	if p.doneInit {
+		return
+	}
+	p.anchors = make(map[string]*Node)
+	p.expect(yaml_STREAM_START_EVENT)
+	p.doneInit = true
+}
+
+func (p *parser) destroy() {
+	if p.event.typ != yaml_NO_EVENT {
+		yaml_event_delete(&p.event)
+	}
+	yaml_parser_delete(&p.parser)
+}
+
+// expect consumes an event from the event stream and
+// checks that it's of the expected type.
+func (p *parser) expect(e yaml_event_type_t) {
+	if p.event.typ == yaml_NO_EVENT {
+		if !yaml_parser_parse(&p.parser, &p.event) {
+			p.fail()
+		}
+	}
+	if p.event.typ == yaml_STREAM_END_EVENT {
+		failf("attempted to go past the end of stream; corrupted value?")
+	}
+	if p.event.typ != e {
+		p.parser.problem = fmt.Sprintf("expected %s event but got %s", e, p.event.typ)
+		p.fail()
+	}
+	yaml_event_delete(&p.event)
+	p.event.typ = yaml_NO_EVENT
+}
+
+// peek peeks at the next event in the event stream,
+// puts the results into p.event and returns the event type.
+func (p *parser) peek() yaml_event_type_t {
+	if p.event.typ != yaml_NO_EVENT {
+		return p.event.typ
+	}
+	// It's curious choice from the underlying API to generally return a
+	// positive result on success, but on this case return true in an error
+	// scenario. This was the source of bugs in the past (issue #666).
+	if !yaml_parser_parse(&p.parser, &p.event) || p.parser.error != yaml_NO_ERROR {
+		p.fail()
+	}
+	return p.event.typ
+}
+
+func (p *parser) fail() {
+	var where string
+	var line int
+	if p.parser.context_mark.line != 0 {
+		line = p.parser.context_mark.line
+		// Scanner errors don't iterate line before returning error
+		if p.parser.error == yaml_SCANNER_ERROR {
+			line++
+		}
+	} else if p.parser.problem_mark.line != 0 {
+		line = p.parser.problem_mark.line
+		// Scanner errors don't iterate line before returning error
+		if p.parser.error == yaml_SCANNER_ERROR {
+			line++
+		}
+	}
+	if line != 0 {
+		where = "line " + strconv.Itoa(line) + ": "
+	}
+	var msg string
+	if len(p.parser.problem) > 0 {
+		msg = p.parser.problem
+	} else {
+		msg = "unknown problem parsing YAML content"
+	}
+	failf("%s%s", where, msg)
+}
+
+func (p *parser) anchor(n *Node, anchor []byte) {
+	if anchor != nil {
+		n.Anchor = string(anchor)
+		p.anchors[n.Anchor] = n
+	}
+}
+
+func (p *parser) parse() *Node {
+	p.init()
+	switch p.peek() {
+	case yaml_SCALAR_EVENT:
+		return p.scalar()
+	case yaml_ALIAS_EVENT:
+		return p.alias()
+	case yaml_MAPPING_START_EVENT:
+		return p.mapping()
+	case yaml_SEQUENCE_START_EVENT:
+		return p.sequence()
+	case yaml_DOCUMENT_START_EVENT:
+		return p.document()
+	case yaml_STREAM_END_EVENT:
+		// Happens when attempting to decode an empty buffer.
+		return nil
+	case yaml_TAIL_COMMENT_EVENT:
+		panic("internal error: unexpected tail comment event (please report)")
+	default:
+		panic("internal error: attempted to parse unknown event (please report): " + p.event.typ.String())
+	}
+}
+
+func (p *parser) node(kind Kind, defaultTag, tag, value string) *Node {
+	var style Style
+	if tag != "" && tag != "!" {
+		tag = shortTag(tag)
+		style = TaggedStyle
+	} else if defaultTag != "" {
+		tag = defaultTag
+	} else if kind == ScalarNode {
+		tag, _ = resolve("", value)
+	}
+	n := &Node{
+		Kind:  kind,
+		Tag:   tag,
+		Value: value,
+		Style: style,
+	}
+	if !p.textless {
+		n.Line = p.event.start_mark.line + 1
+		n.Column = p.event.start_mark.column + 1
+		n.HeadComment = string(p.event.head_comment)
+		n.LineComment = string(p.event.line_comment)
+		n.FootComment = string(p.event.foot_comment)
+	}
+	return n
+}
+
+func (p *parser) parseChild(parent *Node) *Node {
+	child := p.parse()
+	parent.Content = append(parent.Content, child)
+	return child
+}
+
+func (p *parser) document() *Node {
+	n := p.node(DocumentNode, "", "", "")
+	p.doc = n
+	p.expect(yaml_DOCUMENT_START_EVENT)
+	p.parseChild(n)
+	if p.peek() == yaml_DOCUMENT_END_EVENT {
+		n.FootComment = string(p.event.foot_comment)
+	}
+	p.expect(yaml_DOCUMENT_END_EVENT)
+	return n
+}
+
+func (p *parser) alias() *Node {
+	n := p.node(AliasNode, "", "", string(p.event.anchor))
+	n.Alias = p.anchors[n.Value]
+	if n.Alias == nil {
+		failf("unknown anchor '%s' referenced", n.Value)
+	}
+	p.expect(yaml_ALIAS_EVENT)
+	return n
+}
+
+func (p *parser) scalar() *Node {
+	var parsedStyle = p.event.scalar_style()
+	var nodeStyle Style
+	switch {
+	case parsedStyle&yaml_DOUBLE_QUOTED_SCALAR_STYLE != 0:
+		nodeStyle = DoubleQuotedStyle
+	case parsedStyle&yaml_SINGLE_QUOTED_SCALAR_STYLE != 0:
+		nodeStyle = SingleQuotedStyle
+	case parsedStyle&yaml_LITERAL_SCALAR_STYLE != 0:
+		nodeStyle = LiteralStyle
+	case parsedStyle&yaml_FOLDED_SCALAR_STYLE != 0:
+		nodeStyle = FoldedStyle
+	}
+	var nodeValue = string(p.event.value)
+	var nodeTag = string(p.event.tag)
+	var defaultTag string
+	if nodeStyle == 0 {
+		if nodeValue == "<<" {
+			defaultTag = mergeTag
+		}
+	} else {
+		defaultTag = strTag
+	}
+	n := p.node(ScalarNode, defaultTag, nodeTag, nodeValue)
+	n.Style |= nodeStyle
+	p.anchor(n, p.event.anchor)
+	p.expect(yaml_SCALAR_EVENT)
+	return n
+}
+
+func (p *parser) sequence() *Node {
+	n := p.node(SequenceNode, seqTag, string(p.event.tag), "")
+	if p.event.sequence_style()&yaml_FLOW_SEQUENCE_STYLE != 0 {
+		n.Style |= FlowStyle
+	}
+	p.anchor(n, p.event.anchor)
+	p.expect(yaml_SEQUENCE_START_EVENT)
+	for p.peek() != yaml_SEQUENCE_END_EVENT {
+		p.parseChild(n)
+	}
+	n.LineComment = string(p.event.line_comment)
+	n.FootComment = string(p.event.foot_comment)
+	p.expect(yaml_SEQUENCE_END_EVENT)
+	return n
+}
+
+func (p *parser) mapping() *Node {
+	n := p.node(MappingNode, mapTag, string(p.event.tag), "")
+	block := true
+	if p.event.mapping_style()&yaml_FLOW_MAPPING_STYLE != 0 {
+		block = false
+		n.Style |= FlowStyle
+	}
+	p.anchor(n, p.event.anchor)
+	p.expect(yaml_MAPPING_START_EVENT)
+	for p.peek() != yaml_MAPPING_END_EVENT {
+		k := p.parseChild(n)
+		if block && k.FootComment != "" {
+			// Must be a foot comment for the prior value when being dedented.
+			if len(n.Content) > 2 {
+				n.Content[len(n.Content)-3].FootComment = k.FootComment
+				k.FootComment = ""
+			}
+		}
+		v := p.parseChild(n)
+		if k.FootComment == "" && v.FootComment != "" {
+			k.FootComment = v.FootComment
+			v.FootComment = ""
+		}
+		if p.peek() == yaml_TAIL_COMMENT_EVENT {
+			if k.FootComment == "" {
+				k.FootComment = string(p.event.foot_comment)
+			}
+			p.expect(yaml_TAIL_COMMENT_EVENT)
+		}
+	}
+	n.LineComment = string(p.event.line_comment)
+	n.FootComment = string(p.event.foot_comment)
+	if n.Style&FlowStyle == 0 && n.FootComment != "" && len(n.Content) > 1 {
+		n.Content[len(n.Content)-2].FootComment = n.FootComment
+		n.FootComment = ""
+	}
+	p.expect(yaml_MAPPING_END_EVENT)
+	return n
+}
+
+// ----------------------------------------------------------------------------
+// Decoder, unmarshals a node into a provided value.
+
+type decoder struct {
+	doc     *Node
+	aliases map[*Node]bool
+	terrors []string
+
+	stringMapType  reflect.Type
+	generalMapType reflect.Type
+
+	knownFields bool
+	uniqueKeys  bool
+	decodeCount int
+	aliasCount  int
+	aliasDepth  int
+
+	mergedFields map[interface{}]bool
+}
+
+var (
+	nodeType       = reflect.TypeOf(Node{})
+	durationType   = reflect.TypeOf(time.Duration(0))
+	stringMapType  = reflect.TypeOf(map[string]interface{}{})
+	generalMapType = reflect.TypeOf(map[interface{}]interface{}{})
+	ifaceType      = generalMapType.Elem()
+	timeType       = reflect.TypeOf(time.Time{})
+	ptrTimeType    = reflect.TypeOf(&time.Time{})
+)
+
+func newDecoder() *decoder {
+	d := &decoder{
+		stringMapType:  stringMapType,
+		generalMapType: generalMapType,
+		uniqueKeys:     true,
+	}
+	d.aliases = make(map[*Node]bool)
+	return d
+}
+
+func (d *decoder) terror(n *Node, tag string, out reflect.Value) {
+	if n.Tag != "" {
+		tag = n.Tag
+	}
+	value := n.Value
+	if tag != seqTag && tag != mapTag {
+		if len(value) > 10 {
+			value = " `" + value[:7] + "...`"
+		} else {
+			value = " `" + value + "`"
+		}
+	}
+	d.terrors = append(d.terrors, fmt.Sprintf("line %d: cannot unmarshal %s%s into %s", n.Line, shortTag(tag), value, out.Type()))
+}
+
+func (d *decoder) callUnmarshaler(n *Node, u Unmarshaler) (good bool) {
+	err := u.UnmarshalYAML(n)
+	if e, ok := err.(*TypeError); ok {
+		d.terrors = append(d.terrors, e.Errors...)
+		return false
+	}
+	if err != nil {
+		fail(err)
+	}
+	return true
+}
+
+func (d *decoder) callObsoleteUnmarshaler(n *Node, u obsoleteUnmarshaler) (good bool) {
+	terrlen := len(d.terrors)
+	err := u.UnmarshalYAML(func(v interface{}) (err error) {
+		defer handleErr(&err)
+		d.unmarshal(n, reflect.ValueOf(v))
+		if len(d.terrors) > terrlen {
+			issues := d.terrors[terrlen:]
+			d.terrors = d.terrors[:terrlen]
+			return &TypeError{issues}
+		}
+		return nil
+	})
+	if e, ok := err.(*TypeError); ok {
+		d.terrors = append(d.terrors, e.Errors...)
+		return false
+	}
+	if err != nil {
+		fail(err)
+	}
+	return true
+}
+
+// d.prepare initializes and dereferences pointers and calls UnmarshalYAML
+// if a value is found to implement it.
+// It returns the initialized and dereferenced out value, whether
+// unmarshalling was already done by UnmarshalYAML, and if so whether
+// its types unmarshalled appropriately.
+//
+// If n holds a null value, prepare returns before doing anything.
+func (d *decoder) prepare(n *Node, out reflect.Value) (newout reflect.Value, unmarshaled, good bool) {
+	if n.ShortTag() == nullTag {
+		return out, false, false
+	}
+	again := true
+	for again {
+		again = false
+		if out.Kind() == reflect.Ptr {
+			if out.IsNil() {
+				out.Set(reflect.New(out.Type().Elem()))
+			}
+			out = out.Elem()
+			again = true
+		}
+		if out.CanAddr() {
+			outi := out.Addr().Interface()
+			if u, ok := outi.(Unmarshaler); ok {
+				good = d.callUnmarshaler(n, u)
+				return out, true, good
+			}
+			if u, ok := outi.(obsoleteUnmarshaler); ok {
+				good = d.callObsoleteUnmarshaler(n, u)
+				return out, true, good
+			}
+		}
+	}
+	return out, false, false
+}
+
+func (d *decoder) fieldByIndex(n *Node, v reflect.Value, index []int) (field reflect.Value) {
+	if n.ShortTag() == nullTag {
+		return reflect.Value{}
+	}
+	for _, num := range index {
+		for {
+			if v.Kind() == reflect.Ptr {
+				if v.IsNil() {
+					v.Set(reflect.New(v.Type().Elem()))
+				}
+				v = v.Elem()
+				continue
+			}
+			break
+		}
+		v = v.Field(num)
+	}
+	return v
+}
+
+const (
+	// 400,000 decode operations is ~500kb of dense object declarations, or
+	// ~5kb of dense object declarations with 10000% alias expansion
+	alias_ratio_range_low = 400000
+
+	// 4,000,000 decode operations is ~5MB of dense object declarations, or
+	// ~4.5MB of dense object declarations with 10% alias expansion
+	alias_ratio_range_high = 4000000
+
+	// alias_ratio_range is the range over which we scale allowed alias ratios
+	alias_ratio_range = float64(alias_ratio_range_high - alias_ratio_range_low)
+)
+
+func allowedAliasRatio(decodeCount int) float64 {
+	switch {
+	case decodeCount <= alias_ratio_range_low:
+		// allow 99% to come from alias expansion for small-to-medium documents
+		return 0.99
+	case decodeCount >= alias_ratio_range_high:
+		// allow 10% to come from alias expansion for very large documents
+		return 0.10
+	default:
+		// scale smoothly from 99% down to 10% over the range.
+		// this maps to 396,000 - 400,000 allowed alias-driven decodes over the range.
+		// 400,000 decode operations is ~100MB of allocations in worst-case scenarios (single-item maps).
+		return 0.99 - 0.89*(float64(decodeCount-alias_ratio_range_low)/alias_ratio_range)
+	}
+}
+
+func (d *decoder) unmarshal(n *Node, out reflect.Value) (good bool) {
+	d.decodeCount++
+	if d.aliasDepth > 0 {
+		d.aliasCount++
+	}
+	if d.aliasCount > 100 && d.decodeCount > 1000 && float64(d.aliasCount)/float64(d.decodeCount) > allowedAliasRatio(d.decodeCount) {
+		failf("document contains excessive aliasing")
+	}
+	if out.Type() == nodeType {
+		out.Set(reflect.ValueOf(n).Elem())
+		return true
+	}
+	switch n.Kind {
+	case DocumentNode:
+		return d.document(n, out)
+	case AliasNode:
+		return d.alias(n, out)
+	}
+	out, unmarshaled, good := d.prepare(n, out)
+	if unmarshaled {
+		return good
+	}
+	switch n.Kind {
+	case ScalarNode:
+		good = d.scalar(n, out)
+	case MappingNode:
+		good = d.mapping(n, out)
+	case SequenceNode:
+		good = d.sequence(n, out)
+	case 0:
+		if n.IsZero() {
+			return d.null(out)
+		}
+		fallthrough
+	default:
+		failf("cannot decode node with unknown kind %d", n.Kind)
+	}
+	return good
+}
+
+func (d *decoder) document(n *Node, out reflect.Value) (good bool) {
+	if len(n.Content) == 1 {
+		d.doc = n
+		d.unmarshal(n.Content[0], out)
+		return true
+	}
+	return false
+}
+
+func (d *decoder) alias(n *Node, out reflect.Value) (good bool) {
+	if d.aliases[n] {
+		// TODO this could actually be allowed in some circumstances.
+		failf("anchor '%s' value contains itself", n.Value)
+	}
+	d.aliases[n] = true
+	d.aliasDepth++
+	good = d.unmarshal(n.Alias, out)
+	d.aliasDepth--
+	delete(d.aliases, n)
+	return good
+}
+
+var zeroValue reflect.Value
+
+func resetMap(out reflect.Value) {
+	for _, k := range out.MapKeys() {
+		out.SetMapIndex(k, zeroValue)
+	}
+}
+
+func (d *decoder) null(out reflect.Value) bool {
+	if out.CanAddr() {
+		switch out.Kind() {
+		case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
+			out.Set(reflect.Zero(out.Type()))
+			return true
+		}
+	}
+	return false
+}
+
+func (d *decoder) scalar(n *Node, out reflect.Value) bool {
+	var tag string
+	var resolved interface{}
+	if n.indicatedString() {
+		tag = strTag
+		resolved = n.Value
+	} else {
+		tag, resolved = resolve(n.Tag, n.Value)
+		if tag == binaryTag {
+			data, err := base64.StdEncoding.DecodeString(resolved.(string))
+			if err != nil {
+				failf("!!binary value contains invalid base64 data")
+			}
+			resolved = string(data)
+		}
+	}
+	if resolved == nil {
+		return d.null(out)
+	}
+	if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() {
+		// We've resolved to exactly the type we want, so use that.
+		out.Set(resolvedv)
+		return true
+	}
+	// Perhaps we can use the value as a TextUnmarshaler to
+	// set its value.
+	if out.CanAddr() {
+		u, ok := out.Addr().Interface().(encoding.TextUnmarshaler)
+		if ok {
+			var text []byte
+			if tag == binaryTag {
+				text = []byte(resolved.(string))
+			} else {
+				// We let any value be unmarshaled into TextUnmarshaler.
+				// That might be more lax than we'd like, but the
+				// TextUnmarshaler itself should bowl out any dubious values.
+				text = []byte(n.Value)
+			}
+			err := u.UnmarshalText(text)
+			if err != nil {
+				fail(err)
+			}
+			return true
+		}
+	}
+	switch out.Kind() {
+	case reflect.String:
+		if tag == binaryTag {
+			out.SetString(resolved.(string))
+			return true
+		}
+		out.SetString(n.Value)
+		return true
+	case reflect.Interface:
+		out.Set(reflect.ValueOf(resolved))
+		return true
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		// This used to work in v2, but it's very unfriendly.
+		isDuration := out.Type() == durationType
+
+		switch resolved := resolved.(type) {
+		case int:
+			if !isDuration && !out.OverflowInt(int64(resolved)) {
+				out.SetInt(int64(resolved))
+				return true
+			}
+		case int64:
+			if !isDuration && !out.OverflowInt(resolved) {
+				out.SetInt(resolved)
+				return true
+			}
+		case uint64:
+			if !isDuration && resolved <= math.MaxInt64 && !out.OverflowInt(int64(resolved)) {
+				out.SetInt(int64(resolved))
+				return true
+			}
+		case float64:
+			if !isDuration && resolved <= math.MaxInt64 && !out.OverflowInt(int64(resolved)) {
+				out.SetInt(int64(resolved))
+				return true
+			}
+		case string:
+			if out.Type() == durationType {
+				d, err := time.ParseDuration(resolved)
+				if err == nil {
+					out.SetInt(int64(d))
+					return true
+				}
+			}
+		}
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		switch resolved := resolved.(type) {
+		case int:
+			if resolved >= 0 && !out.OverflowUint(uint64(resolved)) {
+				out.SetUint(uint64(resolved))
+				return true
+			}
+		case int64:
+			if resolved >= 0 && !out.OverflowUint(uint64(resolved)) {
+				out.SetUint(uint64(resolved))
+				return true
+			}
+		case uint64:
+			if !out.OverflowUint(uint64(resolved)) {
+				out.SetUint(uint64(resolved))
+				return true
+			}
+		case float64:
+			if resolved <= math.MaxUint64 && !out.OverflowUint(uint64(resolved)) {
+				out.SetUint(uint64(resolved))
+				return true
+			}
+		}
+	case reflect.Bool:
+		switch resolved := resolved.(type) {
+		case bool:
+			out.SetBool(resolved)
+			return true
+		case string:
+			// This offers some compatibility with the 1.1 spec (https://yaml.org/type/bool.html).
+			// It only works if explicitly attempting to unmarshal into a typed bool value.
+			switch resolved {
+			case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON":
+				out.SetBool(true)
+				return true
+			case "n", "N", "no", "No", "NO", "off", "Off", "OFF":
+				out.SetBool(false)
+				return true
+			}
+		}
+	case reflect.Float32, reflect.Float64:
+		switch resolved := resolved.(type) {
+		case int:
+			out.SetFloat(float64(resolved))
+			return true
+		case int64:
+			out.SetFloat(float64(resolved))
+			return true
+		case uint64:
+			out.SetFloat(float64(resolved))
+			return true
+		case float64:
+			out.SetFloat(resolved)
+			return true
+		}
+	case reflect.Struct:
+		if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() {
+			out.Set(resolvedv)
+			return true
+		}
+	case reflect.Ptr:
+		panic("yaml internal error: please report the issue")
+	}
+	d.terror(n, tag, out)
+	return false
+}
+
+func settableValueOf(i interface{}) reflect.Value {
+	v := reflect.ValueOf(i)
+	sv := reflect.New(v.Type()).Elem()
+	sv.Set(v)
+	return sv
+}
+
+func (d *decoder) sequence(n *Node, out reflect.Value) (good bool) {
+	l := len(n.Content)
+
+	var iface reflect.Value
+	switch out.Kind() {
+	case reflect.Slice:
+		out.Set(reflect.MakeSlice(out.Type(), l, l))
+	case reflect.Array:
+		if l != out.Len() {
+			failf("invalid array: want %d elements but got %d", out.Len(), l)
+		}
+	case reflect.Interface:
+		// No type hints. Will have to use a generic sequence.
+		iface = out
+		out = settableValueOf(make([]interface{}, l))
+	default:
+		d.terror(n, seqTag, out)
+		return false
+	}
+	et := out.Type().Elem()
+
+	j := 0
+	for i := 0; i < l; i++ {
+		e := reflect.New(et).Elem()
+		if ok := d.unmarshal(n.Content[i], e); ok {
+			out.Index(j).Set(e)
+			j++
+		}
+	}
+	if out.Kind() != reflect.Array {
+		out.Set(out.Slice(0, j))
+	}
+	if iface.IsValid() {
+		iface.Set(out)
+	}
+	return true
+}
+
+func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) {
+	l := len(n.Content)
+	if d.uniqueKeys {
+		nerrs := len(d.terrors)
+		for i := 0; i < l; i += 2 {
+			ni := n.Content[i]
+			for j := i + 2; j < l; j += 2 {
+				nj := n.Content[j]
+				if ni.Kind == nj.Kind && ni.Value == nj.Value {
+					d.terrors = append(d.terrors, fmt.Sprintf("line %d: mapping key %#v already defined at line %d", nj.Line, nj.Value, ni.Line))
+				}
+			}
+		}
+		if len(d.terrors) > nerrs {
+			return false
+		}
+	}
+	switch out.Kind() {
+	case reflect.Struct:
+		return d.mappingStruct(n, out)
+	case reflect.Map:
+		// okay
+	case reflect.Interface:
+		iface := out
+		if isStringMap(n) {
+			out = reflect.MakeMap(d.stringMapType)
+		} else {
+			out = reflect.MakeMap(d.generalMapType)
+		}
+		iface.Set(out)
+	default:
+		d.terror(n, mapTag, out)
+		return false
+	}
+
+	outt := out.Type()
+	kt := outt.Key()
+	et := outt.Elem()
+
+	stringMapType := d.stringMapType
+	generalMapType := d.generalMapType
+	if outt.Elem() == ifaceType {
+		if outt.Key().Kind() == reflect.String {
+			d.stringMapType = outt
+		} else if outt.Key() == ifaceType {
+			d.generalMapType = outt
+		}
+	}
+
+	mergedFields := d.mergedFields
+	d.mergedFields = nil
+
+	var mergeNode *Node
+
+	mapIsNew := false
+	if out.IsNil() {
+		out.Set(reflect.MakeMap(outt))
+		mapIsNew = true
+	}
+	for i := 0; i < l; i += 2 {
+		if isMerge(n.Content[i]) {
+			mergeNode = n.Content[i+1]
+			continue
+		}
+		k := reflect.New(kt).Elem()
+		if d.unmarshal(n.Content[i], k) {
+			if mergedFields != nil {
+				ki := k.Interface()
+				if mergedFields[ki] {
+					continue
+				}
+				mergedFields[ki] = true
+			}
+			kkind := k.Kind()
+			if kkind == reflect.Interface {
+				kkind = k.Elem().Kind()
+			}
+			if kkind == reflect.Map || kkind == reflect.Slice {
+				failf("invalid map key: %#v", k.Interface())
+			}
+			e := reflect.New(et).Elem()
+			if d.unmarshal(n.Content[i+1], e) || n.Content[i+1].ShortTag() == nullTag && (mapIsNew || !out.MapIndex(k).IsValid()) {
+				out.SetMapIndex(k, e)
+			}
+		}
+	}
+
+	d.mergedFields = mergedFields
+	if mergeNode != nil {
+		d.merge(n, mergeNode, out)
+	}
+
+	d.stringMapType = stringMapType
+	d.generalMapType = generalMapType
+	return true
+}
+
+func isStringMap(n *Node) bool {
+	if n.Kind != MappingNode {
+		return false
+	}
+	l := len(n.Content)
+	for i := 0; i < l; i += 2 {
+		shortTag := n.Content[i].ShortTag()
+		if shortTag != strTag && shortTag != mergeTag {
+			return false
+		}
+	}
+	return true
+}
+
+func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) {
+	sinfo, err := getStructInfo(out.Type())
+	if err != nil {
+		panic(err)
+	}
+
+	var inlineMap reflect.Value
+	var elemType reflect.Type
+	if sinfo.InlineMap != -1 {
+		inlineMap = out.Field(sinfo.InlineMap)
+		elemType = inlineMap.Type().Elem()
+	}
+
+	for _, index := range sinfo.InlineUnmarshalers {
+		field := d.fieldByIndex(n, out, index)
+		d.prepare(n, field)
+	}
+
+	mergedFields := d.mergedFields
+	d.mergedFields = nil
+	var mergeNode *Node
+	var doneFields []bool
+	if d.uniqueKeys {
+		doneFields = make([]bool, len(sinfo.FieldsList))
+	}
+	name := settableValueOf("")
+	l := len(n.Content)
+	for i := 0; i < l; i += 2 {
+		ni := n.Content[i]
+		if isMerge(ni) {
+			mergeNode = n.Content[i+1]
+			continue
+		}
+		if !d.unmarshal(ni, name) {
+			continue
+		}
+		sname := name.String()
+		if mergedFields != nil {
+			if mergedFields[sname] {
+				continue
+			}
+			mergedFields[sname] = true
+		}
+		if info, ok := sinfo.FieldsMap[sname]; ok {
+			if d.uniqueKeys {
+				if doneFields[info.Id] {
+					d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s already set in type %s", ni.Line, name.String(), out.Type()))
+					continue
+				}
+				doneFields[info.Id] = true
+			}
+			var field reflect.Value
+			if info.Inline == nil {
+				field = out.Field(info.Num)
+			} else {
+				field = d.fieldByIndex(n, out, info.Inline)
+			}
+			d.unmarshal(n.Content[i+1], field)
+		} else if sinfo.InlineMap != -1 {
+			if inlineMap.IsNil() {
+				inlineMap.Set(reflect.MakeMap(inlineMap.Type()))
+			}
+			value := reflect.New(elemType).Elem()
+			d.unmarshal(n.Content[i+1], value)
+			inlineMap.SetMapIndex(name, value)
+		} else if d.knownFields {
+			d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s not found in type %s", ni.Line, name.String(), out.Type()))
+		}
+	}
+
+	d.mergedFields = mergedFields
+	if mergeNode != nil {
+		d.merge(n, mergeNode, out)
+	}
+	return true
+}
+
+func failWantMap() {
+	failf("map merge requires map or sequence of maps as the value")
+}
+
+func (d *decoder) merge(parent *Node, merge *Node, out reflect.Value) {
+	mergedFields := d.mergedFields
+	if mergedFields == nil {
+		d.mergedFields = make(map[interface{}]bool)
+		for i := 0; i < len(parent.Content); i += 2 {
+			k := reflect.New(ifaceType).Elem()
+			if d.unmarshal(parent.Content[i], k) {
+				d.mergedFields[k.Interface()] = true
+			}
+		}
+	}
+
+	switch merge.Kind {
+	case MappingNode:
+		d.unmarshal(merge, out)
+	case AliasNode:
+		if merge.Alias != nil && merge.Alias.Kind != MappingNode {
+			failWantMap()
+		}
+		d.unmarshal(merge, out)
+	case SequenceNode:
+		for i := 0; i < len(merge.Content); i++ {
+			ni := merge.Content[i]
+			if ni.Kind == AliasNode {
+				if ni.Alias != nil && ni.Alias.Kind != MappingNode {
+					failWantMap()
+				}
+			} else if ni.Kind != MappingNode {
+				failWantMap()
+			}
+			d.unmarshal(ni, out)
+		}
+	default:
+		failWantMap()
+	}
+
+	d.mergedFields = mergedFields
+}
+
+func isMerge(n *Node) bool {
+	return n.Kind == ScalarNode && n.Value == "<<" && (n.Tag == "" || n.Tag == "!" || shortTag(n.Tag) == mergeTag)
+}
diff --git a/vendor/gopkg.in/yaml.v3/emitterc.go b/vendor/gopkg.in/yaml.v3/emitterc.go
new file mode 100644
index 000000000..0f47c9ca8
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/emitterc.go
@@ -0,0 +1,2020 @@
+//
+// Copyright (c) 2011-2019 Canonical Ltd
+// Copyright (c) 2006-2010 Kirill Simonov
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package yaml
+
+import (
+	"bytes"
+	"fmt"
+)
+
+// Flush the buffer if needed.
+func flush(emitter *yaml_emitter_t) bool {
+	if emitter.buffer_pos+5 >= len(emitter.buffer) {
+		return yaml_emitter_flush(emitter)
+	}
+	return true
+}
+
+// Put a character to the output buffer.
+func put(emitter *yaml_emitter_t, value byte) bool {
+	if emitter.buffer_pos+5 >= len(emitter.buffer) && !yaml_emitter_flush(emitter) {
+		return false
+	}
+	emitter.buffer[emitter.buffer_pos] = value
+	emitter.buffer_pos++
+	emitter.column++
+	return true
+}
+
+// Put a line break to the output buffer.
+func put_break(emitter *yaml_emitter_t) bool {
+	if emitter.buffer_pos+5 >= len(emitter.buffer) && !yaml_emitter_flush(emitter) {
+		return false
+	}
+	switch emitter.line_break {
+	case yaml_CR_BREAK:
+		emitter.buffer[emitter.buffer_pos] = '\r'
+		emitter.buffer_pos += 1
+	case yaml_LN_BREAK:
+		emitter.buffer[emitter.buffer_pos] = '\n'
+		emitter.buffer_pos += 1
+	case yaml_CRLN_BREAK:
+		emitter.buffer[emitter.buffer_pos+0] = '\r'
+		emitter.buffer[emitter.buffer_pos+1] = '\n'
+		emitter.buffer_pos += 2
+	default:
+		panic("unknown line break setting")
+	}
+	if emitter.column == 0 {
+		emitter.space_above = true
+	}
+	emitter.column = 0
+	emitter.line++
+	// [Go] Do this here and below and drop from everywhere else (see commented lines).
+	emitter.indention = true
+	return true
+}
+
+// Copy a character from a string into buffer.
+func write(emitter *yaml_emitter_t, s []byte, i *int) bool {
+	if emitter.buffer_pos+5 >= len(emitter.buffer) && !yaml_emitter_flush(emitter) {
+		return false
+	}
+	p := emitter.buffer_pos
+	w := width(s[*i])
+	switch w {
+	case 4:
+		emitter.buffer[p+3] = s[*i+3]
+		fallthrough
+	case 3:
+		emitter.buffer[p+2] = s[*i+2]
+		fallthrough
+	case 2:
+		emitter.buffer[p+1] = s[*i+1]
+		fallthrough
+	case 1:
+		emitter.buffer[p+0] = s[*i+0]
+	default:
+		panic("unknown character width")
+	}
+	emitter.column++
+	emitter.buffer_pos += w
+	*i += w
+	return true
+}
+
+// Write a whole string into buffer.
+func write_all(emitter *yaml_emitter_t, s []byte) bool {
+	for i := 0; i < len(s); {
+		if !write(emitter, s, &i) {
+			return false
+		}
+	}
+	return true
+}
+
+// Copy a line break character from a string into buffer.
+func write_break(emitter *yaml_emitter_t, s []byte, i *int) bool {
+	if s[*i] == '\n' {
+		if !put_break(emitter) {
+			return false
+		}
+		*i++
+	} else {
+		if !write(emitter, s, i) {
+			return false
+		}
+		if emitter.column == 0 {
+			emitter.space_above = true
+		}
+		emitter.column = 0
+		emitter.line++
+		// [Go] Do this here and above and drop from everywhere else (see commented lines).
+		emitter.indention = true
+	}
+	return true
+}
+
+// Set an emitter error and return false.
+func yaml_emitter_set_emitter_error(emitter *yaml_emitter_t, problem string) bool {
+	emitter.error = yaml_EMITTER_ERROR
+	emitter.problem = problem
+	return false
+}
+
+// Emit an event.
+func yaml_emitter_emit(emitter *yaml_emitter_t, event *yaml_event_t) bool {
+	emitter.events = append(emitter.events, *event)
+	for !yaml_emitter_need_more_events(emitter) {
+		event := &emitter.events[emitter.events_head]
+		if !yaml_emitter_analyze_event(emitter, event) {
+			return false
+		}
+		if !yaml_emitter_state_machine(emitter, event) {
+			return false
+		}
+		yaml_event_delete(event)
+		emitter.events_head++
+	}
+	return true
+}
+
+// Check if we need to accumulate more events before emitting.
+//
+// We accumulate extra
+//  - 1 event for DOCUMENT-START
+//  - 2 events for SEQUENCE-START
+//  - 3 events for MAPPING-START
+//
+func yaml_emitter_need_more_events(emitter *yaml_emitter_t) bool {
+	if emitter.events_head == len(emitter.events) {
+		return true
+	}
+	var accumulate int
+	switch emitter.events[emitter.events_head].typ {
+	case yaml_DOCUMENT_START_EVENT:
+		accumulate = 1
+		break
+	case yaml_SEQUENCE_START_EVENT:
+		accumulate = 2
+		break
+	case yaml_MAPPING_START_EVENT:
+		accumulate = 3
+		break
+	default:
+		return false
+	}
+	if len(emitter.events)-emitter.events_head > accumulate {
+		return false
+	}
+	var level int
+	for i := emitter.events_head; i < len(emitter.events); i++ {
+		switch emitter.events[i].typ {
+		case yaml_STREAM_START_EVENT, yaml_DOCUMENT_START_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT:
+			level++
+		case yaml_STREAM_END_EVENT, yaml_DOCUMENT_END_EVENT, yaml_SEQUENCE_END_EVENT, yaml_MAPPING_END_EVENT:
+			level--
+		}
+		if level == 0 {
+			return false
+		}
+	}
+	return true
+}
+
+// Append a directive to the directives stack.
+func yaml_emitter_append_tag_directive(emitter *yaml_emitter_t, value *yaml_tag_directive_t, allow_duplicates bool) bool {
+	for i := 0; i < len(emitter.tag_directives); i++ {
+		if bytes.Equal(value.handle, emitter.tag_directives[i].handle) {
+			if allow_duplicates {
+				return true
+			}
+			return yaml_emitter_set_emitter_error(emitter, "duplicate %TAG directive")
+		}
+	}
+
+	// [Go] Do we actually need to copy this given garbage collection
+	// and the lack of deallocating destructors?
+	tag_copy := yaml_tag_directive_t{
+		handle: make([]byte, len(value.handle)),
+		prefix: make([]byte, len(value.prefix)),
+	}
+	copy(tag_copy.handle, value.handle)
+	copy(tag_copy.prefix, value.prefix)
+	emitter.tag_directives = append(emitter.tag_directives, tag_copy)
+	return true
+}
+
+// Increase the indentation level.
+func yaml_emitter_increase_indent(emitter *yaml_emitter_t, flow, indentless bool) bool {
+	emitter.indents = append(emitter.indents, emitter.indent)
+	if emitter.indent < 0 {
+		if flow {
+			emitter.indent = emitter.best_indent
+		} else {
+			emitter.indent = 0
+		}
+	} else if !indentless {
+		// [Go] This was changed so that indentations are more regular.
+		if emitter.states[len(emitter.states)-1] == yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE {
+			// The first indent inside a sequence will just skip the "- " indicator.
+			emitter.indent += 2
+		} else {
+			// Everything else aligns to the chosen indentation.
+			emitter.indent = emitter.best_indent*((emitter.indent+emitter.best_indent)/emitter.best_indent)
+		}
+	}
+	return true
+}
+
+// State dispatcher.
+func yaml_emitter_state_machine(emitter *yaml_emitter_t, event *yaml_event_t) bool {
+	switch emitter.state {
+	default:
+	case yaml_EMIT_STREAM_START_STATE:
+		return yaml_emitter_emit_stream_start(emitter, event)
+
+	case yaml_EMIT_FIRST_DOCUMENT_START_STATE:
+		return yaml_emitter_emit_document_start(emitter, event, true)
+
+	case yaml_EMIT_DOCUMENT_START_STATE:
+		return yaml_emitter_emit_document_start(emitter, event, false)
+
+	case yaml_EMIT_DOCUMENT_CONTENT_STATE:
+		return yaml_emitter_emit_document_content(emitter, event)
+
+	case yaml_EMIT_DOCUMENT_END_STATE:
+		return yaml_emitter_emit_document_end(emitter, event)
+
+	case yaml_EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE:
+		return yaml_emitter_emit_flow_sequence_item(emitter, event, true, false)
+
+	case yaml_EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE:
+		return yaml_emitter_emit_flow_sequence_item(emitter, event, false, true)
+
+	case yaml_EMIT_FLOW_SEQUENCE_ITEM_STATE:
+		return yaml_emitter_emit_flow_sequence_item(emitter, event, false, false)
+
+	case yaml_EMIT_FLOW_MAPPING_FIRST_KEY_STATE:
+		return yaml_emitter_emit_flow_mapping_key(emitter, event, true, false)
+
+	case yaml_EMIT_FLOW_MAPPING_TRAIL_KEY_STATE:
+		return yaml_emitter_emit_flow_mapping_key(emitter, event, false, true)
+
+	case yaml_EMIT_FLOW_MAPPING_KEY_STATE:
+		return yaml_emitter_emit_flow_mapping_key(emitter, event, false, false)
+
+	case yaml_EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE:
+		return yaml_emitter_emit_flow_mapping_value(emitter, event, true)
+
+	case yaml_EMIT_FLOW_MAPPING_VALUE_STATE:
+		return yaml_emitter_emit_flow_mapping_value(emitter, event, false)
+
+	case yaml_EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE:
+		return yaml_emitter_emit_block_sequence_item(emitter, event, true)
+
+	case yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE:
+		return yaml_emitter_emit_block_sequence_item(emitter, event, false)
+
+	case yaml_EMIT_BLOCK_MAPPING_FIRST_KEY_STATE:
+		return yaml_emitter_emit_block_mapping_key(emitter, event, true)
+
+	case yaml_EMIT_BLOCK_MAPPING_KEY_STATE:
+		return yaml_emitter_emit_block_mapping_key(emitter, event, false)
+
+	case yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE:
+		return yaml_emitter_emit_block_mapping_value(emitter, event, true)
+
+	case yaml_EMIT_BLOCK_MAPPING_VALUE_STATE:
+		return yaml_emitter_emit_block_mapping_value(emitter, event, false)
+
+	case yaml_EMIT_END_STATE:
+		return yaml_emitter_set_emitter_error(emitter, "expected nothing after STREAM-END")
+	}
+	panic("invalid emitter state")
+}
+
+// Expect STREAM-START.
+func yaml_emitter_emit_stream_start(emitter *yaml_emitter_t, event *yaml_event_t) bool {
+	if event.typ != yaml_STREAM_START_EVENT {
+		return yaml_emitter_set_emitter_error(emitter, "expected STREAM-START")
+	}
+	if emitter.encoding == yaml_ANY_ENCODING {
+		emitter.encoding = event.encoding
+		if emitter.encoding == yaml_ANY_ENCODING {
+			emitter.encoding = yaml_UTF8_ENCODING
+		}
+	}
+	if emitter.best_indent < 2 || emitter.best_indent > 9 {
+		emitter.best_indent = 2
+	}
+	if emitter.best_width >= 0 && emitter.best_width <= emitter.best_indent*2 {
+		emitter.best_width = 80
+	}
+	if emitter.best_width < 0 {
+		emitter.best_width = 1<<31 - 1
+	}
+	if emitter.line_break == yaml_ANY_BREAK {
+		emitter.line_break = yaml_LN_BREAK
+	}
+
+	emitter.indent = -1
+	emitter.line = 0
+	emitter.column = 0
+	emitter.whitespace = true
+	emitter.indention = true
+	emitter.space_above = true
+	emitter.foot_indent = -1
+
+	if emitter.encoding != yaml_UTF8_ENCODING {
+		if !yaml_emitter_write_bom(emitter) {
+			return false
+		}
+	}
+	emitter.state = yaml_EMIT_FIRST_DOCUMENT_START_STATE
+	return true
+}
+
+// Expect DOCUMENT-START or STREAM-END.
+func yaml_emitter_emit_document_start(emitter *yaml_emitter_t, event *yaml_event_t, first bool) bool {
+
+	if event.typ == yaml_DOCUMENT_START_EVENT {
+
+		if event.version_directive != nil {
+			if !yaml_emitter_analyze_version_directive(emitter, event.version_directive) {
+				return false
+			}
+		}
+
+		for i := 0; i < len(event.tag_directives); i++ {
+			tag_directive := &event.tag_directives[i]
+			if !yaml_emitter_analyze_tag_directive(emitter, tag_directive) {
+				return false
+			}
+			if !yaml_emitter_append_tag_directive(emitter, tag_directive, false) {
+				return false
+			}
+		}
+
+		for i := 0; i < len(default_tag_directives); i++ {
+			tag_directive := &default_tag_directives[i]
+			if !yaml_emitter_append_tag_directive(emitter, tag_directive, true) {
+				return false
+			}
+		}
+
+		implicit := event.implicit
+		if !first || emitter.canonical {
+			implicit = false
+		}
+
+		if emitter.open_ended && (event.version_directive != nil || len(event.tag_directives) > 0) {
+			if !yaml_emitter_write_indicator(emitter, []byte("..."), true, false, false) {
+				return false
+			}
+			if !yaml_emitter_write_indent(emitter) {
+				return false
+			}
+		}
+
+		if event.version_directive != nil {
+			implicit = false
+			if !yaml_emitter_write_indicator(emitter, []byte("%YAML"), true, false, false) {
+				return false
+			}
+			if !yaml_emitter_write_indicator(emitter, []byte("1.1"), true, false, false) {
+				return false
+			}
+			if !yaml_emitter_write_indent(emitter) {
+				return false
+			}
+		}
+
+		if len(event.tag_directives) > 0 {
+			implicit = false
+			for i := 0; i < len(event.tag_directives); i++ {
+				tag_directive := &event.tag_directives[i]
+				if !yaml_emitter_write_indicator(emitter, []byte("%TAG"), true, false, false) {
+					return false
+				}
+				if !yaml_emitter_write_tag_handle(emitter, tag_directive.handle) {
+					return false
+				}
+				if !yaml_emitter_write_tag_content(emitter, tag_directive.prefix, true) {
+					return false
+				}
+				if !yaml_emitter_write_indent(emitter) {
+					return false
+				}
+			}
+		}
+
+		if yaml_emitter_check_empty_document(emitter) {
+			implicit = false
+		}
+		if !implicit {
+			if !yaml_emitter_write_indent(emitter) {
+				return false
+			}
+			if !yaml_emitter_write_indicator(emitter, []byte("---"), true, false, false) {
+				return false
+			}
+			if emitter.canonical || true {
+				if !yaml_emitter_write_indent(emitter) {
+					return false
+				}
+			}
+		}
+
+		if len(emitter.head_comment) > 0 {
+			if !yaml_emitter_process_head_comment(emitter) {
+				return false
+			}
+			if !put_break(emitter) {
+				return false
+			}
+		}
+
+		emitter.state = yaml_EMIT_DOCUMENT_CONTENT_STATE
+		return true
+	}
+
+	if event.typ == yaml_STREAM_END_EVENT {
+		if emitter.open_ended {
+			if !yaml_emitter_write_indicator(emitter, []byte("..."), true, false, false) {
+				return false
+			}
+			if !yaml_emitter_write_indent(emitter) {
+				return false
+			}
+		}
+		if !yaml_emitter_flush(emitter) {
+			return false
+		}
+		emitter.state = yaml_EMIT_END_STATE
+		return true
+	}
+
+	return yaml_emitter_set_emitter_error(emitter, "expected DOCUMENT-START or STREAM-END")
+}
+
+// Expect the root node.
+func yaml_emitter_emit_document_content(emitter *yaml_emitter_t, event *yaml_event_t) bool {
+	emitter.states = append(emitter.states, yaml_EMIT_DOCUMENT_END_STATE)
+
+	if !yaml_emitter_process_head_comment(emitter) {
+		return false
+	}
+	if !yaml_emitter_emit_node(emitter, event, true, false, false, false) {
+		return false
+	}
+	if !yaml_emitter_process_line_comment(emitter) {
+		return false
+	}
+	if !yaml_emitter_process_foot_comment(emitter) {
+		return false
+	}
+	return true
+}
+
+// Expect DOCUMENT-END.
+func yaml_emitter_emit_document_end(emitter *yaml_emitter_t, event *yaml_event_t) bool {
+	if event.typ != yaml_DOCUMENT_END_EVENT {
+		return yaml_emitter_set_emitter_error(emitter, "expected DOCUMENT-END")
+	}
+	// [Go] Force document foot separation.
+	emitter.foot_indent = 0
+	if !yaml_emitter_process_foot_comment(emitter) {
+		return false
+	}
+	emitter.foot_indent = -1
+	if !yaml_emitter_write_indent(emitter) {
+		return false
+	}
+	if !event.implicit {
+		// [Go] Allocate the slice elsewhere.
+		if !yaml_emitter_write_indicator(emitter, []byte("..."), true, false, false) {
+			return false
+		}
+		if !yaml_emitter_write_indent(emitter) {
+			return false
+		}
+	}
+	if !yaml_emitter_flush(emitter) {
+		return false
+	}
+	emitter.state = yaml_EMIT_DOCUMENT_START_STATE
+	emitter.tag_directives = emitter.tag_directives[:0]
+	return true
+}
+
+// Expect a flow item node.
+func yaml_emitter_emit_flow_sequence_item(emitter *yaml_emitter_t, event *yaml_event_t, first, trail bool) bool {
+	if first {
+		if !yaml_emitter_write_indicator(emitter, []byte{'['}, true, true, false) {
+			return false
+		}
+		if !yaml_emitter_increase_indent(emitter, true, false) {
+			return false
+		}
+		emitter.flow_level++
+	}
+
+	if event.typ == yaml_SEQUENCE_END_EVENT {
+		if emitter.canonical && !first && !trail {
+			if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) {
+				return false
+			}
+		}
+		emitter.flow_level--
+		emitter.indent = emitter.indents[len(emitter.indents)-1]
+		emitter.indents = emitter.indents[:len(emitter.indents)-1]
+		if emitter.column == 0 || emitter.canonical && !first {
+			if !yaml_emitter_write_indent(emitter) {
+				return false
+			}
+		}
+		if !yaml_emitter_write_indicator(emitter, []byte{']'}, false, false, false) {
+			return false
+		}
+		if !yaml_emitter_process_line_comment(emitter) {
+			return false
+		}
+		if !yaml_emitter_process_foot_comment(emitter) {
+			return false
+		}
+		emitter.state = emitter.states[len(emitter.states)-1]
+		emitter.states = emitter.states[:len(emitter.states)-1]
+
+		return true
+	}
+
+	if !first && !trail {
+		if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) {
+			return false
+		}
+	}
+
+	if !yaml_emitter_process_head_comment(emitter) {
+		return false
+	}
+	if emitter.column == 0 {
+		if !yaml_emitter_write_indent(emitter) {
+			return false
+		}
+	}
+
+	if emitter.canonical || emitter.column > emitter.best_width {
+		if !yaml_emitter_write_indent(emitter) {
+			return false
+		}
+	}
+	if len(emitter.line_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0 {
+		emitter.states = append(emitter.states, yaml_EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE)
+	} else {
+		emitter.states = append(emitter.states, yaml_EMIT_FLOW_SEQUENCE_ITEM_STATE)
+	}
+	if !yaml_emitter_emit_node(emitter, event, false, true, false, false) {
+		return false
+	}
+	if len(emitter.line_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0 {
+		if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) {
+			return false
+		}
+	}
+	if !yaml_emitter_process_line_comment(emitter) {
+		return false
+	}
+	if !yaml_emitter_process_foot_comment(emitter) {
+		return false
+	}
+	return true
+}
+
+// Expect a flow key node.
+func yaml_emitter_emit_flow_mapping_key(emitter *yaml_emitter_t, event *yaml_event_t, first, trail bool) bool {
+	if first {
+		if !yaml_emitter_write_indicator(emitter, []byte{'{'}, true, true, false) {
+			return false
+		}
+		if !yaml_emitter_increase_indent(emitter, true, false) {
+			return false
+		}
+		emitter.flow_level++
+	}
+
+	if event.typ == yaml_MAPPING_END_EVENT {
+		if (emitter.canonical || len(emitter.head_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0) && !first && !trail {
+			if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) {
+				return false
+			}
+		}
+		if !yaml_emitter_process_head_comment(emitter) {
+			return false
+		}
+		emitter.flow_level--
+		emitter.indent = emitter.indents[len(emitter.indents)-1]
+		emitter.indents = emitter.indents[:len(emitter.indents)-1]
+		if emitter.canonical && !first {
+			if !yaml_emitter_write_indent(emitter) {
+				return false
+			}
+		}
+		if !yaml_emitter_write_indicator(emitter, []byte{'}'}, false, false, false) {
+			return false
+		}
+		if !yaml_emitter_process_line_comment(emitter) {
+			return false
+		}
+		if !yaml_emitter_process_foot_comment(emitter) {
+			return false
+		}
+		emitter.state = emitter.states[len(emitter.states)-1]
+		emitter.states = emitter.states[:len(emitter.states)-1]
+		return true
+	}
+
+	if !first && !trail {
+		if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) {
+			return false
+		}
+	}
+
+	if !yaml_emitter_process_head_comment(emitter) {
+		return false
+	}
+
+	if emitter.column == 0 {
+		if !yaml_emitter_write_indent(emitter) {
+			return false
+		}
+	}
+
+	if emitter.canonical || emitter.column > emitter.best_width {
+		if !yaml_emitter_write_indent(emitter) {
+			return false
+		}
+	}
+
+	if !emitter.canonical && yaml_emitter_check_simple_key(emitter) {
+		emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE)
+		return yaml_emitter_emit_node(emitter, event, false, false, true, true)
+	}
+	if !yaml_emitter_write_indicator(emitter, []byte{'?'}, true, false, false) {
+		return false
+	}
+	emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_VALUE_STATE)
+	return yaml_emitter_emit_node(emitter, event, false, false, true, false)
+}
+
+// Expect a flow value node.
+func yaml_emitter_emit_flow_mapping_value(emitter *yaml_emitter_t, event *yaml_event_t, simple bool) bool {
+	if simple {
+		if !yaml_emitter_write_indicator(emitter, []byte{':'}, false, false, false) {
+			return false
+		}
+	} else {
+		if emitter.canonical || emitter.column > emitter.best_width {
+			if !yaml_emitter_write_indent(emitter) {
+				return false
+			}
+		}
+		if !yaml_emitter_write_indicator(emitter, []byte{':'}, true, false, false) {
+			return false
+		}
+	}
+	if len(emitter.line_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0 {
+		emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_TRAIL_KEY_STATE)
+	} else {
+		emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_KEY_STATE)
+	}
+	if !yaml_emitter_emit_node(emitter, event, false, false, true, false) {
+		return false
+	}
+	if len(emitter.line_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0 {
+		if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) {
+			return false
+		}
+	}
+	if !yaml_emitter_process_line_comment(emitter) {
+		return false
+	}
+	if !yaml_emitter_process_foot_comment(emitter) {
+		return false
+	}
+	return true
+}
+
+// Expect a block item node.
+func yaml_emitter_emit_block_sequence_item(emitter *yaml_emitter_t, event *yaml_event_t, first bool) bool {
+	if first {
+		if !yaml_emitter_increase_indent(emitter, false, false) {
+			return false
+		}
+	}
+	if event.typ == yaml_SEQUENCE_END_EVENT {
+		emitter.indent = emitter.indents[len(emitter.indents)-1]
+		emitter.indents = emitter.indents[:len(emitter.indents)-1]
+		emitter.state = emitter.states[len(emitter.states)-1]
+		emitter.states = emitter.states[:len(emitter.states)-1]
+		return true
+	}
+	if !yaml_emitter_process_head_comment(emitter) {
+		return false
+	}
+	if !yaml_emitter_write_indent(emitter) {
+		return false
+	}
+	if !yaml_emitter_write_indicator(emitter, []byte{'-'}, true, false, true) {
+		return false
+	}
+	emitter.states = append(emitter.states, yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE)
+	if !yaml_emitter_emit_node(emitter, event, false, true, false, false) {
+		return false
+	}
+	if !yaml_emitter_process_line_comment(emitter) {
+		return false
+	}
+	if !yaml_emitter_process_foot_comment(emitter) {
+		return false
+	}
+	return true
+}
+
+// Expect a block key node.
+func yaml_emitter_emit_block_mapping_key(emitter *yaml_emitter_t, event *yaml_event_t, first bool) bool {
+	if first {
+		if !yaml_emitter_increase_indent(emitter, false, false) {
+			return false
+		}
+	}
+	if !yaml_emitter_process_head_comment(emitter) {
+		return false
+	}
+	if event.typ == yaml_MAPPING_END_EVENT {
+		emitter.indent = emitter.indents[len(emitter.indents)-1]
+		emitter.indents = emitter.indents[:len(emitter.indents)-1]
+		emitter.state = emitter.states[len(emitter.states)-1]
+		emitter.states = emitter.states[:len(emitter.states)-1]
+		return true
+	}
+	if !yaml_emitter_write_indent(emitter) {
+		return false
+	}
+	if len(emitter.line_comment) > 0 {
+		// [Go] A line comment was provided for the key. That's unusual as the
+		//      scanner associates line comments with the value. Either way,
+		//      save the line comment and render it appropriately later.
+		emitter.key_line_comment = emitter.line_comment
+		emitter.line_comment = nil
+	}
+	if yaml_emitter_check_simple_key(emitter) {
+		emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE)
+		return yaml_emitter_emit_node(emitter, event, false, false, true, true)
+	}
+	if !yaml_emitter_write_indicator(emitter, []byte{'?'}, true, false, true) {
+		return false
+	}
+	emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_VALUE_STATE)
+	return yaml_emitter_emit_node(emitter, event, false, false, true, false)
+}
+
+// Expect a block value node.
+func yaml_emitter_emit_block_mapping_value(emitter *yaml_emitter_t, event *yaml_event_t, simple bool) bool {
+	if simple {
+		if !yaml_emitter_write_indicator(emitter, []byte{':'}, false, false, false) {
+			return false
+		}
+	} else {
+		if !yaml_emitter_write_indent(emitter) {
+			return false
+		}
+		if !yaml_emitter_write_indicator(emitter, []byte{':'}, true, false, true) {
+			return false
+		}
+	}
+	if len(emitter.key_line_comment) > 0 {
+		// [Go] Line comments are generally associated with the value, but when there's
+		//      no value on the same line as a mapping key they end up attached to the
+		//      key itself.
+		if event.typ == yaml_SCALAR_EVENT {
+			if len(emitter.line_comment) == 0 {
+				// A scalar is coming and it has no line comments by itself yet,
+				// so just let it handle the line comment as usual. If it has a
+				// line comment, we can't have both so the one from the key is lost.
+				emitter.line_comment = emitter.key_line_comment
+				emitter.key_line_comment = nil
+			}
+		} else if event.sequence_style() != yaml_FLOW_SEQUENCE_STYLE && (event.typ == yaml_MAPPING_START_EVENT || event.typ == yaml_SEQUENCE_START_EVENT) {
+			// An indented block follows, so write the comment right now.
+			emitter.line_comment, emitter.key_line_comment = emitter.key_line_comment, emitter.line_comment
+			if !yaml_emitter_process_line_comment(emitter) {
+				return false
+			}
+			emitter.line_comment, emitter.key_line_comment = emitter.key_line_comment, emitter.line_comment
+		}
+	}
+	emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_KEY_STATE)
+	if !yaml_emitter_emit_node(emitter, event, false, false, true, false) {
+		return false
+	}
+	if !yaml_emitter_process_line_comment(emitter) {
+		return false
+	}
+	if !yaml_emitter_process_foot_comment(emitter) {
+		return false
+	}
+	return true
+}
+
+func yaml_emitter_silent_nil_event(emitter *yaml_emitter_t, event *yaml_event_t) bool {
+	return event.typ == yaml_SCALAR_EVENT && event.implicit && !emitter.canonical && len(emitter.scalar_data.value) == 0
+}
+
+// Expect a node.
+func yaml_emitter_emit_node(emitter *yaml_emitter_t, event *yaml_event_t,
+	root bool, sequence bool, mapping bool, simple_key bool) bool {
+
+	emitter.root_context = root
+	emitter.sequence_context = sequence
+	emitter.mapping_context = mapping
+	emitter.simple_key_context = simple_key
+
+	switch event.typ {
+	case yaml_ALIAS_EVENT:
+		return yaml_emitter_emit_alias(emitter, event)
+	case yaml_SCALAR_EVENT:
+		return yaml_emitter_emit_scalar(emitter, event)
+	case yaml_SEQUENCE_START_EVENT:
+		return yaml_emitter_emit_sequence_start(emitter, event)
+	case yaml_MAPPING_START_EVENT:
+		return yaml_emitter_emit_mapping_start(emitter, event)
+	default:
+		return yaml_emitter_set_emitter_error(emitter,
+			fmt.Sprintf("expected SCALAR, SEQUENCE-START, MAPPING-START, or ALIAS, but got %v", event.typ))
+	}
+}
+
+// Expect ALIAS.
+func yaml_emitter_emit_alias(emitter *yaml_emitter_t, event *yaml_event_t) bool {
+	if !yaml_emitter_process_anchor(emitter) {
+		return false
+	}
+	emitter.state = emitter.states[len(emitter.states)-1]
+	emitter.states = emitter.states[:len(emitter.states)-1]
+	return true
+}
+
+// Expect SCALAR.
+func yaml_emitter_emit_scalar(emitter *yaml_emitter_t, event *yaml_event_t) bool {
+	if !yaml_emitter_select_scalar_style(emitter, event) {
+		return false
+	}
+	if !yaml_emitter_process_anchor(emitter) {
+		return false
+	}
+	if !yaml_emitter_process_tag(emitter) {
+		return false
+	}
+	if !yaml_emitter_increase_indent(emitter, true, false) {
+		return false
+	}
+	if !yaml_emitter_process_scalar(emitter) {
+		return false
+	}
+	emitter.indent = emitter.indents[len(emitter.indents)-1]
+	emitter.indents = emitter.indents[:len(emitter.indents)-1]
+	emitter.state = emitter.states[len(emitter.states)-1]
+	emitter.states = emitter.states[:len(emitter.states)-1]
+	return true
+}
+
+// Expect SEQUENCE-START.
+func yaml_emitter_emit_sequence_start(emitter *yaml_emitter_t, event *yaml_event_t) bool {
+	if !yaml_emitter_process_anchor(emitter) {
+		return false
+	}
+	if !yaml_emitter_process_tag(emitter) {
+		return false
+	}
+	if emitter.flow_level > 0 || emitter.canonical || event.sequence_style() == yaml_FLOW_SEQUENCE_STYLE ||
+		yaml_emitter_check_empty_sequence(emitter) {
+		emitter.state = yaml_EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE
+	} else {
+		emitter.state = yaml_EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE
+	}
+	return true
+}
+
+// Expect MAPPING-START.
+func yaml_emitter_emit_mapping_start(emitter *yaml_emitter_t, event *yaml_event_t) bool {
+	if !yaml_emitter_process_anchor(emitter) {
+		return false
+	}
+	if !yaml_emitter_process_tag(emitter) {
+		return false
+	}
+	if emitter.flow_level > 0 || emitter.canonical || event.mapping_style() == yaml_FLOW_MAPPING_STYLE ||
+		yaml_emitter_check_empty_mapping(emitter) {
+		emitter.state = yaml_EMIT_FLOW_MAPPING_FIRST_KEY_STATE
+	} else {
+		emitter.state = yaml_EMIT_BLOCK_MAPPING_FIRST_KEY_STATE
+	}
+	return true
+}
+
+// Check if the document content is an empty scalar.
+func yaml_emitter_check_empty_document(emitter *yaml_emitter_t) bool {
+	return false // [Go] Huh?
+}
+
+// Check if the next events represent an empty sequence.
+func yaml_emitter_check_empty_sequence(emitter *yaml_emitter_t) bool {
+	if len(emitter.events)-emitter.events_head < 2 {
+		return false
+	}
+	return emitter.events[emitter.events_head].typ == yaml_SEQUENCE_START_EVENT &&
+		emitter.events[emitter.events_head+1].typ == yaml_SEQUENCE_END_EVENT
+}
+
+// Check if the next events represent an empty mapping.
+func yaml_emitter_check_empty_mapping(emitter *yaml_emitter_t) bool {
+	if len(emitter.events)-emitter.events_head < 2 {
+		return false
+	}
+	return emitter.events[emitter.events_head].typ == yaml_MAPPING_START_EVENT &&
+		emitter.events[emitter.events_head+1].typ == yaml_MAPPING_END_EVENT
+}
+
+// Check if the next node can be expressed as a simple key.
+func yaml_emitter_check_simple_key(emitter *yaml_emitter_t) bool {
+	length := 0
+	switch emitter.events[emitter.events_head].typ {
+	case yaml_ALIAS_EVENT:
+		length += len(emitter.anchor_data.anchor)
+	case yaml_SCALAR_EVENT:
+		if emitter.scalar_data.multiline {
+			return false
+		}
+		length += len(emitter.anchor_data.anchor) +
+			len(emitter.tag_data.handle) +
+			len(emitter.tag_data.suffix) +
+			len(emitter.scalar_data.value)
+	case yaml_SEQUENCE_START_EVENT:
+		if !yaml_emitter_check_empty_sequence(emitter) {
+			return false
+		}
+		length += len(emitter.anchor_data.anchor) +
+			len(emitter.tag_data.handle) +
+			len(emitter.tag_data.suffix)
+	case yaml_MAPPING_START_EVENT:
+		if !yaml_emitter_check_empty_mapping(emitter) {
+			return false
+		}
+		length += len(emitter.anchor_data.anchor) +
+			len(emitter.tag_data.handle) +
+			len(emitter.tag_data.suffix)
+	default:
+		return false
+	}
+	return length <= 128
+}
+
+// Determine an acceptable scalar style.
+func yaml_emitter_select_scalar_style(emitter *yaml_emitter_t, event *yaml_event_t) bool {
+
+	no_tag := len(emitter.tag_data.handle) == 0 && len(emitter.tag_data.suffix) == 0
+	if no_tag && !event.implicit && !event.quoted_implicit {
+		return yaml_emitter_set_emitter_error(emitter, "neither tag nor implicit flags are specified")
+	}
+
+	style := event.scalar_style()
+	if style == yaml_ANY_SCALAR_STYLE {
+		style = yaml_PLAIN_SCALAR_STYLE
+	}
+	if emitter.canonical {
+		style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
+	}
+	if emitter.simple_key_context && emitter.scalar_data.multiline {
+		style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
+	}
+
+	if style == yaml_PLAIN_SCALAR_STYLE {
+		if emitter.flow_level > 0 && !emitter.scalar_data.flow_plain_allowed ||
+			emitter.flow_level == 0 && !emitter.scalar_data.block_plain_allowed {
+			style = yaml_SINGLE_QUOTED_SCALAR_STYLE
+		}
+		if len(emitter.scalar_data.value) == 0 && (emitter.flow_level > 0 || emitter.simple_key_context) {
+			style = yaml_SINGLE_QUOTED_SCALAR_STYLE
+		}
+		if no_tag && !event.implicit {
+			style = yaml_SINGLE_QUOTED_SCALAR_STYLE
+		}
+	}
+	if style == yaml_SINGLE_QUOTED_SCALAR_STYLE {
+		if !emitter.scalar_data.single_quoted_allowed {
+			style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
+		}
+	}
+	if style == yaml_LITERAL_SCALAR_STYLE || style == yaml_FOLDED_SCALAR_STYLE {
+		if !emitter.scalar_data.block_allowed || emitter.flow_level > 0 || emitter.simple_key_context {
+			style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
+		}
+	}
+
+	if no_tag && !event.quoted_implicit && style != yaml_PLAIN_SCALAR_STYLE {
+		emitter.tag_data.handle = []byte{'!'}
+	}
+	emitter.scalar_data.style = style
+	return true
+}
+
+// Write an anchor.
+func yaml_emitter_process_anchor(emitter *yaml_emitter_t) bool {
+	if emitter.anchor_data.anchor == nil {
+		return true
+	}
+	c := []byte{'&'}
+	if emitter.anchor_data.alias {
+		c[0] = '*'
+	}
+	if !yaml_emitter_write_indicator(emitter, c, true, false, false) {
+		return false
+	}
+	return yaml_emitter_write_anchor(emitter, emitter.anchor_data.anchor)
+}
+
+// Write a tag.
+func yaml_emitter_process_tag(emitter *yaml_emitter_t) bool {
+	if len(emitter.tag_data.handle) == 0 && len(emitter.tag_data.suffix) == 0 {
+		return true
+	}
+	if len(emitter.tag_data.handle) > 0 {
+		if !yaml_emitter_write_tag_handle(emitter, emitter.tag_data.handle) {
+			return false
+		}
+		if len(emitter.tag_data.suffix) > 0 {
+			if !yaml_emitter_write_tag_content(emitter, emitter.tag_data.suffix, false) {
+				return false
+			}
+		}
+	} else {
+		// [Go] Allocate these slices elsewhere.
+		if !yaml_emitter_write_indicator(emitter, []byte("!<"), true, false, false) {
+			return false
+		}
+		if !yaml_emitter_write_tag_content(emitter, emitter.tag_data.suffix, false) {
+			return false
+		}
+		if !yaml_emitter_write_indicator(emitter, []byte{'>'}, false, false, false) {
+			return false
+		}
+	}
+	return true
+}
+
+// Write a scalar.
+func yaml_emitter_process_scalar(emitter *yaml_emitter_t) bool {
+	switch emitter.scalar_data.style {
+	case yaml_PLAIN_SCALAR_STYLE:
+		return yaml_emitter_write_plain_scalar(emitter, emitter.scalar_data.value, !emitter.simple_key_context)
+
+	case yaml_SINGLE_QUOTED_SCALAR_STYLE:
+		return yaml_emitter_write_single_quoted_scalar(emitter, emitter.scalar_data.value, !emitter.simple_key_context)
+
+	case yaml_DOUBLE_QUOTED_SCALAR_STYLE:
+		return yaml_emitter_write_double_quoted_scalar(emitter, emitter.scalar_data.value, !emitter.simple_key_context)
+
+	case yaml_LITERAL_SCALAR_STYLE:
+		return yaml_emitter_write_literal_scalar(emitter, emitter.scalar_data.value)
+
+	case yaml_FOLDED_SCALAR_STYLE:
+		return yaml_emitter_write_folded_scalar(emitter, emitter.scalar_data.value)
+	}
+	panic("unknown scalar style")
+}
+
+// Write a head comment.
+func yaml_emitter_process_head_comment(emitter *yaml_emitter_t) bool {
+	if len(emitter.tail_comment) > 0 {
+		if !yaml_emitter_write_indent(emitter) {
+			return false
+		}
+		if !yaml_emitter_write_comment(emitter, emitter.tail_comment) {
+			return false
+		}
+		emitter.tail_comment = emitter.tail_comment[:0]
+		emitter.foot_indent = emitter.indent
+		if emitter.foot_indent < 0 {
+			emitter.foot_indent = 0
+		}
+	}
+
+	if len(emitter.head_comment) == 0 {
+		return true
+	}
+	if !yaml_emitter_write_indent(emitter) {
+		return false
+	}
+	if !yaml_emitter_write_comment(emitter, emitter.head_comment) {
+		return false
+	}
+	emitter.head_comment = emitter.head_comment[:0]
+	return true
+}
+
+// Write an line comment.
+func yaml_emitter_process_line_comment(emitter *yaml_emitter_t) bool {
+	if len(emitter.line_comment) == 0 {
+		return true
+	}
+	if !emitter.whitespace {
+		if !put(emitter, ' ') {
+			return false
+		}
+	}
+	if !yaml_emitter_write_comment(emitter, emitter.line_comment) {
+		return false
+	}
+	emitter.line_comment = emitter.line_comment[:0]
+	return true
+}
+
+// Write a foot comment.
+func yaml_emitter_process_foot_comment(emitter *yaml_emitter_t) bool {
+	if len(emitter.foot_comment) == 0 {
+		return true
+	}
+	if !yaml_emitter_write_indent(emitter) {
+		return false
+	}
+	if !yaml_emitter_write_comment(emitter, emitter.foot_comment) {
+		return false
+	}
+	emitter.foot_comment = emitter.foot_comment[:0]
+	emitter.foot_indent = emitter.indent
+	if emitter.foot_indent < 0 {
+		emitter.foot_indent = 0
+	}
+	return true
+}
+
+// Check if a %YAML directive is valid.
+func yaml_emitter_analyze_version_directive(emitter *yaml_emitter_t, version_directive *yaml_version_directive_t) bool {
+	if version_directive.major != 1 || version_directive.minor != 1 {
+		return yaml_emitter_set_emitter_error(emitter, "incompatible %YAML directive")
+	}
+	return true
+}
+
+// Check if a %TAG directive is valid.
+func yaml_emitter_analyze_tag_directive(emitter *yaml_emitter_t, tag_directive *yaml_tag_directive_t) bool {
+	handle := tag_directive.handle
+	prefix := tag_directive.prefix
+	if len(handle) == 0 {
+		return yaml_emitter_set_emitter_error(emitter, "tag handle must not be empty")
+	}
+	if handle[0] != '!' {
+		return yaml_emitter_set_emitter_error(emitter, "tag handle must start with '!'")
+	}
+	if handle[len(handle)-1] != '!' {
+		return yaml_emitter_set_emitter_error(emitter, "tag handle must end with '!'")
+	}
+	for i := 1; i < len(handle)-1; i += width(handle[i]) {
+		if !is_alpha(handle, i) {
+			return yaml_emitter_set_emitter_error(emitter, "tag handle must contain alphanumerical characters only")
+		}
+	}
+	if len(prefix) == 0 {
+		return yaml_emitter_set_emitter_error(emitter, "tag prefix must not be empty")
+	}
+	return true
+}
+
+// Check if an anchor is valid.
+func yaml_emitter_analyze_anchor(emitter *yaml_emitter_t, anchor []byte, alias bool) bool {
+	if len(anchor) == 0 {
+		problem := "anchor value must not be empty"
+		if alias {
+			problem = "alias value must not be empty"
+		}
+		return yaml_emitter_set_emitter_error(emitter, problem)
+	}
+	for i := 0; i < len(anchor); i += width(anchor[i]) {
+		if !is_alpha(anchor, i) {
+			problem := "anchor value must contain alphanumerical characters only"
+			if alias {
+				problem = "alias value must contain alphanumerical characters only"
+			}
+			return yaml_emitter_set_emitter_error(emitter, problem)
+		}
+	}
+	emitter.anchor_data.anchor = anchor
+	emitter.anchor_data.alias = alias
+	return true
+}
+
+// Check if a tag is valid.
+func yaml_emitter_analyze_tag(emitter *yaml_emitter_t, tag []byte) bool {
+	if len(tag) == 0 {
+		return yaml_emitter_set_emitter_error(emitter, "tag value must not be empty")
+	}
+	for i := 0; i < len(emitter.tag_directives); i++ {
+		tag_directive := &emitter.tag_directives[i]
+		if bytes.HasPrefix(tag, tag_directive.prefix) {
+			emitter.tag_data.handle = tag_directive.handle
+			emitter.tag_data.suffix = tag[len(tag_directive.prefix):]
+			return true
+		}
+	}
+	emitter.tag_data.suffix = tag
+	return true
+}
+
+// Check if a scalar is valid.
+func yaml_emitter_analyze_scalar(emitter *yaml_emitter_t, value []byte) bool {
+	var (
+		block_indicators   = false
+		flow_indicators    = false
+		line_breaks        = false
+		special_characters = false
+		tab_characters     = false
+
+		leading_space  = false
+		leading_break  = false
+		trailing_space = false
+		trailing_break = false
+		break_space    = false
+		space_break    = false
+
+		preceded_by_whitespace = false
+		followed_by_whitespace = false
+		previous_space         = false
+		previous_break         = false
+	)
+
+	emitter.scalar_data.value = value
+
+	if len(value) == 0 {
+		emitter.scalar_data.multiline = false
+		emitter.scalar_data.flow_plain_allowed = false
+		emitter.scalar_data.block_plain_allowed = true
+		emitter.scalar_data.single_quoted_allowed = true
+		emitter.scalar_data.block_allowed = false
+		return true
+	}
+
+	if len(value) >= 3 && ((value[0] == '-' && value[1] == '-' && value[2] == '-') || (value[0] == '.' && value[1] == '.' && value[2] == '.')) {
+		block_indicators = true
+		flow_indicators = true
+	}
+
+	preceded_by_whitespace = true
+	for i, w := 0, 0; i < len(value); i += w {
+		w = width(value[i])
+		followed_by_whitespace = i+w >= len(value) || is_blank(value, i+w)
+
+		if i == 0 {
+			switch value[i] {
+			case '#', ',', '[', ']', '{', '}', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`':
+				flow_indicators = true
+				block_indicators = true
+			case '?', ':':
+				flow_indicators = true
+				if followed_by_whitespace {
+					block_indicators = true
+				}
+			case '-':
+				if followed_by_whitespace {
+					flow_indicators = true
+					block_indicators = true
+				}
+			}
+		} else {
+			switch value[i] {
+			case ',', '?', '[', ']', '{', '}':
+				flow_indicators = true
+			case ':':
+				flow_indicators = true
+				if followed_by_whitespace {
+					block_indicators = true
+				}
+			case '#':
+				if preceded_by_whitespace {
+					flow_indicators = true
+					block_indicators = true
+				}
+			}
+		}
+
+		if value[i] == '\t' {
+			tab_characters = true
+		} else if !is_printable(value, i) || !is_ascii(value, i) && !emitter.unicode {
+			special_characters = true
+		}
+		if is_space(value, i) {
+			if i == 0 {
+				leading_space = true
+			}
+			if i+width(value[i]) == len(value) {
+				trailing_space = true
+			}
+			if previous_break {
+				break_space = true
+			}
+			previous_space = true
+			previous_break = false
+		} else if is_break(value, i) {
+			line_breaks = true
+			if i == 0 {
+				leading_break = true
+			}
+			if i+width(value[i]) == len(value) {
+				trailing_break = true
+			}
+			if previous_space {
+				space_break = true
+			}
+			previous_space = false
+			previous_break = true
+		} else {
+			previous_space = false
+			previous_break = false
+		}
+
+		// [Go]: Why 'z'? Couldn't be the end of the string as that's the loop condition.
+		preceded_by_whitespace = is_blankz(value, i)
+	}
+
+	emitter.scalar_data.multiline = line_breaks
+	emitter.scalar_data.flow_plain_allowed = true
+	emitter.scalar_data.block_plain_allowed = true
+	emitter.scalar_data.single_quoted_allowed = true
+	emitter.scalar_data.block_allowed = true
+
+	if leading_space || leading_break || trailing_space || trailing_break {
+		emitter.scalar_data.flow_plain_allowed = false
+		emitter.scalar_data.block_plain_allowed = false
+	}
+	if trailing_space {
+		emitter.scalar_data.block_allowed = false
+	}
+	if break_space {
+		emitter.scalar_data.flow_plain_allowed = false
+		emitter.scalar_data.block_plain_allowed = false
+		emitter.scalar_data.single_quoted_allowed = false
+	}
+	if space_break || tab_characters || special_characters {
+		emitter.scalar_data.flow_plain_allowed = false
+		emitter.scalar_data.block_plain_allowed = false
+		emitter.scalar_data.single_quoted_allowed = false
+	}
+	if space_break || special_characters {
+		emitter.scalar_data.block_allowed = false
+	}
+	if line_breaks {
+		emitter.scalar_data.flow_plain_allowed = false
+		emitter.scalar_data.block_plain_allowed = false
+	}
+	if flow_indicators {
+		emitter.scalar_data.flow_plain_allowed = false
+	}
+	if block_indicators {
+		emitter.scalar_data.block_plain_allowed = false
+	}
+	return true
+}
+
+// Check if the event data is valid.
+func yaml_emitter_analyze_event(emitter *yaml_emitter_t, event *yaml_event_t) bool {
+
+	emitter.anchor_data.anchor = nil
+	emitter.tag_data.handle = nil
+	emitter.tag_data.suffix = nil
+	emitter.scalar_data.value = nil
+
+	if len(event.head_comment) > 0 {
+		emitter.head_comment = event.head_comment
+	}
+	if len(event.line_comment) > 0 {
+		emitter.line_comment = event.line_comment
+	}
+	if len(event.foot_comment) > 0 {
+		emitter.foot_comment = event.foot_comment
+	}
+	if len(event.tail_comment) > 0 {
+		emitter.tail_comment = event.tail_comment
+	}
+
+	switch event.typ {
+	case yaml_ALIAS_EVENT:
+		if !yaml_emitter_analyze_anchor(emitter, event.anchor, true) {
+			return false
+		}
+
+	case yaml_SCALAR_EVENT:
+		if len(event.anchor) > 0 {
+			if !yaml_emitter_analyze_anchor(emitter, event.anchor, false) {
+				return false
+			}
+		}
+		if len(event.tag) > 0 && (emitter.canonical || (!event.implicit && !event.quoted_implicit)) {
+			if !yaml_emitter_analyze_tag(emitter, event.tag) {
+				return false
+			}
+		}
+		if !yaml_emitter_analyze_scalar(emitter, event.value) {
+			return false
+		}
+
+	case yaml_SEQUENCE_START_EVENT:
+		if len(event.anchor) > 0 {
+			if !yaml_emitter_analyze_anchor(emitter, event.anchor, false) {
+				return false
+			}
+		}
+		if len(event.tag) > 0 && (emitter.canonical || !event.implicit) {
+			if !yaml_emitter_analyze_tag(emitter, event.tag) {
+				return false
+			}
+		}
+
+	case yaml_MAPPING_START_EVENT:
+		if len(event.anchor) > 0 {
+			if !yaml_emitter_analyze_anchor(emitter, event.anchor, false) {
+				return false
+			}
+		}
+		if len(event.tag) > 0 && (emitter.canonical || !event.implicit) {
+			if !yaml_emitter_analyze_tag(emitter, event.tag) {
+				return false
+			}
+		}
+	}
+	return true
+}
+
+// Write the BOM character.
+func yaml_emitter_write_bom(emitter *yaml_emitter_t) bool {
+	if !flush(emitter) {
+		return false
+	}
+	pos := emitter.buffer_pos
+	emitter.buffer[pos+0] = '\xEF'
+	emitter.buffer[pos+1] = '\xBB'
+	emitter.buffer[pos+2] = '\xBF'
+	emitter.buffer_pos += 3
+	return true
+}
+
+func yaml_emitter_write_indent(emitter *yaml_emitter_t) bool {
+	indent := emitter.indent
+	if indent < 0 {
+		indent = 0
+	}
+	if !emitter.indention || emitter.column > indent || (emitter.column == indent && !emitter.whitespace) {
+		if !put_break(emitter) {
+			return false
+		}
+	}
+	if emitter.foot_indent == indent {
+		if !put_break(emitter) {
+			return false
+		}
+	}
+	for emitter.column < indent {
+		if !put(emitter, ' ') {
+			return false
+		}
+	}
+	emitter.whitespace = true
+	//emitter.indention = true
+	emitter.space_above = false
+	emitter.foot_indent = -1
+	return true
+}
+
+func yaml_emitter_write_indicator(emitter *yaml_emitter_t, indicator []byte, need_whitespace, is_whitespace, is_indention bool) bool {
+	if need_whitespace && !emitter.whitespace {
+		if !put(emitter, ' ') {
+			return false
+		}
+	}
+	if !write_all(emitter, indicator) {
+		return false
+	}
+	emitter.whitespace = is_whitespace
+	emitter.indention = (emitter.indention && is_indention)
+	emitter.open_ended = false
+	return true
+}
+
+func yaml_emitter_write_anchor(emitter *yaml_emitter_t, value []byte) bool {
+	if !write_all(emitter, value) {
+		return false
+	}
+	emitter.whitespace = false
+	emitter.indention = false
+	return true
+}
+
+func yaml_emitter_write_tag_handle(emitter *yaml_emitter_t, value []byte) bool {
+	if !emitter.whitespace {
+		if !put(emitter, ' ') {
+			return false
+		}
+	}
+	if !write_all(emitter, value) {
+		return false
+	}
+	emitter.whitespace = false
+	emitter.indention = false
+	return true
+}
+
+func yaml_emitter_write_tag_content(emitter *yaml_emitter_t, value []byte, need_whitespace bool) bool {
+	if need_whitespace && !emitter.whitespace {
+		if !put(emitter, ' ') {
+			return false
+		}
+	}
+	for i := 0; i < len(value); {
+		var must_write bool
+		switch value[i] {
+		case ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '_', '.', '~', '*', '\'', '(', ')', '[', ']':
+			must_write = true
+		default:
+			must_write = is_alpha(value, i)
+		}
+		if must_write {
+			if !write(emitter, value, &i) {
+				return false
+			}
+		} else {
+			w := width(value[i])
+			for k := 0; k < w; k++ {
+				octet := value[i]
+				i++
+				if !put(emitter, '%') {
+					return false
+				}
+
+				c := octet >> 4
+				if c < 10 {
+					c += '0'
+				} else {
+					c += 'A' - 10
+				}
+				if !put(emitter, c) {
+					return false
+				}
+
+				c = octet & 0x0f
+				if c < 10 {
+					c += '0'
+				} else {
+					c += 'A' - 10
+				}
+				if !put(emitter, c) {
+					return false
+				}
+			}
+		}
+	}
+	emitter.whitespace = false
+	emitter.indention = false
+	return true
+}
+
+func yaml_emitter_write_plain_scalar(emitter *yaml_emitter_t, value []byte, allow_breaks bool) bool {
+	if len(value) > 0 && !emitter.whitespace {
+		if !put(emitter, ' ') {
+			return false
+		}
+	}
+
+	spaces := false
+	breaks := false
+	for i := 0; i < len(value); {
+		if is_space(value, i) {
+			if allow_breaks && !spaces && emitter.column > emitter.best_width && !is_space(value, i+1) {
+				if !yaml_emitter_write_indent(emitter) {
+					return false
+				}
+				i += width(value[i])
+			} else {
+				if !write(emitter, value, &i) {
+					return false
+				}
+			}
+			spaces = true
+		} else if is_break(value, i) {
+			if !breaks && value[i] == '\n' {
+				if !put_break(emitter) {
+					return false
+				}
+			}
+			if !write_break(emitter, value, &i) {
+				return false
+			}
+			//emitter.indention = true
+			breaks = true
+		} else {
+			if breaks {
+				if !yaml_emitter_write_indent(emitter) {
+					return false
+				}
+			}
+			if !write(emitter, value, &i) {
+				return false
+			}
+			emitter.indention = false
+			spaces = false
+			breaks = false
+		}
+	}
+
+	if len(value) > 0 {
+		emitter.whitespace = false
+	}
+	emitter.indention = false
+	if emitter.root_context {
+		emitter.open_ended = true
+	}
+
+	return true
+}
+
+func yaml_emitter_write_single_quoted_scalar(emitter *yaml_emitter_t, value []byte, allow_breaks bool) bool {
+
+	if !yaml_emitter_write_indicator(emitter, []byte{'\''}, true, false, false) {
+		return false
+	}
+
+	spaces := false
+	breaks := false
+	for i := 0; i < len(value); {
+		if is_space(value, i) {
+			if allow_breaks && !spaces && emitter.column > emitter.best_width && i > 0 && i < len(value)-1 && !is_space(value, i+1) {
+				if !yaml_emitter_write_indent(emitter) {
+					return false
+				}
+				i += width(value[i])
+			} else {
+				if !write(emitter, value, &i) {
+					return false
+				}
+			}
+			spaces = true
+		} else if is_break(value, i) {
+			if !breaks && value[i] == '\n' {
+				if !put_break(emitter) {
+					return false
+				}
+			}
+			if !write_break(emitter, value, &i) {
+				return false
+			}
+			//emitter.indention = true
+			breaks = true
+		} else {
+			if breaks {
+				if !yaml_emitter_write_indent(emitter) {
+					return false
+				}
+			}
+			if value[i] == '\'' {
+				if !put(emitter, '\'') {
+					return false
+				}
+			}
+			if !write(emitter, value, &i) {
+				return false
+			}
+			emitter.indention = false
+			spaces = false
+			breaks = false
+		}
+	}
+	if !yaml_emitter_write_indicator(emitter, []byte{'\''}, false, false, false) {
+		return false
+	}
+	emitter.whitespace = false
+	emitter.indention = false
+	return true
+}
+
+func yaml_emitter_write_double_quoted_scalar(emitter *yaml_emitter_t, value []byte, allow_breaks bool) bool {
+	spaces := false
+	if !yaml_emitter_write_indicator(emitter, []byte{'"'}, true, false, false) {
+		return false
+	}
+
+	for i := 0; i < len(value); {
+		if !is_printable(value, i) || (!emitter.unicode && !is_ascii(value, i)) ||
+			is_bom(value, i) || is_break(value, i) ||
+			value[i] == '"' || value[i] == '\\' {
+
+			octet := value[i]
+
+			var w int
+			var v rune
+			switch {
+			case octet&0x80 == 0x00:
+				w, v = 1, rune(octet&0x7F)
+			case octet&0xE0 == 0xC0:
+				w, v = 2, rune(octet&0x1F)
+			case octet&0xF0 == 0xE0:
+				w, v = 3, rune(octet&0x0F)
+			case octet&0xF8 == 0xF0:
+				w, v = 4, rune(octet&0x07)
+			}
+			for k := 1; k < w; k++ {
+				octet = value[i+k]
+				v = (v << 6) + (rune(octet) & 0x3F)
+			}
+			i += w
+
+			if !put(emitter, '\\') {
+				return false
+			}
+
+			var ok bool
+			switch v {
+			case 0x00:
+				ok = put(emitter, '0')
+			case 0x07:
+				ok = put(emitter, 'a')
+			case 0x08:
+				ok = put(emitter, 'b')
+			case 0x09:
+				ok = put(emitter, 't')
+			case 0x0A:
+				ok = put(emitter, 'n')
+			case 0x0b:
+				ok = put(emitter, 'v')
+			case 0x0c:
+				ok = put(emitter, 'f')
+			case 0x0d:
+				ok = put(emitter, 'r')
+			case 0x1b:
+				ok = put(emitter, 'e')
+			case 0x22:
+				ok = put(emitter, '"')
+			case 0x5c:
+				ok = put(emitter, '\\')
+			case 0x85:
+				ok = put(emitter, 'N')
+			case 0xA0:
+				ok = put(emitter, '_')
+			case 0x2028:
+				ok = put(emitter, 'L')
+			case 0x2029:
+				ok = put(emitter, 'P')
+			default:
+				if v <= 0xFF {
+					ok = put(emitter, 'x')
+					w = 2
+				} else if v <= 0xFFFF {
+					ok = put(emitter, 'u')
+					w = 4
+				} else {
+					ok = put(emitter, 'U')
+					w = 8
+				}
+				for k := (w - 1) * 4; ok && k >= 0; k -= 4 {
+					digit := byte((v >> uint(k)) & 0x0F)
+					if digit < 10 {
+						ok = put(emitter, digit+'0')
+					} else {
+						ok = put(emitter, digit+'A'-10)
+					}
+				}
+			}
+			if !ok {
+				return false
+			}
+			spaces = false
+		} else if is_space(value, i) {
+			if allow_breaks && !spaces && emitter.column > emitter.best_width && i > 0 && i < len(value)-1 {
+				if !yaml_emitter_write_indent(emitter) {
+					return false
+				}
+				if is_space(value, i+1) {
+					if !put(emitter, '\\') {
+						return false
+					}
+				}
+				i += width(value[i])
+			} else if !write(emitter, value, &i) {
+				return false
+			}
+			spaces = true
+		} else {
+			if !write(emitter, value, &i) {
+				return false
+			}
+			spaces = false
+		}
+	}
+	if !yaml_emitter_write_indicator(emitter, []byte{'"'}, false, false, false) {
+		return false
+	}
+	emitter.whitespace = false
+	emitter.indention = false
+	return true
+}
+
+func yaml_emitter_write_block_scalar_hints(emitter *yaml_emitter_t, value []byte) bool {
+	if is_space(value, 0) || is_break(value, 0) {
+		indent_hint := []byte{'0' + byte(emitter.best_indent)}
+		if !yaml_emitter_write_indicator(emitter, indent_hint, false, false, false) {
+			return false
+		}
+	}
+
+	emitter.open_ended = false
+
+	var chomp_hint [1]byte
+	if len(value) == 0 {
+		chomp_hint[0] = '-'
+	} else {
+		i := len(value) - 1
+		for value[i]&0xC0 == 0x80 {
+			i--
+		}
+		if !is_break(value, i) {
+			chomp_hint[0] = '-'
+		} else if i == 0 {
+			chomp_hint[0] = '+'
+			emitter.open_ended = true
+		} else {
+			i--
+			for value[i]&0xC0 == 0x80 {
+				i--
+			}
+			if is_break(value, i) {
+				chomp_hint[0] = '+'
+				emitter.open_ended = true
+			}
+		}
+	}
+	if chomp_hint[0] != 0 {
+		if !yaml_emitter_write_indicator(emitter, chomp_hint[:], false, false, false) {
+			return false
+		}
+	}
+	return true
+}
+
+func yaml_emitter_write_literal_scalar(emitter *yaml_emitter_t, value []byte) bool {
+	if !yaml_emitter_write_indicator(emitter, []byte{'|'}, true, false, false) {
+		return false
+	}
+	if !yaml_emitter_write_block_scalar_hints(emitter, value) {
+		return false
+	}
+	if !yaml_emitter_process_line_comment(emitter) {
+		return false
+	}
+	//emitter.indention = true
+	emitter.whitespace = true
+	breaks := true
+	for i := 0; i < len(value); {
+		if is_break(value, i) {
+			if !write_break(emitter, value, &i) {
+				return false
+			}
+			//emitter.indention = true
+			breaks = true
+		} else {
+			if breaks {
+				if !yaml_emitter_write_indent(emitter) {
+					return false
+				}
+			}
+			if !write(emitter, value, &i) {
+				return false
+			}
+			emitter.indention = false
+			breaks = false
+		}
+	}
+
+	return true
+}
+
+func yaml_emitter_write_folded_scalar(emitter *yaml_emitter_t, value []byte) bool {
+	if !yaml_emitter_write_indicator(emitter, []byte{'>'}, true, false, false) {
+		return false
+	}
+	if !yaml_emitter_write_block_scalar_hints(emitter, value) {
+		return false
+	}
+	if !yaml_emitter_process_line_comment(emitter) {
+		return false
+	}
+
+	//emitter.indention = true
+	emitter.whitespace = true
+
+	breaks := true
+	leading_spaces := true
+	for i := 0; i < len(value); {
+		if is_break(value, i) {
+			if !breaks && !leading_spaces && value[i] == '\n' {
+				k := 0
+				for is_break(value, k) {
+					k += width(value[k])
+				}
+				if !is_blankz(value, k) {
+					if !put_break(emitter) {
+						return false
+					}
+				}
+			}
+			if !write_break(emitter, value, &i) {
+				return false
+			}
+			//emitter.indention = true
+			breaks = true
+		} else {
+			if breaks {
+				if !yaml_emitter_write_indent(emitter) {
+					return false
+				}
+				leading_spaces = is_blank(value, i)
+			}
+			if !breaks && is_space(value, i) && !is_space(value, i+1) && emitter.column > emitter.best_width {
+				if !yaml_emitter_write_indent(emitter) {
+					return false
+				}
+				i += width(value[i])
+			} else {
+				if !write(emitter, value, &i) {
+					return false
+				}
+			}
+			emitter.indention = false
+			breaks = false
+		}
+	}
+	return true
+}
+
+func yaml_emitter_write_comment(emitter *yaml_emitter_t, comment []byte) bool {
+	breaks := false
+	pound := false
+	for i := 0; i < len(comment); {
+		if is_break(comment, i) {
+			if !write_break(emitter, comment, &i) {
+				return false
+			}
+			//emitter.indention = true
+			breaks = true
+			pound = false
+		} else {
+			if breaks && !yaml_emitter_write_indent(emitter) {
+				return false
+			}
+			if !pound {
+				if comment[i] != '#' && (!put(emitter, '#') || !put(emitter, ' ')) {
+					return false
+				}
+				pound = true
+			}
+			if !write(emitter, comment, &i) {
+				return false
+			}
+			emitter.indention = false
+			breaks = false
+		}
+	}
+	if !breaks && !put_break(emitter) {
+		return false
+	}
+
+	emitter.whitespace = true
+	//emitter.indention = true
+	return true
+}
diff --git a/vendor/gopkg.in/yaml.v3/encode.go b/vendor/gopkg.in/yaml.v3/encode.go
new file mode 100644
index 000000000..de9e72a3e
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/encode.go
@@ -0,0 +1,577 @@
+//
+// Copyright (c) 2011-2019 Canonical Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package yaml
+
+import (
+	"encoding"
+	"fmt"
+	"io"
+	"reflect"
+	"regexp"
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+	"unicode/utf8"
+)
+
+type encoder struct {
+	emitter  yaml_emitter_t
+	event    yaml_event_t
+	out      []byte
+	flow     bool
+	indent   int
+	doneInit bool
+}
+
+func newEncoder() *encoder {
+	e := &encoder{}
+	yaml_emitter_initialize(&e.emitter)
+	yaml_emitter_set_output_string(&e.emitter, &e.out)
+	yaml_emitter_set_unicode(&e.emitter, true)
+	return e
+}
+
+func newEncoderWithWriter(w io.Writer) *encoder {
+	e := &encoder{}
+	yaml_emitter_initialize(&e.emitter)
+	yaml_emitter_set_output_writer(&e.emitter, w)
+	yaml_emitter_set_unicode(&e.emitter, true)
+	return e
+}
+
+func (e *encoder) init() {
+	if e.doneInit {
+		return
+	}
+	if e.indent == 0 {
+		e.indent = 4
+	}
+	e.emitter.best_indent = e.indent
+	yaml_stream_start_event_initialize(&e.event, yaml_UTF8_ENCODING)
+	e.emit()
+	e.doneInit = true
+}
+
+func (e *encoder) finish() {
+	e.emitter.open_ended = false
+	yaml_stream_end_event_initialize(&e.event)
+	e.emit()
+}
+
+func (e *encoder) destroy() {
+	yaml_emitter_delete(&e.emitter)
+}
+
+func (e *encoder) emit() {
+	// This will internally delete the e.event value.
+	e.must(yaml_emitter_emit(&e.emitter, &e.event))
+}
+
+func (e *encoder) must(ok bool) {
+	if !ok {
+		msg := e.emitter.problem
+		if msg == "" {
+			msg = "unknown problem generating YAML content"
+		}
+		failf("%s", msg)
+	}
+}
+
+func (e *encoder) marshalDoc(tag string, in reflect.Value) {
+	e.init()
+	var node *Node
+	if in.IsValid() {
+		node, _ = in.Interface().(*Node)
+	}
+	if node != nil && node.Kind == DocumentNode {
+		e.nodev(in)
+	} else {
+		yaml_document_start_event_initialize(&e.event, nil, nil, true)
+		e.emit()
+		e.marshal(tag, in)
+		yaml_document_end_event_initialize(&e.event, true)
+		e.emit()
+	}
+}
+
+func (e *encoder) marshal(tag string, in reflect.Value) {
+	tag = shortTag(tag)
+	if !in.IsValid() || in.Kind() == reflect.Ptr && in.IsNil() {
+		e.nilv()
+		return
+	}
+	iface := in.Interface()
+	switch value := iface.(type) {
+	case *Node:
+		e.nodev(in)
+		return
+	case Node:
+		if !in.CanAddr() {
+			var n = reflect.New(in.Type()).Elem()
+			n.Set(in)
+			in = n
+		}
+		e.nodev(in.Addr())
+		return
+	case time.Time:
+		e.timev(tag, in)
+		return
+	case *time.Time:
+		e.timev(tag, in.Elem())
+		return
+	case time.Duration:
+		e.stringv(tag, reflect.ValueOf(value.String()))
+		return
+	case Marshaler:
+		v, err := value.MarshalYAML()
+		if err != nil {
+			fail(err)
+		}
+		if v == nil {
+			e.nilv()
+			return
+		}
+		e.marshal(tag, reflect.ValueOf(v))
+		return
+	case encoding.TextMarshaler:
+		text, err := value.MarshalText()
+		if err != nil {
+			fail(err)
+		}
+		in = reflect.ValueOf(string(text))
+	case nil:
+		e.nilv()
+		return
+	}
+	switch in.Kind() {
+	case reflect.Interface:
+		e.marshal(tag, in.Elem())
+	case reflect.Map:
+		e.mapv(tag, in)
+	case reflect.Ptr:
+		e.marshal(tag, in.Elem())
+	case reflect.Struct:
+		e.structv(tag, in)
+	case reflect.Slice, reflect.Array:
+		e.slicev(tag, in)
+	case reflect.String:
+		e.stringv(tag, in)
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		e.intv(tag, in)
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		e.uintv(tag, in)
+	case reflect.Float32, reflect.Float64:
+		e.floatv(tag, in)
+	case reflect.Bool:
+		e.boolv(tag, in)
+	default:
+		panic("cannot marshal type: " + in.Type().String())
+	}
+}
+
+func (e *encoder) mapv(tag string, in reflect.Value) {
+	e.mappingv(tag, func() {
+		keys := keyList(in.MapKeys())
+		sort.Sort(keys)
+		for _, k := range keys {
+			e.marshal("", k)
+			e.marshal("", in.MapIndex(k))
+		}
+	})
+}
+
+func (e *encoder) fieldByIndex(v reflect.Value, index []int) (field reflect.Value) {
+	for _, num := range index {
+		for {
+			if v.Kind() == reflect.Ptr {
+				if v.IsNil() {
+					return reflect.Value{}
+				}
+				v = v.Elem()
+				continue
+			}
+			break
+		}
+		v = v.Field(num)
+	}
+	return v
+}
+
+func (e *encoder) structv(tag string, in reflect.Value) {
+	sinfo, err := getStructInfo(in.Type())
+	if err != nil {
+		panic(err)
+	}
+	e.mappingv(tag, func() {
+		for _, info := range sinfo.FieldsList {
+			var value reflect.Value
+			if info.Inline == nil {
+				value = in.Field(info.Num)
+			} else {
+				value = e.fieldByIndex(in, info.Inline)
+				if !value.IsValid() {
+					continue
+				}
+			}
+			if info.OmitEmpty && isZero(value) {
+				continue
+			}
+			e.marshal("", reflect.ValueOf(info.Key))
+			e.flow = info.Flow
+			e.marshal("", value)
+		}
+		if sinfo.InlineMap >= 0 {
+			m := in.Field(sinfo.InlineMap)
+			if m.Len() > 0 {
+				e.flow = false
+				keys := keyList(m.MapKeys())
+				sort.Sort(keys)
+				for _, k := range keys {
+					if _, found := sinfo.FieldsMap[k.String()]; found {
+						panic(fmt.Sprintf("cannot have key %q in inlined map: conflicts with struct field", k.String()))
+					}
+					e.marshal("", k)
+					e.flow = false
+					e.marshal("", m.MapIndex(k))
+				}
+			}
+		}
+	})
+}
+
+func (e *encoder) mappingv(tag string, f func()) {
+	implicit := tag == ""
+	style := yaml_BLOCK_MAPPING_STYLE
+	if e.flow {
+		e.flow = false
+		style = yaml_FLOW_MAPPING_STYLE
+	}
+	yaml_mapping_start_event_initialize(&e.event, nil, []byte(tag), implicit, style)
+	e.emit()
+	f()
+	yaml_mapping_end_event_initialize(&e.event)
+	e.emit()
+}
+
+func (e *encoder) slicev(tag string, in reflect.Value) {
+	implicit := tag == ""
+	style := yaml_BLOCK_SEQUENCE_STYLE
+	if e.flow {
+		e.flow = false
+		style = yaml_FLOW_SEQUENCE_STYLE
+	}
+	e.must(yaml_sequence_start_event_initialize(&e.event, nil, []byte(tag), implicit, style))
+	e.emit()
+	n := in.Len()
+	for i := 0; i < n; i++ {
+		e.marshal("", in.Index(i))
+	}
+	e.must(yaml_sequence_end_event_initialize(&e.event))
+	e.emit()
+}
+
+// isBase60 returns whether s is in base 60 notation as defined in YAML 1.1.
+//
+// The base 60 float notation in YAML 1.1 is a terrible idea and is unsupported
+// in YAML 1.2 and by this package, but these should be marshalled quoted for
+// the time being for compatibility with other parsers.
+func isBase60Float(s string) (result bool) {
+	// Fast path.
+	if s == "" {
+		return false
+	}
+	c := s[0]
+	if !(c == '+' || c == '-' || c >= '0' && c <= '9') || strings.IndexByte(s, ':') < 0 {
+		return false
+	}
+	// Do the full match.
+	return base60float.MatchString(s)
+}
+
+// From http://yaml.org/type/float.html, except the regular expression there
+// is bogus. In practice parsers do not enforce the "\.[0-9_]*" suffix.
+var base60float = regexp.MustCompile(`^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+(?:\.[0-9_]*)?$`)
+
+// isOldBool returns whether s is bool notation as defined in YAML 1.1.
+//
+// We continue to force strings that YAML 1.1 would interpret as booleans to be
+// rendered as quotes strings so that the marshalled output valid for YAML 1.1
+// parsing.
+func isOldBool(s string) (result bool) {
+	switch s {
+	case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON",
+		"n", "N", "no", "No", "NO", "off", "Off", "OFF":
+		return true
+	default:
+		return false
+	}
+}
+
+func (e *encoder) stringv(tag string, in reflect.Value) {
+	var style yaml_scalar_style_t
+	s := in.String()
+	canUsePlain := true
+	switch {
+	case !utf8.ValidString(s):
+		if tag == binaryTag {
+			failf("explicitly tagged !!binary data must be base64-encoded")
+		}
+		if tag != "" {
+			failf("cannot marshal invalid UTF-8 data as %s", shortTag(tag))
+		}
+		// It can't be encoded directly as YAML so use a binary tag
+		// and encode it as base64.
+		tag = binaryTag
+		s = encodeBase64(s)
+	case tag == "":
+		// Check to see if it would resolve to a specific
+		// tag when encoded unquoted. If it doesn't,
+		// there's no need to quote it.
+		rtag, _ := resolve("", s)
+		canUsePlain = rtag == strTag && !(isBase60Float(s) || isOldBool(s))
+	}
+	// Note: it's possible for user code to emit invalid YAML
+	// if they explicitly specify a tag and a string containing
+	// text that's incompatible with that tag.
+	switch {
+	case strings.Contains(s, "\n"):
+		if e.flow {
+			style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
+		} else {
+			style = yaml_LITERAL_SCALAR_STYLE
+		}
+	case canUsePlain:
+		style = yaml_PLAIN_SCALAR_STYLE
+	default:
+		style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
+	}
+	e.emitScalar(s, "", tag, style, nil, nil, nil, nil)
+}
+
+func (e *encoder) boolv(tag string, in reflect.Value) {
+	var s string
+	if in.Bool() {
+		s = "true"
+	} else {
+		s = "false"
+	}
+	e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil)
+}
+
+func (e *encoder) intv(tag string, in reflect.Value) {
+	s := strconv.FormatInt(in.Int(), 10)
+	e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil)
+}
+
+func (e *encoder) uintv(tag string, in reflect.Value) {
+	s := strconv.FormatUint(in.Uint(), 10)
+	e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil)
+}
+
+func (e *encoder) timev(tag string, in reflect.Value) {
+	t := in.Interface().(time.Time)
+	s := t.Format(time.RFC3339Nano)
+	e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil)
+}
+
+func (e *encoder) floatv(tag string, in reflect.Value) {
+	// Issue #352: When formatting, use the precision of the underlying value
+	precision := 64
+	if in.Kind() == reflect.Float32 {
+		precision = 32
+	}
+
+	s := strconv.FormatFloat(in.Float(), 'g', -1, precision)
+	switch s {
+	case "+Inf":
+		s = ".inf"
+	case "-Inf":
+		s = "-.inf"
+	case "NaN":
+		s = ".nan"
+	}
+	e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil)
+}
+
+func (e *encoder) nilv() {
+	e.emitScalar("null", "", "", yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil)
+}
+
+func (e *encoder) emitScalar(value, anchor, tag string, style yaml_scalar_style_t, head, line, foot, tail []byte) {
+	// TODO Kill this function. Replace all initialize calls by their underlining Go literals.
+	implicit := tag == ""
+	if !implicit {
+		tag = longTag(tag)
+	}
+	e.must(yaml_scalar_event_initialize(&e.event, []byte(anchor), []byte(tag), []byte(value), implicit, implicit, style))
+	e.event.head_comment = head
+	e.event.line_comment = line
+	e.event.foot_comment = foot
+	e.event.tail_comment = tail
+	e.emit()
+}
+
+func (e *encoder) nodev(in reflect.Value) {
+	e.node(in.Interface().(*Node), "")
+}
+
+func (e *encoder) node(node *Node, tail string) {
+	// Zero nodes behave as nil.
+	if node.Kind == 0 && node.IsZero() {
+		e.nilv()
+		return
+	}
+
+	// If the tag was not explicitly requested, and dropping it won't change the
+	// implicit tag of the value, don't include it in the presentation.
+	var tag = node.Tag
+	var stag = shortTag(tag)
+	var forceQuoting bool
+	if tag != "" && node.Style&TaggedStyle == 0 {
+		if node.Kind == ScalarNode {
+			if stag == strTag && node.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0 {
+				tag = ""
+			} else {
+				rtag, _ := resolve("", node.Value)
+				if rtag == stag {
+					tag = ""
+				} else if stag == strTag {
+					tag = ""
+					forceQuoting = true
+				}
+			}
+		} else {
+			var rtag string
+			switch node.Kind {
+			case MappingNode:
+				rtag = mapTag
+			case SequenceNode:
+				rtag = seqTag
+			}
+			if rtag == stag {
+				tag = ""
+			}
+		}
+	}
+
+	switch node.Kind {
+	case DocumentNode:
+		yaml_document_start_event_initialize(&e.event, nil, nil, true)
+		e.event.head_comment = []byte(node.HeadComment)
+		e.emit()
+		for _, node := range node.Content {
+			e.node(node, "")
+		}
+		yaml_document_end_event_initialize(&e.event, true)
+		e.event.foot_comment = []byte(node.FootComment)
+		e.emit()
+
+	case SequenceNode:
+		style := yaml_BLOCK_SEQUENCE_STYLE
+		if node.Style&FlowStyle != 0 {
+			style = yaml_FLOW_SEQUENCE_STYLE
+		}
+		e.must(yaml_sequence_start_event_initialize(&e.event, []byte(node.Anchor), []byte(longTag(tag)), tag == "", style))
+		e.event.head_comment = []byte(node.HeadComment)
+		e.emit()
+		for _, node := range node.Content {
+			e.node(node, "")
+		}
+		e.must(yaml_sequence_end_event_initialize(&e.event))
+		e.event.line_comment = []byte(node.LineComment)
+		e.event.foot_comment = []byte(node.FootComment)
+		e.emit()
+
+	case MappingNode:
+		style := yaml_BLOCK_MAPPING_STYLE
+		if node.Style&FlowStyle != 0 {
+			style = yaml_FLOW_MAPPING_STYLE
+		}
+		yaml_mapping_start_event_initialize(&e.event, []byte(node.Anchor), []byte(longTag(tag)), tag == "", style)
+		e.event.tail_comment = []byte(tail)
+		e.event.head_comment = []byte(node.HeadComment)
+		e.emit()
+
+		// The tail logic below moves the foot comment of prior keys to the following key,
+		// since the value for each key may be a nested structure and the foot needs to be
+		// processed only the entirety of the value is streamed. The last tail is processed
+		// with the mapping end event.
+		var tail string
+		for i := 0; i+1 < len(node.Content); i += 2 {
+			k := node.Content[i]
+			foot := k.FootComment
+			if foot != "" {
+				kopy := *k
+				kopy.FootComment = ""
+				k = &kopy
+			}
+			e.node(k, tail)
+			tail = foot
+
+			v := node.Content[i+1]
+			e.node(v, "")
+		}
+
+		yaml_mapping_end_event_initialize(&e.event)
+		e.event.tail_comment = []byte(tail)
+		e.event.line_comment = []byte(node.LineComment)
+		e.event.foot_comment = []byte(node.FootComment)
+		e.emit()
+
+	case AliasNode:
+		yaml_alias_event_initialize(&e.event, []byte(node.Value))
+		e.event.head_comment = []byte(node.HeadComment)
+		e.event.line_comment = []byte(node.LineComment)
+		e.event.foot_comment = []byte(node.FootComment)
+		e.emit()
+
+	case ScalarNode:
+		value := node.Value
+		if !utf8.ValidString(value) {
+			if stag == binaryTag {
+				failf("explicitly tagged !!binary data must be base64-encoded")
+			}
+			if stag != "" {
+				failf("cannot marshal invalid UTF-8 data as %s", stag)
+			}
+			// It can't be encoded directly as YAML so use a binary tag
+			// and encode it as base64.
+			tag = binaryTag
+			value = encodeBase64(value)
+		}
+
+		style := yaml_PLAIN_SCALAR_STYLE
+		switch {
+		case node.Style&DoubleQuotedStyle != 0:
+			style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
+		case node.Style&SingleQuotedStyle != 0:
+			style = yaml_SINGLE_QUOTED_SCALAR_STYLE
+		case node.Style&LiteralStyle != 0:
+			style = yaml_LITERAL_SCALAR_STYLE
+		case node.Style&FoldedStyle != 0:
+			style = yaml_FOLDED_SCALAR_STYLE
+		case strings.Contains(value, "\n"):
+			style = yaml_LITERAL_SCALAR_STYLE
+		case forceQuoting:
+			style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
+		}
+
+		e.emitScalar(value, node.Anchor, tag, style, []byte(node.HeadComment), []byte(node.LineComment), []byte(node.FootComment), []byte(tail))
+	default:
+		failf("cannot encode node with unknown kind %d", node.Kind)
+	}
+}
diff --git a/vendor/gopkg.in/yaml.v3/parserc.go b/vendor/gopkg.in/yaml.v3/parserc.go
new file mode 100644
index 000000000..268558a0d
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/parserc.go
@@ -0,0 +1,1258 @@
+//
+// Copyright (c) 2011-2019 Canonical Ltd
+// Copyright (c) 2006-2010 Kirill Simonov
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package yaml
+
+import (
+	"bytes"
+)
+
+// The parser implements the following grammar:
+//
+// stream               ::= STREAM-START implicit_document? explicit_document* STREAM-END
+// implicit_document    ::= block_node DOCUMENT-END*
+// explicit_document    ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
+// block_node_or_indentless_sequence    ::=
+//                          ALIAS
+//                          | properties (block_content | indentless_block_sequence)?
+//                          | block_content
+//                          | indentless_block_sequence
+// block_node           ::= ALIAS
+//                          | properties block_content?
+//                          | block_content
+// flow_node            ::= ALIAS
+//                          | properties flow_content?
+//                          | flow_content
+// properties           ::= TAG ANCHOR? | ANCHOR TAG?
+// block_content        ::= block_collection | flow_collection | SCALAR
+// flow_content         ::= flow_collection | SCALAR
+// block_collection     ::= block_sequence | block_mapping
+// flow_collection      ::= flow_sequence | flow_mapping
+// block_sequence       ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END
+// indentless_sequence  ::= (BLOCK-ENTRY block_node?)+
+// block_mapping        ::= BLOCK-MAPPING_START
+//                          ((KEY block_node_or_indentless_sequence?)?
+//                          (VALUE block_node_or_indentless_sequence?)?)*
+//                          BLOCK-END
+// flow_sequence        ::= FLOW-SEQUENCE-START
+//                          (flow_sequence_entry FLOW-ENTRY)*
+//                          flow_sequence_entry?
+//                          FLOW-SEQUENCE-END
+// flow_sequence_entry  ::= flow_node | KEY flow_node? (VALUE flow_node?)?
+// flow_mapping         ::= FLOW-MAPPING-START
+//                          (flow_mapping_entry FLOW-ENTRY)*
+//                          flow_mapping_entry?
+//                          FLOW-MAPPING-END
+// flow_mapping_entry   ::= flow_node | KEY flow_node? (VALUE flow_node?)?
+
+// Peek the next token in the token queue.
+func peek_token(parser *yaml_parser_t) *yaml_token_t {
+	if parser.token_available || yaml_parser_fetch_more_tokens(parser) {
+		token := &parser.tokens[parser.tokens_head]
+		yaml_parser_unfold_comments(parser, token)
+		return token
+	}
+	return nil
+}
+
+// yaml_parser_unfold_comments walks through the comments queue and joins all
+// comments behind the position of the provided token into the respective
+// top-level comment slices in the parser.
+func yaml_parser_unfold_comments(parser *yaml_parser_t, token *yaml_token_t) {
+	for parser.comments_head < len(parser.comments) && token.start_mark.index >= parser.comments[parser.comments_head].token_mark.index {
+		comment := &parser.comments[parser.comments_head]
+		if len(comment.head) > 0 {
+			if token.typ == yaml_BLOCK_END_TOKEN {
+				// No heads on ends, so keep comment.head for a follow up token.
+				break
+			}
+			if len(parser.head_comment) > 0 {
+				parser.head_comment = append(parser.head_comment, '\n')
+			}
+			parser.head_comment = append(parser.head_comment, comment.head...)
+		}
+		if len(comment.foot) > 0 {
+			if len(parser.foot_comment) > 0 {
+				parser.foot_comment = append(parser.foot_comment, '\n')
+			}
+			parser.foot_comment = append(parser.foot_comment, comment.foot...)
+		}
+		if len(comment.line) > 0 {
+			if len(parser.line_comment) > 0 {
+				parser.line_comment = append(parser.line_comment, '\n')
+			}
+			parser.line_comment = append(parser.line_comment, comment.line...)
+		}
+		*comment = yaml_comment_t{}
+		parser.comments_head++
+	}
+}
+
+// Remove the next token from the queue (must be called after peek_token).
+func skip_token(parser *yaml_parser_t) {
+	parser.token_available = false
+	parser.tokens_parsed++
+	parser.stream_end_produced = parser.tokens[parser.tokens_head].typ == yaml_STREAM_END_TOKEN
+	parser.tokens_head++
+}
+
+// Get the next event.
+func yaml_parser_parse(parser *yaml_parser_t, event *yaml_event_t) bool {
+	// Erase the event object.
+	*event = yaml_event_t{}
+
+	// No events after the end of the stream or error.
+	if parser.stream_end_produced || parser.error != yaml_NO_ERROR || parser.state == yaml_PARSE_END_STATE {
+		return true
+	}
+
+	// Generate the next event.
+	return yaml_parser_state_machine(parser, event)
+}
+
+// Set parser error.
+func yaml_parser_set_parser_error(parser *yaml_parser_t, problem string, problem_mark yaml_mark_t) bool {
+	parser.error = yaml_PARSER_ERROR
+	parser.problem = problem
+	parser.problem_mark = problem_mark
+	return false
+}
+
+func yaml_parser_set_parser_error_context(parser *yaml_parser_t, context string, context_mark yaml_mark_t, problem string, problem_mark yaml_mark_t) bool {
+	parser.error = yaml_PARSER_ERROR
+	parser.context = context
+	parser.context_mark = context_mark
+	parser.problem = problem
+	parser.problem_mark = problem_mark
+	return false
+}
+
+// State dispatcher.
+func yaml_parser_state_machine(parser *yaml_parser_t, event *yaml_event_t) bool {
+	//trace("yaml_parser_state_machine", "state:", parser.state.String())
+
+	switch parser.state {
+	case yaml_PARSE_STREAM_START_STATE:
+		return yaml_parser_parse_stream_start(parser, event)
+
+	case yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE:
+		return yaml_parser_parse_document_start(parser, event, true)
+
+	case yaml_PARSE_DOCUMENT_START_STATE:
+		return yaml_parser_parse_document_start(parser, event, false)
+
+	case yaml_PARSE_DOCUMENT_CONTENT_STATE:
+		return yaml_parser_parse_document_content(parser, event)
+
+	case yaml_PARSE_DOCUMENT_END_STATE:
+		return yaml_parser_parse_document_end(parser, event)
+
+	case yaml_PARSE_BLOCK_NODE_STATE:
+		return yaml_parser_parse_node(parser, event, true, false)
+
+	case yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE:
+		return yaml_parser_parse_node(parser, event, true, true)
+
+	case yaml_PARSE_FLOW_NODE_STATE:
+		return yaml_parser_parse_node(parser, event, false, false)
+
+	case yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE:
+		return yaml_parser_parse_block_sequence_entry(parser, event, true)
+
+	case yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE:
+		return yaml_parser_parse_block_sequence_entry(parser, event, false)
+
+	case yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE:
+		return yaml_parser_parse_indentless_sequence_entry(parser, event)
+
+	case yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE:
+		return yaml_parser_parse_block_mapping_key(parser, event, true)
+
+	case yaml_PARSE_BLOCK_MAPPING_KEY_STATE:
+		return yaml_parser_parse_block_mapping_key(parser, event, false)
+
+	case yaml_PARSE_BLOCK_MAPPING_VALUE_STATE:
+		return yaml_parser_parse_block_mapping_value(parser, event)
+
+	case yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE:
+		return yaml_parser_parse_flow_sequence_entry(parser, event, true)
+
+	case yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE:
+		return yaml_parser_parse_flow_sequence_entry(parser, event, false)
+
+	case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE:
+		return yaml_parser_parse_flow_sequence_entry_mapping_key(parser, event)
+
+	case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE:
+		return yaml_parser_parse_flow_sequence_entry_mapping_value(parser, event)
+
+	case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE:
+		return yaml_parser_parse_flow_sequence_entry_mapping_end(parser, event)
+
+	case yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE:
+		return yaml_parser_parse_flow_mapping_key(parser, event, true)
+
+	case yaml_PARSE_FLOW_MAPPING_KEY_STATE:
+		return yaml_parser_parse_flow_mapping_key(parser, event, false)
+
+	case yaml_PARSE_FLOW_MAPPING_VALUE_STATE:
+		return yaml_parser_parse_flow_mapping_value(parser, event, false)
+
+	case yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE:
+		return yaml_parser_parse_flow_mapping_value(parser, event, true)
+
+	default:
+		panic("invalid parser state")
+	}
+}
+
+// Parse the production:
+// stream   ::= STREAM-START implicit_document? explicit_document* STREAM-END
+//              ************
+func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t) bool {
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+	if token.typ != yaml_STREAM_START_TOKEN {
+		return yaml_parser_set_parser_error(parser, "did not find expected <stream-start>", token.start_mark)
+	}
+	parser.state = yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE
+	*event = yaml_event_t{
+		typ:        yaml_STREAM_START_EVENT,
+		start_mark: token.start_mark,
+		end_mark:   token.end_mark,
+		encoding:   token.encoding,
+	}
+	skip_token(parser)
+	return true
+}
+
+// Parse the productions:
+// implicit_document    ::= block_node DOCUMENT-END*
+//                          *
+// explicit_document    ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
+//                          *************************
+func yaml_parser_parse_document_start(parser *yaml_parser_t, event *yaml_event_t, implicit bool) bool {
+
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+
+	// Parse extra document end indicators.
+	if !implicit {
+		for token.typ == yaml_DOCUMENT_END_TOKEN {
+			skip_token(parser)
+			token = peek_token(parser)
+			if token == nil {
+				return false
+			}
+		}
+	}
+
+	if implicit && token.typ != yaml_VERSION_DIRECTIVE_TOKEN &&
+		token.typ != yaml_TAG_DIRECTIVE_TOKEN &&
+		token.typ != yaml_DOCUMENT_START_TOKEN &&
+		token.typ != yaml_STREAM_END_TOKEN {
+		// Parse an implicit document.
+		if !yaml_parser_process_directives(parser, nil, nil) {
+			return false
+		}
+		parser.states = append(parser.states, yaml_PARSE_DOCUMENT_END_STATE)
+		parser.state = yaml_PARSE_BLOCK_NODE_STATE
+
+		var head_comment []byte
+		if len(parser.head_comment) > 0 {
+			// [Go] Scan the header comment backwards, and if an empty line is found, break
+			//      the header so the part before the last empty line goes into the
+			//      document header, while the bottom of it goes into a follow up event.
+			for i := len(parser.head_comment) - 1; i > 0; i-- {
+				if parser.head_comment[i] == '\n' {
+					if i == len(parser.head_comment)-1 {
+						head_comment = parser.head_comment[:i]
+						parser.head_comment = parser.head_comment[i+1:]
+						break
+					} else if parser.head_comment[i-1] == '\n' {
+						head_comment = parser.head_comment[:i-1]
+						parser.head_comment = parser.head_comment[i+1:]
+						break
+					}
+				}
+			}
+		}
+
+		*event = yaml_event_t{
+			typ:        yaml_DOCUMENT_START_EVENT,
+			start_mark: token.start_mark,
+			end_mark:   token.end_mark,
+
+			head_comment: head_comment,
+		}
+
+	} else if token.typ != yaml_STREAM_END_TOKEN {
+		// Parse an explicit document.
+		var version_directive *yaml_version_directive_t
+		var tag_directives []yaml_tag_directive_t
+		start_mark := token.start_mark
+		if !yaml_parser_process_directives(parser, &version_directive, &tag_directives) {
+			return false
+		}
+		token = peek_token(parser)
+		if token == nil {
+			return false
+		}
+		if token.typ != yaml_DOCUMENT_START_TOKEN {
+			yaml_parser_set_parser_error(parser,
+				"did not find expected <document start>", token.start_mark)
+			return false
+		}
+		parser.states = append(parser.states, yaml_PARSE_DOCUMENT_END_STATE)
+		parser.state = yaml_PARSE_DOCUMENT_CONTENT_STATE
+		end_mark := token.end_mark
+
+		*event = yaml_event_t{
+			typ:               yaml_DOCUMENT_START_EVENT,
+			start_mark:        start_mark,
+			end_mark:          end_mark,
+			version_directive: version_directive,
+			tag_directives:    tag_directives,
+			implicit:          false,
+		}
+		skip_token(parser)
+
+	} else {
+		// Parse the stream end.
+		parser.state = yaml_PARSE_END_STATE
+		*event = yaml_event_t{
+			typ:        yaml_STREAM_END_EVENT,
+			start_mark: token.start_mark,
+			end_mark:   token.end_mark,
+		}
+		skip_token(parser)
+	}
+
+	return true
+}
+
+// Parse the productions:
+// explicit_document    ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
+//                                                    ***********
+//
+func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event_t) bool {
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+
+	if token.typ == yaml_VERSION_DIRECTIVE_TOKEN ||
+		token.typ == yaml_TAG_DIRECTIVE_TOKEN ||
+		token.typ == yaml_DOCUMENT_START_TOKEN ||
+		token.typ == yaml_DOCUMENT_END_TOKEN ||
+		token.typ == yaml_STREAM_END_TOKEN {
+		parser.state = parser.states[len(parser.states)-1]
+		parser.states = parser.states[:len(parser.states)-1]
+		return yaml_parser_process_empty_scalar(parser, event,
+			token.start_mark)
+	}
+	return yaml_parser_parse_node(parser, event, true, false)
+}
+
+// Parse the productions:
+// implicit_document    ::= block_node DOCUMENT-END*
+//                                     *************
+// explicit_document    ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
+//
+func yaml_parser_parse_document_end(parser *yaml_parser_t, event *yaml_event_t) bool {
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+
+	start_mark := token.start_mark
+	end_mark := token.start_mark
+
+	implicit := true
+	if token.typ == yaml_DOCUMENT_END_TOKEN {
+		end_mark = token.end_mark
+		skip_token(parser)
+		implicit = false
+	}
+
+	parser.tag_directives = parser.tag_directives[:0]
+
+	parser.state = yaml_PARSE_DOCUMENT_START_STATE
+	*event = yaml_event_t{
+		typ:        yaml_DOCUMENT_END_EVENT,
+		start_mark: start_mark,
+		end_mark:   end_mark,
+		implicit:   implicit,
+	}
+	yaml_parser_set_event_comments(parser, event)
+	if len(event.head_comment) > 0 && len(event.foot_comment) == 0 {
+		event.foot_comment = event.head_comment
+		event.head_comment = nil
+	}
+	return true
+}
+
+func yaml_parser_set_event_comments(parser *yaml_parser_t, event *yaml_event_t) {
+	event.head_comment = parser.head_comment
+	event.line_comment = parser.line_comment
+	event.foot_comment = parser.foot_comment
+	parser.head_comment = nil
+	parser.line_comment = nil
+	parser.foot_comment = nil
+	parser.tail_comment = nil
+	parser.stem_comment = nil
+}
+
+// Parse the productions:
+// block_node_or_indentless_sequence    ::=
+//                          ALIAS
+//                          *****
+//                          | properties (block_content | indentless_block_sequence)?
+//                            **********  *
+//                          | block_content | indentless_block_sequence
+//                            *
+// block_node           ::= ALIAS
+//                          *****
+//                          | properties block_content?
+//                            ********** *
+//                          | block_content
+//                            *
+// flow_node            ::= ALIAS
+//                          *****
+//                          | properties flow_content?
+//                            ********** *
+//                          | flow_content
+//                            *
+// properties           ::= TAG ANCHOR? | ANCHOR TAG?
+//                          *************************
+// block_content        ::= block_collection | flow_collection | SCALAR
+//                                                               ******
+// flow_content         ::= flow_collection | SCALAR
+//                                            ******
+func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, indentless_sequence bool) bool {
+	//defer trace("yaml_parser_parse_node", "block:", block, "indentless_sequence:", indentless_sequence)()
+
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+
+	if token.typ == yaml_ALIAS_TOKEN {
+		parser.state = parser.states[len(parser.states)-1]
+		parser.states = parser.states[:len(parser.states)-1]
+		*event = yaml_event_t{
+			typ:        yaml_ALIAS_EVENT,
+			start_mark: token.start_mark,
+			end_mark:   token.end_mark,
+			anchor:     token.value,
+		}
+		yaml_parser_set_event_comments(parser, event)
+		skip_token(parser)
+		return true
+	}
+
+	start_mark := token.start_mark
+	end_mark := token.start_mark
+
+	var tag_token bool
+	var tag_handle, tag_suffix, anchor []byte
+	var tag_mark yaml_mark_t
+	if token.typ == yaml_ANCHOR_TOKEN {
+		anchor = token.value
+		start_mark = token.start_mark
+		end_mark = token.end_mark
+		skip_token(parser)
+		token = peek_token(parser)
+		if token == nil {
+			return false
+		}
+		if token.typ == yaml_TAG_TOKEN {
+			tag_token = true
+			tag_handle = token.value
+			tag_suffix = token.suffix
+			tag_mark = token.start_mark
+			end_mark = token.end_mark
+			skip_token(parser)
+			token = peek_token(parser)
+			if token == nil {
+				return false
+			}
+		}
+	} else if token.typ == yaml_TAG_TOKEN {
+		tag_token = true
+		tag_handle = token.value
+		tag_suffix = token.suffix
+		start_mark = token.start_mark
+		tag_mark = token.start_mark
+		end_mark = token.end_mark
+		skip_token(parser)
+		token = peek_token(parser)
+		if token == nil {
+			return false
+		}
+		if token.typ == yaml_ANCHOR_TOKEN {
+			anchor = token.value
+			end_mark = token.end_mark
+			skip_token(parser)
+			token = peek_token(parser)
+			if token == nil {
+				return false
+			}
+		}
+	}
+
+	var tag []byte
+	if tag_token {
+		if len(tag_handle) == 0 {
+			tag = tag_suffix
+			tag_suffix = nil
+		} else {
+			for i := range parser.tag_directives {
+				if bytes.Equal(parser.tag_directives[i].handle, tag_handle) {
+					tag = append([]byte(nil), parser.tag_directives[i].prefix...)
+					tag = append(tag, tag_suffix...)
+					break
+				}
+			}
+			if len(tag) == 0 {
+				yaml_parser_set_parser_error_context(parser,
+					"while parsing a node", start_mark,
+					"found undefined tag handle", tag_mark)
+				return false
+			}
+		}
+	}
+
+	implicit := len(tag) == 0
+	if indentless_sequence && token.typ == yaml_BLOCK_ENTRY_TOKEN {
+		end_mark = token.end_mark
+		parser.state = yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE
+		*event = yaml_event_t{
+			typ:        yaml_SEQUENCE_START_EVENT,
+			start_mark: start_mark,
+			end_mark:   end_mark,
+			anchor:     anchor,
+			tag:        tag,
+			implicit:   implicit,
+			style:      yaml_style_t(yaml_BLOCK_SEQUENCE_STYLE),
+		}
+		return true
+	}
+	if token.typ == yaml_SCALAR_TOKEN {
+		var plain_implicit, quoted_implicit bool
+		end_mark = token.end_mark
+		if (len(tag) == 0 && token.style == yaml_PLAIN_SCALAR_STYLE) || (len(tag) == 1 && tag[0] == '!') {
+			plain_implicit = true
+		} else if len(tag) == 0 {
+			quoted_implicit = true
+		}
+		parser.state = parser.states[len(parser.states)-1]
+		parser.states = parser.states[:len(parser.states)-1]
+
+		*event = yaml_event_t{
+			typ:             yaml_SCALAR_EVENT,
+			start_mark:      start_mark,
+			end_mark:        end_mark,
+			anchor:          anchor,
+			tag:             tag,
+			value:           token.value,
+			implicit:        plain_implicit,
+			quoted_implicit: quoted_implicit,
+			style:           yaml_style_t(token.style),
+		}
+		yaml_parser_set_event_comments(parser, event)
+		skip_token(parser)
+		return true
+	}
+	if token.typ == yaml_FLOW_SEQUENCE_START_TOKEN {
+		// [Go] Some of the events below can be merged as they differ only on style.
+		end_mark = token.end_mark
+		parser.state = yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE
+		*event = yaml_event_t{
+			typ:        yaml_SEQUENCE_START_EVENT,
+			start_mark: start_mark,
+			end_mark:   end_mark,
+			anchor:     anchor,
+			tag:        tag,
+			implicit:   implicit,
+			style:      yaml_style_t(yaml_FLOW_SEQUENCE_STYLE),
+		}
+		yaml_parser_set_event_comments(parser, event)
+		return true
+	}
+	if token.typ == yaml_FLOW_MAPPING_START_TOKEN {
+		end_mark = token.end_mark
+		parser.state = yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE
+		*event = yaml_event_t{
+			typ:        yaml_MAPPING_START_EVENT,
+			start_mark: start_mark,
+			end_mark:   end_mark,
+			anchor:     anchor,
+			tag:        tag,
+			implicit:   implicit,
+			style:      yaml_style_t(yaml_FLOW_MAPPING_STYLE),
+		}
+		yaml_parser_set_event_comments(parser, event)
+		return true
+	}
+	if block && token.typ == yaml_BLOCK_SEQUENCE_START_TOKEN {
+		end_mark = token.end_mark
+		parser.state = yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE
+		*event = yaml_event_t{
+			typ:        yaml_SEQUENCE_START_EVENT,
+			start_mark: start_mark,
+			end_mark:   end_mark,
+			anchor:     anchor,
+			tag:        tag,
+			implicit:   implicit,
+			style:      yaml_style_t(yaml_BLOCK_SEQUENCE_STYLE),
+		}
+		if parser.stem_comment != nil {
+			event.head_comment = parser.stem_comment
+			parser.stem_comment = nil
+		}
+		return true
+	}
+	if block && token.typ == yaml_BLOCK_MAPPING_START_TOKEN {
+		end_mark = token.end_mark
+		parser.state = yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE
+		*event = yaml_event_t{
+			typ:        yaml_MAPPING_START_EVENT,
+			start_mark: start_mark,
+			end_mark:   end_mark,
+			anchor:     anchor,
+			tag:        tag,
+			implicit:   implicit,
+			style:      yaml_style_t(yaml_BLOCK_MAPPING_STYLE),
+		}
+		if parser.stem_comment != nil {
+			event.head_comment = parser.stem_comment
+			parser.stem_comment = nil
+		}
+		return true
+	}
+	if len(anchor) > 0 || len(tag) > 0 {
+		parser.state = parser.states[len(parser.states)-1]
+		parser.states = parser.states[:len(parser.states)-1]
+
+		*event = yaml_event_t{
+			typ:             yaml_SCALAR_EVENT,
+			start_mark:      start_mark,
+			end_mark:        end_mark,
+			anchor:          anchor,
+			tag:             tag,
+			implicit:        implicit,
+			quoted_implicit: false,
+			style:           yaml_style_t(yaml_PLAIN_SCALAR_STYLE),
+		}
+		return true
+	}
+
+	context := "while parsing a flow node"
+	if block {
+		context = "while parsing a block node"
+	}
+	yaml_parser_set_parser_error_context(parser, context, start_mark,
+		"did not find expected node content", token.start_mark)
+	return false
+}
+
+// Parse the productions:
+// block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END
+//                    ********************  *********** *             *********
+//
+func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
+	if first {
+		token := peek_token(parser)
+		if token == nil {
+			return false
+		}
+		parser.marks = append(parser.marks, token.start_mark)
+		skip_token(parser)
+	}
+
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+
+	if token.typ == yaml_BLOCK_ENTRY_TOKEN {
+		mark := token.end_mark
+		prior_head_len := len(parser.head_comment)
+		skip_token(parser)
+		yaml_parser_split_stem_comment(parser, prior_head_len)
+		token = peek_token(parser)
+		if token == nil {
+			return false
+		}
+		if token.typ != yaml_BLOCK_ENTRY_TOKEN && token.typ != yaml_BLOCK_END_TOKEN {
+			parser.states = append(parser.states, yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE)
+			return yaml_parser_parse_node(parser, event, true, false)
+		} else {
+			parser.state = yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE
+			return yaml_parser_process_empty_scalar(parser, event, mark)
+		}
+	}
+	if token.typ == yaml_BLOCK_END_TOKEN {
+		parser.state = parser.states[len(parser.states)-1]
+		parser.states = parser.states[:len(parser.states)-1]
+		parser.marks = parser.marks[:len(parser.marks)-1]
+
+		*event = yaml_event_t{
+			typ:        yaml_SEQUENCE_END_EVENT,
+			start_mark: token.start_mark,
+			end_mark:   token.end_mark,
+		}
+
+		skip_token(parser)
+		return true
+	}
+
+	context_mark := parser.marks[len(parser.marks)-1]
+	parser.marks = parser.marks[:len(parser.marks)-1]
+	return yaml_parser_set_parser_error_context(parser,
+		"while parsing a block collection", context_mark,
+		"did not find expected '-' indicator", token.start_mark)
+}
+
+// Parse the productions:
+// indentless_sequence  ::= (BLOCK-ENTRY block_node?)+
+//                           *********** *
+func yaml_parser_parse_indentless_sequence_entry(parser *yaml_parser_t, event *yaml_event_t) bool {
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+
+	if token.typ == yaml_BLOCK_ENTRY_TOKEN {
+		mark := token.end_mark
+		prior_head_len := len(parser.head_comment)
+		skip_token(parser)
+		yaml_parser_split_stem_comment(parser, prior_head_len)
+		token = peek_token(parser)
+		if token == nil {
+			return false
+		}
+		if token.typ != yaml_BLOCK_ENTRY_TOKEN &&
+			token.typ != yaml_KEY_TOKEN &&
+			token.typ != yaml_VALUE_TOKEN &&
+			token.typ != yaml_BLOCK_END_TOKEN {
+			parser.states = append(parser.states, yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE)
+			return yaml_parser_parse_node(parser, event, true, false)
+		}
+		parser.state = yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE
+		return yaml_parser_process_empty_scalar(parser, event, mark)
+	}
+	parser.state = parser.states[len(parser.states)-1]
+	parser.states = parser.states[:len(parser.states)-1]
+
+	*event = yaml_event_t{
+		typ:        yaml_SEQUENCE_END_EVENT,
+		start_mark: token.start_mark,
+		end_mark:   token.start_mark, // [Go] Shouldn't this be token.end_mark?
+	}
+	return true
+}
+
+// Split stem comment from head comment.
+//
+// When a sequence or map is found under a sequence entry, the former head comment
+// is assigned to the underlying sequence or map as a whole, not the individual
+// sequence or map entry as would be expected otherwise. To handle this case the
+// previous head comment is moved aside as the stem comment.
+func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) {
+	if stem_len == 0 {
+		return
+	}
+
+	token := peek_token(parser)
+	if token == nil || token.typ != yaml_BLOCK_SEQUENCE_START_TOKEN && token.typ != yaml_BLOCK_MAPPING_START_TOKEN {
+		return
+	}
+
+	parser.stem_comment = parser.head_comment[:stem_len]
+	if len(parser.head_comment) == stem_len {
+		parser.head_comment = nil
+	} else {
+		// Copy suffix to prevent very strange bugs if someone ever appends
+		// further bytes to the prefix in the stem_comment slice above.
+		parser.head_comment = append([]byte(nil), parser.head_comment[stem_len+1:]...)
+	}
+}
+
+// Parse the productions:
+// block_mapping        ::= BLOCK-MAPPING_START
+//                          *******************
+//                          ((KEY block_node_or_indentless_sequence?)?
+//                            *** *
+//                          (VALUE block_node_or_indentless_sequence?)?)*
+//
+//                          BLOCK-END
+//                          *********
+//
+func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
+	if first {
+		token := peek_token(parser)
+		if token == nil {
+			return false
+		}
+		parser.marks = append(parser.marks, token.start_mark)
+		skip_token(parser)
+	}
+
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+
+	// [Go] A tail comment was left from the prior mapping value processed. Emit an event
+	//      as it needs to be processed with that value and not the following key.
+	if len(parser.tail_comment) > 0 {
+		*event = yaml_event_t{
+			typ:          yaml_TAIL_COMMENT_EVENT,
+			start_mark:   token.start_mark,
+			end_mark:     token.end_mark,
+			foot_comment: parser.tail_comment,
+		}
+		parser.tail_comment = nil
+		return true
+	}
+
+	if token.typ == yaml_KEY_TOKEN {
+		mark := token.end_mark
+		skip_token(parser)
+		token = peek_token(parser)
+		if token == nil {
+			return false
+		}
+		if token.typ != yaml_KEY_TOKEN &&
+			token.typ != yaml_VALUE_TOKEN &&
+			token.typ != yaml_BLOCK_END_TOKEN {
+			parser.states = append(parser.states, yaml_PARSE_BLOCK_MAPPING_VALUE_STATE)
+			return yaml_parser_parse_node(parser, event, true, true)
+		} else {
+			parser.state = yaml_PARSE_BLOCK_MAPPING_VALUE_STATE
+			return yaml_parser_process_empty_scalar(parser, event, mark)
+		}
+	} else if token.typ == yaml_BLOCK_END_TOKEN {
+		parser.state = parser.states[len(parser.states)-1]
+		parser.states = parser.states[:len(parser.states)-1]
+		parser.marks = parser.marks[:len(parser.marks)-1]
+		*event = yaml_event_t{
+			typ:        yaml_MAPPING_END_EVENT,
+			start_mark: token.start_mark,
+			end_mark:   token.end_mark,
+		}
+		yaml_parser_set_event_comments(parser, event)
+		skip_token(parser)
+		return true
+	}
+
+	context_mark := parser.marks[len(parser.marks)-1]
+	parser.marks = parser.marks[:len(parser.marks)-1]
+	return yaml_parser_set_parser_error_context(parser,
+		"while parsing a block mapping", context_mark,
+		"did not find expected key", token.start_mark)
+}
+
+// Parse the productions:
+// block_mapping        ::= BLOCK-MAPPING_START
+//
+//                          ((KEY block_node_or_indentless_sequence?)?
+//
+//                          (VALUE block_node_or_indentless_sequence?)?)*
+//                           ***** *
+//                          BLOCK-END
+//
+//
+func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool {
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+	if token.typ == yaml_VALUE_TOKEN {
+		mark := token.end_mark
+		skip_token(parser)
+		token = peek_token(parser)
+		if token == nil {
+			return false
+		}
+		if token.typ != yaml_KEY_TOKEN &&
+			token.typ != yaml_VALUE_TOKEN &&
+			token.typ != yaml_BLOCK_END_TOKEN {
+			parser.states = append(parser.states, yaml_PARSE_BLOCK_MAPPING_KEY_STATE)
+			return yaml_parser_parse_node(parser, event, true, true)
+		}
+		parser.state = yaml_PARSE_BLOCK_MAPPING_KEY_STATE
+		return yaml_parser_process_empty_scalar(parser, event, mark)
+	}
+	parser.state = yaml_PARSE_BLOCK_MAPPING_KEY_STATE
+	return yaml_parser_process_empty_scalar(parser, event, token.start_mark)
+}
+
+// Parse the productions:
+// flow_sequence        ::= FLOW-SEQUENCE-START
+//                          *******************
+//                          (flow_sequence_entry FLOW-ENTRY)*
+//                           *                   **********
+//                          flow_sequence_entry?
+//                          *
+//                          FLOW-SEQUENCE-END
+//                          *****************
+// flow_sequence_entry  ::= flow_node | KEY flow_node? (VALUE flow_node?)?
+//                          *
+//
+func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
+	if first {
+		token := peek_token(parser)
+		if token == nil {
+			return false
+		}
+		parser.marks = append(parser.marks, token.start_mark)
+		skip_token(parser)
+	}
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+	if token.typ != yaml_FLOW_SEQUENCE_END_TOKEN {
+		if !first {
+			if token.typ == yaml_FLOW_ENTRY_TOKEN {
+				skip_token(parser)
+				token = peek_token(parser)
+				if token == nil {
+					return false
+				}
+			} else {
+				context_mark := parser.marks[len(parser.marks)-1]
+				parser.marks = parser.marks[:len(parser.marks)-1]
+				return yaml_parser_set_parser_error_context(parser,
+					"while parsing a flow sequence", context_mark,
+					"did not find expected ',' or ']'", token.start_mark)
+			}
+		}
+
+		if token.typ == yaml_KEY_TOKEN {
+			parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE
+			*event = yaml_event_t{
+				typ:        yaml_MAPPING_START_EVENT,
+				start_mark: token.start_mark,
+				end_mark:   token.end_mark,
+				implicit:   true,
+				style:      yaml_style_t(yaml_FLOW_MAPPING_STYLE),
+			}
+			skip_token(parser)
+			return true
+		} else if token.typ != yaml_FLOW_SEQUENCE_END_TOKEN {
+			parser.states = append(parser.states, yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE)
+			return yaml_parser_parse_node(parser, event, false, false)
+		}
+	}
+
+	parser.state = parser.states[len(parser.states)-1]
+	parser.states = parser.states[:len(parser.states)-1]
+	parser.marks = parser.marks[:len(parser.marks)-1]
+
+	*event = yaml_event_t{
+		typ:        yaml_SEQUENCE_END_EVENT,
+		start_mark: token.start_mark,
+		end_mark:   token.end_mark,
+	}
+	yaml_parser_set_event_comments(parser, event)
+
+	skip_token(parser)
+	return true
+}
+
+//
+// Parse the productions:
+// flow_sequence_entry  ::= flow_node | KEY flow_node? (VALUE flow_node?)?
+//                                      *** *
+//
+func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, event *yaml_event_t) bool {
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+	if token.typ != yaml_VALUE_TOKEN &&
+		token.typ != yaml_FLOW_ENTRY_TOKEN &&
+		token.typ != yaml_FLOW_SEQUENCE_END_TOKEN {
+		parser.states = append(parser.states, yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE)
+		return yaml_parser_parse_node(parser, event, false, false)
+	}
+	mark := token.end_mark
+	skip_token(parser)
+	parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE
+	return yaml_parser_process_empty_scalar(parser, event, mark)
+}
+
+// Parse the productions:
+// flow_sequence_entry  ::= flow_node | KEY flow_node? (VALUE flow_node?)?
+//                                                      ***** *
+//
+func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool {
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+	if token.typ == yaml_VALUE_TOKEN {
+		skip_token(parser)
+		token := peek_token(parser)
+		if token == nil {
+			return false
+		}
+		if token.typ != yaml_FLOW_ENTRY_TOKEN && token.typ != yaml_FLOW_SEQUENCE_END_TOKEN {
+			parser.states = append(parser.states, yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE)
+			return yaml_parser_parse_node(parser, event, false, false)
+		}
+	}
+	parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE
+	return yaml_parser_process_empty_scalar(parser, event, token.start_mark)
+}
+
+// Parse the productions:
+// flow_sequence_entry  ::= flow_node | KEY flow_node? (VALUE flow_node?)?
+//                                                                      *
+//
+func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, event *yaml_event_t) bool {
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+	parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE
+	*event = yaml_event_t{
+		typ:        yaml_MAPPING_END_EVENT,
+		start_mark: token.start_mark,
+		end_mark:   token.start_mark, // [Go] Shouldn't this be end_mark?
+	}
+	return true
+}
+
+// Parse the productions:
+// flow_mapping         ::= FLOW-MAPPING-START
+//                          ******************
+//                          (flow_mapping_entry FLOW-ENTRY)*
+//                           *                  **********
+//                          flow_mapping_entry?
+//                          ******************
+//                          FLOW-MAPPING-END
+//                          ****************
+// flow_mapping_entry   ::= flow_node | KEY flow_node? (VALUE flow_node?)?
+//                          *           *** *
+//
+func yaml_parser_parse_flow_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
+	if first {
+		token := peek_token(parser)
+		parser.marks = append(parser.marks, token.start_mark)
+		skip_token(parser)
+	}
+
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+
+	if token.typ != yaml_FLOW_MAPPING_END_TOKEN {
+		if !first {
+			if token.typ == yaml_FLOW_ENTRY_TOKEN {
+				skip_token(parser)
+				token = peek_token(parser)
+				if token == nil {
+					return false
+				}
+			} else {
+				context_mark := parser.marks[len(parser.marks)-1]
+				parser.marks = parser.marks[:len(parser.marks)-1]
+				return yaml_parser_set_parser_error_context(parser,
+					"while parsing a flow mapping", context_mark,
+					"did not find expected ',' or '}'", token.start_mark)
+			}
+		}
+
+		if token.typ == yaml_KEY_TOKEN {
+			skip_token(parser)
+			token = peek_token(parser)
+			if token == nil {
+				return false
+			}
+			if token.typ != yaml_VALUE_TOKEN &&
+				token.typ != yaml_FLOW_ENTRY_TOKEN &&
+				token.typ != yaml_FLOW_MAPPING_END_TOKEN {
+				parser.states = append(parser.states, yaml_PARSE_FLOW_MAPPING_VALUE_STATE)
+				return yaml_parser_parse_node(parser, event, false, false)
+			} else {
+				parser.state = yaml_PARSE_FLOW_MAPPING_VALUE_STATE
+				return yaml_parser_process_empty_scalar(parser, event, token.start_mark)
+			}
+		} else if token.typ != yaml_FLOW_MAPPING_END_TOKEN {
+			parser.states = append(parser.states, yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE)
+			return yaml_parser_parse_node(parser, event, false, false)
+		}
+	}
+
+	parser.state = parser.states[len(parser.states)-1]
+	parser.states = parser.states[:len(parser.states)-1]
+	parser.marks = parser.marks[:len(parser.marks)-1]
+	*event = yaml_event_t{
+		typ:        yaml_MAPPING_END_EVENT,
+		start_mark: token.start_mark,
+		end_mark:   token.end_mark,
+	}
+	yaml_parser_set_event_comments(parser, event)
+	skip_token(parser)
+	return true
+}
+
+// Parse the productions:
+// flow_mapping_entry   ::= flow_node | KEY flow_node? (VALUE flow_node?)?
+//                                   *                  ***** *
+//
+func yaml_parser_parse_flow_mapping_value(parser *yaml_parser_t, event *yaml_event_t, empty bool) bool {
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+	if empty {
+		parser.state = yaml_PARSE_FLOW_MAPPING_KEY_STATE
+		return yaml_parser_process_empty_scalar(parser, event, token.start_mark)
+	}
+	if token.typ == yaml_VALUE_TOKEN {
+		skip_token(parser)
+		token = peek_token(parser)
+		if token == nil {
+			return false
+		}
+		if token.typ != yaml_FLOW_ENTRY_TOKEN && token.typ != yaml_FLOW_MAPPING_END_TOKEN {
+			parser.states = append(parser.states, yaml_PARSE_FLOW_MAPPING_KEY_STATE)
+			return yaml_parser_parse_node(parser, event, false, false)
+		}
+	}
+	parser.state = yaml_PARSE_FLOW_MAPPING_KEY_STATE
+	return yaml_parser_process_empty_scalar(parser, event, token.start_mark)
+}
+
+// Generate an empty scalar event.
+func yaml_parser_process_empty_scalar(parser *yaml_parser_t, event *yaml_event_t, mark yaml_mark_t) bool {
+	*event = yaml_event_t{
+		typ:        yaml_SCALAR_EVENT,
+		start_mark: mark,
+		end_mark:   mark,
+		value:      nil, // Empty
+		implicit:   true,
+		style:      yaml_style_t(yaml_PLAIN_SCALAR_STYLE),
+	}
+	return true
+}
+
+var default_tag_directives = []yaml_tag_directive_t{
+	{[]byte("!"), []byte("!")},
+	{[]byte("!!"), []byte("tag:yaml.org,2002:")},
+}
+
+// Parse directives.
+func yaml_parser_process_directives(parser *yaml_parser_t,
+	version_directive_ref **yaml_version_directive_t,
+	tag_directives_ref *[]yaml_tag_directive_t) bool {
+
+	var version_directive *yaml_version_directive_t
+	var tag_directives []yaml_tag_directive_t
+
+	token := peek_token(parser)
+	if token == nil {
+		return false
+	}
+
+	for token.typ == yaml_VERSION_DIRECTIVE_TOKEN || token.typ == yaml_TAG_DIRECTIVE_TOKEN {
+		if token.typ == yaml_VERSION_DIRECTIVE_TOKEN {
+			if version_directive != nil {
+				yaml_parser_set_parser_error(parser,
+					"found duplicate %YAML directive", token.start_mark)
+				return false
+			}
+			if token.major != 1 || token.minor != 1 {
+				yaml_parser_set_parser_error(parser,
+					"found incompatible YAML document", token.start_mark)
+				return false
+			}
+			version_directive = &yaml_version_directive_t{
+				major: token.major,
+				minor: token.minor,
+			}
+		} else if token.typ == yaml_TAG_DIRECTIVE_TOKEN {
+			value := yaml_tag_directive_t{
+				handle: token.value,
+				prefix: token.prefix,
+			}
+			if !yaml_parser_append_tag_directive(parser, value, false, token.start_mark) {
+				return false
+			}
+			tag_directives = append(tag_directives, value)
+		}
+
+		skip_token(parser)
+		token = peek_token(parser)
+		if token == nil {
+			return false
+		}
+	}
+
+	for i := range default_tag_directives {
+		if !yaml_parser_append_tag_directive(parser, default_tag_directives[i], true, token.start_mark) {
+			return false
+		}
+	}
+
+	if version_directive_ref != nil {
+		*version_directive_ref = version_directive
+	}
+	if tag_directives_ref != nil {
+		*tag_directives_ref = tag_directives
+	}
+	return true
+}
+
+// Append a tag directive to the directives stack.
+func yaml_parser_append_tag_directive(parser *yaml_parser_t, value yaml_tag_directive_t, allow_duplicates bool, mark yaml_mark_t) bool {
+	for i := range parser.tag_directives {
+		if bytes.Equal(value.handle, parser.tag_directives[i].handle) {
+			if allow_duplicates {
+				return true
+			}
+			return yaml_parser_set_parser_error(parser, "found duplicate %TAG directive", mark)
+		}
+	}
+
+	// [Go] I suspect the copy is unnecessary. This was likely done
+	// because there was no way to track ownership of the data.
+	value_copy := yaml_tag_directive_t{
+		handle: make([]byte, len(value.handle)),
+		prefix: make([]byte, len(value.prefix)),
+	}
+	copy(value_copy.handle, value.handle)
+	copy(value_copy.prefix, value.prefix)
+	parser.tag_directives = append(parser.tag_directives, value_copy)
+	return true
+}
diff --git a/vendor/gopkg.in/yaml.v3/readerc.go b/vendor/gopkg.in/yaml.v3/readerc.go
new file mode 100644
index 000000000..b7de0a89c
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/readerc.go
@@ -0,0 +1,434 @@
+// 
+// Copyright (c) 2011-2019 Canonical Ltd
+// Copyright (c) 2006-2010 Kirill Simonov
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package yaml
+
+import (
+	"io"
+)
+
+// Set the reader error and return 0.
+func yaml_parser_set_reader_error(parser *yaml_parser_t, problem string, offset int, value int) bool {
+	parser.error = yaml_READER_ERROR
+	parser.problem = problem
+	parser.problem_offset = offset
+	parser.problem_value = value
+	return false
+}
+
+// Byte order marks.
+const (
+	bom_UTF8    = "\xef\xbb\xbf"
+	bom_UTF16LE = "\xff\xfe"
+	bom_UTF16BE = "\xfe\xff"
+)
+
+// Determine the input stream encoding by checking the BOM symbol. If no BOM is
+// found, the UTF-8 encoding is assumed. Return 1 on success, 0 on failure.
+func yaml_parser_determine_encoding(parser *yaml_parser_t) bool {
+	// Ensure that we had enough bytes in the raw buffer.
+	for !parser.eof && len(parser.raw_buffer)-parser.raw_buffer_pos < 3 {
+		if !yaml_parser_update_raw_buffer(parser) {
+			return false
+		}
+	}
+
+	// Determine the encoding.
+	buf := parser.raw_buffer
+	pos := parser.raw_buffer_pos
+	avail := len(buf) - pos
+	if avail >= 2 && buf[pos] == bom_UTF16LE[0] && buf[pos+1] == bom_UTF16LE[1] {
+		parser.encoding = yaml_UTF16LE_ENCODING
+		parser.raw_buffer_pos += 2
+		parser.offset += 2
+	} else if avail >= 2 && buf[pos] == bom_UTF16BE[0] && buf[pos+1] == bom_UTF16BE[1] {
+		parser.encoding = yaml_UTF16BE_ENCODING
+		parser.raw_buffer_pos += 2
+		parser.offset += 2
+	} else if avail >= 3 && buf[pos] == bom_UTF8[0] && buf[pos+1] == bom_UTF8[1] && buf[pos+2] == bom_UTF8[2] {
+		parser.encoding = yaml_UTF8_ENCODING
+		parser.raw_buffer_pos += 3
+		parser.offset += 3
+	} else {
+		parser.encoding = yaml_UTF8_ENCODING
+	}
+	return true
+}
+
+// Update the raw buffer.
+func yaml_parser_update_raw_buffer(parser *yaml_parser_t) bool {
+	size_read := 0
+
+	// Return if the raw buffer is full.
+	if parser.raw_buffer_pos == 0 && len(parser.raw_buffer) == cap(parser.raw_buffer) {
+		return true
+	}
+
+	// Return on EOF.
+	if parser.eof {
+		return true
+	}
+
+	// Move the remaining bytes in the raw buffer to the beginning.
+	if parser.raw_buffer_pos > 0 && parser.raw_buffer_pos < len(parser.raw_buffer) {
+		copy(parser.raw_buffer, parser.raw_buffer[parser.raw_buffer_pos:])
+	}
+	parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)-parser.raw_buffer_pos]
+	parser.raw_buffer_pos = 0
+
+	// Call the read handler to fill the buffer.
+	size_read, err := parser.read_handler(parser, parser.raw_buffer[len(parser.raw_buffer):cap(parser.raw_buffer)])
+	parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)+size_read]
+	if err == io.EOF {
+		parser.eof = true
+	} else if err != nil {
+		return yaml_parser_set_reader_error(parser, "input error: "+err.Error(), parser.offset, -1)
+	}
+	return true
+}
+
+// Ensure that the buffer contains at least `length` characters.
+// Return true on success, false on failure.
+//
+// The length is supposed to be significantly less that the buffer size.
+func yaml_parser_update_buffer(parser *yaml_parser_t, length int) bool {
+	if parser.read_handler == nil {
+		panic("read handler must be set")
+	}
+
+	// [Go] This function was changed to guarantee the requested length size at EOF.
+	// The fact we need to do this is pretty awful, but the description above implies
+	// for that to be the case, and there are tests
+
+	// If the EOF flag is set and the raw buffer is empty, do nothing.
+	if parser.eof && parser.raw_buffer_pos == len(parser.raw_buffer) {
+		// [Go] ACTUALLY! Read the documentation of this function above.
+		// This is just broken. To return true, we need to have the
+		// given length in the buffer. Not doing that means every single
+		// check that calls this function to make sure the buffer has a
+		// given length is Go) panicking; or C) accessing invalid memory.
+		//return true
+	}
+
+	// Return if the buffer contains enough characters.
+	if parser.unread >= length {
+		return true
+	}
+
+	// Determine the input encoding if it is not known yet.
+	if parser.encoding == yaml_ANY_ENCODING {
+		if !yaml_parser_determine_encoding(parser) {
+			return false
+		}
+	}
+
+	// Move the unread characters to the beginning of the buffer.
+	buffer_len := len(parser.buffer)
+	if parser.buffer_pos > 0 && parser.buffer_pos < buffer_len {
+		copy(parser.buffer, parser.buffer[parser.buffer_pos:])
+		buffer_len -= parser.buffer_pos
+		parser.buffer_pos = 0
+	} else if parser.buffer_pos == buffer_len {
+		buffer_len = 0
+		parser.buffer_pos = 0
+	}
+
+	// Open the whole buffer for writing, and cut it before returning.
+	parser.buffer = parser.buffer[:cap(parser.buffer)]
+
+	// Fill the buffer until it has enough characters.
+	first := true
+	for parser.unread < length {
+
+		// Fill the raw buffer if necessary.
+		if !first || parser.raw_buffer_pos == len(parser.raw_buffer) {
+			if !yaml_parser_update_raw_buffer(parser) {
+				parser.buffer = parser.buffer[:buffer_len]
+				return false
+			}
+		}
+		first = false
+
+		// Decode the raw buffer.
+	inner:
+		for parser.raw_buffer_pos != len(parser.raw_buffer) {
+			var value rune
+			var width int
+
+			raw_unread := len(parser.raw_buffer) - parser.raw_buffer_pos
+
+			// Decode the next character.
+			switch parser.encoding {
+			case yaml_UTF8_ENCODING:
+				// Decode a UTF-8 character.  Check RFC 3629
+				// (http://www.ietf.org/rfc/rfc3629.txt) for more details.
+				//
+				// The following table (taken from the RFC) is used for
+				// decoding.
+				//
+				//    Char. number range |        UTF-8 octet sequence
+				//      (hexadecimal)    |              (binary)
+				//   --------------------+------------------------------------
+				//   0000 0000-0000 007F | 0xxxxxxx
+				//   0000 0080-0000 07FF | 110xxxxx 10xxxxxx
+				//   0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
+				//   0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+				//
+				// Additionally, the characters in the range 0xD800-0xDFFF
+				// are prohibited as they are reserved for use with UTF-16
+				// surrogate pairs.
+
+				// Determine the length of the UTF-8 sequence.
+				octet := parser.raw_buffer[parser.raw_buffer_pos]
+				switch {
+				case octet&0x80 == 0x00:
+					width = 1
+				case octet&0xE0 == 0xC0:
+					width = 2
+				case octet&0xF0 == 0xE0:
+					width = 3
+				case octet&0xF8 == 0xF0:
+					width = 4
+				default:
+					// The leading octet is invalid.
+					return yaml_parser_set_reader_error(parser,
+						"invalid leading UTF-8 octet",
+						parser.offset, int(octet))
+				}
+
+				// Check if the raw buffer contains an incomplete character.
+				if width > raw_unread {
+					if parser.eof {
+						return yaml_parser_set_reader_error(parser,
+							"incomplete UTF-8 octet sequence",
+							parser.offset, -1)
+					}
+					break inner
+				}
+
+				// Decode the leading octet.
+				switch {
+				case octet&0x80 == 0x00:
+					value = rune(octet & 0x7F)
+				case octet&0xE0 == 0xC0:
+					value = rune(octet & 0x1F)
+				case octet&0xF0 == 0xE0:
+					value = rune(octet & 0x0F)
+				case octet&0xF8 == 0xF0:
+					value = rune(octet & 0x07)
+				default:
+					value = 0
+				}
+
+				// Check and decode the trailing octets.
+				for k := 1; k < width; k++ {
+					octet = parser.raw_buffer[parser.raw_buffer_pos+k]
+
+					// Check if the octet is valid.
+					if (octet & 0xC0) != 0x80 {
+						return yaml_parser_set_reader_error(parser,
+							"invalid trailing UTF-8 octet",
+							parser.offset+k, int(octet))
+					}
+
+					// Decode the octet.
+					value = (value << 6) + rune(octet&0x3F)
+				}
+
+				// Check the length of the sequence against the value.
+				switch {
+				case width == 1:
+				case width == 2 && value >= 0x80:
+				case width == 3 && value >= 0x800:
+				case width == 4 && value >= 0x10000:
+				default:
+					return yaml_parser_set_reader_error(parser,
+						"invalid length of a UTF-8 sequence",
+						parser.offset, -1)
+				}
+
+				// Check the range of the value.
+				if value >= 0xD800 && value <= 0xDFFF || value > 0x10FFFF {
+					return yaml_parser_set_reader_error(parser,
+						"invalid Unicode character",
+						parser.offset, int(value))
+				}
+
+			case yaml_UTF16LE_ENCODING, yaml_UTF16BE_ENCODING:
+				var low, high int
+				if parser.encoding == yaml_UTF16LE_ENCODING {
+					low, high = 0, 1
+				} else {
+					low, high = 1, 0
+				}
+
+				// The UTF-16 encoding is not as simple as one might
+				// naively think.  Check RFC 2781
+				// (http://www.ietf.org/rfc/rfc2781.txt).
+				//
+				// Normally, two subsequent bytes describe a Unicode
+				// character.  However a special technique (called a
+				// surrogate pair) is used for specifying character
+				// values larger than 0xFFFF.
+				//
+				// A surrogate pair consists of two pseudo-characters:
+				//      high surrogate area (0xD800-0xDBFF)
+				//      low surrogate area (0xDC00-0xDFFF)
+				//
+				// The following formulas are used for decoding
+				// and encoding characters using surrogate pairs:
+				//
+				//  U  = U' + 0x10000   (0x01 00 00 <= U <= 0x10 FF FF)
+				//  U' = yyyyyyyyyyxxxxxxxxxx   (0 <= U' <= 0x0F FF FF)
+				//  W1 = 110110yyyyyyyyyy
+				//  W2 = 110111xxxxxxxxxx
+				//
+				// where U is the character value, W1 is the high surrogate
+				// area, W2 is the low surrogate area.
+
+				// Check for incomplete UTF-16 character.
+				if raw_unread < 2 {
+					if parser.eof {
+						return yaml_parser_set_reader_error(parser,
+							"incomplete UTF-16 character",
+							parser.offset, -1)
+					}
+					break inner
+				}
+
+				// Get the character.
+				value = rune(parser.raw_buffer[parser.raw_buffer_pos+low]) +
+					(rune(parser.raw_buffer[parser.raw_buffer_pos+high]) << 8)
+
+				// Check for unexpected low surrogate area.
+				if value&0xFC00 == 0xDC00 {
+					return yaml_parser_set_reader_error(parser,
+						"unexpected low surrogate area",
+						parser.offset, int(value))
+				}
+
+				// Check for a high surrogate area.
+				if value&0xFC00 == 0xD800 {
+					width = 4
+
+					// Check for incomplete surrogate pair.
+					if raw_unread < 4 {
+						if parser.eof {
+							return yaml_parser_set_reader_error(parser,
+								"incomplete UTF-16 surrogate pair",
+								parser.offset, -1)
+						}
+						break inner
+					}
+
+					// Get the next character.
+					value2 := rune(parser.raw_buffer[parser.raw_buffer_pos+low+2]) +
+						(rune(parser.raw_buffer[parser.raw_buffer_pos+high+2]) << 8)
+
+					// Check for a low surrogate area.
+					if value2&0xFC00 != 0xDC00 {
+						return yaml_parser_set_reader_error(parser,
+							"expected low surrogate area",
+							parser.offset+2, int(value2))
+					}
+
+					// Generate the value of the surrogate pair.
+					value = 0x10000 + ((value & 0x3FF) << 10) + (value2 & 0x3FF)
+				} else {
+					width = 2
+				}
+
+			default:
+				panic("impossible")
+			}
+
+			// Check if the character is in the allowed range:
+			//      #x9 | #xA | #xD | [#x20-#x7E]               (8 bit)
+			//      | #x85 | [#xA0-#xD7FF] | [#xE000-#xFFFD]    (16 bit)
+			//      | [#x10000-#x10FFFF]                        (32 bit)
+			switch {
+			case value == 0x09:
+			case value == 0x0A:
+			case value == 0x0D:
+			case value >= 0x20 && value <= 0x7E:
+			case value == 0x85:
+			case value >= 0xA0 && value <= 0xD7FF:
+			case value >= 0xE000 && value <= 0xFFFD:
+			case value >= 0x10000 && value <= 0x10FFFF:
+			default:
+				return yaml_parser_set_reader_error(parser,
+					"control characters are not allowed",
+					parser.offset, int(value))
+			}
+
+			// Move the raw pointers.
+			parser.raw_buffer_pos += width
+			parser.offset += width
+
+			// Finally put the character into the buffer.
+			if value <= 0x7F {
+				// 0000 0000-0000 007F . 0xxxxxxx
+				parser.buffer[buffer_len+0] = byte(value)
+				buffer_len += 1
+			} else if value <= 0x7FF {
+				// 0000 0080-0000 07FF . 110xxxxx 10xxxxxx
+				parser.buffer[buffer_len+0] = byte(0xC0 + (value >> 6))
+				parser.buffer[buffer_len+1] = byte(0x80 + (value & 0x3F))
+				buffer_len += 2
+			} else if value <= 0xFFFF {
+				// 0000 0800-0000 FFFF . 1110xxxx 10xxxxxx 10xxxxxx
+				parser.buffer[buffer_len+0] = byte(0xE0 + (value >> 12))
+				parser.buffer[buffer_len+1] = byte(0x80 + ((value >> 6) & 0x3F))
+				parser.buffer[buffer_len+2] = byte(0x80 + (value & 0x3F))
+				buffer_len += 3
+			} else {
+				// 0001 0000-0010 FFFF . 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+				parser.buffer[buffer_len+0] = byte(0xF0 + (value >> 18))
+				parser.buffer[buffer_len+1] = byte(0x80 + ((value >> 12) & 0x3F))
+				parser.buffer[buffer_len+2] = byte(0x80 + ((value >> 6) & 0x3F))
+				parser.buffer[buffer_len+3] = byte(0x80 + (value & 0x3F))
+				buffer_len += 4
+			}
+
+			parser.unread++
+		}
+
+		// On EOF, put NUL into the buffer and return.
+		if parser.eof {
+			parser.buffer[buffer_len] = 0
+			buffer_len++
+			parser.unread++
+			break
+		}
+	}
+	// [Go] Read the documentation of this function above. To return true,
+	// we need to have the given length in the buffer. Not doing that means
+	// every single check that calls this function to make sure the buffer
+	// has a given length is Go) panicking; or C) accessing invalid memory.
+	// This happens here due to the EOF above breaking early.
+	for buffer_len < length {
+		parser.buffer[buffer_len] = 0
+		buffer_len++
+	}
+	parser.buffer = parser.buffer[:buffer_len]
+	return true
+}
diff --git a/vendor/gopkg.in/yaml.v3/resolve.go b/vendor/gopkg.in/yaml.v3/resolve.go
new file mode 100644
index 000000000..64ae88805
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/resolve.go
@@ -0,0 +1,326 @@
+//
+// Copyright (c) 2011-2019 Canonical Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package yaml
+
+import (
+	"encoding/base64"
+	"math"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+)
+
+type resolveMapItem struct {
+	value interface{}
+	tag   string
+}
+
+var resolveTable = make([]byte, 256)
+var resolveMap = make(map[string]resolveMapItem)
+
+func init() {
+	t := resolveTable
+	t[int('+')] = 'S' // Sign
+	t[int('-')] = 'S'
+	for _, c := range "0123456789" {
+		t[int(c)] = 'D' // Digit
+	}
+	for _, c := range "yYnNtTfFoO~" {
+		t[int(c)] = 'M' // In map
+	}
+	t[int('.')] = '.' // Float (potentially in map)
+
+	var resolveMapList = []struct {
+		v   interface{}
+		tag string
+		l   []string
+	}{
+		{true, boolTag, []string{"true", "True", "TRUE"}},
+		{false, boolTag, []string{"false", "False", "FALSE"}},
+		{nil, nullTag, []string{"", "~", "null", "Null", "NULL"}},
+		{math.NaN(), floatTag, []string{".nan", ".NaN", ".NAN"}},
+		{math.Inf(+1), floatTag, []string{".inf", ".Inf", ".INF"}},
+		{math.Inf(+1), floatTag, []string{"+.inf", "+.Inf", "+.INF"}},
+		{math.Inf(-1), floatTag, []string{"-.inf", "-.Inf", "-.INF"}},
+		{"<<", mergeTag, []string{"<<"}},
+	}
+
+	m := resolveMap
+	for _, item := range resolveMapList {
+		for _, s := range item.l {
+			m[s] = resolveMapItem{item.v, item.tag}
+		}
+	}
+}
+
+const (
+	nullTag      = "!!null"
+	boolTag      = "!!bool"
+	strTag       = "!!str"
+	intTag       = "!!int"
+	floatTag     = "!!float"
+	timestampTag = "!!timestamp"
+	seqTag       = "!!seq"
+	mapTag       = "!!map"
+	binaryTag    = "!!binary"
+	mergeTag     = "!!merge"
+)
+
+var longTags = make(map[string]string)
+var shortTags = make(map[string]string)
+
+func init() {
+	for _, stag := range []string{nullTag, boolTag, strTag, intTag, floatTag, timestampTag, seqTag, mapTag, binaryTag, mergeTag} {
+		ltag := longTag(stag)
+		longTags[stag] = ltag
+		shortTags[ltag] = stag
+	}
+}
+
+const longTagPrefix = "tag:yaml.org,2002:"
+
+func shortTag(tag string) string {
+	if strings.HasPrefix(tag, longTagPrefix) {
+		if stag, ok := shortTags[tag]; ok {
+			return stag
+		}
+		return "!!" + tag[len(longTagPrefix):]
+	}
+	return tag
+}
+
+func longTag(tag string) string {
+	if strings.HasPrefix(tag, "!!") {
+		if ltag, ok := longTags[tag]; ok {
+			return ltag
+		}
+		return longTagPrefix + tag[2:]
+	}
+	return tag
+}
+
+func resolvableTag(tag string) bool {
+	switch tag {
+	case "", strTag, boolTag, intTag, floatTag, nullTag, timestampTag:
+		return true
+	}
+	return false
+}
+
+var yamlStyleFloat = regexp.MustCompile(`^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$`)
+
+func resolve(tag string, in string) (rtag string, out interface{}) {
+	tag = shortTag(tag)
+	if !resolvableTag(tag) {
+		return tag, in
+	}
+
+	defer func() {
+		switch tag {
+		case "", rtag, strTag, binaryTag:
+			return
+		case floatTag:
+			if rtag == intTag {
+				switch v := out.(type) {
+				case int64:
+					rtag = floatTag
+					out = float64(v)
+					return
+				case int:
+					rtag = floatTag
+					out = float64(v)
+					return
+				}
+			}
+		}
+		failf("cannot decode %s `%s` as a %s", shortTag(rtag), in, shortTag(tag))
+	}()
+
+	// Any data is accepted as a !!str or !!binary.
+	// Otherwise, the prefix is enough of a hint about what it might be.
+	hint := byte('N')
+	if in != "" {
+		hint = resolveTable[in[0]]
+	}
+	if hint != 0 && tag != strTag && tag != binaryTag {
+		// Handle things we can lookup in a map.
+		if item, ok := resolveMap[in]; ok {
+			return item.tag, item.value
+		}
+
+		// Base 60 floats are a bad idea, were dropped in YAML 1.2, and
+		// are purposefully unsupported here. They're still quoted on
+		// the way out for compatibility with other parser, though.
+
+		switch hint {
+		case 'M':
+			// We've already checked the map above.
+
+		case '.':
+			// Not in the map, so maybe a normal float.
+			floatv, err := strconv.ParseFloat(in, 64)
+			if err == nil {
+				return floatTag, floatv
+			}
+
+		case 'D', 'S':
+			// Int, float, or timestamp.
+			// Only try values as a timestamp if the value is unquoted or there's an explicit
+			// !!timestamp tag.
+			if tag == "" || tag == timestampTag {
+				t, ok := parseTimestamp(in)
+				if ok {
+					return timestampTag, t
+				}
+			}
+
+			plain := strings.Replace(in, "_", "", -1)
+			intv, err := strconv.ParseInt(plain, 0, 64)
+			if err == nil {
+				if intv == int64(int(intv)) {
+					return intTag, int(intv)
+				} else {
+					return intTag, intv
+				}
+			}
+			uintv, err := strconv.ParseUint(plain, 0, 64)
+			if err == nil {
+				return intTag, uintv
+			}
+			if yamlStyleFloat.MatchString(plain) {
+				floatv, err := strconv.ParseFloat(plain, 64)
+				if err == nil {
+					return floatTag, floatv
+				}
+			}
+			if strings.HasPrefix(plain, "0b") {
+				intv, err := strconv.ParseInt(plain[2:], 2, 64)
+				if err == nil {
+					if intv == int64(int(intv)) {
+						return intTag, int(intv)
+					} else {
+						return intTag, intv
+					}
+				}
+				uintv, err := strconv.ParseUint(plain[2:], 2, 64)
+				if err == nil {
+					return intTag, uintv
+				}
+			} else if strings.HasPrefix(plain, "-0b") {
+				intv, err := strconv.ParseInt("-"+plain[3:], 2, 64)
+				if err == nil {
+					if true || intv == int64(int(intv)) {
+						return intTag, int(intv)
+					} else {
+						return intTag, intv
+					}
+				}
+			}
+			// Octals as introduced in version 1.2 of the spec.
+			// Octals from the 1.1 spec, spelled as 0777, are still
+			// decoded by default in v3 as well for compatibility.
+			// May be dropped in v4 depending on how usage evolves.
+			if strings.HasPrefix(plain, "0o") {
+				intv, err := strconv.ParseInt(plain[2:], 8, 64)
+				if err == nil {
+					if intv == int64(int(intv)) {
+						return intTag, int(intv)
+					} else {
+						return intTag, intv
+					}
+				}
+				uintv, err := strconv.ParseUint(plain[2:], 8, 64)
+				if err == nil {
+					return intTag, uintv
+				}
+			} else if strings.HasPrefix(plain, "-0o") {
+				intv, err := strconv.ParseInt("-"+plain[3:], 8, 64)
+				if err == nil {
+					if true || intv == int64(int(intv)) {
+						return intTag, int(intv)
+					} else {
+						return intTag, intv
+					}
+				}
+			}
+		default:
+			panic("internal error: missing handler for resolver table: " + string(rune(hint)) + " (with " + in + ")")
+		}
+	}
+	return strTag, in
+}
+
+// encodeBase64 encodes s as base64 that is broken up into multiple lines
+// as appropriate for the resulting length.
+func encodeBase64(s string) string {
+	const lineLen = 70
+	encLen := base64.StdEncoding.EncodedLen(len(s))
+	lines := encLen/lineLen + 1
+	buf := make([]byte, encLen*2+lines)
+	in := buf[0:encLen]
+	out := buf[encLen:]
+	base64.StdEncoding.Encode(in, []byte(s))
+	k := 0
+	for i := 0; i < len(in); i += lineLen {
+		j := i + lineLen
+		if j > len(in) {
+			j = len(in)
+		}
+		k += copy(out[k:], in[i:j])
+		if lines > 1 {
+			out[k] = '\n'
+			k++
+		}
+	}
+	return string(out[:k])
+}
+
+// This is a subset of the formats allowed by the regular expression
+// defined at http://yaml.org/type/timestamp.html.
+var allowedTimestampFormats = []string{
+	"2006-1-2T15:4:5.999999999Z07:00", // RCF3339Nano with short date fields.
+	"2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t".
+	"2006-1-2 15:4:5.999999999",       // space separated with no time zone
+	"2006-1-2",                        // date only
+	// Notable exception: time.Parse cannot handle: "2001-12-14 21:59:43.10 -5"
+	// from the set of examples.
+}
+
+// parseTimestamp parses s as a timestamp string and
+// returns the timestamp and reports whether it succeeded.
+// Timestamp formats are defined at http://yaml.org/type/timestamp.html
+func parseTimestamp(s string) (time.Time, bool) {
+	// TODO write code to check all the formats supported by
+	// http://yaml.org/type/timestamp.html instead of using time.Parse.
+
+	// Quick check: all date formats start with YYYY-.
+	i := 0
+	for ; i < len(s); i++ {
+		if c := s[i]; c < '0' || c > '9' {
+			break
+		}
+	}
+	if i != 4 || i == len(s) || s[i] != '-' {
+		return time.Time{}, false
+	}
+	for _, format := range allowedTimestampFormats {
+		if t, err := time.Parse(format, s); err == nil {
+			return t, true
+		}
+	}
+	return time.Time{}, false
+}
diff --git a/vendor/gopkg.in/yaml.v3/scannerc.go b/vendor/gopkg.in/yaml.v3/scannerc.go
new file mode 100644
index 000000000..ca0070108
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/scannerc.go
@@ -0,0 +1,3038 @@
+//
+// Copyright (c) 2011-2019 Canonical Ltd
+// Copyright (c) 2006-2010 Kirill Simonov
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package yaml
+
+import (
+	"bytes"
+	"fmt"
+)
+
+// Introduction
+// ************
+//
+// The following notes assume that you are familiar with the YAML specification
+// (http://yaml.org/spec/1.2/spec.html).  We mostly follow it, although in
+// some cases we are less restrictive that it requires.
+//
+// The process of transforming a YAML stream into a sequence of events is
+// divided on two steps: Scanning and Parsing.
+//
+// The Scanner transforms the input stream into a sequence of tokens, while the
+// parser transform the sequence of tokens produced by the Scanner into a
+// sequence of parsing events.
+//
+// The Scanner is rather clever and complicated. The Parser, on the contrary,
+// is a straightforward implementation of a recursive-descendant parser (or,
+// LL(1) parser, as it is usually called).
+//
+// Actually there are two issues of Scanning that might be called "clever", the
+// rest is quite straightforward.  The issues are "block collection start" and
+// "simple keys".  Both issues are explained below in details.
+//
+// Here the Scanning step is explained and implemented.  We start with the list
+// of all the tokens produced by the Scanner together with short descriptions.
+//
+// Now, tokens:
+//
+//      STREAM-START(encoding)          # The stream start.
+//      STREAM-END                      # The stream end.
+//      VERSION-DIRECTIVE(major,minor)  # The '%YAML' directive.
+//      TAG-DIRECTIVE(handle,prefix)    # The '%TAG' directive.
+//      DOCUMENT-START                  # '---'
+//      DOCUMENT-END                    # '...'
+//      BLOCK-SEQUENCE-START            # Indentation increase denoting a block
+//      BLOCK-MAPPING-START             # sequence or a block mapping.
+//      BLOCK-END                       # Indentation decrease.
+//      FLOW-SEQUENCE-START             # '['
+//      FLOW-SEQUENCE-END               # ']'
+//      BLOCK-SEQUENCE-START            # '{'
+//      BLOCK-SEQUENCE-END              # '}'
+//      BLOCK-ENTRY                     # '-'
+//      FLOW-ENTRY                      # ','
+//      KEY                             # '?' or nothing (simple keys).
+//      VALUE                           # ':'
+//      ALIAS(anchor)                   # '*anchor'
+//      ANCHOR(anchor)                  # '&anchor'
+//      TAG(handle,suffix)              # '!handle!suffix'
+//      SCALAR(value,style)             # A scalar.
+//
+// The following two tokens are "virtual" tokens denoting the beginning and the
+// end of the stream:
+//
+//      STREAM-START(encoding)
+//      STREAM-END
+//
+// We pass the information about the input stream encoding with the
+// STREAM-START token.
+//
+// The next two tokens are responsible for tags:
+//
+//      VERSION-DIRECTIVE(major,minor)
+//      TAG-DIRECTIVE(handle,prefix)
+//
+// Example:
+//
+//      %YAML   1.1
+//      %TAG    !   !foo
+//      %TAG    !yaml!  tag:yaml.org,2002:
+//      ---
+//
+// The correspoding sequence of tokens:
+//
+//      STREAM-START(utf-8)
+//      VERSION-DIRECTIVE(1,1)
+//      TAG-DIRECTIVE("!","!foo")
+//      TAG-DIRECTIVE("!yaml","tag:yaml.org,2002:")
+//      DOCUMENT-START
+//      STREAM-END
+//
+// Note that the VERSION-DIRECTIVE and TAG-DIRECTIVE tokens occupy a whole
+// line.
+//
+// The document start and end indicators are represented by:
+//
+//      DOCUMENT-START
+//      DOCUMENT-END
+//
+// Note that if a YAML stream contains an implicit document (without '---'
+// and '...' indicators), no DOCUMENT-START and DOCUMENT-END tokens will be
+// produced.
+//
+// In the following examples, we present whole documents together with the
+// produced tokens.
+//
+//      1. An implicit document:
+//
+//          'a scalar'
+//
+//      Tokens:
+//
+//          STREAM-START(utf-8)
+//          SCALAR("a scalar",single-quoted)
+//          STREAM-END
+//
+//      2. An explicit document:
+//
+//          ---
+//          'a scalar'
+//          ...
+//
+//      Tokens:
+//
+//          STREAM-START(utf-8)
+//          DOCUMENT-START
+//          SCALAR("a scalar",single-quoted)
+//          DOCUMENT-END
+//          STREAM-END
+//
+//      3. Several documents in a stream:
+//
+//          'a scalar'
+//          ---
+//          'another scalar'
+//          ---
+//          'yet another scalar'
+//
+//      Tokens:
+//
+//          STREAM-START(utf-8)
+//          SCALAR("a scalar",single-quoted)
+//          DOCUMENT-START
+//          SCALAR("another scalar",single-quoted)
+//          DOCUMENT-START
+//          SCALAR("yet another scalar",single-quoted)
+//          STREAM-END
+//
+// We have already introduced the SCALAR token above.  The following tokens are
+// used to describe aliases, anchors, tag, and scalars:
+//
+//      ALIAS(anchor)
+//      ANCHOR(anchor)
+//      TAG(handle,suffix)
+//      SCALAR(value,style)
+//
+// The following series of examples illustrate the usage of these tokens:
+//
+//      1. A recursive sequence:
+//
+//          &A [ *A ]
+//
+//      Tokens:
+//
+//          STREAM-START(utf-8)
+//          ANCHOR("A")
+//          FLOW-SEQUENCE-START
+//          ALIAS("A")
+//          FLOW-SEQUENCE-END
+//          STREAM-END
+//
+//      2. A tagged scalar:
+//
+//          !!float "3.14"  # A good approximation.
+//
+//      Tokens:
+//
+//          STREAM-START(utf-8)
+//          TAG("!!","float")
+//          SCALAR("3.14",double-quoted)
+//          STREAM-END
+//
+//      3. Various scalar styles:
+//
+//          --- # Implicit empty plain scalars do not produce tokens.
+//          --- a plain scalar
+//          --- 'a single-quoted scalar'
+//          --- "a double-quoted scalar"
+//          --- |-
+//            a literal scalar
+//          --- >-
+//            a folded
+//            scalar
+//
+//      Tokens:
+//
+//          STREAM-START(utf-8)
+//          DOCUMENT-START
+//          DOCUMENT-START
+//          SCALAR("a plain scalar",plain)
+//          DOCUMENT-START
+//          SCALAR("a single-quoted scalar",single-quoted)
+//          DOCUMENT-START
+//          SCALAR("a double-quoted scalar",double-quoted)
+//          DOCUMENT-START
+//          SCALAR("a literal scalar",literal)
+//          DOCUMENT-START
+//          SCALAR("a folded scalar",folded)
+//          STREAM-END
+//
+// Now it's time to review collection-related tokens. We will start with
+// flow collections:
+//
+//      FLOW-SEQUENCE-START
+//      FLOW-SEQUENCE-END
+//      FLOW-MAPPING-START
+//      FLOW-MAPPING-END
+//      FLOW-ENTRY
+//      KEY
+//      VALUE
+//
+// The tokens FLOW-SEQUENCE-START, FLOW-SEQUENCE-END, FLOW-MAPPING-START, and
+// FLOW-MAPPING-END represent the indicators '[', ']', '{', and '}'
+// correspondingly.  FLOW-ENTRY represent the ',' indicator.  Finally the
+// indicators '?' and ':', which are used for denoting mapping keys and values,
+// are represented by the KEY and VALUE tokens.
+//
+// The following examples show flow collections:
+//
+//      1. A flow sequence:
+//
+//          [item 1, item 2, item 3]
+//
+//      Tokens:
+//
+//          STREAM-START(utf-8)
+//          FLOW-SEQUENCE-START
+//          SCALAR("item 1",plain)
+//          FLOW-ENTRY
+//          SCALAR("item 2",plain)
+//          FLOW-ENTRY
+//          SCALAR("item 3",plain)
+//          FLOW-SEQUENCE-END
+//          STREAM-END
+//
+//      2. A flow mapping:
+//
+//          {
+//              a simple key: a value,  # Note that the KEY token is produced.
+//              ? a complex key: another value,
+//          }
+//
+//      Tokens:
+//
+//          STREAM-START(utf-8)
+//          FLOW-MAPPING-START
+//          KEY
+//          SCALAR("a simple key",plain)
+//          VALUE
+//          SCALAR("a value",plain)
+//          FLOW-ENTRY
+//          KEY
+//          SCALAR("a complex key",plain)
+//          VALUE
+//          SCALAR("another value",plain)
+//          FLOW-ENTRY
+//          FLOW-MAPPING-END
+//          STREAM-END
+//
+// A simple key is a key which is not denoted by the '?' indicator.  Note that
+// the Scanner still produce the KEY token whenever it encounters a simple key.
+//
+// For scanning block collections, the following tokens are used (note that we
+// repeat KEY and VALUE here):
+//
+//      BLOCK-SEQUENCE-START
+//      BLOCK-MAPPING-START
+//      BLOCK-END
+//      BLOCK-ENTRY
+//      KEY
+//      VALUE
+//
+// The tokens BLOCK-SEQUENCE-START and BLOCK-MAPPING-START denote indentation
+// increase that precedes a block collection (cf. the INDENT token in Python).
+// The token BLOCK-END denote indentation decrease that ends a block collection
+// (cf. the DEDENT token in Python).  However YAML has some syntax pecularities
+// that makes detections of these tokens more complex.
+//
+// The tokens BLOCK-ENTRY, KEY, and VALUE are used to represent the indicators
+// '-', '?', and ':' correspondingly.
+//
+// The following examples show how the tokens BLOCK-SEQUENCE-START,
+// BLOCK-MAPPING-START, and BLOCK-END are emitted by the Scanner:
+//
+//      1. Block sequences:
+//
+//          - item 1
+//          - item 2
+//          -
+//            - item 3.1
+//            - item 3.2
+//          -
+//            key 1: value 1
+//            key 2: value 2
+//
+//      Tokens:
+//
+//          STREAM-START(utf-8)
+//          BLOCK-SEQUENCE-START
+//          BLOCK-ENTRY
+//          SCALAR("item 1",plain)
+//          BLOCK-ENTRY
+//          SCALAR("item 2",plain)
+//          BLOCK-ENTRY
+//          BLOCK-SEQUENCE-START
+//          BLOCK-ENTRY
+//          SCALAR("item 3.1",plain)
+//          BLOCK-ENTRY
+//          SCALAR("item 3.2",plain)
+//          BLOCK-END
+//          BLOCK-ENTRY
+//          BLOCK-MAPPING-START
+//          KEY
+//          SCALAR("key 1",plain)
+//          VALUE
+//          SCALAR("value 1",plain)
+//          KEY
+//          SCALAR("key 2",plain)
+//          VALUE
+//          SCALAR("value 2",plain)
+//          BLOCK-END
+//          BLOCK-END
+//          STREAM-END
+//
+//      2. Block mappings:
+//
+//          a simple key: a value   # The KEY token is produced here.
+//          ? a complex key
+//          : another value
+//          a mapping:
+//            key 1: value 1
+//            key 2: value 2
+//          a sequence:
+//            - item 1
+//            - item 2
+//
+//      Tokens:
+//
+//          STREAM-START(utf-8)
+//          BLOCK-MAPPING-START
+//          KEY
+//          SCALAR("a simple key",plain)
+//          VALUE
+//          SCALAR("a value",plain)
+//          KEY
+//          SCALAR("a complex key",plain)
+//          VALUE
+//          SCALAR("another value",plain)
+//          KEY
+//          SCALAR("a mapping",plain)
+//          BLOCK-MAPPING-START
+//          KEY
+//          SCALAR("key 1",plain)
+//          VALUE
+//          SCALAR("value 1",plain)
+//          KEY
+//          SCALAR("key 2",plain)
+//          VALUE
+//          SCALAR("value 2",plain)
+//          BLOCK-END
+//          KEY
+//          SCALAR("a sequence",plain)
+//          VALUE
+//          BLOCK-SEQUENCE-START
+//          BLOCK-ENTRY
+//          SCALAR("item 1",plain)
+//          BLOCK-ENTRY
+//          SCALAR("item 2",plain)
+//          BLOCK-END
+//          BLOCK-END
+//          STREAM-END
+//
+// YAML does not always require to start a new block collection from a new
+// line.  If the current line contains only '-', '?', and ':' indicators, a new
+// block collection may start at the current line.  The following examples
+// illustrate this case:
+//
+//      1. Collections in a sequence:
+//
+//          - - item 1
+//            - item 2
+//          - key 1: value 1
+//            key 2: value 2
+//          - ? complex key
+//            : complex value
+//
+//      Tokens:
+//
+//          STREAM-START(utf-8)
+//          BLOCK-SEQUENCE-START
+//          BLOCK-ENTRY
+//          BLOCK-SEQUENCE-START
+//          BLOCK-ENTRY
+//          SCALAR("item 1",plain)
+//          BLOCK-ENTRY
+//          SCALAR("item 2",plain)
+//          BLOCK-END
+//          BLOCK-ENTRY
+//          BLOCK-MAPPING-START
+//          KEY
+//          SCALAR("key 1",plain)
+//          VALUE
+//          SCALAR("value 1",plain)
+//          KEY
+//          SCALAR("key 2",plain)
+//          VALUE
+//          SCALAR("value 2",plain)
+//          BLOCK-END
+//          BLOCK-ENTRY
+//          BLOCK-MAPPING-START
+//          KEY
+//          SCALAR("complex key")
+//          VALUE
+//          SCALAR("complex value")
+//          BLOCK-END
+//          BLOCK-END
+//          STREAM-END
+//
+//      2. Collections in a mapping:
+//
+//          ? a sequence
+//          : - item 1
+//            - item 2
+//          ? a mapping
+//          : key 1: value 1
+//            key 2: value 2
+//
+//      Tokens:
+//
+//          STREAM-START(utf-8)
+//          BLOCK-MAPPING-START
+//          KEY
+//          SCALAR("a sequence",plain)
+//          VALUE
+//          BLOCK-SEQUENCE-START
+//          BLOCK-ENTRY
+//          SCALAR("item 1",plain)
+//          BLOCK-ENTRY
+//          SCALAR("item 2",plain)
+//          BLOCK-END
+//          KEY
+//          SCALAR("a mapping",plain)
+//          VALUE
+//          BLOCK-MAPPING-START
+//          KEY
+//          SCALAR("key 1",plain)
+//          VALUE
+//          SCALAR("value 1",plain)
+//          KEY
+//          SCALAR("key 2",plain)
+//          VALUE
+//          SCALAR("value 2",plain)
+//          BLOCK-END
+//          BLOCK-END
+//          STREAM-END
+//
+// YAML also permits non-indented sequences if they are included into a block
+// mapping.  In this case, the token BLOCK-SEQUENCE-START is not produced:
+//
+//      key:
+//      - item 1    # BLOCK-SEQUENCE-START is NOT produced here.
+//      - item 2
+//
+// Tokens:
+//
+//      STREAM-START(utf-8)
+//      BLOCK-MAPPING-START
+//      KEY
+//      SCALAR("key",plain)
+//      VALUE
+//      BLOCK-ENTRY
+//      SCALAR("item 1",plain)
+//      BLOCK-ENTRY
+//      SCALAR("item 2",plain)
+//      BLOCK-END
+//
+
+// Ensure that the buffer contains the required number of characters.
+// Return true on success, false on failure (reader error or memory error).
+func cache(parser *yaml_parser_t, length int) bool {
+	// [Go] This was inlined: !cache(A, B) -> unread < B && !update(A, B)
+	return parser.unread >= length || yaml_parser_update_buffer(parser, length)
+}
+
+// Advance the buffer pointer.
+func skip(parser *yaml_parser_t) {
+	if !is_blank(parser.buffer, parser.buffer_pos) {
+		parser.newlines = 0
+	}
+	parser.mark.index++
+	parser.mark.column++
+	parser.unread--
+	parser.buffer_pos += width(parser.buffer[parser.buffer_pos])
+}
+
+func skip_line(parser *yaml_parser_t) {
+	if is_crlf(parser.buffer, parser.buffer_pos) {
+		parser.mark.index += 2
+		parser.mark.column = 0
+		parser.mark.line++
+		parser.unread -= 2
+		parser.buffer_pos += 2
+		parser.newlines++
+	} else if is_break(parser.buffer, parser.buffer_pos) {
+		parser.mark.index++
+		parser.mark.column = 0
+		parser.mark.line++
+		parser.unread--
+		parser.buffer_pos += width(parser.buffer[parser.buffer_pos])
+		parser.newlines++
+	}
+}
+
+// Copy a character to a string buffer and advance pointers.
+func read(parser *yaml_parser_t, s []byte) []byte {
+	if !is_blank(parser.buffer, parser.buffer_pos) {
+		parser.newlines = 0
+	}
+	w := width(parser.buffer[parser.buffer_pos])
+	if w == 0 {
+		panic("invalid character sequence")
+	}
+	if len(s) == 0 {
+		s = make([]byte, 0, 32)
+	}
+	if w == 1 && len(s)+w <= cap(s) {
+		s = s[:len(s)+1]
+		s[len(s)-1] = parser.buffer[parser.buffer_pos]
+		parser.buffer_pos++
+	} else {
+		s = append(s, parser.buffer[parser.buffer_pos:parser.buffer_pos+w]...)
+		parser.buffer_pos += w
+	}
+	parser.mark.index++
+	parser.mark.column++
+	parser.unread--
+	return s
+}
+
+// Copy a line break character to a string buffer and advance pointers.
+func read_line(parser *yaml_parser_t, s []byte) []byte {
+	buf := parser.buffer
+	pos := parser.buffer_pos
+	switch {
+	case buf[pos] == '\r' && buf[pos+1] == '\n':
+		// CR LF . LF
+		s = append(s, '\n')
+		parser.buffer_pos += 2
+		parser.mark.index++
+		parser.unread--
+	case buf[pos] == '\r' || buf[pos] == '\n':
+		// CR|LF . LF
+		s = append(s, '\n')
+		parser.buffer_pos += 1
+	case buf[pos] == '\xC2' && buf[pos+1] == '\x85':
+		// NEL . LF
+		s = append(s, '\n')
+		parser.buffer_pos += 2
+	case buf[pos] == '\xE2' && buf[pos+1] == '\x80' && (buf[pos+2] == '\xA8' || buf[pos+2] == '\xA9'):
+		// LS|PS . LS|PS
+		s = append(s, buf[parser.buffer_pos:pos+3]...)
+		parser.buffer_pos += 3
+	default:
+		return s
+	}
+	parser.mark.index++
+	parser.mark.column = 0
+	parser.mark.line++
+	parser.unread--
+	parser.newlines++
+	return s
+}
+
+// Get the next token.
+func yaml_parser_scan(parser *yaml_parser_t, token *yaml_token_t) bool {
+	// Erase the token object.
+	*token = yaml_token_t{} // [Go] Is this necessary?
+
+	// No tokens after STREAM-END or error.
+	if parser.stream_end_produced || parser.error != yaml_NO_ERROR {
+		return true
+	}
+
+	// Ensure that the tokens queue contains enough tokens.
+	if !parser.token_available {
+		if !yaml_parser_fetch_more_tokens(parser) {
+			return false
+		}
+	}
+
+	// Fetch the next token from the queue.
+	*token = parser.tokens[parser.tokens_head]
+	parser.tokens_head++
+	parser.tokens_parsed++
+	parser.token_available = false
+
+	if token.typ == yaml_STREAM_END_TOKEN {
+		parser.stream_end_produced = true
+	}
+	return true
+}
+
+// Set the scanner error and return false.
+func yaml_parser_set_scanner_error(parser *yaml_parser_t, context string, context_mark yaml_mark_t, problem string) bool {
+	parser.error = yaml_SCANNER_ERROR
+	parser.context = context
+	parser.context_mark = context_mark
+	parser.problem = problem
+	parser.problem_mark = parser.mark
+	return false
+}
+
+func yaml_parser_set_scanner_tag_error(parser *yaml_parser_t, directive bool, context_mark yaml_mark_t, problem string) bool {
+	context := "while parsing a tag"
+	if directive {
+		context = "while parsing a %TAG directive"
+	}
+	return yaml_parser_set_scanner_error(parser, context, context_mark, problem)
+}
+
+func trace(args ...interface{}) func() {
+	pargs := append([]interface{}{"+++"}, args...)
+	fmt.Println(pargs...)
+	pargs = append([]interface{}{"---"}, args...)
+	return func() { fmt.Println(pargs...) }
+}
+
+// Ensure that the tokens queue contains at least one token which can be
+// returned to the Parser.
+func yaml_parser_fetch_more_tokens(parser *yaml_parser_t) bool {
+	// While we need more tokens to fetch, do it.
+	for {
+		// [Go] The comment parsing logic requires a lookahead of two tokens
+		// so that foot comments may be parsed in time of associating them
+		// with the tokens that are parsed before them, and also for line
+		// comments to be transformed into head comments in some edge cases.
+		if parser.tokens_head < len(parser.tokens)-2 {
+			// If a potential simple key is at the head position, we need to fetch
+			// the next token to disambiguate it.
+			head_tok_idx, ok := parser.simple_keys_by_tok[parser.tokens_parsed]
+			if !ok {
+				break
+			} else if valid, ok := yaml_simple_key_is_valid(parser, &parser.simple_keys[head_tok_idx]); !ok {
+				return false
+			} else if !valid {
+				break
+			}
+		}
+		// Fetch the next token.
+		if !yaml_parser_fetch_next_token(parser) {
+			return false
+		}
+	}
+
+	parser.token_available = true
+	return true
+}
+
+// The dispatcher for token fetchers.
+func yaml_parser_fetch_next_token(parser *yaml_parser_t) (ok bool) {
+	// Ensure that the buffer is initialized.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+
+	// Check if we just started scanning.  Fetch STREAM-START then.
+	if !parser.stream_start_produced {
+		return yaml_parser_fetch_stream_start(parser)
+	}
+
+	scan_mark := parser.mark
+
+	// Eat whitespaces and comments until we reach the next token.
+	if !yaml_parser_scan_to_next_token(parser) {
+		return false
+	}
+
+	// [Go] While unrolling indents, transform the head comments of prior
+	// indentation levels observed after scan_start into foot comments at
+	// the respective indexes.
+
+	// Check the indentation level against the current column.
+	if !yaml_parser_unroll_indent(parser, parser.mark.column, scan_mark) {
+		return false
+	}
+
+	// Ensure that the buffer contains at least 4 characters.  4 is the length
+	// of the longest indicators ('--- ' and '... ').
+	if parser.unread < 4 && !yaml_parser_update_buffer(parser, 4) {
+		return false
+	}
+
+	// Is it the end of the stream?
+	if is_z(parser.buffer, parser.buffer_pos) {
+		return yaml_parser_fetch_stream_end(parser)
+	}
+
+	// Is it a directive?
+	if parser.mark.column == 0 && parser.buffer[parser.buffer_pos] == '%' {
+		return yaml_parser_fetch_directive(parser)
+	}
+
+	buf := parser.buffer
+	pos := parser.buffer_pos
+
+	// Is it the document start indicator?
+	if parser.mark.column == 0 && buf[pos] == '-' && buf[pos+1] == '-' && buf[pos+2] == '-' && is_blankz(buf, pos+3) {
+		return yaml_parser_fetch_document_indicator(parser, yaml_DOCUMENT_START_TOKEN)
+	}
+
+	// Is it the document end indicator?
+	if parser.mark.column == 0 && buf[pos] == '.' && buf[pos+1] == '.' && buf[pos+2] == '.' && is_blankz(buf, pos+3) {
+		return yaml_parser_fetch_document_indicator(parser, yaml_DOCUMENT_END_TOKEN)
+	}
+
+	comment_mark := parser.mark
+	if len(parser.tokens) > 0 && (parser.flow_level == 0 && buf[pos] == ':' || parser.flow_level > 0 && buf[pos] == ',') {
+		// Associate any following comments with the prior token.
+		comment_mark = parser.tokens[len(parser.tokens)-1].start_mark
+	}
+	defer func() {
+		if !ok {
+			return
+		}
+		if len(parser.tokens) > 0 && parser.tokens[len(parser.tokens)-1].typ == yaml_BLOCK_ENTRY_TOKEN {
+			// Sequence indicators alone have no line comments. It becomes
+			// a head comment for whatever follows.
+			return
+		}
+		if !yaml_parser_scan_line_comment(parser, comment_mark) {
+			ok = false
+			return
+		}
+	}()
+
+	// Is it the flow sequence start indicator?
+	if buf[pos] == '[' {
+		return yaml_parser_fetch_flow_collection_start(parser, yaml_FLOW_SEQUENCE_START_TOKEN)
+	}
+
+	// Is it the flow mapping start indicator?
+	if parser.buffer[parser.buffer_pos] == '{' {
+		return yaml_parser_fetch_flow_collection_start(parser, yaml_FLOW_MAPPING_START_TOKEN)
+	}
+
+	// Is it the flow sequence end indicator?
+	if parser.buffer[parser.buffer_pos] == ']' {
+		return yaml_parser_fetch_flow_collection_end(parser,
+			yaml_FLOW_SEQUENCE_END_TOKEN)
+	}
+
+	// Is it the flow mapping end indicator?
+	if parser.buffer[parser.buffer_pos] == '}' {
+		return yaml_parser_fetch_flow_collection_end(parser,
+			yaml_FLOW_MAPPING_END_TOKEN)
+	}
+
+	// Is it the flow entry indicator?
+	if parser.buffer[parser.buffer_pos] == ',' {
+		return yaml_parser_fetch_flow_entry(parser)
+	}
+
+	// Is it the block entry indicator?
+	if parser.buffer[parser.buffer_pos] == '-' && is_blankz(parser.buffer, parser.buffer_pos+1) {
+		return yaml_parser_fetch_block_entry(parser)
+	}
+
+	// Is it the key indicator?
+	if parser.buffer[parser.buffer_pos] == '?' && (parser.flow_level > 0 || is_blankz(parser.buffer, parser.buffer_pos+1)) {
+		return yaml_parser_fetch_key(parser)
+	}
+
+	// Is it the value indicator?
+	if parser.buffer[parser.buffer_pos] == ':' && (parser.flow_level > 0 || is_blankz(parser.buffer, parser.buffer_pos+1)) {
+		return yaml_parser_fetch_value(parser)
+	}
+
+	// Is it an alias?
+	if parser.buffer[parser.buffer_pos] == '*' {
+		return yaml_parser_fetch_anchor(parser, yaml_ALIAS_TOKEN)
+	}
+
+	// Is it an anchor?
+	if parser.buffer[parser.buffer_pos] == '&' {
+		return yaml_parser_fetch_anchor(parser, yaml_ANCHOR_TOKEN)
+	}
+
+	// Is it a tag?
+	if parser.buffer[parser.buffer_pos] == '!' {
+		return yaml_parser_fetch_tag(parser)
+	}
+
+	// Is it a literal scalar?
+	if parser.buffer[parser.buffer_pos] == '|' && parser.flow_level == 0 {
+		return yaml_parser_fetch_block_scalar(parser, true)
+	}
+
+	// Is it a folded scalar?
+	if parser.buffer[parser.buffer_pos] == '>' && parser.flow_level == 0 {
+		return yaml_parser_fetch_block_scalar(parser, false)
+	}
+
+	// Is it a single-quoted scalar?
+	if parser.buffer[parser.buffer_pos] == '\'' {
+		return yaml_parser_fetch_flow_scalar(parser, true)
+	}
+
+	// Is it a double-quoted scalar?
+	if parser.buffer[parser.buffer_pos] == '"' {
+		return yaml_parser_fetch_flow_scalar(parser, false)
+	}
+
+	// Is it a plain scalar?
+	//
+	// A plain scalar may start with any non-blank characters except
+	//
+	//      '-', '?', ':', ',', '[', ']', '{', '}',
+	//      '#', '&', '*', '!', '|', '>', '\'', '\"',
+	//      '%', '@', '`'.
+	//
+	// In the block context (and, for the '-' indicator, in the flow context
+	// too), it may also start with the characters
+	//
+	//      '-', '?', ':'
+	//
+	// if it is followed by a non-space character.
+	//
+	// The last rule is more restrictive than the specification requires.
+	// [Go] TODO Make this logic more reasonable.
+	//switch parser.buffer[parser.buffer_pos] {
+	//case '-', '?', ':', ',', '?', '-', ',', ':', ']', '[', '}', '{', '&', '#', '!', '*', '>', '|', '"', '\'', '@', '%', '-', '`':
+	//}
+	if !(is_blankz(parser.buffer, parser.buffer_pos) || parser.buffer[parser.buffer_pos] == '-' ||
+		parser.buffer[parser.buffer_pos] == '?' || parser.buffer[parser.buffer_pos] == ':' ||
+		parser.buffer[parser.buffer_pos] == ',' || parser.buffer[parser.buffer_pos] == '[' ||
+		parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '{' ||
+		parser.buffer[parser.buffer_pos] == '}' || parser.buffer[parser.buffer_pos] == '#' ||
+		parser.buffer[parser.buffer_pos] == '&' || parser.buffer[parser.buffer_pos] == '*' ||
+		parser.buffer[parser.buffer_pos] == '!' || parser.buffer[parser.buffer_pos] == '|' ||
+		parser.buffer[parser.buffer_pos] == '>' || parser.buffer[parser.buffer_pos] == '\'' ||
+		parser.buffer[parser.buffer_pos] == '"' || parser.buffer[parser.buffer_pos] == '%' ||
+		parser.buffer[parser.buffer_pos] == '@' || parser.buffer[parser.buffer_pos] == '`') ||
+		(parser.buffer[parser.buffer_pos] == '-' && !is_blank(parser.buffer, parser.buffer_pos+1)) ||
+		(parser.flow_level == 0 &&
+			(parser.buffer[parser.buffer_pos] == '?' || parser.buffer[parser.buffer_pos] == ':') &&
+			!is_blankz(parser.buffer, parser.buffer_pos+1)) {
+		return yaml_parser_fetch_plain_scalar(parser)
+	}
+
+	// If we don't determine the token type so far, it is an error.
+	return yaml_parser_set_scanner_error(parser,
+		"while scanning for the next token", parser.mark,
+		"found character that cannot start any token")
+}
+
+func yaml_simple_key_is_valid(parser *yaml_parser_t, simple_key *yaml_simple_key_t) (valid, ok bool) {
+	if !simple_key.possible {
+		return false, true
+	}
+
+	// The 1.2 specification says:
+	//
+	//     "If the ? indicator is omitted, parsing needs to see past the
+	//     implicit key to recognize it as such. To limit the amount of
+	//     lookahead required, the “:” indicator must appear at most 1024
+	//     Unicode characters beyond the start of the key. In addition, the key
+	//     is restricted to a single line."
+	//
+	if simple_key.mark.line < parser.mark.line || simple_key.mark.index+1024 < parser.mark.index {
+		// Check if the potential simple key to be removed is required.
+		if simple_key.required {
+			return false, yaml_parser_set_scanner_error(parser,
+				"while scanning a simple key", simple_key.mark,
+				"could not find expected ':'")
+		}
+		simple_key.possible = false
+		return false, true
+	}
+	return true, true
+}
+
+// Check if a simple key may start at the current position and add it if
+// needed.
+func yaml_parser_save_simple_key(parser *yaml_parser_t) bool {
+	// A simple key is required at the current position if the scanner is in
+	// the block context and the current column coincides with the indentation
+	// level.
+
+	required := parser.flow_level == 0 && parser.indent == parser.mark.column
+
+	//
+	// If the current position may start a simple key, save it.
+	//
+	if parser.simple_key_allowed {
+		simple_key := yaml_simple_key_t{
+			possible:     true,
+			required:     required,
+			token_number: parser.tokens_parsed + (len(parser.tokens) - parser.tokens_head),
+			mark:         parser.mark,
+		}
+
+		if !yaml_parser_remove_simple_key(parser) {
+			return false
+		}
+		parser.simple_keys[len(parser.simple_keys)-1] = simple_key
+		parser.simple_keys_by_tok[simple_key.token_number] = len(parser.simple_keys) - 1
+	}
+	return true
+}
+
+// Remove a potential simple key at the current flow level.
+func yaml_parser_remove_simple_key(parser *yaml_parser_t) bool {
+	i := len(parser.simple_keys) - 1
+	if parser.simple_keys[i].possible {
+		// If the key is required, it is an error.
+		if parser.simple_keys[i].required {
+			return yaml_parser_set_scanner_error(parser,
+				"while scanning a simple key", parser.simple_keys[i].mark,
+				"could not find expected ':'")
+		}
+		// Remove the key from the stack.
+		parser.simple_keys[i].possible = false
+		delete(parser.simple_keys_by_tok, parser.simple_keys[i].token_number)
+	}
+	return true
+}
+
+// max_flow_level limits the flow_level
+const max_flow_level = 10000
+
+// Increase the flow level and resize the simple key list if needed.
+func yaml_parser_increase_flow_level(parser *yaml_parser_t) bool {
+	// Reset the simple key on the next level.
+	parser.simple_keys = append(parser.simple_keys, yaml_simple_key_t{
+		possible:     false,
+		required:     false,
+		token_number: parser.tokens_parsed + (len(parser.tokens) - parser.tokens_head),
+		mark:         parser.mark,
+	})
+
+	// Increase the flow level.
+	parser.flow_level++
+	if parser.flow_level > max_flow_level {
+		return yaml_parser_set_scanner_error(parser,
+			"while increasing flow level", parser.simple_keys[len(parser.simple_keys)-1].mark,
+			fmt.Sprintf("exceeded max depth of %d", max_flow_level))
+	}
+	return true
+}
+
+// Decrease the flow level.
+func yaml_parser_decrease_flow_level(parser *yaml_parser_t) bool {
+	if parser.flow_level > 0 {
+		parser.flow_level--
+		last := len(parser.simple_keys) - 1
+		delete(parser.simple_keys_by_tok, parser.simple_keys[last].token_number)
+		parser.simple_keys = parser.simple_keys[:last]
+	}
+	return true
+}
+
+// max_indents limits the indents stack size
+const max_indents = 10000
+
+// Push the current indentation level to the stack and set the new level
+// the current column is greater than the indentation level.  In this case,
+// append or insert the specified token into the token queue.
+func yaml_parser_roll_indent(parser *yaml_parser_t, column, number int, typ yaml_token_type_t, mark yaml_mark_t) bool {
+	// In the flow context, do nothing.
+	if parser.flow_level > 0 {
+		return true
+	}
+
+	if parser.indent < column {
+		// Push the current indentation level to the stack and set the new
+		// indentation level.
+		parser.indents = append(parser.indents, parser.indent)
+		parser.indent = column
+		if len(parser.indents) > max_indents {
+			return yaml_parser_set_scanner_error(parser,
+				"while increasing indent level", parser.simple_keys[len(parser.simple_keys)-1].mark,
+				fmt.Sprintf("exceeded max depth of %d", max_indents))
+		}
+
+		// Create a token and insert it into the queue.
+		token := yaml_token_t{
+			typ:        typ,
+			start_mark: mark,
+			end_mark:   mark,
+		}
+		if number > -1 {
+			number -= parser.tokens_parsed
+		}
+		yaml_insert_token(parser, number, &token)
+	}
+	return true
+}
+
+// Pop indentation levels from the indents stack until the current level
+// becomes less or equal to the column.  For each indentation level, append
+// the BLOCK-END token.
+func yaml_parser_unroll_indent(parser *yaml_parser_t, column int, scan_mark yaml_mark_t) bool {
+	// In the flow context, do nothing.
+	if parser.flow_level > 0 {
+		return true
+	}
+
+	block_mark := scan_mark
+	block_mark.index--
+
+	// Loop through the indentation levels in the stack.
+	for parser.indent > column {
+
+		// [Go] Reposition the end token before potential following
+		//      foot comments of parent blocks. For that, search
+		//      backwards for recent comments that were at the same
+		//      indent as the block that is ending now.
+		stop_index := block_mark.index
+		for i := len(parser.comments) - 1; i >= 0; i-- {
+			comment := &parser.comments[i]
+
+			if comment.end_mark.index < stop_index {
+				// Don't go back beyond the start of the comment/whitespace scan, unless column < 0.
+				// If requested indent column is < 0, then the document is over and everything else
+				// is a foot anyway.
+				break
+			}
+			if comment.start_mark.column == parser.indent+1 {
+				// This is a good match. But maybe there's a former comment
+				// at that same indent level, so keep searching.
+				block_mark = comment.start_mark
+			}
+
+			// While the end of the former comment matches with
+			// the start of the following one, we know there's
+			// nothing in between and scanning is still safe.
+			stop_index = comment.scan_mark.index
+		}
+
+		// Create a token and append it to the queue.
+		token := yaml_token_t{
+			typ:        yaml_BLOCK_END_TOKEN,
+			start_mark: block_mark,
+			end_mark:   block_mark,
+		}
+		yaml_insert_token(parser, -1, &token)
+
+		// Pop the indentation level.
+		parser.indent = parser.indents[len(parser.indents)-1]
+		parser.indents = parser.indents[:len(parser.indents)-1]
+	}
+	return true
+}
+
+// Initialize the scanner and produce the STREAM-START token.
+func yaml_parser_fetch_stream_start(parser *yaml_parser_t) bool {
+
+	// Set the initial indentation.
+	parser.indent = -1
+
+	// Initialize the simple key stack.
+	parser.simple_keys = append(parser.simple_keys, yaml_simple_key_t{})
+
+	parser.simple_keys_by_tok = make(map[int]int)
+
+	// A simple key is allowed at the beginning of the stream.
+	parser.simple_key_allowed = true
+
+	// We have started.
+	parser.stream_start_produced = true
+
+	// Create the STREAM-START token and append it to the queue.
+	token := yaml_token_t{
+		typ:        yaml_STREAM_START_TOKEN,
+		start_mark: parser.mark,
+		end_mark:   parser.mark,
+		encoding:   parser.encoding,
+	}
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce the STREAM-END token and shut down the scanner.
+func yaml_parser_fetch_stream_end(parser *yaml_parser_t) bool {
+
+	// Force new line.
+	if parser.mark.column != 0 {
+		parser.mark.column = 0
+		parser.mark.line++
+	}
+
+	// Reset the indentation level.
+	if !yaml_parser_unroll_indent(parser, -1, parser.mark) {
+		return false
+	}
+
+	// Reset simple keys.
+	if !yaml_parser_remove_simple_key(parser) {
+		return false
+	}
+
+	parser.simple_key_allowed = false
+
+	// Create the STREAM-END token and append it to the queue.
+	token := yaml_token_t{
+		typ:        yaml_STREAM_END_TOKEN,
+		start_mark: parser.mark,
+		end_mark:   parser.mark,
+	}
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce a VERSION-DIRECTIVE or TAG-DIRECTIVE token.
+func yaml_parser_fetch_directive(parser *yaml_parser_t) bool {
+	// Reset the indentation level.
+	if !yaml_parser_unroll_indent(parser, -1, parser.mark) {
+		return false
+	}
+
+	// Reset simple keys.
+	if !yaml_parser_remove_simple_key(parser) {
+		return false
+	}
+
+	parser.simple_key_allowed = false
+
+	// Create the YAML-DIRECTIVE or TAG-DIRECTIVE token.
+	token := yaml_token_t{}
+	if !yaml_parser_scan_directive(parser, &token) {
+		return false
+	}
+	// Append the token to the queue.
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce the DOCUMENT-START or DOCUMENT-END token.
+func yaml_parser_fetch_document_indicator(parser *yaml_parser_t, typ yaml_token_type_t) bool {
+	// Reset the indentation level.
+	if !yaml_parser_unroll_indent(parser, -1, parser.mark) {
+		return false
+	}
+
+	// Reset simple keys.
+	if !yaml_parser_remove_simple_key(parser) {
+		return false
+	}
+
+	parser.simple_key_allowed = false
+
+	// Consume the token.
+	start_mark := parser.mark
+
+	skip(parser)
+	skip(parser)
+	skip(parser)
+
+	end_mark := parser.mark
+
+	// Create the DOCUMENT-START or DOCUMENT-END token.
+	token := yaml_token_t{
+		typ:        typ,
+		start_mark: start_mark,
+		end_mark:   end_mark,
+	}
+	// Append the token to the queue.
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce the FLOW-SEQUENCE-START or FLOW-MAPPING-START token.
+func yaml_parser_fetch_flow_collection_start(parser *yaml_parser_t, typ yaml_token_type_t) bool {
+
+	// The indicators '[' and '{' may start a simple key.
+	if !yaml_parser_save_simple_key(parser) {
+		return false
+	}
+
+	// Increase the flow level.
+	if !yaml_parser_increase_flow_level(parser) {
+		return false
+	}
+
+	// A simple key may follow the indicators '[' and '{'.
+	parser.simple_key_allowed = true
+
+	// Consume the token.
+	start_mark := parser.mark
+	skip(parser)
+	end_mark := parser.mark
+
+	// Create the FLOW-SEQUENCE-START of FLOW-MAPPING-START token.
+	token := yaml_token_t{
+		typ:        typ,
+		start_mark: start_mark,
+		end_mark:   end_mark,
+	}
+	// Append the token to the queue.
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce the FLOW-SEQUENCE-END or FLOW-MAPPING-END token.
+func yaml_parser_fetch_flow_collection_end(parser *yaml_parser_t, typ yaml_token_type_t) bool {
+	// Reset any potential simple key on the current flow level.
+	if !yaml_parser_remove_simple_key(parser) {
+		return false
+	}
+
+	// Decrease the flow level.
+	if !yaml_parser_decrease_flow_level(parser) {
+		return false
+	}
+
+	// No simple keys after the indicators ']' and '}'.
+	parser.simple_key_allowed = false
+
+	// Consume the token.
+
+	start_mark := parser.mark
+	skip(parser)
+	end_mark := parser.mark
+
+	// Create the FLOW-SEQUENCE-END of FLOW-MAPPING-END token.
+	token := yaml_token_t{
+		typ:        typ,
+		start_mark: start_mark,
+		end_mark:   end_mark,
+	}
+	// Append the token to the queue.
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce the FLOW-ENTRY token.
+func yaml_parser_fetch_flow_entry(parser *yaml_parser_t) bool {
+	// Reset any potential simple keys on the current flow level.
+	if !yaml_parser_remove_simple_key(parser) {
+		return false
+	}
+
+	// Simple keys are allowed after ','.
+	parser.simple_key_allowed = true
+
+	// Consume the token.
+	start_mark := parser.mark
+	skip(parser)
+	end_mark := parser.mark
+
+	// Create the FLOW-ENTRY token and append it to the queue.
+	token := yaml_token_t{
+		typ:        yaml_FLOW_ENTRY_TOKEN,
+		start_mark: start_mark,
+		end_mark:   end_mark,
+	}
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce the BLOCK-ENTRY token.
+func yaml_parser_fetch_block_entry(parser *yaml_parser_t) bool {
+	// Check if the scanner is in the block context.
+	if parser.flow_level == 0 {
+		// Check if we are allowed to start a new entry.
+		if !parser.simple_key_allowed {
+			return yaml_parser_set_scanner_error(parser, "", parser.mark,
+				"block sequence entries are not allowed in this context")
+		}
+		// Add the BLOCK-SEQUENCE-START token if needed.
+		if !yaml_parser_roll_indent(parser, parser.mark.column, -1, yaml_BLOCK_SEQUENCE_START_TOKEN, parser.mark) {
+			return false
+		}
+	} else {
+		// It is an error for the '-' indicator to occur in the flow context,
+		// but we let the Parser detect and report about it because the Parser
+		// is able to point to the context.
+	}
+
+	// Reset any potential simple keys on the current flow level.
+	if !yaml_parser_remove_simple_key(parser) {
+		return false
+	}
+
+	// Simple keys are allowed after '-'.
+	parser.simple_key_allowed = true
+
+	// Consume the token.
+	start_mark := parser.mark
+	skip(parser)
+	end_mark := parser.mark
+
+	// Create the BLOCK-ENTRY token and append it to the queue.
+	token := yaml_token_t{
+		typ:        yaml_BLOCK_ENTRY_TOKEN,
+		start_mark: start_mark,
+		end_mark:   end_mark,
+	}
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce the KEY token.
+func yaml_parser_fetch_key(parser *yaml_parser_t) bool {
+
+	// In the block context, additional checks are required.
+	if parser.flow_level == 0 {
+		// Check if we are allowed to start a new key (not nessesary simple).
+		if !parser.simple_key_allowed {
+			return yaml_parser_set_scanner_error(parser, "", parser.mark,
+				"mapping keys are not allowed in this context")
+		}
+		// Add the BLOCK-MAPPING-START token if needed.
+		if !yaml_parser_roll_indent(parser, parser.mark.column, -1, yaml_BLOCK_MAPPING_START_TOKEN, parser.mark) {
+			return false
+		}
+	}
+
+	// Reset any potential simple keys on the current flow level.
+	if !yaml_parser_remove_simple_key(parser) {
+		return false
+	}
+
+	// Simple keys are allowed after '?' in the block context.
+	parser.simple_key_allowed = parser.flow_level == 0
+
+	// Consume the token.
+	start_mark := parser.mark
+	skip(parser)
+	end_mark := parser.mark
+
+	// Create the KEY token and append it to the queue.
+	token := yaml_token_t{
+		typ:        yaml_KEY_TOKEN,
+		start_mark: start_mark,
+		end_mark:   end_mark,
+	}
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce the VALUE token.
+func yaml_parser_fetch_value(parser *yaml_parser_t) bool {
+
+	simple_key := &parser.simple_keys[len(parser.simple_keys)-1]
+
+	// Have we found a simple key?
+	if valid, ok := yaml_simple_key_is_valid(parser, simple_key); !ok {
+		return false
+
+	} else if valid {
+
+		// Create the KEY token and insert it into the queue.
+		token := yaml_token_t{
+			typ:        yaml_KEY_TOKEN,
+			start_mark: simple_key.mark,
+			end_mark:   simple_key.mark,
+		}
+		yaml_insert_token(parser, simple_key.token_number-parser.tokens_parsed, &token)
+
+		// In the block context, we may need to add the BLOCK-MAPPING-START token.
+		if !yaml_parser_roll_indent(parser, simple_key.mark.column,
+			simple_key.token_number,
+			yaml_BLOCK_MAPPING_START_TOKEN, simple_key.mark) {
+			return false
+		}
+
+		// Remove the simple key.
+		simple_key.possible = false
+		delete(parser.simple_keys_by_tok, simple_key.token_number)
+
+		// A simple key cannot follow another simple key.
+		parser.simple_key_allowed = false
+
+	} else {
+		// The ':' indicator follows a complex key.
+
+		// In the block context, extra checks are required.
+		if parser.flow_level == 0 {
+
+			// Check if we are allowed to start a complex value.
+			if !parser.simple_key_allowed {
+				return yaml_parser_set_scanner_error(parser, "", parser.mark,
+					"mapping values are not allowed in this context")
+			}
+
+			// Add the BLOCK-MAPPING-START token if needed.
+			if !yaml_parser_roll_indent(parser, parser.mark.column, -1, yaml_BLOCK_MAPPING_START_TOKEN, parser.mark) {
+				return false
+			}
+		}
+
+		// Simple keys after ':' are allowed in the block context.
+		parser.simple_key_allowed = parser.flow_level == 0
+	}
+
+	// Consume the token.
+	start_mark := parser.mark
+	skip(parser)
+	end_mark := parser.mark
+
+	// Create the VALUE token and append it to the queue.
+	token := yaml_token_t{
+		typ:        yaml_VALUE_TOKEN,
+		start_mark: start_mark,
+		end_mark:   end_mark,
+	}
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce the ALIAS or ANCHOR token.
+func yaml_parser_fetch_anchor(parser *yaml_parser_t, typ yaml_token_type_t) bool {
+	// An anchor or an alias could be a simple key.
+	if !yaml_parser_save_simple_key(parser) {
+		return false
+	}
+
+	// A simple key cannot follow an anchor or an alias.
+	parser.simple_key_allowed = false
+
+	// Create the ALIAS or ANCHOR token and append it to the queue.
+	var token yaml_token_t
+	if !yaml_parser_scan_anchor(parser, &token, typ) {
+		return false
+	}
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce the TAG token.
+func yaml_parser_fetch_tag(parser *yaml_parser_t) bool {
+	// A tag could be a simple key.
+	if !yaml_parser_save_simple_key(parser) {
+		return false
+	}
+
+	// A simple key cannot follow a tag.
+	parser.simple_key_allowed = false
+
+	// Create the TAG token and append it to the queue.
+	var token yaml_token_t
+	if !yaml_parser_scan_tag(parser, &token) {
+		return false
+	}
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce the SCALAR(...,literal) or SCALAR(...,folded) tokens.
+func yaml_parser_fetch_block_scalar(parser *yaml_parser_t, literal bool) bool {
+	// Remove any potential simple keys.
+	if !yaml_parser_remove_simple_key(parser) {
+		return false
+	}
+
+	// A simple key may follow a block scalar.
+	parser.simple_key_allowed = true
+
+	// Create the SCALAR token and append it to the queue.
+	var token yaml_token_t
+	if !yaml_parser_scan_block_scalar(parser, &token, literal) {
+		return false
+	}
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce the SCALAR(...,single-quoted) or SCALAR(...,double-quoted) tokens.
+func yaml_parser_fetch_flow_scalar(parser *yaml_parser_t, single bool) bool {
+	// A plain scalar could be a simple key.
+	if !yaml_parser_save_simple_key(parser) {
+		return false
+	}
+
+	// A simple key cannot follow a flow scalar.
+	parser.simple_key_allowed = false
+
+	// Create the SCALAR token and append it to the queue.
+	var token yaml_token_t
+	if !yaml_parser_scan_flow_scalar(parser, &token, single) {
+		return false
+	}
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Produce the SCALAR(...,plain) token.
+func yaml_parser_fetch_plain_scalar(parser *yaml_parser_t) bool {
+	// A plain scalar could be a simple key.
+	if !yaml_parser_save_simple_key(parser) {
+		return false
+	}
+
+	// A simple key cannot follow a flow scalar.
+	parser.simple_key_allowed = false
+
+	// Create the SCALAR token and append it to the queue.
+	var token yaml_token_t
+	if !yaml_parser_scan_plain_scalar(parser, &token) {
+		return false
+	}
+	yaml_insert_token(parser, -1, &token)
+	return true
+}
+
+// Eat whitespaces and comments until the next token is found.
+func yaml_parser_scan_to_next_token(parser *yaml_parser_t) bool {
+
+	scan_mark := parser.mark
+
+	// Until the next token is not found.
+	for {
+		// Allow the BOM mark to start a line.
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+		if parser.mark.column == 0 && is_bom(parser.buffer, parser.buffer_pos) {
+			skip(parser)
+		}
+
+		// Eat whitespaces.
+		// Tabs are allowed:
+		//  - in the flow context
+		//  - in the block context, but not at the beginning of the line or
+		//  after '-', '?', or ':' (complex value).
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+
+		for parser.buffer[parser.buffer_pos] == ' ' || ((parser.flow_level > 0 || !parser.simple_key_allowed) && parser.buffer[parser.buffer_pos] == '\t') {
+			skip(parser)
+			if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+				return false
+			}
+		}
+
+		// Check if we just had a line comment under a sequence entry that
+		// looks more like a header to the following content. Similar to this:
+		//
+		// - # The comment
+		//   - Some data
+		//
+		// If so, transform the line comment to a head comment and reposition.
+		if len(parser.comments) > 0 && len(parser.tokens) > 1 {
+			tokenA := parser.tokens[len(parser.tokens)-2]
+			tokenB := parser.tokens[len(parser.tokens)-1]
+			comment := &parser.comments[len(parser.comments)-1]
+			if tokenA.typ == yaml_BLOCK_SEQUENCE_START_TOKEN && tokenB.typ == yaml_BLOCK_ENTRY_TOKEN && len(comment.line) > 0 && !is_break(parser.buffer, parser.buffer_pos) {
+				// If it was in the prior line, reposition so it becomes a
+				// header of the follow up token. Otherwise, keep it in place
+				// so it becomes a header of the former.
+				comment.head = comment.line
+				comment.line = nil
+				if comment.start_mark.line == parser.mark.line-1 {
+					comment.token_mark = parser.mark
+				}
+			}
+		}
+
+		// Eat a comment until a line break.
+		if parser.buffer[parser.buffer_pos] == '#' {
+			if !yaml_parser_scan_comments(parser, scan_mark) {
+				return false
+			}
+		}
+
+		// If it is a line break, eat it.
+		if is_break(parser.buffer, parser.buffer_pos) {
+			if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) {
+				return false
+			}
+			skip_line(parser)
+
+			// In the block context, a new line may start a simple key.
+			if parser.flow_level == 0 {
+				parser.simple_key_allowed = true
+			}
+		} else {
+			break // We have found a token.
+		}
+	}
+
+	return true
+}
+
+// Scan a YAML-DIRECTIVE or TAG-DIRECTIVE token.
+//
+// Scope:
+//      %YAML    1.1    # a comment \n
+//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+//      %TAG    !yaml!  tag:yaml.org,2002:  \n
+//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+//
+func yaml_parser_scan_directive(parser *yaml_parser_t, token *yaml_token_t) bool {
+	// Eat '%'.
+	start_mark := parser.mark
+	skip(parser)
+
+	// Scan the directive name.
+	var name []byte
+	if !yaml_parser_scan_directive_name(parser, start_mark, &name) {
+		return false
+	}
+
+	// Is it a YAML directive?
+	if bytes.Equal(name, []byte("YAML")) {
+		// Scan the VERSION directive value.
+		var major, minor int8
+		if !yaml_parser_scan_version_directive_value(parser, start_mark, &major, &minor) {
+			return false
+		}
+		end_mark := parser.mark
+
+		// Create a VERSION-DIRECTIVE token.
+		*token = yaml_token_t{
+			typ:        yaml_VERSION_DIRECTIVE_TOKEN,
+			start_mark: start_mark,
+			end_mark:   end_mark,
+			major:      major,
+			minor:      minor,
+		}
+
+		// Is it a TAG directive?
+	} else if bytes.Equal(name, []byte("TAG")) {
+		// Scan the TAG directive value.
+		var handle, prefix []byte
+		if !yaml_parser_scan_tag_directive_value(parser, start_mark, &handle, &prefix) {
+			return false
+		}
+		end_mark := parser.mark
+
+		// Create a TAG-DIRECTIVE token.
+		*token = yaml_token_t{
+			typ:        yaml_TAG_DIRECTIVE_TOKEN,
+			start_mark: start_mark,
+			end_mark:   end_mark,
+			value:      handle,
+			prefix:     prefix,
+		}
+
+		// Unknown directive.
+	} else {
+		yaml_parser_set_scanner_error(parser, "while scanning a directive",
+			start_mark, "found unknown directive name")
+		return false
+	}
+
+	// Eat the rest of the line including any comments.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+
+	for is_blank(parser.buffer, parser.buffer_pos) {
+		skip(parser)
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+	}
+
+	if parser.buffer[parser.buffer_pos] == '#' {
+		// [Go] Discard this inline comment for the time being.
+		//if !yaml_parser_scan_line_comment(parser, start_mark) {
+		//	return false
+		//}
+		for !is_breakz(parser.buffer, parser.buffer_pos) {
+			skip(parser)
+			if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+				return false
+			}
+		}
+	}
+
+	// Check if we are at the end of the line.
+	if !is_breakz(parser.buffer, parser.buffer_pos) {
+		yaml_parser_set_scanner_error(parser, "while scanning a directive",
+			start_mark, "did not find expected comment or line break")
+		return false
+	}
+
+	// Eat a line break.
+	if is_break(parser.buffer, parser.buffer_pos) {
+		if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) {
+			return false
+		}
+		skip_line(parser)
+	}
+
+	return true
+}
+
+// Scan the directive name.
+//
+// Scope:
+//      %YAML   1.1     # a comment \n
+//       ^^^^
+//      %TAG    !yaml!  tag:yaml.org,2002:  \n
+//       ^^^
+//
+func yaml_parser_scan_directive_name(parser *yaml_parser_t, start_mark yaml_mark_t, name *[]byte) bool {
+	// Consume the directive name.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+
+	var s []byte
+	for is_alpha(parser.buffer, parser.buffer_pos) {
+		s = read(parser, s)
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+	}
+
+	// Check if the name is empty.
+	if len(s) == 0 {
+		yaml_parser_set_scanner_error(parser, "while scanning a directive",
+			start_mark, "could not find expected directive name")
+		return false
+	}
+
+	// Check for an blank character after the name.
+	if !is_blankz(parser.buffer, parser.buffer_pos) {
+		yaml_parser_set_scanner_error(parser, "while scanning a directive",
+			start_mark, "found unexpected non-alphabetical character")
+		return false
+	}
+	*name = s
+	return true
+}
+
+// Scan the value of VERSION-DIRECTIVE.
+//
+// Scope:
+//      %YAML   1.1     # a comment \n
+//           ^^^^^^
+func yaml_parser_scan_version_directive_value(parser *yaml_parser_t, start_mark yaml_mark_t, major, minor *int8) bool {
+	// Eat whitespaces.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+	for is_blank(parser.buffer, parser.buffer_pos) {
+		skip(parser)
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+	}
+
+	// Consume the major version number.
+	if !yaml_parser_scan_version_directive_number(parser, start_mark, major) {
+		return false
+	}
+
+	// Eat '.'.
+	if parser.buffer[parser.buffer_pos] != '.' {
+		return yaml_parser_set_scanner_error(parser, "while scanning a %YAML directive",
+			start_mark, "did not find expected digit or '.' character")
+	}
+
+	skip(parser)
+
+	// Consume the minor version number.
+	if !yaml_parser_scan_version_directive_number(parser, start_mark, minor) {
+		return false
+	}
+	return true
+}
+
+const max_number_length = 2
+
+// Scan the version number of VERSION-DIRECTIVE.
+//
+// Scope:
+//      %YAML   1.1     # a comment \n
+//              ^
+//      %YAML   1.1     # a comment \n
+//                ^
+func yaml_parser_scan_version_directive_number(parser *yaml_parser_t, start_mark yaml_mark_t, number *int8) bool {
+
+	// Repeat while the next character is digit.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+	var value, length int8
+	for is_digit(parser.buffer, parser.buffer_pos) {
+		// Check if the number is too long.
+		length++
+		if length > max_number_length {
+			return yaml_parser_set_scanner_error(parser, "while scanning a %YAML directive",
+				start_mark, "found extremely long version number")
+		}
+		value = value*10 + int8(as_digit(parser.buffer, parser.buffer_pos))
+		skip(parser)
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+	}
+
+	// Check if the number was present.
+	if length == 0 {
+		return yaml_parser_set_scanner_error(parser, "while scanning a %YAML directive",
+			start_mark, "did not find expected version number")
+	}
+	*number = value
+	return true
+}
+
+// Scan the value of a TAG-DIRECTIVE token.
+//
+// Scope:
+//      %TAG    !yaml!  tag:yaml.org,2002:  \n
+//          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+//
+func yaml_parser_scan_tag_directive_value(parser *yaml_parser_t, start_mark yaml_mark_t, handle, prefix *[]byte) bool {
+	var handle_value, prefix_value []byte
+
+	// Eat whitespaces.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+
+	for is_blank(parser.buffer, parser.buffer_pos) {
+		skip(parser)
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+	}
+
+	// Scan a handle.
+	if !yaml_parser_scan_tag_handle(parser, true, start_mark, &handle_value) {
+		return false
+	}
+
+	// Expect a whitespace.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+	if !is_blank(parser.buffer, parser.buffer_pos) {
+		yaml_parser_set_scanner_error(parser, "while scanning a %TAG directive",
+			start_mark, "did not find expected whitespace")
+		return false
+	}
+
+	// Eat whitespaces.
+	for is_blank(parser.buffer, parser.buffer_pos) {
+		skip(parser)
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+	}
+
+	// Scan a prefix.
+	if !yaml_parser_scan_tag_uri(parser, true, nil, start_mark, &prefix_value) {
+		return false
+	}
+
+	// Expect a whitespace or line break.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+	if !is_blankz(parser.buffer, parser.buffer_pos) {
+		yaml_parser_set_scanner_error(parser, "while scanning a %TAG directive",
+			start_mark, "did not find expected whitespace or line break")
+		return false
+	}
+
+	*handle = handle_value
+	*prefix = prefix_value
+	return true
+}
+
+func yaml_parser_scan_anchor(parser *yaml_parser_t, token *yaml_token_t, typ yaml_token_type_t) bool {
+	var s []byte
+
+	// Eat the indicator character.
+	start_mark := parser.mark
+	skip(parser)
+
+	// Consume the value.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+
+	for is_alpha(parser.buffer, parser.buffer_pos) {
+		s = read(parser, s)
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+	}
+
+	end_mark := parser.mark
+
+	/*
+	 * Check if length of the anchor is greater than 0 and it is followed by
+	 * a whitespace character or one of the indicators:
+	 *
+	 *      '?', ':', ',', ']', '}', '%', '@', '`'.
+	 */
+
+	if len(s) == 0 ||
+		!(is_blankz(parser.buffer, parser.buffer_pos) || parser.buffer[parser.buffer_pos] == '?' ||
+			parser.buffer[parser.buffer_pos] == ':' || parser.buffer[parser.buffer_pos] == ',' ||
+			parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '}' ||
+			parser.buffer[parser.buffer_pos] == '%' || parser.buffer[parser.buffer_pos] == '@' ||
+			parser.buffer[parser.buffer_pos] == '`') {
+		context := "while scanning an alias"
+		if typ == yaml_ANCHOR_TOKEN {
+			context = "while scanning an anchor"
+		}
+		yaml_parser_set_scanner_error(parser, context, start_mark,
+			"did not find expected alphabetic or numeric character")
+		return false
+	}
+
+	// Create a token.
+	*token = yaml_token_t{
+		typ:        typ,
+		start_mark: start_mark,
+		end_mark:   end_mark,
+		value:      s,
+	}
+
+	return true
+}
+
+/*
+ * Scan a TAG token.
+ */
+
+func yaml_parser_scan_tag(parser *yaml_parser_t, token *yaml_token_t) bool {
+	var handle, suffix []byte
+
+	start_mark := parser.mark
+
+	// Check if the tag is in the canonical form.
+	if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) {
+		return false
+	}
+
+	if parser.buffer[parser.buffer_pos+1] == '<' {
+		// Keep the handle as ''
+
+		// Eat '!<'
+		skip(parser)
+		skip(parser)
+
+		// Consume the tag value.
+		if !yaml_parser_scan_tag_uri(parser, false, nil, start_mark, &suffix) {
+			return false
+		}
+
+		// Check for '>' and eat it.
+		if parser.buffer[parser.buffer_pos] != '>' {
+			yaml_parser_set_scanner_error(parser, "while scanning a tag",
+				start_mark, "did not find the expected '>'")
+			return false
+		}
+
+		skip(parser)
+	} else {
+		// The tag has either the '!suffix' or the '!handle!suffix' form.
+
+		// First, try to scan a handle.
+		if !yaml_parser_scan_tag_handle(parser, false, start_mark, &handle) {
+			return false
+		}
+
+		// Check if it is, indeed, handle.
+		if handle[0] == '!' && len(handle) > 1 && handle[len(handle)-1] == '!' {
+			// Scan the suffix now.
+			if !yaml_parser_scan_tag_uri(parser, false, nil, start_mark, &suffix) {
+				return false
+			}
+		} else {
+			// It wasn't a handle after all.  Scan the rest of the tag.
+			if !yaml_parser_scan_tag_uri(parser, false, handle, start_mark, &suffix) {
+				return false
+			}
+
+			// Set the handle to '!'.
+			handle = []byte{'!'}
+
+			// A special case: the '!' tag.  Set the handle to '' and the
+			// suffix to '!'.
+			if len(suffix) == 0 {
+				handle, suffix = suffix, handle
+			}
+		}
+	}
+
+	// Check the character which ends the tag.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+	if !is_blankz(parser.buffer, parser.buffer_pos) {
+		yaml_parser_set_scanner_error(parser, "while scanning a tag",
+			start_mark, "did not find expected whitespace or line break")
+		return false
+	}
+
+	end_mark := parser.mark
+
+	// Create a token.
+	*token = yaml_token_t{
+		typ:        yaml_TAG_TOKEN,
+		start_mark: start_mark,
+		end_mark:   end_mark,
+		value:      handle,
+		suffix:     suffix,
+	}
+	return true
+}
+
+// Scan a tag handle.
+func yaml_parser_scan_tag_handle(parser *yaml_parser_t, directive bool, start_mark yaml_mark_t, handle *[]byte) bool {
+	// Check the initial '!' character.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+	if parser.buffer[parser.buffer_pos] != '!' {
+		yaml_parser_set_scanner_tag_error(parser, directive,
+			start_mark, "did not find expected '!'")
+		return false
+	}
+
+	var s []byte
+
+	// Copy the '!' character.
+	s = read(parser, s)
+
+	// Copy all subsequent alphabetical and numerical characters.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+	for is_alpha(parser.buffer, parser.buffer_pos) {
+		s = read(parser, s)
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+	}
+
+	// Check if the trailing character is '!' and copy it.
+	if parser.buffer[parser.buffer_pos] == '!' {
+		s = read(parser, s)
+	} else {
+		// It's either the '!' tag or not really a tag handle.  If it's a %TAG
+		// directive, it's an error.  If it's a tag token, it must be a part of URI.
+		if directive && string(s) != "!" {
+			yaml_parser_set_scanner_tag_error(parser, directive,
+				start_mark, "did not find expected '!'")
+			return false
+		}
+	}
+
+	*handle = s
+	return true
+}
+
+// Scan a tag.
+func yaml_parser_scan_tag_uri(parser *yaml_parser_t, directive bool, head []byte, start_mark yaml_mark_t, uri *[]byte) bool {
+	//size_t length = head ? strlen((char *)head) : 0
+	var s []byte
+	hasTag := len(head) > 0
+
+	// Copy the head if needed.
+	//
+	// Note that we don't copy the leading '!' character.
+	if len(head) > 1 {
+		s = append(s, head[1:]...)
+	}
+
+	// Scan the tag.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+
+	// The set of characters that may appear in URI is as follows:
+	//
+	//      '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', '&',
+	//      '=', '+', '$', ',', '.', '!', '~', '*', '\'', '(', ')', '[', ']',
+	//      '%'.
+	// [Go] TODO Convert this into more reasonable logic.
+	for is_alpha(parser.buffer, parser.buffer_pos) || parser.buffer[parser.buffer_pos] == ';' ||
+		parser.buffer[parser.buffer_pos] == '/' || parser.buffer[parser.buffer_pos] == '?' ||
+		parser.buffer[parser.buffer_pos] == ':' || parser.buffer[parser.buffer_pos] == '@' ||
+		parser.buffer[parser.buffer_pos] == '&' || parser.buffer[parser.buffer_pos] == '=' ||
+		parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '$' ||
+		parser.buffer[parser.buffer_pos] == ',' || parser.buffer[parser.buffer_pos] == '.' ||
+		parser.buffer[parser.buffer_pos] == '!' || parser.buffer[parser.buffer_pos] == '~' ||
+		parser.buffer[parser.buffer_pos] == '*' || parser.buffer[parser.buffer_pos] == '\'' ||
+		parser.buffer[parser.buffer_pos] == '(' || parser.buffer[parser.buffer_pos] == ')' ||
+		parser.buffer[parser.buffer_pos] == '[' || parser.buffer[parser.buffer_pos] == ']' ||
+		parser.buffer[parser.buffer_pos] == '%' {
+		// Check if it is a URI-escape sequence.
+		if parser.buffer[parser.buffer_pos] == '%' {
+			if !yaml_parser_scan_uri_escapes(parser, directive, start_mark, &s) {
+				return false
+			}
+		} else {
+			s = read(parser, s)
+		}
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+		hasTag = true
+	}
+
+	if !hasTag {
+		yaml_parser_set_scanner_tag_error(parser, directive,
+			start_mark, "did not find expected tag URI")
+		return false
+	}
+	*uri = s
+	return true
+}
+
+// Decode an URI-escape sequence corresponding to a single UTF-8 character.
+func yaml_parser_scan_uri_escapes(parser *yaml_parser_t, directive bool, start_mark yaml_mark_t, s *[]byte) bool {
+
+	// Decode the required number of characters.
+	w := 1024
+	for w > 0 {
+		// Check for a URI-escaped octet.
+		if parser.unread < 3 && !yaml_parser_update_buffer(parser, 3) {
+			return false
+		}
+
+		if !(parser.buffer[parser.buffer_pos] == '%' &&
+			is_hex(parser.buffer, parser.buffer_pos+1) &&
+			is_hex(parser.buffer, parser.buffer_pos+2)) {
+			return yaml_parser_set_scanner_tag_error(parser, directive,
+				start_mark, "did not find URI escaped octet")
+		}
+
+		// Get the octet.
+		octet := byte((as_hex(parser.buffer, parser.buffer_pos+1) << 4) + as_hex(parser.buffer, parser.buffer_pos+2))
+
+		// If it is the leading octet, determine the length of the UTF-8 sequence.
+		if w == 1024 {
+			w = width(octet)
+			if w == 0 {
+				return yaml_parser_set_scanner_tag_error(parser, directive,
+					start_mark, "found an incorrect leading UTF-8 octet")
+			}
+		} else {
+			// Check if the trailing octet is correct.
+			if octet&0xC0 != 0x80 {
+				return yaml_parser_set_scanner_tag_error(parser, directive,
+					start_mark, "found an incorrect trailing UTF-8 octet")
+			}
+		}
+
+		// Copy the octet and move the pointers.
+		*s = append(*s, octet)
+		skip(parser)
+		skip(parser)
+		skip(parser)
+		w--
+	}
+	return true
+}
+
+// Scan a block scalar.
+func yaml_parser_scan_block_scalar(parser *yaml_parser_t, token *yaml_token_t, literal bool) bool {
+	// Eat the indicator '|' or '>'.
+	start_mark := parser.mark
+	skip(parser)
+
+	// Scan the additional block scalar indicators.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+
+	// Check for a chomping indicator.
+	var chomping, increment int
+	if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' {
+		// Set the chomping method and eat the indicator.
+		if parser.buffer[parser.buffer_pos] == '+' {
+			chomping = +1
+		} else {
+			chomping = -1
+		}
+		skip(parser)
+
+		// Check for an indentation indicator.
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+		if is_digit(parser.buffer, parser.buffer_pos) {
+			// Check that the indentation is greater than 0.
+			if parser.buffer[parser.buffer_pos] == '0' {
+				yaml_parser_set_scanner_error(parser, "while scanning a block scalar",
+					start_mark, "found an indentation indicator equal to 0")
+				return false
+			}
+
+			// Get the indentation level and eat the indicator.
+			increment = as_digit(parser.buffer, parser.buffer_pos)
+			skip(parser)
+		}
+
+	} else if is_digit(parser.buffer, parser.buffer_pos) {
+		// Do the same as above, but in the opposite order.
+
+		if parser.buffer[parser.buffer_pos] == '0' {
+			yaml_parser_set_scanner_error(parser, "while scanning a block scalar",
+				start_mark, "found an indentation indicator equal to 0")
+			return false
+		}
+		increment = as_digit(parser.buffer, parser.buffer_pos)
+		skip(parser)
+
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+		if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' {
+			if parser.buffer[parser.buffer_pos] == '+' {
+				chomping = +1
+			} else {
+				chomping = -1
+			}
+			skip(parser)
+		}
+	}
+
+	// Eat whitespaces and comments to the end of the line.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+	for is_blank(parser.buffer, parser.buffer_pos) {
+		skip(parser)
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+	}
+	if parser.buffer[parser.buffer_pos] == '#' {
+		if !yaml_parser_scan_line_comment(parser, start_mark) {
+			return false
+		}
+		for !is_breakz(parser.buffer, parser.buffer_pos) {
+			skip(parser)
+			if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+				return false
+			}
+		}
+	}
+
+	// Check if we are at the end of the line.
+	if !is_breakz(parser.buffer, parser.buffer_pos) {
+		yaml_parser_set_scanner_error(parser, "while scanning a block scalar",
+			start_mark, "did not find expected comment or line break")
+		return false
+	}
+
+	// Eat a line break.
+	if is_break(parser.buffer, parser.buffer_pos) {
+		if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) {
+			return false
+		}
+		skip_line(parser)
+	}
+
+	end_mark := parser.mark
+
+	// Set the indentation level if it was specified.
+	var indent int
+	if increment > 0 {
+		if parser.indent >= 0 {
+			indent = parser.indent + increment
+		} else {
+			indent = increment
+		}
+	}
+
+	// Scan the leading line breaks and determine the indentation level if needed.
+	var s, leading_break, trailing_breaks []byte
+	if !yaml_parser_scan_block_scalar_breaks(parser, &indent, &trailing_breaks, start_mark, &end_mark) {
+		return false
+	}
+
+	// Scan the block scalar content.
+	if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+		return false
+	}
+	var leading_blank, trailing_blank bool
+	for parser.mark.column == indent && !is_z(parser.buffer, parser.buffer_pos) {
+		// We are at the beginning of a non-empty line.
+
+		// Is it a trailing whitespace?
+		trailing_blank = is_blank(parser.buffer, parser.buffer_pos)
+
+		// Check if we need to fold the leading line break.
+		if !literal && !leading_blank && !trailing_blank && len(leading_break) > 0 && leading_break[0] == '\n' {
+			// Do we need to join the lines by space?
+			if len(trailing_breaks) == 0 {
+				s = append(s, ' ')
+			}
+		} else {
+			s = append(s, leading_break...)
+		}
+		leading_break = leading_break[:0]
+
+		// Append the remaining line breaks.
+		s = append(s, trailing_breaks...)
+		trailing_breaks = trailing_breaks[:0]
+
+		// Is it a leading whitespace?
+		leading_blank = is_blank(parser.buffer, parser.buffer_pos)
+
+		// Consume the current line.
+		for !is_breakz(parser.buffer, parser.buffer_pos) {
+			s = read(parser, s)
+			if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+				return false
+			}
+		}
+
+		// Consume the line break.
+		if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) {
+			return false
+		}
+
+		leading_break = read_line(parser, leading_break)
+
+		// Eat the following indentation spaces and line breaks.
+		if !yaml_parser_scan_block_scalar_breaks(parser, &indent, &trailing_breaks, start_mark, &end_mark) {
+			return false
+		}
+	}
+
+	// Chomp the tail.
+	if chomping != -1 {
+		s = append(s, leading_break...)
+	}
+	if chomping == 1 {
+		s = append(s, trailing_breaks...)
+	}
+
+	// Create a token.
+	*token = yaml_token_t{
+		typ:        yaml_SCALAR_TOKEN,
+		start_mark: start_mark,
+		end_mark:   end_mark,
+		value:      s,
+		style:      yaml_LITERAL_SCALAR_STYLE,
+	}
+	if !literal {
+		token.style = yaml_FOLDED_SCALAR_STYLE
+	}
+	return true
+}
+
+// Scan indentation spaces and line breaks for a block scalar.  Determine the
+// indentation level if needed.
+func yaml_parser_scan_block_scalar_breaks(parser *yaml_parser_t, indent *int, breaks *[]byte, start_mark yaml_mark_t, end_mark *yaml_mark_t) bool {
+	*end_mark = parser.mark
+
+	// Eat the indentation spaces and line breaks.
+	max_indent := 0
+	for {
+		// Eat the indentation spaces.
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+		for (*indent == 0 || parser.mark.column < *indent) && is_space(parser.buffer, parser.buffer_pos) {
+			skip(parser)
+			if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+				return false
+			}
+		}
+		if parser.mark.column > max_indent {
+			max_indent = parser.mark.column
+		}
+
+		// Check for a tab character messing the indentation.
+		if (*indent == 0 || parser.mark.column < *indent) && is_tab(parser.buffer, parser.buffer_pos) {
+			return yaml_parser_set_scanner_error(parser, "while scanning a block scalar",
+				start_mark, "found a tab character where an indentation space is expected")
+		}
+
+		// Have we found a non-empty line?
+		if !is_break(parser.buffer, parser.buffer_pos) {
+			break
+		}
+
+		// Consume the line break.
+		if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) {
+			return false
+		}
+		// [Go] Should really be returning breaks instead.
+		*breaks = read_line(parser, *breaks)
+		*end_mark = parser.mark
+	}
+
+	// Determine the indentation level if needed.
+	if *indent == 0 {
+		*indent = max_indent
+		if *indent < parser.indent+1 {
+			*indent = parser.indent + 1
+		}
+		if *indent < 1 {
+			*indent = 1
+		}
+	}
+	return true
+}
+
+// Scan a quoted scalar.
+func yaml_parser_scan_flow_scalar(parser *yaml_parser_t, token *yaml_token_t, single bool) bool {
+	// Eat the left quote.
+	start_mark := parser.mark
+	skip(parser)
+
+	// Consume the content of the quoted scalar.
+	var s, leading_break, trailing_breaks, whitespaces []byte
+	for {
+		// Check that there are no document indicators at the beginning of the line.
+		if parser.unread < 4 && !yaml_parser_update_buffer(parser, 4) {
+			return false
+		}
+
+		if parser.mark.column == 0 &&
+			((parser.buffer[parser.buffer_pos+0] == '-' &&
+				parser.buffer[parser.buffer_pos+1] == '-' &&
+				parser.buffer[parser.buffer_pos+2] == '-') ||
+				(parser.buffer[parser.buffer_pos+0] == '.' &&
+					parser.buffer[parser.buffer_pos+1] == '.' &&
+					parser.buffer[parser.buffer_pos+2] == '.')) &&
+			is_blankz(parser.buffer, parser.buffer_pos+3) {
+			yaml_parser_set_scanner_error(parser, "while scanning a quoted scalar",
+				start_mark, "found unexpected document indicator")
+			return false
+		}
+
+		// Check for EOF.
+		if is_z(parser.buffer, parser.buffer_pos) {
+			yaml_parser_set_scanner_error(parser, "while scanning a quoted scalar",
+				start_mark, "found unexpected end of stream")
+			return false
+		}
+
+		// Consume non-blank characters.
+		leading_blanks := false
+		for !is_blankz(parser.buffer, parser.buffer_pos) {
+			if single && parser.buffer[parser.buffer_pos] == '\'' && parser.buffer[parser.buffer_pos+1] == '\'' {
+				// Is is an escaped single quote.
+				s = append(s, '\'')
+				skip(parser)
+				skip(parser)
+
+			} else if single && parser.buffer[parser.buffer_pos] == '\'' {
+				// It is a right single quote.
+				break
+			} else if !single && parser.buffer[parser.buffer_pos] == '"' {
+				// It is a right double quote.
+				break
+
+			} else if !single && parser.buffer[parser.buffer_pos] == '\\' && is_break(parser.buffer, parser.buffer_pos+1) {
+				// It is an escaped line break.
+				if parser.unread < 3 && !yaml_parser_update_buffer(parser, 3) {
+					return false
+				}
+				skip(parser)
+				skip_line(parser)
+				leading_blanks = true
+				break
+
+			} else if !single && parser.buffer[parser.buffer_pos] == '\\' {
+				// It is an escape sequence.
+				code_length := 0
+
+				// Check the escape character.
+				switch parser.buffer[parser.buffer_pos+1] {
+				case '0':
+					s = append(s, 0)
+				case 'a':
+					s = append(s, '\x07')
+				case 'b':
+					s = append(s, '\x08')
+				case 't', '\t':
+					s = append(s, '\x09')
+				case 'n':
+					s = append(s, '\x0A')
+				case 'v':
+					s = append(s, '\x0B')
+				case 'f':
+					s = append(s, '\x0C')
+				case 'r':
+					s = append(s, '\x0D')
+				case 'e':
+					s = append(s, '\x1B')
+				case ' ':
+					s = append(s, '\x20')
+				case '"':
+					s = append(s, '"')
+				case '\'':
+					s = append(s, '\'')
+				case '\\':
+					s = append(s, '\\')
+				case 'N': // NEL (#x85)
+					s = append(s, '\xC2')
+					s = append(s, '\x85')
+				case '_': // #xA0
+					s = append(s, '\xC2')
+					s = append(s, '\xA0')
+				case 'L': // LS (#x2028)
+					s = append(s, '\xE2')
+					s = append(s, '\x80')
+					s = append(s, '\xA8')
+				case 'P': // PS (#x2029)
+					s = append(s, '\xE2')
+					s = append(s, '\x80')
+					s = append(s, '\xA9')
+				case 'x':
+					code_length = 2
+				case 'u':
+					code_length = 4
+				case 'U':
+					code_length = 8
+				default:
+					yaml_parser_set_scanner_error(parser, "while parsing a quoted scalar",
+						start_mark, "found unknown escape character")
+					return false
+				}
+
+				skip(parser)
+				skip(parser)
+
+				// Consume an arbitrary escape code.
+				if code_length > 0 {
+					var value int
+
+					// Scan the character value.
+					if parser.unread < code_length && !yaml_parser_update_buffer(parser, code_length) {
+						return false
+					}
+					for k := 0; k < code_length; k++ {
+						if !is_hex(parser.buffer, parser.buffer_pos+k) {
+							yaml_parser_set_scanner_error(parser, "while parsing a quoted scalar",
+								start_mark, "did not find expected hexdecimal number")
+							return false
+						}
+						value = (value << 4) + as_hex(parser.buffer, parser.buffer_pos+k)
+					}
+
+					// Check the value and write the character.
+					if (value >= 0xD800 && value <= 0xDFFF) || value > 0x10FFFF {
+						yaml_parser_set_scanner_error(parser, "while parsing a quoted scalar",
+							start_mark, "found invalid Unicode character escape code")
+						return false
+					}
+					if value <= 0x7F {
+						s = append(s, byte(value))
+					} else if value <= 0x7FF {
+						s = append(s, byte(0xC0+(value>>6)))
+						s = append(s, byte(0x80+(value&0x3F)))
+					} else if value <= 0xFFFF {
+						s = append(s, byte(0xE0+(value>>12)))
+						s = append(s, byte(0x80+((value>>6)&0x3F)))
+						s = append(s, byte(0x80+(value&0x3F)))
+					} else {
+						s = append(s, byte(0xF0+(value>>18)))
+						s = append(s, byte(0x80+((value>>12)&0x3F)))
+						s = append(s, byte(0x80+((value>>6)&0x3F)))
+						s = append(s, byte(0x80+(value&0x3F)))
+					}
+
+					// Advance the pointer.
+					for k := 0; k < code_length; k++ {
+						skip(parser)
+					}
+				}
+			} else {
+				// It is a non-escaped non-blank character.
+				s = read(parser, s)
+			}
+			if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) {
+				return false
+			}
+		}
+
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+
+		// Check if we are at the end of the scalar.
+		if single {
+			if parser.buffer[parser.buffer_pos] == '\'' {
+				break
+			}
+		} else {
+			if parser.buffer[parser.buffer_pos] == '"' {
+				break
+			}
+		}
+
+		// Consume blank characters.
+		for is_blank(parser.buffer, parser.buffer_pos) || is_break(parser.buffer, parser.buffer_pos) {
+			if is_blank(parser.buffer, parser.buffer_pos) {
+				// Consume a space or a tab character.
+				if !leading_blanks {
+					whitespaces = read(parser, whitespaces)
+				} else {
+					skip(parser)
+				}
+			} else {
+				if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) {
+					return false
+				}
+
+				// Check if it is a first line break.
+				if !leading_blanks {
+					whitespaces = whitespaces[:0]
+					leading_break = read_line(parser, leading_break)
+					leading_blanks = true
+				} else {
+					trailing_breaks = read_line(parser, trailing_breaks)
+				}
+			}
+			if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+				return false
+			}
+		}
+
+		// Join the whitespaces or fold line breaks.
+		if leading_blanks {
+			// Do we need to fold line breaks?
+			if len(leading_break) > 0 && leading_break[0] == '\n' {
+				if len(trailing_breaks) == 0 {
+					s = append(s, ' ')
+				} else {
+					s = append(s, trailing_breaks...)
+				}
+			} else {
+				s = append(s, leading_break...)
+				s = append(s, trailing_breaks...)
+			}
+			trailing_breaks = trailing_breaks[:0]
+			leading_break = leading_break[:0]
+		} else {
+			s = append(s, whitespaces...)
+			whitespaces = whitespaces[:0]
+		}
+	}
+
+	// Eat the right quote.
+	skip(parser)
+	end_mark := parser.mark
+
+	// Create a token.
+	*token = yaml_token_t{
+		typ:        yaml_SCALAR_TOKEN,
+		start_mark: start_mark,
+		end_mark:   end_mark,
+		value:      s,
+		style:      yaml_SINGLE_QUOTED_SCALAR_STYLE,
+	}
+	if !single {
+		token.style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
+	}
+	return true
+}
+
+// Scan a plain scalar.
+func yaml_parser_scan_plain_scalar(parser *yaml_parser_t, token *yaml_token_t) bool {
+
+	var s, leading_break, trailing_breaks, whitespaces []byte
+	var leading_blanks bool
+	var indent = parser.indent + 1
+
+	start_mark := parser.mark
+	end_mark := parser.mark
+
+	// Consume the content of the plain scalar.
+	for {
+		// Check for a document indicator.
+		if parser.unread < 4 && !yaml_parser_update_buffer(parser, 4) {
+			return false
+		}
+		if parser.mark.column == 0 &&
+			((parser.buffer[parser.buffer_pos+0] == '-' &&
+				parser.buffer[parser.buffer_pos+1] == '-' &&
+				parser.buffer[parser.buffer_pos+2] == '-') ||
+				(parser.buffer[parser.buffer_pos+0] == '.' &&
+					parser.buffer[parser.buffer_pos+1] == '.' &&
+					parser.buffer[parser.buffer_pos+2] == '.')) &&
+			is_blankz(parser.buffer, parser.buffer_pos+3) {
+			break
+		}
+
+		// Check for a comment.
+		if parser.buffer[parser.buffer_pos] == '#' {
+			break
+		}
+
+		// Consume non-blank characters.
+		for !is_blankz(parser.buffer, parser.buffer_pos) {
+
+			// Check for indicators that may end a plain scalar.
+			if (parser.buffer[parser.buffer_pos] == ':' && is_blankz(parser.buffer, parser.buffer_pos+1)) ||
+				(parser.flow_level > 0 &&
+					(parser.buffer[parser.buffer_pos] == ',' ||
+						parser.buffer[parser.buffer_pos] == '?' || parser.buffer[parser.buffer_pos] == '[' ||
+						parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '{' ||
+						parser.buffer[parser.buffer_pos] == '}')) {
+				break
+			}
+
+			// Check if we need to join whitespaces and breaks.
+			if leading_blanks || len(whitespaces) > 0 {
+				if leading_blanks {
+					// Do we need to fold line breaks?
+					if leading_break[0] == '\n' {
+						if len(trailing_breaks) == 0 {
+							s = append(s, ' ')
+						} else {
+							s = append(s, trailing_breaks...)
+						}
+					} else {
+						s = append(s, leading_break...)
+						s = append(s, trailing_breaks...)
+					}
+					trailing_breaks = trailing_breaks[:0]
+					leading_break = leading_break[:0]
+					leading_blanks = false
+				} else {
+					s = append(s, whitespaces...)
+					whitespaces = whitespaces[:0]
+				}
+			}
+
+			// Copy the character.
+			s = read(parser, s)
+
+			end_mark = parser.mark
+			if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) {
+				return false
+			}
+		}
+
+		// Is it the end?
+		if !(is_blank(parser.buffer, parser.buffer_pos) || is_break(parser.buffer, parser.buffer_pos)) {
+			break
+		}
+
+		// Consume blank characters.
+		if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+			return false
+		}
+
+		for is_blank(parser.buffer, parser.buffer_pos) || is_break(parser.buffer, parser.buffer_pos) {
+			if is_blank(parser.buffer, parser.buffer_pos) {
+
+				// Check for tab characters that abuse indentation.
+				if leading_blanks && parser.mark.column < indent && is_tab(parser.buffer, parser.buffer_pos) {
+					yaml_parser_set_scanner_error(parser, "while scanning a plain scalar",
+						start_mark, "found a tab character that violates indentation")
+					return false
+				}
+
+				// Consume a space or a tab character.
+				if !leading_blanks {
+					whitespaces = read(parser, whitespaces)
+				} else {
+					skip(parser)
+				}
+			} else {
+				if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) {
+					return false
+				}
+
+				// Check if it is a first line break.
+				if !leading_blanks {
+					whitespaces = whitespaces[:0]
+					leading_break = read_line(parser, leading_break)
+					leading_blanks = true
+				} else {
+					trailing_breaks = read_line(parser, trailing_breaks)
+				}
+			}
+			if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+				return false
+			}
+		}
+
+		// Check indentation level.
+		if parser.flow_level == 0 && parser.mark.column < indent {
+			break
+		}
+	}
+
+	// Create a token.
+	*token = yaml_token_t{
+		typ:        yaml_SCALAR_TOKEN,
+		start_mark: start_mark,
+		end_mark:   end_mark,
+		value:      s,
+		style:      yaml_PLAIN_SCALAR_STYLE,
+	}
+
+	// Note that we change the 'simple_key_allowed' flag.
+	if leading_blanks {
+		parser.simple_key_allowed = true
+	}
+	return true
+}
+
+func yaml_parser_scan_line_comment(parser *yaml_parser_t, token_mark yaml_mark_t) bool {
+	if parser.newlines > 0 {
+		return true
+	}
+
+	var start_mark yaml_mark_t
+	var text []byte
+
+	for peek := 0; peek < 512; peek++ {
+		if parser.unread < peek+1 && !yaml_parser_update_buffer(parser, peek+1) {
+			break
+		}
+		if is_blank(parser.buffer, parser.buffer_pos+peek) {
+			continue
+		}
+		if parser.buffer[parser.buffer_pos+peek] == '#' {
+			seen := parser.mark.index+peek
+			for {
+				if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+					return false
+				}
+				if is_breakz(parser.buffer, parser.buffer_pos) {
+					if parser.mark.index >= seen {
+						break
+					}
+					if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) {
+						return false
+					}
+					skip_line(parser)
+				} else if parser.mark.index >= seen {
+					if len(text) == 0 {
+						start_mark = parser.mark
+					}
+					text = read(parser, text)
+				} else {
+					skip(parser)
+				}
+			}
+		}
+		break
+	}
+	if len(text) > 0 {
+		parser.comments = append(parser.comments, yaml_comment_t{
+			token_mark: token_mark,
+			start_mark: start_mark,
+			line: text,
+		})
+	}
+	return true
+}
+
+func yaml_parser_scan_comments(parser *yaml_parser_t, scan_mark yaml_mark_t) bool {
+	token := parser.tokens[len(parser.tokens)-1]
+
+	if token.typ == yaml_FLOW_ENTRY_TOKEN && len(parser.tokens) > 1 {
+		token = parser.tokens[len(parser.tokens)-2]
+	}
+
+	var token_mark = token.start_mark
+	var start_mark yaml_mark_t
+	var next_indent = parser.indent
+	if next_indent < 0 {
+		next_indent = 0
+	}
+
+	var recent_empty = false
+	var first_empty = parser.newlines <= 1
+
+	var line = parser.mark.line
+	var column = parser.mark.column
+
+	var text []byte
+
+	// The foot line is the place where a comment must start to
+	// still be considered as a foot of the prior content.
+	// If there's some content in the currently parsed line, then
+	// the foot is the line below it.
+	var foot_line = -1
+	if scan_mark.line > 0 {
+		foot_line = parser.mark.line-parser.newlines+1
+		if parser.newlines == 0 && parser.mark.column > 1 {
+			foot_line++
+		}
+	}
+
+	var peek = 0
+	for ; peek < 512; peek++ {
+		if parser.unread < peek+1 && !yaml_parser_update_buffer(parser, peek+1) {
+			break
+		}
+		column++
+		if is_blank(parser.buffer, parser.buffer_pos+peek) {
+			continue
+		}
+		c := parser.buffer[parser.buffer_pos+peek]
+		var close_flow = parser.flow_level > 0 && (c == ']' || c == '}')
+		if close_flow || is_breakz(parser.buffer, parser.buffer_pos+peek) {
+			// Got line break or terminator.
+			if close_flow || !recent_empty {
+				if close_flow || first_empty && (start_mark.line == foot_line && token.typ != yaml_VALUE_TOKEN || start_mark.column-1 < next_indent) {
+					// This is the first empty line and there were no empty lines before,
+					// so this initial part of the comment is a foot of the prior token
+					// instead of being a head for the following one. Split it up.
+					// Alternatively, this might also be the last comment inside a flow
+					// scope, so it must be a footer.
+					if len(text) > 0 {
+						if start_mark.column-1 < next_indent {
+							// If dedented it's unrelated to the prior token.
+							token_mark = start_mark
+						}
+						parser.comments = append(parser.comments, yaml_comment_t{
+							scan_mark:  scan_mark,
+							token_mark: token_mark,
+							start_mark: start_mark,
+							end_mark:   yaml_mark_t{parser.mark.index + peek, line, column},
+							foot:       text,
+						})
+						scan_mark = yaml_mark_t{parser.mark.index + peek, line, column}
+						token_mark = scan_mark
+						text = nil
+					}
+				} else {
+					if len(text) > 0 && parser.buffer[parser.buffer_pos+peek] != 0 {
+						text = append(text, '\n')
+					}
+				}
+			}
+			if !is_break(parser.buffer, parser.buffer_pos+peek) {
+				break
+			}
+			first_empty = false
+			recent_empty = true
+			column = 0
+			line++
+			continue
+		}
+
+		if len(text) > 0 && (close_flow || column-1 < next_indent && column != start_mark.column) {
+			// The comment at the different indentation is a foot of the
+			// preceding data rather than a head of the upcoming one.
+			parser.comments = append(parser.comments, yaml_comment_t{
+				scan_mark:  scan_mark,
+				token_mark: token_mark,
+				start_mark: start_mark,
+				end_mark:   yaml_mark_t{parser.mark.index + peek, line, column},
+				foot:       text,
+			})
+			scan_mark = yaml_mark_t{parser.mark.index + peek, line, column}
+			token_mark = scan_mark
+			text = nil
+		}
+
+		if parser.buffer[parser.buffer_pos+peek] != '#' {
+			break
+		}
+
+		if len(text) == 0 {
+			start_mark = yaml_mark_t{parser.mark.index + peek, line, column}
+		} else {
+			text = append(text, '\n')
+		}
+
+		recent_empty = false
+
+		// Consume until after the consumed comment line.
+		seen := parser.mark.index+peek
+		for {
+			if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) {
+				return false
+			}
+			if is_breakz(parser.buffer, parser.buffer_pos) {
+				if parser.mark.index >= seen {
+					break
+				}
+				if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) {
+					return false
+				}
+				skip_line(parser)
+			} else if parser.mark.index >= seen {
+				text = read(parser, text)
+			} else {
+				skip(parser)
+			}
+		}
+
+		peek = 0
+		column = 0
+		line = parser.mark.line
+		next_indent = parser.indent
+		if next_indent < 0 {
+			next_indent = 0
+		}
+	}
+
+	if len(text) > 0 {
+		parser.comments = append(parser.comments, yaml_comment_t{
+			scan_mark:  scan_mark,
+			token_mark: start_mark,
+			start_mark: start_mark,
+			end_mark:   yaml_mark_t{parser.mark.index + peek - 1, line, column},
+			head:       text,
+		})
+	}
+	return true
+}
diff --git a/vendor/gopkg.in/yaml.v3/sorter.go b/vendor/gopkg.in/yaml.v3/sorter.go
new file mode 100644
index 000000000..9210ece7e
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/sorter.go
@@ -0,0 +1,134 @@
+//
+// Copyright (c) 2011-2019 Canonical Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package yaml
+
+import (
+	"reflect"
+	"unicode"
+)
+
+type keyList []reflect.Value
+
+func (l keyList) Len() int      { return len(l) }
+func (l keyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
+func (l keyList) Less(i, j int) bool {
+	a := l[i]
+	b := l[j]
+	ak := a.Kind()
+	bk := b.Kind()
+	for (ak == reflect.Interface || ak == reflect.Ptr) && !a.IsNil() {
+		a = a.Elem()
+		ak = a.Kind()
+	}
+	for (bk == reflect.Interface || bk == reflect.Ptr) && !b.IsNil() {
+		b = b.Elem()
+		bk = b.Kind()
+	}
+	af, aok := keyFloat(a)
+	bf, bok := keyFloat(b)
+	if aok && bok {
+		if af != bf {
+			return af < bf
+		}
+		if ak != bk {
+			return ak < bk
+		}
+		return numLess(a, b)
+	}
+	if ak != reflect.String || bk != reflect.String {
+		return ak < bk
+	}
+	ar, br := []rune(a.String()), []rune(b.String())
+	digits := false
+	for i := 0; i < len(ar) && i < len(br); i++ {
+		if ar[i] == br[i] {
+			digits = unicode.IsDigit(ar[i])
+			continue
+		}
+		al := unicode.IsLetter(ar[i])
+		bl := unicode.IsLetter(br[i])
+		if al && bl {
+			return ar[i] < br[i]
+		}
+		if al || bl {
+			if digits {
+				return al
+			} else {
+				return bl
+			}
+		}
+		var ai, bi int
+		var an, bn int64
+		if ar[i] == '0' || br[i] == '0' {
+			for j := i - 1; j >= 0 && unicode.IsDigit(ar[j]); j-- {
+				if ar[j] != '0' {
+					an = 1
+					bn = 1
+					break
+				}
+			}
+		}
+		for ai = i; ai < len(ar) && unicode.IsDigit(ar[ai]); ai++ {
+			an = an*10 + int64(ar[ai]-'0')
+		}
+		for bi = i; bi < len(br) && unicode.IsDigit(br[bi]); bi++ {
+			bn = bn*10 + int64(br[bi]-'0')
+		}
+		if an != bn {
+			return an < bn
+		}
+		if ai != bi {
+			return ai < bi
+		}
+		return ar[i] < br[i]
+	}
+	return len(ar) < len(br)
+}
+
+// keyFloat returns a float value for v if it is a number/bool
+// and whether it is a number/bool or not.
+func keyFloat(v reflect.Value) (f float64, ok bool) {
+	switch v.Kind() {
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return float64(v.Int()), true
+	case reflect.Float32, reflect.Float64:
+		return v.Float(), true
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return float64(v.Uint()), true
+	case reflect.Bool:
+		if v.Bool() {
+			return 1, true
+		}
+		return 0, true
+	}
+	return 0, false
+}
+
+// numLess returns whether a < b.
+// a and b must necessarily have the same kind.
+func numLess(a, b reflect.Value) bool {
+	switch a.Kind() {
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return a.Int() < b.Int()
+	case reflect.Float32, reflect.Float64:
+		return a.Float() < b.Float()
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return a.Uint() < b.Uint()
+	case reflect.Bool:
+		return !a.Bool() && b.Bool()
+	}
+	panic("not a number")
+}
diff --git a/vendor/gopkg.in/yaml.v3/writerc.go b/vendor/gopkg.in/yaml.v3/writerc.go
new file mode 100644
index 000000000..b8a116bf9
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/writerc.go
@@ -0,0 +1,48 @@
+// 
+// Copyright (c) 2011-2019 Canonical Ltd
+// Copyright (c) 2006-2010 Kirill Simonov
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package yaml
+
+// Set the writer error and return false.
+func yaml_emitter_set_writer_error(emitter *yaml_emitter_t, problem string) bool {
+	emitter.error = yaml_WRITER_ERROR
+	emitter.problem = problem
+	return false
+}
+
+// Flush the output buffer.
+func yaml_emitter_flush(emitter *yaml_emitter_t) bool {
+	if emitter.write_handler == nil {
+		panic("write handler not set")
+	}
+
+	// Check if the buffer is empty.
+	if emitter.buffer_pos == 0 {
+		return true
+	}
+
+	if err := emitter.write_handler(emitter, emitter.buffer[:emitter.buffer_pos]); err != nil {
+		return yaml_emitter_set_writer_error(emitter, "write error: "+err.Error())
+	}
+	emitter.buffer_pos = 0
+	return true
+}
diff --git a/vendor/gopkg.in/yaml.v3/yaml.go b/vendor/gopkg.in/yaml.v3/yaml.go
new file mode 100644
index 000000000..8cec6da48
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/yaml.go
@@ -0,0 +1,698 @@
+//
+// Copyright (c) 2011-2019 Canonical Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package yaml implements YAML support for the Go language.
+//
+// Source code and other details for the project are available at GitHub:
+//
+//   https://github.com/go-yaml/yaml
+//
+package yaml
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"reflect"
+	"strings"
+	"sync"
+	"unicode/utf8"
+)
+
+// The Unmarshaler interface may be implemented by types to customize their
+// behavior when being unmarshaled from a YAML document.
+type Unmarshaler interface {
+	UnmarshalYAML(value *Node) error
+}
+
+type obsoleteUnmarshaler interface {
+	UnmarshalYAML(unmarshal func(interface{}) error) error
+}
+
+// The Marshaler interface may be implemented by types to customize their
+// behavior when being marshaled into a YAML document. The returned value
+// is marshaled in place of the original value implementing Marshaler.
+//
+// If an error is returned by MarshalYAML, the marshaling procedure stops
+// and returns with the provided error.
+type Marshaler interface {
+	MarshalYAML() (interface{}, error)
+}
+
+// Unmarshal decodes the first document found within the in byte slice
+// and assigns decoded values into the out value.
+//
+// Maps and pointers (to a struct, string, int, etc) are accepted as out
+// values. If an internal pointer within a struct is not initialized,
+// the yaml package will initialize it if necessary for unmarshalling
+// the provided data. The out parameter must not be nil.
+//
+// The type of the decoded values should be compatible with the respective
+// values in out. If one or more values cannot be decoded due to a type
+// mismatches, decoding continues partially until the end of the YAML
+// content, and a *yaml.TypeError is returned with details for all
+// missed values.
+//
+// Struct fields are only unmarshalled if they are exported (have an
+// upper case first letter), and are unmarshalled using the field name
+// lowercased as the default key. Custom keys may be defined via the
+// "yaml" name in the field tag: the content preceding the first comma
+// is used as the key, and the following comma-separated options are
+// used to tweak the marshalling process (see Marshal).
+// Conflicting names result in a runtime error.
+//
+// For example:
+//
+//     type T struct {
+//         F int `yaml:"a,omitempty"`
+//         B int
+//     }
+//     var t T
+//     yaml.Unmarshal([]byte("a: 1\nb: 2"), &t)
+//
+// See the documentation of Marshal for the format of tags and a list of
+// supported tag options.
+//
+func Unmarshal(in []byte, out interface{}) (err error) {
+	return unmarshal(in, out, false)
+}
+
+// A Decoder reads and decodes YAML values from an input stream.
+type Decoder struct {
+	parser      *parser
+	knownFields bool
+}
+
+// NewDecoder returns a new decoder that reads from r.
+//
+// The decoder introduces its own buffering and may read
+// data from r beyond the YAML values requested.
+func NewDecoder(r io.Reader) *Decoder {
+	return &Decoder{
+		parser: newParserFromReader(r),
+	}
+}
+
+// KnownFields ensures that the keys in decoded mappings to
+// exist as fields in the struct being decoded into.
+func (dec *Decoder) KnownFields(enable bool) {
+	dec.knownFields = enable
+}
+
+// Decode reads the next YAML-encoded value from its input
+// and stores it in the value pointed to by v.
+//
+// See the documentation for Unmarshal for details about the
+// conversion of YAML into a Go value.
+func (dec *Decoder) Decode(v interface{}) (err error) {
+	d := newDecoder()
+	d.knownFields = dec.knownFields
+	defer handleErr(&err)
+	node := dec.parser.parse()
+	if node == nil {
+		return io.EOF
+	}
+	out := reflect.ValueOf(v)
+	if out.Kind() == reflect.Ptr && !out.IsNil() {
+		out = out.Elem()
+	}
+	d.unmarshal(node, out)
+	if len(d.terrors) > 0 {
+		return &TypeError{d.terrors}
+	}
+	return nil
+}
+
+// Decode decodes the node and stores its data into the value pointed to by v.
+//
+// See the documentation for Unmarshal for details about the
+// conversion of YAML into a Go value.
+func (n *Node) Decode(v interface{}) (err error) {
+	d := newDecoder()
+	defer handleErr(&err)
+	out := reflect.ValueOf(v)
+	if out.Kind() == reflect.Ptr && !out.IsNil() {
+		out = out.Elem()
+	}
+	d.unmarshal(n, out)
+	if len(d.terrors) > 0 {
+		return &TypeError{d.terrors}
+	}
+	return nil
+}
+
+func unmarshal(in []byte, out interface{}, strict bool) (err error) {
+	defer handleErr(&err)
+	d := newDecoder()
+	p := newParser(in)
+	defer p.destroy()
+	node := p.parse()
+	if node != nil {
+		v := reflect.ValueOf(out)
+		if v.Kind() == reflect.Ptr && !v.IsNil() {
+			v = v.Elem()
+		}
+		d.unmarshal(node, v)
+	}
+	if len(d.terrors) > 0 {
+		return &TypeError{d.terrors}
+	}
+	return nil
+}
+
+// Marshal serializes the value provided into a YAML document. The structure
+// of the generated document will reflect the structure of the value itself.
+// Maps and pointers (to struct, string, int, etc) are accepted as the in value.
+//
+// Struct fields are only marshalled if they are exported (have an upper case
+// first letter), and are marshalled using the field name lowercased as the
+// default key. Custom keys may be defined via the "yaml" name in the field
+// tag: the content preceding the first comma is used as the key, and the
+// following comma-separated options are used to tweak the marshalling process.
+// Conflicting names result in a runtime error.
+//
+// The field tag format accepted is:
+//
+//     `(...) yaml:"[<key>][,<flag1>[,<flag2>]]" (...)`
+//
+// The following flags are currently supported:
+//
+//     omitempty    Only include the field if it's not set to the zero
+//                  value for the type or to empty slices or maps.
+//                  Zero valued structs will be omitted if all their public
+//                  fields are zero, unless they implement an IsZero
+//                  method (see the IsZeroer interface type), in which
+//                  case the field will be excluded if IsZero returns true.
+//
+//     flow         Marshal using a flow style (useful for structs,
+//                  sequences and maps).
+//
+//     inline       Inline the field, which must be a struct or a map,
+//                  causing all of its fields or keys to be processed as if
+//                  they were part of the outer struct. For maps, keys must
+//                  not conflict with the yaml keys of other struct fields.
+//
+// In addition, if the key is "-", the field is ignored.
+//
+// For example:
+//
+//     type T struct {
+//         F int `yaml:"a,omitempty"`
+//         B int
+//     }
+//     yaml.Marshal(&T{B: 2}) // Returns "b: 2\n"
+//     yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n"
+//
+func Marshal(in interface{}) (out []byte, err error) {
+	defer handleErr(&err)
+	e := newEncoder()
+	defer e.destroy()
+	e.marshalDoc("", reflect.ValueOf(in))
+	e.finish()
+	out = e.out
+	return
+}
+
+// An Encoder writes YAML values to an output stream.
+type Encoder struct {
+	encoder *encoder
+}
+
+// NewEncoder returns a new encoder that writes to w.
+// The Encoder should be closed after use to flush all data
+// to w.
+func NewEncoder(w io.Writer) *Encoder {
+	return &Encoder{
+		encoder: newEncoderWithWriter(w),
+	}
+}
+
+// Encode writes the YAML encoding of v to the stream.
+// If multiple items are encoded to the stream, the
+// second and subsequent document will be preceded
+// with a "---" document separator, but the first will not.
+//
+// See the documentation for Marshal for details about the conversion of Go
+// values to YAML.
+func (e *Encoder) Encode(v interface{}) (err error) {
+	defer handleErr(&err)
+	e.encoder.marshalDoc("", reflect.ValueOf(v))
+	return nil
+}
+
+// Encode encodes value v and stores its representation in n.
+//
+// See the documentation for Marshal for details about the
+// conversion of Go values into YAML.
+func (n *Node) Encode(v interface{}) (err error) {
+	defer handleErr(&err)
+	e := newEncoder()
+	defer e.destroy()
+	e.marshalDoc("", reflect.ValueOf(v))
+	e.finish()
+	p := newParser(e.out)
+	p.textless = true
+	defer p.destroy()
+	doc := p.parse()
+	*n = *doc.Content[0]
+	return nil
+}
+
+// SetIndent changes the used indentation used when encoding.
+func (e *Encoder) SetIndent(spaces int) {
+	if spaces < 0 {
+		panic("yaml: cannot indent to a negative number of spaces")
+	}
+	e.encoder.indent = spaces
+}
+
+// Close closes the encoder by writing any remaining data.
+// It does not write a stream terminating string "...".
+func (e *Encoder) Close() (err error) {
+	defer handleErr(&err)
+	e.encoder.finish()
+	return nil
+}
+
+func handleErr(err *error) {
+	if v := recover(); v != nil {
+		if e, ok := v.(yamlError); ok {
+			*err = e.err
+		} else {
+			panic(v)
+		}
+	}
+}
+
+type yamlError struct {
+	err error
+}
+
+func fail(err error) {
+	panic(yamlError{err})
+}
+
+func failf(format string, args ...interface{}) {
+	panic(yamlError{fmt.Errorf("yaml: "+format, args...)})
+}
+
+// A TypeError is returned by Unmarshal when one or more fields in
+// the YAML document cannot be properly decoded into the requested
+// types. When this error is returned, the value is still
+// unmarshaled partially.
+type TypeError struct {
+	Errors []string
+}
+
+func (e *TypeError) Error() string {
+	return fmt.Sprintf("yaml: unmarshal errors:\n  %s", strings.Join(e.Errors, "\n  "))
+}
+
+type Kind uint32
+
+const (
+	DocumentNode Kind = 1 << iota
+	SequenceNode
+	MappingNode
+	ScalarNode
+	AliasNode
+)
+
+type Style uint32
+
+const (
+	TaggedStyle Style = 1 << iota
+	DoubleQuotedStyle
+	SingleQuotedStyle
+	LiteralStyle
+	FoldedStyle
+	FlowStyle
+)
+
+// Node represents an element in the YAML document hierarchy. While documents
+// are typically encoded and decoded into higher level types, such as structs
+// and maps, Node is an intermediate representation that allows detailed
+// control over the content being decoded or encoded.
+//
+// It's worth noting that although Node offers access into details such as
+// line numbers, colums, and comments, the content when re-encoded will not
+// have its original textual representation preserved. An effort is made to
+// render the data plesantly, and to preserve comments near the data they
+// describe, though.
+//
+// Values that make use of the Node type interact with the yaml package in the
+// same way any other type would do, by encoding and decoding yaml data
+// directly or indirectly into them.
+//
+// For example:
+//
+//     var person struct {
+//             Name    string
+//             Address yaml.Node
+//     }
+//     err := yaml.Unmarshal(data, &person)
+// 
+// Or by itself:
+//
+//     var person Node
+//     err := yaml.Unmarshal(data, &person)
+//
+type Node struct {
+	// Kind defines whether the node is a document, a mapping, a sequence,
+	// a scalar value, or an alias to another node. The specific data type of
+	// scalar nodes may be obtained via the ShortTag and LongTag methods.
+	Kind  Kind
+
+	// Style allows customizing the apperance of the node in the tree.
+	Style Style
+
+	// Tag holds the YAML tag defining the data type for the value.
+	// When decoding, this field will always be set to the resolved tag,
+	// even when it wasn't explicitly provided in the YAML content.
+	// When encoding, if this field is unset the value type will be
+	// implied from the node properties, and if it is set, it will only
+	// be serialized into the representation if TaggedStyle is used or
+	// the implicit tag diverges from the provided one.
+	Tag string
+
+	// Value holds the unescaped and unquoted represenation of the value.
+	Value string
+
+	// Anchor holds the anchor name for this node, which allows aliases to point to it.
+	Anchor string
+
+	// Alias holds the node that this alias points to. Only valid when Kind is AliasNode.
+	Alias *Node
+
+	// Content holds contained nodes for documents, mappings, and sequences.
+	Content []*Node
+
+	// HeadComment holds any comments in the lines preceding the node and
+	// not separated by an empty line.
+	HeadComment string
+
+	// LineComment holds any comments at the end of the line where the node is in.
+	LineComment string
+
+	// FootComment holds any comments following the node and before empty lines.
+	FootComment string
+
+	// Line and Column hold the node position in the decoded YAML text.
+	// These fields are not respected when encoding the node.
+	Line   int
+	Column int
+}
+
+// IsZero returns whether the node has all of its fields unset.
+func (n *Node) IsZero() bool {
+	return n.Kind == 0 && n.Style == 0 && n.Tag == "" && n.Value == "" && n.Anchor == "" && n.Alias == nil && n.Content == nil &&
+		n.HeadComment == "" && n.LineComment == "" && n.FootComment == "" && n.Line == 0 && n.Column == 0
+}
+
+
+// LongTag returns the long form of the tag that indicates the data type for
+// the node. If the Tag field isn't explicitly defined, one will be computed
+// based on the node properties.
+func (n *Node) LongTag() string {
+	return longTag(n.ShortTag())
+}
+
+// ShortTag returns the short form of the YAML tag that indicates data type for
+// the node. If the Tag field isn't explicitly defined, one will be computed
+// based on the node properties.
+func (n *Node) ShortTag() string {
+	if n.indicatedString() {
+		return strTag
+	}
+	if n.Tag == "" || n.Tag == "!" {
+		switch n.Kind {
+		case MappingNode:
+			return mapTag
+		case SequenceNode:
+			return seqTag
+		case AliasNode:
+			if n.Alias != nil {
+				return n.Alias.ShortTag()
+			}
+		case ScalarNode:
+			tag, _ := resolve("", n.Value)
+			return tag
+		case 0:
+			// Special case to make the zero value convenient.
+			if n.IsZero() {
+				return nullTag
+			}
+		}
+		return ""
+	}
+	return shortTag(n.Tag)
+}
+
+func (n *Node) indicatedString() bool {
+	return n.Kind == ScalarNode &&
+		(shortTag(n.Tag) == strTag ||
+			(n.Tag == "" || n.Tag == "!") && n.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0)
+}
+
+// SetString is a convenience function that sets the node to a string value
+// and defines its style in a pleasant way depending on its content.
+func (n *Node) SetString(s string) {
+	n.Kind = ScalarNode
+	if utf8.ValidString(s) {
+		n.Value = s
+		n.Tag = strTag
+	} else {
+		n.Value = encodeBase64(s)
+		n.Tag = binaryTag
+	}
+	if strings.Contains(n.Value, "\n") {
+		n.Style = LiteralStyle
+	}
+}
+
+// --------------------------------------------------------------------------
+// Maintain a mapping of keys to structure field indexes
+
+// The code in this section was copied from mgo/bson.
+
+// structInfo holds details for the serialization of fields of
+// a given struct.
+type structInfo struct {
+	FieldsMap  map[string]fieldInfo
+	FieldsList []fieldInfo
+
+	// InlineMap is the number of the field in the struct that
+	// contains an ,inline map, or -1 if there's none.
+	InlineMap int
+
+	// InlineUnmarshalers holds indexes to inlined fields that
+	// contain unmarshaler values.
+	InlineUnmarshalers [][]int
+}
+
+type fieldInfo struct {
+	Key       string
+	Num       int
+	OmitEmpty bool
+	Flow      bool
+	// Id holds the unique field identifier, so we can cheaply
+	// check for field duplicates without maintaining an extra map.
+	Id int
+
+	// Inline holds the field index if the field is part of an inlined struct.
+	Inline []int
+}
+
+var structMap = make(map[reflect.Type]*structInfo)
+var fieldMapMutex sync.RWMutex
+var unmarshalerType reflect.Type
+
+func init() {
+	var v Unmarshaler
+	unmarshalerType = reflect.ValueOf(&v).Elem().Type()
+}
+
+func getStructInfo(st reflect.Type) (*structInfo, error) {
+	fieldMapMutex.RLock()
+	sinfo, found := structMap[st]
+	fieldMapMutex.RUnlock()
+	if found {
+		return sinfo, nil
+	}
+
+	n := st.NumField()
+	fieldsMap := make(map[string]fieldInfo)
+	fieldsList := make([]fieldInfo, 0, n)
+	inlineMap := -1
+	inlineUnmarshalers := [][]int(nil)
+	for i := 0; i != n; i++ {
+		field := st.Field(i)
+		if field.PkgPath != "" && !field.Anonymous {
+			continue // Private field
+		}
+
+		info := fieldInfo{Num: i}
+
+		tag := field.Tag.Get("yaml")
+		if tag == "" && strings.Index(string(field.Tag), ":") < 0 {
+			tag = string(field.Tag)
+		}
+		if tag == "-" {
+			continue
+		}
+
+		inline := false
+		fields := strings.Split(tag, ",")
+		if len(fields) > 1 {
+			for _, flag := range fields[1:] {
+				switch flag {
+				case "omitempty":
+					info.OmitEmpty = true
+				case "flow":
+					info.Flow = true
+				case "inline":
+					inline = true
+				default:
+					return nil, errors.New(fmt.Sprintf("unsupported flag %q in tag %q of type %s", flag, tag, st))
+				}
+			}
+			tag = fields[0]
+		}
+
+		if inline {
+			switch field.Type.Kind() {
+			case reflect.Map:
+				if inlineMap >= 0 {
+					return nil, errors.New("multiple ,inline maps in struct " + st.String())
+				}
+				if field.Type.Key() != reflect.TypeOf("") {
+					return nil, errors.New("option ,inline needs a map with string keys in struct " + st.String())
+				}
+				inlineMap = info.Num
+			case reflect.Struct, reflect.Ptr:
+				ftype := field.Type
+				for ftype.Kind() == reflect.Ptr {
+					ftype = ftype.Elem()
+				}
+				if ftype.Kind() != reflect.Struct {
+					return nil, errors.New("option ,inline may only be used on a struct or map field")
+				}
+				if reflect.PtrTo(ftype).Implements(unmarshalerType) {
+					inlineUnmarshalers = append(inlineUnmarshalers, []int{i})
+				} else {
+					sinfo, err := getStructInfo(ftype)
+					if err != nil {
+						return nil, err
+					}
+					for _, index := range sinfo.InlineUnmarshalers {
+						inlineUnmarshalers = append(inlineUnmarshalers, append([]int{i}, index...))
+					}
+					for _, finfo := range sinfo.FieldsList {
+						if _, found := fieldsMap[finfo.Key]; found {
+							msg := "duplicated key '" + finfo.Key + "' in struct " + st.String()
+							return nil, errors.New(msg)
+						}
+						if finfo.Inline == nil {
+							finfo.Inline = []int{i, finfo.Num}
+						} else {
+							finfo.Inline = append([]int{i}, finfo.Inline...)
+						}
+						finfo.Id = len(fieldsList)
+						fieldsMap[finfo.Key] = finfo
+						fieldsList = append(fieldsList, finfo)
+					}
+				}
+			default:
+				return nil, errors.New("option ,inline may only be used on a struct or map field")
+			}
+			continue
+		}
+
+		if tag != "" {
+			info.Key = tag
+		} else {
+			info.Key = strings.ToLower(field.Name)
+		}
+
+		if _, found = fieldsMap[info.Key]; found {
+			msg := "duplicated key '" + info.Key + "' in struct " + st.String()
+			return nil, errors.New(msg)
+		}
+
+		info.Id = len(fieldsList)
+		fieldsList = append(fieldsList, info)
+		fieldsMap[info.Key] = info
+	}
+
+	sinfo = &structInfo{
+		FieldsMap:          fieldsMap,
+		FieldsList:         fieldsList,
+		InlineMap:          inlineMap,
+		InlineUnmarshalers: inlineUnmarshalers,
+	}
+
+	fieldMapMutex.Lock()
+	structMap[st] = sinfo
+	fieldMapMutex.Unlock()
+	return sinfo, nil
+}
+
+// IsZeroer is used to check whether an object is zero to
+// determine whether it should be omitted when marshaling
+// with the omitempty flag. One notable implementation
+// is time.Time.
+type IsZeroer interface {
+	IsZero() bool
+}
+
+func isZero(v reflect.Value) bool {
+	kind := v.Kind()
+	if z, ok := v.Interface().(IsZeroer); ok {
+		if (kind == reflect.Ptr || kind == reflect.Interface) && v.IsNil() {
+			return true
+		}
+		return z.IsZero()
+	}
+	switch kind {
+	case reflect.String:
+		return len(v.String()) == 0
+	case reflect.Interface, reflect.Ptr:
+		return v.IsNil()
+	case reflect.Slice:
+		return v.Len() == 0
+	case reflect.Map:
+		return v.Len() == 0
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return v.Int() == 0
+	case reflect.Float32, reflect.Float64:
+		return v.Float() == 0
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return v.Uint() == 0
+	case reflect.Bool:
+		return !v.Bool()
+	case reflect.Struct:
+		vt := v.Type()
+		for i := v.NumField() - 1; i >= 0; i-- {
+			if vt.Field(i).PkgPath != "" {
+				continue // Private field
+			}
+			if !isZero(v.Field(i)) {
+				return false
+			}
+		}
+		return true
+	}
+	return false
+}
diff --git a/vendor/gopkg.in/yaml.v3/yamlh.go b/vendor/gopkg.in/yaml.v3/yamlh.go
new file mode 100644
index 000000000..7c6d00770
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/yamlh.go
@@ -0,0 +1,807 @@
+//
+// Copyright (c) 2011-2019 Canonical Ltd
+// Copyright (c) 2006-2010 Kirill Simonov
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package yaml
+
+import (
+	"fmt"
+	"io"
+)
+
+// The version directive data.
+type yaml_version_directive_t struct {
+	major int8 // The major version number.
+	minor int8 // The minor version number.
+}
+
+// The tag directive data.
+type yaml_tag_directive_t struct {
+	handle []byte // The tag handle.
+	prefix []byte // The tag prefix.
+}
+
+type yaml_encoding_t int
+
+// The stream encoding.
+const (
+	// Let the parser choose the encoding.
+	yaml_ANY_ENCODING yaml_encoding_t = iota
+
+	yaml_UTF8_ENCODING    // The default UTF-8 encoding.
+	yaml_UTF16LE_ENCODING // The UTF-16-LE encoding with BOM.
+	yaml_UTF16BE_ENCODING // The UTF-16-BE encoding with BOM.
+)
+
+type yaml_break_t int
+
+// Line break types.
+const (
+	// Let the parser choose the break type.
+	yaml_ANY_BREAK yaml_break_t = iota
+
+	yaml_CR_BREAK   // Use CR for line breaks (Mac style).
+	yaml_LN_BREAK   // Use LN for line breaks (Unix style).
+	yaml_CRLN_BREAK // Use CR LN for line breaks (DOS style).
+)
+
+type yaml_error_type_t int
+
+// Many bad things could happen with the parser and emitter.
+const (
+	// No error is produced.
+	yaml_NO_ERROR yaml_error_type_t = iota
+
+	yaml_MEMORY_ERROR   // Cannot allocate or reallocate a block of memory.
+	yaml_READER_ERROR   // Cannot read or decode the input stream.
+	yaml_SCANNER_ERROR  // Cannot scan the input stream.
+	yaml_PARSER_ERROR   // Cannot parse the input stream.
+	yaml_COMPOSER_ERROR // Cannot compose a YAML document.
+	yaml_WRITER_ERROR   // Cannot write to the output stream.
+	yaml_EMITTER_ERROR  // Cannot emit a YAML stream.
+)
+
+// The pointer position.
+type yaml_mark_t struct {
+	index  int // The position index.
+	line   int // The position line.
+	column int // The position column.
+}
+
+// Node Styles
+
+type yaml_style_t int8
+
+type yaml_scalar_style_t yaml_style_t
+
+// Scalar styles.
+const (
+	// Let the emitter choose the style.
+	yaml_ANY_SCALAR_STYLE yaml_scalar_style_t = 0
+
+	yaml_PLAIN_SCALAR_STYLE         yaml_scalar_style_t = 1 << iota // The plain scalar style.
+	yaml_SINGLE_QUOTED_SCALAR_STYLE                                 // The single-quoted scalar style.
+	yaml_DOUBLE_QUOTED_SCALAR_STYLE                                 // The double-quoted scalar style.
+	yaml_LITERAL_SCALAR_STYLE                                       // The literal scalar style.
+	yaml_FOLDED_SCALAR_STYLE                                        // The folded scalar style.
+)
+
+type yaml_sequence_style_t yaml_style_t
+
+// Sequence styles.
+const (
+	// Let the emitter choose the style.
+	yaml_ANY_SEQUENCE_STYLE yaml_sequence_style_t = iota
+
+	yaml_BLOCK_SEQUENCE_STYLE // The block sequence style.
+	yaml_FLOW_SEQUENCE_STYLE  // The flow sequence style.
+)
+
+type yaml_mapping_style_t yaml_style_t
+
+// Mapping styles.
+const (
+	// Let the emitter choose the style.
+	yaml_ANY_MAPPING_STYLE yaml_mapping_style_t = iota
+
+	yaml_BLOCK_MAPPING_STYLE // The block mapping style.
+	yaml_FLOW_MAPPING_STYLE  // The flow mapping style.
+)
+
+// Tokens
+
+type yaml_token_type_t int
+
+// Token types.
+const (
+	// An empty token.
+	yaml_NO_TOKEN yaml_token_type_t = iota
+
+	yaml_STREAM_START_TOKEN // A STREAM-START token.
+	yaml_STREAM_END_TOKEN   // A STREAM-END token.
+
+	yaml_VERSION_DIRECTIVE_TOKEN // A VERSION-DIRECTIVE token.
+	yaml_TAG_DIRECTIVE_TOKEN     // A TAG-DIRECTIVE token.
+	yaml_DOCUMENT_START_TOKEN    // A DOCUMENT-START token.
+	yaml_DOCUMENT_END_TOKEN      // A DOCUMENT-END token.
+
+	yaml_BLOCK_SEQUENCE_START_TOKEN // A BLOCK-SEQUENCE-START token.
+	yaml_BLOCK_MAPPING_START_TOKEN  // A BLOCK-SEQUENCE-END token.
+	yaml_BLOCK_END_TOKEN            // A BLOCK-END token.
+
+	yaml_FLOW_SEQUENCE_START_TOKEN // A FLOW-SEQUENCE-START token.
+	yaml_FLOW_SEQUENCE_END_TOKEN   // A FLOW-SEQUENCE-END token.
+	yaml_FLOW_MAPPING_START_TOKEN  // A FLOW-MAPPING-START token.
+	yaml_FLOW_MAPPING_END_TOKEN    // A FLOW-MAPPING-END token.
+
+	yaml_BLOCK_ENTRY_TOKEN // A BLOCK-ENTRY token.
+	yaml_FLOW_ENTRY_TOKEN  // A FLOW-ENTRY token.
+	yaml_KEY_TOKEN         // A KEY token.
+	yaml_VALUE_TOKEN       // A VALUE token.
+
+	yaml_ALIAS_TOKEN  // An ALIAS token.
+	yaml_ANCHOR_TOKEN // An ANCHOR token.
+	yaml_TAG_TOKEN    // A TAG token.
+	yaml_SCALAR_TOKEN // A SCALAR token.
+)
+
+func (tt yaml_token_type_t) String() string {
+	switch tt {
+	case yaml_NO_TOKEN:
+		return "yaml_NO_TOKEN"
+	case yaml_STREAM_START_TOKEN:
+		return "yaml_STREAM_START_TOKEN"
+	case yaml_STREAM_END_TOKEN:
+		return "yaml_STREAM_END_TOKEN"
+	case yaml_VERSION_DIRECTIVE_TOKEN:
+		return "yaml_VERSION_DIRECTIVE_TOKEN"
+	case yaml_TAG_DIRECTIVE_TOKEN:
+		return "yaml_TAG_DIRECTIVE_TOKEN"
+	case yaml_DOCUMENT_START_TOKEN:
+		return "yaml_DOCUMENT_START_TOKEN"
+	case yaml_DOCUMENT_END_TOKEN:
+		return "yaml_DOCUMENT_END_TOKEN"
+	case yaml_BLOCK_SEQUENCE_START_TOKEN:
+		return "yaml_BLOCK_SEQUENCE_START_TOKEN"
+	case yaml_BLOCK_MAPPING_START_TOKEN:
+		return "yaml_BLOCK_MAPPING_START_TOKEN"
+	case yaml_BLOCK_END_TOKEN:
+		return "yaml_BLOCK_END_TOKEN"
+	case yaml_FLOW_SEQUENCE_START_TOKEN:
+		return "yaml_FLOW_SEQUENCE_START_TOKEN"
+	case yaml_FLOW_SEQUENCE_END_TOKEN:
+		return "yaml_FLOW_SEQUENCE_END_TOKEN"
+	case yaml_FLOW_MAPPING_START_TOKEN:
+		return "yaml_FLOW_MAPPING_START_TOKEN"
+	case yaml_FLOW_MAPPING_END_TOKEN:
+		return "yaml_FLOW_MAPPING_END_TOKEN"
+	case yaml_BLOCK_ENTRY_TOKEN:
+		return "yaml_BLOCK_ENTRY_TOKEN"
+	case yaml_FLOW_ENTRY_TOKEN:
+		return "yaml_FLOW_ENTRY_TOKEN"
+	case yaml_KEY_TOKEN:
+		return "yaml_KEY_TOKEN"
+	case yaml_VALUE_TOKEN:
+		return "yaml_VALUE_TOKEN"
+	case yaml_ALIAS_TOKEN:
+		return "yaml_ALIAS_TOKEN"
+	case yaml_ANCHOR_TOKEN:
+		return "yaml_ANCHOR_TOKEN"
+	case yaml_TAG_TOKEN:
+		return "yaml_TAG_TOKEN"
+	case yaml_SCALAR_TOKEN:
+		return "yaml_SCALAR_TOKEN"
+	}
+	return "<unknown token>"
+}
+
+// The token structure.
+type yaml_token_t struct {
+	// The token type.
+	typ yaml_token_type_t
+
+	// The start/end of the token.
+	start_mark, end_mark yaml_mark_t
+
+	// The stream encoding (for yaml_STREAM_START_TOKEN).
+	encoding yaml_encoding_t
+
+	// The alias/anchor/scalar value or tag/tag directive handle
+	// (for yaml_ALIAS_TOKEN, yaml_ANCHOR_TOKEN, yaml_SCALAR_TOKEN, yaml_TAG_TOKEN, yaml_TAG_DIRECTIVE_TOKEN).
+	value []byte
+
+	// The tag suffix (for yaml_TAG_TOKEN).
+	suffix []byte
+
+	// The tag directive prefix (for yaml_TAG_DIRECTIVE_TOKEN).
+	prefix []byte
+
+	// The scalar style (for yaml_SCALAR_TOKEN).
+	style yaml_scalar_style_t
+
+	// The version directive major/minor (for yaml_VERSION_DIRECTIVE_TOKEN).
+	major, minor int8
+}
+
+// Events
+
+type yaml_event_type_t int8
+
+// Event types.
+const (
+	// An empty event.
+	yaml_NO_EVENT yaml_event_type_t = iota
+
+	yaml_STREAM_START_EVENT   // A STREAM-START event.
+	yaml_STREAM_END_EVENT     // A STREAM-END event.
+	yaml_DOCUMENT_START_EVENT // A DOCUMENT-START event.
+	yaml_DOCUMENT_END_EVENT   // A DOCUMENT-END event.
+	yaml_ALIAS_EVENT          // An ALIAS event.
+	yaml_SCALAR_EVENT         // A SCALAR event.
+	yaml_SEQUENCE_START_EVENT // A SEQUENCE-START event.
+	yaml_SEQUENCE_END_EVENT   // A SEQUENCE-END event.
+	yaml_MAPPING_START_EVENT  // A MAPPING-START event.
+	yaml_MAPPING_END_EVENT    // A MAPPING-END event.
+	yaml_TAIL_COMMENT_EVENT
+)
+
+var eventStrings = []string{
+	yaml_NO_EVENT:             "none",
+	yaml_STREAM_START_EVENT:   "stream start",
+	yaml_STREAM_END_EVENT:     "stream end",
+	yaml_DOCUMENT_START_EVENT: "document start",
+	yaml_DOCUMENT_END_EVENT:   "document end",
+	yaml_ALIAS_EVENT:          "alias",
+	yaml_SCALAR_EVENT:         "scalar",
+	yaml_SEQUENCE_START_EVENT: "sequence start",
+	yaml_SEQUENCE_END_EVENT:   "sequence end",
+	yaml_MAPPING_START_EVENT:  "mapping start",
+	yaml_MAPPING_END_EVENT:    "mapping end",
+	yaml_TAIL_COMMENT_EVENT:   "tail comment",
+}
+
+func (e yaml_event_type_t) String() string {
+	if e < 0 || int(e) >= len(eventStrings) {
+		return fmt.Sprintf("unknown event %d", e)
+	}
+	return eventStrings[e]
+}
+
+// The event structure.
+type yaml_event_t struct {
+
+	// The event type.
+	typ yaml_event_type_t
+
+	// The start and end of the event.
+	start_mark, end_mark yaml_mark_t
+
+	// The document encoding (for yaml_STREAM_START_EVENT).
+	encoding yaml_encoding_t
+
+	// The version directive (for yaml_DOCUMENT_START_EVENT).
+	version_directive *yaml_version_directive_t
+
+	// The list of tag directives (for yaml_DOCUMENT_START_EVENT).
+	tag_directives []yaml_tag_directive_t
+
+	// The comments
+	head_comment []byte
+	line_comment []byte
+	foot_comment []byte
+	tail_comment []byte
+
+	// The anchor (for yaml_SCALAR_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT, yaml_ALIAS_EVENT).
+	anchor []byte
+
+	// The tag (for yaml_SCALAR_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT).
+	tag []byte
+
+	// The scalar value (for yaml_SCALAR_EVENT).
+	value []byte
+
+	// Is the document start/end indicator implicit, or the tag optional?
+	// (for yaml_DOCUMENT_START_EVENT, yaml_DOCUMENT_END_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT, yaml_SCALAR_EVENT).
+	implicit bool
+
+	// Is the tag optional for any non-plain style? (for yaml_SCALAR_EVENT).
+	quoted_implicit bool
+
+	// The style (for yaml_SCALAR_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT).
+	style yaml_style_t
+}
+
+func (e *yaml_event_t) scalar_style() yaml_scalar_style_t     { return yaml_scalar_style_t(e.style) }
+func (e *yaml_event_t) sequence_style() yaml_sequence_style_t { return yaml_sequence_style_t(e.style) }
+func (e *yaml_event_t) mapping_style() yaml_mapping_style_t   { return yaml_mapping_style_t(e.style) }
+
+// Nodes
+
+const (
+	yaml_NULL_TAG      = "tag:yaml.org,2002:null"      // The tag !!null with the only possible value: null.
+	yaml_BOOL_TAG      = "tag:yaml.org,2002:bool"      // The tag !!bool with the values: true and false.
+	yaml_STR_TAG       = "tag:yaml.org,2002:str"       // The tag !!str for string values.
+	yaml_INT_TAG       = "tag:yaml.org,2002:int"       // The tag !!int for integer values.
+	yaml_FLOAT_TAG     = "tag:yaml.org,2002:float"     // The tag !!float for float values.
+	yaml_TIMESTAMP_TAG = "tag:yaml.org,2002:timestamp" // The tag !!timestamp for date and time values.
+
+	yaml_SEQ_TAG = "tag:yaml.org,2002:seq" // The tag !!seq is used to denote sequences.
+	yaml_MAP_TAG = "tag:yaml.org,2002:map" // The tag !!map is used to denote mapping.
+
+	// Not in original libyaml.
+	yaml_BINARY_TAG = "tag:yaml.org,2002:binary"
+	yaml_MERGE_TAG  = "tag:yaml.org,2002:merge"
+
+	yaml_DEFAULT_SCALAR_TAG   = yaml_STR_TAG // The default scalar tag is !!str.
+	yaml_DEFAULT_SEQUENCE_TAG = yaml_SEQ_TAG // The default sequence tag is !!seq.
+	yaml_DEFAULT_MAPPING_TAG  = yaml_MAP_TAG // The default mapping tag is !!map.
+)
+
+type yaml_node_type_t int
+
+// Node types.
+const (
+	// An empty node.
+	yaml_NO_NODE yaml_node_type_t = iota
+
+	yaml_SCALAR_NODE   // A scalar node.
+	yaml_SEQUENCE_NODE // A sequence node.
+	yaml_MAPPING_NODE  // A mapping node.
+)
+
+// An element of a sequence node.
+type yaml_node_item_t int
+
+// An element of a mapping node.
+type yaml_node_pair_t struct {
+	key   int // The key of the element.
+	value int // The value of the element.
+}
+
+// The node structure.
+type yaml_node_t struct {
+	typ yaml_node_type_t // The node type.
+	tag []byte           // The node tag.
+
+	// The node data.
+
+	// The scalar parameters (for yaml_SCALAR_NODE).
+	scalar struct {
+		value  []byte              // The scalar value.
+		length int                 // The length of the scalar value.
+		style  yaml_scalar_style_t // The scalar style.
+	}
+
+	// The sequence parameters (for YAML_SEQUENCE_NODE).
+	sequence struct {
+		items_data []yaml_node_item_t    // The stack of sequence items.
+		style      yaml_sequence_style_t // The sequence style.
+	}
+
+	// The mapping parameters (for yaml_MAPPING_NODE).
+	mapping struct {
+		pairs_data  []yaml_node_pair_t   // The stack of mapping pairs (key, value).
+		pairs_start *yaml_node_pair_t    // The beginning of the stack.
+		pairs_end   *yaml_node_pair_t    // The end of the stack.
+		pairs_top   *yaml_node_pair_t    // The top of the stack.
+		style       yaml_mapping_style_t // The mapping style.
+	}
+
+	start_mark yaml_mark_t // The beginning of the node.
+	end_mark   yaml_mark_t // The end of the node.
+
+}
+
+// The document structure.
+type yaml_document_t struct {
+
+	// The document nodes.
+	nodes []yaml_node_t
+
+	// The version directive.
+	version_directive *yaml_version_directive_t
+
+	// The list of tag directives.
+	tag_directives_data  []yaml_tag_directive_t
+	tag_directives_start int // The beginning of the tag directives list.
+	tag_directives_end   int // The end of the tag directives list.
+
+	start_implicit int // Is the document start indicator implicit?
+	end_implicit   int // Is the document end indicator implicit?
+
+	// The start/end of the document.
+	start_mark, end_mark yaml_mark_t
+}
+
+// The prototype of a read handler.
+//
+// The read handler is called when the parser needs to read more bytes from the
+// source. The handler should write not more than size bytes to the buffer.
+// The number of written bytes should be set to the size_read variable.
+//
+// [in,out]   data        A pointer to an application data specified by
+//                        yaml_parser_set_input().
+// [out]      buffer      The buffer to write the data from the source.
+// [in]       size        The size of the buffer.
+// [out]      size_read   The actual number of bytes read from the source.
+//
+// On success, the handler should return 1.  If the handler failed,
+// the returned value should be 0. On EOF, the handler should set the
+// size_read to 0 and return 1.
+type yaml_read_handler_t func(parser *yaml_parser_t, buffer []byte) (n int, err error)
+
+// This structure holds information about a potential simple key.
+type yaml_simple_key_t struct {
+	possible     bool        // Is a simple key possible?
+	required     bool        // Is a simple key required?
+	token_number int         // The number of the token.
+	mark         yaml_mark_t // The position mark.
+}
+
+// The states of the parser.
+type yaml_parser_state_t int
+
+const (
+	yaml_PARSE_STREAM_START_STATE yaml_parser_state_t = iota
+
+	yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE           // Expect the beginning of an implicit document.
+	yaml_PARSE_DOCUMENT_START_STATE                    // Expect DOCUMENT-START.
+	yaml_PARSE_DOCUMENT_CONTENT_STATE                  // Expect the content of a document.
+	yaml_PARSE_DOCUMENT_END_STATE                      // Expect DOCUMENT-END.
+	yaml_PARSE_BLOCK_NODE_STATE                        // Expect a block node.
+	yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE // Expect a block node or indentless sequence.
+	yaml_PARSE_FLOW_NODE_STATE                         // Expect a flow node.
+	yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE        // Expect the first entry of a block sequence.
+	yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE              // Expect an entry of a block sequence.
+	yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE         // Expect an entry of an indentless sequence.
+	yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE           // Expect the first key of a block mapping.
+	yaml_PARSE_BLOCK_MAPPING_KEY_STATE                 // Expect a block mapping key.
+	yaml_PARSE_BLOCK_MAPPING_VALUE_STATE               // Expect a block mapping value.
+	yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE         // Expect the first entry of a flow sequence.
+	yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE               // Expect an entry of a flow sequence.
+	yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE   // Expect a key of an ordered mapping.
+	yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE // Expect a value of an ordered mapping.
+	yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE   // Expect the and of an ordered mapping entry.
+	yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE            // Expect the first key of a flow mapping.
+	yaml_PARSE_FLOW_MAPPING_KEY_STATE                  // Expect a key of a flow mapping.
+	yaml_PARSE_FLOW_MAPPING_VALUE_STATE                // Expect a value of a flow mapping.
+	yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE          // Expect an empty value of a flow mapping.
+	yaml_PARSE_END_STATE                               // Expect nothing.
+)
+
+func (ps yaml_parser_state_t) String() string {
+	switch ps {
+	case yaml_PARSE_STREAM_START_STATE:
+		return "yaml_PARSE_STREAM_START_STATE"
+	case yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE:
+		return "yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE"
+	case yaml_PARSE_DOCUMENT_START_STATE:
+		return "yaml_PARSE_DOCUMENT_START_STATE"
+	case yaml_PARSE_DOCUMENT_CONTENT_STATE:
+		return "yaml_PARSE_DOCUMENT_CONTENT_STATE"
+	case yaml_PARSE_DOCUMENT_END_STATE:
+		return "yaml_PARSE_DOCUMENT_END_STATE"
+	case yaml_PARSE_BLOCK_NODE_STATE:
+		return "yaml_PARSE_BLOCK_NODE_STATE"
+	case yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE:
+		return "yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE"
+	case yaml_PARSE_FLOW_NODE_STATE:
+		return "yaml_PARSE_FLOW_NODE_STATE"
+	case yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE:
+		return "yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE"
+	case yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE:
+		return "yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE"
+	case yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE:
+		return "yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE"
+	case yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE:
+		return "yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE"
+	case yaml_PARSE_BLOCK_MAPPING_KEY_STATE:
+		return "yaml_PARSE_BLOCK_MAPPING_KEY_STATE"
+	case yaml_PARSE_BLOCK_MAPPING_VALUE_STATE:
+		return "yaml_PARSE_BLOCK_MAPPING_VALUE_STATE"
+	case yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE:
+		return "yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE"
+	case yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE:
+		return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE"
+	case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE:
+		return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE"
+	case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE:
+		return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE"
+	case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE:
+		return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE"
+	case yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE:
+		return "yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE"
+	case yaml_PARSE_FLOW_MAPPING_KEY_STATE:
+		return "yaml_PARSE_FLOW_MAPPING_KEY_STATE"
+	case yaml_PARSE_FLOW_MAPPING_VALUE_STATE:
+		return "yaml_PARSE_FLOW_MAPPING_VALUE_STATE"
+	case yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE:
+		return "yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE"
+	case yaml_PARSE_END_STATE:
+		return "yaml_PARSE_END_STATE"
+	}
+	return "<unknown parser state>"
+}
+
+// This structure holds aliases data.
+type yaml_alias_data_t struct {
+	anchor []byte      // The anchor.
+	index  int         // The node id.
+	mark   yaml_mark_t // The anchor mark.
+}
+
+// The parser structure.
+//
+// All members are internal. Manage the structure using the
+// yaml_parser_ family of functions.
+type yaml_parser_t struct {
+
+	// Error handling
+
+	error yaml_error_type_t // Error type.
+
+	problem string // Error description.
+
+	// The byte about which the problem occurred.
+	problem_offset int
+	problem_value  int
+	problem_mark   yaml_mark_t
+
+	// The error context.
+	context      string
+	context_mark yaml_mark_t
+
+	// Reader stuff
+
+	read_handler yaml_read_handler_t // Read handler.
+
+	input_reader io.Reader // File input data.
+	input        []byte    // String input data.
+	input_pos    int
+
+	eof bool // EOF flag
+
+	buffer     []byte // The working buffer.
+	buffer_pos int    // The current position of the buffer.
+
+	unread int // The number of unread characters in the buffer.
+
+	newlines int // The number of line breaks since last non-break/non-blank character
+
+	raw_buffer     []byte // The raw buffer.
+	raw_buffer_pos int    // The current position of the buffer.
+
+	encoding yaml_encoding_t // The input encoding.
+
+	offset int         // The offset of the current position (in bytes).
+	mark   yaml_mark_t // The mark of the current position.
+
+	// Comments
+
+	head_comment []byte // The current head comments
+	line_comment []byte // The current line comments
+	foot_comment []byte // The current foot comments
+	tail_comment []byte // Foot comment that happens at the end of a block.
+	stem_comment []byte // Comment in item preceding a nested structure (list inside list item, etc)
+
+	comments      []yaml_comment_t // The folded comments for all parsed tokens
+	comments_head int
+
+	// Scanner stuff
+
+	stream_start_produced bool // Have we started to scan the input stream?
+	stream_end_produced   bool // Have we reached the end of the input stream?
+
+	flow_level int // The number of unclosed '[' and '{' indicators.
+
+	tokens          []yaml_token_t // The tokens queue.
+	tokens_head     int            // The head of the tokens queue.
+	tokens_parsed   int            // The number of tokens fetched from the queue.
+	token_available bool           // Does the tokens queue contain a token ready for dequeueing.
+
+	indent  int   // The current indentation level.
+	indents []int // The indentation levels stack.
+
+	simple_key_allowed bool                // May a simple key occur at the current position?
+	simple_keys        []yaml_simple_key_t // The stack of simple keys.
+	simple_keys_by_tok map[int]int         // possible simple_key indexes indexed by token_number
+
+	// Parser stuff
+
+	state          yaml_parser_state_t    // The current parser state.
+	states         []yaml_parser_state_t  // The parser states stack.
+	marks          []yaml_mark_t          // The stack of marks.
+	tag_directives []yaml_tag_directive_t // The list of TAG directives.
+
+	// Dumper stuff
+
+	aliases []yaml_alias_data_t // The alias data.
+
+	document *yaml_document_t // The currently parsed document.
+}
+
+type yaml_comment_t struct {
+
+	scan_mark  yaml_mark_t // Position where scanning for comments started
+	token_mark yaml_mark_t // Position after which tokens will be associated with this comment
+	start_mark yaml_mark_t // Position of '#' comment mark
+	end_mark   yaml_mark_t // Position where comment terminated
+
+	head []byte
+	line []byte
+	foot []byte
+}
+
+// Emitter Definitions
+
+// The prototype of a write handler.
+//
+// The write handler is called when the emitter needs to flush the accumulated
+// characters to the output.  The handler should write @a size bytes of the
+// @a buffer to the output.
+//
+// @param[in,out]   data        A pointer to an application data specified by
+//                              yaml_emitter_set_output().
+// @param[in]       buffer      The buffer with bytes to be written.
+// @param[in]       size        The size of the buffer.
+//
+// @returns On success, the handler should return @c 1.  If the handler failed,
+// the returned value should be @c 0.
+//
+type yaml_write_handler_t func(emitter *yaml_emitter_t, buffer []byte) error
+
+type yaml_emitter_state_t int
+
+// The emitter states.
+const (
+	// Expect STREAM-START.
+	yaml_EMIT_STREAM_START_STATE yaml_emitter_state_t = iota
+
+	yaml_EMIT_FIRST_DOCUMENT_START_STATE       // Expect the first DOCUMENT-START or STREAM-END.
+	yaml_EMIT_DOCUMENT_START_STATE             // Expect DOCUMENT-START or STREAM-END.
+	yaml_EMIT_DOCUMENT_CONTENT_STATE           // Expect the content of a document.
+	yaml_EMIT_DOCUMENT_END_STATE               // Expect DOCUMENT-END.
+	yaml_EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE   // Expect the first item of a flow sequence.
+	yaml_EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE   // Expect the next item of a flow sequence, with the comma already written out
+	yaml_EMIT_FLOW_SEQUENCE_ITEM_STATE         // Expect an item of a flow sequence.
+	yaml_EMIT_FLOW_MAPPING_FIRST_KEY_STATE     // Expect the first key of a flow mapping.
+	yaml_EMIT_FLOW_MAPPING_TRAIL_KEY_STATE     // Expect the next key of a flow mapping, with the comma already written out
+	yaml_EMIT_FLOW_MAPPING_KEY_STATE           // Expect a key of a flow mapping.
+	yaml_EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE  // Expect a value for a simple key of a flow mapping.
+	yaml_EMIT_FLOW_MAPPING_VALUE_STATE         // Expect a value of a flow mapping.
+	yaml_EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE  // Expect the first item of a block sequence.
+	yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE        // Expect an item of a block sequence.
+	yaml_EMIT_BLOCK_MAPPING_FIRST_KEY_STATE    // Expect the first key of a block mapping.
+	yaml_EMIT_BLOCK_MAPPING_KEY_STATE          // Expect the key of a block mapping.
+	yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE // Expect a value for a simple key of a block mapping.
+	yaml_EMIT_BLOCK_MAPPING_VALUE_STATE        // Expect a value of a block mapping.
+	yaml_EMIT_END_STATE                        // Expect nothing.
+)
+
+// The emitter structure.
+//
+// All members are internal.  Manage the structure using the @c yaml_emitter_
+// family of functions.
+type yaml_emitter_t struct {
+
+	// Error handling
+
+	error   yaml_error_type_t // Error type.
+	problem string            // Error description.
+
+	// Writer stuff
+
+	write_handler yaml_write_handler_t // Write handler.
+
+	output_buffer *[]byte   // String output data.
+	output_writer io.Writer // File output data.
+
+	buffer     []byte // The working buffer.
+	buffer_pos int    // The current position of the buffer.
+
+	raw_buffer     []byte // The raw buffer.
+	raw_buffer_pos int    // The current position of the buffer.
+
+	encoding yaml_encoding_t // The stream encoding.
+
+	// Emitter stuff
+
+	canonical   bool         // If the output is in the canonical style?
+	best_indent int          // The number of indentation spaces.
+	best_width  int          // The preferred width of the output lines.
+	unicode     bool         // Allow unescaped non-ASCII characters?
+	line_break  yaml_break_t // The preferred line break.
+
+	state  yaml_emitter_state_t   // The current emitter state.
+	states []yaml_emitter_state_t // The stack of states.
+
+	events      []yaml_event_t // The event queue.
+	events_head int            // The head of the event queue.
+
+	indents []int // The stack of indentation levels.
+
+	tag_directives []yaml_tag_directive_t // The list of tag directives.
+
+	indent int // The current indentation level.
+
+	flow_level int // The current flow level.
+
+	root_context       bool // Is it the document root context?
+	sequence_context   bool // Is it a sequence context?
+	mapping_context    bool // Is it a mapping context?
+	simple_key_context bool // Is it a simple mapping key context?
+
+	line       int  // The current line.
+	column     int  // The current column.
+	whitespace bool // If the last character was a whitespace?
+	indention  bool // If the last character was an indentation character (' ', '-', '?', ':')?
+	open_ended bool // If an explicit document end is required?
+
+	space_above bool // Is there's an empty line above?
+	foot_indent int  // The indent used to write the foot comment above, or -1 if none.
+
+	// Anchor analysis.
+	anchor_data struct {
+		anchor []byte // The anchor value.
+		alias  bool   // Is it an alias?
+	}
+
+	// Tag analysis.
+	tag_data struct {
+		handle []byte // The tag handle.
+		suffix []byte // The tag suffix.
+	}
+
+	// Scalar analysis.
+	scalar_data struct {
+		value                 []byte              // The scalar value.
+		multiline             bool                // Does the scalar contain line breaks?
+		flow_plain_allowed    bool                // Can the scalar be expessed in the flow plain style?
+		block_plain_allowed   bool                // Can the scalar be expressed in the block plain style?
+		single_quoted_allowed bool                // Can the scalar be expressed in the single quoted style?
+		block_allowed         bool                // Can the scalar be expressed in the literal or folded styles?
+		style                 yaml_scalar_style_t // The output style.
+	}
+
+	// Comments
+	head_comment []byte
+	line_comment []byte
+	foot_comment []byte
+	tail_comment []byte
+
+	key_line_comment []byte
+
+	// Dumper stuff
+
+	opened bool // If the stream was already opened?
+	closed bool // If the stream was already closed?
+
+	// The information associated with the document nodes.
+	anchors *struct {
+		references int  // The number of references.
+		anchor     int  // The anchor id.
+		serialized bool // If the node has been emitted?
+	}
+
+	last_anchor_id int // The last assigned anchor id.
+
+	document *yaml_document_t // The currently emitted document.
+}
diff --git a/vendor/gopkg.in/yaml.v3/yamlprivateh.go b/vendor/gopkg.in/yaml.v3/yamlprivateh.go
new file mode 100644
index 000000000..e88f9c54a
--- /dev/null
+++ b/vendor/gopkg.in/yaml.v3/yamlprivateh.go
@@ -0,0 +1,198 @@
+// 
+// Copyright (c) 2011-2019 Canonical Ltd
+// Copyright (c) 2006-2010 Kirill Simonov
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package yaml
+
+const (
+	// The size of the input raw buffer.
+	input_raw_buffer_size = 512
+
+	// The size of the input buffer.
+	// It should be possible to decode the whole raw buffer.
+	input_buffer_size = input_raw_buffer_size * 3
+
+	// The size of the output buffer.
+	output_buffer_size = 128
+
+	// The size of the output raw buffer.
+	// It should be possible to encode the whole output buffer.
+	output_raw_buffer_size = (output_buffer_size*2 + 2)
+
+	// The size of other stacks and queues.
+	initial_stack_size  = 16
+	initial_queue_size  = 16
+	initial_string_size = 16
+)
+
+// Check if the character at the specified position is an alphabetical
+// character, a digit, '_', or '-'.
+func is_alpha(b []byte, i int) bool {
+	return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'Z' || b[i] >= 'a' && b[i] <= 'z' || b[i] == '_' || b[i] == '-'
+}
+
+// Check if the character at the specified position is a digit.
+func is_digit(b []byte, i int) bool {
+	return b[i] >= '0' && b[i] <= '9'
+}
+
+// Get the value of a digit.
+func as_digit(b []byte, i int) int {
+	return int(b[i]) - '0'
+}
+
+// Check if the character at the specified position is a hex-digit.
+func is_hex(b []byte, i int) bool {
+	return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'F' || b[i] >= 'a' && b[i] <= 'f'
+}
+
+// Get the value of a hex-digit.
+func as_hex(b []byte, i int) int {
+	bi := b[i]
+	if bi >= 'A' && bi <= 'F' {
+		return int(bi) - 'A' + 10
+	}
+	if bi >= 'a' && bi <= 'f' {
+		return int(bi) - 'a' + 10
+	}
+	return int(bi) - '0'
+}
+
+// Check if the character is ASCII.
+func is_ascii(b []byte, i int) bool {
+	return b[i] <= 0x7F
+}
+
+// Check if the character at the start of the buffer can be printed unescaped.
+func is_printable(b []byte, i int) bool {
+	return ((b[i] == 0x0A) || // . == #x0A
+		(b[i] >= 0x20 && b[i] <= 0x7E) || // #x20 <= . <= #x7E
+		(b[i] == 0xC2 && b[i+1] >= 0xA0) || // #0xA0 <= . <= #xD7FF
+		(b[i] > 0xC2 && b[i] < 0xED) ||
+		(b[i] == 0xED && b[i+1] < 0xA0) ||
+		(b[i] == 0xEE) ||
+		(b[i] == 0xEF && // #xE000 <= . <= #xFFFD
+			!(b[i+1] == 0xBB && b[i+2] == 0xBF) && // && . != #xFEFF
+			!(b[i+1] == 0xBF && (b[i+2] == 0xBE || b[i+2] == 0xBF))))
+}
+
+// Check if the character at the specified position is NUL.
+func is_z(b []byte, i int) bool {
+	return b[i] == 0x00
+}
+
+// Check if the beginning of the buffer is a BOM.
+func is_bom(b []byte, i int) bool {
+	return b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF
+}
+
+// Check if the character at the specified position is space.
+func is_space(b []byte, i int) bool {
+	return b[i] == ' '
+}
+
+// Check if the character at the specified position is tab.
+func is_tab(b []byte, i int) bool {
+	return b[i] == '\t'
+}
+
+// Check if the character at the specified position is blank (space or tab).
+func is_blank(b []byte, i int) bool {
+	//return is_space(b, i) || is_tab(b, i)
+	return b[i] == ' ' || b[i] == '\t'
+}
+
+// Check if the character at the specified position is a line break.
+func is_break(b []byte, i int) bool {
+	return (b[i] == '\r' || // CR (#xD)
+		b[i] == '\n' || // LF (#xA)
+		b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85)
+		b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028)
+		b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9) // PS (#x2029)
+}
+
+func is_crlf(b []byte, i int) bool {
+	return b[i] == '\r' && b[i+1] == '\n'
+}
+
+// Check if the character is a line break or NUL.
+func is_breakz(b []byte, i int) bool {
+	//return is_break(b, i) || is_z(b, i)
+	return (
+		// is_break:
+		b[i] == '\r' || // CR (#xD)
+		b[i] == '\n' || // LF (#xA)
+		b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85)
+		b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028)
+		b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029)
+		// is_z:
+		b[i] == 0)
+}
+
+// Check if the character is a line break, space, or NUL.
+func is_spacez(b []byte, i int) bool {
+	//return is_space(b, i) || is_breakz(b, i)
+	return (
+		// is_space:
+		b[i] == ' ' ||
+		// is_breakz:
+		b[i] == '\r' || // CR (#xD)
+		b[i] == '\n' || // LF (#xA)
+		b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85)
+		b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028)
+		b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029)
+		b[i] == 0)
+}
+
+// Check if the character is a line break, space, tab, or NUL.
+func is_blankz(b []byte, i int) bool {
+	//return is_blank(b, i) || is_breakz(b, i)
+	return (
+		// is_blank:
+		b[i] == ' ' || b[i] == '\t' ||
+		// is_breakz:
+		b[i] == '\r' || // CR (#xD)
+		b[i] == '\n' || // LF (#xA)
+		b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85)
+		b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028)
+		b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029)
+		b[i] == 0)
+}
+
+// Determine the width of the character.
+func width(b byte) int {
+	// Don't replace these by a switch without first
+	// confirming that it is being inlined.
+	if b&0x80 == 0x00 {
+		return 1
+	}
+	if b&0xE0 == 0xC0 {
+		return 2
+	}
+	if b&0xF0 == 0xE0 {
+		return 3
+	}
+	if b&0xF8 == 0xF0 {
+		return 4
+	}
+	return 0
+
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index f5b95720e..921bc55ec 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1,8 +1,8 @@
 ## workspace
-# code.gitea.io/sdk/gitea v0.21.0
-## explicit; go 1.23
+# code.gitea.io/sdk/gitea v0.22.1
+## explicit; go 1.23.0
 code.gitea.io/sdk/gitea
-# codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.1.0
+# codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0
 ## explicit; go 1.23.0
 codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2
 # github.com/42wim/httpsig v1.2.3
@@ -21,41 +21,55 @@ github.com/atotto/clipboard
 # github.com/aymanbagabas/go-osc52/v2 v2.0.1
 ## explicit; go 1.16
 github.com/aymanbagabas/go-osc52/v2
-# github.com/carlmjohnson/requests v0.24.3
-## explicit; go 1.22
+# github.com/bahlo/generic-list-go v0.2.0
+## explicit; go 1.18
+github.com/bahlo/generic-list-go
+# github.com/buger/jsonparser v1.1.1
+## explicit; go 1.13
+github.com/buger/jsonparser
+# github.com/carlmjohnson/requests v0.25.1
+## explicit; go 1.23.0
 github.com/carlmjohnson/requests
 github.com/carlmjohnson/requests/internal/minitrue
-github.com/carlmjohnson/requests/internal/slicex
 # github.com/charmbracelet/bubbles v0.21.0
 ## explicit; go 1.23.0
 github.com/charmbracelet/bubbles/cursor
 github.com/charmbracelet/bubbles/key
 github.com/charmbracelet/bubbles/runeutil
 github.com/charmbracelet/bubbles/textinput
-# github.com/charmbracelet/bubbletea v1.3.5
-## explicit; go 1.23.0
+# github.com/charmbracelet/bubbletea v1.3.10
+## explicit; go 1.24.0
 github.com/charmbracelet/bubbletea
-# github.com/charmbracelet/colorprofile v0.3.1
-## explicit; go 1.23.0
+# github.com/charmbracelet/colorprofile v0.3.3
+## explicit; go 1.24.2
 github.com/charmbracelet/colorprofile
 # github.com/charmbracelet/lipgloss v1.1.0
 ## explicit; go 1.18
 github.com/charmbracelet/lipgloss
-# github.com/charmbracelet/x/ansi v0.9.2
-## explicit; go 1.23.0
+# github.com/charmbracelet/x/ansi v0.10.3
+## explicit; go 1.24.2
 github.com/charmbracelet/x/ansi
-github.com/charmbracelet/x/ansi/kitty
 github.com/charmbracelet/x/ansi/parser
 # github.com/charmbracelet/x/cellbuf v0.0.13
 ## explicit; go 1.18
 github.com/charmbracelet/x/cellbuf
-# github.com/charmbracelet/x/term v0.2.1
-## explicit; go 1.18
+# github.com/charmbracelet/x/term v0.2.2
+## explicit; go 1.24.0
 github.com/charmbracelet/x/term
+# github.com/clipperhouse/displaywidth v0.4.1
+## explicit; go 1.18
+github.com/clipperhouse/displaywidth
+# github.com/clipperhouse/stringish v0.1.1
+## explicit; go 1.18
+github.com/clipperhouse/stringish
+# github.com/clipperhouse/uax29/v2 v2.3.0
+## explicit; go 1.18
+github.com/clipperhouse/uax29/v2/graphemes
+github.com/clipperhouse/uax29/v2/internal/iterators
 # github.com/cucumber/gherkin/go/v26 v26.2.0
 ## explicit; go 1.19
 github.com/cucumber/gherkin/go/v26
-# github.com/cucumber/godog v0.15.0
+# github.com/cucumber/godog v0.15.1
 ## explicit; go 1.16
 github.com/cucumber/godog
 github.com/cucumber/godog/colors
@@ -111,8 +125,8 @@ github.com/hashicorp/go-immutable-radix
 # github.com/hashicorp/go-memdb v1.3.5
 ## explicit; go 1.13
 github.com/hashicorp/go-memdb
-# github.com/hashicorp/go-retryablehttp v0.7.7
-## explicit; go 1.19
+# github.com/hashicorp/go-retryablehttp v0.7.8
+## explicit; go 1.23
 github.com/hashicorp/go-retryablehttp
 # github.com/hashicorp/go-version v1.7.0
 ## explicit
@@ -123,6 +137,9 @@ github.com/hashicorp/golang-lru/simplelru
 # github.com/inconshreveable/mousetrap v1.1.0
 ## explicit; go 1.18
 github.com/inconshreveable/mousetrap
+# github.com/invopop/jsonschema v0.13.0
+## explicit; go 1.18
+github.com/invopop/jsonschema
 # github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
 ## explicit
 github.com/kballard/go-shellquote
@@ -132,20 +149,24 @@ github.com/kr/pretty
 # github.com/kr/text v0.2.0
 ## explicit
 github.com/kr/text
-# github.com/ktrysmt/go-bitbucket v0.9.86
+# github.com/ktrysmt/go-bitbucket v0.9.87
 ## explicit; go 1.23.0
 github.com/ktrysmt/go-bitbucket
-# github.com/lucasb-eyer/go-colorful v1.2.0
+# github.com/lucasb-eyer/go-colorful v1.3.0
 ## explicit; go 1.12
 github.com/lucasb-eyer/go-colorful
+# github.com/mailru/easyjson v0.7.7
+## explicit; go 1.12
+github.com/mailru/easyjson/buffer
+github.com/mailru/easyjson/jwriter
 # github.com/mattn/go-isatty v0.0.20
 ## explicit; go 1.15
 github.com/mattn/go-isatty
 # github.com/mattn/go-localereader v0.0.1
 ## explicit
 github.com/mattn/go-localereader
-# github.com/mattn/go-runewidth v0.0.16
-## explicit; go 1.9
+# github.com/mattn/go-runewidth v0.0.19
+## explicit; go 1.20
 github.com/mattn/go-runewidth
 # github.com/mitchellh/mapstructure v1.5.0
 ## explicit; go 1.14
@@ -169,7 +190,7 @@ github.com/rogpeppe/go-internal/fmtsort
 # github.com/sergi/go-diff v1.4.0
 ## explicit; go 1.13
 github.com/sergi/go-diff/diffmatchpatch
-# github.com/shoenig/test v1.12.1
+# github.com/shoenig/test v1.12.2
 ## explicit; go 1.18
 github.com/shoenig/test/interfaces
 github.com/shoenig/test/internal/assertions
@@ -177,22 +198,23 @@ github.com/shoenig/test/internal/constraints
 github.com/shoenig/test/internal/util
 github.com/shoenig/test/must
 github.com/shoenig/test/wait
-# github.com/spf13/cobra v1.9.1
+# github.com/spf13/cobra v1.10.1
 ## explicit; go 1.15
 github.com/spf13/cobra
-# github.com/spf13/pflag v1.0.6
+# github.com/spf13/pflag v1.0.10
 ## explicit; go 1.12
 github.com/spf13/pflag
+# github.com/wk8/go-ordered-map/v2 v2.1.8
+## explicit; go 1.18
+github.com/wk8/go-ordered-map/v2
 # github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
 ## explicit; go 1.19
 github.com/xo/terminfo
-# gitlab.com/gitlab-org/api/client-go v0.130.1
+# gitlab.com/gitlab-org/api/client-go v0.159.0
 ## explicit; go 1.23.0
 gitlab.com/gitlab-org/api/client-go
-# golang.org/x/crypto v0.39.0
+# golang.org/x/crypto v0.43.0
 ## explicit; go 1.24.0
-# golang.org/x/crypto v0.42.0
-## go 1.24.0
 golang.org/x/crypto/blake2b
 golang.org/x/crypto/blake2s
 golang.org/x/crypto/blowfish
@@ -206,49 +228,44 @@ golang.org/x/crypto/sha3
 golang.org/x/crypto/ssh
 golang.org/x/crypto/ssh/agent
 golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
-# golang.org/x/mod v0.25.0
+# golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
 ## explicit; go 1.24.0
+golang.org/x/exp/constraints
 # golang.org/x/mod v0.28.0
 ## explicit; go 1.24.0
+# golang.org/x/mod v0.29.0
+## explicit; go 1.24.0
 golang.org/x/mod/semver
-# golang.org/x/net v0.41.0
+# golang.org/x/net v0.46.0
 ## explicit; go 1.24.0
-# golang.org/x/net v0.44.0
-## go 1.24.0
 golang.org/x/net/context
 golang.org/x/net/html
 golang.org/x/net/html/atom
 golang.org/x/net/publicsuffix
-# golang.org/x/oauth2 v0.30.0
-## explicit; go 1.23.0
+# golang.org/x/oauth2 v0.32.0
+## explicit; go 1.24.0
 golang.org/x/oauth2
 golang.org/x/oauth2/bitbucket
 golang.org/x/oauth2/clientcredentials
 golang.org/x/oauth2/internal
-# golang.org/x/sync v0.15.0
-## explicit; go 1.24.0
 # golang.org/x/sync v0.17.0
 ## explicit; go 1.24.0
 golang.org/x/sync/errgroup
-# golang.org/x/sys v0.33.0
+# golang.org/x/sys v0.37.0
 ## explicit; go 1.24.0
-# golang.org/x/sys v0.36.0
-## go 1.24.0
 golang.org/x/sys/cpu
 golang.org/x/sys/unix
 golang.org/x/sys/windows
-# golang.org/x/text v0.26.0
+# golang.org/x/text v0.30.0
 ## explicit; go 1.24.0
-# golang.org/x/text v0.29.0
-## go 1.24.0
 golang.org/x/text/transform
-# golang.org/x/time v0.12.0
-## explicit; go 1.23.0
-golang.org/x/time/rate
-# golang.org/x/tools v0.34.0
+# golang.org/x/time v0.14.0
 ## explicit; go 1.24.0
+golang.org/x/time/rate
 # golang.org/x/tools v0.37.0
 ## explicit; go 1.24.0
+# golang.org/x/tools v0.38.0
+## explicit; go 1.24.0
 golang.org/x/tools/go/analysis
 golang.org/x/tools/go/analysis/checker
 golang.org/x/tools/go/analysis/internal
@@ -274,10 +291,12 @@ golang.org/x/tools/internal/event/label
 golang.org/x/tools/internal/facts
 golang.org/x/tools/internal/gcimporter
 golang.org/x/tools/internal/gocommand
-golang.org/x/tools/internal/moreiters
 golang.org/x/tools/internal/packagesinternal
 golang.org/x/tools/internal/pkgbits
 golang.org/x/tools/internal/stdlib
 golang.org/x/tools/internal/typeparams
 golang.org/x/tools/internal/typesinternal
 golang.org/x/tools/internal/versions
+# gopkg.in/yaml.v3 v3.0.1
+## explicit
+gopkg.in/yaml.v3
diff --git a/website/Makefile b/website/Makefile
index ba2c7925b..29c84620e 100644
--- a/website/Makefile
+++ b/website/Makefile
@@ -1,4 +1,4 @@
-RTA_VERSION = 0.17.0  # run-that-app version to use
+RTA_VERSION = 0.24.2  # run-that-app version to use
 
 .DEFAULT_GOAL := help
 
@@ -25,6 +25,6 @@ unit: ../tools/rta@${RTA_VERSION}  # runs the unit tests
 
 ../tools/rta@${RTA_VERSION}:
 	@rm -f ../tools/rta*
-	@(cd ../tools && curl https://raw.githubusercontent.com/kevgo/run-that-app/main/download.sh | sh)
+	@(cd ../tools && curl https://raw.githubusercontent.com/kevgo/run-that-app/main/download.sh | sh -s ${RTA_VERSION})
 	@mv ../tools/rta ../tools/rta@${RTA_VERSION}
 	@ln -s rta@${RTA_VERSION} ../tools/rta
diff --git a/website/book.toml b/website/book.toml
index d588d2e48..991cbab10 100644
--- a/website/book.toml
+++ b/website/book.toml
@@ -4,7 +4,7 @@ description = "Git Town user manual"
 language = "en"
 multilingual = false
 src = "src"
-title = "Git Town 22.1"
+title = "Git Town 22.2"
 
 [preprocessor.gittown]
 command = "../tools/rta node ../tools/mdbook_preprocessor/mdbook_preprocessor.js"
diff --git a/website/src/SUMMARY.md b/website/src/SUMMARY.md
index c34154aa0..d5633bb50 100644
--- a/website/src/SUMMARY.md
+++ b/website/src/SUMMARY.md
@@ -10,6 +10,7 @@
   - [LazyGit](how-to/lazygit.md)
   - [Shell prompt](how-to/shell-prompt.md)
   - [Hotfixes](how-to/hotfixes.md)
+  - [Sync lineage](how-to/sync-lineage.md)
 - [Commands](all-commands.md)
   - [Basic workflow](basic-commands.md)
     - [hack](commands/hack.md)
@@ -69,10 +70,12 @@
     - [Perennial regex](preferences/perennial-regex.md)
     - [Unknown branch type](preferences/unknown-branch-type.md)
   - [Create]()
+    - [Branch prefix](preferences/branch-prefix.md)
     - [New branch type](preferences/new-branch-type.md)
     - [Share new branches](preferences/share-new-branches.md)
     - [Stash](preferences/stash.md)
   - [Hosting]()
+    - [Browser](preferences/browser.md)
     - [Development remote](preferences/dev-remote.md)
     - [Forge Type](preferences/forge-type.md)
     - [Origin hostname](preferences/hosting-origin-hostname.md)
@@ -99,5 +102,4 @@
     - [Sync tags](preferences/sync-tags.md)
     - [Sync with upstream](preferences/sync-upstream.md)
   - [Offline mode](preferences/offline.md)
-  - [BROWSER env variable](preferences/browser.md)
   - [Branch lineage](preferences/parent.md)
diff --git a/website/src/commands/append.md b/website/src/commands/append.md
index ed2674737..ef4ca41f3 100644
--- a/website/src/commands/append.md
+++ b/website/src/commands/append.md
@@ -1,7 +1,9 @@
 # git town append
 
+<a type="git-town-command" />
+
 ```command-summary
-git town append <branch-name> [-p | --prototype] [-d | --detached] [-c | --commit] [-m | --message <message>] [--propose] [--dry-run] [-v | --verbose]
+git town append <branch-name> [--auto-resolve] [-b | --beam] [-c | --commit] [-d | --detached] [--dry-run] [-h | --help] [(-m | --message) <message>] [--propose] [-p | --prototype] [--push] [--stash] [--sync] [-v | --verbose]
 ```
 
 The _append_ command creates a new feature branch with the given name as a
@@ -40,10 +42,25 @@ branch with the main branch as its parent.
 
 ## Options
 
-#### `-p`<br>`--prototype`
+#### `--auto-resolve`
 
-Adding the `--prototype` aka `-p` switch creates a
-[prototype branch](../branch-types.md#prototype-branches).
+Disables automatic resolution of
+[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
+
+#### `-b`<br>`--beam`
+
+Moves ("beams") one or more commits from the current branch to the new child
+branch that gets created. Lets you select the commits to beam via a visual
+dialog. Beaming suppresses all branch updates. Any merge conflicts encountered
+while beaming arise from moving the beamed commits.
+
+#### `-c`<br>`--commit`
+
+When given, commits the currently staged changes into the branch to create and
+remains on the current branch. This is intended to quickly commit changes
+unrelated to the current branch into another branch and keep hacking on the
+current branch. Committing suppresses all branch updates to allow you to get
+your open changes committed.
 
 #### `-d`<br>`--detached`<br>`--no-detached`
 
@@ -56,22 +73,16 @@ In detached mode, feature branches don't receive updates from the perennial
 branch at the root of your branch hierarchy. This can be useful in busy
 monorepos.
 
-#### `-c`<br>`--commit`
+#### `--dry-run`
 
-When given, commits the currently staged changes into the branch to create and
-remains on the current branch. This is intended to quickly commit changes
-unrelated to the current branch into another branch and keep hacking on the
-current branch. Committing suppresses all branch updates to allow you to get
-your open changes committed.
+Use the `--dry-run` flag to test-drive this command. It prints the Git commands
+that would be run but doesn't execute them.
 
-#### `-b`<br>`--beam`
+#### `-h`<br>`--help`
 
-Moves ("beams") one or more commits from the current branch to the new child
-branch that gets created. Lets you select the commits to beam via a visual
-dialog. Beaming suppresses all branch updates. Any merge conflicts encountered
-while beaming arise from moving the beamed commits.
+Display help for this command.
 
-#### `-m`<br>`--message`
+#### `-m <msg>`<br>`--message <msg>`
 
 Commit message to use together with `--commit`. Implies `--commit`.
 
@@ -82,6 +93,11 @@ Propose the created branch.
 To always propose new branches, set the
 [share new branches](../preferences/share-new-branches.md) setting to `propose`.
 
+#### `-p`<br>`--prototype`
+
+Adding the `--prototype` aka `-p` switch creates a
+[prototype branch](../branch-types.md#prototype-branches).
+
 #### `--push`<br>`--no-push`
 
 The `--push`/`--no-push` argument overrides the
@@ -96,21 +112,11 @@ Enables or disables [stashing](../preferences/stash.md) for this invocation.
 Enables or disables [automatic syncing](../preferences/auto-sync.md) of the
 current branch before appending the new one.
 
-#### `--dry-run`
-
-Use the `--dry-run` flag to test-drive this command. It prints the Git commands
-that would be run but doesn't execute them.
-
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
 determine the repository state.
 
-#### `--auto-resolve`
-
-Disables automatic resolution of
-[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
-
 ## Configuration
 
 If [share-new-branches](../preferences/share-new-branches.md) is configured,
@@ -125,6 +131,10 @@ creates a branch with the given [type](../branch-types.md).
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [hack](hack.md) creates the new branch as a child of the
   [main branch](../preferences/main-branch.md)
 - [prepend](prepend.md) creates the new branch as a parent of the current branch
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/branch.md b/website/src/commands/branch.md
index 9c25bdd6d..5bf3024c5 100644
--- a/website/src/commands/branch.md
+++ b/website/src/commands/branch.md
@@ -1,7 +1,9 @@
 # git town branch
 
+<a type="git-town-command" />
+
 ```command-summary
-git town branch [-v | --verbose]
+git town branch [(-d | --display-types) <branch-types>] [-h | --help] [(-o | --order) <asc|desc>] [-v | --verbose]
 ```
 
 The _branch_ command is Git Town's equivalent of the
@@ -11,13 +13,17 @@ branches.
 
 ## Options
 
-#### `-d`<br>`--display-types`
+#### `-d <branch-type>`<br>`--display-types <branch-type>`
 
 This flag allows customizing whether Git Town also displays the branch type in
 addition to the branch name when showing a list of branches. More info
 [here](../preferences/display-types.md#cli-flags).
 
-#### `-o`<br>`--order`
+#### `-h`<br>`--help`
+
+Display help for this command.
+
+#### `-o <asc|desc>`<br>`--order <asc|desc>`
 
 The `--order` flag allows customizing the order in which branches get displayed.
 More info [here](../preferences/order.md#cli-flag)
@@ -29,7 +35,11 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [switch](switch.md) displays the branch hierarchy and lets you switch to a new
   branch in it
 - [walk](walk.md) executes a shell command or opens a shell in each of your
   local branches
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/completions.md b/website/src/commands/completions.md
index 3815e2fab..75512a317 100644
--- a/website/src/commands/completions.md
+++ b/website/src/commands/completions.md
@@ -1,7 +1,9 @@
 # git town completions
 
+<a type="git-town-command" />
+
 ```command-summary
-git town completions (bash|fish|powershell|zsh) [--no-descriptions]
+git town completions (bash|fish|powershell|zsh) [-h | --help] [--no-descriptions]
 ```
 
 The _completions_ command outputs shell scripts that enable auto-completion for
@@ -58,6 +60,10 @@ autoload -Uz compinit
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `--no-descriptions`
 
 The `--no-descriptions` flag outputs shorter completions without descriptions of
diff --git a/website/src/commands/compress.md b/website/src/commands/compress.md
index 6309996a3..1f04edc36 100644
--- a/website/src/commands/compress.md
+++ b/website/src/commands/compress.md
@@ -1,7 +1,9 @@
 # git town compress
 
+<a type="git-town-command" />
+
 ```command-summary
-git town compress [-m <text> | --message <text>] [-s | --stack] [--dry-run] [-v | --verbose]
+git town compress [--dry-run] [-h | --help] [(-m | --message) <text>] [--no-verify] [-s | --stack] [-v | --verbose]
 ```
 
 The _compress_ command squashes all commits on a branch into a single commit.
@@ -42,6 +44,15 @@ this command.
 
 ## Options
 
+#### `--dry-run`
+
+Use the `--dry-run` flag to test-drive this command. It prints the Git commands
+that would be run but doesn't execute them.
+
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-m <text>`<br>`--message <text>`
 
 By default the now compressed commit uses the commit message of the first commit
@@ -126,11 +137,6 @@ changes from the old `commit 1a`, `commit 1b`, and `commit 1c`. The new
 `commit 2a` contains the changes made in `branch 2`, i.e. the changes from the
 old `commit 2a`, `commit 2b`, and `commit 2c`.
 
-#### `--dry-run`
-
-Use the `--dry-run` flag to test-drive this command. It prints the Git commands
-that would be run but doesn't execute them.
-
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -138,5 +144,9 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - the [compress sync strategy](../preferences/sync-feature-strategy.md#compress)
   automatically compresses branches when they get synced
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/config-get-parent.md b/website/src/commands/config-get-parent.md
index 14f2694e6..4f8520668 100644
--- a/website/src/commands/config-get-parent.md
+++ b/website/src/commands/config-get-parent.md
@@ -1,14 +1,22 @@
 # git town config get-parent
 
+<a type="git-town-command" />
+
 ```command-summary
-git town config get-parent [<branch-name>] [-v | --verbose]
+git town config get-parent [<branch-name>] [-h | --help] [-v | --verbose]
 ```
 
+</a>
+
 The _config get-parent_ command outputs the parent branch of the current or
 given branch.
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
diff --git a/website/src/commands/config-remove.md b/website/src/commands/config-remove.md
index 3957082f3..f9ce63000 100644
--- a/website/src/commands/config-remove.md
+++ b/website/src/commands/config-remove.md
@@ -1,7 +1,9 @@
 # git town config remove
 
+<a type="git-town-command" />
+
 ```command-summary
-git town config remove [-v | --verbose]
+git town config remove [-h | --help] [-v | --verbose]
 ```
 
 The _config remove_ command removes all Git Town related configuration from the
@@ -9,6 +11,10 @@ current Git repository.
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
diff --git a/website/src/commands/config.md b/website/src/commands/config.md
index 2098ed36d..80bcf1a7e 100644
--- a/website/src/commands/config.md
+++ b/website/src/commands/config.md
@@ -1,10 +1,9 @@
 # git town config
 
+<a type="git-town-command" />
+
 ```command-summary
-git town config [-v | --verbose]
-git town config get-parent [<branch-name>] [-v | --verbose]
-git town config remove [-v | --verbose]
-git town init [-v | --verbose]
+git town config [(-d | --display-types) <type>] [-h | --help] [-v | --verbose]
 ```
 
 The _config_ command displays and updates the local Git Town configuration.
@@ -21,6 +20,16 @@ Running without a subcommand shows the current Git Town configuration.
 
 ## Options
 
+#### `-d <branch-types>`<br>`--display-types <branch-types>`
+
+This flag allows customizing whether Git Town also displays the branch type in
+addition to the branch name when showing a list of branches. More info
+[here](../preferences/display-types.md#cli-flags).
+
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
diff --git a/website/src/commands/continue.md b/website/src/commands/continue.md
index 48ff56fda..47e964d49 100644
--- a/website/src/commands/continue.md
+++ b/website/src/commands/continue.md
@@ -1,7 +1,9 @@
 # git town continue
 
+<a type="git-town-command" />
+
 ```command-summary
-git town continue [-v | --verbose]
+git town continue [-h | --help] [-v | --verbose]
 ```
 
 When a Git Town command encounters a problem that it cannot resolve, for example
@@ -12,6 +14,10 @@ and execute all remaining operations of the original command.
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -19,7 +25,11 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [skip](skip.md) ignores all remaining merge conflicts on the current branch
   and then continues the currently suspended Git Town command
 - [undo](undo.md) aborts the currently suspended Git Town command and returns
   the repository to the state it was in before you ran that command
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/contribute.md b/website/src/commands/contribute.md
index de98eb7ff..fbdbd028a 100644
--- a/website/src/commands/contribute.md
+++ b/website/src/commands/contribute.md
@@ -1,7 +1,9 @@
 # git town contribute
 
+<a type="git-town-command" />
+
 ```command-summary
-git town contribute [<branch-name>...] [-v | --verbose]
+git town contribute [<branch-name>...] [-h | --help] [-v | --verbose]
 ```
 
 The _contribute_ command makes some of your branches
@@ -40,6 +42,10 @@ git town contribute somebody-elses-branch
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -47,7 +53,11 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [feature](feature.md) makes the chosen branches feature branches
 - [observe](observe.md) makes the chosen branches observed
-- [prototype](prototype.md) makes the chosen branches prototype branches
 - [park](park.md) makes the chosen branches parked
+- [prototype](prototype.md) makes the chosen branches prototype branches
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/delete.md b/website/src/commands/delete.md
index 108a12735..702c8935e 100644
--- a/website/src/commands/delete.md
+++ b/website/src/commands/delete.md
@@ -1,7 +1,9 @@
 # git town delete
 
+<a type="git-town-command" />
+
 ```command-summary
-git town delete [<branch-name>...] [-v | --verbose]
+git town delete [<branch-name>...] [--dry-run] [-h | --help] [-v | --verbose]
 ```
 
 The _delete_ command deletes the given branch from the local and if possible the
@@ -59,6 +61,10 @@ When called with a branch name, it deletes the given branch.
 Use the `--dry-run` flag to test-drive this command. It prints the Git commands
 that would be run but doesn't execute them.
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
diff --git a/website/src/commands/detach.md b/website/src/commands/detach.md
index b1b521cf8..e2726ad10 100644
--- a/website/src/commands/detach.md
+++ b/website/src/commands/detach.md
@@ -1,7 +1,9 @@
 # git town detach
 
+<a type="git-town-command" />
+
 ```command-summary
-git town detach [--dry-run] [-v | --verbose]
+git town detach [--dry-run] [-h | --help] [-v | --verbose]
 ```
 
 The _detach_ command removes the current branch from the stack it is in and
@@ -49,6 +51,10 @@ command, and remove merge commits by [compressing](compress.md).
 Use the `--dry-run` flag to test-drive this command. It prints the Git commands
 that would be run but doesn't execute them.
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -56,6 +62,10 @@ determine the repository state.
 
 ## See also
 
-- [swap](swap.md) move the current branch up in the stack
+<!-- keep-sorted start -->
+
 - [set-parent](set-parent.md) move the current branch and its descendents under
   a different parent
+- [swap](swap.md) move the current branch up in the stack
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/diff-parent.md b/website/src/commands/diff-parent.md
index 282889afe..cf66c1f81 100644
--- a/website/src/commands/diff-parent.md
+++ b/website/src/commands/diff-parent.md
@@ -1,7 +1,9 @@
 # git town diff-parent
 
+<a type="git-town-command" />
+
 ```command-summary
-git town diff-parent [-v | --verbose]
+git town diff-parent [-h | --help] [-v | --verbose]
 ```
 
 The _diff-parent_ command displays the changes made on a feature branch, i.e.
@@ -9,6 +11,10 @@ the diff between the current branch and its parent branch.
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
diff --git a/website/src/commands/down.md b/website/src/commands/down.md
index dbd3de609..b157394d6 100644
--- a/website/src/commands/down.md
+++ b/website/src/commands/down.md
@@ -1,7 +1,9 @@
 # git town down
 
+<a type="git-town-command" />
+
 ```command-summary
-git town down [-m | --merge] [-v | --verbose]
+git town down [(-d | --display-types) <type>] [-h | --help] [-m | --merge] [(-o | --order) <asc|desc>] [-v | --verbose]
 ```
 
 The _down_ command moves one position down in the current stack by switching to
@@ -36,12 +38,16 @@ main
 
 ## Options
 
-#### `-d`<br>`--display-types`
+#### `-d <branch-types>`<br>`--display-types <branch-types>`
 
 This flag allows customizing whether Git Town also displays the branch type in
 addition to the branch name when showing a list of branches. More info
 [here](../preferences/display-types.md#cli-flags).
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-m`<br>`--merge`
 
 The `--merge` aka `-m` flag has the same effect as the
@@ -52,7 +58,7 @@ branch.
 This is useful when you have uncommitted changes in your current branch and want
 to move them down to the parent branch.
 
-#### `-o`<br>`--order`
+#### `-o <asc|desc>`<br>`--order <asc|desc>`
 
 The `--order` flag allows customizing the order in which branches get displayed.
 More info [here](../preferences/order.md#cli-flag)
@@ -64,8 +70,12 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [branch](branch.md) displays the branch hierarchy
-- [switch](switch.md) interactively switch between branches
 - [swap](swap.md) changes the stack by swapping the position of current branch
   with its parent
+- [switch](switch.md) interactively switch between branches
 - [up](up.md) moves one position up in the current stack
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/feature.md b/website/src/commands/feature.md
index d0e60da13..b9b736464 100644
--- a/website/src/commands/feature.md
+++ b/website/src/commands/feature.md
@@ -1,7 +1,9 @@
 # git town feature
 
+<a type="git-town-command" />
+
 ```command-summary
-git town feature [<branch-name>...] [-v | --verbose]
+git town feature [<branch-name>...] [-h | --help] [-v | --verbose]
 ```
 
 The _feature_ command makes some of your branches
@@ -31,6 +33,10 @@ git town feature somebody-elses-branch
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -38,7 +44,11 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [contribute](contribute.md) makes the chosen branches contribution branches
 - [observe](observe.md) makes the chosen branches feature branches
-- [prototype](prototype.md) makes the chosen branches prototype branches
 - [park](park.md) makes the chosen branches parked
+- [prototype](prototype.md) makes the chosen branches prototype branches
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/hack.md b/website/src/commands/hack.md
index b5b07ebd6..df673e77a 100644
--- a/website/src/commands/hack.md
+++ b/website/src/commands/hack.md
@@ -1,7 +1,9 @@
 # git town hack
 
+<a type="git-town-command" />
+
 ```command-summary
-git town hack [<branch-name>...] [-p | --prototype] [-d | --detached] [-c | --commit] [-m | --message <message>] [--propose] [--dry-run] [-v | --verbose]
+git town hack [<branch-name>...] [--auto-resolve] [-b | --beam] [-c | --commit] [-d | --detached] [--dry-run] [-h | --help] [(-m | --message) <message>] [--propose] [-p | --prototype] [--stash] [--sync] [-v | --verbose]
 ```
 
 The _hack_ command ("let's start hacking") creates a new feature branch with the
@@ -45,10 +47,25 @@ branch with its upstream counterpart. You can control this behavior with the
 
 ## Options
 
-#### `-p`<br>`--prototype`
+#### `--auto-resolve`
 
-Adding the `--prototype` aka `-p` switch creates a
-[prototype branch](../branch-types.md#prototype-branches).
+Disables automatic resolution of
+[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
+
+#### `-b`<br>`--beam`
+
+Moves ("beams") one or more commits from the current branch to the new feature
+branch that gets created. Lets you select the commits to beam via a visual
+dialog. Beaming suppresses all branch updates. Any merge conflicts encountered
+while beaming arise from moving the beamed commits.
+
+#### `-c`<br>`--commit`
+
+When given, commits the currently staged changes into the branch to create and
+remains on the current branch. This is intended to quickly commit changes
+unrelated to the current branch into another branch and keep hacking on the
+current branch. Committing suppresses all branch updates to allow you to get
+your open changes committed.
 
 #### `-d`<br>`--detached`<br>`--no-detached`
 
@@ -61,22 +78,16 @@ In detached mode, feature branches don't receive updates from the perennial
 branch at the root of your branch hierarchy. This can be useful in busy
 monorepos.
 
-#### `-c`<br>`--commit`
+#### `--dry-run`
 
-When given, commits the currently staged changes into the branch to create and
-remains on the current branch. This is intended to quickly commit changes
-unrelated to the current branch into another branch and keep hacking on the
-current branch. Committing suppresses all branch updates to allow you to get
-your open changes committed.
+Use the `--dry-run` flag to test-drive this command. It prints the Git commands
+that would be run but doesn't execute them.
 
-#### `-b`<br>`--beam`
+#### `-h`<br>`--help`
 
-Moves ("beams") one or more commits from the current branch to the new feature
-branch that gets created. Lets you select the commits to beam via a visual
-dialog. Beaming suppresses all branch updates. Any merge conflicts encountered
-while beaming arise from moving the beamed commits.
+Display help for this command.
 
-#### `-m`<br>`--message`
+#### `-m <text>`<br>`--message <text>`
 
 Commit message to use together with `--commit`. Implies `--commit`.
 
@@ -87,6 +98,11 @@ Propose the created branch.
 To always propose new branches, set the
 [share new branches](../preferences/share-new-branches.md) setting to `propose`.
 
+#### `-p`<br>`--prototype`
+
+Adding the `--prototype` aka `-p` switch creates a
+[prototype branch](../branch-types.md#prototype-branches).
+
 #### `--stash`<br>`--no-stash`
 
 Enables or disables [stashing](../preferences/stash.md) for this invocation.
@@ -96,21 +112,11 @@ Enables or disables [stashing](../preferences/stash.md) for this invocation.
 Enables or disables [automatic syncing](../preferences/auto-sync.md) before
 creating the new branch.
 
-#### `--dry-run`
-
-Use the `--dry-run` flag to test-drive this command. It prints the Git commands
-that would be run but doesn't execute them.
-
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
 determine the repository state.
 
-#### `--auto-resolve`
-
-Disables automatic resolution of
-[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
-
 ## Configuration
 
 If the configuration setting
@@ -125,5 +131,9 @@ create the remote tracking branch.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [append](append.md) creates the new branch as a child of the current branch
 - [prepend](prepend.md) creates the new branch as a parent of the current branch
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/help.md b/website/src/commands/help.md
index 8ebf6ecf2..50555ef71 100644
--- a/website/src/commands/help.md
+++ b/website/src/commands/help.md
@@ -1,7 +1,15 @@
 # git town help
 
+<a type="git-town-command" />
+
 ```command-summary
-git town help
+git town help [-h | --help]
 ```
 
 The _help_ command displays detailed information about each Git Town command.
+
+## Options
+
+#### `-h`<br>`--help`
+
+Display help for this command.
diff --git a/website/src/commands/init.md b/website/src/commands/init.md
index 52fe134f3..83eef2a75 100644
--- a/website/src/commands/init.md
+++ b/website/src/commands/init.md
@@ -1,7 +1,9 @@
 # git town init
 
+<a type="git-town-command" />
+
 ```command-summary
-git town init [-v | --verbose]
+git town init [-h | --help] [-v | --verbose]
 ```
 
 The _init_ command launches Git Town's setup assistant. The setup assistant
@@ -10,6 +12,10 @@ to adjust them.
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
diff --git a/website/src/commands/merge.md b/website/src/commands/merge.md
index 93c514a20..22318c5e9 100644
--- a/website/src/commands/merge.md
+++ b/website/src/commands/merge.md
@@ -1,7 +1,9 @@
 # git town merge
 
+<a type="git-town-command" />
+
 ```command-summary
-git town merge [--dry-run] [-v | --verbose]
+git town merge [--dry-run] [-h | --help] [-v | --verbose]
 ```
 
 The _merge_ command merges the current branch into the branch ahead of it in the
@@ -54,6 +56,10 @@ consolidate these commits, run [git town sync](sync.md).
 Use the `--dry-run` flag to test-drive this command. It prints the Git commands
 that would be run but doesn't execute them.
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
diff --git a/website/src/commands/observe.md b/website/src/commands/observe.md
index 030a0c9e9..cb270ac91 100644
--- a/website/src/commands/observe.md
+++ b/website/src/commands/observe.md
@@ -1,7 +1,9 @@
 # git town observe
 
+<a type="git-town-command" />
+
 ```command-summary
-git town observe [<branch-name>...] [-v | --verbose]
+git town observe [<branch-name>...] [-h | --help] [-v | --verbose]
 ```
 
 The _observe_ command makes some of your branches
@@ -34,6 +36,10 @@ git town observe somebody-elses-branch
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -41,7 +47,11 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [contribute](contribute.md) makes the chosen branches contribution branches
 - [feature](feature.md) makes the chosen branches feature branches
-- [prototype](prototype.md) makes the chosen branches prototype branches
 - [park](park.md) makes the chosen branches parked
+- [prototype](prototype.md) makes the chosen branches prototype branches
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/offline.md b/website/src/commands/offline.md
index dc0bb82d5..edd5af622 100644
--- a/website/src/commands/offline.md
+++ b/website/src/commands/offline.md
@@ -1,7 +1,9 @@
 # git town offline
 
+<a type="git-town-command" />
+
 ```command-summary
-git town offline [<status>] [-v | --verbose]
+git town offline [<status>] [-h | --help] [-v | --verbose]
 ```
 
 The _offline_ configuration command displays or changes Git Town's offline mode.
@@ -17,6 +19,10 @@ When called with `no`, `0`, `off`, or `false`, it disables offline mode.
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
diff --git a/website/src/commands/park.md b/website/src/commands/park.md
index c2710f3f0..9c9e23284 100644
--- a/website/src/commands/park.md
+++ b/website/src/commands/park.md
@@ -1,7 +1,9 @@
 # git town park
 
+<a type="git-town-command" />
+
 ```command-summary
-git town park [<branch-name>...] [-v | --verbose]
+git town park [<branch-name>...] [-h | --help] [-v | --verbose]
 ```
 
 The _park_ command [parks](../branch-types.md#parked-branches) some of your
@@ -26,6 +28,10 @@ git town park alpha beta
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -33,7 +39,11 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [contribute](contribute.md) makes the chosen branches contribution branches
 - [feature](feature.md) makes the chosen branches feature branches
 - [observe](observe.md) makes the chosen branches observed
 - [prototype](prototype.md) makes the chosen branches prototype branches
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/prepend.md b/website/src/commands/prepend.md
index 9f4f1b174..20b52e965 100644
--- a/website/src/commands/prepend.md
+++ b/website/src/commands/prepend.md
@@ -1,7 +1,9 @@
 # git town prepend
 
+<a type="git-town-command" />
+
 ```command-summary
-git town prepend [<branch-name>...] [-b | --beam] [--body <string>] [--propose] [-p | --prototype] [-t <text> | --title <text>] [-d | --detached] [-c | --commit] [-m | --message <message>] [--dry-run] [-v | --verbose]
+git town prepend [<branch-name>...] [--auto-resolve] [-b | --beam] [--body <string>] [-c | --commit] [-d | --detached] [--dry-run] [-h | --help] [(-m | --message) <message>] [--propose] [-p | --prototype] [--push] [--stash] [--sync] [(-t | --title) <text>] [-v | --verbose]
 ```
 
 The _prepend_ command creates a new feature branch as the parent of the current
@@ -38,27 +40,30 @@ it. To do so, it pushes the new branch.
 
 ## Options
 
-#### `--body <string>`
-
-Pre-populate the body of the pull request to create with the given text.
-Requires `--propose`.
+#### `--auto-resolve`
 
-#### `--propose`
+Disables automatic resolution of
+[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
 
-Propose the created branch.
+#### `-b`<br>`--beam`
 
-To always propose new branches, set the
-[share new branches](../preferences/share-new-branches.md) setting to `propose`.
+Moves ("beams") one or more commits from the current branch to the new parent
+branch that gets created. Lets you select the commits to beam via a visual
+dialog. Beaming suppresses all branch updates. Any merge conflicts encountered
+while beaming arise from moving the beamed commits.
 
-#### `-p`<br>`--prototype`
+#### `--body <string>`
 
-Adding the `--prototype` aka `-p` switch creates a
-[prototype branch](../branch-types.md#prototype-branches).
+Pre-populate the body of the pull request to create with the given text.
+Requires `--propose`.
 
-#### `-t <text>`<br>`--title <text>`
+#### `-c`<br>`--commit`
 
-Pre-populate the title of the pull request to create with the given text.
-Requires `--propose`.
+When given, commits the currently staged changes into the branch to create and
+remains on the current branch. This is intended to quickly commit changes
+unrelated to the current branch into another branch and keep hacking on the
+current branch. Committing suppresses all branch updates to allow you to get
+your open changes committed.
 
 #### `-d`<br>`--detached`<br>`--no-detached`
 
@@ -71,25 +76,31 @@ In detached mode, feature branches don't receive updates from the perennial
 branch at the root of your branch hierarchy. This can be useful in busy
 monorepos.
 
-#### `-c`<br>`--commit`
+#### `--dry-run`
 
-When given, commits the currently staged changes into the branch to create and
-remains on the current branch. This is intended to quickly commit changes
-unrelated to the current branch into another branch and keep hacking on the
-current branch. Committing suppresses all branch updates to allow you to get
-your open changes committed.
+Use the `--dry-run` flag to test-drive this command. It prints the Git commands
+that would be run but doesn't execute them.
 
-#### `-b`<br>`--beam`
+#### `-h`<br>`--help`
 
-Moves ("beams") one or more commits from the current branch to the new parent
-branch that gets created. Lets you select the commits to beam via a visual
-dialog. Beaming suppresses all branch updates. Any merge conflicts encountered
-while beaming arise from moving the beamed commits.
+Display help for this command.
 
-#### `-m`<br>`--message`
+#### `-m <text>`<br>`--message <text>`
 
 Commit message to use together with `--commit`. Implies `--commit`.
 
+#### `--propose`
+
+Propose the created branch.
+
+To always propose new branches, set the
+[share new branches](../preferences/share-new-branches.md) setting to `propose`.
+
+#### `-p`<br>`--prototype`
+
+Adding the `--prototype` aka `-p` switch creates a
+[prototype branch](../branch-types.md#prototype-branches).
+
 #### `--push`<br>`--no-push`
 
 The `--push`/`--no-push` argument overrides the
@@ -104,21 +115,16 @@ Enables or disables [stashing](../preferences/stash.md) for this invocation.
 Enables or disables [automatic syncing](../preferences/auto-sync.md) of the
 current branch before prepending the new one.
 
-#### `--dry-run`
+#### `-t <text>`<br>`--title <text>`
 
-Use the `--dry-run` flag to test-drive this command. It prints the Git commands
-that would be run but doesn't execute them.
+Pre-populate the title of the pull request to create with the given text.
+Requires `--propose`.
 
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
 determine the repository state.
 
-#### `--auto-resolve`
-
-Disables automatic resolution of
-[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
-
 ## Configuration
 
 If [share-new-branches](../preferences/share-new-branches.md) is configured,
@@ -132,6 +138,10 @@ creates a branch with the given [type](../branch-types.md).
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [append](append.md) creates the new branch as a child of the current branch
 - [hack](hack.md) creates the new branch as a child of the
   [main branch](../preferences/main-branch.md)
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/propose.md b/website/src/commands/propose.md
index b6e4ada89..e92ba8541 100644
--- a/website/src/commands/propose.md
+++ b/website/src/commands/propose.md
@@ -1,7 +1,9 @@
 # git town propose
 
+<a type="git-town-command" />
+
 ```command-summary
-git town propose [-b <text> | --body <text>] [-f <path> | --body-file <path>] [-t <text> | --title <text>] [-d | --detached] [--dry-run] [-v | --verbose]
+git town propose [--auto-resolve] [(-b | --body) <text>] [(-f | --body-file) <path>] [--dry-run] [-h | --help] [-s | --stack] [(-t | --title) <text>] [-v | --verbose]
 ```
 
 The _propose_ command helps create a new pull request (also known as merge
@@ -25,6 +27,11 @@ You can configure the browser which Git Town opens using the
 
 ## Options
 
+#### `--auto-resolve`
+
+Disables automatic resolution of
+[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
+
 #### `-b <text>`<br>`--body <text>`
 
 Pre-populate the body of the pull request with the given text.
@@ -35,31 +42,30 @@ When called with the `--body-file` aka `-f` flag, it pre-populates the body of
 the pull request with the content of the given file. The filename `-` reads the
 body text from STDIN.
 
-#### `-t <text>`<br>`--title <text>`
+#### `--dry-run`
 
-When called with the `--title <title>` aka `-t` flag, the _propose_ command
-pre-populate the title of the pull request to the given text.
+Use the `--dry-run` flag to test-drive this command. It prints the Git commands
+that would be run but doesn't execute them.
+
+#### `-h`<br>`--help`
+
+Display help for this command.
 
 #### `-s`<br>`--stack`
 
 The `--stack` aka `-s` parameter makes Git Town propose all branches in the
 stack that the current branch belongs to.
 
-#### `--dry-run`
+#### `-t <text>`<br>`--title <text>`
 
-Use the `--dry-run` flag to test-drive this command. It prints the Git commands
-that would be run but doesn't execute them.
+When called with the `--title <title>` aka `-t` flag, the _propose_ command
+pre-populate the title of the pull request to the given text.
 
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
 determine the repository state.
 
-#### `--auto-resolve`
-
-Disables automatic resolution of
-[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
-
 ## Configuration
 
 You can configure the forge type with the
@@ -70,5 +76,9 @@ When using SSH identities, this command uses the hostname in the
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [repo](repo.md) opens the website for the repository in the browser
 - [ship](ship.md) ships the current branch
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/prototype.md b/website/src/commands/prototype.md
index fb3077316..2979d541d 100644
--- a/website/src/commands/prototype.md
+++ b/website/src/commands/prototype.md
@@ -1,7 +1,9 @@
 # git town prototype
 
+<a type="git-town-command" />
+
 ```command-summary
-git town prototype [<branch-name>...] [-v | --verbose]
+git town prototype [<branch-name>...] [-h | --help] [-v | --verbose]
 ```
 
 The _prototype_ command marks some of your branches as
@@ -26,6 +28,10 @@ git town prototype alpha beta
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -33,7 +39,11 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [contribute](contribute.md) makes the chosen branches contribution branches
 - [feature](feature.md) makes the chosen branches feature branches
 - [observe](observe.md) makes the chosen branches observed
 - [park](park.md) makes the chosen branches parked
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/rename.md b/website/src/commands/rename.md
index 895a00256..31fd0c712 100644
--- a/website/src/commands/rename.md
+++ b/website/src/commands/rename.md
@@ -1,7 +1,9 @@
 # git town rename
 
+<a type="git-town-command" />
+
 ```command-summary
-git town rename [<old-name>] <new-name> [-f | --force] [--dry-run] [-v | --verbose]
+git town rename [<old-name>] <new-name> [--dry-run] [-f | --force] [-h | --help] [-v | --verbose]
 ```
 
 The _rename_ command renames the current branch and its tracking branch. The
@@ -24,15 +26,19 @@ given name.
 
 ## Options
 
+#### `--dry-run`
+
+Use the `--dry-run` flag to test-drive this command. It prints the Git commands
+that would be run but doesn't execute them.
+
 #### `-f`<br>`--force`
 
 Renaming perennial branches requires confirmation with the `--force` aka `-f`
 flag.
 
-#### `--dry-run`
+#### `-h`<br>`--help`
 
-Use the `--dry-run` flag to test-drive this command. It prints the Git commands
-that would be run but doesn't execute them.
+Display help for this command.
 
 #### `-v`<br>`--verbose`
 
diff --git a/website/src/commands/repo.md b/website/src/commands/repo.md
index c2a8a07c3..72a52abfb 100644
--- a/website/src/commands/repo.md
+++ b/website/src/commands/repo.md
@@ -1,7 +1,9 @@
 # git town repo
 
+<a type="git-town-command" />
+
 ```command-summary
-git town repo [<remote-name>] [-v | --verbose]
+git town repo [<remote-name>] [-h | --help] [-v | --verbose]
 ```
 
 The _repo_ command ("show me the repository") opens the homepage of the current
@@ -23,6 +25,10 @@ given name.
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -39,5 +45,9 @@ setting to tell Git Town about the hostname when using ssh identities.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [propose](propose.md) opens the website displaying the pull request for the
   current branch
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/runlog.md b/website/src/commands/runlog.md
index 3707e197a..604459aa4 100644
--- a/website/src/commands/runlog.md
+++ b/website/src/commands/runlog.md
@@ -1,7 +1,9 @@
 # git town runlog
 
+<a type="git-town-command" />
+
 ```command-summary
-git town runlog [-v | --verbose]
+git town runlog [-h | --help] [-v | --verbose]
 ```
 
 Git Town records the SHA of all local and remote branches before and after each
@@ -13,6 +15,10 @@ command made.
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -20,5 +26,9 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [status show](status-show.md) displays the runstate, i.e. detailed information
   for the current or last Git Town command
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/set-parent.md b/website/src/commands/set-parent.md
index 15ba1b0cb..d95ea0f53 100644
--- a/website/src/commands/set-parent.md
+++ b/website/src/commands/set-parent.md
@@ -1,7 +1,9 @@
 # git town set-parent
 
+<a type="git-town-command" />
+
 ```command-summary
-git town set-parent [<branch>] [-v | --verbose]
+git town set-parent [<branch>] [--auto-resolve] [-h | --help] [--none] [-v | --verbose]
 ```
 
 The _set-parent_ command moves a branch and all its children below another
@@ -80,6 +82,15 @@ parent.
 
 ## Options
 
+#### `--auto-resolve`
+
+Disables automatic resolution of
+[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
+
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `--none`
 
 The `--none` option assigns no parent (removes the assigned parent), making the
@@ -90,13 +101,12 @@ branch a perennial branch.
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
 determine the repository state.
 
-#### `--auto-resolve`
-
-Disables automatic resolution of
-[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
-
 ## See also
 
+<!-- keep-sorted start -->
+
 - [detach](detach.md) extract the current branch from a stack, leaving its
   children in the stack.
 - [swap](swap.md) move the current branch up in the stack
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/ship.md b/website/src/commands/ship.md
index e095d586e..f079f3a6f 100644
--- a/website/src/commands/ship.md
+++ b/website/src/commands/ship.md
@@ -1,7 +1,9 @@
 # git town ship
 
+<a type="git-town-command" />
+
 ```command-summary
-git town ship [<branch-name>] [-m <text> | --message <text>] [-s <name> | --strategy <name>] [-p | --to-parent] [--dry-run] [-v | --verbose]
+git town ship [<branch-name>] [--dry-run] [-h | --help] [(-m | --message) <text>] [(-f | --message-file) <path>] [(-s | --strategy) <name>] [-p | --to-parent] [-v | --verbose]
 ```
 
 _Notice: Most people don't need to use this command. The recommended way to
@@ -27,6 +29,15 @@ When called with a positional argument, it ships the branch with the given name.
 
 ## Options
 
+#### `--dry-run`
+
+Use the `--dry-run` flag to test-drive this command. It prints the Git commands
+that would be run but doesn't execute them.
+
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-m <text>`<br>`--message <text>`
 
 Similar to `git commit`, the `--message <message>` aka `-m` parameter allows
@@ -48,11 +59,6 @@ child branch, you need to first ship or [delete](delete.md) all its ancestor
 branches. If you really want to ship into a non-perennial branch, you can
 override the protection against that with the `--to-parent` aka `-p` option.
 
-#### `--dry-run`
-
-Use the `--dry-run` flag to test-drive this command. It prints the Git commands
-that would be run but doesn't execute them.
-
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -81,6 +87,10 @@ you can
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [propose](propose.md) creates a pull request for the current branch
 - [repo](repo.md) opens the website of your forge in the browser, so that you
   can ship branches there
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/skip.md b/website/src/commands/skip.md
index 94aee2355..574cf8183 100644
--- a/website/src/commands/skip.md
+++ b/website/src/commands/skip.md
@@ -1,7 +1,9 @@
 # git town skip
 
+<a type="git-town-command" />
+
 ```command-summary
-git town skip [-v | --verbose]
+git town skip [-h | --help] [-v | --verbose]
 ```
 
 The _skip_ command allows to skip a Git branch with merge conflicts when syncing
@@ -9,6 +11,10 @@ all feature branches.
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -16,8 +22,12 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [continue](continue.md) tries to continue the currently suspended Git Town
   command by re-running the Git command that failed.
 - [undo](undo.md) aborts the currently suspended Git Town command and undoes all
   the changes it did so far, leaving your repository in the same state it was in
   before you started the failing Git Town command
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/status-reset.md b/website/src/commands/status-reset.md
index 48a42df11..19c2fb7e3 100644
--- a/website/src/commands/status-reset.md
+++ b/website/src/commands/status-reset.md
@@ -1,7 +1,9 @@
 # git town status reset
 
+<a type="git-town-command" />
+
 ```command-summary
-git town status reset [-v | --verbose]
+git town status reset [-h | --help] [-v | --verbose]
 ```
 
 The _status reset_ command deletes the persisted runstate. This is only needed
@@ -9,6 +11,10 @@ if the runstate is corrupted and causes Git Town to crash.
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -16,5 +22,9 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [status show](status-show.md) displays the runstate that this command would
   delete
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/status-show.md b/website/src/commands/status-show.md
index dd1072fe9..704c9dc2a 100644
--- a/website/src/commands/status-show.md
+++ b/website/src/commands/status-show.md
@@ -1,7 +1,9 @@
 # git town status show
 
+<a type="git-town-command" />
+
 ```command-summary
-git town status show [-v | --verbose]
+git town status show [-h | --help] [-v | --verbose]
 ```
 
 The _status show_ command displays Git Town's runstate, i.e. detailed
@@ -10,6 +12,10 @@ command, including its path on the filesystem.
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -17,7 +23,11 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [runlog](runlog.md) displays an overview of the most recently executed Git
   Town commands
 - [status reset](status-reset.md) deletes the runstate. This can solve errors
   after upgrading Git Town.
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/status.md b/website/src/commands/status.md
index 8dedac956..d24cfdccc 100644
--- a/website/src/commands/status.md
+++ b/website/src/commands/status.md
@@ -1,8 +1,9 @@
 # git town status
 
+<a type="git-town-command" />
+
 ```command-summary
-git town status [-p | --pending] [-v | --verbose]
-git town status reset [-v | --verbose]
+git town status [-h | --help] [-p | --pending] [-v | --verbose]
 ```
 
 The _status_ command indicates whether Git Town has encountered a merge conflict
@@ -18,6 +19,10 @@ persisted runstate.
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-p`<br>`--pending`
 
 The `--pending` aka `-p` argument causes this command to output only the name of
diff --git a/website/src/commands/swap.md b/website/src/commands/swap.md
index 6ed44e698..360faaf85 100644
--- a/website/src/commands/swap.md
+++ b/website/src/commands/swap.md
@@ -1,7 +1,9 @@
 # git town swap
 
+<a type="git-town-command" />
+
 ```command-summary
-git town swap [--dry-run] [-v | --verbose]
+git town swap [--auto-resolve] [--dry-run] [-h | --help] [-v | --verbose]
 ```
 
 The _swap_ command switches the position of the current branch with the branch
@@ -47,24 +49,32 @@ be owned by you, i.e. you cannot swap
 
 ## Options
 
+#### `--auto-resolve`
+
+Disables automatic resolution of
+[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
+
 #### `--dry-run`
 
 Use the `--dry-run` flag to test-drive this command. It prints the Git commands
 that would be run but doesn't execute them.
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
 determine the repository state.
 
-#### `--auto-resolve`
-
-Disables automatic resolution of
-[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
-
 ## See also
 
-- [set-parent](set-parent.md) moves the current branch and its descendents under
-  a different parent
+<!-- keep-sorted start -->
+
 - [detach](detach.md) extracts the current branch from a stack, leaving its
   children in the stack.
+- [set-parent](set-parent.md) moves the current branch and its descendents under
+  a different parent
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/switch.md b/website/src/commands/switch.md
index 0442cd1ed..f8d4eca86 100644
--- a/website/src/commands/switch.md
+++ b/website/src/commands/switch.md
@@ -1,7 +1,9 @@
 # git town switch
 
+<a type="git-town-command" />
+
 ```command-summary
-git town switch [<branch-name-regex>...] [-a | --all] [-d | --display-types] [-m | --merge] [-t <name> | --type <name>] [-v | --verbose]
+git town switch [<branch-name-regex>...] [-a | --all] [(-d | --display-types) <type>] [-h | --help] [-m | --merge] [(-o | --order) <asc|desc>] [--stash] [(-t | --type) <name>] [-v | --verbose]
 ```
 
 The _switch_ command displays the branch hierarchy on your machine and allows
@@ -37,12 +39,16 @@ git town switch me- main
 
 The `--all` aka `-a` flag also displays both local and remote branches.
 
-#### `-d`<br>`--display-types`
+#### `-d <branch-types>`<br>`--display-types <branch-types>`
 
 This flag allows customizing whether Git Town also displays the branch type in
 addition to the branch name when showing a list of branches. More info
 [here](../preferences/display-types.md#cli-flags).
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-m`<br>`--merge`
 
 The `--merge` aka `-m` flag has the same effect as the
@@ -53,11 +59,17 @@ branch.
 This is useful when you have uncommitted changes in your current branch and want
 to move them to the new branch.
 
-#### `-o`<br>`--order`
+#### `-o <asc|desc>`<br>`--order <asc|desc>`
 
 The `--order` flag allows customizing the order in which branches get displayed.
 More info [here](../preferences/order.md#cli-flag)
 
+#### `--stash`
+
+When set, Git Town stashes the open changes before switching to the new branch
+and then unstashes them. This guarantees that the switch will work, at the cost
+of resetting any stashed changes.
+
 #### `-t <name>`<br>`--type <name>`
 
 The `--type` aka `-t` flag reduces the list of branches to those that have the
@@ -101,6 +113,10 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [branch](branch.md) displays the branch hierarchy
 - [walk](walk.md) executes a shell command or opens a shell in each of your
   local branches
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/sync.md b/website/src/commands/sync.md
index 6d8456f8c..c5ae57137 100644
--- a/website/src/commands/sync.md
+++ b/website/src/commands/sync.md
@@ -1,7 +1,9 @@
 # git town sync
 
+<a type="git-town-command" />
+
 ```command-summary
-git town sync [-a | --all] [--no-push] [-s | --stack] [-d | --detached] [-p | --prune] [--dry-run] [-v | --verbose]
+git town sync [-a | --all] [--auto-resolve] [-d | --detached] [--dry-run] [-h | --help] [-p | --prune] [--push] [-s | --stack] [-v | --verbose]
 ```
 
 The _sync_ command ("synchronize this branch") updates your local Git workspace
@@ -74,15 +76,10 @@ if it's going to be deleted right afterwards.
 By default this command syncs only the current branch. The `--all` aka `-a`
 parameter makes Git Town sync all local branches.
 
-#### `--push`<br>`--no-push`
-
-The `--push`/`--no-push` argument overrides the
-[push-branches](../preferences/push-branches.md) config setting.
-
-#### `-s`<br>`--stack`
+#### `--auto-resolve`
 
-The `--stack` aka `-s` parameter makes Git Town sync all branches in the stack
-that the current branch belongs to.
+Disables automatic resolution of
+[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
 
 #### `-d`<br>`--detached`<br>`--no-detached`
 
@@ -95,26 +92,35 @@ In detached mode, feature branches don't receive updates from the perennial
 branch at the root of your branch hierarchy. This can be useful in busy
 monorepos.
 
+#### `--dry-run`
+
+Use the `--dry-run` flag to test-drive this command. It prints the Git commands
+that would be run but doesn't execute them.
+
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-p`<br>`--prune`
 
 The `--prune` aka `-p` flag removes (prunes) empty branches, i.e. branches that
 effectively don't make any changes.
 
-#### `--dry-run`
+#### `--push`<br>`--no-push`
 
-Use the `--dry-run` flag to test-drive this command. It prints the Git commands
-that would be run but doesn't execute them.
+The `--push`/`--no-push` argument overrides the
+[push-branches](../preferences/push-branches.md) config setting.
+
+#### `-s`<br>`--stack`
+
+The `--stack` aka `-s` parameter makes Git Town sync all branches in the stack
+that the current branch belongs to.
 
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
 determine the repository state.
 
-#### `--auto-resolve`
-
-Disables automatic resolution of
-[phantom merge conflicts](../stacked-changes.md#avoid-phantom-conflicts).
-
 ## Configuration
 
 [sync-perennial-strategy](../preferences/sync-perennial-strategy.md) configures
@@ -135,6 +141,8 @@ tags with the [development remote](../preferences/dev-remote.md).
 
 When you run into merge conflicts:
 
+<!-- keep-sorted start -->
+
 - [continue](continue.md) allows you to resume the suspended Git Town command
   after you have resolved the merge conflicts by re-running the failed Git
   command
@@ -143,3 +151,5 @@ When you run into merge conflicts:
 - [undo](undo.md) aborts the currently suspended Git Town command and undoes all
   the changes it did, bringing your Git repository back to the state it was
   before you ran the currently suspended Git Town command
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/undo.md b/website/src/commands/undo.md
index 0d5c3bdee..dc9c3bc6f 100644
--- a/website/src/commands/undo.md
+++ b/website/src/commands/undo.md
@@ -1,7 +1,9 @@
 # git town undo
 
+<a type="git-town-command" />
+
 ```command-summary
-git town undo [-v | --verbose]
+git town undo [-h | --help] [-v | --verbose]
 ```
 
 The _undo_ command reverts the last fully executed Git Town command. It performs
@@ -10,6 +12,10 @@ the state it was before you ran the problematic command.
 
 ## Options
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-v`<br>`--verbose`
 
 The `--verbose` aka `-v` flag prints all Git commands run under the hood to
@@ -17,7 +23,11 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [continue](continue.md) continues the currently suspended Git Town command
   after you have resolved the conflicting changes
 - [skip](skip.md) ignores all remaining merge conflicts on the current branch
   and then continues the currently suspended Git Town command
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/up.md b/website/src/commands/up.md
index 62baa1571..00917af0d 100644
--- a/website/src/commands/up.md
+++ b/website/src/commands/up.md
@@ -1,7 +1,9 @@
 # git town up
 
+<a type="git-town-command" />
+
 ```command-summary
-git town up [-m | --merge] [-v | --verbose]
+git town up [(-d | --display-types) <type>] [-h | --help] [-m | --merge] [(-o | --order) <asc|desc>] [-v | --verbose]
 ```
 
 The _up_ command moves one position up in the current stack by switching to a
@@ -39,12 +41,16 @@ main
 
 ## Options
 
-#### `-d`<br>`--display-types`
+#### `-d <branch-types>`<br>`--display-types <branch-types>`
 
 This flag allows customizing whether Git Town also displays the branch type in
 addition to the branch name when showing a list of branches. More info
 [here](../preferences/display-types.md#cli-flags).
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
 #### `-m`<br>`--merge`
 
 The `--merge` aka `-m` flag has the same effect as the
@@ -55,7 +61,7 @@ branch.
 This is useful when you have uncommitted changes in your current branch and want
 to move them up to a child branch.
 
-#### `-o`<br>`--order`
+#### `-o <asc|desc>`<br>`--order <asc|desc>`
 
 The `--order` flag allows customizing the order in which branches get displayed.
 More info [here](../preferences/order.md#cli-flag)
@@ -67,8 +73,12 @@ determine the repository state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [branch](branch.md) displays the branch hierarchy
 - [down](down.md) moves one position down in the current stack
-- [switch](switch.md) interactively switch between branches
 - [swap](swap.md) changes the stack by swapping the position of current branch
   with its parent
+- [switch](switch.md) interactively switch between branches
+
+<!-- keep-sorted end -->
diff --git a/website/src/commands/walk.md b/website/src/commands/walk.md
index 657736544..0a39af2db 100644
--- a/website/src/commands/walk.md
+++ b/website/src/commands/walk.md
@@ -1,7 +1,9 @@
 # git town walk
 
+<a type="git-town-command" />
+
 ```command-summary
-git town walk [-a | --all] [-s | --stack] [--dry-run] [-v | --verbose] [<command and arguments>]
+git town walk [<command and arguments>] [-a | --all] [--dry-run] [-h | --help] [-s | --stack] [-v | --verbose]
 ```
 
 The _walk_ command ("walking the branch hierarchy") executes a given command for
@@ -52,15 +54,19 @@ Running `git town walk --stack make lint` produces this output:
 
 Iterate through all local branches.
 
-#### `-s`<br>`--stack`
-
-Iterate through all branches of the stack that the current branch belongs to.
-
 #### `--dry-run`
 
 Test-drive this command: It prints the commands that would be run but doesn't
 execute them.
 
+#### `-h`<br>`--help`
+
+Display help for this command.
+
+#### `-s`<br>`--stack`
+
+Iterate through all branches of the stack that the current branch belongs to.
+
 #### `-v`<br>`--verbose`
 
 Print all Git commands executed under the hood to determine the repository
@@ -68,7 +74,11 @@ state.
 
 ## See also
 
+<!-- keep-sorted start -->
+
 - [branch](branch.md) displays the branch hierarchy and highlights the currently
   checked out branch in it
 - [switch](switch.md) displays the branch hierarchy and lets you select a branch
   to switch to
+
+<!-- keep-sorted end -->
diff --git a/website/src/configuration-commands.md b/website/src/configuration-commands.md
index e806b2173..f875567c4 100644
--- a/website/src/configuration-commands.md
+++ b/website/src/configuration-commands.md
@@ -10,11 +10,16 @@ configuration entries, run `git config --get-regexp git-town`. The following
 commands read and write the configuration entries for you so that you don't have
 to run Git configuration commands manually:
 
+<!-- keep-sorted start by_regex=["[^]]+"] -->
+
+- [git town completions](commands/completions.md) - set up shell autocomplete
 - [git town config](commands/config.md) - display or update your Git Town
   configuration
+- [git town config get-parent](commands/config-get-parent.md) - display the name
+  of the parent branch
+- [git town config remove](commands/config-remove.md) - remove the Git Town
+  configuration
 - [git town init](commands/init.md) - setup assistant for all config settings
 - [git town offline](commands/offline.md) - enable/disable offline mode
-- git town config sync-perennial-strategy - display or set the strategy to
-  update perennial branches
-- git town config sync-feature-strategy - display or set the strategy to sync
-  via merges or rebases
+
+<!-- keep-sorted end -->
diff --git a/website/src/configuration-file.md b/website/src/configuration-file.md
index e941697ac..cb41ed6ea 100644
--- a/website/src/configuration-file.md
+++ b/website/src/configuration-file.md
@@ -20,6 +20,7 @@ perennial-regex = ""
 perennials = []
 
 [create]
+branch-prefix = ""
 new-branch-type = "feature"
 share-new-branches = "no"
 
diff --git a/website/src/how-to/sync-lineage.md b/website/src/how-to/sync-lineage.md
new file mode 100644
index 000000000..a8b45c368
--- /dev/null
+++ b/website/src/how-to/sync-lineage.md
@@ -0,0 +1,13 @@
+# Sync branch lineage across team members or repository clones
+
+Teams using Git Town, as well as individual users working across multiple
+machines or repository clones, often need a way to share branch lineage.
+
+Git Town supports this via proposals. When you [sync](../commands/sync.md) a
+branch and Git Town doesn't know its parent, it checks your forge for an
+existing proposal. If it finds one, Git Town uses the proposal's target branch
+as the parent.
+
+To automate proposal creation for new branches, set
+[`share-new-branches`](https://www.git-town.com/preferences/share-new-branches.html)
+to `propose`.
diff --git a/website/src/how-tos.md b/website/src/how-tos.md
index abcf1b7a4..fcd4ddb14 100644
--- a/website/src/how-tos.md
+++ b/website/src/how-tos.md
@@ -18,3 +18,6 @@ Town. Learn how to:
 - Work with [stacked changes](stacked-changes.md)
 
 - Integrate Git Town with [LazyGit](how-to/lazygit.md)
+
+- [Sync branch lineage](how-to/sync-lineage.md) across different repository
+  clones
diff --git a/website/src/install.sh b/website/src/install.sh
index 8c8551f8e..8c1a6ae36 100755
--- a/website/src/install.sh
+++ b/website/src/install.sh
@@ -3,7 +3,7 @@ set -e
 
 # This script installs the Git Town executable in the user's HOME directory.
 
-VERSION=22.1.0             # the version of Git Town to install
+VERSION=22.2.0             # the version of Git Town to install
 DEST=$HOME/.local/bin      # the folder into which to install the Git Town executable
 TMP_DIR=.git-town-download # temporary folder to use
 
diff --git a/website/src/preferences/branch-prefix.md b/website/src/preferences/branch-prefix.md
new file mode 100644
index 000000000..04b931627
--- /dev/null
+++ b/website/src/preferences/branch-prefix.md
@@ -0,0 +1,48 @@
+# Branch prefix
+
+When set, Git Town automatically adds this prefix to branches it creates.
+
+For example, with a branch prefix of `kevgo-`:
+
+- `git town hack example` creates branch `kevgo-example`
+- `git town append child` creates branch `kevgo-child`
+- `git town prepend parent` creates branch `kevgo-parent`
+- `git town rename other` renames the current branch to `kevgo-other`
+
+If the branch name you provide already includes the configured prefix, Git Town
+won't add it again. For instance, with prefix `kevgo-`, running
+`git town hack kevgo-example` creates `kevgo-example` (not
+`kevgo-kevgo-example`).
+
+## configure in config file
+
+In the [config file](../configuration-file.md), define the branch prefix within
+the `[create]` section:
+
+```toml
+[create]
+branch-prefix = "kevgo-"
+```
+
+## configure in Git metadata
+
+To manually set the branch prefix, use the following command:
+
+```wrap
+git config [--global] git-town.branch-prefix 'kevgo-'
+```
+
+The optional `--global` flag applies this setting to all Git repositories on
+your local machine. When not present, the setting applies to the current repo.
+
+## environment variable
+
+You can configure the branch prefix by setting the `GIT_TOWN_BRANCH_PREFIX`
+environment variable.
+
+If you want to use your GitHub username as the branch prefix, set this
+environment variable once with a call like this:
+
+```bash
+export GIT_TOWN_BRANCH_PREFIX=$(gh api user --jq '.login')
+```
diff --git a/website/src/preferences/browser.md b/website/src/preferences/browser.md
index e1db67533..1923fa7a1 100644
--- a/website/src/preferences/browser.md
+++ b/website/src/preferences/browser.md
@@ -1,5 +1,28 @@
-# BROWSER environment variable
+# Browser
 
-On non-Windows systems, Git Town will first read the `BROWSER` environment
-variable to determine the browser command. If it isn't set, Git Town will try
-various common commands like `open`, `xdg-open`, or `x-www-browser`.
+By default, Git Town launches your system's default browser by trying common
+commands like `open`, `xdg-open`, or `x-www-browser`.
+
+You can override this behavior to use a specific browser. Disable browser
+launching entirely by setting `(none)` as the browser executable.
+
+## configure in config file
+
+```toml
+[hosting]
+browser = "<browser executable>"
+```
+
+## configure in Git metadata
+
+```wrap
+git config [--global] git-town.browser '<browser executable>'
+```
+
+The optional `--global` flag applies this setting to all Git repositories on
+your local machine. When not present, the setting applies to the current repo.
+
+## environment variable
+
+Git Town uses the `BROWSER` environment variable that is also used by other
+tools.
diff --git a/website/src/preferences/share-new-branches.md b/website/src/preferences/share-new-branches.md
index 159b5d014..c27833e1f 100644
--- a/website/src/preferences/share-new-branches.md
+++ b/website/src/preferences/share-new-branches.md
@@ -6,9 +6,9 @@ This setting allows you to change how Git Town shares new branches created with
 
 Allowed values:
 
-- **no:** Keep new branches local on your machine until you
-  [sync](../commands/sync.md) or [propose](../commands/propose.md) them (default
-  behavior).
+- **no/false/0:** Don't share new branches, keep them local on your machine
+  until you [sync](../commands/sync.md) or [propose](../commands/propose.md)
+  them (default behavior).
 - **push:** Push new branches to the [development remote](dev-remote.md).
 - **propose:** Create a pull request for the new branch. This is similar to
   always adding the [propose flag](../commands/hack.md#--propose).
diff --git a/website/src/stacked-changes.md b/website/src/stacked-changes.md
index 688b410da..fa280d48b 100644
--- a/website/src/stacked-changes.md
+++ b/website/src/stacked-changes.md
@@ -1,79 +1,60 @@
 # Stacked changes
 
-[Stacked changes](https://newsletter.pragmaticengineer.com/p/stacked-diffs)
-implement and review a complex change as a series of smaller feature branches
-that build on top of each other. Benefits of stacked changes are:
+[Stacked changes](https://newsletter.pragmaticengineer.com/p/stacked-diffs) let
+you implement and review complex work as a series of smaller, focused feature
+branches that build on top of each other.
 
-- developer and reviewer maintain momentum and block less on each other
-- breaking up the problem of developing/reviewing a complex problem into
-  developing/reviewing many smaller problems
-- minimize merge conflicts by shipping parts of a complex change that are
-  already approved separately from parts still under review
+Key benefits of stacked changes:
 
-The
-[single responsibility principle](https://en.wikipedia.org/wiki/Single-responsibility_principle)
-applies to feature branches the same way it applies to functions, classes, and
-methods. Feature branches should also perform only one change. Implementing,
-refactoring, reviewing, and resolving merge conflicts on such
-single-responsibility branches is easier than with branches that combine
-unrelated changes.
-
-Git Town provides wide reaching support for stacked changes. When using stacked
-changes, try to fast-forward your feature branches into the main branch to avoid
-empty merge conflicts when syncing the stack later. On GitLab that's
-[straightforward](https://docs.gitlab.com/ee/user/project/merge_requests/methods/#fast-forward-merge).
-GitHub does not provide a fast-forward merge option out of the box but you can
-achieve it with the
-[fast-forward ship strategy](preferences/ship-strategy.md#fast-forward) together
-with the [compress](preferences/sync-feature-strategy.md#compress) or
-[rebase](preferences/sync-feature-strategy.md#rebase) sync strategy. The
-[Git Town GitHub Action](https://github.com/marketplace/actions/git-town-github-action)
-adds a visual description of which branch of the stack the pull request is for.
+- Developers and reviewers maintain momentum and block each other less
+- Large, complex changes are broken into smaller, easier-to-review parts
+- Merge conflicts are reduced by shipping already approved parts separately from
+  work still under review
+
+Implementing a complex change as a stack of branches requires running a lot more
+Git commands. Git Town provides first-class support for stacked changes and
+automates this extra work for you.
 
 ## Example
 
-Let's say we want to add a new feature to an existing codebase. Before we can do
-that cleanly, we need to get the code base ready:
+Suppose you are adding a new feature to an existing codebase. Before we can do
+that cleanly, you need to prepare the code base:
 
-1. Make the architecture more flexible so that we can add the new feature in a
-   clean way.
-2. Clean up some technical drift: improve the names of variables and functions.
-3. Build the feature on top of this modernized codebase
+1. Refactor the architecture to make it easier to add the new feature cleanly
+2. Clean up technical drift: rename variables, functions, etc
+3. Build the feature on top of the improved codebase
 
-Implementing all these changes in a single feature branch is risky. Some changes
-like the refactor in (1) touch a lot of files that other people might change as
-well. We want to review and merge them as fast as possible to minimize merge
-conflicts. Other changes like building the actual feature in (3) will take a
-while to build. We should therefore make both changes in separate branches. At
-the same time, the feature (3) depends on the changes in (1) and (2) and drives
-the changes in (2). We want to develop these changes together. The solution is a
-stack of feature branches.
+Putting all these changes into one feature branch is risky. The refactor in (1)
+touches many files that other people may also be changing. We want to review and
+merge this quickly to minimize conflicts. The feature in (3) might take longer
+to build. Both changes should therefore live in separate branches.
+
+However, the feature in (3) depends on (1) and (2). We need to develop them
+together while reviewing them independently. This is a perfect use case for
+stacked branches.
 
 ## Branch 1: refactor
 
-The first feature branch contains the refactor. We create a feature branch named
-`1-refactor` off the main branch to contain it.
+Start by creating a branch for the refactor:
 
 ```
 git town hack 1-refactor
 ```
 
-[git town hack](commands/hack.md) creates a new feature branch off the main
-branch. We perform the refactor and commit it.
+[git town hack](commands/hack.md) creates a new feature branch off the `main`
+branch. Implement the refactor and commit your changes.
 
 ## Branch 2: rename foo
 
-With the refactored architecture in place, we can update the names of some
-variables, functions, and files whose role has changed as the code base has
-evolved. Since these changes require the refactor, we perform them on top of
-branch `1-refactor`:
+Next, perform some renames that depend on the refactor. Create a new branch on
+top of `1-refactor`:
 
 ```
 git town append 2-rename-foo
 ```
 
 [git town append](commands/append.md) creates a new feature branch on top of the
-currently checked out branch (which is `1-refactor`). We now have this lineage:
+current branch. The resulting lineage looks like this:
 
 ```
 main
@@ -83,21 +64,14 @@ main
 *   2-rename-foo
 ```
 
-Branch `2-rename-foo` builds on top of `1-refactor` and thereby contains all the
-changes made there. We commit the changes that rename the `foo` variable.
-Because we used `git town append` to create the new branch, Git Town knows about
-the lineage and creates the proposal (aka pull request) for branch
-`2-rename-foo` against branch `1-refactor`. This way, the proposal for branch
-`2-rename-foo` shows only the changes made in that branch (renaming the
-variable) and not the refactor made in branch 1.
+Branch `2-rename-foo` now includes the refactor from branch 1. When you open a
+PR, Git Town will target `1-refactor` automatically, so reviewers see only the
+renames, not the refactor diff.
 
 ## Branch 3: rename bar
 
-This is a different change from renaming `foo` and has different reviewers.
-Let's perform it in a different branch. Some of these changes might happen on
-the same lines on which we also renamed `foo` earlier. We don't want to deal
-with merge conflicts coming from that. So let's make this change on top of the
-change we made in step 2:
+This change is independent of renaming `foo` and may have a different reviewer.
+Create another branch on top of `2-rename-foo`:
 
 ```
 git town append 3-rename-bar
@@ -117,182 +91,195 @@ main
 
 ## Extend the refactoring
 
-While renaming `bar`, we discover another improvement for the architecture.
-Let's add it to the refactoring branch.
+While working on `3-rename-bar`, you discover another improvement for the
+architecture. Add it to `1-refactor`:
 
-```
+```sh
 git checkout 1-refactor
 # make the changes and commit them
 git checkout 3-rename-bar
 ```
 
-Back on branch `3-rename-bar`, the additional refactor we just added isn't
-visible because the commit for it exists only in branch `1-refactor` right now.
-Let's propagate these changes through the entire branch chain so that they
-become visible in branches 2 and 3 as well:
+The additional architecture changes we just committed exist only in
+`1-refactor`. To propagate them through the other branches in the stack, run:
 
 ```
-git town sync
+git town sync --stack --detached
 ```
 
-Because we created the branches with `git town append`, Git Town knows about the
-branch lineage and [git town sync](commands/sync.md) can update all branches in
-the right order. It updates the `main` branch, merges `main` into branch 1. Then
-it merges branch 1 into branch 2 and branch 2 into branch 3.
-
-## Shipping the refactor
+This command does the following things:
 
-We got the approval for the refactor from step 1. Let's ship it!
+1. Pulls updates from `1-refactor` into `2-rename-foo`
+2. Pulls updates from `2-rename-foo` into `3-rename-bar`
 
-```
-git town ship 1-refactor
-```
+## Shipping the refactor
 
-You have to use the [git town ship](commands/ship.md) command here because it
-updates the lineage that Git Town keeps track of. With branch `1-refactor`
-shipped, our lineage now looks like this:
+Once the refactor is approved, you or somebody else merges this pull request.
+The stack now looks like this:
 
 ```
 main
  \
-  2-rename-foo
+  1-refactor (the remote branch is gone, the local branch still exists)
    \
-*   3-rename-bar
+    2-rename-foo
+     \
+*     3-rename-bar
 ```
 
-If you ship feature branches through the API or UI of your forge, run
-`git town sync --all`, or `git town sync` on the youngest child branch, to
-update the lineage.
+## Keeping the stack up to date
 
-## Synchronizing our work with the rest of the world
-
-We have been at it for a while. Other developers on the team have made changes
-to the codebase as well. We don't want our branches to deviate too much from the
-`main` branch since that leads to more severe merge conflicts later. Let's get
-all our branches in sync with the rest of the world!
+We have been at it for a while. Other team members made changes to the codebase
+as well. We don't want our local branches to deviate too much from the rest of
+the codebase, since that leads to merge conflicts later. Let's get our local Git
+workspace in sync with the rest of the universe!
 
 ```
 git town sync --all
 ```
 
-This pulls updates for the `main` branch, then merges it into `2-rename-foo`,
-then `2-rename-foo` into `3-rename-bar`.
+[git town sync](commands/sync.md) updates all branches:
+
+1. Pulls updates made by other people into our local `main` branch
+2. Deletes branch `1-refactor` from our local Git workspace because it was
+   shipped at the remote
+3. Pulls updates from `main` into `2-rename-foo`
+4. Pulls updates from `2-rename-foo` into `3-rename-bar`
 
-## Branch 4: building the new feature
+## Build the new feature
 
-We can now add the new feature on top of the code base we prepared:
+We can now add the new feature on top of the cleaned-up code base:
 
 ```
 git town append 4-add-feature
 ```
 
-Let's stop here and review what we have done.
-
-- Each change happens in its own feature branch.
-- Our feature branches build on top of each other and see changes in their
-  parent branches.
-- We review and ship each feature branch in the chain in isolation.
-- `git town hack` creates a feature branch as a child of the main branch.
-- `git town append` creates a feature branch as a child of the current feature
-  branch.
-- `git town sync` keeps a feature branch chain up to date with the rest of the
-  world
-- `git town ship` ships the oldest feature branch in a branch chain.
-
-Single-responsibility branches are easier to reason about and faster to
-implement, debug, review, and ship than branches performing multiple changes.
-They encounter fewer and smaller merge conflicts which are easier to resolve
-than merge conflicts on branches that implement many different changes. You can
-review and ship parts of your complex change before the entire change is
-finished. You can still make different changes in parallel, just commit them to
-the correct branch.
+Now you have a clean, reviewable stack:
+
+- Each change lives in its own branch
+- Branches build on top of each other
+- All branches get reviewed independently
+- [git town hack](https://www.git-town.com/commands/hack.html) starts a stack by
+  creating its first branch
+- [git town append](https://www.git-town.com/commands/append.html) extends a
+  stack by adding a branch to its end
+- You always ship the oldest branch in the stack
+- `git town sync` keeps the stack up to date with other changes made to the
+  codebase
 
 ## Best practices
 
-#### One change per branch
+### One change per branch
+
+The
+[single responsibility principle](https://en.wikipedia.org/wiki/Single-responsibility_principle)
+applies to feature branches just as it does to functions, classes, and modules.
+Single-responsibility branches are easier to reason about, less likely to
+conflict, and allow shipping work faster.
 
 When you have an idea that is different from what you currently work on, resist
 the urge to code it in the current feature branch. Implement it in its own
 feature, parent, or child branch.
 
-If you can't create a new branch right now, write the idea down and implement it
-later.
+If you can't create a new branch right now, write your idea down and implement
+it later.
+
+### Avoid unnecessary stacking
+
+Only stack changes that depend on each other. If they don't, create them as
+independent top-level feature branches that have `main` as their parent. This
+setup has the advantage that you can ship any branch in any order.
+
+It's okay to have multiple stacks.
+
+### Keep your stack organized
 
-#### Keep the stack in sync
+Branches must be shipped oldest-first. Git Town provides powerful commands to
+organize the branches in your stack:
 
-Stacks are more susceptible to phantom merge conflicts than stand-alone
-branches. Don't forget to populate changes across all branches in your stack by
-running `git town sync --stack` or `git town sync --all`.
+- [git town hack](commands/hack.md) starts a new stack
+- [git town append](commands/append.md) appends a new branch to the end of a
+  stack
+- [git town prepend](commands/prepend.md) inserts a new branch between the
+  current branch and its parent
+- [git town detach](commands/detach.md) extracts a branch from a stack and makes
+  its own independent stack
+- [git town swap](commands/swap.md) switches the position of the current branch
+  and its parent in the stack
+- [git town set-parent](commands/set-parent.md) changes the parent for the
+  current branch and all its descendents
 
-#### Avoid unnecessary stacking
+### Navigate your stack efficiently
 
-To reduce merge conflicts, feature branches should not diverge too much from the
-main development branch. Stacking multiple changes on top of each other
-amplifies this divergence. Overly "tall" stacks are therefore an anti-pattern to
-avoid. It's often better to work in independent top-level feature branches by
-default, and only stack branches if the changes they contain really depend on
-each other. This way you can get your changes reviewed and shipped concurrently
-and in any order, i.e. faster and with fewer merge conflicts.
+To help commit the right changes to the right branch, Git Town provides powerful
+commands to navigate stacks:
 
-#### Organize branch chains in the order you want to ship
+- [git town branch](commands/branch.md) shows you where you are in the stack
+  hierarchy
+- [git town switch](commands/switch.md) allows you to jump to any branch using a
+  visual dialog with VIM motions
+- [git town down](commands/up.md) switches to the parent branch
+- [git town up](commands/up.md) switches to the child branch
+- [git town walk](commands/walk.md) executes a CLI command or opens an
+  interactive shell on each branch of the stack
 
-You always have to ship the oldest branch first. You can use
-[git town prepend](commands/prepend.md) to insert a feature branch as a parent
-of the current feature branch or [set parent](commands/set-parent.md) to change
-the order of branches.
+### Embed the stack lineage into pull requests
 
-#### Avoid phantom conflicts
+The
+[Git Town GitHub Action](https://github.com/marketplace/actions/git-town-github-action)
+adds a visual graph of which branch of the stack the pull request is for. This
+provides context when reviewing changes.
+
+### Keep the stack in sync
+
+Stacks are more prone to phantom merge conflicts than stand-alone branches. Run
+`git town sync --stack` or `git town sync --all` regularly to propagate changes
+across your stacks.
 
-_Phantom conflicts_ occur when Git reports a merge or rebase conflict that -
-when looked at with more context - isn't a real conflict. Phantom conflicts can
-occur when multiple branches in a stack modify the same line in the same file,
-and you ship using squash-merges.
+### Avoid phantom conflicts
 
-After you ship the oldest branch of such a stack, the main branch contains a new
-commit that makes the same changes as the shipped branch, but as a different
-commit than the one(s) on the shipped branch. As this new commit populates
-through the stack in the next sync, Git sees sees two changes to the same lines
-and assumes a conflict.
+_Phantom conflicts_ occur when Git reports a merge or rebase conflict that isn't
+a real conflict. They can occur when multiple branches in a stack modify the
+same line in the same file, and you ship using squash-merges.
 
-Git Town can resolve these phantom conflicts because it tracks the branch
-hierarchy, can investigate such conflicts, and execute multiple Git commands to
-resolve them.
+After shipping the oldest branch from a stack using a squash-merge, `main`
+contains a new commit with the same changes as the shipped branch but a
+different commit hash. When syncing, Git sees the new commit on main and the
+commit on the shipped branch as conflicting edits to the same line.
 
-Here are some best practices to minimize phantom merge conflicts:
+Git Town can detect and automatically resolve many of these phantom conflicts
+because it tracks the branch hierarchy and understands the relationships between
+commits.
 
-1. Sync frequently. In a synced stack, each branch builds directly on top of its
-   parent, so changes are linear and easy for Git to reconcile. In an unsynced
-   stack, sibling branches evolve concurrently, making conflicts more likely,
-   especially when they touch the same files.
+To minimize phantom conflicts:
 
-   If you are hesitant to sync because it takes too long, use the
+1. **Sync frequently.** In a synced stack, each branch builds directly on top of
+   its parent, so changes are linear and easy for Git to reconcile. Branches in
+   an unsynced stack drift apart, making conflicts more likely.
+
+   If syncing takes too long, use
    [--detached](commands/sync.md#-d--detached--no-detached) and
-   [--no-push](commands/sync.md#--push--no-push) flags to speed it up.
-
-2. Enable Git's [rerere](https://git-scm.com/book/en/v2/Git-Tools-Rerere)
-   feature. This lets Git remember how you resolved past conflicts and applies
-   those resolutions in the future.
-
-3. Ship using a
-   [fast-forward merge](https://git-scm.com/docs/git-merge#_fast_forward_merge).
-   This ensures the commits on main are byte-for-byte identical to those on the
-   shipped branchs. This preserved shared history avoids unnecessary merges or
-   rebases that likely produce phantom conflicts.
-
-   - GitLab supports this
-     [natively](https://docs.gitlab.com/ee/user/project/merge_requests/methods/#fast-forward-merge).
-   - GitHub doesn’t support fast-forward merges via the UI, but you can achieve
-     the same effect by [shipping locally](commands/ship.md) with Git Town's
-     [fast-forward strategy](preferences/ship-strategy.md#fast-forward) and then
-     pushing the result. See GitHub’s
+   [--no-push](commands/sync.md#--push--no-push) to speed it up.
+
+2. **Enable [rerere](https://git-scm.com/book/en/v2/Git-Tools-Rerere).** Git
+   remembers how you resolved past conflicts and reuses those resolutions
+   automatically.
+
+3. **Ship using
+   [fast-forward merges](https://git-scm.com/docs/git-merge#_fast_forward_merge).**
+   Fast-forwarding keeps commit history between your stack and `main` identical,
+   avoiding synthetic differences that cause phantom conflicts.
+
+   - [GitLab supports this natively](https://docs.gitlab.com/ee/user/project/merge_requests/methods/#fast-forward-merge).
+   - On GitHub, use [git town ship](commands/ship.md) with the
+     [fast-forward strategy](preferences/ship-strategy.md#fast-forward) to
+     achieve the same effect. See GitHub’s
      [docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squashing-and-merging-a-long-running-branch)
      for details.
 
-4. If a feature branch has too many commits and you're resolving the same
-   conflicts repeatedly, [compress](commands/compress.md) it down to a single
+4. **Compress noisy branches.** If a branch has too many commits and keeps
+   hitting the same conflicts, [compress](commands/compress.md) it to a single
    commit.
 
-5. Focus your feature branches to implement only a single change. This reduces
-   the amount of context you need to process when resolving merge conflicts, and
-   makes it easier to see which branch makes which change and why, and what the
-   correct resolution is.
+5. **Keep branches focused.** Small, single-purpose branches make it easier to
+   understand and resolve conflicts, and to see what changed, why, and where.



Test patch:
diff --git a/internal/cli/flags/type_test.go b/internal/cli/flags/type_test.go
index 2ce7bd93f..3296db394 100644
--- a/internal/cli/flags/type_test.go
+++ b/internal/cli/flags/type_test.go
@@ -41,7 +41,7 @@ func TestBranchTypeFlag(t *testing.T) {
 			"c,f,o":                 {configdomain.BranchTypeContributionBranch, configdomain.BranchTypeFeatureBranch, configdomain.BranchTypeObservedBranch},
 		}
 		for give, want := range tests {
-			have, err := flags.ParseBranchTypes(give)
+			have, err := flags.ParseBranchTypes(give, "test")
 			must.NoError(t, err)
 			must.Eq(t, want, have)
 		}
diff --git a/internal/config/configdomain/branch_prefix_test.go b/internal/config/configdomain/branch_prefix_test.go
new file mode 100644
index 000000000..46aec61a1
--- /dev/null
+++ b/internal/config/configdomain/branch_prefix_test.go
@@ -0,0 +1,58 @@
+package configdomain_test
+
+import (
+	"testing"
+
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+	"github.com/shoenig/test/must"
+)
+
+func TestBranchPrefix(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Apply", func(t *testing.T) {
+		t.Parallel()
+
+		t.Run("empty prefix", func(t *testing.T) {
+			t.Parallel()
+			prefix := configdomain.BranchPrefix("")
+			branch := gitdomain.NewLocalBranchName("feature")
+			have := prefix.Apply(branch)
+			want := gitdomain.NewLocalBranchName("feature")
+			must.EqOp(t, want, have)
+		})
+
+		t.Run("non-empty prefix", func(t *testing.T) {
+			t.Parallel()
+			prefix := configdomain.BranchPrefix("prefix-")
+			branch := gitdomain.NewLocalBranchName("feature")
+			have := prefix.Apply(branch)
+			want := gitdomain.NewLocalBranchName("prefix-feature")
+			must.EqOp(t, want, have)
+		})
+
+		t.Run("branch already contains the prefix", func(t *testing.T) {
+			t.Parallel()
+			prefix := configdomain.BranchPrefix("prefix")
+			branch := gitdomain.NewLocalBranchName("prefix-branch")
+			have := prefix.Apply(branch)
+			want := gitdomain.NewLocalBranchName("prefix-branch")
+			must.EqOp(t, want, have)
+		})
+	})
+
+	t.Run("ParseBranchPrefix", func(t *testing.T) {
+		t.Parallel()
+		tests := map[string]Option[configdomain.BranchPrefix]{
+			"":        None[configdomain.BranchPrefix](),
+			"prefix-": Some(configdomain.BranchPrefix("prefix-")),
+		}
+		for give, want := range tests {
+			have, err := configdomain.ParseBranchPrefix(give, "test")
+			must.NoError(t, err)
+			must.Eq(t, want, have)
+		}
+	})
+}
diff --git a/internal/config/configdomain/branch_type_test.go b/internal/config/configdomain/branch_type_test.go
index 787c0a4a4..82ad1d5cd 100644
--- a/internal/config/configdomain/branch_type_test.go
+++ b/internal/config/configdomain/branch_type_test.go
@@ -32,7 +32,7 @@ func TestBranchType(t *testing.T) {
 			"pr":                      Some(configdomain.BranchTypePrototypeBranch),
 		}
 		for give, want := range tests {
-			have, err := configdomain.ParseBranchType(give)
+			have, err := configdomain.ParseBranchType(give, "test")
 			must.NoError(t, err)
 			must.Eq(t, want, have)
 		}
diff --git a/internal/config/configdomain/perennial_regex_test.go b/internal/config/configdomain/perennial_regex_test.go
index 56c919eb1..8f3c8dbf3 100644
--- a/internal/config/configdomain/perennial_regex_test.go
+++ b/internal/config/configdomain/perennial_regex_test.go
@@ -13,7 +13,7 @@ func TestPerennialRegex(t *testing.T) {
 
 	t.Run("only characters, no wildcards matches all branch names that contain that phrase", func(t *testing.T) {
 		t.Parallel()
-		perennialRegexOpt, err := configdomain.ParsePerennialRegex("release")
+		perennialRegexOpt, err := configdomain.ParsePerennialRegex("release", "test")
 		must.NoError(t, err)
 		perennialRegex := perennialRegexOpt.GetOrPanic()
 		tests := map[string]bool{
@@ -31,7 +31,7 @@ func TestPerennialRegex(t *testing.T) {
 
 	t.Run("with wildcards", func(t *testing.T) {
 		t.Parallel()
-		perennialRegexOpt, err := configdomain.ParsePerennialRegex("release-.*")
+		perennialRegexOpt, err := configdomain.ParsePerennialRegex("release-.*", "test")
 		must.NoError(t, err)
 		perennialRegex := perennialRegexOpt.GetOrPanic()
 		tests := map[string]bool{
diff --git a/internal/config/configdomain/share_new_branches_test.go b/internal/config/configdomain/share_new_branches_test.go
new file mode 100644
index 000000000..1d438b117
--- /dev/null
+++ b/internal/config/configdomain/share_new_branches_test.go
@@ -0,0 +1,46 @@
+package configdomain_test
+
+import (
+	"testing"
+
+	"github.com/git-town/git-town/v22/internal/config/configdomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+	"github.com/shoenig/test/must"
+)
+
+func TestParseShareNewBranches(t *testing.T) {
+	t.Parallel()
+
+	t.Run("empty string", func(t *testing.T) {
+		t.Parallel()
+		have, err := configdomain.ParseShareNewBranches("", "test source")
+		must.NoError(t, err)
+		must.True(t, have.IsNone())
+	})
+
+	t.Run("invalid value", func(t *testing.T) {
+		t.Parallel()
+		_, err := configdomain.ParseShareNewBranches("zonk", "test source")
+		must.Error(t, err)
+		must.StrContains(t, err.Error(), "invalid value for \"test source\": \"zonk\"")
+	})
+
+	t.Run("valid values", func(t *testing.T) {
+		t.Parallel()
+		tests := map[string]Option[configdomain.ShareNewBranches]{
+			"no":      Some(configdomain.ShareNewBranchesNone),
+			"false":   Some(configdomain.ShareNewBranchesNone),
+			"0":       Some(configdomain.ShareNewBranchesNone),
+			"push":    Some(configdomain.ShareNewBranchesPush),
+			"propose": Some(configdomain.ShareNewBranchesPropose),
+		}
+		for give, want := range tests {
+			t.Run(give, func(t *testing.T) {
+				t.Parallel()
+				have, err := configdomain.ParseShareNewBranches(give, "test source")
+				must.NoError(t, err)
+				must.Eq(t, want, have)
+			})
+		}
+	})
+}
diff --git a/internal/config/configfile/load_test.go b/internal/config/configfile/load_test.go
index 9e1b94896..fc1761ce6 100644
--- a/internal/config/configfile/load_test.go
+++ b/internal/config/configfile/load_test.go
@@ -36,16 +36,18 @@ perennial-regex = "release-.*"
 unknown-type = "prototype"
 
 [create]
+branch-prefix = "feature-"
 new-branch-type = "prototype"
 push-new-branches = true
 share-new-branches = "push"
 
 [hosting]
+browser = "chrome"
 forge-type = "github"
 origin-hostname = "github.com"
 
 [propose]
-lineage = "ci"
+lineage = "cli"
 
 [ship]
 delete-tracking-branch = false
@@ -81,16 +83,18 @@ prototype-branches = "compress"
 					UnknownType:       Ptr("prototype"),
 				},
 				Create: &configfile.Create{
+					BranchPrefix:     Ptr("feature-"),
 					NewBranchType:    Ptr("prototype"),
 					PushNewbranches:  Ptr(true),
 					ShareNewBranches: Ptr("push"),
 				},
 				Hosting: &configfile.Hosting{
+					Browser:        Ptr("chrome"),
 					ForgeType:      Ptr("github"),
 					OriginHostname: Ptr("github.com"),
 				},
 				Propose: &configfile.Propose{
-					Lineage: Ptr("ci"),
+					Lineage: Ptr("cli"),
 				},
 				Ship: &configfile.Ship{
 					DeleteTrackingBranch: Ptr(false),
diff --git a/internal/config/configfile/save_test.go b/internal/config/configfile/save_test.go
index 18a67bfc7..57cca3277 100644
--- a/internal/config/configfile/save_test.go
+++ b/internal/config/configfile/save_test.go
@@ -45,48 +45,72 @@ func TestSave(t *testing.T) {
 		t.Parallel()
 		t.Run("all options given", func(t *testing.T) {
 			t.Parallel()
+			contributionRegex := asserts.NoError1(configdomain.ParseContributionRegex("contribution-", "test"))
+			featureRegex := asserts.NoError1(configdomain.ParseFeatureRegex("feature-", "test"))
+			observedRegex := asserts.NoError1(configdomain.ParseObservedRegex("observed-", "test"))
+			perennialRegex := asserts.NoError1(configdomain.ParsePerennialRegex("perennial-", "test"))
 			have := configfile.RenderTOML(configdomain.PartialConfig{
-				AutoResolve: Some(configdomain.AutoResolve(false)),
-				DevRemote:   Some(gitdomain.RemoteOrigin),
+				AutoResolve:       Some(configdomain.AutoResolve(false)),
+				BranchPrefix:      Some(configdomain.BranchPrefix("feature-")),
+				Browser:           Some(configdomain.Browser("chrome")),
+				ContributionRegex: contributionRegex,
+				Detached:          Some(configdomain.Detached(true)),
+				DevRemote:         Some(gitdomain.RemoteOrigin),
 				DisplayTypes: Some(configdomain.DisplayTypes{
 					BranchTypes: []configdomain.BranchType{configdomain.BranchTypeMainBranch, configdomain.BranchTypePerennialBranch},
 					Quantifier:  configdomain.QuantifierNo,
 				}),
-				ForgeType:                asserts.NoError1(forgedomain.ParseForgeType("github")),
+				FeatureRegex:             featureRegex,
+				ForgeType:                asserts.NoError1(forgedomain.ParseForgeType("github", "test")),
+				GitHubConnectorType:      Some(forgedomain.GitHubConnectorTypeGh),
+				GitLabConnectorType:      Some(forgedomain.GitLabConnectorTypeGlab),
 				HostingOriginHostname:    configdomain.ParseHostingOriginHostname("forge"),
 				MainBranch:               Some(gitdomain.NewLocalBranchName("main")),
 				NewBranchType:            Some(configdomain.NewBranchType(configdomain.BranchTypePrototypeBranch)),
+				ObservedRegex:            observedRegex,
 				Order:                    Some(configdomain.OrderDesc),
 				PerennialBranches:        gitdomain.NewLocalBranchNames("qa", "staging"),
-				PerennialRegex:           asserts.NoError1(configdomain.ParsePerennialRegex("perennial-")),
+				PerennialRegex:           perennialRegex,
 				ProposalsShowLineage:     Some(forgedomain.ProposalsShowLineageCLI),
+				PushBranches:             Some(configdomain.PushBranches(true)),
 				PushHook:                 Some(configdomain.PushHook(true)),
 				ShareNewBranches:         Some(configdomain.ShareNewBranchesPropose),
 				ShipDeleteTrackingBranch: Some(configdomain.ShipDeleteTrackingBranch(true)),
 				ShipStrategy:             Some(configdomain.ShipStrategyAPI),
+				Stash:                    Some(configdomain.Stash(true)),
 				SyncFeatureStrategy:      Some(configdomain.SyncFeatureStrategyMerge),
 				SyncPerennialStrategy:    Some(configdomain.SyncPerennialStrategyRebase),
 				SyncPrototypeStrategy:    Some(configdomain.SyncPrototypeStrategyCompress),
 				SyncTags:                 Some(configdomain.SyncTags(true)),
 				SyncUpstream:             Some(configdomain.SyncUpstream(true)),
+				UnknownBranchType:        Some(configdomain.UnknownBranchType(configdomain.BranchTypePrototypeBranch)),
 			})
 			want := `
 # See https://www.git-town.com/configuration-file for details
 
 [branches]
+contribution-regex = "contribution-"
+display-types = "no main perennial"
+feature-regex = "feature-"
 main = "main"
+observed-regex = "observed-"
 order = "desc"
 perennials = ["qa", "staging"]
 perennial-regex = "perennial-"
-display-types = "no main perennial"
+unknown-branch-type = "prototype"
 
 [create]
+branch-prefix = "feature-"
 new-branch-type = "prototype"
 share-new-branches = "propose"
+stash = true
 
 [hosting]
+browser = "chrome"
 dev-remote = "origin"
 forge-type = "github"
+github-connector-type = "gh"
+gitlab-connector-type = "glab"
 origin-hostname = "forge"
 
 [propose]
@@ -98,9 +122,11 @@ strategy = "api"
 
 [sync]
 auto-resolve = false
+detached = true
 feature-strategy = "merge"
 perennial-strategy = "rebase"
 prototype-strategy = "compress"
+push-branches = true
 push-hook = true
 tags = true
 upstream = true
diff --git a/internal/config/normal_contig_test.go b/internal/config/normal_config_test.go
similarity index 100%
rename from internal/config/normal_contig_test.go
rename to internal/config/normal_config_test.go
diff --git a/internal/config/parse_snapshot_test.go b/internal/config/parse_snapshot_test.go
index 6ee1d7f00..5bbd90584 100644
--- a/internal/config/parse_snapshot_test.go
+++ b/internal/config/parse_snapshot_test.go
@@ -16,7 +16,7 @@ func TestNewBranchTypeOverridesFromSnapshot(t *testing.T) {
 		"git-town-branch.branch-3.parent":     "main",
 		"git-town.prototype-branches":         "foo",
 	}
-	have, err := config.NewBranchTypeOverridesInSnapshot(snapshot, nil)
+	have, err := config.NewBranchTypeOverridesInSnapshot(snapshot, false, nil)
 	must.NoError(t, err)
 	want := configdomain.BranchTypeOverrides{
 		"branch-1": configdomain.BranchTypeFeatureBranch,
diff --git a/internal/config/validated_config_test.go b/internal/config/validated_config_test.go
index 37c5ca44b..75e9492f5 100644
--- a/internal/config/validated_config_test.go
+++ b/internal/config/validated_config_test.go
@@ -102,7 +102,7 @@ func TestValidatedConfig(t *testing.T) {
 		observed := gitdomain.NewLocalBranchName("observed")
 		perennial1 := gitdomain.NewLocalBranchName("perennial-1")
 		perennial2 := gitdomain.NewLocalBranchName("perennial-2")
-		perennialRegexOpt, err := configdomain.ParsePerennialRegex("peren*")
+		perennialRegexOpt, err := configdomain.ParsePerennialRegex("peren*", "test")
 		must.NoError(t, err)
 		config := config.ValidatedConfig{
 			ValidatedConfigData: configdomain.ValidatedConfigData{
diff --git a/internal/forge/bitbucketcloud/test_connector.go b/internal/forge/bitbucketcloud/test_connector.go
index 123191273..0a31d8414 100644
--- a/internal/forge/bitbucketcloud/test_connector.go
+++ b/internal/forge/bitbucketcloud/test_connector.go
@@ -35,6 +35,7 @@ func (self TestConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 	}
 	proposal := forgedomain.BitbucketCloudProposalData{
 		ProposalData: forgedomain.ProposalData{
+			Active:       true,
 			Body:         None[string](),
 			MergeWithAPI: true,
 			Number:       123,
diff --git a/internal/forge/bitbucketdatacenter/test_connector.go b/internal/forge/bitbucketdatacenter/test_connector.go
index 291031b11..b23a9ef42 100644
--- a/internal/forge/bitbucketdatacenter/test_connector.go
+++ b/internal/forge/bitbucketdatacenter/test_connector.go
@@ -34,6 +34,7 @@ func (self TestConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 		return None[forgedomain.Proposal](), nil
 	}
 	data := forgedomain.ProposalData{
+		Active:       true,
 		Body:         None[string](),
 		MergeWithAPI: true,
 		Number:       123,
diff --git a/internal/forge/forgedomain/api_cache_test.go b/internal/forge/forgedomain/api_cache_test.go
new file mode 100644
index 000000000..13487308d
--- /dev/null
+++ b/internal/forge/forgedomain/api_cache_test.go
@@ -0,0 +1,399 @@
+package forgedomain_test
+
+import (
+	"testing"
+
+	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
+	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+	"github.com/shoenig/test/must"
+)
+
+func TestAPICache(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Clear", func(t *testing.T) {
+		t.Parallel()
+
+		t.Run("removes all cached results", func(t *testing.T) {
+			t.Parallel()
+			cache := &forgedomain.APICache{}
+			proposal := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source",
+					Target: "target",
+				},
+			}
+			cache.RegisterLookupResult("source", "target", Some(proposal))
+			cache.RegisterSearchResult("source", []forgedomain.Proposal{proposal})
+			cache.Clear()
+			_, has := cache.Lookup("source", "target")
+			must.False(t, has)
+			_, has = cache.LookupSearch("source")
+			must.False(t, has)
+		})
+	})
+
+	t.Run("Lookup", func(t *testing.T) {
+		t.Parallel()
+
+		t.Run("empty cache returns unknown", func(t *testing.T) {
+			t.Parallel()
+			cache := &forgedomain.APICache{}
+			have, has := cache.Lookup("source", "target")
+			must.False(t, has)
+			must.True(t, have.IsNone())
+		})
+
+		t.Run("cache contains existing lookup result", func(t *testing.T) {
+			t.Parallel()
+
+			t.Run("matching lookup result", func(t *testing.T) {
+				t.Parallel()
+
+				t.Run("has proposal", func(t *testing.T) {
+					t.Parallel()
+					cache := &forgedomain.APICache{}
+					giveProposal := forgedomain.Proposal{
+						Data: forgedomain.ProposalData{
+							Source: "source",
+							Target: "target",
+							Title:  "Test PR",
+						},
+					}
+					cache.RegisterLookupResult("source", "target", Some(giveProposal))
+					lookupResult, has := cache.Lookup("source", "target")
+					must.True(t, has)
+					haveProposal, has := lookupResult.Get()
+					must.True(t, has)
+					must.EqOp(t, "Test PR", haveProposal.Data.Data().Title)
+				})
+
+				t.Run("without proposal", func(t *testing.T) {
+					t.Parallel()
+					cache := &forgedomain.APICache{}
+					cache.RegisterLookupResult("source", "target", None[forgedomain.Proposal]())
+					lookupResult, has := cache.Lookup("source", "target")
+					must.True(t, has)
+					must.True(t, lookupResult.IsNone())
+				})
+			})
+
+			t.Run("mismatching lookup result", func(t *testing.T) {
+				t.Parallel()
+
+				t.Run("different source", func(t *testing.T) {
+					t.Parallel()
+					cache := &forgedomain.APICache{}
+					giveProposal := forgedomain.Proposal{
+						Data: forgedomain.ProposalData{
+							Source: "other",
+							Target: "target",
+						},
+					}
+					cache.RegisterLookupResult("other", "target", Some(giveProposal))
+					_, has := cache.Lookup("source", "target")
+					must.False(t, has)
+				})
+
+				t.Run("different target", func(t *testing.T) {
+					t.Parallel()
+					cache := &forgedomain.APICache{}
+					giveProposal := forgedomain.Proposal{
+						Data: forgedomain.ProposalData{
+							Source: "source",
+							Target: "other",
+						},
+					}
+					cache.RegisterLookupResult("source", "other", Some(giveProposal))
+					_, has := cache.Lookup("source", "target")
+					must.False(t, has)
+				})
+			})
+		})
+
+		t.Run("cache contains existing search result", func(t *testing.T) {
+			t.Parallel()
+
+			t.Run("matching search result that contains the target", func(t *testing.T) {
+				t.Parallel()
+				cache := &forgedomain.APICache{}
+				proposal1 := forgedomain.Proposal{
+					Data: forgedomain.ProposalData{
+						Source: "source",
+						Target: "target",
+						Title:  "Test PR",
+					},
+				}
+				cache.RegisterSearchResult("source", []forgedomain.Proposal{proposal1})
+				lookupResult, has := cache.Lookup("source", "target")
+				must.True(t, has)
+				haveProposal, has := lookupResult.Get()
+				must.True(t, has)
+				must.EqOp(t, "Test PR", haveProposal.Data.Data().Title)
+			})
+
+			t.Run("matching search result that does not contain the target", func(t *testing.T) {
+				t.Parallel()
+				cache := &forgedomain.APICache{}
+				proposal1 := forgedomain.Proposal{
+					Data: forgedomain.ProposalData{
+						Source: "source",
+						Target: "other",
+					},
+				}
+				cache.RegisterSearchResult("source", []forgedomain.Proposal{proposal1})
+				lookupResult, has := cache.Lookup("source", "target")
+				must.True(t, has)
+				must.True(t, lookupResult.IsNone())
+			})
+
+			t.Run("matching search result that contains no proposals", func(t *testing.T) {
+				t.Parallel()
+				cache := &forgedomain.APICache{}
+				cache.RegisterSearchResult("source", []forgedomain.Proposal{})
+				lookupResult, has := cache.Lookup("source", "target")
+				must.True(t, has)
+				must.True(t, lookupResult.IsNone())
+			})
+		})
+
+		t.Run("cache contains both lookup and search result", func(t *testing.T) {
+			t.Parallel()
+			cache := &forgedomain.APICache{}
+			lookupProposal := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source",
+					Target: "target",
+					Title:  "Lookup PR",
+				},
+			}
+			searchProposal := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source",
+					Target: "target",
+					Title:  "Search PR",
+				},
+			}
+			// Register lookup first so it's checked before search result in the loop
+			cache.RegisterLookupResult("source", "target", Some(lookupProposal))
+			cache.RegisterSearchResult("source", []forgedomain.Proposal{searchProposal})
+			result, has := cache.Lookup("source", "target")
+			must.True(t, has)
+			haveProposal, has := result.Get()
+			must.True(t, has)
+			must.EqOp(t, "Lookup PR", haveProposal.Data.Data().Title)
+		})
+	})
+
+	t.Run("LookupSearch", func(t *testing.T) {
+		t.Parallel()
+
+		t.Run("empty cache returns unknown", func(t *testing.T) {
+			t.Parallel()
+			cache := &forgedomain.APICache{}
+			source := gitdomain.NewLocalBranchName("feature")
+			_, knows := cache.LookupSearch(source)
+			must.False(t, knows)
+		})
+
+		t.Run("contains matching search result", func(t *testing.T) {
+			t.Parallel()
+			cache := &forgedomain.APICache{}
+			proposal1 := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source",
+					Target: "target",
+					Title:  "PR 1",
+				},
+			}
+			proposal2 := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source",
+					Target: "target",
+					Title:  "PR 2",
+				},
+			}
+			giveProposals := []forgedomain.Proposal{proposal1, proposal2}
+			cache.RegisterSearchResult("source", giveProposals)
+			haveProposals, has := cache.LookupSearch("source")
+			must.True(t, has)
+			must.EqOp(t, 2, len(haveProposals))
+			must.EqOp(t, "PR 1", haveProposals[0].Data.Data().Title)
+			must.EqOp(t, "PR 2", haveProposals[1].Data.Data().Title)
+		})
+
+		t.Run("contains mismatching search result", func(t *testing.T) {
+			t.Parallel()
+			cache := &forgedomain.APICache{}
+			cache.RegisterSearchResult("other", []forgedomain.Proposal{})
+			_, knows := cache.LookupSearch("source")
+			must.False(t, knows)
+		})
+
+		t.Run("ignores lookup results", func(t *testing.T) {
+			t.Parallel()
+			cache := &forgedomain.APICache{}
+			proposal := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source",
+					Target: "target",
+				},
+			}
+			cache.RegisterLookupResult("source", "target", Some(proposal))
+			_, knows := cache.LookupSearch("source")
+			must.False(t, knows)
+		})
+	})
+
+	t.Run("RegisterLookupResult", func(t *testing.T) {
+		t.Parallel()
+
+		t.Run("registers new lookup result", func(t *testing.T) {
+			t.Parallel()
+			cache := &forgedomain.APICache{}
+			proposal := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source",
+					Target: "target",
+				},
+			}
+			cache.RegisterLookupResult("source", "target", Some(proposal))
+			got, knows := cache.Lookup("source", "target")
+			must.True(t, knows)
+			must.True(t, got.IsSome())
+		})
+
+		t.Run("overwrites an existing lookup result", func(t *testing.T) {
+			t.Parallel()
+			cache := &forgedomain.APICache{}
+			proposal1 := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source",
+					Target: "target",
+					Title:  "First PR",
+				},
+			}
+			proposal2 := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source",
+					Target: "target",
+					Title:  "Second PR",
+				},
+			}
+			cache.RegisterLookupResult("source", "target", Some(proposal1))
+			cache.RegisterLookupResult("source", "target", Some(proposal2))
+			result, has := cache.Lookup("source", "target")
+			must.True(t, has)
+			haveProposal, has := result.Get()
+			must.True(t, has)
+			must.EqOp(t, "Second PR", haveProposal.Data.Data().Title)
+		})
+
+		t.Run("registers multiple different source-target pairs", func(t *testing.T) {
+			t.Parallel()
+			cache := &forgedomain.APICache{}
+			proposal1 := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source1",
+					Target: "target",
+					Title:  "First PR",
+				},
+			}
+			proposal2 := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source2",
+					Target: "target",
+					Title:  "Second PR",
+				},
+			}
+			cache.RegisterLookupResult("source1", "target", Some(proposal1))
+			cache.RegisterLookupResult("source2", "target", Some(proposal2))
+			result1, has1 := cache.Lookup("source1", "target")
+			must.True(t, has1)
+			haveProposal1, has := result1.Get()
+			must.True(t, has)
+			must.EqOp(t, "First PR", haveProposal1.Data.Data().Title)
+			result2, has := cache.Lookup("source2", "target")
+			must.True(t, has)
+			haveProposal2, has := result2.Get()
+			must.True(t, has)
+			must.EqOp(t, "Second PR", haveProposal2.Data.Data().Title)
+		})
+	})
+
+	t.Run("RegisterSearchResult", func(t *testing.T) {
+		t.Parallel()
+
+		t.Run("registers new search result", func(t *testing.T) {
+			t.Parallel()
+			cache := &forgedomain.APICache{}
+			proposal := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source",
+					Target: "target",
+					Title:  "PR 1",
+				},
+			}
+			cache.RegisterSearchResult("source", []forgedomain.Proposal{proposal})
+			result, has := cache.LookupSearch("source")
+			must.True(t, has)
+			must.EqOp(t, 1, len(result))
+			must.EqOp(t, "PR 1", result[0].Data.Data().Title)
+		})
+
+		t.Run("overwrites existing search result", func(t *testing.T) {
+			t.Parallel()
+			cache := &forgedomain.APICache{}
+			proposal1 := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source",
+					Target: "target",
+					Title:  "First PR",
+				},
+			}
+			proposal2 := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source",
+					Target: "target",
+					Title:  "Second PR",
+				},
+			}
+			cache.RegisterSearchResult("source", []forgedomain.Proposal{proposal1})
+			cache.RegisterSearchResult("source", []forgedomain.Proposal{proposal2})
+			haveProposals, has := cache.LookupSearch("source")
+			must.True(t, has)
+			must.EqOp(t, 1, len(haveProposals))
+			must.EqOp(t, "Second PR", haveProposals[0].Data.Data().Title)
+		})
+
+		t.Run("registers multiple different sources", func(t *testing.T) {
+			t.Parallel()
+			cache := &forgedomain.APICache{}
+			proposal1 := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source1",
+					Target: "target",
+					Title:  "First PR",
+				},
+			}
+			proposal2 := forgedomain.Proposal{
+				Data: forgedomain.ProposalData{
+					Source: "source2",
+					Target: "target",
+					Title:  "Second PR",
+				},
+			}
+			cache.RegisterSearchResult("source1", []forgedomain.Proposal{proposal1})
+			cache.RegisterSearchResult("source2", []forgedomain.Proposal{proposal2})
+			haveProposals1, has := cache.LookupSearch("source1")
+			must.True(t, has)
+			must.EqOp(t, 1, len(haveProposals1))
+			must.EqOp(t, "First PR", haveProposals1[0].Data.Data().Title)
+			haveProposals2, has := cache.LookupSearch("source2")
+			must.True(t, has)
+			must.EqOp(t, 1, len(haveProposals2))
+			must.EqOp(t, "Second PR", haveProposals2[0].Data.Data().Title)
+		})
+	})
+}
diff --git a/internal/forge/forgedomain/forge_type_test.go b/internal/forge/forgedomain/forge_type_test.go
index 3064f07ec..e465c6b29 100644
--- a/internal/forge/forgedomain/forge_type_test.go
+++ b/internal/forge/forgedomain/forge_type_test.go
@@ -36,7 +36,7 @@ func TestParseForgeType(t *testing.T) {
 			"Gitea":                Some(forgedomain.ForgeTypeGitea),
 		}
 		for give, want := range tests {
-			have, err := forgedomain.ParseForgeType(give)
+			have, err := forgedomain.ParseForgeType(give, "test")
 			must.NoError(t, err)
 			must.Eq(t, want, have)
 		}
@@ -44,7 +44,7 @@ func TestParseForgeType(t *testing.T) {
 
 	t.Run("invalid content", func(t *testing.T) {
 		t.Parallel()
-		_, err := forgedomain.ParseForgeType("zonk")
+		_, err := forgedomain.ParseForgeType("zonk", "test")
 		must.Error(t, err)
 	})
 }
diff --git a/internal/forge/forgedomain/github_connector_type_test.go b/internal/forge/forgedomain/github_connector_type_test.go
index eca7ff959..8899a6d00 100644
--- a/internal/forge/forgedomain/github_connector_type_test.go
+++ b/internal/forge/forgedomain/github_connector_type_test.go
@@ -25,7 +25,7 @@ func TestParseGitHubConnectorType(t *testing.T) {
 		},
 		"invalid": {
 			val: None[forgedomain.GitHubConnectorType](),
-			err: errors.New(`unknown GitHubConnectorType: "invalid"`),
+			err: errors.New(`unknown GitHubConnectorType defined in "test": "invalid"`),
 		},
 		"": {
 			val: None[forgedomain.GitHubConnectorType](),
@@ -33,7 +33,7 @@ func TestParseGitHubConnectorType(t *testing.T) {
 		},
 	}
 	for give, want := range tests {
-		have, err := forgedomain.ParseGitHubConnectorType(give)
+		have, err := forgedomain.ParseGitHubConnectorType(give, "test")
 		must.Eq(t, want.err, err)
 		must.Eq(t, want.val, have)
 	}
diff --git a/internal/forge/forgedomain/proposal_data_test.go b/internal/forge/forgedomain/proposal_data_test.go
index d81006f5c..93ca0cef5 100644
--- a/internal/forge/forgedomain/proposal_data_test.go
+++ b/internal/forge/forgedomain/proposal_data_test.go
@@ -17,6 +17,7 @@ func TestBitbucketCloudProposalData(t *testing.T) {
 		t.Parallel()
 		data := forgedomain.BitbucketCloudProposalData{
 			ProposalData: forgedomain.ProposalData{
+				Active:       true,
 				Body:         Some("body"),
 				MergeWithAPI: true,
 				Number:       123,
@@ -33,6 +34,7 @@ func TestBitbucketCloudProposalData(t *testing.T) {
 		fmt.Println(string(serialized))
 		want := `
 {
+  "Active": true,
   "Body": "body",
   "MergeWithAPI": true,
   "Number": 123,
diff --git a/internal/forge/forgedomain/proposals_show_lineage_test.go b/internal/forge/forgedomain/proposals_show_lineage_test.go
index 4d32291cb..5afb245c5 100644
--- a/internal/forge/forgedomain/proposals_show_lineage_test.go
+++ b/internal/forge/forgedomain/proposals_show_lineage_test.go
@@ -82,7 +82,7 @@ func TestParseProposalsShowLineage(t *testing.T) {
 		},
 	}
 	for _, tt := range tests {
-		have, err := forgedomain.ParseProposalsShowLineage(tt.give)
+		have, err := forgedomain.ParseProposalsShowLineage(tt.give, "test")
 		must.EqOp(t, err != nil, tt.err)
 		must.True(t, have.Equal(tt.want))
 	}
diff --git a/internal/forge/forgejo/test_connector.go b/internal/forge/forgejo/test_connector.go
index e47ee1e43..b0ce4fedd 100644
--- a/internal/forge/forgejo/test_connector.go
+++ b/internal/forge/forgejo/test_connector.go
@@ -35,6 +35,7 @@ func (self TestConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 	}
 	return Some(forgedomain.Proposal{
 		Data: forgedomain.ProposalData{
+			Active:       true,
 			Body:         None[string](),
 			MergeWithAPI: true,
 			Number:       123,
diff --git a/internal/forge/gh/parser_test.go b/internal/forge/gh/parser_test.go
index 0e4a870f1..e6194d471 100644
--- a/internal/forge/gh/parser_test.go
+++ b/internal/forge/gh/parser_test.go
@@ -15,7 +15,7 @@ func TestParseJSONOutput(t *testing.T) {
 	t.Run("invalid JSON", func(t *testing.T) {
 		t.Parallel()
 		give := `[zonk`
-		_, err := gh.ParseJSONOutput(give, "branch")
+		_, err := gh.ParseJSONOutput(give)
 		must.Error(t, err)
 	})
 
@@ -42,16 +42,16 @@ func TestParseJSONOutput(t *testing.T) {
     "url": "https://github.com/git-town/git-town/pull/4871"
   }
 ]`
-		_, err := gh.ParseJSONOutput(give, "branch")
-		must.Error(t, err)
+		_, err := gh.ParseJSONOutput(give)
+		must.NoError(t, err)
 	})
 
 	t.Run("no results", func(t *testing.T) {
 		t.Parallel()
 		give := `[]`
-		have, err := gh.ParseJSONOutput(give, "branch")
+		have, err := gh.ParseJSONOutput(give)
 		must.NoError(t, err)
-		must.Eq(t, None[forgedomain.Proposal](), have)
+		must.Eq(t, []forgedomain.Proposal{}, have)
 	})
 
 	t.Run("single result", func(t *testing.T) {
@@ -68,20 +68,22 @@ func TestParseJSONOutput(t *testing.T) {
     "url": "https://github.com/git-town/git-town/pull/5079"
   }
 ]`
-		have, err := gh.ParseJSONOutput(give, "branch")
+		have, err := gh.ParseJSONOutput(give)
 		must.NoError(t, err)
-		want := Some(forgedomain.Proposal{
-			Data: forgedomain.ProposalData{
-				Body:         Some("GitLab also provides a CLI app. This PR adds support for it similar to GitHub.\n"),
-				MergeWithAPI: true,
-				Number:       5079,
-				Source:       "kg-glab",
-				Target:       "main",
-				Title:        "glab connector type",
-				URL:          "https://github.com/git-town/git-town/pull/5079",
+		want := []forgedomain.Proposal{
+			{
+				Data: forgedomain.ProposalData{
+					Body:         Some("GitLab also provides a CLI app. This PR adds support for it similar to GitHub.\n"),
+					MergeWithAPI: true,
+					Number:       5079,
+					Source:       "kg-glab",
+					Target:       "main",
+					Title:        "glab connector type",
+					URL:          "https://github.com/git-town/git-town/pull/5079",
+				},
+				ForgeType: forgedomain.ForgeTypeGitHub,
 			},
-			ForgeType: forgedomain.ForgeTypeGitHub,
-		})
+		}
 		must.Eq(t, want, have)
 	})
 }
diff --git a/internal/forge/gitea/test_connector.go b/internal/forge/gitea/test_connector.go
index 83b84aba2..c3bb06fbb 100644
--- a/internal/forge/gitea/test_connector.go
+++ b/internal/forge/gitea/test_connector.go
@@ -29,6 +29,7 @@ func (self TestConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 	}
 	return Some(forgedomain.Proposal{
 		Data: forgedomain.ProposalData{
+			Active:       true,
 			Body:         None[string](),
 			MergeWithAPI: true,
 			Number:       123,
diff --git a/internal/forge/github/test_connector.go b/internal/forge/github/test_connector.go
index e3f9f1cd5..d6af74bdf 100644
--- a/internal/forge/github/test_connector.go
+++ b/internal/forge/github/test_connector.go
@@ -35,6 +35,7 @@ func (self TestConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 	}
 	return Some(forgedomain.Proposal{
 		Data: forgedomain.ProposalData{
+			Active:       true,
 			Body:         None[string](),
 			MergeWithAPI: true,
 			Number:       123,
diff --git a/internal/forge/gitlab/test_connector.go b/internal/forge/gitlab/test_connector.go
index 147cb5ec7..11d580f77 100644
--- a/internal/forge/gitlab/test_connector.go
+++ b/internal/forge/gitlab/test_connector.go
@@ -35,6 +35,7 @@ func (self TestConnector) FindProposal(branch, target gitdomain.LocalBranchName)
 	}
 	return Some(forgedomain.Proposal{
 		Data: forgedomain.ProposalData{
+			Active:       true,
 			Body:         None[string](),
 			MergeWithAPI: true,
 			Number:       123,
diff --git a/internal/forge/glab/parser_test.go b/internal/forge/glab/parser_test.go
index 21e9ca50e..b14ac3697 100644
--- a/internal/forge/glab/parser_test.go
+++ b/internal/forge/glab/parser_test.go
@@ -15,7 +15,7 @@ func TestParseJsonOutput(t *testing.T) {
 	t.Run("invalid JSON", func(t *testing.T) {
 		t.Parallel()
 		give := `[zonk`
-		_, err := glab.ParseJSONOutput(give, "branch")
+		_, err := glab.ParseJSONOutput(give)
 		must.Error(t, err)
 	})
 
@@ -23,37 +23,42 @@ func TestParseJsonOutput(t *testing.T) {
 		t.Parallel()
 		give := `[
 		  {"id":394611593,"iid":5,"target_branch":"main","source_branch":"kg-test","project_id":61831152,"title":"foo","state":"opened","imported":false,"imported_from":"none","created_at":"2025-06-27T20:47:24.4Z","updated_at":"2025-06-27T20:47:25.483Z","upvotes":0,"downvotes":0,"author":{"id":22930529,"username":"kev.lar","name":"Kevin Goslar","state":"active","locked":false,"created_at":null,"avatar_url":"https://secure.gravatar.com/avatar/4905ffb1f10f3d653f25087d324657059267357bfd075294e1fda5c6c63c9f5f?s=80\u0026d=identicon","web_url":"https://gitlab.com/kev.lar"},"assignee":null,"assignees":[],"reviewers":[],"source_project_id":61831152,"target_project_id":61831152,"labels":[],"label_details":null,"description":"","draft":false,"milestone":null,"merge_when_pipeline_succeeds":false,"detailed_merge_status":"mergeable","merge_user":null,"merged_at":null,"merge_after":null,"prepared_at":"2025-06-27T20:47:25.477Z","closed_by":null,"closed_at":null,"sha":"b2b4af2c973ed3165f8561e6bc2d6eed43aea0b2","merge_commit_sha":"","squash_commit_sha":"","user_notes_count":0,"should_remove_source_branch":false,"force_remove_source_branch":true,"allow_collaboration":false,"allow_maintainer_to_push":false,"web_url":"https://gitlab.com/git-town-qa/test-repo/-/merge_requests/5","references":{"short":"!5","relative":"!5","full":"git-town-qa/test-repo!5"},"discussion_locked":false,"time_stats":{"human_time_estimate":"","human_total_time_spent":"","time_estimate":0,"total_time_spent":0},"squash":false,"squash_on_merge":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"merged_by":null},
-			{"id":328691588,"iid":1,"target_branch":"main","source_branch":"branch-1","project_id":61831152,"title":"Changes in branch 1","state":"opened","imported":false,"imported_from":"none","created_at":"2024-09-19T15:27:48.828Z","updated_at":"2025-05-13T15:37:47.839Z","upvotes":0,"downvotes":0,"author":{"id":22930529,"username":"kev.lar","name":"Kevin Goslar","state":"active","locked":false,"created_at":null,"avatar_url":"https://secure.gravatar.com/avatar/4905ffb1f10f3d653f25087d324657059267357bfd075294e1fda5c6c63c9f5f?s=80\u0026d=identicon","web_url":"https://gitlab.com/kev.lar"},"assignee":null,"assignees":[],"reviewers":[],"source_project_id":61831152,"target_project_id":61831152,"labels":[],"label_details":null,"description":"This is the proposal body text.","draft":false,"milestone":null,"merge_when_pipeline_succeeds":false,"detailed_merge_status":"mergeable","merge_user":null,"merged_at":null,"merge_after":null,"prepared_at":"2024-09-19T15:27:50.592Z","closed_by":null,"closed_at":null,"sha":"5d3b08006a05b1c75879af8f074775686e49447c","merge_commit_sha":"","squash_commit_sha":"","user_notes_count":0,"should_remove_source_branch":false,"force_remove_source_branch":true,"allow_collaboration":false,"allow_maintainer_to_push":false,"web_url":"https://gitlab.com/git-town-qa/test-repo/-/merge_requests/1","references":{"short":"!1","relative":"!1","full":"git-town-qa/test-repo!1"},"discussion_locked":false,"time_stats":{"human_time_estimate":"","human_total_time_spent":"","time_estimate":0,"total_time_spent":0},"squash":false,"squash_on_merge":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"merged_by":null},
+			{"id":328691588,"iid":1,"target_branch":"main","source_branch":"branch-1","project_id":61831152,"title":"Changes in branch 1","state":"opened","imported":false,"imported_from":"none","created_at":"2024-09-19T15:27:48.828Z","updated_at":"2025-05-13T15:37:47.839Z","upvotes":0,"downvotes":0,"author":{"id":22930529,"username":"kev.lar","name":"Kevin Goslar","state":"active","locked":false,"created_at":null,"avatar_url":"https://secure.gravatar.com/avatar/4905ffb1f10f3d653f25087d324657059267357bfd075294e1fda5c6c63c9f5f?s=80\u0026d=identicon","web_url":"https://gitlab.com/kev.lar"},"assignee":null,"assignees":[],"reviewers":[],"source_project_id":61831152,"target_project_id":61831152,"labels":[],"label_details":null,"description":"This is the proposal body text.","draft":false,"milestone":null,"merge_when_pipeline_succeeds":false,"detailed_merge_status":"mergeable","merge_user":null,"merged_at":null,"merge_after":null,"prepared_at":"2024-09-19T15:27:50.592Z","closed_by":null,"closed_at":null,"sha":"5d3b08006a05b1c75879af8f074775686e49447c","merge_commit_sha":"","squash_commit_sha":"","user_notes_count":0,"should_remove_source_branch":false,"force_remove_source_branch":true,"allow_collaboration":false,"allow_maintainer_to_push":false,"web_url":"https://gitlab.com/git-town-qa/test-repo/-/merge_requests/1","references":{"short":"!1","relative":"!1","full":"git-town-qa/test-repo!1"},"discussion_locked":false,"time_stats":{"human_time_estimate":"","human_total_time_spent":"","time_estimate":0,"total_time_spent":0},"squash":false,"squash_on_merge":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"merged_by":null}
 		]`
-		_, err := glab.ParseJSONOutput(give, "branch")
-		must.Error(t, err)
+		have, err := glab.ParseJSONOutput(give)
+		must.NoError(t, err)
+		must.Len(t, 2, have)
+		must.EqOp(t, have[0].Data.Data().Number, 5)
+		must.EqOp(t, have[1].Data.Data().Number, 1)
 	})
 
 	t.Run("no results", func(t *testing.T) {
 		t.Parallel()
 		give := `[]`
-		have, err := glab.ParseJSONOutput(give, "branch")
+		have, err := glab.ParseJSONOutput(give)
 		must.NoError(t, err)
-		must.Eq(t, None[forgedomain.Proposal](), have)
+		must.Eq(t, []forgedomain.Proposal{}, have)
 	})
 
 	t.Run("single result", func(t *testing.T) {
 		t.Parallel()
 		give := `[{"id":394611593,"iid":5,"target_branch":"main","source_branch":"kg-test","project_id":61831152,"title":"foo","state":"opened","imported":false,"imported_from":"none","created_at":"2025-06-27T20:47:24.4Z","updated_at":"2025-06-27T20:47:25.483Z","upvotes":0,"downvotes":0,"author":{"id":22930529,"username":"kev.lar","name":"Kevin Goslar","state":"active","locked":false,"created_at":null,"avatar_url":"https://secure.gravatar.com/avatar/4905ffb1f10f3d653f25087d324657059267357bfd075294e1fda5c6c63c9f5f?s=80\u0026d=identicon","web_url":"https://gitlab.com/kev.lar"},"assignee":null,"assignees":[],"reviewers":[],"source_project_id":61831152,"target_project_id":61831152,"labels":[],"label_details":null,"description":"","draft":false,"milestone":null,"merge_when_pipeline_succeeds":false,"detailed_merge_status":"mergeable","merge_user":null,"merged_at":null,"merge_after":null,"prepared_at":"2025-06-27T20:47:25.477Z","closed_by":null,"closed_at":null,"sha":"b2b4af2c973ed3165f8561e6bc2d6eed43aea0b2","merge_commit_sha":"","squash_commit_sha":"","user_notes_count":0,"should_remove_source_branch":false,"force_remove_source_branch":true,"allow_collaboration":false,"allow_maintainer_to_push":false,"web_url":"https://gitlab.com/git-town-qa/test-repo/-/merge_requests/5","references":{"short":"!5","relative":"!5","full":"git-town-qa/test-repo!5"},"discussion_locked":false,"time_stats":{"human_time_estimate":"","human_total_time_spent":"","time_estimate":0,"total_time_spent":0},"squash":false,"squash_on_merge":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"merged_by":null}]`
-		have, err := glab.ParseJSONOutput(give, "branch")
+		have, err := glab.ParseJSONOutput(give)
 		must.NoError(t, err)
-		want := Some(forgedomain.Proposal{
-			Data: forgedomain.ProposalData{
-				Body:         None[string](),
-				MergeWithAPI: true,
-				Number:       5,
-				Source:       "kg-test",
-				Target:       "main",
-				Title:        "foo",
-				URL:          "https://gitlab.com/git-town-qa/test-repo/-/merge_requests/5",
+		want := []forgedomain.Proposal{
+			{
+				Data: forgedomain.ProposalData{
+					Body:         None[string](),
+					MergeWithAPI: true,
+					Number:       5,
+					Source:       "kg-test",
+					Target:       "main",
+					Title:        "foo",
+					URL:          "https://gitlab.com/git-town-qa/test-repo/-/merge_requests/5",
+				},
+				ForgeType: forgedomain.ForgeTypeGitLab,
 			},
-			ForgeType: forgedomain.ForgeTypeGitLab,
-		})
+		}
 		must.Eq(t, want, have)
 	})
 }
diff --git a/internal/forge/proposal_stack_lineage_builder_test.go b/internal/forge/proposal_stack_lineage_builder_test.go
index 37bd57f40..a33354eb6 100644
--- a/internal/forge/proposal_stack_lineage_builder_test.go
+++ b/internal/forge/proposal_stack_lineage_builder_test.go
@@ -52,10 +52,10 @@ func TestProposalStackLineageBuilder_CheckLineageAndProposals(t *testing.T) {
 	})
 	var connector forgedomain.ProposalFinder = &mockProposalFinder{}
 	args := forge.ProposalStackLineageArgs{
-		Connector:                connector,
+		Connector:                Some(connector),
 		CurrentBranch:            featureBranchA,
 		Lineage:                  lineage,
-		MainAndPerennialBranches: gitdomain.NewLocalBranchNames(mainBranch.String()),
+		MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
 	}
 	tree, err := forge.NewProposalStackLineageTree(args)
 	must.NoError(t, err)
@@ -69,7 +69,7 @@ func TestProposalStackLineageBuilder_CheckLineageAndProposals(t *testing.T) {
    - https://www.github.com/git-town/pull/97 :point_left:
      - https://www.github.com/git-town/pull/98
 
-Stack generated by [Git Town](https://github.com/git-town/git-town)
+<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
 `[1:]
 
 	// act
@@ -119,10 +119,10 @@ func TestProposalStackLineageBuilder_CurrentBranchHasMultipleDirectDescendants(t
 		t.Parallel()
 		var connector forgedomain.ProposalFinder = &mockProposalFinder{}
 		args := forge.ProposalStackLineageArgs{
-			Connector:                connector,
+			Connector:                Some(connector),
 			CurrentBranch:            featureBranchA,
 			Lineage:                  lineage,
-			MainAndPerennialBranches: gitdomain.NewLocalBranchNames(mainBranch.String()),
+			MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
 		}
 		want := `
 -------------------------
@@ -132,7 +132,7 @@ func TestProposalStackLineageBuilder_CurrentBranchHasMultipleDirectDescendants(t
        - https://www.github.com/git-town/pull/99
      - https://www.github.com/git-town/pull/100
 
-Stack generated by [Git Town](https://github.com/git-town/git-town)
+<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
 `
 		have := forge.NewProposalStackLineageBuilder(args, MutableNone[forge.ProposalStackLineageTree]())
 		builder, hasBuilder := have.Get()
@@ -147,10 +147,10 @@ Stack generated by [Git Town](https://github.com/git-town/git-town)
 		t.Parallel()
 		var connector forgedomain.ProposalFinder = &mockProposalFinder{}
 		args := forge.ProposalStackLineageArgs{
-			Connector:                connector,
+			Connector:                Some(connector),
 			CurrentBranch:            featureBranchB,
 			Lineage:                  lineage,
-			MainAndPerennialBranches: gitdomain.NewLocalBranchNames(mainBranch.String()),
+			MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
 		}
 		want := `
 -------------------------
@@ -160,7 +160,7 @@ Stack generated by [Git Town](https://github.com/git-town/git-town)
        - https://www.github.com/git-town/pull/99
      - https://www.github.com/git-town/pull/100
 
-Stack generated by [Git Town](https://github.com/git-town/git-town)
+<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
 `
 		have := forge.NewProposalStackLineageBuilder(args, MutableNone[forge.ProposalStackLineageTree]())
 		builder, hasBuilder := have.Get()
@@ -175,10 +175,10 @@ Stack generated by [Git Town](https://github.com/git-town/git-town)
 		t.Parallel()
 		var connector forgedomain.ProposalFinder = &mockProposalFinder{}
 		args := forge.ProposalStackLineageArgs{
-			Connector:                connector,
+			Connector:                Some(connector),
 			CurrentBranch:            featureBranchC,
 			Lineage:                  lineage,
-			MainAndPerennialBranches: gitdomain.NewLocalBranchNames(mainBranch.String()),
+			MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
 		}
 		want := `
 -------------------------
@@ -188,7 +188,7 @@ Stack generated by [Git Town](https://github.com/git-town/git-town)
        - https://www.github.com/git-town/pull/99 :point_left:
      - https://www.github.com/git-town/pull/100
 
-Stack generated by [Git Town](https://github.com/git-town/git-town)
+<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
 `
 		have := forge.NewProposalStackLineageBuilder(args, MutableNone[forge.ProposalStackLineageTree]())
 		builder, hasBuilder := have.Get()
@@ -203,10 +203,10 @@ Stack generated by [Git Town](https://github.com/git-town/git-town)
 		t.Parallel()
 		var connector forgedomain.ProposalFinder = &mockProposalFinder{}
 		args := forge.ProposalStackLineageArgs{
-			Connector:                connector,
+			Connector:                Some(connector),
 			CurrentBranch:            featureBranchD,
 			Lineage:                  lineage,
-			MainAndPerennialBranches: gitdomain.NewLocalBranchNames(mainBranch.String()),
+			MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
 		}
 		want := `
 -------------------------
@@ -216,7 +216,7 @@ Stack generated by [Git Town](https://github.com/git-town/git-town)
      - https://www.github.com/git-town/pull/98
        - https://www.github.com/git-town/pull/99
 
-Stack generated by [Git Town](https://github.com/git-town/git-town)
+<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
 `
 		have := forge.NewProposalStackLineageBuilder(args, MutableNone[forge.ProposalStackLineageTree]())
 		builder, hasBuilder := have.Get()
@@ -231,10 +231,10 @@ Stack generated by [Git Town](https://github.com/git-town/git-town)
 		t.Parallel()
 		var connector forgedomain.ProposalFinder = &mockProposalFinder{}
 		args := forge.ProposalStackLineageArgs{
-			Connector:                connector,
+			Connector:                Some(connector),
 			CurrentBranch:            featureBranchE,
 			Lineage:                  lineage,
-			MainAndPerennialBranches: gitdomain.NewLocalBranchNames(mainBranch.String()),
+			MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
 		}
 		want := `
 -------------------------
@@ -242,7 +242,7 @@ Stack generated by [Git Town](https://github.com/git-town/git-town)
    - https://www.github.com/git-town/pull/101 :point_left:
      - https://www.github.com/git-town/pull/102
 
-Stack generated by [Git Town](https://github.com/git-town/git-town)
+<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
 `
 		have := forge.NewProposalStackLineageBuilder(args, MutableNone[forge.ProposalStackLineageTree]())
 		builder, hasBuilder := have.Get()
@@ -257,10 +257,10 @@ Stack generated by [Git Town](https://github.com/git-town/git-town)
 		t.Parallel()
 		var connector forgedomain.ProposalFinder = &mockProposalFinder{}
 		args := forge.ProposalStackLineageArgs{
-			Connector:                connector,
+			Connector:                Some(connector),
 			CurrentBranch:            featureBranchF,
 			Lineage:                  lineage,
-			MainAndPerennialBranches: gitdomain.NewLocalBranchNames(mainBranch.String()),
+			MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
 		}
 		want := `
 -------------------------
@@ -268,7 +268,7 @@ Stack generated by [Git Town](https://github.com/git-town/git-town)
    - https://www.github.com/git-town/pull/101
      - https://www.github.com/git-town/pull/102 :point_left:
 
-Stack generated by [Git Town](https://github.com/git-town/git-town)
+<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
 `
 		have := forge.NewProposalStackLineageBuilder(args, MutableNone[forge.ProposalStackLineageTree]())
 		builder, hasBuilder := have.Get()
@@ -286,7 +286,7 @@ func TestProposalStackLineageBuilder_NoLineage(t *testing.T) {
 	lineage := configdomain.NewLineageWith(configdomain.LineageData{})
 	var connector forgedomain.ProposalFinder = &mockProposalFinder{}
 	args := forge.ProposalStackLineageArgs{
-		Connector:                connector,
+		Connector:                Some(connector),
 		CurrentBranch:            gitdomain.NewLocalBranchName("main"),
 		Lineage:                  lineage,
 		MainAndPerennialBranches: lineage.Roots(),
@@ -310,7 +310,7 @@ func TestProposalStackLineageBuilder_NoLineageForMainAndPerennialBranches(t *tes
 	})
 	var connector forgedomain.ProposalFinder = &mockProposalFinder{}
 	args := forge.ProposalStackLineageArgs{
-		Connector:                connector,
+		Connector:                Some(connector),
 		CurrentBranch:            mainBranch,
 		Lineage:                  lineage,
 		MainAndPerennialBranches: lineage.Roots(),
@@ -375,10 +375,10 @@ func TestProposalStackLineageBuilder_PrependBranch(t *testing.T) {
 	})
 	var connector forgedomain.ProposalFinder = &mockProposalFinder{}
 	args := forge.ProposalStackLineageArgs{
-		Connector:                connector,
+		Connector:                Some(connector),
 		CurrentBranch:            featureBranchA,
 		Lineage:                  lineage,
-		MainAndPerennialBranches: gitdomain.NewLocalBranchNames(mainBranch.String()),
+		MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
 	}
 
 	tree, err := forge.NewProposalStackLineageTree(args)
@@ -393,7 +393,7 @@ func TestProposalStackLineageBuilder_PrependBranch(t *testing.T) {
      - https://www.github.com/git-town/pull/98
        - https://www.github.com/git-town/pull/100
 
-Stack generated by [Git Town](https://github.com/git-town/git-town)
+<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
 `[1:]
 	must.EqOp(t, original, builder.Build(args))
 
@@ -412,10 +412,10 @@ Stack generated by [Git Town](https://github.com/git-town/git-town)
 	must.True(t, lineage.Parent(featureBranchD).EqualSome(featureBranchC))
 
 	args = forge.ProposalStackLineageArgs{
-		Connector:                connector,
+		Connector:                Some(connector),
 		CurrentBranch:            featureBranchA,
 		Lineage:                  lineage,
-		MainAndPerennialBranches: gitdomain.NewLocalBranchNames(mainBranch.String()),
+		MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
 	}
 	tree, err = forge.NewProposalStackLineageTree(args)
 	must.NoError(t, err)
@@ -436,10 +436,10 @@ func TestProposalStackLineageBuilder_SwapBranch(t *testing.T) {
 	})
 	var connector forgedomain.ProposalFinder = &mockProposalFinder{}
 	args := forge.ProposalStackLineageArgs{
-		Connector:                connector,
+		Connector:                Some(connector),
 		CurrentBranch:            featureBranchA,
 		Lineage:                  lineage,
-		MainAndPerennialBranches: gitdomain.NewLocalBranchNames(mainBranch.String()),
+		MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
 	}
 
 	tree, err := forge.NewProposalStackLineageTree(args)
@@ -453,7 +453,7 @@ func TestProposalStackLineageBuilder_SwapBranch(t *testing.T) {
    - https://www.github.com/git-town/pull/97 :point_left:
      - https://www.github.com/git-town/pull/98
 
-Stack generated by [Git Town](https://github.com/git-town/git-town)
+<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
 `[1:]
 	must.EqOp(t, original, builder.Build(args))
 
@@ -472,7 +472,7 @@ Stack generated by [Git Town](https://github.com/git-town/git-town)
    - https://www.github.com/git-town/pull/98
      - https://www.github.com/git-town/pull/97 :point_left:
 
-Stack generated by [Git Town](https://github.com/git-town/git-town)
+<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
 `[1:]
 
 	// act
diff --git a/internal/git/commands_test.go b/internal/git/commands_test.go
index 5ae1e4830..8fd7b5589 100644
--- a/internal/git/commands_test.go
+++ b/internal/git/commands_test.go
@@ -355,6 +355,58 @@ func TestBackendCommands(t *testing.T) {
 		})
 	})
 
+	t.Run("BranchesAvailableInCurrentWorktree", func(t *testing.T) {
+		t.Parallel()
+		t.Run("includes branches not checked out anywhere", func(t *testing.T) {
+			t.Parallel()
+			runtime := testruntime.Create(t)
+			runtime.CreateBranch("branch1", initial.BranchName())
+			runtime.CreateBranch("branch2", initial.BranchName())
+			runtime.CheckoutBranch("branch1")
+			available := asserts.NoError1(runtime.Git.BranchesAvailableInCurrentWorktree(runtime))
+			must.SliceContainsAll(t, gitdomain.LocalBranchNames{
+				gitdomain.NewLocalBranchName("initial"),
+				gitdomain.NewLocalBranchName("branch1"),
+				gitdomain.NewLocalBranchName("branch2"),
+			}, available)
+		})
+
+		t.Run("excludes branch checked out in another worktree", func(t *testing.T) {
+			t.Parallel()
+			runtime := testruntime.Create(t)
+			runtime.CreateBranch("branch1", initial.BranchName())
+			runtime.CreateBranch("branch2", initial.BranchName())
+			worktreeDir := t.TempDir()
+			runtime.AddWorktree(worktreeDir, "branch1")
+			available := asserts.NoError1(runtime.Git.BranchesAvailableInCurrentWorktree(runtime))
+			must.SliceContainsAll(t, gitdomain.LocalBranchNames{
+				gitdomain.NewLocalBranchName("initial"),
+				gitdomain.NewLocalBranchName("branch2"),
+			}, available)
+		})
+
+		t.Run("excludes remote branches", func(t *testing.T) {
+			t.Parallel()
+			origin := testruntime.Create(t)
+			local := testruntime.Clone(origin.TestRunner, t.TempDir())
+			origin.CreateBranch("remote-branch", initial.BranchName())
+			local.Fetch()
+			available := asserts.NoError1(local.Git.BranchesAvailableInCurrentWorktree(local))
+			must.SliceContainsAll(t, gitdomain.LocalBranchNames{
+				gitdomain.NewLocalBranchName("initial"),
+			}, available)
+		})
+
+		t.Run("empty repository", func(t *testing.T) {
+			t.Parallel()
+			runtime := testruntime.Create(t)
+			available := asserts.NoError1(runtime.Git.BranchesAvailableInCurrentWorktree(runtime))
+			must.SliceContainsAll(t, gitdomain.LocalBranchNames{
+				gitdomain.NewLocalBranchName("initial"),
+			}, available)
+		})
+	})
+
 	t.Run("BranchesSnapshot", func(t *testing.T) {
 		t.Parallel()
 		t.Run("recognizes the active branch names", func(t *testing.T) {
@@ -661,7 +713,7 @@ func TestBackendCommands(t *testing.T) {
 				runtime := testruntime.Create(t)
 				runtime.CreateBranch("branch", initial.BranchName())
 				worktreeDir := t.TempDir()
-				runtime.CreateWorktree(worktreeDir, "branch")
+				runtime.AddWorktree(worktreeDir, "branch")
 				commits := asserts.NoError1(runtime.Git.CommitsInBranch(runtime, initial, None[gitdomain.LocalBranchName]()))
 				want := gitdomain.BranchesSnapshot{
 					Active: Some[gitdomain.LocalBranchName]("initial"),
diff --git a/internal/git/gitdomain/branch_infos_test.go b/internal/git/gitdomain/branch_infos_test.go
index 77d0ab353..5459ff4e5 100644
--- a/internal/git/gitdomain/branch_infos_test.go
+++ b/internal/git/gitdomain/branch_infos_test.go
@@ -63,6 +63,131 @@ func TestBranchInfos(t *testing.T) {
 		})
 	})
 
+	t.Run("BranchesInOtherWorktrees", func(t *testing.T) {
+		t.Parallel()
+		t.Run("empty BranchInfos", func(t *testing.T) {
+			t.Parallel()
+			branchInfos := gitdomain.BranchInfos{}
+			have := branchInfos.BranchesInOtherWorktrees()
+			want := gitdomain.LocalBranchNames{}
+			must.Eq(t, want, have)
+		})
+		t.Run("no branches in other worktrees", func(t *testing.T) {
+			t.Parallel()
+			branchInfos := gitdomain.BranchInfos{
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-1"),
+					SyncStatus: gitdomain.SyncStatusLocalOnly,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-2"),
+					SyncStatus: gitdomain.SyncStatusUpToDate,
+					RemoteName: Some(gitdomain.NewRemoteBranchName("origin/branch-2")),
+				},
+			}
+			have := branchInfos.BranchesInOtherWorktrees()
+			want := gitdomain.LocalBranchNames{}
+			must.Eq(t, want, have)
+		})
+		t.Run("one branch in another worktree with local name", func(t *testing.T) {
+			t.Parallel()
+			branchInfos := gitdomain.BranchInfos{
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-1"),
+					SyncStatus: gitdomain.SyncStatusLocalOnly,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-2"),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+			}
+			have := branchInfos.BranchesInOtherWorktrees()
+			want := gitdomain.NewLocalBranchNames("branch-2")
+			must.Eq(t, want, have)
+		})
+		t.Run("one branch in another worktree with remote name only", func(t *testing.T) {
+			t.Parallel()
+			branchInfos := gitdomain.BranchInfos{
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-1"),
+					SyncStatus: gitdomain.SyncStatusLocalOnly,
+				},
+				{
+					LocalName:  None[gitdomain.LocalBranchName](),
+					RemoteName: Some(gitdomain.NewRemoteBranchName("origin/branch-2")),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+			}
+			have := branchInfos.BranchesInOtherWorktrees()
+			want := gitdomain.NewLocalBranchNames("branch-2")
+			must.Eq(t, want, have)
+		})
+		t.Run("multiple branches in other worktrees", func(t *testing.T) {
+			t.Parallel()
+			branchInfos := gitdomain.BranchInfos{
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-1"),
+					SyncStatus: gitdomain.SyncStatusLocalOnly,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-2"),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-3"),
+					SyncStatus: gitdomain.SyncStatusUpToDate,
+					RemoteName: Some(gitdomain.NewRemoteBranchName("origin/branch-3")),
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-4"),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+				{
+					LocalName:  None[gitdomain.LocalBranchName](),
+					RemoteName: Some(gitdomain.NewRemoteBranchName("origin/branch-5")),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+			}
+			have := branchInfos.BranchesInOtherWorktrees()
+			want := gitdomain.NewLocalBranchNames("branch-2", "branch-4", "branch-5")
+			must.Eq(t, want, have)
+		})
+		t.Run("mixed statuses including other worktrees", func(t *testing.T) {
+			t.Parallel()
+			branchInfos := gitdomain.BranchInfos{
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("local-only"),
+					SyncStatus: gitdomain.SyncStatusLocalOnly,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("up-to-date"),
+					SyncStatus: gitdomain.SyncStatusUpToDate,
+					RemoteName: Some(gitdomain.NewRemoteBranchName("origin/up-to-date")),
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("other-worktree-1"),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("ahead"),
+					SyncStatus: gitdomain.SyncStatusAhead,
+					RemoteName: Some(gitdomain.NewRemoteBranchName("origin/ahead")),
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("other-worktree-2"),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("deleted-at-remote"),
+					SyncStatus: gitdomain.SyncStatusDeletedAtRemote,
+				},
+			}
+			have := branchInfos.BranchesInOtherWorktrees()
+			want := gitdomain.NewLocalBranchNames("other-worktree-1", "other-worktree-2")
+			must.Eq(t, want, have)
+		})
+	})
+
 	t.Run("FindByRemote", func(t *testing.T) {
 		t.Parallel()
 		t.Run("has a local branch with matching tracking branch", func(t *testing.T) {
diff --git a/internal/git/gitdomain/commit_message_test.go b/internal/git/gitdomain/commit_message_test.go
index 7e267016f..aaa889bf6 100644
--- a/internal/git/gitdomain/commit_message_test.go
+++ b/internal/git/gitdomain/commit_message_test.go
@@ -14,30 +14,30 @@ func TestCommitMessage(t *testing.T) {
 		t.Parallel()
 		tests := map[gitdomain.CommitMessage]gitdomain.CommitMessageParts{
 			"title": {
-				Subject: "title",
-				Text:    "",
+				Title: "title",
+				Body:  "",
 			},
 			"title\nbody": {
-				Subject: "title",
-				Text:    "body",
+				Title: "title",
+				Body:  "body",
 			},
 			"title\n\nbody": {
-				Subject: "title",
-				Text:    "body",
+				Title: "title",
+				Body:  "body",
 			},
 			"title\n\n\nbody": {
-				Subject: "title",
-				Text:    "body",
+				Title: "title",
+				Body:  "body",
 			},
 			"title\nbody1\nbody2\n": {
-				Subject: "title",
-				Text:    "body1\nbody2\n",
+				Title: "title",
+				Body:  "body1\nbody2\n",
 			},
 		}
 		for give, want := range tests {
 			have := give.Parts()
-			must.EqOp(t, want.Subject, have.Subject)
-			must.EqOp(t, want.Text, have.Text)
+			must.EqOp(t, want.Title, have.Title)
+			must.EqOp(t, want.Body, have.Body)
 		}
 	})
 }
diff --git a/internal/git/gitdomain/local_branch_names_test.go b/internal/git/gitdomain/local_branch_names_test.go
index ebf18fe51..fa5201ad0 100644
--- a/internal/git/gitdomain/local_branch_names_test.go
+++ b/internal/git/gitdomain/local_branch_names_test.go
@@ -46,14 +46,14 @@ func TestLocalBranchNames(t *testing.T) {
 
 	t.Run("Hoist", func(t *testing.T) {
 		t.Parallel()
-		t.Run("haystack contains needle", func(t *testing.T) {
+		t.Run("haystack contains needles", func(t *testing.T) {
 			t.Parallel()
-			branches := gitdomain.NewLocalBranchNames("one", "two", "three")
-			have := branches.Hoist("two")
-			want := gitdomain.NewLocalBranchNames("two", "one", "three")
+			branches := gitdomain.NewLocalBranchNames("one", "two", "three", "four")
+			have := branches.Hoist("four", "two")
+			want := gitdomain.NewLocalBranchNames("four", "two", "one", "three")
 			must.Eq(t, want, have)
 		})
-		t.Run("haystack does not contain needle", func(t *testing.T) {
+		t.Run("haystack does not contain needles", func(t *testing.T) {
 			t.Parallel()
 			branches := gitdomain.NewLocalBranchNames("one", "two", "three")
 			have := branches.Hoist("zonk")
diff --git a/internal/gohacks/strings_test.go b/internal/gohacks/strings_test.go
index 82b084316..46840b8d6 100644
--- a/internal/gohacks/strings_test.go
+++ b/internal/gohacks/strings_test.go
@@ -47,3 +47,19 @@ func TestIndent(t *testing.T) {
 		must.EqOp(t, want, have)
 	})
 }
+
+func TestLeadingWhitespace(t *testing.T) {
+	t.Parallel()
+	tests := map[string]string{
+		"    text":   "    ",   // spaces
+		"\t\ttext":   "\t\t",   // tabs
+		"  \t  text": "  \t  ", // mixed spaces and tabs
+		"text":       "",       // no indentation
+		"":           "",       // empty string
+		"    ":       "    ",   // only whitespace
+	}
+	for give, want := range tests {
+		have := gohacks.LeadingWhitespace(give)
+		must.EqOp(t, want, have)
+	}
+}
diff --git a/internal/gohacks/stringslice/change_indent_non_empty_test.go b/internal/gohacks/stringslice/change_indent_non_empty_test.go
new file mode 100644
index 000000000..ae252b18f
--- /dev/null
+++ b/internal/gohacks/stringslice/change_indent_non_empty_test.go
@@ -0,0 +1,56 @@
+package stringslice_test
+
+import (
+	"testing"
+
+	"github.com/git-town/git-town/v22/internal/gohacks/stringslice"
+	"github.com/shoenig/test/must"
+)
+
+func TestIndentNonEmpty(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name       string
+		giveLines  []string
+		giveIndent string
+		wantLines  []string
+	}{
+		{
+			name:       "add spaces",
+			giveLines:  []string{"one", "two"},
+			giveIndent: "    ",
+			wantLines:  []string{"    one", "    two"},
+		},
+		{
+			name:       "add tabs",
+			giveLines:  []string{"one", "two"},
+			giveIndent: "\t\t",
+			wantLines:  []string{"\t\tone", "\t\ttwo"},
+		},
+		{
+			name:       "no indentation",
+			giveLines:  []string{"one", "two"},
+			giveIndent: "",
+			wantLines:  []string{"one", "two"},
+		},
+		{
+			name:       "preserve empty lines",
+			giveLines:  []string{"one", "", "two"},
+			giveIndent: "  ",
+			wantLines:  []string{"  one", "", "  two"},
+		},
+		{
+			name:       "remove existing indentation and add new",
+			giveLines:  []string{"  one", "    two"},
+			giveIndent: "\t",
+			wantLines:  []string{"\tone", "\ttwo"},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			haveLines := stringslice.ChangeIndentNonEmpty(tt.giveLines, tt.giveIndent)
+			must.Eq(t, tt.wantLines, haveLines)
+		})
+	}
+}
diff --git a/internal/gohacks/stringslice/equal_ignore_whitespace_test.go b/internal/gohacks/stringslice/equal_ignore_whitespace_test.go
new file mode 100644
index 000000000..66322cc24
--- /dev/null
+++ b/internal/gohacks/stringslice/equal_ignore_whitespace_test.go
@@ -0,0 +1,52 @@
+package stringslice_test
+
+import (
+	"testing"
+
+	"github.com/git-town/git-town/v22/internal/gohacks/stringslice"
+	"github.com/shoenig/test/must"
+)
+
+func TestEqualIgnoreWhitespace(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name  string
+		give1 []string
+		give2 []string
+		want  bool
+	}{
+		{
+			name:  "exact match",
+			give1: []string{"one", "two"},
+			give2: []string{"one", "two"},
+			want:  true,
+		},
+		{
+			name:  "match with different indentation",
+			give1: []string{"    one", "    two"},
+			give2: []string{"  one", "  two"},
+			want:  true,
+		},
+		{
+			name:  "no match - different content",
+			give1: []string{"one", "three"},
+			give2: []string{"one", "two"},
+			want:  false,
+		},
+		{
+			name:  "no match - different length",
+			give1: []string{"one"},
+			give2: []string{"one", "two"},
+			want:  false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			have := stringslice.EqualIgnoreWhitespace(tt.give1, tt.give2)
+			must.EqOp(t, tt.want, have)
+		})
+	}
+}
diff --git a/internal/gohacks/stringslice/locate_section_test.go b/internal/gohacks/stringslice/locate_section_test.go
new file mode 100644
index 000000000..cb19f5f53
--- /dev/null
+++ b/internal/gohacks/stringslice/locate_section_test.go
@@ -0,0 +1,87 @@
+package stringslice_test
+
+import (
+	"testing"
+
+	"github.com/git-town/git-town/v22/internal/gohacks/stringslice"
+	"github.com/shoenig/test/must"
+)
+
+func TestLocateSection(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name         string
+		giveHaystack []string
+		giveNeedle   []string
+		wantIndex    int
+		wantFound    bool
+	}{
+		{
+			name: "section at beginning",
+			giveHaystack: []string{
+				"one",
+				"two",
+				"Some text",
+			},
+			giveNeedle: []string{
+				"one",
+				"two",
+			},
+			wantIndex: 0,
+			wantFound: true,
+		},
+		{
+			name: "section in middle",
+			giveHaystack: []string{
+				"Some text",
+				"    one",
+				"    two",
+				"More text",
+			},
+			giveNeedle: []string{
+				"one",
+				"two",
+			},
+			wantIndex: 1,
+			wantFound: true,
+		},
+		{
+			name: "section at end",
+			giveHaystack: []string{
+				"Some text",
+				"More text",
+				"  one",
+				"  two",
+			},
+			giveNeedle: []string{
+				"one",
+				"two",
+			},
+			wantIndex: 2,
+			wantFound: true,
+		},
+		{
+			name: "section not found",
+			giveHaystack: []string{
+				"one",
+				"three",
+			},
+			giveNeedle: []string{
+				"one",
+				"two",
+			},
+			wantIndex: -1,
+			wantFound: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			haveIdx, haveFound := stringslice.LocateSection(tt.giveHaystack, tt.giveNeedle)
+			must.EqOp(t, tt.wantIndex, haveIdx)
+			must.EqOp(t, tt.wantFound, haveFound)
+		})
+	}
+}
diff --git a/internal/gohacks/stringslice/non_empty_lines_test.go b/internal/gohacks/stringslice/non_empty_lines_test.go
new file mode 100644
index 000000000..e41244e01
--- /dev/null
+++ b/internal/gohacks/stringslice/non_empty_lines_test.go
@@ -0,0 +1,31 @@
+package stringslice_test
+
+import (
+	"testing"
+
+	"github.com/git-town/git-town/v22/internal/gohacks/stringslice"
+	"github.com/shoenig/test/must"
+)
+
+func TestNonEmptyLines(t *testing.T) {
+	t.Parallel()
+	tests := map[string][]string{
+		"":                                {},
+		"hello":                           {"hello"},
+		"line1\nline2\nline3":             {"line1", "line2", "line3"},
+		"line1\n\nline2\n\nline3":         {"line1", "line2", "line3"},
+		"line1\n   \nline2\n\t\nline3":    {"line1", "line2", "line3"},
+		"  line1  \n  line2  \n  line3  ": {"line1", "line2", "line3"},
+		"line1\n\n   \nline2\n\t\t\n  line3  \n\n": {"line1", "line2", "line3"},
+		"line1\nline2\n": {"line1", "line2"},
+		"\n\n\n":         {},
+		"   \n\t\n  ":    {},
+	}
+	for input, expected := range tests {
+		t.Run(input, func(t *testing.T) {
+			t.Parallel()
+			have := stringslice.NonEmptyLines(input)
+			must.Eq(t, expected, have)
+		})
+	}
+}
diff --git a/internal/gohacks/stringslice/replace_regex_test.go b/internal/gohacks/stringslice/replace_regex_test.go
new file mode 100644
index 000000000..8b0cf3ab9
--- /dev/null
+++ b/internal/gohacks/stringslice/replace_regex_test.go
@@ -0,0 +1,29 @@
+package stringslice_test
+
+import (
+	"regexp"
+	"testing"
+
+	"github.com/git-town/git-town/v22/internal/gohacks/stringslice"
+	"github.com/shoenig/test/must"
+)
+
+func TestReplaceRegex(t *testing.T) {
+	t.Parallel()
+
+	t.Run("has matches", func(t *testing.T) {
+		t.Parallel()
+		give := []string{"one", "two 123", "three"}
+		want := []string{"one", "two SHA", "three"}
+		have := stringslice.ReplaceRegex(give, regexp.MustCompile(`\d+`), "SHA")
+		must.Eq(t, want, have)
+	})
+
+	t.Run("no matches", func(t *testing.T) {
+		t.Parallel()
+		give := []string{"one", "two"}
+		want := []string{"one", "two"}
+		have := stringslice.ReplaceRegex(give, regexp.MustCompile(`\d+`), "SHA")
+		must.Eq(t, want, have)
+	})
+}
diff --git a/internal/gohacks/stringslice/trim_empty_lines_test.go b/internal/gohacks/stringslice/trim_empty_lines_test.go
new file mode 100644
index 000000000..023b4f554
--- /dev/null
+++ b/internal/gohacks/stringslice/trim_empty_lines_test.go
@@ -0,0 +1,62 @@
+package stringslice_test
+
+import (
+	"testing"
+
+	"github.com/git-town/git-town/v22/internal/gohacks/stringslice"
+	"github.com/shoenig/test/must"
+)
+
+func TestTrimEmptyLines(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name string
+		give []string
+		want []string
+	}{
+		{
+			name: "no empty lines",
+			give: []string{"one", "two"},
+			want: []string{"one", "two"},
+		},
+		{
+			name: "trailing empty line",
+			give: []string{"one", "two", ""},
+			want: []string{"one", "two"},
+		},
+		{
+			name: "multiple trailing empty lines",
+			give: []string{"one", "two", "", "", ""},
+			want: []string{"one", "two"},
+		},
+		{
+			name: "leading empty lines",
+			give: []string{"", "", "one", "two"},
+			want: []string{"one", "two"},
+		},
+		{
+			name: "empty lines before, middle, and after",
+			give: []string{"", "one", "", "two", ""},
+			want: []string{"one", "", "two"},
+		},
+		{
+			name: "empty string",
+			give: []string{},
+			want: []string{},
+		},
+		{
+			name: "only empty lines",
+			give: []string{"", "", ""},
+			want: []string{},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			have := stringslice.TrimEmptyLines(tt.give)
+			must.Eq(t, tt.want, have)
+		})
+	}
+}
diff --git a/internal/state/runstate/save_load_test.go b/internal/state/runstate/save_load_test.go
index 2488eb9af..ea2eec616 100644
--- a/internal/state/runstate/save_load_test.go
+++ b/internal/state/runstate/save_load_test.go
@@ -55,7 +55,7 @@ func TestLoadSave(t *testing.T) {
 				&opcodes.BranchCreateAndCheckoutExistingParent{Ancestors: gitdomain.NewLocalBranchNames("one", "two", "three"), Branch: "branch"},
 				&opcodes.BranchCurrentReset{Base: "branch"},
 				&opcodes.BranchCurrentResetToParent{CurrentBranch: "branch"},
-				&opcodes.BranchCurrentResetToSHA{SetToSHA: "111111"},
+				&opcodes.BranchCurrentResetToSHA{SHA: "111111"},
 				&opcodes.BranchCurrentResetToSHAIfNeeded{MustHaveSHA: "222222", SetToSHA: "111111"},
 				&opcodes.BranchEnsureShippableChanges{Branch: "branch", Parent: "parent"},
 				&opcodes.BranchLocalDelete{Branch: "branch"},
@@ -67,6 +67,7 @@ func TestLoadSave(t *testing.T) {
 				&opcodes.BranchReset{Target: "branch"},
 				&opcodes.BranchTrackingCreate{Branch: "branch"},
 				&opcodes.BranchTrackingCreateIfLocalExists{Branch: "branch"},
+				&opcodes.BranchTrackingCreateIfNeeded{CurrentBranch: "branch"},
 				&opcodes.BranchTrackingDelete{Branch: "origin/branch"},
 				&opcodes.BranchTypeOverrideSet{Branch: "branch", BranchType: configdomain.BranchTypeFeatureBranch},
 				&opcodes.BranchTypeOverrideRemove{Branch: "branch"},
@@ -92,7 +93,7 @@ func TestLoadSave(t *testing.T) {
 				&opcodes.ConflictMergePhantomFinalize{},
 				&opcodes.ConflictMergePhantomResolveAll{CurrentBranch: "current", ParentBranch: gitdomain.NewLocalBranchNameOption("parent"), ParentSHA: Some(gitdomain.NewSHA("123456"))},
 				&opcodes.ConflictResolve{FilePath: "file", Resolution: gitdomain.ConflictResolutionOurs},
-				&opcodes.ConnectorProposalMerge{Branch: "branch", CommitMessage: Some(gitdomain.CommitMessage("commit message")), Proposal: forgedomain.Proposal{Data: forgedomain.BitbucketCloudProposalData{ProposalData: forgedomain.ProposalData{Body: Some("body"), MergeWithAPI: true, Number: 123, Source: "source", Target: "target", Title: "title", URL: "url"}}, ForgeType: forgedomain.ForgeTypeBitbucket}},
+				&opcodes.ConnectorProposalMerge{Branch: "branch", CommitMessage: Some(gitdomain.CommitMessage("commit message")), Proposal: forgedomain.Proposal{Data: forgedomain.BitbucketCloudProposalData{ProposalData: forgedomain.ProposalData{Active: true, Body: Some("body"), MergeWithAPI: true, Number: 123, Source: "source", Target: "target", Title: "title", URL: "url"}}, ForgeType: forgedomain.ForgeTypeBitbucket}},
 				&opcodes.ExecuteShellCommand{Args: []string{"arg1", "arg2"}, Executable: "executable"},
 				&opcodes.ExitToShell{},
 				&opcodes.FetchUpstream{Branch: "branch"},
@@ -113,15 +114,14 @@ func TestLoadSave(t *testing.T) {
 				&opcodes.MessageQueue{Message: "message"},
 				&opcodes.ProgramEndOfBranch{},
 				&opcodes.ProposalCreate{Branch: "branch", MainBranch: "main"},
-				&opcodes.ProposalUpdateTarget{Proposal: forgedomain.Proposal{Data: forgedomain.ProposalData{Body: Some("body"), MergeWithAPI: true, Number: 123, Source: "source", Target: "target", Title: "title", URL: "url"}, ForgeType: forgedomain.ForgeTypeGitLab}, NewBranch: "new-target", OldBranch: "old-target"},
-				&opcodes.ProposalUpdateTargetToGrandParent{Branch: "branch", Proposal: forgedomain.Proposal{Data: forgedomain.ProposalData{Body: Some("body"), MergeWithAPI: true, Number: 123, Source: "source", Target: "target", Title: "title", URL: "url"}, ForgeType: forgedomain.ForgeTypeGitea}, OldTarget: "old-target"},
-				&opcodes.ProposalUpdateSource{Proposal: forgedomain.Proposal{Data: forgedomain.ProposalData{Body: None[string](), MergeWithAPI: false, Number: 123, Source: "source", Target: "target", Title: "title", URL: "url"}, ForgeType: forgedomain.ForgeTypeForgejo}, NewBranch: "new-target", OldBranch: "old-target"},
+				&opcodes.ProposalUpdateTarget{Proposal: forgedomain.Proposal{Data: forgedomain.ProposalData{Active: true, Body: Some("body"), MergeWithAPI: true, Number: 123, Source: "source", Target: "target", Title: "title", URL: "url"}, ForgeType: forgedomain.ForgeTypeGitLab}, NewBranch: "new-target", OldBranch: "old-target"},
+				&opcodes.ProposalUpdateTargetToGrandParent{Branch: "branch", Proposal: forgedomain.Proposal{Data: forgedomain.ProposalData{Active: true, Body: Some("body"), MergeWithAPI: true, Number: 123, Source: "source", Target: "target", Title: "title", URL: "url"}, ForgeType: forgedomain.ForgeTypeGitea}, OldTarget: "old-target"},
+				&opcodes.ProposalUpdateSource{Proposal: forgedomain.Proposal{Data: forgedomain.ProposalData{Active: true, Body: None[string](), MergeWithAPI: false, Number: 123, Source: "source", Target: "target", Title: "title", URL: "url"}, ForgeType: forgedomain.ForgeTypeForgejo}, NewBranch: "new-target", OldBranch: "old-target"},
 				&opcodes.PullCurrentBranch{},
 				&opcodes.PushCurrentBranch{},
 				&opcodes.PushCurrentBranchForce{ForceIfIncludes: true},
 				&opcodes.PushCurrentBranchForceIfNeeded{CurrentBranch: "branch", ForceIfIncludes: true},
 				&opcodes.PushCurrentBranchForceIgnoreError{},
-				&opcodes.PushCurrentBranchIfLocal{CurrentBranch: "branch"},
 				&opcodes.PushCurrentBranchIfNeeded{CurrentBranch: "branch"},
 				&opcodes.PushTags{},
 				&opcodes.RebaseAbort{},
@@ -208,7 +208,7 @@ func TestLoadSave(t *testing.T) {
     },
     {
       "data": {
-        "SetToSHA": "111111"
+        "SHA": "111111"
       },
       "type": "BranchCurrentResetToSHA"
     },
@@ -286,6 +286,12 @@ func TestLoadSave(t *testing.T) {
       },
       "type": "BranchTrackingCreateIfLocalExists"
     },
+    {
+      "data": {
+        "CurrentBranch": "branch"
+      },
+      "type": "BranchTrackingCreateIfNeeded"
+    },
     {
       "data": {
         "Branch": "origin/branch"
@@ -445,6 +451,7 @@ func TestLoadSave(t *testing.T) {
         "CommitMessage": "commit message",
         "Proposal": {
           "data": {
+            "Active": true,
             "Body": "body",
             "MergeWithAPI": true,
             "Number": 123,
@@ -601,6 +608,7 @@ func TestLoadSave(t *testing.T) {
         "OldBranch": "old-target",
         "Proposal": {
           "data": {
+            "Active": true,
             "Body": "body",
             "MergeWithAPI": true,
             "Number": 123,
@@ -620,6 +628,7 @@ func TestLoadSave(t *testing.T) {
         "OldTarget": "old-target",
         "Proposal": {
           "data": {
+            "Active": true,
             "Body": "body",
             "MergeWithAPI": true,
             "Number": 123,
@@ -639,6 +648,7 @@ func TestLoadSave(t *testing.T) {
         "OldBranch": "old-target",
         "Proposal": {
           "data": {
+            "Active": true,
             "Body": null,
             "MergeWithAPI": false,
             "Number": 123,
@@ -677,12 +687,6 @@ func TestLoadSave(t *testing.T) {
       "data": {},
       "type": "PushCurrentBranchForceIgnoreError"
     },
-    {
-      "data": {
-        "CurrentBranch": "branch"
-      },
-      "type": "PushCurrentBranchIfLocal"
-    },
     {
       "data": {
         "CurrentBranch": "branch"
diff --git a/internal/subshell/retry_test.go b/internal/subshell/retry_test.go
new file mode 100644
index 000000000..18ee72fbf
--- /dev/null
+++ b/internal/subshell/retry_test.go
@@ -0,0 +1,330 @@
+package subshell_test
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/git-town/git-town/v22/internal/gohacks"
+	"github.com/git-town/git-town/v22/internal/subshell"
+	. "github.com/git-town/git-town/v22/pkg/prelude"
+	"github.com/shoenig/test/must"
+)
+
+func TestBackendRunner_RetryOnIndexLock(t *testing.T) {
+	t.Parallel()
+
+	t.Run("detects index.lock error pattern correctly", func(t *testing.T) {
+		t.Parallel()
+		tmpDir := t.TempDir()
+
+		testCases := []struct {
+			name        string
+			errorMsg    string
+			shouldRetry bool
+		}{
+			{
+				name:        "exact match",
+				errorMsg:    "fatal: Unable to create '.git/index.lock': File exists.",
+				shouldRetry: true,
+			},
+			{
+				name:        "with path variations",
+				errorMsg:    "fatal: Unable to create '/path/to/repo/.git/index.lock': File exists.",
+				shouldRetry: true,
+			},
+			{
+				name:        "missing 'fatal' prefix but has both patterns",
+				errorMsg:    "Unable to create '.git/index.lock': File exists.",
+				shouldRetry: false, // doesn't match because "fatal: Unable to create" is required
+			},
+			{
+				name:        "only has 'Unable to create'",
+				errorMsg:    "fatal: Unable to create '.git/something'",
+				shouldRetry: false,
+			},
+			{
+				name:        "only has 'File exists'",
+				errorMsg:    "index.lock': File exists.",
+				shouldRetry: false,
+			},
+			{
+				name:        "completely different error",
+				errorMsg:    "fatal: not a git repository",
+				shouldRetry: false,
+			},
+		}
+
+		for _, tc := range testCases {
+			t.Run(tc.name, func(t *testing.T) {
+				t.Parallel()
+				// Create a separate runner for each subtest to avoid data races
+				runner := subshell.BackendRunner{Dir: Some(tmpDir), Verbose: false, CommandsCounter: NewMutable(new(gohacks.Counter))}
+				scriptPath := filepath.Join(tmpDir, fmt.Sprintf("test-%s.sh", tc.name))
+				scriptContent := fmt.Sprintf(`#!/bin/bash
+>&2 echo %q
+exit 1
+`, tc.errorMsg)
+				must.NoError(t, os.WriteFile(scriptPath, []byte(scriptContent), 0o600))
+				must.NoError(t, os.Chmod(scriptPath, 0o700))
+
+				start := time.Now()
+				_, err := runner.Query("bash", scriptPath)
+				duration := time.Since(start)
+
+				must.Error(t, err)
+				if tc.shouldRetry {
+					// Should take at least 4 seconds (exhausted retries)
+					must.GreaterEq(t, 4*time.Second, duration)
+				} else {
+					// Should fail immediately
+					must.Less(t, 500*time.Millisecond, duration)
+				}
+			})
+		}
+	})
+
+	t.Run("does not retry on non-lock errors", func(t *testing.T) {
+		t.Parallel()
+		tmpDir := t.TempDir()
+		runner := subshell.BackendRunner{Dir: Some(tmpDir), Verbose: false, CommandsCounter: NewMutable(new(gohacks.Counter))}
+
+		// Create a script that fails with a different error
+		scriptPath := filepath.Join(tmpDir, "other-error.sh")
+		scriptContent := `#!/bin/bash
+>&2 echo "fatal: Some other error"
+exit 1
+`
+		must.NoError(t, os.WriteFile(scriptPath, []byte(scriptContent), 0o600))
+		must.NoError(t, os.Chmod(scriptPath, 0o700))
+
+		start := time.Now()
+		_, err := runner.Query("bash", scriptPath)
+		duration := time.Since(start)
+
+		// Should fail immediately
+		must.Error(t, err)
+		// Should complete quickly without retries
+		must.Less(t, 500*time.Millisecond, duration)
+	})
+
+	t.Run("exhausts retries and fails after max attempts", func(t *testing.T) {
+		t.Parallel()
+		tmpDir := t.TempDir()
+		runner := subshell.BackendRunner{Dir: Some(tmpDir), Verbose: false, CommandsCounter: NewMutable(new(gohacks.Counter))}
+
+		// Create a script that counts attempts and always fails with lock error
+		counterFile := filepath.Join(tmpDir, "attempt-counter")
+		scriptPath := filepath.Join(tmpDir, "always-fails.sh")
+		scriptContent := `#!/bin/bash
+COUNTER_FILE="` + counterFile + `"
+if [ ! -f "$COUNTER_FILE" ]; then
+    echo "1" > "$COUNTER_FILE"
+else
+    COUNT=$(cat "$COUNTER_FILE")
+    echo $((COUNT + 1)) > "$COUNTER_FILE"
+fi
+>&2 echo "fatal: Unable to create '.git/index.lock': File exists."
+exit 1
+`
+		must.NoError(t, os.WriteFile(scriptPath, []byte(scriptContent), 0o600))
+		must.NoError(t, os.Chmod(scriptPath, 0o700))
+
+		start := time.Now()
+		_, err := runner.Query("bash", scriptPath)
+		duration := time.Since(start)
+
+		// Should fail after exhausting retries
+		must.Error(t, err)
+		// Should take at least 4 seconds (5 attempts with 4 delays between them)
+		must.GreaterEq(t, 4*time.Second, duration)
+		// Verify it was called exactly 5 times
+		counterBytes, err := os.ReadFile(counterFile)
+		must.NoError(t, err)
+		must.EqOp(t, "5\n", string(counterBytes))
+	})
+
+	t.Run("retries and succeeds on transient lock error", func(t *testing.T) {
+		t.Parallel()
+		tmpDir := t.TempDir()
+		runner := subshell.BackendRunner{Dir: Some(tmpDir), Verbose: false, CommandsCounter: NewMutable(new(gohacks.Counter))}
+
+		// Create a script that fails twice with lock error, then succeeds
+		scriptPath := filepath.Join(tmpDir, "retry-script.sh")
+		scriptContent := `#!/bin/bash
+COUNTER_FILE="` + tmpDir + `/counter"
+if [ ! -f "$COUNTER_FILE" ]; then
+    echo "0" > "$COUNTER_FILE"
+fi
+COUNT=$(cat "$COUNTER_FILE")
+echo $((COUNT + 1)) > "$COUNTER_FILE"
+
+if [ "$COUNT" -lt "2" ]; then
+    >&2 echo "fatal: Unable to create '.git/index.lock': File exists."
+    exit 1
+else
+    echo "success"
+    exit 0
+fi
+`
+		must.NoError(t, os.WriteFile(scriptPath, []byte(scriptContent), 0o600))
+		must.NoError(t, os.Chmod(scriptPath, 0o700))
+
+		start := time.Now()
+		output, err := runner.Query("bash", scriptPath)
+		duration := time.Since(start)
+
+		must.NoError(t, err)
+		must.EqOp(t, "success\n", output)
+		// Should take at least 2 seconds (2 retries * 1 second delay)
+		must.GreaterEq(t, 2*time.Second, duration)
+	})
+
+	t.Run("succeeds immediately when no lock error", func(t *testing.T) {
+		t.Parallel()
+		tmpDir := t.TempDir()
+		runner := subshell.BackendRunner{Dir: Some(tmpDir), Verbose: false, CommandsCounter: NewMutable(new(gohacks.Counter))}
+		start := time.Now()
+		output, err := runner.Query("echo", "success")
+		duration := time.Since(start)
+		must.NoError(t, err)
+		must.EqOp(t, "success\n", output)
+		// Should complete quickly without any retries
+		must.Less(t, 100*time.Millisecond, duration)
+	})
+}
+
+func TestFrontendRunner_RetryOnIndexLock(t *testing.T) {
+	t.Parallel()
+
+	t.Run("does not retry on non-lock errors", func(t *testing.T) {
+		t.Parallel()
+		tmpDir := t.TempDir()
+		backendRunner := subshell.BackendRunner{Dir: Some(tmpDir), Verbose: false, CommandsCounter: NewMutable(new(gohacks.Counter))}
+		runner := &subshell.FrontendRunner{
+			Backend:          backendRunner,
+			GetCurrentBranch: nil,
+			GetCurrentSHA:    nil,
+			PrintBranchNames: false,
+			PrintCommands:    false,
+			CommandsCounter:  NewMutable(new(gohacks.Counter)),
+		}
+
+		// Script that fails with a different error
+		scriptPath := filepath.Join(tmpDir, "different-error.sh")
+		scriptContent := `#!/bin/bash
+>&2 echo "fatal: repository not found"
+exit 128
+`
+		must.NoError(t, os.WriteFile(scriptPath, []byte(scriptContent), 0o600))
+		must.NoError(t, os.Chmod(scriptPath, 0o700))
+
+		start := time.Now()
+		err := runner.Run("bash", scriptPath)
+		duration := time.Since(start)
+
+		// Should fail immediately
+		must.Error(t, err)
+		// Should complete quickly without retries
+		must.Less(t, 500*time.Millisecond, duration)
+	})
+
+	t.Run("exhausts retries and fails", func(t *testing.T) {
+		t.Parallel()
+		tmpDir := t.TempDir()
+		backendRunner := subshell.BackendRunner{Dir: Some(tmpDir), Verbose: false, CommandsCounter: NewMutable(new(gohacks.Counter))}
+		runner := &subshell.FrontendRunner{
+			Backend:          backendRunner,
+			GetCurrentBranch: nil,
+			GetCurrentSHA:    nil,
+			PrintBranchNames: false,
+			PrintCommands:    false,
+			CommandsCounter:  NewMutable(new(gohacks.Counter)),
+		}
+
+		// Create a script that always fails with lock error
+		scriptPath := filepath.Join(tmpDir, "always-lock.sh")
+		scriptContent := `#!/bin/bash
+>&2 echo "fatal: Unable to create '.git/index.lock': File exists."
+exit 1
+`
+		must.NoError(t, os.WriteFile(scriptPath, []byte(scriptContent), 0o600))
+		must.NoError(t, os.Chmod(scriptPath, 0o700))
+
+		start := time.Now()
+		err := runner.Run("bash", scriptPath)
+		duration := time.Since(start)
+
+		// Should fail after exhausting retries
+		must.Error(t, err)
+		// Should take at least 4 seconds (5 attempts with 4 delays between them)
+		must.GreaterEq(t, 4*time.Second, duration)
+	})
+
+	t.Run("retries and succeeds on transient lock error", func(t *testing.T) {
+		t.Parallel()
+		tmpDir := t.TempDir()
+		backendRunner := subshell.BackendRunner{Dir: Some(tmpDir), Verbose: false, CommandsCounter: NewMutable(new(gohacks.Counter))}
+		runner := &subshell.FrontendRunner{
+			Backend:          backendRunner,
+			GetCurrentBranch: nil,
+			GetCurrentSHA:    nil,
+			PrintBranchNames: false,
+			PrintCommands:    false,
+			CommandsCounter:  NewMutable(new(gohacks.Counter)),
+		}
+
+		// Create a script that fails once with lock error, then succeeds
+		scriptPath := filepath.Join(tmpDir, "retry-once.sh")
+		scriptContent := `#!/bin/bash
+COUNTER_FILE="` + tmpDir + `/counter"
+if [ ! -f "$COUNTER_FILE" ]; then
+    echo "0" > "$COUNTER_FILE"
+fi
+COUNT=$(cat "$COUNTER_FILE")
+echo $((COUNT + 1)) > "$COUNTER_FILE"
+
+if [ "$COUNT" -lt "1" ]; then
+    >&2 echo "fatal: Unable to create '.git/index.lock': File exists."
+    exit 1
+else
+    echo "success"
+    exit 0
+fi
+`
+		must.NoError(t, os.WriteFile(scriptPath, []byte(scriptContent), 0o600))
+		must.NoError(t, os.Chmod(scriptPath, 0o700))
+
+		start := time.Now()
+		err := runner.Run("bash", scriptPath)
+		duration := time.Since(start)
+
+		must.NoError(t, err)
+		// Should take at least 1 second (1 retry * 1 second delay)
+		must.GreaterEq(t, 1*time.Second, duration)
+	})
+
+	t.Run("succeeds immediately when no lock error", func(t *testing.T) {
+		t.Parallel()
+		tmpDir := t.TempDir()
+		backendRunner := subshell.BackendRunner{Dir: Some(tmpDir), Verbose: false, CommandsCounter: NewMutable(new(gohacks.Counter))}
+		runner := &subshell.FrontendRunner{
+			Backend:          backendRunner,
+			GetCurrentBranch: nil, // not needed for this test
+			GetCurrentSHA:    nil, // not needed for this test
+			PrintBranchNames: false,
+			PrintCommands:    false,
+			CommandsCounter:  NewMutable(new(gohacks.Counter)),
+		}
+
+		start := time.Now()
+		err := runner.Run("echo", "success")
+		duration := time.Since(start)
+
+		must.NoError(t, err)
+		// Should complete quickly without any retries
+		must.Less(t, 100*time.Millisecond, duration)
+	})
+}
diff --git a/internal/test/commands/test_commands.go b/internal/test/commands/test_commands.go
index 0dcc2ac14..ea5844fc3 100644
--- a/internal/test/commands/test_commands.go
+++ b/internal/test/commands/test_commands.go
@@ -27,6 +27,7 @@ import (
 const (
 	ConfigFileCommitMessage = "persisted config file"
 	FileCommitMessage       = "persisted file"
+	MainBranchName          = "main"
 	deletedText             = " (deleted)"
 )
 
@@ -63,10 +64,10 @@ func (self *TestCommands) CheckoutBranch(branch gitdomain.LocalBranchName) {
 	asserts.NoError(self.Git.CheckoutBranchUncached(self.TestRunner, branch, false))
 }
 
-func (self *TestCommands) CommitSHA(querier subshelldomain.Querier, title string, branch gitdomain.LocalBranchName, parent gitdomain.BranchName) gitdomain.SHA {
+func (self *TestCommands) CommitSHA(querier subshelldomain.Querier, title gitdomain.CommitTitle, branch gitdomain.LocalBranchName, parent gitdomain.BranchName) gitdomain.SHA {
 	commits := asserts.NoError1(self.Git.CommitsInFeatureBranch(querier, branch, parent))
 	for _, commit := range commits {
-		if commit.Message.Parts().Subject == title {
+		if commit.Message.Parts().Title == title {
 			return commit.SHA
 		}
 	}
@@ -74,13 +75,23 @@ func (self *TestCommands) CommitSHA(querier subshelldomain.Querier, title string
 }
 
 func (self *TestCommands) CommitSHAs() gitdomain.Commits {
-	result := gitdomain.Commits{}
 	output := self.MustQuery("git", "log", "--all", "--format=%H %s")
 	if output == "" {
-		return result
+		return gitdomain.Commits{}
 	}
-	for _, line := range strings.Split(output, "\n") {
-		sha, commitMessage, _ := strings.Cut(line, " ")
+	lines := stringslice.NonEmptyLines(output)
+	result := make(gitdomain.Commits, 0, len(lines))
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+		sha, commitMessage, found := strings.Cut(line, " ")
+		if !found {
+			// No space found means the entire line is the SHA (empty commit message)
+			sha = line
+			commitMessage = ""
+		}
 		result = append(result, gitdomain.Commit{
 			Message: gitdomain.CommitMessage(commitMessage),
 			SHA:     gitdomain.NewSHA(sha),
@@ -117,17 +128,14 @@ func (self *TestCommands) Commits(fields []string, lineage configdomain.Lineage,
 func (self *TestCommands) CommitsInBranch(branch gitdomain.LocalBranchName, parentOpt Option[gitdomain.BranchName], fields []string) []testgit.Commit {
 	args := []string{"log", "--format=%H%x00%s%x00%an <%ae>", "--topo-order", "--reverse"}
 	if parent, hasParent := parentOpt.Get(); hasParent {
-		args = append(args, fmt.Sprintf("%s..%s", parent.RefName(), branch.RefName()))
+		args = append(args, parent.RefName()+".."+branch.RefName())
 	} else {
 		args = append(args, branch.RefName())
 	}
 	output := self.MustQuery("git", args...)
-	lines := strings.Split(output, "\n")
+	lines := stringslice.NonEmptyLines(output)
 	result := make([]testgit.Commit, 0, len(lines))
 	for _, line := range lines {
-		if len(strings.TrimSpace(line)) == 0 {
-			continue
-		}
 		parts := strings.Split(line, "\x00")
 		commit := testgit.Commit{
 			Branch:  branch,
@@ -161,24 +169,24 @@ func (self *TestCommands) CommitsInBranch(branch gitdomain.LocalBranchName, pare
 // ConnectTrackingBranch connects the branch with the given name to its counterpart at origin.
 // The branch must exist.
 func (self *TestCommands) ConnectTrackingBranch(name gitdomain.LocalBranchName) {
-	self.MustRun("git", "branch", "--set-upstream-to=origin/"+name.String(), name.String())
+	self.MustRun("git", "branch", fmt.Sprintf("--set-upstream-to=origin/%s", name), name.String())
 }
 
-// CreateBranch creates a new branch with the given name.
+// CreateAndCheckoutBranch creates a new branch with the given name and checks it out.
 // The created branch is a normal branch.
-// To create feature branches, use CreateFeatureBranch.
+// To create feature branches, use CreateAndCheckoutFeatureBranch.
 func (self *TestCommands) CreateAndCheckoutBranch(name gitdomain.LocalBranchName, parent gitdomain.BranchName) {
 	self.Git.CurrentBranchCache.Set(name)
 	self.MustRun("git", "checkout", "-b", name.String(), parent.String())
 }
 
-// creates a feature branch with the given name in this repository
+// CreateAndCheckoutFeatureBranch creates a feature branch with the given name and checks it out.
 func (self *TestCommands) CreateAndCheckoutFeatureBranch(name gitdomain.LocalBranchName, parent gitdomain.LocalBranchName) {
 	asserts.NoError(self.Git.CreateAndCheckoutBranchWithParent(self, name, parent.Location()))
 	asserts.NoError(gitconfig.SetParent(self, name, parent))
 }
 
-// CreateBranch creates a new branch with the given name.
+// CreateBranch creates a new branch with the given name without checking it out.
 // The created branch is a normal branch.
 // To create feature branches, use CreateFeatureBranch.
 func (self *TestCommands) CreateBranch(name gitdomain.LocalBranchName, parent gitdomain.BranchName) {
@@ -189,7 +197,7 @@ func (self *TestCommands) CreateBranchOfType(name gitdomain.LocalBranchName, par
 	if parent, hasParent := parentOpt.Get(); hasParent {
 		self.CreateFeatureBranch(name, parent.BranchName())
 	} else {
-		self.CreateBranch(name, "main")
+		self.CreateBranch(name, MainBranchName)
 	}
 	asserts.NoError(gitconfig.SetBranchTypeOverride(self.TestRunner, branchType, name))
 }
@@ -213,13 +221,13 @@ func (self *TestCommands) CreateCommit(commit testgit.Commit) {
 	self.MustRun("git", commands...)
 }
 
-// creates a feature branch with the given name in this repository
+// CreateFeatureBranch creates a feature branch with the given name without checking it out.
 func (self *TestCommands) CreateFeatureBranch(name gitdomain.LocalBranchName, parent gitdomain.BranchName) {
 	self.CreateBranch(name, parent)
 	asserts.NoError(self.Config.NormalConfig.SetParent(self.TestRunner, name, parent.LocalName()))
 }
 
-// creates a file with the given name and content in this repository
+// CreateFile creates a file with the given name and content in this repository.
 func (self *TestCommands) CreateFile(name, content string) {
 	filePath := filepath.Join(self.WorkingDir, name)
 	folderPath := filepath.Dir(filePath)
@@ -239,7 +247,7 @@ func (self *TestCommands) CreateLocalBranchUsingGitTown(branchSetup datatable.Br
 	if parent, hasParent := branchSetup.Parent.Get(); hasParent {
 		self.CreateChildBranch(branchSetup.Name, parent)
 	} else {
-		self.CreateAndCheckoutBranch(branchSetup.Name, "main")
+		self.CreateAndCheckoutBranch(branchSetup.Name, MainBranchName)
 	}
 	// step 2: create the tracking branch
 	if branchSetup.Locations.Contains(testgit.LocationOrigin) {
@@ -269,17 +277,18 @@ func (self *TestCommands) CreateLocalBranchUsingGitTown(branchSetup datatable.Br
 
 // CreateStandaloneTag creates a tag not on a branch.
 func (self *TestCommands) CreateStandaloneTag(name string) {
-	self.MustRun("git", "checkout", "-b", "temp")
+	const tempBranchName = "temp"
+	self.MustRun("git", "checkout", "-b", tempBranchName)
 	filePath := filepath.Join(self.WorkingDir, "a.txt")
 	file, err := os.Create(filePath)
 	asserts.NoError(err)
 	err = file.Close()
 	asserts.NoError(err)
 	self.MustRun("git", "add", "-A")
-	self.MustRun("git", "commit", "-m", "temp")
+	self.MustRun("git", "commit", "-m", tempBranchName)
 	self.MustRun("git", "tag", "-a", name, "-m", name)
 	self.MustRun("git", "checkout", "-")
-	self.MustRun("git", "branch", "-D", "temp")
+	self.MustRun("git", "branch", "-D", tempBranchName)
 }
 
 // CreateTag creates a tag with the given name.
@@ -287,11 +296,7 @@ func (self *TestCommands) CreateTag(name string) {
 	self.MustRun("git", "tag", "-a", name, "-m", name)
 }
 
-func (self *TestCommands) CreateWorktree(path string, branch gitdomain.LocalBranchName) {
-	self.MustRun("git", "worktree", "add", path, branch.String())
-}
-
-// provides the first ancestor of the given branch that actually exists in the repo
+// ExistingParent provides the first ancestor of the given branch that actually exists in the repo.
 func (self *TestCommands) ExistingParent(branch gitdomain.LocalBranchName, lineage configdomain.Lineage) Option[gitdomain.BranchName] {
 	for {
 		parentOpt := lineage.Parent(branch)
@@ -319,7 +324,7 @@ func (self *TestCommands) FileContent(filename string) string {
 	return asserts.NoError1(self.FileContentErr(filename))
 }
 
-// FileContent provides the current content of a file.
+// FileContentErr provides the current content of a file, returning an error if the file cannot be read.
 func (self *TestCommands) FileContentErr(filename string) (string, error) {
 	content, err := os.ReadFile(filepath.Join(self.WorkingDir, filename))
 	return string(content), err
@@ -338,14 +343,7 @@ func (self *TestCommands) FileContentInCommit(location gitdomain.Location, filen
 // FilesInBranch provides the list of the files present in the given branch.
 func (self *TestCommands) FilesInBranch(branch gitdomain.LocalBranchName) []string {
 	output := self.MustQuery("git", "ls-tree", "-r", "--name-only", branch.String())
-	var result []string
-	for _, line := range strings.Split(output, "\n") {
-		file := strings.TrimSpace(line)
-		if len(file) > 0 {
-			result = append(result, file)
-		}
-	}
-	return result
+	return stringslice.NonEmptyLines(output)
 }
 
 // FilesInBranches provides a data table of files and their content in all branches.
@@ -375,7 +373,7 @@ func (self *TestCommands) FilesInBranches(mainBranch gitdomain.LocalBranchName)
 // FilesInCommit provides the names of the files that the commit with the given SHA changes.
 func (self *TestCommands) FilesInCommit(sha gitdomain.SHA) []string {
 	output := self.MustQuery("git", "show", "--name-only", "--pretty=format:", sha.String())
-	return strings.Split(output, "\n")
+	return stringslice.NonEmptyLines(output)
 }
 
 func (self *TestCommands) FilesInWorkspace() []string {
@@ -413,10 +411,11 @@ func (self *TestCommands) HasFile(name, content string) string {
 
 func (self *TestCommands) Lineage() configdomain.Lineage {
 	localSnapshot, _ := gitconfig.LoadSnapshot(self.TestRunner, Some(configdomain.ConfigScopeLocal), configdomain.UpdateOutdatedNo)
-	localGitConfig, _ := config.NewPartialConfigFromSnapshot(localSnapshot, false, nil)
+	localGitConfig, _ := config.NewPartialConfigFromSnapshot(localSnapshot, false, false, nil)
 	return localGitConfig.Lineage
 }
 
+// LineageText formats the given lineage as a readable text representation.
 func (self *TestCommands) LineageText(lineage configdomain.Lineage) string {
 	return format.BranchLineage(lineage, configdomain.OrderAsc)
 }
@@ -532,7 +531,7 @@ func (self *TestCommands) RenameRemote(oldName, newName string) {
 	self.MustRun("git", "remote", "rename", oldName, newName)
 }
 
-// provides the SHA that the given branch points to
+// SHAforBranch provides the SHA that the given branch points to.
 func (self *TestCommands) SHAforBranch(branch gitdomain.LocalBranchName) gitdomain.SHA {
 	output := self.MustQuery("git", "rev-parse", branch.String())
 	return gitdomain.NewSHA(output)
@@ -545,7 +544,7 @@ func (self *TestCommands) SHAsForCommit(message gitdomain.CommitMessage) gitdoma
 		panic(fmt.Sprintf("cannot find the SHA of commit %q", message))
 	}
 	shasWithMessage := make(gitdomain.SHAs, 0, 1)
-	for _, text := range strings.Split(output, "\n") {
+	for text := range strings.SplitSeq(output, "\n") {
 		shaText, commitMessage, found := strings.Cut(text, " ")
 		if found && commitMessage == message.String() {
 			sha := gitdomain.NewSHA(shaText)
@@ -574,12 +573,7 @@ func (self *TestCommands) StashOpenFiles() {
 // Tags provides a list of the tags in this repository.
 func (self *TestCommands) Tags() []string {
 	output := self.MustQuery("git", "tag")
-	lines := strings.Split(output, "\n")
-	result := make([]string, len(lines))
-	for l, line := range lines {
-		result[l] = strings.TrimSpace(line)
-	}
-	return result
+	return stringslice.NonEmptyLines(output)
 }
 
 // UncommittedFiles provides the names of the files not committed into Git.
diff --git a/internal/test/commands/test_commands_test.go b/internal/test/commands/test_commands_test.go
index d375795f6..d11a475e0 100644
--- a/internal/test/commands/test_commands_test.go
+++ b/internal/test/commands/test_commands_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/config/gitconfig"
 	"github.com/git-town/git-town/v22/internal/git/gitdomain"
+	"github.com/git-town/git-town/v22/internal/test/datatable"
 	"github.com/git-town/git-town/v22/internal/test/filesystem"
 	"github.com/git-town/git-town/v22/internal/test/fixture"
 	"github.com/git-town/git-town/v22/internal/test/testgit"
@@ -33,6 +34,109 @@ func TestTestCommands(t *testing.T) {
 		must.Eq(t, gitdomain.Remotes{gitdomain.RemoteOrigin}, remotes)
 	})
 
+	t.Run("AddSubmodule", func(t *testing.T) {
+		t.Skip("AddSubmodule requires git config protocol.file.allow=always which may not be set")
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		submoduleOrigin := testruntime.Create(t)
+		runtime.AddSubmodule(submoduleOrigin.WorkingDir)
+		output := runtime.MustQuery("git", "submodule", "status")
+		must.StrContains(t, output, submoduleOrigin.WorkingDir)
+	})
+
+	t.Run("AddWorktree", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateBranch("branch-1", "initial")
+		worktreePath := filepath.Join(t.TempDir(), "worktree1")
+		runtime.AddWorktree(worktreePath, "branch-1")
+		output := runtime.MustQuery("git", "worktree", "list")
+		must.StrContains(t, output, worktreePath)
+	})
+
+	t.Run("AmendCommit", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "initial",
+			FileContent: "initial content",
+			FileName:    "file1.txt",
+			Message:     "initial commit message",
+		})
+		runtime.CreateFile("file2.txt", "new content")
+		runtime.StageFiles("file2.txt")
+		runtime.AmendCommit()
+		commits := runtime.Commits([]string{"FILE NAME"}, runtime.Config.NormalConfig.Lineage, configdomain.OrderAsc)
+		must.Len(t, 1, commits)
+		files := runtime.FilesInCommit(commits[0].SHA)
+		must.Eq(t, []string{"file1.txt", "file2.txt"}, files)
+	})
+
+	t.Run("CheckoutBranch", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateBranch("branch-1", "initial")
+		runtime.CheckoutBranch("branch-1")
+		currentBranch := asserts.NoError1(runtime.Git.CurrentBranch(runtime)).GetOrPanic()
+		must.EqOp(t, "branch-1", currentBranch)
+	})
+
+	t.Run("CommitSHA", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.CreateGitTown(t)
+		runtime.CreateFeatureBranch("feature1", "main")
+		runtime.CheckoutBranch("feature1")
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "feature1",
+			FileContent: "content",
+			FileName:    "file1.txt",
+			Message:     "test commit",
+		})
+		sha := runtime.CommitSHA(runtime, "test commit", "feature1", "main")
+		must.EqOp(t, 40, len(sha.String()))
+	})
+
+	t.Run("CommitSHAs", func(t *testing.T) {
+		t.Parallel()
+		t.Run("includes commits with empty messages", func(t *testing.T) {
+			t.Parallel()
+			runtime := testruntime.Create(t)
+			// Create a commit with a normal message
+			runtime.CreateCommit(testgit.Commit{
+				Branch:      "initial",
+				FileContent: "content1",
+				FileName:    "file1",
+				Message:     "first commit",
+			})
+			// Create a commit with an empty message
+			runtime.CreateFile("file2", "content2")
+			runtime.StageFiles("file2")
+			runtime.MustRun("git", "commit", "--allow-empty-message", "-m", "")
+			// Get all commits
+			commits := runtime.CommitSHAs()
+			must.EqOp(t, "", commits[0].Message)
+			must.EqOp(t, "first commit", commits[1].Message)
+			must.EqOp(t, "initial commit", commits[2].Message)
+			must.EqOp(t, 40, len(commits[0].SHA.String()))
+			must.EqOp(t, 40, len(commits[1].SHA.String()))
+			must.EqOp(t, 40, len(commits[2].SHA.String()))
+			must.NotEqOp(t, commits[0].SHA.String(), commits[1].SHA.String())
+			must.NotEqOp(t, commits[0].SHA.String(), commits[2].SHA.String())
+			must.NotEqOp(t, commits[1].SHA.String(), commits[2].SHA.String())
+		})
+	})
+
+	t.Run("CommitStagedChanges", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateFile("file1.txt", "content")
+		runtime.StageFiles("file1.txt")
+		runtime.CommitStagedChanges("test commit")
+		commits := runtime.Commits([]string{}, runtime.Config.NormalConfig.Lineage, configdomain.OrderAsc)
+		must.Len(t, 1, commits)
+		must.EqOp(t, "test commit", commits[0].Message)
+	})
+
 	t.Run("Commits", func(t *testing.T) {
 		t.Parallel()
 		runtime := testruntime.Create(t)
@@ -60,6 +164,36 @@ func TestTestCommands(t *testing.T) {
 		must.EqOp(t, "second commit", commits[1].Message)
 	})
 
+	t.Run("CommitsInBranch", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateBranch("branch-1", "initial")
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "branch-1",
+			FileContent: "content",
+			FileName:    "file1.txt",
+			Message:     "first commit",
+		})
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "branch-1",
+			FileContent: "content",
+			FileName:    "file2.txt",
+			Message:     "second commit",
+		})
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "initial",
+			FileContent: "content",
+			FileName:    "file3.txt",
+			Message:     "third commit",
+		})
+		commits := runtime.CommitsInBranch("branch-1", None[gitdomain.BranchName](), []string{})
+		must.Len(t, 2, commits)
+		must.EqOp(t, "branch-1", commits[0].Branch)
+		must.EqOp(t, "first commit", commits[0].Message)
+		must.EqOp(t, "branch-1", commits[1].Branch)
+		must.EqOp(t, "second commit", commits[1].Message)
+	})
+
 	t.Run("ConnectTrackingBranch", func(t *testing.T) {
 		t.Parallel()
 		// replicating the situation this is used in,
@@ -74,16 +208,40 @@ func TestTestCommands(t *testing.T) {
 		runtime.PushBranch()
 	})
 
+	t.Run("CreateAndCheckoutBranch", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateAndCheckoutBranch("branch-1", "initial")
+		currentBranch := asserts.NoError1(runtime.Git.CurrentBranch(runtime)).GetOrPanic()
+		must.EqOp(t, "branch-1", currentBranch)
+		branches, _, err := runtime.LocalBranchesMainFirst("initial")
+		must.NoError(t, err)
+		want := gitdomain.NewLocalBranchNames("initial", "branch-1")
+		must.Eq(t, want, branches)
+	})
+
+	t.Run("CreateAndCheckoutFeatureBranch", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.CreateGitTown(t)
+		runtime.CreateAndCheckoutFeatureBranch("feature1", "main")
+		currentBranch := asserts.NoError1(runtime.Git.CurrentBranch(runtime)).GetOrPanic()
+		must.EqOp(t, "feature1", currentBranch)
+		runtime.Config.Reload(runtime.TestRunner)
+		parent := runtime.Config.NormalConfig.Lineage.Parent("feature1")
+		must.EqOp(t, "main", parent.GetOrPanic())
+		must.EqOp(t, configdomain.BranchTypeFeatureBranch, runtime.Config.BranchType("feature1"))
+	})
+
 	t.Run("CreateBranch", func(t *testing.T) {
 		t.Run("simple branch name", func(t *testing.T) {
 			t.Parallel()
 			runtime := testruntime.Create(t)
-			runtime.CreateBranch("branch1", "initial")
+			runtime.CreateBranch("branch-1", "initial")
 			currentBranch := asserts.NoError1(runtime.Git.CurrentBranch(runtime)).GetOrPanic()
 			must.EqOp(t, "initial", currentBranch)
 			branches, _, err := runtime.LocalBranchesMainFirst("initial")
 			must.NoError(t, err)
-			want := gitdomain.NewLocalBranchNames("initial", "branch1")
+			want := gitdomain.NewLocalBranchNames("initial", "branch-1")
 			must.Eq(t, want, branches)
 		})
 
@@ -100,6 +258,31 @@ func TestTestCommands(t *testing.T) {
 		})
 	})
 
+	t.Run("CreateBranchOfType", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.CreateGitTown(t)
+		runtime.CreateBranchOfType("observed-1", Some(gitdomain.NewLocalBranchName("main")), configdomain.BranchTypeObservedBranch)
+		runtime.Config.Reload(runtime.TestRunner)
+		parent := runtime.Config.NormalConfig.Lineage.Parent("observed-1")
+		must.EqOp(t, "main", parent.GetOrPanic())
+		must.EqOp(t, configdomain.BranchTypeObservedBranch, runtime.Config.BranchType("observed-1"))
+		branches, _, err := runtime.LocalBranchesMainFirst("main")
+		must.NoError(t, err)
+		must.True(t, branches.Contains("observed-1"))
+	})
+
+	t.Run("CreateChildBranch", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.CreateGitTown(t)
+		runtime.CreateChildBranch("child-1", "main")
+		currentBranch := asserts.NoError1(runtime.Git.CurrentBranch(runtime)).GetOrPanic()
+		must.EqOp(t, "child-1", currentBranch)
+		runtime.Config.Reload(runtime.TestRunner)
+		parent := runtime.Config.NormalConfig.Lineage.Parent("child-1")
+		must.EqOp(t, "main", parent.GetOrPanic())
+		must.EqOp(t, configdomain.BranchTypeFeatureBranch, runtime.Config.BranchType("child-1"))
+	})
+
 	t.Run("CreateCommit", func(t *testing.T) {
 		t.Run("minimal arguments", func(t *testing.T) {
 			t.Parallel()
@@ -171,6 +354,62 @@ func TestTestCommands(t *testing.T) {
 		})
 	})
 
+	t.Run("CreateFolder", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateFolder("myfolder")
+		info, err := os.Stat(filepath.Join(runtime.WorkingDir, "myfolder"))
+		must.NoError(t, err)
+		must.True(t, info.IsDir())
+	})
+
+	t.Run("CreateLocalBranchUsingGitTown", func(t *testing.T) {
+		t.Parallel()
+		fixture := fixture.NewMemoized(t.TempDir()).AsFixture()
+		devRepo := fixture.DevRepo.GetOrPanic()
+		branchSetup := datatable.BranchSetup{
+			Name:       "feature-1",
+			Parent:     Some(gitdomain.NewLocalBranchName("main")),
+			BranchType: Some(configdomain.BranchTypeFeatureBranch),
+			Locations:  []testgit.Location{testgit.LocationLocal, testgit.LocationOrigin},
+		}
+		devRepo.CreateLocalBranchUsingGitTown(branchSetup)
+		branches, _, err := devRepo.LocalBranchesMainFirst("main")
+		must.NoError(t, err)
+		must.True(t, branches.Contains("feature-1"))
+		devRepo.Config.Reload(devRepo.TestRunner)
+		parent := devRepo.Config.NormalConfig.Lineage.Parent("feature-1")
+		must.EqOp(t, "main", parent.GetOrPanic())
+		must.EqOp(t, configdomain.BranchTypeFeatureBranch, devRepo.Config.BranchType("feature-1"))
+	})
+
+	t.Run("CreateStandaloneTag", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateStandaloneTag("v1.0.0")
+		tags := runtime.Tags()
+		must.Eq(t, []string{"v1.0.0"}, tags)
+	})
+
+	t.Run("CreateTag", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateTag("v1.0.0")
+		tags := runtime.Tags()
+		must.Eq(t, []string{"v1.0.0"}, tags)
+	})
+
+	t.Run("ExistingParent", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.CreateGitTown(t)
+		runtime.CreateFeatureBranch("feature-1", "main")
+		runtime.CreateFeatureBranch("feature-2", "feature-1")
+		runtime.Config.Reload(runtime.TestRunner)
+		lineage := runtime.Config.NormalConfig.Lineage
+		parent := runtime.ExistingParent("feature-2", lineage)
+		must.EqOp(t, "feature-1", parent.GetOrPanic())
+	})
+
 	t.Run("Fetch", func(t *testing.T) {
 		t.Parallel()
 		repo := testruntime.Create(t)
@@ -179,6 +418,31 @@ func TestTestCommands(t *testing.T) {
 		repo.Fetch()
 	})
 
+	t.Run("FileContent", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateFile("file1.txt", "test content")
+		content := runtime.FileContent("file1.txt")
+		must.EqOp(t, "test content", content)
+	})
+
+	t.Run("FileContentErr", func(t *testing.T) {
+		t.Run("file exists", func(t *testing.T) {
+			t.Parallel()
+			runtime := testruntime.Create(t)
+			runtime.CreateFile("file1.txt", "test content")
+			content, err := runtime.FileContentErr("file1.txt")
+			must.NoError(t, err)
+			must.EqOp(t, "test content", content)
+		})
+		t.Run("file does not exist", func(t *testing.T) {
+			t.Parallel()
+			runtime := testruntime.Create(t)
+			_, err := runtime.FileContentErr("nonexistent.txt")
+			must.Error(t, err)
+		})
+	})
+
 	t.Run("FileContentInCommit", func(t *testing.T) {
 		t.Parallel()
 		runtime := testruntime.Create(t)
@@ -195,6 +459,51 @@ func TestTestCommands(t *testing.T) {
 		must.EqOp(t, "hello world", content)
 	})
 
+	t.Run("FilesInBranch", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "initial",
+			FileContent: "content1",
+			FileName:    "file1.txt",
+			Message:     "first commit",
+		})
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "initial",
+			FileContent: "content2",
+			FileName:    "file2.txt",
+			Message:     "second commit",
+		})
+		files := runtime.FilesInBranch("initial")
+		must.Eq(t, []string{"file1.txt", "file2.txt"}, files)
+	})
+
+	t.Run("FilesInBranches", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "initial",
+			FileContent: "content",
+			FileName:    "file1.txt",
+			Message:     "commit",
+		})
+		runtime.CreateAndCheckoutBranch("branch-1", "initial")
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "branch-1",
+			FileContent: "content2",
+			FileName:    "file2.txt",
+			Message:     "branch commit",
+		})
+		table := runtime.FilesInBranches("initial")
+		want := `
+| BRANCH   | NAME      | CONTENT  |
+| initial  | file1.txt | content  |
+| branch-1 | file1.txt | content  |
+|          | file2.txt | content2 |
+`[1:]
+		must.Eq(t, want, table.String())
+	})
+
 	t.Run("FilesInCommit", func(t *testing.T) {
 		t.Parallel()
 		runtime := testruntime.Create(t)
@@ -214,17 +523,26 @@ func TestTestCommands(t *testing.T) {
 		must.Eq(t, []string{"f1.txt", "f2.txt"}, fileNames)
 	})
 
+	t.Run("FilesInWorkspace", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateFile("file1.txt", "content")
+		runtime.CreateFile("file2.txt", "content")
+		files := runtime.FilesInWorkspace()
+		must.Eq(t, []string{"file1.txt", "file2.txt"}, files)
+	})
+
 	t.Run("HasBranchesOutOfSync", func(t *testing.T) {
 		t.Run("branches are in sync", func(t *testing.T) {
 			t.Parallel()
 			fixture := fixture.NewMemoized(t.TempDir()).AsFixture()
 			devRepo := fixture.DevRepo.GetOrPanic()
-			devRepo.CreateBranch("branch1", "main")
-			devRepo.CheckoutBranch("branch1")
+			devRepo.CreateBranch("branch-1", "main")
+			devRepo.CheckoutBranch("branch-1")
 			devRepo.CreateFile("file1", "content")
 			devRepo.StageFiles("file1")
 			devRepo.CommitStagedChanges("stuff")
-			devRepo.PushBranchToRemote("branch1", gitdomain.RemoteOrigin)
+			devRepo.PushBranchToRemote("branch-1", gitdomain.RemoteOrigin)
 			have, _ := devRepo.HasBranchesOutOfSync()
 			must.False(t, have)
 		})
@@ -233,8 +551,8 @@ func TestTestCommands(t *testing.T) {
 			t.Parallel()
 			fixture := fixture.NewMemoized(t.TempDir()).AsFixture()
 			devRepo := fixture.DevRepo.GetOrPanic()
-			devRepo.CreateBranch("branch1", "main")
-			devRepo.PushBranch()
+			devRepo.CreateAndCheckoutBranch("branch-1", "main")
+			devRepo.PushBranchToRemote("branch-1", gitdomain.RemoteOrigin)
 			devRepo.CreateFile("file1", "content")
 			devRepo.StageFiles("file1")
 			devRepo.CommitStagedChanges("stuff")
@@ -246,7 +564,7 @@ func TestTestCommands(t *testing.T) {
 			t.Parallel()
 			fixture := fixture.NewMemoized(t.TempDir()).AsFixture()
 			devRepo := fixture.DevRepo.GetOrPanic()
-			devRepo.CreateBranch("branch1", "main")
+			devRepo.CreateBranch("branch-1", "main")
 			devRepo.PushBranch()
 			originRepo := fixture.OriginRepo.GetOrPanic()
 			originRepo.CheckoutBranch("main")
@@ -310,6 +628,42 @@ func TestTestCommands(t *testing.T) {
 		})
 	})
 
+	t.Run("Lineage", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.CreateGitTown(t)
+		runtime.CreateFeatureBranch("feature-1", "main")
+		lineage := runtime.Lineage()
+		parent := lineage.Parent("feature-1").GetOrPanic()
+		must.EqOp(t, "main", parent)
+	})
+
+	t.Run("LineageText", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.CreateGitTown(t)
+		runtime.CreateFeatureBranch("feature-1", "main")
+		lineage := runtime.Lineage()
+		text := runtime.LineageText(lineage)
+		want := `
+main
+  feature-1`[1:]
+		must.Eq(t, want, text)
+	})
+
+	t.Run("LocalBranches", func(t *testing.T) {
+		t.Parallel()
+		fixture := fixture.NewMemoized(t.TempDir()).AsFixture()
+		devRepo := fixture.DevRepo.GetOrPanic()
+		originRepo := fixture.OriginRepo.GetOrPanic()
+		devRepo.CreateBranch("b1", "main")
+		devRepo.CreateBranch("b2", "main")
+		originRepo.CreateBranch("remote-branch", "main")
+		devRepo.Fetch()
+		have, _, err := devRepo.LocalBranches()
+		must.NoError(t, err)
+		want := gitdomain.NewLocalBranchNames("b1", "b2", "main")
+		must.Eq(t, want, have)
+	})
+
 	t.Run("LocalBranchesMainFirst", func(t *testing.T) {
 		t.Parallel()
 		origin := testruntime.Create(t)
@@ -326,6 +680,58 @@ func TestTestCommands(t *testing.T) {
 		must.Eq(t, want, branches)
 	})
 
+	t.Run("LocalBranchesOrderedHierarchically", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.CreateGitTown(t)
+		runtime.CreateFeatureBranch("feature-1", "main")
+		runtime.CreateFeatureBranch("feature-2", "feature-1")
+		lineage := runtime.Config.NormalConfig.Lineage
+		have, _, err := runtime.LocalBranchesOrderedHierarchically(lineage, configdomain.OrderAsc)
+		must.NoError(t, err)
+		want := gitdomain.NewLocalBranchNames("main", "feature-1", "feature-2", "initial")
+		must.Eq(t, want, have)
+	})
+
+	t.Run("MergeBranch", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateBranch("branch-1", "initial")
+		runtime.CheckoutBranch("branch-1")
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "branch-1",
+			FileContent: "content",
+			FileName:    "file1.txt",
+			Message:     "commit on branch-1",
+		})
+		runtime.CheckoutBranch("initial")
+		err := runtime.MergeBranch("branch-1")
+		must.NoError(t, err)
+		files := runtime.FilesInBranch("initial")
+		must.Eq(t, []string{"file1.txt"}, files)
+	})
+
+	t.Run("PushBranch", func(t *testing.T) {
+		t.Parallel()
+		fixture := fixture.NewMemoized(t.TempDir()).AsFixture()
+		dev := fixture.DevRepo.GetOrPanic()
+		dev.CreateAndCheckoutBranch("b1", "main")
+		dev.PushBranchToRemote("b1", gitdomain.RemoteOrigin)
+		dev.CreateCommit(testgit.Commit{
+			Branch:      "b1",
+			FileContent: "content",
+			FileName:    "file1.txt",
+			Message:     "commit",
+		})
+		dev.PushBranch()
+		devCommits := dev.CommitsInBranch("b1", None[gitdomain.BranchName](), []string{})
+		must.Len(t, 1, devCommits)
+		must.EqOp(t, "commit", devCommits[0].Message)
+		origin := fixture.OriginRepo.GetOrPanic()
+		originCommits := origin.CommitsInBranch("b1", None[gitdomain.BranchName](), []string{})
+		must.Len(t, 1, originCommits)
+		must.EqOp(t, devCommits[0].Message, originCommits[0].Message)
+	})
+
 	t.Run("PushBranchToRemote", func(t *testing.T) {
 		t.Parallel()
 		dev := testruntime.Create(t)
@@ -339,6 +745,49 @@ func TestTestCommands(t *testing.T) {
 		must.Eq(t, want, branches)
 	})
 
+	t.Run("RebaseAgainstBranch", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "initial",
+			FileContent: "content1",
+			FileName:    "file1.txt",
+			Message:     "base commit",
+		})
+		runtime.CreateBranch("branch-1", "initial")
+		runtime.CheckoutBranch("branch-1")
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "branch-1",
+			FileContent: "content2",
+			FileName:    "file2.txt",
+			Message:     "branch commit",
+		})
+		runtime.CheckoutBranch("initial")
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "initial",
+			FileContent: "content3",
+			FileName:    "file3.txt",
+			Message:     "another base commit",
+		})
+		runtime.CheckoutBranch("branch-1")
+		err := runtime.RebaseAgainstBranch("initial")
+		must.NoError(t, err)
+		commits := runtime.CommitsInBranch("branch-1", None[gitdomain.BranchName](), []string{"FILE NAME"})
+		must.Len(t, 3, commits)
+		must.EqOp(t, "base commit", commits[0].Message)
+		must.EqOp(t, "another base commit", commits[1].Message)
+		must.EqOp(t, "branch commit", commits[2].Message)
+	})
+
+	t.Run("Reload", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.CreateGitTown(t)
+		runtime.CreateFeatureBranch("feature1", "main")
+		runtime.Reload()
+		parent := runtime.Config.NormalConfig.Lineage.Parent("feature1")
+		must.EqOp(t, "main", parent.GetOrPanic())
+	})
+
 	t.Run("RemoveBranch", func(t *testing.T) {
 		t.Parallel()
 		runtime := testruntime.Create(t)
@@ -354,6 +803,16 @@ func TestTestCommands(t *testing.T) {
 		must.Eq(t, wantBranches, branches)
 	})
 
+	t.Run("RemovePerennialBranchConfiguration", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		must.NoError(t, gitconfig.SetPerennialBranches(runtime.TestRunner, gitdomain.NewLocalBranchNames("qa"), configdomain.ConfigScopeLocal))
+		must.NoError(t, runtime.RemovePerennialBranchConfiguration())
+		runtime.Config.Reload(runtime.TestRunner)
+		perennials := runtime.Config.NormalConfig.PerennialBranches
+		must.Len(t, 0, perennials)
+	})
+
 	t.Run("RemoveRemote", func(t *testing.T) {
 		t.Parallel()
 		repo := testruntime.Create(t)
@@ -365,6 +824,27 @@ func TestTestCommands(t *testing.T) {
 		must.Len(t, 0, remotes)
 	})
 
+	t.Run("RemoveUnnecessaryFiles", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		_, err := os.Stat(filepath.Join(runtime.WorkingDir, ".git", "hooks"))
+		must.NoError(t, err)
+		runtime.RemoveUnnecessaryFiles()
+		_, err = os.Stat(filepath.Join(runtime.WorkingDir, ".git", "hooks"))
+		must.Error(t, err)
+	})
+
+	t.Run("RenameRemote", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		origin := testruntime.Create(t)
+		runtime.AddRemote(gitdomain.RemoteOrigin, origin.WorkingDir)
+		runtime.RenameRemote("origin", "upstream")
+		remotes, err := runtime.Git.Remotes(runtime)
+		must.NoError(t, err)
+		must.Eq(t, gitdomain.Remotes{gitdomain.RemoteUpstream}, remotes)
+	})
+
 	t.Run("SHAForCommit", func(t *testing.T) {
 		t.Parallel()
 		repo := testruntime.Create(t)
@@ -380,6 +860,49 @@ func TestTestCommands(t *testing.T) {
 		must.EqOp(t, 40, len(sha))
 	})
 
+	t.Run("SHAforBranch", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "initial",
+			FileContent: "content",
+			FileName:    "file1.txt",
+			Message:     "test commit",
+		})
+		branchSHA := runtime.SHAforBranch("initial")
+		commitSHA := runtime.SHAsForCommit("test commit").First()
+		must.EqOp(t, 40, len(branchSHA.String()))
+		must.Eq(t, commitSHA, branchSHA)
+	})
+
+	t.Run("StageFiles", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateFile("file1.txt", "content")
+		runtime.CreateFile("file2.txt", "content")
+		runtime.StageFiles("file1.txt", "file2.txt")
+		output := runtime.MustQuery("git", "status", "--porcelain")
+		want := `
+A  file1.txt
+A  file2.txt`[1:]
+		must.EqOp(t, want, output)
+	})
+
+	t.Run("Tags", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateCommit(testgit.Commit{
+			Branch:      "initial",
+			FileContent: "content",
+			FileName:    "file1.txt",
+			Message:     "test commit",
+		})
+		runtime.CreateTag("v1.0.0")
+		runtime.CreateTag("v2.0.0")
+		tags := runtime.Tags()
+		must.Eq(t, []string{"v1.0.0", "v2.0.0"}, tags)
+	})
+
 	t.Run("UncommittedFiles", func(t *testing.T) {
 		t.Parallel()
 		runtime := testruntime.Create(t)
@@ -388,4 +911,17 @@ func TestTestCommands(t *testing.T) {
 		files := runtime.UncommittedFiles()
 		must.Eq(t, []string{"f1.txt", "f2.txt"}, files)
 	})
+
+	t.Run("UnstashOpenFiles", func(t *testing.T) {
+		t.Parallel()
+		runtime := testruntime.Create(t)
+		runtime.CreateFile("file1.txt", "content")
+		runtime.StashOpenFiles()
+		uncommitted := runtime.UncommittedFiles()
+		must.Len(t, 0, uncommitted)
+		err := runtime.UnstashOpenFiles()
+		must.NoError(t, err)
+		files := runtime.UncommittedFiles()
+		must.Eq(t, []string{"file1.txt"}, files)
+	})
 }
diff --git a/internal/test/cucumber/golden.go b/internal/test/cucumber/golden.go
new file mode 100644
index 000000000..ba441ad07
--- /dev/null
+++ b/internal/test/cucumber/golden.go
@@ -0,0 +1,79 @@
+package cucumber
+
+import (
+	"fmt"
+	"os"
+	"regexp"
+	"strings"
+
+	"github.com/git-town/git-town/v22/internal/gohacks"
+	"github.com/git-town/git-town/v22/internal/gohacks/stringslice"
+)
+
+// ChangeFeatureFile updates the given section of the given feature file with the given new section.
+func ChangeFeatureFile(filePath, oldSection, newSection string) error {
+	// read file
+	content, err := os.ReadFile(filePath)
+	if err != nil {
+		return fmt.Errorf("failed to read feature file: %w", err)
+	}
+	fileLines := stringslice.Lines(string(content))
+
+	// normalize file lines for searching
+	normalizedFileLines := ReplaceSHAPlaceholder(fileLines)
+	normalizedFileLines = ReplaceSHA(normalizedFileLines)
+	normalizedFileLines = NormalizeWhitespace(normalizedFileLines)
+
+	// normalize old section for searching
+	oldSectionLines := stringslice.TrimEmptyLines(stringslice.Lines(oldSection))
+	oldSectionLines = ReplaceSHAPlaceholder(oldSectionLines)
+	oldSectionLines = ReplaceSHA(oldSectionLines)
+	oldSectionLines = NormalizeWhitespace(oldSectionLines)
+
+	// normalize new section
+	newSectionLines := stringslice.TrimEmptyLines(stringslice.Lines(newSection))
+	newSectionLines = NormalizeWhitespace(newSectionLines)
+
+	// find the old section in the file
+	startLine, found := stringslice.LocateSection(normalizedFileLines, oldSectionLines)
+	if !found {
+		fmt.Println("WANTED SECTION START")
+		fmt.Println(strings.Join(oldSectionLines, "\n"))
+		fmt.Println("WANTED SECTION END")
+		fmt.Println("FILE CONTENT START")
+		fmt.Println(strings.Join(normalizedFileLines, "\n"))
+		fmt.Println("FILE CONTENT END")
+		return fmt.Errorf("could not find section in feature file %q", filePath)
+	}
+
+	// indent the new section the same way the old one is indented in the file
+	indentation := gohacks.LeadingWhitespace(fileLines[startLine])
+	indentedNewSectionLines := stringslice.ChangeIndentNonEmpty(newSectionLines, indentation)
+
+	// replace the old section with the new one
+	newLines := append([]string{}, fileLines[:startLine]...)
+	newLines = append(newLines, indentedNewSectionLines...)
+	newLines = append(newLines, fileLines[startLine+len(oldSectionLines):]...)
+
+	// Write back to the file
+	newContent := strings.Join(newLines, "\n")
+	//nolint:gosec // need permission 644 for feature files
+	if err := os.WriteFile(filePath, []byte(newContent), 0o644); err != nil {
+		return fmt.Errorf("failed to write feature file: %w", err)
+	}
+	return nil
+}
+
+// NormalizeWhitespace collapses redundant whitespace in the given lines.
+func NormalizeWhitespace(lines []string) []string {
+	return stringslice.ReplaceRegex(lines, regexp.MustCompile(`\s{2,}`), " ")
+}
+
+func ReplaceSHA(lines []string) []string {
+	return stringslice.ReplaceRegex(lines, regexp.MustCompile(`[0-9a-f]{40}`), "SHA")
+}
+
+// ReplaceSHAPlaceholder replaces all placeholders like "{{ sha.* }}" with "SHA".
+func ReplaceSHAPlaceholder(lines []string) []string {
+	return stringslice.ReplaceRegex(lines, regexp.MustCompile(`\{\{.*?\}\}`), "SHA")
+}
diff --git a/internal/test/cucumber/golden_test.go b/internal/test/cucumber/golden_test.go
new file mode 100644
index 000000000..5f07bb70c
--- /dev/null
+++ b/internal/test/cucumber/golden_test.go
@@ -0,0 +1,337 @@
+package cucumber_test
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/git-town/git-town/v22/internal/test/cucumber"
+	"github.com/shoenig/test/must"
+)
+
+func TestNormalizeWhitespace(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name string
+		give []string
+		want []string
+	}{
+		{
+			name: "multiple spaces",
+			give: []string{
+				"one    two",
+				"three     four",
+			},
+			want: []string{
+				"one two",
+				"three four",
+			},
+		},
+		{
+			name: "tabs",
+			give: []string{
+				"one\t\t\t\ttwo",
+				"three\t\tfour",
+			},
+			want: []string{
+				"one two",
+				"three four",
+			},
+		},
+		{
+			name: "mixed whitespace",
+			give: []string{
+				"one \t  \t two",
+				"three\t \tfour",
+			},
+			want: []string{
+				"one two",
+				"three four",
+			},
+		},
+		{
+			name: "single spaces unchanged",
+			give: []string{
+				"one two",
+				"three four five",
+			},
+			want: []string{
+				"one two",
+				"three four five",
+			},
+		},
+		{
+			name: "leading and trailing whitespace",
+			give: []string{
+				"  leading",
+				"trailing  ",
+				"  both  ",
+			},
+			want: []string{
+				" leading",
+				"trailing ",
+				" both ",
+			},
+		},
+		{
+			name: "no whitespace",
+			give: []string{
+				"nowhitespace",
+				"stillnone",
+			},
+			want: []string{
+				"nowhitespace",
+				"stillnone",
+			},
+		},
+		{
+			name: "empty strings",
+			give: []string{
+				"",
+				"not empty",
+				"",
+			},
+			want: []string{
+				"",
+				"not empty",
+				"",
+			},
+		},
+		{
+			name: "empty slice",
+			give: []string{},
+			want: []string{},
+		},
+		{
+			name: "newlines and other whitespace",
+			give: []string{
+				"one\n\n\ntwo",
+				"three\r\rfour",
+				"five\f\fsix",
+			},
+			want: []string{
+				"one two",
+				"three four",
+				"five six",
+			},
+		},
+		{
+			name: "real-world example with table formatting",
+			give: []string{
+				"      | BRANCH      | COMMAND   |",
+				"      | main   | git fetch |",
+			},
+			want: []string{
+				" | BRANCH | COMMAND |",
+				" | main | git fetch |",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			have := cucumber.NormalizeWhitespace(tt.give)
+			must.Eq(t, tt.want, have)
+		})
+	}
+}
+
+func TestReplaceSHA(t *testing.T) {
+	t.Parallel()
+	give := []string{
+		"d721118fcd545d37e87100b22ef13169160bdb3c",
+		"no sha",
+		"",
+		"gggggggggggggggggggggggggggggggggggggggg", // invalid hex (should not match)
+		"0123456789abcdef0123456789abcdef01234567", // valid hex (should match)
+	}
+	want := []string{
+		"SHA",
+		"no sha",
+		"",
+		"gggggggggggggggggggggggggggggggggggggggg", // invalid hex (should not be replaced)
+		"SHA",
+	}
+	have := cucumber.ReplaceSHA(give)
+	must.Eq(t, want, have)
+}
+
+func TestReplaceSHAPlaceholder(t *testing.T) {
+	t.Parallel()
+
+	give := []string{
+		"one {{ sha 'foo' }} two",
+		"one {{ sha-in-origin 'bar' }} two",
+		"git reset --hard {{ sha-initial 'alpha commit' }}",
+		"no placeholder",
+		"",
+	}
+	want := []string{
+		"one SHA two",
+		"one SHA two",
+		"git reset --hard SHA",
+		"no placeholder",
+		"",
+	}
+	have := cucumber.ReplaceSHAPlaceholder(give)
+	must.Eq(t, want, have)
+}
+
+func TestUpdateFeatureFileWithCommands(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		file     string
+		oldTable string
+		newTable string
+		want     string
+	}{
+		{
+			name: "replace table with proper indentation",
+			file: `
+Feature: test
+  Scenario: test
+    Then Git Town runs the commands
+      | BRANCH | COMMAND |
+      | main   | git fetch |
+    And some other step`[1:],
+			oldTable: `
+      | BRANCH | COMMAND |
+      | main   | git fetch |`[1:],
+			newTable: `
+| BRANCH | COMMAND |
+| main   | git pull |`[1:],
+			want: `
+Feature: test
+  Scenario: test
+    Then Git Town runs the commands
+      | BRANCH | COMMAND |
+      | main | git pull |
+    And some other step`[1:],
+		},
+		{
+			name: "replace table with different number of rows",
+			file: `
+Feature: test
+  Scenario: test
+    Then Git Town runs the commands
+      | BRANCH      | COMMAND   |
+      | development | git fetch |
+    And done`[1:],
+			oldTable: `
+			| BRANCH      | COMMAND |
+			| development | git fetch |`[1:],
+			newTable: `
+			| BRANCH | COMMAND |
+			| main   | git init |
+			| main   | git pull |`[1:],
+			want: `
+Feature: test
+  Scenario: test
+    Then Git Town runs the commands
+      | BRANCH | COMMAND |
+      | main | git init |
+      | main | git pull |
+    And done`[1:],
+		},
+		{
+			name: "old table has more empty lines",
+			file: `
+Feature: test
+  Scenario: test
+    Then Git Town runs the commands
+      | BRANCH | COMMAND |
+      | main   | git fetch |
+    And done`[1:],
+			oldTable: `
+
+			| BRANCH | COMMAND |
+			| main | git fetch |
+
+`,
+			newTable: `
+			| BRANCH | COMMAND |
+			| main | git init |
+			| main | git pull |`[1:],
+			want: `
+Feature: test
+  Scenario: test
+    Then Git Town runs the commands
+      | BRANCH | COMMAND |
+      | main | git init |
+      | main | git pull |
+    And done`[1:],
+		},
+		{
+			name: "newTable has more empty lines",
+			file: `
+Feature: test
+  Scenario: test
+    Then Git Town runs the commands
+      | BRANCH | COMMAND |
+      | main   | git fetch |
+    And done`[1:],
+			oldTable: `
+			| BRANCH | COMMAND |
+			| main | git fetch |`[1:],
+			newTable: `
+
+			| BRANCH | COMMAND |
+			| main | git init |
+			| main | git pull |
+
+`,
+			want: `
+Feature: test
+  Scenario: test
+    Then Git Town runs the commands
+      | BRANCH | COMMAND |
+      | main | git init |
+      | main | git pull |
+    And done`[1:],
+		},
+		{
+			name: "SHA placeholders",
+			file: `
+Feature: test
+  Scenario: test
+    Then Git Town runs the commands
+      | BRANCH | COMMAND |
+      | main   | git reset --hard {{ sha 'commit' }} |
+    And done`[1:],
+			oldTable: `
+      | BRANCH | COMMAND                                                   |
+      | main | git reset --hard d721118fcd545d37e87100b22ef13169160bdb3c |`[1:],
+			newTable: `
+			| BRANCH | COMMAND                                                   |
+			| main | git reset --soft d721118fcd545d37e87100b22ef13169160bdb3c |`[1:],
+			want: `
+Feature: test
+  Scenario: test
+    Then Git Town runs the commands
+      | BRANCH | COMMAND |
+      | main | git reset --soft d721118fcd545d37e87100b22ef13169160bdb3c |
+    And done`[1:],
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			tmpDir := t.TempDir()
+			tmpFile := filepath.Join(tmpDir, "test.feature")
+			err := os.WriteFile(tmpFile, []byte(tt.file), 0o600)
+			must.NoError(t, err)
+
+			err = cucumber.ChangeFeatureFile(tmpFile, tt.oldTable, tt.newTable)
+			must.NoError(t, err)
+
+			result, err := os.ReadFile(tmpFile)
+			must.NoError(t, err)
+			must.EqOp(t, tt.want, string(result))
+		})
+	}
+}
diff --git a/internal/test/cucumber/scenario_state.go b/internal/test/cucumber/scenario_state.go
index e1605e5ed..f40ece006 100644
--- a/internal/test/cucumber/scenario_state.go
+++ b/internal/test/cucumber/scenario_state.go
@@ -96,11 +96,15 @@ func (self *ScenarioState) CaptureState() {
 
 // compareExistingCommits compares the commits in the Git environment of the given ScenarioState
 // against the given Gherkin table.
-func (self *ScenarioState) compareGherkinTable(table *godog.Table) error {
+func (self *ScenarioState) compareGherkinTable(table *godog.Table, scenarioURI string) error {
 	fields := helpers.TableFields(table)
 	commitTable := self.fixture.CommitTable(fields)
 	diff, errorCount := commitTable.EqualGherkin(table)
 	if errorCount != 0 {
+		if CukeUpdate {
+			expectedTable := datatable.FromGherkin(table)
+			return ChangeFeatureFile(scenarioURI, expectedTable.String(), commitTable.String())
+		}
 		fmt.Printf("\nERROR! Found %d differences in the existing commits\n\n", errorCount)
 		fmt.Println(diff)
 		return errors.New("mismatching commits found, see diff above")
diff --git a/internal/test/cucumber/steps.go b/internal/test/cucumber/steps.go
index 472ac9e5d..c18feec6d 100644
--- a/internal/test/cucumber/steps.go
+++ b/internal/test/cucumber/steps.go
@@ -15,12 +15,12 @@ import (
 	"github.com/acarl005/stripansi"
 	"github.com/cucumber/godog"
 	messages "github.com/cucumber/messages/go/v21"
-	"github.com/git-town/git-town/v22/internal/browser"
 	"github.com/git-town/git-town/v22/internal/cli/dialog/dialogcomponents"
 	"github.com/git-town/git-town/v22/internal/cli/format"
 	"github.com/git-town/git-town/v22/internal/cli/print"
 	"github.com/git-town/git-town/v22/internal/config/configdomain"
 	"github.com/git-town/git-town/v22/internal/config/configfile"
+	"github.com/git-town/git-town/v22/internal/config/envconfig"
 	"github.com/git-town/git-town/v22/internal/config/gitconfig"
 	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
 	"github.com/git-town/git-town/v22/internal/git/gitdomain"
@@ -43,6 +43,9 @@ import (
 // the global FixtureFactory instance.
 var fixtureFactory *fixture.Factory
 
+// CukeUpdate indicates whether to update .feature files with actual command output when tests fail
+var CukeUpdate bool
+
 // dedicated type for storing data in context.Context
 type key int
 
@@ -51,12 +54,14 @@ const (
 	keyScenarioState key = iota
 	keyScenarioName
 	keyScenarioTags
+	keyScenarioURI
 )
 
 func InitializeScenario(scenarioContext *godog.ScenarioContext) {
 	scenarioContext.Before(func(ctx context.Context, scenario *godog.Scenario) (context.Context, error) {
 		ctx = context.WithValue(ctx, keyScenarioName, scenario.Name)
 		ctx = context.WithValue(ctx, keyScenarioTags, scenario.Tags)
+		ctx = context.WithValue(ctx, keyScenarioURI, scenario.Uri)
 		return ctx, nil
 	})
 
@@ -239,13 +244,13 @@ func defineSteps(sc *godog.ScenarioContext) {
 		state.fixture.AddUpstream()
 	})
 
-	sc.Step(`^a proposal for this branch does not exist`, func(ctx context.Context) {
+	sc.Step(`^a proposal for this branch does not exist$`, func(ctx context.Context) {
 		state := ctx.Value(keyScenarioState).(*ScenarioState)
 		devRepo := state.fixture.DevRepo.GetOrPanic()
 		devRepo.TestRunner.ProposalOverride = Some(forgedomain.OverrideNoProposal)
 	})
 
-	sc.Step(`^a proposal for this branch exists at "([^"]+)"`, func(ctx context.Context, url string) {
+	sc.Step(`^a proposal for this branch exists at "([^"]+)"$`, func(ctx context.Context, url string) {
 		state := ctx.Value(keyScenarioState).(*ScenarioState)
 		devRepo := state.fixture.DevRepo.GetOrPanic()
 		devRepo.TestRunner.ProposalOverride = Some(url)
@@ -261,7 +266,7 @@ func defineSteps(sc *godog.ScenarioContext) {
 		return nil
 	})
 
-	sc.Step(`^a remote "([^"]+)" pointing to "([^"]+)"`, func(ctx context.Context, name, url string) {
+	sc.Step(`^a remote "([^"]+)" pointing to "([^"]+)"$`, func(ctx context.Context, name, url string) {
 		state := ctx.Value(keyScenarioState).(*ScenarioState)
 		devRepo := state.fixture.DevRepo.GetOrPanic()
 		devRepo.AddRemote(gitdomain.Remote(name), url)
@@ -272,7 +277,7 @@ func defineSteps(sc *godog.ScenarioContext) {
 		state.fixture.OriginRepo.GetOrPanic().CreateStandaloneTag(name)
 	})
 
-	sc.Step(`^branch "([^"]+)" is active in another worktree`, func(ctx context.Context, branch string) {
+	sc.Step(`^branch "([^"]+)" is active in another worktree$`, func(ctx context.Context, branch string) {
 		state := ctx.Value(keyScenarioState).(*ScenarioState)
 		state.fixture.AddSecondWorktree(gitdomain.NewLocalBranchName(branch))
 	})
@@ -281,7 +286,7 @@ func defineSteps(sc *godog.ScenarioContext) {
 		state := ctx.Value(keyScenarioState).(*ScenarioState)
 		devRepo := state.fixture.DevRepo.GetOrPanic()
 		branch := gitdomain.NewLocalBranchName(branchName)
-		wantOpt := asserts.NoError1(configdomain.ParseBranchType(branchTypeName))
+		wantOpt := asserts.NoError1(configdomain.ParseBranchType(branchTypeName, "test"))
 		want := wantOpt.GetOrPanic()
 		have := devRepo.Config.BranchType(branch)
 		if have != want {
@@ -290,12 +295,12 @@ func defineSteps(sc *godog.ScenarioContext) {
 		return nil
 	})
 
-	sc.Step(`^commit "([^"]+)" on branch "([^"]+)" now has this full commit message`, func(ctx context.Context, title, branchText string, expected *godog.DocString) error {
+	sc.Step(`^commit "([^"]+)" on branch "([^"]+)" now has this full commit message$`, func(ctx context.Context, title, branchText string, expected *godog.DocString) error {
 		state := ctx.Value(keyScenarioState).(*ScenarioState)
 		devRepo := state.fixture.DevRepo.GetOrPanic()
 		branch := gitdomain.NewLocalBranchName(branchText)
 		parent := devRepo.Config.NormalConfig.Lineage.Parent(branch).GetOrPanic()
-		sha := devRepo.CommitSHA(devRepo, title, branch, parent.BranchName())
+		sha := devRepo.CommitSHA(devRepo, gitdomain.CommitTitle(title), branch, parent.BranchName())
 		have := asserts.NoError1(devRepo.Git.CommitMessage(devRepo, sha)).String()
 		want := expected.Content
 		if have != want {
@@ -477,6 +482,10 @@ func defineSteps(sc *godog.ScenarioContext) {
 		})
 		diff, errorCount := table.EqualDataTable(expanded)
 		if errorCount != 0 {
+			if CukeUpdate {
+				scenarioURI := ctx.Value(keyScenarioURI).(string)
+				return ChangeFeatureFile(scenarioURI, expanded.String(), table.String())
+			}
 			fmt.Printf("\nERROR! Found %d differences in the commands run\n\n", errorCount)
 			fmt.Println(diff)
 			return errors.New("mismatching commands run, see diff above")
@@ -484,7 +493,7 @@ func defineSteps(sc *godog.ScenarioContext) {
 		return nil
 	})
 
-	sc.Step(`^Git Town runs without errors`, func(ctx context.Context) error {
+	sc.Step(`^Git Town runs without errors$`, func(ctx context.Context) error {
 		state := ctx.Value(keyScenarioState).(*ScenarioState)
 		exitCode := state.runExitCode.GetOrPanic()
 		if exitCode != 0 {
@@ -698,7 +707,7 @@ func defineSteps(sc *godog.ScenarioContext) {
 		updateInitialSHAs(state)
 		env := os.Environ()
 		if browserPath, has := state.browserVariable.Get(); has {
-			env = envvars.Replace(env, browser.EnvVarName, browserPath)
+			env = envvars.Replace(env, envconfig.Browser, browserPath)
 		}
 		output, exitCode := devRepo.MustQueryStringCodeWith(cmd, &subshell.Options{
 			Env:   env,
@@ -817,7 +826,7 @@ func defineSteps(sc *godog.ScenarioContext) {
 		devRepo.MockCommitMessage(message)
 		env := os.Environ()
 		if browserPath, has := state.browserVariable.Get(); has {
-			env = envvars.Replace(env, browser.EnvVarName, browserPath)
+			env = envvars.Replace(env, envconfig.Browser, browserPath)
 		}
 		output, exitCode := devRepo.MustQueryStringCodeWith(cmd, &subshell.Options{
 			Env: env,
@@ -868,7 +877,7 @@ func defineSteps(sc *godog.ScenarioContext) {
 		updateInitialSHAs(state)
 		env := os.Environ()
 		if browserPath, has := state.browserVariable.Get(); has {
-			env = envvars.Replace(env, browser.EnvVarName, browserPath)
+			env = envvars.Replace(env, envconfig.Browser, browserPath)
 		}
 		env = append(env, envVar1, envVar2, envVar3, envVar4)
 		for a, answer := range helpers.TableToInputEnv(input) {
@@ -902,7 +911,7 @@ func defineSteps(sc *godog.ScenarioContext) {
 		updateInitialSHAs(state)
 		env := os.Environ()
 		if browserPath, has := state.browserVariable.Get(); has {
-			env = envvars.Replace(env, browser.EnvVarName, browserPath)
+			env = envvars.Replace(env, envconfig.Browser, browserPath)
 		}
 		for a, answer := range helpers.TableToInputEnv(input) {
 			env = append(env, fmt.Sprintf("%s_%02d=%s", dialogcomponents.InputKey, a, answer))
@@ -972,7 +981,7 @@ func defineSteps(sc *godog.ScenarioContext) {
 
 	sc.Step(`^no tool to open browsers is installed$`, func(ctx context.Context) {
 		state := ctx.Value(keyScenarioState).(*ScenarioState)
-		state.browserVariable = Some(string(browser.EnvVarNone))
+		state.browserVariable = Some(string(configdomain.NoBrowser))
 	})
 
 	sc.Step(`^no uncommitted files exist now$`, func(ctx context.Context) error {
@@ -1217,7 +1226,7 @@ func defineSteps(sc *godog.ScenarioContext) {
 	sc.Step(`^the coworker sets the "sync-feature-strategy" to "(merge|rebase)"$`, func(ctx context.Context, value string) {
 		state := ctx.Value(keyScenarioState).(*ScenarioState)
 		coworkerRepo := state.fixture.CoworkerRepo.GetOrPanic()
-		syncFeatureStrategy := asserts.NoError1(configdomain.ParseSyncFeatureStrategy(value))
+		syncFeatureStrategy := asserts.NoError1(configdomain.ParseSyncFeatureStrategy(value, "test"))
 		_ = gitconfig.SetSyncFeatureStrategy(coworkerRepo.TestRunner, syncFeatureStrategy.GetOrPanic(), configdomain.ConfigScopeLocal)
 	})
 
@@ -1457,7 +1466,8 @@ func defineSteps(sc *godog.ScenarioContext) {
 				return errors.New(`please use the step "the initial commits exist now" instead`)
 			}
 		}
-		return state.compareGherkinTable(table)
+		scenarioURI := ctx.Value(keyScenarioURI).(string)
+		return state.compareGherkinTable(table, scenarioURI)
 	})
 
 	sc.Step(`^these committed files exist now$`, func(ctx context.Context, table *godog.Table) error {
@@ -1466,6 +1476,11 @@ func defineSteps(sc *godog.ScenarioContext) {
 		fileTable := devRepo.FilesInBranches("main")
 		diff, errorCount := fileTable.EqualGherkin(table)
 		if errorCount != 0 {
+			if CukeUpdate {
+				scenarioURI := ctx.Value(keyScenarioURI).(string)
+				expectedTable := datatable.FromGherkin(table)
+				return ChangeFeatureFile(scenarioURI, expectedTable.String(), fileTable.String())
+			}
 			fmt.Printf("\nERROR! Found %d differences in the existing files\n\n", errorCount)
 			fmt.Println(diff)
 			return errors.New("mismatching files found, see diff above")
@@ -1544,7 +1559,7 @@ func defineSteps(sc *godog.ScenarioContext) {
 
 	// This step exists to avoid re-creating commits with the same SHA as existing commits
 	// because that can cause flaky tests.
-	sc.Step(`wait 1 second to ensure new Git timestamps`, func() {
+	sc.Step(`^wait 1 second to ensure new Git timestamps$`, func() {
 		time.Sleep(1 * time.Second)
 	})
 }
@@ -1559,7 +1574,7 @@ func runCommand(state *ScenarioState, command string, captureState bool) {
 	var runOutput string
 	env := os.Environ()
 	if browserVariable, hasBrowserOverride := state.browserVariable.Get(); hasBrowserOverride {
-		env = envvars.Replace(env, browser.EnvVarName, browserVariable)
+		env = envvars.Replace(env, envconfig.Browser, browserVariable)
 	}
 	if hasDevRepo {
 		runOutput, exitCode = devRepo.MustQueryStringCodeWith(command, &subshell.Options{
@@ -1569,7 +1584,7 @@ func runCommand(state *ScenarioState, command string, captureState bool) {
 	} else {
 		parts := asserts.NoError1(shellquote.Split(command))
 		cmd, args := parts[0], parts[1:]
-		subProcess := exec.Command(cmd, args...) // #nosec
+		subProcess := exec.CommandContext(context.Background(), cmd, args...) // #nosec
 		subProcess.Dir = state.fixture.Dir
 		outputBytes, _ := subProcess.CombinedOutput()
 		runOutput = string(outputBytes)
diff --git a/internal/test/datatable/branch_setup_table.go b/internal/test/datatable/branch_setup_table.go
index 9936b4398..31319344a 100644
--- a/internal/test/datatable/branch_setup_table.go
+++ b/internal/test/datatable/branch_setup_table.go
@@ -30,7 +30,7 @@ func ParseBranchSetupTable(table *godog.Table) []BranchSetup {
 			case "NAME":
 				name = gitdomain.NewLocalBranchNameOption(cell.Value)
 			case "TYPE":
-				branchType = asserts.NoError1(configdomain.ParseBranchType(cell.Value))
+				branchType = asserts.NoError1(configdomain.ParseBranchType(cell.Value, "cucumber table"))
 			case "PARENT":
 				if len(cell.Value) > 0 {
 					parent = gitdomain.NewLocalBranchNameOption(cell.Value)
diff --git a/internal/test/datatable/data_table.go b/internal/test/datatable/data_table.go
index a407551df..c0e07315f 100644
--- a/internal/test/datatable/data_table.go
+++ b/internal/test/datatable/data_table.go
@@ -104,14 +104,14 @@ func (self *DataTable) String() string {
 		formatStrings[w] = fmt.Sprintf("| %%-%dv ", width)
 	}
 	// render the self using this format
-	result := ""
+	result := strings.Builder{}
 	for row := range self.Cells {
 		for col := range self.Cells[row] {
-			result += fmt.Sprintf(formatStrings[col], gohacks.EscapeNewLines(self.Cells[row][col]))
+			result.WriteString(fmt.Sprintf(formatStrings[col], gohacks.EscapeNewLines(self.Cells[row][col])))
 		}
-		result += "|\n"
+		result.WriteString("|\n")
 	}
-	return result
+	return result.String()
 }
 
 // columns provides the self data organized into columns.
diff --git a/internal/test/envvars/prepend_path.go b/internal/test/envvars/prepend_path.go
index 02814629d..686a0fea4 100644
--- a/internal/test/envvars/prepend_path.go
+++ b/internal/test/envvars/prepend_path.go
@@ -9,8 +9,7 @@ import (
 // This function assumes there is only one PATH entry.
 func PrependPath(envVars []string, directory string) []string {
 	for e, envVar := range envVars {
-		if strings.HasPrefix(envVar, "PATH=") {
-			_, value, _ := strings.Cut(envVar, "=")
+		if value, isPath := strings.CutPrefix(envVar, "PATH="); isPath {
 			envVars[e] = "PATH=" + directory + string(os.PathListSeparator) + value
 			return envVars
 		}
diff --git a/internal/test/filesystem/folder_name.go b/internal/test/filesystem/folder_name.go
index e1e95eb62..64b57db0c 100644
--- a/internal/test/filesystem/folder_name.go
+++ b/internal/test/filesystem/folder_name.go
@@ -1,23 +1,26 @@
 package filesystem
 
-import "unicode"
+import (
+	"strings"
+	"unicode"
+)
 
 // FolderName converts the given scenario name into a string
 // that can be used safely as a folder name on the filesystem.
 func FolderName(scenarioName string) string {
-	result := ""
+	result := strings.Builder{}
 	lastRune := ' '
 	for _, r := range scenarioName {
 		if unicode.IsLetter(r) {
 			r = unicode.ToLower(r)
-			result += string(r)
+			result.WriteRune(r)
 			lastRune = r
 			continue
 		}
 		if lastRune != '_' {
-			result += "_"
+			result.WriteRune('_')
 			lastRune = '_'
 		}
 	}
-	return result
+	return result.String()
 }
diff --git a/internal/test/fixture/factory.go b/internal/test/fixture/factory.go
index e45592f86..7dac5e00c 100644
--- a/internal/test/fixture/factory.go
+++ b/internal/test/fixture/factory.go
@@ -41,14 +41,6 @@ func CreateFactory() Factory {
 	}
 }
 
-// CreateEmptyFixture provides an empty Fixture consisting of a brand-new Git repository.
-// This is useful for scenarios that require testing the behavior of Git Town in a fresh repository.
-func (self *Factory) CreateEmptyFixture(scenarioName string) Fixture {
-	envDirName := filesystem.FolderName(scenarioName) + "_" + self.Counter.NextAsString()
-	envPath := filepath.Join(self.Dir, envDirName)
-	return NewFresh(envPath).AsFixture()
-}
-
 // CreateFixture provides a new Fixture for the scenario with the given name.
 func (self *Factory) CreateFixture(scenarioName string) Fixture {
 	envDirName := filesystem.FolderName(scenarioName) + "_" + self.Counter.NextAsString()
diff --git a/internal/test/fixture/factory_test.go b/internal/test/fixture/factory_test.go
index 1d5eac2e6..46e3c5db2 100644
--- a/internal/test/fixture/factory_test.go
+++ b/internal/test/fixture/factory_test.go
@@ -11,15 +11,6 @@ import (
 func TestFixtureFactory(t *testing.T) {
 	t.Parallel()
 
-	t.Run("CreateEmptyFixture", func(t *testing.T) {
-		t.Parallel()
-		factory := fixture.CreateFactory()
-		defer factory.Remove()
-		result := factory.CreateEmptyFixture("foo")
-		_, err := os.Stat(result.DevRepo.GetOrPanic().WorkingDir)
-		must.False(t, os.IsNotExist(err))
-	})
-
 	t.Run("CreateFixture", func(t *testing.T) {
 		t.Parallel()
 		factory := fixture.CreateFactory()
diff --git a/internal/test/fixture/fixture_test.go b/internal/test/fixture/fixture_test.go
index 5dc0d456c..55ccc505b 100644
--- a/internal/test/fixture/fixture_test.go
+++ b/internal/test/fixture/fixture_test.go
@@ -93,13 +93,11 @@ func TestFixture(t *testing.T) {
 			})
 			// get the CommitTable
 			table := cloned.CommitTable([]string{"LOCATION", "FILE NAME", "FILE CONTENT"})
-			must.Len(t, 3, table.Cells)
-			must.EqOp(t, table.Cells[1][0], "local, origin")
-			must.EqOp(t, table.Cells[1][1], "local-origin.md")
-			must.EqOp(t, table.Cells[1][2], "one")
-			must.EqOp(t, table.Cells[2][0], "origin")
-			must.EqOp(t, table.Cells[2][1], "origin.md")
-			must.EqOp(t, table.Cells[2][2], "two")
+			must.Eq(t, table.Cells, [][]string{
+				{"LOCATION", "FILE NAME", "FILE CONTENT"},
+				{"local, origin", "local-origin.md", "one"},
+				{"origin", "origin.md", "two"},
+			})
 		})
 
 		t.Run("with upstream repo", func(t *testing.T) {
@@ -124,13 +122,11 @@ func TestFixture(t *testing.T) {
 			})
 			// get the CommitTable
 			table := cloned.CommitTable([]string{"LOCATION", "FILE NAME", "FILE CONTENT"})
-			must.Len(t, 3, table.Cells)
-			must.EqOp(t, table.Cells[1][0], "local")
-			must.EqOp(t, table.Cells[1][1], "local.md")
-			must.EqOp(t, table.Cells[1][2], "one")
-			must.EqOp(t, table.Cells[2][0], "upstream")
-			must.EqOp(t, table.Cells[2][1], "upstream.md")
-			must.EqOp(t, table.Cells[2][2], "two")
+			must.Eq(t, table.Cells, [][]string{
+				{"LOCATION", "FILE NAME", "FILE CONTENT"},
+				{"local", "local.md", "one"},
+				{"upstream", "upstream.md", "two"},
+			})
 		})
 	})
 
diff --git a/internal/test/fixture/fresh.go b/internal/test/fixture/fresh.go
deleted file mode 100644
index a702994ca..000000000
--- a/internal/test/fixture/fresh.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package fixture
-
-import (
-	"os"
-
-	"github.com/git-town/git-town/v22/internal/test/commands"
-	"github.com/git-town/git-town/v22/internal/test/testruntime"
-	"github.com/git-town/git-town/v22/pkg/asserts"
-	. "github.com/git-town/git-town/v22/pkg/prelude"
-)
-
-// An empty Git repo for testing.
-// This is useful for scenarios that require testing the behavior of Git Town in a fresh repository.
-type Fresh struct {
-	Dir string
-}
-
-// NewFresh provides a Fresh instance in the given directory.
-//
-// The repo has no branches.
-func NewFresh(dir string) Fresh {
-	binPath := binPath(dir)
-	devRepoPath := developerRepoPath(dir)
-	// create the "developer" repo
-	err := os.MkdirAll(devRepoPath, 0o744)
-	asserts.NoError(err)
-	// initialize the repo in the folder
-	devRepo := testruntime.InitializeNoInitialCommit(devRepoPath, dir, binPath)
-	devRepo.RemoveUnnecessaryFiles()
-	return Fresh{dir}
-}
-
-// allows using this fresh environment as a Fixture
-func (self Fresh) AsFixture() Fixture {
-	binDir := binPath(self.Dir)
-	developerDir := developerRepoPath(self.Dir)
-	devRepo := testruntime.New(developerDir, self.Dir, binDir)
-	return Fixture{
-		CoworkerRepo:   MutableNone[commands.TestCommands](),
-		DevRepo:        MutableSome(&devRepo),
-		Dir:            self.Dir,
-		OriginRepo:     MutableNone[commands.TestCommands](),
-		SecondWorktree: MutableNone[commands.TestCommands](),
-		SubmoduleRepo:  MutableNone[commands.TestCommands](),
-		UpstreamRepo:   MutableNone[commands.TestCommands](),
-	}
-}
diff --git a/internal/test/fixture/fresh_test.go b/internal/test/fixture/fresh_test.go
deleted file mode 100644
index 993548960..000000000
--- a/internal/test/fixture/fresh_test.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package fixture_test
-
-import (
-	"path/filepath"
-	"testing"
-
-	"github.com/git-town/git-town/v22/internal/test/fixture"
-	"github.com/git-town/git-town/v22/pkg/asserts"
-	"github.com/shoenig/test/must"
-)
-
-func TestNewFreshFixture(t *testing.T) {
-	t.Parallel()
-	gitEnvRootDir := t.TempDir()
-	result := fixture.NewFresh(gitEnvRootDir).AsFixture()
-	devRepo := result.DevRepo.GetOrPanic()
-	// verify the developer repo
-	asserts.IsGitRepo(t, filepath.Join(gitEnvRootDir, "developer"))
-	branch := asserts.NoError1(devRepo.Git.CurrentBranch(devRepo.TestRunner)).GetOrPanic()
-	must.EqOp(t, "initial", branch)
-}
diff --git a/internal/test/ostools/unix.go b/internal/test/ostools/unix.go
index 8ca6c8688..b8aa847fb 100644
--- a/internal/test/ostools/unix.go
+++ b/internal/test/ostools/unix.go
@@ -1,5 +1,4 @@
 //go:build !windows
-// +build !windows
 
 package ostools
 
diff --git a/internal/test/ostools/windows.go b/internal/test/ostools/windows.go
index b10d52bf7..b15636c59 100644
--- a/internal/test/ostools/windows.go
+++ b/internal/test/ostools/windows.go
@@ -1,5 +1,4 @@
 //go:build windows
-// +build windows
 
 package ostools
 
diff --git a/internal/test/output/git_town_output_parser.go b/internal/test/output/git_town_output_parser.go
index 721e9459c..9fb17cab8 100644
--- a/internal/test/output/git_town_output_parser.go
+++ b/internal/test/output/git_town_output_parser.go
@@ -4,6 +4,7 @@ import (
 	"strings"
 
 	"github.com/acarl005/stripansi"
+	"github.com/git-town/git-town/v22/internal/gohacks/stringslice"
 	. "github.com/git-town/git-town/v22/pkg/prelude"
 )
 
@@ -31,7 +32,7 @@ func (self CommandType) String() string { return string(self) }
 // GitCommandsInGitTownOutput provides the Git commands mentioned in the given Git Town output.
 func GitCommandsInGitTownOutput(output string) []ExecutedGitCommand {
 	var result []ExecutedGitCommand
-	for _, line := range strings.Split(output, "\n") {
+	for _, line := range stringslice.NonEmptyLines(output) {
 		if lineContainsFrontendCommand(line) {
 			if line, hasLine := parseFrontendLine(line).Get(); hasLine {
 				result = append(result, line)
diff --git a/internal/test/testgit/locations.go b/internal/test/testgit/locations.go
index bc8f5bf60..97c1d8897 100644
--- a/internal/test/testgit/locations.go
+++ b/internal/test/testgit/locations.go
@@ -1,6 +1,9 @@
 package testgit
 
-import "strings"
+import (
+	"slices"
+	"strings"
+)
 
 type Locations []Location
 
@@ -15,12 +18,7 @@ func NewLocations(cucumberFormat string) Locations {
 
 // Contains indicates whether this Locations instance contains the given location.
 func (self Locations) Contains(location Location) bool {
-	for _, myLocation := range self {
-		if myLocation == location {
-			return true
-		}
-	}
-	return false
+	return slices.Contains(self, location)
 }
 
 // indicates whetehr this Locations instance contains exactly the given elements
diff --git a/main_test.go b/main_test.go
index f407278da..06d8418e4 100644
--- a/main_test.go
+++ b/main_test.go
@@ -25,6 +25,7 @@ func TestMain(_ *testing.M) {
 	flagSmoke := len(os.Getenv("smoke")) > 0
 	flagMessyOutput := os.Getenv("messyoutput")
 	flagVerbose := len(os.Getenv("verbose")) > 0
+	cucumber.CukeUpdate = len(os.Getenv("cukeupdate")) > 0
 	switch {
 	case flagThis:
 		options.Format = "pretty"
diff --git a/pkg/set/set_test.go b/pkg/set/set_test.go
index 2d8326bbe..c2977c9a1 100644
--- a/pkg/set/set_test.go
+++ b/pkg/set/set_test.go
@@ -29,6 +29,39 @@ func TestSet(t *testing.T) {
 		must.Eq(t, []int{1, 2, 3}, have.Values())
 	})
 
+	t.Run("Contains", func(t *testing.T) {
+		t.Parallel()
+
+		t.Run("returns false for empty set", func(t *testing.T) {
+			t.Parallel()
+			s := set.New[int]()
+			must.False(t, s.Contains(1))
+		})
+
+		t.Run("returns true when value exists", func(t *testing.T) {
+			t.Parallel()
+			s := set.New(1, 2, 3)
+			must.True(t, s.Contains(1))
+			must.True(t, s.Contains(2))
+			must.True(t, s.Contains(3))
+		})
+
+		t.Run("returns false when value does not exist", func(t *testing.T) {
+			t.Parallel()
+			s := set.New(1, 2, 3)
+			must.False(t, s.Contains(4))
+			must.False(t, s.Contains(0))
+		})
+
+		t.Run("works with string type", func(t *testing.T) {
+			t.Parallel()
+			s := set.New("a", "b", "c")
+			must.True(t, s.Contains("a"))
+			must.True(t, s.Contains("b"))
+			must.False(t, s.Contains("d"))
+		})
+	})
+
 	t.Run("New", func(t *testing.T) {
 		t.Parallel()
 
diff --git a/text-runner-tests/gittown-command.test.ts b/text-runner-tests/gittown-command.test.ts
new file mode 100644
index 000000000..dbdbc6575
--- /dev/null
+++ b/text-runner-tests/gittown-command.test.ts
@@ -0,0 +1,206 @@
+import { deepEqual, equal } from "node:assert/strict"
+import { suite, test } from "node:test"
+import * as command from "../text-runner/git-town-command.ts"
+
+suite("SummarySection", () => {
+  suite("args", () => {
+    const tests = [
+      {
+        desc: "append command",
+        give:
+          "git town append <branch-name> [-p | --prototype] [-d | --detached] [-c | --commit] [(-m | --message) <message>] [--propose] [--dry-run] [-v | --verbose]",
+        want: [
+          ["-p", "--prototype"],
+          ["-d", "--detached"],
+          ["-c", "--commit"],
+          ["-m", "--message string"],
+          ["--propose"],
+          ["--dry-run"],
+          ["-v", "--verbose"],
+        ],
+      },
+      {
+        desc: "completions command",
+        give: "git town completions (bash|fish|powershell|zsh) [--no-descriptions] [-h | --help]",
+        want: [
+          ["--no-descriptions"],
+          ["-h", "--help"],
+        ],
+      },
+      {
+        desc: "config get-parent command",
+        give: "git town config get-parent [<branch-name>] [-v | --verbose] [-h | --help]",
+        want: [
+          ["-v", "--verbose"],
+          ["-h", "--help"],
+        ],
+      },
+    ]
+    for (const { desc, give, want } of tests) {
+      test(desc, () => {
+        const summarySection = new command.SummarySection(give)
+        const have = summarySection.args()
+        deepEqual(have, want)
+      })
+    }
+  })
+
+  suite("command", () => {
+    const tests = {
+      "git town append": "append",
+      "git town config get-parent": "config get-parent",
+      "git town sync": "sync",
+      "git town ship": "ship",
+      "git town append <branch-name> [-p | --prototype]": "append",
+      "git town sync --all": "sync",
+      "git town ship --all": "ship",
+      "git town completions (bash|fish|powershell|zsh) [--no-descriptions] [-h | --help]": "completions",
+      "git town config get-parent [<branch-name>] [-v | --verbose] [-h | --help]": "config get-parent",
+    }
+    for (const [give, want] of Object.entries(tests)) {
+      test(give, () => {
+        const summarySection = new command.SummarySection(give)
+        const have = summarySection.command().name
+        equal(have, want)
+      })
+    }
+  })
+})
+
+suite("HelpOutput.flags", () => {
+  test("append command", () => {
+    const output = new command.HelpOutput(`
+Create a new feature branch as a child of the current branch.
+
+Consider this stack:
+
+main
+ \
+* feature-1
+
+We are on the "feature-1" branch,
+which is a child of branch "main".
+After running "git town append feature-2",
+the repository will have these branches:
+
+main
+ \
+  feature-1
+   \
+*   feature-2
+
+The new branch "feature-2"
+is a child of "feature-1".
+
+If there are no uncommitted changes,
+it also syncs all affected branches.
+
+Usage:
+  git-town append <branch> [flags]
+
+Flags:
+      --auto-resolve     auto-resolve phantom merge conflicts
+  -b, --beam             beam some commits from this branch to the new branch
+  -c, --commit           commit the stashed changes into the new branch
+  -d, --detached         don't update the perennial root branch
+      --dry-run          print but do not run the Git commands
+  -h, --help             help for append
+  -m, --message string   the commit message
+      --propose          propose the new branch
+  -p, --prototype        create a prototype branch
+      --push             push local branches
+      --stash            stash uncommitted changes when creating branches
+      --sync             sync branches (default true)
+  -v, --verbose          display all Git commands run under the hood
+`)
+    const have = output.flags()
+    const want = [
+      ["--auto-resolve"],
+      ["-b", "--beam"],
+      ["-c", "--commit"],
+      ["-d", "--detached"],
+      ["--dry-run"],
+      ["-h", "--help"],
+      ["-m", "--message string"],
+      ["--propose"],
+      ["-p", "--prototype"],
+      ["--push"],
+      ["--stash"],
+      ["--sync"],
+      ["-v", "--verbose"],
+    ]
+    deepEqual(have, want)
+  })
+
+  test("branch command", () => {
+    const output = new command.HelpOutput(`
+Display the local branch hierarchy and types.
+
+Git Town's equivalent of the "git branch" command.
+
+Usage:
+  git-town branch [flags]
+
+Flags:
+  -d, --display-types string[="all"]   display the branch types
+  -h, --help                           help for branch
+  -o, --order string                   sort order for branch list (asc or desc)
+  -v, --verbose                        display all Git commands run under the hood
+`)
+    const have = output.flags()
+    const want = [
+      ["-d", "--display-types string"],
+      ["-h", "--help"],
+      ["-o", "--order string"],
+      ["-v", "--verbose"],
+    ]
+    deepEqual(have, want)
+  })
+})
+
+suite("removeNegatedFlag", () => {
+  const tests = [
+    {
+      desc: "has negated flag",
+      give: ["-d", "--detached", "--no-detached"],
+      want: ["-d", "--detached"],
+    },
+    {
+      desc: "no negated flag",
+      give: ["-d", "--detached"],
+      want: ["-d", "--detached"],
+    },
+    {
+      desc: "allows a single negated flag",
+      give: ["--no-detached"],
+      want: ["--no-detached"],
+    },
+  ]
+  for (const { desc, give, want } of tests) {
+    test(desc, () => {
+      const have = command.removeNegatedFlag(give)
+      deepEqual(have, want)
+    })
+  }
+})
+
+suite("standardizeArgument", () => {
+  const tests = [
+    {
+      desc: "has argument",
+      give: ["-m <msg>", "--message <msg>"],
+      want: ["-m", "--message string"],
+    },
+    {
+      desc: "no argument",
+      give: ["-p", "--prototype"],
+      want: ["-p", "--prototype"],
+    },
+  ]
+  for (const { desc, give, want } of tests) {
+    test(desc, () => {
+      const have = command.standardizeArgument(give)
+      deepEqual(have, want)
+    })
+  }
+})
diff --git a/tools/format_unittests/go.mod b/tools/format_unittests/go.mod
index f0ccdd542..7752caee8 100644
--- a/tools/format_unittests/go.mod
+++ b/tools/format_unittests/go.mod
@@ -1,3 +1,3 @@
 module github.com/git-town/git-town/tools/format_unittests
 
-go 1.24.6
+go 1.24.9
diff --git a/tools/lint_cached_connectors/main_test.go b/tools/lint_cached_connectors/main_test.go
new file mode 100644
index 000000000..9b070f085
--- /dev/null
+++ b/tools/lint_cached_connectors/main_test.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+	"testing"
+
+	"github.com/shoenig/test/must"
+)
+
+func TestUncachedFilePath(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name       string
+		cachedFile string
+		pkgPath    string
+		want       string
+	}{
+		{
+			name:       "cached_api_connector.go",
+			cachedFile: "cached_api_connector.go",
+			pkgPath:    "internal/forge/gitlab",
+			want:       "internal/forge/gitlab/api_connector.go",
+		},
+		{
+			name:       "cached_connector.go",
+			cachedFile: "cached_connector.go",
+			pkgPath:    "internal/forge/glab",
+			want:       "internal/forge/glab/connector.go",
+		},
+		{
+			name:       "full path with cached_api_connector.go",
+			cachedFile: "/home/user/project/internal/forge/gitlab/cached_api_connector.go",
+			pkgPath:    "internal/forge/gitlab",
+			want:       "internal/forge/gitlab/api_connector.go",
+		},
+		{
+			name:       "file without cached_ prefix",
+			cachedFile: "api_connector.go",
+			pkgPath:    "internal/forge/gitlab",
+			want:       "internal/forge/gitlab/api_connector.go",
+		},
+		{
+			name:       "empty package path",
+			cachedFile: "cached_connector.go",
+			pkgPath:    "",
+			want:       "connector.go",
+		},
+		{
+			name:       "multiple cached_ prefixes",
+			cachedFile: "cached_cached_connector.go",
+			pkgPath:    "internal/forge/gitlab",
+			want:       "internal/forge/gitlab/cached_connector.go",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			have := uncachedFilePath(tt.cachedFile, tt.pkgPath)
+			must.EqOp(t, tt.want, have)
+		})
+	}
+}
diff --git a/tools/lint_steps/stepdef_regex_test.go b/tools/lint_steps/stepdef_regex_test.go
new file mode 100644
index 000000000..5f049f688
--- /dev/null
+++ b/tools/lint_steps/stepdef_regex_test.go
@@ -0,0 +1,79 @@
+package main_test
+
+import (
+	"testing"
+
+	lintSteps "github.com/git-town/git-town/tools/lint_steps"
+	"github.com/shoenig/test/must"
+)
+
+func TestCheckStepRegexAnchors(t *testing.T) {
+	t.Parallel()
+
+	t.Run("all steps properly anchored", func(t *testing.T) {
+		t.Parallel()
+		give := []lintSteps.StepDefinition{
+			{Line: 1, Text: "^the first step$"},
+			{Line: 2, Text: "^the second step$"},
+			{Line: 3, Text: "^a step with (.*) capture$"},
+		}
+		have := lintSteps.CheckStepRegexAnchors(give)
+		must.SliceEmpty(t, have)
+	})
+
+	t.Run("multiple unanchored steps", func(t *testing.T) {
+		t.Parallel()
+		give := []lintSteps.StepDefinition{
+			{Line: 1, Text: "^properly anchored step$"},
+			{Line: 2, Text: "missing start$"},
+			{Line: 3, Text: "^missing end"},
+			{Line: 4, Text: "missing both"},
+		}
+		have := lintSteps.CheckStepRegexAnchors(give)
+		want := []lintSteps.StepDefinition{
+			{Line: 2, Text: "missing start$"},
+			{Line: 3, Text: "^missing end"},
+			{Line: 4, Text: "missing both"},
+		}
+		must.Eq(t, want, have)
+	})
+
+	t.Run("step missing both anchors", func(t *testing.T) {
+		t.Parallel()
+		give := []lintSteps.StepDefinition{
+			{Line: 1, Text: "^properly anchored step$"},
+			{Line: 2, Text: "missing both anchors"},
+		}
+		have := lintSteps.CheckStepRegexAnchors(give)
+		want := []lintSteps.StepDefinition{
+			{Line: 2, Text: "missing both anchors"},
+		}
+		must.Eq(t, want, have)
+	})
+
+	t.Run("step missing end anchor", func(t *testing.T) {
+		t.Parallel()
+		give := []lintSteps.StepDefinition{
+			{Line: 1, Text: "^properly anchored step$"},
+			{Line: 2, Text: "^missing end anchor"},
+		}
+		have := lintSteps.CheckStepRegexAnchors(give)
+		want := []lintSteps.StepDefinition{
+			{Line: 2, Text: "^missing end anchor"},
+		}
+		must.Eq(t, want, have)
+	})
+
+	t.Run("step missing start anchor", func(t *testing.T) {
+		t.Parallel()
+		give := []lintSteps.StepDefinition{
+			{Line: 1, Text: "^properly anchored step$"},
+			{Line: 2, Text: "missing start anchor$"},
+		}
+		have := lintSteps.CheckStepRegexAnchors(give)
+		want := []lintSteps.StepDefinition{
+			{Line: 2, Text: "missing start anchor$"},
+		}
+		must.Eq(t, want, have)
+	})
+}
diff --git a/tools/optioncompare_in_tests/go.mod b/tools/optioncompare_in_tests/go.mod
index 1e98facc7..981ba5c80 100644
--- a/tools/optioncompare_in_tests/go.mod
+++ b/tools/optioncompare_in_tests/go.mod
@@ -1,3 +1,3 @@
 module optioncompare_in_tests
 
-go 1.24.6
+go 1.24.9
diff --git a/vendor/github.com/cucumber/godog/test_context.go b/vendor/github.com/cucumber/godog/test_context.go
index 8156c6d70..add9f47b0 100644
--- a/vendor/github.com/cucumber/godog/test_context.go
+++ b/vendor/github.com/cucumber/godog/test_context.go
@@ -5,12 +5,14 @@ import (
 	"fmt"
 	"reflect"
 	"regexp"
+	"runtime"
+
+	messages "github.com/cucumber/messages/go/v21"
 
 	"github.com/cucumber/godog/formatters"
 	"github.com/cucumber/godog/internal/builder"
 	"github.com/cucumber/godog/internal/flags"
 	"github.com/cucumber/godog/internal/models"
-	messages "github.com/cucumber/messages/go/v21"
 )
 
 // GherkinDocument represents gherkin document.
@@ -342,6 +344,10 @@ func (ctx ScenarioContext) stepWithKeyword(expr interface{}, stepFunc interface{
 		Nested:       isNested,
 	}
 
+	// Get the file and line number of the call that created this step with a
+	// call to one of the Step, Given, When, or Then wrappers.
+	_, def.File, def.Line, _ = runtime.Caller(2)
+
 	// stash the step
 	ctx.suite.steps = append(ctx.suite.steps, def)
 }
diff --git a/vendor/github.com/shoenig/test/internal/assertions/assertions.go b/vendor/github.com/shoenig/test/internal/assertions/assertions.go
index d6254e1e9..f1b5bd523 100644
--- a/vendor/github.com/shoenig/test/internal/assertions/assertions.go
+++ b/vendor/github.com/shoenig/test/internal/assertions/assertions.go
@@ -142,6 +142,26 @@ func Unreachable() (s string) {
 	return
 }
 
+func Panic(f func()) (s string) {
+	defer func() {
+		if r := recover(); r == nil {
+			s = "expected panic; did not panic"
+		}
+	}()
+	f()
+	return
+}
+
+func NotPanic(f func()) (s string) {
+	defer func() {
+		if r := recover(); r != nil {
+			s = fmt.Sprintf("expected not to panic; panicked: %v", r)
+		}
+	}()
+	f()
+	return
+}
+
 func Error(err error) (s string) {
 	if err == nil {
 		s = "expected non-nil error; got nil\n"
diff --git a/vendor/github.com/shoenig/test/must/must.go b/vendor/github.com/shoenig/test/must/must.go
index 491977d49..11632466a 100644
--- a/vendor/github.com/shoenig/test/must/must.go
+++ b/vendor/github.com/shoenig/test/must/must.go
@@ -51,6 +51,18 @@ func Unreachable(t T, settings ...Setting) {
 	invoke(t, assertions.Unreachable(), settings...)
 }
 
+// Panic asserts func f panics.
+func Panic(t T, f func(), settings ...Setting) {
+	t.Helper()
+	invoke(t, assertions.Panic(f), settings...)
+}
+
+// NotPanic asserts func f does not panic.
+func NotPanic(t T, f func(), settings ...Setting) {
+	t.Helper()
+	invoke(t, assertions.NotPanic(f), settings...)
+}
+
 // Error asserts err is a non-nil error.
 func Error(t T, err error, settings ...Setting) {
 	t.Helper()