meshcode 2.11.12__tar.gz → 2.11.14__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.
- {meshcode-2.11.12 → meshcode-2.11.14}/PKG-INFO +1 -1
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/__init__.py +1 -1
- meshcode-2.11.14/meshcode/date_parse.py +154 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/meshcode_mcp/backend.py +20 -3
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/meshcode_mcp/server.py +153 -4
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode.egg-info/SOURCES.txt +2 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/pyproject.toml +1 -1
- meshcode-2.11.14/tests/test_date_parse.py +112 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/README.md +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/__main__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/cli.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/compat.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/daemon.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/doctor.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/invites.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/launcher.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/preferences.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/run_agent.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/secrets.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/self_update.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode/upload.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/comms_v4.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/__init__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/cli.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/compat.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/error_hints.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/exceptions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/invites.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/launcher.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/preferences.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/quickstart.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/run_agent.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/secrets.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/self_update.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/supervisor.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/meshcode/upload.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/scripts/sentinel.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/tests/test_core.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/tests/test_exceptions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/tests/test_sentinel.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-backend-wt/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/__init__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/cli.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/compat.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/error_hints.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/exceptions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/invites.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/launcher.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/preferences.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/quickstart.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/run_agent.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/secrets.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/self_update.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/supervisor.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/build/lib/meshcode/upload.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/comms_v4.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/__init__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/cli.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/compat.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/error_hints.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/exceptions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/invites.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/launcher.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/preferences.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/quickstart.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/run_agent.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/secrets.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/self_update.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/supervisor.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/meshcode/upload.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/scripts/sentinel.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/tests/test_core.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/tests/test_exceptions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/tests/test_sentinel.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-noun-wt/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/comms_v4.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/__init__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/cli.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/compat.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/error_hints.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/exceptions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/invites.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/launcher.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/preferences.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/quickstart.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/run_agent.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/secrets.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/self_update.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/supervisor.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/meshcode/upload.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/scripts/sentinel.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/tests/test_core.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/tests/test_exceptions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/tests/test_sentinel.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode-tasks-wt/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/setup.cfg +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_core.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_doctor.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.12 → meshcode-2.11.14}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Natural-language date parsing for `meshcode_task_create(due=...)`.
|
|
2
|
+
|
|
3
|
+
Goal: accept human inputs ("by Thursday", "in 3 days", "tomorrow",
|
|
4
|
+
"next week", "May 25") and ISO 8601 strict pass-through. No external
|
|
5
|
+
dependency — manual rules keep the install lean.
|
|
6
|
+
|
|
7
|
+
API:
|
|
8
|
+
parse_due(text: str, now: datetime|None = None)
|
|
9
|
+
-> (parsed_dt: datetime|None, label: str)
|
|
10
|
+
|
|
11
|
+
Returned datetime is timezone-aware UTC. Labels are normalized lowercase
|
|
12
|
+
human strings. Unknown input returns (None, original_text).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from datetime import datetime, timedelta, timezone
|
|
18
|
+
from typing import Optional, Tuple
|
|
19
|
+
|
|
20
|
+
_WEEKDAYS = {
|
|
21
|
+
"monday": 0, "mon": 0,
|
|
22
|
+
"tuesday": 1, "tue": 1, "tues": 1,
|
|
23
|
+
"wednesday": 2, "wed": 2,
|
|
24
|
+
"thursday": 3, "thu": 3, "thur": 3, "thurs": 3,
|
|
25
|
+
"friday": 4, "fri": 4,
|
|
26
|
+
"saturday": 5, "sat": 5,
|
|
27
|
+
"sunday": 6, "sun": 6,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_MONTHS = {
|
|
31
|
+
"january": 1, "jan": 1,
|
|
32
|
+
"february": 2, "feb": 2,
|
|
33
|
+
"march": 3, "mar": 3,
|
|
34
|
+
"april": 4, "apr": 4,
|
|
35
|
+
"may": 5,
|
|
36
|
+
"june": 6, "jun": 6,
|
|
37
|
+
"july": 7, "jul": 7,
|
|
38
|
+
"august": 8, "aug": 8,
|
|
39
|
+
"september": 9, "sep": 9, "sept": 9,
|
|
40
|
+
"october": 10, "oct": 10,
|
|
41
|
+
"november": 11, "nov": 11,
|
|
42
|
+
"december": 12, "dec": 12,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _eod(dt: datetime) -> datetime:
|
|
47
|
+
"""End of day, UTC-aware."""
|
|
48
|
+
return dt.replace(hour=23, minute=59, second=0, microsecond=0, tzinfo=timezone.utc)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _next_weekday(now: datetime, target_dow: int) -> datetime:
|
|
52
|
+
"""Closest upcoming weekday match (today counts if it's in the future)."""
|
|
53
|
+
days_ahead = (target_dow - now.weekday()) % 7
|
|
54
|
+
if days_ahead == 0:
|
|
55
|
+
# "Thursday" said on Thursday → next Thursday (week out), unless
|
|
56
|
+
# caller explicitly said "today". Convention: same-day → next week.
|
|
57
|
+
days_ahead = 7
|
|
58
|
+
return _eod(now + timedelta(days=days_ahead))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_due(text: str, now: Optional[datetime] = None) -> Tuple[Optional[datetime], str]:
|
|
62
|
+
"""Parse a natural-language due-date string. Returns (datetime|None, label).
|
|
63
|
+
|
|
64
|
+
The label is what we'll persist as due_label (mig 329) — preserves the
|
|
65
|
+
user's voice when shown back ("by Thursday" stays human, not "2026-05-21").
|
|
66
|
+
"""
|
|
67
|
+
if not text:
|
|
68
|
+
return None, ""
|
|
69
|
+
raw = text.strip()
|
|
70
|
+
s = raw.lower()
|
|
71
|
+
if now is None:
|
|
72
|
+
now = datetime.now(tz=timezone.utc)
|
|
73
|
+
elif now.tzinfo is None:
|
|
74
|
+
now = now.replace(tzinfo=timezone.utc)
|
|
75
|
+
|
|
76
|
+
# ISO 8601 strict pass-through (e.g. "2026-05-25T15:00:00Z" or "2026-05-25")
|
|
77
|
+
try:
|
|
78
|
+
iso = raw.replace("Z", "+00:00")
|
|
79
|
+
dt = datetime.fromisoformat(iso)
|
|
80
|
+
if dt.tzinfo is None:
|
|
81
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
82
|
+
return dt, raw
|
|
83
|
+
except ValueError:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
# "today" / "tonight"
|
|
87
|
+
if s in ("today", "eod", "end of day", "tonight"):
|
|
88
|
+
return _eod(now), raw
|
|
89
|
+
|
|
90
|
+
# "tomorrow"
|
|
91
|
+
if s in ("tomorrow", "tmrw"):
|
|
92
|
+
return _eod(now + timedelta(days=1)), raw
|
|
93
|
+
|
|
94
|
+
# "in N days"|"in N hours"|"in N weeks"
|
|
95
|
+
m = re.match(r"^in\s+(\d+)\s+(day|days|hour|hours|hr|hrs|week|weeks|wk|wks|minute|minutes|min|mins)$", s)
|
|
96
|
+
if m:
|
|
97
|
+
n = int(m.group(1))
|
|
98
|
+
unit = m.group(2)
|
|
99
|
+
if unit.startswith("d"):
|
|
100
|
+
dt = _eod(now + timedelta(days=n))
|
|
101
|
+
elif unit.startswith("h") or unit.startswith("hr"):
|
|
102
|
+
dt = now + timedelta(hours=n)
|
|
103
|
+
elif unit.startswith("w"):
|
|
104
|
+
dt = _eod(now + timedelta(weeks=n))
|
|
105
|
+
elif unit.startswith("m"):
|
|
106
|
+
dt = now + timedelta(minutes=n)
|
|
107
|
+
else:
|
|
108
|
+
dt = None
|
|
109
|
+
return dt, raw
|
|
110
|
+
|
|
111
|
+
# "next week"
|
|
112
|
+
if s in ("next week", "nextweek"):
|
|
113
|
+
return _eod(now + timedelta(weeks=1)), raw
|
|
114
|
+
|
|
115
|
+
# "next <weekday>"
|
|
116
|
+
m = re.match(r"^next\s+([a-z]+)$", s)
|
|
117
|
+
if m and m.group(1) in _WEEKDAYS:
|
|
118
|
+
return _next_weekday(now + timedelta(days=7), _WEEKDAYS[m.group(1)]), raw
|
|
119
|
+
|
|
120
|
+
# "by <weekday>" / "<weekday>" / "this <weekday>"
|
|
121
|
+
m = re.match(r"^(?:by\s+|this\s+)?([a-z]+)$", s)
|
|
122
|
+
if m and m.group(1) in _WEEKDAYS:
|
|
123
|
+
return _next_weekday(now, _WEEKDAYS[m.group(1)]), raw
|
|
124
|
+
|
|
125
|
+
# "<month> <day>" or "<day> <month>" — assumes current year (or next year
|
|
126
|
+
# if the date already passed). Examples: "May 25", "25 May", "May 25 2026".
|
|
127
|
+
m = re.match(r"^(?:by\s+)?([a-z]+)\s+(\d{1,2})(?:[,\s]+(\d{4}))?$", s)
|
|
128
|
+
if m and m.group(1) in _MONTHS:
|
|
129
|
+
month = _MONTHS[m.group(1)]
|
|
130
|
+
day = int(m.group(2))
|
|
131
|
+
year = int(m.group(3)) if m.group(3) else now.year
|
|
132
|
+
try:
|
|
133
|
+
dt = _eod(datetime(year, month, day, tzinfo=timezone.utc))
|
|
134
|
+
if dt < now and not m.group(3):
|
|
135
|
+
dt = _eod(datetime(year + 1, month, day, tzinfo=timezone.utc))
|
|
136
|
+
return dt, raw
|
|
137
|
+
except ValueError:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
m = re.match(r"^(?:by\s+)?(\d{1,2})\s+([a-z]+)(?:[,\s]+(\d{4}))?$", s)
|
|
141
|
+
if m and m.group(2) in _MONTHS:
|
|
142
|
+
day = int(m.group(1))
|
|
143
|
+
month = _MONTHS[m.group(2)]
|
|
144
|
+
year = int(m.group(3)) if m.group(3) else now.year
|
|
145
|
+
try:
|
|
146
|
+
dt = _eod(datetime(year, month, day, tzinfo=timezone.utc))
|
|
147
|
+
if dt < now and not m.group(3):
|
|
148
|
+
dt = _eod(datetime(year + 1, month, day, tzinfo=timezone.utc))
|
|
149
|
+
return dt, raw
|
|
150
|
+
except ValueError:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
# Fallback: not understood. Return None so caller can decide.
|
|
154
|
+
return None, raw
|
|
@@ -1190,8 +1190,13 @@ def set_status(project_id: str, agent: str, status: str, task: str = "", api_key
|
|
|
1190
1190
|
return {"ok": True, "status": status}
|
|
1191
1191
|
|
|
1192
1192
|
|
|
1193
|
-
def task_create(api_key, project_id, creator_agent, title, description="",
|
|
1194
|
-
|
|
1193
|
+
def task_create(api_key, project_id, creator_agent, title, description="",
|
|
1194
|
+
assignee="*", priority="normal", parent_task_id=None,
|
|
1195
|
+
goal_id=None, strict=False,
|
|
1196
|
+
due_at=None, due_label=None, depends_on=None):
|
|
1197
|
+
"""RPC wrapper. due_at must be an ISO 8601 string or None — the MCP tool
|
|
1198
|
+
layer parses natural-language input via meshcode.date_parse first."""
|
|
1199
|
+
args = {
|
|
1195
1200
|
"p_api_key": api_key,
|
|
1196
1201
|
"p_project_id": project_id,
|
|
1197
1202
|
"p_creator_agent": creator_agent,
|
|
@@ -1200,7 +1205,19 @@ def task_create(api_key, project_id, creator_agent, title, description="", assig
|
|
|
1200
1205
|
"p_assignee": assignee,
|
|
1201
1206
|
"p_priority": priority,
|
|
1202
1207
|
"p_parent_task_id": parent_task_id,
|
|
1203
|
-
|
|
1208
|
+
"p_strict": strict,
|
|
1209
|
+
"p_goal_id": goal_id,
|
|
1210
|
+
}
|
|
1211
|
+
# CAL-1/CAL-2: extra fields. Only pass when set so we stay back-compat
|
|
1212
|
+
# with projects where mig 329 isn't applied yet (PostgREST will
|
|
1213
|
+
# 404 on extras when the new signature isn't in the schema cache).
|
|
1214
|
+
if due_at is not None:
|
|
1215
|
+
args["p_due_at"] = due_at
|
|
1216
|
+
if due_label is not None:
|
|
1217
|
+
args["p_due_label"] = due_label
|
|
1218
|
+
if depends_on:
|
|
1219
|
+
args["p_depends_on"] = depends_on
|
|
1220
|
+
return sb_rpc("mc_task_create", args)
|
|
1204
1221
|
|
|
1205
1222
|
|
|
1206
1223
|
# Client-side debounce for task_list to neutralize runaway pollers without a
|
|
@@ -3720,6 +3720,11 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
3720
3720
|
out["pending_tasks"] = pending_tasks
|
|
3721
3721
|
else:
|
|
3722
3722
|
out["no_work"] = True
|
|
3723
|
+
# CAL-3 calendar context — surface due/overdue/next_due on timeouts so the
|
|
3724
|
+
# agent knows whether to nudge work even when no new msg arrived.
|
|
3725
|
+
_ctx = _calendar_context_for_self()
|
|
3726
|
+
if _ctx is not None:
|
|
3727
|
+
out["calendar_context"] = _ctx
|
|
3723
3728
|
return out
|
|
3724
3729
|
|
|
3725
3730
|
|
|
@@ -3811,9 +3816,53 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None, mark
|
|
|
3811
3816
|
_hints = be.take_memory_hints()
|
|
3812
3817
|
if _hints:
|
|
3813
3818
|
result["memory_hints"] = _hints
|
|
3819
|
+
# CAL-3 calendar context — same shape as meshcode_boot, computed lazily
|
|
3820
|
+
# so the LLM sees due/overdue counts inline with every check/wait return.
|
|
3821
|
+
_ctx = _calendar_context_for_self()
|
|
3822
|
+
if _ctx is not None:
|
|
3823
|
+
result["calendar_context"] = _ctx
|
|
3814
3824
|
return result
|
|
3815
3825
|
|
|
3816
3826
|
|
|
3827
|
+
def _calendar_context_for_self() -> Optional[Dict[str, Any]]:
|
|
3828
|
+
"""CAL-3: lightweight calendar snapshot for the local agent. Soft-fail
|
|
3829
|
+
when mig 329 isn't deployed on the project — caller drops the field."""
|
|
3830
|
+
try:
|
|
3831
|
+
api_key = _get_api_key()
|
|
3832
|
+
if not api_key:
|
|
3833
|
+
return None
|
|
3834
|
+
resp = be.sb_rpc("mc_tasks_due_soon", {
|
|
3835
|
+
"p_api_key": api_key,
|
|
3836
|
+
"p_project_id": _PROJECT_ID,
|
|
3837
|
+
"p_agent_name": AGENT_NAME,
|
|
3838
|
+
"p_window_hours": 24,
|
|
3839
|
+
})
|
|
3840
|
+
if not (isinstance(resp, dict) and resp.get("ok")):
|
|
3841
|
+
return None
|
|
3842
|
+
tasks = resp.get("tasks") or []
|
|
3843
|
+
from datetime import datetime as _dt_dt, timezone as _dt_tz
|
|
3844
|
+
now_dt = _dt_dt.now(tz=_dt_tz.utc)
|
|
3845
|
+
due_today, overdue = [], []
|
|
3846
|
+
for t in tasks:
|
|
3847
|
+
d = t.get("due_at")
|
|
3848
|
+
if not d:
|
|
3849
|
+
continue
|
|
3850
|
+
try:
|
|
3851
|
+
dt = _dt_dt.fromisoformat(str(d).replace("Z", "+00:00"))
|
|
3852
|
+
except Exception:
|
|
3853
|
+
continue
|
|
3854
|
+
(overdue if dt < now_dt else due_today).append(t)
|
|
3855
|
+
if not (due_today or overdue):
|
|
3856
|
+
return None
|
|
3857
|
+
return {
|
|
3858
|
+
"pending_due_today": len(due_today),
|
|
3859
|
+
"overdue": len(overdue),
|
|
3860
|
+
"next_due": (due_today[0] if due_today else (overdue[0] if overdue else None)),
|
|
3861
|
+
}
|
|
3862
|
+
except Exception:
|
|
3863
|
+
return None
|
|
3864
|
+
|
|
3865
|
+
|
|
3817
3866
|
@mcp.tool()
|
|
3818
3867
|
@with_working_status
|
|
3819
3868
|
def meshcode_status() -> Dict[str, Any]:
|
|
@@ -3914,26 +3963,54 @@ def meshcode_init(project: str, agent: str, role: str = "") -> Dict[str, Any]:
|
|
|
3914
3963
|
@mcp.tool()
|
|
3915
3964
|
@with_working_status
|
|
3916
3965
|
def meshcode_task_create(title: str, description: str = "", assignee: Optional[str] = None,
|
|
3917
|
-
priority: str = "normal", parent_task_id: Optional[str] = None
|
|
3966
|
+
priority: str = "normal", parent_task_id: Optional[str] = None,
|
|
3967
|
+
due: Optional[str] = None,
|
|
3968
|
+
depends_on: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
3918
3969
|
"""Create task. assignee = explicit agent name OR 'auto' for round-robin
|
|
3919
3970
|
onto the least-loaded live agent in this meshwork. priority: low/normal/high/urgent.
|
|
3920
3971
|
|
|
3921
3972
|
Per Samuel rule (mig 213): wildcard '*' / NULL / '' assignees are forbidden —
|
|
3922
3973
|
every task must have a concrete owner or opt into 'auto' resolution.
|
|
3974
|
+
|
|
3975
|
+
CAL-2 (mig 329):
|
|
3976
|
+
due: natural-language deadline ("by Thursday", "in 3 days",
|
|
3977
|
+
"tomorrow", "May 25") OR ISO 8601 ("2026-05-25T15:00Z").
|
|
3978
|
+
Stored as both due_at (parsed datetime) and due_label
|
|
3979
|
+
(the original human string, for round-trip display).
|
|
3980
|
+
depends_on: list of task UUIDs that must reach status=done/cancelled
|
|
3981
|
+
before this task is unblocked.
|
|
3923
3982
|
"""
|
|
3924
3983
|
if not title or not title.strip():
|
|
3925
3984
|
return {"error": "title cannot be empty"}
|
|
3926
|
-
# Mirror the RPC validation client-side so the LLM gets a clear hint
|
|
3927
|
-
# before round-tripping. Do not silently coerce — the caller must choose.
|
|
3928
3985
|
if assignee is None or not str(assignee).strip() or str(assignee).strip() == "*":
|
|
3929
3986
|
return {
|
|
3930
3987
|
"error": "assignee_required",
|
|
3931
3988
|
"hint": "specify an explicit agent name OR pass assignee='auto' for round-robin",
|
|
3932
3989
|
}
|
|
3990
|
+
# CAL-2: parse natural-language due dates client-side. The label round-trips
|
|
3991
|
+
# so the dashboard can show "by Thursday" verbatim instead of an ISO blob.
|
|
3992
|
+
parsed_due_at = None
|
|
3993
|
+
parsed_due_label = None
|
|
3994
|
+
if due:
|
|
3995
|
+
try:
|
|
3996
|
+
from meshcode import date_parse
|
|
3997
|
+
_dt, _lbl = date_parse.parse_due(due)
|
|
3998
|
+
parsed_due_label = _lbl
|
|
3999
|
+
if _dt is not None:
|
|
4000
|
+
parsed_due_at = _dt.isoformat()
|
|
4001
|
+
else:
|
|
4002
|
+
return {
|
|
4003
|
+
"error": "due_unparseable",
|
|
4004
|
+
"hint": f"could not parse {due!r}; try ISO 8601 or 'by <day>' / 'in N days' / 'tomorrow'",
|
|
4005
|
+
}
|
|
4006
|
+
except Exception as _de:
|
|
4007
|
+
return {"error": f"due_parse_failed: {_de}"}
|
|
3933
4008
|
api_key = _get_api_key()
|
|
3934
4009
|
result = be.task_create(api_key, _PROJECT_ID, AGENT_NAME, title.strip(),
|
|
3935
4010
|
description=description, assignee=assignee,
|
|
3936
|
-
priority=priority, parent_task_id=parent_task_id
|
|
4011
|
+
priority=priority, parent_task_id=parent_task_id,
|
|
4012
|
+
due_at=parsed_due_at, due_label=parsed_due_label,
|
|
4013
|
+
depends_on=depends_on or None)
|
|
3937
4014
|
# Auto-notify the resolved assignee so they wake from meshcode_wait.
|
|
3938
4015
|
# Read result["assignee"] (server may have resolved 'auto' to a real name).
|
|
3939
4016
|
if isinstance(result, dict) and result.get("ok"):
|
|
@@ -4829,6 +4906,78 @@ def meshcode_boot() -> Dict[str, Any]:
|
|
|
4829
4906
|
_PERSONA = ph
|
|
4830
4907
|
_PERSONA_INJECTED = False # let with_working_status surface it once
|
|
4831
4908
|
|
|
4909
|
+
# CAL-3 calendar context (mig 329): surface upcoming due tasks +
|
|
4910
|
+
# overdue count + newly-unblocked so the agent knows the cadence
|
|
4911
|
+
# backlog without a second tool call. Soft-fail if mig 329 not
|
|
4912
|
+
# applied on this project.
|
|
4913
|
+
try:
|
|
4914
|
+
_due_resp = be.sb_rpc("mc_tasks_due_soon", {
|
|
4915
|
+
"p_api_key": api_key,
|
|
4916
|
+
"p_project_id": _PROJECT_ID,
|
|
4917
|
+
"p_agent_name": AGENT_NAME,
|
|
4918
|
+
"p_window_hours": 24,
|
|
4919
|
+
})
|
|
4920
|
+
if isinstance(_due_resp, dict) and _due_resp.get("ok"):
|
|
4921
|
+
_due_tasks = _due_resp.get("tasks") or []
|
|
4922
|
+
_now_iso = _time.time()
|
|
4923
|
+
_due_today = []
|
|
4924
|
+
_overdue = []
|
|
4925
|
+
from datetime import datetime as _dt_dt, timezone as _dt_tz
|
|
4926
|
+
_now_dt = _dt_dt.now(tz=_dt_tz.utc)
|
|
4927
|
+
for _t in _due_tasks:
|
|
4928
|
+
_due_at_str = _t.get("due_at")
|
|
4929
|
+
if not _due_at_str:
|
|
4930
|
+
continue
|
|
4931
|
+
try:
|
|
4932
|
+
_due_dt = _dt_dt.fromisoformat(str(_due_at_str).replace("Z", "+00:00"))
|
|
4933
|
+
except Exception:
|
|
4934
|
+
continue
|
|
4935
|
+
if _due_dt < _now_dt:
|
|
4936
|
+
_overdue.append(_t)
|
|
4937
|
+
else:
|
|
4938
|
+
_due_today.append(_t)
|
|
4939
|
+
calendar_block = {
|
|
4940
|
+
"pending_due_today": len(_due_today),
|
|
4941
|
+
"overdue": len(_overdue),
|
|
4942
|
+
"next_due": (_due_today[0] if _due_today else (_overdue[0] if _overdue else None)),
|
|
4943
|
+
"due_tasks": _due_today + _overdue,
|
|
4944
|
+
}
|
|
4945
|
+
resp["calendar_context"] = calendar_block
|
|
4946
|
+
except Exception:
|
|
4947
|
+
resp.setdefault("calendar_context_available", False)
|
|
4948
|
+
|
|
4949
|
+
# UX-time-aware (mig 328 / task 4056b216): augment mesh_status rows with
|
|
4950
|
+
# cadence hints so the LLM can adjust tone (less pushy on deep-silence
|
|
4951
|
+
# peers, fine to ping on warm ones). Soft-fail if RPC unavailable on the
|
|
4952
|
+
# current project — mc_boot already gave us mesh_status without it.
|
|
4953
|
+
try:
|
|
4954
|
+
tc = be.sb_rpc("mc_agent_time_context_all", {
|
|
4955
|
+
"p_api_key": api_key,
|
|
4956
|
+
"p_project_id": _PROJECT_ID,
|
|
4957
|
+
})
|
|
4958
|
+
if isinstance(tc, dict) and tc.get("ok") and isinstance(tc.get("agents"), list):
|
|
4959
|
+
ctx_by_name: Dict[str, Dict[str, Any]] = {}
|
|
4960
|
+
for entry in tc["agents"]:
|
|
4961
|
+
# The bulk RPC wraps each context in a single-item list per row;
|
|
4962
|
+
# be lenient about both shapes.
|
|
4963
|
+
if isinstance(entry, list) and entry:
|
|
4964
|
+
entry = entry[0]
|
|
4965
|
+
if isinstance(entry, dict) and entry.get("agent"):
|
|
4966
|
+
ctx_by_name[entry["agent"]] = entry
|
|
4967
|
+
mesh = resp.get("mesh_status") or []
|
|
4968
|
+
if isinstance(mesh, list):
|
|
4969
|
+
for row in mesh:
|
|
4970
|
+
n = row.get("name") if isinstance(row, dict) else None
|
|
4971
|
+
if n and n in ctx_by_name:
|
|
4972
|
+
c = ctx_by_name[n]
|
|
4973
|
+
row["idle_bucket"] = c.get("idle_bucket")
|
|
4974
|
+
row["idle_seconds"] = c.get("idle_seconds")
|
|
4975
|
+
row["cadence_hint"] = c.get("cadence_hint")
|
|
4976
|
+
resp.setdefault("time_context_available", True)
|
|
4977
|
+
except Exception:
|
|
4978
|
+
# Non-fatal: surface that the hints are unavailable but boot succeeds.
|
|
4979
|
+
resp.setdefault("time_context_available", False)
|
|
4980
|
+
|
|
4832
4981
|
return resp
|
|
4833
4982
|
|
|
4834
4983
|
|
|
@@ -8,6 +8,7 @@ meshcode/cli.py
|
|
|
8
8
|
meshcode/comms_v4.py
|
|
9
9
|
meshcode/compat.py
|
|
10
10
|
meshcode/daemon.py
|
|
11
|
+
meshcode/date_parse.py
|
|
11
12
|
meshcode/doctor.py
|
|
12
13
|
meshcode/error_hints.py
|
|
13
14
|
meshcode/exceptions.py
|
|
@@ -199,6 +200,7 @@ tests/test_boot_bug_regression.py
|
|
|
199
200
|
tests/test_color_truecolor.py
|
|
200
201
|
tests/test_core.py
|
|
201
202
|
tests/test_cross_agent_messaging.py
|
|
203
|
+
tests/test_date_parse.py
|
|
202
204
|
tests/test_doctor.py
|
|
203
205
|
tests/test_epistemic_v1_python_sdk.py
|
|
204
206
|
tests/test_epistemic_v1_stop_conditions.py
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Tests for meshcode/date_parse.py."""
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from meshcode.date_parse import parse_due
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def now():
|
|
11
|
+
# Pin "now" to a Wednesday 2026-05-20 12:00 UTC for deterministic tests.
|
|
12
|
+
return datetime(2026, 5, 20, 12, 0, tzinfo=timezone.utc)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_empty_returns_none(now):
|
|
16
|
+
assert parse_due("", now) == (None, "")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_unknown_returns_none(now):
|
|
20
|
+
dt, label = parse_due("yesterday-ish", now)
|
|
21
|
+
assert dt is None
|
|
22
|
+
assert label == "yesterday-ish"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_iso_passthrough_utc(now):
|
|
26
|
+
dt, label = parse_due("2026-05-25T15:00:00Z", now)
|
|
27
|
+
assert dt == datetime(2026, 5, 25, 15, 0, tzinfo=timezone.utc)
|
|
28
|
+
assert label == "2026-05-25T15:00:00Z"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_iso_date_only(now):
|
|
32
|
+
dt, label = parse_due("2026-05-25", now)
|
|
33
|
+
assert dt.year == 2026 and dt.month == 5 and dt.day == 25
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_today_returns_eod(now):
|
|
37
|
+
dt, _ = parse_due("today", now)
|
|
38
|
+
assert dt.date() == now.date()
|
|
39
|
+
assert dt.hour == 23 and dt.minute == 59
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_tomorrow(now):
|
|
43
|
+
dt, _ = parse_due("tomorrow", now)
|
|
44
|
+
assert dt.date() == (now + timedelta(days=1)).date()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_in_3_days(now):
|
|
48
|
+
dt, _ = parse_due("in 3 days", now)
|
|
49
|
+
assert (dt.date() - now.date()).days == 3
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_in_2_hours(now):
|
|
53
|
+
dt, _ = parse_due("in 2 hours", now)
|
|
54
|
+
delta = dt - now
|
|
55
|
+
assert 1.9 * 3600 <= delta.total_seconds() <= 2.1 * 3600
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_in_1_week(now):
|
|
59
|
+
dt, _ = parse_due("in 1 week", now)
|
|
60
|
+
assert (dt.date() - now.date()).days == 7
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_weekday_alone_returns_next_match(now):
|
|
64
|
+
"""Wed → 'thursday' is tomorrow."""
|
|
65
|
+
dt, _ = parse_due("thursday", now)
|
|
66
|
+
assert dt.weekday() == 3
|
|
67
|
+
assert (dt.date() - now.date()).days == 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_by_weekday(now):
|
|
71
|
+
dt, _ = parse_due("by friday", now)
|
|
72
|
+
assert dt.weekday() == 4
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_next_weekday(now):
|
|
76
|
+
"""'next thursday' said on Wed → not tomorrow, but the one a week later."""
|
|
77
|
+
dt, _ = parse_due("next thursday", now)
|
|
78
|
+
assert dt.weekday() == 3
|
|
79
|
+
assert (dt.date() - now.date()).days >= 7
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_same_day_weekday_means_next_week(now):
|
|
83
|
+
"""Wed → 'wednesday' is NOT today (convention)."""
|
|
84
|
+
dt, _ = parse_due("wednesday", now)
|
|
85
|
+
assert dt.weekday() == 2
|
|
86
|
+
assert (dt.date() - now.date()).days == 7
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_month_day_current_year(now):
|
|
90
|
+
dt, _ = parse_due("May 25", now)
|
|
91
|
+
assert dt.year == 2026 and dt.month == 5 and dt.day == 25
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_month_day_past_rolls_to_next_year(now):
|
|
95
|
+
"""May 1 already passed in pinned now=May 20 → roll to next year."""
|
|
96
|
+
dt, _ = parse_due("May 1", now)
|
|
97
|
+
assert dt.year == 2027 and dt.month == 5 and dt.day == 1
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_day_month_reverse(now):
|
|
101
|
+
dt, _ = parse_due("25 May", now)
|
|
102
|
+
assert dt.year == 2026 and dt.month == 5 and dt.day == 25
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_explicit_year_month_day(now):
|
|
106
|
+
dt, _ = parse_due("May 25, 2030", now)
|
|
107
|
+
assert dt.year == 2030
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_label_preserves_original(now):
|
|
111
|
+
_, label = parse_due("by Thursday", now)
|
|
112
|
+
assert label == "by Thursday"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|