Skip to content

feat!: builder runs on one Go binary that the session installs, and a merge releases it (Go port) - #30

Merged
Lutherwaves merged 22 commits into
mainfrom
feat/go-m3
Oct 27, 2026
Merged

Lutherwaves merged 22 commits into
mainfrom
feat/go-m3

Conversation

@Lutherwaves

Copy link
Copy Markdown
Owner

Milestone 3 of the Go port, into feat/go. After this merges, feat/go merges to main once, and that merge is released as builder 3.

What a person can now do

  • Install the binary from the session. When the binary is missing, the session hook offers it once: yes, not now (quiet for 7 days) or never. On yes, the agent runs scripts/install.sh and reports it under ↗ Outside. The hook stays offline.
  • Trust what gets installed. scripts/install.sh downloads the binary of the plugin's version and checks its .sha256. With a signed-in gh, it also checks the build provenance. It then checks that builder version prints the tag. Only then does it install to ~/.config/builder/bin/builder-<v>. A failed check leaves nothing behind. It keeps three versions, so other harnesses keep theirs.
  • Get a clear stop in a skill. budget, reach and security stop when the shim says the binary is not installed or is off, and offer the install once.
  • Release by merging. release-please is gone. release.yml and publish.yml are adapted from matchblox's:
    • go-semantic-release computes the version and the tag is pushed.
    • Four binaries are built, each with a checksum, an SBOM and attestations.
    • The installer runs on Linux and macOS (amd64 and arm64), then the release becomes latest.
    • A bot PR bumps the manifests. It merges only after publish verified the release.

Carried from the milestone 2 review

  1. scripts/install.sh ships at the path that the shim's "not installed" line names, with that text unchanged.
  2. README.md:55 and docs/install.md:3 no longer say Python 3.
  3. Bash 3.2: the shim, the offer and the installer pass under Bash 3.2.57 in a bash:3.2 container. A new macos CI job runs them under /bin/bash.
  4. Paid evals: skipped, as agreed.

Before the feat/go → main merge (owner)

RELEASING.md → "Before the first release":

  1. Allow auto-merge.
  2. Create RELEASE_TOKEN.
  3. Push the seed tag v2.1.0. Without it, go-semantic-release cannot see the old builder-v* tags, and check-next-version.sh refuses the release.
  4. Set RELEASE_ENABLED=true.

Then squash-merge feat/go with a feat!: title, so that the release is 3.0.0.

Calls made without the owner

  • install.answer (<verdict> <epoch>) belongs to the shim and the installer. ledger.json is unchanged.
  • install-answer takes later and never only. Yes is the install itself.
  • opencode runs hooks/session-start, so one script owns the no-binary text.
  • The skill's stop line matches the shim's own messages, not every exit 3. reach matchblox-notify exits 3 on its normal no-route path, and the spec keeps that code.
  • The installer takes BUILDER_RELEASE_URL, for tests and a local release. The checksum and provenance checks still apply.
  • matchblox's latest, Homebrew, docs and site steps are dropped.

Proof

  • bash scripts/validate.sh: ✓ all checks passed.
  • bash tests/test_validate.sh: rc 0.
  • go test -count=1 ./...: ok.
  • actionlint: clean.
  • test_shim, test_offer, test_install: rc 0 under Bash 3.2.57.
  • Local release, end to end:
    • A build stamped with -X main.version=v2.1.0 and its .sha256 installs with gh signed out.
    • With gh signed in, the installer correctly refuses the build, because it has no attestation.
    • The hook with the binary shows no offer. Without the binary, it shows the offer word for word, with empty stderr. After install-answer later, it shows no offer.
  • Not proven until the first real release: the workflows on GitHub (draft, tag, attest, verify on macOS, the bot PR).

🤖 Generated with Claude Code

Lutherwaves and others added 22 commits October 27, 2026 22:44
* docs: plan for Go port milestone 2

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(shim): bin/builder runs with a broken PATH

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(mcpmerge): keep a .mcp.json's key order and numbers

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(hook): the session hook and the opencode plugin run bin/builder

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(harness): setup.sh merges .mcp.json with jq or the builder binary

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills): budget, reach and security run bin/builder

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(templates): the template and report checks in Go

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(evals): the eval runner and parity setup without Python

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor!: builder runs without Python

BREAKING CHANGE: the builder binary is required; milestone 3 adds its installer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(validate): exclude the template test from the old-name guard

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(validate): a failed build is one error, and the budget README pastes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nary is off

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…broken PATH

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…y --version

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…it fails the subject check

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…alled or is off

reach matchblox-notify exits 3 on its normal no-route path, so the rule now keys on the shim's message too.Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the release

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…at to do if it stalls

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ar is a symlink

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… says how to stop retrying

The shim exits 4, not 3, when it cannot read the plugin version, so the
hook shows the soul and the root line without an install block that could
never succeed. The yes block names install-answer later for a failed
install. The snooze has a named constant. Tests cover a plugin with no
version and a binary that prints and fails.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…any other source

curl takes https only, also on a redirect, and gives up after a 10 s
connect or 300 s in all. A BUILDER_RELEASE_URL may also be file://; the
result line names it, and it writes no yes to install.answer. A signed-in
gh with no attestation command reports the provenance as not checked, with
the reason. A missing manifest and an unwritable install.answer each print
only the clean line on stderr.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the last bump, and fail loudly

Publish refuses a tag whose commit is not on main. The release job pushes
the tag through push-tag.sh, which refuses a tag that exists on another
commit, lightweight or annotated. check-bump-merged.sh stops a release while
the previous release's bump pull request is open. The publish wait, a
failed auto-merge, a closed pull request and the 40-minute wait now fail the
job, and merging=true is set only once auto-merge is on. bump-version.sh
takes the same versions as release.yml. RELEASING.md adds the tag ruleset
step and a row for each new refusal. Tests cover the new scripts and the
negative paths of promote-changelog and check-next-version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ter 10 minutes

actionlint v1.7.12 installs through the Go checksum database, so
validate.sh lints the workflows instead of skipping them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and a foreign tag stops the job before a draft

check-bump-merged.sh compares the plugin with the newest tag whose release
is published and verified, and skips a tag whose release is a draft, a
pre-release or missing. A broken release no longer blocks the fixed one.
push-tag.sh --check runs before gh release create, so a tag on another
commit leaves no draft for publish.yml to adopt. The publish main check
uses the origin/main of the full checkout. RELEASING.md says how to clear
a foreign tag and what to do when no bump pull request exists.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Lutherwaves
Lutherwaves changed the base branch from feat/go to main October 27, 2026 20:44
@Lutherwaves Lutherwaves changed the title feat: the session installs the builder binary, and a merge releases it (Go port, milestone 3) feat!: builder runs on one Go binary that the session installs, and a merge releases it (Go port) Oct 27, 2026
@Lutherwaves
Lutherwaves merged commit 31a13ab into main Oct 27, 2026
9 checks passed
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.

1 participant