Skip to content

docs(openspec): archive add-vale-binary-packages and land the cli pin - #91

Merged
thecodedrift merged 2 commits into
mainfrom
openspec/add-vale-binary-packages-3-archive
Aug 10, 2026
Merged

docs(openspec): archive add-vale-binary-packages and land the cli pin#91
thecodedrift merged 2 commits into
mainfrom
openspec/add-vale-binary-packages-3-archive

Conversation

@thecodedrift

@thecodedrift thecodedrift commented Aug 7, 2026

Copy link
Copy Markdown
Member

Stack (root → tip):

Unit 3 of add-vale-binary-packages, stacked on #86.

What is here today

Only the archive: openspec/changes/add-vale-binary-packages/ moves to openspec/changes/archive/2026-08-07-add-vale-binary-packages/, and its deltas are promoted into openspec/specs/ (cli, infrastructure, and a new vale-binary-packages capability).

Splitting the archive out of #86 is what makes the stack legible. #86 was the tip while still carrying an unarchived change directory, so stack: openspec-archived failed there. With this branch on top, #86 is no longer the tip and its gate skips, while this PR carries the archive and satisfies it.

What is still missing

Tasks 6.1, 6.2, 6.3 and 7.3 are unchecked, and they stay unchecked until the work lands here:

  • packages/cli gains the six @taskless/vale-* packages in optionalDependencies, pinned at literal exact stamped versions. Not workspace:*, which would silently re-point at the newest stamp, and not devDependencies, which consumers never install.
  • Verification that a supported platform resolves a binary from the CLI's module context, that an unsupported platform installs cleanly with none present, and that publishing a newer platform package leaves an unchanged CLI on its pinned version.

That pin cannot be written yet. An exact version can only name something that exists on npm, and none of the six names are published. Publishing them the first time needs a manual bootstrap, because a trusted publisher cannot be registered for a package that does not exist.

Sequence to green

  1. Merge feat(vale): add the six Vale platform packages and pinned manifest #72 (the six packages and the pinned manifest).
  2. Merge feat(vale): fetch, verify, and publish the Vale platform packages #86 (fetch, verify, stamp, and the release workflow).
  3. Bootstrap the six @taskless/vale-* names on npm by hand.
  4. Register trusted publishers for each of them on npmjs.com.
  5. Run the release workflow with phase: publish to exercise OIDC end to end and produce a real stamped version set.
  6. Write the optionalDependencies pin on this branch against those stamped versions, check off tasks 6.x and 7.3, and mark this PR ready.

Why this is a draft

Archiving the change before the CLI pin exists would record the work as finished while the piece that makes it useful to consumers is still unwritten. The draft state is the guard: it keeps this branch unmergeable until step 6 above is done. A draft still counts for stack-tip detection, so #86's gate flips regardless.

Fixes OSS-22

Built on top of #72

Publishes the Vale binary as per-platform npm packages from this repo, so a first-class engine isn't a host prerequisite.

