thth 3.1.0__tar.gz → 3.1.2__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.
Files changed (486) hide show
  1. {thth-3.1.0 → thth-3.1.2}/PKG-INFO +2 -2
  2. {thth-3.1.0 → thth-3.1.2}/README.md +1 -1
  3. {thth-3.1.0 → thth-3.1.2}/mcp/server.py +87 -8
  4. {thth-3.1.0 → thth-3.1.2}/skills/thth/SKILL.md +3 -0
  5. {thth-3.1.0 → thth-3.1.2}/skills/thth-admin/SKILL.md +1 -1
  6. {thth-3.1.0 → thth-3.1.2}/tests/test_admin_http_mcp.py +3 -1
  7. {thth-3.1.0 → thth-3.1.2}/tests/test_ask.py +5 -1
  8. {thth-3.1.0 → thth-3.1.2}/tests/test_replies.py +13 -2
  9. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_json_contract.py +4 -1
  10. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_leave.py +2 -1
  11. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_leave_providers.py +13 -5
  12. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_read_coordination.py +3 -3
  13. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_carryover_timeout.py +1 -1
  14. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_review_followups.py +1 -1
  15. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_stop_observation.py +1 -1
  16. {thth-3.1.0 → thth-3.1.2}/tests/test_v21b_approved_writes.py +3 -0
  17. thth-3.1.2/tests/test_v311_leave_same_user.py +68 -0
  18. thth-3.1.2/tests/test_v311_mastodon_retract.py +218 -0
  19. thth-3.1.2/tests/test_v311_morning_overdue.py +58 -0
  20. thth-3.1.2/tests/test_v311_morning_own_exclusion.py +93 -0
  21. thth-3.1.2/tests/test_v311_morning_unanswered_window.py +62 -0
  22. thth-3.1.2/tests/test_v311_notifications_guidance.py +51 -0
  23. thth-3.1.2/tests/test_v311_replies_owned.py +103 -0
  24. thth-3.1.2/tests/test_v312_leave_shared_token.py +126 -0
  25. thth-3.1.2/tests/test_v312_own_handles_stopped.py +112 -0
  26. thth-3.1.2/tests/test_v312_report_admin.py +269 -0
  27. thth-3.1.2/tests/test_v312_report_channel.py +275 -0
  28. thth-3.1.2/tests/test_v312_report_inbox.py +312 -0
  29. thth-3.1.2/tests/test_v312_report_mcp_user.py +154 -0
  30. thth-3.1.2/thth/VERSION +1 -0
  31. {thth-3.1.0 → thth-3.1.2}/thth/adapters/mastodon.py +51 -0
  32. {thth-3.1.0 → thth-3.1.2}/thth/admin_log.py +4 -2
  33. {thth-3.1.0 → thth-3.1.2}/thth/admin_report.py +3 -0
  34. {thth-3.1.0 → thth-3.1.2}/thth/cli.py +25 -3
  35. {thth-3.1.0 → thth-3.1.2}/thth/collect.py +30 -0
  36. {thth-3.1.0 → thth-3.1.2}/thth/incident.py +9 -0
  37. {thth-3.1.0 → thth-3.1.2}/thth/leave.py +51 -16
  38. {thth-3.1.0 → thth-3.1.2}/thth/morning.py +179 -21
  39. {thth-3.1.0 → thth-3.1.2}/thth/operations_handoff.py +28 -1
  40. {thth-3.1.0 → thth-3.1.2}/thth/redact.py +64 -0
  41. {thth-3.1.0 → thth-3.1.2}/thth/replies.py +62 -5
  42. thth-3.1.2/thth/report_inbox.py +898 -0
  43. {thth-3.1.0 → thth-3.1.2}/thth/report_isolation.py +2 -0
  44. {thth-3.1.0 → thth-3.1.2}/thth/report_service.py +105 -0
  45. {thth-3.1.0 → thth-3.1.2}/thth/retract_cli.py +7 -5
  46. {thth-3.1.0 → thth-3.1.2}/thth/unanswered.py +2 -1
  47. thth-3.1.0/thth/VERSION +0 -1
  48. {thth-3.1.0 → thth-3.1.2}/.gitignore +0 -0
  49. {thth-3.1.0 → thth-3.1.2}/LICENSE +0 -0
  50. {thth-3.1.0 → thth-3.1.2}/accounts.example/bluesky.json +0 -0
  51. {thth-3.1.0 → thth-3.1.2}/accounts.example/mastodon.json +0 -0
  52. {thth-3.1.0 → thth-3.1.2}/accounts.example/threads.json +0 -0
  53. {thth-3.1.0 → thth-3.1.2}/accounts.example/x.json +0 -0
  54. {thth-3.1.0 → thth-3.1.2}/bin/thth +0 -0
  55. {thth-3.1.0 → thth-3.1.2}/bin/thth-remote +0 -0
  56. {thth-3.1.0 → thth-3.1.2}/bin/thth-run +0 -0
  57. {thth-3.1.0 → thth-3.1.2}/pyproject.toml +0 -0
  58. {thth-3.1.0 → thth-3.1.2}/systemd/thth@.service +0 -0
  59. {thth-3.1.0 → thth-3.1.2}/tests/__init__.py +0 -0
  60. {thth-3.1.0 → thth-3.1.2}/tests/conftest.py +0 -0
  61. {thth-3.1.0 → thth-3.1.2}/tests/coordination_snapshot.py +0 -0
  62. {thth-3.1.0 → thth-3.1.2}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
  63. {thth-3.1.0 → thth-3.1.2}/tests/fixtures/build_injection_eval.py +0 -0
  64. {thth-3.1.0 → thth-3.1.2}/tests/fixtures/callback/worker_responses.json +0 -0
  65. {thth-3.1.0 → thth-3.1.2}/tests/fixtures/injection_eval.json +0 -0
  66. {thth-3.1.0 → thth-3.1.2}/tests/fixtures/umami-bile.md +0 -0
  67. {thth-3.1.0 → thth-3.1.2}/tests/helpers/__init__.py +0 -0
  68. {thth-3.1.0 → thth-3.1.2}/tests/helpers/fake_oauth_server.py +0 -0
  69. {thth-3.1.0 → thth-3.1.2}/tests/helpers/fake_push_adapter.py +0 -0
  70. {thth-3.1.0 → thth-3.1.2}/tests/helpers/fake_redirect_server.py +0 -0
  71. {thth-3.1.0 → thth-3.1.2}/tests/helpers/fake_search_adapter.py +0 -0
  72. {thth-3.1.0 → thth-3.1.2}/tests/helpers/fake_threads_reader.py +0 -0
  73. {thth-3.1.0 → thth-3.1.2}/tests/helpers/fake_threads_writes.py +0 -0
  74. {thth-3.1.0 → thth-3.1.2}/tests/helpers/fake_x.py +0 -0
  75. {thth-3.1.0 → thth-3.1.2}/tests/helpers/hold_lock.py +0 -0
  76. {thth-3.1.0 → thth-3.1.2}/tests/helpers/hold_repo_lock.py +0 -0
  77. {thth-3.1.0 → thth-3.1.2}/tests/helpers/worker_probe.mjs +0 -0
  78. {thth-3.1.0 → thth-3.1.2}/tests/test_27_acceptance_fixes.py +0 -0
  79. {thth-3.1.0 → thth-3.1.2}/tests/test_account_name_check.py +0 -0
  80. {thth-3.1.0 → thth-3.1.2}/tests/test_account_report.py +0 -0
  81. {thth-3.1.0 → thth-3.1.2}/tests/test_accounts_dir.py +0 -0
  82. {thth-3.1.0 → thth-3.1.2}/tests/test_adapter_boundary.py +0 -0
  83. {thth-3.1.0 → thth-3.1.2}/tests/test_admin_log.py +0 -0
  84. {thth-3.1.0 → thth-3.1.2}/tests/test_admin_notifications.py +0 -0
  85. {thth-3.1.0 → thth-3.1.2}/tests/test_admin_repairs.py +0 -0
  86. {thth-3.1.0 → thth-3.1.2}/tests/test_admin_report.py +0 -0
  87. {thth-3.1.0 → thth-3.1.2}/tests/test_after_cli.py +0 -0
  88. {thth-3.1.0 → thth-3.1.2}/tests/test_analytics_comparison.py +0 -0
  89. {thth-3.1.0 → thth-3.1.2}/tests/test_analytics_report.py +0 -0
  90. {thth-3.1.0 → thth-3.1.2}/tests/test_api_diagnostic.py +0 -0
  91. {thth-3.1.0 → thth-3.1.2}/tests/test_api_error_body.py +0 -0
  92. {thth-3.1.0 → thth-3.1.2}/tests/test_app_env_cli.py +0 -0
  93. {thth-3.1.0 → thth-3.1.2}/tests/test_approval.py +0 -0
  94. {thth-3.1.0 → thth-3.1.2}/tests/test_approve_cmd.py +0 -0
  95. {thth-3.1.0 → thth-3.1.2}/tests/test_atlas_fifth_review.py +0 -0
  96. {thth-3.1.0 → thth-3.1.2}/tests/test_atlas_fourth_review.py +0 -0
  97. {thth-3.1.0 → thth-3.1.2}/tests/test_atlas_review.py +0 -0
  98. {thth-3.1.0 → thth-3.1.2}/tests/test_atlas_second_review.py +0 -0
  99. {thth-3.1.0 → thth-3.1.2}/tests/test_atlas_third_review.py +0 -0
  100. {thth-3.1.0 → thth-3.1.2}/tests/test_audit1_fixes.py +0 -0
  101. {thth-3.1.0 → thth-3.1.2}/tests/test_audit2_fixes.py +0 -0
  102. {thth-3.1.0 → thth-3.1.2}/tests/test_audit_b_counterexamples.py +0 -0
  103. {thth-3.1.0 → thth-3.1.2}/tests/test_audit_b_m1_semantics.py +0 -0
  104. {thth-3.1.0 → thth-3.1.2}/tests/test_audit_b_n1_depth.py +0 -0
  105. {thth-3.1.0 → thth-3.1.2}/tests/test_audit_b_r1_closure.py +0 -0
  106. {thth-3.1.0 → thth-3.1.2}/tests/test_audit_v201.py +0 -0
  107. {thth-3.1.0 → thth-3.1.2}/tests/test_audit_v23_details.py +0 -0
  108. {thth-3.1.0 → thth-3.1.2}/tests/test_auth.py +0 -0
  109. {thth-3.1.0 → thth-3.1.2}/tests/test_auth_identity_required.py +0 -0
  110. {thth-3.1.0 → thth-3.1.2}/tests/test_behind_remote.py +0 -0
  111. {thth-3.1.0 → thth-3.1.2}/tests/test_bluesky_adapter.py +0 -0
  112. {thth-3.1.0 → thth-3.1.2}/tests/test_bluesky_secret_scrub.py +0 -0
  113. {thth-3.1.0 → thth-3.1.2}/tests/test_board_needs_review.py +0 -0
  114. {thth-3.1.0 → thth-3.1.2}/tests/test_board_running.py +0 -0
  115. {thth-3.1.0 → thth-3.1.2}/tests/test_bundle.py +0 -0
  116. {thth-3.1.0 → thth-3.1.2}/tests/test_charcount.py +0 -0
  117. {thth-3.1.0 → thth-3.1.2}/tests/test_cli_entrypoint.py +0 -0
  118. {thth-3.1.0 → thth-3.1.2}/tests/test_cli_help.py +0 -0
  119. {thth-3.1.0 → thth-3.1.2}/tests/test_cli_vm_path.py +0 -0
  120. {thth-3.1.0 → thth-3.1.2}/tests/test_collect.py +0 -0
  121. {thth-3.1.0 → thth-3.1.2}/tests/test_collect_engagements.py +0 -0
  122. {thth-3.1.0 → thth-3.1.2}/tests/test_collect_runs.py +0 -0
  123. {thth-3.1.0 → thth-3.1.2}/tests/test_collect_sent.py +0 -0
  124. {thth-3.1.0 → thth-3.1.2}/tests/test_comparability.py +0 -0
  125. {thth-3.1.0 → thth-3.1.2}/tests/test_control_chars_refused.py +0 -0
  126. {thth-3.1.0 → thth-3.1.2}/tests/test_conversation_paging.py +0 -0
  127. {thth-3.1.0 → thth-3.1.2}/tests/test_count_for.py +0 -0
  128. {thth-3.1.0 → thth-3.1.2}/tests/test_docs_links.py +0 -0
  129. {thth-3.1.0 → thth-3.1.2}/tests/test_doctor.py +0 -0
  130. {thth-3.1.0 → thth-3.1.2}/tests/test_doctor_all_permissions.py +0 -0
  131. {thth-3.1.0 → thth-3.1.2}/tests/test_doctor_next_step.py +0 -0
  132. {thth-3.1.0 → thth-3.1.2}/tests/test_duplicate_front_matter_keys.py +0 -0
  133. {thth-3.1.0 → thth-3.1.2}/tests/test_editorial_audit.py +0 -0
  134. {thth-3.1.0 → thth-3.1.2}/tests/test_editorial_n3_n4.py +0 -0
  135. {thth-3.1.0 → thth-3.1.2}/tests/test_engagements.py +0 -0
  136. {thth-3.1.0 → thth-3.1.2}/tests/test_external_acceptance.py +0 -0
  137. {thth-3.1.0 → thth-3.1.2}/tests/test_fake_api.py +0 -0
  138. {thth-3.1.0 → thth-3.1.2}/tests/test_form_spec.py +0 -0
  139. {thth-3.1.0 → thth-3.1.2}/tests/test_form_spec_cli.py +0 -0
  140. {thth-3.1.0 → thth-3.1.2}/tests/test_forms.py +0 -0
  141. {thth-3.1.0 → thth-3.1.2}/tests/test_fresh_install.py +0 -0
  142. {thth-3.1.0 → thth-3.1.2}/tests/test_front_matter_injection.py +0 -0
  143. {thth-3.1.0 → thth-3.1.2}/tests/test_healthcheck_notifications.py +0 -0
  144. {thth-3.1.0 → thth-3.1.2}/tests/test_hypotheses.py +0 -0
  145. {thth-3.1.0 → thth-3.1.2}/tests/test_hypothesis_boundaries.py +0 -0
  146. {thth-3.1.0 → thth-3.1.2}/tests/test_hypothesis_units.py +0 -0
  147. {thth-3.1.0 → thth-3.1.2}/tests/test_impact_cli.py +0 -0
  148. {thth-3.1.0 → thth-3.1.2}/tests/test_inbox_granted_scopes.py +0 -0
  149. {thth-3.1.0 → thth-3.1.2}/tests/test_incident_notifications.py +0 -0
  150. {thth-3.1.0 → thth-3.1.2}/tests/test_independent_thread_review.py +0 -0
  151. {thth-3.1.0 → thth-3.1.2}/tests/test_independent_topic_review.py +0 -0
  152. {thth-3.1.0 → thth-3.1.2}/tests/test_independent_topic_review_r2.py +0 -0
  153. {thth-3.1.0 → thth-3.1.2}/tests/test_inflight.py +0 -0
  154. {thth-3.1.0 → thth-3.1.2}/tests/test_inflight_tag_body_hash.py +0 -0
  155. {thth-3.1.0 → thth-3.1.2}/tests/test_jst.py +0 -0
  156. {thth-3.1.0 → thth-3.1.2}/tests/test_lint.py +0 -0
  157. {thth-3.1.0 → thth-3.1.2}/tests/test_list_projections.py +0 -0
  158. {thth-3.1.0 → thth-3.1.2}/tests/test_llm_trial.py +0 -0
  159. {thth-3.1.0 → thth-3.1.2}/tests/test_llm_trial_engage.py +0 -0
  160. {thth-3.1.0 → thth-3.1.2}/tests/test_lock.py +0 -0
  161. {thth-3.1.0 → thth-3.1.2}/tests/test_m2_independent.py +0 -0
  162. {thth-3.1.0 → thth-3.1.2}/tests/test_maintain.py +0 -0
  163. {thth-3.1.0 → thth-3.1.2}/tests/test_mastodon_adapter.py +0 -0
  164. {thth-3.1.0 → thth-3.1.2}/tests/test_mastodon_visibility.py +0 -0
  165. {thth-3.1.0 → thth-3.1.2}/tests/test_max_per_run.py +0 -0
  166. {thth-3.1.0 → thth-3.1.2}/tests/test_mcp.py +0 -0
  167. {thth-3.1.0 → thth-3.1.2}/tests/test_mcp_robustness.py +0 -0
  168. {thth-3.1.0 → thth-3.1.2}/tests/test_measured.py +0 -0
  169. {thth-3.1.0 → thth-3.1.2}/tests/test_media_accounts.py +0 -0
  170. {thth-3.1.0 → thth-3.1.2}/tests/test_media_next_steps.py +0 -0
  171. {thth-3.1.0 → thth-3.1.2}/tests/test_media_roundtrip.py +0 -0
  172. {thth-3.1.0 → thth-3.1.2}/tests/test_mismatch_diagnostics.py +0 -0
  173. {thth-3.1.0 → thth-3.1.2}/tests/test_n5_reply_null_element.py +0 -0
  174. {thth-3.1.0 → thth-3.1.2}/tests/test_n6_replies_incomplete_ledger.py +0 -0
  175. {thth-3.1.0 → thth-3.1.2}/tests/test_n7_doctor_error_body.py +0 -0
  176. {thth-3.1.0 → thth-3.1.2}/tests/test_no_limits.py +0 -0
  177. {thth-3.1.0 → thth-3.1.2}/tests/test_observation_provenance.py +0 -0
  178. {thth-3.1.0 → thth-3.1.2}/tests/test_operations_handoff.py +0 -0
  179. {thth-3.1.0 → thth-3.1.2}/tests/test_packaging.py +0 -0
  180. {thth-3.1.0 → thth-3.1.2}/tests/test_preview.py +0 -0
  181. {thth-3.1.0 → thth-3.1.2}/tests/test_production_mode.py +0 -0
  182. {thth-3.1.0 → thth-3.1.2}/tests/test_pull_cli.py +0 -0
  183. {thth-3.1.0 → thth-3.1.2}/tests/test_queue.py +0 -0
  184. {thth-3.1.0 → thth-3.1.2}/tests/test_quiet_hours.py +0 -0
  185. {thth-3.1.0 → thth-3.1.2}/tests/test_r2_r4_r5.py +0 -0
  186. {thth-3.1.0 → thth-3.1.2}/tests/test_read_cli_bare_runtimeerror.py +0 -0
  187. {thth-3.1.0 → thth-3.1.2}/tests/test_reason_adoption.py +0 -0
  188. {thth-3.1.0 → thth-3.1.2}/tests/test_redact.py +0 -0
  189. {thth-3.1.0 → thth-3.1.2}/tests/test_redirect_token_leak.py +0 -0
  190. {thth-3.1.0 → thth-3.1.2}/tests/test_refresh.py +0 -0
  191. {thth-3.1.0 → thth-3.1.2}/tests/test_refresh_destination.py +0 -0
  192. {thth-3.1.0 → thth-3.1.2}/tests/test_rejudge_new.py +0 -0
  193. {thth-3.1.0 → thth-3.1.2}/tests/test_rejudge_r2_r3.py +0 -0
  194. {thth-3.1.0 → thth-3.1.2}/tests/test_replies_refresh.py +0 -0
  195. {thth-3.1.0 → thth-3.1.2}/tests/test_repo_lock.py +0 -0
  196. {thth-3.1.0 → thth-3.1.2}/tests/test_repo_sync.py +0 -0
  197. {thth-3.1.0 → thth-3.1.2}/tests/test_report_checkout_import.py +0 -0
  198. {thth-3.1.0 → thth-3.1.2}/tests/test_report_http.py +0 -0
  199. {thth-3.1.0 → thth-3.1.2}/tests/test_report_isolation.py +0 -0
  200. {thth-3.1.0 → thth-3.1.2}/tests/test_report_service.py +0 -0
  201. {thth-3.1.0 → thth-3.1.2}/tests/test_reverse_audit.py +0 -0
  202. {thth-3.1.0 → thth-3.1.2}/tests/test_review_case_id_cluster_m2.py +0 -0
  203. {thth-3.1.0 → thth-3.1.2}/tests/test_review_case_id_cluster_r2.py +0 -0
  204. {thth-3.1.0 → thth-3.1.2}/tests/test_review_cli.py +0 -0
  205. {thth-3.1.0 → thth-3.1.2}/tests/test_review_evidence_account.py +0 -0
  206. {thth-3.1.0 → thth-3.1.2}/tests/test_review_form_spec_and_case_id.py +0 -0
  207. {thth-3.1.0 → thth-3.1.2}/tests/test_review_provenance.py +0 -0
  208. {thth-3.1.0 → thth-3.1.2}/tests/test_review_records.py +0 -0
  209. {thth-3.1.0 → thth-3.1.2}/tests/test_revoke.py +0 -0
  210. {thth-3.1.0 → thth-3.1.2}/tests/test_roundtrip.py +0 -0
  211. {thth-3.1.0 → thth-3.1.2}/tests/test_run_missing_env.py +0 -0
  212. {thth-3.1.0 → thth-3.1.2}/tests/test_run_record_failclosed.py +0 -0
  213. {thth-3.1.0 → thth-3.1.2}/tests/test_runs_collected.py +0 -0
  214. {thth-3.1.0 → thth-3.1.2}/tests/test_runs_read_rows.py +0 -0
  215. {thth-3.1.0 → thth-3.1.2}/tests/test_schedule_multiple.py +0 -0
  216. {thth-3.1.0 → thth-3.1.2}/tests/test_schedule_view.py +0 -0
  217. {thth-3.1.0 → thth-3.1.2}/tests/test_scopes_source_requested.py +0 -0
  218. {thth-3.1.0 → thth-3.1.2}/tests/test_search_engage_targets.py +0 -0
  219. {thth-3.1.0 → thth-3.1.2}/tests/test_search_media_routes.py +0 -0
  220. {thth-3.1.0 → thth-3.1.2}/tests/test_secret_value_redact.py +0 -0
  221. {thth-3.1.0 → thth-3.1.2}/tests/test_secrets_grep.py +0 -0
  222. {thth-3.1.0 → thth-3.1.2}/tests/test_security_small_fixes.py +0 -0
  223. {thth-3.1.0 → thth-3.1.2}/tests/test_select.py +0 -0
  224. {thth-3.1.0 → thth-3.1.2}/tests/test_selfupdate.py +0 -0
  225. {thth-3.1.0 → thth-3.1.2}/tests/test_send.py +0 -0
  226. {thth-3.1.0 → thth-3.1.2}/tests/test_send_cli.py +0 -0
  227. {thth-3.1.0 → thth-3.1.2}/tests/test_send_control_chars.py +0 -0
  228. {thth-3.1.0 → thth-3.1.2}/tests/test_send_media.py +0 -0
  229. {thth-3.1.0 → thth-3.1.2}/tests/test_send_visibility.py +0 -0
  230. {thth-3.1.0 → thth-3.1.2}/tests/test_sent_integrity.py +0 -0
  231. {thth-3.1.0 → thth-3.1.2}/tests/test_server_json.py +0 -0
  232. {thth-3.1.0 → thth-3.1.2}/tests/test_site.py +0 -0
  233. {thth-3.1.0 → thth-3.1.2}/tests/test_skill_doc.py +0 -0
  234. {thth-3.1.0 → thth-3.1.2}/tests/test_study_report.py +0 -0
  235. {thth-3.1.0 → thth-3.1.2}/tests/test_systemd.py +0 -0
  236. {thth-3.1.0 → thth-3.1.2}/tests/test_thread_publish.py +0 -0
  237. {thth-3.1.0 → thth-3.1.2}/tests/test_thread_read.py +0 -0
  238. {thth-3.1.0 → thth-3.1.2}/tests/test_threadrun.py +0 -0
  239. {thth-3.1.0 → thth-3.1.2}/tests/test_threads_read_permissions.py +0 -0
  240. {thth-3.1.0 → thth-3.1.2}/tests/test_threadshape.py +0 -0
  241. {thth-3.1.0 → thth-3.1.2}/tests/test_thth_root.py +0 -0
  242. {thth-3.1.0 → thth-3.1.2}/tests/test_token_exists.py +0 -0
  243. {thth-3.1.0 → thth-3.1.2}/tests/test_token_scopes_record.py +0 -0
  244. {thth-3.1.0 → thth-3.1.2}/tests/test_token_set.py +0 -0
  245. {thth-3.1.0 → thth-3.1.2}/tests/test_topic.py +0 -0
  246. {thth-3.1.0 → thth-3.1.2}/tests/test_topic_advice.py +0 -0
  247. {thth-3.1.0 → thth-3.1.2}/tests/test_topic_cli.py +0 -0
  248. {thth-3.1.0 → thth-3.1.2}/tests/test_topic_models.py +0 -0
  249. {thth-3.1.0 → thth-3.1.2}/tests/test_topic_review_fixes.py +0 -0
  250. {thth-3.1.0 → thth-3.1.2}/tests/test_topic_shelf_observers.py +0 -0
  251. {thth-3.1.0 → thth-3.1.2}/tests/test_topic_store.py +0 -0
  252. {thth-3.1.0 → thth-3.1.2}/tests/test_topics_record_concurrent.py +0 -0
  253. {thth-3.1.0 → thth-3.1.2}/tests/test_topics_tool.py +0 -0
  254. {thth-3.1.0 → thth-3.1.2}/tests/test_trial_frictions.py +0 -0
  255. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_admin_transaction.py +0 -0
  256. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_approve_account.py +0 -0
  257. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_length.py +0 -0
  258. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_lock_wait.py +0 -0
  259. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_mastodon_scopes.py +0 -0
  260. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_mentions.py +0 -0
  261. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_post_urls.py +0 -0
  262. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_repo_paths.py +0 -0
  263. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_reposts.py +0 -0
  264. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_study_add.py +0 -0
  265. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_tool_version.py +0 -0
  266. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_unanswered.py +0 -0
  267. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_unanswered_scope.py +0 -0
  268. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_where_arguments.py +0 -0
  269. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_where_filters.py +0 -0
  270. {thth-3.1.0 → thth-3.1.2}/tests/test_v210_who_arguments.py +0 -0
  271. {thth-3.1.0 → thth-3.1.2}/tests/test_v211_auth_integration.py +0 -0
  272. {thth-3.1.0 → thth-3.1.2}/tests/test_v211_authflow.py +0 -0
  273. {thth-3.1.0 → thth-3.1.2}/tests/test_v211_bluesky_stdin.py +0 -0
  274. {thth-3.1.0 → thth-3.1.2}/tests/test_v211_doctor_binding.py +0 -0
  275. {thth-3.1.0 → thth-3.1.2}/tests/test_v211_mastodon_auth.py +0 -0
  276. {thth-3.1.0 → thth-3.1.2}/tests/test_v211_x_auth.py +0 -0
  277. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_approval_relay.py +0 -0
  278. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_ask_coordination.py +0 -0
  279. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_budget.py +0 -0
  280. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_budget_auth.py +0 -0
  281. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_budget_mcp.py +0 -0
  282. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_deletion.py +0 -0
  283. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_job_completion_fault.py +0 -0
  284. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_leave_gate.py +0 -0
  285. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_leave_token_safety.py +0 -0
  286. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_mastodon_guide.py +0 -0
  287. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_mcp_compat.py +0 -0
  288. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_missing_token.py +0 -0
  289. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_server_writes.py +0 -0
  290. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_token_parent.py +0 -0
  291. {thth-3.1.0 → thth-3.1.2}/tests/test_v212_write_storage.py +0 -0
  292. {thth-3.1.0 → thth-3.1.2}/tests/test_v2131_send_dry_run.py +0 -0
  293. {thth-3.1.0 → thth-3.1.2}/tests/test_v2131_threads_publish_wait.py +0 -0
  294. {thth-3.1.0 → thth-3.1.2}/tests/test_v2132_send_inflight_hint.py +0 -0
  295. {thth-3.1.0 → thth-3.1.2}/tests/test_v2132_threads_create_timeout.py +0 -0
  296. {thth-3.1.0 → thth-3.1.2}/tests/test_v2132_threads_create_unknown.py +0 -0
  297. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_acceptance_fixes.py +0 -0
  298. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_adts_asf.py +0 -0
  299. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_approval_attachments.py +0 -0
  300. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_attachment_kinds.py +0 -0
  301. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_bluesky_external.py +0 -0
  302. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_bluesky_media.py +0 -0
  303. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_bluesky_metadata.py +0 -0
  304. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_bluesky_metadata_uri.py +0 -0
  305. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_bluesky_metadata_video.py +0 -0
  306. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_bluesky_record.py +0 -0
  307. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_bluesky_record_video.py +0 -0
  308. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_bluesky_video.py +0 -0
  309. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_bluesky_webm_named.py +0 -0
  310. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_bmff_typed_metadata.py +0 -0
  311. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_budget_lock.py +0 -0
  312. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_capabilities.py +0 -0
  313. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_graphemes.py +0 -0
  314. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_auth_media.py +0 -0
  315. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_bmff_audio.py +0 -0
  316. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_flac.py +0 -0
  317. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_gif.py +0 -0
  318. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_link.py +0 -0
  319. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_media.py +0 -0
  320. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_mp3.py +0 -0
  321. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_ogg_opus.py +0 -0
  322. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
  323. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_poll.py +0 -0
  324. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
  325. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_quote.py +0 -0
  326. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_thumbnail.py +0 -0
  327. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_wav.py +0 -0
  328. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_webm_audio.py +0 -0
  329. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_mastodon_webm_video.py +0 -0
  330. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_media_cleanup.py +0 -0
  331. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_media_formats.py +0 -0
  332. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_media_foundation.py +0 -0
  333. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_media_prepared.py +0 -0
  334. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_media_relay.py +0 -0
  335. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_media_relay_refusals.py +0 -0
  336. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_media_upload.py +0 -0
  337. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_receipt_deadline.py +0 -0
  338. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_stage810_audit.py +0 -0
  339. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_static_diagnostics.py +0 -0
  340. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_systemd_worker.py +0 -0
  341. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_threads_ghost.py +0 -0
  342. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_threads_gif.py +0 -0
  343. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_threads_link.py +0 -0
  344. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_threads_media.py +0 -0
  345. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_threads_media_spoiler.py +0 -0
  346. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_threads_poll.py +0 -0
  347. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_threads_quote.py +0 -0
  348. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_threads_quote_media.py +0 -0
  349. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_threads_replies.py +0 -0
  350. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_threads_text.py +0 -0
  351. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_threads_text_spoiler.py +0 -0
  352. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_threads_video.py +0 -0
  353. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_transport_review.py +0 -0
  354. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_video_typed_metadata.py +0 -0
  355. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_vorbis_comment_boundary.py +0 -0
  356. {thth-3.1.0 → thth-3.1.2}/tests/test_v213_webm_schema_boundary.py +0 -0
  357. {thth-3.1.0 → thth-3.1.2}/tests/test_v2141_draft_lint_reason.py +0 -0
  358. {thth-3.1.0 → thth-3.1.2}/tests/test_v2141_managed_repo_umask.py +0 -0
  359. {thth-3.1.0 → thth-3.1.2}/tests/test_v214_release_signing.py +0 -0
  360. {thth-3.1.0 → thth-3.1.2}/tests/test_v214_x_adapter.py +0 -0
  361. {thth-3.1.0 → thth-3.1.2}/tests/test_v214_x_auth.py +0 -0
  362. {thth-3.1.0 → thth-3.1.2}/tests/test_v214_x_media.py +0 -0
  363. {thth-3.1.0 → thth-3.1.2}/tests/test_v214_x_posts_budget.py +0 -0
  364. {thth-3.1.0 → thth-3.1.2}/tests/test_v28_tags.py +0 -0
  365. {thth-3.1.0 → thth-3.1.2}/tests/test_v310_morning.py +0 -0
  366. {thth-3.1.0 → thth-3.1.2}/tests/test_v310_search_timeout.py +0 -0
  367. {thth-3.1.0 → thth-3.1.2}/tests/test_v310_watch_words.py +0 -0
  368. {thth-3.1.0 → thth-3.1.2}/tests/test_v31_analysis.py +0 -0
  369. {thth-3.1.0 → thth-3.1.2}/tests/test_v31_collection.py +0 -0
  370. {thth-3.1.0 → thth-3.1.2}/tests/test_value_domains_shown.py +0 -0
  371. {thth-3.1.0 → thth-3.1.2}/tests/test_version.py +0 -0
  372. {thth-3.1.0 → thth-3.1.2}/tests/test_where_cli.py +0 -0
  373. {thth-3.1.0 → thth-3.1.2}/tests/test_who_cli.py +0 -0
  374. {thth-3.1.0 → thth-3.1.2}/tests/test_writeback_rebase.py +0 -0
  375. {thth-3.1.0 → thth-3.1.2}/tests/test_writeback_validate.py +0 -0
  376. {thth-3.1.0 → thth-3.1.2}/tests/test_x_metrics.py +0 -0
  377. {thth-3.1.0 → thth-3.1.2}/thth/__init__.py +0 -0
  378. {thth-3.1.0 → thth-3.1.2}/thth/__main__.py +0 -0
  379. {thth-3.1.0 → thth-3.1.2}/thth/_grapheme_data.py +0 -0
  380. {thth-3.1.0 → thth-3.1.2}/thth/account_cli.py +0 -0
  381. {thth-3.1.0 → thth-3.1.2}/thth/account_report.py +0 -0
  382. {thth-3.1.0 → thth-3.1.2}/thth/accounts.py +0 -0
  383. {thth-3.1.0 → thth-3.1.2}/thth/adapters/__init__.py +0 -0
  384. {thth-3.1.0 → thth-3.1.2}/thth/adapters/auth_mastodon.py +0 -0
  385. {thth-3.1.0 → thth-3.1.2}/thth/adapters/auth_threads.py +0 -0
  386. {thth-3.1.0 → thth-3.1.2}/thth/adapters/auth_x.py +0 -0
  387. {thth-3.1.0 → thth-3.1.2}/thth/adapters/base.py +0 -0
  388. {thth-3.1.0 → thth-3.1.2}/thth/adapters/bluesky.py +0 -0
  389. {thth-3.1.0 → thth-3.1.2}/thth/adapters/bluesky_media.py +0 -0
  390. {thth-3.1.0 → thth-3.1.2}/thth/adapters/bluesky_video.py +0 -0
  391. {thth-3.1.0 → thth-3.1.2}/thth/adapters/mastodon_media.py +0 -0
  392. {thth-3.1.0 → thth-3.1.2}/thth/adapters/threads.py +0 -0
  393. {thth-3.1.0 → thth-3.1.2}/thth/adapters/threads_media.py +0 -0
  394. {thth-3.1.0 → thth-3.1.2}/thth/adapters/x.py +0 -0
  395. {thth-3.1.0 → thth-3.1.2}/thth/adapters/x_media.py +0 -0
  396. {thth-3.1.0 → thth-3.1.2}/thth/admin_notifications.py +0 -0
  397. {thth-3.1.0 → thth-3.1.2}/thth/after_cli.py +0 -0
  398. {thth-3.1.0 → thth-3.1.2}/thth/analytics_comparison.py +0 -0
  399. {thth-3.1.0 → thth-3.1.2}/thth/analytics_report.py +0 -0
  400. {thth-3.1.0 → thth-3.1.2}/thth/analytics_shapes.py +0 -0
  401. {thth-3.1.0 → thth-3.1.2}/thth/analytics_threads.py +0 -0
  402. {thth-3.1.0 → thth-3.1.2}/thth/api_diagnostic.py +0 -0
  403. {thth-3.1.0 → thth-3.1.2}/thth/appconfig.py +0 -0
  404. {thth-3.1.0 → thth-3.1.2}/thth/appenv.py +0 -0
  405. {thth-3.1.0 → thth-3.1.2}/thth/approval.py +0 -0
  406. {thth-3.1.0 → thth-3.1.2}/thth/approval_jobs.py +0 -0
  407. {thth-3.1.0 → thth-3.1.2}/thth/approval_relay.py +0 -0
  408. {thth-3.1.0 → thth-3.1.2}/thth/ask.py +0 -0
  409. {thth-3.1.0 → thth-3.1.2}/thth/ask_cli.py +0 -0
  410. {thth-3.1.0 → thth-3.1.2}/thth/audioformats.py +0 -0
  411. {thth-3.1.0 → thth-3.1.2}/thth/authclients.py +0 -0
  412. {thth-3.1.0 → thth-3.1.2}/thth/authflow.py +0 -0
  413. {thth-3.1.0 → thth-3.1.2}/thth/authpassword.py +0 -0
  414. {thth-3.1.0 → thth-3.1.2}/thth/bluesky_metadata.py +0 -0
  415. {thth-3.1.0 → thth-3.1.2}/thth/budget_x.py +0 -0
  416. {thth-3.1.0 → thth-3.1.2}/thth/budget_x_posts.py +0 -0
  417. {thth-3.1.0 → thth-3.1.2}/thth/bundle.py +0 -0
  418. {thth-3.1.0 → thth-3.1.2}/thth/collection_context.py +0 -0
  419. {thth-3.1.0 → thth-3.1.2}/thth/collection_status.py +0 -0
  420. {thth-3.1.0 → thth-3.1.2}/thth/core.py +0 -0
  421. {thth-3.1.0 → thth-3.1.2}/thth/deletion.py +0 -0
  422. {thth-3.1.0 → thth-3.1.2}/thth/doctor.py +0 -0
  423. {thth-3.1.0 → thth-3.1.2}/thth/ebmlaudio.py +0 -0
  424. {thth-3.1.0 → thth-3.1.2}/thth/ebmlvideo.py +0 -0
  425. {thth-3.1.0 → thth-3.1.2}/thth/engagements.py +0 -0
  426. {thth-3.1.0 → thth-3.1.2}/thth/flacformats.py +0 -0
  427. {thth-3.1.0 → thth-3.1.2}/thth/forms.py +0 -0
  428. {thth-3.1.0 → thth-3.1.2}/thth/graphemes.py +0 -0
  429. {thth-3.1.0 → thth-3.1.2}/thth/handoff_cursor.py +0 -0
  430. {thth-3.1.0 → thth-3.1.2}/thth/healthcheck.py +0 -0
  431. {thth-3.1.0 → thth-3.1.2}/thth/httpsafe.py +0 -0
  432. {thth-3.1.0 → thth-3.1.2}/thth/inflight.py +0 -0
  433. {thth-3.1.0 → thth-3.1.2}/thth/jst.py +0 -0
  434. {thth-3.1.0 → thth-3.1.2}/thth/leave_gate.py +0 -0
  435. {thth-3.1.0 → thth-3.1.2}/thth/lint.py +0 -0
  436. {thth-3.1.0 → thth-3.1.2}/thth/lock.py +0 -0
  437. {thth-3.1.0 → thth-3.1.2}/thth/maintain.py +0 -0
  438. {thth-3.1.0 → thth-3.1.2}/thth/managed_repo.py +0 -0
  439. {thth-3.1.0 → thth-3.1.2}/thth/measured.py +0 -0
  440. {thth-3.1.0 → thth-3.1.2}/thth/media.py +0 -0
  441. {thth-3.1.0 → thth-3.1.2}/thth/media_capabilities.py +0 -0
  442. {thth-3.1.0 → thth-3.1.2}/thth/media_cleanup.py +0 -0
  443. {thth-3.1.0 → thth-3.1.2}/thth/media_delivery.py +0 -0
  444. {thth-3.1.0 → thth-3.1.2}/thth/media_relay.py +0 -0
  445. {thth-3.1.0 → thth-3.1.2}/thth/media_uploads.py +0 -0
  446. {thth-3.1.0 → thth-3.1.2}/thth/mediaformats.py +0 -0
  447. {thth-3.1.0 → thth-3.1.2}/thth/mpeg_audio.py +0 -0
  448. {thth-3.1.0 → thth-3.1.2}/thth/oauth.py +0 -0
  449. {thth-3.1.0 → thth-3.1.2}/thth/oggformats.py +0 -0
  450. {thth-3.1.0 → thth-3.1.2}/thth/postid.py +0 -0
  451. {thth-3.1.0 → thth-3.1.2}/thth/queuefile.py +0 -0
  452. {thth-3.1.0 → thth-3.1.2}/thth/read_coordination.py +0 -0
  453. {thth-3.1.0 → thth-3.1.2}/thth/read_window.py +0 -0
  454. {thth-3.1.0 → thth-3.1.2}/thth/report.py +0 -0
  455. {thth-3.1.0 → thth-3.1.2}/thth/report_details.py +0 -0
  456. {thth-3.1.0 → thth-3.1.2}/thth/report_http.py +0 -0
  457. {thth-3.1.0 → thth-3.1.2}/thth/runs.py +0 -0
  458. {thth-3.1.0 → thth-3.1.2}/thth/scopes.py +0 -0
  459. {thth-3.1.0 → thth-3.1.2}/thth/secrets_fs.py +0 -0
  460. {thth-3.1.0 → thth-3.1.2}/thth/select.py +0 -0
  461. {thth-3.1.0 → thth-3.1.2}/thth/selfupdate.py +0 -0
  462. {thth-3.1.0 → thth-3.1.2}/thth/sent.py +0 -0
  463. {thth-3.1.0 → thth-3.1.2}/thth/server_files.py +0 -0
  464. {thth-3.1.0 → thth-3.1.2}/thth/server_writes.py +0 -0
  465. {thth-3.1.0 → thth-3.1.2}/thth/stop_observation.py +0 -0
  466. {thth-3.1.0 → thth-3.1.2}/thth/study_cli.py +0 -0
  467. {thth-3.1.0 → thth-3.1.2}/thth/study_report.py +0 -0
  468. {thth-3.1.0 → thth-3.1.2}/thth/systemd_gen.py +0 -0
  469. {thth-3.1.0 → thth-3.1.2}/thth/tags.py +0 -0
  470. {thth-3.1.0 → thth-3.1.2}/thth/thread_read.py +0 -0
  471. {thth-3.1.0 → thth-3.1.2}/thth/threadrun.py +0 -0
  472. {thth-3.1.0 → thth-3.1.2}/thth/threads_read_cli.py +0 -0
  473. {thth-3.1.0 → thth-3.1.2}/thth/threadshape.py +0 -0
  474. {thth-3.1.0 → thth-3.1.2}/thth/threadthrow.py +0 -0
  475. {thth-3.1.0 → thth-3.1.2}/thth/tool_version.py +0 -0
  476. {thth-3.1.0 → thth-3.1.2}/thth/topic_advice.py +0 -0
  477. {thth-3.1.0 → thth-3.1.2}/thth/topic_cli.py +0 -0
  478. {thth-3.1.0 → thth-3.1.2}/thth/topic_models.py +0 -0
  479. {thth-3.1.0 → thth-3.1.2}/thth/topic_store.py +0 -0
  480. {thth-3.1.0 → thth-3.1.2}/thth/topics.py +0 -0
  481. {thth-3.1.0 → thth-3.1.2}/thth/vorbisformats.py +0 -0
  482. {thth-3.1.0 → thth-3.1.2}/thth/watch_cli.py +0 -0
  483. {thth-3.1.0 → thth-3.1.2}/thth/where_cli.py +0 -0
  484. {thth-3.1.0 → thth-3.1.2}/thth/who_cli.py +0 -0
  485. {thth-3.1.0 → thth-3.1.2}/thth/writeback.py +0 -0
  486. {thth-3.1.0 → thth-3.1.2}/thth/x_metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thth
