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.
- 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,144 @@
|
|
|
1
|
+
"""Immutable, read-only diagnostic evidence for failed host qualification checks.
|
|
2
|
+
|
|
3
|
+
This module deliberately translates existing qualification results only. It
|
|
4
|
+
does not decide whether execution is admitted, retried, or resumed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import asdict, dataclass
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import tempfile
|
|
15
|
+
import uuid
|
|
16
|
+
from typing import Iterable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DRIFT_CATEGORIES = frozenset({
|
|
20
|
+
"Runtime Database", "Runtime Identity", "Runtime Schema",
|
|
21
|
+
"Execution Host Version", "Bootstrap Contract", "Checkpoint Format",
|
|
22
|
+
"Memory Format", "Report Format", "Configuration", "Workspace",
|
|
23
|
+
"Repository", "Capability", "Producer Contract", "Execution Policy",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class DriftEvidence:
|
|
29
|
+
drift_id: str
|
|
30
|
+
category: str
|
|
31
|
+
severity: str
|
|
32
|
+
expected_value: str
|
|
33
|
+
observed_value: str
|
|
34
|
+
resolution_recommendation: str
|
|
35
|
+
detection_timestamp: str
|
|
36
|
+
qualification_stage: str
|
|
37
|
+
affected_component: str
|
|
38
|
+
affected_repository: str
|
|
39
|
+
affected_runtime: str
|
|
40
|
+
|
|
41
|
+
def payload(self) -> dict[str, str]:
|
|
42
|
+
return asdict(self)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def category_for(identifier: str, stage: str) -> str:
|
|
46
|
+
"""Map stable check IDs to the canonical, extensible drift taxonomy."""
|
|
47
|
+
identifier = identifier.casefold()
|
|
48
|
+
if identifier in {"telemetry_storage", "storage_schema"}:
|
|
49
|
+
return "Runtime Database" if identifier == "telemetry_storage" else "Runtime Schema"
|
|
50
|
+
if identifier in {"host_identity", "workspace_identity", "target_repository_identity"}:
|
|
51
|
+
return "Runtime Identity"
|
|
52
|
+
if identifier in {"execution_host_version", "runner_version"}:
|
|
53
|
+
return "Execution Host Version"
|
|
54
|
+
if identifier == "bootstrap_contract":
|
|
55
|
+
return "Bootstrap Contract"
|
|
56
|
+
if identifier == "checkpoint_format":
|
|
57
|
+
return "Checkpoint Format"
|
|
58
|
+
if identifier == "memory_format":
|
|
59
|
+
return "Memory Format"
|
|
60
|
+
if identifier == "report_format":
|
|
61
|
+
return "Report Format"
|
|
62
|
+
if identifier == "configuration" or identifier == "configuration_schema":
|
|
63
|
+
return "Configuration"
|
|
64
|
+
if identifier in {"runtime_components", "provider_support", "required_capabilities"}:
|
|
65
|
+
return "Capability"
|
|
66
|
+
if "producer" in identifier:
|
|
67
|
+
return "Producer Contract"
|
|
68
|
+
if identifier == "execution_mode":
|
|
69
|
+
return "Execution Policy"
|
|
70
|
+
if stage == "Workspace Preflight":
|
|
71
|
+
return "Repository" if identifier.startswith(("git_", "worktree_", "managed_")) else "Workspace"
|
|
72
|
+
return "Capability" if stage == "Capability Preflight" else "Workspace"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def evidence_for_checks(
|
|
76
|
+
checks: Iterable[object], *, stage: str, repository: str, runtime: str = "Engineering Platform"
|
|
77
|
+
) -> tuple[DriftEvidence, ...]:
|
|
78
|
+
"""Create deterministic evidence for every failed pre-existing check."""
|
|
79
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
80
|
+
evidence: list[DriftEvidence] = []
|
|
81
|
+
for check in checks:
|
|
82
|
+
if getattr(check, "outcome", None) != "FAIL":
|
|
83
|
+
continue
|
|
84
|
+
identifier = str(getattr(check, "identifier", "unknown"))
|
|
85
|
+
reason = str(getattr(check, "reason", "Observed qualification check failed."))
|
|
86
|
+
recovery = str(getattr(check, "recovery", "Resolve the reported qualification drift."))
|
|
87
|
+
evidence.append(DriftEvidence(
|
|
88
|
+
drift_id=f"drift-{uuid.uuid4().hex}",
|
|
89
|
+
category=category_for(identifier, stage), severity="BLOCKING",
|
|
90
|
+
expected_value=f"{identifier}: PASS", observed_value=reason,
|
|
91
|
+
resolution_recommendation=recovery, detection_timestamp=now,
|
|
92
|
+
qualification_stage=stage, affected_component=identifier,
|
|
93
|
+
affected_repository=repository, affected_runtime=runtime,
|
|
94
|
+
))
|
|
95
|
+
return tuple(evidence)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def persist(root: Path, evidence: Iterable[DriftEvidence]) -> tuple[dict[str, str], ...]:
|
|
99
|
+
"""Append immutable evidence documents; never rewrite a prior observation."""
|
|
100
|
+
items = tuple(evidence)
|
|
101
|
+
if not items:
|
|
102
|
+
return ()
|
|
103
|
+
directory = root / ".engineering" / "drift-evidence"
|
|
104
|
+
try:
|
|
105
|
+
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
106
|
+
for item in items:
|
|
107
|
+
descriptor, temporary = tempfile.mkstemp(prefix=".drift-", dir=directory)
|
|
108
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
109
|
+
handle.write(json.dumps(item.payload(), sort_keys=True, separators=(",", ":")) + "\n")
|
|
110
|
+
handle.flush()
|
|
111
|
+
os.fsync(handle.fileno())
|
|
112
|
+
os.replace(temporary, directory / f"{item.drift_id}.json")
|
|
113
|
+
except OSError:
|
|
114
|
+
# Existing fail-closed qualification remains authoritative if local
|
|
115
|
+
# diagnostic persistence is unavailable.
|
|
116
|
+
return tuple(item.payload() for item in items)
|
|
117
|
+
return tuple(item.payload() for item in items)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def summary(evidence: Iterable[dict[str, object]]) -> str:
|
|
121
|
+
"""Return one compact, operator-facing explanation without source inspection."""
|
|
122
|
+
items = list(evidence)
|
|
123
|
+
if not items:
|
|
124
|
+
return "No drift detected."
|
|
125
|
+
first = items[0]
|
|
126
|
+
return (
|
|
127
|
+
f"{first.get('qualification_stage', 'Qualification')} blocked by "
|
|
128
|
+
f"{first.get('affected_component', 'an unresolved component')} "
|
|
129
|
+
f"({first.get('category', 'Drift')}). Expected: {first.get('expected_value')}. "
|
|
130
|
+
f"Observed: {first.get('observed_value')}. Required action: "
|
|
131
|
+
f"{first.get('resolution_recommendation')}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def guidance(evidence: Iterable[dict[str, object]]) -> dict[str, object]:
|
|
136
|
+
"""Read-only retry/resume advice; it does not change lifecycle authority."""
|
|
137
|
+
items = list(evidence)
|
|
138
|
+
action = items[0].get("resolution_recommendation") if items else "No action required."
|
|
139
|
+
return {
|
|
140
|
+
"retry_appropriate": bool(items),
|
|
141
|
+
"resume_appropriate": False if items else True,
|
|
142
|
+
"operator_intervention_required": bool(items),
|
|
143
|
+
"prerequisite": action,
|
|
144
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Fail-closed emergency stop and workspace rollback for one live run."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
import signal
|
|
10
|
+
import sqlite3
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import replace
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
|
|
15
|
+
from .agent_state import StateStore
|
|
16
|
+
from .execution_lease import Lease, liveness, release
|
|
17
|
+
from .execution_timing import complete_active_phase
|
|
18
|
+
from .live_status import write_runner_process
|
|
19
|
+
from .prompt_history import record_prompt_execution
|
|
20
|
+
from .providers import GitProvider, LocalProcessProvider
|
|
21
|
+
from .storage import EngineeringStorageError, load_projection, open_storage, record_emergency_recovery, record_execution_dismissal, store_projection
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
RUN_ID_PATTERN = re.compile(r"inbox-[a-z0-9-]{6,64}$")
|
|
25
|
+
BRANCH_PATTERN = re.compile(r"codex/[A-Za-z0-9._/-]+$")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EmergencyRecoveryError(RuntimeError):
|
|
29
|
+
"""Raised when a destructive recovery cannot be proven safe."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class RecoveryPlan:
|
|
34
|
+
run_id: str
|
|
35
|
+
branch: str
|
|
36
|
+
baseline_branch: str
|
|
37
|
+
baseline_head: str
|
|
38
|
+
process_group: int | None
|
|
39
|
+
host_pid: int
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _git(root: Path, *args: str) -> str:
|
|
43
|
+
result = GitProvider().execute(root, "git", *args)
|
|
44
|
+
if result.returncode:
|
|
45
|
+
raise EmergencyRecoveryError("De Git-werkmap kan niet veilig worden gecontroleerd.")
|
|
46
|
+
return result.stdout.strip()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _live(root: Path, run_id: str, *, central_database: Path | None = None) -> dict[str, object]:
|
|
50
|
+
live = load_projection(root, "live_status", central_database=central_database) or {}
|
|
51
|
+
if live.get("run_id") != run_id:
|
|
52
|
+
raise EmergencyRecoveryError("Deze uitvoering is niet de huidige uitvoering.")
|
|
53
|
+
if liveness(root, run_id, central_database=central_database).get("state") != "LIVE":
|
|
54
|
+
raise EmergencyRecoveryError("Deze uitvoering is niet meer actief; de noodactie is niet nodig.")
|
|
55
|
+
if live.get("execution_mode") != "MANAGED":
|
|
56
|
+
raise EmergencyRecoveryError("Noodherstel met rollback is alleen beschikbaar voor een beheerde uitvoering.")
|
|
57
|
+
return live
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _runner(root: Path, run_id: str) -> tuple[int, int] | None:
|
|
61
|
+
try:
|
|
62
|
+
runner = json.loads((root / ".engineering" / "status" / "runner_process.json").read_text(encoding="utf-8"))
|
|
63
|
+
except (OSError, json.JSONDecodeError):
|
|
64
|
+
return None
|
|
65
|
+
pid, group = runner.get("pid"), runner.get("process_group")
|
|
66
|
+
if runner.get("run_id") != run_id:
|
|
67
|
+
return None
|
|
68
|
+
if not isinstance(pid, int) or pid <= 0 or not isinstance(group, int) or group <= 0:
|
|
69
|
+
raise EmergencyRecoveryError("De door deze uitvoering beheerde Codex-procesgroep is ongeldig.")
|
|
70
|
+
return pid, group
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _host_pid(root: Path, run_id: str, *, central_database: Path | None = None) -> int:
|
|
74
|
+
connection = open_storage(root) if central_database is None else sqlite3.connect(central_database.resolve(), isolation_level=None)
|
|
75
|
+
try:
|
|
76
|
+
row = connection.execute(
|
|
77
|
+
"SELECT process_id FROM execution_run_leases WHERE run_id=? AND lease_state='ACTIVE' ORDER BY created_at DESC LIMIT 1",
|
|
78
|
+
(run_id,),
|
|
79
|
+
).fetchone()
|
|
80
|
+
finally:
|
|
81
|
+
connection.close()
|
|
82
|
+
pid = row[0] if row else None
|
|
83
|
+
if not isinstance(pid, int) or pid <= 0:
|
|
84
|
+
raise EmergencyRecoveryError("De Execution Host van deze uitvoering is niet beschikbaar.")
|
|
85
|
+
return pid
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _require_central_project_ownership(database: Path, project_id: str | None, run_id: str) -> None:
|
|
89
|
+
"""Reject a dashboard action whose run is not owned by its selected project."""
|
|
90
|
+
if not isinstance(project_id, str) or not project_id:
|
|
91
|
+
raise EmergencyRecoveryError("Er is geen geldig project geselecteerd voor deze noodactie.")
|
|
92
|
+
try:
|
|
93
|
+
connection = sqlite3.connect(database.resolve(), isolation_level=None)
|
|
94
|
+
try:
|
|
95
|
+
row = connection.execute(
|
|
96
|
+
"SELECT project_id FROM ep_execution_runs WHERE run_id=?", (run_id,)
|
|
97
|
+
).fetchone()
|
|
98
|
+
finally:
|
|
99
|
+
connection.close()
|
|
100
|
+
except sqlite3.Error as error:
|
|
101
|
+
raise EmergencyRecoveryError("De centrale uitvoeringstoestand is niet beschikbaar.") from error
|
|
102
|
+
if row is None or row[0] != project_id:
|
|
103
|
+
raise EmergencyRecoveryError("Deze uitvoering behoort niet tot het geselecteerde project.")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _process_command(root: Path, pid: int) -> str:
|
|
107
|
+
result = LocalProcessProvider().execute(root, ("ps", "-p", str(pid), "-o", "command="))
|
|
108
|
+
return result.stdout.strip() if result.returncode == 0 else ""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _plan(
|
|
112
|
+
root: Path, run_id: str, *, central_database: Path | None = None, project_id: str | None = None,
|
|
113
|
+
) -> RecoveryPlan:
|
|
114
|
+
if not RUN_ID_PATTERN.fullmatch(run_id):
|
|
115
|
+
raise EmergencyRecoveryError("De opgegeven run-ID is ongeldig.")
|
|
116
|
+
if central_database is not None:
|
|
117
|
+
_require_central_project_ownership(central_database, project_id, run_id)
|
|
118
|
+
live = _live(root, run_id, central_database=central_database)
|
|
119
|
+
state = StateStore(root / ".engineering" / "engineering-runs", central_database=central_database, emit_local_projection=central_database is None).load(run_id)
|
|
120
|
+
if state is None:
|
|
121
|
+
raise EmergencyRecoveryError("De actieve uitvoering heeft geen canoniek checkpoint.")
|
|
122
|
+
if any(
|
|
123
|
+
pull_request is not None
|
|
124
|
+
for pull_request in (
|
|
125
|
+
state.pull_request, state.implementation_pull_request,
|
|
126
|
+
state.finalization_pull_request, state.reconciliation_pull_request,
|
|
127
|
+
)
|
|
128
|
+
):
|
|
129
|
+
raise EmergencyRecoveryError("Er is al een pull request geregistreerd; de noodknop verwijdert geen pull requests of hun branches.")
|
|
130
|
+
recovery = live.get("workspace_recovery")
|
|
131
|
+
if not isinstance(recovery, dict):
|
|
132
|
+
raise EmergencyRecoveryError("Deze uitvoering heeft geen veilige herstelbasis geregistreerd.")
|
|
133
|
+
baseline_branch = recovery.get("baseline_branch")
|
|
134
|
+
baseline_head = recovery.get("baseline_head")
|
|
135
|
+
preexisting = recovery.get("preexisting_branches")
|
|
136
|
+
if (
|
|
137
|
+
recovery.get("baseline_clean") is not True
|
|
138
|
+
or baseline_branch != "main"
|
|
139
|
+
or not isinstance(baseline_head, str)
|
|
140
|
+
or not re.fullmatch(r"[0-9a-f]{40}", baseline_head)
|
|
141
|
+
or not isinstance(preexisting, list)
|
|
142
|
+
):
|
|
143
|
+
raise EmergencyRecoveryError("De uitvoering begon niet vanaf een aantoonbaar schone main-basis.")
|
|
144
|
+
branch = _git(root, "branch", "--show-current")
|
|
145
|
+
head = _git(root, "rev-parse", "HEAD")
|
|
146
|
+
if head != baseline_head:
|
|
147
|
+
raise EmergencyRecoveryError("Er zijn commits gemaakt; de noodknop verwijdert geen gecommitteerd werk of pull requests.")
|
|
148
|
+
if branch != "main" and (not BRANCH_PATTERN.fullmatch(branch) or branch in preexisting):
|
|
149
|
+
raise EmergencyRecoveryError("De actieve branch is niet aantoonbaar door deze uitvoering aangemaakt.")
|
|
150
|
+
runner = _runner(root, run_id)
|
|
151
|
+
host_pid = _host_pid(root, run_id, central_database=central_database)
|
|
152
|
+
runner_pid, group = runner if runner is not None else (None, None)
|
|
153
|
+
runner_command = _process_command(root, runner_pid) if runner_pid is not None else ""
|
|
154
|
+
host_command = _process_command(root, host_pid)
|
|
155
|
+
if runner_pid is not None and (not runner_command or "codex" not in runner_command.casefold()):
|
|
156
|
+
raise EmergencyRecoveryError("De geregistreerde Codex-runner is niet meer veilig identificeerbaar.")
|
|
157
|
+
if not host_command or ("execution_host" not in host_command and "engineering-execution-host" not in host_command):
|
|
158
|
+
raise EmergencyRecoveryError("De geregistreerde Execution Host is niet meer veilig identificeerbaar.")
|
|
159
|
+
return RecoveryPlan(run_id, branch, baseline_branch, baseline_head, group, host_pid)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def preview(
|
|
163
|
+
root: Path, run_id: object, *, central_database: Path | None = None, project_id: str | None = None,
|
|
164
|
+
) -> dict[str, object]:
|
|
165
|
+
"""Return a display-safe, non-mutating emergency recovery eligibility view."""
|
|
166
|
+
if not isinstance(run_id, str):
|
|
167
|
+
return {"available": False}
|
|
168
|
+
try:
|
|
169
|
+
plan = _plan(root, run_id, central_database=central_database, project_id=project_id)
|
|
170
|
+
except (EmergencyRecoveryError, EngineeringStorageError, OSError):
|
|
171
|
+
return {"available": False}
|
|
172
|
+
return {
|
|
173
|
+
"available": True,
|
|
174
|
+
"run_id": plan.run_id,
|
|
175
|
+
"branch": plan.branch,
|
|
176
|
+
"baseline_branch": plan.baseline_branch,
|
|
177
|
+
"rollback_changes": True,
|
|
178
|
+
"remove_branch": plan.branch != plan.baseline_branch,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _stop(plan: RecoveryPlan, root: Path) -> None:
|
|
183
|
+
try:
|
|
184
|
+
if plan.process_group is not None:
|
|
185
|
+
try:
|
|
186
|
+
os.killpg(plan.process_group, signal.SIGTERM)
|
|
187
|
+
except ProcessLookupError:
|
|
188
|
+
# Codex can finish between preview and confirmation. The
|
|
189
|
+
# leased Execution Host is still the authoritative stop target.
|
|
190
|
+
pass
|
|
191
|
+
os.kill(plan.host_pid, signal.SIGTERM)
|
|
192
|
+
except ProcessLookupError as error:
|
|
193
|
+
raise EmergencyRecoveryError("De uitvoering stopte al voordat de noodactie kon worden uitgevoerd.") from error
|
|
194
|
+
deadline = time.monotonic() + 3
|
|
195
|
+
while time.monotonic() < deadline:
|
|
196
|
+
if not _process_command(root, plan.host_pid):
|
|
197
|
+
return
|
|
198
|
+
time.sleep(0.1)
|
|
199
|
+
raise EmergencyRecoveryError("De Execution Host reageert niet op de noodstop; er is niets teruggedraaid.")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _release_lease(root: Path, run_id: str, *, central_database: Path | None = None) -> None:
|
|
203
|
+
connection = open_storage(root) if central_database is None else sqlite3.connect(central_database.resolve(), isolation_level=None)
|
|
204
|
+
try:
|
|
205
|
+
row = connection.execute(
|
|
206
|
+
"SELECT lease_id,host_identity,host_instance_id,acquired_at,last_heartbeat_at,expires_at,lease_state FROM execution_run_leases WHERE run_id=? AND lease_state='ACTIVE' ORDER BY created_at DESC LIMIT 1",
|
|
207
|
+
(run_id,),
|
|
208
|
+
).fetchone()
|
|
209
|
+
finally:
|
|
210
|
+
connection.close()
|
|
211
|
+
if not row:
|
|
212
|
+
return
|
|
213
|
+
release(root, Lease(row[0], run_id, row[1], row[2], row[3], row[4], row[5], row[6]), central_database=central_database)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def execute(
|
|
217
|
+
root: Path, run_id: str, *, central_database: Path | None = None, project_id: str | None = None,
|
|
218
|
+
) -> dict[str, object]:
|
|
219
|
+
"""Stop exactly one verified host, then restore its clean local baseline."""
|
|
220
|
+
plan = _plan(root, run_id, central_database=central_database, project_id=project_id)
|
|
221
|
+
_stop(plan, root)
|
|
222
|
+
_release_lease(root, run_id, central_database=central_database)
|
|
223
|
+
_git(root, "restore", "--source", plan.baseline_head, "--staged", "--worktree", "--", ".")
|
|
224
|
+
_git(root, "clean", "-fd", "--", ".")
|
|
225
|
+
removed_branch: str | None = None
|
|
226
|
+
if plan.branch != plan.baseline_branch:
|
|
227
|
+
_git(root, "switch", plan.baseline_branch)
|
|
228
|
+
_git(root, "branch", "-D", "--", plan.branch)
|
|
229
|
+
removed_branch = plan.branch
|
|
230
|
+
if _git(root, "rev-parse", "HEAD") != plan.baseline_head or _git(root, "status", "--porcelain", "--untracked-files=all"):
|
|
231
|
+
raise EmergencyRecoveryError("De noodstop is uitgevoerd, maar de werkmap kon niet volledig worden teruggedraaid.")
|
|
232
|
+
write_runner_process(root, run_id, None)
|
|
233
|
+
try:
|
|
234
|
+
state = StateStore(root / ".engineering" / "engineering-runs", central_database=central_database, emit_local_projection=central_database is None).load(run_id)
|
|
235
|
+
except (EngineeringStorageError, ValueError) as error:
|
|
236
|
+
raise EmergencyRecoveryError("De annulering kon niet veilig als eindstatus worden vastgelegd.") from error
|
|
237
|
+
if state is None:
|
|
238
|
+
raise EmergencyRecoveryError("De actieve uitvoering heeft geen canoniek checkpoint.")
|
|
239
|
+
cancelled_at = datetime.now(timezone.utc).isoformat()
|
|
240
|
+
cancelled = replace(
|
|
241
|
+
state, phase="FAILED", terminal=True, next_action="operator_emergency_rollback",
|
|
242
|
+
terminal_condition="operator_emergency_rollback",
|
|
243
|
+
diagnostic="De operator heeft deze uitvoering via de noodstop geannuleerd en de lokale werkmap teruggedraaid.",
|
|
244
|
+
)
|
|
245
|
+
StateStore(root / ".engineering" / "engineering-runs", central_database=central_database, emit_local_projection=central_database is None).save(cancelled)
|
|
246
|
+
complete_active_phase(root, run_id, "TOTAL_EXECUTION", outcome="FAILED", central_database=central_database)
|
|
247
|
+
record_prompt_execution(
|
|
248
|
+
root, run_id=run_id, terminal_state="FAILED", prompt_title=Path(state.prompt_path).stem,
|
|
249
|
+
executed_at=cancelled_at, target_branch=plan.branch,
|
|
250
|
+
central_database=central_database,
|
|
251
|
+
)
|
|
252
|
+
record_execution_dismissal(
|
|
253
|
+
root, run_id=run_id, terminal_state="FAILED", dismissed_at=cancelled_at,
|
|
254
|
+
dismissed_by="dashboard_emergency_recovery",
|
|
255
|
+
central_database=central_database,
|
|
256
|
+
)
|
|
257
|
+
record_emergency_recovery(
|
|
258
|
+
root, run_id=run_id, cancelled_at=cancelled_at, rolled_back=True,
|
|
259
|
+
removed_branch=removed_branch,
|
|
260
|
+
central_database=central_database,
|
|
261
|
+
)
|
|
262
|
+
outcome = {"run_id": run_id, "stopped": True, "rolled_back": True, "removed_branch": removed_branch, "branch": plan.baseline_branch, "cancelled_at": cancelled_at}
|
|
263
|
+
connection = open_storage(root) if central_database is None else sqlite3.connect(central_database.resolve(), isolation_level=None)
|
|
264
|
+
try:
|
|
265
|
+
store_projection(connection, f"emergency_recovery:{run_id}", outcome, classification="RECOVERY_EXPORT")
|
|
266
|
+
finally:
|
|
267
|
+
connection.close()
|
|
268
|
+
return outcome
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Local, advisory Engineering Memory persistence.
|
|
2
|
+
|
|
3
|
+
This module deliberately owns only bounded local memory. The runner remains
|
|
4
|
+
the lifecycle orchestrator and repository evidence remains authoritative.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import tempfile
|
|
14
|
+
|
|
15
|
+
from .agent_state import TransactionState
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _memory_path(root: Path) -> Path:
|
|
19
|
+
return root / ".engineering" / "memory" / "engineering-memory.json"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_engineering_memory(root: Path) -> dict[str, object]:
|
|
23
|
+
try:
|
|
24
|
+
raw = json.loads(_memory_path(root).read_text(encoding="utf-8"))
|
|
25
|
+
return raw if isinstance(raw, dict) else {}
|
|
26
|
+
except (OSError, json.JSONDecodeError):
|
|
27
|
+
return {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def retrieve_engineering_memory(root: Path, prompt_path: Path) -> str:
|
|
31
|
+
"""Return safe advisory metadata; repository evidence remains authoritative."""
|
|
32
|
+
try:
|
|
33
|
+
entries = load_engineering_memory(root).get("transactions", [])
|
|
34
|
+
except AttributeError:
|
|
35
|
+
return "\n\nEngineering Memory: no prior safe transaction metadata is available."
|
|
36
|
+
objective = prompt_path.stem.lower()
|
|
37
|
+
relevant = [
|
|
38
|
+
entry
|
|
39
|
+
for entry in entries[-10:]
|
|
40
|
+
if any(word in objective for word in entry.get("classification", "").split())
|
|
41
|
+
]
|
|
42
|
+
return (
|
|
43
|
+
"\n\nEngineering Memory (advisory only; repository evidence overrides it): "
|
|
44
|
+
+ json.dumps(relevant[-3:], sort_keys=True)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def capture_engineering_memory(
|
|
49
|
+
root: Path, state: TransactionState, reviewer_records: tuple[dict[str, object], ...] = ()
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Atomically store bounded metadata, never prompts, source content or credentials."""
|
|
52
|
+
path = _memory_path(root)
|
|
53
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
54
|
+
raw = load_engineering_memory(root)
|
|
55
|
+
classification = " ".join(
|
|
56
|
+
part
|
|
57
|
+
for part in Path(state.prompt_path).stem.lower().replace("_", "-").split("-")
|
|
58
|
+
if part.isalpha()
|
|
59
|
+
)[:120]
|
|
60
|
+
entry = {
|
|
61
|
+
"classification": classification,
|
|
62
|
+
"repository": state.repository,
|
|
63
|
+
"outcome": state.phase,
|
|
64
|
+
"repair_iterations": state.repair_iterations,
|
|
65
|
+
"implementation_pr": state.implementation_pull_request,
|
|
66
|
+
"finalization_pr": state.finalization_pull_request,
|
|
67
|
+
"confidence": 1.0,
|
|
68
|
+
"usage_count": 0,
|
|
69
|
+
"last_successful_use": datetime.now(timezone.utc).isoformat(),
|
|
70
|
+
}
|
|
71
|
+
reviewer_index = {
|
|
72
|
+
item.get("reviewer"): dict(item)
|
|
73
|
+
for item in raw.get("reviewers", [])
|
|
74
|
+
if isinstance(item, dict) and isinstance(item.get("reviewer"), str)
|
|
75
|
+
}
|
|
76
|
+
for record in reviewer_records:
|
|
77
|
+
reviewer = record.get("reviewer")
|
|
78
|
+
if not isinstance(reviewer, str):
|
|
79
|
+
continue
|
|
80
|
+
previous = reviewer_index.get(reviewer, {})
|
|
81
|
+
usage = int(previous.get("usage_count", 0)) + 1
|
|
82
|
+
successful = int(previous.get("successful_outcomes", 0)) + (
|
|
83
|
+
0 if record.get("failed") else 1
|
|
84
|
+
)
|
|
85
|
+
accepted = int(previous.get("accepted_recommendations", 0)) + int(
|
|
86
|
+
record.get("accepted_recommendations", 0)
|
|
87
|
+
)
|
|
88
|
+
recommended = (
|
|
89
|
+
int(previous.get("recommendation_count", 0))
|
|
90
|
+
+ int(record.get("accepted_recommendations", 0))
|
|
91
|
+
+ int(record.get("rejected_recommendations", 0))
|
|
92
|
+
)
|
|
93
|
+
confidence = round(successful / usage, 2)
|
|
94
|
+
reviewer_index[reviewer] = {
|
|
95
|
+
"reviewer": reviewer,
|
|
96
|
+
"capability": record.get("capability", "engineering"),
|
|
97
|
+
"usage_count": usage,
|
|
98
|
+
"successful_outcomes": successful,
|
|
99
|
+
"accepted_recommendations": accepted,
|
|
100
|
+
"recommendation_count": recommended,
|
|
101
|
+
"recommendation_acceptance_rate": round(accepted / recommended, 2)
|
|
102
|
+
if recommended
|
|
103
|
+
else 0.0,
|
|
104
|
+
"average_duration": 0,
|
|
105
|
+
"last_successful_use": datetime.now(timezone.utc).isoformat()
|
|
106
|
+
if not record.get("failed")
|
|
107
|
+
else previous.get("last_successful_use"),
|
|
108
|
+
"future_confidence": confidence,
|
|
109
|
+
}
|
|
110
|
+
reviewers = list(reviewer_index.values())[-50:]
|
|
111
|
+
raw = {
|
|
112
|
+
"schema_version": 2,
|
|
113
|
+
"transactions": [item for item in raw.get("transactions", []) if isinstance(item, dict)][
|
|
114
|
+
-49:
|
|
115
|
+
]
|
|
116
|
+
+ [entry],
|
|
117
|
+
"reviewers": reviewers,
|
|
118
|
+
"capability_metrics": {
|
|
119
|
+
"most_frequently_used": max(reviewers, key=lambda item: item["usage_count"])["reviewer"]
|
|
120
|
+
if reviewers
|
|
121
|
+
else None,
|
|
122
|
+
"highest_value": max(reviewers, key=lambda item: item["future_confidence"])["reviewer"]
|
|
123
|
+
if reviewers
|
|
124
|
+
else None,
|
|
125
|
+
"repository_areas": sorted(
|
|
126
|
+
{str(item.get("capability", "engineering")) for item in reviewers}
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
descriptor, temporary = tempfile.mkstemp(prefix=".memory.", suffix=".tmp", dir=path.parent)
|
|
131
|
+
try:
|
|
132
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
133
|
+
json.dump(raw, handle, indent=2, sort_keys=True)
|
|
134
|
+
handle.write("\n")
|
|
135
|
+
handle.flush()
|
|
136
|
+
os.fsync(handle.fileno())
|
|
137
|
+
os.replace(temporary, path)
|
|
138
|
+
finally:
|
|
139
|
+
Path(temporary).unlink(missing_ok=True)
|