thth 2.0.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 (211) hide show
  1. thth-2.0.0/.gitignore +11 -0
  2. thth-2.0.0/LICENSE +21 -0
  3. thth-2.0.0/PKG-INFO +95 -0
  4. thth-2.0.0/README.md +71 -0
  5. thth-2.0.0/accounts.example/bluesky.json +25 -0
  6. thth-2.0.0/accounts.example/mastodon.json +25 -0
  7. thth-2.0.0/accounts.example/threads.json +25 -0
  8. thth-2.0.0/bin/thth +15 -0
  9. thth-2.0.0/bin/thth-remote +31 -0
  10. thth-2.0.0/bin/thth-run +7 -0
  11. thth-2.0.0/mcp/server.py +280 -0
  12. thth-2.0.0/pyproject.toml +94 -0
  13. thth-2.0.0/skills/thth/SKILL.md +117 -0
  14. thth-2.0.0/systemd/thth@.service +11 -0
  15. thth-2.0.0/tests/__init__.py +0 -0
  16. thth-2.0.0/tests/conftest.py +404 -0
  17. thth-2.0.0/tests/fixtures/build_injection_eval.py +174 -0
  18. thth-2.0.0/tests/fixtures/injection_eval.json +59 -0
  19. thth-2.0.0/tests/fixtures/umami-bile.md +22 -0
  20. thth-2.0.0/tests/helpers/__init__.py +0 -0
  21. thth-2.0.0/tests/helpers/fake_oauth_server.py +90 -0
  22. thth-2.0.0/tests/helpers/fake_push_adapter.py +68 -0
  23. thth-2.0.0/tests/helpers/fake_threads_reader.py +39 -0
  24. thth-2.0.0/tests/helpers/hold_lock.py +31 -0
  25. thth-2.0.0/tests/helpers/hold_repo_lock.py +31 -0
  26. thth-2.0.0/tests/helpers/share_ledger.py +92 -0
  27. thth-2.0.0/tests/test_account_report.py +225 -0
  28. thth-2.0.0/tests/test_accounts_dir.py +781 -0
  29. thth-2.0.0/tests/test_adapter_boundary.py +723 -0
  30. thth-2.0.0/tests/test_api_error_body.py +195 -0
  31. thth-2.0.0/tests/test_app_env_cli.py +224 -0
  32. thth-2.0.0/tests/test_approval.py +140 -0
  33. thth-2.0.0/tests/test_approve_cmd.py +335 -0
  34. thth-2.0.0/tests/test_ask.py +739 -0
  35. thth-2.0.0/tests/test_atlas_fifth_review.py +238 -0
  36. thth-2.0.0/tests/test_atlas_fourth_review.py +165 -0
  37. thth-2.0.0/tests/test_atlas_review.py +165 -0
  38. thth-2.0.0/tests/test_atlas_second_review.py +155 -0
  39. thth-2.0.0/tests/test_atlas_third_review.py +124 -0
  40. thth-2.0.0/tests/test_audit1_fixes.py +729 -0
  41. thth-2.0.0/tests/test_audit2_fixes.py +57 -0
  42. thth-2.0.0/tests/test_audit_b_counterexamples.py +139 -0
  43. thth-2.0.0/tests/test_audit_b_m1_semantics.py +98 -0
  44. thth-2.0.0/tests/test_audit_b_n1_depth.py +138 -0
  45. thth-2.0.0/tests/test_audit_b_r1_closure.py +162 -0
  46. thth-2.0.0/tests/test_audit_v23_details.py +276 -0
  47. thth-2.0.0/tests/test_auth.py +436 -0
  48. thth-2.0.0/tests/test_bluesky_adapter.py +699 -0
  49. thth-2.0.0/tests/test_bluesky_secret_scrub.py +207 -0
  50. thth-2.0.0/tests/test_board_needs_review.py +137 -0
  51. thth-2.0.0/tests/test_board_running.py +139 -0
  52. thth-2.0.0/tests/test_bundle.py +306 -0
  53. thth-2.0.0/tests/test_charcount.py +29 -0
  54. thth-2.0.0/tests/test_cli_entrypoint.py +27 -0
  55. thth-2.0.0/tests/test_collect.py +592 -0
  56. thth-2.0.0/tests/test_comparability.py +150 -0
  57. thth-2.0.0/tests/test_conversation_paging.py +110 -0
  58. thth-2.0.0/tests/test_docs_links.py +173 -0
  59. thth-2.0.0/tests/test_doctor.py +238 -0
  60. thth-2.0.0/tests/test_doctor_next_step.py +257 -0
  61. thth-2.0.0/tests/test_editorial_audit.py +180 -0
  62. thth-2.0.0/tests/test_editorial_n3_n4.py +88 -0
  63. thth-2.0.0/tests/test_external_acceptance.py +100 -0
  64. thth-2.0.0/tests/test_fake_api.py +307 -0
  65. thth-2.0.0/tests/test_form_spec.py +227 -0
  66. thth-2.0.0/tests/test_form_spec_cli.py +328 -0
  67. thth-2.0.0/tests/test_forms.py +349 -0
  68. thth-2.0.0/tests/test_fresh_install.py +530 -0
  69. thth-2.0.0/tests/test_hypotheses.py +279 -0
  70. thth-2.0.0/tests/test_hypothesis_boundaries.py +17 -0
  71. thth-2.0.0/tests/test_hypothesis_units.py +626 -0
  72. thth-2.0.0/tests/test_impact_cli.py +136 -0
  73. thth-2.0.0/tests/test_independent_thread_review.py +103 -0
  74. thth-2.0.0/tests/test_independent_topic_review.py +144 -0
  75. thth-2.0.0/tests/test_independent_topic_review_r2.py +67 -0
  76. thth-2.0.0/tests/test_inflight.py +38 -0
  77. thth-2.0.0/tests/test_jst.py +37 -0
  78. thth-2.0.0/tests/test_lint.py +125 -0
  79. thth-2.0.0/tests/test_list_projections.py +126 -0
  80. thth-2.0.0/tests/test_llm_trial.py +472 -0
  81. thth-2.0.0/tests/test_lock.py +62 -0
  82. thth-2.0.0/tests/test_m2_independent.py +96 -0
  83. thth-2.0.0/tests/test_maintain.py +176 -0
  84. thth-2.0.0/tests/test_mastodon_adapter.py +704 -0
  85. thth-2.0.0/tests/test_max_per_run.py +169 -0
  86. thth-2.0.0/tests/test_mcp.py +108 -0
  87. thth-2.0.0/tests/test_measured.py +766 -0
  88. thth-2.0.0/tests/test_media_accounts.py +201 -0
  89. thth-2.0.0/tests/test_media_next_steps.py +156 -0
  90. thth-2.0.0/tests/test_media_roundtrip.py +273 -0
  91. thth-2.0.0/tests/test_mismatch_diagnostics.py +122 -0
  92. thth-2.0.0/tests/test_n5_reply_null_element.py +103 -0
  93. thth-2.0.0/tests/test_n6_replies_incomplete_ledger.py +96 -0
  94. thth-2.0.0/tests/test_n7_doctor_error_body.py +113 -0
  95. thth-2.0.0/tests/test_no_limits.py +86 -0
  96. thth-2.0.0/tests/test_observation_provenance.py +108 -0
  97. thth-2.0.0/tests/test_packaging.py +399 -0
  98. thth-2.0.0/tests/test_preview.py +30 -0
  99. thth-2.0.0/tests/test_production_mode.py +37 -0
  100. thth-2.0.0/tests/test_queue.py +105 -0
  101. thth-2.0.0/tests/test_quiet_hours.py +163 -0
  102. thth-2.0.0/tests/test_r2_r4_r5.py +122 -0
  103. thth-2.0.0/tests/test_reason_adoption.py +676 -0
  104. thth-2.0.0/tests/test_redact.py +66 -0
  105. thth-2.0.0/tests/test_refresh.py +164 -0
  106. thth-2.0.0/tests/test_refresh_destination.py +205 -0
  107. thth-2.0.0/tests/test_rejudge_new.py +44 -0
  108. thth-2.0.0/tests/test_rejudge_r2_r3.py +119 -0
  109. thth-2.0.0/tests/test_replies.py +159 -0
  110. thth-2.0.0/tests/test_replies_refresh.py +372 -0
  111. thth-2.0.0/tests/test_repo_lock.py +129 -0
  112. thth-2.0.0/tests/test_repo_sync.py +180 -0
  113. thth-2.0.0/tests/test_reverse_audit.py +325 -0
  114. thth-2.0.0/tests/test_review_case_id_cluster_m2.py +73 -0
  115. thth-2.0.0/tests/test_review_case_id_cluster_r2.py +135 -0
  116. thth-2.0.0/tests/test_review_cli.py +474 -0
  117. thth-2.0.0/tests/test_review_evidence_account.py +152 -0
  118. thth-2.0.0/tests/test_review_form_spec_and_case_id.py +127 -0
  119. thth-2.0.0/tests/test_review_provenance.py +177 -0
  120. thth-2.0.0/tests/test_review_records.py +399 -0
  121. thth-2.0.0/tests/test_revoke.py +173 -0
  122. thth-2.0.0/tests/test_roundtrip.py +98 -0
  123. thth-2.0.0/tests/test_run_missing_env.py +49 -0
  124. thth-2.0.0/tests/test_run_record_failclosed.py +292 -0
  125. thth-2.0.0/tests/test_runs_collected.py +23 -0
  126. thth-2.0.0/tests/test_schedule_multiple.py +126 -0
  127. thth-2.0.0/tests/test_schedule_view.py +65 -0
  128. thth-2.0.0/tests/test_secrets_grep.py +56 -0
  129. thth-2.0.0/tests/test_select.py +198 -0
  130. thth-2.0.0/tests/test_selfupdate.py +783 -0
  131. thth-2.0.0/tests/test_send.py +138 -0
  132. thth-2.0.0/tests/test_send_cli.py +60 -0
  133. thth-2.0.0/tests/test_send_media.py +207 -0
  134. thth-2.0.0/tests/test_send_visibility.py +320 -0
  135. thth-2.0.0/tests/test_sent_integrity.py +123 -0
  136. thth-2.0.0/tests/test_server_json.py +106 -0
  137. thth-2.0.0/tests/test_share.py +413 -0
  138. thth-2.0.0/tests/test_share_safety.py +239 -0
  139. thth-2.0.0/tests/test_share_sync_concurrent.py +93 -0
  140. thth-2.0.0/tests/test_systemd.py +73 -0
  141. thth-2.0.0/tests/test_thread_publish.py +971 -0
  142. thth-2.0.0/tests/test_threadrun.py +212 -0
  143. thth-2.0.0/tests/test_threadshape.py +604 -0
  144. thth-2.0.0/tests/test_thth_root.py +103 -0
  145. thth-2.0.0/tests/test_token_exists.py +43 -0
  146. thth-2.0.0/tests/test_token_set.py +380 -0
  147. thth-2.0.0/tests/test_topic.py +48 -0
  148. thth-2.0.0/tests/test_topic_advice.py +408 -0
  149. thth-2.0.0/tests/test_topic_cli.py +480 -0
  150. thth-2.0.0/tests/test_topic_models.py +199 -0
  151. thth-2.0.0/tests/test_topic_review_fixes.py +1383 -0
  152. thth-2.0.0/tests/test_topic_shelf_observers.py +302 -0
  153. thth-2.0.0/tests/test_topic_store.py +147 -0
  154. thth-2.0.0/tests/test_topics_record_concurrent.py +42 -0
  155. thth-2.0.0/tests/test_topics_tool.py +550 -0
  156. thth-2.0.0/tests/test_trial_frictions.py +201 -0
  157. thth-2.0.0/tests/test_value_domains_shown.py +107 -0
  158. thth-2.0.0/tests/test_version.py +22 -0
  159. thth-2.0.0/tests/test_writeback_rebase.py +55 -0
  160. thth-2.0.0/tests/test_writeback_validate.py +97 -0
  161. thth-2.0.0/thth/VERSION +1 -0
  162. thth-2.0.0/thth/__init__.py +22 -0
  163. thth-2.0.0/thth/__main__.py +18 -0
  164. thth-2.0.0/thth/account_cli.py +474 -0
  165. thth-2.0.0/thth/account_report.py +740 -0
  166. thth-2.0.0/thth/accounts.py +418 -0
  167. thth-2.0.0/thth/adapters/__init__.py +71 -0
  168. thth-2.0.0/thth/adapters/base.py +314 -0
  169. thth-2.0.0/thth/adapters/bluesky.py +772 -0
  170. thth-2.0.0/thth/adapters/mastodon.py +705 -0
  171. thth-2.0.0/thth/adapters/threads.py +580 -0
  172. thth-2.0.0/thth/appenv.py +204 -0
  173. thth-2.0.0/thth/approval.py +231 -0
  174. thth-2.0.0/thth/ask.py +527 -0
  175. thth-2.0.0/thth/ask_cli.py +142 -0
  176. thth-2.0.0/thth/bundle.py +428 -0
  177. thth-2.0.0/thth/cli.py +2572 -0
  178. thth-2.0.0/thth/collect.py +897 -0
  179. thth-2.0.0/thth/core.py +814 -0
  180. thth-2.0.0/thth/doctor.py +281 -0
  181. thth-2.0.0/thth/forms.py +255 -0
  182. thth-2.0.0/thth/inflight.py +69 -0
  183. thth-2.0.0/thth/jst.py +43 -0
  184. thth-2.0.0/thth/lint.py +186 -0
  185. thth-2.0.0/thth/lock.py +185 -0
  186. thth-2.0.0/thth/maintain.py +294 -0
  187. thth-2.0.0/thth/measured.py +455 -0
  188. thth-2.0.0/thth/oauth.py +718 -0
  189. thth-2.0.0/thth/postid.py +74 -0
  190. thth-2.0.0/thth/queuefile.py +199 -0
  191. thth-2.0.0/thth/redact.py +41 -0
  192. thth-2.0.0/thth/replies.py +175 -0
  193. thth-2.0.0/thth/report.py +418 -0
  194. thth-2.0.0/thth/runs.py +61 -0
  195. thth-2.0.0/thth/scopes.py +22 -0
  196. thth-2.0.0/thth/secrets_fs.py +68 -0
  197. thth-2.0.0/thth/select.py +338 -0
  198. thth-2.0.0/thth/selfupdate.py +516 -0
  199. thth-2.0.0/thth/sent.py +129 -0
  200. thth-2.0.0/thth/share.py +666 -0
  201. thth-2.0.0/thth/share_cli.py +140 -0
  202. thth-2.0.0/thth/systemd_gen.py +116 -0
  203. thth-2.0.0/thth/threadrun.py +611 -0
  204. thth-2.0.0/thth/threadshape.py +607 -0
  205. thth-2.0.0/thth/threadthrow.py +553 -0
  206. thth-2.0.0/thth/topic_advice.py +753 -0
  207. thth-2.0.0/thth/topic_cli.py +2267 -0
  208. thth-2.0.0/thth/topic_models.py +2248 -0
  209. thth-2.0.0/thth/topic_store.py +418 -0
  210. thth-2.0.0/thth/topics.py +802 -0
  211. thth-2.0.0/thth/writeback.py +408 -0
