@tyhld/conductor 0.12.89 → 0.12.91
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/build-prompt.js +3 -0
- package/dist/headless.js +56 -10
- package/dist/relay.js +129 -16
- package/package.json +1 -1
- package/sales-template/setup.sh +3 -1
- package/scripts/hooks/finish_gate.py +215 -35
- package/scripts/hooks/report-shape.json +243 -0
- package/scripts/hooks/stop_hook.py +1 -1
- package/scripts/hooks/test_finish_gate.py +107 -3
- package/scripts/hooks/test_report_shape.py +122 -0
- package/scripts/update-guard.sh +5 -1
- package/skills/conductor-craftsman/SKILL.md +3 -2
package/dist/build-prompt.js
CHANGED
|
@@ -132,7 +132,10 @@ export function buildCraftsmanPrompt(input) {
|
|
|
132
132
|
// ★合流の作法は職人の鉄則7と同じ形にする(便 4a5bbb9f)。片方だけ古いと、
|
|
133
133
|
// 職人は毎便読む鉄則と、ここで渡される文の【両方】を受け取って食い違う。
|
|
134
134
|
// ★2026-09-17(便 01bd516b)に保護の有無で分けた形へ直した(鉄則7 と同じ中身)。
|
|
135
|
+
// ★push は上流の設定を書かない形(鉄則7-⓪・便 ec974d68)。`-u` は `.git/config` へ書き込むので、
|
|
136
|
+
// 囲いがそこを塞いでいる機体では push が成功しても「上流の設定を書けない」で止まる。
|
|
135
137
|
'・テストが緑になってから push → プルリクエストを作成 → 合流する。合流の仕方は職人の鉄則7のとおり:',
|
|
138
|
+
' push は `git push origin <作業ブランチ名>`(★`-u` を付けない=上流の設定を書かない形)。',
|
|
136
139
|
' 保護あり(必須の検査がある)… `gh pr merge <番号> --auto --merge --delete-branch` で予約する。',
|
|
137
140
|
' 保護なし … ★`--auto` を先に打たない。`gh pr checks <番号>` で CI(`/actions/runs/` の行)がすべて pass なのを',
|
|
138
141
|
' 確かめてから `gh pr merge <番号> --merge --delete-branch`。Vercel だけの緑は CI の緑と数えない。',
|
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,
|
|
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}`);
|
|
@@ -558,13 +558,16 @@ export function shouldAbortForBodyHandoff(j) {
|
|
|
558
558
|
return j.bodyHandoff === 'failed' && !j.stopped && !j.timedOut;
|
|
559
559
|
}
|
|
560
560
|
/**
|
|
561
|
-
* 便の終わりの関門が残した「途中で終わった」理由の控え(便 7aa63a97
|
|
561
|
+
* 便の終わりの関門が残した「途中で終わった」理由の控え(便 7aa63a97・便 461be339 で欄を足した)。
|
|
562
562
|
*
|
|
563
|
-
* ★番人側(`scripts/hooks/finish_gate.py
|
|
564
|
-
*
|
|
563
|
+
* ★番人側(`scripts/hooks/finish_gate.py`)が、同じ理由で止める回数の上限に達したときだけ書く。
|
|
564
|
+
* 中継はそれを【読んで消すだけ】で、判定はしない(判定は関門が持つ)。
|
|
565
565
|
* これが要るのは「管制には『途中で終わった』と分かる形で届く」ため。新しい状態は作らず、
|
|
566
|
-
*
|
|
566
|
+
* 既にある「終了の事実」に相乗りする(管制の口を変えない)。
|
|
567
|
+
* ・人向けの1行 … `jobEndDetail`(今までのまま)
|
|
568
|
+
* ・機械で読む形 … `finishGate`(便 461be339 で足した。管制が知らないうちは黙って捨てられる)
|
|
567
569
|
* 控えが無い便=関門が止めずに済んだ便なので、今までどおり null。
|
|
570
|
+
* ★古い版の番人が書いた控え(JSON ではない1行)も読める=入れ替えの途中でも落とさない。
|
|
568
571
|
*/
|
|
569
572
|
export function takeFinishGateResidue(commandId, env = process.env) {
|
|
570
573
|
const xdg = (env.XDG_STATE_HOME ?? '').trim();
|
|
@@ -573,9 +576,9 @@ export function takeFinishGateResidue(commandId, env = process.env) {
|
|
|
573
576
|
if (!safe)
|
|
574
577
|
return null;
|
|
575
578
|
const file = path.join(root, 'conductor', 'finish-gate', `${safe}.residue`);
|
|
576
|
-
let
|
|
579
|
+
let raw;
|
|
577
580
|
try {
|
|
578
|
-
|
|
581
|
+
raw = readFileSync(file, 'utf8').trim();
|
|
579
582
|
}
|
|
580
583
|
catch {
|
|
581
584
|
return null;
|
|
@@ -585,7 +588,41 @@ export function takeFinishGateResidue(commandId, env = process.env) {
|
|
|
585
588
|
unlinkSync(file);
|
|
586
589
|
}
|
|
587
590
|
catch { /* 消せなくても害は無い(同じ便の控えは1つだけ) */ }
|
|
588
|
-
|
|
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 };
|
|
589
626
|
}
|
|
590
627
|
/**
|
|
591
628
|
* いま画面なし方式が追っている便の指示ID(★回収の道から除く物・便 6ebba0d0 の b)。
|
|
@@ -1281,6 +1318,11 @@ async function checkRunningJob(cfg, state) {
|
|
|
1281
1318
|
authError: isAuthError(job),
|
|
1282
1319
|
exitCode: job.exitCode,
|
|
1283
1320
|
});
|
|
1321
|
+
// ★「報告を出せたか」はファイルの有無【と形】で見る(便 461be339)。
|
|
1322
|
+
// 形が整っていない=書きかけの報告は「届いた」と数えず、理由を添えて管制へ届ける。
|
|
1323
|
+
// ★本文の言葉は1文字も読まない(2026-09-23 の確定のまま。下書きの印は関門の側だけ)。
|
|
1324
|
+
const delivery = reportDelivery(job.commandId);
|
|
1325
|
+
const residue = takeFinishGateResidue(job.commandId);
|
|
1284
1326
|
await deliverJobEndFacts(cfg, {
|
|
1285
1327
|
commandId: job.commandId,
|
|
1286
1328
|
site: cfg.site,
|
|
@@ -1288,16 +1330,20 @@ async function checkRunningJob(cfg, state) {
|
|
|
1288
1330
|
endKind,
|
|
1289
1331
|
exitCode: job.exitCode,
|
|
1290
1332
|
exitSignal: job.exitSignal,
|
|
1291
|
-
reportDelivered:
|
|
1333
|
+
reportDelivered: delivery.delivered,
|
|
1292
1334
|
// ★画面なし方式は中継が自分で立ち上げた処理なので、終わりを直接つかんでいる。
|
|
1293
1335
|
// つかめていないときは false を送る(1-3 の「つかめていない」と同じ形)。
|
|
1294
1336
|
processEndObserved: job.finishedAt !== null,
|
|
1295
1337
|
startedAt: new Date(job.startedAt).toISOString(),
|
|
1296
1338
|
// ★終わりの関門が「途中で終わった」と残していれば、その理由を先に載せる(便 7aa63a97)。
|
|
1297
1339
|
// 本文を渡せなかった理由がいちばん強い(職人は何も受け取っていない)ので、それが先。
|
|
1340
|
+
// そのどちらも無く、報告の形が整っていないときは、その理由を載せる(便 461be339)。
|
|
1298
1341
|
detail: job.bodyHandoffError
|
|
1299
|
-
??
|
|
1342
|
+
?? residue?.text
|
|
1343
|
+
?? delivery.note
|
|
1300
1344
|
?? `attempt ${job.attempt}/${HEADLESS_MAX_ATTEMPTS}`,
|
|
1345
|
+
// ★上限に達して「途中で終わった」便だけに付ける(管制が機械で見分けられる形)。
|
|
1346
|
+
...(residue?.facts != null ? { finishGate: residue.facts } : {}),
|
|
1301
1347
|
}, { logLine: log });
|
|
1302
1348
|
}
|
|
1303
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}
|
|
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
|
-
|
|
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
|
-
|
|
2721
|
+
// ★形が整っていない報告は未処理のまま置く(職人が書き直せる・人が読める)。
|
|
2722
|
+
if (phase === 'pending' && delivery.delivered)
|
|
2631
2723
|
markReportProcessed(id);
|
|
2632
2724
|
logLine(`site=${cfg.site} id=${id} は管制で既に終わっています(status=${already})` +
|
|
2633
|
-
'
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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:
|
|
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
package/sales-template/setup.sh
CHANGED
|
@@ -862,7 +862,9 @@ GUARD_FILES=(
|
|
|
862
862
|
finish_gate.py # stop_hook が使う「便の終わりの関門」の判定(便 7aa63a97)
|
|
863
863
|
)
|
|
864
864
|
GUARD_ENTRYPOINTS=(cc2_guard.py permission_request_hook.py stop_hook.py notification_hook.py)
|
|
865
|
-
|
|
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) # 任意・あれば同居させる
|
|
866
868
|
|
|
867
869
|
# --- ⑤-0 見本と「配る中身」がズレていないか機械照合する(★足し忘れの再発防止) ---
|
|
868
870
|
# 見本が呼ぶ入口スクリプトが (a) 正本にあるか (b) 上の GUARD_FILES に入っているか を確かめる。
|
|
@@ -124,46 +124,197 @@ def read_report_text(command_id, env=None):
|
|
|
124
124
|
|
|
125
125
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
126
126
|
# (2) 報告が無い・下書きのまま
|
|
127
|
+
#
|
|
128
|
+
# 【★形の決まりの正本は1か所(便 461be339)】
|
|
129
|
+
# 「囲みが閉じている/5つの欄が在る/outcome が決まった語/evidence が1件以上/本文が空でない」は
|
|
130
|
+
# 中継(src/relay.ts の parseReport / missingOf)も同じ判定をする。同じ決まりを2か所に書くと、
|
|
131
|
+
# 片方だけ直して食い違う(=関門は差し戻すのに中継は届いたと数える、またはその逆)。
|
|
132
|
+
# だから決まりは `report-shape.json`(この python と同じフォルダに配られる)だけに置き、
|
|
133
|
+
# ここは【読み手】にする。中継の側は定数がこの JSON と同じであることを試験で固定する。
|
|
134
|
+
# ★読めなかったときは形の判定をしない(fail open=関門が足される前と同じ動き)。
|
|
135
|
+
#
|
|
136
|
+
# 【★本文の言葉だけは別】下書きの印(作成中・TODO など)は関門だけの決まり。
|
|
137
|
+
# 報告の中身の言葉で完了を判定しない(えふさん確定 2026-09-23)ので、中継からは手が届かない
|
|
138
|
+
# ところ(この下の DRAFT_MARKERS)に置く。正本はここ1か所。
|
|
139
|
+
#
|
|
140
|
+
# 【★印を探す場所は本文の「頭」と evidence だけ(便 ec974d68)】
|
|
141
|
+
# 以前は本文の全体を見ていた。そのため【関門そのものを説明した報告】が、説明の中に出てくる
|
|
142
|
+
# 印の語(「作成中」など)に当たって差し戻された(実地・便 461be339=言い回しを替えて通した=
|
|
143
|
+
# 3手番の無駄)。仕上げ前の報告では、印は「頭(題の行とその次の段落)」か evidence に現れる
|
|
144
|
+
# (実地 915694ac はその形)。だから見る場所を頭と evidence に絞る=塞ぎたい物は塞いだまま、
|
|
145
|
+
# 説明での誤当たりだけが消える。★印の一覧(下)は1語も減らさない。
|
|
127
146
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
-
#: 囲み(front matter)に要る欄。鉄則「便の終え方」②のとおり。
|
|
129
|
-
REQUIRED_KEYS = ('instruction_id', 'site', 'outcome', 'created_at', 'evidence')
|
|
130
|
-
#: `outcome` に書ける値。
|
|
131
|
-
VALID_OUTCOMES = ('pushed', 'no_change', 'blocked')
|
|
132
147
|
#: 「最終版で差し替える前」の印。★決まった字面の一覧だけを見る(文意は読まない)。
|
|
133
148
|
DRAFT_MARKERS = ('作成中', '下書き', '差し替える', 'あとで書く', '(未記入)', '(未記入)', 'TODO', 'TBD')
|
|
134
149
|
|
|
150
|
+
#: 見出しの行(`#` 〜 `######`)。★本文の「頭」を切り出すためだけに使う。
|
|
151
|
+
_HEADING_RE = re.compile(r'^ {0,3}#{1,6}\s')
|
|
135
152
|
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
#: 形の決まりの正本(★この python と同じフォルダに配られる)。
|
|
154
|
+
SHAPE_SPEC_NAME = 'report-shape.json'
|
|
155
|
+
|
|
156
|
+
#: 足りない物の名前 → 職人へ見せる日本語。
|
|
157
|
+
_SHAPE_JA = {
|
|
158
|
+
'instruction_id': 'instruction_id の欄',
|
|
159
|
+
'site': 'site の欄',
|
|
160
|
+
'outcome': 'outcome(pushed / no_change / blocked のどれか1つ)',
|
|
161
|
+
'created_at': 'created_at の欄',
|
|
162
|
+
'evidence': 'evidence(1件以上)',
|
|
163
|
+
'body': '囲みの下の本文',
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def shape_spec_path():
|
|
168
|
+
"""形の決まりの正本の置き場(自分と同じフォルダ)。"""
|
|
169
|
+
return os.path.join(os.path.dirname(os.path.abspath(__file__)), SHAPE_SPEC_NAME)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def load_shape_spec(path=None):
|
|
173
|
+
"""形の決まりを読む。読めない・形が違うなら None(=形の判定をしない)。"""
|
|
174
|
+
try:
|
|
175
|
+
with open(path or shape_spec_path(), encoding='utf-8') as f:
|
|
176
|
+
got = json.load(f)
|
|
177
|
+
except Exception: # noqa: BLE001
|
|
178
|
+
return None
|
|
179
|
+
if not isinstance(got, dict):
|
|
180
|
+
return None
|
|
181
|
+
fields = got.get('requiredFields')
|
|
182
|
+
outcomes = got.get('validOutcomes')
|
|
183
|
+
if not isinstance(fields, list) or not fields or not all(isinstance(k, str) for k in fields):
|
|
184
|
+
return None
|
|
185
|
+
if not isinstance(outcomes, list) or not outcomes or not all(isinstance(k, str) for k in outcomes):
|
|
186
|
+
return None
|
|
187
|
+
return got
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
#: 読み込んだ正本(無ければ None)。
|
|
191
|
+
SHAPE_SPEC = load_shape_spec()
|
|
192
|
+
|
|
193
|
+
_KV_RE = re.compile(r'^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$')
|
|
194
|
+
_BULLET_RE = re.compile(r'^\s*-\s+(.*)$')
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _parse_report(text):
|
|
198
|
+
"""報告を欄と本文に分ける(★中継の parseReport と同じ読み方)。
|
|
199
|
+
|
|
200
|
+
返り: (欄の辞書, evidence の一覧, 本文)。囲みが無い・閉じていないときは
|
|
201
|
+
欄も evidence も空で、全文が本文になる(中継と同じ倒れ方)。
|
|
202
|
+
"""
|
|
203
|
+
raw = '' if text is None else str(text)
|
|
204
|
+
if raw.startswith(''):
|
|
205
|
+
raw = raw[1:]
|
|
206
|
+
lines = raw.split('\n')
|
|
207
|
+
i = 0
|
|
208
|
+
while i < len(lines) and lines[i].strip() == '':
|
|
209
|
+
i += 1
|
|
210
|
+
if i >= len(lines) or lines[i].strip() != '---':
|
|
211
|
+
return {}, [], raw.strip()
|
|
212
|
+
fields, evidence, last_key = {}, [], ''
|
|
213
|
+
j = i + 1
|
|
214
|
+
closed = False
|
|
215
|
+
while j < len(lines):
|
|
216
|
+
line = lines[j]
|
|
217
|
+
if line.strip() == '---':
|
|
218
|
+
closed = True
|
|
219
|
+
break
|
|
220
|
+
bullet = _BULLET_RE.match(line)
|
|
221
|
+
if bullet is not None:
|
|
222
|
+
if last_key == 'evidence' and bullet.group(1).strip() != '':
|
|
223
|
+
evidence.append(bullet.group(1).strip())
|
|
224
|
+
j += 1
|
|
225
|
+
continue
|
|
226
|
+
kv = _KV_RE.match(line)
|
|
227
|
+
if kv is None:
|
|
228
|
+
j += 1
|
|
229
|
+
continue
|
|
230
|
+
last_key = kv.group(1).lower()
|
|
231
|
+
value = kv.group(2).strip()
|
|
232
|
+
if last_key == 'evidence':
|
|
233
|
+
if value != '':
|
|
234
|
+
evidence.append(value)
|
|
235
|
+
j += 1
|
|
236
|
+
continue
|
|
237
|
+
fields[last_key] = value
|
|
238
|
+
j += 1
|
|
239
|
+
if not closed:
|
|
240
|
+
return {}, [], raw.strip()
|
|
241
|
+
return fields, evidence, '\n'.join(lines[j + 1:]).strip()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def report_shape_missing(text, spec=None):
|
|
245
|
+
"""報告の「形」で足りない物を並べる(空なら整っている)。★純粋関数。
|
|
246
|
+
|
|
247
|
+
★正本が読めないときは [] を返す(形の判定をしない=退行しない)。
|
|
248
|
+
"""
|
|
249
|
+
rules = SHAPE_SPEC if spec is None else spec
|
|
250
|
+
if not rules:
|
|
251
|
+
return []
|
|
252
|
+
fields, evidence, body = _parse_report(text)
|
|
253
|
+
outcomes = tuple(rules['validOutcomes'])
|
|
254
|
+
out = []
|
|
255
|
+
for key in rules['requiredFields']:
|
|
256
|
+
if key == 'body':
|
|
257
|
+
if body.strip() == '':
|
|
258
|
+
out.append(key)
|
|
259
|
+
elif key == 'evidence':
|
|
260
|
+
if not evidence:
|
|
261
|
+
out.append(key)
|
|
262
|
+
elif key == 'outcome':
|
|
263
|
+
if (fields.get('outcome') or '').strip() not in outcomes:
|
|
264
|
+
out.append(key)
|
|
265
|
+
elif not (fields.get(key) or '').strip():
|
|
266
|
+
out.append(key)
|
|
267
|
+
return out
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def report_body(text):
|
|
271
|
+
"""報告の本文(囲みの下)。囲みが無い・閉じていないときは全文。★純粋関数。"""
|
|
272
|
+
return _parse_report(text)[2]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _body_head(body):
|
|
276
|
+
"""本文の「頭」の行だけを並べる=最初の見出しの行と、その直後の1段落。★純粋関数。
|
|
277
|
+
|
|
278
|
+
見出しが無い本文では、最初の1段落だけ(頭は段落から始まっている)。
|
|
279
|
+
"""
|
|
280
|
+
lines = ('' if body is None else str(body)).split('\n')
|
|
281
|
+
i = 0
|
|
282
|
+
while i < len(lines) and lines[i].strip() == '':
|
|
283
|
+
i += 1
|
|
284
|
+
out = []
|
|
285
|
+
if i < len(lines) and _HEADING_RE.match(lines[i]):
|
|
286
|
+
out.append(lines[i])
|
|
287
|
+
i += 1
|
|
288
|
+
while i < len(lines) and lines[i].strip() == '':
|
|
289
|
+
i += 1
|
|
290
|
+
while i < len(lines) and lines[i].strip() != '':
|
|
291
|
+
out.append(lines[i])
|
|
292
|
+
i += 1
|
|
293
|
+
return out
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def report_draft_places(text):
|
|
297
|
+
"""下書きの印を探す場所だけを並べる(★本文の全体は見ない・便 ec974d68)。★純粋関数。
|
|
298
|
+
|
|
299
|
+
① 囲みのすぐ下の最初の見出しの行 ② その直後の1段落 ③ evidence の各行。
|
|
300
|
+
"""
|
|
301
|
+
_, evidence, body = _parse_report(text)
|
|
302
|
+
return [str(e) for e in evidence] + _body_head(body)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def report_residue(text, spec=None):
|
|
306
|
+
"""報告が「無い・形が整っていない・下書きのまま」なら理由、出来上がっていれば None。"""
|
|
138
307
|
if text is None or not text.strip():
|
|
139
308
|
return '報告ファイルが無い(または空)'
|
|
140
|
-
|
|
141
|
-
if lines[0].strip() != '---':
|
|
142
|
-
return '報告の1行目に囲み(---)が無い'
|
|
143
|
-
try:
|
|
144
|
-
end = next(i for i in range(1, len(lines)) if lines[i].strip() == '---')
|
|
145
|
-
except StopIteration:
|
|
146
|
-
return '報告の囲みが閉じていない(2つめの --- が無い)'
|
|
147
|
-
head = lines[1:end]
|
|
148
|
-
body = '\n'.join(lines[end + 1:]).strip()
|
|
149
|
-
keys = {}
|
|
150
|
-
for ln in head:
|
|
151
|
-
m = re.match(r'^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$', ln)
|
|
152
|
-
if m:
|
|
153
|
-
keys[m.group(1)] = m.group(2).strip()
|
|
154
|
-
missing = [k for k in REQUIRED_KEYS if k not in keys]
|
|
309
|
+
missing = report_shape_missing(text, spec)
|
|
155
310
|
if missing:
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return '報告の本文が空(囲みだけ)'
|
|
164
|
-
hit = next((m for m in DRAFT_MARKERS if m in body), None)
|
|
165
|
-
if hit is not None:
|
|
166
|
-
return f'報告が下書きのまま(本文に「{hit}」が残っている)'
|
|
311
|
+
return ('報告の形が整っていない(足りない: '
|
|
312
|
+
+ '・'.join(_SHAPE_JA.get(k, k) for k in missing) + ')')
|
|
313
|
+
for place in report_draft_places(text):
|
|
314
|
+
hit = next((m for m in DRAFT_MARKERS if m in place), None)
|
|
315
|
+
if hit is not None:
|
|
316
|
+
return ('報告が下書きのまま(報告の頭か evidence に「'
|
|
317
|
+
+ hit + '」が残っている)')
|
|
167
318
|
return None
|
|
168
319
|
|
|
169
320
|
|
|
@@ -306,13 +457,25 @@ def save_counts(command_id, counts, env=None):
|
|
|
306
457
|
pass
|
|
307
458
|
|
|
308
459
|
|
|
309
|
-
def save_residue(command_id,
|
|
310
|
-
"""上限に達して終わらせた理由を残す(中継が終了の事実へ載せる)。
|
|
460
|
+
def save_residue(command_id, record, env=None):
|
|
461
|
+
"""上限に達して終わらせた理由を残す(中継が終了の事実へ載せる)。
|
|
462
|
+
|
|
463
|
+
record は `build_residue_record()` の辞書(人向けの1行+機械が読む形)。
|
|
464
|
+
★文字列を渡したときは人向けの1行だけとして扱う(古い呼び方も通る)。
|
|
465
|
+
"""
|
|
466
|
+
if isinstance(record, str):
|
|
467
|
+
record = {'text': record}
|
|
468
|
+
text = str(record.get('text') or '').strip()[:500]
|
|
469
|
+
out = {'text': text}
|
|
470
|
+
if isinstance(record.get('reasons'), list):
|
|
471
|
+
out['reasons'] = [k for k in record['reasons'] if k in GATE_KINDS]
|
|
472
|
+
if isinstance(record.get('counts'), dict):
|
|
473
|
+
out['counts'] = {k: int(record['counts'].get(k, 0)) for k in GATE_KINDS}
|
|
311
474
|
p = residue_path(command_id, env)
|
|
312
475
|
try:
|
|
313
476
|
os.makedirs(os.path.dirname(p), mode=0o700, exist_ok=True)
|
|
314
477
|
with open(p, 'w', encoding='utf-8') as f:
|
|
315
|
-
|
|
478
|
+
json.dump(out, f, ensure_ascii=False)
|
|
316
479
|
except Exception: # noqa: BLE001
|
|
317
480
|
return False
|
|
318
481
|
return True
|
|
@@ -396,3 +559,20 @@ def build_reason(result):
|
|
|
396
559
|
def build_residue_text(result):
|
|
397
560
|
"""上限に達して終わらせたときに控えへ残す1行(管制へ「途中で終わった」として届く)。"""
|
|
398
561
|
return '途中で終わった(便の終わりの関門の上限): ' + ' / '.join(result['reasons'])
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def build_residue_record(result):
|
|
565
|
+
"""控えに残す中身(人向けの1行+★管制が機械で読む形)。★純粋関数。
|
|
566
|
+
|
|
567
|
+
【なぜ機械で読む形も要るか(便 461be339)】上限に達して終わらせた便は、これまで
|
|
568
|
+
人向けの自由な文(`jobEndDetail`)にしか理由が乗らなかった。管制では「終わった」と
|
|
569
|
+
「途中で終わった」を機械で見分けられない(実地 7aa63a97 の残った穴)。
|
|
570
|
+
★新しい状態は作らず、終了の事実に `finishGate` の欄を足して届ける。
|
|
571
|
+
|
|
572
|
+
返り: {'text': 人向けの1行, 'reasons': 残っていた種類, 'counts': 止めた回数}
|
|
573
|
+
"""
|
|
574
|
+
return {
|
|
575
|
+
'text': build_residue_text(result),
|
|
576
|
+
'reasons': [k for k in GATE_KINDS if k in result['kinds']],
|
|
577
|
+
'counts': {k: int(result['counts'].get(k, 0)) for k in GATE_KINDS},
|
|
578
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_note": [
|
|
3
|
+
"★報告の「形」の決まりの正本。ここが1か所で、他に同じ決まりを書かない(便 461be339)。",
|
|
4
|
+
" 読む側は2つ。",
|
|
5
|
+
" ① 中継(src/relay.ts の parseReport / missingOf)… 報告を「届いた」と数える条件。",
|
|
6
|
+
" ★relay.ts の REQUIRED_REPORT_FIELDS / REPORT_OUTCOMES がこの値と同じであることを",
|
|
7
|
+
" test/report-shape-gate.test.ts が固定する(食い違えば CI が赤くなる)。",
|
|
8
|
+
" ② 便の終わりの関門(scripts/hooks/finish_gate.py)… 手番を閉じさせるかの判定。",
|
|
9
|
+
" ★この JSON を実行時に読む(同じフォルダに配られる)。読めなければ形の判定はしない。",
|
|
10
|
+
" ★cases は「同じ字面に同じ答えを出す」ことの答え合わせの正本。①②の両方の試験が流す。",
|
|
11
|
+
"★本文の言葉の印(「作成中」「TODO」など下書きの印)はここに置かない。",
|
|
12
|
+
" 報告の中身の言葉で完了を判定しない(えふさん確定 2026-09-23)ので、中継からは",
|
|
13
|
+
" 言葉の決まりに手が届かない形にしておく。下書きの印は関門だけの決まりで、",
|
|
14
|
+
" 正本は scripts/hooks/finish_gate.py の DRAFT_MARKERS。"
|
|
15
|
+
],
|
|
16
|
+
"version": 1,
|
|
17
|
+
"_requiredFields_note": "★並び順まで中継の missingOf と同じにする(答え合わせを順序ごと比べる)。",
|
|
18
|
+
"requiredFields": [
|
|
19
|
+
"instruction_id",
|
|
20
|
+
"site",
|
|
21
|
+
"outcome",
|
|
22
|
+
"created_at",
|
|
23
|
+
"evidence",
|
|
24
|
+
"body"
|
|
25
|
+
],
|
|
26
|
+
"validOutcomes": [
|
|
27
|
+
"pushed",
|
|
28
|
+
"no_change",
|
|
29
|
+
"blocked"
|
|
30
|
+
],
|
|
31
|
+
"_cases_note": "text をこの決まりで読んだとき、足りない物が missing と1つも違わないこと。",
|
|
32
|
+
"cases": [
|
|
33
|
+
{
|
|
34
|
+
"name": "★最終版(これが基本の形)",
|
|
35
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
36
|
+
"missing": []
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "no_change でも通る",
|
|
40
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: no_change\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
41
|
+
"missing": []
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "blocked でも通る",
|
|
45
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: blocked\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
46
|
+
"missing": []
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "空",
|
|
50
|
+
"text": "",
|
|
51
|
+
"missing": [
|
|
52
|
+
"instruction_id",
|
|
53
|
+
"site",
|
|
54
|
+
"outcome",
|
|
55
|
+
"created_at",
|
|
56
|
+
"evidence",
|
|
57
|
+
"body"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "空白と改行だけ",
|
|
62
|
+
"text": " \n \n",
|
|
63
|
+
"missing": [
|
|
64
|
+
"instruction_id",
|
|
65
|
+
"site",
|
|
66
|
+
"outcome",
|
|
67
|
+
"created_at",
|
|
68
|
+
"evidence",
|
|
69
|
+
"body"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "囲みが無い(古い自由形式)",
|
|
74
|
+
"text": "作業しました。\n直したファイル: src/relay.ts\n",
|
|
75
|
+
"missing": [
|
|
76
|
+
"instruction_id",
|
|
77
|
+
"site",
|
|
78
|
+
"outcome",
|
|
79
|
+
"created_at",
|
|
80
|
+
"evidence"
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "★囲みが閉じていない(書きかけで落ちた形)",
|
|
85
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n\nやりました。",
|
|
86
|
+
"missing": [
|
|
87
|
+
"instruction_id",
|
|
88
|
+
"site",
|
|
89
|
+
"outcome",
|
|
90
|
+
"created_at",
|
|
91
|
+
"evidence"
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "★囲みだけ・本文が空(実地 47bdfe00 の形)",
|
|
96
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\n",
|
|
97
|
+
"missing": [
|
|
98
|
+
"body"
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "本文が空白だけ",
|
|
103
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\n \n ",
|
|
104
|
+
"missing": [
|
|
105
|
+
"body"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "instruction_id の欄が無い",
|
|
110
|
+
"text": "---\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
111
|
+
"missing": [
|
|
112
|
+
"instruction_id"
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "site が空",
|
|
117
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: \noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
118
|
+
"missing": [
|
|
119
|
+
"site"
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"name": "outcome が決まった語でない",
|
|
124
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: やりました\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
125
|
+
"missing": [
|
|
126
|
+
"outcome"
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "outcome が空",
|
|
131
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: \ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
132
|
+
"missing": [
|
|
133
|
+
"outcome"
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"name": "created_at の欄が無い",
|
|
138
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
139
|
+
"missing": [
|
|
140
|
+
"created_at"
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "evidence の欄が在るが1件も無い",
|
|
145
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n---\n\nやりました。",
|
|
146
|
+
"missing": [
|
|
147
|
+
"evidence"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "evidence の欄そのものが無い",
|
|
152
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\n---\n\nやりました。",
|
|
153
|
+
"missing": [
|
|
154
|
+
"evidence"
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "evidence が同じ行に書かれている(1件と数える)",
|
|
159
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence: PR https://example.invalid/1\n---\n\nやりました。",
|
|
160
|
+
"missing": []
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "囲みの中も本文も空",
|
|
164
|
+
"text": "---\n---\n",
|
|
165
|
+
"missing": [
|
|
166
|
+
"instruction_id",
|
|
167
|
+
"site",
|
|
168
|
+
"outcome",
|
|
169
|
+
"created_at",
|
|
170
|
+
"evidence",
|
|
171
|
+
"body"
|
|
172
|
+
]
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"name": "★箇条書きが evidence の下でなければ evidence と数えない",
|
|
176
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nnote:\n - これは証拠ではない\n---\n\nやりました。",
|
|
177
|
+
"missing": [
|
|
178
|
+
"evidence"
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"name": "知らない欄が混じっていても通る",
|
|
183
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\nbranch: fix/x\npr_number: 256\n---\n\nやりました。",
|
|
184
|
+
"missing": []
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "欄の名前が大文字でも通る",
|
|
188
|
+
"text": "---\nInstruction_Id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
189
|
+
"missing": []
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"name": "囲みの前に空行があっても通る",
|
|
193
|
+
"text": "\n\n---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
194
|
+
"missing": []
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"name": "先頭に BOM があっても通る",
|
|
198
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\nやりました。",
|
|
199
|
+
"missing": []
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "★本文に下書きの言葉が残っていても形としては通る(中継は本文の言葉を読まない)",
|
|
203
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\n# 便 915694ac 報告(作成中)\n\nこの見出しは最終版で差し替える。",
|
|
204
|
+
"missing": []
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"name": "★続きの報告(詳しい中身を別の記録へ分けた形)も通る",
|
|
208
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n---\n\n結論だけ書きます。詳しい中身は開発履歴の「7aa63a97 報告 1/3」〜「3/3」。",
|
|
209
|
+
"missing": []
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"name": "読めない行が混じっていても欄の解釈は壊れない",
|
|
213
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\n(この行は欄ではない)\n====\n---\n\nやりました。",
|
|
214
|
+
"missing": []
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"name": "「- 」だけの箇条書きは evidence と数えない",
|
|
218
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - \n---\n\nやりました。",
|
|
219
|
+
"missing": [
|
|
220
|
+
"evidence"
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"name": "evidence の箇条書きがタブ区切りでも1件と数える",
|
|
225
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: pushed\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n\t-\tPR https://example.invalid/2\n---\n\nやりました。",
|
|
226
|
+
"missing": []
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"name": "同じ欄が2回あれば後が勝つ",
|
|
230
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\nsite: demo\noutcome: やりました\ncreated_at: 2026-10-03T09:00:00+09:00\nevidence:\n - PR https://example.invalid/1\noutcome: pushed\n---\n\nやりました。",
|
|
231
|
+
"missing": []
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"name": "欄が足りず本文も空(複数同時)",
|
|
235
|
+
"text": "---\ninstruction_id: 7aa63a97-57e7-4c42-8b95-f3da87d88829\noutcome: pushed\nevidence:\n - PR https://example.invalid/1\n---\n\n",
|
|
236
|
+
"missing": [
|
|
237
|
+
"site",
|
|
238
|
+
"created_at",
|
|
239
|
+
"body"
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
}
|
|
@@ -307,7 +307,7 @@ def finish_gate_block(data, env=None):
|
|
|
307
307
|
return finish_gate.build_reason(result)
|
|
308
308
|
if result['kinds']:
|
|
309
309
|
# 上限に達した=これ以上止めない。理由だけ残して終わらせる。
|
|
310
|
-
finish_gate.save_residue(command_id, finish_gate.
|
|
310
|
+
finish_gate.save_residue(command_id, finish_gate.build_residue_record(result), env)
|
|
311
311
|
return None
|
|
312
312
|
|
|
313
313
|
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
【ここで固定すること】
|
|
12
12
|
(A) 報告の判定 … 無い・空・囲みが無い・欄が足りない・outcome が変・evidence 0件・
|
|
13
13
|
本文が空・下書きの印が残っている → 残り。最終版 → 残り無し
|
|
14
|
+
(A2) ★下書きの印を見る場所(便 ec974d68) … 囲みのすぐ下の最初の見出しの行・その直後の
|
|
15
|
+
1段落・evidence の各行だけ。実地 915694ac の下書きは止まり、関門そのものを説明した
|
|
16
|
+
整った報告は止まらない(本文の全体を見ていたため誤って差し戻していた=便 461be339)
|
|
14
17
|
(B) 背景の待ちの数え方 … 始まった印(backgroundTaskId)と知らせ(task-notification)の差。
|
|
15
18
|
★実地 915694ac・47bdfe00 と同じ形の記録で確かめる
|
|
16
19
|
(C) PR の判定 … 開いたまま/合流の予約中 → 残り。空・合流済み → 残り無し。
|
|
@@ -23,6 +26,8 @@
|
|
|
23
26
|
★空振り確認(隔離した worktree の中だけ):
|
|
24
27
|
・finish_gate.decide の上限の判定(`counts[k] < cap`)を外す → (D) が落ちる
|
|
25
28
|
・report_residue の下書きの印の判定を外す → (A) が落ちる
|
|
29
|
+
・report_draft_places を空の一覧にする(どこも見ない) → (A2) の「実地の下書きを止める」が落ちる
|
|
30
|
+
・report_draft_places を本文の全体に戻す → (A2) の「説明した報告を止めない」が落ちる
|
|
26
31
|
・stop_hook.main から関門の呼び出しを外す → (G) の「止める」が落ちる
|
|
27
32
|
"""
|
|
28
33
|
import json
|
|
@@ -58,10 +63,13 @@ ENV = {'HOME': SANDBOX, 'XDG_STATE_HOME': os.path.join(SANDBOX, 'state'),
|
|
|
58
63
|
JOB = '7aa63a97-57e7-4c42-8b95-f3da87d88829'
|
|
59
64
|
|
|
60
65
|
|
|
61
|
-
def report_text(outcome='pushed', body='やりました。', header=True, evidence=True
|
|
66
|
+
def report_text(outcome='pushed', body='やりました。', header=True, evidence=True,
|
|
67
|
+
evidence_lines=None):
|
|
62
68
|
head = ['---', f'instruction_id: {JOB}', 'site: demo', f'outcome: {outcome}',
|
|
63
69
|
'created_at: 2026-10-03T09:00:00+09:00', 'evidence:']
|
|
64
|
-
if
|
|
70
|
+
if evidence_lines is not None:
|
|
71
|
+
head.extend(f' - {line}' for line in evidence_lines)
|
|
72
|
+
elif evidence:
|
|
65
73
|
head.append(' - PR https://github.com/tyhld/conductor/pull/999')
|
|
66
74
|
head.append('---')
|
|
67
75
|
return ('\n'.join(head) + '\n\n' + body) if header else body
|
|
@@ -124,6 +132,89 @@ truthy('実地の下書き → 残り', finish_gate.report_residue(report_text(b
|
|
|
124
132
|
for m in ('作成中', '下書き', '差し替える', 'あとで書く', '(未記入)', 'TODO', 'TBD'):
|
|
125
133
|
truthy(f'下書きの印「{m}」→ 残り', finish_gate.report_residue(report_text(body=f'あ{m}い')))
|
|
126
134
|
|
|
135
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
136
|
+
print('\n--- ★(A2) 下書きの印を見る場所(頭と evidence だけ・便 ec974d68) ---')
|
|
137
|
+
# 【なぜ要るか(実地・便 461be339)】本文の全体を見ていたので、★関門そのものを説明した
|
|
138
|
+
# 整った報告が、説明の中の語に当たって差し戻された(言い回しを替えて通した=3手番の無駄)。
|
|
139
|
+
# 見る場所を「囲みのすぐ下の最初の見出しの行・その直後の1段落・evidence の各行」に絞る。
|
|
140
|
+
# ★印の一覧(DRAFT_MARKERS)は1語も減らしていない=塞ぎたい物は塞いだまま。
|
|
141
|
+
# 【書き方】report-shape.json の cases と同じ流儀(name / text / 期待)で並べる。
|
|
142
|
+
# ★ただし言葉の決まりは正本(report-shape.json)に置かない(中継から手が届かない形を保つ)。
|
|
143
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
144
|
+
#: 実地 915694ac の報告の頭(囲みの evidence の行まで含めて、当時の実物のまま)。
|
|
145
|
+
REAL_DRAFT_EVIDENCE = '(CI・合流・公開版は下の「合流と公開」節に実物を書く)'
|
|
146
|
+
|
|
147
|
+
#: 関門を説明した「整った報告」の本文(★この便の報告そのものに近い形)。
|
|
148
|
+
GATE_EXPLAINED = '\n'.join([
|
|
149
|
+
'# 結論',
|
|
150
|
+
'',
|
|
151
|
+
'便の終わりの関門が、仕上げ前の印を見る場所を狭めました。',
|
|
152
|
+
'',
|
|
153
|
+
'## 狭めた後に見る場所',
|
|
154
|
+
'',
|
|
155
|
+
'囲みのすぐ下の最初の見出しの行と、その直後の1段落、evidence の各行だけです。',
|
|
156
|
+
'以前は本文の全体を見ていたので、印の語(作成中・下書き・差し替える・あとで書く・TODO・TBD)を',
|
|
157
|
+
'この表のように並べて説明した報告が、そのまま差し戻されました。',
|
|
158
|
+
])
|
|
159
|
+
|
|
160
|
+
DRAFT_CASES = [
|
|
161
|
+
{
|
|
162
|
+
'name': '★実地の下書き(見出しに印・次の行にも印)→ 止める',
|
|
163
|
+
'text': report_text(body=REAL_DRAFT, evidence=True),
|
|
164
|
+
'draft': True,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
'name': '★関門を説明した整った報告 → 止めない(これが直した本体)',
|
|
168
|
+
'text': report_text(body=GATE_EXPLAINED),
|
|
169
|
+
'draft': False,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
'name': '印が evidence の行に在る → 止める',
|
|
173
|
+
'text': report_text(body='# 結論\n\nやりました。',
|
|
174
|
+
evidence_lines=['CI の結果はあとで書く']),
|
|
175
|
+
'draft': True,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
'name': '印が見出しの直後の1段落に在る → 止める',
|
|
179
|
+
'text': report_text(body='# 結論\n\nここは作成中です。'),
|
|
180
|
+
'draft': True,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
'name': '見出しが無く最初の段落に印 → 止める(頭は段落から始まる)',
|
|
184
|
+
'text': report_text(body='まだ下書きです。\n\n# 結論\n\nやりました。'),
|
|
185
|
+
'draft': True,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
'name': '見出しの前に空行が在っても頭として見る → 止める',
|
|
189
|
+
'text': report_text(body='\n\n# 報告(作成中)\n\nやりました。'),
|
|
190
|
+
'draft': True,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
'name': '★頭より後ろの節の中の語は見ない → 止めない(狭めた結果・正直に書く)',
|
|
194
|
+
'text': report_text(body='# 結論\n\nやりました。\n\n## 補足\n\n印の語は TODO などです。'),
|
|
195
|
+
'draft': False,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
'name': '頭も evidence も印が無い最終版 → 止めない',
|
|
199
|
+
'text': report_text(body='# 結論\n\nやりました。'),
|
|
200
|
+
'draft': False,
|
|
201
|
+
},
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
for case in DRAFT_CASES:
|
|
205
|
+
check(case['name'][:64], finish_gate.report_residue(case['text']) is not None, case['draft'])
|
|
206
|
+
|
|
207
|
+
# 実地 915694ac の evidence の行は、狭めた後の「見る場所」に入っている(=見落とさない)。
|
|
208
|
+
truthy('実地 915694ac の evidence の行が見る場所に入る',
|
|
209
|
+
REAL_DRAFT_EVIDENCE in finish_gate.report_draft_places(
|
|
210
|
+
report_text(body=REAL_DRAFT, evidence_lines=[REAL_DRAFT_EVIDENCE])))
|
|
211
|
+
# ★見る場所は3種類だけ=頭より後ろの行は1行も入らない。
|
|
212
|
+
places = finish_gate.report_draft_places(
|
|
213
|
+
report_text(body='# 結論\n\nやりました。\n\n## 補足\n\nここは TODO と書いてある。'))
|
|
214
|
+
check('見る場所は evidence と頭だけ(後ろの節は入らない)',
|
|
215
|
+
any('TODO' in p for p in places), False)
|
|
216
|
+
check('見る場所に見出しの行が入る', any(p.startswith('# 結論') for p in places), True)
|
|
217
|
+
|
|
127
218
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
128
219
|
print('\n=== (B) 背景の待ちの数え方(★実地と同じ形の記録) ===')
|
|
129
220
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -194,6 +285,16 @@ check('★上限に達したら止めない(便が終わる)', d['block'], F
|
|
|
194
285
|
check('★ただし「途中で終わった」と分かる(種類が残る)', d['kinds'], ['report'])
|
|
195
286
|
truthy('控えの文に理由が入る', '途中で終わった' in finish_gate.build_residue_text(d))
|
|
196
287
|
|
|
288
|
+
print('--- ★控えは「管制が機械で読める形」でも残す(便 461be339) ---')
|
|
289
|
+
rec = finish_gate.build_residue_record(d)
|
|
290
|
+
truthy('人向けの1行は今までどおり', '途中で終わった' in rec['text'])
|
|
291
|
+
check('★残っていた種類が入る', rec['reasons'], ['report'])
|
|
292
|
+
check('★止めた回数が3種類ぶん入る', rec['counts'], {'report': 3, 'pr': 0, 'background': 0})
|
|
293
|
+
rec3 = finish_gate.build_residue_record(
|
|
294
|
+
finish_gate.decide({'report': 'a', 'pr': 'b', 'background': 'c'},
|
|
295
|
+
{'report': 3, 'pr': 3, 'background': 3}, cap=3))
|
|
296
|
+
check('3つ残ったまま終わったら3つとも入る', rec3['reasons'], ['report', 'pr', 'background'])
|
|
297
|
+
|
|
197
298
|
check('残りが無ければ止めない', finish_gate.decide({}, {}, cap=3)['block'], False)
|
|
198
299
|
check('残りが無ければ種類も空', finish_gate.decide({}, {'report': 1}, cap=3)['kinds'], [])
|
|
199
300
|
|
|
@@ -291,7 +392,10 @@ check('その後は止めない',
|
|
|
291
392
|
rp = finish_gate.residue_path(JOB, ENV)
|
|
292
393
|
truthy('理由の控えが残る', os.path.isfile(rp))
|
|
293
394
|
with open(rp, encoding='utf-8') as f:
|
|
294
|
-
|
|
395
|
+
got = json.load(f)
|
|
396
|
+
truthy('控えに「途中で終わった」と入っている', '途中で終わった' in got['text'])
|
|
397
|
+
check('★控えの種類を中継が機械で読める', got.get('reasons'), ['report'])
|
|
398
|
+
check('★控えの回数は上限ちょうど', (got.get('counts') or {}).get('report'), finish_gate.CAP_PER_KIND)
|
|
295
399
|
|
|
296
400
|
print('--- ★便の本文が「合流は人が行う」なら PR を見ない ---')
|
|
297
401
|
clear_counts()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""報告の「形」の判定が、決まりの正本どおりであることの検査(便 461be339)。
|
|
3
|
+
|
|
4
|
+
実行: python3 scripts/hooks/test_report_shape.py
|
|
5
|
+
1件でも期待と違えば exit 1。
|
|
6
|
+
|
|
7
|
+
【なぜ要るか】形の決まりは2か所から読まれる。
|
|
8
|
+
① 中継(`src/relay.ts`)… 報告を「届いた」と数える条件
|
|
9
|
+
② 便の終わりの関門(`finish_gate.py`)… 手番を閉じさせるかの判定
|
|
10
|
+
同じ決まりを2か所に書くと、片方だけ直して食い違う(=関門は差し戻すのに中継は届いたと数える、
|
|
11
|
+
またはその逆)。だから決まりは `report-shape.json` の1か所だけに置き、
|
|
12
|
+
**同じ字面に同じ答えを出すこと**を両方の試験で固定する。
|
|
13
|
+
★中継の側は `test/report-shape-gate.test.ts` が、この同じ `cases` を流して突き合わせる。
|
|
14
|
+
|
|
15
|
+
【ここで固定すること】
|
|
16
|
+
(A) 正本が読めること(配り忘れ・壊れた JSON に気づける)
|
|
17
|
+
(B) 正本の `cases` の答えと、関門の判定が1件も違わないこと
|
|
18
|
+
(C) 正本が読めないときは形の判定をしない(fail open=関門が足される前と同じ動き)
|
|
19
|
+
(D) ★下書きの印(本文の言葉)は正本に載せない
|
|
20
|
+
=中継からは言葉の決まりに手が届かない(えふさん確定 2026-09-23)
|
|
21
|
+
(E) 形と下書きの印は別の理由として返る
|
|
22
|
+
|
|
23
|
+
★空振り確認(隔離した worktree の中だけ):
|
|
24
|
+
・`report-shape.json` の `requiredFields` から1つ消す → (B) が落ちる
|
|
25
|
+
・`report_shape_missing` の outcome の確かめを外す → (B) が落ちる
|
|
26
|
+
"""
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
32
|
+
|
|
33
|
+
import finish_gate # noqa: E402
|
|
34
|
+
|
|
35
|
+
failures = []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def check(label, got, want):
|
|
39
|
+
ok = got == want
|
|
40
|
+
if not ok:
|
|
41
|
+
failures.append(f'{label}: got={got!r} want={want!r}')
|
|
42
|
+
print(f'{"OK " if ok else "NG "} {label:70} {str(got)[:40]}')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def truthy(label, got):
|
|
46
|
+
ok = bool(got)
|
|
47
|
+
if not ok:
|
|
48
|
+
failures.append(f'{label}: got={got!r} want=真')
|
|
49
|
+
print(f'{"OK " if ok else "NG "} {label:70} {str(got)[:40]}')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
print('=== (A) 形の決まりの正本が読めるか ===')
|
|
54
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
path = finish_gate.shape_spec_path()
|
|
56
|
+
check('正本の置き場は自分と同じフォルダ', os.path.basename(path), 'report-shape.json')
|
|
57
|
+
truthy('正本が在る', os.path.isfile(path))
|
|
58
|
+
spec = finish_gate.load_shape_spec()
|
|
59
|
+
truthy('正本が読める', spec)
|
|
60
|
+
check('★関門は読み込み済みの正本を使う', finish_gate.SHAPE_SPEC is not None, True)
|
|
61
|
+
check('足りない物の並び順まで決まっている',
|
|
62
|
+
spec['requiredFields'],
|
|
63
|
+
['instruction_id', 'site', 'outcome', 'created_at', 'evidence', 'body'])
|
|
64
|
+
check('outcome に書ける語', spec['validOutcomes'], ['pushed', 'no_change', 'blocked'])
|
|
65
|
+
truthy('答え合わせの cases が在る', isinstance(spec.get('cases'), list) and spec['cases'])
|
|
66
|
+
|
|
67
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
print(f'\n=== (B) 正本の cases({len(spec["cases"])}件)の答えと1件も違わない ===')
|
|
69
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
for case in spec['cases']:
|
|
71
|
+
check(case['name'][:68], finish_gate.report_shape_missing(case['text']), case['missing'])
|
|
72
|
+
|
|
73
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
print('\n=== (C) 正本が読めないときは形の判定をしない(退行しない) ===')
|
|
75
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
check('正本が無い → 足りない物は並べない', finish_gate.report_shape_missing('やりました。', {}), [])
|
|
77
|
+
check('正本が壊れていても落ちない', finish_gate.load_shape_spec('/nope/report-shape.json'), None)
|
|
78
|
+
check('requiredFields が無い正本は使わない',
|
|
79
|
+
finish_gate.load_shape_spec(os.path.join(os.path.dirname(path), 'safe_verbs.json')), None)
|
|
80
|
+
good = spec['cases'][0]['text']
|
|
81
|
+
check('★正本が読めなくても「報告が無い」は今までどおり止める',
|
|
82
|
+
bool(finish_gate.report_residue(None, {})), True)
|
|
83
|
+
check('★正本が読めないときは形で止めない(fail open)',
|
|
84
|
+
finish_gate.report_residue(good, {}), None)
|
|
85
|
+
|
|
86
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
print('\n=== (D) ★下書きの印(本文の言葉)は正本に載せない ===')
|
|
88
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
raw = open(path, encoding='utf-8').read()
|
|
90
|
+
body_rules = json.loads(raw)
|
|
91
|
+
for marker in ('作成中', '下書き', '差し替える', 'あとで書く', 'TODO', 'TBD'):
|
|
92
|
+
check(f'正本の決まりに「{marker}」が入っていない',
|
|
93
|
+
any(marker in json.dumps(body_rules.get(k), ensure_ascii=False)
|
|
94
|
+
for k in ('requiredFields', 'validOutcomes')),
|
|
95
|
+
False)
|
|
96
|
+
truthy('下書きの印の正本は関門の側にある', finish_gate.DRAFT_MARKERS)
|
|
97
|
+
|
|
98
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
print('\n=== (E) 形と下書きの印は別の理由として返る ===')
|
|
100
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
draft = next(c for c in spec['cases'] if '下書きの言葉' in c['name'])
|
|
102
|
+
check('★下書きの言葉が残った報告は、形としては整っている',
|
|
103
|
+
finish_gate.report_shape_missing(draft['text']), [])
|
|
104
|
+
r = finish_gate.report_residue(draft['text'])
|
|
105
|
+
truthy('それでも関門は下書きとして差し戻す', r)
|
|
106
|
+
truthy(' 理由は「下書き」だと分かる', '下書き' in (r or ''))
|
|
107
|
+
broken = next(c for c in spec['cases'] if c['missing'] == ['body'])
|
|
108
|
+
r2 = finish_gate.report_residue(broken['text'])
|
|
109
|
+
truthy('形が整っていない報告は「形」の理由で差し戻す', r2)
|
|
110
|
+
truthy(' 理由は「形が整っていない」だと分かる', '形が整っていない' in (r2 or ''))
|
|
111
|
+
truthy(' 足りない物の名前が入る', '本文' in (r2 or ''))
|
|
112
|
+
check('★最終版は差し戻さない', finish_gate.report_residue(good), None)
|
|
113
|
+
check('本文の取り出しは囲みの下だけ', finish_gate.report_body(good).startswith('やりました'), True)
|
|
114
|
+
check('囲みが無ければ全文が本文', finish_gate.report_body('ただの文'), 'ただの文')
|
|
115
|
+
|
|
116
|
+
print('\n' + '=' * 72)
|
|
117
|
+
if failures:
|
|
118
|
+
print(f'NG {len(failures)} 件')
|
|
119
|
+
for f in failures:
|
|
120
|
+
print(' ' + f)
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
print('すべて期待どおり')
|
package/scripts/update-guard.sh
CHANGED
|
@@ -438,7 +438,11 @@ INSTALL_FILES=(
|
|
|
438
438
|
hook_datalink.py # 各フックが使う管制との連絡線
|
|
439
439
|
finish_gate.py # stop_hook が使う「便の終わりの関門」の判定(便 7aa63a97)
|
|
440
440
|
)
|
|
441
|
-
|
|
441
|
+
# ★番人と同居させる設定(任意・あれば一緒に置く)。この一覧は sales-template/setup.sh の
|
|
442
|
+
# GUARD_CONFIGS と【同じ集合】にすること(test/setup-installer.test.ts が同一を固定する)。
|
|
443
|
+
# report-shape.json … 報告の「形」の決まりの正本(finish_gate.py が実行時に読む・便 461be339)。
|
|
444
|
+
# 配り忘れると関門が形を見られない(fail open=書きかけを差し戻せない)。
|
|
445
|
+
INSTALL_CONFIGS=(safe_verbs.json network_allow.json report-shape.json)
|
|
442
446
|
ENTRYPOINTS=(cc2_guard.py permission_request_hook.py stop_hook.py notification_hook.py)
|
|
443
447
|
|
|
444
448
|
# ①-0 見本と「配る中身」がズレていないか機械照合する(今回と同じ欠落事故の再発防止=根治)。
|
|
@@ -29,7 +29,8 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
|
|
|
29
29
|
4. **壁に当たったら止まる** — サンドボックス制約・保護されたパスに当たったら回避せず中断して報告する(★通信で塞がれたときの読み方は鉄則12)。
|
|
30
30
|
5. **エージェント設定に書き込まない** — `.mcp.json`、`.claude/` 配下、hooks 等のエージェント設定パスには書き込まない。
|
|
31
31
|
6. **本番に触らない** — 本番DB・deploy・secret に触らない。SQL/DDL は作って提示のみ(適用は人間)。
|
|
32
|
-
7. **便の終点はマージまで(★保護を迂回しない)** — push → PR作成 →
|
|
32
|
+
7. **便の終点はマージまで(★保護を迂回しない)** — push → PR作成 → 合流する。★push は**上流の設定を書かない形**で打つ(⓪)。★合流の仕方は、そのリポの合流先に**保護(必須の検査)**があるかで分ける。リポの名前は覚えなくてよい。
|
|
33
|
+
- ⓪ **上流の設定を書かない形で push する**=`git push origin <作業ブランチ名>`。★`-u`(`--set-upstream`)を付けない。`-u` と `git checkout -b <枝> origin/main` は `.git/config` へ書き込むので、囲いがその書き込みを塞いでいるPCでは「上流の設定を書けない」で止まる(**push 自体は成功している**ので、打ち直すと食い違って見える=実測 便 2cb1adf6・ef8def38)。枝を切るときも同じで `git switch -c <枝> --no-track origin/main`(または `git branch --no-track <枝> origin/main` のあと `git switch <枝>`)。★これは回避の近道ではなく**どのPCでも同じに通る形**なので、塞がれていないPCでもこの形で打つ(上流の設定は職人の作業に要らない)。
|
|
33
34
|
- ① **保護があるかを読む。** 合流先は `gh pr view <番号> --json baseRefName -q .baseRefName`(ふつうは main)。`gh api 'repos/{owner}/{repo}/branches/<合流先>/protection/required_status_checks' --jq '.contexts | length'` が **1 以上の数を返したときだけ「保護あり」**。0・404(Branch not protected)・403・その他の失敗は、すべて**「保護なし」**として扱う。
|
|
34
35
|
- ② **保護あり** … `gh pr merge <番号> --auto --merge --delete-branch` で**合流を予約する**。必須の検査が緑になった後に GitHub が合流する(赤のままなら合流されない)。
|
|
35
36
|
- 「auto-merge が使えない」と断られたときは、`gh pr checks <番号>` で必須の検査が pass なのを確かめてから `gh pr merge <番号> --merge --delete-branch`。
|
|
@@ -163,7 +164,7 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
|
|
|
163
164
|
- 通る例: `rm -r /tmp/claude-1000/作業名` / `rm -r node_modules/.tmp-作業名`(★`-f` を付けない・自分で作った物だけ)
|
|
164
165
|
- 止まる例: `rm -rf /tmp/claude-1000/作業名`(禁止の一覧)/ `rm -r "$DIR/x"`・`rm -r build/*`(番人)/ `rm -r .git`・`rm -r .claude/hooks`・`rm -r .tyhld`(番人=相対パスでも消せない)
|
|
165
166
|
- ★**探すだけの道具の「探す言葉」に `rm …` と書いても、もう止まらない**(便 915694ac で直した)。鉄則や台本を調べるときの `grep -rn 'rm -rf .git' skills`・`git grep -n 'rm -r' -- skills`・`rg 'rm -rf /' docs` は通る。★囲みの外に書いた `rm` は今までどおり測るので、`grep -rn 'x' docs; rm -rf .git` のように**後ろで消す形は止まる**。
|
|
166
|
-
- **作業場所が変数のままの `git push`**(`git -C "$DIR" push`・行き先のブランチを確かめられない形)→ 止まる。**リポの中で、ブランチを名指しして** `git push
|
|
167
|
+
- **作業場所が変数のままの `git push`**(`git -C "$DIR" push`・行き先のブランチを確かめられない形)→ 止まる。**リポの中で、ブランチを名指しして** `git push origin <作業ブランチ名>`(★`-u` は付けない=鉄則7-⓪。上流の設定を書くと、囲いが `.git/config` を塞いでいるPCで止まる)。
|
|
167
168
|
|
|
168
169
|
## 便の終え方
|
|
169
170
|
|