thth 3.3.0__tar.gz → 3.4.0__tar.gz

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