core-cli 1.0.2__tar.gz → 2.1.0__tar.gz

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 (65) hide show
  1. {core_cli-1.0.2 → core_cli-2.1.0}/.github/workflows/ci.yml +9 -5
  2. core_cli-2.1.0/PKG-INFO +113 -0
  3. core_cli-2.1.0/README.md +84 -0
  4. {core_cli-1.0.2 → core_cli-2.1.0}/pyproject.toml +22 -3
  5. core_cli-2.1.0/src/core_cli/client.py +349 -0
  6. core_cli-2.1.0/src/core_cli/command.py +77 -0
  7. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/main.py +0 -2
  8. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/code/__init__.py +0 -1
  9. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/code/actions.py +3 -17
  10. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/code/audit_duplicates.py +3 -3
  11. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/code/bridges.py +4 -6
  12. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/code/check_imports.py +3 -3
  13. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/code/docstrings.py +3 -3
  14. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/code/fix_atomic.py +3 -17
  15. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/code/format.py +4 -4
  16. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/code/integrity.py +2 -2
  17. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/code/lint.py +3 -3
  18. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/code/logging.py +3 -17
  19. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/code/test.py +3 -3
  20. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/lane/claim.py +4 -4
  21. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/lane/list.py +4 -4
  22. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/lane/next.py +6 -6
  23. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/lane/propose.py +5 -6
  24. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/project/__init__.py +2 -3
  25. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/project/onboard.py +4 -4
  26. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/project/scout.py +3 -3
  27. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/proposals/__init__.py +2 -1
  28. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/proposals/create.py +3 -3
  29. core_cli-2.1.0/src/core_cli/resources/proposals/integrate.py +37 -0
  30. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/proposals/list.py +3 -3
  31. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/proposals/manage.py +120 -7
  32. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/symbols/__init__.py +0 -1
  33. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/symbols/audit.py +10 -6
  34. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/symbols/fix_ids.py +12 -6
  35. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/symbols/resolve_duplicates.py +14 -6
  36. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/vectors/__init__.py +1 -7
  37. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/vectors/hub.py +1 -1
  38. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/vectors/query.py +5 -5
  39. core_cli-2.1.0/tests/test_client.py +107 -0
  40. {core_cli-1.0.2 → core_cli-2.1.0}/tests/test_code.py +0 -47
  41. core_cli-2.1.0/tests/test_contract.py +124 -0
  42. {core_cli-1.0.2 → core_cli-2.1.0}/tests/test_proposals.py +96 -18
  43. core_cli-1.0.2/PKG-INFO +0 -72
  44. core_cli-1.0.2/README.md +0 -46
  45. core_cli-1.0.2/src/core_cli/resources/code/check_ui.py +0 -61
  46. core_cli-1.0.2/src/core_cli/resources/project/docs.py +0 -41
  47. core_cli-1.0.2/src/core_cli/resources/proposals/integrate.py +0 -57
  48. core_cli-1.0.2/src/core_cli/resources/secrets/__init__.py +0 -10
  49. core_cli-1.0.2/src/core_cli/resources/secrets/hub.py +0 -9
  50. core_cli-1.0.2/src/core_cli/resources/secrets/manage.py +0 -160
  51. core_cli-1.0.2/src/core_cli/resources/symbols/sync.py +0 -39
  52. core_cli-1.0.2/src/core_cli/resources/vectors/rebuild.py +0 -71
  53. core_cli-1.0.2/src/core_cli/resources/vectors/status.py +0 -40
  54. core_cli-1.0.2/src/core_cli/resources/vectors/sync.py +0 -50
  55. core_cli-1.0.2/src/core_cli/resources/vectors/sync_code.py +0 -58
  56. {core_cli-1.0.2 → core_cli-2.1.0}/.gitignore +0 -0
  57. {core_cli-1.0.2 → core_cli-2.1.0}/LICENSE +0 -0
  58. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/__init__.py +0 -0
  59. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/__init__.py +0 -0
  60. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/code/hub.py +0 -0
  61. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/lane/__init__.py +0 -0
  62. {core_cli-1.0.2 → core_cli-2.1.0}/src/core_cli/resources/symbols/hub.py +0 -0
  63. {core_cli-1.0.2 → core_cli-2.1.0}/tests/__init__.py +0 -0
  64. {core_cli-1.0.2 → core_cli-2.1.0}/tests/test_lane.py +0 -0
  65. {core_cli-1.0.2 → core_cli-2.1.0}/tests/test_project.py +0 -0
