| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398 |
- 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<StatumModule>();
- application.add_module<SpryModule>();
- 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<DatabaseConfigurator>();
- 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<InvercargillSql.Connection>();
- 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<ClankerSaddle.Data.Migrations.M0001_Initial>();
- db.add_migration<ClankerSaddle.Data.Migrations.M0002_BatchImprovements>();
- db.add_migration<ClankerSaddle.Data.Migrations.M0003_EventIndexes>();
- db.add_migration<ClankerSaddle.Data.Migrations.M0004_ProjectUnderstandings>();
- db.add_migration<ClankerSaddle.Data.Migrations.M0005_Discoveries>();
- db.add_migration<ClankerSaddle.Data.Migrations.M0006_Advisor>();
- db.add_migration<ClankerSaddle.Data.Migrations.M0007_ParkingGuard>();
- db.add_migration<ClankerSaddle.Data.Migrations.M0008_AnswerWake>();
- db.add_migration<ClankerSaddle.Data.Migrations.M0009_TurnCapPark>();
- db.add_migration<ClankerSaddle.Data.Migrations.M0010_ModelConfig>();
- db.add_entity<Project>(Project.entity_mapping);
- db.add_entity<ProjectLink>(ProjectLink.entity_mapping);
- db.add_entity<TaskEntity>(TaskEntity.entity_mapping);
- db.add_entity<TaskComment>(TaskComment.entity_mapping);
- db.add_entity<TaskBlock>(TaskBlock.entity_mapping);
- db.add_entity<EventRecord>(EventRecord.entity_mapping);
- db.add_entity<Saddle>(Saddle.entity_mapping);
- db.add_entity<Session>(Session.entity_mapping);
- db.add_entity<Rule>(Rule.entity_mapping);
- db.add_entity<RuleProject>(RuleProject.entity_mapping);
- db.add_entity<TaskRule>(TaskRule.entity_mapping);
- db.add_entity<ProjectNote>(ProjectNote.entity_mapping);
- db.add_entity<ProjectUnderstanding>(ProjectUnderstanding.entity_mapping);
- db.add_entity<Discovery>(Discovery.entity_mapping);
- db.add_entity<Capability>(Capability.entity_mapping);
- db.add_entity<SaddleCapability>(SaddleCapability.entity_mapping);
- db.add_entity<TaskCapability>(TaskCapability.entity_mapping);
- db.add_entity<PushSubscription>(PushSubscription.entity_mapping);
- db.add_entity<TaskNotifyPref>(TaskNotifyPref.entity_mapping);
- db.add_entity<Goal>(Goal.entity_mapping);
- db.add_entity<GoalLink>(GoalLink.entity_mapping);
- db.add_entity<AdvisorChat>(AdvisorChat.entity_mapping);
- db.add_entity<AdvisorMessage>(AdvisorMessage.entity_mapping);
- db.add_entity<AdvisorSuggestion>(AdvisorSuggestion.entity_mapping);
- db.add_entity<Stack>(Stack.entity_mapping);
- db.add_entity<StackProject>(StackProject.entity_mapping);
- db.add_entity<Note>(Note.entity_mapping);
- db.add_entity<ModelConfig>(ModelConfig.entity_mapping);
- db.migrate_on_startup();
- application.add_scoped<ProjectService>();
- application.add_scoped<TaskService>();
- application.add_scoped<EventService>();
- application.add_scoped<GoalService>();
- application.add_scoped<StackService>();
- application.add_scoped<NoteService>();
- application.add_scoped<UnderstandingService>();
- application.add_scoped<DiscoveryService>();
- application.add_scoped<SaddleService>();
- application.add_scoped<LifecycleService>();
- application.add_scoped<RuleService>();
- application.add_scoped<CapabilityService>();
- application.add_scoped<AllocationService>();
- application.add_scoped<TimelineService>();
- application.add_scoped<ChatService>();
- application.add_singleton<SaddlePollState>();
- application.add_singleton<AnswerDeliveryHub>();
- application.add_singleton<StatePulse>();
- application.add_singleton<PushService>();
- application.add_singleton<Dispatcher>();
- application.add_singleton<ModelConfigService>();
- application.add_singleton<AdvisorService>();
- application.add_singleton<AdvisorCallback>();
- // 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<PushService>().ensure_keys_from_config();
- key_scope.release();
- var statum = application.configure_with<StatumConfigurator>();
- statum.global<ClankerSaddle.Services.PulsePublic>(StatePulse.SLOT_TYPE,
- new ClankerSaddle.Services.PulsePublic());
- // spry:pages-begin
- statum.add_page<HomePage, HomeEntrypoint>();
- statum.add_page<LoginPage, LoginEntrypoint>();
- statum.add_page<ProjectsPage, ProjectsEntrypoint>();
- statum.add_page<ProjectDetailPage, ProjectDetailEntrypoint>();
- statum.add_page<TasksPage, TasksEntrypoint>();
- statum.add_page<TaskDetailPage, TaskDetailEntrypoint>();
- statum.add_page<TaskLogPage, TaskLogEntrypoint>();
- statum.add_page<GoalsPage, GoalsEntrypoint>();
- statum.add_page<GoalDetailPage, GoalDetailEntrypoint>();
- statum.add_page<SaddlesPage, SaddlesEntrypoint>();
- statum.add_page<RulesPage, RulesEntrypoint>();
- statum.add_page<DiscoveriesPage, DiscoveriesEntrypoint>();
- statum.add_page<ChatsPage, ChatsEntrypoint>();
- statum.add_page<ChatDetailPage, ChatDetailEntrypoint>();
- statum.add_page<StacksPage, StacksEntrypoint>();
- statum.add_page<StackDetailPage, StackDetailEntrypoint>();
- statum.add_page<UsersPage, UsersEntrypoint>();
- statum.add_page<ModelSettingsPage, ModelSettingsEntrypoint>();
- statum.add_page<ProfilePage, ProfileEntrypoint>();
- // spry:pages-end
- // spry:actions-begin
- statum.action<LoginAction>();
- statum.action<Spry.Actions.LogoutAction>();
- statum.action<CreateProjectAction>();
- statum.action<UpdateProjectAction>();
- statum.action<AddProjectLinkAction>();
- statum.action<RemoveProjectLinkAction>();
- statum.action<DeleteProjectAction>();
- statum.action<CreateTaskAction>();
- statum.action<UpdateTaskAction>();
- statum.action<AddCommentAction>();
- statum.action<AnswerBlockAction>();
- statum.action<EnqueueTaskAction>();
- statum.action<DeleteTaskAction>();
- statum.action<CancelTaskAction>();
- statum.action<RetryTaskAction>();
- statum.action<ResumeTaskAction>();
- statum.action<TaskForceWakeAction>();
- statum.action<RefreshTasksDataAction>();
- statum.action<CreateGoalAction>();
- statum.action<StartGoalAction>();
- statum.action<UpdateDraftGoalAction>();
- statum.action<PauseGoalAction>();
- statum.action<ResumeGoalAction>();
- statum.action<GoalForceWakeAction>();
- statum.action<CancelGoalAction>();
- statum.action<RetryGoalRunAction>();
- statum.action<RequeueGoalRunAction>();
- statum.action<GoalCommentAction>();
- statum.action<AnswerGoalBlockAction>();
- statum.action<AddGoalLinkAction>();
- statum.action<RemoveGoalLinkAction>();
- statum.action<RefreshGoalDataAction>();
- statum.action<PushSubscribeAction>();
- statum.action<PushUnsubscribeAction>();
- statum.action<PushTestAction>();
- statum.action<ToggleTaskNotifyAction>();
- statum.action<RefreshTaskDataAction>();
- statum.action<RefreshDashboardAction>();
- statum.action<RegisterSaddleAction>();
- statum.action<UnregisterSaddleAction>();
- statum.action<RefreshSaddleStatusAction>();
- statum.action<UpdateSaddleIdentityAction>();
- statum.action<UpdateSaddlePromptNoteAction>();
- statum.action<CreateRuleAction>();
- statum.action<UpdateRuleAction>();
- statum.action<DeleteRuleAction>();
- statum.action<AttachProjectRuleAction>();
- statum.action<DetachProjectRuleAction>();
- statum.action<AttachTaskRuleAction>();
- statum.action<DetachTaskRuleAction>();
- statum.action<DeleteProjectNoteAction>();
- statum.action<RefreshUnderstandingAction>();
- statum.action<UpdateDiscoveryAction>();
- statum.action<DeleteDiscoveryAction>();
- statum.action<StartAdvisorChatAction>();
- statum.action<SendAdvisorMessageAction>();
- statum.action<AnswerAdvisorQuestionAction>();
- statum.action<DeleteAdvisorChatAction>();
- statum.action<RefreshAdvisorChatAction>();
- statum.action<DecideAdvisorSuggestionAction>();
- statum.action<CreateStackAction>();
- statum.action<UpdateStackAction>();
- statum.action<DeleteStackAction>();
- statum.action<AddStackMemberAction>();
- statum.action<RemoveStackMemberAction>();
- statum.action<ApplyStackToProjectAction>();
- statum.action<ApplyStackToGoalAction>();
- statum.action<AddNoteAction>();
- statum.action<ToggleNoteAction>();
- statum.action<DeleteNoteAction>();
- statum.action<PromoteNoteAction>();
- statum.action<CreateCapabilityAction>();
- statum.action<DeleteCapabilityAction>();
- statum.action<AttachSaddleCapabilityAction>();
- statum.action<DetachSaddleCapabilityAction>();
- statum.action<AttachTaskCapabilityAction>();
- statum.action<DetachTaskCapabilityAction>();
- statum.action<CreateUserAction>();
- statum.action<AlterManagedUserAction>();
- statum.action<SetManagedUserEnabledAction>();
- statum.action<GrantUserAdminAction>();
- statum.action<RevokeUserAdminAction>();
- statum.action<DeleteManagedUserAction>();
- statum.action<ChangeMyPasswordAction>();
- statum.action<SaveModelConfigAction>();
- // spry:actions-end
- // spry:resources-begin
- statum.add_resource<ClankerSaddle.AppCss>();
- statum.add_resource<ClankerSaddle.AppJs>();
- statum.add_resource<ClankerSaddle.TaskLogJs>();
- statum.add_resource<ClankerSaddle.FaviconResource>();
- statum.add_resource<ClankerSaddle.EmblemResource>();
- // spry:resources-end
- // spry:auth-begin
- // spry:auth-end
- application.add_endpoint<SaddleNextTaskEndpoint>(
- new EndpointRoute("/api/saddle/next-task", Method.POST));
- application.add_endpoint<SaddleChunksEndpoint>(
- new EndpointRoute("/api/saddle/chunks", Method.POST));
- application.add_endpoint<SaddleResultsEndpoint>(
- new EndpointRoute("/api/saddle/results", Method.POST));
- application.add_endpoint<SaddleNoteEndpoint>(
- new EndpointRoute("/api/saddle/note", Method.POST));
- application.add_endpoint<SaddleUnderstandingEndpoint>(
- new EndpointRoute("/api/saddle/understanding", Method.POST));
- application.add_endpoint<SaddleUnderstandingEndpoint>(
- new EndpointRoute("/api/saddle/understanding", Method.GET));
- application.add_endpoint<SaddleDiscoveryEndpoint>(
- new EndpointRoute("/api/saddle/discovery", Method.POST));
- application.add_endpoint<SaddleDiscoveryEndpoint>(
- new EndpointRoute("/api/saddle/discoveries", Method.GET));
- application.add_endpoint<SaddleDiscoveryUpdateEndpoint>(
- new EndpointRoute("/api/saddle/discovery/update", Method.POST));
- application.add_endpoint<SaddleTaskCreateEndpoint>(
- new EndpointRoute("/api/saddle/task/create", Method.POST));
- application.add_endpoint<SaddleTaskDeleteEndpoint>(
- new EndpointRoute("/api/saddle/task/delete", Method.POST));
- application.add_endpoint<SaddleTaskEnqueueEndpoint>(
- new EndpointRoute("/api/saddle/task/enqueue", Method.POST));
- application.add_endpoint<SaddleTaskListEndpoint>(
- new EndpointRoute("/api/saddle/tasks", Method.GET));
- application.add_endpoint<SaddleCanonListEndpoint>(
- new EndpointRoute("/api/saddle/canon", Method.GET));
- application.add_endpoint<SaddleCanonTextEndpoint>(
- new EndpointRoute("/api/saddle/canon/{slug}", Method.GET));
- application.add_endpoint<SaddleEthosEndpoint>(
- new EndpointRoute("/api/saddle/ethos", Method.GET));
- application.add_endpoint<SaddleTaskDetailEndpoint>(
- new EndpointRoute("/api/saddle/task/{id}", Method.GET));
- application.add_endpoint<SaddleCapabilitiesEndpoint>(
- new EndpointRoute("/api/saddle/capabilities", Method.GET));
- application.add_endpoint<SaddleGoalsEndpoint>(
- new EndpointRoute("/api/saddle/goals", Method.GET));
- application.add_endpoint<ClankerSaddle.Api.TaskEventsEndpoint>(
- new EndpointRoute("/tasks/{task_id}/events", Method.GET));
- application.add_endpoint<ClankerSaddle.Api.EventStreamSseEndpoint>(
- new EndpointRoute("/tasks/{task_id}/events/stream", Method.GET));
- application.add_endpoint<ClankerSaddle.Api.ServiceWorkerEndpoint>(
- new EndpointRoute("/sw.js", Method.GET));
- application.add_endpoint<ClankerSaddle.Api.AdvisorChatEndpoint>(
- new EndpointRoute("/api/advisor/chat", Method.POST));
- application.add_endpoint<ClankerSaddle.Api.AdvisorChatEndpoint>(
- new EndpointRoute("/api/advisor/turn", Method.POST));
- application.add_endpoint<ClankerSaddle.Api.AdvisorChatEndpoint>(
- new EndpointRoute("/api/advisor/answer", Method.POST));
- application.add_endpoint<ClankerSaddle.Api.AdvisorChatEndpoint>(
- new EndpointRoute("/api/advisor/chat/delete", Method.POST));
- application.add_endpoint<ClankerSaddle.Api.AdvisorChatDetailEndpoint>(
- new EndpointRoute("/api/advisor/chat/{id}", Method.GET));
- application.add_endpoint<ClankerSaddle.Api.ProjectDeleteEndpoint>(
- 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<UserService>(),
- 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<Dispatcher>().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<AdvisorCallback>().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");
- }
|