caokit 0.3.0.dev0__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.
- caokit/__init__.py +3 -0
- caokit/adapters.py +259 -0
- caokit/cli.py +704 -0
- caokit/company.py +115 -0
- caokit/config.py +191 -0
- caokit/data/adapters/antigravity.toml +35 -0
- caokit/data/adapters/claude.toml +38 -0
- caokit/data/adapters/codex.toml +33 -0
- caokit/data/roles/engineer.md +90 -0
- caokit/data/roles/general.md +177 -0
- caokit/data/roles/inspector.md +55 -0
- caokit/data/roles/scout.md +77 -0
- caokit/data/scaffold/company/BACKLOG.md +5 -0
- caokit/data/scaffold/company/COMPANY.md +28 -0
- caokit/data/scaffold/company/DECISIONS.md +5 -0
- caokit/data/scaffold/company/README.md +16 -0
- caokit/data/scaffold/company/TEAM.md +7 -0
- caokit/data/scaffold/company/contracts/ARTIFACT_CONTRACT.md +44 -0
- caokit/data/scaffold/company/contracts/COMMUNICATION_CONTRACT.md +38 -0
- caokit/data/scaffold/company/contracts/COMPANY_CONTRACT.md +42 -0
- caokit/data/scaffold/company/contracts/REVIEW_CONTRACT.md +36 -0
- caokit/data/scaffold/company/contracts/SAFETY_CONTRACT.md +40 -0
- caokit/data/scaffold/company/contracts/STATE_CONTRACT.md +31 -0
- caokit/data/scaffold/company/contracts/TASK_CONTRACT.md +26 -0
- caokit/data/scaffold/company/prompts/first-run.md +18 -0
- caokit/data/scaffold/company/tasks/TASK-0001-smoke-test.md +109 -0
- caokit/data/scaffold/company/tasks/TASK_TEMPLATE.md +73 -0
- caokit/doctor.py +287 -0
- caokit/gitignore.py +36 -0
- caokit/init_project.py +183 -0
- caokit/install.py +242 -0
- caokit/launch.py +102 -0
- caokit/project.py +359 -0
- caokit/render.py +129 -0
- caokit/reset.py +208 -0
- caokit/roster.py +95 -0
- caokit/schedule.py +104 -0
- caokit/stack.py +117 -0
- caokit/tasks.py +180 -0
- caokit/worktrees.py +107 -0
- caokit-0.3.0.dev0.dist-info/METADATA +382 -0
- caokit-0.3.0.dev0.dist-info/RECORD +46 -0
- caokit-0.3.0.dev0.dist-info/WHEEL +5 -0
- caokit-0.3.0.dev0.dist-info/entry_points.txt +2 -0
- caokit-0.3.0.dev0.dist-info/licenses/LICENSE +21 -0
- caokit-0.3.0.dev0.dist-info/top_level.txt +1 -0
caokit/__init__.py
ADDED
caokit/adapters.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Tool adapters: declarative TOML describing how a CLI fills roles.
|
|
2
|
+
|
|
3
|
+
An adapter says which CAO provider a tool maps to, whether CAO can enforce its
|
|
4
|
+
tool limits (native) or only ask nicely (soft), how to detect/authenticate it,
|
|
5
|
+
how to validate model IDs, and which roles it may fill.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
import tomllib
|
|
11
|
+
from collections.abc import Callable, Sequence
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from importlib import resources
|
|
14
|
+
from typing import Any, Protocol
|
|
15
|
+
|
|
16
|
+
# The four roles of the chain of command. Order is the display/render order.
|
|
17
|
+
ROLES = ("general", "engineer", "scout", "inspector")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AdapterError(Exception):
|
|
21
|
+
"""Raised when an adapter is unknown or a tool cannot answer a query."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ProcessResult(Protocol):
|
|
25
|
+
"""The subset of ``subprocess.CompletedProcess`` that caokit reads.
|
|
26
|
+
|
|
27
|
+
Declared structurally so tests can pass tiny fakes and production passes
|
|
28
|
+
the real thing.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
returncode: Process exit status.
|
|
32
|
+
stdout: Captured standard output (text).
|
|
33
|
+
stderr: Captured standard error (text).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
returncode: int
|
|
37
|
+
stdout: Any
|
|
38
|
+
stderr: Any
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# A runner executes an argv and returns its result. Injected so that no unit
|
|
42
|
+
# test needs a real CLI installed.
|
|
43
|
+
Runner = Callable[[Sequence[str]], ProcessResult]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def default_runner(argv: Sequence[str], timeout: int = 60) -> ProcessResult:
|
|
47
|
+
"""Run ``argv`` for real, capturing text output, with a timeout (60s by default).
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
argv: Command and arguments; never passed through a shell.
|
|
51
|
+
timeout: Seconds to wait before giving up.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The completed process with ``stdout``/``stderr`` as text.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
OSError: The binary does not exist or cannot be executed.
|
|
58
|
+
subprocess.TimeoutExpired: The command ran longer than ``timeout`` seconds.
|
|
59
|
+
"""
|
|
60
|
+
return subprocess.run(list(argv), capture_output=True, text=True, timeout=timeout)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class RoleCap:
|
|
65
|
+
"""What one adapter can do for one role.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
can_fill: False means the role is rejected for this tool.
|
|
69
|
+
recommended_model: Advice only; users may override with a warning.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
can_fill: bool # False means the role is rejected for this tool
|
|
73
|
+
recommended_model: str | None # advice only; users may override with a warning
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class Adapter:
|
|
78
|
+
"""Parsed contents of ``data/adapters/<tool>.toml``.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
id: Tool identifier, equal to the TOML file stem (e.g. ``claude``).
|
|
82
|
+
cao_provider: Value for the CAO profile's ``provider:`` field.
|
|
83
|
+
enforcement: ``native`` (CAO denies tools) or ``soft`` (prompt-only).
|
|
84
|
+
status: ``active`` or ``parked`` (parked tools cannot be selected).
|
|
85
|
+
default_vendor: Vendor used when a model ID matches no prefix.
|
|
86
|
+
binary: Executable name looked up on PATH.
|
|
87
|
+
version_cmd: Command that prints the tool version.
|
|
88
|
+
auth_cmd: Command that succeeds only when logged in; empty if none.
|
|
89
|
+
models_source: ``cmd`` (ask the tool) or ``shape`` (regex/alias only).
|
|
90
|
+
models_list_cmd: Command listing model IDs when ``models_source`` is ``cmd``.
|
|
91
|
+
model_aliases: Accepted short model names for shape-validated tools.
|
|
92
|
+
model_shape: Regex a model ID must match for shape-validated tools.
|
|
93
|
+
vendor_map: Model-ID prefix to vendor; ``""`` is the catch-all.
|
|
94
|
+
roles: Per-role capability and recommended model.
|
|
95
|
+
frontmatter: Fixed extra frontmatter fields for rendered profiles.
|
|
96
|
+
notes: Text appended to every rendered profile as "Tool notes".
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
id: str
|
|
100
|
+
cao_provider: str # value for the CAO profile's ``provider:`` field
|
|
101
|
+
enforcement: str # "native" (CAO denies tools) or "soft" (prompt-only)
|
|
102
|
+
status: str # "active" or "parked" (parked tools cannot be selected)
|
|
103
|
+
default_vendor: str # vendor used when a model ID matches no prefix
|
|
104
|
+
binary: str
|
|
105
|
+
version_cmd: list[str]
|
|
106
|
+
auth_cmd: list[str] # empty when the tool has no cheap non-interactive auth check
|
|
107
|
+
models_source: str # "cmd" = ask the tool, "shape" = regex/alias check only
|
|
108
|
+
models_list_cmd: list[str]
|
|
109
|
+
model_aliases: list[str]
|
|
110
|
+
model_shape: str
|
|
111
|
+
vendor_map: dict[str, str] # model-ID prefix -> vendor; "" is the catch-all
|
|
112
|
+
frontmatter: dict[str, str] # fixed extra frontmatter fields for this tool
|
|
113
|
+
roles: dict[str, RoleCap]
|
|
114
|
+
notes: str = "" # appended to every rendered profile as "Tool notes"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def adapter_ids() -> list[str]:
|
|
118
|
+
"""Return the IDs of every adapter shipped with the package.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Sorted adapter IDs (TOML file stems), parked adapters included.
|
|
122
|
+
"""
|
|
123
|
+
d = resources.files("caokit") / "data" / "adapters"
|
|
124
|
+
return sorted(p.name[:-5] for p in d.iterdir() if p.name.endswith(".toml"))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def load_adapter(tool: str) -> Adapter:
|
|
128
|
+
"""Load the adapter for ``tool``.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
tool: Adapter ID such as ``claude``; only ``[a-z0-9]+`` is accepted.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The parsed adapter.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
AdapterError: ``tool`` is malformed or no such adapter ships.
|
|
138
|
+
"""
|
|
139
|
+
# Restrict to [a-z0-9]+ so a tool name can never traverse out of data/adapters.
|
|
140
|
+
if not re.fullmatch(r"[a-z0-9]+", tool or ""):
|
|
141
|
+
raise AdapterError(f"unknown tool {tool!r}")
|
|
142
|
+
p = resources.files("caokit") / "data" / "adapters" / (tool + ".toml")
|
|
143
|
+
if not p.is_file():
|
|
144
|
+
raise AdapterError(f"unknown tool {tool!r}")
|
|
145
|
+
d = tomllib.loads(p.read_text(encoding="utf-8"))
|
|
146
|
+
m = d.get("models", {})
|
|
147
|
+
# Roles missing from the TOML default to "cannot fill", never to "can".
|
|
148
|
+
roles = {
|
|
149
|
+
r: RoleCap(
|
|
150
|
+
can_fill=d.get("roles", {}).get(r, {}).get("can_fill", "no") == "yes",
|
|
151
|
+
recommended_model=d.get("roles", {}).get(r, {}).get("recommended_model"),
|
|
152
|
+
)
|
|
153
|
+
for r in ROLES
|
|
154
|
+
}
|
|
155
|
+
return Adapter(
|
|
156
|
+
id=d["id"],
|
|
157
|
+
cao_provider=d["cao_provider"],
|
|
158
|
+
enforcement=d["enforcement"],
|
|
159
|
+
status=d["status"],
|
|
160
|
+
default_vendor=d["default_vendor"],
|
|
161
|
+
binary=d["detect"]["binary"],
|
|
162
|
+
version_cmd=list(d["detect"]["version_cmd"]),
|
|
163
|
+
auth_cmd=list(d["detect"].get("auth_cmd", [])),
|
|
164
|
+
models_source=m.get("source", "shape"),
|
|
165
|
+
models_list_cmd=list(m.get("list_cmd", [])),
|
|
166
|
+
model_aliases=list(m.get("aliases", [])),
|
|
167
|
+
model_shape=m.get("shape", ".*"),
|
|
168
|
+
vendor_map=dict(m.get("vendor_map", {})),
|
|
169
|
+
frontmatter=dict(d.get("frontmatter", {})),
|
|
170
|
+
roles=roles,
|
|
171
|
+
notes=d.get("notes", {}).get("text", ""),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def vendor_of(adapter: Adapter, model: str | None) -> str:
|
|
176
|
+
"""Return the vendor of a model, e.g. ``claude-*`` on Antigravity is anthropic.
|
|
177
|
+
|
|
178
|
+
This is what lets the cross-provider rule see that an Antigravity inspector
|
|
179
|
+
running a Claude model is not independent of a Claude engineer.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
adapter: The tool the model runs on.
|
|
183
|
+
model: Model ID, or None for the tool's default.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
The vendor name, e.g. ``anthropic``; the adapter default if unmatched.
|
|
187
|
+
"""
|
|
188
|
+
if model:
|
|
189
|
+
# Longest prefix first, so a specific prefix beats a shorter one.
|
|
190
|
+
for prefix, vendor in sorted(adapter.vendor_map.items(), key=lambda kv: -len(kv[0])):
|
|
191
|
+
if prefix and model.startswith(prefix):
|
|
192
|
+
return vendor
|
|
193
|
+
return adapter.vendor_map.get("", adapter.default_vendor)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def list_models(adapter: Adapter, runner: Runner = default_runner) -> list[str]:
|
|
197
|
+
"""Return the model IDs the tool reports.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
adapter: The tool to query.
|
|
201
|
+
runner: Executes the list command; injected for tests.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Model IDs, or an empty list for tools validated by shape only.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
AdapterError: The tool cannot be run, is not logged in, or listed nothing.
|
|
208
|
+
"""
|
|
209
|
+
if adapter.models_source != "cmd":
|
|
210
|
+
return [] # shape-validated tools have no list to fetch
|
|
211
|
+
cmd = " ".join(adapter.models_list_cmd)
|
|
212
|
+
try:
|
|
213
|
+
r = runner(adapter.models_list_cmd)
|
|
214
|
+
except (OSError, subprocess.SubprocessError) as e:
|
|
215
|
+
raise AdapterError(f"cannot run {cmd}: {e}") from e
|
|
216
|
+
if r.returncode != 0:
|
|
217
|
+
detail = (r.stderr or r.stdout).strip()[:200]
|
|
218
|
+
raise AdapterError(f"`{cmd}` failed (logged in?): {detail}")
|
|
219
|
+
ids = []
|
|
220
|
+
for line in r.stdout.splitlines():
|
|
221
|
+
first = line.split("\t")[0].strip() # `agy models` prints "<id>\t<label>"
|
|
222
|
+
# Keep only ID-shaped tokens; this drops banners like "Fetching available models...".
|
|
223
|
+
if first and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", first):
|
|
224
|
+
ids.append(first)
|
|
225
|
+
if not ids:
|
|
226
|
+
raise AdapterError(f"`{cmd}` listed no models")
|
|
227
|
+
return ids
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def validate_model(
|
|
231
|
+
adapter: Adapter, model: str, runner: Runner = default_runner
|
|
232
|
+
) -> tuple[list[str], list[str]]:
|
|
233
|
+
"""Check a model ID against the tool; return ``(errors, warnings)``.
|
|
234
|
+
|
|
235
|
+
Tools that can list models reject unlisted IDs (errors). Tools that cannot
|
|
236
|
+
(Claude) only warn when the ID is not a known alias or ``claude-*`` shape.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
adapter: The tool the model is for.
|
|
240
|
+
model: Model ID to check.
|
|
241
|
+
runner: Executes the list command; injected for tests.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
A ``(errors, warnings)`` pair of message lists.
|
|
245
|
+
"""
|
|
246
|
+
if adapter.models_source == "cmd":
|
|
247
|
+
try:
|
|
248
|
+
known = list_models(adapter, runner)
|
|
249
|
+
except AdapterError as e:
|
|
250
|
+
return [f"cannot validate model {model!r} for {adapter.id}: {e}"], []
|
|
251
|
+
if model not in known:
|
|
252
|
+
cmd = " ".join(adapter.models_list_cmd)
|
|
253
|
+
return [f"model {model!r} is not listed by `{cmd}`"], []
|
|
254
|
+
return [], []
|
|
255
|
+
if model in adapter.model_aliases or re.fullmatch(adapter.model_shape, model):
|
|
256
|
+
return [], []
|
|
257
|
+
return [], [
|
|
258
|
+
f"model {model!r} for {adapter.id} is not a known alias or claude-* ID; cannot verify it"
|
|
259
|
+
]
|