thth 3.1.1__tar.gz → 3.2.0__tar.gz

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