@tyhld/conductor 0.12.88 → 0.12.90

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.
@@ -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
+ }
@@ -3,10 +3,17 @@
3
3
 
4
4
  【いつ走るか】職人が応答を返し終えた瞬間(=入力待ちに入る瞬間)。
5
5
 
6
- 【何をするか】
7
- 紐づく指示(ConductorCommand)が【まだ終わっていない】、かつ最後の発言が【人の判断を
8
- 求める問いかけ】のときだけ、その発言を kind='message' として管制へ送る。
9
- 管制側ではフックが待っている間だけカードが出る。
6
+ 【何をするか】★2つの役目を持つ。順番は「① 終わりの関門 → ② 確認カード」ではなく
7
+ 「① 人の問いかけなら確認カード → ② そうでなければ終わりの関門」。人の判断が先。
8
+
9
+ ① 確認カード(前からの役目)… 紐づく指示(ConductorCommand)が【まだ終わっていない】、
10
+ かつ最後の発言が【人の判断を求める問いかけ】のときだけ、その発言を kind='message' として
11
+ 管制へ送る。管制側ではフックが待っている間だけカードが出る。
12
+
13
+ ② ★便の終わりの関門(便 7aa63a97 で足した)… 人への問いかけではない手番の終わりでは、
14
+ 【実物】を見て「まだ残っている」なら `{"decision":"block","reason":…}` を返して続けさせる。
15
+ 見るのは PR・報告ファイル・背景に回した待ちの3つだけで、職人の言葉は読まない。
16
+ 判定と回数の上限は finish_gate.py にある(そこの説明文に実地の3件と理由を書いた)。
10
17
 
11
18
  【★カードを出す条件は2つ(両方満たしたときだけ出す)】
12
19
  (1) 紐づく指示がまだ終わっていないこと
@@ -40,6 +47,7 @@ import sys
40
47
 
41
48
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
42
49
 
50
+ import finish_gate # noqa: E402
43
51
  import hook_datalink # noqa: E402
44
52
  import summary_ja # noqa: E402
45
53
 
@@ -223,6 +231,86 @@ def build_payload(data, command_id):
223
231
  }
224
232
 
225
233
 
