sibyl-cli 0.2.0__py3-none-any.whl
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.
- parallel_developer/__init__.py +5 -0
- parallel_developer/cli.py +649 -0
- parallel_developer/controller/__init__.py +1398 -0
- parallel_developer/controller/commands.py +132 -0
- parallel_developer/controller/events.py +17 -0
- parallel_developer/controller/flow.py +43 -0
- parallel_developer/controller/history.py +70 -0
- parallel_developer/controller/pause.py +94 -0
- parallel_developer/controller/workflow_runner.py +135 -0
- parallel_developer/orchestrator.py +1234 -0
- parallel_developer/services/__init__.py +14 -0
- parallel_developer/services/codex_monitor.py +627 -0
- parallel_developer/services/log_manager.py +161 -0
- parallel_developer/services/tmux_manager.py +245 -0
- parallel_developer/services/worktree_manager.py +119 -0
- parallel_developer/stores/__init__.py +20 -0
- parallel_developer/stores/session_manifest.py +165 -0
- parallel_developer/stores/settings_store.py +242 -0
- parallel_developer/ui/widgets.py +269 -0
- sibyl_cli-0.2.0.dist-info/METADATA +15 -0
- sibyl_cli-0.2.0.dist-info/RECORD +23 -0
- sibyl_cli-0.2.0.dist-info/WHEEL +4 -0
- sibyl_cli-0.2.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Controller command definitions and helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Awaitable, Callable, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class CommandSuggestion:
|
|
11
|
+
name: str
|
|
12
|
+
description: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CommandOption:
|
|
17
|
+
label: str
|
|
18
|
+
value: object
|
|
19
|
+
description: Optional[str] = None
|
|
20
|
+
display: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class CommandSpecEntry:
|
|
25
|
+
description: str
|
|
26
|
+
handler: Callable[[Optional[object]], Awaitable[None]]
|
|
27
|
+
options: Optional[List[CommandOption]] = None
|
|
28
|
+
options_provider: Optional[Callable[[], List[CommandOption]]] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_command_specs(
|
|
32
|
+
controller,
|
|
33
|
+
*,
|
|
34
|
+
flow_mode_cls,
|
|
35
|
+
boss_mode_cls,
|
|
36
|
+
merge_mode_cls,
|
|
37
|
+
) -> Dict[str, CommandSpecEntry]:
|
|
38
|
+
FlowMode = flow_mode_cls # alias for brevity
|
|
39
|
+
MergeMode = merge_mode_cls
|
|
40
|
+
specs = {
|
|
41
|
+
"/attach": CommandSpecEntry(
|
|
42
|
+
"tmux接続の方法を切り替える",
|
|
43
|
+
controller._cmd_attach,
|
|
44
|
+
options=[
|
|
45
|
+
CommandOption("auto - 自動でターミナルを開く", "auto", "自動でターミナルを開く"),
|
|
46
|
+
CommandOption("manual - コマンド入力で手動接続", "manual", "コマンド入力で手動接続"),
|
|
47
|
+
CommandOption("now - 即座にtmux attachを実行", "now", "即座にtmux attachを実行"),
|
|
48
|
+
],
|
|
49
|
+
),
|
|
50
|
+
"/boss": CommandSpecEntry(
|
|
51
|
+
"Bossの挙動を切り替える",
|
|
52
|
+
controller._cmd_boss,
|
|
53
|
+
options=[
|
|
54
|
+
CommandOption("skip - Boss評価をスキップ", boss_mode_cls.SKIP.value, "Boss評価をスキップ"),
|
|
55
|
+
CommandOption("score - 採点のみ実施", boss_mode_cls.SCORE.value, "採点のみ実施"),
|
|
56
|
+
CommandOption("rewrite - 採点後にBossが統合実装", boss_mode_cls.REWRITE.value, "採点後にBossが統合実装"),
|
|
57
|
+
],
|
|
58
|
+
),
|
|
59
|
+
"/flow": CommandSpecEntry(
|
|
60
|
+
"フロー自動化レベルを切り替える",
|
|
61
|
+
controller._cmd_flow,
|
|
62
|
+
options=[
|
|
63
|
+
CommandOption("manual - 採点段階への移行や採択を手動で行う", FlowMode.MANUAL.value, "採点段階への移行や採択を手動で行う"),
|
|
64
|
+
CommandOption("auto_review - 採点段階への移行は自動、採択は手動", FlowMode.AUTO_REVIEW.value, "採点段階への移行は自動、採択は手動"),
|
|
65
|
+
CommandOption("auto_select - 採点段階への移行は手動、採択は自動", FlowMode.AUTO_SELECT.value, "採点段階への移行は手動、採択は自動"),
|
|
66
|
+
CommandOption("full_auto - 採点段階への移行・採択まで自動", FlowMode.FULL_AUTO.value, "採点段階への移行・採択まで自動"),
|
|
67
|
+
],
|
|
68
|
+
),
|
|
69
|
+
"/merge": CommandSpecEntry(
|
|
70
|
+
"マージ方式を切り替える",
|
|
71
|
+
controller._cmd_merge,
|
|
72
|
+
options=[
|
|
73
|
+
CommandOption(
|
|
74
|
+
"manual - ホストが従来どおりマージ",
|
|
75
|
+
MergeMode.MANUAL.value,
|
|
76
|
+
"ホスト側がFast-Forwardなどを実行",
|
|
77
|
+
),
|
|
78
|
+
CommandOption(
|
|
79
|
+
"auto - エージェントがコミット・統合作業",
|
|
80
|
+
MergeMode.AUTO.value,
|
|
81
|
+
"採択エージェントにコミット/統合手順を送る",
|
|
82
|
+
),
|
|
83
|
+
],
|
|
84
|
+
),
|
|
85
|
+
"/parallel": CommandSpecEntry(
|
|
86
|
+
"ワーカー数を設定する",
|
|
87
|
+
controller._cmd_parallel,
|
|
88
|
+
options=[CommandOption(f"{n} - ワーカーを{n}人起動", str(n), f"ワーカーを{n}人起動") for n in range(1, 5)],
|
|
89
|
+
),
|
|
90
|
+
"/mode": CommandSpecEntry(
|
|
91
|
+
"実行対象を切り替える",
|
|
92
|
+
controller._cmd_mode,
|
|
93
|
+
options=[
|
|
94
|
+
CommandOption("main - メインCodexのみ稼働", "main", "メインCodexのみ稼働"),
|
|
95
|
+
CommandOption("parallel - メイン+ワーカーを起動", "parallel", "メイン+ワーカーを起動"),
|
|
96
|
+
],
|
|
97
|
+
),
|
|
98
|
+
"/resume": CommandSpecEntry(
|
|
99
|
+
"保存セッションを再開する",
|
|
100
|
+
controller._cmd_resume,
|
|
101
|
+
options_provider=controller._build_resume_options,
|
|
102
|
+
),
|
|
103
|
+
"/continue": CommandSpecEntry("ワーカーの作業を続行する", controller._cmd_continue),
|
|
104
|
+
"/log": CommandSpecEntry(
|
|
105
|
+
"ログをコピーや保存する",
|
|
106
|
+
controller._cmd_log,
|
|
107
|
+
options=[
|
|
108
|
+
CommandOption("copy - ログをクリップボードへコピー", "copy", "ログをクリップボードへコピー"),
|
|
109
|
+
CommandOption("save - ログをファイルへ保存", "save", "ログをファイルへ保存"),
|
|
110
|
+
],
|
|
111
|
+
),
|
|
112
|
+
"/commit": CommandSpecEntry(
|
|
113
|
+
"Gitコミットを操作する",
|
|
114
|
+
controller._cmd_commit,
|
|
115
|
+
options=[
|
|
116
|
+
CommandOption("manual - 現在の変更をコミット", "manual", "現在の変更をその場でコミット"),
|
|
117
|
+
CommandOption("auto - 自動コミットをON/OFF", "auto", "サイクル開始時に自動コミットをON/OFF"),
|
|
118
|
+
],
|
|
119
|
+
),
|
|
120
|
+
"/status": CommandSpecEntry(
|
|
121
|
+
"現在の状態を表示する",
|
|
122
|
+
controller._cmd_status,
|
|
123
|
+
),
|
|
124
|
+
"/scoreboard": CommandSpecEntry(
|
|
125
|
+
"最新スコアを表示する",
|
|
126
|
+
controller._cmd_scoreboard,
|
|
127
|
+
),
|
|
128
|
+
"/done": CommandSpecEntry("採点フェーズへ移行する", controller._cmd_done),
|
|
129
|
+
"/help": CommandSpecEntry("コマンド一覧を表示する", controller._cmd_help),
|
|
130
|
+
"/exit": CommandSpecEntry("CLI を終了する", controller._cmd_exit),
|
|
131
|
+
}
|
|
132
|
+
return specs
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Controller → UI 間で利用するイベント種別を定義。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ControllerEventType(str, Enum):
|
|
9
|
+
STATUS = "status"
|
|
10
|
+
LOG = "log"
|
|
11
|
+
LOG_COPY = "log_copy"
|
|
12
|
+
LOG_SAVE = "log_save"
|
|
13
|
+
SCOREBOARD = "scoreboard"
|
|
14
|
+
SELECTION_REQUEST = "selection_request"
|
|
15
|
+
SELECTION_FINISHED = "selection_finished"
|
|
16
|
+
PAUSE_STATE = "pause_state"
|
|
17
|
+
QUIT = "quit"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Workerフロー制御の補助機能をまとめたモジュール。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping, Optional, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .events import ControllerEventType
|
|
8
|
+
from ..orchestrator import CycleLayout, WorkerDecision
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
11
|
+
from . import CLIController
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WorkerFlowHelper:
|
|
15
|
+
"""Controllerのワーカーフロー関連処理をまとめるヘルパー。"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, controller: "CLIController", flow_mode_cls) -> None:
|
|
18
|
+
self._controller = controller
|
|
19
|
+
self._controller_event_type = ControllerEventType
|
|
20
|
+
self._flow_mode_cls = flow_mode_cls
|
|
21
|
+
|
|
22
|
+
def handle_worker_decision(
|
|
23
|
+
self,
|
|
24
|
+
fork_map: Mapping[str, str],
|
|
25
|
+
completion_info: Mapping[str, Any],
|
|
26
|
+
layout: CycleLayout,
|
|
27
|
+
) -> WorkerDecision:
|
|
28
|
+
c = self._controller
|
|
29
|
+
flow_mode = getattr(c, "_flow_mode", self._flow_mode_cls.MANUAL)
|
|
30
|
+
if flow_mode in {self._flow_mode_cls.AUTO_REVIEW, self._flow_mode_cls.FULL_AUTO}:
|
|
31
|
+
c._emit(
|
|
32
|
+
self._controller_event_type.LOG,
|
|
33
|
+
{
|
|
34
|
+
"text": f"[flow {c._flow_mode_display()}] ワーカーの処理が完了しました。採点フェーズへ進みます。",
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
return WorkerDecision(action="done")
|
|
38
|
+
|
|
39
|
+
command = c._await_worker_command()
|
|
40
|
+
if str(command).lower() == "continue":
|
|
41
|
+
instruction = c._await_continuation_instruction()
|
|
42
|
+
return WorkerDecision(action="continue", instruction=instruction)
|
|
43
|
+
return WorkerDecision(action="done")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Controller input/cycle history helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
8
|
+
from .controller import CLIController, Orchestrator, CycleLayout # noqa: F401
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HistoryManager:
|
|
12
|
+
"""Handles input history and cycle snapshots for CLIController."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._input_history: List[str] = []
|
|
16
|
+
self._history_cursor: int = 0
|
|
17
|
+
self._cycle_history: List[Dict[str, object]] = []
|
|
18
|
+
|
|
19
|
+
# Input history ---------------------------------------------------------
|
|
20
|
+
def record_input(self, text: str) -> None:
|
|
21
|
+
entry = text.strip()
|
|
22
|
+
if not entry:
|
|
23
|
+
return
|
|
24
|
+
if self._input_history and self._input_history[-1] == entry:
|
|
25
|
+
self._history_cursor = len(self._input_history)
|
|
26
|
+
return
|
|
27
|
+
self._input_history.append(entry)
|
|
28
|
+
self._history_cursor = len(self._input_history)
|
|
29
|
+
|
|
30
|
+
def history_previous(self) -> Optional[str]:
|
|
31
|
+
if not self._input_history:
|
|
32
|
+
return None
|
|
33
|
+
if self._history_cursor > 0:
|
|
34
|
+
self._history_cursor -= 1
|
|
35
|
+
return self._input_history[self._history_cursor]
|
|
36
|
+
|
|
37
|
+
def history_next(self) -> Optional[str]:
|
|
38
|
+
if not self._input_history:
|
|
39
|
+
return None
|
|
40
|
+
if self._history_cursor < len(self._input_history) - 1:
|
|
41
|
+
self._history_cursor += 1
|
|
42
|
+
return self._input_history[self._history_cursor]
|
|
43
|
+
self._history_cursor = len(self._input_history)
|
|
44
|
+
return ""
|
|
45
|
+
|
|
46
|
+
def reset_cursor(self) -> None:
|
|
47
|
+
self._history_cursor = len(self._input_history)
|
|
48
|
+
|
|
49
|
+
# Cycle history ---------------------------------------------------------
|
|
50
|
+
def record_cycle_snapshot(self, result, cycle_id: int, last_instruction: Optional[str]) -> None:
|
|
51
|
+
snapshot = {
|
|
52
|
+
"cycle_id": cycle_id,
|
|
53
|
+
"selected_session": result.selected_session,
|
|
54
|
+
"scoreboard": dict(result.sessions_summary),
|
|
55
|
+
"instruction": last_instruction,
|
|
56
|
+
}
|
|
57
|
+
self._cycle_history.append(snapshot)
|
|
58
|
+
|
|
59
|
+
def last_snapshot(self) -> Optional[Dict[str, object]]:
|
|
60
|
+
if not self._cycle_history:
|
|
61
|
+
return None
|
|
62
|
+
return self._cycle_history[-1]
|
|
63
|
+
|
|
64
|
+
def pop_snapshot(self) -> Optional[Dict[str, object]]:
|
|
65
|
+
if not self._cycle_history:
|
|
66
|
+
return None
|
|
67
|
+
return self._cycle_history.pop()
|
|
68
|
+
|
|
69
|
+
def set_cycle_history(self, history_list: List[Dict[str, object]]) -> None:
|
|
70
|
+
self._cycle_history = history_list
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Pause/escape handling helpers for CLIController."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import subprocess
|
|
7
|
+
from subprocess import PIPE
|
|
8
|
+
from typing import List, Optional, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from .events import ControllerEventType
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
13
|
+
from . import CLIController
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PauseHelper:
|
|
17
|
+
"""Encapsulates pause state transitions and broadcast logic."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, controller: "CLIController") -> None:
|
|
20
|
+
self._controller = controller
|
|
21
|
+
|
|
22
|
+
def handle_escape(self) -> None:
|
|
23
|
+
controller = self._controller
|
|
24
|
+
controller.broadcast_escape()
|
|
25
|
+
if not controller._paused:
|
|
26
|
+
controller._paused = True
|
|
27
|
+
controller._emit(ControllerEventType.LOG, {"text": "一時停止モードに入りました。追加指示は現在のワーカーペインへ送信されます。"})
|
|
28
|
+
controller._emit_status("一時停止モード")
|
|
29
|
+
controller._emit_pause_state()
|
|
30
|
+
return
|
|
31
|
+
if controller._running:
|
|
32
|
+
current_id = controller._current_cycle_id
|
|
33
|
+
if current_id is not None:
|
|
34
|
+
controller._cancelled_cycles.add(current_id)
|
|
35
|
+
controller._current_cycle_id = None
|
|
36
|
+
controller._running = False
|
|
37
|
+
if controller._continue_future and not controller._continue_future.done():
|
|
38
|
+
controller._continue_future.set_result("done")
|
|
39
|
+
if controller._continuation_input_future and not controller._continuation_input_future.done():
|
|
40
|
+
controller._continuation_input_future.set_result("")
|
|
41
|
+
controller._awaiting_continuation_input = False
|
|
42
|
+
controller._paused = False
|
|
43
|
+
controller._emit(ControllerEventType.LOG, {"text": "現在のサイクルをキャンセルし、前の状態へ戻しました。"})
|
|
44
|
+
controller._emit_status("待機中")
|
|
45
|
+
controller._emit_pause_state()
|
|
46
|
+
controller._perform_revert(silent=True)
|
|
47
|
+
|
|
48
|
+
async def dispatch_paused_instruction(self, instruction: str) -> None:
|
|
49
|
+
loop = asyncio.get_running_loop()
|
|
50
|
+
await loop.run_in_executor(None, lambda: self._send_instruction_to_panes(instruction))
|
|
51
|
+
|
|
52
|
+
def _send_instruction_to_panes(self, instruction: str) -> None:
|
|
53
|
+
controller = self._controller
|
|
54
|
+
session_name = controller._config.tmux_session
|
|
55
|
+
pane_ids = self._tmux_list_panes()
|
|
56
|
+
if pane_ids is None:
|
|
57
|
+
return
|
|
58
|
+
if len(pane_ids) <= 2:
|
|
59
|
+
controller._emit(ControllerEventType.LOG, {"text": f"tmuxセッション {session_name} にワーカーペインが見つからず、追加指示を送信できませんでした。"})
|
|
60
|
+
return
|
|
61
|
+
worker_panes = pane_ids[2:]
|
|
62
|
+
for pane_id in worker_panes:
|
|
63
|
+
subprocess.run(
|
|
64
|
+
["tmux", "send-keys", "-t", pane_id, instruction, "Enter"],
|
|
65
|
+
check=False,
|
|
66
|
+
)
|
|
67
|
+
preview = instruction.replace("\n", " ")[:60]
|
|
68
|
+
if len(instruction) > 60:
|
|
69
|
+
preview += "..."
|
|
70
|
+
controller._emit(ControllerEventType.LOG, {"text": f"[pause] {len(worker_panes)} ワーカーペインへ追加指示を送信: {preview}"})
|
|
71
|
+
controller._paused = False
|
|
72
|
+
controller._emit_pause_state()
|
|
73
|
+
controller._emit_status("待機中")
|
|
74
|
+
|
|
75
|
+
def _tmux_list_panes(self) -> Optional[List[str]]:
|
|
76
|
+
controller = self._controller
|
|
77
|
+
session_name = controller._config.tmux_session
|
|
78
|
+
try:
|
|
79
|
+
result = subprocess.run(
|
|
80
|
+
["tmux", "list-panes", "-t", session_name, "-F", "#{pane_id}"],
|
|
81
|
+
check=False,
|
|
82
|
+
stdout=PIPE,
|
|
83
|
+
stderr=PIPE,
|
|
84
|
+
text=True,
|
|
85
|
+
)
|
|
86
|
+
except FileNotFoundError:
|
|
87
|
+
controller._emit(ControllerEventType.LOG, {"text": "tmux コマンドが見つかりません。tmuxがインストールされているか確認してください。"})
|
|
88
|
+
return None
|
|
89
|
+
if result.returncode != 0:
|
|
90
|
+
message = (result.stderr or result.stdout or "").strip()
|
|
91
|
+
if message:
|
|
92
|
+
controller._emit(ControllerEventType.LOG, {"text": f"tmux list-panes に失敗しました: {message}"})
|
|
93
|
+
return None
|
|
94
|
+
return [line.strip() for line in (result.stdout or "").splitlines() if line.strip()]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""CLIController の指示実行フローを担当する WorkflowRunner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Dict, List, Optional, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from .events import ControllerEventType
|
|
9
|
+
from ..orchestrator import CandidateInfo, OrchestrationResult, SelectionDecision
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
12
|
+
from . import CLIController
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WorkflowRunner:
|
|
16
|
+
"""CLIController から切り離した実行フロー管理."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, controller: "CLIController") -> None:
|
|
19
|
+
self._controller = controller
|
|
20
|
+
|
|
21
|
+
async def run(self, instruction: str) -> None:
|
|
22
|
+
c = self._controller
|
|
23
|
+
if c._running:
|
|
24
|
+
c._emit(ControllerEventType.LOG, {"text": "別の指示を処理中です。完了を待ってから再度実行してください。"})
|
|
25
|
+
return
|
|
26
|
+
if c._selection_context:
|
|
27
|
+
c._emit(ControllerEventType.LOG, {"text": "候補選択待ちです。/pick <n> で選択してください。"})
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
c._maybe_auto_commit()
|
|
31
|
+
|
|
32
|
+
c._cycle_counter += 1
|
|
33
|
+
cycle_id = c._cycle_counter
|
|
34
|
+
c._current_cycle_id = cycle_id
|
|
35
|
+
c._running = True
|
|
36
|
+
c._emit_status("メインセッションを準備中...")
|
|
37
|
+
c._active_main_session_id = None
|
|
38
|
+
c._pre_cycle_selected_session = c._last_selected_session
|
|
39
|
+
c._pre_cycle_selected_session_set = True
|
|
40
|
+
|
|
41
|
+
logs_dir = c._create_cycle_logs_dir()
|
|
42
|
+
|
|
43
|
+
orchestrator = c._builder(
|
|
44
|
+
worker_count=c._config.worker_count,
|
|
45
|
+
log_dir=logs_dir,
|
|
46
|
+
session_name=c._config.tmux_session,
|
|
47
|
+
reuse_existing_session=c._config.reuse_existing_session,
|
|
48
|
+
session_namespace=c._session_namespace,
|
|
49
|
+
boss_mode=c._config.boss_mode,
|
|
50
|
+
project_root=c._worktree_root,
|
|
51
|
+
worktree_storage_root=c._worktree_storage_root,
|
|
52
|
+
log_hook=self._controller._log_hook,
|
|
53
|
+
merge_mode=c._config.merge_mode,
|
|
54
|
+
)
|
|
55
|
+
c._active_orchestrator = orchestrator
|
|
56
|
+
c._last_tmux_manager = getattr(orchestrator, "_tmux", None)
|
|
57
|
+
main_hook = getattr(orchestrator, "set_main_session_hook", None)
|
|
58
|
+
if callable(main_hook):
|
|
59
|
+
main_hook(c._on_main_session_started)
|
|
60
|
+
worker_decider = getattr(orchestrator, "set_worker_decider", None)
|
|
61
|
+
if callable(worker_decider):
|
|
62
|
+
worker_decider(c._handle_worker_decision)
|
|
63
|
+
|
|
64
|
+
loop = asyncio.get_running_loop()
|
|
65
|
+
|
|
66
|
+
def selector(
|
|
67
|
+
candidates: List[CandidateInfo],
|
|
68
|
+
scoreboard: Optional[Dict[str, Dict[str, object]]] = None,
|
|
69
|
+
) -> SelectionDecision:
|
|
70
|
+
return c._select_candidates(candidates, scoreboard)
|
|
71
|
+
|
|
72
|
+
resume_session = c._last_selected_session
|
|
73
|
+
|
|
74
|
+
def run_cycle() -> OrchestrationResult:
|
|
75
|
+
return orchestrator.run_cycle(
|
|
76
|
+
instruction,
|
|
77
|
+
selector=selector,
|
|
78
|
+
resume_session_id=resume_session,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
auto_attach_task: Optional[asyncio.Task[None]] = None
|
|
82
|
+
cancelled = False
|
|
83
|
+
try:
|
|
84
|
+
c._emit(ControllerEventType.LOG, {"text": f"指示を開始: {instruction}"})
|
|
85
|
+
if c._attach_mode == "auto":
|
|
86
|
+
auto_attach_task = asyncio.create_task(c._handle_attach_command(force=False))
|
|
87
|
+
result: OrchestrationResult = await loop.run_in_executor(None, run_cycle)
|
|
88
|
+
if getattr(result, "merge_outcome", None) is not None:
|
|
89
|
+
c._handle_merge_outcome(result.merge_outcome)
|
|
90
|
+
if cycle_id in c._cancelled_cycles:
|
|
91
|
+
cancelled = True
|
|
92
|
+
c._cancelled_cycles.discard(cycle_id)
|
|
93
|
+
else:
|
|
94
|
+
c._last_scoreboard = dict(result.sessions_summary)
|
|
95
|
+
c._last_instruction = instruction
|
|
96
|
+
c._last_selected_session = result.selected_session
|
|
97
|
+
c._active_main_session_id = result.selected_session
|
|
98
|
+
c._config.reuse_existing_session = True
|
|
99
|
+
c._emit(ControllerEventType.SCOREBOARD, {"scoreboard": c._last_scoreboard})
|
|
100
|
+
c._emit(ControllerEventType.LOG, {"text": "指示が完了しました。"})
|
|
101
|
+
if result.artifact:
|
|
102
|
+
manifest = c._build_manifest(result, logs_dir)
|
|
103
|
+
c._manifest_store.save_manifest(manifest)
|
|
104
|
+
c._emit(ControllerEventType.LOG, {"text": f"セッションを保存しました: {manifest.session_id}"})
|
|
105
|
+
c._record_cycle_snapshot(result, cycle_id)
|
|
106
|
+
except Exception as exc: # noqa: BLE001
|
|
107
|
+
c._emit(ControllerEventType.LOG, {"text": f"エラーが発生しました: {exc}"})
|
|
108
|
+
finally:
|
|
109
|
+
c._selection_context = None
|
|
110
|
+
if c._current_cycle_id == cycle_id:
|
|
111
|
+
c._current_cycle_id = None
|
|
112
|
+
c._running = False
|
|
113
|
+
c._awaiting_continuation_input = False
|
|
114
|
+
if c._continuation_input_future and not c._continuation_input_future.done():
|
|
115
|
+
c._continuation_input_future.set_result("")
|
|
116
|
+
c._continuation_input_future = None
|
|
117
|
+
if cancelled:
|
|
118
|
+
c._emit_status("待機中")
|
|
119
|
+
c._emit_pause_state()
|
|
120
|
+
c._perform_revert(silent=True)
|
|
121
|
+
else:
|
|
122
|
+
c._emit_status("一時停止中" if c._paused else "待機中")
|
|
123
|
+
c._emit_pause_state()
|
|
124
|
+
if auto_attach_task:
|
|
125
|
+
try:
|
|
126
|
+
await auto_attach_task
|
|
127
|
+
except Exception: # noqa: BLE001
|
|
128
|
+
pass
|
|
129
|
+
c._pre_cycle_selected_session = None
|
|
130
|
+
c._pre_cycle_selected_session_set = False
|
|
131
|
+
|
|
132
|
+
if cancelled and c._queued_instruction:
|
|
133
|
+
queued = c._queued_instruction
|
|
134
|
+
c._queued_instruction = None
|
|
135
|
+
await self.run(queued)
|