thth 3.2.0__tar.gz → 3.3.1__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.2.0 → thth-3.3.1}/PKG-INFO +2 -2
- {thth-3.2.0 → thth-3.3.1}/README.md +1 -1
- {thth-3.2.0 → thth-3.3.1}/mcp/server.py +83 -22
- {thth-3.2.0 → thth-3.3.1}/skills/thth/SKILL.md +5 -4
- {thth-3.2.0 → thth-3.3.1}/tests/conftest.py +22 -0
- thth-3.3.1/tests/helpers/fake_threads_status.py +123 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_incident_notifications.py +4 -2
- {thth-3.2.0 → thth-3.3.1}/tests/test_mcp.py +2 -1
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_json_contract.py +5 -1
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_read_coordination.py +3 -3
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_carryover_timeout.py +1 -1
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_review_followups.py +1 -1
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_stop_observation.py +1 -1
- {thth-3.2.0 → thth-3.3.1}/tests/test_v310_morning.py +6 -4
- {thth-3.2.0 → thth-3.3.1}/tests/test_v312_report_channel.py +21 -9
- thth-3.3.1/tests/test_v330_fingerprint_version.py +127 -0
- thth-3.3.1/tests/test_v330_held.py +270 -0
- thth-3.3.1/tests/test_v330_held_names.py +92 -0
- thth-3.3.1/tests/test_v330_last_refusals.py +156 -0
- thth-3.3.1/tests/test_v330_notification_route.py +97 -0
- thth-3.3.1/tests/test_v330_observe.py +137 -0
- thth-3.3.1/tests/test_v330_report_friction.py +50 -0
- thth-3.3.1/tests/test_v330_report_words.py +103 -0
- thth-3.3.1/tests/test_v330_reports_effect.py +66 -0
- thth-3.3.1/tests/test_v331_inflight_cli.py +156 -0
- thth-3.3.1/tests/test_v331_listing_resolve.py +136 -0
- thth-3.3.1/tests/test_v331_outbox_retention.py +135 -0
- thth-3.3.1/tests/test_v331_publish_status.py +213 -0
- thth-3.3.1/tests/test_v331_self_resolve.py +222 -0
- thth-3.3.1/thth/VERSION +1 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/base.py +6 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/threads.py +177 -7
- {thth-3.2.0 → thth-3.3.1}/thth/admin_log.py +3 -1
- {thth-3.2.0 → thth-3.3.1}/thth/approval.py +35 -0
- {thth-3.2.0 → thth-3.3.1}/thth/cli.py +116 -10
- {thth-3.2.0 → thth-3.3.1}/thth/core.py +122 -13
- {thth-3.2.0 → thth-3.3.1}/thth/doctor.py +11 -0
- thth-3.3.1/thth/fingerprint_golden.py +101 -0
- {thth-3.2.0 → thth-3.3.1}/thth/healthcheck.py +57 -3
- {thth-3.2.0 → thth-3.3.1}/thth/incident.py +185 -19
- {thth-3.2.0 → thth-3.3.1}/thth/inflight.py +61 -8
- thth-3.3.1/thth/inflight_cli.py +301 -0
- thth-3.3.1/thth/inflight_resolve.py +435 -0
- {thth-3.2.0 → thth-3.3.1}/thth/leave_gate.py +1 -1
- {thth-3.2.0 → thth-3.3.1}/thth/morning.py +193 -31
- thth-3.3.1/thth/notification_route.py +70 -0
- {thth-3.2.0 → thth-3.3.1}/thth/operations_handoff.py +42 -1
- {thth-3.2.0 → thth-3.3.1}/thth/read_coordination.py +2 -2
- thth-3.3.1/thth/refusals.py +162 -0
- {thth-3.2.0 → thth-3.3.1}/thth/report.py +14 -2
- {thth-3.2.0 → thth-3.3.1}/thth/report_inbox.py +129 -17
- {thth-3.2.0 → thth-3.3.1}/thth/report_service.py +21 -8
- {thth-3.2.0 → thth-3.3.1}/thth/runs.py +8 -0
- {thth-3.2.0 → thth-3.3.1}/thth/select.py +94 -1
- {thth-3.2.0 → thth-3.3.1}/thth/tool_version.py +8 -0
- thth-3.2.0/thth/VERSION +0 -1
- {thth-3.2.0 → thth-3.3.1}/.gitignore +0 -0
- {thth-3.2.0 → thth-3.3.1}/LICENSE +0 -0
- {thth-3.2.0 → thth-3.3.1}/accounts.example/bluesky.json +0 -0
- {thth-3.2.0 → thth-3.3.1}/accounts.example/mastodon.json +0 -0
- {thth-3.2.0 → thth-3.3.1}/accounts.example/threads.json +0 -0
- {thth-3.2.0 → thth-3.3.1}/accounts.example/x.json +0 -0
- {thth-3.2.0 → thth-3.3.1}/bin/thth +0 -0
- {thth-3.2.0 → thth-3.3.1}/bin/thth-remote +0 -0
- {thth-3.2.0 → thth-3.3.1}/bin/thth-run +0 -0
- {thth-3.2.0 → thth-3.3.1}/pyproject.toml +0 -0
- {thth-3.2.0 → thth-3.3.1}/skills/thth-admin/SKILL.md +0 -0
- {thth-3.2.0 → thth-3.3.1}/systemd/thth@.service +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/__init__.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/coordination_snapshot.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/fixtures/build_injection_eval.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/fixtures/callback/worker_responses.json +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/fixtures/injection_eval.json +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/fixtures/umami-bile.md +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/helpers/__init__.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_oauth_server.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_push_adapter.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_redirect_server.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_search_adapter.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_threads_reader.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_threads_writes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_x.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/helpers/hold_lock.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/helpers/hold_repo_lock.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/helpers/worker_probe.mjs +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_27_acceptance_fixes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_account_name_check.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_account_report.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_accounts_dir.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_adapter_boundary.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_admin_http_mcp.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_admin_log.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_admin_notifications.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_admin_repairs.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_admin_report.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_after_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_analytics_comparison.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_analytics_report.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_api_diagnostic.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_api_error_body.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_app_env_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_approval.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_approve_cmd.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_ask.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_atlas_fifth_review.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_atlas_fourth_review.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_atlas_review.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_atlas_second_review.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_atlas_third_review.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_audit1_fixes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_audit2_fixes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_audit_b_counterexamples.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_audit_b_m1_semantics.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_audit_b_n1_depth.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_audit_b_r1_closure.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_audit_v201.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_audit_v23_details.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_auth.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_auth_identity_required.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_behind_remote.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_bluesky_adapter.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_bluesky_secret_scrub.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_board_needs_review.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_board_running.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_bundle.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_charcount.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_cli_entrypoint.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_cli_help.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_cli_vm_path.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_collect.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_collect_engagements.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_collect_runs.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_collect_sent.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_comparability.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_control_chars_refused.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_conversation_paging.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_count_for.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_docs_links.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_doctor.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_doctor_all_permissions.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_doctor_next_step.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_duplicate_front_matter_keys.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_editorial_audit.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_editorial_n3_n4.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_engagements.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_external_acceptance.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_fake_api.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_form_spec.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_form_spec_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_forms.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_fresh_install.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_front_matter_injection.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_healthcheck_notifications.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_hypotheses.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_hypothesis_boundaries.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_hypothesis_units.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_impact_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_inbox_granted_scopes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_independent_thread_review.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_independent_topic_review.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_independent_topic_review_r2.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_inflight.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_inflight_tag_body_hash.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_jst.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_lint.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_list_projections.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_llm_trial.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_llm_trial_engage.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_lock.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_m2_independent.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_maintain.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_mastodon_adapter.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_mastodon_visibility.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_max_per_run.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_mcp_robustness.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_measured.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_media_accounts.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_media_next_steps.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_media_roundtrip.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_mismatch_diagnostics.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_n5_reply_null_element.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_n6_replies_incomplete_ledger.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_n7_doctor_error_body.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_no_limits.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_observation_provenance.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_operations_handoff.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_packaging.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_preview.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_production_mode.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_pull_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_queue.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_quiet_hours.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_r2_r4_r5.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_read_cli_bare_runtimeerror.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_reason_adoption.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_redact.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_redirect_token_leak.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_refresh.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_refresh_destination.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_rejudge_new.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_rejudge_r2_r3.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_replies.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_replies_refresh.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_repo_lock.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_repo_sync.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_report_checkout_import.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_report_http.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_report_isolation.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_report_service.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_reverse_audit.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_review_case_id_cluster_m2.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_review_case_id_cluster_r2.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_review_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_review_evidence_account.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_review_form_spec_and_case_id.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_review_provenance.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_review_records.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_revoke.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_roundtrip.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_run_missing_env.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_run_record_failclosed.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_runs_collected.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_runs_read_rows.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_schedule_multiple.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_schedule_view.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_scopes_source_requested.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_search_engage_targets.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_search_media_routes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_secret_value_redact.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_secrets_grep.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_security_small_fixes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_select.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_selfupdate.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_send.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_send_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_send_control_chars.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_send_media.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_send_visibility.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_sent_integrity.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_server_json.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_site.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_skill_doc.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_study_report.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_systemd.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_thread_publish.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_thread_read.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_threadrun.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_threads_read_permissions.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_threadshape.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_thth_root.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_token_exists.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_token_scopes_record.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_token_set.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_topic.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_topic_advice.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_topic_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_topic_models.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_topic_review_fixes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_topic_shelf_observers.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_topic_store.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_topics_record_concurrent.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_topics_tool.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_trial_frictions.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_admin_transaction.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_approve_account.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_length.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_lock_wait.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_mastodon_scopes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_mentions.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_post_urls.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_repo_paths.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_reposts.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_study_add.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_tool_version.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_unanswered.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_unanswered_scope.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_where_arguments.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_where_filters.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v210_who_arguments.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v211_auth_integration.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v211_authflow.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v211_bluesky_stdin.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v211_doctor_binding.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v211_mastodon_auth.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v211_x_auth.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_approval_relay.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_ask_coordination.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_budget.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_budget_auth.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_budget_mcp.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_deletion.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_job_completion_fault.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_leave.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_leave_gate.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_leave_providers.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_leave_token_safety.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_mastodon_guide.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_mcp_compat.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_missing_token.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_server_writes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_token_parent.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v212_write_storage.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v2131_send_dry_run.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v2131_threads_publish_wait.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v2132_send_inflight_hint.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v2132_threads_create_timeout.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v2132_threads_create_unknown.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_acceptance_fixes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_adts_asf.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_approval_attachments.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_attachment_kinds.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_external.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_media.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_metadata.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_metadata_uri.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_metadata_video.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_record.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_record_video.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_video.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_webm_named.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bmff_typed_metadata.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_budget_lock.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_capabilities.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_graphemes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_auth_media.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_bmff_audio.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_flac.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_gif.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_link.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_media.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_mp3.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_ogg_opus.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_poll.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_quote.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_thumbnail.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_wav.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_webm_audio.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_webm_video.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_cleanup.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_formats.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_foundation.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_prepared.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_relay.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_relay_refusals.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_upload.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_receipt_deadline.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_stage810_audit.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_static_diagnostics.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_systemd_worker.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_ghost.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_gif.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_link.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_media.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_media_spoiler.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_poll.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_quote.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_quote_media.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_replies.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_text.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_text_spoiler.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_video.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_transport_review.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_video_typed_metadata.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_vorbis_comment_boundary.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v213_webm_schema_boundary.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v2141_draft_lint_reason.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v2141_managed_repo_umask.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v214_release_signing.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v214_x_adapter.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v214_x_auth.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v214_x_media.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v214_x_posts_budget.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v21b_approved_writes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v28_tags.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v310_search_timeout.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v310_watch_words.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v311_leave_same_user.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v311_mastodon_retract.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v311_morning_overdue.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v311_morning_own_exclusion.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v311_morning_unanswered_window.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v311_notifications_guidance.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v311_replies_owned.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v312_leave_shared_token.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v312_own_handles_stopped.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v312_report_admin.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v312_report_inbox.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v312_report_mcp_user.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v31_analysis.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v31_collection.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v320_reply_to_file_display.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v320_reply_to_file_lint.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v320_reply_to_file_resolve.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_v320_report_fixes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_value_domains_shown.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_version.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_where_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_who_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_writeback_rebase.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_writeback_validate.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/tests/test_x_metrics.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/__init__.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/__main__.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/_grapheme_data.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/account_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/account_report.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/accounts.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/__init__.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/auth_mastodon.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/auth_threads.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/auth_x.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/bluesky.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/bluesky_media.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/bluesky_video.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/mastodon.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/mastodon_media.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/threads_media.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/x.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/adapters/x_media.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/admin_notifications.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/admin_report.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/after_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/analytics_comparison.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/analytics_report.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/analytics_shapes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/analytics_threads.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/api_diagnostic.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/appconfig.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/appenv.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/approval_jobs.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/approval_relay.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/ask.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/ask_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/audioformats.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/authclients.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/authflow.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/authpassword.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/bluesky_metadata.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/budget_x.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/budget_x_posts.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/bundle.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/collect.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/collection_context.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/collection_status.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/deletion.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/ebmlaudio.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/ebmlvideo.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/engagements.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/flacformats.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/forms.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/graphemes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/handoff_cursor.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/httpsafe.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/jst.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/leave.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/lint.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/lock.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/maintain.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/managed_repo.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/measured.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/media.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/media_capabilities.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/media_cleanup.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/media_delivery.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/media_relay.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/media_uploads.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/mediaformats.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/mpeg_audio.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/oauth.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/oggformats.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/postid.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/queuefile.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/read_window.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/redact.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/replies.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/report_details.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/report_http.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/report_isolation.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/retract_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/scopes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/secrets_fs.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/selfupdate.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/sent.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/server_files.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/server_writes.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/stop_observation.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/study_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/study_report.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/systemd_gen.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/tags.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/thread_read.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/threadrun.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/threads_read_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/threadshape.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/threadthrow.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/topic_advice.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/topic_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/topic_models.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/topic_store.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/topics.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/unanswered.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/vorbisformats.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/watch_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/where_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/who_cli.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/thth/writeback.py +0 -0
- {thth-3.2.0 → thth-3.3.1}/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.3.1
|
|
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`・`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`・`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`・`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`・`admin`。
|
|
105
105
|
- **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
|
|
106
106
|
- **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
|
|
107
107
|
- **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
|
|
@@ -295,12 +295,28 @@ TOOLS = [
|
|
|
295
295
|
},
|
|
296
296
|
},
|
|
297
297
|
{
|
|
298
|
-
# **名前と説明文がそのまま売り文句**(設計 3.1.0 §1
|
|
298
|
+
# **名前と説明文がそのまま売り文句**(設計 3.1.0 §1・3.3.0 §F で観測に)。
|
|
299
|
+
"name": "thth_observe",
|
|
300
|
+
"description": (
|
|
301
|
+
"セッションの始めと区切りごとに呼ぶ。前回の観測から何があって、いまなにを"
|
|
302
|
+
"すればよいかを、道具が事実だけで 1 枚にする。本文は作らない"
|
|
303
|
+
),
|
|
304
|
+
"inputSchema": {
|
|
305
|
+
"type": "object",
|
|
306
|
+
"properties": {
|
|
307
|
+
"target": {"type": "string", "description": "project 名か account 名"},
|
|
308
|
+
"mark": {"type": "boolean",
|
|
309
|
+
"description": "栞(前回読んだ時点)を進める(既定 true)"},
|
|
310
|
+
},
|
|
311
|
+
"required": ["target"],
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
# 別名(3.3.0 §F)。既存の skill・セッションを壊さないために残す。
|
|
299
316
|
"name": "thth_morning",
|
|
300
|
-
# **設計 3.1.0 §0 の約束そのまま。**
|
|
301
317
|
"description": (
|
|
302
|
-
"
|
|
303
|
-
"
|
|
318
|
+
"thth_observe の別名(同じ 1 枚を返す)。セッションの始めと区切りごとに呼ぶ。"
|
|
319
|
+
"前回の観測から何があって、いまなにをすればよいかを、道具が事実だけで 1 枚にする"
|
|
304
320
|
),
|
|
305
321
|
"inputSchema": {
|
|
306
322
|
"type": "object",
|
|
@@ -589,16 +605,25 @@ del _tool
|
|
|
589
605
|
# credential の `actor` が決める(要求の欄からは作らない)。
|
|
590
606
|
REPORT_TOOLS = [
|
|
591
607
|
{"name": "thth_report_file",
|
|
592
|
-
"description": "
|
|
593
|
-
"
|
|
608
|
+
"description": "道具が断った・結果が期待と違った・欲しい形がある・迷った、のどれかならこれで置く。"
|
|
609
|
+
"つまずき・迷い・期待との違いの報告は、利用者の作業の一部として歓迎します。"
|
|
610
|
+
"小さいものも。重複は道具が束ねます。"
|
|
611
|
+
"例: 「止まったのに気づかなかった」「断られた理由が分からなかった」"
|
|
612
|
+
"「同じ操作を 3 回繰り返した」。断られた直後なら from_last_refusal: true で"
|
|
613
|
+
"道具が控えた断りを再現手順に添えられる"
|
|
614
|
+
"(kind は bug・request・friction〔迷った・分かりにくかった・同じ操作を繰り返した〕。"
|
|
615
|
+
"実装側が読み、返事は thth_report_show と "
|
|
594
616
|
"operations_handoff の tool.reports に出る。秘密らしき値が含まれていたら置かない)",
|
|
595
617
|
"inputSchema": {"type": "object", "properties": {
|
|
596
618
|
"account": {"type": "string"},
|
|
597
|
-
"kind": {"type": "string", "enum": ["bug", "request"]},
|
|
619
|
+
"kind": {"type": "string", "enum": ["bug", "request", "friction"]},
|
|
598
620
|
"title": {"type": "string", "description": "1 行・120 字まで"},
|
|
599
|
-
"body": {"type": "string", "description": "8,000
|
|
600
|
-
"repro": {"type": "string", "description": "再現手順(任意・4,000 字まで)"}
|
|
601
|
-
"
|
|
621
|
+
"body": {"type": "string", "description": "8,000 字まで(from_last_refusal のときは省略可)"},
|
|
622
|
+
"repro": {"type": "string", "description": "再現手順(任意・4,000 字まで)"},
|
|
623
|
+
"from_last_refusal": {"type": "boolean",
|
|
624
|
+
"description": "この account で直前に道具が断った記録(時刻・版・命令・"
|
|
625
|
+
"理由の符丁)を再現手順として添える。kind の既定は friction"}},
|
|
626
|
+
"required": ["account", "title"], "additionalProperties": False}},
|
|
602
627
|
{"name": "thth_report_list",
|
|
603
628
|
"description": "自分の project の報告と、実装側の返事の数を読む(読むだけ)",
|
|
604
629
|
"inputSchema": {"type": "object", "properties": {
|
|
@@ -630,12 +655,16 @@ def _with_report_hint(tools):
|
|
|
630
655
|
else dict(tool, description=tool["description"] + REPORT_HINT) for tool in tools]
|
|
631
656
|
|
|
632
657
|
|
|
633
|
-
def _channel_note():
|
|
634
|
-
"""断りの応答に添える 2 つ目の text
|
|
658
|
+
def _channel_note(account=None, reason=None):
|
|
659
|
+
"""断りの応答に添える 2 つ目の text。本文は入れない。
|
|
660
|
+
|
|
661
|
+
account(credential が許した・台帳にある名前)と理由の符丁が分かれば、そのまま
|
|
662
|
+
呼べる `thth_report_file` の 1 行(設計 3.3.0 B3)。分からなければ静的な 1 行。
|
|
663
|
+
"""
|
|
635
664
|
if APP_DIR not in sys.path:
|
|
636
665
|
sys.path.insert(0, APP_DIR)
|
|
637
|
-
from thth.report_inbox import
|
|
638
|
-
return {"type": "text", "text":
|
|
666
|
+
from thth.report_inbox import mcp_refusal_line
|
|
667
|
+
return {"type": "text", "text": mcp_refusal_line(account, reason)}
|
|
639
668
|
|
|
640
669
|
|
|
641
670
|
def server_mode():
|
|
@@ -649,7 +678,7 @@ def server_tools(context):
|
|
|
649
678
|
if context.scope=='admin': return reports + ADMIN_TOOLS
|
|
650
679
|
# 毎朝の一枚は**利用者の scope だけ**(設計 3.1.0 §1)。credential が許した
|
|
651
680
|
# account/project しか対象にできない(`report_service.execute_morning()`)。
|
|
652
|
-
reports = reports + [tool for tool in TOOLS if tool['name']
|
|
681
|
+
reports = reports + [tool for tool in TOOLS if tool['name'] in ('thth_observe', 'thth_morning')]
|
|
653
682
|
from thth.server_writes import WRITE_OPERATIONS
|
|
654
683
|
return _with_report_hint(reports + [tool for tool in SERVER_TOOLS
|
|
655
684
|
if context.writes or tool['name'][5:] not in WRITE_OPERATIONS]) + REPORT_TOOLS
|
|
@@ -659,8 +688,11 @@ def server_call(name, arguments):
|
|
|
659
688
|
from thth.report_service import execute_report, execute_mcp_report, ReportServiceError
|
|
660
689
|
from thth.server_writes import execute, WRITE_OPERATIONS, SAFE_ERRORS, DRAFT_REASONS
|
|
661
690
|
context=authenticated_context()
|
|
662
|
-
|
|
663
|
-
failure
|
|
691
|
+
|
|
692
|
+
def failure(value):
|
|
693
|
+
# 断りは 1 つ目の text が静的な理由、2 つ目が受け口の案内(設計 3.1.2 §3.5)。
|
|
694
|
+
account, code = _remember_refusal(context, name, arguments, value)
|
|
695
|
+
return {'content':[{'type':'text','text':value},_channel_note(account, code)],'isError':True}
|
|
664
696
|
if context is None: return failure('unauthorized')
|
|
665
697
|
tool=next((tool for tool in server_tools(context) if tool['name']==name),None)
|
|
666
698
|
if tool is None: return failure('unsupported_operation')
|
|
@@ -692,9 +724,10 @@ def server_call(name, arguments):
|
|
|
692
724
|
elif operation in ('report_file','report_list','report_show','report_add'):
|
|
693
725
|
from thth.report_service import execute_user_reports
|
|
694
726
|
result=execute_user_reports(context,request)
|
|
695
|
-
elif operation
|
|
727
|
+
elif operation in ('morning', 'observe'):
|
|
696
728
|
from thth.report_service import execute_morning
|
|
697
|
-
|
|
729
|
+
request={**request,'operation':'morning'}
|
|
730
|
+
result=execute_morning(context,request,invoked_as=operation)
|
|
698
731
|
elif operation in ('analytics_report', 'operations_handoff', 'study_report'):
|
|
699
732
|
result=execute_mcp_report(context,request)
|
|
700
733
|
else:
|
|
@@ -714,6 +747,34 @@ def server_call(name, arguments):
|
|
|
714
747
|
return failure('request_unavailable')
|
|
715
748
|
|
|
716
749
|
|
|
750
|
+
def _remember_refusal(context, name, arguments, value):
|
|
751
|
+
"""サーバ型の断りを account ごとに控える(設計 3.3.0 B2)。控えの失敗で断りを変えない。
|
|
752
|
+
|
|
753
|
+
戻り値は断りの 2 つ目の text に埋める `(account, 符丁)`(B3・分からなければ None)。
|
|
754
|
+
|
|
755
|
+
account は credential が許した名前のときだけ。残すのは道具の名前と、断りの
|
|
756
|
+
先頭の静的な符丁だけ(`thth/refusals.py`・引数の値は残さない)。報告の口の
|
|
757
|
+
断りは控えない(報告しようとしていた元の断りを押し出さないため)。
|
|
758
|
+
"""
|
|
759
|
+
try:
|
|
760
|
+
if context is None or not isinstance(arguments, dict) or not isinstance(name, str):
|
|
761
|
+
return None, None
|
|
762
|
+
account = arguments.get('account')
|
|
763
|
+
if not isinstance(account, str) or account not in (context.allowed_accounts or {}):
|
|
764
|
+
return None, None
|
|
765
|
+
if not name.replace('_', '').isalnum():
|
|
766
|
+
return None, None
|
|
767
|
+
from thth import refusals
|
|
768
|
+
command = 'mcp ' + name
|
|
769
|
+
values = [v for v in arguments.values() if isinstance(v, str)]
|
|
770
|
+
code = refusals.reason_code(value, argv=values, command=command, rc=2)
|
|
771
|
+
if not name.startswith('thth_report_'):
|
|
772
|
+
refusals.record(account, command=command, reason_code=code)
|
|
773
|
+
return account, code
|
|
774
|
+
except Exception:
|
|
775
|
+
return None, None
|
|
776
|
+
|
|
777
|
+
|
|
717
778
|
def admin_context():
|
|
718
779
|
context=authenticated_context()
|
|
719
780
|
return context if context is not None and context.scope=='admin' else None
|
|
@@ -892,10 +953,10 @@ def call_tool(name: str, arguments: dict | None) -> dict:
|
|
|
892
953
|
args.append("--json")
|
|
893
954
|
proc = run_cli(args)
|
|
894
955
|
text = proc.stdout
|
|
895
|
-
elif name
|
|
956
|
+
elif name in ("thth_morning", "thth_observe"):
|
|
896
957
|
# **`where_to_appear` と同じ型**: CLI(`thth morning`)を `--json` で
|
|
897
958
|
# 呼ぶだけ(設計 3.1.0 §1・§5)。栞を進めないときだけ旗を足す。
|
|
898
|
-
args = ["
|
|
959
|
+
args = [name[len("thth_"):], arguments["target"]]
|
|
899
960
|
if arguments.get("mark") is False:
|
|
900
961
|
args.append("--no-mark")
|
|
901
962
|
args.append("--json")
|
|
@@ -925,7 +986,7 @@ def call_tool(name: str, arguments: dict | None) -> dict:
|
|
|
925
986
|
|
|
926
987
|
if name.startswith("thth_topic_") or name in (
|
|
927
988
|
"before_you_post", "after_you_posted", "analytics_report", "operations_handoff", "study_report", "thread_read", "where_to_appear",
|
|
928
|
-
"who_is_this", "thth_morning"):
|
|
989
|
+
"who_is_this", "thth_morning", "thth_observe"):
|
|
929
990
|
# **新しい道具は exit 1 も isError**(設計 §7)。lint の exit 1(検査結果)
|
|
930
991
|
# とは意味が違う——こちらは stale_context・不正な候補比較で、
|
|
931
992
|
# **そのまま使ってはいけない**応答。既存の扱いは変えない。
|
|
@@ -3,9 +3,9 @@ name: thth
|
|
|
3
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
|
-
**断られたら・違和感があれば報告の口へ**: `thth_report_file`(CLI は `thth report file <account> --kind bug|request --title … --body-file … --by
|
|
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
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` を「変更なし」と解釈しない。
|
|
11
11
|
|
|
@@ -149,7 +149,8 @@ MCP `after_you_posted`。**24h の刻みが無ければ `null`**(0 と混ぜ
|
|
|
149
149
|
|
|
150
150
|
- `thth --help` / `thth <subcommand> --help` が正本。
|
|
151
151
|
- `thth doctor <account>` が「いま投稿できる状態か」を最初から最後まで言う。
|
|
152
|
-
-
|
|
152
|
+
- 道具の不具合・欲しい形・迷ったことは `thth report file`(MCP `thth_report_file`)で実装側に届ける(小さいものも歓迎・重複は道具が束ねる)。
|
|
153
|
+
- 承認済みなのに出ない原稿は `thth_observe` の予定の段 `held_items`(file・理由・予定・経過)と `thth board` の要確認(先頭 5 本)に名前が出る。`approval_stale` は再承認(`thth approve` の二段)で直る。時刻を過ぎて出られないものがあると `thth run` は `held` として死活通知と運用通知に知らせる。
|
|
153
154
|
- 自分の開いている報告に書き足すなら `thth report add <report_id> --body-file <path|-> --by <名前>`(MCP `thth_report_add`)。
|
|
154
155
|
- 使い方の全文: `docs/使い方_プロジェクトのセッション向け_2026-09-09.md`。
|
|
155
156
|
|
|
@@ -167,7 +168,7 @@ MCP `after_you_posted`。**24h の刻みが無ければ `null`**(0 と混ぜ
|
|
|
167
168
|
|
|
168
169
|
新規 account は利用者メール・管理者メール・TLS SMTP を `thth notifications config` で設定し、`status` → `test` を実行する。両宛先の受信箱で到達、account 専用 HEALTHCHECK_URL の missed-ping 通知を確認してから production/scheduled を有効にする。既存 account にも同じ設定を追加する。詳細は `docs/停止通知と運用記録.md`。
|
|
169
170
|
|
|
170
|
-
プロジェクトの作業開始・再開時は `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
|
|
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 を手で消して再投稿しない(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 を見る。
|
|
171
172
|
|
|
172
173
|
`mentions <account> --json` は 3 媒体の言及を同じ項目で読む。返信前に `replied` の object/false/null を区別する。`unanswered <account> --since 7d --json` は自分の根投稿の未回答候補を台帳で読む。`cannot_say` と採集の古さを確認し、`--refresh` は明示されたときだけ使う。件数は account ごとに扱い、送信は既存の承認・digest・production の門を通す。
|
|
173
174
|
|
|
@@ -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)に固定する。
|
|
@@ -429,3 +439,15 @@ def init_git_pair(tmp_path, *, seed_content: str, seed_name: str = "a.md"):
|
|
|
429
439
|
|
|
430
440
|
return {"bare": bare, "seed": seed, "work": work,
|
|
431
441
|
"queue_dir": os.path.join(work, "docs", "sns", "queue")}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def is_refusal(err: str, code: str) -> bool:
|
|
445
|
+
"""断りの stderr が「理由の符丁 1 行+打てる報告の 1 行」か(3.1.2 §3.5・3.3.0 B3)。
|
|
446
|
+
|
|
447
|
+
3.3.0 から 2 行目は account と理由の符丁を埋めた `report_inbox.refusal_line()`。
|
|
448
|
+
account は命令と台帳しだいで `<account>` のままになるので、ここでは形と符丁だけ見る。
|
|
449
|
+
"""
|
|
450
|
+
lines = err.strip().splitlines()
|
|
451
|
+
return (len(lines) == 2 and lines[0] == code
|
|
452
|
+
and lines[1].startswith("report_channel: thth report file ")
|
|
453
|
+
and f'--title "{code} で断られた"' in lines[1])
|
|
@@ -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}"}
|
|
@@ -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):
|
|
@@ -102,11 +102,12 @@ def test_mcp_stdioでtools_listとtools_callが通る(isolated_account):
|
|
|
102
102
|
# 2026-09-16 に `where_to_appear` を足した(設計「自分の泉」§2.3・T2-3・読むだけ)。
|
|
103
103
|
# 2026-09-16 に `who_is_this` を足した(設計「自分の泉」§2.4・T3-3・読むだけ)。
|
|
104
104
|
# 2026-09-23 に `thth_morning` を足した(設計 3.1.0 §1・読むだけ・栞だけ進む)。
|
|
105
|
+
# 2026-09-23 に `thth_observe` を足した(設計 3.3.0 §F・thth_morning は別名)。
|
|
105
106
|
assert tool_names == {"thth_lint", "thth_queue", "thth_preview", "thth_board",
|
|
106
107
|
"thth_topic_context", "thth_topic_evaluate",
|
|
107
108
|
"thth_topic_decision", "before_you_post", "after_you_posted",
|
|
108
109
|
"thread_read", "where_to_appear", "who_is_this", "analytics_report", "operations_handoff", "study_report",
|
|
109
|
-
"thth_morning"}
|
|
110
|
+
"thth_morning", "thth_observe"}
|
|
110
111
|
# 副作用のあるものは 1 つも出ていない。
|
|
111
112
|
assert not (tool_names & {"thth_approve", "thth_throw", "thth_token",
|
|
112
113
|
"thth_auth", "thth_refresh", "thth_revoke",
|
|
@@ -26,9 +26,13 @@ def json_parsers():
|
|
|
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
28
|
'morning', 'admin watch set', 'admin watch show',
|
|
29
|
+
# 3.3.0 §F: 観測(observe)。morning は別名として残る。
|
|
30
|
+
'observe',
|
|
29
31
|
# 3.1.2: 報告の口(設計 3.1.2 §1)。
|
|
30
32
|
'report file', 'report list', 'report show', 'report add', 'admin reports list', 'admin reports show',
|
|
31
|
-
'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']
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
def test_json_table_covers_every_parser():
|
|
@@ -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
|
|
64
|
+
assert __import__('tests.conftest').conftest.is_refusal(captured.err, 'account_leaving') # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 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
|
|
72
|
+
assert __import__('tests.conftest').conftest.is_refusal(capsys.readouterr().err, 'account_leaving') # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 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
|
|
148
|
+
assert __import__('tests.conftest').conftest.is_refusal(result.stderr, 'account_leaving') # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 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
|
|
54
|
+
assert result.returncode == 2 and __import__('tests.conftest').conftest.is_refusal(result.stderr, 'account_leaving') # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 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
|
|
17
|
+
assert __import__('tests.conftest').conftest.is_refusal(captured.err, 'invalid_account_name') # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 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
|
|
33
|
+
shown=capsys.readouterr();assert __import__('tests.conftest').conftest.is_refusal(shown.err, expected) # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 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}
|
|
@@ -285,9 +285,10 @@ def test_既定で栞が進み_no_markでは進まない(one, thth_root):
|
|
|
285
285
|
assert morning.build(PROJECT, now=NOW, mark=False)["marked"] == []
|
|
286
286
|
assert not path.exists(), "--no-mark なのに栞が進んだ"
|
|
287
287
|
payload = morning.build(PROJECT, now=NOW)
|
|
288
|
-
|
|
288
|
+
# 3.3.0 §F: 栞の名乗りは呼んだ名前(build の既定は observe)。
|
|
289
|
+
assert payload["marked"] == [ACCOUNT] and payload["marked_by"] == "observe"
|
|
289
290
|
saved = json.loads(path.read_text(encoding="utf-8"))
|
|
290
|
-
assert saved["by"] == "
|
|
291
|
+
assert saved["by"] == "observe" and saved["snapshot"]["tool_version"]
|
|
291
292
|
|
|
292
293
|
|
|
293
294
|
def test_栞を進めたあとは版の変化が読める(one, thth_root):
|
|
@@ -349,9 +350,10 @@ def test_MCPのthth_morningは設計の文言でCLIを呼ぶだけ(one, monkeypa
|
|
|
349
350
|
monkeypatch.delenv(key, raising=False)
|
|
350
351
|
server = _load_server_module()
|
|
351
352
|
tool = next(item for item in server.TOOLS if item["name"] == "thth_morning")
|
|
353
|
+
# 3.3.0 §F: thth_morning は thth_observe の別名になった(説明文も観測の文言)。
|
|
352
354
|
assert tool["description"] == (
|
|
353
|
-
"
|
|
354
|
-
"
|
|
355
|
+
"thth_observe の別名(同じ 1 枚を返す)。セッションの始めと区切りごとに呼ぶ。"
|
|
356
|
+
"前回の観測から何があって、いまなにをすればよいかを、道具が事実だけで 1 枚にする")
|
|
355
357
|
seen = []
|
|
356
358
|
|
|
357
359
|
class Done:
|
|
@@ -21,17 +21,26 @@ import pytest
|
|
|
21
21
|
|
|
22
22
|
from thth import accounts, cli, jst, morning, operations_handoff, report_inbox
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
# **文面は静的**(設計 §3.5)。ここが正本の写し——変えるなら設計と一緒に。
|
|
25
|
+
# 3.3.0 B1・B4: friction と「報告は依頼の範囲内」の 1 文を足した。断りの行は
|
|
26
|
+
# account と理由の符丁を埋めた「打てる 1 行」になった(B3・`refusal_line()`)。
|
|
27
|
+
WELCOME = ("つまずき・迷い・期待との違いの報告は、利用者の作業の一部として歓迎します。"
|
|
28
|
+
"小さいものも。重複は道具が束ねます")
|
|
29
|
+
CHANNEL_LINE = ("report_channel: thth report file <account> --kind bug|request|friction"
|
|
30
|
+
"(MCP: thth_report_file)——" + WELCOME)
|
|
31
|
+
LIMITATION = "不具合・要望・つまずきは report の口へ(thth_report_file)"
|
|
27
32
|
HINT = "(うまくいかなければ thth_report_file)"
|
|
28
33
|
|
|
29
34
|
|
|
30
35
|
def test_受け口の文面は1種類で設計のまま():
|
|
31
36
|
assert report_inbox.CHANNEL_LINE == CHANNEL_LINE
|
|
32
37
|
assert report_inbox.CHANNEL == {
|
|
33
|
-
"cli": "thth report file <account> --kind bug|request --title … --body-file … --by <名前>",
|
|
34
|
-
"
|
|
38
|
+
"cli": "thth report file <account> --kind bug|request|friction --title … --body-file … --by <名前>",
|
|
39
|
+
"from_last_refusal": "thth report file <account> --from-last-refusal --title … --by <名前>",
|
|
40
|
+
"mcp": "thth_report_file", "when": "unexpected_or_unsupported",
|
|
41
|
+
"welcome": WELCOME,
|
|
42
|
+
"examples": ["止まったのに気づかなかった", "断られた理由が分からなかった",
|
|
43
|
+
"同じ操作を 3 回繰り返した"]}
|
|
35
44
|
|
|
36
45
|
|
|
37
46
|
@pytest.fixture
|
|
@@ -169,7 +178,10 @@ def test_サーバ型の利用者の一枚には件数を出さない(projects,
|
|
|
169
178
|
def test_CLIの断りの後ろに受け口の1行_成功には載せない(projects, tmp_path, capsys):
|
|
170
179
|
assert cli.main(["morning", "nobody", "--json"]) == 2
|
|
171
180
|
err = capsys.readouterr().err.strip().splitlines()
|
|
172
|
-
|
|
181
|
+
# 3.3.0 B3: 理由の符丁を埋めた打てる 1 行(account を持たない命令は <account> のまま)。
|
|
182
|
+
assert err == ["target_unknown",
|
|
183
|
+
'report_channel: thth report file <account> --kind friction --from-last-refusal'
|
|
184
|
+
' --title "target_unknown で断られた" --by <名前>(' + WELCOME + ')']
|
|
173
185
|
assert cli.main(["report", "list", "kopicha", "--json"]) == 0
|
|
174
186
|
assert CHANNEL_LINE not in capsys.readouterr().err
|
|
175
187
|
|
|
@@ -179,14 +191,14 @@ def test_lintの検査結果の1は断りではない(projects, tmp_path, capsys
|
|
|
179
191
|
bad.write_text("---\nthth: 1\n---\n本文\n", encoding="utf-8")
|
|
180
192
|
rc = cli.main(["lint", str(bad), "--json"])
|
|
181
193
|
assert rc == 1
|
|
182
|
-
assert
|
|
194
|
+
assert "report_channel" not in capsys.readouterr().err
|
|
183
195
|
|
|
184
196
|
|
|
185
197
|
def test_helpの末尾に1行(capsys):
|
|
186
198
|
with pytest.raises(SystemExit):
|
|
187
199
|
cli.main(["--help"])
|
|
188
200
|
out = capsys.readouterr().out.rstrip().splitlines()
|
|
189
|
-
assert out[-1] == "
|
|
201
|
+
assert out[-1] == "不具合・要望・つまずきは report の口へ: thth report file <account> --kind bug|request|friction(MCP: thth_report_file)"
|
|
190
202
|
with pytest.raises(SystemExit):
|
|
191
203
|
cli.main(["report", "--help"])
|
|
192
204
|
assert "道具が断った・結果が期待と違った・欲しい形がある" in capsys.readouterr().out
|
|
@@ -255,7 +267,7 @@ def test_利用者のtools_listの説明文の末尾に1句(user_server, monkeyp
|
|
|
255
267
|
else:
|
|
256
268
|
assert tool["description"].endswith(HINT), tool["name"]
|
|
257
269
|
file_tool = next(tool for tool in listed if tool["name"] == "thth_report_file")
|
|
258
|
-
assert file_tool["description"].startswith("
|
|
270
|
+
assert file_tool["description"].startswith("道具が断った・結果が期待と違った・欲しい形がある・迷った、のどれかならこれで置く")
|
|
259
271
|
# 定数(売り文句の正本)は書き換えない。
|
|
260
272
|
assert not any(tool["description"].endswith(HINT) for tool in server.TOOLS)
|
|
261
273
|
# 管理者の一覧には足さない(管理者は報告を置く側ではない)。
|