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.

Files changed (83) hide show
  1. convergent_cli/__init__.py +1 -0
  2. convergent_cli/auth.py +227 -0
  3. convergent_cli/build_context.py +225 -0
  4. convergent_cli/calibration_finish.py +310 -0
  5. convergent_cli/calibration_frontier.py +499 -0
  6. convergent_cli/calibration_runner.py +1413 -0
  7. convergent_cli/calibration_send.py +44 -0
  8. convergent_cli/codex_app_server.py +127 -0
  9. convergent_cli/command_tree.py +102 -0
  10. convergent_cli/commands/__init__.py +58 -0
  11. convergent_cli/commands/agents.py +729 -0
  12. convergent_cli/commands/auth.py +117 -0
  13. convergent_cli/commands/calibration.py +3857 -0
  14. convergent_cli/commands/calibration_labels_load.py +627 -0
  15. convergent_cli/commands/capabilities.py +85 -0
  16. convergent_cli/commands/dataset_authoring.py +964 -0
  17. convergent_cli/commands/dataset_import.py +763 -0
  18. convergent_cli/commands/datasets.py +678 -0
  19. convergent_cli/commands/docs.py +120 -0
  20. convergent_cli/commands/doctor.py +124 -0
  21. convergent_cli/commands/environments.py +63 -0
  22. convergent_cli/commands/findings.py +206 -0
  23. convergent_cli/commands/graders.py +803 -0
  24. convergent_cli/commands/harness.py +366 -0
  25. convergent_cli/commands/issues.py +388 -0
  26. convergent_cli/commands/production.py +173 -0
  27. convergent_cli/commands/profile.py +86 -0
  28. convergent_cli/commands/rubrics.py +14 -0
  29. convergent_cli/commands/runs.py +1763 -0
  30. convergent_cli/commands/setup.py +968 -0
  31. convergent_cli/commands/task_qc.py +1041 -0
  32. convergent_cli/commands/task_qc_reports.py +393 -0
  33. convergent_cli/commands/verdicts.py +156 -0
  34. convergent_cli/commands/workspaces.py +170 -0
  35. convergent_cli/commands/worlds.py +292 -0
  36. convergent_cli/confirm.py +59 -0
  37. convergent_cli/control_client.py +141 -0
  38. convergent_cli/customer_client.py +1272 -0
  39. convergent_cli/docs_client.py +165 -0
  40. convergent_cli/errors.py +212 -0
  41. convergent_cli/harness_run.py +1169 -0
  42. convergent_cli/harness_tree.py +854 -0
  43. convergent_cli/installed_agent.py +1071 -0
  44. convergent_cli/json_files.py +23 -0
  45. convergent_cli/local_files.py +64 -0
  46. convergent_cli/main.py +106 -0
  47. convergent_cli/oauth.py +431 -0
  48. convergent_cli/output.py +15 -0
  49. convergent_cli/paging.py +62 -0
  50. convergent_cli/run_calibration.py +361 -0
  51. convergent_cli/run_usage.py +99 -0
  52. convergent_cli/sessions.py +275 -0
  53. convergent_cli/setup_agents.py +190 -0
  54. convergent_cli/setup_auth.py +463 -0
  55. convergent_cli/setup_instrument.py +244 -0
  56. convergent_cli/setup_mcp.py +525 -0
  57. convergent_cli/setup_wizard.py +125 -0
  58. convergent_cli/skill_files/convergent/SKILL.md +25 -0
  59. convergent_cli/skill_files/convergent-instrument/SKILL.md +15 -0
  60. convergent_cli/skill_files/convergent-verify/SKILL.md +15 -0
  61. convergent_cli/skills.py +489 -0
  62. convergent_cli/state.py +297 -0
  63. convergent_cli/transport.py +242 -0
  64. convergent_cli/verdict_feedback.py +161 -0
  65. convergent_cli/verifier_bench_preflight.py +32 -0
  66. convergent_cli/world_docker.py +145 -0
  67. convergent_cli/world_files.py +432 -0
  68. convergent_cli/world_live.py +660 -0
  69. convergent_cli/world_mcp.py +109 -0
  70. convergent_cli/world_probe_requirements.txt +29 -0
  71. convergent_cli/world_template/Dockerfile +10 -0
  72. convergent_cli/world_template/README.md +38 -0
  73. convergent_cli/world_template/convergent.toml +11 -0
  74. convergent_cli/world_template/examples/default/environment/catalog.json +3 -0
  75. convergent_cli/world_template/examples/default/instruction.md +1 -0
  76. convergent_cli/world_template/requirements.txt +1 -0
  77. convergent_cli/world_template/schemas/catalog.json +5 -0
  78. convergent_cli/world_template/server.py +25 -0
  79. convergent_cli-0.1.0.dist-info/METADATA +679 -0
  80. convergent_cli-0.1.0.dist-info/RECORD +83 -0
  81. convergent_cli-0.1.0.dist-info/WHEEL +4 -0
  82. convergent_cli-0.1.0.dist-info/entry_points.txt +2 -0
  83. 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"]