@tyhld/conductor 0.12.110 → 0.12.112

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.
@@ -310,6 +310,60 @@ const ENTRY_SYNC = [
310
310
  // 今までどおり人が設置台本を打つ)。ここで揃えるのは【受け口のファイルだけ】。
311
311
  { from: 'scripts/codex/codex-notify.py', dir: 'hooks', to: 'codex-notify.py', exe: true },
312
312
  ];
313
+ // ── 番人の同期を、版の入れ替えと同じ時に1回起こす(便 b649d1f8・ADR-012 の穴) ──────
314
+ // 【何が起きていたか(実測 2026-10-06)】
315
+ // 受け口(codex-notify.py)は上の ENTRY_SYNC で【入れ替えと同時】に届くのに、
316
+ // 番人(stop_hook.py など)は update-guard.sh の INSTALL_FILES からしか届かず、
317
+ // それを流すのは tyhld-guard-sync.timer=【1時間ごと】。
318
+ // メインPC が 0.12.110 になった 09:57 から、番人がそろう 10:28:45 までの31分間、
319
+ // 「受け口だけ新しく、番人が古い」半死の状態になった。その穴の中で流れた試験
320
+ // e13246e8 は、報告の本文が1文字も管制に載らなかった。
321
+ // =★番人を直した版を公開するたび、各PCで最長1時間その直しが効かない。
322
+ //
323
+ // 【なぜ ENTRY_SYNC に足すのではなく、同期を起こすのか(甲ではなく乙を選んだ理由)】
324
+ // ① 番人は1ファイルで完結しない。stop_hook.py は finish_gate / hook_datalink /
325
+ // summary_ja を import し、finish_gate.py は report-shape.json を実行時に読む。
326
+ // ENTRY_SYNC は1ファイルずつ写すので、ここへ stop_hook.py だけを足すと
327
+ // 「入口だけ新しく、判定の本体と形の決まりが古い」というズレた組み合わせを
328
+ // 新しく作ってしまう(=いま直している穴と同じ種類の事故)。
329
+ // ② それを避けて番人一式(INSTALL_FILES + INSTALL_CONFIGS)を ENTRY_SYNC へ並べると、
330
+ // 配る物の一覧がリポジトリの中で【2か所】になる。今回の穴は「一覧が2つあって
331
+ // 片方に stop_hook.py が無かった」事そのものなので、3つ目を作るのは根治にならない。
332
+ // ③ 同期を1回起こせば、配る中身を決めるのは今までどおり update-guard.sh の
333
+ // INSTALL_FILES だけ(正本は1つ)。番人一式が【同じ版から同時に】置かれるので、
334
+ // ファイルどうしで版がずれる組み合わせが原理的に作れない。
335
+ //
336
+ // 【番人を置いていないPCへ広がらない理由】
337
+ // このユニットを置くのは update-guard.sh の ①-f(APPLY=1 のときだけ)=番人を
338
+ // 設置したPCだけ。つまり【ユニットが在る ⟺ 番人が置いてある】。
339
+ // 下の syncGuardNow は、ユニットの実体が無ければ systemctl を1回も呼ばずに戻る。
340
+ // ★ここから番人を新しく設置する道は1本も無い(置く中身を1バイトも持っていない)。
341
+ /** 番人のコードと配線を正本へ揃える systemd --user サービス(systemd/tyhld-guard-sync.service)。 */
342
+ export const GUARD_SYNC_UNIT = 'tyhld-guard-sync.service';
343
+ /** そのPCに番人の同期サービスが設置されているか(=番人が置いてあるPCか)。 */
344
+ export function guardSyncUnitPath(home) {
345
+ return path.join(home, '.config', 'systemd', 'user', GUARD_SYNC_UNIT);
346
+ }
347
+ /**
348
+ * 番人の同期を1回起こす。★入れ替えた配布物から番人一式が置き直される。
349
+ * 戻り: 記録に残す1行(何もしなかった理由まで分かる形にする)。
350
+ *
351
+ * ★`--no-block` で頼むだけにする理由: 同期の本体(update-guard.sh)は公開 npm への
352
+ * 問い合わせを含み、数十秒かかることがある。入れ替えの手を止めて待つと、常駐の
353
+ * 起こし直しがその分遅れる。頼んだ後は systemd が面倒を見るので、待たなくてよい。
354
+ * ★失敗しても入れ替えは止めない(今までどおり1時間ごとの同期で追いつく)。
355
+ */
356
+ export function syncGuardNow(d) {
357
+ const unit = guardSyncUnitPath(d.home);
358
+ if (!existsSync(unit))
359
+ return '番人の同期: このPCには番人が置かれていないので何もしませんでした';
360
+ const r = d.exec('systemctl', ['--user', 'start', '--no-block', GUARD_SYNC_UNIT], { timeoutMs: 15_000 });
361
+ if (r.status !== 0) {
362
+ return `番人の同期: 起こせませんでした(終了 ${r.status}・${(r.error ?? r.stderr).trim().slice(-120)})`
363
+ + '。1時間ごとの同期で追いつきます';
364
+ }
365
+ return '番人の同期: 起こしました(番人一式が、入れ替えた版から置き直されます)';
366
+ }
313
367
  /** 中身が1バイトも違わないか(★同じなら書き換えない=毎回の入れ替えで日付だけ動かない)。 */
