Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/actions/setup-sc-lint/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)"
}
6 changes: 5 additions & 1 deletion .github/scripts/release_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 13 additions & 3 deletions .github/workflows/release-preflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions boundaries/wyvern-host/host.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{ from = "wyvern-host", to = "wyvern-cli" },
{ from = "wyvern-host", to = "wyvern-mcp" },
{ from = "wyvern-host", to = "wry" },
{ from = "wyvern-host", to = "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"]
Expand Down
6 changes: 4 additions & 2 deletions boundaries/wyvern-mcp/mcp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ 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"]
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"]
Expand Down
9 changes: 8 additions & 1 deletion boundaries/wyvern-schema/schema.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{ 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]
io_owns = ["type_definitions", "validation_logic", "error_message_formatting"]
Expand Down
8 changes: 6 additions & 2 deletions boundaries/wyvern-viewer/viewer.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{ from = "wyvern-viewer", to = "wyvern" },
{ from = "wyvern-viewer", to = "wyvern-mcp" },
{ from = "wyvern-viewer", to = "wyvern-host" },
{ from = "wyvern-viewer", to = "wyvern-schema" },
]

[ownership]
io_owns = ["webview_open_url", "webview_show_hide", "viewer_lifecycle_stdin", "chrome_presentation_ipc"]
Expand Down
15 changes: 14 additions & 1 deletion boundaries/wyvern-wizard/wizard.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{ 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]
io_owns = ["wizard_session", "history_cursor", "stack_snapshot", "navigation"]
Expand Down
9 changes: 7 additions & 2 deletions boundaries/wyvern/cli.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -20,8 +21,12 @@ allowed_dependencies = [
"tempfile",
"libc",
]
forbidden_dependencies = ["wyvern-mcp", "wry", "winit", "rfd"]
forbidden_edges = ["wyvern-cli -> wyvern-mcp"]
forbidden_edges = [
{ from = "wyvern-cli", to = "wyvern-mcp" },
{ from = "wyvern-cli", to = "wry" },
{ from = "wyvern-cli", to = "winit" },
{ from = "wyvern-cli", to = "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"]
Expand Down
2 changes: 1 addition & 1 deletion crates/wyvern-host/tests/report_review_finish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
38 changes: 25 additions & 13 deletions docs/plans/phase-J/.plan-hardening/first-release-record.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
1 change: 0 additions & 1 deletion release/publish-channel-contracts.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 0 additions & 1 deletion release/publish-channel-contracts.toml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 12 additions & 1 deletion scripts/check-boundaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -188,6 +189,16 @@ 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 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
if src == owner:
forbidden.add(dst)
if allowed or forbidden:
cargo_deps = cargo_dep_names(pkg / "Cargo.toml")

Expand Down
1 change: 1 addition & 0 deletions site/announcements/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<h1>Announcements</h1>
<p>Press releases and release announcements for wyvern.</p>
<ul>
<li><a href="wyvern-v0.5.0.md">wyvern v0.5.0</a> — Headless CI and Agent Hardening</li>
<li><a href="wyvern-v0.4.0.md">wyvern v0.4.0</a> — XHTML Reporting, Native Path Pickers, Examples Catalog</li>
<li><a href="wyvern-v0.3.1.md">wyvern v0.3.1</a> — CLI Extension Runtime, Help System, Skill Catalog (fixes v0.3.0 crates.io publish)</li>
</ul>
Expand Down
37 changes: 37 additions & 0 deletions site/announcements/wyvern-v0.5.0.md
Original file line number Diff line number Diff line change
@@ -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.
Loading