[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6H5ZRW9CY3F0B95F3AD4A78":3,"skill-same:01M6H5ZRW9CY3F0B95F3AD4A78":66},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":19,"commitSha":44,"skillDirectory":45,"forksCount":46,"repositoryHtmlUrl":47,"repositoryHomepageUrl":6,"ownerHtmlUrl":48,"ownerBlogUrl":49,"ownerDescription":50,"updatedAt":51,"install":52,"resources":65},"01M6H5ZRW9CY3F0B95F3AD4A78","merge-to-main-and-release",null,"Refresh linux-next, open\u002Fupdate the PR to main, merge once green, bump VERSION, tag, and trigger the workflow_dispatch release. Runs on a daily cadence so the latest linux-next work reaches a downloadable Linux release operators can smoke-test on a Fedora Silverblue host.","8007342\u002Ftillandsias","8007342","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F162944123?v=4","01M6H5SFAKAV1RH8MY0C9X2SZZ","Tlatoāni","01M6H5SFF2BBW9HGM3DXAZ84TG",6,"GPL-3.0","https:\u002F\u002Fgithub.com\u002F8007342\u002Ftillandsias\u002Fblob\u002Fmain\u002Fskills\u002Fmerge-to-main-and-release\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"main","merge to main and release","merge to main","release","merge","update","pr","open","work","smoke","bump","workflow","next","green","bump version","operators","host","trigger","cadence","fedora","2026-11-10 10:12:30",false,"---\nname: merge-to-main-and-release\ndescription: Refresh linux-next, open\u002Fupdate the PR to main, merge once green, bump VERSION, tag, and trigger the workflow_dispatch release. Runs on a daily cadence so the latest linux-next work reaches a downloadable Linux release operators can smoke-test on a Fedora Silverblue host.\n---\n\n# Merge to Main and Release\n\nThis skill closes the loop between the work-loop (`\u002Fadvance-work-from-plan`) and downloadable releases. It promotes the current `linux-next` HEAD to `main` via PR, bumps the version, tags, and triggers the `workflow_dispatch`-only release workflow that publishes the Linux musl binaries (and the macOS \u002F Windows artifacts) on a GitHub Release.\n\nThe user's stated goal: keep a fresh Linux release available so they can install and smoke-test on a Fedora Silverblue host without needing to build locally. Daily cadence is the right balance — local CI catches regressions on every commit; this skill catches the \"did all of today's work cohere into a shippable artifact\" gate.\n\n---\n\n## 0 — Pre-flight\n\nEstablish identity and verify the environment is releasable:\n\n```bash\ndate -u +%Y-%m-%dT%H:%MZ\ntest \"$(git rev-parse --abbrev-ref HEAD)\" = linux-next   # assert, don't display\ngit fetch origin --prune\ngit pull --ff-only origin linux-next         # local linux-next == origin\u002Flinux-next\ntest -z \"$(git status --porcelain)\"          # assert clean, don't display\n```\n\n> Those two lines used to be bare `git rev-parse` \u002F `git status --short` with a\n> `# MUST be ...` comment beside them. A comment is not a gate: both commands\n> exit 0 on the wrong branch and on a dirty tree, printing the disagreeing\n> answer to a log nobody re-reads. Same family as the `null`-vs-empty traps\n> below — a step that CONSULTS an external fact instead of REQUIRING it.\n\nIf the worktree is dirty, stop and file\u002Fupdate a plan blocker. Do not stash\nrelease inputs or auto-artifact churn; local state is volatile and hidden stashes\nmake release evidence unrecoverable.\n\nAlso verify there are no local-only commits:\n\n```bash\ntest \"$(git rev-list --count origin\u002Flinux-next..HEAD)\" -eq 0\n```\n\nIf the branch is not `linux-next`, log + exit without escalating — the release flow only ships from the Linux integration branch.\n\n### Cloud minutes (order 1966-47vt)\n\nRead `cloud.usage` (MCP; on the host `tillandsias --cloud-usage`) and keep\nthe used\u002Fcap per provider for step 8. A metric saying\n`unavailable: permission-missing:\u003Cprovider>:\u003Cpermission>` is a grant to\nreport, never a zero.\n\n### Tray-parity completeness (release-scoped gate — order 243 semantic split)\n\nThe per-build litmus (`litmus:tray-parity-matrix-complete`) only gates each\nhost's OWN column so sibling verification debt cannot block unrelated local\nbuilds. The ALL-platform completeness requirement from\n`plan\u002Fissues\u002Ftray-feature-parity-matrix-2026-06-28.md` (\"the matrix should be\nall-green on `required` rows before that release\") lives HERE instead:\n\n```bash\n# 746-htj9: the sanctioned YAML path — ruby is NOT present in every\n# environment this runbook runs in (it broke the forge once already).\n. scripts\u002Fplan-binary-probe.sh\nPLAN=\"$(resolve_plan_binary)\" || { echo 'blocked: no runnable tillandsias-plan (run scripts\u002Fcycle-preflight.sh)'; exit 1; }\ngaps=\"$(\"$PLAN\" yaml-json openspec\u002Ftray-parity-matrix.yaml | jq -r '.features[] | select(.parity==\"required\") | . as $f | (\"linux\",\"macos\",\"windows\") | select($f[.] != \"done\") | \"\\(.): \\($f.capability) status=\\($f[.])\"')\"\nprintf '%s\\n' \"$gaps\" | grep -v '^$' || true\necho \"parity gaps: $(printf '%s' \"$gaps\" | grep -c . || true)\"\ntest -z \"$gaps\"\n```\n\nIf this exits non-zero, the parity matrix has unverified\u002Fincomplete `required`\ncells. This is an **advisory hold, operator-overridable**: record the gap list\nin the cycle outcome (§8) and in `plan\u002Floop_status.md`, and proceed ONLY if the\noperator has recorded a release-with-parity-gaps approval for the cycle (The\nTlatoāni owns that call — releases shipped with parity gaps before this gate\nexisted, so a hard stop would strand the daily Linux release on macOS\u002FWindows\nverification debt; make the debt loud, not invisible).\n\n---\n\n## 1 — Decide the promotion path, THEN (maybe) compute a version\n\n**Two paths exist, and choosing wrong discards evidence (952-mrsl, learned\nduring the v56.8.31.3 stable promotion):**\n\n- **PROMOTE-EXISTING** (the default when a blessed release already exists):\n  a daily release was already cut, its assets were verified by a blessing\n  round, and the ask is \"make it stable\". Do NOT cut a new version — a fresh\n  cut discards the exact artifacts the blessing verified. Skip this step's\n  computation entirely: merge linux-next → main (steps 2-3), SKIP the\n  VERSION bump (step 4) when main's merged VERSION already equals the\n  existing tag, skip tag\u002Fdispatch\u002Fwatch (steps 5-7), and instead flip the\n  existing release: `gh release edit vX.Y.Z.N --prerelease=false --latest`.\n  Verify `\u002Freleases\u002Flatest` resolves to it. Stable then serves byte-for-byte\n  what was blessed. Use `scripts\u002Fpromote-stable.sh \u003Ctag>`, which also moves\n  the `stable` tag; that push needs no `.\u002Fbuild.sh --check`, because a tag on\n  a commit already on origin\u002Fmain skips the gate stamp (1768-2k56).\n  **Promote only while the land queue is idle.** A promotion during a land\n  run half-promoted on 2026-10-17: the release flipped, the tag push did not.\n  The script now says `refused:stable-tag-push-refused:\u003Csha>` and prints the\n  push that finishes it.\n  **Reading a channel tag in a clone? Sync it first** (1271-ff72):\n  `git fetch origin '+refs\u002Ftags\u002Fstable:refs\u002Ftags\u002Fstable' '+refs\u002Ftags\u002Funstable:refs\u002Ftags\u002Funstable'`.\n  A plain `git fetch --tags` REFUSES to move these two (\"would clobber\n  existing tag\"), so a clone's `stable`\u002F`unstable` can sit days behind\n  origin's. The release tooling reads origin (`git ls-remote`) and is\n  unaffected; a human `git log stable` in a stale clone is not.\n- **CUT-NEW** (no existing blessed release, or the tree moved past it):\n  compute the next version and run every step below.\n- **FIX-FORWARD with zero code delta** (a published cut lost one platform's\n  artifacts to a runner or workflow defect, and the repair touches only\n  `.github\u002Fworkflows\u002F`, the README ledger and `plan\u002F`): this is CUT-NEW for\n  the version and the tag, but the §3 gate is COMPOSED rather than re-run.\n  Assert the premise, never assume it — from the tree the previous cut's\n  `--ci-full --install` gated (`\u003Cgated>`) to `origin\u002Flinux-next`, the diff\n  outside `plan\u002F`, `docs\u002F`, `skills\u002F`, `cheatsheets\u002F`, `README.md`,\n  `.github\u002Fworkflows\u002Frelease.yml` and the previous cut's own\n  `VERSION`\u002F`Cargo.lock`\u002F`crates\u002F*\u002FCargo.toml` bump MUST be empty; the delta\n  itself was gated by the land tool's `.\u002Fbuild.sh --check`; and\n  `scripts\u002Frelease-preflight.sh` is re-run fresh at merge time (it is cheap and\n  it is the only check that reads the NEW tag). Record the composition in the\n  PR body and the ledger row in those words. If the diff is not empty, it is a\n  CUT-NEW and the full gate runs. Measured on v56.9.19.2 (2026-09-19): the\n  Windows tray job of v56.9.19.1 died on a runner staging gap; re-running a\n  two-hour gate on a tree whose code bytes the gate had already passed would\n  have measured nothing new, and the assertion above is what makes that claim\n  checkable rather than convenient.\n\n**The version scheme is epoch-anchored CalVer** (operator ruling 2026-08-31):\n`\u003Cyears_since_epoch>.\u003Cmonth>.\u003Cday>.\u003Cbuild>` — e.g. `56.8.31.3` for the third\nbuild of 2026-08-31. `scripts\u002Fbump-version.sh` is the scheme's executable\ndefinition (`openspec\u002Fspecs\u002Fversioning\u002Fspec.md` is the spec); NEVER re-derive\nthe format here. This section previously computed `MAJOR.MINOR.YYMMDD.N` by\nhand — under the epoch scheme that hand-derivation produces the malformed\nhybrid `v56.8.260901.1`, whose six-digit third field is the exact Store-cap\nviolation the cutover retired. Delegate:\n\n```bash\n# CUT-NEW path only. Same UTC day → build+1; new UTC day → \u003Cy>.\u003Cm>.\u003Cd>.1.\n# The bump lands via a release\u002Fversion-bump-* branch (step 4), never here.\n# NOTE the layout-preserving scratch: bump-version.sh anchors VERSION to its\n# OWN script path (SCRIPT_DIR\u002F..), not to $PWD — copying VERSION alone into a\n# scratch dir and cd-ing there would bump the REAL checkout's file.\nnew_version=\"$(\n  tmp=$(mktemp -d) && mkdir -p \"$tmp\u002Fscripts\" \\\n  && cp VERSION \"$tmp\u002FVERSION\" && cp scripts\u002Fbump-version.sh \"$tmp\u002Fscripts\u002F\" \\\n  && bash \"$tmp\u002Fscripts\u002Fbump-version.sh\" --bump-build >\u002Fdev\u002Fnull \\\n  && tr -d '[:space:]' \u003C \"$tmp\u002FVERSION\" && rm -rf \"$tmp\"\n)\"\nnew_tag=\"v${new_version}\"\necho \"Computed: $new_tag (dry-run bump in a scratch layout; the real bump lands in step 4)\"\n```\n\nSanity: the third field is a day-of-month (1-31), never six digits; the first\nfield is ≥56 and ≤65535.\n\n---\n\n## 2 — Open or update the PR\n\nGitHub has at most one open PR `linux-next → main` at a time. Reuse it if present; open a new one otherwise.\n\n```bash\nif pr=\"$(scripts\u002Fresolve-open-pr.sh main linux-next)\"; then\n    existing_pr=\"${pr#ok:open-pr:}\"\nelse\n    case \"$pr\" in\n        blocked:open-pr:gh-failed:*) echo \"$pr\"; exit 1 ;;   # retryable, NOT \"no PR\"\n    esac\n    gh pr create --base main --head linux-next \\\n        --title \"release: ${new_tag} — daily linux-next promotion\" \\\n        --body \"Automated daily promotion of linux-next → main by the \\`merge-to-main-and-release\\` skill. The follow-on tag + workflow_dispatch trigger publishes ${new_tag} for Linux Silverblue smoke-test.\"\n    pr=\"$(scripts\u002Fresolve-open-pr.sh main linux-next)\" || { echo \"$pr\"; exit 1; }\n    existing_pr=\"${pr#ok:open-pr:}\"\nfi\necho \"PR #${existing_pr}\"\n```\n\n> The trap this replaces — the THIRD instance of one class in this file, after\n> step 3's `gh pr checks --watch` note and step 7's. The step used to be:\n>\n> ```bash\n> existing_pr=$(gh pr list --base main --head linux-next --state open --json number --jq '.[0].number')\n> if [[ -z \"$existing_pr\" ]]; then ...\n> ```\n>\n> `--jq '.[0].number'` on an EMPTY array prints the literal `null`, not an empty\n> string. With no open PR the emptiness test was FALSE, so `gh pr create` never\n> ran and the runbook announced `PR #null` and walked into step 3 to merge a\n> pull request that does not exist. The re-query after creation had the same\n> shape, so a creation that silently failed produced `PR #null` too.\n>\n> `scripts\u002Fresolve-open-pr.sh` asserts a positive fact and, like its sibling\n> `resolve-release-run.sh`, keeps \"gh failed\" distinct from \"gh has nothing to\n> say\" — here that distinction decides between retrying and creating a PR.\n\nUpdate the PR body with today's `${new_tag}` even if reusing — the human reviewer should see which version is being shipped.\n\n---\n\n## 3 — Gate LOCALLY, then merge\n\n> **This step changed on 2026-08-03 and the change is silent, so read it.**\n> Push CI was removed (operator directive: only the release may spend cloud\n> minutes). `main`'s three required status contexts were cleared, because they\n> named jobs that no longer exist and made every PR permanently unmergeable.\n>\n> The trap: `gh pr checks --watch` does NOT fail when there are no checks. It\n> prints `no checks reported on the '\u003Cbranch>' branch` and **exits 0**,\n> immediately. So the old text — \"ALL required checks pass → proceed to merge\" —\n> now means \"merge with zero verification\" while reading exactly like a gate that\n> passed. That is a false green, which is worse than a hard break.\n>\n> The verification did not disappear; it moved to this host. Run it here.\n\n**Announce a cut freeze before running the gate.** From the moment\n`.\u002Fbuild.sh --ci-full` starts until the step-5 back-merge is pushed, any CODE\nlanding on `linux-next` — the branch this gate verifies — moves the head past\nthe commit this gate verified and forces a full re-gate. State the window\n(start time, expected end at the back-merge push) so other hosts hold code\nlandings until it lifts (drill: plan\u002Fissues\u002Ffleet-restart-2026-09-12.md, Cut freeze versus gate freeze).\n\n- Platform-branch pushes (`osx-next`, `windows-next`) move nothing the gate\n  reads and are not frozen; `plan\u002F`, `docs\u002F` and `skills\u002F` pushes through the\n  plan-only lane are exempt — but exempt means allowed, not free: a plan-only\n  closure push during a relay gate cost one host a re-integrate and a second\n  8-minute gate. The question before any push in the window is \"is anyone\n  mid-gate\", not \"am I exempt\" (drill: plan\u002Fissues\u002Ffleet-restart-2026-09-12.md, An exemption describes what is allowed, not what is free).\n- The freeze is enforced by nobody: the pre-push hook checks the trunk merge\n  and the gate stamp, never whether a freeze is live, so a push that starts\n  before the announced window and lands during it goes through clean\n  regardless (yolanda, 2026-09-14; drill: plan\u002Fissues\u002Ffleet-restart-2026-09-12.md, yolanda reported a freeze breach that was not one, and found a real gap doing it).\n\n**Before the gate, if this host rebaked its images today** (any `--init`\nafter a version change, the post-restart checklist), warm podman's ID-mapped\nlayer copy of the forge image once — `podman run --rm --userns=keep-id\nlocalhost\u002Ftillandsias-forge:\u003Cinstalled VERSION> true` (exit 2 from the\nentrypoint's credential refusal is expected; the copy is what you want). The\nfull pre-build litmus runs four forge-runtime fixtures under `--userns=keep-id`\nand each one's budget expires while podman copies a 3.1 GB image; the new\nkill-time adjudicator reads them as NOT contended, genuinely too slow, which\nis true and misleading. Measured on the v56.9.2.1 cut: four reds, all gone\nafter one warm (plan\u002Fissues\u002Fenhancement-keep-id-layer-copy-cold-after-image-\nrebake-2026-09-02.md). Also clear `target-guest\u002F` if it was staged by an\nearlier `--install` run under a bumped VERSION — `build-guest-binaries.sh\n--verify` reads that as an integrity mismatch, not stale staging.\n\n`gh` resolves the repository from the working directory: run every `gh`\ncommand from the checkout (or pass `-R \u003Cowner>\u002F\u003Crepo>`), never from a scratch\ndirectory — `gh release download` from elsewhere fails with \"not a git\nrepository\" and looks like a missing asset.\n\n```bash\n# DECLARE THE FREEZE FIRST (order 1176-9vqn). Until this existed the freeze was\n# announced in ledger prose and enforced by nothing: MEASURED on yolanda during\n# the v56.9.13.1 cut, a host merged trunk, gated for 2476 s and pushed, and\n# every pre-push check passed because none of them was about a freeze. It was\n# harmless only because the push went to windows-next; aimed at linux-next it\n# would have put code into the frozen branch with every check green.\n#\n# THE WINDOW IS WHY A DISCIPLINE RULE CANNOT COVER THIS: the gate below runs\n# 41 minutes on yolanda and longer on the floor, so a freeze declared inside\n# that window is invisible to a land that checked before it started, and the\n# land pushes on completion without re-asking. The marker lives on origin and\n# the hook consults it AT the push.\n#\n# Plan-only pushes stay admitted by design — the plan lane is how coordination\n# keeps moving during a cut, and every host used it under the last freeze.\nscripts\u002Frelease-freeze.sh set \"${release_source_branch:-linux-next}\" \"cut ${new_tag}\"\n# -> ok:freeze-set:refs\u002Ftillandsias\u002Ffreeze\u002F\u003Cbranch>\u002F\u003Chost>\u002F\u003Cepoch>\n# A `refused:freeze:unreachable:*` here is a STOP: an unreachable origin means\n# the cut cannot publish either. Do not proceed with an undeclared freeze.\n#\n# LANDING A FIX TO THE CUT WHILE FROZEN (order 1265-8qr6). The hook refuses\n# the cut host's own code push like anyone's. Do NOT clear for it: a clear\n# leaves origin with no marker for the whole land, gate included, and every\n# host reads ok:freeze-none. Hand the freeze over instead:\n#   scripts\u002Frelease-freeze.sh land \"${release_source_branch:-linux-next}\"\n#   # -> ok:freeze-landing:refs\u002Ftillandsias\u002Ffreeze-landing\u002F\u003Cbranch>\u002F\u003Chost>\u002F\u003Cepoch>\n#   #    (written BEFORE the freeze marker goes; status reads\n#   #    frozen-landing:\u003Cbranch>:by=\u003Chost>)\n#   ... land the fix ...\n#   scripts\u002Frelease-freeze.sh set \"${release_source_branch:-linux-next}\"\n#   # the re-freeze writes the freeze marker BEFORE the landing marker goes\n# The land queue runs only on the cut host, and the cut host decides when to\n# start it, so hold the queue during a land: while the landing marker is live,\n# land the cut's fix alone and start no queue run until the re-freeze. The\n# queue cannot tell the cut's fix from anyone else's candidate (identity at\n# push time is self-declared), so this is procedure, not a check.\n# For every other host, frozen-landing means HOLD code exactly as frozen:\n# only ok:freeze-none is clear. There is no cut-host exemption in the hook;\n# host identity at push time is self-declared, so the freeze is a\n# coordination signal, not an authentication boundary.\n\n# THE GATE. This is the whole safety net now — there is nothing server-side.\n.\u002Fbuild.sh --ci-full || { echo \"gate failed — do NOT merge\"; exit 1; }\nscripts\u002Frelease-preflight.sh || { echo \"preflight refused — do NOT merge\"; exit 1; }\n```\n\n**The gate INSTALLS (1122-6sqz).** `--ci-full` ends with \"continuing to\ninstall\" and puts the tree's launcher on the host that ran it, under\n`TILLANDSIAS_SKIP_VERSION_BUMP=1` too — that flag skips the bump, nothing skips\nthe install. On a coordinator desktop that replaces the operator's local build\nwith the tree's VERSION label: measured 2026-09-11 on macuahuitl, v56.9.11.3\nbecame v56.9.5.1 (HEAD code, older label). Operator ruling 2026-09-12: the\nlabel is a monotonic counter and simply rolls forward on the next local build,\n\"CRDT style\" — so note `tillandsias --version` before the gate, expect it to\nchange, and do not re-install for the coordinator under SKIP_VERSION_BUMP.\n\n`release-preflight.sh` carries the two checks that used to live inside\n`release.yml` (VERSION monotonicity, retired CLI flags) plus the plan-ledger and\nactions-budget gates. It prints one line: `ok:release-preflight` or\n`blocked:\u003Creason>`.\n\nOnly once both are green:\n\n```bash\ngh pr merge ${existing_pr} --merge            # merge commit — preserves linux-next history\n```\n\nNote `--auto` is dropped: auto-merge waits for checks that will never report, so\nthe PR would simply sit there. Merge directly, having gated locally.\n\nIf either gate fails: surface the failure, do NOT merge, do NOT tag, write a\nledger entry. The next cycle retries.\n\nUse `--merge` (not `--squash`): the linux-next history is the audit log of the daily work-loop and integration cron cycles. Preserve it.\n\n---\n\n**Cloud budget refusal (order 1966-47vt).** Before merging, re-read\n`cloud.usage`: when Cloudflare `workers_builds_minutes` used is ≥ 2,700 of\n3,000 for the month, do NOT merge to main unless the fix is urgent; say so in\nthe cycle outcome.\n\n## 4 — Bump VERSION on main via PR (branch protection, order 476)\n\n`main` has server-side branch protection (enabled 2026-07-28, order 476\nprong (a)): direct pushes are rejected for EVERYONE including admins\n(`enforce_admins: true`), so the VERSION bump lands through a short-lived PR.\n\n**Corrected 2026-08-03 (order 598-h774).** This section used to say \"PRs to main\nrequire the three CI checks but ZERO approvals\" and \"ci.yml has no path filters,\nso a VERSION-only PR still gets its checks\". Both are now false: `ci.yml` is\ndeleted, and `main`'s `required_status_checks.contexts` is `[]`. What remains is\nthe PR requirement and `enforce_admins: true` — no server-side verification of\ncontent at all. Step 3's local gate is what stands in for it, and it already ran\nagainst this tree. **But the bump branch still needs its own `.\u002Fbuild.sh\n--check` before the push** (measured on the v56.9.2.1 cut, 2026-09-02): the\npre-push hook compares the gate STAMP against the tree bytes, and the bump\nchanges VERSION — so the push is refused with\n\"the tree changed since .\u002Fbuild.sh --check last passed\". Run\n`TILLANDSIAS_CLASS_SELECTOR=off TILLANDSIAS_SKIP_VERSION_BUMP=1 .\u002Fbuild.sh --check`\non the bump branch. The selector must be OFF here: since 765-xpct a plain\n`--check` writes a SCOPED stamp (classes computed against linux-next), and\nthe hook cannot honour a scoped stamp on a brand-new branch with no local\nbase (\"the gate stamp is scoped to 'other,rust' but refs\u002Fheads\u002Frelease\u002F…\nhas no usable local base\"). Measured on the v56.9.23.1 cut, one wasted gate.\nThe same applies to the step-5 back-merge gate. Then\n`git add Cargo.lock` and `--amend` it into the bump commit if the check\ntouched it (still VERSION + Cargo only, 702-eusw), then push.\n**Corrected 2026-10-17 (order 1768-2k56):** the bump changes VERSION ONLY;\nthe crates' `Cargo.toml` versions do not move (measured on the v56.10.16.2\ncut). The `git add` below still names them so a future bump that does touch\nthem is not silently left out.\n\n```bash\ngit checkout main\ngit pull --ff-only origin main\nbump_branch=\"release\u002Fversion-bump-${new_version}\"\ngit checkout -b \"${bump_branch}\"\necho \"${new_version}\" > VERSION\n# THE BUMP COMMIT CARRIES VERSION + CARGO FILES ONLY (702-eusw, hit live\n# 2026-08-31): the pre-push guard refuses any commit that sweeps a VERSION\n# bump in with unrelated files. Run scripts\u002Fbump-version.sh to sync the\n# Cargo.toml semver, then add ONLY those files — never `git add -A` here\n# (a stray loop-status fragment once rode along and the push was refused).\nscripts\u002Fbump-version.sh >\u002Fdev\u002Fnull\ngit add VERSION Cargo.lock crates\u002F*\u002FCargo.toml 2>\u002Fdev\u002Fnull || git add VERSION\ngit commit -m \"release: bump VERSION to ${new_version}\n\nThe merge-to-main-and-release skill bumped VERSION as part of the\ndaily linux-next → main promotion (via PR: main is branch-protected,\norder 476). Tag ${new_tag} follows.\"\ngit push origin \"${bump_branch}\"\ngh pr create --base main --head \"${bump_branch}\" \\\n    --title \"release: bump VERSION to ${new_version}\" \\\n    --body \"Automated VERSION bump for ${new_tag} by the merge-to-main-and-release skill. Auto-merges when CI is green (0 approvals required by branch protection).\"\n# No --auto and no --watch: with zero required contexts, auto-merge waits for a\n# check that will never report and the PR sits open forever, while `checks\n# --watch` exits 0 on \"no checks reported\" and reads like a gate that passed.\n# Step 3 already gated this tree locally; merge directly.\ngh pr merge \"${bump_branch}\" --merge\ngit checkout main\ngit pull origin main                           # now contains the bump merge\ngit push origin --delete \"${bump_branch}\" 2>\u002Fdev\u002Fnull || true\ngit branch -D \"${bump_branch}\"\n\n# RETURN THE CHECKOUT. Non-negotiable, and the reason is not tidiness.\n#\n# The forge launcher seeds a new forge's branch from the HOST CHECKOUT'S CURRENT\n# BRANCH (read_host_project_current_branch, crates\u002Ftillandsias-headless\u002Fsrc\u002F\n# main.rs:2701 -> TILLANDSIAS_FORGE_SEED_BRANCH), and the guest converges to it\n# (images\u002Fdefault\u002Flib-common.sh:565 checkout_forge_seed_branch). So leaving this\n# checkout parked on `main` silently pins EVERY forge launched afterwards to\n# `main`.\n#\n# That is not cosmetic. `main` does not carry crates\u002Ftillandsias-plan\u002Fsrc\u002F\n# answer.rs or methodology.rs, so a forge seeded from it builds a PRE-EXPERT\n# binary; ensure_forge_experts then truthfully reports `experts: ready` while\n# every plan_answer \u002F methodology_path call returns confidence=unsupported.\n# Order 531 is exactly this, and it made milestone 391 ungradeable from inside a\n# forge. Two prior cycles absorbed it by noticing and hand-rebasing\n# (plan\u002Floop_status.md:203, :243).\ngit checkout \"${release_source_branch:-linux-next}\"\ngit pull --ff-only origin \"${release_source_branch:-linux-next}\" || true\n```\n\nCapture `release_source_branch=\"$(git symbolic-ref --short HEAD)\"` at the START\nof the release flow, before the first `git checkout main`, so this returns to\nwherever the operator actually was rather than assuming `linux-next`.\n\nVerify before considering the release complete:\n\n```bash\ngit symbolic-ref --short HEAD   # MUST NOT be `main`\n```\n\nA release that ends with the checkout on `main` is not finished, however green\nthe tag is: it has armed the next forge launch to build an expert-less binary\nthat reports itself ready.\n\nConfirm `cat VERSION` on main equals `${new_version}` before tagging. If\nthe bump PR's checks fail, surface the run URL and stop — do not tag a\nmain whose VERSION was not bumped. If main advanced concurrently (another\nrelease ran), re-run from the `git pull --ff-only` with a fresh branch; if\nstill conflicting after 3 attempts, write an `ESCALATION:` line in\n`plan\u002Fissues\u002Fmulti-host-integration-loop-2026-05-24.md` and stop.\n\n---\n\n## 5 — Tag, THEN back-merge, THEN push both (order 898-zhf3)\n\n> **THIS STEP USED TO SAY \"push the tag, then back-merge, never before\". THAT\n> ORDER IS UNEXECUTABLE and it had almost certainly never been run on a host\n> with the pre-push hook installed.** Measured on macuahuitl 2026-08-26 while\n> cutting v0.4.260826.1: `git push origin \"${new_tag}\"` was REFUSED —\n>\n> ```\n> ✗ pre-push refused: release preflight says blocked:version-not-monotonic\n>   ERROR: Version 0.4.260817.1 is LESS than latest release v0.4.260826.1\n>   REMEDY: git fetch origin && git merge origin\u002Fmain\n> ```\n>\n> **The guard is correct.** The moment `git tag` creates the tag LOCALLY,\n> `verify-version-monotonic.sh` resolves it as \"latest release\" and compares it\n> against the branch's VERSION — which is still the pre-release value, because\n> the back-merge is the very thing the old text deferred until after this push.\n> So: tag push requires the back-merge; the back-merge was prescribed after the\n> tag push. Circular, on every host that has run `scripts\u002Finstall-hooks.sh`.\n>\n> The pressure at that moment is toward `git push --no-verify`, which is the one\n> exit the hook's own text warns against — and it would push the release tag\n> past the only remaining gate.\n\n**Create the tag. Do NOT push it yet.**\n\n```bash\ngit tag -a \"${new_tag}\" -m \"Release ${new_version}\n\nDaily linux-next → main promotion via the merge-to-main-and-release\nskill. See PR #${existing_pr} for the merged work range.\n\"\n```\n\nThe annotated tag carries the PR reference so the GitHub Release page links back to the merged work.\n\n**WHY DEFERRING THE PUSH IS SAFE, which is the concern the old ordering was\nprotecting and which survives intact.** The old text's stated reason was \"the\ntag must keep pointing at main's bump-merge commit\". It does: the tag is created\nagainst main's bump-merge SHA and a back-merge into `linux-next` cannot move a\ntag that points at a commit on `main`. **Verified, not assumed** — during the\nv0.4.260826.1 cut `git rev-list -n1 v0.4.260826.1` returned `341ab0010` both\nbefore and after the back-merge.\n\n### The back-merge is part of the cut, not a tidy-up (order 800-vk2p)\n\n```bash\n# The moment ${new_tag} EXISTS — locally is enough — release-preflight gate 1\n# refuses blocked:version-not-monotonic on every branch still carrying the\n# pre-release VERSION. At v0.4.260817.1 that was all three platform branches,\n# fleet-wide, within seconds — including this release's OWN ledger records, so\n# step 8 cannot push until this lands. Precedents: f6424070d, 97cd7068b.\n#\n# It also refuses THIS host's tag push, which is why the back-merge comes first\n# (898-zhf3). \"Locally is enough\" is the correction: the old text said \"is\n# pushed\", and the guard resolves the tag from the local ref.\ntest \"$(git symbolic-ref --short HEAD)\" = \"${release_source_branch:-linux-next}\"\ngit fetch origin\ngit merge origin\u002Fmain          # back-merge the VERSION bump; merge, never rebase\nTILLANDSIAS_CLASS_SELECTOR=off TILLANDSIAS_SKIP_VERSION_BUMP=1 .\u002Fbuild.sh --check\n                               # the merge changed VERSION, so the gate stamp is\n                               # stale and the pushes below would be refused.\n                               # MEASURED 2026-10-17 (1768-2k56): this is a ~3 s\n                               # memo hit, not a full gate; the bytes it covers\n                               # already passed step 3's gate\n\n# CLEAR THE FREEZE BEFORE THE BACK-MERGE PUSH, and note WHY it is here rather\n# than at the end of the cut (order 1176-9vqn): the back-merge push below is\n# itself a CODE push to the frozen branch, carrying the VERSION bump merge. A\n# freeze still live at this line would refuse the cut's own push — correctly,\n# which is the point: the mechanism does not know you are the coordinator. So\n# the freeze's lifetime runs from the gate start to exactly here, which is the\n# declared policy (hold code lands on the release branch from the gate start to\n# the stage-2 back-merge push) expressed as a lifetime rather than a promise.\nscripts\u002Frelease-freeze.sh clear \"${release_source_branch:-linux-next}\"\n# -> ok:freeze-cleared:\u003Cn>; clearing removes every marker for the branch\n#    (freeze and landing markers alike), whoever set it, so a coordinator can always clear a freeze another host\n#    declared. `ok:freeze-cleared:0` means it was already clear — not an error.\n\n# NOW both pushes succeed. Order between them does not matter; both are gated\n# on the back-merge above, not on each other. The tag push is unaffected by a\n# freeze either way: the check only looks at refs\u002Fheads\u002F*. A tag on a commit\n# already on origin\u002Fmain skips the gate stamp (1768-2k56); release-preflight and\n# the version guard still run on it, which is the 898-zhf3 ordering above.\ngit push origin \"${new_tag}\"\ngit push origin \"${release_source_branch:-linux-next}\"\n```\n\n**VERIFY THE TAG DID NOT MOVE, rather than assuming it.** The whole reason the\nold ordering existed was to keep the tag on main's bump-merge commit:\n\n```bash\ngit rev-list -n1 \"${new_tag}\"   # MUST equal main's bump-merge SHA\n```\n\nA back-merge into the source branch cannot move a tag pointing at a commit on\n`main` — but check it, because this is the property the reordering trades on.\n\n`windows-next` and `osx-next` then self-heal on their next mandated\n`git merge origin\u002Flinux-next` (methodology `pull_merge_cadence.pre_push_gate`),\nso this one push unblocks the fleet. No `--no-verify` is ever needed —\n`pre-push-version-guard.sh` already classifies a VERSION equal to\n`origin\u002Fmain`'s as sync-forward.\n\n---\n\n## 6 — Trigger the release workflow\n\n`release.yml` is `workflow_dispatch`-only. The skill explicitly triggers it, never via `on: push: tags:` (the workflow is intentionally manual to keep release authorship traceable).\n\n**One dispatch, ON MAIN, with the version as input (order 1953-jmvb).**\nOperator ruling 2026-11-01: \"let's drop the compiler build, and start the\nrelease from main\". The run's ref is `refs\u002Fheads\u002Fmain`, so the Nix and\ncargo caches it saves are restored by the next release inside the ONE daily\ncloud build — from a tag ref they never were (1950-b4f3: five releases, zero\nrestores, 66-min seed run cancelled by the operator). The workflow refuses\nany other ref, reads VERSION from main's tree, and creates `v\u003CVERSION>` at\nits own SHA or verifies the tag step 5 pushed points there. The Sigstore SAN\nis `release.yml@refs\u002Fheads\u002Fmain`. litmus:release-is-one-cloud-build refuses\na dispatch on any other ref or a seed input. Main's HEAD must be the\nbump-merge SHA the tag names (step 5 asserts that) — do not push anything\nto main between step 5 and this dispatch.\n\n```bash\ngh workflow run release.yml --ref main -f version=\"${new_version}\"\nsleep 5\ngh run list --workflow=release.yml --branch=main --limit 1\n```\n\n(The old `recipe-publish.yml` custom-rootfs workflow was removed in the 2026-06 Fedora pivot — Windows\u002FmacOS now fetch official Fedora WSL\u002FCloud images directly, so there is no rootfs CI to coordinate with anymore.)\n\n---\n\n## 7 — Wait for the release build + surface artifacts\n\n```bash\nrun_id=\"$(scripts\u002Fresolve-release-run.sh \"${new_tag}\")\" || { echo \"$run_id\"; exit 1; }\nrun_id=\"${run_id#ok:release-run:}\"\ngh run watch \"${run_id}\"                     # blocks until green or red\nassets=\"$(gh release view \"${new_tag}\" --json assets --jq '.assets[].name')\"\n```\n\n**Assert, don't display.** v56.9.11.1 (2026-09-11) published Linux and macOS\nassets while the Windows tray job FAILED (1122-xi2f); `gh release view`\nprinted a healthy-looking release and the gap was found a day later by a\nWindows host spending an assignment slot on it. A platform job can fail\nwhile the run still publishes the others' assets. So list the asset names\nand REQUIRE all three sets — failing this step loudly and naming the missing\nplatform — rather than just printing whatever exists. The only sanctioned\nabsence is the unsigned MSIX (see Hard guardrails), never a platform's\nprimary asset (drill: plan\u002Fissues\u002Ffleet-restart-2026-09-12.md, v56.9.11.1 was cut 2026-09-11). Asset names measured on v56.9.13.1\n(drill: plan\u002Fissues\u002Ffleet-restart-2026-09-12.md, v56.9.13.1 PUBLISHED):\n\n```bash\nfor a in tillandsias-linux-x86_64 SHA256SUMS \\\n         \"tillandsias-tray-${new_version}-windows-x64.zip\" tillandsias-tray.exe SHA256SUMS-windows \\\n         \"tillandsias-tray-${new_version}-macos-arm64.tar.gz\" Tillandsias.dmg SHA256SUMS-macos; do\n  printf '%s\\n' \"$assets\" | grep -qxF \"$a\" || { echo \"blocked:release-asset-missing:$a\"; exit 1; }\ndone\necho \"ok:release-assets:all-three-sets\"\ngh release view \"${new_tag}\" --json url --jq '.url'\n```\n\nThe MSIX is never asserted here — Step 7's asset check expects the HONEST\nWITHHOLD on unsigned builds (see Hard guardrails), not a flagged-missing\nplatform asset.\n\n> The trap this replaces, and it is the same family as the `gh pr checks\n> --watch` note in step 5. The step used to be a bare command substitution:\n>\n> ```bash\n> run_id=$(gh run list --workflow=release.yml --branch=\"${new_tag}\" --limit 1 --json databaseId --jq '.[0].databaseId')\n> ```\n>\n> When step 6's dispatch did not actually start a run — wrong ref, a\n> `workflow_dispatch` that silently no-oped, a tag that never reached the\n> remote — `gh run list` prints nothing and exits **0**. The substitution\n> succeeded with an EMPTY `run_id` and the runbook walked into `gh run watch \"\"`\n> having never noticed the thing it was waiting for did not exist. On an empty\n> result set `--jq '.[0].databaseId'` prints the literal `null`, so a bare\n> emptiness test is not enough either.\n>\n> `scripts\u002Fresolve-release-run.sh` asserts a positive fact and separates \"gh\n> failed\" from \"gh had nothing to say\" — they are different faults.\n> Pinned by `scripts\u002Ftest-resolve-release-run.sh` (6\u002F6, fake `gh`, no network).\n> Found by the 601-462g audit, in this runbook.\n\nThe Linux Silverblue smoke-test artifact is the `tillandsias-linux-x86_64` musl binary. Surface its URL to the user.\n\n---\n\n## 8 — Record the cycle outcome\n\n**The ledger row is on origin BEFORE any smoke go is sent.** The curl-install\nsmoke's §0.2b reads the row for the tag under test and files `NO LEDGER ROW`\nas a finding when it is absent — correctly, and it did so twice on\n2026-09-19 (macneo, pirria) because the coordinator sent both gos while the\nv56.9.19.1 row was still waiting on a refused land (the row had taken the\ntable past the distillation threshold). A smoke started before the row lands\nproduces a finding that is right at run time and stale by the time it is\nread; the cheaper order is row first, gos second, and a go message names the\nrow's landed SHA.\n\nAppend a one-line entry to `plan\u002Fissues\u002Flinux-next-work-queue-2026-05-25.md`:\n\n```\n- \u003CUTC>  `\u003Cmerge_sha>`  Release ${new_tag} — merged PR #${existing_pr} to main, tagged, workflow_dispatch triggered. Linux build: \u003Cnix_build_duration> (Nix cache: \u003Chit|miss>), total run: \u003Ctotal_run_time>. Linux artifact: \u003Cbrowser_download_url>.\n```\n\nAdd the `cloud.usage` reading to that entry: `cloud: cloudflare \u003Cused>\u002F3000 min, github cache \u003Cbytes>\u002F10GiB` (order 1966-47vt).\n\n**Also record the release in the README ledger** (operator directive\n2026-07-16; order 380) — **APPEND for a new tag, EDIT for a tag that already\nhas a row.** Which one you are doing is decided by the TAG, not by which step\nyou are in, so ask before writing:\n\n```bash\ngrep -cE \"^\\| ${new_tag}( |\\()\" README.md   # 0 -> append  |  1 -> EDIT that row  |  >1 -> a duplicate already exists\n```\n\n**WHY THIS BRANCH EXISTS, and it is not hypothetical.** This step used to say\nonly \"append a row\". PROMOTE-EXISTING (step 1) promotes a tag that was cut\nearlier, so **its row exists by construction — the cut wrote it** — and an\nunconditional append puts a second row on the same release. That is exactly how\n`v0.4.260826.1` and `v0.4.260817.1` each acquired two rows, and the pairs were\nnot copies: the second of each was a later, richer rewrite, so resolving them\nmeant deleting prose a human had written. They were only cleared by being\nfolded into a DISTILLED span. Order 380 carried this as its last open item.\n\nMEASURED 2026-09-16: promoting `v56.9.13.1` to stable, the correct action was to\nEDIT its existing cut row — adding that it was promoted, by flag flip rather\nthan a fresh cut, and naming the defects shipping with it. Appending would have\nproduced the third instance of this defect.\n\n- **A CUT appends** one new row at the TOP of the table body.\n- **A PROMOTION EDITS THE EXISTING ROW** in place: change the `RELEASE` cell to\n  carry `(**STABLE**)`, say it was promoted and on what date, and — because a\n  promotion ships whatever the cut shipped — name any known defect that goes\n  stable with it. Do not restate the cut's feature list; it is already there.\n\nFor an append, the new row's cells are:\n\n- `RELEASE`: `${new_tag} (daily)` — or `(**STABLE**)` when this release is a\n  stable-channel promotion.\n- `INTENDED FEATURES`: the planned work this release set out to ship\n  (orders\u002Ffeatures from the merged range), stated whether or not complete —\n  incomplete intentions stay listed with their follow-on order.\n- `BUGFIXES`: unplanned maintenance — regressions from previous milestones or\n  breakage discovered during the day's development.\n\nKeep rows honest and compact (the evidence trail lives in `plan\u002F`); when the\ntable exceeds ~10 rows, distill the oldest rows into the `*Older releases*`\nline per the semantic-distillation policy.\n\n**You are TOLD when that threshold is crossed — you no longer have to notice it\nhere** (order 914-nkc4). `scripts\u002Frelease-preflight.sh` runs\n`scripts\u002Fcheck-ledger-distillation.sh`, which reports\n`due:ledger-distillation:\u003Cn>-rows:threshold=\u003Ct>` and names the destination line.\nIt is ADVISORY and cannot refuse a cut: an over-long table costs a long README,\nnever a false verdict. The threshold lives in that script rather than in this\nsentence, so the number cannot drift between the two. This paragraph used to be\nthe whole mechanism, and the table reached 19 rows — nearly double — with no row\never distilled, because a sentence in a runbook step is something a reader is\nexpected to notice while doing something else.\n\n**Append the release's observatorium snapshot** (order 1661-vckd). The\ndashboard at observatorium.tillandsias.org plots one durable observation per\nrelease, and its durable storage is THIS repository: Cloudflare's deploy only\nreads `observatorium\u002Fhistory\u002Fsnapshots.jsonl`, it never writes it (it has no\npush credential, and must not). So the cut appends exactly one line, naming\nthe tag, in the same ledger push.\n\nThis line is the dashboard's HEADLINE CentiColon grade. Workers Builds runs no\nlitmus, so a deploy can trace a requirement but never positively test one; only\na host with recorded litmus results can. So the cut records a pre-build litmus\nrun on THIS tree first, then grades. `collect.mjs` refuses to append a\nrelease snapshot whose grade is unmeasured or graded over no recorded results:\n\n```bash\nscripts\u002Frun-litmus-test.sh --phase pre-build --compact   # records per-test results for this tree's bytes\n(cd observatorium && node scripts\u002Fbuild.mjs --release-snapshot \"${new_tag}\")\ntail -1 observatorium\u002Fhistory\u002Fsnapshots.jsonl | grep -q \"\\\"release\\\":\\\"${new_tag}\\\"\" \\\n  || { echo \"observatorium snapshot does not name ${new_tag}\"; exit 1; }\ngit add observatorium\u002Fhistory\u002Fsnapshots.jsonl\n```\n\nIt builds `tillandsias-plan` from this checkout (or uses `TILLANDSIAS_PLAN_BIN`)\nto fold the plan and grade CentiColons; a metric it cannot observe is recorded\nas an explicit gap in the line, never as a zero. A failing litmus test does not\nblock the snapshot: it un-earns its requirements, and the grade says so.\n\nPush the ledger update to `linux-next` so other hosts and the next work-loop see the release happened.\n\nBefore success, confirm the release ledger update was pushed and the local\nbranch is not ahead of upstream.\n\n---\n\n## Hard guardrails\n\n- **NEVER push directly to `main`** — always via PR. Since 2026-07-28 this\n  is server-enforced (order 476: branch protection with `enforce_admins`,\n  PR required, 0 approvals, and since 2026-08-03 zero required status\n  contexts), so a direct push fails loudly — including\n  the VERSION bump, which is why step 4 uses a PR.\n- **NEVER `git push --force`** — main is protected (force-pushes and\n  deletions rejected server-side).\n- **NEVER skip the workflow_dispatch step**: the release workflow is manual-only by design. If the user wants automatic-on-tag, they edit release.yml first.\n- **NEVER bump VERSION on linux-next**; only on main as part of the release commit. Sibling hosts (osx-next \u002F windows-next) consume VERSION from their respective merge points; bumping it on linux-next desyncs them.\n- The release ships three platform artifact SETS to ONE GitHub release with matching versions:\n  Linux musl (`release` job), macOS arm64 thin tray + DMG (`macos-release`), Windows x64 thin\n  tray zip (`windows-release`). The macOS\u002FWindows jobs `needs: release` and\n  upload via `--clobber`. The MSIX is built but **an UNSIGNED MSIX is\n  deliberately WITHHELD from release assets** with its checksum line dropped\n  (2026-08-31: unsigned MSIX is inert for sideload — Add-AppxPackage\n  0x800B0100 — so shipping it only produces uninstallable downloads; the\n  withhold guard runs whenever the signing account is absent). Step 7's asset\n  check should therefore expect the HONEST WITHHOLD on unsigned builds, not\n  flag the MSIX missing. The Store channel is unaffected: Microsoft re-signs\n  submitted MSIX at $0 (776-g6r3), and Store submission uses a locally built\n  manifest-legal package, not the release asset.\n- **NEVER cancel an in-flight release** — let it complete or fail, then handle in the next cycle.\n- **NEVER end a cut without the step-5 back-merge pushed to the source branch.**\n  Until it lands, every sibling push is refused `blocked:version-not-monotonic`\n  for something none of them did (order 800-vk2p).\n\n---\n\n## Failure recovery\n\nIf any STEP fails:\n\n- **STEP 3 CI fails**: surface the failing run URL, do NOT merge, do NOT tag. The next 24-hour cycle retries. The work-loop continues to land fixes meanwhile.\n- **STEP 5 tag-push fails (existing tag)**: another run beat us. Skip release; cycle ends successfully (someone else released).\n- **STEP 5 back-merge push fails**: the fleet is blocked until it lands. Fixing and pushing the back-merge takes priority over everything else in the cycle — the refusal siblings are seeing names this exact remedy, so leaving it undone strands every host that reads it (order 800-vk2p).\n- **STEP 6 workflow trigger fails**: the tag is on main; user can manually run `gh workflow run release.yml --ref main -f version=${new_version}` (1953-jmvb: on main, never on the tag). Surface a clear next-step instruction.\n- **STEP 7 release build fails**: the tag exists on main but no GitHub Release was published. Next cycle does NOT retry the tag — it bumps to N+1. The orchestrator deals with the failed build separately.\n\n---\n\n## Why daily, not on-every-commit?\n\n- Every-commit releases would flood the GitHub Releases page and overwhelm the manual workflow_dispatch budget.\n- Linux Silverblue smoke-tests cost the user real time — daily is enough granularity to catch regressions without churn.\n- The work-loop + integration cron already enforce continuous green on every commit; this skill adds the \"shippable artifact\" gate on top.\n\n---\n\n## Orchestrator-steered cadence\n\nThe \u002Floop \u002F schedule that drives this skill is set up cloud-side via the `schedule` skill so it persists across user sessions. The orchestrator can re-cadence (e.g. to 12 hours for active development weeks, or pause via `gh workflow disable` + a coordination note) by editing the schedule out-of-band; the skill itself does not assume a particular cadence.\n\n---\n\n## File layout\n\nThe canonical file lives at `skills\u002Fmerge-to-main-and-release\u002FSKILL.md`. Each agent runtime (`.claude\u002F`, `.opencode\u002F`, `.codex\u002F`, `.gemini\u002F`, `.github\u002F`) accesses it via a symlink under its `skills\u002F` directory pointing at `..\u002F..\u002Fskills\u002Fmerge-to-main-and-release`, so there is exactly one source of truth. Editing the canonical file updates the skill for all runtimes simultaneously.\n","# Merge to Main and Release\n\nThis skill closes the loop between the work-loop (`\u002Fadvance-work-from-plan`) and downloadable releases. It promotes the current `linux-next` HEAD to `main` via PR, bumps the version, tags, and triggers the `workflow_dispatch`-only release workflow that publishes the Linux musl binaries (and the macOS \u002F Windows artifacts) on a GitHub Release.\n\nThe user's stated goal: keep a fresh Linux release available so they can install and smoke-test on a Fedora Silverblue host without needing to build locally. Daily cadence is the right balance — local CI catches regressions on every commit; this skill catches the \"did all of today's work cohere into a shippable artifact\" gate.\n\n---\n\n## 0 — Pre-flight\n\nEstablish identity and verify the environment is releasable:\n\n```bash\ndate -u +%Y-%m-%dT%H:%MZ\ntest \"$(git rev-parse --abbrev-ref HEAD)\" = linux-next   # assert, don't display\ngit fetch origin --prune\ngit pull --ff-only origin linux-next         # local linux-next == origin\u002Flinux-next\ntest -z \"$(git status --porcelain)\"          # assert clean, don't display\n```\n\n> Those two lines used to be bare `git rev-parse` \u002F `git status --short` with a\n> `# MUST be ...` comment beside them. A comment is not a gate: both commands\n> exit 0 on the wrong branch and on a dirty tree, printing the disagreeing\n> answer to a log nobody re-reads. Same family as the `null`-vs-empty traps\n> below — a step that CONSULTS an external fact instead of REQUIRING it.\n\nIf the worktree is dirty, stop and file\u002Fupdate a plan blocker. Do not stash\nrelease inputs or auto-artifact churn; local state is volatile and hidden stashes\nmake release evidence unrecoverable.\n\nAlso verify there are no local-only commits:\n\n```bash\ntest \"$(git rev-list --count origin\u002Flinux-next..HEAD)\" -eq 0\n```\n\nIf the branch is not `linux-next`, log + exit without escalating — the release flow only ships from the Linux integration branch.\n\n### Cloud minutes (order 1966-47vt)\n\nRead `cloud.usage` (MCP; on the host `tillandsias --cloud-usage`) and keep\nthe used\u002Fcap per provider for step 8. A metric saying\n`unavailable: permission-missing:\u003Cprovider>:\u003Cpermission>` is a grant to\nreport, never a zero.\n\n### Tray-parity completeness (release-scoped gate — order 243 semantic split)\n\nThe per-build litmus (`litmus:tray-parity-matrix-complete`) only gates each\nhost's OWN column so sibling verification debt cannot block unrelated local\nbuilds. The ALL-platform completeness requirement from\n`plan\u002Fissues\u002Ftray-feature-parity-matrix-2026-06-28.md` (\"the matrix should be\nall-green on `required` rows before that release\") lives HERE instead:\n\n```bash\n# 746-htj9: the sanctioned YAML path — ruby is NOT present in every\n# environment this runbook runs in (it broke the forge once already).\n. scripts\u002Fplan-binary-probe.sh\nPLAN=\"$(resolve_plan_binary)\" || { echo 'blocked: no runnable tillandsias-plan (run scripts\u002Fcycle-preflight.sh)'; exit 1; }\ngaps=\"$(\"$PLAN\" yaml-json openspec\u002Ftray-parity-matrix.yaml | jq -r '.features[] | select(.parity==\"required\") | . as $f | (\"linux\",\"macos\",\"windows\") | select($f[.] != \"done\") | \"\\(.): \\($f.capability) status=\\($f[.])\"')\"\nprintf '%s\\n' \"$gaps\" | grep -v '^$' || true\necho \"parity gaps: $(printf '%s' \"$gaps\" | grep -c . || true)\"\ntest -z \"$gaps\"\n```\n\nIf this exits non-zero, the parity matrix has unverified\u002Fincomplete `required`\ncells. This is an **advisory hold, operator-overridable**: record the gap list\nin the cycle outcome (§8) and in `plan\u002Floop_status.md`, and proceed ONLY if the\noperator has recorded a release-with-parity-gaps approval for the cycle (The\nTlatoāni owns that call — releases shipped with parity gaps before this gate\nexisted, so a hard stop would strand the daily Linux release on macOS\u002FWindows\nverification debt; make the debt loud, not invisible).\n\n---\n\n## 1 — Decide the promotion path, THEN (maybe) compute a version\n\n**Two paths exist, and choosing wrong discards evidence (952-mrsl, learned\nduring the v56.8.31.3 stable promotion):**\n\n- **PROMOTE-EXISTING** (the default when a blessed release already exists):\n  a daily release was already cut, its assets were verified by a blessing\n  round, and the ask is \"make it stable\". Do NOT cut a new version — a fresh\n  cut discards the exact artifacts the blessing verified. Skip this step's\n  computation entirely: merge linux-next → main (steps 2-3), SKIP the\n  VERSION bump (step 4) when main's merged VERSION already equals the\n  existing tag, skip tag\u002Fdispatch\u002Fwatch (steps 5-7), and instead flip the\n  existing release: `gh release edit vX.Y.Z.N --prerelease=false --latest`.\n  Verify `\u002Freleases\u002Flatest` resolves to it. Stable then serves byte-for-byte\n  what was blessed. Use `scripts\u002Fpromote-stable.sh \u003Ctag>`, which also moves\n  the `stable` tag; that push needs no `.\u002Fbuild.sh --check`, because a tag on\n  a commit already on origin\u002Fmain skips the gate stamp (1768-2k56).\n  **Promote only while the land queue is idle.** A promotion during a land\n  run half-promoted on 2026-10-17: the release flipped, the tag push did not.\n  The script now says `refused:stable-tag-push-refused:\u003Csha>` and prints the\n  push that finishes it.\n  **Reading a channel tag in a clone? Sync it first** (1271-ff72):\n  `git fetch origin '+refs\u002Ftags\u002Fstable:refs\u002Ftags\u002Fstable' '+refs\u002Ftags\u002Funstable:refs\u002Ftags\u002Funstable'`.\n  A plain `git fetch --tags` REFUSES to move these two (\"would clobber\n  existing tag\"), so a clone's `stable`\u002F`unstable` can sit days behind\n  origin's. The release tooling reads origin (`git ls-remote`) and is\n  unaffected; a human `git log stable` in a stale clone is not.\n- **CUT-NEW** (no existing blessed release, or the tree moved past it):\n  compute the next version and run every step below.\n- **FIX-FORWARD with zero code delta** (a published cut lost one platform's\n  artifacts to a runner or workflow defect, and the repair touches only\n  `.github\u002Fworkflows\u002F`, the README ledger and `plan\u002F`): this is CUT-NEW for\n  the version and the tag, but the §3 gate is COMPOSED rather than re-run.\n  Assert the premise, never assume it — from the tree the previous cut's\n  `--ci-full --install` gated (`\u003Cgated>`) to `origin\u002Flinux-next`, the diff\n  outside `plan\u002F`, `docs\u002F`, `skills\u002F`, `cheatsheets\u002F`, `README.md`,\n  `.github\u002Fworkflows\u002Frelease.yml` and the previous cut's own\n  `VERSION`\u002F`Cargo.lock`\u002F`crates\u002F*\u002FCargo.toml` bump MUST be empty; the delta\n  itself was gated by the land tool's `.\u002Fbuild.sh --check`; and\n  `scripts\u002Frelease-preflight.sh` is re-run fresh at merge time (it is cheap and\n  it is the only check that reads the NEW tag). Record the composition in the\n  PR body and the ledger row in those words. If the diff is not empty, it is a\n  CUT-NEW and the full gate runs. Measured on v56.9.19.2 (2026-09-19): the\n  Windows tray job of v56.9.19.1 died on a runner staging gap; re-running a\n  two-hour gate on a tree whose code bytes the gate had already passed would\n  have measured nothing new, and the assertion above is what makes that claim\n  checkable rather than convenient.\n\n**The version scheme is epoch-anchored CalVer** (operator ruling 2026-08-31):\n`\u003Cyears_since_epoch>.\u003Cmonth>.\u003Cday>.\u003Cbuild>` — e.g. `56.8.31.3` for the third\nbuild of 2026-08-31. `scripts\u002Fbump-version.sh` is the scheme's executable\ndefinition (`openspec\u002Fspecs\u002Fversioning\u002Fspec.md` is the spec); NEVER re-derive\nthe format here. This section previously computed `MAJOR.MINOR.YYMMDD.N` by\nhand — under the epoch scheme that hand-derivation produces the malformed\nhybrid `v56.8.260901.1`, whose six-digit third field is the exact Store-cap\nviolation the cutover retired. Delegate:\n\n```bash\n# CUT-NEW path only. Same UTC day → build+1; new UTC day → \u003Cy>.\u003Cm>.\u003Cd>.1.\n# The bump lands via a release\u002Fversion-bump-* branch (step 4), never here.\n# NOTE the layout-preserving scratch: bump-version.sh anchors VERSION to its\n# OWN script path (SCRIPT_DIR\u002F..), not to $PWD — copying VERSION alone into a\n# scratch dir and cd-ing there would bump the REAL checkout's file.\nnew_version=\"$(\n  tmp=$(mktemp -d) && mkdir -p \"$tmp\u002Fscripts\" \\\n  && cp VERSION \"$tmp\u002FVERSION\" && cp scripts\u002Fbump-version.sh \"$tmp\u002Fscripts\u002F\" \\\n  && bash \"$tmp\u002Fscripts\u002Fbump-version.sh\" --bump-build >\u002Fdev\u002Fnull \\\n  && tr -d '[:space:]' \u003C \"$tmp\u002FVERSION\" && rm -rf \"$tmp\"\n)\"\nnew_tag=\"v${new_version}\"\necho \"Computed: $new_tag (dry-run bump in a scratch layout; the real bump lands in step 4)\"\n```\n\nSanity: the third field is a day-of-month (1-31), never six digits; the first\nfield is ≥56 and ≤65535.\n\n---\n\n## 2 — Open or update the PR\n\nGitHub has at most one open PR `linux-next → main` at a time. Reuse it if present; open a new one otherwise.\n\n```bash\nif pr=\"$(scripts\u002Fresolve-open-pr.sh main linux-next)\"; then\n    existing_pr=\"${pr#ok:open-pr:}\"\nelse\n    case \"$pr\" in\n        blocked:open-pr:gh-failed:*) echo \"$pr\"; exit 1 ;;   # retryable, NOT \"no PR\"\n    esac\n    gh pr create --base main --head linux-next \\\n        --title \"release: ${new_tag} — daily linux-next promotion\" \\\n        --body \"Automated daily promotion of linux-next → main by the \\`merge-to-main-and-release\\` skill. The follow-on tag + workflow_dispatch trigger publishes ${new_tag} for Linux Silverblue smoke-test.\"\n    pr=\"$(scripts\u002Fresolve-open-pr.sh main linux-next)\" || { echo \"$pr\"; exit 1; }\n    existing_pr=\"${pr#ok:open-pr:}\"\nfi\necho \"PR #${existing_pr}\"\n```\n\n> The trap this replaces — the THIRD instance of one class in this file, after\n> step 3's `gh pr checks --watch` note and step 7's. The step used to be:\n>\n> ```bash\n> existing_pr=$(gh pr list --base main --head linux-next --state open --json number --jq '.[0].number')\n> if [[ -z \"$existing_pr\" ]]; then ...\n> ```\n>\n> `--jq '.[0].number'` on an EMPTY array prints the literal `null`, not an empty\n> string. With no open PR the emptiness test was FALSE, so `gh pr create` never\n> ran and the runbook announced `PR #null` and walked into step 3 to merge a\n> pull request that does not exist. The re-query after creation had the same\n> shape, so a creation that silently failed produced `PR #null` too.\n>\n> `scripts\u002Fresolve-open-pr.sh` asserts a positive fact and, like its sibling\n> `resolve-release-run.sh`, keeps \"gh failed\" distinct from \"gh has nothing to\n> say\" — here that distinction decides between retrying and creating a PR.\n\nUpdate the PR body with today's `${new_tag}` even if reusing — the human reviewer should see which version is being shipped.\n\n---\n\n## 3 — Gate LOCALLY, then merge\n\n> **This step changed on 2026-08-03 and the change is silent, so read it.**\n> Push CI was removed (operator directive: only the release may spend cloud\n> minutes). `main`'s three required status contexts were cleared, because they\n> named jobs that no longer exist and made every PR permanently unmergeable.\n>\n> The trap: `gh pr checks --watch` does NOT fail when there are no checks. It\n> prints `no checks reported on the '\u003Cbranch>' branch` and **exits 0**,\n> immediately. So the old text — \"ALL required checks pass → proceed to merge\" —\n> now means \"merge with zero verification\" while reading exactly like a gate that\n> passed. That is a false green, which is worse than a hard break.\n>\n> The verification did not disappear; it moved to this host. Run it here.\n\n**Announce a cut freeze before running the gate.** From the moment\n`.\u002Fbuild.sh --ci-full` starts until the step-5 back-merge is pushed, any CODE\nlanding on `linux-next` — the branch this gate verifies — moves the head past\nthe commit this gate verified and forces a full re-gate. State the window\n(start time, expected end at the back-merge push) so other hosts hold code\nlandings until it lifts (drill: plan\u002Fissues\u002Ffleet-restart-2026-09-12.md, Cut freeze versus gate freeze).\n\n- Platform-branch pushes (`osx-next`, `windows-next`) move nothing the gate\n  reads and are not frozen; `plan\u002F`, `docs\u002F` and `skills\u002F` pushes through the\n  plan-only lane are exempt — but exempt means allowed, not free: a plan-only\n  closure push during a relay gate cost one host a re-integrate and a second\n  8-minute gate. The question before any push in the window is \"is anyone\n  mid-gate\", not \"am I exempt\" (drill: plan\u002Fissues\u002Ffleet-restart-2026-09-12.md, An exemption describes what is allowed, not what is free).\n- The freeze is enforced by nobody: the pre-push hook checks the trunk merge\n  and the gate stamp, never whether a freeze is live, so a push that starts\n  before the announced window and lands during it goes through clean\n  regardless (yolanda, 2026-09-14; drill: plan\u002Fissues\u002Ffleet-restart-2026-09-12.md, yolanda reported a freeze breach that was not one, and found a real gap doing it).\n\n**Before the gate, if this host rebaked its images today** (any `--init`\nafter a version change, the post-restart checklist), warm podman's ID-mapped\nlayer copy of the forge image once — `podman run --rm --userns=keep-id\nlocalhost\u002Ftillandsias-forge:\u003Cinstalled VERSION> true` (exit 2 from the\nentrypoint's credential refusal is expected; the copy is what you want). The\nfull pre-build litmus runs four forge-runtime fixtures under `--userns=keep-id`\nand each one's budget expires while podman copies a 3.1 GB image; the new\nkill-time adjudicator reads them as NOT contended, genuinely too slow, which\nis true and misleading. Measured on the v56.9.2.1 cut: four reds, all gone\nafter one warm (plan\u002Fissues\u002Fenhancement-keep-id-layer-copy-cold-after-image-\nrebake-2026-09-02.md). Also clear `target-guest\u002F` if it was staged by an\nearlier `--install` run under a bumped VERSION — `build-guest-binaries.sh\n--verify` reads that as an integrity mismatch, not stale staging.\n\n`gh` resolves the repository from the working directory: run every `gh`\ncommand from the checkout (or pass `-R \u003Cowner>\u002F\u003Crepo>`), never from a scratch\ndirectory — `gh release download` from elsewhere fails with \"not a git\nrepository\" and looks like a missing asset.\n\n```bash\n# DECLARE THE FREEZE FIRST (order 1176-9vqn). Until this existed the freeze was\n# announced in ledger prose and enforced by nothing: MEASURED on yolanda during\n# the v56.9.13.1 cut, a host merged trunk, gated for 2476 s and pushed, and\n# every pre-push check passed because none of them was about a freeze. It was\n# harmless only because the push went to windows-next; aimed at linux-next it\n# would have put code into the frozen branch with every check green.\n#\n# THE WINDOW IS WHY A DISCIPLINE RULE CANNOT COVER THIS: the gate below runs\n# 41 minutes on yolanda and longer on the floor, so a freeze declared inside\n# that window is invisible to a land that checked before it started, and the\n# land pushes on completion without re-asking. The marker lives on origin and\n# the hook consults it AT the push.\n#\n# Plan-only pushes stay admitted by design — the plan lane is how coordination\n# keeps moving during a cut, and every host used it under the last freeze.\nscripts\u002Frelease-freeze.sh set \"${release_source_branch:-linux-next}\" \"cut ${new_tag}\"\n# -> ok:freeze-set:refs\u002Ftillandsias\u002Ffreeze\u002F\u003Cbranch>\u002F\u003Chost>\u002F\u003Cepoch>\n# A `refused:freeze:unreachable:*` here is a STOP: an unreachable origin means\n# the cut cannot publish either. Do not proceed with an undeclared freeze.\n#\n# LANDING A FIX TO THE CUT WHILE FROZEN (order 1265-8qr6). The hook refuses\n# the cut host's own code push like anyone's. Do NOT clear for it: a clear\n# leaves origin with no marker for the whole land, gate included, and every\n# host reads ok:freeze-none. Hand the freeze over instead:\n#   scripts\u002Frelease-freeze.sh land \"${release_source_branch:-linux-next}\"\n#   # -> ok:freeze-landing:refs\u002Ftillandsias\u002Ffreeze-landing\u002F\u003Cbranch>\u002F\u003Chost>\u002F\u003Cepoch>\n#   #    (written BEFORE the freeze marker goes; status reads\n#   #    frozen-landing:\u003Cbranch>:by=\u003Chost>)\n#   ... land the fix ...\n#   scripts\u002Frelease-freeze.sh set \"${release_source_branch:-linux-next}\"\n#   # the re-freeze writes the freeze marker BEFORE the landing marker goes\n# The land queue runs only on the cut host, and the cut host decides when to\n# start it, so hold the queue during a land: while the landing marker is live,\n# land the cut's fix alone and start no queue run until the re-freeze. The\n# queue cannot tell the cut's fix from anyone else's candidate (identity at\n# push time is self-declared), so this is procedure, not a check.\n# For every other host, frozen-landing means HOLD code exactly as frozen:\n# only ok:freeze-none is clear. There is no cut-host exemption in the hook;\n# host identity at push time is self-declared, so the freeze is a\n# coordination signal, not an authentication boundary.\n\n# THE GATE. This is the whole safety net now — there is nothing server-side.\n.\u002Fbuild.sh --ci-full || { echo \"gate failed — do NOT merge\"; exit 1; }\nscripts\u002Frelease-preflight.sh || { echo \"preflight refused — do NOT merge\"; exit 1; }\n```\n\n**The gate INSTALLS (1122-6sqz).** `--ci-full` ends with \"continuing to\ninstall\" and puts the tree's launcher on the host that ran it, under\n`TILLANDSIAS_SKIP_VERSION_BUMP=1` too — that flag skips the bump, nothing skips\nthe install. On a coordinator desktop that replaces the operator's local build\nwith the tree's VERSION label: measured 2026-09-11 on macuahuitl, v56.9.11.3\nbecame v56.9.5.1 (HEAD code, older label). Operator ruling 2026-09-12: the\nlabel is a monotonic counter and simply rolls forward on the next local build,\n\"CRDT style\" — so note `tillandsias --version` before the gate, expect it to\nchange, and do not re-install for the coordinator under SKIP_VERSION_BUMP.\n\n`release-preflight.sh` carries the two checks that used to live inside\n`release.yml` (VERSION monotonicity, retired CLI flags) plus the plan-ledger and\nactions-budget gates. It prints one line: `ok:release-preflight` or\n`blocked:\u003Creason>`.\n\nOnly once both are green:\n\n```bash\ngh pr merge ${existing_pr} --merge            # merge commit — preserves linux-next history\n```\n\nNote `--auto` is dropped: auto-merge waits for checks that will never report, so\nthe PR would simply sit there. Merge directly, having gated locally.\n\nIf either gate fails: surface the failure, do NOT merge, do NOT tag, write a\nledger entry. The next cycle retries.\n\nUse `--merge` (not `--squash`): the linux-next history is the audit log of the daily work-loop and integration cron cycles. Preserve it.\n\n---\n\n**Cloud budget refusal (order 1966-47vt).** Before merging, re-read\n`cloud.usage`: when Cloudflare `workers_builds_minutes` used is ≥ 2,700 of\n3,000 for the month, do NOT merge to main unless the fix is urgent; say so in\nthe cycle outcome.\n\n## 4 — Bump VERSION on main via PR (branch protection, order 476)\n\n`main` has server-side branch protection (enabled 2026-07-28, order 476\nprong (a)): direct pushes are rejected for EVERYONE including admins\n(`enforce_admins: true`), so the VERSION bump lands through a short-lived PR.\n\n**Corrected 2026-08-03 (order 598-h774).** This section used to say \"PRs to main\nrequire the three CI checks but ZERO approvals\" and \"ci.yml has no path filters,\nso a VERSION-only PR still gets its checks\". Both are now false: `ci.yml` is\ndeleted, and `main`'s `required_status_checks.contexts` is `[]`. What remains is\nthe PR requirement and `enforce_admins: true` — no server-side verification of\ncontent at all. Step 3's local gate is what stands in for it, and it already ran\nagainst this tree. **But the bump branch still needs its own `.\u002Fbuild.sh\n--check` before the push** (measured on the v56.9.2.1 cut, 2026-09-02): the\npre-push hook compares the gate STAMP against the tree bytes, and the bump\nchanges VERSION — so the push is refused with\n\"the tree changed since .\u002Fbuild.sh --check last passed\". Run\n`TILLANDSIAS_CLASS_SELECTOR=off TILLANDSIAS_SKIP_VERSION_BUMP=1 .\u002Fbuild.sh --check`\non the bump branch. The selector must be OFF here: since 765-xpct a plain\n`--check` writes a SCOPED stamp (classes computed against linux-next), and\nthe hook cannot honour a scoped stamp on a brand-new branch with no local\nbase (\"the gate stamp is scoped to 'other,rust' but refs\u002Fheads\u002Frelease\u002F…\nhas no usable local base\"). Measured on the v56.9.23.1 cut, one wasted gate.\nThe same applies to the step-5 back-merge gate. Then\n`git add Cargo.lock` and `--amend` it into the bump commit if the check\ntouched it (still VERSION + Cargo only, 702-eusw), then push.\n**Corrected 2026-10-17 (order 1768-2k56):** the bump changes VERSION ONLY;\nthe crates' `Cargo.toml` versions do not move (measured on the v56.10.16.2\ncut). The `git add` below still names them so a future bump that does touch\nthem is not silently left out.\n\n```bash\ngit checkout main\ngit pull --ff-only origin main\nbump_branch=\"release\u002Fversion-bump-${new_version}\"\ngit checkout -b \"${bump_branch}\"\necho \"${new_version}\" > VERSION\n# THE BUMP COMMIT CARRIES VERSION + CARGO FILES ONLY (702-eusw, hit live\n# 2026-08-31): the pre-push guard refuses any commit that sweeps a VERSION\n# bump in with unrelated files. Run scripts\u002Fbump-version.sh to sync the\n# Cargo.toml semver, then add ONLY those files — never `git add -A` here\n# (a stray loop-status fragment once rode along and the push was refused).\nscripts\u002Fbump-version.sh >\u002Fdev\u002Fnull\ngit add VERSION Cargo.lock crates\u002F*\u002FCargo.toml 2>\u002Fdev\u002Fnull || git add VERSION\ngit commit -m \"release: bump VERSION to ${new_version}\n\nThe merge-to-main-and-release skill bumped VERSION as part of the\ndaily linux-next → main promotion (via PR: main is branch-protected,\norder 476). Tag ${new_tag} follows.\"\ngit push origin \"${bump_branch}\"\ngh pr create --base main --head \"${bump_branch}\" \\\n    --title \"release: bump VERSION to ${new_version}\" \\\n    --body \"Automated VERSION bump for ${new_tag} by the merge-to-main-and-release skill. Auto-merges when CI is green (0 approvals required by branch protection).\"\n# No --auto and no --watch: with zero required contexts, auto-merge waits for a\n# check that will never report and the PR sits open forever, while `checks\n# --watch` exits 0 on \"no checks reported\" and reads like a gate that passed.\n# Step 3 already gated this tree locally; merge directly.\ngh pr merge \"${bump_branch}\" --merge\ngit checkout main\ngit pull origin main                           # now contains the bump merge\ngit push origin --delete \"${bump_branch}\" 2>\u002Fdev\u002Fnull || true\ngit branch -D \"${bump_branch}\"\n\n# RETURN THE CHECKOUT. Non-negotiable, and the reason is not tidiness.\n#\n# The forge launcher seeds a new forge's branch from the HOST CHECKOUT'S CURRENT\n# BRANCH (read_host_project_current_branch, crates\u002Ftillandsias-headless\u002Fsrc\u002F\n# main.rs:2701 -> TILLANDSIAS_FORGE_SEED_BRANCH), and the guest converges to it\n# (images\u002Fdefault\u002Flib-common.sh:565 checkout_forge_seed_branch). So leaving this\n# checkout parked on `main` silently pins EVERY forge launched afterwards to\n# `main`.\n#\n# That is not cosmetic. `main` does not carry crates\u002Ftillandsias-plan\u002Fsrc\u002F\n# answer.rs or methodology.rs, so a forge seeded from it builds a PRE-EXPERT\n# binary; ensure_forge_experts then truthfully reports `experts: ready` while\n# every plan_answer \u002F methodology_path call returns confidence=unsupported.\n# Order 531 is exactly this, and it made milestone 391 ungradeable from inside a\n# forge. Two prior cycles absorbed it by noticing and hand-rebasing\n# (plan\u002Floop_status.md:203, :243).\ngit checkout \"${release_source_branch:-linux-next}\"\ngit pull --ff-only origin \"${release_source_branch:-linux-next}\" || true\n```\n\nCapture `release_source_branch=\"$(git symbolic-ref --short HEAD)\"` at the START\nof the release flow, before the first `git checkout main`, so this returns to\nwherever the operator actually was rather than assuming `linux-next`.\n\nVerify before considering the release complete:\n\n```bash\ngit symbolic-ref --short HEAD   # MUST NOT be `main`\n```\n\nA release that ends with the checkout on `main` is not finished, however green\nthe tag is: it has armed the next forge launch to build an expert-less binary\nthat reports itself ready.\n\nConfirm `cat VERSION` on main equals `${new_version}` before tagging. If\nthe bump PR's checks fail, surface the run URL and stop — do not tag a\nmain whose VERSION was not bumped. If main advanced concurrently (another\nrelease ran), re-run from the `git pull --ff-only` with a fresh branch; if\nstill conflicting after 3 attempts, write an `ESCALATION:` line in\n`plan\u002Fissues\u002Fmulti-host-integration-loop-2026-05-24.md` and stop.\n\n---\n\n## 5 — Tag, THEN back-merge, THEN push both (order 898-zhf3)\n\n> **THIS STEP USED TO SAY \"push the tag, then back-merge, never before\". THAT\n> ORDER IS UNEXECUTABLE and it had almost certainly never been run on a host\n> with the pre-push hook installed.** Measured on macuahuitl 2026-08-26 while\n> cutting v0.4.260826.1: `git push origin \"${new_tag}\"` was REFUSED —\n>\n> ```\n> ✗ pre-push refused: release preflight says blocked:version-not-monotonic\n>   ERROR: Version 0.4.260817.1 is LESS than latest release v0.4.260826.1\n>   REMEDY: git fetch origin && git merge origin\u002Fmain\n> ```\n>\n> **The guard is correct.** The moment `git tag` creates the tag LOCALLY,\n> `verify-version-monotonic.sh` resolves it as \"latest release\" and compares it\n> against the branch's VERSION — which is still the pre-release value, because\n> the back-merge is the very thing the old text deferred until after this push.\n> So: tag push requires the back-merge; the back-merge was prescribed after the\n> tag push. Circular, on every host that has run `scripts\u002Finstall-hooks.sh`.\n>\n> The pressure at that moment is toward `git push --no-verify`, which is the one\n> exit the hook's own text warns against — and it would push the release tag\n> past the only remaining gate.\n\n**Create the tag. Do NOT push it yet.**\n\n```bash\ngit tag -a \"${new_tag}\" -m \"Release ${new_version}\n\nDaily linux-next → main promotion via the merge-to-main-and-release\nskill. See PR #${existing_pr} for the merged work range.\n\"\n```\n\nThe annotated tag carries the PR reference so the GitHub Release page links back to the merged work.\n\n**WHY DEFERRING THE PUSH IS SAFE, which is the concern the old ordering was\nprotecting and which survives intact.** The old text's stated reason was \"the\ntag must keep pointing at main's bump-merge commit\". It does: the tag is created\nagainst main's bump-merge SHA and a back-merge into `linux-next` cannot move a\ntag that points at a commit on `main`. **Verified, not assumed** — during the\nv0.4.260826.1 cut `git rev-list -n1 v0.4.260826.1` returned `341ab0010` both\nbefore and after the back-merge.\n\n### The back-merge is part of the cut, not a tidy-up (order 800-vk2p)\n\n```bash\n# The moment ${new_tag} EXISTS — locally is enough — release-preflight gate 1\n# refuses blocked:version-not-monotonic on every branch still carrying the\n# pre-release VERSION. At v0.4.260817.1 that was all three platform branches,\n# fleet-wide, within seconds — including this release's OWN ledger records, so\n# step 8 cannot push until this lands. Precedents: f6424070d, 97cd7068b.\n#\n# It also refuses THIS host's tag push, which is why the back-merge comes first\n# (898-zhf3). \"Locally is enough\" is the correction: the old text said \"is\n# pushed\", and the guard resolves the tag from the local ref.\ntest \"$(git symbolic-ref --short HEAD)\" = \"${release_source_branch:-linux-next}\"\ngit fetch origin\ngit merge origin\u002Fmain          # back-merge the VERSION bump; merge, never rebase\nTILLANDSIAS_CLASS_SELECTOR=off TILLANDSIAS_SKIP_VERSION_BUMP=1 .\u002Fbuild.sh --check\n                               # the merge changed VERSION, so the gate stamp is\n                               # stale and the pushes below would be refused.\n                               # MEASURED 2026-10-17 (1768-2k56): this is a ~3 s\n                               # memo hit, not a full gate; the bytes it covers\n                               # already passed step 3's gate\n\n# CLEAR THE FREEZE BEFORE THE BACK-MERGE PUSH, and note WHY it is here rather\n# than at the end of the cut (order 1176-9vqn): the back-merge push below is\n# itself a CODE push to the frozen branch, carrying the VERSION bump merge. A\n# freeze still live at this line would refuse the cut's own push — correctly,\n# which is the point: the mechanism does not know you are the coordinator. So\n# the freeze's lifetime runs from the gate start to exactly here, which is the\n# declared policy (hold code lands on the release branch from the gate start to\n# the stage-2 back-merge push) expressed as a lifetime rather than a promise.\nscripts\u002Frelease-freeze.sh clear \"${release_source_branch:-linux-next}\"\n# -> ok:freeze-cleared:\u003Cn>; clearing removes every marker for the branch\n#    (freeze and landing markers alike), whoever set it, so a coordinator can always clear a freeze another host\n#    declared. `ok:freeze-cleared:0` means it was already clear — not an error.\n\n# NOW both pushes succeed. Order between them does not matter; both are gated\n# on the back-merge above, not on each other. The tag push is unaffected by a\n# freeze either way: the check only looks at refs\u002Fheads\u002F*. A tag on a commit\n# already on origin\u002Fmain skips the gate stamp (1768-2k56); release-preflight and\n# the version guard still run on it, which is the 898-zhf3 ordering above.\ngit push origin \"${new_tag}\"\ngit push origin \"${release_source_branch:-linux-next}\"\n```\n\n**VERIFY THE TAG DID NOT MOVE, rather than assuming it.** The whole reason the\nold ordering existed was to keep the tag on main's bump-merge commit:\n\n```bash\ngit rev-list -n1 \"${new_tag}\"   # MUST equal main's bump-merge SHA\n```\n\nA back-merge into the source branch cannot move a tag pointing at a commit on\n`main` — but check it, because this is the property the reordering trades on.\n\n`windows-next` and `osx-next` then self-heal on their next mandated\n`git merge origin\u002Flinux-next` (methodology `pull_merge_cadence.pre_push_gate`),\nso this one push unblocks the fleet. No `--no-verify` is ever needed —\n`pre-push-version-guard.sh` already classifies a VERSION equal to\n`origin\u002Fmain`'s as sync-forward.\n\n---\n\n## 6 — Trigger the release workflow\n\n`release.yml` is `workflow_dispatch`-only. The skill explicitly triggers it, never via `on: push: tags:` (the workflow is intentionally manual to keep release authorship traceable).\n\n**One dispatch, ON MAIN, with the version as input (order 1953-jmvb).**\nOperator ruling 2026-11-01: \"let's drop the compiler build, and start the\nrelease from main\". The run's ref is `refs\u002Fheads\u002Fmain`, so the Nix and\ncargo caches it saves are restored by the next release inside the ONE daily\ncloud build — from a tag ref they never were (1950-b4f3: five releases, zero\nrestores, 66-min seed run cancelled by the operator). The workflow refuses\nany other ref, reads VERSION from main's tree, and creates `v\u003CVERSION>` at\nits own SHA or verifies the tag step 5 pushed points there. The Sigstore SAN\nis `release.yml@refs\u002Fheads\u002Fmain`. litmus:release-is-one-cloud-build refuses\na dispatch on any other ref or a seed input. Main's HEAD must be the\nbump-merge SHA the tag names (step 5 asserts that) — do not push anything\nto main between step 5 and this dispatch.\n\n```bash\ngh workflow run release.yml --ref main -f version=\"${new_version}\"\nsleep 5\ngh run list --workflow=release.yml --branch=main --limit 1\n```\n\n(The old `recipe-publish.yml` custom-rootfs workflow was removed in the 2026-06 Fedora pivot — Windows\u002FmacOS now fetch official Fedora WSL\u002FCloud images directly, so there is no rootfs CI to coordinate with anymore.)\n\n---\n\n## 7 — Wait for the release build + surface artifacts\n\n```bash\nrun_id=\"$(scripts\u002Fresolve-release-run.sh \"${new_tag}\")\" || { echo \"$run_id\"; exit 1; }\nrun_id=\"${run_id#ok:release-run:}\"\ngh run watch \"${run_id}\"                     # blocks until green or red\nassets=\"$(gh release view \"${new_tag}\" --json assets --jq '.assets[].name')\"\n```\n\n**Assert, don't display.** v56.9.11.1 (2026-09-11) published Linux and macOS\nassets while the Windows tray job FAILED (1122-xi2f); `gh release view`\nprinted a healthy-looking release and the gap was found a day later by a\nWindows host spending an assignment slot on it. A platform job can fail\nwhile the run still publishes the others' assets. So list the asset names\nand REQUIRE all three sets — failing this step loudly and naming the missing\nplatform — rather than just printing whatever exists. The only sanctioned\nabsence is the unsigned MSIX (see Hard guardrails), never a platform's\nprimary asset (drill: plan\u002Fissues\u002Ffleet-restart-2026-09-12.md, v56.9.11.1 was cut 2026-09-11). Asset names measured on v56.9.13.1\n(drill: plan\u002Fissues\u002Ffleet-restart-2026-09-12.md, v56.9.13.1 PUBLISHED):\n\n```bash\nfor a in tillandsias-linux-x86_64 SHA256SUMS \\\n         \"tillandsias-tray-${new_version}-windows-x64.zip\" tillandsias-tray.exe SHA256SUMS-windows \\\n         \"tillandsias-tray-${new_version}-macos-arm64.tar.gz\" Tillandsias.dmg SHA256SUMS-macos; do\n  printf '%s\\n' \"$assets\" | grep -qxF \"$a\" || { echo \"blocked:release-asset-missing:$a\"; exit 1; }\ndone\necho \"ok:release-assets:all-three-sets\"\ngh release view \"${new_tag}\" --json url --jq '.url'\n```\n\nThe MSIX is never asserted here — Step 7's asset check expects the HONEST\nWITHHOLD on unsigned builds (see Hard guardrails), not a flagged-missing\nplatform asset.\n\n> The trap this replaces, and it is the same family as the `gh pr checks\n> --watch` note in step 5. The step used to be a bare command substitution:\n>\n> ```bash\n> run_id=$(gh run list --workflow=release.yml --branch=\"${new_tag}\" --limit 1 --json databaseId --jq '.[0].databaseId')\n> ```\n>\n> When step 6's dispatch did not actually start a run — wrong ref, a\n> `workflow_dispatch` that silently no-oped, a tag that never reached the\n> remote — `gh run list` prints nothing and exits **0**. The substitution\n> succeeded with an EMPTY `run_id` and the runbook walked into `gh run watch \"\"`\n> having never noticed the thing it was waiting for did not exist. On an empty\n> result set `--jq '.[0].databaseId'` prints the literal `null`, so a bare\n> emptiness test is not enough either.\n>\n> `scripts\u002Fresolve-release-run.sh` asserts a positive fact and separates \"gh\n> failed\" from \"gh had nothing to say\" — they are different faults.\n> Pinned by `scripts\u002Ftest-resolve-release-run.sh` (6\u002F6, fake `gh`, no network).\n> Found by the 601-462g audit, in this runbook.\n\nThe Linux Silverblue smoke-test artifact is the `tillandsias-linux-x86_64` musl binary. Surface its URL to the user.\n\n---\n\n## 8 — Record the cycle outcome\n\n**The ledger row is on origin BEFORE any smoke go is sent.** The curl-install\nsmoke's §0.2b reads the row for the tag under test and files `NO LEDGER ROW`\nas a finding when it is absent — correctly, and it did so twice on\n2026-09-19 (macneo, pirria) because the coordinator sent both gos while the\nv56.9.19.1 row was still waiting on a refused land (the row had taken the\ntable past the distillation threshold). A smoke started before the row lands\nproduces a finding that is right at run time and stale by the time it is\nread; the cheaper order is row first, gos second, and a go message names the\nrow's landed SHA.\n\nAppend a one-line entry to `plan\u002Fissues\u002Flinux-next-work-queue-2026-05-25.md`:\n\n```\n- \u003CUTC>  `\u003Cmerge_sha>`  Release ${new_tag} — merged PR #${existing_pr} to main, tagged, workflow_dispatch triggered. Linux build: \u003Cnix_build_duration> (Nix cache: \u003Chit|miss>), total run: \u003Ctotal_run_time>. Linux artifact: \u003Cbrowser_download_url>.\n```\n\nAdd the `cloud.usage` reading to that entry: `cloud: cloudflare \u003Cused>\u002F3000 min, github cache \u003Cbytes>\u002F10GiB` (order 1966-47vt).\n\n**Also record the release in the README ledger** (operator directive\n2026-07-16; order 380) — **APPEND for a new tag, EDIT for a tag that already\nhas a row.** Which one you are doing is decided by the TAG, not by which step\nyou are in, so ask before writing:\n\n```bash\ngrep -cE \"^\\| ${new_tag}( |\\()\" README.md   # 0 -> append  |  1 -> EDIT that row  |  >1 -> a duplicate already exists\n```\n\n**WHY THIS BRANCH EXISTS, and it is not hypothetical.** This step used to say\nonly \"append a row\". PROMOTE-EXISTING (step 1) promotes a tag that was cut\nearlier, so **its row exists by construction — the cut wrote it** — and an\nunconditional append puts a second row on the same release. That is exactly how\n`v0.4.260826.1` and `v0.4.260817.1` each acquired two rows, and the pairs were\nnot copies: the second of each was a later, richer rewrite, so resolving them\nmeant deleting prose a human had written. They were only cleared by being\nfolded into a DISTILLED span. Order 380 carried this as its last open item.\n\nMEASURED 2026-09-16: promoting `v56.9.13.1` to stable, the correct action was to\nEDIT its existing cut row — adding that it was promoted, by flag flip rather\nthan a fresh cut, and naming the defects shipping with it. Appending would have\nproduced the third instance of this defect.\n\n- **A CUT appends** one new row at the TOP of the table body.\n- **A PROMOTION EDITS THE EXISTING ROW** in place: change the `RELEASE` cell to\n  carry `(**STABLE**)`, say it was promoted and on what date, and — because a\n  promotion ships whatever the cut shipped — name any known defect that goes\n  stable with it. Do not restate the cut's feature list; it is already there.\n\nFor an append, the new row's cells are:\n\n- `RELEASE`: `${new_tag} (daily)` — or `(**STABLE**)` when this release is a\n  stable-channel promotion.\n- `INTENDED FEATURES`: the planned work this release set out to ship\n  (orders\u002Ffeatures from the merged range), stated whether or not complete —\n  incomplete intentions stay listed with their follow-on order.\n- `BUGFIXES`: unplanned maintenance — regressions from previous milestones or\n  breakage discovered during the day's development.\n\nKeep rows honest and compact (the evidence trail lives in `plan\u002F`); when the\ntable exceeds ~10 rows, distill the oldest rows into the `*Older releases*`\nline per the semantic-distillation policy.\n\n**You are TOLD when that threshold is crossed — you no longer have to notice it\nhere** (order 914-nkc4). `scripts\u002Frelease-preflight.sh` runs\n`scripts\u002Fcheck-ledger-distillation.sh`, which reports\n`due:ledger-distillation:\u003Cn>-rows:threshold=\u003Ct>` and names the destination line.\nIt is ADVISORY and cannot refuse a cut: an over-long table costs a long README,\nnever a false verdict. The threshold lives in that script rather than in this\nsentence, so the number cannot drift between the two. This paragraph used to be\nthe whole mechanism, and the table reached 19 rows — nearly double — with no row\never distilled, because a sentence in a runbook step is something a reader is\nexpected to notice while doing something else.\n\n**Append the release's observatorium snapshot** (order 1661-vckd). The\ndashboard at observatorium.tillandsias.org plots one durable observation per\nrelease, and its durable storage is THIS repository: Cloudflare's deploy only\nreads `observatorium\u002Fhistory\u002Fsnapshots.jsonl`, it never writes it (it has no\npush credential, and must not). So the cut appends exactly one line, naming\nthe tag, in the same ledger push.\n\nThis line is the dashboard's HEADLINE CentiColon grade. Workers Builds runs no\nlitmus, so a deploy can trace a requirement but never positively test one; only\na host with recorded litmus results can. So the cut records a pre-build litmus\nrun on THIS tree first, then grades. `collect.mjs` refuses to append a\nrelease snapshot whose grade is unmeasured or graded over no recorded results:\n\n```bash\nscripts\u002Frun-litmus-test.sh --phase pre-build --compact   # records per-test results for this tree's bytes\n(cd observatorium && node scripts\u002Fbuild.mjs --release-snapshot \"${new_tag}\")\ntail -1 observatorium\u002Fhistory\u002Fsnapshots.jsonl | grep -q \"\\\"release\\\":\\\"${new_tag}\\\"\" \\\n  || { echo \"observatorium snapshot does not name ${new_tag}\"; exit 1; }\ngit add observatorium\u002Fhistory\u002Fsnapshots.jsonl\n```\n\nIt builds `tillandsias-plan` from this checkout (or uses `TILLANDSIAS_PLAN_BIN`)\nto fold the plan and grade CentiColons; a metric it cannot observe is recorded\nas an explicit gap in the line, never as a zero. A failing litmus test does not\nblock the snapshot: it un-earns its requirements, and the grade says so.\n\nPush the ledger update to `linux-next` so other hosts and the next work-loop see the release happened.\n\nBefore success, confirm the release ledger update was pushed and the local\nbranch is not ahead of upstream.\n\n---\n\n## Hard guardrails\n\n- **NEVER push directly to `main`** — always via PR. Since 2026-07-28 this\n  is server-enforced (order 476: branch protection with `enforce_admins`,\n  PR required, 0 approvals, and since 2026-08-03 zero required status\n  contexts), so a direct push fails loudly — including\n  the VERSION bump, which is why step 4 uses a PR.\n- **NEVER `git push --force`** — main is protected (force-pushes and\n  deletions rejected server-side).\n- **NEVER skip the workflow_dispatch step**: the release workflow is manual-only by design. If the user wants automatic-on-tag, they edit release.yml first.\n- **NEVER bump VERSION on linux-next**; only on main as part of the release commit. Sibling hosts (osx-next \u002F windows-next) consume VERSION from their respective merge points; bumping it on linux-next desyncs them.\n- The release ships three platform artifact SETS to ONE GitHub release with matching versions:\n  Linux musl (`release` job), macOS arm64 thin tray + DMG (`macos-release`), Windows x64 thin\n  tray zip (`windows-release`). The macOS\u002FWindows jobs `needs: release` and\n  upload via `--clobber`. The MSIX is built but **an UNSIGNED MSIX is\n  deliberately WITHHELD from release assets** with its checksum line dropped\n  (2026-08-31: unsigned MSIX is inert for sideload — Add-AppxPackage\n  0x800B0100 — so shipping it only produces uninstallable downloads; the\n  withhold guard runs whenever the signing account is absent). Step 7's asset\n  check should therefore expect the HONEST WITHHOLD on unsigned builds, not\n  flag the MSIX missing. The Store channel is unaffected: Microsoft re-signs\n  submitted MSIX at $0 (776-g6r3), and Store submission uses a locally built\n  manifest-legal package, not the release asset.\n- **NEVER cancel an in-flight release** — let it complete or fail, then handle in the next cycle.\n- **NEVER end a cut without the step-5 back-merge pushed to the source branch.**\n  Until it lands, every sibling push is refused `blocked:version-not-monotonic`\n  for something none of them did (order 800-vk2p).\n\n---\n\n## Failure recovery\n\nIf any STEP fails:\n\n- **STEP 3 CI fails**: surface the failing run URL, do NOT merge, do NOT tag. The next 24-hour cycle retries. The work-loop continues to land fixes meanwhile.\n- **STEP 5 tag-push fails (existing tag)**: another run beat us. Skip release; cycle ends successfully (someone else released).\n- **STEP 5 back-merge push fails**: the fleet is blocked until it lands. Fixing and pushing the back-merge takes priority over everything else in the cycle — the refusal siblings are seeing names this exact remedy, so leaving it undone strands every host that reads it (order 800-vk2p).\n- **STEP 6 workflow trigger fails**: the tag is on main; user can manually run `gh workflow run release.yml --ref main -f version=${new_version}` (1953-jmvb: on main, never on the tag). Surface a clear next-step instruction.\n- **STEP 7 release build fails**: the tag exists on main but no GitHub Release was published. Next cycle does NOT retry the tag — it bumps to N+1. The orchestrator deals with the failed build separately.\n\n---\n\n## Why daily, not on-every-commit?\n\n- Every-commit releases would flood the GitHub Releases page and overwhelm the manual workflow_dispatch budget.\n- Linux Silverblue smoke-tests cost the user real time — daily is enough granularity to catch regressions without churn.\n- The work-loop + integration cron already enforce continuous green on every commit; this skill adds the \"shippable artifact\" gate on top.\n\n---\n\n## Orchestrator-steered cadence\n\nThe \u002Floop \u002F schedule that drives this skill is set up cloud-side via the `schedule` skill so it persists across user sessions. The orchestrator can re-cadence (e.g. to 12 hours for active development weeks, or pause via `gh workflow disable` + a coordination note) by editing the schedule out-of-band; the skill itself does not assume a particular cadence.\n\n---\n\n## File layout\n\nThe canonical file lives at `skills\u002Fmerge-to-main-and-release\u002FSKILL.md`. Each agent runtime (`.claude\u002F`, `.opencode\u002F`, `.codex\u002F`, `.gemini\u002F`, `.github\u002F`) accesses it via a symlink under its `skills\u002F` directory pointing at `..\u002F..\u002Fskills\u002Fmerge-to-main-and-release`, so there is exactly one source of truth. Editing the canonical file updates the skill for all runtimes simultaneously.\n",{"name":5,"description":7},"bf1668fd1df1b3363b826d083f060234cd2e1d0c","skills\u002Fmerge-to-main-and-release",0,"https:\u002F\u002Fgithub.com\u002F8007342\u002Ftillandsias","https:\u002F\u002Fgithub.com\u002F8007342","https:\u002F\u002Ftlatoani.net","I used to be a software engineer.","2026-11-15 01:25:25",{"source":8,"skillName":5,"commands":53,"prompt":63,"zipAvailable":64},[54,57,60],{"tool":55,"command":56},"npx","npx skills add 8007342\u002Ftillandsias --skill merge-to-main-and-release",{"tool":58,"command":59},"bunx","bunx skills add 8007342\u002Ftillandsias --skill merge-to-main-and-release",{"tool":61,"command":62},"pnpm","pnpm dlx skills add 8007342\u002Ftillandsias --skill merge-to-main-and-release","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002F8007342\u002Ftillandsias\u002Fblob\u002Fmain\u002Fskills\u002Fmerge-to-main-and-release\u002FSKILL.md",true,[],[67,95,128,147,169,181],{"skillNo":68,"name":69,"nameZh":6,"description":70,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":71,"licenseFileUrl":6,"sourceHtmlUrl":72,"categories":73,"keywords":74,"publishedAt":39},"01M6H60M8Y949GNECQVKAYFNH3","start-a-new-project","Use when the user wants to start a new project, asks how to begin, or is in an empty or nearly empty repository (\"how do I start?\", \"I want to build an app\", \"help me set up a new project\"). Guides them, in plain language, to describe the app as specs first (spec-driven development with OpenSpec), recommends Flutter (and the Flame engine for games and graphics), and sets up specs, a plan folder for longer work, and a README. Works for any project, not only Tillandsias.","MIT","https:\u002F\u002Fgithub.com\u002F8007342\u002Ftillandsias\u002Fblob\u002Fmain\u002Fskills\u002Fstart-a-new-project\u002FSKILL.md",[],[75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94],"start","new project","project","start a new project","development","SET","flutter","README","use","only","up","openspec","do","as","app","how to","flame","games","spec-driven","for",{"skillNo":96,"name":97,"nameZh":6,"description":98,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":99,"categories":100,"keywords":109,"publishedAt":39},"01M6H60EXYDCPSD1QGC89HM0ZC","smoke-curl-install-and-test-e2e","Clean-room end-to-end smoke test of a PUBLISHED release. Curl-installs the latest release binary from GitHub, does a full `podman system reset --force`, runs `tillandsias --debug --init` from a pristine state, and (if init is clean) launches `tillandsias --cloud 8007342\u002Ftillandsias --opencode` with ONE short fixed smoke prompt (run the credential guard, reply with a fixed token; never the \u002Fmeta-orchestration loop, 1941-smkp). Every issue observed — by this skill's agent during install\u002Freset\u002Finit, or by the agent's one turn inside the forge — is filed as a plan\u002Fissues work packet for later pickup by \u002Fadvance-work-from-plan.","https:\u002F\u002Fgithub.com\u002F8007342\u002Ftillandsias\u002Fblob\u002Fmain\u002Fskills\u002Fsmoke-curl-install-and-test-e2e\u002FSKILL.md",[101,106],{"categoryNo":102,"categoryName":103,"taxonomyType":104,"taxonomyTypeDesc":105},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":107,"categoryName":108,"taxonomyType":104,"taxonomyTypeDesc":105},"01JBSEEDCAT000000000000023","Skill与智能体开发",[110,111,112,113,28,114,115,116,117,118,119,120,121,122,27,123,124,125,126,127],"test","e2e","curl","install","smoke curl install and test e2e","orchestration","init","podman","issues","forge","Meta","loop","plan","short","never","clean","end","reset",{"skillNo":129,"name":130,"nameZh":6,"description":131,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":71,"licenseFileUrl":6,"sourceHtmlUrl":132,"categories":133,"keywords":134,"publishedAt":39},"01M6H609K8BAW74S59C68QRKC3","project-discipline","How THIS project wants its branches and pushes shaped — read its branch-discipline level (0 push to main freely; 1 integration branch + pull requests; 2 work refs into the integration branch), the drift between what it declares and what its history shows, which hooks it has installed, how to raise it, and the work format each hook refusal asks for. Every refusal that says \"use \u002Fproject-discipline for instructions\" resolves here. Works for any project in a Tillandsias forge, not only Tillandsias.","https:\u002F\u002Fgithub.com\u002F8007342\u002Ftillandsias\u002Fblob\u002Fmain\u002Fskills\u002Fproject-discipline\u002FSKILL.md",[],[135,77,136,137,138,139,119,140,27,141,83,84,142,19,143,144,90,145,146,94],"discipline","project discipline","pull requests","pull","push","history","branch","hooks","branches","drift","requests","which",{"skillNo":148,"name":149,"nameZh":6,"description":150,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":151,"categories":152,"keywords":153,"publishedAt":39},"01M6H605G5NAKPP1RE10T3716W","probe-macos-tray-on-windows","Daily cross-platform health probe — compiles the macos-tray crate from the local Windows host. Expected to succeed as a stub (the crate is fully cfg-gated for non-macOS); a build failure here means a shared module or crate broke for everyone. Files structured findings to plan\u002Fissues\u002Fprobe-macos-tray-on-windows-findings-\u003CDATE>.md.","https:\u002F\u002Fgithub.com\u002F8007342\u002Ftillandsias\u002Fblob\u002Fmain\u002Fskills\u002Fprobe-macos-tray-on-windows\u002FSKILL.md",[],[154,155,156,157,118,158,159,122,160,161,162,163,164,165,166,167,88,35,94,168],"macos","windows","probe","probe macos tray on windows","CFG","health","daily","files","build","gated","local","shared","platform","module","expected",{"skillNo":170,"name":171,"nameZh":6,"description":172,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":173,"categories":174,"keywords":175,"publishedAt":39},"01M6H603KPFQJNC7WWD6X5Q4AA","multihost-orchestration","Compatibility alias for the canonical coordinate-multihost-work skill.","https:\u002F\u002Fgithub.com\u002F8007342\u002Ftillandsias\u002Fblob\u002Fmain\u002Fskills\u002Fmultihost-orchestration\u002FSKILL.md",[],[115,176,27,177,178,179,180,94],"multihost orchestration","compatibility","coordinate","alias","canonical",{"skillNo":182,"name":183,"nameZh":6,"description":184,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":185,"categories":186,"keywords":189,"publishedAt":39},"01M6H5ZXRBC66CSA5N28HP2BY7","meta-orchestration","Host-aware Tillandsias recurring runtime loop: sync remote state, drain claimable plan work, run eligible e2e smoke gates, coordinate integrations on mutable Linux, release when warranted, update plan, commit, and push before exit.","https:\u002F\u002Fgithub.com\u002F8007342\u002Ftillandsias\u002Fblob\u002Fmain\u002Fskills\u002Fmeta-orchestration\u002FSKILL.md",[187,188],{"categoryNo":102,"categoryName":103,"taxonomyType":104,"taxonomyTypeDesc":105},{"categoryNo":107,"categoryName":108,"taxonomyType":104,"taxonomyTypeDesc":105},[115,120,190,191,192,193,24,22,194,139,121,122,195,27,196,28,197,178,35,198],"meta orchestration","commit","sync","run","state","integrations","recurring","exit","remote"]