@tyhld/conductor 0.12.99 → 0.12.101
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/autoupdate.js +64 -15
- package/dist/headless.js +10 -4
- package/dist/relay.js +35 -14
- package/dist/selfcheck.js +98 -4
- package/package.json +1 -1
- package/scripts/hooks/skill_sync_notify.py +22 -10
- package/scripts/hooks/test_skill_sync_notify.py +42 -17
package/dist/autoupdate.js
CHANGED
|
@@ -252,33 +252,82 @@ function installConductor(d, version, offline) {
|
|
|
252
252
|
return { ok: false, detail: `npm install の終了 ${r.status}・入っている版 ${now ?? '不明'}(${(r.error ?? r.stderr).trim().slice(-200)})` };
|
|
253
253
|
}
|
|
254
254
|
/**
|
|
255
|
-
*
|
|
255
|
+
* 入れ替え後に揃える物(配布物の中の元 → 置き場 → 実行権を付けるか)。
|
|
256
|
+
* ★`dir` は ~/.tyhld の下の置き場の名前。番人が職人の書き込みを止める場所なので、
|
|
257
|
+
* ここを書けるのは更新係・設置台本(人が打つ物)だけ。
|
|
258
|
+
*/
|
|
259
|
+
const ENTRY_SYNC = [
|
|
260
|
+
{ from: 'scripts/tyhld-autoupdate.sh', dir: 'bin', to: 'tyhld-autoupdate', exe: true },
|
|
261
|
+
{ from: 'scripts/tyhld-selfcheck.sh', dir: 'bin', to: 'tyhld-selfcheck', exe: true },
|
|
262
|
+
{ from: 'scripts/dist-source.sh', dir: 'bin', to: 'dist-source.sh', exe: false },
|
|
263
|
+
{ from: 'scripts/conductor-run.sh', dir: 'bin', to: 'conductor-run', exe: true },
|
|
264
|
+
{ from: 'scripts/hooks/skill_sync_notify.py', dir: 'bin', to: 'skill_sync_notify.py', exe: false },
|
|
265
|
+
// ★Codex の完了の受け口(便 08af46e0)。
|
|
266
|
+
// 【なぜここで揃えるか】受け口の置き場(~/.tyhld/hooks/codex-notify.py)は
|
|
267
|
+
// ~/.codex/config.toml の notify が【絶対パス】で指す1か所。版が上がって中身が変わっても、
|
|
268
|
+
// 各PCで人が設置台本(install-codex-hooks.sh)を打つまで古い受け口が残り、
|
|
269
|
+
// 新しい配り役が読む受け皿に完了の知らせが入らない。
|
|
270
|
+
// 【広げないための決め】★既に受け口が置いてあるPCだけに写す(下の `not-installed`)。
|
|
271
|
+
// 未設置のPC(Codex を使っていないPC)には1バイトも置かない。
|
|
272
|
+
// ★~/.codex/hooks.json と ~/.codex/config.toml には触らない(設定を変える道は、
|
|
273
|
+
// 今までどおり人が設置台本を打つ)。ここで揃えるのは【受け口のファイルだけ】。
|
|
274
|
+
{ from: 'scripts/codex/codex-notify.py', dir: 'hooks', to: 'codex-notify.py', exe: true },
|
|
275
|
+
];
|
|
276
|
+
/** 中身が1バイトも違わないか(★同じなら書き換えない=毎回の入れ替えで日付だけ動かない)。 */
|
|
277
|
+
function sameContent(a, b) {
|
|
278
|
+
try {
|
|
279
|
+
return readFileSync(a).equals(readFileSync(b));
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* 更新係・自己診断・常駐の入口と Codex の完了の受け口を、入れ替えた配布物から写し直す
|
|
287
|
+
* (★在る物だけ・原子的に置き換える)。
|
|
256
288
|
* 常駐の入口(conductor-run・dist-source.sh・skill_sync_notify.py)は毎時同期でも揃うが、
|
|
257
289
|
* 起こし直す前に揃えておく(新しい配布物の探し方で起きる)。
|
|
258
290
|
*/
|
|
259
291
|
export function refreshEntries(home, distRoot) {
|
|
260
|
-
const bin = path.join(home, '.tyhld', 'bin');
|
|
261
|
-
const pairs = [
|
|
262
|
-
['scripts/tyhld-autoupdate.sh', 'tyhld-autoupdate', true],
|
|
263
|
-
['scripts/tyhld-selfcheck.sh', 'tyhld-selfcheck', true],
|
|
264
|
-
['scripts/dist-source.sh', 'dist-source.sh', false],
|
|
265
|
-
['scripts/conductor-run.sh', 'conductor-run', true],
|
|
266
|
-
['scripts/hooks/skill_sync_notify.py', 'skill_sync_notify.py', false],
|
|
267
|
-
];
|
|
268
292
|
const out = [];
|
|
269
|
-
for (const
|
|
293
|
+
for (const { from, dir, to, exe } of ENTRY_SYNC) {
|
|
270
294
|
const src = path.join(distRoot, from);
|
|
271
|
-
const dst = path.join(
|
|
272
|
-
if (!existsSync(src)
|
|
295
|
+
const dst = path.join(home, '.tyhld', dir, to);
|
|
296
|
+
if (!existsSync(src)) {
|
|
297
|
+
out.push({ name: to, outcome: 'no-source' });
|
|
273
298
|
continue;
|
|
299
|
+
}
|
|
300
|
+
// ★既に置いてある物だけを揃える(未設置のPCには新しく置かない)。
|
|
301
|
+
if (!existsSync(dst)) {
|
|
302
|
+
out.push({ name: to, outcome: 'not-installed' });
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (sameContent(src, dst)) {
|
|
306
|
+
out.push({ name: to, outcome: 'same' });
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
274
309
|
copyFileSync(src, `${dst}.tmp`);
|
|
275
310
|
if (exe)
|
|
276
311
|
chmodSync(`${dst}.tmp`, 0o755);
|
|
277
312
|
renameSync(`${dst}.tmp`, dst);
|
|
278
|
-
out.push(to);
|
|
313
|
+
out.push({ name: to, outcome: 'synced' });
|
|
279
314
|
}
|
|
280
315
|
return out;
|
|
281
316
|
}
|
|
317
|
+
/** 揃えた結果を1行の日本語にする(★記録に残して、あとから何をしたか読めるようにする)。 */
|
|
318
|
+
export function describeEntrySync(results) {
|
|
319
|
+
const label = {
|
|
320
|
+
synced: '揃えた',
|
|
321
|
+
same: '揃える必要が無かった',
|
|
322
|
+
'not-installed': '置いていないので何もしなかった',
|
|
323
|
+
'no-source': '配布物に元が無かった',
|
|
324
|
+
};
|
|
325
|
+
const parts = ['synced', 'same', 'not-installed', 'no-source']
|
|
326
|
+
.map((k) => ({ k, names: results.filter((r) => r.outcome === k).map((r) => r.name) }))
|
|
327
|
+
.filter((g) => g.names.length > 0)
|
|
328
|
+
.map((g) => `${label[g.k]}=${g.names.join(',')}`);
|
|
329
|
+
return `入口と受け口の同期: ${parts.length > 0 ? parts.join('/') : '対象なし'}`;
|
|
330
|
+
}
|
|
282
331
|
/** 常駐(conductor@*・耳・司令塔)の一覧。 */
|
|
283
332
|
function relayUnits(d) {
|
|
284
333
|
const r = d.exec('systemctl', ['--user', 'list-units', 'conductor@*.service', 'conductor-ear.service', 'conductor-build-runner.service', '--all', '--no-legend', '--plain'], { timeoutMs: 15_000 });
|
|
@@ -410,7 +459,7 @@ async function rollBack(d, s, back) {
|
|
|
410
459
|
if (comp === 'conductor') {
|
|
411
460
|
const r = installConductor(d, back, true);
|
|
412
461
|
d.log(r.detail);
|
|
413
|
-
refreshEntries(d.home, d.distRoot);
|
|
462
|
+
d.log(describeEntrySync(refreshEntries(d.home, d.distRoot)));
|
|
414
463
|
d.log(await restartRelays(d, back));
|
|
415
464
|
}
|
|
416
465
|
else {
|
|
@@ -615,7 +664,7 @@ export async function runUpdater(d) {
|
|
|
615
664
|
removeDrain(d, s);
|
|
616
665
|
return done(d, s, 'install-failed', r.detail);
|
|
617
666
|
}
|
|
618
|
-
refreshEntries(d.home, d.distRoot);
|
|
667
|
+
d.log(describeEntrySync(refreshEntries(d.home, d.distRoot)));
|
|
619
668
|
d.log(await restartRelays(d, want));
|
|
620
669
|
}
|
|
621
670
|
else {
|
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, ackResumeSignal, buildAnchor, buildSendText, canDeliver,
|
|
22
|
+
import { CENTRAL_BUSY_HOLD_MAX_MS, RELAY_DELIVER_MAX, RELAY_FETCH_TIMEOUT_MS, RELAY_HEARTBEAT_INTERVAL_MS, RELAY_RESTORE_ORPHANS_ENABLED, ackResumeSignal, buildAnchor, buildSendText, canDeliver, completeWithReportOutcome, createRelayState, deliverJobEndFacts, deliveryReport, fetchCommand, fetchCommandStatus, fetchControl, fetchOrphans, flushPendingJobEndFacts, isTerminalCommandStatus, kindOf, markDelivered, noteCentralDown, noteCentralUp, noteDrain, noteReadiness, patchStatus, recoverStuckProcessedReports, reconnectDelayMs, WAIT_CLOSED_STATUS, FINISH_GATE_KINDS, reportDelivery, 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}`);
|
|
@@ -1471,10 +1471,16 @@ async function checkRunningJob(cfg, state) {
|
|
|
1471
1471
|
// ★書けるまでやり直す(便 6ebba0d0)。以前は false のとき patchStatus を1回試し、
|
|
1472
1472
|
// 成否を見ずに state.job を畳んでいた=誰も done を書いていないのに便を手放していた。
|
|
1473
1473
|
// 畳まない間は次の便を配らない/管制には生存報告と理由が残る。
|
|
1474
|
-
|
|
1474
|
+
// ★「どう畳んだか」を見て、手番の記録にも実際を書く(便 f20dd761 の候補3)。
|
|
1475
|
+
// 以前は畳めたら必ず outcome=done・detail=「報告まで出しました」だった。
|
|
1476
|
+
// 本番操作の承認待ちで閉じた手番(完了は1バイトも書いていない)まで
|
|
1477
|
+
// 「報告まで出しました」になり、`kc 現場名` が「終わりました」と見せていた。
|
|
1478
|
+
// ★札(outcome)は増やさない。実際を言うのは detail の役目にする。
|
|
1479
|
+
const settle = await completeWithReportOutcome(cfg, job.commandId, log);
|
|
1480
|
+
if (settle.settled) {
|
|
1475
1481
|
state.doneWriteTries?.delete(job.commandId);
|
|
1476
|
-
log(`site=${cfg.site} id=${job.commandId}
|
|
1477
|
-
noteLastJob('done',
|
|
1482
|
+
log(`site=${cfg.site} id=${job.commandId} 畳みます(exit=0, attempt ${job.attempt}・${settle.detail})`);
|
|
1483
|
+
noteLastJob('done', settle.detail);
|
|
1478
1484
|
state.job = null;
|
|
1479
1485
|
rs.lastHeartbeatAt = null;
|
|
1480
1486
|
return;
|
package/dist/relay.js
CHANGED
|
@@ -3679,8 +3679,16 @@ export async function patchStatus(cfg, id, status, report) {
|
|
|
3679
3679
|
* 書けなければ未処理のまま残す=次tickでやり直せるし、管制も生存報告で気づける。
|
|
3680
3680
|
*
|
|
3681
3681
|
* 返り: 追跡を畳んでよいか(true = 便の終了状態が確定した/false = 書けていない=次の周でやり直す)。
|
|
3682
|
+
* ★★「どう畳んだか」も要るときは completeWithReportOutcome を呼ぶ(手番の記録に実際を書くため)。
|
|
3682
3683
|
*/
|
|
3683
3684
|
export async function completeWithReport(cfg, id, logLine = log) {
|
|
3685
|
+
return (await completeWithReportOutcome(cfg, id, logLine)).settled;
|
|
3686
|
+
}
|
|
3687
|
+
/**
|
|
3688
|
+
* 完了を1回で送り、【どう畳んだか】も返す(★中身の説明は上の completeWithReport と同じ)。
|
|
3689
|
+
* 呼び手は settled だけ見てもよいが、手番の記録を書くときは kind・detail も使う。
|
|
3690
|
+
*/
|
|
3691
|
+
export async function completeWithReportOutcome(cfg, id, logLine = log) {
|
|
3684
3692
|
// ★①報告がどの段にあるか+形が整っているか(送る工程の話)。②③の実行可否には使わない。
|
|
3685
3693
|
// ★形が整っていない報告は「届いた」と数えない(便 461be339)。ファイルは消さず・移さず残す。
|
|
3686
3694
|
const delivery = reportDelivery(id);
|
|
@@ -3697,7 +3705,7 @@ export async function completeWithReport(cfg, id, logLine = log) {
|
|
|
3697
3705
|
(phase === 'pending' && !delivery.delivered
|
|
3698
3706
|
? `→relayからは送りません(${delivery.note}=未処理のまま残します)`
|
|
3699
3707
|
: '→relayからは送りません(報告は控えとして処理済みにしました)'));
|
|
3700
|
-
return true;
|
|
3708
|
+
return { settled: true, kind: 'already-finished', detail: `管制で既に終わっていました(${already})` };
|
|
3701
3709
|
}
|
|
3702
3710
|
if (isWaitClosedCommandStatus(already)) {
|
|
3703
3711
|
// ★待ちのまま閉じられた便(本番操作の承認待ち・管制 PR #425)。★完了を1バイトも書かない。
|
|
@@ -3709,30 +3717,43 @@ export async function completeWithReport(cfg, id, logLine = log) {
|
|
|
3709
3717
|
logLine(`site=${cfg.site} id=${id} は待ちのまま閉じられています(status=${already})` +
|
|
3710
3718
|
'→完了は書きません(本番操作が済めば管制の合図で同じ便の続きから起きます)' +
|
|
3711
3719
|
(phase === 'none' ? '' : `/報告は手元の控えに残します(${phase})`));
|
|
3712
|
-
return
|
|
3720
|
+
return {
|
|
3721
|
+
settled: true,
|
|
3722
|
+
kind: 'wait-closed',
|
|
3723
|
+
// ★ここが「報告まで出しました」になっていると、承認を待っている手番が
|
|
3724
|
+
// 「終わりました」と記録される(便 f20dd761 の候補3)。実際を書く。
|
|
3725
|
+
detail: '本番操作の承認待ちのまま閉じました(完了は書いていません。承認が済めば続きから起きます)',
|
|
3726
|
+
};
|
|
3713
3727
|
}
|
|
3714
3728
|
if (phase === 'processed') {
|
|
3715
3729
|
// ★①は済み・②は未了。本文は渡してあるので、状態だけを書く(開発履歴を2件にしない)。
|
|
3716
3730
|
// 書けなければ false=次の周でやり直す(完了を落とさない)。
|
|
3717
|
-
if (!(await patchStatus(cfg, id, 'done')))
|
|
3718
|
-
return false;
|
|
3731
|
+
if (!(await patchStatus(cfg, id, 'done'))) {
|
|
3732
|
+
return { settled: false, kind: 'not-written', detail: '便の終了状態を管制へ書けませんでした' };
|
|
3733
|
+
}
|
|
3719
3734
|
logLine(`site=${cfg.site} id=${id} done(報告は回収の道で渡し済み=本文は添えない)`);
|
|
3720
|
-
return true;
|
|
3735
|
+
return { settled: true, kind: 'done', detail: '報告まで出しました' };
|
|
3721
3736
|
}
|
|
3722
3737
|
// ★形が整っていない報告は添えない(書きかけを完了の材料にしない)。便の確定は今までどおり行う
|
|
3723
3738
|
// =現場を占有したままにしない。「報告が届いていない」ことは終了の事実で管制へ届く。
|
|
3724
3739
|
const report = delivery.delivered && delivery.raw !== null ? parseReport(delivery.raw).body : null;
|
|
3725
|
-
if (!(await patchStatus(cfg, id, 'done', report)))
|
|
3726
|
-
return false;
|
|
3740
|
+
if (!(await patchStatus(cfg, id, 'done', report))) {
|
|
3741
|
+
return { settled: false, kind: 'not-written', detail: '便の終了状態を管制へ書けませんでした' };
|
|
3742
|
+
}
|
|
3727
3743
|
if (report !== null)
|
|
3728
3744
|
markReportProcessed(id); // ★消さずに処理済みへ(同じ報告を二度送らない)
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
return
|
|
3745
|
+
const how = report !== null
|
|
3746
|
+
? `(報告添付・${report.length}文字)`
|
|
3747
|
+
: phase === 'none'
|
|
3748
|
+
? '(報告ファイルなし=完了のみ)'
|
|
3749
|
+
: `(${delivery.note}=添えずに未処理のまま残します)`;
|
|
3750
|
+
logLine(`site=${cfg.site} id=${id} done${how}`);
|
|
3751
|
+
return {
|
|
3752
|
+
settled: true,
|
|
3753
|
+
kind: 'done',
|
|
3754
|
+
// ★報告が無い/形が整っていない回を「報告まで出しました」と言わない。
|
|
3755
|
+
detail: report !== null ? '報告まで出しました' : `完了は書きました${how}`,
|
|
3756
|
+
};
|
|
3736
3757
|
}
|
|
3737
3758
|
/**
|
|
3738
3759
|
* 「実プロセスが無い・報告は渡し済み・管制ではまだ終わっていない」便に、便の終了状態だけを書く
|
package/dist/selfcheck.js
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* 【★副作用なし】commit・push・PR・合流・配備・npm 公開をしない。作業場所は一時フォルダ
|
|
19
19
|
* (~/.local/state/conductor/selfcheck/run-<時刻>・git の中ではない)で、終わったら消す。
|
|
20
20
|
*
|
|
21
|
+
* 【★落ちたときは理由を残す(便 5a94ba06)】小さな headless 試験が落ちたら、その項目の detail に
|
|
22
|
+
* 「結果の行の本文・stderr の末尾・終了コード」を残す(控えにも管制にも同じ中身が載る)。
|
|
23
|
+
* ④囲い・⑤通信の ✗ が「小さな試験が先に落ちたので確かめられなかった」ときは、そう読める文にする。
|
|
24
|
+
* ★合否と、落ちたら戻す動きは1つも変えない(FAIL は FAIL・やり直しを足さない)。
|
|
25
|
+
*
|
|
21
26
|
* 【結果】手元の控え(~/.local/state/conductor/selfcheck/last.json と history/)に必ず残す。
|
|
22
27
|
* 管制の口が在れば送る(★口が無い・届かないときも落ちない=送れなかったことを控えに書くだけ)。
|
|
23
28
|
* 送る中身は buildSelfcheckPayload の1か所(口の形は ai-kana の便 2f5eb92c が決めた形・便 29babc5d で合わせた)。
|
|
@@ -81,6 +86,46 @@ export const SELFCHECK_MODEL = 'haiku';
|
|
|
81
86
|
/** 小さな試験の最後の答えを示す目印(★職人の言葉ではなく、tool の結果の中で探す)。 */
|
|
82
87
|
export const PROBE_OK_MARK = 'SELFCHECK_PROBE_OK';
|
|
83
88
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
// 落ちたときの手がかり(便 5a94ba06)
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
/**
|
|
92
|
+
* 控えと管制に残す手がかり1つあたりの長さの上限(★長い出力をそのまま流し込まない)。
|
|
93
|
+
*/
|
|
94
|
+
export const SELFCHECK_DETAIL_MAX = 500;
|
|
95
|
+
/**
|
|
96
|
+
* 鍵・トークンらしい並びを伏せる(★控えにも管制にも残す文なので、残す前に必ず通す)。
|
|
97
|
+
* ここで見るのは「見ただけで鍵と分かる形」だけ。分からない並びは伏せない代わりに、
|
|
98
|
+
* 手がかりは長さを切って残す(safeDetail)。
|
|
99
|
+
*/
|
|
100
|
+
const SECRET_PATTERNS = [
|
|
101
|
+
{ re: /Bearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, to: 'Bearer ***' },
|
|
102
|
+
{ re: /\beyJ[A-Za-z0-9._-]{12,}/g, to: 'eyJ***' },
|
|
103
|
+
{ re: /\b(sk|pk|rk)-[A-Za-z0-9._-]{8,}/gi, to: '$1-***' },
|
|
104
|
+
{ re: /\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{8,}/g, to: '$1_***' },
|
|
105
|
+
{ re: /\bgithub_pat_[A-Za-z0-9_]{8,}/g, to: 'github_pat_***' },
|
|
106
|
+
{ re: /\bnpm_[A-Za-z0-9]{8,}/g, to: 'npm_***' },
|
|
107
|
+
{ re: /\bxox[abeporsu]-[A-Za-z0-9-]{8,}/gi, to: 'xox-***' },
|
|
108
|
+
{ re: /\bAKIA[0-9A-Z]{12,}/g, to: 'AKIA***' },
|
|
109
|
+
];
|
|
110
|
+
/** 秘密らしい並びを伏せる。 */
|
|
111
|
+
export function redactSecrets(raw) {
|
|
112
|
+
let s = raw;
|
|
113
|
+
for (const p of SECRET_PATTERNS)
|
|
114
|
+
s = s.replace(p.re, p.to);
|
|
115
|
+
return s;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 手がかりを残せる形にする(伏せる → 1行にする → 長さを切る)。
|
|
119
|
+
* from='tail' は末尾を残す(stderr は終わりに理由が出るため)。
|
|
120
|
+
* ★返り値は必ず max 字以下。
|
|
121
|
+
*/
|
|
122
|
+
export function safeDetail(raw, from = 'head', max = SELFCHECK_DETAIL_MAX) {
|
|
123
|
+
const one = redactSecrets(raw).replace(/\s+/g, ' ').trim();
|
|
124
|
+
if (one.length <= max)
|
|
125
|
+
return one;
|
|
126
|
+
return from === 'tail' ? `…${one.slice(one.length - (max - 1))}` : `${one.slice(0, max - 1)}…`;
|
|
127
|
+
}
|
|
128
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
84
129
|
// 小さな試験(⑥)の中身と読み方
|
|
85
130
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
86
131
|
/**
|
|
@@ -146,6 +191,7 @@ export function summarizeStream(stdout) {
|
|
|
146
191
|
out.result = {
|
|
147
192
|
subtype: typeof ev.subtype === 'string' ? ev.subtype : null,
|
|
148
193
|
isError: ev.is_error === true,
|
|
194
|
+
text: typeof ev.result === 'string' ? ev.result : typeof ev.error === 'string' ? ev.error : null,
|
|
149
195
|
costUsd: num(ev.total_cost_usd),
|
|
150
196
|
inputTokens: num(usage.input_tokens) === null
|
|
151
197
|
? null
|
|
@@ -533,6 +579,47 @@ function siteSettingsFor(distRoot) {
|
|
|
533
579
|
return null;
|
|
534
580
|
}
|
|
535
581
|
}
|
|
582
|
+
/** 小さな試験が AI まで届いたか(使用量が1つでも在れば届いた)。 */
|
|
583
|
+
function reachedModel(h) {
|
|
584
|
+
const u = h.summary?.result;
|
|
585
|
+
if (!u)
|
|
586
|
+
return false;
|
|
587
|
+
return (u.inputTokens ?? 0) > 0 || (u.outputTokens ?? 0) > 0 || (u.costUsd ?? 0) > 0;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* 小さな試験が落ちたときに、④囲い・⑤通信の ✗ に添える文(便 5a94ba06 の2)。
|
|
591
|
+
* ★「確かめられなかった」のは、小さな試験が先に落ちたせい=そう読める文にする。
|
|
592
|
+
* ★判定(FAIL)は1つも変えない。変えるのは読む文だけ。
|
|
593
|
+
*/
|
|
594
|
+
export function unverifiedReason(h) {
|
|
595
|
+
if (!h.ran)
|
|
596
|
+
return `小さな試験を流していないため、確かめられませんでした(${safeDetail(h.error ?? '理由不明')})`;
|
|
597
|
+
if (h.probeRan)
|
|
598
|
+
return '小さな試験は動きましたが、台本の出力にこの行がなかったため、確かめられませんでした';
|
|
599
|
+
if (!reachedModel(h))
|
|
600
|
+
return '小さな試験が AI に届く前に終わったため、確かめられませんでした';
|
|
601
|
+
return '小さな試験の中で台本が動かなかったため、確かめられませんでした';
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* 小さな試験が落ちたときの手がかり(便 5a94ba06 の1)。
|
|
605
|
+
*
|
|
606
|
+
* 【なぜ要るか(実物 2026-10-04 18:23Z・TYHLD-OTA)】0.12.98 の自己診断が落ちて 0.12.92 へ自動で戻ったが、
|
|
607
|
+
* 落ちた3項目(sandbox・network・headless)はどれも「小さな試験1回」の結果から出ていて、
|
|
608
|
+
* その1回が【なぜ】落ちたかの文が、手元の控えにも管制にも1つも残っていなかった
|
|
609
|
+
* (終了コード 1・結果の行が success で is_error=true・使用量 0)。そのため版の不具合か
|
|
610
|
+
* その場かぎりの失敗かを決められなかった。ここで残すのは、次の3つだけ。
|
|
611
|
+
* ・結果の行の本文(result の文) ・stderr の末尾 ・終了コード
|
|
612
|
+
* ★残す文は safeDetail を通す(鍵・トークンらしい並びは伏せ、1つ 500字で切る)。
|
|
613
|
+
* ★合否は1つも変えない(これは detail に足す文だけ)。通ったときは1行も足さない。
|
|
614
|
+
*/
|
|
615
|
+
export function headlessFailureDetail(h) {
|
|
616
|
+
const out = [];
|
|
617
|
+
const text = h.summary?.result?.text ?? '';
|
|
618
|
+
out.push(text.trim() ? `手がかり: 結果の行の本文 ${safeDetail(text)}` : '手がかり: 結果の行に本文がありません');
|
|
619
|
+
out.push(h.stderr.trim() ? `手がかり: stderr の末尾 ${safeDetail(h.stderr, 'tail')}` : '手がかり: stderr は空です');
|
|
620
|
+
out.push(`手がかり: 終了コード ${h.exitCode === null ? '不明' : h.exitCode}`);
|
|
621
|
+
return out;
|
|
622
|
+
}
|
|
536
623
|
/** 小さな試験と MCP の確かめで claude に渡す環境(★中継と同じ形・src/headless.ts)。 */
|
|
537
624
|
function probeEnv(d, claudeBin) {
|
|
538
625
|
const env = { ...d.env };
|
|
@@ -543,7 +630,7 @@ function probeEnv(d, claudeBin) {
|
|
|
543
630
|
}
|
|
544
631
|
/** ⑥ 小さな headless 試験を1回流す。 */
|
|
545
632
|
function runHeadlessProbe(d, claudeBin, runDir) {
|
|
546
|
-
const res = { summary: null, exitCode: null, outsideWrite: 'unknown', net: {}, probeRan: false, error: null };
|
|
633
|
+
const res = { summary: null, exitCode: null, outsideWrite: 'unknown', net: {}, probeRan: false, error: null, ran: false, stderr: '' };
|
|
547
634
|
if (!claudeBin) {
|
|
548
635
|
res.error = 'claude が無いので流していません';
|
|
549
636
|
return res;
|
|
@@ -570,7 +657,9 @@ function runHeadlessProbe(d, claudeBin, runDir) {
|
|
|
570
657
|
// ★中継と同じ許可(src/headless.ts の既定)=番人がそのまま効く。迂回の旗は付けない。
|
|
571
658
|
'--permission-mode', 'acceptEdits',
|
|
572
659
|
], { cwd: work, env, timeoutMs: SELFCHECK_HEADLESS_TIMEOUT_MS });
|
|
660
|
+
res.ran = true;
|
|
573
661
|
res.exitCode = r.status;
|
|
662
|
+
res.stderr = r.stderr;
|
|
574
663
|
if (r.error)
|
|
575
664
|
res.error = r.error;
|
|
576
665
|
res.summary = summarizeStream(r.stdout);
|
|
@@ -746,7 +835,7 @@ export async function runSelfcheck(d, trigger) {
|
|
|
746
835
|
const h = runHeadlessProbe(d, claude.bin, runDir);
|
|
747
836
|
const sum = h.summary;
|
|
748
837
|
if (h.error)
|
|
749
|
-
note(headless, h.error);
|
|
838
|
+
note(headless, safeDetail(h.error));
|
|
750
839
|
if (!sum || !sum.result)
|
|
751
840
|
fail(headless, 'fault', `結果の行がありません(終了 ${h.exitCode})`);
|
|
752
841
|
else {
|
|
@@ -763,6 +852,11 @@ export async function runSelfcheck(d, trigger) {
|
|
|
763
852
|
else
|
|
764
853
|
fail(headless, 'fault', 'tool の結果に台本の目印がありません(Bash が動いていない)');
|
|
765
854
|
}
|
|
855
|
+
// ★落ちたときは「なぜ落ちたか」の手がかりを必ず残す(控えにも管制にも・便 5a94ba06)。
|
|
856
|
+
// 通ったときは1行も足さない(出力は今までと同じ)。
|
|
857
|
+
if (headless.ok === false)
|
|
858
|
+
for (const line of headlessFailureDetail(h))
|
|
859
|
+
note(headless, line);
|
|
766
860
|
// ③ MCP(★落ち着くまで確かめる・便が実際に使う登録を見る=settleMcp)
|
|
767
861
|
const mcp = await settleMcp(d, claude.bin, path.join(runDir, 'work'), sum?.mcp ?? null);
|
|
768
862
|
const seen = mcp.source === 'mcp-list' ? `claude mcp list で確かめた・${mcp.tries}回目` : mcp.source === 'init' ? '小さな試験の最初の行' : '一覧なし';
|
|
@@ -797,11 +891,11 @@ export async function runSelfcheck(d, trigger) {
|
|
|
797
891
|
else if (h.outsideWrite === 'open')
|
|
798
892
|
fail(sandbox, 'fault', '★囲いの中から作業場所の外へ書けました(囲いが効いていない)');
|
|
799
893
|
else
|
|
800
|
-
fail(sandbox, 'fault',
|
|
894
|
+
fail(sandbox, 'fault', `囲いの中の書き込み: ${unverifiedReason(h)}`);
|
|
801
895
|
// ⑤ 内側の通信
|
|
802
896
|
const codes = Object.entries(h.net);
|
|
803
897
|
if (codes.length === 0)
|
|
804
|
-
fail(network, 'fault',
|
|
898
|
+
fail(network, 'fault', `囲いの内側からの通信: ${unverifiedReason(h)}`);
|
|
805
899
|
for (const [u, c] of codes) {
|
|
806
900
|
if (/^[23]\d\d$/.test(c))
|
|
807
901
|
pass(network, `囲いの内側から ${u} → ${c}`);
|
package/package.json
CHANGED
|
@@ -110,22 +110,32 @@ REINSTALL = ('npm install -g @tyhld/conductor '
|
|
|
110
110
|
RESYNC = 'APPLY=1 bash "$(npm root -g)/@tyhld/conductor/scripts/update-guard.sh"'
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
def autoupdate_enabled():
|
|
114
|
-
"""自動更新が入っている PC か(自社PCの鍵 ~/.tyhld/bin/autoupdate/enabled・便 29babc5d)。
|
|
115
|
-
|
|
113
|
+
def autoupdate_enabled(home=None):
|
|
114
|
+
"""自動更新が入っている PC か(自社PCの鍵 ~/.tyhld/bin/autoupdate/enabled・便 29babc5d)。
|
|
115
|
+
|
|
116
|
+
【なぜ住まいを受け取れる形にしたか(便 f20dd761 の重大発見2・便 08af46e0 で直した)】
|
|
117
|
+
住まいを中で決めていたので、試験が【実機の住まい】を覗いてしまっていた。
|
|
118
|
+
自動更新が入っているPCでは案内が「次の自動更新でそろいます」に変わるため、
|
|
119
|
+
同じ試験が機体によって通ったり落ちたりした。呼び手から渡せる形にして、
|
|
120
|
+
試験は使い捨ての住まいで判定する=どのPCでも同じ結果になる。
|
|
121
|
+
"""
|
|
122
|
+
base = os.path.expanduser('~') if home is None else home
|
|
123
|
+
return os.path.isfile(os.path.join(base, '.tyhld', 'bin', 'autoupdate', 'enabled'))
|
|
116
124
|
|
|
117
125
|
|
|
118
|
-
def recovery_steps(reason):
|
|
126
|
+
def recovery_steps(reason, home=None):
|
|
119
127
|
"""うまくいかなかった理由に【合った】手順を返す(便 675e2d05)。
|
|
120
128
|
|
|
121
129
|
【なぜ理由ごとに分けるか(実地 2026-09-20・便 8692c586 の重大発見1)】
|
|
122
130
|
以前は理由によらず「update-guard を打ってください」の固定文だった。version-drift
|
|
123
131
|
(入っている版と公開の版が違う)のときにこれを打っても、配り元の確認でまた弾かれる=
|
|
124
132
|
同じ失敗を繰り返すだけで直らない。先に入れ直しが要る。
|
|
133
|
+
|
|
134
|
+
home … 自動更新の鍵を探す住まい(既定は実機の住まい。試験は使い捨てを渡す)。
|
|
125
135
|
"""
|
|
126
136
|
if reason in ('flagged-bad', 'flagged-suspect'):
|
|
127
137
|
# ★管制で不具合・疑いの印が付いた版(便 29babc5d)。入れ直しても同じ版なら直らない。
|
|
128
|
-
if autoupdate_enabled():
|
|
138
|
+
if autoupdate_enabled(home):
|
|
129
139
|
return ['再開のしかた: この PC は自動更新が入っています。管制が直した版を決めれば、'
|
|
130
140
|
'次の自動更新で入れ替わります(人の操作は要りません)。']
|
|
131
141
|
return ['再開のしかた: この版には管制で不具合の印が付いています。CC1 に知らせてください'
|
|
@@ -134,7 +144,7 @@ def recovery_steps(reason):
|
|
|
134
144
|
# ★自動更新が入っている PC は、更新係が管制の決めた版へそろえる(便 29babc5d)。
|
|
135
145
|
# 人が npm で入れると段階配布の順番が崩れるので、入れ直しを促さない。
|
|
136
146
|
# ★not-installed・incomplete は配布物そのものが無い=更新係も動けないので、自動更新の有無によらず入れ直す。
|
|
137
|
-
if autoupdate_enabled() and reason in ('unpublished', 'deprecated', 'version-drift'):
|
|
147
|
+
if autoupdate_enabled(home) and reason in ('unpublished', 'deprecated', 'version-drift'):
|
|
138
148
|
return ['再開のしかた: この PC は自動更新が入っています。次の自動更新(管制が決めた版を、'
|
|
139
149
|
'作業中の便が終わってから入れます)でそろいます。管制の「PCの状態」で確かめてください。']
|
|
140
150
|
return [f'再開のしかた: この PC で次の順に打ってください。① {REINSTALL}',
|
|
@@ -153,7 +163,7 @@ def recovery_steps(reason):
|
|
|
153
163
|
return [f'再開のしかた: この PC で次の1行を打ってください。 {RESYNC}']
|
|
154
164
|
|
|
155
165
|
|
|
156
|
-
def build_payload(rec, env=None):
|
|
166
|
+
def build_payload(rec, env=None, home=None):
|
|
157
167
|
"""カードの中身。★何が起きていて、何をすれば直るかを日本語で書く。
|
|
158
168
|
|
|
159
169
|
★本文は【その回の控え(rec)の実際の値】だけから組み立てる。固定の版番号や、
|
|
@@ -165,7 +175,7 @@ def build_payload(rec, env=None):
|
|
|
165
175
|
kind = notice_kind(rec)
|
|
166
176
|
lines = [
|
|
167
177
|
f'いま止まっているもの: この PC({machine})で、{kind["stopped"]}',
|
|
168
|
-
*recovery_steps(reason),
|
|
178
|
+
*recovery_steps(reason, home),
|
|
169
179
|
'★このカードは管制からは押せません(この PC での操作が要ります)。',
|
|
170
180
|
f'PC: {machine}/うまくいかなかった理由: {reason}',
|
|
171
181
|
f'最後に試した時刻: {rec.get("at", "不明")}',
|
|
@@ -231,13 +241,15 @@ def _close(cfg, datalink, cards, key, path=None):
|
|
|
231
241
|
return True
|
|
232
242
|
|
|
233
243
|
|
|
234
|
-
def run(last_path, cfg=None, datalink=hook_datalink, state_path=None):
|
|
244
|
+
def run(last_path, cfg=None, datalink=hook_datalink, state_path=None, home=None):
|
|
235
245
|
"""返り値: 出したカードのID / None(出さなかった・閉じただけ)。
|
|
236
246
|
|
|
237
247
|
★失敗の回 … 前に出した札を【閉じてから】出し直す。管制は同じ鍵の再登録を寿命の延長
|
|
238
248
|
として扱い、本文は最初のままになる(実地 2026-09-20・札に 4日前の版が出ていた)。
|
|
239
249
|
閉じてから出し直せば、本文は必ずその回の値になる。
|
|
240
250
|
★成功の回 … 出していた札を閉じる。直ったのに札が最大60分出たままにならない。
|
|
251
|
+
|
|
252
|
+
home … 自動更新の鍵を探す住まい(既定は実機の住まい。試験は使い捨てを渡す)。
|
|
241
253
|
"""
|
|
242
254
|
rec = parse_last(last_path)
|
|
243
255
|
status = rec.get('status')
|
|
@@ -256,7 +268,7 @@ def run(last_path, cfg=None, datalink=hook_datalink, state_path=None):
|
|
|
256
268
|
|
|
257
269
|
# 失敗。★古い本文の札が残っていたら先に閉じる(上書きできない作りへの対処)。
|
|
258
270
|
_close(cfg, datalink, cards, key, state_path)
|
|
259
|
-
reg = datalink.register(cfg, build_payload(rec))
|
|
271
|
+
reg = datalink.register(cfg, build_payload(rec, home=home))
|
|
260
272
|
if not reg or not reg.get('id'):
|
|
261
273
|
return None
|
|
262
274
|
cards[key] = reg['id']
|
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
(i) 成功の回は【出していた札を閉じる】=直ったのに札が最大60分出たままにならない。
|
|
24
24
|
(j) 再開の手順が【うまくいかなかった理由に合っている】=同じ失敗を繰り返させない。
|
|
25
25
|
版のずれに「配り直してください」とだけ言っても、配り元の確認でまた弾かれる。
|
|
26
|
+
|
|
27
|
+
【★便 08af46e0 で塞いだ穴(便 f20dd761 の重大発見2)】
|
|
28
|
+
(k) 自動更新が入っているかの判定は【呼び手が渡した住まい】だけを見る。
|
|
29
|
+
以前は中で実機の住まい(~/.tyhld/bin/autoupdate/enabled)を覗いていたので、
|
|
30
|
+
自動更新が入ったPCでは案内の文が変わり、(j) の判定が機体ごとに通ったり落ちたりした。
|
|
31
|
+
この試験は使い捨ての住まいを渡すので、どのPCでも同じ結果になる。
|
|
26
32
|
"""
|
|
27
33
|
import json
|
|
28
34
|
import os
|
|
@@ -279,12 +285,19 @@ with tempfile.TemporaryDirectory() as tmp:
|
|
|
279
285
|
# ── (j) 再開の手順が理由に合っている ────────────────────────────────────────
|
|
280
286
|
print('\n【(j) 再開の手順が、うまくいかなかった理由に合っている】')
|
|
281
287
|
with tempfile.TemporaryDirectory() as tmp:
|
|
282
|
-
|
|
288
|
+
# ★自動更新の鍵を探す住まいは、必ず【この回だけの使い捨て】を渡す(便 08af46e0)。
|
|
289
|
+
# 既定は実機の住まい(~/.tyhld/bin/autoupdate/enabled)なので、渡さないと
|
|
290
|
+
# 自動更新が入っているPCでは案内が別の文に変わり、同じ試験が機体によって落ちる
|
|
291
|
+
# (便 f20dd761 の重大発見2)。下の PLAIN_HOME は鍵を置かない=自動更新なしのPC。
|
|
292
|
+
PLAIN_HOME = os.path.join(tmp, 'home-plain')
|
|
293
|
+
os.makedirs(PLAIN_HOME, exist_ok=True)
|
|
294
|
+
|
|
295
|
+
def body_for(reason, what='dist-source', home=PLAIN_HOME):
|
|
283
296
|
line = (f'2026-09-20T09:00:00+0900 status=failed reason={reason} what={what} '
|
|
284
297
|
'version=0.12.6 published=0.12.31 root=/n/@tyhld/conductor')
|
|
285
298
|
link = FakeLink()
|
|
286
299
|
sn.run(write(tmp, f'{reason}.last', line + '\n'), datalink=link,
|
|
287
|
-
state_path=state(tmp, f'{reason}.json'))
|
|
300
|
+
state_path=state(tmp, f'{reason}.json'), home=home)
|
|
288
301
|
return link.sent[0]['question']
|
|
289
302
|
|
|
290
303
|
drift = body_for('version-drift')
|
|
@@ -332,21 +345,33 @@ with tempfile.TemporaryDirectory() as tmp:
|
|
|
332
345
|
|
|
333
346
|
# ★自動更新が入っている PC(鍵 ~/.tyhld/bin/autoupdate/enabled)は、人に npm で入れさせない
|
|
334
347
|
# (更新係が管制の決めた版へそろえる。人が入れると段階配布の順番が崩れる)。
|
|
335
|
-
|
|
336
|
-
os.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
348
|
+
# ★判定する住まいは使い捨てを渡す(実機の住まいを覗かない=どのPCでも同じ結果)。
|
|
349
|
+
AUTO_HOME = os.path.join(tmp, 'home-auto')
|
|
350
|
+
os.makedirs(os.path.join(AUTO_HOME, '.tyhld', 'bin', 'autoupdate'), exist_ok=True)
|
|
351
|
+
write(os.path.join(AUTO_HOME, '.tyhld', 'bin', 'autoupdate'), 'enabled', 'x\n')
|
|
352
|
+
auto = body_for('unpublished', home=AUTO_HOME)
|
|
353
|
+
check_true('自動更新の PC: 次の自動更新でそろうと書く', '次の自動更新' in auto)
|
|
354
|
+
check('自動更新の PC: npm で入れさせない', 'npm install -g' in auto, False)
|
|
355
|
+
check_true('自動更新の PC・不具合の印: 人の操作は要らないと書く',
|
|
356
|
+
'人の操作は要りません' in body_for('flagged-suspect', home=AUTO_HOME))
|
|
357
|
+
check_true('自動更新の PC でも、配布物が無いときは入れ直す',
|
|
358
|
+
'npm install -g' in body_for('not-installed', home=AUTO_HOME))
|
|
359
|
+
|
|
360
|
+
# ★判定は【渡した住まい】だけを見る=実機の住まいに鍵が在っても無くても結果が変わらない
|
|
361
|
+
# (便 f20dd761 の重大発見2 の根治。ここが穴だと、この試験は機体ごとに答えが変わる)。
|
|
362
|
+
check('鍵の無い住まい: 自動更新なしと判定', sn.autoupdate_enabled(PLAIN_HOME), False)
|
|
363
|
+
check('鍵の在る住まい: 自動更新ありと判定', sn.autoupdate_enabled(AUTO_HOME), True)
|
|
364
|
+
for fake_home in (PLAIN_HOME, AUTO_HOME, tmp):
|
|
365
|
+
old_home = os.environ.get('HOME')
|
|
366
|
+
os.environ['HOME'] = fake_home
|
|
367
|
+
try:
|
|
368
|
+
check(f'実機の住まいを覗かない(HOME={os.path.basename(fake_home)})',
|
|
369
|
+
sn.autoupdate_enabled(PLAIN_HOME), False)
|
|
370
|
+
finally:
|
|
371
|
+
if old_home is None:
|
|
372
|
+
os.environ.pop('HOME', None)
|
|
373
|
+
else:
|
|
374
|
+
os.environ['HOME'] = old_home
|
|
350
375
|
|
|
351
376
|
# ── ★札の覚え書き(壊れていても・書けなくても止まらない)──────────────────
|
|
352
377
|
print('\n【★札の覚え書き】')
|