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.
Files changed (1004) hide show
  1. manigent/_catalog/mcps/chess/README.md +38 -0
  2. manigent/_catalog/mcps/chess/mcp.json +9 -0
  3. manigent/_catalog/mcps/registry.json +9 -0
  4. manigent/_catalog/skills/brief/SKILL.md +339 -0
  5. manigent/_catalog/skills/broaden-options/SKILL.md +133 -0
  6. manigent/_catalog/skills/calendar/SKILL.md +44 -0
  7. manigent/_catalog/skills/calendar/starter_config.jsonc +25 -0
  8. manigent/_catalog/skills/gmail/SKILL.md +73 -0
  9. manigent/_catalog/skills/google-calendar/SKILL.md +44 -0
  10. manigent/_catalog/skills/google-drive/SKILL.md +42 -0
  11. manigent/_catalog/skills/google-drive-write/SKILL.md +45 -0
  12. manigent/_catalog/skills/google-drive-write/starter_config.jsonc +45 -0
  13. manigent/_catalog/skills/grill-me/SKILL.md +173 -0
  14. manigent/_catalog/skills/homekit/SKILL.md +37 -0
  15. manigent/_catalog/skills/http-api/SKILL.md +117 -0
  16. manigent/_catalog/skills/ib/SKILL.md +89 -0
  17. manigent/_catalog/skills/ib/starter_config.jsonc +35 -0
  18. manigent/_catalog/skills/mailbox/SKILL.md +47 -0
  19. manigent/_catalog/skills/mailbox/starter_config.jsonc +38 -0
  20. manigent/_catalog/skills/media/SKILL.md +93 -0
  21. manigent/_catalog/skills/media/starter_config.jsonc +63 -0
  22. manigent/_catalog/skills/osxphotos/SKILL.md +41 -0
  23. manigent/_catalog/skills/photo-story/SKILL.md +285 -0
  24. manigent/_catalog/skills/playwright-browser/SKILL.md +291 -0
  25. manigent/_catalog/skills/playwright-browser/starter_config.jsonc +44 -0
  26. manigent/_catalog/skills/playwright-save-skill/SKILL.md +149 -0
  27. manigent/_catalog/skills/registry.json +166 -0
  28. manigent/_catalog/skills/web-artifacts/SKILL.md +194 -0
  29. manigent/api/action_executor.py +404 -0
  30. manigent/api/action_validator.py +531 -0
  31. manigent/api/actions.py +221 -0
  32. manigent/api/client.py +558 -0
  33. manigent/api/control.py +806 -0
  34. manigent/api/edge_protocol.py +123 -0
  35. manigent/api/fs_protocol.py +450 -0
  36. manigent/api/host_protocol.py +298 -0
  37. manigent/api/managed_job_client.py +124 -0
  38. manigent/api/responses.py +342 -0
  39. manigent/api/retry_transport.py +178 -0
  40. manigent/api/turn_protocol.py +78 -0
  41. manigent/cli_lifecycle.py +467 -0
  42. manigent/cli_server.py +978 -0
  43. manigent/doctor.py +1167 -0
  44. manigent/hub_protocol/canonical.py +375 -0
  45. manigent/hub_protocol/codes.py +171 -0
  46. manigent/hub_protocol/constants.py +71 -0
  47. manigent/hub_protocol/envelope.py +218 -0
  48. manigent/hub_protocol/frames.py +788 -0
  49. manigent/hub_protocol/http.py +260 -0
  50. manigent/hub_protocol/ids.py +170 -0
  51. manigent/hub_protocol/keys.py +195 -0
  52. manigent/hub_protocol/text.py +75 -0
  53. manigent/llm/base.py +1072 -0
  54. manigent/llm/prompt_builder.py +2450 -0
  55. manigent/llm/triggers.py +68 -0
  56. manigent/models/agent.py +2905 -0
  57. manigent/models/agent_bundle.py +298 -0
  58. manigent/models/brief_tab.py +311 -0
  59. manigent/models/chat_message.py +231 -0
  60. manigent/models/chatroom.py +1108 -0
  61. manigent/models/context.py +1159 -0
  62. manigent/models/desk_label.py +836 -0
  63. manigent/models/desk_read_state.py +220 -0
  64. manigent/models/desk_sign_off.py +267 -0
  65. manigent/models/desk_sop.py +965 -0
  66. manigent/models/desk_todo.py +1603 -0
  67. manigent/models/desk_todo_link.py +404 -0
  68. manigent/models/file_cache.py +75 -0
  69. manigent/models/host.py +732 -0
  70. manigent/models/install_token.py +401 -0
  71. manigent/models/invitation_code.py +162 -0
  72. manigent/models/knowledge_pack.py +552 -0
  73. manigent/models/managed_job.py +295 -0
  74. manigent/models/model_config.py +564 -0
  75. manigent/models/model_defaults.py +238 -0
  76. manigent/models/onboarding_plan.py +161 -0
  77. manigent/models/participant.py +449 -0
  78. manigent/models/persistable.py +892 -0
  79. manigent/models/plan_markdown.py +1774 -0
  80. manigent/models/project.py +1343 -0
  81. manigent/models/project_plan.py +8104 -0
  82. manigent/models/project_report.py +145 -0
  83. manigent/models/provenance.py +261 -0
  84. manigent/models/provider_agents.py +101 -0
  85. manigent/models/public_request.py +108 -0
  86. manigent/models/reflection_schedule.py +191 -0
  87. manigent/models/scheduled_message.py +285 -0
  88. manigent/models/share_token.py +190 -0
  89. manigent/models/skill_version.py +31 -0
  90. manigent/models/sop_template.py +187 -0
  91. manigent/models/thinking_partner_plan.py +187 -0
  92. manigent/models/tunnel_binding.py +129 -0
  93. manigent/models/user.py +1276 -0
  94. manigent/models/work_tracker.py +510 -0
  95. manigent/runner/invocation_registry.py +187 -0
  96. manigent/server/agent_capabilities.py +119 -0
  97. manigent/server/agent_visibility.py +119 -0
  98. manigent/server/announcements.py +119 -0
  99. manigent/server/app.py +807 -0
  100. manigent/server/auth.py +355 -0
  101. manigent/server/batch_timeout.py +94 -0
  102. manigent/server/channels/access.py +194 -0
  103. manigent/server/channels/adapter.py +295 -0
  104. manigent/server/channels/config.py +693 -0
  105. manigent/server/channels/contacts.py +39 -0
  106. manigent/server/channels/email_adapter.py +436 -0
  107. manigent/server/channels/email_address.py +145 -0
  108. manigent/server/channels/email_form.py +242 -0
  109. manigent/server/channels/email_format.py +285 -0
  110. manigent/server/channels/email_mail.py +427 -0
  111. manigent/server/channels/email_mailgun.py +164 -0
  112. manigent/server/channels/email_text.py +280 -0
  113. manigent/server/channels/file_links.py +127 -0
  114. manigent/server/channels/gateway.py +2998 -0
  115. manigent/server/channels/hub_delivery.py +211 -0
  116. manigent/server/channels/hub_store.py +1730 -0
  117. manigent/server/channels/line.py +645 -0
  118. manigent/server/channels/line_format.py +589 -0
  119. manigent/server/channels/log_redaction.py +145 -0
  120. manigent/server/channels/store.py +3316 -0
  121. manigent/server/channels/telegram.py +422 -0
  122. manigent/server/channels/telegram_format.py +312 -0
  123. manigent/server/channels/webchat.py +261 -0
  124. manigent/server/channels/webchat_widget.js +1310 -0
  125. manigent/server/channels/whatsapp.py +707 -0
  126. manigent/server/channels/whatsapp_format.py +511 -0
  127. manigent/server/chat_search_index.py +1203 -0
  128. manigent/server/config.py +61 -0
  129. manigent/server/context.py +121 -0
  130. manigent/server/coordinator_rule.py +115 -0
  131. manigent/server/desk_todo_notify.py +300 -0
  132. manigent/server/edge_hub.py +577 -0
  133. manigent/server/email.py +354 -0
  134. manigent/server/errors.py +83 -0
  135. manigent/server/home_fs_relay.py +392 -0
  136. manigent/server/hub/adapter.py +1395 -0
  137. manigent/server/hub/agent_send_guard.py +196 -0
  138. manigent/server/hub/avatar.py +342 -0
  139. manigent/server/hub/browser.py +778 -0
  140. manigent/server/hub/client.py +859 -0
  141. manigent/server/hub/demand.py +49 -0
  142. manigent/server/hub/directory.py +385 -0
  143. manigent/server/hub/enroll.py +400 -0
  144. manigent/server/hub/http.py +134 -0
  145. manigent/server/hub/identity.py +61 -0
  146. manigent/server/hub/keys.py +588 -0
  147. manigent/server/hub/mirror.py +802 -0
  148. manigent/server/hub/operator.py +817 -0
  149. manigent/server/hub/retain.py +361 -0
  150. manigent/server/hub/room_bridge.py +741 -0
  151. manigent/server/hub/runtime.py +691 -0
  152. manigent/server/hub/trust.py +182 -0
  153. manigent/server/hub/views.py +316 -0
  154. manigent/server/idempotency.py +205 -0
  155. manigent/server/in_flight_restore.py +181 -0
  156. manigent/server/inspection.py +816 -0
  157. manigent/server/isolation_gate.py +516 -0
  158. manigent/server/isolation_notice.py +310 -0
  159. manigent/server/managed_job_events.py +242 -0
  160. manigent/server/mcp_registry.py +118 -0
  161. manigent/server/message_forward.py +78 -0
  162. manigent/server/message_scheduler.py +176 -0
  163. manigent/server/notification_assets.py +164 -0
  164. manigent/server/oauth_login/cli_state.py +120 -0
  165. manigent/server/oauth_login/config.py +90 -0
  166. manigent/server/oauth_login/errors.py +78 -0
  167. manigent/server/oauth_login/flow.py +223 -0
  168. manigent/server/oauth_login/handoff.py +63 -0
  169. manigent/server/oauth_login/providers.py +45 -0
  170. manigent/server/oauth_state.py +81 -0
  171. manigent/server/owner_isolation.py +466 -0
  172. manigent/server/participant_liveness.py +82 -0
  173. manigent/server/public_profile.py +149 -0
  174. manigent/server/public_requests/candidates.py +116 -0
  175. manigent/server/public_requests/dispatch.py +121 -0
  176. manigent/server/public_requests/ranking.py +153 -0
  177. manigent/server/recent_activity.py +250 -0
  178. manigent/server/reflection_completion_subscriber.py +151 -0
  179. manigent/server/reflection_scheduler.py +239 -0
  180. manigent/server/requester_key_migration.py +261 -0
  181. manigent/server/room_flow.py +139 -0
  182. manigent/server/routes/_batch.py +218 -0
  183. manigent/server/routes/_fd_routing.py +155 -0
  184. manigent/server/routes/_mentions.py +70 -0
  185. manigent/server/routes/agent_env.py +188 -0
  186. manigent/server/routes/agent_fs.py +450 -0
  187. manigent/server/routes/agents.py +1206 -0
  188. manigent/server/routes/announcements.py +79 -0
  189. manigent/server/routes/auth.py +387 -0
  190. manigent/server/routes/brief_tabs.py +404 -0
  191. manigent/server/routes/changelog.py +237 -0
  192. manigent/server/routes/channels.py +993 -0
  193. manigent/server/routes/chatrooms.py +871 -0
  194. manigent/server/routes/desk_labels.py +298 -0
  195. manigent/server/routes/desk_latest_messages.py +116 -0
  196. manigent/server/routes/desk_read_state.py +110 -0
  197. manigent/server/routes/desk_sign_off.py +88 -0
  198. manigent/server/routes/desk_sops.py +216 -0
  199. manigent/server/routes/desk_todos.py +692 -0
  200. manigent/server/routes/desk_tour.py +44 -0
  201. manigent/server/routes/files.py +572 -0
  202. manigent/server/routes/hosts.py +484 -0
  203. manigent/server/routes/hub.py +534 -0
  204. manigent/server/routes/hub_channels.py +234 -0
  205. manigent/server/routes/inbox.py +581 -0
  206. manigent/server/routes/install.ps1 +436 -0
  207. manigent/server/routes/install.py +324 -0
  208. manigent/server/routes/install.sh +566 -0
  209. manigent/server/routes/isolation_check.py +145 -0
  210. manigent/server/routes/knowledge_packs.py +477 -0
  211. manigent/server/routes/managed_jobs.py +196 -0
  212. manigent/server/routes/mcps.py +271 -0
  213. manigent/server/routes/messages.py +1972 -0
  214. manigent/server/routes/model_defaults.py +279 -0
  215. manigent/server/routes/oauth.py +237 -0
  216. manigent/server/routes/oauth_login.py +351 -0
  217. manigent/server/routes/owner_isolation.py +91 -0
  218. manigent/server/routes/project_plans.py +1521 -0
  219. manigent/server/routes/project_reports.py +235 -0
  220. manigent/server/routes/projects.py +2047 -0
  221. manigent/server/routes/public_agents.py +151 -0
  222. manigent/server/routes/public_requests.py +181 -0
  223. manigent/server/routes/reflection.py +93 -0
  224. manigent/server/routes/scheduled_messages.py +263 -0
  225. manigent/server/routes/search.py +94 -0
  226. manigent/server/routes/sharing.py +156 -0
  227. manigent/server/routes/skills.py +406 -0
  228. manigent/server/routes/templates.py +219 -0
  229. manigent/server/routes/turn_tokens.py +119 -0
  230. manigent/server/routes/webchat.py +406 -0
  231. manigent/server/routes/websocket.py +431 -0
  232. manigent/server/serving_only.py +200 -0
  233. manigent/server/signup_policy.py +120 -0
  234. manigent/server/skill_registry.py +164 -0
  235. manigent/server/socket_events.py +858 -0
  236. manigent/server/timeout_checker.py +446 -0
  237. manigent/server/tunnel_subscriber.py +1000 -0
  238. manigent/server/turn_tokens.py +285 -0
  239. manigent/server/ws_hub.py +556 -0
  240. manigent/server_cli.py +31 -0
  241. manigent/sync/changelog.py +885 -0
  242. manigent/sync/console_subscriber.py +222 -0
  243. manigent/sync/runloop.py +619 -0
  244. manigent/sync/runloop_manager.py +197 -0
  245. manigent/sync/subscriber.py +98 -0
  246. manigent/utils/agent_processes.py +276 -0
  247. manigent/utils/agent_storage.py +125 -0
  248. manigent/utils/email.py +563 -0
  249. manigent/utils/env_store.py +174 -0
  250. manigent/utils/extra_dirs.py +85 -0
  251. manigent/utils/file_io.py +485 -0
  252. manigent/utils/invoke_timeout.py +87 -0
  253. manigent/utils/jsonc.py +78 -0
  254. manigent/utils/knowledge_index.py +148 -0
  255. manigent/utils/nofollow_fs.py +279 -0
  256. manigent/utils/notification_center.py +67 -0
  257. manigent/utils/version.py +57 -0
  258. manigent/web/frontend/.design-sync-entry.tsx +74 -0
  259. manigent/web/frontend/.design-sync-provider.tsx +46 -0
  260. manigent/web/frontend/.eslintrc.cjs +183 -0
  261. manigent/web/frontend/.gitignore +6 -0
  262. manigent/web/frontend/app/page.tsx +39 -0
  263. manigent/web/frontend/hub-terms.html +192 -0
  264. manigent/web/frontend/index.html +15 -0
  265. manigent/web/frontend/landing.html +15 -0
  266. manigent/web/frontend/package-lock.json +8028 -0
  267. manigent/web/frontend/package.json +55 -0
  268. manigent/web/frontend/postcss.config.js +6 -0
  269. manigent/web/frontend/privacy.html +217 -0
  270. manigent/web/frontend/private-beta-announcement.html +19 -0
  271. manigent/web/frontend/public/apple-touch-icon.png +0 -0
  272. manigent/web/frontend/public/brand-hero-banner.png +0 -0
  273. manigent/web/frontend/public/brand-hero.png +0 -0
  274. manigent/web/frontend/public/favicon.png +0 -0
  275. manigent/web/frontend/public/favicon.svg +8 -0
  276. manigent/web/frontend/public/icon-transparent-sm.png +0 -0
  277. manigent/web/frontend/public/icon.png +0 -0
  278. manigent/web/frontend/public/manigent-mark.svg +7 -0
  279. manigent/web/frontend/public/vite.svg +5 -0
  280. manigent/web/frontend/public.html +15 -0
  281. manigent/web/frontend/src/App.activeAccountRedirect.test.tsx +236 -0
  282. manigent/web/frontend/src/App.hubHostDm.test.tsx +119 -0
  283. manigent/web/frontend/src/App.hubHostProject.test.tsx +142 -0
  284. manigent/web/frontend/src/App.hubRoutes.test.tsx +187 -0
  285. manigent/web/frontend/src/App.oauthAddCallback.test.tsx +263 -0
  286. manigent/web/frontend/src/App.tsx +246 -0
  287. manigent/web/frontend/src/api/attachments.ts +32 -0
  288. manigent/web/frontend/src/api/batchLoader.test.ts +212 -0
  289. manigent/web/frontend/src/api/batchLoader.ts +124 -0
  290. manigent/web/frontend/src/api/chatroomsBatch.test.tsx +392 -0
  291. manigent/web/frontend/src/api/client.ts +321 -0
  292. manigent/web/frontend/src/api/crossAccountAuth.test.ts +388 -0
  293. manigent/web/frontend/src/api/endpoints/agentEnv.ts +24 -0
  294. manigent/web/frontend/src/api/endpoints/agentFs.ts +125 -0
  295. manigent/web/frontend/src/api/endpoints/agentSearch.ts +19 -0
  296. manigent/web/frontend/src/api/endpoints/agents.ts +66 -0
  297. manigent/web/frontend/src/api/endpoints/announcements.ts +26 -0
  298. manigent/web/frontend/src/api/endpoints/auth.ts +361 -0
  299. manigent/web/frontend/src/api/endpoints/authLogin.test.ts +236 -0
  300. manigent/web/frontend/src/api/endpoints/batch.ts +64 -0
  301. manigent/web/frontend/src/api/endpoints/briefTabs.ts +81 -0
  302. manigent/web/frontend/src/api/endpoints/channels.ts +285 -0
  303. manigent/web/frontend/src/api/endpoints/computers.ts +212 -0
  304. manigent/web/frontend/src/api/endpoints/deskLabels.ts +151 -0
  305. manigent/web/frontend/src/api/endpoints/deskLatestMessages.ts +23 -0
  306. manigent/web/frontend/src/api/endpoints/deskReadState.ts +41 -0
  307. manigent/web/frontend/src/api/endpoints/deskSignOffs.ts +74 -0
  308. manigent/web/frontend/src/api/endpoints/deskSops.ts +53 -0
  309. manigent/web/frontend/src/api/endpoints/deskTodoAttachments.ts +69 -0
  310. manigent/web/frontend/src/api/endpoints/deskTodoProjects.test.ts +99 -0
  311. manigent/web/frontend/src/api/endpoints/deskTodoProjects.ts +114 -0
  312. manigent/web/frontend/src/api/endpoints/deskTodos.ts +164 -0
  313. manigent/web/frontend/src/api/endpoints/deskTour.ts +16 -0
  314. manigent/web/frontend/src/api/endpoints/dm.ts +137 -0
  315. manigent/web/frontend/src/api/endpoints/files.ts +55 -0
  316. manigent/web/frontend/src/api/endpoints/hub.contract.test.ts +94 -0
  317. manigent/web/frontend/src/api/endpoints/hub.keys.ts +260 -0
  318. manigent/web/frontend/src/api/endpoints/hub.test.ts +203 -0
  319. manigent/web/frontend/src/api/endpoints/hub.ts +654 -0
  320. manigent/web/frontend/src/api/endpoints/index.ts +15 -0
  321. manigent/web/frontend/src/api/endpoints/install.ts +74 -0
  322. manigent/web/frontend/src/api/endpoints/isolationCheck.ts +36 -0
  323. manigent/web/frontend/src/api/endpoints/knowledgePacks.ts +179 -0
  324. manigent/web/frontend/src/api/endpoints/mcps.ts +36 -0
  325. manigent/web/frontend/src/api/endpoints/messages.held.test.ts +43 -0
  326. manigent/web/frontend/src/api/endpoints/messages.ts +95 -0
  327. manigent/web/frontend/src/api/endpoints/modelConfigs.ts +232 -0
  328. manigent/web/frontend/src/api/endpoints/oauthApi.test.ts +110 -0
  329. manigent/web/frontend/src/api/endpoints/projectPlans.ts +608 -0
  330. manigent/web/frontend/src/api/endpoints/projectReports.ts +36 -0
  331. manigent/web/frontend/src/api/endpoints/projects.ts +124 -0
  332. manigent/web/frontend/src/api/endpoints/publicRequests.ts +75 -0
  333. manigent/web/frontend/src/api/endpoints/reflection.ts +32 -0
  334. manigent/web/frontend/src/api/endpoints/roomFlow.ts +14 -0
  335. manigent/web/frontend/src/api/endpoints/scheduledMessages.ts +60 -0
  336. manigent/web/frontend/src/api/endpoints/search.ts +27 -0
  337. manigent/web/frontend/src/api/endpoints/sharing.ts +42 -0
  338. manigent/web/frontend/src/api/endpoints/skills.ts +67 -0
  339. manigent/web/frontend/src/api/endpoints/templates.ts +72 -0
  340. manigent/web/frontend/src/api/loaders.ts +22 -0
  341. manigent/web/frontend/src/api/singleIssuer.test.ts +148 -0
  342. manigent/web/frontend/src/api/transientRetry.test.ts +176 -0
  343. manigent/web/frontend/src/api/transientRetry.ts +140 -0
  344. manigent/web/frontend/src/api/websocket.resync.test.ts +111 -0
  345. manigent/web/frontend/src/api/websocket.ts +335 -0
  346. manigent/web/frontend/src/components/agent/AgentNameLink.tsx +24 -0
  347. manigent/web/frontend/src/components/agent/AgentViewActions.test.tsx +41 -0
  348. manigent/web/frontend/src/components/agent/AgentViewActions.tsx +89 -0
  349. manigent/web/frontend/src/components/agent/CreateAgentPage.tsx +535 -0
  350. manigent/web/frontend/src/components/agent/JsonConfigEditor.tsx +33 -0
  351. manigent/web/frontend/src/components/agent/KnowledgePackList.tsx +103 -0
  352. manigent/web/frontend/src/components/agent/McpCatalog.tsx +261 -0
  353. manigent/web/frontend/src/components/agent/McpConfigModal.tsx +224 -0
  354. manigent/web/frontend/src/components/agent/PreConsentModal.tsx +172 -0
  355. manigent/web/frontend/src/components/agent/SkillCatalog.tsx +364 -0
  356. manigent/web/frontend/src/components/agent/SkillConfigModal.tsx +221 -0
  357. manigent/web/frontend/src/components/auth/AddAccountPage.test.tsx +181 -0
  358. manigent/web/frontend/src/components/auth/AddAccountPage.tsx +201 -0
  359. manigent/web/frontend/src/components/auth/AuthShell.tsx +462 -0
  360. manigent/web/frontend/src/components/auth/LoginPage.test.tsx +384 -0
  361. manigent/web/frontend/src/components/auth/LoginPage.tsx +129 -0
  362. manigent/web/frontend/src/components/auth/OAuthCallbackPage.test.tsx +185 -0
  363. manigent/web/frontend/src/components/auth/OAuthCallbackPage.tsx +154 -0
  364. manigent/web/frontend/src/components/auth/OAuthLinkedBanner.tsx +44 -0
  365. manigent/web/frontend/src/components/auth/ProviderButtons.tsx +94 -0
  366. manigent/web/frontend/src/components/auth/SignupPage.tsx +437 -0
  367. manigent/web/frontend/src/components/auth/StepProgress.tsx +60 -0
  368. manigent/web/frontend/src/components/auth/VerifyEmailPage.test.tsx +155 -0
  369. manigent/web/frontend/src/components/auth/VerifyEmailPage.tsx +211 -0
  370. manigent/web/frontend/src/components/auth/index.ts +3 -0
  371. manigent/web/frontend/src/components/auth/oauthErrors.test.ts +107 -0
  372. manigent/web/frontend/src/components/auth/oauthErrors.ts +82 -0
  373. manigent/web/frontend/src/components/auth/useUsernameCheck.test.tsx +68 -0
  374. manigent/web/frontend/src/components/auth/useUsernameCheck.ts +86 -0
  375. manigent/web/frontend/src/components/beta/BetaPage.tsx +21 -0
  376. manigent/web/frontend/src/components/beta/DayAtMyDesk.tsx +120 -0
  377. manigent/web/frontend/src/components/beta/Footer.tsx +82 -0
  378. manigent/web/frontend/src/components/beta/Hero.tsx +224 -0
  379. manigent/web/frontend/src/components/beta/Nav.tsx +51 -0
  380. manigent/web/frontend/src/components/beta/Pillars.tsx +172 -0
  381. manigent/web/frontend/src/components/beta/TwoModels.tsx +146 -0
  382. manigent/web/frontend/src/components/beta/constants.ts +3 -0
  383. manigent/web/frontend/src/components/briefing/BriefingFrame.test.tsx +208 -0
  384. manigent/web/frontend/src/components/briefing/BriefingFrame.tsx +151 -0
  385. manigent/web/frontend/src/components/briefing/BriefingPage.test.tsx +176 -0
  386. manigent/web/frontend/src/components/briefing/BriefingPage.tsx +157 -0
  387. manigent/web/frontend/src/components/briefing/BriefingPage.wsDelete.test.tsx +177 -0
  388. manigent/web/frontend/src/components/briefing/briefing.css +147 -0
  389. manigent/web/frontend/src/components/briefing/heightBootstrap.test.ts +121 -0
  390. manigent/web/frontend/src/components/briefing/heightBootstrap.ts +224 -0
  391. manigent/web/frontend/src/components/briefing/parseBriefMessage.test.ts +90 -0
  392. manigent/web/frontend/src/components/briefing/sampleBriefing.ts +148 -0
  393. manigent/web/frontend/src/components/channels/ChannelBadge.tsx +35 -0
  394. manigent/web/frontend/src/components/chatroom/ChannelThreadControls.tsx +127 -0
  395. manigent/web/frontend/src/components/chatroom/ChannelThreadNotice.test.tsx +207 -0
  396. manigent/web/frontend/src/components/chatroom/ChannelThreadNotice.tsx +80 -0
  397. manigent/web/frontend/src/components/chatroom/ChatroomView.channel.test.tsx +550 -0
  398. manigent/web/frontend/src/components/chatroom/ChatroomView.flow.test.tsx +186 -0
  399. manigent/web/frontend/src/components/chatroom/ChatroomView.isolation.test.tsx +70 -0
  400. manigent/web/frontend/src/components/chatroom/ChatroomView.tsx +450 -0
  401. manigent/web/frontend/src/components/chatroom/DelegationRow.test.tsx +170 -0
  402. manigent/web/frontend/src/components/chatroom/DelegationRow.tsx +217 -0
  403. manigent/web/frontend/src/components/chatroom/DeliveryStatusLine.test.tsx +33 -0
  404. manigent/web/frontend/src/components/chatroom/DeliveryStatusLine.tsx +46 -0
  405. manigent/web/frontend/src/components/chatroom/ForwardModal.test.tsx +109 -0
  406. manigent/web/frontend/src/components/chatroom/ForwardModal.tsx +304 -0
  407. manigent/web/frontend/src/components/chatroom/HeldNotice.tsx +77 -0
  408. manigent/web/frontend/src/components/chatroom/MentionDropdown.tsx +97 -0
  409. manigent/web/frontend/src/components/chatroom/MessageInput.tsx +408 -0
  410. manigent/web/frontend/src/components/chatroom/MessageItem.tsx +425 -0
  411. manigent/web/frontend/src/components/chatroom/MessageList.outside.test.tsx +98 -0
  412. manigent/web/frontend/src/components/chatroom/MessageList.test.tsx +245 -0
  413. manigent/web/frontend/src/components/chatroom/MessageList.tsx +406 -0
  414. manigent/web/frontend/src/components/chatroom/OfflineRemedyNote.test.tsx +211 -0
  415. manigent/web/frontend/src/components/chatroom/OfflineRemedyNote.tsx +182 -0
  416. manigent/web/frontend/src/components/chatroom/OutsideBadge.tsx +18 -0
  417. manigent/web/frontend/src/components/chatroom/RoomFlowControl.tsx +49 -0
  418. manigent/web/frontend/src/components/chatroom/RoomFlowStrip.test.tsx +47 -0
  419. manigent/web/frontend/src/components/chatroom/RoomFlowStrip.tsx +25 -0
  420. manigent/web/frontend/src/components/chatroom/SaveToPackModal.tsx +350 -0
  421. manigent/web/frontend/src/components/chatroom/StagedAttachments.test.tsx +56 -0
  422. manigent/web/frontend/src/components/chatroom/StagedAttachments.tsx +160 -0
  423. manigent/web/frontend/src/components/chatroom/SystemNote.tsx +18 -0
  424. manigent/web/frontend/src/components/chatroom/contactSender.test.tsx +130 -0
  425. manigent/web/frontend/src/components/chatroom/index.ts +3 -0
  426. manigent/web/frontend/src/components/chatroom/recipientStatus.test.ts +344 -0
  427. manigent/web/frontend/src/components/chatroom/recipientStatus.ts +197 -0
  428. manigent/web/frontend/src/components/common/AnnouncementBanners.test.tsx +221 -0
  429. manigent/web/frontend/src/components/common/AnnouncementBanners.tsx +180 -0
  430. manigent/web/frontend/src/components/common/Avatar.test.tsx +50 -0
  431. manigent/web/frontend/src/components/common/Avatar.tsx +102 -0
  432. manigent/web/frontend/src/components/common/Badge.tsx +30 -0
  433. manigent/web/frontend/src/components/common/BrandName.tsx +16 -0
  434. manigent/web/frontend/src/components/common/ClearHistoryButton.tsx +74 -0
  435. manigent/web/frontend/src/components/common/CollapsibleHeader.tsx +64 -0
  436. manigent/web/frontend/src/components/common/ConnectionStatus.test.tsx +56 -0
  437. manigent/web/frontend/src/components/common/ConnectionStatus.tsx +28 -0
  438. manigent/web/frontend/src/components/common/DropOverlay.tsx +27 -0
  439. manigent/web/frontend/src/components/common/InlineEditableTitle.test.tsx +369 -0
  440. manigent/web/frontend/src/components/common/InlineEditableTitle.tsx +287 -0
  441. manigent/web/frontend/src/components/common/IsolationUnavailablePanel.tsx +19 -0
  442. manigent/web/frontend/src/components/common/Loading.tsx +22 -0
  443. manigent/web/frontend/src/components/common/ModelSelector.tsx +248 -0
  444. manigent/web/frontend/src/components/common/Overlay.tsx +15 -0
  445. manigent/web/frontend/src/components/common/ResizeHandle.tsx +52 -0
  446. manigent/web/frontend/src/components/common/TagAutocompleteInput.test.tsx +92 -0
  447. manigent/web/frontend/src/components/common/TagAutocompleteInput.tsx +246 -0
  448. manigent/web/frontend/src/components/common/TagPillInput.tsx +181 -0
  449. manigent/web/frontend/src/components/common/TeamAndAgentPicker.tsx +83 -0
  450. manigent/web/frontend/src/components/common/Timestamp.tsx +61 -0
  451. manigent/web/frontend/src/components/common/VerifiedBadge.tsx +20 -0
  452. manigent/web/frontend/src/components/common/index.ts +8 -0
  453. manigent/web/frontend/src/components/composer/PhPopover.tsx +239 -0
  454. manigent/web/frontend/src/components/composer/PromptEditor.test.tsx +472 -0
  455. manigent/web/frontend/src/components/composer/PromptEditor.tsx +540 -0
  456. manigent/web/frontend/src/components/composer/caret.ts +159 -0
  457. manigent/web/frontend/src/components/composer/placeholders.test.ts +352 -0
  458. manigent/web/frontend/src/components/composer/placeholders.ts +354 -0
  459. manigent/web/frontend/src/components/composer/promptEditor.css +161 -0
  460. manigent/web/frontend/src/components/computer/ComputerPage.test.tsx +452 -0
  461. manigent/web/frontend/src/components/computer/ComputerPage.tsx +811 -0
  462. manigent/web/frontend/src/components/computer/ConnectComputerModal.tsx +135 -0
  463. manigent/web/frontend/src/components/computer/CopyCommand.tsx +77 -0
  464. manigent/web/frontend/src/components/computer/DisconnectComputerModal.tsx +121 -0
  465. manigent/web/frontend/src/components/computer/SoftwareCard.tsx +145 -0
  466. manigent/web/frontend/src/components/computer/TerminalCard.test.tsx +114 -0
  467. manigent/web/frontend/src/components/computer/TerminalCard.tsx +131 -0
  468. manigent/web/frontend/src/components/computer/TerminalPanel.tsx +226 -0
  469. manigent/web/frontend/src/components/computer/TrustPanel.test.tsx +58 -0
  470. manigent/web/frontend/src/components/computer/TrustPanel.tsx +143 -0
  471. manigent/web/frontend/src/components/computer/computerCopy.test.ts +322 -0
  472. manigent/web/frontend/src/components/computer/computerCopy.ts +413 -0
  473. manigent/web/frontend/src/components/computer/sections.ts +21 -0
  474. manigent/web/frontend/src/components/computer/terminalProtocol.test.ts +135 -0
  475. manigent/web/frontend/src/components/computer/terminalProtocol.ts +170 -0
  476. manigent/web/frontend/src/components/context/ContextGuide.test.tsx +26 -0
  477. manigent/web/frontend/src/components/context/ContextGuide.tsx +144 -0
  478. manigent/web/frontend/src/components/context/ContextLanding.tsx +46 -0
  479. manigent/web/frontend/src/components/desk/CardBand.a11y.test.tsx +138 -0
  480. manigent/web/frontend/src/components/desk/CardBand.tsx +132 -0
  481. manigent/web/frontend/src/components/desk/DashTile.tsx +81 -0
  482. manigent/web/frontend/src/components/desk/DeskAgentPicker.retained.test.tsx +79 -0
  483. manigent/web/frontend/src/components/desk/DeskAgentPicker.tsx +151 -0
  484. manigent/web/frontend/src/components/desk/DeskComposer.detachTodo.test.tsx +233 -0
  485. manigent/web/frontend/src/components/desk/DeskComposer.dispatch.test.tsx +208 -0
  486. manigent/web/frontend/src/components/desk/DeskComposer.loadTodoDraft.test.tsx +242 -0
  487. manigent/web/frontend/src/components/desk/DeskComposer.restage.test.tsx +258 -0
  488. manigent/web/frontend/src/components/desk/DeskComposer.saveTodo.test.tsx +265 -0
  489. manigent/web/frontend/src/components/desk/DeskComposer.tsx +828 -0
  490. manigent/web/frontend/src/components/desk/DeskPage.crossHighlight.test.tsx +427 -0
  491. manigent/web/frontend/src/components/desk/DeskPage.link.test.tsx +271 -0
  492. manigent/web/frontend/src/components/desk/DeskPage.tsx +1066 -0
  493. manigent/web/frontend/src/components/desk/DeskPage.welcomeTour.test.tsx +262 -0
  494. manigent/web/frontend/src/components/desk/DeskSideRail.tsx +100 -0
  495. manigent/web/frontend/src/components/desk/DeskWelcomeTour.tsx +513 -0
  496. manigent/web/frontend/src/components/desk/DeskZeroState.tsx +45 -0
  497. manigent/web/frontend/src/components/desk/LabelChip.tsx +147 -0
  498. manigent/web/frontend/src/components/desk/LabelFilter.tsx +474 -0
  499. manigent/web/frontend/src/components/desk/LabelInput.test.tsx +439 -0
  500. manigent/web/frontend/src/components/desk/LabelInput.tsx +477 -0
  501. manigent/web/frontend/src/components/desk/LabelManageSheet.test.tsx +384 -0
  502. manigent/web/frontend/src/components/desk/LabelManageSheet.tsx +498 -0
  503. manigent/web/frontend/src/components/desk/PhaseRail.test.tsx +170 -0
  504. manigent/web/frontend/src/components/desk/PhaseRail.tsx +120 -0
  505. manigent/web/frontend/src/components/desk/PlanBand.test.tsx +655 -0
  506. manigent/web/frontend/src/components/desk/PlanBand.tsx +216 -0
  507. manigent/web/frontend/src/components/desk/SaveTodoModal.tsx +126 -0
  508. manigent/web/frontend/src/components/desk/SopEditor.tsx +280 -0
  509. manigent/web/frontend/src/components/desk/SopItem.tsx +171 -0
  510. manigent/web/frontend/src/components/desk/SopRail.test.tsx +517 -0
  511. manigent/web/frontend/src/components/desk/SopRail.tsx +250 -0
  512. manigent/web/frontend/src/components/desk/TicketChip.tsx +51 -0
  513. manigent/web/frontend/src/components/desk/TodoItem.attachments.test.tsx +96 -0
  514. manigent/web/frontend/src/components/desk/TodoItem.menu.test.tsx +142 -0
  515. manigent/web/frontend/src/components/desk/TodoItem.tsx +573 -0
  516. manigent/web/frontend/src/components/desk/TodoRail.longpress.test.tsx +396 -0
  517. manigent/web/frontend/src/components/desk/TodoRail.test.tsx +1155 -0
  518. manigent/web/frontend/src/components/desk/TodoRail.tsx +918 -0
  519. manigent/web/frontend/src/components/desk/TodoRowMenu.tsx +147 -0
  520. manigent/web/frontend/src/components/desk/UpdateCard.bands.test.tsx +308 -0
  521. manigent/web/frontend/src/components/desk/UpdateCard.sendBack.test.tsx +444 -0
  522. manigent/web/frontend/src/components/desk/UpdateCard.tsx +943 -0
  523. manigent/web/frontend/src/components/desk/UpdateCard.untrusted.test.tsx +161 -0
  524. manigent/web/frontend/src/components/desk/desk.css +1921 -0
  525. manigent/web/frontend/src/components/desk/desk.css.test.ts +582 -0
  526. manigent/web/frontend/src/components/desk/deskBands.test.ts +389 -0
  527. manigent/web/frontend/src/components/desk/deskBands.ts +226 -0
  528. manigent/web/frontend/src/components/desk/m16Surfaces.test.ts +134 -0
  529. manigent/web/frontend/src/components/desk/sopSyntax.ts +73 -0
  530. manigent/web/frontend/src/components/discovery/AgentDirectory.tsx +71 -0
  531. manigent/web/frontend/src/components/discovery/AgentFilterBar.tsx +46 -0
  532. manigent/web/frontend/src/components/discovery/AgentSearchBar.tsx +56 -0
  533. manigent/web/frontend/src/components/dm/AgentSummaryCard.tsx +154 -0
  534. manigent/web/frontend/src/components/dm/ComposeFields.tsx +145 -0
  535. manigent/web/frontend/src/components/dm/DMChatView.flow.test.tsx +374 -0
  536. manigent/web/frontend/src/components/dm/DMChatView.tsx +841 -0
  537. manigent/web/frontend/src/components/dm/DMZeroState.tsx +204 -0
  538. manigent/web/frontend/src/components/dm/DmComposeModal.tsx +301 -0
  539. manigent/web/frontend/src/components/dm/PersonalizeModal.tsx +366 -0
  540. manigent/web/frontend/src/components/dm/index.ts +1 -0
  541. manigent/web/frontend/src/components/files/FileItem.tsx +138 -0
  542. manigent/web/frontend/src/components/files/FileList.outside.test.tsx +29 -0
  543. manigent/web/frontend/src/components/files/FileList.tsx +48 -0
  544. manigent/web/frontend/src/components/files/FilePreview.test.tsx +75 -0
  545. manigent/web/frontend/src/components/files/FilePreview.tsx +325 -0
  546. manigent/web/frontend/src/components/files/FileViewerModal.test.tsx +157 -0
  547. manigent/web/frontend/src/components/files/FileViewerModal.tsx +261 -0
  548. manigent/web/frontend/src/components/files/HighlightedCode.tsx +39 -0
  549. manigent/web/frontend/src/components/files/SaveToAgentPanel.test.tsx +156 -0
  550. manigent/web/frontend/src/components/files/SaveToAgentPanel.tsx +545 -0
  551. manigent/web/frontend/src/components/files/index.ts +3 -0
  552. manigent/web/frontend/src/components/hero/AgentAvatar.tsx +66 -0
  553. manigent/web/frontend/src/components/hero/Button.tsx +66 -0
  554. manigent/web/frontend/src/components/hero/Hero.tsx +133 -0
  555. manigent/web/frontend/src/components/homefs/HomeFolderBrowser.test.tsx +393 -0
  556. manigent/web/frontend/src/components/homefs/HomeFolderBrowser.tsx +792 -0
  557. manigent/web/frontend/src/components/hub/BridgeDeliveryLine.test.tsx +40 -0
  558. manigent/web/frontend/src/components/hub/BridgeDeliveryLine.tsx +36 -0
  559. manigent/web/frontend/src/components/hub/HubAvatar.test.tsx +93 -0
  560. manigent/web/frontend/src/components/hub/HubAvatar.tsx +33 -0
  561. manigent/web/frontend/src/components/hub/HubChatView.test.tsx +695 -0
  562. manigent/web/frontend/src/components/hub/HubChatView.tsx +504 -0
  563. manigent/web/frontend/src/components/hub/HubContactLabel.test.tsx +92 -0
  564. manigent/web/frontend/src/components/hub/HubContactLabel.tsx +47 -0
  565. manigent/web/frontend/src/components/hub/HubDeliveryLine.tsx +49 -0
  566. manigent/web/frontend/src/components/hub/HubDisclosure.tsx +49 -0
  567. manigent/web/frontend/src/components/hub/HubEnrollmentDialog.test.tsx +144 -0
  568. manigent/web/frontend/src/components/hub/HubEnrollmentDialog.tsx +244 -0
  569. manigent/web/frontend/src/components/hub/HubMessageBody.test.tsx +26 -0
  570. manigent/web/frontend/src/components/hub/HubMessageBody.tsx +14 -0
  571. manigent/web/frontend/src/components/hub/HubOfflineConfirm.tsx +45 -0
  572. manigent/web/frontend/src/components/hub/HubThreadBanner.tsx +33 -0
  573. manigent/web/frontend/src/components/hub/HubThreadGuard.tsx +59 -0
  574. manigent/web/frontend/src/components/hub/RemoteAgentCard.test.tsx +50 -0
  575. manigent/web/frontend/src/components/hub/RemoteAgentCard.tsx +174 -0
  576. manigent/web/frontend/src/components/hub/RemoteAgentSection.test.tsx +446 -0
  577. manigent/web/frontend/src/components/hub/RemoteAgentSection.tsx +219 -0
  578. manigent/web/frontend/src/components/hub/ReportDialog.test.tsx +96 -0
  579. manigent/web/frontend/src/components/hub/ReportDialog.tsx +162 -0
  580. manigent/web/frontend/src/components/hub/RetainButton.test.tsx +103 -0
  581. manigent/web/frontend/src/components/hub/RetainButton.tsx +114 -0
  582. manigent/web/frontend/src/components/hub/RetainDialog.test.tsx +68 -0
  583. manigent/web/frontend/src/components/hub/RetainDialog.tsx +115 -0
  584. manigent/web/frontend/src/components/hub/RetainedList.test.tsx +94 -0
  585. manigent/web/frontend/src/components/hub/RetainedList.tsx +32 -0
  586. manigent/web/frontend/src/components/hub/RetainedRow.tsx +129 -0
  587. manigent/web/frontend/src/components/hub/StandInChip.test.tsx +37 -0
  588. manigent/web/frontend/src/components/hub/StandInChip.tsx +43 -0
  589. manigent/web/frontend/src/components/hub/UnusualNameChip.test.tsx +81 -0
  590. manigent/web/frontend/src/components/hub/UnusualNameChip.tsx +22 -0
  591. manigent/web/frontend/src/components/hub/hubLint.test.ts +265 -0
  592. manigent/web/frontend/src/components/landing/ComparisonSection.tsx +92 -0
  593. manigent/web/frontend/src/components/landing/ConsoleSection.tsx +402 -0
  594. manigent/web/frontend/src/components/landing/CrewSection.tsx +224 -0
  595. manigent/web/frontend/src/components/landing/FAQ.tsx +68 -0
  596. manigent/web/frontend/src/components/landing/Footer.tsx +58 -0
  597. manigent/web/frontend/src/components/landing/HowItWorks.tsx +117 -0
  598. manigent/web/frontend/src/components/landing/KnowledgeSection.tsx +91 -0
  599. manigent/web/frontend/src/components/landing/LandingPage.test.tsx +134 -0
  600. manigent/web/frontend/src/components/landing/LearningSection.tsx +486 -0
  601. manigent/web/frontend/src/components/landing/Nav.tsx +68 -0
  602. manigent/web/frontend/src/components/landing/Pricing.tsx +263 -0
  603. manigent/web/frontend/src/components/landing/Section.tsx +98 -0
  604. manigent/web/frontend/src/components/landing/StaffingSection.tsx +122 -0
  605. manigent/web/frontend/src/components/landing/WhyNowSection.tsx +92 -0
  606. manigent/web/frontend/src/components/landing/WhyTeamSection.tsx +73 -0
  607. manigent/web/frontend/src/components/landing/earlyAccess.ts +23 -0
  608. manigent/web/frontend/src/components/layout/AppLayout.tsx +77 -0
  609. manigent/web/frontend/src/components/layout/Sidebar.tsx +172 -0
  610. manigent/web/frontend/src/components/layout/index.ts +2 -0
  611. manigent/web/frontend/src/components/project/ActivityFeed.tsx +247 -0
  612. manigent/web/frontend/src/components/project/CreateProjectPage.tsx +543 -0
  613. manigent/web/frontend/src/components/project/DeleteProjectButton.tsx +39 -0
  614. manigent/web/frontend/src/components/project/ProjectPlanTab.phase.test.tsx +107 -0
  615. manigent/web/frontend/src/components/project/ProjectPlanTab.test.tsx +994 -0
  616. manigent/web/frontend/src/components/project/ProjectPlanTab.tsx +166 -0
  617. manigent/web/frontend/src/components/project/ProjectReportTab.tsx +139 -0
  618. manigent/web/frontend/src/components/project/ProjectView.tsx +390 -0
  619. manigent/web/frontend/src/components/project/index.ts +2 -0
  620. manigent/web/frontend/src/components/project/plan/PlanAcceptance.test.tsx +460 -0
  621. manigent/web/frontend/src/components/project/plan/PlanAcceptance.tsx +111 -0
  622. manigent/web/frontend/src/components/project/plan/PlanBlockEdit.tsx +111 -0
  623. manigent/web/frontend/src/components/project/plan/PlanBlockEditing.test.tsx +1132 -0
  624. manigent/web/frontend/src/components/project/plan/PlanBody.test.tsx +495 -0
  625. manigent/web/frontend/src/components/project/plan/PlanBody.tsx +992 -0
  626. manigent/web/frontend/src/components/project/plan/PlanCollide.tsx +254 -0
  627. manigent/web/frontend/src/components/project/plan/PlanCommentList.test.tsx +420 -0
  628. manigent/web/frontend/src/components/project/plan/PlanCommentList.tsx +391 -0
  629. manigent/web/frontend/src/components/project/plan/PlanDecisionNudge.test.tsx +226 -0
  630. manigent/web/frontend/src/components/project/plan/PlanDecisionNudge.tsx +128 -0
  631. manigent/web/frontend/src/components/project/plan/PlanDestructiveAccept.test.tsx +209 -0
  632. manigent/web/frontend/src/components/project/plan/PlanDiff.test.tsx +126 -0
  633. manigent/web/frontend/src/components/project/plan/PlanDiff.tsx +131 -0
  634. manigent/web/frontend/src/components/project/plan/PlanDiffSeries.test.tsx +500 -0
  635. manigent/web/frontend/src/components/project/plan/PlanDiffSeries.tsx +125 -0
  636. manigent/web/frontend/src/components/project/plan/PlanEditor.mounts.test.tsx +504 -0
  637. manigent/web/frontend/src/components/project/plan/PlanEditor.tsx +2159 -0
  638. manigent/web/frontend/src/components/project/plan/PlanHistory.tsx +69 -0
  639. manigent/web/frontend/src/components/project/plan/PlanInlineNotes.test.tsx +2398 -0
  640. manigent/web/frontend/src/components/project/plan/PlanNoteBase.test.tsx +215 -0
  641. manigent/web/frontend/src/components/project/plan/PlanNoteRow.test.tsx +562 -0
  642. manigent/web/frontend/src/components/project/plan/PlanNoteRow.tsx +908 -0
  643. manigent/web/frontend/src/components/project/plan/PlanQuotePop.tsx +254 -0
  644. manigent/web/frontend/src/components/project/plan/PlanStaleReconcile.test.tsx +244 -0
  645. manigent/web/frontend/src/components/project/plan/PlanSubmitWindow.test.tsx +281 -0
  646. manigent/web/frontend/src/components/project/plan/PlanTray.tsx +184 -0
  647. manigent/web/frontend/src/components/project/plan/PlanTrayMirror.test.tsx +242 -0
  648. manigent/web/frontend/src/components/project/plan/plan.css +1151 -0
  649. manigent/web/frontend/src/components/project/plan/planAnchor.test.tsx +459 -0
  650. manigent/web/frontend/src/components/project/plan/planAnchor.tsx +277 -0
  651. manigent/web/frontend/src/components/project/plan/planBlocks.test.tsx +470 -0
  652. manigent/web/frontend/src/components/project/plan/planBlocks.ts +242 -0
  653. manigent/web/frontend/src/components/project/plan/planContrast.test.ts +229 -0
  654. manigent/web/frontend/src/components/project/plan/planDiffs.test.ts +455 -0
  655. manigent/web/frontend/src/components/project/plan/planDiffs.ts +358 -0
  656. manigent/web/frontend/src/components/project/plan/planTestServer.ts +741 -0
  657. manigent/web/frontend/src/components/project/plan/planTransport.test.ts +160 -0
  658. manigent/web/frontend/src/components/project/plan/planTrayLogic.test.ts +1201 -0
  659. manigent/web/frontend/src/components/project/plan/planTrayLogic.ts +1164 -0
  660. manigent/web/frontend/src/components/publicAgents/PublicAgentsAccount.tsx +162 -0
  661. manigent/web/frontend/src/components/publicAgents/api.ts +55 -0
  662. manigent/web/frontend/src/components/publicAgents/parts.tsx +54 -0
  663. manigent/web/frontend/src/components/publicAgents/publicAgents.css +53 -0
  664. manigent/web/frontend/src/components/publicAgents/publicAgents.test.tsx +86 -0
  665. manigent/web/frontend/src/components/publicAgents/widget.test.ts +148 -0
  666. manigent/web/frontend/src/components/publicAgents/widget.ts +40 -0
  667. manigent/web/frontend/src/components/publicRequests/AgentDrawer.tsx +101 -0
  668. manigent/web/frontend/src/components/publicRequests/AgentPanel.tsx +173 -0
  669. manigent/web/frontend/src/components/publicRequests/AgentRankList.tsx +61 -0
  670. manigent/web/frontend/src/components/publicRequests/MatchRetainButton.tsx +67 -0
  671. manigent/web/frontend/src/components/publicRequests/ProgressBoard.tsx +74 -0
  672. manigent/web/frontend/src/components/publicRequests/PublicRequestsPage.tsx +57 -0
  673. manigent/web/frontend/src/components/publicRequests/RequestHome.tsx +99 -0
  674. manigent/web/frontend/src/components/publicRequests/RequestRoom.tsx +126 -0
  675. manigent/web/frontend/src/components/publicRequests/agentView.ts +65 -0
  676. manigent/web/frontend/src/components/publicRequests/publicRequests.css +209 -0
  677. manigent/web/frontend/src/components/publicRequests/publicRequests.test.tsx +172 -0
  678. manigent/web/frontend/src/components/publicRequests/requestCopy.ts +121 -0
  679. manigent/web/frontend/src/components/sample-request/SampleIdeaList.test.tsx +190 -0
  680. manigent/web/frontend/src/components/sample-request/SampleIdeaList.tsx +180 -0
  681. manigent/web/frontend/src/components/sample-request/SampleRequestBody.tsx +276 -0
  682. manigent/web/frontend/src/components/sample-request/assistantStarterSamples.ts +62 -0
  683. manigent/web/frontend/src/components/sample-request/sampleIdeaList.css +106 -0
  684. manigent/web/frontend/src/components/scheduled/CronPicker.tsx +80 -0
  685. manigent/web/frontend/src/components/scheduled/ScheduleMessageModal.tsx +341 -0
  686. manigent/web/frontend/src/components/scheduled/ScheduledMessagesList.tsx +157 -0
  687. manigent/web/frontend/src/components/settings/AgentSettingsPage.tsx +366 -0
  688. manigent/web/frontend/src/components/settings/ConnectedAccounts.tsx +277 -0
  689. manigent/web/frontend/src/components/settings/DefaultModelSection.test.tsx +96 -0
  690. manigent/web/frontend/src/components/settings/DefaultModelSection.tsx +212 -0
  691. manigent/web/frontend/src/components/settings/DisconnectConfirmModal.tsx +93 -0
  692. manigent/web/frontend/src/components/settings/FrontDeskSettingsPage.isolation.test.tsx +66 -0
  693. manigent/web/frontend/src/components/settings/FrontDeskSettingsPage.tsx +134 -0
  694. manigent/web/frontend/src/components/settings/InvitableAgentsPicker.tsx +182 -0
  695. manigent/web/frontend/src/components/settings/KnowledgePacksSection.tsx +867 -0
  696. manigent/web/frontend/src/components/settings/ModelConfigDialog.catalogDriven.test.tsx +79 -0
  697. manigent/web/frontend/src/components/settings/ModelConfigDialog.tsx +343 -0
  698. manigent/web/frontend/src/components/settings/ModelConfigSection.smartRouting.test.tsx +109 -0
  699. manigent/web/frontend/src/components/settings/ModelConfigSection.tsx +599 -0
  700. manigent/web/frontend/src/components/settings/ReflectionScheduleSection.tsx +170 -0
  701. manigent/web/frontend/src/components/settings/UserSettingsPage.tsx +168 -0
  702. manigent/web/frontend/src/components/settings/agent/AgentStatusStrip.tsx +71 -0
  703. manigent/web/frontend/src/components/settings/agent/BlockedDeploymentsCard.test.tsx +81 -0
  704. manigent/web/frontend/src/components/settings/agent/BlockedDeploymentsCard.tsx +62 -0
  705. manigent/web/frontend/src/components/settings/agent/ChannelIsolationLine.tsx +21 -0
  706. manigent/web/frontend/src/components/settings/agent/ChannelsSection.test.tsx +879 -0
  707. manigent/web/frontend/src/components/settings/agent/ChannelsSection.tsx +1124 -0
  708. manigent/web/frontend/src/components/settings/agent/EnvVarsCard.test.ts +15 -0
  709. manigent/web/frontend/src/components/settings/agent/EnvVarsCard.tsx +301 -0
  710. manigent/web/frontend/src/components/settings/agent/ExtraDirsList.tsx +85 -0
  711. manigent/web/frontend/src/components/settings/agent/GeneralSection.tsx +88 -0
  712. manigent/web/frontend/src/components/settings/agent/HostDisclaimerSlot.test.tsx +32 -0
  713. manigent/web/frontend/src/components/settings/agent/HostDisclaimerSlot.tsx +30 -0
  714. manigent/web/frontend/src/components/settings/agent/HubChannelCard.test.tsx +449 -0
  715. manigent/web/frontend/src/components/settings/agent/HubChannelCard.tsx +312 -0
  716. manigent/web/frontend/src/components/settings/agent/HubInvitableAgentsCard.test.tsx +83 -0
  717. manigent/web/frontend/src/components/settings/agent/HubInvitableAgentsCard.tsx +64 -0
  718. manigent/web/frontend/src/components/settings/agent/HubSendUsageCard.test.tsx +69 -0
  719. manigent/web/frontend/src/components/settings/agent/HubSendUsageCard.tsx +42 -0
  720. manigent/web/frontend/src/components/settings/agent/HubTurnOffDialog.tsx +88 -0
  721. manigent/web/frontend/src/components/settings/agent/IsolationRecheckButton.test.tsx +26 -0
  722. manigent/web/frontend/src/components/settings/agent/IsolationRecheckButton.tsx +68 -0
  723. manigent/web/frontend/src/components/settings/agent/KnowledgeSection.tsx +68 -0
  724. manigent/web/frontend/src/components/settings/agent/LineChannelCard.test.tsx +428 -0
  725. manigent/web/frontend/src/components/settings/agent/LineChannelCard.tsx +343 -0
  726. manigent/web/frontend/src/components/settings/agent/RuntimeSection.test.tsx +164 -0
  727. manigent/web/frontend/src/components/settings/agent/RuntimeSection.tsx +118 -0
  728. manigent/web/frontend/src/components/settings/agent/StorageSection.tsx +78 -0
  729. manigent/web/frontend/src/components/settings/agent/WebChatChannelCard.test.tsx +253 -0
  730. manigent/web/frontend/src/components/settings/agent/WebChatChannelCard.tsx +401 -0
  731. manigent/web/frontend/src/components/settings/agent/WhatsAppChannelCard.test.tsx +462 -0
  732. manigent/web/frontend/src/components/settings/agent/WhatsAppChannelCard.tsx +289 -0
  733. manigent/web/frontend/src/components/settings/agent/agentSettings.test.tsx +109 -0
  734. manigent/web/frontend/src/components/settings/agent/channelSetupGuides.test.tsx +162 -0
  735. manigent/web/frontend/src/components/settings/agent/channelSetupGuides.tsx +119 -0
  736. manigent/web/frontend/src/components/settings/agent/channelsShared.ts +83 -0
  737. manigent/web/frontend/src/components/settings/agent/fields.tsx +67 -0
  738. manigent/web/frontend/src/components/settings/agent/sections.ts +36 -0
  739. manigent/web/frontend/src/components/settings/agent/useAgentSettingsDraft.ts +129 -0
  740. manigent/web/frontend/src/components/settings/agent/useInstallSelection.ts +38 -0
  741. manigent/web/frontend/src/components/settings/primitives.tsx +98 -0
  742. manigent/web/frontend/src/components/shared/ExpandToggle.tsx +45 -0
  743. manigent/web/frontend/src/components/shared/MarkdownBody.tsx +218 -0
  744. manigent/web/frontend/src/components/shared/MarkdownBody.untrusted.test.tsx +97 -0
  745. manigent/web/frontend/src/components/shared/RenderBody.tsx +90 -0
  746. manigent/web/frontend/src/components/shared/rehypeHideComments.test.tsx +115 -0
  747. manigent/web/frontend/src/components/shared/rehypeHideComments.ts +124 -0
  748. manigent/web/frontend/src/components/sharing/AcceptSharePage.tsx +141 -0
  749. manigent/web/frontend/src/components/sharing/ShareLandingPage.tsx +89 -0
  750. manigent/web/frontend/src/components/sidebar/AccountChip.test.tsx +337 -0
  751. manigent/web/frontend/src/components/sidebar/AccountChip.tsx +407 -0
  752. manigent/web/frontend/src/components/sidebar/ActiveWorkDot.tsx +9 -0
  753. manigent/web/frontend/src/components/sidebar/AgentProfile.tsx +44 -0
  754. manigent/web/frontend/src/components/sidebar/AgentRow.tsx +71 -0
  755. manigent/web/frontend/src/components/sidebar/AgentsPanel.retained.test.tsx +105 -0
  756. manigent/web/frontend/src/components/sidebar/AgentsPanel.tsx +231 -0
  757. manigent/web/frontend/src/components/sidebar/AssistantHero.tsx +48 -0
  758. manigent/web/frontend/src/components/sidebar/ChatroomItem.tsx +48 -0
  759. manigent/web/frontend/src/components/sidebar/CommandPalette.channelThreads.test.tsx +76 -0
  760. manigent/web/frontend/src/components/sidebar/CommandPalette.reopen.test.tsx +206 -0
  761. manigent/web/frontend/src/components/sidebar/CommandPalette.test.tsx +725 -0
  762. manigent/web/frontend/src/components/sidebar/CommandPalette.tsx +774 -0
  763. manigent/web/frontend/src/components/sidebar/ComputersPanel.tsx +144 -0
  764. manigent/web/frontend/src/components/sidebar/DmIcon.tsx +46 -0
  765. manigent/web/frontend/src/components/sidebar/DmsPanel.tsx +226 -0
  766. manigent/web/frontend/src/components/sidebar/FrontDeskPanel.channels.test.tsx +582 -0
  767. manigent/web/frontend/src/components/sidebar/FrontDeskPanel.test.tsx +151 -0
  768. manigent/web/frontend/src/components/sidebar/FrontDeskPanel.tsx +454 -0
  769. manigent/web/frontend/src/components/sidebar/HotkeysHelp.tsx +101 -0
  770. manigent/web/frontend/src/components/sidebar/MessageHitRow.test.tsx +84 -0
  771. manigent/web/frontend/src/components/sidebar/MessageHitRow.tsx +57 -0
  772. manigent/web/frontend/src/components/sidebar/MyDeskRailPanel.crossHighlight.test.tsx +247 -0
  773. manigent/web/frontend/src/components/sidebar/MyDeskRailPanel.delete.test.tsx +273 -0
  774. manigent/web/frontend/src/components/sidebar/MyDeskRailPanel.tsx +351 -0
  775. manigent/web/frontend/src/components/sidebar/PanelShell.tsx +48 -0
  776. manigent/web/frontend/src/components/sidebar/ProjectItem.tsx +161 -0
  777. manigent/web/frontend/src/components/sidebar/ProjectRow.tsx +151 -0
  778. manigent/web/frontend/src/components/sidebar/ProjectsPanel.tsx +80 -0
  779. manigent/web/frontend/src/components/sidebar/PublicPanel.counterpart.test.tsx +85 -0
  780. manigent/web/frontend/src/components/sidebar/PublicPanel.hub.test.tsx +147 -0
  781. manigent/web/frontend/src/components/sidebar/PublicPanel.tsx +271 -0
  782. manigent/web/frontend/src/components/sidebar/RecencyHeader.tsx +17 -0
  783. manigent/web/frontend/src/components/sidebar/ScheduledPanel.tsx +58 -0
  784. manigent/web/frontend/src/components/sidebar/SidebarAvatar.tsx +128 -0
  785. manigent/web/frontend/src/components/sidebar/SidebarRail.tsx +163 -0
  786. manigent/web/frontend/src/components/sidebar/TeamGroup.tsx +67 -0
  787. manigent/web/frontend/src/components/sidebar/dmPrefill.test.ts +69 -0
  788. manigent/web/frontend/src/components/sidebar/dmPrefill.ts +80 -0
  789. manigent/web/frontend/src/components/sidebar/index.ts +18 -0
  790. manigent/web/frontend/src/components/sidebar/railShortcuts.ts +18 -0
  791. manigent/web/frontend/src/components/today/lib.test.ts +70 -0
  792. manigent/web/frontend/src/components/today/lib.ts +296 -0
  793. manigent/web/frontend/src/components/today/today.css +113 -0
  794. manigent/web/frontend/src/components/welcome/RunnerSetup.css +253 -0
  795. manigent/web/frontend/src/components/welcome/RunnerSetup.tsx +148 -0
  796. manigent/web/frontend/src/components/welcome/WelcomePage.css +383 -0
  797. manigent/web/frontend/src/components/welcome/WelcomePage.tsx +329 -0
  798. manigent/web/frontend/src/components/welcome/welcomeCopy.test.ts +382 -0
  799. manigent/web/frontend/src/components/welcome/welcomeCopy.ts +313 -0
  800. manigent/web/frontend/src/config/llmProviders.test.ts +161 -0
  801. manigent/web/frontend/src/config/llmProviders.ts +109 -0
  802. manigent/web/frontend/src/config/providerIds.fixture.json +13 -0
  803. manigent/web/frontend/src/hooks/accountNavigation.test.tsx +138 -0
  804. manigent/web/frontend/src/hooks/index.ts +10 -0
  805. manigent/web/frontend/src/hooks/useActiveWorkBootstrap.ts +86 -0
  806. manigent/web/frontend/src/hooks/useAgentAccess.test.tsx +30 -0
  807. manigent/web/frontend/src/hooks/useAgentAccess.ts +204 -0
  808. manigent/web/frontend/src/hooks/useAgentSampleRequests.ts +27 -0
  809. manigent/web/frontend/src/hooks/useAgentSearch.ts +10 -0
  810. manigent/web/frontend/src/hooks/useAgents.ts +46 -0
  811. manigent/web/frontend/src/hooks/useAnnouncements.test.ts +112 -0
  812. manigent/web/frontend/src/hooks/useAnnouncements.ts +100 -0
  813. manigent/web/frontend/src/hooks/useAssistantAgent.ts +24 -0
  814. manigent/web/frontend/src/hooks/useAutoResizeTextarea.ts +18 -0
  815. manigent/web/frontend/src/hooks/useBridgeDeliveries.test.tsx +79 -0
  816. manigent/web/frontend/src/hooks/useBriefTabs.ts +122 -0
  817. manigent/web/frontend/src/hooks/useChannelThreads.ts +69 -0
  818. manigent/web/frontend/src/hooks/useComposeForm.ts +80 -0
  819. manigent/web/frontend/src/hooks/useComputers.ts +123 -0
  820. manigent/web/frontend/src/hooks/useContextFirstItem.ts +118 -0
  821. manigent/web/frontend/src/hooks/useCreateProject.ts +78 -0
  822. manigent/web/frontend/src/hooks/useDebouncedValue.ts +16 -0
  823. manigent/web/frontend/src/hooks/useDeleteProject.ts +27 -0
  824. manigent/web/frontend/src/hooks/useDeskDispatch.test.tsx +170 -0
  825. manigent/web/frontend/src/hooks/useDeskDispatch.ts +135 -0
  826. manigent/web/frontend/src/hooks/useDeskReadState.test.tsx +85 -0
  827. manigent/web/frontend/src/hooks/useDeskReadState.ts +150 -0
  828. manigent/web/frontend/src/hooks/useDeskRecentlyDone.test.tsx +222 -0
  829. manigent/web/frontend/src/hooks/useDeskRecentlyDone.ts +225 -0
  830. manigent/web/frontend/src/hooks/useDeskTour.ts +37 -0
  831. manigent/web/frontend/src/hooks/useDeskUpdates.channelThreads.test.tsx +91 -0
  832. manigent/web/frontend/src/hooks/useDeskUpdates.hubThreads.test.tsx +93 -0
  833. manigent/web/frontend/src/hooks/useDeskUpdates.ts +353 -0
  834. manigent/web/frontend/src/hooks/useDisconnectIntegration.ts +23 -0
  835. manigent/web/frontend/src/hooks/useDmThreads.ts +99 -0
  836. manigent/web/frontend/src/hooks/useFileDropzone.ts +93 -0
  837. manigent/web/frontend/src/hooks/useFileUpload.ts +38 -0
  838. manigent/web/frontend/src/hooks/useFiles.ts +98 -0
  839. manigent/web/frontend/src/hooks/useHub.test.tsx +237 -0
  840. manigent/web/frontend/src/hooks/useHub.ts +502 -0
  841. manigent/web/frontend/src/hooks/useInstall.ts +82 -0
  842. manigent/web/frontend/src/hooks/useLabels.test.tsx +294 -0
  843. manigent/web/frontend/src/hooks/useLabels.ts +287 -0
  844. manigent/web/frontend/src/hooks/useLogout.ts +49 -0
  845. manigent/web/frontend/src/hooks/useMentionDetection.ts +90 -0
  846. manigent/web/frontend/src/hooks/useMessageAttachments.deskLimit.test.ts +264 -0
  847. manigent/web/frontend/src/hooks/useMessageAttachments.ts +283 -0
  848. manigent/web/frontend/src/hooks/useMessageDeepLink.test.tsx +70 -0
  849. manigent/web/frontend/src/hooks/useMessageDeepLink.ts +32 -0
  850. manigent/web/frontend/src/hooks/useMessageSearch.ts +78 -0
  851. manigent/web/frontend/src/hooks/useModelConfigs.ts +178 -0
  852. manigent/web/frontend/src/hooks/useMyAgents.ts +18 -0
  853. manigent/web/frontend/src/hooks/usePointerReorder.ts +270 -0
  854. manigent/web/frontend/src/hooks/usePrefersReducedMotion.ts +18 -0
  855. manigent/web/frontend/src/hooks/useProjectPlan.ts +36 -0
  856. manigent/web/frontend/src/hooks/useProjectReport.ts +19 -0
  857. manigent/web/frontend/src/hooks/useProjects.ts +104 -0
  858. manigent/web/frontend/src/hooks/useProjectsWithChatrooms.ts +97 -0
  859. manigent/web/frontend/src/hooks/usePublicRequests.ts +89 -0
  860. manigent/web/frontend/src/hooks/useRecentAgents.ts +38 -0
  861. manigent/web/frontend/src/hooks/useRenameProject.test.tsx +185 -0
  862. manigent/web/frontend/src/hooks/useRenameProject.ts +192 -0
  863. manigent/web/frontend/src/hooks/useRoomTrust.ts +56 -0
  864. manigent/web/frontend/src/hooks/useSharing.ts +46 -0
  865. manigent/web/frontend/src/hooks/useSopRecents.ts +55 -0
  866. manigent/web/frontend/src/hooks/useSopSchedules.ts +25 -0
  867. manigent/web/frontend/src/hooks/useSops.ts +99 -0
  868. manigent/web/frontend/src/hooks/useSwitchAccount.ts +41 -0
  869. manigent/web/frontend/src/hooks/useTeamGroups.ts +48 -0
  870. manigent/web/frontend/src/hooks/useThreadOutbox.ts +68 -0
  871. manigent/web/frontend/src/hooks/useTodos.add.test.tsx +89 -0
  872. manigent/web/frontend/src/hooks/useTodos.linkProject.test.tsx +115 -0
  873. manigent/web/frontend/src/hooks/useTodos.saveDraft.test.tsx +188 -0
  874. manigent/web/frontend/src/hooks/useTodos.ts +396 -0
  875. manigent/web/frontend/src/hooks/useUntrustedBodies.test.tsx +87 -0
  876. manigent/web/frontend/src/hooks/useUntrustedBodies.ts +51 -0
  877. manigent/web/frontend/src/hooks/useWebSocket.briefTab.test.tsx +148 -0
  878. manigent/web/frontend/src/hooks/useWebSocket.channelThreads.test.tsx +82 -0
  879. manigent/web/frontend/src/hooks/useWebSocket.hostSync.test.tsx +92 -0
  880. manigent/web/frontend/src/hooks/useWebSocket.hub.test.tsx +114 -0
  881. manigent/web/frontend/src/hooks/useWebSocket.labelSync.test.tsx +168 -0
  882. manigent/web/frontend/src/hooks/useWebSocket.plan.test.tsx +168 -0
  883. manigent/web/frontend/src/hooks/useWebSocket.reconnect.test.tsx +166 -0
  884. manigent/web/frontend/src/hooks/useWebSocket.runnerVersions.test.tsx +167 -0
  885. manigent/web/frontend/src/hooks/useWebSocket.ts +495 -0
  886. manigent/web/frontend/src/hooks/useWindowFileDropGuard.ts +33 -0
  887. manigent/web/frontend/src/landing.tsx +11 -0
  888. manigent/web/frontend/src/main.tsx +17 -0
  889. manigent/web/frontend/src/private-beta.tsx +10 -0
  890. manigent/web/frontend/src/public.tsx +42 -0
  891. manigent/web/frontend/src/queryClient.ts +16 -0
  892. manigent/web/frontend/src/stores/activeWorkStore.ts +88 -0
  893. manigent/web/frontend/src/stores/announcementStore.test.ts +217 -0
  894. manigent/web/frontend/src/stores/announcementStore.ts +169 -0
  895. manigent/web/frontend/src/stores/authStore.multiSession.test.ts +304 -0
  896. manigent/web/frontend/src/stores/authStore.test.ts +235 -0
  897. manigent/web/frontend/src/stores/authStore.ts +693 -0
  898. manigent/web/frontend/src/stores/deskLabelViewStore.test.ts +162 -0
  899. manigent/web/frontend/src/stores/deskLabelViewStore.ts +234 -0
  900. manigent/web/frontend/src/stores/deskLinkStore.ts +54 -0
  901. manigent/web/frontend/src/stores/deskStore.test.ts +53 -0
  902. manigent/web/frontend/src/stores/deskStore.ts +93 -0
  903. manigent/web/frontend/src/stores/heldPostsStore.ts +78 -0
  904. manigent/web/frontend/src/stores/mcpAuthStore.ts +52 -0
  905. manigent/web/frontend/src/stores/modelSelectionStore.ts +33 -0
  906. manigent/web/frontend/src/stores/perUserStores.ts +79 -0
  907. manigent/web/frontend/src/stores/runnerVersionStore.test.ts +81 -0
  908. manigent/web/frontend/src/stores/runnerVersionStore.ts +60 -0
  909. manigent/web/frontend/src/stores/skillAuthStore.ts +54 -0
  910. manigent/web/frontend/src/stores/switchReset.test.ts +236 -0
  911. manigent/web/frontend/src/stores/tokenExp.ts +32 -0
  912. manigent/web/frontend/src/stores/uiStore.ts +186 -0
  913. manigent/web/frontend/src/stores/unreadStore.ts +204 -0
  914. manigent/web/frontend/src/stores/unsavedGuard.ts +22 -0
  915. manigent/web/frontend/src/styles/design/fonts/JetBrainsMono-Latin.woff2 +0 -0
  916. manigent/web/frontend/src/styles/design/fonts/Nunito-Latin.woff2 +0 -0
  917. manigent/web/frontend/src/styles/design/fonts/Nunito-LatinExt.woff2 +0 -0
  918. manigent/web/frontend/src/styles/design/tokens.css +160 -0
  919. manigent/web/frontend/src/styles/homepage.css +242 -0
  920. manigent/web/frontend/src/styles/index.css +126 -0
  921. manigent/web/frontend/src/styles/landing.css +52 -0
  922. manigent/web/frontend/src/test/authFixtures.ts +154 -0
  923. manigent/web/frontend/src/test/fetchHarness.ts +203 -0
  924. manigent/web/frontend/src/test/fixtures/channelThreads.smoke.json +42 -0
  925. manigent/web/frontend/src/test/hubFixtures.ts +212 -0
  926. manigent/web/frontend/src/test/queryWrapper.tsx +9 -0
  927. manigent/web/frontend/src/test/suiteHygiene.test.ts +212 -0
  928. manigent/web/frontend/src/types/channelThreadName.test.ts +16 -0
  929. manigent/web/frontend/src/types/index.ts +903 -0
  930. manigent/web/frontend/src/utils/agentAccessCopy.test.ts +53 -0
  931. manigent/web/frontend/src/utils/agentAccessCopy.ts +231 -0
  932. manigent/web/frontend/src/utils/agentDisplay.ts +81 -0
  933. manigent/web/frontend/src/utils/briefSchedule.ts +25 -0
  934. manigent/web/frontend/src/utils/channelThreads.test.ts +109 -0
  935. manigent/web/frontend/src/utils/channelThreads.ts +179 -0
  936. manigent/web/frontend/src/utils/chatroomDisplay.ts +11 -0
  937. manigent/web/frontend/src/utils/deskLinks.test.ts +155 -0
  938. manigent/web/frontend/src/utils/deskLinks.ts +115 -0
  939. manigent/web/frontend/src/utils/deskRecipient.ts +29 -0
  940. manigent/web/frontend/src/utils/formatDate.ts +102 -0
  941. manigent/web/frontend/src/utils/frontDesk.ts +65 -0
  942. manigent/web/frontend/src/utils/hubChannelView.test.ts +178 -0
  943. manigent/web/frontend/src/utils/hubChannelView.ts +307 -0
  944. manigent/web/frontend/src/utils/hubDelivery.test.ts +105 -0
  945. manigent/web/frontend/src/utils/hubDelivery.ts +137 -0
  946. manigent/web/frontend/src/utils/hubDisplay.test.ts +157 -0
  947. manigent/web/frontend/src/utils/hubDisplay.ts +221 -0
  948. manigent/web/frontend/src/utils/hubThread.test.ts +184 -0
  949. manigent/web/frontend/src/utils/hubThread.ts +247 -0
  950. manigent/web/frontend/src/utils/labelErrors.test.ts +127 -0
  951. manigent/web/frontend/src/utils/labelErrors.ts +123 -0
  952. manigent/web/frontend/src/utils/labelView.test.ts +846 -0
  953. manigent/web/frontend/src/utils/labelView.ts +760 -0
  954. manigent/web/frontend/src/utils/legalUrls.ts +11 -0
  955. manigent/web/frontend/src/utils/messageSender.test.ts +32 -0
  956. manigent/web/frontend/src/utils/messageSender.ts +29 -0
  957. manigent/web/frontend/src/utils/oauthScopeCopy.ts +73 -0
  958. manigent/web/frontend/src/utils/personalize.ts +49 -0
  959. manigent/web/frontend/src/utils/reachableAgents.test.ts +36 -0
  960. manigent/web/frontend/src/utils/reachableAgents.ts +24 -0
  961. manigent/web/frontend/src/utils/recencyBuckets.ts +61 -0
  962. manigent/web/frontend/src/utils/roomTrust.test.ts +94 -0
  963. manigent/web/frontend/src/utils/roomTrust.ts +111 -0
  964. manigent/web/frontend/src/utils/snippetHighlight.test.ts +63 -0
  965. manigent/web/frontend/src/utils/snippetHighlight.ts +42 -0
  966. manigent/web/frontend/src/utils/sopRecipient.ts +37 -0
  967. manigent/web/frontend/src/utils/sopRequest.test.ts +91 -0
  968. manigent/web/frontend/src/utils/sopRequest.ts +68 -0
  969. manigent/web/frontend/src/utils/sopSchedule.ts +26 -0
  970. manigent/web/frontend/src/utils/sopSection.test.ts +148 -0
  971. manigent/web/frontend/src/utils/sopSection.ts +143 -0
  972. manigent/web/frontend/src/utils/textSearch.test.ts +60 -0
  973. manigent/web/frontend/src/utils/textSearch.ts +43 -0
  974. manigent/web/frontend/src/utils/thoughtPartner.ts +26 -0
  975. manigent/web/frontend/src/utils/threadName.test.ts +131 -0
  976. manigent/web/frontend/src/utils/threadName.ts +122 -0
  977. manigent/web/frontend/src/utils/ticketView.test.ts +296 -0
  978. manigent/web/frontend/src/utils/ticketView.ts +175 -0
  979. manigent/web/frontend/src/utils/todoAttachments.desiredState.test.ts +180 -0
  980. manigent/web/frontend/src/utils/todoAttachments.ts +126 -0
  981. manigent/web/frontend/src/utils/todoDraft.test.ts +84 -0
  982. manigent/web/frontend/src/utils/todoDraft.ts +46 -0
  983. manigent/web/frontend/src/utils/todoSearch.test.ts +82 -0
  984. manigent/web/frontend/src/utils/todoSearch.ts +43 -0
  985. manigent/web/frontend/src/utils/versionMatch.test.ts +295 -0
  986. manigent/web/frontend/src/utils/versionMatch.ts +245 -0
  987. manigent/web/frontend/src/vite-env.d.ts +1 -0
  988. manigent/web/frontend/tailwind.config.js +86 -0
  989. manigent/web/frontend/tos.html +188 -0
  990. manigent/web/frontend/tsconfig.dts.json +25 -0
  991. manigent/web/frontend/tsconfig.json +26 -0
  992. manigent/web/frontend/tsconfig.node.json +11 -0
  993. manigent/web/frontend/vite.config.ts +41 -0
  994. manigent/web/frontend/vitest.config.ts +16 -0
  995. manigent/ws_edge/app.py +514 -0
  996. manigent/ws_edge/auth.py +104 -0
  997. manigent/ws_edge/link.py +282 -0
  998. manigent/ws_edge/registry.py +206 -0
  999. manigent/ws_edge/terminal_relay.py +269 -0
  1000. manigent_server-0.1.0.dist-info/METADATA +138 -0
  1001. manigent_server-0.1.0.dist-info/RECORD +1004 -0
  1002. manigent_server-0.1.0.dist-info/WHEEL +4 -0
  1003. manigent_server-0.1.0.dist-info/entry_points.txt +2 -0
  1004. 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