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.
Potentially problematic release.
This version of engineering-platform might be problematic. Click here for more details.
- engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
- engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
- engineering_platform/__init__.py +1 -0
- engineering_platform/__main__.py +7 -0
- engineering_platform/agent_state.py +530 -0
- engineering_platform/agent_trust.py +174 -0
- engineering_platform/assets/dashboard.css +1317 -0
- engineering_platform/assets/dashboard.js +8534 -0
- engineering_platform/assets/dashboard_locales.mjs +4049 -0
- engineering_platform/assets/dashboard_status_store.mjs +41 -0
- engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
- engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-dark.png +0 -0
- engineering_platform/assets/operations-console/icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-transparent.png +0 -0
- engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
- engineering_platform/capability_preflight.py +285 -0
- engineering_platform/capability_review.py +261 -0
- engineering_platform/central_data_transfer.py +195 -0
- engineering_platform/central_database.py +245 -0
- engineering_platform/central_store_migration.py +1672 -0
- engineering_platform/codex_capacity.py +81 -0
- engineering_platform/codex_chat.py +226 -0
- engineering_platform/codex_observability.py +153 -0
- engineering_platform/component_lock.py +40 -0
- engineering_platform/component_logging.py +420 -0
- engineering_platform/console_presentation.py +14 -0
- engineering_platform/console_route_ownership.py +83 -0
- engineering_platform/contracts/__init__.py +38 -0
- engineering_platform/contracts/ep_consumer.py +391 -0
- engineering_platform/contracts/models.py +105 -0
- engineering_platform/contracts/projection.py +401 -0
- engineering_platform/dashboard_browser_validation.py +206 -0
- engineering_platform/dashboard_state.py +630 -0
- engineering_platform/dashboard_supervisor.swift +105 -0
- engineering_platform/dashboard_translation.py +129 -0
- engineering_platform/dependabot_producer.py +349 -0
- engineering_platform/drift_diagnostics.py +144 -0
- engineering_platform/emergency_recovery.py +268 -0
- engineering_platform/engineering_memory.py +139 -0
- engineering_platform/ep_consumer_credentials.py +473 -0
- engineering_platform/evidence_projection.py +213 -0
- engineering_platform/execution_activity.py +218 -0
- engineering_platform/execution_context.py +132 -0
- engineering_platform/execution_errors.py +42 -0
- engineering_platform/execution_evidence.py +24 -0
- engineering_platform/execution_executor.py +730 -0
- engineering_platform/execution_finalization.py +44 -0
- engineering_platform/execution_host.py +3306 -0
- engineering_platform/execution_lease.py +365 -0
- engineering_platform/execution_lifecycle.py +447 -0
- engineering_platform/execution_models.py +43 -0
- engineering_platform/execution_readiness.py +166 -0
- engineering_platform/execution_reporting.py +1607 -0
- engineering_platform/execution_repository.py +253 -0
- engineering_platform/execution_timeout_policy.py +56 -0
- engineering_platform/execution_timing.py +440 -0
- engineering_platform/execution_transaction.py +28 -0
- engineering_platform/external_producer_binding.py +235 -0
- engineering_platform/file_inbox.py +249 -0
- engineering_platform/forensic_attribution.py +338 -0
- engineering_platform/forensic_attribution_v2.py +134 -0
- engineering_platform/forensic_delta.py +299 -0
- engineering_platform/golden_scenario.py +63 -0
- engineering_platform/historical_dashboard_configuration.py +171 -0
- engineering_platform/host_admin.py +199 -0
- engineering_platform/host_preflight.py +231 -0
- engineering_platform/installation_relocation.py +122 -0
- engineering_platform/investigation_ledger.py +89 -0
- engineering_platform/legacy_inbox_migration.py +79 -0
- engineering_platform/lifecycle_worker.py +223 -0
- engineering_platform/live_status.py +267 -0
- engineering_platform/local_api.py +209 -0
- engineering_platform/local_api_keychain.py +51 -0
- engineering_platform/local_repository_binding.py +138 -0
- engineering_platform/managed_autonomy.py +509 -0
- engineering_platform/managed_codex_runtime.py +105 -0
- engineering_platform/parity_context.py +203 -0
- engineering_platform/parity_lifecycle_dispatcher.py +488 -0
- engineering_platform/platform_admin.py +13 -0
- engineering_platform/platform_api.py +428 -0
- engineering_platform/platform_bootstrap.py +385 -0
- engineering_platform/platform_components.py +65 -0
- engineering_platform/platform_version.py +171 -0
- engineering_platform/pr_check_repair.py +276 -0
- engineering_platform/pr_evidence_backfill.py +278 -0
- engineering_platform/producer.py +209 -0
- engineering_platform/project_agent.py +366 -0
- engineering_platform/project_agent_service.py +244 -0
- engineering_platform/project_topology.py +126 -0
- engineering_platform/prompt_history.py +591 -0
- engineering_platform/provider_context.py +136 -0
- engineering_platform/provider_context_benchmark.py +41 -0
- engineering_platform/provider_context_scope.py +90 -0
- engineering_platform/provider_interruption.py +168 -0
- engineering_platform/provider_process_identity.py +80 -0
- engineering_platform/provider_readiness.py +138 -0
- engineering_platform/provider_recovery.py +647 -0
- engineering_platform/provider_usage.py +497 -0
- engineering_platform/providers.py +471 -0
- engineering_platform/qualification.py +220 -0
- engineering_platform/recommendation_handoff.py +238 -0
- engineering_platform/report_analysis.py +193 -0
- engineering_platform/repository_attachment.py +171 -0
- engineering_platform/repository_handoff.py +95 -0
- engineering_platform/resources.py +38 -0
- engineering_platform/reviewer_evidence.py +70 -0
- engineering_platform/schemas/repository-attachment.schema.json +61 -0
- engineering_platform/server.py +3679 -0
- engineering_platform/server_console_services.py +2024 -0
- engineering_platform/server_relay.py +172 -0
- engineering_platform/server_service.py +122 -0
- engineering_platform/status_model.py +135 -0
- engineering_platform/status_reconciliation.py +34 -0
- engineering_platform/storage.py +2440 -0
- engineering_platform/submission_cli.py +77 -0
- engineering_platform/submission_intake.py +45 -0
- engineering_platform/submission_service.py +317 -0
- engineering_platform/telemetry.py +951 -0
- engineering_platform/templates/workspace-config.json +25 -0
- engineering_platform/validation_identity.py +50 -0
- engineering_platform/validation_profile.py +211 -0
- engineering_platform/workspace_preflight.py +263 -0
- engineering_platform/worktree_provenance.py +147 -0
- engineering_platform/worktree_tooling.py +18 -0
- engineering_platform-2.2.0.dist-info/METADATA +18 -0
- engineering_platform-2.2.0.dist-info/RECORD +130 -0
- engineering_platform-2.2.0.dist-info/WHEEL +5 -0
- engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
- engineering_platform-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2024 @@
|
|
|
1
|
+
"""Private Engineering Status dashboard with distinct queue recovery and execution retry actions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from html import escape
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import plistlib
|
|
14
|
+
import re
|
|
15
|
+
import select
|
|
16
|
+
import shlex
|
|
17
|
+
import shutil
|
|
18
|
+
import sqlite3
|
|
19
|
+
import socket
|
|
20
|
+
import subprocess # noqa: F401 - Compatibility mock target; process execution is provider-owned.
|
|
21
|
+
import sys
|
|
22
|
+
from threading import Lock, Timer
|
|
23
|
+
import time
|
|
24
|
+
import uuid
|
|
25
|
+
from urllib.parse import parse_qs, urlsplit
|
|
26
|
+
from .platform_api import PlatformConfiguration
|
|
27
|
+
from . import central_database
|
|
28
|
+
from .platform_bootstrap import provision_runtime_workspace as provision_workspace
|
|
29
|
+
from .providers import CodexCliProvider, GitHubProvider, GitProvider, LaunchdProvider, LocalProcessProvider, TailscaleProvider, codex_cli_executable, engineering_platform_codex_cli_prefix
|
|
30
|
+
from .provider_readiness import runtime_details as provider_runtime_details, status as provider_readiness_status
|
|
31
|
+
from .component_logging import (
|
|
32
|
+
DEFAULT_LOG_LEVEL,
|
|
33
|
+
LOG_LEVEL_ENVIRONMENT,
|
|
34
|
+
VALID_LEVELS,
|
|
35
|
+
clear_component_log as clear_stored_component_log,
|
|
36
|
+
component_log as stored_component_log,
|
|
37
|
+
component_log_page as stored_component_log_page,
|
|
38
|
+
component_log_version,
|
|
39
|
+
component_lifecycle_context,
|
|
40
|
+
component_logger,
|
|
41
|
+
log_event,
|
|
42
|
+
prune_component_logs,
|
|
43
|
+
shutdown_signal_logging,
|
|
44
|
+
)
|
|
45
|
+
from .component_lock import DuplicateComponentInstanceError, single_instance
|
|
46
|
+
from .agent_state import is_valid_commit_evidence_record, redact_diagnostic
|
|
47
|
+
from .pr_check_repair import PullRequestCheckRepairError, admit as admit_pr_check_repair, attempted as pr_check_repair_attempted, check_summary as pr_check_repair_check_summary, mark_dispatch_failed as mark_pr_check_repair_dispatch_failed, repair_state as pr_check_repair_state
|
|
48
|
+
from .codex_chat import (
|
|
49
|
+
CodexChatError,
|
|
50
|
+
chat_model,
|
|
51
|
+
clear_history as clear_codex_chat_history,
|
|
52
|
+
history as codex_chat_history,
|
|
53
|
+
respond as codex_chat_response,
|
|
54
|
+
)
|
|
55
|
+
from .codex_capacity import read_remaining_percent
|
|
56
|
+
from .telemetry import clear_telemetry, daily_statistics, daily_timing_detail, execution_timing, prune_telemetry
|
|
57
|
+
from .prompt_history import prompt_history, report_for_prompt_history, report_path_for_prompt_history
|
|
58
|
+
from .report_analysis import analyze as analyze_terminal_report
|
|
59
|
+
from .recommendation_handoff import handoff_from_report
|
|
60
|
+
from .storage import (
|
|
61
|
+
EngineeringStorageError,
|
|
62
|
+
load_projection,
|
|
63
|
+
open_storage,
|
|
64
|
+
)
|
|
65
|
+
from .provider_usage import provider_usage_summary
|
|
66
|
+
from .execution_activity import terminal_activity_summary
|
|
67
|
+
from .execution_lifecycle import projection as lifecycle_projection
|
|
68
|
+
from .platform_version import CURRENT_PLATFORM_VERSION, EngineeringPlatformManifest
|
|
69
|
+
from .resources import package_path
|
|
70
|
+
from .platform_components import PLATFORM_COMPONENT_BY_ID, PLATFORM_COMPONENTS
|
|
71
|
+
from . import dashboard_state
|
|
72
|
+
from . import managed_codex_runtime
|
|
73
|
+
from . import server_relay
|
|
74
|
+
from .console_presentation import APP_ICON_DARK, APP_ICON_LIGHT, ASSET_DIRECTORY, WEB_MANIFEST
|
|
75
|
+
|
|
76
|
+
RELAY_LABEL = PLATFORM_COMPONENT_BY_ID["dashboard_relay"].lifecycle_label or ""
|
|
77
|
+
DASHBOARD_VERSION = CURRENT_PLATFORM_VERSION
|
|
78
|
+
DASHBOARD_STARTED_AT = time.monotonic()
|
|
79
|
+
DASHBOARD_SNAPSHOT_SOURCE = str(uuid.uuid4())
|
|
80
|
+
LOOPBACK_ADDRESS = "127.0.0.1"
|
|
81
|
+
CODEX_PROCESS = re.compile(r"(?:^|\s)(?:\S*/)?codex(?:\s|$)")
|
|
82
|
+
RATE_LIMIT_CACHE_SECONDS = 60
|
|
83
|
+
RETRYABLE_REPORT_ANALYSIS_STATUSES = frozenset({
|
|
84
|
+
"provider_failed", "provider_unavailable", "invalid_structured_response",
|
|
85
|
+
})
|
|
86
|
+
_REPORT_ANALYSIS_RETRY_LOCK = Lock()
|
|
87
|
+
_REPORT_ANALYSIS_RETRY_RUNS: set[str] = set()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class CodexCapacityReserveConflict(ValueError):
|
|
91
|
+
"""A new reserve exceeds fresh capacity or cannot be safely verified."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, code: str, *, remaining_percent: float | None = None) -> None:
|
|
94
|
+
super().__init__(code)
|
|
95
|
+
self.code = code
|
|
96
|
+
self.remaining_percent = remaining_percent
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _validate_codex_capacity_reserve_update(root: Path, key: object, value: object) -> None:
|
|
100
|
+
"""Fail closed before a reserve increase can make future admission impossible."""
|
|
101
|
+
if key != "codex_capacity_reserve_percent" or not isinstance(value, int) or isinstance(value, bool):
|
|
102
|
+
return
|
|
103
|
+
previous = int(central_database.capacity_configuration(root)["codex_capacity_reserve_percent"])
|
|
104
|
+
if value <= previous:
|
|
105
|
+
return
|
|
106
|
+
remaining = read_remaining_percent()
|
|
107
|
+
if remaining is None:
|
|
108
|
+
raise CodexCapacityReserveConflict("codex_capacity_reserve_unavailable")
|
|
109
|
+
if value > remaining:
|
|
110
|
+
raise CodexCapacityReserveConflict(
|
|
111
|
+
"codex_capacity_reserve_exceeds_remaining", remaining_percent=remaining,
|
|
112
|
+
)
|
|
113
|
+
_rate_limit_cache_lock = Lock()
|
|
114
|
+
_rate_limit_cache: tuple[float, bytes] | None = None
|
|
115
|
+
CODEX_IDENTITY_CACHE_SECONDS = 300
|
|
116
|
+
CODEX_UPDATE_CACHE_SECONDS = 900
|
|
117
|
+
CODEX_CLI_PACKAGE = "@openai/codex"
|
|
118
|
+
GIT_INDEX_LOCK_STALE_SECONDS = 300
|
|
119
|
+
_codex_identity_cache_lock = Lock()
|
|
120
|
+
_codex_identity_cache: tuple[float, dict[str, str]] | None = None
|
|
121
|
+
_codex_update_cache_lock = Lock()
|
|
122
|
+
_codex_update_cache: tuple[float, dict[str, object]] | None = None
|
|
123
|
+
_codex_update_install_lock = Lock()
|
|
124
|
+
_provider_install_lock = Lock()
|
|
125
|
+
_provider_login_lock = Lock()
|
|
126
|
+
_snapshot_revision_lock = Lock()
|
|
127
|
+
_snapshot_fingerprint: bytes | None = None
|
|
128
|
+
_snapshot_revision = 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _legacy_emergency_recovery() -> object:
|
|
132
|
+
"""Load retired direct-dashboard recovery support only on an old request."""
|
|
133
|
+
from . import emergency_recovery
|
|
134
|
+
|
|
135
|
+
return emergency_recovery
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class EmergencyRecoveryError(RuntimeError):
|
|
139
|
+
"""Compatibility error for the retired direct-dashboard recovery route."""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def execute_emergency_recovery(*args: object, **kwargs: object) -> object:
|
|
143
|
+
return _legacy_emergency_recovery().execute(*args, **kwargs)
|
|
144
|
+
AUDITABLE_USER_ACTIONS = frozenset(
|
|
145
|
+
{
|
|
146
|
+
"chat_downloaded",
|
|
147
|
+
"component_log_downloaded",
|
|
148
|
+
"telemetry_cleared",
|
|
149
|
+
"telemetry_copied",
|
|
150
|
+
"telemetry_downloaded",
|
|
151
|
+
"telemetry_detail_json_downloaded",
|
|
152
|
+
"telemetry_detail_markdown_downloaded",
|
|
153
|
+
"prompt_history_report_copied",
|
|
154
|
+
"prompt_history_report_downloaded",
|
|
155
|
+
"prompt_history_analysis_copied",
|
|
156
|
+
"prompt_history_analysis_downloaded",
|
|
157
|
+
"prompt_history_details_json_downloaded",
|
|
158
|
+
"prompt_history_details_markdown_downloaded",
|
|
159
|
+
"report_copied",
|
|
160
|
+
"report_analysis_copied",
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _unavailable_status() -> bytes:
|
|
166
|
+
"""Compatibility façade for the dashboard state module."""
|
|
167
|
+
return dashboard_state.unavailable_status()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _canonical_checkpoint(root: Path, run_id: str | None) -> dict[str, object]:
|
|
171
|
+
"""Read one lifecycle checkpoint from SQLite, never from its JSON copy."""
|
|
172
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id):
|
|
173
|
+
return {}
|
|
174
|
+
try:
|
|
175
|
+
connection = open_storage(root)
|
|
176
|
+
try:
|
|
177
|
+
row = connection.execute(
|
|
178
|
+
"SELECT payload FROM engineering_transactions WHERE run_id=?", (run_id,)
|
|
179
|
+
).fetchone()
|
|
180
|
+
finally:
|
|
181
|
+
connection.close()
|
|
182
|
+
payload = json.loads(row[0]) if row else {}
|
|
183
|
+
except (EngineeringStorageError, TypeError, json.JSONDecodeError):
|
|
184
|
+
return {}
|
|
185
|
+
if isinstance(payload, dict) and payload:
|
|
186
|
+
return payload
|
|
187
|
+
# Compatibility-only reader for an already-terminal legacy checkpoint.
|
|
188
|
+
try:
|
|
189
|
+
legacy = json.loads(
|
|
190
|
+
(root / ".engineering" / "engineering-runs" / f"{run_id}.json").read_text(encoding="utf-8")
|
|
191
|
+
)
|
|
192
|
+
except (OSError, json.JSONDecodeError):
|
|
193
|
+
return {}
|
|
194
|
+
return legacy if isinstance(legacy, dict) else {}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _status(root: Path) -> bytes:
|
|
198
|
+
"""Compatibility façade for the stable status projection."""
|
|
199
|
+
return dashboard_state.status(root)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _sse_status(root: Path) -> bytes:
|
|
203
|
+
"""Encode the status as a single SSE data line."""
|
|
204
|
+
return dashboard_state.sse_status(root)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _sse_snapshot(root: Path) -> bytes:
|
|
208
|
+
"""Return the complete read-only dashboard projection for one SSE update.
|
|
209
|
+
|
|
210
|
+
The browser receives this snapshot when it connects and only when one of
|
|
211
|
+
its observable values changes. This keeps the dashboard event-driven
|
|
212
|
+
without giving the dashboard any transaction authority.
|
|
213
|
+
"""
|
|
214
|
+
snapshot = dashboard_state.snapshot(
|
|
215
|
+
root,
|
|
216
|
+
status_reader=_sse_status,
|
|
217
|
+
unavailable_reader=_unavailable_status,
|
|
218
|
+
prompt_started_reader=_prompt_started,
|
|
219
|
+
usage_reader=_codex_usage,
|
|
220
|
+
rate_limits_reader=_codex_rate_limits,
|
|
221
|
+
usage_for_run_reader=_codex_usage_for_run,
|
|
222
|
+
completion_commits_reader=_completion_commits,
|
|
223
|
+
last_executed_commits_reader=_last_executed_commits,
|
|
224
|
+
reviewer_agents_reader=_reviewer_agents_for_run,
|
|
225
|
+
execution_reader=_last_executed_agent_execution,
|
|
226
|
+
runtime_metadata_reader=_last_executed_runtime_metadata,
|
|
227
|
+
report_analysis_available_reader=_report_analysis_available_for_run,
|
|
228
|
+
telemetry_reader=lambda workspace: daily_statistics(
|
|
229
|
+
workspace,
|
|
230
|
+
days=int(central_database.console_interval_configuration(workspace)["telemetry_retention_days"]),
|
|
231
|
+
),
|
|
232
|
+
process_metrics_reader=_codex_process_metrics,
|
|
233
|
+
build_commit_reader=_build_commit,
|
|
234
|
+
component_log_versions_reader=_component_log_versions,
|
|
235
|
+
dashboard_version=DASHBOARD_VERSION,
|
|
236
|
+
worker_version="retired-direct-dashboard",
|
|
237
|
+
)
|
|
238
|
+
try:
|
|
239
|
+
payload = json.loads(snapshot)
|
|
240
|
+
except json.JSONDecodeError:
|
|
241
|
+
return snapshot
|
|
242
|
+
if not isinstance(payload, dict):
|
|
243
|
+
return snapshot
|
|
244
|
+
payload["workspace_git_lock"] = _workspace_git_lock(root)
|
|
245
|
+
status = payload.get("status")
|
|
246
|
+
payload["emergency_recovery"] = _legacy_emergency_recovery().preview(
|
|
247
|
+
root, status.get("run_id") if isinstance(status, dict) else None
|
|
248
|
+
)
|
|
249
|
+
# Git state is deliberately projected with the SSE payload instead of
|
|
250
|
+
# being fixed in the initial HTML response. A managed execution can switch
|
|
251
|
+
# to its transaction branch after the operator opens the dashboard.
|
|
252
|
+
payload["workspace_git"] = _workspace_git_projection(root)
|
|
253
|
+
payload["workspace_worktrees"] = _workspace_worktrees(root)
|
|
254
|
+
# Provider capacity belongs to the account and EP installation, never to
|
|
255
|
+
# this repository root. The standalone Server overlays CENTRAL capacity
|
|
256
|
+
# history onto this otherwise historical projection.
|
|
257
|
+
fingerprint = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode()
|
|
258
|
+
# HTTP refreshes and SSE delivery can complete out of order in a browser.
|
|
259
|
+
# Attach one process-scoped monotone revision to every changed projection,
|
|
260
|
+
# so the client can retain the newest coherent lifecycle snapshot.
|
|
261
|
+
global _snapshot_fingerprint, _snapshot_revision
|
|
262
|
+
with _snapshot_revision_lock:
|
|
263
|
+
if fingerprint != _snapshot_fingerprint:
|
|
264
|
+
_snapshot_fingerprint = fingerprint
|
|
265
|
+
_snapshot_revision += 1
|
|
266
|
+
payload["snapshot_source"] = DASHBOARD_SNAPSHOT_SOURCE
|
|
267
|
+
payload["snapshot_revision"] = _snapshot_revision
|
|
268
|
+
return json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _prompt_history(root: Path) -> bytes:
|
|
272
|
+
"""Return historical terminal evidence without a watcher-local queue."""
|
|
273
|
+
try:
|
|
274
|
+
runs = prompt_history(root, queued_retry_children=[])
|
|
275
|
+
for run in runs:
|
|
276
|
+
run["analysis_available"] = _report_analysis_available_for_run(
|
|
277
|
+
root, run.get("run_id")
|
|
278
|
+
)
|
|
279
|
+
payload = {"runs": runs}
|
|
280
|
+
except Exception:
|
|
281
|
+
payload = {"runs": []}
|
|
282
|
+
return json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _project_history_evidence(history: dict[str, object], report: str) -> list[str]:
|
|
286
|
+
"""Project bounded report evidence without mixing it into storage access."""
|
|
287
|
+
target_repository = re.search(
|
|
288
|
+
r"^- Target Repository: `([^`\n]+)`$", report, re.MULTILINE
|
|
289
|
+
)
|
|
290
|
+
if target_repository:
|
|
291
|
+
history["target_repository"] = target_repository.group(1)
|
|
292
|
+
evidence: list[str] = []
|
|
293
|
+
for report_label, display_label in (
|
|
294
|
+
("Execution Host", "Execution Host"),
|
|
295
|
+
("Target Repository", "Target repository"),
|
|
296
|
+
("Target Commit", "Target commit"),
|
|
297
|
+
("Producer ID", "Producer"),
|
|
298
|
+
("Producer Type", "Producer type"),
|
|
299
|
+
("Mission ID", "Mission"),
|
|
300
|
+
("Engineering Action ID", "Engineering action"),
|
|
301
|
+
("Correlation ID", "Correlation"),
|
|
302
|
+
):
|
|
303
|
+
match = re.search(
|
|
304
|
+
rf"^- {re.escape(report_label)}: `([^`\n]+)`$", report, re.MULTILINE
|
|
305
|
+
)
|
|
306
|
+
if match:
|
|
307
|
+
evidence.append(f"{display_label}: {match.group(1)}")
|
|
308
|
+
changed = len(re.findall(r"^- Changed file: `", report, re.MULTILINE))
|
|
309
|
+
if changed:
|
|
310
|
+
evidence.append(f"Evidence Bundle: {changed} gewijzigde bestanden")
|
|
311
|
+
return evidence
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _project_prompt_history_detail(
|
|
315
|
+
entry: dict[str, object],
|
|
316
|
+
*,
|
|
317
|
+
execution: object,
|
|
318
|
+
runtime: object,
|
|
319
|
+
reviewers: object,
|
|
320
|
+
commits: object,
|
|
321
|
+
usage: dict[str, object],
|
|
322
|
+
report: str | None,
|
|
323
|
+
lifecycle: dict[str, object] | None = None,
|
|
324
|
+
pull_requests: object = (),
|
|
325
|
+
commit_timeline: object = (),
|
|
326
|
+
) -> bytes:
|
|
327
|
+
"""Project one immutable history row into dashboard detail JSON.
|
|
328
|
+
|
|
329
|
+
Storage retrieval belongs to the route projection; this function owns the
|
|
330
|
+
presentation shape and the bounded evidence derived from its report.
|
|
331
|
+
"""
|
|
332
|
+
history = dict(entry)
|
|
333
|
+
evidence = _project_history_evidence(history, report) if report is not None else []
|
|
334
|
+
handoff = handoff_from_report(report) if report is not None else None
|
|
335
|
+
return json.dumps(
|
|
336
|
+
{
|
|
337
|
+
"history": history,
|
|
338
|
+
"execution": execution,
|
|
339
|
+
"runtime": runtime,
|
|
340
|
+
"reviewers": reviewers,
|
|
341
|
+
"commits": commits,
|
|
342
|
+
"commit_timeline": commit_timeline,
|
|
343
|
+
"pull_requests": pull_requests,
|
|
344
|
+
"usage": usage,
|
|
345
|
+
"evidence": evidence,
|
|
346
|
+
"recommendation_handoff": handoff,
|
|
347
|
+
"lifecycle": lifecycle or {},
|
|
348
|
+
},
|
|
349
|
+
ensure_ascii=False,
|
|
350
|
+
separators=(",", ":"),
|
|
351
|
+
).encode()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _pull_requests_for_run(root: Path, run_id: str | None) -> list[dict[str, object]]:
|
|
355
|
+
"""Project only checkpoint-owned Managed pull-request evidence as links."""
|
|
356
|
+
checkpoint = _canonical_checkpoint(root, run_id)
|
|
357
|
+
repository = checkpoint.get("repository")
|
|
358
|
+
if (
|
|
359
|
+
checkpoint.get("execution_mode") != "MANAGED"
|
|
360
|
+
or not isinstance(repository, str)
|
|
361
|
+
or not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repository)
|
|
362
|
+
):
|
|
363
|
+
return []
|
|
364
|
+
links: list[dict[str, object]] = []
|
|
365
|
+
for role, field in (
|
|
366
|
+
("implementation", "implementation_pull_request"),
|
|
367
|
+
("finalization", "finalization_pull_request"),
|
|
368
|
+
):
|
|
369
|
+
number = checkpoint.get(field)
|
|
370
|
+
if isinstance(number, int) and not isinstance(number, bool) and number > 0:
|
|
371
|
+
link: dict[str, object] = {
|
|
372
|
+
"role": role,
|
|
373
|
+
"number": number,
|
|
374
|
+
"url": f"https://github.com/{repository}/pull/{number}",
|
|
375
|
+
}
|
|
376
|
+
link.update(_pull_request_github_metrics(root, repository, number))
|
|
377
|
+
links.append(link)
|
|
378
|
+
return links
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _pull_request_github_metrics(root: Path, repository: str, number: int) -> dict[str, int]:
|
|
382
|
+
"""Read bounded, display-only GitHub counts for already-linked PR evidence.
|
|
383
|
+
|
|
384
|
+
The checkpoint remains the authority for the PR link. These metrics enrich
|
|
385
|
+
its detail view only when the local checkout still proves that it belongs
|
|
386
|
+
to the same GitHub repository; unavailable evidence is omitted rather than
|
|
387
|
+
represented as a misleading zero.
|
|
388
|
+
"""
|
|
389
|
+
try:
|
|
390
|
+
remote = GitProvider().execute(root, "git", "remote", "get-url", "origin")
|
|
391
|
+
match = re.search(r"github\.com[:/]([^/\s]+)/([^/\s]+?)(?:\.git)?$", remote.stdout.strip()) if remote.returncode == 0 else None
|
|
392
|
+
if not match or f"{match.group(1)}/{match.group(2)}" != repository:
|
|
393
|
+
return {}
|
|
394
|
+
payload = GitHubProvider().github(
|
|
395
|
+
"pr", "view", str(number), "--repo", repository,
|
|
396
|
+
"--json", "number,commits,changedFiles,statusCheckRollup",
|
|
397
|
+
)
|
|
398
|
+
pull_request = json.loads(payload)
|
|
399
|
+
except (OSError, RuntimeError, json.JSONDecodeError):
|
|
400
|
+
return {}
|
|
401
|
+
if not isinstance(pull_request, dict) or pull_request.get("number") != number:
|
|
402
|
+
return {}
|
|
403
|
+
metrics: dict[str, int] = {}
|
|
404
|
+
commits = pull_request.get("commits")
|
|
405
|
+
if isinstance(commits, list):
|
|
406
|
+
metrics["commit_count"] = len(commits)
|
|
407
|
+
changed_files = pull_request.get("changedFiles")
|
|
408
|
+
if isinstance(changed_files, int) and not isinstance(changed_files, bool) and changed_files >= 0:
|
|
409
|
+
metrics["changed_file_count"] = changed_files
|
|
410
|
+
checks = pull_request.get("statusCheckRollup")
|
|
411
|
+
if isinstance(checks, list):
|
|
412
|
+
# GitHub's Checks tab counts check runs. `statusCheckRollup` also
|
|
413
|
+
# contains legacy status contexts such as Owner Authorization; those
|
|
414
|
+
# belong to commit status, not the visible Checks count.
|
|
415
|
+
metrics["check_count"] = sum(
|
|
416
|
+
1 for check in checks
|
|
417
|
+
if isinstance(check, dict)
|
|
418
|
+
and check.get("__typename") == "CheckRun"
|
|
419
|
+
and str(check.get("name") or "").strip()
|
|
420
|
+
)
|
|
421
|
+
return metrics
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _terminal_run_diagnostic(root: Path, run_id: str | None) -> str | None:
|
|
425
|
+
"""Return the checkpoint-owned block reason before consulting legacy logs."""
|
|
426
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id):
|
|
427
|
+
return None
|
|
428
|
+
checkpoint = _canonical_checkpoint(root, run_id)
|
|
429
|
+
if (
|
|
430
|
+
checkpoint.get("phase") in {"BLOCKED", "FAILED"}
|
|
431
|
+
and checkpoint.get("terminal") is True
|
|
432
|
+
and isinstance(checkpoint.get("diagnostic"), str)
|
|
433
|
+
and checkpoint["diagnostic"].strip()
|
|
434
|
+
):
|
|
435
|
+
return redact_diagnostic(checkpoint["diagnostic"], limit=500)
|
|
436
|
+
try:
|
|
437
|
+
connection = open_storage(root)
|
|
438
|
+
try:
|
|
439
|
+
rows = connection.execute(
|
|
440
|
+
"SELECT payload FROM engineering_component_logs "
|
|
441
|
+
"WHERE component='inbox' ORDER BY id DESC LIMIT 200"
|
|
442
|
+
).fetchall()
|
|
443
|
+
finally:
|
|
444
|
+
connection.close()
|
|
445
|
+
except (EngineeringStorageError, OSError, sqlite3.DatabaseError):
|
|
446
|
+
return None
|
|
447
|
+
for row in rows:
|
|
448
|
+
try:
|
|
449
|
+
event = json.loads(row[0])
|
|
450
|
+
except (TypeError, json.JSONDecodeError):
|
|
451
|
+
continue
|
|
452
|
+
if not isinstance(event, dict) or event.get("run_id") != run_id or event.get("event") != "job_failed":
|
|
453
|
+
continue
|
|
454
|
+
diagnostic = event.get("diagnostic")
|
|
455
|
+
if isinstance(diagnostic, str) and diagnostic.strip():
|
|
456
|
+
return redact_diagnostic(diagnostic, limit=500)
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _prompt_history_detail(root: Path, run_id: str | None) -> bytes:
|
|
461
|
+
"""Return private, immutable operational evidence for one history row."""
|
|
462
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id):
|
|
463
|
+
return b""
|
|
464
|
+
entry = next((item for item in prompt_history(root) if item.get("run_id") == run_id), None)
|
|
465
|
+
if entry is None:
|
|
466
|
+
return b""
|
|
467
|
+
execution = json.loads(_last_executed_agent_execution(root, run_id))
|
|
468
|
+
runtime = json.loads(_last_executed_runtime_metadata(root, run_id))
|
|
469
|
+
reviewers = json.loads(_reviewer_agents_for_run(root, run_id))
|
|
470
|
+
commits = _commits_for_run(root, run_id)
|
|
471
|
+
pull_requests = _pull_requests_for_run(root, run_id)
|
|
472
|
+
commit_timeline = _commit_timeline_for_run(root, run_id)
|
|
473
|
+
usage: dict[str, object] = {}
|
|
474
|
+
try:
|
|
475
|
+
provider_summary = provider_usage_summary(root, run_id)
|
|
476
|
+
# Legacy runs have no invocation rows. Retain the historic aggregate
|
|
477
|
+
# projection without fabricating invocation detail.
|
|
478
|
+
if provider_summary.get("invocation_detail") == "UNAVAILABLE":
|
|
479
|
+
usage = provider_summary
|
|
480
|
+
connection = open_storage(root)
|
|
481
|
+
row = connection.execute(
|
|
482
|
+
"SELECT input_tokens, output_tokens, total_tokens FROM execution_runs WHERE run_id = ?", (run_id,)
|
|
483
|
+
).fetchone()
|
|
484
|
+
connection.close()
|
|
485
|
+
if row:
|
|
486
|
+
usage.update(
|
|
487
|
+
{
|
|
488
|
+
label: value
|
|
489
|
+
for label, value in zip(("input_tokens", "output_tokens", "total_tokens"), row)
|
|
490
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
491
|
+
}
|
|
492
|
+
)
|
|
493
|
+
else:
|
|
494
|
+
usage = provider_summary
|
|
495
|
+
except Exception:
|
|
496
|
+
usage = {}
|
|
497
|
+
report: str | None = None
|
|
498
|
+
try:
|
|
499
|
+
report = _report_for_run(root, run_id).decode("utf-8")
|
|
500
|
+
except UnicodeDecodeError:
|
|
501
|
+
pass
|
|
502
|
+
if diagnostic := _terminal_run_diagnostic(root, run_id):
|
|
503
|
+
entry["execution_diagnostic"] = diagnostic
|
|
504
|
+
if str(entry.get("status") or "").upper() in {"BLOCKED", "FAILED"}:
|
|
505
|
+
entry["blocking_reason"] = diagnostic
|
|
506
|
+
entry["execution_activity_summary"] = terminal_activity_summary(root, run_id)
|
|
507
|
+
return _project_prompt_history_detail(
|
|
508
|
+
entry,
|
|
509
|
+
execution=execution,
|
|
510
|
+
runtime=runtime,
|
|
511
|
+
reviewers=reviewers,
|
|
512
|
+
commits=commits,
|
|
513
|
+
pull_requests=pull_requests,
|
|
514
|
+
commit_timeline=commit_timeline,
|
|
515
|
+
usage=usage,
|
|
516
|
+
report=report,
|
|
517
|
+
lifecycle=lifecycle_projection(root, run_id),
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _rate_limit_window_label(duration_minutes: int) -> str:
|
|
522
|
+
"""Use a neutral label that reflects the window actually reported by Codex."""
|
|
523
|
+
if duration_minutes == 300:
|
|
524
|
+
return "5-uursvenster"
|
|
525
|
+
if duration_minutes == 10_080:
|
|
526
|
+
return "Weekvenster"
|
|
527
|
+
if duration_minutes % 1_440 == 0:
|
|
528
|
+
return f"{duration_minutes // 1_440}-daags venster"
|
|
529
|
+
if duration_minutes % 60 == 0:
|
|
530
|
+
return f"{duration_minutes // 60}-uursvenster"
|
|
531
|
+
return f"{duration_minutes}-minutenvenster"
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _normalize_rate_limits(payload: object) -> dict[str, object]:
|
|
535
|
+
"""Keep only safe, displayable quota values from Codex's read-only response."""
|
|
536
|
+
if not isinstance(payload, dict):
|
|
537
|
+
return {}
|
|
538
|
+
limits = payload.get("rateLimits")
|
|
539
|
+
if not isinstance(limits, dict):
|
|
540
|
+
return {}
|
|
541
|
+
windows: list[dict[str, int | str]] = []
|
|
542
|
+
for key in ("primary", "secondary"):
|
|
543
|
+
item = limits.get(key)
|
|
544
|
+
if not isinstance(item, dict):
|
|
545
|
+
continue
|
|
546
|
+
used = item.get("usedPercent")
|
|
547
|
+
duration = item.get("windowDurationMins")
|
|
548
|
+
resets_at = item.get("resetsAt")
|
|
549
|
+
if (
|
|
550
|
+
not isinstance(used, (int, float))
|
|
551
|
+
or isinstance(used, bool)
|
|
552
|
+
or not isinstance(duration, int)
|
|
553
|
+
or isinstance(duration, bool)
|
|
554
|
+
or not isinstance(resets_at, int)
|
|
555
|
+
or isinstance(resets_at, bool)
|
|
556
|
+
):
|
|
557
|
+
continue
|
|
558
|
+
windows.append(
|
|
559
|
+
{
|
|
560
|
+
"label": _rate_limit_window_label(duration),
|
|
561
|
+
"used_percent": max(0, min(100, round(used))),
|
|
562
|
+
"window_minutes": duration,
|
|
563
|
+
"resets_at": resets_at,
|
|
564
|
+
}
|
|
565
|
+
)
|
|
566
|
+
credits = payload.get("rateLimitResetCredits")
|
|
567
|
+
available = credits.get("availableCount") if isinstance(credits, dict) else None
|
|
568
|
+
normalized: dict[str, object] = {"windows": windows}
|
|
569
|
+
if isinstance(available, int) and not isinstance(available, bool) and available >= 0:
|
|
570
|
+
normalized["reset_credits"] = available
|
|
571
|
+
return normalized if windows or "reset_credits" in normalized else {}
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _remaining_rate_limit_capacity(rate_limits: dict[str, object]) -> float | None:
|
|
575
|
+
"""Return the most restrictive safe remaining quota percentage."""
|
|
576
|
+
windows = rate_limits.get("windows")
|
|
577
|
+
if not isinstance(windows, list):
|
|
578
|
+
return None
|
|
579
|
+
remaining_values: list[float] = []
|
|
580
|
+
for window in windows:
|
|
581
|
+
if not isinstance(window, dict):
|
|
582
|
+
continue
|
|
583
|
+
used = window.get("used_percent")
|
|
584
|
+
if not isinstance(used, (int, float)) or isinstance(used, bool):
|
|
585
|
+
continue
|
|
586
|
+
remaining_values.append(max(0.0, min(100.0, 100.0 - float(used))))
|
|
587
|
+
return min(remaining_values) if remaining_values else None
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _codex_cli_installation_path(executable: str | None) -> str | None:
|
|
591
|
+
"""Return EP's managed CLI prefix, never a PATH-resolved alternative."""
|
|
592
|
+
if not executable:
|
|
593
|
+
return None
|
|
594
|
+
managed_prefix = engineering_platform_codex_cli_prefix()
|
|
595
|
+
if Path(executable).expanduser() == managed_prefix / "bin" / "codex":
|
|
596
|
+
return str(managed_prefix)
|
|
597
|
+
return None
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _codex_provider_identity(*, refresh: bool = False) -> dict[str, str]:
|
|
601
|
+
"""Return the active provider identity and its locally resolved executable path."""
|
|
602
|
+
global _codex_identity_cache
|
|
603
|
+
now = time.monotonic()
|
|
604
|
+
with _codex_identity_cache_lock:
|
|
605
|
+
if not refresh and _codex_identity_cache and now - _codex_identity_cache[0] < CODEX_IDENTITY_CACHE_SECONDS:
|
|
606
|
+
return dict(_codex_identity_cache[1])
|
|
607
|
+
|
|
608
|
+
identity = {"provider": "Codex CLI", "provider_version": "versie niet beschikbaar"}
|
|
609
|
+
executable = codex_cli_executable()
|
|
610
|
+
if executable:
|
|
611
|
+
if installation_path := _codex_cli_installation_path(executable):
|
|
612
|
+
identity["provider_path"] = installation_path
|
|
613
|
+
try:
|
|
614
|
+
completed = LocalProcessProvider().execute(Path.cwd(), (executable, "--version"))
|
|
615
|
+
except OSError:
|
|
616
|
+
completed = None
|
|
617
|
+
if completed and completed.returncode == 0:
|
|
618
|
+
match = re.search(
|
|
619
|
+
r"(?<!\d)(\d+\.\d+\.\d+)(?!\d)",
|
|
620
|
+
(completed.stdout or completed.stderr).strip(),
|
|
621
|
+
)
|
|
622
|
+
if match:
|
|
623
|
+
identity["provider_version"] = match.group(1)
|
|
624
|
+
with _codex_identity_cache_lock:
|
|
625
|
+
_codex_identity_cache = (now, identity)
|
|
626
|
+
return dict(identity)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _codex_rate_limits() -> bytes:
|
|
630
|
+
"""Read current Codex quota windows without persisting account or credit data."""
|
|
631
|
+
global _rate_limit_cache
|
|
632
|
+
now = time.monotonic()
|
|
633
|
+
with _rate_limit_cache_lock:
|
|
634
|
+
if _rate_limit_cache and now - _rate_limit_cache[0] < RATE_LIMIT_CACHE_SECONDS:
|
|
635
|
+
return _rate_limit_cache[1]
|
|
636
|
+
identity = _codex_provider_identity()
|
|
637
|
+
provider = CodexCliProvider()
|
|
638
|
+
process = None
|
|
639
|
+
try:
|
|
640
|
+
process = provider.app_server()
|
|
641
|
+
if process.stdin is None or process.stdout is None:
|
|
642
|
+
return json.dumps(identity, separators=(",", ":")).encode()
|
|
643
|
+
process.stdin.write(
|
|
644
|
+
json.dumps(
|
|
645
|
+
{
|
|
646
|
+
"method": "initialize",
|
|
647
|
+
"id": 1,
|
|
648
|
+
"params": {
|
|
649
|
+
"clientInfo": {
|
|
650
|
+
"name": "engineering_platform_dashboard",
|
|
651
|
+
"title": "EP Operations",
|
|
652
|
+
"version": DASHBOARD_VERSION,
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
}
|
|
656
|
+
)
|
|
657
|
+
+ "\n"
|
|
658
|
+
)
|
|
659
|
+
process.stdin.flush()
|
|
660
|
+
deadline = time.monotonic() + 5
|
|
661
|
+
requested = False
|
|
662
|
+
while time.monotonic() < deadline:
|
|
663
|
+
ready, _, _ = select.select((process.stdout,), (), (), max(0, deadline - time.monotonic()))
|
|
664
|
+
if not ready:
|
|
665
|
+
break
|
|
666
|
+
line = process.stdout.readline()
|
|
667
|
+
if not line:
|
|
668
|
+
break
|
|
669
|
+
response = json.loads(line)
|
|
670
|
+
if response.get("id") == 1 and not requested:
|
|
671
|
+
process.stdin.write(json.dumps({"method": "initialized", "params": {}}) + "\n")
|
|
672
|
+
process.stdin.write(
|
|
673
|
+
json.dumps({"method": "account/rateLimits/read", "id": 2, "params": {}})
|
|
674
|
+
+ "\n"
|
|
675
|
+
)
|
|
676
|
+
process.stdin.flush()
|
|
677
|
+
requested = True
|
|
678
|
+
elif response.get("id") == 2:
|
|
679
|
+
result = {**identity, **_normalize_rate_limits(response.get("result"))}
|
|
680
|
+
encoded = json.dumps(result, separators=(",", ":")).encode()
|
|
681
|
+
with _rate_limit_cache_lock:
|
|
682
|
+
_rate_limit_cache = (time.monotonic(), encoded)
|
|
683
|
+
return encoded
|
|
684
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
685
|
+
return json.dumps(identity, separators=(",", ":")).encode()
|
|
686
|
+
finally:
|
|
687
|
+
if process is not None:
|
|
688
|
+
provider.close_app_server(process)
|
|
689
|
+
return json.dumps(identity, separators=(",", ":")).encode()
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _github_rate_limit_status() -> dict[str, object]:
|
|
693
|
+
"""Read GitHub quota state without changing GitHub or repository state."""
|
|
694
|
+
try:
|
|
695
|
+
payload = json.loads(GitHubProvider().github("api", "rate_limit"))
|
|
696
|
+
except (OSError, RuntimeError, json.JSONDecodeError) as error:
|
|
697
|
+
# A rate-limit response can itself be a 403/429. Do not show a red
|
|
698
|
+
# operator banner for unrelated authentication or network failures.
|
|
699
|
+
return {"limited": "rate limit" in str(error).lower()}
|
|
700
|
+
resources = payload.get("resources") if isinstance(payload, dict) else None
|
|
701
|
+
if not isinstance(resources, dict):
|
|
702
|
+
return {"limited": False}
|
|
703
|
+
exhausted: list[tuple[str, int]] = []
|
|
704
|
+
for name in ("core", "graphql", "search"):
|
|
705
|
+
resource = resources.get(name)
|
|
706
|
+
if not isinstance(resource, dict):
|
|
707
|
+
continue
|
|
708
|
+
remaining, reset = resource.get("remaining"), resource.get("reset")
|
|
709
|
+
if isinstance(remaining, int) and remaining <= 0:
|
|
710
|
+
exhausted.append((name, reset if isinstance(reset, int) else 0))
|
|
711
|
+
if not exhausted:
|
|
712
|
+
return {"limited": False}
|
|
713
|
+
reset_at = min((reset for _, reset in exhausted if reset > 0), default=None)
|
|
714
|
+
return {"limited": True, "reset_at": reset_at}
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
class RateLimitResetError(RuntimeError):
|
|
718
|
+
"""Raised when Codex cannot safely consume a reset credit."""
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class CodexCliUpdateError(RuntimeError):
|
|
722
|
+
"""Raised when the local Codex CLI cannot be updated and verified safely."""
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
_CODEX_CLI_VERSION = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$")
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _codex_cli_version(value: object) -> str | None:
|
|
729
|
+
candidate = str(value or "").strip().removeprefix("v")
|
|
730
|
+
return candidate if _CODEX_CLI_VERSION.fullmatch(candidate) else None
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _codex_cli_version_key(value: str) -> tuple[int, int, int, int, str]:
|
|
734
|
+
"""Compare normal releases above prereleases without accepting arbitrary text."""
|
|
735
|
+
base, separator, prerelease = value.partition("-")
|
|
736
|
+
major, minor, patch = (int(part) for part in base.split("."))
|
|
737
|
+
return major, minor, patch, 1 if not separator else 0, prerelease
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _npm_executable() -> str | None:
|
|
741
|
+
"""Resolve npm for managed-CLI installation without selecting another CLI."""
|
|
742
|
+
return shutil.which("npm")
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _execution_active(root: Path) -> bool:
|
|
746
|
+
"""Return whether an Execution Host lifecycle is actively using this installation."""
|
|
747
|
+
try:
|
|
748
|
+
status = json.loads(_status(root))
|
|
749
|
+
except (OSError, TypeError, ValueError, json.JSONDecodeError):
|
|
750
|
+
return False
|
|
751
|
+
return (
|
|
752
|
+
isinstance(status, dict)
|
|
753
|
+
and status.get("watcher_state") in {"ENGINEERING_RUN_ACTIVE", "WAITING_FOR_OPERATOR_MERGE"}
|
|
754
|
+
and isinstance(status.get("run_id"), str)
|
|
755
|
+
and bool(status["run_id"])
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _codex_cli_update_status(root: Path, *, refresh: bool = False) -> dict[str, object]:
|
|
760
|
+
"""Read the published Codex CLI version without exposing account or npm output."""
|
|
761
|
+
global _codex_update_cache
|
|
762
|
+
now = time.monotonic()
|
|
763
|
+
with _codex_update_cache_lock:
|
|
764
|
+
if not refresh and _codex_update_cache and now - _codex_update_cache[0] < CODEX_UPDATE_CACHE_SECONDS:
|
|
765
|
+
return dict(_codex_update_cache[1])
|
|
766
|
+
|
|
767
|
+
current = _codex_cli_version(_codex_provider_identity(refresh=refresh).get("provider_version"))
|
|
768
|
+
npm = _npm_executable()
|
|
769
|
+
if current is None or npm is None:
|
|
770
|
+
status: dict[str, object] = {"state": "unavailable", "update_available": False}
|
|
771
|
+
else:
|
|
772
|
+
try:
|
|
773
|
+
completed = LocalProcessProvider().execute(root, (npm, "view", CODEX_CLI_PACKAGE, "version", "--json"))
|
|
774
|
+
latest_raw = json.loads(completed.stdout) if completed.returncode == 0 else None
|
|
775
|
+
candidates = latest_raw if isinstance(latest_raw, list) else [latest_raw]
|
|
776
|
+
versions = [version for value in candidates if (version := _codex_cli_version(value)) is not None]
|
|
777
|
+
latest = max(versions, key=_codex_cli_version_key, default=None)
|
|
778
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
779
|
+
latest = None
|
|
780
|
+
if latest is None:
|
|
781
|
+
status = {"state": "unavailable", "update_available": False, "current_version": current}
|
|
782
|
+
else:
|
|
783
|
+
update_available = _codex_cli_version_key(latest) > _codex_cli_version_key(current)
|
|
784
|
+
status = {
|
|
785
|
+
"state": "update_available" if update_available else "current",
|
|
786
|
+
"update_available": update_available,
|
|
787
|
+
"current_version": current,
|
|
788
|
+
"latest_version": latest,
|
|
789
|
+
}
|
|
790
|
+
with _codex_update_cache_lock:
|
|
791
|
+
_codex_update_cache = (now, status)
|
|
792
|
+
return dict(status)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _install_codex_cli_update(root: Path) -> dict[str, object]:
|
|
796
|
+
"""Provision the EP-owned runtime through the canonical lifecycle."""
|
|
797
|
+
with _codex_update_install_lock:
|
|
798
|
+
if _execution_active(root):
|
|
799
|
+
raise CodexCliUpdateError("codex_cli_update_execution_active")
|
|
800
|
+
try:
|
|
801
|
+
return managed_codex_runtime.provision(root)
|
|
802
|
+
except managed_codex_runtime.ManagedCodexRuntimeError as error:
|
|
803
|
+
raise CodexCliUpdateError(str(error)) from error
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _consume_codex_rate_limit_reset_credit() -> str:
|
|
807
|
+
"""Consume exactly one available Codex reset credit through its app-server API."""
|
|
808
|
+
global _rate_limit_cache
|
|
809
|
+
provider = CodexCliProvider()
|
|
810
|
+
process = None
|
|
811
|
+
try:
|
|
812
|
+
process = provider.app_server()
|
|
813
|
+
if process.stdin is None or process.stdout is None:
|
|
814
|
+
raise RateLimitResetError("Codex-reset is niet beschikbaar.")
|
|
815
|
+
process.stdin.write(
|
|
816
|
+
json.dumps(
|
|
817
|
+
{
|
|
818
|
+
"method": "initialize",
|
|
819
|
+
"id": 1,
|
|
820
|
+
"params": {
|
|
821
|
+
"clientInfo": {
|
|
822
|
+
"name": "engineering_platform_dashboard",
|
|
823
|
+
"title": "EP Operations",
|
|
824
|
+
"version": DASHBOARD_VERSION,
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
}
|
|
828
|
+
)
|
|
829
|
+
+ "\n"
|
|
830
|
+
)
|
|
831
|
+
process.stdin.flush()
|
|
832
|
+
deadline = time.monotonic() + 5
|
|
833
|
+
requested = False
|
|
834
|
+
while time.monotonic() < deadline:
|
|
835
|
+
ready, _, _ = select.select((process.stdout,), (), (), max(0, deadline - time.monotonic()))
|
|
836
|
+
if not ready:
|
|
837
|
+
break
|
|
838
|
+
line = process.stdout.readline()
|
|
839
|
+
if not line:
|
|
840
|
+
break
|
|
841
|
+
response = json.loads(line)
|
|
842
|
+
if response.get("id") == 1 and not requested:
|
|
843
|
+
process.stdin.write(json.dumps({"method": "initialized", "params": {}}) + "\n")
|
|
844
|
+
process.stdin.write(
|
|
845
|
+
json.dumps(
|
|
846
|
+
{
|
|
847
|
+
"method": "account/rateLimitResetCredit/consume",
|
|
848
|
+
"id": 2,
|
|
849
|
+
"params": {"idempotencyKey": str(uuid.uuid4())},
|
|
850
|
+
}
|
|
851
|
+
)
|
|
852
|
+
+ "\n"
|
|
853
|
+
)
|
|
854
|
+
process.stdin.flush()
|
|
855
|
+
requested = True
|
|
856
|
+
elif response.get("id") == 2:
|
|
857
|
+
result = response.get("result")
|
|
858
|
+
outcome = result.get("outcome") if isinstance(result, dict) else None
|
|
859
|
+
if outcome not in {"reset", "nothingToReset", "noCredit", "alreadyRedeemed"}:
|
|
860
|
+
raise RateLimitResetError("Codex-reset kon niet worden bevestigd.")
|
|
861
|
+
with _rate_limit_cache_lock:
|
|
862
|
+
_rate_limit_cache = None
|
|
863
|
+
return outcome
|
|
864
|
+
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
865
|
+
raise RateLimitResetError("Codex-reset is niet beschikbaar.") from error
|
|
866
|
+
finally:
|
|
867
|
+
if process is not None:
|
|
868
|
+
provider.close_app_server(process)
|
|
869
|
+
raise RateLimitResetError("Codex-reset reageerde niet op tijd.")
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _latest_codex_log(root: Path) -> bytes:
|
|
873
|
+
"""Retire the former checkout-local Codex diagnostic reader."""
|
|
874
|
+
return b"Lokale Codex CLI-diagnoselogs zijn retired; gebruik CENTRAL componentlogs."
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _component_log(root: Path, component: str) -> bytes:
|
|
878
|
+
"""Historical reader constrained to canonical component identities."""
|
|
879
|
+
return stored_component_log(root, component)
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def _component_log_page(root: Path, component: str, query: dict[str, list[str]]) -> dict[str, object]:
|
|
883
|
+
"""Historical reader with no alias expansion or local fallback."""
|
|
884
|
+
return stored_component_log_page(root, component)
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _clear_component_log(root: Path, component: str) -> None:
|
|
888
|
+
clear_stored_component_log(root, component)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _component_log_versions(root: Path) -> dict[str, str]:
|
|
892
|
+
return {component.id: component_log_version(root, component.id) for component in PLATFORM_COMPONENTS}
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _platform_health(_root: Path) -> dict[str, object]:
|
|
896
|
+
"""Historical projection derived from the sole canonical model."""
|
|
897
|
+
components = {component.id: {"healthy": True, "state": "available"} for component in PLATFORM_COMPONENTS}
|
|
898
|
+
return {"health": "ok", "healthy": True, "components": components}
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _component_uptime_seconds(component: str) -> int | None:
|
|
902
|
+
"""Legacy diagnostic accepts only a canonical component identifier."""
|
|
903
|
+
return None if component in PLATFORM_COMPONENT_BY_ID else None
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def _component_processes(component: str) -> list[dict[str, int | str]]:
|
|
907
|
+
return [] if component not in PLATFORM_COMPONENT_BY_ID else []
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _process_elapsed_seconds(value: str) -> int:
|
|
911
|
+
"""Retained parser for historical process evidence, not a component owner."""
|
|
912
|
+
if not re.fullmatch(r"(?:\d+-)?\d{1,2}:\d{2}:\d{2}", value):
|
|
913
|
+
raise ValueError("Ongeldige procesduur.")
|
|
914
|
+
days, clock = (value.split("-", 1) + [""])[:2] if "-" in value else ("0", value)
|
|
915
|
+
hours, minutes, seconds = (int(part) for part in clock.split(":"))
|
|
916
|
+
return int(days) * 86_400 + hours * 3600 + minutes * 60 + seconds
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def _component_details(root: Path, component: str) -> dict[str, object]:
|
|
920
|
+
if component not in PLATFORM_COMPONENT_BY_ID:
|
|
921
|
+
raise ValueError("Onbekend Engineering Platform-onderdeel")
|
|
922
|
+
return {"component": component, "health": _platform_health(root)["components"][component]}
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def _launch_agent_health(label: str) -> dict[str, str | bool]:
|
|
927
|
+
"""Inspect one owned LaunchAgent process without changing its state."""
|
|
928
|
+
state = LaunchdProvider().runtime_status(label)
|
|
929
|
+
if state.qualified:
|
|
930
|
+
return {"healthy": True, "state": "running", "detail": "LaunchAgent-proces is actief"}
|
|
931
|
+
if state.detail == "launchctl unavailable":
|
|
932
|
+
return {"healthy": False, "state": "unavailable", "detail": "launchctl ontbreekt"}
|
|
933
|
+
return {"healthy": False, "state": "not_running", "detail": "LaunchAgent is geladen, maar heeft geen actief proces" if state.detail.endswith("no active process") else "LaunchAgent is niet geladen"}
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def _launch_agent_details(label: str) -> dict[str, object]:
|
|
938
|
+
"""Return the safe, owned portion of one per-user LaunchAgent contract."""
|
|
939
|
+
plist_path = Path.home() / "Library" / "LaunchAgents" / f"{label}.plist"
|
|
940
|
+
details: dict[str, object] = {
|
|
941
|
+
"label": label,
|
|
942
|
+
"plist_path": str(plist_path),
|
|
943
|
+
"loaded": False,
|
|
944
|
+
"program_arguments": [],
|
|
945
|
+
"keep_alive": None,
|
|
946
|
+
"run_at_load": None,
|
|
947
|
+
}
|
|
948
|
+
try:
|
|
949
|
+
payload = plistlib.loads(plist_path.read_bytes())
|
|
950
|
+
except (OSError, plistlib.InvalidFileException):
|
|
951
|
+
return details
|
|
952
|
+
if not isinstance(payload, dict):
|
|
953
|
+
return details
|
|
954
|
+
arguments = payload.get("ProgramArguments")
|
|
955
|
+
if isinstance(arguments, list):
|
|
956
|
+
details["program_arguments"] = [str(value) for value in arguments[:8]]
|
|
957
|
+
details["keep_alive"] = bool(payload.get("KeepAlive"))
|
|
958
|
+
details["run_at_load"] = bool(payload.get("RunAtLoad"))
|
|
959
|
+
details["loaded"] = bool(_launch_agent_health(label).get("healthy"))
|
|
960
|
+
return details
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def _registered_worktree_path(root: Path, worktree_path: object, branch: object | None = None) -> Path:
|
|
965
|
+
"""Resolve one worktree from Git's current registration, never from HTTP input."""
|
|
966
|
+
if not isinstance(worktree_path, str) or not worktree_path or (branch is not None and not isinstance(branch, str)):
|
|
967
|
+
raise ValueError("De gekozen worktree is ongeldig.")
|
|
968
|
+
projection = _workspace_worktrees(root)
|
|
969
|
+
candidates = [
|
|
970
|
+
item for item in projection.get("worktrees", [])
|
|
971
|
+
if isinstance(item, dict)
|
|
972
|
+
and isinstance(item.get("path"), str)
|
|
973
|
+
and item["path"] == worktree_path
|
|
974
|
+
and (branch is None or item.get("branch") == branch)
|
|
975
|
+
]
|
|
976
|
+
if len(candidates) != 1:
|
|
977
|
+
raise RuntimeError("De gekozen worktree is niet beschikbaar voor een veilige switch.")
|
|
978
|
+
target = Path(candidates[0]["path"]).resolve()
|
|
979
|
+
if not target.is_absolute():
|
|
980
|
+
raise RuntimeError("De gekozen worktree is niet beschikbaar voor een veilige switch.")
|
|
981
|
+
return target
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _workspace_git_lock(root: Path, *, now: float | None = None) -> dict[str, object]:
|
|
985
|
+
"""Describe the index lock without offering recovery unless it is provably stale."""
|
|
986
|
+
lock_path = root / ".git" / "index.lock"
|
|
987
|
+
try:
|
|
988
|
+
age_seconds = max(0, int((now if now is not None else time.time()) - lock_path.stat().st_mtime))
|
|
989
|
+
except OSError:
|
|
990
|
+
return {"state": "free", "active": False, "stale": False}
|
|
991
|
+
|
|
992
|
+
# lsof is the conservative ownership check: if it is unavailable or cannot
|
|
993
|
+
# determine ownership, recovery remains disabled. Never guess that a lock
|
|
994
|
+
# is stale merely because it is old.
|
|
995
|
+
lsof = shutil.which("lsof")
|
|
996
|
+
if not lsof:
|
|
997
|
+
return {"state": "active", "active": True, "stale": False, "age_seconds": age_seconds}
|
|
998
|
+
try:
|
|
999
|
+
ownership = LocalProcessProvider().execute(root, (lsof, "-t", str(lock_path)))
|
|
1000
|
+
except OSError:
|
|
1001
|
+
return {"state": "active", "active": True, "stale": False, "age_seconds": age_seconds}
|
|
1002
|
+
owner_pids = [line for line in ownership.stdout.splitlines() if line.strip().isdigit()]
|
|
1003
|
+
stale = ownership.returncode == 1 and not owner_pids and age_seconds >= GIT_INDEX_LOCK_STALE_SECONDS
|
|
1004
|
+
return {
|
|
1005
|
+
"state": "stale" if stale else "active",
|
|
1006
|
+
"active": not stale,
|
|
1007
|
+
"stale": stale,
|
|
1008
|
+
"age_seconds": age_seconds,
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def _branch_is_verified_merged_into_main(
|
|
1014
|
+
root: Path, expected_branch: str, branch: str, provider: GitProvider
|
|
1015
|
+
) -> bool:
|
|
1016
|
+
"""Accept an older local head only when a merged PR proves it reached main."""
|
|
1017
|
+
try:
|
|
1018
|
+
remote = provider.execute(root, "git", "remote", "get-url", "origin")
|
|
1019
|
+
match = re.search(r"github\.com[:/]([^/\s]+)/([^/\s]+?)(?:\.git)?$", remote.stdout.strip()) if remote.returncode == 0 else None
|
|
1020
|
+
if not match:
|
|
1021
|
+
return False
|
|
1022
|
+
payload = GitHubProvider().github(
|
|
1023
|
+
"pr", "list", "--repo", f"{match.group(1)}/{match.group(2)}", "--state", "merged", "--head", branch,
|
|
1024
|
+
"--json", "number,headRefName,headRefOid,mergeCommit", "--limit", "2",
|
|
1025
|
+
)
|
|
1026
|
+
pull_requests = json.loads(payload)
|
|
1027
|
+
if not isinstance(pull_requests, list):
|
|
1028
|
+
return False
|
|
1029
|
+
pull_request = next((item for item in pull_requests if isinstance(item, dict) and item.get("headRefName") == branch), None)
|
|
1030
|
+
number = pull_request.get("number") if isinstance(pull_request, dict) else None
|
|
1031
|
+
merge_commit = pull_request.get("mergeCommit") if isinstance(pull_request, dict) else None
|
|
1032
|
+
merge_oid = merge_commit.get("oid") if isinstance(merge_commit, dict) else None
|
|
1033
|
+
local_head = provider.execute(root, "git", "rev-parse", "--verify", branch)
|
|
1034
|
+
if not isinstance(number, int) or not isinstance(merge_oid, str) or local_head.returncode or not local_head.stdout.strip():
|
|
1035
|
+
return False
|
|
1036
|
+
merged_into_main = provider.execute(root, "git", "merge-base", "--is-ancestor", merge_oid, expected_branch)
|
|
1037
|
+
return (
|
|
1038
|
+
merged_into_main.returncode == 0
|
|
1039
|
+
and _github_pull_request_contains_commit(f"{match.group(1)}/{match.group(2)}", number, local_head.stdout.strip())
|
|
1040
|
+
)
|
|
1041
|
+
except (OSError, RuntimeError, ValueError, json.JSONDecodeError):
|
|
1042
|
+
return False
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def _github_pull_request_contains_commit(repository: str, number: int, commit_sha: str) -> bool:
|
|
1046
|
+
"""Use GitHub's immutable PR commit record when a deleted head is not local."""
|
|
1047
|
+
try:
|
|
1048
|
+
payload = GitHubProvider().github("pr", "view", str(number), "--repo", repository, "--json", "commits")
|
|
1049
|
+
pull_request = json.loads(payload)
|
|
1050
|
+
except (OSError, RuntimeError, ValueError, json.JSONDecodeError):
|
|
1051
|
+
return False
|
|
1052
|
+
commits = pull_request.get("commits") if isinstance(pull_request, dict) else None
|
|
1053
|
+
return isinstance(commits, list) and any(
|
|
1054
|
+
isinstance(item, dict) and isinstance(item.get("oid"), str) and item["oid"] == commit_sha
|
|
1055
|
+
for item in commits
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def _github_pull_request_for_detached_commit(
|
|
1060
|
+
root: Path, repository: str, commit_sha: str, expected_branch: str, provider: GitProvider,
|
|
1061
|
+
) -> dict[str, object] | None:
|
|
1062
|
+
"""Return immutable PR evidence for one detached commit, if GitHub can prove it."""
|
|
1063
|
+
try:
|
|
1064
|
+
payload = json.loads(GitHubProvider().github("api", f"repos/{repository}/commits/{commit_sha}/pulls"))
|
|
1065
|
+
if not isinstance(payload, list):
|
|
1066
|
+
return None
|
|
1067
|
+
fallback: dict[str, object] | None = None
|
|
1068
|
+
for item in payload:
|
|
1069
|
+
if not isinstance(item, dict) or not isinstance(item.get("number"), int) or not isinstance(item.get("html_url"), str):
|
|
1070
|
+
continue
|
|
1071
|
+
state = "MERGED" if item.get("merged_at") else str(item.get("state") or "").upper()
|
|
1072
|
+
merge_oid = item.get("merge_commit_sha")
|
|
1073
|
+
verified = False
|
|
1074
|
+
if state == "MERGED" and isinstance(merge_oid, str) and merge_oid:
|
|
1075
|
+
merged = provider.execute(root, "git", "merge-base", "--is-ancestor", merge_oid, expected_branch)
|
|
1076
|
+
if merged.returncode not in {0, 1}:
|
|
1077
|
+
continue
|
|
1078
|
+
verified = merged.returncode == 0
|
|
1079
|
+
evidence = {"number": item["number"], "url": item["html_url"], "state": state, "verified": verified}
|
|
1080
|
+
if verified:
|
|
1081
|
+
return evidence
|
|
1082
|
+
if fallback is None or (state == "OPEN" and fallback.get("state") != "OPEN"):
|
|
1083
|
+
fallback = evidence
|
|
1084
|
+
except (OSError, RuntimeError, ValueError, json.JSONDecodeError):
|
|
1085
|
+
return None
|
|
1086
|
+
return fallback
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
def _workspace_open_pull_requests(root: Path) -> list[dict[str, object]] | None:
|
|
1090
|
+
"""Return PR context, or ``None`` when GitHub cannot authoritatively answer."""
|
|
1091
|
+
try:
|
|
1092
|
+
remote = GitProvider().execute(root, "git", "remote", "get-url", "origin")
|
|
1093
|
+
match = re.search(r"github\.com[:/]([^/\s]+)/([^/\s]+?)(?:\.git)?$", remote.stdout.strip()) if remote.returncode == 0 else None
|
|
1094
|
+
if not match:
|
|
1095
|
+
return None
|
|
1096
|
+
payload = GitHubProvider().github(
|
|
1097
|
+
"pr", "list", "--repo", f"{match.group(1)}/{match.group(2)}", "--state", "open",
|
|
1098
|
+
"--json", "number,title,url,headRefOid,headRefName,isDraft,mergeStateStatus,reviewDecision,reviews,statusCheckRollup", "--limit", "20",
|
|
1099
|
+
)
|
|
1100
|
+
candidates = json.loads(payload)
|
|
1101
|
+
except (OSError, RuntimeError, json.JSONDecodeError):
|
|
1102
|
+
return None
|
|
1103
|
+
if not isinstance(candidates, list):
|
|
1104
|
+
return None
|
|
1105
|
+
result: list[dict[str, object]] = []
|
|
1106
|
+
for candidate in candidates:
|
|
1107
|
+
if not isinstance(candidate, dict):
|
|
1108
|
+
continue
|
|
1109
|
+
number, title, url, branch = candidate.get("number"), candidate.get("title"), candidate.get("url"), candidate.get("headRefName")
|
|
1110
|
+
if isinstance(number, int) and number > 0 and all(isinstance(value, str) for value in (title, url, branch)) and url.startswith("https://github.com/"):
|
|
1111
|
+
head_sha = candidate.get("headRefOid")
|
|
1112
|
+
failed_checks, checks_terminal = pr_check_repair_check_summary(candidate.get("statusCheckRollup"))
|
|
1113
|
+
authorization_requested = _owner_authorization_requested(candidate)
|
|
1114
|
+
repair_state = pr_check_repair_state(root, number, head_sha)
|
|
1115
|
+
repair_active = repair_state in {"QUEUED", "RUNNING"} or (
|
|
1116
|
+
repair_state == "SUBMITTED" and not checks_terminal
|
|
1117
|
+
)
|
|
1118
|
+
repair_completed_for_head = repair_state == "SUBMITTED" and checks_terminal
|
|
1119
|
+
result.append({
|
|
1120
|
+
"number": number,
|
|
1121
|
+
"title": title,
|
|
1122
|
+
"url": url,
|
|
1123
|
+
"branch": branch,
|
|
1124
|
+
"status": _open_pull_request_status(candidate),
|
|
1125
|
+
"owner_approval": _owner_approval_status(candidate, match.group(1)),
|
|
1126
|
+
"owner_authorization_requested": authorization_requested,
|
|
1127
|
+
"failed_checks": failed_checks,
|
|
1128
|
+
# This is an explicit, one-shot operator action for a
|
|
1129
|
+
# human-authored same-repository PR. Endpoint admission
|
|
1130
|
+
# re-reads all GitHub evidence before any provider is used.
|
|
1131
|
+
"check_repair_available": bool(failed_checks) and checks_terminal and not authorization_requested and not repair_active and not pr_check_repair_attempted(root, number, head_sha),
|
|
1132
|
+
"check_repair_state": repair_state if repair_active else None,
|
|
1133
|
+
"check_repair_completed_for_head": repair_completed_for_head,
|
|
1134
|
+
})
|
|
1135
|
+
return result
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def _owner_approval_status(pull_request: dict[str, object], repository_owner: str) -> str:
|
|
1139
|
+
"""Project owner approval, including GitHub's exact-SHA authorization check."""
|
|
1140
|
+
# ``reviewDecision`` only represents GitHub reviews. The policy gate for
|
|
1141
|
+
# high-risk work is published as a legacy commit StatusContext instead, so
|
|
1142
|
+
# it must take precedence over the optional-review fallback below.
|
|
1143
|
+
checks = pull_request.get("statusCheckRollup")
|
|
1144
|
+
if isinstance(checks, list):
|
|
1145
|
+
for check in checks:
|
|
1146
|
+
if not isinstance(check, dict):
|
|
1147
|
+
continue
|
|
1148
|
+
name = str(check.get("name") or check.get("context") or "").casefold()
|
|
1149
|
+
if name != "owner authorization":
|
|
1150
|
+
continue
|
|
1151
|
+
state = str(check.get("state") or check.get("status") or "").upper()
|
|
1152
|
+
conclusion = str(check.get("conclusion") or "").upper()
|
|
1153
|
+
if state not in {"SUCCESS", "COMPLETED"} or conclusion not in {"", "SUCCESS", "NEUTRAL", "SKIPPED"}:
|
|
1154
|
+
return "pending"
|
|
1155
|
+
# This check exists only when the HIGH_RISK owner-authorization
|
|
1156
|
+
# gate applied to this exact commit. A successful result is an
|
|
1157
|
+
# affirmative owner decision, not the absence of a requirement.
|
|
1158
|
+
# Do not fall through to GitHub's optional-review fallback: the
|
|
1159
|
+
# owner may have authorized with the dashboard control rather
|
|
1160
|
+
# than submitted a conventional PR review.
|
|
1161
|
+
return "approved"
|
|
1162
|
+
reviews = pull_request.get("reviews")
|
|
1163
|
+
if not isinstance(reviews, list) or not repository_owner:
|
|
1164
|
+
return "pending"
|
|
1165
|
+
owner_reviews = [
|
|
1166
|
+
review for review in reviews
|
|
1167
|
+
if isinstance(review, dict) and isinstance(review.get("author"), dict)
|
|
1168
|
+
and str(review["author"].get("login") or "").casefold() == repository_owner.casefold()
|
|
1169
|
+
]
|
|
1170
|
+
if not owner_reviews:
|
|
1171
|
+
# GitHub reports ``null`` when its branch policy does not require a
|
|
1172
|
+
# review. Preserve ``pending`` for an unavailable projection, but do
|
|
1173
|
+
# not turn the absence of an optional owner review into a false wait.
|
|
1174
|
+
if "reviewDecision" in pull_request and str(pull_request.get("reviewDecision") or "").upper() != "REVIEW_REQUIRED":
|
|
1175
|
+
return "not_required"
|
|
1176
|
+
return "pending"
|
|
1177
|
+
latest = max(owner_reviews, key=lambda review: str(review.get("submittedAt") or ""))
|
|
1178
|
+
state = str(latest.get("state") or "").upper()
|
|
1179
|
+
if state == "APPROVED":
|
|
1180
|
+
return "approved"
|
|
1181
|
+
if state == "CHANGES_REQUESTED":
|
|
1182
|
+
return "changes_requested"
|
|
1183
|
+
return "pending"
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def _owner_authorization_requested(pull_request: dict[str, object]) -> bool:
|
|
1187
|
+
"""Whether GitHub has requested exact-SHA HIGH_RISK authorization."""
|
|
1188
|
+
checks = pull_request.get("statusCheckRollup")
|
|
1189
|
+
if not isinstance(checks, list):
|
|
1190
|
+
return False
|
|
1191
|
+
return any(
|
|
1192
|
+
isinstance(check, dict)
|
|
1193
|
+
and str(check.get("name") or check.get("context") or "").casefold() == "owner authorization"
|
|
1194
|
+
and str(check.get("state") or check.get("status") or "").upper() == "FAILURE"
|
|
1195
|
+
for check in checks
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
class OwnerAuthorizationRequestError(RuntimeError):
|
|
1200
|
+
"""The exact-SHA Owner Authorization workflow cannot safely be dispatched."""
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def _request_owner_authorization(root: Path, pull_request_number: int) -> dict[str, object]:
|
|
1204
|
+
"""Dispatch the canonical owner workflow for the current HIGH_RISK PR SHA.
|
|
1205
|
+
|
|
1206
|
+
The browser supplies only a PR number. Repository, target branch and SHA
|
|
1207
|
+
are read afresh from GitHub so this endpoint cannot authorize a stale or
|
|
1208
|
+
caller-selected commit. The workflow remains the sole publisher of the
|
|
1209
|
+
``Owner Authorization`` status.
|
|
1210
|
+
"""
|
|
1211
|
+
if isinstance(pull_request_number, bool) or pull_request_number < 1:
|
|
1212
|
+
raise OwnerAuthorizationRequestError("owner_authorization_invalid_request")
|
|
1213
|
+
try:
|
|
1214
|
+
remote = GitProvider().execute(root, "git", "remote", "get-url", "origin")
|
|
1215
|
+
match = re.search(r"github\.com[:/]([^/\s]+)/([^/\s]+?)(?:\.git)?$", remote.stdout.strip()) if remote.returncode == 0 else None
|
|
1216
|
+
if not match:
|
|
1217
|
+
raise OwnerAuthorizationRequestError("owner_authorization_unavailable")
|
|
1218
|
+
repository = f"{match.group(1)}/{match.group(2)}"
|
|
1219
|
+
payload = GitHubProvider().github(
|
|
1220
|
+
"pr", "view", str(pull_request_number), "--repo", repository,
|
|
1221
|
+
"--json", "number,state,headRefOid,baseRefName,statusCheckRollup",
|
|
1222
|
+
)
|
|
1223
|
+
pull_request = json.loads(payload)
|
|
1224
|
+
except OwnerAuthorizationRequestError:
|
|
1225
|
+
raise
|
|
1226
|
+
except (OSError, RuntimeError, json.JSONDecodeError) as error:
|
|
1227
|
+
raise OwnerAuthorizationRequestError("owner_authorization_unavailable") from error
|
|
1228
|
+
if not isinstance(pull_request, dict) or pull_request.get("number") != pull_request_number:
|
|
1229
|
+
raise OwnerAuthorizationRequestError("owner_authorization_unavailable")
|
|
1230
|
+
candidate_sha = pull_request.get("headRefOid")
|
|
1231
|
+
target_branch = pull_request.get("baseRefName")
|
|
1232
|
+
if (
|
|
1233
|
+
str(pull_request.get("state") or "").upper() != "OPEN"
|
|
1234
|
+
or not isinstance(candidate_sha, str)
|
|
1235
|
+
or not re.fullmatch(r"[0-9a-f]{40}", candidate_sha)
|
|
1236
|
+
or not isinstance(target_branch, str)
|
|
1237
|
+
or not target_branch
|
|
1238
|
+
or not _owner_authorization_requested(pull_request)
|
|
1239
|
+
):
|
|
1240
|
+
raise OwnerAuthorizationRequestError("owner_authorization_not_requested")
|
|
1241
|
+
checks = pull_request.get("statusCheckRollup")
|
|
1242
|
+
trusted_delivery_passed = isinstance(checks, list) and any(
|
|
1243
|
+
isinstance(check, dict)
|
|
1244
|
+
and str(check.get("name") or "") == "Trusted Delivery qualification / Qualify trusted delivery"
|
|
1245
|
+
and str(check.get("status") or "").upper() == "COMPLETED"
|
|
1246
|
+
and str(check.get("conclusion") or "").upper() == "SUCCESS"
|
|
1247
|
+
for check in checks
|
|
1248
|
+
)
|
|
1249
|
+
if not trusted_delivery_passed:
|
|
1250
|
+
raise OwnerAuthorizationRequestError("owner_authorization_qualification_pending")
|
|
1251
|
+
try:
|
|
1252
|
+
GitHubProvider().github(
|
|
1253
|
+
"workflow", "run", "owner-authorization.yml", "--repo", repository,
|
|
1254
|
+
"-f", f"repository={repository}", "-f", f"pr_number={pull_request_number}",
|
|
1255
|
+
"-f", f"candidate_sha={candidate_sha}", "-f", f"branch={target_branch}",
|
|
1256
|
+
)
|
|
1257
|
+
except RuntimeError as error:
|
|
1258
|
+
raise OwnerAuthorizationRequestError("owner_authorization_dispatch_failed") from error
|
|
1259
|
+
return {"queued": True, "pull_request": pull_request_number}
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
def _open_pull_request_status(pull_request: dict[str, object]) -> str:
|
|
1263
|
+
"""Classify GitHub's read-only PR check projection for dashboard display.
|
|
1264
|
+
|
|
1265
|
+
An unavailable or incomplete check projection deliberately stays
|
|
1266
|
+
``waiting_for_checks``: the dashboard must never imply that a PR is ready
|
|
1267
|
+
to review or merge until GitHub has reported enough terminal evidence.
|
|
1268
|
+
"""
|
|
1269
|
+
if pull_request.get("isDraft") is True:
|
|
1270
|
+
return "draft"
|
|
1271
|
+
merge_state = str(pull_request.get("mergeStateStatus") or "").upper()
|
|
1272
|
+
if merge_state == "BEHIND":
|
|
1273
|
+
return "branch_update_required"
|
|
1274
|
+
if merge_state in {"DIRTY", "BLOCKED"}:
|
|
1275
|
+
return "issues"
|
|
1276
|
+
review_decision = str(pull_request.get("reviewDecision") or "").upper()
|
|
1277
|
+
if review_decision == "CHANGES_REQUESTED":
|
|
1278
|
+
return "issues"
|
|
1279
|
+
check_rollup = pull_request.get("statusCheckRollup")
|
|
1280
|
+
if not isinstance(check_rollup, list):
|
|
1281
|
+
return "waiting_for_checks"
|
|
1282
|
+
failed_conclusions = {"ACTION_REQUIRED", "CANCELLED", "FAILURE", "STARTUP_FAILURE", "TIMED_OUT"}
|
|
1283
|
+
successful_conclusions = {"NEUTRAL", "SKIPPED", "SUCCESS"}
|
|
1284
|
+
conclusions: list[str] = []
|
|
1285
|
+
for check in check_rollup:
|
|
1286
|
+
if not isinstance(check, dict):
|
|
1287
|
+
return "waiting_for_checks"
|
|
1288
|
+
# Check runs expose ``status`` + ``conclusion``; legacy status
|
|
1289
|
+
# contexts expose their terminal result only as ``state``.
|
|
1290
|
+
conclusion = str(check.get("conclusion") or check.get("state") or "").upper()
|
|
1291
|
+
status = str(check.get("status") or "").upper()
|
|
1292
|
+
if conclusion in failed_conclusions:
|
|
1293
|
+
return "issues"
|
|
1294
|
+
if conclusion not in successful_conclusions or (status and status != "COMPLETED"):
|
|
1295
|
+
return "waiting_for_checks"
|
|
1296
|
+
conclusions.append(conclusion)
|
|
1297
|
+
if review_decision == "REVIEW_REQUIRED":
|
|
1298
|
+
return "ready_for_review"
|
|
1299
|
+
# A repository without required checks can still be ready to merge when
|
|
1300
|
+
# GitHub explicitly reports its merge state as clean. An absent/unknown
|
|
1301
|
+
# state remains fail-closed rather than presented as a false green result.
|
|
1302
|
+
return "ready_to_merge" if merge_state == "CLEAN" else "waiting_for_checks"
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
def _codex_process_metrics(root: Path) -> bytes:
|
|
1307
|
+
"""Measure only the process group explicitly recorded by the Execution Host."""
|
|
1308
|
+
try:
|
|
1309
|
+
runner = json.loads((root / ".engineering" / "status" / "runner_process.json").read_text(encoding="utf-8"))
|
|
1310
|
+
live = json.loads((root / ".engineering" / "status" / "current.json").read_text(encoding="utf-8"))
|
|
1311
|
+
process_group = runner.get("process_group") if runner.get("run_id") == live.get("run_id") else None
|
|
1312
|
+
runner_pid = runner.get("pid") if runner.get("run_id") == live.get("run_id") else None
|
|
1313
|
+
except (OSError, json.JSONDecodeError):
|
|
1314
|
+
process_group, runner_pid = None, None
|
|
1315
|
+
if not isinstance(process_group, int) or process_group <= 0 or not isinstance(runner_pid, int) or runner_pid <= 0:
|
|
1316
|
+
return json.dumps({"process_count": 0, "cpu_percent": 0, "gpu_status": "NO_ACTIVE_EXECUTION_HOST"}, separators=(",", ":")).encode()
|
|
1317
|
+
try:
|
|
1318
|
+
observed = LocalProcessProvider().execute(Path.cwd(), ("ps", "-axo", "pid=,pgid=,pcpu=,command="))
|
|
1319
|
+
except OSError:
|
|
1320
|
+
observed = None
|
|
1321
|
+
processes: list[dict[str, int | float]] = []
|
|
1322
|
+
owner_seen = False
|
|
1323
|
+
if observed and observed.returncode == 0:
|
|
1324
|
+
for line in observed.stdout.splitlines():
|
|
1325
|
+
parts = line.strip().split(maxsplit=3)
|
|
1326
|
+
if len(parts) != 4:
|
|
1327
|
+
continue
|
|
1328
|
+
try:
|
|
1329
|
+
pid, group, cpu = int(parts[0]), int(parts[1]), float(parts[2])
|
|
1330
|
+
owner_seen = owner_seen or (pid == runner_pid and group == process_group)
|
|
1331
|
+
if group == process_group:
|
|
1332
|
+
processes.append({"pid": pid, "cpu_percent": cpu})
|
|
1333
|
+
except ValueError:
|
|
1334
|
+
continue
|
|
1335
|
+
if not owner_seen:
|
|
1336
|
+
processes = []
|
|
1337
|
+
return json.dumps(
|
|
1338
|
+
{
|
|
1339
|
+
"process_count": len(processes),
|
|
1340
|
+
"cpu_percent": round(sum(item["cpu_percent"] for item in processes), 1),
|
|
1341
|
+
"gpu_status": "EXECUTION_HOST_EXTERNAL",
|
|
1342
|
+
},
|
|
1343
|
+
separators=(",", ":"),
|
|
1344
|
+
).encode()
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
def _report_for_run(root: Path, run_id: str | None) -> bytes:
|
|
1348
|
+
"""Return report evidence only for the exact displayed terminal run."""
|
|
1349
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id):
|
|
1350
|
+
return b""
|
|
1351
|
+
try:
|
|
1352
|
+
indexed_report = report_for_prompt_history(root, run_id)
|
|
1353
|
+
if indexed_report is not None:
|
|
1354
|
+
return indexed_report
|
|
1355
|
+
reports = list((root / ".engineering" / "reports").glob(f"*_{run_id}.md"))
|
|
1356
|
+
report = max(reports, key=lambda path: path.stat().st_mtime) if reports else None
|
|
1357
|
+
return report.read_bytes() if report else b""
|
|
1358
|
+
except OSError:
|
|
1359
|
+
return b""
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
def _reviewer_agents_for_run(root: Path, run_id: str | None) -> bytes:
|
|
1363
|
+
"""Project recorded specialist reviewer agents from the exact run report.
|
|
1364
|
+
|
|
1365
|
+
Reviewer agents are independent, read-only advisory calls. The dashboard
|
|
1366
|
+
deliberately derives this view from the immutable terminal report instead
|
|
1367
|
+
of inferring generic Codex sub-agents that the platform does not record.
|
|
1368
|
+
"""
|
|
1369
|
+
try:
|
|
1370
|
+
report = _report_for_run(root, run_id).decode("utf-8")
|
|
1371
|
+
except UnicodeDecodeError:
|
|
1372
|
+
return b"[]"
|
|
1373
|
+
section = re.search(
|
|
1374
|
+
r"^## Reviewer Findings\s*$\n(?P<body>.*?)(?=^##\s|\Z)", report, re.MULTILINE | re.DOTALL
|
|
1375
|
+
)
|
|
1376
|
+
if section is None:
|
|
1377
|
+
return b"[]"
|
|
1378
|
+
|
|
1379
|
+
records: list[dict[str, object]] = []
|
|
1380
|
+
for block in re.split(r"(?=^- Reviewer: )", section.group("body"), flags=re.MULTILINE):
|
|
1381
|
+
reviewer = re.search(r"^- Reviewer:\s*(.+)$", block, re.MULTILINE)
|
|
1382
|
+
if reviewer is None:
|
|
1383
|
+
continue
|
|
1384
|
+
|
|
1385
|
+
def field(name: str) -> str | None:
|
|
1386
|
+
match = re.search(rf"^ - {re.escape(name)}:\s*(.+)$", block, re.MULTILINE)
|
|
1387
|
+
if match is None:
|
|
1388
|
+
return None
|
|
1389
|
+
return " ".join(match.group(1).split())[:180]
|
|
1390
|
+
|
|
1391
|
+
accepted = field("Accepted recommendations")
|
|
1392
|
+
records.append(
|
|
1393
|
+
{
|
|
1394
|
+
"reviewer": " ".join(reviewer.group(1).split())[:80],
|
|
1395
|
+
"capability": field("Capability") or "engineering",
|
|
1396
|
+
"selected_because": field("Selected because") or "Niet vastgelegd.",
|
|
1397
|
+
"accepted_recommendations": int(accepted) if accepted and accepted.isdigit() else 0,
|
|
1398
|
+
"status": "Uitgevoerd",
|
|
1399
|
+
}
|
|
1400
|
+
)
|
|
1401
|
+
if len(records) == 12:
|
|
1402
|
+
break
|
|
1403
|
+
return json.dumps(records, separators=(",", ":")).encode()
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
def _report_analysis_for_run(root: Path, run_id: str | None) -> bytes:
|
|
1407
|
+
"""Return advisory analysis only when it belongs to the displayed terminal run."""
|
|
1408
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id):
|
|
1409
|
+
return b""
|
|
1410
|
+
safe_run_id = os.path.basename(run_id)
|
|
1411
|
+
if safe_run_id != run_id:
|
|
1412
|
+
return b""
|
|
1413
|
+
try:
|
|
1414
|
+
return (root / ".engineering" / "report-analysis" / f"{safe_run_id}.md").read_bytes()
|
|
1415
|
+
except OSError:
|
|
1416
|
+
return b""
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
def _report_analysis_available_for_run(root: Path, run_id: str | None) -> bool:
|
|
1420
|
+
"""Return whether the displayed terminal run has an advisory analysis."""
|
|
1421
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id):
|
|
1422
|
+
return False
|
|
1423
|
+
safe_run_id = os.path.basename(run_id)
|
|
1424
|
+
return safe_run_id == run_id and (root / ".engineering" / "report-analysis" / f"{safe_run_id}.md").is_file()
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
def _report_analysis_processing_status(root: Path, run_id: str | None) -> str | None:
|
|
1428
|
+
"""Read the fixed advisory-processing state without exposing provider diagnostics."""
|
|
1429
|
+
analysis = _report_analysis_for_run(root, run_id).decode("utf-8", errors="replace")
|
|
1430
|
+
match = re.search(r"(?m)^- Status: `([a-z_]+)`$", analysis)
|
|
1431
|
+
return match.group(1) if match else None
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
def _retry_report_analysis(root: Path, run_id: object) -> bytes:
|
|
1435
|
+
"""Regenerate only a retryable advisory analysis for its exact terminal report."""
|
|
1436
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id):
|
|
1437
|
+
raise ValueError("Ongeldige uitvoeringsreferentie voor AI-analyse.")
|
|
1438
|
+
if _report_analysis_processing_status(root, run_id) not in RETRYABLE_REPORT_ANALYSIS_STATUSES:
|
|
1439
|
+
raise ValueError("Deze AI-analyse hoeft niet opnieuw te worden gegenereerd.")
|
|
1440
|
+
report = report_path_for_prompt_history(root, run_id)
|
|
1441
|
+
if report is None:
|
|
1442
|
+
raise ValueError("Engineeringrapport is niet beschikbaar voor deze uitvoering.")
|
|
1443
|
+
with _REPORT_ANALYSIS_RETRY_LOCK:
|
|
1444
|
+
if run_id in _REPORT_ANALYSIS_RETRY_RUNS:
|
|
1445
|
+
raise RuntimeError("AI-analyse wordt al opnieuw gegenereerd.")
|
|
1446
|
+
_REPORT_ANALYSIS_RETRY_RUNS.add(run_id)
|
|
1447
|
+
try:
|
|
1448
|
+
return analyze_terminal_report(root, run_id, report).read_bytes()
|
|
1449
|
+
finally:
|
|
1450
|
+
with _REPORT_ANALYSIS_RETRY_LOCK:
|
|
1451
|
+
_REPORT_ANALYSIS_RETRY_RUNS.discard(run_id)
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
def _current_codex_log(root: Path) -> bytes:
|
|
1455
|
+
"""Retire local current-run diagnostics as an operational read source."""
|
|
1456
|
+
return b"Lokale Codex CLI-diagnoselogs zijn retired; gebruik CENTRAL componentlogs."
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
def _last_executed_codex_log(root: Path) -> bytes:
|
|
1460
|
+
"""Retire local terminal-run diagnostics as an operational read source."""
|
|
1461
|
+
return b"Lokale Codex CLI-diagnoselogs zijn retired; gebruik CENTRAL componentlogs."
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def _codex_usage(root: Path) -> bytes:
|
|
1465
|
+
"""Return usage only when it is bound to the currently displayed run."""
|
|
1466
|
+
try:
|
|
1467
|
+
status = json.loads(_status(root))
|
|
1468
|
+
recorded = json.loads((root / ".engineering" / "status" / "codex_usage.json").read_text(encoding="utf-8"))
|
|
1469
|
+
run_id = recorded.get("run_id")
|
|
1470
|
+
usage = recorded.get("usage")
|
|
1471
|
+
except (OSError, json.JSONDecodeError):
|
|
1472
|
+
return b"{}"
|
|
1473
|
+
displayed_run = status.get("run_id") or status.get("last_executed_run")
|
|
1474
|
+
if run_id != displayed_run or not isinstance(usage, dict):
|
|
1475
|
+
return b"{}"
|
|
1476
|
+
allowed = {key: value for key, value in usage.items() if isinstance(key, str) and isinstance(value, (int, float, str))}
|
|
1477
|
+
return json.dumps(allowed, separators=(",", ":")).encode()
|
|
1478
|
+
|
|
1479
|
+
|
|
1480
|
+
def _codex_usage_for_run(root: Path, run_id: str | None) -> bytes:
|
|
1481
|
+
"""Return CLI usage only when it belongs to the named terminal run."""
|
|
1482
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id):
|
|
1483
|
+
return b"{}"
|
|
1484
|
+
try:
|
|
1485
|
+
recorded = json.loads(
|
|
1486
|
+
(root / ".engineering" / "status" / "codex_usage.json").read_text(encoding="utf-8")
|
|
1487
|
+
)
|
|
1488
|
+
usage = recorded.get("usage")
|
|
1489
|
+
except (OSError, json.JSONDecodeError):
|
|
1490
|
+
return b"{}"
|
|
1491
|
+
if recorded.get("run_id") != run_id or not isinstance(usage, dict):
|
|
1492
|
+
return b"{}"
|
|
1493
|
+
allowed = {
|
|
1494
|
+
key: value
|
|
1495
|
+
for key, value in usage.items()
|
|
1496
|
+
if isinstance(key, str) and isinstance(value, (int, float, str))
|
|
1497
|
+
}
|
|
1498
|
+
return json.dumps(allowed, separators=(",", ":")).encode()
|
|
1499
|
+
|
|
1500
|
+
|
|
1501
|
+
def _completion_commits(root: Path) -> bytes:
|
|
1502
|
+
"""Return only recorded commit evidence for a completed displayed run."""
|
|
1503
|
+
try:
|
|
1504
|
+
status = json.loads(_status(root))
|
|
1505
|
+
if status.get("current_phase") != "COMPLETE":
|
|
1506
|
+
return b"{}"
|
|
1507
|
+
run_id = status.get("run_id")
|
|
1508
|
+
if not isinstance(run_id, str):
|
|
1509
|
+
return b"{}"
|
|
1510
|
+
checkpoint = _canonical_checkpoint(root, run_id)
|
|
1511
|
+
except (OSError, json.JSONDecodeError):
|
|
1512
|
+
return b"{}"
|
|
1513
|
+
labels = {
|
|
1514
|
+
"genesis_commit_sha": "Genesis-commit",
|
|
1515
|
+
"implementation_merge_commit": "Implementatie-mergecommit",
|
|
1516
|
+
"finalization_merge_commit": "Finalisatie-mergecommit",
|
|
1517
|
+
}
|
|
1518
|
+
commits = {labels[key]: checkpoint[key] for key in labels if isinstance(checkpoint.get(key), str)}
|
|
1519
|
+
return json.dumps(commits, separators=(",", ":")).encode()
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
def _commits_for_run(root: Path, run_id: str | None) -> dict[str, str]:
|
|
1523
|
+
"""Return commit evidence owned by one exact terminal execution."""
|
|
1524
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id):
|
|
1525
|
+
return {}
|
|
1526
|
+
checkpoint = _canonical_checkpoint(root, run_id)
|
|
1527
|
+
if not checkpoint:
|
|
1528
|
+
return {}
|
|
1529
|
+
labels = {
|
|
1530
|
+
"genesis_commit_sha": "Genesis-commit",
|
|
1531
|
+
"implementation_merge_commit": "Implementatie-mergecommit",
|
|
1532
|
+
"finalization_merge_commit": "Finalisatie-mergecommit",
|
|
1533
|
+
}
|
|
1534
|
+
return {labels[key]: checkpoint[key] for key in labels if isinstance(checkpoint.get(key), str)}
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
def _commit_timeline_for_run(root: Path, run_id: str | None) -> list[dict[str, str]]:
|
|
1538
|
+
"""Project only strict, checkpoint-owned verified commit events.
|
|
1539
|
+
|
|
1540
|
+
This is intentionally a read-only projection: old or malformed
|
|
1541
|
+
checkpoint values never become dashboard evidence.
|
|
1542
|
+
"""
|
|
1543
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id):
|
|
1544
|
+
return []
|
|
1545
|
+
checkpoint = _canonical_checkpoint(root, run_id)
|
|
1546
|
+
raw = checkpoint.get("commit_evidence")
|
|
1547
|
+
if not isinstance(raw, list):
|
|
1548
|
+
return []
|
|
1549
|
+
events: list[dict[str, str]] = []
|
|
1550
|
+
for item in raw:
|
|
1551
|
+
if not isinstance(item, dict):
|
|
1552
|
+
continue
|
|
1553
|
+
phase, observed_at, commit_sha, description = (
|
|
1554
|
+
item.get("phase"), item.get("observed_at"), item.get("commit_sha"), item.get("description"),
|
|
1555
|
+
)
|
|
1556
|
+
if not is_valid_commit_evidence_record(item):
|
|
1557
|
+
continue
|
|
1558
|
+
events.append({
|
|
1559
|
+
"phase": phase,
|
|
1560
|
+
"observed_at": observed_at,
|
|
1561
|
+
"commit_sha": commit_sha,
|
|
1562
|
+
"description": description,
|
|
1563
|
+
})
|
|
1564
|
+
return sorted(events, key=lambda item: item["observed_at"])
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
def _last_executed_commits(root: Path) -> bytes:
|
|
1568
|
+
"""Return commit evidence bound to the most recent completed run only."""
|
|
1569
|
+
try:
|
|
1570
|
+
status = json.loads(_status(root))
|
|
1571
|
+
run_id = status.get("last_executed_run")
|
|
1572
|
+
if status.get("last_executed_phase") != "COMPLETE":
|
|
1573
|
+
return b"{}"
|
|
1574
|
+
except json.JSONDecodeError:
|
|
1575
|
+
return b"{}"
|
|
1576
|
+
return json.dumps(_commits_for_run(root, run_id), separators=(",", ":")).encode()
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
def _last_executed_agent_execution(root: Path, run_id: str | None) -> bytes:
|
|
1580
|
+
"""Return run-bound AI timing and terminal timestamp evidence."""
|
|
1581
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id):
|
|
1582
|
+
return b"{}"
|
|
1583
|
+
result: dict[str, float] = {}
|
|
1584
|
+
checkpoint = _canonical_checkpoint(root, run_id)
|
|
1585
|
+
seconds = checkpoint.get("agent_execution_seconds")
|
|
1586
|
+
if isinstance(seconds, (int, float)) and not isinstance(seconds, bool) and 0 <= seconds <= 86_400:
|
|
1587
|
+
result["seconds"] = float(seconds)
|
|
1588
|
+
try:
|
|
1589
|
+
timing = execution_timing(root, run_id)
|
|
1590
|
+
total = timing.get("total_execution_seconds")
|
|
1591
|
+
finished_at = timing.get("finished_at")
|
|
1592
|
+
except Exception:
|
|
1593
|
+
total = finished_at = None
|
|
1594
|
+
if isinstance(total, (int, float)) and not isinstance(total, bool) and 0 <= total <= 86_400:
|
|
1595
|
+
result["total_seconds"] = float(total)
|
|
1596
|
+
if isinstance(finished_at, str):
|
|
1597
|
+
result["finished_at"] = finished_at
|
|
1598
|
+
return json.dumps(result, separators=(",", ":")).encode()
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
def _last_executed_runtime_metadata(root: Path, run_id: str | None) -> bytes:
|
|
1602
|
+
"""Project only report-bound runtime provenance for the displayed run."""
|
|
1603
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id):
|
|
1604
|
+
return b"{}"
|
|
1605
|
+
report = _report_for_run(root, run_id)
|
|
1606
|
+
try:
|
|
1607
|
+
text = report.decode("utf-8")
|
|
1608
|
+
except UnicodeDecodeError:
|
|
1609
|
+
return b"{}"
|
|
1610
|
+
fields = {
|
|
1611
|
+
"runtime_provider": "Runtime Provider",
|
|
1612
|
+
"model": "AI Model",
|
|
1613
|
+
"reasoning_profile": "Reasoning Profile",
|
|
1614
|
+
"configuration_profile": "Configuration Profile",
|
|
1615
|
+
"codex_cli_version": "Codex CLI Version",
|
|
1616
|
+
"codex_cli_installation_path": "Codex CLI Installation Path",
|
|
1617
|
+
}
|
|
1618
|
+
metadata: dict[str, str] = {}
|
|
1619
|
+
for key, label in fields.items():
|
|
1620
|
+
match = re.search(rf"^- {re.escape(label)}: `([^`\n]{{1,120}})`$", text, re.MULTILINE)
|
|
1621
|
+
if match:
|
|
1622
|
+
metadata[key] = match.group(1)
|
|
1623
|
+
return json.dumps(metadata, separators=(",", ":")).encode()
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
def _prompt_started(root: Path) -> bytes:
|
|
1627
|
+
"""Return the recorded Inbox start time for the run currently displayed."""
|
|
1628
|
+
try:
|
|
1629
|
+
run_id = json.loads(_status(root)).get("run_id")
|
|
1630
|
+
except json.JSONDecodeError:
|
|
1631
|
+
run_id = None
|
|
1632
|
+
if not isinstance(run_id, str):
|
|
1633
|
+
return b"{}"
|
|
1634
|
+
for record in (root / ".engineering" / "inbox-processing").glob("*/job.json"):
|
|
1635
|
+
try:
|
|
1636
|
+
job = json.loads(record.read_text(encoding="utf-8"))
|
|
1637
|
+
except (OSError, json.JSONDecodeError):
|
|
1638
|
+
continue
|
|
1639
|
+
if job.get("run_id") == run_id and isinstance(job.get("received_at"), str):
|
|
1640
|
+
return json.dumps({"started_at": job["received_at"]}, separators=(",", ":")).encode()
|
|
1641
|
+
return b"{}"
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
def _build_commit(root: Path) -> str:
|
|
1645
|
+
"""Return the local checked-out revision for read-only dashboard identification."""
|
|
1646
|
+
try:
|
|
1647
|
+
observed = GitProvider().execute(root, "git", "rev-parse", "--short=12", "HEAD")
|
|
1648
|
+
except OSError:
|
|
1649
|
+
return ""
|
|
1650
|
+
return observed.stdout.strip() if observed.returncode == 0 else ""
|
|
1651
|
+
|
|
1652
|
+
|
|
1653
|
+
def _tracked_file_count(root: Path) -> str:
|
|
1654
|
+
"""Return the recursive count of files tracked by the workspace Git repository."""
|
|
1655
|
+
try:
|
|
1656
|
+
observed = GitProvider().execute(root, "git", "ls-files", "-z")
|
|
1657
|
+
except OSError:
|
|
1658
|
+
return ""
|
|
1659
|
+
if observed.returncode != 0:
|
|
1660
|
+
return ""
|
|
1661
|
+
separator = b"\0" if isinstance(observed.stdout, bytes) else "\0"
|
|
1662
|
+
return str(sum(1 for path in observed.stdout.split(separator) if path))
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
def _workspace_free_disk_space(root: Path) -> str:
|
|
1666
|
+
"""Return free space on the volume that contains the workspace."""
|
|
1667
|
+
try:
|
|
1668
|
+
free_gigabytes = shutil.disk_usage(root).free / (1024**3)
|
|
1669
|
+
except OSError:
|
|
1670
|
+
return ""
|
|
1671
|
+
return f"{free_gigabytes:.1f} GB"
|
|
1672
|
+
|
|
1673
|
+
|
|
1674
|
+
def _provider_login_status(root: Path) -> dict[str, dict[str, str]]:
|
|
1675
|
+
"""Dashboard projection of the shared token-free provider readiness check."""
|
|
1676
|
+
statuses = provider_readiness_status(root)
|
|
1677
|
+
runtime = provider_runtime_details(root)
|
|
1678
|
+
return {
|
|
1679
|
+
provider: {**value, **runtime.get(provider, {})}
|
|
1680
|
+
for provider, value in statuses.items()
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
|
|
1684
|
+
def _execution_runtime_status() -> dict[str, str]:
|
|
1685
|
+
"""Project a token-free readiness check for the installed Server Python."""
|
|
1686
|
+
# Preserve the installed venv launcher rather than its base interpreter.
|
|
1687
|
+
executable = Path(sys.executable).expanduser().absolute()
|
|
1688
|
+
ready = executable.is_file() and os.access(executable, os.X_OK)
|
|
1689
|
+
return {
|
|
1690
|
+
"state": "READY" if ready else "UNAVAILABLE",
|
|
1691
|
+
"executable": str(executable) if ready else "",
|
|
1692
|
+
"version": sys.version.split()[0] if ready else "",
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
|
|
1696
|
+
def _start_provider_login(root: Path, provider: str) -> None:
|
|
1697
|
+
"""Open one explicit interactive login in Terminal; no credential crosses EP."""
|
|
1698
|
+
commands = {
|
|
1699
|
+
"CODEX": (CodexCliProvider()._executable, "login", "--device-auth"),
|
|
1700
|
+
"GITHUB": ("gh", "auth", "login", "--hostname", "github.com", "--web"),
|
|
1701
|
+
}
|
|
1702
|
+
command = commands.get(provider)
|
|
1703
|
+
if command is None:
|
|
1704
|
+
raise ValueError("Unsupported provider login request.")
|
|
1705
|
+
if provider == "CODEX" and not CodexCliProvider().status().qualified:
|
|
1706
|
+
raise ValueError("Codex CLI is not installed.")
|
|
1707
|
+
if provider == "GITHUB" and shutil.which("gh") is None:
|
|
1708
|
+
raise ValueError("GitHub CLI is not installed.")
|
|
1709
|
+
if sys.platform != "darwin":
|
|
1710
|
+
raise ValueError("Interactive provider login is supported from the local macOS dashboard only.")
|
|
1711
|
+
shell_command = "exec " + " ".join(shlex.quote(part) for part in command)
|
|
1712
|
+
# `do script` can create a background tab when Terminal is not frontmost.
|
|
1713
|
+
# Bring it forward first so the operator receives the interactive device or
|
|
1714
|
+
# browser login prompt instead of a silent, hidden dispatch.
|
|
1715
|
+
apple_script = "\n".join((
|
|
1716
|
+
'tell application "Terminal"',
|
|
1717
|
+
"activate",
|
|
1718
|
+
f"do script {json.dumps(shell_command)}",
|
|
1719
|
+
"end tell",
|
|
1720
|
+
))
|
|
1721
|
+
# Serialize only the AppleScript dispatch itself. A Terminal session can
|
|
1722
|
+
# be cancelled outside EP, so retaining a five-minute "active" lock made
|
|
1723
|
+
# legitimate retries and the other provider appear permanently broken.
|
|
1724
|
+
with _provider_login_lock:
|
|
1725
|
+
completed = LocalProcessProvider().execute(root, ("/usr/bin/osascript", "-e", apple_script))
|
|
1726
|
+
if completed.returncode:
|
|
1727
|
+
raise ValueError("Provider login window could not be opened.")
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
def _install_provider(root: Path, provider: str) -> None:
|
|
1731
|
+
"""Install a missing CLI only after an explicit dashboard request."""
|
|
1732
|
+
if not _provider_install_lock.acquire(blocking=False):
|
|
1733
|
+
raise ValueError("Another provider installation is already in progress.")
|
|
1734
|
+
try:
|
|
1735
|
+
if _execution_active(root):
|
|
1736
|
+
raise ValueError("Provider installation is unavailable while an execution is active.")
|
|
1737
|
+
if provider == "CODEX":
|
|
1738
|
+
try:
|
|
1739
|
+
managed_codex_runtime.provision(root)
|
|
1740
|
+
except managed_codex_runtime.ManagedCodexRuntimeError as error:
|
|
1741
|
+
raise ValueError(str(error)) from error
|
|
1742
|
+
key = "codex"
|
|
1743
|
+
elif provider == "GITHUB":
|
|
1744
|
+
brew = shutil.which("brew")
|
|
1745
|
+
if brew is None:
|
|
1746
|
+
raise ValueError("GitHub CLI installation requires Homebrew on this host.")
|
|
1747
|
+
completed = LocalProcessProvider().execute(root, (brew, "install", "gh"))
|
|
1748
|
+
verification = LocalProcessProvider().execute(root, ("gh", "--version"))
|
|
1749
|
+
key = "github"
|
|
1750
|
+
else:
|
|
1751
|
+
raise ValueError("Unsupported provider installation request.")
|
|
1752
|
+
if provider == "GITHUB" and (completed.returncode or verification.returncode):
|
|
1753
|
+
raise ValueError("Provider installation could not be verified.")
|
|
1754
|
+
if _provider_login_status(root).get(key, {}).get("state") == "UNAVAILABLE":
|
|
1755
|
+
raise ValueError("Provider installation could not be verified.")
|
|
1756
|
+
finally:
|
|
1757
|
+
_provider_install_lock.release()
|
|
1758
|
+
|
|
1759
|
+
|
|
1760
|
+
def _logout_provider(root: Path, provider: str) -> None:
|
|
1761
|
+
"""Remove one locally stored provider session; never expose credentials."""
|
|
1762
|
+
if provider == "CODEX":
|
|
1763
|
+
completed = CodexCliProvider().command("logout")
|
|
1764
|
+
elif provider == "GITHUB":
|
|
1765
|
+
process = LocalProcessProvider()
|
|
1766
|
+
account = process.execute(root, ("gh", "api", "user", "--jq", ".login"))
|
|
1767
|
+
username = account.stdout.strip()
|
|
1768
|
+
if account.returncode or not username or not re.fullmatch(r"[A-Za-z0-9-]+", username):
|
|
1769
|
+
raise ValueError("GitHub session cannot be safely identified for logout.")
|
|
1770
|
+
completed = process.execute(root, ("gh", "auth", "logout", "--hostname", "github.com", "--user", username))
|
|
1771
|
+
else:
|
|
1772
|
+
raise ValueError("Unsupported provider logout request.")
|
|
1773
|
+
if completed.returncode:
|
|
1774
|
+
raise ValueError("Provider logout did not complete.")
|
|
1775
|
+
|
|
1776
|
+
|
|
1777
|
+
def _workspace_git_projection(root: Path) -> dict[str, object]:
|
|
1778
|
+
"""Return the small, read-only Git projection shown in Workspace."""
|
|
1779
|
+
# The browser owns presentation. An empty value is rendered through its
|
|
1780
|
+
# five-locale unavailable label rather than leaking Server-localized copy.
|
|
1781
|
+
unavailable = ""
|
|
1782
|
+
try:
|
|
1783
|
+
provider = GitProvider()
|
|
1784
|
+
branch = provider.execute(root, "git", "branch", "--show-current")
|
|
1785
|
+
revisions = provider.execute(root, "git", "rev-parse", "HEAD", "origin/main")
|
|
1786
|
+
except OSError:
|
|
1787
|
+
return {
|
|
1788
|
+
"branch": unavailable,
|
|
1789
|
+
"commit": unavailable,
|
|
1790
|
+
"origin_main_commit": unavailable,
|
|
1791
|
+
"origin_main_available": False,
|
|
1792
|
+
"main_action_available": False,
|
|
1793
|
+
"branch_cleanup_available": False,
|
|
1794
|
+
}
|
|
1795
|
+
branch_name = branch.stdout.strip() if branch.returncode == 0 and branch.stdout.strip() else unavailable
|
|
1796
|
+
revision_lines = revisions.stdout.splitlines() if revisions.returncode == 0 else []
|
|
1797
|
+
commit = revision_lines[0][:12] if revision_lines else unavailable
|
|
1798
|
+
origin_main_commit = revision_lines[1][:12] if len(revision_lines) == 2 else unavailable
|
|
1799
|
+
origin_main_available = len(revision_lines) == 2
|
|
1800
|
+
return {
|
|
1801
|
+
"branch": branch_name,
|
|
1802
|
+
"commit": commit,
|
|
1803
|
+
"origin_main_commit": origin_main_commit,
|
|
1804
|
+
"origin_main_available": origin_main_available,
|
|
1805
|
+
"main_action_available": origin_main_available and commit != origin_main_commit,
|
|
1806
|
+
"branch_cleanup_available": branch_name == "main",
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
|
|
1810
|
+
def _workspace_worktrees(root: Path) -> dict[str, object]:
|
|
1811
|
+
"""Project local worktrees plus the protected main branch, read-only."""
|
|
1812
|
+
try:
|
|
1813
|
+
provider = GitProvider()
|
|
1814
|
+
observed = provider.execute(root, "git", "worktree", "list", "--porcelain")
|
|
1815
|
+
except OSError:
|
|
1816
|
+
return {"available": False, "worktrees": []}
|
|
1817
|
+
if observed.returncode != 0:
|
|
1818
|
+
return {"available": False, "worktrees": []}
|
|
1819
|
+
|
|
1820
|
+
worktrees: list[dict[str, object]] = []
|
|
1821
|
+
record: dict[str, str | bool] = {}
|
|
1822
|
+
for line in [*str(observed.stdout or "").splitlines(), ""]:
|
|
1823
|
+
if line:
|
|
1824
|
+
key, _, value = line.partition(" ")
|
|
1825
|
+
if key in {"worktree", "HEAD", "branch"}:
|
|
1826
|
+
record[key] = value
|
|
1827
|
+
elif key == "detached":
|
|
1828
|
+
record[key] = True
|
|
1829
|
+
continue
|
|
1830
|
+
path = str(record.get("worktree") or "").strip()
|
|
1831
|
+
if path:
|
|
1832
|
+
reference = str(record.get("branch") or "")
|
|
1833
|
+
try:
|
|
1834
|
+
active = Path(path).resolve() == root.resolve()
|
|
1835
|
+
except OSError:
|
|
1836
|
+
# A stale worktree path is diagnostic evidence only; never
|
|
1837
|
+
# present it as the active checkout.
|
|
1838
|
+
active = False
|
|
1839
|
+
worktrees.append({
|
|
1840
|
+
"path": path,
|
|
1841
|
+
"branch": reference.removeprefix("refs/heads/") or None,
|
|
1842
|
+
"commit": str(record.get("HEAD") or "")[:12] or None,
|
|
1843
|
+
"detached": bool(record.get("detached")),
|
|
1844
|
+
"active": active,
|
|
1845
|
+
})
|
|
1846
|
+
record = {}
|
|
1847
|
+
# `main` is the repository's stable baseline even when it is not currently
|
|
1848
|
+
# checked out in a worktree. The Workspace heading promises branches as
|
|
1849
|
+
# well as worktrees, so project it explicitly instead of hiding it.
|
|
1850
|
+
if not any(item.get("branch") == "main" for item in worktrees):
|
|
1851
|
+
try:
|
|
1852
|
+
main = provider.execute(root, "git", "rev-parse", "--verify", "refs/heads/main")
|
|
1853
|
+
except OSError:
|
|
1854
|
+
main = None
|
|
1855
|
+
if main is not None and main.returncode == 0 and main.stdout.strip():
|
|
1856
|
+
worktrees.append({
|
|
1857
|
+
"path": None,
|
|
1858
|
+
"branch": "main",
|
|
1859
|
+
"commit": main.stdout.strip()[:12],
|
|
1860
|
+
"detached": False,
|
|
1861
|
+
"checked_out": False,
|
|
1862
|
+
})
|
|
1863
|
+
# Stable sorting keeps Git's worktree order intact while pinning main as
|
|
1864
|
+
# the first, recognisable baseline entry.
|
|
1865
|
+
worktrees.sort(key=lambda item: item.get("branch") != "main")
|
|
1866
|
+
# The action itself performs the fresh, fail-closed Git verification.
|
|
1867
|
+
# Keep this projection read-only so periodic dashboard refreshes never
|
|
1868
|
+
# run Git checks inside every listed worktree.
|
|
1869
|
+
for item in worktrees:
|
|
1870
|
+
path = item.get("path")
|
|
1871
|
+
branch = item.get("branch")
|
|
1872
|
+
if isinstance(path, str) and isinstance(branch, str) and branch != "main":
|
|
1873
|
+
item["removable"] = True
|
|
1874
|
+
return {"available": True, "worktrees": worktrees}
|
|
1875
|
+
|
|
1876
|
+
|
|
1877
|
+
def render_console_document(
|
|
1878
|
+
title: str,
|
|
1879
|
+
build_commit: str = "onbekend",
|
|
1880
|
+
workspace_id: str = "onbekend",
|
|
1881
|
+
project_name: str = "Project",
|
|
1882
|
+
workspace_location: str = ".",
|
|
1883
|
+
workspace_free_disk_space: str = "",
|
|
1884
|
+
tracked_files: str = "",
|
|
1885
|
+
workspace_branch: str = "",
|
|
1886
|
+
workspace_commit: str = "",
|
|
1887
|
+
origin_main_commit: str = "",
|
|
1888
|
+
origin_main_available: bool = False,
|
|
1889
|
+
workspace_open_pull_requests: list[dict[str, object]] | None = None,
|
|
1890
|
+
workspace_main_action_hidden: bool = True,
|
|
1891
|
+
workspace_branch_cleanup_hidden: bool = True,
|
|
1892
|
+
platform_version: str = CURRENT_PLATFORM_VERSION,
|
|
1893
|
+
configuration_inbox: str = "",
|
|
1894
|
+
) -> bytes:
|
|
1895
|
+
"""Render the private dashboard with a server-pushed status stream."""
|
|
1896
|
+
page = r"""<!doctype html>
|
|
1897
|
+
<html lang="en">
|
|
1898
|
+
<head>
|
|
1899
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
|
|
1900
|
+
<meta id="dashboardThemeColor" name="theme-color" content="#15151d">
|
|
1901
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
1902
|
+
<meta id="dashboardAppleWebAppTitle" name="apple-mobile-web-app-title" content="$TITLE">
|
|
1903
|
+
<title>$TITLE</title>
|
|
1904
|
+
<link rel="manifest" href="/assets/operations-console/manifest.webmanifest">
|
|
1905
|
+
<link id="dashboardFavicon" rel="icon" type="image/png" sizes="180x180" href="/assets/operations-console/apple-touch-icon-dark.png?v=operations-console-2">
|
|
1906
|
+
<link id="dashboardAppleTouchIcon" rel="apple-touch-icon" sizes="180x180" href="/assets/operations-console/apple-touch-icon-dark.png?v=operations-console-2">
|
|
1907
|
+
<script>try{const state=JSON.parse(localStorage.getItem("engineering-dashboard-client-state-v1")||"{}");document.documentElement.dataset.theme=state.theme==="light"?"light":"dark"}catch{document.documentElement.dataset.theme="dark"}</script>
|
|
1908
|
+
|
|
1909
|
+
|
|
1910
|
+
<link rel="stylesheet" href="/assets/dashboard.css?build=$BUILD_COMMIT">
|
|
1911
|
+
</head>
|
|
1912
|
+
<body data-project-id="$WORKSPACE_ID" data-project-name="$PROJECT_NAME">
|
|
1913
|
+
<a class="skip-link" href="#engineering-dashboard-content" data-i18n="header.skip"></a>
|
|
1914
|
+
<div id="dashboardSplash" role="status" aria-live="polite" data-testid="dashboard-splash"><div class="dashboard-splash__content"><img class="dashboard-splash__icon" src="/assets/operations-console/icon-transparent.png" alt="" aria-hidden="true" data-testid="dashboard-splash-icon"><h2 class="dashboard-splash__title" id="dashboardSplashTitle" data-i18n="dashboard.title">$TITLE</h2><span class="dashboard-splash__version" id="dashboardSplashVersion" data-platform-version="$PLATFORM_VERSION">Engineering Platform $PLATFORM_VERSION</span><span class="dashboard-splash__spinner" aria-hidden="true"></span><span class="dashboard-splash__loading" id="dashboardSplashLoading" data-i18n="dashboard.loading"></span></div></div>
|
|
1915
|
+
<div id="copyToast" role="status" aria-live="polite" aria-atomic="true" popover="manual" hidden data-testid="copy-toast"></div>
|
|
1916
|
+
<div id="pullRefresh" role="status" aria-live="polite" aria-hidden="true" data-testid="pull-refresh" data-i18n="refresh.pull_to_refresh"></div>
|
|
1917
|
+
<div class="dashboard-scroll-region">
|
|
1918
|
+
<div class="dashboard-sticky-header">
|
|
1919
|
+
<header class="dashboard-titlebar"><div class="dashboard-titlebar__brand"><img class="dashboard-app-icon" src="/assets/operations-console/icon-transparent.png" alt="" aria-hidden="true" data-testid="dashboard-app-icon"><h1 id="dashboardTitle" data-i18n="dashboard.title">$TITLE</h1></div><div class="dashboard-titlebar__actions"><div class="dashboard-health" id="dashboardHealth"><button class="dashboard-health__button" id="dashboardHealthIndicator" type="button" aria-expanded="false" aria-controls="dashboardHealthTooltip" data-health-state="unknown" data-testid="dashboard-health-indicator"><span class="dashboard-health__dot" aria-hidden="true"></span><span class="sr-only" id="dashboardHealthAccessibleLabel"></span></button></div><section class="dashboard-health__tooltip" id="dashboardHealthTooltip" role="tooltip"><strong id="dashboardHealthTooltipTitle"></strong><ul id="dashboardHealthChecks"></ul></section><button class="page-refresh" id="pageRefresh" type="button" data-testid="page-refresh" data-i18n-title="refresh.page" data-i18n-aria-label="refresh.page"><span aria-hidden="true">↻</span></button><div class="dashboard-titlebar__options" id="dashboardTitlebarOptions"><button class="dashboard-titlebar__options-toggle" id="dashboardTitlebarOptionsToggle" type="button" aria-expanded="false" aria-controls="dashboardTitlebarOptionsContent" data-testid="titlebar-options-toggle"><span data-i18n="header.options"></span></button><div class="dashboard-titlebar__options-content" id="dashboardTitlebarOptionsContent"><label class="dashboard-locale" for="dashboardLocale"><span data-i18n="language.label"></span><select id="dashboardLocale" class="dashboard-locale__native" data-i18n-aria-label="language.label"><option value="en" data-i18n="language.en"></option><option value="nl" data-i18n="language.nl"></option><option value="de" data-i18n="language.de"></option><option value="fr" data-i18n="language.fr"></option><option value="es" data-i18n="language.es"></option></select><span class="dashboard-locale__picker"><button class="dashboard-locale__button" id="dashboardLocaleButton" type="button" aria-haspopup="listbox" aria-expanded="false" aria-controls="dashboardLocaleMenu"><span id="dashboardLocaleValue"></span><span aria-hidden="true">⌄</span></button><span class="dashboard-locale__menu" id="dashboardLocaleMenu" role="listbox" hidden><button type="button" role="option" data-dashboard-locale="en"></button><button type="button" role="option" data-dashboard-locale="nl"></button><button type="button" role="option" data-dashboard-locale="de"></button><button type="button" role="option" data-dashboard-locale="fr"></button><button type="button" role="option" data-dashboard-locale="es"></button></span></span></label><button class="theme-toggle" id="themeToggle" type="button" role="switch" aria-checked="false" data-i18n-aria-label="header.enable_light" data-testid="theme-toggle"><span class="theme-toggle__label" data-i18n="header.theme"></span></button><button class="section-state-toggle" id="toggleAllSections" type="button" role="switch" aria-checked="false" data-i18n-aria-label="header.open_all" data-testid="toggle-all-sections"><span class="section-state-toggle__label" data-i18n="header.expand"></span></button><label class="auto-refresh-toggle" for="autoRefresh"><input id="autoRefresh" type="checkbox" role="switch" checked><span data-i18n="header.auto_refresh"></span></label></div></div></div></header><aside class="dashboard-status-banner dashboard-status-banner--usage-limit" id="codexUsageLimitBanner" role="alert" aria-live="assertive" hidden data-testid="codex-usage-limit-banner"><strong data-i18n="notification.codex_usage_limit.title"></strong><span data-i18n="notification.codex_usage_limit.body"></span></aside>
|
|
1920
|
+
<aside class="dashboard-status-banner dashboard-status-banner--capacity-reserve" id="codexCapacityReserveBanner" role="alert" aria-live="assertive" hidden data-testid="codex-capacity-reserve-banner"><strong data-i18n="notification.codex_capacity_reserve.title"></strong><span id="codexCapacityReserveMessage"></span><a class="codex-capacity-reserve-banner__action" id="codexCapacityReserveAction" href="#rateLimits" data-i18n="notification.codex_capacity_reserve.action"></a></aside>
|
|
1921
|
+
<aside class="dashboard-status-banner dashboard-status-banner--github-rate-limit" id="githubRateLimitBanner" role="alert" aria-live="assertive" hidden data-testid="github-rate-limit-banner"><strong data-i18n="notification.github_rate_limit.title"></strong><span id="githubRateLimitMessage"></span><button class="github-rate-limit-banner__refresh" id="githubRateLimitRefresh" type="button" data-i18n-aria-label="notification.github_rate_limit.refresh" data-i18n-title="notification.github_rate_limit.refresh"><span aria-hidden="true">↻</span></button></aside>
|
|
1922
|
+
<details class="platform-attention" id="platformAttentionBanner" hidden data-testid="platform-attention-banner"><summary><strong id="platformAttentionTitle"></strong><span id="platformAttentionSummary"></span></summary><div class="platform-attention__items">
|
|
1923
|
+
<aside class="dashboard-status-banner dashboard-status-banner--provider-readiness" id="codexProviderReadinessBanner" role="alert" aria-live="assertive" hidden data-testid="codex-provider-readiness-banner"><strong id="codexProviderReadinessTitle"></strong><span id="codexProviderReadinessMessage"></span><button class="provider-readiness-banner__action" id="codexProviderReadinessAction" type="button" hidden></button></aside>
|
|
1924
|
+
<aside class="dashboard-status-banner dashboard-status-banner--provider-readiness" id="githubProviderReadinessBanner" role="alert" aria-live="assertive" hidden data-testid="github-provider-readiness-banner"><strong id="githubProviderReadinessTitle"></strong><span id="githubProviderReadinessMessage"></span><button class="provider-readiness-banner__action" id="githubProviderReadinessAction" type="button" hidden></button></aside>
|
|
1925
|
+
<aside class="dashboard-status-banner dashboard-status-banner--execution-runtime" id="executionRuntimeBanner" role="alert" aria-live="assertive" hidden data-testid="execution-runtime-banner"><strong id="executionRuntimeTitle"></strong><span id="executionRuntimeMessage"></span><button class="provider-readiness-banner__action" id="executionRuntimeRepair" type="button" hidden></button></aside>
|
|
1926
|
+
</div></details>
|
|
1927
|
+
</div>
|
|
1928
|
+
<main class="dashboard-grid" id="engineering-dashboard-content" tabindex="-1">
|
|
1929
|
+
<details class="inbox-queue" id="queueItems" data-testid="engineering-inbox-queue"><summary><strong data-i18n="section.inbox_queue"></strong></summary><p class="category-description" data-i18n="description.inbox_queue"></p><div class="queue-blocker" id="inboxBlocker" role="alert" hidden></div><p class="estimate-meta" id="queueSummary" data-i18n="logs.loading"></p><ol class="queue-list" id="queueList" aria-live="polite"></ol></details>
|
|
1930
|
+
<details class="prompt-history" id="promptHistory" data-testid="engineering-prompt-history"><summary><strong data-i18n="section.prompt_history"></strong></summary><p class="category-description" data-i18n="description.prompt_history"></p><div class="log-controls"><label for="promptHistoryFilter"><span data-i18n="filter.search"></span><input id="promptHistoryFilter" type="search" maxlength="160" data-sanitize="single-line" data-i18n-placeholder="filter.search_placeholder"></label></div><p class="history-scroll-hint" id="promptHistoryScrollHint" data-i18n="history.horizontal_scroll_hint"></p><div class="log-table-wrap" aria-describedby="promptHistoryScrollHint" role="region" tabindex="0"><table class="log-table" data-i18n-aria-label="history.table_label"><thead><tr><th data-history-sort-key="run_id" data-run-suffix="true" scope="col" data-i18n="table.run_suffix"></th><th data-history-sort-key="status" scope="col" data-i18n="table.status"></th><th data-history-sort-key="title" scope="col" data-i18n="table.prompt_title"></th><th data-history-sort-key="executed_at" scope="col" data-i18n="table.executed_at"></th><th scope="col" data-i18n="table.report"></th><th id="promptHistoryAnalysisHeader" scope="col" data-i18n="table.analysis"></th><th id="promptHistoryChatHeader" scope="col" data-i18n="table.chat"></th><th scope="col" data-i18n="table.action"></th><th id="promptHistoryDetailsHeader" scope="col" data-i18n="table.details"></th></tr></thead><tbody id="promptHistoryRows"><tr><td class="log-empty" colspan="9" data-i18n="logs.loading"></td></tr></tbody></table></div><nav class="log-pagination" id="promptHistoryPagination" data-i18n-aria-label="history.table_label"></nav></details>
|
|
1931
|
+
<details class="current-run" id="currentRun" data-i18n-aria-label="detail.execution" hidden><summary class="current-run__title"><span class="label" id="currentRunTitle" data-i18n="section.active_prompt"></span></summary><div class="current-run__grid"><div class="field"><span class="label" data-i18n="detail.prompt_title"></span><h2 id="currentPrompt" data-i18n="format.loading"></h2></div><div class="field"><span class="label" data-i18n="ui.filename"></span><pre id="currentFile" data-i18n="format.loading"></pre></div>
|
|
1932
|
+
<div class="card" id="executionIdentity"><strong id="executionIdentityTitle" data-i18n="detail.execution"></strong><p class="field"><span class="label" data-i18n="detail.run_id"></span><span id="runId"></span></p><p class="field" id="promptStartedField"><span class="label" data-i18n="ui.prompt_started"></span><span id="promptStarted" data-i18n="format.loading"></span></p></div>
|
|
1933
|
+
<div class="card"><div class="status"><span id="indicator" class="indicator" role="status" data-i18n-aria-label="status.unknown"></span><strong data-i18n="detail.prompt_status"></strong></div><p class="field"><span class="label" data-i18n="ui.execution_status"></span><span id="watcher" data-i18n="format.loading"></span></p><p class="field"><span class="label" data-i18n="ui.phase"></span><span id="phase" data-i18n="format.loading"></span></p><p class="field"><span class="label" data-i18n="ui.current_activity"></span><span id="action" data-i18n="format.loading"></span></p><p class="field" id="providerRecovery" hidden><span class="label" data-i18n="provider_recovery.label"></span><span id="providerRecoveryValue"></span></p></div>
|
|
1934
|
+
<div class="card execution-context" id="executionContext" hidden><strong data-i18n="ui.execution_context"></strong><p class="field"><span class="label" data-i18n="field.execution_mode"></span><span id="executionMode"></span></p><p class="field"><span class="label" data-i18n="field.repository"></span><span id="targetRepository"></span></p><div class="field"><span class="label" data-i18n="detail.target_checkout"></span><pre id="checkoutPath"></pre></div><p class="field"><span class="label" data-i18n="ui.active_branch"></span><span id="activeBranch"></span></p></div>
|
|
1935
|
+
<div class="card" id="processMetrics" hidden><strong data-i18n="ui.local_ai_processes"></strong><p class="field"><span class="label">CPU</span><span id="codexCpu" data-i18n="format.loading"></span></p><p class="field"><span class="label" data-i18n="ui.process_count"></span><span id="codexProcesses" data-i18n="format.loading"></span></p><p class="field"><span class="label" data-i18n="ui.gpu_usage"></span><span id="codexGpu" data-i18n="format.loading"></span></p></div>
|
|
1936
|
+
<div class="card operator-merge-wait" id="operatorMergeWait" hidden><strong id="operatorMergeWaitTitle" data-i18n="merge_wait.title"></strong><p id="operatorMergeWaitDescription"></p><p class="open-pr-status operator-merge-wait__pr-status" id="operatorMergeWaitPullRequestStatus"><span class="open-pr-status__dot" aria-hidden="true"></span><span class="open-pr-status__label"></span></p><p class="open-pr-approval" id="operatorMergeWaitOwnerApproval"></p><p class="estimate-meta" id="operatorMergeWaitLastCheck" hidden></p><div class="operator-merge-wait__actions"><a class="dashboard-action dashboard-action--primary" id="operatorMergePullRequest" target="_blank" rel="noopener noreferrer"></a><button class="dashboard-action" id="operatorMergeStatusCheck" type="button" data-i18n="merge_wait.check_status"></button><button class="dashboard-action dashboard-action--destructive" id="operatorMergeAbort" type="button" data-i18n="action.abort_execution"></button></div></div>
|
|
1937
|
+
<div class="card operator-merge-wait" id="emergencyRecovery" hidden><strong data-i18n="emergency_recovery.title"></strong><p data-i18n="emergency_recovery.description"></p><div class="operator-merge-wait__actions"><button class="dashboard-action dashboard-action--destructive" id="emergencyRecoveryStart" type="button" data-i18n="emergency_recovery.action"></button></div></div>
|
|
1938
|
+
<div class="card status-reconciliation-card" id="statusReconciliation" hidden><strong data-i18n="status_reconciliation.title"></strong><p data-i18n="status_reconciliation.description"></p><div class="operator-merge-wait__actions"><button class="dashboard-action dashboard-action--primary" id="statusReconciliationStart" type="button" data-i18n="status_reconciliation.action"></button></div><p id="statusReconciliationResult" role="status" aria-live="polite"></p></div>
|
|
1939
|
+
<div class="card" id="workspaceProgress"><strong data-i18n="detail.workspace_changes"></strong><p class="field"><span id="workspaceProgressValue" data-i18n="format.loading"></span></p></div>
|
|
1940
|
+
<div class="card" id="predecessorGate" hidden><strong data-i18n="status.blocked"></strong><p class="field"><span class="label" data-i18n="detail.run_id"></span><code id="predecessorRun"></code></p><p class="field"><span class="label" data-i18n="ui.preceding_prompt"></span><span id="predecessorPrompt"></span></p><p class="field"><span class="label" data-i18n="field.terminal_state"></span><span id="predecessorPhase"></span></p><div class="field"><span class="label" data-i18n="ui.recovery_action"></span><pre id="predecessorAction"></pre></div><button class="predecessor-retry" id="predecessorRetry" type="button" data-i18n="recovery.action"></button><p class="predecessor-retry-status" id="predecessorRetryStatus" role="status" aria-live="polite"></p></div>
|
|
1941
|
+
<div class="card"><strong data-i18n="ui.estimated_execution_time"></strong><p class="estimate-primary" id="executionEstimate" data-i18n="estimate.not_available"></p><p class="estimate-meta" id="executionEstimateMeta" hidden></p></div>
|
|
1942
|
+
<div class="card" id="usage" hidden><strong>Codex CLI</strong><div class="field"><span class="label" data-i18n="ui.reported_usage"></span><pre id="usageDetails"></pre></div></div>
|
|
1943
|
+
<div class="card" id="currentDiagnostic" hidden><strong>Codex CLI</strong><pre id="currentLog" data-i18n="format.loading"></pre></div>
|
|
1944
|
+
</div></details>
|
|
1945
|
+
<details class="card card--resource" id="rateLimits" hidden><summary><strong data-i18n="section.remaining_usage"></strong></summary><div class="field"><span class="label" data-i18n="ui.current_ai_provider"></span><span id="rateLimitProvider" data-i18n="format.loading"></span></div><p class="rate-limit-update-status" id="codexCliUpdateStatus" role="status" aria-live="polite"></p><button class="rate-limit-reset" id="codexCliUpdate" type="button" hidden data-i18n="ui.codex_cli_update"></button><div class="field rate-limit-provider-path"><span class="label" data-i18n="ui.installation_path"></span><code id="rateLimitProviderPath" data-i18n="format.not_available"></code></div><div class="field"><span class="label" id="rateLimitLabel">Codex CLI</span><pre id="rateLimitDetails"></pre></div><button class="rate-limit-reset" id="rateLimitReset" type="button" hidden data-i18n="ui.reset_ready"></button><p class="rate-limit-reset-status" id="rateLimitResetStatus" role="status" aria-live="polite"></p></details>
|
|
1946
|
+
<details class="platform-health" id="platformHealth" data-testid="platform-health"><summary><strong data-i18n="section.platform_components"></strong></summary><p class="category-description" data-i18n="description.platform_components"></p><div class="platform-health__components" id="platformHealthComponents" aria-live="polite"><p class="platform-health__empty" data-i18n="ui.component_health_loading"></p></div></details>
|
|
1947
|
+
<dialog class="dashboard-modal-shell dashboard-modal-shell--component component-modal" id="componentModal" aria-labelledby="componentModalTitle"><section class="dashboard-modal-shell__panel component-modal__panel"><header class="dashboard-modal-shell__header component-modal__header"><h2 id="componentModalTitle" data-i18n="ui.component_information"></h2><button class="dashboard-modal-shell__close component-modal__close" id="componentModalClose" type="button" data-i18n-aria-label="sections.close">×</button></header><div id="componentModalContent"></div><div class="component-modal__actions" id="componentModalActions"><button class="component-modal__restart" id="componentModalRestart" type="button" hidden data-i18n="ui.component_restart"></button></div><p class="component-modal__status" id="componentModalStatus" aria-live="polite"></p></section></dialog>
|
|
1948
|
+
<dialog class="dashboard-modal-shell dashboard-modal-shell--evidence telemetry-detail-modal" id="telemetryDetailModal" aria-labelledby="telemetryDetailTitle"><section class="dashboard-modal-shell__panel telemetry-detail-modal__panel"><header class="dashboard-modal-shell__header"><h2 id="telemetryDetailTitle"></h2><div class="prompt-detail-modal__actions"><button class="dashboard-action dashboard-action--download prompt-detail-download" id="telemetryDetailDownloadMarkdown" type="button" hidden>↓</button><button class="dashboard-action dashboard-action--download prompt-detail-download" id="telemetryDetailDownloadJson" type="button" hidden>{}</button><button class="dashboard-modal-shell__close" id="telemetryDetailClose" type="button" data-i18n-aria-label="sections.close">×</button></div></header><p id="telemetryDetailDescription" class="prompt-detail-modal__description"></p><div id="telemetryDetailContent" class="telemetry-detail-modal__content" aria-live="polite"></div></section></dialog>
|
|
1949
|
+
<dialog class="dashboard-modal-shell dashboard-modal-shell--evidence lifecycle-detail-modal" id="lifecycleDetailModal" aria-labelledby="lifecycleDetailTitle"><section class="dashboard-modal-shell__panel lifecycle-detail-modal__panel"><header class="dashboard-modal-shell__header"><h2 id="lifecycleDetailTitle"></h2><button class="dashboard-modal-shell__close" id="lifecycleDetailClose" type="button" data-i18n-aria-label="sections.close">×</button></header><div id="lifecycleDetailContent" class="lifecycle-detail-modal__content" aria-live="polite"></div></section></dialog>
|
|
1950
|
+
<dialog class="dashboard-modal-shell dashboard-modal-shell--evidence execution-mode-modal" id="executionModeModal" aria-labelledby="executionModeModalTitle"><section class="dashboard-modal-shell__panel execution-mode-modal__panel"><header class="dashboard-modal-shell__header execution-mode-modal__header"><h2 id="executionModeModalTitle" data-i18n="execution_mode_info.title"></h2><button class="dashboard-modal-shell__close execution-mode-modal__close" id="executionModeModalClose" type="button" data-i18n-aria-label="sections.close">×</button></header><div class="execution-mode-modal__content"><p data-i18n="execution_mode_info.intro"></p><section class="execution-mode-modal__definition"><h3 data-i18n="execution_mode_info.managed_title"></h3><p data-i18n="execution_mode_info.managed_body"></p></section><section class="execution-mode-modal__definition"><h3 data-i18n="execution_mode_info.genesis_title"></h3><p data-i18n="execution_mode_info.genesis_body"></p></section></div></section></dialog>
|
|
1951
|
+
<dialog class="dashboard-modal-shell dashboard-modal-shell--evidence confirmation-modal" id="operatorMergeWaitModal" aria-labelledby="operatorMergeWaitModalTitle"><section class="dashboard-modal-shell__panel confirmation-modal__panel"><header class="dashboard-modal-shell__header confirmation-modal__header"><h2 id="operatorMergeWaitModalTitle" data-i18n="merge_wait.title"></h2><button class="dashboard-modal-shell__close" id="operatorMergeWaitModalClose" type="button" data-i18n-aria-label="sections.close">×</button></header><p id="operatorMergeWaitModalDescription"></p><p class="estimate-meta" id="operatorMergeWaitModalLastCheck" hidden></p><section class="merge-wait-context" data-i18n-aria-label="merge_wait.context_label"><p id="operatorMergeWaitModalContextIntro"></p><dl><div><dt data-i18n="merge_wait.context_run"></dt><dd id="operatorMergeWaitModalRunId"></dd></div><div><dt data-i18n="merge_wait.context_prompt"></dt><dd id="operatorMergeWaitModalPrompt"></dd></div><div><dt data-i18n="merge_wait.pull_request_status"></dt><dd><span class="open-pr-status" id="operatorMergeWaitModalPullRequestStatus"><span class="open-pr-status__dot" aria-hidden="true"></span><span class="open-pr-status__label"></span></span></dd></div><div><dt data-i18n="merge_wait.owner_approval"></dt><dd><span class="open-pr-approval" id="operatorMergeWaitModalOwnerApproval"></span></dd></div></dl></section><div class="dashboard-modal-shell__actions confirmation-modal__actions"><button class="dashboard-modal-shell__action" id="operatorMergeWaitModalStatusCheck" type="button" data-i18n="merge_wait.check_status"></button><button class="dashboard-modal-shell__action dashboard-modal-shell__action--destructive" id="operatorMergeWaitModalAbort" type="button" data-i18n="action.abort_execution"></button><a class="dashboard-modal-shell__action dashboard-modal-shell__action--primary" id="operatorMergeWaitModalPullRequest" target="_blank" rel="noopener noreferrer"></a></div></section></dialog>
|
|
1952
|
+
<dialog class="dashboard-modal-shell dashboard-modal-shell--confirmation confirmation-modal" id="confirmationModal" aria-labelledby="confirmationModalTitle"><section class="dashboard-modal-shell__panel confirmation-modal__panel"><header class="dashboard-modal-shell__header confirmation-modal__header"><h2 id="confirmationModalTitle" data-i18n="ui.confirm_action"></h2><button class="dashboard-modal-shell__close confirmation-modal__close" id="confirmationModalClose" type="button" data-i18n-aria-label="sections.close">×</button></header><div id="confirmationModalText"></div><div class="dashboard-modal-shell__actions confirmation-modal__actions"><button class="dashboard-modal-shell__action" id="confirmationModalCancel" type="button" data-i18n="action.cancel"></button><button class="dashboard-modal-shell__action dashboard-modal-shell__action--primary" id="confirmationModalConfirm" type="button" data-i18n="action.confirm"></button></div></section></dialog>
|
|
1953
|
+
<dialog class="dashboard-modal-shell dashboard-modal-shell--confirmation confirmation-modal" id="dashboardErrorModal" aria-labelledby="dashboardErrorModalTitle"><section class="dashboard-modal-shell__panel confirmation-modal__panel"><header class="dashboard-modal-shell__header confirmation-modal__header"><h2 id="dashboardErrorModalTitle" data-i18n="ui.action_failed"></h2><button class="dashboard-modal-shell__close confirmation-modal__close" id="dashboardErrorModalClose" type="button" data-i18n-aria-label="action.close">×</button></header><p id="dashboardErrorModalText" aria-live="assertive"></p><div class="dashboard-modal-shell__actions confirmation-modal__actions"><button class="dashboard-modal-shell__action" id="dashboardErrorModalRecover" type="button" hidden></button><button class="dashboard-modal-shell__action dashboard-modal-shell__action--primary" id="dashboardErrorModalDismiss" type="button" data-i18n="action.close"></button></div></section></dialog>
|
|
1954
|
+
<dialog class="dashboard-modal-shell dashboard-modal-shell--confirmation confirmation-modal" id="workspaceBranchMainResultModal" aria-labelledby="workspaceBranchMainResultTitle"><section class="dashboard-modal-shell__panel confirmation-modal__panel"><header class="dashboard-modal-shell__header confirmation-modal__header"><h2 id="workspaceBranchMainResultTitle" data-i18n="workspace.branch_main_result_title"></h2><button class="dashboard-modal-shell__close confirmation-modal__close" id="workspaceBranchMainResultClose" type="button" data-i18n-aria-label="action.close">×</button></header><div id="workspaceBranchMainResultContent" aria-live="polite"></div><div class="dashboard-modal-shell__actions confirmation-modal__actions"><button class="dashboard-modal-shell__action dashboard-modal-shell__action--primary" id="workspaceBranchMainResultDismiss" type="button" data-i18n="action.close"></button></div></section></dialog>
|
|
1955
|
+
<dialog class="dashboard-modal-shell dashboard-modal-shell--evidence report-view-modal" id="promptHistoryReportModal" aria-labelledby="promptHistoryReportModalTitle"><section class="dashboard-modal-shell__panel report-view-modal__panel"><header class="dashboard-modal-shell__header report-view-modal__header"><h2 class="report-view-modal__title" id="promptHistoryReportModalTitle" data-modal-glyph="report" data-i18n="history.report_title"></h2><div class="report-view-modal__actions"><button class="dashboard-action dashboard-action--primary report-analysis-retry" id="promptHistoryReportRetry" type="button" hidden data-i18n="history.retry_analysis">↻</button><button class="dashboard-action dashboard-action--download download download--glyph" id="promptHistoryReportDownload" type="button" hidden>⇩</button><button class="dashboard-action dashboard-action--copy copy copy--glyph" id="promptHistoryReportCopy" type="button" hidden>⧉</button><button class="dashboard-modal-shell__close report-view-modal__close" id="promptHistoryReportClose" type="button" data-i18n-aria-label="sections.close">×</button></div></header><article class="markdown-document report-view-modal__content" id="promptHistoryReportContent" data-i18n="history.report_loading"></article></section></dialog>
|
|
1956
|
+
<dialog class="dashboard-modal-shell dashboard-modal-shell--evidence prompt-detail-modal" id="promptHistoryDetailModal" aria-labelledby="promptHistoryDetailTitle"><section class="dashboard-modal-shell__panel prompt-detail-modal__panel"><header class="dashboard-modal-shell__header prompt-detail-modal__header"><h2 id="promptHistoryDetailTitle" data-i18n="history.details_loading"></h2><div class="prompt-detail-modal__actions"><button class="dashboard-action dashboard-action--download prompt-detail-download" id="promptHistoryDetailDownloadMarkdown" type="button" hidden>↓</button><button class="dashboard-action dashboard-action--download prompt-detail-download" id="promptHistoryDetailDownloadJson" type="button" hidden>{}</button><button class="dashboard-modal-shell__close prompt-detail-modal__close" id="promptHistoryDetailClose" type="button" data-i18n-aria-label="sections.close">×</button></div></header><p class="prompt-detail-modal__description" id="promptHistoryDetailDescription"></p><div class="prompt-detail-modal__content" id="promptHistoryDetailContent" data-i18n="history.details_loading"></div></section></dialog>
|
|
1957
|
+
<dialog class="dashboard-modal-shell dashboard-modal-shell--chat prompt-chat-modal" id="promptHistoryChatModal" aria-labelledby="promptHistoryChatTitle"><section class="dashboard-modal-shell__panel prompt-chat-modal__panel"><header class="dashboard-modal-shell__header prompt-chat-modal__header"><h2 id="promptHistoryChatTitle" data-i18n="section.ai_conversation"></h2><div class="prompt-detail-modal__actions prompt-chat-modal__actions"><button class="dashboard-action dashboard-action--download download download--glyph" id="downloadChat" type="button" hidden>⇩</button><button class="dashboard-action dashboard-action--copy" id="copyChat" type="button" hidden data-i18n-title="chat.copy_title" data-i18n-aria-label="chat.copy_title">⧉</button><button class="dashboard-action dashboard-action--destructive" id="clearChat" type="button" hidden>⌫</button><button class="dashboard-modal-shell__close prompt-chat-modal__close" id="promptHistoryChatClose" type="button" data-i18n-aria-label="sections.close">×</button></div></header><p class="prompt-chat-modal__description" id="promptHistoryChatDescription"></p><section class="codex-chat" id="codexChat"><div class="codex-chat__details"><div class="chat-messages" id="chatMessages" aria-live="polite" data-i18n-aria-label="section.ai_conversation"></div><label class="label chat-question-label" for="chatInput" data-i18n="section.new_ai_question"></label><div class="chat-compose"><textarea id="chatInput" class="chat-input" rows="5" maxlength="2000" autocomplete="off" data-sanitize="multiline" data-i18n-placeholder="history.chat_placeholder"></textarea><button class="chat-send" id="chatSend" type="button" data-i18n-aria-label="action.confirm"><span aria-hidden="true">➤</span></button></div><div class="chat-meta"><p class="field"><span class="label" data-i18n="detail.model"></span><span id="chatModel">$CHAT_MODEL</span></p><p class="chat-status" id="chatStatus"></p></div></div></section></section></dialog>
|
|
1958
|
+
<button id="loadComponentLogs" type="button" hidden data-i18n="logs.loading"></button>
|
|
1959
|
+
<details class="technical-details" id="componentLogs"><summary><strong data-i18n="section.logs"></strong></summary><p class="estimate-meta" data-i18n="description.logs"></p><div class="log-controls" id="componentLogControls" hidden><label for="logFilter"><span data-i18n="filter.search"></span><input id="logFilter" type="search" maxlength="160" data-sanitize="single-line" data-i18n-placeholder="filter.search_placeholder"></label><label for="logLevelFilter"><span data-i18n="filter.level"></span><select id="logLevelFilter"><option value="" data-i18n="filter.all_levels"></option><option value="ERROR" data-i18n="filter.error"></option><option value="WARNING" data-i18n="filter.warning"></option><option value="INFO" data-i18n="filter.info"></option><option value="DEBUG" data-i18n="filter.debug"></option></select></label><label for="logTimePreset"><span data-i18n="filter.time_period"></span><select id="logTimePreset"><option value="" data-i18n="filter.all_time"></option><option value="today" data-i18n="filter.today"></option><option value="yesterday" data-i18n="filter.yesterday"></option><option value="day" data-i18n="filter.specific_day"></option><option value="range" data-i18n="filter.custom_range"></option></select></label><label for="logSpecificDate" id="logSpecificDateControl" hidden><span data-i18n="filter.specific_day"></span><input id="logSpecificDate" type="date"></label><label for="logDateFrom" id="logDateFromControl" hidden><span data-i18n="filter.from"></span><input id="logDateFrom" type="datetime-local"></label><label for="logDateTo" id="logDateToControl" hidden><span data-i18n="filter.to"></span><input id="logDateTo" type="datetime-local"></label></div><div class="technical-grid"><div class="card"><div class="log-card-header"><strong data-i18n="section.logs"></strong><div class="log-card-actions"><button class="dashboard-action dashboard-action--download download download--glyph component-log-download" data-component="platform" data-testid="download-platform-log" type="button" data-i18n-title="logs.download_platform" data-i18n-aria-label="logs.download_platform">⇩</button><button class="dashboard-action dashboard-action--destructive clear-component-log" data-component="platform" data-testid="clear-platform-log" type="button" data-i18n-title="action.clear_logs" data-i18n-aria-label="action.clear_logs">⌫</button></div></div><div class="log-table-wrap"><table class="log-table" data-i18n-aria-label="logs.platform_entries"><thead><tr><th data-i18n="table.number"></th><th data-i18n="table.timestamp"></th><th data-i18n="table.level"></th><th data-i18n="table.event"></th><th data-i18n="table.run_id"></th><th data-i18n="table.details"></th></tr></thead><tbody id="platformComponentLog"><tr><td class="log-empty" colspan="6" data-i18n="logs.loading"></td></tr></tbody></table></div><nav class="log-pagination" id="platformLogPagination" data-i18n-aria-label="section.logs"></nav></div></div></details>
|
|
1960
|
+
<details class="technical-details" id="technicalDetails" hidden><summary><strong data-i18n="section.technical_details"></strong></summary><p class="category-description" id="technicalDetailsDescription" data-i18n="description.technical_details"></p><p class="technical-diagnosis-summary" id="technicalHealthySummary" hidden></p><div class="technical-grid" id="technicalDiagnosisDetails">
|
|
1961
|
+
<div class="card"><strong id="technicalRepositoryTitle" data-i18n="technical.repository"></strong><p class="field"><span class="label" id="technicalRepositoryStateLabel" data-i18n="technical.repository_status"></span><span id="repositoryState"></span></p><p class="field"><span class="label technical-repository-label" id="technicalWorkspaceStateLabel"><span data-i18n="technical.workspace_status"></span><span class="technical-info" id="technicalWorkspaceStateInfo" role="img" tabindex="0" data-i18n-title="technical.workspace_status_help" data-i18n-aria-label="technical.workspace_status_help">i</span></span><span id="workspaceState"></span></p><div class="technical-git-lock" id="technicalGitLock"><p class="field"><span class="label technical-repository-label" id="technicalGitLockLabel"><span data-i18n="technical.git_lock"></span><span class="technical-info" id="technicalGitLockInfo" role="img" tabindex="0" data-i18n-title="technical.git_lock_help" data-i18n-aria-label="technical.git_lock_help">i</span></span><span id="technicalGitLockState" data-i18n="format.loading"></span></p><p class="technical-git-lock__detail" id="technicalGitLockDetail" hidden></p><button class="queue-blocker__repair" id="technicalGitLockRecover" type="button" hidden data-i18n="technical.git_lock_recovery_action"></button><p class="technical-git-lock__status" id="technicalGitLockRecoveryStatus" role="status" aria-live="polite"></p></div></div>
|
|
1962
|
+
<div class="card"><strong id="technicalHostPreflightTitle" data-i18n="technical.host_preflight"></strong><p class="field"><span class="label" id="technicalExecutionHostLabel" data-i18n="technical.execution_host"></span><span id="executionHostName" data-i18n="format.unavailable"></span></p><p class="field"><span class="label" id="technicalExecutionHostVersionLabel" data-i18n="technical.execution_host_version"></span><span id="executionHostVersion" data-i18n="format.unavailable"></span></p><p class="field"><span class="label" id="technicalRuntimeLabel" data-i18n="technical.runtime"></span><span id="executionHostRuntime" data-i18n="format.unavailable"></span></p><p class="field"><span class="label" id="technicalRuntimeVersionLabel" data-i18n="detail.codex_cli_version"></span><span id="executionHostRuntimeVersion" data-i18n="format.unavailable"></span></p><p class="field"><span class="label" id="technicalRuntimePathLabel" data-i18n="detail.codex_cli_installation_path"></span><code id="executionHostRuntimePath" data-i18n="format.unavailable"></code></p><p class="field"><span class="label" id="technicalRuntimePromptTransportLabel" data-i18n="technical.runtime_prompt_transport"></span><span id="executionHostTransport" data-i18n="format.unavailable"></span></p><p class="field"><span class="label" id="technicalHostStatusLabel" data-i18n="technical.host_status"></span><span id="hostPreflightStatus" data-i18n="format.unavailable"></span></p><p class="field"><span class="label" id="technicalLastCheckLabel" data-i18n="technical.last_check"></span><span id="hostPreflightTimestamp" data-i18n="format.unavailable"></span></p><p class="field"><span class="label" id="technicalWorkspacePreflightStatusLabel" data-i18n="technical.workspace_status"></span><span id="workspacePreflightStatus" data-i18n="format.unavailable"></span></p><p class="field"><span class="label" id="technicalLastWorkspaceCheckLabel" data-i18n="technical.last_workspace_check"></span><span id="workspacePreflightTimestamp" data-i18n="format.unavailable"></span></p><p class="field"><span class="label" id="technicalCapabilityStatusLabel" data-i18n="technical.capability_status"></span><span id="capabilityPreflightStatus" data-i18n="format.unavailable"></span></p><p class="field"><span class="label" id="technicalRecoverabilityLabel" data-i18n="technical.recoverability"></span><span id="capabilityRecoverability" data-i18n="format.unavailable"></span></p><p class="field"><span class="label" id="technicalFailureOriginLabel" data-i18n="technical.failure_origin"></span><span id="capabilityFailureOrigin" data-i18n="format.unavailable"></span></p><p class="field"><span class="label" id="technicalRecommendationLabel" data-i18n="technical.recommended_action"></span><span id="capabilityRecommendation" data-i18n="format.unavailable"></span></p></div>
|
|
1963
|
+
<div class="card" id="driftDiagnosticsCard" hidden><strong data-i18n="technical.current_drift"></strong><p class="field"><span class="label" data-i18n="technical.severity"></span><span id="driftSeverity"></span></p><p class="field"><span class="label" data-i18n="technical.affected_component"></span><span id="driftComponent"></span></p><p class="field"><span class="label" data-i18n="technical.expected_state"></span><span id="driftExpected"></span></p><p class="field"><span class="label" data-i18n="technical.observed_state"></span><span id="driftObserved"></span></p><p class="field"><span class="label" data-i18n="technical.resolution"></span><span id="driftResolution"></span></p></div>
|
|
1964
|
+
<div class="card" id="technicalDiagnosticsCard"><strong id="technicalDiagnosticsTitle" data-i18n="technical.diagnostics"></strong><p id="diag"></p></div>
|
|
1965
|
+
</div></details>
|
|
1966
|
+
<details class="card card--context workspace-card" id="workspaceCard" data-testid="engineering-workspace"><summary><strong data-i18n="section.workspace"></strong></summary><p class="field"><span class="label" data-workspace-label="workspace.name" data-i18n="workspace.name"></span><span>$WORKSPACE_ID</span></p><div class="field"><span class="label" data-workspace-label="ui.workspace_location" data-i18n="ui.workspace_location"></span><pre>$WORKSPACE_LOCATION</pre></div><p class="field" id="workspaceFreeDiskSpace"><span class="label" data-workspace-label="workspace.free_disk_space" data-i18n="workspace.free_disk_space"></span><span>$WORKSPACE_FREE_DISK_SPACE</span></p><p class="field"><span class="label" data-workspace-label="detail.tracked_files" data-i18n="detail.tracked_files"></span><span>$TRACKED_FILES</span></p><p class="field"><span class="label" data-workspace-label="workspace.current_branch" data-i18n="workspace.current_branch"></span><code id="workspaceBranch">$WORKSPACE_BRANCH</code></p><p class="field"><span class="label" data-workspace-label="workspace.current_commit" data-i18n="workspace.current_commit"></span><code id="workspaceCommit">$WORKSPACE_COMMIT</code></p><p class="field" id="workspaceOriginMain" $ORIGIN_MAIN_HIDDEN><span class="label" data-workspace-label="workspace.origin_main_commit" data-i18n="workspace.origin_main_commit"></span><code id="workspaceOriginMainCommit">$ORIGIN_MAIN_COMMIT</code></p>$WORKSPACE_OPEN_PULL_REQUESTS<div class="workspace-branch-actions"><button class="workspace-branch-main" id="workspaceBranchMain" type="button" $WORKSPACE_MAIN_ACTION_HIDDEN data-i18n="workspace.branch_main_action"></button></div></details>
|
|
1967
|
+
<details class="card card--context workspace-card configuration-card" id="configuration" data-testid="dashboard-configuration"><summary><strong data-i18n="section.configuration"></strong></summary><p class="category-description" data-i18n="description.configuration"></p><section class="configuration-server-settings" id="configurationServerSettings" aria-labelledby="configurationServerSettingsTitle"><h2 id="configurationServerSettingsTitle" data-i18n="configuration.server_settings"></h2><div class="configuration-controls"><label for="configurationInboxScanInterval"><span data-i18n="configuration.inbox_scan_interval"></span><select id="configurationInboxScanInterval"><option value="5" data-i18n="configuration.seconds_5"></option><option value="15" data-i18n="configuration.seconds_15"></option><option value="30" data-i18n="configuration.seconds_30"></option><option value="60" data-i18n="configuration.seconds_60"></option></select></label><label for="configurationOpenPrInterval"><span data-i18n="configuration.open_pr_interval"></span><select id="configurationOpenPrInterval"><option value="30" data-i18n="configuration.seconds_30"></option><option value="60" data-i18n="configuration.seconds_60"></option></select></label></div></section><div class="configuration-controls"><label for="configurationLogRetention"><span data-i18n="configuration.log_retention"></span><select id="configurationLogRetention"><option value="30"></option><option value="60"></option><option value="90"></option><option value="120"></option><option value="180"></option><option value="360"></option></select></label><label for="configurationLogLevel"><span data-i18n="configuration.log_level"></span><select id="configurationLogLevel"><option value="INFO" data-i18n="filter.info"></option><option value="DEBUG" data-i18n="filter.debug"></option></select></label><label for="configurationDashboardStreamInterval"><span data-i18n="configuration.dashboard_stream_interval"></span><select id="configurationDashboardStreamInterval"><option value="1"></option><option value="2"></option><option value="3"></option><option value="4"></option><option value="5"></option><option value="6"></option><option value="7"></option><option value="8"></option><option value="9"></option><option value="10"></option></select></label><label for="configurationPlatformHealthInterval"><span data-i18n="configuration.platform_health_interval"></span><select id="configurationPlatformHealthInterval"><option value="5" data-i18n="configuration.seconds_5"></option><option value="15" data-i18n="configuration.seconds_15"></option><option value="30" data-i18n="configuration.seconds_30"></option><option value="60" data-i18n="configuration.seconds_60"></option></select></label><label for="configurationComponentDetailsInterval"><span data-i18n="configuration.component_details_interval"></span><select id="configurationComponentDetailsInterval"><option value="5" data-i18n="configuration.seconds_5"></option><option value="15" data-i18n="configuration.seconds_15"></option><option value="30" data-i18n="configuration.seconds_30"></option><option value="60" data-i18n="configuration.seconds_60"></option></select></label><p id="configurationStatus" role="status" aria-live="polite"></p></div><section class="configuration-readonly-settings" aria-labelledby="configurationReadonlySettingsTitle"><h2 id="configurationReadonlySettingsTitle" data-i18n="configuration.readonly_platform_settings"></h2><p class="field configuration-field"><span class="label"><span data-i18n="configuration.operator_merge_interval"></span><span class="configuration-info" role="img" tabindex="0" data-i18n-title="configuration.operator_merge_interval_help" data-i18n-aria-label="configuration.operator_merge_interval_help">i</span></span><span data-i18n="configuration.seconds_60"></span></p><p class="field configuration-field"><span class="label"><span data-i18n="configuration.required_checks_interval"></span><span class="configuration-info" role="img" tabindex="0" data-i18n-title="configuration.required_checks_interval_help" data-i18n-aria-label="configuration.required_checks_interval_help">i</span></span><span data-i18n="configuration.seconds_15"></span></p><p class="field configuration-field"><span class="label"><span data-i18n="configuration.lease_heartbeat_interval"></span><span class="configuration-info" role="img" tabindex="0" data-i18n-title="configuration.lease_heartbeat_interval_help" data-i18n-aria-label="configuration.lease_heartbeat_interval_help">i</span></span><span data-i18n="configuration.seconds_15"></span></p><p class="field configuration-field"><span class="label"><span data-i18n="configuration.lease_timeout"></span><span class="configuration-info" role="img" tabindex="0" data-i18n-title="configuration.lease_timeout_help" data-i18n-aria-label="configuration.lease_timeout_help">i</span></span><span data-i18n="configuration.seconds_90"></span></p><p class="field configuration-field"><span class="label"><span data-i18n="configuration.github_retry_backoff"></span><span class="configuration-info" role="img" tabindex="0" data-i18n-title="configuration.github_retry_backoff_help" data-i18n-aria-label="configuration.github_retry_backoff_help">i</span></span><span data-i18n="configuration.github_retry_backoff_value"></span></p></section><section class="configuration-timeout-policy" aria-labelledby="configurationTimeoutPolicyTitle"><h2 id="configurationTimeoutPolicyTitle" data-i18n="configuration.timeout_policy"></h2><p data-i18n="configuration.timeout_policy_description"></p><p class="field configuration-field"><span class="label" data-i18n="configuration.timeout.specialist_review"></span><span data-i18n="configuration.minutes_5"></span></p><p class="field configuration-field"><span class="label" data-i18n="configuration.timeout.implementation"></span><span data-i18n="configuration.minutes_15"></span></p><p class="field configuration-field"><span class="label" data-i18n="configuration.timeout.local_repository_validation"></span><span data-i18n="configuration.minutes_15"></span></p><p class="field configuration-field"><span class="label" data-i18n="configuration.timeout.autonomous_quality_control"></span><span data-i18n="configuration.minutes_10"></span></p><p class="field configuration-field"><span class="label" data-i18n="configuration.timeout.repair"></span><span data-i18n="configuration.minutes_15"></span></p><p class="field configuration-field"><span class="label" data-i18n="configuration.timeout.finalization"></span><span data-i18n="configuration.minutes_15"></span></p><p class="field configuration-field"><span class="label" data-i18n="configuration.timeout.end_reconciliation"></span><span data-i18n="configuration.minutes_10"></span></p></section></details>
|
|
1968
|
+
</main></div>
|
|
1969
|
+
<footer class="footer" aria-live="polite"><span class="footer__item"><span class="label" id="platformVersionLabel" data-i18n="footer.platform_name"></span> <span class="footer__version-word" id="platformVersionWord" data-i18n="footer.version_word"></span> <span id="platformVersion" data-i18n="format.loading"></span></span><span class="footer__separator" aria-hidden="true">·</span><span class="footer__item" id="lastRefresh" data-i18n="format.loading"></span><span class="footer__separator" aria-hidden="true">·</span><span class="footer__item" id="updateMode" data-i18n="format.loading"></span></footer><span id="dashboardVersion" hidden></span><span id="workerVersion" hidden></span>
|
|
1970
|
+
<script>window.ENGINEERING_PLATFORM_DASHBOARD_BUILD="$BUILD_COMMIT";</script>
|
|
1971
|
+
<script src="/assets/dashboard.js?build=$BUILD_COMMIT" type="module"></script>
|
|
1972
|
+
|
|
1973
|
+
</body>
|
|
1974
|
+
</html>"""
|
|
1975
|
+
def open_pull_request_item(pull_request: dict[str, object]) -> str:
|
|
1976
|
+
authorization = (
|
|
1977
|
+
f'<button class="open-pr-owner-authorization" '
|
|
1978
|
+
f'data-open-pull-request-owner-authorization="{pull_request["number"]}" '
|
|
1979
|
+
f'type="button" data-i18n="workspace.open_pull_request.authorize_owner"></button>'
|
|
1980
|
+
if pull_request.get("owner_authorization_requested") is True else ""
|
|
1981
|
+
)
|
|
1982
|
+
repair = (
|
|
1983
|
+
f'<button class="open-pr-check-repair" '
|
|
1984
|
+
f'data-open-pull-request-check-repair="{pull_request["number"]}" '
|
|
1985
|
+
f'data-open-pull-request-failed-checks="{escape(json.dumps(pull_request.get("failed_checks", [])), quote=True)}" '
|
|
1986
|
+
f'type="button" data-i18n="workspace.open_pull_request.repair_failed_checks"></button>'
|
|
1987
|
+
if pull_request.get("check_repair_available") is True else ""
|
|
1988
|
+
)
|
|
1989
|
+
return (
|
|
1990
|
+
f'<li data-open-pull-request="{pull_request["number"]}"><a href="{escape(str(pull_request["url"]), quote=True)}" '
|
|
1991
|
+
f'target="_blank" rel="noreferrer">PR #{pull_request["number"]} — {escape(str(pull_request["title"]))}</a>'
|
|
1992
|
+
f'<span class="open-pr-status open-pr-status--{escape(str(pull_request.get("status", "waiting_for_checks")), quote=True)}">'
|
|
1993
|
+
f'<span class="open-pr-status__dot" aria-hidden="true"></span><span class="open-pr-status__label"></span></span>'
|
|
1994
|
+
f'{authorization}{repair}<code>{escape(str(pull_request["branch"]))}</code></li>'
|
|
1995
|
+
)
|
|
1996
|
+
|
|
1997
|
+
pull_request_items = "".join(
|
|
1998
|
+
open_pull_request_item(pull_request)
|
|
1999
|
+
for pull_request in workspace_open_pull_requests or []
|
|
2000
|
+
)
|
|
2001
|
+
workspace_open_pull_requests_html = (
|
|
2002
|
+
f'<section id="workspaceOpenPullRequests" class="workspace-open-prs" aria-live="polite"><div class="workspace-open-prs__header"><strong data-i18n="workspace.open_pull_requests"></strong><button class="workspace-open-prs__refresh" id="workspaceOpenPullRequestsRefresh" type="button" data-i18n-title="workspace.open_pull_requests_refresh" data-i18n-aria-label="workspace.open_pull_requests_refresh">↻</button></div><ul>{pull_request_items}</ul></section>'
|
|
2003
|
+
if pull_request_items else ""
|
|
2004
|
+
)
|
|
2005
|
+
return (
|
|
2006
|
+
page.replace("$TITLE", escape(title))
|
|
2007
|
+
.replace("$BUILD_COMMIT", escape(build_commit))
|
|
2008
|
+
.replace("$CHAT_MODEL", escape(chat_model()))
|
|
2009
|
+
.replace("$WORKSPACE_ID", escape(workspace_id))
|
|
2010
|
+
.replace("$PROJECT_NAME", escape(project_name))
|
|
2011
|
+
.replace("$WORKSPACE_LOCATION", escape(workspace_location))
|
|
2012
|
+
.replace("$WORKSPACE_FREE_DISK_SPACE", escape(workspace_free_disk_space))
|
|
2013
|
+
.replace("$TRACKED_FILES", escape(tracked_files))
|
|
2014
|
+
.replace("$WORKSPACE_BRANCH", escape(workspace_branch))
|
|
2015
|
+
.replace("$WORKSPACE_COMMIT", escape(workspace_commit))
|
|
2016
|
+
.replace("$ORIGIN_MAIN_COMMIT", escape(origin_main_commit))
|
|
2017
|
+
.replace("$ORIGIN_MAIN_HIDDEN", "" if origin_main_available else "hidden")
|
|
2018
|
+
.replace("$WORKSPACE_OPEN_PULL_REQUESTS", workspace_open_pull_requests_html)
|
|
2019
|
+
.replace("$WORKSPACE_MAIN_ACTION_HIDDEN", "hidden" if workspace_main_action_hidden else "")
|
|
2020
|
+
.replace("$BRANCH_CLEANUP_HIDDEN", "hidden" if workspace_branch_cleanup_hidden else "")
|
|
2021
|
+
.replace("$PLATFORM_VERSION", escape(platform_version))
|
|
2022
|
+
.replace("$CONFIGURATION_INBOX", escape(configuration_inbox))
|
|
2023
|
+
.encode()
|
|
2024
|
+
)
|