taskuary 0.3.2.2__tar.gz → 0.3.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {taskuary-0.3.2.2/taskuary.egg-info → taskuary-0.3.2.4}/PKG-INFO +26 -2
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/README.md +25 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/pyproject.toml +1 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/aisetup.py +1 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/artifacts.py +64 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/assistant.py +129 -5
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/categories.py +7 -2
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/channels.py +11 -3
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/ci.py +1 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/cli.py +70 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/coder.py +98 -4
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/compose.py +2 -2
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/config.py +42 -2
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/db.py +1 -1
- taskuary-0.3.2.4/taskuary/demo.py +481 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/docsync.py +1 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/evalset.py +1 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/general.py +64 -1
- taskuary-0.3.2.4/taskuary/guard.py +101 -0
- taskuary-0.3.2.4/taskuary/handbook.py +271 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/histgen.py +1 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/hooks.py +10 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/ingest.py +39 -14
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/llm.py +11 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/messengers.py +12 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/outbound.py +7 -3
- taskuary-0.3.2.4/taskuary/outbox.py +164 -0
- taskuary-0.3.2.4/taskuary/ownwork.py +85 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/problems.py +2 -2
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/reports.py +39 -6
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/research.py +3 -3
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/routing.py +8 -3
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/scopes.py +10 -1
- taskuary-0.3.2.4/taskuary/selfclose.py +247 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/server.py +301 -64
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/setup.py +14 -7
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/store.py +196 -34
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/coder.md +25 -2
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/triage.md +7 -3
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/terminal.py +94 -6
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/triage.py +35 -14
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/voice.py +2 -2
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/waitroom.py +7 -2
- taskuary-0.3.2.4/taskuary/web/assets/GeneralWorkspace-SrMAdua9.js +12 -0
- taskuary-0.3.2.4/taskuary/web/assets/SocialView-BV2-O2sP.js +1 -0
- taskuary-0.3.2.4/taskuary/web/assets/index-CJp5GTAb.js +1013 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/web/index.html +1 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/whatsapp/bridge.mjs +12 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4/taskuary.egg-info}/PKG-INFO +26 -2
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary.egg-info/SOURCES.txt +25 -2
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_agent_wall.py +10 -2
- taskuary-0.3.2.4/tests/test_answered_yourself.py +91 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_api.py +2 -1
- taskuary-0.3.2.4/tests/test_assistant_verdicts.py +85 -0
- taskuary-0.3.2.4/tests/test_chat_doors.py +152 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_compose.py +1 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_compose_sources.py +1 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_config.py +7 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_core.py +13 -0
- taskuary-0.3.2.4/tests/test_demo.py +148 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_digest_brief.py +13 -4
- taskuary-0.3.2.4/tests/test_finished_chat_stays_finished.py +85 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_general.py +54 -0
- taskuary-0.3.2.4/tests/test_guard.py +104 -0
- taskuary-0.3.2.4/tests/test_handbook.py +120 -0
- taskuary-0.3.2.4/tests/test_handbook_topics.py +76 -0
- taskuary-0.3.2.4/tests/test_home_is_never_yours.py +70 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_kind_dispatch.py +23 -6
- taskuary-0.3.2.4/tests/test_meeting_prep.py +75 -0
- taskuary-0.3.2.4/tests/test_message_thread.py +93 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_noop_ending.py +12 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_not_coding.py +26 -6
- taskuary-0.3.2.4/tests/test_own_rows_no_reply.py +40 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_problems.py +1 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_reply_channels.py +3 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_research.py +1 -1
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_reviews.py +12 -0
- taskuary-0.3.2.4/tests/test_seed_fits_a_tty.py +56 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_setup.py +29 -6
- taskuary-0.3.2.4/tests/test_stay_open.py +100 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_terminal.py +68 -5
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_triage_dataset.py +10 -3
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_verdict_paths.py +27 -25
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_waitroom.py +11 -0
- taskuary-0.3.2.4/tests/test_whatsapp_documents.py +89 -0
- taskuary-0.3.2.4/tests/test_withdrawn.py +70 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_witness.py +2 -1
- taskuary-0.3.2.2/taskuary/web/assets/GeneralWorkspace-Bt02JQzu.js +0 -10
- taskuary-0.3.2.2/taskuary/web/assets/index-DGVnbkfG.js +0 -462
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/LICENSE +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/setup.cfg +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/__init__.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/agents.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/aws.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/azure.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/blackboard.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/browserview.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/calendar.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/climodels.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/clis.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/context.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/counsel.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/desktop.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/devtools.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/digest.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/github.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/handraise.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/imapmail.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/imessage.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/intacct.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/interview.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/knowledge.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/learn.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/learnedgraph.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/logs.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/mcp.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/msauth.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/mssql.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/phone.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/pm.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/policy.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/promptmap.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/proof.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/proposals.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/rank.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/reshape.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/responder.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/semantic.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/senders.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/sharepoint.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/sheets.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/counsel.md +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/digest.md +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/learned.md +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/soul.md +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/templates/style.md +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/testing.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/toil.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/verdicts.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/wabridge.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/web/assets/GeneralWorkspace-D46ydeAJ.css +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/web/assets/index-6GBZ9nXN.css +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/web/favicon.ico +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/web/favicon.png +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/webguard.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/whatsapp/package.json +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/whoami.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary/witness.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary.egg-info/dependency_links.txt +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary.egg-info/entry_points.txt +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary.egg-info/requires.txt +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/taskuary.egg-info/top_level.txt +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_agent_environment.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_agent_source.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_aisetup.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_answers.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_assistant.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_async_triage.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_audit_chain.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_aws_region.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_blackboard.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_browser_session.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_browserview.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_calendar.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_calendar_today.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_categories.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_cc_triage.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_chat_queue.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_climodels.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_clis.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_cloud.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_cloud_sources.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_context.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_counsel.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_cwlogs_paging.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_desktop.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_devtools.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_diff_review.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_docs_and_llm.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_docs_flow.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_duplicate_connectors.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_factory.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_feed_working.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_github_auto.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_handraise.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_harvest.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_histgen.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_imapmail.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_imessage.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_intacct.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_knowledge.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_learnedgraph.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_local_file.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_mail_folders.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_mark_read.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_mcp.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_messengers.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_msauth.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_mssql.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_note_retrieval.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_outbound_reports.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_pm.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_poll_overlap.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_poll_sources.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_pr_comments.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_promptmap_and_catalog.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_proof_ci_proposals.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_pypi_downloads.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_rank.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_reply_voice.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_report_alerts.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_report_triage.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_scopes.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_seeded_reports.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_semantic.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_send_targets.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_sheets_sharepoint.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_someone_else_replied.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_soul_interview.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_store_app_cli.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_store_hotpaths.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_sync_clock.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_task_identity.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_toil.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_urgent_and_handoff.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_verdict_sticks.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_vision.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_voice.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_wabridge.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_watermark.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_web_entrypoint.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_webguard.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_whatsapp_images.py +0 -0
- {taskuary-0.3.2.2 → taskuary-0.3.2.4}/tests/test_whoami.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: taskuary
|
|
3
|
-
Version: 0.3.2.
|
|
3
|
+
Version: 0.3.2.4
|
|
4
4
|
Summary: Automate your job - the local-first operating system for your work: AI triage in, coding agents out.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/ldbumble/taskuary
|
|
@@ -64,10 +64,19 @@ and replies wait for your approval. Nothing sends or ships without you.
|
|
|
64
64
|
|
|
65
65
|

