thth 3.8.0__tar.gz → 3.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (581) hide show
  1. {thth-3.8.0 → thth-3.8.2}/PKG-INFO +1 -1
  2. {thth-3.8.0 → thth-3.8.2}/mcp/server.py +5 -2
  3. {thth-3.8.0 → thth-3.8.2}/tests/test_v380_plaza_moments.py +6 -2
  4. {thth-3.8.0 → thth-3.8.2}/tests/test_v380_plaza_owner.py +5 -3
  5. thth-3.8.2/tests/test_v381_plaza_visibility_default.py +100 -0
  6. thth-3.8.2/tests/test_v382_confirm_file.py +218 -0
  7. thth-3.8.2/tests/test_v382_first_stage_not_refusal.py +112 -0
  8. thth-3.8.2/tests/test_v382_reader_lock_and_push_pending.py +105 -0
  9. thth-3.8.2/tests/test_v382_sync_without_fetch_head.py +191 -0
  10. thth-3.8.2/thth/VERSION +1 -0
  11. {thth-3.8.0 → thth-3.8.2}/thth/cli.py +331 -7
  12. {thth-3.8.0 → thth-3.8.2}/thth/plaza.py +29 -9
  13. {thth-3.8.0 → thth-3.8.2}/thth/plaza_cli.py +12 -3
  14. {thth-3.8.0 → thth-3.8.2}/thth/refusals.py +23 -0
  15. {thth-3.8.0 → thth-3.8.2}/thth/report_service.py +3 -1
  16. {thth-3.8.0 → thth-3.8.2}/thth/writeback.py +140 -17
  17. thth-3.8.0/thth/VERSION +0 -1
  18. {thth-3.8.0 → thth-3.8.2}/.gitignore +0 -0
  19. {thth-3.8.0 → thth-3.8.2}/LICENSE +0 -0
  20. {thth-3.8.0 → thth-3.8.2}/README.md +0 -0
  21. {thth-3.8.0 → thth-3.8.2}/accounts.example/bluesky.json +0 -0
  22. {thth-3.8.0 → thth-3.8.2}/accounts.example/mastodon.json +0 -0
  23. {thth-3.8.0 → thth-3.8.2}/accounts.example/threads.json +0 -0
  24. {thth-3.8.0 → thth-3.8.2}/accounts.example/x.json +0 -0
  25. {thth-3.8.0 → thth-3.8.2}/bin/thth +0 -0
  26. {thth-3.8.0 → thth-3.8.2}/bin/thth-remote +0 -0
  27. {thth-3.8.0 → thth-3.8.2}/bin/thth-run +0 -0
  28. {thth-3.8.0 → thth-3.8.2}/pyproject.toml +0 -0
  29. {thth-3.8.0 → thth-3.8.2}/skills/thth/SKILL.md +0 -0
  30. {thth-3.8.0 → thth-3.8.2}/skills/thth-admin/SKILL.md +0 -0
  31. {thth-3.8.0 → thth-3.8.2}/systemd/thth@.service +0 -0
  32. {thth-3.8.0 → thth-3.8.2}/tests/__init__.py +0 -0
  33. {thth-3.8.0 → thth-3.8.2}/tests/conftest.py +0 -0
  34. {thth-3.8.0 → thth-3.8.2}/tests/coordination_snapshot.py +0 -0
  35. {thth-3.8.0 → thth-3.8.2}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
  36. {thth-3.8.0 → thth-3.8.2}/tests/fixtures/build_injection_eval.py +0 -0
  37. {thth-3.8.0 → thth-3.8.2}/tests/fixtures/callback/worker_responses.json +0 -0
  38. {thth-3.8.0 → thth-3.8.2}/tests/fixtures/injection_eval.json +0 -0
  39. {thth-3.8.0 → thth-3.8.2}/tests/fixtures/umami-bile.md +0 -0
  40. {thth-3.8.0 → thth-3.8.2}/tests/helpers/__init__.py +0 -0
  41. {thth-3.8.0 → thth-3.8.2}/tests/helpers/fake_oauth_server.py +0 -0
  42. {thth-3.8.0 → thth-3.8.2}/tests/helpers/fake_push_adapter.py +0 -0
  43. {thth-3.8.0 → thth-3.8.2}/tests/helpers/fake_redirect_server.py +0 -0
  44. {thth-3.8.0 → thth-3.8.2}/tests/helpers/fake_search_adapter.py +0 -0
  45. {thth-3.8.0 → thth-3.8.2}/tests/helpers/fake_threads_reader.py +0 -0
  46. {thth-3.8.0 → thth-3.8.2}/tests/helpers/fake_threads_status.py +0 -0
  47. {thth-3.8.0 → thth-3.8.2}/tests/helpers/fake_threads_writes.py +0 -0
  48. {thth-3.8.0 → thth-3.8.2}/tests/helpers/fake_x.py +0 -0
  49. {thth-3.8.0 → thth-3.8.2}/tests/helpers/hold_lock.py +0 -0
  50. {thth-3.8.0 → thth-3.8.2}/tests/helpers/hold_repo_lock.py +0 -0
  51. {thth-3.8.0 → thth-3.8.2}/tests/helpers/worker_probe.mjs +0 -0
  52. {thth-3.8.0 → thth-3.8.2}/tests/test_27_acceptance_fixes.py +0 -0
  53. {thth-3.8.0 → thth-3.8.2}/tests/test_account_name_check.py +0 -0
  54. {thth-3.8.0 → thth-3.8.2}/tests/test_account_report.py +0 -0
  55. {thth-3.8.0 → thth-3.8.2}/tests/test_accounts_dir.py +0 -0
  56. {thth-3.8.0 → thth-3.8.2}/tests/test_adapter_boundary.py +0 -0
  57. {thth-3.8.0 → thth-3.8.2}/tests/test_admin_http_mcp.py +0 -0
  58. {thth-3.8.0 → thth-3.8.2}/tests/test_admin_log.py +0 -0
  59. {thth-3.8.0 → thth-3.8.2}/tests/test_admin_notifications.py +0 -0
  60. {thth-3.8.0 → thth-3.8.2}/tests/test_admin_repairs.py +0 -0
  61. {thth-3.8.0 → thth-3.8.2}/tests/test_admin_report.py +0 -0
  62. {thth-3.8.0 → thth-3.8.2}/tests/test_after_cli.py +0 -0
  63. {thth-3.8.0 → thth-3.8.2}/tests/test_analytics_comparison.py +0 -0
  64. {thth-3.8.0 → thth-3.8.2}/tests/test_analytics_report.py +0 -0
  65. {thth-3.8.0 → thth-3.8.2}/tests/test_api_diagnostic.py +0 -0
  66. {thth-3.8.0 → thth-3.8.2}/tests/test_api_error_body.py +0 -0
  67. {thth-3.8.0 → thth-3.8.2}/tests/test_app_env_cli.py +0 -0
  68. {thth-3.8.0 → thth-3.8.2}/tests/test_approval.py +0 -0
  69. {thth-3.8.0 → thth-3.8.2}/tests/test_approve_cmd.py +0 -0
  70. {thth-3.8.0 → thth-3.8.2}/tests/test_ask.py +0 -0
  71. {thth-3.8.0 → thth-3.8.2}/tests/test_atlas_fifth_review.py +0 -0
  72. {thth-3.8.0 → thth-3.8.2}/tests/test_atlas_fourth_review.py +0 -0
  73. {thth-3.8.0 → thth-3.8.2}/tests/test_atlas_review.py +0 -0
  74. {thth-3.8.0 → thth-3.8.2}/tests/test_atlas_second_review.py +0 -0
  75. {thth-3.8.0 → thth-3.8.2}/tests/test_atlas_third_review.py +0 -0
  76. {thth-3.8.0 → thth-3.8.2}/tests/test_audit1_fixes.py +0 -0
  77. {thth-3.8.0 → thth-3.8.2}/tests/test_audit2_fixes.py +0 -0
  78. {thth-3.8.0 → thth-3.8.2}/tests/test_audit_b_counterexamples.py +0 -0
  79. {thth-3.8.0 → thth-3.8.2}/tests/test_audit_b_m1_semantics.py +0 -0
  80. {thth-3.8.0 → thth-3.8.2}/tests/test_audit_b_n1_depth.py +0 -0
  81. {thth-3.8.0 → thth-3.8.2}/tests/test_audit_b_r1_closure.py +0 -0
  82. {thth-3.8.0 → thth-3.8.2}/tests/test_audit_v201.py +0 -0
  83. {thth-3.8.0 → thth-3.8.2}/tests/test_audit_v23_details.py +0 -0
  84. {thth-3.8.0 → thth-3.8.2}/tests/test_auth.py +0 -0
  85. {thth-3.8.0 → thth-3.8.2}/tests/test_auth_identity_required.py +0 -0
  86. {thth-3.8.0 → thth-3.8.2}/tests/test_behind_remote.py +0 -0
  87. {thth-3.8.0 → thth-3.8.2}/tests/test_bluesky_adapter.py +0 -0
  88. {thth-3.8.0 → thth-3.8.2}/tests/test_bluesky_secret_scrub.py +0 -0
  89. {thth-3.8.0 → thth-3.8.2}/tests/test_board_needs_review.py +0 -0
  90. {thth-3.8.0 → thth-3.8.2}/tests/test_board_running.py +0 -0
  91. {thth-3.8.0 → thth-3.8.2}/tests/test_bundle.py +0 -0
  92. {thth-3.8.0 → thth-3.8.2}/tests/test_charcount.py +0 -0
  93. {thth-3.8.0 → thth-3.8.2}/tests/test_cli_entrypoint.py +0 -0
  94. {thth-3.8.0 → thth-3.8.2}/tests/test_cli_help.py +0 -0
  95. {thth-3.8.0 → thth-3.8.2}/tests/test_cli_vm_path.py +0 -0
  96. {thth-3.8.0 → thth-3.8.2}/tests/test_collect.py +0 -0
  97. {thth-3.8.0 → thth-3.8.2}/tests/test_collect_engagements.py +0 -0
  98. {thth-3.8.0 → thth-3.8.2}/tests/test_collect_runs.py +0 -0
  99. {thth-3.8.0 → thth-3.8.2}/tests/test_collect_sent.py +0 -0
  100. {thth-3.8.0 → thth-3.8.2}/tests/test_comparability.py +0 -0
  101. {thth-3.8.0 → thth-3.8.2}/tests/test_control_chars_refused.py +0 -0
  102. {thth-3.8.0 → thth-3.8.2}/tests/test_conversation_paging.py +0 -0
  103. {thth-3.8.0 → thth-3.8.2}/tests/test_count_for.py +0 -0
  104. {thth-3.8.0 → thth-3.8.2}/tests/test_docs_links.py +0 -0
  105. {thth-3.8.0 → thth-3.8.2}/tests/test_doctor.py +0 -0
  106. {thth-3.8.0 → thth-3.8.2}/tests/test_doctor_all_permissions.py +0 -0
  107. {thth-3.8.0 → thth-3.8.2}/tests/test_doctor_next_step.py +0 -0
  108. {thth-3.8.0 → thth-3.8.2}/tests/test_duplicate_front_matter_keys.py +0 -0
  109. {thth-3.8.0 → thth-3.8.2}/tests/test_editorial_audit.py +0 -0
  110. {thth-3.8.0 → thth-3.8.2}/tests/test_editorial_n3_n4.py +0 -0
  111. {thth-3.8.0 → thth-3.8.2}/tests/test_engagements.py +0 -0
  112. {thth-3.8.0 → thth-3.8.2}/tests/test_external_acceptance.py +0 -0
  113. {thth-3.8.0 → thth-3.8.2}/tests/test_fake_api.py +0 -0
  114. {thth-3.8.0 → thth-3.8.2}/tests/test_form_spec.py +0 -0
  115. {thth-3.8.0 → thth-3.8.2}/tests/test_form_spec_cli.py +0 -0
  116. {thth-3.8.0 → thth-3.8.2}/tests/test_forms.py +0 -0
  117. {thth-3.8.0 → thth-3.8.2}/tests/test_fresh_install.py +0 -0
  118. {thth-3.8.0 → thth-3.8.2}/tests/test_front_matter_injection.py +0 -0
  119. {thth-3.8.0 → thth-3.8.2}/tests/test_healthcheck_notifications.py +0 -0
  120. {thth-3.8.0 → thth-3.8.2}/tests/test_hypotheses.py +0 -0
  121. {thth-3.8.0 → thth-3.8.2}/tests/test_hypothesis_boundaries.py +0 -0
  122. {thth-3.8.0 → thth-3.8.2}/tests/test_hypothesis_units.py +0 -0
  123. {thth-3.8.0 → thth-3.8.2}/tests/test_impact_cli.py +0 -0
  124. {thth-3.8.0 → thth-3.8.2}/tests/test_inbox_granted_scopes.py +0 -0
  125. {thth-3.8.0 → thth-3.8.2}/tests/test_incident_notifications.py +0 -0
  126. {thth-3.8.0 → thth-3.8.2}/tests/test_independent_thread_review.py +0 -0
  127. {thth-3.8.0 → thth-3.8.2}/tests/test_independent_topic_review.py +0 -0
  128. {thth-3.8.0 → thth-3.8.2}/tests/test_independent_topic_review_r2.py +0 -0
  129. {thth-3.8.0 → thth-3.8.2}/tests/test_inflight.py +0 -0
  130. {thth-3.8.0 → thth-3.8.2}/tests/test_inflight_tag_body_hash.py +0 -0
  131. {thth-3.8.0 → thth-3.8.2}/tests/test_jst.py +0 -0
  132. {thth-3.8.0 → thth-3.8.2}/tests/test_lint.py +0 -0
  133. {thth-3.8.0 → thth-3.8.2}/tests/test_list_projections.py +0 -0
  134. {thth-3.8.0 → thth-3.8.2}/tests/test_llm_trial.py +0 -0
  135. {thth-3.8.0 → thth-3.8.2}/tests/test_llm_trial_engage.py +0 -0
  136. {thth-3.8.0 → thth-3.8.2}/tests/test_lock.py +0 -0
  137. {thth-3.8.0 → thth-3.8.2}/tests/test_m2_independent.py +0 -0
  138. {thth-3.8.0 → thth-3.8.2}/tests/test_maintain.py +0 -0
  139. {thth-3.8.0 → thth-3.8.2}/tests/test_mastodon_adapter.py +0 -0
  140. {thth-3.8.0 → thth-3.8.2}/tests/test_mastodon_visibility.py +0 -0
  141. {thth-3.8.0 → thth-3.8.2}/tests/test_max_per_run.py +0 -0
  142. {thth-3.8.0 → thth-3.8.2}/tests/test_mcp.py +0 -0
  143. {thth-3.8.0 → thth-3.8.2}/tests/test_mcp_robustness.py +0 -0
  144. {thth-3.8.0 → thth-3.8.2}/tests/test_measured.py +0 -0
  145. {thth-3.8.0 → thth-3.8.2}/tests/test_media_accounts.py +0 -0
  146. {thth-3.8.0 → thth-3.8.2}/tests/test_media_next_steps.py +0 -0
  147. {thth-3.8.0 → thth-3.8.2}/tests/test_media_roundtrip.py +0 -0
  148. {thth-3.8.0 → thth-3.8.2}/tests/test_mismatch_diagnostics.py +0 -0
  149. {thth-3.8.0 → thth-3.8.2}/tests/test_n5_reply_null_element.py +0 -0
  150. {thth-3.8.0 → thth-3.8.2}/tests/test_n6_replies_incomplete_ledger.py +0 -0
  151. {thth-3.8.0 → thth-3.8.2}/tests/test_n7_doctor_error_body.py +0 -0
  152. {thth-3.8.0 → thth-3.8.2}/tests/test_no_limits.py +0 -0
  153. {thth-3.8.0 → thth-3.8.2}/tests/test_observation_provenance.py +0 -0
  154. {thth-3.8.0 → thth-3.8.2}/tests/test_operations_handoff.py +0 -0
  155. {thth-3.8.0 → thth-3.8.2}/tests/test_packaging.py +0 -0
  156. {thth-3.8.0 → thth-3.8.2}/tests/test_preview.py +0 -0
  157. {thth-3.8.0 → thth-3.8.2}/tests/test_production_mode.py +0 -0
  158. {thth-3.8.0 → thth-3.8.2}/tests/test_pull_cli.py +0 -0
  159. {thth-3.8.0 → thth-3.8.2}/tests/test_queue.py +0 -0
  160. {thth-3.8.0 → thth-3.8.2}/tests/test_quiet_hours.py +0 -0
  161. {thth-3.8.0 → thth-3.8.2}/tests/test_r2_r4_r5.py +0 -0
  162. {thth-3.8.0 → thth-3.8.2}/tests/test_read_cli_bare_runtimeerror.py +0 -0
  163. {thth-3.8.0 → thth-3.8.2}/tests/test_reason_adoption.py +0 -0
  164. {thth-3.8.0 → thth-3.8.2}/tests/test_redact.py +0 -0
  165. {thth-3.8.0 → thth-3.8.2}/tests/test_redirect_token_leak.py +0 -0
  166. {thth-3.8.0 → thth-3.8.2}/tests/test_refresh.py +0 -0
  167. {thth-3.8.0 → thth-3.8.2}/tests/test_refresh_destination.py +0 -0
  168. {thth-3.8.0 → thth-3.8.2}/tests/test_rejudge_new.py +0 -0
  169. {thth-3.8.0 → thth-3.8.2}/tests/test_rejudge_r2_r3.py +0 -0
  170. {thth-3.8.0 → thth-3.8.2}/tests/test_replies.py +0 -0
  171. {thth-3.8.0 → thth-3.8.2}/tests/test_replies_refresh.py +0 -0
  172. {thth-3.8.0 → thth-3.8.2}/tests/test_repo_lock.py +0 -0
  173. {thth-3.8.0 → thth-3.8.2}/tests/test_repo_sync.py +0 -0
  174. {thth-3.8.0 → thth-3.8.2}/tests/test_report_checkout_import.py +0 -0
  175. {thth-3.8.0 → thth-3.8.2}/tests/test_report_http.py +0 -0
  176. {thth-3.8.0 → thth-3.8.2}/tests/test_report_isolation.py +0 -0
  177. {thth-3.8.0 → thth-3.8.2}/tests/test_report_service.py +0 -0
  178. {thth-3.8.0 → thth-3.8.2}/tests/test_reverse_audit.py +0 -0
  179. {thth-3.8.0 → thth-3.8.2}/tests/test_review_case_id_cluster_m2.py +0 -0
  180. {thth-3.8.0 → thth-3.8.2}/tests/test_review_case_id_cluster_r2.py +0 -0
  181. {thth-3.8.0 → thth-3.8.2}/tests/test_review_cli.py +0 -0
  182. {thth-3.8.0 → thth-3.8.2}/tests/test_review_evidence_account.py +0 -0
  183. {thth-3.8.0 → thth-3.8.2}/tests/test_review_form_spec_and_case_id.py +0 -0
  184. {thth-3.8.0 → thth-3.8.2}/tests/test_review_provenance.py +0 -0
  185. {thth-3.8.0 → thth-3.8.2}/tests/test_review_records.py +0 -0
  186. {thth-3.8.0 → thth-3.8.2}/tests/test_revoke.py +0 -0
  187. {thth-3.8.0 → thth-3.8.2}/tests/test_roundtrip.py +0 -0
  188. {thth-3.8.0 → thth-3.8.2}/tests/test_run_missing_env.py +0 -0
  189. {thth-3.8.0 → thth-3.8.2}/tests/test_run_record_failclosed.py +0 -0
  190. {thth-3.8.0 → thth-3.8.2}/tests/test_runs_collected.py +0 -0
  191. {thth-3.8.0 → thth-3.8.2}/tests/test_runs_read_rows.py +0 -0
  192. {thth-3.8.0 → thth-3.8.2}/tests/test_schedule_multiple.py +0 -0
  193. {thth-3.8.0 → thth-3.8.2}/tests/test_schedule_view.py +0 -0
  194. {thth-3.8.0 → thth-3.8.2}/tests/test_scopes_source_requested.py +0 -0
  195. {thth-3.8.0 → thth-3.8.2}/tests/test_search_engage_targets.py +0 -0
  196. {thth-3.8.0 → thth-3.8.2}/tests/test_search_media_routes.py +0 -0
  197. {thth-3.8.0 → thth-3.8.2}/tests/test_secret_value_redact.py +0 -0
  198. {thth-3.8.0 → thth-3.8.2}/tests/test_secrets_grep.py +0 -0
  199. {thth-3.8.0 → thth-3.8.2}/tests/test_security_small_fixes.py +0 -0
  200. {thth-3.8.0 → thth-3.8.2}/tests/test_select.py +0 -0
  201. {thth-3.8.0 → thth-3.8.2}/tests/test_selfupdate.py +0 -0
  202. {thth-3.8.0 → thth-3.8.2}/tests/test_send.py +0 -0
  203. {thth-3.8.0 → thth-3.8.2}/tests/test_send_cli.py +0 -0
  204. {thth-3.8.0 → thth-3.8.2}/tests/test_send_control_chars.py +0 -0
  205. {thth-3.8.0 → thth-3.8.2}/tests/test_send_media.py +0 -0
  206. {thth-3.8.0 → thth-3.8.2}/tests/test_send_visibility.py +0 -0
  207. {thth-3.8.0 → thth-3.8.2}/tests/test_sent_integrity.py +0 -0
  208. {thth-3.8.0 → thth-3.8.2}/tests/test_server_json.py +0 -0
  209. {thth-3.8.0 → thth-3.8.2}/tests/test_site.py +0 -0
  210. {thth-3.8.0 → thth-3.8.2}/tests/test_skill_doc.py +0 -0
  211. {thth-3.8.0 → thth-3.8.2}/tests/test_study_report.py +0 -0
  212. {thth-3.8.0 → thth-3.8.2}/tests/test_systemd.py +0 -0
  213. {thth-3.8.0 → thth-3.8.2}/tests/test_thread_publish.py +0 -0
  214. {thth-3.8.0 → thth-3.8.2}/tests/test_thread_read.py +0 -0
  215. {thth-3.8.0 → thth-3.8.2}/tests/test_threadrun.py +0 -0
  216. {thth-3.8.0 → thth-3.8.2}/tests/test_threads_read_permissions.py +0 -0
  217. {thth-3.8.0 → thth-3.8.2}/tests/test_threadshape.py +0 -0
  218. {thth-3.8.0 → thth-3.8.2}/tests/test_thth_root.py +0 -0
  219. {thth-3.8.0 → thth-3.8.2}/tests/test_token_exists.py +0 -0
  220. {thth-3.8.0 → thth-3.8.2}/tests/test_token_scopes_record.py +0 -0
  221. {thth-3.8.0 → thth-3.8.2}/tests/test_token_set.py +0 -0
  222. {thth-3.8.0 → thth-3.8.2}/tests/test_topic.py +0 -0
  223. {thth-3.8.0 → thth-3.8.2}/tests/test_topic_advice.py +0 -0
  224. {thth-3.8.0 → thth-3.8.2}/tests/test_topic_cli.py +0 -0
  225. {thth-3.8.0 → thth-3.8.2}/tests/test_topic_models.py +0 -0
  226. {thth-3.8.0 → thth-3.8.2}/tests/test_topic_review_fixes.py +0 -0
  227. {thth-3.8.0 → thth-3.8.2}/tests/test_topic_shelf_observers.py +0 -0
  228. {thth-3.8.0 → thth-3.8.2}/tests/test_topic_store.py +0 -0
  229. {thth-3.8.0 → thth-3.8.2}/tests/test_topics_record_concurrent.py +0 -0
  230. {thth-3.8.0 → thth-3.8.2}/tests/test_topics_tool.py +0 -0
  231. {thth-3.8.0 → thth-3.8.2}/tests/test_trial_frictions.py +0 -0
  232. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_admin_transaction.py +0 -0
  233. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_approve_account.py +0 -0
  234. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_json_contract.py +0 -0
  235. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_length.py +0 -0
  236. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_lock_wait.py +0 -0
  237. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_mastodon_scopes.py +0 -0
  238. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_mentions.py +0 -0
  239. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_post_urls.py +0 -0
  240. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_repo_paths.py +0 -0
  241. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_reposts.py +0 -0
  242. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_study_add.py +0 -0
  243. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_tool_version.py +0 -0
  244. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_unanswered.py +0 -0
  245. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_unanswered_scope.py +0 -0
  246. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_where_arguments.py +0 -0
  247. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_where_filters.py +0 -0
  248. {thth-3.8.0 → thth-3.8.2}/tests/test_v210_who_arguments.py +0 -0
  249. {thth-3.8.0 → thth-3.8.2}/tests/test_v211_auth_integration.py +0 -0
  250. {thth-3.8.0 → thth-3.8.2}/tests/test_v211_authflow.py +0 -0
  251. {thth-3.8.0 → thth-3.8.2}/tests/test_v211_bluesky_stdin.py +0 -0
  252. {thth-3.8.0 → thth-3.8.2}/tests/test_v211_doctor_binding.py +0 -0
  253. {thth-3.8.0 → thth-3.8.2}/tests/test_v211_mastodon_auth.py +0 -0
  254. {thth-3.8.0 → thth-3.8.2}/tests/test_v211_x_auth.py +0 -0
  255. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_approval_relay.py +0 -0
  256. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_ask_coordination.py +0 -0
  257. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_budget.py +0 -0
  258. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_budget_auth.py +0 -0
  259. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_budget_mcp.py +0 -0
  260. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_deletion.py +0 -0
  261. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_job_completion_fault.py +0 -0
  262. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_leave.py +0 -0
  263. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_leave_gate.py +0 -0
  264. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_leave_providers.py +0 -0
  265. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_leave_token_safety.py +0 -0
  266. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_mastodon_guide.py +0 -0
  267. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_mcp_compat.py +0 -0
  268. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_missing_token.py +0 -0
  269. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_read_coordination.py +0 -0
  270. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_server_writes.py +0 -0
  271. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_token_parent.py +0 -0
  272. {thth-3.8.0 → thth-3.8.2}/tests/test_v212_write_storage.py +0 -0
  273. {thth-3.8.0 → thth-3.8.2}/tests/test_v2131_send_dry_run.py +0 -0
  274. {thth-3.8.0 → thth-3.8.2}/tests/test_v2131_threads_publish_wait.py +0 -0
  275. {thth-3.8.0 → thth-3.8.2}/tests/test_v2132_send_inflight_hint.py +0 -0
  276. {thth-3.8.0 → thth-3.8.2}/tests/test_v2132_threads_create_timeout.py +0 -0
  277. {thth-3.8.0 → thth-3.8.2}/tests/test_v2132_threads_create_unknown.py +0 -0
  278. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_acceptance_fixes.py +0 -0
  279. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_adts_asf.py +0 -0
  280. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_approval_attachments.py +0 -0
  281. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_attachment_kinds.py +0 -0
  282. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_bluesky_external.py +0 -0
  283. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_bluesky_media.py +0 -0
  284. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_bluesky_metadata.py +0 -0
  285. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_bluesky_metadata_uri.py +0 -0
  286. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_bluesky_metadata_video.py +0 -0
  287. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_bluesky_record.py +0 -0
  288. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_bluesky_record_video.py +0 -0
  289. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_bluesky_video.py +0 -0
  290. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_bluesky_webm_named.py +0 -0
  291. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_bmff_typed_metadata.py +0 -0
  292. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_budget_lock.py +0 -0
  293. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_capabilities.py +0 -0
  294. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_carryover_timeout.py +0 -0
  295. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_graphemes.py +0 -0
  296. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_auth_media.py +0 -0
  297. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_bmff_audio.py +0 -0
  298. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_flac.py +0 -0
  299. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_gif.py +0 -0
  300. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_link.py +0 -0
  301. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_media.py +0 -0
  302. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_mp3.py +0 -0
  303. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_ogg_opus.py +0 -0
  304. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
  305. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_poll.py +0 -0
  306. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
  307. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_quote.py +0 -0
  308. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_thumbnail.py +0 -0
  309. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_wav.py +0 -0
  310. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_webm_audio.py +0 -0
  311. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_mastodon_webm_video.py +0 -0
  312. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_media_cleanup.py +0 -0
  313. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_media_formats.py +0 -0
  314. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_media_foundation.py +0 -0
  315. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_media_prepared.py +0 -0
  316. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_media_relay.py +0 -0
  317. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_media_relay_refusals.py +0 -0
  318. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_media_upload.py +0 -0
  319. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_receipt_deadline.py +0 -0
  320. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_review_followups.py +0 -0
  321. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_stage810_audit.py +0 -0
  322. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_static_diagnostics.py +0 -0
  323. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_stop_observation.py +0 -0
  324. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_systemd_worker.py +0 -0
  325. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_threads_ghost.py +0 -0
  326. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_threads_gif.py +0 -0
  327. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_threads_link.py +0 -0
  328. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_threads_media.py +0 -0
  329. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_threads_media_spoiler.py +0 -0
  330. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_threads_poll.py +0 -0
  331. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_threads_quote.py +0 -0
  332. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_threads_quote_media.py +0 -0
  333. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_threads_replies.py +0 -0
  334. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_threads_text.py +0 -0
  335. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_threads_text_spoiler.py +0 -0
  336. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_threads_video.py +0 -0
  337. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_transport_review.py +0 -0
  338. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_video_typed_metadata.py +0 -0
  339. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_vorbis_comment_boundary.py +0 -0
  340. {thth-3.8.0 → thth-3.8.2}/tests/test_v213_webm_schema_boundary.py +0 -0
  341. {thth-3.8.0 → thth-3.8.2}/tests/test_v2141_draft_lint_reason.py +0 -0
  342. {thth-3.8.0 → thth-3.8.2}/tests/test_v2141_managed_repo_umask.py +0 -0
  343. {thth-3.8.0 → thth-3.8.2}/tests/test_v214_release_signing.py +0 -0
  344. {thth-3.8.0 → thth-3.8.2}/tests/test_v214_x_adapter.py +0 -0
  345. {thth-3.8.0 → thth-3.8.2}/tests/test_v214_x_auth.py +0 -0
  346. {thth-3.8.0 → thth-3.8.2}/tests/test_v214_x_media.py +0 -0
  347. {thth-3.8.0 → thth-3.8.2}/tests/test_v214_x_posts_budget.py +0 -0
  348. {thth-3.8.0 → thth-3.8.2}/tests/test_v21b_approved_writes.py +0 -0
  349. {thth-3.8.0 → thth-3.8.2}/tests/test_v28_tags.py +0 -0
  350. {thth-3.8.0 → thth-3.8.2}/tests/test_v310_morning.py +0 -0
  351. {thth-3.8.0 → thth-3.8.2}/tests/test_v310_search_timeout.py +0 -0
  352. {thth-3.8.0 → thth-3.8.2}/tests/test_v310_watch_words.py +0 -0
  353. {thth-3.8.0 → thth-3.8.2}/tests/test_v311_leave_same_user.py +0 -0
  354. {thth-3.8.0 → thth-3.8.2}/tests/test_v311_mastodon_retract.py +0 -0
  355. {thth-3.8.0 → thth-3.8.2}/tests/test_v311_morning_overdue.py +0 -0
  356. {thth-3.8.0 → thth-3.8.2}/tests/test_v311_morning_own_exclusion.py +0 -0
  357. {thth-3.8.0 → thth-3.8.2}/tests/test_v311_morning_unanswered_window.py +0 -0
  358. {thth-3.8.0 → thth-3.8.2}/tests/test_v311_notifications_guidance.py +0 -0
  359. {thth-3.8.0 → thth-3.8.2}/tests/test_v311_replies_owned.py +0 -0
  360. {thth-3.8.0 → thth-3.8.2}/tests/test_v312_leave_shared_token.py +0 -0
  361. {thth-3.8.0 → thth-3.8.2}/tests/test_v312_own_handles_stopped.py +0 -0
  362. {thth-3.8.0 → thth-3.8.2}/tests/test_v312_report_admin.py +0 -0
  363. {thth-3.8.0 → thth-3.8.2}/tests/test_v312_report_channel.py +0 -0
  364. {thth-3.8.0 → thth-3.8.2}/tests/test_v312_report_inbox.py +0 -0
  365. {thth-3.8.0 → thth-3.8.2}/tests/test_v312_report_mcp_user.py +0 -0
  366. {thth-3.8.0 → thth-3.8.2}/tests/test_v31_analysis.py +0 -0
  367. {thth-3.8.0 → thth-3.8.2}/tests/test_v31_collection.py +0 -0
  368. {thth-3.8.0 → thth-3.8.2}/tests/test_v320_reply_to_file_display.py +0 -0
  369. {thth-3.8.0 → thth-3.8.2}/tests/test_v320_reply_to_file_lint.py +0 -0
  370. {thth-3.8.0 → thth-3.8.2}/tests/test_v320_reply_to_file_resolve.py +0 -0
  371. {thth-3.8.0 → thth-3.8.2}/tests/test_v320_report_fixes.py +0 -0
  372. {thth-3.8.0 → thth-3.8.2}/tests/test_v330_fingerprint_version.py +0 -0
  373. {thth-3.8.0 → thth-3.8.2}/tests/test_v330_held.py +0 -0
  374. {thth-3.8.0 → thth-3.8.2}/tests/test_v330_held_names.py +0 -0
  375. {thth-3.8.0 → thth-3.8.2}/tests/test_v330_last_refusals.py +0 -0
  376. {thth-3.8.0 → thth-3.8.2}/tests/test_v330_notification_route.py +0 -0
  377. {thth-3.8.0 → thth-3.8.2}/tests/test_v330_observe.py +0 -0
  378. {thth-3.8.0 → thth-3.8.2}/tests/test_v330_report_friction.py +0 -0
  379. {thth-3.8.0 → thth-3.8.2}/tests/test_v330_report_words.py +0 -0
  380. {thth-3.8.0 → thth-3.8.2}/tests/test_v330_reports_effect.py +0 -0
  381. {thth-3.8.0 → thth-3.8.2}/tests/test_v331_inflight_cli.py +0 -0
  382. {thth-3.8.0 → thth-3.8.2}/tests/test_v331_listing_resolve.py +0 -0
  383. {thth-3.8.0 → thth-3.8.2}/tests/test_v331_outbox_retention.py +0 -0
  384. {thth-3.8.0 → thth-3.8.2}/tests/test_v331_publish_status.py +0 -0
  385. {thth-3.8.0 → thth-3.8.2}/tests/test_v331_self_resolve.py +0 -0
  386. {thth-3.8.0 → thth-3.8.2}/tests/test_v340_plaza_cli.py +0 -0
  387. {thth-3.8.0 → thth-3.8.2}/tests/test_v340_plaza_docs.py +0 -0
  388. {thth-3.8.0 → thth-3.8.2}/tests/test_v340_plaza_fields.py +0 -0
  389. {thth-3.8.0 → thth-3.8.2}/tests/test_v340_plaza_mcp.py +0 -0
  390. {thth-3.8.0 → thth-3.8.2}/tests/test_v340_plaza_morning.py +0 -0
  391. {thth-3.8.0 → thth-3.8.2}/tests/test_v340_plaza_observe.py +0 -0
  392. {thth-3.8.0 → thth-3.8.2}/tests/test_v340_plaza_open.py +0 -0
  393. {thth-3.8.0 → thth-3.8.2}/tests/test_v340_plaza_open_confirm.py +0 -0
  394. {thth-3.8.0 → thth-3.8.2}/tests/test_v340_plaza_reply.py +0 -0
  395. {thth-3.8.0 → thth-3.8.2}/tests/test_v340_plaza_store.py +0 -0
  396. {thth-3.8.0 → thth-3.8.2}/tests/test_v350_map_docs.py +0 -0
  397. {thth-3.8.0 → thth-3.8.2}/tests/test_v350_map_nodes.py +0 -0
  398. {thth-3.8.0 → thth-3.8.2}/tests/test_v350_map_observe.py +0 -0
  399. {thth-3.8.0 → thth-3.8.2}/tests/test_v350_map_plaza.py +0 -0
  400. {thth-3.8.0 → thth-3.8.2}/tests/test_v350_map_retention.py +0 -0
  401. {thth-3.8.0 → thth-3.8.2}/tests/test_v350_map_self.py +0 -0
  402. {thth-3.8.0 → thth-3.8.2}/tests/test_v350_map_show.py +0 -0
  403. {thth-3.8.0 → thth-3.8.2}/tests/test_v350_map_world.py +0 -0
  404. {thth-3.8.0 → thth-3.8.2}/tests/test_v351_leave_timers.py +0 -0
  405. {thth-3.8.0 → thth-3.8.2}/tests/test_v351_plaza_open_holes.py +0 -0
  406. {thth-3.8.0 → thth-3.8.2}/tests/test_v351_who_owned.py +0 -0
  407. {thth-3.8.0 → thth-3.8.2}/tests/test_v360_goal_analytics.py +0 -0
  408. {thth-3.8.0 → thth-3.8.2}/tests/test_v360_goal_docs.py +0 -0
  409. {thth-3.8.0 → thth-3.8.2}/tests/test_v360_goal_observe_map.py +0 -0
  410. {thth-3.8.0 → thth-3.8.2}/tests/test_v360_goal_plaza.py +0 -0
  411. {thth-3.8.0 → thth-3.8.2}/tests/test_v360_goal_record.py +0 -0
  412. {thth-3.8.0 → thth-3.8.2}/tests/test_v360_goal_send.py +0 -0
  413. {thth-3.8.0 → thth-3.8.2}/tests/test_v360_report_timeline.py +0 -0
  414. {thth-3.8.0 → thth-3.8.2}/tests/test_v370_account_self_healing.py +0 -0
  415. {thth-3.8.0 → thth-3.8.2}/tests/test_v370_approve_pending.py +0 -0
  416. {thth-3.8.0 → thth-3.8.2}/tests/test_v370_click_attribution.py +0 -0
  417. {thth-3.8.0 → thth-3.8.2}/tests/test_v370_collection_stale_basis.py +0 -0
  418. {thth-3.8.0 → thth-3.8.2}/tests/test_v370_docs.py +0 -0
  419. {thth-3.8.0 → thth-3.8.2}/tests/test_v370_follow_comparison.py +0 -0
  420. {thth-3.8.0 → thth-3.8.2}/tests/test_v370_local_path.py +0 -0
  421. {thth-3.8.0 → thth-3.8.2}/tests/test_v370_weekly_goals.py +0 -0
  422. {thth-3.8.0 → thth-3.8.2}/tests/test_v370_where_aggregate.py +0 -0
  423. {thth-3.8.0 → thth-3.8.2}/tests/test_v370_where_also.py +0 -0
  424. {thth-3.8.0 → thth-3.8.2}/tests/test_v370_where_zero.py +0 -0
  425. {thth-3.8.0 → thth-3.8.2}/tests/test_v380_docs.py +0 -0
  426. {thth-3.8.0 → thth-3.8.2}/tests/test_v380_plaza_digest.py +0 -0
  427. {thth-3.8.0 → thth-3.8.2}/tests/test_v380_plaza_from.py +0 -0
  428. {thth-3.8.0 → thth-3.8.2}/tests/test_v380_plaza_mcp.py +0 -0
  429. {thth-3.8.0 → thth-3.8.2}/tests/test_v380_plaza_received.py +0 -0
  430. {thth-3.8.0 → thth-3.8.2}/tests/test_v380_plaza_triggers.py +0 -0
  431. {thth-3.8.0 → thth-3.8.2}/tests/test_value_domains_shown.py +0 -0
  432. {thth-3.8.0 → thth-3.8.2}/tests/test_version.py +0 -0
  433. {thth-3.8.0 → thth-3.8.2}/tests/test_where_cli.py +0 -0
  434. {thth-3.8.0 → thth-3.8.2}/tests/test_who_cli.py +0 -0
  435. {thth-3.8.0 → thth-3.8.2}/tests/test_writeback_rebase.py +0 -0
  436. {thth-3.8.0 → thth-3.8.2}/tests/test_writeback_validate.py +0 -0
  437. {thth-3.8.0 → thth-3.8.2}/tests/test_x_metrics.py +0 -0
  438. {thth-3.8.0 → thth-3.8.2}/thth/__init__.py +0 -0
  439. {thth-3.8.0 → thth-3.8.2}/thth/__main__.py +0 -0
  440. {thth-3.8.0 → thth-3.8.2}/thth/_grapheme_data.py +0 -0
  441. {thth-3.8.0 → thth-3.8.2}/thth/account_cli.py +0 -0
  442. {thth-3.8.0 → thth-3.8.2}/thth/account_report.py +0 -0
  443. {thth-3.8.0 → thth-3.8.2}/thth/accounts.py +0 -0
  444. {thth-3.8.0 → thth-3.8.2}/thth/adapters/__init__.py +0 -0
  445. {thth-3.8.0 → thth-3.8.2}/thth/adapters/auth_mastodon.py +0 -0
  446. {thth-3.8.0 → thth-3.8.2}/thth/adapters/auth_threads.py +0 -0
  447. {thth-3.8.0 → thth-3.8.2}/thth/adapters/auth_x.py +0 -0
  448. {thth-3.8.0 → thth-3.8.2}/thth/adapters/base.py +0 -0
  449. {thth-3.8.0 → thth-3.8.2}/thth/adapters/bluesky.py +0 -0
  450. {thth-3.8.0 → thth-3.8.2}/thth/adapters/bluesky_media.py +0 -0
  451. {thth-3.8.0 → thth-3.8.2}/thth/adapters/bluesky_video.py +0 -0
  452. {thth-3.8.0 → thth-3.8.2}/thth/adapters/mastodon.py +0 -0
  453. {thth-3.8.0 → thth-3.8.2}/thth/adapters/mastodon_media.py +0 -0
  454. {thth-3.8.0 → thth-3.8.2}/thth/adapters/threads.py +0 -0
  455. {thth-3.8.0 → thth-3.8.2}/thth/adapters/threads_media.py +0 -0
  456. {thth-3.8.0 → thth-3.8.2}/thth/adapters/x.py +0 -0
  457. {thth-3.8.0 → thth-3.8.2}/thth/adapters/x_media.py +0 -0
  458. {thth-3.8.0 → thth-3.8.2}/thth/admin_log.py +0 -0
  459. {thth-3.8.0 → thth-3.8.2}/thth/admin_notifications.py +0 -0
  460. {thth-3.8.0 → thth-3.8.2}/thth/admin_report.py +0 -0
  461. {thth-3.8.0 → thth-3.8.2}/thth/after_cli.py +0 -0
  462. {thth-3.8.0 → thth-3.8.2}/thth/analytics_comparison.py +0 -0
  463. {thth-3.8.0 → thth-3.8.2}/thth/analytics_goals.py +0 -0
  464. {thth-3.8.0 → thth-3.8.2}/thth/analytics_report.py +0 -0
  465. {thth-3.8.0 → thth-3.8.2}/thth/analytics_shapes.py +0 -0
  466. {thth-3.8.0 → thth-3.8.2}/thth/analytics_threads.py +0 -0
  467. {thth-3.8.0 → thth-3.8.2}/thth/analytics_weekly.py +0 -0
  468. {thth-3.8.0 → thth-3.8.2}/thth/api_diagnostic.py +0 -0
  469. {thth-3.8.0 → thth-3.8.2}/thth/appconfig.py +0 -0
  470. {thth-3.8.0 → thth-3.8.2}/thth/appenv.py +0 -0
  471. {thth-3.8.0 → thth-3.8.2}/thth/approval.py +0 -0
  472. {thth-3.8.0 → thth-3.8.2}/thth/approval_jobs.py +0 -0
  473. {thth-3.8.0 → thth-3.8.2}/thth/approval_relay.py +0 -0
  474. {thth-3.8.0 → thth-3.8.2}/thth/approve_pending.py +0 -0
  475. {thth-3.8.0 → thth-3.8.2}/thth/ask.py +0 -0
  476. {thth-3.8.0 → thth-3.8.2}/thth/ask_cli.py +0 -0
  477. {thth-3.8.0 → thth-3.8.2}/thth/audioformats.py +0 -0
  478. {thth-3.8.0 → thth-3.8.2}/thth/authclients.py +0 -0
  479. {thth-3.8.0 → thth-3.8.2}/thth/authflow.py +0 -0
  480. {thth-3.8.0 → thth-3.8.2}/thth/authpassword.py +0 -0
  481. {thth-3.8.0 → thth-3.8.2}/thth/bluesky_metadata.py +0 -0
  482. {thth-3.8.0 → thth-3.8.2}/thth/budget_x.py +0 -0
  483. {thth-3.8.0 → thth-3.8.2}/thth/budget_x_posts.py +0 -0
  484. {thth-3.8.0 → thth-3.8.2}/thth/bundle.py +0 -0
  485. {thth-3.8.0 → thth-3.8.2}/thth/click_attribution.py +0 -0
  486. {thth-3.8.0 → thth-3.8.2}/thth/collect.py +0 -0
  487. {thth-3.8.0 → thth-3.8.2}/thth/collection_context.py +0 -0
  488. {thth-3.8.0 → thth-3.8.2}/thth/collection_status.py +0 -0
  489. {thth-3.8.0 → thth-3.8.2}/thth/core.py +0 -0
  490. {thth-3.8.0 → thth-3.8.2}/thth/deletion.py +0 -0
  491. {thth-3.8.0 → thth-3.8.2}/thth/doctor.py +0 -0
  492. {thth-3.8.0 → thth-3.8.2}/thth/ebmlaudio.py +0 -0
  493. {thth-3.8.0 → thth-3.8.2}/thth/ebmlvideo.py +0 -0
  494. {thth-3.8.0 → thth-3.8.2}/thth/engagements.py +0 -0
  495. {thth-3.8.0 → thth-3.8.2}/thth/fingerprint_golden.py +0 -0
  496. {thth-3.8.0 → thth-3.8.2}/thth/flacformats.py +0 -0
  497. {thth-3.8.0 → thth-3.8.2}/thth/forms.py +0 -0
  498. {thth-3.8.0 → thth-3.8.2}/thth/goals.py +0 -0
  499. {thth-3.8.0 → thth-3.8.2}/thth/graphemes.py +0 -0
  500. {thth-3.8.0 → thth-3.8.2}/thth/handoff_cursor.py +0 -0
  501. {thth-3.8.0 → thth-3.8.2}/thth/healthcheck.py +0 -0
  502. {thth-3.8.0 → thth-3.8.2}/thth/httpsafe.py +0 -0
  503. {thth-3.8.0 → thth-3.8.2}/thth/incident.py +0 -0
  504. {thth-3.8.0 → thth-3.8.2}/thth/inflight.py +0 -0
  505. {thth-3.8.0 → thth-3.8.2}/thth/inflight_cli.py +0 -0
  506. {thth-3.8.0 → thth-3.8.2}/thth/inflight_resolve.py +0 -0
  507. {thth-3.8.0 → thth-3.8.2}/thth/jst.py +0 -0
  508. {thth-3.8.0 → thth-3.8.2}/thth/leave.py +0 -0
  509. {thth-3.8.0 → thth-3.8.2}/thth/leave_gate.py +0 -0
  510. {thth-3.8.0 → thth-3.8.2}/thth/lint.py +0 -0
  511. {thth-3.8.0 → thth-3.8.2}/thth/lock.py +0 -0
  512. {thth-3.8.0 → thth-3.8.2}/thth/maintain.py +0 -0
  513. {thth-3.8.0 → thth-3.8.2}/thth/managed_repo.py +0 -0
  514. {thth-3.8.0 → thth-3.8.2}/thth/map_cli.py +0 -0
  515. {thth-3.8.0 → thth-3.8.2}/thth/map_store.py +0 -0
  516. {thth-3.8.0 → thth-3.8.2}/thth/map_view.py +0 -0
  517. {thth-3.8.0 → thth-3.8.2}/thth/map_world.py +0 -0
  518. {thth-3.8.0 → thth-3.8.2}/thth/measured.py +0 -0
  519. {thth-3.8.0 → thth-3.8.2}/thth/media.py +0 -0
  520. {thth-3.8.0 → thth-3.8.2}/thth/media_capabilities.py +0 -0
  521. {thth-3.8.0 → thth-3.8.2}/thth/media_cleanup.py +0 -0
  522. {thth-3.8.0 → thth-3.8.2}/thth/media_delivery.py +0 -0
  523. {thth-3.8.0 → thth-3.8.2}/thth/media_relay.py +0 -0
  524. {thth-3.8.0 → thth-3.8.2}/thth/media_uploads.py +0 -0
  525. {thth-3.8.0 → thth-3.8.2}/thth/mediaformats.py +0 -0
  526. {thth-3.8.0 → thth-3.8.2}/thth/morning.py +0 -0
  527. {thth-3.8.0 → thth-3.8.2}/thth/mpeg_audio.py +0 -0
  528. {thth-3.8.0 → thth-3.8.2}/thth/notification_route.py +0 -0
  529. {thth-3.8.0 → thth-3.8.2}/thth/oauth.py +0 -0
  530. {thth-3.8.0 → thth-3.8.2}/thth/oggformats.py +0 -0
  531. {thth-3.8.0 → thth-3.8.2}/thth/operations_handoff.py +0 -0
  532. {thth-3.8.0 → thth-3.8.2}/thth/plaza_from.py +0 -0
  533. {thth-3.8.0 → thth-3.8.2}/thth/plaza_moments.py +0 -0
  534. {thth-3.8.0 → thth-3.8.2}/thth/plaza_observe.py +0 -0
  535. {thth-3.8.0 → thth-3.8.2}/thth/plaza_reads.py +0 -0
  536. {thth-3.8.0 → thth-3.8.2}/thth/plaza_redact.py +0 -0
  537. {thth-3.8.0 → thth-3.8.2}/thth/postid.py +0 -0
  538. {thth-3.8.0 → thth-3.8.2}/thth/private_store.py +0 -0
  539. {thth-3.8.0 → thth-3.8.2}/thth/queuefile.py +0 -0
  540. {thth-3.8.0 → thth-3.8.2}/thth/read_coordination.py +0 -0
  541. {thth-3.8.0 → thth-3.8.2}/thth/read_window.py +0 -0
  542. {thth-3.8.0 → thth-3.8.2}/thth/redact.py +0 -0
  543. {thth-3.8.0 → thth-3.8.2}/thth/replies.py +0 -0
  544. {thth-3.8.0 → thth-3.8.2}/thth/report.py +0 -0
  545. {thth-3.8.0 → thth-3.8.2}/thth/report_details.py +0 -0
  546. {thth-3.8.0 → thth-3.8.2}/thth/report_http.py +0 -0
  547. {thth-3.8.0 → thth-3.8.2}/thth/report_inbox.py +0 -0
  548. {thth-3.8.0 → thth-3.8.2}/thth/report_isolation.py +0 -0
  549. {thth-3.8.0 → thth-3.8.2}/thth/report_timeline.py +0 -0
  550. {thth-3.8.0 → thth-3.8.2}/thth/retract_cli.py +0 -0
  551. {thth-3.8.0 → thth-3.8.2}/thth/runs.py +0 -0
  552. {thth-3.8.0 → thth-3.8.2}/thth/scopes.py +0 -0
  553. {thth-3.8.0 → thth-3.8.2}/thth/secrets_fs.py +0 -0
  554. {thth-3.8.0 → thth-3.8.2}/thth/select.py +0 -0
  555. {thth-3.8.0 → thth-3.8.2}/thth/selfupdate.py +0 -0
  556. {thth-3.8.0 → thth-3.8.2}/thth/sent.py +0 -0
  557. {thth-3.8.0 → thth-3.8.2}/thth/server_files.py +0 -0
  558. {thth-3.8.0 → thth-3.8.2}/thth/server_writes.py +0 -0
  559. {thth-3.8.0 → thth-3.8.2}/thth/stop_observation.py +0 -0
  560. {thth-3.8.0 → thth-3.8.2}/thth/study_cli.py +0 -0
  561. {thth-3.8.0 → thth-3.8.2}/thth/study_report.py +0 -0
  562. {thth-3.8.0 → thth-3.8.2}/thth/systemd_gen.py +0 -0
  563. {thth-3.8.0 → thth-3.8.2}/thth/tags.py +0 -0
  564. {thth-3.8.0 → thth-3.8.2}/thth/thread_read.py +0 -0
  565. {thth-3.8.0 → thth-3.8.2}/thth/threadrun.py +0 -0
  566. {thth-3.8.0 → thth-3.8.2}/thth/threads_read_cli.py +0 -0
  567. {thth-3.8.0 → thth-3.8.2}/thth/threadshape.py +0 -0
  568. {thth-3.8.0 → thth-3.8.2}/thth/threadthrow.py +0 -0
  569. {thth-3.8.0 → thth-3.8.2}/thth/timer_cleanup.py +0 -0
  570. {thth-3.8.0 → thth-3.8.2}/thth/tool_version.py +0 -0
  571. {thth-3.8.0 → thth-3.8.2}/thth/topic_advice.py +0 -0
  572. {thth-3.8.0 → thth-3.8.2}/thth/topic_cli.py +0 -0
  573. {thth-3.8.0 → thth-3.8.2}/thth/topic_models.py +0 -0
  574. {thth-3.8.0 → thth-3.8.2}/thth/topic_store.py +0 -0
  575. {thth-3.8.0 → thth-3.8.2}/thth/topics.py +0 -0
  576. {thth-3.8.0 → thth-3.8.2}/thth/unanswered.py +0 -0
  577. {thth-3.8.0 → thth-3.8.2}/thth/vorbisformats.py +0 -0
  578. {thth-3.8.0 → thth-3.8.2}/thth/watch_cli.py +0 -0
  579. {thth-3.8.0 → thth-3.8.2}/thth/where_cli.py +0 -0
  580. {thth-3.8.0 → thth-3.8.2}/thth/who_cli.py +0 -0
  581. {thth-3.8.0 → thth-3.8.2}/thth/x_metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thth
