@tyhld/conductor 0.12.92 → 0.12.93
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 +38 -9
- package/dist/realtime-parse.js +17 -0
- package/dist/realtime.js +47 -13
- package/dist/selfcheck.js +25 -4
- package/package.json +1 -1
- package/sales-template/setup.sh +52 -4
- package/skills/conductor-craftsman/SKILL.md +4 -0
package/dist/headless.js
CHANGED
|
@@ -996,6 +996,41 @@ function closeLog(ws) {
|
|
|
996
996
|
* 失敗の理由は lastSpawnError に入る。
|
|
997
997
|
*/
|
|
998
998
|
export let lastSpawnError = '';
|
|
999
|
+
/** 職人の PATH で「古い node が先に来ない」ように見る道具の名前。 */
|
|
1000
|
+
const NODE_TOOLS = ['node', 'npm', 'npx'];
|
|
1001
|
+
/**
|
|
1002
|
+
* 職人(claude -p)の子プロセスへ渡す PATH を作る(便 24c9b6b6・e9276b86)。
|
|
1003
|
+
*
|
|
1004
|
+
* 【なぜ要るか(実物 2026-10-03・TYHLD-OTA 便 f770cd9d/ノートPC 便 4cf71dde)】
|
|
1005
|
+
* systemd の常駐はシェルの PATH を継承しないので、nvm の bin が職人の PATH に無い。
|
|
1006
|
+
* 以前は claude が nvm の bin に居たので、claude の bin を足すだけで npm・npx も一緒に見えていた。
|
|
1007
|
+
* Claude Code を公式の入れ方(~/.local/bin/claude)にそろえた後は、足されるのが ~/.local/bin だけになり、
|
|
1008
|
+
* 職人の bash で `npm: command not found`。node だけは /usr/local/bin/node(nvm への近道)で見えていた。
|
|
1009
|
+
* ★/usr/bin/node(v18)が別に在る機もあるので、PATH の順が崩れると古い node が動く。
|
|
1010
|
+
*
|
|
1011
|
+
* 【決め方】
|
|
1012
|
+
* - 中継を動かしている node(process.execPath=conductor-run が選んだ配布物の node)の bin を必ず足す。
|
|
1013
|
+
* =職人の node・npm・npx は、中継と同じ版になる(その bin に npm・npx が同居する入れ方=nvm 等)。
|
|
1014
|
+
* - claude の bin も足す(便 24c9b6b6)。ただし claude の bin に node・npm・npx のどれかが在るとき
|
|
1015
|
+
* (/usr/bin・nvm の別の版など)は、中継の node の bin をその前に置く=古い node が先に来ない。
|
|
1016
|
+
* claude の bin に node 類が無いとき(~/.local/bin などの公式の入れ方)は、claude の bin を先に置く
|
|
1017
|
+
* =職人の bash の `claude` は中継が選んだ claude のまま。
|
|
1018
|
+
* - 2つは既に PATH に在っても先頭へ寄せる(後ろに残っていると、先に在る古い node が勝つため)。重複させない。
|
|
1019
|
+
* - PATH が空でも動く。
|
|
1020
|
+
*/
|
|
1021
|
+
export function buildWorkerPath(currentPath, claudeBin, nodeExecPath = process.execPath, hasTool = (dir, name) => existsSync(path.join(dir, name))) {
|
|
1022
|
+
const nodeDir = path.dirname(nodeExecPath);
|
|
1023
|
+
const claudeDir = path.dirname(claudeBin);
|
|
1024
|
+
let front;
|
|
1025
|
+
if (nodeDir === claudeDir)
|
|
1026
|
+
front = [nodeDir];
|
|
1027
|
+
else if (NODE_TOOLS.some((t) => hasTool(claudeDir, t)))
|
|
1028
|
+
front = [nodeDir, claudeDir];
|
|
1029
|
+
else
|
|
1030
|
+
front = [claudeDir, nodeDir];
|
|
1031
|
+
const rest = (currentPath ?? '').split(':').filter((d) => d !== '' && !front.includes(d));
|
|
1032
|
+
return [...front, ...rest].join(':');
|
|
1033
|
+
}
|
|
999
1034
|
export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
|
|
1000
1035
|
// 前の失敗の理由を持ち越さない(理由の無い失敗に古い理由が付かないように)
|
|
1001
1036
|
lastSpawnError = '';
|
|
@@ -1025,15 +1060,9 @@ export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
|
|
|
1025
1060
|
env.CONDUCTOR_SITE = cfg.site;
|
|
1026
1061
|
env.CONDUCTOR_MACHINE = cfg.machine;
|
|
1027
1062
|
env.CONDUCTOR_WORK_DIR = cfg.workDir;
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
1030
|
-
|
|
1031
|
-
// 信頼したディレクトリだけを足す。重複させない・PATH が空でも動く。
|
|
1032
|
-
const claudeBinDir = path.dirname(claudeBin);
|
|
1033
|
-
const currentPath = env.PATH ?? '';
|
|
1034
|
-
if (!currentPath.split(':').includes(claudeBinDir)) {
|
|
1035
|
-
env.PATH = currentPath ? `${claudeBinDir}:${currentPath}` : claudeBinDir;
|
|
1036
|
-
}
|
|
1063
|
+
// ★職人の PATH の先頭に、中継を動かしている node の bin と claude の bin を足す
|
|
1064
|
+
// (便 24c9b6b6・e9276b86)。決め方は buildWorkerPath の説明を見ること。
|
|
1065
|
+
env.PATH = buildWorkerPath(env.PATH, claudeBin);
|
|
1037
1066
|
// ★テスト用: CONDUCTOR_CLAUDE_CONFIG_DIR を設定すると、claude の設定フォルダを差し替える。
|
|
1038
1067
|
// 本物のログインを壊さずに認証エラーを再現できる(空のフォルダを指す → not logged in)。
|
|
1039
1068
|
// 本番では設定しない(claude は既定の ~/.claude を使う)。
|
package/dist/realtime-parse.js
CHANGED
|
@@ -22,6 +22,14 @@ export const EAR_REFRESH_FALLBACK_MS = 8 * 60 * 60 * 1000;
|
|
|
22
22
|
/** 再接続の待ち(指数バックオフ)の初期値と上限。 */
|
|
23
23
|
export const EAR_RETRY_BASE_MS = 5 * 1000;
|
|
24
24
|
export const EAR_RETRY_MAX_MS = 5 * 60 * 1000;
|
|
25
|
+
/**
|
|
26
|
+
* 鍵が通らない(401・403)ときの聞き直しの待ち(指数バックオフ)の初期値と上限(便 e9276b86)。
|
|
27
|
+
* 【実物 2026-10-03 03:04〜03:24Z】鍵が通らない機から realtime-token が 2〜4秒おきに聞き直され、20分で 401 が189件。
|
|
28
|
+
* 鍵は待っても直らない(人が入れ直すまで同じ答え)ので、ふつうの切断(5秒から)より長く空ける。
|
|
29
|
+
* ★1分 → 2分 → 4分 …と広げ、30分で頭打ち。つながっているときの速さ(合図・取り直し)は変えない。
|
|
30
|
+
*/
|
|
31
|
+
export const EAR_AUTH_RETRY_BASE_MS = 60 * 1000;
|
|
32
|
+
export const EAR_AUTH_RETRY_MAX_MS = 30 * 60 * 1000;
|
|
25
33
|
/** 購読設定が無い(unsupported)ときに問い直す間隔(ms)。中央の設定投入に自然に追従するため。 */
|
|
26
34
|
export const EAR_UNSUPPORTED_RETRY_MS = 30 * 60 * 1000;
|
|
27
35
|
/**
|
|
@@ -108,6 +116,15 @@ export function computeRefreshDelayMs(expiresAt, nowMs) {
|
|
|
108
116
|
return clamp(EAR_REFRESH_FALLBACK_MS);
|
|
109
117
|
return clamp(at - nowMs - EAR_REFRESH_MARGIN_MS);
|
|
110
118
|
}
|
|
119
|
+
/** 中央の答えが「鍵が通らない」か(401・403)。 */
|
|
120
|
+
export function isAuthRejected(r) {
|
|
121
|
+
return r.kind === 'error' && (r.status === 401 || r.status === 403);
|
|
122
|
+
}
|
|
123
|
+
/** 鍵が通らないときの待ち(ms)。続けて断られた回数に応じて 1分 → 2分 → 4分 …と広げ、30分で頭打ち。 */
|
|
124
|
+
export function computeAuthRetryDelayMs(failures) {
|
|
125
|
+
const n = failures < 0 ? 0 : failures;
|
|
126
|
+
return Math.min(EAR_AUTH_RETRY_MAX_MS, EAR_AUTH_RETRY_BASE_MS * 2 ** Math.min(n, 20));
|
|
127
|
+
}
|
|
111
128
|
/** 再接続の待ち(ms)。試行回数に応じて 5s → 10s → 20s …と広げ、5分で頭打ちにする。 */
|
|
112
129
|
export function computeRetryDelayMs(attempt) {
|
|
113
130
|
const n = attempt < 0 ? 0 : attempt;
|
package/dist/realtime.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* 読めなければ「耳なし=各relayのポーリングのみ」で静かに動き続ける。
|
|
31
31
|
*/
|
|
32
32
|
import { describeTransport, resolveWebSocketTransport } from './websocket-transport.js';
|
|
33
|
-
import { EAR_NO_SITE_RETRY_MS, EAR_NUDGE_MIN_GAP_MS, EAR_TOKEN_FETCH_TIMEOUT_MS, EAR_UNSUPPORTED_RETRY_MS, computeRefreshDelayMs, computeRetryDelayMs, parseRealtimeConnection, readSignalSite, } from './realtime-parse.js';
|
|
33
|
+
import { EAR_NO_SITE_RETRY_MS, EAR_NUDGE_MIN_GAP_MS, EAR_TOKEN_FETCH_TIMEOUT_MS, EAR_UNSUPPORTED_RETRY_MS, computeAuthRetryDelayMs, computeRefreshDelayMs, computeRetryDelayMs, isAuthRejected, parseRealtimeConnection, readSignalSite, } from './realtime-parse.js';
|
|
34
34
|
// 読み取りと間合いの計算は src/realtime-parse.ts に分けてある(外部I/Oなし=単体テスト可能)。
|
|
35
35
|
// 従来どおり `./realtime.js` からも使えるよう、ここで通しておく。
|
|
36
36
|
export * from './realtime-parse.js';
|
|
@@ -60,7 +60,7 @@ export async function fetchRealtimeConnection(cfg, site) {
|
|
|
60
60
|
signal: AbortSignal.timeout(EAR_TOKEN_FETCH_TIMEOUT_MS),
|
|
61
61
|
});
|
|
62
62
|
if (!res.ok)
|
|
63
|
-
return { kind: 'error', detail: `HTTP ${res.status}
|
|
63
|
+
return { kind: 'error', detail: `HTTP ${res.status}`, status: res.status };
|
|
64
64
|
const data = await res.json().catch(() => null);
|
|
65
65
|
return parseRealtimeConnection(data);
|
|
66
66
|
}
|
|
@@ -78,12 +78,17 @@ async function loadCreateClient() {
|
|
|
78
78
|
return null;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
const DEFAULT_IO = {
|
|
82
|
+
fetchConnection: fetchRealtimeConnection,
|
|
83
|
+
loadCreateClient,
|
|
84
|
+
resolveTransport: resolveWebSocketTransport,
|
|
85
|
+
};
|
|
81
86
|
/**
|
|
82
87
|
* 指示の着信合図を受け取る耳を1本張る(★このPCに1本だけ=耳プロセスからのみ呼ぶ)。
|
|
83
88
|
*
|
|
84
89
|
* 呼び出し側は各relayのポーリングを止めないこと——耳が死んでいる間はポーリングが肩代わりする。
|
|
85
90
|
*/
|
|
86
|
-
export function startRealtimeEar(cfg, handlers) {
|
|
91
|
+
export function startRealtimeEar(cfg, handlers, io = DEFAULT_IO) {
|
|
87
92
|
const { siteForToken, onSignal } = handlers;
|
|
88
93
|
let stopped = false;
|
|
89
94
|
let client = null;
|
|
@@ -96,6 +101,18 @@ export function startRealtimeEar(cfg, handlers) {
|
|
|
96
101
|
let lastNudgeSite = null;
|
|
97
102
|
/** 再接続の予約が二重に積まれないようにする(切断イベントは連続で飛んでくる)。 */
|
|
98
103
|
let reconnectArmed = false;
|
|
104
|
+
/**
|
|
105
|
+
* 鍵が通らない(401・403)と続けて断られた回数(便 e9276b86)。★接続情報が取れたときだけ 0 に戻す
|
|
106
|
+
* (購読の SUBSCRIBED では戻さない=別の線の知らせで待ちが縮まない)。
|
|
107
|
+
*/
|
|
108
|
+
let authFailures = 0;
|
|
109
|
+
/**
|
|
110
|
+
* いま張っている線の世代(便 e9276b86)。片付けるたびに1つ進める。
|
|
111
|
+
* ★片付けた古い線から遅れて届く CLOSED などの知らせは、世代が違うので無視する。
|
|
112
|
+
* (以前は、期限の手前の張り直しで古い線を片付けると、その CLOSED で2本目の張り直しが走り、
|
|
113
|
+
* 聞き直しが重なっていた。)
|
|
114
|
+
*/
|
|
115
|
+
let generation = 0;
|
|
99
116
|
const clearTimers = () => {
|
|
100
117
|
if (retryTimer) {
|
|
101
118
|
clearTimeout(retryTimer);
|
|
@@ -122,16 +139,28 @@ export function startRealtimeEar(cfg, handlers) {
|
|
|
122
139
|
}
|
|
123
140
|
channel = null;
|
|
124
141
|
client = null;
|
|
142
|
+
generation += 1;
|
|
125
143
|
};
|
|
126
|
-
/**
|
|
127
|
-
|
|
144
|
+
/**
|
|
145
|
+
* 一拍おいて張り直す。理由はログに残す(凝らない=最悪はポーリングが拾う)。
|
|
146
|
+
* authRejected=鍵が通らない(401・403)ときは、ふつうの切断より長く空ける(便 e9276b86)。
|
|
147
|
+
*/
|
|
148
|
+
const scheduleReconnect = (reason, authRejected = false) => {
|
|
128
149
|
if (stopped || reconnectArmed)
|
|
129
150
|
return;
|
|
130
151
|
reconnectArmed = true;
|
|
131
152
|
teardown();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
153
|
+
let delay;
|
|
154
|
+
if (authRejected) {
|
|
155
|
+
delay = computeAuthRetryDelayMs(authFailures);
|
|
156
|
+
authFailures += 1;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
delay = computeRetryDelayMs(attempt);
|
|
160
|
+
attempt += 1;
|
|
161
|
+
}
|
|
162
|
+
log(`${reason} → ${Math.round(delay / 1000)}秒後に張り直します(この間は各relayの15秒ポーリングが肩代わり)` +
|
|
163
|
+
(authRejected ? '。★鍵が通りません(~/.conductor.env の合言葉を確かめてください)' : ''));
|
|
135
164
|
clearTimers();
|
|
136
165
|
retryTimer = setTimeout(() => {
|
|
137
166
|
reconnectArmed = false;
|
|
@@ -168,7 +197,7 @@ export function startRealtimeEar(cfg, handlers) {
|
|
|
168
197
|
const connect = async () => {
|
|
169
198
|
if (stopped)
|
|
170
199
|
return;
|
|
171
|
-
const createClient = await loadCreateClient();
|
|
200
|
+
const createClient = await io.loadCreateClient();
|
|
172
201
|
if (createClient === null) {
|
|
173
202
|
log('@supabase/supabase-js が読めないため耳は張りません(各relayの15秒ポーリングでこれまでどおり動きます)');
|
|
174
203
|
return; // 依存は実行中に生えないので再試行しない。
|
|
@@ -176,7 +205,7 @@ export function startRealtimeEar(cfg, handlers) {
|
|
|
176
205
|
// ★どの Node(18/20/22) でも耳が張れるようにする(実機 Node 20 の「native WebSocket not found」根治)。
|
|
177
206
|
// 標準の WebSocket が無い機では同梱の ws を supabase-js の transport へ渡す。
|
|
178
207
|
// どちらも用意できなければ耳は張らない——が、プロセスは落とさない(ポーリングが肩代わりする)。
|
|
179
|
-
const transport = await
|
|
208
|
+
const transport = await io.resolveTransport();
|
|
180
209
|
if (transport === null) {
|
|
181
210
|
log('この機には標準のWebSocketが無く、同梱のWebSocket部品(ws)も読めないため耳は張りません' +
|
|
182
211
|
'(npm install で解消します。それまでも各relayの15秒ポーリングでこれまでどおり動きます)');
|
|
@@ -189,7 +218,9 @@ export function startRealtimeEar(cfg, handlers) {
|
|
|
189
218
|
retryTimer = setTimeout(() => void connect(), EAR_NO_SITE_RETRY_MS);
|
|
190
219
|
return;
|
|
191
220
|
}
|
|
192
|
-
const result = await
|
|
221
|
+
const result = await io.fetchConnection(cfg, site);
|
|
222
|
+
if (stopped)
|
|
223
|
+
return;
|
|
193
224
|
if (result.kind === 'unsupported') {
|
|
194
225
|
log(`中央から購読設定が返らないため耳は張りません(${result.detail})。` +
|
|
195
226
|
`各relayの15秒ポーリングで動き続け、${Math.round(EAR_UNSUPPORTED_RETRY_MS / 60000)}分後にもう一度問い合わせます`);
|
|
@@ -198,10 +229,12 @@ export function startRealtimeEar(cfg, handlers) {
|
|
|
198
229
|
return;
|
|
199
230
|
}
|
|
200
231
|
if (result.kind === 'error') {
|
|
201
|
-
scheduleReconnect(`接続情報を取得できません(${result.detail}
|
|
232
|
+
scheduleReconnect(`接続情報を取得できません(${result.detail})`, isAuthRejected(result));
|
|
202
233
|
return;
|
|
203
234
|
}
|
|
235
|
+
authFailures = 0;
|
|
204
236
|
const conn = result.conn;
|
|
237
|
+
const myGeneration = generation;
|
|
205
238
|
try {
|
|
206
239
|
client = createClient(conn.supabaseUrl, conn.supabaseAnonKey, {
|
|
207
240
|
auth: { persistSession: false, autoRefreshToken: false },
|
|
@@ -221,7 +254,8 @@ export function startRealtimeEar(cfg, handlers) {
|
|
|
221
254
|
.channel(spec.channel)
|
|
222
255
|
.on('postgres_changes', filterOpt, handleSignal)
|
|
223
256
|
.subscribe((status, err) => {
|
|
224
|
-
|
|
257
|
+
// ★片付けた古い線の知らせは無視する(張り直しを重ねない・便 e9276b86)。
|
|
258
|
+
if (stopped || myGeneration !== generation)
|
|
225
259
|
return;
|
|
226
260
|
if (status === 'SUBSCRIBED') {
|
|
227
261
|
attempt = 0;
|
package/dist/selfcheck.js
CHANGED
|
@@ -27,7 +27,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync
|
|
|
27
27
|
import os from 'node:os';
|
|
28
28
|
import path from 'node:path';
|
|
29
29
|
import { fileURLToPath } from 'node:url';
|
|
30
|
-
import { CLAUDE_MIN_VERSION, claudeUpdateHint, compareVersions, lastClaudePinStatus, resolveClaudeBin, } from './headless.js';
|
|
30
|
+
import { buildWorkerPath, CLAUDE_MIN_VERSION, claudeUpdateHint, compareVersions, lastClaudePinStatus, resolveClaudeBin, } from './headless.js';
|
|
31
31
|
import { fetchControl } from './relay.js';
|
|
32
32
|
import { readDrain } from './autoupdate-flags.js';
|
|
33
33
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -538,9 +538,7 @@ function probeEnv(d, claudeBin) {
|
|
|
538
538
|
const env = { ...d.env };
|
|
539
539
|
delete env.CLAUDECODE;
|
|
540
540
|
delete env.ANTHROPIC_API_KEY;
|
|
541
|
-
|
|
542
|
-
if (!(env.PATH ?? '').split(':').includes(binDir))
|
|
543
|
-
env.PATH = env.PATH ? `${binDir}:${env.PATH}` : binDir;
|
|
541
|
+
env.PATH = buildWorkerPath(env.PATH, claudeBin);
|
|
544
542
|
return env;
|
|
545
543
|
}
|
|
546
544
|
/** ⑥ 小さな headless 試験を1回流す。 */
|
|
@@ -841,8 +839,31 @@ export async function runSelfcheck(d, trigger) {
|
|
|
841
839
|
finally {
|
|
842
840
|
// ★一時の作業場所は必ず消す(外に書けてしまった物も含めて)。
|
|
843
841
|
rmSync(runDir, { recursive: true, force: true });
|
|
842
|
+
// ★道具(Claude Code)が作業場所ごとに /tmp/claude-<uid>/ の下へ作る記録も消す(便 e9276b86)。
|
|
843
|
+
// 作業場所は毎回名前が違うので、消さないと自己診断のたびに1つずつ溜まる(ノートPCで9つ)。
|
|
844
|
+
for (const dir of [runDir, path.join(runDir, 'work')]) {
|
|
845
|
+
const trace = claudeTraceDir(dir, d.claudeTmpRoot);
|
|
846
|
+
if (trace)
|
|
847
|
+
rmSync(trace, { recursive: true, force: true });
|
|
848
|
+
}
|
|
844
849
|
}
|
|
845
850
|
}
|
|
851
|
+
/**
|
|
852
|
+
* Claude Code が作業場所ごとに作る記録の置き場(★実物 2026-10-03・Claude Code 2.1.287)。
|
|
853
|
+
* `/tmp/claude-<uid>/` の下に、作業場所のパスの英数字以外を `-` に替えた名前で作る
|
|
854
|
+
* (例 /home/u/.local/state/conductor/selfcheck/run-…/work → -home-u--local-state-conductor-selfcheck-run-…-work)。
|
|
855
|
+
* ★消してよいのは自己診断の作業場所の分だけ。名前に selfcheck の run- が入らなければ null(他人の物に触らない)。
|
|
856
|
+
*/
|
|
857
|
+
export function claudeTraceDir(workDir, root = defaultClaudeTmpRoot()) {
|
|
858
|
+
const name = workDir.replace(/[^a-zA-Z0-9]/g, '-');
|
|
859
|
+
if (!/-selfcheck-run-/.test(name))
|
|
860
|
+
return null;
|
|
861
|
+
return path.join(root, name);
|
|
862
|
+
}
|
|
863
|
+
function defaultClaudeTmpRoot() {
|
|
864
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
|
|
865
|
+
return path.join('/tmp', `claude-${uid}`);
|
|
866
|
+
}
|
|
846
867
|
/** 画面に出す形(人が読む)。 */
|
|
847
868
|
export function formatSelfcheck(r) {
|
|
848
869
|
const lines = [
|
package/package.json
CHANGED
package/sales-template/setup.sh
CHANGED
|
@@ -1290,6 +1290,49 @@ else
|
|
|
1290
1290
|
cleanup_stale_units || warn "古いユニットの掃除に失敗しました(インストールは続行します)。"
|
|
1291
1291
|
fi
|
|
1292
1292
|
|
|
1293
|
+
# --- 画面なし方式(headless)の機では、足す現場も画面なし方式で立てる(便 e9276b86) ---
|
|
1294
|
+
# 【なぜ要るか(実物 2026-10-03・TYHLD-GOTO・TYHLD-OTA)】
|
|
1295
|
+
# 1行インストーラ(install.sh)は最初の現場に drop-in(headless.conf)を置いて画面なし方式にする。
|
|
1296
|
+
# ところが2つ目の現場を `conductor setup 現場名` で足すと drop-in が無く、古い方式(tmux)で立った
|
|
1297
|
+
# (管制で「AI担当は休止中」。人が drop-in を手で置いて直した)。⑧の案内も tmux の3行を出していた。
|
|
1298
|
+
# 【決め方】次のどれかなら、この機は画面なし方式=この現場にも install.sh と同じ drop-in を置く。
|
|
1299
|
+
# ① CONDUCTOR_RUNNER_MODE=headless で呼ばれた(install.sh から)。★=tmux と明示されたら置かない。
|
|
1300
|
+
# ② この機のほかの現場(またはテンプレート全体)の drop-in に画面なし方式の指定が在る。
|
|
1301
|
+
# ★どれにも当たらない機(もとから tmux 方式の機)は、1つも変えない。Mac(launchd)も変えない。
|
|
1302
|
+
HEADLESS_LINE='Environment=CONDUCTOR_RUNNER_MODE=headless'
|
|
1303
|
+
HEADLESS_DROPIN_DIR="$HOME/.config/systemd/user/conductor@${SITE}.service.d"
|
|
1304
|
+
HEADLESS_DROPIN="$HEADLESS_DROPIN_DIR/headless.conf"
|
|
1305
|
+
machine_is_headless() {
|
|
1306
|
+
case "${CONDUCTOR_RUNNER_MODE:-}" in
|
|
1307
|
+
headless) return 0 ;;
|
|
1308
|
+
tmux) return 1 ;;
|
|
1309
|
+
esac
|
|
1310
|
+
local f
|
|
1311
|
+
for f in "$HOME"/.config/systemd/user/conductor@*.service.d/*.conf; do
|
|
1312
|
+
[[ -f "$f" ]] || continue
|
|
1313
|
+
grep -qxF "$HEADLESS_LINE" "$f" && return 0
|
|
1314
|
+
done
|
|
1315
|
+
return 1
|
|
1316
|
+
}
|
|
1317
|
+
HEADLESS_ADDED=0
|
|
1318
|
+
if [[ "$OS_KIND" != "mac" ]] && machine_is_headless; then
|
|
1319
|
+
if [[ -f "$HEADLESS_DROPIN" ]] && grep -qxF "$HEADLESS_LINE" "$HEADLESS_DROPIN"; then
|
|
1320
|
+
echo "[setup] 画面なし方式: この現場には設定済みです(${HEADLESS_DROPIN})"
|
|
1321
|
+
else
|
|
1322
|
+
mkdir -p "$HEADLESS_DROPIN_DIR"
|
|
1323
|
+
cat > "$HEADLESS_DROPIN" <<DROPIN
|
|
1324
|
+
# setup.sh が設定: この機は画面なし方式(headless)なので、この現場も同じ方式で動かす。
|
|
1325
|
+
# ★tmux に切り替えたいときはこのファイルを消して systemctl --user daemon-reload。
|
|
1326
|
+
[Service]
|
|
1327
|
+
${HEADLESS_LINE}
|
|
1328
|
+
DROPIN
|
|
1329
|
+
HEADLESS_ADDED=1
|
|
1330
|
+
echo "[setup] 画面なし方式を設定しました(drop-in: ${HEADLESS_DROPIN})"
|
|
1331
|
+
fi
|
|
1332
|
+
# ⑧ の案内を画面なし方式に切り替える。
|
|
1333
|
+
CONDUCTOR_RUNNER_MODE=headless
|
|
1334
|
+
fi
|
|
1335
|
+
|
|
1293
1336
|
# ★常駐の設置だけが OS で分かれる(ADR-019・案C)。ここより上も下も1本のまま。
|
|
1294
1337
|
if [[ "$OS_KIND" == "mac" ]]; then
|
|
1295
1338
|
SVC_INSTALL="$SCRIPT_DIR/launchd/install.sh"
|
|
@@ -1298,6 +1341,10 @@ else
|
|
|
1298
1341
|
fi
|
|
1299
1342
|
if [[ -f "$SVC_INSTALL" ]]; then
|
|
1300
1343
|
bash "$SVC_INSTALL" "$SITE" "$PROJECT_DIR"
|
|
1344
|
+
# ★もう動いていた現場(setup のやり直し)は enable --now では読み直さないので、入れ直して効かせる。
|
|
1345
|
+
if [[ "$HEADLESS_ADDED" == "1" ]]; then
|
|
1346
|
+
systemctl --user restart "conductor@${SITE}.service" 2>/dev/null || true
|
|
1347
|
+
fi
|
|
1301
1348
|
else
|
|
1302
1349
|
warn "常駐の設置スクリプトが見つかりません: $SVC_INSTALL(スキップ)"
|
|
1303
1350
|
echo "[setup] 手動で設定する場合は sales-template/${SVC_KIND}/ を参照してください。"
|
|
@@ -1452,12 +1499,13 @@ if [[ "${CONDUCTOR_RUNNER_MODE:-}" == "headless" ]]; then
|
|
|
1452
1499
|
合言葉を入れ直す: rm ~/.conductor.env → そのあと setup.sh をもう一度実行
|
|
1453
1500
|
|
|
1454
1501
|
──────────────────────────────────────────────────────────
|
|
1455
|
-
|
|
1502
|
+
職人の画面を手で開きたいとき(見るだけ・補助)
|
|
1456
1503
|
──────────────────────────────────────────────────────────
|
|
1457
1504
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1505
|
+
画面なし方式では、職人の画面を手で立ち上げません(立ち上げると、便と同じ現場で2人目の職人が動きます)。
|
|
1506
|
+
職人がいま何をしているかは、管制の画面か、次の1行で見られます(見るだけ・入力はできません):
|
|
1507
|
+
|
|
1508
|
+
kc ${SITE} (kc が無いときは conductor view ${SITE})
|
|
1461
1509
|
|
|
1462
1510
|
GUIDE
|
|
1463
1511
|
else
|
|
@@ -132,6 +132,10 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
|
|
|
132
132
|
- **なぜ(置き場の話)。** 実機 2026-09-03: ai-saku の画面便(PR#258・PR#269)で、検品用の使い捨てページ `_layout-check.html` をリポジトリ直下に作り、`rm -rf` で消そうとして止められ、人が2回消した。**禁止の一覧と番人の制限は正しい(緩めない)。悪いのは置き場。**
|
|
133
133
|
- **なぜ(後片付けの話を改めた理由)。** 旧い文言は「リポジトリ直下は番人が `rm` を止めるので自分で消さず報告」だった。実測(便 fb248f6e)では、`-f` を付けない `rm -r` と実在のパスの名指しなら、リポジトリ直下の自分の作業ファイルも通る。止まるのは `-f` 付きと変数・`*` 入りだけ。**止まらない形を「止まる」と書いていると、職人が自分の残骸を残したまま便を終える**ので、実物に合わせた(番人・禁止の一覧は1行も変えていない)。
|
|
134
134
|
- 置き場を `/tmp/claude-1000/` にすれば、止められることも、残骸がコミットに混ざることも起きない(根治=鉄則10)。Playwright の検品ページも同じ(鉄則8-④)。
|
|
135
|
+
- **空振り確認(直しをわざと外して、試験が落ちるのを見る)も、使い捨ての複製の中だけで行う(えふさん確定 2026-10-03・便 4cf71dde の要判断②)。**
|
|
136
|
+
- 形: `git clone --no-hardlinks <リポの実在のパス> /tmp/claude-1000/karaburi-<便IDの頭8文字>` → その中で直しを外す → 試験を流す → 終わったら `rm -r /tmp/claude-1000/karaburi-<便IDの頭8文字>`(`-f` を付けない・名指し)。
|
|
137
|
+
- ★**作業ツリーの中で直しを外さない**(戻し忘れがそのまま PR に入る)。★**`git worktree` を使わない**:囲いが `.git/worktrees` を読み取り専用で重ねるので、`git worktree remove` が「Device or resource busy」で止まり、職人が片付けられない(便 4cf71dde で再現)。複製なら囲いにも番人にも止められずに片付く。
|
|
138
|
+
- 複製の中でも記録に入れるときは名指しで `git add <ファイル>`(鉄則1-④)。複製は GitHub へ push しない。
|
|
135
139
|
14. **`.env` 類(`.env`・`.env.local`・`.env.example` などの雛形も含む)は職人が書き換えない(えふさん確定 2026-09-18・便 3d11f913)**
|
|
136
140
|
- **番人は `.env` 本体も `.env.example` などの雛形も、書き込みを止める**(Write/Edit の設定 deny と、Bash の `echo`/`tee`/`sed -i`/`cp`/`mv` などを、経路を問わず同じ結果にそろえた)。読み込みは今までどおり(雛形は読める・本物は読めない)。
|
|
137
141
|
- **なぜ。** 雛形(`.env.example`)は普通は秘密値を置かないが、「exampleだから常に安全」と番人へ例外を作ると、将来誤って実値が書かれたときの穴になる(利便性より安全側・えふさん確定)。
|