taskuary 0.3.3.0__tar.gz → 0.3.3.2__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 (285) hide show
  1. {taskuary-0.3.3.0/taskuary.egg-info → taskuary-0.3.3.2}/PKG-INFO +61 -57
  2. taskuary-0.3.3.2/README.md +140 -0
  3. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/pyproject.toml +1 -1
  4. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/concierge.py +27 -8
  5. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/funnel.py +86 -20
  6. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/ingest.py +23 -0
  7. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/reports.py +8 -1
  8. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/server.py +17 -2
  9. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/coder.md +9 -3
  10. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/triage.md +2 -0
  11. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/terminal.py +74 -4
  12. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/triage.py +4 -0
  13. taskuary-0.3.3.0/taskuary/web/assets/GeneralWorkspace-Duk0uAEw.js → taskuary-0.3.3.2/taskuary/web/assets/GeneralWorkspace-C75zMBzq.js +1 -1
  14. taskuary-0.3.3.0/taskuary/web/assets/HubView-DAcJFCZG.js → taskuary-0.3.3.2/taskuary/web/assets/HubView-BtNf0bG5.js +1 -1
  15. taskuary-0.3.3.2/taskuary/web/assets/index-CE8to-uH.js +1140 -0
  16. taskuary-0.3.3.2/taskuary/web/assets/index-C_xWyjgP.css +32 -0
  17. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/web/index.html +2 -2
  18. {taskuary-0.3.3.0 → taskuary-0.3.3.2/taskuary.egg-info}/PKG-INFO +61 -57
  19. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary.egg-info/SOURCES.txt +4 -4
  20. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_assistant_reactions.py +128 -2
  21. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_digest_brief.py +10 -2
  22. taskuary-0.3.3.0/README.md +0 -136
  23. taskuary-0.3.3.0/taskuary/web/assets/index-DGWFy6LS.js +0 -1128
  24. taskuary-0.3.3.0/taskuary/web/assets/index-DYyvp4cD.css +0 -32
  25. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/LICENSE +0 -0
  26. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/setup.cfg +0 -0
  27. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/__init__.py +0 -0
  28. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/agents.py +0 -0
  29. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/aisetup.py +0 -0
  30. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/artifacts.py +0 -0
  31. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/assistant.py +0 -0
  32. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/aws.py +0 -0
  33. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/azure.py +0 -0
  34. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/blackboard.py +0 -0
  35. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/browserview.py +0 -0
  36. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/calendar.py +0 -0
  37. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/categories.py +0 -0
  38. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/channels.py +0 -0
  39. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/ci.py +0 -0
  40. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/cli.py +0 -0
  41. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/climodels.py +0 -0
  42. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/clis.py +0 -0
  43. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/coder.py +0 -0
  44. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/compose.py +0 -0
  45. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/config.py +0 -0
  46. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/context.py +0 -0
  47. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/counsel.py +0 -0
  48. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/db.py +0 -0
  49. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/demo.py +0 -0
  50. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/deps.py +0 -0
  51. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/desktop.py +0 -0
  52. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/devtools.py +0 -0
  53. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/digest.py +0 -0
  54. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/docsync.py +0 -0
  55. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/evalset.py +0 -0
  56. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/evening.py +0 -0
  57. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/general.py +0 -0
  58. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/github.py +0 -0
  59. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/guard.py +0 -0
  60. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/handbook.py +0 -0
  61. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/handraise.py +0 -0
  62. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/histgen.py +0 -0
  63. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/hooks.py +0 -0
  64. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/hub.py +0 -0
  65. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/imapmail.py +0 -0
  66. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/imessage.py +0 -0
  67. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/intacct.py +0 -0
  68. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/interview.py +0 -0
  69. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/invoice_workflow.py +0 -0
  70. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/knowledge.py +0 -0
  71. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/learn.py +0 -0
  72. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/learnedgraph.py +0 -0
  73. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/live.py +0 -0
  74. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/llm.py +0 -0
  75. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/logs.py +0 -0
  76. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/mcp.py +0 -0
  77. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/messengers.py +0 -0
  78. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/msauth.py +0 -0
  79. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/mssql.py +0 -0
  80. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/outbound.py +0 -0
  81. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/outbox.py +0 -0
  82. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/ownwork.py +0 -0
  83. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/phone.py +0 -0
  84. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/playbooks.py +0 -0
  85. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/pm.py +0 -0
  86. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/policy.py +0 -0
  87. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/problems.py +0 -0
  88. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/projects.py +0 -0
  89. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/promptmap.py +0 -0
  90. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/proof.py +0 -0
  91. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/proposals.py +0 -0
  92. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/quickbooks.py +0 -0
  93. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/rank.py +0 -0
  94. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/remote_assistant.py +0 -0
  95. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/research.py +0 -0
  96. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/reshape.py +0 -0
  97. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/responder.py +0 -0
  98. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/routing.py +0 -0
  99. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/scopes.py +0 -0
  100. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/selfclose.py +0 -0
  101. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/semantic.py +0 -0
  102. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/senders.py +0 -0
  103. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/session_artifacts.py +0 -0
  104. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/setup.py +0 -0
  105. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/sharepoint.py +0 -0
  106. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/sheets.py +0 -0
  107. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/skills/soul-interview/SKILL.md +0 -0
  108. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/spawn.py +0 -0
  109. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/store.py +0 -0
  110. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/teller.py +0 -0
  111. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/counsel.md +0 -0
  112. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/digest.md +0 -0
  113. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/how-it-works.md +0 -0
  114. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/learned.md +0 -0
  115. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/playbook.md +0 -0
  116. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/soul.md +0 -0
  117. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/style.md +0 -0
  118. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/testing.py +0 -0
  119. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/toil.py +0 -0
  120. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/update.py +0 -0
  121. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/verdicts.py +0 -0
  122. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/voice.py +0 -0
  123. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/wabridge.py +0 -0
  124. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/waitroom.py +0 -0
  125. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/web/assets/GeneralWorkspace--IqTbJGE.css +0 -0
  126. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/web/favicon.ico +0 -0
  127. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/web/favicon.png +0 -0
  128. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/webguard.py +0 -0
  129. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/whatsapp/bridge.mjs +0 -0
  130. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/whatsapp/package.json +0 -0
  131. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/whoami.py +0 -0
  132. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/witness.py +0 -0
  133. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/zoho.py +0 -0
  134. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary.egg-info/dependency_links.txt +0 -0
  135. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary.egg-info/entry_points.txt +0 -0
  136. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary.egg-info/requires.txt +0 -0
  137. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary.egg-info/top_level.txt +0 -0
  138. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_agent_environment.py +0 -0
  139. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_agent_source.py +0 -0
  140. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_agent_wall.py +0 -0
  141. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_aisetup.py +0 -0
  142. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_answered_yourself.py +0 -0
  143. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_answers.py +0 -0
  144. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_api.py +0 -0
  145. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_assistant.py +0 -0
  146. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_assistant_opens.py +0 -0
  147. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_assistant_verdicts.py +0 -0
  148. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_async_triage.py +0 -0
  149. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_audit_chain.py +0 -0
  150. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_audit_fixes.py +0 -0
  151. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_aws_region.py +0 -0
  152. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_blackboard.py +0 -0
  153. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_browser_session.py +0 -0
  154. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_browserview.py +0 -0
  155. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_calendar.py +0 -0
  156. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_calendar_today.py +0 -0
  157. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_categories.py +0 -0
  158. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_cc.py +0 -0
  159. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_cc_triage.py +0 -0
  160. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_chat_doors.py +0 -0
  161. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_chat_is_not_one_task.py +0 -0
  162. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_chat_queue.py +0 -0
  163. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_climodels.py +0 -0
  164. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_clis.py +0 -0
  165. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_cloud.py +0 -0
  166. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_cloud_sources.py +0 -0
  167. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_compose.py +0 -0
  168. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_compose_sources.py +0 -0
  169. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_compose_workflows.py +0 -0
  170. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_concierge.py +0 -0
  171. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_config.py +0 -0
  172. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_context.py +0 -0
  173. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_core.py +0 -0
  174. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_counsel.py +0 -0
  175. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_cwlogs_paging.py +0 -0
  176. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_demo.py +0 -0
  177. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_deps.py +0 -0
  178. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_desktop.py +0 -0
  179. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_devtools.py +0 -0
  180. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_diff_review.py +0 -0
  181. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_docs_and_llm.py +0 -0
  182. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_docs_flow.py +0 -0
  183. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_duplicate_connectors.py +0 -0
  184. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_evening_brief.py +0 -0
  185. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_factory.py +0 -0
  186. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_feed_working.py +0 -0
  187. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_finished_chat_stays_finished.py +0 -0
  188. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_floating_assistant.py +0 -0
  189. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_follow_up_verdict.py +0 -0
  190. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_funnel.py +0 -0
  191. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_general.py +0 -0
  192. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_github_auto.py +0 -0
  193. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_guard.py +0 -0
  194. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_handbook.py +0 -0
  195. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_handbook_topics.py +0 -0
  196. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_handoff_channels.py +0 -0
  197. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_handraise.py +0 -0
  198. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_harvest.py +0 -0
  199. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_histgen.py +0 -0
  200. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_home_is_never_yours.py +0 -0
  201. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_imap_history.py +0 -0
  202. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_imapmail.py +0 -0
  203. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_imessage.py +0 -0
  204. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_intacct.py +0 -0
  205. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_invoice_workflow.py +0 -0
  206. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_kind_dispatch.py +0 -0
  207. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_knowledge.py +0 -0
  208. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_learnedgraph.py +0 -0
  209. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_lifecycle.py +0 -0
  210. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_live.py +0 -0
  211. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_local_file.py +0 -0
  212. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_mail_folders.py +0 -0
  213. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_mark_read.py +0 -0
  214. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_mcp.py +0 -0
  215. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_meeting_prep.py +0 -0
  216. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_message_thread.py +0 -0
  217. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_messengers.py +0 -0
  218. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_msauth.py +0 -0
  219. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_mssql.py +0 -0
  220. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_noop_ending.py +0 -0
  221. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_not_coding.py +0 -0
  222. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_note_retrieval.py +0 -0
  223. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_outbound_reports.py +0 -0
  224. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_own_rows_no_reply.py +0 -0
  225. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_playbooks.py +0 -0
  226. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_pm.py +0 -0
  227. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_poll_overlap.py +0 -0
  228. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_poll_parallel.py +0 -0
  229. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_poll_sources.py +0 -0
  230. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_pr_comments.py +0 -0
  231. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_problems.py +0 -0
  232. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_problems_dismiss.py +0 -0
  233. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_projects.py +0 -0
  234. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_promptmap_and_catalog.py +0 -0
  235. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_proof_ci_proposals.py +0 -0
  236. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_pypi_downloads.py +0 -0
  237. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_quickbooks.py +0 -0
  238. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_rank.py +0 -0
  239. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_rate_limit.py +0 -0
  240. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_reclassify.py +0 -0
  241. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_remote_assistant.py +0 -0
  242. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_reply_channels.py +0 -0
  243. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_reply_voice.py +0 -0
  244. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_report_alerts.py +0 -0
  245. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_report_triage.py +0 -0
  246. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_research.py +0 -0
  247. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_reviews.py +0 -0
  248. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_scopes.py +0 -0
  249. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_seed_fits_a_tty.py +0 -0
  250. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_seeded_reports.py +0 -0
  251. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_semantic.py +0 -0
  252. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_send_targets.py +0 -0
  253. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_session_artifacts.py +0 -0
  254. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_setup.py +0 -0
  255. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_sheets_sharepoint.py +0 -0
  256. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_someone_else_replied.py +0 -0
  257. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_soul_interview.py +0 -0
  258. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_stay_open.py +0 -0
  259. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_stay_open_doors.py +0 -0
  260. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_store_app_cli.py +0 -0
  261. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_store_hotpaths.py +0 -0
  262. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_sync_clock.py +0 -0
  263. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_task_identity.py +0 -0
  264. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_teller.py +0 -0
  265. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_terminal.py +0 -0
  266. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_toil.py +0 -0
  267. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_triage_dataset.py +0 -0
  268. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_triage_images.py +0 -0
  269. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_update.py +0 -0
  270. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_update_refused.py +0 -0
  271. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_urgent_and_handoff.py +0 -0
  272. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_verdict_paths.py +0 -0
  273. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_verdict_sticks.py +0 -0
  274. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_vision.py +0 -0
  275. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_voice.py +0 -0
  276. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_wabridge.py +0 -0
  277. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_waitroom.py +0 -0
  278. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_watermark.py +0 -0
  279. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_web_entrypoint.py +0 -0
  280. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_webguard.py +0 -0
  281. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_whatsapp_documents.py +0 -0
  282. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_whatsapp_images.py +0 -0
  283. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_whoami.py +0 -0
  284. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_withdrawn.py +0 -0
  285. {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_witness.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskuary
3
- Version: 0.3.3.0
3
+ Version: 0.3.3.2
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
@@ -62,22 +62,18 @@ Dynamic: license-file
62
62
  # Taskuary
63
63
 
64
64
  [![CI](https://github.com/ldbumble/taskuary/actions/workflows/ci.yml/badge.svg)](https://github.com/ldbumble/taskuary/actions/workflows/ci.yml)
65
- [![PyPI](https://img.shields.io/pypi/v/taskuary.svg?cacheSeconds=600)](https://pypi.org/project/taskuary/)
65
+ [![PyPI](https://img.shields.io/pypi/v/taskuary.svg?cacheSeconds=300&release=0.3.3.2)](https://pypi.org/project/taskuary/)
66
66
  [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776ab.svg)](https://github.com/ldbumble/taskuary)
67
67
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
68
68
 
69
- ## Your inbox, staffed by AI agents.
69
+ ## Your inbox, staffed by AI agents
70
70
 
71
- Taskuary brings your mail, chats, issues, and scheduled reports into one local conversation:
72
- a live work pipe on the left and chat on the right. AI triage decides what is real work,
73
- your coding CLI works the tasks in your own repos, and replies wait for your approval.
74
- Nothing sends or ships without you.
71
+ Taskuary turns incoming messages into organized work. It sorts what matters, hands tasks to
72
+ your agents, and brings decisions back to you. Nothing sends or ships without your approval.
75
73
 
76
- ![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)
74
+ ![Taskuary: the pipe ranked by triage beside the assistant chat - a mail pulled in with its drafted reply, the Timeline in task view, the Reports and the Board.](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/hero.gif)
77
75
 
78
- Taskuary is early—currently **v0.3.3.0**—and moving fast. The funnel, review queue,
79
- agent sessions, and reports pipeline are in daily use; breaking changes are still possible
80
- before 1.0.
76
+ Taskuary is early—currently **v0.3.3.2**—so breaking changes are still possible before 1.0.
81
77
 
82
78
  <p align="center">
83
79
  <a href="https://taskuary.com/demo/"><img
@@ -85,43 +81,61 @@ before 1.0.
85
81
  alt="Try Taskuary now, in your browser"></a>
86
82
  </p>
87
83
 
88
- <p align="center"><sub>The real app over invented work — the unified pipe and chat, triage,
89
- a coding session mid-run, and reports with charts. Nothing to install and nothing connects
90
- to anything.</sub></p>
84
+ <p align="center"><sub>The real app with invented data. Nothing connects, sends, or runs.</sub></p>
91
85
 
92
- ## One timeline, one chat
86
+ ## What Taskuary can do
93
87
 
94
- Outlook, Gmail, Teams, Slack, Telegram, WhatsApp, GitHub, Jira, alerts, and reports all
95
- arrive in the pipe beside the same conversation. **Unread** is the prioritized walkthrough;
96
- **All** is the recent timeline. Click any row—or name a sender, subject, or task in chat—to
97
- pull it into the conversation with its context, triage reasoning, draft, and available actions.
88
+ **One Timeline and Assistant**
89
+
90
+ Mail, chats, issues, alerts, and reports appear on one Timeline and in one Assistant chat.
91
+ Open any item to see its context, status, and available actions.
98
92
 
99
93
  ![Taskuary's unified work pipe and chat, populated only with fictional demo data.](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/screenshot-chat.png)
100
94
 
101
- *Everything shown is generated by the built-in demo. Dana Whitfield, Northwind Facilities,
102
- the people, messages, repositories, tasks, and reports are all fictional.*
103
-
104
- The same conversation also speaks up when something falls between tasks: a reply went
105
- unanswered, a meeting needs context, a task went quiet, or a pattern appears across the
106
- week's work. Each observation carries its evidence and can be acted on, dismissed, snoozed,
107
- or taught away with **Not this**. These observations now appear in this same chat instead of
108
- as separate Assistant posts on the Timeline.
109
-
110
- These checks can watch the systems you run on, not only incoming messages. Point one at a
111
- Sage Intacct query, a database, a file, a REST or MCP tool, a cloud log group, or an agent
112
- skill—no saved report has to stand behind it—and Taskuary reads it silently on schedule.
113
- Describe what a check should watch and AI can write the source cards using only connected
114
- systems, after reading their real schema and asking rather than guessing a filter. The same
115
- help sits on every source card in the report builder. Voice, schedule, model, and thresholds
116
- remain yours to change; a check stays silent when there is nothing useful to add to the chat.
117
-
118
- Coding work can go straight to Claude Code, Codex, Gemini, Cursor, Copilot, or any CLI that
119
- accepts a prompt on stdin. You watch the live terminal, answer questions, review the diff,
120
- and approve what happens next. Research, marketing, and other general work belong in a
121
- conversational workspace built with assistant-ui and your existing Taskuary AI connections.
122
- Chat and terminal are two views of the same session: model, history, queued instructions,
123
- image attachments, and the side-by-side browser stay put when you switch. The same workspace
124
- also appears on the Wall.
95
+ **AI triage**
96
+
97
+ Triage files routine updates, turns real requests into tasks, and puts urgent decisions first.
98
+
99
+ **Agents in your own repos**
100
+
101
+ Send coding work to Claude Code, Codex, Gemini, Cursor, Copilot, or another CLI.
102
+
103
+ **Live workspaces**
104
+
105
+ Watch the terminal, answer questions, review changes, and use the built-in browser without
106
+ losing the session.
107
+
108
+ **Scheduled checks**
109
+
110
+ Watch databases, files, APIs, cloud logs, reports, or agent skills. Quiet checks stay silent.
111
+
112
+ **You stay in control**
113
+
114
+ Replies and proposed actions wait in Review. You decide what sends, runs, closes, or gets dismissed.
115
+
116
+ **Catches what slipped**
117
+
118
+ Surface unanswered replies, upcoming meetings, quiet tasks, failed reports, and important changes.
119
+
120
+ ## How work flows
121
+
122
+ ### 1. Work arrives and gets sorted
123
+
124
+ Taskuary reads connected inboxes, separates tasks from noise, and shows the result on one Timeline.
125
+
126
+ ![The Timeline in task view: every message with its verdict, and the drafted reply waiting on you](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/screenshot-timeline-crop.png)
127
+
128
+ ### 2. An agent works on it
129
+
130
+ Tasks move from **Queued** to **Agent working**. The Board shows live progress and anything waiting on you.
131
+
132
+ ![Taskuary Board with queued, active, and waiting work](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/screenshot-board.png)
133
+
134
+ ### 3. The result comes back to you
135
+
136
+ Finished work, draft replies, questions, and loose ends return for review. You decide what to send, change, snooze, or dismiss.
137
+
138
+ ![Taskuary assistant showing loose ends and suggested actions](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/screenshot-assistant.png)
125
139
 
126
140
  ## Install
127
141
 
@@ -164,25 +178,15 @@ connection before it goes live.
164
178
  taskuary --demo # or: docker compose --profile demo up
165
179
  ```
166
180
 
167
- A full Taskuary over invented work — a morning's mail and chats already triaged with the
168
- reasons attached, drafts waiting in Review, agents on the board with their transcripts
169
- playing, live task handoffs on the Board, and the durable, high-signal company Hub.
170
- Every Hub post names its human or agent author and originating task; Board handoffs are short-lived
171
- checkout coordination that rolls up nightly. The AI answers from a script and every door
172
- to the outside world is shut at the API layer (`taskuary/demo.py`): nothing sends, no
173
- connection can be made or edited, no tool runs, and no CLI starts. Nobody's real data is in
174
- it — Dana Whitfield and Northwind Facilities are invented.
181
+ The demo is the real interface with fictional work and scripted replies. It cannot connect to
182
+ outside systems, send messages, run tools, or start agents. Its changes reset when you reload.
175
183
 
176
184
  ## Installs
177
185
 
178
186
  ![Daily installs of taskuary from PyPI, mirror traffic excluded](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/downloads.svg)
179
187
 
180
- Redrawn every morning by [`downloads.yml`](.github/workflows/downloads.yml) from PyPI's own
181
- numbers, with mirror traffic excluded — a full PyPI mirror pulls every release, so counting it
182
- would let a handful of real users read as hundreds. What it does **not** claim is that CI
183
- installs have been removed: the field that would separate `pip install` in a build runner from
184
- one on somebody's laptop is in PyPI's BigQuery dataset, not in the public API. The daily series
185
- is [docs/downloads.csv](docs/downloads.csv).
188
+ Updated daily from PyPI with mirror traffic excluded. The raw series is
189
+ [docs/downloads.csv](docs/downloads.csv).
186
190
 
187
191
  ## Documentation
188
192
 
@@ -0,0 +1,140 @@
1
+ # Taskuary
2
+
3
+ [![CI](https://github.com/ldbumble/taskuary/actions/workflows/ci.yml/badge.svg)](https://github.com/ldbumble/taskuary/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/taskuary.svg?cacheSeconds=300&release=0.3.3.2)](https://pypi.org/project/taskuary/)
5
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776ab.svg)](https://github.com/ldbumble/taskuary)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ ## Your inbox, staffed by AI agents
9
+
10
+ Taskuary turns incoming messages into organized work. It sorts what matters, hands tasks to
11
+ your agents, and brings decisions back to you. Nothing sends or ships without your approval.
12
+
13
+ ![Taskuary: the pipe ranked by triage beside the assistant chat - a mail pulled in with its drafted reply, the Timeline in task view, the Reports and the Board.](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/hero.gif)
14
+
15
+ Taskuary is early—currently **v0.3.3.2**—so breaking changes are still possible before 1.0.
16
+
17
+ <p align="center">
18
+ <a href="https://taskuary.com/demo/"><img
19
+ 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"
20
+ alt="Try Taskuary now, in your browser"></a>
21
+ </p>
22
+
23
+ <p align="center"><sub>The real app with invented data. Nothing connects, sends, or runs.</sub></p>
24
+
25
+ ## What Taskuary can do
26
+
27
+ **One Timeline and Assistant**
28
+
29
+ Mail, chats, issues, alerts, and reports appear on one Timeline and in one Assistant chat.
30
+ Open any item to see its context, status, and available actions.
31
+
32
+ ![Taskuary's unified work pipe and chat, populated only with fictional demo data.](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/screenshot-chat.png)
33
+
34
+ **AI triage**
35
+
36
+ Triage files routine updates, turns real requests into tasks, and puts urgent decisions first.
37
+
38
+ **Agents in your own repos**
39
+
40
+ Send coding work to Claude Code, Codex, Gemini, Cursor, Copilot, or another CLI.
41
+
42
+ **Live workspaces**
43
+
44
+ Watch the terminal, answer questions, review changes, and use the built-in browser without
45
+ losing the session.
46
+
47
+ **Scheduled checks**
48
+
49
+ Watch databases, files, APIs, cloud logs, reports, or agent skills. Quiet checks stay silent.
50
+
51
+ **You stay in control**
52
+
53
+ Replies and proposed actions wait in Review. You decide what sends, runs, closes, or gets dismissed.
54
+
55
+ **Catches what slipped**
56
+
57
+ Surface unanswered replies, upcoming meetings, quiet tasks, failed reports, and important changes.
58
+
59
+ ## How work flows
60
+
61
+ ### 1. Work arrives and gets sorted
62
+
63
+ Taskuary reads connected inboxes, separates tasks from noise, and shows the result on one Timeline.
64
+
65
+ ![The Timeline in task view: every message with its verdict, and the drafted reply waiting on you](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/screenshot-timeline-crop.png)
66
+
67
+ ### 2. An agent works on it
68
+
69
+ Tasks move from **Queued** to **Agent working**. The Board shows live progress and anything waiting on you.
70
+
71
+ ![Taskuary Board with queued, active, and waiting work](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/screenshot-board.png)
72
+
73
+ ### 3. The result comes back to you
74
+
75
+ Finished work, draft replies, questions, and loose ends return for review. You decide what to send, change, snooze, or dismiss.
76
+
77
+ ![Taskuary assistant showing loose ends and suggested actions](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/screenshot-assistant.png)
78
+
79
+ ## Install
80
+
81
+ ### Windows app
82
+
83
+ Download the latest single-file
84
+ [Taskuary.exe](https://github.com/ldbumble/taskuary/releases/latest/download/Taskuary.exe)
85
+ and open it. No Python or installer is required.
86
+
87
+ ### Python
88
+
89
+ Python 3.10 or newer works on Windows, macOS, and Linux:
90
+
91
+ ```bash
92
+ pip install taskuary
93
+ taskuary
94
+ ```
95
+
96
+ Taskuary opens at [http://127.0.0.1:7787](http://127.0.0.1:7787). For a native desktop
97
+ window instead, install `pip install "taskuary[desktop]"` and run `taskuary-desktop`.
98
+
99
+ ### Docker
100
+
101
+ ```bash
102
+ git clone https://github.com/ldbumble/taskuary
103
+ cd taskuary
104
+ docker compose up
105
+ ```
106
+
107
+ Then open [http://127.0.0.1:7787](http://127.0.0.1:7787). Docker runs the web app;
108
+ coding CLIs and the optional WhatsApp bridge remain on the host.
109
+
110
+ On first run, connect an AI provider or local Ollama model, add at least one inbound
111
+ channel, then choose the coding CLI that should receive tasks. The setup wizards test each
112
+ connection before it goes live.
113
+
114
+ ## Try it without installing anything
115
+
116
+ ```bash
117
+ taskuary --demo # or: docker compose --profile demo up
118
+ ```
119
+
120
+ The demo is the real interface with fictional work and scripted replies. It cannot connect to
121
+ outside systems, send messages, run tools, or start agents. Its changes reset when you reload.
122
+
123
+ ## Installs
124
+
125
+ ![Daily installs of taskuary from PyPI, mirror traffic excluded](https://raw.githubusercontent.com/ldbumble/taskuary/master/docs/downloads.svg)
126
+
127
+ Updated daily from PyPI with mirror traffic excluded. The raw series is
128
+ [docs/downloads.csv](docs/downloads.csv).
129
+
130
+ ## Documentation
131
+
132
+ - [Getting started](docs/getting-started.md)—installation, first-run setup, Docker, and data
133
+ - [Product guide](docs/product-guide.md)—the workflow, learning loop, agents, and operator documents
134
+ - [Integrations](docs/integrations.md)—channels, AI providers, work systems, and report sources
135
+ - [Reports and proactive checks](docs/reports-and-assistant.md)—the report pipeline, AI-written source cards, and what Taskuary watches
136
+ - [Status and roadmap](docs/roadmap.md)—what works today and what is next
137
+ - [Contributing](CONTRIBUTING.md)—development setup and contribution guide
138
+
139
+ Taskuary is free and open source under the [MIT License](LICENSE). Issues and pull requests
140
+ are welcome; security reports belong in [SECURITY.md](SECURITY.md).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "taskuary"
7
- version = "0.3.3.0"
7
+ version = "0.3.3.2"
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" }
@@ -1005,23 +1005,35 @@ def _sender_for(store, words: list) -> str:
1005
1005
  return max(tally, key=tally.get) if tally else ''
1006
1006
 
1007
1007
 
1008
+ # "skip all the fyi from Chana and Dovid" names a LANE, not a subject. Read as subject words it
1009
+ # wrote "fyi from chana@ours.com" and "dovid from dovid@ours.com" - one rule about a word nobody
1010
+ # writes in a subject line, one about a man's own name (the 2026-09-03 break test).
1011
+ LANE_RULE_WORDS = {'fyi': 'fyi', 'fyis': 'fyi', 'report': 'report', 'reports': 'report'}
1012
+
1008
1013
  def _rules_from(swept: list) -> list:
1009
- """One rule per sender, carrying the words that hit THEIR mail: [{'sender', 'words'}]. A sender
1010
- who was swept on their name alone gets a rule about them; everyone else needs their subject
1011
- words, so a rule can never quietly grow into "everything from this person"."""
1012
- by = {}
1014
+ """One rule per sender: [{'sender', 'words'}] or [{'sender', 'lane'}]. A sender swept on their
1015
+ name alone gets a rule about them; a sweep that named a LANE gets that lane from that sender;
1016
+ everyone else needs their subject words, so a rule can never quietly grow into "everything from
1017
+ this person"."""
1018
+ by, lanes = {}, {}
1013
1019
  for x in swept:
1014
1020
  key = x['email'] or (x['who'] or '').lower()
1015
1021
  if not key: continue
1016
1022
  by.setdefault(key, set()).update(x['words'])
1023
+ for w in x['words']:
1024
+ if w in LANE_RULE_WORDS: lanes.setdefault(key, set()).add(LANE_RULE_WORDS[w])
1017
1025
  out = []
1018
1026
  for key, words in by.items():
1019
- subject = sorted(w for w in words if not any(w in part for part in re.split(r'[@.\s]+', key)))
1027
+ if lanes.get(key):
1028
+ out.append({'sender': key, 'lane': sorted(lanes[key])[0], 'words': []}); continue
1029
+ own = set(re.split(r'[@.\s]+', key))
1030
+ subject = sorted(w for w in words if w not in LANE_RULE_WORDS and not any(w in part for part in own))
1020
1031
  out.append({'sender': key, 'words': subject or sorted(words)})
1021
1032
  return out
1022
1033
 
1023
1034
 
1024
1035
  def _rule_words(rule: dict) -> str:
1036
+ if rule.get('lane'): return f"every {rule['lane']} from {rule['sender']}"
1025
1037
  return f"{' '.join(rule['words'][:4])} from {rule['sender']}" if rule.get('sender') else ' '.join(rule['words'][:4])
1026
1038
 
1027
1039
 
@@ -1153,7 +1165,10 @@ def _carry_out(store, tid: int, text: str, words: dict, item0: dict | None, acto
1153
1165
  return {'say': say_, 'options': [], 'decision': None}
1154
1166
  say_ = (f"Split: {out['ref']} keeps \"{out['kept']}\" and {out['newRef']} is \"{out['title']}\". "
1155
1167
  'Each is its own job now, so an agent sent at one only gets that one.'
1156
- if out.get('newRef') else "There is only one ask in that one - nothing to split.")
1168
+ if out.get('newRef') else
1169
+ ('I can only see one ask in that one' + (f" - {out['why']}" if out.get('why') else '')
1170
+ + '. Tell me the second job in your own words and I will make it, or open '
1171
+ + f"{out.get('ref') or 'the task'} and split it there. It stays on the table either way."))
1157
1172
  record(store, tid, 'assistant', say_)
1158
1173
  return {'say': say_, 'options': [], 'decision': {'verb': 'split', **out}}
1159
1174
  if words['verb'] == 'remember':
@@ -1248,8 +1263,12 @@ def split_item(store, item: dict, text: str, actor: str = 'owner') -> dict:
1248
1263
  new = ingest.split_message(store, item['mid'], actor)
1249
1264
  t = store.get_task(new) or {}
1250
1265
  return {'ref': None, 'kept': '', 'newRef': task_ref(new), 'taskId': new, 'title': t.get('Title') or ''}
1251
- prop = reshape.propose_split(store, tid, brain(store, fast=True))
1252
- if not prop.get('two') or not (prop.get('second') or {}).get('title'): return {'ref': task_ref(tid), 'kept': (store.get_task(tid) or {}).get('Title') or ''}
1266
+ # NOT the fast brain: reading two jobs out of one mail is a judgement, and the two-sentence
1267
+ # gear missed a mail that plainly carried two ("add Priya to the list" + "remove the Bulk
1268
+ # Approve button") and answered "only one ask" (the 2026-09-03 break test)
1269
+ prop = reshape.propose_split(store, tid, brain(store))
1270
+ if not prop.get('two') or not (prop.get('second') or {}).get('title'):
1271
+ return {'ref': task_ref(tid), 'kept': (store.get_task(tid) or {}).get('Title') or '', 'why': prop.get('why') or ''}
1253
1272
  new = reshape.split_task(store, tid, prop['second'], prop.get('first'), prop.get('move_message_ids') or [], actor)
1254
1273
  return {'ref': task_ref(tid), 'kept': (store.get_task(tid) or {}).get('Title') or '',
1255
1274
  'newRef': task_ref(new), 'taskId': new, 'title': (store.get_task(new) or {}).get('Title') or ''}
@@ -44,6 +44,7 @@ LANE_WORDS = {'blocked': ('agent waiting', 'you'), 'time': ('coming up', 'workin
44
44
  'asked': ('asked you', 'working'), 'forgotten': ('slipped', 'info'), 'report': ('landed', 'info'), 'fyi': ('fyi', None),
45
45
  'working': ('agent working', 'working')} # in hand: at the very top, nothing to do until the agent stops or asks
46
46
  SOON_MIN, ALERT_MIN = 120, 15 # a meeting inside two hours is time-sensitive; inside fifteen it interrupts
47
+ SETUP_GRACE_MIN = 15 # a walk-through the owner is still in does not raise its own hand
47
48
  LATER_HOURS = 3 # "not now" - it comes back this much later
48
49
  FEED_DAYS = 7
49
50
  # the owner's two knobs (Settings > Assistant): how far back the pipe reaches for ordinary mail - a
@@ -96,6 +97,9 @@ def muted(rule: dict, i: dict) -> bool:
96
97
  from .routing import tokens
97
98
  key = str(rule.get('sender') or '').lower()
98
99
  if key and key not in (str(i.get('email') or '').lower(), str(i.get('who') or '').lower()): return False
100
+ # a rule can name a LANE rather than words: "skip all the fyi from Chana" is every fyi she sends,
101
+ # not the mails with 'fyi' in the subject (2026-09-03)
102
+ if rule.get('lane'): return i.get('lane') == rule['lane'] and bool(key)
99
103
  words = [w for w in (rule.get('words') or []) if w]
100
104
  if not words: return bool(key)
101
105
  return like(words, set(tokens(f"{i.get('who') or ''} {i.get('email') or ''} {i.get('title') or ''}"))) >= min(2, len(words))
@@ -113,23 +117,41 @@ def _item(key, kind, lane, title, *, who='', when='', since='', why='', mid=None
113
117
 
114
118
 
115
119
  # ── the producers: each reads one thing the hub holds ─────────────────────────────────────────
120
+ def _feed_skip(r: dict) -> bool:
121
+ """Rows that are nobody asking anything: our own sends, withdrawn lines, an auto-reply, and
122
+ anything on a task that is over (unless a draft on it still waits for a yes)."""
123
+ if r.get('Direction') == 'out' or r.get('MsgStatus') == 'withdrawn' or r.get('Channel') == 'assistant': return True
124
+ if r.get('TaskStatus') in ('done', 'dropped') and r.get('ReviewStatus') != 'pending': return True
125
+ return bool(_OOO.match(str(r.get('Subject') or '')))
126
+
127
+
128
+ def thread_speaker(rows: list) -> dict:
129
+ """Which row speaks for each conversation: {cid: MessageId}. A draft waiting for a yes speaks
130
+ whatever its age, then an agent parked on a question, then the newest line. Reviews and agents
131
+ used to be EXEMPT from the one-line-per-thread rule rather than winning it, so the older mail on
132
+ the same thread came up again as "asked you" - TQ-0002 twice after the wrap, Yosef three times
133
+ in the All list (the 2026-09-03 break test)."""
134
+ best = {}
135
+ for r in rows:
136
+ cid = r.get('ConversationId')
137
+ if not cid or _feed_skip(r): continue
138
+ rank = 2 if (r.get('ReviewStatus') == 'pending' and r.get('ReviewId')) else 1 if r.get('AgentWaiting') else 0
139
+ if cid not in best or rank > best[cid][0]: best[cid] = (rank, r['MessageId']) # newest first, so ties keep the newest
140
+ return {cid: mid for cid, (_rank, mid) in best.items()}
141
+
142
+
116
143
  def from_feed(store, rows: list) -> list:
117
144
  out, agents, reviews, threads = [], set(), set(), {}
145
+ speaks, more = thread_speaker(rows), {}
118
146
  for r in rows:
119
- if r.get('Direction') == 'out' or r.get('MsgStatus') == 'withdrawn' or r.get('Channel') == 'assistant': continue
120
- if r.get('TaskStatus') in ('done', 'dropped') and r.get('ReviewStatus') != 'pending': continue
121
- if _OOO.match(str(r.get('Subject') or '')): continue # an auto-reply is nobody asking anything
122
- # one line per conversation: rows come newest first, so the newest speaks for the thread
147
+ if _feed_skip(r): continue
148
+ # ONE line per conversation, and it says how much of the thread it stands for: two WhatsApp
149
+ # lines from the same person are one row, and the Timeline showing two of them read as the
150
+ # pipe having lost one (the owner, 2026-09-03)
123
151
  cid = r.get('ConversationId')
124
- if cid and r.get('ReviewStatus') != 'pending' and not r.get('AgentWaiting'):
125
- # one line per conversation, and it SAYS how much of the thread it stands for: two
126
- # WhatsApp lines from the same person are one row, and the Timeline showing two of them
127
- # read as the pipe having lost one (the owner, 2026-09-03)
128
- if cid in threads:
129
- held = threads[cid] # None when the thread's first row made no item at all
130
- if held is not None: held['more'] = held.get('more', 0) + 1
131
- continue
132
- threads[cid] = None # filled in below, once the item exists
152
+ if cid and speaks.get(cid) != r['MessageId']:
153
+ more[cid] = more.get(cid, 0) + 1
154
+ continue
133
155
  who = r.get('FromName') or r.get('FromEmail') or r.get('SourceName') or r.get('Channel') or ''
134
156
  base = dict(who=who, when=r.get('SentAt'), mid=r['MessageId'], tid=r.get('TaskId'), channel=r.get('Channel') or '',
135
157
  category=r.get('Category') or '', preview=r.get('Preview'), cid=cid, email=r.get('FromEmail') or '')
@@ -148,14 +170,23 @@ def from_feed(store, rows: list) -> list:
148
170
  else ('a reply is drafted for you to send' if r.get('HasDraft') else 'a reply is owed - draft it with AI or write it'),
149
171
  draft=bool(r.get('HasDraft')), summary=agent_found(store, r.get('TaskId')),
150
172
  sig=hashlib.sha1(str(rv.get('DraftText') or '').encode()).hexdigest()[:10], # the draft's fingerprint: a rewrite is news
151
- **base)); continue
173
+ **base))
174
+ if cid: threads[cid] = out[-1] # the draft speaks for its thread
175
+ continue
152
176
  if r.get('AgentWaiting') and r.get('TaskId'):
153
177
  if r['TaskId'] in agents: continue
154
178
  agents.add(r['TaskId'])
155
179
  out.append(_item(f"agent:{r['TaskId']}", 'agent', 'blocked', r.get('Title') or subj, agent=r.get('Working') or 'agent',
156
- why=f"{r.get('Working') or 'the agent'} stopped and is waiting on you", **base)); continue
180
+ why=f"{r.get('Working') or 'the agent'} stopped and is waiting on you", **base))
181
+ if cid: threads[cid] = out[-1]
182
+ continue
157
183
  base['working'] = r.get('Working') or '' # an agent has it: build() lets these go, by name
158
184
  if r.get('Channel') == 'report':
185
+ # a run whose own first line says it could not summarise (no AI connector) is on the
186
+ # Timeline and nowhere else - three of them came out of the pipe, one per turn, on a
187
+ # fresh install's first day (the 2026-09-03 break test)
188
+ from .reports import NO_BRAIN
189
+ if NO_BRAIN in str(r.get('Preview') or ''): continue
159
190
  sid = report_source_id(store, r.get('SourceName'))
160
191
  bad = report_failed(store, sid, subj)
161
192
  out.append(_item(f"report:{r['MessageId']}", 'report', 'report', subj, bad=bad, source_id=sid,
@@ -177,7 +208,10 @@ def from_feed(store, rows: list) -> list:
177
208
  continue
178
209
  if cat == 'info':
179
210
  out.append(_item(f"msg:{r['MessageId']}", 'fyi', 'fyi', subj, why=r.get('RouteReason') or 'a person told you something; nothing to do', **base))
180
- if cid and cid in threads and threads[cid] is None and out: threads[cid] = out[-1]
211
+ if cid and out and threads.get(cid) is None: threads[cid] = out[-1]
212
+ for cid, n in more.items(): # the rest of each thread, counted on the row that speaks for it
213
+ held = threads.get(cid)
214
+ if held is not None: held['more'] = held.get('more', 0) + n
181
215
  return out
182
216
 
183
217
 
@@ -227,6 +261,7 @@ def from_agents(store) -> list:
227
261
  """Live sessions parked on a question - whatever the feed window, an agent waiting is waiting."""
228
262
  from . import terminal as term, waitroom
229
263
  out = []
264
+ fresh_setup = (datetime.now() - timedelta(minutes=SETUP_GRACE_MIN)).strftime('%Y-%m-%d %H:%M:%S')
230
265
  try: live = term.live_sessions(tail=6)
231
266
  except Exception: return out
232
267
  for t in live:
@@ -234,9 +269,19 @@ def from_agents(store) -> list:
234
269
  if not tid: continue
235
270
  task = store.get_task(tid) or {}
236
271
  if task.get('SourceRef') == 'assistant:dock' or task.get('Status') in ('done', 'dropped'): continue
272
+ # A WALK-THROUGH the owner just started is a conversation they are IN. It parked because it
273
+ # said its piece and is waiting for their next line - and the by-the-way bar announced
274
+ # "assistant stopped on TQ-0009 and is waiting on you" sixty seconds after they opened it
275
+ # (the 2026-09-03 break test). It comes back as a blocked row only once it has been left.
276
+ if task.get('SourceRef') == 'assistant:setup' and _ts(t.get('started')) >= fresh_setup: continue
237
277
  waiting = t.get('waiting') if t.get('waiting') is not None else (t.get('idle') or 0) >= term.IDLE_WAITING
238
278
  if not waiting: continue
239
279
  tail = [str(x).strip() for x in (t.get('tail') or []) if str(x).strip()]
280
+ # ...read off the RENDERED screen when there is one, with the TUI's chrome dropped: the
281
+ # card showed a theme toolbar where the agent's question belonged (2026-09-03)
282
+ if t.get('sid'):
283
+ try: tail = [x for x in term.asking_lines(t['sid'], 4)] or tail
284
+ except Exception as e: logger.debug(f'funnel: no rendered screen for {t.get("sid")} - {e}')
240
285
  asking = waitroom.looks_like_question(tail)
241
286
  agent = t.get('agent') or t.get('label') or 'agent'
242
287
  out.append(_item(f"agent:{tid}", 'agent', 'blocked', task.get('Title') or f'task {tid}', who=agent, when=t.get('started'),
@@ -391,11 +436,20 @@ def _aged_out(i: dict, now: datetime, hours: int) -> bool:
391
436
  return bool(when) and when < now - timedelta(hours=hours)
392
437
 
393
438
 
439
+ RUN_STALE_MIN = 20 # a 'running' run row nobody has touched for this long is not working anything
440
+
394
441
  def working_tids(store) -> set:
395
- """Tasks an agent has right now - a live session or a running headless run. Nothing about them is
396
- the owner's to do until the agent stops."""
442
+ """Tasks an agent has right now - a live session, or a headless run that is actually running.
443
+ Nothing about them is the owner's to do until the agent stops.
444
+
445
+ A run row left at 'running' by a session that died used to be proof enough: TQ-0006 sat in the
446
+ working lane with no session and "nothing for you", and the outage task vanished from the pipe
447
+ altogether - not read, not offered, not findable (the 2026-09-03 break test). A row nobody has
448
+ touched for RUN_STALE_MIN is a corpse, not a worker."""
397
449
  from . import terminal as term
398
- out = {r['TaskId'] for r in store.running_runs() if r.get('TaskId')}
450
+ fresh = (datetime.now() - timedelta(minutes=RUN_STALE_MIN)).strftime('%Y-%m-%d %H:%M:%S')
451
+ out = {r['TaskId'] for r in store.running_runs()
452
+ if r.get('TaskId') and str(r.get('UpdatedAt') or r.get('StartedAt') or '') >= fresh}
399
453
  try:
400
454
  for t in term.live_sessions(tail=0):
401
455
  if not t.get('taskId'): continue
@@ -428,11 +482,23 @@ def build(store, now: datetime = None, keep_surfaced: bool = False) -> dict:
428
482
  # an agent mid-job: nothing to do here yet, whatever the mail or the idea says about the task - so it
429
483
  # rides at the TOP of the pipe as 'in hand', and drops to the front when the agent stops or asks
430
484
  busy = working_tids(store)
485
+ live_tids = busy | {i['tid'] for i in items if i['kind'] == 'agent' and i.get('tid')} # working, parked or asking: an agent is on it
486
+ stale_before = (now - timedelta(minutes=RUN_STALE_MIN)).strftime('%Y-%m-%d %H:%M:%S')
431
487
  # ...and a task whose STATUS says in_progress is in the middle of being worked, whether or not a
432
488
  # session is alive right now: the owner reads it that way ("it's in middle of working... it should
433
489
  # say working so it's not in funnel"), and it comes back to the front the moment the watcher moves
434
490
  # it to waiting or the agent asks (2026-09-03).
435
- def held(tid): return bool(tid) and (tid in busy or (store.get_task(tid) or {}).get('Status') == 'in_progress')
491
+ # 'in_progress' means an agent is mid-job, and the owner reads it that way even between sessions
492
+ # ("it's in middle of working... it should say working so it's not in funnel"). But nobody moves
493
+ # the status back when a session DIES, so the status alone held a task in the working lane for
494
+ # ever: TQ-0006 sat there with no session and "nothing for you", and the outage task fell out of
495
+ # the pipe entirely (the 2026-09-03 break test). Mid-job is a live agent, or a task somebody has
496
+ # touched in the last RUN_STALE_MIN; anything older is abandoned, and comes back to the owner.
497
+ def mid_job(tid):
498
+ t = store.get_task(tid) or {}
499
+ if t.get('Status') != 'in_progress': return False
500
+ return tid in live_tids or str(t.get('UpdatedAt') or t.get('CreatedAt') or '') >= stale_before
501
+ def held(tid): return bool(tid) and (tid in busy or mid_job(tid))
436
502
  def in_hand(i): return i['kind'] not in ('agent', 'review', 'action') and (i.get('working') or held(i.get('tid')))
437
503
  # ...under the SAME key the parked agent will have (agent:<tid>), so shown-once and the page's live
438
504
  # row follow the task through stopping and starting instead of losing it at each change
@@ -263,6 +263,13 @@ def ingest_message(store, msg: dict, actor: str = 'router', llm=None, file_only:
263
263
  with _PENDING_LOCK: _PENDING[mid] = msg
264
264
  return {'status': 'queued', 'task_id': None, 'message_id': mid}
265
265
 
266
+ # a chat opener with nothing behind it yet: on the timeline, and no task, no draft, no agent.
267
+ # The line that follows it carries the ask, and the reader is shown this one as its opening.
268
+ if is_opener(msg):
269
+ mid = _land(store, msg, None, 'filed')
270
+ store.add_route(mid, None, 'file', None, 'an opening line on a chat - waiting for the ask it opens', [], 'triage')
271
+ logger.info(f"ingest: chat opener filed, waiting for the point - {(msg.get('body') or '')[:40]}")
272
+ return {'status': 'filed', 'task_id': None, 'message_id': mid}
266
273
  r = route(msg, store.snapshots(), float(cfg.get('attach_threshold', 0.42)))
267
274
  r = own_thread_only(store, msg, r)
268
275
  # ...and on a chat, the room it shares with the task is not a reason to join it
@@ -734,6 +741,22 @@ def is_chat(msg: dict) -> bool:
734
741
  return str(msg.get('channel') or '').lower() in CHAT_CHANNELS
735
742
 
736
743
 
744
+ # "hey" is not an ask. On WhatsApp it became a reply task with a drafted "Hey - what's up?", the
745
+ # real question arrived two lines later and attached to it, and the pipe offered the GREETING for
746
+ # approval (the 2026-09-03 break test). A chat opener waits for the sentence it opens: it is filed
747
+ # on the timeline, and the next line - which exchange_lines hands the reader as context - is the ask.
748
+ _OPENER = re.compile(r"^\s*(hi|hey+|hello+|yo|sup|hiya|morning|good (morning|afternoon|evening)|shalom|hey there|you there|u there"
749
+ r"|quick (q|question)|got a (sec|second|minute|min)|are you (there|around|free)|can i ask you something"
750
+ r"|knock knock|\W*)\W*$", re.I)
751
+
752
+ def is_opener(msg: dict) -> bool:
753
+ """A chat line that opens a conversation and asks nothing - the greeting before the point."""
754
+ if not is_chat(msg): return False
755
+ body = ' '.join(str(msg.get('body') or '').split())
756
+ if not body or len(body) > 60: return False
757
+ return bool(_OPENER.match(body))
758
+
759
+
737
760
  def _secs(a: str, b: str) -> float:
738
761
  """Seconds between two 'YYYY-MM-DD HH:MM:SS' stamps; inf when either is unreadable, so a
739
762
  missing timestamp never passes for "typed a moment ago"."""