thth-2.0.0/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ .DS_Store
2
+ __pycache__/
3
+ *.pyc
4
+ .venv/
5
+ logs/
6
+ state/
7
+ .pytest_cache/
8
+ dist/
9
+ .claude/worktrees/
10
+ callback/.wrangler/
11
+ callback/node_modules/
thth-2.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gotoq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
thth-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.5
2
+ Name: thth
3
+ Version: 2.0.0
4
+ Summary: 承認済みの投稿だけを投げる門(Threads・Bluesky・Mastodon)。人が承認し、記録は利用者の git に残る。
5
+ Project-URL: Homepage, https://thth.me
6
+ Project-URL: Source, https://github.com/aokings/thth
7
+ Author: Masaru Aoki
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: approval,bluesky,cli,mastodon,mcp,social,threads
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Communications
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.10
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest-xdist>=3; extra == 'dev'
22
+ Requires-Dist: pytest>=7; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # THTH(ThreadsThrower・承認済み投稿の投げ手)
26
+
27
+ 設計: [docs/設計_THTH_2026-09-08.md](docs/設計_THTH_2026-09-08.md)(§8 の裁定済み。現行の設計書)。
28
+
29
+ 承認済み(`status: approved`)の投稿ファイルを読み、静かな時間帯と最短間隔を守って 1 件だけ投げ、
30
+ post_id を書き戻して push し、投稿済みの返信を拾って追記し、走った記録と鮮度を出し、
31
+ 走らなかったことを外部に知らせるまでを持つ。意味(何を書くか・承認・返信の解釈・採集箱への昇格)は持たない。
32
+ watchtower(`~/Developer/watchtower`)の隣に同じ流儀で並べる。watchtower は POST しないので投稿は混ぜない。
33
+
34
+ ## 版
35
+
36
+ **2.0.0**。門を無料で開ける版(`pip install`・`thth ask`・`thth share`・台帳を repo の外へ)。範囲と裁定は [docs/設計_v2_泉と門_2026-09-13.md](docs/設計_v2_泉と門_2026-09-13.md) §3・§7、その前の版は [docs/設計_v1.0.0_他人が導入できる版_2026-09-12.md](docs/設計_v1.0.0_他人が導入できる版_2026-09-12.md)、導入は [docs/導入_自分のMetaアプリで動かす.md](docs/導入_自分のMetaアプリで動かす.md)、文書の索引は [docs/README.md](docs/README.md)。
37
+
38
+ `thth/VERSION` は `2.0.0`(版は `thth/VERSION` の 1 か所・`server.json` はテストで一致を強制)。`origin/release` は v1.3.0(`0bd8208`)のままで、**v2.0.0 はまだ配っていない**(配布は masaru の一言)。出口条件の試験は(運用日誌 thth-notes: `記録/試験_LLMに選ばせる_2026-09-13.md`)。
39
+
40
+ **`main` への push は保存だけ。`release` を進める操作が配布**(VM は `release` だけを追う。設計 §3.2.1)。
41
+
42
+ ## 使う人へ
43
+
44
+ 各プロジェクトのセッションが読むのは [docs/使い方_プロジェクトのセッション向け_2026-09-09.md](docs/使い方_プロジェクトのセッション向け_2026-09-09.md) **だけ**。設計書は作った側の記録なので読まなくてよい。
45
+
46
+ ## いまの状態(2026-09-13)
47
+
48
+ 版 **2.0.0**。全件テスト **1828 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
49
+
50
+ **媒体**: Threads(稼働)・Bluesky・Mastodon(同席用の台帳あり・未稼働)。
51
+
52
+ **本番稼働中**: Threads の 4 アカウント(nigamilab・asmon 関東・kopicha・masaru の同席用)。
53
+ timer(systemd・`thth systemd` で生成)で毎時投稿、返信の採集(`thth replies`)と数の採集
54
+ (`thth measured`)は稼働、トークン更新は `thth maintain` が毎日。
55
+
56
+ **入口は 4 つ**: 厚い CLI(`bin/thth`・`python -m thth`)、薄い MCP(`mcp/server.py`・読み取りと
57
+ 同席の投稿・`before_you_post`)、timer、そして `pip install`(`thth`・`thth-mcp` の entry point・
58
+ **依存 0**。ただし **PyPI への登録と upload は masaru の手で、まだしていない**)。
59
+
60
+ **トピックの棚**(`thth topics`): 観測者ごとに並ぶ・打ち消し `retract-note`・`history`。
61
+
62
+ ### v2.0.0 で増えた口
63
+
64
+ - **台帳を repo の外へ**(設計 v2 §3・裁定 §7-1)。正は `$THTH_ROOT/accounts/`(`$THTH_ACCOUNTS_DIR` があればそちら)。
65
+ repo の `accounts/` は**1 版だけ互換で読む**(stderr に警告 1 行・VM を止めないため)。
66
+ `thth account migrate` が repo の中を外へ **copy**(移動しない・上書きしない・冪等)、
67
+ `thth account add <name> --media threads|bluesky|mastodon --project <p>` が
68
+ `accounts.example/<media>.json` の雛形から 1 本書く(**必ず `production: false`**)。
69
+ **書く先は互換に落ちていても常に外**。`doctor`・`board` が置き場を 1 行で言う。
70
+ - **`thth ask before-you-post <account> --topic <語>`**(設計 v2 §1)。この語・この型・この時刻帯で
71
+ スレッドがどう伸びたかを、件数と期間つきで返す。**読むだけ・手元の台帳だけ**(`provenance.source`
72
+ は `local`。泉のサーバはまだ無い)。原稿本文は渡さないし、答えにも出ない。
73
+ **n が閾値(既定 20)に満たない群は中央値を返さず `cannot_say` に理由を出す**——
74
+ 手元の水ではほとんどが `cannot_say` になる。それが正しい答えで、rc は 0。MCP からは `before_you_post`。
75
+ - **`thth share on|off|status|log|sync`**(設計 v2 §3・裁定 §7-3)。**既定 off**、設定が無い・壊れて
76
+ いるときも off。off のあいだは outbox が **0 バイト**。on にしても
77
+ `$THTH_ROOT/state/share/outbox/<YYYY-MM>.ndjson` に積むだけで、**送り先はまだ無い**(v2-5)。
78
+ 積んだ全部は `thth share log` で読める。落ちるのは語・audience・型・件数・時刻帯・
79
+ post_id の**塩つき sha256**(塩は outbox に出ない)。本文・返信本文・返信者の username・
80
+ 自分の判断・アカウント名・トークン・repo のパス・生の post_id は落ちない。
81
+ - **英語の文書**: [README.en.md](README.en.md)・[docs/usage.en.md](docs/usage.en.md)・[llms.txt](llms.txt)。
82
+ - **skill**: `skills/thth/SKILL.md`(wheel にも入る)。
83
+
84
+ - **動くもの**(`thth --help` の全 27 サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`share`・`board`・`collect`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`。
85
+ - **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
86
+ - **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
87
+ - **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
88
+ - **masaru の手が要るもの**: PyPI の登録と upload、repo を public にする切替と `LICENSE`、MCP registry への登録、tag と `release` を進める操作。
89
+
90
+ ## MCP registry
91
+
92
+ registry は「この PyPI の名前を名乗ってよいのは誰か」を、**配布物の README にこの 1 行があるか**で確かめる(設計 v2-4 §3・一次資料は quickstart・**L2**)。だから消さないこと——消すと登録(`mcp-publisher publish`)が通らなくなる。形(`server.json` と版の一致)は `tests/test_server_json.py` が見張る。
93
+
94
+ mcp-name: io.github.aokings/thth
95
+
thth-2.0.0/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # THTH(ThreadsThrower・承認済み投稿の投げ手)
2
+
3
+ 設計: [docs/設計_THTH_2026-09-08.md](docs/設計_THTH_2026-09-08.md)(§8 の裁定済み。現行の設計書)。
4
+
5
+ 承認済み(`status: approved`)の投稿ファイルを読み、静かな時間帯と最短間隔を守って 1 件だけ投げ、
6
+ post_id を書き戻して push し、投稿済みの返信を拾って追記し、走った記録と鮮度を出し、
7
+ 走らなかったことを外部に知らせるまでを持つ。意味(何を書くか・承認・返信の解釈・採集箱への昇格)は持たない。
8
+ watchtower(`~/Developer/watchtower`)の隣に同じ流儀で並べる。watchtower は POST しないので投稿は混ぜない。
9
+
10
+ ## 版
11
+
12
+ **2.0.0**。門を無料で開ける版(`pip install`・`thth ask`・`thth share`・台帳を repo の外へ)。範囲と裁定は [docs/設計_v2_泉と門_2026-09-13.md](docs/設計_v2_泉と門_2026-09-13.md) §3・§7、その前の版は [docs/設計_v1.0.0_他人が導入できる版_2026-09-12.md](docs/設計_v1.0.0_他人が導入できる版_2026-09-12.md)、導入は [docs/導入_自分のMetaアプリで動かす.md](docs/導入_自分のMetaアプリで動かす.md)、文書の索引は [docs/README.md](docs/README.md)。
13
+
14
+ `thth/VERSION` は `2.0.0`(版は `thth/VERSION` の 1 か所・`server.json` はテストで一致を強制)。`origin/release` は v1.3.0(`0bd8208`)のままで、**v2.0.0 はまだ配っていない**(配布は masaru の一言)。出口条件の試験は(運用日誌 thth-notes: `記録/試験_LLMに選ばせる_2026-09-13.md`)。
15
+
16
+ **`main` への push は保存だけ。`release` を進める操作が配布**(VM は `release` だけを追う。設計 §3.2.1)。
17
+
18
+ ## 使う人へ
19
+
20
+ 各プロジェクトのセッションが読むのは [docs/使い方_プロジェクトのセッション向け_2026-09-09.md](docs/使い方_プロジェクトのセッション向け_2026-09-09.md) **だけ**。設計書は作った側の記録なので読まなくてよい。
21
+
22
+ ## いまの状態(2026-09-13)
23
+
24
+ 版 **2.0.0**。全件テスト **1828 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
25
+
26
+ **媒体**: Threads(稼働)・Bluesky・Mastodon(同席用の台帳あり・未稼働)。
27
+
28
+ **本番稼働中**: Threads の 4 アカウント(nigamilab・asmon 関東・kopicha・masaru の同席用)。
29
+ timer(systemd・`thth systemd` で生成)で毎時投稿、返信の採集(`thth replies`)と数の採集
30
+ (`thth measured`)は稼働、トークン更新は `thth maintain` が毎日。
31
+
32
+ **入口は 4 つ**: 厚い CLI(`bin/thth`・`python -m thth`)、薄い MCP(`mcp/server.py`・読み取りと
33
+ 同席の投稿・`before_you_post`)、timer、そして `pip install`(`thth`・`thth-mcp` の entry point・
34
+ **依存 0**。ただし **PyPI への登録と upload は masaru の手で、まだしていない**)。
35
+
36
+ **トピックの棚**(`thth topics`): 観測者ごとに並ぶ・打ち消し `retract-note`・`history`。
37
+
38
+ ### v2.0.0 で増えた口
39
+
40
+ - **台帳を repo の外へ**(設計 v2 §3・裁定 §7-1)。正は `$THTH_ROOT/accounts/`(`$THTH_ACCOUNTS_DIR` があればそちら)。
41
+ repo の `accounts/` は**1 版だけ互換で読む**(stderr に警告 1 行・VM を止めないため)。
42
+ `thth account migrate` が repo の中を外へ **copy**(移動しない・上書きしない・冪等)、
43
+ `thth account add <name> --media threads|bluesky|mastodon --project <p>` が
44
+ `accounts.example/<media>.json` の雛形から 1 本書く(**必ず `production: false`**)。
45
+ **書く先は互換に落ちていても常に外**。`doctor`・`board` が置き場を 1 行で言う。
46
+ - **`thth ask before-you-post <account> --topic <語>`**(設計 v2 §1)。この語・この型・この時刻帯で
47
+ スレッドがどう伸びたかを、件数と期間つきで返す。**読むだけ・手元の台帳だけ**(`provenance.source`
48
+ は `local`。泉のサーバはまだ無い)。原稿本文は渡さないし、答えにも出ない。
49
+ **n が閾値(既定 20)に満たない群は中央値を返さず `cannot_say` に理由を出す**——
50
+ 手元の水ではほとんどが `cannot_say` になる。それが正しい答えで、rc は 0。MCP からは `before_you_post`。
51
+ - **`thth share on|off|status|log|sync`**(設計 v2 §3・裁定 §7-3)。**既定 off**、設定が無い・壊れて
52
+ いるときも off。off のあいだは outbox が **0 バイト**。on にしても
53
+ `$THTH_ROOT/state/share/outbox/<YYYY-MM>.ndjson` に積むだけで、**送り先はまだ無い**(v2-5)。
54
+ 積んだ全部は `thth share log` で読める。落ちるのは語・audience・型・件数・時刻帯・
55
+ post_id の**塩つき sha256**(塩は outbox に出ない)。本文・返信本文・返信者の username・
56
+ 自分の判断・アカウント名・トークン・repo のパス・生の post_id は落ちない。
57
+ - **英語の文書**: [README.en.md](README.en.md)・[docs/usage.en.md](docs/usage.en.md)・[llms.txt](llms.txt)。
58
+ - **skill**: `skills/thth/SKILL.md`(wheel にも入る)。
59
+
60
+ - **動くもの**(`thth --help` の全 27 サブコマンド): `lint`・`preview`・`approve`・`account`・`revoke`・`posts`・`replies`・`measured`・`threads`・`topics`・`forms`・`queue`・`schedule`・`throw`・`run`・`systemd`・`share`・`board`・`collect`・`auth`・`refresh`・`maintain`・`send`・`doctor`・`app`・`token`・`ask`。
61
+ - **最初の本番投稿の記録**: 2026-09-09、@aoking に疎通確認を 1 本(`17916074118445631`)。
62
+ - **未着手**: X・Facebook ページ・Instagram の各アダプタ。トピック検索の権限(tester には降りない)。泉のサーバ(v2-5)。
63
+ - **権限の制約**: tester に降りる scope は 5 つ。削除はできない。
64
+ - **masaru の手が要るもの**: PyPI の登録と upload、repo を public にする切替と `LICENSE`、MCP registry への登録、tag と `release` を進める操作。
65
+
66
+ ## MCP registry
67
+
68
+ registry は「この PyPI の名前を名乗ってよいのは誰か」を、**配布物の README にこの 1 行があるか**で確かめる(設計 v2-4 §3・一次資料は quickstart・**L2**)。だから消さないこと——消すと登録(`mcp-publisher publish`)が通らなくなる。形(`server.json` と版の一致)は `tests/test_server_json.py` が見張る。
69
+
70
+ mcp-name: io.github.aokings/thth
71
+
@@ -0,0 +1,25 @@
1
+ {
2
+ "account": "demo-bluesky",
3
+ "project": "demo",
4
+ "media": "bluesky",
5
+ "handle": "demo.bsky.social",
6
+ "service": "https://bsky.social",
7
+ "user_id": "",
8
+ "repo_dir": "$THTH_ROOT/repos/demo",
9
+ "queue_dir": "docs/sns/queue",
10
+ "replies_dir": "data/sns/replies",
11
+ "quiet_hours": ["22:00", "07:00"],
12
+ "min_interval_hours": 6,
13
+ "max_per_run": 1,
14
+ "tick_minutes": 10,
15
+ "collect_days": 14,
16
+ "hashtags": false,
17
+ "stale_days": 7,
18
+ "env": "~/.config/thth/demo-bluesky.env",
19
+ "token": "~/.config/thth/demo-bluesky.token",
20
+ "ping": "wrapper",
21
+ "timeout": 300,
22
+ "dry_run_env": "THTH_DRY_RUN",
23
+ "production": false,
24
+ "scheduled": false
25
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "account": "demo-mastodon",
3
+ "project": "demo",
4
+ "media": "mastodon",
5
+ "handle": "demo",
6
+ "instance": "https://mastodon.example",
7
+ "user_id": "",
8
+ "repo_dir": "$THTH_ROOT/repos/demo",
9
+ "queue_dir": "docs/sns/queue",
10
+ "replies_dir": "data/sns/replies",
11
+ "quiet_hours": ["22:00", "07:00"],
12
+ "min_interval_hours": 6,
13
+ "max_per_run": 1,
14
+ "tick_minutes": 10,
15
+ "collect_days": 14,
16
+ "hashtags": false,
17
+ "stale_days": 7,
18
+ "env": "~/.config/thth/demo-mastodon.env",
19
+ "token": "~/.config/thth/demo-mastodon.token",
20
+ "ping": "wrapper",
21
+ "timeout": 300,
22
+ "dry_run_env": "THTH_DRY_RUN",
23
+ "production": false,
24
+ "scheduled": false
25
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "account": "demo-threads",
3
+ "project": "demo",
4
+ "media": "threads",
5
+ "handle": "demo",
6
+ "user_id": "",
7
+ "redirect_uri": "https://example.invalid/",
8
+ "repo_dir": "$THTH_ROOT/repos/demo",
9
+ "queue_dir": "docs/sns/queue",
10
+ "replies_dir": "data/sns/replies",
11
+ "quiet_hours": ["22:00", "07:00"],
12
+ "min_interval_hours": 6,
13
+ "max_per_run": 1,
14
+ "tick_minutes": 10,
15
+ "collect_days": 14,
16
+ "hashtags": false,
17
+ "stale_days": 7,
18
+ "env": "~/.config/thth/demo-threads.env",
19
+ "token": "~/.config/thth/demo-threads.token",
20
+ "ping": "wrapper",
21
+ "timeout": 300,
22
+ "dry_run_env": "THTH_DRY_RUN",
23
+ "production": false,
24
+ "scheduled": false
25
+ }
thth-2.0.0/bin/thth ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env python3
2
+ """thth CLI の唯一の入口(発注 §2)。argparse でサブコマンドに振り分けるだけで、
3
+ 判断は `thth.cli`・`thth.core` 等に置く。"""
4
+ import os
5
+ import sys
6
+
7
+ # realpath を通す: PATH に置くために symlink を張る(例 ~/.local/bin/thth →
8
+ # /srv/thth/app/bin/thth)と、abspath では symlink 自身の場所を指してしまい
9
+ # repo の親が見つからない(2026-09-09 VM 設置で実際に踏んだ)。
10
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
11
+
12
+ from thth.cli import main # noqa: E402
13
+
14
+ if __name__ == "__main__":
15
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ # 手元(Mac)から VM の `thth` を呼ぶ薄いラッパ(masaru 指摘 2026-09-10)。
3
+ #
4
+ # **なぜ要るか。** 各プロジェクトのセッションは `ssh wt 'PATH=...; thth ...'` と
5
+ # 打っていた。安全で特定の作業をするために、いちばん鈍器な道具(生の ssh)を
6
+ # 持たせていたことになる。実際、権限判定に何度も弾かれ、ループの形では止められた。
7
+ # **道具を特定にすれば、許可も特定にできる**(`Bash(thth:*)` の 1 行で済む)。
8
+ #
9
+ # 引数はそのまま VM の `thth` に渡す。手元のシェルに展開させない
10
+ # (VM 側のパスは手元に存在しないので、glob は使わずディレクトリを渡すこと)。
11
+ # 秘密を対話で入れるもの(`token set`・`auth`)だけ tty を割り当てる——tty が
12
+ # 無いと getpass がエコーを止められず、**手元の画面にトークンが出る**。
13
+ set -euo pipefail
14
+
15
+ HOST="${THTH_SSH_HOST:-wt}"
16
+ REMOTE='PATH="$HOME/.local/bin:$PATH"; exec thth'
17
+ for arg in "$@"; do
18
+ REMOTE+=" $(printf '%q' "$arg")"
19
+ done
20
+
21
+ # `set -u` の下で空配列を展開すると古い bash(macOS 既定の 3.2)で落ちるので、
22
+ # 文字列で持つ(引数は 1 つだけなので分割の心配が無い)。
23
+ TTY_FLAG=""
24
+ case "${1:-} ${2:-}" in
25
+ "token set"|"auth "*) TTY_FLAG="-t" ;;
26
+ esac
27
+
28
+ if [ -n "$TTY_FLAG" ]; then
29
+ exec ssh -t "$HOST" "$REMOTE"
30
+ fi
31
+ exec ssh "$HOST" "$REMOTE"
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # systemd timer から呼ばれる薄いラッパ(発注 §2)。`thth run <account>` を呼ぶだけ。
3
+ # flock・inflight の確認は core 側(thth/lock.py・thth/core.py)に置いてあるので、
4
+ # ここには置かない(§3.7: 入口を増やしてもロックを踏まない経路を作らないため)。
5
+ set -euo pipefail
6
+ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ exec "$DIR/thth" run "$@"
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env python3
2
+ """薄い MCP(stdio)。発注 §4・設計 §3.7。
3
+
4
+ 中身は CLI(`bin/thth`)を subprocess で呼んで `--json` の出力を返すだけ。
5
+ 判断・整形・条件分岐はここに書かない(MCP に業務論理を持たせない・§3.7)。
6
+
7
+ T1 で出すのは読み取り 4 本だけ: thth_lint・thth_queue・thth_preview・thth_board。
8
+ thth_throw・thth_approve・thth_auth・thth_refresh は T1 では作らない(§4)。
9
+
10
+ MCP の stdio トランスポートは改行区切りの JSON-RPC 2.0(1 メッセージ 1 行、
11
+ 埋め込み改行なし)。追加の依存を持ち込まないため(標準ライブラリのみ・発注 §0-4)
12
+ 最小限のループを自前で書く。
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import subprocess
19
+ import sys
20
+
21
+ APP_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
+
23
+ # **CLI の在り処は 2 通りある**(設計 v2 §3・`pip install thth`)。
24
+ #
25
+ # - repo をそのまま使う運用: このファイルの 1 つ上に `bin/thth` がある(従来)。
26
+ # - `pip install thth`: wheel の中では**このファイル自身が `thth/mcp_server.py`**
27
+ # として入る(`pyproject.toml` の force-include)。隣に `bin/` は無い。
28
+ #
29
+ # **無いものを指したまま subprocess を起こさない。** `bin/thth` が実在するときだけ
30
+ # それを使い、無ければ `python -m thth` に落とす(`thth/__main__.py`・同じ `main()`)。
31
+ # `THTH_BIN` は従来どおり環境変数で上書きできる(テストはこの属性を差し替える)。
32
+ _REPO_BIN = os.path.join(APP_DIR, "bin", "thth")
33
+ THTH_BIN = os.environ.get("THTH_BIN") or (_REPO_BIN if os.path.exists(_REPO_BIN) else None)
34
+
35
+ TOOLS = [
36
+ {
37
+ "name": "thth_lint",
38
+ "description": "docs/sns/queue の 1 ファイルを形式検査する(読むだけ・副作用なし)。",
39
+ "inputSchema": {
40
+ "type": "object",
41
+ "properties": {"file": {"type": "string", "description": "queue ファイルのパス"}},
42
+ "required": ["file"],
43
+ },
44
+ },
45
+ {
46
+ "name": "thth_queue",
47
+ "description": "draft/approved/posted/型外 の一覧と、次に出るもの・いつかを返す(読むだけ)。",
48
+ "inputSchema": {
49
+ "type": "object",
50
+ "properties": {"account": {"type": "string", "description": "省略時は全アカウント"}},
51
+ },
52
+ },
53
+ {
54
+ "name": "thth_preview",
55
+ "description": "実際に投げる本文そのもの(媒体の節。前後に何も足さない)を返す(読むだけ)。",
56
+ "inputSchema": {
57
+ "type": "object",
58
+ "properties": {"file": {"type": "string", "description": "queue ファイルのパス"}},
59
+ "required": ["file"],
60
+ },
61
+ },
62
+ {
63
+ "name": "thth_topic_context",
64
+ "description": (
65
+ "原稿のトピックを決める前に**最初に**呼ぶ。原稿と(あれば)記事本文から、"
66
+ "判断の入力を固定して返す。記事がまだ無ければ、取りに行くべき URL と"
67
+ "必要な JSON の形を返す(読むだけ・副作用なし)。"
68
+ "使う順序: context → 足りない資料を取る → evaluate。"
69
+ ),
70
+ "inputSchema": {
71
+ "type": "object",
72
+ "properties": {
73
+ "file": {"type": "string", "description": "queue ファイルのパス"},
74
+ "article": {"type": "object", "description": "記事証拠(ArticleEvidence)"},
75
+ "article_url": {"type": "string",
76
+ "description": "本文に URL が複数ある場合の主対象"},
77
+ },
78
+ "required": ["file"],
79
+ },
80
+ },
81
+ {
82
+ "name": "thth_topic_evaluate",
83
+ "description": (
84
+ "記事本文と候補比較を渡して、**引用が本文に在るか・参照が実在するか・"
85
+ "観測が新しいか・投稿者が偏っていないか**を検査させる。"
86
+ "THTH は候補を作らないし順位も付けない——足りないものを言うだけ。"
87
+ "候補が suitable でなければ別の候補に繰り上げず、比較し直しを求める。"
88
+ ),
89
+ "inputSchema": {
90
+ "type": "object",
91
+ "properties": {
92
+ "file": {"type": "string"},
93
+ "article": {"type": "object"},
94
+ "proposal": {"type": "object", "description": "候補比較(TopicProposal)"},
95
+ "article_url": {"type": "string"},
96
+ },
97
+ "required": ["file", "article", "proposal"],
98
+ },
99
+ },
100
+ {
101
+ "name": "thth_topic_decision",
102
+ "description": "保存済みの判断を読む(読むだけ)。今の原稿と食い違っていれば freshness で言う。",
103
+ "inputSchema": {
104
+ "type": "object",
105
+ "properties": {"id": {"type": "string", "description": "decision_id"}},
106
+ "required": ["id"],
107
+ },
108
+ },
109
+ {
110
+ "name": "thth_board",
111
+ "description": "アカウントごとの最終投稿・approved 待ち・inflight・型外の骨を返す(読むだけ)。",
112
+ "inputSchema": {"type": "object", "properties": {}},
113
+ },
114
+ {
115
+ # **名前と説明文がそのまま売り文句**(設計 v2 §1 規約 6)。LLM は説明文で
116
+ # 呼ぶかを決めるので、**この文言は設計 v2 §1 の正本から動かさない。**
117
+ "name": "before_you_post",
118
+ # **設計 v2 §1 規約 6 の文言そのまま。** 実装が 1 語でも足すと、
119
+ # 「正本はどっちだ」が始まる(監査 2・B7 で「。言えないことは
120
+ # cannot_say に出る」を足していたのを外した)。**設計書ではなく
121
+ # 実装を戻す。**
122
+ "description": (
123
+ "投稿する前に呼ぶ。この語とこの型で、スレッドがどう伸びたかの実績を、"
124
+ "件数と期間つきで返す"
125
+ ),
126
+ "inputSchema": {
127
+ "type": "object",
128
+ "properties": {
129
+ "account": {"type": "string", "description": "account 名"},
130
+ "topic": {"type": "string", "description": "語(トピック)"},
131
+ "kind": {"type": "string", "description": "型(行動・年度付き 等)"},
132
+ "hour_band": {"type": "string",
133
+ "description": "出す予定の時刻帯(朝・昼・夕・深夜)"},
134
+ "is_reply": {"type": "boolean", "description": "返信として出すか"},
135
+ "window_days": {"type": "integer", "description": "直近何日(既定 30)"},
136
+ "min_n": {"type": "integer",
137
+ "description": "中央値を返す下限(既定 20)"},
138
+ },
139
+ "required": ["account", "topic"],
140
+ },
141
+ },
142
+ ]
143
+
144
+
145
+ def cli_argv(args: list) -> list:
146
+ """CLI を起こす argv。`THTH_BIN` があればそれ、無ければ `python -m thth`。"""
147
+ head = [sys.executable, THTH_BIN] if THTH_BIN else [sys.executable, "-m", "thth"]
148
+ return [*head, *args]
149
+
150
+
151
+ def run_cli(args: list, *, stdin_text: str | None = None) -> subprocess.CompletedProcess:
152
+ return subprocess.run(cli_argv(args), capture_output=True,
153
+ text=True, input=stdin_text)
154
+
155
+
156
+ def _topic_stdin(arguments: dict, *, with_proposal: bool) -> tuple:
157
+ """記事・候補比較は**標準入力に 1 つの JSON** で渡す(設計 §7)。
158
+
159
+ 外部モデルが指定した任意のサーバファイルへ書いて受け渡さない。stdin は 1 本
160
+ しかないので、記事と候補比較を別々の口にせず 1 つの封筒にまとめる
161
+ (**設計からの変更・理由はここ**)。CLI 側はファイル入力と同じ検査関数へ
162
+ 合流する。
163
+ """
164
+ args = ["topics", "suggest", arguments["file"], "--input-json-stdin"]
165
+ if arguments.get("article_url"):
166
+ args += ["--article-url", arguments["article_url"]]
167
+ payload = {"article": arguments.get("article")}
168
+ if with_proposal:
169
+ payload["proposal"] = arguments.get("proposal")
170
+ return args, json.dumps(payload, ensure_ascii=False)
171
+
172
+
173
+ def call_tool(name: str, arguments: dict | None) -> dict:
174
+ """CLI を呼んで結果を返すだけ。判断(条件分岐・整形)をここに書かない。"""
175
+ arguments = arguments or {}
176
+ if name == "thth_lint":
177
+ proc = run_cli(["lint", arguments["file"], "--json"])
178
+ text = proc.stdout
179
+ elif name == "thth_queue":
180
+ args = ["queue"]
181
+ if arguments.get("account"):
182
+ args.append(arguments["account"])
183
+ args.append("--json")
184
+ proc = run_cli(args)
185
+ text = proc.stdout
186
+ elif name == "thth_preview":
187
+ proc = run_cli(["preview", arguments["file"]])
188
+ text = proc.stdout if proc.returncode == 0 else proc.stderr
189
+ elif name in ("thth_topic_context", "thth_topic_evaluate"):
190
+ args, payload = _topic_stdin(
191
+ arguments, with_proposal=(name == "thth_topic_evaluate"))
192
+ proc = run_cli(args, stdin_text=payload)
193
+ text = proc.stdout
194
+ elif name == "thth_topic_decision":
195
+ proc = run_cli(["topics", "decision", arguments["id"], "--json"])
196
+ text = proc.stdout
197
+ elif name == "thth_board":
198
+ proc = run_cli(["board", "--json"])
199
+ text = proc.stdout
200
+ elif name == "before_you_post":
201
+ args = ["ask", "before-you-post", arguments["account"],
202
+ "--topic", arguments["topic"]]
203
+ if arguments.get("kind"):
204
+ args += ["--kind", arguments["kind"]]
205
+ if arguments.get("hour_band"):
206
+ args += ["--hour-band", arguments["hour_band"]]
207
+ if arguments.get("is_reply"):
208
+ args.append("--reply")
209
+ if arguments.get("window_days") is not None:
210
+ args += ["--window-days", str(arguments["window_days"])]
211
+ if arguments.get("min_n") is not None:
212
+ args += ["--min-n", str(arguments["min_n"])]
213
+ args.append("--json")
214
+ proc = run_cli(args)
215
+ text = proc.stdout
216
+ else:
217
+ return {"content": [{"type": "text", "text": f"unknown tool: {name}"}], "isError": True}
218
+
219
+ if name.startswith("thth_topic_") or name == "before_you_post":
220
+ # **新しい道具は exit 1 も isError**(設計 §7)。lint の exit 1(検査結果)
221
+ # とは意味が違う——こちらは stale_context・不正な候補比較で、
222
+ # **そのまま使ってはいけない**応答。既存の扱いは変えない。
223
+ # `before_you_post` も同じ(設計 v2 §1): exit 1 は台帳が無い、2 は問いが
224
+ # 受け取れない。**`cannot_say` だらけの答えは exit 0 で、error ではない。**
225
+ is_error = proc.returncode != 0
226
+ else:
227
+ is_error = proc.returncode not in (0, 1) # lint は 1 も正常な「検査結果」
228
+ if not text:
229
+ text = proc.stderr
230
+ return {"content": [{"type": "text", "text": text}], "isError": is_error}
231
+
232
+
233
+ def _send(obj: dict) -> None:
234
+ sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
235
+ sys.stdout.flush()
236
+
237
+
238
+ def _handle_request(req: dict):
239
+ method = req.get("method")
240
+ req_id = req.get("id")
241
+
242
+ if method == "initialize":
243
+ return {
244
+ "jsonrpc": "2.0", "id": req_id,
245
+ "result": {
246
+ "protocolVersion": "2024-11-05",
247
+ "capabilities": {"tools": {}},
248
+ "serverInfo": {"name": "thth", "version": "0.1.0"},
249
+ },
250
+ }
251
+ if method == "notifications/initialized":
252
+ return None
253
+ if method == "tools/list":
254
+ return {"jsonrpc": "2.0", "id": req_id, "result": {"tools": TOOLS}}
255
+ if method == "tools/call":
256
+ params = req.get("params") or {}
257
+ result = call_tool(params.get("name"), params.get("arguments"))
258
+ return {"jsonrpc": "2.0", "id": req_id, "result": result}
259
+ if req_id is not None:
260
+ return {"jsonrpc": "2.0", "id": req_id,
261
+ "error": {"code": -32601, "message": f"method not found: {method}"}}
262
+ return None
263
+
264
+
265
+ def main() -> None:
266
+ for line in sys.stdin:
267
+ line = line.strip()
268
+ if not line:
269
+ continue
270
+ try:
271
+ req = json.loads(line)
272
+ except json.JSONDecodeError:
273
+ continue
274
+ resp = _handle_request(req)
275
+ if resp is not None:
276
+ _send(resp)
277
+
278
+
279
+ if __name__ == "__main__":
280
+ main()