@tyhld/conductor 0.12.78 → 0.12.80

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/README.md CHANGED
@@ -358,6 +358,12 @@ shellcheck -x --severity=warning scripts/*.sh sales-template/*.sh sales-template
358
358
  | `version` が公開 npm の `latest` より**新しい** | build → テスト → `npm publish`(公開 npm へ) |
359
359
  | 同じ・古い | **何もしない**(版を上げ忘れても事故りません) |
360
360
 
361
+ **版を上げる必要があるのは、配布物を変えたときだけです**(便 fb248f6e)。
362
+ 配布物=`package.json` の `files` に載る物(`README.md` も含む)と、`dist` の元(`src`・`tsconfig.json`)、`package.json`・`package-lock.json`。
363
+ `docs/`・`test/`・`.github/` だけの変更は版を上げずに緑で合流でき、合流しても公開は「出しません」で何も起きません。
364
+ 配布物を変えたのに版がそのままだと、試験(`test/release-public-npm.test.ts` の「★空振り確認」)が赤で止めます。
365
+ くらべる相手は公開 npm の `latest` を出したコミット(`gitHead`)で、それが確かめられないときは安全側(版上げを求める)に倒れます。
366
+
361
367
  ★**人が `npm publish` を打ってはいけません。**
362
368
 
363
369
  <details><summary>なぜ手で打たないのか(実測 2026-09-04)</summary>
package/dist/headless.js CHANGED
@@ -273,6 +273,53 @@ export function validateActiveJobOwnership(rec, cfg) {
273
273
  // PATH に居ないため spawn('claude') が ENOENT で失敗する(実測 2026-10-01)。
274
274
  // conductor-run が node を探すのと同じ考え方で、既知の場所を順に探す。
275
275
  // ─────────────────────────────────────────────────────────────────────────────
276
+ // ── Claude Code の版管理 ─────────────────────────────────────────────────
277
+ //
278
+ // 1台に複数の Claude Code が入っていると、古い方を使って通信の許可 "*" が
279
+ // 効かない等の不具合が起きる(実測 2026-10-02 メインPC)。
280
+ // 候補のうち実在する物の版を確かめ、いちばん新しい版を使う。
281
+ // ─────────────────────────────────────────────────────────────────────────
282
+ /**
283
+ * 必要な最低の版。これより古い Claude Code では職人を起動しない。
284
+ *
285
+ * 根拠: Claude Code 2.1.x 系で画面なし方式(-p, --output-format stream-json)と
286
+ * 通信の許可 "*"(allowedTools: "*")が動くことが確かめられたのは太田さんの PC の
287
+ * 2.1.286(便 bcce7be9)。2.1.86 では "*" が効かない疑い(便 a9754912、メインPC)。
288
+ * 安全側に倒して 2.1.200 を最低とする(200 未満は "*" 対応が不明確な版)。
289
+ * ★確かめられた版は 2.1.286 だけなので、200 は推定の境界。
290
+ */
291
+ export const CLAUDE_MIN_VERSION = '2.1.200';
292
+ /** claude --version の出力から版番号を取り出す。取れなければ null。 */
293
+ export function parseClaudeVersion(output) {
294
+ // "2.1.286 (Claude Code)" → "2.1.286"
295
+ const m = output.match(/(\d+\.\d+\.\d+)/);
296
+ return m ? m[1] : null;
297
+ }
298
+ /** 版番号を比べる。a > b なら正、a < b なら負、同じなら 0。 */
299
+ export function compareVersions(a, b) {
300
+ const pa = a.split('.').map(Number);
301
+ const pb = b.split('.').map(Number);
302
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
303
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
304
+ if (diff !== 0)
305
+ return diff;
306
+ }
307
+ return 0;
308
+ }
309
+ /** claude の実行ファイルから版番号を取得する。取れなければ null。 */
310
+ export function getClaudeVersion(binPath) {
311
+ try {
312
+ const out = execFileSync(binPath, ['--version'], {
313
+ timeout: 5_000,
314
+ encoding: 'utf8',
315
+ stdio: ['ignore', 'pipe', 'ignore'],
316
+ });
317
+ return parseClaudeVersion(out);
318
+ }
319
+ catch {
320
+ return null;
321
+ }
322
+ }
276
323
  /** claude の実行ファイルの既知の候補を返す(新しい node の版から順)。 */
