taskuary 0.3.3.2__tar.gz → 0.3.3.3__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.2/taskuary.egg-info → taskuary-0.3.3.3}/PKG-INFO +29 -18
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/README.md +28 -17
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/pyproject.toml +1 -1
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/assistant.py +2 -2
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/clis.py +11 -1
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/concierge.py +46 -4
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/funnel.py +15 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/general.py +19 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/interview.py +37 -4
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/messengers.py +16 -1
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/reports.py +39 -7
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/server.py +100 -5
- taskuary-0.3.3.3/taskuary/skills/soul-interview/SKILL.md +92 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/store.py +13 -2
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/terminal.py +78 -7
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/wabridge.py +22 -5
- taskuary-0.3.3.2/taskuary/web/assets/GeneralWorkspace-C75zMBzq.js → taskuary-0.3.3.3/taskuary/web/assets/GeneralWorkspace-BpOeJv4L.js +1 -1
- taskuary-0.3.3.2/taskuary/web/assets/HubView-BtNf0bG5.js → taskuary-0.3.3.3/taskuary/web/assets/HubView-BhP6xVxh.js +1 -1
- taskuary-0.3.3.2/taskuary/web/assets/index-CE8to-uH.js → taskuary-0.3.3.3/taskuary/web/assets/index-B0RiuVsJ.js +113 -113
- taskuary-0.3.3.2/taskuary/web/assets/index-C_xWyjgP.css → taskuary-0.3.3.3/taskuary/web/assets/index-BVYz6QiM.css +1 -1
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/web/index.html +2 -2
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/whatsapp/bridge.mjs +73 -13
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/whatsapp/package.json +1 -1
- taskuary-0.3.3.3/taskuary/whatsapp/policy.mjs +51 -0
- taskuary-0.3.3.3/taskuary/whatsapp/policy.test.mjs +37 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3/taskuary.egg-info}/PKG-INFO +29 -18
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary.egg-info/SOURCES.txt +9 -4
- taskuary-0.3.3.3/tests/test_agent_identity.py +70 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_agent_source.py +10 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_assistant.py +14 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_floating_assistant.py +19 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_follow_up_verdict.py +11 -3
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_funnel.py +35 -0
- taskuary-0.3.3.3/tests/test_ignore_sender.py +90 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_live.py +22 -9
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_messengers.py +27 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_promptmap_and_catalog.py +12 -6
- taskuary-0.3.3.3/tests/test_report_order_and_research.py +109 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_soul_interview.py +51 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_terminal.py +12 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_wabridge.py +16 -4
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_waitroom.py +26 -0
- taskuary-0.3.3.2/taskuary/skills/soul-interview/SKILL.md +0 -57
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/LICENSE +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/setup.cfg +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/__init__.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/agents.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/aisetup.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/artifacts.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/aws.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/azure.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/blackboard.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/browserview.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/calendar.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/categories.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/channels.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/ci.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/cli.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/climodels.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/coder.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/compose.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/config.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/context.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/counsel.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/db.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/demo.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/deps.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/desktop.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/devtools.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/digest.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/docsync.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/evalset.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/evening.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/github.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/guard.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/handbook.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/handraise.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/histgen.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/hooks.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/hub.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/imapmail.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/imessage.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/ingest.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/intacct.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/invoice_workflow.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/knowledge.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/learn.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/learnedgraph.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/live.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/llm.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/logs.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/mcp.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/msauth.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/mssql.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/outbound.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/outbox.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/ownwork.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/phone.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/playbooks.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/pm.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/policy.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/problems.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/projects.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/promptmap.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/proof.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/proposals.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/quickbooks.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/rank.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/remote_assistant.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/research.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/reshape.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/responder.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/routing.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/scopes.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/selfclose.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/semantic.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/senders.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/session_artifacts.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/setup.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/sharepoint.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/sheets.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/spawn.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/teller.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/templates/coder.md +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/templates/counsel.md +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/templates/digest.md +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/templates/how-it-works.md +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/templates/learned.md +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/templates/playbook.md +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/templates/soul.md +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/templates/style.md +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/templates/triage.md +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/testing.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/toil.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/triage.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/update.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/verdicts.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/voice.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/waitroom.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/web/assets/GeneralWorkspace--IqTbJGE.css +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/web/favicon.ico +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/web/favicon.png +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/webguard.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/whoami.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/witness.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary/zoho.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary.egg-info/dependency_links.txt +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary.egg-info/entry_points.txt +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary.egg-info/requires.txt +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/taskuary.egg-info/top_level.txt +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_agent_environment.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_agent_wall.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_aisetup.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_answered_yourself.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_answers.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_api.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_assistant_opens.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_assistant_reactions.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_assistant_verdicts.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_async_triage.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_audit_chain.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_audit_fixes.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_aws_region.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_blackboard.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_browser_session.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_browserview.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_calendar.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_calendar_today.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_categories.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_cc.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_cc_triage.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_chat_doors.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_chat_is_not_one_task.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_chat_queue.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_climodels.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_clis.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_cloud.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_cloud_sources.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_compose.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_compose_sources.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_compose_workflows.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_concierge.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_config.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_context.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_core.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_counsel.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_cwlogs_paging.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_demo.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_deps.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_desktop.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_devtools.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_diff_review.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_digest_brief.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_docs_and_llm.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_docs_flow.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_duplicate_connectors.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_evening_brief.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_factory.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_feed_working.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_finished_chat_stays_finished.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_general.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_github_auto.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_guard.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_handbook.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_handbook_topics.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_handoff_channels.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_handraise.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_harvest.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_histgen.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_home_is_never_yours.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_imap_history.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_imapmail.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_imessage.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_intacct.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_invoice_workflow.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_kind_dispatch.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_knowledge.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_learnedgraph.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_lifecycle.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_local_file.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_mail_folders.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_mark_read.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_mcp.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_meeting_prep.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_message_thread.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_msauth.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_mssql.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_noop_ending.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_not_coding.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_note_retrieval.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_outbound_reports.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_own_rows_no_reply.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_playbooks.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_pm.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_poll_overlap.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_poll_parallel.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_poll_sources.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_pr_comments.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_problems.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_problems_dismiss.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_projects.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_proof_ci_proposals.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_pypi_downloads.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_quickbooks.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_rank.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_rate_limit.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_reclassify.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_remote_assistant.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_reply_channels.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_reply_voice.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_report_alerts.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_report_triage.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_research.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_reviews.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_scopes.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_seed_fits_a_tty.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_seeded_reports.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_semantic.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_send_targets.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_session_artifacts.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_setup.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_sheets_sharepoint.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_someone_else_replied.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_stay_open.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_stay_open_doors.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_store_app_cli.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_store_hotpaths.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_sync_clock.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_task_identity.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_teller.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_toil.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_triage_dataset.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_triage_images.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_update.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_update_refused.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_urgent_and_handoff.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_verdict_paths.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_verdict_sticks.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_vision.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_voice.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_watermark.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_web_entrypoint.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_webguard.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_whatsapp_documents.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_whatsapp_images.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_whoami.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/tests/test_withdrawn.py +0 -0
- {taskuary-0.3.3.2 → taskuary-0.3.3.3}/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.3
|
|
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,7 +62,7 @@ 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
|
|
|
@@ -71,9 +71,9 @@ Dynamic: license-file
|
|
|
71
71
|
Taskuary turns incoming messages into organized work. It sorts what matters, hands tasks to
|
|
72
72
|
your agents, and brings decisions back to you. Nothing sends or ships without your approval.
|
|
73
73
|
|
|
74
|
-