3
- Version: 3.1.0
3
+ Version: 3.1.2
4
4
  Summary: 承認済みの投稿だけを投げる門(Threads・Bluesky・Mastodon)。人が承認し、記録は利用者の git に残る。
5
5
  Project-URL: Homepage, https://thth.me
6
6
  Project-URL: Source, https://github.com/aokings/thth
@@ -125,7 +125,7 @@ timer(systemd・`thth systemd` で生成)は10分ごとに実行し、投稿
125
125
  - **英語の文書**: [README.en.md](README.en.md)・[docs/usage.en.md](docs/usage.en.md)・[llms.txt](llms.txt)。
126
126
  - **skill**: `skills/thth/SKILL.md`(wheel にも入る)。
127
127
 
128
- - **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`・`notifications`・`admin`。
128
+ - **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`・`notifications`・`report`・`admin`。
129
129
  - **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
130
130
  - **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
131
131
  - **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
@@ -101,7 +101,7 @@ timer(systemd・`thth systemd` で生成)は10分ごとに実行し、投稿
101
101
  - **英語の文書**: [README.en.md](README.en.md)・[docs/usage.en.md](docs/usage.en.md)・[llms.txt](llms.txt)。
102
102
  - **skill**: `skills/thth/SKILL.md`(wheel にも入る)。
