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>
First pilot of the AI Workbench ghost-state pattern. A reusable
`<AiProposalInbox module="todo" />` component renders pending proposals
for a given module as dashed-outline ghost cards above the real content —
zero UI when the AI is idle, approve / reject inline when it's not.
- `data/ai/proposals/queries.ts` — reactive `useAiProposals` live query
with module / status / missionId filters. Module filter resolves via
the tool registry so each proposal auto-routes to the right page.
- `components/ai/AiProposalInbox.svelte` — the drop-in inbox component.
Shows tool description + params + AI rationale; approve runs the
original intent under the AI actor context (preserving attribution),
reject stores the row with status=rejected for the next planner pass.
- Wired into /todo for the pilot. Other modules opt in by adding one
line once their tools land in DEFAULT_AI_POLICY.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The prior dance (93bb94a12 drop .ts, bb278fb3c switch to .js) kept
breaking one consumer or the other:
- bare specifiers (no extension) satisfied svelte-check but broke the
Node ESM loader invoked via @tailwindcss/node during SSR — SSR of
every (app) route 500'd with ERR_MODULE_NOT_FOUND on 'theme'.
- .js extensions satisfied svelte-check and Vite but still broke the
Tailwind loader, because the files on disk are .ts — Node ESM walks
the actual filesystem and can't rewrite .js → .ts the way tsc does
at type-check time.
Flip the web app's tsconfig to "allowImportingTsExtensions": true and
put the .ts extensions back. tsc now accepts the imports, and Node's
loader finds the real file on disk. No build step, no emit, and the
shared-types package stays a pure source-only TS workspace.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the staging layer that turns AI-attributed tool calls into user-reviewed
proposals instead of direct writes.
- `data/ai/policy.ts` — per-tool AiPolicy (`auto` | `propose` | `deny`) with
module-level defaults and a global fallback. `user` and `system` actors
always bypass (they ARE the decision / are trusted subsystems).
- `data/ai/proposals/` — Proposal + Intent types, store with
create/list/approve/reject/expire. Proposals are local-only (do NOT
sync); the approved write syncs through the normal module path.
- `tools/executor.ts` routes by actor+policy: `auto` runs directly under
`runAsAsync(actor, ...)`, `propose` stages a Proposal carrying rationale
+ mission metadata, `deny` refuses. `executeToolRaw` bypasses the policy
gate — used only on the approval path where consent already exists.
Default policy is conservative: read-only and append-only self-state
(log_drink, log_meal) auto-execute; everything that mutates user-visible
records defaults to propose.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extends the creating/updating hooks to capture the ambient actor
synchronously and freeze it onto every write:
- `__lastActor` on each record (whole-record attribution for Workbench badges)
- `__fieldActors` parallel to `__fieldTimestamps` (field-level attribution
for inline diff rendering — e.g. "AI changed due date, user changed title")
- `actor` on `_pendingChanges` rows so mana-sync + cross-device views can
distinguish AI- vs user-initiated writes
Also adds `kontextDoc` to v17 (missing from schema while module was live)
alongside the new `pendingProposals` table for staged AI intents.
Actor is captured in-hook rather than at emit time because
`trackPendingChange` is deferred via setTimeout and would otherwise lose
ambient context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a discriminated Actor union (user | ai | system) threaded through
the event pipeline so downstream consumers can distinguish human writes from
AI-initiated ones and derived subsystem writes.
- `EventMeta.actor: Actor` is required (no legacy fallback — pre-launch)
- `emitDomainEvent` takes an options bag `{ actor?, causedBy? }`; falls back
to the ambient actor set by `runAs` / `runAsAsync`
- `runAs` / `runAsAsync` pin the actor at defined boundaries (tool executor,
mission runner, projection dispatcher) — primitives capture synchronously
so ambient context is never SoT past the write moment
Foundation for the AI Workbench. Follow-up: mana-sync server must accept
and persist `actor` in pending-change payloads.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New Mana module "Kontext" — one editable markdown document per user,
displayed as a workbench page. Toggle between rendered preview and a
raw textarea editor (Cmd/Ctrl+E); debounced autosave 500 ms.
Content is encrypted at rest (new `kontextDoc` table, singleton row).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The .mana backup parser in src/lib/data/backup/format.ts imports
inflateRaw from pako but the package was never declared. The
production Vite build fails to resolve it — pnpm let it through
locally only because some other workspace dep hoists pako into
node_modules.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- mail/ListView: add a11y_click_events_have_key_events ignore to match
the existing a11y_no_static_element_interactions suppression
- sleep/MorningLog + companion/CompanionChat: mark intentional
initial-value state reads with state_referenced_locally ignore
- goals/GoalEditor: add tabindex="-1" to the dialog role element
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- shared-types/index.ts: drop .ts import extensions (web-app tsconfig
has no allowImportingTsExtensions; bundler resolution handles it)
- backup/format.test.ts: narrow buildZip return to Uint8Array<ArrayBuffer>
so the Blob() constructor accepts it under strict lib.dom
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Local-first module with meditatePresets/Sessions/Settings tables, hub
ListView with stats + recent sessions, and SessionPlayer with
BreathingCircle + MoodPicker. Route at /meditate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds four audit scripts (module health, inter-module coupling, per-function
cognitive complexity, D3 treemap) with generated reports under docs/ and
an iframe-embedded workbench app at /admin/complexity. Reports regenerate
weekly via the module-health GitHub Action.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
eventstream was confusingly branded "Events" in the app registry,
colliding with the real events calendar module. Renamed to activity
(DE: Aktivität) since it's a live activity feed across all modules.
cycles -> period (DE: Periode) makes the menstrual-tracking module
self-describing. Tables cycles/cycleDayLogs/cycleSymptoms renamed to
periods/periodDayLogs/periodSymptoms; field cycleId -> periodId;
TimeBlockType 'cycle' -> 'period'; domain event CycleDayLogged ->
PeriodDayLogged. Generic "cycle" usages (billing, lifecycle, breath,
bicycle, import cycles) left untouched.
Constant disambiguation: prior DEFAULT_PERIOD_LENGTH (bleeding days)
renamed to DEFAULT_BLEEDING_DAYS; prior DEFAULT_CYCLE_LENGTH (28d full
cycle) is now DEFAULT_PERIOD_LENGTH.
Pre-launch, no data migration needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moves the BYOK key CRUD from the standalone /settings/ai-keys subpage
directly under the new BYOK tier card in the main AI settings section.
Users now manage keys in-context where they toggle the tier.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>