|
|
75
75
|
|
|
76
|
-
Taskuary is early—currently **v0.3.3.
|
|
76
|
+
Taskuary is early—currently **v0.3.3.3**—so breaking changes are still possible before 1.0.
|
|
77
77
|
|
|
78
78
|
<p align="center">
|
|
79
79
|
<a href="https://taskuary.com/demo/"><img
|
|
@@ -85,16 +85,27 @@ Taskuary is early—currently **v0.3.3.2**—so breaking changes are still possi
|
|
|
85
85
|
|
|
86
86
|
## What Taskuary can do
|
|
87
87
|
|
|
88
|
-
**
|
|
88
|
+
**Connect every system. Keep control.**
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
Bring your mail, chats, issue trackers, alerts, reports, and AI agents into one control center
|
|
91
|
+
that runs on your machine. Every input lands on a single Timeline with its context, status, and
|
|
92
|
+
available actions intact. Taskuary gives your tools one place to work together while you decide
|
|
93
|
+
what can run, what can send, and what needs your attention.
|
|
92
94
|
|
|
93
|
-

|
|
95
|
+

|
|
94
96
|
|
|
95
|
-
**
|
|
97
|
+
**Turn incoming work into tasks automatically**
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
AI triage reads every incoming item, understands what it means, and turns actionable requests
|
|
100
|
+
into ready-to-run tasks. Routine updates are filed, questions get draft replies, and urgent
|
|
101
|
+
decisions move to the front. Every choice comes with a reason, and you can change it at any time.
|
|
102
|
+
|
|
103
|
+
**Watch the work. Approve the outcome.**
|
|
104
|
+
|
|
105
|
+
Taskuary keeps the system moving and brings you in when your judgment matters. The Assistant
|
|
106
|
+
speaks up when a reply is waiting, a task has gone quiet, a report has failed, or a meeting is
|
|
107
|
+
about to start. Each update arrives once, with the evidence and next actions attached, so you can
|
|
108
|
+
review the work, make the call, and approve what happens next.
|
|
98
109
|
|
|
99
110
|
**Agents in your own repos**
|
|
100
111
|
|
|
@@ -105,25 +116,25 @@ Send coding work to Claude Code, Codex, Gemini, Cursor, Copilot, or another CLI.
|
|
|
105
116
|
Watch the terminal, answer questions, review changes, and use the built-in browser without
|
|
106
117
|
losing the session.
|
|
107
118
|
|
|
108
|
-
**
|
|
119
|
+
**Describe a check. Let it run itself.**
|
|
109
120
|
|
|
110
|
-
|
|
121
|
+
Say what you want watched in plain words and Taskuary builds it: read a folder of CSVs every
|
|
122
|
+
morning, total the AP bills due in the next 30 days, count helpdesk tickets by day. Reports read
|
|
123
|
+
and summarize, on a schedule or on demand, and can come back with an AI summary. Workflows write
|
|
124
|
+
data and keep state. Quiet checks stay quiet, so the only one you hear about is the one that
|
|
125
|
+
failed.
|
|
111
126
|
|
|
112
127
|
**You stay in control**
|
|
113
128
|
|
|
114
129
|
Replies and proposed actions wait in Review. You decide what sends, runs, closes, or gets dismissed.
|
|
115
130
|
|
|
116
|
-
**Catches what slipped**
|
|
117
|
-
|
|
118
|
-
Surface unanswered replies, upcoming meetings, quiet tasks, failed reports, and important changes.
|
|
119
|
-
|
|
120
131
|
## How work flows
|
|
121
132
|
|
|
122
133
|
### 1. Work arrives and gets sorted
|
|
123
134
|
|
|
124
135
|
Taskuary reads connected inboxes, separates tasks from noise, and shows the result on one Timeline.
|
|
125
136
|
|
|
126
|
-

|
|
137
|
+

|
|
127
138
|
|
|
128
139
|
### 2. An agent works on it
|
|
129
140
|
|
|
@@ -135,7 +146,7 @@ Tasks move from **Queued** to **Agent working**. The Board shows live progress a
|
|
|
135
146
|
|
|
136
147
|
Finished work, draft replies, questions, and loose ends return for review. You decide what to send, change, snooze, or dismiss.
|
|
137
148
|
|
|
138
|
-

