@tyhld/conductor 0.12.40 → 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/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
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
|
1045
|
-
//
|
|
1046
|
-
|
|
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。送らない側に倒すため)
|
|
@@ -2462,7 +2496,7 @@ export async function relayTick(cfg, state, hooks) {
|
|
|
2462
2496
|
// 画面にエコーされたアンカー行を「今回ジョブの境界」として記録し、完了検出は
|
|
2463
2497
|
// アンカーより下=今回ジョブの出力だけを見る(churn・過去ジョブ DONE に強い)。
|
|
2464
2498
|
const anchor = buildAnchor(cmd.id);
|
|
2465
|
-
const text = buildSendText(cmd.body, cfg.site, anchor);
|
|
2499
|
+
const text = buildSendText(cmd.body, cfg.site, anchor, kindOf(cfg));
|
|
2466
2500
|
if (!sendToTmux(cfg.site, text)) {
|
|
2467
2501
|
// 送信失敗(sendToTmux は内部で例外も握って false を返す)。status は queued のまま据え置く。
|
|
2468
2502
|
// 失敗の理由(tmux の stderr)は sendToTmux が直前の行に出している。
|
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('すべて期待どおり')
|
|
@@ -68,3 +68,29 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
|
|
|
68
68
|
- **なぜ。** 雛形(`.env.example`)は普通は秘密値を置かないが、「exampleだから常に安全」と番人へ例外を作ると、将来誤って実値が書かれたときの穴になる(利便性より安全側・えふさん確定)。
|
|
69
69
|
- **雛形(`.env.example` など)を直すときの正しい形=「案を作る→人が1回で反映」。** 職人は変更後の中身を**リポジトリの外(`/tmp/claude-1000/` の下)**に作り、報告に次の2つを載せる。① 変更の**差分**(何を足す/消すか)。② 人が**素のターミナルで1回貼るだけで反映できる1つのコマンド**(例:`cp /tmp/claude-1000/<下書き> <リポ>/.env.example`)。★このコマンドは**人が自分のターミナルで**打つ(職人の番人は人の端末には掛からない)。職人が `.env.example` を直接書き換えようとしても番人が止めるので、**回避せず、この形で案を出す**。
|
|
70
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)を英語ではなく日本語で、「プルリクエストを作成する」「テストを実行する」のように「何をするか」が一目で分かる簡潔な形(〜を作成する/実行する/削除する 等)で書く。この説明は管制の確認カードにそのまま表示され、非エンジニアのお客さまが読んで許可を判断する。
|