engineering-platform 2.2.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 (130) hide show
  1. engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
  2. engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
  3. engineering_platform/__init__.py +1 -0
  4. engineering_platform/__main__.py +7 -0
  5. engineering_platform/agent_state.py +530 -0
  6. engineering_platform/agent_trust.py +174 -0
  7. engineering_platform/assets/dashboard.css +1317 -0
  8. engineering_platform/assets/dashboard.js +8534 -0
  9. engineering_platform/assets/dashboard_locales.mjs +4049 -0
  10. engineering_platform/assets/dashboard_status_store.mjs +41 -0
  11. engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
  12. engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
  13. engineering_platform/assets/operations-console/icon-dark.png +0 -0
  14. engineering_platform/assets/operations-console/icon-light.png +0 -0
  15. engineering_platform/assets/operations-console/icon-transparent.png +0 -0
  16. engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
  17. engineering_platform/capability_preflight.py +285 -0
  18. engineering_platform/capability_review.py +261 -0
  19. engineering_platform/central_data_transfer.py +195 -0
  20. engineering_platform/central_database.py +245 -0
  21. engineering_platform/central_store_migration.py +1672 -0
  22. engineering_platform/codex_capacity.py +81 -0
  23. engineering_platform/codex_chat.py +226 -0
  24. engineering_platform/codex_observability.py +153 -0
  25. engineering_platform/component_lock.py +40 -0
  26. engineering_platform/component_logging.py +420 -0
  27. engineering_platform/console_presentation.py +14 -0
  28. engineering_platform/console_route_ownership.py +83 -0
  29. engineering_platform/contracts/__init__.py +38 -0
  30. engineering_platform/contracts/ep_consumer.py +391 -0
  31. engineering_platform/contracts/models.py +105 -0
  32. engineering_platform/contracts/projection.py +401 -0
  33. engineering_platform/dashboard_browser_validation.py +206 -0
  34. engineering_platform/dashboard_state.py +630 -0
  35. engineering_platform/dashboard_supervisor.swift +105 -0
  36. engineering_platform/dashboard_translation.py +129 -0
  37. engineering_platform/dependabot_producer.py +349 -0
  38. engineering_platform/drift_diagnostics.py +144 -0
  39. engineering_platform/emergency_recovery.py +268 -0
  40. engineering_platform/engineering_memory.py +139 -0
  41. engineering_platform/ep_consumer_credentials.py +473 -0
  42. engineering_platform/evidence_projection.py +213 -0
  43. engineering_platform/execution_activity.py +218 -0
  44. engineering_platform/execution_context.py +132 -0
  45. engineering_platform/execution_errors.py +42 -0
  46. engineering_platform/execution_evidence.py +24 -0
  47. engineering_platform/execution_executor.py +730 -0
  48. engineering_platform/execution_finalization.py +44 -0
  49. engineering_platform/execution_host.py +3306 -0
  50. engineering_platform/execution_lease.py +365 -0
  51. engineering_platform/execution_lifecycle.py +447 -0
  52. engineering_platform/execution_models.py +43 -0
  53. engineering_platform/execution_readiness.py +166 -0
  54. engineering_platform/execution_reporting.py +1607 -0
  55. engineering_platform/execution_repository.py +253 -0
  56. engineering_platform/execution_timeout_policy.py +56 -0
  57. engineering_platform/execution_timing.py +440 -0
  58. engineering_platform/execution_transaction.py +28 -0
  59. engineering_platform/external_producer_binding.py +235 -0
  60. engineering_platform/file_inbox.py +249 -0
  61. engineering_platform/forensic_attribution.py +338 -0
  62. engineering_platform/forensic_attribution_v2.py +134 -0
  63. engineering_platform/forensic_delta.py +299 -0
  64. engineering_platform/golden_scenario.py +63 -0
  65. engineering_platform/historical_dashboard_configuration.py +171 -0
  66. engineering_platform/host_admin.py +199 -0
  67. engineering_platform/host_preflight.py +231 -0
  68. engineering_platform/installation_relocation.py +122 -0
  69. engineering_platform/investigation_ledger.py +89 -0
  70. engineering_platform/legacy_inbox_migration.py +79 -0
  71. engineering_platform/lifecycle_worker.py +223 -0
  72. engineering_platform/live_status.py +267 -0
  73. engineering_platform/local_api.py +209 -0
  74. engineering_platform/local_api_keychain.py +51 -0
  75. engineering_platform/local_repository_binding.py +138 -0
  76. engineering_platform/managed_autonomy.py +509 -0
  77. engineering_platform/managed_codex_runtime.py +105 -0
  78. engineering_platform/parity_context.py +203 -0
  79. engineering_platform/parity_lifecycle_dispatcher.py +488 -0
  80. engineering_platform/platform_admin.py +13 -0
  81. engineering_platform/platform_api.py +428 -0
  82. engineering_platform/platform_bootstrap.py +385 -0
  83. engineering_platform/platform_components.py +65 -0
  84. engineering_platform/platform_version.py +171 -0
  85. engineering_platform/pr_check_repair.py +276 -0
  86. engineering_platform/pr_evidence_backfill.py +278 -0
  87. engineering_platform/producer.py +209 -0
  88. engineering_platform/project_agent.py +366 -0
  89. engineering_platform/project_agent_service.py +244 -0
  90. engineering_platform/project_topology.py +126 -0
  91. engineering_platform/prompt_history.py +591 -0
  92. engineering_platform/provider_context.py +136 -0
  93. engineering_platform/provider_context_benchmark.py +41 -0
  94. engineering_platform/provider_context_scope.py +90 -0
  95. engineering_platform/provider_interruption.py +168 -0
  96. engineering_platform/provider_process_identity.py +80 -0
  97. engineering_platform/provider_readiness.py +138 -0
  98. engineering_platform/provider_recovery.py +647 -0
  99. engineering_platform/provider_usage.py +497 -0
  100. engineering_platform/providers.py +471 -0
  101. engineering_platform/qualification.py +220 -0
  102. engineering_platform/recommendation_handoff.py +238 -0
  103. engineering_platform/report_analysis.py +193 -0
  104. engineering_platform/repository_attachment.py +171 -0
  105. engineering_platform/repository_handoff.py +95 -0
  106. engineering_platform/resources.py +38 -0
  107. engineering_platform/reviewer_evidence.py +70 -0
  108. engineering_platform/schemas/repository-attachment.schema.json +61 -0
  109. engineering_platform/server.py +3679 -0
  110. engineering_platform/server_console_services.py +2024 -0
  111. engineering_platform/server_relay.py +172 -0
  112. engineering_platform/server_service.py +122 -0
  113. engineering_platform/status_model.py +135 -0
  114. engineering_platform/status_reconciliation.py +34 -0
  115. engineering_platform/storage.py +2440 -0
  116. engineering_platform/submission_cli.py +77 -0
  117. engineering_platform/submission_intake.py +45 -0
  118. engineering_platform/submission_service.py +317 -0
  119. engineering_platform/telemetry.py +951 -0
  120. engineering_platform/templates/workspace-config.json +25 -0
  121. engineering_platform/validation_identity.py +50 -0
  122. engineering_platform/validation_profile.py +211 -0
  123. engineering_platform/workspace_preflight.py +263 -0
  124. engineering_platform/worktree_provenance.py +147 -0
  125. engineering_platform/worktree_tooling.py +18 -0
  126. engineering_platform-2.2.0.dist-info/METADATA +18 -0
  127. engineering_platform-2.2.0.dist-info/RECORD +130 -0
  128. engineering_platform-2.2.0.dist-info/WHEEL +5 -0
  129. engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
  130. engineering_platform-2.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,730 @@
