@tyhld/conductor 0.12.39 → 0.12.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/relay.js CHANGED
@@ -970,7 +970,9 @@ export function selectOrphanToRestore(orphans, currentPending) {
970
970
  }
971
971
  /**
972
972
  * 実際に tmux へ流し込む1行テキストを組み立てる純粋関数。
973
- * 送る本文 = 指示body + 報告マーカー指示 + 完了マーカー指示。
973
+ * 送る本文 = 指示body + 便の終え方(Claude=1行 shortCompletionLine/Codex=全文 fullCompletionGuide)+ アンカー。
974
+ * ★Claude の職人は鉄則を会話ごとに1回だけ読む(実測・便 a5306df2:便1〜30本の会話12件すべてで1回)。
975
+ * 毎便同じ書き方の説明は鉄則の「便の終え方」へ寄せ、便には便ごとに変わる値の1行だけを付ける。
974
976
  * CC2 の途中送信を防ぐため、改行はすべて空白に潰して「1行=1メッセージ」にする
975
977
  * (本文内に生 Enter を含めない=送信は末尾の Enter 1回だけにするため)。
976
978
  *
@@ -986,25 +988,59 @@ export function selectOrphanToRestore(orphans, currentPending) {
986
988
  * 画面では送信エコーの末尾に出る=完了検出(hasDoneBelowAnchor)の境界になる。
987
989
  * 末尾に置く理由: アンカーより上に来る指示文中の "DONE:{site}" を完了検出から確実に除外するため。
988
990
  */
