Skip to content

feat(list): publish a JSON Schema for the schema-2 envelope - #3747

Merged
max-sixty merged 4 commits into
mainfrom
machine-readable-output
Aug 6, 2026
Merged

max-sixty merged 4 commits into
mainfrom
machine-readable-output

Conversation

@max-sixty

@max-sixty max-sixty commented Aug 6, 2026 •

Copy link
Copy Markdown
Owner

wt list --format=json schema 2 now has a published, machine-readable contract at worktrunk.dev/schema/list-v2.json.

The schema was already derived — test_schema_generates built one, asserted it compiled, and threw it away, with a comment saying "until the schema export ships." This ships it: wt list --print-schema prints the document (a developer entry point alongside --help-page, intercepted before clap), and a new step in test_docs_are_in_sync commits it to docs/static/schema/list-v2.json, the same generate-and-commit pattern as llms.txt. It shells out rather than calling schema_for! because JsonEnvelope lives in the bin-only crate::commands tree.

Two things had to be fixed for the document to be usable.

The contract. schema_for! generates under schemars' deserialize contract, which marks a skip_serializing_if field required — nothing supplies it on the way in. The first document I generated therefore required default_branch, upstream, pr, checks, summary and vars on every item, all of which the absence rule routinely omits, so it rejected the output it documents. Generating under for_serialize() fixes it.

The vocabularies. Four fields — checks.status, display.state, default_branch.integration.reason and worktree.operation — were &'static str, so the schema described them as bare strings. They are now JsonCheckStatus, JsonMainState, JsonIntegrationReason and JsonOperation, each converted from its domain enum by an exhaustive match, so a new CiStatus, MainState, IntegrationReason or InProgressOperation variant is a compile error rather than a value silently missing from the published vocabulary. The emitted JSON is unchanged; the existing envelope snapshot passes untouched.

Before and after, for one item
// before — rejects its own output, and loses the vocabulary
"required": ["default_branch", "upstream", "pr", "checks", "summary", "vars", "display"],
"status": { "type": "string" }

// after
"required": ["branch", "head", "display"],
"status": { "enum": ["passed", "running", "failed"] }

Testing

test_schema_accepts_envelopes validates a battery — every CiStatus over both sources, every MainState, a populated worktree row, an integrated row with an upstream and a dev server, plus the absent and null arms of the absence rule — against the same document --print-schema emits.

Validating proves nothing about a type the battery never instantiates, so the test also pins every non-Nullable_ type in the document to a path that must carry a non-null value. A new Json* type fails until the battery reaches it, and a row that stops populating one fails too — the check reports the type names rather than leaving the gap to a reader. This needed a jsonschema dev-dependency: schemars only generates, and derives the document from the types without ever seeing an envelope, so nothing otherwise tied the two together.

The test was confirmed to fail on the bug it exists for. Reverting to for_deserialize() makes it report pr, checks, summary, vars and display.columns as wrongly required.

The dependency is dev-only: reqwest, rustls and async-trait stay unselected so no HTTP stack comes along, and cargo tree --package worktrunk --edges normal -i jsonschema finds no path to it.

One direction it deliberately does not cover: a loosening. If a field reverted to &'static str the schema would say type: string and anything would validate. That direction is held by the compiler instead, via the exhaustive matches.

Notes for review

  • schema_document() lives in json_v2.rs beside the types, not in help.rs, so --print-schema and the test compile the same document rather than two constructions that could drift on the contract setting.
  • The lychee exclusion for worktrunk.dev/schema/ follows the entry directly above it: a generated link that 404s until the site deploys.

This was written by Claude Code on behalf of max-sixty

`wt list --format=json` schema 2 had a derived schema that was never
emitted — `test_schema_generates` checked it compiled and threw it away.
Generate it from `wt list --print-schema` and commit the result to
docs/static/schema/list-v2.json, served at worktrunk.dev/schema/list-v2.json.

Two things had to be right for the document to be usable:

Generate under schemars' serialize contract, not its default deserialize
one. They disagree on `required`: under the deserialize contract a
`skip_serializing_if` field is required, because nothing supplies it on the
way in. The schema would have rejected the output it documents — every item
carrying `default_branch`, `upstream`, `pr`, `checks`, `summary` and `vars`
whether or not the absence rule omitted them.

