diff --git a/docs/ARCHITECTURE-v0.md b/docs/ARCHITECTURE-v0.md index 748b905..b2b660b 100644 --- a/docs/ARCHITECTURE-v0.md +++ b/docs/ARCHITECTURE-v0.md @@ -295,6 +295,17 @@ The CLI can *offer* to write the matching `mise.toml` stanza (helpful), but inst ### 8b. mise GitHub-backend realities (grounded) — and why release-please-per-binary is load-bearing +> **Correction (2026-06-02, RFC 0001 + issue #23 validation).** Parts of this section are +> superseded. (i) mise has **no `tag_regex`** — per-stream selection uses **`version_prefix`** +> (a *leading*-prefix stripper only). (ii) The single-binary-per-entry *install collision* was +> a **scheduler** dedup bug, **fixed in mise 2026.4.12 (PR #9093)**; co-installing multiple +> binaries from one repo now works *for prefix or single-release shapes*. (iii) **But** a +> strict-semver tag policy forces **build-metadata** streams (`v0.5.0+iii`), which SemVer +> precedence collapses and `version_prefix` cannot select — so those remain natively +> unresolvable, which is why the **`skillrig` mise backend plugin** is justified on capability. +> See [`docs/rfcs/0001-mise-skillrig-backend.md`](rfcs/0001-mise-skillrig-backend.md) and the +> spike under `specledger/013-mise-backend/`. The grounded prose below is retained for history. + Verified against mise's GitHub backend docs (current as of early 2026). Three findings shape the template: **(1) The one-binary-per-entry limit — the constraint that shapes the origin's release strategy.** mise's GitHub backend installs **a single binary per tool entry**; it does **not** natively fetch multiple binaries from one release (confirmed: `mise use github:org/repo` installs only the first asset and skips the rest; the community workaround is a per-tool postinstall `curl` for extra assets). This collides with the naive reading of "one monorepo origin with several backing CLIs in `cmd/`" if that monorepo cuts **one release containing all binaries**. The resolution — and it happens to be the pattern already chosen for versioning hygiene — is **per-CLI tagged release streams via release-please** (`oxid-v1.4.0`, `foo-v2.1.0`, …) rather than one monolithic release. So: diff --git a/docs/rfcs/0001-mise-skillrig-backend.md b/docs/rfcs/0001-mise-skillrig-backend.md new file mode 100644 index 0000000..386e56b --- /dev/null +++ b/docs/rfcs/0001-mise-skillrig-backend.md @@ -0,0 +1,377 @@ +# RFC 0001 — The `skillrig` mise backend plugin + +**Status:** Draft for review +**Author:** generated from issue [#23](https://github.com/skillrig/cli/issues/23) +**Spike:** [`specledger/013-mise-backend/research/2026-06-02-mise-backend-plugin.md`](../../specledger/013-mise-backend/research/2026-06-02-mise-backend-plugin.md) +**Relates to:** `docs/ARCHITECTURE-v0.md` §8 (backing-CLI provisioning), §8b (mise realities), §13 vNext +**Bootstraps:** a **new, separate repository** — `skillrig/mise-skillrig` (the plugin is Lua, not Go; it does **not** live in `skillrig/cli`) + +> **Scope note (pre-release marker, per `CLAUDE.md`).** No backward compatibility is planned. +> This RFC defines a new artifact and a new convention-versioned origin contract; it may +> change freely until it ships. + +--- + +## 1. Summary + +skillrig origins are **co-located monorepos**: one repo holds an org's agent skills *and* the +private backing CLIs those skills require (`cmd/`), released by the same pipeline so a skill +and its tool version, release, and are vendored/verified as one unit (`ARCHITECTURE-v0` §1). +A skill declares its tool via `metadata.x-skillrig.requires`; provisioning of the binary is +delegated to **mise** (`ARCHITECTURE-v0` §8 / R17 — *"skillrig declares and verifies, mise +installs"*). + +This RFC specifies a **mise backend plugin** named `skillrig` so that N backing CLIs in one +origin become N **distinct** mise tools, addressed `skillrig:@`, each tracking +its own independent release stream: + +```toml +# consumer mise.toml +"skillrig:iii" = "latest" +"skillrig:console" = "0.2.0" +``` + +It is a separate, independently-released repo. This document also defines the **origin-side +contract** (a convention-versioned `[[binaries]]` block) the plugin depends on, and the +changes to the **origin template** and **`skillrig` CLI** that make the three pieces work +together. + +## 2. Motivation — why native mise is not enough *for this origin* + +The naive plan ("one monorepo, consume each CLI via mise's `github` backend") fails, and the +obvious fixes don't apply under the origin's tag policy. There are **two independent layers**: + +- **Layer A — install scheduler.** mise once keyed install jobs by `@`, + so two aliases pointing at `github:org/repo@` deduped into one job (only one + binary installed). **Fixed in mise 2026.4.12 (PR #9093)** by re-keying to + `@`. *Necessary, and we require it — but it only fixes Layer A.* +- **Layer B — version resolution.** PR #9093 explicitly *"does not touch version listing or + resolution."* The origin's **tag policy enforces strict semver**, which **forbids** prefix + tags (`iii-v0.1.0` is not valid semver) but **permits** build-metadata tags (`0.1.0+iii` + is). Per SemVer 2.0.0, *build metadata MUST be ignored for precedence* — so `0.5.0+iii` and + `0.2.0+console` are indistinguishable to mise's resolver: the version set collapses, + `latest` for one stream resolves to the max across **all** streams, and `version_prefix` + (a *leading*-prefix stripper — there is **no** `tag_regex`/suffix selector) cannot pick a + `+iii` suffix. + +The full native design space (spike Finding 1b): + +| Option | Independent versions | Co-location | Strict-semver tags | Native mise | +|---|:---:|:---:|:---:|:---:| +| (a) prefix streams `iii-v*` + `version_prefix` | ✅ | ✅ | ❌ forbidden by policy | ✅ | +| (b) build-metadata streams `0.5.0+iii` | ✅ | ✅ | ✅ | ❌ **broken (Layer B)** | +| (c) one release, all binaries, `asset_pattern` | ❌ | ✅ | ✅ | ✅ (post-#9093) | +| (d) separate repo per CLI | ✅ | ❌ | ✅ | ✅ | +| (e) **`skillrig` backend plugin** | ✅ | ✅ | ✅ | ✅ (plugin owns listing) | + +Only **(e)** satisfies *independent versioning + co-location + strict-semver* together, +because a backend plugin's `BackendListVersions` hook **owns version listing** and can map +build-metadata tags to per-tool version streams — the one thing native mise structurally +cannot do. That is the capability justification for this plugin. + +## 3. Goals / non-goals + +**Goals** +- Co-install any number of an origin's backing CLIs as distinct `skillrig:` mise tools, + each tracking its own build-metadata release stream, on a strict-semver origin. +- Drive resolution from the **origin's own metadata** (zero per-consumer `asset_pattern` + boilerplate); the consumer writes only `skillrig: = ""`. +- **Checksum-verify** every downloaded binary against the origin's published checksums. +- Resolve a GitHub token centrally (handles the private-origin keyring-404 gotcha). +- Keep skillrig's "no extra architecture" promise: a small published plugin + a metadata + block in the origin. **No registry/index service.** + +**Non-goals (v1)** +- Replacing mise. skillrig still *declares + verifies*; mise *installs* (R17). +- Binding the binary to the skill's `treeSha`/`commit` (tamper-evidence parity) — **v2**. +- SLSA/GPG attestation — **v2** (origin ships sha256 checksums today). +- Public-CLI provisioning (`terraform`, `gh`) — those stay on mise's stock backends. +- Windows-first support — best-effort; Linux/macOS are the v1 targets. + +## 4. How the three pieces fit together + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ORIGIN MONOREPO my-org/my-skills (stood up from the origin template) │ +│ │ +│ cmd/iii/ cmd/console/ ... ── goreleaser builds per-CLI assets │ +│ release-please (per-package) ── cuts tags v0.5.0+iii , v0.2.0+cons. │ +│ releases: iii_0.5.0_linux_amd64.tar.gz + checksums.txt (per stream) │ +│ │ +│ .skillrig-origin.toml ─ [[binaries]] : stream selector + asset template│ +│ index.json ─ generated by `skillrig index`; mirrors binaries│ +└───────────────┬───────────────────────────────────────────────────────────┘ + │ (1) plugin fetches index.json/.skillrig-origin.toml + tags + assets + │ authenticated via MISE_GITHUB_TOKEN / GITHUB_TOKEN / gh + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ mise + skillrig backend plugin (skillrig/mise-skillrig, Lua) │ +│ BackendListVersions → list tags, filter by `+iii`, return clean semver │ +│ BackendInstall → resolve tag, pick asset, sha256-verify, extract │ +│ BackendExecEnv → put bin on PATH │ +└───────────────┬───────────────────────────────────────────────────────────┘ + │ (2) + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ CONSUMER REPO │ +│ mise.toml: "skillrig:iii" = "latest" ← written by `skillrig add` │ +│ .skillrig/config.toml: origin = "my-org/my-skills" │ +│ skill's SKILL.md: metadata.x-skillrig.requires: [{tool: iii, ...}] │ +└───────────────────────────────────────────────────────────────────────────┘ +``` + +1. **Origin template** ships the release pipeline that produces per-stream tags + assets + + `checksums.txt`, *and* the `[[binaries]]` metadata describing them (§5). +2. **The plugin** reads that metadata to resolve/install each tool (§6). +3. **The `skillrig` CLI** auto-wires the consumer's `mise.toml` when a vendored skill requires + a binary, and reuses its own origin resolution + token resolver (§7). + +## 5. The origin-side contract (`[[binaries]]`) — OQ2 + +The per-binary stream + asset conventions do **not** exist in the origin metadata today. We +add them as a **convention-versioned** block in `.skillrig-origin.toml`, mirrored into the +generated `index.json` so the plugin can fetch a single file. This is `skillrig/cli` + +origin-template work, shared by the plugin and any native-stanza generator (AP-04 — one +contract, many readers). + +```toml +# .skillrig-origin.toml (origin repo root) +skillrig-convention = 1 +origin = "my-org/my-skills" + +[[binaries]] +name = "iii" # → mise tool name `skillrig:iii` +stream = "+iii" # semver BUILD-METADATA suffix identifying this stream +asset = "iii_{version}_{os}_{arch}.tar.gz" # {version}=semver core, {os}/{arch} mapped +checksums = "checksums.txt" # asset in the same release; sha256 filename lines +bin = "iii" # executable path inside the archive (post strip) + + # optional: map mise RUNTIME tokens → this asset's tokens (defaults: linux/darwin, amd64/arm64) + [binaries.platforms.linux-x64] + os = "linux" + arch = "amd64" + [binaries.platforms.darwin-arm64] + os = "darwin" + arch = "arm64" + +[[binaries]] +name = "console" +stream = "+console" +asset = "console_{version}_{os}_{arch}.tar.gz" +checksums = "checksums.txt" +bin = "console" +``` + +`skillrig index` (already the catalog generator) emits the same data into `index.json`: + +```jsonc +{ + "skillrigConvention": 1, + "origin": "my-org/my-skills", + "binaries": [ + { "name": "iii", "stream": "+iii", + "asset": "iii_{version}_{os}_{arch}.tar.gz", + "checksums": "checksums.txt", "bin": "iii", + "platforms": { "linux-x64": {"os":"linux","arch":"amd64"}, "...": {} } } + ], + "skills": { /* unchanged */ } +} +``` + +**Convention versioning (R5e).** The plugin reads `skillrigConvention` and fails clearly +against an incompatible origin rather than mis-resolving. v1 understands convention `1`. + +## 6. The plugin — design (OQ1) + +**Repo / naming.** New repo **`skillrig/mise-skillrig`**; mise backend name `skillrig` +(tools addressed `skillrig:`); the plugin name need not match the repo. Installed via +`mise plugin install skillrig https://github.com/skillrig/mise-skillrig` (or registered in the +mise plugin registry for the short name). **Released on its own cadence**, independent of the +`skillrig` CLI. + +**Repo layout (bootstrap target).** + +``` +skillrig/mise-skillrig/ +├── metadata.lua # plugin name, version, author +├── hooks/ +│ ├── backend_list_versions.lua # BackendListVersions +│ ├── backend_install.lua # BackendInstall +│ └── backend_exec_env.lua # BackendExecEnv +├── lib/ +│ ├── origin.lua # resolve origin + fetch index.json/.skillrig-origin.toml +│ ├── github.lua # authed GitHub API (tags, releases, asset download) +│ ├── stream.lua # build-metadata stream parse/filter/sort +│ └── checksum.lua # sha256 verify against checksums.txt +├── mise-tasks/ # test/lint tasks +├── .github/workflows/ # CI + release +└── README.md +``` + +**Origin resolution (inside the plugin).** The plugin must know which origin to read. +Precedence (mirrors the CLI for consistency): +`ctx.options.origin` (per-tool in `mise.toml`) → `SKILLRIG_ORIGIN` env → error with a fix. + +```toml +# explicit per-tool origin (when not using SKILLRIG_ORIGIN) +[tools."skillrig:iii"] +version = "latest" +origin = "my-org/my-skills" +``` + +### 6.1 `BackendListVersions` — the load-bearing hook + +Owns version listing, which is exactly why native mise can't do this. It fetches the origin's +tags, keeps only those whose build metadata matches the tool's `stream`, strips the `+stream` +suffix, and returns clean semver cores ascending. + +```lua +-- hooks/backend_list_versions.lua +function PLUGIN:BackendListVersions(ctx) + local meta = origin.binary_meta(ctx.tool) -- from index.json, by ctx.tool + local tags = github.list_tags(origin.repo()) -- authed; e.g. {"v0.5.0+iii","v0.2.0+console"} + local stream = meta.stream -- "+iii" + local versions = {} + for _, tag in ipairs(tags) do + local core, build = stream.parse(tag) -- "0.5.0", "iii" (strips leading v) + if build == stream.suffix(meta.stream) then -- belongs to THIS stream + versions[#versions + 1] = core + end + end + stream.sort_semver_asc(versions) -- ascending; mise applies no extra sort + return { versions = versions } +end +``` + +`latest` then naturally resolves to the max **within the stream** — the behavior native mise +cannot produce for build-metadata tags. + +### 6.2 `BackendInstall` — resolve, download, verify, extract + +```lua +-- hooks/backend_install.lua +function PLUGIN:BackendInstall(ctx) + local meta = origin.binary_meta(ctx.tool) + local tag = "v" .. ctx.version .. meta.stream -- "0.5.0" + "+iii" → "v0.5.0+iii" + local rel = github.release_by_tag(origin.repo(), tag) -- authed + + local plat = meta.platforms[RUNTIME.osType .. "-" .. RUNTIME.archType] or stream.default_plat() + local name = stream.render(meta.asset, { -- "iii_0.5.0_linux_amd64.tar.gz" + version = ctx.version, os = plat.os, arch = plat.arch, + }) + + local asset = github.find_asset(rel, name) + local file = github.download(asset, ctx.download_path) + + -- checksum verify (mise does NOT verify for custom backends — the plugin must) + local sums = github.download(github.find_asset(rel, meta.checksums), ctx.download_path) + checksum.verify_sha256(file, name, sums) -- abort on mismatch + + archive.extract(file, ctx.install_path, { strip = "auto" }) + return {} +end +``` + +### 6.3 `BackendExecEnv` — PATH + +```lua +-- hooks/backend_exec_env.lua +function PLUGIN:BackendExecEnv(ctx) + return { env_vars = { { key = "PATH", value = ctx.install_path .. "/bin" } } } +end +``` + +(If `bin` is at the archive root rather than `bin/`, `BackendInstall` normalizes it into +`install_path/bin/` so this hook stays trivial.) + +## 7. Auth (the keyring-404 gotcha) and CLI integration + +**Token resolution.** mise's own github-token precedence already centralizes this for the +plugin's GitHub API calls: `MISE_GITHUB_TOKEN` → `GITHUB_API_TOKEN` → `GITHUB_TOKEN` → +`settings.github.credential_command` (host via `MISE_CREDENTIAL_HOST`) → native OAuth → +`github_tokens.toml` → gh `hosts.yml` → `git credential fill`. The documented gotcha — +**mise cannot read a gh token stored in the OS keyring** (only `hosts.yml`) — is handled by +documenting either `MISE_GITHUB_TOKEN=$(gh auth token)` or a `credential_command`. The plugin +reuses this resolver via mise's HTTP/token helpers; it introduces **no new credential +surface** (consistent with `ARCHITECTURE-v0` §2b — no write credential anywhere). + +**`skillrig` CLI auto-wiring (in this repo, `skillrig/cli`).** When `skillrig add ` +vendors a skill whose `metadata.x-skillrig.requires` names a binary sourced from the origin, +the CLI writes the matching `mise.toml` stanza: + +```toml +"skillrig:iii" = ">=0.4.0" # from requires[].version +``` + +This is the one piece of plugin support that belongs in the Go CLI (it owns `add`, origin +resolution, and the lock). It is gated behind the same dry-run/force discipline as `add`. + +## 8. Verification depth (OQ3) + +- **v1 — checksum-only.** sha256 against the origin's `checksums.txt` (already published). + This matches what mise's native `github` backend offers, re-implemented in the plugin + because mise does not verify for custom backends. +- **v2 — provenance / treeSha parity.** Bind the binary's release tag/commit to the + `treeSha`/`commit` the skill's lock entry already records, so a backing CLI is tamper-evident + to the same standard as the skill that required it. Deferred behind a real trigger + (`ARCHITECTURE-v0` §13 vNext); needs the lock schema to carry a binary reference. +- **SLSA/GPG** — not a current github-backend feature and not in v1; revisit with v2. + +## 9. Origin template changes + +The batteries-included template (`ARCHITECTURE-v0` §2d) gains: +1. **Per-package release-please** config emitting **build-metadata** tags (`v+`) + per CLI in `cmd/`, plus **goreleaser** producing `name_{version}_{os}_{arch}.tar.gz` + archives and a `checksums.txt` per release. +2. A populated **`.skillrig-origin.toml` `[[binaries]]`** block (§5) and the `index.yml` + workflow regenerating `index.json` (binaries + skills) on merge. +3. Docs: require **mise ≥ 2026.4.12** (Layer A), install the `skillrig` plugin, and set + `SKILLRIG_ORIGIN` / token. A worked `mise.toml` example. + +> If an adopting org *can* relax its tag policy, the template should note that **prefix +> streams + `version_prefix`** (option (a)) work on stock mise with no plugin — the plugin is +> the answer specifically for the **strict-semver + independent-versioning** org. + +## 10. Alternatives considered + +- **(a) Prefix tag streams + native `version_prefix`.** Cleanest natively, but the origin's + strict-semver tag policy forbids non-semver prefix tags. Viable only if the policy relaxes. +- **(c) One monolithic release with all binaries + `asset_pattern`.** Works on mise 2026.4.12, + but sacrifices **independent versioning** — every CLI bumps together. Rejected. +- **(d) Separate repo per CLI.** Cleanest for mise, but breaks the **co-location** that lets a + skill and its CLI ship/verify as one unit (`ARCHITECTURE-v0` §1). Rejected. +- **Native-stanza generator only** (`skillrig add` writes `[tool_alias]`+`asset_pattern`). + Attractive (no new repo), but inherits Layer B — it cannot generate a working stanza for + build-metadata streams. Useful as a *complement* for prefix-stream origins, not a substitute. +- **skillrig pulls binaries itself** (`ARCHITECTURE-v0` §13 vNext). Re-absorbs the job given to + mise (R17) and means owning cross-OS/arch selection + cache. The plugin keeps mise as the + installer. Rejected for now. + +## 11. Open questions + +1. **Plugin↔origin metadata fetch:** raw `index.json` over the GitHub API vs. a contents API + call for `.skillrig-origin.toml`; caching policy within a mise run. +2. **`{os}/{arch}` defaults:** the default RUNTIME→asset token mapping before any + `[binaries.platforms]` override (e.g. `x64`→`amd64`, `darwin`→`darwin`/`macos`). +3. **Archive shapes:** `strip = "auto"` heuristics for single-binary tarballs vs. nested dirs; + raw (un-archived) asset support. +4. **mise registry submission:** short name `skillrig` vs. installing by git URL for v1. +5. **convention bump policy:** does the plugin support conventions `N` and `N-1`? +6. **v2 treeSha binding:** lock schema for a binary reference and how `verify`/`doctor` check it. + +## 12. Phasing + +- **P0 (this RFC + spike):** decision recorded; `[[binaries]]` contract drafted; §8b corrected. +- **P1 — origin contract:** implement `[[binaries]]` in `.skillrig-origin.toml` + `skillrig + index` emission + template release pipeline (build-metadata tags + checksums). *(in + `skillrig/cli` + origin template)* +- **P2 — plugin v1:** bootstrap `skillrig/mise-skillrig`; three hooks; build-metadata stream + resolution; checksum verify; auth; tests against a fixture origin. +- **P3 — CLI auto-wiring:** `skillrig add` writes `skillrig:` stanzas from `requires`. +- **P4 (v2):** treeSha/provenance binding; SLSA; registry submission. + +## References + +See the spike: `specledger/013-mise-backend/research/2026-06-02-mise-backend-plugin.md` +(mise discussions #9074/#8266, PR #9093, mise github-backend / backend-plugin-development / +github-tokens docs, SemVer 2.0.0 build-metadata precedence). diff --git a/specledger/013-mise-backend/research/2026-06-02-mise-backend-plugin.md b/specledger/013-mise-backend/research/2026-06-02-mise-backend-plugin.md new file mode 100644 index 0000000..98c5e92 --- /dev/null +++ b/specledger/013-mise-backend/research/2026-06-02-mise-backend-plugin.md @@ -0,0 +1,257 @@ +# Research: mise backend for skillrig — multi-binary co-install from one origin monorepo + +**Date**: 2026-06-02 +**Context**: Issue [#23](https://github.com/skillrig/cli/issues/23) proposes a `skillrig` +mise **backend plugin** (vfox-style, Lua) so that an origin monorepo shipping N backing +CLIs in `cmd/` can be consumed as N distinct mise tools (`skillrig:jira`, `skillrig:tfc`, +…). The trigger was a validated collision: mise's stock `github` backend keyed a tool by +`owner/repo` and tracked one version per tool, so co-installing 2+ release streams from one +repo collapsed into a single tool. This spike answers the issue's four open questions and +the prior `ARCHITECTURE-v0 §8b` claims, to decide **whether** to build the plugin and, if +so, the contract it depends on. +**Time-box**: ~45 min (web + docs). + +## Question + +1. **OQ4 (gate):** Does a *current* mise (`asset_pattern` + aliases, ≥2026.5) cover + multi-binary-from-one-repo **natively**, before we commit to a plugin? +2. **OQ2:** Origin-metadata contract — does `.skillrig-origin.toml` / `index.json` already + carry per-binary stream + asset conventions, or do we add a `[[binaries]]` section? +3. **OQ3:** Verification depth for v1 — checksum-only vs. full provenance / treeSha tie-in. +4. **OQ1:** Plugin home / name & its own release cadence. + +Plus: validate the backend-plugin hook surface and the auth gotcha the issue documents. + +--- + +## Findings + +### Finding 1 — PR #9093 fixes the **scheduler**, not version resolution — partial fix only — *high confidence* + +> **Correction (after reviewing the PR, 2026-06-02).** An earlier draft of this finding +> claimed mise 2026.4.12 fully resolves the multi-binary case. That is **wrong** — it +> conflated two independent layers. The fix is necessary but **not sufficient** for the +> origin's actual tag scheme (see Finding 1b). + +There are **two layers**, and PR #9093 touches only the first: + +- **Layer A — install scheduler dedup.** The old dependency-graph node key was + `@`, so two aliases resolving to the same + `github:owner/repo@` collapsed into one install job. **PR #9093** re-keys nodes to + `@`, so distinct aliases get distinct scheduler slots and each + installs with its own options (`asset_pattern`, `bin_path`, `postinstall`). The PR + description explicitly states it **"does not touch version listing or resolution."** Shipped + in **mise 2026.4.12** (discussion #9074, *"tested with 2026.4.12, works OK ✅"*). +- **Layer B — version listing / resolution.** *Unchanged by #9093.* This is where the + origin's tag scheme breaks (Finding 1b). + +The native multi-asset pattern (discussion #8266) — which #9093 makes work — is `[tool_alias]` ++ per-platform `asset_pattern`: + +```toml +[tool_alias] +guest-init = "github:jingkaihe/matchlock" + +[tools."github:jingkaihe/matchlock"] +version = "latest" +platforms.linux-x64 = { asset_pattern = "matchlock-linux-amd64" } + +[tools.guest-init] +version = "latest" +platforms.linux-x64 = { asset_pattern = "guest-init-linux-amd64" } +``` + +This works **when both binaries ship in one release at one version** (each alias picks its +asset by pattern). It does **not** give *independent* version streams — see Finding 1b. + +### Finding 1b — semver **build-metadata** streams are structurally unresolvable natively — *high confidence (by spec)* + +The origin's constraint: its **tag policy enforces strict semver**, which *forbids* prefix +tags (`iii-v0.1.0` is not valid semver — leading non-numeric) but *permits* **build-metadata** +tags (`0.1.0+iii` is valid semver). So the org expresses independent CLI streams as +`v0.5.0+iii`, `v0.2.0+console`, etc. + +Per **SemVer 2.0.0**: *"Build metadata MUST be ignored when determining version precedence … +two versions that differ only in the build metadata have the same precedence."* Every +semver-respecting resolver (npm, Helm, and mise's `latest`/range resolution) treats +`0.5.0+iii` and `0.5.0+console` as the **same version**. Consequences for the github backend: + +- The listed version set collapses to `{0.5.0, 0.2.0}` with **no stream identity**. +- `latest` for the `console` alias resolves to `0.5.0` (the max across **all** streams), then + looks for a `console-*` asset in the `0.5.0+iii` release — **which doesn't contain it** → + install fails / wrong binary. +- mise's `version_prefix` only strips a **leading** prefix; there is **no** `version_suffix` + or `tag_regex`, so a `+iii` **suffix** cannot select a stream. +- Even *exact* pins (`= "0.5.0+iii"`) are brittle: precedence-based matching treats build + metadata as equal, and it defeats the point of tracking `latest` per stream. + +**So the native design space is:** + +| Option | Independent versions? | Co-location? | Strict-semver tags? | Native mise works? | +|---|---|---|---|---| +| (a) prefix streams `iii-v*` + `version_prefix` | ✅ | ✅ | ❌ (forbidden by policy) | ✅ | +| (b) build-metadata streams `0.5.0+iii` | ✅ | ✅ | ✅ | ❌ **broken (Layer B)** | +| (c) one release, all binaries, `asset_pattern` | ❌ | ✅ | ✅ | ✅ (post-#9093) | +| (d) separate repo per CLI | ✅ | ❌ | ✅ | ✅ | +| (e) **custom `skillrig:` backend plugin** | ✅ | ✅ | ✅ | ✅ (plugin owns listing) | + +**Implication — this *flips* the earlier conclusion.** A dedicated `skillrig:` backend plugin +is justified on **capability**, not merely ergonomics: its `BackendListVersions` hook **owns +version listing**, so it can parse the `+iii` build metadata, filter tags to that stream, and +present a clean per-tool version list — the one thing native mise structurally cannot do for +build-metadata streams under a strict-semver tag policy. Options (a)/(c)/(d) each sacrifice +something the architecture wants (policy compliance / independent versioning / co-location). + +### Finding 2 — `tag_regex` does not exist; the real knob is per-tool `version_prefix` — *high confidence* + +The issue (and `ARCHITECTURE-v0 §8b`) assume a `tag_regex` option keyed per tool. The +github-backend docs describe **no** `tag_regex`. Per-stream tag selection in a monorepo is +done with **`version_prefix`**: + +```toml +[tools] +"github:my-org/my-skills" = { version = "latest", version_prefix = "jira-v" } +``` + +So a build-metadata stream (`1.7.0+jira`) is *not* natively selectable, but a **prefix** +stream (`jira-v1.7.0`) is — via `version_prefix`. This matters: it means the **prefix tag +convention** (which release-please already produces per-package) is the natively-supported +one, and the origin template should standardize on it. + +### Finding 3 — backend-plugin hook surface (vfox-style) — *high confidence* + +Three Lua hooks under `hooks/`, addressed `plugin:tool`: + +| Hook | ctx fields | returns | +|---|---|---| +| `BackendListVersions(ctx)` | `ctx.tool`, `ctx.options` | `{versions = {…ascending semver…}}` | +| `BackendInstall(ctx)` | `ctx.tool`, `ctx.version`, `ctx.install_path`, `ctx.download_path`, `ctx.options` | `{}` | +| `BackendExecEnv(ctx)` | `ctx.tool`, `ctx.version`, `ctx.install_path`, `ctx.options` | `{env_vars = {{key="PATH", value=install_path.."/bin"}}}` | + +- `RUNTIME` injects `osType` / `archType` / `envType` (gnu|musl) for asset selection. +- **The plugin owns download + extraction + verification** into `install_path`; mise does + **not** checksum-verify for a custom backend (unlike the first-party `github` backend, + which does sha256 + `mise.lock`). So a plugin must *re-implement* checksum verification + that the native `github` backend gives for free. +- A backend plugin is a **git repo** addressed `plugin:tool`; the plugin name need not match + the repo. Installed via `mise plugin install `; optional PR to the mise + registry for a short name. There is a `jdx/mise-backend-plugin-template` (hooks + + `metadata.lua` + CI + lint). + +### Finding 4 — auth: the gotcha is real, but mise has first-class hooks — *high confidence* + +The issue's claim is confirmed: mise reads gh's token from `hosts.yml` **but cannot read a +gh token stored in an OS keyring/credential-helper** (the common macOS case), so it falls +back to anonymous and a private origin's `/releases` returns **404**. mise's documented +token precedence (github.com): + +1. `MISE_GITHUB_TOKEN` → 2. `GITHUB_API_TOKEN` → 3. `GITHUB_TOKEN` → +4. `settings.github.credential_command` → 5. native GitHub OAuth → +6. `github_tokens.toml` (per-host) → 7. gh CLI `hosts.yml` → 8. `git credential fill`. + +- The **enterprise-clean path is `credential_command`** (host passed via + `MISE_CREDENTIAL_HOST`), e.g. `op read 'op://Private/GitHub Token/credential'` — works for + the *native* github backend too, no plugin required. +- **Correction to §8b:** the doc already self-corrected that env vars precede + `credential_command`; this spike confirms env-vars-first (1–3 above 4). + +> Net: "central auth" is **not** a plugin-only value-add — `credential_command` + +> `MISE_GITHUB_TOKEN` already centralize it for the native backend. The keyring-404 is fixed +> by *documenting* `MISE_GITHUB_TOKEN=$(gh auth token)` or a `credential_command`, with or +> without a plugin. + +### Finding 5 — what a plugin buys (after the Finding 1b correction) — *high confidence on #1, medium on rest* + +The plugin's value is now led by **capability** (Finding 1b), with ergonomics following: + +1. **Resolves build-metadata streams — the capability native mise lacks.** Under a + strict-semver tag policy, `BackendListVersions` is the *only* place that can map + `0.5.0+iii` / `0.2.0+console` tags to distinct per-tool version lists. Native mise cannot + (Layer B / SemVer precedence). This is the load-bearing justification. +2. **Zero per-consumer boilerplate.** Even where native co-install *could* work (prefix + streams), it needs the consumer to hand-author per-platform `asset_pattern` + + `version_prefix` for *every* binary and to *know* each asset-name template. A `skillrig:` + backend learns stream + asset template from the **origin's own metadata**, so the consumer + writes only `skillrig:jira = "latest"`. +3. **One addressing scheme** (`plugin:tool`) instead of `[tool_alias]` indirection. +4. **Tamper-evidence parity with skills** — tie the binary's release tag/commit to the + `treeSha`/`commit` skillrig already records in `.skillrig/skills-lock.json`. + +…against real costs: a **separate Lua repo + release cadence**, and **re-implementing** +download/checksum that the native `github` backend provides for free. + +### Finding 6 — verification depth options — *medium confidence* + +- Native `github` backend: sha256 `checksum` + `mise.lock` lockfile; SLSA/GPG **not** + documented for the github backend (the issue's "optionally SLSA/attestation" is + aspirational, not a current github-backend feature). +- A custom backend must implement its own checksum verify (Finding 3). The origin already + ships `_checksums.txt` per release (per the issue's evidence), so checksum-verify is cheap. +- **Provenance/treeSha tie-in** (bind binary tag↔skill `commit`) is the genuinely + skillrig-specific guarantee, but it's strictly more than mise gives natively and belongs in + a later phase. + +--- + +## Decisions + +- **OQ4 — PARTIALLY resolved; plugin still justified.** PR #9093 (mise 2026.4.12) fixes the + install **scheduler** dedup (Layer A) — so multi-asset-from-one-release and same-version + distinct aliases now work. It does **not** touch version **resolution** (Layer B). The + origin's strict-semver tag policy forces **build-metadata** streams (`0.5.0+iii`), which + SemVer precedence collapses — natively unresolvable, and no `version_prefix`/`tag_regex` + can select a `+` suffix. So native mise covers the *easy* shapes but **not** the origin's + actual one. **The plugin is justified on capability** (its `BackendListVersions` owns + listing), not just ergonomics. The earlier "RESOLVED — ergonomics only" conclusion was + **wrong** and is corrected here (Findings 1, 1b). +- **OQ1.** If built, the plugin lives in its **own git repo** (e.g. `skillrig/mise-skillrig`), + backend name `skillrig` (used as `skillrig:`), released on its **own cadence** + independent of the CLI, optionally registered in the mise plugin registry. It is **not** + Go code in `skillrig/cli`. +- **OQ2.** The per-binary stream + asset conventions are **not** in `index.json`/origin + config today. They must be added as a **convention-versioned** contract (a `[[binaries]]` + / `tools` block in `.skillrig-origin.toml`, surfaced into `index.json`). This is + **`skillrig/cli` + origin-template work regardless** of plugin-vs-native, because both the + plugin *and* a native-stanza generator read the same metadata. +- **OQ3.** v1 = **checksum-only** (sha256 against the origin's `_checksums.txt`), matching + what the native backend already does; **provenance/treeSha binding is v2** behind a real + trigger. +- **§8b correction needed.** `tag_regex` → `version_prefix` (a *leading*-prefix stripper, no + suffix/regex selector exists); the one-binary-per-entry framing is **partly** superseded by + 2026.4.12 (scheduler only); "central auth" is achievable natively via + `credential_command`/`MISE_GITHUB_TOKEN`. + +## Recommendations + +1. **Build the dedicated `skillrig:` backend plugin** in its own repo — it is the only option + that satisfies *independent versioning + co-location + strict-semver tag policy* + simultaneously (Finding 1b, option (e)). The RFC commits to it as the new repo's reason to + exist. Its `BackendListVersions` parses build-metadata streams; `BackendInstall` + downloads + checksum-verifies the per-tool asset; `BackendExecEnv` puts it on PATH. +2. **Land the origin-metadata contract first**, in `skillrig/cli` + the origin template: + a **convention-versioned** `[[binaries]]` block in `.skillrig-origin.toml` surfaced into + `index.json`, carrying each binary's **stream selector** (build-metadata tag suffix) + + **asset-name template** per os/arch + checksum source. Both the plugin and any + `skillrig add` auto-wiring read this one contract (AP-04). +3. **Document the native fallbacks honestly** in the RFC (options (a)/(c)/(d)) so adopters who + *can* relax the tag policy to prefix streams, or accept monolithic releases, aren't forced + onto the plugin. The plugin is the answer **for the strict-semver + independent-versioning + org**; it is not the only way to consume an origin's CLIs. +4. **OQ3 verification:** checksum-only for v1 (origin already ships `_checksums.txt`); + treeSha/commit binding to `.skillrig/skills-lock.json` is v2. +5. **Correct `ARCHITECTURE-v0 §8b`** per the decisions above and pin the convention-version + that carries the new `[[binaries]]` block; **require mise ≥ 2026.4.12** (Layer A fix) in + the template. + +## References + +- Issue #23 — skillrig/cli (this repo). +- jdx/mise discussion **#9074** + **PR #9093** — multi-binary dedup fix (2026.4.12). +- jdx/mise discussion **#8266** — `tool_alias` + `asset_pattern` multi-binary pattern. +- mise docs — *GitHub Backend* (`asset_pattern`, `version_prefix`, `platforms`, `checksum`, + `mise.lock`, `api_url`). +- mise docs — *Backend Plugin Development* (hooks, ctx fields, `plugin:tool`, distribution) + + `jdx/mise-backend-plugin-template`. +- mise docs — *GitHub Tokens* (token precedence; keyring-404; `credential_command` / + `MISE_CREDENTIAL_HOST`). +- vfox docs — backend/plugin authoring (`RUNTIME` object).