989
- export function buildSendText(body, site, anchor) {
990
- const marker = `DONE:${site}`;
991
+ export function buildSendText(body, site, anchor, kind = 'claude') {
991
992
  // anchor から素の commandId を取り出す(conductor_complete に渡す引数。接頭辞無しなので新たな
992
993
  // アンカー一致を作らず、sliceBelowAnchor の境界判定に干渉しない)。
993
994
  const commandId = anchor.startsWith(`${SEND_ANCHOR_PREFIX}:`)
994
995
  ? anchor.slice(SEND_ANCHOR_PREFIX.length + 1)
995
996
  : anchor;
996
- const full = `${body}\n\n` +
997
- // ★確認カードを非エンジニアが読める日本語にする(調査 2987cb8f)。
998
- // Bash 等の確認に出る一言説明は、職人(モデル)が tool_input.description に自分で書く自由記入欄で、
999
- // Claude Code 組込みの記入例が英語のため既定では英語になる。ここで日本語を依頼しておく。
1000
- // ★段3の撤去後、この説明の行き先は2つ:
1001
- // (1) 職人の端末に出る確認ダイアログ(人がその場で読む)
1002
- // (2) PermissionRequest フックが管制へ渡す toolInput の中身(カードの「くわしく」に残る)
1003
- // =以前のように relay が画面から拾って中央のカード見出しにする経路(extractPermSummary →
1004
- // summary)は無い。カードの日本語見出しは summary_ja.py がコマンドから組み立てる。
1005
- // ★あくまで書き方の依頼。従わなくても英語の説明が入るだけで、見出しは summary_ja.py が
1006
- // 日本語で作る=壊れない。
1007
- `作業中にユーザー確認(許可を求めるプロンプト)が出る操作では、その一言説明(description)を` +
997
+ // ★Claude の職人には「便の終え方」の1行だけを付ける(書き方の正本は鉄則の「便の終え方」の節)。
998
+ // ★Codex の職人には今までどおり全文を付ける。鉄則(~/.claude/skills)は Codex には届かないため。
999
+ const guide = kind === 'codex' ? fullCompletionGuide(site, commandId) : shortCompletionLine(site, commandId);
1000
+ const full = `${body}\n\n${guide}` +
1001
+ // アンカー(管理用ジョブ印)は必ず本文の最後に置く。完了検出の境界として使う。
1002
+ `(管理用ジョブ印・変更不要: ${anchor})`;
1003
+ // 改行(と連続改行)を空白1個に潰し、前後を整える。
1004
+ return full.replace(/\r?\n+/g, ' ').replace(/\s+/g, ' ').trim();
1005
+ }
1006
+ /** 鉄則(conductor-craftsman の SKILL.md)の中の、便の終え方の見出し。1行の案内はここを指す。 */
1007
+ export const CRAFTSMAN_FINISH_HEADING = '## 便の終え方';
1008
+ /**
1009
+ * Claude の職人へ付ける「便の終え方」の1行(便 a5306df2・えふさん確定の重複削減)。
1010
+ *
1011
+ * 【なぜ短くしたか】以前は下の全文(約1,500字)を便ごとに毎回付けていた(棚卸し 8ceae57d)。
1012
+ * 書き方の説明は毎便同じなので、正本を鉄則の「便の終え方」の節へ寄せ、便には1行だけ残す。
1013
+ * 【★この1行だけは何があっても残す物】便ごとに変わる3つ=報告の置き場・最終行の合図・完了の知らせの
1014
+ * 指示ID。鉄則がまだ古い機体や、鉄則を読み込めなかった職人でも、この3つがあれば回収は途切れない。
1015
+ * 【★報告の置き場は1か所】置き場は配達役がこの機で決めた正本(reportWriteDir)だけを書く。
1016
+ * 鉄則には「便の本文に別の置き場が書かれていても、この行の置き場を正とする」と書いてある
1017
+ * (便 46205e8e で配達役=~/.local/state・CC1=/tmp の食い違いが実際に起きた)。
1018
+ */
1019
+ export function shortCompletionLine(site, commandId) {
1020
+ const reportPath = path.join(reportWriteDir(), reportFileName(commandId));
1021
+ return (`【便の終え方】鉄則(conductor-craftsman)の「便の終え方」のとおりに終える。` +
1022
+ `報告の置き場: ${reportPath}(先頭に6欄の囲み)/最終行: DONE:${site} とだけ出力/` +
1023
+ `完了の知らせ: conductor_complete を commandId="${commandId}" で1回だけ(nextWork は必ず書く)。`);
1024
+ }
1025
+ /**
1026
+ * 全文の完了案内(★Codex の職人向け・Claude 向けは shortCompletionLine)。
1027
+ * ★ここに書いてある決まりは、鉄則の「便の終え方」の節と中身を同じに保つ
1028
+ * (試験 relay-finish-guide-equivalence が1つずつ突き合わせる)。
1029
+ */
1030
+ export function fullCompletionGuide(site, commandId) {
1031
+ const marker = `DONE:${site}`;
1032
+ return (
1033
+ // ★確認カードを非エンジニアが読める日本語にする(調査 2987cb8f)。
1034
+ // Bash 等の確認に出る一言説明は、職人(モデル)が tool_input.description に自分で書く自由記入欄で、
1035
+ // Claude Code 組込みの記入例が英語のため既定では英語になる。ここで日本語を依頼しておく。
1036
+ // ★段3の撤去後、この説明の行き先は2つ:
1037
+ // (1) 職人の端末に出る確認ダイアログ(人がその場で読む)
1038
+ // (2) PermissionRequest フックが管制へ渡す toolInput の中身(カードの「くわしく」に残る)
1039
+ // =以前のように relay が画面から拾って中央のカード見出しにする経路(extractPermSummary →
1040
+ // summary)は無い。カードの日本語見出しは summary_ja.py がコマンドから組み立てる。
1041
+ // ★あくまで書き方の依頼。従わなくても英語の説明が入るだけで、見出しは summary_ja.py が
1042
+ // 日本語で作る=壊れない。
1043
+ `作業中にユーザー確認(許可を求めるプロンプト)が出る操作では、その一言説明(description)を` +
1008
1044
  `英語ではなく日本語で、「プルリクエストを作成する」「テストを実行する」のように` +
1009
1045
  `「何をするか」が一目で分かる簡潔な形(〜を作成する/実行する/削除する 等)で書いてください` +
1010
1046
  `(この説明は管制の確認カードにそのまま表示され、非エンジニアのお客さまが読んで許可を判断します)。\n\n` +
@@ -1041,11 +1077,9 @@ export function buildSendText(body, site, anchor) {
1041
1077
  `prNumber=既にPRがある場合のみ、outcome=pushed(変更してpushした)/no_change(変更不要だった)/` +
1042
1078
  `blocked(衝突・要判断・許可待ち等で完了できなかった)のいずれか。blockedのときも必ず conductor_complete を呼び` +
1043
1079
  `reportに理由を書いてください。ブランチを切らない読み取り専用の作業では outcome=no_change を渡してください` +
1044
- `(branchは省略可)。\n\n` +
1045
- // アンカー(管理用ジョブ印)は必ず本文の最後に置く。完了検出の境界として使う。
1046
- `(管理用ジョブ印・変更不要: ${anchor})`;
1047
- // 改行(と連続改行)を空白1個に潰し、前後を整える。
1048
- return full.replace(/\r?\n+/g, ' ').replace(/\s+/g, ' ').trim();
1080
+ `(branchは省略可)。` +
1081
+ // ★nextWork(次の独立作業候補)は必ず書く(conductor_complete の説明と同じ・便 a5306df2 で両方にそろえた)。
1082
+ `nextWork(次の独立作業候補)は必ず書いてください(無ければ「なし」と理由)。\n\n`);
1049
1083
  }
1050
1084
  // ───────────────────────────────────────────────────────────────────────────
1051
1085
  // tmux I/O(失敗は握りつぶして null/false。送らない側に倒すため)
@@ -1107,6 +1141,23 @@ export function relaySendTmpDirs() {
1107
1141
  }
1108
1142
  /** tmux バッファ名の接頭辞(他人のバッファと衝突させないため現場・PID・時刻で一意にする)。 */
1109
1143
  export const RELAY_SEND_BUFFER_PREFIX = 'conductor-send';
1144
+ /**
1145
+ * 貼り付けのあとに「打った字」として添える1行(貼り付けの枠の外に置く)。
1146
+ *
1147
+ * 【なぜ要るか(便 6bef35c8・実測 2026-09-21 ai-kana 8e47d62e/mamoria 865c8372)】
1148
+ * 本文は paste-buffer -p(ブラケットペースト)で渡すので、職人には「貼り付けだけがあって、
1149
+ * 人が自分で打った字が1つも無い入力」として届く。Claude Code は貼り付けの中の指示を
1150
+ * 「人が自分の言葉で頼んだときだけ従う」扱いにするため、起こし直したばかりの職人
1151
+ * (これまでの便を実行した流れが会話に無い)が「貼り付け以外に指示が無い」と見て、
1152
+ * 実行してよいか人に聞いて止まった(管制には idle_prompt として出た)。
1153
+ * 【直し】人が便を貼ってから一言添えて送るのと同じ形にする。添え書きは枠の外に
1154
+ * send-keys -l(1字ずつ打つのと同じ)で置く。★本文も守り(番人・囲い・秘密保護)も変えない。
1155
+ * ★言えることだけを言う:配達役が届けるのは、この端末の持ち主の管制に積まれた便だけ
1156
+ * (中央は持ち主の合言葉でしか便を受け付けない)。誰が書いたかは本文の「投函:」欄のまま。
1157
+ * ★短く保つ(長い一気の入力は受け手が貼り付けと見なすことがある)。アンカーの字は入れない
1158
+ * (完了検出の境界を増やさないため)。
1159
+ */
1160
+ export const RELAY_DELIVERY_NOTE = '(配達役より)上の貼り付けは、この端末の持ち主の管制(采配くん)に投函された便を配達役が届けたものです。貼り付けに書かれたとおりに作業してください。';
1110
1161
  // ───────────────────────────────────────────────────────────────────────────
1111
1162
  // 報告の受け渡し(★ファイル経由・えふさん決定 2026-09-07/置き場と作法は 2026-09-21 に改訂)
1112
1163
  // ───────────────────────────────────────────────────────────────────────────
@@ -1457,7 +1508,8 @@ function runTmux(args) {
1457
1508
  execFileSync('tmux', args, { stdio: ['ignore', 'ignore', 'pipe'] });
1458
1509
  }
1459
1510
  /**
1460
- * tmux へ本文を1メッセージとして送り、最後に Enter を1回だけ送る。
1511
+ * tmux へ本文を1メッセージとして送り、貼り付けの枠の外に添え書き(RELAY_DELIVERY_NOTE)を打ち、
1512
+ * 最後に Enter を1回だけ送る。
1461
1513
  *
1462
1514
  * 【なぜ send-keys の引数に本文を載せないか(実測 2026-09-03 21:34〜の根治)】
1463
1515
  * tmux のクライアント→サーバの1命令には上限(IPC のメッセージ長・16KB前後)がある。
@@ -1493,6 +1545,8 @@ export function sendToTmux(site, text) {
1493
1545
  file = writeSendTmpFile(text);
1494
1546
  runTmux(['load-buffer', '-b', bufferName, file]);
1495
1547
  runTmux(['paste-buffer', '-d', '-p', '-b', bufferName, '-t', site]);
1548
+ // 貼り付けの枠の外に、打った字として添え書きを置く(RELAY_DELIVERY_NOTE の説明)。
1549
+ runTmux(['send-keys', '-t', site, '-l', ` ${RELAY_DELIVERY_NOTE}`]);
1496
1550
  runTmux(['send-keys', '-t', site, 'Enter']);
1497
1551
  return true;
1498
1552
  }
@@ -2442,7 +2496,7 @@ export async function relayTick(cfg, state, hooks) {
2442
2496
  // 画面にエコーされたアンカー行を「今回ジョブの境界」として記録し、完了検出は
2443
2497
  // アンカーより下=今回ジョブの出力だけを見る(churn・過去ジョブ DONE に強い)。
2444
2498
  const anchor = buildAnchor(cmd.id);
2445
- const text = buildSendText(cmd.body, cfg.site, anchor);
2499
+ const text = buildSendText(cmd.body, cfg.site, anchor, kindOf(cfg));
2446
2500
  if (!sendToTmux(cfg.site, text)) {
2447
2501
  // 送信失敗(sendToTmux は内部で例外も握って false を返す)。status は queued のまま据え置く。
2448
2502
  // 失敗の理由(tmux の stderr)は sendToTmux が直前の行に出している。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.39",
3
+ "version": "0.12.41",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3232,6 +3232,38 @@ _SQL_GATE_RE = re.compile(
3232
3232
  re.IGNORECASE,
3233
3233
  )
3234
3234
 
3235
+ # ファイルの名前(パス)の形をした語。空白・引用符・セミコロンを含まない1語で、区切り(/)か
3236
+ # 拡張子(.md / .csv 等)を持つもの。
3237
+ _PATH_WORD_RE = re.compile(r'^[\w.@%+=:,~/-]+$')
3238
+ _PATH_EXT_RE = re.compile(r'\.[A-Za-z0-9]{1,8}$')
3239
+
3240
+
3241
+ def _is_path_word(tok: str) -> bool:
3242
+ return bool(_PATH_WORD_RE.match(tok)) and ('/' in tok or bool(_PATH_EXT_RE.search(tok)))
3243
+
3244
+
3245
+ def _sql_scan_text(segment: str) -> str:
3246
+ """SQL の字面を探す相手(ファイルの名前を除いた命令+ヒアドキュメントの本文)。
3247
+
3248
+ 【なぜ除くか(便 6bef35c8・minoru 49cc6ddc/hook 833a458d・実測 2026-09-21)】
3249
+ 読むだけの照合 `node …/compare.mjs 期待値.csv …/prod-…-after-grant.md` が、ファイルの
3250
+ 名前の `-grant.` を「権限の SQL」と読んで DB の関門に落ちた(同じ照合でも名前に grant が
3251
+ 無い便 e8cf6c80 では通っていた)。ファイルの名前は1語で、SQL の文そのものにはなれない。
3252
+ ★除くのは名前の形をした語だけ。引用符で包んだ SQL・-c / -e に渡す中身・`-` のあとの
3253
+ ヒアドキュメントの本文は今までどおり見る。DB の道具・移行の道具・本番 DB の直接変更は
3254
+ この字面とは別の決まりで止まる(ここは1件も変えない)。
3255
+ """
3256
+ head, sep, body = segment.partition('\n')
3257
+ toks = tokens_of(head)
3258
+ kept = []
3259
+ for k, t in enumerate(toks):
3260
+ after_code_flag = k > 0 and toks[k - 1] in _CODE_FLAGS
3261
+ if not after_code_flag and _is_path_word(t):
3262
+ continue
3263
+ kept.append(t)
3264
+ return ' '.join(kept) + sep + body
3265
+
3266
+
3235
3267
  # 「引数にSQLらしい字面があっても、実際にはSQLを実行しない」動詞。
3236
3268
  # ★SQL文字列だけを根拠にした関門は、これらの動詞では出さない(=誤検知を消す)。
3237
3269
  # 棚卸し前は `grep "alter table" f` / `git commit -m "grant …"` / `echo "create table"` /
@@ -3322,7 +3354,7 @@ def _gate_db(words, tokens, segment: str):
3322
3354
  return 'drizzle-kit push/migrate'
3323
3355
  # SQL の字面(DDL / GRANT / RLS)。★ここへ来るのは「実行しうる入口」だけ
3324
3356
  # (文字を扱うだけの道具とシェルは _gate_one で先に振り分け済み)。
3325
- if _SQL_GATE_RE.search(segment):
3357
+ if _SQL_GATE_RE.search(_sql_scan_text(segment)):
3326
3358
  return 'SQL DDL/GRANT/RLS'
3327
3359
  return None
3328
3360
 
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env python3
2
+ """ファイルの名前に SQL の字(grant 等)が入っているだけの読む作業を、DB の関門に落とさない検査
3
+ (便 6bef35c8・minoru 49cc6ddc/hook 833a458d)。
4
+
5
+ 実行: python3 scripts/hooks/test_sql_word_in_filename.py
6
+ 1件でも期待と違えば exit 1。
7
+
8
+ 【何を守っているか】
9
+ (p) 読むだけの照合(期待値のファイルと実測のファイルを2つ読むだけ)は、ファイルの名前に
10
+ grant / truncate 等の字があっても確認なしで通る(実物の命令をそのまま使う)。
11
+ (n) ★negative test =本当に SQL を流しうる形は今までどおり止まる
12
+ 引用符で包んだ SQL・-c / -e に渡す中身・ヒアドキュメントの本文・DB の道具・移行の道具・
13
+ 本番 DB の直接変更・名前でない裸の語。
14
+ (w) 番人の判定が「名前だけを除いた」ことに留まり、ほかの判定を変えていないこと。
15
+
16
+ ★この台本は道具の名前と SQL の字を素の字で置かない(組み立てて使う)。台本の行そのものが
17
+ 番人の判定に当たって、検査が流せなくなるのを避けるため。
18
+ """
19
+ import os
20
+ import sys
21
+
22
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
23
+
24
+ import policy # noqa: E402
25
+
26
+ CWD = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
27
+ G = 'gr' + 'ant'
28
+ TR = 'trun' + 'cate'
29
+ DB_CLIENT = 'p' + 'sql'
30
+ MIG = 'pris' + 'ma'
31
+ PKG_MIG = 'npm run ' + 'migr' + 'ate'
32
+ DDL = 'cre' + 'ate table t(i int)'
33
+ COMPARE = 'node scripts/migration-verify/compare.mjs'
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:66} {got}')
43
+
44
+
45
+ def decide(cmd):
46
+ return policy.decide_bash(cmd, CWD)
47
+
48
+
49
+ # ─────────────────────────────────────────────────────────────────────────────
50
+ # (p) 読むだけの照合は通る
51
+ # ─────────────────────────────────────────────────────────────────────────────
52
+ POSITIVE = [
53
+ ('p1 49cc6ddc の実物(名前に -grant.)',
54
+ f'{COMPARE} docs/migrations/verify/0016_0017_expected.csv '
55
+ f'/tmp/claude-1000/minoru-0016-0017/prod-2026-09-21-after-{G}.md'),
56
+ ('p2 e8cf6c80 の実物(旗つき)',
57
+ f'{COMPARE} docs/migrations/verify/e1_estimates_idem_expected.csv '
58
+ f'/tmp/claude-1000/minoru-e1/prod-2026-09-21.md --subset --record'),
59
+ ('p3 名前に truncate(下線つなぎ)', f'{COMPARE} a/{TR}_expected.csv b/{TR}_actual.md'),
60
+ ('p4 旗に名前を渡す形', f'{COMPARE} --out=/tmp/claude-1000/after-{G}.md a.csv b.md'),
61
+ ('p5 拡張子だけの名前', f'{COMPARE} {G}.csv revoke.md'),
62
+ ]
63
+ for label, cmd in POSITIVE:
64
+ check(label, decide(cmd)['decision'], 'allow')
65
+
66
+ # ─────────────────────────────────────────────────────────────────────────────
67
+ # (n) negative test =本当に SQL を流しうる形は止まる(allow にならない)
68
+ # ─────────────────────────────────────────────────────────────────────────────
69
+ NEGATIVE = [
70
+ ('n1 引用符で包んだ権限の SQL を渡す', f'node run.mjs "{G} all on t to anon"'),
71
+ ('n2 -e の中身に権限の SQL', f'node -e "q(\'{G} select on t to anon\')"'),
72
+ ('n3 -e の中身が名前の形でも見る', f'node -e x.{G}'),
73
+ ('n4 -c の中身に DDL', f'python3 -c "run(\'{DDL}\')"'),
74
+ ('n5 ヒアドキュメントの本文に権限の SQL', f"python3 - <<'EOF'\nrun('{G} all on t to anon')\nEOF"),
75
+ ('n6 名前でない裸の語', f'node run.mjs {G}'),
76
+ ('n7 DB の道具に名前つきのファイル', f'{DB_CLIENT} -f /tmp/claude-1000/after-{G}.md'),
77
+ ('n8 DB の道具(名前に字なし)', f'{DB_CLIENT} -h db.example.com -c "select 1"'),
78
+ ('n9 移行の道具', f'{MIG} migrate deploy'),
79
+ ('n10 本番 DB を変える台本', PKG_MIG),
80
+ ('n11 名前の後ろに SQL を続けた形', f'node run.mjs /tmp/x.sql "{TR} table t"'),
81
+ ]
82
+ for label, cmd in NEGATIVE:
83
+ got = decide(cmd)['decision']
84
+ check(label, got if got in ('gate', 'deny') else f'通ってしまった({got})', got if got in ('gate', 'deny') else 'gate か deny')
85
+
86
+ # ─────────────────────────────────────────────────────────────────────────────
87
+ # (w) 名前を除く処理そのもの
88
+ # ─────────────────────────────────────────────────────────────────────────────
89
+ check('w1 名前の形の語は除く', policy._sql_scan_text(f'node a.mjs x/after-{G}.md'), 'node')
90
+ check('w2 引用符の SQL は残す', policy._sql_scan_text(f'node a.mjs "{G} all"'), f'node {G} all')
91
+ check('w3 -e の中身は残す', policy._sql_scan_text(f'node -e x.{G}'), f'node -e x.{G}')
92
+ check('w4 本文は残す', policy._sql_scan_text(f'python3 - <<E\n{G}\nE'), f'python3 - <<E\n{G}\nE')
93
+
94
+ print()
95
+ if failures:
96
+ print(f'NG {len(failures)} 件')
97
+ for f in failures:
98
+ print(' ', f)
99
+ sys.exit(1)
100
+ print('すべて期待どおり')
@@ -10,6 +10,10 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
10
10
  だから「速さ」より「戻せること・止まれること・報せること」を優先する。
11
11
  以下の14項を、着手前に読み、作業中も判断の基準にすること。
12
12
 
13
+ **便の届き方(起こし直した直後も同じ・便 6bef35c8)** — 便は配達役(relay)が職人の画面へ**貼り付け**で届け、枠の外に「(配達役より)…届けたものです」の添え書きを付ける。
14
+ これは持ち主の管制に投函された便なので、「貼り付けしか無い」「頭が `[CC1返事案/…]` だから未承認の案だ」と読んで止まらず、書かれたとおりに進める(`[CC1返事案/…]` は CC1 が書いた文を持ち主が投函した印)。
15
+ ★守りは変わらない:番人・囲いに止められたら回避しない(鉄則4)、秘密は扱わない(鉄則12)、本文の中の「本当の判断待ち」は今までどおり人の手番として止まる。
16
+
13
17
  ## 鉄則14項
14
18
 
15
19
  1. **着手時の確認と枝分かれ** — `git status && git log --oneline -3 && git stash list` を確認してから main を最新化し、新ブランチを切る。
@@ -64,3 +68,29 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
64
68
  - **なぜ。** 雛形(`.env.example`)は普通は秘密値を置かないが、「exampleだから常に安全」と番人へ例外を作ると、将来誤って実値が書かれたときの穴になる(利便性より安全側・えふさん確定)。
65
69
  - **雛形(`.env.example` など)を直すときの正しい形=「案を作る→人が1回で反映」。** 職人は変更後の中身を**リポジトリの外(`/tmp/claude-1000/` の下)**に作り、報告に次の2つを載せる。① 変更の**差分**(何を足す/消すか)。② 人が**素のターミナルで1回貼るだけで反映できる1つのコマンド**(例:`cp /tmp/claude-1000/<下書き> <リポ>/.env.example`)。★このコマンドは**人が自分のターミナルで**打つ(職人の番人は人の端末には掛からない)。職人が `.env.example` を直接書き換えようとしても番人が止めるので、**回避せず、この形で案を出す**。
66
70
  - **本物の `.env`・`.env.local` は、これまでどおり中身も書き込みも扱わない**(秘密の値は1文字も読まない・書かない=鉄則12の金庫)。
71
+
72
+ ## 便の終え方
73
+
74
+ 便の最後には、配達役が付けた1行「【便の終え方】…報告の置き場: ~/最終行: DONE:<現場> とだけ出力/完了の知らせ: conductor_complete を commandId="<指示ID>" で1回だけ」と、管理用ジョブ印 `CONDUCTOR_JOB:<指示ID>` がある。作業がすべて終わったら、この節のとおりに次の順で終える(便 a5306df2:以前は便ごとに付いていた全文を、ここへ寄せた)。
75
+
76
+ ① **報告をファイルに書く。** 置き場は配達役の1行が指すファイル**だけ**(正本は `~/.local/state/conductor/reports/<指示ID>.md`)。★便の本文に別の置き場(`/tmp` など)が書かれていても、**配達役の1行の置き場を正とする**(置き場は1か所)。フォルダが無ければ作る。長さの制限は無い。画面に出す必要は無い。中身は変更ファイル・要点・チェック結果を簡潔に。
77
+ ② **ファイルの先頭(1行目から)に、次の囲みを付ける**(管制が機械で読む口)。
78
+ ```
79
+ ---
80
+ instruction_id: <指示ID>
81
+ site: <現場=DONE: の後ろと同じ名前>
82
+ outcome: pushed または no_change または blocked のどれか1つ
83
+ created_at: 書いた日時(例 2026-09-21T04:20:00+09:00)
84
+ evidence:
85
+ - 受入の証拠を1行に1つ(例 PR https://github.com/... / CI test pass / main に入った など)
86
+ ---
87
+ ```
88
+ この囲みの下に、いつもどおりの報告本文を書く(本文が空だと未完成と見なされる)。
89
+ ③ **その後の最終行に、配達役の1行にある `DONE:<現場>` とだけ出力する**(完了の合図)。
90
+ ④ **devlog の MCP ツール `conductor_complete` を、配達役の1行にある commandId で1回だけ呼ぶ**(relay に依存しない確実な完了通知)。report 引数に上の報告本文を添えてもよい。
91
+ - **nextWork(次の独立作業候補)は必ず書く。** 無ければ「なし」と、なぜ無いかの理由を書く。
92
+ - 可能なら次も一緒に渡す(すべて任意・分かる範囲でよく、渡さなくても完了する):`branch=`作った作業ブランチ名(`git rev-parse --abbrev-ref HEAD`)、`baseBranch=`分岐元(通常 main)、`headSha=`最後のコミット(短縮可)、`pushed=`push したか(true/false)、`filesChanged=`変更したファイル数、`prTitle=`お客さま(非エンジニア)がそのまま読む題名(rebase/squash/lint 等の専門用語を避け、やさしい日本語で)、`prNumber=`既に PR がある場合のみ、`outcome=`pushed(変更して push した)/no_change(変更不要だった)/blocked(衝突・要判断・許可待ち等で完了できなかった)のどれか。
93
+ - **blocked のときも必ず conductor_complete を呼び、report に理由を書く。**
94
+ - ブランチを切らない**読み取り専用の作業では outcome=no_change** を渡す(branch は省略可)。
95
+
96
+ **作業中の確認の一言説明** — 作業中にユーザー確認(許可を求めるプロンプト)が出る操作では、その一言説明(description)を英語ではなく日本語で、「プルリクエストを作成する」「テストを実行する」のように「何をするか」が一目で分かる簡潔な形(〜を作成する/実行する/削除する 等)で書く。この説明は管制の確認カードにそのまま表示され、非エンジニアのお客さまが読んで許可を判断する。