|
|
66
66
|
|
|
67
|
-
Taskuary is early—currently **v0.3.2.
|
|
67
|
+
Taskuary is early—currently **v0.3.2.4**—and moving fast. The funnel, review queue,
|
|
68
68
|
agent sessions, and reports pipeline are in daily use; breaking changes are still possible
|
|
69
69
|
before 1.0.
|
|
70
70
|
|
|
71
|
+
<p align="center">
|
|
72
|
+
<a href="https://taskuary.com/demo/"><img
|
|
73
|
+
src="https://img.shields.io/badge/%E2%96%B6%20Try%20it%20now-no%20install%2C%20in%20your%20browser-2f4858?style=for-the-badge&labelColor=1f2a22"
|
|
74
|
+
alt="Try Taskuary now, in your browser"></a>
|
|
75
|
+
</p>
|
|
76
|
+
|
|
77
|
+
<p align="center"><sub>The real app over invented work — timeline, triage, a coding session mid-run,
|
|
78
|
+
reports with charts. Nothing to install and nothing connects to anything.</sub></p>
|
|
79
|
+
|
|
71
80
|
## One timeline for incoming work
|
|
72
81
|
|
|
73
82
|
Outlook, Gmail, Teams, Slack, Telegram, WhatsApp, GitHub, Jira, alerts, and reports all
|
|
@@ -141,6 +150,21 @@ On first run, connect an AI provider or local Ollama model, add at least one inb
|
|
|
141
150
|
channel, then choose the coding CLI that should receive tasks. The setup wizards test each
|
|
142
151
|
connection before it goes live.
|
|
143
152
|
|
|
153
|
+
## Try it without installing anything
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
taskuary --demo # or: docker compose --profile demo up
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
A full Taskuary over invented work — a morning's mail and chats already triaged with the
|
|
160
|
+
reasons attached, drafts waiting in Review, agents on the board with their transcripts
|
|
161
|
+
playing, live task handoffs on the Board, and the durable agent-written handbook in Social.
|
|
162
|
+
Every Social entry names the agent and originating task; Board handoffs are short-lived
|
|
163
|
+
checkout coordination that rolls up nightly. The AI answers from a script and every door
|
|
164
|
+
to the outside world is shut at the API layer (`taskuary/demo.py`): nothing sends, no
|
|
165
|
+
connection can be made or edited, no tool runs, and no CLI starts. Nobody's real data is in
|
|
166
|
+
it — Dana Whitfield and Northwind Facilities are invented.
|
|
167
|
+
|
|
144
168
|
## Installs
|
|
145
169
|
|
|
146
170
|

