graphite-code 0.3.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.
Files changed (112) hide show
  1. graphite/__init__.py +41 -0
  2. graphite/__main__.py +7 -0
  3. graphite/_cleanup_worker.py +525 -0
  4. graphite/activation.py +164 -0
  5. graphite/agent_hooks.py +577 -0
  6. graphite/agent_settings.py +226 -0
  7. graphite/analyze.py +146 -0
  8. graphite/answer_contract.py +420 -0
  9. graphite/bootstrap.py +210 -0
  10. graphite/buildlock.py +99 -0
  11. graphite/cache.py +131 -0
  12. graphite/channel.py +1325 -0
  13. graphite/cli.py +3053 -0
  14. graphite/cluster.py +111 -0
  15. graphite/config.py +209 -0
  16. graphite/context.py +355 -0
  17. graphite/daemon.py +745 -0
  18. graphite/daemon_health.py +733 -0
  19. graphite/debt.py +118 -0
  20. graphite/dependency_install.py +1597 -0
  21. graphite/detach.py +33 -0
  22. graphite/doctor.py +678 -0
  23. graphite/doctor_probes.py +2100 -0
  24. graphite/engine_identity.py +238 -0
  25. graphite/export/__init__.py +6 -0
  26. graphite/export/html.py +244 -0
  27. graphite/export/json.py +39 -0
  28. graphite/export/md.py +68 -0
  29. graphite/extract/__init__.py +4 -0
  30. graphite/extract/ast.py +1964 -0
  31. graphite/freshness.py +127 -0
  32. graphite/git.py +406 -0
  33. graphite/graph.py +117 -0
  34. graphite/graph_io.py +188 -0
  35. graphite/health.py +147 -0
  36. graphite/hook_entry.py +68 -0
  37. graphite/hookinstall.py +224 -0
  38. graphite/hookshim.py +86 -0
  39. graphite/incident_ledger.py +247 -0
  40. graphite/ingest.py +279 -0
  41. graphite/init.py +791 -0
  42. graphite/io.py +32 -0
  43. graphite/listing.py +51 -0
  44. graphite/llm.py +518 -0
  45. graphite/llm_probe.py +157 -0
  46. graphite/mcp.py +7 -0
  47. graphite/mcp_server.py +450 -0
  48. graphite/natural_query.py +252 -0
  49. graphite/overlays.py +713 -0
  50. graphite/probe_process.py +879 -0
  51. graphite/probe_workspace.py +728 -0
  52. graphite/process_contracts.py +22 -0
  53. graphite/provider_observer.py +397 -0
  54. graphite/query.py +646 -0
  55. graphite/query_plan.py +97 -0
  56. graphite/replacement_audit.py +291 -0
  57. graphite/resolve.py +660 -0
  58. graphite/review.py +782 -0
  59. graphite/routing/__init__.py +5 -0
  60. graphite/routing/approval.py +362 -0
  61. graphite/routing/classifier.py +169 -0
  62. graphite/routing/claude_executor.py +419 -0
  63. graphite/routing/claude_probe.py +102 -0
  64. graphite/routing/cli_identity.py +84 -0
  65. graphite/routing/codex_executor.py +383 -0
  66. graphite/routing/codex_probe.py +93 -0
  67. graphite/routing/context_builder.py +327 -0
  68. graphite/routing/contracts.py +802 -0
  69. graphite/routing/diff_policy.py +468 -0
  70. graphite/routing/edit_apply.py +166 -0
  71. graphite/routing/effort.py +43 -0
  72. graphite/routing/lifecycle.py +771 -0
  73. graphite/routing/lifecycle_operator.py +227 -0
  74. graphite/routing/lifecycle_service.py +555 -0
  75. graphite/routing/lifecycle_storage.py +977 -0
  76. graphite/routing/ollama_executor.py +341 -0
  77. graphite/routing/ollama_probe.py +72 -0
  78. graphite/routing/openrouter_executor.py +338 -0
  79. graphite/routing/openrouter_probe.py +188 -0
  80. graphite/routing/policy.py +815 -0
  81. graphite/routing/probe_runner.py +543 -0
  82. graphite/routing/process_runner.py +523 -0
  83. graphite/routing/profiles.py +554 -0
  84. graphite/routing/prompt.py +58 -0
  85. graphite/routing/registry.py +444 -0
  86. graphite/routing/route_pool.py +629 -0
  87. graphite/routing/route_pool_execution.py +275 -0
  88. graphite/routing/schema_validation.py +169 -0
  89. graphite/routing/service.py +1263 -0
  90. graphite/routing/settings.py +99 -0
  91. graphite/routing/shadow.py +201 -0
  92. graphite/routing/storage.py +4001 -0
  93. graphite/routing/telemetry.py +346 -0
  94. graphite/routing/worktree.py +259 -0
  95. graphite/routing/zai_edit.py +113 -0
  96. graphite/routing/zai_executor.py +191 -0
  97. graphite/routing/zai_probe.py +126 -0
  98. graphite/savings.py +84 -0
  99. graphite/ts_bridge.py +142 -0
  100. graphite/ts_resolver.mjs +314 -0
  101. graphite/typescript_activation.py +1586 -0
  102. graphite/usage_ledger.py +156 -0
  103. graphite/validation.py +148 -0
  104. graphite/watch.py +167 -0
  105. graphite/windows_job.py +368 -0
  106. graphite/windows_startup.py +144 -0
  107. graphite/windows_task.py +212 -0
  108. graphite_code-0.3.0.dist-info/METADATA +743 -0
  109. graphite_code-0.3.0.dist-info/RECORD +112 -0
  110. graphite_code-0.3.0.dist-info/WHEEL +4 -0
  111. graphite_code-0.3.0.dist-info/entry_points.txt +3 -0
  112. graphite_code-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,22 @@
