thth 3.2.0__tar.gz → 3.3.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 (502) hide show
  1. {thth-3.2.0 → thth-3.3.0}/PKG-INFO +2 -2
  2. {thth-3.2.0 → thth-3.3.0}/README.md +1 -1
  3. {thth-3.2.0 → thth-3.3.0}/mcp/server.py +83 -22
  4. {thth-3.2.0 → thth-3.3.0}/skills/thth/SKILL.md +4 -3
  5. {thth-3.2.0 → thth-3.3.0}/tests/conftest.py +12 -0
  6. {thth-3.2.0 → thth-3.3.0}/tests/test_mcp.py +2 -1
  7. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_json_contract.py +2 -0
  8. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_read_coordination.py +3 -3
  9. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_carryover_timeout.py +1 -1
  10. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_review_followups.py +1 -1
  11. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_stop_observation.py +1 -1
  12. {thth-3.2.0 → thth-3.3.0}/tests/test_v310_morning.py +6 -4
  13. {thth-3.2.0 → thth-3.3.0}/tests/test_v312_report_channel.py +21 -9
  14. thth-3.3.0/tests/test_v330_fingerprint_version.py +127 -0
  15. thth-3.3.0/tests/test_v330_held.py +270 -0
  16. thth-3.3.0/tests/test_v330_held_names.py +92 -0
  17. thth-3.3.0/tests/test_v330_last_refusals.py +156 -0
  18. thth-3.3.0/tests/test_v330_notification_route.py +97 -0
  19. thth-3.3.0/tests/test_v330_observe.py +137 -0
  20. thth-3.3.0/tests/test_v330_report_friction.py +50 -0
  21. thth-3.3.0/tests/test_v330_report_words.py +103 -0
  22. thth-3.3.0/tests/test_v330_reports_effect.py +66 -0
  23. thth-3.3.0/thth/VERSION +1 -0
  24. {thth-3.2.0 → thth-3.3.0}/thth/approval.py +35 -0
  25. {thth-3.2.0 → thth-3.3.0}/thth/cli.py +107 -10
  26. {thth-3.2.0 → thth-3.3.0}/thth/core.py +30 -0
  27. {thth-3.2.0 → thth-3.3.0}/thth/doctor.py +11 -0
  28. thth-3.3.0/thth/fingerprint_golden.py +101 -0
  29. {thth-3.2.0 → thth-3.3.0}/thth/healthcheck.py +46 -2
  30. {thth-3.2.0 → thth-3.3.0}/thth/incident.py +108 -16
  31. {thth-3.2.0 → thth-3.3.0}/thth/morning.py +175 -31
  32. thth-3.3.0/thth/notification_route.py +70 -0
  33. {thth-3.2.0 → thth-3.3.0}/thth/operations_handoff.py +42 -1
  34. {thth-3.2.0 → thth-3.3.0}/thth/read_coordination.py +2 -2
  35. thth-3.3.0/thth/refusals.py +162 -0
  36. {thth-3.2.0 → thth-3.3.0}/thth/report.py +14 -2
  37. {thth-3.2.0 → thth-3.3.0}/thth/report_inbox.py +129 -17
  38. {thth-3.2.0 → thth-3.3.0}/thth/report_service.py +21 -8
  39. {thth-3.2.0 → thth-3.3.0}/thth/select.py +94 -1
  40. {thth-3.2.0 → thth-3.3.0}/thth/tool_version.py +8 -0
  41. thth-3.2.0/thth/VERSION +0 -1
  42. {thth-3.2.0 → thth-3.3.0}/.gitignore +0 -0
  43. {thth-3.2.0 → thth-3.3.0}/LICENSE +0 -0
  44. {thth-3.2.0 → thth-3.3.0}/accounts.example/bluesky.json +0 -0
  45. {thth-3.2.0 → thth-3.3.0}/accounts.example/mastodon.json +0 -0
  46. {thth-3.2.0 → thth-3.3.0}/accounts.example/threads.json +0 -0
  47. {thth-3.2.0 → thth-3.3.0}/accounts.example/x.json +0 -0
  48. {thth-3.2.0 → thth-3.3.0}/bin/thth +0 -0
  49. {thth-3.2.0 → thth-3.3.0}/bin/thth-remote +0 -0
  50. {thth-3.2.0 → thth-3.3.0}/bin/thth-run +0 -0
  51. {thth-3.2.0 → thth-3.3.0}/pyproject.toml +0 -0
  52. {thth-3.2.0 → thth-3.3.0}/skills/thth-admin/SKILL.md +0 -0
  53. {thth-3.2.0 → thth-3.3.0}/systemd/thth@.service +0 -0
  54. {thth-3.2.0 → thth-3.3.0}/tests/__init__.py +0 -0
  55. {thth-3.2.0 → thth-3.3.0}/tests/coordination_snapshot.py +0 -0
  56. {thth-3.2.0 → thth-3.3.0}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
  57. {thth-3.2.0 → thth-3.3.0}/tests/fixtures/build_injection_eval.py +0 -0
  58. {thth-3.2.0 → thth-3.3.0}/tests/fixtures/callback/worker_responses.json +0 -0
  59. {thth-3.2.0 → thth-3.3.0}/tests/fixtures/injection_eval.json +0 -0
  60. {thth-3.2.0 → thth-3.3.0}/tests/fixtures/umami-bile.md +0 -0
  61. {thth-3.2.0 → thth-3.3.0}/tests/helpers/__init__.py +0 -0
  62. {thth-3.2.0 → thth-3.3.0}/tests/helpers/fake_oauth_server.py +0 -0
  63. {thth-3.2.0 → thth-3.3.0}/tests/helpers/fake_push_adapter.py +0 -0
  64. {thth-3.2.0 → thth-3.3.0}/tests/helpers/fake_redirect_server.py +0 -0
  65. {thth-3.2.0 → thth-3.3.0}/tests/helpers/fake_search_adapter.py +0 -0
  66. {thth-3.2.0 → thth-3.3.0}/tests/helpers/fake_threads_reader.py +0 -0
  67. {thth-3.2.0 → thth-3.3.0}/tests/helpers/fake_threads_writes.py +0 -0
  68. {thth-3.2.0 → thth-3.3.0}/tests/helpers/fake_x.py +0 -0
  69. {thth-3.2.0 → thth-3.3.0}/tests/helpers/hold_lock.py +0 -0
  70. {thth-3.2.0 → thth-3.3.0}/tests/helpers/hold_repo_lock.py +0 -0
  71. {thth-3.2.0 → thth-3.3.0}/tests/helpers/worker_probe.mjs +0 -0
  72. {thth-3.2.0 → thth-3.3.0}/tests/test_27_acceptance_fixes.py +0 -0
  73. {thth-3.2.0 → thth-3.3.0}/tests/test_account_name_check.py +0 -0
  74. {thth-3.2.0 → thth-3.3.0}/tests/test_account_report.py +0 -0
  75. {thth-3.2.0 → thth-3.3.0}/tests/test_accounts_dir.py +0 -0
  76. {thth-3.2.0 → thth-3.3.0}/tests/test_adapter_boundary.py +0 -0
  77. {thth-3.2.0 → thth-3.3.0}/tests/test_admin_http_mcp.py +0 -0
  78. {thth-3.2.0 → thth-3.3.0}/tests/test_admin_log.py +0 -0
  79. {thth-3.2.0 → thth-3.3.0}/tests/test_admin_notifications.py +0 -0
  80. {thth-3.2.0 → thth-3.3.0}/tests/test_admin_repairs.py +0 -0
  81. {thth-3.2.0 → thth-3.3.0}/tests/test_admin_report.py +0 -0
  82. {thth-3.2.0 → thth-3.3.0}/tests/test_after_cli.py +0 -0
  83. {thth-3.2.0 → thth-3.3.0}/tests/test_analytics_comparison.py +0 -0
  84. {thth-3.2.0 → thth-3.3.0}/tests/test_analytics_report.py +0 -0
  85. {thth-3.2.0 → thth-3.3.0}/tests/test_api_diagnostic.py +0 -0
  86. {thth-3.2.0 → thth-3.3.0}/tests/test_api_error_body.py +0 -0
  87. {thth-3.2.0 → thth-3.3.0}/tests/test_app_env_cli.py +0 -0
  88. {thth-3.2.0 → thth-3.3.0}/tests/test_approval.py +0 -0
  89. {thth-3.2.0 → thth-3.3.0}/tests/test_approve_cmd.py +0 -0
  90. {thth-3.2.0 → thth-3.3.0}/tests/test_ask.py +0 -0
  91. {thth-3.2.0 → thth-3.3.0}/tests/test_atlas_fifth_review.py +0 -0
  92. {thth-3.2.0 → thth-3.3.0}/tests/test_atlas_fourth_review.py +0 -0
  93. {thth-3.2.0 → thth-3.3.0}/tests/test_atlas_review.py +0 -0
  94. {thth-3.2.0 → thth-3.3.0}/tests/test_atlas_second_review.py +0 -0
  95. {thth-3.2.0 → thth-3.3.0}/tests/test_atlas_third_review.py +0 -0
  96. {thth-3.2.0 → thth-3.3.0}/tests/test_audit1_fixes.py +0 -0
  97. {thth-3.2.0 → thth-3.3.0}/tests/test_audit2_fixes.py +0 -0
  98. {thth-3.2.0 → thth-3.3.0}/tests/test_audit_b_counterexamples.py +0 -0
  99. {thth-3.2.0 → thth-3.3.0}/tests/test_audit_b_m1_semantics.py +0 -0
  100. {thth-3.2.0 → thth-3.3.0}/tests/test_audit_b_n1_depth.py +0 -0
  101. {thth-3.2.0 → thth-3.3.0}/tests/test_audit_b_r1_closure.py +0 -0
  102. {thth-3.2.0 → thth-3.3.0}/tests/test_audit_v201.py +0 -0
  103. {thth-3.2.0 → thth-3.3.0}/tests/test_audit_v23_details.py +0 -0
  104. {thth-3.2.0 → thth-3.3.0}/tests/test_auth.py +0 -0
  105. {thth-3.2.0 → thth-3.3.0}/tests/test_auth_identity_required.py +0 -0
  106. {thth-3.2.0 → thth-3.3.0}/tests/test_behind_remote.py +0 -0
  107. {thth-3.2.0 → thth-3.3.0}/tests/test_bluesky_adapter.py +0 -0
  108. {thth-3.2.0 → thth-3.3.0}/tests/test_bluesky_secret_scrub.py +0 -0
  109. {thth-3.2.0 → thth-3.3.0}/tests/test_board_needs_review.py +0 -0
  110. {thth-3.2.0 → thth-3.3.0}/tests/test_board_running.py +0 -0
  111. {thth-3.2.0 → thth-3.3.0}/tests/test_bundle.py +0 -0
  112. {thth-3.2.0 → thth-3.3.0}/tests/test_charcount.py +0 -0
  113. {thth-3.2.0 → thth-3.3.0}/tests/test_cli_entrypoint.py +0 -0
  114. {thth-3.2.0 → thth-3.3.0}/tests/test_cli_help.py +0 -0
  115. {thth-3.2.0 → thth-3.3.0}/tests/test_cli_vm_path.py +0 -0
  116. {thth-3.2.0 → thth-3.3.0}/tests/test_collect.py +0 -0
  117. {thth-3.2.0 → thth-3.3.0}/tests/test_collect_engagements.py +0 -0
  118. {thth-3.2.0 → thth-3.3.0}/tests/test_collect_runs.py +0 -0
  119. {thth-3.2.0 → thth-3.3.0}/tests/test_collect_sent.py +0 -0
  120. {thth-3.2.0 → thth-3.3.0}/tests/test_comparability.py +0 -0
  121. {thth-3.2.0 → thth-3.3.0}/tests/test_control_chars_refused.py +0 -0
  122. {thth-3.2.0 → thth-3.3.0}/tests/test_conversation_paging.py +0 -0
  123. {thth-3.2.0 → thth-3.3.0}/tests/test_count_for.py +0 -0
  124. {thth-3.2.0 → thth-3.3.0}/tests/test_docs_links.py +0 -0
  125. {thth-3.2.0 → thth-3.3.0}/tests/test_doctor.py +0 -0
  126. {thth-3.2.0 → thth-3.3.0}/tests/test_doctor_all_permissions.py +0 -0
  127. {thth-3.2.0 → thth-3.3.0}/tests/test_doctor_next_step.py +0 -0
  128. {thth-3.2.0 → thth-3.3.0}/tests/test_duplicate_front_matter_keys.py +0 -0
  129. {thth-3.2.0 → thth-3.3.0}/tests/test_editorial_audit.py +0 -0
  130. {thth-3.2.0 → thth-3.3.0}/tests/test_editorial_n3_n4.py +0 -0
  131. {thth-3.2.0 → thth-3.3.0}/tests/test_engagements.py +0 -0
  132. {thth-3.2.0 → thth-3.3.0}/tests/test_external_acceptance.py +0 -0
  133. {thth-3.2.0 → thth-3.3.0}/tests/test_fake_api.py +0 -0
  134. {thth-3.2.0 → thth-3.3.0}/tests/test_form_spec.py +0 -0
  135. {thth-3.2.0 → thth-3.3.0}/tests/test_form_spec_cli.py +0 -0
  136. {thth-3.2.0 → thth-3.3.0}/tests/test_forms.py +0 -0
  137. {thth-3.2.0 → thth-3.3.0}/tests/test_fresh_install.py +0 -0
  138. {thth-3.2.0 → thth-3.3.0}/tests/test_front_matter_injection.py +0 -0
  139. {thth-3.2.0 → thth-3.3.0}/tests/test_healthcheck_notifications.py +0 -0
  140. {thth-3.2.0 → thth-3.3.0}/tests/test_hypotheses.py +0 -0
  141. {thth-3.2.0 → thth-3.3.0}/tests/test_hypothesis_boundaries.py +0 -0
  142. {thth-3.2.0 → thth-3.3.0}/tests/test_hypothesis_units.py +0 -0
  143. {thth-3.2.0 → thth-3.3.0}/tests/test_impact_cli.py +0 -0
  144. {thth-3.2.0 → thth-3.3.0}/tests/test_inbox_granted_scopes.py +0 -0
  145. {thth-3.2.0 → thth-3.3.0}/tests/test_incident_notifications.py +0 -0
  146. {thth-3.2.0 → thth-3.3.0}/tests/test_independent_thread_review.py +0 -0
  147. {thth-3.2.0 → thth-3.3.0}/tests/test_independent_topic_review.py +0 -0
  148. {thth-3.2.0 → thth-3.3.0}/tests/test_independent_topic_review_r2.py +0 -0
  149. {thth-3.2.0 → thth-3.3.0}/tests/test_inflight.py +0 -0
  150. {thth-3.2.0 → thth-3.3.0}/tests/test_inflight_tag_body_hash.py +0 -0
  151. {thth-3.2.0 → thth-3.3.0}/tests/test_jst.py +0 -0
  152. {thth-3.2.0 → thth-3.3.0}/tests/test_lint.py +0 -0
  153. {thth-3.2.0 → thth-3.3.0}/tests/test_list_projections.py +0 -0
  154. {thth-3.2.0 → thth-3.3.0}/tests/test_llm_trial.py +0 -0
  155. {thth-3.2.0 → thth-3.3.0}/tests/test_llm_trial_engage.py +0 -0
  156. {thth-3.2.0 → thth-3.3.0}/tests/test_lock.py +0 -0
  157. {thth-3.2.0 → thth-3.3.0}/tests/test_m2_independent.py +0 -0
  158. {thth-3.2.0 → thth-3.3.0}/tests/test_maintain.py +0 -0
  159. {thth-3.2.0 → thth-3.3.0}/tests/test_mastodon_adapter.py +0 -0
  160. {thth-3.2.0 → thth-3.3.0}/tests/test_mastodon_visibility.py +0 -0
  161. {thth-3.2.0 → thth-3.3.0}/tests/test_max_per_run.py +0 -0
  162. {thth-3.2.0 → thth-3.3.0}/tests/test_mcp_robustness.py +0 -0
  163. {thth-3.2.0 → thth-3.3.0}/tests/test_measured.py +0 -0
  164. {thth-3.2.0 → thth-3.3.0}/tests/test_media_accounts.py +0 -0
  165. {thth-3.2.0 → thth-3.3.0}/tests/test_media_next_steps.py +0 -0
  166. {thth-3.2.0 → thth-3.3.0}/tests/test_media_roundtrip.py +0 -0
  167. {thth-3.2.0 → thth-3.3.0}/tests/test_mismatch_diagnostics.py +0 -0
  168. {thth-3.2.0 → thth-3.3.0}/tests/test_n5_reply_null_element.py +0 -0
  169. {thth-3.2.0 → thth-3.3.0}/tests/test_n6_replies_incomplete_ledger.py +0 -0
  170. {thth-3.2.0 → thth-3.3.0}/tests/test_n7_doctor_error_body.py +0 -0
  171. {thth-3.2.0 → thth-3.3.0}/tests/test_no_limits.py +0 -0
  172. {thth-3.2.0 → thth-3.3.0}/tests/test_observation_provenance.py +0 -0
  173. {thth-3.2.0 → thth-3.3.0}/tests/test_operations_handoff.py +0 -0
  174. {thth-3.2.0 → thth-3.3.0}/tests/test_packaging.py +0 -0
  175. {thth-3.2.0 → thth-3.3.0}/tests/test_preview.py +0 -0
  176. {thth-3.2.0 → thth-3.3.0}/tests/test_production_mode.py +0 -0
  177. {thth-3.2.0 → thth-3.3.0}/tests/test_pull_cli.py +0 -0
  178. {thth-3.2.0 → thth-3.3.0}/tests/test_queue.py +0 -0
  179. {thth-3.2.0 → thth-3.3.0}/tests/test_quiet_hours.py +0 -0
  180. {thth-3.2.0 → thth-3.3.0}/tests/test_r2_r4_r5.py +0 -0
  181. {thth-3.2.0 → thth-3.3.0}/tests/test_read_cli_bare_runtimeerror.py +0 -0
  182. {thth-3.2.0 → thth-3.3.0}/tests/test_reason_adoption.py +0 -0
  183. {thth-3.2.0 → thth-3.3.0}/tests/test_redact.py +0 -0
  184. {thth-3.2.0 → thth-3.3.0}/tests/test_redirect_token_leak.py +0 -0
  185. {thth-3.2.0 → thth-3.3.0}/tests/test_refresh.py +0 -0
  186. {thth-3.2.0 → thth-3.3.0}/tests/test_refresh_destination.py +0 -0
  187. {thth-3.2.0 → thth-3.3.0}/tests/test_rejudge_new.py +0 -0
  188. {thth-3.2.0 → thth-3.3.0}/tests/test_rejudge_r2_r3.py +0 -0
  189. {thth-3.2.0 → thth-3.3.0}/tests/test_replies.py +0 -0
  190. {thth-3.2.0 → thth-3.3.0}/tests/test_replies_refresh.py +0 -0
  191. {thth-3.2.0 → thth-3.3.0}/tests/test_repo_lock.py +0 -0
  192. {thth-3.2.0 → thth-3.3.0}/tests/test_repo_sync.py +0 -0
  193. {thth-3.2.0 → thth-3.3.0}/tests/test_report_checkout_import.py +0 -0
  194. {thth-3.2.0 → thth-3.3.0}/tests/test_report_http.py +0 -0
  195. {thth-3.2.0 → thth-3.3.0}/tests/test_report_isolation.py +0 -0
  196. {thth-3.2.0 → thth-3.3.0}/tests/test_report_service.py +0 -0
  197. {thth-3.2.0 → thth-3.3.0}/tests/test_reverse_audit.py +0 -0
  198. {thth-3.2.0 → thth-3.3.0}/tests/test_review_case_id_cluster_m2.py +0 -0
  199. {thth-3.2.0 → thth-3.3.0}/tests/test_review_case_id_cluster_r2.py +0 -0
  200. {thth-3.2.0 → thth-3.3.0}/tests/test_review_cli.py +0 -0
  201. {thth-3.2.0 → thth-3.3.0}/tests/test_review_evidence_account.py +0 -0
  202. {thth-3.2.0 → thth-3.3.0}/tests/test_review_form_spec_and_case_id.py +0 -0
  203. {thth-3.2.0 → thth-3.3.0}/tests/test_review_provenance.py +0 -0
  204. {thth-3.2.0 → thth-3.3.0}/tests/test_review_records.py +0 -0
  205. {thth-3.2.0 → thth-3.3.0}/tests/test_revoke.py +0 -0
  206. {thth-3.2.0 → thth-3.3.0}/tests/test_roundtrip.py +0 -0
  207. {thth-3.2.0 → thth-3.3.0}/tests/test_run_missing_env.py +0 -0
  208. {thth-3.2.0 → thth-3.3.0}/tests/test_run_record_failclosed.py +0 -0
  209. {thth-3.2.0 → thth-3.3.0}/tests/test_runs_collected.py +0 -0
  210. {thth-3.2.0 → thth-3.3.0}/tests/test_runs_read_rows.py +0 -0
  211. {thth-3.2.0 → thth-3.3.0}/tests/test_schedule_multiple.py +0 -0
  212. {thth-3.2.0 → thth-3.3.0}/tests/test_schedule_view.py +0 -0
  213. {thth-3.2.0 → thth-3.3.0}/tests/test_scopes_source_requested.py +0 -0
  214. {thth-3.2.0 → thth-3.3.0}/tests/test_search_engage_targets.py +0 -0
  215. {thth-3.2.0 → thth-3.3.0}/tests/test_search_media_routes.py +0 -0
  216. {thth-3.2.0 → thth-3.3.0}/tests/test_secret_value_redact.py +0 -0
  217. {thth-3.2.0 → thth-3.3.0}/tests/test_secrets_grep.py +0 -0
  218. {thth-3.2.0 → thth-3.3.0}/tests/test_security_small_fixes.py +0 -0
  219. {thth-3.2.0 → thth-3.3.0}/tests/test_select.py +0 -0
  220. {thth-3.2.0 → thth-3.3.0}/tests/test_selfupdate.py +0 -0
  221. {thth-3.2.0 → thth-3.3.0}/tests/test_send.py +0 -0
  222. {thth-3.2.0 → thth-3.3.0}/tests/test_send_cli.py +0 -0
  223. {thth-3.2.0 → thth-3.3.0}/tests/test_send_control_chars.py +0 -0
  224. {thth-3.2.0 → thth-3.3.0}/tests/test_send_media.py +0 -0
  225. {thth-3.2.0 → thth-3.3.0}/tests/test_send_visibility.py +0 -0
  226. {thth-3.2.0 → thth-3.3.0}/tests/test_sent_integrity.py +0 -0
  227. {thth-3.2.0 → thth-3.3.0}/tests/test_server_json.py +0 -0
  228. {thth-3.2.0 → thth-3.3.0}/tests/test_site.py +0 -0
  229. {thth-3.2.0 → thth-3.3.0}/tests/test_skill_doc.py +0 -0
  230. {thth-3.2.0 → thth-3.3.0}/tests/test_study_report.py +0 -0
  231. {thth-3.2.0 → thth-3.3.0}/tests/test_systemd.py +0 -0
  232. {thth-3.2.0 → thth-3.3.0}/tests/test_thread_publish.py +0 -0
  233. {thth-3.2.0 → thth-3.3.0}/tests/test_thread_read.py +0 -0
  234. {thth-3.2.0 → thth-3.3.0}/tests/test_threadrun.py +0 -0
  235. {thth-3.2.0 → thth-3.3.0}/tests/test_threads_read_permissions.py +0 -0
  236. {thth-3.2.0 → thth-3.3.0}/tests/test_threadshape.py +0 -0
  237. {thth-3.2.0 → thth-3.3.0}/tests/test_thth_root.py +0 -0
  238. {thth-3.2.0 → thth-3.3.0}/tests/test_token_exists.py +0 -0
  239. {thth-3.2.0 → thth-3.3.0}/tests/test_token_scopes_record.py +0 -0
  240. {thth-3.2.0 → thth-3.3.0}/tests/test_token_set.py +0 -0
  241. {thth-3.2.0 → thth-3.3.0}/tests/test_topic.py +0 -0
  242. {thth-3.2.0 → thth-3.3.0}/tests/test_topic_advice.py +0 -0
  243. {thth-3.2.0 → thth-3.3.0}/tests/test_topic_cli.py +0 -0
  244. {thth-3.2.0 → thth-3.3.0}/tests/test_topic_models.py +0 -0
  245. {thth-3.2.0 → thth-3.3.0}/tests/test_topic_review_fixes.py +0 -0
  246. {thth-3.2.0 → thth-3.3.0}/tests/test_topic_shelf_observers.py +0 -0
  247. {thth-3.2.0 → thth-3.3.0}/tests/test_topic_store.py +0 -0
  248. {thth-3.2.0 → thth-3.3.0}/tests/test_topics_record_concurrent.py +0 -0
  249. {thth-3.2.0 → thth-3.3.0}/tests/test_topics_tool.py +0 -0
  250. {thth-3.2.0 → thth-3.3.0}/tests/test_trial_frictions.py +0 -0
  251. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_admin_transaction.py +0 -0
  252. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_approve_account.py +0 -0
  253. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_length.py +0 -0
  254. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_lock_wait.py +0 -0
  255. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_mastodon_scopes.py +0 -0
  256. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_mentions.py +0 -0
  257. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_post_urls.py +0 -0
  258. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_repo_paths.py +0 -0
  259. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_reposts.py +0 -0
  260. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_study_add.py +0 -0
  261. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_tool_version.py +0 -0
  262. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_unanswered.py +0 -0
  263. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_unanswered_scope.py +0 -0
  264. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_where_arguments.py +0 -0
  265. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_where_filters.py +0 -0
  266. {thth-3.2.0 → thth-3.3.0}/tests/test_v210_who_arguments.py +0 -0
  267. {thth-3.2.0 → thth-3.3.0}/tests/test_v211_auth_integration.py +0 -0
  268. {thth-3.2.0 → thth-3.3.0}/tests/test_v211_authflow.py +0 -0
  269. {thth-3.2.0 → thth-3.3.0}/tests/test_v211_bluesky_stdin.py +0 -0
  270. {thth-3.2.0 → thth-3.3.0}/tests/test_v211_doctor_binding.py +0 -0
  271. {thth-3.2.0 → thth-3.3.0}/tests/test_v211_mastodon_auth.py +0 -0
  272. {thth-3.2.0 → thth-3.3.0}/tests/test_v211_x_auth.py +0 -0
  273. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_approval_relay.py +0 -0
  274. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_ask_coordination.py +0 -0
  275. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_budget.py +0 -0
  276. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_budget_auth.py +0 -0
  277. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_budget_mcp.py +0 -0
  278. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_deletion.py +0 -0
  279. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_job_completion_fault.py +0 -0
  280. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_leave.py +0 -0
  281. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_leave_gate.py +0 -0
  282. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_leave_providers.py +0 -0
  283. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_leave_token_safety.py +0 -0
  284. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_mastodon_guide.py +0 -0
  285. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_mcp_compat.py +0 -0
  286. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_missing_token.py +0 -0
  287. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_server_writes.py +0 -0
  288. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_token_parent.py +0 -0
  289. {thth-3.2.0 → thth-3.3.0}/tests/test_v212_write_storage.py +0 -0
  290. {thth-3.2.0 → thth-3.3.0}/tests/test_v2131_send_dry_run.py +0 -0
  291. {thth-3.2.0 → thth-3.3.0}/tests/test_v2131_threads_publish_wait.py +0 -0
  292. {thth-3.2.0 → thth-3.3.0}/tests/test_v2132_send_inflight_hint.py +0 -0
  293. {thth-3.2.0 → thth-3.3.0}/tests/test_v2132_threads_create_timeout.py +0 -0
  294. {thth-3.2.0 → thth-3.3.0}/tests/test_v2132_threads_create_unknown.py +0 -0
  295. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_acceptance_fixes.py +0 -0
  296. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_adts_asf.py +0 -0
  297. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_approval_attachments.py +0 -0
  298. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_attachment_kinds.py +0 -0
  299. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_bluesky_external.py +0 -0
  300. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_bluesky_media.py +0 -0
  301. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_bluesky_metadata.py +0 -0
  302. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_bluesky_metadata_uri.py +0 -0
  303. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_bluesky_metadata_video.py +0 -0
  304. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_bluesky_record.py +0 -0
  305. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_bluesky_record_video.py +0 -0
  306. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_bluesky_video.py +0 -0
  307. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_bluesky_webm_named.py +0 -0
  308. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_bmff_typed_metadata.py +0 -0
  309. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_budget_lock.py +0 -0
  310. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_capabilities.py +0 -0
  311. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_graphemes.py +0 -0
  312. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_auth_media.py +0 -0
  313. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_bmff_audio.py +0 -0
  314. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_flac.py +0 -0
  315. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_gif.py +0 -0
  316. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_link.py +0 -0
  317. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_media.py +0 -0
  318. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_mp3.py +0 -0
  319. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_ogg_opus.py +0 -0
  320. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
  321. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_poll.py +0 -0
  322. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
  323. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_quote.py +0 -0
  324. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_thumbnail.py +0 -0
  325. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_wav.py +0 -0
  326. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_webm_audio.py +0 -0
  327. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_mastodon_webm_video.py +0 -0
  328. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_media_cleanup.py +0 -0
  329. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_media_formats.py +0 -0
  330. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_media_foundation.py +0 -0
  331. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_media_prepared.py +0 -0
  332. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_media_relay.py +0 -0
  333. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_media_relay_refusals.py +0 -0
  334. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_media_upload.py +0 -0
  335. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_receipt_deadline.py +0 -0
  336. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_stage810_audit.py +0 -0
  337. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_static_diagnostics.py +0 -0
  338. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_systemd_worker.py +0 -0
  339. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_threads_ghost.py +0 -0
  340. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_threads_gif.py +0 -0
  341. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_threads_link.py +0 -0
  342. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_threads_media.py +0 -0
  343. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_threads_media_spoiler.py +0 -0
  344. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_threads_poll.py +0 -0
  345. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_threads_quote.py +0 -0
  346. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_threads_quote_media.py +0 -0
  347. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_threads_replies.py +0 -0
  348. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_threads_text.py +0 -0
  349. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_threads_text_spoiler.py +0 -0
  350. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_threads_video.py +0 -0
  351. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_transport_review.py +0 -0
  352. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_video_typed_metadata.py +0 -0
  353. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_vorbis_comment_boundary.py +0 -0
  354. {thth-3.2.0 → thth-3.3.0}/tests/test_v213_webm_schema_boundary.py +0 -0
  355. {thth-3.2.0 → thth-3.3.0}/tests/test_v2141_draft_lint_reason.py +0 -0
  356. {thth-3.2.0 → thth-3.3.0}/tests/test_v2141_managed_repo_umask.py +0 -0
  357. {thth-3.2.0 → thth-3.3.0}/tests/test_v214_release_signing.py +0 -0
  358. {thth-3.2.0 → thth-3.3.0}/tests/test_v214_x_adapter.py +0 -0
  359. {thth-3.2.0 → thth-3.3.0}/tests/test_v214_x_auth.py +0 -0
  360. {thth-3.2.0 → thth-3.3.0}/tests/test_v214_x_media.py +0 -0
  361. {thth-3.2.0 → thth-3.3.0}/tests/test_v214_x_posts_budget.py +0 -0
  362. {thth-3.2.0 → thth-3.3.0}/tests/test_v21b_approved_writes.py +0 -0
  363. {thth-3.2.0 → thth-3.3.0}/tests/test_v28_tags.py +0 -0
  364. {thth-3.2.0 → thth-3.3.0}/tests/test_v310_search_timeout.py +0 -0
  365. {thth-3.2.0 → thth-3.3.0}/tests/test_v310_watch_words.py +0 -0
  366. {thth-3.2.0 → thth-3.3.0}/tests/test_v311_leave_same_user.py +0 -0
  367. {thth-3.2.0 → thth-3.3.0}/tests/test_v311_mastodon_retract.py +0 -0
  368. {thth-3.2.0 → thth-3.3.0}/tests/test_v311_morning_overdue.py +0 -0
  369. {thth-3.2.0 → thth-3.3.0}/tests/test_v311_morning_own_exclusion.py +0 -0
  370. {thth-3.2.0 → thth-3.3.0}/tests/test_v311_morning_unanswered_window.py +0 -0
  371. {thth-3.2.0 → thth-3.3.0}/tests/test_v311_notifications_guidance.py +0 -0
  372. {thth-3.2.0 → thth-3.3.0}/tests/test_v311_replies_owned.py +0 -0
  373. {thth-3.2.0 → thth-3.3.0}/tests/test_v312_leave_shared_token.py +0 -0
  374. {thth-3.2.0 → thth-3.3.0}/tests/test_v312_own_handles_stopped.py +0 -0
  375. {thth-3.2.0 → thth-3.3.0}/tests/test_v312_report_admin.py +0 -0
  376. {thth-3.2.0 → thth-3.3.0}/tests/test_v312_report_inbox.py +0 -0
  377. {thth-3.2.0 → thth-3.3.0}/tests/test_v312_report_mcp_user.py +0 -0
  378. {thth-3.2.0 → thth-3.3.0}/tests/test_v31_analysis.py +0 -0
  379. {thth-3.2.0 → thth-3.3.0}/tests/test_v31_collection.py +0 -0
  380. {thth-3.2.0 → thth-3.3.0}/tests/test_v320_reply_to_file_display.py +0 -0
  381. {thth-3.2.0 → thth-3.3.0}/tests/test_v320_reply_to_file_lint.py +0 -0
  382. {thth-3.2.0 → thth-3.3.0}/tests/test_v320_reply_to_file_resolve.py +0 -0
  383. {thth-3.2.0 → thth-3.3.0}/tests/test_v320_report_fixes.py +0 -0
  384. {thth-3.2.0 → thth-3.3.0}/tests/test_value_domains_shown.py +0 -0
  385. {thth-3.2.0 → thth-3.3.0}/tests/test_version.py +0 -0
  386. {thth-3.2.0 → thth-3.3.0}/tests/test_where_cli.py +0 -0
  387. {thth-3.2.0 → thth-3.3.0}/tests/test_who_cli.py +0 -0
  388. {thth-3.2.0 → thth-3.3.0}/tests/test_writeback_rebase.py +0 -0
  389. {thth-3.2.0 → thth-3.3.0}/tests/test_writeback_validate.py +0 -0
  390. {thth-3.2.0 → thth-3.3.0}/tests/test_x_metrics.py +0 -0
  391. {thth-3.2.0 → thth-3.3.0}/thth/__init__.py +0 -0
  392. {thth-3.2.0 → thth-3.3.0}/thth/__main__.py +0 -0
  393. {thth-3.2.0 → thth-3.3.0}/thth/_grapheme_data.py +0 -0
  394. {thth-3.2.0 → thth-3.3.0}/thth/account_cli.py +0 -0
  395. {thth-3.2.0 → thth-3.3.0}/thth/account_report.py +0 -0
  396. {thth-3.2.0 → thth-3.3.0}/thth/accounts.py +0 -0
  397. {thth-3.2.0 → thth-3.3.0}/thth/adapters/__init__.py +0 -0
  398. {thth-3.2.0 → thth-3.3.0}/thth/adapters/auth_mastodon.py +0 -0
  399. {thth-3.2.0 → thth-3.3.0}/thth/adapters/auth_threads.py +0 -0
  400. {thth-3.2.0 → thth-3.3.0}/thth/adapters/auth_x.py +0 -0
  401. {thth-3.2.0 → thth-3.3.0}/thth/adapters/base.py +0 -0
  402. {thth-3.2.0 → thth-3.3.0}/thth/adapters/bluesky.py +0 -0
  403. {thth-3.2.0 → thth-3.3.0}/thth/adapters/bluesky_media.py +0 -0
  404. {thth-3.2.0 → thth-3.3.0}/thth/adapters/bluesky_video.py +0 -0
  405. {thth-3.2.0 → thth-3.3.0}/thth/adapters/mastodon.py +0 -0
  406. {thth-3.2.0 → thth-3.3.0}/thth/adapters/mastodon_media.py +0 -0
  407. {thth-3.2.0 → thth-3.3.0}/thth/adapters/threads.py +0 -0
  408. {thth-3.2.0 → thth-3.3.0}/thth/adapters/threads_media.py +0 -0
  409. {thth-3.2.0 → thth-3.3.0}/thth/adapters/x.py +0 -0
  410. {thth-3.2.0 → thth-3.3.0}/thth/adapters/x_media.py +0 -0
  411. {thth-3.2.0 → thth-3.3.0}/thth/admin_log.py +0 -0
  412. {thth-3.2.0 → thth-3.3.0}/thth/admin_notifications.py +0 -0
  413. {thth-3.2.0 → thth-3.3.0}/thth/admin_report.py +0 -0
  414. {thth-3.2.0 → thth-3.3.0}/thth/after_cli.py +0 -0
  415. {thth-3.2.0 → thth-3.3.0}/thth/analytics_comparison.py +0 -0
  416. {thth-3.2.0 → thth-3.3.0}/thth/analytics_report.py +0 -0
  417. {thth-3.2.0 → thth-3.3.0}/thth/analytics_shapes.py +0 -0
  418. {thth-3.2.0 → thth-3.3.0}/thth/analytics_threads.py +0 -0
  419. {thth-3.2.0 → thth-3.3.0}/thth/api_diagnostic.py +0 -0
  420. {thth-3.2.0 → thth-3.3.0}/thth/appconfig.py +0 -0
  421. {thth-3.2.0 → thth-3.3.0}/thth/appenv.py +0 -0
  422. {thth-3.2.0 → thth-3.3.0}/thth/approval_jobs.py +0 -0
  423. {thth-3.2.0 → thth-3.3.0}/thth/approval_relay.py +0 -0
  424. {thth-3.2.0 → thth-3.3.0}/thth/ask.py +0 -0
  425. {thth-3.2.0 → thth-3.3.0}/thth/ask_cli.py +0 -0
  426. {thth-3.2.0 → thth-3.3.0}/thth/audioformats.py +0 -0
  427. {thth-3.2.0 → thth-3.3.0}/thth/authclients.py +0 -0
  428. {thth-3.2.0 → thth-3.3.0}/thth/authflow.py +0 -0
  429. {thth-3.2.0 → thth-3.3.0}/thth/authpassword.py +0 -0
  430. {thth-3.2.0 → thth-3.3.0}/thth/bluesky_metadata.py +0 -0
  431. {thth-3.2.0 → thth-3.3.0}/thth/budget_x.py +0 -0
  432. {thth-3.2.0 → thth-3.3.0}/thth/budget_x_posts.py +0 -0
  433. {thth-3.2.0 → thth-3.3.0}/thth/bundle.py +0 -0
  434. {thth-3.2.0 → thth-3.3.0}/thth/collect.py +0 -0
  435. {thth-3.2.0 → thth-3.3.0}/thth/collection_context.py +0 -0
  436. {thth-3.2.0 → thth-3.3.0}/thth/collection_status.py +0 -0
  437. {thth-3.2.0 → thth-3.3.0}/thth/deletion.py +0 -0
  438. {thth-3.2.0 → thth-3.3.0}/thth/ebmlaudio.py +0 -0
  439. {thth-3.2.0 → thth-3.3.0}/thth/ebmlvideo.py +0 -0
  440. {thth-3.2.0 → thth-3.3.0}/thth/engagements.py +0 -0
  441. {thth-3.2.0 → thth-3.3.0}/thth/flacformats.py +0 -0
  442. {thth-3.2.0 → thth-3.3.0}/thth/forms.py +0 -0
  443. {thth-3.2.0 → thth-3.3.0}/thth/graphemes.py +0 -0
  444. {thth-3.2.0 → thth-3.3.0}/thth/handoff_cursor.py +0 -0
  445. {thth-3.2.0 → thth-3.3.0}/thth/httpsafe.py +0 -0
  446. {thth-3.2.0 → thth-3.3.0}/thth/inflight.py +0 -0
  447. {thth-3.2.0 → thth-3.3.0}/thth/jst.py +0 -0
  448. {thth-3.2.0 → thth-3.3.0}/thth/leave.py +0 -0
  449. {thth-3.2.0 → thth-3.3.0}/thth/leave_gate.py +0 -0
  450. {thth-3.2.0 → thth-3.3.0}/thth/lint.py +0 -0
  451. {thth-3.2.0 → thth-3.3.0}/thth/lock.py +0 -0
  452. {thth-3.2.0 → thth-3.3.0}/thth/maintain.py +0 -0
  453. {thth-3.2.0 → thth-3.3.0}/thth/managed_repo.py +0 -0
  454. {thth-3.2.0 → thth-3.3.0}/thth/measured.py +0 -0
  455. {thth-3.2.0 → thth-3.3.0}/thth/media.py +0 -0
  456. {thth-3.2.0 → thth-3.3.0}/thth/media_capabilities.py +0 -0
  457. {thth-3.2.0 → thth-3.3.0}/thth/media_cleanup.py +0 -0
  458. {thth-3.2.0 → thth-3.3.0}/thth/media_delivery.py +0 -0
  459. {thth-3.2.0 → thth-3.3.0}/thth/media_relay.py +0 -0
  460. {thth-3.2.0 → thth-3.3.0}/thth/media_uploads.py +0 -0
  461. {thth-3.2.0 → thth-3.3.0}/thth/mediaformats.py +0 -0
  462. {thth-3.2.0 → thth-3.3.0}/thth/mpeg_audio.py +0 -0
  463. {thth-3.2.0 → thth-3.3.0}/thth/oauth.py +0 -0
  464. {thth-3.2.0 → thth-3.3.0}/thth/oggformats.py +0 -0
  465. {thth-3.2.0 → thth-3.3.0}/thth/postid.py +0 -0
  466. {thth-3.2.0 → thth-3.3.0}/thth/queuefile.py +0 -0
  467. {thth-3.2.0 → thth-3.3.0}/thth/read_window.py +0 -0
  468. {thth-3.2.0 → thth-3.3.0}/thth/redact.py +0 -0
  469. {thth-3.2.0 → thth-3.3.0}/thth/replies.py +0 -0
  470. {thth-3.2.0 → thth-3.3.0}/thth/report_details.py +0 -0
  471. {thth-3.2.0 → thth-3.3.0}/thth/report_http.py +0 -0
  472. {thth-3.2.0 → thth-3.3.0}/thth/report_isolation.py +0 -0
  473. {thth-3.2.0 → thth-3.3.0}/thth/retract_cli.py +0 -0
  474. {thth-3.2.0 → thth-3.3.0}/thth/runs.py +0 -0
  475. {thth-3.2.0 → thth-3.3.0}/thth/scopes.py +0 -0
  476. {thth-3.2.0 → thth-3.3.0}/thth/secrets_fs.py +0 -0
  477. {thth-3.2.0 → thth-3.3.0}/thth/selfupdate.py +0 -0
  478. {thth-3.2.0 → thth-3.3.0}/thth/sent.py +0 -0
  479. {thth-3.2.0 → thth-3.3.0}/thth/server_files.py +0 -0
  480. {thth-3.2.0 → thth-3.3.0}/thth/server_writes.py +0 -0
  481. {thth-3.2.0 → thth-3.3.0}/thth/stop_observation.py +0 -0
  482. {thth-3.2.0 → thth-3.3.0}/thth/study_cli.py +0 -0
  483. {thth-3.2.0 → thth-3.3.0}/thth/study_report.py +0 -0
  484. {thth-3.2.0 → thth-3.3.0}/thth/systemd_gen.py +0 -0
  485. {thth-3.2.0 → thth-3.3.0}/thth/tags.py +0 -0
  486. {thth-3.2.0 → thth-3.3.0}/thth/thread_read.py +0 -0
  487. {thth-3.2.0 → thth-3.3.0}/thth/threadrun.py +0 -0
  488. {thth-3.2.0 → thth-3.3.0}/thth/threads_read_cli.py +0 -0
  489. {thth-3.2.0 → thth-3.3.0}/thth/threadshape.py +0 -0
  490. {thth-3.2.0 → thth-3.3.0}/thth/threadthrow.py +0 -0
  491. {thth-3.2.0 → thth-3.3.0}/thth/topic_advice.py +0 -0
  492. {thth-3.2.0 → thth-3.3.0}/thth/topic_cli.py +0 -0
  493. {thth-3.2.0 → thth-3.3.0}/thth/topic_models.py +0 -0
  494. {thth-3.2.0 → thth-3.3.0}/thth/topic_store.py +0 -0
  495. {thth-3.2.0 → thth-3.3.0}/thth/topics.py +0 -0
  496. {thth-3.2.0 → thth-3.3.0}/thth/unanswered.py +0 -0
  497. {thth-3.2.0 → thth-3.3.0}/thth/vorbisformats.py +0 -0
  498. {thth-3.2.0 → thth-3.3.0}/thth/watch_cli.py +0 -0
  499. {thth-3.2.0 → thth-3.3.0}/thth/where_cli.py +0 -0
  500. {thth-3.2.0 → thth-3.3.0}/thth/who_cli.py +0 -0
  501. {thth-3.2.0 → thth-3.3.0}/thth/writeback.py +0 -0
  502. {thth-3.2.0 → thth-3.3.0}/thth/x_metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thth
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: 承認済みの投稿だけを投げる門(Threads・Bluesky・Mastodon)。人が承認し、記録は利用者の git に残る。
5
5
  Project-URL: Homepage, https://thth.me
