@tyhld/conductor 0.12.11 → 0.12.12

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.11",
3
+ "version": "0.12.12",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,7 @@
21
21
  "!scripts/hooks/__pycache__",
22
22
  "scripts/update-guard.sh",
23
23
  "scripts/dist-source.sh",
24
+ "scripts/conductor-run.sh",
24
25
  "scripts/codex",
25
26
  "!scripts/codex/__pycache__",
26
27
  "skills",
@@ -105,51 +105,45 @@ if ! PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" command
105
105
  fi
106
106
  echo "[install] systemd PATH での node 解決: OK"
107
107
 
108
- # --- 2) conductor コマンドのパス解決 -----------------------------------------
109
- # npm グローバルインストール先は環境依存(~/.npm-global/bin, /usr/local/bin,
110
- # ~/.volta/bin 等)で systemd PATH にない可能性がある。
111
- # ユニット配置時に ExecStart にフルパスを書き込んで根治する。
112
- #
113
- # ★どちらを使うかの順番(2026-09-04 に直した見落とし)
114
- # ① 開発機(~/projects/conductor/dist/cli.js が在る)… そのリポジトリの中身を使う
115
- # ② お客さま機(無い) … npm で入れた全体版を使う
116
- #
117
- # 【なぜ①を先に見るか】以前は npm の全体版を先に掴んでいた。開発機で
118
- # `npm i -g @tyhld/conductor` を一度でも打つと、常駐がリポジトリではなく
119
- # 【配布済みの古い版】で動いてしまう。直したはずの不具合が現場で再現し続け、
120
- # 原因を「直っていない」と読み違える(=いちばん質の悪い事故)。
121
- # リポジトリが在る機体=開発機なので、そこではリポジトリを正とする。
122
- REPO_CLI="$HOME/projects/conductor/dist/cli.js"
123
- CONDUCTOR_BIN=""
124
- CONDUCTOR_SRC=""
125
-
126
- if [[ -f "$REPO_CLI" ]]; then
127
- # 開発機。node のフルパスを頭に付ける(systemd はシェルの PATH を継承しないため)。
128
- NODE_BIN="$(command -v node || true)"
129
- [[ -n "$NODE_BIN" && -L "$NODE_BIN" ]] && NODE_BIN="$(readlink -f "$NODE_BIN")"
130
- [[ -n "$NODE_BIN" ]] || NODE_BIN="/usr/bin/env node"
131
- CONDUCTOR_BIN="$NODE_BIN $REPO_CLI"
132
- CONDUCTOR_SRC="この機体のリポジトリ(開発機)"
133
- elif command -v conductor >/dev/null 2>&1; then
134
- CONDUCTOR_BIN="$(command -v conductor)"
135
- [[ -L "$CONDUCTOR_BIN" ]] && CONDUCTOR_BIN="$(readlink -f "$CONDUCTOR_BIN")"
136
- CONDUCTOR_SRC="npm で入れた全体版"
108
+ # --- 2) 常駐を動かすコマンドを決める -----------------------------------------
109
+ # ★2026-09-16(えふさん確定・便 81a08d58)に決め方を変えた。
110
+ # 【旧】開発機(~/projects/conductor/dist/cli.js が在る)では、そのリポジトリの中身を使う。
111
+ # =★職人の作業ツリーの build が常駐として動いていた。職人がブランチを切り替えて build した
112
+ # 瞬間、配達・完了検出・生存報告が作りかけで動く。
113
+ # さらに node の置き場(nvm の版つき)を ExecStart に焼き込んでいたので、node の版を変えると
114
+ # 203/EXEC で再起動を繰り返す形だった(2026-09-04 に耳で実際に起きた)。
115
+ # 【新】開発機でもお客さま機でも「入っている配布物」だけを動かす。ExecStart には動かない入口
116
+ # (/bin/bash ~/.tyhld/bin/conductor-run)だけを書き、配布物と node は起動のたびに探す。
117
+ # ★作業ツリーは選ばない。作業ツリーを試すときは、サービスを使わず端末で直接動かす
118
+ # (端末を閉じれば終わる=入れっぱなしの上書きが残らない)。
119
+ . "$SCRIPT_DIR/../../scripts/dist-source.sh"
120
+
121
+ # 入口の一式を写す元=公開済みの正規版(作業ツリーからは写さない・便 4061dd71 と同じ判定)。
122
+ if ! dist_source_for_setup "$(cd "$SCRIPT_DIR/../.." && pwd)"; then
123
+ fail "常駐を動かす采配くんを確かめられません(${DIST_REASON})。先に npm i -g @tyhld/conductor --@tyhld:registry=https://registry.npmjs.org を実行してください。"
137
124
  fi
125
+ BUNDLE_SRC="$DIST_ROOT"
126
+ # ★ユニットの正本も配り元から取る(作業ツリーの写しを配らない)。
127
+ [[ -f "$BUNDLE_SRC/systemd/conductor@.service" ]] && UNIT_SRC="$BUNDLE_SRC/systemd/conductor@.service"
138
128
 
