Claude Code absorbed MCP natively, so Triple-C's own MCP server library is removed. That deletion set the direction for everything else here: Triple-C shows state and launches things; Claude Code edits its own config.
Scheduler UI incl. task creation with cron presets
Things worth a reviewer's attention
The MCP removal had a real hazard. Old containers were created with network_mode: triple-c-net-<projectId>. Deleting the network code outright would leave them unable to start — Docker refuses a container pointed at a missing user-defined network. There's a migration shim in docker/legacy_cleanup.rs: any container carrying a non-empty triple-c.mcp-fingerprint or attached to a triple-c-net-* network is force-recreated onto the default bridge. Both the shim and the label write can be deleted a release later.
A security improvement fell out of it. The Docker socket was being auto-mounted whenever a project enabled a stdio+Docker MCP server, independent of allow_docker_access. That implicit mount is gone.
Bugs fixed that weren't in scope. Scheduled tasks ran --dangerously-skip-permissions unconditionally, ignoring the project setting. Reset destroyed volumes with no confirmation while the less-destructive Remove confirmed. Cancelling authentication didn't cancel, locking out retries for 15 minutes. An invalid cron silently unscheduled every task in the container — rebuild_crontab pipes the whole crontab and swallowed the rejection with || true.
Docs corrected. README/HOW-TO-USE/CLAUDE.md all claimed Reset preserves credentials. It doesn't — remove_project_volumes deletes both volumes.
Not verified
No agent could launch the Tauri GUI, so the Project Home restructure has never been seen running. setup-token completion needs a browser and a subscription, so its parser is validated against captured pty output rather than a real token; the keychain round-trip had no secret service available. The Auth Bridge was exercised end-to-end against a real container.
Claude Code absorbed MCP natively, so Triple-C's own MCP server library is removed. That deletion set the direction for everything else here: **Triple-C shows state and launches things; Claude Code edits its own config.**
8 commits, 119 files, +14,230 / −4,726. Rust: 34 tests, zero warnings. Frontend: 33 → 87 tests.
## What's in it
| | |
|---|---|
| **MCP removed** | Frontend, backend, entrypoint injection, docs — with a self-healing migration |
| **Permission modes** | `Plan / Default / Accept Edits / Bypass` wired to `--permission-mode` |
| **Project Home** | `ProjectCard.tsx` (1,257 lines) → tabbed main-area view |
| **Tier-1 polish** | 3 WCAG AA contrast failures fixed, focus rings, accessible `Modal` |
| **Auth Bridge** | Generic loopback bridge for `aws sso login`, Concourse `fly login`, `claude login` |
| **Shared auth token** | Keychain-backed `CLAUDE_CODE_OAUTH_TOKEN`, per-project opt-out |
| **Automation** | Scheduler UI incl. task creation with cron presets |
## Things worth a reviewer's attention
**The MCP removal had a real hazard.** Old containers were created with `network_mode: triple-c-net-<projectId>`. Deleting the network code outright would leave them unable to start — Docker refuses a container pointed at a missing user-defined network. There's a migration shim in `docker/legacy_cleanup.rs`: any container carrying a non-empty `triple-c.mcp-fingerprint` or attached to a `triple-c-net-*` network is force-recreated onto the default bridge. **Both the shim and the label write can be deleted a release later.**
**A security improvement fell out of it.** The Docker socket was being auto-mounted whenever a project enabled a stdio+Docker MCP server, independent of `allow_docker_access`. That implicit mount is gone.
**Bugs fixed that weren't in scope.** Scheduled tasks ran `--dangerously-skip-permissions` unconditionally, ignoring the project setting. Reset destroyed volumes with no confirmation while the less-destructive Remove confirmed. Cancelling authentication didn't cancel, locking out retries for 15 minutes. An invalid cron silently unscheduled *every* task in the container — `rebuild_crontab` pipes the whole crontab and swallowed the rejection with `|| true`.
**Docs corrected.** README/HOW-TO-USE/CLAUDE.md all claimed Reset preserves credentials. It doesn't — `remove_project_volumes` deletes both volumes.
## Not verified
No agent could launch the Tauri GUI, so the Project Home restructure has never been seen running. `setup-token` completion needs a browser and a subscription, so its parser is validated against captured pty output rather than a real token; the keychain round-trip had no secret service available. The Auth Bridge *was* exercised end-to-end against a real container.
Companion docs: `DESIGN-REVIEW.md`, `ROADMAP.md`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k
Claude Code manages MCP natively now (`claude mcp add/list/remove`,
`.mcp.json`, `/mcp`), so Triple-C's own MCP server library is redundant.
Deletes components/mcp/, hooks/useMcpServers.ts, the MCP sidebar tab and
rail icon, the per-project enable checkboxes on ProjectCard, the
mcpServers slice of the Zustand store, the four IPC wrappers, and the
McpServer/McpTransportType types.
Rust backend is untouched in this commit; the commands simply become
unreachable. Backend removal and the legacy container/network cleanup
follow separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the removal begun in the previous commit.
Backend: deletes models/mcp_server.rs, storage/mcp_store.rs and
commands/mcp_commands.rs, the McpStore on AppState, the four IPC
handlers, Project::enabled_mcp_servers, build_mcp_servers_json(),
compute_mcp_fingerprint(), the MCP_SERVERS_JSON env injection, the
mcp-fingerprint label, and the whole MCP container lifecycle.
create_container() and container_needs_recreation() lose their
mcp_servers/network_name parameters.
Container: entrypoint.sh no longer merges MCP_SERVERS_JSON into
~/.claude.json. MCP_SERVERS_JSON stays in the reserved env blocklist.
Security: the Docker socket is no longer auto-mounted for stdio+Docker
MCP servers — it now mounts only when allow_docker_access is set.
Migration: old containers were created with
network_mode=triple-c-net-<projectId> and refuse to start once that
network is gone. docker/network.rs becomes docker/legacy_cleanup.rs with
label-driven, best-effort removal of leftover MCP containers and the
per-project network, called on both delete and recreate.
container_needs_recreation() now forces a rebuild for any container
carrying a non-empty triple-c.mcp-fingerprint label or attached to a
triple-c-net-* network, moving it onto the default bridge. Both can be
dropped a release later.
Docs: drops the MCP sections from README/HOW-TO-USE/TECHNICAL and adds a
short note pointing at Claude Code's native `claude mcp` / `/mcp` /
.mcp.json instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Permission modes: replaces the binary full_permissions flag with a
PermissionMode enum (Plan/Default/AcceptEdits/Bypass). Flag mapping is
defined once in PermissionMode::cli_args() and used by the terminal, the
web terminal, and the scheduler:
Plan -> --permission-mode plan
Default -> (no flag)
AcceptEdits -> --permission-mode acceptEdits
Bypass -> --dangerously-skip-permissions
Choices verified against `claude --permission-mode` on 2.1.226.
full_permissions is retained and effective_permission_mode() falls back
to it, so existing projects.json needs no migration.
Bug fix: triple-c-task-runner ran `claude -p ... --dangerously-skip-
permissions` unconditionally, ignoring the project's setting entirely.
It now reads TRIPLE_C_PERMISSION_MODE, which is injected into the
container, added to the reserved env blocklist, propagated through the
entrypoint's cron env filter, and tracked by a new
triple-c.permission-mode label so a change forces recreation.
Introspection: new commands/inspect_commands.rs exposes read-only views
into the container over docker exec — Claude sessions (parsed from
~/.claude/projects/<cwd>/<uuid>.jsonl), installed capabilities (skills,
agents, commands, hooks, plugins, natively-configured MCP servers), and
the triple-c-scheduler task list, logs and notifications.
Task/session ids are validated against a strict allowlist and every
parameterized call runs as a bare argv vector via bollard, so no shell
is involved. Stopped containers return empty results rather than errors.
No UI yet; that lands with the Project Home view.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DESIGN-REVIEW.md is Fable's review of the v0.3.0 UI: token gaps and three
WCAG AA contrast failures, the modal/accessibility audit, and an IA
proposal that promotes the project from a sidebar card to a tabbed
main-area view.
ROADMAP.md covers Claude Code feature coverage — the five settings.json
keys currently surfaced, the gaps worth closing, the ones deliberately
skipped, the authentication handoff design, and phase sequencing.
Also published as an artifact for easier reading.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Project Home (DESIGN-REVIEW §B2): the project is promoted from a 280px
sidebar card to a first-class main-area view. ProjectCard.tsx (1,257
lines) is replaced by a select-only ProjectRow plus tabs for Overview,
Sessions, Automation, Config and Files. The PortMappings, FileManager
and ContainerProgress modals are absorbed rather than reimplemented.
Config gains a Saved/Saving/Failed indicator — save-on-blur failures
previously reached only console.error.
Tier-1 polish (DESIGN-REVIEW §A): new elevation, muted-accent, disabled
and focus-ring tokens; a global :focus-visible ring with every
focus:outline-none removed; filled buttons moved to --accent-emphasis
and white-on-success toggles retired, fixing three WCAG AA failures
(2.1:1, 2.5:1, 2.4:1); a shared Modal primitive with role="dialog",
focus trap and restore, adopted by all remaining modals; status
indicators that carry a glyph and word rather than colour alone.
Ctrl+Shift+W closes a tab, deliberately not Ctrl+W — that is readline's
kill-word, used constantly in the terminal this app is built around.
Auth Bridge: a general loopback-callback bridge so browser logins run
inside a container (aws sso login, Concourse fly login, claude login)
can complete against the host browser. Listeners are discovered from
/proc/net/tcp{,6} — ss/netstat/lsof are absent from the image — bound on
host 127.0.0.1 only, and tunnelled in over the Docker API via socat,
which keeps working on Docker Desktop where container IPs are not
routable. Falls back to [::1] because Node resolves localhost to IPv6
first, so claude login often binds ::1 alone. Opt-in per project.
This extracts create_attached_exec() and moves the existing terminal
session path onto it, so there is one attached-exec implementation
rather than two.
Shared auth token: `claude setup-token` is run in a container, the token
is stored in the OS keychain and injected as CLAUDE_CODE_OAUTH_TOKEN
into Anthropic-backend projects. Contrary to the initial design note,
setup-token uses an Anthropic-hosted redirect and blocks on a stdin
paste prompt rather than a loopback callback, so a stdin command is
required for the flow to complete.
The token is never logged, never returned to the frontend, and is
redacted from the streamed output with a stateful matcher that withholds
any tail that could still grow into a secret. Change detection uses a
random rotation id rather than a hash, since a hash in a docker-inspect
readable label would be an offline verification oracle.
Frontend 33 -> 51 tests; Rust 34 tests. Both builds clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UI for the shared Claude token: a Settings section showing token state
with Authenticate and Revoke, an acquisition modal built on the shared
Modal (sign-in link handed to the host browser via the opener plugin,
plus the code input that answers `setup-token`'s stdin prompt — the flow
cannot complete without it), and a per-project opt-out toggle shown only
for the Anthropic backend.
Cancellation: acquire_claude_token previously had only two exits,
completion and a 15-minute timeout, and held the single-flight guard for
the whole time. Closing the dialog therefore locked the user out of
retrying for up to 15 minutes. Adds cancel_claude_token, backed by a
oneshot claimed and released in lockstep with the input guard, selected
on in the run loop so it wins the race and tears the exec down. The
dialog's Cancel now calls it and closes either way.
Also refreshes CLAUDE.md, which had drifted: it documented the deleted
ProjectCard, and asserted that new IPC commands need permission grants
in capabilities/default.json — they do not, that file covers plugin
commands only. Adds the conventions that would otherwise bite:
container_needs_recreation() is purely label-based and never diffs env,
so container-affecting state needs its own label; and #[serde(default)]
on a bool yields false regardless of intent.
Corrects the claim that Reset preserves credentials. Reset calls
remove_project_volumes, which deletes both the home and claude-config
volumes, so it wipes ~/.claude, the OAuth token, installed skills and
session transcripts.
84 frontend tests, 34 Rust tests, both builds clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reset is destructive in a way its name does not advertise:
rebuild_project_container deletes both project volumes, so it wipes the
claude login, anything installed in the container, and every saved
session transcript. It was a single unconfirmed click in the overflow
menu, while the comparably destructive Remove already confirmed. Adds
ConfirmResetModal, which names each loss and says explicitly that the
host-side mounted folders are untouched.
Docs: the user guides still described the pre-Project-Home UI. Sixteen
factually wrong statements corrected, including "expand the Config
panel" (six sites), the actions table (Reset and Remove are in an
overflow menu, Files is a tab), a progress modal that no longer exists,
a double-click-to-rename gesture ProjectRow never had, the Full
Permissions boolean, an incomplete reserved-env list, and the claim in
TECHNICAL.md that OAuth tokens survive a Reset. Both layout diagrams and
the project tree were rebuilt from the filesystem.
New sections cover permission modes with the exact CLI mapping, Project
Home, Sessions, capability tiles, Automation, shared authentication, the
Auth Bridge and its security posture, and keyboard shortcuts.
Known gap recorded rather than papered over: the Automation tab manages
existing scheduled tasks but cannot create them — no add command is
registered — so task creation remains `triple-c-scheduler add` in the
terminal.
87 frontend tests, 34 Rust tests, both builds clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the Automation tab: it could list, toggle, run, log and remove
tasks but not create them, so task creation still meant dropping to the
CLI. Adds add_scheduled_task and update_scheduled_task, plus a task
editor with cron presets and a plain-English reading of the expression.
Every field is free user text, so all of it goes to the scheduler as a
bare argv vector through bollard — no shell, no quoting. Validation is
shape-only rather than metacharacter scrubbing: length caps, no control
characters in single-line fields, no leading-dash name, absolute
working_dir. Verified by round-tripping a prompt containing
`; rm -rf /`, `$(id)`, backticks and newlines: it landed byte-for-byte
in the task JSON with nothing executed.
The scheduler CLI has no `edit`, so update is add-then-remove with the
add first — a rejected edit leaves the original intact. The new id is
surfaced in the editor rather than hidden.
Root-cause fix, and the more serious half of this commit:
triple-c-scheduler never validated --schedule, and rebuild_crontab
regenerates the entire crontab and pipes it to `crontab`, which rejects
the whole file if any line is malformed — with the error thrown away by
`2>/dev/null || true`. A single bad schedule therefore silently
unscheduled every other task in the container while reporting success.
Reproduced directly. It matters because the global CLAUDE.md tells
Claude to drive this CLI, so Claude could trigger it unprompted.
`add` now validates the expression and exits non-zero, and
rebuild_crontab reports a rejected crontab instead of swallowing it,
keeping the offending file for inspection. Verified against the real CLI
in this container: a bad schedule is refused without disturbing an
existing task's crontab entry, and `0 9 * * 1-5`, `*/30 * * * *`,
`0,30 8-17 * * *` and `0 0 1 1 *` are all still accepted. The Rust layer
validates independently, agreeing with vixie cron on 23 probed
expressions including `1/2` and `*/0` being invalid.
121 frontend tests, 44 Rust tests, both builds clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
build-windows failed on this PR with "linker `link.exe` not found",
while build-linux and build-container passed — the code was fine, the
runner environment was not.
The job installs Rust and Node conditionally but assumed the MSVC C++
toolchain was hand-provisioned. A runner without it registers normally,
advertises windows-latest, accepts the job, downloads the entire crate
graph and only then fails at link time. That also means a bare runner
coming online turns a job that would have queued for a capable machine
into a failed build.
Installs the VC++ workload when vswhere cannot find it, matching the
existing conditional Rust and Node steps. rustc locates MSVC through
vswhere and the registry rather than PATH, so no dev-shell activation is
needed. Installer exit 3010 (success, reboot pending) is treated as
success.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
%VSEXIT% and %ERRORLEVEL% inside a parenthesised cmd block are
substituted at parse time, not run time, so the installer's real exit
code was never read. Uses delayed expansion now.
Also checks for the .NET 3.5 runtime before building: WiX candle.exe
needs it, and Tauri aborts the whole bundle when the MSI target fails,
which silently suppresses the NSIS installer too. Fails early with the
exact dism command rather than at bundle time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WiX's candle.exe/light.exe are 32-bit. A SYSTEM-run runner has
%LOCALAPPDATA% under C:\Windows\system32\config\systemprofile, where
Tauri caches the WiX toolset — and WOW64 redirection sends 32-bit
processes reading System32 to SysWOW64, which has no such directory. The
CLR then fails to start with 0x80131700 and Tauri reports only "failed
to run candle.exe".
Verified: the same binary and identity exits 0 from C:\wixtest and
0x80131700 from the systemprofile path.
Pointing LOCALAPPDATA outside System32 avoids redirection, needs no
stored credential, and is a no-op for runners already running as a
normal user.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rust's `dirs` crate resolves LOCALAPPDATA on Windows through
SHGetKnownFolderPath, which reads the process token rather than the
environment, so the override changed nothing and 32-bit candle.exe still
hit WOW64 redirection under the SYSTEM profile.
Removing it rather than leaving a plausible-looking non-fix in the
workflow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WiX's candle.exe/light.exe are 32-bit. On a SYSTEM-run runner Tauri
caches WiX under C:\Windows\system32\config\systemprofile\..., and WOW64
redirection sends 32-bit processes to SysWOW64 where that directory does
not exist, so candle exits 0x80131700. Tauri aborts the whole bundle on
one target's failure, so the MSI was suppressing the NSIS installer too
and Windows produced no artifact at all.
NSIS is what the project already relies on for Windows upgrades. Drops
the .NET 3.5 gate, which existed only for WiX; keeps the MSVC step,
which is what makes the app link. Artifact collection now fails when no
installer is produced rather than tolerating an empty directory.
To restore the MSI, run the runner as a normal user and set
--bundles msi,nsis.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dropping the MSI did not help: makensis.exe is 32-bit like candle.exe
and failed the same way ("Unable to start child process, error 0x2").
The cause was WOW64 redirection sending 32-bit processes reading
C:\Windows\System32 to SysWOW64, where the toolset directory does not
exist.
The build VM now carries junctions from the SysWOW64 view of
systemprofile\AppData\Local\tauri and systemprofile\.cache to the
System32 originals. Verified on the runner: candle.exe reports WiX
3.14.1.8722 and makensis reports v3.11, both exiting 0 from the path
that previously failed.
Both targets build again, so the .msi comes back. Artifact collection
fails if either installer is missing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jknapp
merged commit aca6c49e3c into main2026-08-10 05:43:09 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Claude Code absorbed MCP natively, so Triple-C's own MCP server library is removed. That deletion set the direction for everything else here: Triple-C shows state and launches things; Claude Code edits its own config.
8 commits, 119 files, +14,230 / −4,726. Rust: 34 tests, zero warnings. Frontend: 33 → 87 tests.
What's in it
Plan / Default / Accept Edits / Bypasswired to--permission-modeProjectCard.tsx(1,257 lines) → tabbed main-area viewModalaws sso login, Concoursefly login,claude loginCLAUDE_CODE_OAUTH_TOKEN, per-project opt-outThings worth a reviewer's attention
The MCP removal had a real hazard. Old containers were created with
network_mode: triple-c-net-<projectId>. Deleting the network code outright would leave them unable to start — Docker refuses a container pointed at a missing user-defined network. There's a migration shim indocker/legacy_cleanup.rs: any container carrying a non-emptytriple-c.mcp-fingerprintor attached to atriple-c-net-*network is force-recreated onto the default bridge. Both the shim and the label write can be deleted a release later.A security improvement fell out of it. The Docker socket was being auto-mounted whenever a project enabled a stdio+Docker MCP server, independent of
allow_docker_access. That implicit mount is gone.Bugs fixed that weren't in scope. Scheduled tasks ran
--dangerously-skip-permissionsunconditionally, ignoring the project setting. Reset destroyed volumes with no confirmation while the less-destructive Remove confirmed. Cancelling authentication didn't cancel, locking out retries for 15 minutes. An invalid cron silently unscheduled every task in the container —rebuild_crontabpipes the whole crontab and swallowed the rejection with|| true.Docs corrected. README/HOW-TO-USE/CLAUDE.md all claimed Reset preserves credentials. It doesn't —
remove_project_volumesdeletes both volumes.Not verified
No agent could launch the Tauri GUI, so the Project Home restructure has never been seen running.
setup-tokencompletion needs a browser and a subscription, so its parser is validated against captured pty output rather than a real token; the keychain round-trip had no secret service available. The Auth Bridge was exercised end-to-end against a real container.Companion docs:
DESIGN-REVIEW.md,ROADMAP.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k
Project Home (DESIGN-REVIEW §B2): the project is promoted from a 280px sidebar card to a first-class main-area view. ProjectCard.tsx (1,257 lines) is replaced by a select-only ProjectRow plus tabs for Overview, Sessions, Automation, Config and Files. The PortMappings, FileManager and ContainerProgress modals are absorbed rather than reimplemented. Config gains a Saved/Saving/Failed indicator — save-on-blur failures previously reached only console.error. Tier-1 polish (DESIGN-REVIEW §A): new elevation, muted-accent, disabled and focus-ring tokens; a global :focus-visible ring with every focus:outline-none removed; filled buttons moved to --accent-emphasis and white-on-success toggles retired, fixing three WCAG AA failures (2.1:1, 2.5:1, 2.4:1); a shared Modal primitive with role="dialog", focus trap and restore, adopted by all remaining modals; status indicators that carry a glyph and word rather than colour alone. Ctrl+Shift+W closes a tab, deliberately not Ctrl+W — that is readline's kill-word, used constantly in the terminal this app is built around. Auth Bridge: a general loopback-callback bridge so browser logins run inside a container (aws sso login, Concourse fly login, claude login) can complete against the host browser. Listeners are discovered from /proc/net/tcp{,6} — ss/netstat/lsof are absent from the image — bound on host 127.0.0.1 only, and tunnelled in over the Docker API via socat, which keeps working on Docker Desktop where container IPs are not routable. Falls back to [::1] because Node resolves localhost to IPv6 first, so claude login often binds ::1 alone. Opt-in per project. This extracts create_attached_exec() and moves the existing terminal session path onto it, so there is one attached-exec implementation rather than two. Shared auth token: `claude setup-token` is run in a container, the token is stored in the OS keychain and injected as CLAUDE_CODE_OAUTH_TOKEN into Anthropic-backend projects. Contrary to the initial design note, setup-token uses an Anthropic-hosted redirect and blocks on a stdin paste prompt rather than a loopback callback, so a stdin command is required for the flow to complete. The token is never logged, never returned to the frontend, and is redacted from the streamed output with a stateful matcher that withholds any tail that could still grow into a secret. Change detection uses a random rotation id rather than a hash, since a hash in a docker-inspect readable label would be an offline verification oracle. Frontend 33 -> 51 tests; Rust 34 tests. Both builds clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Dropping the MSI did not help: makensis.exe is 32-bit like candle.exe and failed the same way ("Unable to start child process, error 0x2"). The cause was WOW64 redirection sending 32-bit processes reading C:\Windows\System32 to SysWOW64, where the toolset directory does not exist. The build VM now carries junctions from the SysWOW64 view of systemprofile\AppData\Local\tauri and systemprofile\.cache to the System32 originals. Verified on the runner: candle.exe reports WiX 3.14.1.8722 and makensis reports v3.11, both exiting 0 from the path that previously failed. Both targets build again, so the .msi comes back. Artifact collection fails if either installer is missing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>