6
6
  Project-URL: Source, https://github.com/aokings/thth
@@ -125,7 +125,7 @@ timer(systemd・`thth systemd` で生成)は10分ごとに実行し、投稿
125
125
  - **英語の文書**: [README.en.md](README.en.md)・[docs/usage.en.md](docs/usage.en.md)・[llms.txt](llms.txt)。
126
126
  - **skill**: `skills/thth/SKILL.md`(wheel にも入る)。
127
127
 
128
- - **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`・`notifications`・`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`・`location`・`notifications`・`report`・`admin`。
129
129
  - **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
130
130
  - **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
131
131
  - **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
@@ -101,7 +101,7 @@ timer(systemd・`thth systemd` で生成)は10分ごとに実行し、投稿
101
101
  - **英語の文書**: [README.en.md](README.en.md)・[docs/usage.en.md](docs/usage.en.md)・[llms.txt](llms.txt)。
102
102
  - **skill**: `skills/thth/SKILL.md`(wheel にも入る)。
103
103
 
104
- - **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`・`notifications`・`report`・`admin`。
104
+ - **動くもの**(`thth --help` の全サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`after`・`analytics-report`・`study-report`・`study`・`unanswered`・`handoff-report`・`observe`・`morning`・`serve-reports`・`approval-worker`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`board`・`collect`・`pull`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`・`mentions`・`profile`・`thread`・`where`・`who`・`retract`・`location`・`notifications`・`report`・`admin`。
105
105
  - **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
106
106
  - **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
107
107
  - **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
@@ -295,12 +295,28 @@ TOOLS = [
295
295
  },
296
296
  },
297
297
  {
298
- # **名前と説明文がそのまま売り文句**(設計 3.1.0 §1)。
298
+ # **名前と説明文がそのまま売り文句**(設計 3.1.0 §1・3.3.0 §F で観測に)。
299
+ "name": "thth_observe",
300
+ "description": (
301
+ "セッションの始めと区切りごとに呼ぶ。前回の観測から何があって、いまなにを"
302
+ "すればよいかを、道具が事実だけで 1 枚にする。本文は作らない"
303
+ ),
304
+ "inputSchema": {
305
+ "type": "object",
306
+ "properties": {
307
+ "target": {"type": "string", "description": "project 名か account 名"},
308
+ "mark": {"type": "boolean",
309
+ "description": "栞(前回読んだ時点)を進める(既定 true)"},
310
+ },
311
+ "required": ["target"],
312
+ },
313
+ },
314
+ {
315
+ # 別名(3.3.0 §F)。既存の skill・セッションを壊さないために残す。
299
316
  "name": "thth_morning",
300
- # **設計 3.1.0 §0 の約束そのまま。**
301
317
  "description": (
302
- "朝いちばんに呼ぶ。昨日から何があって、今日なにをすればよいかを、"
303
- "道具が事実だけで 1 枚にする。本文は作らない"
318
+ "thth_observe の別名(同じ 1 枚を返す)。セッションの始めと区切りごとに呼ぶ。"
319
+ "前回の観測から何があって、いまなにをすればよいかを、道具が事実だけで 1 枚にする"
304
320
  ),
305
321
  "inputSchema": {
306
322
  "type": "object",
@@ -589,16 +605,25 @@ del _tool
589
605
  # credential の `actor` が決める(要求の欄からは作らない)。
590
606
  REPORT_TOOLS = [
591
607
  {"name": "thth_report_file",
592
- "description": "道具が断った・結果が期待と違った・欲しい形がある、のどれかならこれで置く"
593
- "(kind は bug か request。実装側が読み、返事は thth_report_show と "
608
+ "description": "道具が断った・結果が期待と違った・欲しい形がある・迷った、のどれかならこれで置く。"
609
+ "つまずき・迷い・期待との違いの報告は、利用者の作業の一部として歓迎します。"
610
+ "小さいものも。重複は道具が束ねます。"
611
+ "例: 「止まったのに気づかなかった」「断られた理由が分からなかった」"
612
+ "「同じ操作を 3 回繰り返した」。断られた直後なら from_last_refusal: true で"
613
+ "道具が控えた断りを再現手順に添えられる"
614
+ "(kind は bug・request・friction〔迷った・分かりにくかった・同じ操作を繰り返した〕。"
615
+ "実装側が読み、返事は thth_report_show と "
594
616
  "operations_handoff の tool.reports に出る。秘密らしき値が含まれていたら置かない)",
595
617
  "inputSchema": {"type": "object", "properties": {
596
618
  "account": {"type": "string"},
597
- "kind": {"type": "string", "enum": ["bug", "request"]},
619
+ "kind": {"type": "string", "enum": ["bug", "request", "friction"]},
598
620
  "title": {"type": "string", "description": "1 行・120 字まで"},
599
- "body": {"type": "string", "description": "8,000 字まで"},
600
- "repro": {"type": "string", "description": "再現手順(任意・4,000 字まで)"}},
601
- "required": ["account", "kind", "title", "body"], "additionalProperties": False}},
621
+ "body": {"type": "string", "description": "8,000 字まで(from_last_refusal のときは省略可)"},
622
+ "repro": {"type": "string", "description": "再現手順(任意・4,000 字まで)"},
623
+ "from_last_refusal": {"type": "boolean",
624
+ "description": "この account で直前に道具が断った記録(時刻・版・命令・"
625
+ "理由の符丁)を再現手順として添える。kind の既定は friction"}},
626
+ "required": ["account", "title"], "additionalProperties": False}},
602
627
  {"name": "thth_report_list",
603
628
  "description": "自分の project の報告と、実装側の返事の数を読む(読むだけ)",
604
629
  "inputSchema": {"type": "object", "properties": {
@@ -630,12 +655,16 @@ def _with_report_hint(tools):
630
655
  else dict(tool, description=tool["description"] + REPORT_HINT) for tool in tools]
631
656
 
632
657
 
633
- def _channel_note():
634
- """断りの応答に添える 2 つ目の text(静的な 1 行・本文も account 名も入れない)。"""
658
+ def _channel_note(account=None, reason=None):
659
+ """断りの応答に添える 2 つ目の text。本文は入れない。
660
+
661
+ account(credential が許した・台帳にある名前)と理由の符丁が分かれば、そのまま
662
+ 呼べる `thth_report_file` の 1 行(設計 3.3.0 B3)。分からなければ静的な 1 行。
663
+ """
635
664
  if APP_DIR not in sys.path:
636
665
  sys.path.insert(0, APP_DIR)
637
- from thth.report_inbox import CHANNEL_LINE
638
- return {"type": "text", "text": CHANNEL_LINE}
666
+ from thth.report_inbox import mcp_refusal_line
667
+ return {"type": "text", "text": mcp_refusal_line(account, reason)}
639
668
 
640
669
 
641
670
  def server_mode():
@@ -649,7 +678,7 @@ def server_tools(context):
649
678
  if context.scope=='admin': return reports + ADMIN_TOOLS
650
679
  # 毎朝の一枚は**利用者の scope だけ**(設計 3.1.0 §1)。credential が許した
651
680
  # account/project しか対象にできない(`report_service.execute_morning()`)。
652
- reports = reports + [tool for tool in TOOLS if tool['name']=='thth_morning']
681
+ reports = reports + [tool for tool in TOOLS if tool['name'] in ('thth_observe', 'thth_morning')]
653
682
  from thth.server_writes import WRITE_OPERATIONS
654
683
  return _with_report_hint(reports + [tool for tool in SERVER_TOOLS
655
684
  if context.writes or tool['name'][5:] not in WRITE_OPERATIONS]) + REPORT_TOOLS
@@ -659,8 +688,11 @@ def server_call(name, arguments):
659
688
  from thth.report_service import execute_report, execute_mcp_report, ReportServiceError
660
689
  from thth.server_writes import execute, WRITE_OPERATIONS, SAFE_ERRORS, DRAFT_REASONS
661
690
  context=authenticated_context()
662
- # 断りは 1 つ目の text が静的な理由、2 つ目が受け口の案内(設計 3.1.2 §3.5)。
663
- failure=lambda value:{'content':[{'type':'text','text':value},_channel_note()],'isError':True}
691
+
692
+ def failure(value):
693
+ # 断りは 1 つ目の text が静的な理由、2 つ目が受け口の案内(設計 3.1.2 §3.5)。
694
+ account, code = _remember_refusal(context, name, arguments, value)
695
+ return {'content':[{'type':'text','text':value},_channel_note(account, code)],'isError':True}
664
696
  if context is None: return failure('unauthorized')
665
697
  tool=next((tool for tool in server_tools(context) if tool['name']==name),None)
666
698
  if tool is None: return failure('unsupported_operation')
@@ -692,9 +724,10 @@ def server_call(name, arguments):
692
724
  elif operation in ('report_file','report_list','report_show','report_add'):
693
725
  from thth.report_service import execute_user_reports
694
726
  result=execute_user_reports(context,request)
695
- elif operation=='morning':
727
+ elif operation in ('morning', 'observe'):
696
728
  from thth.report_service import execute_morning
697
- result=execute_morning(context,request)
729
+ request={**request,'operation':'morning'}
730
+ result=execute_morning(context,request,invoked_as=operation)
698
731
  elif operation in ('analytics_report', 'operations_handoff', 'study_report'):
699
732
  result=execute_mcp_report(context,request)
700
733
  else:
@@ -714,6 +747,34 @@ def server_call(name, arguments):
714
747
  return failure('request_unavailable')
715
748
 
716
749
 
750
+ def _remember_refusal(context, name, arguments, value):
751
+ """サーバ型の断りを account ごとに控える(設計 3.3.0 B2)。控えの失敗で断りを変えない。
752
+
753
+ 戻り値は断りの 2 つ目の text に埋める `(account, 符丁)`(B3・分からなければ None)。
754
+
755
+ account は credential が許した名前のときだけ。残すのは道具の名前と、断りの
756
+ 先頭の静的な符丁だけ(`thth/refusals.py`・引数の値は残さない)。報告の口の
757
+ 断りは控えない(報告しようとしていた元の断りを押し出さないため)。
758
+ """
759
+ try:
760
+ if context is None or not isinstance(arguments, dict) or not isinstance(name, str):
761
+ return None, None
762
+ account = arguments.get('account')
763
+ if not isinstance(account, str) or account not in (context.allowed_accounts or {}):
764
+ return None, None
765
+ if not name.replace('_', '').isalnum():
766
+ return None, None
767
+ from thth import refusals
768
+ command = 'mcp ' + name
769
+ values = [v for v in arguments.values() if isinstance(v, str)]
770
+ code = refusals.reason_code(value, argv=values, command=command, rc=2)
771
+ if not name.startswith('thth_report_'):
772
+ refusals.record(account, command=command, reason_code=code)
773
+ return account, code
774
+ except Exception:
775
+ return None, None
776
+
777
+
717
778
  def admin_context():
718
779
  context=authenticated_context()
719
780
  return context if context is not None and context.scope=='admin' else None
@@ -892,10 +953,10 @@ def call_tool(name: str, arguments: dict | None) -> dict:
892
953
  args.append("--json")
893
954
  proc = run_cli(args)
894
955
  text = proc.stdout
895
- elif name == "thth_morning":
956
+ elif name in ("thth_morning", "thth_observe"):
896
957
  # **`where_to_appear` と同じ型**: CLI(`thth morning`)を `--json` で
897
958
  # 呼ぶだけ(設計 3.1.0 §1・§5)。栞を進めないときだけ旗を足す。
898
- args = ["morning", arguments["target"]]
959
+ args = [name[len("thth_"):], arguments["target"]]
899
960
  if arguments.get("mark") is False:
900
961
  args.append("--no-mark")
901
962
  args.append("--json")
@@ -925,7 +986,7 @@ def call_tool(name: str, arguments: dict | None) -> dict:
925
986
 
926
987
  if name.startswith("thth_topic_") or name in (
927
988
  "before_you_post", "after_you_posted", "analytics_report", "operations_handoff", "study_report", "thread_read", "where_to_appear",
928
- "who_is_this", "thth_morning"):
989
+ "who_is_this", "thth_morning", "thth_observe"):
929
990
  # **新しい道具は exit 1 も isError**(設計 §7)。lint の exit 1(検査結果)
930
991
  # とは意味が違う——こちらは stale_context・不正な候補比較で、
931
992
  # **そのまま使ってはいけない**応答。既存の扱いは変えない。
@@ -3,9 +3,9 @@ name: thth
3
3
  description: 投稿する前・返信を書く前・出したあと・絡みに行く先を選ぶときに呼ぶ道具。Threads・Bluesky・Mastodon への下書きを人の承認を通してから出し、枝を読み、相手を知り、伸びを測る。
4
4
  ---
5
5
 
6
- **開始時は、まず `thth_morning`(CLI は `thth morning <project|account> --json`)を 1 回。** 昨日から何があって今日なにをすればよいかが 6 段(道具・返していないもの・昨日の自分・世間・予定・次の一手)で返る。`handoff-report --since-last-read` を包含するので、版の確認もこれ 1 回で済む。呼ぶと栞(前回読んだ時点)が進む——進めたくないときだけ `mark: false`(CLI は `--no-mark`)。取れなかった段は `{"value": null, "cannot_say": "<理由>"}`(`provider_timeout`・`budget_exhausted`・`scope_missing`・`no_watch_words`・`not_supported`・`unavailable`)で、**0 件とは別**。第 3 段の監視語は**管理者が入れる**(`thth admin watch set <account> 語 … --by <名前>`)——語が無い account は `no_watch_words` で飛ぶので、**代わりに語を選ばない**。第 5 段は候補の列挙だけで、**本文は返らない**(書くのはあなたと人の会話の側)。詳しくは `docs/使い方_毎朝の一枚_3.1.md`。
6
+ **セッションの始めと区切りごとに `thth_observe`(CLI は `thth observe <project|account> --json`)。** 前回の観測から何があって、いまなにをすればよいかが 6 段(道具・返していないもの・前回の観測から・世間・予定・次の一手)で返る(3.3.0。`thth_morning`/`thth morning` は同じものの別名で、JSON の `invoked_as` だけが違う。第 2 段は前回の栞の時刻から今まで・栞が無い/7 日より古いときは前日 JST で、`window.basis` が `since_last_observe` か `yesterday_jst`)。`handoff-report --since-last-read` を包含するので、版の確認もこれ 1 回で済む。呼ぶと栞(前回読んだ時点)が進む——進めたくないときだけ `mark: false`(CLI は `--no-mark`)。取れなかった段は `{"value": null, "cannot_say": "<理由>"}`(`provider_timeout`・`budget_exhausted`・`scope_missing`・`no_watch_words`・`not_supported`・`unavailable`)で、**0 件とは別**。第 3 段の監視語は**管理者が入れる**(`thth admin watch set <account> 語 … --by <名前>`)——語が無い account は `no_watch_words` で飛ぶので、**代わりに語を選ばない**。第 5 段は候補の列挙だけで、**本文は返らない**(書くのはあなたと人の会話の側)。詳しくは `docs/使い方_毎朝の一枚_3.1.md`。
7
7
 
8
- **断られたら・違和感があれば報告の口へ**: `thth_report_file`(CLI は `thth report file <account> --kind bug|request --title … --body-file … --by <名前>`)。道具が断った・結果が期待と違った・欲しい形がある、のどれかなら 1 回で置ける。返事は `thth_report_show` と `handoff-report --since-last-read` の `tool.reports` に出る。秘密らしき値が含まれていたら置かれない(伏せてから置き直す)。
8
+ **断られたら・違和感があれば報告の口へ**: `thth_report_file`(CLI は `thth report file <account> --kind bug|request|friction --title … --body-file … --by <名前>`)。つまずき・迷い・期待との違いの報告は、利用者の作業の一部として歓迎します。小さいものも。重複は道具が束ねます。例: 「止まったのに気づかなかった」「断られた理由が分からなかった」「同じ操作を 3 回繰り返した」。道具が断った・結果が期待と違った・欲しい形がある・迷った(`friction`)、のどれかなら 1 回で置ける。**断られた直後なら、断りの最後の `report_channel:` の行をそのまま打てばよい**(`--from-last-refusal` が、道具が控えた直前の断り——時刻・版・命令・理由の符丁だけで、本文と引数の値は控えていない——を再現手順に添える。MCP は `thth_report_file {"account": …, "title": …, "from_last_refusal": true}`)。置いた報告がこの版で閉じたかは `thth_observe` の 0 段 `project_reports` に出る。返事は `thth_report_show` と `handoff-report --since-last-read` の `tool.reports` に出る。秘密らしき値が含まれていたら置かれない(伏せてから置き直す)。
9
9
 
10
10
  段だけを読み直したいときは `thth handoff-report <account> --since-last-read --json` を読む。`tool.changed_since_last_read` が真なら、手元の `tool.notes_root_local_hint` を手掛かりに `tool.release_notes` の相対名を読んでから作業する。既読の記録は `--mark-read --by <名前>` の明示時だけ。`tool.notes_reason` が `notes_directory_unavailable` なら、このインストールには読める docs がない。空の `release_notes` を「変更なし」と解釈しない。
11
11
 
@@ -149,7 +149,8 @@ MCP `after_you_posted`。**24h の刻みが無ければ `null`**(0 と混ぜ
149
149
 
150
150
  - `thth --help` / `thth <subcommand> --help` が正本。
151
151
  - `thth doctor <account>` が「いま投稿できる状態か」を最初から最後まで言う。
152
- - 道具の不具合・欲しい形は `thth report file`(MCP `thth_report_file`)で実装側に届ける。
152
+ - 道具の不具合・欲しい形・迷ったことは `thth report file`(MCP `thth_report_file`)で実装側に届ける(小さいものも歓迎・重複は道具が束ねる)。
153
+ - 承認済みなのに出ない原稿は `thth_observe` の予定の段 `held_items`(file・理由・予定・経過)と `thth board` の要確認(先頭 5 本)に名前が出る。`approval_stale` は再承認(`thth approve` の二段)で直る。時刻を過ぎて出られないものがあると `thth run` は `held` として死活通知と運用通知に知らせる。
153
154
  - 自分の開いている報告に書き足すなら `thth report add <report_id> --body-file <path|-> --by <名前>`(MCP `thth_report_add`)。
154
155
  - 使い方の全文: `docs/使い方_プロジェクトのセッション向け_2026-09-09.md`。
155
156
 
@@ -429,3 +429,15 @@ def init_git_pair(tmp_path, *, seed_content: str, seed_name: str = "a.md"):
429
429
 
430
430
  return {"bare": bare, "seed": seed, "work": work,
431
431
  "queue_dir": os.path.join(work, "docs", "sns", "queue")}
432
+
433
+
434
+ def is_refusal(err: str, code: str) -> bool:
435
+ """断りの stderr が「理由の符丁 1 行+打てる報告の 1 行」か(3.1.2 §3.5・3.3.0 B3)。
436
+
437
+ 3.3.0 から 2 行目は account と理由の符丁を埋めた `report_inbox.refusal_line()`。
438
+ account は命令と台帳しだいで `<account>` のままになるので、ここでは形と符丁だけ見る。
439
+ """
440
+ lines = err.strip().splitlines()
441
+ return (len(lines) == 2 and lines[0] == code
442
+ and lines[1].startswith("report_channel: thth report file ")
443
+ and f'--title "{code} で断られた"' in lines[1])
@@ -102,11 +102,12 @@ def test_mcp_stdioでtools_listとtools_callが通る(isolated_account):
102
102
  # 2026-09-16 に `where_to_appear` を足した(設計「自分の泉」§2.3・T2-3・読むだけ)。
103
103
  # 2026-09-16 に `who_is_this` を足した(設計「自分の泉」§2.4・T3-3・読むだけ)。
104
104
  # 2026-09-23 に `thth_morning` を足した(設計 3.1.0 §1・読むだけ・栞だけ進む)。
105
+ # 2026-09-23 に `thth_observe` を足した(設計 3.3.0 §F・thth_morning は別名)。
105
106
  assert tool_names == {"thth_lint", "thth_queue", "thth_preview", "thth_board",
106
107
  "thth_topic_context", "thth_topic_evaluate",
107
108
  "thth_topic_decision", "before_you_post", "after_you_posted",
108
109
  "thread_read", "where_to_appear", "who_is_this", "analytics_report", "operations_handoff", "study_report",
109
- "thth_morning"}
110
+ "thth_morning", "thth_observe"}
110
111
  # 副作用のあるものは 1 つも出ていない。
111
112
  assert not (tool_names & {"thth_approve", "thth_throw", "thth_token",
112
113
  "thth_auth", "thth_refresh", "thth_revoke",
@@ -26,6 +26,8 @@ def json_parsers():
26
26
  COMMANDS = '''lint|preview|approve|account|revoke|posts|replies|measured|threads|study-report|analytics-report|after|topics|forms|queue|schedule|throw|handoff-report|board|pull|maintain|doctor|app show|admin inventory|admin account|admin log|admin tokens|admin timers|admin release|admin diff|ask before-you-post|mentions|profile|thread|where|who|retract|location|topics suggest|topics observe|topics record-decision|topics decision|topics observation|topics retract|topics unretract|topics profile|topics adopt-reason|topics adoptions|topics record-vocabulary|topics vocabulary|topics record-review|topics review|topics record-form-spec|topics form-spec|topics form-check|topics improvements|topics impact|topics record-hypothesis|topics hypotheses'''.split('|') + ['account add', 'account migrate', 'unanswered', 'study add', 'admin deletion sync', 'admin budget', 'admin media cleanup-retry', 'admin media gc',
27
27
  # 3.1.0: 毎朝の一枚と監視語(設計 3.1.0 §1・§3)。
28
28
  'morning', 'admin watch set', 'admin watch show',
29
+ # 3.3.0 §F: 観測(observe)。morning は別名として残る。
30
+ 'observe',
29
31
  # 3.1.2: 報告の口(設計 3.1.2 §1)。
30
32
  'report file', 'report list', 'report show', 'report add', 'admin reports list', 'admin reports show',
31
33
  'admin reports reply', 'admin reports close', 'admin reports export']
@@ -61,7 +61,7 @@ def test_cli_reads_do_not_wait_or_call_core_when_leave_holds_exclusive(env,conte
61
61
  start=time.monotonic();rc=cli.main(command);elapsed=time.monotonic()-start
62
62
  captured=capsys.readouterr();assert rc==2 and elapsed<1
63
63
  assert json.loads(captured.out)=={'error':'account_leaving','cannot_say':['account_leaving']}
64
- assert captured.err.strip()=='account_leaving\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
64
+ assert __import__('tests.conftest').conftest.is_refusal(captured.err, 'account_leaving') # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 1 行
65
65
 
66
66
 
67
67
  def test_dry_send_input_then_nonwaiting_lease(env,tmp_path,capsys):
@@ -69,7 +69,7 @@ def test_dry_send_input_then_nonwaiting_lease(env,tmp_path,capsys):
69
69
  with exclusive():
70
70
  start=time.monotonic();rc=cli.main(['send','alpha','--text-file',str(text)])
71
71
  assert time.monotonic()-start<1 and rc==2
72
- assert capsys.readouterr().err.strip()=='account_leaving\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
72
+ assert __import__('tests.conftest').conftest.is_refusal(capsys.readouterr().err, 'account_leaving') # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 1 行
73
73
  assert not (env['root']/'state/alpha/sent').exists()
74
74
 
75
75
 
@@ -145,7 +145,7 @@ def test_real_cli_child_rejects_ex_under_deadline(env,content):
145
145
  result=subprocess.run([sys.executable,'-c',"from thth.cli import main;raise SystemExit(main(['queue','alpha','--json']))"],capture_output=True,text=True,timeout=2)
146
146
  except subprocess.TimeoutExpired:pytest.fail('reader waited for the exit drain')
147
147
  assert result.returncode==2 and json.loads(result.stdout)['cannot_say']==['account_leaving']
148
- assert result.stderr.strip()=='account_leaving\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
148
+ assert __import__('tests.conftest').conftest.is_refusal(result.stderr, 'account_leaving') # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 1 行
149
149
 
150
150
 
151
151
  def test_report_holds_lease_through_calculation_and_release(env,monkeypatch):
@@ -51,7 +51,7 @@ raise SystemExit(cli.main(json.loads(sys.argv[1])))
51
51
  # cannot trap this pytest process before an elapsed-time assertion.
52
52
  result = subprocess.run([sys.executable, '-c', code, json.dumps(command), str(marker)],
53
53
  capture_output=True, text=True, timeout=3)
54
- assert result.returncode == 2 and result.stderr.strip() == 'account_leaving\n' + __import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
54
+ assert result.returncode == 2 and __import__('tests.conftest').conftest.is_refusal(result.stderr, 'account_leaving') # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 1 行
55
55
  if operation == 'replies':
56
56
  assert json.loads(result.stdout) == {'error': 'account_leaving', 'cannot_say': ['account_leaving']}
57
57
  assert not marker.exists()
@@ -14,7 +14,7 @@ def test_run_invalid_name_before_any_storage_or_effect(monkeypatch,capsys,name):
14
14
  monkeypatch.setattr('thth.incident.notify',forbidden)
15
15
  assert cli.main(['run',name])==2
16
16
  captured=capsys.readouterr();value=json.loads(captured.out)
17
- assert captured.err.strip()=='invalid_account_name\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
17
+ assert __import__('tests.conftest').conftest.is_refusal(captured.err, 'invalid_account_name') # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 1 行
18
18
  assert value['error']==value['record_unavailable']=='invalid_account_name'
19
19
  assert value['account'] is None and value['runs_recorded'] is False
20
20
  assert '使えない字' in value['message'] and 'account_stopped' not in captured.out
@@ -30,7 +30,7 @@ def test_run_reason_and_no_side_effect(env,monkeypatch,capsys,state,expected):
30
30
  monkeypatch.setattr('thth.healthcheck.notify',lambda *a,**kw:pytest.fail('healthcheck notification'))
31
31
  monkeypatch.setattr('thth.incident.notify',lambda *a,**kw:pytest.fail('incident notification'))
32
32
  assert cli.main(['run','alpha'])==2
33
- shown=capsys.readouterr();assert shown.err.strip()==expected+'\n'+__import__('thth.report_inbox').report_inbox.CHANNEL_LINE # 3.1.2 §3.5: 断りの理由行の後ろに受け口の案内 1 行
33
+ shown=capsys.readouterr();assert __import__('tests.conftest').conftest.is_refusal(shown.err, expected) # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 1 行
34
34
  value=json.loads(shown.out)
35
35
  assert value=={'account':'alpha','mode':'production','action':'skip','status':'error','error':expected,
36
36
  'runs_recorded':False,'record_unavailable':expected}
@@ -285,9 +285,10 @@ def test_既定で栞が進み_no_markでは進まない(one, thth_root):
285
285
  assert morning.build(PROJECT, now=NOW, mark=False)["marked"] == []
286
286
  assert not path.exists(), "--no-mark なのに栞が進んだ"
287
287
  payload = morning.build(PROJECT, now=NOW)
288
- assert payload["marked"] == [ACCOUNT] and payload["marked_by"] == "morning"
288
+ # 3.3.0 §F: 栞の名乗りは呼んだ名前(build の既定は observe)。
289
+ assert payload["marked"] == [ACCOUNT] and payload["marked_by"] == "observe"
289
290
  saved = json.loads(path.read_text(encoding="utf-8"))
290
- assert saved["by"] == "morning" and saved["snapshot"]["tool_version"]
291
+ assert saved["by"] == "observe" and saved["snapshot"]["tool_version"]
291
292
 
292
293
 
293
294
  def test_栞を進めたあとは版の変化が読める(one, thth_root):
@@ -349,9 +350,10 @@ def test_MCPのthth_morningは設計の文言でCLIを呼ぶだけ(one, monkeypa
349
350
  monkeypatch.delenv(key, raising=False)
350
351
  server = _load_server_module()
351
352
  tool = next(item for item in server.TOOLS if item["name"] == "thth_morning")
353
+ # 3.3.0 §F: thth_morning は thth_observe の別名になった(説明文も観測の文言)。
352
354
  assert tool["description"] == (
353
- "朝いちばんに呼ぶ。昨日から何があって、今日なにをすればよいかを、"
354
- "道具が事実だけで 1 枚にする。本文は作らない")
355
+ "thth_observe の別名(同じ 1 枚を返す)。セッションの始めと区切りごとに呼ぶ。"
356
+ "前回の観測から何があって、いまなにをすればよいかを、道具が事実だけで 1 枚にする")
355
357
  seen = []
356
358
 
357
359
  class Done:
@@ -21,17 +21,26 @@ import pytest
21
21
 
22
22
  from thth import accounts, cli, jst, morning, operations_handoff, report_inbox
23
23
 
24
- # **文面は 1 種類・静的**(設計 §3.5)。ここが正本の写し——変えるなら設計と一緒に。
25
- CHANNEL_LINE = "report_channel: thth report file <account> --kind bug|request(MCP: thth_report_file)"
26
- LIMITATION = "不具合と要望は report の口へ(thth_report_file)"
24
+ # **文面は静的**(設計 §3.5)。ここが正本の写し——変えるなら設計と一緒に。
25
+ # 3.3.0 B1・B4: friction と「報告は依頼の範囲内」の 1 文を足した。断りの行は
26
+ # account と理由の符丁を埋めた「打てる 1 行」になった(B3・`refusal_line()`)。
27
+ WELCOME = ("つまずき・迷い・期待との違いの報告は、利用者の作業の一部として歓迎します。"
28
+ "小さいものも。重複は道具が束ねます")
29
+ CHANNEL_LINE = ("report_channel: thth report file <account> --kind bug|request|friction"
30
+ "(MCP: thth_report_file)——" + WELCOME)
31
+ LIMITATION = "不具合・要望・つまずきは report の口へ(thth_report_file)"
27
32
  HINT = "(うまくいかなければ thth_report_file)"
28
33
 
29
34
 
30
35
  def test_受け口の文面は1種類で設計のまま():
31
36
  assert report_inbox.CHANNEL_LINE == CHANNEL_LINE
32
37
  assert report_inbox.CHANNEL == {
33
- "cli": "thth report file <account> --kind bug|request --title … --body-file … --by <名前>",
34
- "mcp": "thth_report_file", "when": "unexpected_or_unsupported"}
38
+ "cli": "thth report file <account> --kind bug|request|friction --title … --body-file … --by <名前>",
39
+ "from_last_refusal": "thth report file <account> --from-last-refusal --title … --by <名前>",
40
+ "mcp": "thth_report_file", "when": "unexpected_or_unsupported",
41
+ "welcome": WELCOME,
42
+ "examples": ["止まったのに気づかなかった", "断られた理由が分からなかった",
43
+ "同じ操作を 3 回繰り返した"]}
35
44
 
36
45
 
37
46
  @pytest.fixture
@@ -169,7 +178,10 @@ def test_サーバ型の利用者の一枚には件数を出さない(projects,
169
178
  def test_CLIの断りの後ろに受け口の1行_成功には載せない(projects, tmp_path, capsys):
170
179
  assert cli.main(["morning", "nobody", "--json"]) == 2
171
180
  err = capsys.readouterr().err.strip().splitlines()
172
- assert err == ["target_unknown", CHANNEL_LINE]
181
+ # 3.3.0 B3: 理由の符丁を埋めた打てる 1 行(account を持たない命令は <account> のまま)。
182
+ assert err == ["target_unknown",
183
+ 'report_channel: thth report file <account> --kind friction --from-last-refusal'
184
+ ' --title "target_unknown で断られた" --by <名前>(' + WELCOME + ')']
173
185
  assert cli.main(["report", "list", "kopicha", "--json"]) == 0
174
186
  assert CHANNEL_LINE not in capsys.readouterr().err
175
187
 
@@ -179,14 +191,14 @@ def test_lintの検査結果の1は断りではない(projects, tmp_path, capsys
179
191
  bad.write_text("---\nthth: 1\n---\n本文\n", encoding="utf-8")
180
192
  rc = cli.main(["lint", str(bad), "--json"])
181
193
  assert rc == 1
182
- assert CHANNEL_LINE not in capsys.readouterr().err
194
+ assert "report_channel" not in capsys.readouterr().err
183
195
 
184
196
 
185
197
  def test_helpの末尾に1行(capsys):
186
198
  with pytest.raises(SystemExit):
187
199
  cli.main(["--help"])
188
200
  out = capsys.readouterr().out.rstrip().splitlines()
189
- assert out[-1] == "不具合と要望は report の口へ: thth report file <account> --kind bug|request(MCP: thth_report_file)"
201
+ assert out[-1] == "不具合・要望・つまずきは report の口へ: thth report file <account> --kind bug|request|friction(MCP: thth_report_file)"
190
202
  with pytest.raises(SystemExit):
191
203
  cli.main(["report", "--help"])
192
204
  assert "道具が断った・結果が期待と違った・欲しい形がある" in capsys.readouterr().out
@@ -255,7 +267,7 @@ def test_利用者のtools_listの説明文の末尾に1句(user_server, monkeyp
255
267
  else:
256
268
  assert tool["description"].endswith(HINT), tool["name"]
257
269
  file_tool = next(tool for tool in listed if tool["name"] == "thth_report_file")
258
- assert file_tool["description"].startswith("道具が断った・結果が期待と違った・欲しい形がある、のどれかならこれで置く")
270
+ assert file_tool["description"].startswith("道具が断った・結果が期待と違った・欲しい形がある・迷った、のどれかならこれで置く")
259
271
  # 定数(売り文句の正本)は書き換えない。
260
272
  assert not any(tool["description"].endswith(HINT) for tool in server.TOOLS)
261
273
  # 管理者の一覧には足さない(管理者は報告を置く側ではない)。
@@ -0,0 +1,127 @@
1
+ """3.3.0 A3 版上げで承認が壊れるときは、その版が言う(FINGERPRINT_VERSION と golden)。
2
+
3
+ - 既知の入力で指紋の計算が変わったのに `approval.FINGERPRINT_VERSION` を上げて
4
+ いなければ落ちる(`fingerprint_golden.drift()`)。
5
+ - 指紋の版が変わった版へ上がったら、`handoff-report` の `tool` と `thth morning` の
6
+ 0 段に「この版で再承認が要る原稿: n 本(account ごと)」、運用通知に 1 回だけ。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+
13
+ from tests.conftest import write_queue_file
14
+ from tests.test_v330_held import held_run # noqa: F401
15
+ from thth import approval, fingerprint_golden, handoff_cursor, jst, morning
16
+ from thth import healthcheck, incident
17
+ from thth import operations_handoff
18
+ from thth import tags as tags_mod
19
+ from thth import tool_version
20
+
21
+
22
+ def test_いまの指紋はいまの版のgoldenと一致する():
23
+ assert fingerprint_golden.drift() == [], (
24
+ "承認の指紋の計算が変わりました。意図した変更なら approval.FINGERPRINT_VERSION を"
25
+ " 1 上げ、FINGERPRINT_HISTORY に道具の版を足し、thth/fingerprint_golden.py の"
26
+ " GOLDEN に新しい版の行を足してください(既存の承認は approval_stale になります)")
27
+
28
+
29
+ def test_版と履歴とgoldenは揃っている():
30
+ assert max(approval.FINGERPRINT_HISTORY) == approval.FINGERPRINT_VERSION
31
+ assert approval.FINGERPRINT_VERSION in fingerprint_golden.GOLDEN
32
+ for introduced in approval.FINGERPRINT_HISTORY.values():
33
+ assert tool_version.numbers(introduced) is not None
34
+ # 見本は全部 golden に行がある(比べ漏らさない)。
35
+ names = {case["name"] for case in fingerprint_golden.CASES}
36
+ assert names == set(fingerprint_golden.GOLDEN[approval.FINGERPRINT_VERSION])
37
+
38
+
39
+ def test_タグの付け方が変わると検出する(monkeypatch):
40
+ original = tags_mod.prepared
41
+
42
+ def changed(media, text, topic, *, hashtags):
43
+ out = original(media, text, topic, hashtags=hashtags)
44
+ return out + "!" if media == "bluesky" and topic else out
45
+
46
+ monkeypatch.setattr(tags_mod, "prepared", changed)
47
+ drift = fingerprint_golden.drift()
48
+ assert "bluesky_topic_tag" in drift and "threads_plain" not in drift
49
+
50
+
51
+ def test_版を上げてgoldenを足さなければ落ちる(monkeypatch):
52
+ monkeypatch.setattr(approval, "FINGERPRINT_VERSION", approval.FINGERPRINT_VERSION + 1)
53
+ assert fingerprint_golden.drift() == ["no_golden_for_version"]
54
+
55
+
56
+ def test_道具の版の間に指紋の版が変わったか():
57
+ assert approval.fingerprint_changed_between("2.7.3", "3.3.0") is True
58
+ assert approval.fingerprint_changed_between("2.8.0", "3.3.0") is False
59
+ assert approval.fingerprint_changed_between("3.2.0", "3.3.0") is False
60
+ assert approval.fingerprint_changed_between(None, "3.3.0") is None
61
+ summary = tool_version.summary("2.7.0")
62
+ assert summary["fingerprint_version"] == approval.FINGERPRINT_VERSION
63
+ assert summary["fingerprint_changed_since_last_read"] is True
64
+ assert tool_version.summary()["fingerprint_changed_since_last_read"] is None
65
+
66
+
67
+ def _cursor_at(name, version, now):
68
+ node = operations_handoff.answer(name, now=now)["by_account"][name]
69
+ node["tool"] = {**node["tool"], "version": version}
70
+ handoff_cursor.write(name, node, "テスト", now)
71
+
72
+
73
+ def test_指紋の版が変わった版へ上がるとhandoffとmorningが再承認の本数を言う(isolated_account):
74
+ name = isolated_account["name"]
75
+ now = jst.now_jst()
76
+ write_queue_file(isolated_account["queue_dir"], "stale.md",
77
+ fm_overrides={"approved_sha": "deadbeef",
78
+ "publish_at": "2026-09-10T08:00:00+09:00"})
79
+ write_queue_file(isolated_account["queue_dir"], "ok.md",
80
+ fm_overrides={"publish_at": "2026-09-10T09:00:00+09:00"},
81
+ body="## threads\n\n別の本文。\n")
82
+ _cursor_at(name, "2.7.0", now)
83
+ payload = operations_handoff.answer(name, now=now, since_last_read=True)
84
+ needed = payload["by_account"][name]["tool"]["reapproval_required"]
85
+ assert needed == {"n": 1, "fingerprint_version": approval.FINGERPRINT_VERSION,
86
+ "files": ["stale.md"], "cannot_say": None}
87
+ assert "この版で再承認が要る原稿: 1 本" in operations_handoff.render_markdown(payload)
88
+
89
+ tool = {s["section"]: s for s in morning.build(name, now=now, mark=False)["sections"]}["tool"]
90
+ assert tool["value"]["reapproval_required"]["n"] == 1
91
+ assert tool["value"]["reapproval_required"]["by_account"] == {name: 1}
92
+ lines = []
93
+ morning.render(morning.build(name, now=now, mark=False), out=lines.append)
94
+ assert any("この版で再承認が要る原稿: 1 本" in line for line in lines)
95
+
96
+
97
+ def test_指紋の版が変わっていなければ言わない(isolated_account):
98
+ name = isolated_account["name"]
99
+ now = jst.now_jst()
100
+ write_queue_file(isolated_account["queue_dir"], "stale.md",
101
+ fm_overrides={"approved_sha": "deadbeef"})
102
+ _cursor_at(name, "2.8.0", now)
103
+ payload = operations_handoff.answer(name, now=now, since_last_read=True)
104
+ assert "reapproval_required" not in payload["by_account"][name]["tool"]
105
+ tool = {s["section"]: s for s in morning.build(name, now=now, mark=False)["sections"]}["tool"]
106
+ assert tool["value"]["reapproval_required"] is None
107
+
108
+
109
+ def test_指紋の版が上がったら運用通知に1回だけ流す(held_run): # noqa: F811
110
+ write_queue_file(held_run.account["queue_dir"], "later.md",
111
+ fm_overrides={"approved_sha": "deadbeef",
112
+ "publish_at": "2026-09-10T08:00:00+09:00"})
113
+ outbox = Path(held_run.state) / incident.STATE_FILE
114
+ assert held_run.run() == 0
115
+ # 初めての run は版を控えるだけ(どこから変わったか言えないので知らせない)。
116
+ assert json.loads(outbox.read_text())["fingerprint_version"] == approval.FINGERPRINT_VERSION
117
+ assert held_run.mails == []
118
+ row = json.loads(outbox.read_text())
119
+ row["fingerprint_version"] = approval.FINGERPRINT_VERSION - 1
120
+ incident._save(outbox, row)
121
+ assert held_run.run() == 0
122
+ assert held_run.mails == [("user@example.org", "notice", "reapproval_required: 1"),
123
+ ("admin@example.org", "notice", "reapproval_required: 1")]
124
+ assert held_run.run() == 0
125
+ assert len(held_run.mails) == 2 # 1 回だけ
126
+ text = healthcheck.reason_text("reapproval_required: 1")
127
+ assert "再承認が要る原稿: 1 本" in text