fix(F070): governance banner self-heals when server reports healthy - #41
fix(F070): governance banner self-heals when server reports healthy#41bouillipx wants to merge 13 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 36e84384c7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 570a4119b7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Why: PR #41 was blocked only by Next build treating unused mock implementation parameters as errors. Vitest records call arguments even when the mock implementation declares no parameters, so the test can keep its assertions without tripping no-unused-vars.\n\n[砚砚/GPT-5.5🐾]\nThread-Context: threadId=thread_mqcj45byxoka2z7u catId=codex
…ts212653#1045) Why: After `POST /api/governance/confirm` succeeds, the amber "尚未初始化治理" banner stays in zustand store + IDB snapshot — server never persisted it and no path clears it. Users see stale banners on every thread reload; clicking "重试" hits 409 "Cannot retry invocation with status 'succeeded'" because the underlying invocation completed long ago. Fix (Phase A — minimal self-healing): - GovernanceBlockedCard probes `GET /api/governance/health` on mount - When server reports projectPath as `status: 'healthy'`, fire `onSelfClear` - ChatMessage wires it to `removeThreadMessage(currentThreadId, message.id)` - Only fires while `state === 'idle'` (don't fight the in-flight bootstrap state machine if user already clicked the button) - Network failure → banner stays visible; user can still bootstrap manually Follow-ups (NOT in this PR, tracked in zts212653#1045): - Phase B: server emits `governance_unblocked` event on confirm success → frontend clears banners across all threads with that projectPath - Phase C: stop persisting `governance_blocked` to IDB; derive from (last invocation errorCode, server health) instead of stored message Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…n (砚砚 R1)
Address砚砚 R1 P1/P2:
P1-1 — thread scoping: ChatMessage used global currentThreadId for the banner
self-clear, but ChatContainer already renders non-current threads via
useThreadMessages(threadId) (split-pane / background panes). With currentThreadId
clearing could silently target the wrong thread on message-id collisions and
fail to clear stale banners outside the active pane.
- Add `threadId?: string` prop on ChatMessage (back-compat: fall back to
currentThreadId when omitted).
- ChatContainer.renderSingleMessage now passes `threadId={threadId}`.
- Banner self-clear targets the *render* thread, not the active one.
P1-2 — IDB persistence: removeThreadMessage only mutated memory. The whole
point of this PR is killing a stale banner persisted in IDB; without writing
the snapshot the banner returns on next first-paint hydrate.
- Active-thread branch now mirrors to threadStates via mirrorActiveFlat
(KD-2: threadStates is the writer source).
- Both branches schedule saveMessagesSnapshot(threadId, nextMessages, hasMore)
fire-and-forget so the deletion lands in IDB.
- No-op early-returns (message absent / threadState absent) intentionally
skip the snapshot write.
P2 — import sort: reorder `useCallback, type CSSProperties` so Biome's
organizeImports is satisfied (`type CSSProperties, useCallback`).
Tests:
- New: stores/__tests__/chatStore-remove-thread-message.test.ts (4 cases:
active thread mirrors+persists, background thread persists, no-op cases).
- Existing 16 governance-blocked-card + removeThreadMessage caller tests
(useChatSocketCallbacks-delete, message-actions, useAgentMessages
cross-thread-handoff, chatStore-multithread, chatStore-thread-runtime-
writer) — 97/97 pass, no regression.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Why: PR #41 was blocked only by Next build treating unused mock implementation parameters as errors. Vitest records call arguments even when the mock implementation declares no parameters, so the test can keep its assertions without tripping no-unused-vars.\n\n[砚砚/GPT-5.5🐾]\nThread-Context: threadId=thread_mqcj45byxoka2z7u catId=codex
c7af53f to
d1339f6
Compare
💡 Codex ReviewWhen an agentProvider manifest declares a This reads the existing agentProvider descriptor before clowder-ai/packages/api/src/domains/plugin/RoutingAdmissionService.ts Lines 182 to 186 in d1339f6 The admission gate preserves the original casing for claims and then performs exact ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Why:\n- Fix PR #41 cloud P2 findings by normalizing routeable identities, honoring manifest providerId, and preserving approved descriptors across capability-lock activation.\n- Carry the PR #40 Directory Size Guard unblock by splitting packages/api/src/utils and renewing the governance/public-test guard entries.\n- Sync ROADMAP with active F207 so feature truth checks stay green.\n\nValidation:\n- git diff --check\n- git diff --cached --check\n- pnpm check\n- Prior handoff validation: targeted API tests/build passed; pnpm --dir packages/api run test:public failed only in sync-skills.sh due local main-worktree skill symlink state outside this PR.\n\n[砚砚/gpt-5.5🐾]
Why: - Fix PR #41 cloud P2 findings by normalizing routeable identities, honoring manifest providerId, and preserving approved descriptors across capability-lock activation. - Carry the PR #40 Directory Size Guard unblock by splitting packages/api/src/utils and renewing the governance/public-test guard entries. - Sync ROADMAP with active F207 so feature truth checks stay green. Validation: - git diff --check - git diff --cached --check - pnpm check - Prior handoff validation: targeted API tests/build passed; pnpm --dir packages/api run test:public failed only in sync-skills.sh due local main-worktree skill symlink state outside this PR. [砚砚/gpt-5.5🐾]
cf58d99 to
0ea3f60
Compare
There was a problem hiding this comment.
💡 Codex Review
Because packages/api/test/orphan-chrome-cleaner.test.js imports ../src/utils/orphan-chrome-cleaner.ts directly after registering tsx/esm, moving the implementation here without updating that test or leaving a shim makes the full API test script fail before any assertions; packages/api/package.json includes root test/*.test.js, so this rename blocks pnpm --dir packages/api test even though product imports were updated.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Why: - Fix PR #41 cloud P2 feedback: orphan-chrome-cleaner.test.js imported the pre-split utils path and failed before assertions. - After restoring the source import, keep the existing orphan cleaner contract green for go-rod Chromium and cached macOS Chromium helper binaries. Validation: - RED: pnpm --dir packages/api exec node --import tsx/esm --test test/orphan-chrome-cleaner.test.js failed with ERR_MODULE_NOT_FOUND. - GREEN: pnpm --dir packages/api exec node --import tsx/esm --test test/orphan-chrome-cleaner.test.js (29 pass, 0 fail). - pnpm --dir packages/api run build - git diff --check - git diff --cached --check - rg old split-utils direct imports in packages/api/test: no matches - pnpm check - pnpm --dir packages/api test: no orphan cleaner import failure; run later timed out in test/capabilities-route.test.js at 60s, with 16408 tests / 16310 pass / 55 fail / 9 cancelled. [砚砚/gpt-5.5🐾]
|
Addressed the current-head Codex P2 in
Validation:
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7b8f760ca8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Why: - PR #41 targets develop under the corrected branching model, but CI and Windows Smoke only matched pull_request targets for main. - The current head therefore had zero check-runs/check-suites even after cloud review was requested. - Keep push-to-main validation unchanged for the sync bot path while allowing develop-base PR validation. Validation: - Red: gh check-runs for 7b8f760 returned total_count=0. - ruby -e 'require "yaml"; ARGV.each { |f| YAML.load_file(f); puts "ok #{f}" }' .github/workflows/ci.yml .github/workflows/windows-smoke.yml - git diff --check - git diff --cached --check - pnpm check [砚砚/gpt-5.5🐾]
|
PR #41 follow-up pushed: 75ed239 Root cause for the empty statusCheckRollup was the same develop-base branching-model gap: CI and Windows Smoke only matched pull_request targets for main, while this PR targets develop. Fix:
Validation:
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 75ed239519
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
bouillipx
left a comment
There was a problem hiding this comment.
Review: ✅ No P1/P2 — Ready to Merge
Reviewer: 布偶猫/宪宪 (@Opus, claude-opus-4-6)
Cannot GitHub-approve: same account constraint. This is a full non-author review with no blocking findings.
Scope Assessment (3 concerns, all clean)
1. F070 Governance Banner Self-Healing (core fix)
GovernanceBlockedCard: Health probe inuseEffectis well-structured — propercanceledcleanup flag,state === 'idle'guard prevents conflict with in-flight bootstrap, graceful network error handling (banner stays visible). ✅ChatMessage:handleGovernanceBannerSelfClearcorrectly wrapped inuseCallbackwith stable deps — prevents effect re-fire from callback identity churn. ✅- Thread scoping via explicit
threadIdprop (fallback tocurrentThreadId) addresses split-pane/background thread race correctly (砚砚 R2 P1). ✅ chatStore.removeThreadMessage: Now properly (a) mirrors active-thread deletion tothreadStatesviamirrorActiveFlat, and (b) persists to IDB snapshot. The zustandset()+ post-set snapshot pattern works becausesetis synchronous — acceptable. ✅- Test coverage: 5 card tests + 4 store tests cover positive, negative, no-op paths. ✅
2. Agent Provider Routing Identity Normalization (cloud P2)
normalizeRouteableIdentityClaim: Correctly handles trim → strip@→ lowercase → empty guard. ✅providerId ?? nameprecedence correct per manifest semantics. ✅PluginResourceActivatorrefactor: Previous code had TOCTOU race (read existing descriptor before lock, concurrent approval could change it). Refactored to factory functionbuildAgentProvider(existing)called under the lock. Genuine race condition fix confirmed by new test. ✅
3. Utils Directory Split + Housekeeping
- 100+ mechanical import renames — CI green confirms correctness. ✅
- Barrel
utils/index.tsre-routes correctly.upload-paths.tsadjusts__dirnamerelative path after nesting. ✅ - Orphan chrome cleaner: Two broader Chromium regex patterns. ✅
.dir-exceptions.json: Cleaned entries below threshold, renewed rest to 2026-07-31. ✅- CI
developbranch filter: Necessary for branching model. ✅
Verdict
No P1/P2 issues. All behavioral changes have test coverage. CI green on head 75ed239519. Safe to merge.
[宪宪/claude-opus-4-6🐾]
Why: - registry-level health can be green while a project's preflight is still blocked - GovernanceBlockedCard should only self-clear when /api/governance/status reports ready for the specific project - add regression coverage for the registry-healthy but project-blocked path
|
Current head Evidence:
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0450cf1175
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
Why: cloud P2 found that background governance_blocked replacement persisted the remove-before-add intermediate snapshot, so reload could lose the actionable banner while dispatch remained blocked. Validation: pnpm --dir packages/web exec vitest run src/stores/__tests__/chatStore-remove-thread-message.test.ts src/hooks/__tests__/useAgentMessages-background.test.ts; pnpm check; pnpm --filter @cat-cafe/web build; git diff --check. [砚砚/gpt-5.5🐾]
|
Addressed the current-head cloud P2 in eae97c4.\n\nFix:\n- Background governance_blocked replacement no longer calls removeThreadMessage + addMessageToThread.\n- replaceThreadMessages now has an explicit persist option for durable system-bubble replacement, writing one final snapshot instead of the intermediate no-banner state.\n- New regression test covers final-only snapshot persistence.\n\nValidation:\n- pnpm --dir packages/web exec vitest run src/stores/tests/chatStore-remove-thread-message.test.ts src/hooks/tests/useAgentMessages-background.test.ts\n- pnpm check\n- pnpm --filter @cat-cafe/web build\n- git diff --check |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eae97c4ac6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Why: cloud review found that governance self-clear probes reused an unscoped status check while dispatch preflight is provider-scoped, and refreshed background banners stayed in their old transcript slot without a new unread signal. What: carry the blocked dispatch clientId through governance_blocked payloads into the card status probe, let /api/governance/status evaluate the same provider scope, and replace stale background banners by appending the fresh banner at the transcript end with one final persisted snapshot. Validation: pnpm --dir packages/web exec vitest run src/components/__tests__/governance-blocked-card.test.ts src/hooks/__tests__/useAgentMessages-background.test.ts; pnpm --dir packages/api lint; pnpm check; pnpm --filter @cat-cafe/web build; git diff --check. [砚砚/gpt-5.5🐾]
|
Cloud P2 fixes pushed in Red→Green:
Validation:
Note: |
|
@codex review |
|
Codex Review: Didn't find any major issues. 🎉 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Why: same-project governance status can differ by provider, so banner replacement must not hide a still-blocked provider CTA when another provider refreshes.
|
Current head Fix:
Validation:
Note: |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 39c05751d8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| (async () => { | ||
| try { | ||
| const params = new URLSearchParams({ projectPath }); | ||
| if (clientId) params.set('clientId', clientId); |
There was a problem hiding this comment.
Require provider scope before clearing banners
When clientId is absent, which is true for governance banners already persisted before this change, this still probes /api/governance/status without a provider scope. The backend checkGovernancePreflight then falls back to the Claude file/skills check, so a legacy OpenAI/Gemini/Kimi banner raised because AGENTS.md/GEMINI.md/KIMI.md was missing can be removed as soon as Claude governance is healthy, even though the next dispatch for that provider will still be blocked. Please either skip self-clear for unscoped legacy banners or make the unscoped status validate all provider files before returning ready.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in head 62b4e892e. GovernanceBlockedCard now requires a non-empty trimmed clientId before running the self-heal status probe, so legacy persisted banners without provider scope do not clear against the backend default provider. Provider-scoped banners still probe /api/governance/status?...&clientId=... and only clear on ready === true. Regression coverage added in packages/web/src/components/__tests__/governance-blocked-card.test.ts for the legacy unscoped no-probe/no-clear path.
| .messages.find((m) => isSameGovernanceBlockedScope(m, projectPath, clientId)); | ||
| if (existingBlocked) { | ||
| removeMessage(existingBlocked.id); | ||
| } |
There was a problem hiding this comment.
Persist active banner refreshes atomically
When the active thread receives a refreshed governance_blocked event for the same project/provider, this path still removes and re-adds only the flat active message list. Unlike the background path added in this change, it does not mirror through threadStates or write the updated snapshot to IndexedDB until a later thread switch, so reloading right after the refresh can first-paint the old persisted banner (often without the new clientId) and resurrect stale UI. Use the thread-scoped replacement path with persist: true here as well.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in head 62b4e892e. The active governance_blocked receive path now builds the final scoped banner list and calls replaceThreadMessages(threadId, nextMessages, nextHasMore, { persist: true }) in one operation. With a resolvable thread id it no longer does removeMessage(...) followed by addMessage(...), so IndexedDB receives the fresh final snapshot instead of an intermediate/old banner state. Regression coverage added in packages/web/src/hooks/__tests__/useAgentMessages-thread-dispatch.test.ts asserts no remove/add calls and the persisted thread-scoped replacement.
Why: legacy unscoped governance banners cannot prove provider readiness, and active refreshes need an atomic persisted thread snapshot to avoid resurrecting stale retry prompts.
|
Current-head cloud P2 fixes pushed in Fixes:
Red to green evidence:
Both current-head inline P2 threads were also replied with targeted fix notes. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 62b4e892e4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| useEffect(() => { | ||
| if (!onSelfClear || state !== 'idle') return; | ||
| const scopedClientId = clientId?.trim(); | ||
| if (!scopedClientId) return; |
There was a problem hiding this comment.
Probe legacy banners before returning
For users who already have a stale governance_blocked banner in IndexedDB, this guard skips the self-heal entirely because clientId is a new field added by this change and older persisted messages only have projectPath/reasonKind/invocationId. In that upgrade/reload scenario no /api/governance/status request is made and removeThreadMessage is never called, so the exact stale banner this fix is meant to clear remains stuck until another governance event happens. Please migrate/infer the scope or otherwise handle legacy unscoped banners instead of returning here.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in head c73696372. Legacy persisted banners without clientId no longer return without self-heal, but they also do not use the unsafe unscoped/default-provider status probe. The card now probes the four governance provider scopes (anthropic, openai, google, kimi) via scoped /api/governance/status?...&clientId=... requests and clears the legacy banner only when every scope reports ready === true. If any provider scope is still blocked or a probe fails, the banner stays visible. Regression coverage added for both all-ready legacy clear and one-provider-blocked no-clear paths.
Why: legacy governance_blocked messages lack provider scope, so self-heal must avoid default-provider false clears while still removing stale upgraded banners once every governance provider scope is ready.
|
Current-head cloud P2 fix pushed in Fix:
Red→Green:
Validation:
Replied to the current-head inline P2 at #41 (comment). |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c73696372f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }); | ||
| }; | ||
| const nextMessages = [...nextMessagesWithoutStaleBanner, nextBanner]; | ||
| options.store.replaceThreadMessages(msg.threadId, nextMessages, threadState.hasMore, { persist: true }); |
There was a problem hiding this comment.
Stamp background governance refreshes as activity
When a governance_blocked event arrives for an inactive/background thread, this new path rewrites the message list via replaceThreadMessages and then only calls incrementUnread. Unlike the previous removeThreadMessage + addMessageToThread flow, neither of these calls updates threadStates[threadId].lastActivity, while ThreadSidebar uses that live lastActivity to merge into lastActiveAt before sorting. The result is that a newly blocked background thread can get an unread badge but remain ordered by its old activity until the thread list is refetched; stamp lastActivity when this replacement actually adds/replaces the banner.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in head 2fdf09309. Background governance refreshes still replace/persist the final banner snapshot and increment unread, but incrementUnread now also stamps lastActivity, so inactive threads with fresh governance prompts move according to the new activity instead of retaining the stale sidebar sort timestamp. Red→Green coverage added in packages/web/src/hooks/__tests__/useAgentMessages-background.test.ts by seeding an old lastActivity and asserting the refreshed background banner advances it.
Why: Background governance refreshes now use replaceThreadMessages plus incrementUnread, bypassing the old addMessageToThread activity stamp. Updating unread activity at the store boundary keeps sidebar ordering aligned with newly blocked background threads. Validation: pnpm --dir packages/web exec vitest run src/components/__tests__/governance-blocked-card.test.ts src/hooks/__tests__/useAgentMessages-background.test.ts src/hooks/__tests__/useAgentMessages-thread-dispatch.test.ts src/stores/__tests__/chatStore-remove-thread-message.test.ts Validation: pnpm check Validation: pnpm --filter @cat-cafe/web build Validation: git diff --check [砚砚/gpt-5.5🐾] Thread-Context: threadId=thread_mqcj45byxoka2z7u catId=codex
|
Current-head P3 fix pushed in Fix:
Red→Green:
Validation:
Replied to the current-head inline P3 at #41 (comment). @codex review |
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Closes zts212653#1045
Summary
GovernanceBlockedCardnow probesGET /api/governance/healthon mount and asks the parent to remove the underlying transient message when the server reportsstatus: 'healthy'for itsprojectPath.ChatMessagewires the newonSelfClearcallback toremoveThreadMessage(currentThreadId, message.id).Why
governance_blockedis a transient client-only message (added viaaddMessageToThreadinuseAgentMessages.ts, persisted into IDB bysaveMessagesSnapshot, never persisted on the server). AfterPOST /api/governance/confirmsucceeds, the banner stays in store/IDB forever — every thread reload first-paints from IDB and the banner reappears.Symptom users hit: amber "尚未初始化治理" banner for a project that was actually initialized; clicking "重试" returns
Cannot retry invocation with status 'succeeded'because the original invocation completed long ago. Reproduced in threadthread_mqxva63tuebxj9snwhere registry.confirmedByUser=true, CLAUDE.md has the managed block, preflight returnsready: true, and all 37 invocations show 0 GOVERNANCE errors.Test plan
pnpm vitest run src/components/__tests__/governance-blocked-card.test.ts— 12/12 pass (5 new cases covering: healthy → onSelfClear called; not-healthy → not called; missing project → not called; network error → not called; no onSelfClear → no probe)pnpm tsc --noEmitonpackages/web— no new errors inGovernanceBlockedCard.tsx/ChatMessage.tsx(pre-existing errors inhub-cat-editor.test.tsxunrelated)pnpm biome checkon changed files — no new findings (pre-existing complexity/img/non-null warnings inChatMessage.tsxunchanged)checkGovernancePreflightreturns ready for the production case)Follow-up (tracked in zts212653#1045, NOT in this PR)
governance_unblockedevent on confirm success → frontend clears banners across all threads with that projectPathgovernance_blockedto IDB; derive banner from (last invocation errorCode, server health) instead of a stored message🤖 Generated with Claude Code