1
+ """Pure process-launch contract builders shared across platforms."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Mapping
5
+ import threading
6
+
7
+ # Cooperative same-process boundary: repository-owned Windows launchers that
8
+ # enable inheritance must hold this lock. Normal launches expose child handles
9
+ # only for CreateProcessW and constrain them with HANDLE_LIST. The lock cannot
10
+ # serialize untrusted native threads; restoration failures are retried and fail
11
+ # the launch, retaining ownership for cleanup rather than claiming isolation.
12
+ WINDOWS_PROCESS_CREATION_LOCK = threading.RLock()
13
+
14
+
15
+ def build_windows_environment_block(environment: Mapping[str, str]) -> str:
16
+ """Build a sorted, double-NUL-terminated Unicode environment block."""
17
+ entries: list[str] = []
18
+ for key, value in sorted(environment.items(), key=lambda item: item[0].upper()):
19
+ if not key or "=" in key or "\0" in key or "\0" in value:
20
+ raise ValueError("invalid environment")
21
+ entries.append(f"{key}={value}")
22
+ return "\0".join(entries) + "\0\0"
@@ -0,0 +1,397 @@
1
+ """Bounded provider lifecycle observation without graph or execution authority."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import math
6
+ import re
7
+ from collections import Counter
8
+ from collections.abc import Callable, Iterable
9
+ from dataclasses import dataclass
10
+
11
+ from .routing.contracts import Effort
12
+ from .routing.lifecycle import (
13
+ LifecycleProviderId,
14
+ LifecycleReasonCode,
15
+ ProviderCompatibilityPolicy,
16
+ ProviderRuntimeIdentity,
17
+ RuntimeKind,
18
+ )
19
+ from .routing.lifecycle_service import (
20
+ LifecycleServiceError,
21
+ ProviderLifecycleService,
22
+ VerificationManifest,
23
+ )
24
+ from .routing.probe_runner import ProviderProbeError
25
+
26
+ _HEX_64 = re.compile(r"^[0-9a-f]{64}$")
27
+ _TARGET_ID = re.compile(r"^[a-z0-9][a-z0-9._-]{0,127}$")
28
+ _PROBE_REASON_CODES = frozenset(
29
+ {
30
+ "probe_auth_unhealthy",
31
+ "probe_capability_missing",
32
+ "probe_dns_busy",
33
+ "probe_endpoint_invalid",
34
+ "probe_executable_invalid",
35
+ "probe_failed",
36
+ "probe_http_status",
37
+ "probe_identity_changed",
38
+ "probe_model_unavailable",
39
+ "probe_protocol_invalid",
40
+ "probe_request_invalid",
41
+ "probe_response_too_large",
42
+ "probe_timeout",
43
+ "probe_unavailable",
44
+ "probe_version_invalid",
45
+ }
46
+ )
47
+ _PERSISTENCE_REASONS = frozenset(
48
+ {
49
+ "lifecycle_invalidation_failed",
50
+ "lifecycle_observation_invalid",
51
+ "lifecycle_persistence_failed",
52
+ "verification_manifest_invalid",
53
+ }
54
+ )
55
+
56
+ Probe = Callable[[float, int], ProviderRuntimeIdentity]
57
+
58
+
59
+ @dataclass(frozen=True, slots=True)
60
+ class VerificationManifestRequest:
61
+ """Exact hash-only inputs for optional non-executing manifest preparation."""
62
+
63
+ requested_model: str
64
+ expected_effective_model: str
65
+ effort: Effort
66
+ max_input_tokens: int
67
+ max_output_tokens: int
68
+ timeout_seconds: int
69
+ expires_at: int
70
+ fixture_repository_commit: str
71
+ graph_fingerprint: str
72
+ prompt_contract_hash: str
73
+ response_contract_hash: str
74
+ max_cost_microunits: int | None = None
75
+
76
+
77
+ @dataclass(frozen=True, slots=True)
78
+ class ProviderObserverOptions:
79
+ """Hard bounds for a provider observation worker."""
80
+
81
+ enabled_providers: tuple[LifecycleProviderId, ...] = tuple(LifecycleProviderId)
82
+ interval_seconds: float = 300.0
83
+ timeout_seconds: float = 15.0
84
+ max_observations_per_cycle: int = 4
85
+ backoff_cap_seconds: float = 3_600.0
86
+ jitter_ratio: float = 0.1
87
+
88
+ def validate(self) -> None:
89
+ try:
90
+ providers = tuple(LifecycleProviderId(value) for value in self.enabled_providers)
91
+ except (TypeError, ValueError):
92
+ raise ValueError("observer_enabled_providers_invalid") from None
93
+ if len(providers) > len(LifecycleProviderId) or len(set(providers)) != len(providers):
94
+ raise ValueError("observer_enabled_providers_invalid")
95
+ if not _bounded_number(self.interval_seconds, 1.0, 86_400.0):
96
+ raise ValueError("observer_interval_invalid")
97
+ if not _bounded_number(self.timeout_seconds, 0.1, 30.0):
98
+ raise ValueError("observer_timeout_invalid")
99
+ if (
100
+ isinstance(self.max_observations_per_cycle, bool)
101
+ or not isinstance(self.max_observations_per_cycle, int)
102
+ or not 1 <= self.max_observations_per_cycle <= len(LifecycleProviderId)
103
+ ):
104
+ raise ValueError("observer_cycle_limit_invalid")
105
+ if (
106
+ not _bounded_number(self.backoff_cap_seconds, self.interval_seconds, 86_400.0)
107
+ or not _bounded_number(self.jitter_ratio, 0.0, 0.5)
108
+ ):
109
+ raise ValueError("observer_backoff_invalid")
110
+
111
+
112
+ def _bounded_number(value: object, minimum: float, maximum: float) -> bool:
113
+ return (
114
+ not isinstance(value, bool)
115
+ and isinstance(value, (int, float))
116
+ and math.isfinite(value)
117
+ and minimum <= float(value) <= maximum
118
+ )
119
+
120
+
121
+ @dataclass(frozen=True, slots=True)
122
+ class ProviderObservationTarget:
123
+ """One repository lifecycle boundary and its non-inference probe."""
124
+
125
+ target_id: str
126
+ boundary_digest: str
127
+ provider: LifecycleProviderId
128
+ runtime_kind: RuntimeKind
129
+ policy: ProviderCompatibilityPolicy
130
+ lifecycle_service: ProviderLifecycleService
131
+ probe: Probe
132
+ machine_cache_key: str | None = None
133
+ verification_request: VerificationManifestRequest | None = None
134
+
135
+ def __post_init__(self) -> None:
136
+ try:
137
+ provider = LifecycleProviderId(self.provider)
138
+ runtime_kind = RuntimeKind(self.runtime_kind)
139
+ except (TypeError, ValueError):
140
+ raise ValueError("observer_target_invalid") from None
141
+ if (
142
+ not isinstance(self.target_id, str)
143
+ or _TARGET_ID.fullmatch(self.target_id) is None
144
+ or not isinstance(self.boundary_digest, str)
145
+ or _HEX_64.fullmatch(self.boundary_digest) is None
146
+ or not isinstance(self.policy, ProviderCompatibilityPolicy)
147
+ or self.policy.provider is not provider
148
+ or self.policy.runtime_kind is not runtime_kind
149
+ or not callable(self.probe)
150
+ or (
151
+ self.verification_request is not None
152
+ and not isinstance(
153
+ self.verification_request, VerificationManifestRequest
154
+ )
155
+ )
156
+ ):
157
+ raise ValueError("observer_target_invalid")
158
+ if self.machine_cache_key is not None and (
159
+ runtime_kind is not RuntimeKind.LOCAL_CLI
160
+ or not isinstance(self.machine_cache_key, str)
161
+ or _HEX_64.fullmatch(self.machine_cache_key) is None
162
+ ):
163
+ raise ValueError("observer_cache_scope_invalid")
164
+ object.__setattr__(self, "provider", provider)
165
+ object.__setattr__(self, "runtime_kind", runtime_kind)
166
+
167
+
168
+ @dataclass(frozen=True, slots=True)
169
+ class ProviderObservationSummary:
170
+ attempted: int
171
+ deferred: int
172
+ succeeded: int
173
+ failed: int
174
+ state_counts: dict[str, int]
175
+ reason_counts: dict[str, int]
176
+ prepared_manifests: tuple[VerificationManifest, ...] = ()
177
+
178
+ def to_status(self) -> dict[str, object]:
179
+ """Return the bounded aggregate safe for daemon status and health."""
180
+ return {
181
+ "attempted": self.attempted,
182
+ "deferred": self.deferred,
183
+ "succeeded": self.succeeded,
184
+ "failed": self.failed,
185
+ "state_counts": dict(self.state_counts),
186
+ "reason_counts": dict(self.reason_counts),
187
+ }
188
+
189
+
190
+ @dataclass(slots=True)
191
+ class _Schedule:
192
+ next_due: float = 0.0
193
+ consecutive_failures: int = 0
194
+
195
+
196
+ class ProviderObserver:
197
+ """Observe due providers once and persist only lifecycle transitions."""
198
+
199
+ def __init__(self, options: ProviderObserverOptions | None = None) -> None:
200
+ self.options = options or ProviderObserverOptions()
201
+ self.options.validate()
202
+ self._enabled = frozenset(
203
+ LifecycleProviderId(value) for value in self.options.enabled_providers
204
+ )
205
+ self._schedules: dict[str, _Schedule] = {}
206
+
207
+ def run_cycle(
208
+ self,
209
+ targets: Iterable[ProviderObservationTarget],
210
+ *,
211
+ now: float,
212
+ ) -> ProviderObservationSummary:
213
+ if not _bounded_number(now, 0.0, 10**12):
214
+ raise ValueError("observer_time_invalid")
215
+ normalized = tuple(targets)
216
+ if len(normalized) > 1_000 or any(
217
+ not isinstance(target, ProviderObservationTarget) for target in normalized
218
+ ):
219
+ raise ValueError("observer_targets_invalid")
220
+ identifiers = [target.target_id for target in normalized]
221
+ if len(set(identifiers)) != len(identifiers):
222
+ raise ValueError("observer_targets_invalid")
223
+ current_identifiers = set(identifiers)
224
+ for target_id in tuple(self._schedules):
225
+ if target_id not in current_identifiers:
226
+ del self._schedules[target_id]
227
+
228
+ enabled = [target for target in normalized if target.provider in self._enabled]
229
+ due = sorted(
230
+ (
231
+ target
232
+ for target in enabled
233
+ if self._schedules.setdefault(target.target_id, _Schedule()).next_due
234
+ <= float(now)
235
+ ),
236
+ key=lambda target: target.target_id,
237
+ )
238
+ selected = due[: self.options.max_observations_per_cycle]
239
+ cache: dict[str, ProviderRuntimeIdentity | ProviderProbeError] = {}
240
+ states: Counter[str] = Counter()
241
+ reasons: Counter[str] = Counter()
242
+ succeeded = 0
243
+ prepared_manifests: list[VerificationManifest] = []
244
+
245
+ for target in selected:
246
+ schedule = self._schedules[target.target_id]
247
+ try:
248
+ identity = self._probe(target, int(now), cache)
249
+ result = target.lifecycle_service.observe(
250
+ boundary_digest=target.boundary_digest,
251
+ identity=identity,
252
+ policy=target.policy,
253
+ )
254
+ if target.verification_request is not None:
255
+ if result.state.value != "verification_required":
256
+ raise LifecycleServiceError("verification_manifest_invalid")
257
+ request = target.verification_request
258
+ prepared_manifests.append(
259
+ target.lifecycle_service.prepare_verification_manifest(
260
+ boundary_digest=target.boundary_digest,
261
+ requested_model=request.requested_model,
262
+ expected_effective_model=request.expected_effective_model,
263
+ effort=request.effort,
264
+ max_input_tokens=request.max_input_tokens,
265
+ max_output_tokens=request.max_output_tokens,
266
+ timeout_seconds=request.timeout_seconds,
267
+ expires_at=request.expires_at,
268
+ fixture_repository_commit=request.fixture_repository_commit,
269
+ graph_fingerprint=request.graph_fingerprint,
270
+ prompt_contract_hash=request.prompt_contract_hash,
271
+ response_contract_hash=request.response_contract_hash,
272
+ max_cost_microunits=request.max_cost_microunits,
273
+ )
274
+ )
275
+ except ProviderProbeError as exc:
276
+ reason = self._record_unavailable(target, int(now), exc)
277
+ states["unavailable"] += 1
278
+ reasons[reason] += 1
279
+ self._back_off(target.target_id, schedule, float(now))
280
+ except LifecycleServiceError as exc:
281
+ reasons[_lifecycle_reason(exc)] += 1
282
+ self._back_off(target.target_id, schedule, float(now))
283
+ except Exception:
284
+ reasons["lifecycle_persistence_failed"] += 1
285
+ self._back_off(target.target_id, schedule, float(now))
286
+ else:
287
+ states[result.state.value] += 1
288
+ reasons[result.reason.value] += 1
289
+ succeeded += 1
290
+ schedule.consecutive_failures = 0
291
+ schedule.next_due = float(now) + self._jittered_delay(
292
+ target.target_id, self.options.interval_seconds, 0
293
+ )
294
+
295
+ attempted = len(selected)
296
+ return ProviderObservationSummary(
297
+ attempted=attempted,
298
+ deferred=max(0, len(due) - attempted),
299
+ succeeded=succeeded,
300
+ failed=attempted - succeeded,
301
+ state_counts=dict(sorted(states.items())),
302
+ reason_counts=dict(sorted(reasons.items())),
303
+ prepared_manifests=tuple(prepared_manifests),
304
+ )
305
+
306
+ def _probe(
307
+ self,
308
+ target: ProviderObservationTarget,
309
+ observed_at: int,
310
+ cache: dict[str, ProviderRuntimeIdentity | ProviderProbeError],
311
+ ) -> ProviderRuntimeIdentity:
312
+ cache_key = target.machine_cache_key
313
+ if cache_key is not None and cache_key in cache:
314
+ cached = cache[cache_key]
315
+ if isinstance(cached, ProviderProbeError):
316
+ raise cached
317
+ identity = cached
318
+ else:
319
+ try:
320
+ identity = target.probe(float(self.options.timeout_seconds), observed_at)
321
+ except ProviderProbeError as exc:
322
+ sanitized = ProviderProbeError(_probe_reason(exc.code))
323
+ if cache_key is not None:
324
+ cache[cache_key] = sanitized
325
+ raise sanitized from None
326
+ except Exception:
327
+ sanitized = ProviderProbeError("probe_failed")
328
+ if cache_key is not None:
329
+ cache[cache_key] = sanitized
330
+ raise sanitized from None
331
+ if cache_key is not None:
332
+ cache[cache_key] = identity
333
+ if (
334
+ not isinstance(identity, ProviderRuntimeIdentity)
335
+ or identity.provider is not target.provider
336
+ or identity.runtime_kind is not target.runtime_kind
337
+ or identity.policy_version != target.policy.policy_version
338
+ or identity.observed_at != observed_at
339
+ ):
340
+ raise ProviderProbeError("probe_protocol_invalid")
341
+ return identity
342
+
343
+ def _record_unavailable(
344
+ self,
345
+ target: ProviderObservationTarget,
346
+ observed_at: int,
347
+ error: ProviderProbeError,
348
+ ) -> str:
349
+ probe_reason = _probe_reason(error.code)
350
+ lifecycle_reason = (
351
+ LifecycleReasonCode.CREDENTIAL_UNHEALTHY
352
+ if probe_reason == "probe_auth_unhealthy"
353
+ else LifecycleReasonCode.RUNTIME_MISSING
354
+ )
355
+ try:
356
+ target.lifecycle_service.mark_unavailable(
357
+ boundary_digest=target.boundary_digest,
358
+ provider=target.provider,
359
+ runtime_kind=target.runtime_kind,
360
+ policy_version=target.policy.policy_version,
361
+ observed_at=observed_at,
362
+ reason=lifecycle_reason,
363
+ )
364
+ except LifecycleServiceError as exc:
365
+ return _lifecycle_reason(exc)
366
+ if lifecycle_reason is LifecycleReasonCode.CREDENTIAL_UNHEALTHY:
367
+ return lifecycle_reason.value
368
+ if probe_reason in {"probe_timeout", "probe_dns_busy"}:
369
+ return probe_reason
370
+ return lifecycle_reason.value if probe_reason == "probe_executable_invalid" else probe_reason
371
+
372
+ def _back_off(self, target_id: str, schedule: _Schedule, now: float) -> None:
373
+ schedule.consecutive_failures = min(schedule.consecutive_failures + 1, 30)
374
+ delay = min(
375
+ self.options.backoff_cap_seconds,
376
+ self.options.interval_seconds * (2 ** schedule.consecutive_failures),
377
+ )
378
+ schedule.next_due = now + self._jittered_delay(
379
+ target_id, delay, schedule.consecutive_failures
380
+ )
381
+
382
+ def _jittered_delay(self, target_id: str, delay: float, attempt: int) -> float:
383
+ if self.options.jitter_ratio == 0:
384
+ return float(delay)
385
+ digest = hashlib.sha256(f"{target_id}:{attempt}".encode("ascii")).digest()
386
+ unit = int.from_bytes(digest[:8], "big") / ((1 << 64) - 1)
387
+ factor = 1.0 + ((unit * 2.0) - 1.0) * self.options.jitter_ratio
388
+ return max(0.1, float(delay) * factor)
389
+
390
+
391
+ def _probe_reason(code: object) -> str:
392
+ return code if isinstance(code, str) and code in _PROBE_REASON_CODES else "probe_failed"
393
+
394
+
395
+ def _lifecycle_reason(error: LifecycleServiceError) -> str:
396
+ code = str(error)
397
+ return code if code in _PERSISTENCE_REASONS else "lifecycle_persistence_failed"