@tyhld/conductor 0.12.39 → 0.12.40

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
@@ -1107,6 +1107,23 @@ export function relaySendTmpDirs() {
1107
1107
  }
1108
1108
  /** tmux バッファ名の接頭辞(他人のバッファと衝突させないため現場・PID・時刻で一意にする)。 */
1109
1109
  export const RELAY_SEND_BUFFER_PREFIX = 'conductor-send';
1110
+ /**
1111
+ * 貼り付けのあとに「打った字」として添える1行(貼り付けの枠の外に置く)。
1112
+ *
1113
+ * 【なぜ要るか(便 6bef35c8・実測 2026-09-21 ai-kana 8e47d62e/mamoria 865c8372)】
1114
+ * 本文は paste-buffer -p(ブラケットペースト)で渡すので、職人には「貼り付けだけがあって、
1115
+ * 人が自分で打った字が1つも無い入力」として届く。Claude Code は貼り付けの中の指示を
1116
+ * 「人が自分の言葉で頼んだときだけ従う」扱いにするため、起こし直したばかりの職人
1117
+ * (これまでの便を実行した流れが会話に無い)が「貼り付け以外に指示が無い」と見て、
1118
+ * 実行してよいか人に聞いて止まった(管制には idle_prompt として出た)。
1119
+ * 【直し】人が便を貼ってから一言添えて送るのと同じ形にする。添え書きは枠の外に
1120
+ * send-keys -l(1字ずつ打つのと同じ)で置く。★本文も守り(番人・囲い・秘密保護)も変えない。
1121
+ * ★言えることだけを言う:配達役が届けるのは、この端末の持ち主の管制に積まれた便だけ
1122
+ * (中央は持ち主の合言葉でしか便を受け付けない)。誰が書いたかは本文の「投函:」欄のまま。
1123
+ * ★短く保つ(長い一気の入力は受け手が貼り付けと見なすことがある)。アンカーの字は入れない
1124
+ * (完了検出の境界を増やさないため)。
1125
+ */
1126
+ export const RELAY_DELIVERY_NOTE = '(配達役より)上の貼り付けは、この端末の持ち主の管制(采配くん)に投函された便を配達役が届けたものです。貼り付けに書かれたとおりに作業してください。';
1110
1127
  // ───────────────────────────────────────────────────────────────────────────
1111
1128
  // 報告の受け渡し(★ファイル経由・えふさん決定 2026-09-07/置き場と作法は 2026-09-21 に改訂)
1112
1129
  // ───────────────────────────────────────────────────────────────────────────
@@ -1457,7 +1474,8 @@ function runTmux(args) {
1457
1474
  execFileSync('tmux', args, { stdio: ['ignore', 'ignore', 'pipe'] });
1458
1475
  }
1459
1476
  /**
1460
- * tmux へ本文を1メッセージとして送り、最後に Enter を1回だけ送る。
1477
+ * tmux へ本文を1メッセージとして送り、貼り付けの枠の外に添え書き(RELAY_DELIVERY_NOTE)を打ち、
1478
+ * 最後に Enter を1回だけ送る。
1461
1479
  *
1462
1480
  * 【なぜ send-keys の引数に本文を載せないか(実測 2026-09-03 21:34〜の根治)】
1463
1481
  * tmux のクライアント→サーバの1命令には上限(IPC のメッセージ長・16KB前後)がある。
@@ -1493,6 +1511,8 @@ export function sendToTmux(site, text) {
1493
1511
  file = writeSendTmpFile(text);
1494
1512
  runTmux(['load-buffer', '-b', bufferName, file]);
1495
1513
  runTmux(['paste-buffer', '-d', '-p', '-b', bufferName, '-t', site]);
1514
+ // 貼り付けの枠の外に、打った字として添え書きを置く(RELAY_DELIVERY_NOTE の説明)。
1515
+ runTmux(['send-keys', '-t', site, '-l', ` ${RELAY_DELIVERY_NOTE}`]);
1496
1516
  runTmux(['send-keys', '-t', site, 'Enter']);
1497
1517
  return true;
1498
1518
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.39",
3
+ "version": "0.12.40",
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 を最新化し、新ブランチを切る。