thth 3.1.1__tar.gz → 3.2.0__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.
- {thth-3.1.1 → thth-3.2.0}/PKG-INFO +2 -2
- {thth-3.1.1 → thth-3.2.0}/README.md +1 -1
- {thth-3.1.1 → thth-3.2.0}/mcp/server.py +95 -8
- {thth-3.1.1 → thth-3.2.0}/skills/thth/SKILL.md +10 -0
- {thth-3.1.1 → thth-3.2.0}/skills/thth-admin/SKILL.md +1 -1
- {thth-3.1.1 → thth-3.2.0}/tests/conftest.py +4 -2
- {thth-3.1.1 → thth-3.2.0}/tests/test_admin_http_mcp.py +3 -1
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_json_contract.py +4 -1
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_leave.py +2 -1
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_leave_providers.py +13 -5
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_read_coordination.py +3 -3
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_carryover_timeout.py +1 -1
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_review_followups.py +1 -1
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_stop_observation.py +1 -1
- {thth-3.1.1 → thth-3.2.0}/tests/test_v311_leave_same_user.py +7 -5
- thth-3.2.0/tests/test_v312_leave_shared_token.py +126 -0
- thth-3.2.0/tests/test_v312_own_handles_stopped.py +112 -0
- thth-3.2.0/tests/test_v312_report_admin.py +269 -0
- thth-3.2.0/tests/test_v312_report_channel.py +275 -0
- thth-3.2.0/tests/test_v312_report_inbox.py +312 -0
- thth-3.2.0/tests/test_v312_report_mcp_user.py +154 -0
- thth-3.2.0/tests/test_v320_reply_to_file_display.py +146 -0
- thth-3.2.0/tests/test_v320_reply_to_file_lint.py +122 -0
- thth-3.2.0/tests/test_v320_reply_to_file_resolve.py +318 -0
- thth-3.2.0/tests/test_v320_report_fixes.py +174 -0
- thth-3.2.0/thth/VERSION +1 -0
- {thth-3.1.1 → thth-3.2.0}/thth/admin_log.py +6 -2
- {thth-3.1.1 → thth-3.2.0}/thth/admin_report.py +3 -0
- {thth-3.1.1 → thth-3.2.0}/thth/approval.py +23 -0
- {thth-3.1.1 → thth-3.2.0}/thth/cli.py +55 -6
- {thth-3.1.1 → thth-3.2.0}/thth/core.py +82 -14
- {thth-3.1.1 → thth-3.2.0}/thth/leave.py +46 -16
- {thth-3.1.1 → thth-3.2.0}/thth/lint.py +72 -1
- {thth-3.1.1 → thth-3.2.0}/thth/morning.py +79 -10
- {thth-3.1.1 → thth-3.2.0}/thth/operations_handoff.py +42 -7
- {thth-3.1.1 → thth-3.2.0}/thth/queuefile.py +118 -0
- {thth-3.1.1 → thth-3.2.0}/thth/redact.py +64 -0
- {thth-3.1.1 → thth-3.2.0}/thth/replies.py +24 -1
- {thth-3.1.1 → thth-3.2.0}/thth/report.py +29 -0
- thth-3.2.0/thth/report_inbox.py +1005 -0
- {thth-3.1.1 → thth-3.2.0}/thth/report_isolation.py +2 -0
- {thth-3.1.1 → thth-3.2.0}/thth/report_service.py +116 -0
- {thth-3.1.1 → thth-3.2.0}/thth/runs.py +3 -1
- {thth-3.1.1 → thth-3.2.0}/thth/select.py +146 -13
- {thth-3.1.1 → thth-3.2.0}/thth/sent.py +9 -1
- {thth-3.1.1 → thth-3.2.0}/thth/unanswered.py +2 -1
- thth-3.1.1/thth/VERSION +0 -1
- {thth-3.1.1 → thth-3.2.0}/.gitignore +0 -0
- {thth-3.1.1 → thth-3.2.0}/LICENSE +0 -0
- {thth-3.1.1 → thth-3.2.0}/accounts.example/bluesky.json +0 -0
- {thth-3.1.1 → thth-3.2.0}/accounts.example/mastodon.json +0 -0
- {thth-3.1.1 → thth-3.2.0}/accounts.example/threads.json +0 -0
- {thth-3.1.1 → thth-3.2.0}/accounts.example/x.json +0 -0
- {thth-3.1.1 → thth-3.2.0}/bin/thth +0 -0
- {thth-3.1.1 → thth-3.2.0}/bin/thth-remote +0 -0
- {thth-3.1.1 → thth-3.2.0}/bin/thth-run +0 -0
- {thth-3.1.1 → thth-3.2.0}/pyproject.toml +0 -0
- {thth-3.1.1 → thth-3.2.0}/systemd/thth@.service +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/__init__.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/coordination_snapshot.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/fixtures/build_injection_eval.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/fixtures/callback/worker_responses.json +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/fixtures/injection_eval.json +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/fixtures/umami-bile.md +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/helpers/__init__.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/helpers/fake_oauth_server.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/helpers/fake_push_adapter.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/helpers/fake_redirect_server.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/helpers/fake_search_adapter.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/helpers/fake_threads_reader.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/helpers/fake_threads_writes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/helpers/fake_x.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/helpers/hold_lock.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/helpers/hold_repo_lock.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/helpers/worker_probe.mjs +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_27_acceptance_fixes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_account_name_check.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_account_report.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_accounts_dir.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_adapter_boundary.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_admin_log.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_admin_notifications.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_admin_repairs.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_admin_report.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_after_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_analytics_comparison.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_analytics_report.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_api_diagnostic.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_api_error_body.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_app_env_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_approval.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_approve_cmd.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_ask.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_atlas_fifth_review.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_atlas_fourth_review.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_atlas_review.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_atlas_second_review.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_atlas_third_review.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_audit1_fixes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_audit2_fixes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_audit_b_counterexamples.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_audit_b_m1_semantics.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_audit_b_n1_depth.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_audit_b_r1_closure.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_audit_v201.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_audit_v23_details.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_auth.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_auth_identity_required.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_behind_remote.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_bluesky_adapter.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_bluesky_secret_scrub.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_board_needs_review.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_board_running.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_bundle.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_charcount.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_cli_entrypoint.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_cli_help.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_cli_vm_path.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_collect.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_collect_engagements.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_collect_runs.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_collect_sent.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_comparability.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_control_chars_refused.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_conversation_paging.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_count_for.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_docs_links.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_doctor.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_doctor_all_permissions.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_doctor_next_step.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_duplicate_front_matter_keys.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_editorial_audit.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_editorial_n3_n4.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_engagements.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_external_acceptance.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_fake_api.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_form_spec.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_form_spec_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_forms.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_fresh_install.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_front_matter_injection.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_healthcheck_notifications.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_hypotheses.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_hypothesis_boundaries.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_hypothesis_units.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_impact_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_inbox_granted_scopes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_incident_notifications.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_independent_thread_review.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_independent_topic_review.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_independent_topic_review_r2.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_inflight.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_inflight_tag_body_hash.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_jst.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_lint.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_list_projections.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_llm_trial.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_llm_trial_engage.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_lock.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_m2_independent.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_maintain.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_mastodon_adapter.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_mastodon_visibility.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_max_per_run.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_mcp.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_mcp_robustness.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_measured.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_media_accounts.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_media_next_steps.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_media_roundtrip.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_mismatch_diagnostics.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_n5_reply_null_element.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_n6_replies_incomplete_ledger.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_n7_doctor_error_body.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_no_limits.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_observation_provenance.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_operations_handoff.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_packaging.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_preview.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_production_mode.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_pull_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_queue.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_quiet_hours.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_r2_r4_r5.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_read_cli_bare_runtimeerror.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_reason_adoption.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_redact.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_redirect_token_leak.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_refresh.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_refresh_destination.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_rejudge_new.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_rejudge_r2_r3.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_replies.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_replies_refresh.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_repo_lock.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_repo_sync.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_report_checkout_import.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_report_http.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_report_isolation.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_report_service.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_reverse_audit.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_review_case_id_cluster_m2.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_review_case_id_cluster_r2.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_review_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_review_evidence_account.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_review_form_spec_and_case_id.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_review_provenance.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_review_records.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_revoke.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_roundtrip.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_run_missing_env.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_run_record_failclosed.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_runs_collected.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_runs_read_rows.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_schedule_multiple.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_schedule_view.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_scopes_source_requested.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_search_engage_targets.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_search_media_routes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_secret_value_redact.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_secrets_grep.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_security_small_fixes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_select.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_selfupdate.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_send.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_send_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_send_control_chars.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_send_media.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_send_visibility.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_sent_integrity.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_server_json.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_site.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_skill_doc.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_study_report.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_systemd.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_thread_publish.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_thread_read.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_threadrun.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_threads_read_permissions.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_threadshape.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_thth_root.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_token_exists.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_token_scopes_record.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_token_set.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_topic.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_topic_advice.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_topic_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_topic_models.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_topic_review_fixes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_topic_shelf_observers.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_topic_store.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_topics_record_concurrent.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_topics_tool.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_trial_frictions.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_admin_transaction.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_approve_account.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_length.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_lock_wait.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_mastodon_scopes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_mentions.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_post_urls.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_repo_paths.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_reposts.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_study_add.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_tool_version.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_unanswered.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_unanswered_scope.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_where_arguments.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_where_filters.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v210_who_arguments.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v211_auth_integration.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v211_authflow.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v211_bluesky_stdin.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v211_doctor_binding.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v211_mastodon_auth.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v211_x_auth.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_approval_relay.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_ask_coordination.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_budget.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_budget_auth.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_budget_mcp.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_deletion.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_job_completion_fault.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_leave_gate.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_leave_token_safety.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_mastodon_guide.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_mcp_compat.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_missing_token.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_server_writes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_token_parent.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v212_write_storage.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v2131_send_dry_run.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v2131_threads_publish_wait.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v2132_send_inflight_hint.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v2132_threads_create_timeout.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v2132_threads_create_unknown.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_acceptance_fixes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_adts_asf.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_approval_attachments.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_attachment_kinds.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_bluesky_external.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_bluesky_media.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_bluesky_metadata.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_bluesky_metadata_uri.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_bluesky_metadata_video.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_bluesky_record.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_bluesky_record_video.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_bluesky_video.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_bluesky_webm_named.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_bmff_typed_metadata.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_budget_lock.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_capabilities.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_graphemes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_auth_media.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_bmff_audio.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_flac.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_gif.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_link.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_media.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_mp3.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_ogg_opus.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_poll.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_quote.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_thumbnail.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_wav.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_webm_audio.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_mastodon_webm_video.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_media_cleanup.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_media_formats.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_media_foundation.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_media_prepared.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_media_relay.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_media_relay_refusals.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_media_upload.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_receipt_deadline.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_stage810_audit.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_static_diagnostics.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_systemd_worker.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_threads_ghost.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_threads_gif.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_threads_link.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_threads_media.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_threads_media_spoiler.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_threads_poll.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_threads_quote.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_threads_quote_media.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_threads_replies.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_threads_text.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_threads_text_spoiler.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_threads_video.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_transport_review.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_video_typed_metadata.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_vorbis_comment_boundary.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v213_webm_schema_boundary.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v2141_draft_lint_reason.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v2141_managed_repo_umask.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v214_release_signing.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v214_x_adapter.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v214_x_auth.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v214_x_media.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v214_x_posts_budget.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v21b_approved_writes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v28_tags.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v310_morning.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v310_search_timeout.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v310_watch_words.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v311_mastodon_retract.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v311_morning_overdue.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v311_morning_own_exclusion.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v311_morning_unanswered_window.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v311_notifications_guidance.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v311_replies_owned.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v31_analysis.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_v31_collection.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_value_domains_shown.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_version.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_where_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_who_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_writeback_rebase.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_writeback_validate.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/tests/test_x_metrics.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/__init__.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/__main__.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/_grapheme_data.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/account_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/account_report.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/accounts.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/__init__.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/auth_mastodon.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/auth_threads.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/auth_x.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/base.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/bluesky.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/bluesky_media.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/bluesky_video.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/mastodon.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/mastodon_media.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/threads.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/threads_media.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/x.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/adapters/x_media.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/admin_notifications.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/after_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/analytics_comparison.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/analytics_report.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/analytics_shapes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/analytics_threads.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/api_diagnostic.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/appconfig.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/appenv.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/approval_jobs.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/approval_relay.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/ask.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/ask_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/audioformats.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/authclients.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/authflow.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/authpassword.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/bluesky_metadata.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/budget_x.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/budget_x_posts.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/bundle.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/collect.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/collection_context.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/collection_status.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/deletion.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/doctor.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/ebmlaudio.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/ebmlvideo.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/engagements.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/flacformats.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/forms.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/graphemes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/handoff_cursor.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/healthcheck.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/httpsafe.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/incident.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/inflight.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/jst.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/leave_gate.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/lock.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/maintain.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/managed_repo.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/measured.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/media.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/media_capabilities.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/media_cleanup.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/media_delivery.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/media_relay.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/media_uploads.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/mediaformats.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/mpeg_audio.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/oauth.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/oggformats.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/postid.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/read_coordination.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/read_window.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/report_details.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/report_http.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/retract_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/scopes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/secrets_fs.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/selfupdate.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/server_files.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/server_writes.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/stop_observation.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/study_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/study_report.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/systemd_gen.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/tags.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/thread_read.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/threadrun.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/threads_read_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/threadshape.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/threadthrow.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/tool_version.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/topic_advice.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/topic_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/topic_models.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/topic_store.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/topics.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/vorbisformats.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/watch_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/where_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/who_cli.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/writeback.py +0 -0
- {thth-3.1.1 → thth-3.2.0}/thth/x_metrics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: thth
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: 承認済みの投稿だけを投げる門(Threads・Bluesky・Mastodon)。人が承認し、記録は利用者の git に残る。
|
|
5
5
|
Project-URL: Homepage, https://thth.me
|
|
6
6
|
Project-URL: Source, https://github.com/aokings/thth
|
|
@@ -125,7 +125,7 @@ timer(systemd・`thth systemd` で生成)は10分ごとに実行し、投稿
|
|
|
125
125
|
- **英語の文書**: [README.en.md](README.en.md)・[docs/usage.en.md](docs/usage.en.md)・[llms.txt](llms.txt)。
|
|
126
126
|
- **skill**: `skills/thth/SKILL.md`(wheel にも入る)。
|
|
127
127
|
|
|
128
|
-
- **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`・`notifications`・`admin`。
|
|
128
|
+
- **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`・`notifications`・`report`・`admin`。
|
|
129
129
|
- **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
|
|
130
130
|
- **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
|
|
131
131
|
- **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
|
|
@@ -101,7 +101,7 @@ timer(systemd・`thth systemd` で生成)は10分ごとに実行し、投稿
|
|
|
101
101
|
- **英語の文書**: [README.en.md](README.en.md)・[docs/usage.en.md](docs/usage.en.md)・[llms.txt](llms.txt)。
|
|
102
102
|
- **skill**: `skills/thth/SKILL.md`(wheel にも入る)。
|
|
103
103
|
|
|
104
|
-
- **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`・`notifications`・`admin`。
|
|
104
|
+
- **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`・`notifications`・`report`・`admin`。
|
|
105
105
|
- **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
|
|
106
106
|
- **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
|
|
107
107
|
- **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
|
|
@@ -534,6 +534,24 @@ ADMIN_TOOLS.append({'name':'thth_admin_watch_set','description':'Replace the wat
|
|
|
534
534
|
'words':{'type':'array','items':{'type':'string'}}},
|
|
535
535
|
'required':['account','words','by'],'additionalProperties':False}})
|
|
536
536
|
|
|
537
|
+
# 報告の口の実装側(設計 3.1.2 §1)。返事と閉じるは `by` 必須・変更ログに
|
|
538
|
+
# presence-only で残る。書き出し(export)は repo に書くので CLI だけ。
|
|
539
|
+
ADMIN_TOOLS += [
|
|
540
|
+
{'name':'thth_admin_reports_list','description':'List bug reports and requests from every project (status open by default, or closed/all); administrator only; read-only',
|
|
541
|
+
'inputSchema':{'type':'object','properties':{'status':{'type':'string','enum':['open','closed','all']}},
|
|
542
|
+
'required':[],'additionalProperties':False}},
|
|
543
|
+
{'name':'thth_admin_reports_show','description':'Read one report with its body, reproduction steps and replies; administrator only; read-only',
|
|
544
|
+
'inputSchema':{'type':'object','properties':{'report_id':{'type':'string'}},
|
|
545
|
+
'required':['report_id'],'additionalProperties':False}},
|
|
546
|
+
{'name':'thth_admin_reports_reply','description':'Add a reply to one report (shown to the reporter in operations_handoff tool.reports); administrator only, by required; refused if the text looks like a secret',
|
|
547
|
+
'inputSchema':{'type':'object','properties':{'report_id':{'type':'string'},'text':{'type':'string'},'by':{'type':'string'}},
|
|
548
|
+
'required':['report_id','text','by'],'additionalProperties':False}},
|
|
549
|
+
{'name':'thth_admin_reports_close','description':'Close one report with a reason (fixed, wontfix, duplicate, invalid) and the version that settled it; administrator only, by required',
|
|
550
|
+
'inputSchema':{'type':'object','properties':{'report_id':{'type':'string'},'by':{'type':'string'},
|
|
551
|
+
'reason':{'type':'string','enum':['fixed','wontfix','duplicate','invalid']},'version':{'type':'string'}},
|
|
552
|
+
'required':['report_id','reason','version','by'],'additionalProperties':False}},
|
|
553
|
+
]
|
|
554
|
+
|
|
537
555
|
SERVER_TOOLS = [
|
|
538
556
|
{"name":"thth_"+name,"description":"Scoped server "+name,
|
|
539
557
|
"inputSchema":{"type":"object","properties":{key:{"type":"string"} for key in ("account",*keys)},
|
|
@@ -566,6 +584,60 @@ del _tool
|
|
|
566
584
|
|
|
567
585
|
|
|
568
586
|
|
|
587
|
+
# 報告の口の利用者側(設計 3.1.2 §1)。**サーバ型だけに出す**——手元の stdio
|
|
588
|
+
# (pip 版)で置いても実装側には届かないので、届かない口は並べない。誰が置いたかは
|
|
589
|
+
# credential の `actor` が決める(要求の欄からは作らない)。
|
|
590
|
+
REPORT_TOOLS = [
|
|
591
|
+
{"name": "thth_report_file",
|
|
592
|
+
"description": "道具が断った・結果が期待と違った・欲しい形がある、のどれかならこれで置く"
|
|
593
|
+
"(kind は bug か request。実装側が読み、返事は thth_report_show と "
|
|
594
|
+
"operations_handoff の tool.reports に出る。秘密らしき値が含まれていたら置かない)",
|
|
595
|
+
"inputSchema": {"type": "object", "properties": {
|
|
596
|
+
"account": {"type": "string"},
|
|
597
|
+
"kind": {"type": "string", "enum": ["bug", "request"]},
|
|
598
|
+
"title": {"type": "string", "description": "1 行・120 字まで"},
|
|
599
|
+
"body": {"type": "string", "description": "8,000 字まで"},
|
|
600
|
+
"repro": {"type": "string", "description": "再現手順(任意・4,000 字まで)"}},
|
|
601
|
+
"required": ["account", "kind", "title", "body"], "additionalProperties": False}},
|
|
602
|
+
{"name": "thth_report_list",
|
|
603
|
+
"description": "自分の project の報告と、実装側の返事の数を読む(読むだけ)",
|
|
604
|
+
"inputSchema": {"type": "object", "properties": {
|
|
605
|
+
"status": {"type": "string", "enum": ["open", "closed", "all"]}},
|
|
606
|
+
"additionalProperties": False}},
|
|
607
|
+
{"name": "thth_report_show",
|
|
608
|
+
"description": "報告 1 件の本文と実装側の返事を読む(読むだけ)",
|
|
609
|
+
"inputSchema": {"type": "object", "properties": {"report_id": {"type": "string"}},
|
|
610
|
+
"required": ["report_id"], "additionalProperties": False}},
|
|
611
|
+
# 報告した側の追記(設計 3.2.0 §4.5-1)。
|
|
612
|
+
{"name": "thth_report_add",
|
|
613
|
+
"description": "自分の project の開いている報告に書き足す(返事と同じ列に並ぶ・4,000 字まで・"
|
|
614
|
+
"閉じた報告には足せない・秘密らしき値が含まれていたら足さない)",
|
|
615
|
+
"inputSchema": {"type": "object", "properties": {
|
|
616
|
+
"report_id": {"type": "string"},
|
|
617
|
+
"text": {"type": "string", "description": "4,000 字まで"}},
|
|
618
|
+
"required": ["report_id", "text"], "additionalProperties": False}},
|
|
619
|
+
]
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# **うまくいかなければ報告の口へ**(設計 3.1.2 §3.5)。利用者の道具の説明文の
|
|
623
|
+
# 末尾に 1 句。定数(TOOLS 等)は書き換えず、並べるときに足す——「売り文句」の
|
|
624
|
+
# 正本(設計 v2 §1・「自分の泉」§2)はそのまま残る。
|
|
625
|
+
REPORT_HINT = "(うまくいかなければ thth_report_file)"
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _with_report_hint(tools):
|
|
629
|
+
return [tool if tool["name"].startswith("thth_report_")
|
|
630
|
+
else dict(tool, description=tool["description"] + REPORT_HINT) for tool in tools]
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _channel_note():
|
|
634
|
+
"""断りの応答に添える 2 つ目の text(静的な 1 行・本文も account 名も入れない)。"""
|
|
635
|
+
if APP_DIR not in sys.path:
|
|
636
|
+
sys.path.insert(0, APP_DIR)
|
|
637
|
+
from thth.report_inbox import CHANNEL_LINE
|
|
638
|
+
return {"type": "text", "text": CHANNEL_LINE}
|
|
639
|
+
|
|
640
|
+
|
|
569
641
|
def server_mode():
|
|
570
642
|
return 'THTH_REPORT_CREDENTIALS' in os.environ or 'THTH_REPORT_TOKEN' in os.environ
|
|
571
643
|
|
|
@@ -579,15 +651,16 @@ def server_tools(context):
|
|
|
579
651
|
# account/project しか対象にできない(`report_service.execute_morning()`)。
|
|
580
652
|
reports = reports + [tool for tool in TOOLS if tool['name']=='thth_morning']
|
|
581
653
|
from thth.server_writes import WRITE_OPERATIONS
|
|
582
|
-
return reports + [tool for tool in SERVER_TOOLS
|
|
583
|
-
if context.writes or tool['name'][5:] not in WRITE_OPERATIONS]
|
|
654
|
+
return _with_report_hint(reports + [tool for tool in SERVER_TOOLS
|
|
655
|
+
if context.writes or tool['name'][5:] not in WRITE_OPERATIONS]) + REPORT_TOOLS
|
|
584
656
|
|
|
585
657
|
|
|
586
658
|
def server_call(name, arguments):
|
|
587
659
|
from thth.report_service import execute_report, execute_mcp_report, ReportServiceError
|
|
588
660
|
from thth.server_writes import execute, WRITE_OPERATIONS, SAFE_ERRORS, DRAFT_REASONS
|
|
589
661
|
context=authenticated_context()
|
|
590
|
-
|
|
662
|
+
# 断りは 1 つ目の text が静的な理由、2 つ目が受け口の案内(設計 3.1.2 §3.5)。
|
|
663
|
+
failure=lambda value:{'content':[{'type':'text','text':value},_channel_note()],'isError':True}
|
|
591
664
|
if context is None: return failure('unauthorized')
|
|
592
665
|
tool=next((tool for tool in server_tools(context) if tool['name']==name),None)
|
|
593
666
|
if tool is None: return failure('unsupported_operation')
|
|
@@ -613,6 +686,12 @@ def server_call(name, arguments):
|
|
|
613
686
|
result=execute_admin_write(context,request)
|
|
614
687
|
elif operation in WRITE_OPERATIONS:
|
|
615
688
|
result=execute(context,request,via='mcp')
|
|
689
|
+
elif operation.startswith('admin_reports_'):
|
|
690
|
+
from thth.report_service import execute_admin_reports
|
|
691
|
+
result=execute_admin_reports(context,request)
|
|
692
|
+
elif operation in ('report_file','report_list','report_show','report_add'):
|
|
693
|
+
from thth.report_service import execute_user_reports
|
|
694
|
+
result=execute_user_reports(context,request)
|
|
616
695
|
elif operation=='morning':
|
|
617
696
|
from thth.report_service import execute_morning
|
|
618
697
|
result=execute_morning(context,request)
|
|
@@ -626,7 +705,11 @@ def server_call(name, arguments):
|
|
|
626
705
|
# 理由は静的な符丁の表にあるものだけ。lint の自由文は通さない。
|
|
627
706
|
detail=getattr(exc,'reason',None)
|
|
628
707
|
return failure('invalid_draft: '+(detail if detail in DRAFT_REASONS else 'validation_failed'))
|
|
629
|
-
|
|
708
|
+
from thth.report_inbox import REASONS as REPORT_REASONS
|
|
709
|
+
if str(exc)=='duplicate_report' and getattr(exc,'report_id',None):
|
|
710
|
+
# 既存の id は同じ account の報告だけ(重複の検査がそう絞っている)。
|
|
711
|
+
return failure('duplicate_report: '+exc.report_id)
|
|
712
|
+
return failure(str(exc) if str(exc) in SAFE_ERRORS or str(exc) in REPORT_REASONS or str(exc) in ('budget_change_durability_unconfirmed','budget_change_partially_recorded','budget_change_refused','watch_change_refused') else 'request_unavailable')
|
|
630
713
|
except Exception:
|
|
631
714
|
return failure('request_unavailable')
|
|
632
715
|
|
|
@@ -670,7 +753,7 @@ def call_tool(name: str, arguments: dict | None) -> dict:
|
|
|
670
753
|
if isinstance(name, str) and name.startswith("thth_admin_"):
|
|
671
754
|
context = admin_context()
|
|
672
755
|
if context is None:
|
|
673
|
-
return {"content": [{"type": "text", "text": "unauthorized"}], "isError": True}
|
|
756
|
+
return {"content": [{"type": "text", "text": "unauthorized"}, _channel_note()], "isError": True}
|
|
674
757
|
from thth.report_service import execute_report, ReportServiceError
|
|
675
758
|
try:
|
|
676
759
|
if "operation" in arguments: raise ReportServiceError("invalid_request")
|
|
@@ -679,7 +762,7 @@ def call_tool(name: str, arguments: dict | None) -> dict:
|
|
|
679
762
|
return {"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False)}]}
|
|
680
763
|
except (ValueError, TypeError, ReportServiceError) as error:
|
|
681
764
|
reason = "invalid_options" if str(error) == "invalid_options" else "invalid_request"
|
|
682
|
-
return {"content": [{"type": "text", "text": reason}], "isError": True}
|
|
765
|
+
return {"content": [{"type": "text", "text": reason}, _channel_note()], "isError": True}
|
|
683
766
|
if name == "thth_lint":
|
|
684
767
|
proc = run_cli(["lint", arguments["file"], "--json"])
|
|
685
768
|
text = proc.stdout
|
|
@@ -838,7 +921,7 @@ def call_tool(name: str, arguments: dict | None) -> dict:
|
|
|
838
921
|
proc = run_cli(args)
|
|
839
922
|
text = proc.stdout
|
|
840
923
|
else:
|
|
841
|
-
return {"content": [{"type": "text", "text": f"unknown tool: {name}"}], "isError": True}
|
|
924
|
+
return {"content": [{"type": "text", "text": f"unknown tool: {name}"}, _channel_note()], "isError": True}
|
|
842
925
|
|
|
843
926
|
if name.startswith("thth_topic_") or name in (
|
|
844
927
|
"before_you_post", "after_you_posted", "analytics_report", "operations_handoff", "study_report", "thread_read", "where_to_appear",
|
|
@@ -854,7 +937,11 @@ def call_tool(name: str, arguments: dict | None) -> dict:
|
|
|
854
937
|
is_error = proc.returncode not in (0, 1) # lint は 1 も正常な「検査結果」
|
|
855
938
|
if not text:
|
|
856
939
|
text = proc.stderr
|
|
857
|
-
|
|
940
|
+
content = [{"type": "text", "text": text}]
|
|
941
|
+
# 断りなら受け口の案内を 2 つ目に(CLI の理由行に既に載っていれば重ねない)。
|
|
942
|
+
if is_error and _channel_note()["text"] not in text:
|
|
943
|
+
content.append(_channel_note())
|
|
944
|
+
return {"content": content, "isError": is_error}
|
|
858
945
|
|
|
859
946
|
|
|
860
947
|
def _send(obj: dict) -> None:
|
|
@@ -5,6 +5,8 @@ description: 投稿する前・返信を書く前・出したあと・絡みに
|
|
|
5
5
|
|
|
6
6
|
**開始時は、まず `thth_morning`(CLI は `thth morning <project|account> --json`)を 1 回。** 昨日から何があって今日なにをすればよいかが 6 段(道具・返していないもの・昨日の自分・世間・予定・次の一手)で返る。`handoff-report --since-last-read` を包含するので、版の確認もこれ 1 回で済む。呼ぶと栞(前回読んだ時点)が進む——進めたくないときだけ `mark: false`(CLI は `--no-mark`)。取れなかった段は `{"value": null, "cannot_say": "<理由>"}`(`provider_timeout`・`budget_exhausted`・`scope_missing`・`no_watch_words`・`not_supported`・`unavailable`)で、**0 件とは別**。第 3 段の監視語は**管理者が入れる**(`thth admin watch set <account> 語 … --by <名前>`)——語が無い account は `no_watch_words` で飛ぶので、**代わりに語を選ばない**。第 5 段は候補の列挙だけで、**本文は返らない**(書くのはあなたと人の会話の側)。詳しくは `docs/使い方_毎朝の一枚_3.1.md`。
|
|
7
7
|
|
|
8
|
+
**断られたら・違和感があれば報告の口へ**: `thth_report_file`(CLI は `thth report file <account> --kind bug|request --title … --body-file … --by <名前>`)。道具が断った・結果が期待と違った・欲しい形がある、のどれかなら 1 回で置ける。返事は `thth_report_show` と `handoff-report --since-last-read` の `tool.reports` に出る。秘密らしき値が含まれていたら置かれない(伏せてから置き直す)。
|
|
9
|
+
|
|
8
10
|
段だけを読み直したいときは `thth handoff-report <account> --since-last-read --json` を読む。`tool.changed_since_last_read` が真なら、手元の `tool.notes_root_local_hint` を手掛かりに `tool.release_notes` の相対名を読んでから作業する。既読の記録は `--mark-read --by <名前>` の明示時だけ。`tool.notes_reason` が `notes_directory_unavailable` なら、このインストールには読める docs がない。空の `release_notes` を「変更なし」と解釈しない。
|
|
9
11
|
|
|
10
12
|
添付を付ける前に `tool.capabilities` を読む(媒体ごとの「出せるもの」表。`thth doctor` にも同じ表が出る。`unverified` は「非対応」ではない)。
|
|
@@ -106,6 +108,12 @@ found_by: where_to_appear|manual|mention
|
|
|
106
108
|
untracked/コミット前のファイル)。語を選ぶときだけ `before_you_post`
|
|
107
109
|
(原稿本文は渡さない・件数と期間つきで返す)。
|
|
108
110
|
|
|
111
|
+
**まだ出ていない自分の原稿へ返すとき(3.2.0)**: `reply_to` の代わりに
|
|
112
|
+
`reply_to_file: <同じ queue・同じ account の原稿のファイル名>` と書く(パスは書けない・
|
|
113
|
+
`reply_to` との併用不可)。指した原稿が出るまで待ち、出たらその post_id に返す——
|
|
114
|
+
**root には落とさない**。待っている間は `thth board`・`thth morning` に「返信待ち」として
|
|
115
|
+
名前と待ち先が出る(時刻超過には数えない)。承認の指紋は `file:<名前>`。
|
|
116
|
+
|
|
109
117
|
### 5. 承認——二段(そのまま)
|
|
110
118
|
|
|
111
119
|
```bash
|
|
@@ -141,6 +149,8 @@ MCP `after_you_posted`。**24h の刻みが無ければ `null`**(0 と混ぜ
|
|
|
141
149
|
|
|
142
150
|
- `thth --help` / `thth <subcommand> --help` が正本。
|
|
143
151
|
- `thth doctor <account>` が「いま投稿できる状態か」を最初から最後まで言う。
|
|
152
|
+
- 道具の不具合・欲しい形は `thth report file`(MCP `thth_report_file`)で実装側に届ける。
|
|
153
|
+
- 自分の開いている報告に書き足すなら `thth report add <report_id> --body-file <path|-> --by <名前>`(MCP `thth_report_add`)。
|
|
144
154
|
- 使い方の全文: `docs/使い方_プロジェクトのセッション向け_2026-09-09.md`。
|
|
145
155
|
|
|
146
156
|
## 運用と分析のレポート(v3系・2.5.0 から)
|
|
@@ -3,7 +3,7 @@ name: thth-admin
|
|
|
3
3
|
description: Read authenticated THTH administrator inventory, changes and token status; hand findings to a person.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
開始時は `thth admin diff --since-last-read --json` を読み、変化があった account を `thth admin account <name> --json`
|
|
6
|
+
開始時は `thth admin diff --since-last-read --json` を読み、変化があった account を `thth admin account <name> --json` で確認する。利用者からの不具合と要望は `thth admin reports list --json`(MCP `thth_admin_reports_list`・既定は開いているものだけ)で読み、`show <id>` で全文を見る。返事は `thth admin reports reply <id> --by <名前> --text-file <file>`、閉じるときは `close <id> --by <名前> --reason fixed|wontfix|duplicate|invalid --version <版>`。返事と閉じるは人(masaru)の判断を経てから打つ。写しを残すときは `thth admin reports export --to <私有の dir> --by <名前>`。**私有の置き場へ。公開 repo には置かない**(本文は利用者が書いた文。既定の行き先は無い)。週1回 `thth admin tokens --json` を確認する。
|
|
7
7
|
|
|
8
8
|
読んだ結果から承認・投稿・設定変更をしない。必要な対処は根拠・欠測理由・観測時刻とともに人に渡す。記録が無い null は正常・停止・期限なしの証拠ではない。API probe は人が明示的に必要とした場合の CLI `--probe` のみ。
|
|
9
9
|
|
|
@@ -35,8 +35,10 @@ DEFAULT_FM = {
|
|
|
35
35
|
"posted_at": None,
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
# `reply_to_file`(設計 3.2.0)・`retracted_at` は既定の front-matter に入れない
|
|
39
|
+
# (`fm_overrides` で渡したときだけ書く)——既存の試験の原稿を 1 行も変えないため。
|
|
38
40
|
FM_ORDER = ["thth", "account", "publish_at", "status", "approved_sha", "approved_at",
|
|
39
|
-
"topic", "reply_to", "post_id", "posted_at"]
|
|
41
|
+
"topic", "reply_to", "reply_to_file", "post_id", "posted_at", "retracted_at"]
|
|
40
42
|
|
|
41
43
|
# quiet_hours の既定(22:00〜07:00)の外にある値。frozen_now_jst() の既定値に使う。
|
|
42
44
|
FIXED_NOW_JST = datetime.datetime(2026, 9, 9, 10, 0, 0, tzinfo=jst.JST)
|
|
@@ -143,7 +145,7 @@ def make_queue_text(fm_overrides=None, body="## threads\n\n本文です。\n",
|
|
|
143
145
|
try:
|
|
144
146
|
fm["approved_sha"] = approval_mod.compute_approved_sha(
|
|
145
147
|
section=section, account=fm.get("account"),
|
|
146
|
-
reply_to=
|
|
148
|
+
reply_to=approval_mod.reply_to_for_fingerprint(fm), topic=fm.get("topic"),
|
|
147
149
|
publish_at=publish_at)
|
|
148
150
|
except ValueError:
|
|
149
151
|
pass # publish_at が壊れている型外テスト等: 計算できないので触らない
|
|
@@ -39,7 +39,9 @@ def test_admin_all_registry_and_mcp_auth_rechecked(credentials,monkeypatch):
|
|
|
39
39
|
assert server.admin_context().scope=='admin'
|
|
40
40
|
assert {tool['name'] for tool in server.ADMIN_TOOLS}=={
|
|
41
41
|
'thth_admin_inventory','thth_admin_account','thth_admin_diff','thth_admin_log',
|
|
42
|
-
'thth_admin_tokens','thth_admin_release','thth_admin_budget_set','thth_admin_watch_set'
|
|
42
|
+
'thth_admin_tokens','thth_admin_release','thth_admin_budget_set','thth_admin_watch_set',
|
|
43
|
+
# 報告の口の実装側(設計 3.1.2 §1)。
|
|
44
|
+
'thth_admin_reports_list','thth_admin_reports_show','thth_admin_reports_reply','thth_admin_reports_close'}
|
|
43
45
|
result=server.call_tool('thth_admin_inventory',{})
|
|
44
46
|
assert set(json.loads(result['content'][0]['text'])['by_account'])=={'first','second'}
|
|
45
47
|
assert server.call_tool('thth_admin_diff',{'mark_read':True})['isError']
|
|
@@ -25,7 +25,10 @@ def json_parsers():
|
|
|
25
25
|
# must acquire a table row, rather than quietly escaping the contract.
|
|
26
26
|
COMMANDS = '''lint|preview|approve|account|revoke|posts|replies|measured|threads|study-report|analytics-report|after|topics|forms|queue|schedule|throw|handoff-report|board|pull|maintain|doctor|app show|admin inventory|admin account|admin log|admin tokens|admin timers|admin release|admin diff|ask before-you-post|mentions|profile|thread|where|who|retract|location|topics suggest|topics observe|topics record-decision|topics decision|topics observation|topics retract|topics unretract|topics profile|topics adopt-reason|topics adoptions|topics record-vocabulary|topics vocabulary|topics record-review|topics review|topics record-form-spec|topics form-spec|topics form-check|topics improvements|topics impact|topics record-hypothesis|topics hypotheses'''.split('|') + ['account add', 'account migrate', 'unanswered', 'study add', 'admin deletion sync', 'admin budget', 'admin media cleanup-retry', 'admin media gc',
|
|
27
27
|
# 3.1.0: 毎朝の一枚と監視語(設計 3.1.0 §1・§3)。
|
|
28
|
-
'morning', 'admin watch set', 'admin watch show'
|
|
28
|
+
'morning', 'admin watch set', 'admin watch show',
|
|
29
|
+
# 3.1.2: 報告の口(設計 3.1.2 §1)。
|
|
30
|
+
'report file', 'report list', 'report show', 'report add', 'admin reports list', 'admin reports show',
|
|
31
|
+
'admin reports reply', 'admin reports close', 'admin reports export']
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
def test_json_table_covers_every_parser():
|
|
@@ -149,7 +149,8 @@ def test_shared_manual_grant_does_not_tell_person_to_revoke_other_account(env,wo
|
|
|
149
149
|
a=env['root']/'secrets/alpha.json';b=env['root']/'secrets/beta.json';b.write_bytes(a.read_bytes())
|
|
150
150
|
assert leave.command(SimpleNamespace(name='alpha',by='operator',json=False))==0
|
|
151
151
|
result=leave.read('alpha')
|
|
152
|
-
|
|
152
|
+
# 3.1.2 件 6・裁定 09-23: 値だけの共有(別ファイル)なら自分の file は消し、相手の file は残す。
|
|
153
|
+
assert result['remote']=='unconfirmed_shared' and not a.exists() and b.exists()
|
|
153
154
|
out=capsys.readouterr().out
|
|
154
155
|
assert '切り分け' in out and '本人が解除' not in out
|
|
155
156
|
|
|
@@ -84,8 +84,11 @@ def test_shared_mastodon_grant_refuses_remote_revoke_but_shared_client_alone_doe
|
|
|
84
84
|
cfg,tokenpath=configure(env,provider,monkeypatch,'mastodon');old=tokenpath.read_bytes()
|
|
85
85
|
other=env['root']/'accounts/beta.json';b=json.loads(other.read_text());b.update(media='mastodon',instance=provider['base']);other.write_text(json.dumps(b))
|
|
86
86
|
(env['root']/'secrets/beta.json').write_bytes(old)
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
# 3.1.2 件 6: 共有の接続は遠隔で失効させず(calls==[])、止まらずに退出を終える。
|
|
88
|
+
# 裁定 09-23: 値だけの共有(別ファイル)なら自分の token file は消す。相手の file は残る。
|
|
89
|
+
result=leave.run('alpha',by='operator')
|
|
90
|
+
assert result['phase']=='completed' and result['remote']=='unconfirmed_shared' and 'token_shared' not in result['preserved']
|
|
91
|
+
assert provider['calls']==[] and not tokenpath.exists() and (env['root']/'secrets/beta.json').read_bytes()==old
|
|
89
92
|
|
|
90
93
|
|
|
91
94
|
def test_b_credential_commit_before_revoke_is_rechecked_after_inventory(env,provider,monkeypatch):
|
|
@@ -99,8 +102,11 @@ def test_b_credential_commit_before_revoke_is_rechecked_after_inventory(env,prov
|
|
|
99
102
|
authflow.commit_manual('beta',cfg_b,token_a,snapshot=snapshot,session=None,by='operator')
|
|
100
103
|
return {'status':'revoked'}
|
|
101
104
|
monkeypatch.setattr(approval_relay,'signed_request',worker)
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
# 3.1.2 件 6: 共有の接続は遠隔で失効させず(calls==[])、止まらずに退出を終える。
|
|
106
|
+
# 裁定 09-23: 値だけの共有(別ファイル)なら自分の token file は消す。相手の file は残る。
|
|
107
|
+
result=leave.run('alpha',by='operator')
|
|
108
|
+
assert result['phase']=='completed' and result['remote']=='unconfirmed_shared' and 'token_shared' not in result['preserved']
|
|
109
|
+
assert provider['calls']==[] and not tokenpath.exists()
|
|
104
110
|
assert json.loads((env['root']/'secrets/beta.json').read_text())==token_a
|
|
105
111
|
|
|
106
112
|
|
|
@@ -166,5 +172,7 @@ def test_b_exchange_to_commit_holds_registry_lease_and_late_shared_grant_vetoes_
|
|
|
166
172
|
assert a.is_alive() and provider['calls']==[]
|
|
167
173
|
release.set();a.join(4);b.join(4)
|
|
168
174
|
assert not a.is_alive() and not b.is_alive() and results==[0]
|
|
169
|
-
|
|
175
|
+
# 3.1.2 件 6: 共有の接続は遠隔で失効させず(calls==[])、止まらずに退出を終える。
|
|
176
|
+
assert leave_errors==[] and provider['calls']==[]
|
|
177
|
+
assert leave.read('alpha')['phase']=='completed' and leave.read('alpha')['remote']=='unconfirmed_shared'
|
|
170
178
|
assert accounts.load_token(accounts.load_account('beta'))['access_token']==value['access_token']
|
|
@@ -61,7 +61,7 @@ def test_cli_reads_do_not_wait_or_call_core_when_leave_holds_exclusive(env,conte
|
|
|
61
61
|
start=time.monotonic();rc=cli.main(command);elapsed=time.monotonic()-start
|
|
62
62
|
captured=capsys.readouterr();assert rc==2 and elapsed<1
|
|
63
63
|
assert json.loads(captured.out)=={'error':'account_leaving','cannot_say':['account_leaving']}
|
|
64
|
-
assert captured.err.strip()=='account_leaving'
|
|
64
|
+
assert captured.err.strip()=='account_leaving\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
def test_dry_send_input_then_nonwaiting_lease(env,tmp_path,capsys):
|
|
@@ -69,7 +69,7 @@ def test_dry_send_input_then_nonwaiting_lease(env,tmp_path,capsys):
|
|
|
69
69
|
with exclusive():
|
|
70
70
|
start=time.monotonic();rc=cli.main(['send','alpha','--text-file',str(text)])
|
|
71
71
|
assert time.monotonic()-start<1 and rc==2
|
|
72
|
-
assert capsys.readouterr().err.strip()=='account_leaving'
|
|
72
|
+
assert capsys.readouterr().err.strip()=='account_leaving\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
|
|
73
73
|
assert not (env['root']/'state/alpha/sent').exists()
|
|
74
74
|
|
|
75
75
|
|
|
@@ -145,7 +145,7 @@ def test_real_cli_child_rejects_ex_under_deadline(env,content):
|
|
|
145
145
|
result=subprocess.run([sys.executable,'-c',"from thth.cli import main;raise SystemExit(main(['queue','alpha','--json']))"],capture_output=True,text=True,timeout=2)
|
|
146
146
|
except subprocess.TimeoutExpired:pytest.fail('reader waited for the exit drain')
|
|
147
147
|
assert result.returncode==2 and json.loads(result.stdout)['cannot_say']==['account_leaving']
|
|
148
|
-
assert result.stderr.strip()=='account_leaving'
|
|
148
|
+
assert result.stderr.strip()=='account_leaving\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
|
|
149
149
|
|
|
150
150
|
|
|
151
151
|
def test_report_holds_lease_through_calculation_and_release(env,monkeypatch):
|
|
@@ -51,7 +51,7 @@ raise SystemExit(cli.main(json.loads(sys.argv[1])))
|
|
|
51
51
|
# cannot trap this pytest process before an elapsed-time assertion.
|
|
52
52
|
result = subprocess.run([sys.executable, '-c', code, json.dumps(command), str(marker)],
|
|
53
53
|
capture_output=True, text=True, timeout=3)
|
|
54
|
-
assert result.returncode == 2 and result.stderr.strip() == 'account_leaving'
|
|
54
|
+
assert result.returncode == 2 and result.stderr.strip() == 'account_leaving\n' + __import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
|
|
55
55
|
if operation == 'replies':
|
|
56
56
|
assert json.loads(result.stdout) == {'error': 'account_leaving', 'cannot_say': ['account_leaving']}
|
|
57
57
|
assert not marker.exists()
|
|
@@ -14,7 +14,7 @@ def test_run_invalid_name_before_any_storage_or_effect(monkeypatch,capsys,name):
|
|
|
14
14
|
monkeypatch.setattr('thth.incident.notify',forbidden)
|
|
15
15
|
assert cli.main(['run',name])==2
|
|
16
16
|
captured=capsys.readouterr();value=json.loads(captured.out)
|
|
17
|
-
assert captured.err.strip()=='invalid_account_name'
|
|
17
|
+
assert captured.err.strip()=='invalid_account_name\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
|
|
18
18
|
assert value['error']==value['record_unavailable']=='invalid_account_name'
|
|
19
19
|
assert value['account'] is None and value['runs_recorded'] is False
|
|
20
20
|
assert '使えない字' in value['message'] and 'account_stopped' not in captured.out
|
|
@@ -30,7 +30,7 @@ def test_run_reason_and_no_side_effect(env,monkeypatch,capsys,state,expected):
|
|
|
30
30
|
monkeypatch.setattr('thth.healthcheck.notify',lambda *a,**kw:pytest.fail('healthcheck notification'))
|
|
31
31
|
monkeypatch.setattr('thth.incident.notify',lambda *a,**kw:pytest.fail('incident notification'))
|
|
32
32
|
assert cli.main(['run','alpha'])==2
|
|
33
|
-
shown=capsys.readouterr();assert shown.err.strip()==expected
|
|
33
|
+
shown=capsys.readouterr();assert shown.err.strip()==expected+'\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
|
|
34
34
|
value=json.loads(shown.out)
|
|
35
35
|
assert value=={'account':'alpha','mode':'production','action':'skip','status':'error','error':expected,
|
|
36
36
|
'runs_recorded':False,'record_unavailable':expected}
|
|
@@ -45,14 +45,16 @@ def test_same_user_different_token_leave_completes_and_leaves_other_account_unto
|
|
|
45
45
|
assert accounts.load_token(accounts.load_account('beta'))['access_token']==other_access
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
def
|
|
48
|
+
def test_same_token_bytes_do_not_revoke_remotely(env,provider,monkeypatch):
|
|
49
49
|
_,tokenpath=configure(env,provider,monkeypatch,'mastodon')
|
|
50
50
|
beta_token,_=same_user_beta(env,provider,tokenpath,'mastodon',same_bytes=True)
|
|
51
51
|
old=tokenpath.read_bytes()
|
|
52
|
-
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
assert
|
|
52
|
+
# 3.1.2 件 6: 失効は投げない(他方も死ぬ)。止まらずに completed まで進む
|
|
53
|
+
# (完了までの形は tests/test_v312_leave_shared_token.py)。
|
|
54
|
+
result=leave.run('alpha',by='operator')
|
|
55
|
+
assert result['phase']=='completed' and result['remote']=='unconfirmed_shared'
|
|
56
|
+
# 裁定 09-23: 値だけの共有(別ファイル)なら自分の token file は消す。相手の file は残る。
|
|
57
|
+
assert provider['calls']==[] and not tokenpath.exists() and beta_token.read_bytes()==old
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
def test_threads_same_user_is_still_token_shared_and_preserved(env,monkeypatch):
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""3.1.2 件 6: 退出で token が他の生きている account と共有なら、遠隔で失効させずに終える。
|
|
2
|
+
|
|
3
|
+
実測(2026-09-23 12:50): 同じ app で同じ利用者が認可すると Mastodon は既存の access token を
|
|
4
|
+
返すので、kopicha-server-mastodon と kopicha-mastodon の token は値が同じだった。失効すると
|
|
5
|
+
他方も死ぬので投げない(判定は正しい)——が、**退出が `worker_revoked` で止まるのは誤り**。
|
|
6
|
+
|
|
7
|
+
masaru 裁定(09-23): **値だけの共有(別ファイル・別 inode)なら、自分の token file と env は
|
|
8
|
+
消して完了する**(退出した account の秘密を VM に残さない・消しても失効にはならず、相手は
|
|
9
|
+
自分のファイルを持つ)。**同じファイル・同じ inode の共有**(path 一致・dev/ino 一致・app
|
|
10
|
+
directory 配下)のときだけ従前どおり残す(`preserved: ['token_shared']`)。
|
|
11
|
+
|
|
12
|
+
見るのは(fake Mastodon・loopback):
|
|
13
|
+
(a) 値だけ共有 → completed・`remote: unconfirmed_shared`・`/oauth/revoke` は呼ばれない・
|
|
14
|
+
自分の token file は無い・他方の token file は byte 一致・他方は停止しない。
|
|
15
|
+
(a') ファイル共有(同じ path)→ 自分の token file は残る(相手のファイルでもあるので)。
|
|
16
|
+
(b) 値が違う 2 台帳 → 3.1.1 どおり自分の token だけ失効して completed。
|
|
17
|
+
(c) 3.1.1 の形で `worker_revoked` に止まった row(token を消す対象から外していた)からの
|
|
18
|
+
再実行が (a) で完了し、自分の token file も消える。
|
|
19
|
+
"""
|
|
20
|
+
import json
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
from thth import accounts, leave, leave_gate
|
|
25
|
+
from tests.test_v212_server_writes import env # noqa: F401(fixture)
|
|
26
|
+
from tests.test_v212_leave_providers import provider, configure # noqa: F401(fixture)
|
|
27
|
+
from tests.test_v311_leave_same_user import same_user_beta
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _assert_other_untouched(beta_token, beta_ledger, token_bytes, ledger_bytes):
|
|
31
|
+
assert beta_token.read_bytes() == token_bytes and beta_ledger.read_bytes() == ledger_bytes
|
|
32
|
+
assert not leave_gate.stopped("beta") and leave.read("beta") is None
|
|
33
|
+
accounts.load_account("beta")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_a_値だけ共有なら失効を投げず自分のtoken_fileは消して完了する(env, provider, monkeypatch, capsys):
|
|
37
|
+
_, tokenpath = configure(env, provider, monkeypatch, "mastodon")
|
|
38
|
+
beta_token, beta_ledger = same_user_beta(env, provider, tokenpath, "mastodon", same_bytes=True)
|
|
39
|
+
token_bytes, ledger_bytes = beta_token.read_bytes(), beta_ledger.read_bytes()
|
|
40
|
+
alpha_ledger = env["root"] / "accounts/alpha.json"
|
|
41
|
+
alpha_state = env["root"] / "state/alpha"
|
|
42
|
+
|
|
43
|
+
class Args:
|
|
44
|
+
name, by, json = "alpha", "operator", False
|
|
45
|
+
assert leave.command(Args()) == 0
|
|
46
|
+
out = capsys.readouterr().out
|
|
47
|
+
assert "remote_unconfirmed: 他 account と共有の接続です。" in out
|
|
48
|
+
row = leave.read("alpha")
|
|
49
|
+
assert row["phase"] == "completed" and row["remote"] == "unconfirmed_shared"
|
|
50
|
+
assert "token_shared" not in row["preserved"] and row["deleted"]["token"] == 1
|
|
51
|
+
assert provider["calls"] == [] # /oauth/revoke は呼ばれない
|
|
52
|
+
assert not tokenpath.exists() # 退出した account の秘密は残さない
|
|
53
|
+
assert not alpha_ledger.exists() and not alpha_state.exists()
|
|
54
|
+
_assert_other_untouched(beta_token, beta_ledger, token_bytes, ledger_bytes)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_a2_ファイルごとの共有なら自分のtoken_fileも残す(env, provider, monkeypatch):
|
|
58
|
+
_, tokenpath = configure(env, provider, monkeypatch, "mastodon")
|
|
59
|
+
beta_token, beta_ledger = same_user_beta(env, provider, tokenpath, "mastodon", same_bytes=True)
|
|
60
|
+
# beta の台帳が alpha と同じ token file を指す(path 一致)。
|
|
61
|
+
ledger = json.loads(beta_ledger.read_text())
|
|
62
|
+
ledger["token"] = str(tokenpath)
|
|
63
|
+
beta_ledger.write_text(json.dumps(ledger))
|
|
64
|
+
token_bytes, ledger_bytes = tokenpath.read_bytes(), beta_ledger.read_bytes()
|
|
65
|
+
result = leave.run("alpha", by="operator")
|
|
66
|
+
assert result["phase"] == "completed" and result["remote"] == "unconfirmed_shared"
|
|
67
|
+
assert result["preserved"] == ["token_shared"] and "token" not in result["deleted"]
|
|
68
|
+
assert provider["calls"] == []
|
|
69
|
+
_assert_other_untouched(tokenpath, beta_ledger, token_bytes, ledger_bytes)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_b_値が違う2台帳は自分のtokenだけ失効して完了する(env, provider, monkeypatch):
|
|
73
|
+
_, tokenpath = configure(env, provider, monkeypatch, "mastodon")
|
|
74
|
+
beta_token, beta_ledger = same_user_beta(env, provider, tokenpath, "mastodon")
|
|
75
|
+
token_bytes, ledger_bytes = beta_token.read_bytes(), beta_ledger.read_bytes()
|
|
76
|
+
result = leave.run("alpha", by="operator")
|
|
77
|
+
assert result["phase"] == "completed" and result["remote"] == "confirmed"
|
|
78
|
+
sent = [form["token"][0] for path, form, _ in provider["calls"]]
|
|
79
|
+
assert [path for path, _, _ in provider["calls"]] == ["/oauth/revoke"]
|
|
80
|
+
assert sent == [provider["access"]] and not tokenpath.exists()
|
|
81
|
+
_assert_other_untouched(beta_token, beta_ledger, token_bytes, ledger_bytes)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _stuck_at_worker_revoked(monkeypatch):
|
|
85
|
+
"""停止と worker の失効までは進み、遠隔の段の手前で落ちた row を作る。"""
|
|
86
|
+
original = leave_gate.credentials
|
|
87
|
+
monkeypatch.setattr(leave_gate, "credentials",
|
|
88
|
+
lambda *a, **k: (_ for _ in ()).throw(ValueError("stop_here")))
|
|
89
|
+
with pytest.raises(ValueError):
|
|
90
|
+
leave.run("alpha", by="operator")
|
|
91
|
+
monkeypatch.setattr(leave_gate, "credentials", original)
|
|
92
|
+
row = leave.read("alpha")
|
|
93
|
+
assert row["phase"] == "worker_revoked" and leave_gate.stopped("alpha")
|
|
94
|
+
return row
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_c_31_1の形でworker_revokedに止まったrowから再実行して完了する(env, provider, monkeypatch):
|
|
98
|
+
_, tokenpath = configure(env, provider, monkeypatch, "mastodon")
|
|
99
|
+
beta_token, beta_ledger = same_user_beta(env, provider, tokenpath, "mastodon", same_bytes=True)
|
|
100
|
+
token_bytes, ledger_bytes = beta_token.read_bytes(), beta_ledger.read_bytes()
|
|
101
|
+
row = _stuck_at_worker_revoked(monkeypatch)
|
|
102
|
+
# 3.1.1 は値だけの共有でも token を消す対象から外し preserved に入れていた。その形に戻す。
|
|
103
|
+
row["targets"] = [target for target in row["targets"] if target["category"] != "token"]
|
|
104
|
+
row["inventory_sha256"] = leave._hash(row["targets"])
|
|
105
|
+
row["preserved"] = ["token_shared"]
|
|
106
|
+
assert row["token_shared"] is True
|
|
107
|
+
leave._save(row)
|
|
108
|
+
result = leave.run("alpha", by="operator")
|
|
109
|
+
assert result["phase"] == "completed" and result["remote"] == "unconfirmed_shared"
|
|
110
|
+
assert provider["calls"] == [] and not tokenpath.exists()
|
|
111
|
+
assert "token_shared" not in result["preserved"]
|
|
112
|
+
_assert_other_untouched(beta_token, beta_ledger, token_bytes, ledger_bytes)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_ファイル共有なのに消す対象にtokenが残るrowは矛盾として止める(env, provider, monkeypatch):
|
|
116
|
+
_, tokenpath = configure(env, provider, monkeypatch, "mastodon")
|
|
117
|
+
row = _stuck_at_worker_revoked(monkeypatch)
|
|
118
|
+
assert any(target["category"] == "token" for target in row["targets"])
|
|
119
|
+
# ファイルごと共有と記録したのに token を消す対象に残した row。
|
|
120
|
+
row["token_shared"] = True
|
|
121
|
+
row["preserved"] = sorted(set(row["preserved"]) | {"token_shared"})
|
|
122
|
+
leave._save(row)
|
|
123
|
+
with pytest.raises(ValueError, match="^shared_credential_revoke_refused$"):
|
|
124
|
+
leave.run("alpha", by="operator")
|
|
125
|
+
assert provider["calls"] == [] and tokenpath.exists() and leave_gate.stopped("alpha")
|
|
126
|
+
assert leave.read("alpha")["phase"] == "worker_revoked"
|