@tyhld/conductor 0.12.54 → 0.12.56
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/cli.js +3 -1
- package/dist/headless.js +122 -7
- package/dist/relay.js +55 -8
- package/package.json +1 -1
- package/sales-template/install.sh +118 -12
- package/sales-template/setup.sh +34 -6
- package/skills/conductor-craftsman/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -165,6 +165,8 @@ function loadConfig(args) {
|
|
|
165
165
|
runnerMode,
|
|
166
166
|
};
|
|
167
167
|
}
|
|
168
|
+
/** 報告ファイルの拒否回数を追跡する(常駐の生存期間にわたって保持する)。 */
|
|
169
|
+
const reportRejectCounts = new Map();
|
|
168
170
|
/** 1回分の報告を送る。送信結果(HTTPステータス)をログ表示する。 */
|
|
169
171
|
async function sendReport(cfg) {
|
|
170
172
|
const body = {
|
|
@@ -223,7 +225,7 @@ async function sendReport(cfg) {
|
|
|
223
225
|
// ★配達役がいま追っている便の報告は送らない(完了の道が完了と一緒に渡すため)。
|
|
224
226
|
if (pending.ids.length > 0) {
|
|
225
227
|
const tracking = relayStateForReport?.pending?.id;
|
|
226
|
-
await sendPendingReportFiles(cfg, { exclude: tracking ? [tracking] : [] });
|
|
228
|
+
await sendPendingReportFiles(cfg, { exclude: tracking ? [tracking] : [], rejectCounts: reportRejectCounts });
|
|
227
229
|
}
|
|
228
230
|
}
|
|
229
231
|
/** `conductor start`: 起動直後に1回 + 以後 intervalSec ごとに報告し続ける。 */
|
package/dist/headless.js
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { spawn } from 'node:child_process';
|
|
18
18
|
import { createInterface } from 'node:readline';
|
|
19
|
+
import { existsSync } from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import os from 'node:os';
|
|
19
22
|
import { RELAY_DELIVER_MAX, RELAY_FETCH_TIMEOUT_MS, RELAY_HEARTBEAT_INTERVAL_MS, RELAY_PENDING_TIMEOUT_MS, RELAY_RESTORE_ORPHANS_ENABLED, buildAnchor, buildSendText, canDeliver, completeWithReport, createRelayState, deliveryReport, fetchCommand, fetchCommandStatus, fetchControl, fetchOrphans, isTerminalCommandStatus, kindOf, markDelivered, noteCentralDown, noteCentralUp, noteReadiness, patchStatus, reconnectDelayMs, selectOrphanToRestore, sendHeartbeat, } from './relay.js';
|
|
20
23
|
const log = (msg) => {
|
|
21
24
|
console.log(`[headless] ${msg}`);
|
|
@@ -36,6 +39,67 @@ export const EVENT_TEXT_MAX = 500;
|
|
|
36
39
|
/** resume 時に職人へ渡す続行指示。 */
|
|
37
40
|
const RESUME_PROMPT = '前回の作業の続きを行ってください。中断された箇所から再開してください。';
|
|
38
41
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
// claude の実行ファイルの探索
|
|
43
|
+
//
|
|
44
|
+
// systemd のユーザー常駐はシェルの PATH を継承しない。nvm の下に入った claude は
|
|
45
|
+
// PATH に居ないため spawn('claude') が ENOENT で失敗する(実測 2026-10-01)。
|
|
46
|
+
// conductor-run が node を探すのと同じ考え方で、既知の場所を順に探す。
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
/** claude の実行ファイルの既知の候補を返す(新しい node の版から順)。 */
|
|
49
|
+
function claudeBinCandidates() {
|
|
50
|
+
const h = os.homedir();
|
|
51
|
+
const candidates = [];
|
|
52
|
+
// nvm の各 node 版の bin(新しい版から)
|
|
53
|
+
const nvmDir = path.join(h, '.nvm', 'versions', 'node');
|
|
54
|
+
try {
|
|
55
|
+
const { readdirSync } = require('node:fs');
|
|
56
|
+
const versions = readdirSync(nvmDir)
|
|
57
|
+
.filter((v) => v.startsWith('v'))
|
|
58
|
+
.sort((a, b) => {
|
|
59
|
+
// v24.21.0 → [24, 21, 0] で降順
|
|
60
|
+
const pa = a.slice(1).split('.').map(Number);
|
|
61
|
+
const pb = b.slice(1).split('.').map(Number);
|
|
62
|
+
for (let i = 0; i < 3; i++) {
|
|
63
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
64
|
+
return (pb[i] ?? 0) - (pa[i] ?? 0);
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
});
|
|
68
|
+
for (const v of versions) {
|
|
69
|
+
candidates.push(path.join(nvmDir, v, 'bin', 'claude'));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// nvm が無い場合
|
|
74
|
+
}
|
|
75
|
+
// その他の既知の場所
|
|
76
|
+
candidates.push(path.join(h, '.local', 'bin', 'claude'), path.join(h, '.claude', 'local', 'claude'), '/usr/local/bin/claude', '/usr/bin/claude');
|
|
77
|
+
return candidates;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* claude の実行ファイルのフルパスを返す。見つからなければ null。
|
|
81
|
+
*
|
|
82
|
+
* 探す順:
|
|
83
|
+
* 1. 環境変数 CONDUCTOR_CLAUDE_BIN(テスト用の差し替え口)
|
|
84
|
+
* 2. nvm の各 node 版の bin/claude(新しい版から)
|
|
85
|
+
* 3. ~/.local/bin/claude
|
|
86
|
+
* 4. ~/.claude/local/claude
|
|
87
|
+
* 5. /usr/local/bin/claude, /usr/bin/claude
|
|
88
|
+
*/
|
|
89
|
+
export function resolveClaudeBin() {
|
|
90
|
+
const override = process.env.CONDUCTOR_CLAUDE_BIN;
|
|
91
|
+
if (override) {
|
|
92
|
+
if (existsSync(override))
|
|
93
|
+
return override;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
for (const candidate of claudeBinCandidates()) {
|
|
97
|
+
if (existsSync(candidate))
|
|
98
|
+
return candidate;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
103
|
// 秘密のマスク
|
|
40
104
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
105
|
/** 秘密の値を検出するパターン。マッチした部分を *** に置換してからイベントを送る。 */
|
|
@@ -66,9 +130,11 @@ export function maskSecrets(text) {
|
|
|
66
130
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
67
131
|
// ヘルパー
|
|
68
132
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
-
/** commandId からセッション ID を作る(resume で再利用する決定的な値)。
|
|
133
|
+
/** commandId からセッション ID を作る(resume で再利用する決定的な値)。
|
|
134
|
+
* ★commandId はそれ自体が UUID なので、そのまま使う(プレフィックスを付けない)。
|
|
135
|
+
*/
|
|
70
136
|
export function sessionIdForCommand(commandId) {
|
|
71
|
-
return
|
|
137
|
+
return commandId;
|
|
72
138
|
}
|
|
73
139
|
/** stream-json の1行を CommandEvent に変換する。無関係な行は null。 */
|
|
74
140
|
export function streamEventToCommandEvent(parsed, attempt) {
|
|
@@ -121,8 +187,16 @@ export function isAuthError(job) {
|
|
|
121
187
|
*
|
|
122
188
|
* 【stdout】stream-json 形式。行ごとに JSON を出す。readline で1行ずつ読む。
|
|
123
189
|
* 【stderr】蓄積して認証エラーの検出に使う。
|
|
190
|
+
*
|
|
191
|
+
* 【起動失敗】claude が見つからない(ENOENT)・実行権限がない(EACCES)などで
|
|
192
|
+
* spawn が失敗した場合、null を返す。呼び出し側はエラーイベントを管制へ送る。
|
|
124
193
|
*/
|
|
125
194
|
export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
|
|
195
|
+
const claudeBin = resolveClaudeBin();
|
|
196
|
+
if (claudeBin === null) {
|
|
197
|
+
log(`ERROR claude が見つかりません。探した場所: nvm の各版の bin, ~/.local/bin, ~/.claude/local, /usr/local/bin, /usr/bin`);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
126
200
|
const env = { ...process.env };
|
|
127
201
|
delete env.CLAUDECODE;
|
|
128
202
|
delete env.ANTHROPIC_API_KEY;
|
|
@@ -136,11 +210,18 @@ export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
|
|
|
136
210
|
];
|
|
137
211
|
if (isResume)
|
|
138
212
|
args.push('--resume');
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
213
|
+
let proc;
|
|
214
|
+
try {
|
|
215
|
+
proc = spawn(claudeBin, args, {
|
|
216
|
+
cwd: cfg.workDir,
|
|
217
|
+
env,
|
|
218
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
log(`ERROR claude の起動に失敗しました (${claudeBin}): ${err instanceof Error ? err.message : String(err)}`);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
144
225
|
const job = {
|
|
145
226
|
commandId: cmd.id,
|
|
146
227
|
process: proc,
|
|
@@ -156,8 +237,19 @@ export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
|
|
|
156
237
|
lastStopCheckAt: Date.now(),
|
|
157
238
|
stderrBuf: '',
|
|
158
239
|
};
|
|
240
|
+
// spawn の 'error' イベント(ENOENT 等は非同期で届く。同期例外と両方捕まえる)
|
|
241
|
+
proc.on('error', (err) => {
|
|
242
|
+
if (!job.completed) {
|
|
243
|
+
job.completed = true;
|
|
244
|
+
job.exitCode = -1;
|
|
245
|
+
job.exitSignal = null;
|
|
246
|
+
job.stderrBuf += `\nspawn error: ${err.message}`;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
159
249
|
// stdin: 指示本文を書いて閉じる
|
|
160
250
|
if (proc.stdin) {
|
|
251
|
+
// stdin の write エラーは spawn 失敗時に起きうる(握り潰す)
|
|
252
|
+
proc.stdin.on('error', () => { });
|
|
161
253
|
if (isResume) {
|
|
162
254
|
proc.stdin.write(RESUME_PROMPT);
|
|
163
255
|
}
|
|
@@ -411,6 +503,17 @@ export async function restoreHeadlessOrphans(cfg, state) {
|
|
|
411
503
|
// ★孤児は resume で再開する(元の指示本文ではなく続行プロンプトを渡す)。
|
|
412
504
|
const cmd = { id: pick.id, body: pick.body };
|
|
413
505
|
const job = spawnClaudeProcess(cfg, cmd, sessionId, attempt, true);
|
|
506
|
+
if (job === null) {
|
|
507
|
+
await sendCommandEvents(cfg, pick.id, [{
|
|
508
|
+
type: 'error',
|
|
509
|
+
text: 'claude が見つかりません(孤児復元を中止)',
|
|
510
|
+
at: new Date().toISOString(),
|
|
511
|
+
attempt,
|
|
512
|
+
}]);
|
|
513
|
+
state.relay.abandoned.add(pick.id);
|
|
514
|
+
log(`ERROR site=${cfg.site} id=${pick.id} 孤児復元: claude が見つからない → 放棄`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
414
517
|
state.job = job;
|
|
415
518
|
markDelivered(state.relay, pick.id);
|
|
416
519
|
state.relay.lastHeartbeatAt = null;
|
|
@@ -507,6 +610,18 @@ export async function headlessTick(cfg, state, hooks) {
|
|
|
507
610
|
const sessionId = sessionIdForCommand(cmd.id);
|
|
508
611
|
const isResume = attempt > 1;
|
|
509
612
|
const job = spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume);
|
|
613
|
+
if (job === null) {
|
|
614
|
+
// ★起動失敗を管制へ知らせる(「起動しました」とは言わない)
|
|
615
|
+
await sendCommandEvents(cfg, cmd.id, [{
|
|
616
|
+
type: 'error',
|
|
617
|
+
text: 'claude が見つかりません(起動できませんでした)',
|
|
618
|
+
at: new Date().toISOString(),
|
|
619
|
+
attempt,
|
|
620
|
+
}]);
|
|
621
|
+
rs.abandoned.add(cmd.id);
|
|
622
|
+
log(`ERROR site=${cfg.site} id=${cmd.id} claude が見つからない → 放棄`);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
510
625
|
state.job = job;
|
|
511
626
|
markDelivered(rs, cmd.id);
|
|
512
627
|
rs.lastHeartbeatAt = null;
|
package/dist/relay.js
CHANGED
|
@@ -1094,8 +1094,8 @@ export function fullCompletionGuide(site, commandId) {
|
|
|
1094
1094
|
`---\n` +
|
|
1095
1095
|
`この囲みの下に、いつもどおりの報告本文を書いてください(本文が空だと未完成と見なされます)。\n` +
|
|
1096
1096
|
`その後の最終行に必ず ${marker} とだけ出力してください。\n\n` +
|
|
1097
|
-
// ★案D(relay非依存の確実な完了通知): 完了したら必ず
|
|
1098
|
-
`そして完了したら必ず、
|
|
1097
|
+
// ★案D(relay非依存の確実な完了通知): 完了したら必ず ai-kana MCP の conductor_complete を呼ぶ。
|
|
1098
|
+
`そして完了したら必ず、ai-kana の MCP ツール conductor_complete を commandId="${commandId}" で1回だけ呼んでください` +
|
|
1099
1099
|
`(relayに依存しない確実な完了通知です。report 引数に上記の報告本文を添えても構いません)。\n\n` +
|
|
1100
1100
|
// ★成果引数の案内(devlog-tracker PR #100): conductor_complete に任意の成果を添えられる。
|
|
1101
1101
|
// すべて任意・分からなければ省略可=渡さなくても従来どおり完了する。既存の完了検出
|
|
@@ -1225,6 +1225,10 @@ export const RELAY_DELIVERY_NOTE = '(配達役より)上の貼り付けは
|
|
|
1225
1225
|
// 実体はこのファイルの下(★relay.ts は試験から直に読まれるので、部品を別ファイルに分けない)。
|
|
1226
1226
|
/** 処理済みの控えを置く下位フォルダの名前。 */
|
|
1227
1227
|
export const PROCESSED_DIR = 'processed';
|
|
1228
|
+
/** 中央が恒久的に拒否した報告を退ける下位フォルダの名前。 */
|
|
1229
|
+
export const REJECTED_DIR = 'rejected';
|
|
1230
|
+
/** 同じ報告を拒否されたまま送り直す上限回数。これを超えたら rejected/ へ移す。 */
|
|
1231
|
+
export const REPORT_REJECT_MAX = 3;
|
|
1228
1232
|
/** 処理済みの控えを残す期間(既定30日)。★未処理はこの期間では消さない。 */
|
|
1229
1233
|
export const REPORT_KEEP_PROCESSED_MS = 30 * 24 * 60 * 60 * 1000;
|
|
1230
1234
|
/**
|
|
@@ -1348,6 +1352,27 @@ export function markReportProcessed(commandId, env, now = Date.now()) {
|
|
|
1348
1352
|
}
|
|
1349
1353
|
return moved;
|
|
1350
1354
|
}
|
|
1355
|
+
/**
|
|
1356
|
+
* 中央が恒久的に拒否した報告を【rejected/】へ退ける(★消さない・毎周の送り直しを止めるだけ)。
|
|
1357
|
+
* 返り: 退けられたら true。
|
|
1358
|
+
*/
|
|
1359
|
+
export function markReportRejected(commandId, env) {
|
|
1360
|
+
let moved = false;
|
|
1361
|
+
for (const file of pendingCandidates(commandId, env)) {
|
|
1362
|
+
if (!existsSync(file))
|
|
1363
|
+
continue;
|
|
1364
|
+
const dest = path.join(path.dirname(file), REJECTED_DIR, path.basename(file));
|
|
1365
|
+
try {
|
|
1366
|
+
mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
1367
|
+
renameSync(file, dest);
|
|
1368
|
+
moved = true;
|
|
1369
|
+
}
|
|
1370
|
+
catch {
|
|
1371
|
+
// 移せなかったら触らない
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return moved;
|
|
1375
|
+
}
|
|
1351
1376
|
/**
|
|
1352
1377
|
* まだ管制へ渡していない報告の指示ID(古い順)。★生存報告に載せて管制へ知らせるための一覧。
|
|
1353
1378
|
* - `processed/` の中は数えない(もう渡した物)。
|
|
@@ -1428,13 +1453,14 @@ export function isInstructionMissingReport(raw) {
|
|
|
1428
1453
|
}
|
|
1429
1454
|
/**
|
|
1430
1455
|
* 未処理の報告ファイルの全文を中央へ送り、受け取られた物だけ処理済みにする。
|
|
1431
|
-
* 返り:
|
|
1456
|
+
* 返り: 送った件数・処理済みにした件数・未処理のまま残した件数・拒否で退けた件数。
|
|
1432
1457
|
*/
|
|
1433
1458
|
export async function sendPendingReportFiles(cfg, opts = {}) {
|
|
1434
1459
|
const env = opts.env;
|
|
1435
1460
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
1436
1461
|
const logLine = opts.logLine ?? log;
|
|
1437
1462
|
const exclude = new Set(opts.exclude ?? []);
|
|
1463
|
+
const rejectCounts = opts.rejectCounts ?? new Map();
|
|
1438
1464
|
const items = [];
|
|
1439
1465
|
for (const id of pendingReportIds(env, REPORT_FILES_MAX_PER_ROUND).ids) {
|
|
1440
1466
|
for (const file of pendingCandidates(id, env)) {
|
|
@@ -1452,6 +1478,7 @@ export async function sendPendingReportFiles(cfg, opts = {}) {
|
|
|
1452
1478
|
let sent = 0;
|
|
1453
1479
|
let processed = 0;
|
|
1454
1480
|
let kept = 0;
|
|
1481
|
+
let rejected = 0;
|
|
1455
1482
|
for (let i = 0; i < items.length; i += REPORT_FILES_BATCH) {
|
|
1456
1483
|
const chunk = items.slice(i, i + REPORT_FILES_BATCH);
|
|
1457
1484
|
let results = null;
|
|
@@ -1484,20 +1511,40 @@ export async function sendPendingReportFiles(cfg, opts = {}) {
|
|
|
1484
1511
|
const r = results?.[k];
|
|
1485
1512
|
if (r && r.ok === true && markReportProcessed(chunk[k].id, env)) {
|
|
1486
1513
|
processed++;
|
|
1514
|
+
rejectCounts.delete(chunk[k].id);
|
|
1487
1515
|
}
|
|
1488
1516
|
else {
|
|
1489
|
-
|
|
1490
|
-
|
|
1517
|
+
if (r && r.ok === false) {
|
|
1518
|
+
// ★中央が拒否した(不完全な便ID 等)。連続拒否回数を数え、上限を超えたら rejected/ へ退ける。
|
|
1519
|
+
const count = (rejectCounts.get(chunk[k].id) ?? 0) + 1;
|
|
1520
|
+
rejectCounts.set(chunk[k].id, count);
|
|
1491
1521
|
const why = String(r.reason ?? r.error ?? '理由なし').slice(0, 200);
|
|
1492
|
-
|
|
1522
|
+
if (count >= REPORT_REJECT_MAX) {
|
|
1523
|
+
if (markReportRejected(chunk[k].id, env)) {
|
|
1524
|
+
rejected++;
|
|
1525
|
+
rejectCounts.delete(chunk[k].id);
|
|
1526
|
+
logLine(`site=${cfg.site} id=${chunk[k].id} の報告ファイルは ${count} 回拒否されたため退けました: ${why}`);
|
|
1527
|
+
}
|
|
1528
|
+
else {
|
|
1529
|
+
kept++;
|
|
1530
|
+
logLine(`site=${cfg.site} id=${chunk[k].id} の報告ファイルは受け取られませんでした(未処理のまま残す): ${why}`);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
else {
|
|
1534
|
+
kept++;
|
|
1535
|
+
logLine(`site=${cfg.site} id=${chunk[k].id} の報告ファイルは受け取られませんでした(${count}/${REPORT_REJECT_MAX}回目・未処理のまま残す): ${why}`);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
else {
|
|
1539
|
+
kept++;
|
|
1493
1540
|
}
|
|
1494
1541
|
}
|
|
1495
1542
|
}
|
|
1496
1543
|
}
|
|
1497
1544
|
if (items.length > 0) {
|
|
1498
|
-
logLine(`site=${cfg.site} 報告ファイル: 送った ${sent} 件・処理済み ${processed} 件・未処理のまま ${kept} 件`);
|
|
1545
|
+
logLine(`site=${cfg.site} 報告ファイル: 送った ${sent} 件・処理済み ${processed} 件・未処理のまま ${kept} 件${rejected > 0 ? `・退けた ${rejected} 件` : ''}`);
|
|
1499
1546
|
}
|
|
1500
|
-
return { sent, processed, kept };
|
|
1547
|
+
return { sent, processed, kept, rejected };
|
|
1501
1548
|
}
|
|
1502
1549
|
/**
|
|
1503
1550
|
* 古い控えを片付ける。★消すのは `processed/` の中だけ(未処理は何日たっても消さない)。
|
package/package.json
CHANGED
|
@@ -613,9 +613,13 @@ else
|
|
|
613
613
|
|
|
614
614
|
doing "Node ${NODE_MAJOR_WANTED} を入れます"
|
|
615
615
|
nvm install "$NODE_MAJOR_WANTED" || die "Node ${NODE_MAJOR_WANTED} を入れられませんでした"
|
|
616
|
+
# ★nvm alias default が lts/* のように解決できない値だと、nvm use default が
|
|
617
|
+
# 終了コード3で黙って落ちる(実測 2026-09-28 FshimaNP)。入れたばかりの版を
|
|
618
|
+
# 名指しで使ったうえで、既定もその版に上書きする。
|
|
619
|
+
nvm use "$NODE_MAJOR_WANTED" >/dev/null \
|
|
620
|
+
|| die "Node ${NODE_MAJOR_WANTED} に切り替えられませんでした。nvm use ${NODE_MAJOR_WANTED} を手動でお試しください。"
|
|
616
621
|
nvm alias default "$NODE_MAJOR_WANTED" >/dev/null \
|
|
617
622
|
|| die 'Node の既定の版を決められませんでした'
|
|
618
|
-
nvm use default >/dev/null || die 'Node に切り替えられませんでした'
|
|
619
623
|
say " 用意できました: $(node --version)"
|
|
620
624
|
record "Node: 入れた($(node --version))"
|
|
621
625
|
fi
|
|
@@ -896,12 +900,84 @@ CONSOLE_URL=''
|
|
|
896
900
|
CONSOLE_TOKEN=''
|
|
897
901
|
|
|
898
902
|
if [[ -f "$ENV_FILE" ]]; then
|
|
899
|
-
skip '接続情報はあります'
|
|
900
903
|
# 権限が緩んでいたら締める(常駐側も起動時に直すが、ここでも揃えておく)。
|
|
901
904
|
if [[ "$DRY_RUN" != "1" ]]; then
|
|
902
905
|
chmod 600 "$ENV_FILE" 2>/dev/null || true
|
|
903
906
|
fi
|
|
904
|
-
|
|
907
|
+
# ★合言葉が管制に通るかを確かめる(実測 2026-09-28: 古い合言葉が 401 なのに
|
|
908
|
+
# 「接続情報はある」で飛ばされ、あとで「指示が届かない」だけが起きた)。
|
|
909
|
+
# 通らなければ入れ直しを聞く。確かめられないとき(ネット不調)は従来どおりスキップ。
|
|
910
|
+
_ENV_URL=''; _ENV_TOKEN=''
|
|
911
|
+
if [[ "$DRY_RUN" != "1" ]]; then
|
|
912
|
+
while IFS='=' read -r k v; do
|
|
913
|
+
case "$k" in
|
|
914
|
+
CONDUCTOR_URL) _ENV_URL="$v" ;;
|
|
915
|
+
CONDUCTOR_TOKEN) _ENV_TOKEN="$v" ;;
|
|
916
|
+
esac
|
|
917
|
+
done < "$ENV_FILE"
|
|
918
|
+
fi
|
|
919
|
+
if [[ -n "$_ENV_URL" && -n "$_ENV_TOKEN" ]]; then
|
|
920
|
+
_check="$(CONDUCTOR_URL="$_ENV_URL" CONDUCTOR_TOKEN="$_ENV_TOKEN" python3 <<'PYEOF' 2>/dev/null || echo unreachable
|
|
921
|
+
import os, urllib.error, urllib.request
|
|
922
|
+
PING_ID = '00000000-0000-4000-8000-000000000000'
|
|
923
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
924
|
+
def redirect_request(self, *a, **k):
|
|
925
|
+
return None
|
|
926
|
+
url = (os.environ.get('CONDUCTOR_URL') or '').rstrip('/')
|
|
927
|
+
token = os.environ.get('CONDUCTOR_TOKEN') or ''
|
|
928
|
+
req = urllib.request.Request(
|
|
929
|
+
f'{url}/api/conductor/hook-requests/{PING_ID}',
|
|
930
|
+
method='GET', headers={'Authorization': f'Bearer {token}'})
|
|
931
|
+
try:
|
|
932
|
+
with urllib.request.build_opener(_NoRedirect).open(req, timeout=10) as res:
|
|
933
|
+
code = getattr(res, 'status', 0) or 0
|
|
934
|
+
print('ok' if 200 <= code < 300 else 'bad-url')
|
|
935
|
+
except urllib.error.HTTPError as e:
|
|
936
|
+
if e.code == 404 or 200 <= e.code < 300:
|
|
937
|
+
print('ok')
|
|
938
|
+
elif e.code in (401, 403):
|
|
939
|
+
print('bad-token')
|
|
940
|
+
elif 300 <= e.code < 400:
|
|
941
|
+
print('bad-url')
|
|
942
|
+
else:
|
|
943
|
+
print('unreachable')
|
|
944
|
+
except Exception:
|
|
945
|
+
print('unreachable')
|
|
946
|
+
PYEOF
|
|
947
|
+
)"
|
|
948
|
+
unset _ENV_TOKEN
|
|
949
|
+
case "$_check" in
|
|
950
|
+
ok)
|
|
951
|
+
skip '接続情報はあります(管制への接続も確認しました)'
|
|
952
|
+
record '接続情報: 済み(接続確認OK)'
|
|
953
|
+
;;
|
|
954
|
+
bad-token)
|
|
955
|
+
say ''
|
|
956
|
+
say ' ★いまの合言葉では管制に拒否されました(401)。'
|
|
957
|
+
say ' 合言葉が変わった可能性があります。入れ直しますか?'
|
|
958
|
+
reenter=''
|
|
959
|
+
ask_tty reenter ' 入れ直す (y/N)' ''
|
|
960
|
+
if [[ "$(lower "$reenter")" == "y" ]]; then
|
|
961
|
+
rm -f "$ENV_FILE"
|
|
962
|
+
say ' 古い接続情報を消しました。もう一度この1行を打ってください。'
|
|
963
|
+
say ' (続きから進みます)'
|
|
964
|
+
exit 1
|
|
965
|
+
else
|
|
966
|
+
note '★入れ直さずに進みます。指示が届かないときは rm ~/.conductor.env のあと、もう一度この1行を打ってください'
|
|
967
|
+
skip '接続情報はあります(★合言葉が古い可能性があります)'
|
|
968
|
+
record '接続情報: 済み(★合言葉が古い可能性あり)'
|
|
969
|
+
fi
|
|
970
|
+
;;
|
|
971
|
+
*)
|
|
972
|
+
# ★ネット不調は止めない。従来どおりスキップ。
|
|
973
|
+
skip '接続情報はあります'
|
|
974
|
+
record '接続情報: 済み'
|
|
975
|
+
;;
|
|
976
|
+
esac
|
|
977
|
+
else
|
|
978
|
+
skip '接続情報はあります'
|
|
979
|
+
record '接続情報: 済み'
|
|
980
|
+
fi
|
|
905
981
|
note '★入れ直したいときは rm ~/.conductor.env のあと、もう一度この1行を打ってください'
|
|
906
982
|
elif [[ "$DRY_RUN" == "1" ]]; then
|
|
907
983
|
plan '管制のアドレスと合言葉をお聞きして、接続情報を作ります'
|
|
@@ -1040,6 +1116,35 @@ else
|
|
|
1040
1116
|
record '設置: 実行した'
|
|
1041
1117
|
fi
|
|
1042
1118
|
|
|
1119
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
1120
|
+
# ⑬½ 画面なし方式(headless)を既定にする
|
|
1121
|
+
#
|
|
1122
|
+
# ★新しいPCは画面なし方式で設置する(えふさん確定)。既存のPCは切り替えない
|
|
1123
|
+
# (プログラムの既定は tmux のまま)。ここは install.sh(新規設置の入口)だけが
|
|
1124
|
+
# 通るので、既存PCには影響しない。
|
|
1125
|
+
# ★systemd の drop-in で環境変数を渡す。ユニット本体は変えない。
|
|
1126
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
1127
|
+
|
|
1128
|
+
UNIT_DIR_INST="$HOME/.config/systemd/user"
|
|
1129
|
+
HEADLESS_DROPIN_DIR="$UNIT_DIR_INST/conductor@${SITE}.service.d"
|
|
1130
|
+
HEADLESS_DROPIN="$HEADLESS_DROPIN_DIR/headless.conf"
|
|
1131
|
+
|
|
1132
|
+
if [[ "$DRY_RUN" == "1" ]]; then
|
|
1133
|
+
plan '画面なし方式(headless)を設定します'
|
|
1134
|
+
else
|
|
1135
|
+
mkdir -p "$HEADLESS_DROPIN_DIR"
|
|
1136
|
+
cat > "$HEADLESS_DROPIN" <<'DROPIN'
|
|
1137
|
+
# install.sh が設定: 画面なし方式(headless)で動かす。
|
|
1138
|
+
# ★tmux に切り替えたいときはこのファイルを消して systemctl --user daemon-reload。
|
|
1139
|
+
[Service]
|
|
1140
|
+
Environment=CONDUCTOR_RUNNER_MODE=headless
|
|
1141
|
+
DROPIN
|
|
1142
|
+
systemctl --user daemon-reload 2>/dev/null || true
|
|
1143
|
+
systemctl --user restart "conductor@${SITE}.service" 2>/dev/null || true
|
|
1144
|
+
say " 画面なし方式を設定しました(drop-in: ${HEADLESS_DROPIN})"
|
|
1145
|
+
record '方式: 画面なし(headless)'
|
|
1146
|
+
fi
|
|
1147
|
+
|
|
1043
1148
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
1044
1149
|
# ⑭ 締め(★自己点検の○×は、すぐ上に設置係が出しています)
|
|
1045
1150
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
@@ -1073,16 +1178,11 @@ ${BAR}
|
|
|
1073
1178
|
✗ が残っているときは、その行に書いてある直し方をお試しください。
|
|
1074
1179
|
|
|
1075
1180
|
${BAR}
|
|
1076
|
-
|
|
1181
|
+
これで準備は終わりです
|
|
1077
1182
|
${BAR}
|
|
1078
1183
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
source ~/.bashrc
|
|
1082
|
-
kc ${SITE}
|
|
1083
|
-
|
|
1084
|
-
(1行目は、いま用意した合言葉をこの画面に読み込みます。
|
|
1085
|
-
2行目で職人が開きます。★次からは「kc ${SITE}」の1行だけで開けます)
|
|
1184
|
+
管制から便を出すと自動で動きます。
|
|
1185
|
+
職人の画面を手で開く必要はありません。
|
|
1086
1186
|
|
|
1087
1187
|
${BAR}
|
|
1088
1188
|
いちばん確実な「動いています」の確かめ方
|
|
@@ -1091,7 +1191,13 @@ ${BAR}
|
|
|
1091
1191
|
管制の画面(采配くん)をブラウザで開いてください。
|
|
1092
1192
|
そこに「${SITE}」が出ていれば、つながって動いています。
|
|
1093
1193
|
|
|
1094
|
-
|
|
1194
|
+
出てこないときは1〜2分待って、開き直してください。
|
|
1095
1195
|
それでも出ないときは、この画面をそのままサポートへお送りください。
|
|
1096
1196
|
|
|
1197
|
+
${BAR}
|
|
1198
|
+
職人の画面を手で開きたいとき(tmux 方式・補助)
|
|
1199
|
+
${BAR}
|
|
1200
|
+
|
|
1201
|
+
source ~/.bashrc && kc ${SITE}
|
|
1202
|
+
|
|
1097
1203
|
GUIDE
|
package/sales-template/setup.sh
CHANGED
|
@@ -108,6 +108,31 @@ MIN_CONDUCTOR_VERSION="0.12.3"
|
|
|
108
108
|
fail() { echo; echo "[setup] エラー: $1" >&2; exit 1; }
|
|
109
109
|
warn() { echo "[setup] warn: $1" >&2; }
|
|
110
110
|
|
|
111
|
+
# 入力元。curl | bash では /dev/tty から読む。テスト等で端末が無い場合は標準入力に落ちる。
|
|
112
|
+
# install.sh の CONDUCTOR_INSTALL_TTY と同じ差し替え口を使う。
|
|
113
|
+
_SETUP_TTY="${CONDUCTOR_INSTALL_TTY:-/dev/tty}"
|
|
114
|
+
# ★/dev/tty は「端末が無いプロセス」でもファイルとしては在る(-r が真を返す)。
|
|
115
|
+
# 実際に開けるかで判定する。開けなければ標準入力に落ちる。
|
|
116
|
+
_SETUP_TTY_OK=0
|
|
117
|
+
if exec 8<"$_SETUP_TTY" 2>/dev/null; then
|
|
118
|
+
exec 8<&-
|
|
119
|
+
_SETUP_TTY_OK=1
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# setup_read — /dev/tty があれば使い、無ければ標準入力から読む。
|
|
123
|
+
# 使い方: setup_read [-s] <変数名> <プロンプト>
|
|
124
|
+
setup_read() {
|
|
125
|
+
local silent='' var prompt
|
|
126
|
+
if [[ "${1:-}" == "-s" ]]; then silent='-s'; shift; fi
|
|
127
|
+
var="$1"; prompt="$2"
|
|
128
|
+
# shellcheck disable=SC2229 # $silent は動的な旗、$var は動的な変数名。意図どおり。
|
|
129
|
+
if [[ "$_SETUP_TTY_OK" == "1" ]]; then
|
|
130
|
+
IFS= read -r $silent -p "$prompt" "$var" <"$_SETUP_TTY" || printf -v "$var" ''
|
|
131
|
+
else
|
|
132
|
+
IFS= read -r $silent -p "$prompt" "$var" || printf -v "$var" ''
|
|
133
|
+
fi
|
|
134
|
+
}
|
|
135
|
+
|
|
111
136
|
# version_lt <a> <b> — a が b より古いか。
|
|
112
137
|
# ★`sort -V` に頼らない(Mac の sort には無い版がある=ADR-019 で Mac も対象)。
|
|
113
138
|
# 数字3つ(major.minor.patch)だけを見る。読めない字は 0 とみなす(安全側=古い扱い)。
|
|
@@ -512,12 +537,12 @@ else
|
|
|
512
537
|
echo "[setup] ★この2つは弊社からお渡しした値です。手元の案内をご覧ください。"
|
|
513
538
|
echo
|
|
514
539
|
|
|
515
|
-
|
|
540
|
+
setup_read input_url " ① 管制のアドレス(CONDUCTOR_URL・例: https://…): "
|
|
516
541
|
[[ -z "$input_url" ]] && fail "管制のアドレスが空です。"
|
|
517
542
|
|
|
518
543
|
# ★合言葉は画面に出さない(-s)。画面共有・録画に残さないため。
|
|
519
544
|
# 何も出ないと固まったと思われるので、受け取ったことを必ず1行返す。
|
|
520
|
-
|
|
545
|
+
setup_read -s input_secret " ② 合言葉(CONDUCTOR_TOKEN・入力しても画面には出ません): "
|
|
521
546
|
echo
|
|
522
547
|
[[ -z "$input_secret" ]] && fail "合言葉が空です。"
|
|
523
548
|
echo "[setup] 受け取りました(${#input_secret} 文字)。画面には表示していません。"
|
|
@@ -821,7 +846,8 @@ fi
|
|
|
821
846
|
if [[ -f "$SETTINGS_SRC" ]]; then
|
|
822
847
|
if [[ -f "$SETTINGS_DST" ]]; then
|
|
823
848
|
echo "[setup] $SETTINGS_DST は既に存在します。上書きしますか?"
|
|
824
|
-
|
|
849
|
+
setup_read overwrite " 上書き (y/N): "
|
|
850
|
+
# shellcheck disable=SC2154 # overwrite は setup_read で間接代入される
|
|
825
851
|
if [[ "$(lower "$overwrite")" != "y" ]]; then
|
|
826
852
|
echo "[setup] 現場の settings.json をスキップしました。"
|
|
827
853
|
else
|
|
@@ -843,9 +869,10 @@ if [[ -f "$SETTINGS_SRC" ]]; then
|
|
|
843
869
|
# Supabase 設定
|
|
844
870
|
if [[ -f "$SETTINGS_DST" ]] && grep -q '<SUPABASE_PROJECT_ID>' "$SETTINGS_DST"; then
|
|
845
871
|
echo
|
|
846
|
-
|
|
872
|
+
setup_read use_supabase "[setup] Supabase を使いますか? (y/N): "
|
|
873
|
+
# shellcheck disable=SC2154 # use_supabase は setup_read で間接代入される
|
|
847
874
|
if [[ "$(lower "$use_supabase")" == "y" ]]; then
|
|
848
|
-
|
|
875
|
+
setup_read supabase_id " Supabase Project Reference ID: "
|
|
849
876
|
if [[ -n "$supabase_id" ]]; then
|
|
850
877
|
sed_replace "s|<SUPABASE_PROJECT_ID>|${supabase_id}|g" "$SETTINGS_DST"
|
|
851
878
|
echo "[setup] <SUPABASE_PROJECT_ID> → $supabase_id に置換"
|
|
@@ -885,7 +912,8 @@ if [[ "$PROFILE" == "proto" ]]; then
|
|
|
885
912
|
write_env_example=1
|
|
886
913
|
if [[ -f "$ENV_EXAMPLE_DST" ]]; then
|
|
887
914
|
echo "[setup] $ENV_EXAMPLE_DST は既に存在します。上書きしますか?"
|
|
888
|
-
|
|
915
|
+
setup_read overwrite_env " 上書き (y/N): "
|
|
916
|
+
# shellcheck disable=SC2154 # overwrite_env は setup_read で間接代入される
|
|
889
917
|
[[ "$(lower "$overwrite_env")" == "y" ]] || write_env_example=0
|
|
890
918
|
fi
|
|
891
919
|
if [[ "$write_env_example" == "1" ]]; then
|
|
@@ -156,7 +156,7 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
|
|
|
156
156
|
```
|
|
157
157
|
この囲みの下に、いつもどおりの報告本文を書く(本文が空だと未完成と見なされる)。
|
|
158
158
|
③ **その後の最終行に、配達役の1行にある `DONE:<現場>` とだけ出力する**(完了の合図)。
|
|
159
|
-
④ **
|
|
159
|
+
④ **ai-kana の MCP ツール `conductor_complete` を、配達役の1行にある commandId で1回だけ呼ぶ**(relay に依存しない確実な完了通知)。report 引数に上の報告本文を添えてもよい。
|
|
160
160
|
- **nextWork(次の独立作業候補)は必ず書く。** 無ければ「なし」と、なぜ無いかの理由を書く。
|
|
161
161
|
- 可能なら次も一緒に渡す(すべて任意・分かる範囲でよく、渡さなくても完了する):`branch=`作った作業ブランチ名(`git rev-parse --abbrev-ref HEAD`)、`baseBranch=`分岐元(通常 main)、`headSha=`最後のコミット(短縮可)、`pushed=`push したか(true/false)、`filesChanged=`変更したファイル数、`prTitle=`お客さま(非エンジニア)がそのまま読む題名(rebase/squash/lint 等の専門用語を避け、やさしい日本語で)、`prNumber=`既に PR がある場合のみ、`outcome=`pushed(変更して push した)/no_change(変更不要だった)/blocked(衝突・要判断・許可待ち等で完了できなかった)のどれか。
|
|
162
162
|
- **blocked のときも必ず conductor_complete を呼び、report に理由を書く。**
|