@tyhld/conductor 0.12.81 → 0.12.83

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/dist/view.js CHANGED
@@ -8,6 +8,8 @@
8
8
  * =「見えない」だけでなく「触ると危ない」状態だった。
9
9
  *
10
10
  * 【この画面の約束(えふさん確定 2026-10-02)】
11
+ * ⓪ 既定は「開いたまま流れ続ける」(判断7・便 a56e49bf)。流れ続ける部分は
12
+ * src/view-stream.ts に置く。このファイルは「いまの様子を1回出す」(--once)形を持つ。
11
13
  * ① 通常=人間向けの日本語/詳細=Claude Code の生ログ の2段構成。
12
14
  * 以前の画面をそのまま流すのではなく、非エンジニアが読んで分かる形にする。
13
15
  * ② 「🟠 作業中」と出してよいのは、実プロセスが動いている + 最近の作業記録がある
@@ -472,27 +474,40 @@ export function readRawLogLines(v) {
472
474
  // 入口(cli.ts が呼ぶ)
473
475
  // ─────────────────────────────────────────────────────────────────────────────
474
476
  /**
475
- * `conductor view <現場名> [--detail]` の本体。
476
- * ★何も起動しない・通信しない・入力を受け付けない。画面に出して終わる。
477
+ * `conductor view <現場名> [--once] [--detail]` の本体。
478
+ * ★何も起動しない・通信しない・入力を受け付けない。
479
+ *
480
+ * 【既定は「開いたまま流れ続ける」(えふさん確定 2026-10-02・判断7)】
481
+ * 画面あり方式の頃のように、開いておけば職人の動きが下へ流れ続ける。
482
+ * Ctrl+C で抜ける(★抜けても職人・見守り役には何も起きない)。
483
+ * 【--once】これまでどおり「いまの様子を1回出して終わる」(古い呼び方を壊さない)。
484
+ * 【--detail】生の記録(秘密は隠したもの)も続けて出す。意味は変えない。
477
485
  */
478
- export function runViewCommand(argv) {
486
+ export async function runViewCommand(argv) {
479
487
  const detail = argv.includes('--detail');
488
+ const once = argv.includes('--once');
480
489
  const site = argv.find((a) => !a.startsWith('-'));
481
490
  if (site === undefined || site.length === 0) {
482
- console.error('使い方: conductor view <現場名> [--detail]');
491
+ console.error('使い方: conductor view <現場名> [--once] [--detail]');
483
492
  return 2;
484
493
  }
485
- // ★現場のフォルダが無い=この PC の現場ではない。何も作らずに知らせる。
494
+ // ★現場のフォルダが無い=この PC の現場ではない。何も作らずに、次の1手を添えて知らせる。
486
495
  const projectDir = path.join(os.homedir(), 'projects', site);
487
496
  if (!existsSync(projectDir)) {
488
497
  console.error(`そんな現場はありません: ${site}`);
498
+ console.error('次の1手:このPCの現場の名前は ls ~/projects で確かめられます(大文字小文字も合わせてください)。');
489
499
  return 1;
490
500
  }
491
- const v = collectSiteView(site);
492
- console.log(renderNormalView(v));
493
- if (detail) {
494
- console.log('');
495
- console.log(renderDetailView(v, readRawLogLines(v)));
501
+ if (once) {
502
+ const v = collectSiteView(site);
503
+ console.log(renderNormalView(v));
504
+ if (detail) {
505
+ console.log('');
506
+ console.log(renderDetailView(v, readRawLogLines(v)));
507
+ }
508
+ return 0;
496
509
  }
497
- return 0;
510
+ // ★流れ続ける画面は別のファイルに置く(--once の道は1バイトも変えない)。
511
+ const { runFollowCommand } = await import('./view-stream.js');
512
+ return runFollowCommand(site, detail);
498
513
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.81",
3
+ "version": "0.12.83",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -387,6 +387,172 @@ run() {
387
387
  "$@"
388
388
  }
389
389
 
390
+ # >>> preflight >>> ★この印の間が正本。install.sh と update-guard.sh で1文字も違わないことをテストが固定する。
391
+ # ═════════════════════════════════════════════════════════════════════════════
392
+ # 先頭の確かめ(職人が始まるのに要る物・便 ff8f4be9)
393
+ #
394
+ # 【なぜ要るか(実機 2026-10-02・ノートPC)】
395
+ # 便 7901973d が3回続けて起動に失敗した。囲いの部品 bubblewrap・socat が入っておらず、
396
+ # 設定 sandbox.failIfUnavailable のため職人が起動を断っていた。更新までは気づかれずに動き、
397
+ # 更新の後に職人が始まらなくなった。また、最低版(えふさん確定 2.1.282)より古い
398
+ # Claude Code では中継が職人を始めない。=設定を配る前にここで気づき、人が打つ1行を出す。
399
+ #
400
+ # 【決め】
401
+ # ・確かめるだけ。★台本が自分で sudo を打たない・勝手に入れない(人の窓)。
402
+ # ・Claude Code は中継(src/headless.ts の resolveClaudeBin)と同じ場所を同じ順で探し、
403
+ # いちばん新しい版で判定する(CONDUCTOR_CLAUDE_BIN の指定があればそれだけ)。
404
+ # ・囲いの部品は、その機の仕組みに要る物だけを見る。
405
+ # Linux(WSL を含む)… bwrap と socat
406
+ # Mac … OS に組み込み(追加の部品は要らない)=何も求めない
407
+ # それ以外 … 止めずに「確かめられませんでした」と出す
408
+ # ・出す1行は、そのままコピペで動く形(山かっこの穴埋めなし)。
409
+ #
410
+ # 【使い方】preflight_check <claude が無いときの扱い: need|later>
411
+ # 結果: PREFLIGHT_NG(足りない物の説明と1行。空なら通過)/PREFLIGHT_INFO(止めない注意)
412
+ # /PREFLIGHT_CLAUDE_BIN(中継が使うはずの claude・見つからなければ空)
413
+ # ★later … 後の段で Claude Code を入れる台本(1行インストーラ)用。無いことだけは止めない
414
+ # (古いときは止める)。
415
+ # ═════════════════════════════════════════════════════════════════════════════
416
+ # ★src/headless.ts の CLAUDE_MIN_VERSION と同じ値(テストで一致を固定)。
417
+ PREFLIGHT_CLAUDE_MIN='2.1.282'
418
+
419
+ # a が b より古いか(数字3つだけを見る。読めない字は 0=古い扱い)。
420
+ preflight_ver_lt() {
421
+ local a="${1%%-*}" b="${2%%-*}" i x y
422
+ local -a A B
423
+ IFS='.' read -r -a A <<< "$a"
424
+ IFS='.' read -r -a B <<< "$b"
425
+ for i in 0 1 2; do
426
+ x="${A[i]:-0}"; y="${B[i]:-0}"
427
+ [[ "$x" =~ ^[0-9]+$ ]] || x=0
428
+ [[ "$y" =~ ^[0-9]+$ ]] || y=0
429
+ if (( x < y )); then return 0; fi
430
+ if (( x > y )); then return 1; fi
431
+ done
432
+ return 1
433
+ }
434
+
435
+ # 中継と同じ候補を同じ順で出す(1行に1つ)。
436
+ preflight_claude_candidates() {
437
+ if [[ -n "${CONDUCTOR_CLAUDE_BIN:-}" ]]; then
438
+ printf '%s\n' "$CONDUCTOR_CLAUDE_BIN"
439
+ return 0
440
+ fi
441
+ local d
442
+ local -a nv=()
443
+ for d in "$HOME/.nvm/versions/node"/v*; do
444
+ [[ -d "$d" ]] && nv+=("${d##*/}")
445
+ done
446
+ if (( ${#nv[@]} > 0 )); then
447
+ # 新しい node の版から(v24.21.0 → 24 21 0 で降順)
448
+ while IFS= read -r d; do
449
+ printf '%s\n' "$HOME/.nvm/versions/node/$d/bin/claude"
450
+ done < <(printf '%s\n' "${nv[@]}" | sort -t. -k1.2,1nr -k2,2nr -k3,3nr)
451
+ fi
452
+ printf '%s\n' "$HOME/.local/bin/claude" "$HOME/.claude/local/claude" /usr/local/bin/claude /usr/bin/claude
453
+ }
454
+
455
+ # その claude の入れ方に合う更新の1行(src/headless.ts の claudeUpdateHint と同じ見分け方)。
456
+ preflight_claude_update_line() {
457
+ local bin="$1" real="$1" t i
458
+ # 実体を辿る(★readlink -f は使わない=無いパスでも文字列を返すため。1段ずつ辿る)
459
+ for i in 1 2 3 4 5 6 7 8; do
460
+ [[ -L "$real" ]] || break
461
+ t="$(readlink "$real" 2>/dev/null || true)"
462
+ [[ -n "$t" ]] || break
463
+ case "$t" in
464
+ /*) real="$t" ;;
465
+ *) real="$(dirname "$real")/$t" ;;
466
+ esac
467
+ done
468
+ case "$real$bin" in
469
+ */node_modules/@anthropic-ai/claude-code/*|*/.nvm/versions/node/*)
470
+ printf ' PATH="%s:$PATH" npm install -g @anthropic-ai/claude-code@latest\n' "$(dirname "$bin")"
471
+ return 0 ;;
472
+ esac
473
+ case "$real" in
474
+ "$HOME"/.local/share/claude/*|"$HOME"/.claude/local/*)
475
+ printf ' "%s" update\n' "$bin"
476
+ return 0 ;;
477
+ esac
478
+ printf ' (入れ方を見分けられませんでした。どちらか合う方を打ってください)\n'
479
+ printf ' ネイティブ導入なら: "%s" update\n' "$bin"
480
+ printf ' npm 導入なら : npm install -g @anthropic-ai/claude-code@latest\n'
481
+ }
482
+
483
+ preflight_check() {
484
+ local claude_mode="${1:-need}" c v best='' best_v='' unknown='' os missing=''
485
+ PREFLIGHT_NG=''
486
+ PREFLIGHT_INFO=''
487
+ PREFLIGHT_CLAUDE_BIN=''
488
+
489
+ # ── ① Claude Code が在り、最低版以上か ──
490
+ while IFS= read -r c; do
491
+ [[ -n "$c" && -x "$c" ]] || continue
492
+ v="$("$c" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)"
493
+ if [[ -z "$v" ]]; then
494
+ [[ -n "$unknown" ]] || unknown="$c"
495
+ continue
496
+ fi
497
+ if [[ -z "$best_v" ]] || preflight_ver_lt "$best_v" "$v"; then
498
+ best="$c"; best_v="$v"
499
+ fi
500
+ done < <(preflight_claude_candidates)
501
+
502
+ # shellcheck disable=SC2034 # update-guard.sh の ⓪-b が使う(install.sh では使わない)
503
+ PREFLIGHT_CLAUDE_BIN="${best:-$unknown}"
504
+ if [[ -n "$best_v" ]]; then
505
+ if preflight_ver_lt "$best_v" "$PREFLIGHT_CLAUDE_MIN"; then
506
+ PREFLIGHT_NG+="■ 職人のソフト(Claude Code)が古いです(${best} = ${best_v}、必要な版 ${PREFLIGHT_CLAUDE_MIN} 以上)。"$'\n'
507
+ PREFLIGHT_NG+=" この機の入れ方に合う1行:"$'\n'
508
+ PREFLIGHT_NG+="$(preflight_claude_update_line "$best")"$'\n'
509
+ else
510
+ PREFLIGHT_INFO+="✓ 職人のソフト(Claude Code): ${best} = ${best_v}(必要な版 ${PREFLIGHT_CLAUDE_MIN} 以上)"$'\n'
511
+ fi
512
+ elif [[ -n "$unknown" ]]; then
513
+ # ★中継も版の読めない claude は止めずに使う。ここでも止めない(作りをそろえる)。
514
+ PREFLIGHT_INFO+="⚠ 職人のソフト(Claude Code)の版を確かめられませんでした(${unknown})。"$'\n'
515
+ elif [[ "$claude_mode" == "need" ]]; then
516
+ PREFLIGHT_NG+="■ 職人のソフト(Claude Code)が見つかりません。"$'\n'
517
+ PREFLIGHT_NG+=" 入れる1行:"$'\n'
518
+ PREFLIGHT_NG+=" npm install -g @anthropic-ai/claude-code@latest"$'\n'
519
+ fi
520
+
521
+ # ── ② 囲いの部品(その機の仕組みに要る物だけ) ──
522
+ os="$(uname -s 2>/dev/null || echo unknown)"
523
+ case "$os" in
524
+ Linux)
525
+ command -v bwrap >/dev/null 2>&1 || missing+=' bwrap(bubblewrap)'
526
+ command -v socat >/dev/null 2>&1 || missing+=' socat'
527
+ if [[ -n "$missing" ]]; then
528
+ PREFLIGHT_NG+="■ 囲いの部品が足りません:${missing}"$'\n'
529
+ PREFLIGHT_NG+=" (無いと、職人が安全のため起動を断ります)"$'\n'
530
+ if command -v apt-get >/dev/null 2>&1; then
531
+ PREFLIGHT_NG+=" 入れる1行(Ubuntu のパスワードを聞かれます):"$'\n'
532
+ PREFLIGHT_NG+=" sudo apt-get install -y bubblewrap socat"$'\n'
533
+ # ★WSL かどうか(検査は CONDUCTOR_PROC_VERSION_FILE で見本を渡せる・既定は本物)
534
+ if [[ -r "${CONDUCTOR_PROC_VERSION_FILE:-/proc/version}" ]] && grep -qi microsoft "${CONDUCTOR_PROC_VERSION_FILE:-/proc/version}"; then
535
+ PREFLIGHT_NG+=" sudo のパスワードが分からないとき(Windows の PowerShell で打つ):"$'\n'
536
+ PREFLIGHT_NG+=" wsl.exe -u root apt-get install -y bubblewrap socat"$'\n'
537
+ fi
538
+ else
539
+ PREFLIGHT_NG+=" この機の入れ方で bubblewrap と socat を入れてください(apt-get が見つかりません)。"$'\n'
540
+ fi
541
+ else
542
+ PREFLIGHT_INFO+="✓ 囲いの部品: bwrap・socat"$'\n'
543
+ fi
544
+ ;;
545
+ Darwin)
546
+ PREFLIGHT_INFO+="✓ 囲いの部品: Mac は OS に組み込み(追加の部品は要りません)"$'\n'
547
+ ;;
548
+ *)
549
+ PREFLIGHT_INFO+="⚠ 囲いの部品: この機(${os})では確かめられませんでした(止めずに進みます)。"$'\n'
550
+ ;;
551
+ esac
552
+ [[ -z "$PREFLIGHT_NG" ]]
553
+ }
554
+ # <<< preflight <<<
555
+
390
556
  # ═════════════════════════════════════════════════════════════════════════════
391
557
  # ⓪ 引数と土台の確認
392
558
  # ═════════════════════════════════════════════════════════════════════════════
@@ -475,6 +641,22 @@ MSG
475
641
  ;;
476
642
  esac
477
643
 
644
+ # --- 職人が始まるのに要る物(★先頭で確かめる・台本は自分で入れない) -----------
645
+ # Claude Code がまだ無いのはかまわない(⑥で入れる)。古いとき・囲いの部品が無いときは止まる。
646
+ PREFLIGHT_GUIDE=''
647
+ if preflight_check later; then
648
+ printf '%s' "$PREFLIGHT_INFO" | sed 's/^/ /'
649
+ else
650
+ printf '%s' "$PREFLIGHT_INFO" | sed 's/^/ /'
651
+ PREFLIGHT_GUIDE="$(printf '職人が始まるのに要る物が足りません。次の1行を打ってください。\n\n%s' "$PREFLIGHT_NG")"
652
+ if [[ "$DRY_RUN" == "1" ]]; then
653
+ printf '%s\n' "$PREFLIGHT_GUIDE" | sed 's/^/ /'
654
+ plan '足りない物があるため、ここで案内して止まります'
655
+ else
656
+ halt "$PREFLIGHT_GUIDE"
657
+ fi
658
+ fi
659
+
478
660
  # --- WSL のときは systemd(常駐の仕組み)が要る -------------------------------
479
661
  # 【なぜ止めるか】systemd が無いと、采配くんの常駐(指示を受け取り続ける係)を
480
662
  # 登録できない。ここを飛ばして先へ進めると、設置は成功したように見えて
@@ -710,6 +892,11 @@ else
710
892
  record '職人: 入れた'
711
893
  fi
712
894
 
895
+ # ★入っていた・入れた職人が、最低版以上か(古いと中継が仕事を始めない)。
896
+ if [[ "$DRY_RUN" != "1" ]] && ! preflight_check need; then
897
+ halt "$(printf '職人が始まるのに要る物が足りません。次の1行を打ってください。\n\n%s' "$PREFLIGHT_NG")"
898
+ fi
899
+
713
900
  # ═════════════════════════════════════════════════════════════════════════════
714
901
  # ⑦ 職人のログイン(★お客さまの手番・ここで止まることがある)
715
902
  # ═════════════════════════════════════════════════════════════════════════════
@@ -264,7 +264,9 @@ sed_replace() { # sed_replace <置換式> <ファイル>
264
264
  # ★常駐の生死を OS ごとに確かめる(自己点検⑦で使う)。
265
265
  service_is_running() { # service_is_running <systemd のユニット名> <launchd のラベル>
266
266
  if [[ "$OS_KIND" == "mac" ]]; then
267
- launchctl list 2>/dev/null | grep -q "[[:space:]]$2\$"
267
+ # ★grep -q は一致した時点で読むのをやめ、launchctl の残りの書き込みが SIGPIPE(141)になる。
268
+ # pipefail の下ではそれが「動いていない」に化けるので、grep に最後まで読ませる。
269
+ launchctl list 2>/dev/null | grep "[[:space:]]$2\$" >/dev/null
268
270
  else
269
271
  systemctl --user is-active --quiet "$1"
270
272
  fi
@@ -28,6 +28,12 @@ import summary_ja # noqa: E402
28
28
 
29
29
  failures = []
30
30
 
31
+ # ★機体に依らなくする: 採配くんの職人の中で流すと、中継が渡した CONDUCTOR_SITE・
32
+ # CONDUCTOR_MACHINE が入っていて、cwd から決まるはずの現場名を上書きする
33
+ # (ノートPCでだけ NG 3件・CI では無い)。ここで外し、上書きの試験は値を注入して確かめる。
34
+ for _k in ('CONDUCTOR_SITE', 'CONDUCTOR_MACHINE'):
35
+ os.environ.pop(_k, None)
36
+
31
37
 
32
38
  def check(label, got, want):
33
39
  ok = got == want
@@ -1186,6 +1192,11 @@ _NEV = {'hook_event_name': 'Notification', 'notification_type': 'permission_prom
1186
1192
  'cwd': '/home/fshim/projects/devlog-tracker', 'session_id': 'sess-9'}
1187
1193
  _pay = notification_hook.build_payload(_NEV)
1188
1194
  check('現場名が入る', _pay['site'], 'devlog-tracker')
1195
+ os.environ['CONDUCTOR_SITE'] = 'override-site'
1196
+ try:
1197
+ check('現場名は CONDUCTOR_SITE が cwd より先に効く', notification_hook.build_payload(_NEV)['site'], 'override-site')
1198
+ finally:
1199
+ os.environ.pop('CONDUCTOR_SITE', None)
1189
1200
  check('sessionId は空でない', bool(_pay['sessionId']), True)
1190
1201
  check('端末で押すことが本文に書いてある', '端末' in _pay['question'], True)
1191
1202
  check('管制からは押せないと書いてある', '管制からは押せません' in _pay['question'], True)
@@ -80,6 +80,217 @@ info() { echo "[update-guard] $*"; }
80
80
  warn() { echo "[update-guard] 注意: $*" >&2; }
81
81
  fail() { echo "[update-guard] エラー: $*" >&2; exit 1; }
82
82
 
83
+ # >>> preflight >>> ★この印の間が正本。install.sh と update-guard.sh で1文字も違わないことをテストが固定する。
84
+ # ═════════════════════════════════════════════════════════════════════════════
85
+ # 先頭の確かめ(職人が始まるのに要る物・便 ff8f4be9)
86
+ #
87
+ # 【なぜ要るか(実機 2026-10-02・ノートPC)】
88
+ # 便 7901973d が3回続けて起動に失敗した。囲いの部品 bubblewrap・socat が入っておらず、
89
+ # 設定 sandbox.failIfUnavailable のため職人が起動を断っていた。更新までは気づかれずに動き、
90
+ # 更新の後に職人が始まらなくなった。また、最低版(えふさん確定 2.1.282)より古い
91
+ # Claude Code では中継が職人を始めない。=設定を配る前にここで気づき、人が打つ1行を出す。
92
+ #
93
+ # 【決め】
94
+ # ・確かめるだけ。★台本が自分で sudo を打たない・勝手に入れない(人の窓)。
95
+ # ・Claude Code は中継(src/headless.ts の resolveClaudeBin)と同じ場所を同じ順で探し、
96
+ # いちばん新しい版で判定する(CONDUCTOR_CLAUDE_BIN の指定があればそれだけ)。
97
+ # ・囲いの部品は、その機の仕組みに要る物だけを見る。
98
+ # Linux(WSL を含む)… bwrap と socat
99
+ # Mac … OS に組み込み(追加の部品は要らない)=何も求めない
100
+ # それ以外 … 止めずに「確かめられませんでした」と出す
101
+ # ・出す1行は、そのままコピペで動く形(山かっこの穴埋めなし)。
102
+ #
103
+ # 【使い方】preflight_check <claude が無いときの扱い: need|later>
104
+ # 結果: PREFLIGHT_NG(足りない物の説明と1行。空なら通過)/PREFLIGHT_INFO(止めない注意)
105
+ # /PREFLIGHT_CLAUDE_BIN(中継が使うはずの claude・見つからなければ空)
106
+ # ★later … 後の段で Claude Code を入れる台本(1行インストーラ)用。無いことだけは止めない
107
+ # (古いときは止める)。
108
+ # ═════════════════════════════════════════════════════════════════════════════
109
+ # ★src/headless.ts の CLAUDE_MIN_VERSION と同じ値(テストで一致を固定)。
110
+ PREFLIGHT_CLAUDE_MIN='2.1.282'
111
+
112
+ # a が b より古いか(数字3つだけを見る。読めない字は 0=古い扱い)。
113
+ preflight_ver_lt() {
114
+ local a="${1%%-*}" b="${2%%-*}" i x y
115
+ local -a A B
116
+ IFS='.' read -r -a A <<< "$a"
117
+ IFS='.' read -r -a B <<< "$b"
118
+ for i in 0 1 2; do
119
+ x="${A[i]:-0}"; y="${B[i]:-0}"
120
+ [[ "$x" =~ ^[0-9]+$ ]] || x=0
121
+ [[ "$y" =~ ^[0-9]+$ ]] || y=0
122
+ if (( x < y )); then return 0; fi
123
+ if (( x > y )); then return 1; fi
124
+ done
125
+ return 1
126
+ }
127
+
128
+ # 中継と同じ候補を同じ順で出す(1行に1つ)。
129
+ preflight_claude_candidates() {
130
+ if [[ -n "${CONDUCTOR_CLAUDE_BIN:-}" ]]; then
131
+ printf '%s\n' "$CONDUCTOR_CLAUDE_BIN"
132
+ return 0
133
+ fi
134
+ local d
135
+ local -a nv=()
136
+ for d in "$HOME/.nvm/versions/node"/v*; do
137
+ [[ -d "$d" ]] && nv+=("${d##*/}")
138
+ done
139
+ if (( ${#nv[@]} > 0 )); then
140
+ # 新しい node の版から(v24.21.0 → 24 21 0 で降順)
141
+ while IFS= read -r d; do
142
+ printf '%s\n' "$HOME/.nvm/versions/node/$d/bin/claude"
143
+ done < <(printf '%s\n' "${nv[@]}" | sort -t. -k1.2,1nr -k2,2nr -k3,3nr)
144
+ fi
145
+ printf '%s\n' "$HOME/.local/bin/claude" "$HOME/.claude/local/claude" /usr/local/bin/claude /usr/bin/claude
146
+ }
147
+
148
+ # その claude の入れ方に合う更新の1行(src/headless.ts の claudeUpdateHint と同じ見分け方)。
149
+ preflight_claude_update_line() {
150
+ local bin="$1" real="$1" t i
151
+ # 実体を辿る(★readlink -f は使わない=無いパスでも文字列を返すため。1段ずつ辿る)
152
+ for i in 1 2 3 4 5 6 7 8; do
153
+ [[ -L "$real" ]] || break
154
+ t="$(readlink "$real" 2>/dev/null || true)"
155
+ [[ -n "$t" ]] || break
156
+ case "$t" in
157
+ /*) real="$t" ;;
158
+ *) real="$(dirname "$real")/$t" ;;
159
+ esac
160
+ done
161
+ case "$real$bin" in
162
+ */node_modules/@anthropic-ai/claude-code/*|*/.nvm/versions/node/*)
163
+ printf ' PATH="%s:$PATH" npm install -g @anthropic-ai/claude-code@latest\n' "$(dirname "$bin")"
164
+ return 0 ;;
165
+ esac
166
+ case "$real" in
167
+ "$HOME"/.local/share/claude/*|"$HOME"/.claude/local/*)
168
+ printf ' "%s" update\n' "$bin"
169
+ return 0 ;;
170
+ esac
171
+ printf ' (入れ方を見分けられませんでした。どちらか合う方を打ってください)\n'
172
+ printf ' ネイティブ導入なら: "%s" update\n' "$bin"
173
+ printf ' npm 導入なら : npm install -g @anthropic-ai/claude-code@latest\n'
174
+ }
175
+
176
+ preflight_check() {
177
+ local claude_mode="${1:-need}" c v best='' best_v='' unknown='' os missing=''
178
+ PREFLIGHT_NG=''
179
+ PREFLIGHT_INFO=''
180
+ PREFLIGHT_CLAUDE_BIN=''
181
+
182
+ # ── ① Claude Code が在り、最低版以上か ──
183
+ while IFS= read -r c; do
184
+ [[ -n "$c" && -x "$c" ]] || continue
185
+ v="$("$c" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)"
186
+ if [[ -z "$v" ]]; then
187
+ [[ -n "$unknown" ]] || unknown="$c"
188
+ continue
189
+ fi
190
+ if [[ -z "$best_v" ]] || preflight_ver_lt "$best_v" "$v"; then
191
+ best="$c"; best_v="$v"
192
+ fi
193
+ done < <(preflight_claude_candidates)
194
+
195
+ # shellcheck disable=SC2034 # update-guard.sh の ⓪-b が使う(install.sh では使わない)
196
+ PREFLIGHT_CLAUDE_BIN="${best:-$unknown}"
197
+ if [[ -n "$best_v" ]]; then
198
+ if preflight_ver_lt "$best_v" "$PREFLIGHT_CLAUDE_MIN"; then
199
+ PREFLIGHT_NG+="■ 職人のソフト(Claude Code)が古いです(${best} = ${best_v}、必要な版 ${PREFLIGHT_CLAUDE_MIN} 以上)。"$'\n'
200
+ PREFLIGHT_NG+=" この機の入れ方に合う1行:"$'\n'
201
+ PREFLIGHT_NG+="$(preflight_claude_update_line "$best")"$'\n'
202
+ else
203
+ PREFLIGHT_INFO+="✓ 職人のソフト(Claude Code): ${best} = ${best_v}(必要な版 ${PREFLIGHT_CLAUDE_MIN} 以上)"$'\n'
204
+ fi
205
+ elif [[ -n "$unknown" ]]; then
206
+ # ★中継も版の読めない claude は止めずに使う。ここでも止めない(作りをそろえる)。
207
+ PREFLIGHT_INFO+="⚠ 職人のソフト(Claude Code)の版を確かめられませんでした(${unknown})。"$'\n'
208
+ elif [[ "$claude_mode" == "need" ]]; then
209
+ PREFLIGHT_NG+="■ 職人のソフト(Claude Code)が見つかりません。"$'\n'
210
+ PREFLIGHT_NG+=" 入れる1行:"$'\n'
211
+ PREFLIGHT_NG+=" npm install -g @anthropic-ai/claude-code@latest"$'\n'
212
+ fi
213
+
214
+ # ── ② 囲いの部品(その機の仕組みに要る物だけ) ──
215
+ os="$(uname -s 2>/dev/null || echo unknown)"
216
+ case "$os" in
217
+ Linux)
218
+ command -v bwrap >/dev/null 2>&1 || missing+=' bwrap(bubblewrap)'
219
+ command -v socat >/dev/null 2>&1 || missing+=' socat'
220
+ if [[ -n "$missing" ]]; then
221
+ PREFLIGHT_NG+="■ 囲いの部品が足りません:${missing}"$'\n'
222
+ PREFLIGHT_NG+=" (無いと、職人が安全のため起動を断ります)"$'\n'
223
+ if command -v apt-get >/dev/null 2>&1; then
224
+ PREFLIGHT_NG+=" 入れる1行(Ubuntu のパスワードを聞かれます):"$'\n'
225
+ PREFLIGHT_NG+=" sudo apt-get install -y bubblewrap socat"$'\n'
226
+ # ★WSL かどうか(検査は CONDUCTOR_PROC_VERSION_FILE で見本を渡せる・既定は本物)
227
+ if [[ -r "${CONDUCTOR_PROC_VERSION_FILE:-/proc/version}" ]] && grep -qi microsoft "${CONDUCTOR_PROC_VERSION_FILE:-/proc/version}"; then
228
+ PREFLIGHT_NG+=" sudo のパスワードが分からないとき(Windows の PowerShell で打つ):"$'\n'
229
+ PREFLIGHT_NG+=" wsl.exe -u root apt-get install -y bubblewrap socat"$'\n'
230
+ fi
231
+ else
232
+ PREFLIGHT_NG+=" この機の入れ方で bubblewrap と socat を入れてください(apt-get が見つかりません)。"$'\n'
233
+ fi
234
+ else
235
+ PREFLIGHT_INFO+="✓ 囲いの部品: bwrap・socat"$'\n'
236
+ fi
237
+ ;;
238
+ Darwin)
239
+ PREFLIGHT_INFO+="✓ 囲いの部品: Mac は OS に組み込み(追加の部品は要りません)"$'\n'
240
+ ;;
241
+ *)
242
+ PREFLIGHT_INFO+="⚠ 囲いの部品: この機(${os})では確かめられませんでした(止めずに進みます)。"$'\n'
243
+ ;;
244
+ esac
245
+ [[ -z "$PREFLIGHT_NG" ]]
246
+ }
247
+ # <<< preflight <<<
248
+
249
+ info "⓪ 職人が始まるのに要る物の確かめ(★設定を配る前・ここで足りなければ1バイトも配りません)"
250
+ if preflight_check need; then
251
+ printf '%s' "$PREFLIGHT_INFO" | sed 's/^/[update-guard] /'
252
+ else
253
+ printf '%s' "$PREFLIGHT_INFO" | sed 's/^/[update-guard] /'
254
+ printf '%s' "$PREFLIGHT_NG" | sed 's/^/[update-guard] /' >&2
255
+ if [[ "$SYNC_ONLY" == "1" ]]; then
256
+ # ★同期モード(無人・1時間ごと)は設定の中身を作り変えない(番人のコードと配線だけ)。
257
+ # ここで止めると番人の直しまで届かなくなるので、知らせて続ける。
258
+ warn "職人が始まるのに要る物が足りません(上記)。同期モードなので番人のコードと配線だけ続けます。"
259
+ else
260
+ warn "職人が始まるのに要る物が足りません(上記)。★設定は1バイトも配っていません。"
261
+ warn " 上の1行を打ってから、もう一度このスクリプトを流してください。"
262
+ exit 1
263
+ fi
264
+ fi
265
+
266
+ # ─── ⓪-b 職人が管制へ知らせる口(ai-kana の MCP)が使えるか(★確認だけのときだけ・止めない) ───
267
+ # 【なぜ要るか(実機 2026-10-02・便 fb248f6e)】職人の会話に ai-kana の道具が1つも出ず、完了の知らせ
268
+ # (conductor_complete)も開発履歴の読み取りもできなかった。便を配る前に人が気づけるよう、
269
+ # 確認だけのときに `claude mcp list` の結果を1行で出す。
270
+ # ★確認だけ(APPLY なし・同期モードでない)のときだけ流す=適用・1時間ごとの同期では外へ問い合わせない。
271
+ # ★鍵・トークンの値は読まない・出さない(出すのは「つながっているか」だけ)。
272
+ if [[ "$APPLY" != "1" && "$SYNC_ONLY" != "1" && -n "${PREFLIGHT_CLAUDE_BIN:-}" ]]; then
273
+ info "⓪-b 職人が管制へ知らせる口(ai-kana)の確かめ(確認だけ・止めません)"
274
+ if command -v timeout >/dev/null 2>&1; then
275
+ mcp_out="$(timeout 60 "$PREFLIGHT_CLAUDE_BIN" mcp list 2>/dev/null || true)"
276
+ else
277
+ mcp_out="$("$PREFLIGHT_CLAUDE_BIN" mcp list 2>/dev/null || true)"
278
+ fi
279
+ mcp_ok="$(printf '%s\n' "$mcp_out" | grep -E '^(claude\.ai )?ai-kana: .*Connected' | sed -E 's/: .* - /: /' || true)"
280
+ mcp_ng="$(printf '%s\n' "$mcp_out" | grep -E '^(claude\.ai )?ai-kana: ' | grep -v 'Connected' | sed -E 's/: .* - /: /' || true)"
281
+ if [[ -n "$mcp_ok" ]]; then
282
+ printf '%s\n' "$mcp_ok" | sed 's/^/[update-guard] ✓ /'
283
+ [[ -z "$mcp_ng" ]] || printf '%s\n' "$mcp_ng" | sed 's/^/[update-guard] (予備・使っていない)/'
284
+ else
285
+ warn "職人が管制へ知らせる口(ai-kana)がつながっていません。便は動きますが、完了の知らせと開発履歴の読み書きができません。"
286
+ [[ -z "$mcp_ng" ]] || printf '%s\n' "$mcp_ng" | sed 's/^/[update-guard] /' >&2
287
+ warn " 人の手番(1手ずつ): ① Ubuntu の画面で claude と打つ ② 画面の中で /mcp と打つ"
288
+ warn " ③ ai-kana を選んで Enter → Authenticate を選ぶ ④ 開いたブラウザでログインする ⑤ /exit で閉じる"
289
+ warn " (claude.ai ai-kana のほうは https://claude.ai の設定 → コネクタ で ai-kana を有効にする)"
290
+ fi
291
+ fi
292
+
293
+
83
294
  # ─── ①-src 配り元を決める(★公開済みの正規版だけ・便 4061dd71) ──────────────
84
295
  # 【なぜ要るか(実測 2026-09-16・開発履歴 6b405ae6)】
85
296
  # 1時間ごとの自動同期の本体が /home/fshim/projects/conductor/scripts/update-guard.sh