234
+ # ─────────────────────────────────────────────────────────────────────────────
235
+ # ★便の終わりの関門(finish_gate.py の判定へ、実物を集めて渡すだけの層)
236
+ # ─────────────────────────────────────────────────────────────────────────────
237
+ def load_job_body(site, env=None):
238
+ """中継が残した「いま走っている便」の本文(無ければ空)。★便の本文だけを読む。"""
239
+ env = os.environ if env is None else env
240
+ if not site:
241
+ return ''
242
+ xdg = (env.get('XDG_STATE_HOME') or '').strip()
243
+ root = xdg if xdg else os.path.join((env.get('HOME') or os.path.expanduser('~')), '.local', 'state')
244
+ for name in (f'active-job-{site}.json', f'last-job-{site}.json'):
245
+ try:
246
+ with open(os.path.join(root, 'conductor', name), encoding='utf-8') as f:
247
+ got = json.load(f)
248
+ except Exception: # noqa: BLE001
249
+ continue
250
+ if isinstance(got, dict) and isinstance(got.get('body'), str):
251
+ return got['body']
252
+ return ''
253
+
254
+
255
+ def gate_enabled(env=None):
256
+ """関門を効かせる機体か。★画面なし方式のときだけ(対話の画面は1文字も変えない)。
257
+
258
+ 対話の画面では「手番の終わり」は人への受け渡しなので、止めると人と話せなくなる。
259
+ 印が無い機体では何もしない(fail open=いまと同じ動き・退行しない)。
260
+ """
261
+ env = os.environ if env is None else env
262
+ return (env.get('CONDUCTOR_RUNNER_MODE') or '').strip().lower() == 'headless'
263
+
264
+
265
+ def collect_residues(data, command_id, env=None):
266
+ """残っている物を【実物】で集める({種類: 理由})。読めないものは「無い」側へ倒す。"""
267
+ env = os.environ if env is None else env
268
+ site = (env.get('CONDUCTOR_SITE') or '').strip()
269
+ work_dir = (env.get('CONDUCTOR_WORK_DIR') or '').strip() or (data.get('cwd') or '')
270
+ residues = {}
271
+
272
+ # (2) 報告 … 置き場の実物を見る。
273
+ residues['report'] = finish_gate.report_residue(
274
+ finish_gate.read_report_text(command_id, env))
275
+
276
+ # (3) 背景に回した待ち … 会話の記録の実物を見る。
277
+ residues['background'] = finish_gate.background_residue(
278
+ finish_gate.outstanding_background_tasks(data.get('transcript_path')))
279
+
280
+ # (1) PR … 便の本文が「合流は人が行う/合流しない」と言っていれば見ない(ADR-007)。
281
+ if not finish_gate.human_merge_requested(load_job_body(site, env)) and work_dir:
282
+ residues['pr'] = finish_gate.open_pr_residue(
283
+ finish_gate.read_open_prs(work_dir, finish_gate.current_branch(work_dir)))
284
+ return {k: v for k, v in residues.items() if v}
285
+
286
+
287
+ def finish_gate_block(data, env=None):
288
+ """関門の判定。続けさせるなら職人へ返す文、終わらせるなら None。
289
+
290
+ ★止めるのは「実物が残っていて、かつ回数の上限に達していない」ときだけ。
291
+ 上限に達したら止めず、理由を控えに残す(管制へ「途中で終わった」として届く)。
292
+ """
293
+ env = os.environ if env is None else env
294
+ if not gate_enabled(env):
295
+ return None
296
+ command_id = find_command_id(data.get('transcript_path'))
297
+ if not command_id:
298
+ return None # 便ではない(対話の作業・自己診断)→ 止めない
299
+ residues = collect_residues(data, command_id, env)
300
+ result = finish_gate.decide(
301
+ residues,
302
+ finish_gate.load_counts(command_id, env),
303
+ cap=finish_gate.cap_per_kind(env),
304
+ )
305
+ if result['block']:
306
+ finish_gate.save_counts(command_id, result['counts'], env)
307
+ return finish_gate.build_reason(result)
308
+ if result['kinds']:
309
+ # 上限に達した=これ以上止めない。理由だけ残して終わらせる。
310
+ finish_gate.save_residue(command_id, finish_gate.build_residue_record(result), env)
311
+ return None
312
+
313
+
226
314
  def run(data, cfg=None, datalink=hook_datalink):
227
315
  """返り値: 送った確認のID / None(送らなかった)。"""
228
316
  message = data.get('last_assistant_message')
@@ -264,11 +352,33 @@ def main() -> int:
264
352
  data = json.loads(raw) if raw.strip() else {}
265
353
  except Exception: # noqa: BLE001
266
354
  return 0
355
+
356
+ # ① 人が答えるべき問いかけで終わった手番は、前からの確認カードの経路だけを通す。
357
+ # ★関門で止めると人の答えが流れる(ADR-024 §3 の取りこぼしを作らない)。
358
+ message = data.get('last_assistant_message')
359
+ asks_human = isinstance(message, str) and needs_human_answer(message)
360
+ if asks_human:
361
+ try:
362
+ run(data)
363
+ except Exception: # noqa: BLE001
364
+ pass
365
+ return 0
366
+
367
+ # ② 便の終わりの関門。残っていれば続けさせる。
368
+ # ★ここで何が起きても便を壊さない(例外は握って、いまと同じ「何もしない」へ倒す)。
369
+ try:
370
+ reason = finish_gate_block(data)
371
+ except Exception: # noqa: BLE001
372
+ reason = None
373
+ if reason:
374
+ print(json.dumps({'decision': 'block', 'reason': reason}, ensure_ascii=False))
375
+ return 0
376
+
267
377
  try:
268
378
  run(data)
269
379
  except Exception: # noqa: BLE001
270
380
  pass
271
- return 0 # ★Stop フックは会話を止めない(何があっても exit 0・出力なし)
381
+ return 0 # ★止めないときは今までどおり(exit 0・出力なし)
272
382
 
273
383
 
274
384
  if __name__ == '__main__':