Monitoring coverage was 3 of 89 active entities. Three bugs, each hidden by
discarded errors in checkdefaults:
- writeCheck generated a fresh uuid, inserted the check entity ON CONFLICT
(slug) DO NOTHING, then wrote a check_defs row referencing it. On any
re-seed the slug already existed, the entity insert no-oped, and the FK
violated — aborting the ingest transaction and surfacing as an unrelated
failure several entities later. Re-seeding has been broken since; prod's
coverage was frozen at its first successful seed. This is what
TestSeedIngestIdempotentAndNoDuplicateEdges had been reporting.
- shortSlug truncated to the last 8 chars, so all 21 ingress routes collapsed
to ".network" and overwrote each other; service:jellyfin collided with
lxc:jellyfin.
- The ssh-script checker never read the `args` config checkdefaults wrote, so
process_check.sh always ran without its unit name and returned "unknown".
Coverage is now 75/89. Monitoring is declared per entity type in
seeds/ontology.yaml and resolved through the is-a hierarchy, so a type can say
it warrants nothing (site, lan, mesh, cluster) and never be reported as a gap.
coverageSweep raises an `unmonitored` signal only where a type declares
monitoring it lacks — 8 real gaps, no false positives.
Also:
- entity_types.attribute_schema was never ingested: the seed loader read
"attribute_schema" but the YAML says "attributes", so all 60 types stored
JSON null.
- ListExecutions ignored its declared target/action/correlation_id filters and
paginated on a non-unique target slug, dropping and repeating rows.
- started_at was captured but only written at terminal state, so a running
execution reported NULL for its whole life. The three MCP auto-run copies
wrote no timing at all; they are now one autoRun helper.
- SSH output was buffered to completion and discarded entirely on timeout.
Both sshExec copies now stream through a shared execlog sink into
execution_logs, and keep partial output when a command is cancelled.
- executions.correlation_id was a random per-execution uuid that correlated
nothing; it is now the chat session id, which is what lets the chat tail
live output.
- reversible_low had no auto-run branch despite policy declaring it
unattended. Since computeCommandRisk never returns it, the class only arises
when an agent declares it over a read_only command — so gating it penalised
candor without adding safety.
- backup-target gains a backup-freshness checker (portable find -mmin, since
the first target is on macOS), resolving its host by walking backs-up-to
backwards. The pre-deploy pg_dump is now a tracked backup target.
UI: an Executions section on entity detail with live output tailing, and
streamed output under a running `run` call in the chat timeline.
Migrations 022-024. Ops.svelte and context.ts exclude execution.output from
their refetch triggers, which would otherwise fire once a second per command.
Co-Authored-By: Claude <noreply@anthropic.com>
.prettierrc.json was missing "semi": false, so prettier wanted to add
semicolons to a codebase written without them (763 semicolon-free
statements vs. 150 with, in hand-written .ts; zero hand-written .svelte
files use them at all). That's why prettier --check failed on 249 files
— not because the code was unformatted, but because the config didn't
match the actual house style. Added "semi": false; left printWidth/etc
as configured (printWidth barely moves the failure count: 218/213/212
files at 100/120/140).
Ran `prettier --write .` with the corrected config. Verified
semantics-preserving before and after:
- eslint: 142 problems both before and after, byte-identical
- build passes, 38/38 tests pass
- token-stream diff (whitespace/semicolons/quotes normalized) on all
218 changed files: only 52 had any remaining token change, all either
trailing-comma removal (matching trailingComma: "none") or import/
ternary reflow — no semantic changes
- live smoke test: Knowledge, Tasks, Fleet map, and a chat window
(AgentTrace, markdown, Scope graph, activity rail) all render
correctly, no console errors
Most of the diff is shadcn/ui vendor files (lib/components/ui/) moving
from the CLI's own style (double quotes, tabs, semicolons) to house
style; re-running `shadcn-svelte add` on a component will need a
follow-up format pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- drop the unused KnowledgeItem type import
- the svelte/no-at-html-tags disable comment sat on the wrong line: the
multi-line Card.Description opening tag meant "next line" wasn't the
line with {@html}, so it never suppressed. Reformatted so the {@html}
is on its own line, directly after the disable comment. Sanitization
(DOMPurify with ALLOWED_TAGS: ['b']) is unchanged — this was a false
positive, verified live (search results still render <b> highlights,
no script/attribute injection).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Taskbar: the theme toggle is icon-only now, matching the Settings
button beside it. The theme name moves into the title/aria-label so
an icon-only control still has an accessible name and the current
theme stays discoverable on hover.
- Pages: Fleet, Knowledge, Learning, Ops and Signals used p-4 (or
p-4 md:p-6) while Tasks used p-2, so windows didn't line up. All now
p-2. App Store and Settings are deliberately untouched — they have no
root padding, using a full-bleed header whose border spans the window;
insetting them would break that.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- FleetMap: service-centric host -> container -> service graph replacing
the WebGL 3D force graph, with health coloring, hover-to-trace blast
radius, and click-to-open
- Desktop background: configurable CSS pattern picker in Settings ->
Appearance (8 patterns, color/fill/opacity/fade/size/rotation),
replacing the hardcoded ambient graph background
- Fix missing data-orientation/data-disabled Tailwind custom variants so
the shadcn Slider's track actually renders
- Rename "Knowledge Base" app to "Fleet"; scope its table to the same
fleet entities as the graph (compute-entity descendants + service)
instead of all entities
- Remove dead code: EntityGraph, GraphBackground, categories.ts,
MultiSelectFilter (all superseded by the above)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Windows: make floating windows fully opaque (drop backdrop-blur/color-mix
transparency), add margin around windows, reduce Overview padding to p-2.
- DataTable: fix Toolbar always rendering an empty padded bar (children
slot was always truthy regardless of actual content); split header/body
into separate tables so the scrollbar no longer overlaps the sticky
header; make sort work for derived/synthetic columns by sorting on the
column's accessor instead of a nonexistent row key.
- Overview: enable sorting on Status and Task columns via accessors.
- TaskContextPanel: give the Activity pane more height by default (Scope
30% / Activity 70%), fixing that the saved split sizes were never
actually applied to the bound Pane sizes.
- windows.ts: clamp new/resized windows to the desktop viewport so
content-heavy entity windows can't grow taller than the visible screen;
fixes a bad defaultSize.height ('30vh', an invalid non-numeric value)
that had silently left window height unconstrained.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Problem: the frontend had an implicit OS+Apps metaphor (desktop, floating
windows, an app registry) but the contract was informal — the mascot was
hardcoded into the shell, all apps were statically imported into one
800KB bundle, and there was no install/uninstall path.
Change: three phases landed.
- Phase 1 (contract + docked kind): AppDef extended with docked/noIcon
and optional geometry; the mascot registered as a docked app via a
generic DockedLayer that replaces the hardcoded <MascotLayer />;
openAppWindow branches on docked → toggleDocked; persisted docked
visibility store (absent key = visible, no APPS import to avoid a
static cycle).
- Phase 2 (lazy loading): AppDef.component is now a dynamic-import
loader; LazyApp renders with a loading skeleton; Vite code-splits
each app (main bundle 800KB→485KB); the LazyMascot wrapper is gone
since the lazy loader breaks the import cycle directly.
- Phase 3 (installable apps, local bundles): AppManifest + catalog +
installApp/uninstallApp + localStorage persistence; reactive apps
store (built-in + installed) and derived appById; App Store page;
Notes demo app; icons.ts and WindowLayer's orphan-close react to
registration so installs appear without a reload.
- Structure: data-table casing unified to PascalCase; the mislabeled
DataTable.svelte.ts (pure types, not runes) renamed to types.ts;
LazyApp colocated with its desktop-shell consumers; app-store moved
under lib/ so the dependency direction is consistent.
Risk: the app registry is now a reactive store, not a static array, so
every consumer (Desktop, DockedLayer, Taskbar, icons, windows) reads
from derived stores. Two static-cycle traps are documented in
docs/mbse/components.md §9: docked.ts must not import APPS (it would
fire a TDZ at init via the apps.ts→pages→windows.ts→here path), and
apps.ts must not statically import the mascot (the lazy loader defers
its module graph). Remote bundle loading, the /api/v1/apps endpoint,
and permission enforcement are deliberately NOT in this commit — they
are security-critical and deferred to Phase 4 with an ADR.
Verification: vitest 38/38; svelte-check + tsc clean for changed files;
eslint clean; vite build green; runtime smoke confirmed (install
Notes → icon appears → open → uninstall → icon + window gone; survives
reload). docs/mbse/components.md Component 9 and the plan updated.
Plan: plans/2026-07-21-frontend-os-apps-architecture.md
sticky top-0 on each <th> (not the <thead> itself — more consistent
sticky support across browsers for table headers) plus a background so
scrolled rows don't show through underneath it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The taskbar's gear icon reopened the full-page "Connect to Oikos" screen
even once already connected. Split that: Config.svelte stays as the
first-run/unconfigured screen; a new Settings app (windowed, like Tasks or
Operations) now handles in-session changes, with a section list (Connection,
Appearance) built to grow — future settings are one more entry, not a new
screen.
- pages/Settings.svelte: Connection (server URL/token/Authentik, reusing
config.ts + oidc.ts) and Appearance (Terracotta/Carbon picker) sections.
- apps.ts: registered as a normal desktop app.
- Taskbar's gear button now opens the Settings window; removed the
onOpenConnection prop threaded through App -> Desktop -> Taskbar, since
Settings' "Forget saved connection" (clear config + reload) replaces it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the sidebar + hash-routed page shell with a desktop metaphor:
draggable app icons, apps opening as floating wmkit windows, a centered
"What should Nomos do?" task launcher, and a bottom taskbar showing all
open windows plus a system tray.
- New app registry ($lib/apps.ts) — adding an app is one entry, nothing
else to touch.
- New desktop shell components (Desktop, WindowLayer, DesktopIcon,
Taskbar, TaskLauncher) under $lib/components/desktop-shell/.
- Icon positions are a persisted, collision-avoiding grid ($lib/stores/icons.ts).
- Window layout persists across reloads (wmkit persist), with
drag-to-maximize, F6 window cycling, and now a right-click desktop menu
(cascade/tile/show desktop/reset icons) plus Cmd/Ctrl+Z undo/redo for
window moves, resizes, and closes.
- Taskbar buttons get a hover-close and self-correct their title once a
new task's real goal is known.
- New task windows (desktop launcher and the Tasks app's "New task"
button) open as a window, not a dialog, and hand off to the real
session window once the backend assigns an id.
- Fixed a real gap along the way: GET /sessions/{id} couldn't tell
"session deleted" from "session has no messages yet" (both returned
200 with an empty list) — cmd/nomos/main.go now checks existence and
404s, so a stale/persisted task window shows "Task not found" instead
of a misleadingly empty, live-looking chat.
- Test coverage for the new pure logic (icon placement/collision
avoidance, app registry id helpers) plus a vitest matchMedia polyfill
needed to import anything touching the theme store.
Deletes the now-superseded sidebar shell, MinimizedWindowsBar, and the
standalone Chat/EntityDetail pages (folded into the window layer).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Clicking a task now opens it as a wmkit floating window (like entity
windows already do) instead of navigating away from wherever you were.
Several task windows can be open and actively streaming at once, each
fully independent — no "which one's on screen" guard needed, since
each window owns its own store bundle:
- chat.ts: chatFor(sessionId)/loadSessionChat/sendSessionMessage give
each window its own messages/streaming/connectionState, alongside
the existing singleton path the main Chat page still uses unchanged.
- workspace.ts: same split for plan/questions/touched/health-diffs
(workspaceFor/startSessionWorkspace), each with its own live-event
watermark since several windows can watch the same event stream.
- activity.ts: activityLogFor(sessionId) mirrors the global derivation.
SessionGraph.svelte, OperatorQuestion.svelte, and ActivityTimeline.svelte
were converted from store-importing to prop-driven (matching the new
ChatThread.svelte, extracted from Chat.svelte's transcript/input so both
the main page and task windows share one implementation instead of
duplicating markup/styling) so each can render either the global
"current session" or a specific window's session.
Also: minimized-window taskbar chips now cap at a max width with
middle-ellipsis truncation instead of growing unbounded, and the
window header's title/action-button row is fixed to genuinely match
heights (not just share a center point) for more robust alignment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the Fleet/Network/Identity/Knowledge category tabs (which
scoped entity fetches server-side) with a single "Types" multiselect
shared by both the table and graph views — both now fetch the whole
entity set (paginated via the new fetchAllEntities) and filter
client-side, defaulting to fleet's types. Table and graph also share
one search/highlight field instead of two separately-labeled ones.
Along the way, fixed a real bug the wider entity set exposed: the
treegrid's parent/child grouping fired one fetchGraph call per
candidate root entity, fine for the old ~50-entity fleet scope but an
ERR_INSUFFICIENT_RESOURCES flood once scoped to the full ~1700-entity
set. Replaced with a single whole-graph fetch, deriving parent/child
pairs from its edges client-side.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every place that showed entity detail (Knowledge Base's right sidebar,
the EntitySheet drawer used by Knowledge and the chat session graph,
the standalone /entity/:slug page) now opens the entity in its own
floating, draggable, resizable window instead — several can be open
side by side, and clicking a relation inside one opens another,
building up a stack. Windows are managed by one global wmkit instance
(new $lib/stores/windows.ts + $lib/components/EntityDesktop.svelte,
mounted once in App.svelte), themed with the app's own card/border/ring
tokens rather than wmkit's bundled themes (app.css).
- Delete EntitySheet.svelte (redundant) and the KnowledgeBase resizable
detail pane; row/graph-node click handlers now call
openEntityWindow(slug) instead of setting local sidebar state.
- SessionGraph (chat's "Scope" mini-graph): clicking a node opens its
window directly instead of a click-through mini-detail panel with
its own resize handle and "Full detail" button — that whole
subsystem is now dead and removed. Node highlight ring is kept
(still useful to see what you last opened) and now clears itself via
an effect watching the shared window-manager store, so closing a
window drops the highlight instead of leaving it pointing at nothing
— same fix applied to Knowledge Base's row highlight.
- Compact the entity-detail panel's padding (container + each
DetailSection) now that it's typically viewed in a small window
rather than a full-height sidebar.
- Fix KnowledgeBase's browse pane losing its flex-1/min-w-0 (and thus
full width) when the wrapping single-child div around it was removed
along with the old detail-pane split.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Knowledge Base / Fleet browsing:
- EntityTable renders as a treegrid (arbitrary depth, expand/collapse,
ARIA row/level/expanded), grouped by parent-child relationships
derived entirely from the live ontology graph (cardinality ->
direction; typeDepth specificity for ties) rather than a hardcoded
relationship list — see loadFleetGrouping in KnowledgeBase.svelte.
- Fold Services and Storage categories into Fleet (services/pools/
volumes/datasets now nest under the compute entity or pool that
provides/contains them instead of having their own browsing tabs).
- Drop `cluster` entities from Fleet browsing so a host's `located-at`
(site) relationship wins the tree-parent slot without needing a
hardcoded priority override — member-of simply has no valid target
left to point at.
- Add a "show destroyed/inactive" Switch (default off) filtering on
entity.state, replacing an always-on checkbox.
Entity detail panel:
- Split the Relations section into Outgoing/Incoming groups (relative
to the viewed entity), and scope the section's count to edges
actually incident to it rather than the whole depth-1 neighborhood.
Dev experience:
- Auto-fill the SPA's token from the dev server's own OIKOS_API_TOKEN
(vite.config.ts define + main.ts, dev-only, only when unconfigured)
so the "Connect to Oikos" prompt doesn't reappear on every reload.
- .claude/launch.json: autoPort, since port 5173 is often already
claimed by another worktree's dev server.
Adds ui/checkbox and ui/switch (bits-ui primitives, following the
existing shadcn-svelte wrapper pattern) and fetchOntology()/
RelationshipTypeDef to api.ts. Also fixes a missing types.ts import
in api.ts (ChatEvent/MessageContent) that predates this branch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5 warnings → 0:
1. ActivityTimeline.svelte:103 — replaced deprecated <svelte:component
this={icon}> with direct dynamic component rendering ({@const IconComp
= icon}<IconComp />). In Svelte 5 runes mode, components are dynamic by
default; <svelte:component> is unnecessary.
2. DetailSection.svelte:18 — 'let open = (defaultOpen)' captured only
the initial value. Changed to (false) + to sync with
defaultOpen prop changes.
3. EntitySheet.svelte:10 — 'let currentSlug = (slug)' had the same
issue. Changed to <string|null>(null) + (the was
already there, now the initial value doesn't reference the prop).
4. theme.svelte.ts:23 — 'applyClass(current)' at module level referenced a
variable, capturing only the initial value. Changed to apply the
plain storedTheme() result for initialization; setTheme() already calls
applyClass() on changes.
5. Chat.svelte:326 — unused CSS selector '.prose-chat
:global(:first-child):is(h1,h2,h3)' replaced with explicit
:global(> h1:first-child) etc. (the :first-child pseudo wasn't matching
because the scoped wrapper div is the actual first child).
Build is now warning-free.
- Plan/Activity panels: humanize step titles, richer icons, empty states
matching Scope's illustration style, pretty-printed expandable detail
- Activity log renamed to Event log; every step now expandable
- Chat: middle-truncate header title, remove redundant task-list rail and
header stat cluster (duplicated in the sidebar), simplify markdown styling
- Fix --font-mono actually being a monospace font (was aliased to DM Sans)
- Replace rotating loader-circle spinner with a smoother fading-blade Spinner
- SessionGraph entity detail panel: resizable and self-clamping against its
live container size (was overflowing into sibling sections), close button
- Dev launch config: fetch bearer token from the running api container so
`npm run dev` works against the local compose stack without a hardcoded
secret in a tracked file
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
P1: add docker compose (logs|ps|top|config|images|port|cp) to read-only
allowlist. docker compose logs was classified as config_mutation, causing
individual approval cards for read-only inspection commands.
P2: remove approval entries from activityLog. They were always status=running
and never transitioned to done (the derived store builds from tool-call
text, not execution status), causing AgentIndicator to latch onto a stale
'Approval: ...' entry and never clear — even after the session completed.
P3: remove InlineApproval from Chat.svelte. The green 'Completed in 1s on
lxc:...' boxes were noise in the chat stream. Approval UX belongs in the
Operations page (already has it via Ops.svelte), not inline in the chat.
P4: stale execution cleanup. Startup sweep (mark >1hr non-terminal as
cancelled) + 5-min periodic sweep (mark >10min non-terminal as cancelled).
98 orphaned executions accumulated from eval testing (39 running from
apt_upgrade:audit timeouts, 19 pending_approval, 3 approved).
P5: refuse second config_mutation run when an approval is already pending
for the session. Without this, the agent queues N individual approvals
before the operator can respond — confirmed in session 20757eb9 (two
approval cards for what should have been one plan-level approval).
VERSION 0.7.0 → 0.7.1
- Terracotta (light) and Carbon (dark) themes with toggle
- Inknut Antiqua headings, DM Sans body
- Dot grid background on EntityGraph and GraphBackground
- Theme-adaptive graph colors on EntityGraph
- Art Nouveau chat styling (borders, underlines, blockquote quotes)
- Bullet point styles in chat prose
- Task goal in header, rename Overview→Tasks, New Task labels
- Logo uses var(--primary) for theme awareness
- AgentIndicator now shows only during stream or when running tools exist
(not on session status=executing which never cleared)
- Activity timeline: oldest-first ordering (reads top-to-bottom naturally)
- Removed unused liveStatus derivation and currentTask import from Chat
- Plan: Phase A+B+C for activity gaps + plan-approve-once policy
- Tool calls in Activity timeline are now tagged with current plan step
- Indented entries show which step they belong to
- Step tracking via update_plan_step(status=running) tool calls
- Removed inline tool renderers from chat (health summary, fleet snapshot, etc.)
— all tool output now visible only in sidebar Activity timeline
- New ActivityTimeline: unified timeline in sidebar showing all agent actions
(goal, plan steps, tool calls, knowledge, completion) in reverse chron order
- activityLog derived store merges messages + planSteps + currentTask
- AgentIndicator stays in chat (thinking/working indicator), simplified props
- ToolCallGroup removed from chat — tools visible only in sidebar timeline
- SessionDigest replaced by ActivityTimeline
- PlanProgress restored in sidebar (conceptual steps, separate from timeline)
- New AgentIndicator component: replaces 3 separate indicators
(loading dots, ToolCallGroup summary, activity bar) with one
- Positioned as last item in message list — scrolls naturally
- Shows current tool action: 'Researching lxc:nfs-export…' etc
- Spinner during work, check on completion, X on error
- Fades out 3s after turn completes
- Activity bar, loading dots, statusLabel removed from Chat
- Continue button moved to sidebar session panel
- Activity bar moved to bottom of message list (before messagesEnd)
- Smart scroll: auto-scroll only during streaming or when near bottom
- Scrolling up pauses auto-scroll until next send
- Removed duplicate $effect block
- Plan: tool timeline in sidebar (plans/2026-07-14-tool-timeline-sidebar.md)
SPA navigates to 127.0.0.1:18901/oidc/start, passing ret URL.
Go opens browser, waits for callback, saves token, returns HTML with
<meta refresh> back to Wails app with ?desktop=1&token=TOKEN.
main.ts extracts token from URL on reload.
SPA fetches /oidc/open (returns session ID immediately), then polls
/oidc/result every 500ms. Go server opens browser in a goroutine.
Webview never leaves the Wails origin. Token is saved to keychain and
returned through the poll response.
The local HTTP server approach (fetch to 127.0.0.1) doesn't work in the
Wails webview. Simplify: use window.open() to launch OIDC in the real
browser. After authentication, the callback page at the server shows the
token. User copies and pastes into the Token tab.
Also fix: SetSize before app.Run() crashes with nil pointer — use
WebviewWindowOptions width/height directly from restored state.
The Wails runtime isn't reliably loading for IPC calls. Replace the
binding-based StartOIDCLogin with a local HTTP server on 127.0.0.1:18901:
- /oidc/login?apiUrl=... — opens system browser, waits for token
- /oidc/callback — Authentik redirect target, exchanges code
- /oidc/config?apiUrl=... — fetches OIDC provider config
- SPA detects desktop via ?desktop=1 URL param
- SPA calls localhost directly via fetch() instead of Wails IPC
ConfigService.StartOIDCLogin():
- Fetches OIDC config from the API
- Generates PKCE params
- Starts local HTTP server on 127.0.0.1:18901
- Opens system browser to Authentik
- Captures callback directly (no copy-paste)
- Exchanges code for token, saves to keychain
- Returns token to SPA → auto-connects
Config.svelte detects Wails environment and calls the binding.
12 of 33 MCP tools now render as rich inline cards instead of raw JSON:
EntityCard, HealthSummary, LXCList, EntityTable, KnowledgeResults,
BlastRadius, ChangeLog, FleetSnapshot, MetricChart.
Architecture:
- Server: annotateJSONResult() wraps queryRows with __renderer hints
- Registry: match/dispatch system maps tool names to Svelte components
- Chat: inline dispatch with 5-card limit, overflow to collapsed group
- ToolCallGroup: unmatched prop, hides when all matched, ARIA labels
Tests: 3 new Go tests for annotateJSONResult (wrap, no-op, multi-row).
Problem: the Oikos control room was browser-only — no native desktop
experience (system tray, notifications, keychain-persisted auth).
Change: add a Wails v3 thin-shell desktop app at cmd/desktop/ that embeds
the existing SPA in a webview. The Go side is ~380 lines — no bundled
server, no Postgres connection. It reads auth from the OS keychain,
injects it into the SPA on load, and the SPA talks HTTPS to the homelab
same as a browser.
Phase 1.0 — Scaffold + window:
- Embed web/dist/ into the Wails binary
- Inject window.__OIKOS_CONFIG__ with keychain-stored apiUrl + token
- 1400×900 window, min 1024×700
- System tray: Open/Quit, click toggles window
Phase 1.1 — Native shell:
- Poll /api/v1/dashboard/summary every 30s; osascript notification
when approvals or critical signals increase
- Save/restore window position to ~/.config/oikos/window.json
- EnableAutoStart/DisableAutoStart — macOS LaunchAgent plist
Phase 1.2 — Token management:
- Config.svelte calls window.wails.Call.ByName('SaveConfig') after
successful connection — persists to OS keychain
- ConfigService binds SaveConfig, ClearConfig, EnableAutoStart,
DisableAutoStart to the Wails runtime
Phase 1.3 — Auto-update:
- Poll Gitea releases API every 6h, compare semver, show dialog
- 'Check for Updates' tray menu item triggers immediate poll
Phase 1.4 — Distribution:
- macOS entitlements.plist: network client + keychain access
- .gitea/workflows/desktop.yml: CI builds macOS arm64 + Linux amd64
on 'desktop-*' / 'v*' tags, attaches artifacts to release
- Makefile: desktop (build), desktop-package (build + zip/tar.gz)
- CONTRIBUTING.md: documented desktop app + commands
Risk: low. Wails v3 alpha API may shift; the Go glue is ~380 lines and
trivially portable. The desktop app is additive — zero changes to the
existing server or SPA logic. No config mutation, no infrastructure
impact.
Verification: go build, go vet, go mod tidy all pass.
Two fixes to the new category taxonomy:
- Knowledge Base couldn't show a document/investigation/runbook's own
markdown body — knowledge_entities.content was never exposed by any
endpoint (GetEntityKnowledge answers "what knowledge references this
entity", not "what is this entity's content"). Add GET
/api/v1/knowledge/content/{id} and render it with the existing
marked+DOMPurify pipeline in a new Content section.
- The graph hid any edge whose other endpoint wasn't in the active
category, so nodes with only cross-category neighbors rendered as
disconnected dots. Queried the real relationship table: ~70% of infra
edges cross Fleet/Network/Services/Storage lines (compute+network+
software+storage+physical used to be one "infrastructure" layer).
EntityGraph now keeps 1-hop neighbors visible but dimmed instead of
hiding them, so the edges — and what they connect to — stay visible.
- categories.ts: `cognition` domain conflated true knowledge (document/
investigation/runbook, 58 entities) with operational telemetry
(execution/check/task/signal/approval/pattern/skill/classification/
feedback, 300+ entities with their own Operations/Signals/Learning
pages). Mapping the whole domain to Knowledge pulled in 245 execution
entities fanning out from ~17 compute nodes via `targets` edges — the
single biggest source of graph clutter. Knowledge now maps by type
(document/investigation/runbook only); the rest of cognition is
excluded from Knowledge Base browsing entirely.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the layer-based (Infrastructure/Governance/Cognition) browsing tabs
with a synthesized category taxonomy built from the ontology's finer-grained
`domain` field, since layer lumped unrelated entity types (an LXC and a DNS
record and a storage volume) into one bucket. Network and Fleet each span
two domains, so the table view now fans out per-domain fetches and merges,
while the graph view maps domain->category client-side. Also carries over
several detail-panel polish items (Tasks-not-raw-executions, slug URL
encoding, MultiSelectFilter) from earlier in this session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the separate Entities/Graph nav items with one Knowledge Base
page that browses all entities as either a table or a force-graph,
scoped by ontology layer (Infrastructure/Governance/Cognition), with a
resizable browse/detail split instead of a slide-over sheet.
- New KnowledgeBase.svelte: layer tabs, view toggle, resizable
browse/detail split (pattern from Chat.svelte's rail).
- EntityTable/EntityGraph extracted as presentational sub-components;
their search/filter/root/depth toolbars live in the shared page
toolbar (not the resizable pane) so they don't truncate when the
divider is dragged narrow, and both views start flush with the
detail pane for consistent height.
- EntityTable columns are sortable (slug/type/name/state/health).
- EntityDetailContent redesigned as a single-column list of
collapsible sections (DetailSection.svelte), collapsed by default
when empty; relation entries are clickable and select the entity in
the browse pane + detail pane (and drill in-place in EntitySheet
wherever it's used elsewhere in the app).
- api.ts: add layer filter to fetchEntities, add fetchEntityTypes for
client-side graph layer scoping (the graph endpoint has no layer
param).
Old hash routes (#/entities, #/graph) redirect to #/kb.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 0 of plans/2026-07-12-wails-desktop-app.md. The control-room SPA
is no longer embedded (web/embed.go deleted); it's a standalone static
build served separately (make ui / make deploy-ui). The api process
adds CORS and drops the dev-open auth bypass — every route now needs a
real bearer token, including SSE (?token= query param, EventSource
can't set headers) and api's own /agent proxy to nomos (previously
unauthenticated by omission).
nomos was an unauthenticated client of api's /mcp and approval-decision
endpoints; closing dev-open would have broken it, so it now sends
Authorization: Bearer $OIKOS_MCP_BEARER_TOKEN on every call back to api.
SPA gets a runtime config module (config.ts) and a Config.svelte
first-launch/reconfigure page, reachable afterwards via a "Connection"
entry in the sidebar footer. Every fetch() in api.ts routes through
fetchWithAuth so the same build works same-origin (browser prod, Vite
dev proxy) or cross-origin (future Wails webview, remote access).
Six gaps found against the plan and the live Caddy topology while
implementing — documented in the plan's "Plan review" section, most
notably: api's own /agent mount was never behind combinedAuth (fixed),
and production's Authentik forward-auth needs a bearer-token bypass for
API routes that this repo's Caddyfile.oikos reference copy now has, but
the real dtoro/caddy-conf deploy does not yet.
Verified live: cross-origin static SPA + API, CORS, bearer auth, SSE
query-token auth, and localStorage persistence all confirmed working
in-browser. Full Go test suite and npm run build pass with no
regressions against the pre-change baseline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Overview replaces Tasks as the default route: a centered new-task entry
with live fleet metrics, a scrollable/filterable task table, and an
ambient canvas rendering of the real entity graph (autonomous camera
drift + mouse parallax) behind it. Tasks sidebar entry is removed;
its status-bucketing logic moves to lib/tasks.ts for reuse.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Search hits and entity-knowledge hits never selected an id column, so
every KnowledgeHit.Id defaulted to the zero UUID. The frontend's keyed
{#each results as hit (hit.id)} then had all-duplicate keys, which
silently broke Svelte 5's if-block branch swap for the results panel —
search would set searched=true (Clear button appeared) but the view
never switched away from "Recently learned". Select e.id in both
queries and key the each block on hit.slug (guaranteed unique) instead.
Events, Agent, and Audit were standalone read-only pages that never
cross-referenced the entity they related to. Fold them into EntityDetail
as entity-scoped cards (Agent activity, Audit trail) alongside the
existing Signals/Executions/Knowledge cards, and give the Signals card
real Ack/Mute/Resolve actions. Signals stays a standalone page since
it's the only one with cross-entity triage value (badge count, actions).
Also fixes the underlying reason those new cards would've stayed empty:
agent_activity rows were never tagged with entity_id at insert time
(cmd/nomos/store.go, internal/mcp/server.go), even though the column
and the API filter both support it. Added a best-effort resolver that
checks common tool-arg keys (target, entity_slug, slug, ...) against
the entities table.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes the reviewed gaps: keyboard-inaccessible delete controls (SessionRail,
Entities row), case-sensitive entity filter, two competing entity-detail
navigation patterns (standardize on EntitySheet), non-clickable Overview KPI
cards, a bare button bypassing the shared Button component, inconsistent
blur-only vs live filtering, and an unenforced sanitization assumption on
search snippet HTML (now using the already-present dompurify dependency).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the chat right rail's ad-hoc Digest+Graph stack with a single
TaskContextPanel that renders the task's live working state, driven by the
always-on events stream (not the per-turn chat SSE) so it keeps updating
during server-side auto-continuation/resume:
- GoalHeader: goal + status pill (planning/executing/awaiting_input/done/
failed), sourced from the sessions list.
- PlanProgress: ordered steps with live status icons + progress bar, hydrated
via new GET /sessions/{id}/plan; clicking a step with a target opens its
EntitySheet (no fake "jump to transcript" — bits-ui Collapsible content
isn't force-mounted, so a DOM-scroll jump would silently no-op for
collapsed tool groups).
- OperatorQuestion: the pinned structured question card (prompt/why/entity
chips/option buttons/free-text), hydrated via new GET /sessions/{id}/
questions; answering POSTs to the existing answer endpoint.
- SessionGraph upgraded to a live entity panel: entity.touched pulses the
node (animated ring) and shows "Now touching <slug>"; health.changed shows
a transient diff badge for touched entities.
- SessionDigest gains a success/failure/partial outcome banner and now also
refetches when the task's status changes, not just on session switch.
Two bugs found and fixed while wiring this up:
- workspace.ts's status-refresh trigger only covered goal.set/task.status;
question.raised/answered didn't refresh the sessions list, so GoalHeader's
pill went stale after answering via the panel (resumeSession runs entirely
server-side — no client 'done' event to piggyback a refresh on). Now every
status-affecting event triggers the (debounced) refetch.
- Forgot to rebuild the nomos container after adding the /plan and
/questions endpoints, so they silently fell through to the old default GET
handler — caught via a live curl diff against the running container,
not a code read.
Verified end-to-end against the live stack: goal/plan/question all update
without a reload as the agent works; answering a question via the panel
resumes the agent and the header pill correctly flips to Executing;
entity.touched pulses the live graph.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>