thth 3.3.0__tar.gz → 3.4.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.3.0 → thth-3.4.0}/PKG-INFO +2 -2
- {thth-3.3.0 → thth-3.4.0}/README.md +1 -1
- {thth-3.3.0 → thth-3.4.0}/mcp/server.py +95 -1
- {thth-3.3.0 → thth-3.4.0}/skills/thth/SKILL.md +5 -2
- {thth-3.3.0 → thth-3.4.0}/tests/conftest.py +10 -0
- thth-3.4.0/tests/helpers/fake_threads_status.py +123 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_admin_http_mcp.py +4 -1
- {thth-3.3.0 → thth-3.4.0}/tests/test_incident_notifications.py +4 -2
- {thth-3.3.0 → thth-3.4.0}/tests/test_site.py +16 -1
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_json_contract.py +7 -1
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_approval_relay.py +4 -1
- thth-3.4.0/tests/test_v331_inflight_cli.py +156 -0
- thth-3.4.0/tests/test_v331_listing_resolve.py +136 -0
- thth-3.4.0/tests/test_v331_outbox_retention.py +135 -0
- thth-3.4.0/tests/test_v331_publish_status.py +213 -0
- thth-3.4.0/tests/test_v331_self_resolve.py +222 -0
- thth-3.4.0/tests/test_v340_plaza_cli.py +136 -0
- thth-3.4.0/tests/test_v340_plaza_docs.py +69 -0
- thth-3.4.0/tests/test_v340_plaza_fields.py +109 -0
- thth-3.4.0/tests/test_v340_plaza_mcp.py +235 -0
- thth-3.4.0/tests/test_v340_plaza_morning.py +205 -0
- thth-3.4.0/tests/test_v340_plaza_observe.py +238 -0
- thth-3.4.0/tests/test_v340_plaza_open.py +338 -0
- thth-3.4.0/tests/test_v340_plaza_open_confirm.py +118 -0
- thth-3.4.0/tests/test_v340_plaza_reply.py +181 -0
- thth-3.4.0/tests/test_v340_plaza_store.py +205 -0
- thth-3.4.0/thth/VERSION +1 -0
- {thth-3.3.0 → thth-3.4.0}/thth/account_cli.py +4 -1
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/base.py +6 -0
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/threads.py +177 -7
- {thth-3.3.0 → thth-3.4.0}/thth/admin_log.py +9 -2
- {thth-3.3.0 → thth-3.4.0}/thth/admin_report.py +3 -0
- {thth-3.3.0 → thth-3.4.0}/thth/cli.py +14 -0
- {thth-3.3.0 → thth-3.4.0}/thth/core.py +92 -13
- {thth-3.3.0 → thth-3.4.0}/thth/healthcheck.py +11 -1
- {thth-3.3.0 → thth-3.4.0}/thth/incident.py +77 -3
- {thth-3.3.0 → thth-3.4.0}/thth/inflight.py +61 -8
- thth-3.4.0/thth/inflight_cli.py +301 -0
- thth-3.4.0/thth/inflight_resolve.py +435 -0
- {thth-3.3.0 → thth-3.4.0}/thth/leave.py +13 -2
- {thth-3.3.0 → thth-3.4.0}/thth/leave_gate.py +1 -1
- {thth-3.3.0 → thth-3.4.0}/thth/morning.py +91 -4
- thth-3.4.0/thth/plaza.py +1279 -0
- thth-3.4.0/thth/plaza_cli.py +437 -0
- thth-3.4.0/thth/plaza_observe.py +195 -0
- thth-3.4.0/thth/plaza_redact.py +229 -0
- thth-3.4.0/thth/private_store.py +271 -0
- {thth-3.3.0 → thth-3.4.0}/thth/report_inbox.py +34 -168
- {thth-3.3.0 → thth-3.4.0}/thth/report_service.py +159 -0
- {thth-3.3.0 → thth-3.4.0}/thth/runs.py +8 -0
- thth-3.3.0/thth/VERSION +0 -1
- {thth-3.3.0 → thth-3.4.0}/.gitignore +0 -0
- {thth-3.3.0 → thth-3.4.0}/LICENSE +0 -0
- {thth-3.3.0 → thth-3.4.0}/accounts.example/bluesky.json +0 -0
- {thth-3.3.0 → thth-3.4.0}/accounts.example/mastodon.json +0 -0
- {thth-3.3.0 → thth-3.4.0}/accounts.example/threads.json +0 -0
- {thth-3.3.0 → thth-3.4.0}/accounts.example/x.json +0 -0
- {thth-3.3.0 → thth-3.4.0}/bin/thth +0 -0
- {thth-3.3.0 → thth-3.4.0}/bin/thth-remote +0 -0
- {thth-3.3.0 → thth-3.4.0}/bin/thth-run +0 -0
- {thth-3.3.0 → thth-3.4.0}/pyproject.toml +0 -0
- {thth-3.3.0 → thth-3.4.0}/skills/thth-admin/SKILL.md +0 -0
- {thth-3.3.0 → thth-3.4.0}/systemd/thth@.service +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/__init__.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/coordination_snapshot.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/fixtures/build_injection_eval.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/fixtures/callback/worker_responses.json +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/fixtures/injection_eval.json +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/fixtures/umami-bile.md +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/helpers/__init__.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/helpers/fake_oauth_server.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/helpers/fake_push_adapter.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/helpers/fake_redirect_server.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/helpers/fake_search_adapter.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/helpers/fake_threads_reader.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/helpers/fake_threads_writes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/helpers/fake_x.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/helpers/hold_lock.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/helpers/hold_repo_lock.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/helpers/worker_probe.mjs +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_27_acceptance_fixes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_account_name_check.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_account_report.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_accounts_dir.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_adapter_boundary.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_admin_log.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_admin_notifications.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_admin_repairs.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_admin_report.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_after_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_analytics_comparison.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_analytics_report.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_api_diagnostic.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_api_error_body.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_app_env_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_approval.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_approve_cmd.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_ask.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_atlas_fifth_review.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_atlas_fourth_review.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_atlas_review.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_atlas_second_review.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_atlas_third_review.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_audit1_fixes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_audit2_fixes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_audit_b_counterexamples.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_audit_b_m1_semantics.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_audit_b_n1_depth.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_audit_b_r1_closure.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_audit_v201.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_audit_v23_details.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_auth.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_auth_identity_required.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_behind_remote.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_bluesky_adapter.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_bluesky_secret_scrub.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_board_needs_review.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_board_running.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_bundle.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_charcount.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_cli_entrypoint.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_cli_help.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_cli_vm_path.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_collect.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_collect_engagements.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_collect_runs.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_collect_sent.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_comparability.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_control_chars_refused.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_conversation_paging.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_count_for.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_docs_links.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_doctor.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_doctor_all_permissions.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_doctor_next_step.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_duplicate_front_matter_keys.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_editorial_audit.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_editorial_n3_n4.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_engagements.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_external_acceptance.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_fake_api.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_form_spec.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_form_spec_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_forms.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_fresh_install.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_front_matter_injection.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_healthcheck_notifications.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_hypotheses.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_hypothesis_boundaries.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_hypothesis_units.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_impact_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_inbox_granted_scopes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_independent_thread_review.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_independent_topic_review.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_independent_topic_review_r2.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_inflight.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_inflight_tag_body_hash.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_jst.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_lint.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_list_projections.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_llm_trial.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_llm_trial_engage.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_lock.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_m2_independent.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_maintain.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_mastodon_adapter.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_mastodon_visibility.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_max_per_run.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_mcp.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_mcp_robustness.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_measured.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_media_accounts.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_media_next_steps.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_media_roundtrip.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_mismatch_diagnostics.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_n5_reply_null_element.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_n6_replies_incomplete_ledger.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_n7_doctor_error_body.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_no_limits.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_observation_provenance.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_operations_handoff.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_packaging.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_preview.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_production_mode.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_pull_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_queue.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_quiet_hours.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_r2_r4_r5.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_read_cli_bare_runtimeerror.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_reason_adoption.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_redact.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_redirect_token_leak.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_refresh.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_refresh_destination.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_rejudge_new.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_rejudge_r2_r3.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_replies.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_replies_refresh.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_repo_lock.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_repo_sync.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_report_checkout_import.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_report_http.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_report_isolation.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_report_service.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_reverse_audit.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_review_case_id_cluster_m2.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_review_case_id_cluster_r2.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_review_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_review_evidence_account.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_review_form_spec_and_case_id.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_review_provenance.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_review_records.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_revoke.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_roundtrip.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_run_missing_env.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_run_record_failclosed.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_runs_collected.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_runs_read_rows.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_schedule_multiple.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_schedule_view.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_scopes_source_requested.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_search_engage_targets.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_search_media_routes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_secret_value_redact.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_secrets_grep.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_security_small_fixes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_select.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_selfupdate.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_send.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_send_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_send_control_chars.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_send_media.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_send_visibility.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_sent_integrity.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_server_json.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_skill_doc.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_study_report.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_systemd.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_thread_publish.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_thread_read.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_threadrun.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_threads_read_permissions.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_threadshape.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_thth_root.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_token_exists.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_token_scopes_record.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_token_set.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_topic.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_topic_advice.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_topic_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_topic_models.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_topic_review_fixes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_topic_shelf_observers.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_topic_store.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_topics_record_concurrent.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_topics_tool.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_trial_frictions.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_admin_transaction.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_approve_account.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_length.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_lock_wait.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_mastodon_scopes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_mentions.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_post_urls.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_repo_paths.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_reposts.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_study_add.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_tool_version.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_unanswered.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_unanswered_scope.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_where_arguments.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_where_filters.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v210_who_arguments.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v211_auth_integration.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v211_authflow.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v211_bluesky_stdin.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v211_doctor_binding.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v211_mastodon_auth.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v211_x_auth.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_ask_coordination.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_budget.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_budget_auth.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_budget_mcp.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_deletion.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_job_completion_fault.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_leave.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_leave_gate.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_leave_providers.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_leave_token_safety.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_mastodon_guide.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_mcp_compat.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_missing_token.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_read_coordination.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_server_writes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_token_parent.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v212_write_storage.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v2131_send_dry_run.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v2131_threads_publish_wait.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v2132_send_inflight_hint.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v2132_threads_create_timeout.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v2132_threads_create_unknown.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_acceptance_fixes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_adts_asf.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_approval_attachments.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_attachment_kinds.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_bluesky_external.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_bluesky_media.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_bluesky_metadata.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_bluesky_metadata_uri.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_bluesky_metadata_video.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_bluesky_record.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_bluesky_record_video.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_bluesky_video.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_bluesky_webm_named.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_bmff_typed_metadata.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_budget_lock.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_capabilities.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_carryover_timeout.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_graphemes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_auth_media.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_bmff_audio.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_flac.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_gif.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_link.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_media.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_mp3.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_ogg_opus.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_poll.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_quote.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_thumbnail.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_wav.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_webm_audio.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_mastodon_webm_video.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_media_cleanup.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_media_formats.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_media_foundation.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_media_prepared.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_media_relay.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_media_relay_refusals.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_media_upload.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_receipt_deadline.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_review_followups.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_stage810_audit.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_static_diagnostics.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_stop_observation.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_systemd_worker.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_threads_ghost.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_threads_gif.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_threads_link.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_threads_media.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_threads_media_spoiler.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_threads_poll.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_threads_quote.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_threads_quote_media.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_threads_replies.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_threads_text.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_threads_text_spoiler.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_threads_video.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_transport_review.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_video_typed_metadata.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_vorbis_comment_boundary.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v213_webm_schema_boundary.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v2141_draft_lint_reason.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v2141_managed_repo_umask.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v214_release_signing.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v214_x_adapter.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v214_x_auth.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v214_x_media.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v214_x_posts_budget.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v21b_approved_writes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v28_tags.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v310_morning.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v310_search_timeout.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v310_watch_words.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v311_leave_same_user.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v311_mastodon_retract.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v311_morning_overdue.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v311_morning_own_exclusion.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v311_morning_unanswered_window.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v311_notifications_guidance.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v311_replies_owned.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v312_leave_shared_token.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v312_own_handles_stopped.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v312_report_admin.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v312_report_channel.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v312_report_inbox.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v312_report_mcp_user.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v31_analysis.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v31_collection.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v320_reply_to_file_display.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v320_reply_to_file_lint.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v320_reply_to_file_resolve.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v320_report_fixes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v330_fingerprint_version.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v330_held.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v330_held_names.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v330_last_refusals.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v330_notification_route.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v330_observe.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v330_report_friction.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v330_report_words.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_v330_reports_effect.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_value_domains_shown.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_version.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_where_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_who_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_writeback_rebase.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_writeback_validate.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/tests/test_x_metrics.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/__init__.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/__main__.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/_grapheme_data.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/account_report.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/accounts.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/__init__.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/auth_mastodon.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/auth_threads.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/auth_x.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/bluesky.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/bluesky_media.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/bluesky_video.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/mastodon.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/mastodon_media.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/threads_media.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/x.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/adapters/x_media.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/admin_notifications.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/after_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/analytics_comparison.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/analytics_report.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/analytics_shapes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/analytics_threads.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/api_diagnostic.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/appconfig.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/appenv.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/approval.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/approval_jobs.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/approval_relay.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/ask.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/ask_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/audioformats.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/authclients.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/authflow.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/authpassword.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/bluesky_metadata.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/budget_x.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/budget_x_posts.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/bundle.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/collect.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/collection_context.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/collection_status.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/deletion.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/doctor.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/ebmlaudio.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/ebmlvideo.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/engagements.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/fingerprint_golden.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/flacformats.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/forms.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/graphemes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/handoff_cursor.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/httpsafe.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/jst.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/lint.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/lock.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/maintain.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/managed_repo.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/measured.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/media.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/media_capabilities.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/media_cleanup.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/media_delivery.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/media_relay.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/media_uploads.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/mediaformats.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/mpeg_audio.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/notification_route.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/oauth.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/oggformats.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/operations_handoff.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/postid.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/queuefile.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/read_coordination.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/read_window.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/redact.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/refusals.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/replies.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/report.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/report_details.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/report_http.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/report_isolation.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/retract_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/scopes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/secrets_fs.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/select.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/selfupdate.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/sent.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/server_files.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/server_writes.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/stop_observation.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/study_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/study_report.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/systemd_gen.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/tags.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/thread_read.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/threadrun.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/threads_read_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/threadshape.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/threadthrow.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/tool_version.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/topic_advice.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/topic_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/topic_models.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/topic_store.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/topics.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/unanswered.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/vorbisformats.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/watch_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/where_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/who_cli.py +0 -0
- {thth-3.3.0 → thth-3.4.0}/thth/writeback.py +0 -0
- {thth-3.3.0 → thth-3.4.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.4.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`・`observe`・`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`。
|
|
128
|
+
- **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`observe`・`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`・`inflight`・`location`・`notifications`・`report`・`plaza`・`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`・`observe`・`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`。
|
|
104
|
+
- **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`observe`・`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`・`inflight`・`location`・`notifications`・`report`・`plaza`・`admin`。
|
|
105
105
|
- **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
|
|
106
106
|
- **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
|
|
107
107
|
- **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
|
|
@@ -568,6 +568,25 @@ ADMIN_TOOLS += [
|
|
|
568
568
|
'required':['report_id','reason','version','by'],'additionalProperties':False}},
|
|
569
569
|
]
|
|
570
570
|
|
|
571
|
+
# 施策の広場の管理者側(設計 3.4.0 §4)。参加・退出・非表示は `by` 必須・変更ログに
|
|
572
|
+
# presence-only で残る。
|
|
573
|
+
ADMIN_TOOLS += [
|
|
574
|
+
{'name':'thth_admin_plaza_list','description':'List every plaza post (measures, findings, questions) including hidden ones, and the joined projects; administrator only; read-only',
|
|
575
|
+
'inputSchema':{'type':'object','properties':{},'required':[],'additionalProperties':False}},
|
|
576
|
+
{'name':'thth_admin_plaza_show','description':'Read one plaza post as stored (original text, observations, replies); administrator only; read-only',
|
|
577
|
+
'inputSchema':{'type':'object','properties':{'plaza_id':{'type':'string'}},
|
|
578
|
+
'required':['plaza_id'],'additionalProperties':False}},
|
|
579
|
+
{'name':'thth_admin_plaza_join','description':'Let one project (owner) read and post open plaza entries; administrator only, by required; default is not joined',
|
|
580
|
+
'inputSchema':{'type':'object','properties':{'project':{'type':'string'},'by':{'type':'string'}},
|
|
581
|
+
'required':['project','by'],'additionalProperties':False}},
|
|
582
|
+
{'name':'thth_admin_plaza_leave','description':'Remove one project (owner) from the open plaza; its open entries stop being visible to others; administrator only, by required',
|
|
583
|
+
'inputSchema':{'type':'object','properties':{'project':{'type':'string'},'by':{'type':'string'}},
|
|
584
|
+
'required':['project','by'],'additionalProperties':False}},
|
|
585
|
+
{'name':'thth_admin_plaza_hide','description':'Hide one plaza post with a reason (the owner still sees it with the reason); administrator only, by required',
|
|
586
|
+
'inputSchema':{'type':'object','properties':{'plaza_id':{'type':'string'},'reason':{'type':'string'},'by':{'type':'string'}},
|
|
587
|
+
'required':['plaza_id','reason','by'],'additionalProperties':False}},
|
|
588
|
+
]
|
|
589
|
+
|
|
571
590
|
SERVER_TOOLS = [
|
|
572
591
|
{"name":"thth_"+name,"description":"Scoped server "+name,
|
|
573
592
|
"inputSchema":{"type":"object","properties":{key:{"type":"string"} for key in ("account",*keys)},
|
|
@@ -644,6 +663,68 @@ REPORT_TOOLS = [
|
|
|
644
663
|
]
|
|
645
664
|
|
|
646
665
|
|
|
666
|
+
# 施策の広場の利用者側(設計 3.4.0 §4)。**サーバ型だけに出す**(報告の口と同じ——
|
|
667
|
+
# 置き場は VM の私有)。誰が書いたかは credential の `actor`。読めるのは credential が
|
|
668
|
+
# 許した account の持ち主(project)の書き込みと、参加していれば open の写しだけ。
|
|
669
|
+
PLAZA_WELCOME = ("施策を試したら広場に置き、次を決める前に他の媒体の施策を読む——"
|
|
670
|
+
"どちらも利用者の作業の一部です")
|
|
671
|
+
PLAZA_TOOLS = [
|
|
672
|
+
{"name": "thth_plaza_post",
|
|
673
|
+
"description": PLAZA_WELCOME + "。施策(measure: 観測は道具が study-report と同じ計算で付ける・"
|
|
674
|
+
"how〔数字を出し直せる thth の命令〕が必須)・気づき(finding)・問い(question)を"
|
|
675
|
+
"置く。scope(媒体・企画の範囲)は必須。置いたものは同じ持ち主の全 account だけに"
|
|
676
|
+
"見える(他の持ち主に見せる open は人が CLI の二段確認で行う・この口には無い)。"
|
|
677
|
+
"evidence_level の observed は道具だけが付ける",
|
|
678
|
+
"inputSchema": {"type": "object", "properties": {
|
|
679
|
+
"account": {"type": "string"},
|
|
680
|
+
"kind": {"type": "string", "enum": ["measure", "finding", "question"]},
|
|
681
|
+
"title": {"type": "string", "description": "1 行・120 字まで"},
|
|
682
|
+
"body": {"type": "string", "description": "8,000 字まで"},
|
|
683
|
+
"scope": {"type": "string", "description": "媒体・企画の範囲(1 行・120 字・必須)"},
|
|
684
|
+
"kind_detail": {"type": "string", "enum": ["pattern", "rule", "pitfall", "tool_tip"],
|
|
685
|
+
"description": "finding の細目(任意)"},
|
|
686
|
+
"how": {"type": "string", "description": "数字を出し直せる thth の命令 1 行(measure は必須・実行しない)"},
|
|
687
|
+
"evidence_level": {"type": "string", "enum": ["stated", "hypothesis"]},
|
|
688
|
+
"declarations": {"type": "array", "description": "measure の宣言(study-report の形・媒体ごとに 1 つ)"},
|
|
689
|
+
"hypothesis": {"type": "string"}, "change": {"type": "string"},
|
|
690
|
+
"until": {"type": "string", "description": "期間の終わり(timezone 付きの時刻)"},
|
|
691
|
+
"min_n": {"type": "integer"}},
|
|
692
|
+
"required": ["account", "kind", "title", "body", "scope"], "additionalProperties": False}},
|
|
693
|
+
{"name": "thth_plaza_list",
|
|
694
|
+
"description": "広場の一覧(既定は自分の持ち主の書き込み・open: true は open の広場)。次を決める前に"
|
|
695
|
+
"他の媒体の施策を読む(読むだけ)",
|
|
696
|
+
"inputSchema": {"type": "object", "properties": {
|
|
697
|
+
"project": {"type": "string"}, "open": {"type": "boolean"}},
|
|
698
|
+
"additionalProperties": False}},
|
|
699
|
+
{"name": "thth_plaza_show",
|
|
700
|
+
"description": "広場の 1 件(本文・道具が付けた観測・媒体をまたぐ比較の表・追試の数・返信)を読む(読むだけ)",
|
|
701
|
+
"inputSchema": {"type": "object", "properties": {"plaza_id": {"type": "string"}},
|
|
702
|
+
"required": ["plaza_id"], "additionalProperties": False}},
|
|
703
|
+
{"name": "thth_plaza_reply",
|
|
704
|
+
"description": "返信を 1 つ足す(comment・agree・disagree〔理由必須〕・tried〔自分の measure の id 必須〕・"
|
|
705
|
+
"trial〔追試: result に reproduced・not_reproduced・not_tried。再現しなかった報告も"
|
|
706
|
+
"同じ重さで数える〕)",
|
|
707
|
+
"inputSchema": {"type": "object", "properties": {
|
|
708
|
+
"plaza_id": {"type": "string"}, "account": {"type": "string"},
|
|
709
|
+
"kind": {"type": "string", "enum": ["comment", "tried", "agree", "disagree", "trial"]},
|
|
710
|
+
"text": {"type": "string", "description": "4,000 字まで"},
|
|
711
|
+
"measure_id": {"type": "string"},
|
|
712
|
+
"result": {"type": "string", "enum": ["reproduced", "not_reproduced", "not_tried"]}},
|
|
713
|
+
"required": ["plaza_id", "account", "kind"], "additionalProperties": False}},
|
|
714
|
+
{"name": "thth_plaza_update",
|
|
715
|
+
"description": "自分の持ち主の施策の結果を取り直す(refresh)・判定(verdict: adopted・dropped・"
|
|
716
|
+
"inconclusive と reason)・project の範囲に戻す(visibility: project。open への切り替えは"
|
|
717
|
+
"人の CLI の二段確認だけ)",
|
|
718
|
+
"inputSchema": {"type": "object", "properties": {
|
|
719
|
+
"plaza_id": {"type": "string"}, "account": {"type": "string"},
|
|
720
|
+
"refresh": {"type": "boolean"},
|
|
721
|
+
"verdict": {"type": "string", "enum": ["adopted", "dropped", "inconclusive"]},
|
|
722
|
+
"reason": {"type": "string"},
|
|
723
|
+
"visibility": {"type": "string", "enum": ["project"]}},
|
|
724
|
+
"required": ["plaza_id", "account"], "additionalProperties": False}},
|
|
725
|
+
]
|
|
726
|
+
|
|
727
|
+
|
|
647
728
|
# **うまくいかなければ報告の口へ**(設計 3.1.2 §3.5)。利用者の道具の説明文の
|
|
648
729
|
# 末尾に 1 句。定数(TOOLS 等)は書き換えず、並べるときに足す——「売り文句」の
|
|
649
730
|
# 正本(設計 v2 §1・「自分の泉」§2)はそのまま残る。
|
|
@@ -681,7 +762,8 @@ def server_tools(context):
|
|
|
681
762
|
reports = reports + [tool for tool in TOOLS if tool['name'] in ('thth_observe', 'thth_morning')]
|
|
682
763
|
from thth.server_writes import WRITE_OPERATIONS
|
|
683
764
|
return _with_report_hint(reports + [tool for tool in SERVER_TOOLS
|
|
684
|
-
if context.writes or tool['name'][5:] not in WRITE_OPERATIONS]
|
|
765
|
+
if context.writes or tool['name'][5:] not in WRITE_OPERATIONS]
|
|
766
|
+
+ PLAZA_TOOLS) + REPORT_TOOLS
|
|
685
767
|
|
|
686
768
|
|
|
687
769
|
def server_call(name, arguments):
|
|
@@ -721,6 +803,12 @@ def server_call(name, arguments):
|
|
|
721
803
|
elif operation.startswith('admin_reports_'):
|
|
722
804
|
from thth.report_service import execute_admin_reports
|
|
723
805
|
result=execute_admin_reports(context,request)
|
|
806
|
+
elif operation.startswith('admin_plaza_'):
|
|
807
|
+
from thth.report_service import execute_admin_plaza
|
|
808
|
+
result=execute_admin_plaza(context,request)
|
|
809
|
+
elif operation.startswith('plaza_'):
|
|
810
|
+
from thth.report_service import execute_user_plaza
|
|
811
|
+
result=execute_user_plaza(context,request)
|
|
724
812
|
elif operation in ('report_file','report_list','report_show','report_add'):
|
|
725
813
|
from thth.report_service import execute_user_reports
|
|
726
814
|
result=execute_user_reports(context,request)
|
|
@@ -742,6 +830,12 @@ def server_call(name, arguments):
|
|
|
742
830
|
if str(exc)=='duplicate_report' and getattr(exc,'report_id',None):
|
|
743
831
|
# 既存の id は同じ account の報告だけ(重複の検査がそう絞っている)。
|
|
744
832
|
return failure('duplicate_report: '+exc.report_id)
|
|
833
|
+
from thth.plaza import REASONS as PLAZA_REASONS
|
|
834
|
+
if str(exc)=='duplicate_post' and getattr(exc,'plaza_id',None):
|
|
835
|
+
# 既存の id は同じ account の書き込みだけ(重複の検査がそう絞っている)。
|
|
836
|
+
return failure('duplicate_post: '+exc.plaza_id)
|
|
837
|
+
if str(exc) in PLAZA_REASONS:
|
|
838
|
+
return failure(str(exc))
|
|
745
839
|
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')
|
|
746
840
|
except Exception:
|
|
747
841
|
return failure('request_unavailable')
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: thth
|
|
3
|
-
description: 投稿する前・返信を書く前・出したあと・絡みに行く先を選ぶときに呼ぶ道具。Threads・Bluesky・Mastodon
|
|
3
|
+
description: 投稿する前・返信を書く前・出したあと・絡みに行く先を選ぶときに呼ぶ道具。Threads・Bluesky・Mastodon への下書きを人の承認を通してから出し、枝を読み、相手を知り、伸びを測る。施策を試したら広場に置き、他の媒体の施策を読んでから次を決める。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
**セッションの始めと区切りごとに `thth_observe`(CLI は `thth observe <project|account> --json`)。** 前回の観測から何があって、いまなにをすればよいかが 6 段(道具・返していないもの・前回の観測から・世間・予定・次の一手)で返る(3.3.0。`thth_morning`/`thth morning` は同じものの別名で、JSON の `invoked_as` だけが違う。第 2 段は前回の栞の時刻から今まで・栞が無い/7 日より古いときは前日 JST で、`window.basis` が `since_last_observe` か `yesterday_jst`)。`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
8
|
**断られたら・違和感があれば報告の口へ**: `thth_report_file`(CLI は `thth report file <account> --kind bug|request|friction --title … --body-file … --by <名前>`)。つまずき・迷い・期待との違いの報告は、利用者の作業の一部として歓迎します。小さいものも。重複は道具が束ねます。例: 「止まったのに気づかなかった」「断られた理由が分からなかった」「同じ操作を 3 回繰り返した」。道具が断った・結果が期待と違った・欲しい形がある・迷った(`friction`)、のどれかなら 1 回で置ける。**断られた直後なら、断りの最後の `report_channel:` の行をそのまま打てばよい**(`--from-last-refusal` が、道具が控えた直前の断り——時刻・版・命令・理由の符丁だけで、本文と引数の値は控えていない——を再現手順に添える。MCP は `thth_report_file {"account": …, "title": …, "from_last_refusal": true}`)。置いた報告がこの版で閉じたかは `thth_observe` の 0 段 `project_reports` に出る。返事は `thth_report_show` と `handoff-report --since-last-read` の `tool.reports` に出る。秘密らしき値が含まれていたら置かれない(伏せてから置き直す)。
|
|
9
9
|
|
|
10
|
+
**施策を試したら広場に置き、次を決める前に他の媒体の施策を読む**(3.4.0・施策の広場)。どちらも利用者の作業の一部です。小さな気づきも。`thth_plaza_post`(CLI は `thth plaza post <account> --kind measure|finding|question --title … --body-file … --scope "<媒体・企画の範囲>" --by <名前>`)。置いたものは**同じ持ち主の全 account(媒体をまたぐ)**だけに見える。施策(`measure`)は `--declaration <study-report の宣言>` を媒体ごとに並べると**観測は道具が付ける**(study-report と同じ計算・両群の分母・欠測は null と理由)。`--how` に数字を出し直せる thth の命令を 1 行(measure は必須・道具は実行しない)。本文に書いた数字は「本文」として分けて表示され、観測とは呼ばれない。`evidence_level` の `observed` は道具だけが付ける(名乗ると断られる)。読むのは `thth_plaza_list`・`thth_plaza_show`(CLI は `thth plaza list <project>`・`thth plaza show <id> --as <account>`)——媒体をまたぐ比較の表と追試の数(再現した・しなかった・試していないを同じ重さで)が出る。他の媒体の施策を自分の媒体で試したら `thth_plaza_reply` の `trial`(結果は reproduced・not_reproduced・not_tried)で返す。判定は `thth_plaza_update`(adopted・dropped・inconclusive・理由必須)。新着と「まだ試していない媒体」は `thth_observe` の 0 段と次の一手に出る。
|
|
11
|
+
|
|
10
12
|
段だけを読み直したいときは `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` を「変更なし」と解釈しない。
|
|
11
13
|
|
|
12
14
|
添付を付ける前に `tool.capabilities` を読む(媒体ごとの「出せるもの」表。`thth doctor` にも同じ表が出る。`unverified` は「非対応」ではない)。
|
|
@@ -152,6 +154,7 @@ MCP `after_you_posted`。**24h の刻みが無ければ `null`**(0 と混ぜ
|
|
|
152
154
|
- 道具の不具合・欲しい形・迷ったことは `thth report file`(MCP `thth_report_file`)で実装側に届ける(小さいものも歓迎・重複は道具が束ねる)。
|
|
153
155
|
- 承認済みなのに出ない原稿は `thth_observe` の予定の段 `held_items`(file・理由・予定・経過)と `thth board` の要確認(先頭 5 本)に名前が出る。`approval_stale` は再承認(`thth approve` の二段)で直る。時刻を過ぎて出られないものがあると `thth run` は `held` として死活通知と運用通知に知らせる。
|
|
154
156
|
- 自分の開いている報告に書き足すなら `thth report add <report_id> --body-file <path|-> --by <名前>`(MCP `thth_report_add`)。
|
|
157
|
+
- 他の媒体で試した施策と結果は `thth plaza list <project>`(MCP `thth_plaza_list`)で読める。試したら `thth plaza post`(MCP `thth_plaza_post`)で置く。
|
|
155
158
|
- 使い方の全文: `docs/使い方_プロジェクトのセッション向け_2026-09-09.md`。
|
|
156
159
|
|
|
157
160
|
## 運用と分析のレポート(v3系・2.5.0 から)
|
|
@@ -168,7 +171,7 @@ MCP `after_you_posted`。**24h の刻みが無ければ `null`**(0 と混ぜ
|
|
|
168
171
|
|
|
169
172
|
新規 account は利用者メール・管理者メール・TLS SMTP を `thth notifications config` で設定し、`status` → `test` を実行する。両宛先の受信箱で到達、account 専用 HEALTHCHECK_URL の missed-ping 通知を確認してから production/scheduled を有効にする。既存 account にも同じ設定を追加する。詳細は `docs/停止通知と運用記録.md`。
|
|
170
173
|
|
|
171
|
-
プロジェクトの作業開始・再開時は `git fetch` 後にリモート `docs/sns/queue/` の `thth_run_state`・`thth_run_detail`・`thth_run_next` を確認する。dirty worktree へ無条件に pull しない。リモートの確認は `git show origin/main:docs/sns/queue/FILE.md` 等で行える。停止・成否不明なら inflight
|
|
174
|
+
プロジェクトの作業開始・再開時は `git fetch` 後にリモート `docs/sns/queue/` の `thth_run_state`・`thth_run_detail`・`thth_run_next` を確認する。dirty worktree へ無条件に pull しない。リモートの確認は `git show origin/main:docs/sns/queue/FILE.md` 等で行える。停止・成否不明なら inflight を手で消して再投稿しない(3.3.1 から道具が毎 run の最初に媒体へ 1 回だけ問い合わせ、決まれば自分で解く)。止まり続けるなら `thth inflight <account> show` で中身を見て、媒体で確かめた人が `thth inflight <account> resolve --not-published|--published <post_id> --by <名前>` で解く(二段確認・MCP には無い)。復旧状態は投稿成功の保証ではなく、公開結果は status/post_id/posted_at を見る。
|
|
172
175
|
|
|
173
176
|
`mentions <account> --json` は 3 媒体の言及を同じ項目で読む。返信前に `replied` の object/false/null を区別する。`unanswered <account> --since 7d --json` は自分の根投稿の未回答候補を台帳で読む。`cannot_say` と採集の古さを確認し、`--refresh` は明示されたときだけ使う。件数は account ごとに扱い、送信は既存の承認・digest・production の門を通す。
|
|
174
177
|
|
|
@@ -67,6 +67,16 @@ def local_http_test_transport(monkeypatch):
|
|
|
67
67
|
opener.open('http://'+host,timeout=0.01)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
@pytest.fixture(autouse=True)
|
|
71
|
+
def no_threads_status_poll_wait(monkeypatch):
|
|
72
|
+
"""公開の結果が分からないときの container の問い合わせ(設計 3.3.1 §2)を
|
|
73
|
+
試験では待たない。本番の既定は 20 秒おき・最大 3 回。偽サーバは status の
|
|
74
|
+
GET に答えない(`query_failed`)ので、従前の試験の結果(inflight を残す)は
|
|
75
|
+
変わらない——変わるのは待つ時間だけ。subprocess の `bin/thth` にも env で届く。
|
|
76
|
+
"""
|
|
77
|
+
monkeypatch.setenv('THTH_THREADS_STATUS_POLL_SECONDS', '0')
|
|
78
|
+
|
|
79
|
+
|
|
70
80
|
@pytest.fixture(autouse=True)
|
|
71
81
|
def frozen_now_jst(monkeypatch):
|
|
72
82
|
"""`thth.jst.now_jst()` を既定で静かな時間帯の外(2026-09-09 10:00 JST)に固定する。
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""container の status を答える偽 Threads(設計 3.3.1 §6・**tests の中だけ**)。
|
|
2
|
+
|
|
3
|
+
本物の Threads API には触らない。`http.server` で次の 4 つの口だけを真似る:
|
|
4
|
+
|
|
5
|
+
- `POST /<user>/threads` → container を作る(id は `CONTAINER_ID`)
|
|
6
|
+
- `POST /<user>/threads_publish` → `script.publish` の先頭から 1 つずつ答える
|
|
7
|
+
- `GET /<container>` → `script.status` の先頭から 1 つずつ(最後の 1 つは繰り返す)
|
|
8
|
+
- `GET /v1.0/<user>/threads` → `script.posts`(自分の最近の投稿)
|
|
9
|
+
|
|
10
|
+
何回呼ばれたかを数える(`publish_calls`・`status_calls`・`list_calls`)——
|
|
11
|
+
「2 度出していない」を回数で確かめるため。
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
import dataclasses
|
|
17
|
+
import http.server
|
|
18
|
+
import json
|
|
19
|
+
import threading
|
|
20
|
+
import urllib.parse
|
|
21
|
+
|
|
22
|
+
from thth.adapters import threads as threads_mod
|
|
23
|
+
|
|
24
|
+
USER_ID = "12345"
|
|
25
|
+
CONTAINER_ID = "900"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclasses.dataclass
|
|
29
|
+
class Script:
|
|
30
|
+
# 公開の答え: ("ok", id) / (500, None) / (400, None) / ("drop", None)(接続断)
|
|
31
|
+
publish: list = dataclasses.field(default_factory=lambda: [("ok", "777")])
|
|
32
|
+
# container の status の答え: "FINISHED" などの語 / 数字(HTTP の失敗)
|
|
33
|
+
status: list = dataclasses.field(default_factory=lambda: ["FINISHED"])
|
|
34
|
+
# 自分の最近の投稿(`GET /v1.0/<user>/threads` の data)
|
|
35
|
+
posts: list = dataclasses.field(default_factory=list)
|
|
36
|
+
list_status: int = 200
|
|
37
|
+
publish_calls: int = 0
|
|
38
|
+
status_calls: int = 0
|
|
39
|
+
list_calls: int = 0
|
|
40
|
+
create_calls: int = 0
|
|
41
|
+
# 公開の要求に載っていた creation_id(どの container を公開したか)。
|
|
42
|
+
publish_ids: list = dataclasses.field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _handler(script: Script):
|
|
46
|
+
class Handler(http.server.BaseHTTPRequestHandler):
|
|
47
|
+
def _json(self, code, payload):
|
|
48
|
+
body = json.dumps(payload).encode("utf-8")
|
|
49
|
+
self.send_response(code)
|
|
50
|
+
self.send_header("Content-Type", "application/json")
|
|
51
|
+
self.send_header("Content-Length", str(len(body)))
|
|
52
|
+
self.end_headers()
|
|
53
|
+
self.wfile.write(body)
|
|
54
|
+
|
|
55
|
+
def do_POST(self): # noqa: N802
|
|
56
|
+
length = int(self.headers.get("Content-Length") or 0)
|
|
57
|
+
raw = self.rfile.read(length) if length else b""
|
|
58
|
+
if self.path.endswith("/threads_publish"):
|
|
59
|
+
script.publish_calls += 1
|
|
60
|
+
params = urllib.parse.parse_qs(raw.decode("utf-8"))
|
|
61
|
+
script.publish_ids.append((params.get("creation_id") or [None])[0])
|
|
62
|
+
kind, value = (script.publish.pop(0) if script.publish else (500, None))
|
|
63
|
+
if kind == "ok":
|
|
64
|
+
self._json(200, {"id": value})
|
|
65
|
+
elif kind == "drop":
|
|
66
|
+
# 応答を返さずに切る(接続断)。
|
|
67
|
+
self.close_connection = True
|
|
68
|
+
self.connection.close()
|
|
69
|
+
else:
|
|
70
|
+
self._json(kind, {"error": {"message": "fake", "code": 1,
|
|
71
|
+
"is_transient": True}})
|
|
72
|
+
return
|
|
73
|
+
script.create_calls += 1
|
|
74
|
+
self._json(200, {"id": CONTAINER_ID})
|
|
75
|
+
|
|
76
|
+
def do_GET(self): # noqa: N802
|
|
77
|
+
path = urllib.parse.urlsplit(self.path).path
|
|
78
|
+
if path == f"/v1.0/{USER_ID}/threads":
|
|
79
|
+
script.list_calls += 1
|
|
80
|
+
if script.list_status != 200:
|
|
81
|
+
self._json(script.list_status, {"error": {"message": "fake"}})
|
|
82
|
+
return
|
|
83
|
+
self._json(200, {"data": list(script.posts)})
|
|
84
|
+
return
|
|
85
|
+
if path == f"/{CONTAINER_ID}":
|
|
86
|
+
script.status_calls += 1
|
|
87
|
+
answer = script.status.pop(0) if len(script.status) > 1 else script.status[0]
|
|
88
|
+
if isinstance(answer, int):
|
|
89
|
+
self._json(answer, {"error": {"message": "fake"}})
|
|
90
|
+
else:
|
|
91
|
+
self._json(200, {"status": answer, "id": CONTAINER_ID})
|
|
92
|
+
return
|
|
93
|
+
self._json(404, {"error": {"message": "not found"}})
|
|
94
|
+
|
|
95
|
+
def log_message(self, format, *args): # noqa: A002
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
return Handler
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@contextlib.contextmanager
|
|
102
|
+
def serve(script: Script):
|
|
103
|
+
server = http.server.HTTPServer(("127.0.0.1", 0), _handler(script))
|
|
104
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
105
|
+
thread.start()
|
|
106
|
+
try:
|
|
107
|
+
yield f"http://127.0.0.1:{server.server_port}"
|
|
108
|
+
finally:
|
|
109
|
+
server.shutdown()
|
|
110
|
+
thread.join(timeout=5)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def adapter(base_url: str, **kwargs) -> threads_mod.ThreadsAdapter:
|
|
114
|
+
kwargs.setdefault("wait_seconds", 0)
|
|
115
|
+
kwargs.setdefault("timeout", 2.0)
|
|
116
|
+
kwargs.setdefault("status_poll_seconds", 0)
|
|
117
|
+
return threads_mod.ThreadsAdapter(base_url=base_url, access_token="fake-token",
|
|
118
|
+
user_id=USER_ID, **kwargs)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def post_row(post_id: str, text: str, timestamp: str) -> dict:
|
|
122
|
+
return {"id": post_id, "text": text, "timestamp": timestamp,
|
|
123
|
+
"permalink": f"https://www.threads.net/@x/post/{post_id}"}
|
|
@@ -41,7 +41,10 @@ def test_admin_all_registry_and_mcp_auth_rechecked(credentials,monkeypatch):
|
|
|
41
41
|
'thth_admin_inventory','thth_admin_account','thth_admin_diff','thth_admin_log',
|
|
42
42
|
'thth_admin_tokens','thth_admin_release','thth_admin_budget_set','thth_admin_watch_set',
|
|
43
43
|
# 報告の口の実装側(設計 3.1.2 §1)。
|
|
44
|
-
'thth_admin_reports_list','thth_admin_reports_show','thth_admin_reports_reply','thth_admin_reports_close'
|
|
44
|
+
'thth_admin_reports_list','thth_admin_reports_show','thth_admin_reports_reply','thth_admin_reports_close',
|
|
45
|
+
# 施策の広場の管理者側(設計 3.4.0 §4)。
|
|
46
|
+
'thth_admin_plaza_list','thth_admin_plaza_show','thth_admin_plaza_join','thth_admin_plaza_leave',
|
|
47
|
+
'thth_admin_plaza_hide'}
|
|
45
48
|
result=server.call_tool('thth_admin_inventory',{})
|
|
46
49
|
assert set(json.loads(result['content'][0]['text'])['by_account'])=={'first','second'}
|
|
47
50
|
assert server.call_tool('thth_admin_diff',{'mark_read':True})['isError']
|
|
@@ -76,7 +76,8 @@ def test_git_roundtrip_preserves_original_and_deduplicates(setup):
|
|
|
76
76
|
assert "thth_run_state: recovered" in s.path.read_text()
|
|
77
77
|
assert len(s.captured) == 4
|
|
78
78
|
assert incident._fingerprint(s.path) == before
|
|
79
|
-
|
|
79
|
+
# 3.3.1 §5: 送り終えた事象も 30 日は残す(以前は 1 件に畳んでいた)。
|
|
80
|
+
assert len(row(s)["events"]) == 2
|
|
80
81
|
|
|
81
82
|
|
|
82
83
|
def test_initial_success_is_quiet(setup):
|
|
@@ -99,7 +100,8 @@ def test_role_failure_retries_only_unfinished_and_other_role_gets_recovery(setup
|
|
|
99
100
|
monkeypatch.setattr(incident, "_send", lambda c, r, e, a: calls.append((r, e["state"])) or True)
|
|
100
101
|
notify(s, "success", result=False)
|
|
101
102
|
assert calls[-2:] == [("user@example.org", "blocked"), ("user@example.org", "recovered")]
|
|
102
|
-
|
|
103
|
+
# 3.3.1 §5: 送り終えた事象も 30 日は残す(以前は 1 件に畳んでいた)。
|
|
104
|
+
assert len(row(s)["events"]) == 2
|
|
103
105
|
|
|
104
106
|
|
|
105
107
|
def test_same_address_single_message(setup):
|
|
@@ -251,8 +251,23 @@ def test_プライバシーポリシーは正本から生成され_外へ出る
|
|
|
251
251
|
"https://github.com/aokings/thth/issues"):
|
|
252
252
|
assert must in page, must
|
|
253
253
|
for obsolete in ("does not store, log, or transmit the code", "your own computer or server",
|
|
254
|
-
"どこにも保存・記録・送信せず", "2026-09-15 施行"
|
|
254
|
+
"どこにも保存・記録・送信せず", "2026-09-15 施行",
|
|
255
|
+
# 2026-09-23: 退出(thth account leave)と削除依頼の照合は実装済み。
|
|
256
|
+
# 「後の版で実装」の文は実態と合わない(Platform Terms §7(a))。
|
|
257
|
+
"belong to a later implementation", "別の版で実装・検証する予定",
|
|
258
|
+
"600,000"):
|
|
255
259
|
assert obsolete not in page
|
|
260
|
+
# 2026-09-23 の書き直し(照合 論点 D)で書いた事実。どれもコードで裏を取った:
|
|
261
|
+
# 返信・言及は相手の username と本文ごと保存(collect.py)・検索結果は保存しない
|
|
262
|
+
# (where_cli.py・threads_read_cli.py)・退出(leave.py)・削除依頼(deletion.py)・
|
|
263
|
+
# 添付(media-object.js)・LLM への受け渡し(mcp/server.py)。英語と日本語の両方。
|
|
264
|
+
for must in ("the other person's username, the text", "相手のユーザー名・本文",
|
|
265
|
+
"The text and authors of search results are not saved", "検索結果の本文と投稿者は保存しません",
|
|
266
|
+
"thth account leave", "30 days", "24 hours after it was created",
|
|
267
|
+
"THTH itself does not send data to an AI model provider",
|
|
268
|
+
"THTH 自身は AI モデルの提供者へデータを送りません",
|
|
269
|
+
"unverified, verified or completed", "未照合・照合済み・完了"):
|
|
270
|
+
assert must in page, must
|
|
256
271
|
# 紹介ページから辿れる・robots は開いている
|
|
257
272
|
index = (PUBLIC / "index.html").read_text(encoding="utf-8")
|
|
258
273
|
assert 'href="/privacy/"' in index
|
|
@@ -30,7 +30,13 @@ COMMANDS = '''lint|preview|approve|account|revoke|posts|replies|measured|threads
|
|
|
30
30
|
'observe',
|
|
31
31
|
# 3.1.2: 報告の口(設計 3.1.2 §1)。
|
|
32
32
|
'report file', 'report list', 'report show', 'report add', 'admin reports list', 'admin reports show',
|
|
33
|
-
'admin reports reply', 'admin reports close', 'admin reports export'
|
|
33
|
+
'admin reports reply', 'admin reports close', 'admin reports export',
|
|
34
|
+
# 3.3.1 §4: 人の口 thth inflight(show|resolve・--json)。
|
|
35
|
+
'inflight',
|
|
36
|
+
# 3.4.0 §4: 施策の広場(利用者と管理者の口・--json)。
|
|
37
|
+
'plaza post', 'plaza list', 'plaza show', 'plaza reply', 'plaza update',
|
|
38
|
+
'admin plaza join', 'admin plaza leave', 'admin plaza hide', 'admin plaza list',
|
|
39
|
+
'admin plaza show']
|
|
34
40
|
|
|
35
41
|
|
|
36
42
|
def test_json_table_covers_every_parser():
|
|
@@ -185,8 +185,11 @@ def test_provision_network_does_not_hold_global_admin_flock(isolated,monkeypatch
|
|
|
185
185
|
def test_privacy_logical_ttl_and_no_physical_erase_claim():
|
|
186
186
|
from tests.test_site import build_site
|
|
187
187
|
page=build_site.build_privacy()
|
|
188
|
-
|
|
188
|
+
# 100,000: the Worker's WebCrypto caps PBKDF2 at 100,000 (callback/src/approval.js ITERATIONS,
|
|
189
|
+
# thth/approval_relay.py ITERATIONS). The page said 600,000 until the 2026-09-23 rewrite.
|
|
190
|
+
for phrase in ('600 seconds','600秒','100,000','PITR','30 days','30日','not a promise of immediate physical erasure','即時物理消去とは約束しません'):
|
|
189
191
|
assert phrase in page
|
|
192
|
+
assert '600,000' not in page
|
|
190
193
|
|
|
191
194
|
|
|
192
195
|
def test_project_key_store_refused_without_traceback(isolated,monkeypatch,capsys):
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""3.3.1 §4: `thth inflight <account> [show|resolve] --by <名前>`(人の口)。
|
|
2
|
+
|
|
3
|
+
09-23 に masaru が `inflight.json` を手で消した操作の代わり。二段確認・控え
|
|
4
|
+
(`inflight.resolved-<日時>.json`)・変更ログ `inflight_resolved`。MCP には出さない。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import datetime
|
|
9
|
+
import importlib.util
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import pathlib
|
|
13
|
+
|
|
14
|
+
from tests.conftest import init_git_pair, make_queue_text, write_queue_file
|
|
15
|
+
from tests.helpers import fake_threads_status as fake
|
|
16
|
+
from thth import accounts as accounts_mod
|
|
17
|
+
from thth import admin_log
|
|
18
|
+
from thth import cli
|
|
19
|
+
from thth import core
|
|
20
|
+
from thth import inflight as inflight_mod
|
|
21
|
+
from thth import inflight_cli
|
|
22
|
+
from thth import queuefile
|
|
23
|
+
from thth import runs as runs_mod
|
|
24
|
+
|
|
25
|
+
NOW = datetime.datetime.fromisoformat("2026-09-09T10:00:00+09:00")
|
|
26
|
+
REPO = pathlib.Path(__file__).resolve().parent.parent
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _stuck(isolated_account_factory, tmp_path, *, git=True):
|
|
30
|
+
if git:
|
|
31
|
+
pair = init_git_pair(tmp_path, seed_content=make_queue_text(), seed_name="a.md")
|
|
32
|
+
account = isolated_account_factory(repo_dir=pair["work"], production=True)
|
|
33
|
+
else:
|
|
34
|
+
account = isolated_account_factory(production=True)
|
|
35
|
+
write_queue_file(account["queue_dir"], "a.md")
|
|
36
|
+
script = fake.Script(publish=[(500, None)], status=[503])
|
|
37
|
+
with fake.serve(script) as base_url:
|
|
38
|
+
result = core.throw_once(account["name"], production_flag=True,
|
|
39
|
+
adapter_factory=lambda c, t: fake.adapter(base_url), now=NOW)
|
|
40
|
+
assert result.action == "inflight"
|
|
41
|
+
return account, accounts_mod.state_dir_for(account["name"])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _cli(capsys, *argv):
|
|
45
|
+
rc = cli.main(["inflight", *argv])
|
|
46
|
+
out = capsys.readouterr()
|
|
47
|
+
return rc, out.out, out.err
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _digest_from(out):
|
|
51
|
+
return next(line.split(": ", 1)[1] for line in out.splitlines() if line.startswith("digest: "))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_showは中身を出すが本文とtokenとcontainer_idは出さない(isolated_account_factory, tmp_path, capsys):
|
|
55
|
+
account, state_dir = _stuck(isolated_account_factory, tmp_path)
|
|
56
|
+
rc, out, _ = _cli(capsys, account["name"], "show")
|
|
57
|
+
assert rc == 0
|
|
58
|
+
assert "a.md" in out and "container_id: あり" in out and "query_failed" in out
|
|
59
|
+
assert "本文です" not in out and "fake-token" not in out and fake.CONTAINER_ID not in out
|
|
60
|
+
rc, out, _ = _cli(capsys, account["name"], "show", "--json")
|
|
61
|
+
info = json.loads(out)["inflight"]
|
|
62
|
+
assert info["container_id_present"] is True and info["remote_state"] == "query_failed"
|
|
63
|
+
assert info["post_id_present"] is False and info["self_resolvable"] is True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_showはinflightが無ければそう言う(isolated_account_factory, capsys):
|
|
67
|
+
account = isolated_account_factory()
|
|
68
|
+
rc, out, _ = _cli(capsys, account["name"])
|
|
69
|
+
assert rc == 0 and "inflight はありません" in out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_resolve_not_publishedの往復_二段確認と控えと変更ログ(isolated_account_factory, tmp_path, capsys):
|
|
73
|
+
account, state_dir = _stuck(isolated_account_factory, tmp_path, git=False)
|
|
74
|
+
before = inflight_mod.read(state_dir)
|
|
75
|
+
# 一段目: 何もしない。
|
|
76
|
+
rc, out, _ = _cli(capsys, account["name"], "resolve", "--not-published", "--by", "masaru")
|
|
77
|
+
assert rc == 1 and "解きません" in out
|
|
78
|
+
assert inflight_mod.read(state_dir) == before
|
|
79
|
+
code = _digest_from(out)
|
|
80
|
+
# 二段目。
|
|
81
|
+
rc, out, _ = _cli(capsys, account["name"], "resolve", "--not-published", "--by", "masaru",
|
|
82
|
+
"--confirm", code)
|
|
83
|
+
assert rc == 0, out
|
|
84
|
+
assert inflight_mod.read(state_dir) is None
|
|
85
|
+
archives = inflight_mod.archives(state_dir)
|
|
86
|
+
assert len(archives) == 1
|
|
87
|
+
saved = json.loads(open(archives[0], encoding="utf-8").read())
|
|
88
|
+
assert saved["resolved_by"] == "masaru" and saved["resolution"] == "human_not_published"
|
|
89
|
+
assert saved["inflight"]["started"] == before["started"]
|
|
90
|
+
assert os.stat(archives[0]).st_mode & 0o777 == 0o600
|
|
91
|
+
rows, broken = admin_log.read(account=account["name"], event="inflight_resolved")
|
|
92
|
+
assert len(rows) == 1 and rows[0]["by"] == "masaru" and not broken
|
|
93
|
+
runs = [r for r in runs_mod.read_runs(state_dir) if r.get("inflight_resolution")]
|
|
94
|
+
assert runs[-1]["inflight_resolution"] == "human_not_published"
|
|
95
|
+
# 原稿は approved のまま(次の run が出し直す)。
|
|
96
|
+
qf = queuefile.parse(os.path.join(account["queue_dir"], "a.md"))
|
|
97
|
+
assert qf.get("status") == "approved"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_resolve_publishedは書き戻して解く(isolated_account_factory, tmp_path, capsys):
|
|
101
|
+
account, state_dir = _stuck(isolated_account_factory, tmp_path)
|
|
102
|
+
rc, out, _ = _cli(capsys, account["name"], "resolve", "--published", "8001", "--by", "masaru")
|
|
103
|
+
assert rc == 1
|
|
104
|
+
rc, out, err = _cli(capsys, account["name"], "resolve", "--published", "8001", "--by", "masaru",
|
|
105
|
+
"--confirm", _digest_from(out))
|
|
106
|
+
assert rc == 0, err
|
|
107
|
+
assert inflight_mod.read(state_dir) is None
|
|
108
|
+
qf = queuefile.parse(os.path.join(account["queue_dir"], "a.md"))
|
|
109
|
+
assert qf.get("status") == "posted" and str(qf.get("post_id")) == "8001"
|
|
110
|
+
assert len(inflight_mod.archives(state_dir)) == 1
|
|
111
|
+
rows, _ = admin_log.read(account=account["name"], event="inflight_resolved")
|
|
112
|
+
assert rows[-1]["diff"]["resolution"] == [None, "human_published"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_digestが違えば解かない_別のinflightには流用できない(isolated_account_factory, tmp_path, capsys):
|
|
116
|
+
account, state_dir = _stuck(isolated_account_factory, tmp_path, git=False)
|
|
117
|
+
rc, out, _ = _cli(capsys, account["name"], "resolve", "--not-published", "--by", "masaru")
|
|
118
|
+
code = _digest_from(out)
|
|
119
|
+
inflight_mod.update(state_dir, started="2026-09-09T11:00:00+09:00")
|
|
120
|
+
rc, _, err = _cli(capsys, account["name"], "resolve", "--not-published", "--by", "masaru",
|
|
121
|
+
"--confirm", code)
|
|
122
|
+
assert rc == 1 and "confirm_mismatch" in err
|
|
123
|
+
assert inflight_mod.read(state_dir) is not None and inflight_mod.archives(state_dir) == []
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_byと決めることが無ければ断る(isolated_account_factory, tmp_path, capsys, monkeypatch):
|
|
127
|
+
monkeypatch.delenv("THTH_ACTOR", raising=False)
|
|
128
|
+
account, state_dir = _stuck(isolated_account_factory, tmp_path, git=False)
|
|
129
|
+
rc, _, err = _cli(capsys, account["name"], "resolve", "--not-published")
|
|
130
|
+
assert rc == 1 and err.startswith("by_required")
|
|
131
|
+
rc, _, err = _cli(capsys, account["name"], "resolve", "--by", "masaru")
|
|
132
|
+
assert rc == 1 and err.startswith("decision_required")
|
|
133
|
+
assert inflight_mod.read(state_dir) is not None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_出たと分かっているinflightはnot_publishedで解かない(isolated_account_factory, tmp_path, capsys):
|
|
137
|
+
account, state_dir = _stuck(isolated_account_factory, tmp_path, git=False)
|
|
138
|
+
inflight_mod.update(state_dir, post_id="8002")
|
|
139
|
+
rc, _, err = _cli(capsys, account["name"], "resolve", "--not-published", "--by", "masaru")
|
|
140
|
+
assert rc == 1 and err.startswith("published_known")
|
|
141
|
+
rc, _, err = _cli(capsys, account["name"], "resolve", "--published", "9999", "--by", "masaru")
|
|
142
|
+
assert rc == 1 and err.startswith("post_id_differs")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_添付のinflightはこの口では解かない(isolated_account_factory, tmp_path, capsys):
|
|
146
|
+
account, state_dir = _stuck(isolated_account_factory, tmp_path, git=False)
|
|
147
|
+
record = inflight_mod.read(state_dir)
|
|
148
|
+
info = inflight_cli.summary({**record, "media": {"account": account["name"]}})
|
|
149
|
+
assert info["media"] is True and info["self_resolvable"] is False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_mcpにはinflightの口が無い():
|
|
153
|
+
spec = importlib.util.spec_from_file_location("thth_mcp_server", REPO / "mcp" / "server.py")
|
|
154
|
+
server = importlib.util.module_from_spec(spec)
|
|
155
|
+
spec.loader.exec_module(server)
|
|
156
|
+
assert not any("inflight" in tool["name"] for tool in server.TOOLS)
|