Enables the M1 parallel-reads optimisation on the webapp side. Both
consumers of runPlannerLoop pass an isParallelSafe predicate derived
from the tool catalog:
isParallelSafe: (name) =>
AI_TOOL_CATALOG_BY_NAME.get(name)?.defaultPolicy === 'auto'
Auto-policy tools (list_tasks, get_habits, nutrition_summary, …) run
via Promise.all in batches of 10 when the LLM fans them out in one
round. Propose-policy tools — which surface to the user as Proposal
cards — stay sequential so intent ordering in the inbox is preserved
and pre-execute guardrails can reason about prior-step state.
Tests: 31 existing companion + mission tests pass unchanged; the
parallel path is exercised via the new loop.test.ts cases shipped
with the M1 commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the M1 reminderChannel into the mana-ai mission runner with two
initial producers in services/mana-ai/src/planner/reminders.ts:
- tokenBudgetReminder — warns at 75% of the agent's daily cap, emits a
stronger "wrap up NOW" message at/above 100%. Uses pretick usage +
accumulated round usage so the warning tracks drift during a long
plan.
- retryLoopReminder — shape is in place (round≥3 + last 2 failures),
currently limited to the single lastCall LoopState exposes. Extends
cleanly once LoopState carries the full failure window.
buildReminderChannel composes active producers; the tick hoists
pretickUsage24h so the channel has the baseline. Each round the loop
re-evaluates the producers, so usage drift across rounds surfaces on
the NEXT turn.
Also exports LoopState + ReminderChannel from @mana/shared-ai top-level
so consumers don't need to reach into /planner.
Tests: 13 new bun tests covering thresholds, pretick+round summing,
composition, and per-round re-evaluation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three Claude-Code-inspired primitives for runPlannerLoop, derived from the
reverse-engineering reports in docs/reports/:
1. **Policy gate** (@mana/tool-registry) — evaluatePolicy() gates every tool
dispatch: denies admin-scope, denies destructive tools not in the user's
opt-in list, rate-limits per tool (30/60s default), flags prompt-injection
markers in freetext without blocking. Wired into mana-mcp with a
per-user rolling invocation log and POLICY_MODE env (off|log-only|enforce,
default log-only). mana-ai uses detectInjectionMarker only — tool dispatch
there is plan-only, so rate-limit/destructive checks don't apply yet.
2. **Reminder channel** (packages/shared-ai/src/planner/loop.ts) — new
reminderChannel callback in PlannerLoopInput. Called once per round with
LoopState snapshot (round, toolCallCount, usage, lastCall); returned
strings wrap in <reminder> tags and inject as transient system messages
into THIS LLM request only. Never pushed to messages[] — the Claude-Code
<system-reminder> pattern that keeps the KV-cache prefix stable.
3. **Parallel reads** (loop.ts) — isParallelSafe predicate enables
Promise.all dispatch when every tool_call in a round is parallel-safe,
in batches of PARALLEL_TOOL_BATCH_SIZE=10. Any non-safe call downgrades
the whole round to sequential. messages[] always appends in source
order, never completion order, so the debug log stays linear.
Default-off (undefined predicate) preserves pre-M1 behaviour.
Tests: 21 new in tool-registry (policy), 9 new in shared-ai (5 parallel,
4 reminder). All 74 green, type-check clean across 4 packages.
Design/plan: docs/plans/agent-loop-improvements-m1.md
Reports: docs/reports/claude-code-architecture.md,
docs/reports/mana-agent-improvements-from-claude-code.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation for autonomous Claude-driven testing. Plan:
docs/plans/mana-mcp-and-personas.md.
New packages
- @mana/tool-registry — schema-first ToolSpec<InputSchema, OutputSchema>
with zod generics, scope ('user-space' | 'admin') and policyHint
('read' | 'write' | 'destructive'). sync-client helpers speak the
mana-sync push/pull protocol directly so RLS and field-level LWW are
preserved. MasterKeyClient fetches per-user MKs via the existing
mana-auth GET /api/v1/me/encryption-vault/key endpoint (JWT-gated,
ZK-aware, already audited) — no new service-key endpoint built.
ZeroKnowledgeUserError surfaced as a typed throw.
- @mana/shared-crypto — AES-GCM-256 primitives extracted from the web
app's $lib/data/crypto/aes.ts so the server-side tool handlers and the
browser produce byte-for-byte identical wire format
(enc:1:{b64(iv)}.{b64(ct)}). Web app aes.ts now re-exports from
shared-crypto — 5 existing importers unchanged, svelte-check stays
green.
New service
- services/mana-mcp (:3069, Bun/Hono) — MCP Streamable HTTP gateway.
JWKS auth against mana-auth, per-user session isolation (session-id
belongs to the user who opened it — cross-user access returns 403),
admin-scoped tools filtered out before registration. MasterKeyClient
cached per process with a 5-minute TTL.
11 tools registered
- habits.{create,list,update,archive}, spaces.list (plaintext, M1)
- todo.{create,list,complete}, notes.{create,search}, journal.add
(encrypted — field lists match
apps/mana/apps/web/src/lib/data/crypto/registry.ts verbatim)
Infra
- Port 3069 added to docs/PORT_SCHEMA.md
- services/mana-mcp/CLAUDE.md with architecture, auth model,
tool-authoring recipe, local smoke-test steps
- Root CLAUDE.md services list updated
Type-check green across shared-crypto, mana-tool-registry, mana-mcp.
svelte-check on apps/mana/apps/web stays at 0 errors / 0 warnings.
Boot smoke verified: /health returns registry.loaded=true, unauthed
/mcp → 401, invalid-JWT /mcp → 401 with descriptive message.
Decisions locked in for later milestones (per plan D1–D10):
- Personas will be real mana-auth users (users.kind='persona'), no
service-key bypass (D1, D2)
- Tool-registry is the SSOT; mana-ai and the legacy
apps/api/src/mcp/server.ts get merged into it in M4 (three current
parallel tool catalogs collapse to one)
- Persona-runner (:3070) will be a separate service using the Claude
Agent SDK + MCP client (D5)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New providers gemini-deep-research + gemini-deep-research-max on the
Interactions API (preview-04-2026). Submit/poll split, tier parameter
selects between standard (~minutes, $1–3) and max (up to 60 min, $3–7).
- Parser matches the real response shape: flat `outputs` array of
thought|text|image items, url_citation annotations without title,
`usage.total_input_tokens` / `total_output_tokens`.
- Route generalisation: /v1/research/async accepts `provider` with
default 'openai-deep-research' (backward compatible) and dispatches
to the right submit/poll pair.
- New internal service-to-service endpoint /v1/internal/research/async
gated by X-Service-Key + X-User-Id for credit accounting. Enables
mana-ai to drive deep-research jobs on the mission owner's wallet
without requiring a user JWT.
- Pricing: 300 credits (standard) / 1500 credits (max). Conservative
markup over the ~$3/$7 ceiling so the first runs can't surprise us.
- Docs: AGENT_PROVIDER_IDS + pricing + env map + auto-router stay in
sync; CLAUDE.md Phase 3b now current; API_KEYS.md references the
new providers under GOOGLE_GENAI_API_KEY.
Verified with a real smoke test against the Gemini API: submit + poll
both succeed, completed response parsed cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PlayView used Tailwind palette classes for game-status feedback:
bg-emerald-500/10 + text-emerald-300 (won) → bg-success/10 + text-success
bg-amber-500/10 + text-amber-300 (lost) → bg-warning/10 + text-warning
border-red-500/20 + bg-red-500/10 +
text-red-300 (error) → border-error/20 + bg-error/10 + text-error
placeholder-white/30 focus:border-purple-400/50 → placeholder:text-muted-foreground/60 focus:border-primary/50
Semantic status now tracks the theme (errors are red in dark, darker red
in light, etc.) instead of being fixed hex ramps.
The `bg-purple-500` / `bg-purple-500/30` / `hover:bg-purple-600` classes
on the user's chat bubble and submit buttons STAY — purple is the who
module's primary identity colour (historical-deck accent `#a855f7` is
semantically the same hue). Documented in brand-literals.md §who.
Also harden two validators against mid-rename states where git ls-files
returns paths that aren't on disk yet — both now skip unreadable files
instead of crashing the pre-commit hook (caught while migrating who).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The plan-doc commits 129971ffc + 9db044178 dropped the
audit-theme-tokens → validate-theme-variables rename, the
validate-theme-tokens → validate-theme-utilities rename, the new
validate-theme-parity script, brand-literals.md, and the corresponding
package.json + lint-staged.config.js + themes.css wiring. The files
still existed on disk (git mv changes survived) but were untracked.
Restore the validator suite so `pnpm run validate:all` works again:
- validate:theme-variables (CSS var names: --muted → --color-muted)
- validate:theme-utilities (Tailwind: no white/N, no neutral palette)
- validate:theme-parity (every --color-* in :root ⇔ .dark + each
[data-theme="..."])
All three wired into validate:all and lint-staged. `pnpm run validate:all`
is clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mana/shared-pwa gains PWAShareTarget + PWAShareTargetParams types
plus ManifestConfig.share_target pass-through. createPWAConfig now
accepts an optional `shareTarget` and threads it into the generated
manifest. Other apps keep working unchanged — the field is omitted
unless set.
Web app wiring:
- vite.config.ts passes shareTarget: { action: '/articles/add',
method: 'GET', params: { title, text, url } } so the installed PWA
shows up as a destination in the Android / Chromium share sheet.
- AddUrlForm reads ?url / ?text / ?title in onMount; falls back to
the first URL-shaped token in ?text because some senders (Chrome
Android, WhatsApp) put the shared link there instead of ?url. When
a URL is pre-filled the Readability preview auto-triggers, so the
user just hits "In Leseliste speichern" to confirm.
- New /articles/settings route hosts the bookmarklet (drag-to-
bookmarks-bar button + copy-to-clipboard + expandable snippet
viewer) and a short Share-Target explainer with an iOS-Safari
caveat. Linked from the ListView via a new gear button next to
"+ Neu speichern".
Bookmarklet form (origin-prefixed so it works across tenants):
javascript:void(window.open('${origin}/articles/add?url='+…))
Not in scope (plan marked optional): _pendingUrls offline queue.
Share without internet shows the existing error + retry state today;
can slot in as M7b if users hit it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shared-types/src/index.ts re-exports with explicit .ts extensions
(Tailwind v4 module resolver needs them). TS 5.7 requires consumers
to opt in via allowImportingTsExtensions. The flag only type-checks
when noEmit:true; the NestJS builder also needs
rewriteRelativeImportExtensions so tsc still emits valid JS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract member management from /spaces/members into a reusable
workbench-card ListView so users can drop the surface into any scene.
- lib/modules/spaces/ListView.svelte — hint + invite + members + pending
invitations, all theme-token driven
- APP_ICONS.spaces icon (three-silhouette cluster, teal→indigo)
- MANA_APPS entry id=spaces (beta tier, shared-space management)
- registerApp({ id: 'spaces' }) so the card is scene-droppable
- /spaces/+page.svelte as the new canonical route wrapper
- /spaces/members/+page.svelte kept as legacy alias
- SpaceSwitcher menu now links to /spaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five new entries in AI_TOOL_CATALOG (shared-ai/src/tools/schemas.ts):
list_articles auto Read-only listing with status +
query filter. Default hides
archived; 'all' includes them.
save_article propose URL → Readability → encrypted save.
Delegates to articlesStore.saveFromUrl
which already handles scope-aware
dedupe. Duplicates surface as
success:true with duplicate:true.
archive_article propose setStatus('archived') after
scoped existence check.
tag_article propose Case-insensitive dedupe over
globalTags; tagMutations.createTag
fills in when missing. Junction
write via articleTagOps.addTag.
add_article_highlight propose Snaps to the first verbatim
occurrence of `text` in the
decrypted article.content. Fails
cleanly when the snippet isn't
found — no orphan highlights.
Policy, client executor, and server planner derive automatically from
the catalog (see root CLAUDE.md §"AI Tool Catalog") so no manual
registration in policy.ts / services/mana-ai is needed.
Skipped from the M6 plan: <AiProposalInbox module="articles" />. The
component doesn't exist in the current codebase — after the
pendingProposals-table drop in Dexie v29 the inbox surface moved to
the mission-detail cross-module view, and articles proposals show up
there automatically. Documented in docs/plans/articles-module.md.
Also updated: plan doc now marks M1–M6 as DONE with commit refs and
the next-step pointer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes surfaced by the end-to-end smoke test.
1. broadcast-track.ts: inner route paths double-prefixed.
Routes were declared as '/track/open/:token' etc, then
mounted at '/api/v1/track', yielding '/api/v1/track/track/open/:token'
— every tracking endpoint returned 404. Dropped the redundant
'/track/' prefix so the full path is now
'/api/v1/track/{open,click,unsubscribe}/:token' as the
orchestrator + client both expect.
Verified with live curl:
- /track/open/BAD → 200 image/gif 42 bytes (graceful no-signal)
- /track/click/?url missing → 400 missing url
- /track/click?url=javascript: → 400 bad url
- /track/click?url=https://ok + bad token → 302 graceful
- /track/unsubscribe/BAD GET → 400 HTML
- /track/unsubscribe/BAD POST → 400 (RFC 8058)
2. shared-branding/tsconfig.json: allowImportingTsExtensions
missing. shared-types/src/index.ts uses explicit .ts
imports (intentional, for Tailwind's module resolver); any
downstream tsconfig without allowImportingTsExtensions emits
8 errors. shared-auth already had this fix — shared-branding
gets the same treatment. noEmit:true is set, so no rewrite
flag needed.
Verified: shared-branding pnpm check → 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three modules move from "dedicated route only" to "first-class
apps in the launcher". After this they show up in the AppDrawer
pill, can be pinned to workbench scenes, and get a direct URL from
the app switcher.
MANA_APPS entries added:
- agents (/agents) — AI agent management. Icon: smiling robot head
with antenna dot. violet→fuchsia gradient, status
beta, requiredTier beta.
- timeline (/timeline) — Chronological view across modules. Icon: vertical
event dots with connecting axis. amber→orange,
status beta, requiredTier beta.
Plus: broadcast's MANA_APPS entry already existed but had no URL
override, so the auto-derived /broadcast didn't match the real route
at /broadcasts. Added an APP_URL_OVERRIDES entry mapping
id='broadcast' → '/broadcasts' so the app switcher lands the user on
the right page. Icon + module.config stay singular.
Route wiring:
- /agents previously only had /agents/templates/ as a subroute. Added
/agents/+page.svelte that renders the existing ai-agents ListView
(at $lib/modules/ai-agents/), so the top-level URL works from the
AppDrawer.
- /timeline already had a root +page.svelte — no work there.
- /broadcasts already had a root +page.svelte — no work there.
/spaces/members page chrome:
- Swapped the hand-rolled header for @mana/shared-ui PageHeader with
backHref="/", breadcrumb "Workbench › Mitglieder verwalten", and the
space name + type as the description. Feels like a native Mana page
now instead of an orphaned admin route.
- Dropped the ~60 lines of unused .type-chip CSS (moved the chip info
into the PageHeader description string).
- Container bumped to 720px max-width to match other admin pages.
0 errors across 7236 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pocket-style module for saving arbitrary web URLs, extracting readable
content server-side via @mana/shared-rss (Readability + JSDOM), and
storing it AES-GCM encrypted in IndexedDB for offline reading.
M1 skeleton: Dexie v33 (articles, articleHighlights, articleTags),
crypto registry entries, module registration, app-registry entry with
orange icon, empty-state ListView. articleTags is a pure junction
into the existing globalTags system (appId 'tags') — same pattern as
noteTags, eventTags, placeTags.
M2 URL save + reader: POST /api/v1/articles/extract (one endpoint,
not two — client caches the preview payload to avoid a double
server fetch). AddUrlForm with scope-aware dedupe, DetailView with
ReaderView typography shell (serif/sans, light/sepia/dark, size
slider), auto-tracked reading progress with scroll restore.
M3 highlights: TreeWalker-based plain-text offset resolution
(lib/offsets.ts), highlights store, floating HighlightMenu with
create + edit modes, HighlightLayer orchestrator that wraps/unwraps
highlight spans whenever highlights or htmlVersion changes. Four
colours (yellow/green/blue/pink), optional notes, click-to-edit,
dark-mode-aware overlay colours.
Drive-by: removed stale 'pendingProposals' entry from the plaintext
allowlist — the table was dropped in Dexie v29 and the allowlist
audit was flagging it as a dead entry.
Plan: docs/plans/articles-module.md. M4 (tags + filter + progress),
M5 (news:type='saved' migration), M6 (AI tools), M7 (share target),
M8 (highlights view + stats) still open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the M7/M9/M10 plan items in one pass since they share patterns.
ListView (M7)
- 4 stats cards at the top: versendet YTD, Ø Öffnungsrate, Ø Klickrate,
Entwürfe. Same layout pattern as invoices for consistency.
- Status filter chips with live counts per status.
- Search across name + subject.
- Row now shows open-rate per-campaign when available.
- Settings gear in the header matches the invoices polish.
Dashboard widget (M10)
- BroadcastsWidget.svelte: 2x stats (sent YTD + avg open rate), next
scheduled link, last sent link with open-rate badge. Empty state
nudges toward creating a first campaign.
- Registered as 'broadcasts' in WIDGET_REGISTRY and the component map.
- Medium default size, no requiredBackend (reads from Dexie only;
stats are mirrored from the last DetailView poll so no server
round-trip for the widget).
AI tools (M9)
- 3 tools added to @mana/shared-ai's AI_TOOL_CATALOG:
- create_campaign_draft (propose) — generates HTML body from a
topic, lands as a draft; user picks audience + sends via UI
- list_campaigns (auto) — id/name/subject/status/recipients
- get_campaign_stats (auto) — rates as 0..1 floats
- broadcast/tools.ts: execute handlers with an HTML→CampaignContent
shim (stores both html and a minimal Tiptap JSON placeholder so
ListView renders without the editor having to remount). stripHtml
helper derives plaintext.
- Registered in data/tools/init.ts after library.
Suggest-style tools (suggest_subject_lines) deliberately omitted —
they're pure generative and don't need an executor. The LLM can
produce subject ideas without a tool call.
Verified:
- pnpm check: 0 broadcast errors (4 pre-existing errors in articles
module from parallel work, not mine)
- shared-ai test suite: 44/44 green (function-schema roundtrips the
expanded catalog cleanly)
- mana-ai drift guard: 41/41 green
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Library module had no AI tool coverage post the M1 skeleton. Adds
four tools so the agent can curate the reading/watch list alongside
other modules:
- create_library_entry (propose) — books/movies/series/comics with
creators, year, status, rating, tags, genres. Default status
"planned" covers the most common flow ("add to watchlist").
- update_library_entry_status (propose) — status transitions
planned → active → completed (also paused / dropped). Auto-
stamps startedAt/completedAt on the matching transitions so the
existing Dexie projections (streaks, progress) fire correctly.
- rate_library_entry (propose) — 1-5 stars, thin wrapper over the
store's rate() method.
- list_library_entries (auto) — id/kind/title/status/rating/year,
filterable by kind + status.
Coverage table in apps/mana/CLAUDE.md updated (+library, +invoices
row that wasn't listed). Total now 67 tools / 21 modules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Repositions the switcher from its floating spot in the top right of
the workbench into the bottom-fixed PillNav so it sits with the rest
of the nav chrome. Matches how every other persistent nav control
(app switcher, AI tier, sync status) lives in the PillNav.
Mechanics:
- @mana/shared-ui PillNavigation gains a `startSlot?: Snippet` prop
rendered inside .pill-nav-container, before AppDrawer. Generic slot
— any host component drops in.
- (app)/+layout.svelte passes the existing <SpaceSwitcher /> as the
snippet (authenticated only). The old .space-bar wrapper above
<main> is removed along with its CSS.
- SpaceSwitcher trigger is restyled to match Pill conventions: pill
radius 999px, 32px height, 0.8125rem text, tighter paddings, shorter
name cap (7rem). Visually merges with the surrounding Pills.
- Dropdown menu flips upward (bottom: calc(100% + 4px)) because the
PillNav is position:fixed bottom — opening downward would land
off-screen.
Type-check: 0 errors across 7200 files.
Scope tests: 10/10 pass.
Go tests + bun tests (mana-auth): all pass.
Plan: docs/plans/spaces-foundation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Core authoring loop works end-to-end: create a draft, filter an audience
from contacts, write content in a rich-text editor, save. Send is still
stubbed (M4 gets mana-mail's bulk endpoint).
Dependencies
- @tiptap/core + starter-kit + image + link + placeholder (3.22.4)
- shared-auth/tsconfig: allowImportingTsExtensions +
rewriteRelativeImportExtensions so tsc accepts shared-types' explicit
.ts imports. Was blocking EVERY pnpm install postinstall hook in the
repo — fixing it here unblocks everyone, not just broadcast.
Module
- queries.ts: useAllCampaigns / useAllTemplates with scoped-db + crypto,
computeStats (counts + open/click rates per year), formatRate helper
- stores/settings.svelte.ts: singleton with ensure/get/update, same
pattern as invoices settings
- stores/campaigns.svelte.ts: createCampaign (pulls sender defaults from
settings), updateCampaign / updateContent / updateAudience (draft-only
edit guard), schedule / cancel / duplicate / deleteCampaign, plus an
applyServerStatus hook for M4's orchestrator to write back progress
Audience
- audience/segment-builder.ts: pure matchContact / filterAudience /
countAudience / describeAudience. AND semantics across filters. Drops
contacts without a usable email so estimatedCount never inflates.
- audience/AudienceBuilder.svelte: tag-chip UI with live count, dedup
(same tag twice toggles op instead of stacking), greys out already-
referenced tags in the picker
Editor
- editor/Editor.svelte: Tiptap wrapper with onMount / onDestroy, toolbar
(bold/italic/H1/H2/lists/link/image), bind on content (Tiptap JSON +
derived HTML/plaintext). Image upload reuses invoices' mana-media
uploader pragmatically; extract to @mana/shared-uload later.
Compose wizard
- views/ComposeView.svelte: 4-step stepper (Audience → Content →
Preflight → Send). Steps 3+4 stubbed pragmatically. Autosave on step
change so content survives navigation. Step 3/4 gated on earlier
readiness so the user can't skip.
Routes
- /broadcasts/new: bootstraps a draft + redirects to edit
- /broadcasts/[id]/edit: guarded on status=='draft'
- ListView: working "+ Neue Kampagne" button, rows open edit
Tests
- 17 unit tests for segment-builder covering tag has/not-has/AND,
email eq/contains case-insensitivity, no-email filtering, no-mutation,
describeAudience resolver + fallback
Plan: docs/plans/broadcast-module.md §M2.
Next: M3 HTML-render with email-safe inlining + preview.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New 1:N email-campaign module (newsletters / announcements). M1 scope:
- types (LocalCampaign / LocalBroadcastTemplate / LocalBroadcastSettings),
constants (STATUS_LABELS, BROADCAST_SETTINGS_ID, rate-limit hints)
- collections.ts: Dexie table refs, no guest seed (a demo campaign that
might accidentally hit real SMTP felt wrong)
- module.config registered in module-registry
- Dexie v32 wired in (already in tree from a parallel Spaces commit
picking it up via lint-staged — matches what the module expects)
- encryption registry entries for all three tables (type-safe via
entry<T>), content + audience always encrypted because the recipient
graph is a leakable business secret
- app entry (requiredTier: alpha) + megaphone gradient icon
(indigo→cyan, sits between mail and invoices in the comm family)
- route /broadcasts mounts ListView with empty-state placeholder
Status machine defined: draft → scheduled → sending → sent, with
cancelled as the off-ramp from draft/scheduled. No CRUD yet — that's M2.
Plan: docs/plans/broadcast-module.md.
Next: M2 AudienceBuilder + Tiptap editor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migration from user-level tier to Space-level tier, following the
Spaces foundation plan. User-visible effect: the tier that gates
module access now belongs to the active Space, not the user account.
Personal Spaces inherit the user's old tier on signup so nothing
downgrades.
shared-types:
- New SpaceTier type ('guest' | 'public' | 'beta' | 'alpha' | 'founder').
- New spaceTierMeets(actual, required) helper.
- SpaceMetadata gains an optional `tier` field.
mana-auth:
- createPersonalSpaceFor reads user.accessTier and stamps it into the
personal Space's metadata.tier. A founder-tier user setting up their
first Space keeps founder access in that Space.
- databaseHooks.user.create.after now forwards accessTier into the
personal-space creator.
apps/web (scope layer):
- ActiveSpace gains a required `tier: SpaceTier`; rawToActiveSpace
reads it from organization.metadata, defaulting to 'public' if
missing or invalid.
- New getEffectiveTier(userFallback) helper resolves the tier to use
for gating: prefers the active Space's tier, falls back to the
caller-supplied user tier during the boot window.
apps/web ((app) layout):
- `effectiveTier` $derived replaces every authStore.user?.tier reference
in the layout's access-gating logic (appItems, routeBlocked,
routeTierLabels). AuthGate deeper in the UI keeps using user.tier as
its own fallback — the tier move is additive, not destructive.
What this does NOT do yet:
- The user.accessTier column still exists and is still the initial
source for personal-space tier. Removing it is a later cleanup once
every code path reads through the Space primitive.
- No admin API for setting tier on a Space (PUT /api/v1/admin/spaces/
:id/tier). Follow-up when admin tooling needs it — today admins still
set user.accessTier, which flows to the personal space on next
signup.
Resolves the MANA_APPS-tier-patch workaround memory: future sessions
can adjust tier per Space instead of per User.
0 errors across 7151 files. 10/10 scope tests pass.
Plan: docs/plans/spaces-foundation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tailwind v4's module loader follows imports out of @source-scanned
packages (e.g. shared-branding/spaces.ts imports from @mana/shared-
types) via Node's ESM resolver, which mandates explicit extensions on
relative paths. Without them Vite's Tailwind integration blew up
with `Cannot find module '.../shared-types/src/theme'` at dev-server
boot.
Downstream tsconfigs all run `moduleResolution: "bundler"`, which
accepts `.ts` suffixes on relative imports without requiring
`allowImportingTsExtensions`. No downstream code changes needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The last open item from the plan. Missions can now draft invoices from
chat context, mark customer payments, and read status for autonomous
follow-up cadences.
Tool catalog (packages/shared-ai/src/tools/schemas.ts)
- create_invoice (propose) — clientName + lines[] + currency + due
- mark_invoice_paid (propose) — by id, optional back-dated paidAt
- list_invoices (auto) — with status + limit filter
- get_invoice_stats (auto) — open/overdue/YTD per currency
Had to widen the tool-parameter type vocabulary so create_invoice can
declare lines as a typed array. Touched three places:
- ToolSchema-side: the catalog's `type` string is already free-form so
'array' / 'object' just pass through
- ModuleTool-side (apps/mana/apps/web/src/lib/data/tools/types.ts): added
'array' | 'object' to the union so TS doesn't narrow the executor's
param signatures
- function-schema translator (packages/shared-ai): mapParamType +
JsonSchemaProperty both gained the two new types; the catalog-typo
guard test now uses 'fruit' as its sentinel (array no longer unknown)
Executor (apps/mana/apps/web/src/lib/modules/invoices/tools.ts)
- coerceLines accepts either a real array or a JSON-stringified array
(planners vary), skips malformed entries, converts major→minor units
- create_invoice pulls the generated number back from Dexie so the
success message shows "Entwurf 2026-0042 …" — the user recognises it
- mark_invoice_paid normalises YYYY-MM-DD → ISO so the store's timestamp
invariant (ISO throughout) stays intact
- list_invoices derives overdue on read (consistent with useAllInvoices),
returns major-unit amounts so the LLM reasons in user-facing numbers
- get_invoice_stats returns counts + open/overdue/YTD per currency
Registration: invoicesTools added to tools/init.ts. mana-ai drift guard
is happy (41/41 green); webapp + shared-ai type-check 0 errors; full
invoice test suite 59/59 green.
Closes: docs/plans/invoices-module.md §M8. All plan milestones now DONE.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Carries per-round token counts from the mana-llm response body
(prompt_tokens + completion_tokens) back through LlmCompletionResponse
→ PlannerLoopResult. The loop sums across rounds and exposes a single
aggregate on result.usage.
Lets mana-ai's tick re-activate per-agent daily-token budget tracking
— tokensUsed was stubbed to 0 in the migration commit (6) because the
loop didn't surface usage yet. Now recordTokenUsage + agentTokenUsage24h
get real numbers again, and the mana_ai_tokens_used_total Prometheus
counter is accurate.
Additive only: consumers without usage needs ignore the new field,
and providers that don't return usage produce zeros (not undefined —
the loop still exposes the object so downstream branches stay trivial).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The runPlannerLoop test file and the webapp's mission-runner test each
had their own inline scripted LLM mock — same interface, diverged
slightly. Consolidates into packages/shared-ai/src/planner/mock-llm.ts
and re-exports from the package root so any consumer can drive the
loop deterministically.
Both existing test files now use the shared client. 5 + 3 tests pass,
44 total in shared-ai still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The companion chat had its own ad-hoc 3-round tool-calling pipeline:
build a system prompt with tool descriptions, ask the LLM to emit
```tool JSON blocks, regex-extract, execute, feed back the result as
a synthetic user message. Same fragility class as the old text-JSON
planner — and now unnecessary since mana-llm speaks native function
calling.
Migrates companion/engine.ts to the shared runPlannerLoop, same as
the mission runner (commit 5a) and the server tick (commit 6). Tools
go to the LLM as proper function-schemas; tool_calls come back
structured; the executor runs them directly under USER_ACTOR.
Extends shared-ai/planner/loop.ts with an optional priorMessages[]
input field so the chat can preserve multi-turn history between
turns (missions don't need this and leave it empty).
Deletes the old llm-tasks/companion-chat.ts LlmTask wrapper. Nothing
else imported it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves the canonical SpaceType + SPACE_MODULE_ALLOWLIST to @mana/shared-types
(framework-free) so the Bun services can consume them without pulling in
Svelte. shared-branding keeps only the UI-facing labels and descriptions
and re-exports the canonical types for frontend convenience.
Wires two Better Auth organization hooks in mana-auth:
- beforeCreateOrganization asserts metadata.type is a valid SpaceType,
rejecting the create with a BAD_REQUEST otherwise.
- beforeDeleteOrganization rejects deletion of the personal space.
Covered by bun tests (11 assertions) for the helper module.
No migration and no schema change — type lives in the existing
organization.metadata jsonb column.
Plan: docs/plans/spaces-foundation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces SpaceType ('personal' | 'brand' | 'club' | 'family' | 'team' |
'practice') and SPACE_MODULE_ALLOWLIST as the shared-branding primitives
for the Spaces refactor that replaces the user-vs-org polymorphy with a
single tenancy primitive (Notion/Linear pattern).
Pure additive — no runtime behaviour change yet. Better Auth config,
Dexie migration, scope wrapper and rolling module migration follow in
separate commits.
Plan: docs/plans/spaces-foundation.md
Social-relay plan now defers brand storage to the Spaces primitive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the mobile-app deletion unblocked \`@context/mobile\`, five more
pre-existing failures surfaced across shared packages and two services.
All were silent-masked by the postinstall \`|| true\` for months.
- **shared-ai**: \`planner/loop.ts\` imported \`ToolSchema\` from
\`../tools/function-schema\`, which only imports (not re-exports) the
type. Fixed to import from the source (\`../tools/schemas\`).
- **shared-logger**: \`typeof window !== 'undefined'\` blows up under
tsconfigs that don't include the DOM lib (e.g. uload-server's
\`bun-types\`-only config), because shared-logger is consumed via
source import. Replaced with a \`globalThis\`-indirected check that
compiles under any lib configuration.
- **shared-hono**: \`credits.ts\` returned \`res.json()\` directly as
\`Promise<T | null>\`. Modern \`@types/node\` / undici types return
\`unknown\` strictly — cast to \`T\` at the boundary so the generic
contract is explicit.
- **uload-server**: \`routes/analytics.ts\` + \`routes/email.ts\` still
imported \`AuthUser\` from a \`middleware/jwt-auth\` module that was
deleted during the migration to \`@mana/shared-hono\`. Replaced with
\`AuthVariables\` from shared-hono, which matches the actual context
shape set by \`authMiddleware()\`.
- **manavoxel/web**: \`guestSeed\` collection entries were wrapped in
arrow functions, but \`local-store\` expects \`T[]\` directly and
iterates \`seed.length\` — which on a function is 0. The "guest
seed" was silently dead; eager-evaluating \`generateGuestWorld()\`
once and sharing the result fixes both the type and the runtime.
Verified: \`pnpm run type-check\` from the repo root now exits 0 —
76/76 tasks successful, no failures. First fully green state since
well before the postinstall \`|| true\` was introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces the new planner pipeline both the webapp runner and the
mana-ai tick will swap onto in the next commits. Additive for now —
the legacy buildPlannerPrompt + parsePlannerResponse stay exported so
callers can migrate one at a time; they get removed once the last
consumer is gone.
- planner/loop.ts — runPlannerLoop orchestrates a multi-turn chat
against a caller-supplied LlmClient. Tool-calls from the LLM are
handed to an onToolCall callback and their results fed back as
tool-messages. Parallel tool-calls in one turn execute sequentially
to keep the message log linear for debugging. Stops on assistant
stop, empty tool_calls, or a hard max-rounds ceiling (default 5).
- planner/system-prompt.ts — new buildSystemPrompt. ~40-line German
system frame, no tool listing (the SDK-level tools field carries
the schemas now), no JSON format example, no "please return JSON"
plea. User frame renders mission + linked inputs + last 3
iteration summaries, same as before.
- Five test cases covering the loop: immediate stop, single tool
call with result feedback, parallel calls execute in order, tool
failures propagate as tool-messages the LLM can react to, and
maxRounds ceiling fires with the right stopReason.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New outbound-finance module that issues invoices to clients. M1 scope:
- types, constants, collections with demo seed (not auto-loaded)
- module.config registered in module-registry
- Dexie v27 with invoices / invoiceClients / invoiceSettings tables
- encryption registry entries for all three tables (type-safe via entry<T>)
- app entry (requiredTier: alpha) + gradient icon (emerald→teal, QR corner)
- route /invoices mounts ListView with empty state
Money stored as integers in minor units (Rappen/cents) to avoid float
drift. Totals kept plaintext for liveQuery aggregation; lines encrypted
as a whole array so titles ride alongside. Settings is a singleton with
stable sentinel id so sync dedupes on it.
Plan: docs/plans/invoices-module.md. Next: M2 CRUD + number generator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single bridge between the AI_TOOL_CATALOG shape and the wire format every
provider (Gemini, OpenAI-compat, Ollama ≥ 0.3) speaks for native tool
calling. Keeps the catalog as the source of truth — the runner never
reads catalog entries directly; it asks this converter for function-spec
shapes to hand the LLM.
- No _rationale or wrapper-tool injection: the runner doesn't need it
and the added schema noise would hurt planner quality.
- Throws on unknown parameter types so catalog typos (e.g. "array"
instead of "string") fail loudly instead of coercing silently.
- Preserves enum constraints; drops the enum key entirely when absent
so Gemini doesn't reject empty-enum function-declarations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After yesterday's type-check cascade repair (c34175afa), the root
\`pnpm run type-check\` progressed through 5 more packages but still
stopped on two pre-existing failures:
- \`services/mana-media\` delivery route: \`c.body(transformedBuffer)\`
passed a Node \`Buffer<ArrayBufferLike>\`, but Hono 4.7 types the
body argument as \`Uint8Array<ArrayBuffer>\` (strict — no
ArrayBufferLike). \`Uint8Array.from(buf)\` gives a clean copy with a
fresh \`ArrayBuffer\` backing that the strict type accepts. Runtime
cost for a handful of KB per image transform is negligible next to
the Sharp pipeline that produced the buffer.
- \`packages/shared-llm\`: same rune issue as local-stt + local-llm —
\`store.svelte.ts\` uses \`$state\` and transitively pulls in
\`local-llm/src/svelte.svelte.ts\`. Plain tsc can't resolve Svelte 5
runes. Same treatment: \`type-check\` script explicitly skips with a
message pointing at svelte-check.
Root \`pnpm run type-check\` now reaches \`@context/mobile\`, which has
real code-level type errors (adapter shape mismatches, an RN event-
handler typing drift, and a deleted Supabase module still imported by
\`utils/supabaseTest.ts\`). Those need domain changes, not config
tweaks — out of scope for this repair pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Yesterday's postinstall fix (\`d1d37749f\`) removed the \`|| true\`
guards, which in turn exposed that \`pnpm run type-check\` at the
root had been red for a long time but nobody noticed. Several per-
package scripts were genuinely broken:
- \`@mana/test-config\`: \`vitest.config.base.ts\` and \`.svelte.ts\`
pass \`all: true\` to the coverage block. Vitest 4 removed that flag
(including uncovered files is now the default), so tsc reports
\`'all' does not exist in type 'CoverageOptions'\`. Removed both.
- \`@mana/credits\`: \`tsconfig.json\` include glob had
\`"src/**/*.svelte"\`, which makes tsc try to parse .svelte files
as TS source. It can't. Removed .svelte from include; added
\`"exclude": ["src/web/**"]\` — the web consumer layer is checked by
svelte-check in the apps that import it, not here.
- \`@mana/local-stt\` + \`@mana/local-llm\`: ship \`svelte.svelte.ts\`
files that use Svelte 5 runes (\`$state\` etc.). Plain tsc has no
rune support — \`$state\` is not a name it knows about. Both
packages' \`type-check\` scripts now explicitly skip with a message
pointing at svelte-check as the right tool. The rune code is still
type-checked by svelte-check when a consumer app runs \`pnpm check\`.
- \`@manavoxel/shared\`: was missing its \`tsconfig.json\` entirely,
so the \`type-check\` script ran tsc with no config, which dumped
the CLI help and exited non-zero. Added a minimal bundler-mode
tsconfig matching the pattern used by sibling packages.
\`pnpm run type-check\` now goes further than it has in months —
next failure is a real pre-existing Hono type mismatch in
\`services/mana-media/apps/api/src/routes/delivery.ts\` (Buffer vs
c.body signature), which is out of scope here and needs a proper
code fix, not a config fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The root postinstall was `node scripts/generate-env.mjs || true &&
pnpm run build:packages || true`. Two failures were being swallowed:
1. shared-auth's build has been broken for a while. shared-types
re-exports its submodules with explicit `.ts` extensions
(`export * from './theme.ts'`), which only works for downstream
consumers that set `allowImportingTsExtensions: true`. shared-auth
didn't — tsc emitted TS5097 on every re-export, the build failed,
`|| true` hid it, every `pnpm install` appeared clean.
2. The filter `@mana/*` matches everything in the workspace, including
`@mana/web` — the full 27-module SvelteKit build. On postinstall
this kicked off vite, which OOM-aborted during SW generation.
That's the original reason `|| true` was added, judging by shape.
Fixes:
- Dropped the `.ts` suffix from shared-types/src/index.ts re-exports.
shared-types is consumed in bundler-mode tsconfigs everywhere, so no
extension is the portable form. shared-types' own `tsc --noEmit`
still passes.
- Narrowed the filter from `@mana/*` (name-glob, matches apps) to
`./packages/*` (path-glob, only workspace packages). Scope drops
from 133 → 39 projects; build:packages now runs cleanly in ~15s.
- Removed both `|| true` guards. A broken postinstall now fails
loudly instead of producing a half-built state nobody notices.
Verified: `pnpm install` completes exit 0 in 13s; all 39 packages
build green.
Closes audit item #37 (postinstall swallows errors).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the Quiz CRUD surface for the AI agent. Five new tools:
- update_quiz (propose) — rename/archive/pin + description/category
- update_quiz_question (propose) — text, type+options, explanation;
rejects a type swap without a matching optionsJson
- delete_quiz_question (propose) — symmetric to add_quiz_question
- get_quiz_questions (auto) — lets the planner see existing questions
before appending more (avoids duplicates)
- get_quiz_stats (auto) — attemptCount / avgScore / bestScore /
lastAttemptAt; enables adaptive missions like "analyze my weak spots
and generate harder questions"
delete_quiz deliberately left out — too destructive to leave in the
AI's hands when the user can delete manually in two clicks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Quiz is now an AI-accessible module. The agent can mint empty quizzes
and append questions across all four types (single / multi / truefalse
/ text) via a single add_quiz_question tool whose optionsJson payload
shape is documented in the catalog description. list_quizzes (auto)
returns decrypted metadata so the planner can reference existing
quizzes when extending them. Enables missions like "baue ein Quiz aus
meinen Notizen zu Thema X" — planner reads via list_notes, proposes
create_quiz, then N × add_quiz_question.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI previously ran `pnpm run test || true` — test failures were silently
swallowed with no artifact, so we had no visibility into what was actually
passing across 1,296 test files.
- New `test:coverage` turbo pipeline task + root script; packages that opt
in by declaring their own `test:coverage` get picked up automatically.
- Wired up three high-value Vitest targets: apps/mana/apps/web (main
frontend, ~590 tests), shared-ui (Svelte component library), and
shared-storage (S3 client). Each emits lcov.info + coverage-summary.json
+ browsable HTML.
- apps/mana/apps/web `"test"` was running in watch mode (just `vitest`),
which hangs under turbo orchestration — changed to `vitest run` and
added `test:watch` for the interactive case.
- CI uploads coverage artifacts (14-day retention) regardless of whether
tests passed. `continue-on-error: true` replaces `|| true` so a failed
suite shows up as a warning annotation on the PR rather than being
invisible. Flip to a hard gate once main is green for a full week.
- Testing guideline documents the pattern + the template vitest config
+ the planned 80% threshold.
- ESLint flat-config `vitest.config.ts` ignore only matched at the root;
widened to `**/vitest.config.{ts,js,mjs}` so nested configs don't trip
the project-service parser.
Coverage baseline produced locally:
shared-storage: 91.37% lines (6 files, 123 tests)
shared-ui: 2.87% lines (mostly Svelte components, untested)
apps/mana/web: 9/59 test files fail — pre-existing, not regression
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The JWT already carried a `tier` claim but nothing on the server read it
— AuthGate enforcement was client-only, so a valid JWT could hit paid
LLM/research endpoints regardless of the user's access tier.
- shared-hono authMiddleware now extracts `tier` into `c.userTier`,
defaulting unknown/missing claims to `public` (never silently grants
higher access).
- New `requireTier(minTier)` middleware + `hasTier`/`getTierLevel`
helpers. Tier hierarchy (guest < public < beta < alpha < founder) is
mirrored locally to avoid pulling the Svelte-facing shared-branding
package into Bun services.
- Applied `requireTier('beta')` as defense-in-depth on resource-heavy
apps/api modules (chat, context, food, guides, news-research, picture,
plants, research, traces, who) and the MCP endpoint. Pure CRUD modules
stay auth-only — access there is gated by ownership, not tier.
- DEV_BYPASS_AUTH now injects `userTier` (defaults to founder, override
via DEV_USER_TIER).
- Authentication guideline documents the pattern + test suite covers
hierarchy, passes-at-minimum, and rejection paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `app-registry/types.ts` now includes `tips` in the inline help shape,
matching `ModuleHelp` and what `AppPage.svelte` actually renders.
Drops 3 recurring type errors.
- `event-scout` template's `{ kind: 'daily' }` cadence now carries the
required `atHour` / `atMinute` fields (daily 08:00). Drops the 4th
type error — svelte-check is clean.
- `apps/mana/CLAUDE.md` gains a "Scene Scope" section documenting the
pattern: wire `filterBySceneScopeBatch` in the query AND render
`<ScopeEmptyState>` from the empty branch, so users always see why
the list is empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add discover_events (auto) and suggest_event (propose) to shared-ai
tool catalog. discover_events reads the discovery feed, suggest_event
creates a proposal to save a discovered event to the user's calendar.
- Add Event-Scout agent template with daily "Events der Woche" mission.
Policy: discover_events=auto, suggest_event=propose, all else denied.
- Add frontend tool implementations in events/tools.ts — discover_events
calls the feed API, suggest_event delegates to discoveryStore.saveEvent.
- Add feedback.ts — computes implicit user profile from save/dismiss
history (category affinity + source quality as 0–2x weight multipliers).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Branding: research-lab registered in @mana/shared-branding with requiredTier: 'beta' + a custom flask-on-purple icon, so guest/public users are filtered out of the workbench picker.
- Backend: compare routes now return resultId alongside each CompareEntry so the frontend can wire ratings to the eval_results rows in research.*.
- Frontend: click-to-rate stars in CompareColumn (persists via POST /v1/runs/:runId/results/:resultId/rate), recent-run list rows are now buttons that navigate to /research-lab/runs/[id], and the detail route reconstructs CompareEntry shapes from eval_results + reuses CompareColumn for a full read-only view of any past run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move getUserMessage() to the base LlmError class so every error type
gets a German explanation with a clickable settings deep-link:
- TierTooLowError: "Kein KI-Modell aktiviert. Mindestens X benötigt."
- ProviderBlockedError: "… hat die Anfrage blockiert (Inhaltsfilter)."
- BackendUnreachableError: "… ist nicht erreichbar."
- EdgeLoadFailedError: "Browser-Modell konnte nicht geladen werden."
- Generic fallback: also includes the settings link now
The companion engine now catches LlmError (base class) instead of
only NoTierAvailableError, covering all failure modes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Error messages now include a clickable Markdown link
"KI-Einstellungen öffnen" that navigates to /?app=settings#ai-options,
which opens the settings panel in the workbench, switches to the AI
tab, and scrolls to the LLM options section.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Track skip reasons per tier in the orchestrator (no-consent,
no-backend, not-available, not-ready, runtime-error) and expose
them via NoTierAvailableError.getUserMessage() with actionable
German text pointing the user to the right settings page.
Before: "No tier could run task 'companion.chat' (attempted: cloud)"
After: "Cloud (Gemini): Cloud-Einwilligung fehlt. Aktiviere sie
unter Einstellungen → KI."
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New Bun/Hono service on port 3068 that bundles many web-research providers
behind a unified interface for side-by-side comparison. All eval runs
persist in research.* (mana_platform) so quality can be reviewed later.
Providers (Phase 1+2):
search: searxng, duckduckgo, brave, tavily, exa, serper
extract: readability (via mana-search), jina-reader, firecrawl
Endpoints:
POST /v1/search, /v1/search/compare — single + fan-out
POST /v1/extract, /v1/extract/compare — single + fan-out
GET /v1/runs, /v1/runs/:id — history
POST /v1/runs/:run/results/:id/rate — manual eval
GET /v1/providers, /v1/providers/health — catalog + readiness
Auto-routing: when `provider` is omitted, queries are classified via regex
(fast path, 0ms) with optional mana-llm fallback, then routed to the first
available provider for that query type (news → tavily, academic → exa,
semantic → exa, etc.).
Credits: server-key calls go through mana-credits reserve → commit/refund
so failed provider calls don't charge the user. BYO-keys supported via
research.provider_configs (UI arrives in Phase 4).
Cache: Redis with graceful degradation (1h TTL for search, 24h for
extract). Pay-per-use APIs only — no subscription-gated providers.
Docs: docs/plans/mana-research-service.md + docs/reports/web-research-capabilities.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New module for managing wishes/gift ideas with lists, price targets,
product URLs, price history, and AI tools. Includes ListView with
filter tabs, inline list management, search, and DetailView with
notes and price history. Encrypted at rest (title, description, URLs,
notes). Registered in database v24, module-registry, crypto registry,
seed registry, tool init, and DnD type system.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
M1 skeleton for a new media-consumption module. Single-table design with
a `kind: 'book' | 'movie' | 'series' | 'comic'` discriminator and a
discriminated `details` union for kind-specific fields (pages / runtime /
episode tracker / issue count). Shared kern: status, rating, review,
favourites, times counter, completedAt — which enables cross-media
queries like a year-in-review.
Dexie migration v26 was already registered in module-registry.ts /
database.ts via the preceding wetter commit (62aac6dfd); this commit
adds the actual module code, encryption registry entry, app-icon,
MANA_APPS entry, Kreativität & Medien category row, and the module
plan at docs/plans/library-module.md.
Encrypted fields (via ENCRYPTION_REGISTRY):
title, originalTitle, creators, review, tags
Plaintext (intentional):
kind, status, year, rating, genres, completedAt, isFavorite, times,
externalIds, details — all needed for the tab filter, status chips,
Jahresrückblick range-scan, and progress UIs.
Product decisions (frozen in the plan):
- audiobooks = kind='book' with details.format='audio'
- manga = kind='comic' (no sub-discriminator)
- metadata lookup (M7) lands as an endpoint in apps/api, not a
standalone service
Guest seed ships one example per kind (Dune, Arrival, Severance, Saga)
so first-run users immediately see what the module does.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New module providing weather data for the DACH region via three sources:
- Open-Meteo (DWD ICON-D2 model) for current conditions and 7-day forecast
- DWD warnings endpoint for severe weather alerts
- Rainbow.ai / Open-Meteo fallback for minute-level rain nowcast
Includes API proxy with in-memory caching, Svelte 5 UI with location
picker, hourly/daily forecast, alert cards, and precipitation bar chart.
Two AI tools (get_weather, get_rain_forecast) enable the companion to
answer weather questions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Six P2 items from the AI Workbench audit:
#7 Prompt ↔ loop budget sync:
System prompt now says "1 bis 5 Schritte pro Planungsrunde, bis zu 5
Planungsrunden" — matches MAX_REASONING_LOOP_ITERATIONS. Cross-ref
comment added to runner.ts.
#9 SceneHeader: useAgents() → useAgent(id):
Only loads the single bound agent instead of the full agent list.
Eliminates unnecessary Dexie churn on every scene header render.
#10 Unified scope filter:
New scope-filter.ts with filterByScopeTagMap() (batch, sync) and
filterByScopeAsync() (per-record). Both scope-context.ts (AI) and
scene-scope.svelte.ts (UI) now import from the shared module —
zero duplicated filter logic.
#11 Research dedup:
Research input ID changed from `news-research-${Date.now()}` to
`news-research-${mission.id}` — re-runs overwrite instead of
appending duplicates.
#12 Kontext injection policy clarified:
loadAgentKontextAsResolvedInput no longer falls back to the global
singleton. Comment + code aligned: kontext injection is explicit
(via input picker), not auto. Dead loadKontextAsResolvedInput
kept for potential future opt-in auto-inject feature.
Audit doc updated with all items marked DONE.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Four P1 fixes from the AI Workbench audit:
#3 N+1 junction queries → batch lookups:
- TagLinkOps gains getTagIdsForMany(entityIds) — single
where(field).anyOf(ids).toArray() instead of N calls.
- filterBySceneScopeBatch() uses a pre-fetched Map<id, tagId[]>.
- All 4 module queries (notes, todo, contacts, calendar) migrated.
- 500 notes now = 2 Dexie queries (records + junctions) instead of 501.
#4 Vault-locked detection in readLocalNote:
- Catches VaultLockedError from decryptRecords.
- Throws descriptive "Vault ist gesperrt" instead of returning null.
- Tools surface it as a clear error to the planner ("bitte Vault
entsperren") instead of "Notiz nicht gefunden".
#5 Debug log hardening:
- Resolved-input content truncated to 500 chars before storage.
- Time-based purge: entries older than 7 days auto-deleted.
- Reduces privacy exposure if device is stolen/profile synced.
#6 Timeout 90s → 180s:
- 5 LLM calls on slow models (Ollama/GPU) regularly hit 90s.
- 180s gives comfortable headroom for the reasoning loop.
Audit doc updated with status markers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>