314
368
  function sameContent(a, b) {
315
369
  try {
@@ -529,6 +583,8 @@ async function rollBack(d, s, back) {
529
583
  const r = installConductor(d, back, true);
530
584
  d.log(r.detail);
531
585
  d.log(describeEntrySync(refreshEntries(d.home, d.distRoot)));
586
+ // ★戻すときも番人を一緒に戻す(入口だけ古い版、番人だけ新しい版を残さない)。
587
+ d.log(syncGuardNow(d));
532
588
  d.log(await restartRelays(d, back));
533
589
  }
534
590
  else {
@@ -785,6 +841,8 @@ export async function runUpdater(d) {
785
841
  return done(d, s, 'install-failed', r.detail);
786
842
  }
787
843
  d.log(describeEntrySync(refreshEntries(d.home, d.distRoot)));
844
+ // ★番人も【この瞬間】そろえる(1時間ごとの同期を待たない=便 b649d1f8 の穴の根治)。
845
+ d.log(syncGuardNow(d));
788
846
  d.log(await restartRelays(d, want));
789
847
  }
790
848
  else {
package/dist/relay.js CHANGED
@@ -661,7 +661,10 @@ export function relinkClosedCodexNotices(cfg, state, deps = {}, env = process.en
661
661
  turnId: notice.turnId ?? null,
662
662
  });
663
663
  logLine(`site=${cfg.site} id=${job.id} 便を閉じたあとに届いた終わりの知らせを、この便の報告として` +
664
- `結び直しました(印が一致・閉じてから${Math.round((now - job.at) / 1000)}秒): ${got.note}` +
664
+ // ★どの道の知らせで本文が載ったかを記録に出す(便 b649d1f8・決定4。ここは便を閉じる
665
+ // 道ではないが、知らせが報告になる3つ目の場所なので、記録の読み方をそろえる)。
666
+ `結び直しました(印が一致・閉じてから${Math.round((now - job.at) / 1000)}秒` +
667
+ `${codexRouteNote(notice.route)}): ${got.note}` +
665
668
  '/★便の状態は書き換えていません(本文は後から届ける道で管制へ渡します)');
666
669
  }
667
670
  }
@@ -1424,6 +1427,17 @@ export function codexNotifyDir(env = process.env) {
1424
1427
  const home = (env.HOME ?? '').trim();
1425
1428
  return home ? path.join(home, '.tyhld', 'codex-notify') : '';
1426
1429
  }