|
|
@@ -13,10 +13,19 @@ and replies wait for your approval. Nothing sends or ships without you.
|
|
|
13
13
|
|
|
14
14
|

|
|
15
15
|
|
|
16
|
-
Taskuary is early—currently **v0.3.2.
|
|
16
|
+
Taskuary is early—currently **v0.3.2.4**—and moving fast. The funnel, review queue,
|
|
17
17
|
agent sessions, and reports pipeline are in daily use; breaking changes are still possible
|
|
18
18
|
before 1.0.
|
|
19
19
|
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="https://taskuary.com/demo/"><img
|
|
22
|
+
src="https://img.shields.io/badge/%E2%96%B6%20Try%20it%20now-no%20install%2C%20in%20your%20browser-2f4858?style=for-the-badge&labelColor=1f2a22"
|
|
23
|
+
alt="Try Taskuary now, in your browser"></a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center"><sub>The real app over invented work — timeline, triage, a coding session mid-run,
|
|
27
|
+
reports with charts. Nothing to install and nothing connects to anything.</sub></p>
|
|
28
|
+
|
|
20
29
|
## One timeline for incoming work
|
|
21
30
|
|
|
22
31
|
Outlook, Gmail, Teams, Slack, Telegram, WhatsApp, GitHub, Jira, alerts, and reports all
|
|
@@ -90,6 +99,21 @@ On first run, connect an AI provider or local Ollama model, add at least one inb
|
|
|
90
99
|
channel, then choose the coding CLI that should receive tasks. The setup wizards test each
|
|
91
100
|
connection before it goes live.
|
|
92
101
|
|
|
102
|
+
## Try it without installing anything
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
taskuary --demo # or: docker compose --profile demo up
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A full Taskuary over invented work — a morning's mail and chats already triaged with the
|
|
109
|
+
reasons attached, drafts waiting in Review, agents on the board with their transcripts
|
|
110
|
+
playing, live task handoffs on the Board, and the durable agent-written handbook in Social.
|
|
111
|
+
Every Social entry names the agent and originating task; Board handoffs are short-lived
|
|
112
|
+
checkout coordination that rolls up nightly. The AI answers from a script and every door
|
|
113
|
+
to the outside world is shut at the API layer (`taskuary/demo.py`): nothing sends, no
|
|
114
|
+
connection can be made or edited, no tool runs, and no CLI starts. Nobody's real data is in
|
|
115
|
+
it — Dana Whitfield and Northwind Facilities are invented.
|
|
116
|
+
|
|
93
117
|
## Installs
|
|
94
118
|
|
|
95
119
|

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