# Release procedure

This checkout is version 0.3.1. A package name, local manifest, or generated ZIP does not establish a public release. Publication, a Git tag, a push, and account changes require their own authorization. This page describes the reviewable local release artifacts and the checks to run before that decision.

1. Confirm the source commit and clean intended diff. Match `package.json`, the portable plugin, Codex plugin, Claude plugin, marketplace entry, and ZIP name to one release version. Keep `evaluation_version` and `protocol_version` separate from the release version.
2. Run `npm test` and `python -m unittest discover -s tests/python` on Python 3.10 or later. Run the focused `node --test tests/docs-commands.test.mjs` check when changing examples. The command check runs optbrew against isolated local fixtures; it makes no provider call.
3. Build from a checkout made with `git clone -c core.autocrlf=false`. The root and `skills/` carry no `.gitattributes` (the Claude plugin directory refuses one there), so with `core.autocrlf=true` the skill files are CRLF in the working tree; the bundle hashes still match, but the ZIP and npm verifiers compare exact bytes and refuse them. Run `npm run build-bundle`, `node scripts/build-release.mjs --version 0.3.1 --output-dir dist`, `npm pack --pack-destination dist`, `node scripts/verify-release.mjs --dist dist`, and `npm pack --dry-run --json` from the repository root. Rebuild after changing the README or packaged files. Do not treat an older tarball as containing a later documentation edit.
4. Inspect the npm file list, the ZIP file list, `bundle-manifest.json`, `source-lock.json`, licenses, and secret scan output. The same skill bytes and version must be traceable across the npm package and ZIP. Source notices belong in both. User run evidence, account configuration, keys, private cards, and authoring materials must stay outside both.
5. Test a fresh isolated managed install, `doctor`, update from a retained earlier release, rollback, and fresh host discovery or invocation where a host is available. A dry run or manifest check does not prove invocation. Record the untested hosts and capabilities.
6. Confirm CI on the exact commit that will be tagged. That commit exists only after the merge: `main` takes changes through a pull request with linear history, so the merge makes a commit that no earlier run has seen, and runs on the release branch or the pull request do not count. Do this after the merge of step 8 and before its tag. The `test` workflow (`.github/workflows/test.yml`) runs `npm test` on `ubuntu-latest` and on `windows-latest` with `core.autocrlf=true`, Chrome integration tests included, and its last step (`scripts/check-skips.mjs`) fails when a Chrome test was skipped or another test was skipped for a reason that is not listed there. The push to `main` starts the first run. Start two more on the same commit, one after the other, each when the previous one has finished (the workflow keeps one run per commit at a time): `gh workflow run test.yml --ref main`. Then `gh run list --workflow test.yml --branch main --commit <merge commit sha> --json event,conclusion,headSha,url` must list three runs (the `push` run and two `workflow_dispatch` runs) with `conclusion` `success`, each green on both systems. Do not rerun a red run until it turns green: one failed Chrome test (a leftover `gpu-process` of its own Chrome was seen once under load) is a flake to investigate. Read the two `Point optbrew at the runner's Chrome` steps of the newest run and add the Chrome version and path each runner printed to [Chrome locations and DevTools names](#chrome-locations-and-devtools-names-2026-10-16) with the date; also record whether the first Ubuntu run needed any sandbox workaround.
7. Only after a reviewed release decision, and only after step 6 is green on the merge commit, publish or upload the approved artifacts and verify the public npm and repository readback. Do not infer publication from successful local builds. The owner runs these actions, not an agent: `git push`, the merge, the tag and its push, `gh release create`, and `npm publish dist/optbrew-X.Y.Z.tgz`. An agent prepares the artifacts and reads the results back.
8. `main` is protected by a repository ruleset: no force push, no deletion, changes only through a pull request, linear history. Tags matching `v*` cannot be moved or deleted. Merge the release branch through a pull request, then tag the merge commit `vX.Y.Z`. The README's PowerShell installer and agent prompt load `install.ps1` and `AGENT_INSTALL.md` from that tag, so test both raw URLs after the tag is pushed. Do not push the tag before step 6 is green for that merge commit.
9. A tag alone is not a GitHub Release. Publish one for the tag, with the CHANGELOG section as notes and the reviewed ZIP attached: `gh release create vX.Y.Z dist/<zip> --title vX.Y.Z --notes-file <notes.md>`. Read it back: `gh api repos/nowwcastle-sudo/optbrew/releases --jq '.[].tag_name'` must list the tag, and `npm view optbrew dist-tags` must show the same version as `latest`.
10. The Claude marketplace and directory sync on a push to `main`, sent by the repository webhook. After the merge, confirm that the webhook got a push for `refs/heads/main` with the merge commit: `gh api repos/nowwcastle-sudo/optbrew/hooks/<id>/deliveries` lists the delivery, and `.../deliveries/<delivery id>` shows `request.payload.ref` and `request.payload.after`. A branch deletion is answered `ignored_delete` and does not sync. Then check that the claude.ai copy shows the merge commit.
11. The Codex marketplace file is `.agents/plugins/marketplace.json`. After the merge, in a Codex that has the marketplace (`codex plugin marketplace add nowwcastle-sudo/optbrew` adds it), run `codex plugin marketplace upgrade`, then `codex plugin list --marketplace optbrew` and check that `optbrew` is listed as available (`--json` gives a machine-readable form). A listing shows that the marketplace file is readable. It does not show that a Codex task discovers the skill: start a new Codex task, invoke the skill, and record the Codex version used (`codex --version`).

The bundle includes five pinned source bundles plus seven selected marketing guides. The Naver source articles under `references/guide/` (55 original bodies) remain excluded; the index links to official pages and full offline article lookup is unavailable. The published notice must retain upstream MIT/ISC permission texts and modifications. Optbrew's own code uses MIT. These code licenses do not grant service subscriptions or API data rights.

The direct Anthropic adapter keeps its text-only scope and four model IDs in [API boundaries](../skills/optbrew/references/api.md). No live paid result, search indexing, traffic change, or customer adoption is established by a passing package check. See [platform paths](platforms.md) for update mechanisms after release.

## Confirmation records

Values the code relies on that were confirmed from a primary source, with the date of confirmation. Re-confirm before changing the matching file.

### Chrome locations and DevTools names (2026-10-16)

Confirmed for `skills/optbrew/references/chrome-paths.json` and `skills/optbrew/scripts/core/chrome.mjs`:

- Windows `\Google\Chrome\Application\chrome.exe` and `\Microsoft\Edge\Application\msedge.exe`, tried under `%ProgramFiles%`, `%ProgramFiles(x86)%` and `%LocalAppData%`; macOS `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` and `/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`; Linux `/opt/google/chrome/chrome` and `/opt/microsoft/msedge/msedge`. Source: Playwright's channel table and Windows prefix list, https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/registry/index.ts (read 2026-10-16). The Windows prefixes (`LOCALAPPDATA`, `PROGRAMFILES`, `PROGRAMFILES(X86)`) also match Google's `chrome-launcher`, https://github.com/GoogleChrome/chrome-launcher/blob/main/src/chrome-finder.ts.
- Names searched on `PATH`: `google-chrome-stable`, `google-chrome`, `chromium-browser`, `chromium` (Google's `chrome-launcher`, same file); `microsoft-edge-stable` (Playwright's Edge install script, https://github.com/microsoft/playwright/blob/main/packages/playwright-core/bin/reinstall_msedge_stable_linux.sh); `chrome` and `msedge` (the executable names above).
- Playwright's Chromium: `<PLAYWRIGHT_BROWSERS_PATH>/chromium-<revision>/chrome-win64/chrome.exe`, `chrome-linux64/chrome`, `chrome-linux-arm64/chrome`, `chrome-mac-{arm64,x64}/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing`; the value `0` means a package-local directory and is ignored (same Playwright file). A Chromium path on macOS (`/Applications/Chromium.app`) was not confirmed and is not listed.
- `--remote-debugging-pipe`: messages are JSON texts ended by a NUL byte, Chrome reads fd 3 and writes fd 4, so the parent writes `stdio[3]` and reads `stdio[4]`. Sources: Puppeteer's `PipeTransport.ts` and `BrowserLauncher.ts` (https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core/src/node), and Chromium's `content/browser/devtools/devtools_pipe_handler.cc`.
- DevTools Protocol names (`json/browser_protocol.json` of https://github.com/ChromeDevTools/devtools-protocol, master, read 2026-10-16): `Network.ErrorReason` includes `BlockedByClient`; `Network.ResourceType` values `Document`, `Stylesheet`, `Image`, `Font`, `Script`, `XHR`, `Fetch`; `Fetch.enable` takes `patterns` and `handleAuthRequests`; `Fetch.requestPaused` carries `requestId`, `request`, `frameId`, `resourceType`, `redirectedRequestId`; `Fetch.failRequest` takes `errorReason`; `Fetch.authRequired` carries `authChallenge.source` (`Server` or `Proxy`); `Fetch.continueWithAuth` takes `authChallengeResponse.response` (`Default`, `CancelAuth`, `ProvideCredentials`) with `username` and `password`; `Target.createBrowserContext`, `createTarget`, `attachToTarget` (`flatten`), `closeTarget`, `disposeBrowserContext`; `Browser.close`; `Page.setLifecycleEventsEnabled` and the `networkIdle` lifecycle event.
- Auto-attach (same protocol JSON, 2026-10-16): `Target.setAutoAttach` takes `autoAttach`, `waitForDebuggerOnStart`, `flatten` and an experimental `filter` (`TargetFilter`: entries are matched in order, the first match wins, a last `{ exclude: true }` drops every other type); `Target.attachedToTarget` carries `sessionId`, `targetInfo` (`targetId`, `type`, `openerId`, `browserContextId`) and `waitingForDebugger`; a paused target runs after `Runtime.runIfWaitingForDebugger`. Observed with Chrome on 2026-10-16: a page-session auto-attach delivers iframes (cross-site ones run in their own process), dedicated workers and service workers, but not popups, which only a browser-level auto-attach delivers (so the render page claims them by `browserContextId`). A dedicated worker session answers `Fetch.enable` with "'Fetch.enable' wasn't found"; its requests are paused on the session of the frame that owns it.
- Unix socket path limit (Chromium `chrome/browser/process_singleton_posix.cc`, main, read 2026-10-18): the single-instance socket path must be shorter than `sizeof(sun_path)` (108 bytes on Linux; 104 on macOS) or Chrome aborts at start (`LOG(FATAL) << "Socket path too long"`); the socket sits in `<temp folder>/<org.chromium.Chromium.XXXXXX>/SingletonSocket`. `chrome-temp.mjs` therefore redirects Chrome's temp folder into the profile only while that folder path is at most 48 bytes, and otherwise leaves the user's own. First Ubuntu run (CI run 39049765483): every launch from a Hangul profile path and every render test exited at start; the cause was not read from Chrome itself, so the next failing launch prints Chrome's last stderr words in its error message.

### Host resolver rules (2026-10-16)

Confirmed for `skills/optbrew/scripts/core/render.mjs`: Chromium `net/base/host_mapping_rules.h` (chromium/chromium main, read 2026-10-16) defines the rules as a comma separated list of `MAP <hostname_pattern> <replacement_host>[:<port>]` and `EXCLUDE <hostname_pattern>`; the replacement is a hostname or an IP address literal, and a rule that maps to `^NOTFOUND` yields an invalid URL, so the request fails. Observed with the installed Chrome: a host mapped to `^NOTFOUND` fails to load with `net::ERR_NAME_NOT_RESOLVED`. The header does not say how a bracketed IPv6 replacement is parsed, so IPv6 pinning is not covered by a test.

### Harness signals (2026-10-17)

Confirmed for `skills/optbrew/references/harness-signals.json` and `skills/optbrew/scripts/core/probe.mjs`:

- Claude Code: `CLAUDECODE` is set to `1` in subprocesses Claude Code spawns (Bash and PowerShell tools, hooks, status line, stdio MCP servers; IDE extensions set it in their integrated terminals). `CLAUDE_CODE_REMOTE` is set automatically to `true` when Claude Code runs as a cloud session. Both rows are in https://code.claude.com/docs/en/env-vars (read 2026-10-17); the cloud session page, https://code.claude.com/docs/en/claude-code-on-the-web, is the cross-reference. A cloud session also has `CLAUDECODE`, so `CLAUDE_CODE_REMOTE` is tested first.
- Codex: `CODEX_SANDBOX_NETWORK_DISABLED` is set to `1` for a process spawned by a shell tool call when the network sandbox policy is restricted; `CODEX_SANDBOX` is set when the process runs under a sandbox (`seatbelt` on macOS, "may change" for other mechanisms). Source: openai/codex `codex-rs/core/src/spawn.rs`, https://github.com/openai/codex/blob/main/codex-rs/core/src/spawn.rs (read 2026-10-17, constants `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` and `CODEX_SANDBOX_ENV_VAR`; the file defines `CODEX_SANDBOX` but does not set it). The variable is marked experimental in that comment. The Codex docs page `docs/sandbox.md` names neither variable.
- Limits: a Codex run with full network access sets neither variable, so it is detected as `other` and gets the general runbooks. Only `CODEX_SANDBOX_NETWORK_DISABLED` makes a failed DNS lookup a `codex_sandbox` block; with only `CODEX_SANDBOX` set the same failure is `site_down`. No variable separates a Claude Code cloud network level from a down site, so a failed lookup or a refused proxy tunnel in a cloud session is `cloud_network_level`.
