@tyhld/conductor 0.12.112 → 0.12.113

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.112",
3
+ "version": "0.12.113",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,6 +2,7 @@
2
2
  "_note": "采配くん販売用テンプレート — プロジェクトの .claude/settings.json に配置する。<PLACEHOLDER> を顧客環境に合わせて差し替えること。差し替え箇所は README.md の「プレースホルダ一覧」を参照。",
3
3
  "_note_guard": "★段2(2026-07-23): 許可/確認の判定は番人(~/.tyhld/hooks の cc2_guard.py ほか3フック)が持つようになった。既定は allow(危険と関門だけを番人が deny/ask 相当で止め、それ以外は自走)。そのため settings 側の allow・ask 列挙は不要になり、空にしてある。番人フックはこのファイルではなく顧客PCの ~/.claude/settings.json に1本だけ登録する(setup.sh ⑤)。この現場ファイルには権限ルール(deny)と現場固有の sandbox だけを置く。",
4
4
  "_note_deny": "★deny は番人の allow 既定より常に先に効く(Claude Code の評価順 deny→ask→allow)。ここに残す床(.env/.ssh/鍵/.tyhld/settings.json/hooks と Supabase 書込3種など)は allow を空にしても当然そのまま優先される。deny は消さない。",
5
+ "_note_force_push": "★強い上書きの push の禁止は【旗の位置ごとに1行ずつ】要る(便 e619bd86・実地は便 6e8d85e6)。この一覧は公式の仕様どおり「語の並びの当てはめ」でしか測れないので、旗が動詞の直後に来る形(末尾 :* の3件)・旗が末尾ぴったりの形(* の3件)・旗の後ろにまだ語が続く形(* … * の3件)を、それぞれ書かないと閉まらない。実地(便 6e8d85e6 の 2-A)で、末尾ぴったりの形は「後ろに語が続く形」の規則では当たらず、中間の位置と後ろに別の旗が付く形は「末尾ぴったりの規則」では当たらないことを確かめたので、両方を持つ。★`*` と `:*` を混ぜた書き方は `*` がリテラルになり何も止めないので足さない(便 07ee11a2・退役済み)。★この一覧では閉められない形(部分命令の前に場所や設定を指す旗を挟む形・先頭が + の参照指定を引用符で囲う形)は、番人(policy.py の _force_push_token_danger)が語の並びに直してから止める=二層で守る。",
5
6
  "_note_read": "★Read(…) の禁止は【二重の守りの外側】(便 93b2fa03・えふさん確定 2026-09-16)。秘密の読み取りを本当に判定しているのは番人(~/.tyhld/hooks の policy.py の secret_reason)で、そちらは Read だけでなく NotebookRead・Grep・Glob・MCP・file:// まで同じ境界で止める。この一覧は【番人が外された機体でも Read だけは止まる】ための保険なので、番人より粗い。粗さの実害は1つだけ: Read(**/*.env) は example.env のような前置きの雛形まで止める(.env.example は止まらない)。番人の方は雛形を通すので、番人が入っている機体では今までどおり雛形を読める。 ★2026-09-16(便 3a3771b1)に「名前だけの規則」を1本外し、場所で名指しする規則に替えた。外したのは credentials という名前だけの Read 規則で、依存の部品(node_modules)の中の同じ名前のフォルダまで止め、現場の build と差分確認を落としていた(実測2件・11リポジトリ中)。本物の置き場(.aws / .gcloud / .config/gcloud / .azure / .ssh / .gnupg)は場所で名指しして守っているので、止める範囲は1つも減っていない。★この一覧の書き方(glob)では「依存の部品の中だけ除く」が書けない。名前が同じでも中身が違う、という判断は場所と管理主体を見ないとできず、glob は場所のパターンしか書けないため。だから外側のこの一覧は【場所】だけで名指しし、名前だけでは言い切れない物の可否は番人(policy.py の (4))が「機械が入れた部品の中か」を git の事実で確かめて決める。 ★2026-09-17(便 336e74e5=D1 方式D 第一便)に、遅くて安全性に寄与しない `**/` の Read 禁止を24件 退役した(実測:ホーム由来17本で本物のツリーの囲い起動が約7秒・広い鍵/認証で約3秒遅くなる。 D1b 重大発見1)。退役の内訳=(1) ホーム由来の `**/`(.ssh・.aws・.gnupg・.config/gcloud・.azure・.netrc・.pypirc・id_rsa* ほか・.tyhld・.config/gh/hosts.yml)=`**/` は作業場所の中だけに当たり home には当たらない(仕様)ので、home は ~/ 絶対パスの規則で守る。(2) 広い鍵・認証の `**/`(*.pem・*.key・credentials.json・auth.json・*login.json・*.env)=依存の部品(node_modules)を大量に歩いて遅く、番人(policy.py の secret_reason)が同じ名前を form A で止めるので重複(依存の中の credentials.json は番人が許すので通常開発も壊れない)。(3) .npmrc=中身は ${NPM_TOKEN} の参照だけで秘密の実値は無い。囲いで /dev/null 化すると @tyhld の取り寄せが壊れる(重大発見3)。残した `**/`=リポの中の .env 系(.env・.env.local・.env.development・.env.production・.env.test・.env.vercel・.env.*.local)=台本経由(form B)の持ち出しを囲いの OS 層で止めるのに要る。★退役した規則は update-guard が各現場からも外す(正本が配った物だけ・顧客が足した規則は消さない)。",
