terminal-agent-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. terminal_agent/__init__.py +7 -0
  2. terminal_agent/__main__.py +7 -0
  3. terminal_agent/agent/__init__.py +9 -0
  4. terminal_agent/agent/loop.py +338 -0
  5. terminal_agent/checkpoints/__init__.py +9 -0
  6. terminal_agent/checkpoints/manager.py +151 -0
  7. terminal_agent/cli/commands/__init__.py +26 -0
  8. terminal_agent/cli/commands/checkpoint.py +76 -0
  9. terminal_agent/cli/commands/clean.py +170 -0
  10. terminal_agent/cli/commands/config_cmd.py +32 -0
  11. terminal_agent/cli/commands/diff.py +45 -0
  12. terminal_agent/cli/commands/doctor.py +136 -0
  13. terminal_agent/cli/commands/resume.py +109 -0
  14. terminal_agent/cli/commands/run.py +184 -0
  15. terminal_agent/cli/commands/setup.py +229 -0
  16. terminal_agent/cli/commands/status.py +62 -0
  17. terminal_agent/cli/commands/test.py +38 -0
  18. terminal_agent/cli/commands/trace.py +60 -0
  19. terminal_agent/cli/main.py +94 -0
  20. terminal_agent/cli/theme.py +55 -0
  21. terminal_agent/cli/ui.py +147 -0
  22. terminal_agent/config/__init__.py +30 -0
  23. terminal_agent/config/schema.py +100 -0
  24. terminal_agent/config/settings.py +85 -0
  25. terminal_agent/context/__init__.py +11 -0
  26. terminal_agent/context/engine.py +173 -0
  27. terminal_agent/context/ranker.py +87 -0
  28. terminal_agent/git/__init__.py +10 -0
  29. terminal_agent/git/adapter.py +219 -0
  30. terminal_agent/planner/__init__.py +10 -0
  31. terminal_agent/planner/contract.py +58 -0
  32. terminal_agent/planner/planner.py +30 -0
  33. terminal_agent/providers/__init__.py +30 -0
  34. terminal_agent/providers/anthropic_provider.py +141 -0
  35. terminal_agent/providers/base.py +57 -0
  36. terminal_agent/providers/detector.py +397 -0
  37. terminal_agent/providers/factory.py +29 -0
  38. terminal_agent/providers/gemini_provider.py +130 -0
  39. terminal_agent/providers/mock_provider.py +80 -0
  40. terminal_agent/providers/ollama_provider.py +124 -0
  41. terminal_agent/providers/openai_provider.py +124 -0
  42. terminal_agent/recovery/__init__.py +10 -0
  43. terminal_agent/recovery/classifier.py +64 -0
  44. terminal_agent/recovery/strategies.py +62 -0
  45. terminal_agent/sandbox/__init__.py +13 -0
  46. terminal_agent/sandbox/base.py +41 -0
  47. terminal_agent/sandbox/docker.py +129 -0
  48. terminal_agent/sandbox/local.py +129 -0
  49. terminal_agent/security/__init__.py +13 -0
  50. terminal_agent/security/classifier.py +151 -0
  51. terminal_agent/security/policy.py +51 -0
  52. terminal_agent/security/secrets.py +105 -0
  53. terminal_agent/session/__init__.py +33 -0
  54. terminal_agent/session/manager.py +195 -0
  55. terminal_agent/session/models.py +160 -0
  56. terminal_agent/telemetry/__init__.py +12 -0
  57. terminal_agent/telemetry/events.py +87 -0
  58. terminal_agent/telemetry/metrics.py +38 -0
  59. terminal_agent/tools/__init__.py +33 -0
  60. terminal_agent/tools/base.py +59 -0
  61. terminal_agent/tools/checkpoint_tools.py +72 -0
  62. terminal_agent/tools/command_tools.py +125 -0
  63. terminal_agent/tools/file_tools.py +340 -0
  64. terminal_agent/tools/git_tools.py +124 -0
  65. terminal_agent/tools/registry.py +109 -0
  66. terminal_agent/verifier/__init__.py +13 -0
  67. terminal_agent/verifier/assertions.py +115 -0
  68. terminal_agent/verifier/engine.py +125 -0
  69. terminal_agent/verifier/runners.py +80 -0
  70. terminal_agent_cli-0.1.0.dist-info/METADATA +438 -0
  71. terminal_agent_cli-0.1.0.dist-info/RECORD +74 -0
  72. terminal_agent_cli-0.1.0.dist-info/WHEEL +4 -0
  73. terminal_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
  74. terminal_agent_cli-0.1.0.dist-info/licenses/LICENSE +23 -0
