cohorte-engine 1.0.0a1__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.
- cohorte/__init__.py +3 -0
- cohorte/adapters/__init__.py +1 -0
- cohorte/adapters/claude.py +438 -0
- cohorte/adapters/codex.py +638 -0
- cohorte/adapters/events.py +157 -0
- cohorte/adapters/figma.py +75 -0
- cohorte/adapters/git.py +146 -0
- cohorte/adapters/graphify.py +153 -0
- cohorte/adapters/hosting.py +166 -0
- cohorte/adapters/native_login.py +126 -0
- cohorte/adapters/providers.py +64 -0
- cohorte/adapters/serena.py +181 -0
- cohorte/application/__init__.py +3 -0
- cohorte/application/alignment.py +192 -0
- cohorte/application/context.py +367 -0
- cohorte/application/delivery.py +235 -0
- cohorte/application/discovery.py +106 -0
- cohorte/application/durable.py +231 -0
- cohorte/application/fleet.py +395 -0
- cohorte/application/intake.py +137 -0
- cohorte/application/kanban.py +154 -0
- cohorte/application/maintenance.py +475 -0
- cohorte/application/metrics.py +194 -0
- cohorte/application/migration.py +282 -0
- cohorte/application/multisurface.py +560 -0
- cohorte/application/patch.py +217 -0
- cohorte/application/preparation.py +349 -0
- cohorte/application/readiness.py +44 -0
- cohorte/application/service.py +126 -0
- cohorte/application/vertical.py +377 -0
- cohorte/assets/prompts/README.md +4 -0
- cohorte/cli/__init__.py +1 -0
- cohorte/cli/main.py +1576 -0
- cohorte/domain/__init__.py +1 -0
- cohorte/domain/auth.py +125 -0
- cohorte/domain/errors.py +67 -0
- cohorte/domain/evidence.py +100 -0
- cohorte/domain/models.py +532 -0
- cohorte/domain/redaction.py +36 -0
- cohorte/execution/__init__.py +3 -0
- cohorte/execution/checks.py +107 -0
- cohorte/execution/scheduler.py +53 -0
- cohorte/persistence/__init__.py +3 -0
- cohorte/persistence/sqlite.py +1204 -0
- cohorte/protocol/__init__.py +3 -0
- cohorte/protocol/models.py +52 -0
- cohorte/protocol/rpc.py +332 -0
- cohorte/py.typed +1 -0
- cohorte/service/__init__.py +1 -0
- cohorte/service/host.py +316 -0
- cohorte/service/windows_pipe.py +235 -0
- cohorte_engine-1.0.0a1.dist-info/METADATA +200 -0
- cohorte_engine-1.0.0a1.dist-info/RECORD +56 -0
- cohorte_engine-1.0.0a1.dist-info/WHEEL +4 -0
- cohorte_engine-1.0.0a1.dist-info/entry_points.txt +2 -0
- cohorte_engine-1.0.0a1.dist-info/licenses/LICENSE +4 -0
cohorte/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""External adapters. Imports never start a provider or authentication flow."""
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from collections.abc import Callable, Mapping
|
|
9
|
+
from contextlib import suppress
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, NoReturn, TypeVar
|
|
13
|
+
|
|
14
|
+
from cohorte.adapters.events import AgentEvents, AgentEventSink, claude_usage
|
|
15
|
+
from cohorte.application.durable import RunStopped
|
|
16
|
+
from cohorte.application.preparation import (
|
|
17
|
+
BrainstormContribution,
|
|
18
|
+
BrainstormPerspectiveTurn,
|
|
19
|
+
BrainstormSynthesis,
|
|
20
|
+
BrainstormSynthesisTurn,
|
|
21
|
+
)
|
|
22
|
+
from cohorte.application.vertical import AgentReport, AgentReview
|
|
23
|
+
from cohorte.domain.auth import (
|
|
24
|
+
AccountStatus,
|
|
25
|
+
BillingEvidence,
|
|
26
|
+
Capability,
|
|
27
|
+
CapabilitySet,
|
|
28
|
+
ConnectionState,
|
|
29
|
+
EffectiveAuthMode,
|
|
30
|
+
require_subscription,
|
|
31
|
+
)
|
|
32
|
+
from cohorte.domain.errors import CohorteError, ErrorCode
|
|
33
|
+
from cohorte.domain.models import RunStatus, StrictModel
|
|
34
|
+
|
|
35
|
+
StructuredOutput = TypeVar("StructuredOutput", bound=StrictModel)
|
|
36
|
+
_ROUTING_KEYS = frozenset(
|
|
37
|
+
{
|
|
38
|
+
"ANTHROPIC_API_KEY",
|
|
39
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
40
|
+
"ANTHROPIC_BASE_URL",
|
|
41
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
42
|
+
"CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
|
|
43
|
+
"CLAUDE_CODE_OAUTH_SCOPES",
|
|
44
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
45
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
46
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
47
|
+
"AWS_PROFILE",
|
|
48
|
+
"AWS_ACCESS_KEY_ID",
|
|
49
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def sanitized_claude_env(source: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
55
|
+
env = dict(os.environ if source is None else source)
|
|
56
|
+
for key in _ROUTING_KEYS:
|
|
57
|
+
env.pop(key, None)
|
|
58
|
+
return env
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def claude_capabilities(version: str | None) -> CapabilitySet:
|
|
62
|
+
supported = Capability(support="supported", version=version)
|
|
63
|
+
unknown = Capability(support="unknown", version=version)
|
|
64
|
+
return CapabilitySet(
|
|
65
|
+
structured_output=supported,
|
|
66
|
+
streaming=supported,
|
|
67
|
+
tool_permissions=supported,
|
|
68
|
+
user_questions=unknown,
|
|
69
|
+
interrupt=unknown,
|
|
70
|
+
session_resume=unknown,
|
|
71
|
+
read_only_role=unknown,
|
|
72
|
+
native_auth_status=supported,
|
|
73
|
+
model_catalog=unknown,
|
|
74
|
+
usage_reporting=unknown,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def raise_claude_result_error(result: str | None) -> NoReturn:
|
|
79
|
+
if result and "organization has disabled claude subscription access" in result.lower():
|
|
80
|
+
raise CohorteError(
|
|
81
|
+
ErrorCode.PROVIDER_UNAVAILABLE,
|
|
82
|
+
"Claude subscription access is disabled for this organization",
|
|
83
|
+
"the phase cannot start through the subscription",
|
|
84
|
+
remediation="ask the organization admin to enable access or select an eligible Claude account",
|
|
85
|
+
)
|
|
86
|
+
raise CohorteError(
|
|
87
|
+
ErrorCode.OUTPUT_INVALID,
|
|
88
|
+
"Claude turn failed",
|
|
89
|
+
"the workflow phase was rejected",
|
|
90
|
+
remediation="inspect provider diagnostics and retry the phase",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def inspect_claude_account(
|
|
95
|
+
executable: str | None = None, environ: Mapping[str, str] | None = None
|
|
96
|
+
) -> AccountStatus:
|
|
97
|
+
source = os.environ if environ is None else environ
|
|
98
|
+
executable = executable or shutil.which("claude")
|
|
99
|
+
now = datetime.now(UTC)
|
|
100
|
+
version: str | None = None
|
|
101
|
+
if executable is None:
|
|
102
|
+
connection, mode, evidence = (
|
|
103
|
+
ConnectionState.UNAVAILABLE,
|
|
104
|
+
EffectiveAuthMode.UNKNOWN,
|
|
105
|
+
BillingEvidence.UNVERIFIED,
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
env = sanitized_claude_env(source)
|
|
109
|
+
try:
|
|
110
|
+
version_result = subprocess.run(
|
|
111
|
+
[executable, "--version"],
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
check=False,
|
|
115
|
+
timeout=5,
|
|
116
|
+
env=env,
|
|
117
|
+
)
|
|
118
|
+
if version_result.returncode == 0:
|
|
119
|
+
version = (version_result.stdout or version_result.stderr).strip()[:200]
|
|
120
|
+
result = subprocess.run(
|
|
121
|
+
[executable, "auth", "status", "--json"],
|
|
122
|
+
capture_output=True,
|
|
123
|
+
text=True,
|
|
124
|
+
check=False,
|
|
125
|
+
timeout=10,
|
|
126
|
+
env=env,
|
|
127
|
+
)
|
|
128
|
+
data = json.loads(result.stdout) if result.returncode == 0 else {}
|
|
129
|
+
if not isinstance(data, dict):
|
|
130
|
+
data = {}
|
|
131
|
+
except (OSError, subprocess.TimeoutExpired, json.JSONDecodeError):
|
|
132
|
+
data = {}
|
|
133
|
+
if data.get("loggedIn") is True:
|
|
134
|
+
connection = ConnectionState.CONNECTED
|
|
135
|
+
if (
|
|
136
|
+
data.get("authMethod") == "claude.ai"
|
|
137
|
+
and data.get("apiProvider") == "firstParty"
|
|
138
|
+
and data.get("subscriptionType")
|
|
139
|
+
):
|
|
140
|
+
mode, evidence = EffectiveAuthMode.SUBSCRIPTION, BillingEvidence.RUNTIME_REPORTED
|
|
141
|
+
elif data.get("apiProvider") not in (None, "firstParty"):
|
|
142
|
+
mode, evidence = EffectiveAuthMode.THIRD_PARTY, BillingEvidence.RUNTIME_REPORTED
|
|
143
|
+
elif data.get("authMethod") == "api_key":
|
|
144
|
+
mode, evidence = EffectiveAuthMode.API, BillingEvidence.RUNTIME_REPORTED
|
|
145
|
+
else:
|
|
146
|
+
mode, evidence = EffectiveAuthMode.UNKNOWN, BillingEvidence.UNVERIFIED
|
|
147
|
+
else:
|
|
148
|
+
connection, mode, evidence = (
|
|
149
|
+
ConnectionState.LOGIN_REQUIRED,
|
|
150
|
+
EffectiveAuthMode.UNKNOWN,
|
|
151
|
+
BillingEvidence.UNVERIFIED,
|
|
152
|
+
)
|
|
153
|
+
# An override in the parent process makes the effective route ambiguous even
|
|
154
|
+
# when the isolated CLI status command reports a subscription account.
|
|
155
|
+
if any(source.get(key) for key in _ROUTING_KEYS) and connection == ConnectionState.CONNECTED:
|
|
156
|
+
mode, evidence = EffectiveAuthMode.UNKNOWN, BillingEvidence.UNVERIFIED
|
|
157
|
+
return AccountStatus(
|
|
158
|
+
account_id="claude-native",
|
|
159
|
+
connection_state=connection,
|
|
160
|
+
effective_auth_mode=mode,
|
|
161
|
+
billing_evidence=evidence,
|
|
162
|
+
capabilities=claude_capabilities(version),
|
|
163
|
+
runtime_version=version,
|
|
164
|
+
checked_at=now,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ClaudeAdapter:
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
cwd: Path,
|
|
172
|
+
environ: Mapping[str, str] | None = None,
|
|
173
|
+
model: str | None = None,
|
|
174
|
+
stop_requested: Callable[[], RunStatus | None] | None = None,
|
|
175
|
+
event_sink: AgentEventSink | None = None,
|
|
176
|
+
) -> None:
|
|
177
|
+
self.cwd = cwd.resolve(strict=True)
|
|
178
|
+
self.environ = dict(os.environ if environ is None else environ)
|
|
179
|
+
self.model = model
|
|
180
|
+
self.stop_requested = stop_requested
|
|
181
|
+
self.event_sink = event_sink
|
|
182
|
+
|
|
183
|
+
def _require_subscription(self) -> AccountStatus:
|
|
184
|
+
status = inspect_claude_account(environ=self.environ)
|
|
185
|
+
require_subscription(status)
|
|
186
|
+
return status
|
|
187
|
+
|
|
188
|
+
def _structured_turn_with_session(
|
|
189
|
+
self,
|
|
190
|
+
workspace: Path,
|
|
191
|
+
prompt: str,
|
|
192
|
+
output: type[StructuredOutput],
|
|
193
|
+
read_only: bool,
|
|
194
|
+
phase: str = "probe",
|
|
195
|
+
) -> tuple[StructuredOutput, str]:
|
|
196
|
+
if self.stop_requested is not None:
|
|
197
|
+
stopped = self.stop_requested()
|
|
198
|
+
if stopped is not None:
|
|
199
|
+
raise RunStopped(stopped.value)
|
|
200
|
+
self._require_subscription()
|
|
201
|
+
events = AgentEvents(self.event_sink, "claude", phase, read_only)
|
|
202
|
+
try:
|
|
203
|
+
from claude_agent_sdk import (
|
|
204
|
+
ClaudeAgentOptions,
|
|
205
|
+
ClaudeSDKClient,
|
|
206
|
+
HookContext,
|
|
207
|
+
HookInput,
|
|
208
|
+
HookMatcher,
|
|
209
|
+
ResultMessage,
|
|
210
|
+
)
|
|
211
|
+
from claude_agent_sdk.types import SyncHookJSONOutput
|
|
212
|
+
except ImportError as error:
|
|
213
|
+
raise CohorteError(
|
|
214
|
+
ErrorCode.PROVIDER_UNAVAILABLE,
|
|
215
|
+
"Claude Agent SDK is not installed",
|
|
216
|
+
"the phase cannot start",
|
|
217
|
+
remediation="install the claude optional dependency",
|
|
218
|
+
) from error
|
|
219
|
+
workspace = workspace.resolve(strict=True)
|
|
220
|
+
tools = ["Read", "Grep", "Glob"]
|
|
221
|
+
if not read_only:
|
|
222
|
+
tools += ["Edit", "Write"]
|
|
223
|
+
|
|
224
|
+
async def guard_tool(
|
|
225
|
+
hook_input: HookInput, _tool_use_id: str | None, _context: HookContext
|
|
226
|
+
) -> SyncHookJSONOutput:
|
|
227
|
+
name = hook_input.get("tool_name")
|
|
228
|
+
params = hook_input.get("tool_input", {})
|
|
229
|
+
key = "file_path" if name in {"Read", "Edit", "Write"} else "path"
|
|
230
|
+
path = params.get(key) if isinstance(params, dict) else None
|
|
231
|
+
permitted = name in tools and (
|
|
232
|
+
(name in {"Grep", "Glob"} and path is None)
|
|
233
|
+
or (isinstance(path, str) and bool(path))
|
|
234
|
+
)
|
|
235
|
+
if permitted and path is not None:
|
|
236
|
+
target = (workspace / path).resolve()
|
|
237
|
+
permitted = target == workspace or workspace in target.parents
|
|
238
|
+
if name in {"Read", "Grep", "Glob", "Edit", "Write"}:
|
|
239
|
+
events.tool(
|
|
240
|
+
str(name).lower(),
|
|
241
|
+
decision="allow" if permitted else "deny",
|
|
242
|
+
outcome="unknown" if permitted else "denied",
|
|
243
|
+
source="pre_tool_hook",
|
|
244
|
+
)
|
|
245
|
+
if permitted:
|
|
246
|
+
return {
|
|
247
|
+
"hookSpecificOutput": {
|
|
248
|
+
"hookEventName": "PreToolUse",
|
|
249
|
+
"permissionDecision": "allow",
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
"hookSpecificOutput": {
|
|
254
|
+
"hookEventName": "PreToolUse",
|
|
255
|
+
"permissionDecision": "deny",
|
|
256
|
+
"permissionDecisionReason": "repository path and tool policy",
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
options = ClaudeAgentOptions(
|
|
261
|
+
cwd=str(workspace),
|
|
262
|
+
model=self.model,
|
|
263
|
+
fallback_model=None,
|
|
264
|
+
tools=tools,
|
|
265
|
+
allowed_tools=tools,
|
|
266
|
+
disallowed_tools=["Bash", "NotebookEdit", "Task"]
|
|
267
|
+
+ ([] if not read_only else ["Edit", "Write"]),
|
|
268
|
+
permission_mode="dontAsk",
|
|
269
|
+
setting_sources=[],
|
|
270
|
+
mcp_servers={},
|
|
271
|
+
strict_mcp_config=True,
|
|
272
|
+
hooks={
|
|
273
|
+
"PreToolUse": [HookMatcher(matcher="Read|Grep|Glob|Edit|Write", hooks=[guard_tool])]
|
|
274
|
+
},
|
|
275
|
+
max_turns=20 if read_only else 40,
|
|
276
|
+
env=sanitized_claude_env(self.environ),
|
|
277
|
+
output_format={"type": "json_schema", "schema": output.model_json_schema()},
|
|
278
|
+
)
|
|
279
|
+
events.started()
|
|
280
|
+
|
|
281
|
+
async def run() -> tuple[Any, str]:
|
|
282
|
+
async with ClaudeSDKClient(options=options) as client:
|
|
283
|
+
stop_status: RunStatus | None = None
|
|
284
|
+
|
|
285
|
+
async def watch_stop() -> None:
|
|
286
|
+
nonlocal stop_status
|
|
287
|
+
assert self.stop_requested is not None
|
|
288
|
+
while True:
|
|
289
|
+
status = self.stop_requested()
|
|
290
|
+
if status is not None:
|
|
291
|
+
stop_status = status
|
|
292
|
+
await client.interrupt()
|
|
293
|
+
return
|
|
294
|
+
await asyncio.sleep(0.2)
|
|
295
|
+
|
|
296
|
+
watcher = (
|
|
297
|
+
asyncio.create_task(watch_stop()) if self.stop_requested is not None else None
|
|
298
|
+
)
|
|
299
|
+
try:
|
|
300
|
+
await client.query(prompt)
|
|
301
|
+
async for message in client.receive_response():
|
|
302
|
+
if watcher is not None and watcher.done():
|
|
303
|
+
watcher.result()
|
|
304
|
+
if stop_status is not None:
|
|
305
|
+
raise RunStopped(stop_status.value)
|
|
306
|
+
if isinstance(message, ResultMessage):
|
|
307
|
+
claude_usage(
|
|
308
|
+
events,
|
|
309
|
+
getattr(message, "usage", None),
|
|
310
|
+
getattr(message, "total_cost_usd", None),
|
|
311
|
+
)
|
|
312
|
+
denials = getattr(message, "permission_denials", None)
|
|
313
|
+
if isinstance(denials, list) and denials:
|
|
314
|
+
events.permission_denials(len(denials))
|
|
315
|
+
if message.is_error:
|
|
316
|
+
raise_claude_result_error(message.result)
|
|
317
|
+
return (
|
|
318
|
+
message.structured_output
|
|
319
|
+
if message.structured_output is not None
|
|
320
|
+
else message.result,
|
|
321
|
+
message.session_id,
|
|
322
|
+
)
|
|
323
|
+
if watcher is not None and watcher.done():
|
|
324
|
+
watcher.result()
|
|
325
|
+
if stop_status is not None:
|
|
326
|
+
raise RunStopped(stop_status.value)
|
|
327
|
+
finally:
|
|
328
|
+
if watcher is not None:
|
|
329
|
+
watcher.cancel()
|
|
330
|
+
with suppress(asyncio.CancelledError):
|
|
331
|
+
await watcher
|
|
332
|
+
raise CohorteError(
|
|
333
|
+
ErrorCode.OUTPUT_INVALID,
|
|
334
|
+
"Claude turn ended without a result",
|
|
335
|
+
"the workflow phase was rejected",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
raw, session_ref = asyncio.run(run())
|
|
340
|
+
try:
|
|
341
|
+
# SDK structured_output is a dict; JSON mode accepts wire enums.
|
|
342
|
+
value = output.model_validate_json(raw if isinstance(raw, str) else json.dumps(raw))
|
|
343
|
+
except (ValueError, TypeError) as error:
|
|
344
|
+
raise CohorteError(
|
|
345
|
+
ErrorCode.OUTPUT_INVALID,
|
|
346
|
+
f"Claude returned invalid structured output for {output.__name__}",
|
|
347
|
+
"the workflow phase was rejected",
|
|
348
|
+
remediation="inspect provider diagnostics and retry the phase",
|
|
349
|
+
details={"validation_error": str(error)[:2000]},
|
|
350
|
+
) from error
|
|
351
|
+
except RunStopped:
|
|
352
|
+
events.stopped()
|
|
353
|
+
raise
|
|
354
|
+
except Exception as error:
|
|
355
|
+
events.failed(error)
|
|
356
|
+
raise
|
|
357
|
+
events.finished(session_ref)
|
|
358
|
+
return value, session_ref
|
|
359
|
+
|
|
360
|
+
def _structured_turn(
|
|
361
|
+
self,
|
|
362
|
+
workspace: Path,
|
|
363
|
+
prompt: str,
|
|
364
|
+
output: type[StructuredOutput],
|
|
365
|
+
read_only: bool,
|
|
366
|
+
phase: str,
|
|
367
|
+
) -> StructuredOutput:
|
|
368
|
+
return self._structured_turn_with_session(workspace, prompt, output, read_only, phase)[0]
|
|
369
|
+
|
|
370
|
+
def brainstorm_perspective(
|
|
371
|
+
self, workspace: Path, prompt: str, perspective: str
|
|
372
|
+
) -> BrainstormPerspectiveTurn:
|
|
373
|
+
contribution, session_ref = self._structured_turn_with_session(
|
|
374
|
+
workspace, prompt, BrainstormContribution, True, "brainstorm_perspective"
|
|
375
|
+
)
|
|
376
|
+
return BrainstormPerspectiveTurn(
|
|
377
|
+
session_ref=session_ref,
|
|
378
|
+
contribution=contribution.model_copy(
|
|
379
|
+
update={"contribution_id": perspective, "perspective": perspective}
|
|
380
|
+
),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def brainstorm_synthesis(self, workspace: Path, prompt: str) -> BrainstormSynthesisTurn:
|
|
384
|
+
synthesis, session_ref = self._structured_turn_with_session(
|
|
385
|
+
workspace, prompt, BrainstormSynthesis, True, "brainstorm_synthesis"
|
|
386
|
+
)
|
|
387
|
+
return BrainstormSynthesisTurn(session_ref=session_ref, synthesis=synthesis)
|
|
388
|
+
|
|
389
|
+
def build(self, workspace: Path, prompt: str) -> AgentReport:
|
|
390
|
+
return self._structured_turn(workspace, prompt, AgentReport, False, "build")
|
|
391
|
+
|
|
392
|
+
def review(self, workspace: Path, prompt: str) -> AgentReview:
|
|
393
|
+
return self._structured_turn(workspace, prompt, AgentReview, True, "review")
|
|
394
|
+
|
|
395
|
+
def fix(self, workspace: Path, prompt: str) -> AgentReport:
|
|
396
|
+
return self._structured_turn(workspace, prompt, AgentReport, False, "fix")
|
|
397
|
+
|
|
398
|
+
def verify_live(self) -> dict[str, Any]:
|
|
399
|
+
status = self._require_subscription()
|
|
400
|
+
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, ResultMessage
|
|
401
|
+
|
|
402
|
+
async def run() -> str | None:
|
|
403
|
+
options = ClaudeAgentOptions(
|
|
404
|
+
cwd=str(self.cwd),
|
|
405
|
+
tools=[],
|
|
406
|
+
allowed_tools=[],
|
|
407
|
+
disallowed_tools=["Bash", "Edit", "Write", "NotebookEdit", "Task"],
|
|
408
|
+
permission_mode="dontAsk",
|
|
409
|
+
setting_sources=[],
|
|
410
|
+
mcp_servers={},
|
|
411
|
+
strict_mcp_config=True,
|
|
412
|
+
env=sanitized_claude_env(self.environ),
|
|
413
|
+
max_turns=1,
|
|
414
|
+
)
|
|
415
|
+
async with ClaudeSDKClient(options=options) as client:
|
|
416
|
+
await client.query("Reply with exactly G0_CLAUDE_OK. Do not use any tool.")
|
|
417
|
+
async for message in client.receive_response():
|
|
418
|
+
if isinstance(message, ResultMessage):
|
|
419
|
+
if message.is_error:
|
|
420
|
+
raise_claude_result_error(message.result)
|
|
421
|
+
return message.result
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
if (asyncio.run(run()) or "").strip() != "G0_CLAUDE_OK":
|
|
425
|
+
raise CohorteError(
|
|
426
|
+
ErrorCode.OUTPUT_INVALID,
|
|
427
|
+
"Claude live probe returned an unexpected response",
|
|
428
|
+
"the account/runtime combination is not verified",
|
|
429
|
+
)
|
|
430
|
+
return {
|
|
431
|
+
"provider": "claude",
|
|
432
|
+
"account_id": status.account_id,
|
|
433
|
+
"effective_auth_mode": status.effective_auth_mode.value,
|
|
434
|
+
"billing_evidence": status.billing_evidence.value,
|
|
435
|
+
"runtime_version": status.runtime_version,
|
|
436
|
+
"probe": "passed",
|
|
437
|
+
"checked_at": datetime.now(UTC).isoformat(),
|
|
438
|
+
}
|