Two small follow-ups to the inline-edit header:
- Reduce the right padding from 2.5rem to 0.25rem so the scene name
sits visually adjacent to the first card. With the carousel's own
1rem gap between flex children the total breathing room is now
~20px instead of ~56px, which matches how hero titles usually
relate to the cards below / beside them.
- prettier-ignore on the <h1> and <p> element blocks. Prettier's
default Svelte formatting moved {scene.name} onto its own line
with indentation whitespace, which contenteditable renders
verbatim as literal leading / trailing spaces in the editor
content. Keep the mustache on the same line as the tag so the
text value is what the store actually holds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Foundation for the Multi-Agent Workbench roadmap
(docs/plans/multi-agent-workbench.md). Every event, record, and
sync_changes row now carries a principal identity + cached display
name in addition to the three-kind discriminator.
Shape change (source of truth in @mana/shared-ai):
Before: { kind: 'user' | 'ai' | 'system', ...kind-specific fields }
After: discriminated union on kind, with
- common: principalId, displayName
- 'user': principalId = userId
- 'ai': principalId = agentId + missionId/iterationId/rationale
- 'system': principalId = one of SYSTEM_* sentinel strings
('system:projection', 'system:mission-runner', etc.)
Key design calls (from the plan's Q&A):
- System sub-sources get distinct principalIds (not a shared 'system'
bucket) — lets Workbench filter + revert distinguish projection
writes from migration writes from server-iteration writes
- displayName cached on the record so renaming an agent doesn't
rewrite history
- normalizeActor() compat shim fills principalId/displayName on
legacy rows with 'legacy:*' sentinels so historical events never
crash the timeline
New exports:
- BaseActor / UserActor / AiActor / SystemActor (narrowed types)
- makeUserActor, makeAgentActor, makeSystemActor (factories with
typed return)
- SYSTEM_PROJECTION, SYSTEM_RULE, SYSTEM_MIGRATION, SYSTEM_STREAM,
SYSTEM_MISSION_RUNNER (principalId constants)
- LEGACY_USER_PRINCIPAL, LEGACY_AI_PRINCIPAL, LEGACY_SYSTEM_PRINCIPAL
- isUserActor / isFromMissionRunner predicates
Webapp:
- data/events/actor.ts now re-exports from shared-ai, keeps runtime
ambient-context (runAs, getCurrentActor) local
- bindDefaultUser(userId, displayName) lets the auth layer replace
the legacy placeholder with the real logged-in user actor at login
- Mission runner + server-iteration-staging stamp LEGACY_AI_PRINCIPAL
as the agentId placeholder — Phase 2 will thread the real agent
- Streaks projection uses makeSystemActor(SYSTEM_PROJECTION)
- All test fixtures migrated to factories
Service:
- mana-ai/db/iteration-writer.ts stamps makeSystemActor(
SYSTEM_MISSION_RUNNER) instead of the old { kind:'system',
source:'mission-runner' } shape. Phase 3 will switch this to an
agent actor per mission.
Tests: 26 shared-ai + 21 webapp vitest + 35 mana-ai — all green.
svelte-check: 0 errors, 0 warnings.
No behavior change; purely a type + shape upgrade. Old sync_changes
rows parse via the normalizeActor compat shim at read time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SceneHeader switches from a button-that-opens-a-dialog to inline
contenteditable fields. Click the title → caret appears, type →
Enter commits, Escape reverts, blur commits. Same for the
description except Enter is allowed (multi-line).
- contenteditable="plaintext-only" keeps pasted rich text from
leaking styles into the store
- onfocus selects the existing text so the first keystroke
replaces rather than appends (matches the expected rename feel)
- :empty-based ::before placeholder shows "Beschreibung
hinzufügen…" on blank descriptions, including while focused
(slightly dimmer) so the edit target is still visible before
the first character
- Empty name on blur reverts to the previous value (empty scene
names aren't a valid state the rest of the UI handles)
- Empty description commits as null, matching the store's
setSceneDescription contract
handleEditActiveScene and the SceneHeader.onEdit prop are gone —
the dialog is no longer reachable from the header. It stays
hooked up to SceneTabs' right-click → Umbenennen path for users
who prefer the modal.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New SceneHeader component: big scene name (clamp 2.75rem–4.5rem
responsive) plus a muted description underneath, or an italic
"Beschreibung hinzufügen…" placeholder when empty. The whole block
is a button — clicking it opens the existing scene edit dialog,
now pulling double duty for both name and description.
Wired through PageCarousel's new leading snippet from the previous
commit, so the header scrolls with the track and stays anchored to
the visual start of the carousel without needing a second scroll
container.
SceneRenameDialog grows a description textarea (maxlength 240,
3 rows, vertically resizable) and onSubmit now passes (name,
description). The caller translates an empty description to null
so the DB column reflects "no description set" rather than an
empty string — keeps WorkbenchScene.description truthy checks
honest.
handleEditActiveScene resolves the currently-active scene and
opens the dialog pre-filled; used by the SceneHeader click.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Consumers of PageCarousel can now pass a \`leading\` Snippet that
renders as the first flex child inside .fokus-track, ahead of the
page wrappers. Used on the workbench homepage for the scene header
(name + description). Scrolls with the track rather than sticking
in place — reads as an intro block, not app chrome, and doesn't
steal viewport from the cards on narrow screens.
Styled as flex-aligned, align-self:stretch so its intrinsic layout
decides the height and it centres vertically against the cards.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Schema + store preparation for the new scene header on the homepage
(next commit). Scenes get an optional free-text description stamped
as its own field — LWW through the existing mana-sync pipeline, no
new sync contract. The unused icon field is removed everywhere:
- types/workbench-scenes.ts: description?: string | null replaces icon
- stores/workbench-scenes.svelte.ts: createScene, renameScene,
duplicateScene, toScene, patchScene all updated. New method
setSceneDescription(id, value) mirrors renameScene so the caller
can change just the description without re-submitting the name.
- components/workbench/scenes/SceneTabs.svelte: the tab-bar rendered
{#if scene.icon} before the name — scene.name is unique enough to
identify a scene, and the UI direction is away from emoji chrome.
Existing scene rows in Dexie simply omit description (undefined → null
on read); the icon field on old rows is ignored and will age out of
the schema the next time the row is written.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the pre-step research call throws (mana-search down, missing tier,
402 credits, etc.), the runner used to swallow the error and feed an
empty input to the planner — which then made up a story about a "failed
web search" and fell back to create_task. Now we inject an explicit
"research failed" ResolvedInput with the actual error message, plus
write the truncated message into phaseDetail so it's visible in the
mission card without DevTools.
Bundles an in-flight actor refactor merge in runner.ts (makeAgentActor
+ LEGACY_AI_PRINCIPAL) — those lines came from the parallel Phase-1
identity work, not this fix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The standalone "Cloud Sync" pill sat fifth in the bar-mode pill row
on mobile / narrow layouts, pushing the pill row to 5+ items and
duplicating actions the user menu already surfaces (Sync-Einstellungen,
credits link). Move the sync status + actions into userMenuBarItems
so they appear as a labelled section inside the bar that opens when
the user pill is tapped.
Render order for signed-in users now goes:
account · settings · theme-mode · theme · [Sync section] · logout
The section header uses the existing `divider: true, label: 'Sync'`
shape PillDropdownBar already understands, so the status line ("Cloud
Sync aktiv", etc.) sits under a real heading. For guests the section
is skipped entirely — sync doesn't apply before login.
Left the non-barMode popover path alone (showSyncStatus + syncStatusItems
still drive the inline PillDropdown when a popover is in use) since
the complaint was specifically about the bar's fifth pill on mobile.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mission objectives matching /recherch|research|news|finde|suche|aktuelle|neueste/i
trigger a synchronous deep-research call (mana-search + mana-llm via the
existing /api/v1/research/start-sync pipeline) before the planner runs;
the summary plus top-8 source URLs are injected as a synthetic ResolvedInput
so the planner can stage save_news_article proposals against real URLs.
The kontext singleton is auto-attached to every mission's planner input
(decrypted client-side, gated on non-empty content + not already linked).
save_news_article is a new proposable tool routed through articlesStore
.saveFromUrl (Readability via /api/v1/news/extract/save). AiProposalInbox
mounted on /news so the user can approve/reject inline. mana-ai planner
tool list mirrors the new tool to keep the boot-time drift guard happy.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The shared-hono credits client returns DEFAULT_BALANCE=1000 when
/api/v1/internal/credits/balance/:userId responds with no row, so
local-dev accounts silently diverge from production — credit-gated
flows look free in dev and only blow up after deploy. Seeding a
real credits.balances row makes the fallback unreachable and the
dev stack exercises the same code path as prod.
Default is 10_000 credits (overridable via CREDITS env var) and
is applied alongside the existing tier + role + sync-gift upserts,
so setup-dev-user.sh stays a single idempotent pass. Existing dev
accounts (tills95, tilljkb, rajiehq) were backfilled manually
once; re-running the script won't clobber a higher balance
because the ON CONFLICT uses GREATEST.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Running pnpm type-check inside apps/api failed before any real
error could run, blocked by two structural errors: drizzle.presi.config.ts
and scripts/generate-who-dossiers.ts are deliberately outside src/
but are matched by the include pattern, tripping TS6059 against
rootDir=src. And @mana/shared-types imports peer files with explicit
.ts extensions, which needs allowImportingTsExtensions under
moduleResolution=bundler.
Remove rootDir (we're noEmit anyway — Bun runs src/index.ts
directly, tsc is only a lint pass), drop the unused outDir, add
noEmit explicitly, and enable allowImportingTsExtensions. Type-check
now completes cleanly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Admin-gated backend endpoints (e.g. POST /api/v1/admin/sync/:id/gift,
GET /api/v1/admin/users/:id/tier) check auth.users.role === 'admin',
which is orthogonal to access_tier. The script was already lifting
every dev account to tier=founder but left role at the 'user'
default, so founders couldn't exercise the admin UI flows against
their local stack. Wire role alongside tier (both via env-overridable
defaults) and reflect it in the success output so re-runs surface
what's being applied.
Backfilled the existing three dev accounts (tills95, tilljkb,
rajiehq) to role=admin manually once; re-running the script now is
idempotent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Go binary's config.go hardcoded "postgresql://…/mana" as the
DATABASE_URL fallback, but no database named "mana" exists locally
or in the macmini compose stack — the platform DB is mana_platform.
Anyone running the crawler without an explicit override got a
"database \"mana\" does not exist" crash at startup. The dev:crawler
script in package.json had been papering over this by setting
DATABASE_URL explicitly; drop that override now that the binary
default is correct.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two halves of the same "why is sync inactive in dev" fix:
- package.json: new dev:credits script and mana-credits added to
the dev:mana:servers concurrently group. The service was never
started by pnpm dev:mana:all, so the frontend's
GET /api/v1/sync/status failed, syncBilling.load() caught the
error and defaulted to inactive — while mana-sync (Go) was
actually fail-open on the billing check, making the UI
indicator lie about the backend state.
- scripts/dev/setup-dev-user.sh: after the existing
email-verify + tier-lift UPDATE, upsert a row into
credits.sync_subscriptions with is_gifted=true. Mirrors what
POST /api/v1/admin/sync/:id/gift would do, so every new dev
user gets Cloud Sync from the first login without a separate
admin call. The credits schema lives inside mana_platform, so
no new database needed — just a second statement in the same
psql heredoc.
Existing dev users (tills95, tilljkb, rajiehq) were backfilled
manually with the same INSERT … ON CONFLICT DO UPDATE once;
future runs of setup-dev-user.sh stay idempotent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Public ingress for the Mission Key-Grant audit endpoint
(/api/v1/me/ai-audit) so the Workbench "Datenzugriff" tab can reach
mana-ai from the browser. Background tick + /metrics stay internal;
only the JWT-gated user endpoint is exposed.
Requires a Cloudflare DNS record pointing mana-ai.mana.how at the
tunnel CNAME (one-off: \`cloudflared tunnel route dns
1435166a-0e3f-4222-8de6-744f32cea5c9 mana-ai.mana.how\`), then sync
via scripts/mac-mini/sync-tunnel-config.sh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two visible issues on /login and /register in the Mana web app:
- The "Zum Anmeldeformular springen" skip link was visible in the
top-left corner instead of only on keyboard focus. It's an a11y
nicety that in practice clutters the page for every user. Remove
the button (and the dead skipToForm() handler in LoginPage) rather
than try to fix an sr-only variant that apparently doesn't survive
the Tailwind v4 @source pipeline here.
- The form card rendered ~full-width on desktop even though the
existing max-w-[440px] / max-w-[480px] arbitrary-value classes
should have constrained it. Replace those with style:max-width
inline so the 440px cap is guaranteed regardless of how Tailwind
picks up classes from the shared-auth-ui package.
Same changes applied to RegisterPage (only width — no skip link
there) so the two pages stay visually identical.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
useRegisterSW() reaches for navigator.serviceWorker at call time, which
crashes SvelteKit SSR with "ReferenceError: navigator is not defined"
(Node has no navigator). The prod mana-web container crash-looped on
every request after the rebuild because the layout mounts this
component unconditionally.
Fix: branch on \$app/environment's \`browser\` flag. On the server,
hand back a noop writable + async-noop updater so the downstream
template code stays unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The 2rem linear-gradient mask at each end of the tag strip was
supposed to hint at scrollable overflow, but it cuts off the
leading + trailing tags on narrow layouts where they're already
just barely visible. Dropping both mask-image declarations lets
the pills render edge-to-edge. Overflow scrolling + hidden
scrollbar stay as they were, plus the hover-lift removal from
the previous commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The whole pill family used a translateY(-1px / -2px) on :hover that
made the chrome jitter every time the cursor crossed anything —
especially noticeable in the TagStrip, PillNav, and the user-menu
bar where pills sit close together and a 1-2px jump reads as
twitching rather than polish.
Removed the hover transform (and the matching :active reset on
the base Pill) from:
- Pill.svelte — base primitive, covers PillNav + every bar pill
- AppDrawer.svelte — .glass-pill in the app switcher
- PillTagSelector.svelte — .glass-pill in the quick-input tag selector
- PillTimeRangeSelector.svelte — .glass-pill in the calendar range picker
- UserMenuPanel.svelte — .chip hover in the popover variant
Background, border, and shadow hover states are kept — only the
vertical displacement is gone.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the right-hand user pill is clicked as a guest (barMode), the
opening bar used to show just a decorative "Menü" label pill on
the left and then settings + theme toggles — none of which are a
useful first action for someone not logged in.
- phosphor-icon-map: register SignIn as `login`, so PillDropdownBar
can render it the same way as the existing `logout` glyph.
- PillNavigation.userMenuBarItems: prepend an "Anmelden" item for
guests (no userEmail + loginHref given) that navigates to
loginHref. It becomes the first CTA in the bar, above settings /
theme / language.
- PillNavigation.userBarConfig: drop the bar-header label when the
viewer is a guest. The decorative "Menü" pill added nothing
alongside the new Anmelden action and just cluttered the leading
edge of the bar.
The popover variant of the user menu (UserMenuPanel) already had
its own guest login button — this mirrors that behaviour for the
bottom-bar variant used on mobile / narrow layouts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vite-plugin-pwa's \`virtual:pwa-register/svelte\` imports workbox-window
at build time. The package was resolved transitively via @vite-pwa/
sveltekit but not installed in the webapp's own node_modules when
pnpm fetches only the workspace deps in a restricted Docker context.
Result: Rollup 'failed to resolve import "workbox-window"' at build.
Pinning the direct dep so the Docker build picks it up in the
filtered pnpm install.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Webapp package.json gained a @mana/shared-ai workspace dep (Mission
Grant types + canonical HKDF derivation). Without the package in the
Dockerfile COPY list, pnpm install aborts with
ERR_PNPM_WORKSPACE_PKG_NOT_FOUND. Caught during the first mana-web
rebuild after the Mission Grant rollout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Webapp now passes:
- PUBLIC_MANA_AI_URL / PUBLIC_MANA_AI_URL_CLIENT → getManaAiUrl()
resolves these; powers the Workbench "Datenzugriff" tab fetch.
- PUBLIC_AI_MISSION_GRANTS (default false) → gates the MissionGrant
dialog + audit tab. Flip to "true" in .env once the keypair is
provisioned.
Follow-up for operator: add a Cloudflare tunnel route for
mana-ai.mana.how → mana-ai:3067 (mirroring the existing pattern
for credits/events/llm) so the audit fetch resolves from the browser.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the single-line summary ("Planner failed: fetch …") with
full diagnostic detail: error name + message + last-active phase +
stack trace, all persisted onto the iteration itself. UI expands a
collapsed details block next to each failed iteration, so the user
can see *where* it broke ("TypeError in calling-llm") without opening
DevTools.
Paired with a one-click Retry button that re-runs the mission under
the same config — useful while debugging a flaky backend (GPU server
down, Gemini quota, etc.).
- `packages/shared-ai/src/missions/types.ts` — new
`MissionIteration.errorDetails: { name, message, phase?, stack? }`
- `finishIteration` accepts the field, deep-clones it, and also now
clears the transient phase markers (currentPhase/phaseStartedAt/
phaseDetail/cancelRequested) whenever an iteration finalises — keeps
the schema honest (phases are sub-state of \`running\` only).
- `runMission` tracks \`lastPhase\` via a new \`enterPhase\` helper that
wraps setIterationPhase. The catch handler populates errorDetails
with lastPhase + message + stack.
- ListView: \`<details>\` block under each failed iteration + Retry
button (disabled while another run is in-flight).
77/77 webapp tests still green; svelte-check clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
shared-hono declares @mana/shared-logger as a workspace dep. Without
that package in the installer stage, Bun fails at runtime with ENOENT
reading /app/packages/shared-hono/node_modules/@mana/shared-logger.
Caught when mana-ai crash-looped on first boot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wires the "Als Notiz speichern" action at the bottom of the Kontext
widget (UI itself landed in 003f75f7e) to actually open Notes next
to Kontext and focus the new note:
- workbench-scenes: new addAppAfter(appId, anchorAppId). addApp()
always appended, which pushed Notes to the far end of the
carousel; addAppAfter inserts directly after the anchor (Kontext)
and no-ops if the target is already open so the user's current
position isn't yanked around.
- notes/stores/selection: new transient in-memory focus signal
(focusedNoteId) that cross-module callers populate. Kept
non-persistent intentionally — surviving a remount would re-open
random notes after page loads.
- notes/ListView: $effect reads focusedNoteId, waits for the
Dexie liveQuery to surface the just-created row, opens it in
the inline editor, clears the focus signal, then scrolls the
matching data-note-id element into view via queueMicrotask so
the DOM has rendered the editor variant.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire the Mission Key-Grant feature into the production Mac Mini
compose stack so mana-ai can boot and mana-auth can mint grants.
- New mana-ai service block (port 3066) — 256m mem limit, depends on
postgres + mana-llm, tick interval configurable via
MANA_AI_TICK_INTERVAL_MS / MANA_AI_TICK_ENABLED. Pulls
MANA_AI_PRIVATE_KEY_PEM from env; absent = grants silently disabled.
- mana-auth environment gains MANA_AI_PUBLIC_KEY_PEM (default empty
so existing deployments without the keypair degrade to 503
GRANT_NOT_CONFIGURED rather than failing to boot).
- mana-auth Dockerfile rewritten to the two-stage pnpm+bun pattern
used by mana-credits/mana-events — required now that mana-auth has
a @mana/shared-ai workspace dep. The previous single-stage
Dockerfile with service-scoped build context couldn't resolve any
@mana/* imports; that only worked historically because it fell
through at runtime via a pre-built layer.
- mana-ai Dockerfile copies packages/shared-ai into the installer
stage alongside shared-hono.
The build contexts for mana-auth flip from services/mana-auth to the
repo root. Existing CI/CD paths (scripts/mac-mini/build-app.sh) pass
through to docker compose build and pick up the new context
automatically — no script edits needed.
Flip-on procedure: on the Mac Mini, set MANA_AI_PUBLIC_KEY_PEM +
MANA_AI_PRIVATE_KEY_PEM in .env (already done, see
secrets/mana-ai/README.md on the host), then rebuild mana-auth +
build mana-ai.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Frontend plumbing for the Kontext "Aus URL" inline panel (the UI
itself was committed earlier as part of 003f75f7e):
- kontext/api.ts: new module-scoped fetch wrapper that talks to
/api/v1/context/import-url with a Bearer token. Kept in kontext/
rather than a shared helper because it's the only caller; moving
it to `context/` would mix two unrelated modules again.
- kontext/stores/kontext.svelte.ts: new appendContent(chunk) method.
The singleton row is encrypted at rest, so we decrypt the current
content before concatenating with a '\n\n---\n\n' separator and
writing back — going through setContent() to keep encryption +
Dexie hook behaviour consistent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New backend endpoint that wraps mana-crawler + mana-llm in a single
call so the Kontext "Aus URL" UI can hit one route:
- Starts a crawl job (single page or up-to-20-page deep crawl) via
mana-crawler's /api/v1/crawl, polls status up to 90s, then fetches
paginated results.
- When multiple pages are returned, joins them into one markdown
document with H1-per-page section headers separated by ---.
- When summarize=true, routes the collected markdown through
mana-llm/chat/completions with a system prompt that asks for
"Überblick / Kernaussagen / Details" H2 structure in the source
language. sanitizeSummary() strips the common local-LLM artefacts
(```markdown fences, "Hier ist …:" preamble, stray leading H1)
so the output drops cleanly into the Kontext doc. On summary
failure the endpoint returns 502 rather than silently falling
back to the raw crawl.
- Credits are validated + consumed via @mana/shared-hono/credits
(1 credit crawl-only, 5 crawl+summary) under the new
AI_CONTEXT_IMPORT_URL action.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three small config changes so the Kontext "Aus URL" flow (next commit)
is runnable from a plain `pnpm dev:mana:all`:
- package.json: include mana-crawler in the dev:mana:servers
concurrently group, and pass DATABASE_URL=…/mana_platform so the
Go binary doesn't try to connect to a non-existent `mana` DB (its
hardcoded default).
- .env.development: publish MANA_CRAWLER_URL=http://localhost:3023
(the crawler's default binary port — the macmini container is
a 3014 override, kept only in docker-compose). Also surface
MANA_LLM_DEFAULT_MODEL for the summariser.
- docker-compose.macmini.yml: inject MANA_CRAWLER_URL + the
default-model env into the mana-api container so production
can reach the internal crawler and pick the summariser model
consistently.
No runtime code touched.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-push runs svelte-check --fail-on-warnings. Two items were blocking:
- +layout.svelte: drop four .pill-nav-toggle* rules that have no
matching markup anywhere in the app (dead CSS from the pill-nav
rework that removed the collapse toggle).
- kontext KontextView.svelte: drop the explicit `: Phase[]` annotation
on `let importPhases = \$derived.by(...)`. Svelte 5's runes return a
wrapped type that TypeScript reads as a function when the annotation
is present, which is what produced the "not callable" + "state
invalid placement" chain. Inferred type is the same Phase[] shape.
No runtime behaviour change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the "iteration is running, no feedback" black hole. The user now
sees, per running iteration:
⏳ Frage Planner · frage Planner an ⏱ 23s
[Abbrechen]
Phases (\`IterationPhase\`):
resolving-inputs → calling-llm → parsing-response →
staging-proposals → finalizing
The runner advances through these via \`setIterationPhase\` between each
await, writing currentPhase + phaseDetail + phaseStartedAt onto the
iteration. UI reads them via Dexie liveQuery — no polling.
Cancel:
- \`requestIterationCancel\` writes cancelRequested=true on the iteration
- runner polls \`isCancelRequested\` between every phase + per stage step
- cancellation finalises as \`failed\` with summary \`'cancelled by user'\`
- UI button is disabled + relabelled "Wird abgebrochen…" until the next
poll picks it up
Hard timeout: 90 s wall-clock per iteration via Promise.race against a
CancelledError. Wedged backends (e.g. flaky mana-llm) fail fast with
"timeout after 90s" instead of sitting in \`running\` forever.
Elapsed counter is a \$state variable ticking once a second, scoped to
the ListView component — Dexie isn't touched. Auto-cleaned on
component destroy.
shared-ai re-exports \`IterationPhase\` so server-side mana-ai can
inspect the same phase enum (no consumer there yet, but the type is
ready for the run-status endpoint planned in HEALTH page).
77/77 webapp tests still green; svelte-check clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 4 — everything needed to flip the Mission Key-Grant feature on
safely per deployment. No new behaviour; purely operational plumbing.
- PUBLIC_AI_MISSION_GRANTS feature flag (default off). hooks.server.ts
injects window.__PUBLIC_AI_MISSION_GRANTS__, api/config.ts exposes
isMissionGrantsEnabled(). Grant UI (dialog + status box) and the
Workbench "Datenzugriff" tab both hide when the flag is off.
- PUBLIC_MANA_AI_URL added to the injection set so the webapp can reach
the new audit endpoint from production.
- Prometheus alerts (new mana_ai_alerts group):
- ManaAIServiceDown (warning, 2m)
- ManaAIGrantScopeViolation (critical, 0m) — MUST stay at 0; any
increment pages immediately
- ManaAIGrantSkipsHigh (warning, 15m) — flags keypair drift
- ManaAIPlannerParseFailures (warning, 10m) — prompt/LLM drift
- Runbook in docs/plans/ai-mission-key-grant.md: initial keypair gen,
leak-response procedure (rotate + invalidate all grants + audit),
scope-violation triage.
- User-facing doc in apps/docs security.mdx: new "AI Mission Grants"
section with the three hard constraints (ZK users blocked, scope
changes invalidate cryptographically, revocation is one click) plus
an honest threat-model comparison column showing where grants shift
the tradeoff.
Rollout remaining (not code): generate keypair on Mac Mini, provision
MANA_AI_PRIVATE_KEY_PEM + MANA_AI_PUBLIC_KEY_PEM via Docker secrets,
flip PUBLIC_AI_MISSION_GRANTS=true starting with till-only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3 — user-facing side of the Mission Key-Grant rollout. Users
can now opt into server-side execution, revoke it, and inspect every
decrypt the runner has performed.
Webapp:
- MissionGrantDialog explains the scope (record count, tables, TTL,
audit visibility, revocation) and calls requestMissionGrant. Error
paths render distinctly for ZK, not-configured, missing vault.
- Mission detail shows a Server-Zugriff box with status pill
(aktiv/abgelaufen/nicht erteilt), Neu-erteilen + Zurückziehen
buttons. Only renders for missions with at least one encrypted-
table input.
- store.ts: setMissionGrant / revokeMissionGrant helpers, Proxy-
stripped like the rest of the store's writes.
- Workbench adds a Timeline/Datenzugriff tab switch. Audit tab queries
the new GET /api/v1/me/ai-audit endpoint, renders decrypt events
with color-coded status pills (ok/failed/scope-violation) and
stable reason strings.
- getManaAiUrl() added to api/config for the audit fetch.
mana-ai:
- GET /api/v1/me/ai-audit (JWT-gated via shared-hono authMiddleware)
backed by readDecryptAudit() — withUser + RLS double-gate so a user
can only read their own rows.
- Limit capped at 1000, newest-first.
Missions without a grant continue to work exactly as before; the
grant UI is purely additive.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Browser \`structuredClone\` itself fails on Svelte 5 \$state Proxies
("Failed to execute 'structuredClone' on 'Window': [object Array]
could not be cloned") — it doesn't transparently unwrap the Proxy the
way I'd hoped. The structured-clone algorithm refuses any non-cloneable
host object, including Svelte's reactive wrappers.
JSON.parse(JSON.stringify(...)) traverses through the Proxy by reading
each property normally, producing a plain-data copy that Dexie can
serialise without complaint.
Mission payloads are pure JSON values (strings/numbers/arrays/objects)
so JSON-roundtrip is lossless. The new \`deepClone\` helper is local to
this file with a comment pointing at the structured-clone failure.
77/77 webapp tests still green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`createMission`, `updateMission`, and `{start,finish}Iteration` all
received caller-supplied objects that can be Svelte 5 \$state Proxies
(MissionInputPicker binds the inputs array with \$state). IndexedDB's
structured-clone algorithm doesn't accept proxied arrays and throws
`DataCloneError: [object Array] could not be cloned` — visible to
users as "Mission anlegen" failing silently after clicking Create.
Wrap each proxy-carrying payload in `structuredClone()` at the store
boundary:
- createMission: `inputs` + `cadence`
- updateMission: whole `patch` (anything can be proxy)
- startIteration: `plan`
- finishIteration: `plan` (conditional)
`structuredClone` is the native browser / Bun helper; strips Proxies
while preserving Dates / Maps / Sets / nested plain data. Store stays
robust to any future caller that forgets to snapshot before passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 2 of Mission Key-Grant. The tick loop now honours a mission's
grant by unwrapping the MDK and passing it + the record allowlist into
the resolvers. Encrypted modules (notes, tasks, calendar, journal,
kontext) resolve server-side instead of returning null.
- crypto/decrypt-value.ts: mirror of webapp AES-GCM wire format
(enc:1:<iv>.<ct>) — read-only, server never wraps
- db/resolvers/encrypted.ts: factory + 5 concrete resolvers. Scope-
violation bumps a metric + writes a structured audit row, decrypt
failures same. Zero-decrypt (no grant, or record absent) = silent
null, no audit noise.
- db/audit.ts: best-effort append to mana_ai.decrypt_audit; write
failures never cascade into tick failures.
- cron/tick.ts: buildResolverContext unwraps grant per mission; MDK
reference only lives for the scope of planOneMission.
- ResolverContext plumbed through resolveServerInputs; existing goals
resolver unchanged semantically.
- Metrics: mana_ai_decrypts_total{table}, mana_ai_grant_skips_total
{reason}, mana_ai_grant_scope_violations_total{table} (alert > 0).
Missions without a grant still run exactly as before — plaintext
resolvers fire, encrypted ones short-circuit to null. No behaviour
regression for existing users.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 of the Mission Key-Grant rollout. Webapp can now request a
wrapped per-mission data key; mana-ai can unwrap and (Phase 2) use it.
mana-auth:
- POST /api/v1/me/ai-mission-grant — HKDF-derives MDK from the user
master key, RSA-OAEP-2048-wraps with the mana-ai public key, returns
{ wrappedKey, derivation, issuedAt, expiresAt }
- MissionGrantService refuses zero-knowledge users (409 ZK_ACTIVE) and
returns 503 GRANT_NOT_CONFIGURED when MANA_AI_PUBLIC_KEY_PEM is unset
- TTL clamped to [1h, 30d]
mana-ai:
- configureMissionGrantKey + unwrapMissionGrant with structured failure
reasons (not-configured / expired / malformed / wrap-rejected)
- mana_ai.decrypt_audit table + RLS policy scoped to
app.current_user_id — append-only row per server-side decrypt attempt
- MANA_AI_PRIVATE_KEY_PEM env slot; absent = grants silently disabled
No existing behaviour changes: missions without a grant run exactly as
before. Grant flow is wired end-to-end but unused until Phase 2 lands
the encrypted resolver.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Foundation for Phase 2+ of the Mission Key-Grant flow: lets mana-ai
execute missions that depend on encrypted inputs (notes/tasks/events/
journal/kontext) without needing an open browser tab. Opt-in per
mission, Zero-Knowledge users excluded.
- Canonical HKDF-SHA256 derivation (scope-bound via tables + recordIds
in the HKDF info string → scope changes invalidate the grant
cryptographically, not just via a runtime check)
- Mission.grant field on the shared Mission type
- Golden snapshot + drift-guard test so webapp wrap path and mana-auth
wrap endpoint can't silently diverge
- Ideas backlog at docs/future/AI_AGENTS_IDEAS.md
- Full rollout plan at docs/plans/ai-mission-key-grant.md
- COMPANION_BRAIN_ARCHITECTURE.md §21 captures the flow + privacy
guarantees + non-goals
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Groups the six AI-Workbench apps plus the Companion chat under a
dedicated category, moved to order 0 so they're the first thing a user
sees when adding a page. Brain icon.
- `categories.ts`
- New `ai` type added to AppCategory union at order 0
- Old 'companion' category stays for derived projections (myday,
activity, goals) that aren't themselves AI-driven — renamed doc
only, behavior unchanged
- APP_CATEGORY_MAP reassigns: companion, ai-missions, ai-workbench,
ai-rituals, ai-policy, ai-insights, ai-health → 'ai'
- `AppPagePicker.svelte` collapsed-state map gains `ai: false` so the
new category is expanded by default (it's the user's new front door)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts the previous /companion-carousel misstep. The user's model is
that Missions / Workbench / Rituals / Policy / Insights / Health each
live as their OWN app in the root `/` workbench scene, alongside todo /
calendar / notes / etc. — openable from the normal app picker, freely
combinable with any other module card.
- New modules, each with a ListView.svelte usable inside AppPage's
PageShell (no self-wrapping, no shell-control props):
`lib/modules/ai-missions/ListView.svelte`
`lib/modules/ai-workbench/ListView.svelte`
`lib/modules/ai-rituals/ListView.svelte`
`lib/modules/ai-policy/ListView.svelte`
`lib/modules/ai-insights/ListView.svelte`
`lib/modules/ai-health/ListView.svelte`
- Registered in `app-registry/apps.ts`: `ai-missions`, `ai-workbench`,
`ai-rituals`, `ai-policy`, `ai-insights`, `ai-health`. Each picks a
distinct color + icon.
- `/companion/+page.svelte` restored to the simple chat it was before.
The companion app (chat) remains as its own registered app —
unchanged.
- Removed: `lib/modules/companion/pages/` + the
`workbench-settings.svelte.ts` store (both were the dead-end
PageCarousel approach).
User model now:
` / ` (workbench root) → [+ App] → any of the 6 AI apps + any module
/companion → full-screen chat (unchanged)
/todo, /calendar, … → module-inline ghost inbox stays
svelte-check clean; webapp AI tests 71/71 green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drops the split /companion/missions + /companion/workbench +
/companion/rituals sub-routes and rebuilds /companion around the
shared PageCarousel pattern (same one /todo uses). Every feature is
now a self-contained page the user opens/closes/reorders/resizes in
one unified surface.
New pages:
- **Home** (default) — compact stats + one-click shortcuts to every
other page, with "offen" badge when already open
- **Chat** — conversation sidebar + active chat inline
- **Missions** — list ↔ create ↔ detail master-detail inside one pane
- **Workbench** — timeline grouped by iteration + Revert per bucket,
Mission filter dropdown (replaces the old ?mission=… query-param)
- **Rituals** — migrated from /companion/rituals
- **Policy** — NEW: 3-way per-tool toggle (auto/propose/deny) with
localStorage-backed overrides merged into DEFAULT_AI_POLICY live
- **Insights** — NEW: approval rate, 14-day bar chart of AI events,
per-mission stats, top-5 recurring user-feedback strings. All from
local Dexie liveQueries, no server calls.
- **Health** — NEW: foreground runner status, manual tick trigger,
link out to status.mana.how for server-runner uptime
Plumbing:
- `stores/workbench-settings.svelte.ts` — persistent open-pages list
in localStorage (id + widthPx + heightPx + maximized); open/close/
resize/moveLeft/moveRight helpers
- `pages/page-meta.ts` — central registry (title, color, icon,
description) consumed by home shortcuts + PagePicker
- `pages/PagePicker.svelte` — lists available (not-yet-open) pages
with icon + description
- Old sub-routes deleted: /companion/{missions,workbench,rituals}/
Webapp tests still 71/71 green; svelte-check clean on the new pages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wires mana-ai into the existing observability stack so tick throughput,
plan-failure rates, planner latencies, and snapshot refresh health are
visible in Grafana + Prometheus, and the service's uptime surfaces on
status.mana.how under the "Internal" section.
- `src/metrics.ts` — prom-client Registry with `mana_ai_` prefix.
Counters: ticks_total, plans_produced_total, plans_written_back_total,
parse_failures_total, mission_errors_total, snapshots_new/updated,
snapshot_rows_applied_total, http_requests_total.
Histograms: tick_duration_seconds (0.1–120s), planner_request_
duration_seconds (0.25–60s), http_request_duration_seconds (0.005–10s).
- `src/index.ts` — HTTP middleware labels every request by
method/path/status; `/metrics` serves the Prometheus text format.
- `src/cron/tick.ts` — increments counters + wraps the tick with
`tickDuration.startTimer()`. Snapshot stats fold through.
- `src/planner/client.ts` — wraps `complete()` in a latency histogram
timer so planner tail latency shows up separately from tick duration.
- `docker/prometheus/prometheus.yml` —
1. New `mana-ai` scrape job against `mana-ai:3066/metrics` (30s).
2. `/health` added to the `blackbox-internal` job so uptime shows on
status.mana.how alongside mana-geocoding.
- `scripts/generate-status-page.sh` — friendly label for the new probe:
`mana-ai:3066/health` → "Mana AI Runner" (generator already iterates
`blackbox-internal`, no other changes needed).
- `package.json` — prom-client ^15.1.3
All 17 Bun tests still pass; tsc clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sync status, user menu (bar-mode + overlay fallback) and logout now use
the shared Pill component like the rest of PillNavigation. All pill
styling now lives in a single place.
- Pill gains an escape-hatch `data?: Record<string, string>` prop for
arbitrary data-* attributes (used by the user-menu trigger which is
click()-ed via querySelector from the consuming layout) and a
bindable `element` binding for imperative focus/positioning
(replaces the old bind:this={userMenuTrigger}).
- Remove the now-dead inline .pill / .glass-pill / .logout-pill /
.pill-label / .pill.active / .pill.icon-only CSS from PillNavigation
(all living in Pill.svelte now). ~110 lines of CSS gone.
- The mobile override that forced 44px min-height on .pill is also gone;
Pill sizes are controlled via the size prop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the O(N sync_changes) LWW replay in every tick with an
incremental snapshot table refresh. Each tick now applies only the
delta since the last run, then runs a single indexed SELECT on the
snapshot table to find due missions.
- `db/migrate.ts` — idempotent migration. Creates `mana_ai` schema and
`mana_ai.mission_snapshots` table on boot. Partial index on
active+nextRunAt powers the tick's "due" query.
- `db/snapshot-refresh.ts`
- `refreshSnapshots(sql)` one-pass: joins sync_changes and snapshots
on (user_id, mission_id), picks out pairs whose source max
created_at exceeds the snapshot cursor. Per-pair refresh wrapped
in `withUser` for RLS scoping on the source SELECT.
- Bootstrap: missing snapshot rows seed from a full replay of their
mission's history; subsequent ticks apply only the delta.
- Delete tombstones purge the snapshot row.
- `db/missions-projection.ts` `listDueMissions` — single SELECT against
`mana_ai.mission_snapshots` with an indexed WHERE. Dropped the legacy
cross-user scan + per-user two-phase read (unused now). `mergeAndFilter`
stays for its existing test coverage.
- `cron/tick.ts` calls `refreshSnapshots` before `listDueMissions` and
logs when the refresh actually applied rows. No behaviour change
externally.
- `index.ts` awaits `migrate()` on boot (top-level `await` — Bun
supports it natively).
Closes the last item on the AI-Workbench roadmap's "future work" list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PillNavigation rendered three near-identical inline pill blocks (prepended
elements, main nav items, appended elements). Consolidate onto the Pill
component so the visual base stays in lockstep with the bottom-stack bars.
- Extend Pill with size='sm'|'md'. sm = 36px with 18px icons (PillNav
style); md = 44px with 20px icons (bar pills, default).
- Move the icon-only padding override into Pill itself.
- Extract the Mana-Logo SVG (duplicated inline) to ManaLogoIcon.svelte.
- Replace the three inline pill loops in PillNavigation with <Pill size='sm'>.
Mana-logo and iconSvg cases ride the `leading` snippet. onClick vs href
disambiguation is collapsed into a single Pill call per item.
- Remove the now-unreachable .pill-icon scoped CSS that was only meaningful
for the removed inline SVGs (Phosphor icon sizing comes from the size
prop).
Net: ~70 lines removed from PillNavigation.svelte without changing the
render output. Bar-mode triggers (sync / ai / theme / user) still render
inline because their logic is too entangled with activeBarId — leave for
a follow-up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>