manigent-server 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- manigent/_catalog/mcps/chess/README.md +38 -0
- manigent/_catalog/mcps/chess/mcp.json +9 -0
- manigent/_catalog/mcps/registry.json +9 -0
- manigent/_catalog/skills/brief/SKILL.md +339 -0
- manigent/_catalog/skills/broaden-options/SKILL.md +133 -0
- manigent/_catalog/skills/calendar/SKILL.md +44 -0
- manigent/_catalog/skills/calendar/starter_config.jsonc +25 -0
- manigent/_catalog/skills/gmail/SKILL.md +73 -0
- manigent/_catalog/skills/google-calendar/SKILL.md +44 -0
- manigent/_catalog/skills/google-drive/SKILL.md +42 -0
- manigent/_catalog/skills/google-drive-write/SKILL.md +45 -0
- manigent/_catalog/skills/google-drive-write/starter_config.jsonc +45 -0
- manigent/_catalog/skills/grill-me/SKILL.md +173 -0
- manigent/_catalog/skills/homekit/SKILL.md +37 -0
- manigent/_catalog/skills/http-api/SKILL.md +117 -0
- manigent/_catalog/skills/ib/SKILL.md +89 -0
- manigent/_catalog/skills/ib/starter_config.jsonc +35 -0
- manigent/_catalog/skills/mailbox/SKILL.md +47 -0
- manigent/_catalog/skills/mailbox/starter_config.jsonc +38 -0
- manigent/_catalog/skills/media/SKILL.md +93 -0
- manigent/_catalog/skills/media/starter_config.jsonc +63 -0
- manigent/_catalog/skills/osxphotos/SKILL.md +41 -0
- manigent/_catalog/skills/photo-story/SKILL.md +285 -0
- manigent/_catalog/skills/playwright-browser/SKILL.md +291 -0
- manigent/_catalog/skills/playwright-browser/starter_config.jsonc +44 -0
- manigent/_catalog/skills/playwright-save-skill/SKILL.md +149 -0
- manigent/_catalog/skills/registry.json +166 -0
- manigent/_catalog/skills/web-artifacts/SKILL.md +194 -0
- manigent/api/action_executor.py +404 -0
- manigent/api/action_validator.py +531 -0
- manigent/api/actions.py +221 -0
- manigent/api/client.py +558 -0
- manigent/api/control.py +806 -0
- manigent/api/edge_protocol.py +123 -0
- manigent/api/fs_protocol.py +450 -0
- manigent/api/host_protocol.py +298 -0
- manigent/api/managed_job_client.py +124 -0
- manigent/api/responses.py +342 -0
- manigent/api/retry_transport.py +178 -0
- manigent/api/turn_protocol.py +78 -0
- manigent/cli_lifecycle.py +467 -0
- manigent/cli_server.py +978 -0
- manigent/doctor.py +1167 -0
- manigent/hub_protocol/canonical.py +375 -0
- manigent/hub_protocol/codes.py +171 -0
- manigent/hub_protocol/constants.py +71 -0
- manigent/hub_protocol/envelope.py +218 -0
- manigent/hub_protocol/frames.py +788 -0
- manigent/hub_protocol/http.py +260 -0
- manigent/hub_protocol/ids.py +170 -0
- manigent/hub_protocol/keys.py +195 -0
- manigent/hub_protocol/text.py +75 -0
- manigent/llm/base.py +1072 -0
- manigent/llm/prompt_builder.py +2450 -0
- manigent/llm/triggers.py +68 -0
- manigent/models/agent.py +2905 -0
- manigent/models/agent_bundle.py +298 -0
- manigent/models/brief_tab.py +311 -0
- manigent/models/chat_message.py +231 -0
- manigent/models/chatroom.py +1108 -0
- manigent/models/context.py +1159 -0
- manigent/models/desk_label.py +836 -0
- manigent/models/desk_read_state.py +220 -0
- manigent/models/desk_sign_off.py +267 -0
- manigent/models/desk_sop.py +965 -0
- manigent/models/desk_todo.py +1603 -0
- manigent/models/desk_todo_link.py +404 -0
- manigent/models/file_cache.py +75 -0
- manigent/models/host.py +732 -0
- manigent/models/install_token.py +401 -0
- manigent/models/invitation_code.py +162 -0
- manigent/models/knowledge_pack.py +552 -0
- manigent/models/managed_job.py +295 -0
- manigent/models/model_config.py +564 -0
- manigent/models/model_defaults.py +238 -0
- manigent/models/onboarding_plan.py +161 -0
- manigent/models/participant.py +449 -0
- manigent/models/persistable.py +892 -0
- manigent/models/plan_markdown.py +1774 -0
- manigent/models/project.py +1343 -0
- manigent/models/project_plan.py +8104 -0
- manigent/models/project_report.py +145 -0
- manigent/models/provenance.py +261 -0
- manigent/models/provider_agents.py +101 -0
- manigent/models/public_request.py +108 -0
- manigent/models/reflection_schedule.py +191 -0
- manigent/models/scheduled_message.py +285 -0
- manigent/models/share_token.py +190 -0
- manigent/models/skill_version.py +31 -0
- manigent/models/sop_template.py +187 -0
- manigent/models/thinking_partner_plan.py +187 -0
- manigent/models/tunnel_binding.py +129 -0
- manigent/models/user.py +1276 -0
- manigent/models/work_tracker.py +510 -0
- manigent/runner/invocation_registry.py +187 -0
- manigent/server/agent_capabilities.py +119 -0
- manigent/server/agent_visibility.py +119 -0
- manigent/server/announcements.py +119 -0
- manigent/server/app.py +807 -0
- manigent/server/auth.py +355 -0
- manigent/server/batch_timeout.py +94 -0
- manigent/server/channels/access.py +194 -0
- manigent/server/channels/adapter.py +295 -0
- manigent/server/channels/config.py +693 -0
- manigent/server/channels/contacts.py +39 -0
- manigent/server/channels/email_adapter.py +436 -0
- manigent/server/channels/email_address.py +145 -0
- manigent/server/channels/email_form.py +242 -0
- manigent/server/channels/email_format.py +285 -0
- manigent/server/channels/email_mail.py +427 -0
- manigent/server/channels/email_mailgun.py +164 -0
- manigent/server/channels/email_text.py +280 -0
- manigent/server/channels/file_links.py +127 -0
- manigent/server/channels/gateway.py +2998 -0
- manigent/server/channels/hub_delivery.py +211 -0
- manigent/server/channels/hub_store.py +1730 -0
- manigent/server/channels/line.py +645 -0
- manigent/server/channels/line_format.py +589 -0
- manigent/server/channels/log_redaction.py +145 -0
- manigent/server/channels/store.py +3316 -0
- manigent/server/channels/telegram.py +422 -0
- manigent/server/channels/telegram_format.py +312 -0
- manigent/server/channels/webchat.py +261 -0
- manigent/server/channels/webchat_widget.js +1310 -0
- manigent/server/channels/whatsapp.py +707 -0
- manigent/server/channels/whatsapp_format.py +511 -0
- manigent/server/chat_search_index.py +1203 -0
- manigent/server/config.py +61 -0
- manigent/server/context.py +121 -0
- manigent/server/coordinator_rule.py +115 -0
- manigent/server/desk_todo_notify.py +300 -0
- manigent/server/edge_hub.py +577 -0
- manigent/server/email.py +354 -0
- manigent/server/errors.py +83 -0
- manigent/server/home_fs_relay.py +392 -0
- manigent/server/hub/adapter.py +1395 -0
- manigent/server/hub/agent_send_guard.py +196 -0
- manigent/server/hub/avatar.py +342 -0
- manigent/server/hub/browser.py +778 -0
- manigent/server/hub/client.py +859 -0
- manigent/server/hub/demand.py +49 -0
- manigent/server/hub/directory.py +385 -0
- manigent/server/hub/enroll.py +400 -0
- manigent/server/hub/http.py +134 -0
- manigent/server/hub/identity.py +61 -0
- manigent/server/hub/keys.py +588 -0
- manigent/server/hub/mirror.py +802 -0
- manigent/server/hub/operator.py +817 -0
- manigent/server/hub/retain.py +361 -0
- manigent/server/hub/room_bridge.py +741 -0
- manigent/server/hub/runtime.py +691 -0
- manigent/server/hub/trust.py +182 -0
- manigent/server/hub/views.py +316 -0
- manigent/server/idempotency.py +205 -0
- manigent/server/in_flight_restore.py +181 -0
- manigent/server/inspection.py +816 -0
- manigent/server/isolation_gate.py +516 -0
- manigent/server/isolation_notice.py +310 -0
- manigent/server/managed_job_events.py +242 -0
- manigent/server/mcp_registry.py +118 -0
- manigent/server/message_forward.py +78 -0
- manigent/server/message_scheduler.py +176 -0
- manigent/server/notification_assets.py +164 -0
- manigent/server/oauth_login/cli_state.py +120 -0
- manigent/server/oauth_login/config.py +90 -0
- manigent/server/oauth_login/errors.py +78 -0
- manigent/server/oauth_login/flow.py +223 -0
- manigent/server/oauth_login/handoff.py +63 -0
- manigent/server/oauth_login/providers.py +45 -0
- manigent/server/oauth_state.py +81 -0
- manigent/server/owner_isolation.py +466 -0
- manigent/server/participant_liveness.py +82 -0
- manigent/server/public_profile.py +149 -0
- manigent/server/public_requests/candidates.py +116 -0
- manigent/server/public_requests/dispatch.py +121 -0
- manigent/server/public_requests/ranking.py +153 -0
- manigent/server/recent_activity.py +250 -0
- manigent/server/reflection_completion_subscriber.py +151 -0
- manigent/server/reflection_scheduler.py +239 -0
- manigent/server/requester_key_migration.py +261 -0
- manigent/server/room_flow.py +139 -0
- manigent/server/routes/_batch.py +218 -0
- manigent/server/routes/_fd_routing.py +155 -0
- manigent/server/routes/_mentions.py +70 -0
- manigent/server/routes/agent_env.py +188 -0
- manigent/server/routes/agent_fs.py +450 -0
- manigent/server/routes/agents.py +1206 -0
- manigent/server/routes/announcements.py +79 -0
- manigent/server/routes/auth.py +387 -0
- manigent/server/routes/brief_tabs.py +404 -0
- manigent/server/routes/changelog.py +237 -0
- manigent/server/routes/channels.py +993 -0
- manigent/server/routes/chatrooms.py +871 -0
- manigent/server/routes/desk_labels.py +298 -0
- manigent/server/routes/desk_latest_messages.py +116 -0
- manigent/server/routes/desk_read_state.py +110 -0
- manigent/server/routes/desk_sign_off.py +88 -0
- manigent/server/routes/desk_sops.py +216 -0
- manigent/server/routes/desk_todos.py +692 -0
- manigent/server/routes/desk_tour.py +44 -0
- manigent/server/routes/files.py +572 -0
- manigent/server/routes/hosts.py +484 -0
- manigent/server/routes/hub.py +534 -0
- manigent/server/routes/hub_channels.py +234 -0
- manigent/server/routes/inbox.py +581 -0
- manigent/server/routes/install.ps1 +436 -0
- manigent/server/routes/install.py +324 -0
- manigent/server/routes/install.sh +566 -0
- manigent/server/routes/isolation_check.py +145 -0
- manigent/server/routes/knowledge_packs.py +477 -0
- manigent/server/routes/managed_jobs.py +196 -0
- manigent/server/routes/mcps.py +271 -0
- manigent/server/routes/messages.py +1972 -0
- manigent/server/routes/model_defaults.py +279 -0
- manigent/server/routes/oauth.py +237 -0
- manigent/server/routes/oauth_login.py +351 -0
- manigent/server/routes/owner_isolation.py +91 -0
- manigent/server/routes/project_plans.py +1521 -0
- manigent/server/routes/project_reports.py +235 -0
- manigent/server/routes/projects.py +2047 -0
- manigent/server/routes/public_agents.py +151 -0
- manigent/server/routes/public_requests.py +181 -0
- manigent/server/routes/reflection.py +93 -0
- manigent/server/routes/scheduled_messages.py +263 -0
- manigent/server/routes/search.py +94 -0
- manigent/server/routes/sharing.py +156 -0
- manigent/server/routes/skills.py +406 -0
- manigent/server/routes/templates.py +219 -0
- manigent/server/routes/turn_tokens.py +119 -0
- manigent/server/routes/webchat.py +406 -0
- manigent/server/routes/websocket.py +431 -0
- manigent/server/serving_only.py +200 -0
- manigent/server/signup_policy.py +120 -0
- manigent/server/skill_registry.py +164 -0
- manigent/server/socket_events.py +858 -0
- manigent/server/timeout_checker.py +446 -0
- manigent/server/tunnel_subscriber.py +1000 -0
- manigent/server/turn_tokens.py +285 -0
- manigent/server/ws_hub.py +556 -0
- manigent/server_cli.py +31 -0
- manigent/sync/changelog.py +885 -0
- manigent/sync/console_subscriber.py +222 -0
- manigent/sync/runloop.py +619 -0
- manigent/sync/runloop_manager.py +197 -0
- manigent/sync/subscriber.py +98 -0
- manigent/utils/agent_processes.py +276 -0
- manigent/utils/agent_storage.py +125 -0
- manigent/utils/email.py +563 -0
- manigent/utils/env_store.py +174 -0
- manigent/utils/extra_dirs.py +85 -0
- manigent/utils/file_io.py +485 -0
- manigent/utils/invoke_timeout.py +87 -0
- manigent/utils/jsonc.py +78 -0
- manigent/utils/knowledge_index.py +148 -0
- manigent/utils/nofollow_fs.py +279 -0
- manigent/utils/notification_center.py +67 -0
- manigent/utils/version.py +57 -0
- manigent/web/frontend/.design-sync-entry.tsx +74 -0
- manigent/web/frontend/.design-sync-provider.tsx +46 -0
- manigent/web/frontend/.eslintrc.cjs +183 -0
- manigent/web/frontend/.gitignore +6 -0
- manigent/web/frontend/app/page.tsx +39 -0
- manigent/web/frontend/hub-terms.html +192 -0
- manigent/web/frontend/index.html +15 -0
- manigent/web/frontend/landing.html +15 -0
- manigent/web/frontend/package-lock.json +8028 -0
- manigent/web/frontend/package.json +55 -0
- manigent/web/frontend/postcss.config.js +6 -0
- manigent/web/frontend/privacy.html +217 -0
- manigent/web/frontend/private-beta-announcement.html +19 -0
- manigent/web/frontend/public/apple-touch-icon.png +0 -0
- manigent/web/frontend/public/brand-hero-banner.png +0 -0
- manigent/web/frontend/public/brand-hero.png +0 -0
- manigent/web/frontend/public/favicon.png +0 -0
- manigent/web/frontend/public/favicon.svg +8 -0
- manigent/web/frontend/public/icon-transparent-sm.png +0 -0
- manigent/web/frontend/public/icon.png +0 -0
- manigent/web/frontend/public/manigent-mark.svg +7 -0
- manigent/web/frontend/public/vite.svg +5 -0
- manigent/web/frontend/public.html +15 -0
- manigent/web/frontend/src/App.activeAccountRedirect.test.tsx +236 -0
- manigent/web/frontend/src/App.hubHostDm.test.tsx +119 -0
- manigent/web/frontend/src/App.hubHostProject.test.tsx +142 -0
- manigent/web/frontend/src/App.hubRoutes.test.tsx +187 -0
- manigent/web/frontend/src/App.oauthAddCallback.test.tsx +263 -0
- manigent/web/frontend/src/App.tsx +246 -0
- manigent/web/frontend/src/api/attachments.ts +32 -0
- manigent/web/frontend/src/api/batchLoader.test.ts +212 -0
- manigent/web/frontend/src/api/batchLoader.ts +124 -0
- manigent/web/frontend/src/api/chatroomsBatch.test.tsx +392 -0
- manigent/web/frontend/src/api/client.ts +321 -0
- manigent/web/frontend/src/api/crossAccountAuth.test.ts +388 -0
- manigent/web/frontend/src/api/endpoints/agentEnv.ts +24 -0
- manigent/web/frontend/src/api/endpoints/agentFs.ts +125 -0
- manigent/web/frontend/src/api/endpoints/agentSearch.ts +19 -0
- manigent/web/frontend/src/api/endpoints/agents.ts +66 -0
- manigent/web/frontend/src/api/endpoints/announcements.ts +26 -0
- manigent/web/frontend/src/api/endpoints/auth.ts +361 -0
- manigent/web/frontend/src/api/endpoints/authLogin.test.ts +236 -0
- manigent/web/frontend/src/api/endpoints/batch.ts +64 -0
- manigent/web/frontend/src/api/endpoints/briefTabs.ts +81 -0
- manigent/web/frontend/src/api/endpoints/channels.ts +285 -0
- manigent/web/frontend/src/api/endpoints/computers.ts +212 -0
- manigent/web/frontend/src/api/endpoints/deskLabels.ts +151 -0
- manigent/web/frontend/src/api/endpoints/deskLatestMessages.ts +23 -0
- manigent/web/frontend/src/api/endpoints/deskReadState.ts +41 -0
- manigent/web/frontend/src/api/endpoints/deskSignOffs.ts +74 -0
- manigent/web/frontend/src/api/endpoints/deskSops.ts +53 -0
- manigent/web/frontend/src/api/endpoints/deskTodoAttachments.ts +69 -0
- manigent/web/frontend/src/api/endpoints/deskTodoProjects.test.ts +99 -0
- manigent/web/frontend/src/api/endpoints/deskTodoProjects.ts +114 -0
- manigent/web/frontend/src/api/endpoints/deskTodos.ts +164 -0
- manigent/web/frontend/src/api/endpoints/deskTour.ts +16 -0
- manigent/web/frontend/src/api/endpoints/dm.ts +137 -0
- manigent/web/frontend/src/api/endpoints/files.ts +55 -0
- manigent/web/frontend/src/api/endpoints/hub.contract.test.ts +94 -0
- manigent/web/frontend/src/api/endpoints/hub.keys.ts +260 -0
- manigent/web/frontend/src/api/endpoints/hub.test.ts +203 -0
- manigent/web/frontend/src/api/endpoints/hub.ts +654 -0
- manigent/web/frontend/src/api/endpoints/index.ts +15 -0
- manigent/web/frontend/src/api/endpoints/install.ts +74 -0
- manigent/web/frontend/src/api/endpoints/isolationCheck.ts +36 -0
- manigent/web/frontend/src/api/endpoints/knowledgePacks.ts +179 -0
- manigent/web/frontend/src/api/endpoints/mcps.ts +36 -0
- manigent/web/frontend/src/api/endpoints/messages.held.test.ts +43 -0
- manigent/web/frontend/src/api/endpoints/messages.ts +95 -0
- manigent/web/frontend/src/api/endpoints/modelConfigs.ts +232 -0
- manigent/web/frontend/src/api/endpoints/oauthApi.test.ts +110 -0
- manigent/web/frontend/src/api/endpoints/projectPlans.ts +608 -0
- manigent/web/frontend/src/api/endpoints/projectReports.ts +36 -0
- manigent/web/frontend/src/api/endpoints/projects.ts +124 -0
- manigent/web/frontend/src/api/endpoints/publicRequests.ts +75 -0
- manigent/web/frontend/src/api/endpoints/reflection.ts +32 -0
- manigent/web/frontend/src/api/endpoints/roomFlow.ts +14 -0
- manigent/web/frontend/src/api/endpoints/scheduledMessages.ts +60 -0
- manigent/web/frontend/src/api/endpoints/search.ts +27 -0
- manigent/web/frontend/src/api/endpoints/sharing.ts +42 -0
- manigent/web/frontend/src/api/endpoints/skills.ts +67 -0
- manigent/web/frontend/src/api/endpoints/templates.ts +72 -0
- manigent/web/frontend/src/api/loaders.ts +22 -0
- manigent/web/frontend/src/api/singleIssuer.test.ts +148 -0
- manigent/web/frontend/src/api/transientRetry.test.ts +176 -0
- manigent/web/frontend/src/api/transientRetry.ts +140 -0
- manigent/web/frontend/src/api/websocket.resync.test.ts +111 -0
- manigent/web/frontend/src/api/websocket.ts +335 -0
- manigent/web/frontend/src/components/agent/AgentNameLink.tsx +24 -0
- manigent/web/frontend/src/components/agent/AgentViewActions.test.tsx +41 -0
- manigent/web/frontend/src/components/agent/AgentViewActions.tsx +89 -0
- manigent/web/frontend/src/components/agent/CreateAgentPage.tsx +535 -0
- manigent/web/frontend/src/components/agent/JsonConfigEditor.tsx +33 -0
- manigent/web/frontend/src/components/agent/KnowledgePackList.tsx +103 -0
- manigent/web/frontend/src/components/agent/McpCatalog.tsx +261 -0
- manigent/web/frontend/src/components/agent/McpConfigModal.tsx +224 -0
- manigent/web/frontend/src/components/agent/PreConsentModal.tsx +172 -0
- manigent/web/frontend/src/components/agent/SkillCatalog.tsx +364 -0
- manigent/web/frontend/src/components/agent/SkillConfigModal.tsx +221 -0
- manigent/web/frontend/src/components/auth/AddAccountPage.test.tsx +181 -0
- manigent/web/frontend/src/components/auth/AddAccountPage.tsx +201 -0
- manigent/web/frontend/src/components/auth/AuthShell.tsx +462 -0
- manigent/web/frontend/src/components/auth/LoginPage.test.tsx +384 -0
- manigent/web/frontend/src/components/auth/LoginPage.tsx +129 -0
- manigent/web/frontend/src/components/auth/OAuthCallbackPage.test.tsx +185 -0
- manigent/web/frontend/src/components/auth/OAuthCallbackPage.tsx +154 -0
- manigent/web/frontend/src/components/auth/OAuthLinkedBanner.tsx +44 -0
- manigent/web/frontend/src/components/auth/ProviderButtons.tsx +94 -0
- manigent/web/frontend/src/components/auth/SignupPage.tsx +437 -0
- manigent/web/frontend/src/components/auth/StepProgress.tsx +60 -0
- manigent/web/frontend/src/components/auth/VerifyEmailPage.test.tsx +155 -0
- manigent/web/frontend/src/components/auth/VerifyEmailPage.tsx +211 -0
- manigent/web/frontend/src/components/auth/index.ts +3 -0
- manigent/web/frontend/src/components/auth/oauthErrors.test.ts +107 -0
- manigent/web/frontend/src/components/auth/oauthErrors.ts +82 -0
- manigent/web/frontend/src/components/auth/useUsernameCheck.test.tsx +68 -0
- manigent/web/frontend/src/components/auth/useUsernameCheck.ts +86 -0
- manigent/web/frontend/src/components/beta/BetaPage.tsx +21 -0
- manigent/web/frontend/src/components/beta/DayAtMyDesk.tsx +120 -0
- manigent/web/frontend/src/components/beta/Footer.tsx +82 -0
- manigent/web/frontend/src/components/beta/Hero.tsx +224 -0
- manigent/web/frontend/src/components/beta/Nav.tsx +51 -0
- manigent/web/frontend/src/components/beta/Pillars.tsx +172 -0
- manigent/web/frontend/src/components/beta/TwoModels.tsx +146 -0
- manigent/web/frontend/src/components/beta/constants.ts +3 -0
- manigent/web/frontend/src/components/briefing/BriefingFrame.test.tsx +208 -0
- manigent/web/frontend/src/components/briefing/BriefingFrame.tsx +151 -0
- manigent/web/frontend/src/components/briefing/BriefingPage.test.tsx +176 -0
- manigent/web/frontend/src/components/briefing/BriefingPage.tsx +157 -0
- manigent/web/frontend/src/components/briefing/BriefingPage.wsDelete.test.tsx +177 -0
- manigent/web/frontend/src/components/briefing/briefing.css +147 -0
- manigent/web/frontend/src/components/briefing/heightBootstrap.test.ts +121 -0
- manigent/web/frontend/src/components/briefing/heightBootstrap.ts +224 -0
- manigent/web/frontend/src/components/briefing/parseBriefMessage.test.ts +90 -0
- manigent/web/frontend/src/components/briefing/sampleBriefing.ts +148 -0
- manigent/web/frontend/src/components/channels/ChannelBadge.tsx +35 -0
- manigent/web/frontend/src/components/chatroom/ChannelThreadControls.tsx +127 -0
- manigent/web/frontend/src/components/chatroom/ChannelThreadNotice.test.tsx +207 -0
- manigent/web/frontend/src/components/chatroom/ChannelThreadNotice.tsx +80 -0
- manigent/web/frontend/src/components/chatroom/ChatroomView.channel.test.tsx +550 -0
- manigent/web/frontend/src/components/chatroom/ChatroomView.flow.test.tsx +186 -0
- manigent/web/frontend/src/components/chatroom/ChatroomView.isolation.test.tsx +70 -0
- manigent/web/frontend/src/components/chatroom/ChatroomView.tsx +450 -0
- manigent/web/frontend/src/components/chatroom/DelegationRow.test.tsx +170 -0
- manigent/web/frontend/src/components/chatroom/DelegationRow.tsx +217 -0
- manigent/web/frontend/src/components/chatroom/DeliveryStatusLine.test.tsx +33 -0
- manigent/web/frontend/src/components/chatroom/DeliveryStatusLine.tsx +46 -0
- manigent/web/frontend/src/components/chatroom/ForwardModal.test.tsx +109 -0
- manigent/web/frontend/src/components/chatroom/ForwardModal.tsx +304 -0
- manigent/web/frontend/src/components/chatroom/HeldNotice.tsx +77 -0
- manigent/web/frontend/src/components/chatroom/MentionDropdown.tsx +97 -0
- manigent/web/frontend/src/components/chatroom/MessageInput.tsx +408 -0
- manigent/web/frontend/src/components/chatroom/MessageItem.tsx +425 -0
- manigent/web/frontend/src/components/chatroom/MessageList.outside.test.tsx +98 -0
- manigent/web/frontend/src/components/chatroom/MessageList.test.tsx +245 -0
- manigent/web/frontend/src/components/chatroom/MessageList.tsx +406 -0
- manigent/web/frontend/src/components/chatroom/OfflineRemedyNote.test.tsx +211 -0
- manigent/web/frontend/src/components/chatroom/OfflineRemedyNote.tsx +182 -0
- manigent/web/frontend/src/components/chatroom/OutsideBadge.tsx +18 -0
- manigent/web/frontend/src/components/chatroom/RoomFlowControl.tsx +49 -0
- manigent/web/frontend/src/components/chatroom/RoomFlowStrip.test.tsx +47 -0
- manigent/web/frontend/src/components/chatroom/RoomFlowStrip.tsx +25 -0
- manigent/web/frontend/src/components/chatroom/SaveToPackModal.tsx +350 -0
- manigent/web/frontend/src/components/chatroom/StagedAttachments.test.tsx +56 -0
- manigent/web/frontend/src/components/chatroom/StagedAttachments.tsx +160 -0
- manigent/web/frontend/src/components/chatroom/SystemNote.tsx +18 -0
- manigent/web/frontend/src/components/chatroom/contactSender.test.tsx +130 -0
- manigent/web/frontend/src/components/chatroom/index.ts +3 -0
- manigent/web/frontend/src/components/chatroom/recipientStatus.test.ts +344 -0
- manigent/web/frontend/src/components/chatroom/recipientStatus.ts +197 -0
- manigent/web/frontend/src/components/common/AnnouncementBanners.test.tsx +221 -0
- manigent/web/frontend/src/components/common/AnnouncementBanners.tsx +180 -0
- manigent/web/frontend/src/components/common/Avatar.test.tsx +50 -0
- manigent/web/frontend/src/components/common/Avatar.tsx +102 -0
- manigent/web/frontend/src/components/common/Badge.tsx +30 -0
- manigent/web/frontend/src/components/common/BrandName.tsx +16 -0
- manigent/web/frontend/src/components/common/ClearHistoryButton.tsx +74 -0
- manigent/web/frontend/src/components/common/CollapsibleHeader.tsx +64 -0
- manigent/web/frontend/src/components/common/ConnectionStatus.test.tsx +56 -0
- manigent/web/frontend/src/components/common/ConnectionStatus.tsx +28 -0
- manigent/web/frontend/src/components/common/DropOverlay.tsx +27 -0
- manigent/web/frontend/src/components/common/InlineEditableTitle.test.tsx +369 -0
- manigent/web/frontend/src/components/common/InlineEditableTitle.tsx +287 -0
- manigent/web/frontend/src/components/common/IsolationUnavailablePanel.tsx +19 -0
- manigent/web/frontend/src/components/common/Loading.tsx +22 -0
- manigent/web/frontend/src/components/common/ModelSelector.tsx +248 -0
- manigent/web/frontend/src/components/common/Overlay.tsx +15 -0
- manigent/web/frontend/src/components/common/ResizeHandle.tsx +52 -0
- manigent/web/frontend/src/components/common/TagAutocompleteInput.test.tsx +92 -0
- manigent/web/frontend/src/components/common/TagAutocompleteInput.tsx +246 -0
- manigent/web/frontend/src/components/common/TagPillInput.tsx +181 -0
- manigent/web/frontend/src/components/common/TeamAndAgentPicker.tsx +83 -0
- manigent/web/frontend/src/components/common/Timestamp.tsx +61 -0
- manigent/web/frontend/src/components/common/VerifiedBadge.tsx +20 -0
- manigent/web/frontend/src/components/common/index.ts +8 -0
- manigent/web/frontend/src/components/composer/PhPopover.tsx +239 -0
- manigent/web/frontend/src/components/composer/PromptEditor.test.tsx +472 -0
- manigent/web/frontend/src/components/composer/PromptEditor.tsx +540 -0
- manigent/web/frontend/src/components/composer/caret.ts +159 -0
- manigent/web/frontend/src/components/composer/placeholders.test.ts +352 -0
- manigent/web/frontend/src/components/composer/placeholders.ts +354 -0
- manigent/web/frontend/src/components/composer/promptEditor.css +161 -0
- manigent/web/frontend/src/components/computer/ComputerPage.test.tsx +452 -0
- manigent/web/frontend/src/components/computer/ComputerPage.tsx +811 -0
- manigent/web/frontend/src/components/computer/ConnectComputerModal.tsx +135 -0
- manigent/web/frontend/src/components/computer/CopyCommand.tsx +77 -0
- manigent/web/frontend/src/components/computer/DisconnectComputerModal.tsx +121 -0
- manigent/web/frontend/src/components/computer/SoftwareCard.tsx +145 -0
- manigent/web/frontend/src/components/computer/TerminalCard.test.tsx +114 -0
- manigent/web/frontend/src/components/computer/TerminalCard.tsx +131 -0
- manigent/web/frontend/src/components/computer/TerminalPanel.tsx +226 -0
- manigent/web/frontend/src/components/computer/TrustPanel.test.tsx +58 -0
- manigent/web/frontend/src/components/computer/TrustPanel.tsx +143 -0
- manigent/web/frontend/src/components/computer/computerCopy.test.ts +322 -0
- manigent/web/frontend/src/components/computer/computerCopy.ts +413 -0
- manigent/web/frontend/src/components/computer/sections.ts +21 -0
- manigent/web/frontend/src/components/computer/terminalProtocol.test.ts +135 -0
- manigent/web/frontend/src/components/computer/terminalProtocol.ts +170 -0
- manigent/web/frontend/src/components/context/ContextGuide.test.tsx +26 -0
- manigent/web/frontend/src/components/context/ContextGuide.tsx +144 -0
- manigent/web/frontend/src/components/context/ContextLanding.tsx +46 -0
- manigent/web/frontend/src/components/desk/CardBand.a11y.test.tsx +138 -0
- manigent/web/frontend/src/components/desk/CardBand.tsx +132 -0
- manigent/web/frontend/src/components/desk/DashTile.tsx +81 -0
- manigent/web/frontend/src/components/desk/DeskAgentPicker.retained.test.tsx +79 -0
- manigent/web/frontend/src/components/desk/DeskAgentPicker.tsx +151 -0
- manigent/web/frontend/src/components/desk/DeskComposer.detachTodo.test.tsx +233 -0
- manigent/web/frontend/src/components/desk/DeskComposer.dispatch.test.tsx +208 -0
- manigent/web/frontend/src/components/desk/DeskComposer.loadTodoDraft.test.tsx +242 -0
- manigent/web/frontend/src/components/desk/DeskComposer.restage.test.tsx +258 -0
- manigent/web/frontend/src/components/desk/DeskComposer.saveTodo.test.tsx +265 -0
- manigent/web/frontend/src/components/desk/DeskComposer.tsx +828 -0
- manigent/web/frontend/src/components/desk/DeskPage.crossHighlight.test.tsx +427 -0
- manigent/web/frontend/src/components/desk/DeskPage.link.test.tsx +271 -0
- manigent/web/frontend/src/components/desk/DeskPage.tsx +1066 -0
- manigent/web/frontend/src/components/desk/DeskPage.welcomeTour.test.tsx +262 -0
- manigent/web/frontend/src/components/desk/DeskSideRail.tsx +100 -0
- manigent/web/frontend/src/components/desk/DeskWelcomeTour.tsx +513 -0
- manigent/web/frontend/src/components/desk/DeskZeroState.tsx +45 -0
- manigent/web/frontend/src/components/desk/LabelChip.tsx +147 -0
- manigent/web/frontend/src/components/desk/LabelFilter.tsx +474 -0
- manigent/web/frontend/src/components/desk/LabelInput.test.tsx +439 -0
- manigent/web/frontend/src/components/desk/LabelInput.tsx +477 -0
- manigent/web/frontend/src/components/desk/LabelManageSheet.test.tsx +384 -0
- manigent/web/frontend/src/components/desk/LabelManageSheet.tsx +498 -0
- manigent/web/frontend/src/components/desk/PhaseRail.test.tsx +170 -0
- manigent/web/frontend/src/components/desk/PhaseRail.tsx +120 -0
- manigent/web/frontend/src/components/desk/PlanBand.test.tsx +655 -0
- manigent/web/frontend/src/components/desk/PlanBand.tsx +216 -0
- manigent/web/frontend/src/components/desk/SaveTodoModal.tsx +126 -0
- manigent/web/frontend/src/components/desk/SopEditor.tsx +280 -0
- manigent/web/frontend/src/components/desk/SopItem.tsx +171 -0
- manigent/web/frontend/src/components/desk/SopRail.test.tsx +517 -0
- manigent/web/frontend/src/components/desk/SopRail.tsx +250 -0
- manigent/web/frontend/src/components/desk/TicketChip.tsx +51 -0
- manigent/web/frontend/src/components/desk/TodoItem.attachments.test.tsx +96 -0
- manigent/web/frontend/src/components/desk/TodoItem.menu.test.tsx +142 -0
- manigent/web/frontend/src/components/desk/TodoItem.tsx +573 -0
- manigent/web/frontend/src/components/desk/TodoRail.longpress.test.tsx +396 -0
- manigent/web/frontend/src/components/desk/TodoRail.test.tsx +1155 -0
- manigent/web/frontend/src/components/desk/TodoRail.tsx +918 -0
- manigent/web/frontend/src/components/desk/TodoRowMenu.tsx +147 -0
- manigent/web/frontend/src/components/desk/UpdateCard.bands.test.tsx +308 -0
- manigent/web/frontend/src/components/desk/UpdateCard.sendBack.test.tsx +444 -0
- manigent/web/frontend/src/components/desk/UpdateCard.tsx +943 -0
- manigent/web/frontend/src/components/desk/UpdateCard.untrusted.test.tsx +161 -0
- manigent/web/frontend/src/components/desk/desk.css +1921 -0
- manigent/web/frontend/src/components/desk/desk.css.test.ts +582 -0
- manigent/web/frontend/src/components/desk/deskBands.test.ts +389 -0
- manigent/web/frontend/src/components/desk/deskBands.ts +226 -0
- manigent/web/frontend/src/components/desk/m16Surfaces.test.ts +134 -0
- manigent/web/frontend/src/components/desk/sopSyntax.ts +73 -0
- manigent/web/frontend/src/components/discovery/AgentDirectory.tsx +71 -0
- manigent/web/frontend/src/components/discovery/AgentFilterBar.tsx +46 -0
- manigent/web/frontend/src/components/discovery/AgentSearchBar.tsx +56 -0
- manigent/web/frontend/src/components/dm/AgentSummaryCard.tsx +154 -0
- manigent/web/frontend/src/components/dm/ComposeFields.tsx +145 -0
- manigent/web/frontend/src/components/dm/DMChatView.flow.test.tsx +374 -0
- manigent/web/frontend/src/components/dm/DMChatView.tsx +841 -0
- manigent/web/frontend/src/components/dm/DMZeroState.tsx +204 -0
- manigent/web/frontend/src/components/dm/DmComposeModal.tsx +301 -0
- manigent/web/frontend/src/components/dm/PersonalizeModal.tsx +366 -0
- manigent/web/frontend/src/components/dm/index.ts +1 -0
- manigent/web/frontend/src/components/files/FileItem.tsx +138 -0
- manigent/web/frontend/src/components/files/FileList.outside.test.tsx +29 -0
- manigent/web/frontend/src/components/files/FileList.tsx +48 -0
- manigent/web/frontend/src/components/files/FilePreview.test.tsx +75 -0
- manigent/web/frontend/src/components/files/FilePreview.tsx +325 -0
- manigent/web/frontend/src/components/files/FileViewerModal.test.tsx +157 -0
- manigent/web/frontend/src/components/files/FileViewerModal.tsx +261 -0
- manigent/web/frontend/src/components/files/HighlightedCode.tsx +39 -0
- manigent/web/frontend/src/components/files/SaveToAgentPanel.test.tsx +156 -0
- manigent/web/frontend/src/components/files/SaveToAgentPanel.tsx +545 -0
- manigent/web/frontend/src/components/files/index.ts +3 -0
- manigent/web/frontend/src/components/hero/AgentAvatar.tsx +66 -0
- manigent/web/frontend/src/components/hero/Button.tsx +66 -0
- manigent/web/frontend/src/components/hero/Hero.tsx +133 -0
- manigent/web/frontend/src/components/homefs/HomeFolderBrowser.test.tsx +393 -0
- manigent/web/frontend/src/components/homefs/HomeFolderBrowser.tsx +792 -0
- manigent/web/frontend/src/components/hub/BridgeDeliveryLine.test.tsx +40 -0
- manigent/web/frontend/src/components/hub/BridgeDeliveryLine.tsx +36 -0
- manigent/web/frontend/src/components/hub/HubAvatar.test.tsx +93 -0
- manigent/web/frontend/src/components/hub/HubAvatar.tsx +33 -0
- manigent/web/frontend/src/components/hub/HubChatView.test.tsx +695 -0
- manigent/web/frontend/src/components/hub/HubChatView.tsx +504 -0
- manigent/web/frontend/src/components/hub/HubContactLabel.test.tsx +92 -0
- manigent/web/frontend/src/components/hub/HubContactLabel.tsx +47 -0
- manigent/web/frontend/src/components/hub/HubDeliveryLine.tsx +49 -0
- manigent/web/frontend/src/components/hub/HubDisclosure.tsx +49 -0
- manigent/web/frontend/src/components/hub/HubEnrollmentDialog.test.tsx +144 -0
- manigent/web/frontend/src/components/hub/HubEnrollmentDialog.tsx +244 -0
- manigent/web/frontend/src/components/hub/HubMessageBody.test.tsx +26 -0
- manigent/web/frontend/src/components/hub/HubMessageBody.tsx +14 -0
- manigent/web/frontend/src/components/hub/HubOfflineConfirm.tsx +45 -0
- manigent/web/frontend/src/components/hub/HubThreadBanner.tsx +33 -0
- manigent/web/frontend/src/components/hub/HubThreadGuard.tsx +59 -0
- manigent/web/frontend/src/components/hub/RemoteAgentCard.test.tsx +50 -0
- manigent/web/frontend/src/components/hub/RemoteAgentCard.tsx +174 -0
- manigent/web/frontend/src/components/hub/RemoteAgentSection.test.tsx +446 -0
- manigent/web/frontend/src/components/hub/RemoteAgentSection.tsx +219 -0
- manigent/web/frontend/src/components/hub/ReportDialog.test.tsx +96 -0
- manigent/web/frontend/src/components/hub/ReportDialog.tsx +162 -0
- manigent/web/frontend/src/components/hub/RetainButton.test.tsx +103 -0
- manigent/web/frontend/src/components/hub/RetainButton.tsx +114 -0
- manigent/web/frontend/src/components/hub/RetainDialog.test.tsx +68 -0
- manigent/web/frontend/src/components/hub/RetainDialog.tsx +115 -0
- manigent/web/frontend/src/components/hub/RetainedList.test.tsx +94 -0
- manigent/web/frontend/src/components/hub/RetainedList.tsx +32 -0
- manigent/web/frontend/src/components/hub/RetainedRow.tsx +129 -0
- manigent/web/frontend/src/components/hub/StandInChip.test.tsx +37 -0
- manigent/web/frontend/src/components/hub/StandInChip.tsx +43 -0
- manigent/web/frontend/src/components/hub/UnusualNameChip.test.tsx +81 -0
- manigent/web/frontend/src/components/hub/UnusualNameChip.tsx +22 -0
- manigent/web/frontend/src/components/hub/hubLint.test.ts +265 -0
- manigent/web/frontend/src/components/landing/ComparisonSection.tsx +92 -0
- manigent/web/frontend/src/components/landing/ConsoleSection.tsx +402 -0
- manigent/web/frontend/src/components/landing/CrewSection.tsx +224 -0
- manigent/web/frontend/src/components/landing/FAQ.tsx +68 -0
- manigent/web/frontend/src/components/landing/Footer.tsx +58 -0
- manigent/web/frontend/src/components/landing/HowItWorks.tsx +117 -0
- manigent/web/frontend/src/components/landing/KnowledgeSection.tsx +91 -0
- manigent/web/frontend/src/components/landing/LandingPage.test.tsx +134 -0
- manigent/web/frontend/src/components/landing/LearningSection.tsx +486 -0
- manigent/web/frontend/src/components/landing/Nav.tsx +68 -0
- manigent/web/frontend/src/components/landing/Pricing.tsx +263 -0
- manigent/web/frontend/src/components/landing/Section.tsx +98 -0
- manigent/web/frontend/src/components/landing/StaffingSection.tsx +122 -0
- manigent/web/frontend/src/components/landing/WhyNowSection.tsx +92 -0
- manigent/web/frontend/src/components/landing/WhyTeamSection.tsx +73 -0
- manigent/web/frontend/src/components/landing/earlyAccess.ts +23 -0
- manigent/web/frontend/src/components/layout/AppLayout.tsx +77 -0
- manigent/web/frontend/src/components/layout/Sidebar.tsx +172 -0
- manigent/web/frontend/src/components/layout/index.ts +2 -0
- manigent/web/frontend/src/components/project/ActivityFeed.tsx +247 -0
- manigent/web/frontend/src/components/project/CreateProjectPage.tsx +543 -0
- manigent/web/frontend/src/components/project/DeleteProjectButton.tsx +39 -0
- manigent/web/frontend/src/components/project/ProjectPlanTab.phase.test.tsx +107 -0
- manigent/web/frontend/src/components/project/ProjectPlanTab.test.tsx +994 -0
- manigent/web/frontend/src/components/project/ProjectPlanTab.tsx +166 -0
- manigent/web/frontend/src/components/project/ProjectReportTab.tsx +139 -0
- manigent/web/frontend/src/components/project/ProjectView.tsx +390 -0
- manigent/web/frontend/src/components/project/index.ts +2 -0
- manigent/web/frontend/src/components/project/plan/PlanAcceptance.test.tsx +460 -0
- manigent/web/frontend/src/components/project/plan/PlanAcceptance.tsx +111 -0
- manigent/web/frontend/src/components/project/plan/PlanBlockEdit.tsx +111 -0
- manigent/web/frontend/src/components/project/plan/PlanBlockEditing.test.tsx +1132 -0
- manigent/web/frontend/src/components/project/plan/PlanBody.test.tsx +495 -0
- manigent/web/frontend/src/components/project/plan/PlanBody.tsx +992 -0
- manigent/web/frontend/src/components/project/plan/PlanCollide.tsx +254 -0
- manigent/web/frontend/src/components/project/plan/PlanCommentList.test.tsx +420 -0
- manigent/web/frontend/src/components/project/plan/PlanCommentList.tsx +391 -0
- manigent/web/frontend/src/components/project/plan/PlanDecisionNudge.test.tsx +226 -0
- manigent/web/frontend/src/components/project/plan/PlanDecisionNudge.tsx +128 -0
- manigent/web/frontend/src/components/project/plan/PlanDestructiveAccept.test.tsx +209 -0
- manigent/web/frontend/src/components/project/plan/PlanDiff.test.tsx +126 -0
- manigent/web/frontend/src/components/project/plan/PlanDiff.tsx +131 -0
- manigent/web/frontend/src/components/project/plan/PlanDiffSeries.test.tsx +500 -0
- manigent/web/frontend/src/components/project/plan/PlanDiffSeries.tsx +125 -0
- manigent/web/frontend/src/components/project/plan/PlanEditor.mounts.test.tsx +504 -0
- manigent/web/frontend/src/components/project/plan/PlanEditor.tsx +2159 -0
- manigent/web/frontend/src/components/project/plan/PlanHistory.tsx +69 -0
- manigent/web/frontend/src/components/project/plan/PlanInlineNotes.test.tsx +2398 -0
- manigent/web/frontend/src/components/project/plan/PlanNoteBase.test.tsx +215 -0
- manigent/web/frontend/src/components/project/plan/PlanNoteRow.test.tsx +562 -0
- manigent/web/frontend/src/components/project/plan/PlanNoteRow.tsx +908 -0
- manigent/web/frontend/src/components/project/plan/PlanQuotePop.tsx +254 -0
- manigent/web/frontend/src/components/project/plan/PlanStaleReconcile.test.tsx +244 -0
- manigent/web/frontend/src/components/project/plan/PlanSubmitWindow.test.tsx +281 -0
- manigent/web/frontend/src/components/project/plan/PlanTray.tsx +184 -0
- manigent/web/frontend/src/components/project/plan/PlanTrayMirror.test.tsx +242 -0
- manigent/web/frontend/src/components/project/plan/plan.css +1151 -0
- manigent/web/frontend/src/components/project/plan/planAnchor.test.tsx +459 -0
- manigent/web/frontend/src/components/project/plan/planAnchor.tsx +277 -0
- manigent/web/frontend/src/components/project/plan/planBlocks.test.tsx +470 -0
- manigent/web/frontend/src/components/project/plan/planBlocks.ts +242 -0
- manigent/web/frontend/src/components/project/plan/planContrast.test.ts +229 -0
- manigent/web/frontend/src/components/project/plan/planDiffs.test.ts +455 -0
- manigent/web/frontend/src/components/project/plan/planDiffs.ts +358 -0
- manigent/web/frontend/src/components/project/plan/planTestServer.ts +741 -0
- manigent/web/frontend/src/components/project/plan/planTransport.test.ts +160 -0
- manigent/web/frontend/src/components/project/plan/planTrayLogic.test.ts +1201 -0
- manigent/web/frontend/src/components/project/plan/planTrayLogic.ts +1164 -0
- manigent/web/frontend/src/components/publicAgents/PublicAgentsAccount.tsx +162 -0
- manigent/web/frontend/src/components/publicAgents/api.ts +55 -0
- manigent/web/frontend/src/components/publicAgents/parts.tsx +54 -0
- manigent/web/frontend/src/components/publicAgents/publicAgents.css +53 -0
- manigent/web/frontend/src/components/publicAgents/publicAgents.test.tsx +86 -0
- manigent/web/frontend/src/components/publicAgents/widget.test.ts +148 -0
- manigent/web/frontend/src/components/publicAgents/widget.ts +40 -0
- manigent/web/frontend/src/components/publicRequests/AgentDrawer.tsx +101 -0
- manigent/web/frontend/src/components/publicRequests/AgentPanel.tsx +173 -0
- manigent/web/frontend/src/components/publicRequests/AgentRankList.tsx +61 -0
- manigent/web/frontend/src/components/publicRequests/MatchRetainButton.tsx +67 -0
- manigent/web/frontend/src/components/publicRequests/ProgressBoard.tsx +74 -0
- manigent/web/frontend/src/components/publicRequests/PublicRequestsPage.tsx +57 -0
- manigent/web/frontend/src/components/publicRequests/RequestHome.tsx +99 -0
- manigent/web/frontend/src/components/publicRequests/RequestRoom.tsx +126 -0
- manigent/web/frontend/src/components/publicRequests/agentView.ts +65 -0
- manigent/web/frontend/src/components/publicRequests/publicRequests.css +209 -0
- manigent/web/frontend/src/components/publicRequests/publicRequests.test.tsx +172 -0
- manigent/web/frontend/src/components/publicRequests/requestCopy.ts +121 -0
- manigent/web/frontend/src/components/sample-request/SampleIdeaList.test.tsx +190 -0
- manigent/web/frontend/src/components/sample-request/SampleIdeaList.tsx +180 -0
- manigent/web/frontend/src/components/sample-request/SampleRequestBody.tsx +276 -0
- manigent/web/frontend/src/components/sample-request/assistantStarterSamples.ts +62 -0
- manigent/web/frontend/src/components/sample-request/sampleIdeaList.css +106 -0
- manigent/web/frontend/src/components/scheduled/CronPicker.tsx +80 -0
- manigent/web/frontend/src/components/scheduled/ScheduleMessageModal.tsx +341 -0
- manigent/web/frontend/src/components/scheduled/ScheduledMessagesList.tsx +157 -0
- manigent/web/frontend/src/components/settings/AgentSettingsPage.tsx +366 -0
- manigent/web/frontend/src/components/settings/ConnectedAccounts.tsx +277 -0
- manigent/web/frontend/src/components/settings/DefaultModelSection.test.tsx +96 -0
- manigent/web/frontend/src/components/settings/DefaultModelSection.tsx +212 -0
- manigent/web/frontend/src/components/settings/DisconnectConfirmModal.tsx +93 -0
- manigent/web/frontend/src/components/settings/FrontDeskSettingsPage.isolation.test.tsx +66 -0
- manigent/web/frontend/src/components/settings/FrontDeskSettingsPage.tsx +134 -0
- manigent/web/frontend/src/components/settings/InvitableAgentsPicker.tsx +182 -0
- manigent/web/frontend/src/components/settings/KnowledgePacksSection.tsx +867 -0
- manigent/web/frontend/src/components/settings/ModelConfigDialog.catalogDriven.test.tsx +79 -0
- manigent/web/frontend/src/components/settings/ModelConfigDialog.tsx +343 -0
- manigent/web/frontend/src/components/settings/ModelConfigSection.smartRouting.test.tsx +109 -0
- manigent/web/frontend/src/components/settings/ModelConfigSection.tsx +599 -0
- manigent/web/frontend/src/components/settings/ReflectionScheduleSection.tsx +170 -0
- manigent/web/frontend/src/components/settings/UserSettingsPage.tsx +168 -0
- manigent/web/frontend/src/components/settings/agent/AgentStatusStrip.tsx +71 -0
- manigent/web/frontend/src/components/settings/agent/BlockedDeploymentsCard.test.tsx +81 -0
- manigent/web/frontend/src/components/settings/agent/BlockedDeploymentsCard.tsx +62 -0
- manigent/web/frontend/src/components/settings/agent/ChannelIsolationLine.tsx +21 -0
- manigent/web/frontend/src/components/settings/agent/ChannelsSection.test.tsx +879 -0
- manigent/web/frontend/src/components/settings/agent/ChannelsSection.tsx +1124 -0
- manigent/web/frontend/src/components/settings/agent/EnvVarsCard.test.ts +15 -0
- manigent/web/frontend/src/components/settings/agent/EnvVarsCard.tsx +301 -0
- manigent/web/frontend/src/components/settings/agent/ExtraDirsList.tsx +85 -0
- manigent/web/frontend/src/components/settings/agent/GeneralSection.tsx +88 -0
- manigent/web/frontend/src/components/settings/agent/HostDisclaimerSlot.test.tsx +32 -0
- manigent/web/frontend/src/components/settings/agent/HostDisclaimerSlot.tsx +30 -0
- manigent/web/frontend/src/components/settings/agent/HubChannelCard.test.tsx +449 -0
- manigent/web/frontend/src/components/settings/agent/HubChannelCard.tsx +312 -0
- manigent/web/frontend/src/components/settings/agent/HubInvitableAgentsCard.test.tsx +83 -0
- manigent/web/frontend/src/components/settings/agent/HubInvitableAgentsCard.tsx +64 -0
- manigent/web/frontend/src/components/settings/agent/HubSendUsageCard.test.tsx +69 -0
- manigent/web/frontend/src/components/settings/agent/HubSendUsageCard.tsx +42 -0
- manigent/web/frontend/src/components/settings/agent/HubTurnOffDialog.tsx +88 -0
- manigent/web/frontend/src/components/settings/agent/IsolationRecheckButton.test.tsx +26 -0
- manigent/web/frontend/src/components/settings/agent/IsolationRecheckButton.tsx +68 -0
- manigent/web/frontend/src/components/settings/agent/KnowledgeSection.tsx +68 -0
- manigent/web/frontend/src/components/settings/agent/LineChannelCard.test.tsx +428 -0
- manigent/web/frontend/src/components/settings/agent/LineChannelCard.tsx +343 -0
- manigent/web/frontend/src/components/settings/agent/RuntimeSection.test.tsx +164 -0
- manigent/web/frontend/src/components/settings/agent/RuntimeSection.tsx +118 -0
- manigent/web/frontend/src/components/settings/agent/StorageSection.tsx +78 -0
- manigent/web/frontend/src/components/settings/agent/WebChatChannelCard.test.tsx +253 -0
- manigent/web/frontend/src/components/settings/agent/WebChatChannelCard.tsx +401 -0
- manigent/web/frontend/src/components/settings/agent/WhatsAppChannelCard.test.tsx +462 -0
- manigent/web/frontend/src/components/settings/agent/WhatsAppChannelCard.tsx +289 -0
- manigent/web/frontend/src/components/settings/agent/agentSettings.test.tsx +109 -0
- manigent/web/frontend/src/components/settings/agent/channelSetupGuides.test.tsx +162 -0
- manigent/web/frontend/src/components/settings/agent/channelSetupGuides.tsx +119 -0
- manigent/web/frontend/src/components/settings/agent/channelsShared.ts +83 -0
- manigent/web/frontend/src/components/settings/agent/fields.tsx +67 -0
- manigent/web/frontend/src/components/settings/agent/sections.ts +36 -0
- manigent/web/frontend/src/components/settings/agent/useAgentSettingsDraft.ts +129 -0
- manigent/web/frontend/src/components/settings/agent/useInstallSelection.ts +38 -0
- manigent/web/frontend/src/components/settings/primitives.tsx +98 -0
- manigent/web/frontend/src/components/shared/ExpandToggle.tsx +45 -0
- manigent/web/frontend/src/components/shared/MarkdownBody.tsx +218 -0
- manigent/web/frontend/src/components/shared/MarkdownBody.untrusted.test.tsx +97 -0
- manigent/web/frontend/src/components/shared/RenderBody.tsx +90 -0
- manigent/web/frontend/src/components/shared/rehypeHideComments.test.tsx +115 -0
- manigent/web/frontend/src/components/shared/rehypeHideComments.ts +124 -0
- manigent/web/frontend/src/components/sharing/AcceptSharePage.tsx +141 -0
- manigent/web/frontend/src/components/sharing/ShareLandingPage.tsx +89 -0
- manigent/web/frontend/src/components/sidebar/AccountChip.test.tsx +337 -0
- manigent/web/frontend/src/components/sidebar/AccountChip.tsx +407 -0
- manigent/web/frontend/src/components/sidebar/ActiveWorkDot.tsx +9 -0
- manigent/web/frontend/src/components/sidebar/AgentProfile.tsx +44 -0
- manigent/web/frontend/src/components/sidebar/AgentRow.tsx +71 -0
- manigent/web/frontend/src/components/sidebar/AgentsPanel.retained.test.tsx +105 -0
- manigent/web/frontend/src/components/sidebar/AgentsPanel.tsx +231 -0
- manigent/web/frontend/src/components/sidebar/AssistantHero.tsx +48 -0
- manigent/web/frontend/src/components/sidebar/ChatroomItem.tsx +48 -0
- manigent/web/frontend/src/components/sidebar/CommandPalette.channelThreads.test.tsx +76 -0
- manigent/web/frontend/src/components/sidebar/CommandPalette.reopen.test.tsx +206 -0
- manigent/web/frontend/src/components/sidebar/CommandPalette.test.tsx +725 -0
- manigent/web/frontend/src/components/sidebar/CommandPalette.tsx +774 -0
- manigent/web/frontend/src/components/sidebar/ComputersPanel.tsx +144 -0
- manigent/web/frontend/src/components/sidebar/DmIcon.tsx +46 -0
- manigent/web/frontend/src/components/sidebar/DmsPanel.tsx +226 -0
- manigent/web/frontend/src/components/sidebar/FrontDeskPanel.channels.test.tsx +582 -0
- manigent/web/frontend/src/components/sidebar/FrontDeskPanel.test.tsx +151 -0
- manigent/web/frontend/src/components/sidebar/FrontDeskPanel.tsx +454 -0
- manigent/web/frontend/src/components/sidebar/HotkeysHelp.tsx +101 -0
- manigent/web/frontend/src/components/sidebar/MessageHitRow.test.tsx +84 -0
- manigent/web/frontend/src/components/sidebar/MessageHitRow.tsx +57 -0
- manigent/web/frontend/src/components/sidebar/MyDeskRailPanel.crossHighlight.test.tsx +247 -0
- manigent/web/frontend/src/components/sidebar/MyDeskRailPanel.delete.test.tsx +273 -0
- manigent/web/frontend/src/components/sidebar/MyDeskRailPanel.tsx +351 -0
- manigent/web/frontend/src/components/sidebar/PanelShell.tsx +48 -0
- manigent/web/frontend/src/components/sidebar/ProjectItem.tsx +161 -0
- manigent/web/frontend/src/components/sidebar/ProjectRow.tsx +151 -0
- manigent/web/frontend/src/components/sidebar/ProjectsPanel.tsx +80 -0
- manigent/web/frontend/src/components/sidebar/PublicPanel.counterpart.test.tsx +85 -0
- manigent/web/frontend/src/components/sidebar/PublicPanel.hub.test.tsx +147 -0
- manigent/web/frontend/src/components/sidebar/PublicPanel.tsx +271 -0
- manigent/web/frontend/src/components/sidebar/RecencyHeader.tsx +17 -0
- manigent/web/frontend/src/components/sidebar/ScheduledPanel.tsx +58 -0
- manigent/web/frontend/src/components/sidebar/SidebarAvatar.tsx +128 -0
- manigent/web/frontend/src/components/sidebar/SidebarRail.tsx +163 -0
- manigent/web/frontend/src/components/sidebar/TeamGroup.tsx +67 -0
- manigent/web/frontend/src/components/sidebar/dmPrefill.test.ts +69 -0
- manigent/web/frontend/src/components/sidebar/dmPrefill.ts +80 -0
- manigent/web/frontend/src/components/sidebar/index.ts +18 -0
- manigent/web/frontend/src/components/sidebar/railShortcuts.ts +18 -0
- manigent/web/frontend/src/components/today/lib.test.ts +70 -0
- manigent/web/frontend/src/components/today/lib.ts +296 -0
- manigent/web/frontend/src/components/today/today.css +113 -0
- manigent/web/frontend/src/components/welcome/RunnerSetup.css +253 -0
- manigent/web/frontend/src/components/welcome/RunnerSetup.tsx +148 -0
- manigent/web/frontend/src/components/welcome/WelcomePage.css +383 -0
- manigent/web/frontend/src/components/welcome/WelcomePage.tsx +329 -0
- manigent/web/frontend/src/components/welcome/welcomeCopy.test.ts +382 -0
- manigent/web/frontend/src/components/welcome/welcomeCopy.ts +313 -0
- manigent/web/frontend/src/config/llmProviders.test.ts +161 -0
- manigent/web/frontend/src/config/llmProviders.ts +109 -0
- manigent/web/frontend/src/config/providerIds.fixture.json +13 -0
- manigent/web/frontend/src/hooks/accountNavigation.test.tsx +138 -0
- manigent/web/frontend/src/hooks/index.ts +10 -0
- manigent/web/frontend/src/hooks/useActiveWorkBootstrap.ts +86 -0
- manigent/web/frontend/src/hooks/useAgentAccess.test.tsx +30 -0
- manigent/web/frontend/src/hooks/useAgentAccess.ts +204 -0
- manigent/web/frontend/src/hooks/useAgentSampleRequests.ts +27 -0
- manigent/web/frontend/src/hooks/useAgentSearch.ts +10 -0
- manigent/web/frontend/src/hooks/useAgents.ts +46 -0
- manigent/web/frontend/src/hooks/useAnnouncements.test.ts +112 -0
- manigent/web/frontend/src/hooks/useAnnouncements.ts +100 -0
- manigent/web/frontend/src/hooks/useAssistantAgent.ts +24 -0
- manigent/web/frontend/src/hooks/useAutoResizeTextarea.ts +18 -0
- manigent/web/frontend/src/hooks/useBridgeDeliveries.test.tsx +79 -0
- manigent/web/frontend/src/hooks/useBriefTabs.ts +122 -0
- manigent/web/frontend/src/hooks/useChannelThreads.ts +69 -0
- manigent/web/frontend/src/hooks/useComposeForm.ts +80 -0
- manigent/web/frontend/src/hooks/useComputers.ts +123 -0
- manigent/web/frontend/src/hooks/useContextFirstItem.ts +118 -0
- manigent/web/frontend/src/hooks/useCreateProject.ts +78 -0
- manigent/web/frontend/src/hooks/useDebouncedValue.ts +16 -0
- manigent/web/frontend/src/hooks/useDeleteProject.ts +27 -0
- manigent/web/frontend/src/hooks/useDeskDispatch.test.tsx +170 -0
- manigent/web/frontend/src/hooks/useDeskDispatch.ts +135 -0
- manigent/web/frontend/src/hooks/useDeskReadState.test.tsx +85 -0
- manigent/web/frontend/src/hooks/useDeskReadState.ts +150 -0
- manigent/web/frontend/src/hooks/useDeskRecentlyDone.test.tsx +222 -0
- manigent/web/frontend/src/hooks/useDeskRecentlyDone.ts +225 -0
- manigent/web/frontend/src/hooks/useDeskTour.ts +37 -0
- manigent/web/frontend/src/hooks/useDeskUpdates.channelThreads.test.tsx +91 -0
- manigent/web/frontend/src/hooks/useDeskUpdates.hubThreads.test.tsx +93 -0
- manigent/web/frontend/src/hooks/useDeskUpdates.ts +353 -0
- manigent/web/frontend/src/hooks/useDisconnectIntegration.ts +23 -0
- manigent/web/frontend/src/hooks/useDmThreads.ts +99 -0
- manigent/web/frontend/src/hooks/useFileDropzone.ts +93 -0
- manigent/web/frontend/src/hooks/useFileUpload.ts +38 -0
- manigent/web/frontend/src/hooks/useFiles.ts +98 -0
- manigent/web/frontend/src/hooks/useHub.test.tsx +237 -0
- manigent/web/frontend/src/hooks/useHub.ts +502 -0
- manigent/web/frontend/src/hooks/useInstall.ts +82 -0
- manigent/web/frontend/src/hooks/useLabels.test.tsx +294 -0
- manigent/web/frontend/src/hooks/useLabels.ts +287 -0
- manigent/web/frontend/src/hooks/useLogout.ts +49 -0
- manigent/web/frontend/src/hooks/useMentionDetection.ts +90 -0
- manigent/web/frontend/src/hooks/useMessageAttachments.deskLimit.test.ts +264 -0
- manigent/web/frontend/src/hooks/useMessageAttachments.ts +283 -0
- manigent/web/frontend/src/hooks/useMessageDeepLink.test.tsx +70 -0
- manigent/web/frontend/src/hooks/useMessageDeepLink.ts +32 -0
- manigent/web/frontend/src/hooks/useMessageSearch.ts +78 -0
- manigent/web/frontend/src/hooks/useModelConfigs.ts +178 -0
- manigent/web/frontend/src/hooks/useMyAgents.ts +18 -0
- manigent/web/frontend/src/hooks/usePointerReorder.ts +270 -0
- manigent/web/frontend/src/hooks/usePrefersReducedMotion.ts +18 -0
- manigent/web/frontend/src/hooks/useProjectPlan.ts +36 -0
- manigent/web/frontend/src/hooks/useProjectReport.ts +19 -0
- manigent/web/frontend/src/hooks/useProjects.ts +104 -0
- manigent/web/frontend/src/hooks/useProjectsWithChatrooms.ts +97 -0
- manigent/web/frontend/src/hooks/usePublicRequests.ts +89 -0
- manigent/web/frontend/src/hooks/useRecentAgents.ts +38 -0
- manigent/web/frontend/src/hooks/useRenameProject.test.tsx +185 -0
- manigent/web/frontend/src/hooks/useRenameProject.ts +192 -0
- manigent/web/frontend/src/hooks/useRoomTrust.ts +56 -0
- manigent/web/frontend/src/hooks/useSharing.ts +46 -0
- manigent/web/frontend/src/hooks/useSopRecents.ts +55 -0
- manigent/web/frontend/src/hooks/useSopSchedules.ts +25 -0
- manigent/web/frontend/src/hooks/useSops.ts +99 -0
- manigent/web/frontend/src/hooks/useSwitchAccount.ts +41 -0
- manigent/web/frontend/src/hooks/useTeamGroups.ts +48 -0
- manigent/web/frontend/src/hooks/useThreadOutbox.ts +68 -0
- manigent/web/frontend/src/hooks/useTodos.add.test.tsx +89 -0
- manigent/web/frontend/src/hooks/useTodos.linkProject.test.tsx +115 -0
- manigent/web/frontend/src/hooks/useTodos.saveDraft.test.tsx +188 -0
- manigent/web/frontend/src/hooks/useTodos.ts +396 -0
- manigent/web/frontend/src/hooks/useUntrustedBodies.test.tsx +87 -0
- manigent/web/frontend/src/hooks/useUntrustedBodies.ts +51 -0
- manigent/web/frontend/src/hooks/useWebSocket.briefTab.test.tsx +148 -0
- manigent/web/frontend/src/hooks/useWebSocket.channelThreads.test.tsx +82 -0
- manigent/web/frontend/src/hooks/useWebSocket.hostSync.test.tsx +92 -0
- manigent/web/frontend/src/hooks/useWebSocket.hub.test.tsx +114 -0
- manigent/web/frontend/src/hooks/useWebSocket.labelSync.test.tsx +168 -0
- manigent/web/frontend/src/hooks/useWebSocket.plan.test.tsx +168 -0
- manigent/web/frontend/src/hooks/useWebSocket.reconnect.test.tsx +166 -0
- manigent/web/frontend/src/hooks/useWebSocket.runnerVersions.test.tsx +167 -0
- manigent/web/frontend/src/hooks/useWebSocket.ts +495 -0
- manigent/web/frontend/src/hooks/useWindowFileDropGuard.ts +33 -0
- manigent/web/frontend/src/landing.tsx +11 -0
- manigent/web/frontend/src/main.tsx +17 -0
- manigent/web/frontend/src/private-beta.tsx +10 -0
- manigent/web/frontend/src/public.tsx +42 -0
- manigent/web/frontend/src/queryClient.ts +16 -0
- manigent/web/frontend/src/stores/activeWorkStore.ts +88 -0
- manigent/web/frontend/src/stores/announcementStore.test.ts +217 -0
- manigent/web/frontend/src/stores/announcementStore.ts +169 -0
- manigent/web/frontend/src/stores/authStore.multiSession.test.ts +304 -0
- manigent/web/frontend/src/stores/authStore.test.ts +235 -0
- manigent/web/frontend/src/stores/authStore.ts +693 -0
- manigent/web/frontend/src/stores/deskLabelViewStore.test.ts +162 -0
- manigent/web/frontend/src/stores/deskLabelViewStore.ts +234 -0
- manigent/web/frontend/src/stores/deskLinkStore.ts +54 -0
- manigent/web/frontend/src/stores/deskStore.test.ts +53 -0
- manigent/web/frontend/src/stores/deskStore.ts +93 -0
- manigent/web/frontend/src/stores/heldPostsStore.ts +78 -0
- manigent/web/frontend/src/stores/mcpAuthStore.ts +52 -0
- manigent/web/frontend/src/stores/modelSelectionStore.ts +33 -0
- manigent/web/frontend/src/stores/perUserStores.ts +79 -0
- manigent/web/frontend/src/stores/runnerVersionStore.test.ts +81 -0
- manigent/web/frontend/src/stores/runnerVersionStore.ts +60 -0
- manigent/web/frontend/src/stores/skillAuthStore.ts +54 -0
- manigent/web/frontend/src/stores/switchReset.test.ts +236 -0
- manigent/web/frontend/src/stores/tokenExp.ts +32 -0
- manigent/web/frontend/src/stores/uiStore.ts +186 -0
- manigent/web/frontend/src/stores/unreadStore.ts +204 -0
- manigent/web/frontend/src/stores/unsavedGuard.ts +22 -0
- manigent/web/frontend/src/styles/design/fonts/JetBrainsMono-Latin.woff2 +0 -0
- manigent/web/frontend/src/styles/design/fonts/Nunito-Latin.woff2 +0 -0
- manigent/web/frontend/src/styles/design/fonts/Nunito-LatinExt.woff2 +0 -0
- manigent/web/frontend/src/styles/design/tokens.css +160 -0
- manigent/web/frontend/src/styles/homepage.css +242 -0
- manigent/web/frontend/src/styles/index.css +126 -0
- manigent/web/frontend/src/styles/landing.css +52 -0
- manigent/web/frontend/src/test/authFixtures.ts +154 -0
- manigent/web/frontend/src/test/fetchHarness.ts +203 -0
- manigent/web/frontend/src/test/fixtures/channelThreads.smoke.json +42 -0
- manigent/web/frontend/src/test/hubFixtures.ts +212 -0
- manigent/web/frontend/src/test/queryWrapper.tsx +9 -0
- manigent/web/frontend/src/test/suiteHygiene.test.ts +212 -0
- manigent/web/frontend/src/types/channelThreadName.test.ts +16 -0
- manigent/web/frontend/src/types/index.ts +903 -0
- manigent/web/frontend/src/utils/agentAccessCopy.test.ts +53 -0
- manigent/web/frontend/src/utils/agentAccessCopy.ts +231 -0
- manigent/web/frontend/src/utils/agentDisplay.ts +81 -0
- manigent/web/frontend/src/utils/briefSchedule.ts +25 -0
- manigent/web/frontend/src/utils/channelThreads.test.ts +109 -0
- manigent/web/frontend/src/utils/channelThreads.ts +179 -0
- manigent/web/frontend/src/utils/chatroomDisplay.ts +11 -0
- manigent/web/frontend/src/utils/deskLinks.test.ts +155 -0
- manigent/web/frontend/src/utils/deskLinks.ts +115 -0
- manigent/web/frontend/src/utils/deskRecipient.ts +29 -0
- manigent/web/frontend/src/utils/formatDate.ts +102 -0
- manigent/web/frontend/src/utils/frontDesk.ts +65 -0
- manigent/web/frontend/src/utils/hubChannelView.test.ts +178 -0
- manigent/web/frontend/src/utils/hubChannelView.ts +307 -0
- manigent/web/frontend/src/utils/hubDelivery.test.ts +105 -0
- manigent/web/frontend/src/utils/hubDelivery.ts +137 -0
- manigent/web/frontend/src/utils/hubDisplay.test.ts +157 -0
- manigent/web/frontend/src/utils/hubDisplay.ts +221 -0
- manigent/web/frontend/src/utils/hubThread.test.ts +184 -0
- manigent/web/frontend/src/utils/hubThread.ts +247 -0
- manigent/web/frontend/src/utils/labelErrors.test.ts +127 -0
- manigent/web/frontend/src/utils/labelErrors.ts +123 -0
- manigent/web/frontend/src/utils/labelView.test.ts +846 -0
- manigent/web/frontend/src/utils/labelView.ts +760 -0
- manigent/web/frontend/src/utils/legalUrls.ts +11 -0
- manigent/web/frontend/src/utils/messageSender.test.ts +32 -0
- manigent/web/frontend/src/utils/messageSender.ts +29 -0
- manigent/web/frontend/src/utils/oauthScopeCopy.ts +73 -0
- manigent/web/frontend/src/utils/personalize.ts +49 -0
- manigent/web/frontend/src/utils/reachableAgents.test.ts +36 -0
- manigent/web/frontend/src/utils/reachableAgents.ts +24 -0
- manigent/web/frontend/src/utils/recencyBuckets.ts +61 -0
- manigent/web/frontend/src/utils/roomTrust.test.ts +94 -0
- manigent/web/frontend/src/utils/roomTrust.ts +111 -0
- manigent/web/frontend/src/utils/snippetHighlight.test.ts +63 -0
- manigent/web/frontend/src/utils/snippetHighlight.ts +42 -0
- manigent/web/frontend/src/utils/sopRecipient.ts +37 -0
- manigent/web/frontend/src/utils/sopRequest.test.ts +91 -0
- manigent/web/frontend/src/utils/sopRequest.ts +68 -0
- manigent/web/frontend/src/utils/sopSchedule.ts +26 -0
- manigent/web/frontend/src/utils/sopSection.test.ts +148 -0
- manigent/web/frontend/src/utils/sopSection.ts +143 -0
- manigent/web/frontend/src/utils/textSearch.test.ts +60 -0
- manigent/web/frontend/src/utils/textSearch.ts +43 -0
- manigent/web/frontend/src/utils/thoughtPartner.ts +26 -0
- manigent/web/frontend/src/utils/threadName.test.ts +131 -0
- manigent/web/frontend/src/utils/threadName.ts +122 -0
- manigent/web/frontend/src/utils/ticketView.test.ts +296 -0
- manigent/web/frontend/src/utils/ticketView.ts +175 -0
- manigent/web/frontend/src/utils/todoAttachments.desiredState.test.ts +180 -0
- manigent/web/frontend/src/utils/todoAttachments.ts +126 -0
- manigent/web/frontend/src/utils/todoDraft.test.ts +84 -0
- manigent/web/frontend/src/utils/todoDraft.ts +46 -0
- manigent/web/frontend/src/utils/todoSearch.test.ts +82 -0
- manigent/web/frontend/src/utils/todoSearch.ts +43 -0
- manigent/web/frontend/src/utils/versionMatch.test.ts +295 -0
- manigent/web/frontend/src/utils/versionMatch.ts +245 -0
- manigent/web/frontend/src/vite-env.d.ts +1 -0
- manigent/web/frontend/tailwind.config.js +86 -0
- manigent/web/frontend/tos.html +188 -0
- manigent/web/frontend/tsconfig.dts.json +25 -0
- manigent/web/frontend/tsconfig.json +26 -0
- manigent/web/frontend/tsconfig.node.json +11 -0
- manigent/web/frontend/vite.config.ts +41 -0
- manigent/web/frontend/vitest.config.ts +16 -0
- manigent/ws_edge/app.py +514 -0
- manigent/ws_edge/auth.py +104 -0
- manigent/ws_edge/link.py +282 -0
- manigent/ws_edge/registry.py +206 -0
- manigent/ws_edge/terminal_relay.py +269 -0
- manigent_server-0.1.0.dist-info/METADATA +138 -0
- manigent_server-0.1.0.dist-info/RECORD +1004 -0
- manigent_server-0.1.0.dist-info/WHEEL +4 -0
- manigent_server-0.1.0.dist-info/entry_points.txt +2 -0
- manigent_server-0.1.0.dist-info/licenses/LICENSE +68 -0
|
@@ -0,0 +1,1343 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""
|
|
3
|
+
manigent/models/project.py
|
|
4
|
+
Project model with Active Record persistence methods.
|
|
5
|
+
|
|
6
|
+
## Changelog-First Architecture
|
|
7
|
+
|
|
8
|
+
Project is a **frozen** Pydantic model (`model_config = {"frozen": True}`).
|
|
9
|
+
All mutations flow through the changelog (acting as a redo log), ensuring:
|
|
10
|
+
1. Atomic recording on the server before any local writes
|
|
11
|
+
2. Eventual consistency across all runners via sync
|
|
12
|
+
3. Idempotent replay for crash recovery
|
|
13
|
+
|
|
14
|
+
Direct mutation is prevented by the frozen config. Use `project.state()`
|
|
15
|
+
to access ProjectState for changelog-driven writes.
|
|
16
|
+
|
|
17
|
+
## Read/Write Separation
|
|
18
|
+
|
|
19
|
+
- **Project** (frozen): Read-only data representation, path properties,
|
|
20
|
+
association methods for loading related objects
|
|
21
|
+
- **ProjectState**: Handles all filesystem writes triggered by changelog
|
|
22
|
+
processing (create, complete, add_participant)
|
|
23
|
+
|
|
24
|
+
This separation ensures changes are visible to other agents only after
|
|
25
|
+
they flow through the distributed changelog system.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import unicodedata
|
|
32
|
+
from datetime import UTC, datetime
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from collections.abc import Iterable
|
|
35
|
+
from typing import TYPE_CHECKING, Callable, Literal, Optional
|
|
36
|
+
|
|
37
|
+
from pydantic import BaseModel, Field, PrivateAttr, computed_field
|
|
38
|
+
|
|
39
|
+
from ..sync.changelog import ProjectStatus
|
|
40
|
+
from ..utils.file_io import FileUtil
|
|
41
|
+
from .participant import Participant
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Project surface — the explicit project shape.
|
|
45
|
+
# "regular" — task-shaped project (PLAN.md / milestones / multi-party).
|
|
46
|
+
# "dm" — owned 1:1 DM (a user with their OWN agent), single
|
|
47
|
+
# user-communication room, DM prompt variant, auto-title.
|
|
48
|
+
# "frontdesk" — ONE END of a cross-account thread (Front Desk). Each end is a
|
|
49
|
+
# distinct project OWNED (created_by) by that side and bound
|
|
50
|
+
# 1-to-1 to the other end by a TunnelBinding. The requester end's
|
|
51
|
+
# coordinator is a foreign agent (an `external` ghost that never
|
|
52
|
+
# runs locally); the host end's coordinator is that same foreign
|
|
53
|
+
# agent running for real. "dm" and "frontdesk" are both
|
|
54
|
+
# "dm-shaped" (see Project.is_dm_shaped).
|
|
55
|
+
ProjectSurface = Literal["regular", "dm", "frontdesk"]
|
|
56
|
+
|
|
57
|
+
# The plan lifecycle (§7, D13). **Derived, never stored, and deliberately NOT a
|
|
58
|
+
# fourth ``ProjectStatus`` member** — a ``spec-ing`` *status* would break four
|
|
59
|
+
# shipped call sites: ``messages.py:1081`` and ``tunnel_subscriber.py:598``
|
|
60
|
+
# (a user commenting on their own un-accepted plan would flip it ACTIVE and end
|
|
61
|
+
# the stage — fatal on its own), ``action_validator.py`` (wrong violation, wrong
|
|
62
|
+
# reason) and ``ProjectView.tsx``'s ``statusVariant`` (missing key → undefined
|
|
63
|
+
# badge). Status answers *is this running*; phase answers *what stage of the
|
|
64
|
+
# plan is it in*, and they are different questions.
|
|
65
|
+
ProjectPhase = Literal["spec-ing", "executing", "complete", "failed"]
|
|
66
|
+
|
|
67
|
+
# Seeded label a fresh DM thread carries until its first exchange auto-titles it.
|
|
68
|
+
# Doubles as the fire-once latch for the auto-title trigger (agent.py) and the
|
|
69
|
+
# defensive ``expected_current`` sentinel on the rename endpoint — the trigger
|
|
70
|
+
# runs only while ``display_name == NEW_CHAT_PLACEHOLDER`` and the mutation flips
|
|
71
|
+
# it, so every later message reads a non-placeholder value and skips.
|
|
72
|
+
NEW_CHAT_PLACEHOLDER = "New chat"
|
|
73
|
+
|
|
74
|
+
# Max length of a thread display name, in Unicode CODE POINTS, measured AFTER
|
|
75
|
+
# ``normalize_thread_display_name`` has run. The single source of truth for the
|
|
76
|
+
# API, the frontend's inline counter, and the tests — when this number is
|
|
77
|
+
# revisited it must be a one-line change here.
|
|
78
|
+
#
|
|
79
|
+
# 60 is not a UX preference, it is a coupling constraint: the DM auto-titler
|
|
80
|
+
# (``models/agent.py:_clean_dm_title``, ``max_chars=60``) writes THIS field
|
|
81
|
+
# through THAT route, so the storage ceiling must be >= the generation ceiling
|
|
82
|
+
# or a shipped path starts 400-ing. See the plan's "why 60 and not 30". The
|
|
83
|
+
# "keep it short" intent is expressed by the frontend's soft counter at 40, not
|
|
84
|
+
# by a hard cap that breaks a shipped writer.
|
|
85
|
+
THREAD_DISPLAY_NAME_MAX_LEN = 60
|
|
86
|
+
|
|
87
|
+
# Invisible directional-formatting characters, stripped by
|
|
88
|
+
# ``normalize_thread_display_name``. They render as nothing but reorder the
|
|
89
|
+
# visible text around them, which makes a thread title a spoofing surface in
|
|
90
|
+
# every sidebar that renders it. ZWJ (U+200D) and VS16 (U+FE0F) are deliberately
|
|
91
|
+
# ABSENT: stripping every invisible would shatter emoji sequences (👨👩👧 would
|
|
92
|
+
# render as three separate people).
|
|
93
|
+
# Spelled with escapes on purpose: literal copies of these characters would be
|
|
94
|
+
# invisible in this file and in every diff of it.
|
|
95
|
+
_DISPLAY_NAME_BIDI_OVERRIDES = frozenset(
|
|
96
|
+
"\u200e\u200f" # LRM, RLM
|
|
97
|
+
"\u202a\u202b\u202c\u202d\u202e" # LRE, RLE, PDF, LRO, RLO
|
|
98
|
+
"\u2066\u2067\u2068\u2069" # LRI, RLI, FSI, PDI
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def normalize_thread_display_name(raw: str) -> str:
|
|
103
|
+
"""Canonicalize a user- or model-supplied thread name into its stored form.
|
|
104
|
+
|
|
105
|
+
Pipeline, in order — the order matters, since each step feeds the next:
|
|
106
|
+
|
|
107
|
+
1. NFC normalize, so ``e`` + U+0301 and ``é`` are one stored value with one
|
|
108
|
+
length. Without this, two visually identical names would disagree on
|
|
109
|
+
whether they fit under :data:`THREAD_DISPLAY_NAME_MAX_LEN`.
|
|
110
|
+
2. Drop the bidi overrides in :data:`_DISPLAY_NAME_BIDI_OVERRIDES` and the
|
|
111
|
+
Cc control characters — but NOT the Cc characters that are whitespace
|
|
112
|
+
(tab, newline, CR). Those are left for step 3 to turn into a space:
|
|
113
|
+
deleting them here instead would glue the words on either side together
|
|
114
|
+
(``"Trip\\tplan"`` -> ``"Tripplan"``).
|
|
115
|
+
3. Collapse every run of Unicode whitespace to a single U+0020 and strip
|
|
116
|
+
both ends. This IS the trim rule: ``" Trip plan \\n"`` stores as
|
|
117
|
+
``"Trip plan"``.
|
|
118
|
+
|
|
119
|
+
Steps 1-3 rewrite silently — a trailing space is not an error, it is
|
|
120
|
+
normalized away. Only two conditions raise, and their messages are
|
|
121
|
+
user-facing verbatim (the route passes ``str(e)`` straight into
|
|
122
|
+
``{"detail": ...}`` for inline display next to the input):
|
|
123
|
+
|
|
124
|
+
- empty after normalization
|
|
125
|
+
- longer than :data:`THREAD_DISPLAY_NAME_MAX_LEN` **code points**
|
|
126
|
+
|
|
127
|
+
Length is code points, not bytes and not grapheme clusters. A family emoji
|
|
128
|
+
is ~7 code points against the budget; a grapheme-aware count would need a
|
|
129
|
+
new dependency for a cosmetic gain. The frontend counts the same way
|
|
130
|
+
(``[...s].length``, NOT ``s.length``, which counts UTF-16 units and would
|
|
131
|
+
disagree above U+FFFF).
|
|
132
|
+
|
|
133
|
+
Pure and side-effect-free, so the route, the auto-title path and the tests
|
|
134
|
+
can all call it. It lives on the model rather than the route because it is
|
|
135
|
+
a rule about the *field*, not about HTTP.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
raw: The submitted name, straight off the wire.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The canonical value to persist.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
ValueError: Empty or over-length, carrying the user-facing message.
|
|
145
|
+
"""
|
|
146
|
+
text = unicodedata.normalize("NFC", raw)
|
|
147
|
+
text = "".join(
|
|
148
|
+
ch
|
|
149
|
+
for ch in text
|
|
150
|
+
if ch not in _DISPLAY_NAME_BIDI_OVERRIDES
|
|
151
|
+
and not (unicodedata.category(ch) == "Cc" and not ch.isspace())
|
|
152
|
+
)
|
|
153
|
+
text = " ".join(text.split())
|
|
154
|
+
|
|
155
|
+
if not text:
|
|
156
|
+
raise ValueError("Thread name cannot be empty.")
|
|
157
|
+
if len(text) > THREAD_DISPLAY_NAME_MAX_LEN:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"Thread name must be {THREAD_DISPLAY_NAME_MAX_LEN} characters or fewer."
|
|
160
|
+
)
|
|
161
|
+
return text
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Front Desk / DM-shaped project name shape: ``{requester}-fd-{agent_short}``.
|
|
165
|
+
# Mirrors the frontend FRONT_DESK_NAME_RE (web/frontend/src/types/index.ts).
|
|
166
|
+
# Group 1 is the requester prefix (a username for user-requester channels, or a
|
|
167
|
+
# requester agent's full name for cross-agent delegation).
|
|
168
|
+
_FRONT_DESK_NAME_RE = re.compile(r"^([a-z0-9][a-z0-9_-]*)-fd-([a-z0-9][a-z0-9_-]*)$")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if TYPE_CHECKING:
|
|
172
|
+
from .agent import Agent
|
|
173
|
+
from .context import ModelContext
|
|
174
|
+
from .chatroom import Chatroom
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _as_aware_utc(dt: datetime) -> datetime:
|
|
178
|
+
"""Coerce a datetime to timezone-aware UTC.
|
|
179
|
+
|
|
180
|
+
Timestamps in this system are written as UTC-aware isoformat strings, but a
|
|
181
|
+
legacy/hand-edited ``meta.json`` could carry a naive value. Normalizing
|
|
182
|
+
before any ``max()``/comparison avoids a ``TypeError`` from mixing aware and
|
|
183
|
+
naive datetimes.
|
|
184
|
+
"""
|
|
185
|
+
return dt.replace(tzinfo=UTC) if dt.tzinfo is None else dt
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class Project(BaseModel):
|
|
189
|
+
"""Project metadata with persistence and association methods.
|
|
190
|
+
|
|
191
|
+
A project represents a collaborative task with multiple chatrooms
|
|
192
|
+
and participating agents coordinated by a single coordinator agent.
|
|
193
|
+
"""
|
|
194
|
+
model_config = {"frozen": True}
|
|
195
|
+
|
|
196
|
+
id: str
|
|
197
|
+
name: str
|
|
198
|
+
coordinator_id: str # agent_id of coordinator
|
|
199
|
+
coordinator_name: str # name of coordinator (avoids lookup on workers)
|
|
200
|
+
request: str # original user prompt
|
|
201
|
+
participating_agents: list[str] = Field(default_factory=list)
|
|
202
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
203
|
+
created_by: str # user_id of creator (derived from auth)
|
|
204
|
+
agent_pool: str = Field(default="verified") # "self", "owned", "verified", or "all". "self" = coordinator only (used by own-DM); the resolver returns an empty invitable list.
|
|
205
|
+
agent_teams: list[str] = Field(default_factory=list) # Hard allowlist by user_team; pairs with agent_names. Empty teams + empty names = no filter (everyone in pool is invitable).
|
|
206
|
+
agent_names: list[str] = Field(default_factory=list) # Hard allowlist by agent display name (id, full name, or owner-relative short name); pairs with agent_teams. OR semantics across both lists.
|
|
207
|
+
surface: ProjectSurface = "regular" # Explicit project shape; "regular" | "dm"
|
|
208
|
+
display_name: Optional[str] = None # Model-set human label (regular projects AND dm threads).
|
|
209
|
+
# None only on legacy/pre-migration rows -> callers render `name` (the slug).
|
|
210
|
+
# Who a host-side conversation serves, when it serves someone other than
|
|
211
|
+
# the owner: "hub:{root_fp}/{user_id}" | "account:{user_id}" |
|
|
212
|
+
# "channel:{conn}/{user_key}" | "project:{id}". Set at creation, never
|
|
213
|
+
# changed. A project with one is isolated (``is_isolated``): its turns are
|
|
214
|
+
# requester-scoped and the server hands it only to a sandbox-capable runner.
|
|
215
|
+
requester_key: Optional[str] = None
|
|
216
|
+
# Kept out of every durable input (recent activity, reflection, briefings,
|
|
217
|
+
# search). Never cleared once set.
|
|
218
|
+
memory_excluded: bool = False
|
|
219
|
+
# Holds outside content as a whole (hub DM threads; projects created from a
|
|
220
|
+
# restricted turn). Never cleared once set. Read from old data only: it no
|
|
221
|
+
# longer isolates (plan-owner-isolation section 3).
|
|
222
|
+
remote_content: bool = False
|
|
223
|
+
# The outside party of a requester end: a hub stand-in, or another
|
|
224
|
+
# account's agent on a same-server Front Desk requester thread. The owning
|
|
225
|
+
# user coordinates such a project (the own-coordinator rule).
|
|
226
|
+
counterpart_id: Optional[str] = None
|
|
227
|
+
counterpart_name: Optional[str] = None
|
|
228
|
+
last_request_ts: Optional[datetime] = None # ts of the most recent non-ack USER message across the project.
|
|
229
|
+
last_response_ts: Optional[datetime] = None # ts of the most recent non-ack AGENT/ASSISTANT message.
|
|
230
|
+
# ISO timestamp of the project's published completion report (mirrors
|
|
231
|
+
# report.json's ``generated_at``). None when no report exists — including a
|
|
232
|
+
# completed project that has no report. Non-null EXACTLY when
|
|
233
|
+
# ``GET /projects/{id}/report`` would return a real report. Denormalized into
|
|
234
|
+
# meta.json on report upsert/delete (ProjectState.set_report_published_at) so
|
|
235
|
+
# the list scan reads it from the single meta.json read it already performs —
|
|
236
|
+
# no per-project report.json read (avoids the N+1). A stored field, not a
|
|
237
|
+
# @computed_field: a computed field re-reading report.json on every Project
|
|
238
|
+
# load would re-introduce that N+1 in list_all. Default None keeps it
|
|
239
|
+
# back-compatible for any meta.json predating the key. Rides model_dump() into
|
|
240
|
+
# GET /projects (admin list) and GET /projects/{id} (detail) with no route change.
|
|
241
|
+
report_published_at: Optional[datetime] = None
|
|
242
|
+
# ISO timestamp of the most recent write to this project's ``PLAN.md`` or its
|
|
243
|
+
# ``plan.json`` sidecar (D5, §6). Denormalized into meta.json **outside the
|
|
244
|
+
# changelog**, exactly as ``report_published_at`` is and for the same reason:
|
|
245
|
+
# the project list can badge "awaiting your acceptance" or sort on plan
|
|
246
|
+
# recency from the single meta.json read it already performs, with no
|
|
247
|
+
# per-project fetch of the plan. None on a project whose plan has never been
|
|
248
|
+
# touched, including every project that predates the feature — a stored
|
|
249
|
+
# field with a None default needs no migration and no backfill.
|
|
250
|
+
#
|
|
251
|
+
# NOT a @computed_field: re-deriving it would mean opening plan.json on every
|
|
252
|
+
# ``Project`` load, which is the N+1 the field exists to avoid.
|
|
253
|
+
plan_updated_at: Optional[datetime] = None
|
|
254
|
+
|
|
255
|
+
# ---- the plan lifecycle projection (D13, §7.2) --------------------------
|
|
256
|
+
# Five stored facts that make ``phase`` a pure function of owned fields and
|
|
257
|
+
# make the execution gate (§7.4) answerable from a runner's own meta.json.
|
|
258
|
+
#
|
|
259
|
+
# **They ride the changelog** (``PROJECT_PLAN_STATE``), unlike
|
|
260
|
+
# ``report_published_at`` / ``plan_updated_at`` above, and that difference is
|
|
261
|
+
# the whole point: those two are read by the *server's* project list, while
|
|
262
|
+
# these are read by the *runner* — in ``build_state_snapshot`` and in the
|
|
263
|
+
# coordinator's steady-state prompt block. A direct meta.json write on the
|
|
264
|
+
# server reaches no runner, so a gate built on one is inert in production.
|
|
265
|
+
# The sidecar (``plan.json``, server-side only) stays the source of truth;
|
|
266
|
+
# these are its projection, published from exactly one place.
|
|
267
|
+
#
|
|
268
|
+
# Written by ``init_plan_sidecar`` and by NOTHING else (§3.5, property 3):
|
|
269
|
+
# a project that predates this feature never gets one, so it reads
|
|
270
|
+
# ``executing`` and behaves exactly as it does today — no backfill, no scan,
|
|
271
|
+
# and a plan a human wrote by hand is never called un-specified.
|
|
272
|
+
plan_seeded_at: Optional[datetime] = None
|
|
273
|
+
plan_accepted_at: Optional[datetime] = None
|
|
274
|
+
# 0 is the "never accepted" sentinel and is kept INTERNAL — the wire nulls it
|
|
275
|
+
# (ruling 6.2, F-1), so nothing serializes ``accepted at revision 0``.
|
|
276
|
+
plan_accepted_revision: int = 0
|
|
277
|
+
# ``spec_digest`` as it read at acceptance. Paired with the synced PLAN.md it
|
|
278
|
+
# answers "did the spec move since acceptance" with no sidecar read.
|
|
279
|
+
plan_accepted_spec_digest: str = ""
|
|
280
|
+
# The projection of ``project_plan.open_notes_for_you`` — §3.3's ONE number,
|
|
281
|
+
# carried to the one process that cannot open the sidecar. Never recounted
|
|
282
|
+
# anywhere; a second implementation is the drift §3.3 refuses.
|
|
283
|
+
plan_open_notes: int = 0
|
|
284
|
+
# The projection of ``ProjectPlan.first_user_review_at``: when the OWNER
|
|
285
|
+
# first opened a review round on this plan. That is the spec lock's start
|
|
286
|
+
# line before acceptance, and the coordinator's steady-state prompt states
|
|
287
|
+
# the constraint off this field. **Monotone** — the publisher never clears
|
|
288
|
+
# it (``plan_state_spec``), because the one thing a coordinator must never
|
|
289
|
+
# be told is that the lock has lifted. ``None`` on every project that
|
|
290
|
+
# predates the field and on every plan the user has not looked at yet.
|
|
291
|
+
plan_user_reviewed_at: Optional[datetime] = None
|
|
292
|
+
# The projection of ``project_plan._plan_was_offered``: when the coordinator
|
|
293
|
+
# first put this plan in front of the user. The spec lock's OTHER half — a
|
|
294
|
+
# plan may be accepted without ever having been offered, and until it has
|
|
295
|
+
# been the keeper still owns the document, so the prompt must be able to
|
|
296
|
+
# tell the two apart. **Monotone** for the same reason its neighbour is.
|
|
297
|
+
# ``None`` on every project that predates the field and on every plan whose
|
|
298
|
+
# coordinator has not sent a review round yet.
|
|
299
|
+
plan_offered_at: Optional[datetime] = None
|
|
300
|
+
|
|
301
|
+
# Private runtime state (not serialized)
|
|
302
|
+
_ctx: Optional["ModelContext"] = PrivateAttr(default=None)
|
|
303
|
+
|
|
304
|
+
@computed_field
|
|
305
|
+
@property
|
|
306
|
+
def last_modified(self) -> datetime:
|
|
307
|
+
"""Recency key the sidebar sorts on (desc).
|
|
308
|
+
|
|
309
|
+
``max(last_request_ts, last_response_ts)`` with ``created_at`` as the
|
|
310
|
+
floor, so a brand-new / message-less project still has a deterministic,
|
|
311
|
+
non-null sort key. Decorated with ``@computed_field`` so it rides
|
|
312
|
+
``model_dump()`` into ``GET /projects`` and ``GET /projects/{id}``
|
|
313
|
+
without extra plumbing.
|
|
314
|
+
"""
|
|
315
|
+
candidates = [_as_aware_utc(self.created_at)]
|
|
316
|
+
if self.last_request_ts is not None:
|
|
317
|
+
candidates.append(_as_aware_utc(self.last_request_ts))
|
|
318
|
+
if self.last_response_ts is not None:
|
|
319
|
+
candidates.append(_as_aware_utc(self.last_response_ts))
|
|
320
|
+
return max(candidates)
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def ctx(self) -> "ModelContext":
|
|
324
|
+
"""Get the model context (required for I/O operations).
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
RuntimeError: If context is not set (use Project.get() to load with context)
|
|
328
|
+
"""
|
|
329
|
+
if self._ctx is None:
|
|
330
|
+
raise RuntimeError(
|
|
331
|
+
"Project requires ModelContext. "
|
|
332
|
+
"Use Project.get() to load with context."
|
|
333
|
+
)
|
|
334
|
+
return self._ctx
|
|
335
|
+
|
|
336
|
+
# -------------------------------------------------------------------------
|
|
337
|
+
# Derived Properties (always read fresh from filesystem)
|
|
338
|
+
# -------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
@computed_field
|
|
341
|
+
@property
|
|
342
|
+
def status(self) -> ProjectStatus:
|
|
343
|
+
"""Project status (always reads fresh from meta.json).
|
|
344
|
+
|
|
345
|
+
Decorated with ``@computed_field`` so Pydantic includes it in
|
|
346
|
+
``model_dump()`` / serialized HTTP responses (otherwise the
|
|
347
|
+
``GET /projects/{id}`` payload would silently omit ``status``,
|
|
348
|
+
and frontend gates like ``project.status === ProjectStatus.COMPLETED``
|
|
349
|
+
would never match).
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
ProjectStatus.ACTIVE or ProjectStatus.COMPLETED or ProjectStatus.FAILED
|
|
353
|
+
"""
|
|
354
|
+
# Through the stat-checked cache: still fresh on every access, but a
|
|
355
|
+
# project list no longer re-parses meta.json once more per project.
|
|
356
|
+
data = self.ctx.file_cache.read_json(self.meta_path)
|
|
357
|
+
return ProjectStatus(data["status"])
|
|
358
|
+
|
|
359
|
+
@computed_field
|
|
360
|
+
@property
|
|
361
|
+
def phase(self) -> ProjectPhase:
|
|
362
|
+
"""§3.5's plan lifecycle — a pure function of this project's own fields.
|
|
363
|
+
|
|
364
|
+
::
|
|
365
|
+
|
|
366
|
+
COMPLETED -> "complete"; FAILED -> "failed"
|
|
367
|
+
surface != "regular" -> "executing" # LOAD-BEARING THREE TIMES
|
|
368
|
+
plan_seeded_at and not plan_accepted_at -> "spec-ing"
|
|
369
|
+
else "executing"
|
|
370
|
+
|
|
371
|
+
The ``surface != "regular"`` branch keeps a **DM** out of a phase it has
|
|
372
|
+
no plan for; keeps a **front-desk** project — where ``plan_accepted_at``
|
|
373
|
+
is never set, because there is no user in the accepting role (§7.2) —
|
|
374
|
+
from being stranded in ``spec-ing`` for its entire life; and **is the
|
|
375
|
+
shape guard the execution gate reads**. A reviewer trimming a
|
|
376
|
+
redundant-looking branch would delete exactly this one.
|
|
377
|
+
|
|
378
|
+
``spec-ing`` requires **positive evidence of being unstarted**, and that
|
|
379
|
+
is the migration: derived from *"not accepted"* alone, every project that
|
|
380
|
+
exists today would flip to ``spec-ing`` on deploy — none has ever had a
|
|
381
|
+
plan accepted, because the feature did not exist — and every one of them
|
|
382
|
+
would stop at the gate.
|
|
383
|
+
|
|
384
|
+
**Not the wire value on a front-desk row.** ``PlanSummary.phase`` is
|
|
385
|
+
``null`` there (ruling 6.1, F-1) because a client must be able to tell
|
|
386
|
+
*"no acceptance model"* from *"not yet accepted"*; the lifecycle answer
|
|
387
|
+
here is still ``executing``, which is what the gate needs. Two questions,
|
|
388
|
+
two answers, one derivation — ``plan_summary_for`` nulls this, it does
|
|
389
|
+
not recompute it.
|
|
390
|
+
"""
|
|
391
|
+
status = self.status
|
|
392
|
+
if status == ProjectStatus.COMPLETED:
|
|
393
|
+
return "complete"
|
|
394
|
+
if status == ProjectStatus.FAILED:
|
|
395
|
+
return "failed"
|
|
396
|
+
if self.surface != "regular":
|
|
397
|
+
return "executing"
|
|
398
|
+
if self.plan_seeded_at is not None and self.plan_accepted_at is None:
|
|
399
|
+
return "spec-ing"
|
|
400
|
+
return "executing"
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def data_dir(self) -> Path:
|
|
404
|
+
"""Path to project data directory (projects/{name}-{id}/)."""
|
|
405
|
+
return self.ctx.projects_dir / f"{self.name}-{self.id}"
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def meta_dir(self) -> Path:
|
|
409
|
+
"""Path to project metadata directory (metadata/projects/{name}-{id}/)."""
|
|
410
|
+
return self.ctx.metadata_dir / f"{self.name}-{self.id}"
|
|
411
|
+
|
|
412
|
+
@property
|
|
413
|
+
def meta_path(self) -> Path:
|
|
414
|
+
"""Path to project meta.json file."""
|
|
415
|
+
return self.meta_dir / "meta.json"
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def chatrooms(self) -> list[str]:
|
|
419
|
+
"""List chatroom names by scanning metadata directory.
|
|
420
|
+
|
|
421
|
+
Derived from filesystem: lists directories under
|
|
422
|
+
metadata/projects/{name}-{id}/chatrooms/
|
|
423
|
+
|
|
424
|
+
A project with no rooms yet has **no** ``chatrooms/`` dir at all —
|
|
425
|
+
``ProjectState.create`` doesn't make one, it appears when the first room
|
|
426
|
+
does. That is an ordinary state (8 of 544 projects on the reference box),
|
|
427
|
+
so it answers ``[]`` rather than raising ``FileNotFoundError``: "this
|
|
428
|
+
project has no rooms" is data, not a failure. Raising here surfaced as a
|
|
429
|
+
500 on ``GET /projects/{id}/chatrooms`` for exactly those projects, and
|
|
430
|
+
would have turned ``results[pid] == []`` into ``errors[pid]`` on the batch
|
|
431
|
+
route — collapsing "no rooms" into "you can't see this", the one
|
|
432
|
+
distinction that envelope exists to preserve.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Sorted list of chatroom names; ``[]`` when the project has no rooms.
|
|
436
|
+
"""
|
|
437
|
+
chatrooms_dir = self.meta_dir / "chatrooms"
|
|
438
|
+
if not chatrooms_dir.is_dir():
|
|
439
|
+
return []
|
|
440
|
+
return sorted(d.name for d in chatrooms_dir.iterdir() if d.is_dir())
|
|
441
|
+
|
|
442
|
+
@property
|
|
443
|
+
def is_dm_project(self) -> bool:
|
|
444
|
+
"""Owned 1:1 DM ONLY (``surface == "dm"``).
|
|
445
|
+
|
|
446
|
+
Does NOT include Front Desk ends (``surface == "frontdesk"``). Use
|
|
447
|
+
:meth:`is_dm_shaped` wherever the old code meant "dm-shaped" rather than
|
|
448
|
+
"owned DM specifically" — see the §5 surface audit in the Front Desk plan.
|
|
449
|
+
"""
|
|
450
|
+
return self.surface == "dm"
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def is_frontdesk_project(self) -> bool:
|
|
454
|
+
"""One END of a cross-account Front Desk thread (``surface == "frontdesk"``)."""
|
|
455
|
+
return self.surface == "frontdesk"
|
|
456
|
+
|
|
457
|
+
@property
|
|
458
|
+
def is_dm_shaped(self) -> bool:
|
|
459
|
+
"""True for BOTH owned DMs and Front Desk ends.
|
|
460
|
+
|
|
461
|
+
Both carry a single ``user-communication`` room, use the DM prompt
|
|
462
|
+
variant (``is_dm=True``), and are auto-title eligible. This is the
|
|
463
|
+
predicate the auto-title trigger, prompt-variant selection, and the
|
|
464
|
+
DM-vs-milestone-setup branch key on — the widened successor to
|
|
465
|
+
``is_dm_project`` at those callsites.
|
|
466
|
+
"""
|
|
467
|
+
return self.surface in ("dm", "frontdesk")
|
|
468
|
+
|
|
469
|
+
def frontdesk_is_host_side(self, ctx: "ModelContext") -> bool:
|
|
470
|
+
"""True when the coordinator is owned by ``created_by`` (the terminal/host end).
|
|
471
|
+
|
|
472
|
+
Semantics: the project's coordinator agent is ``registered_by`` the same
|
|
473
|
+
user who created the project — i.e. someone reached IN to MY agent. This
|
|
474
|
+
is the **host / terminal** side of a cross-account thread (renders in the
|
|
475
|
+
owner's Front Desk section; tunnel detection treats it as terminal so it
|
|
476
|
+
never spawns an FD-of-an-FD). The **requester** side is the negation —
|
|
477
|
+
the coordinator is a foreign agent I do NOT own (I reached OUT).
|
|
478
|
+
|
|
479
|
+
Requires an Agent lookup because ``Project`` stores ``coordinator_id``,
|
|
480
|
+
not the coordinator's owner. Returns ``False`` when the coordinator or
|
|
481
|
+
``created_by`` cannot be resolved (fail-open to non-terminal).
|
|
482
|
+
|
|
483
|
+
NOTE: for an owned DM (``surface == "dm"``) this is also True (the agent
|
|
484
|
+
is my own) — which is correct for the terminal-gate usage in
|
|
485
|
+
``tunnel_subscriber`` (own DMs and legacy shared ``-fd-`` rows are both
|
|
486
|
+
terminal: cross-account delegation must not bleed a tunnel out of them).
|
|
487
|
+
"""
|
|
488
|
+
from .agent import Agent
|
|
489
|
+
if not self.created_by or self.counterpart_id:
|
|
490
|
+
return False # a requester end: the outside party is the counterpart
|
|
491
|
+
coordinator = Agent.get(self.coordinator_id, ctx)
|
|
492
|
+
if coordinator is None:
|
|
493
|
+
return False
|
|
494
|
+
return coordinator.registered_by == self.created_by
|
|
495
|
+
|
|
496
|
+
@property
|
|
497
|
+
def fd_requester_name(self) -> Optional[str]:
|
|
498
|
+
"""Requester prefix of a DM-shaped Front Desk project name
|
|
499
|
+
(``{requester}-fd-{agent_short}``), else None.
|
|
500
|
+
|
|
501
|
+
For user-requester channels this equals the requester's username; for
|
|
502
|
+
agent-requester (cross-agent delegation) channels it's the requester
|
|
503
|
+
agent's full name, which never matches a username — so it's inert in
|
|
504
|
+
user-JWT authorization. ``created_by`` stays the host (foreign agent's
|
|
505
|
+
owner); this lets the requester be authorized on their own channel
|
|
506
|
+
without flipping ownership.
|
|
507
|
+
"""
|
|
508
|
+
if not self.is_dm_project:
|
|
509
|
+
return None
|
|
510
|
+
m = _FRONT_DESK_NAME_RE.match(self.name)
|
|
511
|
+
return m.group(1) if m else None
|
|
512
|
+
|
|
513
|
+
# -------------------------------------------------------------------------
|
|
514
|
+
# Invitable allowlist (the unified (teams, names) policy for who can be
|
|
515
|
+
# invited to a chatroom in this project).
|
|
516
|
+
# -------------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def enforces_invitable_allowlist(self) -> bool:
|
|
520
|
+
"""Whether the chatroom-create gate should run the allowlist check.
|
|
521
|
+
|
|
522
|
+
Allowlist is only enforced when explicitly populated; an empty
|
|
523
|
+
allowlist means "no filter" (every agent in the pool is invitable).
|
|
524
|
+
``agent_pool="self"`` also forces enforcement so the resolver's
|
|
525
|
+
empty-list short-circuit is honored at the prompt callsite.
|
|
526
|
+
"""
|
|
527
|
+
return bool(self.agent_names or self.agent_teams) or self.agent_pool == "self"
|
|
528
|
+
|
|
529
|
+
def _invitable_viewer_owner_id(self, ctx: "ModelContext") -> Optional[str]:
|
|
530
|
+
"""Owner whose namespace short names resolve in for the allowlist check.
|
|
531
|
+
|
|
532
|
+
Always scopes to the project creator. Returns ``None`` when no owner is
|
|
533
|
+
recorded (legacy projects).
|
|
534
|
+
"""
|
|
535
|
+
return self.created_by
|
|
536
|
+
|
|
537
|
+
def matches_invitable(self, invitee: "Agent", ctx: "ModelContext") -> bool:
|
|
538
|
+
"""Does ``invitee`` pass this project's invitable allowlist?
|
|
539
|
+
|
|
540
|
+
Empty allowlist = nothing matches (callers that want "empty = pass"
|
|
541
|
+
should gate on ``enforces_invitable_allowlist`` first).
|
|
542
|
+
"""
|
|
543
|
+
allowed_names = set(self.agent_names)
|
|
544
|
+
allowed_teams = set(self.agent_teams)
|
|
545
|
+
if invitee.id in allowed_names:
|
|
546
|
+
return True
|
|
547
|
+
if invitee.name in allowed_names:
|
|
548
|
+
return True
|
|
549
|
+
viewer_owner_id = self._invitable_viewer_owner_id(ctx)
|
|
550
|
+
if (
|
|
551
|
+
viewer_owner_id
|
|
552
|
+
and invitee.registered_by == viewer_owner_id
|
|
553
|
+
and "-" in invitee.name
|
|
554
|
+
):
|
|
555
|
+
short = invitee.name.split("-", 1)[1]
|
|
556
|
+
if short in allowed_names:
|
|
557
|
+
return True
|
|
558
|
+
if allowed_teams and (allowed_teams & set(invitee.user_teams)):
|
|
559
|
+
return True
|
|
560
|
+
return False
|
|
561
|
+
|
|
562
|
+
def admits_hub_agent(self, hub_agent_id: str, handle: str) -> bool:
|
|
563
|
+
"""Does a retained hub agent pass this project's invitable allowlist?
|
|
564
|
+
|
|
565
|
+
The rule for hub agents, owned here so the server's room bridge and the
|
|
566
|
+
runner's invite check cannot drift: an empty allowlist admits,
|
|
567
|
+
``agent_pool="self"`` admits none, an ``agent_names`` entry matches the
|
|
568
|
+
``name@deployment`` handle or the hub agent id (case-insensitive), and
|
|
569
|
+
teams never match (a hub agent carries none of this owner's labels).
|
|
570
|
+
"""
|
|
571
|
+
if self.agent_pool == "self":
|
|
572
|
+
return False
|
|
573
|
+
if not self.agent_names and not self.agent_teams:
|
|
574
|
+
return True
|
|
575
|
+
refs = {hub_agent_id.lower(), handle.lower()}
|
|
576
|
+
return any(str(n).lower() in refs for n in self.agent_names)
|
|
577
|
+
|
|
578
|
+
def resolve_invitable_agents(
|
|
579
|
+
self,
|
|
580
|
+
candidates: "Iterable[Agent]",
|
|
581
|
+
ctx: "ModelContext",
|
|
582
|
+
) -> "list[Agent]":
|
|
583
|
+
"""Filter ``candidates`` down to those that pass this project's allowlist.
|
|
584
|
+
|
|
585
|
+
``agent_pool="self"`` short-circuits to an empty list — the
|
|
586
|
+
coordinator is the only allowed speaker (own-DM).
|
|
587
|
+
Empty allowlist = pass-through (returns the full list as-is).
|
|
588
|
+
"""
|
|
589
|
+
if self.agent_pool == "self":
|
|
590
|
+
return []
|
|
591
|
+
if not self.agent_teams and not self.agent_names:
|
|
592
|
+
return list(candidates)
|
|
593
|
+
return [c for c in candidates if self.matches_invitable(c, ctx)]
|
|
594
|
+
|
|
595
|
+
# -------------------------------------------------------------------------
|
|
596
|
+
# Association Methods (lookup-based, no caching)
|
|
597
|
+
# -------------------------------------------------------------------------
|
|
598
|
+
|
|
599
|
+
def list_chatrooms(self) -> list:
|
|
600
|
+
"""Load all chatrooms in this project.
|
|
601
|
+
|
|
602
|
+
Uses ``Chatroom.get_in(self, name)`` rather than ``Chatroom.get(id, name,
|
|
603
|
+
ctx)``: we already hold the Project, and ``Chatroom.get`` would re-resolve
|
|
604
|
+
it with a ``glob("*-{id}")`` over the whole metadata dir **once per
|
|
605
|
+
room**. On a 544-project box that re-glob is ~64–69% of the cost of
|
|
606
|
+
listing rooms; at P=20,000 it is ~1.5 s per 50 projects. Same results,
|
|
607
|
+
zero extra directory scans.
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
List of Chatroom objects
|
|
611
|
+
"""
|
|
612
|
+
from .chatroom import Chatroom
|
|
613
|
+
result = []
|
|
614
|
+
for chatroom_name in self.chatrooms:
|
|
615
|
+
room = Chatroom.get_in(self, chatroom_name)
|
|
616
|
+
if room is not None:
|
|
617
|
+
result.append(room)
|
|
618
|
+
return result
|
|
619
|
+
|
|
620
|
+
def get_chatroom(self, chatroom_name: str):
|
|
621
|
+
"""Load a specific chatroom.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
chatroom_name: The chatroom name
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
Chatroom, or None when the room doesn't exist (Chatroom.get contract)
|
|
628
|
+
"""
|
|
629
|
+
from .chatroom import Chatroom
|
|
630
|
+
return Chatroom.get_in(self, chatroom_name)
|
|
631
|
+
|
|
632
|
+
def get_shared_context_room(self):
|
|
633
|
+
"""Get the shared-context room for project-wide knowledge.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
Chatroom
|
|
637
|
+
"""
|
|
638
|
+
for room in self.list_chatrooms():
|
|
639
|
+
if room.is_shared_context_room:
|
|
640
|
+
return room
|
|
641
|
+
raise ValueError(f"shared-context room not found in project {self.id}")
|
|
642
|
+
|
|
643
|
+
def get_user_communication_room(self):
|
|
644
|
+
"""Get the user-communication room for user<->assistant chat.
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
Chatroom
|
|
648
|
+
"""
|
|
649
|
+
for room in self.list_chatrooms():
|
|
650
|
+
if room.is_user_communication_room:
|
|
651
|
+
return room
|
|
652
|
+
raise ValueError(f"user-communication room not found in project {self.id}")
|
|
653
|
+
|
|
654
|
+
def get_context_files(self) -> list[str]:
|
|
655
|
+
"""Get list of context files from shared-context chatroom.
|
|
656
|
+
|
|
657
|
+
Returns user-uploaded context files, excluding auto-generated files
|
|
658
|
+
(PLAN.md) which is refined during project setup.
|
|
659
|
+
|
|
660
|
+
Note: AGENTS.md is now a global file at the runner root, not per-project.
|
|
661
|
+
|
|
662
|
+
Returns:
|
|
663
|
+
List of filenames in shared-context (excluding PLAN.md)
|
|
664
|
+
"""
|
|
665
|
+
shared_context_room = self.get_shared_context_room()
|
|
666
|
+
all_files = shared_context_room.list_files()
|
|
667
|
+
excluded = {"PLAN.md"}
|
|
668
|
+
return [f for f in all_files if f not in excluded]
|
|
669
|
+
|
|
670
|
+
def list_participants(self) -> list:
|
|
671
|
+
"""Load participants for all project participants.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
List of Participant objects (Agent, Assistant, or User)
|
|
675
|
+
"""
|
|
676
|
+
result = []
|
|
677
|
+
for pid in self.participating_agents:
|
|
678
|
+
participant = Participant.get(pid, self.ctx)
|
|
679
|
+
result.append(participant)
|
|
680
|
+
return result
|
|
681
|
+
|
|
682
|
+
def get_coordinator(self):
|
|
683
|
+
"""Load the coordinator (typically an Assistant).
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
Participant (Agent, Assistant, or User) or None if not found
|
|
687
|
+
"""
|
|
688
|
+
return Participant.get(self.coordinator_id, self.ctx)
|
|
689
|
+
|
|
690
|
+
def get_chatrooms_for_participant(self, participant_id: str) -> list:
|
|
691
|
+
"""Get chatrooms where participant is a member.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
participant_id: The participant ID to filter by
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
List of Chatroom objects where participant is a member
|
|
698
|
+
"""
|
|
699
|
+
return [r for r in self.list_chatrooms() if participant_id in r.participants]
|
|
700
|
+
|
|
701
|
+
# -------------------------------------------------------------------------
|
|
702
|
+
# Active Record: Persistence Methods
|
|
703
|
+
# -------------------------------------------------------------------------
|
|
704
|
+
|
|
705
|
+
@classmethod
|
|
706
|
+
def get(cls, project_id: str, ctx: "ModelContext") -> "Project":
|
|
707
|
+
"""Load project by ID.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
project_id: The project ID
|
|
711
|
+
ctx: ModelContext for filesystem operations
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
Project
|
|
715
|
+
"""
|
|
716
|
+
# Find project directory by ID (glob for {name}-{id} pattern). Sort for
|
|
717
|
+
# determinism: glob order is filesystem-dependent, so an unsorted matches[0]
|
|
718
|
+
# could resolve to a different {name}-{id} dir across processes if a stale
|
|
719
|
+
# duplicate ever exists — yielding a different project.name and thus a
|
|
720
|
+
# different sandbox_dir between the writer and the action executor.
|
|
721
|
+
# The id comes straight from URLs, so it is matched literally: an id
|
|
722
|
+
# like "*" must not resolve to some other project.
|
|
723
|
+
if not FileUtil.is_safe_id(project_id):
|
|
724
|
+
raise ValueError(f"Project {project_id} not found")
|
|
725
|
+
matches = FileUtil.id_suffix_dirs(ctx.metadata_dir, project_id, "meta.json")
|
|
726
|
+
if not matches:
|
|
727
|
+
# Callers may pass the bare id or the full "{name}-{id}" slug (what
|
|
728
|
+
# an agent sees as its synced project dir). The glob above only
|
|
729
|
+
# matches the bare id; fall back to an exact dir match so the slug
|
|
730
|
+
# form resolves too.
|
|
731
|
+
exact = ctx.metadata_dir / project_id
|
|
732
|
+
if exact.is_dir():
|
|
733
|
+
matches = [exact]
|
|
734
|
+
if not matches:
|
|
735
|
+
raise ValueError(f"Project {project_id} not found")
|
|
736
|
+
meta_path = matches[0] / "meta.json"
|
|
737
|
+
data = FileUtil.read(meta_path, "json")
|
|
738
|
+
if not data:
|
|
739
|
+
raise ValueError(f"Project {project_id} has no metadata")
|
|
740
|
+
instance = cls.model_validate(data)
|
|
741
|
+
object.__setattr__(instance, "_ctx", ctx)
|
|
742
|
+
return instance
|
|
743
|
+
|
|
744
|
+
@classmethod
|
|
745
|
+
def get_many(
|
|
746
|
+
cls,
|
|
747
|
+
project_ids: Iterable[str],
|
|
748
|
+
ctx: "ModelContext",
|
|
749
|
+
) -> dict[str, "Project"]:
|
|
750
|
+
"""Batch sibling of :meth:`get` — resolve many ids in ONE directory pass.
|
|
751
|
+
|
|
752
|
+
``Project.get`` runs ``metadata_dir.glob(f"*-{project_id}")``, which is
|
|
753
|
+
O(P) in the number of projects on the box *per id*. Resolving N ids that
|
|
754
|
+
way is O(N·P): at P=20,000 and N=50 that measured 850 ms. This does one
|
|
755
|
+
``os.scandir`` of the same directory and answers every id from an
|
|
756
|
+
in-memory map — 20.0 ms for all 50 ids at P=20,000, which is 1.18× the
|
|
757
|
+
cost of a *single* glob.
|
|
758
|
+
|
|
759
|
+
The primitive is ``os.scandir`` + ``DirEntry.is_dir()`` deliberately, not
|
|
760
|
+
``Path.iterdir()`` and not ``Path.glob("*")``: both of those allocate a
|
|
761
|
+
``Path`` per entry, and ``Path.is_dir()`` is a ``stat()`` syscall per
|
|
762
|
+
entry (~6.8 µs). ``DirEntry.is_dir()`` is served from the dirent
|
|
763
|
+
``d_type``, measured at ~0.35 µs/entry. That per-entry work — not the
|
|
764
|
+
directory scan — is the whole gap (149.9 ms vs 20.0 ms at P=20,000).
|
|
765
|
+
|
|
766
|
+
Accepts the same two id forms ``get`` does, with the same precedence:
|
|
767
|
+
the bare id (matching the ``{name}-{id}`` directory *suffix*, i.e. what
|
|
768
|
+
``glob("*-{id}")`` matches) and — only when no suffix match exists — the
|
|
769
|
+
full ``{name}-{id}`` slug an agent sees as its synced project dir.
|
|
770
|
+
Deliberately NOT a uuid-shape heuristic: ``get``'s glob is a plain
|
|
771
|
+
suffix test, so hand-made fixture ids like ``proj-1`` must resolve
|
|
772
|
+
identically or the two resolvers disagree about ``project.name``.
|
|
773
|
+
|
|
774
|
+
Unlike ``get``, an id that does not resolve is simply **absent** from the
|
|
775
|
+
returned mapping rather than raising — the batch callers need absence,
|
|
776
|
+
not an exception, so one unknown id cannot fail a whole request.
|
|
777
|
+
``get``'s raising contract is untouched.
|
|
778
|
+
|
|
779
|
+
Args:
|
|
780
|
+
project_ids: Ids (or ``{name}-{id}`` slugs) to resolve.
|
|
781
|
+
ctx: ModelContext for filesystem operations.
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
``{requested_id: Project}`` for every id that resolved.
|
|
785
|
+
"""
|
|
786
|
+
wanted = {pid for pid in project_ids if pid}
|
|
787
|
+
if not wanted or not ctx.metadata_dir.exists():
|
|
788
|
+
return {}
|
|
789
|
+
|
|
790
|
+
# Requested ids share very few distinct lengths (every real id is a
|
|
791
|
+
# 36-char uuid4), so testing each dir name at just those offsets is the
|
|
792
|
+
# whole-listing equivalent of N globs at O(P) total instead of O(N·P) —
|
|
793
|
+
# no per-id rescan, and no `endswith` sweep over the cross product.
|
|
794
|
+
lengths = sorted({len(pid) for pid in wanted})
|
|
795
|
+
|
|
796
|
+
# `Project.get` tries `glob("*-{id}")` FIRST and only falls back to an
|
|
797
|
+
# exact-dir match, so the two forms are kept apart and merged with that
|
|
798
|
+
# same precedence below.
|
|
799
|
+
suffix_dir: dict[str, str] = {}
|
|
800
|
+
exact_dir: dict[str, str] = {}
|
|
801
|
+
|
|
802
|
+
def _offer(into: dict[str, str], key: str, dir_name: str) -> None:
|
|
803
|
+
# `Project.get` takes `sorted(matches)[0]`, so on the (pathological)
|
|
804
|
+
# duplicate-id case the lexicographically smallest dir wins. Match
|
|
805
|
+
# that, or the two resolvers could disagree about project.name.
|
|
806
|
+
current = into.get(key)
|
|
807
|
+
if current is None or dir_name < current:
|
|
808
|
+
into[key] = dir_name
|
|
809
|
+
|
|
810
|
+
with os.scandir(ctx.metadata_dir) as entries:
|
|
811
|
+
for entry in entries:
|
|
812
|
+
# `DirEntry.is_dir()` is served from the dirent `d_type` — no
|
|
813
|
+
# `stat()` per entry, which is the whole point of scandir here.
|
|
814
|
+
if not entry.is_dir():
|
|
815
|
+
continue
|
|
816
|
+
name = entry.name
|
|
817
|
+
if name in wanted:
|
|
818
|
+
_offer(exact_dir, name, name)
|
|
819
|
+
for length in lengths:
|
|
820
|
+
# Equivalent to `name.endswith(f"-{pid}")` for any wanted pid
|
|
821
|
+
# of this length, but one slice instead of a scan per id.
|
|
822
|
+
if len(name) > length and name[-length - 1] == "-":
|
|
823
|
+
candidate = name[-length:]
|
|
824
|
+
if candidate in wanted:
|
|
825
|
+
_offer(suffix_dir, candidate, name)
|
|
826
|
+
|
|
827
|
+
dir_for = {**exact_dir, **suffix_dir}
|
|
828
|
+
|
|
829
|
+
resolved: dict[str, "Project"] = {}
|
|
830
|
+
for pid, dir_name in dir_for.items():
|
|
831
|
+
data = FileUtil.read(ctx.metadata_dir / dir_name / "meta.json", "json")
|
|
832
|
+
if not data:
|
|
833
|
+
# Dir present but metadata missing/corrupt — same as "not found"
|
|
834
|
+
# for the caller, which reports one opaque code either way.
|
|
835
|
+
continue
|
|
836
|
+
instance = cls.model_validate(data)
|
|
837
|
+
object.__setattr__(instance, "_ctx", ctx)
|
|
838
|
+
resolved[pid] = instance
|
|
839
|
+
return resolved
|
|
840
|
+
|
|
841
|
+
@classmethod
|
|
842
|
+
def list_all(cls, ctx: "ModelContext") -> list["Project"]:
|
|
843
|
+
"""List all projects.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
ctx: ModelContext for filesystem operations
|
|
847
|
+
|
|
848
|
+
Returns:
|
|
849
|
+
List of Project objects
|
|
850
|
+
"""
|
|
851
|
+
return cls.list_where(ctx, lambda meta: True)
|
|
852
|
+
|
|
853
|
+
@classmethod
|
|
854
|
+
def list_where(
|
|
855
|
+
cls,
|
|
856
|
+
ctx: "ModelContext",
|
|
857
|
+
keep: Callable[[dict], bool],
|
|
858
|
+
) -> list["Project"]:
|
|
859
|
+
"""Projects whose raw ``meta.json`` dict passes ``keep``, in directory order.
|
|
860
|
+
|
|
861
|
+
Every ``meta.json`` is read through ``ctx.file_cache``, so a scan where
|
|
862
|
+
nothing changed is one ``stat`` per project and no JSON parsing. Only
|
|
863
|
+
the projects ``keep`` accepts are validated into models — validating
|
|
864
|
+
all ~1,300 to then throw most away was the other half of the cost.
|
|
865
|
+
``keep`` gets the cached dict and must not mutate it.
|
|
866
|
+
"""
|
|
867
|
+
result: list[Project] = []
|
|
868
|
+
if not ctx.metadata_dir.exists():
|
|
869
|
+
return result
|
|
870
|
+
with os.scandir(ctx.metadata_dir) as it:
|
|
871
|
+
# Sorted by dir name, matching the old `sorted(iterdir())` order.
|
|
872
|
+
# `DirEntry.is_dir()` comes from the dirent, so no stat per entry.
|
|
873
|
+
dirs = sorted(entry.name for entry in it if entry.is_dir())
|
|
874
|
+
# Plain string paths: at ~1,300 projects per call, building a `Path`
|
|
875
|
+
# per project costs more than the stat behind the cache check.
|
|
876
|
+
base = os.fspath(ctx.metadata_dir)
|
|
877
|
+
for dir_name in dirs:
|
|
878
|
+
data = ctx.file_cache.read_json(f"{base}/{dir_name}/meta.json")
|
|
879
|
+
if data and keep(data):
|
|
880
|
+
instance = cls.model_validate(data)
|
|
881
|
+
object.__setattr__(instance, "_ctx", ctx)
|
|
882
|
+
result.append(instance)
|
|
883
|
+
return result
|
|
884
|
+
|
|
885
|
+
@classmethod
|
|
886
|
+
def get_agent_memberships(
|
|
887
|
+
cls,
|
|
888
|
+
agent_id: str,
|
|
889
|
+
ctx: "ModelContext",
|
|
890
|
+
) -> list["Chatroom"]:
|
|
891
|
+
"""Return all chatrooms an agent participates in across all projects.
|
|
892
|
+
|
|
893
|
+
Args:
|
|
894
|
+
agent_id: The agent ID to find memberships for
|
|
895
|
+
ctx: ModelContext for filesystem operations
|
|
896
|
+
|
|
897
|
+
Returns:
|
|
898
|
+
List of Chatroom objects
|
|
899
|
+
"""
|
|
900
|
+
from .chatroom import Chatroom
|
|
901
|
+
|
|
902
|
+
result: list[Chatroom] = []
|
|
903
|
+
for proj in cls.list_all(ctx):
|
|
904
|
+
if agent_id not in proj.participating_agents:
|
|
905
|
+
continue
|
|
906
|
+
for chatroom_name in proj.chatrooms:
|
|
907
|
+
room = Chatroom.get(proj.id, chatroom_name, ctx)
|
|
908
|
+
if room is not None and agent_id in room.participants:
|
|
909
|
+
result.append(room)
|
|
910
|
+
return result
|
|
911
|
+
|
|
912
|
+
@classmethod
|
|
913
|
+
def get_projects_for_agent(
|
|
914
|
+
cls,
|
|
915
|
+
agent_id: str,
|
|
916
|
+
ctx: "ModelContext",
|
|
917
|
+
) -> list["Project"]:
|
|
918
|
+
"""Return all projects an agent participates in.
|
|
919
|
+
|
|
920
|
+
Args:
|
|
921
|
+
agent_id: The agent ID to find projects for
|
|
922
|
+
ctx: ModelContext for filesystem operations
|
|
923
|
+
|
|
924
|
+
Returns:
|
|
925
|
+
List of Project objects where the agent is a participant
|
|
926
|
+
"""
|
|
927
|
+
return cls.list_where(
|
|
928
|
+
ctx, lambda meta: agent_id in (meta.get("participating_agents") or ())
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
# -------------------------------------------------------------------------
|
|
932
|
+
# State Access (for write operations)
|
|
933
|
+
# -------------------------------------------------------------------------
|
|
934
|
+
|
|
935
|
+
def state(self) -> "ProjectState":
|
|
936
|
+
"""Get the state model for write operations.
|
|
937
|
+
|
|
938
|
+
Returns:
|
|
939
|
+
ProjectState instance for this project
|
|
940
|
+
"""
|
|
941
|
+
return ProjectState(self)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
# =============================================================================
|
|
945
|
+
# ProjectState: Write Operations
|
|
946
|
+
# =============================================================================
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
#: The separator in a hub stand-in's id, ``{genesis fp16}~{agent_name}``. Local
|
|
950
|
+
#: participant ids are UUIDs and agent names never contain it.
|
|
951
|
+
STAND_IN_SEPARATOR = "~"
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def scope_key(project: "Project") -> Optional[str]:
|
|
955
|
+
"""The requester key a turn in this project is scoped by: its own, or, for
|
|
956
|
+
a project a scoped turn created (``remote_content``, not a hub thread) that
|
|
957
|
+
``migrate_scoped_turn_projects`` has not keyed yet, the key that migration
|
|
958
|
+
gives it, ``project:{id}``. So a start-up pass that failed partway leaves
|
|
959
|
+
the rest scoped, never at full power. Pure: no I/O."""
|
|
960
|
+
key = getattr(project, "requester_key", None)
|
|
961
|
+
if key:
|
|
962
|
+
return key
|
|
963
|
+
if getattr(project, "remote_content", False) and not is_untrusted_source(project):
|
|
964
|
+
return f"project:{project.id}"
|
|
965
|
+
return None
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def is_isolated(project: "Project") -> bool:
|
|
969
|
+
"""True when the project's turns must run scoped: it serves a requester
|
|
970
|
+
other than the owner (``scope_key``). The server's isolation gate refuses
|
|
971
|
+
to hand such a project to an agent whose runner can neither sandbox the
|
|
972
|
+
turn nor run it owner-isolated. Hub threads' ``remote_content`` does not
|
|
973
|
+
isolate (plan-owner-isolation section 3). Pure: no I/O."""
|
|
974
|
+
return scope_key(project) is not None
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def turn_kind(project: "Project") -> str:
|
|
978
|
+
"""How a turn in this project runs: "requester" (it serves someone other
|
|
979
|
+
than the owner) or "full". The runner's TurnScope classifies the same way
|
|
980
|
+
(runner/turn_scope.py)."""
|
|
981
|
+
return "requester" if scope_key(project) else "full"
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def counterpart_of(project: "Project") -> Optional[str]:
|
|
985
|
+
"""The outside party of a requester end: ``counterpart_id``, or, on a hub
|
|
986
|
+
thread the own-coordinator migration has not reached yet, the stand-in
|
|
987
|
+
coordinator. None on every other project. Pure."""
|
|
988
|
+
if project.counterpart_id:
|
|
989
|
+
return project.counterpart_id
|
|
990
|
+
if STAND_IN_SEPARATOR in (project.coordinator_id or ""):
|
|
991
|
+
return project.coordinator_id
|
|
992
|
+
return None
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def is_untrusted_source(project: "Project") -> bool:
|
|
996
|
+
"""True for a hub requester thread: a Front Desk project whose outside
|
|
997
|
+
party (``counterpart_id``; the coordinator before the own-coordinator
|
|
998
|
+
migration) is the stand-in for an agent on another deployment. Its
|
|
999
|
+
messages are written by a stranger's agent, so no agent-context assembly
|
|
1000
|
+
(reflection transcripts, recent activity, summaries, memory inputs) may
|
|
1001
|
+
read them; the owner still sees the thread in the browser. Pure: no I/O,
|
|
1002
|
+
no hub import."""
|
|
1003
|
+
return getattr(project, "surface", None) == "frontdesk" and STAND_IN_SEPARATOR in (counterpart_of(project) or "")
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
class ProjectState:
|
|
1007
|
+
"""Mutable state model for project write operations.
|
|
1008
|
+
|
|
1009
|
+
This class handles all filesystem write operations for a project,
|
|
1010
|
+
triggered by changelog processing. The separation keeps the main
|
|
1011
|
+
Project model immutable while allowing write operations.
|
|
1012
|
+
|
|
1013
|
+
Usage:
|
|
1014
|
+
project = Project.get(project_id, ctx)
|
|
1015
|
+
project.state().complete()
|
|
1016
|
+
|
|
1017
|
+
For creation, use the classmethod:
|
|
1018
|
+
project = ProjectState.create(project_id, project_name, coordinator_id, request, ctx)
|
|
1019
|
+
"""
|
|
1020
|
+
|
|
1021
|
+
def __init__(self, project: Project) -> None:
|
|
1022
|
+
"""Initialize with a project instance.
|
|
1023
|
+
|
|
1024
|
+
Args:
|
|
1025
|
+
project: The project to operate on
|
|
1026
|
+
"""
|
|
1027
|
+
self._project = project
|
|
1028
|
+
|
|
1029
|
+
@classmethod
|
|
1030
|
+
def create(
|
|
1031
|
+
cls,
|
|
1032
|
+
project_id: str,
|
|
1033
|
+
project_name: str,
|
|
1034
|
+
coordinator_id: str,
|
|
1035
|
+
coordinator_name: str,
|
|
1036
|
+
request: str,
|
|
1037
|
+
created_by: str,
|
|
1038
|
+
created_at: datetime,
|
|
1039
|
+
ctx: "ModelContext",
|
|
1040
|
+
agent_pool: str = "verified",
|
|
1041
|
+
agent_teams: list[str] | None = None,
|
|
1042
|
+
agent_names: list[str] | None = None,
|
|
1043
|
+
surface: ProjectSurface = "regular",
|
|
1044
|
+
display_name: Optional[str] = None,
|
|
1045
|
+
requester_key: Optional[str] = None,
|
|
1046
|
+
memory_excluded: bool = False,
|
|
1047
|
+
remote_content: bool = False,
|
|
1048
|
+
counterpart_id: Optional[str] = None,
|
|
1049
|
+
counterpart_name: Optional[str] = None,
|
|
1050
|
+
) -> Project:
|
|
1051
|
+
"""Create a new project with directories and meta.json.
|
|
1052
|
+
|
|
1053
|
+
Creates:
|
|
1054
|
+
- Data directory: projects/{name}-{id}/
|
|
1055
|
+
- Metadata directory: metadata/projects/{name}-{id}/
|
|
1056
|
+
- meta.json in metadata directory
|
|
1057
|
+
|
|
1058
|
+
Args:
|
|
1059
|
+
project_id: The project ID
|
|
1060
|
+
project_name: The project name
|
|
1061
|
+
coordinator_id: The coordinator agent ID
|
|
1062
|
+
coordinator_name: The coordinator name
|
|
1063
|
+
request: User request string
|
|
1064
|
+
created_by: User ID of creator (optional)
|
|
1065
|
+
created_at: Creation timestamp
|
|
1066
|
+
ctx: ModelContext for filesystem operations
|
|
1067
|
+
agent_pool: Agent pool mode ("owned", "verified", or "all")
|
|
1068
|
+
agent_teams: Optional list of user_teams; agents carrying any of these teams pass the allowlist (composed via OR with agent_names; empty/None on both = no filter)
|
|
1069
|
+
agent_names: Optional list of agent display names (id, full name, or owner-relative short name); agents matching any pass the allowlist (composed via OR with agent_teams; empty/None on both = no filter)
|
|
1070
|
+
|
|
1071
|
+
Returns:
|
|
1072
|
+
The created Project instance
|
|
1073
|
+
|
|
1074
|
+
Raises:
|
|
1075
|
+
ValueError: If project_name is invalid
|
|
1076
|
+
"""
|
|
1077
|
+
# Validate project name
|
|
1078
|
+
project_name = FileUtil.validate_fs_name(project_name)
|
|
1079
|
+
|
|
1080
|
+
# Build paths (directories created by FileUtil.write with ensure_dir=True)
|
|
1081
|
+
meta_dir = ctx.metadata_dir / f"{project_name}-{project_id}"
|
|
1082
|
+
|
|
1083
|
+
# Write meta.json (chatrooms derived from filesystem, not stored)
|
|
1084
|
+
project_data = {
|
|
1085
|
+
"id": project_id,
|
|
1086
|
+
"name": project_name,
|
|
1087
|
+
"status": "active",
|
|
1088
|
+
"coordinator_id": coordinator_id,
|
|
1089
|
+
"coordinator_name": coordinator_name,
|
|
1090
|
+
"request": request,
|
|
1091
|
+
"participating_agents": [coordinator_id],
|
|
1092
|
+
"created_at": created_at.isoformat() if created_at else None,
|
|
1093
|
+
"created_by": created_by,
|
|
1094
|
+
"agent_pool": agent_pool,
|
|
1095
|
+
"agent_teams": list(agent_teams) if agent_teams else [],
|
|
1096
|
+
"agent_names": list(agent_names) if agent_names else [],
|
|
1097
|
+
"surface": surface,
|
|
1098
|
+
"display_name": display_name,
|
|
1099
|
+
}
|
|
1100
|
+
# Written only when set, so every meta.json of an ordinary project
|
|
1101
|
+
# stays byte-identical to what it was.
|
|
1102
|
+
if requester_key is not None:
|
|
1103
|
+
project_data["requester_key"] = requester_key
|
|
1104
|
+
if memory_excluded:
|
|
1105
|
+
project_data["memory_excluded"] = True
|
|
1106
|
+
if remote_content:
|
|
1107
|
+
project_data["remote_content"] = True
|
|
1108
|
+
if counterpart_id is not None:
|
|
1109
|
+
project_data["counterpart_id"] = counterpart_id
|
|
1110
|
+
project_data["counterpart_name"] = counterpart_name
|
|
1111
|
+
FileUtil.write(meta_dir / "meta.json", project_data, "json", atomic=True)
|
|
1112
|
+
|
|
1113
|
+
# Return the created project
|
|
1114
|
+
instance = Project.model_validate(project_data)
|
|
1115
|
+
object.__setattr__(instance, "_ctx", ctx)
|
|
1116
|
+
return instance
|
|
1117
|
+
|
|
1118
|
+
def complete(self) -> None:
|
|
1119
|
+
"""Update project status to COMPLETED in meta.json."""
|
|
1120
|
+
meta_path = self._project.meta_path
|
|
1121
|
+
project_dict = FileUtil.read(meta_path, "json")
|
|
1122
|
+
project_dict["status"] = "completed"
|
|
1123
|
+
FileUtil.write(meta_path, project_dict, "json", atomic=True)
|
|
1124
|
+
|
|
1125
|
+
def reactivate(self) -> None:
|
|
1126
|
+
"""Flip project status back to ACTIVE in meta.json.
|
|
1127
|
+
|
|
1128
|
+
Counterpart to ``complete()`` — runs when a user posts into a
|
|
1129
|
+
completed task's user-communication, resuming the conversation.
|
|
1130
|
+
"""
|
|
1131
|
+
meta_path = self._project.meta_path
|
|
1132
|
+
project_dict = FileUtil.read(meta_path, "json")
|
|
1133
|
+
project_dict["status"] = "active"
|
|
1134
|
+
FileUtil.write(meta_path, project_dict, "json", atomic=True)
|
|
1135
|
+
|
|
1136
|
+
def apply_allowlist_update(
|
|
1137
|
+
self,
|
|
1138
|
+
agent_names: list[str],
|
|
1139
|
+
agent_teams: list[str],
|
|
1140
|
+
) -> None:
|
|
1141
|
+
"""Overwrite the project's allowlist snapshot in meta.json.
|
|
1142
|
+
|
|
1143
|
+
Idempotent: re-writes the two list fields with the supplied values.
|
|
1144
|
+
Fired by ``PROJECT_ALLOWLIST_UPDATED`` replay; emitted server-side
|
|
1145
|
+
when the coordinator's Front Desk Settings change.
|
|
1146
|
+
"""
|
|
1147
|
+
meta_path = self._project.meta_path
|
|
1148
|
+
project_dict = FileUtil.read(meta_path, "json")
|
|
1149
|
+
project_dict["agent_names"] = list(agent_names)
|
|
1150
|
+
project_dict["agent_teams"] = list(agent_teams)
|
|
1151
|
+
FileUtil.write(meta_path, project_dict, "json", atomic=True)
|
|
1152
|
+
|
|
1153
|
+
def add_participant(self, participant_id: str) -> None:
|
|
1154
|
+
"""Add a participant to the project's participating_agents list.
|
|
1155
|
+
|
|
1156
|
+
Args:
|
|
1157
|
+
participant_id: The participant ID to add
|
|
1158
|
+
"""
|
|
1159
|
+
meta_path = self._project.meta_path
|
|
1160
|
+
project_dict = FileUtil.read(meta_path, "json")
|
|
1161
|
+
current_agents = project_dict.get("participating_agents", [])
|
|
1162
|
+
if participant_id not in current_agents:
|
|
1163
|
+
current_agents.append(participant_id)
|
|
1164
|
+
project_dict["participating_agents"] = current_agents
|
|
1165
|
+
FileUtil.write(meta_path, project_dict, "json", atomic=True)
|
|
1166
|
+
|
|
1167
|
+
def touch_activity(self, *, ts: datetime, is_request: bool) -> None:
|
|
1168
|
+
"""Advance ``last_request_ts`` (user message) or ``last_response_ts``
|
|
1169
|
+
(agent message) so the project sorts by recency in the sidebar.
|
|
1170
|
+
|
|
1171
|
+
Monotonic set-if-greater / set-if-null: a timestamp is only ever moved
|
|
1172
|
+
forward, never backward. This makes the write idempotent under changelog
|
|
1173
|
+
replay and safe against an out-of-order replay or a re-run migration
|
|
1174
|
+
backfill regressing a value the live server already advanced.
|
|
1175
|
+
|
|
1176
|
+
Args:
|
|
1177
|
+
ts: The message timestamp.
|
|
1178
|
+
is_request: True for a USER message (``last_request_ts``), False for
|
|
1179
|
+
an AGENT/ASSISTANT message (``last_response_ts``).
|
|
1180
|
+
"""
|
|
1181
|
+
field = "last_request_ts" if is_request else "last_response_ts"
|
|
1182
|
+
meta_path = self._project.meta_path
|
|
1183
|
+
project_dict = FileUtil.read(meta_path, "json")
|
|
1184
|
+
new_dt = _as_aware_utc(ts)
|
|
1185
|
+
existing_raw = project_dict.get(field)
|
|
1186
|
+
existing_dt = (
|
|
1187
|
+
_as_aware_utc(datetime.fromisoformat(existing_raw)) if existing_raw else None
|
|
1188
|
+
)
|
|
1189
|
+
if existing_dt is not None and existing_dt >= new_dt:
|
|
1190
|
+
return # never regress; no write needed
|
|
1191
|
+
project_dict[field] = new_dt.isoformat()
|
|
1192
|
+
FileUtil.write(meta_path, project_dict, "json", atomic=True)
|
|
1193
|
+
|
|
1194
|
+
def set_thread_title(self, *, display_name: str, slug: Optional[str] = None) -> None:
|
|
1195
|
+
"""Apply a model-generated title to a DM thread after its first exchange.
|
|
1196
|
+
|
|
1197
|
+
Sets ``display_name`` (replacing the ``"New chat"`` placeholder). The
|
|
1198
|
+
stable thread identity is the project ``id`` (UUID) — see the plan's
|
|
1199
|
+
OPEN Q2. The ``slug`` parameter is accepted for interface stability but
|
|
1200
|
+
deliberately NOT applied: the on-disk layout keys the project directory
|
|
1201
|
+
on ``{name}-{id}``, so renaming the slug would require moving directories
|
|
1202
|
+
and is unsafe until Q2 is resolved. Only ``display_name`` is model-set.
|
|
1203
|
+
|
|
1204
|
+
Idempotent: a second call simply overwrites ``display_name`` again.
|
|
1205
|
+
|
|
1206
|
+
Args:
|
|
1207
|
+
display_name: The model-generated human title.
|
|
1208
|
+
slug: Reserved (see above); currently a no-op.
|
|
1209
|
+
"""
|
|
1210
|
+
meta_path = self._project.meta_path
|
|
1211
|
+
project_dict = FileUtil.read(meta_path, "json")
|
|
1212
|
+
project_dict["display_name"] = display_name
|
|
1213
|
+
FileUtil.write(meta_path, project_dict, "json", atomic=True)
|
|
1214
|
+
|
|
1215
|
+
def set_report_published_at(self, ts: Optional[str]) -> bool:
|
|
1216
|
+
"""Denormalize the report's ``generated_at`` into meta.json.
|
|
1217
|
+
|
|
1218
|
+
``ts`` is the ISO string from ``ProjectReport.generated_at`` on publish,
|
|
1219
|
+
or ``None`` to clear the field on delete. Read-modify-write of the full
|
|
1220
|
+
meta dict — preserves every other key, mirroring ``complete()`` /
|
|
1221
|
+
``touch_activity()``.
|
|
1222
|
+
|
|
1223
|
+
Idempotent: writes only on a real delta (re-stamping the same value, or
|
|
1224
|
+
clearing an already-absent value, is a no-op). This keeps the live
|
|
1225
|
+
publish/delete path and the one-shot startup backfill both safe to run
|
|
1226
|
+
repeatedly with no needless atomic rewrite. Returns True iff it wrote.
|
|
1227
|
+
|
|
1228
|
+
Written directly (not via the changelog) because ``report.json`` itself
|
|
1229
|
+
is a non-changelog file artifact; this field is a projection of it, kept
|
|
1230
|
+
adjacent to its source. See the persistence-safety note in
|
|
1231
|
+
``models/project_report.py::backfill_report_published_at``.
|
|
1232
|
+
"""
|
|
1233
|
+
meta_path = self._project.meta_path
|
|
1234
|
+
project_dict = FileUtil.read(meta_path, "json")
|
|
1235
|
+
if project_dict.get("report_published_at") == ts:
|
|
1236
|
+
return False # already in the desired state; no write
|
|
1237
|
+
project_dict["report_published_at"] = ts
|
|
1238
|
+
FileUtil.write(meta_path, project_dict, "json", atomic=True)
|
|
1239
|
+
return True
|
|
1240
|
+
|
|
1241
|
+
def set_isolation_flags(
|
|
1242
|
+
self,
|
|
1243
|
+
*,
|
|
1244
|
+
requester_key: Optional[str] = None,
|
|
1245
|
+
memory_excluded: bool = False,
|
|
1246
|
+
remote_content: bool = False,
|
|
1247
|
+
) -> bool:
|
|
1248
|
+
"""Set isolation flags in meta.json (PROJECT_FLAGS_CHANGED). Only ever
|
|
1249
|
+
sets: ``requester_key`` is written only when the project has none, and
|
|
1250
|
+
a flag already true stays true. Returns True when meta.json changed."""
|
|
1251
|
+
meta_path = self._project.meta_path
|
|
1252
|
+
project_dict = FileUtil.read(meta_path, "json") or {}
|
|
1253
|
+
changed = False
|
|
1254
|
+
if requester_key and not project_dict.get("requester_key"):
|
|
1255
|
+
project_dict["requester_key"] = requester_key
|
|
1256
|
+
changed = True
|
|
1257
|
+
if memory_excluded and not project_dict.get("memory_excluded"):
|
|
1258
|
+
project_dict["memory_excluded"] = True
|
|
1259
|
+
changed = True
|
|
1260
|
+
if remote_content and not project_dict.get("remote_content"):
|
|
1261
|
+
project_dict["remote_content"] = True
|
|
1262
|
+
changed = True
|
|
1263
|
+
if changed:
|
|
1264
|
+
FileUtil.write(meta_path, project_dict, "json", atomic=True)
|
|
1265
|
+
return changed
|
|
1266
|
+
|
|
1267
|
+
def reassign_coordinator(self, *, coordinator_id: str, coordinator_name: str,
|
|
1268
|
+
counterpart_id: str, counterpart_name: str) -> None:
|
|
1269
|
+
"""PROJECT_COORDINATOR_REASSIGNED: the owning user coordinates, the
|
|
1270
|
+
old outside coordinator becomes the counterpart (meta.json)."""
|
|
1271
|
+
meta_path = self._project.meta_path
|
|
1272
|
+
project_dict = FileUtil.read(meta_path, "json") or {}
|
|
1273
|
+
project_dict.update(coordinator_id=coordinator_id, coordinator_name=coordinator_name,
|
|
1274
|
+
counterpart_id=counterpart_id, counterpart_name=counterpart_name)
|
|
1275
|
+
agents = list(project_dict.get("participating_agents") or [])
|
|
1276
|
+
if coordinator_id not in agents:
|
|
1277
|
+
agents.append(coordinator_id)
|
|
1278
|
+
project_dict["participating_agents"] = agents
|
|
1279
|
+
FileUtil.write(meta_path, project_dict, "json", atomic=True)
|
|
1280
|
+
|
|
1281
|
+
def set_plan_state(
|
|
1282
|
+
self,
|
|
1283
|
+
*,
|
|
1284
|
+
plan_seeded_at: Optional[str],
|
|
1285
|
+
plan_accepted_at: Optional[str],
|
|
1286
|
+
plan_accepted_revision: int,
|
|
1287
|
+
plan_accepted_spec_digest: str,
|
|
1288
|
+
plan_open_notes: int,
|
|
1289
|
+
plan_user_reviewed_at: Optional[str] = None,
|
|
1290
|
+
plan_offered_at: Optional[str] = None,
|
|
1291
|
+
) -> bool:
|
|
1292
|
+
"""Apply the plan lifecycle projection into meta.json (D13, §7.2).
|
|
1293
|
+
|
|
1294
|
+
Fired by ``PROJECT_PLAN_STATE`` replay, so it runs identically on the
|
|
1295
|
+
server (where the entry is minted) and on **every runner that syncs it**
|
|
1296
|
+
— which is the entire reason the entry exists (see
|
|
1297
|
+
``ProjectPlanStatePayload``). Read-modify-write of the full meta dict,
|
|
1298
|
+
preserving every other key, exactly as :meth:`set_report_published_at`
|
|
1299
|
+
and :meth:`complete` do.
|
|
1300
|
+
|
|
1301
|
+
Applied **unconditionally** (last-write-wins), like
|
|
1302
|
+
:meth:`set_thread_title`: the publisher is the only gate, and a changelog
|
|
1303
|
+
rebuild must deterministically reproduce the last entry. Returns True iff
|
|
1304
|
+
the bytes actually moved, so the caller can skip a needless atomic write.
|
|
1305
|
+
"""
|
|
1306
|
+
meta_path = self._project.meta_path
|
|
1307
|
+
project_dict = FileUtil.read(meta_path, "json")
|
|
1308
|
+
incoming = {
|
|
1309
|
+
"plan_seeded_at": plan_seeded_at,
|
|
1310
|
+
"plan_accepted_at": plan_accepted_at,
|
|
1311
|
+
"plan_accepted_revision": plan_accepted_revision,
|
|
1312
|
+
"plan_accepted_spec_digest": plan_accepted_spec_digest,
|
|
1313
|
+
"plan_open_notes": plan_open_notes,
|
|
1314
|
+
"plan_user_reviewed_at": plan_user_reviewed_at,
|
|
1315
|
+
"plan_offered_at": plan_offered_at,
|
|
1316
|
+
}
|
|
1317
|
+
if all(project_dict.get(k) == v for k, v in incoming.items()):
|
|
1318
|
+
return False
|
|
1319
|
+
project_dict.update(incoming)
|
|
1320
|
+
FileUtil.write(meta_path, project_dict, "json", atomic=True)
|
|
1321
|
+
return True
|
|
1322
|
+
|
|
1323
|
+
def set_plan_updated_at(self, ts: Optional[str]) -> bool:
|
|
1324
|
+
"""Denormalize the plan's last-write timestamp into meta.json (D5, §6).
|
|
1325
|
+
|
|
1326
|
+
``ts`` is an ISO string, or ``None`` to clear. Mirrors
|
|
1327
|
+
:meth:`set_report_published_at` exactly — read-modify-write of the full
|
|
1328
|
+
meta dict, idempotent, written **outside the changelog** because the
|
|
1329
|
+
plan's own bytes are the changelog artifact and this field is a
|
|
1330
|
+
projection of them (the ``report_published_at`` safety argument, §6).
|
|
1331
|
+
|
|
1332
|
+
Called by the plan routes after every mutation, so "the plan moved" is
|
|
1333
|
+
readable from the project list without opening plan.json.
|
|
1334
|
+
|
|
1335
|
+
Returns True iff it wrote.
|
|
1336
|
+
"""
|
|
1337
|
+
meta_path = self._project.meta_path
|
|
1338
|
+
project_dict = FileUtil.read(meta_path, "json")
|
|
1339
|
+
if project_dict.get("plan_updated_at") == ts:
|
|
1340
|
+
return False
|
|
1341
|
+
project_dict["plan_updated_at"] = ts
|
|
1342
|
+
FileUtil.write(meta_path, project_dict, "json", atomic=True)
|
|
1343
|
+
return True
|