The steps max-cli and tg-cli release by, written once. Each CLI's release and test-live skills
(.claude/skills/ in that repository) link here and add what is theirs: their changelog headings,
their pages, their live accounts, and whether the owner signs a report.
A release is reliable when two halves agree: pnpm release:check, everything a program can
decide, and the judgement half, the steps below. The automatic half is
@wirecat/cli-base/release. Both CLIs run the same
checks from it and add their own.
prev=$(git describe --tags --abbrev=0) # the last released tag
git log --oneline "$prev"..origin/main
gh pr list --state merged --base main --search "merged:>=$(git log -1 --format=%cs "$prev")" --json number,title,body
git diff "$prev"..origin/main -- docs/commands.md # generated: the exact commands and options that changedThe last diff drives everything below. A dependency release (cli-core, cli-base) can change
commands without touching this repository's code. The docs/commands.md diff shows it, and
release:check fails "tree unchanged" until the page is regenerated.
pnpm release:checkOne ok / FAIL line per check. On a branch before the changelog is dated, "version not on npm"
and "changelog" are expected to fail; everything else must pass. Fix nothing on your own judgement
here. Report each failure and what it means, then fix it in the release pull request.
Draft the top section from the merged pull requests, by the CLI's changelog rules (its
docs/dev/CONVENTIONS.md). Each entry says what changed as the user sees it, why (unless
obvious), and what to watch for: who is affected, what can break, what to do. Read each pull
request's body and diff. The title is not enough, and a fix can hide in a pull request titled as
something else. Anything that changes a command's output, an exit code, an option or a config key
goes under the "Changed — may break callers" heading.
For every command and option in the docs/commands.md diff, the pages a person reads must describe
it as it now is: the README, docs/*.md, and the agent skill in skills/<cli>/SKILL.md, which
agents read through <cli> skill show.
- Split the pages into groups and give each group to a read-only subagent. Give it the changed
commands, the pages, and the rule "report doc
file:line, what it says, what is true, the sourcepath:line; mark CERTAIN or LIKELY". - Confirm every finding with a grep or a read of the source before editing. Drop a LIKELY finding you cannot confirm.
- Every page is rewritten in place to the current fact: no correction marks, no struck-out text, no dated trail (WC-DOC-3). User pages carry no internal ids either.
docs/commands.mdis generated: never edit it;pnpm generate.
The suites never contact a messenger. Live checks are the other half: by hand, on the owner's real accounts, before a release. A mistake sends a message to a person.
-
Only with the owner's yes, in this session, for the list of checks shown to them. A check not on the approved list is not run.
-
Only the checkout's own wrapper:
bin/tgorbin/max, nevernode dist/bin/<cli>.js. The wrapper keeps config, state, cache and the message store inside the checkout. The bare build opens the owner's real store, and a branch's schema can migrate it. The wrapper reuses a copied login;session startwould be a new device on the real account. -
Only the test chats in the CLI's private cast. Never a real person's chat, not even to read it for a check.
-
The shape, never the content. Record the exit code, whether stdout is exactly one JSON value, whether stderr is empty or one diagnostic, and the keys and item counts. Never record message text, names or phone numbers:
live() { out=$(mktemp) err=$(mktemp) timeout 90 bin/<cli> "$@" --json >"$out" 2>"$err" echo "exit=$? stdout_lines=$(wc -l <"$out") stderr_lines=$(wc -l <"$err")" jq -c 'if type == "object" then {keys: keys, items: (.items | length?)} else {type: type} end' "$out" 2>/dev/null jq -c '.error.code?' "$err" 2>/dev/null rm -f "$out" "$err" }
-
A write is proven on the other side. A message sent from one account is read back, by id, from the other. Pacing matters: a burst of writes gets the account rate-limited.
-
A command that does not exit is a failure. Start a long-running one (
watch,serve,mcp) in the background, take its PID at start ($!), and stop that PID, never by name. -
Snapshot before a change, restore the exact value after, and read it back. Keep the value in a shell variable, not a file. What cannot be put back exactly is not run.
-
Messages a check sent are deleted after.
-
pnpm smoke:liveis one write of each kind in Saved Messages, cleaned up after. It needs the same yes. AnyFAILstops the release until the owner rules on it, even one caused by the account's state rather than the build. To show the code did not change, rungit diff --stat v<prev>..HEAD -- <the paths that step exercises>.
Read the changed code against the CLI's own constraints in its CLAUDE.md. Constraints with a
mechanical guard (stdout carries data only, no library type above the adapter, the test sandbox)
passed in release:check. Review the rest by reading: nothing sends unless the typed command asked
for it, every exit path closes what it opened, no message, token or phone number reaches a log, a
fixture or a document. Mark each one holds or broken, with path:line. A broken one stops the
release.