Skip to content

feat: Inbox-Intelligence epic — digest read layer, trust gates, UI (#484)#488

Merged
jayzalowitz merged 39 commits into
mainfrom
feat/inbox-intelligence-epic
Jun 10, 2026
Merged

feat: Inbox-Intelligence epic — digest read layer, trust gates, UI (#484)#488
jayzalowitz merged 39 commits into
mainfrom
feat/inbox-intelligence-epic

Conversation

@jayzalowitz

Copy link
Copy Markdown
Owner

Implements the Inbox-Intelligence epic (#484) — 13 specs building the digest read
layer, source-agnostic extractors, the cross-cutting trust layer, UI, and launch
fixtures. Every commit builds + tests green; full suite green (70/70 turbo tasks).

What's implemented (tested logic per commit)

Integration seams left (documented in commits)

  • briefing-generator decision-outcome query → actionRequired (spec 01 step 0)
  • structured_payload column + generator write (spec 01/08 data population)
  • MemoryPort.getSignalsForEntity port method + adapters (spec 05 §5)
  • extractor degraded-locale routing on 02/03/06 (spec 12)
  • mobile BriefingScreen mirror (spec 08)

Tests

New: SignalText, capability-matrix, deadline, commitment, security-alert,
clustering, coverage, scope-gate, visibility, locale, digest, entity-linking,
seedUpsert, demo-guard, soak-floor. Full monorepo suite green.

Closes #480 #474 #476 #475 #479 #477 #478 #487 #485 #486 #483 #482 #481

🤖 Generated with Claude Code

jayzalowitz and others added 14 commits June 6, 2026 18:00
…matrix (spec 07, #480)

Normalize any RawSignal (email/calendar/filesystem/voice) into a channel-agnostic
SignalText so commitment/deadline/security/cluster/entity capabilities are
source-agnostic. Extends AuthoringTier with authored_originated/received_shared;
adds a tested capability×source coverage matrix. Foundation for #475/#476/#479.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…spec 10, #483)

- LOCKED: new users default to trust_tier 'observer' (users.ts) — matches DB
  default + CLAUDE.md; resolves the 3-way conflict that forced 'suggest'.
- provisionNewUser: eager empty twin profile + conservative autonomy defaults
  (no spend caps, so the built-in NO_SPEND_WITHOUT_LIMIT gate blocks spend
  until the user sets a budget — safe by construction).
- seedUpsert/buildUpsertSql: shared, tested idempotent upsert helper for
  re-runnable seeds (used by spec 09). Existing seed.ts already idempotent.

Part C (promotion soak-floor hoursInCurrentTier + tier-ladder intro) still TODO.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(spec 10 Part C, #483)

Daily promotion-eligibility job now populates hoursInCurrentTier (from last tier
change or account creation), so the engine actually enforces minDurationInTierHours
(24h observer->suggest, etc). Closes the documented gap where the floor was skipped
in the auto path. Fail-safe 0 keeps a promotion blocked when time can't be derived.
Tier-ladder intro UI folds into spec 08.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
extractDeadline parses absolute/relative dates (chrono-node) from any
text-bearing signal (SignalText-compatible) and returns the earliest credible
FUTURE deadline. situation-interpreter.enrichDeadline stamps rawEvent.deadline
when the connector didn't, so the existing assessUrgency consumer finally gets
fed. Rejects past dates + no-match. v1 leaves per-user-timezone resolution to
spec 12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pec 02, #475)

extractCommitments surfaces the user's own stated obligations ("I'll send the
draft tomorrow" -> "Send the draft tomorrow") from authored SignalText. Gated to
authoredByUser + the commitments source allowlist (safety invariant #8: never
from inbound content). Rule extractor handles modal forms, excludes
questions/past/third-party/hypotheticals, dedups, and emits a deadlineHint for
spec 03. CommitmentStrategy seam left for an LLM path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ly (spec 06, #479)

Adds SituationType.SECURITY_ALERT (enums.ts). classifySituation matches inbound
account-security markers FIRST (precedence over finance/email), urgency=high,
domain=security. The candidate generator emits ONLY a human-review escalation
that says "open the provider directly" with link-free parameters — never an
auto-executable action, never a URL from the untrusted body (safety invariant
#8). Provenance stays untrusted_external regardless of claimed sender.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#477)

clusterSignals groups awareness signals into life-domain topic clusters for the
Topics section. Anchors to known domains (beats the reference product's
mis-filing), guarantees complete + non-overlapping partition, caps cluster count
with overflow merged into "More updates" (logged via onMerge). Deterministic
fallback ships; ClusterStrategy seam for an LLM path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (spec 13, #487)

computeCoverage evaluates the capability x source matrix against a user's
connected accounts -> per-capability available/partial/unavailable + the sources
that would unlock each, plus a coldStart flag (zero sources, distinct from
connected-but-quiet). Excludes mock sources. Drives "connect X to unlock Y"
transparency; UI affordances render in spec 08.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(spec 11, #485)

Scope gate (policy-engine): requiredWriteScope/hasWriteScope/applyScopeGate.
Wired into DecisionMaker.generateCandidates — when grantedScopes is supplied,
un-granted write candidates (send/calendar) downgrade to a human-review "grant
access" item. Fail-safe NOT granted (safety invariant #8). Visibility filter
(decision-engine): isHidden/filterVisible — the single hide predicate the digest
routes input through (briefing-generator wiring lands with spec 01).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… 12, #486)

Migration 063 adds users.language + users.timezone. userRepository.getLocale +
resolveLanguage/resolveTimezone/isNonEnglish helpers (safe fallbacks: en / UTC
with a logged-default flag). Briefing prose locale now reads the user profile
instead of hardcoded 'en'. isNonEnglish is the LLM-vs-rule routing signal for
the extractors (degraded-marker wiring is a follow-up on 02/03/06).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
)

assertDemoSafe (3-gate invariant #0): explicit-only, prod hard-blocked + non-local
needs override, identity isolation via is_demo (migration 064). Never wired into
bin/skytwin-dev/auto-seed — can't run for a real or new user. demo-fixture.ts
guards then upserts the reserved demo user + ingests a synthetic source-varied
corpus (email/calendar/file/voice) through /api/events/ingest; --reset deletes
is_demo rows only. `pnpm demo:fixture`. Guard fully unit-tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#474)

buildDigest partitions items into action-required to-dos (urgency-ordered, capped)
vs domain-clustered topics, with no overlap. Composes the epic: filters hidden
content first (spec 11), clusters topics (spec 04), carries sourceType+deadline
for the UI (spec 07/03). New briefing-prose v2 prompt emits the two-section
structured payload (todos + topics). The structured_payload column + repo read +
render land with spec 08 (UI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pec 05, #478)

extractEntities pulls people (emails) + orgs (suffix-tagged) from SignalText.
resolveEntities links mentions to stable entityIds — exact email key for people
(never fuzzy), token-overlap floor for orgs, conservative mint-on-doubt so a
false merge can't corrupt the graph. linkEntitiesAcrossSignals aggregates "every
signal touching X". Persistence reuses MemoryPort.recordEntity; the
getSignalsForEntity port method is the remaining integration seam.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…481)

twin-briefing.js renders the structured digest: To-dos above Topics, each row
with a source-type chip (email/calendar/file/voice) + citation chips that open
the in-app signal detail (never an external URL — safety #8). Reuses the existing
singleton-delegator + hash-gate + data-action conventions (new open-signal
action). Falls back to prose when structured is null (back-compat). API /latest
passes through structured (nullable, forward-compatible). CSS reuses card/badge
tokens. Mobile BriefingScreen mirror is the remaining part of this spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 6, 2026 22:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements major pieces of the “Inbox-Intelligence” epic across the decision engine, policy/trust gates, DB/bootstrap/demo tooling, and web digest UI, aiming to produce a source-agnostic, access-faithful digest with safety constraints (hide/scope gates) and launch fixtures.

Changes:

  • Added source-agnostic signal normalization (SignalText), capability×source coverage modeling, and new extractors/assemblers (deadlines, commitments, clustering, entity linking, digest builder).
  • Introduced trust/safety gates: inbound SECURITY_ALERT situation handling, write-scope gating utilities, and hidden-content filtering utilities.
  • Added locale/timezone plumbing, demo-fixture guard + fixture seed tooling, and a structured digest render in the web briefing UI.

Reviewed changes

Copilot reviewed 51 out of 52 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
pnpm-lock.yaml Locks new workspace + dependency additions (connectors + chrono-node).
packages/shared-types/src/enums.ts Adds SituationType.SECURITY_ALERT.
packages/policy-prompts/prompts/briefing-prose/v2.md New v2 briefing prompt schema supporting structured todos/topics.
packages/policy-engine/src/scope-gate.ts Implements write-scope requirement mapping + candidate downgrading.
packages/policy-engine/src/index.ts Re-exports scope-gate utilities.
packages/policy-engine/src/tests/scope-gate.test.ts Unit tests for scope gating behavior.
packages/decision-engine/src/visibility-filter.ts Adds shared hidden-content predicate + filter helper.
packages/decision-engine/src/topic-clusterer.ts Adds deterministic domain-based topic clustering with overflow merge.
packages/decision-engine/src/source-coverage.ts Computes capability availability vs connected sources (cold-start + missing sources).
packages/decision-engine/src/situation-interpreter.ts Adds security-alert classification, deadline enrichment, and summary text for security alerts.
packages/decision-engine/src/signal-text.ts Introduces toSignalText normalization across sources + authored-by-user derivation.
packages/decision-engine/src/locale.ts Adds language/timezone resolution helpers + non-English detection.
packages/decision-engine/src/index.ts Exports new digest/intelligence modules from decision-engine.
packages/decision-engine/src/entity-linking.ts Adds entity extraction/resolution utilities for cross-signal linking.
packages/decision-engine/src/digest.ts Adds structured digest builder (todos vs topics) with visibility filtering.
packages/decision-engine/src/decision-maker.ts Adds scope-gate hook and escalate-only candidates for security alerts.
packages/decision-engine/src/deadline-extractor.ts Adds chrono-node powered deadline extraction.
packages/decision-engine/src/commitment-extractor.ts Adds authored-only, matrix-gated commitment extraction (rule-based).
packages/decision-engine/src/capability-source-matrix.ts Adds tested allowlist for capability×source coverage.
packages/decision-engine/src/tests/visibility-filter.test.ts Tests for hidden predicate + filtering behavior.
packages/decision-engine/src/tests/topic-clusterer.test.ts Tests clustering invariants and overflow merge.
packages/decision-engine/src/tests/source-coverage.test.ts Tests coverage computation across connection configs.
packages/decision-engine/src/tests/signal-text.test.ts Tests per-source mapping + fail-safe behavior.
packages/decision-engine/src/tests/security-alert.test.ts Tests security-alert classification + escalate-only candidates.
packages/decision-engine/src/tests/locale.test.ts Tests language/timezone resolution + non-English detection.
packages/decision-engine/src/tests/entity-linking.test.ts Tests conservative entity extraction/resolution behavior.
packages/decision-engine/src/tests/digest.test.ts Tests digest partitioning, urgency ordering, and visibility filtering.
packages/decision-engine/src/tests/deadline-extractor.test.ts Tests deadline parsing behaviors and edge cases.
packages/decision-engine/src/tests/commitment-extractor.test.ts Tests authored-only commitment extraction + negative cases.
packages/decision-engine/src/tests/capability-source-matrix.test.ts Tests allowlist behavior and mock/real parity.
packages/decision-engine/package.json Adds @skytwin/connectors and chrono-node deps.
packages/db/src/seeds/upsert.ts Adds shared idempotent upsert SQL builder + executor for seeds.
packages/db/src/seeds/demo-guard.ts Adds 3-gate demo fixture guard + identity isolation helper.
packages/db/src/seeds/demo-fixtures/signals.ts Adds synthetic multi-source demo signal corpus.
packages/db/src/seeds/demo-fixture.ts Adds opt-in demo fixture command with guard + ingestion loop.
packages/db/src/repositories/user-repository.ts Adds getLocale() for language/timezone reads.
packages/db/src/repositories/trust-tier-audit-repository.ts Adds hoursInCurrentTier() for soak-floor enforcement.
packages/db/src/migrations/064-users-is-demo.sql Adds is_demo column for demo identity isolation.
packages/db/src/migrations/063-user-locale-timezone.sql Adds nullable language and timezone columns.
packages/db/src/index.ts Exposes new seed/upsert and demo-guard utilities.
packages/db/src/tests/upsert.test.ts Tests for upsert SQL builder/executor behavior.
packages/db/src/tests/demo-guard.test.ts Tests demo guard gates + identity isolation.
packages/db/package.json Adds demo:fixture script entry.
packages/connectors/src/authoring-tier.ts Extends AuthoringTier with authored_originated and received_shared.
package.json Adds root demo:fixture script.
apps/worker/src/jobs/promotion-eligibility-check.ts Wires hoursInCurrentTier into trust-tier engine evaluation.
apps/worker/src/tests/promotion-eligibility-check.test.ts Tests soak-floor wiring into promotion evaluation.
apps/worker/src/jobs/briefing-generator.ts Wires briefing language from user profile (no longer hardcoded en).
apps/web/public/js/pages/twin-briefing.js Adds structured digest rendering + citation chips + source chips.
apps/web/public/css/styles.css Adds styles for digest sections, source chips, and citation chips.
apps/api/src/routes/users.ts Sets new-user trust tier to observer and provisions profile/settings.
apps/api/src/routes/twin-briefings.ts Exposes structured_payload as briefing.structured (nullable/additive).
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/decision-engine/src/decision-maker.ts Outdated
Comment thread packages/decision-engine/src/situation-interpreter.ts
Comment thread apps/api/src/routes/users.ts
Comment thread apps/web/public/js/pages/twin-briefing.js
Comment thread packages/decision-engine/src/entity-linking.ts
Comment thread packages/decision-engine/src/entity-linking.ts
jayzalowitz and others added 13 commits June 6, 2026 19:13
Correctness:
- deadline urgency: stale (past-relative-to-now) deadlines no longer read as
  critical; far-out deadlines no longer DOWNGRADE a type's default urgency (#1/#2)
- security markers curated to specific phrases — kill false positives on shipping
  notices / "welcome back" / articles (#3); marker check also applied on the LLM
  path so escalate-only holds regardless of classifier (safety defense-in-depth)
- digest emits signalRefs[] so citation chips actually render (#4)
- scope gate now covers calendar RSVP/invite write actions (#5)
- commitment extractor: clause-level negation (keep real commitments sharing a
  sentence with "if I…") (#6); "by <person>" no longer a deadline hint (#7)
- entity resolver compares full normalized string, not the truncated slug (#10)

Hardening/robustness:
- demo-guard isLocalDbTarget: exact host match, not substring (#8)
- provisionNewUser is genuinely best-effort (try/catch) — never 500s after the
  user row exists
- briefing-generator pinned to prompt v1 until it consumes v2 structured output
  (avoids requesting+discarding todos/topics); v2 deterministic_fallback fixed
- briefing test mock provides userRepository.getLocale so the LLM-prose path is
  actually exercised (#13)

Regression tests added for each. Full suite green (70/70 tasks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…design-review)

Showing the structured two-bucket digest AND the full prose was the same briefing
twice. When structured is present, the prose moves under a "Full briefing"
<details> as the long-form view; falls back to inline prose when there's no
structured payload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One digest, two depths. Default stays the clean view (non-technical users
unaffected); a discoverable header "Power view" toggle (persisted) + per-item
"Details" expander reveal the depth SkyTwin already computes — provenance,
confidence %, urgency reason, why-it-didn't-auto-run (scope/tier/policy), real
source refs, and the explanation — plus a coverage panel ("what I can see,
connect X to unlock Y"). Not buried in settings.

buildDigestItemDetail is the pure view-model (raw codes -> human strings), unit
tested. UI follows the singleton-delegator/hash-gate/data-action conventions.
Digest payload carries optional per-item detail + coverage (generator populates).
Verified rendering via a headless-browser screenshot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(DESIGN.md)

Source of truth grounded in a full element-and-state inventory of the digest
surfaces. Cool-neutral base (refines existing #0f1117 tokens; rejected the
warm/brown direction), iris #7C72E8 as the SINGLE accent meaning "needs you /
act", Fraunces voice + Geist + Geist Mono, action-vs-awareness hierarchy.
Catalogs every element + EVERY state including the gaps never rendered before:
cold-start, scope-blocked grant-access, loading, error, prose-fallback, distinct
security treatment, provenance in default view. CLAUDE.md now points UI + /qa +
/design-review at it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ates (spec 15)

Wires the locked design system into the real digest UI:
- Load Fraunces (twin voice) + Geist + Geist Mono (index.html)
- Iris #7C72E8 as the single accent = "needs you / act"; killed the CAPS
  source-chip soup -> one neutral source mark + a single "·N sources" citation;
  provenance as a dot (neutral, never accent)
- Action zone (to-dos: checkbox + inline Draft/Snooze/Verify/Grant, hover-reveal,
  always-on for security + touch) vs awareness zone (topics: lighter, no edge)
- Twin voice (Fraunces) + value line ("✓ N handled · M need you · K to catch up")
- Power view detail panel + coverage panel restyled to the system
- GAP STATES now designed: loading skeleton, empty-quiet, cold-start ("connect a
  source"), prose-fallback disclosure, distinct security treatment, scope-blocked
  "Grant access". Verified via headless-browser render of the real CSS.

Row-action wiring (draft/snooze/verify) routes/acknowledges until the act layer
lands. App-wide token adoption (vs digest-scoped iris) is a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rror (pre-existing)

app.js:856 registered a non-async DOMContentLoaded handler, but the pairToken
branch (line ~904) uses `await fetch(...)` → "Unexpected reserved word" at parse
time, which aborts ALL app initialization. Every page rendered as an empty
#page-content shell. Present on origin/main; web JS has no type-check or tests, so
it shipped silently. One-word fix (() => → async () =>); verified by booting the
seeded app and touring dashboard/decisions/approvals/settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The digest existed as tested modules but never rendered in the running app:
the briefing generator produces no structured payload, so /latest returned
null and the UI fell back to "No briefing content yet". This closes that
seam so the AI-inbox parity (to-dos vs topics, multi-source) actually shows.

- live-digest.ts: compute the structured digest from a user's recent
  decisions — read each decision's RawSignal through toSignalText (spec 07)
  for real, source-agnostic titles, partition via buildDigest (spec 01/04),
  attach power-view detail (spec 14) and coverage (spec 13).
- twin-briefings /latest: when no structured_payload is stored, compute the
  digest live (best-effort; degrades to prose on error) and synthesize a
  briefing envelope so the page renders parity today. Forward-compatible:
  a stored payload still wins once the worker writes one.
- dashboard: Home leads with a read-only digest hero (action zone first,
  DESIGN.md) linking to the full interactive /briefing; stop showing the
  "connect Google" nag once the twin has produced decisions.
- index.html: first-class "Briefing" nav link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The decisions log mapped auto_executed=false to "You OK'd", which mislabels a
decision still awaiting approval (notably an escalated security alert) as
already approved. Surface the outcome's requires_approval through the API and
add a distinct "Needs you" state so the log matches the Approvals page.

- decision-repository.getOutcomesForDecisions: also select requires_approval.
- decisions route: return requiresApproval per decision.
- decisions.js: Auto / Needs you / You OK'd / Pending, in that order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A structured preference value (e.g. a brand-preference object) fell through to
String(value) and rendered as "[object Object]" in the dashboard "What I've
learned" summaries. Render arrays/objects readably instead. Adds a regression
test covering objects, nested objects, arrays, booleans, strings, numbers, and
null.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The live digest (no stored row) carries the sentinel id 'live'; its "Mark as
read" button POSTed to /briefings/live/read and 400'd on the UUID check. Gate
the New badge + Mark-as-read on a persisted briefing so the control only shows
when there's a real row to mark.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The power-view detail panel rendered noise: "URGENCY: Default for security",
"REFS: email: 77538186" (an internal id slice), "WHY: Account notice" (just the
title again), and no confidence at all. Feed it real technical depth instead:

- confidence: pull decision_outcomes.confidence -> a real percentage.
- source ref: the actual sender/organizer/file ("email: no-reply@accounts.example"),
  not an opaque decision-id slice.
- urgencyReason: a real driver ("Security alert — always sent to you", "New
  invite — awaiting your RSVP", "Routine — no deadline detected") via a new
  optional urgencyReason override on buildDigestItemDetail, instead of the
  generic "Default for <domain>".
- drop the redundant explanation (it duplicated the title).
- honest whyNotAutoExecuted: use the engine's real escalation_reason, and only
  fall back to the trust-tier gate when the item genuinely required approval —
  no fabricated "trust_tier:observer" on escalate-only items.
- normalizeUrgency: map the DB default 'normal' to 'medium', not 'low'
  (silent demotion).
- name the recent-decisions window; drop the redundant maxTodos override.

Adds a DB-mocked buildLiveDigest suite (cold start, to-do mapping + detail,
malformed raw_event, provenance fail-safe, handledCount) plus normalizeUrgency
and urgencyReasonFor helper tests. Fixes the sections-fold test's @skytwin/db
mock to define query so the live-digest path resolves cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gating the Connect-Google/Connect-Gmail heroes on `hasAnyData` hid the
onboarding CTA for users who have decisions but haven't connected Gmail (the
"Calendar connected, Gmail not yet" segment) — the heroes already self-suppress
when actually connected, so the extra gate only hurt real users. Revert to
gating on tourMode only. Also drop a dead `t.kind === 'security'` branch in the
Home digest hero (buildDigest never sets kind).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The digest told you a title and a pile of system metadata (origin, confidence,
"why it escalated") but not the two things that actually matter: what the item
says and what to do about it. Surface both, sourced from data we already had:

- body: the real content (email snippet, event description, file excerpt,
  transcript) via toSignalText, rendered as a one-line preview under each title
  — visible by default, not buried in the power view.
- suggestedAction: the twin's recommended next step, taken from the pipeline's
  selected candidate action ("Accept this calendar invitation", "Review this
  security alert in the provider's official app — don't click links in the
  message"), with sensible fallbacks for escalate-only situations.

UI: the to-do/topic rows now lead with title -> what it says; the power-view
detail leads with the actionable "suggested" step, and the trust metadata
(origin/confidence/refs) drops below it. The Home hero shows the content line
plus an iris "→ next step" so it's actionable without opening anything.

Carries body through DigestItem/DigestTodo/DigestTopicItem + buildDigest, and
adds suggestedAction to DigestItemDetail. Tests cover body extraction, the
pipeline-selected action, and the security/RSVP fallbacks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jayzalowitz and others added 12 commits June 7, 2026 15:47
Two gaps from the last pass: some suggestions were the rule-based engine's raw
internal text ("Apply appropriate labels to this email", "Escalate to user:
Decision needed regarding: transcript"), and the suggestion only showed in the
power-view detail — so in the default view most items had no visible next step.

- suggestedActionFor now maps the structured selected action TYPE to plain
  English ("Accept the invite, or decline / propose another time", "Nothing
  needed — I'll file it", "Take a look and tell me what to do"), with a
  security-specific instruction and situation fallbacks. Every item gets a
  clean, user-facing step — no engine internals leak through.
- The "→ next step" now renders in the row itself for every item (to-do and
  topic), visible without the power view. The power-view detail drops back to
  the trust/technical metadata (origin, confidence, refs) it's meant for.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The detail panel was accurate but spoke the way the system names things, not the
way a person asks. A non-technical user can't parse "ORIGIN: Inbound — untrusted",
"REFS", "NOT AUTO-RUN", a bare "CONFIDENCE: 80%", or "From your twin" — and
"untrusted" reads as a threat rather than "you didn't write this".

Rephrase everything user-facing:
- provenance: "Inbound — untrusted" -> "From someone else"; "From your twin" ->
  "From your assistant"; fail-safe stays "someone else".
- block reasons: "trust level (observer) asks me to check" -> "You've asked me
  to check with you before I act"; "From untrusted content" -> "It came from
  someone else, so I want your OK first". No internal codes leak.
- detail labels: origin/confidence/urgency/not-auto-run/refs become "where it's
  from / written by / how sure I am / why now / why I'm asking you".
- source ref: a real sender or a friendly "your calendar"/"a voice note", not an
  id slice or a filename echo.

Default view was already plain; this brings the power view to the same bar so
"advanced" doesn't mean "fluent in our nouns". Tests updated to assert the plain
wording and that no jargon leaks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the detail expansion uniformly useful and cut the filler:
- add "when" (relative time) — was missing entirely.
- "why now" is explanatory for FYI items too ("Not time-sensitive — just so
  you're aware") instead of the meaningless "Normal priority".
- confidence gets a word: "fairly sure (80%)", "very sure (100%)".
- drop the redundant "written by: someone else" (the sender already shows it);
  keep "written by: you" only when you authored it (genuinely notable).
- friendly source when there's no sender ("a voice note", "your files").

Also rename the page "Twin Briefing" → "Your briefing" with a plain subtitle,
matching the Home hero — "twin" is our metaphor, not a word a first-timer maps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The critical-urgency reason changed to "Urgent — needs your attention now";
update the assertion from /critical/i to /urgent/i. (Caught by the full test
run after the per-file runs passed — the prior commit shipped this red.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
saveRiskAssessment runs `UPDATE candidate_actions ... WHERE id = ?`, but
saveCandidates (the INSERT) ran AFTER it — so the UPDATE hit zero rows, the full
RiskAssessment (overallTier/dimensions) was lost, and only the thin
`{reasoning}` placeholder survived. At approve time the execute-preflight
(getRiskAssessment → parseRiskAssessmentFromRow, which requires overallTier)
then returned null → `risk_assessment_missing`, blocking the ENTIRE
approve→execute path (no action could ever be executed).

Move saveCandidates ahead of the risk-assessment loop so the rows exist when the
UPDATEs land. Adds a regression test asserting saveCandidates is invoked before
the first saveRiskAssessment (via vi.fn invocationCallOrder).

Found via a safe end-to-end execution-stack test (mock adapter + isolated
tokenless user + fake email); verified fixed: fresh fake email → approve →
execution completed via the (mock) adapter, no risk_assessment_missing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…est docs

bin/skytwin-test-execution-stack: a repeatable, no-real-side-effects test of the
full execution path (ingest → decide → policy/spend/risk gate → approval →
execution router → adapter → result). Two safety layers: an isolated TOKENLESS
test user (Direct handlers throw at resolveAccessToken before any Google fetch)
+ USE_MOCK_IRONCLAW (simulated adapter). Spins up its own mock-mode API on a
test port; re-runnable; asserts the stack executed and recorded a result.

docs/testing-openclaw.md: how to exercise the OpenClaw adapter safely against
local Ollama via the openclaw-bridge (verified working: Ollama installed, bridge
completes a fake action end-to-end, simulated, nothing real touched). Notes the
router trust-ranking caveat (direct outranks openclaw, so isolate it to see
OpenClaw execute) and the OPENCLAW_API_URL config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…reachable

The Connect (#/setup) page showed "Not fully synced to IronClaw" + a "Sync to
IronClaw" button even when no IronClaw is configured/reachable (the common
case), so clicking it failed with a connection error. Gate the sync lookup on
ironclawSync.reachable: when IronClaw isn't reachable (no IronClaw, the local
mock, or a remote that's down) the sync affordance is hidden entirely — it's an
advanced feature that only applies to a real, reachable IronClaw. The execution
adapter row still shows its true state (Running / Registered-but-unreachable /
Not detected) via renderAdapterStatus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… offline")

The Credential Vault page (#/credential-vault) showed "Unable to load vault
status. The API may be offline." on every load: the route's getUserId read only
req.user?.id (unset under the localhost dev-auth bypass), with none of the
req.query['userId'] fallback every other route has — so /credential-vault/status
400'd with "userId is required". Add the standard session→query→body userId
fallback (ownership still gated by requireOwnership when a real session exists),
and pass userId on the web's init/rotate/lock/unlock POST bodies so those work
under bypass too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ailed

An optional, unconfigured execution adapter (IronClaw / OpenClaw) rendered as
"Not detected" in the setup page's Live status — which reads like something is
broken. For optional engines, that's not a failure: most users never run them
(the always-available Direct adapter handles actions). renderAdapterStatus now
takes an `optional` flag; an optional adapter that isn't registered shows
"Optional — not connected" (calm, muted) instead of "Not detected". Direct still
shows "Not detected" if it ever went missing (a real problem). This is the
proper fix — correct whether or not a mock IronClaw is running, so no demo
crutch is needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jayzalowitz jayzalowitz merged commit bc1ed5c into main Jun 10, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-source generalization: extractors consume normalized signals

2 participants