対象仕様: Brain向け作業記録インターフェース仕様書
対象 issue: #50 Brain の issue 操作抽象化: gitea 依存を隠す
ステータス: Draft
この文書は、work_record capability の Phase 1 を実装するための実務手順を定義する。
仕様書は「何を実現するか」を定義する。
本書は「どの順で、どのファイルを、どの粒度で変更するか」を定義する。
Phase 1 のゴールは、Brain に Git / Gitea の低レベル操作を見せず、次の Brain-facing API で作業を進められるようにすることである。
work_session_start(project_root)
work_start(issue_id?)
work_observe()
work_diff(path?, level?, record_id?)
work_history(n?, path?)
work_record(message, issue_ids?)
work_publish()
work_finish()
issue_read(issue_id, last_n_comments?)
issue_create(title, body?, label_names?)
issue_comment(issue_id, body)
issue_delete_comment(issue_id, comment_id, reason?)
issue_update_title(issue_id, title)
issue_update_body(issue_id, body)
issue_reopen(issue_id)
issue_close(issue_id)
Phase 1 の最重要方針は、Brain に見える公開面から git_* / gitea_* を外すことである。
ただし既存の git_worker / gitea_issue_worker は捨てない。
これらは internal / worker adapter として残し、Brain-facing API から内部 helper 経由で呼ぶ。
Phase 1 では AI による要約や自動判断を入れない。
work_record() のメッセージ自動生成はしないwork_finish(auto=True) は作らない抽象クラスや複数 backend 対応は Phase 2 でよい。
Phase 1 では、関数またはモジュール単位で次の内部境界を置く。
WorkRecord backend:
observe
record
publish
Issue backend:
read
comment
create
close
Phase 1 の推奨案は、既存の 2 つの MCP エントリポイントを役割分離することである。
butler/mcp_facade.py Brain-facing tools only
butler/worker_mcp.py Worker/internal tools
butler/mcp_facade.py は Brain が接続する公開面とし、work_* / issue_* のみを登録する。
butler/worker_mcp.py は Worker / internal 向け公開面とし、既存の git_* / gitea_* を残す。
実装しやすくするため、登録処理は関数へ分離する。
register_brain_tools(server)
register_worker_tools(server)
Phase 1 では、Brain が接続する MCP から git_* / gitea_* が見えない状態を必須条件にする。
移行期間に互換性が必要な場合でも、Brain-facing と Worker-facing は別エントリポイントまたは明示設定で切り替える。
注意:
既存テストは git_* / gitea_* が mcp_facade.py から見える前提の可能性がある。
公開面分離を行う場合、テストも Brain-facing / Worker-facing に分けて更新する。
仕様では project_root と default_issue を MCP connection スコープで保持する。
実装前に FastMCP で connection-local state を扱えるか確認する。
確認観点:
Context から接続単位の識別子を得られるかFastMCP の lifespan / request context に state を置けるかconnection-local state が難しい場合は、Brain には見せない内部 session key 方式にする。
work_session_start:
内部 session key を生成または更新する
work_* / issue_*:
Context 等から内部 session key を特定する
Brain-facing API に session_id は出さない
禁止:
接続元 IP + timestamp のような不安定な識別子で session を特定しない。
MCP connection スコープで保持する状態は最小限にする。
WorkSessionState:
project_root: str | None
owner: str | None
repo: str | None
default_issue: int | None
work_session_start(project_root) は次を行う。
project_root を絶対パスとして検証するowner / repo 解決は、Phase 1 では現行プロジェクト前提の簡易実装でよい。
将来は Git remote URL や Butler 設定から解決する。
work_start() を実装するwork_start(issue_id?) は、session state の default_issue を更新するだけの軽量な操作である。
挙動:
issue_id が指定された場合は整数として検証するdefault_issue に保存するdefault_issue を返すwork_start は作業をロックしない。
work_record(issue_ids=[...]) でいつでも上書きでき、issue_ids=[] で紐付けなしを明示できる。
Brain-facing tool から直接 git_* / gitea_* を呼ばず、内部 helper を経由する。
推奨ファイルは次の 2 つに固定する。
butler/work_record_backend.py
butler/issue_backend.py
capability 配下の butler/backends/... は Phase 2 以降、backend が増えた時に検討する。
Phase 1 の関数例:
observe_work(project_root: str) -> dict
record_work(project_root: str, message: str, issue_ids: list[int] | None) -> dict
publish_work(project_root: str) -> dict
read_issue(project_root: str, issue_id: int, last_n_comments: int | None) -> dict
comment_issue(project_root: str, issue_id: int, body: str) -> dict
update_issue_body(project_root: str, issue_id: int, body: str) -> dict
create_issue(project_root: str, title: str, body: str | None, label_names: list[str] | None) -> dict
reopen_issue(project_root: str, issue_id: int) -> dict
close_issue(project_root: str, issue_id: int) -> dict
実装上は既存の execute_task() と TaskContract を使い、git_commit / git_push / read_from_gitea / record_to_gitea intent へ委譲してよい。
work_observe() を実装するwork_observe() は最初に実装する価値が高い。
Brain の入口であり、以後の作業誘導の司令塔になるためである。
判定に使う情報:
git status --porcelain 相当の変更有無Phase 1 の未公開記録判定は簡易でよい。
例:
git rev-list --count @{u}..HEAD
upstream がない場合は、現在 branch に未公開記録がある可能性として扱うか、unknown / blocked で返す。
work_observe() は仕様書の work_state を返す。
uninitialized
clean_idle
clean_with_active_issue
dirty
recorded_unpublished
dirty_and_unpublished
blocked
unknown
required_action は必ず構造化する。
{
"tool": "work_record",
"reason": "未記録の変更があります",
"required_args": ["message"]
}
clean_idle は required_action: null とし、available_next を返す。
work_diff() / work_history() を実装する#69 の方針により、work_observe() は diff 本文や履歴詳細を返さない。
その代わりに、Brain が明示的に必要とした時だけ呼ぶ補助ツールを用意する。
work_diff(path?, level?, record_id?):
level="stat" をデフォルトにするlevel="full" は明示指定時だけ返すrecord_id 指定時は特定 work_record の詳細確認として扱うwork_history(n?, path?):
git log 代替として実装するn 省略時は 10 件程度の小さい固定値にするpath 指定時は指定ファイルに関係する記録に絞るrecord_id / message / created_at を含めるgit_equivalents では log を work_history の available な proper tool として扱い、hook / work_vcs から work_history() へ誘導する。
work_record() を実装するwork_record(message, issue_ids?) は、Phase 1 では message 必須。
挙動:
issue_ids の扱いを解決するrecord() を呼ぶ未記録変更がない場合は backend helper の record() を呼ばない。
共通エラー形式に従い、status=error、error_code=policy_blocked、hint="記録対象の未記録変更がありません。work_observe で現在の状態を確認してください" を返す。
issue_ids 解決:
issue_ids omitted:
default_issue があれば [default_issue]
default_issue がなければ紐付けなしで正常実行する
issue_ids=[]:
明示的に紐付けなし
issue_ids=[...]:
指定 issue 群に紐付け
work_record 自体は policy_hint を返さない。
issue 紐付け方針の事前誘導は work_observe が担う。
Brain が work_record(message) を直接呼び、default issue が未設定の場合は、issue 紐付けなしの作業記録として正常実行する。
Phase 1 では、issue と記録のリンクは commit message への #<id> 付与または evidence に残す程度でよい。
Gitea 側への明示リンク API は Phase 2 以降でよい。
work_publish() を実装するwork_publish() は dirty 状態では実行しない。
挙動:
recorded_unpublished:
publish を実行する
dirty / dirty_and_unpublished:
status=error
error_code=policy_blocked
hint="先に work_record で作業を記録してください"
clean_idle / clean_with_active_issue:
status=ok
summary="共有済みです"
backend helper の publish() は既存 git_push intent を使ってよい。
Brain-facing issue tools:
issue_read(issue_id, last_n_comments?)
issue_create(title, body?, label_names?)
issue_comment(issue_id, body)
issue_delete_comment(issue_id, comment_id, reason?)
issue_update_title(issue_id, title)
issue_update_body(issue_id, body)
issue_reopen(issue_id)
issue_close(issue_id)
issue_read:
last_n_comments 省略時は本文のみlast_n_comments=0 も本文のみlast_n_comments > 0 の場合だけコメントを末尾 N 件に絞るissue_create:
title は必須body は任意label_names は任意label_ids は Brain-facing では受けないbody 指定時の構造化本文契約は Brain-facing 仕様に明記するinvalid_body / need_input として不足項目を返すPhase 1 で label name 解決が重い場合は、label_names を受けるが未対応時は policy_blocked または backend_error で返してよい。
ただし仕様としては ID を Brain に要求しないことを守る。
issue_delete_comment:
issue_id と comment_id は必須reason は任意delete_comment() を呼ぶdelete_issue_comment に委譲してよいissue_delete_comment として公開するnot_found または backend_error で返すissue_update_title:
issue_id と title は必須invalid_title または need_input で返すupdate_title() を呼ぶupdate_issue(title=...) に委譲してよいissue_update ではなく、タイトル更新に絞った issue_update_title として公開するissue_update_body:
issue_id と body は必須invalid_body または need_input で返すupdate_body() を呼ぶupdate_issue(body=...) に委譲してよいissue_update ではなく、本文更新に絞った issue_update_body として公開するtitle / state / labels は受けない。必要になった場合は目的別 API を追加するissue_reopen:
issue_id は必須reopen() を呼ぶupdate_issue(state="open") に委譲してよいissue_update ではなく、目的が明確な issue_reopen として公開するissue_close:
issue_id は必須close() を呼ぶstatus=ok とし、closed 後の issue 情報を data.issue に返すwork_finish の提案を受けて呼ぶことを推奨するが、Brain が明示判断して直接呼んでもよいwork_finish() を実装するwork_finish() のロジックは work_record_backend.finish_work() に実装し、MCP tool からはその関数を呼ぶ。これにより unit test から同じ関数を直接テストできる。
finish_work() は実行ではなく提案を返す。
評価順:
1. 未記録変更がある
→ work_record を促す
→ 以降のチェックをスキップ
2. upstream 未設定(_unpublished_count() が None)
→ configure_upstream を案内する
→ 以降のチェックをスキップ
※ 公開状態が確認できないため、issue_close を提案しない
3. 未公開記録がある
→ work_publish を促す
→ 以降のチェックをスキップ
4. ここまで到達したら、未記録・未公開はない
default issue があれば pending_actions を並行評価する
- issue_comment を追加する
- issue_close も追加する
default issue がなければ pending_actions: [] で ok を返す
ステップ 1・2・3 は早期リターンする。
ステップ 4 では issue_comment と issue_close は排他ではなく、同じ pending_actions に並べて返す。
実際にコメントするか、閉じるか、その順序をどうするかは Brain が判断する。
default issue がない場合は、issue に対する提案を作れないため pending_actions: [] とする。
戻り値例:
{
"status": "ok",
"summary": "作業は記録・公開済みです。以下を確認してください",
"data": {
"pending_actions": [
{
"action": "issue_comment",
"reason": "作業要約を issue #50 に残すことを推奨します",
"required_args": ["issue_id", "body"]
},
{
"action": "issue_close",
"reason": "完了条件を満たしていそうです。issue #50 を閉じますか?",
"required_args": ["issue_id"]
}
]
}
}
default issue がない場合:
{
"status": "ok",
"summary": "作業は記録・公開済みです。追加で提案する issue 操作はありません",
"data": {
"pending_actions": []
}
}
Brain-facing MCP の instructions を BWR 用に更新する。
最低限入れる文言:
Butler は作業記録システムです。
セッション開始時は work_session_start(project_root) を呼んでください。
作業開始時や状態確認時は、まず work_observe() を呼んでください。
差分を確認したいときは work_diff() を使ってください。
履歴を確認したいときは work_history() を使ってください。
作業内容の保存には work_record() を使ってください。
記録済み作業の共有には work_publish() を使ってください。
ユーザーが「確定して」「公開して」「反映して」と依頼した場合は、まず work_observe() で状態を確認し、必要に応じて work_record() を促したうえで work_publish() を実行してください。
issue 操作には issue_read / issue_create / issue_comment / issue_delete_comment / issue_update_title / issue_update_body / issue_reopen / issue_close を使ってください。
issue_create の body は、現在の状態・次にすること・ブロック要因を含む構造化本文にしてください。不足時は Butler が追加情報を求めます。
既存 issue の本文を清書・補完する場合は issue_update_body を使ってください。
低レベル backend 操作は Brain 向けには公開されません。
次の文書・resource から Brain-facing の直接 git_* / gitea_* 呼び出し例を除去する。
docs/Butler利用ガイド.md
docs/黒執事開発中用手順書.md
butler/mcp_facade.py の INSTRUCTIONS / USAGE_GUIDE / SKILLS_GUIDE
internal / worker reference では git_* / gitea_* の説明を残してよい。
追加候補:
tests/test_work_record_capability.py
テスト観点:
work_session_start が session state を初期化するwork_observe が uninitialized を返すwork_observe が dirty 状態を返すwork_observe が clean_idle を返すwork_observe が recorded_unpublished を返すwork_start が default issue を設定するwork_record が default issue を使うwork_record が default issue なしでは紐付けなしで正常実行するwork_record(issue_ids=[]) が紐付けなしを明示するwork_record が未記録変更なしでは error_code=policy_blocked を返すwork_publish が dirty 状態で policy_blocked を返すwork_publish が upstream 未設定で error_code=upstream_not_configured を返すwork_finish が pending_actions を返すwork_finish が upstream 未設定では configure_upstream を返し、issue_close を提案しないwork_finish が default issue なしの clean 状態で pending_actions: [] を返すissue_read(last_n_comments=3) が末尾 3 件だけ返すissue_delete_comment(issue_id, comment_id) が内部 delete_issue_comment に委譲するissue_delete_comment が存在しない comment に対して not_found または backend_error を返すissue_update_title(issue_id, title) が内部 update_issue(title=...) に委譲するissue_update_title(issue_id, title) が空タイトルを invalid_title または need_input で返すissue_update_title が存在しない issue_id に対して not_found または backend_error を返すissue_update_body(issue_id, body) が内部 update_issue(body=...) に委譲するissue_update_body(issue_id, body) が空本文を invalid_body または need_input で返すissue_update_body が存在しない issue_id に対して not_found または backend_error を返すissue_reopen(issue_id) が内部 update_issue(state="open") に委譲する既存の tests/test_mcp_facade.py に Brain-facing tool の公開面テストを追加する。
観点:
work_observe 等が含まれるissue_delete_comment が含まれるissue_update_body が含まれるgit_* / gitea_* prefix のツールが含まれないwork_session_start / work_observe が含まれるまず該当テストだけ実行する。
uv run python -m unittest tests.test_work_record_capability
uv run python -m unittest tests.test_mcp_facade
最後に全体を実行する。
uv run python -m unittest
既存の git_* / gitea_* をすぐ削除しない。
Brain-facing から見えないようにし、Worker-facing / internal adapter として残す。
Phase 1 では次を自動実行しない。
Brain-facing error は意味コードで返す。
conflict
auth_failed
not_found
policy_blocked
backend_error
upstream_not_configured
git push failed や exit code 1 をそのまま Brain-facing summary に出さない。
必要なら evidence / diagnostic-only 側に残す。
Brain-facing API に session_id を出さない。
FastMCP の制約で内部 session key が必要な場合でも、Brain には見せない。
pre-tool hook は、単純な git status だけでなく、前置修飾やシェル制御を伴う raw git も検出する。
最低限テストするケース:
git status
cd /tmp && git status
GIT_PAGER=cat git log
env git diff
sudo git commit
time git push
echo foo | git commit -F -
echo git && git status
echo git のように git が単なる引数や表示文字列として出るだけのケースは、実行コマンドとしての git と区別する。
実装完了時に次を確認する。
work_* / issue_* が呼べるgit_* / gitea_* が見えないgit_* / gitea_* が使えるwork_session_start 後、project_root を再指定せずに work_* / issue_* が動くwork_observe が仕様書の work_state を返すwork_record が issue_ids の省略 / 明示 / 空配列を区別するwork_record が未記録変更なしで error_code=policy_blocked を返すwork_publish が dirty 状態で policy_blocked を返すwork_publish が upstream 未設定で error_code=upstream_not_configured を返すwork_finish が pending_actions を返し、自動 close しないwork_finish が upstream 未設定では configure_upstream を返し、issue_close を提案しないwork_diff が Brain-facing ツールとして公開されるwork_history が Brain-facing ツールとして公開されるwork_history(n?, path?) が record_id / message / created_at を返すgit log が hook / work_vcs から work_history() へ誘導されるsudo git ... / env git ... / GIT_PAGER=cat git log / pipe 後段の git をブロックするissue_read はデフォルトでコメントを取得しないissue_read(last_n_comments=N) は末尾 N 件だけ返すissue_delete_comment(issue_id, comment_id) が Brain-facing ツールとして公開されるissue_delete_comment が存在しない comment に対して not_found または backend_error を返すissue_update_title(issue_id, title) が Brain-facing ツールとして公開されるissue_update_title(issue_id, title) は空タイトルを受け付けないissue_update_title が存在しない issue_id に対して not_found または backend_error を返すissue_update_body(issue_id, body) が Brain-facing ツールとして公開されるissue_update_body(issue_id, body) は空本文を受け付けないissue_update_body が存在しない issue_id に対して not_found または backend_error を返すissue_reopen(issue_id) が Brain-facing ツールとして公開されるinspect_runtime_config の Brain 向け出力に internal intent 名が出ないwork_session_start → work_observe を誘導するgit_* / gitea_* 例が除去されているuv run python -m unittest が通る