|
|
139
150
|
|
|
140
151
|
## Install
|
|
141
152
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Taskuary
|
|
2
2
|
|
|
3
3
|
[](https://github.com/ldbumble/taskuary/actions/workflows/ci.yml)
|
|
4
|
-
[](https://pypi.org/project/taskuary/)
|
|
5
5
|
[](https://github.com/ldbumble/taskuary)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
Taskuary turns incoming messages into organized work. It sorts what matters, hands tasks to
|
|
11
11
|
your agents, and brings decisions back to you. Nothing sends or ships without your approval.
|
|
12
12
|
|
|
13
|
-

|
|
14
14
|
|
|
15
|
-
Taskuary is early—currently **v0.3.3.
|
|
15
|
+
Taskuary is early—currently **v0.3.3.3**—so breaking changes are still possible before 1.0.
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
18
|
<a href="https://taskuary.com/demo/"><img
|
|
@@ -24,16 +24,27 @@ Taskuary is early—currently **v0.3.3.2**—so breaking changes are still possi
|
|
|
24
24
|
|
|
25
25
|
## What Taskuary can do
|
|
26
26
|
|
|
27
|
-
**
|
|
27
|
+
**Connect every system. Keep control.**
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
Bring your mail, chats, issue trackers, alerts, reports, and AI agents into one control center
|
|
30
|
+
that runs on your machine. Every input lands on a single Timeline with its context, status, and
|
|
31
|
+
available actions intact. Taskuary gives your tools one place to work together while you decide
|
|
32
|
+
what can run, what can send, and what needs your attention.
|
|
31
33
|
|
|
32
|
-

|
|
34
|
+

|
|
33
35
|
|
|
34
|
-
**
|
|
36
|
+
**Turn incoming work into tasks automatically**
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
AI triage reads every incoming item, understands what it means, and turns actionable requests
|
|
39
|
+
into ready-to-run tasks. Routine updates are filed, questions get draft replies, and urgent
|
|
40
|
+
decisions move to the front. Every choice comes with a reason, and you can change it at any time.
|
|
41
|
+
|
|
42
|
+
**Watch the work. Approve the outcome.**
|
|
43
|
+
|
|
44
|
+
Taskuary keeps the system moving and brings you in when your judgment matters. The Assistant
|
|
45
|
+
speaks up when a reply is waiting, a task has gone quiet, a report has failed, or a meeting is
|
|
46
|
+
about to start. Each update arrives once, with the evidence and next actions attached, so you can
|
|
47
|
+
review the work, make the call, and approve what happens next.
|
|
37
48
|
|
|
38
49
|
**Agents in your own repos**
|
|
39
50
|
|
|
@@ -44,25 +55,25 @@ Send coding work to Claude Code, Codex, Gemini, Cursor, Copilot, or another CLI.
|
|
|
44
55
|
Watch the terminal, answer questions, review changes, and use the built-in browser without
|
|
45
56
|
losing the session.
|
|
46
57
|
|
|
47
|
-
**
|
|
58
|
+
**Describe a check. Let it run itself.**
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
Say what you want watched in plain words and Taskuary builds it: read a folder of CSVs every
|
|
61
|
+
morning, total the AP bills due in the next 30 days, count helpdesk tickets by day. Reports read
|
|
62
|
+
and summarize, on a schedule or on demand, and can come back with an AI summary. Workflows write
|
|
63
|
+
data and keep state. Quiet checks stay quiet, so the only one you hear about is the one that
|
|
64
|
+
failed.
|
|
50
65
|
|
|
51
66
|
**You stay in control**
|
|
52
67
|
|
|
53
68
|
Replies and proposed actions wait in Review. You decide what sends, runs, closes, or gets dismissed.
|
|
54
69
|
|
|
55
|
-
**Catches what slipped**
|
|
56
|
-
|
|
57
|
-
Surface unanswered replies, upcoming meetings, quiet tasks, failed reports, and important changes.
|
|
58
|
-
|
|
59
70
|
## How work flows
|
|
60
71
|
|
|
61
72
|
### 1. Work arrives and gets sorted
|
|
62
73
|
|
|
63
74
|
Taskuary reads connected inboxes, separates tasks from noise, and shows the result on one Timeline.
|
|
64
75
|
|
|
65
|
-

|
|
76
|
+

|
|
66
77
|
|
|
67
78
|
### 2. An agent works on it
|
|
68
79
|
|
|
@@ -74,7 +85,7 @@ Tasks move from **Queued** to **Agent working**. The Board shows live progress a
|
|
|
74
85
|
|
|
75
86
|
Finished work, draft replies, questions, and loose ends return for review. You decide what to send, change, snooze, or dismiss.
|
|
76
87
|
|
|
77
|
-

|
|
78
89
|
|
|
79
90
|
## Install
|
|
80
91
|
|
|
@@ -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.3"
|
|
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" }
|
|
@@ -537,7 +537,7 @@ def raised(store, days: float = 2) -> str:
|
|
|
537
537
|
return '\n'.join(out) or '(the assistant raised nothing in this window)'
|
|
538
538
|
|
|
539
539
|
|
|
540
|
-
def parse(text: str, cands: list, max_lines: int = MAX_LINES) -> list:
|
|
540
|
+
def parse(store, text: str, cands: list, max_lines: int = MAX_LINES) -> list:
|
|
541
541
|
"""The model's list, kept honest: a key it invents must be idea:*, a candidate key keeps its
|
|
542
542
|
kind and its buttons, and the text is the model's when it gave one. Every line keeps its WHY -
|
|
543
543
|
the hub's facts for a candidate (plus the model's read on them), the model's own for an idea -
|
|
@@ -767,7 +767,7 @@ def think(store, cands: list, llm, instruction: str = None, max_lines: int = MAX
|
|
|
767
767
|
from .llm import readable_images
|
|
768
768
|
images = readable_images(store, _people_context(store)[1])
|
|
769
769
|
text = llm(system, user, max_tokens=POST_TOKENS, **({'images': images} if images else {}))
|
|
770
|
-
return parse(text, cands, max_lines), _notes(text), user
|
|
770
|
+
return parse(store, text, cands, max_lines), _notes(text), user
|
|
771
771
|
|
|
772
772
|
|
|
773
773
|
def facts(store, watch_source_ids=None, watch_sources=None, systems_only: bool = False) -> str:
|
|
@@ -54,9 +54,19 @@ READONLY = {'claude': (('--dangerously-skip-permissions',), ('--tools', '')),
|
|
|
54
54
|
# classifier above, which gets no tools at all because the text it classifies is untrusted input.
|
|
55
55
|
# Claude's built-ins are named explicitly, and every MCP tool is denied because MCP permissions
|
|
56
56
|
# are separate from --tools and a connector may expose writes beside reads.
|
|
57
|
+
REPORT_TOOLS = 'Read,Glob,Grep,WebFetch,WebSearch'
|
|
58
|
+
|
|
59
|
+
# --tools and --allowedTools answer different questions and a report needs BOTH: --tools says which
|
|
60
|
+
# built-ins EXIST in this run, --allowedTools says which may be used without asking a human. With
|
|
61
|
+
# the bypass flag dropped and only --tools given, WebFetch and WebSearch existed but still prompted,
|
|
62
|
+
# and a headless report has nobody to click: every fetch came back "Claude requested permissions to
|
|
63
|
+
# use WebFetch, but you haven't granted it yet" and the run narrated the refusal instead of the
|
|
64
|
+
# trending page. Read/Glob/Grep never prompt, which is why the gap stayed invisible for two months
|
|
65
|
+
# (the GitHub Trending report, 2026-09-04). Granting the same five is not a widening - the set the
|
|
66
|
+
# run may use is still exactly the set that exists.
|
|
57
67
|
REPORT_READ = {
|
|
58
68
|
'claude': (('--dangerously-skip-permissions',),
|
|
59
|
-
('--tools',
|
|
69
|
+
('--tools', REPORT_TOOLS, '--allowedTools', REPORT_TOOLS, '--disallowedTools', 'mcp__*')),
|
|
60
70
|
'codex': READONLY['codex'],
|
|
61
71
|
'gemini': READONLY['gemini'],
|
|
62
72
|
}
|
|
@@ -81,8 +81,13 @@ _SAYS = (
|
|
|
81
81
|
(re.compile(r"\b(not (my|our) (issue|problem|job|task|thing)|not (ours|mine)|nothing to do with (me|us)|let them (handle|deal|sort|do)"
|
|
82
82
|
r"|ignore (it|this)|leave it (alone|be)|drop it|no need to (respond|reply|answer))\b", re.I), 'not_ours'),
|
|
83
83
|
(re.compile(r"\b(never|always) (again|file|ignore)|remember (this|that)|from now on\b", re.I), 'not_ours_remember'),
|
|
84
|
+
# "research" came out of this list: the other verbs here read as DIAGNOSING something that
|
|
85
|
+
# exists ("look into that server", "find out why the export drops rows"), but research is the
|
|
86
|
+
# word for reading about the WORLD - "can you research the Factor Elara gravel bike" opened a
|
|
87
|
+
# coding agent on a checkout (the owner, 2026-09-04). Left off the list it reaches the brain,
|
|
88
|
+
# which has the sentence and can tell a product from a production database.
|
|
84
89
|
(re.compile(r"\b(send (it |this |that )?to (the )?(coding agent|coder|codex|claude|gemini|agent)|have the (coder|agent)|let the (coder|agent)|start the (coder|agent)|code it|(coder|codex|claude) (should|can) (review|look|fix|handle)"
|
|
85
|
-
r"|look into|investigate|dig into|find out|figure out|
|
|
90
|
+
r"|look into|investigate|dig into|find out|figure out|check (on |out )?(that|the|this|what|why|if)|ask (the |an )?(assistant|agent|coder) to)\b", re.I), 'coder'),
|
|
86
91
|
(re.compile(r"\b(i'?ll (do|take|handle) (it|this|that)|i will (do|take|handle) (it|this|that)|mine|leave it (to|with) me|(just )?make it a task|create a task|(add|put) (it|this) (on|to) my list)\b", re.I), 'mine'),
|
|
87
92
|
# ENDING AN AGENT is not closing a task: "close the agent working" closed the item on the table
|
|
88
93
|
# instead - a task the owner had not even asked about (2026-09-03). It comes first, so the words
|
|
@@ -137,6 +142,7 @@ SYSTEM = (
|
|
|
137
142
|
"(hand it to the coding agent - put EVERYTHING the owner wants done after a colon, in their words: DECIDE: coder: find out "
|
|
138
143
|
"why the bulk-approve fix from before did not stick, and create an admin login for X), mine (they will do it themselves), "
|
|
139
144
|
"approve (send the drafted reply as it stands), not_ours_sender (the SENDER is noise - file everything from them from now on), "
|
|
145
|
+
"setup (reading, thinking or research with NO system to type at - Taskuary opens it as a conversation with the assistant: nothing is built, no repository is touched, and it can be handed to the coding agent later if it turns out something has to be), "
|
|
140
146
|
"split (this arrival is TWO jobs - Taskuary breaks it in two, each with its own ref), "
|
|
141
147
|
"setting (what they want is a SWITCH - Taskuary puts it in front of them to approve and never changes it itself), "
|
|
142
148
|
"stop_agent (end the AGENT that is running - stopping a session is not closing a task, and never guess which: "
|
|
@@ -149,6 +155,28 @@ SYSTEM = (
|
|
|
149
155
|
"have it wrong is the one thing you must not shrug off.\n"
|
|
150
156
|
"A question or a remark is not a decision: answer it and write no DECIDE line. A polite request is "
|
|
151
157
|
"not a question: \"can you look into that server\" is a hand-off, so decide it.\n"
|
|
158
|
+
# "can you research the Factor Elara gravel bike" opened a coding agent on a checkout (the
|
|
159
|
+
# owner, 2026-09-04), and before that a set-up did the same ("it doesn't need coding agent
|
|
160
|
+
# just a regular agent that will walk me through it"). The example above primes coder; this
|
|
161
|
+
# is the line that stops it generalising to everything phrased as a favour.
|
|
162
|
+
'...but coder and setup are not the same road, and the test is whether there is a SYSTEM to '
|
|
163
|
+
'type at. A repository, a server, a database, a query, a file, an error, a failing report: '
|
|
164
|
+
'coder. Reading about the world, comparing products, weighing an option, working out what to '
|
|
165
|
+
'ask, anything whose answer is a judgement rather than a change: setup. Never send reading '
|
|
166
|
+
'work to the coding agent because the sentence was polite - "can you research X" is a '
|
|
167
|
+
'walk-through, not a hand-off.\n'
|
|
168
|
+
# "ignore" names the act and leaves out the part that lasts. Filing one mail and silencing a
|
|
169
|
+
# sender for ever are different acts, and the owner asked to be consulted on which (2026-09-04:
|
|
170
|
+
# "even without the button when you say ignore unless you say specific just for today it
|
|
171
|
+
# should ask what it should do with it in terms of memory").
|
|
172
|
+
'"Ignore it" and "not ours" name the act but not its SCOPE, and scope is the part that lasts. '
|
|
173
|
+
'Unless they already said which - "just this once", "just for today", "never again", "always", '
|
|
174
|
+
'"from this sender" - do not pick one: say in one line that you can file this one, remember the '
|
|
175
|
+
'kind, or silence the sender, and end with exactly OPTIONS: just this once | this kind from now '
|
|
176
|
+
'on | everything from this sender. This is the one ambiguity worth a question, because the wide '
|
|
177
|
+
'answers are the ones that quietly hide mail later. When they HAVE said which, decide it and '
|
|
178
|
+
'write no options line: this once is not_ours, the kind is not_ours_remember, the sender is '
|
|
179
|
+
'not_ours_sender.\n'
|
|
152
180
|
"The thread you are given is the whole thread, the owner's own sent mail included. When it shows they "
|
|
153
181
|
"already answered, say so as a fact. Only when the thread has no answer from them may you say the mail "
|
|
154
182
|
"has not been read back yet - and then name the Sync button, never blame yourself for not seeing it.")
|
|
@@ -617,6 +645,18 @@ def parse_options(text: str) -> tuple[str, list]:
|
|
|
617
645
|
return (text[:m.start()].strip(), opts if len(opts) >= 2 else [])
|
|
618
646
|
|
|
619
647
|
|
|
648
|
+
def _verdict_why(item: dict) -> str:
|
|
649
|
+
"""Triage's own reason, with the verdict word it opens with trimmed off - "triage filed it as
|
|
650
|
+
fyi - triage: fyi - an automated notification" says it twice (the owner, 2026-09-04: "ask we
|
|
651
|
+
processed as fyi because of x")."""
|
|
652
|
+
w = (item.get('why') or '').strip()
|
|
653
|
+
for pre in ('triage: fyi -', 'triage: fyi', 'triage:'):
|
|
654
|
+
if w.lower().startswith(pre):
|
|
655
|
+
w = w[len(pre):].lstrip(' -·')
|
|
656
|
+
break
|
|
657
|
+
return f' - {w}' if w else ''
|
|
658
|
+
|
|
659
|
+
|
|
620
660
|
def fallback(item: dict | None, opening: bool, pile_items: list = None) -> str:
|
|
621
661
|
"""No model: the facts in the same three beats - where from, what was done, what you need to do."""
|
|
622
662
|
if not item:
|
|
@@ -632,9 +672,11 @@ def fallback(item: dict | None, opening: bool, pile_items: list = None) -> str:
|
|
|
632
672
|
frm = f"{item.get('who') or 'Someone'} wrote on {item.get('channel') or 'email'}" + (f" ({funnel_age(item)})" if funnel_age(item) else '') + f": \"{item['title']}\""
|
|
633
673
|
done = (f"the agent {item['summary']}" if item.get('summary') else
|
|
634
674
|
'triage judged it a reply to write' if item['kind'] == 'review' else 'an agent proposed an action' if item['kind'] == 'action' else
|
|
635
|
-
'a coding task with no agent on it yet' if item.get('coding') else 'nothing has been done with it yet' if item['kind'] in ('asked', 'todo')
|
|
675
|
+
'a coding task with no agent on it yet' if item.get('coding') else 'nothing has been done with it yet' if item['kind'] in ('asked', 'todo')
|
|
676
|
+
else f'triage filed it as fyi{_verdict_why(item)}')
|
|
636
677
|
need = ('approve the draft below, or redraft it' if item['kind'] == 'review' else 'say whether it may run' if item['kind'] == 'action' else
|
|
637
|
-
'reply, hand it to the coding agent, or say it is not ours' if item['kind'] in ('asked', 'todo')
|
|
678
|
+
'reply, hand it to the coding agent, or say it is not ours' if item['kind'] in ('asked', 'todo')
|
|
679
|
+
else 'nothing has to happen - make it a task, tell me to ignore this sender, or move on')
|
|
638
680
|
return f"{frm}. Since then: {done}. From you: {need}."
|
|
639
681
|
if item['kind'] == 'agent':
|
|
640
682
|
return f"{item.get('agent') or 'An agent'} on {item.get('ref') or item['title']} " + (f"asked: {item['tail'][-1]}" if item.get('asking') and item.get('tail') else 'stopped at its prompt') + ' - answer it below.'
|
|
@@ -644,7 +686,7 @@ def fallback(item: dict | None, opening: bool, pile_items: list = None) -> str:
|
|
|
644
686
|
return f"{item['title']}" + (f" ({item.get('who')})" if item.get('who') else '') + f" - {item.get('why') or 'the assistant raised this'}. Draft the follow-up, make it a task, or let it go."
|
|
645
687
|
if item['kind'] == 'meeting': return f"{item['title']} is {item.get('why')}. Prep me, or move on."
|
|
646
688
|
if item['kind'] == 'report':
|
|
647
|
-
return f"{item['title']} landed {funnel_age(item)}" + (' and FAILED - the cause is in it.' if item.get('bad') else '.') + '
|
|
689
|
+
return f"{item['title']} landed {funnel_age(item)}" + (' and FAILED - the cause is in it.' if item.get('bad') else '.') + ' It is open below - run it again, or move on.'
|
|
648
690
|
if item['kind'] == 'agentdone': return f"{item.get('who') or 'The agent'} finished {item.get('ref') or item['title']}" + (f": {item['summary']}" if item.get('summary') else '.') + ' Want to see the final report?'
|
|
649
691
|
lead = {'agent': f"{item.get('agent') or 'An agent'} is waiting on you on {item.get('ref') or item['title']}.",
|
|
650
692
|
'meeting': f"{item['title']} is {item.get('why')}.",
|
|
@@ -634,6 +634,20 @@ def came_in(i: dict) -> bool:
|
|
|
634
634
|
FYI_BATCH = 4 # fyi comes as a handful, not one at a time - there is nothing to do with each
|
|
635
635
|
BLOCKED_AGAIN_MIN = 30 # an agent still waiting comes out again this long after it was shown
|
|
636
636
|
|
|
637
|
+
def _not_yet(i: dict) -> bool:
|
|
638
|
+
"""On the timeline, but nothing to say about it YET - the walk skips it and comes back.
|
|
639
|
+
|
|
640
|
+
A meeting enters the pipe two hours out (SOON_MIN) so the day is visible, and its lane is
|
|
641
|
+
'time', which puts it at the very front of the walk. So the assistant opened with a meeting
|
|
642
|
+
that was still an hour and a half away, ahead of mail that wanted answering now. It is the same
|
|
643
|
+
shape as an agent that is mid-run: real, on the board, and not yours to do anything about until
|
|
644
|
+
it stops (the owner, 2026-09-04: "meetings should not go down into the chat until 15 minutes
|
|
645
|
+
before like a agent in middel of working"). alerts() already drew this line at ALERT_MIN; the
|
|
646
|
+
walk did not.
|
|
647
|
+
"""
|
|
648
|
+
return i['kind'] == 'meeting' and i.get('mins') is not None and i['mins'] > ALERT_MIN
|
|
649
|
+
|
|
650
|
+
|
|
637
651
|
def next_item(store, key: str = None, only: str = None) -> dict | None:
|
|
638
652
|
"""What comes out of the mouth: the named item (read or not - the chat may return to it), or
|
|
639
653
|
the first unread one - of the mail alone when `only` is 'mail'. Something still being triaged is
|
|
@@ -642,6 +656,7 @@ def next_item(store, key: str = None, only: str = None) -> dict | None:
|
|
|
642
656
|
if key: return next((i for i in build(store, keep_surfaced=True)['items'] if i['key'] == key), None) or batch_item(store, key)
|
|
643
657
|
again = (datetime.now() - timedelta(minutes=BLOCKED_AGAIN_MIN)).strftime('%Y-%m-%d %H:%M:%S')
|
|
644
658
|
ready = [i for i in pile(store, force=True)['items'] if not i.get('settling') and i['lane'] != 'working'
|
|
659
|
+
and not _not_yet(i)
|
|
645
660
|
and (not i.get('surfaced') or _ts(i.get('surfaced_at')) <= again)]
|
|
646
661
|
if only == 'mail': ready = [i for i in ready if came_in(i) or i['kind'] in INTERRUPTS]
|
|
647
662
|
return ready[0] if ready else None
|
|
@@ -172,6 +172,25 @@ def dock_task(store, actor='owner') -> tuple[dict, bool]:
|
|
|
172
172
|
return store.get_task(tid), True
|
|
173
173
|
|
|
174
174
|
|
|
175
|
+
def retire_dock(store, actor='owner'):
|
|
176
|
+
"""Close the current dock conversation so the next caller is handed a fresh one - what the
|
|
177
|
+
'New chat' button does, minus the live session there is none of in a just-started process.
|
|
178
|
+
|
|
179
|
+
A relaunch should open a NEW chat, not resume yesterday's (the owner, 2026-09-04: "after you
|
|
180
|
+
restart a new chat should be there not same chat"): the dock task id lives in settings, so
|
|
181
|
+
without this the same conversation - and the same model session - came back forever. A dock
|
|
182
|
+
nobody said anything in is left alone, or every launch would archive an empty guide task."""
|
|
183
|
+
raw = store.get_settings().get('assistant_dock_task_id')
|
|
184
|
+
task = store.get_task(int(raw)) if str(raw or '').isdigit() else None
|
|
185
|
+
if not (task and is_dock(task) and task.get('Status') not in ('done', 'dropped')): return None
|
|
186
|
+
if not history(store, task['TaskId']): return None
|
|
187
|
+
from . import concierge
|
|
188
|
+
store.update_task(task['TaskId'], {'Status': 'done'}, actor)
|
|
189
|
+
store.audit('task', task['TaskId'], 'archive_assistant_dock', actor)
|
|
190
|
+
store.set_setting(f"{concierge.SID_KEY}:{task['TaskId']}", '', actor)
|
|
191
|
+
return task['TaskId']
|
|
192
|
+
|
|
193
|
+
|
|
175
194
|
def dock_snapshot(store) -> str:
|
|
176
195
|
"""A compact, live map of the surfaces the hovering assistant is meant to explain.
|
|
177
196
|
|
|
@@ -17,13 +17,15 @@ SKILL_FILE = Path(__file__).parent / 'skills' / 'soul-interview' / 'SKILL.md'
|
|
|
17
17
|
_LEGACY_LABELS = {
|
|
18
18
|
'who': 'Who are you, and what is your work?',
|
|
19
19
|
'work': 'What kind of work reaches you?',
|
|
20
|
-
'task': 'What
|
|
21
|
-
'never': '
|
|
20
|
+
'task': 'What kind of work is worth turning into a task?',
|
|
21
|
+
'never': 'Which ambiguities or sensitive situations need your judgment?',
|
|
22
22
|
'people': 'Which people and relationships matter?',
|
|
23
23
|
'systems': 'Which systems or sources of truth matter?',
|
|
24
24
|
'voice': 'How should communication sound?',
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
_MANAGED_HEADINGS = ('## Connected systems', '## Project relationships', '## Repository map')
|
|
28
|
+
|
|
27
29
|
|
|
28
30
|
def skill_text() -> str:
|
|
29
31
|
"""The portable interview contract used for both questioning and writing."""
|
|
@@ -45,10 +47,35 @@ def _known(ctx: dict) -> str:
|
|
|
45
47
|
bits = [f"Owner name on file: {ctx['owner'] or '(not set)'}",
|
|
46
48
|
f"Connected sources: {', '.join(ctx['channels']) or 'none yet'}",
|
|
47
49
|
f"Known repositories (mention only if relevant): {', '.join(ctx['repos']) or 'none'}",
|
|
48
|
-
f"Frequent correspondents
|
|
50
|
+
f"Frequent correspondents (activity only; not proof of importance or authority): "
|
|
51
|
+
f"{'; '.join(ctx['writes_most']) or 'nothing ingested yet'}"]
|
|
49
52
|
return 'WHAT TASKUARY ALREADY KNOWS (facts; do not contradict them):\n' + '\n'.join(bits)
|
|
50
53
|
|
|
51
54
|
|
|
55
|
+
def _section(text: str, heading: str) -> str:
|
|
56
|
+
"""One level-two section, through the next level-two heading or end of document."""
|
|
57
|
+
match = re.search(rf'(?ms)^{re.escape(heading)}[^\n]*\n.*?(?=^##\s|\Z)', str(text or ''))
|
|
58
|
+
return match.group(0).rstrip() if match else ''
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _without_section(text: str, heading: str) -> str:
|
|
62
|
+
return re.sub(rf'(?ms)^\s*{re.escape(heading)}[^\n]*\n.*?(?=^##\s|\Z)', '', str(text or '')).strip()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _carry_managed_sections(current: str, drafted: str) -> str:
|
|
66
|
+
"""Keep machine-maintained operational memory out of the model's authorship.
|
|
67
|
+
|
|
68
|
+
The interview rewrites the owner's prose. Connected systems and repository notes are updated
|
|
69
|
+
elsewhere, while project/person edges live in SQLite and are rendered after the save. A model
|
|
70
|
+
draft must therefore be unable to erase or fabricate any of those sections.
|
|
71
|
+
"""
|
|
72
|
+
body = str(drafted or '')
|
|
73
|
+
for heading in _MANAGED_HEADINGS:
|
|
74
|
+
body = _without_section(body, heading)
|
|
75
|
+
carried = [_section(current, heading) for heading in ('## Connected systems', '## Repository map')]
|
|
76
|
+
return '\n\n'.join([body.rstrip()] + [block for block in carried if block]).strip()
|
|
77
|
+
|
|
78
|
+
|
|
52
79
|
def _answers(value) -> list[dict]:
|
|
53
80
|
"""Normalize adaptive transcripts and the previous seven-field request shape."""
|
|
54
81
|
if isinstance(value, list):
|
|
@@ -146,8 +173,14 @@ def draft(store, answers, llm=None) -> str:
|
|
|
146
173
|
|
|
147
174
|
def write(store, answers, actor: str = 'owner', llm=None) -> str:
|
|
148
175
|
"""Draft, save, and audit an owner-controlled SOUL.md."""
|
|
149
|
-
body = draft(store, answers, llm)
|
|
176
|
+
body = _carry_managed_sections(store.get_doc('soul') or '', draft(store, answers, llm))
|
|
150
177
|
store.save_doc('soul', body, actor)
|
|
178
|
+
# Structured project/person memory is not interview prose. Re-render it from the database
|
|
179
|
+
# after every interview, and refresh the carried connection block from current configuration.
|
|
180
|
+
from .docsync import sync_connections, sync_projects
|
|
181
|
+
sync_connections(store, actor)
|
|
182
|
+
sync_projects(store, actor)
|
|
183
|
+
body = store.get_doc('soul') or body
|
|
151
184
|
transcript = _answers(answers)
|
|
152
185
|
store.audit('doc', 0, 'soul_interview', actor,
|
|
153
186
|
detail={'answered': sum(bool(row['a']) for row in transcript),
|
|
@@ -201,7 +201,8 @@ def wa_status(c) -> dict:
|
|
|
201
201
|
jid = str(st.get('jid') or '')
|
|
202
202
|
out = {'connected': bool(st.get('connected')), 'me': st.get('me') or '', 'jid': jid,
|
|
203
203
|
'phone': ('+' + jid.split('@')[0].split(':')[0]) if jid and jid.split(':')[0].split('@')[0].isdigit() else '', # the paired number, from the account jid
|
|
204
|
-
'pairing_code': st.get('pairingCode') or '', 'qr_svg': ''
|
|
204
|
+
'pairing_code': st.get('pairingCode') or '', 'qr_svg': '',
|
|
205
|
+
'reconnect': st.get('reconnect') or {}, 'filter': st.get('filter') or {}}
|
|
205
206
|
if st.get('qr') and not out['connected']:
|
|
206
207
|
import segno
|
|
207
208
|
out['qr_svg'] = segno.make(st['qr'], error='m').svg_data_uri(scale=5, border=2, dark='#1e1e2e', light='#ffffff')
|
|
@@ -222,6 +223,13 @@ def wa_chats(c) -> list:
|
|
|
222
223
|
r['n'] += 1
|
|
223
224
|
if not m.get('fromMe') and m.get('name'): r['name'] = m['name'] # the other side's push name, never ours
|
|
224
225
|
if (m.get('ts') or 0) >= r['last']: r['last'], r['snippet'] = m.get('ts') or 0, (m.get('text') or '')[:80]
|
|
226
|
+
# The bridge sees the routing JID before deciding whether to decrypt. That lets the source
|
|
227
|
+
# picker show a newly active chat without retaining its sender, text, or media first.
|
|
228
|
+
for m in out.get('blockedChats', []):
|
|
229
|
+
jid = m.get('jid') or ''
|
|
230
|
+
if not jid or jid.endswith('@broadcast') or jid in by: continue
|
|
231
|
+
by[jid] = {'jid': jid, 'group': bool(m.get('group')), 'name': '', 'n': 0,
|
|
232
|
+
'last': m.get('last') or 0, 'snippet': 'not opened - chat is not authorized'}
|
|
225
233
|
rows = sorted(by.values(), key=lambda r: -r['last'])
|
|
226
234
|
for r in rows: r['last'] = datetime.fromtimestamp(r['last']).strftime('%Y-%m-%d %H:%M') if r['last'] else ''
|
|
227
235
|
return rows
|
|
@@ -254,6 +262,13 @@ def poll_whatsapp(store, c, sources: list, llm=None, file_only=False) -> int:
|
|
|
254
262
|
srcs = [s for s in sources if s.get('Channel', 'whatsapp') == 'whatsapp' and s.get('Address')]
|
|
255
263
|
star = any(s['Address'] == '*' for s in srcs)
|
|
256
264
|
want = {s['Address'] for s in srcs if s['Address'] != '*'}
|
|
265
|
+
notify_chat = str(cfg.get('notify_chat') or '').strip()
|
|
266
|
+
bridge_want = want | ({notify_chat} if notify_chat else set()) # approvals/assistant may not be an inbound source
|
|
267
|
+
# Filtering here alone was too late: Baileys had already decrypted and downloaded media from
|
|
268
|
+
# every chat. Give the same policy to its pre-decryption hook before asking for messages.
|
|
269
|
+
try: _wa(c, '/filter', {'allDirect': star, 'jids': sorted(bridge_want)})
|
|
270
|
+
except RuntimeError as e:
|
|
271
|
+
if '(404)' not in str(e): raise # an older detached bridge: keep polling until its next restart
|
|
257
272
|
out = _wa(c, f"/messages?after={int(cfg.get('wa_seq') or 0)}")
|
|
258
273
|
n, took = 0, []
|
|
259
274
|
from . import phone, remote_assistant
|
|
@@ -750,10 +750,16 @@ CONNECTION_OF = {'mssql': mssql_connection, 'winrm': winrm_connection, 'database
|
|
|
750
750
|
'google_sheets': _sheets_connection}
|
|
751
751
|
|
|
752
752
|
|
|
753
|
+
# Report types whose data IS the store - they reach no further than the local database, so they
|
|
754
|
+
# can neither be slow nor unreachable. Everything else dials out: a SQL box, an API, a share.
|
|
755
|
+
# run_due_reports runs these FIRST for that reason, so keep the two uses of this list together.
|
|
756
|
+
STORE_BACKED = ('digest', 'evening_inbox', 'automate', 'assistant', 'agent', 'calendar', 'kb_search', 'kb_reindex',
|
|
757
|
+
'metric', 'metric_check', 'handbook_search', 'handbook_write', 'handbook_vote',
|
|
758
|
+
'hub_search', 'hub_write', 'hub_vote', 'hub_comment')
|
|
759
|
+
|
|
760
|
+
|
|
753
761
|
def resolve_cfg(store, cfg: dict) -> dict:
|
|
754
|
-
if cfg.get('type') in
|
|
755
|
-
'metric', 'metric_check', 'handbook_search', 'handbook_write', 'handbook_vote',
|
|
756
|
-
'hub_search', 'hub_write', 'hub_vote', 'hub_comment'):
|
|
762
|
+
if cfg.get('type') in STORE_BACKED:
|
|
757
763
|
return {**cfg, 'store': store} # their data IS the store (the agent's: its profile; the calendar's: the cards; the knowledge base: its index)
|
|
758
764
|
conn = CONNECTION_OF.get(cfg.get('type'))
|
|
759
765
|
if conn:
|
|
@@ -1239,15 +1245,41 @@ def deliver_report(store, src: dict, cfg: dict, subject: str, body: str) -> dict
|
|
|
1239
1245
|
return {'gate': 'auto', 'message_id': mid, 'sent': sent}
|
|
1240
1246
|
|
|
1241
1247
|
|
|
1248
|
+
def _own_data(src: dict) -> bool:
|
|
1249
|
+
"""Does this report read nothing but the store? An unknown type counts as dialling out, which
|
|
1250
|
+
is the conservative half of the split - it waits behind the local ones rather than ahead."""
|
|
1251
|
+
try: return (json.loads(src.get('ConfigJson') or '{}').get('type') or 'rest') in STORE_BACKED
|
|
1252
|
+
except ValueError: return False
|
|
1253
|
+
|
|
1254
|
+
|
|
1242
1255
|
def run_due_reports(store, startup: bool = False) -> int:
|
|
1256
|
+
"""Every due report, own-data ones FIRST and each one walled off from the others.
|
|
1257
|
+
|
|
1258
|
+
On a startup catch-up these run one after another, so a source that is merely unreachable used
|
|
1259
|
+
to hold up everything behind it: a SQL Server that was not there spent 41 s in an ODBC login
|
|
1260
|
+
timeout, and the owner's Morning digest and Assistant post - which read nothing but Taskuary's
|
|
1261
|
+
own database - waited it out (the owner, 2026-09-04: the first item took ~3 minutes). Ordering
|
|
1262
|
+
the local ones first means what they actually read lands while the dial-out ones take their time.
|
|
1263
|
+
|
|
1264
|
+
And one report may not cost the others their run. A failing executor files a FAILED row and
|
|
1265
|
+
returns, but a runner that RAISES used to abort the whole loop, silently skipping every report
|
|
1266
|
+
after it and leaving them un-touched so the next poll started over.
|
|
1267
|
+
"""
|
|
1243
1268
|
from .llm import build_llm
|
|
1244
1269
|
try: llm = build_llm(store)
|
|
1245
1270
|
except Exception: llm = None
|
|
1271
|
+
due = [s for s in store.list_sources() if s['Channel'] == 'report'
|
|
1272
|
+
and is_due(json.loads(s.get('ConfigJson') or '{}'), s.get('LastPolledAt'), startup)]
|
|
1246
1273
|
n = 0
|
|
1247
|
-
for src in
|
|
1248
|
-
|
|
1249
|
-
if is_due(json.loads(src.get('ConfigJson') or '{}'), src.get('LastPolledAt'), startup):
|
|
1274
|
+
for src in sorted(due, key=lambda s: not _own_data(s)):
|
|
1275
|
+
try:
|
|
1250
1276
|
run_report_source(store, src, llm)
|
|
1251
|
-
store.touch_source(src['SourceId'])
|
|
1252
1277
|
n += 1
|
|
1278
|
+
except Exception as e:
|
|
1279
|
+
# touched anyway: a report that blew up must wait for its next slot like any other,
|
|
1280
|
+
# or a poll every few minutes re-runs the broken one for ever
|
|
1281
|
+
logger.warning(f"report {src.get('Address')} raised, skipping it and running the rest - {e}")
|
|
1282
|
+
finally:
|
|
1283
|
+
try: store.touch_source(src['SourceId'])
|
|
1284
|
+
except Exception as e: logger.warning(f"could not touch source {src['SourceId']}: {e}")
|
|
1253
1285
|
return n
|