convergent-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.
Potentially problematic release.
This version of convergent-cli might be problematic. Click here for more details.
- convergent_cli/__init__.py +1 -0
- convergent_cli/auth.py +227 -0
- convergent_cli/build_context.py +225 -0
- convergent_cli/calibration_finish.py +310 -0
- convergent_cli/calibration_frontier.py +499 -0
- convergent_cli/calibration_runner.py +1413 -0
- convergent_cli/calibration_send.py +44 -0
- convergent_cli/codex_app_server.py +127 -0
- convergent_cli/command_tree.py +102 -0
- convergent_cli/commands/__init__.py +58 -0
- convergent_cli/commands/agents.py +729 -0
- convergent_cli/commands/auth.py +117 -0
- convergent_cli/commands/calibration.py +3857 -0
- convergent_cli/commands/calibration_labels_load.py +627 -0
- convergent_cli/commands/capabilities.py +85 -0
- convergent_cli/commands/dataset_authoring.py +964 -0
- convergent_cli/commands/dataset_import.py +763 -0
- convergent_cli/commands/datasets.py +678 -0
- convergent_cli/commands/docs.py +120 -0
- convergent_cli/commands/doctor.py +124 -0
- convergent_cli/commands/environments.py +63 -0
- convergent_cli/commands/findings.py +206 -0
- convergent_cli/commands/graders.py +803 -0
- convergent_cli/commands/harness.py +366 -0
- convergent_cli/commands/issues.py +388 -0
- convergent_cli/commands/production.py +173 -0
- convergent_cli/commands/profile.py +86 -0
- convergent_cli/commands/rubrics.py +14 -0
- convergent_cli/commands/runs.py +1763 -0
- convergent_cli/commands/setup.py +968 -0
- convergent_cli/commands/task_qc.py +1041 -0
- convergent_cli/commands/task_qc_reports.py +393 -0
- convergent_cli/commands/verdicts.py +156 -0
- convergent_cli/commands/workspaces.py +170 -0
- convergent_cli/commands/worlds.py +292 -0
- convergent_cli/confirm.py +59 -0
- convergent_cli/control_client.py +141 -0
- convergent_cli/customer_client.py +1272 -0
- convergent_cli/docs_client.py +165 -0
- convergent_cli/errors.py +212 -0
- convergent_cli/harness_run.py +1169 -0
- convergent_cli/harness_tree.py +854 -0
- convergent_cli/installed_agent.py +1071 -0
- convergent_cli/json_files.py +23 -0
- convergent_cli/local_files.py +64 -0
- convergent_cli/main.py +106 -0
- convergent_cli/oauth.py +431 -0
- convergent_cli/output.py +15 -0
- convergent_cli/paging.py +62 -0
- convergent_cli/run_calibration.py +361 -0
- convergent_cli/run_usage.py +99 -0
- convergent_cli/sessions.py +275 -0
- convergent_cli/setup_agents.py +190 -0
- convergent_cli/setup_auth.py +463 -0
- convergent_cli/setup_instrument.py +244 -0
- convergent_cli/setup_mcp.py +525 -0
- convergent_cli/setup_wizard.py +125 -0
- convergent_cli/skill_files/convergent/SKILL.md +25 -0
- convergent_cli/skill_files/convergent-instrument/SKILL.md +15 -0
- convergent_cli/skill_files/convergent-verify/SKILL.md +15 -0
- convergent_cli/skills.py +489 -0
- convergent_cli/state.py +297 -0
- convergent_cli/transport.py +242 -0
- convergent_cli/verdict_feedback.py +161 -0
- convergent_cli/verifier_bench_preflight.py +32 -0
- convergent_cli/world_docker.py +145 -0
- convergent_cli/world_files.py +432 -0
- convergent_cli/world_live.py +660 -0
- convergent_cli/world_mcp.py +109 -0
- convergent_cli/world_probe_requirements.txt +29 -0
- convergent_cli/world_template/Dockerfile +10 -0
- convergent_cli/world_template/README.md +38 -0
- convergent_cli/world_template/convergent.toml +11 -0
- convergent_cli/world_template/examples/default/environment/catalog.json +3 -0
- convergent_cli/world_template/examples/default/instruction.md +1 -0
- convergent_cli/world_template/requirements.txt +1 -0
- convergent_cli/world_template/schemas/catalog.json +5 -0
- convergent_cli/world_template/server.py +25 -0
- convergent_cli-0.1.0.dist-info/METADATA +679 -0
- convergent_cli-0.1.0.dist-info/RECORD +83 -0
- convergent_cli-0.1.0.dist-info/WHEEL +4 -0
- convergent_cli-0.1.0.dist-info/entry_points.txt +2 -0
- convergent_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""A monotonic budget shared by calibration send requests and polling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import math
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from convergent_cli.errors import NetworkError
|
|
10
|
+
|
|
11
|
+
DEFAULT_SEND_TIMEOUT_SECONDS = 600.0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def send_timeout_seconds(value: str) -> float:
|
|
15
|
+
try:
|
|
16
|
+
seconds = float(value)
|
|
17
|
+
except ValueError as exc:
|
|
18
|
+
raise argparse.ArgumentTypeError("--send-timeout must be finite positive seconds") from exc
|
|
19
|
+
if not math.isfinite(seconds) or seconds <= 0:
|
|
20
|
+
raise argparse.ArgumentTypeError("--send-timeout must be finite positive seconds")
|
|
21
|
+
return seconds
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SendTimeout(NetworkError):
|
|
25
|
+
"""The send budget expired; callers add the latest tenant receipt states."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SendDeadline:
|
|
29
|
+
def __init__(self, seconds: float) -> None:
|
|
30
|
+
self.seconds = seconds
|
|
31
|
+
self._expires_at: float | None = None
|
|
32
|
+
|
|
33
|
+
def remaining(self) -> float:
|
|
34
|
+
now = time.monotonic()
|
|
35
|
+
if self._expires_at is None:
|
|
36
|
+
self._expires_at = now + self.seconds
|
|
37
|
+
remaining = self._expires_at - now
|
|
38
|
+
if remaining <= 0:
|
|
39
|
+
raise SendTimeout("send_timeout")
|
|
40
|
+
return remaining
|
|
41
|
+
|
|
42
|
+
def pause(self) -> None:
|
|
43
|
+
time.sleep(min(5.0, self.remaining()))
|
|
44
|
+
self.remaining()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import json
|
|
5
|
+
import queue
|
|
6
|
+
import subprocess # nosec B404 - fixed argv, no shell
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TextIO
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _read_lines(stream: TextIO, messages: queue.Queue[str | None]) -> None:
|
|
15
|
+
try:
|
|
16
|
+
for line in stream:
|
|
17
|
+
messages.put(line)
|
|
18
|
+
finally:
|
|
19
|
+
messages.put(None)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _response(
|
|
23
|
+
process: subprocess.Popen[str],
|
|
24
|
+
messages: queue.Queue[str | None],
|
|
25
|
+
request_id: int,
|
|
26
|
+
method: str,
|
|
27
|
+
params: Mapping[str, object],
|
|
28
|
+
timeout_seconds: float,
|
|
29
|
+
) -> dict[str, object]:
|
|
30
|
+
if process.stdin is None:
|
|
31
|
+
raise OSError("Codex app-server stdin is unavailable")
|
|
32
|
+
process.stdin.write(json.dumps({"id": request_id, "method": method, "params": params}) + "\n")
|
|
33
|
+
process.stdin.flush()
|
|
34
|
+
deadline = time.monotonic() + timeout_seconds
|
|
35
|
+
while True:
|
|
36
|
+
remaining = deadline - time.monotonic()
|
|
37
|
+
if remaining <= 0:
|
|
38
|
+
raise subprocess.TimeoutExpired(process.args, timeout_seconds)
|
|
39
|
+
try:
|
|
40
|
+
line = messages.get(timeout=remaining)
|
|
41
|
+
except queue.Empty as error:
|
|
42
|
+
raise subprocess.TimeoutExpired(process.args, timeout_seconds) from error
|
|
43
|
+
if line is None:
|
|
44
|
+
raise OSError("Codex app-server exited before answering")
|
|
45
|
+
try:
|
|
46
|
+
body = json.loads(line)
|
|
47
|
+
except json.JSONDecodeError as error:
|
|
48
|
+
raise OSError("Codex app-server returned a non-JSON protocol message") from error
|
|
49
|
+
if not isinstance(body, dict) or body.get("id") != request_id:
|
|
50
|
+
continue
|
|
51
|
+
result = body.get("result")
|
|
52
|
+
if "error" in body or not isinstance(result, dict):
|
|
53
|
+
raise OSError(f"Codex app-server refused {method}")
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _stop(process: subprocess.Popen[str], reader: threading.Thread | None) -> None:
|
|
58
|
+
if process.stdin is not None:
|
|
59
|
+
with contextlib.suppress(OSError):
|
|
60
|
+
process.stdin.close()
|
|
61
|
+
if process.poll() is None:
|
|
62
|
+
with contextlib.suppress(OSError):
|
|
63
|
+
process.terminate()
|
|
64
|
+
try:
|
|
65
|
+
process.wait(timeout=1)
|
|
66
|
+
except subprocess.TimeoutExpired:
|
|
67
|
+
with contextlib.suppress(OSError):
|
|
68
|
+
process.kill()
|
|
69
|
+
with contextlib.suppress(OSError, subprocess.TimeoutExpired):
|
|
70
|
+
process.wait(timeout=1)
|
|
71
|
+
if reader is not None:
|
|
72
|
+
reader.join(timeout=1)
|
|
73
|
+
if process.stdout is not None:
|
|
74
|
+
with contextlib.suppress(OSError):
|
|
75
|
+
process.stdout.close()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def read_effective_mcp_server(
|
|
79
|
+
binary: str,
|
|
80
|
+
cwd: Path,
|
|
81
|
+
environment: Mapping[str, str] | None,
|
|
82
|
+
name: str,
|
|
83
|
+
timeout_seconds: float,
|
|
84
|
+
) -> dict[str, object]:
|
|
85
|
+
process = subprocess.Popen( # nosec B603 - fixed argv, no shell
|
|
86
|
+
[binary, "app-server", "--stdio"],
|
|
87
|
+
stdin=subprocess.PIPE,
|
|
88
|
+
stdout=subprocess.PIPE,
|
|
89
|
+
stderr=subprocess.DEVNULL,
|
|
90
|
+
text=True,
|
|
91
|
+
encoding="utf-8",
|
|
92
|
+
errors="replace",
|
|
93
|
+
bufsize=1,
|
|
94
|
+
cwd=cwd,
|
|
95
|
+
env=dict(environment) if environment is not None else None,
|
|
96
|
+
)
|
|
97
|
+
reader: threading.Thread | None = None
|
|
98
|
+
try:
|
|
99
|
+
if process.stdout is None:
|
|
100
|
+
raise OSError("Codex app-server stdout is unavailable")
|
|
101
|
+
messages: queue.Queue[str | None] = queue.Queue()
|
|
102
|
+
reader = threading.Thread(target=_read_lines, args=(process.stdout, messages), daemon=True)
|
|
103
|
+
reader.start()
|
|
104
|
+
_response(
|
|
105
|
+
process,
|
|
106
|
+
messages,
|
|
107
|
+
1,
|
|
108
|
+
"initialize",
|
|
109
|
+
{"clientInfo": {"name": "cv-setup", "version": "1"}},
|
|
110
|
+
timeout_seconds,
|
|
111
|
+
)
|
|
112
|
+
response = _response(
|
|
113
|
+
process,
|
|
114
|
+
messages,
|
|
115
|
+
2,
|
|
116
|
+
"config/read",
|
|
117
|
+
{"cwd": str(cwd), "includeLayers": False},
|
|
118
|
+
timeout_seconds,
|
|
119
|
+
)
|
|
120
|
+
config = response.get("config")
|
|
121
|
+
servers = config.get("mcp_servers") if isinstance(config, dict) else None
|
|
122
|
+
server = servers.get(name) if isinstance(servers, dict) else None
|
|
123
|
+
if not isinstance(server, dict):
|
|
124
|
+
raise OSError(f"Codex effective config omitted the {name} server")
|
|
125
|
+
return server
|
|
126
|
+
finally:
|
|
127
|
+
_stop(process, reader)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Argparse command binding for the public hosted API CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
|
|
10
|
+
from convergent_cli.confirm import yes_enabled
|
|
11
|
+
|
|
12
|
+
CommandHandler = Callable[[argparse.Namespace], int]
|
|
13
|
+
|
|
14
|
+
_YES_HELP = "Approve this write without prompting. Required when stdin is not a terminal."
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Plane(StrEnum):
|
|
18
|
+
"""Where a command is answered."""
|
|
19
|
+
|
|
20
|
+
LOCAL = "local"
|
|
21
|
+
CONTROL = "control"
|
|
22
|
+
CUSTOMER = "customer"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Effect(StrEnum):
|
|
26
|
+
"""Whether a command only reads or changes state."""
|
|
27
|
+
|
|
28
|
+
READ = "read"
|
|
29
|
+
WRITE = "write"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class CommandInfo:
|
|
34
|
+
"""Packaged metadata for one reachable leaf command."""
|
|
35
|
+
|
|
36
|
+
path: tuple[str, ...]
|
|
37
|
+
description: str
|
|
38
|
+
plane: Plane
|
|
39
|
+
requires_session: bool
|
|
40
|
+
effect: Effect
|
|
41
|
+
mcp_tool: str | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CommandCatalog:
|
|
45
|
+
"""Command metadata mechanically populated as argparse leaves are bound."""
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self._records: dict[tuple[str, ...], CommandInfo] = {}
|
|
49
|
+
|
|
50
|
+
def bind(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
parser: argparse.ArgumentParser,
|
|
54
|
+
handler: CommandHandler,
|
|
55
|
+
info: CommandInfo,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Bind dispatch and metadata for one leaf parser."""
|
|
58
|
+
if not info.path:
|
|
59
|
+
raise ValueError("command path must not be empty")
|
|
60
|
+
if info.path in self._records:
|
|
61
|
+
command = " ".join(info.path)
|
|
62
|
+
raise ValueError(f"duplicate command path {command!r}")
|
|
63
|
+
if _uses_shared_yes(info):
|
|
64
|
+
parser.add_argument("-y", "--yes", action="store_true", dest="yes", help=_YES_HELP)
|
|
65
|
+
handler = _with_yes(handler)
|
|
66
|
+
parser.set_defaults(_handler=handler, _command_info=info)
|
|
67
|
+
self._records[info.path] = info
|
|
68
|
+
|
|
69
|
+
def get(self, path: tuple[str, ...]) -> CommandInfo | None:
|
|
70
|
+
return self._records.get(path)
|
|
71
|
+
|
|
72
|
+
def list(self) -> list[CommandInfo]:
|
|
73
|
+
return [self._records[path] for path in sorted(self._records)]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def require_handler(args: argparse.Namespace) -> CommandHandler:
|
|
77
|
+
handler = getattr(args, "_handler", None)
|
|
78
|
+
if handler is None:
|
|
79
|
+
raise ValueError("parsed command has no handler")
|
|
80
|
+
return handler
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _uses_shared_yes(info: CommandInfo) -> bool:
|
|
84
|
+
return info.effect is Effect.WRITE and info.path[:1] != ("setup",)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _with_yes(handler: CommandHandler) -> CommandHandler:
|
|
88
|
+
def run(args: argparse.Namespace) -> int:
|
|
89
|
+
with yes_enabled(args.yes):
|
|
90
|
+
return handler(args)
|
|
91
|
+
|
|
92
|
+
return run
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
__all__ = [
|
|
96
|
+
"CommandCatalog",
|
|
97
|
+
"CommandHandler",
|
|
98
|
+
"CommandInfo",
|
|
99
|
+
"Effect",
|
|
100
|
+
"Plane",
|
|
101
|
+
"require_handler",
|
|
102
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Explicit command-family composition root for ``cv``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from convergent_cli.command_tree import CommandCatalog
|
|
8
|
+
from convergent_cli.commands import (
|
|
9
|
+
agents,
|
|
10
|
+
auth,
|
|
11
|
+
calibration,
|
|
12
|
+
capabilities,
|
|
13
|
+
datasets,
|
|
14
|
+
docs,
|
|
15
|
+
doctor,
|
|
16
|
+
environments,
|
|
17
|
+
findings,
|
|
18
|
+
graders,
|
|
19
|
+
harness,
|
|
20
|
+
issues,
|
|
21
|
+
production,
|
|
22
|
+
profile,
|
|
23
|
+
rubrics,
|
|
24
|
+
runs,
|
|
25
|
+
setup,
|
|
26
|
+
task_qc,
|
|
27
|
+
verdicts,
|
|
28
|
+
workspaces,
|
|
29
|
+
worlds,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register_commands(subparsers: Any, catalog: CommandCatalog) -> None:
|
|
34
|
+
"""Register shipped command families in the customer-workflow help order."""
|
|
35
|
+
setup.register(subparsers, catalog)
|
|
36
|
+
auth.register(subparsers, catalog)
|
|
37
|
+
profile.register(subparsers, catalog)
|
|
38
|
+
workspaces.register(subparsers, catalog)
|
|
39
|
+
environments.register(subparsers, catalog)
|
|
40
|
+
doctor.register(subparsers, catalog)
|
|
41
|
+
production.register(subparsers, catalog)
|
|
42
|
+
agents.register(subparsers, catalog)
|
|
43
|
+
issues.register(subparsers, catalog)
|
|
44
|
+
findings.register(subparsers, catalog)
|
|
45
|
+
verdicts.register(subparsers, catalog)
|
|
46
|
+
graders.register(subparsers, catalog)
|
|
47
|
+
datasets.register(subparsers, catalog)
|
|
48
|
+
worlds.register(subparsers, catalog)
|
|
49
|
+
harness.register(subparsers, catalog)
|
|
50
|
+
runs.register(subparsers, catalog)
|
|
51
|
+
calibration.register(subparsers, catalog)
|
|
52
|
+
task_qc.register(subparsers, catalog)
|
|
53
|
+
docs.register(subparsers, catalog)
|
|
54
|
+
capabilities.register(subparsers, catalog)
|
|
55
|
+
rubrics.register(subparsers, catalog)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = ["register_commands"]
|