This PR now carries unit 1 of a forward-merging stack: the six packages/vale-<platform>/ workspace packages, the committed checksum manifest that pins what goes into them, and the changesets ignore entries that keep release.yml out of their versions. It publishes nothing and no consumer references it. Unit 2 (#86) adds the fetch, verify, stamp, and two-phase release workflow. Unit 3, the CLI's optionalDependencies pin, cannot be written until these names exist on npm.

Why binary-in-tarball

The only existing npm distribution, @vvago/vale, is third-party and downloads at postinstall. That script runs during a consumer's install under a policy we don't set — pnpm 10 blocks dependency build scripts by default — producing no binary and no error. The objection is mechanism, not provenance: it would stand if the Vale project published it. Binary-in-tarball is integrity-hashed, lockfile-pinned, resolves offline, and needs no lifecycle script. So packages/vale-<platform>/ carries the binary with os/cpu declared and no bin, no code, no scripts — ast-grep's packaging without ast-grep's installation, whose hardlink step already fails here under pnpm dlx.

Versioning

An all-prerelease timestamp, <valeVersion>-<yyyymmddhhmmss>, with a plain <valeVersion> never published. That keeps the Vale version legible, means a packaging fix is a new timestamp on the same base rather than a spent version, and — because a prerelease only satisfies a range naming the same major.minor.patch — makes ^3.17.1 provably unable to resolve. Exact pinning stops being a convention someone can drift from.

Binaries are not committed

Six platforms at 10–20 MB each would live in git history permanently, so a published tarball is not reproducible from a plain clone. SHA256 checksums are committed and reviewed, and the pipeline refuses a mismatch, keeping "what can merge to main" as the trust boundary. The workflow that consumes them (#86) runs in two phases — detect upstream on a schedule and open a PR with the new version and checksums, then publish on merge — so nobody has to notice a Vale release and nothing publishes on bytes nobody signed off on. Safe to automate because publishing is inert: the CLI pins a literal exact version, so a new package reaches nobody until that pin is deliberately bumped.

Resolved open questions

The proposal left three open. All three are answered, and the reasoning is written up in design.md under Resolved Questions.

The matrix is six packages, not ast-grep's seven. Vale 3.17.1 publishes exactly six binary assets, and the packages are those six: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-arm64, win32-x64. ast-grep's seventh is win32-ia32, and Vale ships no 32-bit Windows asset, so there is nothing to package.

No libc or toolchain suffix in the namesvale-linux-x64, not vale-linux-x64-gnu; vale-win32-x64, not -msvc. ast-grep carries those suffixes because Rust target triples disambiguate several builds per platform. Vale publishes exactly one build per os/cpu pair, so a suffix would disambiguate nothing while asserting a toolchain nobody verified.

musl stays on the PATH fallback. Upstream publishes no musl asset, so there is nothing to package for Alpine. That is not only a packaging gap: Vale's Linux build is dynamically linked against glibc (verified as dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0), so it is not a static Go binary and would not run on musl even if it were installed there. The linux packages' READMEs say so plainly rather than leaving a user to discover it as a loader error. This matches the existing gap rather than widening it: findSgBinary() maps every Linux to -gnu today, so Alpine already falls through for ast-grep.

Vale 3.17.1 is the pinned version, recorded in .github/scripts/vale-manifest.json beside the scripts that consume it. The manifest holds the version once, and per platform the asset-name template, the archive member to unpack, and the SHA256 of the release archive — upstream's vale_<version>_checksums.txt covers the archives rather than the executables inside them, so a committed digest is independently checkable against upstream and the archive is verified before anything is unpacked from it. Tracking is the detect phase in #86: a weekly schedule opens a PR whenever upstream is ahead, and a security release takes a manual detect dispatch rather than waiting for the cadence.

Which Vale version the CLI pins is a separate decision, made when the CLI's optionalDependencies land in unit 3.

Also carries two CLAUDE.md fixes

Unrelated to Vale but too small to spend PRs on:

  • The PR-reference table documented only TSKL-, reading as though it's the only bare identifier the Linear integration resolves. It isn't — OSS-23 linked and moved to In Review on PR creation for ref(cli): resolve ast-grep without an install-time step #69.
  • A warning to check for a shallow clone before rebasing or force-pushing. git clone --depth=N implies --single-branch, which breaks --force-with-lease on every branch (it fails stale info, so people fall back to a bare --force) and, more quietly, makes git rebase main correct only while the merge base sits inside the shallow window.

Where this sits

This change is the one exception to "one change, one PR": it is stacked, merging forward, with the archive landing on the last unit. The archive gate skips a PR that is not the tip, so stack: openspec-archived is not expected on this PR at all — #86 is the tip, and the change is archived on unit 3.

PR Change Prerequisites
#72 (this PR) Vale binary packages, unit 1 — packages, manifest, changesets ignore none
#86 Vale binary packages, unit 2 — fetch, verify, stamp, release workflow #72
Vale binary packages, unit 3 — the CLI's optionalDependencies pin #86 published
#70 knowledge prompt export none
#71 Vale engine + engine-selection topic a published Vale binary to resolve

partition-rules-by-engine has landed and is archived, so #71's only remaining prerequisite is a published binary from this stack.

#70 and #71 are coupled by exactly one line: whichever lands second adds the engine-selection topic to TOPICS. Ordering between them doesn't matter.

Downstream, the generator's decision router (TSKL-279) needs a published release containing #70 and #71. It consumes a normal release — no prerelease, no path dependency — so it waits without blocking anything here.

skip-changeset stays on this PR. The six packages are in the changesets ignore list and packages/cli is untouched, so there is nothing here for changesets to version or release. Unit 3 is where a changeset belongs, since that is where a published CLI actually changes.

Fixes OSS-22

Built on top of #86

Unit 2 of the add-vale-binary-packages stack. Unit 1 (#72) added six empty packages and the pinned manifest; this adds the pipeline that fills and publishes them, along with the scripts it runs and their tests. Unit 3, the CLI's optionalDependencies pin, is not here and cannot be written until these names exist on npm.

Two phases, because the trust boundary is code review

detect runs on a weekly schedule with no npm credential and no OIDC identity. It compares the latest upstream Vale release against the version pinned in .github/scripts/vale-manifest.json, and when upstream is ahead it opens a pull request updating that version and all six SHA256 digests, taken from upstream's own vale_<version>_checksums.txt. It publishes nothing.

publish runs on the push to main that merges that pull request, once a human has read the digests.

The split is what makes the automation trustworthy. A single job that discovered a digest and then verified its downloads against the digest it had just discovered would verify nothing at all: whatever it downloaded would match, because the digest came from the same fetch. Separating discovery from verification puts a review in between, so nothing publishes on bytes nobody signed off on, and nobody has to notice a Vale release for the process to run.

What bounds a run

The upstream-version comparison, and only that. The "is this version already on npm?" check release.yml uses cannot work here. Every publish stamps <valeVersion>-<yyyymmddhhmmss>, a version npm has never seen, so such a check would answer "not published" on every single run and could never suppress anything. The comparison against upstream is the only thing that can say "nothing to do."

Why prepare and publish are separate jobs

prepare downloads third-party bytes off the internet. It holds contents: read, no environment, and no id-token, so it cannot publish or mint a token regardless of what it downloads. It verifies every archive against the committed digest and aborts the run on a mismatch before anything is unpacked, then hands over npm pack tarballs.

The credentialed publish job therefore only ever handles bytes that already matched a reviewed digest and are already sealed into a tarball. It does not even check out the repository.

Why packing comes before the artifact upload

actions/upload-artifact does not preserve file modes, and the Vale executable has to reach npm with its executable bit set. npm pack records modes inside the .tgz, so packing first and shipping the tarball through the artifact keeps 0755 intact end to end.

Before this can merge

  1. The six package names do not exist on npm yet. An npm trusted publisher is registered per package, and there is nothing to bind until the name exists, so the first publish of each name is a deliberate one-time manual step by a maintainer. The exact procedure is in the workflow file's header comment. Publish the packed tarball, not the package directory: the committed package.json carries the placeholder version 0.0.0 and no binary, so a bare npm publish from a package directory would burn the name on an empty 0.0.0.
  2. Register the trusted publisher for each of the six names on npmjs.com, bound to the npm-production environment. There is no fallback token path in this workflow on purpose.
  3. After merge, run the workflow manually with phase: publish to exercise the OIDC path end to end.

Merging neither PR in this stack publishes anything

The publish phase triggers on a push to main that touches .github/scripts/vale-manifest.json. Unit 1 adds that manifest but no workflow to fire on it, and this PR adds the workflow but does not touch the manifest. The path filter never matches on either merge. The first publish is always deliberate, whether that is the manual bootstrap or a workflow_dispatch.

Known inherited limitation: the detect PR needs a manual check re-run

The detect phase opens its pull request with GITHUB_TOKEN, and GitHub does not fire workflows on events raised by that token. So Validate will not start on a detect PR, and a maintainer has to re-run checks by hand before merging.

This is the same step the changesets "Version Packages" PR already needs. Verified: Validate on #65 ran with run_attempt: 2, re-run manually before it merged. It is a limitation inherited from how GitHub scopes GITHUB_TOKEN, not a defect in this workflow.

Stack

Forward-merging, per the proposal's delivery table. Unit 1 is repository-only and publishes nothing. Unit 2 publishes packages no consumer references yet. Unit 3 pins packages that by then exist.

This PR is a draft because it is the tip of the stack until unit 3 exists, and the OpenSpec archive gate would otherwise ask it to archive a change that is not finished. It also must not merge before the npm bootstrap above.

skip-changeset is correct here for the same reason it is on #72: the six packages are in the changesets ignore list, and packages/cli is untouched. Unit 3 is where a changeset belongs, since that is where a published CLI actually changes.

Refs OSS-22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Archives the add-vale-binary-packages OpenSpec change by moving its artifacts into openspec/changes/archive/2026-08-07-add-vale-binary-packages/ and promoting the change’s spec deltas into openspec/specs/ (including a new vale-binary-packages capability).

Changes:

  • Added the new vale-binary-packages capability spec describing per-platform Vale binary npm packages and their release/verification constraints.
  • Promoted new/updated requirements into existing cli and infrastructure specs to reflect Vale platform-package pinning (via optionalDependencies) and changesets exclusion for script-versioned packages.
  • Added archived change artifacts (proposal, design, tasks, and delta specs) under the dated archive directory.

Reviewed changes

Copilot reviewed 3 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
openspec/specs/vale-binary-packages/spec.md New capability spec defining requirements for Vale per-platform binary packages.
openspec/specs/infrastructure/spec.md Adds a requirement about excluding workflow-versioned packages from changesets.
openspec/specs/cli/spec.md Adds a requirement describing the CLI’s Vale platform packages as exact-version optionalDependencies.
openspec/changes/archive/2026-08-07-add-vale-binary-packages/tasks.md Archived task checklist for the change (including remaining unchecked items).
openspec/changes/archive/2026-08-07-add-vale-binary-packages/specs/vale-binary-packages/spec.md Delta spec capturing added requirements for the new capability.
openspec/changes/archive/2026-08-07-add-vale-binary-packages/specs/infrastructure/spec.md Delta spec capturing added infrastructure requirements (changesets ignore).
openspec/changes/archive/2026-08-07-add-vale-binary-packages/specs/cli/spec.md Delta spec capturing added CLI requirements (Vale optionalDependencies pin).
openspec/changes/archive/2026-08-07-add-vale-binary-packages/proposal.md Archived proposal describing scope, impact, and delivery shape.
openspec/changes/archive/2026-08-07-add-vale-binary-packages/design.md Archived design rationale and decisions for packaging/versioning/release approach.
openspec/changes/archive/2026-08-07-add-vale-binary-packages/.openspec.yaml Archive metadata for the change directory.

Comment thread openspec/specs/infrastructure/spec.md
@thecodedrift
thecodedrift force-pushed the openspec/add-vale-binary-packages-3-archive branch from 4120280 to 631fd3f Compare August 9, 2026 23:28
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @copilot-pull-request-reviewer — "## Pull request overview — Archives the add-vale-binary-packages OpenSpec change by moving its artifacts into openspec/changes/archive/2026-08-07-…"

Addressed. The one inline finding — the ## Requirements header appended to openspec/specs/infrastructure/spec.md — is a false positive, replied to and resolved on its thread: that header is structurally required by the OpenSpec format, and openspec validate --strict fails without it. I confirmed by making the suggested change (valid → failing) and reverting (failing → valid), so no code change was warranted.

This branch was also restacked onto the rewritten #86 tip in 631fd3f; it now carries a single commit over its base instead of four orphaned duplicates, and the archived design.md correctly carries the aarch64 interpreter fix from #72.

— AI Coding Agent

@thecodedrift
thecodedrift force-pushed the openspec/add-vale-binary-packages-3-archive branch from 631fd3f to 867c5f7 Compare August 9, 2026 23:35
Base automatically changed from openspec/add-vale-binary-packages-2-release to main August 9, 2026 23:37
@thecodedrift
thecodedrift force-pushed the openspec/add-vale-binary-packages-3-archive branch from 867c5f7 to 157cd06 Compare August 9, 2026 23:37
@thecodedrift

Copy link
Copy Markdown
Member Author

Blocked by #92.

#92 gates the vale publish path on the pinned Vale version rather than on the manifest file being touched, and amends design.md D5 to explain why a base-version check works where a stamped-version check cannot. Since this PR archives that change, its design.md needs to carry the amended text — so #91 will need a merge from main after #92 lands, otherwise the archived design contradicts the shipped workflow.

Still also waiting on tasks 6.1–6.3 (the CLI optionalDependencies pin), which need the packages published first.

— AI Coding Agent

thecodedrift added a commit that referenced this pull request Aug 10, 2026
…st file

The publish path fires on a push to main touching vale-manifest.json, and a
`paths:` filter cannot see WHY the file changed. A reworded comment, a
reformat, or a digest correction is indistinguishable from a version bump, and
each one published six packages at a fresh <valeVersion>-<timestamp>. Nothing
downstream absorbs that: every stamp is novel by construction, so there was no
second line of defense.

Add a credential-free `gate` job ahead of `prepare` that asks whether the
pinned Vale version is already published, and skips when it is. It runs before
prepare downloads ~60 MB, so a skip is cheap.

design.md D5 argued an already-published check cannot work here. That is true
of the STAMPED version and false of the BASE version, and the difference is the
whole design: "is 3.17.1-20260810000724 published?" is always no, while "has
anything been published for Vale 3.17.1?" is answered by a published 3.17.1 or
any 3.17.1-* stamp. D5 is amended to draw that line, so the gate does not read
as contradicting it.

Two properties kept deliberately:

- An explicit workflow_dispatch passes --force and is never suppressed. A human
  asking for a publish gets one.
- A skip requires ALL six packages to carry the pinned version. Checking one
  would silently skip a half-published set, so the gate doubles as
  partial-release repair.

Blocks #91: that PR archives this change, so its design.md must carry the
amended D5 before it lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
@thecodedrift
thecodedrift force-pushed the openspec/add-vale-binary-packages-3-archive branch from 157cd06 to 1d48afa Compare August 10, 2026 04:39
@thecodedrift thecodedrift added skip-changeset PR intentionally ships no release note (bypasses the changeset requirement) and removed skip-changeset PR intentionally ships no release note (bypasses the changeset requirement) labels Aug 10, 2026
thecodedrift and others added 2 commits August 9, 2026 23:16
Completes tasks 6.1-6.3 of add-vale-binary-packages, which were blocked until
the packages existed on npm. All six are now published at
3.17.1-20260810052605, so the CLI pins that exact version.

The pin is literal, per D8, and that needed checking rather than assuming:
`packages/*` makes the six vale packages workspace members at 0.0.0, so pnpm
could have linked them locally and silently reproduced the `workspace:*`
behaviour D8 rejects. The lockfile records a registry resolution and
node_modules points into the store at the pinned version, so the literal pin
means what it says.

6.2 verified end to end: createRequire from the CLI resolves the package, the
sibling `vale` is mode 755 and reports `vale version 3.17.1`, and only the
host-matching package installs — the other five are filtered by os/cpu.

6.3 verified against two real published stamps rather than by argument: with
dist-tags.latest at 3.17.1-20260810052605, a project pinned to
3.17.1-20260810045147 installs 045147. `^3.17.1` matches no version at all, so
a range cannot float into a prerelease.

Adds the changeset the proposal calls for ("Release impact: minor"). This is
why `skip-changeset` no longer applies to this PR: it was accurate while the
change was docs-only, and the pin makes it a real release — installing the CLI
now also delivers a binary.

7.3 stays open here by necessity; it edits add-vale-rule-engine's tasks, which
exist only on #71.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
@thecodedrift
thecodedrift force-pushed the openspec/add-vale-binary-packages-3-archive branch from efecade to 1133a3d Compare August 10, 2026 06:17
@thecodedrift
thecodedrift merged commit 6b07695 into main Aug 10, 2026
4 checks passed
@thecodedrift
thecodedrift deleted the openspec/add-vale-binary-packages-3-archive branch August 10, 2026 06:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants