@tyhld/conductor 0.11.11 → 0.11.12
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/relay.js +122 -67
- package/package.json +1 -1
package/dist/relay.js
CHANGED
|
@@ -200,13 +200,6 @@ export const BUSY_MARKERS = [
|
|
|
200
200
|
* '·' のような普通の出力に頻出する記号も入れない。)
|
|
201
201
|
*/
|
|
202
202
|
export const BUSY_GLYPHS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
203
|
-
/**
|
|
204
|
-
* CC2 の作業報告を囲ませる行マーカー(ADR-029 報告集約・送信側)。
|
|
205
|
-
* CC2 はこの2行で報告本文を挟んで出力し、relay が間の行群を抜き出して中央へ送る。
|
|
206
|
-
* BUSY_MARKERS / BUSY_GLYPHS / `DONE:` のいずれとも文字列が衝突しない(アイドル判定・完了判定を汚さない)。
|
|
207
|
-
*/
|
|
208
|
-
export const REPORT_START = 'REPORT_START';
|
|
209
|
-
export const REPORT_END = 'REPORT_END';
|
|
210
203
|
/** 設定から職人の種類を読む。未指定・知らない値は 'claude'(後退させない)。 */
|
|
211
204
|
export function kindOf(cfg) {
|
|
212
205
|
return cfg.agentKind ?? 'claude';
|
|
@@ -940,11 +933,13 @@ export function selectOrphanToRestore(orphans, currentPending) {
|
|
|
940
933
|
* CC2 の途中送信を防ぐため、改行はすべて空白に潰して「1行=1メッセージ」にする
|
|
941
934
|
* (本文内に生 Enter を含めない=送信は末尾の Enter 1回だけにするため)。
|
|
942
935
|
*
|
|
943
|
-
* 完了時に CC2
|
|
944
|
-
* 1)
|
|
945
|
-
*
|
|
946
|
-
*
|
|
947
|
-
*
|
|
936
|
+
* 完了時に CC2 へ求めること(★2026-09-07 に「画面から読む」をやめ、ファイル経由にした):
|
|
937
|
+
* 1) 報告を【ファイル】に書く(置き場と名前は relayReportDirs / reportFileName が決める)。
|
|
938
|
+
* ★画面に長い報告を出す方式は窓が 16〜34 行しかなく、9/5 以降 162 回すべて失敗していた
|
|
939
|
+
* (調査 d1d16a56)。ファイルなら長さの制限が無く、1文字も落ちない。
|
|
940
|
+
* 2) その後の最終行に必ず DONE:{site} とだけ出力(完了の合図。★これは今までどおり画面で見る)。
|
|
941
|
+
* 3) さらに devlog MCP の conductor_complete を commandId=<id> で1回呼ぶ(relay非依存の確実な通知)。
|
|
942
|
+
* ★(3) が先に通っていれば relay は二重に送らない(completeWithReport が中央へ確かめる)。
|
|
948
943
|
*
|
|
949
944
|
* anchor: この送信に固有のアンカートークン(buildAnchor(id)=`CONDUCTOR_JOB:<id>`)。本文の「最後」に置くことで、
|
|
950
945
|
* 画面では送信エコーの末尾に出る=完了検出(hasDoneBelowAnchor)の境界になる。
|
|
@@ -972,8 +967,11 @@ export function buildSendText(body, site, anchor) {
|
|
|
972
967
|
`英語ではなく日本語で、「プルリクエストを作成する」「テストを実行する」のように` +
|
|
973
968
|
`「何をするか」が一目で分かる簡潔な形(〜を作成する/実行する/削除する 等)で書いてください` +
|
|
974
969
|
`(この説明は管制の確認カードにそのまま表示され、非エンジニアのお客さまが読んで許可を判断します)。\n\n` +
|
|
975
|
-
|
|
976
|
-
|
|
970
|
+
// ★報告はファイルに書いてもらう(2026-09-07)。画面に出す方式は窓が小さすぎて
|
|
971
|
+
// 長い報告の先頭が流れ、9/5 以降 162 回すべて失敗していた(調査 d1d16a56)。
|
|
972
|
+
`作業がすべて完了したら、まず報告を次のファイルに書いてください(変更ファイル・要点・` +
|
|
973
|
+
`チェック結果を簡潔に): ${reportFileCandidates(commandId)[0]} ` +
|
|
974
|
+
`(フォルダが無ければ作ってください。長さの制限はありません。画面に出す必要はありません)。` +
|
|
977
975
|
`その後の最終行に必ず ${marker} とだけ出力してください。\n\n` +
|
|
978
976
|
// ★案D(relay非依存の確実な完了通知): 完了したら必ず devlog MCP の conductor_complete を呼ぶ。
|
|
979
977
|
`そして完了したら必ず、devlog の MCP ツール conductor_complete を commandId="${commandId}" で1回だけ呼んでください` +
|
|
@@ -995,53 +993,6 @@ export function buildSendText(body, site, anchor) {
|
|
|
995
993
|
// 改行(と連続改行)を空白1個に潰し、前後を整える。
|
|
996
994
|
return full.replace(/\r?\n+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
997
995
|
}
|
|
998
|
-
/**
|
|
999
|
-
* 画面テキストから CC2 の報告本文を抜き出す純粋関数(ADR-029 報告集約・送信側)。
|
|
1000
|
-
*
|
|
1001
|
-
* 方針:
|
|
1002
|
-
* - 行単位で「装飾を除くと ${REPORT_START} / ${REPORT_END} だけが残る行」をマーカー行とみなす。
|
|
1003
|
-
* 送信本文のエコー行は1行に両マーカー+指示文が同居するため、装飾除去後に空にならず
|
|
1004
|
-
* マーカー行と判定されない(=誤抽出しない)。
|
|
1005
|
-
* - マーカーが複数あれば「最後のペア」(最後の END と、その直前の最も近い START)を採用する。
|
|
1006
|
-
* CC2 の実報告は送信本文エコーより後(画面下側)に出るため、最後のペア採用で実報告を拾える。
|
|
1007
|
-
* - START/END が無い・対応が壊れている・中身が空のときは null(報告なし)。done化は妨げない。
|
|
1008
|
-
*
|
|
1009
|
-
* 限界(今回スコープ外): tmux capture-pane は画面に見えている範囲しか取れないため、
|
|
1010
|
-
* 報告がスクロールで流れると全文は取れない(取れた範囲を report とする)。
|
|
1011
|
-
*/
|
|
1012
|
-
export function extractReport(text) {
|
|
1013
|
-
const lines = text.replace(/\r/g, '').split('\n');
|
|
1014
|
-
// 装飾(ボックス罫線・行頭バレット・空白)を除いた中身を返す。
|
|
1015
|
-
const stripped = (line) => line.replace(/[│|>⏺●○•*·]/g, '').trim();
|
|
1016
|
-
// 最後の END マーカー行を探す。
|
|
1017
|
-
let endIdx = -1;
|
|
1018
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1019
|
-
if (stripped(lines[i]) === REPORT_END) {
|
|
1020
|
-
endIdx = i;
|
|
1021
|
-
break;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
if (endIdx < 0)
|
|
1025
|
-
return null;
|
|
1026
|
-
// その直前にある最も近い START マーカー行を探す。
|
|
1027
|
-
let startIdx = -1;
|
|
1028
|
-
for (let i = endIdx - 1; i >= 0; i--) {
|
|
1029
|
-
if (stripped(lines[i]) === REPORT_START) {
|
|
1030
|
-
startIdx = i;
|
|
1031
|
-
break;
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
if (startIdx < 0)
|
|
1035
|
-
return null;
|
|
1036
|
-
// 間の行群を取り出し、前後の空行を trim。
|
|
1037
|
-
const inner = lines.slice(startIdx + 1, endIdx);
|
|
1038
|
-
while (inner.length > 0 && inner[0].trim() === '')
|
|
1039
|
-
inner.shift();
|
|
1040
|
-
while (inner.length > 0 && inner[inner.length - 1].trim() === '')
|
|
1041
|
-
inner.pop();
|
|
1042
|
-
const report = inner.join('\n').trim();
|
|
1043
|
-
return report.length > 0 ? report : null;
|
|
1044
|
-
}
|
|
1045
996
|
// ───────────────────────────────────────────────────────────────────────────
|
|
1046
997
|
// tmux I/O(失敗は握りつぶして null/false。送らない側に倒すため)
|
|
1047
998
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -1102,6 +1053,76 @@ export function relaySendTmpDirs() {
|
|
|
1102
1053
|
}
|
|
1103
1054
|
/** tmux バッファ名の接頭辞(他人のバッファと衝突させないため現場・PID・時刻で一意にする)。 */
|
|
1104
1055
|
export const RELAY_SEND_BUFFER_PREFIX = 'conductor-send';
|
|
1056
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1057
|
+
// 報告の受け渡し(★ファイル経由・えふさん決定 2026-09-07)
|
|
1058
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1059
|
+
/**
|
|
1060
|
+
* 職人が報告を書く場所(1指示につき1ファイル)。★relay はここだけを見る。
|
|
1061
|
+
*
|
|
1062
|
+
* 【なぜ画面から読むのをやめたか(調査 d1d16a56 で確定した事実)】
|
|
1063
|
+
* 職人は全画面表示で動くので tmux から見える行は 16〜34 行しかない(実測: 代替画面=1・
|
|
1064
|
+
* 履歴の行数 0〜222 行/上限 2000 行)。報告が数十行あると、終わりの目印と DONE 行が
|
|
1065
|
+
* 下に残った時点で【始まりの目印はもう窓の外】になり、対が見つからず報告が null になる。
|
|
1066
|
+
* ★9/5 以降の実測: 報告を添えた完了 0 件 / 報告なしの完了 162 件。
|
|
1067
|
+
* =画面から報告を読む保険は一度も動いたことがなかった。窓の大きさの問題なので、
|
|
1068
|
+
* 目印の書き方をどう工夫しても直らない。だから【画面を読むのをやめて】ファイルにする。
|
|
1069
|
+
*
|
|
1070
|
+
* 置き場(前から順に試す。理由は writeSendTmpFile と同じ):
|
|
1071
|
+
* 第1候補 /tmp/claude-<uid>/conductor-report … 職人の使い捨て置き場と同じ根(鉄則13)
|
|
1072
|
+
* 第2候補 os.tmpdir()/conductor-report-<uid> … /tmp が使えない機体の保険
|
|
1073
|
+
* 環境変数 CONDUCTOR_REPORT_DIR で1つに固定できる(テスト・特殊な機体向け)。
|
|
1074
|
+
*/
|
|
1075
|
+
export function relayReportDirs(env = process.env) {
|
|
1076
|
+
const explicit = (env.CONDUCTOR_REPORT_DIR ?? '').trim();
|
|
1077
|
+
if (explicit)
|
|
1078
|
+
return [explicit];
|
|
1079
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
|
|
1080
|
+
return [
|
|
1081
|
+
path.join('/tmp', `claude-${uid}`, 'conductor-report'),
|
|
1082
|
+
path.join(os.tmpdir(), `conductor-report-${uid}`),
|
|
1083
|
+
];
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* その指示の報告ファイル名。★指示ID で引ける形にする(どの便の報告かを取り違えない)。
|
|
1087
|
+
* 指示ID に紛れ込みうる区切り文字は落とす(別の場所を指す名前を作らせない)。
|
|
1088
|
+
*/
|
|
1089
|
+
export function reportFileName(commandId) {
|
|
1090
|
+
const safe = String(commandId ?? '').replace(/[^A-Za-z0-9_-]/g, '').slice(0, 128);
|
|
1091
|
+
return `${safe || 'unknown'}.md`;
|
|
1092
|
+
}
|
|
1093
|
+
/** 報告ファイルの置き場の候補(前から順に探す)。 */
|
|
1094
|
+
function reportFileCandidates(commandId, env) {
|
|
1095
|
+
return relayReportDirs(env).map((dir) => path.join(dir, reportFileName(commandId)));
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* その指示の報告を読む(消さない)。無ければ null。
|
|
1099
|
+
* ★読むだけで消さないのは、管制へ書けたことを確かめてから消すため
|
|
1100
|
+
* (書けなかったのに消すと報告が永久に失われる)。
|
|
1101
|
+
*/
|
|
1102
|
+
export function readReport(commandId, env) {
|
|
1103
|
+
for (const file of reportFileCandidates(commandId, env)) {
|
|
1104
|
+
try {
|
|
1105
|
+
const text = readFileSync(file, 'utf8').trim();
|
|
1106
|
+
if (text.length > 0)
|
|
1107
|
+
return text;
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
// 無い・読めない → 次の候補へ(報告が無いこと自体は異常ではない)
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
/** その指示の報告ファイルを片付ける(★同じ報告を二度送らないため)。 */
|
|
1116
|
+
export function discardReport(commandId, env) {
|
|
1117
|
+
for (const file of reportFileCandidates(commandId, env)) {
|
|
1118
|
+
try {
|
|
1119
|
+
unlinkSync(file);
|
|
1120
|
+
}
|
|
1121
|
+
catch {
|
|
1122
|
+
// 無ければ何もしない
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1105
1126
|
/** 本文を書いた一時ファイルを作ってパスを返す。どこにも書けなければ例外。 */
|
|
1106
1127
|
function writeSendTmpFile(text) {
|
|
1107
1128
|
let lastErr = new Error('一時ファイルの置き場が1つも作れませんでした');
|
|
@@ -1341,6 +1362,39 @@ export async function patchStatus(cfg, id, status, report) {
|
|
|
1341
1362
|
return false;
|
|
1342
1363
|
}
|
|
1343
1364
|
}
|
|
1365
|
+
/**
|
|
1366
|
+
* 完了を1回で送る(★報告と完了を同じ1回に載せる・えふさん決定 2026-09-07)。
|
|
1367
|
+
*
|
|
1368
|
+
* 【なぜ1回にするか】以前は「報告なしの完了」を先に送り、報告はあとから追いかけていた。
|
|
1369
|
+
* そのため管制の開発履歴が【本文なしの記録】と【あとから来た本文】の2回に分かれ、
|
|
1370
|
+
* 本文が来ないまま終わると「完了したが報告なし」だけが残った。報告はファイルにあるので、
|
|
1371
|
+
* 完了を送るその1回に必ず載せられる。
|
|
1372
|
+
*
|
|
1373
|
+
* 【★二重に送らない】職人が conductor_complete で先に完了させていたら、relay は何も送らない。
|
|
1374
|
+
* 中央へ1回だけ状態を尋ねて確かめる。尋ねられない(不通)ときは送る側に倒す
|
|
1375
|
+
* =完了が落ちるより、同じ報告が2度書かれるほうが害が小さい(中央は上書き保存する)。
|
|
1376
|
+
*
|
|
1377
|
+
* 【後片付け】管制へ書けたときだけ報告ファイルを消す。書けなければ残す=次tickでやり直せる。
|
|
1378
|
+
* 返り: 追跡を畳んでよいか(true = 完了が確定した)。
|
|
1379
|
+
*/
|
|
1380
|
+
export async function completeWithReport(cfg, id, logLine = log) {
|
|
1381
|
+
const already = await fetchCommandStatus(cfg, id);
|
|
1382
|
+
if (isTerminalCommandStatus(already)) {
|
|
1383
|
+
// 職人が自分で完了させていた。★relay は二重に送らない(報告も上書きしない)。
|
|
1384
|
+
discardReport(id);
|
|
1385
|
+
logLine(`site=${cfg.site} id=${id} は職人が自分で完了させていた(status=${already})` +
|
|
1386
|
+
'→relayからは送りません(報告の置き場だけ片付けました)');
|
|
1387
|
+
return true;
|
|
1388
|
+
}
|
|
1389
|
+
const report = readReport(id);
|
|
1390
|
+
if (!(await patchStatus(cfg, id, 'done', report)))
|
|
1391
|
+
return false;
|
|
1392
|
+
if (report !== null)
|
|
1393
|
+
discardReport(id); // ★書けたら片付ける(同じ報告を二度送らない)
|
|
1394
|
+
logLine(`site=${cfg.site} id=${id} done` +
|
|
1395
|
+
(report !== null ? `(報告添付・${report.length}文字)` : '(報告ファイルなし=完了のみ)'));
|
|
1396
|
+
return true;
|
|
1397
|
+
}
|
|
1344
1398
|
/**
|
|
1345
1399
|
* 指示 {id} 単体の現在 status を中央へ問う(外部完了検出の根治プローブ)。
|
|
1346
1400
|
* GET /api/conductor/commands/{id}(テナントトークン Bearer)。応答 JSON から status 文字列を拾う。
|
|
@@ -1650,9 +1704,11 @@ export async function keepaliveTick(cfg, state, deps = {}) {
|
|
|
1650
1704
|
return;
|
|
1651
1705
|
}
|
|
1652
1706
|
if (action === 'done') {
|
|
1653
|
-
// 完了は従来の経路(画面の DONE
|
|
1654
|
-
const report =
|
|
1707
|
+
// 完了は従来の経路(画面の DONE 行)にそのまま乗せる。★報告は職人が書いたファイルから読む。
|
|
1708
|
+
const report = readReport(ka.id);
|
|
1655
1709
|
if (await complete(cfg, ka.id, report)) {
|
|
1710
|
+
if (report !== null)
|
|
1711
|
+
discardReport(ka.id); // ★書けたら片付ける(二度送らない)
|
|
1656
1712
|
logLine(`site=${cfg.site} id=${ka.id} 見張り終了: 画面のDONEを検出し完了を報告` +
|
|
1657
1713
|
`${report != null ? '(報告添付)' : ''}`);
|
|
1658
1714
|
// ★終わったら必ず手を下ろす(挙げっぱなしにすると現場が長くふさがる)。
|
|
@@ -1828,11 +1884,10 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
1828
1884
|
}
|
|
1829
1885
|
const below = sliceForDoneScan(cap, state.pending.anchor, state.pending.screenDoneWide);
|
|
1830
1886
|
if (below !== null && hasStandaloneDone(below, cfg.site)) {
|
|
1831
|
-
//
|
|
1832
|
-
|
|
1833
|
-
const ok = await
|
|
1887
|
+
// ★報告は画面から読まない。職人が書いたファイルを読む(relayReportDirs / readReport)。
|
|
1888
|
+
// 画面を読む方式は窓が 16〜34 行しかなく、9/5 以降 162 回すべて失敗していた(調査 d1d16a56)。
|
|
1889
|
+
const ok = await completeWithReport(cfg, state.pending.id, log);
|
|
1834
1890
|
if (ok) {
|
|
1835
|
-
log(`site=${cfg.site} id=${state.pending.id} done${report != null ? '(報告添付)' : ''}`);
|
|
1836
1891
|
state.pending = null;
|
|
1837
1892
|
}
|
|
1838
1893
|
return; // sent 中は新規取得しない
|