Skip to content

feat(release): accept owner-verified issue-comment evidence for the publish approval gate - #3028

Closed
oscharko wants to merge 7 commits into
devfrom
feat/publish-approval-issue-evidence
Closed

feat(release): accept owner-verified issue-comment evidence for the publish approval gate#3028
oscharko wants to merge 7 commits into
devfrom
feat/publish-approval-issue-evidence

Conversation

@oscharko

@oscharko oscharko commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

GitHub refuses self-approval of one's own pull requests, which makes the publish gate's only evidence form (github-pr-review) structurally unsatisfiable for a solo release owner — the 0.3.0 npm publish was fail-closed forever. By owner decision (2026-08-08, recorded in the working session), the gate accepts a second evidence form at the same strictness: github-issue-comment:<owner>/<repo>#<issue>#<comment>, verified through the GitHub API — comment exists on the referenced issue in this repository, author is in KEIKO_RELEASE_OWNER_GITHUB_LOGINS, and the body carries the literal version-bound phrase Approved-for-publish: <package>@<version>. The pr-review form is untouched; either satisfies the gate. This widens the artifact type, not the trust: every check the pr-review form performs has a same-strength counterpart, and a bare issue link remains insufficient to publish.

Scope

  • In scope: check-release-impact.mjs (new parse/verify path, shared GitHub reader), runbook section, hermetic tests, the 0.3.0 catalog entry binding to the owner's recorded approval (issue release(0.3.0): stabilize the user-reachable UI surface #2802, comment 5225214175).
  • Out of scope: the npm publish itself (runs from dev after merge), any change to the pr-review form.

Reuse And No-Duplication

  • Existing Keiko functionality was inspected before implementation.
  • This PR reuses, extends, generalizes, or consolidates existing functionality where practical.
  • Any new implementation is limited to a documented capability gap in the linked issue.
  • This PR does not introduce a parallel workspace, graph, relationship, policy, evidence, memory, connector, workflow, or UI subsystem where an existing subsystem can be extended.
  • Refactoring or consolidation was considered when existing functionality was close but not shaped for this change.

The GitHub reader is the existing review reader generalized (readGithubResource); validation mirrors the existing review-state validator branch for branch.

Verification

Hermetic: 23/23 in release-impact-governance.test.mjs, including the updated combined-form message pin (failure-first: the old message pin failed against the new code before the update) and the foreign-repository refusal for the comment form. Live, both directions: KEIKO_RELEASE_OWNER_GITHUB_LOGINS=oscharko → PASS end-to-end against the real GitHub API; a foreign login → refuses on exactly the comment author must be an allowed release owner branch. eslint/prettier/check:release-impact green.

Update-Impact

Release-metadata only; no product code, no state stores, no runtime surface. The catalog's 0.3.0 entry moves from the #PENDING#PENDING placeholder to the verified owner approval reference.

Closes nothing; epic #2802.

🤖 Generated with Claude Code