@@ -19,7 +19,9 @@ jobs:
19
19
  python-version: "3.12"
20
20
 
21
21
  - name: Install ruff
22
- run: pip install ruff
22
+ # Pinned: an unpinned ruff picked up new default rules and turned lint
23
+ # red on unchanged code (2026-10). Bump deliberately.
24
+ run: pip install ruff==0.16.10
23
25
 
24
26
  - name: ruff check
25
27
  run: ruff check src/
@@ -37,10 +39,11 @@ jobs:
37
39
  - name: Install mypy + stubs
38
40
  run: pip install mypy types-PyYAML
39
41
 
42
+ - name: Install package
43
+ run: pip install -e .
44
+
40
45
  - name: mypy
41
- # core-runtime ships no type stubs; ignore-missing-imports preserves
42
- # checking of core-cli's own code without requiring stub packages.
43
- run: mypy src/ --ignore-missing-imports --no-error-summary
46
+ run: mypy src/ --no-error-summary
44
47
 
45
48
  test:
46
49
  name: Test
@@ -53,10 +56,11 @@ jobs:
53
56
  python-version: "3.12"
54
57
 
55
58
  - name: Install package + dev deps
56
- # core-runtime is on PyPI (>=2.8.0); installs as a transitive dep.
57
59
  run: pip install -e ".[dev]"
58
60
 
59
61
  - name: Run tests
62
+ # tests/test_contract.py reads CORE's OpenAPI contract from CORE's main
63
+ # branch on GitHub (no CORE checkout here).
60
64
  run: pytest tests/ -v
61
65
 
