using Astralis; using Inversion; using InvercargillSqlInversion; using Spry; using Spry.Authentication; using Statum; using ClankerSaddle; using ClankerSaddle.Api; using ClankerSaddle.Data; using ClankerSaddle.Services; int main(string[] args) { int port = args.length > 1 ? int.parse(args[1]) : 8080; print("ClankerSaddle - LLM task orchestration platform\n"); print(@"Website starting on port $(port)\n"); try { var application = new WebApplication(port); application.use_compression(); application.add_module(); application.add_module(); var db_path = Environment.get_variable("SPRY_DB_PATH") ?? ""; if (db_path.strip().length == 0) { db_path = "./clanker-saddle.sqlite"; } var db = application.container.configure_with(); db.register_connection(@"sqlite://$(db_path)"); // SQLite serialisation posture (the flake family where a SELECT // stepping against a concurrent write lock — the test harness's // fixture writes share this file — returned SQLITE_BUSY, which // the ORM's lazy enumerator reports as end-of-rows: an EMPTY // result for data that exists): WAL readers never block on (and // are never blocked by) writers, and the busy timeout absorbs // the remaining writer-writer window. try { var connection_scope = application.container.create_scope(); var connection = connection_scope.resolve(); connection.execute("PRAGMA journal_mode = WAL"); connection.execute("PRAGMA busy_timeout = 5000"); connection_scope.release(); } catch (Error pragma_error) { print(@"[ClankerSaddle] sqlite pragmas failed: $(pragma_error.message)\n"); } db.add_migration(); db.add_migration(); db.add_migration(); db.add_migration(); db.add_migration(); db.add_migration(); db.add_migration(); db.add_migration(); db.add_migration(); db.add_migration(); db.add_entity(Project.entity_mapping); db.add_entity(ProjectLink.entity_mapping); db.add_entity(TaskEntity.entity_mapping); db.add_entity(TaskComment.entity_mapping); db.add_entity(TaskBlock.entity_mapping); db.add_entity(EventRecord.entity_mapping); db.add_entity(Saddle.entity_mapping); db.add_entity(Session.entity_mapping); db.add_entity(Rule.entity_mapping); db.add_entity(RuleProject.entity_mapping); db.add_entity(TaskRule.entity_mapping); db.add_entity(ProjectNote.entity_mapping); db.add_entity(ProjectUnderstanding.entity_mapping); db.add_entity(Discovery.entity_mapping); db.add_entity(Capability.entity_mapping); db.add_entity(SaddleCapability.entity_mapping); db.add_entity(TaskCapability.entity_mapping); db.add_entity(PushSubscription.entity_mapping); db.add_entity(TaskNotifyPref.entity_mapping); db.add_entity(Goal.entity_mapping); db.add_entity(GoalLink.entity_mapping); db.add_entity(AdvisorChat.entity_mapping); db.add_entity(AdvisorMessage.entity_mapping); db.add_entity(AdvisorSuggestion.entity_mapping); db.add_entity(Stack.entity_mapping); db.add_entity(StackProject.entity_mapping); db.add_entity(Note.entity_mapping); db.add_entity(ModelConfig.entity_mapping); db.migrate_on_startup(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_singleton(); application.add_singleton(); application.add_singleton(); application.add_singleton(); application.add_singleton(); application.add_singleton(); application.add_singleton(); application.add_singleton(); // Web Push (plan 11 task E): the VAPID keypair loads or // generates + persists BEFORE the server accepts requests, so // every subscriber sees the same public key from the first // page render. var key_scope = application.container.create_scope(); key_scope.resolve().ensure_keys_from_config(); key_scope.release(); var statum = application.configure_with(); statum.global(StatePulse.SLOT_TYPE, new ClankerSaddle.Services.PulsePublic()); // spry:pages-begin statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); // spry:pages-end // spry:actions-begin statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); // spry:actions-end // spry:resources-begin statum.add_resource(); statum.add_resource(); statum.add_resource(); statum.add_resource(); statum.add_resource(); // spry:resources-end // spry:auth-begin // spry:auth-end application.add_endpoint( new EndpointRoute("/api/saddle/next-task", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/chunks", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/results", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/note", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/understanding", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/understanding", Method.GET)); application.add_endpoint( new EndpointRoute("/api/saddle/discovery", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/discoveries", Method.GET)); application.add_endpoint( new EndpointRoute("/api/saddle/discovery/update", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/task/create", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/task/delete", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/task/enqueue", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/tasks", Method.GET)); application.add_endpoint( new EndpointRoute("/api/saddle/canon", Method.GET)); application.add_endpoint( new EndpointRoute("/api/saddle/canon/{slug}", Method.GET)); application.add_endpoint( new EndpointRoute("/api/saddle/ethos", Method.GET)); application.add_endpoint( new EndpointRoute("/api/saddle/task/{id}", Method.GET)); application.add_endpoint( new EndpointRoute("/api/saddle/capabilities", Method.GET)); application.add_endpoint( new EndpointRoute("/api/saddle/goals", Method.GET)); application.add_endpoint( new EndpointRoute("/tasks/{task_id}/events", Method.GET)); application.add_endpoint( new EndpointRoute("/tasks/{task_id}/events/stream", Method.GET)); application.add_endpoint( new EndpointRoute("/sw.js", Method.GET)); application.add_endpoint( new EndpointRoute("/api/advisor/chat", Method.POST)); application.add_endpoint( new EndpointRoute("/api/advisor/turn", Method.POST)); application.add_endpoint( new EndpointRoute("/api/advisor/answer", Method.POST)); application.add_endpoint( new EndpointRoute("/api/advisor/chat/delete", Method.POST)); application.add_endpoint( new EndpointRoute("/api/advisor/chat/{id}", Method.GET)); application.add_endpoint( new EndpointRoute("/api/projects/{id}", Method.DELETE)); GLib.Idle.add(() => { seed_admin.begin(application.container); start_dispatcher.begin(application.container); start_advisor_callback.begin(application.container); return false; }); log_canon_health(); application.run(); return 0; } catch (Error e) { printerr("Error: %s\n", e.message); return 1; } } /** * Seeds the admin account from SPRY_SEED_ADMIN_USERNAME and * SPRY_SEED_ADMIN_PASSWORD, defaulting both to admin/change-me when unset. * The password is never logged. */ private async void seed_admin(Container container) { var username = Environment.get_variable("SPRY_SEED_ADMIN_USERNAME") ?? "admin"; var password = Environment.get_variable("SPRY_SEED_ADMIN_PASSWORD") ?? "change-me"; if (username.length == 0 || password.length == 0) { return; } try { var scope = container.create_scope(); yield SpryAuth.ensure_admin(scope.resolve(), username, @"$(username)@example.com", password); scope.release(); print("[ClankerSaddle] Seeded admin account: %s\n", username); } catch (Error e) { printerr("[ClankerSaddle] Admin seeding failed: %s\n", e.message); } } private async void start_dispatcher(Container container) { try { var scope = container.create_scope(); scope.resolve().start(); scope.release(); } catch (Error e) { printerr("[ClankerSaddle] Dispatcher startup failed: %s\n", e.message); } } /** * Starts the advisor completion callback's periodic pass alongside * the dispatcher (goal 27): every ADVISOR_CALLBACK_TICK_S (15s) it * delivers terminal task outcomes into the chats that suggested * them. */ private async void start_advisor_callback(Container container) { try { var scope = container.create_scope(); scope.resolve().start(); scope.release(); } catch (Error e) { printerr("[ClankerSaddle] Advisor callback startup failed: %s\n", e.message); } } /** * The canon health line (goal 27 deploy hardening): the resolved * directory and the text count at startup, or — when nothing * resolves — a LOUD warning naming every candidate path tried and * the CLANKER_CANON_DIR override, so an empty canon shelf is * visible in the container logs without asking the bot. The same * fact rides the dashboard's public data as `canon_texts`. */ private void log_canon_health() { var canon = new ClankerSaddle.Services.Canon(); var directory = canon.directory(); if (directory.length > 0) { print("[ClankerSaddle] Canon: %d texts from %s\n", canon.texts().length, directory); var errors = canon.errors(); if (errors.length > 0) { printerr("[ClankerSaddle] WARNING: canon at %s carries %d " + "unparseable file(s): %s\n", directory, errors.length, string.joinv("; ", errors)); } return; } var candidates = canon.candidates(); printerr("[ClankerSaddle] WARNING: no canon directory resolved — the " + "canonical texts will be ABSENT (/api/saddle/canon answers an " + "empty library and the ethos has nothing to point at). Tried:\n"); foreach (var candidate in candidates) { printerr("[ClankerSaddle] %s\n", candidate); } printerr("[ClankerSaddle] Set CLANKER_CANON_DIR to the directory " + "holding the canon *.md files (or install the " + "share/clanker-saddle/canon package files).\n"); }