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.
File Lines Role cli.py599 Arg parsing, 11 subcommands, DB + mission helpers
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.
Function Location Signature Description _workspace_rootcli.py:42() -> PathEnv-aware workspace root _load_dbcli.py:46() -> DatabaseManagerOpens research.db _load_missioncli.py:51(db, mission_id?) -> dict|NoneSELECT missions by id or latest active _require_missioncli.py:74(args) -> (db, mission)Loads DB + mission; prints context-accurate error if missing _get_mission_ctrlcli.py:92(db) -> MissionControllerFactory
Command Function Location Description init-missioncmd_init_missioncli.py:100yaml.safe_load → MissionController.create_from_config → prints mission summaryadd-scopecmd_add_scopecli.py:137Insert allow/deny scope rule (_classify_asset + db.add_scope_rule) list-scopecmd_list_scopecli.py:165ScopeGate.list_scope() render of allow/deny + forbidden actionsnext-taskcmd_next_taskcli.py:205TaskQueue.get_next_task() — highest-priority pendinglist-taskscmd_list_taskscli.py:231list_open_tasks + list_blocked_tasks + count_by_statusrun-taskcmd_run_taskcli.py:264Scope→risk→human-approval gates → ExecutorAgent → queue.complete_task/failed → ObserverAgent + summarize_observation summarize-targetcmd_summarize_targetcli.py:378MemoryManager.summarize_target + TargetGraph.summarize_graphlist-findingscmd_list_findingscli.py:400FindingVerifier.list_all()validate-findingcmd_validate_findingcli.py:424FindingVerifier.validate_finding with ScopeGate + EvidenceStoregenerate-reportcmd_generate_reportcli.py:449ReportGenerator.generate_reportstatuscmd_statuscli.py:469TaskQueue.count_by_status + FindingVerifier.list_all rollup
Function Location Signature build_parsercli.py:504() -> ArgumentParser (shared --mission-id parent parser for 10 subcommands; init-mission has --config required)maincli.py:578(argv=None) -> int (dispatches args.func, handles KeyboardInterrupt/traceback)
Input Notes mission.yamlYAML config for init-mission (yaml.safe_load) --mission-id M-...Optional on 10 commands; selects specific mission (resume/attach) --allow/--deny + --notesadd-scope patterntask_id positionalrun-task [task_id]; empty means next pendingfinding_id positionalvalidate-finding / generate-report
Output Notes stdout text Human-readable summaries, task/finding listings, reports Exit code 0 success, 1 error, 130 on KeyboardInterrupt DB mutations research.db + workspace dirsEvidence files research_workspace/<mission_id>/evidence/ via EvidenceStore
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 Copy code block
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).
Failure Handling No active mission ERROR: No active mission found. Run init-mission first. → 1--mission-id not foundERROR: No mission with id ... → 1Bad mission.yaml yaml.safe_load may throw; Mission.validate raises ValueError → prints and 1run-task scope blockedqueue.block_task(task_id, reason) → 1run-task risk blockedqueue.block_task → 1run-task needs human approvalqueue.update_task_status(needs_approval) → 1 (stub executor has no handler)Unknown task/finding id ERROR: Task ... not found. → 1
_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.
Test file Covers tests/test_cli_mission_id.py--mission-id parent parser, resume by id vs latest active, init-mission exclusiontests/test_mission.pyMission.validate, _classify_asset, normalization, controller creationtests/test_task_queue.pycreate_task/get_next_task/block/reset_stale used by next-task/list-tasks/run-tasktests/test_agent_loop.pyLoop-parity: task lifecycle + scope/risk gates
Run: python -m pytest tests/test_cli_mission_id.py tests/test_agent_loop.py -v
Change Where Add a subcommand cli.py:504 build_parser subparser + new cmd_* function + wire set_defaults(func=...)Change mission resolution cli.py:51 _load_mission / cli.py:74 _require_missionAdjust scope rendering cli.py:165 cmd_list_scopeExtend run-task pipeline cli.py:264 cmd_run_task (keep scope→risk→approval ordering)
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