@tyhld/conductor 0.12.80 → 0.12.82
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 +13 -0
- package/dist/cli.js +7 -0
- package/dist/headless.js +54 -13
- package/dist/relay.js +169 -11
- package/package.json +1 -1
- package/sales-template/install.sh +187 -0
- package/sales-template/setup.sh +3 -1
- package/scripts/hooks/test_policy.py +11 -0
- package/scripts/update-guard.sh +211 -0
package/README.md
CHANGED
|
@@ -106,6 +106,19 @@ APPLY=1 bash scripts/update-allowed-domains.sh # 実際に配る
|
|
|
106
106
|
これを流さないと職人の動きは変わりません(`update-guard.sh ①-d`。同一なら何もしません)。
|
|
107
107
|
- **`setup.sh` からは呼びません**。顧客機の初期設置に大きなダウンロードを足すと設置そのものが止まるためです(ADR-010)。
|
|
108
108
|
|
|
109
|
+
### 職人が始まるのに要る物(先頭の確かめ・0.12.82〜)
|
|
110
|
+
|
|
111
|
+
1行インストーラ(`sales-template/install.sh`)と `scripts/update-guard.sh` は、**設定を配る前に**次を確かめ、足りなければ止まって「人が打つ1行」を出します。★台本は自分で `sudo` を打たず、何も入れません。
|
|
112
|
+
|
|
113
|
+
- **職人のソフト(Claude Code)が 2.1.282 以上**(えふさん確定 2026-10-02)。中継と同じ場所を同じ順で探し、いちばん新しい版で判定します。上げ方は入れ方で違います。
|
|
114
|
+
- ネイティブ導入(`~/.local/bin/claude`)… `~/.local/bin/claude update`
|
|
115
|
+
- npm 導入(nvm の bin)… `npm install -g @anthropic-ai/claude-code@latest`
|
|
116
|
+
- **囲いの部品(Linux・WSL)**: `bwrap`(bubblewrap)と `socat`。無いと職人が安全のため起動を断ります(設定 `sandbox.failIfUnavailable`)。
|
|
117
|
+
- `sudo apt-get install -y bubblewrap socat`
|
|
118
|
+
- sudo のパスワードが分からないとき(WSL・Windows の PowerShell で): `wsl.exe -u root apt-get install -y bubblewrap socat`
|
|
119
|
+
- Mac は囲いが OS に組み込みなので求めません。分からない機は止めずに「確かめられませんでした」と出します。
|
|
120
|
+
- 確認だけ(`bash scripts/update-guard.sh`)のときは、職人が管制へ知らせる口(ai-kana の MCP)がつながっているかも1行で出します(止めません)。
|
|
121
|
+
|
|
109
122
|
---
|
|
110
123
|
|
|
111
124
|
## 使い方
|
package/dist/cli.js
CHANGED
|
@@ -195,6 +195,9 @@ async function sendReport(cfg) {
|
|
|
195
195
|
body.undeliveredSec = d.undeliveredSec;
|
|
196
196
|
if (d.queuedCommandId !== null)
|
|
197
197
|
body.queuedCommandId = d.queuedCommandId;
|
|
198
|
+
// ★配った便の完了待ちの長さ('waiting-done' のときだけ)。管制が長すぎる待ちを見つけられる。
|
|
199
|
+
if (d.waitingSec !== null)
|
|
200
|
+
body.waitingSec = d.waitingSec;
|
|
198
201
|
}
|
|
199
202
|
// ★未処理の報告(まだ管制へ渡せていない控え)の指示IDを載せる(便 57c04e6d)。
|
|
200
203
|
// 1件も無ければ載せない=今までと同じ本文。多すぎるときは上限までにして総数を添える。
|
|
@@ -348,6 +351,10 @@ async function start(args) {
|
|
|
348
351
|
}
|
|
349
352
|
nudgeWatcher = watchNudges(cfg.site, () => {
|
|
350
353
|
console.log(`[relay] site=${cfg.site} 耳から合図 → 指示の取得を前倒しします`);
|
|
354
|
+
// ★合図が空振りしたときに理由を1行出せるよう、受けた時刻を relay へ渡す(案B-3)。
|
|
355
|
+
// relayTick が「前の周が走行中」「追跡中」で return するとき、しぼり付きで理由を記録する。
|
|
356
|
+
if (relayStateForReport)
|
|
357
|
+
relayStateForReport.nudgedAt = Date.now();
|
|
351
358
|
void relayRunner?.run();
|
|
352
359
|
});
|
|
353
360
|
}
|
package/dist/headless.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { execFileSync, spawn } from 'node:child_process';
|
|
18
18
|
import { createInterface } from 'node:readline';
|
|
19
|
-
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import os from 'node:os';
|
|
22
22
|
import { CENTRAL_BUSY_HOLD_MAX_MS, RELAY_DELIVER_MAX, RELAY_FETCH_TIMEOUT_MS, RELAY_HEARTBEAT_INTERVAL_MS, RELAY_RESTORE_ORPHANS_ENABLED, buildAnchor, buildSendText, canDeliver, completeWithReport, createRelayState, deliveryReport, fetchCommand, fetchCommandStatus, fetchControl, fetchOrphans, isTerminalCommandStatus, kindOf, markDelivered, noteCentralDown, noteCentralUp, noteReadiness, patchStatus, recoverStuckProcessedReports, reconnectDelayMs, selectOrphanToRestore, sendHeartbeat, } from './relay.js';
|
|
@@ -282,13 +282,13 @@ export function validateActiveJobOwnership(rec, cfg) {
|
|
|
282
282
|
/**
|
|
283
283
|
* 必要な最低の版。これより古い Claude Code では職人を起動しない。
|
|
284
284
|
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
285
|
+
* えふさん確定(2026-10-02・判断1): 2.1.282。
|
|
286
|
+
* 根拠: 公式文書(code.claude.com/docs/en/managed-settings)が、管理設定の安全側の
|
|
287
|
+
* 決まりに Claude Code v2.1.282 以降を求めている。通信の許可 "*" が効くと実測できたのは
|
|
288
|
+
* 2.1.286(便 bcce7be9)。2.1.86 では効かない疑い(便 a9754912、メインPC)。
|
|
289
|
+
* ★scripts/preflight.sh の CLAUDE_MIN_VERSION も同じ値にする(試験で一致を固定)。
|
|
290
290
|
*/
|
|
291
|
-
export const CLAUDE_MIN_VERSION = '2.1.
|
|
291
|
+
export const CLAUDE_MIN_VERSION = '2.1.282';
|
|
292
292
|
/** claude --version の出力から版番号を取り出す。取れなければ null。 */
|
|
293
293
|
export function parseClaudeVersion(output) {
|
|
294
294
|
// "2.1.286 (Claude Code)" → "2.1.286"
|
|
@@ -320,8 +320,42 @@ export function getClaudeVersion(binPath) {
|
|
|
320
320
|
return null;
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* 選ばれた claude の入れ方に合う「更新の1行」を返す(そのままコピペで動く形)。
|
|
325
|
+
*
|
|
326
|
+
* 入れ方は2通り在り、上げ方が違う(実測 2026-10-02 ノートPC: ネイティブ導入は
|
|
327
|
+
* npm の1行では上がらず、claude update で 2.1.70 → 2.1.287 になった)。
|
|
328
|
+
* - ネイティブ導入(~/.local/bin/claude → ~/.local/share/claude/versions/…、
|
|
329
|
+
* または ~/.claude/local/claude)… その claude で `update`
|
|
330
|
+
* - npm 導入(実体が node_modules/@anthropic-ai/claude-code の下、または nvm の bin)
|
|
331
|
+
* … その bin の npm で `install -g @anthropic-ai/claude-code@latest`
|
|
332
|
+
* - 見分けられない … 両方を書く
|
|
333
|
+
*/
|
|
334
|
+
export function claudeUpdateHint(bin) {
|
|
335
|
+
let real = bin;
|
|
336
|
+
try {
|
|
337
|
+
real = realpathSync(bin);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// 実体を辿れなくても、置き場の名前だけで見分ける
|
|
341
|
+
}
|
|
342
|
+
const h = os.homedir();
|
|
343
|
+
const npmLine = (dir) => `PATH="${dir}:$PATH" npm install -g @anthropic-ai/claude-code@latest`;
|
|
344
|
+
const isNpm = (p) => p.includes('/node_modules/@anthropic-ai/claude-code/') || p.includes('/.nvm/versions/node/');
|
|
345
|
+
const isNative = (p) => p.startsWith(path.join(h, '.local', 'share', 'claude') + '/') ||
|
|
346
|
+
p.startsWith(path.join(h, '.claude', 'local') + '/');
|
|
347
|
+
if (isNpm(real) || isNpm(bin)) {
|
|
348
|
+
return `npm 導入の更新: ${npmLine(path.dirname(bin))}`;
|
|
349
|
+
}
|
|
350
|
+
if (isNative(real) || isNative(bin)) {
|
|
351
|
+
return `ネイティブ導入の更新: "${bin}" update`;
|
|
352
|
+
}
|
|
353
|
+
return `入れ方を見分けられませんでした。ネイティブ導入なら "${bin}" update / npm 導入なら npm install -g @anthropic-ai/claude-code@latest`;
|
|
354
|
+
}
|
|
355
|
+
/** ホームの外に在る、claude の既知の置き場(既定の候補)。 */
|
|
356
|
+
export const CLAUDE_SYSTEM_BIN_DIRS = ['/usr/local/bin', '/usr/bin'];
|
|
323
357
|
/** claude の実行ファイルの既知の候補を返す(新しい node の版から順)。 */
|
|
324
|
-
function claudeBinCandidates() {
|
|
358
|
+
function claudeBinCandidates(systemDirs) {
|
|
325
359
|
const h = os.homedir();
|
|
326
360
|
const candidates = [];
|
|
327
361
|
// nvm の各 node 版の bin(新しい版から)
|
|
@@ -350,7 +384,7 @@ function claudeBinCandidates() {
|
|
|
350
384
|
}
|
|
351
385
|
}
|
|
352
386
|
// その他の既知の場所
|
|
353
|
-
candidates.push(path.join(h, '.local', 'bin', 'claude'), path.join(h, '.claude', 'local', 'claude'),
|
|
387
|
+
candidates.push(path.join(h, '.local', 'bin', 'claude'), path.join(h, '.claude', 'local', 'claude'), ...systemDirs.map(d => path.join(d, 'claude')));
|
|
354
388
|
return candidates;
|
|
355
389
|
}
|
|
356
390
|
/**
|
|
@@ -360,15 +394,18 @@ function claudeBinCandidates() {
|
|
|
360
394
|
* 1. 環境変数 CONDUCTOR_CLAUDE_BIN(テスト用の差し替え口)→ 指定があればそれだけ
|
|
361
395
|
* 2. 候補のうち実在する物の版を確かめ、いちばん新しい版を使う
|
|
362
396
|
* 3. 版を確かめられない物は後回し(版が分かる物が1つもないときだけ使う)
|
|
397
|
+
*
|
|
398
|
+
* systemDirs はホームの外の置き場(既定 /usr/local/bin・/usr/bin)。試験は機体に
|
|
399
|
+
* 依らないよう、ここを作り物のフォルダに差し替える。
|
|
363
400
|
*/
|
|
364
|
-
export function resolveClaudeBin() {
|
|
401
|
+
export function resolveClaudeBin(systemDirs = CLAUDE_SYSTEM_BIN_DIRS) {
|
|
365
402
|
const override = process.env.CONDUCTOR_CLAUDE_BIN;
|
|
366
403
|
if (override) {
|
|
367
404
|
if (!existsSync(override))
|
|
368
405
|
return null;
|
|
369
406
|
return { bin: override, version: getClaudeVersion(override) };
|
|
370
407
|
}
|
|
371
|
-
const existing = claudeBinCandidates().filter(c => existsSync(c));
|
|
408
|
+
const existing = claudeBinCandidates(systemDirs).filter(c => existsSync(c));
|
|
372
409
|
if (existing.length === 0)
|
|
373
410
|
return null;
|
|
374
411
|
// 版が分かる候補と分からない候補を分ける
|
|
@@ -792,6 +829,8 @@ function closeLog(ws) {
|
|
|
792
829
|
*/
|
|
793
830
|
export let lastSpawnError = '';
|
|
794
831
|
export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
|
|
832
|
+
// 前の失敗の理由を持ち越さない(理由の無い失敗に古い理由が付かないように)
|
|
833
|
+
lastSpawnError = '';
|
|
795
834
|
const resolved = resolveClaudeBin();
|
|
796
835
|
if (resolved === null) {
|
|
797
836
|
lastSpawnError = 'claude が見つかりません。探した場所: nvm の各版の bin, ~/.local/bin, ~/.claude/local, /usr/local/bin, /usr/bin';
|
|
@@ -801,7 +840,8 @@ export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
|
|
|
801
840
|
const { bin: claudeBin, version: claudeVersion } = resolved;
|
|
802
841
|
// ── 最低版チェック ──
|
|
803
842
|
if (claudeVersion && compareVersions(claudeVersion, CLAUDE_MIN_VERSION) < 0) {
|
|
804
|
-
|
|
843
|
+
// ★候補のうちいちばん新しい物でも最低版より古い=全部が古いときだけここに来る
|
|
844
|
+
lastSpawnError = `Claude Code が古すぎるため仕事を始めません(${claudeBin} = ${claudeVersion}、必要な版 ${CLAUDE_MIN_VERSION} 以上)。${claudeUpdateHint(claudeBin)}`;
|
|
805
845
|
log(`ERROR ${lastSpawnError}`);
|
|
806
846
|
return null;
|
|
807
847
|
}
|
|
@@ -851,7 +891,8 @@ export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
|
|
|
851
891
|
});
|
|
852
892
|
}
|
|
853
893
|
catch (err) {
|
|
854
|
-
|
|
894
|
+
lastSpawnError = `claude の起動に失敗しました (${claudeBin}): ${err instanceof Error ? err.message : String(err)}`;
|
|
895
|
+
log(`ERROR ${lastSpawnError}`);
|
|
855
896
|
return null;
|
|
856
897
|
}
|
|
857
898
|
const logs = openRunLogs(cmd.id, attempt);
|
package/dist/relay.js
CHANGED
|
@@ -239,6 +239,8 @@ export function createRelayState() {
|
|
|
239
239
|
abandoned: new Set(),
|
|
240
240
|
promptPending: false,
|
|
241
241
|
busyRaisedId: null,
|
|
242
|
+
nudgedAt: null,
|
|
243
|
+
lastNudgeLogAt: null,
|
|
242
244
|
central: { failures: 0, detail: '', downSince: null },
|
|
243
245
|
readiness: createReadiness(),
|
|
244
246
|
};
|
|
@@ -277,12 +279,58 @@ export function noteReadiness(state, next, commandId, now = Date.now()) {
|
|
|
277
279
|
export function deliveryReport(state, now = Date.now()) {
|
|
278
280
|
const r = state.readiness;
|
|
279
281
|
const blocked = BLOCKED_STATES.has(r.state) && r.commandId !== null;
|
|
282
|
+
const waiting = r.state === 'waiting-done' && r.commandId !== null;
|
|
280
283
|
return {
|
|
281
284
|
deliveryState: r.state,
|
|
282
285
|
queuedCommandId: blocked ? r.commandId : null,
|
|
283
286
|
undeliveredSec: blocked ? Math.max(0, Math.round((now - r.since) / 1000)) : null,
|
|
287
|
+
waitingSec: waiting ? Math.max(0, Math.round((now - r.since) / 1000)) : null,
|
|
284
288
|
};
|
|
285
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* 配った便の完了を待っている1周を記録する(★無言の周をなくす・案B-2)。
|
|
292
|
+
*
|
|
293
|
+
* 【なぜ要るか】追跡中(pending)の周は、理由を1行も書かずに return する経路が複数あった。
|
|
294
|
+
* `noteBusyEdge` は【変わり目だけ】出す設計なので、同じ状態が続く限り何も出ない。
|
|
295
|
+
* 「作業中の可能性ありスキップ」のログは pending が無い周にしか到達しない。
|
|
296
|
+
* =実機 2026-10-02 は、空の入力待ち画面を「作業中」と誤読したまま10分以上、
|
|
297
|
+
* 現場のログに1行も出なかった。**この1行があれば即座に原因が分かった。**
|
|
298
|
+
*
|
|
299
|
+
* 【★画面をどう読んだかを必ず書く】これが要。「経過」だけでは誤読に気づけない。
|
|
300
|
+
* 【★記録を埋めない】生存通知と同じしぼり(RELAY_HEARTBEAT_INTERVAL_MS=150秒)に相乗りする。
|
|
301
|
+
*
|
|
302
|
+
* @returns 実際に1行出したら true(しぼりで出さなかったら false)。
|
|
303
|
+
*/
|
|
304
|
+
export function noteWaitingDone(state, site, screen, askedCentral, logLine, now = Date.now()) {
|
|
305
|
+
const p = state.pending;
|
|
306
|
+
if (p === null)
|
|
307
|
+
return false;
|
|
308
|
+
const last = p.lastWaitLogAt ?? null;
|
|
309
|
+
if (last !== null && now - last < RELAY_HEARTBEAT_INTERVAL_MS)
|
|
310
|
+
return false;
|
|
311
|
+
p.lastWaitLogAt = now;
|
|
312
|
+
logLine(`site=${site} id=${p.id} 完了待ち(経過${Math.max(0, Math.round((now - p.sentAt) / 1000))}s` +
|
|
313
|
+
`・画面=${screen}・中央へ問い合わせ=${askedCentral ? 'した' : 'しない'})`);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* 合図(耳からの「今すぐ1周」)を受けたのに取得へ進まなかった理由を1行出す(★案B-3)。
|
|
318
|
+
* ★しぼり(RELAY_HEARTBEAT_INTERVAL_MS)付き=記録を埋めない。出したら合図は消費する。
|
|
319
|
+
*
|
|
320
|
+
* @returns 実際に1行出したら true。
|
|
321
|
+
*/
|
|
322
|
+
export function noteNudgeSkipped(state, site, reason, logLine, now = Date.now()) {
|
|
323
|
+
if (state.nudgedAt === null)
|
|
324
|
+
return false;
|
|
325
|
+
state.nudgedAt = null; // 合図は1回ぶん。消費して次の合図を待つ。
|
|
326
|
+
if (state.lastNudgeLogAt !== null &&
|
|
327
|
+
now - state.lastNudgeLogAt < RELAY_HEARTBEAT_INTERVAL_MS) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
state.lastNudgeLogAt = now;
|
|
331
|
+
logLine(`site=${site} 合図を受けましたが${reason}のため、この周では指示を取得しません`);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
286
334
|
/**
|
|
287
335
|
* 管制へつながらないときの、次の再試行までの待ち時間(ms)。純粋関数(I/Oなし=単体テスト可能)。
|
|
288
336
|
*
|
|
@@ -425,6 +473,16 @@ export const CODEX_MARKERS = [
|
|
|
425
473
|
export const CODEX_BUSY_MARKERS = ['esc to interrupt', 'working ('];
|
|
426
474
|
/** Codex の選択メニューのカーソル(実測: U+203A)。★Claude の `❯`(U+276F) とは別物。 */
|
|
427
475
|
export const CODEX_MENU_CURSOR = '›';
|
|
476
|
+
/**
|
|
477
|
+
* Codex の「待機」に要求する、入力欄の行の位置(末尾から何行以内か)。
|
|
478
|
+
*
|
|
479
|
+
* 【根拠(実測)】0.160.0 の起動画面は末尾の空行を落とすと
|
|
480
|
+
* `› Ask Codex to do anything` / 空行 / `GPT-6.1-Sol default · ~/codex-probe` / `? for shortcuts`
|
|
481
|
+
* =入力欄は末尾から4行目。0.152.1 の待機画面は末尾から2〜3行目(test/relay-agent-kind.test.ts)。
|
|
482
|
+
* 余裕を見て8行に取る。★画面を覆う表示(警告・全画面のダイアログ)では入力欄が
|
|
483
|
+
* 1行も見えないので、何行に取っても弾ける=この値は「見えている待機」を取りこぼさないための幅。
|
|
484
|
+
*/
|
|
485
|
+
export const CODEX_IDLE_INPUT_WITHIN_LINES = 8;
|
|
428
486
|
/**
|
|
429
487
|
* 種類ごとの目印を返す。★関数にしてあるのは、定数の宣言順に縛られないようにするため
|
|
430
488
|
* (CLAUDE_MARKERS などはこの下で宣言される)。
|
|
@@ -434,29 +492,52 @@ export function agentProfile(kind) {
|
|
|
434
492
|
return {
|
|
435
493
|
markers: CODEX_MARKERS,
|
|
436
494
|
busyMarkers: CODEX_BUSY_MARKERS,
|
|
495
|
+
// ★Codex は回転印を見ない(起動画面の点字ロゴを作業中と誤読する=調査 67f8c51f)。
|
|
496
|
+
busyGlyphs: [],
|
|
437
497
|
cursor: CODEX_MENU_CURSOR,
|
|
438
498
|
idleRule: 'not-busy',
|
|
499
|
+
idleInputLineWithin: CODEX_IDLE_INPUT_WITHIN_LINES,
|
|
439
500
|
};
|
|
440
501
|
}
|
|
441
502
|
return {
|
|
442
503
|
markers: CLAUDE_MARKERS,
|
|
443
504
|
busyMarkers: BUSY_MARKERS,
|
|
505
|
+
// ★claude は従来どおり BUSY_GLYPHS そのまま(判定を1文字も変えない)。
|
|
506
|
+
busyGlyphs: BUSY_GLYPHS,
|
|
444
507
|
cursor: '❯',
|
|
445
508
|
idleRule: 'prompt',
|
|
509
|
+
idleInputLineWithin: null,
|
|
446
510
|
};
|
|
447
511
|
}
|
|
448
512
|
/** 種類を見て「作業中か」を判定する。claude は従来の hasBusySignal と同一。 */
|
|
449
513
|
export function hasBusySignalFor(text, kind) {
|
|
450
514
|
if (kind === 'claude')
|
|
451
515
|
return hasBusySignal(text);
|
|
516
|
+
const profile = agentProfile(kind);
|
|
452
517
|
const regionText = busyScanRegion(text);
|
|
453
518
|
const lower = regionText.toLowerCase();
|
|
454
|
-
if (
|
|
519
|
+
if (profile.busyMarkers.some((m) => lower.includes(m)))
|
|
455
520
|
return true;
|
|
456
|
-
|
|
521
|
+
// ★回転印は種類ごとの表から引く(Codex は空=見ない)。従来は BUSY_GLYPHS を共有していた。
|
|
522
|
+
if (profile.busyGlyphs.some((g) => regionText.includes(g)))
|
|
457
523
|
return true;
|
|
458
524
|
return false;
|
|
459
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* 「入力欄の行(cursor で始まる最後の行)が末尾から within 行以内に在るか」を返す純粋関数。
|
|
528
|
+
* ★枠の装飾(│ など)は剥がしてから見る。末尾の空行は数えない(tmux は下を空行で埋める)。
|
|
529
|
+
*/
|
|
530
|
+
export function hasInputLineNearBottom(text, cursor, within) {
|
|
531
|
+
const lines = text.replace(/\r/g, '').split('\n');
|
|
532
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '')
|
|
533
|
+
lines.pop();
|
|
534
|
+
const from = Math.max(0, lines.length - within);
|
|
535
|
+
for (let i = lines.length - 1; i >= from; i--) {
|
|
536
|
+
if (lines[i].replace(/^[│|╎┃\s]+/, '').startsWith(cursor))
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
460
541
|
/**
|
|
461
542
|
* 画面テキストから「アイドルか」を判定する純粋関数(判定(a))。
|
|
462
543
|
* 方針:「確実な処理中シグナル(esc to interrupt / compacting / スピナーグリフ)が無く、
|
|
@@ -698,12 +779,21 @@ export function looksLikeAgent(text, kind) {
|
|
|
698
779
|
export function looksIdleFor(text, kind) {
|
|
699
780
|
if (kind === 'claude')
|
|
700
781
|
return looksIdle(text);
|
|
782
|
+
const profile = agentProfile(kind);
|
|
701
783
|
if (hasBusySignalFor(text, kind))
|
|
702
784
|
return false;
|
|
703
785
|
if (hasChoiceMenuFor(text, kind))
|
|
704
786
|
return false;
|
|
705
787
|
// ★その窓が Codex だと分かるときだけ「待機」と言い切る。分からない窓は待機にしない。
|
|
706
|
-
|
|
788
|
+
if (!looksLikeAgent(text, kind))
|
|
789
|
+
return false;
|
|
790
|
+
// ★入力欄が末尾付近に見えることを必須にする(案C-2・AgentProfile.idleInputLineWithin 参照)。
|
|
791
|
+
// 画面を覆う表示(0.160.0 の警告の表示など)は入力欄を隠すので、ここで弾ける。
|
|
792
|
+
if (profile.idleInputLineWithin !== null &&
|
|
793
|
+
!hasInputLineNearBottom(text, profile.cursor, profile.idleInputLineWithin)) {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
return true;
|
|
707
797
|
}
|
|
708
798
|
/**
|
|
709
799
|
* 画面テキストに「完了マーカー DONE:{site} が単独で出力されているか」を判定する純粋関数。
|
|
@@ -2894,6 +2984,7 @@ export function startKeepalive(state, pending, now) {
|
|
|
2894
2984
|
anchor: pending.anchor,
|
|
2895
2985
|
startedAt: now,
|
|
2896
2986
|
lastHeartbeatAt: null,
|
|
2987
|
+
lastCentralProbeAt: null,
|
|
2897
2988
|
screenDoneUsable: true,
|
|
2898
2989
|
};
|
|
2899
2990
|
return true;
|
|
@@ -2916,8 +3007,12 @@ export function decideKeepalive(input) {
|
|
|
2916
3007
|
return 'wait';
|
|
2917
3008
|
if (input.doneSeen)
|
|
2918
3009
|
return 'done';
|
|
2919
|
-
if (input.alive)
|
|
3010
|
+
if (input.alive) {
|
|
3011
|
+
// ★alive が続いていても、しぼりが空いたら中央へ答え合わせする(画面の誤読から必ず抜ける道)。
|
|
3012
|
+
if (input.centralProbeOk === true)
|
|
3013
|
+
return 'alive-probe';
|
|
2920
3014
|
return input.throttleOk ? 'heartbeat' : 'wait';
|
|
3015
|
+
}
|
|
2921
3016
|
if (input.idle)
|
|
2922
3017
|
return 'probe';
|
|
2923
3018
|
return 'wait';
|
|
@@ -2970,9 +3065,34 @@ export async function keepaliveTick(cfg, state, deps = {}) {
|
|
|
2970
3065
|
alive: cap !== null && (kaBusySignal || kaMenu),
|
|
2971
3066
|
idle: cap !== null && looksIdleFor(cap, kindOf(cfg)),
|
|
2972
3067
|
throttleOk: ka.lastHeartbeatAt === null || now - ka.lastHeartbeatAt >= RELAY_HEARTBEAT_INTERVAL_MS,
|
|
3068
|
+
// ★中央への答え合わせのしぼり。生存通知と同じ150秒(RELAY_HEARTBEAT_INTERVAL_MS)。
|
|
3069
|
+
centralProbeOk: ka.lastCentralProbeAt === null || now - ka.lastCentralProbeAt >= RELAY_HEARTBEAT_INTERVAL_MS,
|
|
2973
3070
|
});
|
|
2974
3071
|
if (action === 'wait')
|
|
2975
3072
|
return;
|
|
3073
|
+
if (action === 'alive-probe') {
|
|
3074
|
+
// ★画面は「作業中」に見えるが、中央にも必ず聞く(画面の誤読から抜ける唯一の道・案A-2)。
|
|
3075
|
+
ka.lastCentralProbeAt = now;
|
|
3076
|
+
const alive = await getStatus(cfg, ka.id);
|
|
3077
|
+
if (isCentralFinished(alive)) {
|
|
3078
|
+
logLine(`site=${cfg.site} id=${ka.id} 見張り終了: 中央で終端(status=${alive})を確認` +
|
|
3079
|
+
'(画面は作業中に見えたが中央の真実で畳む)');
|
|
3080
|
+
noteBusyEdge(state, cfg.site, ka.id, false, logLine);
|
|
3081
|
+
state.keepalive = null;
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
// 終端でない=従来の 'heartbeat' と同じ扱い(手は挙げたまま生存通知)。しぼりは尊重する。
|
|
3085
|
+
const raiseAlive = shouldRaiseBusy(kaBusySignal, kaMenu);
|
|
3086
|
+
noteBusyEdge(state, cfg.site, ka.id, raiseAlive, logLine);
|
|
3087
|
+
if (ka.lastHeartbeatAt === null || now - ka.lastHeartbeatAt >= RELAY_HEARTBEAT_INTERVAL_MS) {
|
|
3088
|
+
ka.lastHeartbeatAt = now;
|
|
3089
|
+
logLine(`site=${cfg.site} id=${ka.id} 見張り継続: 職人は生きています→生存通知` +
|
|
3090
|
+
`(中央へ問い合わせ=した・status=${alive ?? '不明'}` +
|
|
3091
|
+
`・経過${Math.round((now - ka.startedAt) / 1000)}s)`);
|
|
3092
|
+
await heartbeat(cfg, ka.id, raiseAlive);
|
|
3093
|
+
}
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
2976
3096
|
if (action === 'heartbeat') {
|
|
2977
3097
|
ka.lastHeartbeatAt = now;
|
|
2978
3098
|
const raise = shouldRaiseBusy(kaBusySignal, kaMenu);
|
|
@@ -3102,8 +3222,11 @@ export async function restoreOrphans(cfg, state) {
|
|
|
3102
3222
|
log(`site=${cfg.site} id=${pick.id} を sent孤児から pending へ復元(★配達はしない・DONE待ちと生存通知だけ)`);
|
|
3103
3223
|
}
|
|
3104
3224
|
export async function relayTick(cfg, state, hooks) {
|
|
3105
|
-
if (state.running)
|
|
3225
|
+
if (state.running) {
|
|
3226
|
+
// ★合図を受けていたなら、空振りした理由を1行残す(しぼり付き・案B-3)。
|
|
3227
|
+
noteNudgeSkipped(state, cfg.site, '前の周がまだ走っている', log);
|
|
3106
3228
|
return; // 前回の tick が走行中ならスキップ
|
|
3229
|
+
}
|
|
3107
3230
|
state.running = true;
|
|
3108
3231
|
// 承認待ちの観測値は毎tick「出ていない」から始める(下でプロンプトを検知したときだけ true にする)。
|
|
3109
3232
|
// ここで false に戻しておくと、プロンプトが消えた/pending が無い/早期 return のどの経路でも、
|
|
@@ -3135,9 +3258,18 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3135
3258
|
await keepaliveTick(cfg, state);
|
|
3136
3259
|
// ── 完了マーカー検出(pending があれば最優先・停止中でも継続) ──
|
|
3137
3260
|
if (state.pending) {
|
|
3138
|
-
|
|
3139
|
-
|
|
3261
|
+
// ★この周の結末を「配った便の完了待ち」として記録する(案B-1)。ここで1回呼べば、
|
|
3262
|
+
// 以下のどの return 経路を通っても管制の札が 'delivered' のまま固まらない。
|
|
3263
|
+
// 同じ状態・同じ指示の間 since は動かないので、待っている長さがそのまま測れる。
|
|
3264
|
+
noteReadiness(state, 'waiting-done', state.pending.id);
|
|
3265
|
+
// ★合図が来ていたなら、取得へ進まない理由を1行残す(しぼり付き・案B-3)。
|
|
3266
|
+
noteNudgeSkipped(state, cfg.site, `配った便(id=${state.pending.id})の完了を待っている`, log);
|
|
3267
|
+
const cap = (hooks?.capture ?? tmuxCapture)(cfg.site);
|
|
3268
|
+
if (cap === null) {
|
|
3269
|
+
// ★無言で返さない(案B-2)。画面が読めない周も、しぼり付きで1行残す。
|
|
3270
|
+
noteWaitingDone(state, cfg.site, '読めない', false, log);
|
|
3140
3271
|
return; // tmux未取得: 次回再確認
|
|
3272
|
+
}
|
|
3141
3273
|
// ── 完了検出の走査範囲を広げてよいかの観測(★状態更新には使わない・ADR-023) ──
|
|
3142
3274
|
// アンカーを一度でも画面で見たら、以後アンカーが流れて消えても全画面から DONE を拾ってよい
|
|
3143
3275
|
// (ADR-020 の根拠)。★ここで観測するのは「どこまで見てよいか」だけで、配達の成否や
|
|
@@ -3211,9 +3343,25 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3211
3343
|
// 【副シグナル温存=無損失縮退】中央 status が読めない(null=エンドポイント未実装/不達)ときだけ、
|
|
3212
3344
|
// 従来の detectExternalDone(別idが配れる=終端の確証)へフォールバックする。中央側 status API が
|
|
3213
3345
|
// 入るまではこの副シグナルが従来どおり効き、入った後は主シグナル(status)が確実に固着を断つ。
|
|
3214
|
-
//
|
|
3346
|
+
// 【★画面の判定から切り離す(根治・案A-2・調査 67f8c51f)】
|
|
3347
|
+
// 従来の条件は「印が付いている+**画面がアイドルに見える**」だった。ところが画面の目印は
|
|
3348
|
+
// 職人側の版上げで必ず壊れる。実機 2026-10-02 は Codex 0.160.0 の起動画面(点字のロゴ)を
|
|
3349
|
+
// 「作業中」と誤読し、idle が永久に false になった=**中央の真実を知る唯一の道が閉じた**。
|
|
3350
|
+
// 中央でとうに done の便を15分抱え、そのあと見張りへ渡って最長3時間ふさがった。
|
|
3351
|
+
// =画面が外れたら逃げ道も消える、という構造そのものが穴。
|
|
3352
|
+
// そこで条件を「印が付いている」だけにし、代わりに時計でしぼる:
|
|
3353
|
+
// ・アイドルに見える周 … 従来どおり毎tick叩く(★いちばん速い道は1バイトも遅くしない)
|
|
3354
|
+
// ・それ以外の周 … 150秒(RELAY_HEARTBEAT_INTERVAL_MS・生存通知と同じ時計)に1回
|
|
3355
|
+
// =HTTP の増分は1指示あたり最大で150秒に1回。画面の読み取りが外れても、中央で終わった便は
|
|
3356
|
+
// 必ず150秒以内の周で追跡解除され、次の便が配られる。
|
|
3215
3357
|
const idle = looksIdleFor(cap, kindOf(cfg));
|
|
3216
|
-
|
|
3358
|
+
const probeBase = state.pending.lastStatusProbeAt ?? state.pending.sentAt;
|
|
3359
|
+
const probeDue = Date.now() - probeBase >= RELAY_HEARTBEAT_INTERVAL_MS;
|
|
3360
|
+
const askedCentral = state.pending.sentMarked && (idle || probeDue);
|
|
3361
|
+
// ★画面をどう読んだかを必ず残す(案B-2)。これが1行あれば今回の誤読は即座に分かった。
|
|
3362
|
+
noteWaitingDone(state, cfg.site, idle ? '待機' : '作業中', askedCentral, log);
|
|
3363
|
+
if (askedCentral) {
|
|
3364
|
+
state.pending.lastStatusProbeAt = Date.now();
|
|
3217
3365
|
const centralStatus = await fetchCommandStatus(cfg, state.pending.id);
|
|
3218
3366
|
// ★本文未着(instruction_missing)も追跡をやめる(便 7d810bbb)。管制は占有を外して人の判断待ちに
|
|
3219
3367
|
// 出している=ここで待ち続けると次の便が拾えない。★配り直さない(人が決める)。
|
|
@@ -3226,7 +3374,9 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3226
3374
|
}
|
|
3227
3375
|
// 中央 status が未知(null)のときのみ、従来の副シグナル(別idが配信可能=終端の確証)で救う。
|
|
3228
3376
|
// 中央が sent/queued 等の「生存中」を明示的に返した場合は解除しない(中央の真実を尊重)。
|
|
3229
|
-
|
|
3377
|
+
// ★副シグナルは「アイドル」を根拠の一部にしている(detectExternalDone)。だから画面が
|
|
3378
|
+
// アイドルに見える周だけ叩く=従来と同じ条件・同じ結論。無駄な取得も増やさない。
|
|
3379
|
+
if (centralStatus === null && idle) {
|
|
3230
3380
|
const next = await fetchCommand(cfg);
|
|
3231
3381
|
const nextId = next?.id ?? null;
|
|
3232
3382
|
if (detectExternalDone({
|
|
@@ -3498,7 +3648,15 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3498
3648
|
// 新しい指示の生存ハートビートは先頭から数え直す(前指示のスロットルを引き継がない)。
|
|
3499
3649
|
state.lastHeartbeatAt = null;
|
|
3500
3650
|
noteReadiness(state, 'delivered', cmd.id);
|
|
3501
|
-
|
|
3651
|
+
// ★記録の文面を事実に合わせる(案C-4・調査 67f8c51f)。
|
|
3652
|
+
// 「貼り付けが入力欄に入ったか」を確かめているのは、受領確認を通る職人(claude)だけ。
|
|
3653
|
+
// それ以外(Codex)は tmux へ送れたかしか見ていないので、画面を覆う表示(起動画面・警告の
|
|
3654
|
+
// 表示)へ打ち込んで本文が捨てられても「配達しました」と書いていた=人が記録を信じて誤診した
|
|
3655
|
+
// (実機 2026-10-02)。確認が入る(要判断3)までの間、書く言葉を実際に確かめた範囲に合わせる。
|
|
3656
|
+
// ★claude の文面は1文字も変えない。
|
|
3657
|
+
log(useAck
|
|
3658
|
+
? `site=${cfg.site} id=${cmd.id} ${landed ? '配達しました' : '配達を試みましたが貼り付けが入らず、何も送信していません'}`
|
|
3659
|
+
: `site=${cfg.site} id=${cmd.id} 送信しました(入力欄に入ったかは未確認)`);
|
|
3502
3660
|
// ★配った時点で管制へ印を書く(便 7d810bbb)。Claude の職人は「配達中(受領確認待ち)」、
|
|
3503
3661
|
// それ以外は今までどおり「配達済み(sent)」。書けなければ次tick以降で書けるまでやり直す
|
|
3504
3662
|
// (markSentAtCentral)。★書けなくても再配達はしない。
|
package/package.json
CHANGED
|
@@ -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
|
# ═════════════════════════════════════════════════════════════════════════════
|
package/sales-template/setup.sh
CHANGED
|
@@ -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
|
-
|
|
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)
|
package/scripts/update-guard.sh
CHANGED
|
@@ -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
|