thth 3.2.0__tar.gz → 3.3.1__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 (510) hide show
  1. {thth-3.2.0 → thth-3.3.1}/PKG-INFO +2 -2
  2. {thth-3.2.0 → thth-3.3.1}/README.md +1 -1
  3. {thth-3.2.0 → thth-3.3.1}/mcp/server.py +83 -22
  4. {thth-3.2.0 → thth-3.3.1}/skills/thth/SKILL.md +5 -4
  5. {thth-3.2.0 → thth-3.3.1}/tests/conftest.py +22 -0
  6. thth-3.3.1/tests/helpers/fake_threads_status.py +123 -0
  7. {thth-3.2.0 → thth-3.3.1}/tests/test_incident_notifications.py +4 -2
  8. {thth-3.2.0 → thth-3.3.1}/tests/test_mcp.py +2 -1
  9. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_json_contract.py +5 -1
  10. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_read_coordination.py +3 -3
  11. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_carryover_timeout.py +1 -1
  12. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_review_followups.py +1 -1
  13. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_stop_observation.py +1 -1
  14. {thth-3.2.0 → thth-3.3.1}/tests/test_v310_morning.py +6 -4
  15. {thth-3.2.0 → thth-3.3.1}/tests/test_v312_report_channel.py +21 -9
  16. thth-3.3.1/tests/test_v330_fingerprint_version.py +127 -0
  17. thth-3.3.1/tests/test_v330_held.py +270 -0
  18. thth-3.3.1/tests/test_v330_held_names.py +92 -0
  19. thth-3.3.1/tests/test_v330_last_refusals.py +156 -0
  20. thth-3.3.1/tests/test_v330_notification_route.py +97 -0
  21. thth-3.3.1/tests/test_v330_observe.py +137 -0
  22. thth-3.3.1/tests/test_v330_report_friction.py +50 -0
  23. thth-3.3.1/tests/test_v330_report_words.py +103 -0
  24. thth-3.3.1/tests/test_v330_reports_effect.py +66 -0
  25. thth-3.3.1/tests/test_v331_inflight_cli.py +156 -0
  26. thth-3.3.1/tests/test_v331_listing_resolve.py +136 -0
  27. thth-3.3.1/tests/test_v331_outbox_retention.py +135 -0
  28. thth-3.3.1/tests/test_v331_publish_status.py +213 -0
  29. thth-3.3.1/tests/test_v331_self_resolve.py +222 -0
  30. thth-3.3.1/thth/VERSION +1 -0
  31. {thth-3.2.0 → thth-3.3.1}/thth/adapters/base.py +6 -0
  32. {thth-3.2.0 → thth-3.3.1}/thth/adapters/threads.py +177 -7
  33. {thth-3.2.0 → thth-3.3.1}/thth/admin_log.py +3 -1
  34. {thth-3.2.0 → thth-3.3.1}/thth/approval.py +35 -0
  35. {thth-3.2.0 → thth-3.3.1}/thth/cli.py +116 -10
  36. {thth-3.2.0 → thth-3.3.1}/thth/core.py +122 -13
  37. {thth-3.2.0 → thth-3.3.1}/thth/doctor.py +11 -0
  38. thth-3.3.1/thth/fingerprint_golden.py +101 -0
  39. {thth-3.2.0 → thth-3.3.1}/thth/healthcheck.py +57 -3
  40. {thth-3.2.0 → thth-3.3.1}/thth/incident.py +185 -19
  41. {thth-3.2.0 → thth-3.3.1}/thth/inflight.py +61 -8
  42. thth-3.3.1/thth/inflight_cli.py +301 -0
  43. thth-3.3.1/thth/inflight_resolve.py +435 -0
  44. {thth-3.2.0 → thth-3.3.1}/thth/leave_gate.py +1 -1
  45. {thth-3.2.0 → thth-3.3.1}/thth/morning.py +193 -31
  46. thth-3.3.1/thth/notification_route.py +70 -0
  47. {thth-3.2.0 → thth-3.3.1}/thth/operations_handoff.py +42 -1
  48. {thth-3.2.0 → thth-3.3.1}/thth/read_coordination.py +2 -2
  49. thth-3.3.1/thth/refusals.py +162 -0
  50. {thth-3.2.0 → thth-3.3.1}/thth/report.py +14 -2
  51. {thth-3.2.0 → thth-3.3.1}/thth/report_inbox.py +129 -17
  52. {thth-3.2.0 → thth-3.3.1}/thth/report_service.py +21 -8
  53. {thth-3.2.0 → thth-3.3.1}/thth/runs.py +8 -0
  54. {thth-3.2.0 → thth-3.3.1}/thth/select.py +94 -1
  55. {thth-3.2.0 → thth-3.3.1}/thth/tool_version.py +8 -0
  56. thth-3.2.0/thth/VERSION +0 -1
  57. {thth-3.2.0 → thth-3.3.1}/.gitignore +0 -0
  58. {thth-3.2.0 → thth-3.3.1}/LICENSE +0 -0
  59. {thth-3.2.0 → thth-3.3.1}/accounts.example/bluesky.json +0 -0
  60. {thth-3.2.0 → thth-3.3.1}/accounts.example/mastodon.json +0 -0
  61. {thth-3.2.0 → thth-3.3.1}/accounts.example/threads.json +0 -0
  62. {thth-3.2.0 → thth-3.3.1}/accounts.example/x.json +0 -0
  63. {thth-3.2.0 → thth-3.3.1}/bin/thth +0 -0
  64. {thth-3.2.0 → thth-3.3.1}/bin/thth-remote +0 -0
  65. {thth-3.2.0 → thth-3.3.1}/bin/thth-run +0 -0
  66. {thth-3.2.0 → thth-3.3.1}/pyproject.toml +0 -0
  67. {thth-3.2.0 → thth-3.3.1}/skills/thth-admin/SKILL.md +0 -0
  68. {thth-3.2.0 → thth-3.3.1}/systemd/thth@.service +0 -0
  69. {thth-3.2.0 → thth-3.3.1}/tests/__init__.py +0 -0
  70. {thth-3.2.0 → thth-3.3.1}/tests/coordination_snapshot.py +0 -0
  71. {thth-3.2.0 → thth-3.3.1}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
  72. {thth-3.2.0 → thth-3.3.1}/tests/fixtures/build_injection_eval.py +0 -0
  73. {thth-3.2.0 → thth-3.3.1}/tests/fixtures/callback/worker_responses.json +0 -0
  74. {thth-3.2.0 → thth-3.3.1}/tests/fixtures/injection_eval.json +0 -0
  75. {thth-3.2.0 → thth-3.3.1}/tests/fixtures/umami-bile.md +0 -0
  76. {thth-3.2.0 → thth-3.3.1}/tests/helpers/__init__.py +0 -0
  77. {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_oauth_server.py +0 -0
  78. {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_push_adapter.py +0 -0
  79. {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_redirect_server.py +0 -0
  80. {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_search_adapter.py +0 -0
  81. {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_threads_reader.py +0 -0
  82. {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_threads_writes.py +0 -0
  83. {thth-3.2.0 → thth-3.3.1}/tests/helpers/fake_x.py +0 -0
  84. {thth-3.2.0 → thth-3.3.1}/tests/helpers/hold_lock.py +0 -0
  85. {thth-3.2.0 → thth-3.3.1}/tests/helpers/hold_repo_lock.py +0 -0
  86. {thth-3.2.0 → thth-3.3.1}/tests/helpers/worker_probe.mjs +0 -0
  87. {thth-3.2.0 → thth-3.3.1}/tests/test_27_acceptance_fixes.py +0 -0
  88. {thth-3.2.0 → thth-3.3.1}/tests/test_account_name_check.py +0 -0
  89. {thth-3.2.0 → thth-3.3.1}/tests/test_account_report.py +0 -0
  90. {thth-3.2.0 → thth-3.3.1}/tests/test_accounts_dir.py +0 -0
  91. {thth-3.2.0 → thth-3.3.1}/tests/test_adapter_boundary.py +0 -0
  92. {thth-3.2.0 → thth-3.3.1}/tests/test_admin_http_mcp.py +0 -0
  93. {thth-3.2.0 → thth-3.3.1}/tests/test_admin_log.py +0 -0
  94. {thth-3.2.0 → thth-3.3.1}/tests/test_admin_notifications.py +0 -0
  95. {thth-3.2.0 → thth-3.3.1}/tests/test_admin_repairs.py +0 -0
  96. {thth-3.2.0 → thth-3.3.1}/tests/test_admin_report.py +0 -0
  97. {thth-3.2.0 → thth-3.3.1}/tests/test_after_cli.py +0 -0
  98. {thth-3.2.0 → thth-3.3.1}/tests/test_analytics_comparison.py +0 -0
  99. {thth-3.2.0 → thth-3.3.1}/tests/test_analytics_report.py +0 -0
  100. {thth-3.2.0 → thth-3.3.1}/tests/test_api_diagnostic.py +0 -0
  101. {thth-3.2.0 → thth-3.3.1}/tests/test_api_error_body.py +0 -0
  102. {thth-3.2.0 → thth-3.3.1}/tests/test_app_env_cli.py +0 -0
  103. {thth-3.2.0 → thth-3.3.1}/tests/test_approval.py +0 -0
  104. {thth-3.2.0 → thth-3.3.1}/tests/test_approve_cmd.py +0 -0
  105. {thth-3.2.0 → thth-3.3.1}/tests/test_ask.py +0 -0
  106. {thth-3.2.0 → thth-3.3.1}/tests/test_atlas_fifth_review.py +0 -0
  107. {thth-3.2.0 → thth-3.3.1}/tests/test_atlas_fourth_review.py +0 -0
  108. {thth-3.2.0 → thth-3.3.1}/tests/test_atlas_review.py +0 -0
  109. {thth-3.2.0 → thth-3.3.1}/tests/test_atlas_second_review.py +0 -0
  110. {thth-3.2.0 → thth-3.3.1}/tests/test_atlas_third_review.py +0 -0
  111. {thth-3.2.0 → thth-3.3.1}/tests/test_audit1_fixes.py +0 -0
  112. {thth-3.2.0 → thth-3.3.1}/tests/test_audit2_fixes.py +0 -0
  113. {thth-3.2.0 → thth-3.3.1}/tests/test_audit_b_counterexamples.py +0 -0
  114. {thth-3.2.0 → thth-3.3.1}/tests/test_audit_b_m1_semantics.py +0 -0
  115. {thth-3.2.0 → thth-3.3.1}/tests/test_audit_b_n1_depth.py +0 -0
  116. {thth-3.2.0 → thth-3.3.1}/tests/test_audit_b_r1_closure.py +0 -0
  117. {thth-3.2.0 → thth-3.3.1}/tests/test_audit_v201.py +0 -0
  118. {thth-3.2.0 → thth-3.3.1}/tests/test_audit_v23_details.py +0 -0
  119. {thth-3.2.0 → thth-3.3.1}/tests/test_auth.py +0 -0
  120. {thth-3.2.0 → thth-3.3.1}/tests/test_auth_identity_required.py +0 -0
  121. {thth-3.2.0 → thth-3.3.1}/tests/test_behind_remote.py +0 -0
  122. {thth-3.2.0 → thth-3.3.1}/tests/test_bluesky_adapter.py +0 -0
  123. {thth-3.2.0 → thth-3.3.1}/tests/test_bluesky_secret_scrub.py +0 -0
  124. {thth-3.2.0 → thth-3.3.1}/tests/test_board_needs_review.py +0 -0
  125. {thth-3.2.0 → thth-3.3.1}/tests/test_board_running.py +0 -0
  126. {thth-3.2.0 → thth-3.3.1}/tests/test_bundle.py +0 -0
  127. {thth-3.2.0 → thth-3.3.1}/tests/test_charcount.py +0 -0
  128. {thth-3.2.0 → thth-3.3.1}/tests/test_cli_entrypoint.py +0 -0
  129. {thth-3.2.0 → thth-3.3.1}/tests/test_cli_help.py +0 -0
  130. {thth-3.2.0 → thth-3.3.1}/tests/test_cli_vm_path.py +0 -0
  131. {thth-3.2.0 → thth-3.3.1}/tests/test_collect.py +0 -0
  132. {thth-3.2.0 → thth-3.3.1}/tests/test_collect_engagements.py +0 -0
  133. {thth-3.2.0 → thth-3.3.1}/tests/test_collect_runs.py +0 -0
  134. {thth-3.2.0 → thth-3.3.1}/tests/test_collect_sent.py +0 -0
  135. {thth-3.2.0 → thth-3.3.1}/tests/test_comparability.py +0 -0
  136. {thth-3.2.0 → thth-3.3.1}/tests/test_control_chars_refused.py +0 -0
  137. {thth-3.2.0 → thth-3.3.1}/tests/test_conversation_paging.py +0 -0
  138. {thth-3.2.0 → thth-3.3.1}/tests/test_count_for.py +0 -0
  139. {thth-3.2.0 → thth-3.3.1}/tests/test_docs_links.py +0 -0
  140. {thth-3.2.0 → thth-3.3.1}/tests/test_doctor.py +0 -0
  141. {thth-3.2.0 → thth-3.3.1}/tests/test_doctor_all_permissions.py +0 -0
  142. {thth-3.2.0 → thth-3.3.1}/tests/test_doctor_next_step.py +0 -0
  143. {thth-3.2.0 → thth-3.3.1}/tests/test_duplicate_front_matter_keys.py +0 -0
  144. {thth-3.2.0 → thth-3.3.1}/tests/test_editorial_audit.py +0 -0
  145. {thth-3.2.0 → thth-3.3.1}/tests/test_editorial_n3_n4.py +0 -0
  146. {thth-3.2.0 → thth-3.3.1}/tests/test_engagements.py +0 -0
  147. {thth-3.2.0 → thth-3.3.1}/tests/test_external_acceptance.py +0 -0
  148. {thth-3.2.0 → thth-3.3.1}/tests/test_fake_api.py +0 -0
  149. {thth-3.2.0 → thth-3.3.1}/tests/test_form_spec.py +0 -0
  150. {thth-3.2.0 → thth-3.3.1}/tests/test_form_spec_cli.py +0 -0
  151. {thth-3.2.0 → thth-3.3.1}/tests/test_forms.py +0 -0
  152. {thth-3.2.0 → thth-3.3.1}/tests/test_fresh_install.py +0 -0
  153. {thth-3.2.0 → thth-3.3.1}/tests/test_front_matter_injection.py +0 -0
  154. {thth-3.2.0 → thth-3.3.1}/tests/test_healthcheck_notifications.py +0 -0
  155. {thth-3.2.0 → thth-3.3.1}/tests/test_hypotheses.py +0 -0
  156. {thth-3.2.0 → thth-3.3.1}/tests/test_hypothesis_boundaries.py +0 -0
  157. {thth-3.2.0 → thth-3.3.1}/tests/test_hypothesis_units.py +0 -0
  158. {thth-3.2.0 → thth-3.3.1}/tests/test_impact_cli.py +0 -0
  159. {thth-3.2.0 → thth-3.3.1}/tests/test_inbox_granted_scopes.py +0 -0
  160. {thth-3.2.0 → thth-3.3.1}/tests/test_independent_thread_review.py +0 -0
  161. {thth-3.2.0 → thth-3.3.1}/tests/test_independent_topic_review.py +0 -0
  162. {thth-3.2.0 → thth-3.3.1}/tests/test_independent_topic_review_r2.py +0 -0
  163. {thth-3.2.0 → thth-3.3.1}/tests/test_inflight.py +0 -0
  164. {thth-3.2.0 → thth-3.3.1}/tests/test_inflight_tag_body_hash.py +0 -0
  165. {thth-3.2.0 → thth-3.3.1}/tests/test_jst.py +0 -0
  166. {thth-3.2.0 → thth-3.3.1}/tests/test_lint.py +0 -0
  167. {thth-3.2.0 → thth-3.3.1}/tests/test_list_projections.py +0 -0
  168. {thth-3.2.0 → thth-3.3.1}/tests/test_llm_trial.py +0 -0
  169. {thth-3.2.0 → thth-3.3.1}/tests/test_llm_trial_engage.py +0 -0
  170. {thth-3.2.0 → thth-3.3.1}/tests/test_lock.py +0 -0
  171. {thth-3.2.0 → thth-3.3.1}/tests/test_m2_independent.py +0 -0
  172. {thth-3.2.0 → thth-3.3.1}/tests/test_maintain.py +0 -0
  173. {thth-3.2.0 → thth-3.3.1}/tests/test_mastodon_adapter.py +0 -0
  174. {thth-3.2.0 → thth-3.3.1}/tests/test_mastodon_visibility.py +0 -0
  175. {thth-3.2.0 → thth-3.3.1}/tests/test_max_per_run.py +0 -0
  176. {thth-3.2.0 → thth-3.3.1}/tests/test_mcp_robustness.py +0 -0
  177. {thth-3.2.0 → thth-3.3.1}/tests/test_measured.py +0 -0
  178. {thth-3.2.0 → thth-3.3.1}/tests/test_media_accounts.py +0 -0
  179. {thth-3.2.0 → thth-3.3.1}/tests/test_media_next_steps.py +0 -0
  180. {thth-3.2.0 → thth-3.3.1}/tests/test_media_roundtrip.py +0 -0
  181. {thth-3.2.0 → thth-3.3.1}/tests/test_mismatch_diagnostics.py +0 -0
  182. {thth-3.2.0 → thth-3.3.1}/tests/test_n5_reply_null_element.py +0 -0
  183. {thth-3.2.0 → thth-3.3.1}/tests/test_n6_replies_incomplete_ledger.py +0 -0
  184. {thth-3.2.0 → thth-3.3.1}/tests/test_n7_doctor_error_body.py +0 -0
  185. {thth-3.2.0 → thth-3.3.1}/tests/test_no_limits.py +0 -0
  186. {thth-3.2.0 → thth-3.3.1}/tests/test_observation_provenance.py +0 -0
  187. {thth-3.2.0 → thth-3.3.1}/tests/test_operations_handoff.py +0 -0
  188. {thth-3.2.0 → thth-3.3.1}/tests/test_packaging.py +0 -0
  189. {thth-3.2.0 → thth-3.3.1}/tests/test_preview.py +0 -0
  190. {thth-3.2.0 → thth-3.3.1}/tests/test_production_mode.py +0 -0
  191. {thth-3.2.0 → thth-3.3.1}/tests/test_pull_cli.py +0 -0
  192. {thth-3.2.0 → thth-3.3.1}/tests/test_queue.py +0 -0
  193. {thth-3.2.0 → thth-3.3.1}/tests/test_quiet_hours.py +0 -0
  194. {thth-3.2.0 → thth-3.3.1}/tests/test_r2_r4_r5.py +0 -0
  195. {thth-3.2.0 → thth-3.3.1}/tests/test_read_cli_bare_runtimeerror.py +0 -0
  196. {thth-3.2.0 → thth-3.3.1}/tests/test_reason_adoption.py +0 -0
  197. {thth-3.2.0 → thth-3.3.1}/tests/test_redact.py +0 -0
  198. {thth-3.2.0 → thth-3.3.1}/tests/test_redirect_token_leak.py +0 -0
  199. {thth-3.2.0 → thth-3.3.1}/tests/test_refresh.py +0 -0
  200. {thth-3.2.0 → thth-3.3.1}/tests/test_refresh_destination.py +0 -0
  201. {thth-3.2.0 → thth-3.3.1}/tests/test_rejudge_new.py +0 -0
  202. {thth-3.2.0 → thth-3.3.1}/tests/test_rejudge_r2_r3.py +0 -0
  203. {thth-3.2.0 → thth-3.3.1}/tests/test_replies.py +0 -0
  204. {thth-3.2.0 → thth-3.3.1}/tests/test_replies_refresh.py +0 -0
  205. {thth-3.2.0 → thth-3.3.1}/tests/test_repo_lock.py +0 -0
  206. {thth-3.2.0 → thth-3.3.1}/tests/test_repo_sync.py +0 -0
  207. {thth-3.2.0 → thth-3.3.1}/tests/test_report_checkout_import.py +0 -0
  208. {thth-3.2.0 → thth-3.3.1}/tests/test_report_http.py +0 -0
  209. {thth-3.2.0 → thth-3.3.1}/tests/test_report_isolation.py +0 -0
  210. {thth-3.2.0 → thth-3.3.1}/tests/test_report_service.py +0 -0
  211. {thth-3.2.0 → thth-3.3.1}/tests/test_reverse_audit.py +0 -0
  212. {thth-3.2.0 → thth-3.3.1}/tests/test_review_case_id_cluster_m2.py +0 -0
  213. {thth-3.2.0 → thth-3.3.1}/tests/test_review_case_id_cluster_r2.py +0 -0
  214. {thth-3.2.0 → thth-3.3.1}/tests/test_review_cli.py +0 -0
  215. {thth-3.2.0 → thth-3.3.1}/tests/test_review_evidence_account.py +0 -0
  216. {thth-3.2.0 → thth-3.3.1}/tests/test_review_form_spec_and_case_id.py +0 -0
  217. {thth-3.2.0 → thth-3.3.1}/tests/test_review_provenance.py +0 -0
  218. {thth-3.2.0 → thth-3.3.1}/tests/test_review_records.py +0 -0
  219. {thth-3.2.0 → thth-3.3.1}/tests/test_revoke.py +0 -0
  220. {thth-3.2.0 → thth-3.3.1}/tests/test_roundtrip.py +0 -0
  221. {thth-3.2.0 → thth-3.3.1}/tests/test_run_missing_env.py +0 -0
  222. {thth-3.2.0 → thth-3.3.1}/tests/test_run_record_failclosed.py +0 -0
  223. {thth-3.2.0 → thth-3.3.1}/tests/test_runs_collected.py +0 -0
  224. {thth-3.2.0 → thth-3.3.1}/tests/test_runs_read_rows.py +0 -0
  225. {thth-3.2.0 → thth-3.3.1}/tests/test_schedule_multiple.py +0 -0
  226. {thth-3.2.0 → thth-3.3.1}/tests/test_schedule_view.py +0 -0
  227. {thth-3.2.0 → thth-3.3.1}/tests/test_scopes_source_requested.py +0 -0
  228. {thth-3.2.0 → thth-3.3.1}/tests/test_search_engage_targets.py +0 -0
  229. {thth-3.2.0 → thth-3.3.1}/tests/test_search_media_routes.py +0 -0
  230. {thth-3.2.0 → thth-3.3.1}/tests/test_secret_value_redact.py +0 -0
  231. {thth-3.2.0 → thth-3.3.1}/tests/test_secrets_grep.py +0 -0
  232. {thth-3.2.0 → thth-3.3.1}/tests/test_security_small_fixes.py +0 -0
  233. {thth-3.2.0 → thth-3.3.1}/tests/test_select.py +0 -0
  234. {thth-3.2.0 → thth-3.3.1}/tests/test_selfupdate.py +0 -0
  235. {thth-3.2.0 → thth-3.3.1}/tests/test_send.py +0 -0
  236. {thth-3.2.0 → thth-3.3.1}/tests/test_send_cli.py +0 -0
  237. {thth-3.2.0 → thth-3.3.1}/tests/test_send_control_chars.py +0 -0
  238. {thth-3.2.0 → thth-3.3.1}/tests/test_send_media.py +0 -0
  239. {thth-3.2.0 → thth-3.3.1}/tests/test_send_visibility.py +0 -0
  240. {thth-3.2.0 → thth-3.3.1}/tests/test_sent_integrity.py +0 -0
  241. {thth-3.2.0 → thth-3.3.1}/tests/test_server_json.py +0 -0
  242. {thth-3.2.0 → thth-3.3.1}/tests/test_site.py +0 -0
  243. {thth-3.2.0 → thth-3.3.1}/tests/test_skill_doc.py +0 -0
  244. {thth-3.2.0 → thth-3.3.1}/tests/test_study_report.py +0 -0
  245. {thth-3.2.0 → thth-3.3.1}/tests/test_systemd.py +0 -0
  246. {thth-3.2.0 → thth-3.3.1}/tests/test_thread_publish.py +0 -0
  247. {thth-3.2.0 → thth-3.3.1}/tests/test_thread_read.py +0 -0
  248. {thth-3.2.0 → thth-3.3.1}/tests/test_threadrun.py +0 -0
  249. {thth-3.2.0 → thth-3.3.1}/tests/test_threads_read_permissions.py +0 -0
  250. {thth-3.2.0 → thth-3.3.1}/tests/test_threadshape.py +0 -0
  251. {thth-3.2.0 → thth-3.3.1}/tests/test_thth_root.py +0 -0
  252. {thth-3.2.0 → thth-3.3.1}/tests/test_token_exists.py +0 -0
  253. {thth-3.2.0 → thth-3.3.1}/tests/test_token_scopes_record.py +0 -0
  254. {thth-3.2.0 → thth-3.3.1}/tests/test_token_set.py +0 -0
  255. {thth-3.2.0 → thth-3.3.1}/tests/test_topic.py +0 -0
  256. {thth-3.2.0 → thth-3.3.1}/tests/test_topic_advice.py +0 -0
  257. {thth-3.2.0 → thth-3.3.1}/tests/test_topic_cli.py +0 -0
  258. {thth-3.2.0 → thth-3.3.1}/tests/test_topic_models.py +0 -0
  259. {thth-3.2.0 → thth-3.3.1}/tests/test_topic_review_fixes.py +0 -0
  260. {thth-3.2.0 → thth-3.3.1}/tests/test_topic_shelf_observers.py +0 -0
  261. {thth-3.2.0 → thth-3.3.1}/tests/test_topic_store.py +0 -0
  262. {thth-3.2.0 → thth-3.3.1}/tests/test_topics_record_concurrent.py +0 -0
  263. {thth-3.2.0 → thth-3.3.1}/tests/test_topics_tool.py +0 -0
  264. {thth-3.2.0 → thth-3.3.1}/tests/test_trial_frictions.py +0 -0
  265. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_admin_transaction.py +0 -0
  266. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_approve_account.py +0 -0
  267. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_length.py +0 -0
  268. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_lock_wait.py +0 -0
  269. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_mastodon_scopes.py +0 -0
  270. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_mentions.py +0 -0
  271. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_post_urls.py +0 -0
  272. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_repo_paths.py +0 -0
  273. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_reposts.py +0 -0
  274. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_study_add.py +0 -0
  275. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_tool_version.py +0 -0
  276. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_unanswered.py +0 -0
  277. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_unanswered_scope.py +0 -0
  278. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_where_arguments.py +0 -0
  279. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_where_filters.py +0 -0
  280. {thth-3.2.0 → thth-3.3.1}/tests/test_v210_who_arguments.py +0 -0
  281. {thth-3.2.0 → thth-3.3.1}/tests/test_v211_auth_integration.py +0 -0
  282. {thth-3.2.0 → thth-3.3.1}/tests/test_v211_authflow.py +0 -0
  283. {thth-3.2.0 → thth-3.3.1}/tests/test_v211_bluesky_stdin.py +0 -0
  284. {thth-3.2.0 → thth-3.3.1}/tests/test_v211_doctor_binding.py +0 -0
  285. {thth-3.2.0 → thth-3.3.1}/tests/test_v211_mastodon_auth.py +0 -0
  286. {thth-3.2.0 → thth-3.3.1}/tests/test_v211_x_auth.py +0 -0
  287. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_approval_relay.py +0 -0
  288. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_ask_coordination.py +0 -0
  289. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_budget.py +0 -0
  290. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_budget_auth.py +0 -0
  291. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_budget_mcp.py +0 -0
  292. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_deletion.py +0 -0
  293. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_job_completion_fault.py +0 -0
  294. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_leave.py +0 -0
  295. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_leave_gate.py +0 -0
  296. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_leave_providers.py +0 -0
  297. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_leave_token_safety.py +0 -0
  298. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_mastodon_guide.py +0 -0
  299. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_mcp_compat.py +0 -0
  300. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_missing_token.py +0 -0
  301. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_server_writes.py +0 -0
  302. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_token_parent.py +0 -0
  303. {thth-3.2.0 → thth-3.3.1}/tests/test_v212_write_storage.py +0 -0
  304. {thth-3.2.0 → thth-3.3.1}/tests/test_v2131_send_dry_run.py +0 -0
  305. {thth-3.2.0 → thth-3.3.1}/tests/test_v2131_threads_publish_wait.py +0 -0
  306. {thth-3.2.0 → thth-3.3.1}/tests/test_v2132_send_inflight_hint.py +0 -0
  307. {thth-3.2.0 → thth-3.3.1}/tests/test_v2132_threads_create_timeout.py +0 -0
  308. {thth-3.2.0 → thth-3.3.1}/tests/test_v2132_threads_create_unknown.py +0 -0
  309. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_acceptance_fixes.py +0 -0
  310. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_adts_asf.py +0 -0
  311. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_approval_attachments.py +0 -0
  312. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_attachment_kinds.py +0 -0
  313. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_external.py +0 -0
  314. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_media.py +0 -0
  315. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_metadata.py +0 -0
  316. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_metadata_uri.py +0 -0
  317. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_metadata_video.py +0 -0
  318. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_record.py +0 -0
  319. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_record_video.py +0 -0
  320. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_video.py +0 -0
  321. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bluesky_webm_named.py +0 -0
  322. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_bmff_typed_metadata.py +0 -0
  323. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_budget_lock.py +0 -0
  324. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_capabilities.py +0 -0
  325. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_graphemes.py +0 -0
  326. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_auth_media.py +0 -0
  327. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_bmff_audio.py +0 -0
  328. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_flac.py +0 -0
  329. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_gif.py +0 -0
  330. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_link.py +0 -0
  331. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_media.py +0 -0
  332. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_mp3.py +0 -0
  333. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_ogg_opus.py +0 -0
  334. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
  335. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_poll.py +0 -0
  336. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
  337. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_quote.py +0 -0
  338. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_thumbnail.py +0 -0
  339. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_wav.py +0 -0
  340. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_webm_audio.py +0 -0
  341. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_mastodon_webm_video.py +0 -0
  342. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_cleanup.py +0 -0
  343. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_formats.py +0 -0
  344. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_foundation.py +0 -0
  345. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_prepared.py +0 -0
  346. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_relay.py +0 -0
  347. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_relay_refusals.py +0 -0
  348. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_media_upload.py +0 -0
  349. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_receipt_deadline.py +0 -0
  350. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_stage810_audit.py +0 -0
  351. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_static_diagnostics.py +0 -0
  352. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_systemd_worker.py +0 -0
  353. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_ghost.py +0 -0
  354. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_gif.py +0 -0
  355. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_link.py +0 -0
  356. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_media.py +0 -0
  357. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_media_spoiler.py +0 -0
  358. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_poll.py +0 -0
  359. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_quote.py +0 -0
  360. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_quote_media.py +0 -0
  361. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_replies.py +0 -0
  362. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_text.py +0 -0
  363. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_text_spoiler.py +0 -0
  364. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_threads_video.py +0 -0
  365. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_transport_review.py +0 -0
  366. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_video_typed_metadata.py +0 -0
  367. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_vorbis_comment_boundary.py +0 -0
  368. {thth-3.2.0 → thth-3.3.1}/tests/test_v213_webm_schema_boundary.py +0 -0
  369. {thth-3.2.0 → thth-3.3.1}/tests/test_v2141_draft_lint_reason.py +0 -0
  370. {thth-3.2.0 → thth-3.3.1}/tests/test_v2141_managed_repo_umask.py +0 -0
  371. {thth-3.2.0 → thth-3.3.1}/tests/test_v214_release_signing.py +0 -0
  372. {thth-3.2.0 → thth-3.3.1}/tests/test_v214_x_adapter.py +0 -0
  373. {thth-3.2.0 → thth-3.3.1}/tests/test_v214_x_auth.py +0 -0
  374. {thth-3.2.0 → thth-3.3.1}/tests/test_v214_x_media.py +0 -0
  375. {thth-3.2.0 → thth-3.3.1}/tests/test_v214_x_posts_budget.py +0 -0
  376. {thth-3.2.0 → thth-3.3.1}/tests/test_v21b_approved_writes.py +0 -0
  377. {thth-3.2.0 → thth-3.3.1}/tests/test_v28_tags.py +0 -0
  378. {thth-3.2.0 → thth-3.3.1}/tests/test_v310_search_timeout.py +0 -0
  379. {thth-3.2.0 → thth-3.3.1}/tests/test_v310_watch_words.py +0 -0
  380. {thth-3.2.0 → thth-3.3.1}/tests/test_v311_leave_same_user.py +0 -0
  381. {thth-3.2.0 → thth-3.3.1}/tests/test_v311_mastodon_retract.py +0 -0
  382. {thth-3.2.0 → thth-3.3.1}/tests/test_v311_morning_overdue.py +0 -0
  383. {thth-3.2.0 → thth-3.3.1}/tests/test_v311_morning_own_exclusion.py +0 -0
  384. {thth-3.2.0 → thth-3.3.1}/tests/test_v311_morning_unanswered_window.py +0 -0
  385. {thth-3.2.0 → thth-3.3.1}/tests/test_v311_notifications_guidance.py +0 -0
  386. {thth-3.2.0 → thth-3.3.1}/tests/test_v311_replies_owned.py +0 -0
  387. {thth-3.2.0 → thth-3.3.1}/tests/test_v312_leave_shared_token.py +0 -0
  388. {thth-3.2.0 → thth-3.3.1}/tests/test_v312_own_handles_stopped.py +0 -0
  389. {thth-3.2.0 → thth-3.3.1}/tests/test_v312_report_admin.py +0 -0
  390. {thth-3.2.0 → thth-3.3.1}/tests/test_v312_report_inbox.py +0 -0
  391. {thth-3.2.0 → thth-3.3.1}/tests/test_v312_report_mcp_user.py +0 -0
  392. {thth-3.2.0 → thth-3.3.1}/tests/test_v31_analysis.py +0 -0
  393. {thth-3.2.0 → thth-3.3.1}/tests/test_v31_collection.py +0 -0
  394. {thth-3.2.0 → thth-3.3.1}/tests/test_v320_reply_to_file_display.py +0 -0
  395. {thth-3.2.0 → thth-3.3.1}/tests/test_v320_reply_to_file_lint.py +0 -0
  396. {thth-3.2.0 → thth-3.3.1}/tests/test_v320_reply_to_file_resolve.py +0 -0
  397. {thth-3.2.0 → thth-3.3.1}/tests/test_v320_report_fixes.py +0 -0
  398. {thth-3.2.0 → thth-3.3.1}/tests/test_value_domains_shown.py +0 -0
  399. {thth-3.2.0 → thth-3.3.1}/tests/test_version.py +0 -0
  400. {thth-3.2.0 → thth-3.3.1}/tests/test_where_cli.py +0 -0
  401. {thth-3.2.0 → thth-3.3.1}/tests/test_who_cli.py +0 -0
  402. {thth-3.2.0 → thth-3.3.1}/tests/test_writeback_rebase.py +0 -0
  403. {thth-3.2.0 → thth-3.3.1}/tests/test_writeback_validate.py +0 -0
  404. {thth-3.2.0 → thth-3.3.1}/tests/test_x_metrics.py +0 -0
  405. {thth-3.2.0 → thth-3.3.1}/thth/__init__.py +0 -0
  406. {thth-3.2.0 → thth-3.3.1}/thth/__main__.py +0 -0
  407. {thth-3.2.0 → thth-3.3.1}/thth/_grapheme_data.py +0 -0
  408. {thth-3.2.0 → thth-3.3.1}/thth/account_cli.py +0 -0
  409. {thth-3.2.0 → thth-3.3.1}/thth/account_report.py +0 -0
  410. {thth-3.2.0 → thth-3.3.1}/thth/accounts.py +0 -0
  411. {thth-3.2.0 → thth-3.3.1}/thth/adapters/__init__.py +0 -0
  412. {thth-3.2.0 → thth-3.3.1}/thth/adapters/auth_mastodon.py +0 -0
  413. {thth-3.2.0 → thth-3.3.1}/thth/adapters/auth_threads.py +0 -0
  414. {thth-3.2.0 → thth-3.3.1}/thth/adapters/auth_x.py +0 -0
  415. {thth-3.2.0 → thth-3.3.1}/thth/adapters/bluesky.py +0 -0
  416. {thth-3.2.0 → thth-3.3.1}/thth/adapters/bluesky_media.py +0 -0
  417. {thth-3.2.0 → thth-3.3.1}/thth/adapters/bluesky_video.py +0 -0
  418. {thth-3.2.0 → thth-3.3.1}/thth/adapters/mastodon.py +0 -0
  419. {thth-3.2.0 → thth-3.3.1}/thth/adapters/mastodon_media.py +0 -0
  420. {thth-3.2.0 → thth-3.3.1}/thth/adapters/threads_media.py +0 -0
  421. {thth-3.2.0 → thth-3.3.1}/thth/adapters/x.py +0 -0
  422. {thth-3.2.0 → thth-3.3.1}/thth/adapters/x_media.py +0 -0
  423. {thth-3.2.0 → thth-3.3.1}/thth/admin_notifications.py +0 -0
  424. {thth-3.2.0 → thth-3.3.1}/thth/admin_report.py +0 -0
  425. {thth-3.2.0 → thth-3.3.1}/thth/after_cli.py +0 -0
  426. {thth-3.2.0 → thth-3.3.1}/thth/analytics_comparison.py +0 -0
  427. {thth-3.2.0 → thth-3.3.1}/thth/analytics_report.py +0 -0
  428. {thth-3.2.0 → thth-3.3.1}/thth/analytics_shapes.py +0 -0
  429. {thth-3.2.0 → thth-3.3.1}/thth/analytics_threads.py +0 -0
  430. {thth-3.2.0 → thth-3.3.1}/thth/api_diagnostic.py +0 -0
  431. {thth-3.2.0 → thth-3.3.1}/thth/appconfig.py +0 -0
  432. {thth-3.2.0 → thth-3.3.1}/thth/appenv.py +0 -0
  433. {thth-3.2.0 → thth-3.3.1}/thth/approval_jobs.py +0 -0
  434. {thth-3.2.0 → thth-3.3.1}/thth/approval_relay.py +0 -0
  435. {thth-3.2.0 → thth-3.3.1}/thth/ask.py +0 -0
  436. {thth-3.2.0 → thth-3.3.1}/thth/ask_cli.py +0 -0
  437. {thth-3.2.0 → thth-3.3.1}/thth/audioformats.py +0 -0
  438. {thth-3.2.0 → thth-3.3.1}/thth/authclients.py +0 -0
  439. {thth-3.2.0 → thth-3.3.1}/thth/authflow.py +0 -0
  440. {thth-3.2.0 → thth-3.3.1}/thth/authpassword.py +0 -0
  441. {thth-3.2.0 → thth-3.3.1}/thth/bluesky_metadata.py +0 -0
  442. {thth-3.2.0 → thth-3.3.1}/thth/budget_x.py +0 -0
  443. {thth-3.2.0 → thth-3.3.1}/thth/budget_x_posts.py +0 -0
  444. {thth-3.2.0 → thth-3.3.1}/thth/bundle.py +0 -0
  445. {thth-3.2.0 → thth-3.3.1}/thth/collect.py +0 -0
  446. {thth-3.2.0 → thth-3.3.1}/thth/collection_context.py +0 -0
  447. {thth-3.2.0 → thth-3.3.1}/thth/collection_status.py +0 -0
  448. {thth-3.2.0 → thth-3.3.1}/thth/deletion.py +0 -0
  449. {thth-3.2.0 → thth-3.3.1}/thth/ebmlaudio.py +0 -0
  450. {thth-3.2.0 → thth-3.3.1}/thth/ebmlvideo.py +0 -0
  451. {thth-3.2.0 → thth-3.3.1}/thth/engagements.py +0 -0
  452. {thth-3.2.0 → thth-3.3.1}/thth/flacformats.py +0 -0
  453. {thth-3.2.0 → thth-3.3.1}/thth/forms.py +0 -0
  454. {thth-3.2.0 → thth-3.3.1}/thth/graphemes.py +0 -0
  455. {thth-3.2.0 → thth-3.3.1}/thth/handoff_cursor.py +0 -0
  456. {thth-3.2.0 → thth-3.3.1}/thth/httpsafe.py +0 -0
  457. {thth-3.2.0 → thth-3.3.1}/thth/jst.py +0 -0
  458. {thth-3.2.0 → thth-3.3.1}/thth/leave.py +0 -0
  459. {thth-3.2.0 → thth-3.3.1}/thth/lint.py +0 -0
  460. {thth-3.2.0 → thth-3.3.1}/thth/lock.py +0 -0
  461. {thth-3.2.0 → thth-3.3.1}/thth/maintain.py +0 -0
  462. {thth-3.2.0 → thth-3.3.1}/thth/managed_repo.py +0 -0
  463. {thth-3.2.0 → thth-3.3.1}/thth/measured.py +0 -0
  464. {thth-3.2.0 → thth-3.3.1}/thth/media.py +0 -0
  465. {thth-3.2.0 → thth-3.3.1}/thth/media_capabilities.py +0 -0
  466. {thth-3.2.0 → thth-3.3.1}/thth/media_cleanup.py +0 -0
  467. {thth-3.2.0 → thth-3.3.1}/thth/media_delivery.py +0 -0
  468. {thth-3.2.0 → thth-3.3.1}/thth/media_relay.py +0 -0
  469. {thth-3.2.0 → thth-3.3.1}/thth/media_uploads.py +0 -0
  470. {thth-3.2.0 → thth-3.3.1}/thth/mediaformats.py +0 -0
  471. {thth-3.2.0 → thth-3.3.1}/thth/mpeg_audio.py +0 -0
  472. {thth-3.2.0 → thth-3.3.1}/thth/oauth.py +0 -0
  473. {thth-3.2.0 → thth-3.3.1}/thth/oggformats.py +0 -0
  474. {thth-3.2.0 → thth-3.3.1}/thth/postid.py +0 -0
  475. {thth-3.2.0 → thth-3.3.1}/thth/queuefile.py +0 -0
  476. {thth-3.2.0 → thth-3.3.1}/thth/read_window.py +0 -0
  477. {thth-3.2.0 → thth-3.3.1}/thth/redact.py +0 -0
  478. {thth-3.2.0 → thth-3.3.1}/thth/replies.py +0 -0
  479. {thth-3.2.0 → thth-3.3.1}/thth/report_details.py +0 -0
  480. {thth-3.2.0 → thth-3.3.1}/thth/report_http.py +0 -0
  481. {thth-3.2.0 → thth-3.3.1}/thth/report_isolation.py +0 -0
  482. {thth-3.2.0 → thth-3.3.1}/thth/retract_cli.py +0 -0
  483. {thth-3.2.0 → thth-3.3.1}/thth/scopes.py +0 -0
  484. {thth-3.2.0 → thth-3.3.1}/thth/secrets_fs.py +0 -0
  485. {thth-3.2.0 → thth-3.3.1}/thth/selfupdate.py +0 -0
  486. {thth-3.2.0 → thth-3.3.1}/thth/sent.py +0 -0
  487. {thth-3.2.0 → thth-3.3.1}/thth/server_files.py +0 -0
  488. {thth-3.2.0 → thth-3.3.1}/thth/server_writes.py +0 -0
  489. {thth-3.2.0 → thth-3.3.1}/thth/stop_observation.py +0 -0
  490. {thth-3.2.0 → thth-3.3.1}/thth/study_cli.py +0 -0
  491. {thth-3.2.0 → thth-3.3.1}/thth/study_report.py +0 -0
  492. {thth-3.2.0 → thth-3.3.1}/thth/systemd_gen.py +0 -0
  493. {thth-3.2.0 → thth-3.3.1}/thth/tags.py +0 -0
  494. {thth-3.2.0 → thth-3.3.1}/thth/thread_read.py +0 -0
  495. {thth-3.2.0 → thth-3.3.1}/thth/threadrun.py +0 -0
  496. {thth-3.2.0 → thth-3.3.1}/thth/threads_read_cli.py +0 -0
  497. {thth-3.2.0 → thth-3.3.1}/thth/threadshape.py +0 -0
  498. {thth-3.2.0 → thth-3.3.1}/thth/threadthrow.py +0 -0
  499. {thth-3.2.0 → thth-3.3.1}/thth/topic_advice.py +0 -0
  500. {thth-3.2.0 → thth-3.3.1}/thth/topic_cli.py +0 -0
  501. {thth-3.2.0 → thth-3.3.1}/thth/topic_models.py +0 -0
  502. {thth-3.2.0 → thth-3.3.1}/thth/topic_store.py +0 -0
  503. {thth-3.2.0 → thth-3.3.1}/thth/topics.py +0 -0
  504. {thth-3.2.0 → thth-3.3.1}/thth/unanswered.py +0 -0
  505. {thth-3.2.0 → thth-3.3.1}/thth/vorbisformats.py +0 -0
  506. {thth-3.2.0 → thth-3.3.1}/thth/watch_cli.py +0 -0
  507. {thth-3.2.0 → thth-3.3.1}/thth/where_cli.py +0 -0
  508. {thth-3.2.0 → thth-3.3.1}/thth/who_cli.py +0 -0
  509. {thth-3.2.0 → thth-3.3.1}/thth/writeback.py +0 -0
  510. {thth-3.2.0 → thth-3.3.1}/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.1
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`・`inflight`・`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`・`inflight`・`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
 
@@ -167,7 +168,7 @@ MCP `after_you_posted`。**24h の刻みが無ければ `null`**(0 と混ぜ
167
168
 
168
169
  新規 account は利用者メール・管理者メール・TLS SMTP を `thth notifications config` で設定し、`status` → `test` を実行する。両宛先の受信箱で到達、account 専用 HEALTHCHECK_URL の missed-ping 通知を確認してから production/scheduled を有効にする。既存 account にも同じ設定を追加する。詳細は `docs/停止通知と運用記録.md`。
169
170
 
170
- プロジェクトの作業開始・再開時は `git fetch` 後にリモート `docs/sns/queue/` の `thth_run_state`・`thth_run_detail`・`thth_run_next` を確認する。dirty worktree へ無条件に pull しない。リモートの確認は `git show origin/main:docs/sns/queue/FILE.md` 等で行える。停止・成否不明なら inflight を消して再投稿せず、示された確認を行う。復旧状態は投稿成功の保証ではなく、公開結果は status/post_id/posted_at を見る。
171
+ プロジェクトの作業開始・再開時は `git fetch` 後にリモート `docs/sns/queue/` の `thth_run_state`・`thth_run_detail`・`thth_run_next` を確認する。dirty worktree へ無条件に pull しない。リモートの確認は `git show origin/main:docs/sns/queue/FILE.md` 等で行える。停止・成否不明なら inflight を手で消して再投稿しない(3.3.1 から道具が毎 run の最初に媒体へ 1 回だけ問い合わせ、決まれば自分で解く)。止まり続けるなら `thth inflight <account> show` で中身を見て、媒体で確かめた人が `thth inflight <account> resolve --not-published|--published <post_id> --by <名前>` で解く(二段確認・MCP には無い)。復旧状態は投稿成功の保証ではなく、公開結果は status/post_id/posted_at を見る。
171
172
 
172
173
  `mentions <account> --json` は 3 媒体の言及を同じ項目で読む。返信前に `replied` の object/false/null を区別する。`unanswered <account> --since 7d --json` は自分の根投稿の未回答候補を台帳で読む。`cannot_say` と採集の古さを確認し、`--refresh` は明示されたときだけ使う。件数は account ごとに扱い、送信は既存の承認・digest・production の門を通す。
173
174
 
@@ -67,6 +67,16 @@ def local_http_test_transport(monkeypatch):
67
67
  opener.open('http://'+host,timeout=0.01)
68
68
 
69
69
 
70
+ @pytest.fixture(autouse=True)
71
+ def no_threads_status_poll_wait(monkeypatch):
72
+ """公開の結果が分からないときの container の問い合わせ(設計 3.3.1 §2)を
73
+ 試験では待たない。本番の既定は 20 秒おき・最大 3 回。偽サーバは status の
74
+ GET に答えない(`query_failed`)ので、従前の試験の結果(inflight を残す)は
75
+ 変わらない——変わるのは待つ時間だけ。subprocess の `bin/thth` にも env で届く。
76
+ """
77
+ monkeypatch.setenv('THTH_THREADS_STATUS_POLL_SECONDS', '0')
78
+
79
+
70
80
  @pytest.fixture(autouse=True)
71
81
  def frozen_now_jst(monkeypatch):
72
82
  """`thth.jst.now_jst()` を既定で静かな時間帯の外(2026-09-09 10:00 JST)に固定する。
@@ -429,3 +439,15 @@ def init_git_pair(tmp_path, *, seed_content: str, seed_name: str = "a.md"):
429
439
 
430
440
  return {"bare": bare, "seed": seed, "work": work,
431
441
  "queue_dir": os.path.join(work, "docs", "sns", "queue")}
442
+
443
+
444
+ def is_refusal(err: str, code: str) -> bool:
445
+ """断りの stderr が「理由の符丁 1 行+打てる報告の 1 行」か(3.1.2 §3.5・3.3.0 B3)。
446
+
447
+ 3.3.0 から 2 行目は account と理由の符丁を埋めた `report_inbox.refusal_line()`。
448
+ account は命令と台帳しだいで `<account>` のままになるので、ここでは形と符丁だけ見る。
449
+ """
450
+ lines = err.strip().splitlines()
451
+ return (len(lines) == 2 and lines[0] == code
452
+ and lines[1].startswith("report_channel: thth report file ")
453
+ and f'--title "{code} で断られた"' in lines[1])
@@ -0,0 +1,123 @@
1
+ """container の status を答える偽 Threads(設計 3.3.1 §6・**tests の中だけ**)。
2
+
3
+ 本物の Threads API には触らない。`http.server` で次の 4 つの口だけを真似る:
4
+
5
+ - `POST /<user>/threads` → container を作る(id は `CONTAINER_ID`)
6
+ - `POST /<user>/threads_publish` → `script.publish` の先頭から 1 つずつ答える
7
+ - `GET /<container>` → `script.status` の先頭から 1 つずつ(最後の 1 つは繰り返す)
8
+ - `GET /v1.0/<user>/threads` → `script.posts`(自分の最近の投稿)
9
+
10
+ 何回呼ばれたかを数える(`publish_calls`・`status_calls`・`list_calls`)——
11
+ 「2 度出していない」を回数で確かめるため。
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import contextlib
16
+ import dataclasses
17
+ import http.server
18
+ import json
19
+ import threading
20
+ import urllib.parse
21
+
22
+ from thth.adapters import threads as threads_mod
23
+
24
+ USER_ID = "12345"
25
+ CONTAINER_ID = "900"
26
+
27
+
28
+ @dataclasses.dataclass
29
+ class Script:
30
+ # 公開の答え: ("ok", id) / (500, None) / (400, None) / ("drop", None)(接続断)
31
+ publish: list = dataclasses.field(default_factory=lambda: [("ok", "777")])
32
+ # container の status の答え: "FINISHED" などの語 / 数字(HTTP の失敗)
33
+ status: list = dataclasses.field(default_factory=lambda: ["FINISHED"])
34
+ # 自分の最近の投稿(`GET /v1.0/<user>/threads` の data)
35
+ posts: list = dataclasses.field(default_factory=list)
36
+ list_status: int = 200
37
+ publish_calls: int = 0
38
+ status_calls: int = 0
39
+ list_calls: int = 0
40
+ create_calls: int = 0
41
+ # 公開の要求に載っていた creation_id(どの container を公開したか)。
42
+ publish_ids: list = dataclasses.field(default_factory=list)
43
+
44
+
45
+ def _handler(script: Script):
46
+ class Handler(http.server.BaseHTTPRequestHandler):
47
+ def _json(self, code, payload):
48
+ body = json.dumps(payload).encode("utf-8")
49
+ self.send_response(code)
50
+ self.send_header("Content-Type", "application/json")
51
+ self.send_header("Content-Length", str(len(body)))
52
+ self.end_headers()
53
+ self.wfile.write(body)
54
+
55
+ def do_POST(self): # noqa: N802
56
+ length = int(self.headers.get("Content-Length") or 0)
57
+ raw = self.rfile.read(length) if length else b""
58
+ if self.path.endswith("/threads_publish"):
59
+ script.publish_calls += 1
60
+ params = urllib.parse.parse_qs(raw.decode("utf-8"))
61
+ script.publish_ids.append((params.get("creation_id") or [None])[0])
62
+ kind, value = (script.publish.pop(0) if script.publish else (500, None))
63
+ if kind == "ok":
64
+ self._json(200, {"id": value})
65
+ elif kind == "drop":
66
+ # 応答を返さずに切る(接続断)。
67
+ self.close_connection = True
68
+ self.connection.close()
69
+ else:
70
+ self._json(kind, {"error": {"message": "fake", "code": 1,
71
+ "is_transient": True}})
72
+ return
73
+ script.create_calls += 1
74
+ self._json(200, {"id": CONTAINER_ID})
75
+
76
+ def do_GET(self): # noqa: N802
77
+ path = urllib.parse.urlsplit(self.path).path
78
+ if path == f"/v1.0/{USER_ID}/threads":
79
+ script.list_calls += 1
80
+ if script.list_status != 200:
81
+ self._json(script.list_status, {"error": {"message": "fake"}})
82
+ return
83
+ self._json(200, {"data": list(script.posts)})
84
+ return
85
+ if path == f"/{CONTAINER_ID}":
86
+ script.status_calls += 1
87
+ answer = script.status.pop(0) if len(script.status) > 1 else script.status[0]
88
+ if isinstance(answer, int):
89
+ self._json(answer, {"error": {"message": "fake"}})
90
+ else:
91
+ self._json(200, {"status": answer, "id": CONTAINER_ID})
92
+ return
93
+ self._json(404, {"error": {"message": "not found"}})
94
+
95
+ def log_message(self, format, *args): # noqa: A002
96
+ pass
97
+
98
+ return Handler
99
+
100
+
101
+ @contextlib.contextmanager
102
+ def serve(script: Script):
103
+ server = http.server.HTTPServer(("127.0.0.1", 0), _handler(script))
104
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
105
+ thread.start()
106
+ try:
107
+ yield f"http://127.0.0.1:{server.server_port}"
108
+ finally:
109
+ server.shutdown()
110
+ thread.join(timeout=5)
111
+
112
+
113
+ def adapter(base_url: str, **kwargs) -> threads_mod.ThreadsAdapter:
114
+ kwargs.setdefault("wait_seconds", 0)
115
+ kwargs.setdefault("timeout", 2.0)
116
+ kwargs.setdefault("status_poll_seconds", 0)
117
+ return threads_mod.ThreadsAdapter(base_url=base_url, access_token="fake-token",
118
+ user_id=USER_ID, **kwargs)
119
+
120
+
121
+ def post_row(post_id: str, text: str, timestamp: str) -> dict:
122
+ return {"id": post_id, "text": text, "timestamp": timestamp,
123
+ "permalink": f"https://www.threads.net/@x/post/{post_id}"}
@@ -76,7 +76,8 @@ def test_git_roundtrip_preserves_original_and_deduplicates(setup):
76
76
  assert "thth_run_state: recovered" in s.path.read_text()
77
77
  assert len(s.captured) == 4
78
78
  assert incident._fingerprint(s.path) == before
79
- assert len(row(s)["events"]) == 1
79
+ # 3.3.1 §5: 送り終えた事象も 30 日は残す(以前は 1 件に畳んでいた)。
80
+ assert len(row(s)["events"]) == 2
80
81
 
81
82
 
82
83
  def test_initial_success_is_quiet(setup):
@@ -99,7 +100,8 @@ def test_role_failure_retries_only_unfinished_and_other_role_gets_recovery(setup
99
100
  monkeypatch.setattr(incident, "_send", lambda c, r, e, a: calls.append((r, e["state"])) or True)
100
101
  notify(s, "success", result=False)
101
102
  assert calls[-2:] == [("user@example.org", "blocked"), ("user@example.org", "recovered")]
102
- assert len(row(s)["events"]) == 1
103
+ # 3.3.1 §5: 送り終えた事象も 30 日は残す(以前は 1 件に畳んでいた)。
104
+ assert len(row(s)["events"]) == 2
103
105
 
104
106
 
105
107
  def test_same_address_single_message(setup):
@@ -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,9 +26,13 @@ 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
- 'admin reports reply', 'admin reports close', 'admin reports export']
33
+ 'admin reports reply', 'admin reports close', 'admin reports export',
34
+ # 3.3.1 §4: 人の口 thth inflight(show|resolve・--json)。
35
+ 'inflight']
32
36
 
33
37
 
34
38
  def test_json_table_covers_every_parser():
@@ -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
  # 管理者の一覧には足さない(管理者は報告を置く側ではない)。