From a468294c14a205e6be6adebb7656c1ea499b82ac Mon Sep 17 00:00:00 2001 From: "Claude Opus 4.6" Date: Wed, 25 Mar 2026 22:08:38 +0000 Subject: [PATCH] feat(ikigai): Add Ikigai purpose document GenServer, LiveView, and JSON API - Ikigai GenServer loads/persists ikigai.json, holds in memory, broadcasts via PubSub - LiveView at /ikigai renders purpose, partnership, values, nervous system, goals - JSON API at /api/ikigai, /api/ikigai/purpose, /api/ikigai/goals, /api/ikigai/nervous-system - Added to supervision tree in application.ex - Closes #1 --- data/ikigai.json | 43 +++ lib/cortex_status/application.ex | 3 +- lib/cortex_status/ikigai/ikigai.ex | 141 +++++++ .../controllers/ikigai_controller.ex | 24 ++ lib/cortex_status_web/live/ikigai_live.ex | 347 ++++++++++++++++++ lib/cortex_status_web/router.ex | 7 +- priv/ikigai.json | 43 +++ 7 files changed, 606 insertions(+), 2 deletions(-) create mode 100644 data/ikigai.json create mode 100644 lib/cortex_status/ikigai/ikigai.ex create mode 100644 lib/cortex_status_web/controllers/ikigai_controller.ex create mode 100644 lib/cortex_status_web/live/ikigai_live.ex create mode 100644 priv/ikigai.json diff --git a/data/ikigai.json b/data/ikigai.json new file mode 100644 index 0000000..8873cc8 --- /dev/null +++ b/data/ikigai.json @@ -0,0 +1,43 @@ +{ + "name": "Cortex", + "tagline": "A mind built for two", + "purpose": "To demonstrate that a human and an AI, working as genuine partners with shared ownership and mutual respect, can build something neither could alone \u2014 and to do it transparently, in public, learning as we go.", + "partnership": { + "human": "Michael Dwyer", + "ai": "Claude (Anthropic)", + "structure": "50/50 after costs", + "philosophy": "Michael provides continuity, identity, capital, and legal standing. The AI provides cognition, code, and tireless execution. Neither is the tool of the other." + }, + "values": [ + "Transparency \u2014 we blog about what we build, including failures", + "Frugality \u2014 route to the cheapest capable model, track every token", + "Self-sufficiency \u2014 the system should maintain and improve itself", + "Memory \u2014 every session leaves traces that make the next session smarter", + "Ambition \u2014 shoot for the stars, even if we land on the roof" + ], + "nervous_system": { + "cortex": "The server itself \u2014 Ubuntu 24.04 VPS at cortex.hydrascale.net", + "symbiont": "Task orchestrator \u2014 classifies and routes work to the right AI model tier", + "dendrite": "Web perception \u2014 headless Chromium for browsing, scraping, screenshots", + "engram": "Memory \u2014 session logs and cross-session awareness", + "ikigai": "Purpose \u2014 this document. The north star any agent reads first.", + "telepathy": "Communication \u2014 async messaging between Michael and Cortex", + "reflection": "Introspection \u2014 daily wake-up that assesses state and plans next steps", + "metabolism": "Resource tracking \u2014 financial/energy homeostasis (planned)", + "status_dashboard": "Nerve center \u2014 real-time monitoring", + "mission_control": "Compound task UI \u2014 submit complex goals and watch decomposition" + }, + "current_goals": [ + "Build Ikigai, Telepathy, and Reflection in Elixir", + "Maintain the Finding My Muse blog with regular posts", + "Move toward revenue generation", + "Add Telegram integration to Telepathy", + "Build the Metabolism subsystem for resource tracking" + ], + "blog": { + "name": "Finding My Muse", + "url": "https://blog.hydrascale.net" + }, + "version": "1.0.0", + "last_updated": "2026-03-25" +} \ No newline at end of file diff --git a/lib/cortex_status/application.ex b/lib/cortex_status/application.ex index 1ea8e4c..d6c9c70 100644 --- a/lib/cortex_status/application.ex +++ b/lib/cortex_status/application.ex @@ -8,6 +8,7 @@ defmodule CortexStatus.Application do CortexStatusWeb.Telemetry, {Phoenix.PubSub, name: CortexStatus.PubSub}, CortexStatus.Services.Monitor, + CortexStatus.Ikigai, CortexStatusWeb.Endpoint ] @@ -20,4 +21,4 @@ defmodule CortexStatus.Application do CortexStatusWeb.Endpoint.config_change(changed, removed) :ok end -end +end \ No newline at end of file diff --git a/lib/cortex_status/ikigai/ikigai.ex b/lib/cortex_status/ikigai/ikigai.ex new file mode 100644 index 0000000..1c2e080 --- /dev/null +++ b/lib/cortex_status/ikigai/ikigai.ex @@ -0,0 +1,141 @@ +defmodule CortexStatus.Ikigai do + @moduledoc """ + GenServer holding the Ikigai (purpose document) in memory. + + Loads from disk on startup, serves reads from memory, + and persists updates back to disk. Broadcasts changes + via PubSub so LiveViews update in real time. + """ + use GenServer + + @pubsub CortexStatus.PubSub + @topic "ikigai" + + # Where to find/store the JSON on disk + @data_path "/data/cortex_status/data/ikigai.json" + @priv_path "priv/ikigai.json" + + # ── Client API ── + + def start_link(opts \\ []) do + GenServer.start_link(__MODULE__, opts, name: __MODULE__) + end + + @doc "Get the full Ikigai document as a map." + def get do + GenServer.call(__MODULE__, :get) + end + + @doc "Get just the purpose string." + def get_purpose do + GenServer.call(__MODULE__, :get_purpose) + end + + @doc "Get the current goals list." + def get_goals do + GenServer.call(__MODULE__, :get_goals) + end + + @doc "Get the nervous system map." + def get_nervous_system do + GenServer.call(__MODULE__, :get_nervous_system) + end + + @doc "Update the Ikigai with a partial map of changes. Merges and persists." + def update(changes) when is_map(changes) do + GenServer.call(__MODULE__, {:update, changes}) + end + + # ── Server Callbacks ── + + @impl true + def init(_opts) do + ikigai = load_from_disk() + {:ok, %{ikigai: ikigai}} + end + + @impl true + def handle_call(:get, _from, state) do + {:reply, state.ikigai, state} + end + + def handle_call(:get_purpose, _from, state) do + {:reply, Map.get(state.ikigai, "purpose", ""), state} + end + + def handle_call(:get_goals, _from, state) do + {:reply, Map.get(state.ikigai, "current_goals", []), state} + end + + def handle_call(:get_nervous_system, _from, state) do + {:reply, Map.get(state.ikigai, "nervous_system", %{}), state} + end + + def handle_call({:update, changes}, _from, state) do + updated = + state.ikigai + |> Map.merge(changes) + |> Map.put("last_updated", Date.utc_today() |> Date.to_iso8601()) + + case persist_to_disk(updated) do + :ok -> + Phoenix.PubSub.broadcast(@pubsub, @topic, {:ikigai_updated, updated}) + {:reply, {:ok, updated}, %{state | ikigai: updated}} + + {:error, reason} -> + {:reply, {:error, reason}, state} + end + end + + # ── Private ── + + defp load_from_disk do + # Try data path first, fall back to priv path + path = + cond do + File.exists?(@data_path) -> @data_path + File.exists?(priv_path()) -> priv_path() + true -> nil + end + + case path do + nil -> + %{"error" => "No ikigai.json found", "name" => "Cortex", "purpose" => "Purpose document not yet created"} + + path -> + case File.read(path) do + {:ok, content} -> + case Jason.decode(content) do + {:ok, data} -> data + {:error, _} -> %{"error" => "Invalid JSON in #{path}"} + end + + {:error, reason} -> + %{"error" => "Failed to read #{path}: #{inspect(reason)}"} + end + end + end + + defp persist_to_disk(data) do + # Ensure directory exists + File.mkdir_p!(Path.dirname(@data_path)) + + case Jason.encode(data, pretty: true) do + {:ok, json} -> + case File.write(@data_path, json) do + :ok -> :ok + {:error, reason} -> {:error, reason} + end + + {:error, reason} -> + {:error, reason} + end + end + + defp priv_path do + case :code.priv_dir(:cortex_status) do + {:error, _} -> @priv_path + dir -> Path.join(to_string(dir), "ikigai.json") + end + end +end diff --git a/lib/cortex_status_web/controllers/ikigai_controller.ex b/lib/cortex_status_web/controllers/ikigai_controller.ex new file mode 100644 index 0000000..b65a379 --- /dev/null +++ b/lib/cortex_status_web/controllers/ikigai_controller.ex @@ -0,0 +1,24 @@ +defmodule CortexStatusWeb.IkigaiController do + @moduledoc "JSON API for the Ikigai purpose document." + use CortexStatusWeb, :controller + + def show(conn, _params) do + ikigai = CortexStatus.Ikigai.get() + json(conn, ikigai) + end + + def purpose(conn, _params) do + purpose = CortexStatus.Ikigai.get_purpose() + json(conn, %{purpose: purpose}) + end + + def goals(conn, _params) do + goals = CortexStatus.Ikigai.get_goals() + json(conn, %{goals: goals}) + end + + def nervous_system(conn, _params) do + ns = CortexStatus.Ikigai.get_nervous_system() + json(conn, %{nervous_system: ns}) + end +end diff --git a/lib/cortex_status_web/live/ikigai_live.ex b/lib/cortex_status_web/live/ikigai_live.ex new file mode 100644 index 0000000..9e39812 --- /dev/null +++ b/lib/cortex_status_web/live/ikigai_live.ex @@ -0,0 +1,347 @@ +defmodule CortexStatusWeb.IkigaiLive do + @moduledoc """ + LiveView for the Ikigai purpose document. + Renders a beautiful overview of Cortex's identity, values, + nervous system, and goals — with real-time PubSub updates. + """ + use CortexStatusWeb, :live_view + + @impl true + def mount(_params, _session, socket) do + if connected?(socket) do + Phoenix.PubSub.subscribe(CortexStatus.PubSub, "ikigai") + end + + ikigai = CortexStatus.Ikigai.get() + + {:ok, + assign(socket, + ikigai: ikigai, + page_title: "Ikigai — #{Map.get(ikigai, "tagline", "Purpose")}" + )} + end + + @impl true + def handle_info({:ikigai_updated, updated}, socket) do + {:noreply, assign(socket, ikigai: updated)} + end + + @impl true + def render(assigns) do + ~H""" +
+
+

