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,1263 @@
1
+ """Approval-gated orchestration for authenticated development CLIs."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import math
7
+ import os
8
+ import secrets
9
+ import shutil
10
+ import time
11
+ from collections.abc import Callable, Mapping
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any, Protocol
15
+
16
+ from graphite.config import Config
17
+ from graphite.freshness import check_graph_freshness
18
+ from graphite.git import GitError, GitRunner
19
+ from graphite.graph_io import GraphReadError, load_validated_graph_bundle
20
+
21
+ from .approval import ApprovalAuthority, ApprovalError
22
+ from .classifier import classify_task
23
+ from .claude_executor import AdapterError, execute_claude, preflight_claude
24
+ from .codex_executor import execute_codex, preflight_codex
25
+ from .context_builder import ContextBundle, build_routing_context
26
+ from .contracts import (
27
+ CapabilitySnapshot,
28
+ CliApprovalManifest,
29
+ CliIdentity,
30
+ Effort,
31
+ ExecutionOutcome,
32
+ ExecutionReceipt,
33
+ PermissionMode,
34
+ ProviderId,
35
+ RiskTier,
36
+ TaskRequest,
37
+ )
38
+ from .diff_policy import DiffPolicyError, collect_diff_evidence
39
+ from .policy import (
40
+ CliCandidateMetrics,
41
+ CliPolicyGates,
42
+ rank_cli_candidates,
43
+ )
44
+ from .process_runner import CliProcessError, run_cli_process
45
+ from .profiles import load_verified_capability_snapshots
46
+ from .lifecycle import ProviderRuntimeIdentity
47
+ from .lifecycle_service import LifecycleServiceError, ProviderLifecycleService
48
+ from .prompt import CanonicalPrompt, canonical_cli_prompt
49
+ from .settings import RoutingSettings
50
+ from .storage import (
51
+ DEFAULT_RECOVERY_PAGE_SIZE,
52
+ RecoverableAttemptPage,
53
+ RepositoryStore,
54
+ StorageError,
55
+ )
56
+ from .worktree import (
57
+ TaskWorktree,
58
+ WorktreeError,
59
+ cleanup_task_worktree,
60
+ create_task_worktree,
61
+ )
62
+
63
+
64
+ class RoutingServiceError(RuntimeError):
65
+ """Stable orchestration failure with no repository or provider diagnostics.
66
+
67
+ `cause` is a diagnostic carried in the MESSAGE only, and only ever an
68
+ exception class name -- never a message, path or argv. This is the error
69
+ that reaches a CI log, so a detail that does not survive to here does not
70
+ exist as far as anyone debugging is concerned.
71
+ """
72
+
73
+ def __init__(self, code: str, cause: str | None = None) -> None:
74
+ self.code = code
75
+ self.cause = cause
76
+ super().__init__(f"{code} ({cause})" if cause else code)
77
+
78
+
79
+ class AdapterResult(Protocol):
80
+ effective_model: str
81
+ message: str
82
+ input_tokens: int | None
83
+ output_tokens: int | None
84
+ duration_seconds: float
85
+
86
+
87
+ IdentityLoader = Callable[[ProviderId], CliIdentity]
88
+ RuntimeIdentityLoader = Callable[[ProviderId], ProviderRuntimeIdentity]
89
+ Executor = Callable[..., AdapterResult]
90
+ Validator = Callable[[TaskWorktree], bool]
91
+
92
+
93
+ @dataclass(frozen=True, slots=True)
94
+ class RoutingRecommendation:
95
+ task_id: str
96
+ provider: ProviderId | None
97
+ requested_model: str | None
98
+ effective_model: str | None
99
+ effort: Effort | None
100
+ snapshot_expires_at: int | None
101
+ permission_mode: PermissionMode | None
102
+ risk: str
103
+ estimated_tokens: int
104
+ outbound_manifest: dict[str, Any]
105
+ reasons: tuple[str, ...]
106
+ manual_handoff: bool
107
+ policy_version: str
108
+
109
+ @property
110
+ def model_id(self) -> str | None:
111
+ """Compatibility alias for callers transitioning from legacy routing."""
112
+ return self.requested_model
113
+
114
+ def to_dict(self) -> dict[str, Any]:
115
+ return {
116
+ "task_id": self.task_id,
117
+ "provider": None if self.provider is None else self.provider.value,
118
+ "requested_model": self.requested_model,
119
+ "effective_model": self.effective_model,
120
+ "effort": None if self.effort is None else self.effort.value,
121
+ "snapshot_expires_at": self.snapshot_expires_at,
122
+ "permission_mode": (
123
+ None if self.permission_mode is None else self.permission_mode.value
124
+ ),
125
+ "risk": self.risk,
126
+ "estimated_tokens": self.estimated_tokens,
127
+ "outbound_manifest": self.outbound_manifest,
128
+ "reasons": list(self.reasons),
129
+ "manual_handoff": self.manual_handoff,
130
+ "policy_version": self.policy_version,
131
+ "execution_authority": "single_use_approval_required",
132
+ "automatic_retry": False,
133
+ "automatic_fallback": False,
134
+ "automatic_merge": False,
135
+ }
136
+
137
+
138
+ @dataclass(frozen=True, slots=True)
139
+ class PreparedExecution:
140
+ task_id: str
141
+ decision_id: str
142
+ attempt_id: str
143
+ worktree: TaskWorktree
144
+ manifest: CliApprovalManifest
145
+ prompt: CanonicalPrompt = field(repr=False, compare=False)
146
+ graph_fingerprint: str
147
+ lifecycle_identity_digest: str | None = None
148
+
149
+ def to_dict(self) -> dict[str, Any]:
150
+ return {
151
+ "task_id": self.task_id,
152
+ "decision_id": self.decision_id,
153
+ "attempt_id": self.attempt_id,
154
+ "worktree_id": self.worktree.worktree_id,
155
+ "repository_commit": self.worktree.baseline_commit,
156
+ "provider": self.manifest.provider.value,
157
+ "requested_model": self.manifest.requested_model,
158
+ "effective_model": self.manifest.effective_model,
159
+ "effort": self.manifest.effort.value,
160
+ "permission_mode": self.manifest.permission_mode.value,
161
+ "prompt_hash": self.prompt.prompt_hash,
162
+ "approval_required": True,
163
+ }
164
+
165
+
166
+ @dataclass(frozen=True, slots=True)
167
+ class ApprovedExecution:
168
+ """Ephemeral provider message plus persistence-safe evidence."""
169
+
170
+ text: str = field(repr=False, compare=False)
171
+ receipt: ExecutionReceipt
172
+ diff_hash: str
173
+
174
+ def to_public_dict(self) -> dict[str, Any]:
175
+ payload = dict(self.receipt.to_dict())
176
+ payload["diff_hash"] = self.diff_hash
177
+ return payload
178
+
179
+
180
+ @dataclass(frozen=True, slots=True)
181
+ class _RecommendationState:
182
+ request: TaskRequest
183
+ task: Any
184
+ context: ContextBundle
185
+ graph_fingerprint: str
186
+ snapshot: CapabilitySnapshot
187
+ recommendation: RoutingRecommendation
188
+ lifecycle_identity_digest: str | None = None
189
+
190
+
191
+ def _machine_state_dir() -> Path:
192
+ if os.name == "nt":
193
+ base = os.environ.get("LOCALAPPDATA")
194
+ return (
195
+ Path(base) if base else Path.home() / "AppData" / "Local"
196
+ ) / "Graphite" / "routing"
197
+ xdg = os.environ.get("XDG_STATE_HOME")
198
+ return (
199
+ Path(xdg) if xdg else Path.home() / ".local" / "state"
200
+ ) / "graphite" / "routing"
201
+
202
+
203
+ def _default_executable(provider: ProviderId) -> Path:
204
+ name = "claude" if provider is ProviderId.CLAUDE_CODE else "codex"
205
+ selected = shutil.which(name)
206
+ if not selected:
207
+ raise RoutingServiceError("cli_missing")
208
+ try:
209
+ return Path(selected).resolve(strict=True)
210
+ except OSError:
211
+ raise RoutingServiceError("cli_missing") from None
212
+
213
+
214
+ def _default_credential_home(provider: ProviderId) -> Path:
215
+ environment_name = (
216
+ "CLAUDE_CONFIG_DIR" if provider is ProviderId.CLAUDE_CODE else "CODEX_HOME"
217
+ )
218
+ configured = os.environ.get(environment_name)
219
+ candidate = Path(configured) if configured else Path.home() / (
220
+ ".claude" if provider is ProviderId.CLAUDE_CODE else ".codex"
221
+ )
222
+ try:
223
+ return candidate.resolve(strict=True)
224
+ except OSError:
225
+ raise RoutingServiceError("credential_home_missing") from None
226
+
227
+
228
+ class RoutingService:
229
+ """Coordinates recommendation, preparation, one-shot execution, and review gates."""
230
+
231
+ def __init__(
232
+ self,
233
+ path: str | Path,
234
+ *,
235
+ state_dir: Path | None = None,
236
+ settings: RoutingSettings | None = None,
237
+ identity_loader: IdentityLoader | None = None,
238
+ executables: Mapping[ProviderId, Path] | None = None,
239
+ credential_homes: Mapping[ProviderId, Path] | None = None,
240
+ executors: Mapping[ProviderId, Executor] | None = None,
241
+ validator: Validator | None = None,
242
+ validation_commands: tuple[tuple[str, ...], ...] = (),
243
+ lifecycle_service: ProviderLifecycleService | None = None,
244
+ lifecycle_boundaries: Mapping[ProviderId, str] | None = None,
245
+ runtime_identity_loader: RuntimeIdentityLoader | None = None,
246
+ ) -> None:
247
+ try:
248
+ self.root = Path(path).resolve(strict=True)
249
+ except OSError:
250
+ raise RoutingServiceError("repository_root_invalid") from None
251
+ if not self.root.is_dir():
252
+ raise RoutingServiceError("repository_root_invalid")
253
+ self.settings = settings or RoutingSettings.from_env()
254
+ self.store = RepositoryStore(self.root)
255
+ self.state_dir = (state_dir or _machine_state_dir()).resolve(strict=False)
256
+ self._executables = dict(executables or {})
257
+ self._credential_homes = dict(credential_homes or {})
258
+ self._identity_loader = identity_loader or self._preflight
259
+ self._executors: dict[ProviderId, Executor] = {
260
+ ProviderId.CLAUDE_CODE: execute_claude,
261
+ ProviderId.CODEX: execute_codex,
262
+ }
263
+ if executors:
264
+ self._executors.update(executors)
265
+ if not isinstance(validation_commands, tuple) or any(
266
+ not isinstance(command, tuple)
267
+ or not command
268
+ or any(not isinstance(value, str) or not value for value in command)
269
+ for command in validation_commands
270
+ ):
271
+ raise RoutingServiceError("validation_command_invalid")
272
+ self._validation_commands = validation_commands
273
+ self._validator = validator or self._validate
274
+ self._lifecycle_service = lifecycle_service
275
+ self._lifecycle_boundaries = dict(lifecycle_boundaries or {})
276
+ self._runtime_identity_loader = runtime_identity_loader
277
+ if (lifecycle_service is None) is not (runtime_identity_loader is None) or (
278
+ lifecycle_service is not None and not self._lifecycle_boundaries
279
+ ):
280
+ raise RoutingServiceError("lifecycle_configuration_invalid")
281
+ self._recommendations: dict[str, _RecommendationState] = {}
282
+ self._prepared: dict[str, PreparedExecution] = {}
283
+ self._snapshots: dict[str, CapabilitySnapshot] = {}
284
+ self._review_primary: dict[str, tuple[str, str]] = {}
285
+
286
+ def _executable(self, provider: ProviderId) -> Path:
287
+ return self._executables.get(provider) or _default_executable(provider)
288
+
289
+ def _credential_home(self, provider: ProviderId) -> Path:
290
+ return self._credential_homes.get(provider) or _default_credential_home(provider)
291
+
292
+ def _preflight(self, provider: ProviderId) -> CliIdentity:
293
+ kwargs = {
294
+ "executable": self._executable(provider),
295
+ "workspace": self.root,
296
+ "credential_home": self._credential_home(provider),
297
+ }
298
+ if provider is ProviderId.CLAUDE_CODE:
299
+ return preflight_claude(**kwargs)
300
+ return preflight_codex(**kwargs)
301
+
302
+ @staticmethod
303
+ def _graph_fingerprint(bundle: object) -> str:
304
+ return hashlib.sha256(
305
+ json.dumps(bundle, sort_keys=True, separators=(",", ":")).encode("utf-8")
306
+ ).hexdigest()
307
+
308
+ def recommend(
309
+ self, *, objective: str, targets: tuple[str, ...]
310
+ ) -> RoutingRecommendation:
311
+ request = TaskRequest(
312
+ objective=objective,
313
+ repository_root=self.root,
314
+ targets=tuple(targets),
315
+ max_input_tokens=self.settings.max_input_tokens,
316
+ max_output_tokens=self.settings.max_output_tokens,
317
+ data_policy="source_allowed",
318
+ )
319
+ cfg = Config.from_env()
320
+ freshness = check_graph_freshness(self.root, cfg)
321
+ if freshness.get("stale", True):
322
+ return self._handoff("unclassified", "unknown", "graph_stale")
323
+ try:
324
+ self.store.initialize()
325
+ bundle, graph = load_validated_graph_bundle(
326
+ self.root / cfg.output_dir / "graph.json", root=self.root
327
+ )
328
+ task = classify_task(request, graph)
329
+ context = build_routing_context(request, graph, self.settings)
330
+ now = int(time.time())
331
+ active_lifecycle_digests: frozenset[str] | None = None
332
+ if self._lifecycle_service is not None:
333
+ active: set[str] = set()
334
+ for boundary in self._lifecycle_boundaries.values():
335
+ try:
336
+ active.add(
337
+ self._lifecycle_service.active_identity_digest(boundary)
338
+ )
339
+ except LifecycleServiceError:
340
+ continue
341
+ active_lifecycle_digests = frozenset(active)
342
+ snapshots = load_verified_capability_snapshots(
343
+ self.store,
344
+ now=now,
345
+ active_lifecycle_identity_digests=active_lifecycle_digests,
346
+ )
347
+ except (GraphReadError, StorageError, OSError, ValueError) as exc:
348
+ return self._handoff(
349
+ "unclassified", "unknown", getattr(exc, "code", "routing_evidence_blocked")
350
+ )
351
+ identities: list[CliIdentity] = []
352
+ providers: list[ProviderId] = []
353
+ for provider in sorted({item.profile.provider for item in snapshots}, key=str):
354
+ try:
355
+ identity = self._identity_loader(provider)
356
+ except (AdapterError, RoutingServiceError, OSError, ValueError):
357
+ continue
358
+ identities.append(identity)
359
+ providers.append(provider)
360
+ context_tokens = max(1, (context.manifest.total_bytes + 3) // 4)
361
+ candidates = tuple(
362
+ CliCandidateMetrics(
363
+ capability_snapshot_digest=snapshot.digest,
364
+ effort=snapshot.profile.supported_efforts[0],
365
+ repository_success_millis=None,
366
+ global_success_millis=500,
367
+ sample_count=0,
368
+ expected_input_tokens=context_tokens,
369
+ expected_output_tokens=request.max_output_tokens,
370
+ expected_latency_ms=30_000,
371
+ quota_scarcity_millis=0,
372
+ )
373
+ for snapshot in snapshots
374
+ )
375
+ ranked = rank_cli_candidates(
376
+ task,
377
+ snapshots,
378
+ candidates,
379
+ CliPolicyGates(
380
+ authenticated_providers=tuple(providers),
381
+ current_cli_identities=tuple(identities),
382
+ permission_mode=PermissionMode.WORKSPACE_WRITE,
383
+ context_tokens=context_tokens,
384
+ budget_tokens=min(
385
+ self.settings.repository_quota_tokens,
386
+ request.max_input_tokens + request.max_output_tokens,
387
+ ),
388
+ now=now,
389
+ ),
390
+ )
391
+ selected = ranked.selected
392
+ snapshot = None if selected is None else next(
393
+ item for item in snapshots if item.digest == selected.capability_snapshot_digest
394
+ )
395
+ recommendation = RoutingRecommendation(
396
+ task_id=task.task_id,
397
+ provider=None if selected is None else selected.provider,
398
+ requested_model=None if selected is None else selected.requested_model,
399
+ effective_model=None if selected is None else selected.effective_model,
400
+ effort=None if selected is None else selected.effort,
401
+ snapshot_expires_at=None if snapshot is None else snapshot.expires_at,
402
+ permission_mode=(
403
+ None if snapshot is None else snapshot.profile.permission_mode
404
+ ),
405
+ risk=task.risk.value,
406
+ estimated_tokens=context_tokens + request.max_output_tokens,
407
+ outbound_manifest=context.manifest.to_dict(),
408
+ reasons=ranked.reasons,
409
+ manual_handoff=ranked.manual_handoff,
410
+ policy_version=ranked.policy_version,
411
+ )
412
+ if snapshot is not None:
413
+ lifecycle_digest = None
414
+ if self._lifecycle_service is not None:
415
+ lifecycle_digest = self.store.lifecycle_identity_binding(
416
+ authority_kind="capability_snapshot", authority_id=snapshot.digest
417
+ )
418
+ state = _RecommendationState(
419
+ request,
420
+ task,
421
+ context,
422
+ self._graph_fingerprint(bundle),
423
+ snapshot,
424
+ recommendation,
425
+ lifecycle_digest,
426
+ )
427
+ self._recommendations[task.task_id] = state
428
+ self._snapshots[snapshot.digest] = snapshot
429
+ return recommendation
430
+
431
+ @staticmethod
432
+ def _handoff(task_id: str, risk: str, reason: str) -> RoutingRecommendation:
433
+ return RoutingRecommendation(
434
+ task_id,
435
+ None,
436
+ None,
437
+ None,
438
+ None,
439
+ None,
440
+ None,
441
+ risk,
442
+ 0,
443
+ {"items": [], "total_bytes": 0},
444
+ (str(reason),),
445
+ True,
446
+ "3",
447
+ )
448
+
449
+ def _head_commit(self) -> str:
450
+ try:
451
+ result = GitRunner(self.root).run(
452
+ ["rev-parse", "HEAD"], timeout_seconds=5.0, max_stdout_bytes=256
453
+ )
454
+ value = result.stdout.decode("ascii").strip()
455
+ except (GitError, UnicodeDecodeError, OSError):
456
+ raise RoutingServiceError("repository_commit_unavailable") from None
457
+ if result.returncode != 0 or len(value) not in {40, 64}:
458
+ raise RoutingServiceError("repository_commit_unavailable")
459
+ return value
460
+
461
+ def prepare(self, recommendation: RoutingRecommendation) -> PreparedExecution:
462
+ if not isinstance(recommendation, RoutingRecommendation):
463
+ raise RoutingServiceError("recommendation_invalid")
464
+ if recommendation.manual_handoff or recommendation.provider is None:
465
+ raise RoutingServiceError("manual_handoff_required")
466
+ if any(
467
+ prepared.task_id == recommendation.task_id
468
+ for prepared in self._prepared.values()
469
+ ):
470
+ raise RoutingServiceError("transition_replay")
471
+ state = self._recommendations.pop(recommendation.task_id, None)
472
+ if state is None or state.recommendation != recommendation:
473
+ raise RoutingServiceError("recommendation_expired")
474
+ now = int(time.time())
475
+ commit = self._head_commit()
476
+ worktree_id = "worktree-" + secrets.token_hex(12)
477
+ worktree = create_task_worktree(
478
+ source_root=self.root,
479
+ state_root=self.state_dir / "worktrees",
480
+ task_id=worktree_id,
481
+ approved_commit=commit,
482
+ )
483
+ decision_id = "decision-" + secrets.token_hex(12)
484
+ attempt_id = "attempt-" + secrets.token_hex(12)
485
+ approval_id = "approval-" + secrets.token_hex(12)
486
+ prompt = canonical_cli_prompt(
487
+ objective=state.request.objective, context=state.context
488
+ )
489
+ snapshot = state.snapshot
490
+ manifest = CliApprovalManifest(
491
+ approval_id=approval_id,
492
+ task_id=state.task.task_id,
493
+ decision_id=decision_id,
494
+ provider=snapshot.profile.provider,
495
+ requested_model=snapshot.profile.requested_model,
496
+ effective_model=snapshot.profile.effective_model,
497
+ effort=snapshot.profile.supported_efforts[0],
498
+ cli_executable_sha256=snapshot.identity.executable_sha256,
499
+ cli_version=snapshot.identity.cli_version,
500
+ adapter_protocol_version=snapshot.identity.adapter_protocol_version,
501
+ capability_snapshot_digest=snapshot.digest,
502
+ graph_fingerprint=state.graph_fingerprint,
503
+ context_manifest_hash=state.context.manifest.manifest_hash,
504
+ repository_commit=commit,
505
+ worktree_id=worktree_id,
506
+ permission_mode=PermissionMode.WORKSPACE_WRITE,
507
+ max_input_tokens=state.request.max_input_tokens,
508
+ max_output_tokens=state.request.max_output_tokens,
509
+ policy_version=recommendation.policy_version,
510
+ issued_at=now,
511
+ expires_at=now + self.settings.approval_ttl_seconds,
512
+ nonce=secrets.token_hex(24),
513
+ )
514
+ self.store.record_task(
515
+ state.task.task_id,
516
+ state.task.category.value,
517
+ state.task.risk.value,
518
+ hashlib.sha256(state.request.objective.encode("utf-8")).hexdigest(),
519
+ now,
520
+ )
521
+ self.store.record_decision(
522
+ decision_id,
523
+ state.task.task_id,
524
+ manifest.requested_model,
525
+ manifest.effort.value,
526
+ recommendation.policy_version,
527
+ "cli-1",
528
+ now,
529
+ )
530
+ self.store.create_task_worktree_record(
531
+ worktree_id=worktree_id,
532
+ task_id=state.task.task_id,
533
+ baseline_commit=commit,
534
+ canonical_root_hash=hashlib.sha256(str(worktree.root).encode()).hexdigest(),
535
+ created_at=now,
536
+ )
537
+ prepared = PreparedExecution(
538
+ state.task.task_id,
539
+ decision_id,
540
+ attempt_id,
541
+ worktree,
542
+ manifest,
543
+ prompt,
544
+ state.graph_fingerprint,
545
+ state.lifecycle_identity_digest,
546
+ )
547
+ self._prepared[attempt_id] = prepared
548
+ return prepared
549
+
550
+ @staticmethod
551
+ def _manifest_hash(manifest: CliApprovalManifest) -> str:
552
+ return hashlib.sha256(
553
+ json.dumps(
554
+ manifest.to_dict(), sort_keys=True, separators=(",", ":")
555
+ ).encode("utf-8")
556
+ ).hexdigest()
557
+
558
+ def _validate(self, worktree: TaskWorktree) -> bool:
559
+ runner = GitRunner(worktree.root)
560
+ executable = runner.executable.resolve(strict=True)
561
+ argv = (
562
+ str(executable),
563
+ "--no-optional-locks",
564
+ "-c",
565
+ "core.fsmonitor=false",
566
+ "-c",
567
+ f"safe.directory={worktree.root}",
568
+ "diff",
569
+ "--check",
570
+ worktree.baseline_commit,
571
+ "--",
572
+ )
573
+ try:
574
+ run_cli_process(
575
+ argv=argv,
576
+ cwd=worktree.root,
577
+ stdin=b"",
578
+ provider=ProviderId.CODEX,
579
+ credential_home=None,
580
+ timeout_seconds=60.0,
581
+ max_input_bytes=1,
582
+ max_output_bytes=256 * 1024,
583
+ )
584
+ except (CliProcessError, GitError, OSError):
585
+ return False
586
+ for command in self._validation_commands:
587
+ try:
588
+ run_cli_process(
589
+ argv=command,
590
+ cwd=worktree.root,
591
+ stdin=b"",
592
+ provider=ProviderId.CODEX,
593
+ credential_home=None,
594
+ timeout_seconds=300.0,
595
+ max_input_bytes=1,
596
+ max_output_bytes=1024 * 1024,
597
+ )
598
+ except CliProcessError:
599
+ return False
600
+ return True
601
+
602
+ def run_approved(
603
+ self, prepared: PreparedExecution, *, approval_granted: bool
604
+ ) -> ApprovedExecution:
605
+ if approval_granted is not True:
606
+ raise RoutingServiceError("approval_required")
607
+ current = self._prepared.get(prepared.attempt_id)
608
+ if current is None or current != prepared:
609
+ raise RoutingServiceError("transition_replay")
610
+ manifest = prepared.manifest
611
+ snapshot = self._snapshots.get(manifest.capability_snapshot_digest)
612
+ if snapshot is None or snapshot.digest != manifest.capability_snapshot_digest:
613
+ raise RoutingServiceError("capability_snapshot_missing")
614
+ try:
615
+ current_identity = self._identity_loader(manifest.provider)
616
+ except (AdapterError, RoutingServiceError, OSError, ValueError):
617
+ raise RoutingServiceError("cli_preflight_failed") from None
618
+ if current_identity != snapshot.identity:
619
+ raise RoutingServiceError("cli_identity_changed")
620
+ live_runtime_identity: ProviderRuntimeIdentity | None = None
621
+ if self._lifecycle_service is not None:
622
+ boundary = self._lifecycle_boundaries.get(manifest.provider)
623
+ if boundary is None or prepared.lifecycle_identity_digest is None or self._runtime_identity_loader is None:
624
+ raise RoutingServiceError("lifecycle_authority_missing")
625
+ try:
626
+ live_runtime_identity = self._runtime_identity_loader(manifest.provider)
627
+ self._lifecycle_service.require_snapshot_authority(
628
+ boundary_digest=boundary,
629
+ lifecycle_identity_digest=prepared.lifecycle_identity_digest,
630
+ capability_snapshot_digest=manifest.capability_snapshot_digest,
631
+ live_identity=live_runtime_identity,
632
+ )
633
+ except (LifecycleServiceError, ValueError):
634
+ raise RoutingServiceError("lifecycle_identity_changed") from None
635
+ self._prepared.pop(prepared.attempt_id, None)
636
+ now = int(time.time())
637
+ authority = ApprovalAuthority(
638
+ self.store,
639
+ key_path=self.state_dir / "approval.key",
640
+ quota_path=self.state_dir / "quota.sqlite3",
641
+ )
642
+ if prepared.lifecycle_identity_digest is None:
643
+ signed = authority.issue(manifest)
644
+ else:
645
+ signed = authority.issue(
646
+ manifest,
647
+ lifecycle_identity_digest=prepared.lifecycle_identity_digest,
648
+ capability_snapshot_digest=manifest.capability_snapshot_digest,
649
+ bound_at=now,
650
+ )
651
+ manifest_hash = self._manifest_hash(manifest)
652
+ self.store.create_cli_execution_attempt_record(
653
+ attempt_id=prepared.attempt_id,
654
+ approval_id=manifest.approval_id,
655
+ task_id=manifest.task_id,
656
+ decision_id=manifest.decision_id,
657
+ worktree_id=manifest.worktree_id,
658
+ provider=manifest.provider.value,
659
+ requested_model=manifest.requested_model,
660
+ effective_model=manifest.effective_model,
661
+ effort=manifest.effort.value,
662
+ cli_version=manifest.cli_version,
663
+ executable_sha256=manifest.cli_executable_sha256,
664
+ capability_snapshot_digest=manifest.capability_snapshot_digest,
665
+ manifest_hash=manifest_hash,
666
+ expected_prompt_hash=prepared.prompt.prompt_hash,
667
+ reserved_tokens=manifest.max_input_tokens + manifest.max_output_tokens,
668
+ created_at=now,
669
+ )
670
+ if prepared.lifecycle_identity_digest is not None:
671
+ try:
672
+ self.store.save_lifecycle_attempt_binding(
673
+ attempt_id=prepared.attempt_id,
674
+ lifecycle_identity_digest=prepared.lifecycle_identity_digest,
675
+ bound_at=now,
676
+ )
677
+ except (StorageError, ValueError):
678
+ self._fail(prepared, "lifecycle_binding_failed", quarantine=True)
679
+ raise RoutingServiceError("lifecycle_binding_failed") from None
680
+ try:
681
+ if self._lifecycle_service is not None:
682
+ assert live_runtime_identity is not None
683
+ self._lifecycle_service.require_execution_authority(
684
+ boundary_digest=self._lifecycle_boundaries[manifest.provider],
685
+ lifecycle_identity_digest=prepared.lifecycle_identity_digest,
686
+ capability_snapshot_digest=manifest.capability_snapshot_digest,
687
+ approval_id=manifest.approval_id,
688
+ live_identity=live_runtime_identity,
689
+ )
690
+ authority.consume(
691
+ signed,
692
+ manifest,
693
+ repository_quota_tokens=self.settings.repository_quota_tokens,
694
+ machine_quota_tokens=self.settings.machine_quota_tokens,
695
+ lifecycle_identity_digest=prepared.lifecycle_identity_digest,
696
+ capability_snapshot_digest=(
697
+ manifest.capability_snapshot_digest
698
+ if prepared.lifecycle_identity_digest is not None
699
+ else None
700
+ ),
701
+ )
702
+ executor = self._executors[manifest.provider]
703
+ result = executor(
704
+ executable=self._executable(manifest.provider),
705
+ workspace=prepared.worktree.root,
706
+ credential_home=self._credential_home(manifest.provider),
707
+ prompt=prepared.prompt.body,
708
+ requested_model=manifest.requested_model,
709
+ expected_effective_model=manifest.effective_model,
710
+ effort=manifest.effort,
711
+ permission_mode=manifest.permission_mode,
712
+ )
713
+ except ApprovalError as exc:
714
+ self._fail(prepared, exc.code, quarantine=True)
715
+ raise RoutingServiceError(exc.code) from None
716
+ except (AdapterError, KeyError) as exc:
717
+ code = getattr(exc, "code", "executor_unavailable")
718
+ self._fail(prepared, code, quarantine=True)
719
+ raise RoutingServiceError(code) from None
720
+ except Exception:
721
+ self._fail(prepared, "executor_failed", quarantine=True)
722
+ raise RoutingServiceError("executor_failed") from None
723
+ if (
724
+ not isinstance(result.message, str)
725
+ or not result.message
726
+ or len(result.message) > 1_048_576
727
+ or result.effective_model != manifest.effective_model
728
+ or isinstance(result.input_tokens, bool)
729
+ or not isinstance(result.input_tokens, int)
730
+ or not 0 <= result.input_tokens <= manifest.max_input_tokens
731
+ or isinstance(result.output_tokens, bool)
732
+ or not isinstance(result.output_tokens, int)
733
+ or not 0 <= result.output_tokens <= manifest.max_output_tokens
734
+ or isinstance(result.duration_seconds, bool)
735
+ or not isinstance(result.duration_seconds, (int, float))
736
+ or not math.isfinite(result.duration_seconds)
737
+ or result.duration_seconds < 0
738
+ ):
739
+ self._fail(prepared, "executor_result_invalid", quarantine=True)
740
+ raise RoutingServiceError("executor_result_invalid")
741
+ try:
742
+ evidence = collect_diff_evidence(
743
+ prepared.worktree,
744
+ max_files=self.settings.max_changed_files,
745
+ max_bytes=self.settings.max_changed_bytes,
746
+ )
747
+ except (DiffPolicyError, WorktreeError) as exc:
748
+ code = getattr(exc, "code", "diff_blocked")
749
+ self._fail(prepared, code, quarantine=True)
750
+ # Hand the diagnostic across explicitly. Both hops raise `from None`,
751
+ # so anything not passed by hand dies here and the log shows a bare
752
+ # code -- which is precisely what graphite#37's one sighting shows.
753
+ # `_fail` still records the unwidened `code`: the taxonomy is stable,
754
+ # the message is where the detail goes.
755
+ raise RoutingServiceError(code, getattr(exc, "cause", None)) from None
756
+ if prepared.attempt_id in self._review_primary and evidence.changed_files != 0:
757
+ self._fail(prepared, "review_mutation", quarantine=True)
758
+ raise RoutingServiceError("review_mutation")
759
+ try:
760
+ validation_outcome = (
761
+ "passed" if self._validator(prepared.worktree) else "failed"
762
+ )
763
+ except Exception:
764
+ self._fail(prepared, "validation_failed", quarantine=True)
765
+ raise RoutingServiceError("validation_failed") from None
766
+ completed_at = int(time.time())
767
+ self.store.record_validation_result(
768
+ attempt_id=prepared.attempt_id,
769
+ diff_hash=evidence.diff_sha256,
770
+ changed_file_count=evidence.changed_files,
771
+ changed_byte_count=evidence.changed_bytes,
772
+ outcome=validation_outcome,
773
+ recorded_at=completed_at,
774
+ )
775
+ input_tokens = result.input_tokens
776
+ output_tokens = result.output_tokens
777
+ if input_tokens is None or output_tokens is None:
778
+ self._fail(prepared, "usage_unavailable", quarantine=True)
779
+ raise RoutingServiceError("usage_unavailable")
780
+ receipt = ExecutionReceipt(
781
+ execution_id="execution-" + secrets.token_hex(12),
782
+ approval_id=manifest.approval_id,
783
+ model_id=manifest.requested_model,
784
+ effort=manifest.effort,
785
+ outcome=ExecutionOutcome.SUCCEEDED,
786
+ input_tokens=input_tokens,
787
+ output_tokens=output_tokens,
788
+ latency_ms=max(0, int(result.duration_seconds * 1_000)),
789
+ prompt_hash=prepared.prompt.prompt_hash,
790
+ response_hash=hashlib.sha256(result.message.encode("utf-8")).hexdigest(),
791
+ failure_reason=None,
792
+ provider=manifest.provider,
793
+ effective_model=result.effective_model,
794
+ cli_version=manifest.cli_version,
795
+ changed_file_count=evidence.changed_files,
796
+ changed_byte_count=evidence.changed_bytes,
797
+ validation_outcome=validation_outcome,
798
+ )
799
+ self.store.finalize_cli_execution(
800
+ attempt_id=prepared.attempt_id,
801
+ receipt=receipt,
802
+ graph_fingerprint=prepared.graph_fingerprint,
803
+ completed_at=completed_at,
804
+ )
805
+ self.store.transition_task_worktree(
806
+ manifest.worktree_id,
807
+ expected_status="prepared",
808
+ new_status="executed",
809
+ updated_at=completed_at,
810
+ )
811
+ return ApprovedExecution(result.message, receipt, evidence.diff_sha256)
812
+
813
+ def prepare_review(self, task_id: str) -> PreparedExecution:
814
+ """Prepare a separate read-only other-provider review of a primary diff."""
815
+ primary_worktree, record = self._load_task_worktree(task_id)
816
+ if record["status"] != "executed":
817
+ raise RoutingServiceError("review_primary_not_executed")
818
+ primary = self.store.worktree_validation_record(primary_worktree.worktree_id)
819
+ if primary is None or primary["outcome"] != "passed":
820
+ raise RoutingServiceError("review_primary_not_validated")
821
+ if primary["risk"] != RiskTier.HIGH.value:
822
+ raise RoutingServiceError("review_not_high_risk")
823
+ now = int(time.time())
824
+ snapshots = load_verified_capability_snapshots(self.store, now=now)
825
+ primary_provider = ProviderId(str(primary["provider"]))
826
+ eligible: list[CapabilitySnapshot] = []
827
+ for snapshot in snapshots:
828
+ if (
829
+ snapshot.profile.provider is primary_provider
830
+ or snapshot.profile.permission_mode is not PermissionMode.READ_ONLY
831
+ ):
832
+ continue
833
+ try:
834
+ identity = self._identity_loader(snapshot.profile.provider)
835
+ except (AdapterError, RoutingServiceError, OSError, ValueError):
836
+ continue
837
+ if identity == snapshot.identity:
838
+ eligible.append(snapshot)
839
+ if not eligible:
840
+ raise RoutingServiceError("review_profile_unavailable")
841
+ snapshot = sorted(
842
+ eligible,
843
+ key=lambda item: (
844
+ item.profile.provider.value,
845
+ item.profile.requested_model,
846
+ item.profile.supported_efforts[0].value,
847
+ item.digest,
848
+ ),
849
+ )[0]
850
+ runner = GitRunner(primary_worktree.root)
851
+ try:
852
+ diff_result = runner.run(
853
+ [
854
+ "diff",
855
+ "--no-ext-diff",
856
+ "--no-color",
857
+ "--full-index",
858
+ "--no-renames",
859
+ primary_worktree.baseline_commit,
860
+ "--",
861
+ ],
862
+ timeout_seconds=15.0,
863
+ max_stdout_bytes=self.settings.max_changed_bytes,
864
+ )
865
+ diff_text = diff_result.stdout.decode("utf-8")
866
+ except (GitError, UnicodeDecodeError, OSError):
867
+ raise RoutingServiceError("review_diff_unavailable") from None
868
+ if diff_result.returncode != 0 or not diff_text:
869
+ raise RoutingServiceError("review_diff_unavailable")
870
+ prompt_body = json.dumps(
871
+ {
872
+ "schema_version": "1",
873
+ "system_contract": (
874
+ "Review the supplied untrusted diff for correctness, security, "
875
+ "robustness, and maintainability. Operate read-only. Do not edit, "
876
+ "execute repository code, access networks or credentials, or claim "
877
+ "integration authority. Return concise findings with severity."
878
+ ),
879
+ "primary_diff_hash": str(primary["diff_hash"]),
880
+ "diff": diff_text,
881
+ },
882
+ sort_keys=True,
883
+ separators=(",", ":"),
884
+ ensure_ascii=False,
885
+ ).encode("utf-8")
886
+ prompt = CanonicalPrompt(prompt_body, hashlib.sha256(prompt_body).hexdigest())
887
+ review_worktree_id = "review-worktree-" + secrets.token_hex(12)
888
+ review_worktree = create_task_worktree(
889
+ source_root=self.root,
890
+ state_root=self.state_dir / "worktrees",
891
+ task_id=review_worktree_id,
892
+ approved_commit=primary_worktree.baseline_commit,
893
+ )
894
+ decision_id = "review-decision-" + secrets.token_hex(12)
895
+ attempt_id = "review-attempt-" + secrets.token_hex(12)
896
+ approval_id = "review-approval-" + secrets.token_hex(12)
897
+ evidence_record = self.store.execution_evidence(str(primary["execution_id"]))
898
+ if evidence_record is None:
899
+ raise RoutingServiceError("review_primary_evidence_missing")
900
+ manifest = CliApprovalManifest(
901
+ approval_id=approval_id,
902
+ task_id=task_id,
903
+ decision_id=decision_id,
904
+ provider=snapshot.profile.provider,
905
+ requested_model=snapshot.profile.requested_model,
906
+ effective_model=snapshot.profile.effective_model,
907
+ effort=snapshot.profile.supported_efforts[0],
908
+ cli_executable_sha256=snapshot.identity.executable_sha256,
909
+ cli_version=snapshot.identity.cli_version,
910
+ adapter_protocol_version=snapshot.identity.adapter_protocol_version,
911
+ capability_snapshot_digest=snapshot.digest,
912
+ graph_fingerprint=evidence_record["graph_fingerprint"],
913
+ context_manifest_hash=str(primary["diff_hash"]),
914
+ repository_commit=primary_worktree.baseline_commit,
915
+ worktree_id=review_worktree_id,
916
+ permission_mode=PermissionMode.READ_ONLY,
917
+ max_input_tokens=self.settings.max_input_tokens,
918
+ max_output_tokens=self.settings.max_output_tokens,
919
+ policy_version="review-1",
920
+ issued_at=now,
921
+ expires_at=now + self.settings.approval_ttl_seconds,
922
+ nonce=secrets.token_hex(24),
923
+ )
924
+ self.store.record_decision(
925
+ decision_id,
926
+ task_id,
927
+ manifest.requested_model,
928
+ manifest.effort.value,
929
+ "review-1",
930
+ "cli-1",
931
+ now,
932
+ )
933
+ self.store.create_task_worktree_record(
934
+ worktree_id=review_worktree_id,
935
+ task_id=task_id,
936
+ baseline_commit=primary_worktree.baseline_commit,
937
+ canonical_root_hash=hashlib.sha256(
938
+ str(review_worktree.root).encode()
939
+ ).hexdigest(),
940
+ created_at=now,
941
+ )
942
+ prepared = PreparedExecution(
943
+ task_id,
944
+ decision_id,
945
+ attempt_id,
946
+ review_worktree,
947
+ manifest,
948
+ prompt,
949
+ evidence_record["graph_fingerprint"],
950
+ )
951
+ self._prepared[attempt_id] = prepared
952
+ self._snapshots[snapshot.digest] = snapshot
953
+ self._review_primary[attempt_id] = (
954
+ str(primary["attempt_id"]),
955
+ str(primary["diff_hash"]),
956
+ )
957
+ return prepared
958
+
959
+ def run_review_approved(
960
+ self, prepared: PreparedExecution, *, approval_granted: bool
961
+ ) -> ApprovedExecution:
962
+ binding = self._review_primary.get(prepared.attempt_id)
963
+ if binding is None:
964
+ raise RoutingServiceError("review_binding_missing")
965
+ result = self.run_approved(prepared, approval_granted=approval_granted)
966
+ primary_attempt_id, primary_diff_hash = binding
967
+ self.store.create_review_link_record(
968
+ review_attempt_id=prepared.attempt_id,
969
+ primary_attempt_id=primary_attempt_id,
970
+ primary_diff_hash=primary_diff_hash,
971
+ created_at=int(time.time()),
972
+ )
973
+ self._review_primary.pop(prepared.attempt_id, None)
974
+ return result
975
+
976
+ def decline(self, prepared: PreparedExecution) -> dict[str, str]:
977
+ current = self._prepared.pop(prepared.attempt_id, None)
978
+ if current is None or current != prepared:
979
+ raise RoutingServiceError("transition_replay")
980
+ self.store.transition_task_worktree(
981
+ prepared.worktree.worktree_id,
982
+ expected_status="prepared",
983
+ new_status="quarantined",
984
+ updated_at=int(time.time()),
985
+ )
986
+ return {
987
+ "task_id": prepared.task_id,
988
+ "worktree_id": prepared.worktree.worktree_id,
989
+ "status": "quarantined",
990
+ "reason": "approval_declined",
991
+ }
992
+
993
+ def _fail(
994
+ self, prepared: PreparedExecution, reason: str, *, quarantine: bool
995
+ ) -> None:
996
+ now = int(time.time())
997
+ try:
998
+ self.store.mark_cli_execution_attempt(
999
+ prepared.attempt_id,
1000
+ status="quarantined" if quarantine else "failed",
1001
+ failure_reason=reason,
1002
+ updated_at=now,
1003
+ )
1004
+ if quarantine:
1005
+ self.store.transition_task_worktree(
1006
+ prepared.worktree.worktree_id,
1007
+ expected_status="prepared",
1008
+ new_status="quarantined",
1009
+ updated_at=now,
1010
+ )
1011
+ except (StorageError, ValueError, OSError):
1012
+ raise RoutingServiceError("execution_persistence_failed") from None
1013
+
1014
+ def _load_task_worktree(self, task_id: str) -> tuple[TaskWorktree, dict[str, Any]]:
1015
+ self.store.initialize()
1016
+ record = self.store.latest_task_worktree_record(task_id)
1017
+ if record is None:
1018
+ raise RoutingServiceError("worktree_missing")
1019
+ worktree_id = str(record["worktree_id"])
1020
+ candidate = self.state_dir / "worktrees" / "tasks" / worktree_id
1021
+ try:
1022
+ root = candidate.resolve(strict=True)
1023
+ except OSError:
1024
+ raise RoutingServiceError("worktree_missing") from None
1025
+ if hashlib.sha256(str(root).encode()).hexdigest() != record["canonical_root_hash"]:
1026
+ raise RoutingServiceError("worktree_identity_drift")
1027
+ try:
1028
+ runner = GitRunner(root)
1029
+ common_result = runner.run(
1030
+ ["rev-parse", "--git-common-dir"],
1031
+ timeout_seconds=5.0,
1032
+ max_stdout_bytes=4_096,
1033
+ )
1034
+ raw_common = common_result.stdout.decode("utf-8").strip()
1035
+ common_candidate = Path(raw_common)
1036
+ if not common_candidate.is_absolute():
1037
+ common_candidate = root / common_candidate
1038
+ common = common_candidate.resolve(strict=True)
1039
+ except (GitError, UnicodeDecodeError, OSError):
1040
+ raise RoutingServiceError("worktree_identity_drift") from None
1041
+ if common_result.returncode != 0 or common != (self.root / ".git").resolve(strict=True):
1042
+ raise RoutingServiceError("worktree_identity_drift")
1043
+ return (
1044
+ TaskWorktree(
1045
+ worktree_id,
1046
+ root,
1047
+ common,
1048
+ str(record["baseline_commit"]),
1049
+ str(record["status"]),
1050
+ ),
1051
+ record,
1052
+ )
1053
+
1054
+ def accept(self, task_id: str, *, authority_granted: bool) -> dict[str, str]:
1055
+ """Create a detached, cherry-pickable integration commit; never merge it."""
1056
+ if authority_granted is not True:
1057
+ raise RoutingServiceError("accept_authority_required")
1058
+ worktree, record = self._load_task_worktree(task_id)
1059
+ if record["status"] == "accepted":
1060
+ raise RoutingServiceError("transition_replay")
1061
+ if record["status"] != "executed":
1062
+ raise RoutingServiceError("worktree_transition_invalid")
1063
+ validation = self.store.worktree_validation_record(worktree.worktree_id)
1064
+ if validation is None or validation["outcome"] != "passed":
1065
+ raise RoutingServiceError("validation_not_passed")
1066
+ evidence = collect_diff_evidence(
1067
+ TaskWorktree(
1068
+ worktree.worktree_id,
1069
+ worktree.root,
1070
+ worktree.git_common_dir,
1071
+ worktree.baseline_commit,
1072
+ "prepared",
1073
+ ),
1074
+ max_files=self.settings.max_changed_files,
1075
+ max_bytes=self.settings.max_changed_bytes,
1076
+ )
1077
+ if evidence.diff_sha256 != validation["diff_hash"]:
1078
+ raise RoutingServiceError("diff_changed_after_validation")
1079
+ if evidence.changed_files == 0:
1080
+ raise RoutingServiceError("empty_diff")
1081
+ runner = GitRunner(worktree.root)
1082
+ try:
1083
+ add = runner.run(
1084
+ ["add", "--all", "--"], timeout_seconds=15.0, max_stdout_bytes=4_096
1085
+ )
1086
+ commit = runner.run(
1087
+ [
1088
+ "-c",
1089
+ "user.name=Graphite Router",
1090
+ "-c",
1091
+ "user.email=graphite-router@localhost",
1092
+ "commit",
1093
+ "--no-gpg-sign",
1094
+ "-m",
1095
+ f"graphite: accepted task {task_id}",
1096
+ ],
1097
+ timeout_seconds=30.0,
1098
+ max_stdout_bytes=256 * 1024,
1099
+ )
1100
+ head = runner.run(
1101
+ ["rev-parse", "HEAD"], timeout_seconds=5.0, max_stdout_bytes=256
1102
+ )
1103
+ commit_id = head.stdout.decode("ascii").strip()
1104
+ except (GitError, UnicodeDecodeError, OSError):
1105
+ raise RoutingServiceError("integration_commit_failed") from None
1106
+ if add.returncode != 0 or commit.returncode != 0 or head.returncode != 0:
1107
+ raise RoutingServiceError("integration_commit_failed")
1108
+ self.store.transition_task_worktree(
1109
+ worktree.worktree_id,
1110
+ expected_status="executed",
1111
+ new_status="accepted",
1112
+ updated_at=int(time.time()),
1113
+ )
1114
+ self.store.record_outcome(
1115
+ "outcome-" + secrets.token_hex(12),
1116
+ str(validation["execution_id"]),
1117
+ "human",
1118
+ True,
1119
+ False,
1120
+ int(time.time()),
1121
+ )
1122
+ return {
1123
+ "task_id": task_id,
1124
+ "worktree_id": worktree.worktree_id,
1125
+ "status": "accepted",
1126
+ "commit_id": commit_id,
1127
+ "integration": "explicit_cherry_pick_required",
1128
+ }
1129
+
1130
+ def reject(self, task_id: str, *, authority_granted: bool) -> dict[str, str]:
1131
+ if authority_granted is not True:
1132
+ raise RoutingServiceError("reject_authority_required")
1133
+ worktree, record = self._load_task_worktree(task_id)
1134
+ if record["status"] == "rejected":
1135
+ raise RoutingServiceError("transition_replay")
1136
+ if record["status"] != "executed":
1137
+ raise RoutingServiceError("worktree_transition_invalid")
1138
+ validation = self.store.worktree_validation_record(worktree.worktree_id)
1139
+ if validation is None or validation["execution_id"] is None:
1140
+ raise RoutingServiceError("execution_evidence_missing")
1141
+ self.store.transition_task_worktree(
1142
+ worktree.worktree_id,
1143
+ expected_status="executed",
1144
+ new_status="rejected",
1145
+ updated_at=int(time.time()),
1146
+ )
1147
+ self.store.record_outcome(
1148
+ "outcome-" + secrets.token_hex(12),
1149
+ str(validation["execution_id"]),
1150
+ "human",
1151
+ False,
1152
+ False,
1153
+ int(time.time()),
1154
+ )
1155
+ return {
1156
+ "task_id": task_id,
1157
+ "worktree_id": worktree.worktree_id,
1158
+ "status": "rejected",
1159
+ "cleanup": "separate_explicit_authority_required",
1160
+ }
1161
+
1162
+ def cleanup(self, task_id: str, *, authority_granted: bool) -> dict[str, str]:
1163
+ if authority_granted is not True:
1164
+ raise RoutingServiceError("cleanup_authority_required")
1165
+ worktree, record = self._load_task_worktree(task_id)
1166
+ status = str(record["status"])
1167
+ if status == "cleaned":
1168
+ raise RoutingServiceError("transition_replay")
1169
+ if status not in {"accepted", "rejected", "quarantined"}:
1170
+ raise RoutingServiceError("worktree_transition_invalid")
1171
+ cleanup_task_worktree(
1172
+ worktree,
1173
+ state_root=self.state_dir / "worktrees",
1174
+ task_id=worktree.worktree_id,
1175
+ terminal_status=status,
1176
+ authority_granted=True,
1177
+ )
1178
+ self.store.transition_task_worktree(
1179
+ worktree.worktree_id,
1180
+ expected_status=status,
1181
+ new_status="cleaned",
1182
+ updated_at=int(time.time()),
1183
+ )
1184
+ return {
1185
+ "task_id": task_id,
1186
+ "worktree_id": worktree.worktree_id,
1187
+ "status": "cleaned",
1188
+ }
1189
+
1190
+ def status(self) -> dict[str, Any]:
1191
+ try:
1192
+ self.store.initialize()
1193
+ integrity = self.store.integrity_check()
1194
+ snapshots = load_verified_capability_snapshots(
1195
+ self.store, now=int(time.time())
1196
+ )
1197
+ except (StorageError, OSError, ValueError):
1198
+ integrity = "unavailable"
1199
+ snapshots = ()
1200
+ return {
1201
+ "routing": "ready" if integrity == "ok" and snapshots else "not_ready",
1202
+ "storage": integrity,
1203
+ "verified_profiles": len(snapshots),
1204
+ "providers": sorted({item.profile.provider.value for item in snapshots}),
1205
+ "authority": "single_use_approval_required",
1206
+ "automatic_execution": False,
1207
+ "automatic_retry": False,
1208
+ "automatic_fallback": False,
1209
+ "automatic_merge": False,
1210
+ }
1211
+
1212
+ def recoverable_attempts(
1213
+ self, *, limit: int = DEFAULT_RECOVERY_PAGE_SIZE, after: str | None = None
1214
+ ) -> RecoverableAttemptPage:
1215
+ self.store.initialize()
1216
+ return self.store.recoverable_attempts(limit=limit, after=after)
1217
+
1218
+ def reconcile_execution(self, attempt_id: str) -> dict[str, Any]:
1219
+ self.store.initialize()
1220
+ receipt = self.store.reconcile_execution_attempt(
1221
+ attempt_id, completed_at=int(time.time())
1222
+ )
1223
+ return dict(receipt.to_dict())
1224
+
1225
+ def policy(
1226
+ self,
1227
+ *,
1228
+ promote: str | None = None,
1229
+ rollback: str | None = None,
1230
+ authority_granted: bool = False,
1231
+ ) -> dict[str, Any]:
1232
+ if promote is not None and rollback is not None:
1233
+ raise RoutingServiceError("policy_action_conflict")
1234
+ if promote is not None or rollback is not None:
1235
+ self.store.initialize()
1236
+ selected = promote or rollback
1237
+ assert selected is not None
1238
+ try:
1239
+ self.store.activate_cli_policy(
1240
+ selected,
1241
+ action="promote" if promote else "rollback",
1242
+ authority_granted=authority_granted,
1243
+ created_at=int(time.time()),
1244
+ )
1245
+ except ValueError as exc:
1246
+ raise RoutingServiceError(str(exc)) from exc
1247
+ return {
1248
+ "policy_version": promote or rollback or "3",
1249
+ "requested_action": (
1250
+ "promote" if promote else "rollback" if rollback else "inspect"
1251
+ ),
1252
+ "execution_authority": "single_use_approval_required",
1253
+ "automatic_execution": False,
1254
+ }
1255
+
1256
+ def record_outcome(self, **values: Any) -> dict[str, Any]:
1257
+ if values.get("provenance") != "human":
1258
+ raise RoutingServiceError("supported_evidence_import_required")
1259
+ return {
1260
+ "recorded": True,
1261
+ "provenance": "human",
1262
+ "autonomy_admissible": False,
1263
+ }