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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Each iteration bucket now carries a Revert button that undoes every AI
write attributed to that iteration. Closes the last open Workbench
feature in the roadmap.
- `data/ai/revert/inverse-operations.ts` — pluggable registry mapping
event types to their undo actions. Ships with inverses for the five
most common proposable outcomes:
* TaskCreated → tasksStore.deleteTask
* TaskCompleted → tasksStore.toggleComplete (back to incomplete)
* CalendarEventCreated → eventsStore.deleteEvent
* PlaceCreated → placesStore.deletePlace
* DrinkLogged → drinkStore.deleteEntry
Events with no registered inverse are tallied as `skippedUnsupported`
— user knows to handle those manually rather than the service
silently doing nothing.
- `data/ai/revert/revert-iteration.ts` — orchestrator. Filters
`_events` by `actor.iterationId + actor.missionId`, sorts
newest-first (so a completion unwinds before the underlying task
deletion), applies each inverse, returns `RevertStats` summary.
- Workbench UI: Revert button with confirm dialog on every bucket.
Shows "X zurückgenommen · Y nicht unterstützt · Z fehlgeschlagen"
result alert.
- 5 unit tests cover: happy path, unsupported types, failure isolation,
user-event skipping, newest-first ordering.
With this, the AI Workbench has full audit + undo semantics: user sees
everything the AI did, can approve/reject at stage time, and can roll
back approved actions after the fact.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both QuickInputBar (InputBar.svelte), CommandBar.svelte, and GlobalSpotlight
were duplicating syntax highlighting and the 150ms search debounce. Pull
these into a new `packages/shared-ui/src/search-core/` module so the two
input surfaces stay in sync on feel and matching rules.
- search-core/highlight.ts — HighlightPattern type, locale-aware
getHighlightPatterns(), and the shared highlightText() (HTML-escape +
span wrap). Patterns were previously in quick-input/highlightPatterns.ts
+ inline in CommandBar.svelte.
- search-core/config.ts — SEARCH_DEBOUNCE_MS = 150. Used from InputBar,
CommandBar, GlobalSpotlight, and apps/mana web SearchEngine.
- quick-input/highlightPatterns.ts + types.ts become thin back-compat
re-exports.
- Public surface: @mana/shared-ui now exports getHighlightPatterns,
highlightText, SEARCH_DEBOUNCE_MS, and the HighlightPattern type.
No UX change. UIs still live in their own files (per earlier split
recommendation: shared backend, separate surfaces).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract Pill.svelte as the single visual primitive (44px, icon+label,
active/primary/danger variants) used by PillDropdownBar and TagStrip.
PillNav keeps its own internal .pill class (36px, icon-only-oriented).
- Extract phosphor-icon-map.ts to deduplicate the icon lookup tables
that previously lived inline in PillDropdownBar.
- Unify bar slot heights in (app)/+layout.svelte: 56px PillNav,
64px for tags / quickinput / tabbar / dropdown-bar. Remove debug
outlines. Collapse bottom-stack gap so bars sit flush below PillNav.
- SceneAppBar wrapped in 64px slot, scene-pill/app-tab 40px to match.
- Enforce single-bar policy: opening one bar closes the others.
- QuickInputBar strip-down: remove leading CheckSquare icon and trailing
nav-toggle snippet; bar is pure search input now.
- Move user-menu (last PillNav pill) to bar-mode with short content:
Einstellungen, Light/Dark/System segmented, Theme, Logout.
- Swap tabs nav icon from Columns to Cards for better readability.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Planner reads `mission.iterations[].userFeedback` — not
`pendingProposals.userFeedback` — so feedback stored only on the
proposal row never reached the next plan. rejectProposal now also
appends to the matching iteration's `userFeedback` (merging with any
existing reasons via "\n· " bullets) when the proposal carries
missionId + iterationId.
Lazy imports the mission store to avoid a proposal↔mission cycle.
Best-effort: proposal rejection still succeeds even if the bubble fails.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rejecting a ghost-card proposal now reveals an inline textarea. Trimmed
feedback lands in `proposal.userFeedback` where the next Planner
iteration already reads it via iteration history — the AI learns from
concrete rejection reasons without needing a settings UI.
- Click "Ablehnen" → textarea + Cancel/Reject buttons replace the
approve/reject row
- Empty submission still rejects (userFeedback stays undefined so the
Planner sees "no reason given" rather than an empty string)
- `rejectingId` + `rejectDraft` are per-inbox local state; only one
reject form is open at a time
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
One-line drop-in per module page. Each module's proposals now render as
ghost cards inline wherever the user already expects to see that
module's content — no separate approvals inbox to hunt for.
Covers all four remaining modules with proposable tools in
AI_PROPOSABLE_TOOL_NAMES:
- calendar → create_event
- places → create_place, visit_place
- drink → undo_drink
- food → (auto-tools only today; the inbox is ready for future
propose-class food tools)
Now the only modules with proposable tools NOT showing the inbox are
those that don't have their own /route (all covered by todo, calendar,
places, drink, food).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 1–9 scroll to the Nth open app on the workbench homepage; 0 opens the
app picker.
- q/w/e toggle the bottom bars (workbench tabs / search / tags); r opens
the user-menu PillDropdownBar (expanding the PillNav first if needed);
t toggles the PillNav visibility.
Adds a `data-user-menu-trigger` hook on the user pill so the layout can
drive the menu bar programmatically without duplicating its config.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Makes the webapp's AI policy and the server's tool allow-list physically
impossible to drift. Adds the missing entries the guard caught on first
run: `complete_tasks_by_title`, `visit_place`, `undo_drink` now have
parameter schemas server-side too.
- `packages/shared-ai/src/policy/proposable-tools.ts`
- `AI_PROPOSABLE_TOOL_NAMES` as `const` array + literal union type
- `AI_PROPOSABLE_TOOL_SET` for set-membership checks
- Webapp `DEFAULT_AI_POLICY` derives its `propose` entries from the
shared list via `Object.fromEntries(...)` — adding a tool there is now
a one-line change in `@mana/shared-ai`
- mana-ai `AI_AVAILABLE_TOOLS`: module-load assertion compares its
hardcoded names against `AI_PROPOSABLE_TOOL_SET` and throws with a
pointed error on drift (extras in one direction, missing in the
other). Service refuses to start on mismatch — better than silent
degradation.
- Bun test (`tools.test.ts`) runs the same contract plus sanity checks
(non-empty description, required params carry docs). Vitest policy
test adds the symmetric check on the webapp side.
All three runtimes now green: webapp 66/66, shared-ai 2/2,
mana-ai 9/9 Bun tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mission-Input-Picker now surfaces open tasks + upcoming calendar events
alongside notes / kontext / goals. When the foreground runner runs, the
corresponding resolvers decrypt the records client-side and hand real
content (title + due date / event time + description) into the Planner
prompt.
- `tasksResolver` + `tasksIndexer` — reads unencrypted subset + decrypts
via `decryptRecords('tasks', …)`. Picker shows only OPEN tasks (not
completed ones) to keep the list relevant. Resolver output is
`[status]{ · fällig date}{ \n description}` — terse by design.
- `calendarResolver` + `calendarIndexer` — similarly decrypts events;
picker prioritizes upcoming events (sorted by startIso), shows
near-term times as "bald: YYYY-MM-DDTHH:MM" to make recency obvious
- Both tables are encrypted client-side — server-side mana-ai resolvers
remain intentionally absent (per the privacy contract in
`services/mana-ai/src/db/resolvers/types.ts`)
With this, a user can create a Mission like "plan my week" and link a
few tasks + calendar events as context; the in-browser planner sees the
full picture (decrypted), while the off-tab runner still plans from
objective + concept only for those missions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Completes the off-tab AI pipeline. mana-ai now writes produced plans
back to `sync_changes` as a server-sourced Mission iteration; the webapp
picks it up on next sync and translates each PlanStep into a local
Proposal via the existing createProposal flow. User sees the resulting
ghost cards in the matching module's AiProposalInbox with full mission
attribution.
Server (mana-ai v0.3):
- `db/connection.ts` — `withUser(sql, userId, fn)` RLS-scoped tx helper
mirroring the Go `withUser` pattern (SET LOCAL app.current_user_id)
- `db/iteration-writer.ts`
- `planToIteration(plan, id, now)` — shared-ai AiPlanOutput → inline
MissionIteration with `source: 'server'` + status='awaiting-review'
- `appendServerIteration(sql, input)` — INSERT sync_changes row with
op=update, data={iterations: [...]} + field_timestamps + actor
JSONB={kind:'system', source:'mission-runner'}
- `cron/tick.ts` — after parse success: build iteration, append to
mission.iterations, persist via appendServerIteration. Stats now
include `plansWrittenBack`.
Actor union:
- `packages/shared-ai/src/actor.ts` + webapp actor: `system.source` gains
`'mission-runner'` so the server's own writes are attributed correctly
and distinguishable from projection/rule writes
Webapp:
- `data/ai/missions/server-iteration-staging.ts`
- `startServerIterationStaging()` subscribes to aiMissions via Dexie
liveQuery; on each Mission update, walks iterations looking for
`source='server'` entries that haven't been staged yet
- For each such iteration: creates a Proposal per PlanStep under
`{kind:'ai', missionId, iterationId, rationale}` so policy + hooks
fire correctly
- Writes proposalIds back into plan[].proposalId + status='staged' so
other tabs and app restarts skip re-staging
- Idempotent: in-memory `processedIterations` Set + durable
proposalId marker
- Wired into (app)/+layout.svelte alongside startMissionTick
- 3 unit tests: translate server iteration → proposal, skip
already-staged, ignore browser iterations
Full pipeline now: user creates Mission in /companion/missions →
mana-ai tick picks it up → calls mana-llm → parses plan →
writes iteration → synced to webapp → staging effect creates
proposals → user approves in /todo (or any module) → task lands with
`{actor: ai, missionId, iterationId, rationale}` attribution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single source of truth for AI Workbench types shared between the webapp
(Vite/SvelteKit) and the server-side mana-ai Bun service. Prevents the
two runtimes from drifting on prompt shape or mission structure.
- `@mana/shared-ai` package:
- `actor.ts` — Actor union (user | ai | system) + helpers, mirrors the
webapp's runtime type so server-side consumers parse incoming actors
without re-declaring
- `missions/types.ts` — Mission, MissionCadence, MissionInputRef,
MissionIteration, PlanStep, MissionState. Adds optional
`iteration.source: 'browser' | 'server'` to distinguish foreground
vs server-produced iterations (groundwork for proposal write-back)
- `planner/prompt.ts` — `buildPlannerPrompt` pure function
- `planner/parser.ts` — `parsePlannerResponse` strict JSON validator
- Vitest smoke tests (2) cover prompt → parse round-trip + unknown-
tool rejection
- Webapp:
- `missions/types.ts` re-exports from shared-ai, keeps webapp-local
`MISSIONS_TABLE` constant + `planStepStatusFromProposal` bridge
- `missions/planner/{types,prompt,parser}.ts` become re-export stubs
so existing imports keep working unchanged
- Existing webapp tests (60) continue to pass — the wire code didn't
move, just its home
Next: mana-ai service imports buildPlannerPrompt/parsePlannerResponse
from shared-ai + wires mana-llm + writes iteration back as a
'source=server' row (tracked in services/mana-ai/CLAUDE.md).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the cross-device attribution loop. When another device pushes a
change with `actor: { kind: 'ai', missionId, … }`, the receiving device
now persists that attribution onto the record so the Workbench timeline
and per-module ghost badges render the same way they would on the
originating device.
- `readFieldActors()` sibling helper next to `readFieldTimestamps` for
reading the per-field actor map off a record
- `applyServerChanges`:
- Insert-new-record: stamp every field with `change.actor`, set
`__lastActor` on the whole record
- Insert-as-upsert: stamp only the winning fields (same LWW condition
as the timestamp merge), update `__lastActor` to the change actor
- Field-level update: same per-field + whole-record stamping
- Pre-actor clients (change.actor undefined) fall back to USER_ACTOR so
legacy rows still have a valid stamp
- All three paths also add the new hidden keys to their "skip" lists so
incoming payloads can't smuggle old bookkeeping fields through
With this, the full pipeline is cross-device:
Device A (AI writes) → meta.actor + __lastActor + pendingChange.actor
mana-sync (Go) → persists actor JSONB on sync_changes row
Device B (sync pull) → applyServerChanges re-stamps __lastActor +
__fieldActors from the incoming change
Device B (Workbench) → renders the AI's activity from `_events` with
correct rationale + mission context
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Matches the wire contract the Go server just learned to persist. Every
PendingChange now carries the actor through the Dexie row into the POST
payload; SyncChange on the receiving side accepts an opaque actor blob.
- `sync.ts`
- `SyncChange.actor?: Actor` on the wire type; documented as opaque +
back-compat with pre-actor clients
- `PendingChange.actor?: Actor` — the pending-changes row already gets
an actor stamped by the Dexie hook, the type now reflects it
- `isValidSyncChange` accepts actor as an object or undefined, never
asserts internal shape (the payload is opaque by design)
- Push payload includes `actor: p.actor` alongside the other fields
- `module-registry.test.ts` — `pendingProposals` added to INTERNAL_TABLES.
It's a local-only staging table that intentionally does NOT sync
(approved writes run the underlying tool, which syncs normally).
Follow-up still open: when applyServerChanges writes a record from an
incoming change, stamp `__lastActor` + `__fieldActors` from the incoming
actor so the Workbench timeline attributes cross-device writes
correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Derived-state writes should be attributed to the projection subsystem,
not to whoever triggered the upstream event. `_streakState` is local-
only today so no cross-device user-visible effect, but once any derived
table joins sync this is the only correct model.
- `markActive` and `ensureSeeded` now run under
`runAsAsync({ kind: 'system', source: 'projection' }, …)`
- Sets the pattern for future projections (DaySnapshot, correlations, …)
to follow verbatim when they start writing persistently
Closes one of the Step-1 follow-ups tracked in
COMPANION_BRAIN_ARCHITECTURE §20. Remaining:
- mana-sync Go + Postgres migration for the `actor` field
- rule-engine to wrap its future writes the same way (no writes today)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single-page view of everything the AI has done, grouped by mission
iteration. Closes the "what did the assistant actually touch today?"
question that used to require raw event-log spelunking.
- `data/ai/timeline/queries.ts`
- `useAiTimeline({ missionId?, module?, limit? })` — reactive live
query over `_events`, filtered to `actor.kind === 'ai'`. Over-fetches
by 3x and client-filters because `actor.kind` isn't indexed; cap at
500 entries keeps it cheap.
- `bucketByIteration(events)` — groups events sharing
`actor.missionId + actor.iterationId` into a single visual unit so
the rationale reads once per iteration rather than once per event.
Pure function, fully unit-tested.
- `routes/(app)/companion/workbench/+page.svelte`
- Buckets rendered chronologically with mission-link header + rationale
- Per-event row shows module + event type + payload title + deep-link
back into the module
- Module dropdown filter + `?mission=…` query-string for mission-scoped
views (linked from /companion/missions detail header)
- `/companion` sidebar + missions detail header now link to the Workbench
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the "blind Planner" gap: users can now attach context records to
a Mission, and the Runner resolves them through the existing resolver
registry before calling the Planner. The LLM sees the actual linked
note content, not just the mission objective.
- `data/ai/missions/input-index.ts` — sibling registry to input-resolvers.
Resolvers turn a ref into prompt text (Runner path); indexers list
candidates for the picker UI (create-form path). Same shape, different
direction; keeps modules decoupled from the AI layer on both ends.
- `data/ai/missions/default-resolvers.ts` — registers indexers for
notes (title + content preview, capped at 200), kontext (the singleton
doc), goals (title + progress). Co-located with the resolvers so the
two halves stay in sync.
- `components/ai/MissionInputPicker.svelte` — drop-in picker: module
selector → candidate list → chip-style selected display. Binds
directly to `MissionInputRef[]` so forms use it as a single control.
- `/companion/missions` — picker wired into the create form between
Konzept and Cadence; detail view's meta block now lists the linked
inputs so users can see what context the Planner will see.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create, review, and control AI Missions from the app. Closes the last
UX gap in the end-to-end pipeline — users no longer need the Dexie
console to drive the Runner.
- `data/ai/missions/queries.ts` — `useMissions({ state? })` live query
+ single-mission `useMission(id)`. Decryption-ready via `decryptRecords`
wrapper (no-op today, future-proof when/if Missions get added to the
crypto registry).
- `routes/(app)/companion/missions/+page.svelte`
- Inline create form: title + objective + markdown concept + cadence
picker (manual / interval-minutes / daily-hour). Weekly + cron are
wired in state but not exposed until a richer picker is worth it.
- List / detail layout, responsive to narrow viewports.
- Detail view: run-now button (invokes runMission with productionDeps),
pause/resume/complete/delete lifecycle actions, iteration history
with per-iteration feedback form.
- Uses shared-icons + scoped CSS with theme-token fallbacks.
- `routes/(app)/companion/+page.svelte` — footer nav links to
/companion/missions and /companion/rituals from the chat sidebar.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Connects the dependency-injected Runner to the real LlmOrchestrator and
drives it on a foreground tick in the app shell. Registers sensible
default input resolvers so Missions linked to notes / kontext / goals
work without per-module opt-in.
- `data/ai/missions/setup.ts`
- `productionDeps` wires `aiPlanTask` through `llmOrchestrator.run`
- `startMissionTick(intervalMs = 60_000)` kicks an immediate run then
schedules `runDueMissions` on interval. Idempotent + overlap-guarded
so a slow LLM run can't pile up ticks.
- `stopMissionTick` clears the interval for teardown / HMR.
- `data/ai/missions/default-resolvers.ts` — resolvers for notes (title +
decrypted content), kontext (singleton markdown), goals (progress
projection). Registered once when the tick starts.
- `(app)/+layout.svelte` wires startMissionTick into the idle-phase init
block alongside startEventStore / startStreakTracker / etc., and
stopMissionTick into the teardown path.
System is now end-to-end runnable in the browser: create a Mission with
cadence 'interval', wait for the tick, see proposals appear in
/todo's AiProposalInbox. Missions UI (create/edit form) still open.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Executes one iteration end-to-end: resolve Mission inputs → build the
policy-filtered tool allowlist → invoke the Planner → stage each
PlannedStep as a Proposal (or auto-run if policy says so) → finalize
the iteration with summary + status.
- `data/ai/missions/runner.ts`
- `runMission(id, deps)` runs a single iteration. Planner + stageStep
are injected so the Runner is unit-testable without a live LLM.
- `runDueMissions(now, deps)` scans for active missions past their
nextRunAt and runs each once. Safe to call on a foreground tick.
- Reuses the iteration id returned by `startIteration` so
`finishIteration` updates the same row (fixed a dup-id bug the
tests caught).
- `data/ai/missions/input-resolvers.ts` — registry: modules register a
resolver at init, Runner looks up by module name. Missing resolvers
degrade gracefully to "fewer inputs", never crash a run.
- `data/ai/missions/available-tools.ts` — exposes only tools the AI
policy rates non-`deny`. Defence-in-depth with the executor + parser.
overallStatus derivation:
0 steps → 'approved' (no-op run is valid)
all steps failed → 'failed'
any step staged (proposal id) → 'awaiting-review'
all steps ran auto → 'approved'
Planner throw is caught and recorded as a failed iteration — one bad
mission can't stall the queue.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Turns a Mission (concept + objective + linked inputs + iteration history)
into a structured plan of tool-call proposals via the shared
LlmOrchestrator.
- `data/ai/missions/planner/types.ts` — AiPlanInput, AiPlanOutput,
PlannedStep, ResolvedInput, AvailableTool
- `data/ai/missions/planner/prompt.ts` — pure builder producing system +
user messages. System prompt enforces a strict fenced-JSON contract and
lists available tools with parameter schema. User prompt injects the
mission content, resolved input records, and the last 3 iterations
(especially any userFeedback so the planner can course-correct).
- `data/ai/missions/planner/parser.ts` — strict parser with a
discriminated ParseResult union. Rejects unknown tools, missing
rationale, malformed shape. Tolerates missing optional fields.
- `llm-tasks/ai-plan.ts` — aiPlanTask LlmTask, minTier 'browser',
contentClass 'personal'. On parse failure returns an empty plan with
an explanatory summary rather than throwing, so the Runner can record
a failed iteration without killing the queue.
No Runner yet — the planner is pure (input in, plan out). Runner (next
commit) will resolve inputs from modules, invoke the task, stage each
PlannedStep as a Proposal under the AI actor, and update the Mission
iteration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Foundational entity for the AI Workbench Runner. A Mission carries the
user's standing instruction (concept + objective), references to the
modules it should draw context from, a cadence, and an append-only
history of iterations. Each iteration records the plan the AI generated
for that run plus the resulting proposal statuses and user feedback.
- `data/ai/missions/types.ts` — Mission, PlanStep, MissionIteration,
MissionCadence union (manual / interval / daily / weekly / cron)
- `data/ai/missions/cadence.ts` — pure `nextRunForCadence(cadence, from)`
used by the store on create / update / finishIteration
- `data/ai/missions/store.ts` — CRUD + lifecycle
(pause / resume / complete / archive / delete) + iteration helpers
(start / finish / addFeedback)
- `data/ai/module.config.ts` — new `ai` sync app; Missions sync
cross-device (unlike the local-only `pendingProposals`)
- `db.version(18)` adds the `aiMissions` Dexie store with indexes on
state, createdAt, nextRunAt, and [state+nextRunAt] for the Runner's
"due now" query
Iterations live inline on the Mission record — append-only, small N,
always loaded together by the Planner. No separate child table.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- COMPANION_BRAIN_ARCHITECTURE §20: Actor model, policy layer,
pendingProposals lifecycle, ghost-UI pilot, roadmap, open follow-ups,
manual test snippet
- DATA_LAYER_AUDIT §9: new Actor columns on records
(`__lastActor`, `__fieldActors`), `pendingProposals` table, write-path
diagrams for user / AI / approval, open mana-sync Go + Postgres work
- apps/mana/CLAUDE.md: short AI Workbench section with pointers + Dexie
hook now lists actor stamping
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>