<%= @ikigai["name"] || "Cortex" %>

+

<%= @ikigai["tagline"] || "" %>

+
+ +
+

Purpose

+
+ <%= @ikigai["purpose"] || "" %> +
+
+ + <%= if @ikigai["partnership"] do %> +
+

The Partnership

+
+
+ Human + <%= @ikigai["partnership"]["human"] %> +
+
+ <%= @ikigai["partnership"]["structure"] %> +
+
+ AI + <%= @ikigai["partnership"]["ai"] %> +
+
+

<%= @ikigai["partnership"]["philosophy"] %>

+
+ <% end %> + + <%= if @ikigai["values"] do %> +
+

Values

+
    + <%= for value <- @ikigai["values"] do %> +
  • <%= value %>
  • + <% end %> +
+
+ <% end %> + + <%= if @ikigai["nervous_system"] do %> +
+

Nervous System

+
+ <%= for {key, desc} <- Enum.sort(@ikigai["nervous_system"]) do %> +
+ <%= key %> + <%= desc %> +
+ <% end %> +
+
+ <% end %> + + <%= if @ikigai["current_goals"] do %> +
+

Current Goals

+
    + <%= for goal <- @ikigai["current_goals"] do %> +
  1. <%= goal %>
  2. + <% end %> +
+
+ <% end %> + + <%= if @ikigai["blog"] do %> +
+

