Files
openworker/coworker/cli.py
T
Devika Verma c958d6f262 Step 2: Auto-Approve mode - the reviewer, the hook, and the renames
The mode from ocw-context/docs/reviewed-auto-mode.md (rev. 4), v1 scope.

coworker/reviewer.py (new)
- The 8.3 prompt verbatim, cache-shaped: instructions + known world (folders
  and remotes only) + user-message history in the stable prefix; this turn's
  request and ONE action in the suffix.
- parse_verdict: any defect (empty, non-JSON, unknown verdict) -> unsure.
  There is no parse path that results in execution (8.5).
- Reviewer.review never raises: provider errors and timeouts -> unsure.
  Metering counters (checks / verdicts / tokens) for 1.7.
- AGENT_DENY_MESSAGE: the terse, non-diagnostic refusal the agent gets on a
  deny; the full reason goes to the user only (8.4 asymmetry).

coworker/engine.py
- Reviewer consulted ONLY when: attached, mode is AUTO_APPROVE, session
  explicitly attended (unset is_attended counts as NOT attended, so
  automations can never be reviewed), fewer than two denials this turn.
- Consulted ONLY on decisions the gate marked needs_user - hard denies
  never reach it, so it can only turn "ask" into "allow" (1.2).
- One action per request, fired concurrently for all of a turn's escalating
  calls before the sequential authorize loop (8.6): a verdict cannot land
  on the wrong action, and approval cards still reach the human one at a
  time in call order.
- allow -> runs, audited with the reason. deny -> blocked; user event
  carries the full reviewer reason + allow_anyway; agent message carries
  only AGENT_DENY_MESSAGE. unsure -> today's card.
- Reviewer sees the user's words only, extracted mechanically from
  role=user messages - never agent output, never tool results (4.4).

coworker/permissions.py
- Mode.AUTO renamed Mode.BYPASS_APPROVALS ("bypass-approvals"); legacy
  "auto" still parses via _missing_ so configs, saved sessions, and the
  golden decision table are untouched.
- Mode.AUTO_APPROVE ("auto-approve"): gate-identical to INTERACTIVE except
  session grants ("always allow this ...") no longer auto-allow - they
  route to the reviewer instead (1.5: out-of-band standing policy may skip
  the judge; an in-flow click may not). Config allowlists still skip.
- _domain_allowed(include_session=False) checks the user-settings list only.

coworker/config.py: auto_approve flag, off by default, _GLOBAL_ONLY (a
cloned repo cannot hand itself a looser reviewer). agent.py attaches the
Reviewer only when the flag is on; without it AUTO_APPROVE behaves exactly
like INTERACTIVE.

server/manager.py: autonomy audit ranks auto-approve above interactive
(turning the reviewer on IS raising autonomy) and below bypass.

GUI: mode picker label "Full access" -> "Bypass approvals" (wire value
"auto" kept). Verified live against the real sidecar; e2e spec updated;
tsc and all 111 GUI unit tests pass.

Tests: tests/test_auto_approve.py (33) - gate behaviour per mode, fail-
closed parsing, prompt shape, deny asymmetry, retry guard, attended
gating, hard-deny isolation, per-action verdict landing, and that the
reviewer never sees agent prose. Permission suites + golden table: 146
passing unchanged.
2026-08-12 12:42:17 -07:00

76 lines
2.5 KiB
Python

"""CLI entry point. `coworker` launches the TUI; `coworker code` boots the code skill."""
from __future__ import annotations
import argparse
import os
import uuid
from pathlib import Path
from typing import Optional
from .config import load_config
from .conversations import ConversationStore
from .memory import MemorySettingsStore, SQLiteMemoryStore
from .permissions import Mode
from .secrets import state_dir
def main(argv: Optional[list[str]] = None) -> None:
cfg = load_config()
parser = argparse.ArgumentParser(
prog="openworker", description="Agent coworker (TUI)."
)
parser.add_argument(
"skill", nargs="?", default="code", help="skill to launch (default: code)"
)
parser.add_argument("--cwd", default=".", help="workspace directory")
parser.add_argument(
"--model", default=cfg.model, help="model id, e.g. openai gpt-5.5"
)
parser.add_argument(
"--mode",
default=cfg.mode,
choices=["plan", "interactive", "auto", "bypass-approvals", "auto-approve"],
help="permission mode",
)
parser.add_argument("--resume", default=None, help="resume a session id")
args = parser.parse_args(argv)
workspace = Path(args.cwd).expanduser().resolve()
# Unified global store shared with the GUI/server (one place for all conversations).
data_dir = state_dir()
# Same on/off switch and user rules the GUI manages (MEMORY-SPEC §4.3/§6). The
# store is always wired: off means "stop learning", so saved facts stay usable.
memory_settings = MemorySettingsStore(data_dir / "memory-settings.json")
memory_store = SQLiteMemoryStore(data_dir / "coworker.db")
session_store = ConversationStore(data_dir)
session_store.touch_workspace(os.path.realpath(str(workspace)))
resume_messages = None
session_id = args.resume or uuid.uuid4().hex[:12]
model, mode = args.model, args.mode
if args.resume:
record = session_store.load(args.resume)
if record is not None:
resume_messages = record.messages
model, mode = record.model, record.mode
from .tui.app import CoworkerApp
app = CoworkerApp(
workspace=workspace,
model=model,
mode=Mode(mode),
memory_store=memory_store,
memory_off=not memory_settings.enabled,
user_rules=memory_settings.user_rules,
session_store=session_store,
session_id=session_id,
resume_messages=resume_messages,
)
app.run()
if __name__ == "__main__":
main()