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.
@@ -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)