139
- if [[ -z "$CONDUCTOR_BIN" ]]; then
140
- fail "conductor コマンドが見つかりません。先に npm i -g @tyhld/conductor を実行してください。"
129
+ if ! dist_runtime_bundle_install "$BUNDLE_SRC"; then
130
+ fail "常駐の入口を置けませんでした: $DIST_RUNTIME_BIN_DIR"
141
131
  fi
142
-
143
- # ★どちらを使ったかを必ず1行出す(読み違えを防ぐ)。
144
- echo "[install] 常駐が使う采配くん: ${CONDUCTOR_SRC}"
145
- echo "[install] 検出した conductor: $CONDUCTOR_BIN"
146
-
147
- # conductor が実際に動くか簡易検証
148
- # ★開発機では CONDUCTOR_BIN が「node と cli.js の2語」になるので、語に割って呼ぶ。
149
- # まとめて1語として呼ぶと、そんな名前のコマンドは無い=必ず失敗する。
150
- read -r -a CONDUCTOR_CMD <<< "$CONDUCTOR_BIN"
151
- if ! "${CONDUCTOR_CMD[@]}" --help >/dev/null 2>&1; then
152
- warn "conductor コマンドの実行に失敗しました。インストールを確認してください。"
132
+ # 導入処理が確かめた置き場を控えに残す(systemd の中ではまずこれを見る)。
133
+ dist_runtime_hint_write "$BUNDLE_SRC"
134
+
135
+ CONDUCTOR_BIN="$DIST_RUNTIME_EXEC"
136
+
137
+ # ★どれを使うかを必ず1行出す(読み違えを防ぐ)。
138
+ if dist_runtime_resolve; then
139
+ echo "[install] 常駐が使う采配くん: 入っている配布物(起動のたびに探す)"
140
+ echo "[install] いま見つかる配布物: $DIST_ROOT(版 ${DIST_VERSION}・node ${DIST_NODE})"
141
+ # 実際に動くか簡易検証(警告だけ・止めない)。
142
+ if ! "$DIST_NODE" "$DIST_ROOT/dist/cli.js" --help >/dev/null 2>&1; then
143
+ warn "conductor の実行に失敗しました。インストールを確認してください。"
144
+ fi
145
+ else
146
+ fail "常駐を動かす采配くんを確かめられません(${DIST_REASON})。先に npm i -g @tyhld/conductor --@tyhld:registry=https://registry.npmjs.org を実行してください。"
153
147
  fi
154
148
 
155
149
  # --- 3) ユニットファイルを配置(__CONDUCTOR_BIN__ をフルパスに置換) ----------
@@ -177,6 +171,20 @@ echo "[install] 配置: $UNIT_DST (ExecStart=$CONDUCTOR_BIN)"
177
171
  # テンプレート本体も台帳に載せる(撤去時にファイルを消す対象)。
178
172
  ledger_add "conductor@.service"
179
173
 
174
+ # ★ExecStart を上書きしている drop-in を退ける(便 81a08d58)。
175
+ # テンプレート全体の上書き(conductor@.service.d)と、この現場だけの上書きの両方を見る。
176
+ # 残すと、上で書いた ExecStart より後から効いて、作業ツリーを動かし続ける。
177
+ for d in "$UNIT_DIR/conductor@.service.d" "$UNIT_DIR/conductor@${SITE}.service.d"; do
178
+ if ! retired="$(dist_runtime_retire_exec_dropins "$d")"; then
179
+ fail "起動方法を上書きしている drop-in を退けられませんでした: $d"
180
+ fi
181
+ if [[ -n "$retired" ]]; then
182
+ while IFS= read -r line; do
183
+ echo "[install] 起動方法を上書きしていた drop-in を控えへ退けました: $line"
184
+ done <<< "$retired"
185
+ fi
186
+ done
187
+
180
188
  # --- 4) WorkingDirectory の解決 -----------------------------------------------
181
189
  DEFAULT_DIR="$HOME/projects/$SITE"
