@tyhld/conductor 0.12.41 → 0.12.42
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
|
@@ -155,6 +155,15 @@ _GATE_NO_ANSWER_MESSAGE = (
|
|
|
155
155
|
)
|
|
156
156
|
_GATE_REFUSED_MESSAGE = 'お客さまが「いいえ」を選んだので、実行しませんでした。'
|
|
157
157
|
|
|
158
|
+
# 職人が人に答えを求める道具(Claude Code 組込み)。
|
|
159
|
+
ASK_TOOL_NAME = 'AskUserQuestion'
|
|
160
|
+
# ★自走モードでは答えを返す道がまだ無い(便 8c506176 で作る)。答えなしで進ませない。
|
|
161
|
+
_ASK_NO_ANSWER_MESSAGE = (
|
|
162
|
+
'人の答えが無いので進めません(自走モードでは、この質問に答える人がいません)。'
|
|
163
|
+
'勝手に決めず、質問の中身(質問文と選択肢)を報告に書いて blocked として報告し'
|
|
164
|
+
'(conductor_complete も outcome=blocked で呼ぶ)、そこで止まって人の答えを待ってください。'
|
|
165
|
+
)
|
|
166
|
+
|
|
158
167
|
|
|
159
168
|
def ask_gate(data, gate, cfg, datalink=hook_datalink, wait_sec=None):
|
|
160
169
|
"""関門を管制の2択で聞き、答えが出るまで待つ(便 ab6e36ca・入口の番人から呼ぶ)。
|
|
@@ -232,6 +241,13 @@ def decide_autorun(data, verdict, gate, cfg=None, datalink=hook_datalink, codex=
|
|
|
232
241
|
"""
|
|
233
242
|
if verdict.get('decision') == policy.DENY:
|
|
234
243
|
return _deny(f'安全のため実行しません({verdict.get("reason", "危険な操作")})。')
|
|
244
|
+
if data.get('tool_name') == ASK_TOOL_NAME:
|
|
245
|
+
# ★職人の質問(AskUserQuestion)は、答えが無いまま通さない(便 7aefb5e1・えふさん確定 2026-09-21)。
|
|
246
|
+
# 以前は下の allow に落ち、updatedInput に元の入力(答え無し)が入るため、0.3秒で
|
|
247
|
+
# 「The user did not answer the questions.」として職人へ返り、人の判断を飛ばして進みえた
|
|
248
|
+
# (2026-09-21 01:46 conductor「PR #194 を合流してよいか」3択・便 8c506176 の重大発見①)。
|
|
249
|
+
# ★管制へ上げて答えを返す本体は便 8c506176 の範囲(ここでは作らない)。
|
|
250
|
+
return _deny(_ASK_NO_ANSWER_MESSAGE)
|
|
235
251
|
if gate:
|
|
236
252
|
return decide_by_asking(data, verdict, gate, cfg, datalink, codex)
|
|
237
253
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""自走モードで職人の質問(AskUserQuestion)を「答えなし」で通さない検査(便 7aefb5e1)。
|
|
3
|
+
|
|
4
|
+
実行: python3 scripts/hooks/test_ask_no_answer.py
|
|
5
|
+
1件でも期待と違えば exit 1。
|
|
6
|
+
|
|
7
|
+
【何が起きていたか(便 8c506176 の重大発見①・実物 2026-09-21 01:46 conductor)】
|
|
8
|
+
職人が AskUserQuestion で「PR #194 を今すぐ合流してよいですか?」と3択で聞いた。自走モードの
|
|
9
|
+
PermissionRequest が関門でないのでその場で allow を返し、updatedInput に元の入力(答え無し)を入れた。
|
|
10
|
+
0.3秒後に職人へ返ったのは「The user did not answer the questions.」(answers={})=人の判断を飛ばして進みえた。
|
|
11
|
+
|
|
12
|
+
【何を守っているか】
|
|
13
|
+
(1) 自走モードの AskUserQuestion は allow にならない(deny)。答えの入っていない入力を返さない
|
|
14
|
+
(2) 職人へ返す文に「進めない」「blocked で報告」「止まって待つ」が入る
|
|
15
|
+
(3) 待たない・管制に頼らない:管制が無くても、登録が壊れても、同じ deny(時間切れで続行する道が無い)
|
|
16
|
+
(4) ほかの道具の扱いは変わらない(ふつうの命令は allow・関門は2択で聞く・危険は deny のまま)
|
|
17
|
+
(5) 従来どおり聞く動き(CC2_AUTORUN=0)は変えていない
|
|
18
|
+
★空振り確認:直す前の版(origin/main)で流すと (1)(2) が落ちること(報告に実測を書く)。
|
|
19
|
+
"""
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
24
|
+
|
|
25
|
+
import permission_request_hook as prh # noqa: E402
|
|
26
|
+
|
|
27
|
+
failures = []
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check(label, got, want):
|
|
31
|
+
ok = got == want
|
|
32
|
+
if not ok:
|
|
33
|
+
failures.append(f'{label}: got={got!r} want={want!r}')
|
|
34
|
+
print(f'{"OK " if ok else "NG "} {label:62} {str(got)[:40]}')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
CFG = {'url': 'https://example.invalid', 'token': 'dummy'}
|
|
38
|
+
|
|
39
|
+
# 01:46 に実際に届いた形(中身は短くした)。
|
|
40
|
+
ASK_INPUT = {'questions': [{
|
|
41
|
+
'question': 'PR #194 を今すぐ合流してよいですか?',
|
|
42
|
+
'header': '合流の可否',
|
|
43
|
+
'multiSelect': False,
|
|
44
|
+
'options': [{'label': '合流しない', 'description': '待つ'},
|
|
45
|
+
{'label': '今すぐ合流する', 'description': '配布も走る'}],
|
|
46
|
+
}]}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def event(tool, tin):
|
|
50
|
+
return {'hook_event_name': 'PermissionRequest', 'tool_name': tool, 'tool_input': tin,
|
|
51
|
+
'session_id': 's1', 'cwd': '/home/x/projects/demo'}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FakeDatalink:
|
|
55
|
+
def __init__(self, cfg=CFG, autorun=True, answer='allow', boom=False):
|
|
56
|
+
self.cfg, self.autorun, self.answer, self.boom = cfg, autorun, answer, boom
|
|
57
|
+
self.registered, self.waited, self.local = 0, 0, []
|
|
58
|
+
|
|
59
|
+
def autorun_enabled(self, env=None):
|
|
60
|
+
return self.autorun
|
|
61
|
+
|
|
62
|
+
def load_config(self):
|
|
63
|
+
return self.cfg
|
|
64
|
+
|
|
65
|
+
def record_autorun_local(self, tool, gate=None):
|
|
66
|
+
self.local.append((tool, gate))
|
|
67
|
+
|
|
68
|
+
def register(self, cfg, payload):
|
|
69
|
+
if self.boom:
|
|
70
|
+
raise RuntimeError('管制が壊れている')
|
|
71
|
+
self.registered += 1
|
|
72
|
+
return {'id': 'req-1', 'status': 'waiting'}
|
|
73
|
+
|
|
74
|
+
def wait_for_decision(self, cfg, request_id, max_wait_sec=None):
|
|
75
|
+
self.waited += 1
|
|
76
|
+
return self.answer
|
|
77
|
+
|
|
78
|
+
def mark_handoff(self, sid):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
def clear_handoff(self, sid):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def abandon(self, cfg, request_id):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def decision(out):
|
|
89
|
+
return (out or {}).get('hookSpecificOutput', {}).get('decision', {})
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
print('── (1)(2) 自走モードの AskUserQuestion は「答えなし」で通らない ' + '─' * 8)
|
|
93
|
+
fake = FakeDatalink()
|
|
94
|
+
d = decision(prh.decide(event('AskUserQuestion', ASK_INPUT), datalink=fake))
|
|
95
|
+
check('(1) 判定は deny', d.get('behavior'), 'deny')
|
|
96
|
+
check('(1) 答えの無い入力(updatedInput)を返さない', 'updatedInput' in d, False)
|
|
97
|
+
msg = d.get('message', '')
|
|
98
|
+
check('(2) 文に「進めません」', '進めません' in msg, True)
|
|
99
|
+
check('(2) 文に「blocked として報告」', 'blocked として報告' in msg, True)
|
|
100
|
+
check('(2) 文に「止まって」「待って」', '止まって' in msg and '待って' in msg, True)
|
|
101
|
+
check('(2) 文に「勝手に決めず」', '勝手に決めず' in msg, True)
|
|
102
|
+
check('(2) 文に「質問の中身」を報告に書く', '質問の中身' in msg, True)
|
|
103
|
+
check('(1) 「聞かずに通した」と記録しない(allow の1行を書かない)', fake.local, [])
|
|
104
|
+
|
|
105
|
+
print('── (3) 待たない・管制に頼らない(時間切れで続行する道が無い) ' + '─' * 8)
|
|
106
|
+
check('(3) 管制へ登録しない', fake.registered, 0)
|
|
107
|
+
check('(3) 答えを待たない', fake.waited, 0)
|
|
108
|
+
for label, dl in [('管制が未設定', FakeDatalink(cfg=None)), ('管制が壊れている', FakeDatalink(boom=True))]:
|
|
109
|
+
check(f'(3) {label}でも deny', decision(prh.decide(event('AskUserQuestion', ASK_INPUT), datalink=dl)).get('behavior'), 'deny')
|
|
110
|
+
check('(3) Codex から呼ばれても deny',
|
|
111
|
+
decision(prh.decide(event('AskUserQuestion', ASK_INPUT), datalink=FakeDatalink(), codex=True)).get('behavior'), 'deny')
|
|
112
|
+
|
|
113
|
+
print('── (4) ほかの道具の扱いは変わらない ' + '─' * 30)
|
|
114
|
+
fake = FakeDatalink()
|
|
115
|
+
d = decision(prh.decide(event('Bash', {'command': 'npx tsc --noEmit'}), datalink=fake))
|
|
116
|
+
check('(4) ふつうの命令は allow', d.get('behavior'), 'allow')
|
|
117
|
+
check('(4) ふつうの命令は入力をそのまま返す', d.get('updatedInput'), {'command': 'npx tsc --noEmit'})
|
|
118
|
+
check('(4) ふつうの命令は聞かずに通した記録を1行', fake.local, [('Bash', None)])
|
|
119
|
+
d = decision(prh.decide(event('Read', {'file_path': '/home/x/projects/demo/README.md'}), datalink=FakeDatalink()))
|
|
120
|
+
check('(4) 読むだけの道具は allow', d.get('behavior'), 'allow')
|
|
121
|
+
fake = FakeDatalink(answer='allow')
|
|
122
|
+
d = decision(prh.decide(event('Bash', {'command': 'npm publish'}), datalink=fake))
|
|
123
|
+
check('(4) 関門は管制の2択で聞く(登録した)', fake.registered, 1)
|
|
124
|
+
check('(4) 関門は人の「はい」で allow', d.get('behavior'), 'allow')
|
|
125
|
+
d = decision(prh.decide(event('Bash', {'command': 'cat .env'}), datalink=FakeDatalink()))
|
|
126
|
+
check('(4) 危険(秘密の読み出し)は deny のまま', d.get('behavior'), 'deny')
|
|
127
|
+
check('(4) 危険の文は今までどおり', d.get('message', '').startswith('安全のため実行しません'), True)
|
|
128
|
+
|
|
129
|
+
print('── (5) 従来どおり聞く動き(CC2_AUTORUN=0)は変えていない ' + '─' * 11)
|
|
130
|
+
fake = FakeDatalink(autorun=False, answer='deny')
|
|
131
|
+
d = decision(prh.decide(event('AskUserQuestion', ASK_INPUT), datalink=fake))
|
|
132
|
+
check('(5) 聞く動きでは管制へ登録する(今までどおり)', fake.registered, 1)
|
|
133
|
+
check('(5) 人の「いいえ」で deny(今までどおり)', d.get('behavior'), 'deny')
|
|
134
|
+
|
|
135
|
+
print()
|
|
136
|
+
if failures:
|
|
137
|
+
print(f'NG {len(failures)} 件')
|
|
138
|
+
for f in failures:
|
|
139
|
+
print(' ', f)
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
print('すべて期待どおり')
|