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.
- {taskuary-0.3.3.0/taskuary.egg-info → taskuary-0.3.3.2}/PKG-INFO +61 -57
- taskuary-0.3.3.2/README.md +140 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/pyproject.toml +1 -1
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/concierge.py +27 -8
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/funnel.py +86 -20
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/ingest.py +23 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/reports.py +8 -1
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/server.py +17 -2
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/coder.md +9 -3
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/triage.md +2 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/terminal.py +74 -4
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/triage.py +4 -0
- taskuary-0.3.3.0/taskuary/web/assets/GeneralWorkspace-Duk0uAEw.js → taskuary-0.3.3.2/taskuary/web/assets/GeneralWorkspace-C75zMBzq.js +1 -1
- taskuary-0.3.3.0/taskuary/web/assets/HubView-DAcJFCZG.js → taskuary-0.3.3.2/taskuary/web/assets/HubView-BtNf0bG5.js +1 -1
- taskuary-0.3.3.2/taskuary/web/assets/index-CE8to-uH.js +1140 -0
- taskuary-0.3.3.2/taskuary/web/assets/index-C_xWyjgP.css +32 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/web/index.html +2 -2
- {taskuary-0.3.3.0 → taskuary-0.3.3.2/taskuary.egg-info}/PKG-INFO +61 -57
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary.egg-info/SOURCES.txt +4 -4
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_assistant_reactions.py +128 -2
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_digest_brief.py +10 -2
- taskuary-0.3.3.0/README.md +0 -136
- taskuary-0.3.3.0/taskuary/web/assets/index-DGWFy6LS.js +0 -1128
- taskuary-0.3.3.0/taskuary/web/assets/index-DYyvp4cD.css +0 -32
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/LICENSE +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/setup.cfg +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/__init__.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/agents.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/aisetup.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/artifacts.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/assistant.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/aws.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/azure.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/blackboard.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/browserview.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/calendar.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/categories.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/channels.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/ci.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/cli.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/climodels.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/clis.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/coder.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/compose.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/config.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/context.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/counsel.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/db.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/demo.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/deps.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/desktop.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/devtools.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/digest.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/docsync.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/evalset.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/evening.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/general.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/github.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/guard.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/handbook.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/handraise.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/histgen.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/hooks.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/hub.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/imapmail.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/imessage.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/intacct.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/interview.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/invoice_workflow.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/knowledge.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/learn.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/learnedgraph.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/live.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/llm.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/logs.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/mcp.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/messengers.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/msauth.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/mssql.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/outbound.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/outbox.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/ownwork.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/phone.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/playbooks.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/pm.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/policy.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/problems.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/projects.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/promptmap.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/proof.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/proposals.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/quickbooks.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/rank.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/remote_assistant.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/research.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/reshape.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/responder.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/routing.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/scopes.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/selfclose.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/semantic.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/senders.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/session_artifacts.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/setup.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/sharepoint.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/sheets.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/skills/soul-interview/SKILL.md +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/spawn.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/store.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/teller.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/counsel.md +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/digest.md +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/how-it-works.md +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/learned.md +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/playbook.md +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/soul.md +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/templates/style.md +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/testing.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/toil.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/update.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/verdicts.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/voice.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/wabridge.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/waitroom.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/web/assets/GeneralWorkspace--IqTbJGE.css +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/web/favicon.ico +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/web/favicon.png +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/webguard.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/whatsapp/bridge.mjs +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/whatsapp/package.json +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/whoami.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/witness.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary/zoho.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary.egg-info/dependency_links.txt +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary.egg-info/entry_points.txt +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary.egg-info/requires.txt +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/taskuary.egg-info/top_level.txt +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_agent_environment.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_agent_source.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_agent_wall.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_aisetup.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_answered_yourself.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_answers.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_api.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_assistant.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_assistant_opens.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_assistant_verdicts.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_async_triage.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_audit_chain.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_audit_fixes.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_aws_region.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_blackboard.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_browser_session.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_browserview.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_calendar.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_calendar_today.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_categories.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_cc.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_cc_triage.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_chat_doors.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_chat_is_not_one_task.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_chat_queue.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_climodels.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_clis.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_cloud.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_cloud_sources.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_compose.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_compose_sources.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_compose_workflows.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_concierge.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_config.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_context.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_core.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_counsel.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_cwlogs_paging.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_demo.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_deps.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_desktop.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_devtools.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_diff_review.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_docs_and_llm.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_docs_flow.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_duplicate_connectors.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_evening_brief.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_factory.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_feed_working.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_finished_chat_stays_finished.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_floating_assistant.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_follow_up_verdict.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_funnel.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_general.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_github_auto.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_guard.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_handbook.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_handbook_topics.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_handoff_channels.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_handraise.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_harvest.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_histgen.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_home_is_never_yours.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_imap_history.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_imapmail.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_imessage.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_intacct.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_invoice_workflow.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_kind_dispatch.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_knowledge.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_learnedgraph.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_lifecycle.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_live.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_local_file.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_mail_folders.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_mark_read.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_mcp.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_meeting_prep.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_message_thread.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_messengers.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_msauth.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_mssql.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_noop_ending.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_not_coding.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_note_retrieval.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_outbound_reports.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_own_rows_no_reply.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_playbooks.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_pm.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_poll_overlap.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_poll_parallel.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_poll_sources.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_pr_comments.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_problems.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_problems_dismiss.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_projects.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_promptmap_and_catalog.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_proof_ci_proposals.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_pypi_downloads.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_quickbooks.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_rank.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_rate_limit.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_reclassify.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_remote_assistant.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_reply_channels.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_reply_voice.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_report_alerts.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_report_triage.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_research.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_reviews.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_scopes.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_seed_fits_a_tty.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_seeded_reports.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_semantic.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_send_targets.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_session_artifacts.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_setup.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_sheets_sharepoint.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_someone_else_replied.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_soul_interview.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_stay_open.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_stay_open_doors.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_store_app_cli.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_store_hotpaths.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_sync_clock.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_task_identity.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_teller.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_terminal.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_toil.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_triage_dataset.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_triage_images.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_update.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_update_refused.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_urgent_and_handoff.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_verdict_paths.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_verdict_sticks.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_vision.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_voice.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_wabridge.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_waitroom.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_watermark.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_web_entrypoint.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_webguard.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_whatsapp_documents.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_whatsapp_images.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_whoami.py +0 -0
- {taskuary-0.3.3.0 → taskuary-0.3.3.2}/tests/test_withdrawn.py +0 -0
- {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.
|
|
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
|
[](https://github.com/ldbumble/taskuary/actions/workflows/ci.yml)
|
|
65
|
-
[](https://pypi.org/project/taskuary/)
|
|
66
66
|
[](https://github.com/ldbumble/taskuary)
|
|
67
67
|
[](LICENSE)
|
|
68
68
|
|
|
69
|
-
## Your inbox, staffed by AI agents
|
|
69
|
+
## Your inbox, staffed by AI agents
|
|
70
70
|
|
|
71
|
-
Taskuary
|
|
72
|
-
|
|
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
|
-

|
|
77
75
|
|
|
78
|
-
Taskuary is early—currently **v0.3.3.
|
|
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
|
|
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
|
-
##
|
|
86
|
+
## What Taskuary can do
|
|
93
87
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|

|
|
100
94
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
and
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
-
|
|
168
|
-
|
|
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
|

|
|
179
187
|
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
[](https://github.com/ldbumble/taskuary/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/taskuary/)
|
|
5
|
+
[](https://github.com/ldbumble/taskuary)
|
|
6
|
+
[](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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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.
|
|
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,
|
|
1010
|
-
|
|
1011
|
-
words, so a rule can never quietly grow into "everything from
|
|
1012
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1252
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
#
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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))
|
|
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))
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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"."""
|