Files
waggle-os/docs/ux-refactor/p7-d15-scope.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

37 KiB
Raw Blame History

P7 — D15 Launch Integrity · Scope (per-screen state grid + approval/audit taxonomy)

Phase sequence (docs/ux-refactor/deltas/open-questions.md:574): P1=D3 · P2=verify+J08 · P3=D2 · P4=D11/D12 · P5=D4 (2026-06-12, 7 commits 73f2ed5→48292ef) · P7=D15 ← here. This is the LAST launch-blocker.

Decision text: open-questions.md §D15 (line 563) — "Launch-blocking from prior-plan P6: per-screen state grid (brief rule 10) + approval/audit taxonomy consolidation (brief rule 7, includes D4-ii alignment). Post-launch: connector /sync real implementation (stub stands), MCP logs." Plus §D4(ii) (line 539) — "Align the card's risk display with the ui/approval-modal.tsx taxonomy (D15 work)."

Scoping pass only. Everything below is grounded in a read-only audit of apps/web/src (routes in routes/*Route.tsx, components in components/os/apps/) plus the backend taxonomy modules. No code was modified. Every claim cites file:line.

D15 has two structurally independent halves. HALF 1 (brief rule 10) makes every screen declare its loading / empty / error / populated states. HALF 2 (brief rule 7) unifies the risk/approval/audit vocabulary across five backend taxonomies and three frontend approval surfaces — and closes the deferred D4(ii) in-chat-card↔modal alignment.


HALF 1 — Per-Screen State Grid (brief rule 10)

Legend: full = all four states distinct and correct · partial = state exists but is degraded/conflated · missing = no such state (the failure mode renders as a different state, usually empty) · n/a = categorically inapplicable (static screen / no data path).

The dominant defect class is error-rendered-as-empty: a fetch failure is swallowed into [] so the user sees "No X yet" when the backend is actually down. On a trust / monetization / activity surface this is a correctness defect, not cosmetics.

State grid (all 30 screens)

# Screen Route loading empty error populated Worst gap
1 Home Cockpit (S01) /home full full full full Overnight tile hidden when offline (HomeCockpit.tsx:626) instead of degraded placeholder; header pill only signal (149)
2 Workspace Desktop (S02) /workspaces/:id/:tab? full full full full research/timeline/settings tabs are static TabPlaceholder stubs (WorkspaceDesktopApp.tsx:955-961,992-998,1000-1006); best-effort feeds render partial-failure as empty (712-735)
3 Command Center (Win+K) overlay (AppShell.tsx:312) partial full missing full No in-surface error at all; search failure → silent fuzzy fallback (CommandCenter.tsx:253-268); overlay outside any error boundary (312 vs SurfaceBoundary-wrapped Outlet) — a render throw blanks the shell
4 Memory Center /memory/:mindScope? full full partial full Timeline sub-tab error-blind: useMemory captures error (useMemory.ts:36) but MemoryRoute.tsx:103 never threads it → fetch failure shows "No memories found" (TimelineTab.tsx:197-202). Memories+Graph tabs are full.
5 Artifact Center /artifacts full full full full Gold standard — cold-load error (full-pane, ArtifactCenterApp.tsx:242) vs transient refetch error (inline banner, 258-263). No material gap.
6 Files /files partial full missing full All failures collapsed to offline=true (FilesApp.tsx:157-158) → cold load error shows "showing cached files" with NO cache; no loading branch in list pane (469-549) → in-flight looks empty; silent mutation catches (228,284,289)
7 Room /room missing full missing full SSE subscribe failure swallowed to console.error (useRoomState.ts:39-41), hook exposes no error/connecting → connecting, idle, and broken all render "No agents running" (RoomApp.tsx:179-187)
8 Agents — Center /agents full full full full No material gap; load gated on connecting (AgentsApp.tsx:74-77); stale-refresh banner (240-247)
9 Agents — Templates /agents (toggle) partial full partial full No connecting gate (TemplatesView.tsx:76) → 401 race; error only as banner AFTER loading clears (240-249); getPersonas() rejection silently falls back to local PERSONAS (52-56, setError NOT called) — backend failure invisible
10 Agents — Builder (S18) /agents (New/template) n/a full partial full Catalog failure → empty picker + single catalogNote (AgentBuilder.tsx:146-148) that doesn't say WHICH catalog failed
11 Skills (Capabilities Hub) /skills partial full partial full Packs-tab catalog failure leaves error null → "No catalog packs found" empty stands in for error (CapabilitiesApp.tsx:533-538); skills-tab error only when list empty (472) — stale list masks reload failure
12 Automations — Center /automations full full full full Reference implementation — loading + empty + error + stale-banner + partial-degradation notice all distinct (AutomationCenterApp.tsx:262-290)
13 Automations — Builder (S20) /automations (New/Edit) full n/a full full No gap — hydration race handled (configPending/configUnavailable, AutomationBuilder.tsx:138-139); C26 check fallback (223-224); approval fails closed (237-249)
14 Automations — Log list /automations (Logs) full full full full Leaf component fully covers 4 states (AutomationLogList.tsx:24-37); parent prefetches so loading/error props unreached (dead-but-correct)
15 Connector Hub /connectors full full full full handleDisconnect failure is console-only, no toast (ConnectorsApp.tsx:170-175) — inconsistent with connect/revoke; "Recommended" empty copy reused for "registry not loaded" (329)
16 MCP Hub /mcps full full full full remote (MCPHubApp.tsx:359)/activity (385) tabs render un-gated by the items-fetch error → server-down on those tabs shows no banner; catalog silently drops Install buttons when registry unreachable (McpCatalog.tsx:85-86)
17 Marketplace /marketplace full full full full Audit tab delegates to InstallAuditPanel (own states) → can disagree with marketplace health (MarketplaceApp.tsx:335); partial facet rejection silently drops sources (170-172); only all-rejected surfaces error (177)
18 Tool Launcher /launcher full missing full full Successful detection with tools:[] → blank scroll area, no "no tools detected" (LauncherApp.tsx:366-371 loading-gated empty); no connecting gate (130-132) → cold-load 401 race
19 Install Audit Panel embedded (/connectors,/mcps,/marketplace) partial full full full Loading is an unconditional early-return (InstallAuditPanel.tsx:92-98) → every type-filter change/reload blanks the whole feed to a spinner (hosts explicitly avoid this); no connecting gate (90)
20 Team Governance /team n/a n/a partial n/a Static tier-upsell card, no data path (TeamGovernanceApp.tsx:3-44); only error coverage is the render-crash boundary. By design (D5 reserves Team work).
21 Approvals /approvals full full missing full Both fetches .catch(() => ({pending:[],count:0})) (ApprovalsApp.tsx:77-78) → server-down shows "No pending approvals" on a TEAMS trust surface; no error branch anywhere
22 Waggle Dance /waggle-dance full full missing full Hook tracks+returns error (useWaggleDance.ts:58) but WaggleDanceApp.tsx:34 omits it from the destructure → load failure renders "No waggle dance signals yet" (86-91). Dead error state.
23 Settings /settings partial n/a partial full No top-level loading (defaults render immediately); getSettings/getPermissions/getTeamStatus swallow errors (SettingsApp.tsx:156,163,165); getTelemetryStatus has NO .catch (168-171) — unhandled rejection. Billing tab is gold standard (500-543).
24 Vault /settings/vault full full missing full loadData Promise.allSettled wrapped in try{}catch{/*ignore*/} (VaultApp.tsx:104-120) → vault-read failure shows "No secrets stored yet" (300); handleReveal failure silently ignored (182)
25 My Profile /settings/profile full missing missing full getProfile().catch(() => {}) (UserProfileApp.tsx:102) + form always renders from '' defaults (65-83) → never-configured, load-failed, and populated are indistinguishable; no empty, no error
26 Mission Control (Cockpit) /settings/mission-control partial partial full full Error is the reference imploffline banner + Retry on all-rejected (CockpitApp.tsx:82-93). Gap: no body skeleton (header icon only, 77-78) → first paint shows "Unknown"/$0.0000; empty is per-tile not screen-level
27 Timeline /settings/timeline full full missing full getTimeline .catch only clears loading (TimelineApp.tsx:84-86), events stay [] → backend error shows "No activity in this period" (168-172). No-workspace guard (109-115) is well handled.
28 Events /settings/events missing full missing full No loading concept (only per-step spinners); useEvents exposes error (useEvents.ts:35) but EventsRoute.tsx:16-20 discards it → both load-pending and load-failed show "No events yet" (EventsApp.tsx:411-417)
29 Usage & Telemetry /settings/usage full partial missing full Per-fetch .catch(() => []) (TelemetryApp.tsx:29-30) coerces failure into a zeroed populated dashboard (3 cards reading 0/$0.0000); !data empty (56-63) nearly unreachable; no error, no retry