182
190
  DROPIN_DIR="$UNIT_DIR/conductor@${SITE}.service.d"
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # conductor-run — 常駐(conductor@*.service)と耳(conductor-ear.service)の入口(★便 81a08d58)。
4
+ #
5
+ # 【なぜ要るか(実測 2026-09-16・開発履歴 4061dd71 の発見5)】
6
+ # この機(開発機)の常駐と耳は、職人の【作業ツリー】の build を動かしていた。
7
+ # 常駐 … conductor@.service の ExecStart が ~/projects/conductor/dist/cli.js
8
+ # 耳 … 配布物を指す元の定義に、作業ツリーを指す上書き(drop-in)が掛かっていた
9
+ # 職人がブランチを切り替えて build した瞬間、配達・完了検出・生存報告が作りかけで動く。
10
+ # さらにどちらも node の置き場(nvm の v24.15.0)を ExecStart に焼き込んでいたので、
11
+ # node の版を変えると 203/EXEC で再起動を繰り返す(2026-09-04 に耳で実際に起きた)。
12
+ #
13
+ # 【えふさん確定(2026-09-16)】「常駐・耳の配布物化 … 推しどおりで承認です」
14
+ # (推し=配布物を指すように揃える。全現場へ配る話ではないので、影響はこの機の中だけ)
15
+ #
16
+ # 【どう動くか】
17
+ # systemd は ExecStart に「/bin/bash <この入口>」だけを持つ(/bin/bash は動かない場所)。
18
+ # この入口が【起動のたびに】入っている配布物とその node を探し、そこへ exec する。
19
+ # =node の版を変えても、次の起動で新しい置き場を見つける。
20
+ # 探し方は毎時同期(update-guard.sh)と同じ dist-source.sh を使う=同じ機で別の版を選ばない。
21
+ #
22
+ # 【★作業ツリーは動かさない】探し方のどの道でも、git の作業場所は選ばない。
23
+ #
24
+ # 【見つからないときは黙って動かさない】
25
+ # 配布物・node・本体(dist/cli.js)のどれかが確かめられなければ【起動しない】。
26
+ # ① 画面(journal)に理由と人の手番を出す
27
+ # ② 控え(~/.tyhld/hooks/runtime-source.last)に失敗を残す
28
+ # ③ 管制へ知らせる(便 29612ff1・4061dd71 と同じ確認カードの口)
29
+ # systemd は Restart=always なので、入れ直せば次の起動で自動的に戻る。
30
+ #
31
+ # 【★公開 npm には問い合わせない】常駐は回線が切れても動き続ける必要がある。
32
+ # 入っている版が古いかどうかは、毎時同期が見て知らせる(同じ見張りを2つ作らない)。
33
+ #
34
+ # 【置き場】この入口は ~/.tyhld/bin/conductor-run として置かれ、隣に dist-source.sh と
35
+ # skill_sync_notify.py を置く(導入処理と毎時同期が配布物から写す)。
36
+ # ~/.tyhld は職人が書けない=入口を後から書き換えられない。
37
+ set -u
38
+
39
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
40
+ . "$HERE/dist-source.sh"
41
+
42
+ RUNTIME_STATE_DIR="${HOME:-}/.tyhld/hooks"
43
+ RUNTIME_STATE="$RUNTIME_STATE_DIR/runtime-source.state" # 最後に【動かせた】配布物
44
+ RUNTIME_LAST="$RUNTIME_STATE_DIR/runtime-source.last" # 直近の【試み】の結果
45
+
46
+ runtime_record() {
47
+ local status="$1" reason="$2" line
48
+ line="$(date '+%Y-%m-%dT%H:%M:%S%z') status=${status} reason=${reason} what=runtime-source version=${DIST_VERSION:-unknown} method=${DIST_METHOD:-none} root=${DIST_ROOT:-none} node=${DIST_NODE:-none} role=${ROLE:-unknown}"
49
+ mkdir -p "$RUNTIME_STATE_DIR" 2>/dev/null || return 0
50
+ printf '%s\n' "$line" > "$RUNTIME_LAST" 2>/dev/null || true
51
+ if [[ "$status" == "ok" ]]; then
52
+ printf '%s\n' "$line" > "$RUNTIME_STATE" 2>/dev/null || true
53
+ fi
54
+ }
55
+
56
+ # 役(start / ear)を記録に載せる(どのサービスが止まったかを外から分かるように)。
57
+ ROLE="${1:-unknown}"
58
+
59
+ if ! dist_runtime_resolve; then
60
+ echo "[conductor-run] 起動しません: 動かす采配くんを確かめられません(${DIST_REASON})。" >&2
61
+ case "$DIST_REASON" in
62
+ not-installed) echo "[conductor-run] この機に @tyhld/conductor が入っていません。" >&2 ;;
63
+ worktree-refused) echo "[conductor-run] ★作業ツリーは動かしません(配布物だけを動かします)。" >&2 ;;
64
+ incomplete) echo "[conductor-run] 入っている配布物の中身が欠けています。" >&2 ;;
65
+ no-version) echo "[conductor-run] 入っている配布物の版が読めません。" >&2 ;;
66
+ no-node) echo "[conductor-run] 配布物を動かす node が見つかりません。" >&2 ;;
67
+ esac
68
+ echo "[conductor-run] 人の手番: npm install -g @tyhld/conductor --@tyhld:registry=https://registry.npmjs.org" >&2
69
+ runtime_record failed "$DIST_REASON"
70
+ if [[ -f "$HERE/skill_sync_notify.py" ]]; then
71
+ PYTHONPATH="$RUNTIME_STATE_DIR${PYTHONPATH:+:$PYTHONPATH}" \
72
+ python3 "$HERE/skill_sync_notify.py" "$RUNTIME_LAST" >/dev/null 2>&1 || true
73
+ fi
74
+ # 78 = 設定の不備(EX_CONFIG)。systemd の記録で他の失敗と見分けられるようにする。
75
+ exit 78
76
+ fi
77
+
78
+ echo "[conductor-run] 配布物で動かします: ${DIST_ROOT}(版 ${DIST_VERSION}・node ${DIST_NODE}・見つけ方 ${DIST_METHOD})" >&2
79
+ runtime_record ok started
80
+ exec "$DIST_NODE" "$DIST_ROOT/dist/cli.js" "$@"
@@ -49,13 +49,62 @@ DIST_VERSION='' # そこに入っている版
49
49
  DIST_PUBLISHED='' # 公開 npm のいまの版