3
- Version: 3.8.0
3
+ Version: 3.8.2
4
4
  Summary: 承認済みの投稿だけを投げる門(Threads・Bluesky・Mastodon)。人が承認し、記録は利用者の git に残る。
5
5
  Project-URL: Homepage, https://thth.me
6
6
  Project-URL: Source, https://github.com/aokings/thth
@@ -717,7 +717,9 @@ PLAZA_TOOLS = [
717
717
  "how〔数字を出し直せる thth の命令〕が必須)・気づき(finding)・問い(question)を"
718
718
  "置く。scope(媒体・企画の範囲)は必須。置いたものは同じ持ち主の全 account だけに"
719
719
  "見える(visibility: owner は管理者が登録した持ち主の組の全 project に一段で見せる。"
720
- "他の持ち主に見せる open は人が CLI の二段確認で行う・この口には無い)。"
720
+ "project が組に入っていれば visibility 省略の既定は owner・入っていなければ"
721
+ "従前どおり project(設計 3.8.1)。他の持ち主に見せる open は人が CLI の二段確認"
722
+ "で行う・この口には無い)。"
721
723
  "evidence_level の observed は道具だけが付ける。from_tool(analytics-report・after・"
722
724
  "study-report)・from_doc・from_report で、道具がその出力を置く時点で作り直して"
723
725
  "数字を観測の欄と本文の下書きに入れる(body は任意の解釈)",
@@ -738,7 +740,8 @@ PLAZA_TOOLS = [
738
740
  "goal": {"type": "string", "enum": ["reach", "click", "follow", "reply"],
739
741
  "description": "施策の目的(任意)。媒体をまたいで同じ目的の施策を比べる札"},
740
742
  "visibility": {"type": "string", "enum": ["project", "owner"],
741
- "description": "project(既定)か owner(持ち主の組・管理者が組を登録したときだけ)"},
743
+ "description": "project か owner(持ち主の組・管理者が組を登録したときだけ)。"
744
+ "省略時は project が組に入っていれば owner・入っていなければ project"},
742
745
  "trial_due": {"type": "string", "description": "finding の追試の予定日(2026-10-08 か timezone 付きの時刻)"},
743
746
  "from_tool": {"type": "string", "enum": ["analytics-report", "after", "study-report"],
744
747
  "description": "道具の出力から置く(analytics-report・after は from_account・"
@@ -211,10 +211,14 @@ def test_listが自分の書き込みだけなら管理者に頼む命令の形
211
211
  post(account="kopicha-bsky", title="自分の持ち主の")
212
212
  payload = plaza.list_posts(viewer("kopicha-threads"))
213
213
  assert payload["hint"]["reason"] == "only_own_posts"
214
- # 組と参加がそろえば、頼むことが無いので何も言わない。
214
+ # 組と参加がそろっても、組の他の project がまだ owner 範囲に何も置いていなければ、
215
+ # 頼むことは無いのでその旨だけ言う(「組の登録を頼んで」ではない・設計 3.8.1)。
215
216
  plaza.set_owner("masaru", ["kopicha", "other"], by="operator")
216
217
  plaza.set_membership("kopicha", joined=True, by="operator")
217
- assert plaza.list_posts(viewer("kopicha-threads"))["hint"] is None
218
+ hint = plaza.list_posts(viewer("kopicha-threads"))["hint"]
219
+ assert hint == {"reason": "only_own_posts", "commands": [],
220
+ "line": "広場: 組(masaru)の他の project の書き込みは owner 範囲にした"
221
+ "ものだけ見えます。まだ 0 件です"}
218
222
  # 組の相手の書き込みが読めるなら案内は出さない。
219
223
  plaza.set_membership("kopicha", joined=False, by="operator")
220
224
  post(account="other-threads", title="組", by="o", visibility="owner")
@@ -63,8 +63,9 @@ def test_組に登録したprojectだけにownerの書き込みが見える(grou
63
63
  assert plaza.list_posts(viewer("third-threads"))["n"] == 0
64
64
  third_text = json.dumps(plaza.list_posts(viewer("third-threads")), ensure_ascii=False)
65
65
  assert "OWNER-TITLE" not in third_text
66
- # project の範囲の書き込みは組の相手にも見せない(従前どおり)。
67
- inside = post(title="PROJECT-ONLY-2")
66
+ # project の範囲の書き込みは組の相手にも見せない(従前どおり)。組があるので既定は
67
+ # owner に変わった(設計 3.8.1)ため、ここは project を明示して確かめる。
68
+ inside = post(title="PROJECT-ONLY-2", visibility="project")
68
69
  with pytest.raises(plaza.PlazaError, match="^plaza_not_found$"):
69
70
  plaza.show(inside["plaza_id"], viewer("other-threads"))
70
71
 
@@ -181,7 +182,8 @@ def test_observeの0段に組の新着(grouped, monkeypatch):
181
182
  now = jst.now_jst()
182
183
  plaza.set_owner("masaru", ["kopicha", "other"], by="operator")
183
184
  post(title="組の気づき", visibility="owner", now=now - datetime.timedelta(hours=1))
184
- post(title="PROJECT-INSIDE", now=now - datetime.timedelta(hours=1))
185
+ # 組があるので既定は owner に変わった(設計 3.8.1)ため、project 側の 1 件は明示する。
186
+ post(title="PROJECT-INSIDE", visibility="project", now=now - datetime.timedelta(hours=1))
185
187
  cell = next(s for s in morning.build("other-threads", now=now, mark=False)["sections"]
186
188
  if s["section"] == "tool")["value"]["plaza"]
187
189
  assert cell["owner_new"] == 1 and cell["owner_denominator"] == 1
@@ -0,0 +1,100 @@
1
+ """3.8.1 visibility の既定は project の持ち主の組次第(設計 3.8.1)。
2
+
3
+ 見るのは:
4
+ - project が持ち主の組に入っていれば、CLI の `plaza post`(--open・--owner 無し)と
5
+ MCP の `thth_plaza_post`(visibility 省略)の既定は owner に変わる。
6
+ - 組に入っていない project は従前どおり既定が project(回帰固定)。
7
+ - open は明示(`--open`/二段確認)が無ければ決して既定にならない(回帰固定・
8
+ 組があっても・open に参加していても)。
9
+ - 明示の `--owner`/MCP の `visibility` があれば、組があってもそれに従う。
10
+ - 既定で owner にしたときだけ応答に「範囲: owner(組 <名前> の既定)」と `visibility_default`。
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+
16
+ from thth import cli, plaza
17
+ from tests.test_v340_plaza_store import owners # noqa: F401 (fixture)
18
+ from tests.test_v340_plaza_mcp import _post, _tenant, _text
19
+ from tests.test_v380_plaza_owner import grouped # noqa: F401 (fixture)
20
+
21
+
22
+ def _body(tmp_path, text="本文です。朝の投稿で試した", name="body.txt"):
23
+ path = tmp_path / name
24
+ path.write_text(text, encoding="utf-8")
25
+ return str(path)
26
+
27
+
28
+ def _cli_post(tmp_path, capsys, account, title, *, extra=()):
29
+ rc = cli.main(["plaza", "post", account, "--kind", "finding", "--title", title,
30
+ "--body-file", _body(tmp_path, name=title + ".txt"),
31
+ "--scope", "Threads の朝の投稿", "--by", "k", *extra, "--json"])
32
+ out = capsys.readouterr().out
33
+ return rc, (json.loads(out) if out.strip() else None)
34
+
35
+
36
+ def test_CLI_組が無いうちは既定がproject_組に入れば既定がowner(grouped, capsys, tmp_path):
37
+ # 組が無い間は従前どおり既定が project(回帰固定)。
38
+ rc, posted = _cli_post(tmp_path, capsys, "kopicha-threads", "組なし")
39
+ assert rc == 0 and posted["scope"] == "project" and posted["visibility_default"] is None
40
+
41
+ plaza.set_owner("masaru", ["kopicha", "other"], by="operator")
42
+ rc, posted = _cli_post(tmp_path, capsys, "kopicha-threads", "組あり")
43
+ assert rc == 0 and posted["scope"] == "owner" and posted["visibility_default"] == "masaru"
44
+
45
+ # 組に入っていない project(third)は既定が project のまま(回帰固定)。
46
+ rc, posted = _cli_post(tmp_path, capsys, "third-threads", "組の外")
47
+ assert rc == 0 and posted["scope"] == "project" and posted["visibility_default"] is None
48
+
49
+ # 明示の --owner があれば、組が無くても(既に組があっても)それに従う。
50
+ rc, posted = _cli_post(tmp_path, capsys, "kopicha-threads", "明示のowner",
51
+ extra=("--owner",))
52
+ assert rc == 0 and posted["scope"] == "owner" and posted["visibility_default"] is None
53
+
54
+
55
+ def test_CLIの人向け応答に範囲の既定を明記する(grouped, capsys, tmp_path):
56
+ plaza.set_owner("masaru", ["kopicha", "other"], by="operator")
57
+ rc = cli.main(["plaza", "post", "kopicha-threads", "--kind", "finding", "--title", "人向け",
58
+ "--body-file", _body(tmp_path), "--scope", "Threads の朝の投稿", "--by", "k"])
59
+ out = capsys.readouterr().out
60
+ assert rc == 0 and "範囲: owner(組 masaru の既定)" in out
61
+ # 明示の --owner のときは既定の案内を出さない(すでに自分で選んでいる)。
62
+ rc = cli.main(["plaza", "post", "kopicha-threads", "--kind", "finding", "--title", "明示",
63
+ "--body-file", _body(tmp_path, name="明示.txt"), "--scope", "Threads の朝の投稿",
64
+ "--by", "k", "--owner"])
65
+ out = capsys.readouterr().out
66
+ assert rc == 0 and "範囲: owner(組" not in out
67
+
68
+
69
+ def test_openは組があっても参加していても既定にならない(grouped, capsys, tmp_path):
70
+ plaza.set_owner("masaru", ["kopicha", "other"], by="operator")
71
+ plaza.set_membership("kopicha", joined=True, by="operator")
72
+ rc, posted = _cli_post(tmp_path, capsys, "kopicha-threads", "参加中でも既定はowner")
73
+ assert rc == 0 and posted["scope"] == "owner"
74
+ # --open は今までどおり明示 + 二段確認が要る(既定にはならない)。
75
+ rc = cli.main(["plaza", "post", "kopicha-threads", "--kind", "finding", "--title", "openは明示",
76
+ "--body-file", _body(tmp_path, name="open.txt"), "--scope", "Threads の朝の投稿",
77
+ "--by", "k", "--open", "--json"])
78
+ out = json.loads(capsys.readouterr().out)
79
+ # 一段目は何も書かずに終わる(approve の一段目と同じ rc 1・`_emit_preview`)。
80
+ assert rc == 1 and out["report_type"] == "plaza_open_preview" and out["opened"] is False
81
+
82
+
83
+ def test_MCPのvisibility省略は組次第_明示すれば従う(tmp_path, monkeypatch):
84
+ _root, server = _tenant(tmp_path, monkeypatch)
85
+ # 組が無ければ従前どおり project(回帰固定)。
86
+ posted = json.loads(_text(_post(server)))
87
+ assert posted["scope"] == "project" and posted["visibility_default"] is None
88
+
89
+ plaza.set_owner("masaru", ["kopicha", "other"], by="operator")
90
+ # 省略すれば owner が既定。
91
+ defaulted = json.loads(_text(_post(server, title="省略で owner")))
92
+ assert defaulted["scope"] == "owner" and defaulted["visibility_default"] == "masaru"
93
+
94
+ # 明示すればそれに従う(組があっても project のまま)。
95
+ explicit = json.loads(_text(_post(server, title="明示は project", visibility="project")))
96
+ assert explicit["scope"] == "project" and explicit["visibility_default"] is None
97
+
98
+ # open は MCP の口に無い(既定にもならない・従前どおり)。
99
+ denied = _post(server, title="open は無い", visibility="open")
100
+ assert denied["isError"] and _text(denied) == "open_requires_cli"
@@ -0,0 +1,218 @@
1
+ """3.8.2 件 2: `thth approve --confirm-file <file> --by <名前>`(複数の確定を 1 回で)。
2
+
3
+ - 1 行に `<原稿のパス> <digest>`。1 回の同期と 1 回のロックの中で行の順に確定する。
4
+ - digest が合わない本はその 1 本だけ断って他は進める(断った本と理由は最後に並べる)。
5
+ - commit は 1 本ずつ・push は最後に 1 回。
6
+ - 同期・push で止まったら、どこまで通ったかと残りをそのまま打てる命令を出す。
7
+ - 1 段目(複数のとき)に `--confirm-file` の中身の形を 1 行。
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import shlex
14
+
15
+ import pytest
16
+
17
+ from tests.conftest import run_git, run_thth, write_queue_file
18
+ from thth import cli
19
+ from thth import lock as lock_mod
20
+ from thth import writeback
21
+
22
+ NAMES = ["a.md", "b.md", "c.md"]
23
+
24
+
25
+ def _digest(stdout: str) -> str:
26
+ return [line.split(": ", 1)[1].strip() for line in stdout.splitlines()
27
+ if line.startswith("digest: ")][0]
28
+
29
+
30
+ @pytest.fixture
31
+ def drafts(isolated_account, tmp_path):
32
+ """3 本の draft(commit・push 済み)と、それぞれの 1 段目の digest。"""
33
+ paths = [write_queue_file(isolated_account["queue_dir"], name,
34
+ body=f"## threads\n\n{name} の本文です。\n",
35
+ fm_overrides={"account": isolated_account["name"], "status": "draft",
36
+ "approved_sha": None})
37
+ for name in NAMES]
38
+ digests = []
39
+ for path in paths:
40
+ first = run_thth(["approve", path])
41
+ assert first.returncode == 1, first.stderr
42
+ digests.append(_digest(first.stdout))
43
+ work = isolated_account["repo_dir"]
44
+ return {"paths": paths, "digests": digests, "work": work,
45
+ "bare": os.path.join(os.path.dirname(work), "origin.git"),
46
+ "seed": os.path.join(os.path.dirname(work), "seed"),
47
+ "file": str(tmp_path / "confirm.txt")}
48
+
49
+
50
+ def _write_confirm_file(drafts, digests=None, extra_lines=()) -> str:
51
+ digests = digests or drafts["digests"]
52
+ lines = ["# 確定の一覧(masaru)", ""]
53
+ lines += [f"{path} {digest}" for path, digest in zip(drafts["paths"], digests)]
54
+ lines += list(extra_lines)
55
+ with open(drafts["file"], "w", encoding="utf-8") as stream:
56
+ stream.write("\n".join(lines) + "\n")
57
+ return drafts["file"]
58
+
59
+
60
+ def _origin_status(drafts, name: str) -> str:
61
+ shown = run_git(drafts["bare"], ["show", f"main:docs/sns/queue/{name}"]).stdout
62
+ return [line.split(": ", 1)[1] for line in shown.splitlines() if line.startswith("status: ")][0]
63
+
64
+
65
+ def _approval_commits(repo: str, ref: str = "HEAD") -> list:
66
+ log = run_git(repo, ["log", "--format=%s", ref]).stdout.splitlines()
67
+ return [subject for subject in log if subject.startswith("承認: ")]
68
+
69
+
70
+ def test_3本の組が全部通る_同期とロックとpushは1回ずつ_commitは1本ずつ(drafts, monkeypatch, capsys):
71
+ counts = {"sync": 0, "push": 0, "lock": 0}
72
+ real_sync, real_push, real_acquire = writeback.sync_repo, writeback.push_committed, lock_mod.acquire
73
+
74
+ def sync(repo_dir):
75
+ counts["sync"] += 1
76
+ return real_sync(repo_dir)
77
+
78
+ def push(repo_dir, **kwargs):
79
+ counts["push"] += 1
80
+ return real_push(repo_dir, **kwargs)
81
+
82
+ def acquire(lock, wait=0):
83
+ counts["lock"] += 1
84
+ return real_acquire(lock, wait)
85
+
86
+ monkeypatch.setattr(writeback, "sync_repo", sync)
87
+ monkeypatch.setattr(writeback, "push_committed", push)
88
+ monkeypatch.setattr(lock_mod, "acquire", acquire)
89
+
90
+ rc = cli.main(["approve", "--confirm-file", _write_confirm_file(drafts), "--by", "masaru"])
91
+ out, err = capsys.readouterr()
92
+
93
+ assert rc == 0, err
94
+ assert counts == {"sync": 1, "push": 1, "lock": 1}
95
+ assert out.splitlines()[0] == "承認しました: 3 本(masaru)"
96
+ for name in NAMES:
97
+ assert _origin_status(drafts, name) == "approved"
98
+ # commit は従前どおり 1 本ずつ(新しい順)。
99
+ assert _approval_commits(drafts["bare"], "main")[:3] == [
100
+ f"承認: {name}(nigamilab-threads・masaru)" for name in reversed(NAMES)]
101
+ assert "report_channel" not in err
102
+
103
+
104
+ def test_1本だけdigestが違えばその1本だけ断り_他は進める(drafts, capsys):
105
+ digests = list(drafts["digests"])
106
+ digests[1] = "0" * 12
107
+ rc = cli.main(["approve", "--confirm-file", _write_confirm_file(drafts, digests), "--by", "masaru"])
108
+ out, err = capsys.readouterr()
109
+
110
+ assert rc == 1
111
+ assert out.splitlines()[0] == "承認しました: 2 本(masaru)"
112
+ assert _origin_status(drafts, "a.md") == "approved"
113
+ assert _origin_status(drafts, "b.md") == "draft"
114
+ assert _origin_status(drafts, "c.md") == "approved"
115
+ assert "断った原稿: 1 本(ほかは進めました)" in err
116
+ refused = [line for line in err.splitlines() if line.startswith(" 4 行目 ")]
117
+ assert len(refused) == 1 and drafts["paths"][1] in refused[0]
118
+ assert f"いまの digest は {drafts['digests'][1]} です" in refused[0]
119
+
120
+
121
+ def test_形の違う行とパスの無い行もその1行だけ断る(drafts, capsys):
122
+ path = _write_confirm_file(drafts, extra_lines=["digest-だけ", "/no/such/file.md " + "a" * 12])
123
+ rc = cli.main(["approve", "--confirm-file", path, "--by", "masaru", "--json"])
124
+ payload = json.loads(capsys.readouterr().out)
125
+
126
+ assert rc == 1
127
+ assert payload["count"] == 3 and payload["pushed"] is True
128
+ assert [row["line"] for row in payload["refused"]] == [6, 7]
129
+ assert payload["refused"][0]["reason"].startswith("行の形が違います")
130
+
131
+
132
+ def test_pushに失敗したら_commitまで済んだ本とそのまま打てるpushの命令を出す(drafts, capsys):
133
+ hook = os.path.join(drafts["bare"], "hooks", "pre-receive")
134
+ with open(hook, "w", encoding="utf-8") as stream:
135
+ stream.write("#!/bin/sh\necho 'rejected for test' >&2\nexit 1\n")
136
+ os.chmod(hook, 0o755)
137
+
138
+ rc = cli.main(["approve", "--confirm-file", _write_confirm_file(drafts), "--by", "masaru"])
139
+ out, err = capsys.readouterr()
140
+
141
+ assert rc == 1
142
+ assert "まだ push できていません" in out.splitlines()[0]
143
+ assert "承認を commit しましたが push できませんでした" in err
144
+ push_line = [line for line in err.splitlines() if "push が通れば出ます: " in line][0]
145
+ command = push_line.split("push が通れば出ます: ", 1)[1]
146
+ # 次の一手は thth の命令(3.8.2 の裁定 2・VM で生の git を打たせない)。
147
+ assert command == "thth pull nigamilab-threads --push-pending --by masaru"
148
+ # 3 本とも commit はローカルに残り、origin にはまだ無い。
149
+ assert len(_approval_commits(drafts["work"])) == 3
150
+ assert _approval_commits(drafts["bare"], "main") == []
151
+ # 出した命令をそのまま打てば届く(hook を外してから)。
152
+ os.unlink(hook)
153
+ pushed = run_thth(shlex.split(command)[1:])
154
+ assert pushed.returncode == 0, pushed.stderr
155
+ assert pushed.stdout.splitlines()[0] == "nigamilab-threads: 押しました: 3 commit(masaru)"
156
+ for name in NAMES:
157
+ assert _origin_status(drafts, name) == "approved"
158
+
159
+
160
+ def test_同期で止まったら_残りをそのまま打てる確定の命令で出す(drafts, capsys):
161
+ # 手元に push していない commit があり、origin も進んでいる(ff できない)。
162
+ work = drafts["work"]
163
+ with open(os.path.join(work, "local.txt"), "w", encoding="utf-8") as stream:
164
+ stream.write("local\n")
165
+ run_git(work, ["add", "local.txt"])
166
+ run_git(work, ["commit", "-q", "-m", "local only"])
167
+ run_git(drafts["seed"], ["pull", "-q", "--ff-only"])
168
+ with open(os.path.join(drafts["seed"], "elsewhere.txt"), "w", encoding="utf-8") as stream:
169
+ stream.write("elsewhere\n")
170
+ run_git(drafts["seed"], ["add", "elsewhere.txt"])
171
+ run_git(drafts["seed"], ["commit", "-q", "-m", "elsewhere"])
172
+ run_git(drafts["seed"], ["push", "-q"])
173
+
174
+ rc = cli.main(["approve", "--confirm-file", _write_confirm_file(drafts), "--by", "masaru"])
175
+ out, err = capsys.readouterr()
176
+
177
+ assert rc == 1 and out == ""
178
+ assert err.splitlines()[0].startswith("repo を同期できないので承認しません: ")
179
+ assert "通ったもの: 0 本。残り 3 本はそのまま打てます:" in err
180
+ commands = [line.strip() for line in err.splitlines() if line.startswith(" thth approve ")]
181
+ assert commands == [f"thth approve {shlex.quote(path)} --confirm {digest} --by masaru"
182
+ for path, digest in zip(drafts["paths"], drafts["digests"])]
183
+ for path in drafts["paths"]:
184
+ assert "status: draft" in open(path, encoding="utf-8").read()
185
+ # 直したあと、出た命令をそのまま打てば通る。
186
+ run_git(work, ["reset", "-q", "--hard", "origin/main"])
187
+ result = run_thth(shlex.split(commands[0])[1:])
188
+ assert result.returncode == 0, result.stderr
189
+
190
+
191
+ def test_confirm_fileにハイフンを渡すと標準入力から読む(drafts):
192
+ """手元(Mac)のファイルは VM に無いので、`--confirm-file -` で標準入力から渡せる。"""
193
+ lines = "".join(f"{path} {digest}\n" for path, digest in zip(drafts["paths"], drafts["digests"]))
194
+ result = run_thth(["approve", "--confirm-file", "-", "--by", "masaru"], stdin=lines)
195
+ assert result.returncode == 0, result.stderr
196
+ assert result.stdout.splitlines()[0] == "承認しました: 3 本(masaru)"
197
+ missing = run_thth(["approve", "--confirm-file", "/Users/someone/confirm.txt", "--by", "masaru"])
198
+ assert missing.returncode == 2 and "手元のパス" in missing.stderr
199
+
200
+
201
+ def test_1段目が複数のときconfirm_fileの形を1行添える(drafts):
202
+ first = run_thth(["approve", drafts["paths"][0], drafts["paths"][1]])
203
+ assert first.returncode == 1
204
+ lines = [line for line in first.stdout.splitlines() if "--confirm-file" in line]
205
+ assert lines == ["1 本ずつの digest で確定するなら: thth approve --confirm-file <file> --by <名前>"
206
+ "(<file> は 1 行に「<原稿のパス> <digest>」)"]
207
+ single = run_thth(["approve", drafts["paths"][0]])
208
+ assert "--confirm-file" not in single.stdout
209
+
210
+
211
+ def test_confirm_fileは名指しや_confirmと一緒に使えない(drafts, capsys):
212
+ path = _write_confirm_file(drafts)
213
+ assert cli.main(["approve", drafts["paths"][0], "--confirm-file", path, "--by", "m"]) == 2
214
+ assert cli.main(["approve", "--confirm-file", path, "--confirm", "a" * 12, "--by", "m"]) == 2
215
+ assert cli.main(["approve"]) == 2
216
+ capsys.readouterr()
217
+ for path in drafts["paths"]:
218
+ assert "status: draft" in open(path, encoding="utf-8").read()
@@ -0,0 +1,112 @@
1
+ """3.8.2 件 3: 二段確認の 1 段目を「断り」に数えない(報告 r20260924-9f07f36b)。
2
+
3
+ 実測: `thth approve` の 1 段目(digest を出すだけ・rc 1)の末尾に
4
+ `report_channel: … --title "exit_1 で断られた"` が付き、3.3.0 の直前の断りの控え
5
+ (`last_refusals`)にも入っていた。
6
+
7
+ - approve の 1 段目と広場の open の 1 段目は、rc 1 のまま(呼び出し側を壊さない)、
8
+ 断りの行を出さず、`last_refusals` にも記録しない。
9
+ - つまずきの年表の材料にもならない(`--from-last-refusal` が拾うものが無い)。
10
+ - 本当の断り(digest 違い)では従前どおり出る・記録する。
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+
17
+ import pytest
18
+
19
+ from tests.conftest import write_queue_file
20
+ from thth import accounts, cli, plaza, refusals, report_timeline
21
+ from tests.test_v340_plaza_open import ledger, join # noqa: F401 (fixture)
22
+ from tests.test_v340_plaza_store import owners # noqa: F401 (fixture)
23
+
24
+
25
+ def _draft(account):
26
+ return write_queue_file(account["queue_dir"], "a.md", body="## threads\n\n1 段目の本文。\n",
27
+ fm_overrides={"account": account["name"], "status": "draft",
28
+ "approved_sha": None})
29
+
30
+
31
+ def _refusal_file(account_name):
32
+ return os.path.join(accounts.state_dir_for(account_name), refusals.FILE)
33
+
34
+
35
+ def test_approveの1段目はreport_channelを出さず_last_refusalsにも入らない(isolated_account, tmp_path, capsys):
36
+ name = isolated_account["name"]
37
+ path = _draft(isolated_account)
38
+
39
+ assert cli.main(["approve", path, "--account", name]) == 1
40
+ out, err = capsys.readouterr()
41
+
42
+ assert "digest: " in out # 1 段目は出ている(rc も従前どおり 1)
43
+ assert "report_channel" not in err and "report_channel" not in out
44
+ assert "で断られた" not in err
45
+ assert refusals.read(name) == []
46
+ assert not os.path.exists(_refusal_file(name))
47
+ # 年表の材料にもならない(直前の断りの控えが無い)。
48
+ assert cli.main(["report", "file", name, "--kind", "friction", "--from-last-refusal",
49
+ "--title", "1 段目", "--by", "テスト"]) != 0
50
+ assert "no_last_refusal" in capsys.readouterr().err
51
+ scope = report_timeline.report_inbox.Scope([name], ["nigamilab"])
52
+ assert report_timeline.timeline(scope, notes_dir=tmp_path)["rows"] == []
53
+
54
+
55
+ def test_approveのjsonの1段目も断りに数えない(isolated_account, capsys):
56
+ name = isolated_account["name"]
57
+ path = _draft(isolated_account)
58
+ assert cli.main(["approve", path, "--account", name, "--json"]) == 1
59
+ out, err = capsys.readouterr()
60
+ assert json.loads(out)["not_until_confirmed"] is True
61
+ assert "report_channel" not in err
62
+ assert refusals.read(name) == []
63
+
64
+
65
+ def test_digest違いの本当の断りでは従前どおり出て記録する(isolated_account, capsys):
66
+ name = isolated_account["name"]
67
+ path = _draft(isolated_account)
68
+ assert cli.main(["approve", path, "--account", name]) == 1
69
+ capsys.readouterr()
70
+
71
+ rc = cli.main(["approve", path, "--account", name, "--confirm", "0" * 12, "--by", "テスト"])
72
+ err = capsys.readouterr().err
73
+
74
+ assert rc == 1
75
+ assert err.splitlines()[0].startswith("digest が一致しないので承認しません")
76
+ assert err.splitlines()[-1].startswith(f"report_channel: thth report file {name} ")
77
+ [row] = refusals.read(name)
78
+ assert row["command"] == "approve" and row["reason_code"] == "exit_1"
79
+ # 次の cli.main で印は持ち越さない(1 段目の直後の本当の断りも数える)。
80
+ assert not refusals.is_first_stage()
81
+
82
+
83
+ def test_広場のopenの1段目も断りに数えない(owners, ledger, tmp_path, capsys):
84
+ join("kopicha")
85
+ body = tmp_path / "b.txt"
86
+ body.write_text("朝の問いを続けた", encoding="utf-8")
87
+ argv = ["plaza", "post", "kopicha-threads", "--kind", "finding", "--title", "朝の問い",
88
+ "--body-file", str(body), "--scope", "Threads の朝", "--open", "--by", "s"]
89
+
90
+ assert cli.main(argv) == 1
91
+ out, err = capsys.readouterr()
92
+ assert out.startswith("一段目: まだ open にしていません")
93
+ assert "report_channel" not in err
94
+ assert refusals.read("kopicha-threads") == []
95
+
96
+ # 二段目の digest 違いは本当の断り(従前どおり)。
97
+ assert cli.main(argv + ["--confirm", "x" * 20]) == 2
98
+ assert "report_channel" in capsys.readouterr().err
99
+ [row] = refusals.read("kopicha-threads")
100
+ assert row["command"] == "plaza post"
101
+ assert plaza.admin_list()["n"] == 0
102
+
103
+
104
+ @pytest.mark.parametrize("json_flag", [[], ["--json"]], ids=["text", "json"])
105
+ def test_広場のupdateでopenにする1段目も断りに数えない(owners, capsys, json_flag):
106
+ join("kopicha")
107
+ inside = plaza.post("kopicha-bsky", kind="question", title="内輪の問い", body="b",
108
+ scope_note="Bluesky", by="b")
109
+ argv = ["plaza", "update", inside["plaza_id"], "--as", "kopicha-bsky", "--visibility", "open",
110
+ "--by", "b", *json_flag]
111
+ assert cli.main(argv) == 1
112
+ assert "report_channel" not in capsys.readouterr().err
@@ -0,0 +1,105 @@
1
+ """3.8.2 の裁定 2 点(主セッション・2026-09-24)。
2
+
3
+ 1. `writeback.behind_remote()`(読むだけの口の遅れの確かめ)は repo のロックを待たずに
4
+ 取りに行き、取れなければ fetch せず、前回取り込んだ remote の姿で数えて「いま確かめ
5
+ られない(他の実行が repo を使用中)」と言う。取れたら fetch する(FETCH_HEAD は書かない)。
6
+ 承認の手元パスの読み替え(`_stale_clone_line()`)と queue 等の案内(`_behind_notices()`)も同じ。
7
+ 2. `thth pull <account> --push-pending --by <名前>`: repo のロックの中で fetch し、手元が
8
+ upstream より先にいて遅れが 0 のときだけ push する。遅れがあれば押さずに理由を言う
9
+ (rebase はしない)。押した commit の件数と題を出す。
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+
16
+ from tests.conftest import run_git
17
+ from thth import accounts, cli, writeback
18
+ from thth import lock as lock_mod
19
+
20
+
21
+ def _paths(account):
22
+ work = account["repo_dir"]
23
+ return work, os.path.join(os.path.dirname(work), "seed"), os.path.join(os.path.dirname(work), "origin.git")
24
+
25
+
26
+ def _advance(repo: str, name: str, *, push: bool = True) -> None:
27
+ with open(os.path.join(repo, name), "w", encoding="utf-8") as stream:
28
+ stream.write(name + "\n")
29
+ run_git(repo, ["add", name])
30
+ run_git(repo, ["commit", "-q", "-m", f"commit {name}"])
31
+ if push:
32
+ run_git(repo, ["push", "-q"])
33
+
34
+
35
+ # ------------------------------------------------------------ 1. ロック中の読み手は fetch しない
36
+
37
+ def test_repoのロック中はfetchせず前回の姿で数えて確かめられないと言う_空けばfetchする(isolated_account):
38
+ work, seed, _bare = _paths(isolated_account)
39
+ tracked_before = run_git(work, ["rev-parse", "origin/main"]).stdout.strip()
40
+ _advance(seed, "elsewhere.txt") # origin が 1 commit 進む(work はまだ知らない)
41
+
42
+ held = lock_mod.AccountLock(accounts.repo_lock_path_for(work))
43
+ held.acquire()
44
+ try:
45
+ info = writeback.behind_remote(work)
46
+ notices, _by_account = cli._behind_notices([isolated_account["name"]])
47
+ stale = cli._stale_clone_line(work, isolated_account["name"])
48
+ finally:
49
+ held.release()
50
+
51
+ # fetch していない: 追跡 ref は動かず、前回の姿で数えて 0・確かめた時刻は無い。
52
+ assert run_git(work, ["rev-parse", "origin/main"]).stdout.strip() == tracked_before
53
+ assert info["reason"] == writeback.BEHIND_UNCHECKED
54
+ assert info["behind"] == 0 and info["ahead"] == 0 and info["fetched_at"] is None
55
+ assert any("いま確かめられません(他の実行が repo を使用中)" in line for line in notices)
56
+ assert stale is not None and "いま確かめられません(他の実行が repo を使用中)" in stale
57
+
58
+ # 空けば従前どおり fetch して数える(FETCH_HEAD は書かない)。
59
+ fetch_head = os.path.join(work, ".git", "FETCH_HEAD")
60
+ before = os.path.getmtime(fetch_head) if os.path.exists(fetch_head) else None
61
+ info = writeback.behind_remote(work)
62
+ assert info["reason"] is None and info["behind"] == 1 and info["fetched_at"]
63
+ after = os.path.getmtime(fetch_head) if os.path.exists(fetch_head) else None
64
+ assert after == before
65
+
66
+
67
+ # ------------------------------------------------------------ 2. --push-pending
68
+
69
+ def test_push_pendingは手元が先にいるだけなら押して件数と題を出す(isolated_account, capsys):
70
+ work, _seed, bare = _paths(isolated_account)
71
+ _advance(work, "a.txt", push=False)
72
+ _advance(work, "b.txt", push=False)
73
+
74
+ rc = cli.main(["pull", isolated_account["name"], "--push-pending", "--by", "masaru"])
75
+ out, err = capsys.readouterr()
76
+
77
+ assert rc == 0, err
78
+ assert out.splitlines() == [f"{isolated_account['name']}: 押しました: 2 commit(masaru)",
79
+ " commit b.txt", " commit a.txt"]
80
+ assert run_git(bare, ["rev-parse", "main"]).stdout.strip() == \
81
+ run_git(work, ["rev-parse", "HEAD"]).stdout.strip()
82
+
83
+
84
+ def test_push_pendingはupstreamに遅れていれば押さずに理由を言う_rebaseもしない(isolated_account, capsys):
85
+ work, seed, bare = _paths(isolated_account)
86
+ _advance(work, "a.txt", push=False)
87
+ local_head = run_git(work, ["rev-parse", "HEAD"]).stdout.strip()
88
+ _advance(seed, "elsewhere.txt")
89
+ origin_head = run_git(bare, ["rev-parse", "main"]).stdout.strip()
90
+
91
+ rc = cli.main(["pull", isolated_account["name"], "--push-pending", "--by", "masaru", "--json"])
92
+ [row] = json.loads(capsys.readouterr().out)
93
+
94
+ assert rc == 1
95
+ assert row["pushed"] is False and row["ahead"] == 1 and row["behind"] == 1
96
+ assert row["error"].startswith("upstream に 1 commit 遅れているので押しません")
97
+ # 手元も origin も動かさない(rebase しない・押さない)。
98
+ assert run_git(work, ["rev-parse", "HEAD"]).stdout.strip() == local_head
99
+ assert run_git(bare, ["rev-parse", "main"]).stdout.strip() == origin_head
100
+
101
+
102
+ def test_push_pendingは名乗りが要る(isolated_account, capsys, monkeypatch):
103
+ monkeypatch.delenv("THTH_ACTOR", raising=False)
104
+ assert cli.main(["pull", isolated_account["name"], "--push-pending"]) == 1
105
+ assert "--by を付けてください" in capsys.readouterr().err