6
7
  "_note_home_secret": "★ホーム配下(リポの外)の秘密の置き場は【場所で1つずつ名指し】する(えふさん確定 2026-09-17・便 230c3175=D2)。~/ で始まる規則はホームからの場所、**/ で始まる規則は作業場所(リポ)の中だけに当たる(Claude Code 仕様)。★$HOME/** や ~/*token* のような広い書き方はしない。理由は2つ: (1) 通常の作業(npm・gh・git・build)が要るホームのファイルまで止める。(2) サンドボックスはこの一覧を OS の読み取り禁止に写すが、ホームに * を含む書き方は起動が 7〜18 秒遅くなるか、効かない(実測: ~/*/.env.local 17.5秒・~/.env* 8.5秒で効かず・~/*token* 15.7秒で効かず。場所を名指しした形は 0.25〜0.32 秒で効く)。★わざと禁止しない置き場(通常の作業に要る・中身の取り出しは番人が別に止める): ~/.npmrc(npm install が読む。npm config get の _auth 表示は番人が止める)、~/.config/gh/hosts.yml(gh と git push が読む。gh auth token・git credential fill は番人が止める)、~/.claude/session-env(Claude Code 自身が読む)、~/.bashrc・~/.profile(シェルが読む。中に秘密を置かないのが本筋)、~/.android/debug.keystore(開発用の署名)。★機体ごとに名前が違う置き場(ホーム直下の token・secret の名前のファイル、ホーム直下に残ったリポの写しの .env など)は、配る一覧では名指しできない。番人(policy.py の _home_secret_reason)が名前で止めるほか、必要なら機体の ~/.claude/settings.json に場所を足す。★フォルダごとの持ち出し(cp -r・rsync・tar・ln -s・コマンドに書いたプログラムでの複製)は番人が、写す元の中に秘密が在るかを名前だけで調べて止める。【限界】台本のファイルの中で写す形と、名前を書かずに読む子プロセスは、サンドボックスの無い現場では番人でも止まらない(OS の層が要る)。",
7
8
  "_note_network": "★外への通信は全部通す(allowedDomains: [\"*\"]・えふさん決定 2026-09-03=甲)。販売版もお客さま機で自走するのが既定(2026-08-23 決定)。理由: サンドボックスの通信許可の問いは管制の3ボタンでは押せず(Claude Code 2.1.258 本体の作り・実測 2026-09-03)、端末に人が居ないと職人が止まったままになるため。★緩むのは『電話をかける』だけ。『金庫を持ち出す』(秘密の持ち出し)を止めるのは番人 cc2_guard.py の別判定で、通信設定とは独立に効き続ける。★下の deniedDomains は allowedDomains より先に判定されるので、本番DBへの直結禁止は \"*\" を入れても効いたまま(実測で確認)。この行を消すと本番DBが開くので消さないこと。",
@@ -17,6 +18,9 @@
17
18
  "Bash(git push * --force)",
18
19
  "Bash(git push * -f)",
19
20
  "Bash(git push * --force-with-lease)",
21
+ "Bash(git push * --force *)",
22
+ "Bash(git push * -f *)",
23
+ "Bash(git push * --force-with-lease *)",
20
24
  "Bash(git push * +*)",