Reference implementations to copy from: Artifact Center (#5, cold vs transient error split), Automations Center (#12, full 5-band grid incl. partial-degradation notice), Mission Control (#26, offline+Retry banner), and Memory Center's Memories tab (MemoryCenterTab.tsx:298-329, role=status/role=alert/Retry/context-empty). The fix pattern already exists in-repo on four screens — P7 propagates it, it does not invent it.

Gaps to close — prioritized (rank = user-facing severity)

Ranking rule (D15-aligned): a missing ERROR state on a core / trust / monetization surface ranks far above a missing EMPTY state on a settings sub-page. Error-rendered-as- empty on a security surface is a correctness defect; a missing zero-tools message is polish. The fix for the dominant class is mechanically identical everywhere (thread the error the hook already captures + render an role=alert block with Retry).

P0 — launch-blocking (missing ERROR on a core/trust/activity surface)

  1. Approvals — error→empty on a trust surface (ApprovalsApp.tsx:77-78). A TEAMS trust-control inbox shows "No pending approvals" when the server is down. Highest user trust cost. Add an error branch + Retry; stop coercing fetch failure to {pending:[]}.
  2. Room — no loading AND no error (useRoomState.ts:39-41, RoomApp.tsx:179-187). The exact SSE-channel failure D3 revived in prior sessions renders identically to "no agents." Add a connecting/error status flag to useRoomState; show connecting skeleton + error+reconnect affordance.
  3. Command Center — no error state + no error boundary (CommandCenter.tsx:253-268, AppShell.tsx:312). The structural half is the real risk: the overlay sits outside the only SurfaceBoundary (AppShell.tsx:300/306) so a render throw blanks the whole shell. Wrap the overlay in an AppErrorBoundary; surface search/execute failures in-body, not just as a toast.
  4. Files — error collapsed into a misleading "offline/cached" state (FilesApp.tsx:157-158,385-389). A cold-load 500/permission failure shows "showing cached files" with no cache. Distinguish a real load error from offline; add a loading branch to the list pane (469-549).
  5. Waggle Dance — dead error state (WaggleDanceApp.tsx:34 omits error the hook already returns at useWaggleDance.ts:58). One-line destructure fix + render. Lowest effort in this tier — the data is already there.
  6. Memory Center / Timeline sub-tab — error-blind (MemoryRoute.tsx:103 doesn't thread useMemory.error; TimelineTab.tsx:43-57 has no error prop). Thread + render. Memory is the lock-in moat surface; a masked failure here is high cost.

P1 — launch-blocking-adjacent (settings/secrets surfaces where error→empty hides failure)

  1. Vault — vault-read failure shows "No secrets stored yet" (VaultApp.tsx:104-120). Secrets surface; an invisible read failure is a trust problem. Add error/offline UI; surface handleReveal failures (182).
  2. Settings — silent error swallows + one unhandled rejection (SettingsApp.tsx:156,163,165 empty catches; 168-171 getTelemetryStatus has NO .catch — a real unhandled promise rejection). Add the missing catch (defect, not polish); add a settings-form loading affordance; propagate load errors. Billing tab (500-543) is the template to match.
  3. Timeline / Events / Usage&Telemetry — error→empty/zeroed (TimelineApp.tsx:84-86; EventsRoute.tsx:16-20 discards useEvents.error; TelemetryApp.tsx:29-30 .catch(() => []) → zeroed dashboard). Settings sub-pages, but the pattern (down backend looks like "you've used nothing") is the same class. Thread error + render; add Events loading.

P2 — nice-to-have (missing/partial states that don't mask a failure as success)

  1. Tool Launcher — missing empty state (LauncherApp.tsx:366-371): zero-tools machine shows a blank pane. Add a "no supported tools detected" empty + a connecting gate (130-132).
  2. My Profile — no empty, no error (UserProfileApp.tsx:102): blank form for never-configured AND load-failed. Add a distinct empty ("set up your profile") + error.
  3. Templates / Skills / MCP Hub / Connectors partial-error masking: connecting gates (TemplatesView.tsx:76), packs-tab error (CapabilitiesApp.tsx:533-538), un-gated MCP remote/activity tabs (MCPHubApp.tsx:359,385), handleDisconnect toast (ConnectorsApp.tsx:170-175). Real but lower-blast-radius.
  4. Loading polish: InstallAuditPanel.tsx:92-98 content-blanking reload; Mission Control / Cockpit body skeletons (CockpitApp.tsx:77-78); Home offline overnight placeholder (HomeCockpit.tsx:626). Flicker, not failure-masking.
  5. Workspace Desktop tab stubs (WorkspaceDesktopApp.tsx:955-961,992-998,1000-1006): research/timeline/settings are TabPlaceholder. These are intentionally deferred (P3/P7 tab embeds, see open-questions.md:621-622) — flag, don't build in P7 unless re-scoped.

HALF 2 — Approval / Audit Taxonomy Consolidation (brief rule 7)

The risk/approval vocabulary is fragmented across five backend taxonomies with no canonical source and three frontend approval surfaces that speak mutually-incompatible dialects. The headline defect (and the named D4(ii) item): the server already computes the full trust taxonomy and emits it on the wire, but the frontend type drops it at the boundary, so the highest-risk approval (installing a third-party/unknown capability) shows the user the least risk information.

The fragmentation (five backend taxonomies, three FE surfaces)

Source Module Risk enum Approval enum Notes
Runtime gating agent/confirmation.ts:124 _riskLevel string low|medium|high (139) ApprovalClass standard|elevated|critical own getApprovalClass() (126-143) hardcodes high→critical/medium→elevated (140-142)
Install assessment agent/trust-model.ts:17 RiskLevel low|medium|high (no critical) ApprovalClass standard|elevated|critical (27) canonical classifyRisk (184) + deriveApprovalClass (196) — tops out at high→critical
Persisted audit core/install-audit.ts:19 AuditRiskLevel …|critical (adds critical) AuditApprovalClass …|blocked (23, adds blocked) DDL CHECK (65-72), no trust_source CHECK; AuditAction 7-val incl. uninstalled
Audit DDL copy hive-mind-core/src/mind/schema.ts:148-149 hand-duplicated CHECK lists (drift hazard, install-audit.ts:62-64 admits it)
RBAC governance server/services/team-capability-governance.ts:23-28 RISK_LEVELS keyed none|low|medium|high (adds none, no critical) PermissionResult allowed|blocked|needs_approval|source_not_allowed riskExceedsThreshold (34): a critical risk falls to ?? 0 → sorts below low
FE in-chat card apps/web/.../lib/types.ts:374-382 none none {requestId,toolName,description,input,rawJson,sourceWorkspaceId}
FE shared modal components/ui/approval-modal.tsx:23-29 riskLevel low|medium|high only none {action,scope[],riskLevel} — name-collides with the card's ApprovalRequest, different shape
FE audit feed extend/InstallAuditPanel.tsx:35-50 free string (String()-coerced) free string the ONLY FE place that reads riskLevel+trustSource+approvalClass — post-hoc

Canonical target vocabulary (what P7 converges on)

Promote a single shared two-axis model to packages/shared/src/risk.ts (or a shared FE type), imported everywhere instead of redeclared. Adopt the widest existing set as canonical so no producer/store mismatch remains:

  • RiskLevel = 'low' | 'medium' | 'high' | 'critical' — adopt the wider AuditRiskLevel (install-audit.ts:19). trust-model.ts must gain a path to 'critical' (today classifyRisk tops out at high, trust-model.ts:184-202).
  • ApprovalClass = 'standard' | 'elevated' | 'critical' | 'blocked' — adopt the wider AuditApprovalClass (install-audit.ts:23). 'blocked' must become a derivable outcome, not a hand-written literal (today only marketplace.ts:231 produces it).
  • TrustSource — canonical at install-audit's 7-value set incl. 'security-gate' (install-audit.ts:20-22); trust-model.ts 6-value set (19-25) is the narrow one.
  • AssessmentMode = 'declared' | 'heuristic' | 'mixed' + RiskFactor[] + PermissionSummary + human explanation — already in trust-model.ts, kept.
  • AuditAction — canonical at the 7-value set incl. 'uninstalled' (added in P5/D4).
  • Two axes, one rule: riskLevel = how dangerous (severity color). approvalClass = how strongly to gate (standard/elevated/critical/blocked). trust-model.ts already separates them cleanly via deriveApprovalClass; the modal collapses to risk-only and must learn approvalClass.

Divergence table (issue | files | severity)

# Issue Files Sev
1 Two ApprovalRequest types share a name, structurally disjoint — card {requestId,toolName,description,input,rawJson,sourceWorkspaceId} vs modal {action,scope[],riskLevel}. Cannot unify by component reuse without reconciling the type first (root of "same taxonomy, not same component"). lib/types.ts:374-382ui/approval-modal.tsx:23-29 HIGH
2 D4(ii) concrete bug: server emits full trustMeta (riskLevel/approvalClass/trustSource/assessmentMode/explanation/permissions) for install_capability, FE card type declares none → risk crosses the wire and is silently dropped. Highest-risk approval shows least info. server/.../chat.ts:937-970ChatApp.tsx:240-289 (+ lib/types.ts:374-382) HIGH
3 Risk scales unaligned — trust-model has two orthogonal axes (riskLevel + approvalClass, critical= never-auto-pass), the shared modal collapses to riskLevel low/med/high with no approvalClass → a critical install and a high install render identically. trust-model.ts:184-202ui/approval-modal.tsx:40-45 HIGH
4 FE modal cannot REPRESENT critical that the audit store + security gate routinely record → a CRITICAL MCP install is demoted to a plain text notice instead of the risk-leveled modal it uses for HIGH. ui/approval-modal.tsx:28install-audit.ts:19, MCPHubApp.tsx:191-195 HIGH
5 Two parallel surfaces render the same approval concept with incompatible vocabularies (modal riskLevel-only vs card no-risk vs audit free-string) — three approval/audit UIs, three field sets. approval-modal.tsx:23 · ChatApp.tsx:222-290 · InstallAuditPanel.tsx:26-45 HIGH
6 'blocked' (AuditApprovalClass) is never DERIVABLE — both derive fns (getApprovalClass, deriveApprovalClass) top out at critical; 'blocked' written only by a route literal (marketplace.ts:231). Derive path and persist path use non-substitutable enums. confirmation.ts:126 + trust-model.ts:196install-audit.ts:23,70, marketplace.ts:231 HIGH
7 RiskLevel (trust-model, 3-val) can NEVER emit critical, but AuditRiskLevel (4-val) accepts it → a risk_level:'critical' row can only be hand-written; producer narrower than store. trust-model.ts:17,318install-audit.ts:19 HIGH
8 Modal consumers manufacture riskLevel client-side from heuristics/hardcodes, NOT from server TrustAssessment — displayed risk ≠ policy-engine risk; same install labelled differently in two places. MarketplaceApp.tsx:55-91 (derives) vs ConnectorsApp.tsx:92/AgentBuilder.tsx:196/AutomationBuilder.tsx:254/MCPHubApp.tsx:216,226 (hardcode) ↔ trust-model.ts:151-202 MEDIUM
9 Trust metadata enriched ONLY for tool==='install_capability' (chat.ts:939); every other gated tool (fs writes, shell, cross-workspace) emits approval with no risk class, though they carry execution risk (PERMISSION_RISK_POINTS codeExecution=2/secrets=2). "Same policy" can't hold without a server risk path for non-install tools. chat.ts:937-970trust-model.ts:160-168 MEDIUM
10 Plain-language action field mismatch — card's description (lib/types.ts:377, rendered ChatApp.tsx:246-248) is NEVER sent by the server (chat.ts:966-970) → card's human line is always empty; modal leads with action. ChatApp.tsx:246-248chat.ts:966-970 MEDIUM
11 team-capability-governance RISK_LEVELS (none|low|medium|high) is a 4th vocabulary using none (no other has it) and lacking criticalriskExceedsThreshold sorts a critical risk via ?? 0 below low. Silent RBAC mis-ordering. team-capability-governance.ts:23-39 MEDIUM
12 Naming-axis confusion — same 3 buckets called riskLevel on some surfaces, approvalClass on others; they are NOT the same axis. Two-axis model exists in code but is collapsed inconsistently per surface. trust-model.ts:196-202 vs approval-modal.tsx/audit/chat trustMeta MEDIUM
13 _riskLevel passed as an untyped string in tool args; confirmation.ts:140-142 hardcodes high→critical/medium→elevated, duplicating deriveApprovalClass (trust-model.ts:196-202) in a second drift-prone place. confirmation.ts:139-142trust-model.ts:196-202 MEDIUM
14 install_audit CHECK-constraint lists hand-duplicated; in-code comment admits "MUST stay in sync … drift silently crashes auditStore.record()." Two copies = no single source of truth. install-audit.ts:62-72hive-mind-core/src/mind/schema.ts:148-149 MEDIUM
15 AuditTrustSource has 7 values incl. security-gate; trust-model TrustSource has 6, resolveTrustSource can never return it; trust_source CHECK is ABSENT from the DDL (install-audit.ts:71) → unconstrained at DB while TS claims a closed set. install-audit.ts:20-22,71trust-model.ts:19-25,67 LOW
16 Audit feed prints risk: <raw> from a String()-coerced unknown (InstallAuditPanel.tsx:143,35-50) with no label/color, while the modal applies RISK_LABELS/RISK_CLASSES → same risk, two visual languages; audit can't be wrong-typed (accepts any string). InstallAuditPanel.tsx:35-50,143approval-modal.tsx:40-45,70-73 LOW
17 trustSource is a taxonomized backend field surfaced on trustMeta (chat.ts:954) but shown in NO approval surface except a hand-built MarketplaceApp scope line (MarketplaceApp.tsx:82-88); 4 of 5 install surfaces omit the dimension that JUSTIFIES the risk. install-audit.ts:20-22 + chat.ts:954 ↔ all builder consumers LOW

D4(ii) — the specific card↔modal alignment work

D4(ii) ratified: the in-chat SSE card is canonical; "same modal" = same policy and risk taxonomy, not same component. The card (ChatApp.tsx:222-290, fed by useChat.ts pendingApproval + lib/types.ts:374-382) and ui/approval-modal.tsx must read the same risk vocabulary. Concretely:

  1. Widen the FE card type (lib/types.ts:374-382) to carry the trust fields the server already emits — riskLevel, approvalClass, trustSource, assessmentMode, explanation, permissions. No parsing change needed — the fields are already on the wire for install_capability (chat.ts:951-958); useChat.ts:252 casts straight to ApprovalRequest, so once the type is widened TS stops hiding them.
  2. Render them in ApprovalGate (ChatApp.tsx:240-289) — at minimum a risk badge + trust-source line, using the same RISK_LABELS/RISK_CLASSES coloring as the modal (approval-modal.tsx:40-45,70-73) so identical risk renders identically in both surfaces.
  3. Modal learns approvalClass (critical especially) so a critical install no longer renders identically to a high one, and so the modal can represent the critical/blocked level the audit trail records (today it cannot — divergence #4).
  4. Critical-tier UX contract, decided once: deriveApprovalClass marks high→critical (full trust context + warning, never auto-pass). The card today offers "Always allow" (ChatApp.tsx:267-274) with NO risk gating — a critical/high install can be permanently granted in one click, which MCPHub's CRITICAL-non-overridable rule (MCPHubApp.tsx:191-217) explicitly forbids. Gate "Always allow" by approvalClass so both surfaces honor the same policy. → RATIFIED (founder, 2026-06-12): gate it. A6 must hide/disable "Always allow" on critical (and high) approvals — no permanent grant for the riskiest actions.
  5. Reconcile the plain-language field — card description (never sent) vs modal action; emit one agreed "what will happen" string server-side (or a shared describeToolUse formatter) feeding both.
  6. (broader, see P7 order) Extend server risk classification beyond install_capability (chat.ts:939 is the only enrichment branch) so the card has a risk class to show for fs/shell/cross-workspace approvals — otherwise the two surfaces can only ever agree on install events.

PRIORITIZED P7 BUILD ORDER

TDD-friendly increments, commit-per-step, matching prior phase-plan cadence (complete → verify → next). Each step states its verify gate. build:packages first (shared → hive-mind-core → core → agent → server) on any cross-package change — the W4 stale-dist/ class (open-questions.md:561, D13). Run tsc --noEmit on touched packages; the server is tsx-transpiled and NOT typechecked by npm run build (CLAUDE.md §2).

Sequencing RATIFIED (founder, 2026-06-12): Track B (P0 error states) FIRST, then Track A. A6 "Always allow" gate RATIFIED: gate by approvalClass (no permanent grant on critical/high).

STATUS — Track B DONE, Track A DONE (2026-06-12)

Track B (P0 error states) — SHIPPED + reviewed + pushed. B1B5 + the 2 confirmed review fixes (548fbe89f3cf2a); adversarial review 2 confirmed/9 refuted (p7-track-b-review-record.md). FE 897/897.

Track A (approval/audit taxonomy + D4(ii)) — SHIPPED + pushed (7bbe5d87a8c374):

  • A1 7bbe5d8 — canonical @waggle/shared/risk.ts (widest-set enums + riskRank/sqlInList).
  • A2a 7675802 — trust-model + confirmation re-pointed; classifyRisk +critical; deriveApprovalClass critical→critical + blocked-flag.
  • A2b d0b9aaa — install-audit + team-governance; fixed critical-sorts-below-low RBAC defect.
  • A3 a412a8e — install_audit CHECK single-sourced + OSS schema.ts parity test (no migration).
  • A4 239cc51 — server emits risk metadata for ALL gated tools (was install-only).
  • A5 dff44b8D4(ii): card type widened + RiskBadge render via shared risk-display.
  • A6 3d9893f — modal represents critical; "Always allow" gated by approvalClass.
  • A7 7a8c374 — shared classifyInstallRisk + audit-panel RISK_LABELS reuse.

Post-launch divergences — ALL CLOSED (2026-06-12, founder "close them too"):

  • #15 (6fc8500) — install_audit.trust_source DDL CHECK added (generated + OSS-mirror parity-locked) with a rebuild migration; the column is no longer unconstrained-at-DB while typed-closed in TS.
  • #8 (c85c295) — actionRisk(kind) in risk-display is now the single source for the non-install approval surfaces' risk; AgentBuilder/AutomationBuilder/ ConnectorsApp/MCPHub/MarketplaceApp pull from it (0 scattered literals). The deeper "displayed ≠ policy-engine risk" for these non-install actions stays an honest action-kind default (no server TrustAssessment feed exists for non-install actions).
  • #17 (3040a6a) — ApprovalModal renders a structured trustSource line via shared TRUST_SOURCE_LABELS; MarketplaceApp populates it (was a hand-built scope string). Omitted when absent (no false provenance).

D15 closure bar = Track A (A1A7) + Track B P0 (B1B5): MET. All 17 taxonomy divergences now closed (HIGH #17 in A1A6; MED/LOW #8/#14/#15/#16/#17 closed).

LAUNCH-BLOCKING (must ship for P7 to close D15)

Track A — Approval/audit taxonomy + D4(ii) (HALF 2). Order matters: the shared type is the foundation everything else imports.

  • A1 — Shared risk module. Create packages/shared/src/risk.ts exporting canonical RiskLevel(+critical), ApprovalClass(+blocked), TrustSource(7-val), AssessmentMode, AuditAction(7-val). Verify: unit tests assert each enum equals the widest existing set; tsc shared green. Commit: feat(shared): canonical risk/approval taxonomy (D15).
  • A2 — Backend imports the shared enums. Re-point trust-model.ts, confirmation.ts, install-audit.ts, team-capability-governance.ts at @waggle/shared risk types; give trust-model.classifyRisk a critical path; make deriveApprovalClass emit blocked; collapse the duplicate getApprovalClass mapping (confirmation.ts:140-142) onto the one mapper. Fix RISK_LEVELS so critical sorts above low (div #11). Verify: existing trust-model/confirmation tests stay green + new cases for critical/blocked; tsc agent+core+server. Commit: refactor(agent,core,server): single risk taxonomy source (D15).
  • A3 — Single audit CHECK source. Generate/import the install_audit CHECK lists from the canonical enums so install-audit.ts and hive-mind-core/src/mind/schema.ts cannot drift; add the missing trust_source CHECK (div #14,#15). Verify: schema-parity test asserts both CHECK strings derive from one constant; auditStore.record() round-trips a critical/blocked row. §7.5 OSS drift-check (scripts/oss-drift-check.sh) since schema.ts is mirrored. Commit: fix(core,hive-mind-core): single-source install_audit CHECK constraints (D15).
  • A4 — Server risk path for ALL gated tools. Extend the chat.ts:939 enrichment branch so every gated tool (not just install_capability) carries riskLevel/approvalClass/trustSource on approval_required, derived from PERMISSION_RISK_POINTS (div #9). Emit a plain-language action string (div #10). Verify: chat-route tests assert trustMeta present for a non-install gated tool; server tsc. Commit: feat(server): risk classification for all gated approvals (D15).
  • A5 — D4(ii): widen FE card type + render. Widen lib/types.ts:374-382 ApprovalRequest with the trust fields; render risk badge + trust-source + explanation in ApprovalGate (ChatApp.tsx:240-289) using shared RISK_LABELS/RISK_CLASSES. Verify: FE unit test — an SSE payload with trustMeta renders a risk badge; tsc -p apps/web/tsconfig.app.json. Commit: feat(web): in-chat approval card renders server risk taxonomy — D4(ii) (D15).
  • A6 — Modal learns approvalClass + critical; gate "Always allow". Add approvalClass to ui/approval-modal.tsx + a critical RISK_LABELS/RISK_CLASSES entry; gate the card's "Always allow" (ChatApp.tsx:267-274) by approvalClass (no permanent grant on critical). Verify: FE tests — critical renders distinctly from high; "Always allow" hidden/disabled on critical. Commit: feat(web): approvalClass + critical tier in shared modal; gate Always-allow (D15).
  • A7 — Authoritative consumer risk. Replace per-call-site riskLevel literals (ConnectorsApp.tsx:92, AgentBuilder.tsx:196, AutomationBuilder.tsx:254, MCPHubApp.tsx:216,226) with one shared classifier (generalize installRiskFor, MarketplaceApp.tsx:57-62) keyed on action-kind + trust/scan signal; surface trustSource in modal scope on all install surfaces (div #8,#17). Validate InstallAuditPanel normalizer against the enums + reuse RISK_LABELS (div #16). Verify: FE tests — same conceptual action yields same risk across surfaces. Commit: refactor(web): authoritative shared risk classifier across approval surfaces (D15).

Track B — State grid P0 errors (HALF 1). Independent of Track A; parallelizable. Each is the same mechanical fix (thread the error the hook already captures + role=alert + Retry).

  • B1 — Approvals error state. ApprovalsApp.tsx:77-78 — stop coercing to empty; add error branch + Retry. Verify: FE test — failed fetch shows error, not "No pending approvals".
  • B2 — Room loading+error. Add connecting/error to useRoomState (useRoomState.ts:39-41); render connecting + error+reconnect in RoomApp (179-187). Verify: test — broken SSE ≠ empty.
  • B3 — Command Center error boundary + in-body error. Wrap the overlay (AppShell.tsx:312) in AppErrorBoundary; surface search/execute failures in-body. Verify: test — a thrown ResultRow doesn't blank the shell.
  • B4 — Files real error vs offline. Split cold-load error from offline (FilesApp.tsx:157-158); add list-pane loading branch (469-549). Verify: test — 500 shows error, not "cached files."
  • B5 — Waggle Dance + Memory/Timeline + Events + Settings catch. One-line error re-wires (WaggleDanceApp.tsx:34; MemoryRoute.tsx:103+TimelineTab prop; EventsRoute.tsx:16-20) + the SettingsApp.tsx:168-171 missing .catch (a real unhandled-rejection defect — do not skip). Verify: per-screen test — failure ≠ empty.

NICE-TO-HAVE (P2 — do if time; not gating D15 closure)

  • B6 — Vault + Settings + Timeline/Usage error states (P1 list items 7-9): trust/secrets matter but are lower-traffic; ship if A+B0 land with margin.
  • B7 — Tool Launcher empty, My Profile empty/error, Templates/Skills/MCP/Connectors partial fixes, loading-flicker polish (P2 list items 10-13).
  • B8 — connecting gates on TemplatesView.tsx:76, LauncherApp.tsx:130-132, InstallAuditPanel.tsx:90 (cold-load 401 race; sibling apps already guard).

D15 closure bar: Track A (A1A7) and Track B P0 (B1B5) are the launch-blocking minimum. B6B8 are nice-to-have and may slip to a fast-follow without re-opening D15, provided no P0 error-as-empty remains on a core/trust surface.


OUT OF SCOPE / POST-LAUNCH LEDGER

Item Why deferred Anchor
Connector /sync real implementation D15 explicitly post-launch; stub stands open-questions.md:563
MCP Hub "Logs" action D15 explicitly post-launch; aria-disabled coming-soon today open-questions.md:563, InstalledMcpList.tsx:135-144
Workspace Desktop research/timeline/settings tab bodies Intentional TabPlaceholder stubs; tab embeds are P3/P7-tab-embed scope, deferred WorkspaceDesktopApp.tsx:955-1006, open-questions.md:621-622
Team Governance real /team surface D5 reserves Team work; static upsell card by design (RBAC Phase 5 founder-DEFERRED) TeamGovernanceApp.tsx:3-44, open-questions.md:547, project_rbac_phase5_deferred.md
Marketplace partial-facet silent drop Silent-by-design; only all-rejected surfaces error MarketplaceApp.tsx:170-179
Workspace-scoped members (vs global roster) Flagged Phase-5 gap in-code WorkspaceDesktopApp.tsx:722-724
Builder catalog "which catalog failed" granularity Partial-error nicety, not a masked-failure defect AgentBuilder.tsx:146-148
errorKind auto-recover for notfound/permission mid-session Intentionally non-revalidating; correct WorkspaceDesktopApp.tsx:662
ChatWindowInstance FALLBACK_MODELS / status-bar chips / activeModels P5→P7 ledgered residuals open-questions.md:594,621-622
memory-mcp vs hive-mind-mcp-server canonical-package decision Out of launch scope; mark dormant open-questions.md:568
teams-server boot error (Fastify instance is already listening) Solo boot path unaffected; classify post-launch open-questions.md:567

Scope authored 2026-06-12. Read-only pass — no code modified, no tests/tsc run. Ground all implementation against re-read source per CLAUDE.md §3.5 (context decay).