1430
+ /** 知らせの「道」を記録に出す形にする。分からなければ空(=今までどおりの行になる)。 */
1431
+ export function codexRouteNote(route) {
1432
+ const r = (route ?? '').trim();
1433
+ if (r === '')
1434
+ return '';
1435
+ if (r === 'notify')
1436
+ return '・道=notify(Codex の notify)';
1437
+ if (r === 'stop')
1438
+ return '・道=stop(Codex の Stop フック)';
1439
+ return `・道=${r}`;
1440
+ }
1427
1441
  /**
1428
1442
  * 書きかけの行が末尾に在るとき、書き終わるのを待つ長さ。これを過ぎても改行が来なければ
1429
1443
  * 書き手が落ちた残骸とみなし、今までどおり捨てて進む(=受け皿が詰まらない)。
@@ -1481,6 +1495,7 @@ export function takeCodexNotices(site, env = process.env) {
1481
1495
  turnId: o.turnId ?? null,
1482
1496
  jobId: typeof o.jobId === 'string' && o.jobId.trim() !== '' ? o.jobId.trim() : null,
1483
1497
  at: parseNoticeAt(o.at),
1498
+ route: typeof o.route === 'string' && o.route.trim() !== '' ? o.route.trim() : null,
1484
1499
  });
1485
1500
  }
1486
1501
  }
@@ -1578,6 +1593,7 @@ export function takeCodexNoticesForJobId(site, commandId, env = process.env) {
1578
1593
  turnId: o.turnId ?? null,
1579
1594
  jobId,
1580
1595
  at: parseNoticeAt(o.at),
1596
+ route: typeof o.route === 'string' && o.route.trim() !== '' ? o.route.trim() : null,
1581
1597
  });
1582
1598
  }
1583
1599
  else {
@@ -5100,7 +5116,9 @@ export async function keepaliveTick(cfg, state, deps = {}) {
5100
5116
  if (!ok)
5101
5117
  return; // 書けなければ畳まない(次の周でやり直す。知らせは使い切っている=囲みは報告ファイルに残る)
5102
5118
  logLine(`site=${cfg.site} id=${ka.id} 見張り終了: 追跡が切れたあとに届いた終わりの知らせで完了を報告` +
5103
- `(印が一致${intake.wrote ? '・囲みから報告ファイル' : body !== null ? '・報告添付' : ''})`);
5119
+ `(印が一致${intake.wrote ? '・囲みから報告ファイル' : body !== null ? '・報告添付' : ''}` +
5120
+ // ★どちらの道の知らせで閉じたかを記録に出す(便 b649d1f8・決定4)。
5121
+ `${codexRouteNote(notice.route)})`);
5104
5122
  await deliverJobEndFacts(cfg, {
5105
5123
  commandId: ka.id,
5106
5124
  site: cfg.site,
@@ -5508,7 +5526,9 @@ export async function relayTick(cfg, state, hooks) {
5508
5526
  : await patchStatus(cfg, state.pending.id, 'done', report);
5509
5527
  if (written) {
5510
5528
  log(`site=${cfg.site} id=${state.pending.id} done(Codexの通知` +
5511
- `${intake.wrote ? '・囲みから報告ファイル' : report != null ? '・報告添付' : ''})`);
5529
+ `${intake.wrote ? '・囲みから報告ファイル' : report != null ? '・報告添付' : ''}` +
5530
+ // ★どちらの道の知らせで閉じたかを記録に出す(便 b649d1f8・決定4)。
5531
+ `${codexRouteNote(notice.route)})`);
5512
5532
  // ★終了の事実を同じ項目で届ける(便 915694ac・1-3)。この配り方では実プロセスを
5513
5533
  // 中継が持っていないので、終わりは【つかめていない】と伝える(終了コードは null)。
5514
5534
  // 届けるのは「中継が完了を確かめた」という事実だけ。★作り話の終了を送らない。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.110",
3
+ "version": "0.12.112",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -332,13 +332,38 @@ DIST_STATE="$GUARD_HOME/dist-source.state" # 最後に【確かめられた】
332
332
  DIST_LAST="$GUARD_HOME/dist-source.last" # 直近の【試み】の結果
333
333
  DIST_NOTIFY="$SCRIPT_DIR/hooks/skill_sync_notify.py"
334
334
 
335
+ # 入っている版と公開の最新がそろっているか(★控えの1行に残す印)。
336
+ # none … 同じ(入っているのが公開の最新)
337
+ # behind … 違う(段階配布の途中・公開より古い正規の版で動いている)
338
+ # unknown… どちらかが読めない
339
+ #
340
+ # 【なぜ要るか(実測 2026-10-06・便 b649d1f8)】
341
+ # メインPC が 0.12.110 へ入れ替わる31分前の同期は、控えに
342
+ # status=ok reason=verified version=0.12.109 published=0.12.110
343
+ # と書いていた。★この行は「うまくいった」としか読めないが、実際には
344
+ # 「これから入れ替わる版の一つ前の番人を配った」回だった。版の食い違いは
345
+ # 2つの値を自分で見比べないと分からず、見落とすと「配られたつもり」になる。
346
+ # 【決め】status と reason は1文字も変えない(成否は変えない・止めない)。
347
+ # ★食い違いは【別の欄】で残す。status=failed にはしない:公開の最新より古い
348
+ # 正規の版で動くのは段階配布では正常で、2026-10-02 にその理由(version-drift)を
349
+ # 「配らない理由」から外したばかり(便 29babc5d)。そこへ戻さない。
350
+ dist_drift() {
351
+ if [[ -z "${DIST_VERSION:-}" || -z "${DIST_PUBLISHED:-}" ]]; then
352
+ printf 'unknown'
353
+ elif [[ "$DIST_VERSION" == "$DIST_PUBLISHED" ]]; then
354
+ printf 'none'
355
+ else
356
+ printf 'behind'
357
+ fi
358
+ }
359
+
335
360
  # 直近の試みを1行残す。★確かめられたときだけ .state も更新する(成功と偽らない)。
336
361
  # ★確認のみ(APPLY!=1)のときは1バイトも書かない。「見ただけ」で控えが動くと、
337
362
  # 控えが「実際に配った記録」でなくなり、外から見て何が起きたのか分からなくなる。
338
363
  dist_record() {
339
364
  [[ "$APPLY" == "1" ]] || return 0
340
365
  local status="$1" reason="$2" line
341
- line="$(date '+%Y-%m-%dT%H:%M:%S%z') status=${status} reason=${reason} what=dist-source version=${DIST_VERSION:-unknown} published=${DIST_PUBLISHED:-unknown} root=${DIST_ROOT:-none}"
366
+ line="$(date '+%Y-%m-%dT%H:%M:%S%z') status=${status} reason=${reason} what=dist-source version=${DIST_VERSION:-unknown} published=${DIST_PUBLISHED:-unknown} drift=$(dist_drift) root=${DIST_ROOT:-none}"
342
367
  mkdir -p "$GUARD_HOME" 2>/dev/null || true
343
368
  printf '%s\n' "$line" > "$DIST_LAST" 2>/dev/null || true
344
369
  if [[ "$status" == "ok" ]]; then
@@ -1174,14 +1199,17 @@ if [[ "$SYNC_ONLY" == "1" ]]; then
1174
1199
  fi
1175
1200
 
1176
1201
  # ─── ④ 各現場の権限ルールを部分マージで更新 ─────────────────────────────────
1202
+ # ★実際に書き換えた現場だけを覚えておく(この下の④-b が「コミットが要るリポ」を名指しする)。
1203
+ wrote_sites=()
1177
1204
  for proj in "${targets[@]}"; do
1178
1205
  dst="$proj/.claude/settings.json"
1179
1206
  if [[ ! -f "$dst" ]]; then
1180
1207
  warn " スキップ(settings.json なし): $proj"
1181
1208
  continue
1182
1209
  fi
1210
+ rc=0
1183
1211
  APPLY="$APPLY" \
1184
- python3 - "$dst" "$SETTINGS_SRC" "$SAMPLE_SETTINGS" <<'PYEOF'
1212
+ python3 - "$dst" "$SETTINGS_SRC" "$SAMPLE_SETTINGS" <<'PYEOF' || rc=$?
1185
1213
  import json, os, shutil, sys, time
1186
1214
 
1187
1215
  dst_path, src_path, sample_path = sys.argv[1], sys.argv[2], sys.argv[3]
@@ -1423,7 +1451,16 @@ with open(tmp, "w", encoding="utf-8") as f:
1423
1451
  f.write("\n")
1424
1452
  os.replace(tmp, dst_path) # 置換は原子的=途中で壊れた settings.json を残さない
1425
1453
  print(f"[update-guard] 更新しました(バックアップ: {backup})")
1454
+ # ★実際に書き換えた回だけ 10 で終わる(呼び元が「コミットが要るリポ」を名指しするため)。
1455
+ sys.exit(10)
1426
1456
  PYEOF
1457
+ # 終わり方の読み方: 0=変更なし/見るだけ、10=実際に書き換えた、それ以外=想定外の失敗。
1458
+ # 想定外の失敗では、今までどおり(set -e と同じ)そこで止まる=黙って先へ進まない。
1459
+ if [[ "$rc" -eq 10 ]]; then
1460
+ wrote_sites+=("$proj")
1461
+ elif [[ "$rc" -ne 0 ]]; then
1462
+ exit "$rc"
1463
+ fi
1427
1464
  done
1428
1465
 
1429
1466
  echo
@@ -1432,3 +1469,36 @@ if [[ "$APPLY" == "1" ]]; then
1432
1469
  else
1433
1470
  info "確認のみで終了しました。適用するには APPLY=1 を付けて再実行してください。"
1434
1471
  fi
1472
+
1473
+ # ─── ④-b 書き換えたリポのうち「コミットが要る」物を名指しで知らせる ──────────────
1474
+ # 【なぜ要るか(実地 2026-10-06)】④ は現場の .claude/settings.json を「禁止の一覧が強い版」へ
1475
+ # 書き換えるが、それを git に入れる所がだれの仕事にもなっていなかった。結果、6つのリポで
1476
+ # 強い設定が git に入らないまま放置された(いちばん長い utatane-backend は約2か月半・git の版は
1477
+ # 禁止 11 件のまま)。放置のあいだ、ほかのPCとクラウドの作業場には弱い設定が配られ、職人の
1478
+ # 合流のたびに誤った「失敗」が出た。=書き換えた直後に名指しで出し、人が CC1 へ渡せるようにする。
1479
+ # 【決め】
1480
+ # ・出すのは「実際に書き換えた回」だけ。変更なしの回・見るだけの回は1行も出さない(表示を変えない)。
1481
+ # ・挙げるのは git が追跡している .claude/settings.json を持つリポだけ。追跡されていない物(?? だけ)は
1482
+ # コミットが要らないので挙げない。
1483
+ # ・確かめは読むだけの git(ls-files)。リポの中身・git の状態を1つも変えない。
1484
+ # ・git が使えない/リポでない置き場は黙って飛ばす(台本を落とさない)。
1485
+ # ★最後に出すのは、人の目に最後まで残るため(この下に別の行を足さない)。
1486
+ if [[ ${#wrote_sites[@]} -gt 0 ]]; then
1487
+ needs_commit=()
1488
+ for proj in "${wrote_sites[@]}"; do
1489
+ # 読むだけ。リポでない置き場・git が無い機では非ゼロで返るので、黙って外れる。
1490
+ if git -C "$proj" ls-files --error-unmatch -- .claude/settings.json >/dev/null 2>&1; then
1491
+ needs_commit+=("$proj")
1492
+ fi
1493
+ done
1494
+ if [[ ${#needs_commit[@]} -gt 0 ]]; then
1495
+ echo
1496
+ info "★コミットが要るリポがあります(git が追跡している .claude/settings.json を書き換えました)"
1497
+ for proj in "${needs_commit[@]}"; do
1498
+ info " - $(basename "$proj") ($proj)"
1499
+ done
1500
+ info " この変更は戻さない(番人の禁止の一覧が強くなった知らせ)。そのリポの職人の便でコミットする。"
1501
+ info " git checkout で戻すと守りが弱くなる。"
1502
+ info " → この一覧を CC1 に伝えてください(コミットの便を出してもらいます)。"
1503
+ fi
1504
+ fi
@@ -121,6 +121,7 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
121
121
  - **電話は自由。** 外への通信(npm / GitHub / API / ドキュメント取得など)は許可済みなので、`Network request outside of sandbox` は出ない。**通信のために止まらない・人を待たない**。最後まで自走して完了まで行く。
122
122
  - **金庫は禁止。** 秘密の持ち出し(`curl -d @.env` / `cp .env` / `tar ~/.ssh` / `base64 .env` / `echo $DATABASE_URL` など)は**番人が今までどおり止める**。通信が自由になったこととは**別の仕組み**なので、「通信が許可されたから秘密も送れる」は成り立たない。本番DB・secret・`.env`・`hooks`・`.tyhld`・`.ssh` への書き込み禁止もそのまま。
123
123
  - **それでも塞がれたら回避しない。** `curl: (56) CONNECT tunnel failed, response 403` のように通信が塞がれたときは、**意図的に閉じてある接続先**(例: 本番DBへの直結)か、設定がその機体にまだ届いていないかのどちらか。**別経路を探さず、そこで止めて報告する**(人の手番=`APPLY=1 bash scripts/update-guard.sh` → 職人を再起動)。
124
+ - ★**この1行(`APPLY=1 bash scripts/update-guard.sh`)を人が流したあとに `★コミットが要るリポがあります` の一覧が出たら、その一覧を CC1 に伝える**(コミットの便を出してもらう)。出た変更は戻さない(`git checkout` で戻すと守りが弱くなる)。
124
125
  13. **使い捨てファイルはリポジトリの中に作らない(えふさん決定 2026-09-03/えふさん確定 2026-10-02・判断6 で後片付けの形を改めた)**
125
126
  - **実機検品・検証用の使い捨てファイル(HTML・画像・ログ等)は、リポジトリの中に作らない。`/tmp/claude-1000/` に置く。**
126
127
  - **リポジトリの中(直下を含む)に作ってしまったときの後片付け(えふさん確定 2026-10-02・判断6)。自分で作った物に限り、`-f` を付けない `rm -r` と実在のパスの名指しで消し、消した物を報告に書く。**