Commit graph

701 commits

Author SHA1 Message Date
Till JS
1198d01263 feat(onboarding): M4 — Screen 3 (Templates) + finish handler
- 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>
2026-04-23 23:03:00 +02:00
Till JS
f20ace0358 test(website): broad automated coverage across the builder surface
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>
2026-04-23 21:07:40 +02:00
Till JS
101af462a8 feat(shared-ai): LLM-facing task tool wrapper for runSubAgent (M3.2)
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>
2026-04-23 19:05:09 +02:00
Till JS
7e3f53f8a5 feat(tool-registry): wardrobe.* MCP tools — listGarments/listOutfits/createOutfit/tryOn (M5)
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>
2026-04-23 18:59:31 +02:00
Till JS
66b7e08df2 feat(shared-ai): runSubAgent() primitive — Claude-Code I2A pattern (M3.1)
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>
2026-04-23 18:59:05 +02:00
Till JS
4966ca69f0 feat(tool-registry): add mood module (log/today/recent)
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>
2026-04-23 18:39:00 +02:00
Till JS
4fc9d6c59c feat(wardrobe): module foundation — garments + outfits space-scoped data layer (M1)
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>
2026-04-23 18:27:37 +02:00
Till JS
f7536bc0b9 feat(shared-ai): route compactor to Haiku-tier model by default (M2.5)
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>
2026-04-23 18:26:50 +02:00
Till JS
cb9a9bb42e refactor(profile,tool-registry): flip meImages from user-scoped to space-scoped (v40)
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>
2026-04-23 18:09:57 +02:00
Till JS
72f7978ed4 feat(agent-loop): expose compactionsDone + compactedReminder producer
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>
2026-04-23 15:36:21 +02:00
Till JS
700861f682 feat(pill-nav): show Anmelden pill for guests next to menu icon
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>
2026-04-23 15:36:09 +02:00
Till JS
3d8214a147 feat(shared-ai): wire compactor into runPlannerLoop (M2.2)
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>
2026-04-23 15:25:35 +02:00
Till JS
13361eb083 feat(shared-ai): compactHistory() — context-window compactor primitive (M2.1)
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>
2026-04-23 15:21:10 +02:00
Till JS
13efae8cd2 feat(website): M5 — AI tools + starter templates
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>
2026-04-23 15:14:45 +02:00
Till JS
8f283726b1 feat(agent-loop): activate retryLoopReminder via LoopState.recentCalls
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>
2026-04-23 15:02:40 +02:00
Till JS
fc635f9830 feat(tool-registry): me.listReferenceImages + me.generateWithReference (M5)
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>
2026-04-23 14:43:56 +02:00
Till JS
57be0f61b1 feat(website): M4 — forms + moduleEmbed
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>
2026-04-23 14:36:52 +02:00
Till JS
7a4f8894e1 feat(website): M3 — 5 more blocks, containers, upload, themes
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>
2026-04-23 14:27:49 +02:00
Till JS
54a12ffd5c feat(webapp): wire isParallelSafe in Companion chat + Mission runner
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>
2026-04-23 14:11:24 +02:00
Till JS
faa472be91 feat(mana-ai): first live reminder producers — token budget + retry-loop
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>
2026-04-23 14:00:04 +02:00
Till JS
e5d230e599 feat(agent-loop): M1 — policy gate + reminder channel + parallel reads
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>
2026-04-23 13:56:40 +02:00
Till JS
16c8818338 feat(mcp): M1+M1.5 MCP gateway + tool-registry + shared-crypto
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>
2026-04-23 13:18:35 +02:00
Till JS
f10a95e842 feat(mana-research): add Gemini 3.1 Pro Deep Research async providers
- 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>
2026-04-22 17:55:30 +02:00
Till JS
52af8c0cec refactor(theming): migrate who semantic colours to theme tokens
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>
2026-04-22 17:19:53 +02:00
Till JS
430aa30cbf refactor(theming): re-apply theme validator suite after parallel rebase
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>
2026-04-22 17:07:48 +02:00
Till JS
8a991f7c39 feat(articles): M7 share-target + bookmarklet — save from anywhere
@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>
2026-04-21 19:03:33 +02:00
Till JS
177734a860 fix(tsconfig): unblock shared-types consumers
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>
2026-04-21 18:53:55 +02:00
Till JS
88eca8a759 feat(spaces): Spaces as workbench card + canonical /spaces route
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>
2026-04-21 18:53:03 +02:00
Till JS
5924f4fac3 feat(articles): M6 AI tools — list / save / archive / tag / highlight
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>
2026-04-21 18:46:13 +02:00
Till JS
12be75e6a6 fix(broadcast): track route paths + shared-branding tsconfig
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>
2026-04-21 18:30:47 +02:00
Till JS
9d6a5a53a8 feat(apps): register agents + timeline as MANA_APPS + broadcast URL fix + members page chrome
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>
2026-04-21 16:32:41 +02:00
Till JS
3357e88a1c feat(articles): new read-it-later module — save / read / highlight
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>
2026-04-21 16:20:23 +02:00
Till JS
75832faef7 feat(broadcast): enhanced ListView + dashboard widget + AI tools
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>
2026-04-21 15:27:59 +02:00
Till JS
8e677c9066 feat(ai): add Library AI tools (create / rate / status / list)
Some checks failed
CI / Build mana-sync (push) Has been cancelled
CI / Build mana-notify (push) Has been cancelled
CI / Build mana-api-gateway (push) Has been cancelled
CI / Build mana-crawler (push) Has been cancelled
CI / Build mana-media (push) Has been cancelled
CI / Build mana-credits (push) Has been cancelled
CI / Build mana-web (push) Has been cancelled
CI / Build chat-backend (push) Has been cancelled
CI / Build chat-web (push) Has been cancelled
CI / Build todo-backend (push) Has been cancelled
CI / Build todo-web (push) Has been cancelled
CI / Build calendar-backend (push) Has been cancelled
CI / Build calendar-web (push) Has been cancelled
CI / Build clock-web (push) Has been cancelled
CI / Build contacts-backend (push) Has been cancelled
CI / Build contacts-web (push) Has been cancelled
CI / Build presi-web (push) Has been cancelled
CI / Build storage-backend (push) Has been cancelled
CI / Build storage-web (push) Has been cancelled
CI / Build telegram-stats-bot (push) Has been cancelled
CI / Build food-backend (push) Has been cancelled
CI / Build food-web (push) Has been cancelled
CI / Build skilltree-web (push) Has been cancelled
Docker Validate / Build calendar-web (push) Has been cancelled
Docker Validate / Build quotes-web (push) Has been cancelled
Docker Validate / Build todo-backend (push) Has been cancelled
Docker Validate / Build todo-web (push) Has been cancelled
Docker Validate / Build mana-auth (push) Has been cancelled
Docker Validate / Build mana-sync (push) Has been cancelled
Docker Validate / Build mana-media (push) Has been cancelled
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>
2026-04-20 21:23:19 +02:00
Till JS
fabd45bd87 feat(spaces): move Space-Switcher into the PillNav start slot
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>
2026-04-20 20:54:41 +02:00
Till JS
264c4c3087 feat(broadcast): M2 audience + editor + compose wizard
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>
2026-04-20 20:41:09 +02:00
Till JS
1f392c1ea6 feat(broadcast): M1 skeleton — module registration + empty ListView
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>
2026-04-20 20:16:35 +02:00
Till JS
79a6da3e2e feat(spaces): move access tier from user to space
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>
2026-04-20 20:10:06 +02:00
Till JS
a12928b7d8 fix(shared-types): add .ts extensions to re-exports for Node ESM resolvers
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>
2026-04-20 19:50:22 +02:00
Till JS
a4bc7d2ee3 feat(invoices): M8 AI tools — create/mark_paid/list/stats
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>
2026-04-20 18:22:20 +02:00
Till JS
0d613e1846 feat(ai): thread TokenUsage through runPlannerLoop → mana-ai budget
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>
2026-04-20 18:21:34 +02:00
Till JS
5b7564b3a4 test(ai): promote MockLlmClient to a shared @mana/shared-ai export
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>
2026-04-20 18:05:46 +02:00
Till JS
9f7d2f24b3 feat(companion): chat on runPlannerLoop with native function calling
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>
2026-04-20 16:45:33 +02:00
Till JS
166d6c6ffb feat(spaces): validate space metadata on Better Auth organization hooks
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>
2026-04-20 16:05:38 +02:00
Till JS
b249345174 feat(spaces): add space types + module allowlist as multi-tenancy foundation
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>
2026-04-20 15:57:57 +02:00
Till JS
0077752456 fix(type-check): clear the last five failures — monorepo type-check is now 76/76 green
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>
2026-04-20 15:53:07 +02:00
Till JS
4daca8970b feat(shared-ai): runPlannerLoop + compact system prompt for function calling
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>
2026-04-20 15:31:01 +02:00
Till JS
2cf89ce26a feat(invoices): M1 skeleton — module registration + empty ListView
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>
2026-04-20 15:28:09 +02:00
Till JS
4523ab24e3 feat(shared-ai): toolToFunctionSchema — catalog → OpenAI function-spec
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>
2026-04-20 15:24:36 +02:00
Till JS
c612a22371 fix(type-check): unblock two more pre-existing failures
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>
2026-04-20 15:20:08 +02:00