62
66
  build:
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.5
2
+ Name: core-cli
3
+ Version: 2.1.0
4
+ Summary: Consumer governance CLI for CORE — an HTTP client for governing repositories against CORE constitutional rules.
5
+ Project-URL: Homepage, https://github.com/DariuszNewecki/CORE
6
+ Project-URL: Repository, https://github.com/DariuszNewecki/core-cli
7
+ Project-URL: Bug Tracker, https://github.com/DariuszNewecki/core-cli/issues
8
+ Author-email: Dariusz Newecki <d.newecki@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: cli,code-quality,constitutional-ai,developer-tools,governance
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Requires-Python: >=3.12
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: pyyaml>=6.0
23
+ Requires-Dist: rich>=13.7
24
+ Requires-Dist: typer>=0.16
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # core-cli
31
+
32
+ The consumer command-line tool for [CORE](https://github.com/DariuszNewecki/CORE): review
33
+ and approve proposals, run checks and fixes on the repository CORE governs, onboard a
34
+ repository (BYOR), and work the assisted remediation lane. Its binary is called `core`.
35
+
36
+ `core-cli` is an HTTP client and nothing more. Every command talks to a **running CORE
37
+ API**, using only the routes in CORE's published
38
+ [OpenAPI contract](https://github.com/DariuszNewecki/CORE/blob/main/docs/reference/openapi.json);
39
+ it does not install or import CORE. The operator tool, `core-admin`, ships separately in
40
+ `core-runtime` and covers the CORE installation itself: secrets, vector store, database
41
+ sync ([which one do I need?](https://dariusznewecki.github.io/CORE/cli-reference/)).
42
+
43
+ ## Prerequisites
44
+
45
+ - Python 3.12+
46
+ - A reachable CORE API, which itself needs PostgreSQL and Qdrant. See CORE's
47
+ [getting started](https://dariusznewecki.github.io/CORE/getting-started/).
48
+
49
+ > **No authentication.** OSS CORE runs in trusted-localhost mode: the API binds to
50
+ > loopback and accepts every request. Anyone who can reach it can approve and execute
51
+ > proposals. Keep it on `127.0.0.1` and do not expose it on a shared network.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install core-cli
57
+ ```
58
+
59
+ Its only dependencies are `typer`, `rich`, `httpx` and `PyYAML`.
60
+
61
+ ## Configure
62
+
63
+ `core` uses `http://127.0.0.1:8000` unless you point it elsewhere:
64
+
65
+ ```bash
66
+ export CORE_API_URL=http://127.0.0.1:8000
67
+ ```
68
+
69
+ On the machine running CORE, `core` can use CORE's Unix socket instead of TCP. On that socket the kernel tells CORE who is calling; over TCP it cannot:
70
+
71
+ ```bash
72
+ export CORE_API_URL=unix:///run/core/api.sock
73
+ ```
74
+
75
+ ## Use
76
+
77
+ ```bash
78
+ core proposals list # proposals awaiting a decision
79
+ core proposals show <id> # risk assessment and planned changes
80
+ core lane list # delegated findings waiting for assisted remediation
81
+ core project onboard <path> # preview delivering the machinery floor (BYOR)
82
+ core project onboard <path> --write # deliver it
83
+ core <group> <command> --help # options for any command
84
+ ```
85
+
86
+ Commands that change files or data **preview by default** and act only with `--write`.
87
+
88
+ ## Commands
89
+
90
+ | Group | What it covers |
91
+ |---|---|
92
+ | `proposals` | `list`, `show`, `create`, `approve`, `reject`, `execute`, `integrate` |
93
+ | `lane` | `list`, `next`, `claim`, `propose`: assisted remediation of delegated findings |
94
+ | `project` | `onboard`, `scout`, `promote`: bring a repository under governance (BYOR) |
95
+ | `code` | quality and verification: `lint`, `format`, `test`, `check-imports`, `audit-duplicates`, … |
96
+ | `symbols` | `audit`, `fix-ids`, `resolve-duplicates` |
97
+ | `vectors` | `query`: semantic search over the governed repository |
98
+
99
+ Every command, option and default is in the generated
100
+ [`core` command reference](https://dariusznewecki.github.io/CORE/reference/core/).
101
+
102
+ ## Architecture
103
+
104
+ The split between the consumer CLI (`core`) and the operator CLI (`core-admin`) is
105
+ recorded in
106
+ [ADR-146](https://github.com/DariuszNewecki/CORE/blob/main/.specs/decisions/ADR-146-cli-consumer-operator-split.md):
107
+ `core` covers operations on the repository CORE governs; `core-admin` covers the CORE
108
+ installation. `tests/test_contract.py` checks every route this client calls against CORE's
109
+ OpenAPI contract.
110
+
111
+ ## License
112
+
113
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,84 @@
1
+ # core-cli
2
+
3
+ The consumer command-line tool for [CORE](https://github.com/DariuszNewecki/CORE): review
4
+ and approve proposals, run checks and fixes on the repository CORE governs, onboard a
5
+ repository (BYOR), and work the assisted remediation lane. Its binary is called `core`.
6
+
7
+ `core-cli` is an HTTP client and nothing more. Every command talks to a **running CORE
8
+ API**, using only the routes in CORE's published
9
+ [OpenAPI contract](https://github.com/DariuszNewecki/CORE/blob/main/docs/reference/openapi.json);
10
+ it does not install or import CORE. The operator tool, `core-admin`, ships separately in
11
+ `core-runtime` and covers the CORE installation itself: secrets, vector store, database
12
+ sync ([which one do I need?](https://dariusznewecki.github.io/CORE/cli-reference/)).
13
+
14
+ ## Prerequisites
15
+
16
+ - Python 3.12+
17
+ - A reachable CORE API, which itself needs PostgreSQL and Qdrant. See CORE's
18
+ [getting started](https://dariusznewecki.github.io/CORE/getting-started/).
19
+
20
+ > **No authentication.** OSS CORE runs in trusted-localhost mode: the API binds to
21
+ > loopback and accepts every request. Anyone who can reach it can approve and execute
22
+ > proposals. Keep it on `127.0.0.1` and do not expose it on a shared network.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install core-cli
28
+ ```
29
+
30
+ Its only dependencies are `typer`, `rich`, `httpx` and `PyYAML`.
31
+
32
+ ## Configure
33
+
34
+ `core` uses `http://127.0.0.1:8000` unless you point it elsewhere:
35
+
36
+ ```bash
37
+ export CORE_API_URL=http://127.0.0.1:8000
38
+ ```
39
+
40
+ On the machine running CORE, `core` can use CORE's Unix socket instead of TCP. On that socket the kernel tells CORE who is calling; over TCP it cannot:
41
+
42
+ ```bash
43
+ export CORE_API_URL=unix:///run/core/api.sock
44
+ ```
45
+
46
+ ## Use
47
+
48
+ ```bash
49
+ core proposals list # proposals awaiting a decision
50
+ core proposals show <id> # risk assessment and planned changes
51
+ core lane list # delegated findings waiting for assisted remediation
52
+ core project onboard <path> # preview delivering the machinery floor (BYOR)
53
+ core project onboard <path> --write # deliver it
54
+ core <group> <command> --help # options for any command
55
+ ```
56
+
57
+ Commands that change files or data **preview by default** and act only with `--write`.
58
+
59
+ ## Commands
60
+
61
+ | Group | What it covers |
62
+ |---|---|
63
+ | `proposals` | `list`, `show`, `create`, `approve`, `reject`, `execute`, `integrate` |
64
+ | `lane` | `list`, `next`, `claim`, `propose`: assisted remediation of delegated findings |
65
+ | `project` | `onboard`, `scout`, `promote`: bring a repository under governance (BYOR) |
66
+ | `code` | quality and verification: `lint`, `format`, `test`, `check-imports`, `audit-duplicates`, … |
67
+ | `symbols` | `audit`, `fix-ids`, `resolve-duplicates` |
68
+ | `vectors` | `query`: semantic search over the governed repository |
69
+
70
+ Every command, option and default is in the generated
71
+ [`core` command reference](https://dariusznewecki.github.io/CORE/reference/core/).
72
+
73
+ ## Architecture
74
+
75
+ The split between the consumer CLI (`core`) and the operator CLI (`core-admin`) is
76
+ recorded in
77
+ [ADR-146](https://github.com/DariuszNewecki/CORE/blob/main/.specs/decisions/ADR-146-cli-consumer-operator-split.md):
78
+ `core` covers operations on the repository CORE governs; `core-admin` covers the CORE
79
+ installation. `tests/test_contract.py` checks every route this client calls against CORE's
80
+ OpenAPI contract.
81
+
82
+ ## License
83
+
84
+ MIT. See [LICENSE](LICENSE).
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "core-cli"
7
- version = "1.0.2"
8
- description = "Consumer governance CLI for CORE — pure HTTP client for governing repositories against CORE constitutional rules."
7
+ version = "2.1.0"
8
+ description = "Consumer governance CLI for CORE — an HTTP client for governing repositories against CORE constitutional rules."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
11
11
  requires-python = ">=3.12"
@@ -23,8 +23,12 @@ classifiers = [
23
23
  "Intended Audience :: Developers",
24
24
  "Development Status :: 4 - Beta",
25
25
  ]
26
+ # HTTP client only: no dependency on core-runtime (ADR-146, amended 2026-10-03).
26
27
  dependencies = [
27
- "core-runtime>=2.9.0",
28
+ "httpx>=0.27",
29
+ "pyyaml>=6.0",
30
+ "rich>=13.7",
31
+ "typer>=0.16",
28
32
  ]
29
33
 
30
34
  [project.optional-dependencies]
@@ -48,3 +52,18 @@ packages = ["src/core_cli"]
48
52
  asyncio_mode = "auto"
49
53
  pythonpath = ["src"]
50
54
  testpaths = ["tests"]
55
+
56
+ [tool.ruff]
57
+ line-length = 88
58
+ target-version = "py312"
59
+ src = ["src"]
60
+
61
+ [tool.ruff.lint]
62
+ # B008: typer.Option / typer.Argument as parameter defaults is Typer's
63
+ # documented idiom, not a bug (CORE ignores it for the same reason).
64
+ ignore = ["B008"]
65
+
66
+ [tool.ruff.lint.isort]
67
+ # Two blank lines after imports, matching CORE's layout.
68
+ known-first-party = ["core_cli"]
69
+ lines-after-imports = 2
@@ -0,0 +1,349 @@
1
+ """HTTP client for the CORE API — core-cli's only link to CORE.
2
+
3
+ core-cli talks to a running CORE over HTTP and nothing else: it has no
4
+ dependency on core-runtime (ADR-146, amended 2026-10-03). Every route called
5
+ here is in CORE's published OpenAPI contract, ``docs/reference/openapi.json``
6
+ in the CORE repository; ``tests/test_contract.py`` checks that.
7
+
8
+ The base URL comes from the ``CORE_API_URL`` environment variable, defaulting
9
+ to the loopback address CORE binds to. ``unix:///path/to/api.sock`` selects
10
+ CORE's Unix socket instead of TCP. On that socket the kernel tells CORE who is
11
+ calling (ADR-132 D10.1 in the CORE repository); over TCP it cannot.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import os
18
+ from typing import Any
19
+
20
+ import httpx
21
+
22
+
23
+ DEFAULT_BASE_URL = "http://127.0.0.1:8000"
24
+ UNIX_SCHEME = "unix://"
25
+ # HTTP needs a host for the request line and Host header; over a Unix socket
26
+ # it names nothing, and CORE does not route on it.
27
+ _UNIX_HTTP_BASE = "http://localhost"
28
+ _TIMEOUT_SECONDS = 30.0
29
+ _LONG_TIMEOUT_SECONDS = 300.0
30
+ _POLL_INTERVAL_SECONDS = 1.0
31
+ _POLL_TERMINAL_STATES = frozenset({"completed", "failed"})
32
+
33
+
34
+ # ID: b50ad322-810a-4014-813e-a02a71cd55e7
35
+ def parse_base_url(base_url: str) -> tuple[str, str | None]:
36
+ """Split a ``CORE_API_URL`` value into (HTTP base URL, Unix socket path).
37
+
38
+ ``unix:///run/core/api.sock`` gives ``("http://localhost", "/run/core/api.sock")``;
39
+ any other value is an HTTP base URL and gives ``(base_url, None)``. The
40
+ socket path must be absolute, so a mistyped ``unix://`` URL fails here
41
+ rather than as a confusing connection error.
42
+ """
43
+ if not base_url.startswith(UNIX_SCHEME):
44
+ return base_url, None
45
+ socket_path = base_url[len(UNIX_SCHEME) :]
46
+ if not socket_path.startswith("/"):
47
+ raise ValueError(
48
+ f"CORE_API_URL {base_url!r}: a Unix socket URL needs an absolute "
49
+ "path, e.g. unix:///run/core/api.sock"
50
+ )
51
+ return _UNIX_HTTP_BASE, socket_path
52
+
53
+
54
+ # ID: e5aeda69-d26e-4353-81eb-83e998e968e9
55
+ class CoreApiError(httpx.HTTPStatusError):
56
+ """An error response (status >= 400) from the CORE API.
57
+
58
+ An ``httpx.HTTPStatusError``, so commands can branch on
59
+ ``exc.response.status_code``; ``status_code`` and ``detail`` (the API's
60
+ ``detail`` field, or the raw body) are also set directly.
61
+ """
62
+
63
+ def __init__(self, response: httpx.Response) -> None:
64
+ try:
65
+ detail = response.json().get("detail", response.text)
66
+ except ValueError:
67
+ detail = response.text
68
+ self.status_code = response.status_code
69
+ self.detail = detail
70
+ super().__init__(
71
+ f"API error {response.status_code}: {detail}",
72
+ request=response.request,
73
+ response=response,
74
+ )
75
+
76
+
77
+ # ID: 49ef4530-5b49-4ebb-a9f6-b59150780e1b
78
+ class CoreApiClient:
79
+ """Async client for the CORE API.
80
+
81
+ Methods mirror the API: flat methods for single routes, plus small
82
+ namespaces (``inspect``, ``lane``, ``project``, ``symbols``, ``vectors``)
83
+ for route families. A response with status >= 400 raises ``CoreApiError``.
84
+ """
85
+
86
+ def __init__(self, base_url: str | None = None) -> None:
87
+ self.base_url = base_url or os.environ.get("CORE_API_URL") or DEFAULT_BASE_URL
88
+ self._http_base, self.socket_path = parse_base_url(self.base_url)
89
+ self.inspect = _Inspect(self)
90
+ self.lane = _Lane(self)
91
+ self.project = _Project(self)
92
+ self.symbols = _Symbols(self)
93
+ self.vectors = _Vectors(self)
94
+
95
+ async def _request(
96
+ self,
97
+ method: str,
98
+ path: str,
99
+ *,
100
+ timeout: float = _TIMEOUT_SECONDS,
101
+ **kwargs: Any,
102
+ ) -> dict:
103
+ client_kwargs: dict[str, Any] = {"timeout": timeout}
104
+ if self.socket_path is not None:
105
+ client_kwargs["transport"] = httpx.AsyncHTTPTransport(uds=self.socket_path)
106
+ async with httpx.AsyncClient(**client_kwargs) as http:
107
+ response = await http.request(method, f"{self._http_base}{path}", **kwargs)
108
+ if response.status_code >= 400:
109
+ raise CoreApiError(response)
110
+ return response.json()
111
+
112
+ async def _poll_run(
113
+ self, run_id: str, timeout_seconds: float = _LONG_TIMEOUT_SECONDS
114
+ ) -> dict:
115
+ """Poll GET /v1/fix/runs/{run_id} until the run completes or fails."""
116
+ async with asyncio.timeout(timeout_seconds):
117
+ while True:
118
+ payload = await self._request("GET", f"/v1/fix/runs/{run_id}")
119
+ if payload.get("status") in _POLL_TERMINAL_STATES:
120
+ return payload
121
+ await asyncio.sleep(_POLL_INTERVAL_SECONDS)
122
+
123
+ # -- actions and fixes ----------------------------------------------
124
+
125
+ # ID: d5e64d0c-4a5f-4518-96d2-d15388306f00
126
+ async def list_actions(self) -> dict:
127
+ """GET /v1/actions — list the registered atomic actions."""
128
+ return await self._request("GET", "/v1/actions")
129
+
130
+ # ID: 7a49a17d-66a0-413b-b76a-5044c399f109
131
+ async def run_fix(
132
+ self,
133
+ fix_id: str,
134
+ target_files: list[str] | None = None,
135
+ write: bool = False,
136
+ params: dict[str, Any] | None = None,
137
+ ) -> dict:
138
+ """POST /v1/fix/run/{fix_id} — dispatch a fix action; poll with _poll_run."""
139
+ return await self._request(
140
+ "POST",
141
+ f"/v1/fix/run/{fix_id}",
142
+ json={
143
+ "target_files": target_files or [],
144
+ "write": write,
145
+ "params": params or {},
146
+ },
147
+ )
148
+
149
+ # -- checks on the governed repository ------------------------------
150
+
151
+ # ID: 80078450-7b0e-4a2e-b62c-b265d179aa74
152
+ async def lint(self) -> dict:
153
+ """POST /v1/lint — black --check and ruff check."""
154
+ return await self._request("POST", "/v1/lint", timeout=_LONG_TIMEOUT_SECONDS)
155
+
156
+ # ID: e07d0d0b-c466-4613-b63b-0df395730ba6
157
+ async def quality_imports(self) -> dict:
158
+ """POST /v1/quality/imports — import-resolution check."""
159
+ return await self._request("POST", "/v1/quality/imports")
160
+
161
+ # ID: 58ba647c-a7ce-4d58-966b-c21f55e7d3fe
162
+ async def quality_tests(self, path: str | None = None) -> dict:
163
+ """POST /v1/quality/tests — pytest, optionally scoped; poll with _poll_run."""
164
+ return await self._request("POST", "/v1/quality/tests", json={"path": path})
165
+
166
+ # ID: 2887d4f0-91a0-4db7-8df9-d33857113212
167
+ async def baseline(self, label: str = "default") -> dict:
168
+ """POST /v1/integrity/baseline — fingerprint src/."""
169
+ return await self._request(
170
+ "POST", "/v1/integrity/baseline", json={"label": label}
171
+ )
172
+
173
+ # ID: 7c860bc0-ec98-4a6f-9589-03c96bbe81b9
174
+ async def verify(self, label: str = "default") -> dict:
175
+ """POST /v1/integrity/verify — compare src/ against a baseline."""
176
+ return await self._request(
177
+ "POST", "/v1/integrity/verify", json={"label": label}
178
+ )
179
+
180
+ # ID: 2798cdc1-6743-4451-9c96-e6ecc2cb1d4e
181
+ async def integrate(self, commit_message: str) -> dict:
182
+ """POST /v1/integrate — stage, format/lint and commit the working tree."""
183
+ return await self._request(
184
+ "POST",
185
+ "/v1/integrate",
186
+ json={"commit_message": commit_message},
187
+ timeout=_LONG_TIMEOUT_SECONDS,
188
+ )
189
+
190
+ # -- proposals --------------------------------------------------------
191
+
192
+ # ID: 8bafbb59-8d23-45a5-8fc5-271b694bacbc
193
+ async def list_proposals(self, status: str | None = None, limit: int = 50) -> dict:
194
+ """GET /v1/proposals."""
195
+ params: dict[str, Any] = {"limit": limit}
196
+ if status is not None:
197
+ params["status"] = status
198
+ return await self._request("GET", "/v1/proposals", params=params)
199
+
200
+ # ID: 957be7d4-98b7-4e47-99d8-115921b3b12f
201
+ async def create_proposal(
202
+ self,
203
+ goal: str,
204
+ actions: list[dict] | None = None,
205
+ files: list[str] | None = None,
206
+ created_by: str = "cli_operator",
207
+ write: bool = True,
208
+ ) -> dict:
209
+ """POST /v1/proposals."""
210
+ return await self._request(
211
+ "POST",
212
+ "/v1/proposals",
213
+ json={
214
+ "goal": goal,
215
+ "actions": actions or [],
216
+ "files": files or [],
217
+ "created_by": created_by,
218
+ "write": write,
219
+ },
220
+ )
221
+
222
+ # ID: 4c806d5c-fe6e-4ac9-a0cc-dae70a01af93
223
+ async def get_proposal(self, proposal_id: str) -> dict:
224
+ """GET /v1/proposals/{proposal_id}."""
225
+ return await self._request("GET", f"/v1/proposals/{proposal_id}")
226
+
227
+ # ID: b01d56fa-f71a-472d-aa2d-c68e420689f0
228
+ async def approve_proposal(
229
+ self, proposal_id: str, approved_by: str, approval_authority: str
230
+ ) -> dict:
231
+ """POST /v1/proposals/{proposal_id}/approve."""
232
+ return await self._request(
233
+ "POST",
234
+ f"/v1/proposals/{proposal_id}/approve",
235
+ json={"approved_by": approved_by, "approval_authority": approval_authority},
236
+ )
237
+
238
+ # ID: 0e5c0e42-34bd-44c0-848f-62a1c0f78037
239
+ async def reject_proposal(self, proposal_id: str, reason: str) -> dict:
240
+ """POST /v1/proposals/{proposal_id}/reject."""
241
+ return await self._request(
242
+ "POST", f"/v1/proposals/{proposal_id}/reject", json={"reason": reason}
243
+ )
244
+
245
+ # ID: bef4a752-8d94-4d20-89e3-fc1ba79899d9
246
+ async def execute_proposal(self, proposal_id: str, write: bool = False) -> dict:
247
+ """POST /v1/proposals/{proposal_id}/execute."""
248
+ return await self._request(
249
+ "POST", f"/v1/proposals/{proposal_id}/execute", json={"write": write}
250
+ )
251
+
252
+
253
+ class _Namespace:
254
+ def __init__(self, client: CoreApiClient) -> None:
255
+ self._client = client
256
+
257
+
258
+ class _Inspect(_Namespace):
259
+ async def analysis_duplicates(self, threshold: float = 0.85) -> dict:
260
+ """GET /v1/analysis/duplicates."""
261
+ return await self._client._request(
262
+ "GET",
263
+ "/v1/analysis/duplicates",
264
+ params={"threshold": threshold},
265
+ timeout=_LONG_TIMEOUT_SECONDS,
266
+ )
267
+
268
+ async def analysis_bridges(self, consuming: str | None = None) -> dict:
269
+ """GET /v1/analysis/bridges."""
270
+ params: dict[str, Any] = {}
271
+ if consuming is not None:
272
+ params["consuming"] = consuming
273
+ return await self._client._request("GET", "/v1/analysis/bridges", params=params)
274
+
275
+
276
+ class _Lane(_Namespace):
277
+ async def list_delegated(self, limit: int = 50) -> dict:
278
+ """GET /v1/lane."""
279
+ return await self._client._request("GET", "/v1/lane", params={"limit": limit})
280
+
281
+ async def next_delegated(self) -> dict:
282
+ """GET /v1/lane/next."""
283
+ return await self._client._request("GET", "/v1/lane/next")
284
+
285
+ async def get_delegated(self, finding_id: str) -> dict:
286
+ """GET /v1/lane/{finding_id}."""
287
+ return await self._client._request("GET", f"/v1/lane/{finding_id}")
288
+
289
+ async def claim(self, finding_id: str, agent: str) -> dict:
290
+ """POST /v1/lane/{finding_id}/claim."""
291
+ return await self._client._request(
292
+ "POST", f"/v1/lane/{finding_id}/claim", params={"agent": agent}
293
+ )
294
+
295
+ async def propose(
296
+ self, finding_id: str, patch: str, validation_run_id: str
297
+ ) -> dict:
298
+ """POST /v1/lane/{finding_id}/propose."""
299
+ return await self._client._request(
300
+ "POST",
301
+ f"/v1/lane/{finding_id}/propose",
302
+ json={"patch": patch, "validation_run_id": validation_run_id},
303
+ )
304
+
305
+
306
+ class _Project(_Namespace):
307
+ async def scout(self, path: str, reset: bool = False) -> dict:
308
+ """POST /v1/project/scout."""
309
+ return await self._client._request(
310
+ "POST", "/v1/project/scout", json={"path": path, "reset": reset}
311
+ )
312
+
313
+ async def onboard(
314
+ self, path: str, write: bool = False, stage: bool = False
315
+ ) -> dict:
316
+ """POST /v1/project/onboard."""
317
+ return await self._client._request(
318
+ "POST",
319
+ "/v1/project/onboard",
320
+ json={"path": path, "write": write, "stage": stage},
321
+ )
322
+
323
+ async def promote(self, path: str) -> dict:
324
+ """POST /v1/project/onboard/promote."""
325
+ return await self._client._request(
326
+ "POST", "/v1/project/onboard/promote", json={"path": path}
327
+ )
328
+
329
+
330
+ class _Symbols(_Namespace):
331
+ async def get_unassigned(self) -> dict:
332
+ """GET /v1/symbols/unassigned."""
333
+ return await self._client._request("GET", "/v1/symbols/unassigned")
334
+
335
+ async def get_drift(self) -> dict:
336
+ """GET /v1/symbols/drift."""
337
+ return await self._client._request("GET", "/v1/symbols/drift")
338
+
339
+
340
+ class _Vectors(_Namespace):
341
+ async def query(
342
+ self, query: str, collection: str = "policies", limit: int = 5
343
+ ) -> dict:
344
+ """POST /v1/vectors/query — semantic search over the governed repository."""
345
+ return await self._client._request(
346
+ "POST",
347
+ "/v1/vectors/query",
348
+ json={"query": query, "collection": collection, "limit": limit},
349
+ )