Deterministic, non-LLM CLI for Flow B's SQLite-backed research loop. Operates directly on research_workspace/research.db via DatabaseManager / MissionController / TaskQueue / ScopeGate / RiskController / ExecutorAgent without the autonomous AgentLoop. Intended for headless/CI, manual triage, and replay of individual tasks.
Flow B file (per AGENTS.md §2, do not add new features here — it is frozen as legacy namespace).
Resolve workspace root from RESEARCH_WORKSPACE env or research_workspace/ (cli.py:42 _workspace_root).
Open the research DB (cli.py:46 _load_db → DatabaseManager(workspace/research.db)).
Resolve the active mission: load by --mission-id if given (status active or paused), else latest active (cli.py:51 _load_mission, cli.py:74 _require_mission).
Implement 11 subcommands (see Public Interfaces) that read/write the same schema AgentLoop uses, preserving atomicity and scope/risk gates.
All reads/writes go through DatabaseManager against research_workspace/research.db (single DB, many missions).
research_workspace/<mission_id>/ dirs (evidence/raw_output, http_responses, screenshots, notes, artifacts, reports, logs, tasks) created by MissionController._init_workspace.
run-task writes: tasks.status, observations, memories, graph_nodes/edges, findings, audit_logs, evidence rows + files.
Workspace root from RESEARCH_WORKSPACE env; default research_workspace/.
Mission risk profile, budgets (max_commands_per_session, max_tasks_active), and scope rules come from the persisted missions row and scope_rules table, not config.yaml (Flow B uses mission.yaml only at creation).
No config.yaml keys are read here.
db.DatabaseManager — connection, schema, add_scope_rule, get_scope_rules, log_audit
mission.Mission (from_dict, _classify_asset), mission.MissionController
scope_gate.ScopeGate — load_from_db, check_scope, list_scope
risk_controller.RiskController — assess_action
tool_router.ToolRouter — only in cmd_run_task via a stub executor (lambda name,args: "[tool] ...")
executor.ExecutorAgent, observer.ObserverAgent, summarizer.summarize_observation
evidence.EvidenceStore, memory.MemoryManager, target_graph.TargetGraph, finding_verifier.FindingVerifier, report_generator.ReportGenerator, task_queue.TaskQueue
PyYAML
Operator directly: python cli.py <command> [--mission-id M-...]
Tests exercising Flow B deterministically without LLM.
Not used by Flow A (main.py/app.py).
flowchart TD
A[main -> build_parser -> parse_args] --> B{has func?}
B -->|no| C[print_help -> 1]
B -->|yes| D[args.func]
D --> E[_require_mission -> _load_db + _load_mission]
E --> F{mission?}
F -->|no| G[return 1 after error]
F -->|yes| H[specific cmd]
H --> I{run-task?}
I -->|yes| J[queue.get_task/get_next_task -> ScopeGate.check_scope -> RiskController.assess_action -> human_approval gate -> ExecutorAgent.execute -> ObserverAgent.observe -> queue.complete/failed]
I -->|no| K[read-only render]
Copy
run-task scope/risk/approval flow mirrors agent_loop.py:607-642 and cli.py:293-341:
ScopeGate.check_scope(asset=target, action_type=phase, tool=first_allowed, risk_level)
RiskController.assess_action(phase, first_tool, json(task)[:300], target, risk_level)
If either sets requires_human_approval, mark needs_approval and bail (H16).
_require_mission is the single chokepoint for mission resolution; all 10 mission-operating commands use it.
init-mission never takes --mission-id (it mints a new M-...).
list-scope's ScopeGate is constructed from the DB mission's allowed_assets/disallowed_assets/forbidden_actions/risk_profile — not from CLI args.
run-task never executes without going through ScopeGate then RiskController.
Every run-task execution is scope-gated (ScopeGate) and risk-gated (RiskController).
ScopeGate enforces: deny-rules first, allow-rules required, forbidden-action exact + substring hard-blocks, third-party flag, sliding-window rate limit (_RateBucket), and high-risk human-approval flag.
RiskController enforces: task/command budgets, action-category permission (exploit/pivot/credential), destructive-pattern deny, dangerous-tool deny, high-risk profile gate.
Run: python -m pytest tests/test_cli_mission_id.py tests/test_agent_loop.py -v
A subcommand is added, removed, or changes its arguments/DB writes.
--mission-id handling or active-mission selection changes.
run-task pipeline (gates, executor/observer wiring) is altered.
docs/architecture.md §Entry Points — cli.py commands
docs/runtime-flows.md §Database-Backed Research Loop
agent_loop.py (docs/components/flow-b/agent-loop.md) — autonomous version of the same loop
docs/database-mission.md — DB layout + mission persistence