385 lines
37 KiB
Markdown
385 lines
37 KiB
Markdown
# 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 impl** — `offline` 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)
|
||
|
||
7. **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).
|
||
8. **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.
|
||
9. **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)
|
||
|
||
10. **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).
|
||
11. **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.
|
||
12. **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.
|
||
13. **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.
|
||
14. **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-382` ↔ `ui/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-970` ↔ `ChatApp.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-202` ↔ `ui/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:28` ↔ `install-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:196` ↔ `install-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,318` ↔ `install-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-970` ↔ `trust-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-248` ↔ `chat.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 `critical` → `riskExceedsThreshold` 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-142` ↔ `trust-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-72` ↔ `hive-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,71` ↔ `trust-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,143` ↔ `approval-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.** B1–B5 + the 2 confirmed
|
||
review fixes (`548fbe8`→`9f3cf2a`); adversarial review 2 confirmed/9 refuted
|
||
(`p7-track-b-review-record.md`). FE 897/897.
|
||
|
||
**Track A (approval/audit taxonomy + D4(ii)) — SHIPPED + pushed (`7bbe5d8`→`7a8c374`):**
|
||
- 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 `dff44b8` — **D4(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 (A1–A7) + Track B P0 (B1–B5): MET. All 17 taxonomy
|
||
divergences now closed (HIGH #1–7 in A1–A6; 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 (A1–A7) **and** Track B P0 (B1–B5) are the launch-blocking
|
||
> minimum. B6–B8 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).*
|