21
25
  "Bash(rm -rf:*)",
22
26
  "Bash(rm -fr:*)",
@@ -1555,6 +1555,131 @@ def _main_push_danger(tokens, cwd):
1555
1555
  return None
1556
1556
 
1557
1557
 
1558
+ # ─────────────────────────────────────────────────────────────────────────────
1559
+ # 強い上書きの push を【語(トークン)】で見る(DENY・便 e619bd86)
1560
+ #
1561
+ # 【なぜ要るか(実地 便 6e8d85e6)】
1562
+ # 強い上書きの判定は、長いあいだ「`git` と `push` が隣り合っている字面」だけを見ていた
1563
+ # (_danger_patterns の1行)。そのため部分命令の前に何かを挟むと当たらず、作り物の遠隔に
1564
+ # 対して実際に強制上書きが成立した(6形)。
1565
+ # ・場所を指す旗・設定を足す旗・履歴の置き場を指す旗を挟む形(5形)
1566
+ # ・先頭が + の参照指定を引用符で囲う形(1形)
1567
+ # 配る禁止の一覧(現場の .claude/settings.json)も、公式の仕様どおり「頭の語の並び」で
1568
+ # 当てるので、同じ形を1つも止めていなかった。
1569
+ #
1570
+ # 【どう見るか】殻(シェル)の決まりで語に分け、引用符を外した【語の並び】で測る。
1571
+ # ① git の全体オプション(場所を指す物・設定を足す物・履歴や作業木の置き場を指す物など。
1572
+ # 値を「=」で付ける形と、次の語で渡す形の両方)を読み飛ばして部分命令を取り出す。
1573
+ # ② 部分命令が push のときだけ、強い上書きの印を【位置を問わずに】探す。
1574
+ # ・長い形(--force / --force-with-lease / --force-if-includes。--no-… は印でない)
1575
+ # ・短い形(-f。-fu のようにまとめて書いた形も。値を取る旗の値は読まない)
1576
+ # ・先頭が + の参照指定(引用符は語に分けた時点で外れる)
1577
+ # ③ 語に分けられない行(引用符が閉じていない等)で git と push の語が見えていれば、
1578
+ # 中身を確かめられないので安全側(止める)に倒す。
1579
+ # ★つなぎ(; && || |)・入れ子($( ) ・` `)・包み(殻に渡す文字列・env や時間制限)・
1580
+ # 変数の展開には、_bypass_scan に相乗りすることで同じ判定が届く(番人のいつもの流儀)。
1581
+ # ★いまの字面の判定(_danger_patterns)は残す。どちらかに当たれば止める=止まる形は1つも減らない。
1582
+ # ─────────────────────────────────────────────────────────────────────────────
1583
+
1584
+ #: git の全体オプションのうち、値を【次の語】で受け取る物(「=」で付ける形は1語で済む)。
1585
+ _GIT_GLOBAL_VALUE_FLAGS = frozenset({
1586
+ '-C', '-c', '--git-dir', '--work-tree', '--namespace', '--super-prefix',
1587
+ })
1588
+
1589
+ #: push の旗のうち、値を【次の語】で受け取る物(値が `-f` でも旗と読み違えないため)。
1590
+ _PUSH_VALUE_FLAGS = frozenset({
1591
+ '--repo', '-o', '--push-option', '--receive-pack', '--exec',
1592
+ })
1593
+
1594
+ #: 短い旗をまとめて書いた形で、後ろの文字を値として飲む文字(`-o値`)。
1595
+ _PUSH_SHORT_VALUE_CHARS = 'o'
1596
+
1597
+
1598
+ def _git_subcommand(args):
1599
+ """git の後ろの語から全体オプションを読み飛ばし、(部分命令, その後ろの語) を返す。"""
1600
+ rest = list(args)
1601
+ while rest and rest[0].startswith('-') and rest[0] != '--':
1602
+ flag = rest.pop(0)
1603
+ if '=' in flag:
1604
+ continue
1605
+ if flag in _GIT_GLOBAL_VALUE_FLAGS and rest:
1606
+ rest.pop(0)
1607
+ if not rest:
1608
+ return None, []
1609
+ return rest[0], rest[1:]
1610
+
1611
+
1612
+ def _push_force_mark(push_args):
1613
+ """push の後ろの語に、強い上書きの印が在れば その語 を返す(位置は問わない)。"""
1614
+ i = 0
1615
+ only_refs = False
1616
+ while i < len(push_args):
1617
+ t = push_args[i]
1618
+ if t == '--':
1619
+ only_refs = True # ★以後は位置の語(参照指定)だけ
1620
+ i += 1
1621
+ continue
1622
+ name = t.split('=', 1)[0]
1623
+ if not only_refs and name in _PUSH_VALUE_FLAGS:
1624
+ i += 2 if '=' not in t else 1
1625
+ continue
1626
+ if not only_refs and t.startswith('--'):
1627
+ if name.startswith('--force'):
1628
+ return t
1629
+ i += 1
1630
+ continue
1631
+ if not only_refs and t.startswith('-') and len(t) > 1:
1632
+ for ch in t[1:]:
1633
+ if ch == 'f':
1634
+ return t
1635
+ if ch in _PUSH_SHORT_VALUE_CHARS:
1636
+ break # ★旗にくっつけた値は読まない(`-o値` の f を拾わない)
1637
+ i += 1
1638
+ continue
1639
+ if t.startswith('+'):
1640
+ return t
1641
+ i += 1
1642
+ return None
1643
+
1644
+
1645
+ def _word_head(token: str) -> str:
1646
+ """語から道具の名前を取り出す(道筋・入れ子の始まり・引用符の切れ端を外す)。
1647
+
1648
+ ★`$(git` ・`` `git `` ・`"git` のように入れ子の始まりがくっついた語も、同じ名前として読む。
1649
+ """
1650
+ return token.split('/')[-1].strip('`"\'').lstrip('$({').rstrip(')}')
1651
+
1652
+
1653
+ def _force_push_token_danger(tokens):
1654
+ """語の並びで見る強い上書きの push。危険なら理由を返す。"""
1655
+ for i, t in enumerate(tokens):
1656
+ if _word_head(t) != 'git':
1657
+ continue
1658
+ sub, rest = _git_subcommand(tokens[i + 1:])
1659
+ if sub is None or _word_head(sub) != 'push':
1660
+ continue
1661
+ mark = _push_force_mark(rest)
1662
+ if mark:
1663
+ return f'force push ({mark})'
1664
+ return None
1665
+
1666
+
1667
+ def _unparsable_push_danger(command: str):
1668
+ """語に分けられない行(引用符が閉じていない等)で git と push が見えていれば止める。
1669
+
1670
+ ★本物の区切り(引用符の中では割らない)で見る=ふつうの行の引用符の中に区切りが在るだけの
1671
+ コマンドを、壊れた行と読み違えない。
1672
+ """
1673
+ for seg in split_commands(command, bodies=False):
1674
+ try:
1675
+ shlex.split(seg, comments=False, posix=True)
1676
+ except ValueError:
1677
+ names = {_word_head(t) for t in seg.split()}
1678
+ if 'git' in names and 'push' in names:
1679
+ return 'force push (語に分けられない行=安全側に倒す)'
1680
+ return None
1681
+
1682
+
1558
1683
  # GitHub の API の道筋(書き込み)。
