From 3c3978176ad93a3f7320d71d38fd4e0350efa678 Mon Sep 17 00:00:00 2001 From: Auto-Skiller Date: Wed, 8 Jul 2026 08:08:36 +0100 Subject: [PATCH] =?UTF-8?q?feat(os-v1):=20runnable=20foundation=20?= =?UTF-8?q?=E2=80=94=20Starlette=20daemon=20(sync=20loop=20+=20SSE=20+=20c?= =?UTF-8?q?ontrols),=20=5Fos=20seed=20YAMLs,=20htmx/Alpine/Cytoscape=20das?= =?UTF-8?q?hboard=20shell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .infra/backend: yaml_io, sync engine (freshness + fill_queue + metrics + index), Starlette app (panels, toggles, SSE, /agent/say), launcher - .infra/frontend: 3-section dashboard shell, dark theme, Alpine floating windows + minimizable chat, Cytoscape maps, SSE wiring - seed: config.yaml, index.yaml, AGENTS.md, _os entity (board/runtime/missions/toolboxes/inbox/os_prompts) shaped to the new schemas - concurrency posture per ADR-0001: simple writes, git is recovery --- .infra/backend/README.md | 36 ++++ .infra/backend/app.py | 257 +++++++++++++++++++++++++++++ .infra/backend/requirements.txt | 7 + .infra/backend/sync.py | 193 ++++++++++++++++++++++ .infra/backend/yaml_io.py | 51 ++++++ .infra/frontend/app.js | 127 ++++++++++++++ .infra/frontend/index.html | 115 +++++++++++++ .infra/frontend/style.css | 92 +++++++++++ AGENTS.md | 55 ++++++ _os/os-board.md | 15 ++ _os/os-inbox.yaml | 30 ++++ _os/os-missions.yaml | 43 +++++ _os/os-runtime.yaml | 37 +++++ _os/os-toolboxes.yaml | 21 +++ _os/os_prompts.yaml | 19 +++ _os/os_prompts/00_boot-Identity.md | 34 ++++ config.yaml | 46 ++++++ index.yaml | 37 +++++ 18 files changed, 1215 insertions(+) create mode 100644 .infra/backend/README.md create mode 100644 .infra/backend/app.py create mode 100644 .infra/backend/requirements.txt create mode 100644 .infra/backend/sync.py create mode 100644 .infra/backend/yaml_io.py create mode 100644 .infra/frontend/app.js create mode 100644 .infra/frontend/index.html create mode 100644 .infra/frontend/style.css create mode 100644 _os/os_prompts/00_boot-Identity.md 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'
{prog}
' + 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 + + + + + + +
+ + +
+
Agentic OS
+
+ Window: + Sync: +
+
+ +
+
+ +
+ + + + +
+
+
+ PLANNING missions + +
+
+
+
+
+ EXECUTION missions + +
+
+
+
+ + + +
+ + + + + + + + +
+
+ + +
+
+
+ + +
\u25C9
+
+
+ Agent + +
+
+
+ + + + 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: {}