103
103
 
104
- - **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`・`notifications`・`admin`。
104
+ - **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`・`notifications`・`report`・`admin`。
105
105
  - **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
106
106
  - **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
107
107
  - **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
@@ -534,6 +534,24 @@ ADMIN_TOOLS.append({'name':'thth_admin_watch_set','description':'Replace the wat
534
534
  'words':{'type':'array','items':{'type':'string'}}},
535
535
  'required':['account','words','by'],'additionalProperties':False}})
536
536
 
537
+ # 報告の口の実装側(設計 3.1.2 §1)。返事と閉じるは `by` 必須・変更ログに
538
+ # presence-only で残る。書き出し(export)は repo に書くので CLI だけ。
539
+ ADMIN_TOOLS += [
540
+ {'name':'thth_admin_reports_list','description':'List bug reports and requests from every project (status open by default, or closed/all); administrator only; read-only',
541
+ 'inputSchema':{'type':'object','properties':{'status':{'type':'string','enum':['open','closed','all']}},
542
+ 'required':[],'additionalProperties':False}},
543
+ {'name':'thth_admin_reports_show','description':'Read one report with its body, reproduction steps and replies; administrator only; read-only',
544
+ 'inputSchema':{'type':'object','properties':{'report_id':{'type':'string'}},
545
+ 'required':['report_id'],'additionalProperties':False}},
546
+ {'name':'thth_admin_reports_reply','description':'Add a reply to one report (shown to the reporter in operations_handoff tool.reports); administrator only, by required; refused if the text looks like a secret',
547
+ 'inputSchema':{'type':'object','properties':{'report_id':{'type':'string'},'text':{'type':'string'},'by':{'type':'string'}},
548
+ 'required':['report_id','text','by'],'additionalProperties':False}},
549
+ {'name':'thth_admin_reports_close','description':'Close one report with a reason (fixed, wontfix, duplicate, invalid) and the version that settled it; administrator only, by required',
550
+ 'inputSchema':{'type':'object','properties':{'report_id':{'type':'string'},'by':{'type':'string'},
551
+ 'reason':{'type':'string','enum':['fixed','wontfix','duplicate','invalid']},'version':{'type':'string'}},
552
+ 'required':['report_id','reason','version','by'],'additionalProperties':False}},
553
+ ]
554
+
537
555
  SERVER_TOOLS = [
538
556
  {"name":"thth_"+name,"description":"Scoped server "+name,
539
557
  "inputSchema":{"type":"object","properties":{key:{"type":"string"} for key in ("account",*keys)},
@@ -566,6 +584,52 @@ del _tool
566
584
 
567
585
 
568
586
 
587
+ # 報告の口の利用者側(設計 3.1.2 §1)。**サーバ型だけに出す**——手元の stdio
588
+ # (pip 版)で置いても実装側には届かないので、届かない口は並べない。誰が置いたかは
589
+ # credential の `actor` が決める(要求の欄からは作らない)。
590
+ REPORT_TOOLS = [
591
+ {"name": "thth_report_file",
592
+ "description": "道具が断った・結果が期待と違った・欲しい形がある、のどれかならこれで置く"
593
+ "(kind は bug か request。実装側が読み、返事は thth_report_show と "
594
+ "operations_handoff の tool.reports に出る。秘密らしき値が含まれていたら置かない)",
595
+ "inputSchema": {"type": "object", "properties": {
596
+ "account": {"type": "string"},
597
+ "kind": {"type": "string", "enum": ["bug", "request"]},
598
+ "title": {"type": "string", "description": "1 行・120 字まで"},
599
+ "body": {"type": "string", "description": "8,000 字まで"},
600
+ "repro": {"type": "string", "description": "再現手順(任意・4,000 字まで)"}},
601
+ "required": ["account", "kind", "title", "body"], "additionalProperties": False}},
602
+ {"name": "thth_report_list",
603
+ "description": "自分の project の報告と、実装側の返事の数を読む(読むだけ)",
604
+ "inputSchema": {"type": "object", "properties": {
605
+ "status": {"type": "string", "enum": ["open", "closed", "all"]}},
606
+ "additionalProperties": False}},
607
+ {"name": "thth_report_show",
608
+ "description": "報告 1 件の本文と実装側の返事を読む(読むだけ)",
609
+ "inputSchema": {"type": "object", "properties": {"report_id": {"type": "string"}},
610
+ "required": ["report_id"], "additionalProperties": False}},
611
+ ]
612
+
613
+
614
+ # **うまくいかなければ報告の口へ**(設計 3.1.2 §3.5)。利用者の道具の説明文の
615
+ # 末尾に 1 句。定数(TOOLS 等)は書き換えず、並べるときに足す——「売り文句」の
616
+ # 正本(設計 v2 §1・「自分の泉」§2)はそのまま残る。
617
+ REPORT_HINT = "(うまくいかなければ thth_report_file)"
618
+
619
+
620
+ def _with_report_hint(tools):
621
+ return [tool if tool["name"].startswith("thth_report_")
622
+ else dict(tool, description=tool["description"] + REPORT_HINT) for tool in tools]
623
+
624
+
625
+ def _channel_note():
626
+ """断りの応答に添える 2 つ目の text(静的な 1 行・本文も account 名も入れない)。"""
627
+ if APP_DIR not in sys.path:
628
+ sys.path.insert(0, APP_DIR)
629
+ from thth.report_inbox import CHANNEL_LINE
630
+ return {"type": "text", "text": CHANNEL_LINE}
631
+
632
+
569
633
  def server_mode():
570
634
  return 'THTH_REPORT_CREDENTIALS' in os.environ or 'THTH_REPORT_TOKEN' in os.environ
571
635
 
@@ -579,15 +643,16 @@ def server_tools(context):
579
643
  # account/project しか対象にできない(`report_service.execute_morning()`)。
580
644
  reports = reports + [tool for tool in TOOLS if tool['name']=='thth_morning']
581
645
  from thth.server_writes import WRITE_OPERATIONS
582
- return reports + [tool for tool in SERVER_TOOLS
583
- if context.writes or tool['name'][5:] not in WRITE_OPERATIONS]
646
+ return _with_report_hint(reports + [tool for tool in SERVER_TOOLS
647
+ if context.writes or tool['name'][5:] not in WRITE_OPERATIONS]) + REPORT_TOOLS
584
648
 
585
649
 
586
650
  def server_call(name, arguments):
587
651
  from thth.report_service import execute_report, execute_mcp_report, ReportServiceError
588
652
  from thth.server_writes import execute, WRITE_OPERATIONS, SAFE_ERRORS, DRAFT_REASONS
589
653
  context=authenticated_context()
590
- failure=lambda value:{'content':[{'type':'text','text':value}],'isError':True}
654
+ # 断りは 1 つ目の text が静的な理由、2 つ目が受け口の案内(設計 3.1.2 §3.5)。
655
+ failure=lambda value:{'content':[{'type':'text','text':value},_channel_note()],'isError':True}
591
656
  if context is None: return failure('unauthorized')
592
657
  tool=next((tool for tool in server_tools(context) if tool['name']==name),None)
593
658
  if tool is None: return failure('unsupported_operation')
@@ -613,6 +678,12 @@ def server_call(name, arguments):
613
678
  result=execute_admin_write(context,request)
614
679
  elif operation in WRITE_OPERATIONS:
615
680
  result=execute(context,request,via='mcp')
681
+ elif operation.startswith('admin_reports_'):
682
+ from thth.report_service import execute_admin_reports
683
+ result=execute_admin_reports(context,request)
684
+ elif operation in ('report_file','report_list','report_show'):
685
+ from thth.report_service import execute_user_reports
686
+ result=execute_user_reports(context,request)
616
687
  elif operation=='morning':
617
688
  from thth.report_service import execute_morning
618
689
  result=execute_morning(context,request)
@@ -626,7 +697,11 @@ def server_call(name, arguments):
626
697
  # 理由は静的な符丁の表にあるものだけ。lint の自由文は通さない。
627
698
  detail=getattr(exc,'reason',None)
628
699
  return failure('invalid_draft: '+(detail if detail in DRAFT_REASONS else 'validation_failed'))
629
- return failure(str(exc) if str(exc) in SAFE_ERRORS or str(exc) in ('budget_change_durability_unconfirmed','budget_change_partially_recorded','budget_change_refused','watch_change_refused') else 'request_unavailable')
700
+ from thth.report_inbox import REASONS as REPORT_REASONS
701
+ if str(exc)=='duplicate_report' and getattr(exc,'report_id',None):
702
+ # 既存の id は同じ account の報告だけ(重複の検査がそう絞っている)。
703
+ return failure('duplicate_report: '+exc.report_id)
704
+ return failure(str(exc) if str(exc) in SAFE_ERRORS or str(exc) in REPORT_REASONS or str(exc) in ('budget_change_durability_unconfirmed','budget_change_partially_recorded','budget_change_refused','watch_change_refused') else 'request_unavailable')
630
705
  except Exception:
631
706
  return failure('request_unavailable')
632
707
 
@@ -670,7 +745,7 @@ def call_tool(name: str, arguments: dict | None) -> dict:
670
745
  if isinstance(name, str) and name.startswith("thth_admin_"):
671
746
  context = admin_context()
672
747
  if context is None:
673
- return {"content": [{"type": "text", "text": "unauthorized"}], "isError": True}
748
+ return {"content": [{"type": "text", "text": "unauthorized"}, _channel_note()], "isError": True}
674
749
  from thth.report_service import execute_report, ReportServiceError
675
750
  try:
676
751
  if "operation" in arguments: raise ReportServiceError("invalid_request")
@@ -679,7 +754,7 @@ def call_tool(name: str, arguments: dict | None) -> dict:
679
754
  return {"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False)}]}
680
755
  except (ValueError, TypeError, ReportServiceError) as error:
681
756
  reason = "invalid_options" if str(error) == "invalid_options" else "invalid_request"
682
- return {"content": [{"type": "text", "text": reason}], "isError": True}
757
+ return {"content": [{"type": "text", "text": reason}, _channel_note()], "isError": True}
683
758
  if name == "thth_lint":
684
759
  proc = run_cli(["lint", arguments["file"], "--json"])
685
760
  text = proc.stdout
@@ -838,7 +913,7 @@ def call_tool(name: str, arguments: dict | None) -> dict:
838
913
  proc = run_cli(args)
839
914
  text = proc.stdout
840
915
  else:
841
- return {"content": [{"type": "text", "text": f"unknown tool: {name}"}], "isError": True}
916
+ return {"content": [{"type": "text", "text": f"unknown tool: {name}"}, _channel_note()], "isError": True}
842
917
 
843
918
  if name.startswith("thth_topic_") or name in (
844
919
  "before_you_post", "after_you_posted", "analytics_report", "operations_handoff", "study_report", "thread_read", "where_to_appear",
@@ -854,7 +929,11 @@ def call_tool(name: str, arguments: dict | None) -> dict:
854
929
  is_error = proc.returncode not in (0, 1) # lint は 1 も正常な「検査結果」
855
930
  if not text:
856
931
  text = proc.stderr
857
- return {"content": [{"type": "text", "text": text}], "isError": is_error}
932
+ content = [{"type": "text", "text": text}]
933
+ # 断りなら受け口の案内を 2 つ目に(CLI の理由行に既に載っていれば重ねない)。
934
+ if is_error and _channel_note()["text"] not in text:
935
+ content.append(_channel_note())
936
+ return {"content": content, "isError": is_error}
858
937
 
859
938
 
860
939
  def _send(obj: dict) -> None:
@@ -5,6 +5,8 @@ description: 投稿する前・返信を書く前・出したあと・絡みに
5
5
 
6
6
  **開始時は、まず `thth_morning`(CLI は `thth morning <project|account> --json`)を 1 回。** 昨日から何があって今日なにをすればよいかが 6 段(道具・返していないもの・昨日の自分・世間・予定・次の一手)で返る。`handoff-report --since-last-read` を包含するので、版の確認もこれ 1 回で済む。呼ぶと栞(前回読んだ時点)が進む——進めたくないときだけ `mark: false`(CLI は `--no-mark`)。取れなかった段は `{"value": null, "cannot_say": "<理由>"}`(`provider_timeout`・`budget_exhausted`・`scope_missing`・`no_watch_words`・`not_supported`・`unavailable`)で、**0 件とは別**。第 3 段の監視語は**管理者が入れる**(`thth admin watch set <account> 語 … --by <名前>`)——語が無い account は `no_watch_words` で飛ぶので、**代わりに語を選ばない**。第 5 段は候補の列挙だけで、**本文は返らない**(書くのはあなたと人の会話の側)。詳しくは `docs/使い方_毎朝の一枚_3.1.md`。
7
7
 
8
+ **断られたら・違和感があれば報告の口へ**: `thth_report_file`(CLI は `thth report file <account> --kind bug|request --title … --body-file … --by <名前>`)。道具が断った・結果が期待と違った・欲しい形がある、のどれかなら 1 回で置ける。返事は `thth_report_show` と `handoff-report --since-last-read` の `tool.reports` に出る。秘密らしき値が含まれていたら置かれない(伏せてから置き直す)。
9
+
8
10
  段だけを読み直したいときは `thth handoff-report <account> --since-last-read --json` を読む。`tool.changed_since_last_read` が真なら、手元の `tool.notes_root_local_hint` を手掛かりに `tool.release_notes` の相対名を読んでから作業する。既読の記録は `--mark-read --by <名前>` の明示時だけ。`tool.notes_reason` が `notes_directory_unavailable` なら、このインストールには読める docs がない。空の `release_notes` を「変更なし」と解釈しない。
9
11
 
10
12
  添付を付ける前に `tool.capabilities` を読む(媒体ごとの「出せるもの」表。`thth doctor` にも同じ表が出る。`unverified` は「非対応」ではない)。
@@ -141,6 +143,7 @@ MCP `after_you_posted`。**24h の刻みが無ければ `null`**(0 と混ぜ
141
143
 
142
144
  - `thth --help` / `thth <subcommand> --help` が正本。
143
145
  - `thth doctor <account>` が「いま投稿できる状態か」を最初から最後まで言う。
146
+ - 道具の不具合・欲しい形は `thth report file`(MCP `thth_report_file`)で実装側に届ける。
144
147
  - 使い方の全文: `docs/使い方_プロジェクトのセッション向け_2026-09-09.md`。
145
148
 
146
149
  ## 運用と分析のレポート(v3系・2.5.0 から)
@@ -3,7 +3,7 @@ name: thth-admin
3
3
  description: Read authenticated THTH administrator inventory, changes and token status; hand findings to a person.
4
4
  ---
5
5
 
6
- 開始時は `thth admin diff --since-last-read --json` を読み、変化があった account を `thth admin account <name> --json` で確認する。週1回 `thth admin tokens --json` を確認する。
6
+ 開始時は `thth admin diff --since-last-read --json` を読み、変化があった account を `thth admin account <name> --json` で確認する。利用者からの不具合と要望は `thth admin reports list --json`(MCP `thth_admin_reports_list`・既定は開いているものだけ)で読み、`show <id>` で全文を見る。返事は `thth admin reports reply <id> --by <名前> --text-file <file>`、閉じるときは `close <id> --by <名前> --reason fixed|wontfix|duplicate|invalid --version <版>`。返事と閉じるは人(masaru)の判断を経てから打つ。写しを残すときは `thth admin reports export --to <私有の dir> --by <名前>`。**私有の置き場へ。公開 repo には置かない**(本文は利用者が書いた文。既定の行き先は無い)。週1回 `thth admin tokens --json` を確認する。
7
7
 
8
8
  読んだ結果から承認・投稿・設定変更をしない。必要な対処は根拠・欠測理由・観測時刻とともに人に渡す。記録が無い null は正常・停止・期限なしの証拠ではない。API probe は人が明示的に必要とした場合の CLI `--probe` のみ。
9
9
 
@@ -39,7 +39,9 @@ def test_admin_all_registry_and_mcp_auth_rechecked(credentials,monkeypatch):
39
39
  assert server.admin_context().scope=='admin'
40
40
  assert {tool['name'] for tool in server.ADMIN_TOOLS}=={
41
41
  'thth_admin_inventory','thth_admin_account','thth_admin_diff','thth_admin_log',
42
- 'thth_admin_tokens','thth_admin_release','thth_admin_budget_set','thth_admin_watch_set'}
42
+ 'thth_admin_tokens','thth_admin_release','thth_admin_budget_set','thth_admin_watch_set',
43
+ # 報告の口の実装側(設計 3.1.2 §1)。
44
+ 'thth_admin_reports_list','thth_admin_reports_show','thth_admin_reports_reply','thth_admin_reports_close'}
43
45
  result=server.call_tool('thth_admin_inventory',{})
44
46
  assert set(json.loads(result['content'][0]['text'])['by_account'])=={'first','second'}
45
47
  assert server.call_tool('thth_admin_diff',{'mark_read':True})['isError']
@@ -780,7 +780,11 @@ def test_本物の台帳でrc0でcannot_sayが出る(tmp_path):
780
780
  answer = json.loads(proc.stdout)
781
781
  assert answer["provenance"]["source"] == "local"
782
782
  assert answer["cannot_say"], "手元の水で言い切ってしまっている"
783
- assert answer["expected"]["branches_24h"]["median"] is None
783
+ # 本物の水は日々増えるので、中央値の有無そのものは固定できない(2026-09-23:
784
+ # kopicha の 24h の観測が n=24 になり、中央値 0 が正直な答えになった)。見るのは
785
+ # 規約 2「n が min_n に満たなければ中央値を言わない」が本物の水でも守られること。
786
+ branches = answer["expected"]["branches_24h"]
787
+ assert (branches["median"] is None) == (branches["n"] < ask_mod.DEFAULT_MIN_N)
784
788
 
785
789
 
786
790
  def test_P3_6_THTH_NOWで時刻を固定できる(account, thth_root):
@@ -24,6 +24,14 @@ def _replies_path(account, post_id: str = "POST1") -> str:
24
24
  return os.path.join(account["repo_dir"], "data", "sns", "replies", f"{post_id}.ndjson")
25
25
 
26
26
 
27
+ def _own_post(account, post_id: str = "POST1") -> None:
28
+ """その account が出した記録(`sent/`)を置く。`thth replies` は置き場を共有する
29
+ 他 account の返信を返さないので(3.1.1)、CLI の試験は所有の記録を先に置く。"""
30
+ from thth import accounts, approval, sent
31
+ sent.write(accounts.state_dir_for(account["name"]), post_id=post_id, text="本文",
32
+ body_hash=approval.compute_body_hash("本文"), sent_at="2026-09-10T09:00:00+09:00")
33
+
34
+
27
35
  def test_返信を読めること(isolated_account):
28
36
  """`kind: reply` の行だけが `replies` に入り、`kind: fetch` の行は `fetches` に入る。"""
29
37
  _write_ndjson(_replies_path(isolated_account), [
@@ -129,14 +137,16 @@ def test_CLIがjsonでloadと同じものを返す(isolated_account, capsys):
129
137
  "post_id": "POST1", "collected_at": "2026-09-10T19:00:00+09:00"},
130
138
  ])
131
139
 
140
+ _own_post(isolated_account)
141
+
132
142
  args = argparse.Namespace(account=isolated_account["name"], post=None, json=True)
133
143
  rc = cli_mod.cmd_replies(args)
134
144
  captured = capsys.readouterr()
135
145
 
136
146
  assert rc == 0
137
147
  printed = json.loads(captured.out)
138
- expected = replies_mod.load(isolated_account["name"])
139
- assert printed == expected
148
+ expected = replies_mod.load(isolated_account["name"], owned_only=True)
149
+ assert printed == expected and [r["id"] for r in printed["replies"]] == ["R1"]
140
150
 
141
151
 
142
152
  def test_CLIの人向け出力は身内に印をつける(isolated_account_factory, capsys):
@@ -149,6 +159,7 @@ def test_CLIの人向け出力は身内に印をつける(isolated_account_facto
149
159
  {"kind": "reply", "id": "R2", "text": "よその返信", "username": "だれか",
150
160
  "post_id": "POST1", "collected_at": "2026-09-10T19:00:00+09:00"},
151
161
  ])
162
+ _own_post(account)
152
163
 
153
164
  args = argparse.Namespace(account=account["name"], post=None, json=False)
154
165
  rc = cli_mod.cmd_replies(args)
@@ -25,7 +25,10 @@ def json_parsers():
25
25
  # must acquire a table row, rather than quietly escaping the contract.
26
26
  COMMANDS = '''lint|preview|approve|account|revoke|posts|replies|measured|threads|study-report|analytics-report|after|topics|forms|queue|schedule|throw|handoff-report|board|pull|maintain|doctor|app show|admin inventory|admin account|admin log|admin tokens|admin timers|admin release|admin diff|ask before-you-post|mentions|profile|thread|where|who|retract|location|topics suggest|topics observe|topics record-decision|topics decision|topics observation|topics retract|topics unretract|topics profile|topics adopt-reason|topics adoptions|topics record-vocabulary|topics vocabulary|topics record-review|topics review|topics record-form-spec|topics form-spec|topics form-check|topics improvements|topics impact|topics record-hypothesis|topics hypotheses'''.split('|') + ['account add', 'account migrate', 'unanswered', 'study add', 'admin deletion sync', 'admin budget', 'admin media cleanup-retry', 'admin media gc',
27
27
  # 3.1.0: 毎朝の一枚と監視語(設計 3.1.0 §1・§3)。
28
- 'morning', 'admin watch set', 'admin watch show']
28
+ 'morning', 'admin watch set', 'admin watch show',
29
+ # 3.1.2: 報告の口(設計 3.1.2 §1)。
30
+ 'report file', 'report list', 'report show', 'admin reports list', 'admin reports show',
31
+ 'admin reports reply', 'admin reports close', 'admin reports export']
29
32
 
30
33
 
31
34
  def test_json_table_covers_every_parser():
@@ -149,7 +149,8 @@ def test_shared_manual_grant_does_not_tell_person_to_revoke_other_account(env,wo
149
149
  a=env['root']/'secrets/alpha.json';b=env['root']/'secrets/beta.json';b.write_bytes(a.read_bytes())
150
150
  assert leave.command(SimpleNamespace(name='alpha',by='operator',json=False))==0
151
151
  result=leave.read('alpha')
152
- assert result['remote']=='unconfirmed_shared' and a.exists() and b.exists()
152
+ # 3.1.2 件 6・裁定 09-23: 値だけの共有(別ファイル)なら自分の file は消し、相手の file は残す。
153
+ assert result['remote']=='unconfirmed_shared' and not a.exists() and b.exists()
153
154
  out=capsys.readouterr().out
154
155
  assert '切り分け' in out and '本人が解除' not in out
155
156
 
@@ -84,8 +84,11 @@ def test_shared_mastodon_grant_refuses_remote_revoke_but_shared_client_alone_doe
84
84
  cfg,tokenpath=configure(env,provider,monkeypatch,'mastodon');old=tokenpath.read_bytes()
85
85
  other=env['root']/'accounts/beta.json';b=json.loads(other.read_text());b.update(media='mastodon',instance=provider['base']);other.write_text(json.dumps(b))
86
86
  (env['root']/'secrets/beta.json').write_bytes(old)
87
- with pytest.raises(ValueError,match='shared_credential'):leave.run('alpha',by='operator')
88
- assert provider['calls']==[] and tokenpath.read_bytes()==old
87
+ # 3.1.2 件 6: 共有の接続は遠隔で失効させず(calls==[])、止まらずに退出を終える。
88
+ # 裁定 09-23: 値だけの共有(別ファイル)なら自分の token file は消す。相手の file は残る。
89
+ result=leave.run('alpha',by='operator')
90
+ assert result['phase']=='completed' and result['remote']=='unconfirmed_shared' and 'token_shared' not in result['preserved']
91
+ assert provider['calls']==[] and not tokenpath.exists() and (env['root']/'secrets/beta.json').read_bytes()==old
89
92
 
90
93
 
91
94
  def test_b_credential_commit_before_revoke_is_rechecked_after_inventory(env,provider,monkeypatch):
@@ -99,8 +102,11 @@ def test_b_credential_commit_before_revoke_is_rechecked_after_inventory(env,prov
99
102
  authflow.commit_manual('beta',cfg_b,token_a,snapshot=snapshot,session=None,by='operator')
100
103
  return {'status':'revoked'}
101
104
  monkeypatch.setattr(approval_relay,'signed_request',worker)
102
- with pytest.raises(ValueError,match='shared_credential'):leave.run('alpha',by='operator')
103
- assert provider['calls']==[] and tokenpath.exists()
105
+ # 3.1.2 件 6: 共有の接続は遠隔で失効させず(calls==[])、止まらずに退出を終える。
106
+ # 裁定 09-23: 値だけの共有(別ファイル)なら自分の token file は消す。相手の file は残る。
107
+ result=leave.run('alpha',by='operator')
108
+ assert result['phase']=='completed' and result['remote']=='unconfirmed_shared' and 'token_shared' not in result['preserved']
109
+ assert provider['calls']==[] and not tokenpath.exists()
104
110
  assert json.loads((env['root']/'secrets/beta.json').read_text())==token_a
105
111
 
106
112
 
@@ -166,5 +172,7 @@ def test_b_exchange_to_commit_holds_registry_lease_and_late_shared_grant_vetoes_
166
172
  assert a.is_alive() and provider['calls']==[]
167
173
  release.set();a.join(4);b.join(4)
168
174
  assert not a.is_alive() and not b.is_alive() and results==[0]
169
- assert leave_errors==['shared_credential_revoke_refused'] and provider['calls']==[]
175
+ # 3.1.2 件 6: 共有の接続は遠隔で失効させず(calls==[])、止まらずに退出を終える。
176
+ assert leave_errors==[] and provider['calls']==[]
177
+ assert leave.read('alpha')['phase']=='completed' and leave.read('alpha')['remote']=='unconfirmed_shared'
170
178
  assert accounts.load_token(accounts.load_account('beta'))['access_token']==value['access_token']
@@ -61,7 +61,7 @@ def test_cli_reads_do_not_wait_or_call_core_when_leave_holds_exclusive(env,conte
61
61
  start=time.monotonic();rc=cli.main(command);elapsed=time.monotonic()-start
62
62
  captured=capsys.readouterr();assert rc==2 and elapsed<1
63
63
  assert json.loads(captured.out)=={'error':'account_leaving','cannot_say':['account_leaving']}
64
- assert captured.err.strip()=='account_leaving'
64
+ assert captured.err.strip()=='account_leaving\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
65
65
 
66
66
 
67
67
  def test_dry_send_input_then_nonwaiting_lease(env,tmp_path,capsys):
@@ -69,7 +69,7 @@ def test_dry_send_input_then_nonwaiting_lease(env,tmp_path,capsys):
69
69
  with exclusive():
70
70
  start=time.monotonic();rc=cli.main(['send','alpha','--text-file',str(text)])
71
71
  assert time.monotonic()-start<1 and rc==2
72
- assert capsys.readouterr().err.strip()=='account_leaving'
72
+ assert capsys.readouterr().err.strip()=='account_leaving\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
73
73
  assert not (env['root']/'state/alpha/sent').exists()
74
74
 
75
75
 
@@ -145,7 +145,7 @@ def test_real_cli_child_rejects_ex_under_deadline(env,content):
145
145
  result=subprocess.run([sys.executable,'-c',"from thth.cli import main;raise SystemExit(main(['queue','alpha','--json']))"],capture_output=True,text=True,timeout=2)
146
146
  except subprocess.TimeoutExpired:pytest.fail('reader waited for the exit drain')
147
147
  assert result.returncode==2 and json.loads(result.stdout)['cannot_say']==['account_leaving']
148
- assert result.stderr.strip()=='account_leaving'
148
+ assert result.stderr.strip()=='account_leaving\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
149
149
 
150
150
 
151
151
  def test_report_holds_lease_through_calculation_and_release(env,monkeypatch):
@@ -51,7 +51,7 @@ raise SystemExit(cli.main(json.loads(sys.argv[1])))
51
51
  # cannot trap this pytest process before an elapsed-time assertion.
52
52
  result = subprocess.run([sys.executable, '-c', code, json.dumps(command), str(marker)],
53
53
  capture_output=True, text=True, timeout=3)
54
- assert result.returncode == 2 and result.stderr.strip() == 'account_leaving'
54
+ assert result.returncode == 2 and result.stderr.strip() == 'account_leaving\n' + __import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
55
55
  if operation == 'replies':
56
56
  assert json.loads(result.stdout) == {'error': 'account_leaving', 'cannot_say': ['account_leaving']}
57
57
  assert not marker.exists()
@@ -14,7 +14,7 @@ def test_run_invalid_name_before_any_storage_or_effect(monkeypatch,capsys,name):
14
14
  monkeypatch.setattr('thth.incident.notify',forbidden)
15
15
  assert cli.main(['run',name])==2
16
16
  captured=capsys.readouterr();value=json.loads(captured.out)
17
- assert captured.err.strip()=='invalid_account_name'
17
+ assert captured.err.strip()=='invalid_account_name\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
18
18
  assert value['error']==value['record_unavailable']=='invalid_account_name'
19
19
  assert value['account'] is None and value['runs_recorded'] is False
20
20
  assert '使えない字' in value['message'] and 'account_stopped' not in captured.out
@@ -30,7 +30,7 @@ def test_run_reason_and_no_side_effect(env,monkeypatch,capsys,state,expected):
30
30
  monkeypatch.setattr('thth.healthcheck.notify',lambda *a,**kw:pytest.fail('healthcheck notification'))
31
31
  monkeypatch.setattr('thth.incident.notify',lambda *a,**kw:pytest.fail('incident notification'))
32
32
  assert cli.main(['run','alpha'])==2
33
- shown=capsys.readouterr();assert shown.err.strip()==expected
33
+ shown=capsys.readouterr();assert shown.err.strip()==expected+'\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
34
34
  value=json.loads(shown.out)
35
35
  assert value=={'account':'alpha','mode':'production','action':'skip','status':'error','error':expected,
36
36
  'runs_recorded':False,'record_unavailable':expected}
@@ -645,6 +645,9 @@ def test_DELETEが飛ぶ経路はretract_cliのdelete_postだけ():
645
645
  src = inspect.getsource(retract_cli)
646
646
  assert src.count(".delete_post(") == 1 # 呼び出しは 1 か所(docstring の言及は除く)
647
647
  assert inspect.getsource(threads_mod).count('method="DELETE"') == 1
648
+ # Mastodon の DELETE(3.1.1)も `delete_post` の中の 1 か所だけ。
649
+ assert inspect.getsource(mastodon).count('"DELETE"') == 1
650
+ assert '"DELETE"' in inspect.getsource(mastodon.MastodonAdapter.delete_post)
648
651
 
649
652
 
650
653
  def test_Threadsのdelete_postは数字以外のidを断る():
@@ -0,0 +1,68 @@
1
+ """3.1.1 件 1: 退出の「共有」判定は token の中身(値・ファイル)に限る。
2
+
3
+ 同じ instance の同じ利用者(user_id 同一)の別台帳は、Mastodon/X では共有と見ない。
4
+ 失効は渡した token の値だけに効くので、相手の token file・台帳・停止状態に触らずに
5
+ 退出が completed まで通ることを、loopback の fake 媒体で固定する。
6
+ """
7
+ import json
8
+ import pytest
9
+ from thth import accounts,leave,leave_gate,approval_relay
10
+ from tests.test_v212_server_writes import env
11
+ from tests.test_v212_leave_providers import provider,configure
12
+
13
+
14
+ def same_user_beta(env,provider,tokenpath,media,*,same_bytes=False):
15
+ """beta を alpha と同じ媒体・同じ instance・同じ user_id にする。token の値は既定で別。"""
16
+ user='synthetic-user-'+media
17
+ a=json.loads(tokenpath.read_text());a['user_id']=user;tokenpath.write_text(json.dumps(a))
18
+ path=env['root']/'accounts/beta.json';b=json.loads(path.read_text());b['media']=media
19
+ if media=='mastodon':b['instance']=provider['base']
20
+ path.write_text(json.dumps(b))
21
+ beta_token=env['root']/'secrets/beta.json'
22
+ if same_bytes:beta_token.write_bytes(tokenpath.read_bytes())
23
+ else:
24
+ t=json.loads(beta_token.read_text());t['user_id']=user;beta_token.write_text(json.dumps(t))
25
+ assert t['access_token']!=a['access_token']
26
+ return beta_token,path
27
+
28
+
29
+ @pytest.mark.parametrize('media',['mastodon','x'])
30
+ def test_same_user_different_token_leave_completes_and_leaves_other_account_untouched(env,provider,monkeypatch,media):
31
+ _,tokenpath=configure(env,provider,monkeypatch,media)
32
+ beta_token,beta_ledger=same_user_beta(env,provider,tokenpath,media)
33
+ other_token=beta_token.read_bytes();other_ledger=beta_ledger.read_bytes()
34
+ other_access=json.loads(other_token)['access_token']
35
+ result=leave.run('alpha',by='operator')
36
+ assert result['phase']=='completed' and result['remote']=='confirmed'
37
+ assert 'token_shared' not in result['preserved'] and not tokenpath.exists()
38
+ # 媒体の失効に来たのは自分の token の値だけ。相手の値は 1 度も送られていない。
39
+ sent=[form['token'][0] for _,form,_ in provider['calls']]
40
+ assert provider['access'] in sent and other_access not in sent
41
+ assert set(sent)<={provider['access'],provider['refresh']}
42
+ # 相手の token file と台帳は byte 一致で残り、停止もしていない。
43
+ assert beta_token.read_bytes()==other_token and beta_ledger.read_bytes()==other_ledger
44
+ assert not leave_gate.stopped('beta') and leave.read('beta') is None
45
+ assert accounts.load_token(accounts.load_account('beta'))['access_token']==other_access
46
+
47
+
48
+ def test_same_token_bytes_do_not_revoke_remotely(env,provider,monkeypatch):
49
+ _,tokenpath=configure(env,provider,monkeypatch,'mastodon')
50
+ beta_token,_=same_user_beta(env,provider,tokenpath,'mastodon',same_bytes=True)
51
+ old=tokenpath.read_bytes()
52
+ # 3.1.2 件 6: 失効は投げない(他方も死ぬ)。止まらずに completed まで進む
53
+ # (完了までの形は tests/test_v312_leave_shared_token.py)。
54
+ result=leave.run('alpha',by='operator')
55
+ assert result['phase']=='completed' and result['remote']=='unconfirmed_shared'
56
+ # 裁定 09-23: 値だけの共有(別ファイル)なら自分の token file は消す。相手の file は残る。
57
+ assert provider['calls']==[] and not tokenpath.exists() and beta_token.read_bytes()==old
58
+
59
+
60
+ def test_threads_same_user_is_still_token_shared_and_preserved(env,monkeypatch):
61
+ monkeypatch.setattr(approval_relay,'signed_request',lambda *a:{'status':'revoked'})
62
+ paths=[env['root']/'secrets'/(n+'.json') for n in ('alpha','beta')]
63
+ for p in paths:
64
+ t=json.loads(p.read_text());t['user_id']='synthetic-threads-user';p.write_text(json.dumps(t))
65
+ old=[p.read_bytes() for p in paths]
66
+ result=leave.run('alpha',by='operator')
67
+ assert result['phase']=='completed' and result['preserved']==['token_shared'] and result['remote']=='unconfirmed_shared'
68
+ assert [p.read_bytes() for p in paths]==old and not leave_gate.stopped('beta')