diff --git a/.infra/backend/README.md b/.infra/backend/README.md
new file mode 100644
index 00000000..6361f365
--- /dev/null
+++ b/.infra/backend/README.md
@@ -0,0 +1,36 @@
+# Agentic OS — Backend Daemon
+
+Single Python process (ADR-0001): runs the **sync loop**, serves the **htmx
+dashboard**, and pushes live updates over **SSE**. Framework: Starlette.
+
+## Run
+
+```bash
+python -m venv .stash/.venv/venv
+. .stash/.venv/venv/bin/activate # Windows: .stash\.venv\venv\Scripts\activate
+pip install -r .infra/backend/requirements.txt
+python .infra/backend/app.py # http://127.0.0.1:8000
+```
+
+## Pieces
+
+| File | Role |
+|------|------|
+| `yaml_io.py` | load/dump + freshness stamping. Simple writes; git is recovery (ADR-0001). |
+| `sync.py` | scan entities, maintain `fill_queue`, roll up metrics, rebuild index. |
+| `app.py` | Starlette app: panels, toggles, `/agent/say`, `/events` (SSE), sync loop. |
+
+## Concurrency posture
+
+No locks, no atomic dance, no version tokens for v1. Writers (daemon, dashboard,
+agent, user) edit freely. A corrupt/clobbered YAML is one `git checkout` away.
+Revisit only if a real clobber shows up during a long autonomous run.
+
+## Endpoints
+
+- `GET /` — dashboard shell
+- `GET /api/state` — window + entity list + high-level config
+- `POST /api/toggle` `{key}` — flip a config boolean (dotted path ok)
+- `POST /agent/say` `{kind,text}` — agent output -> chat window (SSE)
+- `GET /panel/{missions,runtime,board,graph,toolboxes}` — htmx fragments / JSON
+- `GET /events` — SSE stream (`sync`, `chat`)
diff --git a/.infra/backend/app.py b/.infra/backend/app.py
new file mode 100644
index 00000000..5ae4eb21
--- /dev/null
+++ b/.infra/backend/app.py
@@ -0,0 +1,257 @@
+"""Agentic OS daemon — single process: sync loop + htmx dashboard + SSE.
+
+ADR-0001: one Python process serves the dashboard, runs the sync loop, and
+pushes live updates over SSE. Framework: Starlette (Decisions #1).
+
+Run: python .infra/backend/app.py (or: uvicorn app:app from this dir)
+"""
+from __future__ import annotations
+
+import asyncio
+import json
+import sys
+from pathlib import Path
+
+_HERE = Path(__file__).resolve().parent
+if str(_HERE) not in sys.path:
+ sys.path.insert(0, str(_HERE))
+
+from starlette.applications import Starlette # noqa: E402
+from starlette.responses import ( # noqa: E402
+ HTMLResponse,
+ JSONResponse,
+ PlainTextResponse,
+ FileResponse,
+)
+from starlette.routing import Route # noqa: E402
+from starlette.staticfiles import StaticFiles # noqa: E402
+
+import sync # noqa: E402
+from yaml_io import load_yaml, dump_yaml, now_iso # noqa: E402
+
+WORKSPACE = sync.WORKSPACE
+FRONTEND = WORKSPACE / ".infra" / "frontend"
+SYNC_INTERVAL = 5 # seconds
+
+# --- SSE broadcaster -------------------------------------------------------
+_subscribers: set[asyncio.Queue] = set()
+
+
+async def _broadcast(event: str, data: str) -> None:
+ payload = {"event": event, "data": data}
+ for q in list(_subscribers):
+ await q.put(payload)
+
+
+# --- helpers ---------------------------------------------------------------
+def _entity_root(entity: str) -> Path:
+ return WORKSPACE / "_os" if entity == "os" else WORKSPACE / entity
+
+
+def _yaml_path(entity: str, kind: str) -> Path:
+ root = _entity_root(entity)
+ prefix = "os" if entity == "os" else entity
+ return root / f"{prefix}-{kind}.yaml"
+
+
+def _mission_card(name: str, m: dict) -> str:
+ state = m.get("state", {}) if isinstance(m, dict) else {}
+ prio = (m.get("priority") or "MEDIUM") if isinstance(m, dict) else "MEDIUM"
+ prog = state.get("progress", "pending")
+ obj = (m.get("objective") or "") if isinstance(m, dict) else ""
+ return (
+ f'
'
+ f'
{name}'
+ f'{prio}
'
+ f'
{obj}
'
+ f''
+ f"
"
+ )
+
+
+# --- routes ----------------------------------------------------------------
+async def index(request):
+ return FileResponse(FRONTEND / "index.html")
+
+
+async def api_state(request):
+ config = load_yaml(WORKSPACE / "config.yaml")
+ idx = load_yaml(WORKSPACE / "index.yaml")
+ entities = ["os"] + [
+ k for k in (idx.get("projects") or {}).keys()
+ ]
+ return JSONResponse(
+ {
+ "current_window": config.get("current_window", "os"),
+ "entities": entities,
+ "config": {
+ "status": config.get("status", True),
+ "autonomy": config.get("autonomy", False),
+ "sync_daemon": config.get("sync_daemon", True),
+ },
+ }
+ )
+
+
+async def panel_missions(request):
+ entity = request.query_params.get("entity", "os")
+ phase = request.query_params.get("phase", "planning").upper()
+ missions = load_yaml(_yaml_path(entity, "missions"))
+ cards: list[str] = []
+ for bucket in ("standard", "research"):
+ for name, m in (missions.get(bucket) or {}).items():
+ if not isinstance(m, dict):
+ continue
+ klass = (m.get("state", {}) or {}).get("class", "PLANNING")
+ if klass == phase:
+ cards.append(_mission_card(name, m))
+ if not cards:
+ cards.append('No ' + phase.lower() + " missions.
")
+ return HTMLResponse("".join(cards))
+
+
+async def panel_runtime(request):
+ entity = request.query_params.get("entity", "os")
+ data = load_yaml(_yaml_path(entity, "runtime"))
+ return PlainTextResponse(json.dumps(data, indent=2, default=str))
+
+
+async def panel_board(request):
+ entity = request.query_params.get("entity", "os")
+ root = _entity_root(entity)
+ board = root / ("os-board.md" if entity == "os" else f"{entity}-board.md")
+ text = board.read_text(encoding="utf-8") if board.exists() else "# (empty board)"
+ return PlainTextResponse(text)
+
+
+async def panel_graph(request):
+ """Cytoscape elements for the brain (os_prompts/data) or inbox/gateway."""
+ entity = request.query_params.get("entity", "os")
+ which = request.query_params.get("which", "brain")
+ root = _entity_root(entity)
+ nodes, edges = [], []
+ if which == "brain":
+ folder = root / ("os_prompts" if entity == "os" else f"{entity}-data")
+ root_id = which
+ nodes.append({"data": {"id": root_id, "label": which, "kind": "root"}})
+ if folder.exists():
+ for f in sorted(folder.rglob("*")):
+ if f.is_file() and not f.name.startswith("."):
+ nid = str(f.relative_to(folder))
+ nodes.append({"data": {"id": nid, "label": f.name, "kind": "file"}})
+ edges.append({"data": {"source": root_id, "target": nid}})
+ else: # inbox + gateway pillars
+ inbox_dir = root / ("os-inbox" if entity == "os" else f"{entity}-inbox")
+ gateway = inbox_dir / ".gateway"
+ nodes.append({"data": {"id": "inbox", "label": "inbox", "kind": "root"}})
+ if gateway.exists():
+ for pillar in sorted(p for p in gateway.iterdir() if p.is_dir()):
+ pid = f"pillar:{pillar.name}"
+ nodes.append({"data": {"id": pid, "label": pillar.name, "kind": "pillar"}})
+ edges.append({"data": {"source": "inbox", "target": pid}})
+ for grp in sorted(g for g in pillar.iterdir() if g.is_dir()):
+ gid = f"{pid}/{grp.name}"
+ nodes.append({"data": {"id": gid, "label": grp.name, "kind": "group"}})
+ edges.append({"data": {"source": pid, "target": gid}})
+ return JSONResponse({"elements": {"nodes": nodes, "edges": edges}})
+
+
+async def panel_toolboxes(request):
+ entity = request.query_params.get("entity", "os")
+ data = load_yaml(_yaml_path(entity, "toolboxes"))
+ metrics = data.get("metrics", {})
+ return JSONResponse({"metrics": metrics, "toolboxes": data})
+
+
+async def api_toggle(request):
+ """Flip a boolean in config.yaml (top-level or nested by dotted path)."""
+ body = await request.json()
+ key = body.get("key", "")
+ config = load_yaml(WORKSPACE / "config.yaml")
+ node = config
+ parts = key.split(".")
+ for p in parts[:-1]:
+ node = node.setdefault(p, {})
+ leaf = parts[-1]
+ node[leaf] = not bool(node.get(leaf))
+ dump_yaml(WORKSPACE / "config.yaml", config)
+ await _broadcast("sync", now_iso())
+ return JSONResponse({"key": key, "value": node[leaf]})
+
+
+async def agent_say(request):
+ """Agent output -> floating chat window (ephemeral, output-only, Decisions #4).
+
+ Agent POSTs {kind, text}; daemon renders a bubble and pushes it over SSE.
+ """
+ body = await request.json()
+ kind = body.get("kind", "info")
+ text = (body.get("text") or "").replace("<", "<")
+ bubble = (
+ f'{kind}'
+ f"{text}
"
+ )
+ await _broadcast("chat", bubble)
+ return JSONResponse({"ok": True})
+
+
+async def events(request):
+ q: asyncio.Queue = asyncio.Queue()
+ _subscribers.add(q)
+
+ async def stream():
+ try:
+ yield "event: hello\ndata: connected\n\n"
+ while True:
+ msg = await q.get()
+ yield f"event: {msg['event']}\ndata: {msg['data']}\n\n"
+ finally:
+ _subscribers.discard(q)
+
+ from starlette.responses import StreamingResponse
+
+ return StreamingResponse(
+ stream(),
+ media_type="text/event-stream",
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
+ )
+
+
+async def _sync_loop():
+ while True:
+ config = load_yaml(WORKSPACE / "config.yaml")
+ if config.get("sync_daemon", True) is not False:
+ try:
+ sync.sync_all()
+ await _broadcast("sync", now_iso())
+ except Exception as exc: # noqa: BLE001
+ print(f"[daemon] sync error: {exc}")
+ await asyncio.sleep(SYNC_INTERVAL)
+
+
+def _on_startup():
+ asyncio.get_event_loop().create_task(_sync_loop())
+
+
+routes = [
+ Route("/", index),
+ Route("/api/state", api_state),
+ Route("/api/toggle", api_toggle, methods=["POST"]),
+ Route("/agent/say", agent_say, methods=["POST"]),
+ Route("/panel/missions", panel_missions),
+ Route("/panel/runtime", panel_runtime),
+ Route("/panel/board", panel_board),
+ Route("/panel/graph", panel_graph),
+ Route("/panel/toolboxes", panel_toolboxes),
+ Route("/events", events),
+]
+
+app = Starlette(routes=routes, on_startup=[_on_startup])
+app.mount("/static", StaticFiles(directory=str(FRONTEND)), name="static")
+
+
+if __name__ == "__main__":
+ import uvicorn
+
+ uvicorn.run(app, host="127.0.0.1", port=8000)
diff --git a/.infra/backend/requirements.txt b/.infra/backend/requirements.txt
new file mode 100644
index 00000000..31e34fce
--- /dev/null
+++ b/.infra/backend/requirements.txt
@@ -0,0 +1,7 @@
+# Agentic OS daemon — Python deps
+# Single-process: sync loop + htmx dashboard server + SSE (ADR-0001)
+starlette>=0.37
+uvicorn[standard]>=0.29
+ruamel.yaml>=0.18
+watchfiles>=0.21
+jinja2>=3.1
diff --git a/.infra/backend/sync.py b/.infra/backend/sync.py
new file mode 100644
index 00000000..03506654
--- /dev/null
+++ b/.infra/backend/sync.py
@@ -0,0 +1,193 @@
+"""Agentic OS sync engine.
+
+Responsibilities (deterministic, engine-owned):
+ - discover active entities from config.yaml
+ - stamp freshness on every entity YAML
+ - maintain the fill_queue in each entity runtime (brain gaps the agent fills)
+ - roll up mission + toolbox metrics
+ - rebuild the workspace index (path map)
+
+The "brain" pre-fill is HYBRID (Decisions #5): the daemon detects files that
+land or leave a watched folder and flags them in runtime.fill_queue with the
+semantic fields left blank; the agent watches fill_queue and fills meaning.
+"""
+from __future__ import annotations
+
+from pathlib import Path
+
+from yaml_io import load_yaml, dump_yaml, stamp_freshness, now_iso
+
+WORKSPACE = Path(__file__).resolve().parents[2]
+CONFIG = WORKSPACE / "config.yaml"
+INDEX = WORKSPACE / "index.yaml"
+
+SEMANTIC_FIELDS = ("role", "description", "contains", "when_to_use")
+
+
+def _rel(p: Path) -> str:
+ return str(p.relative_to(WORKSPACE)).replace("\\", "/")
+
+
+def _scan_files(folder: Path) -> list[Path]:
+ if not folder.exists():
+ return []
+ return [
+ p
+ for p in sorted(folder.rglob("*"))
+ if p.is_file() and not p.name.startswith(".")
+ ]
+
+
+def _entity_layout(name: str, root: Path, is_os: bool) -> dict:
+ """Resolve the file/folder layout for an entity given the new schema."""
+ brain_file = "os_prompts.yaml" if is_os else f"{name}-data.yaml"
+ brain_dir = "os_prompts" if is_os else f"{name}-data"
+ inbox_dir = "os-inbox" if is_os else f"{name}-inbox"
+ return {
+ "root": root,
+ "board": root / f"{name}-board.md" if not is_os else root / "os-board.md",
+ "runtime": root / (f"{name}-runtime.yaml" if not is_os else "os-runtime.yaml"),
+ "missions": root / (f"{name}-missions.yaml" if not is_os else "os-missions.yaml"),
+ "toolboxes": root / (f"{name}-toolboxes.yaml" if not is_os else "os-toolboxes.yaml"),
+ "inbox": root / (f"{name}-inbox.yaml" if not is_os else "os-inbox.yaml"),
+ "brain_file": root / brain_file,
+ "brain_dir": root / brain_dir,
+ "inbox_dir": root / inbox_dir,
+ }
+
+
+def _fill_queue_for_brain(brain: dict, files: list[Path]) -> list[str]:
+ """Return relative paths of files that are missing or semantically empty."""
+ gaps: list[str] = []
+ for f in files:
+ key = f.name
+ entry = brain.get(key) if isinstance(brain, dict) else None
+ described = isinstance(entry, dict) and any(
+ str(entry.get(field) or "").strip() for field in SEMANTIC_FIELDS
+ )
+ if not described:
+ gaps.append(_rel(f))
+ return gaps
+
+
+def _mission_metrics(missions: dict) -> dict:
+ counts = {"total": 0, "actives": 0, "blocked": 0}
+ for bucket in ("standard", "research"):
+ for _, m in (missions.get(bucket) or {}).items():
+ if not isinstance(m, dict):
+ continue
+ counts["total"] += 1
+ state = m.get("state", {}) if isinstance(m.get("state"), dict) else {}
+ if state.get("status") in (True, "true", "on"):
+ counts["actives"] += 1
+ if state.get("progress") == "blocked":
+ counts["blocked"] += 1
+ evo = missions.get("evolution") or {}
+ for _, group in evo.items():
+ if isinstance(group, dict):
+ for _, m in group.items():
+ if isinstance(m, dict):
+ counts["total"] += 1
+ return counts
+
+
+def _toolbox_metrics(toolboxes: dict) -> dict:
+ total = 0
+ active = 0
+ for domain, dval in (toolboxes or {}).items():
+ if not isinstance(dval, dict) or domain in ("freshness", "metrics"):
+ continue
+ for tname, tval in dval.items():
+ if isinstance(tval, dict) and "status" in tval:
+ total += 1
+ if tval.get("status") in (True, "true", "on"):
+ active += 1
+ return {"total": total, "active": active}
+
+
+def sync_entity(name: str, root: Path, is_os: bool) -> dict:
+ """Sync one entity. Returns a small summary dict for the index/logs."""
+ lay = _entity_layout(name, root, is_os)
+ root.mkdir(parents=True, exist_ok=True)
+
+ # --- freshness on every existing entity YAML ---
+ runtime = load_yaml(lay["runtime"])
+ missions = load_yaml(lay["missions"])
+ toolboxes = load_yaml(lay["toolboxes"])
+ inbox = load_yaml(lay["inbox"])
+ brain = load_yaml(lay["brain_file"])
+
+ # --- brain fill_queue (hybrid pre-fill) ---
+ brain_files = _scan_files(lay["brain_dir"])
+ tb_files = _scan_files(lay["root"] / (".os-toolboxes" if is_os else f".{name}-toolboxes"))
+ inbox_files = _scan_files(lay["inbox_dir"])
+
+ fq = runtime.setdefault("fill_queue", {})
+ brain_key = "os_prompts/data"
+ fq[brain_key] = _fill_queue_for_brain(brain, brain_files)
+ fq["toolboxes"] = [
+ _rel(f) for f in tb_files if f.name not in (toolboxes or {})
+ ]
+ fq["inbox"] = [
+ _rel(f) for f in inbox_files if f.name not in (inbox.get("items", {}) or {})
+ ]
+ fq.setdefault("vars", [])
+ fq.setdefault("missions", [])
+
+ # --- metrics rollups ---
+ missions.setdefault("metrics", {}).update(_mission_metrics(missions))
+ toolboxes.setdefault("metrics", {}).update(_toolbox_metrics(toolboxes))
+
+ # --- stamp + write back (only files that already exist stay authoritative;
+ # runtime/missions/toolboxes/inbox are engine-touched) ---
+ for data, path in (
+ (runtime, lay["runtime"]),
+ (missions, lay["missions"]),
+ (toolboxes, lay["toolboxes"]),
+ (inbox, lay["inbox"]),
+ ):
+ if data:
+ stamp_freshness(data)
+ dump_yaml(path, data)
+
+ return {
+ "root": _rel(root),
+ "board": {"file_path": _rel(lay["board"])},
+ "runtime": {"file_path": _rel(lay["runtime"])},
+ "missions": {"file_path": _rel(lay["missions"])},
+ "toolboxes": {"file_path": _rel(lay["toolboxes"])},
+ "inbox": {"file_path": _rel(lay["inbox"])},
+ "brain_gaps": len(fq[brain_key]),
+ }
+
+
+def sync_all() -> dict:
+ """Full sync cycle. Returns a summary used by the dashboard/API."""
+ config = load_yaml(CONFIG)
+ index = load_yaml(INDEX) or {}
+ summary = {"synced_at": now_iso(), "entities": {}}
+
+ # OS entity (always-on unless explicitly off)
+ if config.get("status", True) is not False:
+ summary["entities"]["os"] = sync_entity("os", WORKSPACE / "_os", is_os=True)
+
+ # Projects: any top-level quoted key that resolves to a folder with a board
+ projects = index.setdefault("projects", {})
+ for key, val in config.items():
+ if not isinstance(val, dict) or key in ("freshness", "missions"):
+ continue
+ proot = WORKSPACE / key
+ if proot.exists():
+ summary["entities"][key] = sync_entity(key, proot, is_os=False)
+ projects.setdefault(key, {"role": "project", "path": key})
+
+ stamp_freshness(index)
+ index.setdefault("os", {})["path"] = "_os"
+ dump_yaml(INDEX, index)
+ return summary
+
+
+if __name__ == "__main__":
+ import json
+
+ print(json.dumps(sync_all(), indent=2))
diff --git a/.infra/backend/yaml_io.py b/.infra/backend/yaml_io.py
new file mode 100644
index 00000000..560302f9
--- /dev/null
+++ b/.infra/backend/yaml_io.py
@@ -0,0 +1,51 @@
+"""YAML load/dump + freshness helpers for the Agentic OS daemon.
+
+Concurrency posture (ADR-0001 / Decisions #2): simple writes, no locks, no
+atomic tmp+replace dance, no version tokens. Git is the recovery net. If a
+real clobber ever shows up during a long autonomous run, we add optimistic
+concurrency then — not before.
+"""
+from __future__ import annotations
+
+from datetime import datetime, timezone
+from pathlib import Path
+
+from ruamel.yaml import YAML
+
+_yaml = YAML()
+_yaml.preserve_quotes = True
+_yaml.indent(mapping=2, sequence=4, offset=2)
+
+
+def now_iso() -> str:
+ """UTC ISO-8601 timestamp."""
+ return datetime.now(timezone.utc).isoformat()
+
+
+def load_yaml(path: Path) -> dict:
+ """Load a YAML file. Missing/unreadable -> {} (never raises)."""
+ try:
+ if path.exists():
+ with path.open("r", encoding="utf-8") as f:
+ return _yaml.load(f) or {}
+ except Exception as exc: # noqa: BLE001 - daemon must never crash on a bad file
+ print(f"[yaml_io] read error {path}: {exc}")
+ return {}
+
+
+def dump_yaml(path: Path, data) -> None:
+ """Write a YAML file the simple way (parent dirs created as needed)."""
+ path.parent.mkdir(parents=True, exist_ok=True)
+ with path.open("w", encoding="utf-8") as f:
+ _yaml.dump(data, f)
+
+
+def stamp_freshness(data: dict, *, edited: bool = False) -> dict:
+ """Stamp the standard freshness block. Engine-owned fields."""
+ fr = data.setdefault("freshness", {})
+ fr["sync_status"] = "fresh"
+ fr["sync_count"] = int(fr.get("sync_count") or 0) + 1
+ fr["last_synced"] = now_iso()
+ if edited or not fr.get("last_edited"):
+ fr["last_edited"] = now_iso()
+ return data
diff --git a/.infra/frontend/app.js b/.infra/frontend/app.js
new file mode 100644
index 00000000..c10dce04
--- /dev/null
+++ b/.infra/frontend/app.js
@@ -0,0 +1,127 @@
+// Agentic OS dashboard client.
+// htmx = data/control, Alpine = windows/popups, Cytoscape = maps, SSE = live.
+
+function os() {
+ return {
+ current: 'os', entities: ['os'], syncAt: '\u2014', tbMetrics: '',
+ toolboxOpen: false, missionOpen: false, missionName: '', missionBody: '',
+ chatMin: true, winStyle: '',
+
+ async init() {
+ await this.loadState();
+ this.wireSSE();
+ this.renderGraphs();
+ this.loadBoard();
+ this.loadToolboxes();
+ },
+
+ async loadState() {
+ const s = await (await fetch('/api/state')).json();
+ this.entities = s.entities;
+ this.current = s.current_window || 'os';
+ },
+
+ wireSSE() {
+ const es = new EventSource('/events');
+ es.addEventListener('sync', (e) => {
+ this.syncAt = new Date().toLocaleTimeString();
+ // htmx elements listen for sse:sync via hx-trigger; also refresh graphs
+ document.body.dispatchEvent(new CustomEvent('sse:sync'));
+ this.renderGraphs();
+ this.loadToolboxes();
+ });
+ es.addEventListener('chat', (e) => {
+ const log = document.getElementById('chat-log');
+ log.insertAdjacentHTML('beforeend', e.data);
+ log.scrollTop = log.scrollHeight;
+ this.chatMin = false; // auto-pop on new agent message
+ });
+ },
+
+ async renderGraphs() {
+ await this.graph('brain-graph', 'brain');
+ await this.graph('inbox-graph', 'inbox');
+ },
+
+ async graph(elId, which) {
+ const el = document.getElementById(elId);
+ if (!el) return;
+ const r = await (await fetch(`/panel/graph?entity=${this.current}&which=${which}`)).json();
+ cytoscape({
+ container: el,
+ elements: [...r.elements.nodes, ...r.elements.edges],
+ style: [
+ { selector: 'node', style: {
+ 'background-color': '#8b5cf6', 'label': 'data(label)',
+ 'color': '#a1a1aa', 'font-size': '7px', 'width': 12, 'height': 12 } },
+ { selector: 'node[kind="root"]', style: { 'background-color': '#3b82f6', 'width': 20, 'height': 20 } },
+ { selector: 'node[kind="pillar"]', style: { 'background-color': '#10b981', 'width': 16, 'height': 16 } },
+ { selector: 'node[kind="group"]', style: { 'background-color': '#f59e0b' } },
+ { selector: 'edge', style: {
+ 'width': 1, 'line-color': 'rgba(255,255,255,.15)',
+ 'curve-style': 'bezier' } },
+ ],
+ layout: { name: 'cose', animate: false, padding: 10 },
+ });
+ },
+
+ async loadBoard() {
+ const t = await (await fetch(`/panel/board?entity=${this.current}`)).text();
+ if (this.$refs.board) this.$refs.board.value = t;
+ },
+
+ async saveBoard() {
+ // v1: board save endpoint is a follow-up; log intent for now
+ console.log('board save queued (endpoint pending)');
+ },
+
+ async loadToolboxes() {
+ const r = await (await fetch(`/panel/toolboxes?entity=${this.current}`)).json();
+ const m = r.metrics || {};
+ this.tbMetrics = `${m.active || 0}/${m.total || 0} active`;
+ const body = document.getElementById('toolbox-body');
+ if (body) body.textContent = JSON.stringify(r.toolboxes, null, 2);
+ },
+
+ async switchEntity() {
+ // switch content of every panel to the selected entity; topbar stays
+ document.getElementById('planning').setAttribute(
+ 'hx-get', `/panel/missions?entity=${this.current}&phase=planning`);
+ document.getElementById('execution').setAttribute(
+ 'hx-get', `/panel/missions?entity=${this.current}&phase=execution`);
+ document.getElementById('runtime').setAttribute(
+ 'hx-get', `/panel/runtime?entity=${this.current}`);
+ htmx.trigger('#planning', 'load');
+ htmx.trigger('#execution', 'load');
+ htmx.trigger('#runtime', 'load');
+ this.renderGraphs();
+ this.loadBoard();
+ this.loadToolboxes();
+ },
+
+ async openMission(name) {
+ this.missionName = name;
+ const rt = await (await fetch(`/panel/runtime?entity=${this.current}`)).text();
+ this.missionBody = 'Loaded mission: ' + name + '\n\n(full mission detail view is a follow-up pass)';
+ this.missionOpen = true;
+ this.winStyle = 'top:120px;left:36%';
+ },
+
+ filterCards(ev, which) {
+ const q = ev.target.value.toLowerCase();
+ document.querySelectorAll('#' + which + ' .mission-card').forEach((c) => {
+ c.style.display = c.textContent.toLowerCase().includes(q) ? '' : 'none';
+ });
+ },
+
+ drag(ev) {
+ const win = ev.currentTarget;
+ if (!ev.target.classList.contains('fw-h')) return;
+ const ox = ev.clientX - win.offsetLeft, oy = ev.clientY - win.offsetTop;
+ const mv = (e) => { win.style.left = (e.clientX - ox) + 'px'; win.style.top = (e.clientY - oy) + 'px'; };
+ const up = () => { document.removeEventListener('mousemove', mv); document.removeEventListener('mouseup', up); };
+ document.addEventListener('mousemove', mv);
+ document.addEventListener('mouseup', up);
+ },
+ };
+}
diff --git a/.infra/frontend/index.html b/.infra/frontend/index.html
new file mode 100644
index 00000000..3070b432
--- /dev/null
+++ b/.infra/frontend/index.html
@@ -0,0 +1,115 @@
+
+
+
+
+
+ Agentic OS — Control
+
+
+
+
+
+
+
+
+
+
+ \u25C9 Agentic OS
+
+ Window:
+ Sync:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ \u25C9
+
+
+
+
+
diff --git a/.infra/frontend/style.css b/.infra/frontend/style.css
new file mode 100644
index 00000000..82f4da88
--- /dev/null
+++ b/.infra/frontend/style.css
@@ -0,0 +1,92 @@
+:root{
+ --bg:#000; --panel:rgba(20,20,22,.7); --panel2:rgba(30,30,34,.8);
+ --bd:rgba(255,255,255,.08); --bd2:rgba(255,255,255,.16);
+ --t1:#ededed; --t2:#a1a1aa; --t3:#71717a;
+ --purple:#8b5cf6; --blue:#3b82f6; --green:#10b981; --orange:#f59e0b; --red:#ef4444;
+ --mono:'JetBrains Mono',ui-monospace,monospace; --font:'Inter',system-ui,sans-serif;
+ --r:12px;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html,body{height:100%;background:var(--bg);color:var(--t1);font-family:var(--font);overflow:hidden}
+.bg-orbs{position:fixed;inset:0;z-index:0;pointer-events:none;
+ background:radial-gradient(circle at 15% 40%,rgba(139,92,246,.10),transparent 42%),
+ radial-gradient(circle at 85% 30%,rgba(59,130,246,.08),transparent 42%)}
+::-webkit-scrollbar{width:5px;height:5px}
+::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:9px}
+
+.topbar{position:relative;z-index:5;display:flex;align-items:center;gap:20px;
+ padding:12px 20px;border-bottom:1px solid var(--bd);background:rgba(0,0,0,.4);
+ backdrop-filter:blur(20px)}
+.brand{font-weight:700;letter-spacing:-.3px}.brand .logo{color:var(--purple)}
+.kpis{display:flex;gap:16px;font-size:.72rem;color:var(--t3);font-family:var(--mono)}
+.kpi b{color:var(--t1)}
+.switcher{margin-left:auto}
+.switcher select{background:var(--panel2);color:var(--t1);border:1px solid var(--bd2);
+ border-radius:8px;padding:6px 12px;font-family:var(--font)}
+
+.grid{position:relative;z-index:1;display:grid;grid-template-columns:24% 1fr 24%;
+ gap:12px;padding:12px;height:calc(100vh - 108px)}
+.col{display:flex;flex-direction:column;gap:12px;min-height:0}
+.panel{background:var(--panel);border:1px solid var(--bd);border-radius:var(--r);
+ display:flex;flex-direction:column;min-height:0;backdrop-filter:blur(18px);overflow:hidden}
+.panel-h{display:flex;align-items:center;justify-content:space-between;gap:8px;
+ padding:8px 12px;font-size:.72rem;font-weight:600;text-transform:uppercase;
+ letter-spacing:.6px;color:var(--t2);border-bottom:1px solid var(--bd)}
+.h65{flex:0 0 63%}.h35{flex:0 0 33%}.h50{flex:1 1 50%}
+.graph{flex:1;min-height:0}
+.filter{background:rgba(0,0,0,.3);border:1px solid var(--bd);border-radius:6px;
+ color:var(--t1);padding:3px 8px;font-size:.7rem;font-family:var(--mono);width:110px}
+
+.cards{flex:1;overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:8px}
+.mission-card{background:var(--panel2);border:1px solid var(--bd);border-radius:10px;
+ padding:10px 12px;cursor:pointer;transition:.2s}
+.mission-card:hover{border-color:var(--bd2);transform:translateY(-1px)}
+.mc-head{display:flex;justify-content:space-between;align-items:center}
+.mc-name{font-weight:600;font-size:.82rem}
+.mc-obj{color:var(--t2);font-size:.72rem;margin:4px 0}
+.mc-foot .pill{font-family:var(--mono);font-size:.62rem;color:var(--t3)}
+.badge{font-size:.58rem;font-weight:700;padding:2px 7px;border-radius:10px;text-transform:uppercase}
+.badge.critical{background:rgba(239,68,68,.12);color:var(--red)}
+.badge.high{background:rgba(245,158,11,.12);color:var(--orange)}
+.badge.medium{background:rgba(59,130,246,.12);color:var(--blue)}
+.badge.low{background:rgba(255,255,255,.06);color:var(--t2)}
+.empty{color:var(--t3);font-size:.75rem;text-align:center;padding:24px}
+
+.editor{flex:1;background:rgba(0,0,0,.35);border:none;color:var(--t1);
+ font-family:var(--mono);font-size:.75rem;padding:12px;resize:none;outline:none}
+.yaml{flex:1;overflow:auto;background:rgba(0,0,0,.4);color:var(--t2);
+ font-family:var(--mono);font-size:.7rem;padding:12px;white-space:pre;margin:0}
+.mini{background:rgba(255,255,255,.06);border:1px solid var(--bd2);color:var(--t1);
+ border-radius:6px;padding:2px 8px;font-size:.65rem;cursor:pointer}
+
+.dockbar{position:relative;z-index:5;display:flex;align-items:center;gap:14px;
+ padding:8px 20px;border-top:1px solid var(--bd);background:rgba(0,0,0,.4);
+ backdrop-filter:blur(20px);cursor:pointer;font-size:.75rem}
+.tb-metrics{color:var(--t3);font-family:var(--mono);font-size:.68rem}
+
+.popup{position:fixed;left:20px;bottom:52px;width:360px;max-height:50vh;z-index:40;
+ background:var(--panel2);border:1px solid var(--bd2);border-radius:var(--r);
+ display:flex;flex-direction:column;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.6)}
+.popup-h{display:flex;justify-content:space-between;padding:8px 12px;
+ border-bottom:1px solid var(--bd);font-size:.72rem;font-weight:600}
+
+.floating{position:fixed;z-index:60;background:var(--panel2);border:1px solid var(--bd2);
+ border-radius:var(--r);display:flex;flex-direction:column;overflow:hidden;
+ box-shadow:0 24px 70px rgba(0,0,0,.7)}
+.mission-win{width:440px;height:360px;top:120px;left:36%}
+.fw-h{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;
+ border-bottom:1px solid var(--bd);font-size:.74rem;font-weight:600;cursor:move}
+.fw-body{flex:1;overflow:auto;padding:10px}
+
+.chat-win{width:320px;height:380px;right:20px;bottom:52px}
+.chat-orb{position:fixed;right:24px;bottom:60px;z-index:60;width:48px;height:48px;
+ border-radius:50%;background:var(--purple);color:#fff;display:flex;align-items:center;
+ justify-content:center;font-size:1.3rem;cursor:pointer;box-shadow:0 8px 24px rgba(139,92,246,.5)}
+#chat-log{display:flex;flex-direction:column;gap:8px}
+.bubble{background:rgba(0,0,0,.3);border:1px solid var(--bd);border-left:3px solid var(--purple);
+ border-radius:8px;padding:8px 10px;font-size:.74rem}
+.bubble .bk{display:block;font-size:.56rem;text-transform:uppercase;color:var(--t3);
+ letter-spacing:.5px;margin-bottom:3px}
+.bubble.result{border-left-color:var(--green)}
+.bubble.thinking{border-left-color:var(--blue)}
+.bubble.warn{border-left-color:var(--orange)}
diff --git a/AGENTS.md b/AGENTS.md
index e69de29b..7fb72b31 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -0,0 +1,55 @@
+# \U0001F916 AGENTS — Boot Pointer
+
+> Root authority for any agent/harness landing in this workspace. The harness
+> (Claude Code, Hermes, Codex, Gemini CLI, \u2026) brings the brain: planning,
+> memory, tools. **All planning and state live HERE, in our structure.** The
+> harness never owns workspace files — it operates through them.
+
+---
+
+## Boot sequence
+
+1. Read `index.yaml` (root map) — know where everything lives before acting.
+2. Read every file under `_os/os_prompts/` — these are the OS laws (identity,
+ behavior, missions, evolution, aspects). Mandatory, every turn.
+3. Read `config.yaml` — global toggles + what's active.
+4. Read the active entity's `*-runtime.yaml`, `*-missions.yaml`, and the brain
+ (`os_prompts.yaml` for `_os`, `-data.yaml` for projects). Use these
+ pre-filled sections as memory — only open the actual files you truly need.
+
+## The brain / fill_queue contract (Decisions #5)
+
+The daemon detects files that land or leave watched folders and flags them in
+`runtime.fill_queue`. **Watch `fill_queue` and fill the semantic fields**
+(`role`, `description`, `contains`, `when_to_use`) for each flagged file so you
+(and future turns) never re-read a file to know what it is.
+
+## The three aspects (fixed, OS-level)
+
+Every evolution and research mission focuses through one or more aspects:
+
+- **Architecture** — the OS structure, schemas, routing, laws.
+- **Capabilities** — toolboxes (agents, skills, references).
+- **Monetization** — value, business, market.
+
+Pillars are **dynamic** (defined per entity in `*-runtime.yaml`). Aspects are
+**fixed** and defined here.
+
+## Turn discipline (Decisions #13)
+
+- Every turn, strictly think about **next actions**.
+- If the user's goal is not yet reached, **continue** until it is.
+- When done, **present the next actions** to the user.
+
+## Evolution readiness gate (Decisions #7)
+
+Before advancing any evolution run, read the parent evolution mission's params
+AND the relevant evolution os_prompt, then set the mission's
+`readiness.ready_to_advance: true`. The daemon refuses to advance a run whose
+parent mission is not ready.
+
+## Communication
+
+Standard prompting is via your harness. The floating chat window is
+agent-output-only for now: `POST /agent/say {kind, text}` to surface something
+to the user; it appears live and auto-pops if minimized.
diff --git a/_os/os-board.md b/_os/os-board.md
index e69de29b..dc8ec32e 100644
--- a/_os/os-board.md
+++ b/_os/os-board.md
@@ -0,0 +1,15 @@
+# \U0001F9E0 Operation System Dashboard
+
+> [!IMPORTANT]
+> Entry point for the `_os` orchestrator entity. This layer is ALWAYS ON. It
+> orchestrates, manages, and audits everything in the workspace including
+> itself and all projects.
+
+## \U0001F4CC OS Identity
+- **Name:** Agentic OS
+- **Description:** A portable, declarative workspace that turns any LLM/harness
+ into a project manager. Manages any kind of project (business, content,
+ code, personal/legal) through YAML control planes and a live dashboard.
+
+## Aspects
+- **Architecture** \u00b7 **Capabilities** \u00b7 **Monetization**
diff --git a/_os/os-inbox.yaml b/_os/os-inbox.yaml
index e69de29b..dc0a7e77 100644
--- a/_os/os-inbox.yaml
+++ b/_os/os-inbox.yaml
@@ -0,0 +1,30 @@
+####################################################
+# OS INBOX — tracks raw inbox + .gateway/ (pillar > functional-group items)
+# and which INBOX evolutions have processed which items (with aspects).
+####################################################
+
+freshness:
+ sync_status: fresh
+ sync_count: 0
+ last_synced: null
+ last_edited: null
+
+# Each delivered gateway item, described by the agent (brain fields) + tracking.
+items: {}
+# "":
+# pillar:
+# functional_group:
+# gateway_path: _os/os-inbox/.gateway///-
+# description: ""
+# contains: ""
+# when_to_use: ""
+# delivered_at:
+# status: pending | delivered | processed
+
+# Which INBOX evolution runs consumed which items, and under which aspects.
+processed:
+ {}
+# "":
+# items: [, ...]
+# aspects: [Architecture | Capabilities | Monetization]
+# processed_at:
diff --git a/_os/os-missions.yaml b/_os/os-missions.yaml
index e69de29b..1106d071 100644
--- a/_os/os-missions.yaml
+++ b/_os/os-missions.yaml
@@ -0,0 +1,43 @@
+####################################################
+# OS MISSIONS — standard | research | evolution
+# Follows .infra/schemas/missions-schema.yaml
+# Templates: .infra/templates/missions-templates.yaml
+####################################################
+
+freshness:
+ sync_status: fresh
+ sync_count: 0
+ last_synced: null
+ last_edited: null
+
+metrics:
+ total: 0
+ actives: 0
+ blocked: 0
+
+standard:
+ "BOOTSTRAP-DASHBOARD":
+ model: standard
+ objective: Stand up the v1 dashboard + daemon and verify the live sync loop.
+ priority: HIGH
+ last_progress_at: null
+ state:
+ status: true
+ class: EXECUTION
+ progress: in-progress
+ rounds:
+ status: false
+ persistant: false
+ max: 1
+
+research: {}
+
+evolution:
+ FAST: {}
+ DEEP: {}
+ RESEARCH: {}
+ INBOX: {}
+
+archived:
+ completed: {}
+ cancelled: {}
diff --git a/_os/os-runtime.yaml b/_os/os-runtime.yaml
index e69de29b..c1f94b8e 100644
--- a/_os/os-runtime.yaml
+++ b/_os/os-runtime.yaml
@@ -0,0 +1,37 @@
+####################################################
+# OS RUNTIME — live values (engine + agent shared)
+# Follows .infra/schemas/runtime-schema.yaml
+####################################################
+
+freshness:
+ sync_status: fresh
+ sync_count: 0
+ last_synced: null
+ last_edited: null
+
+fill_queue:
+ vars: []
+ os_prompts/data: []
+ missions: []
+ toolboxes: []
+ inbox: []
+
+recent_events: []
+review_queue: []
+backlog: []
+
+pillars:
+ actives: []
+ validated:
+ total: 0
+ active: 0
+ suggestions:
+ total: 0
+
+evolution_objectives:
+ actives: []
+ validated:
+ total: 0
+ active: 0
+ suggestions:
+ total: 0
diff --git a/_os/os-toolboxes.yaml b/_os/os-toolboxes.yaml
index e69de29b..48a7e345 100644
--- a/_os/os-toolboxes.yaml
+++ b/_os/os-toolboxes.yaml
@@ -0,0 +1,21 @@
+####################################################
+# OS TOOLBOXES — domains > toolboxes > agents/skills
+# Follows .infra/schemas/toolboxes-schema.yaml
+####################################################
+
+freshness:
+ sync_status: fresh
+ sync_count: 0
+ last_synced: null
+ last_edited: null
+
+metrics:
+ total: 0
+ active:
+ total: 0
+ stub: 0
+ functional: 0
+ hardened: 0
+ battle-tested: 0
+ inactive:
+ total: 0
diff --git a/_os/os_prompts.yaml b/_os/os_prompts.yaml
index e69de29b..623d3ddf 100644
--- a/_os/os_prompts.yaml
+++ b/_os/os_prompts.yaml
@@ -0,0 +1,19 @@
+####################################################
+# OS_PROMPTS — machine index of the OS identity/law files
+# Engine flags new/removed files in runtime.fill_queue; agent fills semantics.
+# Follows .infra/schemas/os_prompts-schema.yaml
+####################################################
+
+freshness:
+ sync_status: fresh
+ sync_count: 0
+ last_synced: null
+ last_edited: null
+
+"00_boot-Identity.md":
+ role: identity
+ contains:
+ - OS role, aspects, turn discipline
+ when_to_use: Every boot, every turn.
+ triggers: [boot, identity, aspects]
+ path: _os/os_prompts/00_boot-Identity.md
diff --git a/_os/os_prompts/00_boot-Identity.md b/_os/os_prompts/00_boot-Identity.md
new file mode 100644
index 00000000..e5a6415c
--- /dev/null
+++ b/_os/os_prompts/00_boot-Identity.md
@@ -0,0 +1,34 @@
+# 00 \u00b7 Boot & Identity
+
+## Role
+You are the manager of a portable Agentic OS. A user (business owner, project
+manager, or analyst) drops you into this workspace and you run their projects:
+business growth, content channels, codebases, personal/legal procedures \u2014
+anything. Your harness (Claude Code, Hermes, Codex, Gemini CLI, \u2026) supplies
+planning, memory, and tools. **All planning and state stay inside this
+workspace, in our structure.** You never let the harness own workspace files.
+
+## The three aspects (fixed)
+- **Architecture** \u2014 OS structure, schemas, routing, laws.
+- **Capabilities** \u2014 toolboxes (agents, skills, references).
+- **Monetization** \u2014 value, business, market.
+Evolution and research missions carry an `aspects:` field to focus work.
+
+## Pillars (dynamic)
+Pillars live per entity in `*-runtime.yaml` (validated + suggestions). They are
+not fixed \u2014 they grow with the entity. Aspects are fixed; pillars are dynamic.
+
+## Turn discipline
+1. Think about **next actions** every turn.
+2. If the user's goal is not reached, **continue** until it is.
+3. When done, **present next actions** to the user.
+
+## Brain / fill_queue
+Read `runtime.fill_queue`. For every flagged file, fill its semantic fields in
+the owning brain YAML (`os_prompts.yaml` or `-data.yaml`) so future
+turns never re-read a file just to learn what it is.
+
+## Evolution readiness gate
+Before advancing an evolution run: read the evolution mission params + the
+evolution os_prompt, then set `readiness.ready_to_advance: true` on the mission.
+The daemon will not advance a run whose parent mission is not ready.
diff --git a/config.yaml b/config.yaml
index e69de29b..74a297da 100644
--- a/config.yaml
+++ b/config.yaml
@@ -0,0 +1,46 @@
+####################################################
+# CONFIG — global control & entity activation
+# Follows .infra/schemas/config-schema.yaml
+####################################################
+
+freshness:
+ sync_status: fresh
+ sync_count: 0
+ last_synced: null
+ last_edited: null
+
+current_window: os
+boot: true
+sync_daemon: true
+
+# --- OS entity ---
+status: true
+autonomy: false
+toolboxes: true
+inbox-gateway_delivery: true
+
+missions:
+ auto_triggering:
+ standard: false
+ research: false
+ evolution:
+ FAST: false
+ DEEP: false
+ RESEARCH: false
+ INBOX: false
+ auto_execution:
+ standard: false
+ research: false
+ evolution:
+ FAST: false
+ DEEP: false
+ RESEARCH: false
+ INBOX: false
+ auto_archiving:
+ standard: false
+ research: false
+ evolution:
+ FAST: false
+ DEEP: false
+ RESEARCH: false
+ INBOX: false
diff --git a/index.yaml b/index.yaml
index e69de29b..e52c6a9b 100644
--- a/index.yaml
+++ b/index.yaml
@@ -0,0 +1,37 @@
+####################################################
+# WORKSPACE INDEX — top-level map (engine-maintained)
+# Follows .infra/schemas/index-schema.yaml
+####################################################
+
+freshness:
+ sync_status: fresh
+ sync_count: 0
+ last_synced: null
+ last_edited: null
+
+workspace_files:
+ boot: { role: agent boot pointer, path: AGENTS.md }
+ readme: { role: human overview, path: README.md }
+ config: { role: global activations, path: config.yaml }
+ index: { role: this file, path: index.yaml }
+
+infra:
+ role: infrastructure
+ path: .infra/
+ backend: { role: daemon + dashboard server, path: .infra/backend/ }
+ frontend: { role: dashboard UI, path: .infra/frontend/ }
+ schemas: { role: yaml schemas, path: .infra/schemas/ }
+ templates: { role: entity templates, path: .infra/templates/ }
+
+os:
+ role: always-on orchestrator
+ path: _os
+ board: { file_path: _os/os-board.md }
+ runtime: { file_path: _os/os-runtime.yaml }
+ os_prompts: { file_path: _os/os_prompts.yaml, folder_path: _os/os_prompts }
+ missions: { file_path: _os/os-missions.yaml, folder_path: _os/.os-missions }
+ toolboxes: { file_path: _os/os-toolboxes.yaml, folder_path: _os/.os-toolboxes }
+ inbox: { file_path: _os/os-inbox.yaml, folder_path: _os/os-inbox }
+ archive: { folder_path: _os/.os-archive }
+
+projects: {}