@@ -0,0 +1,170 @@
1
+ """CLI clean command to prune old sessions, checkpoints, and telemetry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ from datetime import datetime, timedelta, timezone
8
+ from pathlib import Path
9
+ from typing import List, Optional, Tuple
10
+
11
+ import typer
12
+ from rich.table import Table
13
+
14
+ from terminal_agent.checkpoints.manager import CheckpointManager
15
+ from terminal_agent.cli.theme import console
16
+ from terminal_agent.cli.ui import render_header
17
+ from terminal_agent.session.manager import SessionManager
18
+
19
+
20
+ def _dir_size_bytes(path: Path) -> int:
21
+ total = 0
22
+ if not path.exists():
23
+ return 0
24
+ if path.is_file():
25
+ return path.stat().st_size
26
+ for root, _, files in os.walk(path):
27
+ for name in files:
28
+ fp = Path(root) / name
29
+ try:
30
+ total += fp.stat().st_size
31
+ except OSError:
32
+ continue
33
+ return total
34
+
35
+
36
+ def _format_mb(num_bytes: int) -> str:
37
+ return f"{num_bytes / (1024 * 1024):.2f} MB"
38
+
39
+
40
+ def _is_older_than(path: Path, cutoff_ts: Optional[float]) -> bool:
41
+ if cutoff_ts is None:
42
+ return True
43
+ try:
44
+ return path.stat().st_mtime < cutoff_ts
45
+ except OSError:
46
+ return False
47
+
48
+
49
+ def collect_clean_targets(
50
+ working_dir: Path,
51
+ days: Optional[int],
52
+ purge_all: bool,
53
+ ) -> List[Tuple[str, Path, int]]:
54
+ """Return (kind, path, size_bytes) items that would be removed."""
55
+ cutoff = None
56
+ if not purge_all:
57
+ age = days if days is not None else 7
58
+ cutoff = (datetime.now(timezone.utc) - timedelta(days=age)).timestamp()
59
+
60
+ root = working_dir / ".terminal_agent"
61
+ targets: List[Tuple[str, Path, int]] = []
62
+
63
+ sessions_dir = root / "sessions"
64
+ if sessions_dir.exists():
65
+ for json_file in sessions_dir.glob("*.json"):
66
+ if _is_older_than(json_file, cutoff):
67
+ targets.append(("session", json_file, _dir_size_bytes(json_file)))
68
+
69
+ checkpoints_dir = root / "checkpoints"
70
+ if checkpoints_dir.exists():
71
+ for entry in checkpoints_dir.iterdir():
72
+ if entry.is_dir() and _is_older_than(entry, cutoff):
73
+ targets.append(("checkpoint", entry, _dir_size_bytes(entry)))
74
+
75
+ for kind in ("telemetry", "traces"):
76
+ tdir = root / kind
77
+ if not tdir.exists():
78
+ continue
79
+ for entry in tdir.iterdir():
80
+ if entry.is_file() and _is_older_than(entry, cutoff):
81
+ targets.append((kind.rstrip("s"), entry, _dir_size_bytes(entry)))
82
+ elif entry.is_dir() and _is_older_than(entry, cutoff):
83
+ targets.append((kind.rstrip("s"), entry, _dir_size_bytes(entry)))
84
+
85
+ return targets
86
+
87
+
88
+ def apply_deletes(working_dir: Path, days: Optional[int], purge_all: bool) -> List[Path]:
89
+ """Delete matching artifacts. Uses manager helpers for sessions/checkpoints."""
90
+ age = None if purge_all else (days if days is not None else 7)
91
+ SessionManager(working_dir).delete_sessions_older_than(age)
92
+ CheckpointManager(working_dir).prune_checkpoints(age)
93
+
94
+ deleted: List[Path] = []
95
+ for kind, path, _size in collect_clean_targets(working_dir, days=days, purge_all=purge_all):
96
+ if kind in ("session", "checkpoint"):
97
+ deleted.append(path)
98
+ continue
99
+ try:
100
+ if path.is_dir():
101
+ shutil.rmtree(path)
102
+ elif path.exists():
103
+ path.unlink()
104
+ deleted.append(path)
105
+ except OSError:
106
+ continue
107
+ return deleted
108
+
109
+
110
+ def clean_command(
111
+ all_data: bool = typer.Option(
112
+ False,
113
+ "--all",
114
+ help="Purge all sessions, checkpoints, and telemetry logs.",
115
+ ),
116
+ days: int = typer.Option(
117
+ 7,
118
+ "--days",
119
+ help="Purge items older than N days (ignored when --all is set).",
120
+ ),
121
+ dry_run: bool = typer.Option(
122
+ False,
123
+ "--dry-run",
124
+ help="Show files and disk space that would be freed without deleting.",
125
+ ),
126
+ working_dir: Optional[Path] = typer.Option(
127
+ None,
128
+ "--cwd",
129
+ "-C",
130
+ help="Target repository directory",
131
+ ),
132
+ ) -> None:
133
+ """Prune old session, checkpoint, and telemetry artifacts."""
134
+ render_header()
135
+ target_dir = (working_dir or Path.cwd()).resolve()
136
+ root = target_dir / ".terminal_agent"
137
+
138
+ if not root.exists():
139
+ console.print("[agent.muted]No .terminal_agent directory found. Nothing to clean.[/agent.muted]")
140
+ raise typer.Exit(0)
141
+
142
+ targets = collect_clean_targets(target_dir, days=days, purge_all=all_data)
143
+ total_bytes = sum(size for _, _, size in targets)
144
+
145
+ if not targets:
146
+ console.print("[agent.muted]No session, checkpoint, or telemetry files to clean.[/agent.muted]")
147
+ raise typer.Exit(0)
148
+
149
+ table = Table(title="[bold agent.accent]CLEAN TARGETS[/bold agent.accent]", border_style="agent.border")
150
+ table.add_column("Kind", style="agent.text")
151
+ table.add_column("Path", style="agent.muted")
152
+ table.add_column("Size", justify="right")
153
+ for kind, path, size in targets:
154
+ try:
155
+ rel = path.relative_to(target_dir)
156
+ except ValueError:
157
+ rel = path
158
+ table.add_row(kind, str(rel), _format_mb(size))
159
+ console.print(table)
160
+ console.print(
161
+ f"[agent.accent]{len(targets)} item(s)[/agent.accent] — "
162
+ f"[agent.success]{_format_mb(total_bytes)}[/agent.success] would be freed."
163
+ )
164
+
165
+ if dry_run:
166
+ console.print("[agent.muted]Dry run: no files were deleted.[/agent.muted]")
167
+ return
168
+
169
+ apply_deletes(target_dir, days=days, purge_all=all_data)
170
+ console.print("[agent.success]Cleanup complete. Directory structure preserved.[/agent.success]")
@@ -0,0 +1,32 @@
1
+ """CLI config command viewing and generating configuration templates."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+ import typer
6
+ import yaml
7
+ from rich.syntax import Syntax
8
+
9
+ from terminal_agent.cli.theme import SYM_CHECK, console
10
+ from terminal_agent.cli.ui import render_header
11
+ from terminal_agent.config.settings import load_config, save_default_config
12
+
13
+
14
+ def config_command(
15
+ init: bool = typer.Option(False, "--init", "-i", help="Generate default terminal-agent.config.yaml file"),
16
+ working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
17
+ ) -> None:
18
+ """View active configuration or generate a default configuration file."""
19
+ render_header()
20
+ target_dir = (working_dir or Path.cwd()).resolve()
21
+ target_file = target_dir / "terminal-agent.config.yaml"
22
+
23
+ if init:
24
+ save_default_config(target_file)
25
+ console.print(f"{SYM_CHECK} [agent.success]Created default configuration at {target_file}[/agent.success]")
26
+ return
27
+
28
+ config = load_config(target_file)
29
+ yaml_str = yaml.dump(config.model_dump(mode="json"), sort_keys=False, indent=2)
30
+ syntax = Syntax(yaml_str, "yaml", theme="monokai", line_numbers=True)
31
+ console.print(syntax)
32
+
@@ -0,0 +1,45 @@
1
+ """CLI diff command displaying repository changes and stats."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+ import typer
6
+ from rich.panel import Panel
7
+ from rich.syntax import Syntax
8
+
9
+ from terminal_agent.cli.theme import console
10
+ from terminal_agent.cli.ui import render_header
11
+ from terminal_agent.git.adapter import GitAdapter
12
+
13
+
14
+ def diff_command(
15
+ file_path: Optional[str] = typer.Argument(None, help="Specific file path to diff"),
16
+ stat_only: bool = typer.Option(False, "--stat", "-s", help="Show diffstat summary only"),
17
+ working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
18
+ ) -> None:
19
+ """View current git diff and changes made by agent."""
20
+ render_header()
21
+ target_dir = (working_dir or Path.cwd()).resolve()
22
+ git_adapter = GitAdapter(target_dir)
23
+
24
+ if not git_adapter.is_git_repo():
25
+ console.print("[agent.error]Current directory is not a Git repository.[/agent.error]")
26
+ raise typer.Exit(1)
27
+
28
+ diff_stats = git_adapter.get_diff_stats()
29
+ diff_text = git_adapter.get_diff(file_path=file_path, stat=stat_only)
30
+
31
+ console.print(
32
+ f"[agent.accent]Changed Files ({diff_stats.files_changed}):[/agent.accent] "
33
+ f"[agent.success]+{diff_stats.insertions}[/agent.success] / [agent.error]-{diff_stats.deletions}[/agent.error] lines\n"
34
+ )
35
+
36
+ if not diff_text.strip():
37
+ console.print("[agent.muted]No working tree modifications.[/agent.muted]")
38
+ return
39
+
40
+ if stat_only:
41
+ console.print(Panel(diff_text, title="[agent.accent]DIFF STAT[/agent.accent]", border_style="agent.border"))
42
+ else:
43
+ syntax = Syntax(diff_text, "diff", theme="monokai", line_numbers=False)
44
+ console.print(Panel(syntax, title="[agent.accent]WORKING TREE DIFF[/agent.accent]", border_style="agent.border"))
45
+
@@ -0,0 +1,136 @@
1
+ """CLI doctor command checking environment, toolchain, sandbox, and provider availability."""
2
+
3
+ import os
4
+ import shutil
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional
8
+ import typer
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ from terminal_agent.cli.theme import SYM_CHECK, SYM_CROSS, SYM_WARN, console
13
+ from terminal_agent.cli.ui import render_header
14
+ from terminal_agent.config.schema import ProviderType
15
+ from terminal_agent.config.settings import load_config
16
+ from terminal_agent.git.adapter import GitAdapter
17
+ from terminal_agent.providers.detector import OllamaStatus, ProviderStatus, detect_all_providers, detect_ollama
18
+ from terminal_agent.sandbox.docker import DockerSandbox
19
+
20
+
21
+ def doctor_command(
22
+ working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
23
+ ) -> None:
24
+ """Check system toolchains, sandbox prerequisites, and model provider availability."""
25
+ render_header()
26
+ target_dir = (working_dir or Path.cwd()).resolve()
27
+ config = load_config(target_dir / "terminal-agent.config.yaml")
28
+
29
+ console.print("[agent.accent]Running system diagnostics and provider auto-detection...[/agent.accent]\n")
30
+
31
+ # 1. System & Toolchains Table
32
+ sys_table = Table(title="[bold agent.accent]SYSTEM & TOOLCHAINS[/bold agent.accent]", border_style="agent.border")
33
+ sys_table.add_column("Component", style="agent.text", width=22)
34
+ sys_table.add_column("Status", width=10)
35
+ sys_table.add_column("Details", style="agent.muted")
36
+
37
+ # Python Check
38
+ py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
39
+ if sys.version_info >= (3, 10):
40
+ sys_table.add_row("Python Runtime", SYM_CHECK, f"Python {py_ver}")
41
+ else:
42
+ sys_table.add_row("Python Runtime", SYM_CROSS, f"Python {py_ver} (Requires 3.10+)")
43
+
44
+ # Git Check
45
+ git_path = shutil.which("git")
46
+ if git_path:
47
+ git_adapter = GitAdapter(target_dir)
48
+ is_repo = git_adapter.is_git_repo()
49
+ sys_table.add_row("Git CLI", SYM_CHECK, f"Installed, Repository Root: {'Yes' if is_repo else 'No'}")
50
+ else:
51
+ sys_table.add_row("Git CLI", SYM_CROSS, "Git is not installed or not in PATH")
52
+
53
+ # Local Sandbox Check
54
+ sys_table.add_row("Local Sandbox", SYM_CHECK, "Process-tree isolation with psutil timeout manager")
55
+
56
+ # Docker Sandbox Check
57
+ docker_ok = DockerSandbox.is_docker_available()
58
+ if docker_ok:
59
+ sys_table.add_row("Docker Sandbox", SYM_CHECK, "Docker daemon running & available")
60
+ else:
61
+ sys_table.add_row("Docker Sandbox", SYM_WARN, "Docker not running (Local sandbox used as default)")
62
+
63
+ # Workspace Permissions
64
+ try:
65
+ test_file = target_dir / ".doctor_perm_test.tmp"
66
+ test_file.write_text("ok", encoding="utf-8")
67
+ test_file.unlink()
68
+ sys_table.add_row("Workspace Access", SYM_CHECK, f"Read/Write OK on {target_dir.name}")
69
+ except Exception as e:
70
+ sys_table.add_row("Workspace Access", SYM_CROSS, f"Permission error: {e}")
71
+
72
+ console.print(sys_table)
73
+ console.print()
74
+
75
+ # 2. Providers Detection Table
76
+ prov_table = Table(title="[bold agent.accent]MODEL PROVIDERS[/bold agent.accent]", border_style="agent.border")
77
+ prov_table.add_column("Provider", style="agent.text", width=22)
78
+ prov_table.add_column("Status", width=10)
79
+ prov_table.add_column("Details", style="agent.muted")
80
+
81
+ providers = detect_all_providers(config=config, working_dir=target_dir)
82
+ configured_provider_name = config.provider.name.value if hasattr(config.provider.name, "value") else str(config.provider.name)
83
+
84
+ any_usable = False
85
+ configured_is_usable = False
86
+
87
+ for prov_name, info in providers.items():
88
+ is_active_config = (prov_name == configured_provider_name)
89
+ display_label = f"{info.display_name} {'[bold cyan](Active)[/bold cyan]' if is_active_config else ''}"
90
+
91
+ if info.status == ProviderStatus.READY:
92
+ any_usable = True
93
+ if is_active_config:
94
+ configured_is_usable = True
95
+ prov_table.add_row(display_label, SYM_CHECK, info.details)
96
+ elif info.status == ProviderStatus.NOT_CONFIGURED:
97
+ # Optional missing provider -> SYM_WARN / Info, NOT SYM_CROSS
98
+ status_badge = SYM_WARN
99
+ hint_str = f" - Hint: {info.action_hint}" if info.action_hint else ""
100
+ prov_table.add_row(display_label, status_badge, f"{info.details}{hint_str}")
101
+ else:
102
+ # Error or explicitly selected but broken
103
+ status_badge = SYM_CROSS if is_active_config else SYM_WARN
104
+ hint_str = f" - Hint: {info.action_hint}" if info.action_hint else ""
105
+ prov_table.add_row(display_label, status_badge, f"{info.details}{hint_str}")
106
+
107
+ console.print(prov_table)
108
+ console.print()
109
+
110
+ # 3. Overall Readiness Assessment
111
+ if configured_is_usable or any_usable:
112
+ active_provider_info = providers.get(configured_provider_name)
113
+ if active_provider_info and active_provider_info.is_usable:
114
+ lines = [
115
+ "[bold agent.success]Overall Status: READY[/bold agent.success]",
116
+ f"Active Provider: [bold agent.accent]{active_provider_info.display_name}[/bold agent.accent] (Model: {config.provider.model})"
117
+ ]
118
+ else:
119
+ usable_names = [p.display_name for p in providers.values() if p.is_usable and p.name != "mock"]
120
+ if not usable_names:
121
+ usable_names = ["Mock Provider"]
122
+ lines = [
123
+ "[bold agent.success]Overall Status: READY[/bold agent.success]",
124
+ f"Available Provider(s): [bold agent.accent]{', '.join(usable_names)}[/bold agent.accent]",
125
+ "[agent.muted]Run [bold]terminal-agent setup[/bold] to switch default provider.[/agent.muted]"
126
+ ]
127
+ console.print(Panel("\n".join(lines), border_style="agent.success", title="[bold agent.success]DIAGNOSTIC RESULT[/bold agent.success]"))
128
+ else:
129
+ lines = [
130
+ "[bold agent.error]Overall Status: NOT READY[/bold agent.error]",
131
+ "[agent.warning]No model provider is currently configured or reachable.[/agent.warning]",
132
+ "",
133
+ "Next Action:",
134
+ " Run [bold agent.accent]terminal-agent setup[/bold agent.accent] to interactively configure Ollama or cloud API keys."
135
+ ]
136
+ console.print(Panel("\n".join(lines), border_style="agent.error", title="[bold agent.error]DIAGNOSTIC RESULT[/bold agent.error]"))
@@ -0,0 +1,109 @@
1
+ """CLI resume command restoring and continuing an interrupted session."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+ import typer
6
+
7
+ from terminal_agent.agent.loop import AgentLoop
8
+ from terminal_agent.checkpoints.manager import CheckpointManager
9
+ from terminal_agent.cli.theme import SYM_CHECK, console
10
+ from terminal_agent.cli.ui import (
11
+ render_contract_card,
12
+ render_header,
13
+ render_plan,
14
+ render_proof_of_done,
15
+ render_task_card,
16
+ render_tool_event,
17
+ render_verification_result,
18
+ )
19
+ from terminal_agent.config.settings import load_config
20
+ from terminal_agent.context.engine import RepositoryContextEngine
21
+ from terminal_agent.git.adapter import GitAdapter
22
+ from terminal_agent.providers.factory import create_provider
23
+ from terminal_agent.sandbox.local import LocalSandbox
24
+ from terminal_agent.security.policy import SecurityPolicyEnforcer
25
+ from terminal_agent.security.secrets import SecretGuard
26
+ from terminal_agent.session.manager import SessionManager
27
+ from terminal_agent.session.models import VerificationStatus
28
+ from terminal_agent.telemetry.events import TelemetryLogger
29
+ from terminal_agent.tools.registry import ToolRegistry
30
+ from terminal_agent.verifier.engine import IndependentVerifier
31
+
32
+
33
+ def resume_command(
34
+ session_id: Optional[str] = typer.Argument(None, help="Session ID to resume (or latest if omitted)"),
35
+ working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
36
+ ) -> None:
37
+ """Resume an interrupted terminal agent session."""
38
+ render_header()
39
+ target_dir = (working_dir or Path.cwd()).resolve()
40
+ session_mgr = SessionManager(target_dir)
41
+
42
+ if session_id:
43
+ state = session_mgr.load_session(session_id)
44
+ else:
45
+ state = session_mgr.get_latest_session()
46
+
47
+ if not state:
48
+ console.print(f"[agent.error]No resumable session found in {target_dir}.[/agent.error]")
49
+ raise typer.Exit(1)
50
+
51
+ console.print(f"\n[agent.accent]SESSION RESUMED: {state.session_id}[/agent.accent]")
52
+ render_task_card(state.task_description, target_dir)
53
+
54
+ if state.contract:
55
+ render_contract_card(state.contract)
56
+ if state.plan:
57
+ render_plan(state.plan)
58
+
59
+ console.print(f"[agent.muted]Completed steps: {len(state.completed_steps)}[/agent.muted]")
60
+ console.print(f"[agent.muted]Modified files: {', '.join(state.modified_files) if state.modified_files else 'None'}[/agent.muted]")
61
+ console.print(f"[agent.accent]Next: {state.next_step}[/agent.accent]\n")
62
+
63
+ if state.current_status == VerificationStatus.VERIFIED:
64
+ console.print("[agent.success]This session is already VERIFIED.[/agent.success]")
65
+ if state.proof_of_done:
66
+ render_proof_of_done(state.proof_of_done)
67
+ return
68
+
69
+ # Set up runtime to continue execution
70
+ config = load_config(target_dir / "terminal-agent.config.yaml")
71
+ secret_guard = SecretGuard(blocked_patterns=config.security.blocked_paths, working_dir=target_dir)
72
+ security_enforcer = SecurityPolicyEnforcer(security_config=config.security, secret_guard=secret_guard)
73
+ sandbox = LocalSandbox(working_dir=target_dir, default_timeout=config.sandbox.timeout_seconds, secret_guard=secret_guard)
74
+ git_adapter = GitAdapter(target_dir)
75
+ checkpoint_mgr = CheckpointManager(target_dir)
76
+ context_engine = RepositoryContextEngine(target_dir, secret_guard=secret_guard)
77
+ tool_registry = ToolRegistry(target_dir, sandbox, security_enforcer, git_adapter, checkpoint_mgr, secret_guard)
78
+ verifier = IndependentVerifier(sandbox, git_adapter, config.verification, target_dir)
79
+ provider = create_provider(config.provider)
80
+ telemetry_logger = TelemetryLogger(state.session_id, working_dir=target_dir)
81
+
82
+ def on_event(event_type: str, data):
83
+ if event_type == "tool_completed":
84
+ render_tool_event(
85
+ tool_name=data.reason.split()[0] if data.reason else "tool",
86
+ reason=data.reason,
87
+ status="success" if data.success else "error",
88
+ duration_ms=data.duration_ms
89
+ )
90
+ elif event_type == "verification_result":
91
+ render_verification_result(data)
92
+ elif event_type == "proof_of_done":
93
+ render_proof_of_done(data, duration_seconds=state.metrics.execution_time_seconds)
94
+
95
+ agent_loop = AgentLoop(
96
+ session_state=state,
97
+ config=config,
98
+ provider=provider,
99
+ tool_registry=tool_registry,
100
+ verifier=verifier,
101
+ context_engine=context_engine,
102
+ checkpoint_manager=checkpoint_mgr,
103
+ session_manager=session_mgr,
104
+ telemetry_logger=telemetry_logger,
105
+ step_callback=on_event
106
+ )
107
+
108
+ agent_loop.run()
109
+
@@ -0,0 +1,184 @@
1
+ """CLI run command executing autonomous coding tasks."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Dict, List, Optional
5
+ import typer
6
+ import yaml
7
+
8
+ from terminal_agent.agent.loop import AgentLoop
9
+ from terminal_agent.checkpoints.manager import CheckpointManager
10
+ from terminal_agent.cli.theme import SYM_CHECK, SYM_CROSS, console
11
+ from terminal_agent.cli.ui import (
12
+ render_contract_card,
13
+ render_header,
14
+ render_plan,
15
+ render_proof_of_done,
16
+ render_task_card,
17
+ render_tool_event,
18
+ render_verification_result,
19
+ )
20
+ from terminal_agent.config.settings import load_config
21
+ from terminal_agent.context.engine import RepositoryContextEngine
22
+ from terminal_agent.git.adapter import GitAdapter
23
+ from terminal_agent.planner.contract import TaskContractGenerator
24
+ from terminal_agent.providers.detector import ProviderStatus, resolve_active_provider
25
+ from terminal_agent.providers.factory import create_provider
26
+ from terminal_agent.sandbox.docker import DockerSandbox
27
+ from terminal_agent.sandbox.local import LocalSandbox
28
+ from terminal_agent.security.policy import SecurityPolicyEnforcer
29
+ from terminal_agent.security.secrets import SecretGuard
30
+ from terminal_agent.session.manager import SessionManager
31
+ from terminal_agent.session.models import VerificationStatus
32
+ from terminal_agent.telemetry.events import TelemetryLogger
33
+ from terminal_agent.tools.registry import ToolRegistry
34
+ from terminal_agent.verifier.engine import IndependentVerifier
35
+
36
+
37
+ def run_command(
38
+ task: Optional[str] = typer.Argument(None, help="The coding task description to execute autonomously"),
39
+ task_file: Optional[Path] = typer.Option(None, "--task", "-t", help="Path to declarative task YAML file"),
40
+ working_dir: Optional[Path] = typer.Option(None, "--cwd", "-C", help="Target repository directory"),
41
+ provider_name: Optional[str] = typer.Option(None, "--provider", "-p", help="Override model provider"),
42
+ model_name: Optional[str] = typer.Option(None, "--model", "-m", help="Override model name"),
43
+ auto_approve: bool = typer.Option(False, "--yes", "-y", help="Auto-approve confirmation prompts"),
44
+ ) -> None:
45
+ """Execute an autonomous verify-first coding session."""
46
+ render_header()
47
+ target_dir = (working_dir or Path.cwd()).resolve()
48
+
49
+ task_desc = task
50
+ if task_file and task_file.exists():
51
+ try:
52
+ content = yaml.safe_load(task_file.read_text(encoding="utf-8"))
53
+ if isinstance(content, dict):
54
+ task_desc = content.get("prompt") or content.get("task") or str(content)
55
+ else:
56
+ task_desc = str(content)
57
+ except Exception as e:
58
+ console.print(f"[agent.error]Error reading task file: {e}[/agent.error]")
59
+ raise typer.Exit(1)
60
+
61
+ if not task_desc:
62
+ task_desc = typer.prompt("Describe the coding task")
63
+
64
+ render_task_card(task_desc, target_dir)
65
+
66
+ # Load configuration & resolve active provider
67
+ config = load_config(target_dir / "terminal-agent.config.yaml")
68
+ resolved_prov_cfg, prov_info = resolve_active_provider(
69
+ config=config,
70
+ explicit_provider=provider_name,
71
+ explicit_model=model_name,
72
+ working_dir=target_dir
73
+ )
74
+ config.provider = resolved_prov_cfg
75
+
76
+ # Initialize subsystems
77
+ secret_guard = SecretGuard(blocked_patterns=config.security.blocked_paths, working_dir=target_dir)
78
+
79
+ def confirmation_cb(cmd: str, category, reason: str) -> bool:
80
+ if auto_approve:
81
+ return True
82
+ console.print(f"\n[agent.warning]Security Warning: {category.value.upper()} command[/agent.warning]")
83
+ console.print(f"Command: [agent.accent]{cmd}[/agent.accent]")
84
+ console.print(f"Reason: [agent.muted]{reason}[/agent.muted]")
85
+ return typer.confirm("Allow execution?")
86
+
87
+ security_enforcer = SecurityPolicyEnforcer(
88
+ security_config=config.security,
89
+ secret_guard=secret_guard,
90
+ confirmation_callback=confirmation_cb
91
+ )
92
+
93
+ if config.sandbox.mode == "docker" and DockerSandbox.is_docker_available():
94
+ sandbox = DockerSandbox(
95
+ working_dir=target_dir,
96
+ image=config.sandbox.image,
97
+ network=config.sandbox.network,
98
+ timeout_seconds=config.sandbox.timeout_seconds,
99
+ secret_guard=secret_guard
100
+ )
101
+ else:
102
+ sandbox = LocalSandbox(
103
+ working_dir=target_dir,
104
+ default_timeout=config.sandbox.timeout_seconds,
105
+ secret_guard=secret_guard
106
+ )
107
+
108
+ git_adapter = GitAdapter(target_dir)
109
+ checkpoint_mgr = CheckpointManager(target_dir)
110
+ session_mgr = SessionManager(target_dir)
111
+ context_engine = RepositoryContextEngine(target_dir, secret_guard=secret_guard)
112
+
113
+ tool_registry = ToolRegistry(
114
+ working_dir=target_dir,
115
+ sandbox=sandbox,
116
+ security_enforcer=security_enforcer,
117
+ git_adapter=git_adapter,
118
+ checkpoint_manager=checkpoint_mgr,
119
+ secret_guard=secret_guard
120
+ )
121
+
122
+ verifier = IndependentVerifier(
123
+ sandbox=sandbox,
124
+ git_adapter=git_adapter,
125
+ config=config.verification,
126
+ working_dir=target_dir
127
+ )
128
+
129
+ # Initialize Provider
130
+ provider = create_provider(config.provider)
131
+ health_ok, health_msg = provider.check_health()
132
+ if not health_ok and config.provider.name.value != "mock":
133
+ console.print(f"[agent.warning]Provider Notice ({prov_info.display_name}): {health_msg}[/agent.warning]")
134
+ if prov_info.action_hint:
135
+ console.print(f"[agent.accent]Suggested Action: {prov_info.action_hint}[/agent.accent]")
136
+ if not prov_info.is_usable:
137
+ console.print("\n[agent.muted]To configure or switch model providers, run: [bold]terminal-agent setup[/bold][/agent.muted]\n")
138
+
139
+ # Create Session
140
+ session_state = session_mgr.create_session(task_description=task_desc)
141
+ telemetry_logger = TelemetryLogger(session_state.session_id, working_dir=target_dir)
142
+
143
+ # Step callback for live display
144
+ def on_event(event_type: str, data: Any):
145
+ if event_type == "contract_ready":
146
+ render_contract_card(data)
147
+ if session_state.plan:
148
+ render_plan(session_state.plan)
149
+ elif event_type == "checkpoint_created":
150
+ console.print(f"{SYM_CHECK} [agent.muted]Created baseline checkpoint '{data.checkpoint_id}'[/agent.muted]")
151
+ elif event_type == "tool_completed":
152
+ render_tool_event(
153
+ tool_name=data.reason.split()[0] if data.reason else "tool",
154
+ reason=data.reason,
155
+ status="success" if data.success else "error",
156
+ duration_ms=data.duration_ms
157
+ )
158
+ elif event_type == "verification_result":
159
+ render_verification_result(data)
160
+ elif event_type == "repair_plan":
161
+ console.print(f"[agent.warning]Recovery Plan: Category '{data.failure_category.value}' -> {data.recovery_action}[/agent.warning]")
162
+ elif event_type == "proof_of_done":
163
+ render_proof_of_done(data, duration_seconds=session_state.metrics.execution_time_seconds)
164
+
165
+ agent_loop = AgentLoop(
166
+ session_state=session_state,
167
+ config=config,
168
+ provider=provider,
169
+ tool_registry=tool_registry,
170
+ verifier=verifier,
171
+ context_engine=context_engine,
172
+ checkpoint_manager=checkpoint_mgr,
173
+ session_manager=session_mgr,
174
+ telemetry_logger=telemetry_logger,
175
+ step_callback=on_event
176
+ )
177
+
178
+ # Run Loop
179
+ final_state = agent_loop.run()
180
+
181
+ if final_state.current_status == VerificationStatus.VERIFIED:
182
+ console.print(f"\n[agent.success]Task successfully VERIFIED. Session: {final_state.session_id}[/agent.success]")
183
+ else:
184
+ console.print(f"\n[agent.error]Task concluded with status: {final_state.current_status.value}[/agent.error]")