@tyhld/conductor 0.12.88 → 0.12.90

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 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, deliverJobEndFacts, deliveryReport, fetchCommand, fetchCommandStatus, fetchControl, fetchOrphans, flushPendingJobEndFacts, isTerminalCommandStatus, kindOf, markDelivered, noteCentralDown, noteCentralUp, noteDrain, noteReadiness, patchStatus, recoverStuckProcessedReports, reconnectDelayMs, reportPhase, 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, 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}`);
@@ -557,6 +557,73 @@ export function jobEndKindOf(j) {
557
557
  export function shouldAbortForBodyHandoff(j) {
558
558
  return j.bodyHandoff === 'failed' && !j.stopped && !j.timedOut;
559
559
  }
560
+ /**
561
+ * 便の終わりの関門が残した「途中で終わった」理由の控え(便 7aa63a97・便 461be339 で欄を足した)。
562
+ *
563
+ * ★番人側(`scripts/hooks/finish_gate.py`)が、同じ理由で止める回数の上限に達したときだけ書く。
564
+ * 中継はそれを【読んで消すだけ】で、判定はしない(判定は関門が持つ)。
565
+ * これが要るのは「管制には『途中で終わった』と分かる形で届く」ため。新しい状態は作らず、
566
+ * 既にある「終了の事実」に相乗りする(管制の口を変えない)。
567
+ * ・人向けの1行 … `jobEndDetail`(今までのまま)
568
+ * ・機械で読む形 … `finishGate`(便 461be339 で足した。管制が知らないうちは黙って捨てられる)
569
+ * 控えが無い便=関門が止めずに済んだ便なので、今までどおり null。
570
+ * ★古い版の番人が書いた控え(JSON ではない1行)も読める=入れ替えの途中でも落とさない。
571
+ */
572
+ export function takeFinishGateResidue(commandId, env = process.env) {
573
+ const xdg = (env.XDG_STATE_HOME ?? '').trim();
574
+ const root = xdg || path.join(env.HOME ?? os.homedir(), '.local', 'state');
575
+ const safe = String(commandId ?? '').replace(/[^A-Za-z0-9_-]/g, '').slice(0, 128);
576
+ if (!safe)
577
+ return null;
578
+ const file = path.join(root, 'conductor', 'finish-gate', `${safe}.residue`);
579
+ let raw;
580
+ try {
581
+ raw = readFileSync(file, 'utf8').trim();
582
+ }
583
+ catch {
584
+ return null;
585
+ }
586
+ // ★読んだら消す(次の周で二重に届けない)。消せなくても届けることを優先する。
587
+ try {
588
+ unlinkSync(file);
589
+ }
590
+ catch { /* 消せなくても害は無い(同じ便の控えは1つだけ) */ }
591
+ if (raw.length === 0)
592
+ return null;
593
+ return parseFinishGateResidue(raw);
594
+ }
595
+ /** 控えの字面を読む(★純粋関数。JSON でなければ人向けの1行として扱う)。 */
596
+ export function parseFinishGateResidue(raw) {
597
+ const text = String(raw ?? '').trim();
598
+ if (text.length === 0)
599
+ return null;
600
+ let got = null;
601
+ try {
602
+ got = JSON.parse(text);
603
+ }
604
+ catch {
605
+ return { text: text.slice(0, 500), facts: null }; // 古い形(1行だけ)
606
+ }
607
+ if (got === null || typeof got !== 'object' || Array.isArray(got)) {
608
+ return { text: text.slice(0, 500), facts: null };
609
+ }
610
+ const rec = got;
611
+ const line = typeof rec.text === 'string' ? rec.text.trim() : '';
612
+ if (line.length === 0)
613
+ return null;
614
+ const reasons = Array.isArray(rec.reasons)
615
+ ? FINISH_GATE_KINDS.filter((k) => rec.reasons.includes(k))
616
+ : [];
617
+ const countsRaw = (rec.counts ?? null);
618
+ const counts = {};
619
+ for (const k of FINISH_GATE_KINDS) {
620
+ const n = Number(countsRaw?.[k] ?? 0);
621
+ counts[k] = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
622
+ }
623
+ // ★「何が残っていたか」が読めない控えは、機械で読む形を作らない(作り話の事実を送らない)。
624
+ const facts = reasons.length > 0 ? { exhausted: true, reasons: reasons, counts } : null;
625
+ return { text: line.slice(0, 500), facts };
626
+ }
560
627
  /**
561
628
  * いま画面なし方式が追っている便の指示ID(★回収の道から除く物・便 6ebba0d0 の b)。
562
629
  *
@@ -1251,6 +1318,11 @@ async function checkRunningJob(cfg, state) {
1251
1318
  authError: isAuthError(job),
1252
1319
  exitCode: job.exitCode,
1253
1320
  });
1321
+ // ★「報告を出せたか」はファイルの有無【と形】で見る(便 461be339)。
1322
+ // 形が整っていない=書きかけの報告は「届いた」と数えず、理由を添えて管制へ届ける。
1323
+ // ★本文の言葉は1文字も読まない(2026-09-23 の確定のまま。下書きの印は関門の側だけ)。
1324
+ const delivery = reportDelivery(job.commandId);
1325
+ const residue = takeFinishGateResidue(job.commandId);
1254
1326
  await deliverJobEndFacts(cfg, {
1255
1327
  commandId: job.commandId,
1256
1328
  site: cfg.site,
@@ -1258,12 +1330,20 @@ async function checkRunningJob(cfg, state) {
1258
1330
  endKind,
1259
1331
  exitCode: job.exitCode,
1260
1332
  exitSignal: job.exitSignal,
1261
- reportDelivered: reportPhase(job.commandId) !== 'none',
1333
+ reportDelivered: delivery.delivered,
1262
1334
  // ★画面なし方式は中継が自分で立ち上げた処理なので、終わりを直接つかんでいる。
1263
1335
  // つかめていないときは false を送る(1-3 の「つかめていない」と同じ形)。
1264
1336
  processEndObserved: job.finishedAt !== null,
1265
1337
  startedAt: new Date(job.startedAt).toISOString(),
1266
- detail: job.bodyHandoffError ?? `attempt ${job.attempt}/${HEADLESS_MAX_ATTEMPTS}`,
1338
+ // ★終わりの関門が「途中で終わった」と残していれば、その理由を先に載せる(便 7aa63a97)。
1339
+ // 本文を渡せなかった理由がいちばん強い(職人は何も受け取っていない)ので、それが先。
1340
+ // そのどちらも無く、報告の形が整っていないときは、その理由を載せる(便 461be339)。
1341
+ detail: job.bodyHandoffError
1342
+ ?? residue?.text
1343
+ ?? delivery.note
1344
+ ?? `attempt ${job.attempt}/${HEADLESS_MAX_ATTEMPTS}`,
1345
+ // ★上限に達して「途中で終わった」便だけに付ける(管制が機械で見分けられる形)。
1346
+ ...(residue?.facts != null ? { finishGate: residue.facts } : {}),
1267
1347
  }, { logLine: log });
1268
1348
  }
1269
1349
  if (job.timedOut) {
package/dist/relay.js CHANGED
@@ -1475,6 +1475,81 @@ export function reportPhase(commandId, env) {
1475
1475
  }
1476
1476
  return 'none';
1477
1477
  }
1478
+ // ───────────────────────────────────────────────────────────────────────────
1479
+ // ★報告を「届いた」と数える条件=ファイルが在る【かつ】形が整っている(便 461be339)
1480
+ //
1481
+ // 【なぜ要るか(実地 915694ac・前便 7aa63a97 で残った穴)】従来は reportPhase() が
1482
+ // 【ファイルの有無だけ】を見ていた。だから書きかけの報告でも「届いた」と数えられ、
1483
+ // `processed/` へ移され、管制は「終わりました・✅」になった。合流も公開もしていなかった。
1484
+ // 便の終わりの関門(`scripts/hooks/finish_gate.py`)は職人が【きれいに手番を閉じたとき】に
1485
+ // しか鳴らないので、処理が落ちた・上限時間で止められた便では書きかけが完了の材料になる道が
1486
+ // 残っていた。そこを、中継の側でも閉じる。
1487
+ //
1488
+ // 【★見るのは「形」だけ。本文の言葉は1文字も読まない】報告の中身の言葉で完了を判定しない
1489
+ // (えふさん確定 2026-09-23)。見るのは parseReport() が返す `missing` だけ=
1490
+ // 囲みが閉じている/5つの欄が在る/outcome が決まった語/evidence が1件以上/本文が空でない。
1491
+ // 「作成中」「TODO」などの下書きの印を見るのは関門の側だけ(正本は finish_gate.py)。
1492
+ //
1493
+ // 【★形の決まりの正本は1か所】`scripts/hooks/report-shape.json`。
1494
+ // REQUIRED_REPORT_FIELDS / REPORT_OUTCOMES がその値と同じであることを
1495
+ // `test/report-shape-gate.test.ts` が固定し、同じ `cases` を関門の試験とこちらで流す
1496
+ // =2か所に同じ決まりを書かない。
1497
+ //
1498
+ // 【★形が整っていないときに何をするか】
1499
+ // ・「届いた」と数えない(`reportDelivered: false` + 理由を添えて管制へ届ける)
1500
+ // ・ファイルは【消さない・processed/ へも移さない】=職人が書き直せる・人が読める
1501
+ // ・管制へ本文を渡さない(書きかけを完了の材料にしない)
1502
+ //
1503
+ // 【★例外は1つだけ】「作業なし・本文未着」の申告(isInstructionMissingReport・便 7d810bbb)。
1504
+ // 職人は本文を受け取っていないので囲みを書く材料が無い。実地 af960f79 の報告は囲みが無い。
1505
+ // これを止めると「本文未着が管制に届かない」という直した障害が戻るので、形の確かめから外す。
1506
+ // ───────────────────────────────────────────────────────────────────────────
1507
+ /** 報告の「形」で足りない物(空なら整っている)。★本文の言葉は読まない。 */
1508
+ export function reportShapeMissing(raw) {
1509
+ return parseReport(raw).missing;
1510
+ }
1511
+ /** 形が整っていないことを人へ伝える一言(★報告の中身は1文字も写さない)。 */
1512
+ export function reportShapeNote(missing) {
1513
+ return `報告は在りますが形が整っていません(足りない: ${missing.join(', ')})`;
1514
+ }
1515
+ /** その指示の報告の段と形を1回で見る(★未処理 → 処理済みの順・readParsedReport と同じ順)。 */
1516
+ export function reportDelivery(commandId, env) {
1517
+ const groups = [
1518
+ [pendingCandidates(commandId, env), 'pending'],
1519
+ [processedCandidates(commandId, env), 'processed'],
1520
+ ];
1521
+ for (const [files, phase] of groups) {
1522
+ for (const file of files) {
1523
+ const raw = readIfFilled(file);
1524
+ if (raw === null)
1525
+ continue;
1526
+ const instructionMissing = isInstructionMissingReport(raw);
1527
+ const missing = instructionMissing ? [] : reportShapeMissing(raw);
1528
+ const delivered = missing.length === 0;
1529
+ return {
1530
+ phase,
1531
+ raw,
1532
+ delivered,
1533
+ missing,
1534
+ instructionMissing,
1535
+ note: delivered ? null : reportShapeNote(missing),
1536
+ };
1537
+ }
1538
+ }
1539
+ return {
1540
+ phase: 'none',
1541
+ raw: null,
1542
+ delivered: false,
1543
+ missing: [],
1544
+ instructionMissing: false,
1545
+ note: null,
1546
+ };
1547
+ }
1548
+ /** 管制へ渡してよい報告の本文(形が整っていなければ null=渡さない)。 */
1549
+ export function readDeliverableReport(commandId, env) {
1550
+ const d = reportDelivery(commandId, env);
1551
+ return d.delivered && d.raw !== null ? parseReport(d.raw).body : null;
1552
+ }
1478
1553
  /**
1479
1554
  * 管制へ渡し終えた報告を【処理済み】にする(★消さない・削除しない)。
1480
1555
  * 同じ置き場の `processed/` へ移すだけ。移した時刻を更新時刻に刻む(30日の数え始め)。
@@ -1611,6 +1686,7 @@ export async function sendPendingReportFiles(cfg, opts = {}) {
1611
1686
  const exclude = new Set(opts.exclude ?? []);
1612
1687
  const rejectCounts = opts.rejectCounts ?? new Map();
1613
1688
  const items = [];
1689
+ let malformed = 0;
1614
1690
  for (const id of pendingReportIds(env, REPORT_FILES_MAX_PER_ROUND).ids) {
1615
1691
  for (const file of pendingCandidates(id, env)) {
1616
1692
  const raw = readIfFilled(file);
@@ -1619,6 +1695,17 @@ export async function sendPendingReportFiles(cfg, opts = {}) {
1619
1695
  // (便 7d810bbb)。完了の道はその便を完了にしないので、ここで送らないと管制に届かない。
1620
1696
  if (exclude.has(id) && !isInstructionMissingReport(raw))
1621
1697
  break;
1698
+ // ★形が整っていない報告は送らない(便 461be339)。未処理のまま残すので、職人が書き直せば
1699
+ // 次の周でそのまま届く。★本文の言葉は1文字も読まない(見るのは形だけ)。
1700
+ if (!isInstructionMissingReport(raw)) {
1701
+ const missing = reportShapeMissing(raw);
1702
+ if (missing.length > 0) {
1703
+ malformed++;
1704
+ logLine(`site=${cfg.site} id=${id} ${reportShapeNote(missing)}` +
1705
+ '→管制へ送らず未処理のまま残します(職人が書き直せます)');
1706
+ break;
1707
+ }
1708
+ }
1622
1709
  items.push({ id, raw });
1623
1710
  break;
1624
1711
  }
@@ -1690,10 +1777,12 @@ export async function sendPendingReportFiles(cfg, opts = {}) {
1690
1777
  }
1691
1778
  }
1692
1779
  }
1693
- if (items.length > 0) {
1694
- logLine(`site=${cfg.site} 報告ファイル: 送った ${sent} 件・処理済み ${processed} 件・未処理のまま ${kept} 件${rejected > 0 ? `・退けた ${rejected} 件` : ''}`);
1780
+ if (items.length > 0 || malformed > 0) {
1781
+ logLine(`site=${cfg.site} 報告ファイル: 送った ${sent} 件・処理済み ${processed} 件・未処理のまま ${kept} 件` +
1782
+ `${rejected > 0 ? `・退けた ${rejected} 件` : ''}` +
1783
+ `${malformed > 0 ? `・形が整っていない ${malformed} 件(未処理のまま残す)` : ''}`);
1695
1784
  }
1696
- return { sent, processed, kept, rejected };
1785
+ return { sent, processed, kept, rejected, malformed };
1697
1786
  }
1698
1787
  /**
1699
1788
  * 古い控えを片付ける。★消すのは `processed/` の中だけ(未処理は何日たっても消さない)。
@@ -2620,17 +2709,22 @@ export async function patchStatus(cfg, id, status, report) {
2620
2709
  * 返り: 追跡を畳んでよいか(true = 便の終了状態が確定した/false = 書けていない=次の周でやり直す)。
2621
2710
  */
2622
2711
  export async function completeWithReport(cfg, id, logLine = log) {
2623
- // ★①報告がどの段にあるか(送る工程の話)。②③の実行可否には使わない。
2624
- const phase = reportPhase(id);
2712
+ // ★①報告がどの段にあるか+形が整っているか(送る工程の話)。②③の実行可否には使わない。
2713
+ // ★形が整っていない報告は「届いた」と数えない(便 461be339)。ファイルは消さず・移さず残す。
2714
+ const delivery = reportDelivery(id);
2715
+ const phase = delivery.phase;
2625
2716
  // ★②便が終わっているか(確定の工程の話)。管制の真実だけを見る。
2626
2717
  const already = await fetchCommandStatus(cfg, id);
2627
2718
  if (isCentralFinished(already)) {
2628
2719
  // 既に終端(職人の conductor_complete・人の操作・failed 等)。★何も書かない。
2629
2720
  // 控えは消さずに処理済みとして残す(MCP 経由でも同じ控えが残る・えふさん確定 2026-09-21)。
2630
- if (phase === 'pending')
2721
+ // ★形が整っていない報告は未処理のまま置く(職人が書き直せる・人が読める)。
2722
+ if (phase === 'pending' && delivery.delivered)
2631
2723
  markReportProcessed(id);
2632
2724
  logLine(`site=${cfg.site} id=${id} は管制で既に終わっています(status=${already})` +
2633
- '→relayからは送りません(報告は控えとして処理済みにしました)');
2725
+ (phase === 'pending' && !delivery.delivered
2726
+ ? `→relayからは送りません(${delivery.note}=未処理のまま残します)`
2727
+ : '→relayからは送りません(報告は控えとして処理済みにしました)'));
2634
2728
  return true;
2635
2729
  }
2636
2730
  if (phase === 'processed') {
@@ -2641,13 +2735,19 @@ export async function completeWithReport(cfg, id, logLine = log) {
2641
2735
  logLine(`site=${cfg.site} id=${id} done(報告は回収の道で渡し済み=本文は添えない)`);
2642
2736
  return true;
2643
2737
  }
2644
- const report = readReport(id);
2738
+ // ★形が整っていない報告は添えない(書きかけを完了の材料にしない)。便の確定は今までどおり行う
2739
+ // =現場を占有したままにしない。「報告が届いていない」ことは終了の事実で管制へ届く。
2740
+ const report = delivery.delivered && delivery.raw !== null ? parseReport(delivery.raw).body : null;
2645
2741
  if (!(await patchStatus(cfg, id, 'done', report)))
2646
2742
  return false;
2647
2743
  if (report !== null)
2648
2744
  markReportProcessed(id); // ★消さずに処理済みへ(同じ報告を二度送らない)
2649
2745
  logLine(`site=${cfg.site} id=${id} done` +
2650
- (report !== null ? `(報告添付・${report.length}文字)` : '(報告ファイルなし=完了のみ)'));
2746
+ (report !== null
2747
+ ? `(報告添付・${report.length}文字)`
2748
+ : phase === 'none'
2749
+ ? '(報告ファイルなし=完了のみ)'
2750
+ : `(${delivery.note}=添えずに未処理のまま残します)`));
2651
2751
  return true;
2652
2752
  }
2653
2753
  /**
@@ -2957,6 +3057,8 @@ export async function sendHeartbeat(cfg, id, busy = false, liveness) {
2957
3057
  log(`site=${cfg.site} id=${id} heartbeat -> 失敗: ${err instanceof Error ? err.message : String(err)}(縮退で安全)`);
2958
3058
  }
2959
3059
  }
3060
+ /** 便の終わりの関門の種類(控えの数え方の見出し。★`finish_gate.py` の GATE_KINDS と同じ語)。 */
3061
+ export const FINISH_GATE_KINDS = ['report', 'pr', 'background'];
2960
3062
  /** 届けられなかった「終了の事実」の置き場。 */
2961
3063
  export function jobEndDir(env = process.env) {
2962
3064
  const base = (env.CONDUCTOR_STATE_DIR ?? '').trim();
@@ -2993,6 +3095,10 @@ export function jobEndFactsBody(f) {
2993
3095
  body.startedAt = f.startedAt;
2994
3096
  if (f.detail !== undefined && f.detail !== '')
2995
3097
  body.jobEndDetail = f.detail.slice(0, JOB_END_DETAIL_MAX);
3098
+ // ★上限に達して「途中で終わった」便だけに付ける(付いていること自体が印)。
3099
+ // jobEndDetail の人向けの文は今までのまま残す(管制が知らないうちは黙って捨てられる)。
3100
+ if (f.finishGate !== undefined)
3101
+ body.finishGate = f.finishGate;
2996
3102
  return body;
2997
3103
  }
2998
3104
  /** 控えを手元に書く(★届くまで消さない)。書けたか返す。 */
@@ -3310,12 +3416,16 @@ export async function keepaliveTick(cfg, state, deps = {}) {
3310
3416
  return;
3311
3417
  }
3312
3418
  // 完了は従来の経路(画面の DONE 行)にそのまま乗せる。★報告は職人が書いたファイルから読む。
3313
- const report = readReport(ka.id);
3419
+ // ★形が整っていない報告は添えない・移さない(便 461be339)。
3420
+ const kaDelivery = reportDelivery(ka.id);
3421
+ const report = kaDelivery.delivered && kaDelivery.raw !== null
3422
+ ? parseReport(kaDelivery.raw).body
3423
+ : null;
3314
3424
  if (await complete(cfg, ka.id, report)) {
3315
3425
  if (report !== null)
3316
3426
  markReportProcessed(ka.id); // ★消さずに処理済みへ(二度送らない)
3317
3427
  logLine(`site=${cfg.site} id=${ka.id} 見張り終了: 画面のDONEを検出し完了を報告` +
3318
- `${report != null ? '(報告添付)' : ''}`);
3428
+ `${report != null ? '(報告添付)' : kaDelivery.phase === 'none' ? '' : `(${kaDelivery.note})`}`);
3319
3429
  // ★終わったら必ず手を下ろす(挙げっぱなしにすると現場が長くふさがる)。
3320
3430
  noteBusyEdge(state, cfg.site, ka.id, false, logLine);
3321
3431
  state.keepalive = null;
@@ -3501,7 +3611,8 @@ export async function relayTick(cfg, state, hooks) {
3501
3611
  endKind: 'not_started',
3502
3612
  exitCode: null,
3503
3613
  exitSignal: null,
3504
- reportDelivered: reportPhase(state.pending.id) !== 'none',
3614
+ // ★「作業なし・本文未着」の申告は形の確かめから外してある(便 461be339)。
3615
+ reportDelivered: reportDelivery(state.pending.id).delivered,
3505
3616
  processEndObserved: false,
3506
3617
  detail: '配り役が本文未着と申告した便(職人の処理は中継が持っていない)',
3507
3618
  }, { logLine: log });
@@ -3543,7 +3654,7 @@ export async function relayTick(cfg, state, hooks) {
3543
3654
  endKind: 'completed',
3544
3655
  exitCode: null,
3545
3656
  exitSignal: null,
3546
- reportDelivered: report != null || reportPhase(state.pending.id) !== 'none',
3657
+ reportDelivered: report != null || reportDelivery(state.pending.id).delivered,
3547
3658
  processEndObserved: false,
3548
3659
  detail: '配り役が完了の知らせで確かめた(職人の処理は中継が持っていない)',
3549
3660
  }, { logLine: log });
@@ -3556,7 +3667,8 @@ export async function relayTick(cfg, state, hooks) {
3556
3667
  if (below !== null && hasStandaloneDone(below, cfg.site)) {
3557
3668
  // ★報告は画面から読まない。職人が書いたファイルを読む(relayReportDirs / readReport)。
3558
3669
  // 画面を読む方式は窓が 16〜34 行しかなく、9/5 以降 162 回すべて失敗していた(調査 d1d16a56)。
3559
- const reportThere = reportPhase(state.pending.id) !== 'none';
3670
+ // ★「届いた」は形まで見る(便 461be339)。整っていなければ理由も一緒に届ける。
3671
+ const doneDelivery = reportDelivery(state.pending.id);
3560
3672
  const ok = await completeWithReport(cfg, state.pending.id, log);
3561
3673
  if (ok) {
3562
3674
  // ★終了の事実を同じ項目で届ける(便 915694ac・1-3)。画面へ貼る方式では実プロセスを
@@ -3568,9 +3680,10 @@ export async function relayTick(cfg, state, hooks) {
3568
3680
  endKind: 'completed',
3569
3681
  exitCode: null,
3570
3682
  exitSignal: null,
3571
- reportDelivered: reportThere,
3683
+ reportDelivered: doneDelivery.delivered,
3572
3684
  processEndObserved: false,
3573
- detail: '配り役が画面の完了の合図で確かめた(職人の処理は中継が持っていない)',
3685
+ detail: '配り役が画面の完了の合図で確かめた(職人の処理は中継が持っていない)'
3686
+ + (doneDelivery.note !== null ? `/${doneDelivery.note}` : ''),
3574
3687
  }, { logLine: log });
3575
3688
  state.pending = null;
3576
3689
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.88",
3
+ "version": "0.12.90",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -859,9 +859,12 @@ GUARD_FILES=(
859
859
  policy.py # cc2_guard / permission_request_hook が使う判定本体
860
860
  summary_ja.py # permission_request_hook / stop_hook が使う日本語の文言
861
861
  hook_datalink.py # 各フックが使う管制との連絡線
862
+ finish_gate.py # stop_hook が使う「便の終わりの関門」の判定(便 7aa63a97)
862
863
  )
863
864
  GUARD_ENTRYPOINTS=(cc2_guard.py permission_request_hook.py stop_hook.py notification_hook.py)
864
- GUARD_CONFIGS=(safe_verbs.json network_allow.json) # 任意・あれば同居させる
865
+ # ★この一覧は scripts/update-guard.sh の INSTALL_CONFIGS と【同じ集合】にすること。
866
+ # report-shape.json … 報告の「形」の決まりの正本(finish_gate.py が実行時に読む・便 461be339)。
867
+ GUARD_CONFIGS=(safe_verbs.json network_allow.json report-shape.json) # 任意・あれば同居させる
865
868
 
866
869
  # --- ⑤-0 見本と「配る中身」がズレていないか機械照合する(★足し忘れの再発防止) ---
867
870
  # 見本が呼ぶ入口スクリプトが (a) 正本にあるか (b) 上の GUARD_FILES に入っているか を確かめる。