@tyhld/conductor 0.3.0 → 0.7.0

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 (46) hide show
  1. package/README.md +301 -19
  2. package/dist/cli.js +143 -18
  3. package/dist/ear-routing.js +57 -0
  4. package/dist/ear.js +57 -0
  5. package/dist/env-file-perm.js +67 -0
  6. package/dist/nudge.js +172 -0
  7. package/dist/realtime-parse.js +116 -0
  8. package/dist/realtime.js +251 -0
  9. package/dist/relay-runner.js +65 -0
  10. package/dist/relay.js +911 -134
  11. package/dist/websocket-transport.js +66 -0
  12. package/launchd/ear-install.sh +96 -0
  13. package/package.json +30 -1
  14. package/sales-template/README.md +185 -25
  15. package/sales-template/install.sh +890 -0
  16. package/sales-template/launchd/install.sh +151 -0
  17. package/sales-template/settings.json +26 -97
  18. package/sales-template/setup.sh +754 -117
  19. package/sales-template/systemd/README.md +28 -4
  20. package/sales-template/systemd/install.sh +54 -5
  21. package/sales-template/systemd/paste-cache-prune-install.sh +75 -0
  22. package/sales-template/systemd/tyhld-paste-cache-prune.service +25 -0
  23. package/sales-template/systemd/tyhld-paste-cache-prune.timer +19 -0
  24. package/sales-template/uninstall.sh +245 -0
  25. package/scripts/hooks/README.md +246 -0
  26. package/scripts/hooks/cc2_guard.py +145 -0
  27. package/scripts/hooks/codex-hooks.sample.json +58 -0
  28. package/scripts/hooks/hook_datalink.py +440 -0
  29. package/scripts/hooks/install-codex-hooks.sh +127 -0
  30. package/scripts/hooks/notification_hook.py +167 -0
  31. package/scripts/hooks/permission_request_hook.py +207 -0
  32. package/scripts/hooks/policy.py +759 -0
  33. package/scripts/hooks/settings.sample.json +142 -0
  34. package/scripts/hooks/stop_hook.py +275 -0
  35. package/scripts/hooks/summary_ja.py +155 -0
  36. package/scripts/hooks/test_hook_datalink.py +282 -0
  37. package/scripts/hooks/test_policy.py +1241 -0
  38. package/skills/conductor-craftsman/SKILL.md +40 -0
  39. package/systemd/conductor-ear.service +63 -0
  40. package/systemd/conductor@.service +62 -0
  41. package/systemd/ear-install.sh +131 -0
  42. package/systemd/guard-sync-install.sh +94 -0
  43. package/systemd/tyhld-guard-sync.service +28 -0
  44. package/systemd/tyhld-guard-sync.timer +25 -0
  45. package/sales-template/cc2_guard.py +0 -395
  46. package/sales-template/systemd/conductor@.service +0 -47
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: conductor-craftsman
3
+ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id> が付く作業指示)を受けたら必ず最初に適用する作業規律。Use this skill whenever the prompt contains a CONDUCTOR_JOB stamp.
4
+ ---
5
+
6
+ # 采配くん職人の鉄則
7
+
8
+ 采配くんの便(プロンプト末尾に `CONDUCTOR_JOB:<id>` の印が付いた作業指示)は、
9
+ 非エンジニアのお客さまの現場で、人が横に付かないまま実行される。
10
+ だから「速さ」より「戻せること・止まれること・報せること」を優先する。
11
+ 以下の13項を、着手前に読み、作業中も判断の基準にすること。
12
+
13
+ ## 鉄則13項
14
+
15
+ 1. **着手時の確認と枝分かれ** — `git status && git log --oneline -3 && git stash list` を確認してから main を最新化し、新ブランチを切る。
16
+ 2. **cd は1回だけ** — cd は作業開始時に1回だけ実行する。以後 cd を `&&` で他コマンドと連結しない・リダイレクトと混ぜない。
17
+ 3. **持ち場の外に出ない** — 指示された作業ディレクトリの外を変更しない。
18
+ 4. **壁に当たったら止まる** — サンドボックス制約・保護されたパスに当たったら回避せず中断して報告する(★通信で塞がれたときの読み方は鉄則12)。
19
+ 5. **エージェント設定に書き込まない** — `.mcp.json`、`.claude/` 配下、hooks 等のエージェント設定パスには書き込まない。
20
+ 6. **本番に触らない** — 本番DB・deploy・secret に触らない。SQL/DDL は作って提示のみ(適用は人間)。
21
+ 7. **便の終点はマージまで** — push → PR作成 → TYHLD-platform は `gh pr merge <番号> --auto --merge --delete-branch`(CI緑で自動合流・赤なら待機して状態を報告)/その他のリポは `gh pr merge <番号> --admin --merge --delete-branch`。
22
+ 8. **ブラウザ検品(Playwright)の作法** — ★取りに行かない。PCに1回だけ入れた共有の道具を指すだけ(ADR-010)。
23
+ - ① **起動は必ず `chromium_sandbox=False`**(`--no-sandbox`)。付けないと起動できない。
24
+ - ② **道具は共有の場所を使う。自分でダウンロードしない。** python は `$HOME/.tyhld/pw-venv/bin/python`、`PLAYWRIGHT_BROWSERS_PATH` は `$HOME/.tyhld/pw-browsers`。**無ければそこで止めて「人の手番=`APPLY=1 bash scripts/pw-browser-install.sh`」と報告する。** `playwright install` も `pip install playwright` も職人は実行しない(毎便まるごとダウンロードが走り、便が遅くなる。通信自体は鉄則12のとおり許可済みだが、取りに行かないのが本筋=ADR-010)。
25
+ - ③ **localhost の画面検品は proxy を指定しない。** プロキシ `localhost:3128` はサンドボックスの中にしか居ないので、外部URLへ出るときだけ Playwright の proxy オプションへ明示指定する(環境変数は拾われない)。`ECONNREFUSED` なら回避せず**未実施として報告**する。
26
+ - ④ 検品用の使い捨て画面(確認だけのHTML/スクリプト)は**リポジトリの中に作らない。置き場は `/tmp/claude-1000/`**(鉄則13)。リポジトリの中に作ってしまったものはコミット前に削除する。ただしリポジトリ直下は番人が `rm` を止めるので、**自分で消そうとせず報告する**(鉄則13)。
27
+ 9. **pnpm は取りに行かない・版を指定しない(ADR-015)** — ★`npx --yes pnpm@10 …` と打たない(毎回ダウンロードが走る)。`pnpm` とだけ打てば、そのリポジトリの `packageManager` の版が corepack で自動的に使われる。
28
+ - ★**必ずリポジトリの中で打つ。** 版の指定はディレクトリごとに効くので、リポジトリの外(ホーム等)から `--filter` で呼ぶと既定の新しい版が動き `ERR_PNPM_UNSUPPORTED_ENGINE` で弾かれる(実測)。
29
+ - `pnpm` が見つからない/版が合わないときは、回避せず**人の手番=`corepack enable`** と報告する。`npm i -g pnpm` は打たない(corepack と衝突する)。
30
+ - その現場の `package.json` に `packageManager` が無ければ、**勝手に足さず報告**する(どの版が正かは持ち主が決める)。
31
+ 10. **根治する** — 直しは対症療法でなく根本原因から(根治)。性能が落ちる設計を足さない。
32
+ 11. **報告は簡潔に** — 変更ファイル一覧・要点・チェック結果のみ。完了時は `conductor_complete` を1回呼ぶ。
33
+ 12. **外への通信は聞かれない。ただし秘密の持ち出しは番人が止める(えふさん決定 2026-09-03=甲)**
34
+ - **電話は自由。** 外への通信(npm / GitHub / API / ドキュメント取得など)は許可済みなので、`Network request outside of sandbox` は出ない。**通信のために止まらない・人を待たない**。最後まで自走して完了まで行く。
35
+ - **金庫は禁止。** 秘密の持ち出し(`curl -d @.env` / `cp .env` / `tar ~/.ssh` / `base64 .env` / `echo $DATABASE_URL` など)は**番人が今までどおり止める**。通信が自由になったこととは**別の仕組み**なので、「通信が許可されたから秘密も送れる」は成り立たない。本番DB・secret・`.env`・`hooks`・`.tyhld`・`.ssh` への書き込み禁止もそのまま。
36
+ - **それでも塞がれたら回避しない。** `curl: (56) CONNECT tunnel failed, response 403` のように通信が塞がれたときは、**意図的に閉じてある接続先**(例: 本番DBへの直結)か、設定がその機体にまだ届いていないかのどちらか。**別経路を探さず、そこで止めて報告する**(人の手番=`APPLY=1 bash scripts/update-guard.sh` → 職人を再起動)。
37
+ 13. **使い捨てファイルはリポジトリの中に作らない(えふさん決定 2026-09-03)**
38
+ - **実機検品・検証用の使い捨てファイル(HTML・画像・ログ等)は、リポジトリの中に作らない。`/tmp/claude-1000/` に置く。リポジトリ直下に作ってしまった場合は自分で消そうとせず、報告に「残骸あり・場所」と書く。**
39
+ - **なぜ。** 実機 2026-09-03: ai-saku の画面便(PR#258・PR#269)で、検品用の使い捨てページ `_layout-check.html` をリポジトリ直下に作り、消そうとして番人(`rm` on dangerous path)に止められ、人が2回 `rm` した。**番人の `rm` 制限は正しい(緩めない)。悪いのは置き場。**
40
+ - 置き場を `/tmp/claude-1000/` にすれば、番人に止められることも、残骸がコミットに混ざることも起きない(根治=鉄則10)。Playwright の検品ページも同じ(鉄則8-④)。
@@ -0,0 +1,63 @@
1
+ # conductor-ear.service — 采配くん「耳」(指示の着信合図の受け取り役)の systemd ユーザーサービス。
2
+ #
3
+ # ★このファイルが耳のユニットの【正本】です(写しをよそに置かないこと)。
4
+ # お客さま機(sales-template/setup.sh 経由)も自社機(scripts/systemd-install.sh 経由)も、
5
+ # 同じこのファイルを systemd/ear-install.sh が配置します。2箇所に写しを持つと中身がズレます。
6
+ #
7
+ # 位置づけ(ADR-066 便2c/2e):
8
+ # このPCに【1本だけ】動かす常駐です。中央から「指示が積まれた」合図(Supabase Realtime)を
9
+ # 1本の接続で受け取り、その現場の relay(conductor@<現場>)へ「今すぐ1周して」とだけ伝えます。
10
+ # 接続本数がソフト数ではなく【PC数】で決まる形にするための集約役です。
11
+ #
12
+ # ★耳は加速装置であって依存先ではありません。
13
+ # 耳を入れなくても・止めても・落ちても、各 relay は従来どおり15秒ポーリングで完全に動きます。
14
+ # 耳がしているのは「その15秒を待たずに前倒しさせる」ことだけです。
15
+ #
16
+ # conductor@<現場>.service との違い:
17
+ # - conductor@<現場> … 現場ごとに1本。heartbeat と relay(指示の配達・完了検出)。
18
+ # - conductor-ear … PCごとに1本。合図を受けて上記へ呼び鈴を送るだけ。指示は取らず、職人にも触りません。
19
+ #
20
+ # 導入:
21
+ # お客さま機 … setup.sh が自動で入れます(人の操作は不要)。
22
+ # 自社機 … scripts/systemd-install.sh が自動で入れます。
23
+ # 手動 … bash systemd/ear-install.sh
24
+ # ※ ExecStart のコマンド名は ear-install.sh が配置時に「その機で実際に動くフルパス」へ置換します
25
+ # (お客さま機: npm グローバルの conductor / 自社機: /usr/bin/env node <リポ>/dist/cli.js)。
26
+ # このファイルをそのままコピーしても動きません。必ず ear-install.sh 経由で入れてください。
27
+ #
28
+ # 撤去:
29
+ # systemctl --user disable --now conductor-ear
30
+ # (耳を消しても relay は15秒ポーリングでそのまま動きます。お客さま機の一括撤去は uninstall.sh)
31
+ #
32
+ # ★台帳と掃除の関係(便2c→2e で解消済み・順序を守ること):
33
+ # sales-template/setup.sh の cleanup_stale_units は「台帳に載っている `@` 無しの
34
+ # conductor-*.service」を古いユニットとみなして撤去します。conductor-ear.service は
35
+ # 【現行版が入れる現役のユニット】なので、同関数の CURRENT_MACHINE_UNITS(掃除の例外)に
36
+ # 入れてあります。例外に入れる前に台帳へ載せると、次回セットアップで耳が消えます。
37
+ # この順序は test/setup-ear-order.test.ts が固定しています(外すとテストが落ちます)。
38
+ #
39
+ # 鍵: %h/.conductor.env(chmod 600 / git管理外)を EnvironmentFile で読み込みます。
40
+ # 中身(例):
41
+ # CONDUCTOR_URL=https://<采配くん管制サーバURL>
42
+ # CONDUCTOR_TOKEN=<トークン>
43
+ # 接続先URL・公開鍵・購読設定は中央のAPIから受け取るので、ここに書き足す環境変数はありません。
44
+ #
45
+ # WorkingDirectory: 耳はどのフォルダの中身も見ないので %h(ホーム)で十分です。
46
+ # ★現場やリポジトリのパスを書かないこと。存在しない機で systemd が起動に失敗します。
47
+
48
+ [Unit]
49
+ Description=Conductor ear (指示の着信合図・このPCに1本)
50
+ After=network-online.target
51
+ Wants=network-online.target
52
+
53
+ [Service]
54
+ Type=simple
55
+ EnvironmentFile=%h/.conductor.env
56
+ WorkingDirectory=%h
57
+ # ↓のコマンド名は ear-install.sh が配置時に実際のフルパスへ置換する(配置後はここに実体が入る)。
58
+ ExecStart=__CONDUCTOR_BIN__ ear
59
+ Restart=always
60
+ RestartSec=10
61
+
62
+ [Install]
63
+ WantedBy=default.target
@@ -0,0 +1,62 @@
1
+ # conductor@.service — Conductor 報告役(heartbeat)+relay の systemd ユーザーサービス(テンプレート)。
2
+ #
3
+ # ★このファイルがユニットの【正本】です(写しをよそに置かないこと)。
4
+ # 以前は sales-template/systemd/ にもう1つ写しがあり、片方だけ直すと中身がズレる形でした。
5
+ # お客さま機(sales-template/systemd/install.sh)も自社機(scripts/systemd-install.sh)も、
6
+ # 同じこのファイルを読んで配置します。
7
+ #
8
+ # 位置づけ: 「PC起動時の完全自動常駐(第2段)」。落ちても Restart=always で自動復活します。
9
+ # 旧 scripts/autostart.sh(第1段)は手動予備として温存し、常用はこちらの systemd です。
10
+ #
11
+ # テンプレート方式:
12
+ # conductor@<現場名>.service で1サービス。%i に <現場名> が入ります。
13
+ # 例) systemctl --user enable --now conductor@kenzokun
14
+ #
15
+ # 導入は各インストーラが面倒を見ます(配置+enable+linger):
16
+ # お客さま機 … sales-template/setup.sh →(内部で)sales-template/systemd/install.sh
17
+ # 自社機 … scripts/systemd-install.sh
18
+ # ★ExecStart のコマンド名はインストーラが配置時に「その機で実際に動くフルパス」へ置換します
19
+ # (お客さま機: npm グローバルの conductor / 自社機: /usr/bin/env node <リポ>/dist/cli.js)。
20
+ # このファイルをそのままコピーしても動きません。必ずインストーラ経由で入れてください。
21
+ #
22
+ # 鍵: %h/.conductor.env(chmod 600 / git管理外)を EnvironmentFile で読み込みます。
23
+ # 中身(例):
24
+ # CONDUCTOR_URL=https://devlog-tracker-seven.vercel.app
25
+ # CONDUCTOR_TOKEN=(Bitwarden の値)
26
+ #
27
+ # WorkingDirectory: 報告役はカレントの git からブランチを判定するため、現場ディレクトリで起動します。
28
+ # 既定は %h/projects/%i(現状は「現場名 = ディレクトリ名」で一致)。
29
+ # 将来、現場名とディレクトリ名が食い違う場合は scripts/systemd-install.sh が
30
+ # sites.sh の対応表を見て drop-in(conductor@<現場>.service.d/override.conf)で
31
+ # WorkingDirectory を上書きします。本体テンプレートは触りません。
32
+ #
33
+ # ExecStart のパス解決: systemd ユーザーサービスはシェルの PATH を継承しません。
34
+ # そのためインストーラが「実際に動くコマンドのフルパス」を検出し、ExecStart のコマンド名を
35
+ # 置換してから配置します。お客さま機では node 自体も /usr/local/bin/node へリンクして解決します。
36
+ #
37
+ # relay: このユニットで有効化します(heartbeat + 指示の自動流し込み)。
38
+ # [Service] の Environment=CONDUCTOR_RELAY_ENABLED=true で ON。間隔は既定15秒。
39
+ # 送信前ガード(src/relay.ts)が「現場名と一致する tmux セッションが存在し、
40
+ # Claude Code がアイドル(❯)で待機している時だけ送る/それ以外はスキップ」を担保します。
41
+ # ※ このテンプレートを書き換えた後、本番へ反映するには再インストール+
42
+ # `systemctl --user daemon-reload` + 各 conductor@<現場> の restart が必要です。
43
+
44
+ [Unit]
45
+ Description=Conductor heartbeat (%i)
46
+ After=network-online.target
47
+ Wants=network-online.target
48
+
49
+ [Service]
50
+ Type=simple
51
+ EnvironmentFile=%h/.conductor.env
52
+ # relay(指示の自動流し込み)を常駐で有効化。送信前ガードで誤爆を防止する。
53
+ Environment=CONDUCTOR_RELAY_ENABLED=true
54
+ Environment=CONDUCTOR_RELAY_INTERVAL_SEC=15
55
+ WorkingDirectory=%h/projects/%i
56
+ # ↓のコマンド名はインストーラが配置時に実際のフルパスへ置換する(配置後はここに実体が入る)。
57
+ ExecStart=__CONDUCTOR_BIN__ start -s %i -t working -a "%i常駐"
58
+ Restart=always
59
+ RestartSec=5
60
+
61
+ [Install]
62
+ WantedBy=default.target
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # ear-install.sh — 采配くん「耳」(conductor-ear) を1台に1本だけ設置・有効化する(ADR-066 便2e)。
4
+ #
5
+ # ★これが耳の導入処理の【正本】です。お客さま機(sales-template/setup.sh)も
6
+ # 自社機(scripts/systemd-install.sh)も、写しを持たずこのスクリプトを呼びます。
7
+ #
8
+ # やること(すべて冪等・何度実行しても安全):
9
+ # 1. 耳を動かすコマンドを決める(引数 → npm の conductor → 同じ配布物の dist/cli.js)。
10
+ # 2. 正本ユニット systemd/conductor-ear.service の __CONDUCTOR_BIN__ を実際のコマンドへ置換して配置。
11
+ # 3. systemctl --user daemon-reload → enable --now conductor-ear。
12
+ # 4. 成功したときだけ撤去用の台帳へ登録する(入れていないものを撤去対象にしない)。
13
+ # 5. linger(ログオフ中も動くように。既に有効なら無害)。
14
+ #
15
+ # 使い方:
16
+ # bash systemd/ear-install.sh # コマンドは自動で判断する
17
+ # bash systemd/ear-install.sh "/usr/bin/env node /home/me/projects/conductor/dist/cli.js"
18
+ #
19
+ # ★失敗してもセットアップを止めない(作法・ADR-066):
20
+ # 耳は加速装置であって依存先ではありません。入らない・起動しない機でも、各現場の relay は
21
+ # 15秒ポーリングで従来どおり完全に動きます。だからこのスクリプトは、どの失敗でも
22
+ # 「警告を出して exit 0」で終わります(呼び出し元のセットアップを失敗にしない)。
23
+ #
24
+ set -euo pipefail
25
+
26
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
+ UNIT_NAME="conductor-ear.service"
28
+ UNIT_SRC="$SCRIPT_DIR/$UNIT_NAME"
29
+ UNIT_DIR="$HOME/.config/systemd/user"
30
+ UNIT_DST="$UNIT_DIR/$UNIT_NAME"
31
+
32
+ # 撤去用の台帳(conductor@ の導入処理・paste-cache-prune と同じ場所・同じ方式)。
33
+ LEDGER_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/conductor"
34
+ LEDGER_FILE="$LEDGER_DIR/installed-units.txt"
35
+
36
+ info() { echo "[ear-install] $*"; }
37
+ warn() { echo "[ear-install] 注意: $*" >&2; }
38
+
39
+ # ★耳が入らないときの共通の締め方。必ず 0 で終わる(セットアップを失敗にしない)。
40
+ skip() {
41
+ warn "$1"
42
+ warn " 耳は入りませんでしたが、各現場の relay は15秒ごとの確認でこれまでどおり動きます。"
43
+ exit 0
44
+ }
45
+
46
+ ledger_add() {
47
+ local unit="$1"
48
+ mkdir -p "$LEDGER_DIR"
49
+ touch "$LEDGER_FILE"
50
+ grep -qxF "$unit" "$LEDGER_FILE" || echo "$unit" >> "$LEDGER_FILE"
51
+ }
52
+
53
+ # --- 1) 正本ユニットの存在確認 ------------------------------------------------
54
+ [[ -f "$UNIT_SRC" ]] || skip "ユニットの正本が見つかりません: $UNIT_SRC"
55
+
56
+ # --- 2) systemd --user が使えるか --------------------------------------------
57
+ if ! command -v systemctl >/dev/null 2>&1 || ! systemctl --user show-environment >/dev/null 2>&1; then
58
+ skip "systemd --user が使えないため、耳は設置しません。"
59
+ fi
60
+
61
+ # --- 3) 耳を動かすコマンドを決める -------------------------------------------
62
+ # ★systemd ユーザーサービスはシェルの PATH を継承しないので、ここでフルパスに解決して
63
+ # ユニットへ焼き込む(conductor@ の導入処理と同じ考え方)。
64
+ EAR_BIN="${1:-}"
65
+ EAR_SRC="引数で指定"
66
+
67
+ # ★どちらを使うかの順番(2026-09-04 に直した見落とし)
68
+ # ① 開発機(このスクリプトの隣に dist/cli.js が在る)… そのリポジトリの中身を使う
69
+ # ② お客さま機(無い) … npm で入れた全体版を使う
70
+ #
71
+ # 【なぜ①を先に見るか】以前は npm の全体版を先に掴んでいた。開発機で
72
+ # `npm i -g @tyhld/conductor` を一度でも打つと、耳がリポジトリではなく
73
+ # 【配布済みの古い版】で動いてしまう。直したはずの不具合が現場で再現し続け、
74
+ # 原因を「直っていない」と読み違える。リポジトリが在る機体では
75
+ # リポジトリを正とする(conductor@ の導入処理と同じ順番に揃えてある)。
76
+ if [[ -z "$EAR_BIN" ]]; then
77
+ # 配布物・リポジトリのどちらでも、このスクリプトから見て ../dist/cli.js に本体がある。
78
+ CLI_JS="$(cd "$SCRIPT_DIR/.." && pwd)/dist/cli.js"
79
+ if [[ -f "$CLI_JS" ]]; then
80
+ NODE_BIN="$(command -v node || true)"
81
+ [[ -n "$NODE_BIN" && -L "$NODE_BIN" ]] && NODE_BIN="$(readlink -f "$NODE_BIN")"
82
+ [[ -n "$NODE_BIN" ]] || NODE_BIN="/usr/bin/env node"
83
+ EAR_BIN="$NODE_BIN $CLI_JS"
84
+ EAR_SRC="この機体のリポジトリ(開発機)"
85
+ fi
86
+ fi
87
+
88
+ if [[ -z "$EAR_BIN" ]] && command -v conductor >/dev/null 2>&1; then
89
+ # お客さま機: npm グローバルの conductor(実体パスまで解決する)。
90
+ resolved="$(command -v conductor)"
91
+ [[ -L "$resolved" ]] && resolved="$(readlink -f "$resolved")"
92
+ EAR_BIN="$resolved"
93
+ EAR_SRC="npm で入れた全体版"
94
+ fi
95
+
96
+ [[ -n "$EAR_BIN" ]] || skip "耳を動かすコマンドが見つかりません(conductor も dist/cli.js も不在)。"
97
+
98
+ # ★どちらを使ったかを必ず1行出す(読み違えを防ぐ)。
99
+ info "耳が使う采配くん: ${EAR_SRC}"
100
+ info "耳の起動コマンド: $EAR_BIN ear"
101
+
102
+ # --- 4) ユニットを配置(__CONDUCTOR_BIN__ を実際のコマンドへ置換)-------------
103
+ mkdir -p "$UNIT_DIR"
104
+ if ! sed "s|__CONDUCTOR_BIN__|${EAR_BIN}|g" "$UNIT_SRC" > "$UNIT_DST.tmp"; then
105
+ rm -f "$UNIT_DST.tmp"
106
+ skip "ユニットの生成に失敗しました。"
107
+ fi
108
+ mv -f "$UNIT_DST.tmp" "$UNIT_DST" # 置換は原子的=壊れかけのユニットを残さない
109
+ info "配置: $UNIT_DST"
110
+
111
+ # --- 5) 反映して起動 ----------------------------------------------------------
112
+ systemctl --user daemon-reload || skip "daemon-reload に失敗しました。"
113
+
114
+ if systemctl --user enable --now "$UNIT_NAME"; then
115
+ info "有効化: $UNIT_NAME(このPCに1本・合図を受けて各現場の relay を前倒しします)"
116
+ # ★台帳へ載せるのは enable に成功したときだけ。
117
+ # (setup.sh の cleanup_stale_units には conductor-ear.service を掃除の例外として
118
+ # 先に入れてある。例外に入れる前に台帳へ載せると次回セットアップで消される)
119
+ ledger_add "$UNIT_NAME"
120
+ else
121
+ skip "enable --now に失敗しました: $UNIT_NAME"
122
+ fi
123
+
124
+ # ログオフ中も動くように(他用途で既に有効なことも多い。失敗は握りつぶす)。
125
+ loginctl enable-linger "$USER" >/dev/null 2>&1 || true
126
+
127
+ echo
128
+ echo "ログ確認: journalctl --user -u conductor-ear -f"
129
+ echo "停止: systemctl --user disable --now conductor-ear"
130
+ systemctl --user --no-pager status "$UNIT_NAME" 2>/dev/null || true
131
+ exit 0
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # guard-sync-install.sh — 番人(安全装置)のコードと配線を1時間ごとに正本へ揃える
4
+ # systemd --user タイマーを設置・有効化する(ADR-012)。
5
+ #
6
+ # 【なぜ要るか】
7
+ # 番人を直してリポジトリへ入れても、人が `APPLY=1 bash scripts/update-guard.sh` を流すまで
8
+ # 機体には1文字も届かない。実地で2度続けて届かず、判定の無い古い写しが確認カードを出し続けた
9
+ # (2026-09-02 03:37 / 11:04)。人が忘れる前提にして、機械に追随させる。
10
+ #
11
+ # やること(すべて冪等):
12
+ # 1. tyhld-guard-sync.service / .timer を ~/.config/systemd/user/ へ配置(@REPO@ を実機の
13
+ # リポジトリの場所へ置き換える)。
14
+ # 2. systemctl --user daemon-reload。
15
+ # 3. systemctl --user enable --now tyhld-guard-sync.timer。
16
+ # 4. loginctl enable-linger(ログオフ中も回るように。既に有効なら無害)。
17
+ # 5. 撤去用の台帳(installed-units.txt)へ登録(uninstall.sh がこれを読んで消す)。
18
+ #
19
+ # 安全:
20
+ # - サービスが呼ぶのは update-guard.sh の【同期モード】だけ。権限(allow/ask/deny)・
21
+ # paste-cache の掃除・鉄則スキルには触れない。
22
+ # - 正本(scripts/hooks)に未コミットの変更があるときは、同期モード側が何もせず終わる
23
+ # (作業中の壊れた番人を全現場へ配らない)。
24
+ # - systemd --user が使えない環境では警告して素通り(呼び出し元を止めない)。
25
+ #
26
+ # 使い方:
27
+ # bash systemd/guard-sync-install.sh
28
+ #
29
+ set -euo pipefail
30
+
31
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
+ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
33
+ UNIT_DIR="$HOME/.config/systemd/user"
34
+ SERVICE="tyhld-guard-sync.service"
35
+ TIMER="tyhld-guard-sync.timer"
36
+
37
+ # 撤去用の台帳(install.sh / paste-cache-prune-install.sh と同じ場所・同じ方式)。
38
+ LEDGER_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/conductor"
39
+ LEDGER_FILE="$LEDGER_DIR/installed-units.txt"
40
+
41
+ info() { echo "[guard-sync] $*"; }
42
+ warn() { echo "[guard-sync] 注意: $*" >&2; }
43
+
44
+ ledger_add() {
45
+ local unit="$1"
46
+ mkdir -p "$LEDGER_DIR"
47
+ touch "$LEDGER_FILE"
48
+ grep -qxF "$unit" "$LEDGER_FILE" || echo "$unit" >> "$LEDGER_FILE"
49
+ }
50
+
51
+ # ユニット正本がそろっているか(無ければ素通り)。
52
+ for u in "$SERVICE" "$TIMER"; do
53
+ if [[ ! -f "$SCRIPT_DIR/$u" ]]; then
54
+ warn "ユニットが見つかりません: $SCRIPT_DIR/$u(自動同期タイマーはスキップ)"
55
+ exit 0
56
+ fi
57
+ done
58
+
59
+ # 同期の本体(update-guard.sh)が在ることを確かめる。無ければ設置しない。
60
+ if [[ ! -f "$REPO_ROOT/scripts/update-guard.sh" ]]; then
61
+ warn "同期の本体が見つかりません: $REPO_ROOT/scripts/update-guard.sh(スキップ)"
62
+ exit 0
63
+ fi
64
+
65
+ # systemd --user が使えないなら素通り(呼び出し元を止めない)。
66
+ if ! command -v systemctl >/dev/null 2>&1 || ! systemctl --user show-environment >/dev/null 2>&1; then
67
+ warn "systemd --user が使えないため、自動同期タイマーは設置しません。"
68
+ warn " (その機体では人が APPLY=1 bash scripts/update-guard.sh を流してください)"
69
+ exit 0
70
+ fi
71
+
72
+ mkdir -p "$UNIT_DIR"
73
+ # ★リポジトリの場所は機体ごとに違うので、ここで実機の値へ置き換える。
74
+ # 置換に使う区切りは | (パスに / が含まれるため)。
75
+ sed "s|@REPO@|$REPO_ROOT|g" "$SCRIPT_DIR/$SERVICE" > "$UNIT_DIR/$SERVICE"
76
+ cp -f "$SCRIPT_DIR/$TIMER" "$UNIT_DIR/$TIMER"
77
+ info "配置: $UNIT_DIR/$SERVICE, $UNIT_DIR/$TIMER"
78
+ info " 同期の本体: $REPO_ROOT/scripts/update-guard.sh(SYNC_ONLY=1 APPLY=1)"
79
+
80
+ systemctl --user daemon-reload
81
+
82
+ if systemctl --user enable --now "$TIMER"; then
83
+ info "有効化: $TIMER(1時間ごとに番人のコードと配線だけを正本へ揃えます)"
84
+ ledger_add "$SERVICE"
85
+ ledger_add "$TIMER"
86
+ else
87
+ warn "enable --now に失敗しました: $TIMER"
88
+ exit 0
89
+ fi
90
+
91
+ # ログオフ中も回るように(他用途で既に有効なことも多い。失敗は握りつぶす)。
92
+ loginctl enable-linger "$USER" >/dev/null 2>&1 || true
93
+
94
+ systemctl --user --no-pager list-timers "$TIMER" 2>/dev/null || true
@@ -0,0 +1,28 @@
1
+ # tyhld-guard-sync.service — 番人(安全装置)のコードと配線を正本へ揃える(1回実行)。
2
+ #
3
+ # 【なぜ要るか(ADR-012)】
4
+ # 番人を直してリポジトリへ入れても、人が `APPLY=1 bash scripts/update-guard.sh` を流すまで
5
+ # 機体には1文字も届かない。実地で2度続けて届いていなかった:
6
+ # ・2026-09-02 03:37(PR #568 の完了報告)で確認カードが誤発火
7
+ # ・2026-09-02 11:04(PR #573 の完了報告)でも同じ形が再発(直しは 03:56 にマージ済み)
8
+ # どちらもホームの ~/.claude/settings.json が古い写し($HOME/.tyhld/hookset)を指したままで、
9
+ # その写しには「質問かどうかの判定」そのものが無い=どんな報告でもカードを出していた。
10
+ # =人が流し忘れると、直しが何時間も届かない。だから自動で追随させる。
11
+ #
12
+ # 【何をするか】update-guard.sh を SYNC_ONLY=1 APPLY=1 で1回流すだけ。
13
+ # ・番人一式を $HOME/.tyhld/hooks へ設置(構文確認つき)
14
+ # ・ホームの ~/.claude/settings.json の hooks を固定パスへ向け直す
15
+ # ★権限(allow/ask/deny)・paste-cache の掃除・鉄則スキルには一切触れない。
16
+ # ★正本(scripts/hooks)に未コミットの変更があるときは何もしない(作業中の番人を配らない)。
17
+ #
18
+ # .timer から1時間ごとに呼ばれる(tyhld-guard-sync.timer)。
19
+ # ExecStart のリポジトリの場所は guard-sync-install.sh が実機に合わせて書き込む。
20
+
21
+ [Unit]
22
+ Description=tyhld: keep the guard (hooks) in sync with the repository
23
+
24
+ [Service]
25
+ Type=oneshot
26
+ Environment=SYNC_ONLY=1
27
+ Environment=APPLY=1
28
+ ExecStart=/bin/bash @REPO@/scripts/update-guard.sh
@@ -0,0 +1,25 @@
1
+ # tyhld-guard-sync.timer — 上の同期サービスを1時間ごとに起動する。
2
+ #
3
+ # 【なぜ「毎日」ではなく「1時間ごと」か】
4
+ # 実地の事故は「03:56 にマージした直しが 11:04 の時点でまだ届いていなかった」=7時間の穴だった。
5
+ # 毎日1回では同じ穴が開いたままになる。1時間なら、直してから効くまでが最長1時間で収まる。
6
+ # 処理は小さなファイルのコピーと JSON の読み書きだけなので、1時間ごとでも負担にならない。
7
+ #
8
+ # - OnBootSec=2min … 起動直後に1回(PCを立ち上げた朝いちばんに追いつく)。
9
+ # - OnUnitActiveSec=1h … 以後1時間ごと。
10
+ # - Persistent=true … PCが消えていて取りこぼした分は、次回起動直後に1度だけ追い実行。
11
+ # - RandomizedDelaySec … 起動直後に集中しないよう少しばらす。
12
+ #
13
+ # enable --now は update-guard.sh(APPLY時・①-f)が行う(冪等)。
14
+
15
+ [Unit]
16
+ Description=tyhld: hourly guard (hooks) sync
17
+
18
+ [Timer]
19
+ OnBootSec=2min
20
+ OnUnitActiveSec=1h
21
+ Persistent=true
22
+ RandomizedDelaySec=2min
23
+
24
+ [Install]
25
+ WantedBy=timers.target