@tyhld/conductor 0.12.86 → 0.12.87
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 +157 -10
- package/dist/relay.js +196 -0
- package/package.json +1 -1
- package/sales-template/install.sh +22 -2
- package/sales-template/setup.sh +201 -0
- package/scripts/hooks/policy.py +84 -2
- package/scripts/hooks/test_policy.py +64 -0
- package/scripts/update-guard.sh +16 -2
- package/skills/conductor-craftsman/SKILL.md +13 -5
package/dist/headless.js
CHANGED
|
@@ -19,7 +19,7 @@ import { createInterface } from 'node:readline';
|
|
|
19
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
|
-
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, noteDrain, noteReadiness, patchStatus, recoverStuckProcessedReports, reconnectDelayMs, selectOrphanToRestore, sendHeartbeat, } from './relay.js';
|
|
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, deliverJobEndFacts, deliveryReport, fetchCommand, fetchCommandStatus, fetchControl, fetchOrphans, flushPendingJobEndFacts, isTerminalCommandStatus, kindOf, markDelivered, noteCentralDown, noteCentralUp, noteDrain, noteReadiness, patchStatus, recoverStuckProcessedReports, reconnectDelayMs, reportPhase, selectOrphanToRestore, sendHeartbeat, } from './relay.js';
|
|
23
23
|
import { claudePinPath, readClaudePinLine, } from './autoupdate-flags.js';
|
|
24
24
|
const log = (msg) => {
|
|
25
25
|
console.log(`[headless] ${msg}`);
|
|
@@ -528,6 +528,35 @@ export function maskSecrets(text) {
|
|
|
528
528
|
}
|
|
529
529
|
return masked;
|
|
530
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* 便の終わり方を決める(★純粋関数=単体テストできる・便 915694ac)。
|
|
533
|
+
*
|
|
534
|
+
* ★本文を渡せなかったことが先。職人が何も受け取っていないという事実は、
|
|
535
|
+
* ほかのどの終わり方よりも強い(やり直しても同じ結果になる)。
|
|
536
|
+
*/
|
|
537
|
+
export function jobEndKindOf(j) {
|
|
538
|
+
if (j.bodyHandoff === 'failed')
|
|
539
|
+
return 'not_started';
|
|
540
|
+
if (j.timedOut)
|
|
541
|
+
return 'timeout';
|
|
542
|
+
if (j.stopped)
|
|
543
|
+
return 'stopped';
|
|
544
|
+
if (j.authError)
|
|
545
|
+
return 'auth_error';
|
|
546
|
+
return j.exitCode === 0 ? 'completed' : 'error';
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* 本文を渡せなかった便として、走っている処理を止めるか(★純粋関数・便 915694ac)。
|
|
550
|
+
*
|
|
551
|
+
* ★画面なし方式は中継が自分で処理を立ち上げて本文を渡す。
|
|
552
|
+
* ・'done' … 渡せた → 何もしない(★本文未着と申告しない・追跡を続ける)
|
|
553
|
+
* ・'pending' … まだ確かめ中 → 何もしない(迷ったら触らない)
|
|
554
|
+
* ・'failed' … 渡せなかった → 止めて理由つきで管制へ届ける
|
|
555
|
+
* 既に止めた便・上限時間で止めた便は、その終わり方の後始末に任せる。
|
|
556
|
+
*/
|
|
557
|
+
export function shouldAbortForBodyHandoff(j) {
|
|
558
|
+
return j.bodyHandoff === 'failed' && !j.stopped && !j.timedOut;
|
|
559
|
+
}
|
|
531
560
|
/**
|
|
532
561
|
* いま画面なし方式が追っている便の指示ID(★回収の道から除く物・便 6ebba0d0 の b)。
|
|
533
562
|
*
|
|
@@ -1003,6 +1032,8 @@ export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
|
|
|
1003
1032
|
stderrLog: logs?.stderrLog ?? null,
|
|
1004
1033
|
eventsLog: logs?.eventsLog ?? null,
|
|
1005
1034
|
durationMs: 0,
|
|
1035
|
+
finishedAt: null,
|
|
1036
|
+
bodyHandoff: 'pending',
|
|
1006
1037
|
};
|
|
1007
1038
|
// spawn の 'error' イベント(ENOENT 等は非同期で届く。同期例外と両方捕まえる)
|
|
1008
1039
|
proc.on('error', (err) => {
|
|
@@ -1010,22 +1041,46 @@ export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
|
|
|
1010
1041
|
job.completed = true;
|
|
1011
1042
|
job.exitCode = -1;
|
|
1012
1043
|
job.exitSignal = null;
|
|
1044
|
+
job.finishedAt = Date.now();
|
|
1013
1045
|
job.stderrBuf += `\nspawn error: ${err.message}`;
|
|
1014
1046
|
}
|
|
1015
1047
|
});
|
|
1016
1048
|
// stdin: 指示本文を書いて閉じる
|
|
1049
|
+
//
|
|
1050
|
+
// ★「渡せたか」を事実として残す(えふさん確定 2026-10-02・便 915694ac)。
|
|
1051
|
+
// これまでは stdin のエラーをまるごと握り潰していたので、本文を1バイトも渡せていなくても
|
|
1052
|
+
// 中継は管制へ「配達済み(sent)」と書いていた=職人が受け取っていない便を実行中にしていた。
|
|
1053
|
+
// 逆に、渡せた便を「本文未着」と申告してしまう道も作らない(下の headlessTick を参照)。
|
|
1054
|
+
const noteHandoffFailed = (why) => {
|
|
1055
|
+
if (job.bodyHandoff === 'done')
|
|
1056
|
+
return; // 既に書けている(閉じたあとの後始末のエラーは数えない)
|
|
1057
|
+
job.bodyHandoff = 'failed';
|
|
1058
|
+
job.bodyHandoffError ??= why;
|
|
1059
|
+
};
|
|
1017
1060
|
if (proc.stdin) {
|
|
1018
|
-
//
|
|
1019
|
-
proc.stdin.on('error', () => {
|
|
1020
|
-
|
|
1021
|
-
|
|
1061
|
+
// ★エラーは握り潰さず、理由として残す(投げると中継の輪が止まるので、throw はしない)。
|
|
1062
|
+
proc.stdin.on('error', (err) => {
|
|
1063
|
+
noteHandoffFailed(`職人の処理へ本文を渡せませんでした(標準入力: ${err.message})`);
|
|
1064
|
+
});
|
|
1065
|
+
const text = isResume
|
|
1066
|
+
? RESUME_PROMPT
|
|
1067
|
+
: buildSendText(cmd.body, cfg.site, buildAnchor(cmd.id), kindOf(cfg));
|
|
1068
|
+
try {
|
|
1069
|
+
// ★書き出しが本当に流れ切ってから 'done' にする(返事を待たずに「渡せた」と言わない)。
|
|
1070
|
+
proc.stdin.write(text, (err) => {
|
|
1071
|
+
if (err)
|
|
1072
|
+
noteHandoffFailed(`職人の処理へ本文を渡せませんでした(書き出し: ${err.message})`);
|
|
1073
|
+
else if (job.bodyHandoff === 'pending')
|
|
1074
|
+
job.bodyHandoff = 'done';
|
|
1075
|
+
});
|
|
1076
|
+
proc.stdin.end();
|
|
1022
1077
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
const text = buildSendText(cmd.body, cfg.site, anchor, kindOf(cfg));
|
|
1026
|
-
proc.stdin.write(text);
|
|
1078
|
+
catch (err) {
|
|
1079
|
+
noteHandoffFailed(`職人の処理へ本文を渡せませんでした(${err instanceof Error ? err.message : String(err)})`);
|
|
1027
1080
|
}
|
|
1028
|
-
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
noteHandoffFailed('職人の処理の入口(標準入力)が開かなかったので、本文を渡せませんでした');
|
|
1029
1084
|
}
|
|
1030
1085
|
// stdout: stream-json を行ごとに読み、CommandEvent に変換してバッファへ。ログファイルにも書く。
|
|
1031
1086
|
if (proc.stdout) {
|
|
@@ -1071,6 +1126,7 @@ export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
|
|
|
1071
1126
|
job.completed = true;
|
|
1072
1127
|
job.exitCode = code;
|
|
1073
1128
|
job.exitSignal = signal?.toString() ?? null;
|
|
1129
|
+
job.finishedAt = Date.now();
|
|
1074
1130
|
job.durationMs = Date.now() - job.startedAt;
|
|
1075
1131
|
closeLog(job.stdoutLog);
|
|
1076
1132
|
closeLog(job.stderrLog);
|
|
@@ -1174,6 +1230,42 @@ async function checkRunningJob(cfg, state) {
|
|
|
1174
1230
|
...(detail !== undefined ? { detail } : {}),
|
|
1175
1231
|
});
|
|
1176
1232
|
};
|
|
1233
|
+
// ★終了の事実を管制へ必ず届ける(えふさん確定 2026-10-02・便 915694ac)。
|
|
1234
|
+
//
|
|
1235
|
+
// 【えふさん確定】便ID付き報告が到着 + Claude Code プロセス終了 + 中継が終了結果を取得
|
|
1236
|
+
// まで揃ってはじめて「職人の作業終了」。だから中継は【終わり方に関係なく】終わった事実を届ける。
|
|
1237
|
+
// 【ここ(分かれ道の手前)で1回だけ送る理由】終わり方ごとの枝の中で送ると、
|
|
1238
|
+
// ・終了状態を書けずに次の周へ持ち越す枝(下の done が書けないとき)では毎周送ってしまう
|
|
1239
|
+
// ・追跡をやめる枝(放棄・試行上限)では送り忘れる
|
|
1240
|
+
// の両方が起きる。★「プロセスが終わった」ことは枝に関係なく1つの事実なので、ここで1回送る。
|
|
1241
|
+
// 【送る物】中継が実際に取れた物だけ(終了コードが無ければ null=つかめていないと伝える)。
|
|
1242
|
+
// ★作り話の終了を送らない。「報告を出せたか」は報告ファイルの有無だけで見る
|
|
1243
|
+
// (★中身の言葉は1文字も読まない=2026-09-23 の確定のまま)。
|
|
1244
|
+
// 【届かなければ】控えが手元に残り、次の周の flushPendingJobEndFacts が届くまでやり直す。
|
|
1245
|
+
if (!job.endFactsTold) {
|
|
1246
|
+
job.endFactsTold = true;
|
|
1247
|
+
const endKind = jobEndKindOf({
|
|
1248
|
+
bodyHandoff: job.bodyHandoff,
|
|
1249
|
+
timedOut: job.timedOut,
|
|
1250
|
+
stopped: job.stopped,
|
|
1251
|
+
authError: isAuthError(job),
|
|
1252
|
+
exitCode: job.exitCode,
|
|
1253
|
+
});
|
|
1254
|
+
await deliverJobEndFacts(cfg, {
|
|
1255
|
+
commandId: job.commandId,
|
|
1256
|
+
site: cfg.site,
|
|
1257
|
+
finishedAt: new Date(job.finishedAt ?? Date.now()).toISOString(),
|
|
1258
|
+
endKind,
|
|
1259
|
+
exitCode: job.exitCode,
|
|
1260
|
+
exitSignal: job.exitSignal,
|
|
1261
|
+
reportDelivered: reportPhase(job.commandId) !== 'none',
|
|
1262
|
+
// ★画面なし方式は中継が自分で立ち上げた処理なので、終わりを直接つかんでいる。
|
|
1263
|
+
// つかめていないときは false を送る(1-3 の「つかめていない」と同じ形)。
|
|
1264
|
+
processEndObserved: job.finishedAt !== null,
|
|
1265
|
+
startedAt: new Date(job.startedAt).toISOString(),
|
|
1266
|
+
detail: job.bodyHandoffError ?? `attempt ${job.attempt}/${HEADLESS_MAX_ATTEMPTS}`,
|
|
1267
|
+
}, { logLine: log });
|
|
1268
|
+
}
|
|
1177
1269
|
if (job.timedOut) {
|
|
1178
1270
|
// 上限時間で強制停止した → 管制へ理由つきで failed を記録する
|
|
1179
1271
|
const elapsed = Math.round((Date.now() - job.startedAt) / 1000);
|
|
@@ -1302,6 +1394,28 @@ async function checkRunningJob(cfg, state) {
|
|
|
1302
1394
|
}
|
|
1303
1395
|
// ── 実行中のプロセスの監視 ──
|
|
1304
1396
|
const now = Date.now();
|
|
1397
|
+
// ★本文を渡せなかったことが後から分かったとき(便 915694ac)。
|
|
1398
|
+
// 画面なし方式は中継が自分で処理を立ち上げて本文を渡す。渡せていなければ職人は何も
|
|
1399
|
+
// 受け取っていないので、動いているふりをさせず、理由つきで管制へ届けて止める。
|
|
1400
|
+
// ★渡せた便('done')・まだ確かめ中の便('pending')には何もしない=追跡を続ける。
|
|
1401
|
+
if (shouldAbortForBodyHandoff(job)) {
|
|
1402
|
+
const why = job.bodyHandoffError ?? '職人の処理へ本文を渡せませんでした';
|
|
1403
|
+
log(`ERROR site=${cfg.site} id=${job.commandId} ${why} → 止めて管制へ知らせます`);
|
|
1404
|
+
await patchStatus(cfg, job.commandId, 'failed', why.slice(0, EVENT_TEXT_MAX));
|
|
1405
|
+
await sendCommandEvents(cfg, job.commandId, [{
|
|
1406
|
+
type: 'error',
|
|
1407
|
+
text: why.slice(0, EVENT_TEXT_MAX),
|
|
1408
|
+
at: new Date().toISOString(),
|
|
1409
|
+
attempt: job.attempt,
|
|
1410
|
+
}]);
|
|
1411
|
+
rs.abandoned.add(job.commandId);
|
|
1412
|
+
// ★stopped を立ててから止める=終了の後始末で「異常終了 → 自動再開」の道へ入らない
|
|
1413
|
+
// (本文が渡っていないので、同じことをやり直しても同じ結果になる。二重に走らせない)。
|
|
1414
|
+
// 終わり方は 'not_started' として届く(上の endKind は本文の未渡しを先に見る)。
|
|
1415
|
+
job.stopped = true;
|
|
1416
|
+
await stopJob(job);
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1305
1419
|
// stop_requested の確認(間隔を置いて中央へ問い合わせる)
|
|
1306
1420
|
if (now - job.lastStopCheckAt >= HEADLESS_STOP_CHECK_MS) {
|
|
1307
1421
|
job.lastStopCheckAt = now;
|
|
@@ -1603,6 +1717,10 @@ export async function headlessTick(cfg, state, hooks) {
|
|
|
1603
1717
|
log('停止が解除されました。relayを再開します');
|
|
1604
1718
|
}
|
|
1605
1719
|
rs.lastHalted = false;
|
|
1720
|
+
// ── 届いていない「終了の事実」の送り直し(★便 915694ac・黙って捨てない) ──
|
|
1721
|
+
// ★停止トグルの確認より後に置く=止められている現場では外へ出ない。
|
|
1722
|
+
// ★追跡をやめた便・「本文未着」と申告された便の分も、ここで届くまでやり直す。
|
|
1723
|
+
await flushPendingJobEndFacts(cfg, { logLine: log });
|
|
1606
1724
|
// ── 取り残しの後始末(★便 6ebba0d0 の項目4) ──
|
|
1607
1725
|
// 「実プロセスが無い・報告は渡し済み・管制ではまだ終わっていない」便に done を書く。
|
|
1608
1726
|
// ★自分の PC・自分の現場の便だけ(判定は recoverStuckProcessedReports 側)。
|
|
@@ -1695,6 +1813,35 @@ export async function headlessTick(cfg, state, hooks) {
|
|
|
1695
1813
|
log(`ERROR site=${cfg.site} id=${cmd.id} claude が見つからない → 放棄`);
|
|
1696
1814
|
return;
|
|
1697
1815
|
}
|
|
1816
|
+
// ★本文を渡せなかったことが起動のその場で分かったとき(入口が開かない・書き出しが例外)。
|
|
1817
|
+
// 「配達済み(sent)」を1バイトも書かずに、理由つきで管制へ届けて放棄する(便 915694ac)。
|
|
1818
|
+
// ★渡せた便・まだ確かめ中の便は、今までどおりそのまま進む(追跡を続ける)。
|
|
1819
|
+
if (job.bodyHandoff === 'failed') {
|
|
1820
|
+
const why = job.bodyHandoffError ?? '職人の処理へ本文を渡せませんでした';
|
|
1821
|
+
await stopJob(job);
|
|
1822
|
+
await patchStatus(cfg, cmd.id, 'failed', why.slice(0, EVENT_TEXT_MAX));
|
|
1823
|
+
await sendCommandEvents(cfg, cmd.id, [{
|
|
1824
|
+
type: 'error',
|
|
1825
|
+
text: why.slice(0, EVENT_TEXT_MAX),
|
|
1826
|
+
at: new Date().toISOString(),
|
|
1827
|
+
attempt,
|
|
1828
|
+
}]);
|
|
1829
|
+
await deliverJobEndFacts(cfg, {
|
|
1830
|
+
commandId: cmd.id,
|
|
1831
|
+
site: cfg.site,
|
|
1832
|
+
finishedAt: new Date().toISOString(),
|
|
1833
|
+
endKind: 'not_started',
|
|
1834
|
+
exitCode: job.exitCode,
|
|
1835
|
+
exitSignal: job.exitSignal,
|
|
1836
|
+
reportDelivered: false,
|
|
1837
|
+
processEndObserved: job.finishedAt !== null,
|
|
1838
|
+
startedAt: new Date(job.startedAt).toISOString(),
|
|
1839
|
+
detail: why,
|
|
1840
|
+
}, { logLine: log });
|
|
1841
|
+
rs.abandoned.add(cmd.id);
|
|
1842
|
+
log(`ERROR site=${cfg.site} id=${cmd.id} ${why} → 放棄(配達済みとは書きません)`);
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1698
1845
|
state.job = job;
|
|
1699
1846
|
saveActiveJob(cfg.site, { commandId: cmd.id, body: cmd.body, sessionId, attempt, startedAt: Date.now(), site: cfg.site, machine: cfg.machine, workDir: cfg.workDir, gitOrigin: resolveGitOrigin(cfg.workDir) ?? undefined, pid: job.process.pid, postedAt: cmd.sentAt });
|
|
1700
1847
|
markDelivered(rs, cmd.id);
|
package/dist/relay.js
CHANGED
|
@@ -2957,6 +2957,154 @@ export async function sendHeartbeat(cfg, id, busy = false, liveness) {
|
|
|
2957
2957
|
log(`site=${cfg.site} id=${id} heartbeat -> 失敗: ${err instanceof Error ? err.message : String(err)}(縮退で安全)`);
|
|
2958
2958
|
}
|
|
2959
2959
|
}
|
|
2960
|
+
/** 届けられなかった「終了の事実」の置き場。 */
|
|
2961
|
+
export function jobEndDir(env = process.env) {
|
|
2962
|
+
const base = (env.CONDUCTOR_STATE_DIR ?? '').trim();
|
|
2963
|
+
return base
|
|
2964
|
+
? path.join(base, 'job-end')
|
|
2965
|
+
: path.join(env.HOME ?? os.homedir(), '.local', 'state', 'conductor', 'job-end');
|
|
2966
|
+
}
|
|
2967
|
+
/** 1便ぶんの控えのファイル名(★現場と指示IDの両方を入れる=別の現場の便と混ざらない)。 */
|
|
2968
|
+
export function jobEndFactsPath(site, commandId, env = process.env) {
|
|
2969
|
+
const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
2970
|
+
return path.join(jobEndDir(env), `${safe(site)}__${safe(commandId)}.json`);
|
|
2971
|
+
}
|
|
2972
|
+
/** 人向けの一言の上限(管制の列を汚さない)。 */
|
|
2973
|
+
const JOB_END_DETAIL_MAX = 500;
|
|
2974
|
+
/**
|
|
2975
|
+
* 管制へ送る本文(★純粋関数=単体テストできる)。
|
|
2976
|
+
*
|
|
2977
|
+
* ★`state` も `busy` も入れない(上の約束④)。
|
|
2978
|
+
* ★項目名は管制の側の便(ai-kana)が読む物なので、勝手に変えない。
|
|
2979
|
+
*/
|
|
2980
|
+
export function jobEndFactsBody(f) {
|
|
2981
|
+
const body = {
|
|
2982
|
+
// ★この印が在る生存報告は「終わりの知らせ」。管制が知らないうちは黙って捨てられる。
|
|
2983
|
+
jobEnded: true,
|
|
2984
|
+
finishedAt: f.finishedAt,
|
|
2985
|
+
endKind: f.endKind,
|
|
2986
|
+
exitCode: f.exitCode,
|
|
2987
|
+
exitSignal: f.exitSignal,
|
|
2988
|
+
reportDelivered: f.reportDelivered,
|
|
2989
|
+
processEndObserved: f.processEndObserved,
|
|
2990
|
+
};
|
|
2991
|
+
// ★立ち上がった時刻は管制が既に読める項目(startedAt)なので、分かるときだけ添える。
|
|
2992
|
+
if (f.startedAt !== undefined)
|
|
2993
|
+
body.startedAt = f.startedAt;
|
|
2994
|
+
if (f.detail !== undefined && f.detail !== '')
|
|
2995
|
+
body.jobEndDetail = f.detail.slice(0, JOB_END_DETAIL_MAX);
|
|
2996
|
+
return body;
|
|
2997
|
+
}
|
|
2998
|
+
/** 控えを手元に書く(★届くまで消さない)。書けたか返す。 */
|
|
2999
|
+
export function saveJobEndFacts(f, env = process.env) {
|
|
3000
|
+
try {
|
|
3001
|
+
mkdirSync(jobEndDir(env), { recursive: true });
|
|
3002
|
+
writeFileSync(jobEndFactsPath(f.site, f.commandId, env), JSON.stringify(f) + '\n');
|
|
3003
|
+
return true;
|
|
3004
|
+
}
|
|
3005
|
+
catch {
|
|
3006
|
+
return false;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
/** 控えを消す(★届いたときだけ呼ぶ)。 */
|
|
3010
|
+
export function clearJobEndFacts(site, commandId, env = process.env) {
|
|
3011
|
+
try {
|
|
3012
|
+
const p = jobEndFactsPath(site, commandId, env);
|
|
3013
|
+
if (existsSync(p))
|
|
3014
|
+
unlinkSync(p);
|
|
3015
|
+
}
|
|
3016
|
+
catch {
|
|
3017
|
+
// 消せなくても次の周でもう一度送るだけ(管制の側は冪等)。
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
/** その現場の、まだ届いていない「終了の事実」を読む(壊れた控えは飛ばす)。 */
|
|
3021
|
+
export function pendingJobEndFacts(site, env = process.env) {
|
|
3022
|
+
const dir = jobEndDir(env);
|
|
3023
|
+
let names;
|
|
3024
|
+
try {
|
|
3025
|
+
names = readdirSync(dir).filter((n) => n.endsWith('.json'));
|
|
3026
|
+
}
|
|
3027
|
+
catch {
|
|
3028
|
+
return [];
|
|
3029
|
+
}
|
|
3030
|
+
const out = [];
|
|
3031
|
+
for (const n of names.sort()) {
|
|
3032
|
+
try {
|
|
3033
|
+
const raw = readFileSync(path.join(dir, n), 'utf8').trim();
|
|
3034
|
+
if (!raw)
|
|
3035
|
+
continue;
|
|
3036
|
+
const f = JSON.parse(raw);
|
|
3037
|
+
// ★自分の現場の便だけ送る(1台で複数の現場の中継が動くため)。
|
|
3038
|
+
if (!f || f.site !== site || !f.commandId || !f.finishedAt)
|
|
3039
|
+
continue;
|
|
3040
|
+
out.push(f);
|
|
3041
|
+
}
|
|
3042
|
+
catch {
|
|
3043
|
+
// 読めない控えは飛ばす(消さない=人が見つけられるように残す)。
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
return out;
|
|
3047
|
+
}
|
|
3048
|
+
/**
|
|
3049
|
+
* 「終了の事実」を管制へ1回送る。届いたら true。
|
|
3050
|
+
* ★失敗は投げない(中継の輪を止めない)。false を返して、呼び出し側が控えを残す。
|
|
3051
|
+
*/
|
|
3052
|
+
export async function sendJobEndFacts(cfg, f, opts = {}) {
|
|
3053
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
3054
|
+
const logLine = opts.logLine ?? (() => { });
|
|
3055
|
+
try {
|
|
3056
|
+
const res = await doFetch(`${cfg.url}/api/conductor/commands/${encodeURIComponent(f.commandId)}/seen`, {
|
|
3057
|
+
method: 'PATCH',
|
|
3058
|
+
headers: { Authorization: `Bearer ${cfg.token}`, 'Content-Type': 'application/json' },
|
|
3059
|
+
body: JSON.stringify(jobEndFactsBody(f)),
|
|
3060
|
+
signal: AbortSignal.timeout(RELAY_FETCH_TIMEOUT_MS),
|
|
3061
|
+
});
|
|
3062
|
+
if (res.ok)
|
|
3063
|
+
return true;
|
|
3064
|
+
logLine(`site=${cfg.site} id=${f.commandId} 終了の事実 -> HTTP ${res.status}(控えを残して次の周でやり直します)`);
|
|
3065
|
+
return false;
|
|
3066
|
+
}
|
|
3067
|
+
catch (err) {
|
|
3068
|
+
logLine(`site=${cfg.site} id=${f.commandId} 終了の事実 -> 失敗: ` +
|
|
3069
|
+
`${err instanceof Error ? err.message : String(err)}(控えを残して次の周でやり直します)`);
|
|
3070
|
+
return false;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* 「終了の事実」を控えに書いてから送る(★順番が肝)。
|
|
3075
|
+
* 先に控えを書くので、送信中に中継が落ちても事実は失われない。送れたら控えを消す。
|
|
3076
|
+
* 送れなければ残して、次の周の flushPendingJobEndFacts がやり直す。
|
|
3077
|
+
*/
|
|
3078
|
+
export async function deliverJobEndFacts(cfg, f, opts = {}) {
|
|
3079
|
+
const env = opts.env ?? process.env;
|
|
3080
|
+
saveJobEndFacts(f, env);
|
|
3081
|
+
const ok = await sendJobEndFacts(cfg, f, opts);
|
|
3082
|
+
if (ok) {
|
|
3083
|
+
clearJobEndFacts(f.site, f.commandId, env);
|
|
3084
|
+
opts.logLine?.(`site=${cfg.site} id=${f.commandId} 終了の事実を管制へ届けました` +
|
|
3085
|
+
`(終わり方=${f.endKind} 終了コード=${f.exitCode ?? '不明'} 報告=${f.reportDelivered === null ? '不明' : f.reportDelivered ? 'あり' : 'なし'})`);
|
|
3086
|
+
}
|
|
3087
|
+
return ok;
|
|
3088
|
+
}
|
|
3089
|
+
/** 届いていない「終了の事実」をまとめて送り直す(★1周に1回・届くまで何周でも)。 */
|
|
3090
|
+
export async function flushPendingJobEndFacts(cfg, opts = {}) {
|
|
3091
|
+
const env = opts.env ?? process.env;
|
|
3092
|
+
let sent = 0;
|
|
3093
|
+
let kept = 0;
|
|
3094
|
+
for (const f of pendingJobEndFacts(cfg.site, env)) {
|
|
3095
|
+
if (await sendJobEndFacts(cfg, f, opts)) {
|
|
3096
|
+
clearJobEndFacts(f.site, f.commandId, env);
|
|
3097
|
+
sent++;
|
|
3098
|
+
}
|
|
3099
|
+
else {
|
|
3100
|
+
kept++;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
if (sent > 0 || kept > 0) {
|
|
3104
|
+
opts.logLine?.(`site=${cfg.site} 終了の事実の送り直し: 届いた ${sent} 件・残り ${kept} 件`);
|
|
3105
|
+
}
|
|
3106
|
+
return { sent, kept };
|
|
3107
|
+
}
|
|
2960
3108
|
/**
|
|
2961
3109
|
* いま「作業中です」と手を挙げる(busy:true を載せる)かを決める純粋関数。
|
|
2962
3110
|
*
|
|
@@ -3304,6 +3452,9 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3304
3452
|
// pending 分岐も新規指示の取得も従来どおり動く(pending を落とす目的を殺さない)。
|
|
3305
3453
|
// 停止中(relayHalted)でも続けるのは、既に送った指示の後始末は進めるという既存方針と同じ。
|
|
3306
3454
|
await keepaliveTick(cfg, state);
|
|
3455
|
+
// ── 届いていない「終了の事実」の送り直し(★便 915694ac・黙って捨てない) ──
|
|
3456
|
+
// 既に送った便の後始末なので、停止中(relayHalted)でも続ける(keepaliveTick と同じ方針)。
|
|
3457
|
+
await flushPendingJobEndFacts(cfg, { logLine: log });
|
|
3307
3458
|
// ── 完了マーカー検出(pending があれば最優先・停止中でも継続) ──
|
|
3308
3459
|
if (state.pending) {
|
|
3309
3460
|
// ★この周の結末を「配った便の完了待ち」として記録する(案B-1)。ここで1回呼べば、
|
|
@@ -3338,6 +3489,23 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3338
3489
|
if (state.pending.ack === 'missing') {
|
|
3339
3490
|
// ★本文未着と決めた便。職人は仕事を受け取っていないので、完了検出も生存通知もしない。
|
|
3340
3491
|
// 知らせを送れたら sendAckAtCentral が追跡をやめる。★配り直さない。
|
|
3492
|
+
// ★終了の事実も同じ項目で届ける(便 915694ac・1-1「追跡をやめた便・本文未着と申告した便でも届ける」)。
|
|
3493
|
+
// 画面へ貼る方式では実プロセスを中継が持っていないので、終わりは【つかめていない】と伝える
|
|
3494
|
+
// (processEndObserved:false・終了コードは null)。★作り話の終了を送らない。
|
|
3495
|
+
if (!state.pending.endFactsTold) {
|
|
3496
|
+
state.pending.endFactsTold = true;
|
|
3497
|
+
await deliverJobEndFacts(cfg, {
|
|
3498
|
+
commandId: state.pending.id,
|
|
3499
|
+
site: cfg.site,
|
|
3500
|
+
finishedAt: new Date().toISOString(),
|
|
3501
|
+
endKind: 'not_started',
|
|
3502
|
+
exitCode: null,
|
|
3503
|
+
exitSignal: null,
|
|
3504
|
+
reportDelivered: reportPhase(state.pending.id) !== 'none',
|
|
3505
|
+
processEndObserved: false,
|
|
3506
|
+
detail: '配り役が本文未着と申告した便(職人の処理は中継が持っていない)',
|
|
3507
|
+
}, { logLine: log });
|
|
3508
|
+
}
|
|
3341
3509
|
await sendAckAtCentral(cfg, state);
|
|
3342
3510
|
if (state.pending !== null && Date.now() - state.pending.sentAt > RELAY_PENDING_TIMEOUT_MS) {
|
|
3343
3511
|
// 管制へ知らせられないまま上限を過ぎた。★印も書けていなければ、管制は queued のまま返す
|
|
@@ -3365,6 +3533,20 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3365
3533
|
const report = notice.report.trim().length > 0 ? notice.report : null;
|
|
3366
3534
|
if (await patchStatus(cfg, state.pending.id, 'done', report)) {
|
|
3367
3535
|
log(`site=${cfg.site} id=${state.pending.id} done(Codexの通知${report != null ? '・報告添付' : ''})`);
|
|
3536
|
+
// ★終了の事実を同じ項目で届ける(便 915694ac・1-3)。この配り方では実プロセスを
|
|
3537
|
+
// 中継が持っていないので、終わりは【つかめていない】と伝える(終了コードは null)。
|
|
3538
|
+
// 届けるのは「中継が完了を確かめた」という事実だけ。★作り話の終了を送らない。
|
|
3539
|
+
await deliverJobEndFacts(cfg, {
|
|
3540
|
+
commandId: state.pending.id,
|
|
3541
|
+
site: cfg.site,
|
|
3542
|
+
finishedAt: new Date().toISOString(),
|
|
3543
|
+
endKind: 'completed',
|
|
3544
|
+
exitCode: null,
|
|
3545
|
+
exitSignal: null,
|
|
3546
|
+
reportDelivered: report != null || reportPhase(state.pending.id) !== 'none',
|
|
3547
|
+
processEndObserved: false,
|
|
3548
|
+
detail: '配り役が完了の知らせで確かめた(職人の処理は中継が持っていない)',
|
|
3549
|
+
}, { logLine: log });
|
|
3368
3550
|
state.pending = null;
|
|
3369
3551
|
}
|
|
3370
3552
|
return;
|
|
@@ -3374,8 +3556,22 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
3374
3556
|
if (below !== null && hasStandaloneDone(below, cfg.site)) {
|
|
3375
3557
|
// ★報告は画面から読まない。職人が書いたファイルを読む(relayReportDirs / readReport)。
|
|
3376
3558
|
// 画面を読む方式は窓が 16〜34 行しかなく、9/5 以降 162 回すべて失敗していた(調査 d1d16a56)。
|
|
3559
|
+
const reportThere = reportPhase(state.pending.id) !== 'none';
|
|
3377
3560
|
const ok = await completeWithReport(cfg, state.pending.id, log);
|
|
3378
3561
|
if (ok) {
|
|
3562
|
+
// ★終了の事実を同じ項目で届ける(便 915694ac・1-3)。画面へ貼る方式では実プロセスを
|
|
3563
|
+
// 中継が持っていないので、終わりは【つかめていない】と伝える(終了コードは null)。
|
|
3564
|
+
await deliverJobEndFacts(cfg, {
|
|
3565
|
+
commandId: state.pending.id,
|
|
3566
|
+
site: cfg.site,
|
|
3567
|
+
finishedAt: new Date().toISOString(),
|
|
3568
|
+
endKind: 'completed',
|
|
3569
|
+
exitCode: null,
|
|
3570
|
+
exitSignal: null,
|
|
3571
|
+
reportDelivered: reportThere,
|
|
3572
|
+
processEndObserved: false,
|
|
3573
|
+
detail: '配り役が画面の完了の合図で確かめた(職人の処理は中継が持っていない)',
|
|
3574
|
+
}, { logLine: log });
|
|
3379
3575
|
state.pending = null;
|
|
3380
3576
|
}
|
|
3381
3577
|
return; // sent 中は新規取得しない
|
package/package.json
CHANGED
|
@@ -106,6 +106,12 @@ APT_PACKAGES=(
|
|
|
106
106
|
python3-pip
|
|
107
107
|
python3-venv
|
|
108
108
|
rsync
|
|
109
|
+
# ★囲い(sandbox)の部品。無いと職人が起動を断る(設定 sandbox.failIfUnavailable のため)。
|
|
110
|
+
# えふさん確定 2026-10-02・判断1(b)=1行インストーラでここまで入れる。
|
|
111
|
+
# 実物(2026-10-02・ノートPC): これが無くて便 7901973d が3回続けて起動に失敗した。
|
|
112
|
+
# ★先頭の確かめ(preflight)は残す=入れられなかった機では止まって案内する。
|
|
113
|
+
bubblewrap # 囲いの本体。職人の書き込みを決めた場所だけに閉じる
|
|
114
|
+
socat # 囲いの中と外をつなぐ通信の中継
|
|
109
115
|
)
|
|
110
116
|
|
|
111
117
|
# 確認だけモード(何も入れない・何も聞かない)。
|
|
@@ -387,7 +393,7 @@ run() {
|
|
|
387
393
|
"$@"
|
|
388
394
|
}
|
|
389
395
|
|
|
390
|
-
# >>> preflight >>> ★この印の間が正本。install.sh
|
|
396
|
+
# >>> preflight >>> ★この印の間が正本。install.sh・update-guard.sh・setup.sh の3つで1文字も違わないことをテストが固定する。
|
|
391
397
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
392
398
|
# 先頭の確かめ(職人が始まるのに要る物・便 ff8f4be9)
|
|
393
399
|
#
|
|
@@ -432,6 +438,14 @@ preflight_ver_lt() {
|
|
|
432
438
|
return 1
|
|
433
439
|
}
|
|
434
440
|
|
|
441
|
+
# ホームの外の置き場(★src/headless.ts の CLAUDE_SYSTEM_BIN_DIRS と同じ・テストで一致を固定)。
|
|
442
|
+
# ★検査だけが作り物のフォルダに差し替える(CONDUCTOR_PREFLIGHT_SYSTEM_DIRS)。
|
|
443
|
+
# HOME を隔離してもここは絶対パスなので、機体に claude が在ると検査が機体に依って落ちる
|
|
444
|
+
# (実測 2026-10-02 メインPC: /usr/bin/claude = 2.1.86)。中継の側は便 ff8f4be9 で
|
|
445
|
+
# 同じ差し替え(resolveClaudeBin(systemDirs))を入れてある。作りをそろえる。
|
|
446
|
+
# ★差し替えても安全は変わらない(探す場所が変わるだけ・見つからなければ need では止まる)。
|
|
447
|
+
PREFLIGHT_SYSTEM_BIN_DIRS="${CONDUCTOR_PREFLIGHT_SYSTEM_DIRS-/usr/local/bin:/usr/bin}"
|
|
448
|
+
|
|
435
449
|
# 中継と同じ候補を同じ順で出す(1行に1つ)。
|
|
436
450
|
preflight_claude_candidates() {
|
|
437
451
|
if [[ -n "${CONDUCTOR_CLAUDE_BIN:-}" ]]; then
|
|
@@ -440,6 +454,7 @@ preflight_claude_candidates() {
|
|
|
440
454
|
fi
|
|
441
455
|
local d
|
|
442
456
|
local -a nv=()
|
|
457
|
+
local -a sysdirs=()
|
|
443
458
|
for d in "$HOME/.nvm/versions/node"/v*; do
|
|
444
459
|
[[ -d "$d" ]] && nv+=("${d##*/}")
|
|
445
460
|
done
|
|
@@ -449,7 +464,12 @@ preflight_claude_candidates() {
|
|
|
449
464
|
printf '%s\n' "$HOME/.nvm/versions/node/$d/bin/claude"
|
|
450
465
|
done < <(printf '%s\n' "${nv[@]}" | sort -t. -k1.2,1nr -k2,2nr -k3,3nr)
|
|
451
466
|
fi
|
|
452
|
-
printf '%s\n' "$HOME/.local/bin/claude" "$HOME/.claude/local/claude"
|
|
467
|
+
printf '%s\n' "$HOME/.local/bin/claude" "$HOME/.claude/local/claude"
|
|
468
|
+
IFS=':' read -r -a sysdirs <<< "$PREFLIGHT_SYSTEM_BIN_DIRS"
|
|
469
|
+
for d in ${sysdirs[@]+"${sysdirs[@]}"}; do
|
|
470
|
+
[[ -n "$d" ]] && printf '%s\n' "$d/claude"
|
|
471
|
+
done
|
|
472
|
+
return 0
|
|
453
473
|
}
|
|
454
474
|
|
|
455
475
|
# その claude の入れ方に合う更新の1行(src/headless.ts の claudeUpdateHint と同じ見分け方)。
|
package/sales-template/setup.sh
CHANGED
|
@@ -287,6 +287,207 @@ else
|
|
|
287
287
|
fi
|
|
288
288
|
SVC_RESTART_HINT="${SVC_RESTART_CMD} を実行してください"
|
|
289
289
|
|
|
290
|
+
# ─── ⓪ 職人が始まるのに要る物の確かめ(★install.sh・update-guard.sh と同じ正本) ──
|
|
291
|
+
# 【なぜ setup.sh にも入れるか(えふさん確定 2026-10-02・判断5-1/前便 ff8f4be9 の要判断3)】
|
|
292
|
+
# setup.sh を単独で打つ道(Mac・試作の型・開発機の作り直し)は、1行インストーラを通らない。
|
|
293
|
+
# その道だけ確かめが無いと、囲いの部品が無い機・古い Claude Code の機に設置してしまい、
|
|
294
|
+
# 設置は成功したのに職人が1件も始まらない(実物 2026-10-02・便 7901973d が3回続けて起動失敗)。
|
|
295
|
+
# ★印の間は3つの台本で1文字も違わない(test/preflight.test.ts が固定する)。ここを手で直さない。
|
|
296
|
+
# >>> preflight >>> ★この印の間が正本。install.sh・update-guard.sh・setup.sh の3つで1文字も違わないことをテストが固定する。
|
|
297
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
298
|
+
# 先頭の確かめ(職人が始まるのに要る物・便 ff8f4be9)
|
|
299
|
+
#
|
|
300
|
+
# 【なぜ要るか(実機 2026-10-02・ノートPC)】
|
|
301
|
+
# 便 7901973d が3回続けて起動に失敗した。囲いの部品 bubblewrap・socat が入っておらず、
|
|
302
|
+
# 設定 sandbox.failIfUnavailable のため職人が起動を断っていた。更新までは気づかれずに動き、
|
|
303
|
+
# 更新の後に職人が始まらなくなった。また、最低版(えふさん確定 2.1.282)より古い
|
|
304
|
+
# Claude Code では中継が職人を始めない。=設定を配る前にここで気づき、人が打つ1行を出す。
|
|
305
|
+
#
|
|
306
|
+
# 【決め】
|
|
307
|
+
# ・確かめるだけ。★台本が自分で sudo を打たない・勝手に入れない(人の窓)。
|
|
308
|
+
# ・Claude Code は中継(src/headless.ts の resolveClaudeBin)と同じ場所を同じ順で探し、
|
|
309
|
+
# いちばん新しい版で判定する(CONDUCTOR_CLAUDE_BIN の指定があればそれだけ)。
|
|
310
|
+
# ・囲いの部品は、その機の仕組みに要る物だけを見る。
|
|
311
|
+
# Linux(WSL を含む)… bwrap と socat
|
|
312
|
+
# Mac … OS に組み込み(追加の部品は要らない)=何も求めない
|
|
313
|
+
# それ以外 … 止めずに「確かめられませんでした」と出す
|
|
314
|
+
# ・出す1行は、そのままコピペで動く形(山かっこの穴埋めなし)。
|
|
315
|
+
#
|
|
316
|
+
# 【使い方】preflight_check <claude が無いときの扱い: need|later>
|
|
317
|
+
# 結果: PREFLIGHT_NG(足りない物の説明と1行。空なら通過)/PREFLIGHT_INFO(止めない注意)
|
|
318
|
+
# /PREFLIGHT_CLAUDE_BIN(中継が使うはずの claude・見つからなければ空)
|
|
319
|
+
# ★later … 後の段で Claude Code を入れる台本(1行インストーラ)用。無いことだけは止めない
|
|
320
|
+
# (古いときは止める)。
|
|
321
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
322
|
+
# ★src/headless.ts の CLAUDE_MIN_VERSION と同じ値(テストで一致を固定)。
|
|
323
|
+
PREFLIGHT_CLAUDE_MIN='2.1.282'
|
|
324
|
+
|
|
325
|
+
# a が b より古いか(数字3つだけを見る。読めない字は 0=古い扱い)。
|
|
326
|
+
preflight_ver_lt() {
|
|
327
|
+
local a="${1%%-*}" b="${2%%-*}" i x y
|
|
328
|
+
local -a A B
|
|
329
|
+
IFS='.' read -r -a A <<< "$a"
|
|
330
|
+
IFS='.' read -r -a B <<< "$b"
|
|
331
|
+
for i in 0 1 2; do
|
|
332
|
+
x="${A[i]:-0}"; y="${B[i]:-0}"
|
|
333
|
+
[[ "$x" =~ ^[0-9]+$ ]] || x=0
|
|
334
|
+
[[ "$y" =~ ^[0-9]+$ ]] || y=0
|
|
335
|
+
if (( x < y )); then return 0; fi
|
|
336
|
+
if (( x > y )); then return 1; fi
|
|
337
|
+
done
|
|
338
|
+
return 1
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# ホームの外の置き場(★src/headless.ts の CLAUDE_SYSTEM_BIN_DIRS と同じ・テストで一致を固定)。
|
|
342
|
+
# ★検査だけが作り物のフォルダに差し替える(CONDUCTOR_PREFLIGHT_SYSTEM_DIRS)。
|
|
343
|
+
# HOME を隔離してもここは絶対パスなので、機体に claude が在ると検査が機体に依って落ちる
|
|
344
|
+
# (実測 2026-10-02 メインPC: /usr/bin/claude = 2.1.86)。中継の側は便 ff8f4be9 で
|
|
345
|
+
# 同じ差し替え(resolveClaudeBin(systemDirs))を入れてある。作りをそろえる。
|
|
346
|
+
# ★差し替えても安全は変わらない(探す場所が変わるだけ・見つからなければ need では止まる)。
|
|
347
|
+
PREFLIGHT_SYSTEM_BIN_DIRS="${CONDUCTOR_PREFLIGHT_SYSTEM_DIRS-/usr/local/bin:/usr/bin}"
|
|
348
|
+
|
|
349
|
+
# 中継と同じ候補を同じ順で出す(1行に1つ)。
|
|
350
|
+
preflight_claude_candidates() {
|
|
351
|
+
if [[ -n "${CONDUCTOR_CLAUDE_BIN:-}" ]]; then
|
|
352
|
+
printf '%s\n' "$CONDUCTOR_CLAUDE_BIN"
|
|
353
|
+
return 0
|
|
354
|
+
fi
|
|
355
|
+
local d
|
|
356
|
+
local -a nv=()
|
|
357
|
+
local -a sysdirs=()
|
|
358
|
+
for d in "$HOME/.nvm/versions/node"/v*; do
|
|
359
|
+
[[ -d "$d" ]] && nv+=("${d##*/}")
|
|
360
|
+
done
|
|
361
|
+
if (( ${#nv[@]} > 0 )); then
|
|
362
|
+
# 新しい node の版から(v24.21.0 → 24 21 0 で降順)
|
|
363
|
+
while IFS= read -r d; do
|
|
364
|
+
printf '%s\n' "$HOME/.nvm/versions/node/$d/bin/claude"
|
|
365
|
+
done < <(printf '%s\n' "${nv[@]}" | sort -t. -k1.2,1nr -k2,2nr -k3,3nr)
|
|
366
|
+
fi
|
|
367
|
+
printf '%s\n' "$HOME/.local/bin/claude" "$HOME/.claude/local/claude"
|
|
368
|
+
IFS=':' read -r -a sysdirs <<< "$PREFLIGHT_SYSTEM_BIN_DIRS"
|
|
369
|
+
for d in ${sysdirs[@]+"${sysdirs[@]}"}; do
|
|
370
|
+
[[ -n "$d" ]] && printf '%s\n' "$d/claude"
|
|
371
|
+
done
|
|
372
|
+
return 0
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
# その claude の入れ方に合う更新の1行(src/headless.ts の claudeUpdateHint と同じ見分け方)。
|
|
376
|
+
preflight_claude_update_line() {
|
|
377
|
+
local bin="$1" real="$1" t i
|
|
378
|
+
# 実体を辿る(★readlink -f は使わない=無いパスでも文字列を返すため。1段ずつ辿る)
|
|
379
|
+
for i in 1 2 3 4 5 6 7 8; do
|
|
380
|
+
[[ -L "$real" ]] || break
|
|
381
|
+
t="$(readlink "$real" 2>/dev/null || true)"
|
|
382
|
+
[[ -n "$t" ]] || break
|
|
383
|
+
case "$t" in
|
|
384
|
+
/*) real="$t" ;;
|
|
385
|
+
*) real="$(dirname "$real")/$t" ;;
|
|
386
|
+
esac
|
|
387
|
+
done
|
|
388
|
+
case "$real$bin" in
|
|
389
|
+
*/node_modules/@anthropic-ai/claude-code/*|*/.nvm/versions/node/*)
|
|
390
|
+
printf ' PATH="%s:$PATH" npm install -g @anthropic-ai/claude-code@latest\n' "$(dirname "$bin")"
|
|
391
|
+
return 0 ;;
|
|
392
|
+
esac
|
|
393
|
+
case "$real" in
|
|
394
|
+
"$HOME"/.local/share/claude/*|"$HOME"/.claude/local/*)
|
|
395
|
+
printf ' "%s" update\n' "$bin"
|
|
396
|
+
return 0 ;;
|
|
397
|
+
esac
|
|
398
|
+
printf ' (入れ方を見分けられませんでした。どちらか合う方を打ってください)\n'
|
|
399
|
+
printf ' ネイティブ導入なら: "%s" update\n' "$bin"
|
|
400
|
+
printf ' npm 導入なら : npm install -g @anthropic-ai/claude-code@latest\n'
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
preflight_check() {
|
|
404
|
+
local claude_mode="${1:-need}" c v best='' best_v='' unknown='' os missing=''
|
|
405
|
+
PREFLIGHT_NG=''
|
|
406
|
+
PREFLIGHT_INFO=''
|
|
407
|
+
PREFLIGHT_CLAUDE_BIN=''
|
|
408
|
+
|
|
409
|
+
# ── ① Claude Code が在り、最低版以上か ──
|
|
410
|
+
while IFS= read -r c; do
|
|
411
|
+
[[ -n "$c" && -x "$c" ]] || continue
|
|
412
|
+
v="$("$c" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)"
|
|
413
|
+
if [[ -z "$v" ]]; then
|
|
414
|
+
[[ -n "$unknown" ]] || unknown="$c"
|
|
415
|
+
continue
|
|
416
|
+
fi
|
|
417
|
+
if [[ -z "$best_v" ]] || preflight_ver_lt "$best_v" "$v"; then
|
|
418
|
+
best="$c"; best_v="$v"
|
|
419
|
+
fi
|
|
420
|
+
done < <(preflight_claude_candidates)
|
|
421
|
+
|
|
422
|
+
# shellcheck disable=SC2034 # update-guard.sh の ⓪-b が使う(install.sh では使わない)
|
|
423
|
+
PREFLIGHT_CLAUDE_BIN="${best:-$unknown}"
|
|
424
|
+
if [[ -n "$best_v" ]]; then
|
|
425
|
+
if preflight_ver_lt "$best_v" "$PREFLIGHT_CLAUDE_MIN"; then
|
|
426
|
+
PREFLIGHT_NG+="■ 職人のソフト(Claude Code)が古いです(${best} = ${best_v}、必要な版 ${PREFLIGHT_CLAUDE_MIN} 以上)。"$'\n'
|
|
427
|
+
PREFLIGHT_NG+=" この機の入れ方に合う1行:"$'\n'
|
|
428
|
+
PREFLIGHT_NG+="$(preflight_claude_update_line "$best")"$'\n'
|
|
429
|
+
else
|
|
430
|
+
PREFLIGHT_INFO+="✓ 職人のソフト(Claude Code): ${best} = ${best_v}(必要な版 ${PREFLIGHT_CLAUDE_MIN} 以上)"$'\n'
|
|
431
|
+
fi
|
|
432
|
+
elif [[ -n "$unknown" ]]; then
|
|
433
|
+
# ★中継も版の読めない claude は止めずに使う。ここでも止めない(作りをそろえる)。
|
|
434
|
+
PREFLIGHT_INFO+="⚠ 職人のソフト(Claude Code)の版を確かめられませんでした(${unknown})。"$'\n'
|
|
435
|
+
elif [[ "$claude_mode" == "need" ]]; then
|
|
436
|
+
PREFLIGHT_NG+="■ 職人のソフト(Claude Code)が見つかりません。"$'\n'
|
|
437
|
+
PREFLIGHT_NG+=" 入れる1行:"$'\n'
|
|
438
|
+
PREFLIGHT_NG+=" npm install -g @anthropic-ai/claude-code@latest"$'\n'
|
|
439
|
+
fi
|
|
440
|
+
|
|
441
|
+
# ── ② 囲いの部品(その機の仕組みに要る物だけ) ──
|
|
442
|
+
os="$(uname -s 2>/dev/null || echo unknown)"
|
|
443
|
+
case "$os" in
|
|
444
|
+
Linux)
|
|
445
|
+
command -v bwrap >/dev/null 2>&1 || missing+=' bwrap(bubblewrap)'
|
|
446
|
+
command -v socat >/dev/null 2>&1 || missing+=' socat'
|
|
447
|
+
if [[ -n "$missing" ]]; then
|
|
448
|
+
PREFLIGHT_NG+="■ 囲いの部品が足りません:${missing}"$'\n'
|
|
449
|
+
PREFLIGHT_NG+=" (無いと、職人が安全のため起動を断ります)"$'\n'
|
|
450
|
+
if command -v apt-get >/dev/null 2>&1; then
|
|
451
|
+
PREFLIGHT_NG+=" 入れる1行(Ubuntu のパスワードを聞かれます):"$'\n'
|
|
452
|
+
PREFLIGHT_NG+=" sudo apt-get install -y bubblewrap socat"$'\n'
|
|
453
|
+
# ★WSL かどうか(検査は CONDUCTOR_PROC_VERSION_FILE で見本を渡せる・既定は本物)
|
|
454
|
+
if [[ -r "${CONDUCTOR_PROC_VERSION_FILE:-/proc/version}" ]] && grep -qi microsoft "${CONDUCTOR_PROC_VERSION_FILE:-/proc/version}"; then
|
|
455
|
+
PREFLIGHT_NG+=" sudo のパスワードが分からないとき(Windows の PowerShell で打つ):"$'\n'
|
|
456
|
+
PREFLIGHT_NG+=" wsl.exe -u root apt-get install -y bubblewrap socat"$'\n'
|
|
457
|
+
fi
|
|
458
|
+
else
|
|
459
|
+
PREFLIGHT_NG+=" この機の入れ方で bubblewrap と socat を入れてください(apt-get が見つかりません)。"$'\n'
|
|
460
|
+
fi
|
|
461
|
+
else
|
|
462
|
+
PREFLIGHT_INFO+="✓ 囲いの部品: bwrap・socat"$'\n'
|
|
463
|
+
fi
|
|
464
|
+
;;
|
|
465
|
+
Darwin)
|
|
466
|
+
PREFLIGHT_INFO+="✓ 囲いの部品: Mac は OS に組み込み(追加の部品は要りません)"$'\n'
|
|
467
|
+
;;
|
|
468
|
+
*)
|
|
469
|
+
PREFLIGHT_INFO+="⚠ 囲いの部品: この機(${os})では確かめられませんでした(止めずに進みます)。"$'\n'
|
|
470
|
+
;;
|
|
471
|
+
esac
|
|
472
|
+
[[ -z "$PREFLIGHT_NG" ]]
|
|
473
|
+
}
|
|
474
|
+
# <<< preflight <<<
|
|
475
|
+
|
|
476
|
+
step "⓪ 職人が始まるのに要る物の確かめ(★ここで足りなければ1つも置きません)"
|
|
477
|
+
# ★`later`(職人が【無いこと】だけはここで止めない)を使う理由。
|
|
478
|
+
# setup.sh は Claude Code を入れないが、「無い」ときの案内は ① が持っている
|
|
479
|
+
# (入れる2行+ログインの手順=お客さまがそのまま読める形)。ここで `need` にすると
|
|
480
|
+
# その案内に届かず、短い1行だけで終わってしまう。★古い版・囲いの部品不足は、ここで止める。
|
|
481
|
+
if preflight_check later; then
|
|
482
|
+
if [[ -n "$PREFLIGHT_INFO" ]]; then printf '%s' "$PREFLIGHT_INFO"; fi
|
|
483
|
+
else
|
|
484
|
+
printf '%s' "$PREFLIGHT_INFO" >&9
|
|
485
|
+
echo >&9
|
|
486
|
+
printf '%s' "$PREFLIGHT_NG" >&9
|
|
487
|
+
echo "[setup] 上の1行を打ってから、もう一度 setup.sh を実行してください。" >&9
|
|
488
|
+
exit 1
|
|
489
|
+
fi
|
|
490
|
+
|
|
290
491
|
# ─── ① 前提チェック ──────────────────────────────────────────────────────────
|
|
291
492
|
step "① 前提チェック"
|
|
292
493
|
|
package/scripts/hooks/policy.py
CHANGED
|
@@ -2147,18 +2147,100 @@ def rm_path_risk(path: str, work_dir: str = None) -> str:
|
|
|
2147
2147
|
return 'safe'
|
|
2148
2148
|
|
|
2149
2149
|
|
|
2150
|
+
# 削除命令の語そのもの(字面を探すための形)。
|
|
2151
|
+
_RM_WORD_RE = re.compile(r'\brm\b')
|
|
2152
|
+
|
|
2153
|
+
# ★探すだけの道具(引数に書かれた「探す言葉」は実行されない)。
|
|
2154
|
+
# 【なぜ要るか(実物 2026-10-02)】職人が鉄則や台本を調べるとき、探す言葉に
|
|
2155
|
+
# 「rm -rf .git」のような字が入る。それを削除命令として読むと、調べる作業が毎回止まる。
|
|
2156
|
+
# ★ここで免じるのは【引用符で囲まれた中に在る字】だけ。囲みの外に在る削除命令は1件も免じない。
|
|
2157
|
+
_SEARCH_ONLY_VERBS = frozenset(_RECURSIVE_GREP_VERBS | _HIDDEN_SEARCH_VERBS)
|
|
2158
|
+
|
|
2159
|
+
# 引用符で囲まれた1かたまり(中に同じ引用符を含まない形)。
|
|
2160
|
+
# ★名前を _QUOTED_RE にしない(上の「プログラムの中の文字列リテラル」と別物)。
|
|
2161
|
+
_QUOTED_SPAN_RE = re.compile(r"'[^']*'|\"[^\"]*\"")
|
|
2162
|
+
|
|
2163
|
+
|
|
2164
|
+
def _blank_quoted(text: str) -> str:
|
|
2165
|
+
"""引用符で囲まれた中身を同じ長さの空白にする(★長さと位置を変えない)。"""
|
|
2166
|
+
return _QUOTED_SPAN_RE.sub(lambda m: ' ' * len(m.group(0)), text)
|
|
2167
|
+
|
|
2168
|
+
|
|
2169
|
+
def _is_search_only(toks) -> bool:
|
|
2170
|
+
"""このセグメントが「探すだけ」の呼び出しか(下位コマンドの形も見る)。"""
|
|
2171
|
+
verb = verb_of(toks)
|
|
2172
|
+
if verb in _SEARCH_ONLY_VERBS:
|
|
2173
|
+
return True
|
|
2174
|
+
# `git grep …`(版管理の道具の探す口)も探すだけ。
|
|
2175
|
+
return bool(toks) and verb == 'git' and any(t in _SEARCH_ONLY_VERBS for t in toks[1:3])
|
|
2176
|
+
|
|
2177
|
+
|
|
2178
|
+
def _rm_only_in_search_words(command: str) -> bool:
|
|
2179
|
+
"""削除命令の字が【探すだけの道具の引用符の中】にだけ在るか。
|
|
2180
|
+
|
|
2181
|
+
★免じるのはこれが True のときだけ。少しでも判断が付かなければ False(=止める側へ倒す)。
|
|
2182
|
+
・引用符の外に削除命令の字が在るセグメントが1つでも在れば False。
|
|
2183
|
+
・引用符の中に在っても、そのセグメントが探すだけの道具でなければ False。
|
|
2184
|
+
・どこにも見つからなければ False(ヒアドキュメントの本文など、
|
|
2185
|
+
引用符を割らない読み方では覆えない字面=安全側)。
|
|
2186
|
+
"""
|
|
2187
|
+
found_in_search_words = False
|
|
2188
|
+
for seg in split_commands(command, bodies=False):
|
|
2189
|
+
has_rm = bool(_RM_WORD_RE.search(seg))
|
|
2190
|
+
if not has_rm:
|
|
2191
|
+
continue
|
|
2192
|
+
if _RM_WORD_RE.search(_blank_quoted(seg)):
|
|
2193
|
+
return False # 囲みの外に在る=本物の削除命令として扱う
|
|
2194
|
+
if not _is_search_only(tokens_of(seg)):
|
|
2195
|
+
return False
|
|
2196
|
+
found_in_search_words = True
|
|
2197
|
+
return found_in_search_words
|
|
2198
|
+
|
|
2199
|
+
|
|
2200
|
+
def _rm_target_risk(token: str, work_dir: str = None) -> str:
|
|
2201
|
+
"""削除の対象1つぶんの危険度。★シェルの飾り付きの字面と素の字面の、危ない方を採る。
|
|
2202
|
+
|
|
2203
|
+
【なぜ両方見るか(実物 2026-10-02)】`$(rm -rf .git)` の対象は `.git)` として取り出され、
|
|
2204
|
+
閉じ括弧が付いているせいで保護対象(.git)に当たらず素通りしていた。
|
|
2205
|
+
飾りを剥がした字面も測り、危ない方を採る=今まで止めていた形は1件も緩まない。
|
|
2206
|
+
"""
|
|
2207
|
+
risks = {rm_path_risk(token, work_dir=work_dir)}
|
|
2208
|
+
bare = token.strip().strip(_REDIRECT_TRIM)
|
|
2209
|
+
if bare != token:
|
|
2210
|
+
risks.add(rm_path_risk(bare, work_dir=work_dir))
|
|
2211
|
+
if 'dangerous' in risks:
|
|
2212
|
+
return 'dangerous'
|
|
2213
|
+
if 'unknown' in risks:
|
|
2214
|
+
return 'unknown'
|
|
2215
|
+
return 'safe'
|
|
2216
|
+
|
|
2217
|
+
|
|
2150
2218
|
def _rm_danger(command: str, work_dir: str = None):
|
|
2219
|
+
"""削除命令の対象を全部見て、危険なものがあれば理由を返す。
|
|
2220
|
+
|
|
2221
|
+
★止める側の読み方(機械的な区切り)は変えない。探すだけの道具の「探す言葉」に
|
|
2222
|
+
当たったときだけ、最後に免じる(_rm_only_in_search_words)。
|
|
2223
|
+
"""
|
|
2224
|
+
hit = _rm_danger_scan(command, work_dir=work_dir)
|
|
2225
|
+
if hit is None:
|
|
2226
|
+
return None
|
|
2227
|
+
if _rm_only_in_search_words(command):
|
|
2228
|
+
return None
|
|
2229
|
+
return hit
|
|
2230
|
+
|
|
2231
|
+
|
|
2232
|
+
def _rm_danger_scan(command: str, work_dir: str = None):
|
|
2151
2233
|
"""再帰的 rm の対象パスを全部見て、危険なものがあれば理由を返す。"""
|
|
2152
2234
|
if work_dir is None:
|
|
2153
2235
|
work_dir = os.environ.get('CONDUCTOR_WORK_DIR') or None
|
|
2154
|
-
for m in
|
|
2236
|
+
for m in _RM_WORD_RE.finditer(command):
|
|
2155
2237
|
seg = _SPLIT_RE.split(command[m.end():], maxsplit=1)[0]
|
|
2156
2238
|
toks = seg.split()
|
|
2157
2239
|
flags = [t for t in toks if t.startswith('-')]
|
|
2158
2240
|
recursive = any(('r' in f.lstrip('-').lower()) or f in ('--recursive',) for f in flags)
|
|
2159
2241
|
paths = [t for t in toks if not t.startswith('-')]
|
|
2160
2242
|
for p in paths:
|
|
2161
|
-
risk =
|
|
2243
|
+
risk = _rm_target_risk(p, work_dir=work_dir)
|
|
2162
2244
|
if risk == 'dangerous':
|
|
2163
2245
|
return f'rm on dangerous path ({p})'
|
|
2164
2246
|
if risk == 'unknown' and recursive:
|
|
@@ -1912,6 +1912,70 @@ finally:
|
|
|
1912
1912
|
else:
|
|
1913
1913
|
os.environ['CONDUCTOR_WORK_DIR'] = _wd_env2
|
|
1914
1914
|
|
|
1915
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1916
|
+
# 検索の「探す言葉」の中の削除命令(便 915694ac・えふさん確定 2026-10-02)
|
|
1917
|
+
#
|
|
1918
|
+
# 【実物】職人が鉄則や台本を調べるとき、探す言葉に「rm -rf .git」のような字が入る。
|
|
1919
|
+
# 番人はそれを削除命令として読み、調べる作業を毎回止めていた。
|
|
1920
|
+
# 【直し】探すだけの道具(grep 系・rg 系・git grep)の【引用符の中】に在る字は、
|
|
1921
|
+
# 削除命令として数えない。囲みの外に在る字は1件も免じない。
|
|
1922
|
+
# ★安全は1件も緩めない=下の「止まり続ける形」をすべて固定する。
|
|
1923
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1924
|
+
print('\n=== 探す言葉の中の削除命令は、削除命令として読まない ===')
|
|
1925
|
+
_SEARCH_OK = [
|
|
1926
|
+
"grep -rn 'rm ' skills docs",
|
|
1927
|
+
'grep -rn "rm -rf" skills',
|
|
1928
|
+
"grep -n 'rm -rf .git' skills/conductor-craftsman/SKILL.md",
|
|
1929
|
+
"grep -rn 'rm -r /tmp/claude-1000/sagyou' skills",
|
|
1930
|
+
"grep -rln 'rm -r ~' docs",
|
|
1931
|
+
'grep -rn "rm -rf $HOME" docs',
|
|
1932
|
+
"git grep -n 'rm -rf .git' -- skills docs",
|
|
1933
|
+
"rg -n 'rm -rf /' docs",
|
|
1934
|
+
"rg 'rm -rf .tyhld' skills",
|
|
1935
|
+
"grep -rn '-f なしの rm -r' skills",
|
|
1936
|
+
]
|
|
1937
|
+
for _c in _SEARCH_OK:
|
|
1938
|
+
check(f'探す言葉: {_c}', policy.decide_bash(_c)['decision'], policy.ALLOW)
|
|
1939
|
+
|
|
1940
|
+
print('\n=== 探す言葉に見せても、本当に消す形は止まり続ける ===')
|
|
1941
|
+
# ★この一覧はえふさん確定(2026-10-02)の「今までどおり止まること」の全件。
|
|
1942
|
+
_STILL_DENY = [
|
|
1943
|
+
# 包みの中(bash -c・sh -c・eval)
|
|
1944
|
+
"bash -c 'rm -rf .git'",
|
|
1945
|
+
'sh -c "rm -rf .git"',
|
|
1946
|
+
"eval 'rm -rf .git'",
|
|
1947
|
+
'bash -c "grep -rn \'x\' docs; rm -rf .git"',
|
|
1948
|
+
# 命令の置き換え($( ) と ` `)
|
|
1949
|
+
'$(rm -rf .git)',
|
|
1950
|
+
'echo $(rm -rf .git)',
|
|
1951
|
+
'X=$(rm -rf .git)',
|
|
1952
|
+
'`rm -rf .git`',
|
|
1953
|
+
'echo `rm -rf .git`',
|
|
1954
|
+
'$(rm -r ~)',
|
|
1955
|
+
# ; && | で続けた後ろ
|
|
1956
|
+
'echo x; rm -rf .git',
|
|
1957
|
+
"grep -rn 'x' docs && rm -rf .git",
|
|
1958
|
+
"grep -rn 'x' docs | rm -rf .git",
|
|
1959
|
+
"grep -rn 'rm -rf' docs; rm -r .tyhld",
|
|
1960
|
+
# 探す命令に見せかけて後ろで消す形
|
|
1961
|
+
"grep -rn 'rm -rf .git' docs; rm -rf .git",
|
|
1962
|
+
"grep -rn 'rm -rf' skills && rm -r ~",
|
|
1963
|
+
# xargs・find の -exec(消す先が字面に出る形)
|
|
1964
|
+
'xargs rm -rf .git',
|
|
1965
|
+
"find . -name '*.tmp' -exec rm -rf .git {} +",
|
|
1966
|
+
'find docs -type f -exec rm -r {} +',
|
|
1967
|
+
# 変数・* を含む再帰削除(今までどおり)
|
|
1968
|
+
'rm -rf $DIR/x',
|
|
1969
|
+
'rm -rf build/*',
|
|
1970
|
+
]
|
|
1971
|
+
for _c in _STILL_DENY:
|
|
1972
|
+
check(f'止まり続ける: {_c}', policy.decide_bash(_c)['decision'], policy.DENY)
|
|
1973
|
+
|
|
1974
|
+
print('\n=== -f なしの実在パスの削除は今までどおり通る ===')
|
|
1975
|
+
for _c in ['rm -r /tmp/claude-1000/sagyou', 'rm /tmp/claude-1000/a.html',
|
|
1976
|
+
'rm -rf node_modules', 'rm -rf dist']:
|
|
1977
|
+
check(f'通る: {_c}', policy.decide_bash(_c)['decision'], policy.ALLOW)
|
|
1978
|
+
|
|
1915
1979
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
1916
1980
|
print('\n' + '=' * 72)
|
|
1917
1981
|
if failures:
|
package/scripts/update-guard.sh
CHANGED
|
@@ -80,7 +80,7 @@ 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
|
|
83
|
+
# >>> preflight >>> ★この印の間が正本。install.sh・update-guard.sh・setup.sh の3つで1文字も違わないことをテストが固定する。
|
|
84
84
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
85
85
|
# 先頭の確かめ(職人が始まるのに要る物・便 ff8f4be9)
|
|
86
86
|
#
|
|
@@ -125,6 +125,14 @@ preflight_ver_lt() {
|
|
|
125
125
|
return 1
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
# ホームの外の置き場(★src/headless.ts の CLAUDE_SYSTEM_BIN_DIRS と同じ・テストで一致を固定)。
|
|
129
|
+
# ★検査だけが作り物のフォルダに差し替える(CONDUCTOR_PREFLIGHT_SYSTEM_DIRS)。
|
|
130
|
+
# HOME を隔離してもここは絶対パスなので、機体に claude が在ると検査が機体に依って落ちる
|
|
131
|
+
# (実測 2026-10-02 メインPC: /usr/bin/claude = 2.1.86)。中継の側は便 ff8f4be9 で
|
|
132
|
+
# 同じ差し替え(resolveClaudeBin(systemDirs))を入れてある。作りをそろえる。
|
|
133
|
+
# ★差し替えても安全は変わらない(探す場所が変わるだけ・見つからなければ need では止まる)。
|
|
134
|
+
PREFLIGHT_SYSTEM_BIN_DIRS="${CONDUCTOR_PREFLIGHT_SYSTEM_DIRS-/usr/local/bin:/usr/bin}"
|
|
135
|
+
|
|
128
136
|
# 中継と同じ候補を同じ順で出す(1行に1つ)。
|
|
129
137
|
preflight_claude_candidates() {
|
|
130
138
|
if [[ -n "${CONDUCTOR_CLAUDE_BIN:-}" ]]; then
|
|
@@ -133,6 +141,7 @@ preflight_claude_candidates() {
|
|
|
133
141
|
fi
|
|
134
142
|
local d
|
|
135
143
|
local -a nv=()
|
|
144
|
+
local -a sysdirs=()
|
|
136
145
|
for d in "$HOME/.nvm/versions/node"/v*; do
|
|
137
146
|
[[ -d "$d" ]] && nv+=("${d##*/}")
|
|
138
147
|
done
|
|
@@ -142,7 +151,12 @@ preflight_claude_candidates() {
|
|
|
142
151
|
printf '%s\n' "$HOME/.nvm/versions/node/$d/bin/claude"
|
|
143
152
|
done < <(printf '%s\n' "${nv[@]}" | sort -t. -k1.2,1nr -k2,2nr -k3,3nr)
|
|
144
153
|
fi
|
|
145
|
-
printf '%s\n' "$HOME/.local/bin/claude" "$HOME/.claude/local/claude"
|
|
154
|
+
printf '%s\n' "$HOME/.local/bin/claude" "$HOME/.claude/local/claude"
|
|
155
|
+
IFS=':' read -r -a sysdirs <<< "$PREFLIGHT_SYSTEM_BIN_DIRS"
|
|
156
|
+
for d in ${sysdirs[@]+"${sysdirs[@]}"}; do
|
|
157
|
+
[[ -n "$d" ]] && printf '%s\n' "$d/claude"
|
|
158
|
+
done
|
|
159
|
+
return 0
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
# その claude の入れ方に合う更新の1行(src/headless.ts の claudeUpdateHint と同じ見分け方)。
|
|
@@ -109,7 +109,7 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
|
|
|
109
109
|
- ① **起動は必ず `chromium_sandbox=False`**(`--no-sandbox`)。付けないと起動できない。
|
|
110
110
|
- ② **道具は共有の場所を使う。自分でダウンロードしない。** python は `$HOME/.tyhld/pw-venv/bin/python`、`PLAYWRIGHT_BROWSERS_PATH` は `$HOME/.tyhld/pw-browsers`。**無ければそこで止めて「人の手番=`APPLY=1 bash scripts/pw-browser-install.sh`」と報告する。** `playwright install` も `pip install playwright` も職人は実行しない(毎便まるごとダウンロードが走り、便が遅くなる。通信自体は鉄則12のとおり許可済みだが、取りに行かないのが本筋=ADR-010)。
|
|
111
111
|
- ③ **localhost の画面検品は proxy を指定しない。** プロキシ `localhost:3128` はサンドボックスの中にしか居ないので、外部URLへ出るときだけ Playwright の proxy オプションへ明示指定する(環境変数は拾われない)。`ECONNREFUSED` なら回避せず**未実施として報告**する。
|
|
112
|
-
- ④ 検品用の使い捨て画面(確認だけのHTML/スクリプト)は**リポジトリの中に作らない。置き場は `/tmp/claude-1000/`**(鉄則13
|
|
112
|
+
- ④ 検品用の使い捨て画面(確認だけのHTML/スクリプト)は**リポジトリの中に作らない。置き場は `/tmp/claude-1000/`**(鉄則13)。リポジトリの中に作ってしまったものはコミット前に削除する。**自分で作った物に限り、`-f` を付けない `rm -r` と実在のパスの名指しで消し、消した物を報告に書く**(鉄則13)。★止められたら別の消し方を探さず報告する。
|
|
113
113
|
9. **pnpm は取りに行かない・版を指定しない(ADR-015)** — ★`npx --yes pnpm@10 …` と打たない(毎回ダウンロードが走る)。`pnpm` とだけ打てば、そのリポジトリの `packageManager` の版が corepack で自動的に使われる。
|
|
114
114
|
- ★**必ずリポジトリの中で打つ。** 版の指定はディレクトリごとに効くので、リポジトリの外(ホーム等)から `--filter` で呼ぶと既定の新しい版が動き `ERR_PNPM_UNSUPPORTED_ENGINE` で弾かれる(実測)。
|
|
115
115
|
- `pnpm` が見つからない/版が合わないときは、回避せず**人の手番=`corepack enable`** と報告する。`npm i -g pnpm` は打たない(corepack と衝突する)。
|
|
@@ -120,10 +120,17 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
|
|
|
120
120
|
- **電話は自由。** 外への通信(npm / GitHub / API / ドキュメント取得など)は許可済みなので、`Network request outside of sandbox` は出ない。**通信のために止まらない・人を待たない**。最後まで自走して完了まで行く。
|
|
121
121
|
- **金庫は禁止。** 秘密の持ち出し(`curl -d @.env` / `cp .env` / `tar ~/.ssh` / `base64 .env` / `echo $DATABASE_URL` など)は**番人が今までどおり止める**。通信が自由になったこととは**別の仕組み**なので、「通信が許可されたから秘密も送れる」は成り立たない。本番DB・secret・`.env`・`hooks`・`.tyhld`・`.ssh` への書き込み禁止もそのまま。
|
|
122
122
|
- **それでも塞がれたら回避しない。** `curl: (56) CONNECT tunnel failed, response 403` のように通信が塞がれたときは、**意図的に閉じてある接続先**(例: 本番DBへの直結)か、設定がその機体にまだ届いていないかのどちらか。**別経路を探さず、そこで止めて報告する**(人の手番=`APPLY=1 bash scripts/update-guard.sh` → 職人を再起動)。
|
|
123
|
-
13. **使い捨てファイルはリポジトリの中に作らない(えふさん決定 2026-09-03
|
|
124
|
-
- **実機検品・検証用の使い捨てファイル(HTML・画像・ログ等)は、リポジトリの中に作らない。`/tmp/claude-1000/`
|
|
125
|
-
-
|
|
126
|
-
|
|
123
|
+
13. **使い捨てファイルはリポジトリの中に作らない(えふさん決定 2026-09-03/えふさん確定 2026-10-02・判断6 で後片付けの形を改めた)**
|
|
124
|
+
- **実機検品・検証用の使い捨てファイル(HTML・画像・ログ等)は、リポジトリの中に作らない。`/tmp/claude-1000/` に置く。**
|
|
125
|
+
- **リポジトリの中(直下を含む)に作ってしまったときの後片付け(えふさん確定 2026-10-02・判断6)。自分で作った物に限り、`-f` を付けない `rm -r` と実在のパスの名指しで消し、消した物を報告に書く。**
|
|
126
|
+
- 通る例: `rm -r ./_layout-check.html` / `rm -r node_modules/.tmp-作業名`(★実在のパスを名指し・`-f` を付けない)
|
|
127
|
+
- ★**はじめから在った物には触らない。** 自分が作った物かどうか分からなければ消さず、報告に「残骸あり・場所」と書く。
|
|
128
|
+
- ★**`.git`・`.claude/hooks`・`.tyhld` は、相対パスでも削除禁止**(番人が止める。緩めない)。
|
|
129
|
+
- ★**`-f` 付きの再帰削除(`rm -rf` 等)・変数や `*` を含む形は止まる**(禁止の一覧と番人)。
|
|
130
|
+
- ★**止められたら別の消し方(`find -delete`・`python -c` 等)を探さず、消せなかった場所を報告に書く**(鉄則4)。
|
|
131
|
+
- **なぜ(置き場の話)。** 実機 2026-09-03: ai-saku の画面便(PR#258・PR#269)で、検品用の使い捨てページ `_layout-check.html` をリポジトリ直下に作り、`rm -rf` で消そうとして止められ、人が2回消した。**禁止の一覧と番人の制限は正しい(緩めない)。悪いのは置き場。**
|
|
132
|
+
- **なぜ(後片付けの話を改めた理由)。** 旧い文言は「リポジトリ直下は番人が `rm` を止めるので自分で消さず報告」だった。実測(便 fb248f6e)では、`-f` を付けない `rm -r` と実在のパスの名指しなら、リポジトリ直下の自分の作業ファイルも通る。止まるのは `-f` 付きと変数・`*` 入りだけ。**止まらない形を「止まる」と書いていると、職人が自分の残骸を残したまま便を終える**ので、実物に合わせた(番人・禁止の一覧は1行も変えていない)。
|
|
133
|
+
- 置き場を `/tmp/claude-1000/` にすれば、止められることも、残骸がコミットに混ざることも起きない(根治=鉄則10)。Playwright の検品ページも同じ(鉄則8-④)。
|
|
127
134
|
14. **`.env` 類(`.env`・`.env.local`・`.env.example` などの雛形も含む)は職人が書き換えない(えふさん確定 2026-09-18・便 3d11f913)**
|
|
128
135
|
- **番人は `.env` 本体も `.env.example` などの雛形も、書き込みを止める**(Write/Edit の設定 deny と、Bash の `echo`/`tee`/`sed -i`/`cp`/`mv` などを、経路を問わず同じ結果にそろえた)。読み込みは今までどおり(雛形は読める・本物は読めない)。
|
|
129
136
|
- **なぜ。** 雛形(`.env.example`)は普通は秘密値を置かないが、「exampleだから常に安全」と番人へ例外を作ると、将来誤って実値が書かれたときの穴になる(利便性より安全側・えふさん確定)。
|
|
@@ -155,6 +162,7 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
|
|
|
155
162
|
- **`-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
163
|
- 通る例: `rm -r /tmp/claude-1000/作業名` / `rm -r node_modules/.tmp-作業名`(★`-f` を付けない・自分で作った物だけ)
|
|
157
164
|
- 止まる例: `rm -rf /tmp/claude-1000/作業名`(禁止の一覧)/ `rm -r "$DIR/x"`・`rm -r build/*`(番人)/ `rm -r .git`・`rm -r .claude/hooks`・`rm -r .tyhld`(番人=相対パスでも消せない)
|
|
165
|
+
- ★**探すだけの道具の「探す言葉」に `rm …` と書いても、もう止まらない**(便 915694ac で直した)。鉄則や台本を調べるときの `grep -rn 'rm -rf .git' skills`・`git grep -n 'rm -r' -- skills`・`rg 'rm -rf /' docs` は通る。★囲みの外に書いた `rm` は今までどおり測るので、`grep -rn 'x' docs; rm -rf .git` のように**後ろで消す形は止まる**。
|
|
158
166
|
- **作業場所が変数のままの `git push`**(`git -C "$DIR" push`・行き先のブランチを確かめられない形)→ 止まる。**リポの中で、ブランチを名指しして** `git push -u origin <作業ブランチ名>`。
|
|
159
167
|
|
|
160
168
|
## 便の終え方
|