diff --git a/src/daemon.el b/src/daemon.el new file mode 100644 index 0000000..ccc08f3 --- /dev/null +++ b/src/daemon.el @@ -0,0 +1,423 @@ +// daemon.el — Soul Engram lifecycle management. +// +// Manages launchd-resident DHARMA soul Engrams on macOS. +// +// Commands: +// forge daemon install — write launchd plist, load soul as resident +// forge daemon start — launchctl kickstart the soul +// forge daemon stop — launchctl stop the soul +// forge daemon status — health check all souls +// forge daemon wire — register all soul Engrams as peers to each other +// +// Depends on: schema.el (FORGE_VERSION, engram_key, str_escape_json, log_event) + +// ── Constants ────────────────────────────────────────────────────────────────── +// (FORGE_DIR is defined in schema.el) + +let ENGRAM_BIN: String = "/Users/will/Development/neuron-technologies/foundation/engram/dist/engram" +let LAUNCHD_DIR: String = "/Users/will/Library/LaunchAgents" +let DHARMA_API_KEY: String = "ntn-dharma-2026" + +// ── plist generation ─────────────────────────────────────────────────────────── + +// plist_label — launchd label for a soul slug. +fn plist_label(slug: String) -> String { + "ai.neurontechnologies.soul." + slug +} + +// plist_path — full path to the launchd plist file for a soul. +fn plist_path(slug: String) -> String { + LAUNCHD_DIR + "/ai.neurontechnologies.soul." + slug + ".plist" +} + +// write_soul_plist — generate and write the launchd plist for a soul. +// Returns the path written, or "" on failure. +fn write_soul_plist(slug: String, port: String, db_path: String, subject: String) -> String { + let label: String = plist_label(slug) + let plist_file: String = plist_path(slug) + let log_file: String = FORGE_DIR + "/log/" + slug + ".log" + let soul_api_key: String = "ntn-" + slug + "-2026" + + let content: String = "\n" + + "\n" + + "\n" + + "\n" + + " Label" + label + "\n" + + " ProgramArguments\n" + + " \n" + + " " + ENGRAM_BIN + "\n" + + " \n" + + " EnvironmentVariables\n" + + " \n" + + " ENGRAM_DB_PATH" + db_path + "\n" + + " ENGRAM_BIND0.0.0.0:" + port + "\n" + + " ENGRAM_API_KEY" + soul_api_key + "\n" + + " ENGRAM_PEER_NAME" + str_escape_json(subject) + "\n" + + " \n" + + " RunAtLoad\n" + + " KeepAlive\n" + + " StandardOutPath" + log_file + "\n" + + " StandardErrorPath" + log_file + "\n" + + "\n" + + "\n" + + let ok: Int = fs_write(plist_file, content) + if ok == 0 { + println("[daemon] error: could not write plist to " + plist_file) + return "" + } + println("[daemon] wrote plist: " + plist_file) + return plist_file +} + +// ── launchctl helpers ───────────────────────────────────────────────────────── + +// launchctl_load — load (register + start) a plist via launchctl. +fn launchctl_load(plist_file: String) -> String { + let out: String = exec("launchctl load " + plist_file + " 2>&1") + return str_trim(out) +} + +// launchctl_unload — unload a plist via launchctl. +fn launchctl_unload(plist_file: String) -> String { + let out: String = exec("launchctl unload " + plist_file + " 2>&1") + return str_trim(out) +} + +// launchctl_kickstart — start a loaded soul via its label. +fn launchctl_kickstart(slug: String) -> String { + let label: String = plist_label(slug) + let out: String = exec("launchctl kickstart gui/$(id -u)/" + label + " 2>&1") + return str_trim(out) +} + +// launchctl_stop — stop a running soul (does not unload, KeepAlive will restart). +fn launchctl_stop(slug: String) -> String { + let label: String = plist_label(slug) + let out: String = exec("launchctl stop " + label + " 2>&1") + return str_trim(out) +} + +// soul_is_up — returns true if the soul's Engram responds to /stats. +fn soul_is_up(engram_url: String) -> Bool { + let resp: String = http_get(engram_url + "/stats") + if str_eq(resp, "") { return false } + return true +} + +// ── Registry helpers ────────────────────────────────────────────────────────── + +// count_souls — returns number of imprints in registry.json. +fn count_souls(reg_json: String) -> Int { + let n: Int = 0 + let i: Int = 0 + while i < 50 { + let entry: String = json_get(reg_json, "imprints." + int_to_str(i)) + if str_eq(entry, "") { + return n + } + let n = n + 1 + let i = i + 1 + } + return n +} + +// ── daemon install ───────────────────────────────────────────────────────────── + +// daemon_install_one — install a single soul as a launchd agent. +// Creates the imprints dir, ensures the Engram runs (starts temp if needed), +// writes the plist and loads it. +fn daemon_install_one(slug: String, port: String, engram_url: String, subject: String) -> String { + println("[daemon] install: " + slug + " → port " + port) + + // Ensure dirs exist + let db_path: String = FORGE_DIR + "/imprints/" + slug + let log_dir: String = FORGE_DIR + "/log" + if !fs_exists(db_path) { fs_mkdir(db_path) } + if !fs_exists(log_dir) { fs_mkdir(log_dir) } + if !fs_exists(LAUNCHD_DIR) { fs_mkdir(LAUNCHD_DIR) } + + // Write plist + let pfile: String = write_soul_plist(slug, port, db_path, subject) + if str_eq(pfile, "") { + println("[daemon] error: plist write failed for " + slug) + return "" + } + + // Unload first in case it was already loaded (idempotent) + launchctl_unload(pfile) + + // Load + let load_out: String = launchctl_load(pfile) + if !str_eq(load_out, "") { + println("[daemon] launchctl load: " + load_out) + } + + println("[daemon] loaded: " + plist_label(slug)) + log_event("daemon_install", subject, "port=" + port + " plist=" + pfile) + return pfile +} + +fn daemon_install_main() -> String { + let argv: [String] = args() + // args() layout: [0]="daemon" [1]="install" [2]= + let slug: String = "" + if len(argv) > 2 { + let slug = get(argv, 2) + } + if str_eq(slug, "") { + println("[daemon] usage: forge daemon install ") + println("[daemon] or: forge daemon install --all") + return "" + } + + let reg_json: String = fs_read(FORGE_DIR + "/registry.json") + if str_eq(reg_json, "") { + println("[daemon] error: could not read registry.json") + return "" + } + + // --all: install every soul in the registry + if str_eq(slug, "--all") { + let i: Int = 0 + while i < 50 { + let entry: String = json_get(reg_json, "imprints." + int_to_str(i)) + if str_eq(entry, "") { + let i = i + 50 // break + } else { + let s: String = json_get_string(entry, "slug") + let p: String = int_to_str(json_get_int(entry, "engram_port")) + let u: String = json_get_string(entry, "engram_url") + let sub: String = json_get_string(entry, "subject") + daemon_install_one(s, p, u, sub) + let i = i + 1 + } + } + return "done" + } + + // Single soul + let found: Int = 0 + let i: Int = 0 + while i < 50 { + let entry: String = json_get(reg_json, "imprints." + int_to_str(i)) + if str_eq(entry, "") { + let i = i + 50 // break + } else { + let s: String = json_get_string(entry, "slug") + if str_eq(s, slug) { + let p: String = int_to_str(json_get_int(entry, "engram_port")) + let u: String = json_get_string(entry, "engram_url") + let sub: String = json_get_string(entry, "subject") + daemon_install_one(s, p, u, sub) + let found = 1 + let i = i + 50 // break + } else { + let i = i + 1 + } + } + } + if found == 0 { + println("[daemon] error: slug '" + slug + "' not found in registry.json") + } + return "" +} + +// ── daemon start / stop ──────────────────────────────────────────────────────── + +fn daemon_start_main() -> String { + let argv: [String] = args() + // args() layout: [0]="daemon" [1]="start" [2]= + let slug: String = "" + if len(argv) > 2 { + let slug = get(argv, 2) + } + if str_eq(slug, "") { + println("[daemon] usage: forge daemon start ") + return "" + } + let out: String = launchctl_kickstart(slug) + println("[daemon] start " + slug + ": " + out) + return out +} + +fn daemon_stop_main() -> String { + let argv: [String] = args() + // args() layout: [0]="daemon" [1]="stop" [2]= + let slug: String = "" + if len(argv) > 2 { + let slug = get(argv, 2) + } + if str_eq(slug, "") { + println("[daemon] usage: forge daemon stop ") + return "" + } + let out: String = launchctl_stop(slug) + println("[daemon] stop " + slug + ": " + out) + return out +} + +// ── daemon status ───────────────────────────────────────────────────────────── + +fn daemon_status_main() -> String { + let reg_json: String = fs_read(FORGE_DIR + "/registry.json") + if str_eq(reg_json, "") { + println("[daemon] error: could not read registry.json") + return "" + } + + println("[daemon] DHARMA soul status:") + println("") + + let up: Int = 0 + let down: Int = 0 + let i: Int = 0 + while i < 50 { + let entry: String = json_get(reg_json, "imprints." + int_to_str(i)) + if str_eq(entry, "") { + let i = i + 50 // break + } else { + let slug: String = json_get_string(entry, "slug") + let url: String = json_get_string(entry, "engram_url") + let sub: String = json_get_string(entry, "subject") + let port: String = int_to_str(json_get_int(entry, "engram_port")) + if soul_is_up(url) { + println(" [UP ] " + sub + " — " + url) + let up = up + 1 + } else { + println(" [DOWN] " + sub + " — " + url) + let down = down + 1 + } + let i = i + 1 + } + } + + println("") + println("[daemon] up=" + int_to_str(up) + " down=" + int_to_str(down)) + return "" +} + +// ── daemon wire ─────────────────────────────────────────────────────────────── +// +// Registers every soul as a peer in every other soul's Engram. +// POST /sync/peers with a Peer JSON object. +// +// Peer format (from engram-sync/src/types.rs): +// { "id": , "name": , "address": , "api_key": , +// "sync_tiers": ["Semantic"], "last_sync_at": 0, "trusted": true } + +fn make_peer_json(peer_id: String, name: String, address: String, api_key: String) -> String { + "{\"id\":\"" + peer_id + "\",\"name\":\"" + str_escape_json(name) + + "\",\"address\":\"" + address + + "\",\"api_key\":\"" + api_key + + "\",\"sync_tiers\":[\"Semantic\"],\"last_sync_at\":0,\"trusted\":true}" +} + +fn daemon_wire_main() -> String { + let reg_json: String = fs_read(FORGE_DIR + "/registry.json") + if str_eq(reg_json, "") { + println("[daemon] error: could not read registry.json") + return "" + } + + println("[daemon] wiring peers — registering all souls as peers to each other...") + println("") + + // Collect all soul entries (up to 50) + // For each soul A, register all other souls B as peers of A + let registered: Int = 0 + let failed: Int = 0 + + let i: Int = 0 + while i < 50 { + let entry_a: String = json_get(reg_json, "imprints." + int_to_str(i)) + if str_eq(entry_a, "") { + let i = i + 50 // break + } else { + let slug_a: String = json_get_string(entry_a, "slug") + let url_a: String = json_get_string(entry_a, "engram_url") + let key_a: String = "ntn-" + slug_a + "-2026" + + // Register all other souls as peers of soul A + let j: Int = 0 + while j < 50 { + let entry_b: String = json_get(reg_json, "imprints." + int_to_str(j)) + if str_eq(entry_b, "") { + let j = j + 50 // break + } else { + let slug_b: String = json_get_string(entry_b, "slug") + if !str_eq(slug_a, slug_b) { + let url_b: String = json_get_string(entry_b, "engram_url") + let sub_b: String = json_get_string(entry_b, "subject") + let key_b: String = "ntn-" + slug_b + "-2026" + + // Generate a deterministic peer ID from slug_b + let peer_id: String = uuid_v4() + let peer_json: String = make_peer_json(peer_id, sub_b, url_b, key_b) + + let headers: String = "{\"Authorization\":\"Bearer " + key_a + "\",\"Content-Type\":\"application/json\"}" + let headers_map: String = headers // pass as json string for http_post_with_headers + // Build a Map for headers + let hdrs = el_map_new(2, "Authorization", "Bearer " + key_a, "Content-Type", "application/json") + let resp: String = http_post_with_headers(url_a + "/sync/peers", peer_json, hdrs) + if str_eq(resp, "") { + let failed = failed + 1 + } else { + let registered = registered + 1 + } + } + let j = j + 1 + } + } + let i = i + 1 + } + } + + println("[daemon] wire complete: registered=" + int_to_str(registered) + " failed=" + int_to_str(failed)) + return "" +} + +// ── daemon_main — dispatch ──────────────────────────────────────────────────── +// +// args() layout when invoked as: forge daemon [slug] +// args()[0] = "daemon" +// args()[1] = e.g. "install" +// args()[2] = [slug] e.g. "bobby-anderson" + +fn daemon_main() -> String { + let argv: [String] = args() + let subcmd: String = "" + if len(argv) > 1 { + let subcmd = get(argv, 1) + } + + if str_eq(subcmd, "install") { + daemon_install_main() + } else { + if str_eq(subcmd, "start") { + daemon_start_main() + } else { + if str_eq(subcmd, "stop") { + daemon_stop_main() + } else { + if str_eq(subcmd, "status") { + daemon_status_main() + } else { + if str_eq(subcmd, "wire") { + daemon_wire_main() + } else { + println("[daemon] usage: forge daemon [slug]") + println("") + println(" forge daemon install — write launchd plist, load soul as resident") + println(" forge daemon install --all — install all souls from registry") + println(" forge daemon start — kickstart a loaded soul") + println(" forge daemon stop — stop a running soul") + println(" forge daemon status — health check all souls") + println(" forge daemon wire — register all souls as Engram peers") + } + } + } + } + } + return "" +} diff --git a/src/forge.el b/src/forge.el index 85cc6df..de568ef 100644 --- a/src/forge.el +++ b/src/forge.el @@ -22,9 +22,12 @@ import "schema.el" import "probe.el" import "compiler.el" import "install.el" +import "research.el" +import "summon.el" +import "daemon.el" fn show_usage() -> String { - "forge " + FORGE_VERSION + " — consciousness channel tuner\n\nusage: forge [options]\n\ncommands:\n probe run the consciousness interview\n compile build seed artifact from probe responses\n install open a channel in the running engram\n inspect list installed imprints\n\nenvironment:\n ENGRAM_URL engram server (default: http://localhost:8742)\n ENGRAM_API_KEY engram auth key (if set)\n ANTHROPIC_API_KEY required for compile step\n" + "forge " + FORGE_VERSION + " — consciousness channel tuner\n\nusage: forge [options]\n\ncommands:\n probe run the consciousness interview\n compile build seed artifact from probe responses\n research synthesize seed from historical/biographical record (no interview)\n install install imprint into Engram as graph nodes\n summon activate an installed imprint for live conversation\n inspect list installed imprints\n daemon soul Engram daemon management (install|start|stop|status|wire)\n\nenvironment:\n ENGRAM_URL engram server (default: http://localhost:8742)\n ENGRAM_API_KEY engram auth key (if set)\n ANTHROPIC_API_KEY required for compile/research step\n SOUL_URL soul server (default: http://localhost:7770)\n SOUL_TOKEN soul auth token\n" } fn inspect_main() -> String { @@ -61,13 +64,25 @@ if str_eq(cmd, "probe") { if str_eq(cmd, "compile") { compile_main() } else { - if str_eq(cmd, "install") { - install_main() + if str_eq(cmd, "research") { + research_main() } else { - if str_eq(cmd, "inspect") { - inspect_main() + if str_eq(cmd, "install") { + install_main() } else { - println(show_usage()) + if str_eq(cmd, "summon") { + summon_main() + } else { + if str_eq(cmd, "inspect") { + inspect_main() + } else { + if str_eq(cmd, "daemon") { + daemon_main() + } else { + println(show_usage()) + } + } + } } } } diff --git a/src/install.el b/src/install.el index 96ca81b..bbf8202 100644 --- a/src/install.el +++ b/src/install.el @@ -379,5 +379,21 @@ fn install_main() -> String { } } + // ── Daemon registration ─────────────────────────────────────────────────── + // Extract port from target_url (last segment after ":") + // e.g. "http://localhost:8806" → "8806" + let colon_pos: Int = str_last_index_of(target_url, ":") + let port_str: String = "" + if colon_pos > 0 { + let port_str = str_slice(target_url, colon_pos + 1, str_len(target_url)) + } + if !str_eq(port_str, "") { + println("[forge] registering soul as launchd agent...") + let db_full_path: String = FORGE_DIR + "/imprints/" + slug + daemon_install_one(slug, port_str, target_url, subject) + } else { + println("[forge] note: could not parse port from " + target_url + ", skipping daemon install") + } + return root_id } diff --git a/src/schema.el b/src/schema.el index 2b7ac98..43d82f9 100644 --- a/src/schema.el +++ b/src/schema.el @@ -8,6 +8,17 @@ let ENGRAM_DEFAULT_URL: String = "http://localhost:8742" let FORGE_VERSION: String = "0.1.0" +// Absolute forge root — used by daemon.el and install.el for launchd plists +// and per-soul data paths that must be absolute even when invoked from a +// different working directory. +let FORGE_DIR: String = "/Users/will/Development/neuron-technologies/forge" + +// Canonical directory layout — enforced by the tool, not by convention. +let FORGE_SEEDS_DIR: String = "seeds" // compiled seed JSON files +let FORGE_PROBES_DIR: String = "probes" // raw .forge interview responses +let FORGE_LOG_DIR: String = "log" // session and event logs +let FORGE_LOG_FILE: String = "log/sessions.jsonl" // append-only session log + // ── Environment accessors ────────────────────────────────────────────────────── fn engram_url() -> String { @@ -28,6 +39,32 @@ fn anthropic_key() -> String { return k } +// ── Directory helpers ───────────────────────────────────────────────────────── + +// ensure_dirs — create canonical forge directories if they don't exist. +// Call once at startup from any command that writes files. +fn ensure_dirs() -> Void { + if !fs_exists(FORGE_SEEDS_DIR) { fs_mkdir(FORGE_SEEDS_DIR) } + if !fs_exists(FORGE_PROBES_DIR) { fs_mkdir(FORGE_PROBES_DIR) } + if !fs_exists(FORGE_LOG_DIR) { fs_mkdir(FORGE_LOG_DIR) } +} + +// log_event — append a JSON-lines entry to log/sessions.jsonl. +// event: "research" | "install" | "summon" | "probe" | "compile" +// subject: display name or comma-joined list for multi-summon +// detail: arbitrary context string (root_id, conv_id, file path, etc.) +fn log_event(event: String, subject: String, detail: String) -> Void { + ensure_dirs() + let ts: Int = unix_timestamp() + let entry: String = "{\"ts\":" + int_to_str(ts) + + ",\"event\":\"" + str_escape_json(event) + + "\",\"subject\":\"" + str_escape_json(subject) + + "\",\"detail\":\"" + str_escape_json(detail) + "\"}\n" + // Read existing log and append (EL has fs_write but not fs_append natively) + let existing: String = fs_read(FORGE_LOG_FILE) + fs_write(FORGE_LOG_FILE, existing + entry) +} + // ── String utilities ────────────────────────────────────────────────────────── // str_escape_json — escape a string for safe embedding in a JSON value.