Wave 2: the resilience + fleet fixes from dev (the canary's first guarded wave) - #1232
Merged
Merged
Conversation
The fleet projection publish invoked by `amico fleet status --projection` never passed a topology source to the fleet-authority CLI, so the publisher rendered the mode from its base default (standalone) and the validated-but-standalone projection landed in the stable cache on every run — clobbering enrolled machines' correct projections and flipping every consumer (installer, guard, extension fleet surfaces) to standalone. The publish now carries the machine's fleet.json (fleetTopologyPath — the live-layout convention, beside the projection cache) whenever that file exists; absent = unenrolled, and the flag is omitted so the honest base-default standalone semantics are unchanged. Hermetic default: the test world's checkFile is () => false so a suite run never leaks the runner machine's own fleet.json into the seam assertions.
…er excluded With dev as the integration branch and main the tested trunk, the mechanical pre-test of dev happens continuously on real fleet state: the canary runs on the hub + the mini, downloads CI's dev-head artifacts (universal VSIX), and never builds or touches the daily driver. Adds the Canary + Wave glossary terms (Developer tooling).
fix(fleet): pass --topology to the projection publish (#1194)
ADR-0021: the canary — fleet arms, CI artifacts, daily-driver excluded (+ glossary)
…nato re-scope (#1193) The fleet-gated regression pinned the hardware-loop skill's fence scope to Strumento, but the live skill's fence resolves to Intonato. Adjudication from history: the skill moved correctly, the pin was the drifted side. - The pin (and the 'ten' claim inventory) date to 504277c (#1002), when the skill's fence read `using Strumento # reexports Intonato ...`. - The 2026-09-10 freshness audit (armonissima#88, vault 10a93ce) deliberately updated the fence to `using Intonato` — the old `using` was the stale v0.2 reexport idiom: the seam inverted in Strumento v0.2, so Intonato (the chassis) now reexports the substrate (Strumento), not vice versa. - Inventory fallout: the old `Intonato` claim was minted by the old fence comment; the extracted claim set is nine now. Intonato coverage lives in the fence-scope pin and the reexport-chain inversion test. Red→green verified on a fleet machine (live checkouts mounted): the focused test failed with `expected [ 'Intonato' ] to deeply equal [ 'Strumento' ]`, then the full skill_drift_lint file passed 68/68. Stays mount-gated (CI-blind by design).
…efault branch (#1196) The #992 deploy-race guard hardcoded origin/main as its ahead-of-trunk baseline, so under the dev workflow (dev = default branch + integration target) every legitimate ref-build tripped the guard and forced the AMICODE_DEPLOY_OVERRIDE hotfix hatch. The baseline is now the repository's DEFAULT BRANCH, resolved dynamically after the fetch: `git remote show origin`'s HEAD branch first (authoritative — a stale local refs/remotes/origin/HEAD, the exact PR #1195 macbook failure, loses), then the symbolic ref, then the "main" fallback. The refusal + remedy lines name the resolved baseline verbatim, and the OVERRIDE record does too. Refusal semantics (#992: AHEAD/BEHIND/DIVERGED refuse, at builds plain, dirty refuses, overrides must be non-empty and recorded) are unchanged. evaluatePreflight now takes baselineSha/baselineRef instead of originMainSha; headRelation's params renamed to match. #992 pre-flight tests updated to pin the new baseline resolution, plus new #1196 tests: a tree merged into the default branch builds without override; a tree ahead of it still refuses with the remedy naming origin/<default>; and the resolveDefaultBranch preference order (remote-show > symbolic-ref > fallback) is pinned, including the stale-origin/HEAD case.
fix(skills): the hardware-loop fence-scope pin follows the deliberate Strumento v0.2 reexport inversion (#1193)
…un at the integration tip (#421 first-light finding) The canary (amicissimo#421) requires conclusion==success at the exact dev SHA before it runs anything. CI ran on PRs and main pushes only, so the dev tip — a merge commit — never had a run: the gate would report not-blessed forever. Found by the canary's first real run.
ci: run on dev pushes — the canary's blessed-SHA gate needs CI at the integration tip
* fix(engine): stop using assistant prefill for max-steps prompt (#1179) * fix(engine): stop using assistant prefill for max-steps prompt The MAX_STEPS_PROMPT was injected as an assistant-role message at the end of the messages array when an agent hit its step limit. The Bedrock Converse API rejects this for models that do not support assistant message prefill, producing: "This model does not support assistant message prefill. The conversation must end with a user message." Changes: - Send the MAX_STEPS_PROMPT as a user message instead of assistant prefill in both V1 (prompt.ts) and V2 (llm.ts) session runners. The instruction works identically as a user message — the model still gets told to summarize and stop using tools. - Add toolChoice: "none" on the V1 path's last step (the V2 path already had it) as a belt-and-suspenders guard against tool calls. - Create an overlay for provider/error.ts that treats the string "undefined" the same as an empty message, so SDK adapters that coerce JS undefined into the string "undefined" no longer produce "undefined: <actual error>" in the chat panel. * fix(app): detect stale rebuild state on mount via timestamp When a rebuild was interrupted (window reload, extension crash, branch switch), the "rebuilding" localStorage flag persisted but the build process was dead. The existing 5-minute live timeout was too slow — it restarted on every Settings open, so the user saw "Rebuilding..." stuck indefinitely. Fix: store a timestamp when the rebuild starts. On mount, if the build started more than 2 minutes ago and never completed, immediately transition to "failed" with "Rebuild was interrupted" instead of restoring the spinner and waiting 5 minutes. The live timeout stays as a secondary guard for builds that stall during the current mount. * fix(server): harden adoption gate + fix rebuild status delivery (#1200) Three independent fixes for the kill-free rebuild path: 1. Adoption retry: when the gate verdict is "stale" but the PID is alive, wait 1 s and retry the four live checks. This covers the transient unreachable window during a reload (the server flushes dying connections from the old extension host). 2. Health-check timeout: bump probeHealth and challengePassword from 2 s to 5 s — the tight timeout was a plausible cause of transient adoption failures after a window reload. 3. Adoption logging: log all four gate inputs and the verdict to the output channel so a failed adoption is diagnosable from the log. 4. Rebuild status delivery: await the postMessage Thenable for the "done" message before starting the 300 ms reload timer. Without this the reload can race the two-hop delivery (extension → shell → iframe) and the localStorage flag never lands, leaving the Settings dialog stuck on "Rebuilding...". 5. Rebuild phase detail: the building-binary message now says "Overlay changed — rebuilding engine binary (server will restart)" so it is clear WHY the server restarts (the overlay hash differs from the cached hash, meaning the engine source tree changed between builds). The BridgeIo.postToWebview return type widens from void to void | Thenable so the single await site works without touching every other call site. * fix(lineage): repair orphaned entries + propagate diff_version to parents (#1136) (#1174) * fix(app-bundle): omit-agent promptAsync continues session's current agent (#1206) (#1207) The deprecated amicode_ask tool ends the assistant turn and the user's button-click answer arrives via session.promptAsync with no agent field. createUserMessage fell back to agents.defaultInfo() — the global default_agent "plan" — silently flipping an active develop/research session into plan mode mid-campaign. Server fix: hoist the session fetch in createUserMessage and resolve agent as input.agent → session's tracked current agent → global default. Fresh sessions (no tracked agent) still reach defaultInfo() as before. Client fix: thread the session's current agent into both promptAsync calls in message-timeline.tsx (onAsk and widget-prompt) that omitted it — belt and suspenders alongside the server fix. Regression test: omit-agent prompt on a session whose tracked agent is "build" produces a user message stamped "build", not "plan". Closes #1206 * fix(app-bundle): Bedrock cachePoint after reasoning block wedges the session (#1209) Amazon Bedrock rejects "ValidationException: Cache point cannot be inserted after reasoning block" when a cachePoint sits immediately after a reasoning block. Message-level bedrock.cachePoint is emitted after all content parts, so assistant turns that end in reasoning fail on every retry until the message ages out of history — surfaced in-app as the cryptic `undefined:` error prefix. Patch the engine overlay (the source of truth) to anchor the cache breakpoint on the last non-reasoning part, or skip caching for reasoning-only messages: - packages/llm/src/cache-policy.ts (markMessageAt) - packages/opencode/src/provider/transform.ts (applyBedrockCacheOptions) - packages/llm/src/protocols/bedrock-converse.ts (Converse lowering note) plus 6 regression tests. manifest.json updated surgically (+6 entries). Cherry-pick of anomalyco/opencode#36532 (auto-closed unmerged upstream; never present in our pinned base 7fe99387). Verification: drift-gate in sync; materialize verifies the manifest; llm 43/43 and opencode transform 402/402 (incl. 6 new) green in the materialized tree; build:binary compiles and the smoke test passes. Co-authored-by: Jackson Turner <jacktrnr@jacksons-mbp.mynetworksettings.com> * feat: Slack MCP via slack-mcp-server + OAuth PKCE login (#1037) (#1158) * feat(amico-run): `amico slack login` — OAuth PKCE flow (#1156) Adds the `amico slack login` CLI command that runs an OAuth 2.0 PKCE flow: - Starts a temporary HTTP server on localhost:54213 - Opens the browser to Slack's authorize endpoint with S256 challenge - Receives the callback, verifies state (CSRF), exchanges code for a xoxp-* user token via oauth.v2.access - Writes the token to ~/.amico/slack.json (atomic, 0600, AMICO_SLACK_FILE override respected) - 120s timeout, structured error handling for denied consent, state mismatch, EADDRINUSE No client_secret — PKCE only (public client). Part of #1037 (revised approach per Jack's review). * feat(extension): conditional slack-mcp-server MCP entry in config builder (#1157) When a Slack credential exists (readCredential('slack')), the config builder now includes a 'slack' MCP server entry that spawns slack-mcp-server with SLACK_MCP_XOXP_TOKEN threaded into its environment. No credential → no entry → tools invisible to the agent. Minimal-env graft per ADR 0002: only SLACK_MCP_XOXP_TOKEN and SLACK_MCP_ADD_MESSAGE_TOOL are exported to the child process. Part of #1037 (revised approach per Jack's review). * feat(skill): rewrite amico-slack SKILL.md for slack-mcp-server (#1041) Replaces all CLI references (amico-slack send/read/whois/status) with slack-mcp-server MCP tool names (conversations_history, conversations_replies, conversations_add_message, channels_list, users_search, etc.). Key changes: - Dialect switch: text/markdown (not Slack mrkdwn) — bold is **bold** - Soft preview norm for sends documented - LaTeX-to-Unicode conversion guidance preserved - Connection hint: 'amico slack login' for authentication - cli_tool frontmatter key removed - Messages sent as user, not bot Also updates ADR 0016 from proposed → accepted with revised approach note. Part of #1037 (revised approach per Jack's review). * feat(connections): Slack OAuth PKCE flow in the Connections panel (#1037) Wire Slack OAuth directly into the Connections panel so users can connect Slack by clicking 'Connect with Slack' — no terminal command needed. UI changes: - connectionAuthMethods() returns ['browser', 'token'] for Slack - Connection picker shows 'Connect with Slack' button as primary, with token paste as fallback for power users Server changes (extension + overlay mirror): - startAuthResponse() now handles id='slack' with a full PKCE flow: starts a temp HTTP callback server on localhost:54213, opens the browser to Slack's authorize endpoint, exchanges the code for a xoxp-* user token, writes it to ~/.amico/slack.json (atomic, 0600), and persists 'connected' status for the panel to read - 120s timeout on the callback server - Error/success HTML pages rendered to the browser Test: updated the 'non-google ids refuse browser auth' assertion to verify Slack browser auth returns waiting-browser (not a refusal). Part of #1037 (revised approach per Jack's review). * chore: refresh overlay manifest for Slack OAuth UI changes * fix(app-bundle): auto-refresh manifest in dev mode on mismatch When materialize.mjs detects a manifest mismatch and OPENCODE_CHANNEL is not 'prod' (i.e. local dev builds), auto-run refresh_manifest.mjs and re-verify instead of failing. This prevents the 'Rebuild locally' button from aborting when overlay files are edited — overlay edits are expected in dev, and requiring a manual refresh_manifest.mjs step was a deploy- mechanics tax that broke the Connections panel UI changes for #1037. CI (channel=prod) still fails on manifest mismatch as before. * feat: read AMICODE_SLACK_CLIENT_ID from env with shipped default fallback All three OAuth entry points (CLI verb, extension connections, overlay connections) now read the Slack client_id from the AMICODE_SLACK_CLIENT_ID env var first, falling back to the shipped default. This lets users test with their own Slack App immediately, and once Harmoniqs registers the production app, the hardcoded default is updated and no env var is needed. Part of #1037. * feat(connections): Slack App client_id setup flow in Connections panel End users from any Slack workspace can now configure Amicode's Slack connection without touching environment variables: 1. Click 'Connect with Slack' → if no Slack App is configured, the error message explains what's needed 2. Expand 'Slack App not set up?' in the picker → paste the Client ID their admin gave them → Save 3. Click 'Connect with Slack' again → OAuth flow starts Client ID resolution (all three OAuth entry points): env var AMICODE_SLACK_CLIENT_ID → ~/.amico/slack-app.json → empty (error) The picker saves the client_id via a 'slack-app-client-id' pseudo- credential submission that writes ~/.amico/slack-app.json (0600). Once Harmoniqs registers a production Slack App, the shipped default replaces the empty string and no setup step is needed. Part of #1037. * fix(connections): cleaner Slack picker — 'Ask your Slack admin for your Client ID' Simplified the Slack connection picker: - Primary prompt: 'Ask your Slack workspace admin for your Client ID' - Client ID input field is front-and-center (not hidden in a details section) - 'Save & Connect' saves the client_id AND triggers the OAuth flow in one click - 'Don't have a Client ID?' expandable with admin instructions - Token paste remains as advanced fallback Part of #1037. * fix(build): include overlay file hashes in staleness check Both build:binary and build:app used overlay_sha + promoted_at to detect stale materialized trees — but those fields are set at extraction time and don't change when overlay files are edited locally. Local edits went undetected, so 'Rebuild locally' silently shipped stale UI. Now the staleness stamp includes a hash of manifest.files (the per-file content hashes that refresh_manifest.mjs updates). Combined with the auto-refresh in materialize.mjs, the chain is: overlay edit → build:binary materializes → manifest mismatch in dev → auto-refresh manifest (updates file hashes) → new stamp written → build:app sees fresh stamp → reuses the correct tree → UI matches source Part of #1037. * fix(connections): show Slack logo in form, fix external link in webview Two fixes to the Slack connection picker: 1. The Slack form header now shows the Slack icon + bold name instead of the raw id text, consistent with the catalog list's rendering. 2. The api.slack.com/apps link was a plain <a target="_blank"> which is dead inside the VS Code webview iframe. Replaced with an onClick handler that uses the open-external postMessage bridge (the same pattern footer.tsx and home-cards.tsx use). Part of #1037. * fix(connections): move admin hint inside the Client ID dropdown The 'Ask your admin' line was sitting above the input as always-visible text. Folded it into the 'Don't have a Client ID?' details block so the form is cleaner — icon, input, button, with help tucked away for those who need it. Part of #1037. * fix(slack): send client_secret in OAuth token exchange Slack's oauth.v2.access requires client_secret even when PKCE parameters are present — PKCE is additive security on top of the secret, not a replacement. This caused 'bad_client_secret' on every token exchange. Changes: - slack_verb.ts: readSlackAppCredentials() reads both client_id and client_secret from env vars or ~/.amico/slack-app.json; exchangeCode() sends client_secret in the POST body; credential guards re-read on every call (no stale module-level IIFE) - connection-picker.tsx: Slack form now collects both Client ID and Client Secret fields; dropdown updated to 'Where do I find these?' - connections.ts (extension + overlay): submitCredentialResponse handles new 'slack-app-credentials' id carrying JSON {client_id, client_secret}; startAuthResponse reads client_secret alongside client_id - Test isolation: connection test backs up / restores real slack-app.json so the 'no config' refusal test passes on machines with Slack configured Part of #1037. * fix(build): always clear stale vite dist before app build The overlay staleness check correctly re-materializes source files, but vite's incremental build inside the materialized tree can reuse its own cached dist output — serving a stale UI even though the source changed. This was the root cause of 'Rebuild locally' not picking up UI changes. Now build_app_bundle.mjs unconditionally clears packages/app/dist and the .vite cache before running vite build. The 14s vite build is cheap compared to shipping a stale UI. Part of #1037. * feat(connections): auto-refresh status after browser OAuth completes After a browser-based OAuth flow starts (Slack, Google), the panel now polls /amicode/connections every 2s until the connection state settles. Previously the OAuth callback wrote the credential on a background HTTP server but the panel had no way to know — the user had to close and reopen the status popover to see 'connected'. The poll stops automatically when all connections leave the waiting-browser/validating states (the OAuth completed), or after a 2-minute timeout. The interval is cleaned up on component unmount. Part of #1037. * fix(slack): restart server on credential change so MCP entry updates Part of #1037. Closes #1204. * fix(status): scope auth poll to target connection + unstick rebuild flag Two fixes for status popover stability: 1. Browser-auth polling (Slack/Google OAuth) now tracks which connection id started the auth and stops as soon as THAT connection settles. The previous code polled every 2s checking ALL connections, causing the Connections tab to re-render repeatedly while the user was browsing it. 2. The 'Rebuilding...' status could get stuck when the 'done' postMessage raced the window reload (extension → shell → iframe three-hop path). Reduced the stale-build detection from 2 minutes to 30 seconds: if the window reloaded and the extension host is healthy, the build completed and the message was lost — treat as success, not 'still building'. Also writes a rebuild-done marker file as belt-and-suspenders. Part of #1037. * feat(engine): dynamic MCP add/remove for Slack connect/disconnect Part of #1037. Addresses #1205. - Add `remove` method to the MCP service Interface and implementation (idempotent: removing a non-existent server is a no-op) - Add `DELETE /mcp/:name` HTTP route (groups/mcp.ts + handlers/mcp.ts) - Refactor slack_watcher to pass `{ exists: boolean }` to the callback - Extension now calls POST /mcp (add) or DELETE /mcp/slack (remove) dynamically instead of restarting the server - Falls back to server restart if the API call fails - OPENCODE_CONFIG_CONTENT unchanged (still includes Slack at startup) * fix(connections): auth poll fetches independently of popover visibility The browser-auth poll called refetchConnections() which is gated on the popover being visible (the resource's source signal is undefined when shown() is false). During OAuth the user is in the browser with the popover closed, so every poll tick was a no-op — the status never updated. Now the poll fetches /amicode/connections directly (independent of the resource), checks the target connection's state, and only calls refetchConnections() once the OAuth completes (to warm the cache for when the popover reopens). The poll is fully self-contained: it works whether the popover is open, closed, or never opened again. Part of #1037. * fix(engine): emit ToolsChanged after dynamic MCP add The MCP add method (POST /mcp) registered the server and spawned the process but never published a ToolsChanged event. The UI listens for mcp.status.changed SSE events to refresh the MCP tab — without the event, a dynamically added server (like slack-mcp-server after OAuth) was invisible until a manual restart. Now add publishes ToolsChanged, matching what remove already does. Part of #1037. * fix(slack): retry watcher API call when server isn't ready yet The watcher's credential-change callback silently skipped when opencodeReadyUrl was undefined (server not ready). This happens right after a window reload or server restart — the token write from OAuth often lands in that window. The watcher detected the file but the API call never fired. Now retries up to 5 times (1s apart) waiting for the server to become ready before giving up. Part of #1037. * chore(overlay): drop posture-indicator-view — upstream adopted it The fork now ships posture-indicator-view.tsx identically; the overlay copy is a duplicate the drift gate rejects. Remove the overlay file and its manifest exception. * chore(overlay): drop 20 fork-adopted duplicates + add archive sha The upstream fork adopted 20 overlay files identically. The drift gate rejects these as exception-duplicates. Removed the overlay copies, their manifest exceptions, and re-pointed 15 tests from overlay/ to .materialized/ imports. Also added upstream_base_archive_sha256 to the manifest (required when the CI cache has the archive stamp). Drift gate passes locally. * fix(tests): remove tests for upstream-adopted overlay files 11 test files imported from overlay copies that upstream adopted — the overlay files were deleted by the drift-gate fix, breaking both local (.materialized fallback) and CI (no materialized tree). These tests belong with the upstream code now; delete them. Remaining tests that reference the overlay all point at files still committed in the overlay tree. Part of #1037. * docs: ADR 0021 (pause/resume/redirect subagents) + CONTEXT.md glossary Record the design decision for #1208 — Pause, Resume, and Redirect as session-level primitives. ADR covers the decision shape, the foreground- parent edge case, the upstream-base boundary, slice decomposition, and rejected alternatives. CONTEXT.md gains four glossary terms under Agentic work: Pause, Paused, Resume (session control), and Redirect — with Avoid lines disambiguating from cancel/stop/abort and the existing Resume-session nav widget. Refs: #1208 * docs: ADR 0021 — research layout is a resolved contract, not a fixed directory tree Reframe the research project/environment layout as a resolved contract (role → path) rather than a frozen directory constant. Three profiles (separate, monorepo, standalone) reduce to the same contract; the default is byte-identical to today's layout. Fixes two production defects as prerequisites: - The injected project/environment context blocks are dead (env vars never passed to the server spawn) - The research agent card and skill contradict each other on whether the vault or the project is the source of truth Part of #1210. --------- Co-authored-by: Jackson Turner <32987603+jacktrnr@users.noreply.github.com> Co-authored-by: Jackson Turner <jacktrnr@jacksons-mbp.mynetworksettings.com>
…s fleetReady or offers standalone (#1202) The fleet client's checkFleet flipped fleetReady off on ONE failed probe (1.5s timeout) and fired the 'Go Standalone?' popup after 10s, repeatedly, through multi-minute tunnel degradation windows (the 2026-09-15 live log: 37→119 consecutive failed checks during active use). Extract the threshold decision into FleetPollHysteresis (pure, injectable — the server_keepalive #1187 pattern: DEFAULT_*_THRESHOLD + consecutive-failure counting), keep extension.ts's wiring thin: - down-flip (fleetReady=false + status bar) only at 15 consecutive failures (≈30s at the 2s poll); a blip logs a single 'holding' line - standalone offer only after a SUSTAINED outage (150 consecutive ≈ 5 min), once per outage, never re-offering within a 15-min cooldown that survives a recovery — flapping cannot extract repeated popups - up-transition behavior unchanged (SSE re-attach, panel reveal, provider signal); a successful probe clears the consecutive count - thresholds configurable via amicode.fleetTunnelDownConsecutiveFailures / fleetStandaloneOfferConsecutiveFailures / fleetStandaloneOfferCooldownMinutes (0 = the default, the D6 tuning precedent)
…l + honest send refusal + stale-resource read gate On stream loss the session view now STAYS MOUNTED: a reconnecting veil (v2 tokens, pointer-events none) over the last rendered timeline, cleared in place on gap-clear — no reload, no session-list refetch on the reconnect path. A send during the gap refuses honestly (toast, before history/clear) — never a fake success. The #638 staleness machine's transitions are unchanged; the render response is what changed. The session page's cached-timeline read is gated on messagesReady() so a failed refetch stops suspending the whole view.
fix(extension): fleet-poll hysteresis — a blip holds, a sustained outage offers once (#1202)
…le check) The new session-stream-veil/stream-veil-view/stream-gap files plus the 30 changed overlay paths' hashes — the manifest is the overlay's provenance record; the drift gate catches unregistered files by design.
…e-loss fix(app): keep the session view mounted across SSE loss (#1203)
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
- transform.ts: dev's overlay state + main's Bedrock reasoning fix ported (the adapter's silent-drop bug — dev's syncs predate 568306f) - transform.test.ts: main's (the fix's tests) - manifest.json: the union of both sides' overlay hashes
- the manifest re-hashed against the merged overlay (826 files, the union of both sides' entries, the disk as truth) - packages/app/public/amico.svg: the symlink materialized as its content (695B) — the manifest records content hashes, and CI's gate (green on dev) hashes the materialized file; the symlink form was an environmental asymmetry that broke the gate on symlink-materializing checkouts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The first wave the canary would have blessed: dev carries the fleet-poll hysteresis (#1230, #1202), the session-view SSE-loss resilience (#1231, #1203), the fence-scope pin repair (#1198), the pre-flight default-branch baseline (#1199), ADR-0021 + the canary glossary (#1197), and the --topology projection fix (#1195). CI green at the dev tip (conclusion success — the canary's blessed-SHA gate verified it). Deploying to main so the hub's served app carries the veil — the user is living the symptom this wave cures.