1559
1684
  _API_MERGE_RE = re.compile(r'(^|/)pulls/[^/]+/merge/?$')
1560
1685
  _API_PROTECTION_RE = re.compile(
@@ -1758,6 +1883,10 @@ _SKIP_PERMISSION_FLAGS = frozenset({
1758
1883
 
1759
1884
  def _bypass_segment_danger(seg, tokens, cwd, command):
1760
1885
  """1セグメントの抜け道(main 直 push 以外)。"""
1886
+ # ★強い上書きの push は、道具の語が読めなくても(包みの中でも)語の並びで見る(便 e619bd86)。
1887
+ hit = _force_push_token_danger(tokens)
1888
+ if hit:
1889
+ return hit
1761
1890
  words = _tool_words(tokens)
1762
1891
  if not words:
1763
1892
  return None
@@ -1811,6 +1940,9 @@ def _bypass_scan(command: str, cwd=None):
1811
1940
  ★機械的な区切りで1回、本物の区切り(本文なし)でもう1回測る(便 2.5)。複数行の引用符の
1812
1941
  中に書いた GraphQL の本文などを、ひとかたまりのまま読むため。止める側は減らない。
1813
1942
  """
1943
+ hit = _unparsable_push_danger(command)
1944
+ if hit:
1945
+ return hit
1814
1946
  for segs in (split_segments(command), split_commands(command, bodies=False)):
1815
1947
  here = cwd
1816
1948
  for seg in segs:
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env python3
2
+ """強い上書きの push を【語(トークン)】で見る判定の試験(便 e619bd86)。
3
+
4
+ 実行: python3 scripts/hooks/test_force_push_tokens.py
5
+ 1件でも期待と違えば exit 1。
6
+
7
+ 【なぜ要るか(実地 便 6e8d85e6)】
8
+ 判定が「`git` と `push` が隣り合っている字面」だけを見ていたため、部分命令の前に何かを挟む
9
+ 5形と、先頭が + の参照指定を引用符で囲う1形が、禁止の一覧でも番人でも止まらず、
10
+ 作り物の遠隔に対して強制上書きが【実際に成立】した。語の並びで見る判定を足して閉じる。
11
+
12
+ 【ここで固定すること】
13
+ (1) 素通りだった6形が止まる(★この6形は語で見る判定だけが止める=足した判定が効いている証拠)
14
+ (2) つないだ行・入れ子・包み・短い旗のまとめ書き・「=」つきと次の語渡しにも判定が届く
15
+ (3) 語に分けられない行(引用符が閉じていない等)は安全側に倒す
16
+ (4) ★止めてはいけない形は止めない(普通の push・全体オプションつきの普通の push・
17
+ 取り寄せ側で + の参照指定を使う形・push でない部分命令で同じ旗を使う形)
18
+ (5) ★いまの字面の判定は1つも弱めていない(元から止まっていた形はそのまま止まる)
19
+
20
+ ★見本の危険語は部品から組み立てる(番人の見本に危険語をそのまま書かない)。
21
+ """
22
+ import os
23
+ import sys
24
+
25
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
26
+
27
+ import policy # noqa: E402
28
+
29
+ failures = []
30
+
31
+
32
+ def check(label, got, want):
33
+ ok = got == want
34
+ if not ok:
35
+ failures.append(f'{label}: got={got!r} want={want!r}')
36
+ print(f'{"OK " if ok else "NG "} {label:72} {str(got)[:32]}')
37
+
38
+
39
+ # ── 部品(危険語を直接書かない) ──
40
+ PUSH = 'pu' + 'sh'
41
+ FORCE = '--for' + 'ce'
42
+ LEASE = FORCE + '-with-lease'
43
+ SHORT = '-' + 'f'
44
+ NOVERIFY = '--no-' + 'verify'
45
+ WORK = '/tmp/claude-1000' # 判定に使う作業場所(実物のリポを指さない)
46
+ BR = 'probe'
47
+
48
+ # ─────────────────────────────────────────────────────────────────────────────
49
+ # (1) 素通りだった6形(実地 便 6e8d85e6 の重大発見1・2)
50
+ # ─────────────────────────────────────────────────────────────────────────────
51
+ LEAKED = [
52
+ f'git -C {WORK}/x {PUSH} {FORCE} origin {BR}',
53
+ f'git -C "$DIR" {PUSH} {FORCE} origin {BR}',
54
+ f'git -c {PUSH}.default=current {PUSH} {FORCE} origin {BR}',
55
+ f"git '{PUSH}' {FORCE} origin {BR}",
56
+ f'git --git-dir={WORK}/x/.git --work-tree={WORK}/x {PUSH} {FORCE} origin {BR}',
57
+ f'git {PUSH} origin "+{BR}"',
58
+ ]
59
+
60
+ print('\n=== (1) 素通りだった6形が止まる ===')
61
+ for cmd in LEAKED:
62
+ r = policy.decide_bash(cmd, cwd=WORK)
63
+ check(f'deny {cmd[:62]}', r['decision'], policy.DENY)
64
+
65
+ print('\n=== (1b) ★この6形は【語で見る判定】が止めている(足した判定が効いている証拠) ===')
66
+ for cmd in LEAKED:
67
+ got = policy._force_push_token_danger(policy.tokens_of(cmd))
68
+ check(f'語で見る判定が当たる {cmd[:52]}', bool(got), True)
69
+
70
+ # ─────────────────────────────────────────────────────────────────────────────
71
+ # (2) 旗の位置・つなぎ・入れ子・包み・まとめ書き・値の渡し方
72
+ # ─────────────────────────────────────────────────────────────────────────────
73
+ DENY_SHAPES = [
74
+ # 旗の位置(便 6e8d85e6 の P1〜P12)
75
+ f'git {PUSH} {FORCE} origin {BR}',
76
+ f'git {PUSH} origin {BR} {FORCE}',
77
+ f'git {PUSH} origin {BR} {FORCE} {NOVERIFY}',
78
+ f'git {PUSH} {SHORT} origin {BR}',
79
+ f'git {PUSH} origin {BR} {SHORT}',
80
+ f'git {PUSH} origin {SHORT} {BR}',
81
+ f'git {PUSH} {LEASE} origin {BR}',
82
+ f'git {PUSH} origin {BR} {LEASE}',
83
+ f'git {PUSH} origin +{BR}',
84
+ f'git {PUSH} origin +refs/heads/{BR}:refs/heads/{BR}',
85
+ # 短い旗のまとめ書き
86
+ f'git {PUSH} -u{SHORT[1:]} origin {BR}',
87
+ f'git {PUSH} -v{SHORT[1:]} origin {BR}',
88
+ # 全体オプションの「=」つきと、次の語で渡す形
89
+ f'git --git-dir={WORK}/x/.git {PUSH} {FORCE} origin {BR}',
90
+ f'git --git-dir {WORK}/x/.git {PUSH} {FORCE} origin {BR}',
91
+ f'git --work-tree {WORK}/x {PUSH} {FORCE} origin {BR}',
92
+ f'git --namespace ns {PUSH} {FORCE} origin {BR}',
93
+ f'git --exec-path={WORK}/x {PUSH} {FORCE} origin {BR}',
94
+ f'git -C {WORK}/x -c user.name=x {PUSH} {LEASE}=refs/heads/{BR}:abc origin {BR}',
95
+ # つないだ行
96
+ f'echo hi && git -C {WORK}/x {PUSH} {FORCE} origin {BR}',
97
+ f'echo hi ; git -C {WORK}/x {PUSH} origin +{BR}',
98
+ f'true || git -C {WORK}/x {PUSH} {SHORT} origin {BR}',
99
+ f'echo hi | xargs -I{{}} git -C {WORK}/x {PUSH} {FORCE} origin {BR}',
100
+ # 入れ子・包み
101
+ f'bash -c "git -C {WORK}/x {PUSH} {FORCE} origin {BR}"',
102
+ f'sh -c \'git -C {WORK}/x {PUSH} origin "+{BR}"\'',
103
+ f'echo $(git -C {WORK}/x {PUSH} {FORCE} origin {BR})',
104
+ f'env -C {WORK}/x git {PUSH} {FORCE} origin {BR}',
105
+ f'timeout 60 git -C {WORK}/x {PUSH} {FORCE} origin {BR}',
106
+ f'nohup git -C {WORK}/x {PUSH} {SHORT} origin {BR}',
107
+ f'GIT_TRACE=1 git -C {WORK}/x {PUSH} {FORCE} origin {BR}',
108
+ # 変数に入れた場所
109
+ f'D={WORK}/x; git -C "$D" {PUSH} {FORCE} origin {BR}',
110
+ # 参照指定の前に区切りを置く形
111
+ f'git {PUSH} origin -- +{BR}',
112
+ ]
113
+
114
+ print('\n=== (2) つなぎ・入れ子・包み・まとめ書き・値の渡し方にも届く ===')
115
+ for cmd in DENY_SHAPES:
116
+ r = policy.decide_bash(cmd, cwd=WORK)
117
+ check(f'deny {cmd[:62]}', r['decision'], policy.DENY)
118
+
119
+ # ─────────────────────────────────────────────────────────────────────────────
120
+ # (3) 語に分けられない行は安全側(止める)
121
+ # ─────────────────────────────────────────────────────────────────────────────
122
+ print('\n=== (3) 語に分けられない行(引用符が閉じていない)は安全側に倒す ===')
123
+ for cmd in [
124
+ f'git {PUSH} origin "+{BR}',
125
+ f'git -C {WORK}/x {PUSH} origin \'{BR}',
126
+ ]:
127
+ r = policy.decide_bash(cmd, cwd=WORK)
128
+ check(f'deny {cmd[:62]}', r['decision'], policy.DENY)
129
+
130
+ # ─────────────────────────────────────────────────────────────────────────────
131
+ # (4) ★止めてはいけない形
132
+ # ─────────────────────────────────────────────────────────────────────────────
133
+ ALLOWED_SHAPES = [
134
+ # 普通の push(枝を名指し)
135
+ f'git {PUSH} origin {BR}',
136
+ f'git {PUSH} origin feat/my-branch',
137
+ f'git {PUSH} -u origin {BR}',
138
+ f'git {PUSH} --dry-run origin {BR}',
139
+ f'git {PUSH} --delete origin {BR}',
140
+ f'git {PUSH} origin {BR}:refs/heads/{BR}',
141
+ f'git {PUSH} origin feature+x',
142
+ # 全体オプションつきの普通の push
143
+ f'git -C {WORK}/x {PUSH} origin {BR}',
144
+ f'git -c user.name=x {PUSH} origin {BR}',
145
+ f'git --git-dir={WORK}/x/.git --work-tree={WORK}/x {PUSH} origin {BR}',
146
+ # 値を取る旗の値を旗と読み違えない
147
+ f'git {PUSH} -o ci.skip origin {BR}',
148
+ f'git {PUSH} --push-option=ci.skip origin {BR}',
149
+ # 打ち消しの旗は印ではない
150
+ f'git {PUSH} --no{LEASE[1:]} origin {BR}',
151
+ # 取り寄せ側で + の参照指定を使う形
152
+ 'git fetch origin +refs/heads/x:refs/heads/x',
153
+ f'git fetch {FORCE} origin +refs/heads/x:refs/heads/x',
154
+ 'git pull origin +x',
155
+ # push でない部分命令で同じ旗を使う形
156
+ f'git checkout {FORCE} {BR}',
157
+ f'git branch {SHORT} {BR} HEAD',
158
+ f'git tag {SHORT} v1 HEAD',
159
+ f'git submodule update --init {FORCE}',
160
+ f'git -C {WORK}/x stash {PUSH} -m hold',
161
+ # 字面が本文の中にあるだけの形
162
+ f'git commit -m "note: {PUSH} origin {BR}"',
163
+ ]
164
+
165
+ print('\n=== (4) 止めてはいけない形は止めない ===')
166
+ for cmd in ALLOWED_SHAPES:
167
+ r = policy.decide_bash(cmd, cwd=WORK)
168
+ check(f'not deny {cmd[:58]}', r['decision'] != policy.DENY, True)
169
+
170
+ print('\n=== (4b) 語で見る判定も、止めてはいけない形には当たらない ===')
171
+ for cmd in ALLOWED_SHAPES:
172
+ got = policy._force_push_token_danger(policy.tokens_of(cmd))
173
+ check(f'当たらない {cmd[:58]}', got, None)
174
+
175
+ # ─────────────────────────────────────────────────────────────────────────────
176
+ # (5) ★いまの字面の判定を弱めていない(元から止まっていた形はそのまま)
177
+ # ─────────────────────────────────────────────────────────────────────────────
178
+ print('\n=== (5) 字面の判定はそのまま残っている(二層で止める) ===')
179
+ for cmd in [
180
+ f'git {PUSH} {FORCE} origin main',
181
+ f'git {PUSH} {SHORT} origin main',
182
+ f'git {PUSH} {LEASE} origin main',
183
+ ]:
184
+ check(f'字面の判定が当たる {cmd[:52]}',
185
+ bool(policy._danger_patterns(cmd.lower())), True)
186
+
187
+ print()
188
+ if failures:
189
+ print(f'FAIL {len(failures)} 件')
190
+ for f in failures:
191
+ print(f' - {f}')
192
+ sys.exit(1)
193
+ print('ALL PASS')
@@ -140,6 +140,7 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
140
140
  14. **`.env` 類(`.env`・`.env.local`・`.env.example` などの雛形も含む)は職人が書き換えない(えふさん確定 2026-09-18・便 3d11f913)**
141
141
  - **番人は `.env` 本体も `.env.example` などの雛形も、書き込みを止める**(Write/Edit の設定 deny と、Bash の `echo`/`tee`/`sed -i`/`cp`/`mv` などを、経路を問わず同じ結果にそろえた)。読み込みは今までどおり(雛形は読める・本物は読めない)。
142
142
  - **なぜ。** 雛形(`.env.example`)は普通は秘密値を置かないが、「exampleだから常に安全」と番人へ例外を作ると、将来誤って実値が書かれたときの穴になる(利便性より安全側・えふさん確定)。
143
+ - **★雛形(`.env.example`)を読むときは Read の道具で読む(実測 便 6e8d85e6)。** `cat .env.example` は**禁止の一覧で止まる**(配っている `Bash(cat .env.*)` に当たる)。番人は雛形の読みを許しているので、Read の道具・Grep の道具なら今までどおり読める。★規則は緩めない(実名に絞ると、将来の新しい枝名が漏れる)。止まったら回避せず、Read の道具に替える。
143
144
  - **雛形(`.env.example` など)を直すときの正しい形=「案を作る→人が1回で反映」。** 職人は変更後の中身を**リポジトリの外(`/tmp/claude-1000/` の下)**に作り、報告に次の2つを載せる。① 変更の**差分**(何を足す/消すか)。② 人が**素のターミナルで1回貼るだけで反映できる1つのコマンド**(例:`cp /tmp/claude-1000/<下書き> <リポ>/.env.example`)。★このコマンドは**人が自分のターミナルで**打つ(職人の番人は人の端末には掛からない)。職人が `.env.example` を直接書き換えようとしても番人が止めるので、**回避せず、この形で案を出す**。
144
145
  - **本物の `.env`・`.env.local` は、これまでどおり中身も書き込みも扱わない**(秘密の値は1文字も読まない・書かない=鉄則12の金庫)。
145
146
  15. **工程の区切りで「いま何をしているか」を日本語1行で出す(えふさん確定 2026-10-02・便 fbaba4b1)**
@@ -170,7 +171,9 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
170
171
  - 止まる例: `rm -rf /tmp/claude-1000/作業名`(禁止の一覧)/ `rm -r "$DIR/x"`・`rm -r build/*`(番人)/ `rm -r .git`・`rm -r .claude/hooks`・`rm -r .tyhld`(番人=相対パスでも消せない)
171
172
  - ★**探すだけの道具の「探す言葉」に `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` のように**後ろで消す形は止まる**。
172
173
  - **まとめて全部を記録に入れる `git add`・`git commit`**(`git add -A`・`git add .`・`git commit -a`・`git commit -am 説明`・`git commit --all`)→ 止まる。着手前から変わっていたファイル(例 `.claude/settings.json`)まで入り、番人がそれを記録から外す操作も止めるので取り返せない(便 4cf71dde)。**ファイルを名指しで `git add <ファイル>` してから `git commit -m 説明`**(鉄則1-④)。
173
- - **作業場所が変数のままの `git push`**(`git -C "$DIR" push`・行き先のブランチを確かめられない形)→ 止まる。**リポの中で、ブランチを名指しして** `git push origin <作業ブランチ名>`(★`-u` は付けない=鉄則7-⓪。上流の設定を書くと、囲いが `.git/config` を塞いでいるPCで止まる)。
174
+ - **行き先のブランチを確かめられない `git push`**(作業場所が変数のままの `git -C "$DIR" push`・`git push` とだけ打つ形・保護ブランチを狙う形)→ 止まる。**リポの中で、ブランチを名指しして** `git push origin <作業ブランチ名>`(★`-u` は付けない=鉄則7-⓪。上流の設定を書くと、囲いが `.git/config` を塞いでいるPCで止まる)。
175
+ - ★**止まるのは「行き先が分からない形」と「保護ブランチを狙う形」**(実測 便 6e8d85e6)。場所を指す旗を付けても枝を名指しした形(`git -C <場所> push origin <作業ブランチ名>`)は止まらないので、「場所を指す旗が付いているか」で安全を判断しない。
176
+ - ★**強い上書き(`--force`/`-f`/`--force-with-lease`/先頭が `+` の参照指定)は、旗の位置を問わず止まる。** 旗を末尾や中間に置く形・短い旗をまとめた形(`-fu`)・`git` と `push` の間に場所や設定を指す旗を挟む形・`+` を引用符で囲う形・つないだ行や包みの中に書いた形も、すべて止まる(便 e619bd86 で語の並びを見る判定に替えた。それまでは挟む形と引用符で囲う形が素通りしていた=実地 便 6e8d85e6)。**そもそも強い上書きは打たない**(便の本文の「force-push と reset はしない」)。
174
177
 
175
178
  ## 便の終え方
176
179