@tyhld/conductor 0.12.78 → 0.12.79
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/headless.js +101 -24
- package/package.json +1 -1
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
|
|
357
|
+
* claude の実行ファイルのフルパスと版を返す。見つからなければ null。
|
|
311
358
|
*
|
|
312
359
|
* 探す順:
|
|
313
|
-
* 1. 環境変数 CONDUCTOR_CLAUDE_BIN
|
|
314
|
-
* 2.
|
|
315
|
-
* 3.
|
|
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
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
386
|
+
// 版が分かる候補の中でいちばん新しい物を選ぶ
|
|
387
|
+
if (withVersion.length > 0) {
|
|
388
|
+
withVersion.sort((a, b) => compareVersions(b.version, a.version));
|
|
389
|
+
return withVersion[0];
|
|
329
390
|
}
|
|
330
|
-
|
|
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
|
-
*
|
|
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
|
|
732
|
-
if (
|
|
733
|
-
|
|
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
|
}]);
|