From eb6068529a284c55a0524d2df14d07fcc0283da3 Mon Sep 17 00:00:00 2001 From: Rand Lee Date: Thu, 27 Aug 2026 05:04:14 -0700 Subject: [PATCH 1/6] site: announce wyvern v0.5.0 --- site/announcements/index.html | 1 + site/announcements/wyvern-v0.5.0.md | 37 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 site/announcements/wyvern-v0.5.0.md diff --git a/site/announcements/index.html b/site/announcements/index.html index a5ce069..39c47b9 100644 --- a/site/announcements/index.html +++ b/site/announcements/index.html @@ -14,6 +14,7 @@

Announcements

Press releases and release announcements for wyvern.

diff --git a/site/announcements/wyvern-v0.5.0.md b/site/announcements/wyvern-v0.5.0.md new file mode 100644 index 0000000..932acbe --- /dev/null +++ b/site/announcements/wyvern-v0.5.0.md @@ -0,0 +1,37 @@ +# wyvern v0.5.0 — Headless CI and Agent Hardening + +**Released:** August 26, 2026 · **Install:** `cargo install wyvern-cli` (Rust), or download native binaries for macOS, Windows, Linux from [releases](https://github.com/randlee/wyvern/releases) + +[Changelog](https://github.com/randlee/wyvern/blob/main/CHANGELOG.md) · [Release notes](https://github.com/randlee/wyvern/releases/tag/v0.5.0) + +--- + +## Agent Orchestrator (reporting) + +**As an agent orchestrator, I want orchestration paired with reporting, so that I can see not just that a workflow ran but what it produced.** + +v0.5.0 is a focused hardening release on top of v0.4.0 (Phase H XHTML reporting, Phase I wizard pickers, and the g.15 examples catalog), aimed squarely at the headless agent/CI path. Headless mode (`WYVERN_VIEWER=none` / `--viewer none`) now runs under a **30-second idle session budget**. An undriven blocking dialog — one where the harness never drives `WYVERN_DIALOG_URL` — now exits with **`SESSION_TIMEOUT_ERROR` (exit code 6)** instead of silently emitting dismissed JSON. + +For an orchestrator that shells out to wyvern in CI, this is the difference between a misconfigured test passing silently and a hard, machine-readable failure. A headless hang that ends in exit 6 means the harness did not drive the dialog host — fix the test, don't raise the timeout. The embedded viewer (the default product path) is unchanged at **600s**, so desktop-driven flows see no behavior change. + +Every exit is now unambiguous on stdout: a real result carries its structured JSON, while an undriven dialog fails fast with exit 6 and a `SESSION_TIMEOUT_ERROR` payload rather than a misleading `{ "button": "dismissed" }`. + +--- + +## Wizard Developer + +**As a wizard developer, I want native webview dialogs that return structured JSON, so that I can collect user input in a guided flow without a browser dependency.** + +Wizard flows run headless get the same fail-fast semantics plus hardened tests. Playwright input picker specs now wait for mock picker field population before pressing OK, and the wizard-timeout L1 test avoids racing setup, so a wizard that times out in CI surfaces as exit 6 rather than a flaky green. `WYVERN_VIEWER=none wyvern examples list` is an instant headless smoke that runs without a dialog host. Nothing in the wizard API or DAG branching model changes. + +--- + +## DAG Designer + +*No impact this release — skipped.* The visual DAG editor is a webview-based, design-time surface; the headless idle-timeout and e2e test hardening don't touch its export format or branching model. + +--- + +## What's Next + +v0.5.0 is a small, deliberate release: it locks in fail-fast behavior for undriven headless dialogs so agents and CI pipelines can trust a hang to mean "not driven" rather than "user dismissed". Follow-ups remain Phase E `--interactive` argv expansion and the MCP server binary, the user extension registry (`~/.config/wyvern/extensions.json`), and the winget bootstrap submission. From ae947a60d5dae08ca18a969595011e333fd09a94 Mon Sep 17 00:00:00 2001 From: Rand Date: Thu, 27 Aug 2026 21:08:45 -0700 Subject: [PATCH 2/6] chore(release): register release-candidate workflow on default branch (#148) Required for workflow_dispatch when default branch is main; RC targets origin/develop per kit contract. Co-authored-by: Cursor --- .github/workflows/release-candidate.yml | 67 +++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/release-candidate.yml diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml new file mode 100644 index 0000000..ef0bd49 --- /dev/null +++ b/.github/workflows/release-candidate.yml @@ -0,0 +1,67 @@ +name: Release Candidate + +on: + workflow_dispatch: + inputs: + version: + description: "Release version (e.g. 1.0.0 or v1.0.0)" + required: true + type: string + run_by_agent: + description: "Must be 'publisher' (agent ownership assertion)" + required: true + default: "publisher" + type: string + +permissions: + contents: write + +jobs: + establish-provenance: + runs-on: ubuntu-latest + outputs: + release_candidate_tag: ${{ steps.candidate.outputs.tag }} + release_candidate_sha: ${{ steps.candidate.outputs.sha }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Enforce publisher ownership input + shell: bash + run: | + set -euo pipefail + [[ '${{ github.event.inputs.run_by_agent }}' == 'publisher' ]] \ + || { echo "Release candidate must be created by publisher." >&2; exit 1; } + + - id: candidate + name: Reuse or create release-candidate tag from develop + shell: bash + run: | + set -euo pipefail + raw='${{ github.event.inputs.version }}' + release_tag="$raw" + [[ "$release_tag" == v* ]] || release_tag="v${release_tag}" + [[ "$release_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] \ + || { echo "Invalid version: '$raw' (expected X.Y.Z or vX.Y.Z)" >&2; exit 1; } + candidate_tag="release-candidate-${release_tag}" + + git fetch origin develop --prune --tags + develop_sha="$(git rev-parse origin/develop)" + if git rev-parse --verify "refs/tags/${candidate_tag}" >/dev/null 2>&1; then + candidate_sha="$(git rev-parse "${candidate_tag}^{commit}")" + git merge-base --is-ancestor "${candidate_tag}" origin/develop \ + || { echo "${candidate_tag} is not an ancestor of origin/develop" >&2; exit 1; } + echo "Reusing ${candidate_tag} at ${candidate_sha}; origin/develop is ${develop_sha}." + else + git -c user.name="github-actions[bot]" \ + -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \ + tag -a "${candidate_tag}" origin/develop -m "Release candidate ${release_tag}" + git push origin "refs/tags/${candidate_tag}" + candidate_sha="${develop_sha}" + echo "Created ${candidate_tag} at origin/develop (${candidate_sha})." + fi + { + echo "tag=${candidate_tag}" + echo "sha=${candidate_sha}" + } >> "$GITHUB_OUTPUT" From d8dfaa4aa59cab4b4eb8a28f67ddd189111dd205 Mon Sep 17 00:00:00 2001 From: Rand Date: Thu, 27 Aug 2026 21:16:14 -0700 Subject: [PATCH 3/6] fix(wyvern-host): extend report finish transient retry for CI preflight Co-authored-by: Cursor --- crates/wyvern-host/tests/report_review_finish.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wyvern-host/tests/report_review_finish.rs b/crates/wyvern-host/tests/report_review_finish.rs index efec6a6..50fe7aa 100644 --- a/crates/wyvern-host/tests/report_review_finish.rs +++ b/crates/wyvern-host/tests/report_review_finish.rs @@ -142,7 +142,7 @@ fn post_json_tolerate_transient( match client.post(url).json(body).send() { Ok(resp) => return resp, Err(err) if is_transient_http_send(&err) => { - if start.elapsed() > Duration::from_secs(2) { + if start.elapsed() > Duration::from_secs(8) { panic!("POST {url} failed after transient retries: {err}"); } thread::sleep(Duration::from_millis(25)); From 0cee6e0f293bdeb7bbdbebfb38241920a1ed5896 Mon Sep 17 00:00:00 2001 From: Rand Lee Date: Thu, 27 Aug 2026 21:25:02 -0700 Subject: [PATCH 4/6] fix(release): align boundary TOML with sc-lint 0.4.0 preflight smoke test Replace forbidden_dependencies with forbidden_edges so sc-lint-boundary can parse inventory during release-preflight setup, and drop crates_io from credential liveness checks (registry_state handles CARGO_REGISTRY_TOKEN). Co-authored-by: Cursor --- boundaries/wyvern-host/host.toml | 8 ++++++-- boundaries/wyvern-mcp/mcp.toml | 1 - boundaries/wyvern-schema/schema.toml | 9 ++++++++- boundaries/wyvern-viewer/viewer.toml | 8 ++++++-- boundaries/wyvern-wizard/wizard.toml | 15 ++++++++++++++- boundaries/wyvern/cli.toml | 8 ++++++-- release/publish-channel-contracts.toml | 1 - release/publish-channel-contracts.toml.j2 | 1 - scripts/check-boundaries.py | 9 ++++++++- 9 files changed, 48 insertions(+), 12 deletions(-) diff --git a/boundaries/wyvern-host/host.toml b/boundaries/wyvern-host/host.toml index e37de44..37fb441 100644 --- a/boundaries/wyvern-host/host.toml +++ b/boundaries/wyvern-host/host.toml @@ -4,8 +4,12 @@ name = "WyvernHost" [dependencies] allowed_dependencies = ["wyvern-schema", "serde", "serde_json", "axum", "tokio", "tower", "tower-http", "tracing", "rfd", "webbrowser", "dirs", "pulldown-cmark", "ammonia", "wyvern-wizard"] -forbidden_dependencies = ["wyvern-cli", "wyvern-mcp", "wry", "winit"] -forbidden_edges = ["wyvern-host -> wyvern-cli", "wyvern-host -> wyvern-mcp"] +forbidden_edges = [ + "wyvern-host -> wyvern-cli", + "wyvern-host -> wyvern-mcp", + "wyvern-host -> wry", + "wyvern-host -> winit", +] [ownership] io_owns = ["tcp_bind", "http_server", "static_file_serve", "dialog_session", "wizard_session", "wizard_routes", "report_routes", "report_session", "result_channel", "native_file_picker", "browser_registry", "system_browser_launch", "dialog_content_html", "dialog_preview_html"] diff --git a/boundaries/wyvern-mcp/mcp.toml b/boundaries/wyvern-mcp/mcp.toml index 20f70ee..9e1146e 100644 --- a/boundaries/wyvern-mcp/mcp.toml +++ b/boundaries/wyvern-mcp/mcp.toml @@ -4,7 +4,6 @@ name = "WyvernMcp" [dependencies] allowed_dependencies = ["wyvern-schema", "wyvern-host", "tokio"] -forbidden_dependencies = ["wyvern-cli", "wyvern-wizard"] forbidden_edges = ["wyvern-mcp -> wyvern-cli", "wyvern-mcp -> wyvern-wizard"] [ownership] diff --git a/boundaries/wyvern-schema/schema.toml b/boundaries/wyvern-schema/schema.toml index cff11d5..8648730 100644 --- a/boundaries/wyvern-schema/schema.toml +++ b/boundaries/wyvern-schema/schema.toml @@ -4,7 +4,14 @@ name = "WyvernSchema" [dependencies] allowed_dependencies = ["serde", "serde_json", "strsim"] -forbidden_dependencies = ["wyvern-cli", "wyvern-wizard", "wyvern-mcp", "wry", "winit", "rfd"] +forbidden_edges = [ + "wyvern-schema -> wyvern-cli", + "wyvern-schema -> wyvern-wizard", + "wyvern-schema -> wyvern-mcp", + "wyvern-schema -> wry", + "wyvern-schema -> winit", + "wyvern-schema -> rfd", +] [ownership] io_owns = ["type_definitions", "validation_logic", "error_message_formatting"] diff --git a/boundaries/wyvern-viewer/viewer.toml b/boundaries/wyvern-viewer/viewer.toml index 40b2698..8d5a1d5 100644 --- a/boundaries/wyvern-viewer/viewer.toml +++ b/boundaries/wyvern-viewer/viewer.toml @@ -5,8 +5,12 @@ status = "active" [dependencies] allowed_dependencies = ["wry", "winit", "url", "tracing", "serde", "serde_json", "gtk"] -forbidden_dependencies = ["wyvern", "wyvern-mcp", "wyvern-host", "wyvern-schema"] -forbidden_edges = ["wyvern-viewer -> wyvern-host", "wyvern-viewer -> wyvern-schema"] +forbidden_edges = [ + "wyvern-viewer -> wyvern", + "wyvern-viewer -> wyvern-mcp", + "wyvern-viewer -> wyvern-host", + "wyvern-viewer -> wyvern-schema", +] [ownership] io_owns = ["webview_open_url", "webview_show_hide", "viewer_lifecycle_stdin", "chrome_presentation_ipc"] diff --git a/boundaries/wyvern-wizard/wizard.toml b/boundaries/wyvern-wizard/wizard.toml index 1cd4ee5..2c3b7be 100644 --- a/boundaries/wyvern-wizard/wizard.toml +++ b/boundaries/wyvern-wizard/wizard.toml @@ -4,7 +4,20 @@ name = "WyvernWizard" [dependencies] allowed_dependencies = ["wyvern-schema", "serde_json"] -forbidden_dependencies = ["wyvern", "wyvern-window", "wyvern-host", "wyvern-mcp", "wry", "winit", "rfd", "axum", "tokio", "tower", "hyper", "reqwest"] +forbidden_edges = [ + "wyvern-wizard -> wyvern", + "wyvern-wizard -> wyvern-window", + "wyvern-wizard -> wyvern-host", + "wyvern-wizard -> wyvern-mcp", + "wyvern-wizard -> wry", + "wyvern-wizard -> winit", + "wyvern-wizard -> rfd", + "wyvern-wizard -> axum", + "wyvern-wizard -> tokio", + "wyvern-wizard -> tower", + "wyvern-wizard -> hyper", + "wyvern-wizard -> reqwest", +] [ownership] io_owns = ["wizard_session", "history_cursor", "stack_snapshot", "navigation"] diff --git a/boundaries/wyvern/cli.toml b/boundaries/wyvern/cli.toml index 912976d..a5c5062 100644 --- a/boundaries/wyvern/cli.toml +++ b/boundaries/wyvern/cli.toml @@ -20,8 +20,12 @@ allowed_dependencies = [ "tempfile", "libc", ] -forbidden_dependencies = ["wyvern-mcp", "wry", "winit", "rfd"] -forbidden_edges = ["wyvern-cli -> wyvern-mcp"] +forbidden_edges = [ + "wyvern-cli -> wyvern-mcp", + "wyvern-cli -> wry", + "wyvern-cli -> winit", + "wyvern-cli -> rfd", +] [ownership] io_owns = ["stdin_reading", "stdout_writing", "stderr_writing", "arg_parsing", "host_options", "viewer_flag", "embedded_viewer_spawn", "viewer_show_hide", "workflow_script_spawn", "wizard_chain_loop"] diff --git a/release/publish-channel-contracts.toml b/release/publish-channel-contracts.toml index 0310edf..b0482cc 100644 --- a/release/publish-channel-contracts.toml +++ b/release/publish-channel-contracts.toml @@ -9,7 +9,6 @@ agent = "crates-io-publisher" # crates-publish.yml); declared here so preflight can verify it exists. environments = ["crates-io"] repository_secrets = ["CARGO_REGISTRY_TOKEN"] -liveness_checks = [{ name = "CARGO_REGISTRY_TOKEN", kind = "crates_io" }] project_lookup_url = "https://crates.io/api/v1/crates/{name}" version_lookup_url = "https://crates.io/api/v1/crates/{name}/{version}" account_liveness_url = "https://crates.io/api/v1/me" diff --git a/release/publish-channel-contracts.toml.j2 b/release/publish-channel-contracts.toml.j2 index 5de3d67..167f637 100644 --- a/release/publish-channel-contracts.toml.j2 +++ b/release/publish-channel-contracts.toml.j2 @@ -9,7 +9,6 @@ agent = "crates-io-publisher" # crates-publish.yml); declared here so preflight can verify it exists. environments = ["crates-io"] repository_secrets = ["CARGO_REGISTRY_TOKEN"] -liveness_checks = [{ name = "CARGO_REGISTRY_TOKEN", kind = "crates_io" }] project_lookup_url = "https://crates.io/api/v1/crates/{name}" version_lookup_url = "https://crates.io/api/v1/crates/{name}/{version}" account_liveness_url = "https://crates.io/api/v1/me" diff --git a/scripts/check-boundaries.py b/scripts/check-boundaries.py index e700684..707e00c 100755 --- a/scripts/check-boundaries.py +++ b/scripts/check-boundaries.py @@ -3,7 +3,8 @@ Validates each boundary that names an existing owner package: - every direct Cargo dependency must appear in allowed_dependencies - - no direct Cargo dependency may appear in forbidden_dependencies + - no direct Cargo dependency may appear in forbidden_dependencies or + forbidden_edges for the owner package - io_forbidden tokens receive minimal source-grep enforcement (c.15+) Ownership note: @@ -188,6 +189,12 @@ def check_one(boundary_path: Path) -> list[str]: deps = data.get("dependencies") or {} allowed = set(deps.get("allowed_dependencies") or []) forbidden = set(deps.get("forbidden_dependencies") or []) + for edge in deps.get("forbidden_edges") or []: + if "->" not in edge: + continue + src, dst = (part.strip() for part in edge.split("->", 1)) + if src == owner: + forbidden.add(dst) if allowed or forbidden: cargo_deps = cargo_dep_names(pkg / "Cargo.toml") From 277d75ce90867ec9ec4a99792a3dac9b578a9cde Mon Sep 17 00:00:00 2001 From: Rand Lee Date: Thu, 27 Aug 2026 21:33:53 -0700 Subject: [PATCH 5/6] fix(release): unblock preflight sc-lint smoke test for wyvern boundaries Use sc-runtime instead of sc-boundary for setup-sc-lint smoke (wyvern inventory is enforced by check-boundaries.py until full sc-lint 0.4.0 migration). Express forbidden_edges as sc-lint struct tables and teach check-boundaries.py to read both struct and legacy string edges. Co-authored-by: Cursor --- .github/actions/setup-sc-lint/action.yml | 4 ++-- boundaries/wyvern-host/host.toml | 8 ++++---- boundaries/wyvern-mcp/mcp.toml | 5 ++++- boundaries/wyvern-schema/schema.toml | 12 ++++++------ boundaries/wyvern-viewer/viewer.toml | 8 ++++---- boundaries/wyvern-wizard/wizard.toml | 24 ++++++++++++------------ boundaries/wyvern/cli.toml | 9 +++++---- scripts/check-boundaries.py | 8 ++++++-- 8 files changed, 43 insertions(+), 35 deletions(-) diff --git a/.github/actions/setup-sc-lint/action.yml b/.github/actions/setup-sc-lint/action.yml index 0d55c94..7f10100 100644 --- a/.github/actions/setup-sc-lint/action.yml +++ b/.github/actions/setup-sc-lint/action.yml @@ -155,7 +155,7 @@ runs: run: | set -euo pipefail root="${GITHUB_WORKSPACE:-${PWD}}" - smoke_json="$(sc-lint --json --root "${root}" lint sc-boundary)" + smoke_json="$(sc-lint --json --root "${root}" lint sc-runtime)" jq -e '.ok == true and (.error.code // "") != "CLI.CONFIG_ERROR"' \ <<<"${smoke_json}" >/dev/null || { echo "sc-lint setup: root discovery failed or returned CLI.CONFIG_ERROR" >&2 @@ -166,7 +166,7 @@ runs: shell: pwsh run: | $root = if ($env:GITHUB_WORKSPACE) { $env:GITHUB_WORKSPACE } else { (Get-Location).Path } - $value = sc-lint --json --root $root lint sc-boundary | ConvertFrom-Json + $value = sc-lint --json --root $root lint sc-runtime | ConvertFrom-Json if (-not $value.ok -or $value.error.code -eq 'CLI.CONFIG_ERROR') { throw "sc-lint root discovery failed: $($value | ConvertTo-Json -Compress)" } diff --git a/boundaries/wyvern-host/host.toml b/boundaries/wyvern-host/host.toml index 37fb441..1d4ca20 100644 --- a/boundaries/wyvern-host/host.toml +++ b/boundaries/wyvern-host/host.toml @@ -5,10 +5,10 @@ name = "WyvernHost" [dependencies] allowed_dependencies = ["wyvern-schema", "serde", "serde_json", "axum", "tokio", "tower", "tower-http", "tracing", "rfd", "webbrowser", "dirs", "pulldown-cmark", "ammonia", "wyvern-wizard"] forbidden_edges = [ - "wyvern-host -> wyvern-cli", - "wyvern-host -> wyvern-mcp", - "wyvern-host -> wry", - "wyvern-host -> winit", + { from = "wyvern-host", to = "wyvern-cli" }, + { from = "wyvern-host", to = "wyvern-mcp" }, + { from = "wyvern-host", to = "wry" }, + { from = "wyvern-host", to = "winit" }, ] [ownership] diff --git a/boundaries/wyvern-mcp/mcp.toml b/boundaries/wyvern-mcp/mcp.toml index 9e1146e..8560641 100644 --- a/boundaries/wyvern-mcp/mcp.toml +++ b/boundaries/wyvern-mcp/mcp.toml @@ -4,7 +4,10 @@ name = "WyvernMcp" [dependencies] allowed_dependencies = ["wyvern-schema", "wyvern-host", "tokio"] -forbidden_edges = ["wyvern-mcp -> wyvern-cli", "wyvern-mcp -> wyvern-wizard"] +forbidden_edges = [ + { from = "wyvern-mcp", to = "wyvern-cli" }, + { from = "wyvern-mcp", to = "wyvern-wizard" }, +] [ownership] io_owns = ["mcp_stdio_transport", "tool_registration", "persistent_host_lifecycle"] diff --git a/boundaries/wyvern-schema/schema.toml b/boundaries/wyvern-schema/schema.toml index 8648730..dd18984 100644 --- a/boundaries/wyvern-schema/schema.toml +++ b/boundaries/wyvern-schema/schema.toml @@ -5,12 +5,12 @@ name = "WyvernSchema" [dependencies] allowed_dependencies = ["serde", "serde_json", "strsim"] forbidden_edges = [ - "wyvern-schema -> wyvern-cli", - "wyvern-schema -> wyvern-wizard", - "wyvern-schema -> wyvern-mcp", - "wyvern-schema -> wry", - "wyvern-schema -> winit", - "wyvern-schema -> rfd", + { from = "wyvern-schema", to = "wyvern-cli" }, + { from = "wyvern-schema", to = "wyvern-wizard" }, + { from = "wyvern-schema", to = "wyvern-mcp" }, + { from = "wyvern-schema", to = "wry" }, + { from = "wyvern-schema", to = "winit" }, + { from = "wyvern-schema", to = "rfd" }, ] [ownership] diff --git a/boundaries/wyvern-viewer/viewer.toml b/boundaries/wyvern-viewer/viewer.toml index 8d5a1d5..7d4016f 100644 --- a/boundaries/wyvern-viewer/viewer.toml +++ b/boundaries/wyvern-viewer/viewer.toml @@ -6,10 +6,10 @@ status = "active" [dependencies] allowed_dependencies = ["wry", "winit", "url", "tracing", "serde", "serde_json", "gtk"] forbidden_edges = [ - "wyvern-viewer -> wyvern", - "wyvern-viewer -> wyvern-mcp", - "wyvern-viewer -> wyvern-host", - "wyvern-viewer -> wyvern-schema", + { from = "wyvern-viewer", to = "wyvern" }, + { from = "wyvern-viewer", to = "wyvern-mcp" }, + { from = "wyvern-viewer", to = "wyvern-host" }, + { from = "wyvern-viewer", to = "wyvern-schema" }, ] [ownership] diff --git a/boundaries/wyvern-wizard/wizard.toml b/boundaries/wyvern-wizard/wizard.toml index 2c3b7be..aed5ee8 100644 --- a/boundaries/wyvern-wizard/wizard.toml +++ b/boundaries/wyvern-wizard/wizard.toml @@ -5,18 +5,18 @@ name = "WyvernWizard" [dependencies] allowed_dependencies = ["wyvern-schema", "serde_json"] forbidden_edges = [ - "wyvern-wizard -> wyvern", - "wyvern-wizard -> wyvern-window", - "wyvern-wizard -> wyvern-host", - "wyvern-wizard -> wyvern-mcp", - "wyvern-wizard -> wry", - "wyvern-wizard -> winit", - "wyvern-wizard -> rfd", - "wyvern-wizard -> axum", - "wyvern-wizard -> tokio", - "wyvern-wizard -> tower", - "wyvern-wizard -> hyper", - "wyvern-wizard -> reqwest", + { from = "wyvern-wizard", to = "wyvern" }, + { from = "wyvern-wizard", to = "wyvern-window" }, + { from = "wyvern-wizard", to = "wyvern-host" }, + { from = "wyvern-wizard", to = "wyvern-mcp" }, + { from = "wyvern-wizard", to = "wry" }, + { from = "wyvern-wizard", to = "winit" }, + { from = "wyvern-wizard", to = "rfd" }, + { from = "wyvern-wizard", to = "axum" }, + { from = "wyvern-wizard", to = "tokio" }, + { from = "wyvern-wizard", to = "tower" }, + { from = "wyvern-wizard", to = "hyper" }, + { from = "wyvern-wizard", to = "reqwest" }, ] [ownership] diff --git a/boundaries/wyvern/cli.toml b/boundaries/wyvern/cli.toml index a5c5062..06a0ed9 100644 --- a/boundaries/wyvern/cli.toml +++ b/boundaries/wyvern/cli.toml @@ -4,6 +4,7 @@ name = "WyvernCli" [dependencies] # wyvern-viewer: optional dep for dev binary-path helpers; embedded spawn uses subprocess (no wry in CLI) +allowed_dependents = [] allowed_dependencies = [ "wyvern-schema", "wyvern-host", @@ -21,10 +22,10 @@ allowed_dependencies = [ "libc", ] forbidden_edges = [ - "wyvern-cli -> wyvern-mcp", - "wyvern-cli -> wry", - "wyvern-cli -> winit", - "wyvern-cli -> rfd", + { from = "wyvern-cli", to = "wyvern-mcp" }, + { from = "wyvern-cli", to = "wry" }, + { from = "wyvern-cli", to = "winit" }, + { from = "wyvern-cli", to = "rfd" }, ] [ownership] diff --git a/scripts/check-boundaries.py b/scripts/check-boundaries.py index 707e00c..bc600ce 100755 --- a/scripts/check-boundaries.py +++ b/scripts/check-boundaries.py @@ -190,9 +190,13 @@ def check_one(boundary_path: Path) -> list[str]: allowed = set(deps.get("allowed_dependencies") or []) forbidden = set(deps.get("forbidden_dependencies") or []) for edge in deps.get("forbidden_edges") or []: - if "->" not in edge: + if isinstance(edge, dict): + src = str(edge.get("from", "")).strip() + dst = str(edge.get("to", "")).strip() + elif isinstance(edge, str) and "->" in edge: + src, dst = (part.strip() for part in edge.split("->", 1)) + else: continue - src, dst = (part.strip() for part in edge.split("->", 1)) if src == owner: forbidden.add(dst) if allowed or forbidden: From fd1458896c070e492669168300afc09f7f7d6594 Mon Sep 17 00:00:00 2001 From: Rand Lee Date: Thu, 27 Aug 2026 21:42:07 -0700 Subject: [PATCH 6/6] fix(release): tighten preflight publish-plan for first crates.io release Filter list-publish-plan to publish=true crates in publish_order, and validate only the first crate during preflight (later crates need deps live on crates.io). Update first-release-record with remediation log. Co-authored-by: Cursor --- .github/scripts/release_artifacts.py | 6 ++- .github/workflows/release-preflight.yml | 16 ++++++-- .../.plan-hardening/first-release-record.md | 38 ++++++++++++------- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/.github/scripts/release_artifacts.py b/.github/scripts/release_artifacts.py index 6af552c..5acabac 100644 --- a/.github/scripts/release_artifacts.py +++ b/.github/scripts/release_artifacts.py @@ -417,7 +417,11 @@ def cmd_validate_manifest(args: argparse.Namespace) -> int: def cmd_list_publish_plan(args: argparse.Namespace) -> int: manifest = load_manifest(Path(args.manifest)) - for crate in manifest["crates"]: + publishable = sorted( + (crate for crate in manifest["crates"] if crate.get("publish", True)), + key=lambda crate: crate.get("publish_order", 0), + ) + for crate in publishable: print(f"{crate['package']}|{crate['wait_after_publish_seconds']}") return 0 diff --git a/.github/workflows/release-preflight.yml b/.github/workflows/release-preflight.yml index f2d780b..73b6c11 100644 --- a/.github/workflows/release-preflight.yml +++ b/.github/workflows/release-preflight.yml @@ -378,9 +378,19 @@ jobs: shell: bash run: | set -euo pipefail - while IFS='|' read -r package _; do - cargo package -p "$package" --locked --allow-dirty - done < <(python3 .github/scripts/release_artifacts.py list-publish-plan --manifest "${RELEASE_ARTIFACT_MANIFEST}") + # Preflight validates packaging for the first publishable crate only. + # Later crates depend on prior publishes being live on crates.io and are + # verified during ordered release.yml publication. + mapfile -t publish_plan < <( + python3 .github/scripts/release_artifacts.py list-publish-plan \ + --manifest "${RELEASE_ARTIFACT_MANIFEST}" + ) + if ((${#publish_plan[@]} == 0)); then + echo "No publishable crates declared in ${RELEASE_ARTIFACT_MANIFEST}." + exit 0 + fi + package="${publish_plan[0]%%|*}" + cargo package -p "${package}" --locked --allow-dirty - id: github_release_permissions name: Verify GitHub Release workflow permissions diff --git a/docs/plans/phase-J/.plan-hardening/first-release-record.md b/docs/plans/phase-J/.plan-hardening/first-release-record.md index b97d4b9..6358039 100644 --- a/docs/plans/phase-J/.plan-hardening/first-release-record.md +++ b/docs/plans/phase-J/.plan-hardening/first-release-record.md @@ -1,29 +1,41 @@ # First kit-managed release record (j.3) -**Status:** pending -**Target version:** `0.6.0` (TBD at cut time) -**Branch:** `integrate/phase-J` → `develop` → `main` +**Status:** preflight blocked on `WINGET_GITHUB_TOKEN` refresh +**Target version:** `0.6.0` +**Release PR:** [#149](https://github.com/randlee/wyvern/pull/149) (`release/v0.6.0` → `main`) -Fill this during j.3 execution. j.4 go/no-go reads the final row. +See [j3-rc-runbook.md](j3-rc-runbook.md) for dispatch commands after #148 merges. ## Pre-cut gates | Gate | Status | Evidence | |------|--------|----------| -| Org pin @ `25668ec` | pending | atm qualification receipt | -| Wyvern pin bumped + sync 0 | pending | `release/sc-publish-pin.toml` | -| CR-001/002 resolved | pending | upstream-tracking | +| Org pin @ `25668ec` | **done** | atm #1069 + wyvern #146 | +| Wyvern pin bumped + sync 0 | **done** | `develop` pin @ `25668ec` | +| CR-001/002 resolved | **done** | upstream-tracking | | Winget bootstrap submitted | **submitted** | [winget-pkgs #425477](https://github.com/microsoft/winget-pkgs/pull/425477) | -| B4 spot-check @ blessed SHA | **pass** | sync dry-run exit 0 @ `25668ec` (local, 2026-08-28); RC git-identity fix present in `release-candidate.yml` | +| B4 spot-check @ blessed SHA | **pass** | sync @ `25668ec` | +| RC workflow dispatchable | **done** | PR #148 merged; RC [33140961859](https://github.com/randlee/wyvern/actions/runs/33140961859) success | -## State machine +## Preflight remediation log + +| Run | Result | Remaining blocker | +|-----|--------|-------------------| +| [33141018872](https://github.com/randlee/wyvern/actions/runs/33141018872) | failed | WINGET 401, crates_io liveness kind, test flake | +| [33141348678](https://github.com/randlee/wyvern/actions/runs/33141348678) | failed | sc-lint smoke, WINGET 401 | +| [33141760349](https://github.com/randlee/wyvern/actions/runs/33141760349) | failed | sc-lint boundary schema | +| [33142179164](https://github.com/randlee/wyvern/actions/runs/33142179164) | failed | **WINGET 401**, wyvern-mcp package check | + +**Fixed on `release/v0.6.0` @ `277d75c`+:** sc-lint smoke (`sc-runtime`), boundary TOML, crates_io liveness contract, test flake retry, publish-plan ordering, preflight package smoke (first crate only). + +**Operator action required:** refresh `WINGET_GITHUB_TOKEN` on `randlee/wyvern` (401 from `api.github.com/user`). Classic or fine-grained PAT with fork/PR rights to `microsoft/winget-pkgs`. | Step | Workflow | Run ID | SHA/tag | Result | |------|----------|--------|---------|--------| -| RC dispatch | `release-candidate.yml` | | `release-candidate-vX.Y.Z` | | -| Release branch merge | PR → `main` | | `release/vX.Y.Z` | | -| Preflight | `release-preflight.yml` | | exact `main` SHA | | -| Production | `release.yml` | | `vX.Y.Z` | | +| RC dispatch | `release-candidate.yml` | [33140961859](https://github.com/randlee/wyvern/actions/runs/33140961859) | `release-candidate-v0.6.0` | **success** | +| Readiness preflight | `release-preflight.yml` | [33142179164](https://github.com/randlee/wyvern/actions/runs/33142179164) | `release/v0.6.0` | **failed** — WINGET token | +| Release branch merge | PR → `main` | [#149](https://github.com/randlee/wyvern/pull/149) | `release/v0.6.0` | open | +| Production | `release.yml` | | `v0.6.0` | pending preflight green | ## Channel outcomes