Give the three stringly-typed wire fields real enums. `checks.status`,
`display.state` and `default_branch.integration.reason` were
`&'static str`, so the schema described them as bare strings and dropped
their vocabularies. They are now `JsonCheckStatus`, `JsonMainState` and
`JsonIntegrationReason`, each converted from its domain enum by an
exhaustive match, so a new upstream variant is a compile error rather than a
silently unlisted value. Emitted JSON is unchanged.

Verified by validating `wt list --format=json`, `--full`, and
`list statusline --format=json` against the published document.
Nothing tied the generated schema to the output it describes: schemars
derives it from the types without ever seeing an envelope, so the two could
disagree and only a reader would notice. Add a `jsonschema` dev-dependency
and check the property directly.

`schema_document()` moves from `help.rs` to `json_v2.rs`, beside the types
it describes, so `--print-schema` and the test compile the same document
rather than two constructions that could drift on the contract setting.

The battery walks the axes most likely to be got wrong — every `CiStatus`
over both sources, every `MainState`, plus the absent and null arms of the
absence rule. Confirmed it fails on the bug it exists for: generating under
the deserialize contract instead makes it report `pr`, `checks`, `summary`,
`vars` and `display.columns` as wrongly required.

The dependency is dev-only and pulls no HTTP stack — `reqwest` and `tokio`
stay unselected, and `cargo tree --edges normal` finds no path to it.

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worktree.operation is a fourth field of the shape this PR converts, and it stays &'static str. JsonWorktreeV2's pub operation: Tri<&'static str> — doc comment In-progress operation: "rebase", "merge", "cherry_pick", "revert", or "bisect" — lands in the published document as {"$ref": "#/$defs/Nullable_string"}, i.e. {"type": ["string", "null"]}, so the schema drops that vocabulary exactly the way checks.status did before this change. The value comes from operation.into() on InProgressOperation's strum::IntoStaticStr, so a sixth variant would appear in published output with nothing failing to compile — the drift the new exhaustive From impls exist to prevent. A JsonOperation mirroring JsonIntegrationReason would close it, and test_operation_arms already pins the five wire names to assert against.

Two smaller ones inline.

Comment thread src/commands/list/json_v2.rs Outdated
Comment thread src/commands/list/json_v2.rs
Review catch: `worktree.operation` is a fourth field of the shape this
branch converts and it stayed `&'static str`, publishing as
`Nullable_string` and dropping the `merge`/`rebase`/`cherry_pick`/`revert`/
`bisect` vocabulary the way `checks.status` did. It now goes through
`JsonOperation` by exhaustive match, so a sixth `InProgressOperation`
variant is a compile error rather than an unlisted published value.

The validation battery built every item from `item_with`, which makes a
branch row, so `worktree` was absent throughout and the whole subtree —
`operation`, both `JsonReason`s, `changes` and its `diff` — never met the
schema. Add a worktree row, and assert the battery still reaches it so the
gap can't reopen silently.

Also fixes a docstring this branch stranded: deleting `test_schema_generates`
left its comment on the test below, describing the wrong thing and promising
an export this branch ships. Renamed for the helper it actually covers.

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added worktree row only sets git_operation, so the rest of the subtree the fix is named for stays absent-or-null: locked/prunable are None and carry skip_serializing_if, so both fields are omitted, and working_tree_status is None so changes serializes as null. JsonReason, JsonChanges and JsonDiff ship in the published document and still never meet a real object, which makes the guard's message ("battery no longer covers the worktree subtree") claim more than the assertion checks — it pins operation alone. Suggestion inline.

Same class outside the worktree subtree, if it's worth closing in one pass: no battery item sets an integration signal, so check_integration returns None for every row and default_branch.integration lands as Tri::Unknown throughout (the same null test_absence_rule_unresolved_vs_determined asserts for a fresh item). JsonIntegration and JsonIntegrationReason are therefore never validated against output — and JsonIntegrationReason is one of the three enums this PR adds precisely to stop that vocabulary drifting. upstream (item.upstream unset → null) and dev_server (item.url unset → absent) leave JsonUpstream and JsonDevServer in the same position.

