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,276 @@
|
|
|
1
|
+
"""One-shot, evidence-gated repair for a human-authored pull request.
|
|
2
|
+
|
|
3
|
+
This is deliberately separate from the managed execution lifecycle. It never
|
|
4
|
+
claims an Inbox item, resumes a transaction, merges a pull request, or creates
|
|
5
|
+
a pull request. It may only add one host-owned commit to the exact current
|
|
6
|
+
head of an eligible same-repository pull request.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import re
|
|
17
|
+
import tempfile
|
|
18
|
+
|
|
19
|
+
from .central_database import capacity_reserve_from_environment
|
|
20
|
+
from .worktree_tooling import WorktreeToolingError, prepare as prepare_worktree_tooling
|
|
21
|
+
from .codex_capacity import read_remaining_percent
|
|
22
|
+
from .provider_readiness import failures as provider_readiness_failures
|
|
23
|
+
from .providers import CodexCliProvider, GitHubProvider, GitProvider
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
FAILED_CONCLUSIONS = frozenset({"ACTION_REQUIRED", "CANCELLED", "FAILURE", "STARTUP_FAILURE", "TIMED_OUT"})
|
|
27
|
+
SUCCESSFUL_CONCLUSIONS = frozenset({"NEUTRAL", "SKIPPED", "SUCCESS"})
|
|
28
|
+
STATE_DIRECTORY = Path(".engineering") / "status" / "pr-check-repairs"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PullRequestCheckRepairError(RuntimeError):
|
|
32
|
+
"""The request is unsafe, stale, or cannot be admitted."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _repository(root: Path) -> str:
|
|
36
|
+
remote = GitProvider().execute(root, "git", "remote", "get-url", "origin")
|
|
37
|
+
match = re.search(r"github\.com[:/]([^/\s]+)/([^/\s]+?)(?:\.git)?$", remote.stdout.strip()) if remote.returncode == 0 else None
|
|
38
|
+
if not match:
|
|
39
|
+
raise PullRequestCheckRepairError("pr_check_repair_unavailable")
|
|
40
|
+
return f"{match.group(1)}/{match.group(2)}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _state_path(root: Path, number: int, sha: str) -> Path:
|
|
44
|
+
return root / STATE_DIRECTORY / f"{number}-{sha}.json"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _read_state(root: Path, number: int, sha: str) -> dict[str, object] | None:
|
|
48
|
+
try:
|
|
49
|
+
value = json.loads(_state_path(root, number, sha).read_text(encoding="utf-8"))
|
|
50
|
+
except (OSError, json.JSONDecodeError):
|
|
51
|
+
return None
|
|
52
|
+
return value if isinstance(value, dict) else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def attempted(root: Path, number: int, sha: object) -> bool:
|
|
56
|
+
"""Whether this exact remote head already consumed its one repair action."""
|
|
57
|
+
return isinstance(sha, str) and bool(re.fullmatch(r"[0-9a-f]{40}", sha)) and _read_state(root, number, sha) is not None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def failed_check_names(checks: object) -> list[str]:
|
|
61
|
+
"""Return bounded terminal failures from GitHub's check-rollup shape."""
|
|
62
|
+
if not isinstance(checks, list):
|
|
63
|
+
return []
|
|
64
|
+
return [
|
|
65
|
+
str(check.get("name") or check.get("context") or "required check").strip()[:160]
|
|
66
|
+
for check in checks if isinstance(check, dict)
|
|
67
|
+
and str(check.get("conclusion") or check.get("state") or "").upper() in FAILED_CONCLUSIONS
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _write_state(root: Path, number: int, sha: str, payload: dict[str, object]) -> None:
|
|
72
|
+
path = _state_path(root, number, sha)
|
|
73
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
74
|
+
descriptor, temporary = tempfile.mkstemp(prefix=".pr-check-repair-", suffix=".json", dir=path.parent)
|
|
75
|
+
try:
|
|
76
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
77
|
+
json.dump(payload, handle, sort_keys=True)
|
|
78
|
+
handle.write("\n")
|
|
79
|
+
handle.flush()
|
|
80
|
+
os.fsync(handle.fileno())
|
|
81
|
+
os.replace(temporary, path)
|
|
82
|
+
finally:
|
|
83
|
+
Path(temporary).unlink(missing_ok=True)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def check_summary(checks: object) -> tuple[list[str], bool]:
|
|
87
|
+
"""Return failed names and whether the GitHub check projection is stable."""
|
|
88
|
+
if not isinstance(checks, list) or not checks:
|
|
89
|
+
return [], False
|
|
90
|
+
failed = failed_check_names(checks)
|
|
91
|
+
terminal = True
|
|
92
|
+
for check in checks:
|
|
93
|
+
if not isinstance(check, dict):
|
|
94
|
+
return [], False
|
|
95
|
+
conclusion = str(check.get("conclusion") or check.get("state") or "").upper()
|
|
96
|
+
status = str(check.get("status") or "").upper()
|
|
97
|
+
if conclusion not in FAILED_CONCLUSIONS | SUCCESSFUL_CONCLUSIONS or (status and status != "COMPLETED"):
|
|
98
|
+
terminal = False
|
|
99
|
+
return failed, terminal
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def repair_state(root: Path, number: int, sha: object) -> str | None:
|
|
103
|
+
"""Return the durable repair state for this head, including its repair commit."""
|
|
104
|
+
if not isinstance(sha, str) or not re.fullmatch(r"[0-9a-f]{40}", sha):
|
|
105
|
+
return None
|
|
106
|
+
direct = _read_state(root, number, sha)
|
|
107
|
+
if direct is not None:
|
|
108
|
+
status = direct.get("status")
|
|
109
|
+
return str(status) if isinstance(status, str) else None
|
|
110
|
+
try:
|
|
111
|
+
candidates = (root / STATE_DIRECTORY).glob(f"{number}-*.json")
|
|
112
|
+
except OSError:
|
|
113
|
+
return None
|
|
114
|
+
for candidate in candidates:
|
|
115
|
+
try:
|
|
116
|
+
value = json.loads(candidate.read_text(encoding="utf-8"))
|
|
117
|
+
except (OSError, json.JSONDecodeError):
|
|
118
|
+
continue
|
|
119
|
+
if isinstance(value, dict) and value.get("commit_sha") == sha:
|
|
120
|
+
status = value.get("status")
|
|
121
|
+
return str(status) if isinstance(status, str) else None
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def current_evidence(root: Path, number: int) -> dict[str, object]:
|
|
126
|
+
"""Read the exact mutable PR evidence used for admission and projection."""
|
|
127
|
+
if isinstance(number, bool) or number < 1:
|
|
128
|
+
raise PullRequestCheckRepairError("pr_check_repair_invalid_request")
|
|
129
|
+
repository = _repository(root)
|
|
130
|
+
try:
|
|
131
|
+
payload = GitHubProvider().github(
|
|
132
|
+
"pr", "view", str(number), "--repo", repository,
|
|
133
|
+
"--json", "number,state,isDraft,headRefOid,headRefName,baseRefName,headRepository,statusCheckRollup",
|
|
134
|
+
)
|
|
135
|
+
pull_request = json.loads(payload)
|
|
136
|
+
except (RuntimeError, OSError, json.JSONDecodeError) as error:
|
|
137
|
+
raise PullRequestCheckRepairError("pr_check_repair_unavailable") from error
|
|
138
|
+
if not isinstance(pull_request, dict) or pull_request.get("number") != number:
|
|
139
|
+
raise PullRequestCheckRepairError("pr_check_repair_unavailable")
|
|
140
|
+
sha, branch = pull_request.get("headRefOid"), pull_request.get("headRefName")
|
|
141
|
+
head_repository = pull_request.get("headRepository")
|
|
142
|
+
head_repository_name = head_repository.get("nameWithOwner") if isinstance(head_repository, dict) else None
|
|
143
|
+
failed_checks, terminal = check_summary(pull_request.get("statusCheckRollup"))
|
|
144
|
+
eligible = (
|
|
145
|
+
str(pull_request.get("state") or "").upper() == "OPEN"
|
|
146
|
+
and pull_request.get("isDraft") is not True
|
|
147
|
+
and isinstance(sha, str) and bool(re.fullmatch(r"[0-9a-f]{40}", sha))
|
|
148
|
+
and isinstance(branch, str) and bool(branch)
|
|
149
|
+
and head_repository_name == repository
|
|
150
|
+
and terminal and bool(failed_checks)
|
|
151
|
+
)
|
|
152
|
+
state = _read_state(root, number, sha) if isinstance(sha, str) else None
|
|
153
|
+
attempted = state is not None
|
|
154
|
+
return {
|
|
155
|
+
"number": number, "repository": repository, "head_sha": sha, "branch": branch,
|
|
156
|
+
"failed_checks": failed_checks, "checks_terminal": terminal,
|
|
157
|
+
"eligible": eligible and not attempted,
|
|
158
|
+
"repair_state": str(state.get("status")) if state else None,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def admit(root: Path, number: int) -> dict[str, object]:
|
|
163
|
+
"""Atomically reserve exactly one repair attempt for the current PR SHA."""
|
|
164
|
+
evidence = current_evidence(root, number)
|
|
165
|
+
if not evidence["eligible"]:
|
|
166
|
+
raise PullRequestCheckRepairError("pr_check_repair_not_eligible")
|
|
167
|
+
missing = provider_readiness_failures(root, require_github=True)
|
|
168
|
+
if missing:
|
|
169
|
+
raise PullRequestCheckRepairError("pr_check_repair_provider_not_ready")
|
|
170
|
+
remaining = read_remaining_percent()
|
|
171
|
+
reserve = capacity_reserve_from_environment()
|
|
172
|
+
if remaining is None or remaining <= reserve:
|
|
173
|
+
raise PullRequestCheckRepairError("pr_check_repair_capacity_unavailable")
|
|
174
|
+
sha = str(evidence["head_sha"])
|
|
175
|
+
payload = {
|
|
176
|
+
"status": "QUEUED", "number": number, "head_sha": sha,
|
|
177
|
+
"branch": evidence["branch"], "failed_checks": evidence["failed_checks"],
|
|
178
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
179
|
+
}
|
|
180
|
+
_write_state(root, number, sha, payload)
|
|
181
|
+
return evidence
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def mark_dispatch_failed(root: Path, number: int, sha: str) -> None:
|
|
185
|
+
state = _read_state(root, number, sha)
|
|
186
|
+
if state and state.get("status") == "QUEUED":
|
|
187
|
+
_write_state(root, number, sha, {**state, "status": "FAILED", "error": "pr_check_repair_dispatch_failed", "completed_at": datetime.now(timezone.utc).isoformat()})
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _command(root: Path, *arguments: str) -> str:
|
|
191
|
+
completed = GitProvider().execute(root, "git", *arguments)
|
|
192
|
+
if completed.returncode:
|
|
193
|
+
raise PullRequestCheckRepairError("pr_check_repair_failed")
|
|
194
|
+
return completed.stdout.strip()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _prepare_worktree_tooling(worktree: Path) -> None:
|
|
198
|
+
"""Install locked Playwright tooling before an EP-created worktree is used.
|
|
199
|
+
|
|
200
|
+
A linked worktree does not share ``node_modules`` with its source checkout.
|
|
201
|
+
When this repository declares the browser suite, ``npm ci`` is therefore a
|
|
202
|
+
required local preparation step, not a best-effort validation fallback.
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
prepare_worktree_tooling(worktree)
|
|
206
|
+
except WorktreeToolingError:
|
|
207
|
+
raise PullRequestCheckRepairError("pr_check_repair_worktree_tooling_unavailable")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def run(root: Path, number: int, sha: str) -> None:
|
|
211
|
+
"""Execute the already-admitted repair in an isolated, disposable worktree."""
|
|
212
|
+
state = _read_state(root, number, sha)
|
|
213
|
+
if not state or state.get("status") != "QUEUED":
|
|
214
|
+
raise PullRequestCheckRepairError("pr_check_repair_not_eligible")
|
|
215
|
+
evidence = current_evidence(root, number)
|
|
216
|
+
# The durable reservation intentionally makes ``eligible`` false. Fresh
|
|
217
|
+
# remote evidence must nevertheless still identify this exact failed head.
|
|
218
|
+
if (
|
|
219
|
+
evidence.get("head_sha") != sha
|
|
220
|
+
or evidence.get("branch") != state.get("branch")
|
|
221
|
+
or not evidence.get("checks_terminal")
|
|
222
|
+
or not evidence.get("failed_checks")
|
|
223
|
+
):
|
|
224
|
+
raise PullRequestCheckRepairError("pr_check_repair_stale")
|
|
225
|
+
branch = str(state["branch"])
|
|
226
|
+
_write_state(root, number, sha, {**state, "status": "RUNNING", "started_at": datetime.now(timezone.utc).isoformat()})
|
|
227
|
+
worktree = Path(tempfile.mkdtemp(prefix=f"ep-pr-{number}-", dir=root / ".engineering"))
|
|
228
|
+
worktree.rmdir()
|
|
229
|
+
try:
|
|
230
|
+
_command(root, "fetch", "origin", branch)
|
|
231
|
+
_command(root, "worktree", "add", "--detach", str(worktree), sha)
|
|
232
|
+
_prepare_worktree_tooling(worktree)
|
|
233
|
+
prompt = (
|
|
234
|
+
f"Repair only the failed GitHub checks for pull request #{number}.\n"
|
|
235
|
+
f"Current head SHA: {sha}. Failed checks: {', '.join(str(item) for item in state['failed_checks'])}.\n"
|
|
236
|
+
"Work only in this isolated worktree. Inspect the failed-check evidence and make the smallest focused source or test correction. "
|
|
237
|
+
"Do not change the pull request scope. Do not create commits, push, open or merge a pull request, alter GitHub settings, or use destructive Git commands. "
|
|
238
|
+
"Run focused verification where practical. Finish with the working tree containing only the proposed repair."
|
|
239
|
+
)
|
|
240
|
+
result = CodexCliProvider().invoke(
|
|
241
|
+
worktree, ("codex", "exec", "--sandbox", "workspace-write", "-C", str(worktree), prompt),
|
|
242
|
+
)
|
|
243
|
+
if result.returncode:
|
|
244
|
+
raise PullRequestCheckRepairError("pr_check_repair_agent_failed")
|
|
245
|
+
if _command(worktree, "rev-parse", "HEAD") != sha:
|
|
246
|
+
raise PullRequestCheckRepairError("pr_check_repair_scope_conflict")
|
|
247
|
+
if not _command(worktree, "status", "--porcelain", "--untracked-files=all"):
|
|
248
|
+
raise PullRequestCheckRepairError("pr_check_repair_no_change")
|
|
249
|
+
_command(worktree, "diff", "--check")
|
|
250
|
+
_command(worktree, "add", "--all")
|
|
251
|
+
_command(worktree, "commit", "-m", f"fix(ci): repair failed checks for PR #{number}")
|
|
252
|
+
commit = _command(worktree, "rev-parse", "HEAD")
|
|
253
|
+
_command(worktree, "push", "--force-with-lease=refs/heads/" + branch + ":" + sha, "origin", "HEAD:refs/heads/" + branch)
|
|
254
|
+
_write_state(root, number, sha, {**state, "status": "SUBMITTED", "commit_sha": commit, "submitted_at": datetime.now(timezone.utc).isoformat()})
|
|
255
|
+
except PullRequestCheckRepairError as error:
|
|
256
|
+
_write_state(root, number, sha, {**state, "status": "FAILED", "error": str(error), "completed_at": datetime.now(timezone.utc).isoformat()})
|
|
257
|
+
finally:
|
|
258
|
+
try:
|
|
259
|
+
if worktree.exists() and not _command(worktree, "status", "--porcelain", "--untracked-files=all"):
|
|
260
|
+
_command(root, "worktree", "remove", "--", str(worktree))
|
|
261
|
+
except PullRequestCheckRepairError:
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def main() -> int:
|
|
266
|
+
parser = argparse.ArgumentParser()
|
|
267
|
+
parser.add_argument("--root", required=True, type=Path)
|
|
268
|
+
parser.add_argument("--pull-request", required=True, type=int)
|
|
269
|
+
parser.add_argument("--head-sha", required=True)
|
|
270
|
+
arguments = parser.parse_args()
|
|
271
|
+
run(arguments.root.resolve(), arguments.pull_request, arguments.head_sha)
|
|
272
|
+
return 0
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
if __name__ == "__main__":
|
|
276
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Fail-closed recovery of missing historical Managed pull-request evidence.
|
|
2
|
+
|
|
3
|
+
This tool is intentionally operator-invoked and dry-run by default. It never
|
|
4
|
+
creates, edits, merges, or closes a pull request. It can only link a missing
|
|
5
|
+
checkpoint field after the live GitHub record exactly matches the checkpointed
|
|
6
|
+
branch and merge commit for that role.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
from dataclasses import dataclass, replace
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import tempfile
|
|
17
|
+
from typing import Callable, Iterable
|
|
18
|
+
|
|
19
|
+
from .agent_state import StateError, TransactionState
|
|
20
|
+
from .execution_models import PullRequestEvidence
|
|
21
|
+
from .execution_repository import GhCliClient, GitHubClient
|
|
22
|
+
from .providers import GitProvider
|
|
23
|
+
from .storage import EngineeringStorageError, open_storage
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ROLES = ("IMPLEMENTATION", "FINALIZATION")
|
|
27
|
+
ACTOR = "operator_pr_evidence_backfill"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class BackfillDecision:
|
|
32
|
+
run_id: str
|
|
33
|
+
role: str
|
|
34
|
+
outcome: str
|
|
35
|
+
reason: str
|
|
36
|
+
pull_request: int | None = None
|
|
37
|
+
expected_branch: str | None = None
|
|
38
|
+
expected_merge_commit: str | None = None
|
|
39
|
+
|
|
40
|
+
def report(self) -> dict[str, object]:
|
|
41
|
+
return {
|
|
42
|
+
"run_id": self.run_id,
|
|
43
|
+
"role": self.role.lower(),
|
|
44
|
+
"outcome": self.outcome.lower(),
|
|
45
|
+
"reason": self.reason,
|
|
46
|
+
"pull_request": self.pull_request,
|
|
47
|
+
"expected_branch": self.expected_branch,
|
|
48
|
+
"expected_merge_commit": self.expected_merge_commit,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _now() -> str:
|
|
53
|
+
return datetime.now(timezone.utc).isoformat()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _role_evidence(state: TransactionState, role: str) -> tuple[int | None, str | None, str | None]:
|
|
57
|
+
if role == "IMPLEMENTATION":
|
|
58
|
+
return state.implementation_pull_request, state.implementation_branch, state.implementation_merge_commit
|
|
59
|
+
return state.finalization_pull_request, state.finalization_branch, state.finalization_merge_commit
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _candidate_decision(
|
|
63
|
+
state: TransactionState, role: str, github: GitHubClient, repository: str | None,
|
|
64
|
+
main_contains: Callable[[str], bool | None],
|
|
65
|
+
) -> BackfillDecision:
|
|
66
|
+
recorded_pr, branch, merge_commit = _role_evidence(state, role)
|
|
67
|
+
base = dict(run_id=state.run_id, role=role, expected_branch=branch, expected_merge_commit=merge_commit)
|
|
68
|
+
if state.execution_mode != "MANAGED":
|
|
69
|
+
return BackfillDecision(outcome="SKIPPED", reason="not_managed_execution", **base)
|
|
70
|
+
if repository is None or state.repository != repository:
|
|
71
|
+
return BackfillDecision(outcome="SKIPPED", reason="repository_evidence_unavailable_or_mismatched", **base)
|
|
72
|
+
if not state.terminal:
|
|
73
|
+
return BackfillDecision(outcome="SKIPPED", reason="run_not_terminal", **base)
|
|
74
|
+
if recorded_pr is not None:
|
|
75
|
+
return BackfillDecision(outcome="SKIPPED", reason="pull_request_already_recorded", pull_request=recorded_pr, **base)
|
|
76
|
+
if not branch or not merge_commit:
|
|
77
|
+
return BackfillDecision(outcome="SKIPPED", reason="checkpoint_evidence_incomplete", **base)
|
|
78
|
+
try:
|
|
79
|
+
evidence = github.pull_request_for_head_branch(branch)
|
|
80
|
+
except Exception:
|
|
81
|
+
return BackfillDecision(outcome="SKIPPED", reason="github_evidence_unavailable", **base)
|
|
82
|
+
if evidence is None:
|
|
83
|
+
return BackfillDecision(outcome="SKIPPED", reason="pull_request_not_found_for_checkpoint_branch", **base)
|
|
84
|
+
if not _exact_match(evidence, branch, merge_commit):
|
|
85
|
+
return BackfillDecision(outcome="SKIPPED", reason="github_evidence_does_not_exactly_match_checkpoint", **base)
|
|
86
|
+
contained = main_contains(merge_commit)
|
|
87
|
+
if contained is None:
|
|
88
|
+
return BackfillDecision(outcome="SKIPPED", reason="repository_merge_evidence_unavailable", **base)
|
|
89
|
+
if not contained:
|
|
90
|
+
return BackfillDecision(outcome="SKIPPED", reason="merge_commit_not_in_origin_main", **base)
|
|
91
|
+
return BackfillDecision(outcome="APPLIED", reason="exact_github_branch_and_merge_evidence", pull_request=evidence.number, **base)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _exact_match(evidence: PullRequestEvidence, branch: str, merge_commit: str) -> bool:
|
|
95
|
+
return (
|
|
96
|
+
evidence.state == "MERGED"
|
|
97
|
+
and evidence.head_branch == branch
|
|
98
|
+
and evidence.base_branch == "main"
|
|
99
|
+
and evidence.merge_commit == merge_commit
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _load_states(root: Path, run_id: str | None) -> Iterable[TransactionState]:
|
|
104
|
+
connection = open_storage(root, create=False)
|
|
105
|
+
try:
|
|
106
|
+
if run_id:
|
|
107
|
+
rows = connection.execute("SELECT payload FROM engineering_transactions WHERE run_id=?", (run_id,)).fetchall()
|
|
108
|
+
else:
|
|
109
|
+
rows = connection.execute("SELECT payload FROM engineering_transactions ORDER BY run_id").fetchall()
|
|
110
|
+
finally:
|
|
111
|
+
connection.close()
|
|
112
|
+
for (payload,) in rows:
|
|
113
|
+
try:
|
|
114
|
+
yield TransactionState.from_dict(json.loads(payload))
|
|
115
|
+
except (StateError, TypeError, json.JSONDecodeError):
|
|
116
|
+
# A malformed checkpoint is never eligible for recovery. There is
|
|
117
|
+
# no safe run id to write against, so it remains database evidence.
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _current_repository(root: Path) -> str | None:
|
|
122
|
+
result = GitProvider().execute(root, "git", "remote", "get-url", "origin")
|
|
123
|
+
if result.returncode:
|
|
124
|
+
return None
|
|
125
|
+
value = result.stdout.strip().removesuffix(".git")
|
|
126
|
+
if value.startswith("git@github.com:"):
|
|
127
|
+
value = value.removeprefix("git@github.com:")
|
|
128
|
+
elif value.startswith("https://github.com/"):
|
|
129
|
+
value = value.removeprefix("https://github.com/")
|
|
130
|
+
return value if value.count("/") == 1 else None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _origin_main_contains(root: Path, commit: str) -> bool | None:
|
|
134
|
+
"""Refresh only the remote-tracking ref; never change the checkout or branch."""
|
|
135
|
+
provider = GitProvider()
|
|
136
|
+
if provider.execute(root, "git", "fetch", "origin", "main").returncode:
|
|
137
|
+
return None
|
|
138
|
+
result = provider.execute(root, "git", "merge-base", "--is-ancestor", commit, "origin/main")
|
|
139
|
+
return result.returncode == 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _updated_state(state: TransactionState, decision: BackfillDecision) -> TransactionState:
|
|
143
|
+
if decision.role == "IMPLEMENTATION":
|
|
144
|
+
return replace(state, implementation_pull_request=decision.pull_request)
|
|
145
|
+
return replace(state, finalization_pull_request=decision.pull_request)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _record_skip(root: Path, decision: BackfillDecision, *, observed_at: str) -> None:
|
|
149
|
+
connection = open_storage(root)
|
|
150
|
+
try:
|
|
151
|
+
connection.execute(
|
|
152
|
+
"INSERT INTO execution_pr_evidence_backfills(run_id,pr_role,outcome,reason,pr_number,expected_branch,expected_merge_commit,observed_at,actor) VALUES(?,?,?,?,?,?,?,?,?)",
|
|
153
|
+
(decision.run_id, decision.role, decision.outcome, decision.reason, decision.pull_request,
|
|
154
|
+
decision.expected_branch, decision.expected_merge_commit, observed_at, ACTOR),
|
|
155
|
+
)
|
|
156
|
+
finally:
|
|
157
|
+
connection.close()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _write_projection(directory: Path, state: TransactionState) -> None:
|
|
161
|
+
"""Refresh the compatibility JSON only after the canonical commit succeeds."""
|
|
162
|
+
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
163
|
+
target = directory / f"{state.run_id}.json"
|
|
164
|
+
descriptor, temporary = tempfile.mkstemp(prefix=f".{state.run_id}.", suffix=".tmp", dir=directory)
|
|
165
|
+
try:
|
|
166
|
+
os.fchmod(descriptor, 0o600)
|
|
167
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
168
|
+
json.dump(state.to_dict(), handle, indent=2, sort_keys=True)
|
|
169
|
+
handle.write("\n")
|
|
170
|
+
handle.flush()
|
|
171
|
+
os.fsync(handle.fileno())
|
|
172
|
+
os.replace(temporary, target)
|
|
173
|
+
os.chmod(target, 0o600)
|
|
174
|
+
except OSError:
|
|
175
|
+
try:
|
|
176
|
+
os.unlink(temporary)
|
|
177
|
+
except FileNotFoundError:
|
|
178
|
+
pass
|
|
179
|
+
raise
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _apply(root: Path, state: TransactionState, decision: BackfillDecision, *, observed_at: str) -> BackfillDecision:
|
|
183
|
+
"""Atomically persist the exact verified link and its audit record."""
|
|
184
|
+
connection = open_storage(root)
|
|
185
|
+
updated: TransactionState | None = None
|
|
186
|
+
try:
|
|
187
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
188
|
+
row = connection.execute("SELECT payload FROM engineering_transactions WHERE run_id=?", (state.run_id,)).fetchone()
|
|
189
|
+
if not row:
|
|
190
|
+
decision = replace(decision, outcome="SKIPPED", reason="checkpoint_disappeared", pull_request=None)
|
|
191
|
+
else:
|
|
192
|
+
current = TransactionState.from_dict(json.loads(row[0]))
|
|
193
|
+
current_pr, current_branch, current_merge = _role_evidence(current, decision.role)
|
|
194
|
+
if (
|
|
195
|
+
current.execution_mode != "MANAGED" or not current.terminal
|
|
196
|
+
or current_pr is not None
|
|
197
|
+
or current_branch != decision.expected_branch
|
|
198
|
+
or current_merge != decision.expected_merge_commit
|
|
199
|
+
):
|
|
200
|
+
decision = replace(decision, outcome="SKIPPED", reason="checkpoint_changed_since_verification", pull_request=None)
|
|
201
|
+
else:
|
|
202
|
+
updated = _updated_state(current, decision)
|
|
203
|
+
encoded = json.dumps(updated.to_dict(), separators=(",", ":"), sort_keys=True)
|
|
204
|
+
connection.execute(
|
|
205
|
+
"UPDATE engineering_transactions SET payload=?,phase=?,updated_at=CURRENT_TIMESTAMP WHERE run_id=?",
|
|
206
|
+
(encoded, updated.phase, updated.run_id),
|
|
207
|
+
)
|
|
208
|
+
connection.execute(
|
|
209
|
+
"INSERT OR IGNORE INTO execution_lifecycle_events(run_id,phase,checkpoint,recorded_at) VALUES(?,?,?,CURRENT_TIMESTAMP)",
|
|
210
|
+
(updated.run_id, updated.phase, encoded),
|
|
211
|
+
)
|
|
212
|
+
connection.execute(
|
|
213
|
+
"INSERT INTO execution_pr_evidence_backfills(run_id,pr_role,outcome,reason,pr_number,expected_branch,expected_merge_commit,observed_at,actor) VALUES(?,?,?,?,?,?,?,?,?)",
|
|
214
|
+
(decision.run_id, decision.role, decision.outcome, decision.reason, decision.pull_request,
|
|
215
|
+
decision.expected_branch, decision.expected_merge_commit, observed_at, ACTOR),
|
|
216
|
+
)
|
|
217
|
+
connection.execute("COMMIT")
|
|
218
|
+
except (EngineeringStorageError, StateError, OSError, ValueError, json.JSONDecodeError):
|
|
219
|
+
connection.execute("ROLLBACK")
|
|
220
|
+
raise
|
|
221
|
+
finally:
|
|
222
|
+
connection.close()
|
|
223
|
+
if updated is not None:
|
|
224
|
+
try:
|
|
225
|
+
_write_projection(root / ".engineering" / "engineering-runs", updated)
|
|
226
|
+
except OSError:
|
|
227
|
+
return replace(decision, reason="exact_evidence_applied_projection_refresh_failed")
|
|
228
|
+
return decision
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def backfill(root: Path, *, apply: bool = False, run_id: str | None = None,
|
|
232
|
+
github: GitHubClient | None = None, repository: str | None = None,
|
|
233
|
+
main_contains: Callable[[str], bool | None] | None = None) -> dict[str, object]:
|
|
234
|
+
"""Inspect or atomically backfill exact historical Managed PR evidence."""
|
|
235
|
+
client = github or GhCliClient()
|
|
236
|
+
repository = repository if repository is not None else _current_repository(root)
|
|
237
|
+
checked_merges: dict[str, bool | None] = {}
|
|
238
|
+
|
|
239
|
+
def current_main_contains(commit: str) -> bool | None:
|
|
240
|
+
if commit not in checked_merges:
|
|
241
|
+
check = main_contains or (lambda value: _origin_main_contains(root, value))
|
|
242
|
+
checked_merges[commit] = check(commit)
|
|
243
|
+
return checked_merges[commit]
|
|
244
|
+
|
|
245
|
+
decisions: list[BackfillDecision] = []
|
|
246
|
+
for state in _load_states(root, run_id):
|
|
247
|
+
for role in ROLES:
|
|
248
|
+
decision = _candidate_decision(state, role, client, repository, current_main_contains)
|
|
249
|
+
if apply:
|
|
250
|
+
if decision.outcome == "APPLIED":
|
|
251
|
+
decision = _apply(root, state, decision, observed_at=_now())
|
|
252
|
+
else:
|
|
253
|
+
_record_skip(root, decision, observed_at=_now())
|
|
254
|
+
decisions.append(decision)
|
|
255
|
+
applied = sum(item.outcome == "APPLIED" for item in decisions)
|
|
256
|
+
skipped = len(decisions) - applied
|
|
257
|
+
return {"mode": "apply" if apply else "dry_run", "applied": applied, "skipped": skipped, "decisions": [item.report() for item in decisions]}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
261
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
262
|
+
parser.add_argument("--apply", action="store_true", help="persist only exact, verified evidence matches")
|
|
263
|
+
parser.add_argument("--run-id", help="limit recovery to one canonical run id")
|
|
264
|
+
return parser
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def main(argv: list[str] | None = None) -> int:
|
|
268
|
+
args = build_parser().parse_args(argv)
|
|
269
|
+
try:
|
|
270
|
+
report = backfill(Path.cwd().resolve(), apply=args.apply, run_id=args.run_id)
|
|
271
|
+
except EngineeringStorageError as error:
|
|
272
|
+
raise SystemExit(f"PR-evidence recovery could not safely access storage: {error}") from error
|
|
273
|
+
print(json.dumps(report, indent=2, sort_keys=True))
|
|
274
|
+
return 0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__": # pragma: no cover - module entry point
|
|
278
|
+
raise SystemExit(main())
|