50
50
  DIST_REASON='' # 配れないときの理由(1語・記録にそのまま載る)
51
51
 
52
- # この機に入っている配布物の置き場を探す。見つからなければ空。
53
- # ★node の版が変わると /home/…/.nvm/versions/node/<版>/… の <版> が変わる。
54
- # だから固定のパスを覚えない。いま PATH に居る conductor から辿る(版が変わっても付いていく)。
55
- dist_pkg_root() {
52
+ #: どの道で置き場を見つけたか(override / path / hint / scan / npm)。記録と画面に出す。
53
+ DIST_METHOD=''
54
+ #: dist_pkg_find が見つけた置き場(まだ確かめる前の候補)。
55
+ DIST_FOUND=''
56
+
57
+ # ★人が打った導入処理で見つけた置き場の控え(便 81a08d58)。
58
+ # systemd の中(常駐・耳・毎時同期)は PATH に nvm の node も conductor も居ない。
59
+ # そこでまずこの控えを見る。控えは ~/.tyhld の下=職人は書き換えられない。
60
+ DIST_RUNTIME_HINT="${DIST_RUNTIME_HINT:-${HOME:-}/.tyhld/bin/runtime-root}"
61
+
62
+ # 置き場として使ってよいか(配布物の形で、git の作業場所ではない)。
63
+ dist_pkg_usable() {
64
+ local root="$1"
65
+ [[ -n "$root" && -f "$root/package.json" ]] || return 1
66
+ # ★作業ツリーの根には .git が在る。npm で入れた配布物には無い。
67
+ [[ -e "$root/.git" ]] && return 1
68
+ return 0
69
+ }
70
+
71
+ # 決め打ちの置き場の候補を、新しい node から順に並べる。★PATH に頼らない(便 81a08d58)。
72
+ # 【なぜ要るか(実測 2026-09-16)】systemd の中の PATH には nvm の node が無い。
73
+ # いま毎時同期が配布物を見つけられているのは、/usr/local/bin/node が
74
+ # nvm の v24.15.0 を指す近道になっていて、それを通して npm が置き場を答えているから。
75
+ # ★node の版を変えるとこの近道が古いまま残り、見つけられなくなる(2026-09-04 の 203/EXEC と同じ形)。
76
+ # 【どう決めるか】nvm の置き場を直接見て、配布物が入っている node の版を新しい順に並べる。
77
+ # node を入れ替えて古い版を消せば、その版は候補から消える=付いていく。
78
+ dist_pkg_candidates() {
79
+ local h="${HOME:-}" v d
80
+ if [[ -n "$h" && -d "$h/.nvm/versions/node" ]]; then
81
+ while IFS= read -r v; do
82
+ [[ -n "$v" ]] || continue
83
+ d="$h/.nvm/versions/node/$v/lib/node_modules/$CONDUCTOR_PKG"
84
+ dist_pkg_usable "$d" && printf '%s\n' "$d"
85
+ done < <(ls -1 "$h/.nvm/versions/node" 2>/dev/null | sort -rV)
86
+ fi
87
+ for d in "$h/.npm-global/lib/node_modules/$CONDUCTOR_PKG" \
88
+ "$h/.local/lib/node_modules/$CONDUCTOR_PKG" \
89
+ "/usr/local/lib/node_modules/$CONDUCTOR_PKG" \
90
+ "/usr/lib/node_modules/$CONDUCTOR_PKG"; do
91
+ dist_pkg_usable "$d" && printf '%s\n' "$d"
92
+ done
93
+ return 0
94
+ }
95
+
96
+ # この機に入っている配布物の置き場を探す(★結果は DIST_FOUND と DIST_METHOD に入れる)。
97
+ # 順番:
98
+ # 1. 試験用の名指し(CONDUCTOR_DIST_ROOT)
99
+ # 2. いま PATH に居る conductor(人が打ったとき=いちばん確か)
100
+ # 3. 人が打ったときに書き残した控え(systemd の中ではまずこれ)
101
+ # 4. 決め打ちの置き場(nvm を新しい版から・その他の決め打ち)
102
+ # 5. npm が言う全体の置き場
103
+ # ★どの道でも、git の作業場所は置き場にしない。
104
+ dist_pkg_find() {
105
+ DIST_FOUND=''; DIST_METHOD=''
56
106
  if [[ -n "${CONDUCTOR_DIST_ROOT:-}" ]]; then
57
- printf '%s' "$CONDUCTOR_DIST_ROOT"
58
- return 0
107
+ DIST_FOUND="$CONDUCTOR_DIST_ROOT"; DIST_METHOD='override'; return 0
59
108
  fi
60
109
  local bin real root
61
110
  # ★bash で動く書き方(command -v -a は zsh 専用。便 9858450b で踏んだ)。
@@ -64,18 +113,88 @@ dist_pkg_root() {
64
113
  real="$(readlink -f "$bin" 2>/dev/null || printf '%s' "$bin")"
65
114
  # <置き場>/dist/cli.js から2つ上がると配布物の根になる。
66
115
  root="$(cd "$(dirname "$real")/.." 2>/dev/null && pwd)" || root=''
67
- if [[ -n "$root" && -f "$root/package.json" ]]; then
68
- printf '%s' "$root"
69
- return 0
116
+ if dist_pkg_usable "$root"; then
117
+ DIST_FOUND="$root"; DIST_METHOD='path'; return 0
118
+ fi
119
+ fi
120
+ if [[ -f "$DIST_RUNTIME_HINT" ]]; then
121
+ root="$(head -n 1 "$DIST_RUNTIME_HINT" 2>/dev/null)"
122
+ if dist_pkg_usable "$root"; then
123
+ DIST_FOUND="$root"; DIST_METHOD='hint'; return 0
70
124
  fi
71
125
  fi
72
- # 予備=npm が言う全体の置き場から辿る。
126
+ root="$(dist_pkg_candidates | head -n 1)"
127
+ if [[ -n "$root" ]]; then
128
+ DIST_FOUND="$root"; DIST_METHOD='scan'; return 0
129
+ fi
73
130
  local groot
74
131
  groot="$(npm root -g 2>/dev/null)"
75
- if [[ -n "$groot" && -f "$groot/$CONDUCTOR_PKG/package.json" ]]; then
76
- printf '%s' "$groot/$CONDUCTOR_PKG"
77
- return 0
132
+ if [[ -n "$groot" ]] && dist_pkg_usable "$groot/$CONDUCTOR_PKG"; then
133
+ DIST_FOUND="$groot/$CONDUCTOR_PKG"; DIST_METHOD='npm'; return 0
134
+ fi
135
+ return 1
136
+ }
137
+
138
+ # この機に入っている配布物の置き場(見つからなければ空)。★昔からの呼び方を残す。
139
+ dist_pkg_root() {
140
+ dist_pkg_find && printf '%s' "$DIST_FOUND"
141
+ return 0
142
+ }
143
+
144
+ # 導入処理(人が打つ)が確かめた置き場を控えに残す。★導入処理からだけ呼ぶ。
145
+ # 渡すのは、導入処理が入口の一式を写した元=公開済みの正規版(dist_source_for_setup の答え)。
146
+ # お客さまの機では、導入処理は入っている配布物の中から動くので、その置き場そのものになる。
147
+ # ★systemd の中(常駐・耳・毎時同期)では書かない=自分の推測を自分の根拠にしない。
148
+ # ★使えない置き場(git の作業場所・形が違う)は書かない。
149
+ dist_runtime_hint_write() {
150
+ local root="$1"
151
+ dist_pkg_usable "$root" || return 0
152
+ mkdir -p "$(dirname "$DIST_RUNTIME_HINT")" 2>/dev/null || return 0
153
+ printf '%s\n' "$root" > "$DIST_RUNTIME_HINT" 2>/dev/null || true
154
+ }
155
+
156
+ # 置き場に対応する node を返す(★その置き場を持つ node を使う=版が食い違わない)。
157
+ dist_node_for() {
158
+ local root="$1" prefix
159
+ if [[ -n "${CONDUCTOR_DIST_NODE:-}" ]]; then
160
+ printf '%s' "$CONDUCTOR_DIST_NODE"; return 0
161
+ fi
162
+ prefix="${root%/lib/node_modules/$CONDUCTOR_PKG}"
163
+ if [[ "$prefix" != "$root" && -x "$prefix/bin/node" ]]; then
164
+ printf '%s' "$prefix/bin/node"; return 0
165
+ fi
166
+ # 予備=置き場に node が同居しない入れ方(npm-global など)。
167
+ local n
168
+ n="$(command -v node 2>/dev/null || true)"
169
+ [[ -n "$n" ]] && { printf '%s' "$n"; return 0; }
170
+ return 1
171
+ }
172
+
173
+ #: 常駐・耳を動かす node(dist_runtime_resolve が決める)。
174
+ DIST_NODE=''
175
+
176
+ # 常駐と耳を動かす置き場を決める(便 81a08d58)。★作業ツリーは選ばない。
177
+ # 返り 0 … DIST_ROOT / DIST_NODE で動かしてよい
178
+ # 返り 1 … 動かしてはいけない(DIST_REASON に理由)
179
+ # ★公開 npm には問い合わせない。常駐は回線が切れても動き続ける必要があり、
180
+ # 版が古いかどうかは毎時同期(dist_source_resolve)が見て知らせる=同じ見張りを2つ作らない。
181
+ dist_runtime_resolve() {
182
+ DIST_ROOT=''; DIST_NODE=''; DIST_VERSION=''; DIST_PUBLISHED=''; DIST_REASON=''
183
+ if ! dist_pkg_find; then
184
+ DIST_REASON='not-installed'; return 1
185
+ fi
186
+ if ! dist_pkg_usable "$DIST_FOUND"; then
187
+ DIST_REASON='worktree-refused'; return 1
188
+ fi
189
+ if ! dist_source_complete "$DIST_FOUND" || [[ ! -f "$DIST_FOUND/dist/cli.js" ]]; then
190
+ DIST_REASON='incomplete'; return 1
78
191
  fi
192
+ DIST_VERSION="$(dist_installed_version "$DIST_FOUND")"
193
+ [[ -n "$DIST_VERSION" ]] || { DIST_REASON='no-version'; return 1; }
194
+ DIST_NODE="$(dist_node_for "$DIST_FOUND")" || DIST_NODE=''
195
+ [[ -n "$DIST_NODE" ]] || { DIST_REASON='no-node'; return 1; }
196
+ DIST_ROOT="$DIST_FOUND"
197
+ return 0
79
198
  }
80
199
 
81
200
  # 指定の置き場に入っている版(package.json)。読めなければ空。
@@ -114,11 +233,12 @@ dist_published_version() {
114
233
  dist_source_resolve() {
115
234
  DIST_ROOT=''; DIST_VERSION=''; DIST_PUBLISHED=''; DIST_REASON=''
116
235
  local root
117
- root="$(dist_pkg_root)"
118
- if [[ -z "$root" ]]; then
236
+ # ★常駐・耳(dist_runtime_resolve)と同じ探し方を使う=同じ機で別の版を選ばない。
237
+ if ! dist_pkg_find; then
119
238
  DIST_REASON='not-installed'
120
239
  return 1
121
240
  fi
241
+ root="$DIST_FOUND"
122
242
  DIST_VERSION="$(dist_installed_version "$root")"
123
243
  if [[ -z "$DIST_VERSION" ]]; then
124
244
  DIST_REASON='no-version'
@@ -138,6 +258,75 @@ dist_source_resolve() {
138
258
  return 0
139
259
  }
140
260
 
261
+ # ─── 常駐・耳の入口(便 81a08d58)──────────────────────────────────────────
262
+ #: 入口を置く場所。★node の版が変わっても動かない場所(~/.tyhld の下・職人は書けない)。
263
+ DIST_RUNTIME_BIN_DIR="${DIST_RUNTIME_BIN_DIR:-${HOME:-}/.tyhld/bin}"
264
+ #: systemd の ExecStart に書く起動の頭。★/bin/bash と上の入口だけ=どちらも動かない場所。
265
+ #: %h は systemd が起動時に HOME へ置き換える。
266
+ DIST_RUNTIME_EXEC='/bin/bash %h/.tyhld/bin/conductor-run'
267
+
268
+ # 入口の一式(入口・探し方・知らせる係)を、配布物から写す。
269
+ # 返り 0 … 写せて、中身が配布物と同じことを確かめた
270
+ # 返り 1 … 写せなかった(呼び出し側は ExecStart を書き換えない)
271
+ dist_runtime_bundle_install() {
272
+ local src="$1" dst="${2:-$DIST_RUNTIME_BIN_DIR}"
273
+ local pairs=(
274
+ "scripts/conductor-run.sh:conductor-run"
275
+ "scripts/dist-source.sh:dist-source.sh"
276
+ "scripts/hooks/skill_sync_notify.py:skill_sync_notify.py"
277
+ )
278
+ mkdir -p "$dst" 2>/dev/null || return 1
279
+ local pair from to
280
+ for pair in "${pairs[@]}"; do
281
+ from="$src/${pair%%:*}"
282
+ to="$dst/${pair##*:}"
283
+ [[ -f "$from" ]] || return 1
284
+ cp -f "$from" "$to.tmp" 2>/dev/null || return 1
285
+ mv -f "$to.tmp" "$to" 2>/dev/null || return 1 # 入れ替えは原子的=起動中に半端な入口を読ませない
286
+ cmp -s "$from" "$to" || return 1
287
+ done
288
+ chmod 755 "$dst/conductor-run" 2>/dev/null || return 1
289
+ return 0
290
+ }
291
+
292
+ # ExecStart を上書きしている drop-in を、日時つきの控えへ退ける(便 81a08d58)。
293
+ # 【なぜ要るか】耳には「作業ツリーを動かす」上書き(conductor-ear.service.d/override.conf)が
294
+ # 残っている。正本の ExecStart を直しても、この上書きが後から効いて作業ツリーを動かし続ける。
295
+ # 【何をするか】<ユニット>.d/*.conf のうち、ExecStart= の行を持つものだけを
296
+ # <名前>.bak.<日時> へ改名する(systemd は .conf 以外を読まない=効かなくなる)。
297
+ # ★環境変数や作業場所だけを上書きする drop-in には触らない。
298
+ # ★消さない。settings.json と同じ流儀の控えとして残す(中身を後から読める)。
299
+ # 出力: 退けたファイルの場所を1行ずつ。
300
+ dist_runtime_retire_exec_dropins() {
301
+ local dropin_dir="$1" f
302
+ [[ -d "$dropin_dir" ]] || return 0
303
+ for f in "$dropin_dir"/*.conf; do
304
+ [[ -f "$f" ]] || continue
305
+ if grep -qE '^[[:space:]]*ExecStart[[:space:]]*=' "$f" 2>/dev/null; then
306
+ local bak
307
+ bak="$f.bak.$(date '+%Y%m%d-%H%M%S')"
308
+ mv -f "$f" "$bak" 2>/dev/null || return 1
309
+ printf '%s\n' "$bak"
310
+ fi
311
+ done
312
+ return 0
313
+ }
314
+
315
+ # 明示された起動コマンドが作業ツリーを指していないか(★警告に使う)。
316
+ dist_runtime_bin_is_worktree() {
317
+ local cmd="$1" w
318
+ for w in $cmd; do
319
+ case "$w" in
320
+ */dist/cli.js)
321
+ local r
322
+ r="$(cd "$(dirname "$w")/.." 2>/dev/null && pwd)" || continue
323
+ [[ -e "$r/.git" ]] && return 0
324
+ ;;
325
+ esac
326
+ done
327
+ return 1
328
+ }
329
+
141
330
  # 配布物として体裁が揃っているか(配る中身が欠けた置き場を掴まない)。
142
331
  # ★ここで見るのは「采配くんの配布物であること」を決める最小の3つだけ。
143
332
  # 職人の鉄則(skills/)は安全境界ではなく作業規律で、欠けたときの扱いは ①-d が持っている
@@ -60,6 +60,12 @@ NOTICE_KINDS = {
60
60
  'stopped': '職人の決まり(鉄則)が、新しい版に入れ替わっていません。'
61
61
  '職人は古い決まりのまま動きます。',
62
62
  },
63
+ 'runtime-source': {
64
+ 'key': 'runtime-source-missing',
65
+ 'summary': '常駐と耳を起動できません',
66
+ 'stopped': '常駐(配達と生存報告)と耳(指示の着信合図)を動かす采配くん本体が'
67
+ '見つからないため、起動を止めています。管制への配達が止まります。',
68
+ },
63
69
  'dist-source': {
64
70
  'key': 'dist-source-unverified',
65
71
  'summary': '安全装置を配れていません',
@@ -412,6 +412,26 @@ else
412
412
  info " (予定)正本の内容へ揃えます(版 ${CRAFTSMAN_VER} / 中身 ${CRAFTSMAN_SHA})"
413
413
  fi
414
414
 
415
+ # ─── ①-g 常駐と耳の入口を最新へ揃える(機体1回・★同期モードでも行う・便 81a08d58) ───
416
+ # 【なぜ要るか】常駐と耳は ~/.tyhld/bin/conductor-run(入口)から動く。入口と、その隣の
417
+ # 探し方(dist-source.sh)・知らせる係は、導入処理のときに配布物から写したもの。
418
+ # 配布物を直しても、ここで揃えないと入口は古いまま残る。
419
+ # ★入口がまだ無い機体(導入処理を流し直していない機体)には新しく置かない。
420
+ # 常駐と耳がまだ入口を使っていないので、置いても意味が無く、機体に物を増やすだけになる。
421
+ # ★動いている常駐と耳は再起動しない。入口は起動のたびに読まれるので、次の起動から効く。
422
+ if [[ -f "$DIST_RUNTIME_BIN_DIR/conductor-run" ]]; then
423
+ info "①-g 常駐と耳の入口: $DIST_RUNTIME_BIN_DIR"
424
+ if [[ "$APPLY" == "1" ]]; then
425
+ if dist_runtime_bundle_install "$DIST_ROOT"; then
426
+ info " 揃えました(配布物 ${DIST_ROOT}・版 ${DIST_VERSION})"
427
+ else
428
+ warn "常駐と耳の入口を揃えられませんでした: $DIST_RUNTIME_BIN_DIR(★古い入口のまま動きます)"
429
+ fi
430
+ else
431
+ info " (予定)配布物の入口の一式へ揃えます"
432
+ fi
433
+ fi
434
+
415
435
  # ★同期モードは ①-b・①-c・①-d2 を飛ばす(掃除・タイマー・pnpm 点検は「番人のコード」ではない)。
416
436
  # ★鉄則スキル(①-d)は上で【同期モードでも】行う(便 29612ff1)。
417
437
  # 飛ばす分は、人が APPLY=1 で通常どおり流したときに従来どおり行われる。
@@ -59,42 +59,45 @@ if ! command -v systemctl >/dev/null 2>&1 || ! systemctl --user show-environment
59
59
  fi
60
60
 
61
61
  # --- 3) 耳を動かすコマンドを決める -------------------------------------------
62
- # ★systemd ユーザーサービスはシェルの PATH を継承しないので、ここでフルパスに解決して
63
- # ユニットへ焼き込む(conductor@ の導入処理と同じ考え方)。
62
+ # ★2026-09-16(えふさん確定・便 81a08d58)に決め方を変えた。
63
+ # 【旧】開発機(このスクリプトの隣に dist/cli.js が在る)では、そのリポジトリの中身を使う。
64
+ # =★職人の作業ツリーの build が耳として動いていた。職人がブランチを切り替えて build した
65
+ # 瞬間、指示の着信合図が作りかけで動く。
66
+ # さらに node の置き場(nvm の版つき)を ExecStart に焼き込んでいたので、node の版を変えると
67
+ # 203/EXEC で再起動を繰り返した(2026-09-04 に実際に起きた)。
68
+ # 【新】既定は「入っている配布物」だけを動かす。ExecStart には動かない入口
69
+ # (/bin/bash ~/.tyhld/bin/conductor-run)だけを書き、配布物と node は起動のたびに探す。
70
+ # ★開発機でもお客さま機でも同じ。作業ツリーは選ばない。
71
+ # 【作業ツリーを試したいとき】引数で起動コマンドを明示したときだけ、それをそのまま使う
72
+ # (人が打った物に限る)。作業ツリーを指していれば★警告を出す。
73
+ # ふだんの試し方は、サービスを使わずに端末で直接動かすこと(端末を閉じれば終わる=
74
+ # 入れっぱなしの上書きが残らない)。
75
+ . "$SCRIPT_DIR/../scripts/dist-source.sh"
64
76
  EAR_BIN="${1:-}"
65
77
  EAR_SRC="引数で指定"
66
78
 
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="この機体のリポジトリ(開発機)"
79
+ if [[ -n "$EAR_BIN" ]]; then
80
+ if dist_runtime_bin_is_worktree "$EAR_BIN"; then
81
+ warn "★明示された起動コマンドは作業ツリーを指しています: $EAR_BIN"
82
+ warn " 作りかけの build が耳として動きます。試し終わったら、引数なしでこの台本を流し直してください。"
85
83
  fi
84
+ else
85
+ # 入口の一式を写す元=公開済みの正規版(作業ツリーからは写さない・便 4061dd71 と同じ判定)。
86
+ if ! dist_source_for_setup "$(cd "$SCRIPT_DIR/.." && pwd)"; then
87
+ skip "耳を動かす采配くんを確かめられません(${DIST_REASON})。
88
+ 人の手番: npm install -g @tyhld/conductor --@tyhld:registry=https://registry.npmjs.org"
89
+ fi
90
+ if ! dist_runtime_bundle_install "$DIST_ROOT"; then
91
+ skip "耳の入口を置けませんでした: $DIST_RUNTIME_BIN_DIR"
92
+ fi
93
+ # 導入処理が確かめた置き場を控えに残す(systemd の中ではまずこれを見る)。
94
+ dist_runtime_hint_write "$DIST_ROOT"
95
+ # ★ユニットの正本も配り元から取る(作業ツリーの写しを配らない)。
96
+ [[ -f "$DIST_ROOT/systemd/$UNIT_NAME" ]] && UNIT_SRC="$DIST_ROOT/systemd/$UNIT_NAME"
97
+ EAR_BIN="$DIST_RUNTIME_EXEC"
98
+ EAR_SRC="入っている配布物(起動のたびに探す・版 ${DIST_VERSION:-不明})"
86
99
  fi
87
100
 
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
101
  # ★どちらを使ったかを必ず1行出す(読み違えを防ぐ)。
99
102
  info "耳が使う采配くん: ${EAR_SRC}"
100
103
  info "耳の起動コマンド: $EAR_BIN ear"
@@ -108,9 +111,23 @@ fi
108
111
  mv -f "$UNIT_DST.tmp" "$UNIT_DST" # 置換は原子的=壊れかけのユニットを残さない
109
112
  info "配置: $UNIT_DST"
110
113
 
114
+ # ★ExecStart を上書きしている drop-in を退ける(便 81a08d58)。
115
+ # 残すと、上で書いた ExecStart より後から効いて、作業ツリーを動かし続ける。
116
+ if retired="$(dist_runtime_retire_exec_dropins "$UNIT_DIR/$UNIT_NAME.d")"; then
117
+ if [[ -n "$retired" ]]; then
118
+ while IFS= read -r line; do
119
+ info "起動方法を上書きしていた drop-in を控えへ退けました: $line"
120
+ done <<< "$retired"
121
+ fi
122
+ else
123
+ skip "起動方法を上書きしている drop-in を退けられませんでした: $UNIT_DIR/$UNIT_NAME.d"
124
+ fi
125
+
111
126
  # --- 5) 反映して起動 ----------------------------------------------------------
112
127
  systemctl --user daemon-reload || skip "daemon-reload に失敗しました。"
113
128
 
129
+ # ★動いている耳は、再起動するまで古い起動方法のまま。ここでは再起動しない
130
+ # (動いている便を途中で切らないため・人が頃合いを見て再起動する)。
114
131
  if systemctl --user enable --now "$UNIT_NAME"; then
115
132
  info "有効化: $UNIT_NAME(このPCに1本・合図を受けて各現場の relay を前倒しします)"
116
133
  # ★台帳へ載せるのは enable に成功したときだけ。
@@ -125,6 +142,8 @@ fi
125
142
  loginctl enable-linger "$USER" >/dev/null 2>&1 || true
126
143
 
127
144
  echo
145
+ echo "★新しい起動方法は、耳を再起動したときから効きます:"
146
+ echo " systemctl --user restart conductor-ear"
128
147
  echo "ログ確認: journalctl --user -u conductor-ear -f"
129
148
  echo "停止: systemctl --user disable --now conductor-ear"
130
149
  systemctl --user --no-pager status "$UNIT_NAME" 2>/dev/null || true