Comment thread src/commands/list/json_v2.rs
Validating an envelope proves nothing about a type the battery never
instantiates, and most of the document's types were in that position: the
worktree row set only `git_operation`, so `JsonReason`, `JsonChanges` and
`JsonDiff` never met a real object, and no row carried integration signals,
an upstream, a dev server or a forge — leaving `JsonIntegration`,
`JsonIntegrationReason`, `JsonUpstream`, `JsonDevServer`, `GitRepoInfo` and
`GitRepoProvider` unexercised too. `JsonIntegrationReason` is one of the
enums this branch adds to stop exactly that vocabulary drifting.

The previous guard checked `operation` alone while its message claimed the
whole subtree, so it would not have caught any of them.

Populate the rows, then close the loop rather than the instance: every
non-`Nullable_` type in the document is pinned to a path that must carry a
non-null value. A new `Json*` type fails until the battery reaches it, and a
row that stops populating one fails too. Both directions confirmed by
mutation — dropping the forge reports `["GitRepoInfo", "GitRepoProvider"]`,
and removing a row from the table reports `["JsonOperation"]`.
@max-sixty
max-sixty merged commit 7f8ac8e into main Aug 6, 2026
44 checks passed
@max-sixty
max-sixty deleted the machine-readable-output branch August 6, 2026 05:33
max-sixty added a commit that referenced this pull request Aug 7, 2026
…an (#3762)

Three developer entry points — `--help-page`, `--help-description`, and
`--print-schema` — each carried its own copy of a scan that picked the
subcommand out of argv by rejecting entries that looked like the binary:

```rust
.filter(|a| *a != "--help-page" && !a.starts_with('-') && !a.ends_with("/wt"))
.find(|a| !a.contains("target/") && *a != "wt")
```

Neither `ends_with("/wt")` nor `contains("target/")` matches `wt.exe`
under a backslash path, so on every Windows install all three read the
binary's own path as the command. `--print-schema` exited 2; the other
two exited 0 with empty stdout and `Unknown command: C:\...\wt.exe` on
stderr. The scan also took the value of a value-carrying global as the
command, so `wt -C <path> list --print-schema` answered `No JSON schema
for '<path>'`.

clap had already parsed the same argv one call earlier.
`parse_early_globals` runs the real `Cli` definition with
`ignore_errors(true)` and reads `matches.subcommand()` to pick the
alias-splice context, and the comment above it already said that exists
"so the splice path in `augment_help` has no separate arg scanner". The
doc entry points just never asked it. It now carries the name in an
`EarlyGlobals` struct, and the three handlers take `Option<&str>`
instead of `&[String]`.

Error messages are unchanged: `src/cli/mod.rs` has
`#[command(external_subcommand)]` for user aliases, so a name the `Cli`
definition doesn't declare still arrives intact and can be named back in
`Unknown command: X`.

`--print-schema` now prints through `crate::output::print_json` rather
than `serde_json::to_string_pretty` plus std's `println!`. std's panics
with exit 101 when a consumer closes the pipe; anstream's drops the
`BrokenPipe`. This was the last JSON-to-stdout surface still panicking
after #3746 — the schema landed via #3747 while that PR was in flight,
and it lives outside `src/commands/`, where #3746's stdout guard doesn't
scan.

<details>
<summary>Why CI never caught this</summary>

`tests/integration_tests/help.rs` and
`tests/integration_tests/readme_sync.rs` are both
`#![cfg(not(windows))]`, and on Unix the test binary is always named
`wt` under `target/`, which is exactly the shape the scan was written
against. The new test drives all four cases through a symlink named
`wt.exe` — a symlink rather than a copy because the debug binary is
67MB. Verified to fail against the pre-change code.

</details>

`wt merge --help-page` and `wt merge --help-description` still panic on
a closed pipe. Those use std's `println!`/`print!` deliberately, to
preserve the ANSI codes anstream's would strip, so they need a different
mechanism than this one. Left for a follow-up.

> _This was written by Claude Code on behalf of max-sixty_
max-sixty added a commit that referenced this pull request Aug 7, 2026
Release v0.72.0 — 55 commits since v0.71.0.

Minor bump: `cargo semver-checks` reports 5 breaking library changes, so
patch is disallowed pre-1.0.

## Headline changes

- **`wt merge` / `wt step push` no longer autostash the target
worktree** (#3703). Both strategies now advance the target through one
`advance_target` — a compare-and-swap `update-ref`, then `read-tree -m
-u` in the target worktree — so `refs/stash` is never entered and staged
changes stay staged.
- **Forge classification returns to brand-in-hostname** (#3673),
reverting the exact-DNS-label rule 0.71.0 shipped.
`github-enterprise.acme.com` and friends resolve again with no config.
- **`[projects."…"]` keys match by `*` pattern and carry forge
settings** (#3701), so one user-config entry covers every repository on
a self-hosted host.
- **A published JSON Schema for `wt list --format=json` schema 2**
(#3747), plus machine-readable approval state and `branch_outcome`
(#3710).

Full detail in `CHANGELOG.md`.

## One fix made during the release cut

The release audit surfaced a gap this release's own `advance_target`
rewrite introduced, fixed here rather than deferred:

**`wt merge` / `wt step push` now refuse a target worktree parked
mid-operation.** The target sync is a two-tree merge, which refuses an
unmerged index but *not* a stopped cherry-pick or rebase whose conflict
has already been staged. A target paused between steps could therefore
have the push range written into it, and the user's `--continue` would
commit the synced tree as the step's result. The old fast-forward path
got this check for free from `receive.denyCurrentBranch=updateInstead`,
which refused any unclean target outright; both strategies now ask
directly, and the refusal names the worktree holding the operation.

`test_push_refuses_target_mid_operation` covers it in both shapes a
stopped operation can take, and both are mutation-verified. With the
gate disabled, the push succeeds and writes `feature.txt` into the
mid-cherry-pick worktree.

The rebase case was added in response to review feedback on this PR, and
pins a second dependency. A rebase detaches HEAD, so `git worktree list
--porcelain` reports the target with no branch and `worktree_for_branch`
finds it only because `finalize_worktree` backfills from
`rebase-merge/head-name`. That makes the rebase arm the one place this
guarantee rests on a helper of ours rather than on git — the
fast-forward path it replaced got the refusal from `find_shared_symref`.
With the backfill disabled, `wt step push` succeeds against a worktree
parked mid-rebase while the cherry-pick case still passes, so the gap
was real.

## Validation

- Local gate green: `cargo run -- hook pre-merge --yes` — 4570 tests,
clippy, fmt, doc sync.
- Cross-platform nightly green on the cut-from tip `3817df079` (run
31133551751): full nextest matrix on linux/macOS/Windows,
feature-powerset, all three release triples, nix-flake,
minimal-versions, unused-deps, crate-build, link-check.
- Changelog verified entry-by-entry against the diffs by an independent
pass; every one of the 55 commits either maps to an entry or is a
documented skip.
- `main` advanced during the CI wait. #3762 ships in this release and
now has a changelog entry; the other two commits that landed (#3758,
#3749) touch only `.github/`.
- Data-loss surface reviewed by four independent finders over the
cumulative diff. One further finding — a pre-0.72 `approvals.toml` key
containing `*` being reinterpreted as a wildcard on upgrade — was
reviewed and accepted as out of scope for this release.

> _This was written by Claude Code on behalf of max-sixty_

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
worktrunk 0.72.0

Created-by: HarmonybrewBot
Commit-by: HarmonybrewBot
Merged-by: HarmonybrewBot
Description: Created by `brew bump`

---

Created with `brew bump-formula-pr`.<details>
  <summary>release notes</summary>
  <pre>## Release Notes

### Improved

- **A host carrying a forge's name anywhere resolves to that forge again**: 0.71.0 required `github`, `gitlab`, or `gitea` as a whole dot-separated label, which read as an ownership check but wasn't one — an attacker controls their own DNS — while shutting out self-hosters with hyphenated names. `github-enterprise.acme.com`, `mygithub.com`, and the `github-personal` SSH alias classify again, so CI status, `wt switch --prs`, and `repo.provider` work with no config. ([#3673](max-sixty/worktrunk#3673))

- **One `[projects."…"]` entry can cover every repository on a host, and can set the forge**: A key containing `*` matches any run of characters, `/` included, so `[projects."git.company.example/*"]` covers a whole host; every matching entry applies, least- to most-specific. The table also gained `forge.platform` and `forge.hostname`, so a self-hosted host needs one entry here rather than a block in every repo. [Docs](https://worktrunk.dev/config/#user-project-specific-settings) ([#3701](max-sixty/worktrunk#3701), thanks @chrishas35 for the request and @witt-bit for the workspace-scoped case it partly serves)

- **`wt merge` and `wt step push` leave the target worktree's uncommitted changes in place**: The autostash that held a dirty target's changes restored them as unstaged; both now advance the target with a compare-and-swap `update-ref` and `read-tree -m -u`, which leaves uncommitted work untouched. (Breaking: the fast-forward path no longer runs `git push`, so `pre-push` and the receive-side hooks no longer fire, and a failed sync errors with the ref rolled back.) ([#3703](max-sixty/worktrunk#3703), [#3684](max-sixty/worktrunk#3684), [#3693](max-sixty/worktrunk#3693), thanks @gubasso for reporting)

- **Approval state and branch-removal outcomes are machine-readable**: `wt config approvals list --format=json` reports whether a non-interactive run would stop for approval. `wt remove` and `wt step prune` replace `branch_deleted` with `branch_outcome`: `deleted`, `deferred`, `not_attempted`, `retained_unmerged`, `retained_checked_out`, `retained_raced`, `retained_failed`. (Breaking.) ([#3710](max-sixty/worktrunk#3710), thanks @NathanaelRea for the requests)

- **Every commit hash worktrunk prints follows `core.abbrev`**: The `wt list` table, `wt switch --prs`'s `log` tab, and `wt config state`'s CI cache table sliced to 8 characters while `--format=json` carried git's `%h`. All now ask git how wide it abbreviates in this repo. ([#3676](max-sixty/worktrunk#3676), [#3677](max-sixty/worktrunk#3677))

- **`wt list --format=json` schema 2 has a published JSON Schema**: [worktrunk.dev/schema/list-v2.json](https://worktrunk.dev/schema/list-v2.json) holds the contract, and `wt list --print-schema` prints the same document. Four fields that were bare strings are now enumerated vocabularies; the emitted JSON is unchanged. ([#3747](max-sixty/worktrunk#3747))

- **A detached worktree is named by its commit, not `-`**: The Branch cell hardcoded `-`, which reads as missing data rather than a state; it now carries the row's abbreviated HEAD, and the picker and statusline name the worktree the same way. ([#3675](max-sixty/worktrunk#3675))

### Fixed

- **Piped output is plain and no longer panics**: `wt list | head -3` exited 101 with `failed printing to stdout: Broken pipe`, and `wt list` wrote ANSI to a pipe unconditionally. Every stdout surface now exits cleanly, and the human-read ones are plain when piped unless `CLICOLOR_FORCE=1`. ([#3746](max-sixty/worktrunk#3746), [#3766](max-sixty/worktrunk#3766))

- **A CI check that hasn't finished no longer reads as passed**: Each forge's status parser missed documented values, so a GitHub PR parked on an approval gate showed green, and GitLab's `canceling` and Azure DevOps's `postponed` read as no CI at all. ([#3741](max-sixty/worktrunk#3741), [#3740](max-sixty/worktrunk#3740))

- **The shell wrappers survive an `rm` alias and a failing `--execute`**: Aliases bake into the wrapper at parse time, so `alias rm='rm -v'` reached its cleanup — noise on zsh and bash, and on nushell an abort that leaked three temp files, as a failing `--execute` body also did. ([#3714](max-sixty/worktrunk#3714), thanks @Ar4l), ([#3732](max-sixty/worktrunk#3732), [#3734](max-sixty/worktrunk#3734))

- **An alias or hook wrapping `wt switch` or `wt remove` keeps your subdirectory**: The user's position came from the `wt` process's cwd, which inside an alias body is the worktree root, so an aliased `wt remove` from `feature/apps/gateway` landed at the primary worktree's root. Fixes [#3723](max-sixty/worktrunk#3723). ([#3724](max-sixty/worktrunk#3724), thanks @vivienm for reporting)

- **`wt merge` and `wt step push` refuse a target worktree parked mid-operation**: The two-tree sync refuses an unmerged index but not a stopped cherry-pick or rebase whose conflict was already staged, so the push range could land in a paused target and be committed by `--continue`. ([#3759](max-sixty/worktrunk#3759))

- **The Claude plugin's worktree-remove hook resolves against the worktree path**: The hook anchored at `CLAUDE_PROJECT_DIR`, which the `claude agents` view routinely leaves outside any repository, so `wt remove` died with `not a git repository` and the session became undeletable. Its guard now also requires a `.git` entry. ([#3754](max-sixty/worktrunk#3754), [#3767](max-sixty/worktrunk#3767), thanks @judewang for the fix and the report)

- **A Gitea API error is reported as one, not as a parse failure**: `tea api` exits 0 whatever the HTTP status, so both call sites guessed from the body's shape and blamed an API change for an API error. `--include` surfaces the status instead. ([#3713](max-sixty/worktrunk#3713), [#3600](max-sixty/worktrunk#3600))

- **`--print-schema` and the doc-generation help flags name the right command**: All three found the subcommand by scanning `argv` for a `/wt` suffix, which never matches `wt.exe` under a backslash path, so on Windows they read the binary's own path as the command. ([#3762](max-sixty/worktrunk#3762))

- **A multibyte shell name no longer panics**: `extract_filename_from_path` sliced at `len() - 4` to test for `.exe` with no char-boundary check: `SHELL=/bin/日本語 wt config show` panicked, and on macOS every process name goes through it during shell detection. ([#3727](max-sixty/worktrunk#3727))

- **`wt` installed under a dotted name generates shell integration for that name**: `binary_name` used `file_stem`, which cuts at the last dot, so `wt config shell init bash` under `wt.old` emitted a wrapper for `wt`. It now strips only the executable suffix. ([#3719](max-sixty/worktrunk#3719))

- **Concurrent `wt step prune` removals no longer race the worktree registry**: `git worktree remove` reads every sibling under `.git/worktrees/`, so two overlapping removals could have one read a sibling mid-teardown. Registry-mutating removals now serialize behind a second lock. ([#3692](max-sixty/worktrunk#3692))

- **The `wt switch` first-run offer previews the legacy files it removes**: Accepting "Install shell integration?" could delete a deprecated worktrunk-managed wrapper the prompt never named. What gets removed is unchanged. ([#3656](max-sixty/worktrunk#3656))

- **`wt config create --project` writes a resolvable link**: The comment it writes into `.config/wt.toml` carried a raw Zola target, because the link-conversion regex stopped at the first `]` — here the one closing a nested code span. ([#3731](max-sixty/worktrunk#3731))

- **`wt list --branches` counts a local branch containing `/` as local**: The summary tally classified branch-only rows by `branch.contains('/')`, so a local `feature/login` counted under "N remote branches". ([#3687](max-sixty/worktrunk#3687))

- **`wt step relocate`'s human summary counts template-error branches as skipped**: `--format=json` already folded them into `skipped`; the human tally undercounted by the number of branches whose `worktree-path` template failed to expand. ([#3688](max-sixty/worktrunk#3688))

### Documentation

- **SignPath attribution appears with the artifacts it describes**: The code-signing notice and a route to the policy now sit in the install section's Windows block on the README and the docs landing page, as SignPath Foundation's OSS program requires. ([#3709](max-sixty/worktrunk#3709))

- **`wt step copy-ignored`'s `--require-include` example renders as a terminal block**: It was the only `console` block in the command's long help missing the `$ ` prefix. ([#3706](max-sixty/worktrunk#3706))

### Internal

- **Library API rework** (Breaking library API): `cargo-semver-checks` fails five lints — `LegacyForgeAlias` and `Repository::legacy_forge_alias` removed with the forge-classification revert, `Repository::forge_platform_override` removed for one shared resolver, `stage_worktree_removal` gained two parameters, `UserProjectOverrides` gained a `forge` field, and the `real-repo-benches` feature was removed. ([#3673](max-sixty/worktrunk#3673), [#3694](htt

See merge request: Harmonybrew/homebrew-core!16275

This branch was previously deployed

1 inactive deployment
tend — 847a3ee9 Deployed Aug 6, 2026 by max-sixty via review #3723
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants