Files
OpenOPC/opc/cli/app.py
T
2026-07-14 14:35:43 +08:00

8390 lines
358 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""OPC CLI — the primary user interface for the One-Person Company system."""
from __future__ import annotations
import asyncio
from collections import deque
from importlib import resources as importlib_resources
import json
import os
import shlex
import signal
import sys
import tempfile
import time
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path, PureWindowsPath
from typing import Any, Optional
import re
import typer
from rich.console import Console
from rich.markup import escape
from rich.markdown import Markdown
from rich.panel import Panel
from rich.table import Table
from rich.theme import Theme
from opc import __version__
from opc.core.config import OPCConfig, get_opc_home
from opc.core.windows_ssl import (
format_windows_sslkeylog_warning,
pop_windows_sslkeylogfile,
)
from opc.database.store import OPCStore
from opc.layer2_organization.talent_market import TalentMarket
from opc.core.models import OPCEvent
# Proxy is configured via llm_config.yaml or shell environment — not hardcoded.
# os.environ['https_proxy'] = 'http://127.0.0.1:7890'
# os.environ['http_proxy'] = 'http://127.0.0.1:7890'
custom_theme = Theme({
"info": "cyan",
"warning": "yellow",
"error": "red bold",
"success": "green bold",
"agent": "blue",
"tool": "magenta",
})
console = Console(theme=custom_theme)
app = typer.Typer(
name="opc",
help="OPC — One-Person Company: Autonomous AI Agent system with natural language interface.",
no_args_is_help=True,
)
_WINDOWS_DRIVE_PATH_RE = re.compile(r"^[A-Za-z]:[\\/]")
class _CliRepoPath:
"""Native filesystem path with stable Windows-drive display semantics."""
def __init__(self, value: str) -> None:
self._native = Path(value)
self._display = str(PureWindowsPath(value))
def __fspath__(self) -> str:
return os.fspath(self._native)
def __str__(self) -> str:
return self._display
def __getattr__(self, name: str) -> Any:
return getattr(self._native, name)
def _talent_repo_path(value: str) -> Path | _CliRepoPath:
if _WINDOWS_DRIVE_PATH_RE.match(str(value or "")):
return _CliRepoPath(value)
return Path(value)
def _current_command_label(argv: list[str] | None = None) -> str:
args = list(argv or sys.argv)
if len(args) > 1 and not args[1].startswith("-"):
return f"opc {args[1]}"
return "opc"
def _get_config() -> OPCConfig:
config_dir = get_opc_home() / "config"
if config_dir.exists():
return OPCConfig.load(config_dir)
return OPCConfig()
def _channel_runtime_pid_path() -> Path:
from opc.core.config import get_opc_home, get_project_workplace
return get_opc_home() / "run" / "channels.pid"
def _read_channel_runtime_state() -> dict | None:
path = _channel_runtime_pid_path()
if not path.exists():
return None
try:
return json.loads(path.read_text())
except Exception:
return None
def _write_channel_runtime_state(enabled_channels: list[str]) -> None:
path = _channel_runtime_pid_path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps({"pid": os.getpid(), "channels": enabled_channels}, ensure_ascii=False))
def _clear_channel_runtime_state() -> None:
path = _channel_runtime_pid_path()
if path.exists():
path.unlink()
def _pid_is_running(pid: int) -> bool:
try:
os.kill(pid, 0)
except OSError:
return False
return True
def _create_default_skills(opc_home: Path) -> None:
"""Copy bundled skills to the OPC home skills directory."""
import shutil
repo_skills = Path(__file__).parent.parent.parent / "skills" / "core"
target_skills = opc_home / "skills" / "core"
target_skills.mkdir(parents=True, exist_ok=True)
copied = False
if repo_skills.exists():
for skill_file in repo_skills.glob("*.md"):
dest = target_skills / skill_file.name
if not dest.exists():
shutil.copy2(skill_file, dest)
copied = True
if copied:
return
try:
resource_skills = importlib_resources.files("opc").joinpath("skills_assets", "core")
except Exception:
return
if not resource_skills.is_dir():
return
for child in resource_skills.iterdir():
if child.is_file() and child.name.endswith(".md"):
dest = target_skills / child.name
if not dest.exists():
dest.write_bytes(child.read_bytes())
def _progress_callback():
"""Create an async progress callback for streaming agent output."""
async def callback(text: str, **_: Any) -> None:
if text.startswith("[Tool:"):
console.print(f" [tool]{escape(text)}[/tool]")
elif text.startswith("[Suggestion]"):
console.print(Panel(text, title="Suggestion", border_style="yellow"))
elif text.startswith("[Delegating") or text.startswith("[Autonomy]"):
console.print(f" [info]{escape(text)}[/info]")
elif text.startswith("[External approval]"):
console.print(f" [info]{escape(text)}[/info]")
elif text.startswith("[External status]"):
if _should_show_external_status(text):
console.print(f" [info]{escape(text)}[/info]")
elif text.startswith("[External agent denied]"):
console.print(f" [warning]{escape(text)}[/warning]")
elif text.startswith("[External agent failed]") or text.startswith("[External agents exhausted]"):
console.print(f" [warning]{escape(text)}[/warning]")
elif text.startswith("[External:"):
if _cli_verbose_external_progress():
console.print(f" [warning]{escape(text)}[/warning]")
else:
pass
return callback
def _cli_verbose_external_progress() -> bool:
value = os.environ.get("OPC_CLI_VERBOSE_EXTERNAL", "")
return value.strip().lower() in {"1", "true", "yes", "on", "debug"}
def _should_show_external_status(text: str) -> bool:
if _cli_verbose_external_progress():
return True
detail = text.split("]", 1)[1].strip().lower() if "]" in text else text.lower()
if "working; last activity" in detail:
return False
return any(token in detail for token in ("started", "approval", "timeout", "failed", "denied", "exhausted", "awaiting", "requires"))
@dataclass
class _QueuedAssistantLine:
text: str
enqueued_at: float
class _CliRuntimeDisplay:
def __init__(self, rich_console: Console) -> None:
self.console = rich_console
self._assistant_buffer = ""
self._line_queue: deque[_QueuedAssistantLine] = deque()
self._drain_task: asyncio.Task[None] | None = None
self._mode = "smooth"
self._below_exit_since: float | None = None
self._last_exit_at: float | None = None
self._status_snapshot: dict[str, object] = {}
self._checkpoint_hint = ""
self._last_status_render = ""
self._last_status_render_at = 0.0
self._sidecar_quiet_depth = 0
self._sidecar_quiet_event_count = 0
self.has_streamed_content = False
def begin_turn(self) -> None:
self.has_streamed_content = False
self._assistant_buffer = ""
self._line_queue.clear()
self._mode = "smooth"
self._below_exit_since = None
self._last_exit_at = None
self._checkpoint_hint = ""
def status_snapshot(self) -> dict[str, object]:
snapshot = dict(self._status_snapshot)
snapshot["stream_queue_depth"] = len(self._line_queue)
snapshot["stream_drain_mode"] = self._mode
if self._checkpoint_hint:
snapshot["checkpoint_hint"] = self._checkpoint_hint
return snapshot
def set_checkpoint_hint(self, hint: str) -> None:
self._checkpoint_hint = hint.strip()
async def render_status(self, *, force: bool = False) -> None:
await self._render_status(force=force)
async def handle_event(self, event: OPCEvent) -> None:
if getattr(event, "event_type", "") != "runtime_event":
return
payload = dict(getattr(event, "payload", {}) or {})
runtime_type = str(payload.get("type", "") or "").strip()
if self._sidecar_quiet_depth > 0:
self._sidecar_quiet_event_count += 1
if runtime_type == "status_snapshot":
self._status_snapshot = payload
elif runtime_type in {"turn_completed", "turn_failed"}:
self._assistant_buffer = ""
self._line_queue.clear()
return
if runtime_type == "assistant_delta":
self._enqueue_assistant_delta(str(payload.get("text", "") or ""))
self._ensure_drain_task()
return
if runtime_type in {"turn_completed", "turn_failed"}:
self._finalize_assistant_buffer()
await self.flush()
await self._render_status(force=True)
return
if runtime_type == "status_snapshot":
self._status_snapshot = payload
await self._render_status()
return
if runtime_type == "tool_started":
tool_name = str(payload.get("tool_name", "") or "tool").strip()
self.console.print(f" [tool]Running {escape(tool_name)}[/tool]")
return
if runtime_type == "tool_completed":
tool_name = str(payload.get("tool_name", "") or "tool").strip()
elapsed_ms = int(payload.get("elapsed_ms", 0) or 0)
summary = str(payload.get("result_summary", "") or "").strip()
suffix = f" ({elapsed_ms}ms)" if elapsed_ms > 0 else ""
if summary:
self.console.print(f" [tool]{escape(tool_name)} finished{suffix}: {escape(summary)}[/tool]")
else:
self.console.print(f" [tool]{escape(tool_name)} finished{suffix}[/tool]")
return
if runtime_type == "permission_requested":
tool_name = str(payload.get("tool_name", "") or "tool").strip()
rationale = str(payload.get("rationale", "") or "").strip()
self.console.print(
f" [warning]Approval needed for {escape(tool_name)}: {escape(rationale or 'awaiting review')}[/warning]"
)
return
if runtime_type == "sandbox_retry_requested":
tool_name = str(payload.get("tool_name", "") or "tool").strip()
self.console.print(
f" [info]Retrying {escape(tool_name)} with sandbox upgrade "
f"{escape(str(payload.get('from_mode', 'unknown')))} -> "
f"{escape(str(payload.get('to_mode', 'unknown')))}[/info]"
)
return
if runtime_type == "sandbox_retry_completed":
tool_name = str(payload.get("tool_name", "") or "tool").strip()
state = "succeeded" if bool(payload.get("success", False)) else "failed"
self.console.print(f" [info]Sandbox retry {state} for {escape(tool_name)}[/info]")
return
if runtime_type == "context_warning":
pct = int(payload.get("context_remaining_pct", 0) or 0)
self.console.print(f" [warning]Context window low: {pct}% remaining[/warning]")
def _enqueue_assistant_delta(self, text: str) -> None:
if not text:
return
self._assistant_buffer += text
while "\n" in self._assistant_buffer:
line, self._assistant_buffer = self._assistant_buffer.split("\n", 1)
self._line_queue.append(_QueuedAssistantLine(line, time.monotonic()))
def _finalize_assistant_buffer(self) -> None:
if self._assistant_buffer:
self._line_queue.append(_QueuedAssistantLine(self._assistant_buffer, time.monotonic()))
self._assistant_buffer = ""
def _ensure_drain_task(self) -> None:
if self._drain_task is None or self._drain_task.done():
self._drain_task = asyncio.create_task(self._drain_loop())
def _drain_count(self, now: float) -> int:
queued = len(self._line_queue)
if queued == 0:
self._mode = "smooth"
self._below_exit_since = None
return 0
oldest_age_ms = int(max(0.0, now - self._line_queue[0].enqueued_at) * 1000)
severe = queued >= 64 or oldest_age_ms >= 300
if self._mode == "smooth":
if (queued >= 8 or oldest_age_ms >= 120) and (not self._last_exit_at or severe or (now - self._last_exit_at) >= 0.25):
self._mode = "catch_up"
self._below_exit_since = None
else:
below_exit = queued <= 2 and oldest_age_ms <= 40
if below_exit:
if self._below_exit_since is None:
self._below_exit_since = now
elif (now - self._below_exit_since) >= 0.25:
self._mode = "smooth"
self._below_exit_since = None
self._last_exit_at = now
else:
self._below_exit_since = None
return queued if self._mode == "catch_up" else 1
async def _drain_loop(self) -> None:
try:
while self._line_queue:
now = time.monotonic()
count = self._drain_count(now)
for _ in range(min(count, len(self._line_queue))):
line = self._line_queue.popleft()
self.console.print(line.text, markup=False)
self.has_streamed_content = True
await self._render_status()
await asyncio.sleep(0.05)
finally:
self._drain_task = None
async def _render_status(self, *, force: bool = False) -> None:
now = time.monotonic()
snapshot = dict(self._status_snapshot)
stream_queue_depth = len(self._line_queue)
runtime_queue_depth = snapshot.get("queue_depth", 0)
current_tool = str(snapshot.get("current_tool", "") or "").strip()
agent_id = str(snapshot.get("agent_id", "") or snapshot.get("role_id", "") or "").strip()
task_id = str(snapshot.get("task_id", "") or "").strip()
context_remaining_pct = snapshot.get("context_remaining_pct")
turn_cost = snapshot.get("turn_cost_usd")
session_cost = snapshot.get("session_cost_usd")
pending_permission_count = snapshot.get("pending_permission_count")
parts: list[str] = []
parts.append(f"queue={int(runtime_queue_depth or 0)}")
parts.append(f"stream_queue={stream_queue_depth}")
parts.append(f"drain={self._mode}")
if agent_id:
parts.append(f"agent={agent_id}")
if task_id:
parts.append(f"task={task_id}")
if current_tool:
elapsed_ms = int(snapshot.get("tool_elapsed_ms", 0) or 0)
tool_part = f"tool={current_tool}"
if elapsed_ms > 0:
tool_part += f" {elapsed_ms}ms"
parts.append(tool_part)
if context_remaining_pct not in (None, ""):
parts.append(f"context={int(context_remaining_pct)}%")
if turn_cost not in (None, ""):
parts.append(f"turn=${float(turn_cost):.4f}")
if session_cost not in (None, ""):
parts.append(f"session=${float(session_cost):.4f}")
if pending_permission_count not in (None, "") and int(pending_permission_count or 0) > 0:
parts.append(f"approvals={int(pending_permission_count or 0)}")
if self._checkpoint_hint:
parts.append(f"checkpoint={self._checkpoint_hint}")
message = " | ".join(parts)
if not message:
return
if not force and (message == self._last_status_render or (now - self._last_status_render_at) < 0.75):
return
self.console.print(f"[dim][status][/dim] {message}")
self._last_status_render = message
self._last_status_render_at = now
async def flush(self) -> None:
self._finalize_assistant_buffer()
if self._drain_task is not None:
await self._drain_task
while self._line_queue:
line = self._line_queue.popleft()
self.console.print(line.text)
self.has_streamed_content = True
def enter_sidecar_quiet(self) -> None:
self._sidecar_quiet_depth += 1
def exit_sidecar_quiet(self) -> int:
if self._sidecar_quiet_depth > 0:
self._sidecar_quiet_depth -= 1
count = self._sidecar_quiet_event_count
if self._sidecar_quiet_depth == 0:
self._sidecar_quiet_event_count = 0
return count
def _create_cli_engine(config, project: str | None):
from opc.engine import OPCEngine
runtime_display = _CliRuntimeDisplay(console)
engine = OPCEngine(
config=config,
project_id=project,
on_progress=_progress_callback(),
on_runtime_event=runtime_display.handle_event,
on_escalation=_escalation_callback(),
)
return engine, runtime_display
def _attach_cli_runtime_callbacks(state: _InteractiveChatState) -> None:
def on_company_runtime_children(session_id: str, task_ids: list[str]) -> None:
parent_session_id = str(session_id or "").strip()
clean_task_ids = [str(item or "").strip() for item in list(task_ids or []) if str(item or "").strip()]
if parent_session_id and clean_task_ids:
state.session_to_task[parent_session_id] = clean_task_ids[0]
for task_id in clean_task_ids:
state.active_runtime_children[task_id] = clean_task_ids[0]
try:
state.engine.on_company_runtime_children = on_company_runtime_children
except Exception:
pass
def _normalize_escalation_key(value: str) -> str:
return re.sub(r"[\s\-]+", "_", value.strip()).strip("_").casefold()
def _format_escalation_option(option: dict, index: int) -> str:
option_id = str(option.get("id", "")).strip()
label = str(option.get("label", option_id)).strip() or option_id
# Use an explicit numeric prefix and escape markup so Rich never hides the id.
return f" {index}. {escape(label)} ({escape(option_id)})"
def _normalize_escalation_reply(reply: str, options: list[dict]) -> str | None:
raw_reply = reply.strip()
if not raw_reply:
return None
normalized_map: dict[str, str] = {}
for idx, option in enumerate(options, start=1):
option_id = str(option.get("id", "")).strip()
label = str(option.get("label", option_id)).strip()
if not option_id:
continue
normalized_map[option_id.casefold()] = option_id
normalized_map[_normalize_escalation_key(option_id)] = option_id
if label:
normalized_map[label.casefold()] = option_id
normalized_map[_normalize_escalation_key(label)] = option_id
normalized_map[str(idx)] = option_id
alias_map = {
"y": "approve_once",
"yes": "approve_once",
"approve": "approve_once",
"allow": "approve_once",
"n": "deny",
"no": "deny",
"deny": "deny",
"reject": "deny",
"project": "always_project",
"global": "always_global",
}
if alias := alias_map.get(_normalize_escalation_key(raw_reply)):
if alias in normalized_map.values():
return alias
return normalized_map.get(raw_reply.casefold()) or normalized_map.get(_normalize_escalation_key(raw_reply))
def _escalation_callback():
"""Create an async escalation callback for human-in-the-loop."""
async def callback(message: str, options: list[dict]) -> str | None:
console.print(Panel(message, title="Action Required", border_style="yellow"))
if options:
for idx, opt in enumerate(options, start=1):
console.print(_format_escalation_option(opt, idx))
console.print(" Enter the number, label, or internal id.")
while True:
try:
reply = console.input("[bold]Your choice: [/bold]")
except (EOFError, KeyboardInterrupt):
return None
normalized = _normalize_escalation_reply(reply, options)
if normalized is not None:
return normalized
console.print("[warning]Invalid choice. Please enter a listed number, label, or id.[/warning]")
else:
try:
reply = console.input("[bold]Your reply: [/bold]")
return reply.strip()
except (EOFError, KeyboardInterrupt):
return None
return callback
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
@app.command()
def chat(
message: Optional[str] = typer.Argument(None, help="Single message to process"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID to work in"),
model: Optional[str] = typer.Option(None, "--model", "-m", help="Override default LLM model"),
mode: str = typer.Option("task", "--mode", help="Execution mode: task or company"),
agent: Optional[str] = typer.Option(None, "--agent", help="Preferred agent: native, claude_code, codex, cursor, opencode"),
company_profile: str = typer.Option("corporate", "--company-profile", help="Company profile for company mode"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed logs"),
no_markdown: bool = typer.Option(False, "--no-markdown", help="Plain text output"),
):
"""Chat with the OPC system — interactive or single message mode."""
config = _get_config()
if model:
config.llm.default_model = model
if verbose:
config.system.log_level = "DEBUG"
if message:
asyncio.run(_single_message(
config,
message,
project,
no_markdown,
mode=mode,
preferred_agent=agent,
company_profile=company_profile,
))
else:
asyncio.run(_interactive_mode(
config,
project,
no_markdown,
mode=mode,
preferred_agent=agent,
company_profile=company_profile,
explicit_mode=_cli_option_present("--mode"),
explicit_agent=_cli_option_present("--agent"),
explicit_company_profile=_cli_option_present("--company-profile"),
))
@app.command("exec")
def exec_command(
prompt: Optional[str] = typer.Argument(None, help="Prompt to run non-interactively. Reads stdin when omitted."),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID to work in"),
mode: str = typer.Option("task", "--mode", help="Execution mode: task, company, or org"),
company_profile: str = typer.Option("corporate", "--company-profile", help="Company profile for company mode"),
agent: Optional[str] = typer.Option(None, "--agent", help="Preferred agent: native, claude_code, codex, cursor, opencode"),
org_id: Optional[str] = typer.Option(None, "--org", "--org-id", help="Saved org id for org/custom mode"),
session_id: Optional[str] = typer.Option(None, "--session-id", help="Existing task-backed session id to reuse"),
resume: bool = typer.Option(False, "--resume", help="Resume the latest task-backed session in the project"),
json_output: bool = typer.Option(False, "--json", help="Print final JSON payload"),
stream_json: bool = typer.Option(False, "--stream-json", help="Print newline-delimited JSON runtime events"),
no_markdown: bool = typer.Option(False, "--no-markdown", help="Plain text output"),
):
"""Run one non-interactive OPC task for scripts and CI."""
if json_output and stream_json:
console.print("[error]Use either --json or --stream-json, not both.[/error]")
raise typer.Exit(code=2)
if session_id and resume:
console.print("[error]Use either --session-id or --resume, not both.[/error]")
raise typer.Exit(code=2)
message = prompt
if message is None and not sys.stdin.isatty():
message = sys.stdin.read()
config = _get_config()
try:
asyncio.run(_exec_message(
config=config,
prompt=str(message or "").strip(),
project=project,
mode=mode,
company_profile=company_profile,
preferred_agent=agent,
org_id=org_id,
session_id=session_id,
resume=resume,
json_output=json_output,
stream_json=stream_json,
no_markdown=no_markdown,
))
except KeyboardInterrupt as exc:
if stream_json:
_print_exec_event(
{"seq": 0},
"error",
project_id=project or "default",
payload={"code": "cancelled", "error": "Interrupted"},
)
else:
console.print("\n[warning]Interrupted.[/warning]")
raise typer.Exit(code=130) from exc
@app.command()
def secretary(
message: Optional[str] = typer.Argument(None, help="Single secretary message"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID for secretary context"),
model: Optional[str] = typer.Option(None, "--model", "-m", help="Override default LLM model"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed logs"),
no_markdown: bool = typer.Option(False, "--no-markdown", help="Plain text output"),
):
"""Talk directly with the long-term secretary interface."""
config = _get_config()
if model:
config.llm.default_model = model
if verbose:
config.system.log_level = "DEBUG"
if message:
asyncio.run(_single_secretary_message(config, message, project, no_markdown))
else:
asyncio.run(_interactive_secretary_mode(config, project, no_markdown))
@app.command("propose-reorg")
def propose_reorg(
payload: str = typer.Argument(..., help="JSON payload with summary/rationale/changeset"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
):
"""Create a runtime company reorg proposal."""
config = _get_config()
asyncio.run(_propose_reorg(config, payload, project))
@app.command("approve-reorg")
def approve_reorg(
proposal_id: str = typer.Argument(..., help="Reorg proposal ID"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
):
"""Approve a pending company reorg proposal."""
config = _get_config()
asyncio.run(_approve_reorg(config, proposal_id, project, approved=True))
@app.command("deny-reorg")
def deny_reorg(
proposal_id: str = typer.Argument(..., help="Reorg proposal ID"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
):
"""Deny a pending company reorg proposal."""
config = _get_config()
asyncio.run(_approve_reorg(config, proposal_id, project, approved=False))
@app.command("apply-reorg")
def apply_reorg(
proposal_id: str = typer.Argument(..., help="Approved reorg proposal ID"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
):
"""Apply an approved company reorg proposal."""
config = _get_config()
asyncio.run(_apply_reorg(config, proposal_id, project))
@app.command("show-reorg")
def show_reorg(
proposal_id: str = typer.Argument(..., help="Reorg proposal ID"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
):
"""Show a company reorg proposal."""
config = _get_config()
asyncio.run(_show_reorg(config, proposal_id, project))
def _template_has_required_config_files(source: Any) -> bool:
try:
return (
source.is_dir()
and source.joinpath("llm_config.yaml").is_file()
and source.joinpath("system_config.yaml").is_file()
)
except Exception:
return False
def _project_config_template_dir() -> Any | None:
"""Return the best config template source.
Source checkouts use ``project_root/config``. Installed packages fall back
to ``opc/config_templates`` shipped as package data via ``importlib.resources``.
"""
from opc.core.config import get_opc_home
opc_home = get_opc_home()
project_root = opc_home.parent
repo_config = project_root / "config"
if _template_has_required_config_files(repo_config):
return repo_config
try:
packaged_config = importlib_resources.files("opc").joinpath("config_templates")
except Exception:
packaged_config = None
if packaged_config is not None and _template_has_required_config_files(packaged_config):
return packaged_config
return None
def _copy_template_tree(source: Any, target: Path) -> None:
target.mkdir(parents=True, exist_ok=True)
for child in source.iterdir():
dest = target / child.name
if child.is_dir():
_copy_template_tree(child, dest)
elif child.is_file():
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(child.read_bytes())
def _load_config_template(source: Any) -> OPCConfig:
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
_copy_template_tree(source, tmp_path)
return OPCConfig.load(tmp_path)
def _opc_config_initialized(opc_home: Path) -> bool:
config_dir = opc_home / "config"
if not config_dir.exists():
return False
expected = (
"llm_config.yaml",
"system_config.yaml",
"agent_config.yaml",
"org_config.yaml",
)
if any((config_dir / name).exists() for name in expected):
return True
return any(config_dir.iterdir())
def _trust_configured_external_agents(
config: OPCConfig,
opc_home: Path,
*,
project_id: str | None,
) -> None:
from opc.layer5_memory.approval_allowlist import ApprovalAllowlistManager
manager = ApprovalAllowlistManager(opc_home)
manager.ensure_file()
scope_project = project_id.strip() if isinstance(project_id, str) and project_id.strip() else None
for agent_name, agent_config in config.agents.agents.items():
if not getattr(agent_config, "enabled", True):
continue
manager.add_patterns(
"external_agent",
agent_name,
["*"],
project_id=scope_project,
)
def _run_init_external_agent_preflight(
config: OPCConfig,
*,
project_id: str,
opc_home: Path,
workspace_path: Path,
) -> None:
from opc.layer3_agent.preflight import run_external_agent_preflight
results = run_external_agent_preflight(
config,
project_id=project_id,
workspace_path=workspace_path,
opc_home=opc_home,
probe_commands=False,
prepare_surfaces=True,
)
available = sum(1 for item in results if item.available)
configured = sum(1 for item in results if item.enabled)
failures = [item for item in results if item.enabled and item.issues]
console.print(
f"[info]External agent preflight:[/info] {available}/{configured} configured agents found; "
f"collab surfaces and workspace permissions checked."
)
for item in failures:
console.print(f" [warning]- {item.agent}: {'; '.join(item.issues[:2])}[/warning]")
def _render_external_agent_detection(config: OPCConfig) -> None:
import shutil
from opc.layer3_agent.adapters.registry import ADAPTER_CLASSES
for agent_name, agent_config in config.agents.agents.items():
adapter_cls = ADAPTER_CLASSES.get(agent_name)
if adapter_cls is not None:
adapter = adapter_cls(config=agent_config)
command = adapter.configured_command()
found = adapter.resolve_binary()
else:
command = agent_config.command
found = shutil.which(command)
if found:
console.print(f" - {agent_name}: [success]found[/success] ({found})")
else:
console.print(f" - {agent_name}: [warning]not found[/warning] (command={command})")
def _render_external_agent_preflight_table(results: list[Any]) -> None:
table = Table(show_header=True, header_style="bold")
table.add_column("Agent")
table.add_column("Binary")
table.add_column("Mode")
table.add_column("Stdin")
table.add_column("Workspace")
table.add_column("Collab")
table.add_column("Notes")
for item in results:
if not item.enabled:
binary = "[warning]disabled[/warning]"
elif item.available:
binary = f"[success]found[/success]\n{item.binary}"
else:
binary = f"[error]missing[/error]\n{item.command}"
mode = ""
if item.launch_command:
mode = _truncate_status_cell(item.launch_command, limit=180)
elif item.version:
mode = item.version
workspace_ok = all(check.ok for check in item.write_checks)
workspace = "[success]writable[/success]" if workspace_ok else "[error]blocked[/error]"
collab = "[success]ready[/success]" if item.collab_cli and not item.issues else "[warning]check[/warning]"
rpc_transport = str(getattr(item, "collaboration_rpc_transport", "") or "").strip()
if rpc_transport:
collab = f"{collab}\nRPC: {rpc_transport}"
notes = []
if item.issues:
notes.extend(item.issues[:2])
if item.warnings:
notes.extend(item.warnings[:2])
table.add_row(
item.agent,
binary,
mode,
str(getattr(item, "stdin_policy", "") or "-"),
workspace,
collab,
_truncate_status_cell("; ".join(notes) if notes else "ok", limit=160),
)
console.print(table)
def _truncate_status_cell(value: str, *, limit: int) -> str:
text = str(value or "").strip()
if len(text) <= limit:
return text
return text[: max(limit - 3, 0)].rstrip() + "..."
@app.command()
def init(
project: Optional[str] = typer.Argument(None, help="Project ID to initialize"),
yes: bool = typer.Option(False, "--yes", "-y", help="Continue when OPC is already initialized; existing config is preserved."),
external_agent_preflight: bool = typer.Option(
True,
"--external-agent-preflight/--no-external-agent-preflight",
help="Provision and validate external-agent homes, collab CLI, and workspace permissions.",
),
trust_external_agents: bool = typer.Option(
True,
"--trust-external-agents/--no-trust-external-agents",
help="Persist OpenOPC approval rules for configured external-agent launches.",
),
):
"""Initialize OPC configuration and workspace."""
from opc.core.config import get_opc_home, get_project_workplace
opc_home = get_opc_home()
template_dir = _project_config_template_dir()
already_initialized = _opc_config_initialized(opc_home)
config: OPCConfig
if already_initialized:
if not yes and not typer.confirm(
f"OPC is already initialized at {opc_home}. Continue without overwriting existing config?",
default=False,
):
console.print("[warning]Init cancelled. Existing config was left unchanged.[/warning]")
raise typer.Exit(1)
console.print(f"[info]Existing config preserved: {opc_home / 'config'}[/info]")
config = OPCConfig.load(opc_home / "config")
elif template_dir is not None:
# Use repo config template (same setup as maintainers, keys left for user to set)
config = _load_config_template(template_dir)
config.llm.api_key = ""
config.save(opc_home / "config")
else:
config = OPCConfig()
if not config.org.roles:
from opc.core.config import RoleConfig, EscalationRule
browser_tools = [
"browser_navigate",
"browser_navigate_back",
"browser_click",
"browser_snapshot",
"browser_type",
"browser_wait_for",
"browser_scroll",
"browser_select_option",
"browser_take_screenshot",
"browser_close",
]
planning_tools = [
"file_read",
"file_search",
"list_dir",
"todo_write",
"todo_read",
]
engineering_tools = [
"shell_exec",
"file_read",
"file_write",
"file_edit",
"file_search",
"list_dir",
"web_search",
"web_fetch",
"todo_write",
"todo_read",
*browser_tools,
]
review_tools = [
"file_read",
"file_search",
"list_dir",
"shell_exec",
"browser_navigate",
"browser_navigate_back",
"browser_snapshot",
"browser_wait_for",
"browser_scroll",
"browser_select_option",
"browser_take_screenshot",
]
config.org.roles = [
RoleConfig(
id="ceo",
name="CEO",
icon="leader",
responsibility="Strategic intake, high-level routing, final aggregation and delivery to the owner",
reports_to="owner",
can_spawn=["cto", "cmo", "coo"],
tools=planning_tools,
),
RoleConfig(
id="cto",
name="CTO",
icon="code",
responsibility="Technical planning, architecture decisions, code review, and engineering oversight",
reports_to="ceo",
can_spawn=["senior_engineer", "devops_engineer"],
tools=[*planning_tools, "shell_exec", "web_search", "web_fetch"],
),
RoleConfig(
id="cmo",
name="CMO",
icon="marketing",
responsibility="Marketing strategy, content planning, UX review, and brand oversight",
reports_to="ceo",
can_spawn=["content_specialist", "designer"],
tools=[
*planning_tools,
"web_search",
"web_fetch",
"browser_navigate",
"browser_navigate_back",
"browser_snapshot",
"browser_wait_for",
"browser_scroll",
"browser_take_screenshot",
],
),
RoleConfig(
id="coo",
name="COO",
icon="strategy",
responsibility="Operations coordination, process management, cross-team alignment, and quality assurance",
reports_to="ceo",
can_spawn=["qa_analyst"],
tools=[
*planning_tools,
"shell_exec",
"browser_navigate",
"browser_navigate_back",
"browser_snapshot",
"browser_wait_for",
"browser_scroll",
"browser_take_screenshot",
],
),
RoleConfig(
id="senior_engineer",
name="Senior Engineer",
icon="terminal",
responsibility="Code implementation, system development, and technical execution",
reports_to="cto",
preferred_external_agent="codex",
tools=engineering_tools,
),
RoleConfig(
id="devops_engineer",
name="DevOps Engineer",
icon="settings",
responsibility="Infrastructure, deployment, CI/CD, monitoring, and operational hardening",
reports_to="cto",
preferred_external_agent="cursor",
tools=engineering_tools,
),
RoleConfig(
id="content_specialist",
name="Content Specialist",
icon="writing",
responsibility="Documentation, copywriting, presentations, and user-facing writing",
reports_to="cmo",
tools=[
"file_read",
"file_write",
"file_edit",
"file_search",
"list_dir",
"web_search",
"web_fetch",
"todo_write",
"todo_read",
*browser_tools,
],
),
RoleConfig(
id="designer",
name="Designer",
icon="design",
responsibility="Visual design, UX artifacts, wireframes, and design system work",
reports_to="cmo",
tools=[
"file_read",
"file_write",
"file_edit",
"file_search",
"list_dir",
"web_search",
"web_fetch",
"todo_write",
"todo_read",
*browser_tools,
],
),
RoleConfig(
id="qa_analyst",
name="QA Analyst",
icon="bug",
responsibility="Testing, security review, compliance checks, and acceptance validation",
reports_to="coo",
tools=review_tools,
),
]
config.org.escalation_rules = [
EscalationRule(condition="3 consecutive failures with no progress", action="Escalate to owner with failure reason"),
EscalationRule(condition="External account or credentials required", action="Escalate to owner"),
EscalationRule(condition="Security vulnerability severity >= HIGH", action="Immediately halt and escalate"),
EscalationRule(condition="Budget exceeds 80% of limit", action="Alert owner and await instructions"),
]
config.save(opc_home / "config")
# Create default directories. ``agent_homes/`` and ``bin/`` get
# provisioned lazily by the skill installer the first time an
# external agent launches.
for subdir in ["memory", "skills/core", "skills/cache", "skills/learned", "logs", "prompts/talent"]:
(opc_home / subdir).mkdir(parents=True, exist_ok=True)
# Create default skills
_create_default_skills(opc_home)
# Create default global memory.
from opc.layer5_memory.approval_allowlist import ApprovalAllowlistManager
from opc.layer5_memory.markdown_memory import MarkdownMemoryStore
memory_store = MarkdownMemoryStore(opc_home)
if not memory_store.load_raw_text():
memory_store.save_visible_text("# Global Memory", None)
ApprovalAllowlistManager(opc_home).ensure_file()
if project:
if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$", project or ""):
console.print("[error]Invalid project ID (use alphanumeric, hyphens, underscores).[/error]")
raise typer.Exit(1)
proj_dir = opc_home / "projects" / project
project_memory = memory_store.memory_path(project)
workplace = get_project_workplace(project)
if proj_dir.exists() or project_memory.exists() or workplace.exists():
console.print(f"[error]Project '{project}' already exists.[/error]")
raise typer.Exit(1)
proj_dir.mkdir(parents=True, exist_ok=False)
workplace.mkdir(parents=True, exist_ok=False)
memory_store.ensure_memory_file(project, f"# Project Memory ({project})")
console.print(f"[success]Project '{project}' initialized at {proj_dir}[/success]")
console.print(f" Workplace: {workplace}")
if trust_external_agents:
_trust_configured_external_agents(config, opc_home, project_id=project)
if external_agent_preflight:
_run_init_external_agent_preflight(
config,
project_id=project or "default",
opc_home=opc_home,
workspace_path=get_project_workplace(project or "default"),
)
console.print(f"[success]OPC initialized at {opc_home}[/success]")
console.print(f" Config: {opc_home / 'config'}")
console.print(f" Memory: {opc_home / 'memory'}")
console.print(f" Skills: {opc_home / 'skills'}")
console.print(f"\nEdit [bold]{opc_home / 'config' / 'llm_config.yaml'}[/bold] to set your API key.")
@app.command()
def status(
project: str = typer.Option("default", "--project", "-p", help="Project ID used for workspace preflight."),
external_agent_preflight: bool = typer.Option(
True,
"--external-agent-preflight/--no-external-agent-preflight",
help="Run external-agent command, flag, collab, and workspace checks.",
),
probe_agent_commands: bool = typer.Option(
True,
"--probe-agent-commands/--no-probe-agent-commands",
help="Run short --version/--help probes to detect unsupported CLI flags.",
),
):
"""Show OPC system status."""
from opc.core.config import get_opc_home
config = _get_config()
opc_home = get_opc_home()
console.print(Panel(f"[bold]OPC v{__version__}[/bold]", border_style="blue"))
console.print(f" Home: {opc_home}")
console.print(f" Model: {config.llm.default_model}")
console.print(f" Log level: {config.system.log_level}")
console.print(f" Max iterations: {config.system.max_agent_iterations}")
console.print(f" Autonomy mode: {config.autonomy.mode}")
console.print(f" Max auto-approve risk: {config.autonomy.max_auto_approve_risk}")
# Roles
console.print("\n[bold]Organization:[/bold]")
console.print(f" Company: {config.org.company_name}")
for role in config.org.roles:
console.print(f" - {role.name} ({role.id}): {role.responsibility[:60]}...")
# External agents
console.print("\n[bold]External Agents:[/bold]")
for name, agent_config in config.agents.agents.items():
status_str = "[success]enabled[/success]" if agent_config.enabled else "[warning]disabled[/warning]"
console.print(f" - {name}: {status_str}")
if not config.agents.agents:
console.print(" (none configured)")
console.print("\n[bold]External Agent Preflight:[/bold]")
if external_agent_preflight:
from opc.layer3_agent.preflight import run_external_agent_preflight
results = run_external_agent_preflight(
config,
project_id=project,
probe_commands=probe_agent_commands,
prepare_surfaces=True,
)
_render_external_agent_preflight_table(results)
else:
_render_external_agent_detection(config)
# Projects
projects_dir = opc_home / "projects"
if projects_dir.exists():
projects = [p.name for p in projects_dir.iterdir() if p.is_dir()]
console.print(f"\n[bold]Projects:[/bold] {', '.join(projects) if projects else '(none)'}")
# Cost
asyncio.run(_show_cost_summary(config))
asyncio.run(_show_autonomy_summary(config))
@app.command()
def autonomy_status(
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
):
"""Show autonomy policy, learned preferences, and approval stats."""
config = _get_config()
console.print(Panel("[bold]Autonomy Status[/bold]", border_style="blue"))
console.print(f" Mode: {config.autonomy.mode}")
console.print(f" Enabled: {config.autonomy.enabled}")
console.print(f" Max auto-approve risk: {config.autonomy.max_auto_approve_risk}")
console.print(f" Confidence threshold: {config.autonomy.approval_confidence_threshold}")
asyncio.run(_show_autonomy_summary(config, project=project))
@app.command()
def autonomy_reset(
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
):
"""Reset learned autonomy preferences globally or for a project."""
from opc.core.config import get_opc_home
from opc.layer5_memory.approval_allowlist import ApprovalAllowlistManager
from opc.layer5_memory.preference import PreferenceManager
opc_home = get_opc_home()
prefs = PreferenceManager(opc_home)
prefs.reset_autonomy_preferences(project_id=project)
ApprovalAllowlistManager(opc_home).reset(project_id=project)
scope = f"project '{project}'" if project else "global"
console.print(f"[success]Reset learned autonomy preferences and allowlist rules for {scope}.[/success]")
@app.command()
def autonomy_configure(
mode: Optional[str] = typer.Option(None, "--mode", help="Autonomy mode, e.g. bounded"),
max_auto_approve_risk: Optional[str] = typer.Option(None, "--max-risk", help="Max auto-approve risk"),
approval_confidence_threshold: Optional[float] = typer.Option(None, "--confidence-threshold", help="Approval confidence threshold"),
):
"""Update autonomy configuration settings."""
from opc.core.config import get_opc_home
config = _get_config()
if mode is not None:
config.autonomy.mode = mode
if max_auto_approve_risk is not None:
config.autonomy.max_auto_approve_risk = max_auto_approve_risk
if approval_confidence_threshold is not None:
config.autonomy.approval_confidence_threshold = approval_confidence_threshold
config.save(get_opc_home() / "config")
console.print("[success]Updated autonomy configuration.[/success]")
@app.command()
def projects():
"""List all projects."""
from opc.core.config import get_opc_home, get_project_workplace
from opc.layer5_memory.markdown_memory import MarkdownMemoryStore
opc_home = get_opc_home()
memory_store = MarkdownMemoryStore(opc_home)
projects_dir = opc_home / "projects"
if not projects_dir.exists() or not any(projects_dir.iterdir()):
console.print("[warning]No projects found.[/warning]")
console.print("Create one with: opc init <project-name>")
return
for p in sorted(projects_dir.iterdir()):
if p.is_dir():
has_tasks = (p / "tasks.db").exists()
has_memory = memory_store.memory_path(p.name).exists()
has_workplace = get_project_workplace(p.name).exists()
markers = []
if has_tasks:
markers.append("tasks")
if has_memory:
markers.append("memory")
if has_workplace:
markers.append("workplace")
info = f" ({', '.join(markers)})" if markers else ""
console.print(f" - [bold]{p.name}[/bold]{info}")
@app.command()
def skills():
"""List available skills."""
from opc.core.config import get_opc_home
from opc.layer5_memory.skill_library import SkillLibrary
opc_home = get_opc_home()
lib = SkillLibrary(opc_home)
lib.load_all()
skill_list = lib.list_skills()
if not skill_list:
console.print("[warning]No skills found.[/warning]")
console.print(f"Add skills to {opc_home / 'skills'}")
return
console.print(f"[bold]Skills ({len(skill_list)}):[/bold]")
for s in skill_list:
always = " [always]" if s.always else ""
level = f" [{s.level}]" if s.level else ""
console.print(f" - [bold]{s.name}[/bold]{always}{level}")
if s.description:
console.print(f" {s.description}")
@app.command()
def config_show():
"""Show current configuration."""
import yaml
config = _get_config()
console.print(yaml.dump(config.model_dump(), default_flow_style=False))
def _json_safe(value: Any) -> Any:
if hasattr(value, "model_dump"):
return value.model_dump()
if hasattr(value, "__dict__"):
return dict(value.__dict__)
if hasattr(value, "value"):
return value.value
if hasattr(value, "isoformat"):
return value.isoformat()
return str(value)
def _emit_payload(payload: dict[str, Any], *, json_output: bool = False) -> None:
if json_output:
console.print(json.dumps(payload, ensure_ascii=False, indent=2, default=_json_safe))
return
if not payload:
console.print("[success]OK[/success]")
return
table = Table(show_header=False)
table.add_column("Field", style="cyan", no_wrap=True)
table.add_column("Value")
for key, value in payload.items():
if isinstance(value, (list, dict)):
rendered = json.dumps(value, ensure_ascii=False, default=_json_safe)
else:
rendered = str(_json_safe(value))
table.add_row(str(key), _clip_text(rendered, 400))
console.print(table)
async def _run_service_command(
project: str | None,
operation: Any,
*,
json_output: bool = False,
render: Any | None = None,
) -> None:
from opc.plugins.office_ui.services import ServiceError
from opc.plugins.office_ui.services.factory import OfficeServiceFactory
async with OfficeServiceFactory(config=_get_config(), project_id=project) as services:
try:
result = await operation(services)
except ServiceError as exc:
if json_output:
console.print(json.dumps({"ok": False, **exc.to_payload()}, ensure_ascii=False, indent=2))
else:
console.print(f"[error]{escape(exc.message)}[/error]")
raise typer.Exit(code=1) from exc
payload = {"ok": True, **dict(result.payload)}
if render is not None and not json_output:
render(payload)
else:
_emit_payload(payload, json_output=json_output)
def _load_structured_payload(*, payload: str | None = None, file_path: str | None = None) -> Any:
raw = ""
if file_path:
raw = Path(file_path).read_text(encoding="utf-8")
elif payload:
raw = payload
if not raw.strip():
return {}
try:
return json.loads(raw)
except json.JSONDecodeError:
import yaml
return yaml.safe_load(raw) or {}
def _split_csv(value: str | None) -> list[str]:
return [item.strip() for item in str(value or "").split(",") if item.strip()]
def _exec_event_payload(value: Any) -> Any:
if hasattr(value, "model_dump"):
return value.model_dump()
if isinstance(value, dict):
return value
if hasattr(value, "__dict__"):
return dict(value.__dict__)
return _json_safe(value)
def _print_exec_event(
event_state: dict[str, Any],
event_type: str,
*,
project_id: str,
task_id: str = "",
session_id: str = "",
payload: dict[str, Any] | None = None,
) -> None:
event_state["seq"] = int(event_state.get("seq", 0) or 0) + 1
row = {
"type": event_type,
"seq": event_state["seq"],
"timestamp": datetime.now(timezone.utc).isoformat(),
"project_id": project_id,
"task_id": task_id,
"session_id": session_id,
"payload": payload or {},
}
sys.stdout.write(json.dumps(row, ensure_ascii=False, default=_json_safe) + "\n")
sys.stdout.flush()
def _exec_title(prompt: str) -> str:
for line in str(prompt or "").splitlines():
title = line.strip()
if title:
return title[:120]
return "Exec Session"
async def _task_id_for_session(services: Any, *, project_id: str, session_id: str) -> str:
engine = await services.context.engine_for_project(project_id)
store = getattr(engine, "store", None)
if not store or not session_id:
return ""
tasks = await store.get_tasks(project_id=project_id) if hasattr(store, "get_tasks") else []
for task in tasks:
if str(getattr(task, "session_id", "") or "") == session_id:
return str(getattr(task, "id", "") or "")
return ""
async def _resolve_exec_session(
services: Any,
*,
project_id: str,
prompt: str,
mode: str,
company_profile: str,
preferred_agent: str | None,
org_id: str | None,
session_id: str | None,
resume: bool,
) -> dict[str, Any]:
from opc.plugins.office_ui.services import ServiceError
normalized_mode = str(mode or "task").strip().lower()
if normalized_mode == "custom":
normalized_mode = "org"
if normalized_mode not in {"task", "company", "org"}:
raise ServiceError("invalid_mode", "mode must be task, company, or org", {"mode": mode})
if session_id:
task_id = await _task_id_for_session(services, project_id=project_id, session_id=session_id)
if not task_id:
raise ServiceError("session_not_task_backed", "Session is not linked to a task-backed CLI/UI session", {"session_id": session_id})
detail = await services.session.detail(project_id=project_id, task_id=task_id, session_id=session_id, limit=1)
return {**detail.payload, "task_id": task_id, "session_id": session_id, "restored": True}
if resume:
starting = await services.session.resolve_starting_session(project_id=project_id)
payload = dict(starting.payload)
restored_session_id = str(payload.get("session_id", "") or "")
restored_task_id = str(payload.get("task_id", "") or "")
if restored_session_id and not restored_task_id:
restored_task_id = await _task_id_for_session(services, project_id=project_id, session_id=restored_session_id)
if not restored_task_id:
raise ServiceError("session_not_task_backed", "Latest session is not task-backed; create a new exec session without --resume", {"session_id": restored_session_id})
payload["task_id"] = restored_task_id
payload["session_id"] = restored_session_id
payload["restored"] = True
return payload
created = await services.session.create(
project_id=project_id,
title=_exec_title(prompt),
exec_mode=normalized_mode,
company_profile="custom" if normalized_mode == "org" else company_profile,
preferred_agent=preferred_agent,
org_id=org_id,
interface="cli_exec",
)
return dict(created.payload)
async def _exec_message(
*,
config: OPCConfig,
prompt: str,
project: str | None,
mode: str,
company_profile: str,
preferred_agent: str | None,
org_id: str | None,
session_id: str | None,
resume: bool,
json_output: bool,
stream_json: bool,
no_markdown: bool,
) -> None:
from opc.plugins.office_ui.services import ServiceError
from opc.plugins.office_ui.services.factory import OfficeServiceFactory
project_id = str(project or "default").strip() or "default"
event_state: dict[str, Any] = {"seq": 0, "task_id": "", "session_id": ""}
async def on_progress(*args: Any, **kwargs: Any) -> None:
if not stream_json:
return
_print_exec_event(
event_state,
"runtime_update",
project_id=project_id,
task_id=str(event_state.get("task_id", "") or ""),
session_id=str(event_state.get("session_id", "") or ""),
payload={"args": [_exec_event_payload(arg) for arg in args], "kwargs": kwargs},
)
async def on_runtime_event(event: Any) -> None:
if not stream_json:
return
_print_exec_event(
event_state,
"runtime_update",
project_id=project_id,
task_id=str(event_state.get("task_id", "") or ""),
session_id=str(event_state.get("session_id", "") or ""),
payload=_exec_event_payload(event),
)
try:
async with OfficeServiceFactory(
config=config,
project_id=project_id,
on_progress=on_progress,
on_runtime_event=on_runtime_event,
on_escalation=_escalation_callback(),
) as services:
target = await _resolve_exec_session(
services,
project_id=project_id,
prompt=prompt,
mode=mode,
company_profile=company_profile,
preferred_agent=preferred_agent,
org_id=org_id,
session_id=session_id,
resume=resume,
)
task_id = str(target.get("task_id", "") or "")
resolved_session_id = str(target.get("session_id", "") or "")
event_state["task_id"] = task_id
event_state["session_id"] = resolved_session_id
if stream_json:
_print_exec_event(
event_state,
"session_created" if not target.get("restored") else "session_resumed",
project_id=project_id,
task_id=task_id,
session_id=resolved_session_id,
payload=target,
)
response = ""
if prompt:
normalized_mode = str(mode or "task").strip().lower()
send_mode = "company" if normalized_mode in {"company", "org", "custom"} else "task"
send_profile = "custom" if normalized_mode in {"org", "custom"} else company_profile
sent = await services.session.send(
project_id=project_id,
task_id=task_id,
content=prompt,
mode=send_mode,
company_profile=send_profile,
preferred_agent=preferred_agent,
)
response = str(sent.payload.get("response", "") or "")
if stream_json:
_print_exec_event(
event_state,
"message",
project_id=project_id,
task_id=task_id,
session_id=resolved_session_id,
payload={"role": "assistant", "content": response},
)
final_payload = {
"ok": True,
"project_id": project_id,
"task_id": task_id,
"session_id": resolved_session_id,
"mode": mode,
"company_profile": company_profile,
"response": response,
}
if stream_json:
_print_exec_event(
event_state,
"final",
project_id=project_id,
task_id=task_id,
session_id=resolved_session_id,
payload=final_payload,
)
return
if json_output:
console.print(json.dumps(final_payload, ensure_ascii=False, indent=2, default=_json_safe))
return
if response:
_print_response(response, no_markdown=no_markdown)
else:
_emit_payload(final_payload)
except ServiceError as exc:
payload = {"ok": False, **exc.to_payload()}
if stream_json:
_print_exec_event(event_state, "error", project_id=project_id, payload=payload)
elif json_output:
console.print(json.dumps(payload, ensure_ascii=False, indent=2, default=_json_safe))
else:
console.print(f"[error]{escape(exc.message)}[/error]")
raise typer.Exit(code=1) from exc
except ValueError as exc:
payload = {"ok": False, "code": "invalid_argument", "error": str(exc)}
if stream_json:
_print_exec_event(event_state, "error", project_id=project_id, payload=payload)
elif json_output:
console.print(json.dumps(payload, ensure_ascii=False, indent=2, default=_json_safe))
else:
console.print(f"[error]{escape(str(exc))}[/error]")
raise typer.Exit(code=2) from exc
project_app = typer.Typer(help="Manage OPC projects")
app.add_typer(project_app, name="project")
@project_app.command("list")
def project_list(
project: Optional[str] = typer.Option(None, "--project", "-p", help="Active project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""List projects."""
asyncio.run(_run_service_command(project, lambda svc: svc.project.list(active_project_id=project), json_output=json_output))
@project_app.command("show")
def project_show(
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""Show project index payload."""
target = project or "default"
asyncio.run(_run_service_command(project, lambda svc: svc.project.project_index(target, include_snapshot=False), json_output=json_output))
@project_app.command("create")
def project_create(
project_id: str = typer.Argument(..., help="Project ID to create"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Active project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""Create a project."""
asyncio.run(_run_service_command(project, lambda svc: svc.project.create(project_id, active_project_id=project), json_output=json_output))
@project_app.command("switch")
def project_switch(
project_id: str = typer.Argument(..., help="Project ID to prepare/switch"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Current project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""Prepare and validate switching to a project."""
asyncio.run(_run_service_command(project, lambda svc: svc.project.switch(project_id, include_snapshot=False), json_output=json_output))
@project_app.command("rename")
def project_rename(
old_project_id: str = typer.Argument(..., help="Existing project ID"),
new_project_id: str = typer.Argument(..., help="New project ID"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Active project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""Rename a project id and move its persisted project data."""
asyncio.run(_run_service_command(project, lambda svc: svc.project.rename(old_project_id, new_project_id), json_output=json_output))
@project_app.command("delete")
def project_delete(
project_id: str = typer.Argument(..., help="Project ID to delete"),
yes: bool = typer.Option(False, "--yes", "-y", help="Confirm deletion"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Active project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""Delete a project and its persisted UI/runtime data."""
if not yes:
console.print("[warning]Destructive command requires --yes.[/warning]")
raise typer.Exit(code=1)
asyncio.run(_run_service_command(project, lambda svc: svc.project.delete(project_id), json_output=json_output))
session_app = typer.Typer(help="Manage OPC sessions")
app.add_typer(session_app, name="session")
@session_app.command("list")
def session_list(
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
limit: int = typer.Option(50, "--limit", "-n", help="Maximum rows"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
asyncio.run(_run_service_command(project, lambda svc: svc.session.list(project_id=project or "default", limit=limit), json_output=json_output))
@session_app.command("create")
def session_create(
title: str = typer.Argument("New Chat", help="Session title"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
mode: str = typer.Option("task", "--mode", help="task, company, or org"),
company_profile: str = typer.Option("corporate", "--company-profile", help="corporate or custom"),
agent: Optional[str] = typer.Option(None, "--agent", help="Preferred task-mode agent"),
org_id: Optional[str] = typer.Option(None, "--org", "--org-id", help="Saved org id"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
asyncio.run(_run_service_command(project, lambda svc: svc.session.create(project_id=project or "default", title=title, exec_mode=mode, company_profile=company_profile, preferred_agent=agent, org_id=org_id, interface="cli"), json_output=json_output))
@session_app.command("show")
def session_show(
target: str = typer.Argument(..., help="Task ID or session ID"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
limit: int = typer.Option(200, "--limit", "-n", help="Transcript limit"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
asyncio.run(_run_service_command(project, lambda svc: svc.session.detail(project_id=project or "default", task_id=target, session_id=target, limit=limit), json_output=json_output))
@session_app.command("config")
def session_config(
task_id: str = typer.Argument(..., help="Task ID"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
mode: Optional[str] = typer.Option(None, "--mode", help="task, company, or org"),
company_profile: Optional[str] = typer.Option(None, "--company-profile", help="corporate or custom"),
agent: Optional[str] = typer.Option(None, "--agent", help="Preferred task-mode agent"),
org_id: Optional[str] = typer.Option(None, "--org", "--org-id", help="Saved org id"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
asyncio.run(_run_service_command(project, lambda svc: svc.session.update_config(project_id=project or "default", task_id=task_id, exec_mode=mode, company_profile=company_profile, preferred_agent=agent, org_id=org_id), json_output=json_output))
@session_app.command("send")
def session_send(
task_id: str = typer.Argument(..., help="Task ID"),
message: str = typer.Argument(..., help="Message to send"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
mode: str = typer.Option("task", "--mode", help="task or company"),
company_profile: str = typer.Option("corporate", "--company-profile", help="Company profile"),
agent: Optional[str] = typer.Option(None, "--agent", help="Preferred agent"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
asyncio.run(_run_service_command(project, lambda svc: svc.session.send(project_id=project or "default", task_id=task_id, content=message, mode=mode, company_profile=company_profile, preferred_agent=agent), json_output=json_output))
@session_app.command("rename")
def session_rename(
target: str = typer.Argument(..., help="Task ID or session ID"),
title: str = typer.Argument(...),
project: Optional[str] = typer.Option(None, "--project", "-p"),
json_output: bool = typer.Option(False, "--json"),
):
asyncio.run(_run_service_command(project, lambda svc: svc.session.rename(project_id=project or "default", task_id=target, session_id=target, title=title), json_output=json_output))
@session_app.command("delete")
def session_delete(
task_id: str = typer.Argument(...),
yes: bool = typer.Option(False, "--yes", "-y"),
project: Optional[str] = typer.Option(None, "--project", "-p"),
json_output: bool = typer.Option(False, "--json"),
):
if not yes:
console.print("[warning]Destructive command requires --yes.[/warning]")
raise typer.Exit(code=1)
asyncio.run(_run_service_command(project, lambda svc: svc.session.delete(project_id=project or "default", task_id=task_id), json_output=json_output))
@session_app.command("stop")
def session_stop(target: str = typer.Argument(..., help="Task ID or session ID"), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.session.stop(project_id=project or "default", target=target), json_output=json_output))
@session_app.command("continue")
def session_continue(
target: str = typer.Argument(..., help="Task ID or session ID"),
message: Optional[str] = typer.Argument(None, help="Optional resume instruction"),
project: Optional[str] = typer.Option(None, "--project", "-p"),
json_output: bool = typer.Option(False, "--json"),
):
asyncio.run(_run_service_command(project, lambda svc: svc.session.continue_run(project_id=project or "default", target=target, content=message or ""), json_output=json_output))
@session_app.command("resume")
def session_resume(target: str = typer.Argument(..., help="Task ID or session ID"), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.session.resume(project_id=project or "default", target=target), json_output=json_output))
@session_app.command("complete")
def session_complete(task_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.session.complete(project_id=project or "default", task_id=task_id), json_output=json_output))
mode_app = typer.Typer(help="Manage default execution mode")
app.add_typer(mode_app, name="mode")
@mode_app.command("show")
def mode_show(project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.runtime.mode_show(), json_output=json_output))
@mode_app.command("set")
def mode_set(
mode: str = typer.Argument(..., help="task, company, or org"),
project: Optional[str] = typer.Option(None, "--project", "-p"),
profile: str = typer.Option("corporate", "--profile", "--company-profile"),
agent: Optional[str] = typer.Option(None, "--agent"),
org_id: Optional[str] = typer.Option(None, "--org", "--org-id"),
json_output: bool = typer.Option(False, "--json"),
):
asyncio.run(_run_service_command(project, lambda svc: svc.runtime.mode_set(mode=mode, profile=profile, preferred_agent=agent, org_id=org_id), json_output=json_output))
kanban_app = typer.Typer(help="Manage kanban views and tasks")
kanban_task_app = typer.Typer(help="Manage kanban tasks")
kanban_app.add_typer(kanban_task_app, name="task")
app.add_typer(kanban_app, name="kanban")
@kanban_task_app.command("create")
def kanban_task_create(title: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), description: str = typer.Option("", "--description", "-d"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.kanban.create_task(project_id=project or "default", title=title, description=description), json_output=json_output))
@kanban_task_app.command("update")
def kanban_task_update(task_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), title: Optional[str] = typer.Option(None, "--title"), description: Optional[str] = typer.Option(None, "--description"), json_output: bool = typer.Option(False, "--json")):
updates = {k: v for k, v in {"title": title, "description": description}.items() if v is not None}
asyncio.run(_run_service_command(project, lambda svc: svc.kanban.update_task(project_id=project or "default", task_id=task_id, updates=updates), json_output=json_output))
@kanban_task_app.command("move")
def kanban_task_move(task_id: str = typer.Argument(...), column: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.kanban.move_task(project_id=project or "default", task_id=task_id, column_id=column), json_output=json_output))
@kanban_task_app.command("delete")
def kanban_task_delete(task_id: str = typer.Argument(...), yes: bool = typer.Option(False, "--yes", "-y"), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
if not yes:
console.print("[warning]Destructive command requires --yes.[/warning]")
raise typer.Exit(code=1)
asyncio.run(_run_service_command(project, lambda svc: svc.kanban.delete_task(project_id=project or "default", task_id=task_id), json_output=json_output))
@kanban_task_app.command("assign")
def kanban_task_assign(task_id: str = typer.Argument(...), agent_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.kanban.assign(project_id=project or "default", task_id=task_id, agent_id=agent_id), json_output=json_output))
@kanban_task_app.command("status")
def kanban_task_status(task_id: str = typer.Argument(...), status: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.kanban.status(project_id=project or "default", task_id=task_id, status=status), json_output=json_output))
@kanban_app.command("view")
def kanban_view(project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.project.project_index(project or "default", include_snapshot=False), json_output=json_output))
agent_app = typer.Typer(help="Manage UI agents")
app.add_typer(agent_app, name="agent")
@agent_app.command("list")
def agent_list(project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.agent.list(), json_output=json_output))
@agent_app.command("create")
def agent_create(name: str = typer.Argument(...), role_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), office_id: str = typer.Option("office-0", "--office"), description: str = typer.Option("", "--description"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.agent.create(name=name, role_id=role_id, office_id=office_id, description=description), json_output=json_output))
@agent_app.command("create-from-template")
def agent_create_from_template(template_id: str = typer.Argument(...), role_id: Optional[str] = typer.Option(None, "--role", "--role-id"), project: Optional[str] = typer.Option(None, "--project", "-p"), office_id: str = typer.Option("office-0", "--office"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.agent.create_from_template(template_id=template_id, role_id=role_id or template_id, office_id=office_id), json_output=json_output))
@agent_app.command("import-employee")
def agent_import_employee(employee_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), office_id: str = typer.Option("office-0", "--office"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.agent.import_employee(employee_id=employee_id, office_id=office_id), json_output=json_output))
@agent_app.command("detail")
def agent_detail(agent_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.agent.detail(project_id=project or "default", agent_id=agent_id), json_output=json_output))
@agent_app.command("delete")
def agent_delete(agent_id: str = typer.Argument(...), yes: bool = typer.Option(False, "--yes", "-y"), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
if not yes:
console.print("[warning]Destructive command requires --yes.[/warning]")
raise typer.Exit(code=1)
asyncio.run(_run_service_command(project, lambda svc: svc.agent.delete(agent_id), json_output=json_output))
@agent_app.command("move")
def agent_move(agent_id: str = typer.Argument(...), office_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), seat_zone: Optional[str] = typer.Option(None, "--seat-zone"), desk_id: Optional[str] = typer.Option(None, "--desk"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.agent.move(agent_id=agent_id, office_id=office_id, seat_zone=seat_zone, desk_id=desk_id), json_output=json_output))
org_app = typer.Typer(help="Manage organization configuration")
app.add_typer(org_app, name="org")
@org_app.command("info")
def org_info(project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.org.info(), json_output=json_output))
@org_app.command("export")
def org_export(project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(True, "--json/--no-json")):
asyncio.run(_run_service_command(project, lambda svc: svc.org.export_config(), json_output=json_output))
@org_app.command("import")
def org_import(
path: str = typer.Argument(..., help="YAML or JSON org config payload"),
dry_run: bool = typer.Option(False, "--dry-run", help="Validate and preview without applying"),
project: Optional[str] = typer.Option(None, "--project", "-p"),
json_output: bool = typer.Option(False, "--json"),
):
raw = Path(path).read_text(encoding="utf-8")
asyncio.run(_run_service_command(project, lambda svc: svc.org.import_config(raw, dry_run=dry_run), json_output=json_output))
org_saved_app = typer.Typer(help="Manage saved org architectures")
org_app.add_typer(org_saved_app, name="saved")
@org_saved_app.command("list")
def org_saved_list(project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.org.saved_list(), json_output=json_output))
@org_saved_app.command("save")
def org_saved_save(name: str = typer.Argument(...), overwrite: bool = typer.Option(False, "--overwrite"), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.org.saved_save_as(name, overwrite=overwrite), json_output=json_output))
@org_saved_app.command("load")
def org_saved_load(name: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.org.saved_load(name), json_output=json_output))
@org_saved_app.command("delete")
def org_saved_delete(name: str = typer.Argument(...), yes: bool = typer.Option(False, "--yes", "-y"), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
if not yes:
console.print("[warning]Destructive command requires --yes.[/warning]")
raise typer.Exit(code=1)
asyncio.run(_run_service_command(project, lambda svc: svc.org.saved_delete(name), json_output=json_output))
org_role_app = typer.Typer(help="Manage organization roles")
org_app.add_typer(org_role_app, name="role")
@org_role_app.command("add")
def org_role_add(role_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), name: Optional[str] = typer.Option(None, "--name"), responsibility: str = typer.Option("", "--responsibility"), reports_to: str = typer.Option("owner", "--reports-to"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.org.add_role({"role_id": role_id, "name": name or role_id, "responsibility": responsibility, "reports_to": reports_to}), json_output=json_output))
@org_role_app.command("update")
def org_role_update(
role_id: str = typer.Argument(...),
project: Optional[str] = typer.Option(None, "--project", "-p"),
name: Optional[str] = typer.Option(None, "--name"),
responsibility: Optional[str] = typer.Option(None, "--responsibility"),
reports_to: Optional[str] = typer.Option(None, "--reports-to"),
can_spawn: Optional[str] = typer.Option(None, "--can-spawn", help="Comma-separated role ids"),
tools: Optional[str] = typer.Option(None, "--tools", help="Comma-separated tool names"),
agent: Optional[str] = typer.Option(None, "--agent", help="Preferred external agent"),
json_output: bool = typer.Option(False, "--json"),
):
updates = {
key: value
for key, value in {
"name": name,
"responsibility": responsibility,
"reports_to": reports_to,
"preferred_external_agent": agent,
}.items()
if value is not None
}
if can_spawn is not None:
updates["can_spawn"] = _split_csv(can_spawn)
if tools is not None:
updates["tools"] = _split_csv(tools)
asyncio.run(_run_service_command(project, lambda svc: svc.org.update_role(role_id, updates), json_output=json_output))
@org_role_app.command("bulk-add")
def org_role_bulk_add(path: str = typer.Argument(..., help="JSON/YAML list of role objects or object with roles"), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
loaded = _load_structured_payload(file_path=path)
roles = loaded.get("roles", []) if isinstance(loaded, dict) else loaded if isinstance(loaded, list) else []
asyncio.run(_run_service_command(project, lambda svc: svc.org.bulk_add_roles(list(roles or [])), json_output=json_output))
@org_role_app.command("delete")
def org_role_delete(role_id: str = typer.Argument(...), yes: bool = typer.Option(False, "--yes", "-y"), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
if not yes:
console.print("[warning]Destructive command requires --yes.[/warning]")
raise typer.Exit(code=1)
asyncio.run(_run_service_command(project, lambda svc: svc.org.delete_role(role_id), json_output=json_output))
org_policy_app = typer.Typer(help="Manage organization runtime policy")
org_app.add_typer(org_policy_app, name="policy")
@org_policy_app.command("update")
def org_policy_update(payload: Optional[str] = typer.Option(None, "--payload", help="JSON/YAML object"), file_path: Optional[str] = typer.Option(None, "--file", "-f"), profile: str = typer.Option("custom", "--profile"), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
data = _load_structured_payload(payload=payload, file_path=file_path)
asyncio.run(_run_service_command(project, lambda svc: svc.org.update_runtime_policy(data, profile=profile), json_output=json_output))
org_strategy_app = typer.Typer(help="Manage organization strategy")
org_app.add_typer(org_strategy_app, name="strategy")
@org_strategy_app.command("update")
def org_strategy_update(final_decider_role_id: Optional[str] = typer.Option(None, "--final-decider", "--final-decider-role-id"), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.org.update_org_strategy(final_decider_role_id=final_decider_role_id), json_output=json_output))
@org_app.command("reset")
def org_reset(yes: bool = typer.Option(False, "--yes", "-y"), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
if not yes:
console.print("[warning]Destructive command requires --yes.[/warning]")
raise typer.Exit(code=1)
asyncio.run(_run_service_command(project, lambda svc: svc.org.reset_architecture(), json_output=json_output))
runtime_app = typer.Typer(help="Inspect and control runtime state")
app.add_typer(runtime_app, name="runtime")
@runtime_app.command("status")
def runtime_status(project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.runtime.status(project_id=project or "default"), json_output=json_output))
@runtime_app.command("checkpoints")
def runtime_checkpoints(project: Optional[str] = typer.Option(None, "--project", "-p"), limit: int = typer.Option(50, "--limit", "-n"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.runtime.checkpoints(project_id=project or "default", limit=limit), json_output=json_output))
@runtime_app.command("logs")
def runtime_logs(task_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), limit: int = typer.Option(100, "--limit", "-n"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.runtime.logs(project_id=project or "default", task_id=task_id, limit=limit), json_output=json_output))
@runtime_app.command("run")
def runtime_run(task_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.runtime.run_task(project_id=project or "default", task_id=task_id), json_output=json_output))
comms_app = typer.Typer(help="Inspect company-mode comms")
app.add_typer(comms_app, name="comms")
@comms_app.command("state")
def comms_state(task_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.comms.state(project_id=project or "default", task_id=task_id), json_output=json_output))
@comms_app.command("read")
def comms_read(task_id: str = typer.Argument(...), path: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.comms.read(project_id=project or "default", task_id=task_id, path=path), json_output=json_output))
work_item_app = typer.Typer(help="Inspect company-mode work items")
app.add_typer(work_item_app, name="work-item")
@work_item_app.command("list")
def work_item_list(project: Optional[str] = typer.Option(None, "--project", "-p"), role_id: Optional[str] = typer.Option(None, "--role", "--role-id"), status: Optional[str] = typer.Option(None, "--status"), limit: int = typer.Option(100, "--limit", "-n"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.work_item.list(project_id=project or "default", role_id=role_id, status=status, limit=limit), json_output=json_output))
@work_item_app.command("show")
def work_item_show(work_item_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), limit: int = typer.Option(100, "--limit", "-n"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.work_item.show(project_id=project or "default", work_item_id=work_item_id, limit=limit), json_output=json_output))
@work_item_app.command("logs")
def work_item_logs(work_item_id: str = typer.Argument(""), role_id: Optional[str] = typer.Option(None, "--role", "--role-id"), project: Optional[str] = typer.Option(None, "--project", "-p"), limit: int = typer.Option(100, "--limit", "-n"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.work_item.logs(project_id=project or "default", work_item_id=work_item_id, role_id=role_id or "", limit=limit), json_output=json_output))
@work_item_app.command("role-status")
def work_item_role_status(project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.work_item.status_by_role(project_id=project or "default"), json_output=json_output))
channels_app = typer.Typer(help="Manage external messaging channels")
app.add_typer(channels_app, name="channels")
talent_app = typer.Typer(help="Manage recruitable talent templates and employees")
app.add_typer(talent_app, name="talent")
@talent_app.command("list")
def talent_list(
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""List imported talent templates."""
config = _get_config()
market = TalentMarket(get_opc_home(), config)
templates = market.list_templates()
if json_output:
_emit_payload({"templates": templates}, json_output=True)
return
if not templates:
console.print("[warning]No imported talent templates found.[/warning]")
return
console.print(f"[bold]Talent Templates ({len(templates)}):[/bold]")
for template in templates:
domains = ", ".join(template.domains[:4]) if template.domains else "general"
console.print(
f" - [bold]{template.id}[/bold] :: {template.name} "
f"[{template.category}] domains={domains}"
)
@talent_app.command("employees")
def talent_employees(
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""List hired employees."""
config = _get_config()
market = TalentMarket(get_opc_home(), config)
employees = market.list_employees()
if json_output:
_emit_payload({"employees": employees}, json_output=True)
return
if not employees:
console.print("[warning]No employees hired yet.[/warning]")
return
console.print(f"[bold]Employees ({len(employees)}):[/bold]")
for employee in employees:
domains = ", ".join(employee.domains[:4]) if employee.domains else "general"
console.print(
f" - [bold]{employee.employee_id}[/bold] :: {employee.name} "
f"role={employee.role_id} domains={domains}"
)
@talent_app.command("import")
def talent_import(
repo_path: str = typer.Argument(..., help="Local path to the agency-agents repository"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""Import local agency-agent markdown files into recruitable talent templates."""
config = _get_config()
market = TalentMarket(get_opc_home(), config)
imported = market.import_from_repo(_talent_repo_path(repo_path))
config.save(get_opc_home() / "config")
if json_output:
_emit_payload({"count": len(imported), "imported": imported}, json_output=True)
return
console.print(f"[success]Imported {len(imported)} talent templates.[/success]")
@talent_app.command("hire")
def talent_hire(
template_id: str = typer.Argument(..., help="Imported talent template id"),
role_id: str = typer.Argument(..., help="Company role id to staff"),
employee_name: Optional[str] = typer.Option(None, "--name", help="Optional hired employee display name"),
employee_id: Optional[str] = typer.Option(None, "--employee-id", help="Optional employee id override"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""Hire an imported template into a company role."""
config = _get_config()
market = TalentMarket(get_opc_home(), config)
employee = market.hire_template(
template_id,
role_id,
employee_name=employee_name,
employee_id=employee_id,
)
config.save(get_opc_home() / "config")
if json_output:
_emit_payload({"employee": employee}, json_output=True)
return
console.print(
f"[success]Hired {employee.name} into role `{employee.role_id}` "
f"as `{employee.employee_id}`.[/success]"
)
@talent_app.command("scan")
def talent_scan(project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
"""Scan local talent templates."""
asyncio.run(_run_service_command(project, lambda svc: svc.talent.scan(), json_output=json_output))
@talent_app.command("import-selected")
def talent_import_selected(template_ids: list[str] = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
"""Import selected local talent templates."""
asyncio.run(_run_service_command(project, lambda svc: svc.talent.import_selected(template_ids), json_output=json_output))
@talent_app.command("employee-detail")
def talent_employee(employee_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
"""Show employee detail."""
asyncio.run(_run_service_command(project, lambda svc: svc.talent.employee_detail(employee_id), json_output=json_output))
@talent_app.command("import-agent")
def talent_import_agent(employee_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
"""Import an employee as a visual office agent."""
asyncio.run(_run_service_command(project, lambda svc: svc.talent.import_employee_as_agent(employee_id=employee_id), json_output=json_output))
talent_employee_app = typer.Typer(help="Manage hired employees")
talent_app.add_typer(talent_employee_app, name="employee")
@talent_employee_app.command("detail")
def talent_employee_detail(employee_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.talent.employee_detail(employee_id), json_output=json_output))
@talent_employee_app.command("import-agent")
def talent_employee_import_agent(employee_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), office_id: str = typer.Option("office-0", "--office"), json_output: bool = typer.Option(False, "--json")):
asyncio.run(_run_service_command(project, lambda svc: svc.talent.import_employee_as_agent(employee_id=employee_id, office_id=office_id), json_output=json_output))
# ---------------------------------------------------------------------------
# Market subcommands
# ---------------------------------------------------------------------------
market_app = typer.Typer(help="OPC Market — export, import, and manage architecture packages")
app.add_typer(market_app, name="market")
def _render_architecture_presets(presets: list[Any]) -> None:
if not presets:
console.print("[warning]No architecture presets available.[/warning]")
return
table = Table(title=f"Architecture Presets ({len(presets)})")
table.add_column("ID", style="cyan", no_wrap=True)
table.add_column("Name")
table.add_column("Category")
table.add_column("Roles", justify="right")
table.add_column("Templates", justify="right")
table.add_column("Pattern")
for preset in presets:
table.add_row(
str(getattr(preset, "id", "") or ""),
str(getattr(preset, "name", "") or ""),
str(getattr(preset, "category", "") or ""),
str(len(getattr(preset, "roles", []) or [])),
str(len(getattr(preset, "work_item_templates", []) or [])),
str(getattr(preset, "collaboration_pattern", "") or ""),
)
console.print(table)
@market_app.command("export")
def market_export(
package_id: str = typer.Option(..., "--id", help="Package identifier (slug)"),
name: str = typer.Option(..., "--name", help="Display name"),
description: str = typer.Option("", "--desc", help="Package description"),
version: str = typer.Option("1.0.0", "--version", help="Semantic version"),
output_dir: str = typer.Option(".", "--output-dir", "-o", help="Output directory"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""Export the current org as an .opcpkg package."""
asyncio.run(_run_service_command(project, lambda svc: svc.market.export(package_id=package_id, name=name, description=description, version=version, output_dir=output_dir), json_output=json_output))
@market_app.command("browse")
def market_browse(project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
"""Browse market architecture presets."""
asyncio.run(_run_service_command(project, lambda svc: svc.market.browse(), json_output=json_output))
@market_app.command("preview")
def market_preview(preset_id: str = typer.Argument(...), project: Optional[str] = typer.Option(None, "--project", "-p"), json_output: bool = typer.Option(False, "--json")):
"""Preview a built-in architecture preset."""
asyncio.run(_run_service_command(project, lambda svc: svc.market.preview(preset_id), json_output=json_output))
@market_app.command("presets")
def market_presets(
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""List built-in architecture presets."""
from opc.market.architecture_registry import get_all_presets
if json_output:
_emit_payload({"presets": get_all_presets()}, json_output=True)
return
_render_architecture_presets(get_all_presets())
@market_app.command("apply-preset")
def market_apply_preset(
preset_id: str = typer.Argument(..., help="Built-in architecture preset id"),
strategy: str = typer.Option("overwrite", "--strategy", "-s", help="Role id strategy: namespace or overwrite"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""Apply a built-in architecture preset as the active custom organization."""
if strategy not in {"namespace", "overwrite"}:
console.print("[error]Strategy must be namespace or overwrite.[/error]")
raise typer.Exit(code=1)
asyncio.run(_run_service_command(project, lambda svc: svc.market.apply_preset(preset_id=preset_id, strategy=strategy), json_output=json_output))
@market_app.command("install")
def market_install(
path: str = typer.Argument(..., help="Path to .opcpkg directory"),
strategy: str = typer.Option("namespace", "--strategy", "-s", help="Conflict strategy: namespace or overwrite"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""Install an .opcpkg package from a local path."""
asyncio.run(_run_service_command(project, lambda svc: svc.market.install(path=path, strategy=strategy), json_output=json_output))
@market_app.command("list")
def market_list(
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""List installed OPC Market packages."""
asyncio.run(_run_service_command(project, lambda svc: svc.market.list_installed(), json_output=json_output))
@market_app.command("uninstall")
def market_uninstall(
package_id: str = typer.Argument(..., help="Package id to uninstall"),
yes: bool = typer.Option(False, "--yes", "-y", help="Confirm uninstall"),
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project context"),
json_output: bool = typer.Option(False, "--json", help="Print JSON"),
):
"""Uninstall an installed OPC Market package."""
if not yes:
console.print("[warning]Destructive command requires --yes.[/warning]")
raise typer.Exit(code=1)
asyncio.run(_run_service_command(project, lambda svc: svc.market.uninstall(package_id), json_output=json_output))
@channels_app.command("status")
def channels_status():
"""Show configured channel status."""
config = _get_config()
from opc.channels.manager import ChannelManager
from opc.channels.provider_registry import ordered_provider_specs
from opc.layer0_interaction.message_bus import MessageBus
runtime_state = _read_channel_runtime_state()
runtime_running = False
runtime_pid = None
runtime_channels: list[str] = []
if runtime_state:
runtime_pid = int(runtime_state.get("pid", 0) or 0)
runtime_channels = list(runtime_state.get("channels", []) or [])
runtime_running = runtime_pid > 0 and _pid_is_running(runtime_pid)
if runtime_pid > 0 and not runtime_running:
_clear_channel_runtime_state()
manager = ChannelManager(config, MessageBus())
console.print(f"[bold]Runtime:[/bold] {'running' if runtime_running else 'stopped'}")
if runtime_pid:
console.print(f" PID: {runtime_pid}")
console.print("[bold]Channels:[/bold]")
for spec in ordered_provider_specs():
if hasattr(manager, "get_status"):
status = manager.get_status(spec.name)
else:
channel = manager.get_channel(spec.name)
capability = channel.describe_capability() if channel is not None else {}
status = {
"name": spec.name,
"enabled": bool(getattr(getattr(config.channels, spec.name), "enabled", False)),
"available": capability.get("available", channel is not None),
"configured": capability.get("configured", channel is not None),
"delivery_mode": capability.get("delivery_mode", spec.delivery_mode),
"bridge_required": spec.bridge_required,
"last_error": capability.get("last_error", ""),
}
details: list[str] = ["enabled" if status["enabled"] else "disabled"]
if status["enabled"]:
details.append("available" if status["available"] else "dependency-missing")
details.append("configured" if status["configured"] else "config-missing")
if status["bridge_required"]:
details.append("bridge-required")
if status["delivery_mode"]:
details.append(str(status["delivery_mode"]))
if runtime_running and spec.name in runtime_channels:
details.append("runtime-active")
if status["last_error"]:
details.append(f"error={status['last_error']}")
console.print(f" - {spec.name}: {', '.join(details)}")
@channels_app.command("start")
def channels_start(project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID")):
"""Start enabled channels in foreground."""
config = _get_config()
asyncio.run(_run_channel_runtime(config, project))
@channels_app.command("stop")
def channels_stop():
"""Stop foreground channel runtime."""
runtime_state = _read_channel_runtime_state()
if not runtime_state:
console.print("[warning]No running channel runtime found.[/warning]")
return
pid = int(runtime_state.get("pid", 0) or 0)
if pid <= 0 or not _pid_is_running(pid):
_clear_channel_runtime_state()
console.print("[warning]Channel runtime is not running.[/warning]")
return
os.kill(pid, signal.SIGTERM)
console.print(f"[success]Sent stop signal to channel runtime PID {pid}.[/success]")
@channels_app.command("login")
def channels_login(channel: Optional[str] = typer.Argument(None, help="Optional channel name")):
"""Show login/setup guidance for channels."""
from opc.channels.provider_registry import PROVIDER_SPECS, ordered_provider_specs
if channel:
spec = PROVIDER_SPECS.get(channel)
if spec is None:
console.print(f"[warning]Unknown channel `{channel}`.[/warning]")
return
requirements = ", ".join(spec.required_config_fields) if spec.required_config_fields else "no required config fields"
bridge = " Requires a companion bridge/runtime." if spec.bridge_required else ""
console.print(
f"[info]Configure `{channel}` in `.opc/config/channel_config.yaml`. "
f"{escape(spec.login_summary)} Install extras with `{escape(f'pip install -e .[{spec.extra_name}]')}`. "
f"Required config: {escape(requirements)}.{bridge} Then run `opc channels start`.[/info]"
)
return
console.print("[info]Set channel credentials in .opc/config/channel_config.yaml and start the runtime with `opc channels start` or `opc run`.[/info]")
for spec in ordered_provider_specs():
bridge = " bridge-required;" if spec.bridge_required else ""
console.print(
f" - {spec.name}: install `.[{spec.extra_name}]`; required fields: "
f"{', '.join(spec.required_config_fields) or 'none'}; mode: {spec.delivery_mode};{bridge} {spec.login_summary}"
)
@app.command()
def run(project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID")):
"""Run the long-lived engine + channel runtime in foreground."""
config = _get_config()
asyncio.run(_run_channel_runtime(config, project))
# ---------------------------------------------------------------------------
# Async helpers
# ---------------------------------------------------------------------------
async def _single_message(
config,
message: str,
project: str | None,
no_markdown: bool,
*,
mode: str = "task",
preferred_agent: str | None = None,
company_profile: str = "corporate",
) -> None:
engine, runtime_display = _create_cli_engine(config, project)
try:
await engine.initialize()
console.print(f"\n[info]Processing:[/info] {message}\n")
runtime_display.begin_turn()
response = await engine.process_message(
message,
project_id=project,
session_id=str(uuid.uuid4()),
mode=mode,
company_profile=company_profile if mode == "company" else None,
preferred_agent=preferred_agent,
)
await runtime_display.flush()
if not runtime_display.has_streamed_content:
_print_response(response, no_markdown)
except KeyboardInterrupt:
console.print("\n[warning]Interrupted.[/warning]")
except Exception as e:
console.print(f"\n[error]Error: {escape(str(e))}[/error]")
if config.system.log_level == "DEBUG":
import traceback
console.print(escape(traceback.format_exc()))
finally:
await engine.shutdown()
async def _single_secretary_message(config, message: str, project: str | None, no_markdown: bool) -> None:
engine, runtime_display = _create_cli_engine(config, project)
try:
await engine.initialize()
console.print(f"\n[info]Secretary processing:[/info] {message}\n")
runtime_display.begin_turn()
payload = await engine.process_secretary_message(
message,
project_id=project,
session_id=str(uuid.uuid4()),
)
await runtime_display.flush()
if not runtime_display.has_streamed_content:
_print_response(payload.get("response", ""), no_markdown)
except KeyboardInterrupt:
console.print("\n[warning]Interrupted.[/warning]")
except Exception as e:
console.print(f"\n[error]Error: {escape(str(e))}[/error]")
if config.system.log_level == "DEBUG":
import traceback
console.print(escape(traceback.format_exc()))
finally:
await engine.shutdown()
async def _propose_reorg(config, payload: str, project: str | None) -> None:
engine, _runtime_display = _create_cli_engine(config, project)
try:
await engine.initialize()
data = json.loads(payload)
proposal = await engine.propose_company_reorg(
summary=str(data.get("summary", "Runtime company reorg")),
rationale=str(data.get("rationale", data.get("summary", ""))),
title=str(data.get("title", "")),
changeset=data.get("changeset", {}),
session_id=str(uuid.uuid4()),
task_id=data.get("task_id"),
initiated_by=str(data.get("initiated_by", "owner")),
source_role_id=str(data.get("source_role_id", "")),
metadata={"source": "cli"},
)
console.print(f"[success]Created reorg proposal:[/success] {proposal.proposal_id}")
console.print(f"Status: {proposal.status.value}")
console.print(f"Scope: {proposal.scope.value}")
console.print(f"Risk: {proposal.risk_level.value}")
console.print(f"Summary: {proposal.summary}")
if proposal.user_confirmation_required:
await engine._save_reorg_checkpoint(proposal) # noqa: SLF001
console.print("[warning]User confirmation is required before applying this reorg.[/warning]")
finally:
await engine.shutdown()
async def _approve_reorg(config, proposal_id: str, project: str | None, *, approved: bool) -> None:
engine, _runtime_display = _create_cli_engine(config, project)
try:
await engine.initialize()
proposal = await engine.approve_company_reorg(
proposal_id,
approved=approved,
notes=f"{'Approved' if approved else 'Denied'} via CLI.",
)
console.print(f"[success]Updated reorg:[/success] {proposal.proposal_id}")
console.print(f"Status: {proposal.status.value}")
finally:
await engine.shutdown()
async def _apply_reorg(config, proposal_id: str, project: str | None) -> None:
engine, _runtime_display = _create_cli_engine(config, project)
try:
await engine.initialize()
result = await engine.apply_company_reorg(proposal_id)
console.print(f"[success]Applied reorg:[/success] {proposal_id}")
console.print(json.dumps(result, ensure_ascii=False, indent=2))
finally:
await engine.shutdown()
async def _show_reorg(config, proposal_id: str, project: str | None) -> None:
engine, _runtime_display = _create_cli_engine(config, project)
try:
await engine.initialize()
proposal = await engine.show_company_reorg(proposal_id)
if not proposal:
console.print(f"[warning]Unknown reorg proposal:[/warning] {proposal_id}")
return
console.print(json.dumps({
"proposal_id": proposal.proposal_id,
"status": proposal.status.value,
"scope": proposal.scope.value,
"risk_level": proposal.risk_level.value,
"summary": proposal.summary,
"rationale": proposal.rationale,
"impact_summary": proposal.impact_summary,
"approval_notes": proposal.approval_notes,
"metadata": proposal.metadata,
}, ensure_ascii=False, indent=2))
finally:
await engine.shutdown()
_SLASH_DEFAULT_LIMIT = 20
_SLASH_MAX_LIMIT = 100
_VALID_CHAT_MODES = {"task", "company"}
_VALID_COMPANY_PROFILES = {"corporate", "custom"}
_VALID_PREFERRED_AGENTS = {"native", "codex", "claude_code", "cursor", "opencode"}
@dataclass(frozen=True)
class _SlashCommandSpec:
group: str
command: str
description: str
subcommands: tuple[str, ...] = ()
@dataclass
class _InteractiveChatState:
config: OPCConfig
engine: Any
runtime_display: Any
session_id: str
no_markdown: bool
mode: str = "task"
company_profile: str = "corporate"
org_id: str = ""
preferred_agent: str | None = "native"
domains: list[str] = field(default_factory=list)
company_staffing_drafts: dict[str, dict[str, Any]] = field(default_factory=dict)
session_to_task: dict[str, str] = field(default_factory=dict)
active_runtime_children: dict[str, str] = field(default_factory=dict)
runtime_control_state: str = ""
runtime_control_task_id: str = ""
runtime_control_session_id: str = ""
runtime_control_checkpoint_id: str = ""
class BusyCommandPolicy(str, Enum):
IMMEDIATE_READONLY = "immediate_readonly"
BLOCKED_WHEN_BUSY = "blocked_when_busy"
NORMAL_WHEN_IDLE = "normal_when_idle"
@dataclass
class QueuedChatInput:
text: str
project_id: str
session_id: str
mode: str
company_profile: str
preferred_agent: str | None
domains: list[str]
org_id: str = ""
message_metadata: dict[str, Any] | None = None
allow_interactive_followups: bool = False
enqueue_time: float = field(default_factory=time.time)
class ChatTurnController:
"""Keep interactive chat responsive while a turn is running."""
def __init__(self, state: _InteractiveChatState) -> None:
self.state = state
self.queue: deque[QueuedChatInput] = deque()
self.active_task: asyncio.Task[None] | None = None
self.kanban_watch_task: asyncio.Task[None] | None = None
self._kanban_watch_fingerprint = ""
self._lock: asyncio.Lock | None = None
self._closing = False
@property
def is_busy(self) -> bool:
return self.active_task is not None and not self.active_task.done()
def queue_depth(self) -> int:
return len(self.queue)
def kanban_watch_active(self) -> bool:
return self.kanban_watch_task is not None and not self.kanban_watch_task.done()
def queued_items(self) -> list[QueuedChatInput]:
return list(self.queue)
async def submit_user_message(self, text: str) -> None:
item = QueuedChatInput(
text=text,
project_id=_current_project_id(self.state.engine),
session_id=self.state.session_id,
mode=self.state.mode,
company_profile=self.state.company_profile,
org_id=self.state.org_id,
preferred_agent=self.state.preferred_agent,
domains=list(self.state.domains),
)
await self.submit_item(item)
async def submit_control_message(
self,
text: str,
*,
message_metadata: dict[str, Any],
mode: str | None = None,
company_profile: str | None = None,
org_id: str | None = None,
preferred_agent: str | None = None,
domains: list[str] | None = None,
) -> None:
item = QueuedChatInput(
text=text,
project_id=_current_project_id(self.state.engine),
session_id=self.state.session_id,
mode=mode or self.state.mode,
company_profile=company_profile or self.state.company_profile,
org_id=self.state.org_id if org_id is None else org_id,
preferred_agent=self.state.preferred_agent if preferred_agent is None else preferred_agent,
domains=list(self.state.domains if domains is None else domains),
message_metadata=dict(message_metadata),
)
await self.submit_item(item)
async def submit_item(self, item: QueuedChatInput) -> None:
async with self._ensure_lock():
if self.is_busy:
self.queue.append(item)
console.print(
f"[dim]Queued #{len(self.queue)}; it will run after the current turn. "
"Use /queue list or /queue drop <n>.[/dim]"
)
return
self._start_item_locked(item)
async def wait_for_active(self) -> None:
task = self.active_task
if task is not None:
await task
async def shutdown(self) -> None:
self._closing = True
self.queue.clear()
await self.stop_kanban_watch(silent=True)
prepare = getattr(
self.state.engine,
"prepare_active_company_runtimes_for_shutdown",
None,
)
if callable(prepare):
# Persist company checkpoints and put the engine into
# infrastructure-shutdown mode before cancelling the active turn.
# Engine.shutdown() repeats this idempotently when it closes the
# remaining stores and subsystems.
await prepare()
task = self.active_task
if task is None or task.done():
return
task.cancel()
try:
await task
except asyncio.CancelledError:
return
def drop(self, index: int) -> QueuedChatInput | None:
if index < 1 or index > len(self.queue):
return None
items = list(self.queue)
item = items.pop(index - 1)
self.queue = deque(items)
return item
def clear(self) -> int:
count = len(self.queue)
self.queue.clear()
return count
async def start_kanban_watch(
self,
*,
interval: float = 2.0,
limit: int = 100,
initial_fingerprint: str = "",
session_id: str | None = None,
project_scope: bool = False,
) -> None:
if self.kanban_watch_active():
console.print("[dim]Kanban live watch is already running. Use /kanban stop to stop it.[/dim]")
return
self._kanban_watch_fingerprint = initial_fingerprint
self.kanban_watch_task = asyncio.create_task(
self._kanban_watch_loop(
interval=interval,
limit=limit,
session_id=session_id,
project_scope=project_scope,
)
)
scope_label = "project" if project_scope else f"session {(session_id or self.state.session_id)[:8]}"
console.print(
f"[dim]Kanban live watch started for {escape(scope_label)}. "
"New or changed work items will appear here. Exit with /kanban stop.[/dim]"
)
async def stop_kanban_watch(self, *, silent: bool = False) -> None:
task = self.kanban_watch_task
self.kanban_watch_task = None
if task is None or task.done():
if not silent:
console.print("[dim]Kanban live watch is not running.[/dim]")
return
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
if not silent:
console.print("[dim]Kanban live watch stopped.[/dim]")
def _start_item_locked(self, item: QueuedChatInput) -> None:
self.active_task = asyncio.create_task(self._run_item(item))
def _ensure_lock(self) -> asyncio.Lock:
if self._lock is None:
self._lock = asyncio.Lock()
return self._lock
async def _run_item(self, item: QueuedChatInput) -> None:
self.state.session_id = item.session_id
self.state.mode = item.mode
self.state.company_profile = item.company_profile
self.state.org_id = item.org_id
self.state.preferred_agent = item.preferred_agent
self.state.domains = list(item.domains)
try:
await _process_interactive_chat_message(
self.state,
item.text,
message_metadata=item.message_metadata,
interactive_followups=item.allow_interactive_followups,
)
except asyncio.CancelledError:
raise
finally:
async with self._ensure_lock():
self.active_task = None
if not self._closing and self.queue:
next_item = self.queue.popleft()
console.print(f"[dim]Running queued input; {len(self.queue)} remaining.[/dim]")
self._start_item_locked(next_item)
async def _kanban_watch_loop(
self,
*,
interval: float,
limit: int,
session_id: str | None,
project_scope: bool,
) -> None:
try:
while True:
items = await _fetch_kanban_items(
self.state,
limit=limit,
session_id=session_id,
project_scope=project_scope,
)
fingerprint = _kanban_items_fingerprint(items)
if fingerprint != self._kanban_watch_fingerprint:
self._kanban_watch_fingerprint = fingerprint
title = "Kanban Live Update (project)" if project_scope else f"Kanban Live Update (session {(session_id or self.state.session_id)[:8]})"
_render_kanban_items(items, title=title)
await asyncio.sleep(interval)
except asyncio.CancelledError:
raise
except Exception as exc:
console.print(f"[warning]Kanban live watch stopped: {escape(str(exc))}[/warning]")
_SLASH_COMMANDS: tuple[_SlashCommandSpec, ...] = (
_SlashCommandSpec("Chat", "/help", "Show this command list."),
_SlashCommandSpec("Chat", "/quit", "Exit interactive chat."),
_SlashCommandSpec("Chat", "/queue list|drop|clear", "Inspect or edit queued prompts while a turn is running.", ("list", "drop", "clear")),
_SlashCommandSpec("Context", "/status", "Show project, session, mode, agent, domains, model, and cost."),
_SlashCommandSpec("Context", "/mode [task|company] [corporate|custom]", "Set execution mode for future messages.", ("task", "company", "corporate", "custom")),
_SlashCommandSpec("Context", "/agent [native|codex|claude_code|cursor|opencode|none]", "Set preferred external agent.", ("native", "codex", "claude_code", "cursor", "opencode", "none")),
_SlashCommandSpec("Context", "/domains [domain ...|clear]", "Set domain hints for future messages.", ("clear",)),
_SlashCommandSpec("Project", "/project", "Show current project, known projects, and switch/create/delete usage.", ("list", "switch", "create", "rename", "delete")),
_SlashCommandSpec("Project", "/project list", "List known projects."),
_SlashCommandSpec("Project", "/project create <id>", "Create a project through the shared Office service."),
_SlashCommandSpec("Project", "/project switch <id>", "Switch project and restore its latest session."),
_SlashCommandSpec("Project", "/project rename <old_id> <new_id>", "Rename a project id and move persisted project data."),
_SlashCommandSpec("Project", "/project delete <id> --yes", "Delete a project through the shared Office service."),
_SlashCommandSpec("Session", "/stop [task_id|session_id]", "Stop the current or selected runtime and preserve history for follow-up."),
_SlashCommandSpec("Session", "/continue [task_id|session_id] [message]", "Continue a stopped runtime without re-planning."),
_SlashCommandSpec("Session", "/session", "Show current session, recent sessions, and switch/create/delete usage.", ("list", "new", "create", "resume", "show", "config", "send", "rename", "delete", "stop", "continue", "complete")),
_SlashCommandSpec("Session", "/session list [limit]", "List recent sessions."),
_SlashCommandSpec("Session", "/session new", "Start a fresh conversation session."),
_SlashCommandSpec("Session", "/session create [title] [--mode task|company|org] [--agent ...]", "Create a task-backed session through the shared Office service."),
_SlashCommandSpec("Session", "/session resume <session_id>", "Resume a session in this project."),
_SlashCommandSpec("Session", "/session show <session_id|task_id> [--limit N] [--full]", "Show session metadata and transcript."),
_SlashCommandSpec("Session", "/session config <task_id> [--mode ...] [--agent ...] [--org ...]", "Update session execution config through the shared Office service."),
_SlashCommandSpec("Session", "/session send <task_id> <message>", "Send a message to a task-backed session through the shared Office service."),
_SlashCommandSpec("Session", "/session rename <task_id|session_id> <title>", "Rename a task-backed or plain session."),
_SlashCommandSpec("Session", "/session delete <task_id> --yes", "Hard-delete a task-backed session."),
_SlashCommandSpec("Session", "/session stop [task_id|session_id]", "Stop the current or selected runtime through the shared Office service."),
_SlashCommandSpec("Session", "/session continue [task_id|session_id] [message]", "Continue a stopped runtime through the shared Office service."),
_SlashCommandSpec("Session", "/session complete <task_id>", "Complete a task-backed session through the shared Office service."),
_SlashCommandSpec("Tasks", "/tasks [status] [--limit N] [--full]", "List current project tasks."),
_SlashCommandSpec("Tasks", "/task show <task_id> [--limit N] [--full]", "Show task detail and transcript.", ("show", "move", "done", "rename", "delete")),
_SlashCommandSpec("Tasks", "/task move <task_id> todo|in-progress|done|blocked|failed|cancelled", "Move a task through the shared transition API."),
_SlashCommandSpec("Tasks", "/task done <task_id>", "Mark a task done through the shared transition API."),
_SlashCommandSpec("Tasks", "/task rename <task_id> <title>", "Rename a task and its session title."),
_SlashCommandSpec("Tasks", "/task delete <task_id> --yes", "Hard-delete a task and its persisted lifecycle data."),
_SlashCommandSpec("Runtime", "/runtime [--limit N] [--full]", "Show live runtime, active tasks, external sessions, and checkpoints."),
_SlashCommandSpec("Runtime", "/logs <task_id|session_id> [--limit N] [--full]", "Show execution logs, runtime events, tools, and transcript."),
_SlashCommandSpec("Runtime", "/comms <task_id> [--limit N] [--full]", "Show company-mode messages, handoffs, and review notes."),
_SlashCommandSpec("Runtime", "/attachments [--limit N] [--full]", "List current session attachment references."),
_SlashCommandSpec("Runtime", "/staffing [context]", "Open the pending company staffing/agent editor or role context preview.", ("context",)),
_SlashCommandSpec("Board", "/kanban [once|stop|all]", "Show current-session work-item status inline and optionally watch live updates.", ("once", "stop", "all", "project", "--once", "--all")),
_SlashCommandSpec("Board", "/board kanban|pipeline|work-item|role|logs", "Open opc board in read-only inspector mode.", ("kanban", "pipeline", "work-item", "role", "logs")),
_SlashCommandSpec("Work Items", "/work-items list|show|logs|role-status", "Inspect company work items and role progress.", ("list", "show", "logs", "role-status")),
_SlashCommandSpec("Org", "/org", "Show or edit organization config.", ("info", "role", "policy", "strategy", "reset", "saved", "export", "import")),
_SlashCommandSpec("Org", "/org role add|update|delete|bulk-add", "Manage company roles through the shared Office service."),
_SlashCommandSpec("Org", "/org policy update --payload ...", "Update runtime policy through the shared Office service."),
_SlashCommandSpec("Org", "/org strategy update --final-decider <role>", "Update organization strategy."),
_SlashCommandSpec("Org", "/org saved list|save|load|delete", "Manage saved organization architectures."),
_SlashCommandSpec("Agent", "/agent list|detail|create|delete|move|import-employee", "Manage visual office agents.", ("list", "detail", "create", "delete", "move", "create-from-template", "import-employee")),
_SlashCommandSpec("Talent", "/talent list|employees|scan", "Browse talent templates and employees.", ("list", "employees", "scan", "import", "import-repo", "hire", "employee", "import-agent")),
_SlashCommandSpec("Talent", "/talent import <template_id...>", "Import local talent templates."),
_SlashCommandSpec("Talent", "/talent import-repo <path>", "Import talent templates from a local repo."),
_SlashCommandSpec("Talent", "/talent hire <template_id> <role_id> [--name ...]", "Hire a template into a role."),
_SlashCommandSpec("Talent", "/talent employee <employee_id>", "Show employee detail."),
_SlashCommandSpec("Talent", "/talent import-agent <employee_id>", "Import an employee as a visual office agent."),
_SlashCommandSpec("Market", "/market list", "List installed OPC Market packages.", ("browse", "preview", "list", "presets", "apply-preset", "install", "uninstall", "export")),
_SlashCommandSpec("Market", "/market browse|preview <preset_id>", "Browse and preview architecture presets."),
_SlashCommandSpec("Market", "/market presets", "List built-in architecture presets."),
_SlashCommandSpec("Market", "/market apply-preset <preset_id> [--strategy namespace|overwrite]", "Apply a built-in architecture preset as the active custom organization."),
_SlashCommandSpec("Market", "/market install <path> [--strategy namespace|overwrite]", "Install a local .opcpkg package."),
_SlashCommandSpec("Market", "/market uninstall <package_id> --yes", "Uninstall an OPC Market package."),
_SlashCommandSpec("Market", "/market export --id ... --name ...", "Export the current org as a package."),
_SlashCommandSpec("Reorg", "/reorg list|show|approve|deny|apply", "Manage reorg proposals.", ("list", "show", "approve", "deny", "apply")),
_SlashCommandSpec("Diagnostics", "/cost", "Show token and cost counters."),
_SlashCommandSpec("Diagnostics", "/checkpoints [--limit N] [--full]", "List pending execution checkpoints."),
)
CommandSpec = _SlashCommandSpec
_SLASH_ALIASES = {"p": "project", "s": "session", "t": "task", "checkpoint": "checkpoints", "work-item": "work-items", "workitems": "work-items"}
def _initial_company_profile(config: OPCConfig) -> str:
value = str(getattr(getattr(config, "org", None), "company_profile", "") or "corporate").strip().lower()
return value if value in _VALID_COMPANY_PROFILES else "corporate"
def _normalize_interactive_preferred_agent(value: str | None) -> str | None:
normalized = str(value or "native").strip().lower().replace("-", "_")
if normalized in {"none", "auto", "default", "system"}:
return None
return normalized if normalized in _VALID_PREFERRED_AGENTS else "native"
def _cli_option_present(*names: str) -> bool:
argv = list(sys.argv[1:])
for idx, arg in enumerate(argv):
if arg in names:
return True
if any(arg.startswith(f"{name}=") for name in names):
return True
return False
async def _read_persisted_chat_context(state: _InteractiveChatState) -> dict[str, str]:
try:
import sqlite3
db_path = state.engine.opc_home / "ui_state.db"
if not db_path.exists():
return {}
with sqlite3.connect(str(db_path), timeout=1) as db:
db.execute(
"CREATE TABLE IF NOT EXISTS server_state (key TEXT PRIMARY KEY, value TEXT NOT NULL)"
)
rows = db.execute(
"SELECT key, value FROM server_state WHERE key IN (?, ?, ?)",
("exec_mode", "company_profile", "task_preferred_agent"),
).fetchall()
values = {str(key): str(value) for key, value in rows}
return {
"mode": values.get("exec_mode", ""),
"company_profile": values.get("company_profile", ""),
"preferred_agent": values.get("task_preferred_agent", ""),
}
except Exception:
return {}
async def _persist_chat_context(state: _InteractiveChatState) -> None:
try:
import sqlite3
db_path = state.engine.opc_home / "ui_state.db"
db_path.parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(str(db_path), timeout=1) as db:
db.execute(
"CREATE TABLE IF NOT EXISTS server_state (key TEXT PRIMARY KEY, value TEXT NOT NULL)"
)
db.executemany(
"INSERT OR REPLACE INTO server_state (key, value) VALUES (?, ?)",
(
("exec_mode", state.mode),
("company_profile", state.company_profile),
("task_preferred_agent", state.preferred_agent or "native"),
),
)
db.commit()
except Exception as exc:
console.print(f"[warning]Could not persist CLI mode state: {escape(str(exc))}[/warning]")
async def _restore_chat_context(
state: _InteractiveChatState,
*,
restore_mode: bool,
restore_company_profile: bool,
restore_agent: bool,
) -> None:
persisted = await _read_persisted_chat_context(state)
raw_mode = str(persisted.get("mode", "") or "").strip().lower()
raw_profile = str(persisted.get("company_profile", "") or "").strip().lower()
raw_agent = str(persisted.get("preferred_agent", "") or "").strip().lower().replace("-", "_")
if restore_mode:
if raw_mode in {"company", "company_mode"}:
state.mode = "company"
elif raw_mode in {"org", "custom"}:
state.mode = "company"
if restore_company_profile:
state.company_profile = "custom"
elif raw_mode in {"task", "project", "task_mode", "project_mode"}:
state.mode = "task"
if restore_company_profile and raw_profile in _VALID_COMPANY_PROFILES:
state.company_profile = raw_profile
if restore_agent and raw_agent:
state.preferred_agent = _normalize_interactive_preferred_agent(raw_agent)
def _current_project_id(engine: Any) -> str:
return str(getattr(engine, "project_id", None) or "default").strip() or "default"
def _safe_project_id(project_id: str) -> bool:
return bool(re.match(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$", project_id or ""))
def _value_text(value: Any, default: str = "") -> str:
if value is None:
return default
if hasattr(value, "value"):
return str(value.value)
return str(value)
def _format_datetime(value: Any) -> str:
if not value:
return ""
if hasattr(value, "strftime"):
return value.strftime("%Y-%m-%d %H:%M")
return str(value)
def _clip_text(value: Any, limit: int = 240, *, full: bool = False) -> str:
text = str(value or "").replace("\r\n", "\n").strip()
text = " ".join(part.strip() for part in text.splitlines() if part.strip())
if full:
return text
if len(text) <= limit:
return text
return text[: max(0, limit - 3)].rstrip() + "..."
def _json_summary(value: Any, limit: int = 240, *, full: bool = False) -> str:
if isinstance(value, dict):
for key in ("summary", "title", "message", "prompt", "original_message"):
if value.get(key):
return _clip_text(value.get(key), limit, full=full)
try:
return _clip_text(json.dumps(value, ensure_ascii=False, default=str), limit, full=full)
except TypeError:
return _clip_text(value, limit, full=full)
def _coerce_limit(value: str, *, default: int = _SLASH_DEFAULT_LIMIT) -> int:
if not str(value or "").strip():
return default
limit = int(str(value).strip())
return max(1, min(_SLASH_MAX_LIMIT, limit))
def _parse_limit_args(args: list[str], *, default: int = _SLASH_DEFAULT_LIMIT) -> tuple[list[str], int]:
remaining: list[str] = []
limit = default
idx = 0
while idx < len(args):
token = args[idx]
if token == "--limit":
if idx + 1 >= len(args):
raise ValueError("Missing value for --limit.")
limit = _coerce_limit(args[idx + 1], default=default)
idx += 2
continue
if token.startswith("--limit="):
limit = _coerce_limit(token.split("=", 1)[1], default=default)
idx += 1
continue
remaining.append(token)
idx += 1
return remaining, limit
def _parse_view_args(args: list[str], *, default: int = _SLASH_DEFAULT_LIMIT) -> tuple[list[str], int, bool]:
remaining: list[str] = []
full = False
idx = 0
while idx < len(args):
token = args[idx]
if token == "--full":
full = True
idx += 1
continue
remaining.append(token)
idx += 1
remaining, limit = _parse_limit_args(remaining, default=default)
return remaining, limit, full
def _slash_parts(raw_input: str) -> list[str]:
command = raw_input[1:] if raw_input.startswith("/") else raw_input
return shlex.split(command)
def _canonical_slash_command(command: str) -> str:
return _SLASH_ALIASES.get(command.strip().lower(), command.strip().lower())
def _slash_command_names() -> list[str]:
names = {
spec.command.split()[0].lstrip("/")
for spec in _SLASH_COMMANDS
if spec.command.startswith("/")
}
names.update(_SLASH_ALIASES.keys())
return sorted(names)
def _slash_subcommands(command: str) -> list[str]:
canonical = _canonical_slash_command(command)
values: set[str] = set()
for spec in _SLASH_COMMANDS:
root = spec.command.split()[0].lstrip("/")
if root == canonical:
values.update(spec.subcommands)
parts = spec.command.split()
if len(parts) > 1 and not parts[1].startswith("[") and not parts[1].startswith("<"):
values.add(parts[1].split("|", 1)[0])
return sorted(item for item in values if item)
class _OPCSlashCompleter:
def get_completions(self, document: Any, complete_event: Any): # noqa: ANN201 - prompt-toolkit protocol
try:
from prompt_toolkit.completion import Completion
except ImportError:
return
text = str(getattr(document, "text_before_cursor", "") or "")
if not text.startswith("/"):
return
body = text[1:]
if " " not in body:
prefix = body.lower()
if prefix in _slash_command_names():
for suggestion in _slash_subcommands(prefix):
yield Completion(f"/{prefix} {suggestion}", start_position=-len(text))
return
for command in _slash_command_names():
if command.startswith(prefix):
yield Completion(f"/{command}", start_position=-len(text))
return
tokens = body.split()
command = _canonical_slash_command(tokens[0] if tokens else "")
prefix = "" if body.endswith(" ") else (tokens[-1].lower() if len(tokens) > 1 else "")
suggestions = _slash_subcommands(command)
if command == "agent":
suggestions.extend(sorted([*_VALID_PREFERRED_AGENTS, "none"]))
if command == "mode":
suggestions.extend(["task", "company", "corporate", "custom"])
for suggestion in sorted(set(suggestions)):
if suggestion.startswith(prefix):
yield Completion(suggestion, start_position=-len(prefix))
async def get_completions_async(self, document: Any, complete_event: Any): # noqa: ANN201 - prompt-toolkit protocol
for completion in self.get_completions(document, complete_event):
yield completion
def _print_slash_help() -> None:
table = Table(title="OPC Slash Commands", show_lines=False)
table.add_column("Group", style="cyan", no_wrap=True)
table.add_column("Command", style="bold")
table.add_column("What it does")
for spec in _SLASH_COMMANDS:
table.add_row(spec.group, spec.command, spec.description)
table.add_row("Aliases", "/p /s /t", "Short aliases for /project, /session, and /task.")
console.print(table)
def _print_interactive_cost(engine: Any) -> None:
if not getattr(engine, "llm", None):
console.print("[warning]LLM runtime is not available.[/warning]")
return
stats = getattr(engine.llm, "stats", {}) or {}
console.print(
f"Session: tokens_in={stats.get('tokens_in', 0)}, "
f"tokens_out={stats.get('tokens_out', 0)}, "
f"cost=${float(stats.get('estimated_cost', 0.0) or 0.0):.4f}"
)
def _print_context_status(state: _InteractiveChatState) -> None:
table = Table(title="Current Context", show_header=False)
table.add_column("Field", style="cyan", no_wrap=True)
table.add_column("Value")
table.add_row("Project", _current_project_id(state.engine))
table.add_row("Session", state.session_id or "(none)")
table.add_row("Mode", state.mode)
table.add_row("Company profile", state.company_profile if state.mode == "company" else "(inactive)")
if state.org_id:
table.add_row("Org", state.org_id)
table.add_row("Preferred agent", state.preferred_agent or "(system default)")
table.add_row("Domains", ", ".join(state.domains) if state.domains else "(none)")
table.add_row("Runtime control", state.runtime_control_state or "(running/idle)")
if state.runtime_control_task_id:
table.add_row("Runtime task", state.runtime_control_task_id)
if state.runtime_control_checkpoint_id:
table.add_row("Runtime checkpoint", state.runtime_control_checkpoint_id)
console.print(table)
console.print("[dim]Use /mode, /agent, /project, /session, /stop, or /continue to inspect and control context.[/dim]")
def _list_cli_project_ids(engine: Any) -> list[str]:
project_ids = {"default", _current_project_id(engine)}
projects_dir = Path(getattr(engine, "opc_home", get_opc_home())) / "projects"
if projects_dir.is_dir():
for entry in sorted(projects_dir.iterdir()):
if entry.is_dir():
project_ids.add(entry.name)
return sorted(project_ids, key=lambda item: (item != "default", item.lower()))
def _cli_project_paths(engine: Any, project_id: str) -> tuple[Path, Path, Path]:
from opc.core.config import get_project_workplace
opc_home = Path(getattr(engine, "opc_home", get_opc_home()))
return (
opc_home / "projects" / project_id,
opc_home / "memory" / "projects" / f"{project_id}.md",
get_project_workplace(project_id),
)
def _cli_project_exists(engine: Any, project_id: str) -> bool:
if project_id == "default":
return True
return any(path.exists() for path in _cli_project_paths(engine, project_id))
def _render_project_list(state: _InteractiveChatState) -> None:
current = _current_project_id(state.engine)
table = Table(title="Projects")
table.add_column("Project")
table.add_column("Current", justify="center")
table.add_column("Markers")
for project_id in _list_cli_project_ids(state.engine):
project_dir, project_memory, workplace = _cli_project_paths(state.engine, project_id)
markers: list[str] = []
if (project_dir / "tasks.db").exists():
markers.append("tasks")
if project_memory.exists():
markers.append("memory")
if workplace.exists():
markers.append("workplace")
table.add_row(project_id, "*" if project_id == current else "", ", ".join(markers) or "")
console.print(table)
console.print("[dim]Switch with /project switch <id> or /project <id>. Create with /project create <id>. Rename with /project rename <old_id> <new_id>.[/dim]")
def _render_project_picker(state: _InteractiveChatState, project_ids: list[str]) -> None:
current = _current_project_id(state.engine)
table = Table(title="Choose Project")
table.add_column("#", justify="right", style="cyan", no_wrap=True)
table.add_column("Project")
table.add_column("Current", justify="center")
for idx, project_id in enumerate(project_ids, start=1):
table.add_row(str(idx), project_id, "*" if project_id == current else "")
console.print(table)
console.print("[dim]Enter a number or project id. Use `new <id>` to create a project. Blank selects the current project.[/dim]")
def _render_session_picker(sessions: list[Any]) -> None:
if not sessions:
console.print("[info]No sessions found for this project.[/info]")
console.print("[dim]Press Enter to create a new session.[/dim]")
return
table = Table(title="Choose Session")
table.add_column("#", justify="right", style="cyan", no_wrap=True)
table.add_column("Session ID")
table.add_column("Title")
table.add_column("Mode")
table.add_column("Updated")
for idx, item in enumerate(sessions, start=1):
table.add_row(
str(idx),
str(getattr(item, "session_id", "") or ""),
_clip_text(getattr(item, "title", "") or "(untitled)", 72),
str(getattr(item, "mode", "") or ""),
_format_datetime(getattr(item, "updated_at", None)),
)
console.print(table)
console.print("[dim]Enter a number or session id. Press Enter, or type `new`, to create a new session.[/dim]")
async def _load_recent_primary_sessions(store: OPCStore | None, project_id: str | None, *, limit: int = 20) -> list[Any]:
if not store:
return []
return await store.list_sessions(project_id=project_id or "default", parent_session_id=None, limit=limit)
def _session_short_id(session_id: str) -> str:
text = str(session_id or "").strip()
if len(text) <= 12:
return text or "(none)"
return f"{text[:8]}..."
def _chat_bottom_toolbar_text(state: _InteractiveChatState, controller: ChatTurnController | None = None) -> list[tuple[str, str]]:
profile = f" {state.company_profile}" if state.mode == "company" else ""
org = f" org:{state.org_id}" if state.org_id else ""
busy_hint = ""
if controller is not None:
if controller.is_busy:
kanban_hint = " kanban:live" if controller.kanban_watch_active() else ""
busy_hint = f" running queue:{controller.queue_depth()}{kanban_hint} Enter=queue /kanban /logs /runtime"
elif controller.kanban_watch_active():
busy_hint = " kanban:live exit:/kanban stop"
elif controller.queue_depth():
busy_hint = f" queue:{controller.queue_depth()}"
runtime_hint = f" runtime:{state.runtime_control_state}" if state.runtime_control_state else ""
text = (
f" project:{_current_project_id(state.engine)}"
f" session:{_session_short_id(state.session_id)}"
f" mode:{state.mode}{profile}"
f"{org}"
f" agent:{state.preferred_agent or 'system'}"
f"{runtime_hint}"
f"{busy_hint}"
" /mode /agent /project /session /help"
)
return [("class:bottom-toolbar", text)]
def _print_chat_hint() -> None:
console.print("[dim]Hint: /mode switches mode, /agent switches agent, /project switches project, /session switches session.[/dim]")
async def _run_chat_office_service(state: _InteractiveChatState, operation: Any) -> dict[str, Any] | None:
from opc.plugins.office_ui.services import ServiceError
from opc.plugins.office_ui.services.factory import OfficeServiceFactory
try:
async with OfficeServiceFactory(config=state.config, project_id=_current_project_id(state.engine)) as services:
result = await operation(services)
return {"ok": True, **dict(result.payload)}
except ServiceError as exc:
console.print(f"[warning]{escape(exc.message)}[/warning]")
return None
except Exception as exc:
console.print(f"[error]{escape(str(exc))}[/error]")
return None
async def _run_chat_current_office_service(state: _InteractiveChatState, operation: Any) -> dict[str, Any] | None:
from opc.plugins.office_ui.services import ModeState, OfficeServiceContext, OfficeServices, ServiceError
try:
context = OfficeServiceContext(
engine=state.engine,
agent_store=None,
chat_store=None,
event_adapter=None,
mode_state=ModeState(
exec_mode=state.mode,
company_profile=state.company_profile,
task_preferred_agent=state.preferred_agent or "native",
),
)
context.session_to_task = state.session_to_task
context.active_runtime_children = state.active_runtime_children
result = await operation(OfficeServices(context))
return {"ok": True, **dict(result.payload)}
except ServiceError as exc:
console.print(f"[warning]{escape(exc.message)}[/warning]")
return None
except Exception as exc:
console.print(f"[error]{escape(str(exc))}[/error]")
return None
async def _switch_chat_project(state: _InteractiveChatState, project_id: str, *, restore_session: bool = True) -> None:
if not project_id:
console.print("[warning]Usage: /project switch <id>[/warning]")
return
if not _safe_project_id(project_id):
console.print("[warning]Invalid project id. Use letters, numbers, hyphens, and underscores.[/warning]")
return
if not _cli_project_exists(state.engine, project_id):
console.print(f"[warning]Project does not exist: {project_id}[/warning]")
return
await state.engine.shutdown()
engine, runtime_display = _create_cli_engine(state.config, project_id)
await engine.initialize()
state.engine = engine
state.runtime_display = runtime_display
state.session_to_task.clear()
state.active_runtime_children.clear()
state.runtime_control_state = ""
state.runtime_control_task_id = ""
state.runtime_control_session_id = ""
state.runtime_control_checkpoint_id = ""
state.org_id = ""
_attach_cli_runtime_callbacks(state)
console.print(f"[info]Switched to project: {project_id}[/info]")
if not restore_session:
state.session_id = ""
return
state.session_id, restored_latest = await _resolve_starting_session_id(engine)
if restored_latest:
console.print(f"[info]Restored recent session: {state.session_id}[/info]")
else:
console.print(f"[info]Started a new session: {state.session_id}[/info]")
async def _create_and_switch_chat_project(state: _InteractiveChatState, project_id: str) -> bool:
if not _safe_project_id(project_id):
console.print("[warning]Invalid project id. Use letters, numbers, hyphens, and underscores.[/warning]")
return False
payload = await _run_chat_office_service(
state,
lambda svc: svc.project.create(project_id, active_project_id=_current_project_id(state.engine)),
)
if not payload:
return False
console.print(f"[success]Created project: {payload.get('project_id') or project_id}[/success]")
await _switch_chat_project(state, project_id, restore_session=False)
return True
async def _choose_initial_project(state: _InteractiveChatState, *, explicit_project: bool = False) -> None:
if explicit_project:
console.print(f"[info]Project: {_current_project_id(state.engine)}[/info]")
return
while True:
project_ids = _list_cli_project_ids(state.engine)
_render_project_picker(state, project_ids)
try:
choice = console.input("[bold]Project[/bold] [current]: ").strip()
except (EOFError, KeyboardInterrupt):
choice = ""
if not choice:
console.print(f"[info]Project: {_current_project_id(state.engine)}[/info]")
return
lowered = choice.lower()
if lowered in {"quit", "exit", "/quit", "/exit"}:
raise KeyboardInterrupt
if lowered.startswith("/project"):
try:
parts = _slash_parts(choice)
project_args = parts[1:]
before_project = _current_project_id(state.engine)
await _handle_project_slash(state, project_args)
except ValueError as exc:
console.print(f"[warning]Could not parse command: {exc}[/warning]")
continue
subcommand = project_args[0].lower() if project_args else ""
if subcommand in {"switch", "use"} and _current_project_id(state.engine) != before_project:
return
if len(project_args) == 1 and subcommand not in {"list", "ls", "create", "new", "delete", "rm"} and _current_project_id(state.engine) != before_project:
return
continue
new_prefixes = ("new ", "create ", "+ ")
if any(lowered.startswith(prefix) for prefix in new_prefixes):
project_id = choice.split(maxsplit=1)[1].strip() if " " in choice else ""
if not project_id:
console.print("[warning]Usage: new <project_id>[/warning]")
continue
if await _create_and_switch_chat_project(state, project_id):
return
continue
if choice.isdigit():
idx = int(choice)
if 1 <= idx <= len(project_ids):
await _switch_chat_project(state, project_ids[idx - 1], restore_session=False)
return
console.print("[warning]Invalid project number.[/warning]")
continue
if choice in project_ids or _cli_project_exists(state.engine, choice):
await _switch_chat_project(state, choice, restore_session=False)
return
console.print(f"[warning]Project not found: {choice}. Type `new {choice}` to create it.[/warning]")
async def _choose_initial_session(state: _InteractiveChatState) -> None:
sessions = await _load_recent_primary_sessions(
getattr(state.engine, "store", None),
_current_project_id(state.engine),
limit=20,
)
while True:
_render_session_picker(sessions)
try:
choice = console.input("[bold]Session[/bold] [new]: ").strip()
except (EOFError, KeyboardInterrupt):
choice = ""
lowered = choice.lower()
if lowered in {"quit", "exit", "/quit", "/exit"}:
raise KeyboardInterrupt
if not choice or lowered in {"new", "create", "+"}:
_start_new_chat_session(state)
return
if lowered.startswith("/session"):
try:
await _handle_session_slash(state, _slash_parts(choice)[1:])
except ValueError as exc:
console.print(f"[warning]Could not parse command: {exc}[/warning]")
if state.session_id:
return
sessions = await _load_recent_primary_sessions(
getattr(state.engine, "store", None),
_current_project_id(state.engine),
limit=20,
)
continue
if choice.isdigit():
idx = int(choice)
if 1 <= idx <= len(sessions):
await _resume_chat_session(state, str(getattr(sessions[idx - 1], "session_id", "") or ""))
return
console.print("[warning]Invalid session number.[/warning]")
continue
matching = next((item for item in sessions if str(getattr(item, "session_id", "") or "") == choice), None)
if matching:
await _resume_chat_session(state, choice)
return
console.print(f"[warning]Session not found: {choice}. Type `new` to create a new session.[/warning]")
async def _run_interactive_startup_selector(state: _InteractiveChatState, *, explicit_project: bool = False) -> None:
await _choose_initial_project(state, explicit_project=explicit_project)
state.session_id = ""
await _choose_initial_session(state)
profile = f" company_profile={state.company_profile}" if state.mode == "company" else ""
console.print(
f"[success]Ready:[/success] project={_current_project_id(state.engine)} "
f"session={state.session_id} mode={state.mode}{profile} agent={state.preferred_agent or 'system'}"
)
_print_chat_hint()
async def _handle_project_slash(state: _InteractiveChatState, args: list[str]) -> None:
if not args:
console.print(f"[info]Current project: {_current_project_id(state.engine)}[/info]")
_render_project_list(state)
console.print("[dim]Use /project list, /project switch <id>, /project <id>, /project create <id>, /project rename <old_id> <new_id>, or /project delete <id> --yes.[/dim]")
return
subcommand = args[0].lower()
if subcommand in {"list", "ls"}:
_render_project_list(state)
return
if subcommand in {"create", "new"}:
if len(args) < 2:
console.print("[warning]Usage: /project create <id>[/warning]")
return
payload = await _run_chat_office_service(state, lambda svc: svc.project.create(args[1], active_project_id=_current_project_id(state.engine)))
if payload:
console.print(f"[success]Created project: {payload.get('project_id')}[/success]")
return
if subcommand in {"rename", "mv"}:
if len(args) < 3:
console.print("[warning]Usage: /project rename <old_id> <new_id>[/warning]")
return
old_id, new_id = args[1], args[2]
renaming_current = old_id == _current_project_id(state.engine)
payload = await _run_chat_office_service(state, lambda svc: svc.project.rename(old_id, new_id))
if payload:
console.print(f"[success]Renamed project:[/success] {payload.get('old_project_id')} -> {payload.get('new_project_id') or payload.get('project_id')}")
if renaming_current:
await _switch_chat_project(state, new_id)
return
if subcommand in {"delete", "rm"}:
if len(args) < 2:
console.print("[warning]Usage: /project delete <id> --yes[/warning]")
return
remaining, ok = _require_yes_arg(args[2:], usage="/project delete <id> --yes")
if remaining or not ok:
return
deleting_current = args[1] == _current_project_id(state.engine)
payload = await _run_chat_office_service(state, lambda svc: svc.project.delete(args[1]))
if payload:
console.print(f"[success]Deleted project: {payload.get('project_id')}[/success]")
if deleting_current:
await _switch_chat_project(state, "default")
return
if subcommand in {"switch", "use"}:
if len(args) < 2:
console.print("[warning]Usage: /project switch <id>[/warning]")
return
await _switch_chat_project(state, args[1])
return
if len(args) == 1:
await _switch_chat_project(state, args[0])
return
console.print("[warning]Usage: /project [list|create <id>|rename <old_id> <new_id>|switch <id>|delete <id> --yes][/warning]")
async def _render_sessions(state: _InteractiveChatState, args: list[str]) -> None:
if not getattr(state.engine, "store", None):
console.print("[warning]Session store is not available.[/warning]")
return
try:
args, limit, full = _parse_view_args(args)
if args and args[0].isdigit():
limit = _coerce_limit(args[0])
args = args[1:]
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
if args:
console.print("[warning]Usage: /session list [limit][/warning]")
return
sessions = await state.engine.store.list_sessions(
project_id=_current_project_id(state.engine),
parent_session_id=None,
limit=limit,
)
if not sessions:
console.print("[info]No sessions found for this project.[/info]")
return
table = Table(title=f"Recent Sessions ({len(sessions)})")
table.add_column("Session ID")
table.add_column("Current", justify="center")
table.add_column("Title")
table.add_column("Mode")
table.add_column("Status")
table.add_column("Updated")
current_session = str(state.session_id or "")
for item in sessions:
session_id = str(getattr(item, "session_id", "") or "")
table.add_row(
session_id,
"*" if session_id == current_session else "",
_clip_text(getattr(item, "title", "") or "(untitled)", 80, full=full),
str(getattr(item, "mode", "") or ""),
str(getattr(item, "status", "") or ""),
_format_datetime(getattr(item, "updated_at", None)),
)
console.print(table)
console.print("[dim]Switch with /session resume <session_id> or /session <session_id>. Create with /session new.[/dim]")
async def _resume_chat_session(state: _InteractiveChatState, session_id: str) -> None:
if not getattr(state.engine, "store", None):
console.print("[warning]Session store is not available.[/warning]")
return
if not session_id:
console.print("[warning]Usage: /session resume <session_id>[/warning]")
return
target = await state.engine.store.get_session(session_id)
if not target:
console.print(f"[warning]Session not found: {session_id}[/warning]")
return
current_project = _current_project_id(state.engine)
target_project = str(getattr(target, "project_id", "") or "default")
if target_project != current_project:
console.print(
"[warning]"
f"Session {session_id} belongs to project '{target_project}'. "
f"Switch project first with /project {target_project}."
"[/warning]"
)
return
state.session_id = session_id
title = getattr(target, "title", "") or "(untitled)"
console.print(f"[info]Resumed session: {state.session_id} :: {title}[/info]")
def _start_new_chat_session(state: _InteractiveChatState) -> None:
state.session_id = str(uuid.uuid4())
console.print(f"[info]Started a new session: {state.session_id}[/info]")
def _render_transcript_parts(parts: list[Any]) -> str:
lines: list[str] = []
for part in parts:
part_type = getattr(part, "part_type", "")
payload = getattr(part, "payload", {})
if isinstance(part, dict):
part_type = str(part.get("part_type", part_type) or "")
payload = part.get("payload", payload)
if not isinstance(payload, dict):
payload = {}
if part_type == "text":
text = payload.get("text", "")
if text:
lines.append(str(text))
elif part_type in {"subtask_result", "task_result"}:
title = payload.get("task_title", "Task")
summary = payload.get("summary", "")
lines.append(f"{title}: {summary}".strip(": "))
return "\n".join(line for line in lines if line).strip()
def _render_transcript_table(transcript: list[dict[str, Any]], *, limit: int) -> None:
rows: list[tuple[str, str, str, str]] = []
for item in transcript:
message = item.get("message") if isinstance(item, dict) else None
if not message or getattr(message, "summary_flag", False):
continue
content = _render_transcript_parts(item.get("parts", []))
if not content:
continue
role = str(getattr(message, "role", "") or "").strip().lower()
agent_id = str(getattr(message, "agent_id", "") or "").strip()
sender = {
"user": "You",
"assistant": "OPC",
"system": "System",
"subagent": agent_id.replace("_", " ").replace("-", " ").title() if agent_id else "Subagent",
}.get(role, agent_id or role.title() or "OPC")
rows.append((
_format_datetime(getattr(message, "created_at", None)),
role or "assistant",
sender,
_clip_text(content),
))
rows = rows[-limit:]
if not rows:
console.print("[info]No transcript messages found.[/info]")
return
table = Table(title=f"Transcript (last {len(rows)})")
table.add_column("Created")
table.add_column("Role")
table.add_column("Sender")
table.add_column("Content")
for row in rows:
table.add_row(*row)
console.print(table)
async def _resolve_session_or_task(state: _InteractiveChatState, token: str) -> tuple[Any | None, Any | None, str]:
store = getattr(state.engine, "store", None)
if not store:
return None, None, ""
session = await store.get_session(token) if hasattr(store, "get_session") else None
task = None
session_id = str(getattr(session, "session_id", "") or "") if session else ""
if not session and hasattr(store, "get_task"):
task = await store.get_task(token)
session_id = str(getattr(task, "session_id", "") or "") if task else ""
if session_id and hasattr(store, "get_session"):
session = await store.get_session(session_id)
return session, task, session_id
async def _resolve_runtime_control_target(state: _InteractiveChatState, target: str = "") -> tuple[str, str]:
from opc.layer2_organization.company_runtime_identity import load_company_runtime_identity_index
store = _require_chat_store(state, label="Session store")
if store is None:
return "", ""
raw_target = str(target or "").strip() or str(state.session_id or "").strip()
if not raw_target:
console.print("[warning]No current session. Use /session list or /session create first.[/warning]")
return "", ""
project_id = _current_project_id(state.engine)
identity_index = await load_company_runtime_identity_index(store, project_id)
task = await store.get_task(raw_target) if hasattr(store, "get_task") else None
if task is not None:
task_project_id = str(getattr(task, "project_id", "") or "default")
if task_project_id != project_id:
console.print(f"[warning]Target belongs to project '{task_project_id}'. Switch project first.[/warning]")
return "", ""
runtime_identity = identity_index.resolve(task_id=raw_target)
if runtime_identity is not None:
return raw_target, runtime_identity.runtime_session_id
return str(getattr(task, "id", "") or ""), str(getattr(task, "session_id", "") or getattr(task, "parent_session_id", "") or "")
runtime_identity = (
identity_index.resolve(runtime_session_id=raw_target)
or identity_index.resolve(task_session_id=raw_target)
)
if runtime_identity is not None:
control_task_id = (
runtime_identity.ui_anchor_task_id
or runtime_identity.config_source_task_id
)
if control_task_id:
return control_task_id, runtime_identity.runtime_session_id
session = await store.get_session(raw_target) if hasattr(store, "get_session") else None
if session is None:
console.print(f"[warning]Task or session not found: {raw_target}[/warning]")
return "", ""
session_project_id = str(getattr(session, "project_id", "") or "default")
if session_project_id != project_id:
console.print(f"[warning]Target belongs to project '{session_project_id}'. Switch project first.[/warning]")
return "", ""
tasks = list(identity_index.tasks)
candidates = [
item for item in tasks
if str(getattr(item, "session_id", "") or "") == raw_target
]
if not candidates:
console.print(f"[warning]Session is not task-backed: {raw_target}[/warning]")
return "", raw_target
task_mode_anchor = min(
candidates,
key=lambda item: (
bool(str(getattr(item, "parent_session_id", "") or "")),
str(getattr(item, "created_at", "") or ""),
str(getattr(item, "id", "") or ""),
),
)
return str(getattr(task_mode_anchor, "id", "") or ""), raw_target
def _make_cli_runtime_control_context(state: _InteractiveChatState, controller: ChatTurnController | None = None) -> Any:
from opc.plugins.office_ui.services import ModeState, OfficeServiceContext
context = OfficeServiceContext(
engine=state.engine,
agent_store=None,
chat_store=None,
event_adapter=None,
mode_state=ModeState(
exec_mode=state.mode,
company_profile=state.company_profile,
task_preferred_agent=state.preferred_agent or "native",
),
)
context.session_to_task = state.session_to_task
context.active_runtime_children = state.active_runtime_children
def cancel_session_tasks(_task_id: str) -> None:
if controller is not None and controller.active_task is not None and not controller.active_task.done():
controller.active_task.cancel()
context.cancel_session_tasks = cancel_session_tasks
return context
async def _company_runtime_identity_for_session(
state: _InteractiveChatState,
session_id: str | None = None,
) -> Any | None:
from opc.layer2_organization.company_runtime_identity import (
load_company_runtime_identity_index,
)
runtime_session_id = str(session_id or state.session_id or "").strip()
store = getattr(state.engine, "store", None)
if not runtime_session_id or store is None:
return None
try:
identity_index = await load_company_runtime_identity_index(
store,
_current_project_id(state.engine),
)
# The interactive session may be a role/work-item session. Resolve it
# as a task session after trying the canonical root session so both UI
# roots and child channels converge on the same company runtime.
identity = (
identity_index.resolve(runtime_session_id=runtime_session_id)
or identity_index.resolve(task_session_id=runtime_session_id)
)
except Exception:
return None
return identity
async def _latest_company_suspend_checkpoint(
state: _InteractiveChatState,
session_id: str | None = None,
) -> Any | None:
identity = await _company_runtime_identity_for_session(state, session_id)
return identity.checkpoint if identity is not None and identity.resumable else None
async def _company_runtime_execution_identity(
state: _InteractiveChatState,
runtime_identity: Any,
) -> Any:
"""Read execution configuration from the durable runtime config source."""
from opc.plugins.office_ui.execution_identity import execution_identity_from_task
config_source_task_id = str(
getattr(runtime_identity, "config_source_task_id", "") or ""
).strip()
store = getattr(state.engine, "store", None)
if not config_source_task_id or store is None or not hasattr(store, "get_task"):
raise RuntimeError("Company runtime has no durable configuration source.")
config_task = await store.get_task(config_source_task_id)
if config_task is None:
raise RuntimeError("Company runtime configuration source no longer exists.")
return execution_identity_from_task(
config_task,
default_exec_mode=state.mode,
default_company_profile=state.company_profile,
default_preferred_agent=state.preferred_agent or "native",
default_org_id=state.org_id,
)
async def _handle_stop_slash(state: _InteractiveChatState, args: list[str], controller: ChatTurnController | None = None) -> None:
if len(args) > 1:
console.print("[warning]Usage: /stop [task_id|session_id][/warning]")
return
target = args[0] if args else ""
task_id, session_id = await _resolve_runtime_control_target(state, target)
if not task_id:
return
from opc.plugins.office_ui.services import OfficeServices, ServiceError
context = _make_cli_runtime_control_context(state, controller)
try:
result = await OfficeServices(context).session.stop(project_id=_current_project_id(state.engine), target=target or session_id or task_id)
except ServiceError as exc:
console.print(f"[warning]{escape(exc.message)}[/warning]")
return
payload = dict(result.payload)
state.runtime_control_state = str(payload.get("runtime_control_state") or payload.get("status") or "stopped")
state.runtime_control_task_id = str(payload.get("task_id") or task_id)
state.runtime_control_session_id = str(payload.get("resume_parent_session_id") or payload.get("session_id") or session_id)
state.runtime_control_checkpoint_id = str(payload.get("checkpoint_id") or "")
console.print("[success]Stopped.[/success] [dim]Send a message to revise, or /continue to resume.[/dim]")
async def _runtime_control_execution_identity(state: _InteractiveChatState, task_id: str) -> Any:
from opc.layer2_organization.company_runtime_identity import load_company_runtime_identity_index
from opc.plugins.office_ui.execution_identity import execution_identity_from_task
store = getattr(state.engine, "store", None)
task = None
if store is not None and task_id:
try:
identity_index = await load_company_runtime_identity_index(
store,
_current_project_id(state.engine),
)
runtime_identity = identity_index.resolve(task_id=task_id)
task = identity_index.task(
runtime_identity.config_source_task_id if runtime_identity is not None else task_id
)
except Exception:
task = None
if task is None and hasattr(store, "get_task"):
try:
task = await store.get_task(task_id)
except Exception:
task = None
return execution_identity_from_task(
task,
default_exec_mode=state.mode,
default_company_profile=state.company_profile,
default_preferred_agent=state.preferred_agent or "native",
default_org_id=state.org_id,
)
async def _handle_continue_slash(state: _InteractiveChatState, args: list[str], controller: ChatTurnController | None = None) -> None:
target = ""
message_parts = list(args)
if message_parts:
first = message_parts[0]
store = getattr(state.engine, "store", None)
is_target = False
if store is not None:
try:
is_target = bool((hasattr(store, "get_task") and await store.get_task(first)) or (hasattr(store, "get_session") and await store.get_session(first)))
except Exception:
is_target = False
if is_target:
target = message_parts.pop(0)
task_id, session_id = await _resolve_runtime_control_target(state, target)
if not task_id:
return
identity = await _runtime_control_execution_identity(state, task_id)
content = " ".join(message_parts).strip() or "Resume the existing runtime."
checkpoint = await _latest_company_suspend_checkpoint(state, session_id)
if identity.exec_mode in {"company", "org", "custom"} and checkpoint is None:
console.print(
"[warning]No suspended or interrupted company runtime is available to continue.[/warning]"
)
return
if controller is not None and controller.is_busy:
if checkpoint is None and state.runtime_control_state not in {"suspended", "stopped"}:
console.print("[warning]Busy: wait for the current turn or /stop it before /continue.[/warning]")
return
metadata: dict[str, Any] = {"ui_force_resume": True}
if checkpoint is not None:
metadata["response_to_checkpoint_id"] = str(getattr(checkpoint, "checkpoint_id", "") or "")
metadata["response_to_checkpoint_type"] = str(
getattr(checkpoint, "checkpoint_type", "") or "company_runtime_suspended"
)
item = QueuedChatInput(
text=content,
project_id=_current_project_id(state.engine),
session_id=session_id or state.session_id,
mode=identity.exec_mode,
company_profile=identity.company_profile,
org_id=identity.org_id,
preferred_agent=identity.preferred_agent,
domains=list(state.domains),
message_metadata=metadata,
)
state.runtime_control_state = "resuming"
state.runtime_control_task_id = task_id
state.runtime_control_session_id = session_id or state.session_id
state.runtime_control_checkpoint_id = str(getattr(checkpoint, "checkpoint_id", "") or "")
if controller is not None:
await controller.submit_item(item)
else:
previous_session_id = state.session_id
previous_mode = state.mode
previous_profile = state.company_profile
previous_org_id = state.org_id
previous_agent = state.preferred_agent
state.session_id = session_id or state.session_id
state.mode = identity.exec_mode
state.company_profile = identity.company_profile
state.org_id = identity.org_id
state.preferred_agent = identity.preferred_agent
try:
await _process_interactive_chat_message(state, content, message_metadata=metadata)
finally:
state.session_id = previous_session_id
state.mode = previous_mode
state.company_profile = previous_profile
state.org_id = previous_org_id
state.preferred_agent = previous_agent
async def _show_session_detail(state: _InteractiveChatState, args: list[str]) -> None:
if not getattr(state.engine, "store", None):
console.print("[warning]Session store is not available.[/warning]")
return
try:
args, limit, full = _parse_view_args(args)
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
if not args:
console.print("[warning]Usage: /session show <session_id|task_id> [--limit N] [--full][/warning]")
return
session, task, session_id = await _resolve_session_or_task(state, args[0])
if not session_id:
console.print(f"[warning]Session or task not found: {args[0]}[/warning]")
return
project_id = str(getattr(session, "project_id", None) or getattr(task, "project_id", None) or "default")
if project_id != _current_project_id(state.engine):
console.print(f"[warning]Target belongs to project '{project_id}'. Switch project first.[/warning]")
return
table = Table(title=f"Session {session_id}", show_header=False)
table.add_column("Field", style="cyan", no_wrap=True)
table.add_column("Value")
table.add_row("Session ID", session_id)
table.add_row("Project", project_id)
if task:
table.add_row("Task", str(getattr(task, "id", "") or ""))
table.add_row("Task title", _clip_text(getattr(task, "title", "") or "(untitled)", 120, full=full))
if session:
table.add_row("Title", _clip_text(getattr(session, "title", "") or "(untitled)", 120, full=full))
table.add_row("Mode", str(getattr(session, "mode", "") or ""))
table.add_row("Status", str(getattr(session, "status", "") or ""))
table.add_row("Updated", _format_datetime(getattr(session, "updated_at", None)))
console.print(table)
transcript = await state.engine.store.get_session_transcript(session_id)
_render_transcript_table(transcript, limit=limit)
async def _handle_session_slash(state: _InteractiveChatState, args: list[str], controller: ChatTurnController | None = None) -> None:
if not args:
console.print(f"[info]Current session: {state.session_id}[/info]")
await _render_sessions(state, [])
console.print("[dim]Use /session list, /session resume <session_id>, /session <session_id>, /session new, /session create [title], or /session delete <task_id> --yes.[/dim]")
return
subcommand = args[0].lower()
rest = args[1:]
known_subcommands = {
"list", "ls", "new", "create", "resume", "show", "config", "send", "rename", "delete", "stop", "continue", "complete",
}
if len(args) == 1 and subcommand not in known_subcommands:
await _resume_chat_session(state, args[0])
return
if subcommand in {"list", "ls"}:
await _render_sessions(state, rest)
return
if subcommand == "new":
_start_new_chat_session(state)
return
if subcommand == "create":
try:
rest, mode = _extract_option(rest, "--mode", default=state.mode)
rest, profile = _extract_option(rest, "--company-profile", default=state.company_profile)
rest, agent = _extract_option(rest, "--agent", default=state.preferred_agent)
rest, org_id = _extract_option(rest, "--org", default=None)
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
title = " ".join(rest).strip() or "New Chat"
payload = await _run_chat_office_service(
state,
lambda svc: svc.session.create(
project_id=_current_project_id(state.engine),
title=title,
exec_mode=mode,
company_profile=profile,
preferred_agent=agent,
org_id=org_id,
interface="cli",
),
)
if payload:
state.session_id = str(payload.get("session_id") or state.session_id)
console.print(f"[success]Created session:[/success] {state.session_id} task={payload.get('task_id')}")
return
if subcommand == "resume":
await _resume_chat_session(state, rest[0] if rest else "")
return
if subcommand == "show":
await _show_session_detail(state, rest)
return
if subcommand == "config":
if not rest:
console.print("[warning]Usage: /session config <task_id> [--mode ...] [--agent ...] [--org ...][/warning]")
return
task_id = rest[0]
opts = rest[1:]
try:
opts, mode = _extract_option(opts, "--mode", default=None)
opts, profile = _extract_option(opts, "--company-profile", default=None)
opts, agent = _extract_option(opts, "--agent", default=None)
opts, org_id = _extract_option(opts, "--org", default=None)
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
if opts:
console.print("[warning]Usage: /session config <task_id> [--mode ...] [--agent ...] [--org ...][/warning]")
return
payload = await _run_chat_office_service(
state,
lambda svc: svc.session.update_config(
project_id=_current_project_id(state.engine),
task_id=task_id,
exec_mode=mode,
company_profile=profile,
preferred_agent=agent,
org_id=org_id,
),
)
if payload:
console.print(f"[success]Updated session config:[/success] {task_id}")
return
if subcommand == "send":
if len(rest) < 2:
console.print("[warning]Usage: /session send <task_id> <message>[/warning]")
return
task_id = rest[0]
message = " ".join(rest[1:]).strip()
payload = await _run_chat_office_service(
state,
lambda svc: svc.session.send(
project_id=_current_project_id(state.engine),
task_id=task_id,
content=message,
mode=state.mode,
company_profile=state.company_profile,
preferred_agent=state.preferred_agent,
domains=list(state.domains),
),
)
if payload and payload.get("response"):
_print_response(str(payload["response"]), state.no_markdown)
return
if subcommand == "rename":
if len(rest) < 2:
console.print("[warning]Usage: /session rename <task_id|session_id> <title>[/warning]")
return
await _rename_task_and_session(state, rest[0], " ".join(rest[1:]))
return
if subcommand == "delete":
if not rest:
console.print("[warning]Usage: /session delete <task_id> --yes[/warning]")
return
await _delete_task_and_session(state, rest[0], rest[1:], usage="/session delete <task_id> --yes")
return
if subcommand == "stop":
await _handle_stop_slash(state, rest, controller=controller)
return
if subcommand == "continue":
await _handle_continue_slash(state, rest, controller=controller)
return
if subcommand == "complete":
if not rest:
console.print("[warning]Usage: /session complete <task_id>[/warning]")
return
operation = lambda svc: svc.session.complete(project_id=_current_project_id(state.engine), task_id=rest[0])
payload = await _run_chat_office_service(state, operation)
if payload:
console.print(f"[success]Completed session:[/success] {rest[0]}")
return
console.print("[warning]Usage: /session [list|new|create|resume <session_id>|show <session_id|task_id>|config <task_id>|send <task_id> <message>|rename <task_id|session_id> <title>|delete <task_id> --yes|stop [target]|continue [target] [message]|complete <task_id>][/warning]")
def _task_title(task: Any) -> str:
return _clip_text(getattr(task, "title", "") or getattr(task, "description", "") or "(untitled)", 80)
def _task_title_for_view(task: Any, *, full: bool = False) -> str:
return _clip_text(getattr(task, "title", "") or getattr(task, "description", "") or "(untitled)", 80, full=full)
def _require_chat_store(state: _InteractiveChatState, *, label: str = "Task store") -> Any | None:
store = getattr(state.engine, "store", None)
if not store:
console.print(f"[warning]{label} is not available.[/warning]")
return None
return store
async def _get_task_for_current_project(state: _InteractiveChatState, task_id: str) -> Any | None:
store = _require_chat_store(state)
if store is None:
return None
if not task_id:
console.print("[warning]Missing task id.[/warning]")
return None
task = await store.get_task(task_id)
if not task:
console.print(f"[warning]Task not found: {task_id}[/warning]")
return None
project_id = str(getattr(task, "project_id", "") or "default")
if project_id != _current_project_id(state.engine):
console.print(f"[warning]Task belongs to project '{project_id}'. Switch project first with /project {project_id}.[/warning]")
return None
return task
def _parse_delete_yes(args: list[str], *, usage: str) -> bool:
unknown = [arg for arg in args if arg != "--yes"]
if unknown:
console.print(f"[warning]Usage: {usage}[/warning]")
return False
if "--yes" not in args:
console.print(f"[warning]Destructive command requires --yes. Usage: {usage}[/warning]")
return False
return True
def _extract_option(args: list[str], name: str, *, default: str | None = None) -> tuple[list[str], str | None]:
remaining: list[str] = []
value = default
idx = 0
prefix = f"{name}="
while idx < len(args):
token = args[idx]
if token == name:
if idx + 1 >= len(args):
raise ValueError(f"Missing value for {name}.")
value = args[idx + 1]
idx += 2
continue
if token.startswith(prefix):
value = token.split("=", 1)[1]
idx += 1
continue
remaining.append(token)
idx += 1
return remaining, value
def _require_yes_arg(args: list[str], *, usage: str) -> tuple[list[str], bool]:
remaining = [arg for arg in args if arg != "--yes"]
if "--yes" not in args:
console.print(f"[warning]Destructive command requires --yes. Usage: {usage}[/warning]")
return remaining, False
return remaining, True
def _save_cli_config(state: _InteractiveChatState) -> None:
state.config.save(get_opc_home() / "config")
def _refresh_cli_org_runtime(state: _InteractiveChatState) -> None:
engine = state.engine
if hasattr(engine, "config"):
engine.config = state.config
market = getattr(engine, "talent_market", None)
if market is not None and hasattr(market, "config"):
market.config = state.config
org_engine = getattr(engine, "org_engine", None)
if org_engine is None:
return
try:
if hasattr(org_engine, "config"):
org_engine.config = state.config
reload_from_config = getattr(org_engine, "reload_from_config", None)
if callable(reload_from_config):
reload_from_config()
configure_tools = getattr(org_engine, "configure_task_mode_tools", None)
tool_names = getattr(engine, "_task_mode_tool_names", None)
if callable(configure_tools) and callable(tool_names):
configure_tools(tool_names())
except Exception as exc:
console.print(f"[warning]Saved config, but current org runtime refresh failed: {exc}. Restart opc chat if needed.[/warning]")
def _coerce_task_status(value: Any) -> Any | None:
from opc.core.models import TaskStatus
raw = value.value if hasattr(value, "value") else str(value or "")
try:
return TaskStatus(raw)
except ValueError:
return None
async def _transition_task_status(
state: _InteractiveChatState,
*,
task_id: str,
target_status: Any,
reason: str,
target_label: str,
) -> None:
from opc.core.models import TaskStatus
from opc.layer2_organization.work_item_transition import apply_task_status_transition
store = _require_chat_store(state)
if store is None:
return
task = await _get_task_for_current_project(state, task_id)
if task is None:
return
old_status = _coerce_task_status(getattr(task, "status", None))
terminal_statuses = {TaskStatus.DONE, TaskStatus.FAILED, TaskStatus.CANCELLED}
if old_status in terminal_statuses and target_status not in terminal_statuses:
old_label = old_status.value if old_status else _value_text(getattr(task, "status", ""))
console.print(f"[warning]Cannot move terminal task {old_label} back to {target_label}.[/warning]")
return
old_label = _value_text(getattr(task, "status", ""))
try:
await apply_task_status_transition(
store,
task,
target_status_or_phase=target_status,
reason=reason,
)
except Exception as exc:
console.print(f"[error]Failed to transition task {task_id}: {exc}[/error]")
return
console.print(f"[success]Task {task_id} moved: {old_label} -> {target_status.value}[/success]")
async def _rename_task_and_session(
state: _InteractiveChatState,
target: str,
title: str,
*,
usage: str = "/session rename <task_id|session_id> <title>",
) -> None:
store = _require_chat_store(state)
if store is None:
return
title = str(title or "").strip()
if not title:
console.print(f"[warning]Usage: {usage}[/warning]")
return
target = str(target or "").strip()
if not target:
console.print(f"[warning]Usage: {usage}[/warning]")
return
current_project = _current_project_id(state.engine)
task = await store.get_task(target) if hasattr(store, "get_task") else None
if task and str(getattr(task, "project_id", "") or "default") != current_project:
console.print(f"[warning]Task belongs to project '{getattr(task, 'project_id', '')}'. Switch project first with /project {getattr(task, 'project_id', '')}.[/warning]")
return
session = None
session_id = ""
if task is None and hasattr(store, "get_session"):
session = await store.get_session(target)
if session and str(getattr(session, "project_id", "") or "default") != current_project:
console.print(f"[warning]Session belongs to project '{getattr(session, 'project_id', '')}'. Switch project first with /project {getattr(session, 'project_id', '')}.[/warning]")
return
if session and hasattr(store, "get_tasks"):
session_id = str(getattr(session, "session_id", "") or target)
tasks = await store.get_tasks(project_id=current_project)
task = next((item for item in tasks if str(getattr(item, "session_id", "") or "") == session_id), None)
if task is None and session is None:
console.print(f"[warning]Task or session not found: {target}[/warning]")
return
if task is not None:
if not hasattr(store, "save_task"):
console.print("[warning]Task store cannot save tasks.[/warning]")
return
task.title = title
await store.save_task(task)
session_id = str(getattr(task, "session_id", "") or session_id)
memory = getattr(state.engine, "memory", None)
if session_id and memory and hasattr(memory, "update_session_title"):
await memory.update_session_title(session_id, title)
elif session is not None and hasattr(store, "save_session"):
session.title = title
await store.save_session(session)
session_id = str(getattr(session, "session_id", "") or session_id)
task_id = str(getattr(task, "id", "") or "") if task is not None else ""
console.print(f"[success]Renamed {f'task {task_id}' if task_id else 'session'}{f' / session {session_id}' if session_id else ''}: {title}[/success]")
async def _delete_task_and_session(state: _InteractiveChatState, task_id: str, args: list[str], *, usage: str) -> None:
store = _require_chat_store(state)
if store is None:
return
if not _parse_delete_yes(args, usage=usage):
return
if not hasattr(store, "hard_delete_task"):
console.print("[warning]Task store cannot hard-delete tasks.[/warning]")
return
task = await _get_task_for_current_project(state, task_id)
if task is None:
return
session_id = str(getattr(task, "session_id", "") or "")
await store.hard_delete_task(task_id, session_id or None)
console.print(f"[success]Deleted task {task_id}{f' / session {session_id}' if session_id else ''}.[/success]")
async def _handle_task_move_slash(state: _InteractiveChatState, args: list[str]) -> None:
if len(args) != 2:
console.print("[warning]Usage: /task move <task_id> todo|in-progress|done|blocked|failed|cancelled[/warning]")
return
from opc.presentation.kanban import column_to_task_status
target_label = args[1].strip().lower().replace("_", "-")
target_status = column_to_task_status(target_label)
if target_status is None:
console.print("[warning]Unsupported target. Use todo, in-progress, done, blocked, failed, or cancelled.[/warning]")
return
await _transition_task_status(
state,
task_id=args[0],
target_status=target_status,
reason="cli_chat_task_move",
target_label=target_label,
)
async def _handle_task_done_slash(state: _InteractiveChatState, args: list[str]) -> None:
if len(args) != 1:
console.print("[warning]Usage: /task done <task_id>[/warning]")
return
from opc.core.models import TaskStatus
await _transition_task_status(
state,
task_id=args[0],
target_status=TaskStatus.DONE,
reason="cli_chat_task_done",
target_label="done",
)
async def _handle_tasks_slash(state: _InteractiveChatState, args: list[str]) -> None:
if not getattr(state.engine, "store", None):
console.print("[warning]Task store is not available.[/warning]")
return
from opc.core.models import TaskStatus
try:
args, limit, full = _parse_view_args(args)
if args and args[0].isdigit():
limit = _coerce_limit(args[0])
args = args[1:]
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
status = None
if args:
status_token = args[0].strip().lower().replace("-", "_")
try:
status = TaskStatus(status_token)
except ValueError:
valid = ", ".join(item.value for item in TaskStatus)
console.print(f"[warning]Unknown task status '{args[0]}'. Valid: {valid}[/warning]")
return
tasks = await state.engine.store.get_tasks(project_id=_current_project_id(state.engine), status=status)
tasks = tasks[:limit]
if not tasks:
console.print("[info]No tasks found for this project.[/info]")
return
table = Table(title=f"Tasks ({len(tasks)})")
table.add_column("Task ID")
table.add_column("Title")
table.add_column("Status")
table.add_column("Priority", justify="right")
table.add_column("Assigned")
table.add_column("Session ID")
table.add_column("Created")
for task in tasks:
table.add_row(
str(getattr(task, "id", "") or ""),
_task_title_for_view(task, full=full),
_value_text(getattr(task, "status", "")),
str(getattr(task, "priority", "") or ""),
str(getattr(task, "assigned_to", "") or ""),
str(getattr(task, "session_id", "") or ""),
_format_datetime(getattr(task, "created_at", None)),
)
console.print(table)
async def _show_task_detail(state: _InteractiveChatState, args: list[str]) -> None:
if not getattr(state.engine, "store", None):
console.print("[warning]Task store is not available.[/warning]")
return
try:
args, limit, full = _parse_view_args(args)
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
if not args:
console.print("[warning]Usage: /task show <task_id> [--limit N] [--full][/warning]")
return
task = await state.engine.store.get_task(args[0])
if not task:
console.print(f"[warning]Task not found: {args[0]}[/warning]")
return
project_id = str(getattr(task, "project_id", "") or "default")
if project_id != _current_project_id(state.engine):
console.print(f"[warning]Task belongs to project '{project_id}'. Switch project first.[/warning]")
return
table = Table(title=f"Task {getattr(task, 'id', '')}", show_header=False)
table.add_column("Field", style="cyan", no_wrap=True)
table.add_column("Value")
table.add_row("Title", _clip_text(getattr(task, "title", "") or "(untitled)", 120, full=full))
table.add_row("Description", _clip_text(getattr(task, "description", "") or "", 240, full=full))
table.add_row("Status", _value_text(getattr(task, "status", "")))
table.add_row("Project", project_id)
table.add_row("Session", str(getattr(task, "session_id", "") or ""))
table.add_row("Assignee", str(getattr(task, "assigned_to", "") or ""))
table.add_row("Priority", str(getattr(task, "priority", "") or ""))
tags = getattr(task, "tags", []) or []
table.add_row("Tags", ", ".join(str(tag) for tag in tags) if tags else "")
result = getattr(task, "result", None) or {}
if isinstance(result, dict):
table.add_row("Result", _clip_text(result.get("content", "") or result.get("summary", ""), 240, full=full))
artifacts = result.get("artifacts", []) or []
if not isinstance(artifacts, list):
artifacts = [artifacts]
table.add_row("Artifacts", _clip_text(", ".join(str(item) for item in artifacts), 240, full=full))
metadata = getattr(task, "metadata", {}) or {}
table.add_row("Metadata keys", ", ".join(sorted(str(key) for key in metadata.keys())) if isinstance(metadata, dict) else "")
console.print(table)
session_id = str(getattr(task, "session_id", "") or "")
if session_id:
transcript = await state.engine.store.get_session_transcript(session_id)
_render_transcript_table(transcript, limit=limit)
async def _handle_task_slash(state: _InteractiveChatState, args: list[str]) -> None:
if not args:
console.print("[warning]Usage: /task [show|move|done|rename|delete] ...[/warning]")
return
subcommand = args[0].lower()
if subcommand == "show":
await _show_task_detail(state, args[1:])
return
if subcommand == "move":
await _handle_task_move_slash(state, args[1:])
return
if subcommand == "done":
await _handle_task_done_slash(state, args[1:])
return
if subcommand == "rename":
rest = args[1:]
if len(rest) < 2:
console.print("[warning]Usage: /task rename <task_id> <title>[/warning]")
return
await _rename_task_and_session(state, rest[0], " ".join(rest[1:]), usage="/task rename <task_id> <title>")
return
if subcommand == "delete":
rest = args[1:]
if not rest:
console.print("[warning]Usage: /task delete <task_id> --yes[/warning]")
return
await _delete_task_and_session(state, rest[0], rest[1:], usage="/task delete <task_id> --yes")
return
await _show_task_detail(state, args)
async def _handle_checkpoints_slash(state: _InteractiveChatState, args: list[str]) -> None:
if not getattr(state.engine, "store", None):
console.print("[warning]Checkpoint store is not available.[/warning]")
return
try:
args, limit, full = _parse_view_args(args)
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
if args:
console.print("[warning]Usage: /checkpoints [--limit N] [--full][/warning]")
return
checkpoints = await state.engine.store.get_pending_checkpoints(project_id=_current_project_id(state.engine))
checkpoints = checkpoints[:limit]
if not checkpoints:
console.print("[info]No pending checkpoints for this project.[/info]")
return
table = Table(title=f"Pending Checkpoints ({len(checkpoints)})")
table.add_column("Checkpoint ID")
table.add_column("Type")
table.add_column("Task ID")
table.add_column("Session ID")
table.add_column("Updated")
table.add_column("Payload")
for checkpoint in checkpoints:
table.add_row(
str(getattr(checkpoint, "checkpoint_id", "") or ""),
str(getattr(checkpoint, "checkpoint_type", "") or ""),
str(getattr(checkpoint, "task_id", "") or ""),
str(getattr(checkpoint, "session_id", "") or ""),
_format_datetime(getattr(checkpoint, "updated_at", None)),
_json_summary(getattr(checkpoint, "payload", {}) or {}, full=full),
)
console.print(table)
def _render_org_slash(state: _InteractiveChatState) -> None:
org = state.config.org
summary = Table(title="Organization", show_header=False)
summary.add_column("Field", style="cyan", no_wrap=True)
summary.add_column("Value")
summary.add_row("Organization", str(getattr(org, "organization_name", "") or getattr(org, "company_name", "") or ""))
summary.add_row("Company", str(getattr(org, "company_name", "") or ""))
summary.add_row("Default mode", str(getattr(org, "default_mode", "") or ""))
summary.add_row("Company profile", str(getattr(org, "company_profile", "") or ""))
summary.add_row("Topology", str(getattr(org, "topology", "") or ""))
summary.add_row("Execution model", str(getattr(org, "execution_model", "") or ""))
summary.add_row("Final decider", str(getattr(org, "final_decider_role_id", "") or ""))
console.print(summary)
roles = list(getattr(org, "roles", []) or [])
if roles:
table = Table(title=f"Roles ({len(roles)})")
table.add_column("ID")
table.add_column("Name")
table.add_column("Reports To")
table.add_column("Type")
table.add_column("Agent")
table.add_column("Responsibility")
for role in roles:
table.add_row(
str(getattr(role, "id", "") or ""),
str(getattr(role, "name", "") or ""),
str(getattr(role, "reports_to", "") or ""),
str(getattr(role, "role_type", "") or ""),
str(getattr(role, "preferred_external_agent", "") or ""),
_clip_text(getattr(role, "responsibility", "") or "", 120),
)
console.print(table)
employees = list(getattr(org, "employees", []) or [])
if employees:
table = Table(title=f"Employees ({len(employees)})")
table.add_column("Employee ID")
table.add_column("Name")
table.add_column("Role")
table.add_column("Template")
table.add_column("Status")
table.add_column("Agent")
for employee in employees:
table.add_row(
str(getattr(employee, "employee_id", "") or ""),
str(getattr(employee, "name", "") or ""),
str(getattr(employee, "role_id", "") or ""),
str(getattr(employee, "template_id", "") or ""),
str(getattr(employee, "status", "") or ""),
str(getattr(employee, "preferred_external_agent", "") or ""),
)
console.print(table)
teams = list(getattr(org, "teams", []) or [])
if teams:
table = Table(title=f"Teams ({len(teams)})")
table.add_column("Team ID")
table.add_column("Name")
table.add_column("Description")
table.add_column("Seat IDs")
for team in teams:
table.add_row(
str(getattr(team, "team_id", "") or ""),
str(getattr(team, "name", "") or ""),
_clip_text(getattr(team, "description", "") or "", 100),
", ".join(str(item) for item in (getattr(team, "seat_ids", []) or [])),
)
console.print(table)
seats = []
for team in teams:
for seat in getattr(team, "seats", []) or []:
seats.append((team, seat))
if seats:
seat_table = Table(title=f"Seats ({len(seats)})")
seat_table.add_column("Team")
seat_table.add_column("Seat ID")
seat_table.add_column("Name")
seat_table.add_column("Role")
seat_table.add_column("Manager")
seat_table.add_column("Kind")
for team, seat in seats:
seat_table.add_row(
str(getattr(team, "team_id", "") or ""),
str(getattr(seat, "seat_id", "") or ""),
str(getattr(seat, "name", "") or ""),
str(getattr(seat, "role_id", "") or ""),
str(getattr(seat, "manager_role_id", "") or getattr(seat, "manager_seat_id", "") or ""),
str(getattr(seat, "seat_kind", "") or ""),
)
console.print(seat_table)
async def _handle_org_slash(state: _InteractiveChatState, args: list[str]) -> None:
if not args or args[0].lower() in {"info", "show"}:
if not args:
_render_org_slash(state)
return
payload = await _run_chat_office_service(state, lambda svc: svc.org.info())
if payload:
_emit_payload(payload)
return
command = args[0].lower()
rest = args[1:]
if command == "role":
if not rest:
console.print("[warning]Usage: /org role add|update|delete|bulk-add ...[/warning]")
return
action = rest[0].lower()
values = rest[1:]
if action == "add":
try:
values, name = _extract_option(values, "--name")
values, responsibility = _extract_option(values, "--responsibility", default="")
values, reports_to = _extract_option(values, "--reports-to", default="owner")
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
if len(values) != 1:
console.print("[warning]Usage: /org role add <role_id> [--name ...] [--responsibility ...] [--reports-to ...][/warning]")
return
role_id = values[0]
payload = await _run_chat_office_service(
state,
lambda svc: svc.org.add_role({
"role_id": role_id,
"name": name or role_id,
"responsibility": responsibility or "",
"reports_to": reports_to or "owner",
}),
)
elif action == "update":
try:
values, name = _extract_option(values, "--name")
values, responsibility = _extract_option(values, "--responsibility")
values, reports_to = _extract_option(values, "--reports-to")
values, can_spawn = _extract_option(values, "--can-spawn")
values, tools = _extract_option(values, "--tools")
values, agent = _extract_option(values, "--agent")
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
if len(values) != 1:
console.print("[warning]Usage: /org role update <role_id> [--name ...] [--responsibility ...][/warning]")
return
updates = {
key: value
for key, value in {
"name": name,
"responsibility": responsibility,
"reports_to": reports_to,
"preferred_external_agent": agent,
}.items()
if value is not None
}
if can_spawn is not None:
updates["can_spawn"] = _split_csv(can_spawn)
if tools is not None:
updates["tools"] = _split_csv(tools)
payload = await _run_chat_office_service(state, lambda svc: svc.org.update_role(values[0], updates))
elif action == "delete":
if not values:
console.print("[warning]Usage: /org role delete <role_id> --yes[/warning]")
return
remaining, ok = _require_yes_arg(values[1:], usage="/org role delete <role_id> --yes")
if not ok or remaining:
return
payload = await _run_chat_office_service(state, lambda svc: svc.org.delete_role(values[0]))
elif action == "bulk-add":
if len(values) != 1:
console.print("[warning]Usage: /org role bulk-add <json-or-yaml-file>[/warning]")
return
loaded = _load_structured_payload(file_path=values[0])
roles = loaded.get("roles", []) if isinstance(loaded, dict) else loaded if isinstance(loaded, list) else []
payload = await _run_chat_office_service(state, lambda svc: svc.org.bulk_add_roles(list(roles or [])))
else:
console.print("[warning]Usage: /org role add|update|delete|bulk-add ...[/warning]")
return
if payload:
_refresh_cli_org_runtime(state)
_emit_payload(payload)
return
if command == "policy" and rest[:1] == ["update"]:
values = rest[1:]
try:
values, payload_text = _extract_option(values, "--payload")
values, file_path = _extract_option(values, "--file")
values, profile = _extract_option(values, "--profile", default="custom")
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
if values:
console.print("[warning]Usage: /org policy update [--payload JSON/YAML|--file path] [--profile custom][/warning]")
return
data = _load_structured_payload(payload=payload_text, file_path=file_path)
payload = await _run_chat_office_service(state, lambda svc: svc.org.update_runtime_policy(data, profile=profile or "custom"))
if payload:
_refresh_cli_org_runtime(state)
_emit_payload(payload)
return
if command == "strategy" and rest[:1] == ["update"]:
values = rest[1:]
try:
values, final_decider = _extract_option(values, "--final-decider")
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
if values:
console.print("[warning]Usage: /org strategy update --final-decider <role_id>[/warning]")
return
payload = await _run_chat_office_service(state, lambda svc: svc.org.update_org_strategy(final_decider_role_id=final_decider))
if payload:
_refresh_cli_org_runtime(state)
_emit_payload(payload)
return
if command == "reset":
remaining, ok = _require_yes_arg(rest, usage="/org reset --yes")
if not ok or remaining:
return
payload = await _run_chat_office_service(state, lambda svc: svc.org.reset_architecture())
if payload:
_refresh_cli_org_runtime(state)
_emit_payload(payload)
return
if command == "saved":
if not rest:
console.print("[warning]Usage: /org saved list|save|load|delete ...[/warning]")
return
action = rest[0].lower()
values = rest[1:]
if action == "list":
payload = await _run_chat_office_service(state, lambda svc: svc.org.saved_list())
elif action == "save":
if not values:
console.print("[warning]Usage: /org saved save <name> [--overwrite][/warning]")
return
overwrite = "--overwrite" in values[1:]
payload = await _run_chat_office_service(state, lambda svc: svc.org.saved_save_as(values[0], overwrite=overwrite))
elif action == "load":
if len(values) != 1:
console.print("[warning]Usage: /org saved load <name>[/warning]")
return
payload = await _run_chat_office_service(state, lambda svc: svc.org.saved_load(values[0]))
elif action == "delete":
if not values:
console.print("[warning]Usage: /org saved delete <name> --yes[/warning]")
return
remaining, ok = _require_yes_arg(values[1:], usage="/org saved delete <name> --yes")
if not ok or remaining:
return
payload = await _run_chat_office_service(state, lambda svc: svc.org.saved_delete(values[0]))
else:
console.print("[warning]Usage: /org saved list|save|load|delete ...[/warning]")
return
if payload:
_refresh_cli_org_runtime(state)
_emit_payload(payload)
return
if command == "export":
payload = await _run_chat_office_service(state, lambda svc: svc.org.export_config())
if payload:
_emit_payload(payload)
return
if command == "import":
if not rest:
console.print("[warning]Usage: /org import <path> [--dry-run][/warning]")
return
dry_run = "--dry-run" in rest[1:]
raw = Path(rest[0]).read_text(encoding="utf-8")
payload = await _run_chat_office_service(state, lambda svc: svc.org.import_config(raw, dry_run=dry_run))
if payload:
if not dry_run:
_refresh_cli_org_runtime(state)
_emit_payload(payload)
return
console.print("[warning]Usage: /org [info|role|policy|strategy|reset|saved|export|import] ...[/warning]")
def _render_talent_templates(templates: list[Any], *, title: str = "Talent Templates") -> None:
if not templates:
console.print("[info]No talent templates found.[/info]")
return
table = Table(title=f"{title} ({len(templates)})")
table.add_column("Template ID")
table.add_column("Name")
table.add_column("Category")
table.add_column("Domains")
table.add_column("Agent")
table.add_column("Description")
for template in templates:
table.add_row(
str(getattr(template, "id", "") or ""),
str(getattr(template, "name", "") or ""),
str(getattr(template, "category", "") or ""),
", ".join(str(item) for item in (getattr(template, "domains", []) or [])[:4]),
str(getattr(template, "preferred_external_agent", "") or ""),
_clip_text(getattr(template, "description", "") or "", 100),
)
console.print(table)
def _render_talent_employees(employees: list[Any]) -> None:
if not employees:
console.print("[info]No employees hired yet.[/info]")
return
table = Table(title=f"Employees ({len(employees)})")
table.add_column("Employee ID")
table.add_column("Name")
table.add_column("Role")
table.add_column("Template")
table.add_column("Status")
table.add_column("Agent")
for employee in employees:
table.add_row(
str(getattr(employee, "employee_id", "") or ""),
str(getattr(employee, "name", "") or ""),
str(getattr(employee, "role_id", "") or ""),
str(getattr(employee, "template_id", "") or ""),
str(getattr(employee, "status", "") or ""),
str(getattr(employee, "preferred_external_agent", "") or ""),
)
console.print(table)
async def _handle_talent_slash(state: _InteractiveChatState, args: list[str]) -> None:
if not args:
console.print("[warning]Usage: /talent [list|employees|scan|import|import-repo|hire|employee|import-agent] ...[/warning]")
return
market = TalentMarket(get_opc_home(), state.config)
command = args[0].lower()
rest = args[1:]
try:
if command in {"list", "templates"}:
_render_talent_templates(market.list_templates())
return
if command == "employees":
_render_talent_employees(market.list_employees())
return
if command == "scan":
_render_talent_templates(market.scan_local_talent(), title="Local Talent Templates")
return
if command == "import":
if not rest:
console.print("[warning]Usage: /talent import <template_id...>[/warning]")
return
templates = market.import_local_templates(rest)
_save_cli_config(state)
_refresh_cli_org_runtime(state)
console.print(f"[success]Resolved {len(templates)} local talent templates.[/success]")
_render_talent_templates(templates)
return
if command == "import-repo":
if len(rest) != 1:
console.print("[warning]Usage: /talent import-repo <path>[/warning]")
return
templates = market.import_from_repo(_talent_repo_path(rest[0]))
_save_cli_config(state)
_refresh_cli_org_runtime(state)
console.print(f"[success]Imported {len(templates)} talent templates from repo.[/success]")
_render_talent_templates(templates)
return
if command == "hire":
rest, employee_name = _extract_option(rest, "--name")
if len(rest) != 2:
console.print("[warning]Usage: /talent hire <template_id> <role_id> [--name ...][/warning]")
return
employee = market.hire_template(rest[0], rest[1], employee_name=employee_name)
_save_cli_config(state)
_refresh_cli_org_runtime(state)
console.print(f"[success]Hired {employee.name} into role `{employee.role_id}` as `{employee.employee_id}`.[/success]")
return
if command in {"employee", "detail"}:
if len(rest) != 1:
console.print("[warning]Usage: /talent employee <employee_id>[/warning]")
return
payload = await _run_chat_office_service(state, lambda svc: svc.talent.employee_detail(rest[0]))
if payload:
_emit_payload(payload)
return
if command in {"import-agent", "import-employee-as-agent"}:
if len(rest) != 1:
console.print("[warning]Usage: /talent import-agent <employee_id>[/warning]")
return
payload = await _run_chat_office_service(state, lambda svc: svc.talent.import_employee_as_agent(employee_id=rest[0]))
if payload:
_emit_payload(payload)
return
except Exception as exc:
console.print(f"[error]Talent command failed: {exc}[/error]")
return
console.print("[warning]Usage: /talent [list|employees|scan|import|import-repo|hire|employee|import-agent] ...[/warning]")
def _package_value(package: Any, name: str, default: Any = "") -> Any:
if hasattr(package, name):
return getattr(package, name)
if isinstance(package, dict):
return package.get(name, default)
return default
def _render_market_packages(packages: list[Any]) -> None:
if not packages:
console.print("[info]No packages installed.[/info]")
return
table = Table(title=f"Installed Packages ({len(packages)})")
table.add_column("Package ID")
table.add_column("Name")
table.add_column("Version")
table.add_column("Roles")
table.add_column("Templates")
for package in packages:
role_ids = _package_value(package, "role_ids", []) or []
template_ids = _package_value(package, "template_ids", []) or []
table.add_row(
str(_package_value(package, "package_id", "?") or "?"),
str(_package_value(package, "name", "") or ""),
str(_package_value(package, "version", "") or ""),
str(len(role_ids)),
str(len(template_ids)),
)
console.print(table)
async def _handle_market_slash(state: _InteractiveChatState, args: list[str]) -> None:
if not args:
console.print("[warning]Usage: /market [browse|preview|list|presets|apply-preset|install|uninstall|export] ...[/warning]")
return
command = args[0].lower()
rest = args[1:]
try:
if command == "browse":
payload = await _run_chat_office_service(state, lambda svc: svc.market.browse())
if payload:
_emit_payload(payload)
return
if command == "preview":
if len(rest) != 1:
console.print("[warning]Usage: /market preview <preset_id>[/warning]")
return
payload = await _run_chat_office_service(state, lambda svc: svc.market.preview(rest[0]))
if payload:
_emit_payload(payload)
return
if command == "list":
_render_market_packages(list(getattr(state.config.org, "installed_packages", []) or []))
return
if command == "presets":
from opc.market.architecture_registry import get_all_presets
_render_architecture_presets(get_all_presets())
return
if command == "apply-preset":
rest, strategy = _extract_option(rest, "--strategy", default="overwrite")
if len(rest) != 1 or strategy not in {"namespace", "overwrite"}:
console.print("[warning]Usage: /market apply-preset <preset_id> [--strategy namespace|overwrite][/warning]")
return
from opc.market.architecture_registry import apply_architecture_preset_to_config
info = apply_architecture_preset_to_config(
state.config,
rest[0],
strategy=strategy or "overwrite",
clear_existing=True,
)
state.mode = "company"
state.company_profile = "custom"
_save_cli_config(state)
_refresh_cli_org_runtime(state)
console.print(
f"[success]Applied preset {info.package_id}: "
f"{len(info.role_ids)} roles, {len(info.work_item_template_ids or info.template_ids)} templates.[/success]"
)
console.print("[info]Mode set to company; company_profile=custom[/info]")
return
if command == "install":
rest, strategy = _extract_option(rest, "--strategy", default="namespace")
if len(rest) != 1 or strategy not in {"namespace", "overwrite"}:
console.print("[warning]Usage: /market install <path> [--strategy namespace|overwrite][/warning]")
return
from opc.market import PackageLoader, SandboxChecker
loader = PackageLoader(state.config, get_opc_home())
package = loader.load_from_path(Path(rest[0]))
report = SandboxChecker().validate(package)
for warning in report.warnings:
console.print(f"[warning]{warning}[/warning]")
if not report.passed:
for error in report.errors:
console.print(f"[error]{error}[/error]")
console.print("[error]Package failed security check. Installation aborted.[/error]")
return
conflicts = loader.detect_conflicts(package)
if conflicts.has_conflicts:
for role_id in conflicts.role_conflicts:
console.print(f"[warning]Role conflict: {role_id}[/warning]")
for template_id in conflicts.template_conflicts:
console.print(f"[warning]Template conflict: {template_id}[/warning]")
info = loader.install(package, strategy=strategy or "namespace")
_save_cli_config(state)
_refresh_cli_org_runtime(state)
console.print(f"[success]Installed {info.package_id}: {len(info.role_ids)} roles, {len(info.template_ids)} templates.[/success]")
return
if command == "uninstall":
if not rest:
console.print("[warning]Usage: /market uninstall <package_id> --yes[/warning]")
return
remaining, ok = _require_yes_arg(rest[1:], usage="/market uninstall <package_id> --yes")
if not ok or remaining:
if remaining:
console.print("[warning]Usage: /market uninstall <package_id> --yes[/warning]")
return
from opc.market import PackageLoader
loader = PackageLoader(state.config, get_opc_home())
if not loader.uninstall(rest[0]):
console.print(f"[warning]Package not found: {rest[0]}[/warning]")
return
_save_cli_config(state)
_refresh_cli_org_runtime(state)
console.print(f"[success]Uninstalled package {rest[0]}.[/success]")
return
if command == "export":
rest, package_id = _extract_option(rest, "--id")
rest, name = _extract_option(rest, "--name")
rest, description = _extract_option(rest, "--desc", default="")
rest, version = _extract_option(rest, "--version", default="1.0.0")
rest, output_dir = _extract_option(rest, "--output-dir", default=".")
if rest or not package_id or not name:
console.print("[warning]Usage: /market export --id <package_id> --name <name> [--desc ...] [--version ...] [--output-dir ...][/warning]")
return
from opc.market import PackageExporter
exporter = PackageExporter(state.config, get_opc_home())
package = exporter.export_current(
package_id=package_id,
name=name,
description=description or "",
version=version or "1.0.0",
)
out_path = exporter.write_to_path(package, Path(output_dir or "."))
console.print(f"[success]Exported package {package_id} to {out_path}.[/success]")
return
except Exception as exc:
console.print(f"[error]Market command failed: {exc}[/error]")
return
console.print("[warning]Usage: /market [browse|preview|list|presets|apply-preset|install|uninstall|export] ...[/warning]")
def _render_reorg_proposals(proposals: list[Any]) -> None:
if not proposals:
console.print("[info]No reorg proposals found.[/info]")
return
table = Table(title=f"Reorg Proposals ({len(proposals)})")
table.add_column("Proposal ID")
table.add_column("Title")
table.add_column("Status")
table.add_column("Risk")
table.add_column("Scope")
table.add_column("Updated")
table.add_column("Summary")
for proposal in proposals:
table.add_row(
str(getattr(proposal, "proposal_id", "") or ""),
_clip_text(getattr(proposal, "title", "") or "", 50),
_value_text(getattr(proposal, "status", "")),
_value_text(getattr(proposal, "risk_level", "")),
_value_text(getattr(proposal, "scope", "")),
_format_datetime(getattr(proposal, "updated_at", None)),
_clip_text(getattr(proposal, "summary", "") or "", 100),
)
console.print(table)
def _render_reorg_detail(proposal: Any) -> None:
table = Table(title=f"Reorg {getattr(proposal, 'proposal_id', '')}", show_header=False)
table.add_column("Field", style="cyan", no_wrap=True)
table.add_column("Value")
for field_name in ("proposal_id", "title", "status", "scope", "risk_level", "initiated_by", "summary", "rationale", "approval_notes"):
table.add_row(field_name, _clip_text(_value_text(getattr(proposal, field_name, "")), 260))
console.print(table)
detail = {
"changeset": getattr(getattr(proposal, "changeset", None), "__dict__", getattr(proposal, "changeset", None)),
"impact_summary": getattr(proposal, "impact_summary", {}) or {},
"metadata": getattr(proposal, "metadata", {}) or {},
}
console.print(json.dumps(detail, ensure_ascii=False, indent=2, default=str))
async def _handle_reorg_slash(state: _InteractiveChatState, args: list[str]) -> None:
if not args:
console.print("[warning]Usage: /reorg [list|show|approve|deny|apply] ...[/warning]")
return
command = args[0].lower()
rest = args[1:]
try:
if command == "list":
if not getattr(state.engine, "store", None):
console.print("[warning]Reorg store is not available.[/warning]")
return
rest, limit = _parse_limit_args(rest)
if rest:
console.print("[warning]Usage: /reorg list [--limit N][/warning]")
return
proposals = await state.engine.store.list_reorg_proposals(_current_project_id(state.engine), limit=limit)
_render_reorg_proposals(proposals)
return
if command == "show":
if len(rest) != 1:
console.print("[warning]Usage: /reorg show <proposal_id>[/warning]")
return
proposal = await state.engine.show_company_reorg(rest[0])
if not proposal:
console.print(f"[warning]Reorg proposal not found: {rest[0]}[/warning]")
return
_render_reorg_detail(proposal)
return
if command in {"approve", "deny"}:
rest, notes = _extract_option(rest, "--notes", default="")
if len(rest) != 1:
console.print(f"[warning]Usage: /reorg {command} <proposal_id> [--notes ...][/warning]")
return
approved = command == "approve"
proposal = await state.engine.approve_company_reorg(
rest[0],
approved=approved,
notes=notes or ("Approved via CLI." if approved else "Denied via CLI."),
)
console.print(f"[success]Reorg {proposal.proposal_id} {'approved' if approved else 'denied'}.[/success]")
return
if command == "apply":
if not rest:
console.print("[warning]Usage: /reorg apply <proposal_id> --yes[/warning]")
return
remaining, ok = _require_yes_arg(rest[1:], usage="/reorg apply <proposal_id> --yes")
if not ok or remaining:
if remaining:
console.print("[warning]Usage: /reorg apply <proposal_id> --yes[/warning]")
return
result = await state.engine.apply_company_reorg(rest[0])
_refresh_cli_org_runtime(state)
console.print(f"[success]Applied reorg {rest[0]}.[/success]")
console.print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
return
except Exception as exc:
console.print(f"[error]Reorg command failed: {exc}[/error]")
return
console.print("[warning]Usage: /reorg [list|show|approve|deny|apply] ...[/warning]")
def _render_key_value_table(title: str, rows: list[tuple[str, Any]], *, full: bool = False) -> None:
table = Table(title=title, show_header=False)
table.add_column("Field", style="cyan", no_wrap=True)
table.add_column("Value")
for key, value in rows:
table.add_row(key, _clip_text(_value_text(value), 260, full=full))
console.print(table)
def _runtime_session_id_from_task(task: Any) -> str:
metadata = dict(getattr(task, "metadata", {}) or {})
runtime_v2 = dict(metadata.get("runtime_v2", {}) or {})
return str(runtime_v2.get("runtime_session_id", "") or "").strip()
def _row_runtime_session_id(row: Any) -> str:
if isinstance(row, dict):
return str(row.get("runtime_session_id", "") or "").strip()
return str(getattr(row, "runtime_session_id", "") or "").strip()
def _dedupe_runtime_rows(rows: list[Any]) -> list[Any]:
seen: set[str] = set()
results: list[Any] = []
for row in rows:
runtime_session_id = _row_runtime_session_id(row)
if not runtime_session_id or runtime_session_id in seen:
continue
seen.add(runtime_session_id)
results.append(row)
return results
def _runtime_row_metadata(row: Any) -> dict[str, Any]:
if isinstance(row, dict):
return dict(row.get("metadata", {}) or {})
return dict(getattr(row, "metadata", {}) or {})
def _runtime_row_value(row: Any, key: str, default: Any = "") -> Any:
if isinstance(row, dict):
return row.get(key, default)
return getattr(row, key, default)
async def _handle_work_items_slash(state: _InteractiveChatState, args: list[str]) -> None:
if not args:
args = ["list"]
command = args[0].lower()
rest = args[1:]
try:
if command in {"list", "ls"}:
rest, limit, full = _parse_view_args(rest, default=20)
role_id = None
status = None
rest, role_id = _extract_option(rest, "--role", default=None)
rest, status = _extract_option(rest, "--status", default=None)
if rest:
console.print("[warning]Usage: /work-items list [--role <role_id>] [--status <status>] [--limit N][/warning]")
return
payload = await _run_chat_current_office_service(
state,
lambda svc: svc.work_item.list(
project_id=_current_project_id(state.engine),
session_id=state.session_id,
role_id=role_id,
status=status,
limit=limit,
),
)
if payload:
items = list(payload.get("items", []) or payload.get("work_items", []) or [])
if not items:
console.print("[info]No work items found.[/info]")
return
table = Table(title=f"Work Items ({len(items)})")
table.add_column("Work Item")
table.add_column("Title")
table.add_column("Role")
table.add_column("Status")
table.add_column("Task")
table.add_column("Session")
for item in items:
table.add_row(
str(item.get("work_item_id", "") or item.get("id", "") or ""),
_clip_text(item.get("title", "") or "", 70, full=full),
str(item.get("role_id", "") or ""),
str(item.get("status", "") or item.get("phase", "") or ""),
str(item.get("task_id", "") or item.get("runtime_task_id", "") or ""),
str(item.get("session_id", "") or ""),
)
console.print(table)
return
if command == "show":
rest, limit, _full = _parse_view_args(rest, default=50)
if len(rest) != 1:
console.print("[warning]Usage: /work-items show <work_item_id> [--limit N][/warning]")
return
payload = await _run_chat_current_office_service(
state,
lambda svc: svc.work_item.show(project_id=_current_project_id(state.engine), work_item_id=rest[0], limit=limit),
)
if payload:
_emit_payload(payload)
return
if command == "logs":
rest, limit, _full = _parse_view_args(rest, default=50)
rest, role_id = _extract_option(rest, "--role", default=None)
work_item_id = rest[0] if rest else ""
if len(rest) > 1:
console.print("[warning]Usage: /work-items logs [work_item_id] [--role <role_id>] [--limit N][/warning]")
return
payload = await _run_chat_current_office_service(
state,
lambda svc: svc.work_item.logs(
project_id=_current_project_id(state.engine),
session_id=state.session_id,
work_item_id=work_item_id,
role_id=role_id or "",
limit=limit,
),
)
if payload:
_render_work_item_logs_payload(payload, limit=limit, full=_full)
return
if command in {"role-status", "status-by-role"}:
if rest:
console.print("[warning]Usage: /work-items role-status[/warning]")
return
payload = await _run_chat_current_office_service(
state,
lambda svc: svc.work_item.status_by_role(
project_id=_current_project_id(state.engine),
session_id=state.session_id,
),
)
if payload:
_emit_payload(payload)
return
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
console.print("[warning]Usage: /work-items list|show|logs|role-status ...[/warning]")
async def _list_runtime_sessions_for_scope(
store: Any,
*,
project_id: str,
task_id: str | None = None,
session_id: str | None = None,
limit: int = _SLASH_DEFAULT_LIMIT,
) -> list[Any]:
if not hasattr(store, "list_runtime_sessions"):
return []
try:
return await store.list_runtime_sessions(
project_id=project_id,
task_id=task_id,
session_id=session_id,
limit=limit,
)
except TypeError:
rows = await store.list_runtime_sessions(project_id=project_id)
if task_id:
rows = [row for row in rows if str(_runtime_row_value(row, "task_id", "") or "") == task_id]
if session_id:
rows = [row for row in rows if str(_runtime_row_value(row, "session_id", "") or "") == session_id]
return rows[:limit]
def _render_runtime_sessions(rows: list[Any], *, title: str = "Runtime Sessions", full: bool = False) -> None:
if not rows:
console.print("[info]No runtime sessions found.[/info]")
return
table = Table(title=f"{title} ({len(rows)})")
table.add_column("Runtime ID")
table.add_column("Status")
table.add_column("Task")
table.add_column("Session")
table.add_column("Updated")
table.add_column("Summary")
for row in rows:
metadata = _runtime_row_metadata(row)
table.add_row(
_row_runtime_session_id(row),
str(_runtime_row_value(row, "status", "") or ""),
str(_runtime_row_value(row, "task_id", "") or ""),
str(_runtime_row_value(row, "session_id", "") or ""),
_format_datetime(_runtime_row_value(row, "updated_at", "")),
_json_summary(metadata, full=full),
)
console.print(table)
def _render_external_sessions(rows: list[Any], *, full: bool = False) -> None:
if not rows:
console.print("[info]No external agent sessions found.[/info]")
return
table = Table(title=f"External Agent Sessions ({len(rows)})")
table.add_column("Agent")
table.add_column("Status")
table.add_column("Task")
table.add_column("OPC Session")
table.add_column("Provider Session")
table.add_column("Workspace")
table.add_column("Updated")
for row in rows:
table.add_row(
str(getattr(row, "agent_type", "") or ""),
str(getattr(row, "status", "") or ""),
str(getattr(row, "task_id", "") or ""),
str(getattr(row, "opc_session_id", "") or ""),
_clip_text(getattr(row, "session_id", "") or "", 48, full=full),
_clip_text(getattr(row, "workspace_path", "") or "", 80, full=full),
_format_datetime(getattr(row, "updated_at", None)),
)
console.print(table)
def _render_checkpoint_table(checkpoints: list[Any], *, title: str, full: bool = False) -> None:
if not checkpoints:
console.print("[info]No checkpoints found.[/info]")
return
table = Table(title=f"{title} ({len(checkpoints)})")
table.add_column("Checkpoint ID")
table.add_column("Status")
table.add_column("Type")
table.add_column("Task")
table.add_column("Session")
table.add_column("Updated")
table.add_column("Payload")
for checkpoint in checkpoints:
table.add_row(
str(getattr(checkpoint, "checkpoint_id", "") or ""),
str(getattr(checkpoint, "status", "") or ""),
str(getattr(checkpoint, "checkpoint_type", "") or ""),
str(getattr(checkpoint, "task_id", "") or ""),
str(getattr(checkpoint, "session_id", "") or ""),
_format_datetime(getattr(checkpoint, "updated_at", None)),
_json_summary(getattr(checkpoint, "payload", {}) or {}, full=full),
)
console.print(table)
async def _handle_runtime_slash(state: _InteractiveChatState, args: list[str]) -> None:
store = _require_chat_store(state, label="Runtime store")
if store is None:
return
try:
args, limit, full = _parse_view_args(args)
except ValueError as exc:
console.print(f"[warning]{exc}. Try /runtime --limit 20.[/warning]")
return
if args:
console.print("[warning]Usage: /runtime [--limit N] [--full]. Try /help.[/warning]")
return
snapshot = {}
if hasattr(state.runtime_display, "status_snapshot"):
snapshot = state.runtime_display.status_snapshot()
if snapshot:
_render_key_value_table(
"Current Runtime Snapshot",
[
("agent", snapshot.get("agent_id") or snapshot.get("role_id") or ""),
("task", snapshot.get("task_id") or ""),
("tool", snapshot.get("current_tool") or ""),
("queue", snapshot.get("queue_depth", 0)),
("stream_queue", snapshot.get("stream_queue_depth", 0)),
("context_remaining", f"{snapshot.get('context_remaining_pct')}%" if snapshot.get("context_remaining_pct") not in (None, "") else ""),
("turn_cost", f"${float(snapshot.get('turn_cost_usd', 0.0) or 0.0):.4f}"),
("session_cost", f"${float(snapshot.get('session_cost_usd', 0.0) or 0.0):.4f}"),
("approvals", snapshot.get("pending_permission_count", 0)),
("checkpoint", snapshot.get("checkpoint_hint", "")),
],
full=full,
)
from opc.core.models import TaskStatus
tasks = await store.get_tasks(project_id=_current_project_id(state.engine))
terminal = {TaskStatus.DONE, TaskStatus.FAILED, TaskStatus.CANCELLED}
active_tasks = [task for task in tasks if getattr(task, "status", None) not in terminal]
if active_tasks:
table = Table(title=f"Active Tasks ({len(active_tasks[:limit])})")
table.add_column("Task ID")
table.add_column("Title")
table.add_column("Status")
table.add_column("Agent")
table.add_column("Session")
for task in active_tasks[:limit]:
table.add_row(
str(getattr(task, "id", "") or ""),
_clip_text(getattr(task, "title", "") or "", 70, full=full),
_value_text(getattr(task, "status", "")),
str(getattr(task, "assigned_to", "") or ""),
str(getattr(task, "session_id", "") or ""),
)
console.print(table)
else:
console.print("[info]No active tasks for this project.[/info]")
runtime_rows = await _list_runtime_sessions_for_scope(
store,
project_id=_current_project_id(state.engine),
limit=limit,
)
_render_runtime_sessions(_dedupe_runtime_rows(runtime_rows), full=full)
if hasattr(store, "list_external_sessions"):
external = await store.list_external_sessions(project_id=_current_project_id(state.engine), limit=limit)
else:
external = []
_render_external_sessions(external, full=full)
checkpoints = await store.get_pending_checkpoints(project_id=_current_project_id(state.engine))
_render_checkpoint_table(checkpoints[:limit], title="Pending Checkpoints", full=full)
async def _resolve_logs_target(
state: _InteractiveChatState,
target: str,
*,
limit: int,
) -> tuple[Any | None, Any | None, list[Any], str | None]:
store = _require_chat_store(state, label="Log store")
if store is None:
return None, None, [], None
project_id = _current_project_id(state.engine)
task = await store.get_task(target) if hasattr(store, "get_task") else None
session = None
runtime_rows: list[Any] = []
runtime_row = None
if task:
task_project = str(getattr(task, "project_id", "") or "default")
if task_project != project_id:
console.print(f"[warning]Task belongs to project '{task_project}'. Switch project first with /project {task_project}.[/warning]")
return None, None, [], None
session_id = str(getattr(task, "session_id", "") or "")
if session_id and hasattr(store, "get_session"):
session = await store.get_session(session_id)
runtime_rows.extend(await _list_runtime_sessions_for_scope(store, project_id=project_id, task_id=target, limit=limit))
runtime_id = _runtime_session_id_from_task(task)
if runtime_id and hasattr(store, "get_runtime_session"):
row = await store.get_runtime_session(runtime_id)
if row:
runtime_rows.append(row)
return task, session, _dedupe_runtime_rows(runtime_rows), None
if hasattr(store, "get_session"):
session = await store.get_session(target)
if session:
session_project = str(getattr(session, "project_id", "") or "default")
if session_project != project_id:
console.print(f"[warning]Session belongs to project '{session_project}'. Switch project first with /project {session_project}.[/warning]")
return None, None, [], None
runtime_rows.extend(await _list_runtime_sessions_for_scope(store, project_id=project_id, session_id=target, limit=limit))
return None, session, _dedupe_runtime_rows(runtime_rows), None
if hasattr(store, "get_runtime_session"):
runtime_row = await store.get_runtime_session(target)
if runtime_row:
runtime_project = str(runtime_row.get("project_id", "") or "default")
if runtime_project != project_id:
console.print(f"[warning]Runtime session belongs to project '{runtime_project}'. Switch project first with /project {runtime_project}.[/warning]")
return None, None, [], None
task_id = str(runtime_row.get("task_id", "") or "")
session_id = str(runtime_row.get("session_id", "") or "")
if task_id and hasattr(store, "get_task"):
task = await store.get_task(task_id)
if session_id and hasattr(store, "get_session"):
session = await store.get_session(session_id)
return task, session, [runtime_row], None
return None, None, [], target
def _render_task_progress(task: Any, *, limit: int, full: bool = False) -> None:
if not task:
return
metadata = dict(getattr(task, "metadata", {}) or {})
progress = list(metadata.get("progress_log", []) or [])
if not progress:
console.print("[info]No task progress log found.[/info]")
return
table = Table(title=f"Task Progress ({len(progress[-limit:])})")
table.add_column("Entry")
for entry in progress[-limit:]:
table.add_row(_clip_text(entry, 300, full=full))
console.print(table)
async def _render_runtime_log_tables(store: Any, runtime_rows: list[Any], *, limit: int, full: bool = False) -> None:
runtime_ids = [_row_runtime_session_id(row) for row in runtime_rows if _row_runtime_session_id(row)]
if not runtime_ids:
console.print("[info]No runtime session logs found.[/info]")
return
_render_runtime_sessions(runtime_rows, title="Runtime Log Scope", full=full)
for runtime_id in runtime_ids[:limit]:
if hasattr(store, "list_runtime_events"):
events = await store.list_runtime_events(runtime_id, limit=limit)
if events:
table = Table(title=f"Runtime Events: {runtime_id} ({len(events)})")
table.add_column("Created")
table.add_column("Type")
table.add_column("Payload")
for event in events:
table.add_row(
_format_datetime(event.get("created_at")),
str(event.get("event_type", "") or ""),
_json_summary(event.get("payload", {}) or {}, full=full),
)
console.print(table)
if hasattr(store, "list_runtime_transcript_entries"):
entries = (await store.list_runtime_transcript_entries(runtime_id))[-limit:]
if entries:
table = Table(title=f"Runtime Transcript: {runtime_id} ({len(entries)})")
table.add_column("Created")
table.add_column("Role")
table.add_column("Type")
table.add_column("Content")
for entry in entries:
table.add_row(
_format_datetime(entry.get("created_at")),
str(entry.get("role", "") or ""),
str(entry.get("entry_type", "") or ""),
_clip_text(entry.get("content", "") or "", 300, full=full),
)
console.print(table)
if hasattr(store, "list_runtime_tool_calls"):
calls = (await store.list_runtime_tool_calls(runtime_id))[-limit:]
if calls:
table = Table(title=f"Tool Calls: {runtime_id} ({len(calls)})")
table.add_column("Created")
table.add_column("Tool")
table.add_column("Arguments")
for call in calls:
table.add_row(
_format_datetime(call.get("created_at")),
str(call.get("tool_name", "") or ""),
_json_summary(call.get("arguments", {}) or {}, full=full),
)
console.print(table)
if hasattr(store, "list_runtime_tool_results"):
results = (await store.list_runtime_tool_results(runtime_id))[-limit:]
if results:
table = Table(title=f"Tool Results: {runtime_id} ({len(results)})")
table.add_column("Created")
table.add_column("Tool")
table.add_column("Payload")
for result in results:
table.add_row(
_format_datetime(result.get("created_at")),
str(result.get("tool_name", "") or ""),
_json_summary(result.get("payload", {}) or {}, full=full),
)
console.print(table)
if hasattr(store, "list_runtime_permission_grants"):
grants = (await store.list_runtime_permission_grants(runtime_session_id=runtime_id))[-limit:]
if grants:
table = Table(title=f"Permission Grants: {runtime_id} ({len(grants)})")
table.add_column("Scope")
table.add_column("Tool")
table.add_column("Candidate")
table.add_column("Created")
for grant in grants:
table.add_row(
str(grant.get("scope", "") or ""),
str(grant.get("tool_name", "") or ""),
_clip_text(grant.get("candidate", "") or "", 120, full=full),
_format_datetime(grant.get("created_at")),
)
console.print(table)
def _clip_log_text(value: Any, limit: int = 1600, *, full: bool = False) -> str:
text = str(value or "").replace("\r\n", "\n").strip()
if full or len(text) <= limit:
return text
return text[: max(0, limit - 3)].rstrip() + "..."
_EXTERNAL_TRANSCRIPT_RE = re.compile(r"^\[External:(?P<agent>[^:\]]+):(?P<kind>[^\]]+)\]\s*(?P<body>.*)$", re.S)
_STAFFING_CONTROL_REPLIES = {"approve", "approved", "auto", "auto recruit", "deny", "stop", "cancel"}
_IMPORTANT_WORK_ITEM_EVENTS = {
"approval_requested",
"approval_resolved",
"checkpoint_created",
"checkpoint_resolved",
"escalation_created",
"escalation_resolved",
"work_item_failed",
"work_item_blocked",
"work_item_completed",
"runtime_failed",
"error",
}
@dataclass
class _WorkItemLogCell:
kind: str
created_at: Any
label: str
content: str
order: int = 0
status: str = ""
exit_code: Any = None
def _runtime_tool_payload_text(value: dict[str, Any], *, full: bool = False) -> str:
if not isinstance(value, dict):
return _clip_log_text(value, full=full)
for key in ("command", "cmd", "summary", "result_summary", "stdout", "stderr", "output", "text", "message"):
if value.get(key):
text = str(value.get(key) or "")
if key in {"command", "cmd"}:
return f"$ {text}"
return text
return _json_summary(value, limit=1200, full=full)
def _runtime_tool_command_text(value: Any, *, full: bool = False) -> str:
if not isinstance(value, dict):
return _clip_log_text(value, full=full)
for key in ("command", "cmd"):
if value.get(key):
return f"$ {str(value.get(key) or '').strip()}"
return _runtime_tool_payload_text(value, full=full)
def _runtime_tool_result_status(value: Any) -> tuple[str, Any]:
if not isinstance(value, dict):
return "", None
status = str(value.get("status", "") or "").strip()
exit_code = value.get("exit_code")
if exit_code is None:
exit_code = value.get("returncode")
if not status and "success" in value:
status = "completed" if bool(value.get("success")) else "failed"
if not status and exit_code is not None:
status = "completed" if str(exit_code) == "0" else "failed"
return status, exit_code
def _runtime_tool_result_text(value: Any, *, full: bool = False) -> str:
if not isinstance(value, dict):
return _clip_log_text(value, full=full)
parts: list[str] = []
for key in ("stdout", "stderr", "output", "result_summary", "summary", "text", "message"):
if value.get(key):
parts.append(str(value.get(key) or "").strip())
if parts:
return "\n".join(part for part in parts if part)
return _json_summary(value, limit=1200, full=full)
def _runtime_agent_label(entry: dict[str, Any]) -> str:
runtime_id = str(entry.get("runtime_session_id", "") or "")
if ":" in runtime_id:
prefix = runtime_id.split(":", 1)[0].strip()
if prefix:
return prefix
return str(entry.get("role", "") or "runtime").strip() or "runtime"
def _parse_runtime_stream_event(content: str) -> dict[str, Any] | None:
try:
event = json.loads(str(content or "").strip())
except Exception:
return None
if isinstance(event, dict) and isinstance(event.get("msg"), dict):
event = event["msg"]
if not isinstance(event, dict):
return None
event_type = str(event.get("type", "") or "").strip()
return event if event_type else None
def _command_text_from_codex_item(item: dict[str, Any], *, full: bool = False) -> str:
command = item.get("command")
if isinstance(command, list):
text = " ".join(str(part) for part in command if str(part).strip())
else:
text = str(command or "").strip()
if not text:
text = "command execution"
return f"$ {_clip_log_text(text, limit=420, full=full)}"
def _codex_command_output_text(item: dict[str, Any], *, full: bool = False) -> str:
output = str(item.get("aggregated_output", "") or "").strip()
if not output:
return ""
if full:
return output
if len(output) > 600 and output[:1] in {"{", "["}:
return "output omitted; use --full to inspect raw JSON output"
return _clip_log_text(output, limit=900, full=False)
def _runtime_stream_event_cells(
entry: dict[str, Any],
*,
full: bool = False,
skip_tool_events: bool = False,
) -> list[_WorkItemLogCell]:
event = _parse_runtime_stream_event(str(entry.get("content", "") or ""))
if not event:
return []
event_type = str(event.get("type", "") or "").strip()
created_at = entry.get("created_at")
agent = _runtime_agent_label(entry)
item = event.get("item") if isinstance(event.get("item"), dict) else {}
item_type = str(item.get("type", "") or "").strip()
if event_type in {"thread.started", "turn.started", "turn.completed", "turn.failed"}:
if not full:
return []
return [_WorkItemLogCell(
kind="event",
created_at=created_at,
label=f"runtime {event_type}",
content=_json_summary(event, limit=1000, full=full),
order=50,
)]
if item_type == "agent_message" and event_type in {"item.completed", "item.started"}:
text = str(item.get("text", "") or "").strip()
if not text:
return []
return [_WorkItemLogCell(
kind="thinking",
created_at=created_at,
label=agent,
content=text,
order=15,
)]
if item_type == "command_execution":
if skip_tool_events:
return []
if event_type == "item.started":
return [_WorkItemLogCell(
kind="tool_call",
created_at=created_at,
label="command_execution",
content=_command_text_from_codex_item(item, full=full),
order=30,
)]
if event_type == "item.completed":
status = str(item.get("status", "") or "").strip()
exit_code = item.get("exit_code")
content = _codex_command_output_text(item, full=full)
return [_WorkItemLogCell(
kind="tool_result",
created_at=created_at,
label="command_execution",
content=content,
order=35,
status=status,
exit_code=exit_code,
)]
if not full:
return []
return [_WorkItemLogCell(
kind="event",
created_at=created_at,
label=f"runtime {event_type}",
content=_json_summary(event, limit=1000, full=full),
order=50,
)]
def _is_staffing_transcript_noise(role: str, content: str) -> bool:
normalized = " ".join(str(content or "").strip().lower().split())
if not normalized:
return False
if role == "assistant" and normalized.startswith("company mode has a pending manual staffing selection"):
return True
if role == "user" and normalized in _STAFFING_CONTROL_REPLIES:
return True
return False
def _short_runtime_ref(value: str, *, full: bool = False) -> str:
text = str(value or "").strip()
if full or len(text) <= 48:
return text
parts = text.split("::")
if len(parts) >= 3 and parts[0] == "role-runtime":
return f"{parts[0]}::{parts[1][:8]}::{parts[-1]}"
return _clip_text(text, 48, full=False)
def _event_is_high_signal(event_type: str) -> bool:
normalized = str(event_type or "").strip().lower()
if not normalized:
return False
if normalized in _IMPORTANT_WORK_ITEM_EVENTS:
return True
return any(token in normalized for token in ("approval", "checkpoint", "escalation", "fail", "error", "blocked"))
def _render_codex_block(content: Any, *, prefix: str = " ", style: str = "", full: bool = False) -> None:
body = _clip_log_text(content, full=full)
if not body:
return
for line in body.splitlines():
text = f"{prefix}{line}" if line else ""
if style:
console.print(f"[{style}]{escape(text)}[/{style}]")
else:
console.print(escape(text))
def _render_codex_assistant_body(content: str, *, full: bool = False) -> None:
body = _clip_log_text(content, full=full)
if not body:
return
try:
console.print(Markdown(body))
except Exception:
_render_codex_block(body, prefix=" ", full=full)
def _render_work_item_log_cell(cell: _WorkItemLogCell, *, full: bool = False) -> None:
time_text = _format_datetime(cell.created_at)
timestamp = f"[dim]{escape(time_text)}[/dim] " if time_text else ""
label = escape(cell.label)
if cell.kind == "user":
console.print(f"{timestamp}[success]user[/success]")
_render_codex_block(cell.content, prefix=" ", style="success", full=full)
elif cell.kind == "assistant":
console.print(f"{timestamp}[agent]• {label}[/agent]")
_render_codex_assistant_body(cell.content, full=full)
elif cell.kind == "thinking":
console.print(f"{timestamp}[dim]thinking {label}[/dim]")
_render_codex_block(cell.content, prefix=" ", style="dim", full=full)
elif cell.kind == "tool_call":
text = cell.content.strip()
if text.startswith("$ "):
console.print(f"{timestamp}[tool]{escape(text)}[/tool]")
else:
console.print(f"{timestamp}[tool]tool {label}[/tool]")
_render_codex_block(text, prefix=" ", full=full)
elif cell.kind == "tool_result":
heading = f"tool result {cell.label}".strip()
status_bits: list[str] = []
if cell.status:
status_bits.append(f"status={cell.status}")
if cell.exit_code is not None and str(cell.exit_code) != "":
status_bits.append(f"exit_code={cell.exit_code}")
suffix = f" ({', '.join(status_bits)})" if status_bits else ""
style = "success" if cell.status in {"", "completed", "success", "succeeded"} else "error"
console.print(f"{timestamp}[{style}]{escape(heading + suffix)}[/{style}]")
_render_codex_block(cell.content, prefix=" ", full=full)
elif cell.kind in {"permission", "event", "handoff"}:
style = "warning" if cell.kind == "permission" else "info"
console.print(f"{timestamp}[{style}]• {label}[/{style}]")
_render_codex_block(cell.content, prefix=" ", full=full)
else:
console.print(f"{timestamp}[info]• {label}[/info]")
_render_codex_block(cell.content, prefix=" ", full=full)
def _render_work_item_logs_payload(payload: dict[str, Any], *, limit: int, full: bool = False) -> None:
title_bits = []
if payload.get("role_id"):
title_bits.append(f"role={payload['role_id']}")
if payload.get("work_item_id"):
title_bits.append(f"work_item={str(payload['work_item_id'])[:12]}")
if payload.get("session_id"):
title_bits.append(f"session={str(payload['session_id'])[:8]}")
scope_title = "Work Item Logs" + (f" ({', '.join(title_bits)})" if title_bits else "")
console.print(f"[bold]{escape(scope_title)}[/bold]")
work_items = list(payload.get("work_items", []) or [])[:limit]
for item in work_items:
status = str(item.get("phase", "") or item.get("kanban_column", "") or "")
runtime = str(item.get("role_runtime_session_id", "") or item.get("claimed_by_role_runtime_session_id", "") or "").strip()
suffix = f" [{status}]" if status else ""
if runtime:
suffix += f" runtime={_short_runtime_ref(runtime, full=full)}"
console.print(
f"[dim]work-item[/dim] [bold]{escape(str(item.get('work_item_id', '') or '')[:12])}[/bold] "
f"{escape(str(item.get('role_id', '') or ''))} "
f"{escape(_clip_text(item.get('title', '') or '', 96, full=full))}{escape(suffix)}"
)
if work_items:
console.print()
timeline: list[_WorkItemLogCell] = []
structured_tool_calls = list(payload.get("runtime_tool_calls", []) or [])
for item in list(payload.get("transcript", []) or [])[-limit:]:
message = item.get("message") if isinstance(item, dict) else None
if not message or getattr(message, "summary_flag", False):
continue
content = _render_transcript_parts(item.get("parts", []))
if not content:
continue
role = str(getattr(message, "role", "") or "assistant").strip().lower()
if not full and _is_staffing_transcript_noise(role, content):
continue
agent_id = str(getattr(message, "agent_id", "") or "").strip()
sender = {
"user": "user",
"assistant": "assistant",
"system": "system",
"subagent": agent_id or "subagent",
}.get(role, agent_id or role or "assistant")
timeline.append(_WorkItemLogCell(
kind="user" if role == "user" else "assistant",
created_at=getattr(message, "created_at", None),
label=sender,
content=content,
order=10,
))
for entry in list(payload.get("runtime_transcript_entries", []) or [])[-limit:]:
content = str(entry.get("content", "") or "")
if not content:
continue
stream_cells = _runtime_stream_event_cells(entry, full=full, skip_tool_events=bool(structured_tool_calls))
if stream_cells:
timeline.extend(stream_cells)
continue
if _parse_runtime_stream_event(content):
continue
label = str(entry.get("entry_type", "") or "runtime")
kind = "assistant"
order = 20
match = _EXTERNAL_TRANSCRIPT_RE.match(content)
if match:
agent = match.group("agent")
external_kind = match.group("kind").strip().lower()
content = match.group("body").strip()
label = agent
if external_kind == "tool":
if structured_tool_calls:
continue
kind = "tool_call"
order = 30
elif external_kind == "thinking":
kind = "thinking"
order = 15
elif external_kind in {"error", "failed", "failure"}:
kind = "event"
label = f"{agent} {external_kind}"
order = 55
else:
kind = "assistant"
label = agent
timeline.append(_WorkItemLogCell(
kind=kind,
created_at=entry.get("created_at"),
label=label,
content=content,
order=order,
))
for call in structured_tool_calls[-limit:]:
tool_name = str(call.get("tool_name", "") or "tool")
timeline.append(_WorkItemLogCell(
kind="tool_call",
created_at=call.get("created_at"),
label=tool_name,
content=_runtime_tool_command_text(call.get("arguments", {}) or {}, full=full),
order=30,
))
for result in list(payload.get("runtime_tool_results", []) or [])[-limit:]:
tool_name = str(result.get("tool_name", "") or "tool")
status, exit_code = _runtime_tool_result_status(result.get("payload", {}) or {})
timeline.append(_WorkItemLogCell(
kind="tool_result",
created_at=result.get("created_at"),
label=tool_name,
content=_runtime_tool_result_text(result.get("payload", {}) or {}, full=full),
order=35,
status=status,
exit_code=exit_code,
))
for event in list(payload.get("events", []) or [])[-limit:]:
event_type = str(event.get("event_type", "") or "event")
if not full and not _event_is_high_signal(event_type):
continue
work_item_id = str(event.get("work_item_id", "") or "")[:12]
timeline.append(_WorkItemLogCell(
kind="event",
created_at=event.get("created_at"),
label=f"event {event_type}",
content=f"{work_item_id} {_json_summary(event.get('payload', {}) or {}, limit=1000, full=full)}".strip(),
order=50,
))
for event in list(payload.get("runtime_events", []) or [])[-limit:]:
event_type = str(event.get("event_type", "") or "runtime")
if not full and not _event_is_high_signal(event_type):
continue
timeline.append(_WorkItemLogCell(
kind="event",
created_at=event.get("created_at"),
label=f"runtime {event_type}",
content=_json_summary(event.get("payload", {}) or {}, limit=1000, full=full),
order=50,
))
for grant in list(payload.get("runtime_permission_grants", []) or [])[-limit:]:
timeline.append(_WorkItemLogCell(
kind="permission",
created_at=grant.get("created_at"),
label="permission",
content=f"{grant.get('scope', '')} {grant.get('tool_name', '')}: {grant.get('candidate', '')}".strip(),
order=45,
))
for handoff in list(payload.get("handoffs", []) or [])[-limit:]:
timeline.append(_WorkItemLogCell(
kind="handoff",
created_at=handoff.get("created_at"),
label=f"handoff {handoff.get('from_role', '')}->{handoff.get('to_role', '')}",
content=str(handoff.get("summary", "") or _json_summary(handoff, limit=1000, full=full)),
order=40,
))
if not timeline and not work_items:
console.print("[info]No role/work-item logs found for the current session.[/info]")
return
timeline.sort(key=lambda item: (str(item.created_at or ""), item.order, item.label))
for index, item in enumerate(timeline[-limit:]):
if index:
console.print()
_render_work_item_log_cell(item, full=full)
async def _handle_logs_slash(state: _InteractiveChatState, args: list[str]) -> None:
store = _require_chat_store(state, label="Log store")
if store is None:
return
try:
args, limit, full = _parse_view_args(args)
except ValueError as exc:
console.print(f"[warning]{exc}. Try /logs <task_id|session_id> --limit 20.[/warning]")
return
if len(args) != 1:
console.print("[warning]Usage: /logs <task_id|session_id> [--limit N] [--full]. Try /session list or /tasks.[/warning]")
return
task, session, runtime_rows, missing = await _resolve_logs_target(state, args[0], limit=limit)
if missing:
console.print(f"[warning]No task, session, or runtime session found for {missing}. Try /session list or /tasks.[/warning]")
return
if task:
_render_key_value_table(
f"Task Log Target {getattr(task, 'id', '')}",
[
("title", getattr(task, "title", "")),
("status", _value_text(getattr(task, "status", ""))),
("session", getattr(task, "session_id", "")),
("runtime", _runtime_session_id_from_task(task)),
],
full=full,
)
_render_task_progress(task, limit=limit, full=full)
if session:
session_id = str(getattr(session, "session_id", "") or "")
_render_key_value_table(
f"Session Log Target {session_id}",
[
("title", getattr(session, "title", "")),
("status", getattr(session, "status", "")),
("mode", getattr(session, "mode", "")),
],
full=full,
)
if hasattr(store, "get_session_transcript"):
transcript = await store.get_session_transcript(session_id)
_render_transcript_table(transcript, limit=limit)
await _render_runtime_log_tables(store, runtime_rows, limit=limit, full=full)
async def _task_scope_ids_for_comms(state: _InteractiveChatState, task: Any) -> list[str]:
task_id = str(getattr(task, "id", "") or "").strip()
metadata = dict(getattr(task, "metadata", {}) or {})
scope = {task_id}
scope.update(str(item).strip() for item in list(metadata.get("execution_task_ids", []) or []) if str(item).strip())
parent_session_id = str(getattr(task, "session_id", "") or "").strip()
store = getattr(state.engine, "store", None)
if parent_session_id and store and hasattr(store, "get_tasks"):
try:
tasks = await store.get_tasks(project_id=_current_project_id(state.engine))
for candidate in tasks:
if str(getattr(candidate, "parent_session_id", "") or "").strip() == parent_session_id:
scope.add(str(getattr(candidate, "id", "") or "").strip())
except Exception:
pass
scope.discard("")
return sorted(scope)
def _render_agent_messages(messages: list[Any], *, full: bool = False) -> None:
if not messages:
console.print("[info]No company-mode messages found for this task scope.[/info]")
return
table = Table(title=f"Company Messages ({len(messages)})")
table.add_column("Time")
table.add_column("From")
table.add_column("To")
table.add_column("Type")
table.add_column("Status")
table.add_column("Subject")
table.add_column("Body")
for message in messages:
to_agents = getattr(message, "to_agents", []) or []
table.add_row(
_format_datetime(getattr(message, "timestamp", None)),
str(getattr(message, "from_agent", "") or ""),
", ".join(str(item) for item in to_agents),
_value_text(getattr(message, "msg_type", "")),
_value_text(getattr(message, "status", "")),
_clip_text(getattr(message, "subject", "") or "", 80, full=full),
_clip_text(getattr(message, "body", "") or "", 180, full=full),
)
console.print(table)
def _render_handoff_records(records: list[Any], *, full: bool = False) -> None:
if not records:
console.print("[info]No handoff records found for this task scope.[/info]")
return
table = Table(title=f"Handoffs ({len(records)})")
table.add_column("Handoff ID")
table.add_column("From")
table.add_column("To")
table.add_column("Status")
table.add_column("Task")
table.add_column("Summary")
table.add_column("Created")
for record in records:
table.add_row(
str(getattr(record, "handoff_id", "") or ""),
str(getattr(record, "from_role", "") or ""),
str(getattr(record, "to_role", "") or ""),
str(getattr(record, "status", "") or ""),
str(getattr(record, "task_id", "") or ""),
_clip_text(getattr(record, "summary", "") or "", 180, full=full),
_format_datetime(getattr(record, "created_at", None)),
)
console.print(table)
async def _handle_comms_slash(state: _InteractiveChatState, args: list[str]) -> None:
store = _require_chat_store(state, label="Comms store")
if store is None:
return
try:
args, limit, full = _parse_view_args(args)
except ValueError as exc:
console.print(f"[warning]{exc}. Try /comms <task_id>.[/warning]")
return
if len(args) != 1:
console.print("[warning]Usage: /comms <task_id> [--limit N] [--full]. Try /tasks.[/warning]")
return
task = await _get_task_for_current_project(state, args[0])
if task is None:
return
scope_ids = await _task_scope_ids_for_comms(state, task)
console.print(f"[info]Comms task scope: {', '.join(scope_ids)}[/info]")
messages = await store.list_agent_messages_for_tasks(scope_ids, limit=limit) if hasattr(store, "list_agent_messages_for_tasks") else []
_render_agent_messages(messages, full=full)
records: list[Any] = []
if hasattr(store, "get_handoff_records"):
seen: set[str] = set()
for task_id in scope_ids:
for record in await store.get_handoff_records(project_id=_current_project_id(state.engine), task_id=task_id, limit=limit):
handoff_id = str(getattr(record, "handoff_id", "") or "")
if handoff_id and handoff_id not in seen:
seen.add(handoff_id)
records.append(record)
_render_handoff_records(records[:limit], full=full)
metadata = dict(getattr(task, "metadata", {}) or {})
context_snapshot = dict(getattr(task, "context_snapshot", {}) or {})
notes = []
for key in ("structured_review_verdict", "review_verdict", "review_notes", "handoff_context", "handoff_to"):
if metadata.get(key):
notes.append((key, _json_summary(metadata.get(key), full=full)))
if context_snapshot.get(key):
notes.append((f"context.{key}", _json_summary(context_snapshot.get(key), full=full)))
if notes:
_render_key_value_table("Review and Handoff Notes", notes, full=full)
else:
console.print("[info]No review notes or handoff context found on the task.[/info]")
def _collect_attachment_refs(value: Any, source: str, out: list[tuple[str, dict[str, Any]]]) -> None:
if isinstance(value, dict):
if value.get("attachment_id") or value.get("disk_path"):
if value.get("filename") or value.get("mime_type") or value.get("disk_path"):
out.append((source, dict(value)))
for key, nested in value.items():
if key in {"attachment_refs", "attachments"} or isinstance(nested, (dict, list)):
_collect_attachment_refs(nested, source, out)
return
if isinstance(value, list):
for item in value:
_collect_attachment_refs(item, source, out)
async def _handle_attachments_slash(state: _InteractiveChatState, args: list[str]) -> None:
store = _require_chat_store(state, label="Attachment store")
if store is None:
return
try:
args, limit, full = _parse_view_args(args)
except ValueError as exc:
console.print(f"[warning]{exc}. Try /attachments --limit 20.[/warning]")
return
if args:
console.print("[warning]Usage: /attachments [--limit N] [--full].[/warning]")
return
refs: list[tuple[str, dict[str, Any]]] = []
if state.session_id and hasattr(store, "get_session"):
session = await store.get_session(state.session_id)
if session:
_collect_attachment_refs(getattr(session, "metadata", {}) or {}, f"session:{state.session_id}", refs)
if state.session_id and hasattr(store, "get_session_transcript"):
transcript = await store.get_session_transcript(state.session_id)
for idx, item in enumerate(transcript):
if isinstance(item, dict):
message = item.get("message")
_collect_attachment_refs(getattr(message, "metadata", {}) or {}, f"message:{idx}", refs)
for part in item.get("parts", []) or []:
payload = part.get("payload", {}) if isinstance(part, dict) else getattr(part, "payload", {})
_collect_attachment_refs(payload, f"message:{idx}", refs)
if hasattr(store, "get_tasks"):
for task in await store.get_tasks(project_id=_current_project_id(state.engine)):
if str(getattr(task, "session_id", "") or "") == state.session_id:
_collect_attachment_refs(getattr(task, "metadata", {}) or {}, f"task:{getattr(task, 'id', '')}", refs)
_collect_attachment_refs(getattr(task, "result", {}) or {}, f"task:{getattr(task, 'id', '')}", refs)
deduped: list[tuple[str, dict[str, Any]]] = []
seen: set[str] = set()
for source, ref in refs:
key = str(ref.get("attachment_id") or ref.get("disk_path") or ref)
if key in seen:
continue
seen.add(key)
deduped.append((source, ref))
if not deduped:
console.print("[info]No attachments found for the current session.[/info]")
return
table = Table(title=f"Attachments ({len(deduped[:limit])})")
table.add_column("Attachment ID")
table.add_column("Filename")
table.add_column("MIME")
table.add_column("Size")
table.add_column("Source")
table.add_column("Path")
for source, ref in deduped[:limit]:
table.add_row(
str(ref.get("attachment_id", "") or ""),
str(ref.get("filename", "") or ""),
str(ref.get("mime_type", "") or ""),
str(ref.get("size_bytes", "") or ""),
source,
_clip_text(ref.get("disk_path", "") or "", 100, full=full),
)
console.print(table)
async def _handle_mode_slash(state: _InteractiveChatState, args: list[str]) -> None:
if not args:
table = Table(title="Mode")
table.add_column("Mode", style="cyan")
table.add_column("Current", justify="center")
table.add_column("Command")
table.add_row("task", "*" if state.mode == "task" else "", "/mode task")
table.add_row("company/corporate", "*" if state.mode == "company" and state.company_profile == "corporate" else "", "/mode company corporate")
table.add_row("company/custom", "*" if state.mode == "company" and state.company_profile == "custom" else "", "/mode company custom")
console.print(table)
console.print(f"[info]Current mode: {state.mode}; company_profile={state.company_profile}[/info]")
return
mode = args[0].strip().lower()
if mode == "project":
mode = "task"
if mode not in _VALID_CHAT_MODES:
console.print("[warning]Usage: /mode [task|company] [corporate|custom][/warning]")
return
next_company_profile = state.company_profile
if mode == "company":
if len(args) >= 2:
profile = args[1].strip().lower()
if profile not in _VALID_COMPANY_PROFILES:
console.print("[warning]Company profile must be corporate or custom.[/warning]")
return
next_company_profile = profile
state.mode = mode
state.company_profile = next_company_profile
await _persist_chat_context(state)
console.print(f"[success]Mode set to {state.mode}[/success]")
if state.mode == "company":
console.print(f"[info]Company profile: {state.company_profile}[/info]")
async def _handle_agent_slash(state: _InteractiveChatState, args: list[str]) -> None:
if not args:
table = Table(title="Agent")
table.add_column("Agent", style="cyan")
table.add_column("Current", justify="center")
table.add_column("Command")
for agent in ["native", "codex", "claude_code", "cursor", "opencode"]:
table.add_row(agent, "*" if state.preferred_agent == agent else "", f"/agent {agent}")
table.add_row("system default", "*" if state.preferred_agent is None else "", "/agent none")
console.print(table)
console.print(f"[info]Preferred agent: {state.preferred_agent or '(system default)'}[/info]")
return
command = args[0].strip().lower()
if command in {"list", "ls"}:
payload = await _run_chat_office_service(state, lambda svc: svc.agent.list())
if payload:
_emit_payload(payload)
return
if command == "detail":
if len(args) != 2:
console.print("[warning]Usage: /agent detail <agent_id>[/warning]")
return
payload = await _run_chat_office_service(state, lambda svc: svc.agent.detail(project_id=_current_project_id(state.engine), agent_id=args[1]))
if payload:
_emit_payload(payload)
return
if command == "create":
rest = args[1:]
try:
rest, description = _extract_option(rest, "--description", default="")
rest, office_id = _extract_option(rest, "--office", default="office-0")
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
if len(rest) != 2:
console.print("[warning]Usage: /agent create <name> <role_id> [--description ...] [--office ...][/warning]")
return
payload = await _run_chat_office_service(
state,
lambda svc: svc.agent.create(name=rest[0], role_id=rest[1], office_id=office_id or "office-0", description=description or ""),
)
if payload:
_emit_payload(payload)
return
if command == "create-from-template":
rest = args[1:]
try:
rest, role_id = _extract_option(rest, "--role")
rest, office_id = _extract_option(rest, "--office", default="office-0")
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
if len(rest) != 1:
console.print("[warning]Usage: /agent create-from-template <template_id> [--role <role_id>] [--office ...][/warning]")
return
payload = await _run_chat_office_service(
state,
lambda svc: svc.agent.create_from_template(template_id=rest[0], role_id=role_id or rest[0], office_id=office_id or "office-0"),
)
if payload:
_emit_payload(payload)
return
if command in {"import-employee", "import-agent"}:
rest = args[1:]
try:
rest, office_id = _extract_option(rest, "--office", default="office-0")
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
if len(rest) != 1:
console.print("[warning]Usage: /agent import-employee <employee_id> [--office ...][/warning]")
return
payload = await _run_chat_office_service(state, lambda svc: svc.agent.import_employee(employee_id=rest[0], office_id=office_id or "office-0"))
if payload:
_emit_payload(payload)
return
if command == "delete":
if len(args) < 2:
console.print("[warning]Usage: /agent delete <agent_id> --yes[/warning]")
return
remaining, ok = _require_yes_arg(args[2:], usage="/agent delete <agent_id> --yes")
if not ok or remaining:
return
payload = await _run_chat_office_service(state, lambda svc: svc.agent.delete(args[1]))
if payload:
_emit_payload(payload)
return
if command == "move":
rest = args[1:]
try:
rest, seat_zone = _extract_option(rest, "--seat-zone")
rest, desk_id = _extract_option(rest, "--desk")
except ValueError as exc:
console.print(f"[warning]{exc}[/warning]")
return
if len(rest) != 2:
console.print("[warning]Usage: /agent move <agent_id> <office_id> [--seat-zone ...] [--desk ...][/warning]")
return
payload = await _run_chat_office_service(
state,
lambda svc: svc.agent.move(agent_id=rest[0], office_id=rest[1], seat_zone=seat_zone, desk_id=desk_id),
)
if payload:
_emit_payload(payload)
return
agent = args[0].strip().lower().replace("-", "_")
if agent in {"none", "auto", "default"}:
state.preferred_agent = None
await _persist_chat_context(state)
console.print("[success]Preferred agent cleared; using system default.[/success]")
return
if agent not in _VALID_PREFERRED_AGENTS:
valid = ", ".join(sorted([*_VALID_PREFERRED_AGENTS, "none"]))
console.print(f"[warning]Unknown agent '{args[0]}'. Valid: {valid}[/warning]")
return
state.preferred_agent = agent
await _persist_chat_context(state)
console.print(f"[success]Preferred agent set to {agent}[/success]")
def _handle_domains_slash(state: _InteractiveChatState, args: list[str]) -> None:
if not args:
console.print(f"[info]Domains: {', '.join(state.domains) if state.domains else '(none)'}[/info]")
return
if len(args) == 1 and args[0].strip().lower() in {"clear", "none", "reset"}:
state.domains = []
console.print("[success]Domain hints cleared.[/success]")
return
state.domains = [arg.strip().lower() for arg in args if arg.strip()]
console.print(f"[success]Domains set to: {', '.join(state.domains) if state.domains else '(none)'}[/success]")
def _staffing_checkpoint_key(checkpoint: Any) -> str:
return str(
getattr(checkpoint, "checkpoint_id", "")
or getattr(checkpoint, "session_id", "")
or "company_staffing_selection"
)
def _company_staffing_payload(checkpoint: Any) -> dict[str, Any]:
payload = getattr(checkpoint, "payload", {}) or {}
return dict(payload) if isinstance(payload, dict) else {}
def _normalize_cli_staffing_selection(value: Any) -> dict[str, str]:
if not isinstance(value, dict):
return {"kind": "fallback", "id": ""}
kind = str(value.get("kind", "") or "").strip().lower()
if kind in {"employee", "emp"}:
selected_id = str(value.get("id") or value.get("employee_id") or "").strip()
return {"kind": "employee", "id": selected_id} if selected_id else {"kind": "fallback", "id": ""}
if kind in {"template", "tpl"}:
selected_id = str(value.get("id") or value.get("template_id") or "").strip()
return {"kind": "template", "id": selected_id} if selected_id else {"kind": "fallback", "id": ""}
return {"kind": "fallback", "id": ""}
def _company_staffing_default_draft(payload: dict[str, Any]) -> dict[str, Any]:
selections: dict[str, dict[str, str]] = {}
role_agents: dict[str, str] = {}
for role in list(payload.get("staffing_roles", []) or []):
role_id = str(role.get("role_id", "") or "").strip()
if not role_id:
continue
selections[role_id] = _normalize_cli_staffing_selection(role.get("default_selection"))
role_agents[role_id] = (
str(role.get("selected_agent") or role.get("default_agent") or "codex").strip().lower().replace("-", "_")
or "codex"
)
return {"staffing_selections": selections, "recruitment_role_agents": role_agents}
def _company_staffing_resume_metadata(draft: dict[str, Any], checkpoint: Any | None = None) -> dict[str, Any]:
metadata = {
"staffing_action": "manual_approve",
"staffing_selections": dict(draft.get("staffing_selections", {}) or {}),
"recruitment_role_agents": dict(draft.get("recruitment_role_agents", {}) or {}),
}
if checkpoint is not None:
checkpoint_id = str(getattr(checkpoint, "checkpoint_id", "") or "").strip()
if checkpoint_id:
metadata["response_to_checkpoint_id"] = checkpoint_id
metadata["response_to_checkpoint_type"] = "company_staffing_selection"
return metadata
def _staffing_pool_maps(payload: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]]]:
pool = dict(payload.get("staffing_pool", {}) or {})
employees = {
str(item.get("employee_id", "") or "").strip(): dict(item)
for item in list(pool.get("employees", []) or [])
if str(item.get("employee_id", "") or "").strip()
}
templates = {
str(item.get("template_id", "") or "").strip(): dict(item)
for item in list(pool.get("templates", []) or [])
if str(item.get("template_id", "") or "").strip()
}
return employees, templates
def _company_staffing_selection_label(selection: Any, payload: dict[str, Any]) -> str:
normalized = _normalize_cli_staffing_selection(selection)
employees, templates = _staffing_pool_maps(payload)
selected_id = normalized.get("id", "")
if normalized["kind"] == "employee":
employee = employees.get(selected_id, {})
name = str(employee.get("employee_name", "") or selected_id)
return f"{name} ({selected_id})"
if normalized["kind"] == "template":
template = templates.get(selected_id, {})
name = str(template.get("template_name", "") or selected_id)
return f"Hire template: {name} ({selected_id})"
return "role-only fallback"
def _company_staffing_agent_choices(state: _InteractiveChatState) -> list[dict[str, Any]]:
available: set[str] = {"native"}
registry = getattr(state.engine, "adapter_registry", None)
if registry and callable(getattr(registry, "list_available", None)):
try:
available.update(str(item).strip().lower().replace("-", "_") for item in registry.list_available())
except Exception:
pass
ordered = ["native", "codex", "claude_code", "cursor", "opencode"]
return [
{
"agent": agent,
"label": f"{agent} ({'available' if agent in available else 'unavailable'})",
"available": agent in available,
"search": agent.replace("_", " "),
}
for agent in ordered
]
def _company_staffing_employee_choices(payload: dict[str, Any], role: dict[str, Any]) -> list[dict[str, Any]]:
employees, templates = _staffing_pool_maps(payload)
role_id = str(role.get("role_id", "") or "").strip()
same_role_ids = [
str(item or "").strip()
for item in list(role.get("same_role_employee_ids", []) or [])
if str(item or "").strip()
]
choices: list[dict[str, Any]] = []
seen: set[str] = set()
def _append_employee(employee_id: str, prefix: str) -> None:
employee = employees.get(employee_id)
if not employee or employee_id in seen:
return
seen.add(employee_id)
label = f"{prefix}: {employee.get('employee_name') or employee_id} ({employee_id})"
choices.append(
{
"kind": "employee",
"id": employee_id,
"label": label,
"search": " ".join(
str(part or "")
for part in [
employee_id,
employee.get("employee_name"),
employee.get("role_id"),
employee.get("category"),
" ".join(employee.get("domains", []) or []),
" ".join(employee.get("tags", []) or []),
employee.get("description"),
]
).lower(),
}
)
for employee_id in same_role_ids:
_append_employee(employee_id, "same role")
for employee_id, employee in employees.items():
employee_role = str(employee.get("role_id", "") or "").strip()
prefix = "same role" if employee_role == role_id else "employee"
_append_employee(employee_id, prefix)
for template_id, template in templates.items():
choices.append(
{
"kind": "template",
"id": template_id,
"label": f"template: {template.get('template_name') or template_id} ({template_id})",
"search": " ".join(
str(part or "")
for part in [
template_id,
template.get("template_name"),
template.get("category"),
" ".join(template.get("domains", []) or []),
" ".join(template.get("tags", []) or []),
template.get("description"),
]
).lower(),
}
)
if bool(role.get("fallback_available", True)):
choices.append({"kind": "fallback", "id": "", "label": "fallback: role-only execution", "search": f"fallback role only {role_id}"})
return choices
def _company_staffing_filter_options(options: list[dict[str, Any]], query: str) -> list[dict[str, Any]]:
normalized = str(query or "").strip().lower()
if not normalized:
return options
tokens = [token for token in re.split(r"\s+", normalized) if token]
if not tokens:
return options
return [
option
for option in options
if all(token in f"{option.get('label', '')} {option.get('search', '')}".lower() for token in tokens)
]
def _render_company_staffing_summary(
state: _InteractiveChatState,
checkpoint: Any,
draft: dict[str, Any],
) -> None:
payload = _company_staffing_payload(checkpoint)
selections = dict(draft.get("staffing_selections", {}) or {})
role_agents = dict(draft.get("recruitment_role_agents", {}) or {})
agent_status = {item["agent"]: item for item in _company_staffing_agent_choices(state)}
table = Table(title="Company Staffing Preflight")
table.add_column("Role", style="cyan")
table.add_column("Employee / Template")
table.add_column("Agent")
table.add_column("Status")
for role in list(payload.get("staffing_roles", []) or []):
role_id = str(role.get("role_id", "") or "").strip()
if not role_id:
continue
role_label = str(role.get("role_label", "") or role_id)
agent = str(role_agents.get(role_id) or role.get("selected_agent") or role.get("default_agent") or "codex").strip().lower().replace("-", "_")
status = "available" if agent_status.get(agent, {}).get("available") else "unavailable"
table.add_row(
f"{role_label}\n[dim]{role_id}[/dim]",
_company_staffing_selection_label(selections.get(role_id), payload),
agent,
status,
)
console.print(table)
recommended = str(payload.get("recommended_action", "") or "").strip()
summary = str(payload.get("summary", "") or "").strip()
if summary:
console.print(f"[dim]{summary}[/dim]")
if recommended == "auto_recruit":
console.print("[info]r auto recruit (recommended) | e edit | a approve selections | c context | d deny | /staffing reopen this editor later[/info]")
else:
console.print("[info]a approve selections | e edit | c context | r auto recruit | d deny | /staffing reopen this editor later[/info]")
def _render_company_staffing_context_preview(
state: _InteractiveChatState,
checkpoint: Any,
draft: dict[str, Any],
) -> None:
payload = _company_staffing_payload(checkpoint)
original_message = _clip_text(payload.get("original_message"), 360, full=False) or "(empty request)"
selections = dict(draft.get("staffing_selections", {}) or {})
role_agents = dict(draft.get("recruitment_role_agents", {}) or {})
table = Table(title="Initial Role Context Preview")
table.add_column("Role", style="cyan")
table.add_column("Agent")
table.add_column("Staffing")
table.add_column("Context Preview")
for role in list(payload.get("staffing_roles", []) or []):
role_id = str(role.get("role_id", "") or "").strip()
if not role_id:
continue
role_label = str(role.get("role_label", "") or role_id).strip()
role_responsibility = _clip_text(role.get("role_responsibility"), 180, full=False)
selected_agent = str(role_agents.get(role_id) or role.get("selected_agent") or role.get("default_agent") or "codex").strip()
staffing_label = _company_staffing_selection_label(selections.get(role_id), payload)
context_lines = [
f"Owner request: {original_message}",
]
if role_responsibility:
context_lines.append(f"Role responsibility: {role_responsibility}")
context_lines.append(
"Runtime note: exact child WorkItem briefs are created by the manager at execution time; "
"inspect them later with /work-items show or role logs."
)
table.add_row(
f"{role_label}\n[dim]{role_id}[/dim]",
selected_agent,
_clip_text(staffing_label, 120, full=False),
"\n".join(context_lines),
)
console.print(table)
async def _latest_company_staffing_checkpoint(state: _InteractiveChatState) -> Any | None:
getter = getattr(state.engine, "get_latest_pending_checkpoint_for_session", None)
if not callable(getter) or not state.session_id:
return None
try:
checkpoint = await getter(state.session_id)
except Exception:
return None
if str(getattr(checkpoint, "checkpoint_type", "") or "") != "company_staffing_selection":
return None
return checkpoint
def _apply_staffing_choice(draft: dict[str, Any], role_id: str, choice: dict[str, Any], *, column: str) -> None:
if column == "agent":
agent = str(choice.get("agent", "") or "").strip().lower().replace("-", "_")
if agent:
draft.setdefault("recruitment_role_agents", {})[role_id] = agent
return
kind = str(choice.get("kind", "") or "fallback").strip().lower()
selected_id = str(choice.get("id", "") or "").strip()
if kind not in {"employee", "template"}:
draft.setdefault("staffing_selections", {})[role_id] = {"kind": "fallback", "id": ""}
else:
draft.setdefault("staffing_selections", {})[role_id] = {"kind": kind, "id": selected_id}
def _render_staffing_editor_text(
state: _InteractiveChatState,
payload: dict[str, Any],
draft: dict[str, Any],
row_index: int,
column_index: int,
query: str,
choice_index: int,
) -> str:
roles = [dict(role) for role in list(payload.get("staffing_roles", []) or []) if str(role.get("role_id", "") or "").strip()]
if not roles:
return "No staffing roles are available.\nPress q to exit."
row_index = max(0, min(row_index, len(roles) - 1))
role = roles[row_index]
role_id = str(role.get("role_id", "") or "")
column_name = "Employee/Template" if column_index == 0 else "Agent"
options = (
_company_staffing_employee_choices(payload, role)
if column_index == 0
else _company_staffing_agent_choices(state)
)
matches = _company_staffing_filter_options(options, query)[:8]
lines = [
"Company staffing editor",
"Up/Down: role or match | Left/Right/Tab: column | type to search | Enter: select | Esc/q: save | Ctrl-C: cancel",
"",
]
selections = dict(draft.get("staffing_selections", {}) or {})
role_agents = dict(draft.get("recruitment_role_agents", {}) or {})
for idx, item in enumerate(roles):
item_role_id = str(item.get("role_id", "") or "")
marker = ">" if idx == row_index else " "
selected = _company_staffing_selection_label(selections.get(item_role_id), payload)
agent = str(role_agents.get(item_role_id) or item.get("selected_agent") or item.get("default_agent") or "codex")
lines.append(f"{marker} {item.get('role_label') or item_role_id} [{item_role_id}]")
lines.append(f" Employee/Template: {selected}")
lines.append(f" Agent: {agent}")
lines.extend(["", f"Editing {role.get('role_label') or role_id} / {column_name}", f"Search: {query or '(all)'}"])
if not matches:
lines.append(" no matches")
for idx, option in enumerate(matches):
marker = ">" if idx == choice_index else " "
lines.append(f" {marker} {option.get('label')}")
return "\n".join(lines)
async def _edit_company_staffing_draft(
state: _InteractiveChatState,
checkpoint: Any,
draft: dict[str, Any],
) -> dict[str, Any] | None:
payload = _company_staffing_payload(checkpoint)
roles = [dict(role) for role in list(payload.get("staffing_roles", []) or []) if str(role.get("role_id", "") or "").strip()]
if not roles:
console.print("[warning]This staffing checkpoint has no roles to edit.[/warning]")
return draft
working = {
"staffing_selections": dict(draft.get("staffing_selections", {}) or {}),
"recruitment_role_agents": dict(draft.get("recruitment_role_agents", {}) or {}),
}
try:
from prompt_toolkit.application import Application
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout.containers import HSplit, Window
from prompt_toolkit.layout.controls import FormattedTextControl
except Exception:
return _edit_company_staffing_draft_fallback(payload, working)
row_index = 0
column_index = 0
query = ""
choice_index = 0
kb = KeyBindings()
def _current_options() -> list[dict[str, Any]]:
role = roles[row_index]
options = (
_company_staffing_employee_choices(payload, role)
if column_index == 0
else _company_staffing_agent_choices(state)
)
return _company_staffing_filter_options(options, query)
def _reset_choice() -> None:
nonlocal choice_index
choice_index = 0
def _render() -> str:
return _render_staffing_editor_text(state, payload, working, row_index, column_index, query, choice_index)
@kb.add("up")
def _up(event) -> None: # pragma: no cover - exercised through prompt_toolkit in real terminals
nonlocal row_index, choice_index
if query:
choice_index = max(0, choice_index - 1)
else:
row_index = max(0, row_index - 1)
_reset_choice()
@kb.add("down")
def _down(event) -> None: # pragma: no cover
nonlocal row_index, choice_index
if query:
choice_index = min(max(0, len(_current_options()) - 1), choice_index + 1)
else:
row_index = min(len(roles) - 1, row_index + 1)
_reset_choice()
@kb.add("left")
def _left(event) -> None: # pragma: no cover
nonlocal column_index, query
column_index = 0
query = ""
_reset_choice()
@kb.add("right")
@kb.add("tab")
def _right(event) -> None: # pragma: no cover
nonlocal column_index, query
column_index = 1 if column_index == 0 else 0
query = ""
_reset_choice()
@kb.add("backspace")
def _backspace(event) -> None: # pragma: no cover
nonlocal query
query = query[:-1]
_reset_choice()
@kb.add("enter")
def _enter(event) -> None: # pragma: no cover
nonlocal query
options = _current_options()
if not options:
return
role_id = str(roles[row_index].get("role_id", "") or "")
_apply_staffing_choice(working, role_id, options[min(choice_index, len(options) - 1)], column="agent" if column_index else "employee")
query = ""
_reset_choice()
@kb.add("escape")
@kb.add("q")
def _save(event) -> None: # pragma: no cover
event.app.exit(result=True)
@kb.add("c-c")
def _cancel(event) -> None: # pragma: no cover
event.app.exit(result=False)
@kb.add("/")
def _slash(event) -> None: # pragma: no cover
nonlocal query
query = ""
_reset_choice()
@kb.add("<any>")
def _typing(event) -> None: # pragma: no cover
nonlocal query
data = getattr(event, "data", "") or ""
if data and data.isprintable():
query += data
_reset_choice()
app_editor = Application(
layout=Layout(HSplit([Window(content=FormattedTextControl(_render), wrap_lines=False)])),
key_bindings=kb,
full_screen=False,
)
try:
saved = await app_editor.run_async()
except Exception:
return _edit_company_staffing_draft_fallback(payload, working)
return working if saved else None
def _edit_company_staffing_draft_fallback(payload: dict[str, Any], draft: dict[str, Any]) -> dict[str, Any]:
console.print("[warning]Interactive editor unavailable. Use text overrides; blank line saves.[/warning]")
console.print("Examples: senior_engineer=emp:employee-id | senior_engineer=tpl:template-id | senior_engineer=fallback | agent senior_engineer=codex")
roles = {
str(role.get("role_id", "") or "").strip()
for role in list(payload.get("staffing_roles", []) or [])
if str(role.get("role_id", "") or "").strip()
}
while True:
try:
raw = console.input("[bold]staffing edit> [/bold]").strip()
except (EOFError, KeyboardInterrupt):
return draft
if not raw or raw.lower() in {"q", "quit", "save", "done"}:
return draft
if raw.lower().startswith("agent "):
raw = raw.split(" ", 1)[1].strip()
if "=" not in raw:
console.print("[warning]Usage: agent <role_id>=<agent>[/warning]")
continue
role_id, agent = [part.strip() for part in raw.split("=", 1)]
if role_id not in roles:
console.print(f"[warning]Unknown role: {role_id}[/warning]")
continue
draft.setdefault("recruitment_role_agents", {})[role_id] = agent.lower().replace("-", "_")
continue
if "=" not in raw:
console.print("[warning]Usage: <role_id>=emp:<id> | <role_id>=tpl:<id> | <role_id>=fallback[/warning]")
continue
role_id, selection = [part.strip() for part in raw.split("=", 1)]
if role_id not in roles:
console.print(f"[warning]Unknown role: {role_id}[/warning]")
continue
if selection.lower().startswith("emp:"):
draft.setdefault("staffing_selections", {})[role_id] = {"kind": "employee", "id": selection.split(":", 1)[1].strip()}
elif selection.lower().startswith("tpl:"):
draft.setdefault("staffing_selections", {})[role_id] = {"kind": "template", "id": selection.split(":", 1)[1].strip()}
else:
draft.setdefault("staffing_selections", {})[role_id] = {"kind": "fallback", "id": ""}
async def _resume_company_staffing_checkpoint(
state: _InteractiveChatState,
checkpoint: Any,
*,
action: str,
draft: dict[str, Any] | None = None,
controller: ChatTurnController | None = None,
) -> None:
metadata: dict[str, Any]
content: str
if action == "auto":
content = "auto"
metadata = {"staffing_action": "auto_recruit"}
elif action == "deny":
content = "deny"
metadata = {"staffing_action": "deny"}
else:
content = "approve"
metadata = _company_staffing_resume_metadata(draft or _company_staffing_default_draft(_company_staffing_payload(checkpoint)), checkpoint)
if action in {"auto", "deny"}:
checkpoint_id = str(getattr(checkpoint, "checkpoint_id", "") or "").strip()
if checkpoint_id:
metadata["response_to_checkpoint_id"] = checkpoint_id
metadata["response_to_checkpoint_type"] = "company_staffing_selection"
if controller is not None:
await controller.submit_control_message(
content,
message_metadata=metadata,
mode="company",
company_profile=state.company_profile,
preferred_agent=state.preferred_agent,
domains=list(state.domains),
)
return
state.runtime_display.begin_turn()
response = await state.engine.process_message(
content,
project_id=getattr(state.engine, "project_id", None),
session_id=state.session_id,
mode="company",
company_profile=state.company_profile,
preferred_agent=state.preferred_agent,
domains=list(state.domains),
message_metadata=metadata,
)
await state.runtime_display.flush()
await _sync_runtime_checkpoint_hint(state)
if hasattr(state.runtime_display, "render_status"):
await state.runtime_display.render_status(force=True)
if not state.runtime_display.has_streamed_content:
_print_response(response, state.no_markdown)
async def _maybe_run_company_staffing_preflight(state: _InteractiveChatState, controller: ChatTurnController | None = None) -> bool:
checkpoint = await _latest_company_staffing_checkpoint(state)
if checkpoint is None:
return False
key = _staffing_checkpoint_key(checkpoint)
draft = state.company_staffing_drafts.setdefault(key, _company_staffing_default_draft(_company_staffing_payload(checkpoint)))
while True:
_render_company_staffing_summary(state, checkpoint, draft)
try:
choice = console.input("[bold]Company preflight [a/e/c/r/d]: [/bold]").strip().lower()
except (EOFError, KeyboardInterrupt):
console.print("[warning]Staffing selection left pending. Reopen with /staffing.[/warning]")
return True
if choice in {"a", "approve", "y", "yes", ""}:
await _resume_company_staffing_checkpoint(state, checkpoint, action="approve", draft=draft, controller=controller)
state.company_staffing_drafts.pop(key, None)
return True
if choice in {"e", "edit"}:
edited = await _edit_company_staffing_draft(state, checkpoint, draft)
if edited is not None:
draft.clear()
draft.update(edited)
state.company_staffing_drafts[key] = draft
continue
if choice in {"c", "context", "ctx", "preview"}:
_render_company_staffing_context_preview(state, checkpoint, draft)
continue
if choice in {"r", "auto", "auto recruit", "autorecruit"}:
await _resume_company_staffing_checkpoint(state, checkpoint, action="auto", draft=draft, controller=controller)
state.company_staffing_drafts.pop(key, None)
return True
if choice in {"d", "deny", "cancel", "stop", "n", "no"}:
await _resume_company_staffing_checkpoint(state, checkpoint, action="deny", draft=draft, controller=controller)
state.company_staffing_drafts.pop(key, None)
return True
console.print("[warning]Choose a, e, c, r, or d.[/warning]")
async def _handle_company_staffing_shortcut(
state: _InteractiveChatState,
user_input: str,
controller: ChatTurnController | None,
) -> bool:
choice = str(user_input or "").strip().lower()
if choice not in {
"a",
"approve",
"y",
"yes",
"e",
"edit",
"c",
"context",
"ctx",
"preview",
"r",
"auto",
"auto recruit",
"autorecruit",
"d",
"deny",
"cancel",
"stop",
"n",
"no",
}:
return False
checkpoint = await _latest_company_staffing_checkpoint(state)
if checkpoint is None:
return False
key = _staffing_checkpoint_key(checkpoint)
draft = state.company_staffing_drafts.setdefault(
key,
_company_staffing_default_draft(_company_staffing_payload(checkpoint)),
)
if choice in {"a", "approve", "y", "yes"}:
await _resume_company_staffing_checkpoint(state, checkpoint, action="approve", draft=draft, controller=controller)
state.company_staffing_drafts.pop(key, None)
return True
if choice in {"e", "edit"}:
edited = await _edit_company_staffing_draft(state, checkpoint, draft)
if edited is not None:
draft.clear()
draft.update(edited)
state.company_staffing_drafts[key] = draft
_render_company_staffing_summary(state, checkpoint, draft)
return True
if choice in {"c", "context", "ctx", "preview"}:
_render_company_staffing_context_preview(state, checkpoint, draft)
return True
if choice in {"r", "auto", "auto recruit", "autorecruit"}:
await _resume_company_staffing_checkpoint(state, checkpoint, action="auto", draft=draft, controller=controller)
state.company_staffing_drafts.pop(key, None)
return True
if choice in {"d", "deny", "cancel", "stop", "n", "no"}:
await _resume_company_staffing_checkpoint(state, checkpoint, action="deny", draft=draft, controller=controller)
state.company_staffing_drafts.pop(key, None)
return True
return False
async def _handle_staffing_slash(state: _InteractiveChatState, args: list[str], controller: ChatTurnController | None = None) -> None:
if args and args[0].strip().lower() not in {"context", "ctx", "preview"}:
console.print("[warning]Usage: /staffing [context][/warning]")
return
if not args and not await _maybe_run_company_staffing_preflight(state, controller=controller):
console.print("[info]No pending company staffing selection for this session.[/info]")
return
if args:
checkpoint = await _latest_company_staffing_checkpoint(state)
if checkpoint is None:
console.print("[info]No pending company staffing selection for this session.[/info]")
return
key = _staffing_checkpoint_key(checkpoint)
draft = state.company_staffing_drafts.setdefault(key, _company_staffing_default_draft(_company_staffing_payload(checkpoint)))
_render_company_staffing_context_preview(state, checkpoint, draft)
def _busy_slash_policy(command: str, args: list[str]) -> BusyCommandPolicy:
readonly_roots = {
"help",
"?",
"status",
"cost",
"runtime",
"tasks",
"checkpoints",
"checkpoint",
"logs",
"comms",
"attachments",
"work-items",
"work-item",
"queue",
"kanban",
"board",
"stop",
"continue",
}
if command in readonly_roots:
return BusyCommandPolicy.IMMEDIATE_READONLY
if command == "task":
if args and args[0].lower() == "show":
return BusyCommandPolicy.IMMEDIATE_READONLY
return BusyCommandPolicy.BLOCKED_WHEN_BUSY
if command == "project":
if not args or args[0].lower() in {"list", "show"}:
return BusyCommandPolicy.IMMEDIATE_READONLY
return BusyCommandPolicy.BLOCKED_WHEN_BUSY
if command == "session":
if not args or args[0].lower() in {"list", "show", "stop", "continue"}:
return BusyCommandPolicy.IMMEDIATE_READONLY
return BusyCommandPolicy.BLOCKED_WHEN_BUSY
if command == "staffing":
if args and args[0].lower() == "context":
return BusyCommandPolicy.IMMEDIATE_READONLY
return BusyCommandPolicy.BLOCKED_WHEN_BUSY
return BusyCommandPolicy.BLOCKED_WHEN_BUSY
def _print_busy_slash_block(command: str) -> None:
console.print(
f"[warning]Busy: /{escape(command)} changes chat context or runtime state, so it is blocked while a turn is running.[/warning]"
)
console.print("[dim]Available now: /stop, /kanban, /runtime, /work-items, /logs, /comms, /queue, /status.[/dim]")
async def _handle_queue_slash(controller: ChatTurnController | None, args: list[str]) -> None:
if controller is None:
console.print("[warning]/queue is only available in interactive chat.[/warning]")
return
command = args[0].lower() if args else "list"
if command in {"list", "ls"}:
items = controller.queued_items()
if not items:
console.print("[info]No queued prompts.[/info]")
return
table = Table(title=f"Queued Prompts ({len(items)})")
table.add_column("#", justify="right", style="cyan")
table.add_column("Project")
table.add_column("Session")
table.add_column("Mode")
table.add_column("Prompt")
for idx, item in enumerate(items, start=1):
table.add_row(
str(idx),
item.project_id,
_session_short_id(item.session_id),
item.mode,
_clip_text(item.text, 100),
)
console.print(table)
return
if command == "drop":
if len(args) != 2:
console.print("[warning]Usage: /queue drop <n>[/warning]")
return
try:
index = int(args[1])
except ValueError:
console.print("[warning]Queue index must be a number.[/warning]")
return
item = controller.drop(index)
if item is None:
console.print("[warning]No queued prompt at that index.[/warning]")
return
console.print(f"[success]Dropped queued prompt #{index}:[/success] {_clip_text(item.text, 80)}")
return
if command == "clear":
count = controller.clear()
console.print(f"[success]Cleared {count} queued prompt(s).[/success]")
return
console.print("[warning]Usage: /queue list|drop <n>|clear[/warning]")
def _board_launch_args(state: _InteractiveChatState, args: list[str], *, default_view: str = "kanban") -> list[str]:
view = default_view
work_item = ""
role = ""
target = ""
rest = list(args)
if rest:
candidate = rest.pop(0).lower()
if candidate in {"kanban", "pipeline", "work-item", "role", "logs"}:
view = candidate
else:
rest.insert(0, candidate)
if view == "work-item" and rest:
work_item = rest[0]
elif view == "role" and rest:
role = rest[0]
elif view == "logs" and rest:
target = rest[0]
cmd = [
sys.executable,
"-m",
"opc.cli.app",
"board",
"--project",
_current_project_id(state.engine),
"--attach",
"--readonly",
"--refresh-interval",
"1.0",
"--view",
view,
]
if state.session_id:
cmd.extend(["--session", state.session_id])
if work_item:
cmd.extend(["--work-item", work_item])
if role:
cmd.extend(["--role", role])
if target:
cmd.extend(["--target", target])
return cmd
async def _launch_board_inspector(state: _InteractiveChatState, args: list[str], *, default_view: str = "kanban") -> None:
cmd = _board_launch_args(state, args, default_view=default_view)
console.print("[dim]Opening read-only board inspector. Press q or Ctrl-Q in board to return to chat.[/dim]")
display = state.runtime_display
if hasattr(display, "enter_sidecar_quiet"):
display.enter_sidecar_quiet()
try:
process = await asyncio.create_subprocess_exec(*cmd)
await process.wait()
except FileNotFoundError as exc:
console.print(f"[warning]Could not launch board inspector: {escape(str(exc))}[/warning]")
finally:
quiet_events = 0
if hasattr(display, "exit_sidecar_quiet"):
quiet_events = int(display.exit_sidecar_quiet() or 0)
suffix = f" Suppressed {quiet_events} runtime event(s); use /logs or /board logs to inspect them." if quiet_events else ""
console.print(f"[dim]Returned to chat.{suffix}[/dim]")
async def _fetch_kanban_items(
state: _InteractiveChatState,
*,
limit: int = 100,
session_id: str | None = None,
project_scope: bool = False,
) -> list[dict[str, Any]]:
from opc.plugins.office_ui.services.context import ModeState, OfficeServiceContext
from opc.plugins.office_ui.services.work_item import WorkItemService
context = OfficeServiceContext(
engine=state.engine,
agent_store=None,
chat_store=None,
event_adapter=None,
mode_state=ModeState(
exec_mode=state.mode,
company_profile=state.company_profile,
task_preferred_agent=state.preferred_agent or "native",
),
)
scoped_session_id = None if project_scope else str(session_id or state.session_id or "").strip()
result = await WorkItemService(context).list(
project_id=_current_project_id(state.engine),
session_id=scoped_session_id,
limit=limit,
kanban_visible_only=True,
)
return list(result.payload.get("work_items", []) or [])
def _kanban_items_fingerprint(items: list[dict[str, Any]]) -> str:
rows = []
for item in items:
rows.append((
str(item.get("work_item_id", "") or ""),
str(item.get("title", "") or ""),
str(item.get("role_id", "") or ""),
str(item.get("phase", "") or ""),
str(item.get("kanban_column", "") or ""),
str(item.get("runtime_task_id", "") or ""),
str(item.get("session_id", "") or ""),
str(item.get("updated_at", "") or ""),
))
return json.dumps(sorted(rows), ensure_ascii=False, sort_keys=True)
def _render_kanban_items(items: list[dict[str, Any]], *, title: str = "Kanban Work Items") -> None:
if not items:
console.print("[info]No company work items found yet.[/info]")
return
counts: dict[str, int] = {}
for item in items:
column = str(item.get("kanban_column") or item.get("phase") or "unknown")
counts[column] = counts.get(column, 0) + 1
summary = ", ".join(f"{column}:{count}" for column, count in sorted(counts.items()))
table = Table(title=f"{title} ({len(items)} | {summary})")
table.add_column("Work Item")
table.add_column("Title")
table.add_column("Role")
table.add_column("Column")
table.add_column("Phase")
table.add_column("Runtime Task")
table.add_column("Updated")
for item in items:
table.add_row(
str(item.get("work_item_id", "") or "")[:12],
_clip_text(item.get("title", "") or "", 64),
str(item.get("role_id", "") or ""),
str(item.get("kanban_column", "") or ""),
str(item.get("phase", "") or ""),
str(item.get("runtime_task_id", "") or "")[:12],
_clip_text(item.get("updated_at", "") or "", 19),
)
console.print(table)
async def _handle_kanban_slash(state: _InteractiveChatState, args: list[str], controller: ChatTurnController | None = None) -> None:
normalized = [arg.strip().lower() for arg in args]
if normalized and normalized[0] in {"stop", "off", "clear"}:
if controller is None:
console.print("[warning]Kanban live watch is only available in interactive chat.[/warning]")
return
await controller.stop_kanban_watch()
return
watch = True
project_scope = False
for token in normalized:
if token in {"once", "--once", "--summary", "summary"}:
watch = False
elif token in {"all", "project", "--all", "--project"}:
project_scope = True
else:
console.print("[warning]Usage: /kanban [once|stop|all][/warning]")
return
session_id = str(state.session_id or "").strip()
title = "Kanban Work Items (project)" if project_scope else f"Kanban Work Items (session {session_id[:8]})"
try:
items = await _fetch_kanban_items(state, limit=100, session_id=session_id, project_scope=project_scope)
except Exception as exc:
console.print(f"[warning]Could not load kanban work items: {escape(str(exc))}[/warning]")
return
_render_kanban_items(items, title=title)
if watch and controller is not None:
await controller.start_kanban_watch(
initial_fingerprint=_kanban_items_fingerprint(items),
session_id=session_id,
project_scope=project_scope,
)
elif watch:
console.print("[dim]Live watch is only available in interactive chat.[/dim]")
async def _handle_board_slash(state: _InteractiveChatState, args: list[str]) -> None:
if not args:
args = ["kanban"]
await _launch_board_inspector(state, args, default_view="kanban")
async def _handle_chat_slash_command(state: _InteractiveChatState, user_input: str, controller: ChatTurnController | None = None) -> bool:
if not user_input.startswith("/"):
return False
try:
parts = _slash_parts(user_input)
except ValueError as exc:
console.print(f"[warning]Could not parse command: {exc}[/warning]")
return True
if not parts:
_print_slash_help()
return True
command = _canonical_slash_command(parts[0])
args = parts[1:]
if controller is not None and controller.is_busy and command in _slash_command_names():
policy = _busy_slash_policy(command, args)
if policy == BusyCommandPolicy.BLOCKED_WHEN_BUSY:
_print_busy_slash_block(command)
return True
if command in {"help", "?"}:
_print_slash_help()
elif command == "queue":
await _handle_queue_slash(controller, args)
elif command == "status":
_print_interactive_status(state.engine, state)
elif command == "cost":
_print_interactive_cost(state.engine)
elif command == "mode":
await _handle_mode_slash(state, args)
elif command == "agent":
await _handle_agent_slash(state, args)
elif command == "domains":
_handle_domains_slash(state, args)
elif command == "project":
await _handle_project_slash(state, args)
elif command in {"new-session", "new_session"}:
_start_new_chat_session(state)
elif command == "sessions":
await _render_sessions(state, args)
elif command == "resume":
await _resume_chat_session(state, args[0] if args else "")
elif command == "session":
await _handle_session_slash(state, args, controller=controller)
elif command == "stop":
await _handle_stop_slash(state, args, controller=controller)
elif command == "continue":
await _handle_continue_slash(state, args, controller=controller)
elif command == "tasks":
await _handle_tasks_slash(state, args)
elif command == "task":
await _handle_task_slash(state, args)
elif command in {"checkpoints", "checkpoint"}:
await _handle_checkpoints_slash(state, args)
elif command == "org":
await _handle_org_slash(state, args)
elif command == "talent":
await _handle_talent_slash(state, args)
elif command == "market":
await _handle_market_slash(state, args)
elif command == "reorg":
await _handle_reorg_slash(state, args)
elif command == "runtime":
await _handle_runtime_slash(state, args)
elif command in {"work-items", "work-item"}:
await _handle_work_items_slash(state, args)
elif command == "logs":
await _handle_logs_slash(state, args)
elif command == "comms":
await _handle_comms_slash(state, args)
elif command == "attachments":
await _handle_attachments_slash(state, args)
elif command == "staffing":
await _handle_staffing_slash(state, args, controller=controller)
elif command == "kanban":
await _handle_kanban_slash(state, args, controller=controller)
elif command == "board":
await _handle_board_slash(state, args)
else:
console.print(f"[warning]Unknown command: /{escape(command)}. Try /help for available commands.[/warning]")
return True
async def _sync_runtime_checkpoint_hint(state: _InteractiveChatState) -> None:
display = state.runtime_display
if not hasattr(display, "set_checkpoint_hint"):
return
runtime_identity = await _company_runtime_identity_for_session(state)
if runtime_identity is not None and runtime_identity.checkpoint is not None:
checkpoint = runtime_identity.checkpoint
checkpoint_type = str(getattr(checkpoint, "checkpoint_type", "") or "pending")
checkpoint_id = str(getattr(checkpoint, "checkpoint_id", "") or "")
display.set_checkpoint_hint(checkpoint_type or checkpoint_id)
return
getter = getattr(state.engine, "get_latest_pending_checkpoint_for_session", None)
if not callable(getter) or not state.session_id:
display.set_checkpoint_hint("")
return
try:
checkpoint = await getter(state.session_id)
except Exception:
return
if not checkpoint:
display.set_checkpoint_hint("")
return
checkpoint_type = str(getattr(checkpoint, "checkpoint_type", "") or "pending")
checkpoint_id = str(getattr(checkpoint, "checkpoint_id", "") or "")
display.set_checkpoint_hint(checkpoint_type or checkpoint_id)
async def _process_interactive_chat_message(
state: _InteractiveChatState,
user_input: str,
*,
message_metadata: dict[str, Any] | None = None,
interactive_followups: bool = True,
) -> None:
console.print()
try:
state.runtime_display.begin_turn()
effective_metadata = message_metadata
execution_session_id = state.session_id
execution_origin_task_id: str | None = None
execution_mode = state.mode
execution_company_profile = state.company_profile
execution_org_id = state.org_id
execution_preferred_agent = state.preferred_agent
handoff_identity = None
runtime_identity = await _company_runtime_identity_for_session(state)
runtime_checkpoint = (
runtime_identity.checkpoint
if runtime_identity is not None
else None
)
explicit_runtime_type = str(
(effective_metadata or {}).get("response_to_checkpoint_type", "") or ""
).strip()
explicit_runtime_id = str(
(effective_metadata or {}).get("response_to_checkpoint_id", "") or ""
).strip()
is_explicit_runtime_handoff = explicit_runtime_type in {
"company_runtime_suspended",
"company_runtime_interrupted",
}
if is_explicit_runtime_handoff:
if runtime_identity is None or runtime_checkpoint is None:
raise RuntimeError(
"Company runtime checkpoint does not match the current session."
)
checkpoint_id = str(
getattr(runtime_checkpoint, "checkpoint_id", "") or ""
).strip()
checkpoint_type = str(
getattr(runtime_checkpoint, "checkpoint_type", "") or ""
).strip()
checkpoint_status = str(
getattr(runtime_checkpoint, "status", "") or ""
).strip().lower()
if (
not explicit_runtime_id
or explicit_runtime_id != checkpoint_id
or explicit_runtime_type != checkpoint_type
):
raise RuntimeError(
"Company runtime checkpoint identity mismatch; refresh before continuing."
)
if checkpoint_status != "pending":
raise RuntimeError(
f"Company runtime checkpoint is {checkpoint_status or 'not pending'}."
)
execution_session_id = runtime_identity.runtime_session_id
execution_origin_task_id = runtime_identity.ui_anchor_task_id or None
handoff_identity = runtime_identity
elif effective_metadata is None and runtime_checkpoint is not None:
checkpoint_status = str(
getattr(runtime_checkpoint, "status", "") or ""
).strip().lower()
if checkpoint_status != "pending":
raise RuntimeError(
f"Company runtime checkpoint is {checkpoint_status or 'not pending'}."
)
if runtime_identity is None:
raise RuntimeError(
"Company runtime checkpoint does not match the current session."
)
effective_metadata = {
"response_to_checkpoint_id": str(getattr(runtime_checkpoint, "checkpoint_id", "") or ""),
"response_to_checkpoint_type": str(getattr(runtime_checkpoint, "checkpoint_type", "") or "company_runtime_suspended"),
}
execution_session_id = runtime_identity.runtime_session_id
execution_origin_task_id = runtime_identity.ui_anchor_task_id or None
handoff_identity = runtime_identity
elif effective_metadata is None:
if runtime_identity is not None and runtime_identity.pending_checkpoint_id:
# An active checkpoint may only be pending or resuming. Never
# let a malformed identity/status fall through as a new turn.
raise RuntimeError(
"Company runtime checkpoint is not available for a new turn."
)
if state.mode == "company":
effective_metadata = {"company_preflight": "manual"}
if handoff_identity is not None:
runtime_execution_identity = await _company_runtime_execution_identity(
state,
handoff_identity,
)
execution_mode = runtime_execution_identity.exec_mode
execution_company_profile = runtime_execution_identity.company_profile
execution_org_id = runtime_execution_identity.org_id
execution_preferred_agent = runtime_execution_identity.preferred_agent
response = await state.engine.process_message(
user_input,
project_id=getattr(state.engine, "project_id", None),
session_id=execution_session_id,
mode=execution_mode,
org_id=execution_org_id or None,
company_profile=(
execution_company_profile
if execution_mode == "company"
else None
),
preferred_agent=execution_preferred_agent,
domains=list(state.domains),
origin_task_id=execution_origin_task_id,
message_metadata=effective_metadata,
)
await state.runtime_display.flush()
await _sync_runtime_checkpoint_hint(state)
handled_staffing_preflight = False
if state.mode == "company":
if interactive_followups:
handled_staffing_preflight = await _maybe_run_company_staffing_preflight(state)
else:
checkpoint = await _latest_company_staffing_checkpoint(state)
if checkpoint is not None:
key = _staffing_checkpoint_key(checkpoint)
draft = state.company_staffing_drafts.setdefault(
key,
_company_staffing_default_draft(_company_staffing_payload(checkpoint)),
)
_render_company_staffing_summary(state, checkpoint, draft)
console.print("[dim]Type a/e/c/r/d at opc>; /staffing is only a fallback reopen command.[/dim]")
handled_staffing_preflight = True
if hasattr(state.runtime_display, "render_status") and not handled_staffing_preflight:
await state.runtime_display.render_status(force=True)
if not state.runtime_display.has_streamed_content and not handled_staffing_preflight:
_print_response(response, state.no_markdown)
if effective_metadata and (
effective_metadata.get("ui_force_resume")
or str(effective_metadata.get("response_to_checkpoint_type", "") or "") in {
"company_runtime_suspended",
"company_runtime_interrupted",
}
):
remaining_checkpoint = await _latest_company_suspend_checkpoint(state)
if remaining_checkpoint is not None:
state.runtime_control_state = "suspended"
state.runtime_control_checkpoint_id = str(
getattr(remaining_checkpoint, "checkpoint_id", "") or ""
)
else:
state.runtime_control_state = "idle"
state.runtime_control_checkpoint_id = ""
except KeyboardInterrupt:
console.print("\n[warning]Interrupted. Type /quit to exit.[/warning]")
except Exception as e:
console.print(f"[error]Error: {escape(str(e))}[/error]")
async def _interactive_mode(
config,
project: str | None,
no_markdown: bool,
*,
mode: str = "task",
preferred_agent: str | None = None,
company_profile: str | None = None,
explicit_mode: bool = False,
explicit_agent: bool = False,
explicit_company_profile: bool = False,
) -> None:
engine, runtime_display = _create_cli_engine(config, project)
try:
await engine.initialize()
except Exception as e:
console.print(f"[error]Failed to initialize: {escape(str(e))}[/error]")
return
state = _InteractiveChatState(
config=config,
engine=engine,
runtime_display=runtime_display,
session_id="",
no_markdown=no_markdown,
mode="company" if str(mode or "").strip().lower() == "company" else "task",
company_profile=company_profile or _initial_company_profile(config),
preferred_agent=_normalize_interactive_preferred_agent(preferred_agent),
)
_attach_cli_runtime_callbacks(state)
await _restore_chat_context(
state,
restore_mode=not explicit_mode,
restore_company_profile=not explicit_company_profile,
restore_agent=not explicit_agent,
)
console.print(Panel(
f"[bold]OPC v{__version__}[/bold] — One-Person Company\n"
f"Model: {config.llm.default_model}\n"
f"Current mode: {state.mode}"
f"{f' ({state.company_profile})' if state.mode == 'company' else ''}\n"
f"Current agent: {state.preferred_agent or 'system'}\n"
f"Choose a project and session to begin.",
border_style="blue",
))
try:
await _run_interactive_startup_selector(state, explicit_project=bool(project))
except KeyboardInterrupt:
console.print("\n[warning]Interrupted.[/warning]")
await state.engine.shutdown()
return
controller = ChatTurnController(state)
try:
from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.styles import Style
history_path = state.engine.opc_home / "cli_history"
session = PromptSession(
history=FileHistory(str(history_path)),
completer=_OPCSlashCompleter(),
complete_while_typing=True,
bottom_toolbar=lambda: _chat_bottom_toolbar_text(state, controller),
style=Style.from_dict({"bottom-toolbar": "fg:#888888"}),
)
while True:
await _print_pending_checkpoint_hint(state.engine, state.session_id)
try:
with patch_stdout(raw=True):
user_input = await session.prompt_async("opc> ")
except (EOFError, KeyboardInterrupt):
break
user_input = user_input.strip()
if not user_input:
continue
if user_input in ("/quit", "/exit", "exit", "quit"):
break
if user_input.startswith("/"):
await _handle_chat_slash_command(state, user_input, controller=controller)
continue
if await _handle_company_staffing_shortcut(state, user_input, controller):
continue
await controller.submit_user_message(user_input)
continue
except ImportError:
console.print("[warning]prompt-toolkit not available, using basic input[/warning]")
while True:
await _print_pending_checkpoint_hint(state.engine, state.session_id)
try:
user_input = await asyncio.get_event_loop().run_in_executor(
None, lambda: input("opc> ")
)
user_input = user_input.strip()
except (EOFError, KeyboardInterrupt):
break
if user_input in ("/quit", "/exit", "exit", "quit"):
break
if not user_input:
continue
if user_input.startswith("/"):
await _handle_chat_slash_command(state, user_input, controller=controller)
continue
if await _handle_company_staffing_shortcut(state, user_input, controller):
continue
await controller.submit_user_message(user_input)
continue
finally:
await controller.shutdown()
await state.engine.shutdown()
console.print("[info]Goodbye![/info]")
async def _interactive_secretary_mode(config, project: str | None, no_markdown: bool) -> None:
engine, runtime_display = _create_cli_engine(config, project)
try:
await engine.initialize()
except Exception as e:
console.print(f"[error]Failed to initialize: {escape(str(e))}[/error]")
return
console.print(Panel(
f"[bold]OPC Secretary[/bold]\n"
f"Model: {config.llm.default_model}\n"
f"Project: {project or '(none)'}\n"
f"Commands: /quit, /project <id>, /new-session, /sessions, /resume <session_id>, /policies, /help",
border_style="green",
))
try:
from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory
history_path = engine.opc_home / "secretary_cli_history"
prompt = PromptSession(history=FileHistory(str(history_path)))
current_session_id, restored_latest = await _resolve_secretary_session_id(engine)
if restored_latest:
console.print(f"[info]Restored recent secretary session: {current_session_id}[/info]")
else:
console.print(f"[info]Started a new secretary session: {current_session_id}[/info]")
while True:
try:
user_input = await asyncio.get_event_loop().run_in_executor(
None, lambda: prompt.prompt("secretary> ")
)
except (EOFError, KeyboardInterrupt):
break
user_input = user_input.strip()
if not user_input:
continue
if user_input in ("/quit", "/exit", "exit", "quit"):
break
if user_input.startswith("/project "):
new_project = user_input.split(" ", 1)[1].strip()
await engine.shutdown()
engine, runtime_display = _create_cli_engine(config, new_project)
await engine.initialize()
current_session_id, restored_latest = await _resolve_secretary_session_id(engine)
console.print(f"[info]Switched to project: {new_project}[/info]")
if restored_latest:
console.print(f"[info]Restored recent secretary session: {current_session_id}[/info]")
else:
console.print(f"[info]Started a new secretary session: {current_session_id}[/info]")
continue
if user_input == "/new-session":
current_session_id = str(uuid.uuid4())
console.print(f"[info]Started a new secretary session: {current_session_id}[/info]")
continue
if user_input == "/sessions":
sessions = await engine.secretary.list_sessions(engine.project_id, limit=20) if engine.secretary else []
if not sessions:
console.print("[info]No secretary sessions found for this project.[/info]")
else:
console.print("[bold]Recent secretary sessions:[/bold]")
for item in sessions:
title = item.title or "(untitled)"
console.print(f" - {item.session_id} :: {title}")
continue
if user_input.startswith("/resume "):
requested_session_id = user_input.split(" ", 1)[1].strip()
target = await engine.store.get_session(requested_session_id) if engine.store else None
if not target or target.metadata.get("interface") != "secretary":
console.print(f"[warning]Secretary session not found: {requested_session_id}[/warning]")
continue
current_project = engine.project_id or "default"
target_project = target.project_id or "default"
if target_project != current_project:
console.print(
"[warning]"
f"Secretary session {requested_session_id} belongs to project '{target_project}'. "
f"Switch project first with /project {target_project}."
"[/warning]"
)
continue
current_session_id = requested_session_id
console.print(f"[info]Resumed secretary session: {current_session_id}[/info]")
continue
if user_input == "/policies":
summary = engine.secretary.describe_policies(engine.project_id) if engine.secretary else ""
_print_response(summary, no_markdown)
continue
if user_input == "/help":
console.print(
"/quit — Exit\n"
"/project <id> — Switch project\n"
"/new-session — Start a fresh secretary session\n"
"/sessions — List secretary sessions for this project\n"
"/resume <session_id> — Resume an existing secretary session\n"
"/policies — Show active secretary policies\n"
"/help — This help"
)
continue
runtime_display.begin_turn()
payload = await engine.process_secretary_message(
user_input,
project_id=engine.project_id,
session_id=current_session_id,
)
await runtime_display.flush()
if not runtime_display.has_streamed_content:
_print_response(payload.get("response", ""), no_markdown)
except ImportError:
console.print("[warning]prompt-toolkit not available, using basic input[/warning]")
finally:
await engine.shutdown()
console.print("[info]Goodbye![/info]")
async def _show_cost_summary(config) -> None:
from opc.core.config import get_opc_home
db_path = get_opc_home() / "global.db"
if not db_path.exists():
return
store = OPCStore(db_path)
await store.initialize()
try:
costs = await store.get_total_cost()
if costs["total_calls"] > 0:
console.print(f"\n[bold]Cost Summary:[/bold]")
console.print(f" Total calls: {costs['total_calls']}")
console.print(f" Total tokens: {costs['total_tokens_in'] + costs['total_tokens_out']}")
console.print(f" Total cost: ${costs['total_cost']:.4f}")
finally:
await store.close()
async def _show_autonomy_summary(config, project: str | None = None) -> None:
from opc.core.config import get_opc_home
from opc.layer5_memory.approval_allowlist import ApprovalAllowlistManager
from opc.layer5_memory.preference import PreferenceManager
db_path = get_opc_home() / "global.db"
if not db_path.exists():
return
store = OPCStore(db_path)
await store.initialize()
try:
stats = await store.get_autonomy_stats(project_id=project)
console.print(f"\n[bold]Autonomy Summary:[/bold]")
console.print(f" Decisions: {stats['total']}")
console.print(f" Auto-approved: {stats['auto_approved']}")
console.print(f" Escalated: {stats['escalated']}")
console.print(f" Rejected: {stats['rejected']}")
console.print(f" Auto-approval rate: {stats['auto_approval_rate']:.0%}")
prefs = PreferenceManager(get_opc_home())
learned = prefs.get_autonomy_preferences(project_id=project).get("learned_actions", {})
if learned:
console.print(" Learned actions:")
for name, data in list(learned.items())[:10]:
console.print(
f" - {name}: approvals={data.get('approvals', 0)}, "
f"rejections={data.get('rejections', 0)}, "
f"explicit_allow={data.get('explicit_allow', False)}, "
f"explicit_deny={data.get('explicit_deny', False)}"
)
allowlist_lines = ApprovalAllowlistManager(get_opc_home()).summarize(project_id=project, limit=10)
if allowlist_lines:
console.print(" Persisted allowlist:")
for line in allowlist_lines:
console.print(f" {line}")
finally:
await store.close()
def _print_response(text: str, no_markdown: bool = False) -> None:
if no_markdown:
console.print(text)
else:
try:
md = Markdown(text)
console.print(md)
except Exception:
console.print(text)
console.print()
async def _print_pending_checkpoint_hint(engine, session_id: str) -> None:
checkpoint = await engine.get_latest_pending_checkpoint_for_session(session_id)
if not checkpoint:
return
if checkpoint.checkpoint_type == "company_recruitment_confirmation":
console.print(
"[info]Pending staffing confirmation for this session. "
"Reply `1` or `approve` to continue, `2` or `deny` to cancel. "
"Any other input will be treated as feedback/suggestions for revising the proposal.[/info]"
)
return
if checkpoint.checkpoint_type == "company_staffing_selection":
payload = dict(getattr(checkpoint, "payload", {}) or {})
recommended = str(payload.get("recommended_action", "") or "").strip()
if recommended == "auto_recruit":
console.print(
"[info]Pending company staffing preflight for this session. "
"`auto recruit` is recommended; use `/staffing` to review/edit employee, template, and agent choices.[/info]"
)
else:
console.print(
"[info]Pending manual staffing selection for this session. "
"Use `/staffing` to review/edit employee and agent choices, reply `approve` to use defaults, "
"or `auto` / `auto recruit` to run automatic recruitment.[/info]"
)
return
if checkpoint.checkpoint_type == "company_work_item_gate":
console.print(
"[info]Pending work-item confirmation for this session. "
"Reply with `approve` / `continue` or `deny` / `stop`.[/info]"
)
return
if checkpoint.checkpoint_type == "company_delivery_feedback":
console.print(
"[info]Pending delivery self-evolution review for this session. "
"Reply with `approve` to record agreement, or explicit feedback to update employee experience. "
"Start normal follow-up work from the regular conversation after this review is handled.[/info]"
)
return
if checkpoint.checkpoint_type in {"task_user_input", "task_peer_wait", "company_peer_wait"}:
console.print("[info]Pending checkpoint for this session. Your next reply will resume that confirmation flow.[/info]")
def _print_interactive_status(engine, state: _InteractiveChatState | None = None) -> None:
if state is not None:
_print_context_status(state)
if getattr(engine, "llm", None):
stats = getattr(engine.llm, "stats", {}) or {}
model = getattr(getattr(getattr(engine, "config", None), "llm", None), "default_model", "unknown")
console.print(f" Model: {model}")
console.print(f" Project: {_current_project_id(engine)}")
console.print(f" Tokens: in={stats.get('tokens_in', 0)}, out={stats.get('tokens_out', 0)}")
console.print(f" Cost: ${float(stats.get('estimated_cost', 0.0) or 0.0):.4f}")
if getattr(engine, "adapter_registry", None):
available = engine.adapter_registry.list_available()
console.print(f" External agents: {', '.join(available) if available else 'none'}")
async def _load_recent_primary_session(store: OPCStore | None, project_id: str | None) -> tuple[str | None, str]:
sessions = await _load_recent_primary_sessions(store, project_id, limit=1)
if not sessions:
return None, ""
latest = sessions[0]
return latest.session_id, (latest.title or "").strip()
async def _resolve_starting_session_id(engine) -> tuple[str, bool]:
session_id, _ = await _load_recent_primary_session(engine.store, engine.project_id)
if session_id:
return session_id, True
return str(uuid.uuid4()), False
async def _load_recent_secretary_session(engine) -> tuple[str | None, str]:
if not engine.secretary:
return None, ""
sessions = await engine.secretary.list_sessions(engine.project_id, limit=1)
if not sessions:
return None, ""
latest = sessions[0]
return latest.session_id, (latest.title or "").strip()
async def _resolve_secretary_session_id(engine) -> tuple[str, bool]:
session_id, _ = await _load_recent_secretary_session(engine)
if session_id:
return session_id, True
return str(uuid.uuid4()), False
async def _run_channel_runtime(config, project: str | None) -> None:
engine, _runtime_display = _create_cli_engine(config, project)
try:
await engine.initialize()
if not engine.channel_manager:
console.print("[warning]Channel manager is not available.[/warning]")
return
await engine.channel_manager.start_all()
if not engine.channel_manager.enabled_channels:
console.print("[warning]No channels enabled. Update .opc/config/channel_config.yaml first.[/warning]")
return
_write_channel_runtime_state(engine.channel_manager.enabled_channels)
console.print(f"[success]Channel runtime started:[/success] {', '.join(engine.channel_manager.enabled_channels)}")
await engine.message_bus.start()
except KeyboardInterrupt:
console.print("\n[warning]Stopping channel runtime...[/warning]")
finally:
_clear_channel_runtime_state()
await engine.shutdown()
# ── Plugins ────────────────────────────────────────────────────────────────
try:
from opc.plugins.office_ui import register_cli
register_cli(app)
except ImportError:
pass
try:
from opc.plugins.cli_board import register_cli as register_cli_board
register_cli_board(app)
except ImportError:
pass
def main():
keylog_path = pop_windows_sslkeylogfile()
if keylog_path:
console.print(
f"[warning]{escape(format_windows_sslkeylog_warning(_current_command_label(), keylog_path))}[/warning]"
)
app()
if __name__ == "__main__":
main()