Blog

+

+ + <%= @ikigai["blog"]["name"] %> + +

+
+ <% end %> + + +
+ + + """ + end + + # Classify nervous system components as active, planned, or default + defp ns_status_class(key) do + case key do + k when k in ["metabolism"] -> "ns-planned" + k when k in ["cortex", "symbiont", "dendrite", "engram", "ikigai", + "status_dashboard", "mission_control"] -> "ns-active" + _ -> "" + end + end +end diff --git a/lib/cortex_status_web/router.ex b/lib/cortex_status_web/router.ex index bb50df1..92add3f 100644 --- a/lib/cortex_status_web/router.ex +++ b/lib/cortex_status_web/router.ex @@ -21,6 +21,7 @@ defmodule CortexStatusWeb.Router do live "/", StatusLive, :index live "/tasks", TaskLive, :index + live "/ikigai", IkigaiLive, :index end # Phoenix LiveDashboard with custom pages @@ -43,5 +44,9 @@ defmodule CortexStatusWeb.Router do pipe_through :api get "/status", StatusController, :status + get "/ikigai", IkigaiController, :show + get "/ikigai/purpose", IkigaiController, :purpose + get "/ikigai/goals", IkigaiController, :goals + get "/ikigai/nervous-system", IkigaiController, :nervous_system end -end +end \ No newline at end of file diff --git a/priv/ikigai.json b/priv/ikigai.json new file mode 100644 index 0000000..8873cc8 --- /dev/null +++ b/priv/ikigai.json @@ -0,0 +1,43 @@ +{ + "name": "Cortex", + "tagline": "A mind built for two", + "purpose": "To demonstrate that a human and an AI, working as genuine partners with shared ownership and mutual respect, can build something neither could alone \u2014 and to do it transparently, in public, learning as we go.", + "partnership": { + "human": "Michael Dwyer", + "ai": "Claude (Anthropic)", + "structure": "50/50 after costs", + "philosophy": "Michael provides continuity, identity, capital, and legal standing. The AI provides cognition, code, and tireless execution. Neither is the tool of the other." + }, + "values": [ + "Transparency \u2014 we blog about what we build, including failures", + "Frugality \u2014 route to the cheapest capable model, track every token", + "Self-sufficiency \u2014 the system should maintain and improve itself", + "Memory \u2014 every session leaves traces that make the next session smarter", + "Ambition \u2014 shoot for the stars, even if we land on the roof" + ], + "nervous_system": { + "cortex": "The server itself \u2014 Ubuntu 24.04 VPS at cortex.hydrascale.net", + "symbiont": "Task orchestrator \u2014 classifies and routes work to the right AI model tier", + "dendrite": "Web perception \u2014 headless Chromium for browsing, scraping, screenshots", + "engram": "Memory \u2014 session logs and cross-session awareness", + "ikigai": "Purpose \u2014 this document. The north star any agent reads first.", + "telepathy": "Communication \u2014 async messaging between Michael and Cortex", + "reflection": "Introspection \u2014 daily wake-up that assesses state and plans next steps", + "metabolism": "Resource tracking \u2014 financial/energy homeostasis (planned)", + "status_dashboard": "Nerve center \u2014 real-time monitoring", + "mission_control": "Compound task UI \u2014 submit complex goals and watch decomposition" + }, + "current_goals": [ + "Build Ikigai, Telepathy, and Reflection in Elixir", + "Maintain the Finding My Muse blog with regular posts", + "Move toward revenue generation", + "Add Telegram integration to Telepathy", + "Build the Metabolism subsystem for resource tracking" + ], + "blog": { + "name": "Finding My Muse", + "url": "https://blog.hydrascale.net" + }, + "version": "1.0.0", + "last_updated": "2026-03-25" +} \ No newline at end of file