- packages/shared-branding/onboarding-templates.ts:
* 7 templates: Alltag / Arbeit / Health / Sport / Lernen / Entdecken
/ Erinnern — each with a phosphor icon name, German name/desc and
an ordered moduleIds list
* resolveModulesForTemplates() — deduplicates the union of selected
templates' modules (priority-ordered) and caps at 8 (2×4 grid)
- packages/shared-branding/onboarding-templates.spec.ts: 10 tests
covering order preservation, dedup-across-templates, cap honouring,
unknown-id tolerance
- /onboarding/templates/+page.svelte:
* Multi-select grid of 7 tiles (checkmark + primary border when on)
* Finish handler: runs resolveModulesForTemplates → creates a new
"Zuhause" scene with those apps → onboardingStatus.markComplete()
→ navigates to /
* Skip still marks complete (no scene — user lands on DEFAULT_HOME_APPS)
* Prefills selection from onboardingFlow store so back-nav is stable
With this, the 3-screen flow runs end-to-end for a new user:
signup → /onboarding/name → /look → /templates → / with a curated
home scene.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
83 new tests across 5 files — pure-logic, fast, run on every
push. Caught one real bug + motivated one small refactor.
Coverage:
- apps/mana/.../website/constants.test.ts (8): isValidSlug + RESERVED_SLUGS
+ isValidPath. Caught the 1-char-slug bug (regex allowed length 1;
UI + plan say min 2). Fixed the regex in both the webapp and the
mirrored server list.
- apps/mana/.../website/publish.test.ts extended (8 total): adds
self-parent cycle, 3-level nesting, all-orphans, empty-input cases
on top of the original determinism + orphan-drop tests.
- apps/mana/.../website/templates.test.ts (7): parameterised over each
of the 4 bundled templates — clone produces fresh UUIDs, page +
block counts match, navConfig populated. Plus unknown-template and
duplicate-slug rejection. Container-nesting is punted to the smoke
test (none of the bundled templates use columns yet).
- packages/website-blocks/src/schemas.test.ts (38): every block
(11) + sanity-checks (defaults satisfy own schema, enum + length
bounds, required fields). Pure Zod — no Svelte runtime needed.
- packages/website-blocks/src/themes/themes.test.ts (12): preset
parity, resolveTheme overrides, themeCssVars output format +
heading-font fallback.
- apps/api/src/modules/website/reserved-slugs.test.ts (10): mirror of
the client tests for the server SSOT, plus new hostname validation
cases (.mana.how reservation, length, malformed edges).
Refactor:
- apps/api/src/modules/website/reserved-slugs.ts now owns
isValidHostname + RESERVED_HOSTNAMES. domains.ts imports them.
Pure functions live next to the other pure validators; easier to
test + share.
All 83 new tests green. Web-app svelte-check + apps/api type-check
both clean. Existing publish.test.ts / website-blocks tests still
pass (the monorepo-wide count is now well above 83 — these are
the new ones from this commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exposes runSubAgent() as a tool the planner LLM can call natively,
matching Claude Code's `Task` tool shape: { subagent_type, description,
prompt } -> single-string summary.
New exports from @mana/shared-ai:
- TASK_TOOL_NAME = 'task'
- TASK_TOOL_SCHEMA — ToolSchema ready to drop into a runPlannerLoop
`tools` array. subagent_type enum = research|plan|general;
description+prompt required; defaultPolicy: 'auto' (control-flow,
not a user-data write).
- createTaskToolHandler(opts) — factory returning:
- handle(call): structured ToolResult with the sub-agent's
summary as message + data {subAgentType, toolsCalled,
rounds, stopReason, usage}
- cumulativeUsage(): rolled-up TokenUsage across all sub-agent
invocations — parent budget accounting reads from here
- invocationCount(): metric-ready counter
Why not in mana-tool-registry: `task` is a loop-internal control-flow
primitive, not a user-data operation. Registry is for habits/notes/etc.
where MCP exposure and space-scoping matter. task never touches mana-
sync and never crosses the MCP boundary.
Recursion guard is defense-in-depth: the primitive throws
SubAgentRecursionError, this handler catches parentDepth >=
MAX_SUB_AGENT_DEPTH up front and returns a structured ToolResult
instead so the LLM sees it as regular tool-feedback.
Exceptions from the sub-agent (provider down, network) get wrapped
as `{ success: false, message: 'Sub-agent failed: ...' }`. The parent
loop's round continues.
14 new tests covering schema shape, recursion rejection, argument
validation (4 cases), happy path with tool dispatch, cumulative
usage tracking across multiple invocations, exception wrapping,
and parent-dispatcher routing.
107 shared-ai tests green total (was 93).
M3.3 consumer wiring follows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M5 of docs/plans/wardrobe-module.md — exposes the Wardrobe feature
through the shared tool-registry so MCP clients (Claude Desktop)
and the mana-ai mission runner can browse, compose, and try on
outfits alongside the built-in UI. Follows the pattern M5 of the
me-images plan established in packages/mana-tool-registry/src/
modules/me.ts — encrypted reads via mana-sync pull + client-side
filter on `row.spaceId === ctx.spaceId`, writes via pushInsert
with encryptRecordFields, HTTP proxy for the try-on endpoint.
Four tools in packages/mana-tool-registry/src/modules/wardrobe.ts:
- wardrobe.listGarments(category?, tags?, limit?) — read. Pulls
wardrobeGarments from mana-sync, filters to the active space,
decrypts name/brand/color/size/material/tags/notes, applies
optional category + intersection-tag filters, caps at 200 rows
(50 default). Archived + soft-deleted items excluded.
- wardrobe.listOutfits(occasion?, favoriteOnly?, limit?) — read.
Same shape, filters by occasion (closed enum, plaintext —
unencrypted filter) and favorite. garmentIds arrive plaintext
so the agent can immediately resolve them via listGarments when
it needs more than ids.
- wardrobe.createOutfit({ name, garmentIds, occasion?, tags?,
description? }) — write. Encrypts name/description/tags, pushes
an insert tagged with ctx.spaceId. No cross-space validation of
the garmentIds — the calling agent is expected to have called
listGarments first; dangling refs surface visually in the UI
rather than as a hard server error.
- wardrobe.tryOn({ outfitId, prompt?, accessoryOnly?, quality? }) —
write (consumes credits). Biggest tool of the set: pulls the
outfit, its garments, and the caller's meImages in three
separate mana-sync pulls, resolves the primary face-ref +
body-ref, auto-detects accessoryOnly from garment categories
(FACE_ONLY_CATEGORIES: accessory/glasses/jewelry/hat), composes
refs respecting the 8-slot server cap, composes a default DE
prompt from the outfit name + occasion, and proxies to
/api/v1/picture/generate-with-reference with the user's JWT.
Returns the resulting image's URL + mediaId + prompt + model.
Deliberately does NOT persist a picture.images row or update
outfit.lastTryOn from the tool — those live on the client's
imagesStore / wardrobeOutfitsStore and doing them server-side
would race with a user who's also looking at the outfit page.
Agents use tryOn as a preview/inspection primitive; the user
commits from the UI.
Types: 'wardrobe' added to the ModuleId union. registerWardrobeTools
wired into registerAllModules — mana-mcp's createMcpServerForUser
iterates the registry and exposes any user-space tool automatically.
Credit model: quality defaults to 'medium' (10 credits per render),
same tarif as text-to-image generation. The agent pays for the
generation out of the calling user's credit balance via the
standard validateCredits/consumeCredits chain on the server endpoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New packages/shared-ai/src/planner/sub-agent.ts implementing the
"one level deep, fresh messages, restricted tools, single-string
return" sub-agent contract from Claude Code's KN5/I2A launcher.
Four invariants enforced at the primitive level:
1. FRESH messages[] — parent's history never leaks in. The sub-agent
only sees its own system prompt + the task description. Hundreds
of scanned files stay inside the sub-agent.
2. RESTRICTED tool-whitelist — parent's full catalog is filtered
per SubAgentType ('research' = auto-policy only, 'general' =
everything, 'plan' = auto-policy + 3-round cap). Custom filter
overrides the type default.
3. SINGLE RETURN VALUE — sub-agent returns summary:string for
the parent to render as task-tool-result. Individual tool calls
stay in rawResult for debug capture but never cross the boundary.
4. ONE LEVEL DEEP — MAX_SUB_AGENT_DEPTH = 1. parentDepth >= 1 throws
SubAgentRecursionError; the consumer task-tool handler will
also check, this is defense-in-depth.
Model is required (no default) — routing to a cheaper tier like the
compactor does is an explicit decision, not a sneaky default.
Belt-and-suspenders wrapper on onToolCall rejects any tool call
whose name isn't in the whitelist, even if the LLM fabricates one.
14 new tests covering recursion guard, tool filtering per type,
custom filter, whitelist rejection, fresh-messages isolation, usage
roll-up, default summary on max-rounds, type-specific system prompt,
system-prompt override, and end-to-end tool-call -> result -> summary.
93 shared-ai tests green total (was 79).
M3.2 (task tool in registry) and M3.3 (consumer wiring) follow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third encrypted module in @mana/tool-registry, brings the registry to
16 tools across 7 modules. Lets the Anna / Sofia / Maya personas
(whose moduleMix puts mood at 20–30 %) actually exercise their
daily-tracking routine when the runner ticks.
Three tools, all encrypted per the web-app registry
(moodEntries: entry<LocalMoodEntry>(['withWhom', 'notes'])):
- mood.log
Write a mood entry. `level` 1–10, `emotion` + `secondaryEmotions`
from the taxonomy copied verbatim from apps/mana/.../modules/mood/
types.ts (keep in sync if new emotions/activities get added). date
+ time default to server-clock now; personas logging
retrospectively pass them explicitly.
- mood.today
Return every entry for today (or `{ date }`) sorted by time.
Multiple entries per day are normal — the web app timelines them.
- mood.recent
Last N days (default 7), newest first. Useful for
self-reflection turns like "how has your week been?".
Scope decisions
Calendar was on the shortlist but dropped: `events` writes couple to
`timeBlocks` (a separate table/appId), so one tool call becomes two
sync pushes with a shared transaction concern — worth a careful
session, not a drive-by. Goals dropped because `companionGoals` is
owned by the Companion Brain, not a regular module, and has no clear
mana-sync appId convention. Both candidates for a focused follow-up.
Verified
- `pnpm run validate:all` green (crypto registry 202/202, encrypted-
tools audit 9/9 including the 3 new mood tools)
- type-check across tool-registry + mcp + runner green
- registerAllModules → 16 tools, 7 modules:
habits: create/list/update/archive
journal: add 🔐
me: listReferenceImages 🔐 / generateWithReference
mood: log 🔐 / today 🔐 / recent 🔐
notes: create 🔐 / search 🔐
spaces: list
todo: create 🔐 / list 🔐 / complete
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M1 of docs/plans/wardrobe-module.md — pure data layer + backend plumbing,
zero UI (that's M2). A user can now hold a digital wardrobe per space:
brand merch, club Trikots, family Kleiderschrank, team Kostüme, practice
Dresscode, and personal closet all live as separate pools under the same
Dexie tables, space-scoped like tags/scenes/agents after Phase 2c.
Data model — two tables, no join:
- wardrobeGarments (Dexie v41): single clothing items / accessories.
Indexed on `category` + `createdAt` + `isArchived`. Encrypted:
name/brand/color/size/material/tags/notes. Plaintext: category,
mediaIds, counters, timestamps — all indexed or structural.
`mediaIds[0]` is the primary photo used for try-on; additional
ids are alternate views (back, detail) for M7.
- wardrobeOutfits (Dexie v41): named compositions referencing
garment ids. Encrypted: name/description/tags. Plaintext:
garmentIds (FK array), occasion (closed enum — useful for
undecrypted filtering), season, booleans, lastTryOn snapshot.
- picture.images gains `wardrobeOutfitId?: string | null` as a
plaintext back-reference. Try-on results land in the Picture
gallery like any other generation; the outfit detail view
queries them via this id rather than maintaining a third table.
Space scope:
- `wardrobe` added to all five explicit allowlists in shared-types/
spaces.ts (personal is wildcard, no edit needed). Each space type
gets a one-line comment explaining the real-world use case.
- App registry: `wardrobe` entry in shared-branding/mana-apps.ts
with a rose→fuchsia gradient icon (T-shirt on hanger silhouette),
color #e11d48, tier 'beta', status 'beta'.
- Module registry: wardrobeModuleConfig imported + appended to
MODULE_CONFIGS so SYNC_APP_MAP picks it up automatically.
Backend:
- MAX_REFERENCE_IMAGES bumped 4 → 8 in picture/generate-with-
reference (plus the client-side default in ReferenceImagePicker).
Justified with a comment: face + body + top + bottom + shoes +
outerwear + 2 accessories = 8. Cost doesn't scale with ref count
(OpenAI bills per output), so the bump is a pure capability
expansion with no credit-side risk.
- New POST /api/v1/wardrobe/garments/upload wraps uploadImageToMedia
with app='wardrobe'. Registered under /api/v1/wardrobe in index.ts.
Pattern 1:1 with the profile/me-images/upload endpoint; tier-gating
falls out of wardrobe NOT being in RESOURCE_MODULES (tier='guest'
works — consistent with picture's plain CRUD).
Stores emit domain events (WardrobeGarmentAdded, WardrobeOutfitCreated,
WardrobeOutfitTryOn, etc.) so later mana-ai missions can observe
activity without polling.
No UI in this commit. M2 (Garments-Grundlayer) wires the route + grid
+ upload-zone; M3 the Outfit composer; M4 the Try-On integration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
compactHistory() now defaults to DEFAULT_COMPACT_MODEL =
'google/gemini-2.5-flash-lite' when the caller doesn't override. Lite
is ~3–5x cheaper than gemini-2.5-flash with near-identical
summarisation quality — summarisation doesn't need the same tier as
reasoning + tool-calling, and the compactor fires exactly when token
spend is highest, so the cheaper route saves exactly where it matters.
CompactHistoryOptions.model is now optional. All three consumers
(mana-ai tick, webapp Companion, webapp Mission runner) drop their
explicit gemini-2.5-flash override and let the default apply.
This is the pragmatic M2.5: no mana-llm changes. The "tier" abstraction
(X-Model-Tier header, env-routed aliases) from the Claude-Code report
makes sense only once multiple utility tasks need cheaper routing —
topic-detection, classification, command-injection checks. Today only
the compactor wants it, and a model constant is the simplest contract
that works.
2 new tests (default applied + override honoured). 79 shared-ai tests
green, all three consumers type-check clean. One pre-existing unrelated
type error in apps/mana/apps/web/src/lib/modules/wardrobe/queries.ts
(not touched by this commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flips `meImages` out of USER_LEVEL_TABLES so it lives under the same
tenancy model as every other data table (tags, scenes, tasks, …).
Precursor to the Wardrobe module, which is space-scoped across all
six space types — leaving meImages user-global would leave an
inconsistency where the Wardrobe catalog is per-space but its
reference input is cross-space, plus a latent privacy leak in shared
spaces (agents in a brand-space would see the owner's entire pool).
Plan: docs/plans/me-images-space-scope-migration.md.
Key decisions:
- Strict scope, no cross-space fallback. Switching into a brand-space
with no uploaded face shows an empty state and links back to
/profile/me-images; it does not quietly reach into the personal-
space pool. Keeps the mental model clean.
- auth.users.image remains pinned to personal-space primary-avatar.
Only a primary change inside personal space triggers the Better
Auth sync; brand/club/family/team/practice primaries stay local.
- Single Dexie v40 upgrade: stamps `spaceId=_personal:<uid>`
sentinel, `authorId=<uid>`, `visibility='space'` on every existing
row and drops the legacy `userId` column. Dexie upgrades block app
startup, so by the time the new code's scopedForModule reads run,
every row is already space-stamped. reconcileSentinels() on the
next active-space bootstrap rewrites `_personal:<uid>` to the real
personal-space id, same path v28 used.
- Legacy-avatar migration (M2.5) now pins its row to
`_personal:<uid>` explicitly — the legacy avatar is the user's
global SSO identity and belongs in the personal space even if the
migration happens to fire while the user is in a brand space.
Code changes:
- types.ts: LocalMeImage gains spaceId/authorId/visibility (all
optional — stamped by hook). Public MeImage exposes spaceId for
queries that want to branch on space type.
- database.ts: meImages out of USER_LEVEL_TABLES; new v40 upgrade
block that stamps sentinels + drops userId in one pass.
- queries.ts: all four hooks (useAllMeImages, useMeImagesByKind,
useReferenceImages, useImageByPrimary) read via scopedForModule.
Scope-switch triggers automatic re-render via the existing
scopedTable filter path.
- stores/me-images.svelte.ts: setPrimaryInTx uses scopedForModule so
a setPrimary in Brand-space never clears Personal-space's holder.
syncAvatarToAuth gates on activeSpace.type==='personal' so non-
personal primary changes don't leak into Better Auth.
createMeImage accepts optional spaceId override — the legacy-
avatar migration uses it, regular uploads let the hook stamp the
active space.
- migration/legacy-avatar.ts: explicitly passes
spaceId=_personal:<uid> to pin the legacy row into personal space.
- MeImagesView.svelte: subtle badge in the intro card shows the
active space ("Persönlich" for personal, space name otherwise) so
users notice when the pool changes on space switch.
- packages/mana-tool-registry/src/modules/me.ts: me.listReferenceImages
filters pulled rows by row.spaceId === ctx.spaceId. mana-sync
returns all spaces the user belongs to; the tool only wants the
active space's subset.
No schema/index change on meImages (non-indexed fields, pool size
small enough for in-memory scopedTable filter). If perf matters
later, adding [spaceId+kind] is a 5-minute follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the loop on M2: when the compactor fires, the LLM needs to know
it's now seeing a <compact-summary> instead of raw turns so it
doesn't waste a turn asking about lost details or re-executing tools
whose responses are gone.
shared-ai:
- LoopState grows `compactionsDone: number` (cap-1 by current loop
policy, but shape kept as count for future multi-compact cycles).
- runPlannerLoop populates it on each reminder-channel call. New
loop test asserts [0, 1] sequence: round 1 before compaction,
round 2 after.
mana-ai:
- New producer `compactedReminder` — fires severity=info when
compactionsDone >= 1, wrapped in a German one-liner ("frag nicht
nach verlorenen Details").
- Injected FIRST in buildReminderChannel so the LLM frames the rest
of the round with "I'm looking at a summary" context. Metric
surface stays `{producer='compacted', severity='info'}`.
4 new reminder tests (3 pure producer + 1 composition-ordering) +
1 loop-wiring test. 77 shared-ai, 20 reminders.test.ts — green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before: guests had to open the user-menu dropdown to find the login
button. Now the login CTA renders as a visible primary pill immediately
right of the (icon-only) user-menu trigger, so signing in is one click.
Removed the duplicate Anmelden entry from userMenuBarItems — theme,
mode toggle, and language stay in the bar for signed-out users.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PlannerLoopInput grows an optional compactor:
compactor?: {
maxContextTokens: number;
threshold?: number; // default 0.92, matches Claude Code wU2
compact: (messages) => Promise<{ messages, compactedTurns }>;
}
Before each LLM call the loop checks whether promptTokens+completion
has crossed threshold × maxContextTokens. If yes AND we haven't
compacted this run yet, the callback runs, its returned messages
REPLACE the live history, and compactionsDone flips to 1 so a
runaway tool can't re-trigger.
Design choices:
- Fires at most ONCE per loop run. If the fresh (compacted)
history hits the threshold again in the same run, the LLM
round budget will hit first; better to terminate than to
recursively compact a summary.
- No reminder emitted automatically — the caller can wire
that via reminderChannel by reading compactionsDone from
LoopState (next PR; compactionsDone isn't exposed yet to
keep the state surface small).
- compactor callback is injectable, not hardcoded to
compactHistory() from compact.ts. Lets mana-ai route the
compactor LLM call to a cheaper model (Haiku) without
changing the loop.
- Zero maxContextTokens → skip silently (same contract as
shouldCompact()).
Also cleaned up the isParallelSafe non-null-assertion warning by
hoisting the predicate to a local with proper narrowing.
5 new loop tests: below-threshold no-op, single-fire replacement,
once-per-run idempotency, zero-cap bail, no-op when compactor
returns 0 turns. 76 shared-ai tests total, green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Claude-Code wU2 pattern: when token usage hits ~92% of the provider's
context budget, fold all pre-tail turns into a single structured summary
(Goal / Decisions / Tools Called / Current Progress) so subsequent
rounds see a synopsis instead of the raw log.
This commit ships ONLY the primitive. Wiring it into runPlannerLoop
(auto-trigger before the next LLM call when shouldCompact() fires)
is M2.2 so the surface stays small and testable.
New exports from @mana/shared-ai:
- shouldCompact(totalTokens, maxContextTokens, threshold?)
→ boolean; DEFAULT_COMPACT_THRESHOLD = 0.92, matching Claude Code.
Bails safely when maxContextTokens is missing (local models often
don't report usage).
- compactHistory(messages, { llm, model, keepRecent?, temperature? })
→ { messages, summary, compactedTurns, usage? }
Preserves: [0]=system, [1]=first user, [last N]=recent turns
(default 4). Everything between gets sent through the compact
agent with COMPACT_SYSTEM_PROMPT — a fixed 4-section Markdown
schema. Temperature default 0.2 because we want summarisation,
not creativity.
- parseCompactSummary / renderCompactSummary — round-trip helpers.
Parser is tolerant (missing sections → empty string) so a partial
compaction still produces a usable summary.
The summary replaces the middle as a single role='assistant' message
wrapped in <compact-summary> tags. Assistant role (not system) because
some providers reject arbitrary system messages deep in history.
Tests: 17 new across the 4 exports (trigger logic, Markdown round-trip,
structural preservation of anchors + tail, usage passthrough, custom
keepRecent). All 71 shared-ai tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two things:
1. AI tools (9) in the website module — writes go through the standard
proposal flow, reads run auto during planning.
- shared-ai/src/tools/schemas.ts: AI_TOOL_CATALOG entries with
defaultPolicy propose/auto.
- webapp modules/website/tools.ts: execute functions wired to the
existing stores. ModuleTool[] registered in data/tools/init.ts.
- Propose: create_website, apply_website_template, create_website_page,
add_website_block, update_website_block, publish_website
- Auto: list_websites, list_website_pages, list_website_blocks
Server-side mana-tool-registry integration (mana-mcp, mana-ai) is
a M5.x follow-up — webapp flow unblocks the missions-based use case.
2. Starter templates — clone into a fresh site with new UUIDs.
- templates/types.ts: SiteTemplate shape with localId / parentLocalId
so container→child references survive the clone.
- 4 templates: portfolio (4 pages), personal-linktree (1 page, 6 CTAs),
event (3 pages incl. RSVP form), blank (1 empty page). Deferred:
smb-corporate + product-landing (need team/pricing/testimonials
blocks, M6+).
- sitesStore.applyTemplate: walks template, bulk-inserts new rows,
remaps parent refs. Sets navConfig items from template pages.
- TemplatePicker component + /website/new route. Replaces the old
quick-create modal; ListView now links to /new. AppRegistry
context-menu action points there too.
AiProposalInbox integration deferred — the component doesn't exist in
the webapp yet (the plan mentions it aspirationally). defaultPolicy
'propose' is already set so writes stage correctly once the UI catches
up.
Validation:
- pnpm run validate:all: 6/6 gates green
- pnpm run check (web): 0 errors, 0 warnings
- apps/api + packages/shared-ai type-check: green
Plan: docs/plans/website-builder.md (M5 shipped)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends LoopState with a sliding window of the last N ExecutedCalls
(oldest-first), capped at LOOP_STATE_RECENT_CALLS_WINDOW = 5. The loop
maintains the window automatically; reminderChannel producers read it
without touching internal state.
This activates retryLoopReminder which was shape-only in faa472be9.
The guard now fires end-to-end: when round >= 3 and the tail-2 calls
both returned success:false, the LLM sees a "stop retrying, write a
summary instead" <reminder> on the next turn. The tail-2 check rather
than window-wide is deliberate — a flaky run with intermittent success
(F, F, F, OK, F) is not a retry loop, just flaky tools.
Why window=5: retry loops usually manifest within 2-3 consecutive
rounds; a 5-deep window gives room for burst-detection and
stale-tool heuristics without bloating the reminder channel. Cap
keeps the reminder producers O(5) regardless of loop length.
Tests: 3 new (sliding-window cap + slide + order in shared-ai, retry
composition + budget+retry chain + tail-only heuristic in mana-ai).
Total agent-loop tests now 74 across both packages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes M5 of docs/plans/me-images-and-reference-generation.md —
exposes the meImages feature through the shared tool-registry so MCP
clients (Claude Desktop) and the mana-ai mission runner can drive it
alongside the built-in webapp UI.
Two tools in packages/mana-tool-registry/src/modules/me.ts:
- me.listReferenceImages(kind?) — scope: user-space, read. Pulls the
user's meImages rows from mana-sync (app='profile'), filters to
usage.aiReference=true and soft-live records, decrypts the `label`
and `tags` fields with the caller's master key (same pattern as
notes.search). Returns mediaIds + kind + primary-slot info so a
persona can pick references intelligently. ZK users will see this
fail at getMasterKey() — correct, because the label is truly
unrecoverable server-side for them.
- me.generateWithReference({prompt, referenceMediaIds, quality,
size, n}) — scope: user-space, write. Thin proxy over the M3
endpoint POST /api/v1/picture/generate-with-reference in apps/api:
forwards the JWT, lets apps/api re-verify ownership, and returns
the generated images' mediaIds + URLs. Credits are consumed at
the same 3/10/25 tarif as text-to-image, so a persona plan pass
should gate this behind explicit budget rather than leaving it on
auto-policy.
Registered in modules/index.ts + adds 'me' to the ModuleId union in
types.ts. No other wiring needed — mana-mcp's createMcpServerForUser
iterates the registry and exposes any user-space tool, so both tools
become available to Claude Desktop immediately on next deploy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two new block types and the server-side infrastructure for
untrusted input + cross-module data embedding.
Forms:
- packages/website-blocks/src/form: declarative fields (text, email,
tel, url, textarea, number) with required / maxLength / placeholder
per field. Honeypot hidden input in the renderer; public-mode POST
to a same-origin SvelteKit proxy that forwards to mana-api.
- apps/api: website.submissions table (schema.ts + 0001_submissions.sql)
+ POST /public/submit/:siteSlug/:blockId. Loads the current published
snapshot, finds the form block, validates payload against its
declared fields (trim, type check, length cap), rejects honeypot
submissions silently, rate-limits per IP (10 / 5 min) in-memory.
Unknown keys are dropped — clients can only submit declared fields.
- Owner-facing: GET/DELETE /sites/:id/submissions + SubmissionsView
component + /(app)/website/[siteId]/submissions route. Shows
incoming submissions with status pill + payload preview + delete.
- apps/mana/.../routes/s/[siteSlug]/__submit/[blockId]/+server.ts:
same-origin proxy so form posts don't trigger CORS and IP / user-
agent headers are forwarded via SvelteKit's trusted getClientAddress.
M4 first-pass does NOT wire target-module delivery (contacts / notify).
Submissions stay in the inbox until owner-side tool handlers land
(M4.x). `target` enum is intentionally `['inbox']` only for now.
moduleEmbed:
- packages/website-blocks/src/moduleEmbed: source dropdown
(picture.board | library.entries), max-items, layout (grid | list),
optional filter object. The `resolved` field on props is populated at
publish time by the editor-side resolver — public renderer reads it
directly, no Dexie / API round-trip needed.
- apps/mana/.../website/embeds.ts: per-source resolvers. picture.board
enforces `isPublic=true`; library.entries respects filter.isFavorite
/ kind / status so owners can expose a subset (e.g. "my favorites").
- buildSnapshot() walks the tree after assembly and fills in
block.props.resolved for every moduleEmbed. Publish slower, public
visits fast. No cross-service call at render time.
Validation:
- pnpm run validate:all: 6/6 gates green
- pnpm run check (web): 0 errors, 0 warnings
- apps/api type-check: green
Apply Postgres with:
psql "$DATABASE_URL" -f apps/api/drizzle/website/0001_submissions.sql
Plan: docs/plans/website-builder.md (M4 shipped)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expands the builder from 3 M1 blocks to 8. Containers (columns) and
media blocks (image, gallery) are the structural additions; cta and faq
round out the content coverage.
packages/website-blocks:
- image, cta, faq, columns (container), gallery — each with Zod schema,
renderer (mode-aware for edit/preview/public), and fallback inspector.
- Block type extended with optional `children` + `renderChild` snippet
so containers render their children through the same chrome the
outer renderer provides (click-to-select, public-path tagging).
- themes/: 3 presets (classic light, modern dark, warm) with
`resolveTheme` + `themeCssVars` helpers. Public layout now emits
CSS vars via `style=` on the root; block components read
`var(--wb-primary)` / `var(--wb-bg)` / `var(--wb-fg)` / etc.
- Registry updated; new exports + `./themes` subpath export.
apps/mana/apps/web/src/lib/modules/website:
- upload.ts: multipart POST to mana-media with `app=website` scope,
returns { mediaId, url }. 25 MB cap, non-image rejection client-side.
- components/ImageInspector + GalleryInspector: app-side overrides
wired to upload. Registered via `CUSTOM_INSPECTORS` in BlockInspector
so block.type → app-side inspector, fallback to registry otherwise.
- components/SiteSettingsDialog: theme preset picker + color overrides
for primary/bg/fg + footer text. Mounted from a ⚙ button in the
editor's left pane.
- components/BlockRenderer: rebuilt around a byParent map + recursive
`renderBlock` snippet so container blocks can render their children
through the same click-to-select wrapper as top-level blocks.
- routes/s/[siteSlug]: rename `[[...path]]` → `[...path]` (SvelteKit
treats rest segments as optional automatically — double-bracket form
errored at sync time). +page.svelte renders snapshot trees
recursively so published pages match the editor.
apps/api: unchanged.
Validation:
- pnpm run validate:all: all 6 gates green
- pnpm run check (web): 0 errors, 0 warnings
- apps/api type-check: green
- website-blocks tsc: green
Plan: docs/plans/website-builder.md (M3 block shipped)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>