…ublish approval gate (#2802)

GitHub refuses self-approval of one's own pull requests, so a solo
release owner can never mint the github-pr-review artifact the publish
gate demands — the 0.3.0 npm publish was structurally unsatisfiable.
Owner decision (2026-08-08): add a second evidence form at the same
strictness, not a bypass. github-issue-comment:<owner>/<repo>#<issue>#
<comment> verifies through the GitHub API that the comment exists on
the referenced issue in this repository, is authored by a login in
KEIKO_RELEASE_OWNER_GITHUB_LOGINS, and contains the literal
version-bound phrase "Approved-for-publish: <package>@<version>".
The pr-review form is unchanged and either satisfies the gate.

The 0.3.0 catalog entry binds to the owner's recorded approval
(issue #2802, comment 5225214175). Verified live in both directions:
the correct owner login passes, a foreign login refuses on the
comment-author branch, and the hermetic suite pins the parse and
repository boundaries (23/23).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oscharko
oscharko temporarily deployed to keiko-for-quality August 8, 2026 07:56 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@oscharko, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 62eeafcb-5139-4bd3-847b-931aa52375bd

📥 Commits

Reviewing files that changed from the base of the PR and between 211149d and c34c6b9.

📒 Files selected for processing (5)
  • docs/release/release-impact-runbook.md
  • docs/release/release-publish-workflow.md
  • release-impact.catalog.json
  • scripts/__tests__/release-impact-governance.test.mjs
  • scripts/check-release-impact.mjs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@keiko-for-quality

keiko-for-quality Bot commented Aug 8, 2026

Copy link
Copy Markdown

Keiko for Quality — run summary

COMPLETE · head c34c6b9 · 2026-08-08T12:40:21Z · engine v1.8.4 · action 8aafc617614f06d9c90be6fb32fda112ccc1cf78

Metric Count
Total paths 5
Reviewable 3
Excluded 2
Mechanically clean 0
Critical pointer changes (content not reviewable) 0
Replayed from cache 3
Cache miss (path-set shape changed) 0
Freshly reviewed 0
Findings published 0
Suppressed (intra-run duplicate) 0
Suppressed (exact duplicate) 0
Suppressed (similar) 1
Suppressed (dispositioned) 0
Suppressed (outdated recurrence) 0
Rejected (sanitization) 0
Rejected (placement) 0
Read-back failures 0
API failures 0
Duration (s) 3

Budget: 150000 tokens allotted

Recent runs

  • c34c6b9 · complete · fresh 0 · replayed 3 · 3s
  • b02cae5 · complete · fresh 3 · replayed 0 · 16s
  • 736e67d · complete · fresh 3 · replayed 0 · 32s
  • 60b26c9 · complete · fresh 0 · replayed 3 · 2s
  • 49416db · complete · fresh 0 · replayed 3 · 2s

@oscharko
oscharko temporarily deployed to keiko-for-quality August 8, 2026 07:56 — with GitHub Actions Inactive
@oscharko
oscharko enabled auto-merge (squash) August 8, 2026 07:57

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50e027ec4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/check-release-impact.mjs Outdated
Comment thread docs/release/release-impact-runbook.md
Comment thread release-impact.catalog.json
Comment thread scripts/check-release-impact.mjs Outdated
Comment thread scripts/__tests__/release-impact-governance.test.mjs
@oscharko
oscharko disabled auto-merge August 8, 2026 08:17
…njected reader (#2802)

SonarCloud measured the new evidence path at 39% new-code coverage: the
GitHub API branches were hermetically unreachable. The reader is now an
injectable seam (withGithubResourceReader, swap-and-restore), and every
branch is pinned: verified owner comment accepted, wrong issue
refused, missing phrase refused with the demanded phrase named, foreign
author refused, unreadable resource fails closed.

The approval phrase also binds to the catalog ENTRY under validation
(packageName@packageVersion of the record being approved) instead of
re-reading package.json — the fixture-derives-from-producer rule
applied to the gate itself: the phrase can only ever demand the exact
package identity the gate is judging.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oscharko
oscharko temporarily deployed to keiko-for-quality August 8, 2026 08:17 — with GitHub Actions Inactive
@oscharko
oscharko temporarily deployed to keiko-for-quality August 8, 2026 08:18 — with GitHub Actions Inactive
@oscharko
oscharko enabled auto-merge (squash) August 8, 2026 08:19
@oscharko
oscharko temporarily deployed to keiko-for-quality August 8, 2026 08:20 — with GitHub Actions Inactive
@oscharko
oscharko temporarily deployed to keiko-for-quality August 8, 2026 08:21 — with GitHub Actions Inactive
@oscharko
oscharko temporarily deployed to keiko-for-quality August 8, 2026 08:59 — with GitHub Actions Inactive
@oscharko
oscharko temporarily deployed to keiko-for-quality August 8, 2026 08:59 — with GitHub Actions Inactive

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60b26c97b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/check-release-impact.mjs
Comment thread scripts/check-release-impact.mjs
@oscharko
oscharko disabled auto-merge August 8, 2026 09:12
oscharko added a commit that referenced this pull request Aug 8, 2026
…udget

The success-path tests exercised the fixture executable under the
PRODUCTION 2-second spawn budget, which a saturated 4-core CI runner
can exceed — measured twice in a row on the same loaded run (shard
packages 3/3 on #3028), while every idle run stays green. Protocol
tests now inject a generous 30s budget; the bound itself keeps its
dedicated hang test at 250ms, which is the only place the budget IS
the assertion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ew findings (#3028)

- The approval phrase must stand on a line of its own: a substring match
  read a quoted denial ("DO NOT use Approved-for-publish: ...") as an
  affirmative approval.
- Live GitHub verification runs only for the release being published NOW;
  historical approvals are mutable artifacts and re-fetching them on
  every future publish would brick an append-only catalog the moment one
  is edited or deleted. Structural reference validation stays for every
  retained entry.
- One issue-comment artifact authorizes exactly one catalog record —
  scoped to the comment form deliberately, since unchanged staging
  contracts reuse their historical PR-review reference across versions
  by documented practice.
- The publish workflow doc now describes both accepted evidence forms,
  and the 0.3.0 rationale describes the artifact it actually references
  (owner-authored issue comment, not a backfilled PR review).
- Hermetic pins for all of the above: quoted-denial rejection, duplicate
  artifact rejection, and a reader spy proving historical entries are
  never re-fetched while the current release still verifies live.
@oscharko
oscharko temporarily deployed to keiko-for-quality August 8, 2026 11:54 — with GitHub Actions Inactive
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

Comment thread scripts/check-release-impact.mjs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 736e67d2f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/check-release-impact.mjs
Comment thread release-impact.catalog.json
Comment thread docs/release/release-impact-runbook.md
@oscharko
oscharko temporarily deployed to keiko-for-quality August 8, 2026 12:27 — with GitHub Actions Inactive
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@oscharko
oscharko temporarily deployed to keiko-for-quality August 8, 2026 12:28 — with GitHub Actions Inactive
@oscharko
oscharko temporarily deployed to keiko-for-quality August 8, 2026 12:40 — with GitHub Actions Inactive
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@oscharko
oscharko temporarily deployed to keiko-for-quality August 8, 2026 12:40 — with GitHub Actions Inactive
@sonarqubecloud

sonarqubecloud Bot commented Aug 8, 2026

Copy link
Copy Markdown

@oscharko
oscharko enabled auto-merge (squash) August 8, 2026 12:45

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c34c6b9ccc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/release/release-impact-runbook.md
@oscharko
oscharko disabled auto-merge August 8, 2026 13:02
oscharko added a commit that referenced this pull request Aug 8, 2026
…integration (#3037)

* deps(deps-dev): bump postcss from 8.5.23 to 8.5.25

Bumps [postcss](https://github.com/postcss/postcss) from 8.5.23 to 8.5.25.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.23...8.5.25)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.25
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* deps(deps-dev): bump jsdom from 29.1.1 to 30.0.1

Bumps [jsdom](https://github.com/jsdom/jsdom) from 29.1.1 to 30.0.1.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](https://github.com/jsdom/jsdom/compare/v29.1.1...v30.0.1)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 30.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* deps(deps-dev): bump @playwright/test from 1.62.0 to 1.62.1

Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.62.0 to 1.62.1.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.62.0...v1.62.1)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.62.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* deps(deps-dev): bump vite from 8.1.4 to 8.2.0

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.1.4 to 8.2.0.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.2.0/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* deps(deps-dev): bump @vitejs/plugin-react from 6.0.3 to 6.0.5

Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 6.0.3 to 6.0.5.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.5/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* feat(release): accept owner-verified issue-comment evidence for the publish approval gate (#2802)

GitHub refuses self-approval of one's own pull requests, so a solo
release owner can never mint the github-pr-review artifact the publish
gate demands — the 0.3.0 npm publish was structurally unsatisfiable.
Owner decision (2026-08-08): add a second evidence form at the same
strictness, not a bypass. github-issue-comment:<owner>/<repo>#<issue>#
<comment> verifies through the GitHub API that the comment exists on
the referenced issue in this repository, is authored by a login in
KEIKO_RELEASE_OWNER_GITHUB_LOGINS, and contains the literal
version-bound phrase "Approved-for-publish: <package>@<version>".
The pr-review form is unchanged and either satisfies the gate.

The 0.3.0 catalog entry binds to the owner's recorded approval
(issue #2802, comment 5225214175). Verified live in both directions:
the correct owner login passes, a foreign login refuses on the
comment-author branch, and the hermetic suite pins the parse and
repository boundaries (23/23).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(release): cover every issue-comment approval branch through an injected reader (#2802)

SonarCloud measured the new evidence path at 39% new-code coverage: the
GitHub API branches were hermetically unreachable. The reader is now an
injectable seam (withGithubResourceReader, swap-and-restore), and every
branch is pinned: verified owner comment accepted, wrong issue
refused, missing phrase refused with the demanded phrase named, foreign
author refused, unreadable resource fails closed.

The approval phrase also binds to the catalog ENTRY under validation
(packageName@packageVersion of the record being approved) instead of
re-reading package.json — the fixture-derives-from-producer rule
applied to the gate itself: the phrase can only ever demand the exact
package identity the gate is judging.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ui): load a keiko.config.json on the gateway setup page (#2802)

Owner-directed for 0.3.0-beta.2: teams that already maintain a
model-gateway configuration file load it on the first setup page
instead of retyping every field. The parser runs entirely in the
browser, is fail-closed (any structural violation refuses the whole
file rather than applying half of it), and the parsed values land in
the exact same form state — and therefore the same validation and
one-time token test — as manual entry. Whatever the file does not
carry (typically the API token) stays manual.

The control follows the design system (component-scoped .cmp-* classes
in the dialog's CSS module, tokens only, forced-colors variant), is
fully keyboard- and SR-reachable (labelled file input, role=alert /
output status), and localized in both catalogs. The parser module is
deliberately lowercase-distinct from the component file: the two names
differed only by case, which resolved to the wrong module on
case-insensitive filesystems — measured, then renamed.

Coverage baseline extends by exactly the keiko-ui entry (418 -> 420
source files); editor bundle evidence regenerated via the documented
build:ui + editor-release-evidence flow. 145/145 modal tests, 12
parser pins including hostile-input refusals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): name the chat-creation rejection parameter error_ (S7718)

Closes the one open SonarCloud finding (AZ-sNvyxhylc2f8zV9LK,
owner-requested in this PR): the rejection handler's parameter follows
the repository's error_ naming convention. Behavior unchanged; 42/42
widget tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): close two review findings and restore the CI-anchored bundle evidence (#3031)

- The disabled look of the upload action uses an explicit class toggle
  instead of :has(input:disabled), so it cannot depend on selector
  support the deployed browser may lack (reviewer finding, confirmed).
- file.text() rejections resolve to the same visible "invalid" outcome
  instead of escaping as an unhandled rejection (reviewer finding,
  confirmed); handler extracted to hold the function-size bar.
- docs/release/1209-bundle-evidence.json returns to the committed CI
  fingerprint: the local regeneration replaced CI-anchored evidence
  with macOS-measured values — the exact machine-class trap AGENTS
  documents — and CI proved the UI change does not move the CI
  fingerprint at all.

145/145 modal tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(deps): bump github/codeql-action/init from 4.37.1 to 4.37.4

Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.37.1 to 4.37.4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/7188fc363630916deb702c7fdcf4e481b751f97a...f205ea1c3313d32999d8d6a48b4f6530d4437b38)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* ci(deps): bump github/codeql-action/analyze from 4.37.1 to 4.37.4

Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.37.1 to 4.37.4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/7188fc363630916deb702c7fdcf4e481b751f97a...f205ea1c3313d32999d8d6a48b4f6530d4437b38)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(ui): close the nine review findings on the config upload (#3031)

Parser (all fail-closed, each pinned): providers that disagree on any
connection scalar refuse the whole file — one connection per upload;
top-level capability records are authoritative over inline ones,
malformed records refuse the file (same policy as providers), and ids
no imported provider carries refuse it too (an untestable id would
fail Test & Save AFTER a reported success); voice/OCR provider kinds
report their own honest "cannot represent" outcome; an explicitly
empty eligibility declaration clears the form field like manual
emptying; a supported figma.accessToken carries through.

Upload control: a superseded file selection can never overwrite a
newer one (sequence token), and read failures resolve to the visible
"invalid" state.

Server (the security finding, fixed for the whole class): in update
mode a changed gateway URL never inherits the stored token — the
refusal compares CANONICAL endpoint identities so equivalent spellings
keep working, lives server-side so no client path bypasses it, and the
voice connection's existing replace-guard is pinned as covering the
same class (a redundant second guard was measured and removed).

154/154 modal tests, 181/181 gateway-setup tests, sonar clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(release): automate the portable evaluation prerelease end to end (#2802)

Owner order after the beta.0 -> beta.1 cycle: every publishing lesson
lives in the release tooling so none is relearned by hand. One command
(release:prerelease) now dispatches the evaluation build, refuses runs
with failed or missing staging jobs, downloads exactly the four-asset
publish set (drift refuses), embeds locally computed SHA-256 checksums,
verifies the arm64 bundle the way Gatekeeper judges it on a darwin host
(codesign --verify --deep --strict, pinning beta.0's exact "damaged"
signature text) and states the skip out loud anywhere else, publishes
the prerelease with provenance (commit + run id), auto-increments the
beta tag, and prepends a superseded pointer to the predecessor.

Proven live before landing: a plan-only run against the real beta.1
workflow run reproduced the manually published checksums byte for byte
and passed the real seal verification. 13 hermetic pins cover the tag
arithmetic, argument surface, release body (GUI-only approval steps —
never a terminal command), and the encoded lessons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(quality): except codesign's absolute path from knip unresolved-import resolution (#3032)

knip's script parser treats the absolute spawnSync path
/usr/bin/codesign as an import specifier and resolves it on disk, so
the gate passed on darwin and failed on the Linux CI runner — a
platform-dependent verdict. The narrow ignoreUnresolved entry names
exactly this path; ignoreBinaries already carried it for the wrapped
call sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(security): give the keychain protocol tests a load-proof spawn budget

The success-path tests exercised the fixture executable under the
PRODUCTION 2-second spawn budget, which a saturated 4-core CI runner
can exceed — measured twice in a row on the same loaded run (shard
packages 3/3 on #3028), while every idle run stays green. Protocol
tests now inject a generous 30s budget; the bound itself keeps its
dedicated hang test at 250ms, which is the only place the budget IS
the assertion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): close the sixth review wave and the image-capability removal class (#3031)

Parser: a present-but-malformed inline capability refuses the file
(production-parser parity); the setup route's 100-provider ceiling is
mirrored at upload time; endpoint style / API version / output token
parameter refuse with their own honest "cannot represent" outcome
while retry tuning stays tolerated. Upload control: an in-flight file
read blocks submission (the dialog can no longer snapshot a
half-applied form), threaded via onReadPendingChange.

Server, the deepest find of the wave: image capability could NEVER be
removed once stored — the image list was additive at three layers.
Now the wire distinguishes absent (inherits) from explicitly empty
(clears), a present list routes through the verified rebuild instead
of the settings-only patch, an explicitly provided list stops
discovery from re-adding just-removed models, and the provided list is
authoritative in the capability rebuild. Pinned end to end: absent
inherits, empty clears, 182/182.

The upload feature loads as its own chunk: the setup page returned
under the static-export first-load budget (measured with the gate's
exact --require-static-export invocation) instead of raising it.

157/157 modal tests, sonar clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): the pending-read flag follows the current token on every path (#3031)

A superseding selection that failed the size check advanced the token
without starting a read, so the superseded read's stale-return could
never clear the pending flag and the dialog stayed blocked. The size
branch now owns the flag for its token. The reviewer's suggested fix
(clearing on the stale return) would have been wrong the other way —
it would unblock submission while the NEWER read is still in flight;
the flag's contract is "a read for the CURRENT token is in flight".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): report a throwing apply callback instead of leaking a rejection (#3031)

A throwing onApply is a programming error, not a user problem: it now
routes through window.reportError (the page's established channel,
never an unhandled rejection out of a void handler) and shows the
honest failed state instead of a success count.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(release): drive the prerelease orchestration hermetically through seams (#3032)

SonarCloud measured the orchestration at 26% new-code coverage: every
gh-, codesign- and platform-bound path was untestable. The script now
carries three swap-and-restore seams (process runner, host platform,
and a throwing fail() with the CLI shim owning the exit), and a
scripted gh double drives the complete flow end to end: the happy
publish with checksums, provenance and the supersede pointer; the
failed-staging refusal; the drifting-asset-set refusal; the beta.0
damaged-signature refusal; and the stated (never silent) non-darwin
seal skip. The real path stayed byte-identical: the live plan-only
rehearsal against the beta.1 run reproduces the published checksums.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gateway): import voice sections from uploaded configs and close the wave-7 findings (#3031)

The parser refused the exact persisted keiko.config.json this feature was
built for: voice providers tripped the unsupported-setting refusal. Voice
providers now map onto the dedicated voice setup fields (STT/TTS/realtime
roles from stored capability flags, one voice connection per submit); a
realtime provider on a different connection than the speech pair is
skipped with a visible message instead of silently or refusing the file.

Wave-7 review repairs:
- P1: preserve-mode rebuilds classify inherited/resubmitted deployments
  by their STORED capability kinds before the name heuristic — a stored
  embedding provider with a non-heuristic id no longer vanishes when an
  unrelated update (image clear, credential rotation) rebuilds the config.
- Upload parser: reject present-but-malformed scalars and capability
  flags instead of coercing, reject mixed stated/omitted connection
  scalars, mirror the route's model-id constraints, and derive the
  image/workflow flag lists from chat capabilities only.

* fix(ui): harden the config upload against parser regressions and unbounded payloads (#3031)

Wave-8 review repairs: the parser enforces its own size ceiling ahead of
JSON.parse (the upload control's file.size check is no longer the only
guard), and a parser throw — impossible today, pinned via a mocked
regression — reports through the page error channel and shows the honest
invalid state instead of crashing the component.

* chore(release): bump static-export file count for the split upload chunk (#3031)

The dynamic() split of GatewayConfigUpload adds one file to the static
export; the CI-anchored measurement fingerprint and every editor byte
field are untouched — the Linux run confirmed only fileCount drifted.

* fix(gateway): close the wave-9 findings — voice fidelity on import, stored OCR survival (#3031)

- P1 (server): a preserve-mode verified rebuild silently dropped stored
  ocr-vision providers — the rebuild only re-derives chat and embedding.
  Stored OCR ids now leave the candidate set (never chat-probed) and the
  providers are restored verbatim from the current configuration, exactly
  like voice; pinned with a probe spy plus both fix halves sabotage-proven.
- P1 (import): a speech-output provider with voice profiles now carries an
  output voice into the form (uniform profiles losslessly, several voices
  reduce to the neutral persona WITH a visible message) — without it every
  fresh Test & Save failed 400 on a file that carried the information.
- Multi-role realtime providers fill unclaimed speech roles (the route
  merges roles per model id); a skipped realtime claims nothing.
- Voice locality imports uniformly or refuses; inline capability ids must
  match their provider; malformed figma/circuitBreaker/voiceProfiles
  blocks refuse; a tuned circuit breaker is honestly unsupportedSetting;
  voice-only files refuse instead of loading an unsubmittable form.

* perf(ui): load the gateway setup dialog lazily from the settings panel (#3031)

The settings panel's static import pulled the whole dialog into the
first-load chunk — the voice-import surface pushed it 273 B over the
340992 B ceiling. The dialog is gesture-only there (ADR-0042 D3.6),
exactly like the shell's own modals, so it now loads behind the same
next/dynamic boundary; the static-export file count advances to 335.

* test(release): prove every refusal path of the prerelease orchestration hermetically (#3032)

Sonar held the new script to the 85% new-code bar and was right: the
dispatch/poll path, the run seams' failure modes, tag collisions,
cancelled runs, first-beta and already-superseded predecessors, nested
artifacts, incomplete staging sets, a corrupted copy step, and the
non-damaged codesign failure were all unproven. A withSleeper seam makes
the polling paths hermetic (no wall-clock waits); the CLI shim is pinned
end-to-end via a real subprocess. 32 tests, script coverage 96.6% lines /
87.7% branches — uncovered remains only the Atomics sleeper body and the
direct-invocation shim, both exercised outside instrumentation.

* fix(gateway): close the wave-10/11 findings — byte-accurate ceiling, OCR credential rotation, lazy upload copy (#3031)

- Upload parser: the size ceiling measures bytes (TextEncoder), not UTF-16
  units; unknown top-level policy blocks (grounding, reranker, egress)
  refuse as unsupportedSetting instead of being silently dropped on save;
  voice-profile personas validate against the known set and reject
  duplicates; the circuit-breaker extra-key guard uses Object.hasOwn.
- Server: a restored same-endpoint OCR provider follows a credential
  rotation (the fresh token never travels to a different endpoint), and
  OCR restoration applies only to INHERITED deployments — an explicitly
  submitted deployment list can remove OCR providers again.
- The upload control's copy moves to the lazily-loaded optional widget
  catalog and pluralizes per the catalog convention — the eager first-load
  catalogs shrink instead of growing (initial-page ceiling, ADR-0042 D3.6).

* ci(deps): admit MIT-0 in the permissive license allowlist (#3034)

The collected pdfjs/csstools updates pull two transitive packages
(@csstools/color-helpers, @csstools/css-syntax-patches-for-csstree)
published under MIT-0 — MIT without the attribution clause, strictly
more permissive than the MIT, Unlicense, and WTFPL entries already
admitted. The allowlist stays permissive-only; no per-package exception
is needed.

* test(server): satisfy exactOptionalPropertyTypes in the OCR rotation helper (#3031)

The savedOcr map builds entries with explicit undefined kinds; the
declared optional-property type rejected that under
exactOptionalPropertyTypes on the full-graph typecheck.

* fix(gateway): close the wave-13 findings — dedicated embedding connections survive rebuilds (#3031)

- Server (P1): an embedding provider on its OWN endpoint was silently
  rewritten onto the setup-wide gateway connection by any preserve-mode
  rebuild. Dedicated-endpoint embeddings now leave the candidate set and
  are restored verbatim like stored OCR (failure-first pin: connection,
  credential, and kind survive an unrelated image clear, never probed).
- Import: unsupported API key header names refuse at upload time
  (mirroring the gateway's supported set, case-insensitively) instead of
  failing Test & Save after a reported success.
- Settings panel: a failed dialog chunk load now surfaces the redacted
  loading error with a retry control instead of rendering nothing.

* fix(ui): keep uploaded-file content out of the error channel (#3031)

Engine error messages can embed excerpts of the parsed input, and the
uploaded gateway config may carry credentials — both reportError sites
now emit a static, body-free message instead of forwarding the caught
error (the mocked-parser pin asserts the sanitized message).

* fix(release): harden the issue-comment approval gate against its review findings (#3028)

- The approval phrase must stand on a line of its own: a substring match
  read a quoted denial ("DO NOT use Approved-for-publish: ...") as an
  affirmative approval.
- Live GitHub verification runs only for the release being published NOW;
  historical approvals are mutable artifacts and re-fetching them on
  every future publish would brick an append-only catalog the moment one
  is edited or deleted. Structural reference validation stays for every
  retained entry.
- One issue-comment artifact authorizes exactly one catalog record —
  scoped to the comment form deliberately, since unchanged staging
  contracts reuse their historical PR-review reference across versions
  by documented practice.
- The publish workflow doc now describes both accepted evidence forms,
  and the 0.3.0 rationale describes the artifact it actually references
  (owner-authored issue comment, not a backfilled PR review).
- Hermetic pins for all of the above: quoted-denial rejection, duplicate
  artifact rejection, and a reader spy proving historical entries are
  never re-fetched while the current release still verifies live.

* fix(ui): refuse duplicated top-level capability ids in uploaded configs (#3031)

A repeated id silently overwrote the earlier declaration — corrupted
input now refuses the whole file like every other malformed record.

* test(security): pin the omitted-timeout production bound deterministically (#3033)

The read-success protocol test joins the load-proof budget, and the two
tests whose names promised the omitted-timeout default are renamed to
what they actually exercise. The default itself gets REAL pins on both
the read and write paths: the one call that omits timeoutMs runs against
the hanging fixture on purpose — a fast fixture would flake on a
saturated runner (the incident this file hardens against), while the
hang deterministically proves the production bound fires.

* fix(gateway): close the wave-16 findings — every untouched block and connection survives rebuilds (#3031)

- Server (P1): the rebuilt configuration copied only grounding and figma —
  a configured reranker vanished and the persisted config lost its egress
  topology after restart. Every top-level block the rebuild does not
  produce now survives from the current configuration.
- Server (P1): dedicated-embedding identity compares the FULL stored
  connection (endpoint, credential, header) against the stored primary
  provider — a same-endpoint embedding with its own token is restored
  verbatim instead of being rebuilt with the gateway credential, and the
  verbatim-restore token refresh only follows a rotation for providers
  that SHARED the stored gateway connection.
- Server: stored voice ids leave the chat-probe candidate set during
  inherited rebuilds — a succeeding probe persisted a DUPLICATE provider
  next to the restored voice entry.
- Import: a capability flag may only be TRUE on the kind that owns it
  (canonical-parser parity; explicit false stays tolerated everywhere),
  and only a CHAT capability makes the chat flag lists configured — an
  embedding-only declaration no longer clears stored chat flags.
- Dialog: a failed upload-chunk load surfaces the redacted error and a
  retry instead of silently hiding the feature.

All three server repairs are failure-first pinned (probe spies included).

* test(server): pin blank submitted tokens against the endpoint-change guard (#3031)

trimmedSubmittedString already normalizes empty and whitespace-only
strings to undefined, so the exfiltration guard fires — this pin keeps
that equivalence from ever regressing.

* Revert "merge: dependabot #3016 bump @vitejs/plugin-react from 6.0.3 to 6.0.5"

This reverts commit 6fc686af647689e59a49d093fbbf1af837532b5a, reversing
changes made to 21e5e7d52601870d53d979f5938c12930e599f39.

* Revert "merge: dependabot #3015 bump vite from 8.1.4 to 8.2.0"

This reverts commit 21e5e7d52601870d53d979f5938c12930e599f39, reversing
changes made to 2194c2f5743efbdc9f351f31dadd52af01733456.

* fix(release): bind the prerelease tag to the built commit and never orphan the workdir (#3032)

- The release tag targets view.headSha — the exact commit the workflow
  built — instead of the branch ref, so assets and release source cannot
  disagree when the branch advances mid-run or an older --run-id is
  reused (pinned: --target carries the run SHA, never the ref).
- The mkdtemp work directory is removed in a finally block covering every
  exit: refusals through fail() and plan-only runs included (pinned via
  existsSync after a codesign refusal and after a plan-only run).

* fix(ui): close the wave-19 import findings — retry parity, role-bound tuning, lenient defaults (#3031)

- Generic retry tuning must match what the rebuild writes ({2, 500}) or
  the file refuses as unsupportedSetting — a different value would be
  silently rewritten on save. Voice providers stay exempt: the voice
  route inherits their stored tuning.
- Semantic turn detection without realtime support and voice profiles on
  a provider without an output role refuse as corrupted input.
- A partial circuitBreaker block whose omitted keys mean the rebuilt
  defaults now imports instead of refusing.

* fix(release): never orphan the workdir when asset assembly itself refuses (#3037)

A refusal inside downloadAssets (missing artifact, drifting publish set)
threw before the caller's finally existed — the mkdtemp directory now
cleans up on that path too, pinned via existsSync after the
missing-asset refusal.

* fix(release): write the release body via fs, not a shell pipeline (#3037)

CodeQL flagged the sh -c 'cat > path' construction; a direct
writeFileSync removes the shell from the path entirely.

* fix(gateway): patch image flags in place and assert imported embedding kinds (#3037)

- P1: a flags-only image edit (clear or shrink to already-verified ids)
  no longer takes the verified rebuild — it patches the stored capability
  flags in place exactly like workflow eligibility, so a transient smoke
  failure of an unrelated model can never delete that provider during a
  metadata edit (sabotage-proven pin with a flaky-probe double). A NEW
  image claim still demands the vision-probing rebuild (pinned).
- P1: fresh setups accept a client-asserted embeddingModelIds list —
  validated like every deployment list — so an imported embedding whose
  id defies the name heuristic is classified by its declared kind
  instead of being chat-probed and dropped (sabotage-proven pin). The
  upload importer derives the list from the file's capability records
  and the dialog carries it through the payload invisibly.
- The rebuild-class pins whose only trigger was the image clear now ride
  the credential-rotation trigger (the clear is a patch, by design).

* fix(release): bind the prerelease to the built commit's version and its own dispatched run, and keep fenced approval phrases from approving (#3037)

Three review fixes on the 0.3.0 release tooling:

- release-portable-prerelease reads package.json at the workflow run's
  head commit and refuses to publish when that version disagrees with
  the local checkout or when --tag does not name the built version — an
  older --run-id can no longer put another package version's assets
  under a v<version>-beta.N tag.
- dispatchWorkflow captures the pre-dispatch run-id set and polls
  (bounded, through the sleeper seam) until a run appears that was not
  in it, instead of trusting 'gh run list --limit 1' to name the run it
  just created — a previous or concurrent run can sit at the top of
  that list.
- check-release-impact's standalone-line approval check now tracks
  Markdown code fences (backtick and tilde) and blockquotes: a fenced
  example or quoted citation of the Approved-for-publish phrase never
  grants the approval; only a plain standalone line does.

The prerelease fixture tags now derive from the checkout's real
version, so the version-binding happy paths stay green across bumps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(release): verify both macOS seals, copy the publish set via node:fs, and derive approval fixtures from the producer (#3037)

Nine reviewer findings on the portable-prerelease script, its suite, the
release-impact gate suite, and the runbook:

- verifyMacosSeal now codesign-verifies BOTH macOS archives (arm64 and
  x64) — codesign judges the x64 seal statically on an arm64 host — and
  the release body names exactly what was verified; a damaged x64 seal
  refuses the publish (failure-first: the new test showed the old code
  publishing).
- The publish-set assembly uses mkdirSync/copyFileSync behind a new
  withAssetCopier seam instead of /bin/mkdir and /bin/cp spawns; the
  stray-file guard proof is rewired through the seam and still goes red
  when the guard is deleted.
- The accepted "failure" run conclusion is now explained in place: it
  aggregates non-gating lanes while assertStagingJobsSucceeded strictly
  judges the asset-producing jobs.
- The suite resolves the production script path once from
  import.meta.dirname for both the source read and the CLI-shim spawn.
- Source-text pins with behavioral twins are pruned; the four-asset
  publish set is now pinned behaviorally in the create line, and only
  the evaluation-dispatch and skip-warning pins remain as text.
- The non-darwin test asserts the rendered "skipped-non-darwin" line in
  the plan-only release body.
- publishApprovalPhrase is exported and every approval fixture body in
  the governance suite derives from it; the accepting tests also assert
  result.ok.
- The runbook states the approval phrase must stand on a trimmed line of
  its own, outside code fences and blockquotes, matching the gate.

* test(ui): pin the upload guards — sanitized apply-callback failure, read-pending contract, chunk-safe upload queries (#3037)

GatewayConfigUpload.test.tsx: the module mock now passes through to the real
parser by default (the parser-regression pin installs its throw for exactly one
call), adding coverage for two guards the component carries: a throwing onApply
is reported sanitized (exact message equality, no uploaded file content in the
error channel) with the honest invalid state instead of a success count, and the
onReadPendingChange contract — true before the file read starts, false after it
(invocation-order asserted against a Blob.text spy), with the oversize path
ending the flag false and provably never reading or parsing. Each guard was
proven failure-first: removing it turns the matching assertion red.

GatewaySetupDialog.test.tsx: three upload tests queried the lazily loaded
upload control synchronously and only passed because earlier tests had warmed
the dynamic chunk — each fails when run in isolation. They now await
screen.findByLabelText like the file's first upload test, so every test waits
independently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gateway): carry the voice endpoint protocol through setup, rotate restored auth headers, and align the upload parser with the canonical rules (#3037)

- P1: voiceEndpointStyle/voiceApiVersion/voiceRealtimeAuthMode are now
  first-class setup fields — validated against the gateway's enums,
  submitted-over-template, persisted verbatim — so a fresh save of an
  Azure speech endpoint keeps its deployment URL shape instead of
  falling back to the OpenAI-compatible form (sabotage-proven pin plus
  unsupported-style and style/apiVersion pairing refusals). The upload
  parser imports the protocol from the file's voice connection (uniform
  across its providers, canonical pairing enforced, a SKIPPED realtime
  provider contributes nothing) and the dialog carries it invisibly.
- P2: a restored same-connection OCR/embedding provider now follows an
  authentication-header rotation together with the token (red proof:
  fresh token in the obsolete header), via sharesStoredGatewayConnection.
- P2: voice profiles on a realtime-only provider are refused exactly
  like the canonical parser refuses them (pin).
- P2: the undici override is scoped so jsdom 30 gets its declared ^8
  range while every other consumer keeps the 7.29.0 floor.
- The two identical dynamic-chunk loading fallbacks consolidate into
  DynamicChunkLoadFailure with its own suite (alert, retry, reload
  fallback, silent-while-loading).
- Review strengthenings: voice-hijack refusal now asserts the stored
  endpoint survives; the egress pin asserts the normalized value
  exactly; extracted helpers repair the four lint breaches the previous
  head shipped (complexity/max-lines), which is also why lint runs
  repo-wide in this wave's verification.
- Regenerated 1209 bundle evidence (byte metrics only; totals shrink).

* fix(ui): make hidden imported upload state file-scoped, and pin the governance rejections (#3037)

- P1: a corrected re-upload REPLACES the hidden imported embedding-kind
  list (and, when the file speaks about the voice connection, the
  imported endpoint protocol) instead of inheriting the previous
  file's declarations — a second upload declaring model-x as chat no
  longer submits the first file's embedding assertion (sabotage-proven
  dialog pin on the submitted payload).
- The two governance rejection tests now assert result.ok === false in
  addition to their message expectations.

* fix(gateway,release): file-scoped upload corrections, disk-only egress, exact fence closers, and reviewer strengthenings (#3037)

- P1: the approval-phrase fence walker now tracks the opening marker
  and closes only on the same type at same-or-greater length with no
  trailing text (CommonMark) — a ~~~ line inside a backtick fence or a
  shorter backtick fence inside a longer one can no longer smuggle a
  fenced example into an approval (red-proven regression pins).
- P1: a preserve-mode rebuild persists only the egress the stored file
  declares — the runtime aggregate (env-derived proxy/CA/private-network
  opt-ins) stays in the running process and off the disk, mirroring the
  settings-only path (sabotage-proven pin; fresh setups unchanged, incl.
  the bootstrap-config-file scenario).
- P2: stored embedding kinds now follow the inherited-only rule like
  every other stored restore list, so a corrected explicit deployment
  list can turn a mis-kinded embedding back into chat (red-proven pin).
- P2: the imported voice endpoint protocol is BOUND to the uploaded
  URL — a manually retyped endpoint no longer inherits the file's
  deployment-path shape (red-proven dialog pin).
- DynamicChunkLoadFailure takes an optional messageKey prop; the
  prerelease test resolves package.json from its own location; the
  jsdom undici override is pinned exactly (8.10.0); governance
  rejection tests assert result.ok; keiko-ui coverage baseline gains
  the new source file; 1209 bundle evidence reverted to the
  Linux-anchored measurement (the macOS regeneration was a platform
  artifact — nothing editor-fingerprinted actually changed).

* fix(release): fail closed on ambiguous dispatch binding and publish the prerelease draft-first (#3037)

Two reviewer findings on scripts/release-portable-prerelease.mjs:

- P1 dispatch identity: gh cannot return the created run id, so when two
  operators dispatch the same workflow on the same ref concurrently both
  new ids are unseen and .find() could bind — and publish — the competing
  operator's run. The poll now collects ALL unseen workflow_dispatch run
  ids for the ref (the list is also filtered to --event workflow_dispatch):
  exactly one binds, more than one refuses naming the concurrent-dispatch
  ambiguity, never a guess.

- P2 resumable superseding: the release used to go public on create with
  the predecessor supersede edit running afterwards — a transient edit
  failure left a live release without its superseded pointer and no retry,
  because the existing tag was refused up front. The create now carries
  --draft, the supersede edit runs against a not-yet-public release, and
  gh release edit <tag> --draft=false publishes as the LAST step. On
  resume, a DRAFT carrying the target tag (an interrupted run, judged via
  gh release view <tag> --json isDraft) is deleted and recreated fresh;
  a PUBLISHED release keeps the historical refusal.

Failure-first: the racing-dispatch double (two unseen runs), the
draft-order assertions, the failing-supersede no-draft=false pin, and the
draft-resume test were all red on the old code before the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(gateway): pin the endpoint-pairing refusal by code and exact message (#3037)

* fix(ui): validate imported Azure api versions and state the voice retry-tuning reset loudly (#3037)

- P2: the upload parser now mirrors the gateway's API_VERSION_RE
  (YYYY-MM-DD or -preview) — a malformed apiVersion refuses the file at
  upload instead of failing Test & Save after a reported success
  (red-proven pin).
- P2: per-role voice retry tuning the flat form cannot express (the
  owner's file tunes TTS to maxRetries 3) no longer resets silently on
  a fresh import: the upload applies with the reduction STATED, exactly
  like voiceProfilesReduced (red-proven pin; en/de messages).

* fix(release): close three prerelease reviewer findings — indented-code approvals, plan-only draft deletion, stale tag-ref publishing (#3037)

- check-release-impact: a four-space- or tab-indented approval phrase is a
  CommonMark indented code block and can never approve; up to three spaces
  stays an ordinary paragraph (boundary pinned).
- release-portable-prerelease: the interrupted-draft deletion moves onto the
  actual publish path — a --plan-only preview mutates nothing and states the
  pending draft recovery in its plan output.
- release-portable-prerelease: before creating, the requested tag is resolved
  as a remote git ref (annotated tags peeled); an existing ref not at the
  built commit refuses, because gh release create --target binds only a tag
  that does not yet exist. The hermetic double answers the lookup 404 by
  default so every existing scenario keeps its meaning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): make the invisible configured flags file-scoped like every hidden import (#3037)

A first upload declaring an explicit empty flag list left the invisible
configured flag armed forever — a corrected second file that stays
silent about the flag lists then submitted an empty clear the file
never asked for. Each upload now states whether THIS file speaks about
the lists; visible textarea values keep their user-correctable
persistence (red-proven dialog pin on the submitted payload).

* fix(release,ui): bind the release tag atomically at the built commit, and close two voice-protocol binding edges (#3037)

- The tag is now created atomically via a git/refs POST at the built
  commit BEFORE the release exists; an existing-ref conflict re-reads
  and validates, and gh release create runs with --verify-tag instead
  of --target — closing the check-then-create window in which a moved
  tag could re-bind fresh assets (pin relocation: the commit binding
  moved from the --target flag to the POST line, strictly stronger).
- The imported voice endpoint URL is stored trimmed, matching the
  submit-time comparison, and the positive branch of the protocol
  binding is pinned (upload -> submit carries the protocol verbatim).

* test(release): decompose the gh double's answer lookup below the complexity bar (#3037)

* fix(release,gateway): close three reviewer classes — lazy blockquote approvals, unbound --run-id refs, position-based primary classification (#3037)

- P1: the approval walker now carries CommonMark lazy-continuation
  state — a non-blank line directly after a blockquote line renders
  inside the quote and can never approve; a blank line ends the quote
  (red-proven both directions).
- P1: a supplied --run-id must be a workflow_dispatch run on exactly
  the requested ref — a successful evaluation run of an unmerged
  feature branch with a matching version could previously publish
  branch bytes publicly (two red-proven refusals; the dispatch path
  satisfies the rule by construction).
- P2: the stored primary gateway provider is now derived by capability
  instead of array position — a stored file listing a voice provider
  first no longer misclassifies a chat-sharing embedding as dedicated
  and restores it with its revoked credential after a rotation
  (red-proven pin).

* fix(release,gateway): invisible HTML-comment approvals, gap-tolerant supersede, and shared-connection endpoint moves (#3037)

- P1: a phrase inside a multi-line HTML comment renders nothing on
  GitHub and can no longer approve a publish (red-proven; a phrase
  after a CLOSED comment still approves).
- P2: previousBetaTag now picks the GREATEST existing lower beta, so a
  --tag override that skips numbers still supersedes the live latest
  beta (red-proven).
- P2: sharing is judged against the STORED primary connection and a
  shared provider follows the gateway wherever the update moves it —
  URL, token, and header travel together; dedicated connections stay
  untouched (red-proven on an endpoint move).

* fix(release,ui,security): column-aware code indentation, monotonic beta overrides, canonical locality ownership, and a leak-free hanging fixture (#3037)

- P1: indented-code detection in the approval walker now expands tabs
  to 4-column stops (CommonMark) — a space-plus-tab indent renders as
  code and can no longer approve; three plain spaces stay a paragraph
  (red-proven both directions).
- P1: the keychain HANGS fixture execs sleep instead of forking it, so
  the spawn timeout's kill reaches the hanging process itself — no
  orphaned sleeps leak across later suites.
- P2: a --tag override below the highest existing beta refuses — a
  lower newest release would leave the real highest beta unsuperseded
  (red-proven; resuming the highest beta's own draft stays allowed).
- P2: the upload parser rejects voiceProviderLocality on a non-voice
  capability, matching the canonical assertNoVoiceFieldsForNonVoiceKind
  (red-proven).

* fix(release): walk every HTML-comment marker on a line in the approval gate (#3037)

A single '-->' followed by '<!--' on one line closes a comment and
opens the next — judging only the first marker ended the comment state
while GitHub kept rendering the following lines invisibly. Every marker
is now processed in order; an inline comment leaves no open state
(red-proven both directions).

* fix(release,gateway,ui): raw-HTML approval blocks, chat-role primary, and voice-role replacement on corrected uploads (#3037)

- P1: the approval walker excludes raw-HTML blocks — <pre>/<script>/
  <style>/<textarea> run to their closing tag (blank lines included),
  any other '<'-opened block runs to the next blank line; a phrase
  inside renders as markup and can never approve (red-proven; phrases
  after closed blocks still approve).
- P2: the stored primary is the main CHAT connection (absent capability
  defaults to chat) — a dedicated embedding/OCR listed first no longer
  misclassifies chat-sharing providers as dedicated (red-proven).
- P2: an uploaded voice section REPLACES the form's role set, so a
  corrected file that dropped a role cannot silently re-add it via the
  stale visible field (red-proven).
- Review strengthenings: positive dedicated-header pin, tag-POST-
  before-create ordering pin, and the equal-index boundary of the
  monotonicity guard.

* fix(release,ui): list-item approval contexts, default draft resumption, and file-scoped voice profiles (#3037)

- P1: blockquotes and LIST ITEMS share the container-continuation rule
  in the approval walker — an instructional list ('- To approve, use:')
  keeps the marker inside the item, indented or lazy, until a blank
  line ends the container (red-proven; a phrase after the list still
  approves).
- P1: an ordinary retry after a crash between create and publish now
  RESUMES the highest existing beta when it is a draft instead of
  allocating the next number — the old default picked the still-private
  draft as predecessor and left the live release unsuperseded
  (red-proven).
- P2: the output voice is file-scoped with the roles — a corrected
  voice section that dropped voiceProfiles clears the field and its
  configured flag, so the form demands a voice VISIBLY instead of
  silently persisting the removed profile (red-proven).

* refactor(release): extract the container-continuation check below the complexity bar (#3037)

* fix(release,ui): column-zero approval anchoring, canonical URL import rules, secret-ref refusal, and paginated tag lineage (#3037)

- P1: the approval marker must start at column zero — leading
  indentation can be list-item continuation context (including a
  blank-separated child paragraph), so the former three-space paragraph
  tolerance is strengthened away; trailing whitespace stays tolerated
  (red-proven; documented strengthening of the relocated pin).
- P2: the upload parser mirrors validateBaseUrl — absolute http(s),
  plaintext http only on loopback, no credentials/query/fragment — so a
  malformed URL refuses at upload instead of failing Test & Save
  (red-proven).
- P2: apiKeySecretRef refuses on every provider kind: no form field can
  carry a secret reference, and the product's own persisted file keeps
  its credentials in the sealed vault outside the JSON (red-proven).
- P2: release-tag lineage derivation paginates past 100 releases
  (--paginate --slurp), so beta numbering and supersede pointers stay
  correct on long histories.

* fix(release,ui): fence-opaque walker ordering, canonical loopback range, and expanded URL refusal pins (#3037)

- The approval walker judges an OPEN fence before comment/HTML state:
  an unclosed <!-- inside a fenced example no longer poisons the state
  past the closing fence (the ordering pin was red before this fix).
- representableBaseUrl mirrors validateBaseUrl's loopback set exactly —
  localhost, ::1 bracketed or not, and all of 127.0.0.0/8.
- URL refusal pins gain the credential and fragment branches.

* fix(release,server,ui): close the eighth review wave — fence-closer indentation, workflow-identity run binding, publish-boundary tag recheck, canonical voice refusals, protocol-scoped voice mutations (#3037)

- check-release-impact: a would-be closing fence indented four-plus columns
  (or a tab) is fenced CONTENT (CommonMark allows at most three) — judged on
  the raw line, so an over-indented closer can no longer end the fence early
  and let the next column-zero phrase approve; runbook now states the
  column-zero requirement explicitly.
- release-portable-prerelease: a supplied --run-id must belong to the
  portable-assets workflow itself (workflowDatabaseId resolved from the
  workflow path — same-branch impostor workflows with identical job/artifact
  names are refused); the publication boundary re-reads the tag ref and
  refuses a moved OR vanished tag (a draft's tag stays mutable until
  publication; --verify-tag only proved existence at create).
- ADR-0163/portable-assets.yml: reconciled the 'never publishable' evaluation
  wording with the owner-approved beta prerelease program the document's own
  D9 amendment already acknowledges (stable release bundle stays closed).
- keiko-ui gatewayConfigParsing: mirror two more canonical refusals — a
  transcription model requires supportsRealtimeVoice, and every voice
  capability must declare voiceProviderLocality (no silent azure-foundry
  default; drifted absence now surfaces as disagreement, never a rewrite).
- keiko-server gateway-setup: voiceEndpointStyle/voiceApiVersion/
  voiceRealtimeAuthMode are connection mutations — an unscoped protocol
  update across heterogeneous audio connections is refused instead of
  spreading an Azure protocol onto an OpenAI-compatible realtime endpoint.

All behavioral fixes proven failure-first (new pins red on the prior code,
sabotage-inverse for the two script guards); 45+52+198+221 tests green,
tsc, eslint (root+ui), prettier, local sonar PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui,ci): voice section requires its base URL, strict file-scope role gate, zizmor anchors advanced (#3037)

KfQ wave on 4f928ee7 plus the CI red it left:
- gatewayConfigParsing: a voice section whose providers carry no baseUrl is
  refused as invalid (the canonical parser requires baseUrl on every provider
  and the sealed file always carries it) — role ids can no longer arrive
  while voiceBaseUrl stays undefined; new pin red before the fix.
- GatewaySetupDialog.applyUploadedVoiceRoles: the file-scope gate alone
  decides (speaksAboutVoice), now provably whole through the parser refusal.
- .github/zizmor.yml: the portable-assets.yml shell anchors moved two lines
  with the evaluation_build description edit (129→131, 199→201) — exactly
  what check-zizmor-anchors reported in both red coverage lanes.

222/222 modals, 12/12 anchor tests, ui lint, tsc, prettier, sonar PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@oscharko

oscharko commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Integrated into dev through the review-complete collector #3037 (squash commit 8df349b) — every finding raised here was carried, repaired failure-first, and re-reviewed there. Closing in favor of the collector.

@oscharko oscharko closed this Aug 8, 2026
@oscharko

oscharko commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Closed in favor of the review-complete collector #3037, which merged to dev as 8df349b — every finding raised on this PR was carried there, repaired failure-first, and re-reviewed. Resolving the stale thread so the closed PR stops reporting blocked-merge state.

@oscharko
oscharko deleted the feat/publish-approval-issue-evidence branch August 13, 2026 19:13
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