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.
Files changed (46) hide show
  1. caokit/__init__.py +3 -0
  2. caokit/adapters.py +259 -0
  3. caokit/cli.py +704 -0
  4. caokit/company.py +115 -0
  5. caokit/config.py +191 -0
  6. caokit/data/adapters/antigravity.toml +35 -0
  7. caokit/data/adapters/claude.toml +38 -0
  8. caokit/data/adapters/codex.toml +33 -0
  9. caokit/data/roles/engineer.md +90 -0
  10. caokit/data/roles/general.md +177 -0
  11. caokit/data/roles/inspector.md +55 -0
  12. caokit/data/roles/scout.md +77 -0
  13. caokit/data/scaffold/company/BACKLOG.md +5 -0
  14. caokit/data/scaffold/company/COMPANY.md +28 -0
  15. caokit/data/scaffold/company/DECISIONS.md +5 -0
  16. caokit/data/scaffold/company/README.md +16 -0
  17. caokit/data/scaffold/company/TEAM.md +7 -0
  18. caokit/data/scaffold/company/contracts/ARTIFACT_CONTRACT.md +44 -0
  19. caokit/data/scaffold/company/contracts/COMMUNICATION_CONTRACT.md +38 -0
  20. caokit/data/scaffold/company/contracts/COMPANY_CONTRACT.md +42 -0
  21. caokit/data/scaffold/company/contracts/REVIEW_CONTRACT.md +36 -0
  22. caokit/data/scaffold/company/contracts/SAFETY_CONTRACT.md +40 -0
  23. caokit/data/scaffold/company/contracts/STATE_CONTRACT.md +31 -0
  24. caokit/data/scaffold/company/contracts/TASK_CONTRACT.md +26 -0
  25. caokit/data/scaffold/company/prompts/first-run.md +18 -0
  26. caokit/data/scaffold/company/tasks/TASK-0001-smoke-test.md +109 -0
  27. caokit/data/scaffold/company/tasks/TASK_TEMPLATE.md +73 -0
  28. caokit/doctor.py +287 -0
  29. caokit/gitignore.py +36 -0
  30. caokit/init_project.py +183 -0
  31. caokit/install.py +242 -0
  32. caokit/launch.py +102 -0
  33. caokit/project.py +359 -0
  34. caokit/render.py +129 -0
  35. caokit/reset.py +208 -0
  36. caokit/roster.py +95 -0
  37. caokit/schedule.py +104 -0
  38. caokit/stack.py +117 -0
  39. caokit/tasks.py +180 -0
  40. caokit/worktrees.py +107 -0
  41. caokit-0.3.0.dev0.dist-info/METADATA +382 -0
  42. caokit-0.3.0.dev0.dist-info/RECORD +46 -0
  43. caokit-0.3.0.dev0.dist-info/WHEEL +5 -0
  44. caokit-0.3.0.dev0.dist-info/entry_points.txt +2 -0
  45. caokit-0.3.0.dev0.dist-info/licenses/LICENSE +21 -0
  46. caokit-0.3.0.dev0.dist-info/top_level.txt +1 -0
caokit/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """caokit: chain-of-command agent company kit for CAO."""
2
+
3
+ __version__ = "0.3.0.dev0"
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
+ ]