277
324
  function claudeBinCandidates() {
278
325
  const h = os.homedir();
@@ -307,27 +354,42 @@ function claudeBinCandidates() {
307
354
  return candidates;
308
355
  }
309
356
  /**
310
- * claude の実行ファイルのフルパスを返す。見つからなければ null。
357
+ * claude の実行ファイルのフルパスと版を返す。見つからなければ null。
311
358
  *
312
359
  * 探す順:
313
- * 1. 環境変数 CONDUCTOR_CLAUDE_BIN(テスト用の差し替え口)
314
- * 2. nvm の各 node 版の bin/claude(新しい版から)
315
- * 3. ~/.local/bin/claude
316
- * 4. ~/.claude/local/claude
317
- * 5. /usr/local/bin/claude, /usr/bin/claude
360
+ * 1. 環境変数 CONDUCTOR_CLAUDE_BIN(テスト用の差し替え口)→ 指定があればそれだけ
361
+ * 2. 候補のうち実在する物の版を確かめ、いちばん新しい版を使う
362
+ * 3. 版を確かめられない物は後回し(版が分かる物が1つもないときだけ使う)
318
363
  */
319
364
  export function resolveClaudeBin() {
320
365
  const override = process.env.CONDUCTOR_CLAUDE_BIN;
321
366
  if (override) {
322
- if (existsSync(override))
323
- return override;
367
+ if (!existsSync(override))
368
+ return null;
369
+ return { bin: override, version: getClaudeVersion(override) };
370
+ }
371
+ const existing = claudeBinCandidates().filter(c => existsSync(c));
372
+ if (existing.length === 0)
324
373
  return null;
374
+ // 版が分かる候補と分からない候補を分ける
375
+ const withVersion = [];
376
+ const withoutVersion = [];
377
+ for (const bin of existing) {
378
+ const ver = getClaudeVersion(bin);
379
+ if (ver) {
380
+ withVersion.push({ bin, version: ver });
381
+ }
382
+ else {
383
+ withoutVersion.push(bin);
384
+ }
325
385
  }
326
- for (const candidate of claudeBinCandidates()) {
327
- if (existsSync(candidate))
328
- return candidate;
386
+ // 版が分かる候補の中でいちばん新しい物を選ぶ
387
+ if (withVersion.length > 0) {
388
+ withVersion.sort((a, b) => compareVersions(b.version, a.version));
389
+ return withVersion[0];
329
390
  }
330
- return null;
391
+ // 版を確かめられない物だけ → 最初の候補を返す(後方互換)
392
+ return { bin: withoutVersion[0], version: null };
331
393
  }
332
394
  // ─────────────────────────────────────────────────────────────────────────────
333
395
  // 秘密のマスク
@@ -724,15 +786,26 @@ function closeLog(ws) {
724
786
  * 【stdout】stream-json 形式。行ごとに JSON を出す。readline で1行ずつ読む。
725
787
  * 【stderr】蓄積して認証エラーの検出に使う。
726
788
  *
727
- * 【起動失敗】claude が見つからない(ENOENT)・実行権限がない(EACCES)などで
728
- * spawn が失敗した場合、null を返す。呼び出し側はエラーイベントを管制へ送る。
789
+ * 【起動失敗】claude が見つからない(ENOENT)・実行権限がない(EACCES)・
790
+ * 版が古すぎるなどで起動しない場合、null を返す。呼び出し側はエラーイベントを管制へ送る。
791
+ * 失敗の理由は lastSpawnError に入る。
729
792
  */
793
+ export let lastSpawnError = '';
730
794
  export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
731
- const claudeBin = resolveClaudeBin();
732
- if (claudeBin === null) {
733
- log(`ERROR claude が見つかりません。探した場所: nvm の各版の bin, ~/.local/bin, ~/.claude/local, /usr/local/bin, /usr/bin`);
795
+ const resolved = resolveClaudeBin();
796
+ if (resolved === null) {
797
+ lastSpawnError = 'claude が見つかりません。探した場所: nvm の各版の bin, ~/.local/bin, ~/.claude/local, /usr/local/bin, /usr/bin';
798
+ log(`ERROR ${lastSpawnError}`);
799
+ return null;
800
+ }
801
+ const { bin: claudeBin, version: claudeVersion } = resolved;
802
+ // ── 最低版チェック ──
803
+ if (claudeVersion && compareVersions(claudeVersion, CLAUDE_MIN_VERSION) < 0) {
804
+ lastSpawnError = `Claude Code が古すぎます(${claudeBin} = ${claudeVersion}、最低 ${CLAUDE_MIN_VERSION})。npm install -g @anthropic-ai/claude-code で更新してください`;
805
+ log(`ERROR ${lastSpawnError}`);
734
806
  return null;
735
807
  }
808
+ log(`claude bin=${claudeBin} version=${claudeVersion ?? 'unknown'}`);
736
809
  const env = { ...process.env };
737
810
  delete env.CLAUDECODE;
738
811
  delete env.ANTHROPIC_API_KEY;
@@ -794,6 +867,8 @@ export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
794
867
  sessionId,
795
868
  startedAt: Date.now(),
796
869
  attempt,
870
+ claudeBin,
871
+ claudeVersion,
797
872
  completed: false,
798
873
  exitCode: null,
799
874
  exitSignal: null,
@@ -1142,6 +1217,8 @@ async function checkRunningJob(cfg, state) {
1142
1217
  startedAt: new Date(job.startedAt).toISOString(),
1143
1218
  lastActivityAt: new Date(job.lastActivityAt).toISOString(),
1144
1219
  ...(job.process.pid !== undefined ? { pid: job.process.pid } : {}),
1220
+ claudeBin: job.claudeBin,
1221
+ ...(job.claudeVersion ? { claudeVersion: job.claudeVersion } : {}),
1145
1222
  });
1146
1223
  }
1147
1224
  // イベントバッファのフラッシュ
@@ -1238,7 +1315,7 @@ export async function restoreHeadlessOrphans(cfg, state) {
1238
1315
  const cmd = { id: pick.id, body: pick.body };
1239
1316
  const job = spawnClaudeProcess(cfg, cmd, sessionId, attempt, true);
1240
1317
  if (job === null) {
1241
- await patchStatus(cfg, pick.id, 'failed', 'claude が見つかりません(手元の孤児復元を中止)');
1318
+ await patchStatus(cfg, pick.id, 'failed', `${lastSpawnError || 'claude が見つかりません'}(手元の孤児復元を中止)`);
1242
1319
  state.relay.abandoned.add(pick.id);
1243
1320
  log(`ERROR site=${cfg.site} id=${pick.id} 手元の孤児復元: claude が見つからない → 放棄`);
1244
1321
  return;
@@ -1296,10 +1373,10 @@ export async function restoreHeadlessOrphans(cfg, state) {
1296
1373
  const cmd = { id: pick.id, body: pick.body };
1297
1374
  const job = spawnClaudeProcess(cfg, cmd, sessionId, attempt, true);
1298
1375
  if (job === null) {
1299
- await patchStatus(cfg, pick.id, 'failed', 'claude が見つかりません(孤児復元を中止)');
1376
+ await patchStatus(cfg, pick.id, 'failed', `${lastSpawnError || 'claude が見つかりません'}(孤児復元を中止)`);
1300
1377
  await sendCommandEvents(cfg, pick.id, [{
1301
1378
  type: 'error',
1302
- text: 'claude が見つかりません(孤児復元を中止)',
1379
+ text: `${lastSpawnError || 'claude が見つかりません'}(孤児復元を中止)`,
1303
1380
  at: new Date().toISOString(),
1304
1381
  attempt,
1305
1382
  }]);
@@ -1426,10 +1503,10 @@ export async function headlessTick(cfg, state, hooks) {
1426
1503
  : sessionIdForCommand(retry.cmd.id, retry.attempt);
1427
1504
  const job = spawnClaudeProcess(cfg, retry.cmd, sessionId, retry.attempt, isResume);
1428
1505
  if (job === null) {
1429
- await patchStatus(cfg, retry.cmd.id, 'failed', 'claude が見つかりません(自動再開を中止)');
1506
+ await patchStatus(cfg, retry.cmd.id, 'failed', `${lastSpawnError || 'claude が見つかりません'}(自動再開を中止)`);
1430
1507
  await sendCommandEvents(cfg, retry.cmd.id, [{
1431
1508
  type: 'error',
1432
- text: 'claude が見つかりません(自動再開を中止)',
1509
+ text: `${lastSpawnError || 'claude が見つかりません'}(自動再開を中止)`,
1433
1510
  at: new Date().toISOString(),
1434
1511
  attempt: retry.attempt,
1435
1512
  }]);
@@ -1483,10 +1560,10 @@ export async function headlessTick(cfg, state, hooks) {
1483
1560
  const job = spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume);
1484
1561
  if (job === null) {
1485
1562
  // ★起動失敗を管制へ知らせる(「起動しました」とは言わない)
1486
- await patchStatus(cfg, cmd.id, 'failed', 'claude が見つかりません(起動できませんでした)');
1563
+ await patchStatus(cfg, cmd.id, 'failed', `${lastSpawnError || 'claude が見つかりません'}(起動できませんでした)`);
1487
1564
  await sendCommandEvents(cfg, cmd.id, [{
1488
1565
  type: 'error',
1489
- text: 'claude が見つかりません(起動できませんでした)',
1566
+ text: `${lastSpawnError || 'claude が見つかりません'}(起動できませんでした)`,
1490
1567
  at: new Date().toISOString(),
1491
1568
  attempt,
1492
1569
  }]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.78",
3
+ "version": "0.12.80",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -152,7 +152,9 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
152
152
  - **秘密のファイルを読む・引数に名前を書く**(`cat ~/.npmrc`・`--exclude=.npmrc`)→ 止まる。**中身は見に行かない。** 差分は対象を名指しで `git diff -- src docs`、全体で見たいときは除いて `git diff -- . ':(exclude).npmrc'`(`git diff` は中身を読み出す動詞ではないので、この形は通る・鉄則1)。
153
153
  - **標準エラーの捨て先を、あとで文字列の途中に差し込む変数の中に入れる**(`S="$(何か 2>/dev/null)"` と置き、別の行で `"repos/<その変数>/branches/…"` のように使う)→ 止まる。番人は変数を字面で展開してから測り直すので、捨て先が `/dev/null` ではなく**その後ろのパスが貼り付いた別の名前**として取り出され、`/dev` 直下への書き込みに見える(実測 2026-09-27。鉄則7の塊がこれで6便すべて止まっていた)。**文句を捨てる所を、小さな関数1か所に寄せる。**
154
154
  - 通る例: 先に `q() { "$@" 2>/dev/null; }` を置き、以後は `S="$(q 何か)"` と書く(★変数の値に捨て先が入らないので、どこに差し込んでも止まらない)。
155
- - **中身が変わる場所を指す再帰の `rm`**(`rm -rf "$DIR/x"`・`rm -rf build/*` のように変数や `*` を含む)→ 止まる。**消す先を実在のパスで名指しする**(例 `rm -rf /tmp/claude-1000/作業名`・鉄則13)。
155
+ - **`-f` 付き、または中身が変わる場所を指す再帰の `rm`**(`rm -rf`・`rm -fr`・`rm -r -f`・`rm -R`・`rm --recursive`/`rm -r "$DIR/x"`・`rm -r build/*` のように変数や `*` を含む)→ 止まる。`-f` 付きは配布している禁止の一覧(現場の `.claude/settings.json` の deny)で**必ず**止まる(番人より先に効く=消す先が作業ファイルでも通らない・便 fb248f6e)。変数・`*` は番人が止める。**`-f` を付けず、消す先を実在のパスで名指しする**(`/tmp/claude-1000/` の下・鉄則13)。★止められたら**別の消し方を探さず**(`find -delete`・`python -c` 等に回らない)、消せなかった場所を報告に書く(鉄則4)。
156
+ - 通る例: `rm -r /tmp/claude-1000/作業名` / `rm -r node_modules/.tmp-作業名`(★`-f` を付けない・自分で作った物だけ)
157
+ - 止まる例: `rm -rf /tmp/claude-1000/作業名`(禁止の一覧)/ `rm -r "$DIR/x"`・`rm -r build/*`(番人)/ `rm -r .git`・`rm -r .claude/hooks`・`rm -r .tyhld`(番人=相対パスでも消せない)
156
158
  - **作業場所が変数のままの `git push`**(`git -C "$DIR" push`・行き先のブランチを確かめられない形)→ 止まる。**リポの中で、ブランチを名指しして** `git push -u origin <作業ブランチ名>`。
157
159
 
158
160
  ## 便の終え方