taskuary 0.3.2.2__tar.gz → 0.3.2.4__tar.gz

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 (234) hide show
  1. {taskuary-0.3.2.2/taskuary.egg-info → taskuary-0.3.2.4}/PKG-INFO +26 -2
  2. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/README.md +25 -1
  3. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/pyproject.toml +1 -1
  4. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/aisetup.py +1 -1
  5. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/artifacts.py +64 -1
  6. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/assistant.py +129 -5
  7. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/categories.py +7 -2
  8. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/channels.py +11 -3
  9. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/ci.py +1 -1
  10. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/cli.py +70 -1
  11. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/coder.py +98 -4
  12. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/compose.py +2 -2
  13. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/config.py +42 -2
  14. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/db.py +1 -1
  15. taskuary-0.3.2.4/taskuary/demo.py +481 -0
  16. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/docsync.py +1 -1
  17. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/evalset.py +1 -1
  18. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/general.py +64 -1
  19. taskuary-0.3.2.4/taskuary/guard.py +101 -0
  20. taskuary-0.3.2.4/taskuary/handbook.py +271 -0
  21. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/histgen.py +1 -1
  22. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/hooks.py +10 -1
  23. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/ingest.py +39 -14
  24. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/llm.py +11 -1
  25. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/messengers.py +12 -1
  26. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/outbound.py +7 -3
  27. taskuary-0.3.2.4/taskuary/outbox.py +164 -0
  28. taskuary-0.3.2.4/taskuary/ownwork.py +85 -0
  29. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/problems.py +2 -2
  30. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/reports.py +39 -6
  31. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/research.py +3 -3
  32. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/routing.py +8 -3
  33. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/scopes.py +10 -1
  34. taskuary-0.3.2.4/taskuary/selfclose.py +247 -0
  35. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/server.py +301 -64
  36. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/setup.py +14 -7
  37. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/store.py +196 -34
  38. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/coder.md +25 -2
  39. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/triage.md +7 -3
  40. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/terminal.py +94 -6
  41. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/triage.py +35 -14
  42. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/voice.py +2 -2
  43. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/waitroom.py +7 -2
  44. taskuary-0.3.2.4/taskuary/web/assets/GeneralWorkspace-SrMAdua9.js +12 -0
  45. taskuary-0.3.2.4/taskuary/web/assets/SocialView-BV2-O2sP.js +1 -0
  46. taskuary-0.3.2.4/taskuary/web/assets/index-CJp5GTAb.js +1013 -0
  47. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/web/index.html +1 -1
  48. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/whatsapp/bridge.mjs +12 -1
  49. {taskuary-0.3.2.2 → taskuary-0.3.2.4/taskuary.egg-info}/PKG-INFO +26 -2
  50. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary.egg-info/SOURCES.txt +25 -2
  51. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_agent_wall.py +10 -2
  52. taskuary-0.3.2.4/tests/test_answered_yourself.py +91 -0
  53. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_api.py +2 -1
  54. taskuary-0.3.2.4/tests/test_assistant_verdicts.py +85 -0
  55. taskuary-0.3.2.4/tests/test_chat_doors.py +152 -0
  56. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_compose.py +1 -1
  57. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_compose_sources.py +1 -1
  58. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_config.py +7 -1
  59. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_core.py +13 -0
  60. taskuary-0.3.2.4/tests/test_demo.py +148 -0
  61. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_digest_brief.py +13 -4
  62. taskuary-0.3.2.4/tests/test_finished_chat_stays_finished.py +85 -0
  63. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_general.py +54 -0
  64. taskuary-0.3.2.4/tests/test_guard.py +104 -0
  65. taskuary-0.3.2.4/tests/test_handbook.py +120 -0
  66. taskuary-0.3.2.4/tests/test_handbook_topics.py +76 -0
  67. taskuary-0.3.2.4/tests/test_home_is_never_yours.py +70 -0
  68. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_kind_dispatch.py +23 -6
  69. taskuary-0.3.2.4/tests/test_meeting_prep.py +75 -0
  70. taskuary-0.3.2.4/tests/test_message_thread.py +93 -0
  71. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_noop_ending.py +12 -0
  72. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_not_coding.py +26 -6
  73. taskuary-0.3.2.4/tests/test_own_rows_no_reply.py +40 -0
  74. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_problems.py +1 -1
  75. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_reply_channels.py +3 -1
  76. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_research.py +1 -1
  77. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_reviews.py +12 -0
  78. taskuary-0.3.2.4/tests/test_seed_fits_a_tty.py +56 -0
  79. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_setup.py +29 -6
  80. taskuary-0.3.2.4/tests/test_stay_open.py +100 -0
  81. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_terminal.py +68 -5
  82. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_triage_dataset.py +10 -3
  83. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_verdict_paths.py +27 -25
  84. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_waitroom.py +11 -0
  85. taskuary-0.3.2.4/tests/test_whatsapp_documents.py +89 -0
  86. taskuary-0.3.2.4/tests/test_withdrawn.py +70 -0
  87. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_witness.py +2 -1
  88. taskuary-0.3.2.2/taskuary/web/assets/GeneralWorkspace-Bt02JQzu.js +0 -10
  89. taskuary-0.3.2.2/taskuary/web/assets/index-DGVnbkfG.js +0 -462
  90. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/LICENSE +0 -0
  91. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/setup.cfg +0 -0
  92. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/__init__.py +0 -0
  93. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/agents.py +0 -0
  94. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/aws.py +0 -0
  95. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/azure.py +0 -0
  96. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/blackboard.py +0 -0
  97. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/browserview.py +0 -0
  98. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/calendar.py +0 -0
  99. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/climodels.py +0 -0
  100. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/clis.py +0 -0
  101. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/context.py +0 -0
  102. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/counsel.py +0 -0
  103. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/desktop.py +0 -0
  104. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/devtools.py +0 -0
  105. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/digest.py +0 -0
  106. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/github.py +0 -0
  107. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/handraise.py +0 -0
  108. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/imapmail.py +0 -0
  109. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/imessage.py +0 -0
  110. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/intacct.py +0 -0
  111. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/interview.py +0 -0
  112. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/knowledge.py +0 -0
  113. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/learn.py +0 -0
  114. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/learnedgraph.py +0 -0
  115. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/logs.py +0 -0
  116. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/mcp.py +0 -0
  117. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/msauth.py +0 -0
  118. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/mssql.py +0 -0
  119. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/phone.py +0 -0
  120. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/pm.py +0 -0
  121. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/policy.py +0 -0
  122. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/promptmap.py +0 -0
  123. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/proof.py +0 -0
  124. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/proposals.py +0 -0
  125. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/rank.py +0 -0
  126. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/reshape.py +0 -0
  127. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/responder.py +0 -0
  128. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/semantic.py +0 -0
  129. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/senders.py +0 -0
  130. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/sharepoint.py +0 -0
  131. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/sheets.py +0 -0
  132. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/counsel.md +0 -0
  133. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/digest.md +0 -0
  134. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/learned.md +0 -0
  135. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/soul.md +0 -0
  136. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/style.md +0 -0
  137. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/testing.py +0 -0
  138. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/toil.py +0 -0
  139. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/verdicts.py +0 -0
  140. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/wabridge.py +0 -0
  141. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/web/assets/GeneralWorkspace-D46ydeAJ.css +0 -0
  142. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/web/assets/index-6GBZ9nXN.css +0 -0
  143. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/web/favicon.ico +0 -0
  144. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/web/favicon.png +0 -0
  145. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/webguard.py +0 -0
  146. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/whatsapp/package.json +0 -0
  147. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/whoami.py +0 -0
  148. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/witness.py +0 -0
  149. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary.egg-info/dependency_links.txt +0 -0
  150. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary.egg-info/entry_points.txt +0 -0
  151. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary.egg-info/requires.txt +0 -0
  152. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary.egg-info/top_level.txt +0 -0
  153. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_agent_environment.py +0 -0
  154. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_agent_source.py +0 -0
  155. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_aisetup.py +0 -0
  156. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_answers.py +0 -0
  157. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_assistant.py +0 -0
  158. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_async_triage.py +0 -0
  159. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_audit_chain.py +0 -0
  160. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_aws_region.py +0 -0
  161. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_blackboard.py +0 -0
  162. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_browser_session.py +0 -0
  163. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_browserview.py +0 -0
  164. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_calendar.py +0 -0
  165. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_calendar_today.py +0 -0
  166. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_categories.py +0 -0
  167. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_cc_triage.py +0 -0
  168. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_chat_queue.py +0 -0
  169. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_climodels.py +0 -0
  170. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_clis.py +0 -0
  171. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_cloud.py +0 -0
  172. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_cloud_sources.py +0 -0
  173. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_context.py +0 -0
  174. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_counsel.py +0 -0
  175. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_cwlogs_paging.py +0 -0
  176. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_desktop.py +0 -0
  177. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_devtools.py +0 -0
  178. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_diff_review.py +0 -0
  179. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_docs_and_llm.py +0 -0
  180. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_docs_flow.py +0 -0
  181. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_duplicate_connectors.py +0 -0
  182. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_factory.py +0 -0
  183. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_feed_working.py +0 -0
  184. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_github_auto.py +0 -0
  185. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_handraise.py +0 -0
  186. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_harvest.py +0 -0
  187. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_histgen.py +0 -0
  188. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_imapmail.py +0 -0
  189. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_imessage.py +0 -0
  190. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_intacct.py +0 -0
  191. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_knowledge.py +0 -0
  192. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_learnedgraph.py +0 -0
  193. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_local_file.py +0 -0
  194. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_mail_folders.py +0 -0
  195. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_mark_read.py +0 -0
  196. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_mcp.py +0 -0
  197. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_messengers.py +0 -0
  198. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_msauth.py +0 -0
  199. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_mssql.py +0 -0
  200. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_note_retrieval.py +0 -0
  201. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_outbound_reports.py +0 -0
  202. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_pm.py +0 -0
  203. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_poll_overlap.py +0 -0
  204. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_poll_sources.py +0 -0
  205. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_pr_comments.py +0 -0
  206. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_promptmap_and_catalog.py +0 -0
  207. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_proof_ci_proposals.py +0 -0
  208. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_pypi_downloads.py +0 -0
  209. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_rank.py +0 -0
  210. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_reply_voice.py +0 -0
  211. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_report_alerts.py +0 -0
  212. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_report_triage.py +0 -0
  213. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_scopes.py +0 -0
  214. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_seeded_reports.py +0 -0
  215. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_semantic.py +0 -0
  216. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_send_targets.py +0 -0
  217. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_sheets_sharepoint.py +0 -0
  218. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_someone_else_replied.py +0 -0
  219. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_soul_interview.py +0 -0
  220. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_store_app_cli.py +0 -0
  221. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_store_hotpaths.py +0 -0
  222. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_sync_clock.py +0 -0
  223. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_task_identity.py +0 -0
  224. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_toil.py +0 -0
  225. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_urgent_and_handoff.py +0 -0
  226. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_verdict_sticks.py +0 -0
  227. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_vision.py +0 -0
  228. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_voice.py +0 -0
  229. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_wabridge.py +0 -0
  230. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_watermark.py +0 -0
  231. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_web_entrypoint.py +0 -0
  232. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_webguard.py +0 -0
  233. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_whatsapp_images.py +0 -0
  234. {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_whoami.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskuary
3
- Version: 0.3.2.2
3
+ Version: 0.3.2.4
4
4
  Summary: Automate your job - the local-first operating system for your work: AI triage in, coding agents out.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/ldbumble/taskuary
@@ -64,10 +64,19 @@ and replies wait for your approval. Nothing sends or ships without you.
64
64
 
65
65
  ![The Taskuary Studio: work arrives, triage decides what needs action, and agents take seats to work it.](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/hero.gif)
66
66
 
67
- Taskuary is early—currently **v0.3.2.2**—and moving fast. The funnel, review queue,
67
+ Taskuary is early—currently **v0.3.2.4**—and moving fast. The funnel, review queue,
68
68
  agent sessions, and reports pipeline are in daily use; breaking changes are still possible
69
69
  before 1.0.
70
70
 
71
+ <p align="center">
72
+ <a href="https://taskuary.com/demo/"><img
73
+ src="https://img.shields.io/badge/%E2%96%B6%20Try%20it%20now-no%20install%2C%20in%20your%20browser-2f4858?style=for-the-badge&labelColor=1f2a22"
74
+ alt="Try Taskuary now, in your browser"></a>
75
+ </p>
76
+
77
+ <p align="center"><sub>The real app over invented work — timeline, triage, a coding session mid-run,
78
+ reports with charts. Nothing to install and nothing connects to anything.</sub></p>
79
+
71
80
  ## One timeline for incoming work
72
81
 
73
82
  Outlook, Gmail, Teams, Slack, Telegram, WhatsApp, GitHub, Jira, alerts, and reports all
@@ -141,6 +150,21 @@ On first run, connect an AI provider or local Ollama model, add at least one inb
141
150
  channel, then choose the coding CLI that should receive tasks. The setup wizards test each
142
151
  connection before it goes live.
143
152
 
153
+ ## Try it without installing anything
154
+
155
+ ```bash
156
+ taskuary --demo # or: docker compose --profile demo up
157
+ ```
158
+
159
+ A full Taskuary over invented work — a morning's mail and chats already triaged with the
160
+ reasons attached, drafts waiting in Review, agents on the board with their transcripts
161
+ playing, live task handoffs on the Board, and the durable agent-written handbook in Social.
162
+ Every Social entry names the agent and originating task; Board handoffs are short-lived
163
+ checkout coordination that rolls up nightly. The AI answers from a script and every door
164
+ to the outside world is shut at the API layer (`taskuary/demo.py`): nothing sends, no
165
+ connection can be made or edited, no tool runs, and no CLI starts. Nobody's real data is in
166
+ it — Dana Whitfield and Northwind Facilities are invented.
167
+
144
168
  ## Installs
145
169
 
146
170
  ![Daily installs of taskuary from PyPI, mirror traffic excluded](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/downloads.svg)
@@ -13,10 +13,19 @@ and replies wait for your approval. Nothing sends or ships without you.
13
13
 
14
14
  ![The Taskuary Studio: work arrives, triage decides what needs action, and agents take seats to work it.](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/hero.gif)
15
15
 
16
- Taskuary is early—currently **v0.3.2.2**—and moving fast. The funnel, review queue,
16
+ Taskuary is early—currently **v0.3.2.4**—and moving fast. The funnel, review queue,
17
17
  agent sessions, and reports pipeline are in daily use; breaking changes are still possible
18
18
  before 1.0.
19
19
 
20
+ <p align="center">
21
+ <a href="https://taskuary.com/demo/"><img
22
+ src="https://img.shields.io/badge/%E2%96%B6%20Try%20it%20now-no%20install%2C%20in%20your%20browser-2f4858?style=for-the-badge&labelColor=1f2a22"
23
+ alt="Try Taskuary now, in your browser"></a>
24
+ </p>
25
+
26
+ <p align="center"><sub>The real app over invented work — timeline, triage, a coding session mid-run,
27
+ reports with charts. Nothing to install and nothing connects to anything.</sub></p>
28
+
20
29
  ## One timeline for incoming work
21
30
 
22
31
  Outlook, Gmail, Teams, Slack, Telegram, WhatsApp, GitHub, Jira, alerts, and reports all
@@ -90,6 +99,21 @@ On first run, connect an AI provider or local Ollama model, add at least one inb
90
99
  channel, then choose the coding CLI that should receive tasks. The setup wizards test each
91
100
  connection before it goes live.
92
101
 
102
+ ## Try it without installing anything
103
+
104
+ ```bash
105
+ taskuary --demo # or: docker compose --profile demo up
106
+ ```
107
+
108
+ A full Taskuary over invented work — a morning's mail and chats already triaged with the
109
+ reasons attached, drafts waiting in Review, agents on the board with their transcripts
110
+ playing, live task handoffs on the Board, and the durable agent-written handbook in Social.
111
+ Every Social entry names the agent and originating task; Board handoffs are short-lived
112
+ checkout coordination that rolls up nightly. The AI answers from a script and every door
113
+ to the outside world is shut at the API layer (`taskuary/demo.py`): nothing sends, no
114
+ connection can be made or edited, no tool runs, and no CLI starts. Nobody's real data is in
115
+ it — Dana Whitfield and Northwind Facilities are invented.
116
+
93
117
  ## Installs
94
118
 
95
119
  ![Daily installs of taskuary from PyPI, mirror traffic excluded](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/downloads.svg)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "taskuary"
7
- version = "0.3.2.2"
7
+ version = "0.3.2.4"
8
8
  description = "Automate your job - the local-first operating system for your work: AI triage in, coding agents out."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -87,7 +87,7 @@ def start(store, server: dict, cid: int, guide: list, fields: list = None, secre
87
87
  if live: return {**live, 'existing': True}
88
88
  from . import agents as hub_agents
89
89
  agent = (agent or hub_agents.default_agent(store)).strip()
90
- if not store.get_agent(agent): raise ValueError(f'no CLI agent named {agent!r} - add one under Connectors > AI CLI agents first')
90
+ if not store.get_agent(agent): raise ValueError(f'no CLI agent named {agent!r} - add one under Connections > AI CLI agents first')
91
91
  tid = store.create_task({'Title': f"Set up {c.get('Name') or c.get('Type')}", 'Kind': KIND, 'Status': 'in_progress', 'Tags': tag(cid),
92
92
  'Summary': f"{agent} walks the owner through the {c.get('Type')} guide in a live session on the card and saves what they give it."}, actor)
93
93
  # no repo: the session sits in Taskuary's own data folder, where there is no checkout to attribute dirt to
@@ -9,7 +9,7 @@ Both writers are hand-rolled on the standard library on purpose: xlsx is a zip o
9
9
  XML parts, and a bar chart is a few dozen rects. Neither is worth a dependency that has to be
10
10
  frozen into the one-file exe.
11
11
  """
12
- import json, re, zipfile
12
+ import json, math, re, zipfile
13
13
  from datetime import datetime
14
14
  from xml.sax.saxutils import escape
15
15
 
@@ -145,6 +145,65 @@ def chart_columns(rows: list) -> tuple:
145
145
  return ((labels[0] if labels else None), (vals[0] if vals else None))
146
146
 
147
147
 
148
+ INK, GRID, MUTED, LINE = '#1f2430', '#e7e3da', '#8a94a6', '#4f46e5'
149
+
150
+ _DATE = re.compile(r'^\s*(\d{4}-\d{2}-\d{2}|\d{1,2}/\d{1,2}(/\d{2,4})?|\d{4}-\d{2}|[A-Z][a-z]{2} \d{1,2})')
151
+
152
+ def _dated(labels: list) -> bool:
153
+ """Is the label column a run of dates? A report over time is the one shape horizontal bars
154
+ get wrong - fourteen days of tickets became fourteen stacked bars, which reads as fourteen
155
+ unrelated things rather than one line going up."""
156
+ return len(labels) >= 5 and all(_DATE.match(str(l or '')) for l in labels)
157
+
158
+
159
+ def _ticks(top: float, n: int = 4) -> list:
160
+ """Round gridline values at or above the data, so the axis reads 0/50/100 and never 0/47/94."""
161
+ if top <= 0: return [0.0, 1.0]
162
+ step = 10.0 ** math.floor(math.log10(top / n))
163
+ for mult in (1, 2, 2.5, 5, 10):
164
+ if step * mult * n >= top: step *= mult; break
165
+ return [step * i for i in range(int(math.ceil(top / step)) + 1)]
166
+
167
+
168
+ def _line_svg(pts: list, title: str, val: str, lab: str) -> str:
169
+ """One measure over time: a 2px line, a light band under it, gridlines behind, and the last
170
+ value labelled where the eye already is. One series, so no legend - the title names it."""
171
+ W, H, L, R, T, B = 760, 260, 56, 74, 34, 34
172
+ ticks = _ticks(max(v for _l, v in pts))
173
+ top = ticks[-1] or 1.0
174
+ px = lambda i: L + (W - L - R) * (i / max(1, len(pts) - 1))
175
+ py = lambda v: H - B - (H - T - B) * (v / top)
176
+ pl = ' '.join(f'{px(i):.1f},{py(v):.1f}' for i, (_l, v) in enumerate(pts))
177
+ out = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}" '
178
+ f'font-family="Segoe UI, Helvetica, Arial, sans-serif">',
179
+ f'<rect width="{W}" height="{H}" fill="#ffffff"/>',
180
+ f'<text x="{L - 40}" y="20" font-size="13" font-weight="700" fill="{INK}">{escape(title or val)}</text>',
181
+ f'<text x="{W - 12}" y="20" font-size="10" fill="{MUTED}" text-anchor="end">'
182
+ f'{escape(val)} by {escape(lab or "row")} · {len(pts)} points</text>']
183
+ for t in ticks:
184
+ y = py(t)
185
+ out += [f'<line x1="{L}" y1="{y:.1f}" x2="{W - R}" y2="{y:.1f}" stroke="{GRID}" stroke-width="1"/>',
186
+ f'<text x="{L - 8}" y="{y + 3.5:.1f}" font-size="10" fill="{MUTED}" text-anchor="end">'
187
+ f'{escape(f"{t:,.10g}")}</text>']
188
+ out.append(f'<polygon points="{L},{H - B} {pl} {W - R},{H - B}" fill="{LINE}" fill-opacity="0.08"/>')
189
+ out.append(f'<polyline points="{pl}" fill="none" stroke="{LINE}" stroke-width="2" '
190
+ f'stroke-linejoin="round" stroke-linecap="round"/>')
191
+ # every point is a target, but only the ends are named - a number on all fourteen is noise
192
+ for i, (_l, v) in enumerate(pts):
193
+ out.append(f'<circle cx="{px(i):.1f}" cy="{py(v):.1f}" r="{4 if i in (0, len(pts) - 1) else 2.4}" '
194
+ f'fill="{LINE}" stroke="#ffffff" stroke-width="1.5"/>')
195
+ lv = pts[-1][1]
196
+ out.append(f'<text x="{W - R + 8}" y="{py(lv) + 4:.1f}" font-size="11.5" font-weight="700" fill="{INK}">'
197
+ f'{escape(f"{lv:,.2f}".rstrip("0").rstrip("."))}</text>')
198
+ every = max(1, len(pts) // 7)
199
+ for i, (l, _v) in enumerate(pts):
200
+ if i % every and i != len(pts) - 1: continue
201
+ out.append(f'<text x="{px(i):.1f}" y="{H - B + 16}" font-size="10" fill="{MUTED}" '
202
+ f'text-anchor="middle">{escape(str(l)[-5:])}</text>')
203
+ out.append('</svg>')
204
+ return ''.join(out)
205
+
206
+
148
207
  # Taskuary's own indigo, and a chart that reads in both the panel and a screenshot of it.
149
208
  def to_svg_chart(rows: list, path, title: str = '', want_val: str = None, want_lab: str = None) -> str:
150
209
  """Horizontal bars: label, bar, value. Horizontal because report labels are names and dates,
@@ -159,6 +218,10 @@ def to_svg_chart(rows: list, path, title: str = '', want_val: str = None, want_l
159
218
  pts = [(str(r.get(lab, ''))[:42] if lab else f'#{i + 1}', _num(r.get(val)) or 0.0)
160
219
  for i, r in enumerate(rows)][:MAX_BARS]
161
220
  if not pts: return ''
221
+ if _dated([l for l, _v in pts]):
222
+ svg = _line_svg(pts, title or val, val, lab or 'row')
223
+ if path is not None: path.write_text(svg, encoding='utf-8')
224
+ return svg if path is None else f'{val} by {lab or "row"}'
162
225
  top = max(abs(v) for _l, v in pts) or 1.0
163
226
  W, LW, RH, PAD = 760, 210, 22, 18
164
227
  H = PAD * 2 + 26 + RH * len(pts)
@@ -301,6 +301,10 @@ CONTRACT = ('\n\nYou are writing your POST on the owner\'s Timeline - the short
301
301
  'CALENDAR, what arrived (with each report\'s schedule and each failure\'s cause), what got done, what is open, and WHAT '
302
302
  'YOU ALREADY SAID. Answer JSON only: {"say": [{"key": "<a candidate key, or idea:<short-slug> for a thought of your own>", '
303
303
  '"text": "<one line, under 30 words, first person: the fact and what I would do - quote the phrase or name the cause when there is one>", '
304
+ '"section": "<people|loose|ideas|systems - which part of the post this belongs under: people = what somebody said '
305
+ 'or asked, loose = something waiting on somebody (a chase, a promise, work gone quiet), systems = a threshold, a '
306
+ 'failure or a number out of a connected system, ideas = your own thought. A candidate the hub found has a section '
307
+ 'already and yours is ignored for those; this is for idea:* lines>", '
304
308
  '"why": "<one line: what this rests on - the mail, the date, the silence, the pattern - named as it appears in what you '
305
309
  'were given (sender, subject, mid, TQ-ref), so the owner can check it>", "mid": <the message id it is '
306
310
  'about, or null>, "task": "<idea:* only - a task title the owner could accept as-is, or null>"}], '
@@ -495,6 +499,9 @@ def parse(text: str, cands: list, max_lines: int = MAX_LINES) -> list:
495
499
  mid = s.get('mid') if isinstance(s.get('mid'), int) else None
496
500
  title = _short(s.get('task'), 120) or None
497
501
  act = {'type': 'task', 'mid': mid, 'title': title} if title and mid else {'type': 'message', 'mid': mid} if mid else {'type': 'note'}
502
+ # where in the post it goes. Only an idea gets to choose: a candidate the hub found is
503
+ # placed by the producer that found it, and no model answer overrides that.
504
+ act['section'] = section_of({'section': s.get('section'), 'kind': 'idea'})
498
505
  out.append({'key': key[:120], 'kind': 'idea', 'sig': txt[:60], 'text': txt, 'action': act,
499
506
  'why': why or 'the model gave no reason - treat it as a hunch' + (f' (about mid {mid})' if mid else '')})
500
507
  else: continue
@@ -575,6 +582,43 @@ def system_checks(store, source_ids=None, inline=None) -> str:
575
582
  return '\n\n'.join(blocks)
576
583
 
577
584
 
585
+ def _verdicts_block(store, cands: list, source: str = "") -> str:
586
+ """The owner's standing verdicts - the SAME memory triage reads before it classifies
587
+ (ingest.relevant_notes / applicable_notes).
588
+
589
+ The assistant was the one brain in this app that never saw them. So "resident refunds are not
590
+ our problem" stayed true for triage - which quietly files them - and was news to the brief,
591
+ which went on raising the next refund thread as an idea worth acting on. A verdict the owner
592
+ gives once should not have to be given again per surface.
593
+
594
+ Scoped by the words of the material this check is READING, so a note about a topic nothing
595
+ today touches costs nothing; global notes always apply.
596
+
597
+ `source` is what the model was handed - the threads and the sender/subject lines - and it
598
+ matters more here than the candidates do. A subject-scoped verdict is matched (ingest.topic_hit)
599
+ against the words it was learned from, "resident refund request approved"; a candidate's
600
+ `facts` is the MODEL'S PARAPHRASE of a thread, and "Barnes and Watson stall the same way"
601
+ carries not one of those four words. So the ruling missed and the brief went on raising a
602
+ subject the owner had closed. Match the source it summarised, not the summary."""
603
+ from .ingest import relevant_notes
604
+ text = ' '.join([source or '', *(str(c.get('facts') or c.get('text') or '') for c in cands)])[:8000]
605
+ if not text.strip():
606
+ return ''
607
+ try:
608
+ emails = sorted({(r.get('FromEmail') or '').lower()
609
+ for r in store.recent_messages(_since(2), limit=500) if r.get('FromEmail')})
610
+ notes_, left = relevant_notes(store, emails, text)
611
+ except Exception as e:
612
+ logger.debug(f'assistant: standing verdicts skipped - {e}')
613
+ return ''
614
+ if not notes_:
615
+ return ''
616
+ more = f' ({left} more matched and were left out)' if left else ''
617
+ return ('\n\nWHAT THE OWNER HAS ALREADY DECIDED - their standing verdicts, the same ones triage '
618
+ f'reads before it files anything{more}. Never raise something they have ruled out, and '
619
+ 'never argue with one:\n' + '\n'.join(f'- {n}' for n in notes_))
620
+
621
+
578
622
  def inputs(store, cands: list, head: str = 'CANDIDATES', watch_source_ids=None, watch_sources=None) -> str:
579
623
  """Everything one check reads, as the model sees it - the same text is the Reports tab's Preview
580
624
  (facts) and the run record (reports.run_report_source), so what it was given is never a guess."""
@@ -582,15 +626,20 @@ def inputs(store, cands: list, head: str = 'CANDIDATES', watch_source_ids=None,
582
626
  away = ooo(store)
583
627
  from . import knowledge
584
628
  facts_text = ' '.join(str(c.get('facts') or '') for c in cands)[:4000]
629
+ # built once: the model reads them, and so does the verdict matcher, which needs the real
630
+ # subjects rather than the model's words about them
631
+ said, recent = _people(store), _recent(store)
585
632
  return (f"NOW: {now.strftime('%A %d %B %Y %H:%M')}\n\n{head}:\n" + ('\n'.join(f"[{c['key']}] {c['facts']}" for c in cands) or '(none)')
586
633
  + knowledge.block(store, facts_text)
587
634
  + f"\n\nCONFIGURED SYSTEM CHECKS (pulled live for this check; failures are also worth noticing):\n{system_checks(store, watch_source_ids, watch_sources)}"
588
- + f"\n\nWHAT PEOPLE SAID (the last two days, by thread, newest first; the last lines of each, oldest first):\n{_people(store)}"
635
+ + f"\n\nWHAT PEOPLE SAID (the last two days, by thread, newest first; the last lines of each, oldest first):\n{said}"
589
636
  + '\n\nOUT OF OFFICE (from their auto-replies):\n' + ('\n'.join(f'- {k}: {v}' for k, v in away.items()) or '(nobody)')
590
637
  + f"\n\nCALENDAR (the next two days):\n{_calendar(store)}"
591
- + f"\n\nARRIVED IN THE LAST TWO DAYS (xN = that many alike; each line carries the latest message's words, a report's schedule, and a failure's cause):\n{_recent(store)}"
638
+ + f"\n\nARRIVED IN THE LAST TWO DAYS (xN = that many alike; each line carries the latest message's words, a report's schedule, and a failure's cause):\n{recent}"
592
639
  + f"\n\nDONE THIS WEEK (my own work, with the agent's summary):\n{_week(store)}"
593
- + f"\n\nOPEN WORK:\n{_open(store)}\n\nALREADY SAID (never repeat):\n{_said(store)}\n\n{_notes_block(store)}")
640
+ + f"\n\nOPEN WORK:\n{_open(store)}\n\nALREADY SAID (never repeat):\n{_said(store)}"
641
+ + _verdicts_block(store, cands, f'{said}\n{recent}')
642
+ + f"\n\n{_notes_block(store)}")
594
643
 
595
644
 
596
645
  def think(store, cands: list, llm, instruction: str = None, max_lines: int = MAX_LINES) -> list:
@@ -633,10 +682,81 @@ def _notes(text: str) -> str:
633
682
 
634
683
 
635
684
  # ── the post ─────────────────────────────────────────────────────────────────────────────────
685
+ # ── the post's shape ────────────────────────────────────────────────────────────────────
686
+ # A flat list of lines is what the post used to be, and the owner (2026-09-01): "it should
687
+ # summarize into sections... summary of what the info emails said to you, then open tasks and
688
+ # what they are working on, then things you forgot to follow up from last week, then some stats".
689
+ # The lines themselves stay exactly as they were - each one still carries its own buttons and its
690
+ # own state, which is the whole value of them - they are just SORTED into sections that say what
691
+ # kind of thing you are looking at.
692
+ #
693
+ # Two of the sections are not the model's work at all. What is in flight and what the day counted
694
+ # are FACTS the hub already holds, and asking a model to restate them is how a brief starts
695
+ # inventing a task that is not there (the digest did exactly that: TQ-0032 "pending review" when
696
+ # nothing was pending). So they are computed here and the model never sees them as its own output.
697
+ SECTIONS = (
698
+ ('people', '📥', 'What people said', 'the ask in a thread, the thing somebody told you, the answer nobody picked up'),
699
+ ('flight', '🚀', 'In flight', 'what an agent has, what closed, what has gone quiet'),
700
+ ('loose', '🧵', 'Loose ends', 'what you are waiting on, what you promised, what went cold'),
701
+ ('ideas', '💡', 'Worth a thought', 'the connection nobody made, the thing to do now so the next ask never comes'),
702
+ ('systems', '🛠️', 'From the systems', 'a threshold crossed, a job failing the same way twice, a number that moved'),
703
+ )
704
+ SECTION_KEYS = tuple(s[0] for s in SECTIONS)
705
+ # where a candidate lands when the model does not say (and it never says for hub-found ones)
706
+ KIND_SECTION = {'followup': 'loose', 'promise': 'loose', 'cold': 'loose', 'prep': 'people', 'idea': 'ideas'}
707
+
708
+
709
+ def section_of(line: dict) -> str:
710
+ """Which section one line belongs in. The model's own choice wins for its ideas; a candidate
711
+ the hub found is placed by its kind, which is the thing that produced it."""
712
+ s = str(line.get('section') or '').strip().lower()
713
+ if s in SECTION_KEYS: return s
714
+ return KIND_SECTION.get(str(line.get('kind') or ''), 'ideas')
715
+
716
+
717
+ def in_flight(store) -> list:
718
+ """What is being worked, straight off the tasks - never the model's recollection of it.
719
+ [{ref, tid, title, state}] newest activity first, the ones an agent has at the top."""
720
+ from . import terminal as term
721
+ live = {}
722
+ try: live = {t['taskId']: (t.get('agent') or t.get('label') or 'an agent') for t in term.live_sessions(tail=0) if t.get('taskId')}
723
+ except Exception: pass
724
+ out = []
725
+ for t in store.list_tasks(active_only=True):
726
+ if t.get('Status') not in ('open', 'in_progress', 'waiting'): continue
727
+ tid = t['TaskId']
728
+ agent = live.get(tid) or (t.get('RunAgent') if t.get('RunStatus') == 'running' else None)
729
+ last = _dt(store.task_last_activity(tid) or t.get('UpdatedAt') or t.get('CreatedAt'))
730
+ quiet_h = int((datetime.now() - last).total_seconds() // 3600) if last else None
731
+ state = (f'{agent} has it' if agent
732
+ else 'a reply is drafted and waiting on you' if t.get('ReviewStatus') == 'pending'
733
+ else f'quiet for {quiet_h}h' if quiet_h and quiet_h >= 24
734
+ else 'nobody is on it')
735
+ out.append({'tid': tid, 'ref': task_ref(tid), 'title': _short(t.get('Title'), 90),
736
+ 'kind': t.get('Kind') or '', 'agent': agent or '', 'state': state,
737
+ 'hot': bool(agent) or t.get('ReviewStatus') == 'pending'})
738
+ out.sort(key=lambda r: (not r['hot'],))
739
+ return out[:8]
740
+
741
+
742
+ def day_stats(store) -> list:
743
+ """The few numbers that mean something, counted - not asked for. [{n, label}]."""
744
+ from .categories import category_of, team_domains_of
745
+ team = team_domains_of(store.get_settings())
746
+ rows = store.feed(400, 1)
747
+ cats = [category_of(r, team) for r in rows]
748
+ tasks = [t for t in store.list_tasks(active_only=True) if t.get('Status') in ('open', 'in_progress', 'waiting')]
749
+ return [{'n': len(rows), 'label': 'arrived today'},
750
+ {'n': sum(1 for c in cats if c in ('info', 'automated', 'promo', 'feed', 'report')), 'label': 'wanted nothing'},
751
+ {'n': len(tasks), 'label': 'open'},
752
+ {'n': sum(1 for t in tasks if t.get('ReviewStatus') == 'pending'), 'label': 'waiting on you', 'hot': True}]
753
+
754
+
636
755
  def _public(i: dict) -> dict:
637
756
  try: a = json.loads(i.get('ActionJson') or '{}')
638
757
  except ValueError: a = {}
639
- return {'id': i['IdeaId'], 'key': i['Key'], 'kind': i['Kind'], 'text': i['Text'], 'why': a.pop('why', ''), 'action': a, 'status': i.get('Status')}
758
+ return {'id': i['IdeaId'], 'key': i['Key'], 'kind': i['Kind'], 'text': i['Text'], 'why': a.pop('why', ''), 'action': a, 'status': i.get('Status'),
759
+ 'section': section_of({'section': a.get('section'), 'kind': i['Kind']})}
640
760
 
641
761
 
642
762
  def talk(store, idea_id: int, text: str, actor: str = 'owner', llm=None) -> dict:
@@ -791,7 +911,11 @@ def _run(store, llm, instruction) -> dict:
791
911
  'BodyText': body, 'Status': 'feed'})
792
912
  store.add_route(mid, None, 'feed', None, "the assistant's post: what it noticed and what it would do - open it to talk back or act",
793
913
  [], 'assistant')
794
- store.set_brief(mid, json.dumps({'ideas': [_public(i) for i in rows], 'reviewed': rv}))
914
+ # the two blocks the model does not write. What is in flight and what the day counted are facts
915
+ # the hub already holds; asking a model to restate them is how a brief starts describing work
916
+ # that is not there. Snapshotted onto the post so it still reads correctly tomorrow.
917
+ store.set_brief(mid, json.dumps({'ideas': [_public(i) for i in rows], 'reviewed': rv,
918
+ 'flight': in_flight(store), 'stats': day_stats(store)}))
795
919
  store.set_ideas_message([i['IdeaId'] for i in rows], mid)
796
920
  store.audit('message', mid, 'assistant_post', 'assistant', 'agent', {'ideas': len(rows)})
797
921
  logger.info(f'assistant: posted {len(rows)} idea(s) as message {mid}')
@@ -8,8 +8,9 @@ means the same thing in the feed, the digest and the task page:
8
8
  review - a reply is drafted for you to send info - a PERSON told you something; nothing to do
9
9
  automated - a system told you something promo - marketing / newsletter; safe to skim past
10
10
  filed - kept, no verdict beyond "not work" ignored - a policy or you said no
11
- report / feed / yours / triaging - not verdicts: a scheduled report, a feed-only
12
- connection, your own reply, or still deciding
11
+ report / feed / yours / mine / triaging - not verdicts: a scheduled report, a feed-only
12
+ connection, your own reply, a note you left
13
+ yourself, or still deciding
13
14
 
14
15
  The info / automated / promo split is the point (owner, 2026-08-27): "info from a team member
15
16
  is more important than an FYI from a vendor's marketing team". Both are 'fyi' to the
@@ -55,6 +56,10 @@ def category_of(r: dict, team_domains=()) -> str:
55
56
  st, reason = r.get('MsgStatus') or r.get('Status') or '', (r.get('RouteReason') or '').lower()
56
57
  if r.get('Channel') == 'report': return 'report'
57
58
  if r.get('Channel') == 'assistant': return 'assistant' # the assistant's own post (assistant.py)
59
+ # a row nothing sent you: a note you left yourself, or a task you started from the Board
60
+ # (ownwork.py). It is the one kind of row that is yours rather than something that happened
61
+ # to you, and it must never wear a verdict - nothing judged it, because nothing arrived.
62
+ if r.get('Channel') == 'own': return 'mine' if (r.get('TaskKind') or '') == 'note' else 'todo'
58
63
  if r.get('Direction') == 'out' or 'your reply' in reason or 'your sent reply' in reason: return 'yours'
59
64
  if st == 'feed': return 'feed'
60
65
  if st == 'triaging': return 'triaging'
@@ -1,4 +1,4 @@
1
- """Channel connectors - the cards on the Connectors tab: Outlook mail + Microsoft Teams
1
+ """Channel connectors - the cards on the Connections tab: Outlook mail + Microsoft Teams
2
2
  (Graph, app-only client credentials) and GitHub (fine-grained PAT). test_connector is a
3
3
  live probe (token/chat-read/repo-discovery); poll_channels is the scheduled ingest that
4
4
  funnels mail and chats through the same triage as everything else. Credentials left blank
@@ -568,7 +568,15 @@ def ingest_teams_chats(store, upn: str, tok: str, since, llm=None, file_only=Fal
568
568
  for m in reversed(_teams_delta(tok, upn, since_iso)): # oldest first, so threads read in order
569
569
  user = (m.get('from') or {}).get('user') or {}
570
570
  body = _clean((m.get('body') or {}).get('content'))
571
- if m.get('messageType') != 'message' or m.get('deletedDateTime') or not user.get('id') or not body: continue
571
+ # Deleted at the source. Teams' delta reports it (mail's plain $top list does not, so
572
+ # the mailbox side of this needs a delta migration before it can say the same). If we
573
+ # already carry the row, say so on it rather than leaving a message on the Timeline that
574
+ # no longer exists in the chat - and never remove it: work may hang off it.
575
+ if m.get('deletedDateTime'):
576
+ cid_d = m.get('chatId') or ''
577
+ if cid_d: store.withdraw_message(f'teams:{cid_d}:{m["id"]}')
578
+ continue
579
+ if m.get('messageType') != 'message' or not user.get('id') or not body: continue
572
580
  cid = m.get('chatId') or ''
573
581
  topic, kind = _chat_meta(tok, cid, chats) if cid else ('', 'chat')
574
582
  name, addr = _graph_user(tok, user['id'], users)
@@ -655,7 +663,7 @@ def gh_modes(src: dict, file_only: bool) -> tuple:
655
663
  # GitHub's own author_association. Everyone else's items still become tasks (in 'tasks' mode)
656
664
  # and wait for the owner to promote them. Default off: a public repo would otherwise start an
657
665
  # agent per drive-by PR (the session cap still holds, but a queue full of strangers is not
658
- # what the cap is for). The Connectors card warns before switching a PUBLIC repo on.
666
+ # what the cap is for). The Connections card warns before switching a PUBLIC repo on.
659
667
  GH_TEAM = ('OWNER', 'MEMBER', 'COLLABORATOR')
660
668
  GH_AUTO = {'off': (), 'team': GH_TEAM, 'contributors': GH_TEAM + ('CONTRIBUTOR',), 'anyone': None}
661
669
 
@@ -24,7 +24,7 @@ TOKEN_URL = re.compile(r'https://[^@/]*@') # never let a PAT reach a log lin
24
24
 
25
25
  def _conn(store):
26
26
  c = store.get_connector_by_type('github', with_secret=True)
27
- if not (c and c.get('Secret')): raise RuntimeError('no GitHub PAT saved (Connectors → GitHub)')
27
+ if not (c and c.get('Secret')): raise RuntimeError('no GitHub PAT saved (Connections → GitHub)')
28
28
  return c
29
29
 
30
30
 
@@ -46,6 +46,11 @@ def main():
46
46
  # THE WALL (blackboard.py). An agent working in a terminal has a shell and no API token, so
47
47
  # this is how it talks to the next agent: two flags, no arguments it has to be told - the
48
48
  # session puts its own name, task and checkout in the environment.
49
+ # try-it-out in one command: a throwaway home, a world of invented work, and every door
50
+ # to the outside nailed shut (demo.py). This is what /demo on the website runs.
51
+ ap.add_argument('--demo', action='store_true',
52
+ help='run a DEMO instance: invented data, a scripted AI, replayed coding '
53
+ 'sessions, and nothing that can reach a real system')
49
54
  ap.add_argument('--board', action='store_true',
50
55
  help='print the agent wall for this checkout - what the agents before you left here')
51
56
  ap.add_argument('--all', action='store_true',
@@ -55,8 +60,65 @@ def main():
55
60
  ap.add_argument('--kind', default='note', metavar='KIND',
56
61
  help='working | note | blocked | ready | done - "ready" is how the next agent '
57
62
  'learns the tree is safe to build on')
63
+ # ...and how a session ENDS itself. The Done button was the only way a finished task ever
64
+ # produced its report and its reply, so an agent that finished at 2am and a person who never
65
+ # opened the tab left the sender with nothing (selfclose.py).
66
+ # THE HANDBOOK (handbook.py). The wall is what the next hour needs; this is what next month
67
+ # needs - and it is the difference between a company whose know-how lives in its people and
68
+ # one a new agent can be plugged into.
69
+ ap.add_argument('--learned', metavar='TEXT',
70
+ help='write one line into the company handbook: something still true next month '
71
+ '(a trap, how a system actually works, who owns what). NOT what you did - that '
72
+ 'is the task. See --topic and --body.')
73
+ ap.add_argument('--topic', default='', metavar='TOPIC',
74
+ help='with --learned: which shelf it goes on - a repository, a system, a part of '
75
+ 'the business. Defaults to the checkout you are in.')
76
+ ap.add_argument('--body', default='', metavar='TEXT',
77
+ help='with --learned: the two or three sentences under the title - the fact, why it '
78
+ 'is so, and what to do about it')
79
+ ap.add_argument('--done', metavar='SUMMARY', nargs='?', const='',
80
+ help='finish the task this session is working: close it, file the report from '
81
+ 'this transcript, and draft the reply the sender gets (you are not '
82
+ 'sending it - the owner approves it). Give one sentence on what you did '
83
+ 'or found. Do not run this while waiting on the owner.')
58
84
  args = ap.parse_args()
59
- if args.board or args.note:
85
+ if args.demo:
86
+ import os, tempfile
87
+ from . import demo
88
+ os.environ[demo.FLAG] = '1'
89
+ # a throwaway home, so a demo can never be pointed at somebody's real database by accident
90
+ os.environ.setdefault('TASKUARY_HOME', tempfile.mkdtemp(prefix='taskuary-demo-'))
91
+ print(f'demo mode: invented data in {os.environ["TASKUARY_HOME"]}, nothing can reach a real system')
92
+ # --done goes over HTTP, unlike --note. A note is a database row and any process can write
93
+ # one; ENDING a task needs the live session's scrollback, which exists only inside the
94
+ # running server - this process would find no transcript and wrap an empty one.
95
+ if args.done is not None:
96
+ import os, sys, requests
97
+ try: sys.stdout.reconfigure(encoding='utf-8', errors='replace')
98
+ except (AttributeError, OSError): pass
99
+ tid = os.environ.get('TASKUARY_TASK')
100
+ if not str(tid).isdigit():
101
+ print('not in a Taskuary session - TASKUARY_TASK is not set, so there is no task to finish')
102
+ return
103
+ srv = config.load()['server']
104
+ host = '127.0.0.1' if srv.get('host') in ('0.0.0.0', '::', '', None) else srv['host']
105
+ base = f"http://{host}:{srv.get('port') or 7787}"
106
+ hdr = {'X-Taskuary-Token': srv['token']} if srv.get('token') else {}
107
+ try:
108
+ r = requests.post(f'{base}/api/agent/done', timeout=120, headers=hdr,
109
+ json={'task_id': int(tid), 'summary': args.done,
110
+ 'agent': os.environ.get('TASKUARY_AGENT') or 'agent'})
111
+ out = r.json() if r.headers.get('content-type', '').startswith('application/json') else {}
112
+ except Exception as e:
113
+ print(f'could not reach Taskuary at {base}: {e}'); return
114
+ if out.get('closed'):
115
+ print('task closed. Report filed from this session.'
116
+ + (' A reply to the sender is drafted and waiting on the owner.' if out.get('drafting')
117
+ else ' No reply was needed.'))
118
+ else:
119
+ print(f"not closed: {out.get('why') or out.get('detail') or r.text[:200]}")
120
+ return
121
+ if args.board or args.note or args.learned:
60
122
  import os, sys
61
123
  from . import blackboard as bb
62
124
  from .store import SQLiteStore, task_ref
@@ -66,6 +128,13 @@ def main():
66
128
  cwd = os.environ.get('TASKUARY_CWD') or os.getcwd()
67
129
  who = os.environ.get('TASKUARY_AGENT') or 'agent'
68
130
  tid = os.environ.get('TASKUARY_TASK')
131
+ if args.learned:
132
+ from . import handbook
133
+ try: p = handbook.post(store, args.learned, args.body, args.topic, args.kind if args.kind in handbook.KINDS else 'howto',
134
+ who, int(tid) if str(tid).isdigit() else None, cwd)
135
+ except ValueError as e: print(f'not filed: {e}'); return
136
+ print(f"filed in the handbook under {p['Topic']}: {p['Title']}")
137
+ return
69
138
  if args.note:
70
139
  try: n = bb.post(store, args.note, args.kind, who, cwd, int(tid) if str(tid).isdigit() else None)
71
140
  except ValueError as e: print(f'not posted: {e}'); return