1
+ """Codex execution evidence helpers, isolated from lifecycle coordination."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import replace
5
+ from datetime import datetime, timezone
6
+ import json
7
+ import logging
8
+ import os
9
+ from pathlib import Path
10
+ import re
11
+ import signal
12
+ import sqlite3
13
+ import subprocess
14
+ import tempfile
15
+ import time
16
+ from threading import Event, Thread
17
+ from typing import Callable, Mapping
18
+
19
+ from .capability_review import ReviewerResult, ReviewerSelection, reviewer_prompt
20
+ from .codex_observability import codex_final_message as _codex_final_message, extract_codex_runtime_metadata, extract_codex_usage
21
+ from .evidence_projection import ToolProxyEnvironment
22
+ from .execution_context import additional_workspace_write_roots
23
+ from .execution_errors import CodexHandoffTimeout, CodexInvocationError, RunnerError
24
+ from .execution_timeout_policy import SPECIALIST_REVIEW
25
+ from .execution_models import AgentResult
26
+ from .platform_version import detected_codex_cli_version
27
+ from .provider_usage import churn_from_jsonl, usage_from_jsonl, usage_snapshots_from_jsonl
28
+ from .providers import CodexCliProvider
29
+ from .reviewer_evidence import ReviewerEvidence
30
+ from .storage import EngineeringStorageError, open_storage, record_artifact, verify_artifact_integrity
31
+ from .agent_state import redact_diagnostic
32
+ from .component_logging import component_logger, log_event
33
+
34
+
35
+ _LIVE_ACTION_NAME_DISALLOWED = re.compile(r"(?:https?://|[/\\\\`]|\b(?:api[_ -]?key|token|secret|password|authorization|bearer)\b)", re.IGNORECASE)
36
+ _CODEX_USAGE_LIMIT = re.compile(
37
+ r"(?:you(?:'ve| have) hit your usage limit|purchase more credits|try again at)",
38
+ re.IGNORECASE,
39
+ )
40
+ _QUALITY_EVIDENCE_ACTIVITIES = frozenset({"REFACTOR", "TEST_COVERAGE", "DOCUMENTATION", "VALIDATION", "NO_CHANGE_REQUIRED"})
41
+ MAX_RETAINED_VALIDATION_OUTPUT_CHARACTERS = 8_000
42
+ REVIEWER_INVOCATION_TIMEOUT_SECONDS = SPECIALIST_REVIEW.seconds
43
+ _VALIDATION_STREAM_LIMIT = MAX_RETAINED_VALIDATION_OUTPUT_CHARACTERS // 2
44
+ _UNITTEST_FAILURE = re.compile(r"^(?:FAIL|ERROR): [^(]+ \(([^)]+)\)$", re.MULTILINE)
45
+ _UNITTEST_COUNTS = re.compile(r"FAILED \((?P<details>[^)]*)\)")
46
+ _UNITTEST_COUNT = re.compile(r"\b(?P<name>failures|errors)=(?P<count>\d+)\b")
47
+ _TURN_ABORTED = re.compile(r'"type"\s*:\s*"turn_aborted"[^\n]*"reason"\s*:\s*"interrupted"', re.IGNORECASE)
48
+
49
+
50
+ def provider_turn_interruption(stdout: str, stderr: str) -> str | None:
51
+ """Classify only provider-proven interrupted turns without inventing a result."""
52
+ if _TURN_ABORTED.search(f"{stdout}\n{stderr}"):
53
+ return "interrupted"
54
+ return None
55
+
56
+
57
+ def codex_failure_disposition(
58
+ exit_code: int, stdout: str, stderr: str
59
+ ) -> tuple[str, str, str]:
60
+ """Return the safe action and checkpoint status for a Codex CLI failure.
61
+
62
+ A provider-side quota is not an implementation failure and, crucially, is
63
+ not evidence that a prior pull request still awaits an operator. Keep the
64
+ provider wording out of durable state while retaining a specific recovery
65
+ path for the Operations Console.
66
+ """
67
+ if _CODEX_USAGE_LIMIT.search(f"{stderr}\n{stdout}"):
68
+ return (
69
+ "resolve_codex_usage_limit",
70
+ "codex_usage_limit_reached",
71
+ "Codex usage limit reached. Add Codex credits or resume after the account limit resets.",
72
+ )
73
+ return (
74
+ "inspect_codex_cli",
75
+ "codex_invocation_failed",
76
+ f"Codex CLI exited with code {exit_code}; inspect this invocation's console output.",
77
+ )
78
+
79
+
80
+ def project_codex_activity(event: object) -> str | None:
81
+ """Map one JSONL event to an approved, prompt-free activity label."""
82
+ if not isinstance(event, dict) or event.get("type") not in {"item.started", "item.updated"}:
83
+ return None
84
+ item = event.get("item")
85
+ if not isinstance(item, dict):
86
+ return None
87
+ return {
88
+ "reasoning": "Codex plant de volgende stap",
89
+ "command_execution": "Codex voert een opdracht uit",
90
+ "file_change": "Codex bewerkt bestanden",
91
+ "web_search": "Codex onderzoekt referentiemateriaal",
92
+ "mcp_tool_call": "Codex gebruikt een ontwikkeltool",
93
+ "agent_message": "Codex formuleert het resultaat",
94
+ }.get(item.get("type"))
95
+
96
+
97
+ def project_codex_live_action_name(event: object) -> str | None:
98
+ """Return a short, transient Codex reasoning title when it is safe to show.
99
+
100
+ This is intentionally separate from the persisted activity category. The
101
+ value is only rendered in the live status file and is removed when the run
102
+ reaches a terminal phase; reports, history, diagnostics and the database
103
+ never receive it.
104
+ """
105
+ if not isinstance(event, dict) or event.get("type") not in {"item.started", "item.updated"}:
106
+ return None
107
+ item = event.get("item")
108
+ if not isinstance(item, dict) or item.get("type") != "reasoning":
109
+ return None
110
+ text = item.get("text")
111
+ if not isinstance(text, str):
112
+ return None
113
+ title = redact_diagnostic(text, limit=160)
114
+ if len(title) < 4 or "[REDACTED]" in title or _LIVE_ACTION_NAME_DISALLOWED.search(title):
115
+ return None
116
+ return title
117
+
118
+
119
+ def project_codex_command_event(event: object) -> tuple[str, str, str, int | None] | None:
120
+ """Expose direct command boundaries without retaining command content.
121
+
122
+ Codex JSONL identifies command-execution items by a stable item id. The
123
+ host uses this small projection only to classify known validation tools and
124
+ record their observed start/complete boundaries. It must not persist the
125
+ raw command, its output, or any arguments.
126
+ """
127
+ if not isinstance(event, dict) or event.get("type") not in {"item.started", "item.completed"}:
128
+ return None
129
+ item = event.get("item")
130
+ if not isinstance(item, dict) or item.get("type") != "command_execution":
131
+ return None
132
+ item_id = item.get("id")
133
+ if not isinstance(item_id, str) or not item_id:
134
+ return None
135
+ if event["type"] == "item.completed":
136
+ exit_code = item.get("exit_code")
137
+ return ("completed", item_id, "", exit_code if isinstance(exit_code, int) and not isinstance(exit_code, bool) else None)
138
+ command = item.get("command")
139
+ if not isinstance(command, str):
140
+ return None
141
+ return ("started", item_id, command, None)
142
+
143
+
144
+ def redacted_cli_tail(value: str, prompt: str, *, limit: int = 1_200) -> str:
145
+ without_prompt = value.replace(prompt, "[PROMPT_OMITTED]") if prompt else value
146
+ return redact_diagnostic("\n".join(without_prompt.splitlines()[-60:]), limit=limit) or "(empty)"
147
+
148
+
149
+ def format_cli_failure(exit_code: int, stderr: str, stdout: str, prompt: str = "") -> str:
150
+ return "\n".join((f"Codex CLI exit code: {exit_code}", f"stderr tail: {redacted_cli_tail(stderr, prompt)}", f"stdout tail: {redacted_cli_tail(stdout, prompt)}"))
151
+
152
+
153
+ def record_redacted_codex_cli_diagnostic(
154
+ root: Path, run_id: str, detail: str, *, central_database: Path,
155
+ ) -> None:
156
+ """Persist a bounded CLI failure diagnostic through CENTRAL only.
157
+
158
+ The former checkout-local ``.engineering/logs/codex`` file was a second
159
+ durable operational log surface. The Execution Host is owned by the
160
+ lifecycle worker, so its diagnostic belongs to that canonical component
161
+ identity and to the run that produced it.
162
+ """
163
+ logger = component_logger(root, "lifecycle_worker", central_database=central_database)
164
+ log_event(
165
+ logger,
166
+ logging.WARNING,
167
+ "codex_cli_diagnostic",
168
+ run_id=run_id,
169
+ diagnostic=redact_diagnostic(detail, limit=3_000),
170
+ context={"target_component": "lifecycle_worker"},
171
+ )
172
+
173
+
174
+ def _bounded_redacted_validation_tail(value: str | None, *, limit: int = _VALIDATION_STREAM_LIMIT) -> tuple[str | None, bool, bool]:
175
+ if not isinstance(value, str):
176
+ return None, False, False
177
+ tail = value[-limit:]
178
+ # Apply the repository's existing redactor line-by-line so traceback
179
+ # boundaries remain useful while its secret policy remains authoritative.
180
+ rendered = "\n".join(redact_diagnostic(line, limit=limit) for line in tail.replace("\x00", " ").splitlines())
181
+ if len(rendered) > limit:
182
+ rendered = rendered[-limit:]
183
+ return rendered, len(value) > limit, rendered != tail
184
+
185
+
186
+ def validation_failure_artifact_id(command_id: str) -> str:
187
+ return f"validation-failure-diagnostic-{command_id}"
188
+
189
+
190
+ def persist_validation_failure_diagnostic(
191
+ root: Path, *, run_id: str, command_id: str, validation_id: str,
192
+ control_identity: str, exit_code: int | None, stdout: str | None,
193
+ stderr: str | None, capture_available: bool, captured_at: str | None = None,
194
+ central_database: Path | None = None, artifact_root: Path | None = None,
195
+ ) -> str:
196
+ """Persist bounded, redacted, supplementary output for any failed control."""
197
+ # Extract stable unittest identifiers/counts before the generic redactor
198
+ # treats ``name=value`` failure summaries as environment assignments. Raw
199
+ # output remains in process memory only and is never persisted wholesale.
200
+ raw_combined = "\n".join(value for value in (stdout, stderr) if isinstance(value, str))
201
+ identities = list(dict.fromkeys(_UNITTEST_FAILURE.findall(raw_combined)))[:20]
202
+ details = _UNITTEST_COUNTS.search(raw_combined)
203
+ counts = {match.group("name"): int(match.group("count")) for match in _UNITTEST_COUNT.finditer(details.group("details"))} if details else {}
204
+ stdout_tail, stdout_truncated, stdout_redacted = _bounded_redacted_validation_tail(stdout)
205
+ stderr_tail, stderr_truncated, stderr_redacted = _bounded_redacted_validation_tail(stderr)
206
+ capture_is_available = capture_available and stdout_tail is not None and stderr_tail is not None
207
+ created_at = captured_at or datetime.now(timezone.utc).isoformat()
208
+ artifact_id = validation_failure_artifact_id(command_id)
209
+ payload = {
210
+ "schema": "deterministic-validation-failure-diagnostic-v1",
211
+ "validation_id": validation_id, "command_id": command_id,
212
+ "control_identity": control_identity, "authoritative_exit_code": exit_code,
213
+ "captured_at": created_at,
214
+ "capture_status": "AVAILABLE" if capture_is_available else "UNAVAILABLE",
215
+ "stdout_tail": stdout_tail, "stderr_tail": stderr_tail,
216
+ "stdout_truncated": stdout_truncated, "stderr_truncated": stderr_truncated,
217
+ "retained_output_characters": sum(len(value or "") for value in (stdout_tail, stderr_tail)),
218
+ "maximum_retained_output_characters": MAX_RETAINED_VALIDATION_OUTPUT_CHARACTERS,
219
+ "truncation_strategy": "tail_per_stream",
220
+ "redaction_applied": stdout_redacted or stderr_redacted,
221
+ "redaction_policy": "agent_state.redact_diagnostic/v1",
222
+ "failing_test_identities": identities,
223
+ "failure_count": counts.get("failures"), "error_count": counts.get("errors"),
224
+ }
225
+ directory = ((artifact_root / "validation-failure-diagnostics") if artifact_root else
226
+ (root / ".engineering" / "artifacts" / "validation-failure-diagnostics"))
227
+ directory.mkdir(mode=0o700, parents=True, exist_ok=True)
228
+ path = directory / f"{artifact_id}.json"
229
+ descriptor, temporary = tempfile.mkstemp(prefix=f".{artifact_id}.", suffix=".tmp", dir=directory)
230
+ try:
231
+ os.fchmod(descriptor, 0o600)
232
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
233
+ json.dump(payload, handle, indent=2, sort_keys=True)
234
+ handle.write("\n")
235
+ handle.flush()
236
+ os.fsync(handle.fileno())
237
+ os.replace(temporary, path)
238
+ finally:
239
+ Path(temporary).unlink(missing_ok=True)
240
+ try:
241
+ record_artifact(
242
+ root, path, artifact_id=artifact_id,
243
+ artifact_type="VALIDATION_FAILURE_DIAGNOSTIC",
244
+ content_type="application/json", created_at=created_at, run_id=run_id,
245
+ execution_id=command_id,
246
+ central_database=central_database, artifact_root=artifact_root,
247
+ )
248
+ except EngineeringStorageError:
249
+ path.unlink(missing_ok=True)
250
+ raise
251
+ return f"artifact:{artifact_id}"
252
+
253
+
254
+ def load_validation_failure_diagnostic(
255
+ root: Path, artifact_reference: str, *, central_database: Path | None = None,
256
+ artifact_root: Path | None = None,
257
+ ) -> dict[str, object] | None:
258
+ """Read a bound diagnostic only after its immutable artifact verifies."""
259
+ if not artifact_reference.startswith("artifact:"):
260
+ return None
261
+ artifact_id = artifact_reference.removeprefix("artifact:")
262
+ if not verify_artifact_integrity(
263
+ root, artifact_id, central_database=central_database, artifact_root=artifact_root,
264
+ ):
265
+ return None
266
+ connection = open_storage(root) if central_database is None else sqlite3.connect(central_database.resolve(), isolation_level=None)
267
+ try:
268
+ row = connection.execute(
269
+ "SELECT storage_location,artifact_type FROM execution_artifact_records WHERE artifact_id=?",
270
+ (artifact_id,),
271
+ ).fetchone()
272
+ finally:
273
+ connection.close()
274
+ if not row or row[1] != "VALIDATION_FAILURE_DIAGNOSTIC":
275
+ return None
276
+ try:
277
+ authority_root = artifact_root.resolve() if artifact_root is not None else (root / ".engineering").resolve()
278
+ payload_path = (authority_root / str(row[0])).resolve()
279
+ payload_path.relative_to(authority_root)
280
+ payload = json.loads(payload_path.read_text(encoding="utf-8"))
281
+ except (OSError, ValueError, json.JSONDecodeError):
282
+ return None
283
+ return payload if isinstance(payload, dict) else None
284
+
285
+
286
+ _format_cli_failure = format_cli_failure
287
+
288
+ # A managed Engineering transaction has already passed host-owned admission,
289
+ # repository synchronization and an exclusive execution lease. It must be
290
+ # able to create its bounded branch, commit, and draft PR; `workspace-write`
291
+ # deliberately rejects Git index writes and therefore cannot complete that
292
+ # contract. Review-only invocations remain read-only below.
293
+ MANAGED_EXECUTION_SANDBOX = "danger-full-access"
294
+
295
+ class CodexCliClient:
296
+ def __init__(self, provider: CodexCliProvider | None = None) -> None:
297
+ self.provider = provider or CodexCliProvider()
298
+ self.last_usage: dict[str, int | float | str] = {}
299
+ self.last_usage_snapshots: tuple[dict[str, int], ...] = ()
300
+ self.last_churn: dict[str, int] = {}
301
+ self.last_context_escalations: tuple[dict[str, object], ...] = ()
302
+ self.last_execution_seconds: float | None = None
303
+ self.last_runtime_metadata = self._runtime_metadata()
304
+ # This deliberately contains only aggregate counters, plus the
305
+ # approved EP-managed CLI prefix as invocation provenance. Command
306
+ # text and command output are never retained in execution metadata.
307
+ self.last_execution_metadata: dict[str, int] = {
308
+ "modified": 0,
309
+ "created": 0,
310
+ "deleted": 0,
311
+ "codex_commands_executed": 0,
312
+ }
313
+ self._activity_callback: Callable[[str], None] | None = None
314
+ self._transient_action_callback: Callable[[str], None] | None = None
315
+ self._process_callback: Callable[[dict[str, int] | None], None] | None = None
316
+ self._runtime_metadata_callback: Callable[[dict[str, str]], None] | None = None
317
+ self._command_callback: Callable[..., None] | None = None
318
+ self._workspace_progress_callback: Callable[[dict[str, int]], None] | None = None
319
+ self._handoff_deadline_callback: Callable[[], bool] | None = None
320
+
321
+ def _runtime_metadata(self) -> dict[str, str]:
322
+ metadata = {"runtime_provider": "codex_cli"}
323
+ installation_path_reader = getattr(self.provider, "managed_installation_path", None)
324
+ installation_path = installation_path_reader() if callable(installation_path_reader) else None
325
+ if isinstance(installation_path, str) and installation_path:
326
+ metadata["codex_cli_installation_path"] = installation_path
327
+ return metadata
328
+
329
+ def set_activity_callback(self, callback: Callable[[str], None] | None) -> None:
330
+ """Set the optional local-only sink for safe live activity labels."""
331
+ self._activity_callback = callback
332
+
333
+ def set_transient_action_callback(self, callback: Callable[[str], None] | None) -> None:
334
+ """Set the non-persistent sink for a safe live Codex action name."""
335
+ self._transient_action_callback = callback
336
+
337
+ def set_process_callback(self, callback: Callable[[dict[str, int] | None], None] | None) -> None:
338
+ """Set the owned foreground Codex-process sink for runtime metrics."""
339
+ self._process_callback = callback
340
+
341
+ def set_runtime_metadata_callback(
342
+ self, callback: Callable[[dict[str, str]], None] | None
343
+ ) -> None:
344
+ """Publish only explicitly reported runtime settings during a live run."""
345
+ self._runtime_metadata_callback = callback
346
+
347
+ def set_command_callback(self, callback: Callable[..., None] | None) -> None:
348
+ """Set a direct JSONL command-boundary sink for execution telemetry."""
349
+ self._command_callback = callback
350
+
351
+ def set_workspace_progress_callback(
352
+ self, callback: Callable[[dict[str, int]], None] | None
353
+ ) -> None:
354
+ """Set a bounded, filename-free workspace change counter sink."""
355
+ self._workspace_progress_callback = callback
356
+
357
+ def set_handoff_deadline_callback(self, callback: Callable[[], bool] | None) -> None:
358
+ """Set a host-owned deadline check for an externally observable hand-off."""
359
+ self._handoff_deadline_callback = callback
360
+
361
+ def available(self) -> bool:
362
+ return self.provider.command("--version").returncode == 0
363
+
364
+ def version(self) -> str:
365
+ completed = self.provider.command("--version")
366
+ if completed.returncode:
367
+ raise RunnerError("Codex CLI version could not be detected")
368
+ return detected_codex_cli_version(completed.stdout)
369
+
370
+ def review(
371
+ self,
372
+ root: Path,
373
+ selection: ReviewerSelection,
374
+ objective: str,
375
+ evidence: ReviewerEvidence | None = None,
376
+ ) -> ReviewerResult:
377
+ self.last_usage = {}
378
+ self.last_churn = {}
379
+ self.last_context_escalations = ()
380
+ self.last_execution_seconds = None
381
+ self.last_runtime_metadata = self._runtime_metadata()
382
+ schema = {
383
+ "type": "object",
384
+ "additionalProperties": False,
385
+ "required": ["contribution", "recommendations"],
386
+ "properties": {
387
+ "contribution": {"type": "string", "maxLength": 240},
388
+ "recommendations": {
389
+ "type": "array",
390
+ "maxItems": 3,
391
+ "items": {"type": "string", "maxLength": 240},
392
+ },
393
+ },
394
+ }
395
+ state_directory = root / ".engineering"
396
+ state_directory.mkdir(mode=0o700, parents=True, exist_ok=True)
397
+ with tempfile.NamedTemporaryFile(
398
+ "w", encoding="utf-8", suffix=".json", dir=state_directory, delete=False
399
+ ) as handle:
400
+ json.dump(schema, handle)
401
+ schema_path = Path(handle.name)
402
+ try:
403
+ started = time.monotonic()
404
+ proxy = ToolProxyEnvironment()
405
+ with proxy as environment:
406
+ completed = self.provider.invoke(
407
+ root,
408
+ (
409
+ "codex",
410
+ "exec",
411
+ "--sandbox",
412
+ "read-only",
413
+ "-C",
414
+ str(root),
415
+ "--json",
416
+ "--output-schema",
417
+ str(schema_path),
418
+ reviewer_prompt(selection, objective, evidence),
419
+ ), environment=environment, timeout=REVIEWER_INVOCATION_TIMEOUT_SECONDS,
420
+ )
421
+ self.last_context_escalations = proxy.context_escalations()
422
+ self.last_execution_seconds = round(time.monotonic() - started, 3)
423
+ self.last_usage = extract_codex_usage(completed.stdout, completed.stderr)
424
+ self.last_usage.update(usage_from_jsonl(completed.stdout, completed.stderr))
425
+ self.last_usage_snapshots = usage_snapshots_from_jsonl(completed.stdout, completed.stderr)
426
+ self.last_churn = churn_from_jsonl(completed.stdout, completed.stderr)
427
+ self.last_runtime_metadata.update(extract_codex_runtime_metadata(
428
+ completed.stdout, completed.stderr
429
+ ))
430
+ finally:
431
+ schema_path.unlink(missing_ok=True)
432
+ if completed.returncode:
433
+ return ReviewerResult(
434
+ selection.reviewer,
435
+ "Reviewer invocation failed; primary review continues.",
436
+ failed=True,
437
+ usage=dict(self.last_usage), runtime_metadata=dict(self.last_runtime_metadata),
438
+ churn=dict(self.last_churn), duration_seconds=self.last_execution_seconds,
439
+ usage_snapshots=self.last_usage_snapshots,
440
+ )
441
+ try:
442
+ raw = json.loads(_codex_final_message(completed.stdout))
443
+ return ReviewerResult(
444
+ selection.reviewer,
445
+ str(raw["contribution"]),
446
+ tuple(str(value) for value in raw["recommendations"]),
447
+ usage=dict(self.last_usage), runtime_metadata=dict(self.last_runtime_metadata),
448
+ churn=dict(self.last_churn), duration_seconds=self.last_execution_seconds,
449
+ usage_snapshots=self.last_usage_snapshots,
450
+ )
451
+ except (IndexError, KeyError, TypeError, json.JSONDecodeError):
452
+ return ReviewerResult(
453
+ selection.reviewer,
454
+ "Reviewer returned invalid advice; primary review continues.",
455
+ failed=True,
456
+ usage=dict(self.last_usage), runtime_metadata=dict(self.last_runtime_metadata),
457
+ churn=dict(self.last_churn), duration_seconds=self.last_execution_seconds,
458
+ usage_snapshots=self.last_usage_snapshots,
459
+ )
460
+
461
+ def invoke(self, root: Path, prompt: str) -> AgentResult:
462
+ self.last_usage = {}
463
+ self.last_usage_snapshots = ()
464
+ self.last_churn = {}
465
+ self.last_context_escalations = ()
466
+ self.last_execution_seconds = None
467
+ self.last_runtime_metadata = self._runtime_metadata()
468
+ schema = {
469
+ "type": "object",
470
+ "additionalProperties": False,
471
+ "required": [
472
+ "terminal_state",
473
+ "branch",
474
+ "pull_request",
475
+ "terminal_condition",
476
+ "diagnostic",
477
+ "repository_path",
478
+ "commit_sha",
479
+ "validation_evidence",
480
+ "quality_evidence",
481
+ "validation_disposition",
482
+ ],
483
+ "properties": {
484
+ "terminal_state": {
485
+ "type": "string",
486
+ "enum": ["COMPLETE", "WAITING", "BLOCKED", "FAILED"],
487
+ },
488
+ "branch": {"type": ["string", "null"]},
489
+ "pull_request": {"type": ["integer", "null"]},
490
+ "terminal_condition": {
491
+ "type": "string",
492
+ "enum": [
493
+ "repository_reconciled",
494
+ "open_pr_checks_terminal",
495
+ "external_blocked",
496
+ "local_commit_reconciled",
497
+ ],
498
+ },
499
+ "diagnostic": {"type": "string", "maxLength": 500},
500
+ "repository_path": {"type": ["string", "null"]},
501
+ "commit_sha": {"type": ["string", "null"], "pattern": "^[0-9a-f]{40}$"},
502
+ "validation_evidence": {
503
+ "type": "array", "maxItems": 12,
504
+ "items": {"type": "object", "additionalProperties": False,
505
+ "required": ["command", "result"],
506
+ "properties": {"command": {"type": "string", "maxLength": 240}, "result": {"type": "string", "maxLength": 240}}},
507
+ },
508
+ "quality_evidence": {
509
+ "type": "array", "maxItems": 8,
510
+ "items": {"type": "object", "additionalProperties": False,
511
+ "required": ["activity", "result"],
512
+ "properties": {"activity": {"type": "string", "enum": sorted(_QUALITY_EVIDENCE_ACTIVITIES)}, "result": {"type": "string", "maxLength": 240}}},
513
+ },
514
+ "validation_disposition": {
515
+ "type": "string",
516
+ "enum": ["product_failure", "environmental_instability"],
517
+ },
518
+ },
519
+ }
520
+ with tempfile.NamedTemporaryFile(
521
+ "w", encoding="utf-8", suffix=".json", delete=False
522
+ ) as handle:
523
+ json.dump(schema, handle)
524
+ schema_path = Path(handle.name)
525
+ try:
526
+ extra_roots = additional_workspace_write_roots(root)
527
+ command = [
528
+ "codex",
529
+ "exec",
530
+ "--sandbox",
531
+ MANAGED_EXECUTION_SANDBOX,
532
+ "-C",
533
+ str(root),
534
+ "--json",
535
+ ]
536
+ for extra_root in extra_roots:
537
+ command.extend(("--add-dir", str(extra_root)))
538
+ command.extend(("--output-schema", str(schema_path), prompt))
539
+ started = time.monotonic()
540
+ proxy = ToolProxyEnvironment()
541
+ with proxy as environment:
542
+ completed = self._run_invocation(tuple(command), root, environment)
543
+ self.last_context_escalations = proxy.context_escalations()
544
+ self.last_execution_seconds = round(time.monotonic() - started, 3)
545
+ self.last_usage = extract_codex_usage(completed.stdout, completed.stderr)
546
+ # Prefer one final explicit usage snapshot; legacy extraction stays
547
+ # in place for compatibility with older Codex JSONL variants.
548
+ self.last_usage.update(usage_from_jsonl(completed.stdout, completed.stderr))
549
+ self.last_usage_snapshots = usage_snapshots_from_jsonl(completed.stdout, completed.stderr)
550
+ self.last_churn = churn_from_jsonl(completed.stdout, completed.stderr)
551
+ self.last_runtime_metadata.update(extract_codex_runtime_metadata(
552
+ completed.stdout, completed.stderr
553
+ ))
554
+ finally:
555
+ schema_path.unlink(missing_ok=True)
556
+ if completed.returncode:
557
+ detail = _format_cli_failure(completed.returncode, completed.stderr, completed.stdout, prompt)
558
+ next_action, terminal_condition, diagnostic = codex_failure_disposition(
559
+ completed.returncode, completed.stdout, completed.stderr
560
+ )
561
+ interruption = provider_turn_interruption(completed.stdout, completed.stderr)
562
+ raise CodexInvocationError(
563
+ diagnostic,
564
+ detail,
565
+ next_action="NONE" if interruption else next_action,
566
+ terminal_condition="provider_turn_interrupted" if interruption else terminal_condition,
567
+ interruption_reason=interruption,
568
+ )
569
+ try:
570
+ raw = json.loads(_codex_final_message(completed.stdout))
571
+ result = AgentResult(**raw)
572
+ if not isinstance(result.validation_evidence, (list, tuple)) or not isinstance(result.quality_evidence, (list, tuple)):
573
+ raise TypeError("execution evidence must be a list")
574
+ if result.validation_disposition not in {"product_failure", "environmental_instability"}:
575
+ raise TypeError("validation disposition is invalid")
576
+ if any(
577
+ not isinstance(item, dict) or item.get("activity") not in _QUALITY_EVIDENCE_ACTIVITIES
578
+ or not item.get("result")
579
+ for item in result.quality_evidence
580
+ ):
581
+ raise TypeError("quality evidence is invalid")
582
+ result = replace(
583
+ result,
584
+ validation_evidence=tuple(
585
+ {"command": redact_diagnostic(item.get("command", ""), limit=240), "result": redact_diagnostic(item.get("result", ""), limit=240)}
586
+ for item in result.validation_evidence
587
+ if isinstance(item, dict) and item.get("command") and item.get("result")
588
+ ),
589
+ quality_evidence=tuple(
590
+ {"activity": str(item["activity"]), "result": redact_diagnostic(str(item["result"]), limit=240)}
591
+ for item in result.quality_evidence
592
+ ),
593
+ )
594
+ if result.diagnostic is not None:
595
+ result = replace(result, diagnostic=redact_diagnostic(result.diagnostic))
596
+ return result
597
+ except (IndexError, json.JSONDecodeError, TypeError) as error:
598
+ interruption = provider_turn_interruption(completed.stdout, completed.stderr)
599
+ raise CodexInvocationError(
600
+ "Provider turn interrupted before returning the required structured terminal result."
601
+ if interruption else "Codex CLI did not return the required structured terminal result.",
602
+ _format_cli_failure(completed.returncode, completed.stderr, completed.stdout, prompt),
603
+ next_action="NONE" if interruption else "inspect_codex_cli",
604
+ terminal_condition="provider_turn_interrupted" if interruption else "codex_invocation_failed",
605
+ interruption_reason=interruption,
606
+ ) from error
607
+
608
+ def _run_invocation(
609
+ self, command: tuple[str, ...], root: Path, environment: Mapping[str, str] | None = None
610
+ ) -> subprocess.CompletedProcess[str]:
611
+ """Run Codex, streaming only the approved activity projection when enabled."""
612
+ self.last_execution_metadata = {
613
+ "modified": 0,
614
+ "created": 0,
615
+ "deleted": 0,
616
+ "codex_commands_executed": 0,
617
+ }
618
+ if (
619
+ self._activity_callback is None
620
+ and self._transient_action_callback is None
621
+ and self._command_callback is None
622
+ and self._runtime_metadata_callback is None
623
+ and self._workspace_progress_callback is None
624
+ and self._handoff_deadline_callback is None
625
+ ):
626
+ return self.provider.invoke(root, command, environment=environment)
627
+ process = self.provider.spawn_invocation(root, command, environment=environment)
628
+ if self._process_callback is not None:
629
+ try:
630
+ self._process_callback({"pid": process.pid, "process_group": os.getpgid(process.pid)})
631
+ except OSError:
632
+ self._process_callback(None)
633
+ lines: list[str] = []
634
+ last_workspace_progress: dict[str, int] | None = None
635
+ observed_command_ids: set[str] = set()
636
+ watchdog_stop = Event()
637
+ handoff_timed_out = Event()
638
+
639
+ def watchdog() -> None:
640
+ while not watchdog_stop.wait(1):
641
+ if self._handoff_deadline_callback is None or not self._handoff_deadline_callback():
642
+ continue
643
+ handoff_timed_out.set()
644
+ # Invocation processes start their own session. Stopping only
645
+ # the CLI parent can leave stdout open in a child and strand
646
+ # the runner in its read loop after the deadline.
647
+ try:
648
+ os.killpg(os.getpgid(process.pid), signal.SIGTERM)
649
+ except (OSError, ProcessLookupError):
650
+ process.terminate()
651
+ return
652
+
653
+ watchdog_thread = (
654
+ Thread(target=watchdog, name="engineering-pr-handoff-watchdog", daemon=True)
655
+ if self._handoff_deadline_callback is not None else None
656
+ )
657
+ if watchdog_thread is not None:
658
+ watchdog_thread.start()
659
+ try:
660
+ assert process.stdout is not None
661
+ for line in process.stdout:
662
+ if handoff_timed_out.is_set():
663
+ raise CodexHandoffTimeout("Agent did not return after the host-owned PR hand-off deadline.")
664
+ lines.append(line)
665
+ if self._workspace_progress_callback is not None:
666
+ progress = workspace_change_summary(root)
667
+ self.last_execution_metadata.update(progress)
668
+ if progress != last_workspace_progress:
669
+ self._workspace_progress_callback(dict(self.last_execution_metadata))
670
+ last_workspace_progress = progress
671
+ observed_metadata = extract_codex_runtime_metadata(line)
672
+ if len(observed_metadata) > 1:
673
+ self.last_runtime_metadata.update(observed_metadata)
674
+ if self._runtime_metadata_callback is not None:
675
+ self._runtime_metadata_callback(dict(self.last_runtime_metadata))
676
+ try:
677
+ event = json.loads(line)
678
+ activity = project_codex_activity(event)
679
+ except json.JSONDecodeError:
680
+ activity = None
681
+ event = None
682
+ if activity is not None and self._activity_callback is not None:
683
+ self._activity_callback(activity)
684
+ transient_action = project_codex_live_action_name(event)
685
+ if transient_action is not None and self._transient_action_callback is not None:
686
+ self._transient_action_callback(transient_action)
687
+ if self._command_callback is not None:
688
+ command_event = project_codex_command_event(event)
689
+ if command_event is not None:
690
+ if command_event[0] == "started" and command_event[1] not in observed_command_ids:
691
+ observed_command_ids.add(command_event[1])
692
+ self.last_execution_metadata["codex_commands_executed"] += 1
693
+ if self._workspace_progress_callback is not None:
694
+ self._workspace_progress_callback(dict(self.last_execution_metadata))
695
+ self._command_callback(*command_event)
696
+ if handoff_timed_out.is_set():
697
+ raise CodexHandoffTimeout("Agent did not return after the host-owned PR hand-off deadline.")
698
+ return subprocess.CompletedProcess(command, process.wait(), "".join(lines), "")
699
+ finally:
700
+ watchdog_stop.set()
701
+ if watchdog_thread is not None:
702
+ watchdog_thread.join(timeout=1)
703
+ if self._process_callback is not None:
704
+ self._process_callback(None)
705
+
706
+
707
+ def workspace_change_summary(root: Path) -> dict[str, int]:
708
+ """Return only aggregate Git worktree changes for the live status surface."""
709
+ try:
710
+ completed = subprocess.run(
711
+ ("git", "status", "--porcelain=v1", "--untracked-files=all"),
712
+ cwd=root,
713
+ capture_output=True,
714
+ check=False,
715
+ text=True,
716
+ )
717
+ except OSError:
718
+ return {"modified": 0, "created": 0, "deleted": 0}
719
+ if completed.returncode:
720
+ return {"modified": 0, "created": 0, "deleted": 0}
721
+ summary = {"modified": 0, "created": 0, "deleted": 0}
722
+ for line in completed.stdout.splitlines():
723
+ status = line[:2]
724
+ if status == "??" or "A" in status:
725
+ summary["created"] += 1
726
+ elif "D" in status:
727
+ summary["deleted"] += 1
728
+ elif status.strip():
729
+ summary["modified"] += 1
730
+ return summary