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,79 @@
|
|
|
1
|
+
"""One-shot migration of historical iCloud Inbox archives.
|
|
2
|
+
|
|
3
|
+
This tool is migration-only: it moves retained evidence into the current
|
|
4
|
+
archive layout and never admits, queues, dispatches or executes work.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import shutil
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _move(source: Path, destination: Path) -> None:
|
|
16
|
+
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
17
|
+
try:
|
|
18
|
+
os.replace(source, destination)
|
|
19
|
+
except OSError:
|
|
20
|
+
shutil.move(str(source), str(destination))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def migrate_icloud_archives(repo: Path, root: Path) -> dict[str, int]:
|
|
24
|
+
"""Move historical archive evidence, leaving no runtime transport behind."""
|
|
25
|
+
targets = {
|
|
26
|
+
"Running": repo / ".engineering" / "inbox" / "Running",
|
|
27
|
+
"Completed": repo / ".engineering" / "inbox" / "Completed",
|
|
28
|
+
"Failed": repo / ".engineering" / "inbox" / "Failed",
|
|
29
|
+
"Reports": repo / ".engineering" / "reports",
|
|
30
|
+
}
|
|
31
|
+
moved = deleted = 0
|
|
32
|
+
for name, target in targets.items():
|
|
33
|
+
source_directory = root / name
|
|
34
|
+
if not source_directory.is_dir():
|
|
35
|
+
continue
|
|
36
|
+
target.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
37
|
+
for source in source_directory.iterdir():
|
|
38
|
+
if source.is_symlink() or not source.is_file():
|
|
39
|
+
continue
|
|
40
|
+
destination = target / source.name
|
|
41
|
+
if destination.exists():
|
|
42
|
+
source.unlink()
|
|
43
|
+
deleted += 1
|
|
44
|
+
else:
|
|
45
|
+
_move(source, destination)
|
|
46
|
+
moved += 1
|
|
47
|
+
# A skipped symlink or non-file is intentionally retained as
|
|
48
|
+
# historical evidence. It must not turn a safe archive migration
|
|
49
|
+
# into a partial failure merely because the source directory is no
|
|
50
|
+
# longer empty.
|
|
51
|
+
try:
|
|
52
|
+
source_directory.rmdir()
|
|
53
|
+
except OSError:
|
|
54
|
+
pass
|
|
55
|
+
for name in ("status.json", "status.md"):
|
|
56
|
+
source, destination = root / name, repo / ".engineering" / "status" / name
|
|
57
|
+
if not source.is_file() or source.is_symlink():
|
|
58
|
+
continue
|
|
59
|
+
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
60
|
+
if destination.exists():
|
|
61
|
+
source.unlink()
|
|
62
|
+
deleted += 1
|
|
63
|
+
else:
|
|
64
|
+
_move(source, destination)
|
|
65
|
+
moved += 1
|
|
66
|
+
return {"moved": moved, "deleted_duplicates": deleted}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main(argv: list[str] | None = None) -> int:
|
|
70
|
+
parser = argparse.ArgumentParser()
|
|
71
|
+
parser.add_argument("--repo", type=Path, required=True)
|
|
72
|
+
parser.add_argument("--icloud-root", type=Path, required=True)
|
|
73
|
+
args = parser.parse_args(argv)
|
|
74
|
+
print(json.dumps(migrate_icloud_archives(args.repo.resolve(), args.icloud_root.resolve()), sort_keys=True))
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Installed CENTRAL lifecycle-worker composition.
|
|
2
|
+
|
|
3
|
+
The worker deliberately has a very small authority: it observes CENTRAL's
|
|
4
|
+
durable submission records and asks :class:`ParityLifecycleDispatcher` to
|
|
5
|
+
continue one eligible item. Claiming, run identity, historical admission,
|
|
6
|
+
recovery, provider invocation and finalization remain in that dispatcher and
|
|
7
|
+
the preserved EngineeringRunner.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
import sqlite3
|
|
13
|
+
from threading import Event, Lock, Thread
|
|
14
|
+
from time import monotonic
|
|
15
|
+
from typing import Callable, Protocol
|
|
16
|
+
|
|
17
|
+
from .parity_lifecycle_dispatcher import ParityLifecycleDispatcher
|
|
18
|
+
from . import central_database
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
WORKER_RUNNING = "RUNNING"
|
|
22
|
+
WORKER_STOPPED = "STOPPED"
|
|
23
|
+
WORKER_DEGRADED = "DEGRADED"
|
|
24
|
+
OPERATOR_MERGE_RESUME_SECONDS = 60.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Dispatcher(Protocol):
|
|
28
|
+
def dispatch(self, submission_id: str) -> object: ...
|
|
29
|
+
def reconcile_terminal_history(self) -> None: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
DispatcherFactory = Callable[[], Dispatcher]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class LifecycleWorkerDiagnostics:
|
|
37
|
+
state: str
|
|
38
|
+
observed: int
|
|
39
|
+
dispatched: int
|
|
40
|
+
failures: int
|
|
41
|
+
last_submission_id: str | None
|
|
42
|
+
last_error: str | None
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict[str, object]:
|
|
45
|
+
return {
|
|
46
|
+
"state": self.state,
|
|
47
|
+
"observed": self.observed,
|
|
48
|
+
"dispatched": self.dispatched,
|
|
49
|
+
"failures": self.failures,
|
|
50
|
+
"last_submission_id": self.last_submission_id,
|
|
51
|
+
"last_error": self.last_error,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class LifecycleWorker:
|
|
56
|
+
"""One installation-owned observer with one active lifecycle per project."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, data_root, *, dispatcher_factory: DispatcherFactory | None = None,
|
|
59
|
+
idle_seconds: float = 0.25, failure_seconds: float = 1.0) -> None:
|
|
60
|
+
self.data_root = data_root.resolve()
|
|
61
|
+
self._dispatcher_factory = dispatcher_factory or (lambda: ParityLifecycleDispatcher(self.data_root))
|
|
62
|
+
self._idle_seconds = idle_seconds
|
|
63
|
+
self._failure_seconds = failure_seconds
|
|
64
|
+
self._stop = Event()
|
|
65
|
+
self._ready = Event()
|
|
66
|
+
self._lock = Lock()
|
|
67
|
+
self._thread: Thread | None = None
|
|
68
|
+
self._inflight: set[str] = set()
|
|
69
|
+
self._next_merge_resume_at: dict[str, float] = {}
|
|
70
|
+
self._diagnostics = LifecycleWorkerDiagnostics(WORKER_STOPPED, 0, 0, 0, None, None)
|
|
71
|
+
|
|
72
|
+
def diagnostics(self) -> LifecycleWorkerDiagnostics:
|
|
73
|
+
with self._lock:
|
|
74
|
+
return self._diagnostics
|
|
75
|
+
|
|
76
|
+
def _replace(self, **changes: object) -> None:
|
|
77
|
+
with self._lock:
|
|
78
|
+
values = self._diagnostics.to_dict()
|
|
79
|
+
values.update(changes)
|
|
80
|
+
self._diagnostics = LifecycleWorkerDiagnostics(**values) # type: ignore[arg-type]
|
|
81
|
+
|
|
82
|
+
def eligible_submission_ids(self) -> list[str]:
|
|
83
|
+
"""Return one FIFO candidate per project; claims remain dispatcher-owned."""
|
|
84
|
+
with sqlite3.connect(self.data_root / "engineering.db") as connection:
|
|
85
|
+
rows = connection.execute("""SELECT s.submission_id,s.project_id,d.state,d.claimed_at,s.created_at
|
|
86
|
+
FROM ep_submissions s
|
|
87
|
+
LEFT JOIN ep_parity_lifecycle_dispatches d ON d.submission_id=s.submission_id
|
|
88
|
+
WHERE s.state='QUEUED' AND s.admission='ADMITTED'
|
|
89
|
+
AND (
|
|
90
|
+
d.submission_id IS NULL OR d.state IN ('CLAIMED','RUNNING')
|
|
91
|
+
OR (
|
|
92
|
+
d.state='BLOCKED' AND d.operator_resolution='OPEN'
|
|
93
|
+
AND EXISTS (
|
|
94
|
+
SELECT 1 FROM engineering_transactions wait_state
|
|
95
|
+
WHERE wait_state.run_id=d.run_id
|
|
96
|
+
AND wait_state.phase='WAIT_FOR_OPERATOR_MERGE'
|
|
97
|
+
AND COALESCE(json_extract(wait_state.payload, '$.terminal'), 0) IN (0, 'false')
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
AND NOT EXISTS (
|
|
102
|
+
SELECT 1 FROM ep_parity_lifecycle_dispatches prior
|
|
103
|
+
WHERE prior.project_id=s.project_id AND (
|
|
104
|
+
(prior.state IN ('CLAIMED','RUNNING') AND prior.submission_id!=s.submission_id)
|
|
105
|
+
OR (prior.state IN ('BLOCKED','FAILED') AND prior.operator_resolution='OPEN'
|
|
106
|
+
AND prior.submission_id!=s.submission_id)
|
|
107
|
+
OR (prior.operator_resolution='RETRIED' AND prior.resolution_submission_id!=s.submission_id)
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
ORDER BY s.project_id,
|
|
111
|
+
CASE WHEN d.state IN ('CLAIMED','RUNNING') THEN 0 ELSE 1 END,
|
|
112
|
+
COALESCE(d.claimed_at,s.created_at),s.created_at,s.submission_id""").fetchall()
|
|
113
|
+
candidates: list[str] = []
|
|
114
|
+
projects: set[str] = set()
|
|
115
|
+
for submission_id, project_id, _state, _claimed_at, _created_at in rows:
|
|
116
|
+
if str(project_id) in projects:
|
|
117
|
+
continue
|
|
118
|
+
projects.add(str(project_id))
|
|
119
|
+
candidates.append(str(submission_id))
|
|
120
|
+
return candidates
|
|
121
|
+
|
|
122
|
+
def _dispatch(self, submission_id: str) -> None:
|
|
123
|
+
try:
|
|
124
|
+
self._dispatcher_factory().dispatch(submission_id)
|
|
125
|
+
except Exception as error: # Dispatcher persists its own terminal/recovery boundary.
|
|
126
|
+
current = self.diagnostics()
|
|
127
|
+
self._replace(state=WORKER_DEGRADED, failures=current.failures + 1,
|
|
128
|
+
last_error=type(error).__name__)
|
|
129
|
+
else:
|
|
130
|
+
current = self.diagnostics()
|
|
131
|
+
self._replace(state=WORKER_RUNNING, dispatched=current.dispatched + 1)
|
|
132
|
+
finally:
|
|
133
|
+
with self._lock:
|
|
134
|
+
self._inflight.discard(submission_id)
|
|
135
|
+
|
|
136
|
+
def run_once(self) -> bool:
|
|
137
|
+
with self._lock:
|
|
138
|
+
inflight = frozenset(self._inflight)
|
|
139
|
+
now = monotonic()
|
|
140
|
+
candidates = [
|
|
141
|
+
submission_id for submission_id in self.eligible_submission_ids()
|
|
142
|
+
if submission_id not in inflight
|
|
143
|
+
and now >= self._next_merge_resume_at.get(submission_id, 0.0)
|
|
144
|
+
]
|
|
145
|
+
if not candidates:
|
|
146
|
+
return False
|
|
147
|
+
for submission_id in candidates:
|
|
148
|
+
current = self.diagnostics()
|
|
149
|
+
self._replace(observed=current.observed + 1, last_submission_id=submission_id, last_error=None)
|
|
150
|
+
with self._lock:
|
|
151
|
+
self._inflight.add(submission_id)
|
|
152
|
+
# A nonterminal merge wait returns quickly after its one
|
|
153
|
+
# authoritative remote poll. Keep that same canonical run
|
|
154
|
+
# resumable, but never race it with another local lease
|
|
155
|
+
# acquisition before the next bounded poll window.
|
|
156
|
+
self._next_merge_resume_at[submission_id] = now + OPERATOR_MERGE_RESUME_SECONDS
|
|
157
|
+
Thread(
|
|
158
|
+
target=self._dispatch,
|
|
159
|
+
args=(submission_id,),
|
|
160
|
+
name=f"engineering-platform-lifecycle-{submission_id[:12]}",
|
|
161
|
+
daemon=True,
|
|
162
|
+
).start()
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
def _loop(self) -> None:
|
|
166
|
+
self._replace(state=WORKER_RUNNING)
|
|
167
|
+
self._ready.set()
|
|
168
|
+
while not self._stop.is_set():
|
|
169
|
+
# CENTRAL is the sole operational store. Its maintenance routine
|
|
170
|
+
# is interval-bound and skips every active lifecycle.
|
|
171
|
+
central_database.run_periodic_maintenance(self.data_root)
|
|
172
|
+
self.run_once()
|
|
173
|
+
# Even a successful nonterminal continuation gets a short yield:
|
|
174
|
+
# recovery must never become a busy loop if the historical runner
|
|
175
|
+
# deliberately returns a resumable checkpoint.
|
|
176
|
+
self._stop.wait(self._failure_seconds if self.diagnostics().state == WORKER_DEGRADED else self._idle_seconds)
|
|
177
|
+
self._replace(state=WORKER_STOPPED)
|
|
178
|
+
|
|
179
|
+
def _reconcile_terminal_history(self) -> None:
|
|
180
|
+
"""Backfill historical Console rows without delaying Server readiness."""
|
|
181
|
+
reconcile = getattr(self._dispatcher_factory(), "reconcile_terminal_history", None)
|
|
182
|
+
if not callable(reconcile):
|
|
183
|
+
return
|
|
184
|
+
try:
|
|
185
|
+
reconcile()
|
|
186
|
+
except Exception as error:
|
|
187
|
+
# Terminal-history projection is additive only. A stale retained
|
|
188
|
+
# row must not prevent the HTTP Server from accepting fresh
|
|
189
|
+
# CENTRAL submissions or prevent the worker from servicing a
|
|
190
|
+
# project queue.
|
|
191
|
+
current = self.diagnostics()
|
|
192
|
+
self._replace(failures=current.failures + 1, last_error=type(error).__name__)
|
|
193
|
+
|
|
194
|
+
def start(self) -> None:
|
|
195
|
+
if self._thread is not None and self._thread.is_alive():
|
|
196
|
+
return
|
|
197
|
+
self._stop.clear()
|
|
198
|
+
self._ready.clear()
|
|
199
|
+
self._thread = Thread(target=self._loop, name="engineering-platform-lifecycle-worker", daemon=True)
|
|
200
|
+
self._thread.start()
|
|
201
|
+
# CENTRAL owns claims; the preserved Console owns terminal evidence.
|
|
202
|
+
# Historical projection is additive, so it must never make Server
|
|
203
|
+
# readiness depend on an old report or a retained stale row.
|
|
204
|
+
Thread(
|
|
205
|
+
target=self._reconcile_terminal_history,
|
|
206
|
+
name="engineering-platform-terminal-history-reconciliation",
|
|
207
|
+
daemon=True,
|
|
208
|
+
).start()
|
|
209
|
+
|
|
210
|
+
def wait_until_running(self, timeout: float = 5.0) -> bool:
|
|
211
|
+
"""Wait for the child loop's real readiness transition.
|
|
212
|
+
|
|
213
|
+
Server-owned producers depend on lifecycle admission being available;
|
|
214
|
+
this is a composition boundary, not a timing heuristic.
|
|
215
|
+
"""
|
|
216
|
+
return self._ready.wait(timeout) and self.diagnostics().state == WORKER_RUNNING
|
|
217
|
+
|
|
218
|
+
def stop(self, timeout: float = 2.0) -> None:
|
|
219
|
+
self._stop.set()
|
|
220
|
+
if self._thread is not None:
|
|
221
|
+
self._thread.join(timeout)
|
|
222
|
+
if self._thread is None or not self._thread.is_alive():
|
|
223
|
+
self._replace(state=WORKER_STOPPED)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Atomic local status projection for the foreground engineering runner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import tempfile
|
|
10
|
+
from typing import Mapping
|
|
11
|
+
|
|
12
|
+
from .agent_state import TransactionState, redact_diagnostic
|
|
13
|
+
from .storage import EngineeringStorageError, load_execution_context_snapshot, load_forge_governance_handoff_snapshot, load_projection, open_storage, store_projection
|
|
14
|
+
from .providers import GitProvider
|
|
15
|
+
from .execution_activity import cumulative_activity, live_worktree_snapshot
|
|
16
|
+
|
|
17
|
+
_REVIEWER_PROJECTION_PHASE = "CAPABILITY_REVIEW"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _successful_reviewer_agents(value: object) -> list[dict[str, object]]:
|
|
21
|
+
"""Return immutable review evidence only after every reviewer succeeded."""
|
|
22
|
+
if not isinstance(value, list):
|
|
23
|
+
return []
|
|
24
|
+
reviewers = [item for item in value if isinstance(item, dict)]
|
|
25
|
+
if not reviewers or len(reviewers) != len(value):
|
|
26
|
+
return []
|
|
27
|
+
return reviewers if all(item.get("status") == "completed" for item in reviewers) else []
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_live_status(
|
|
31
|
+
root: Path,
|
|
32
|
+
state: TransactionState,
|
|
33
|
+
action: str,
|
|
34
|
+
reviewer_agents: list[dict[str, object]] | None = None,
|
|
35
|
+
runtime_metadata: Mapping[str, str] | None = None,
|
|
36
|
+
workspace_progress: Mapping[str, int] | None = None,
|
|
37
|
+
transient_action: str | None = None,
|
|
38
|
+
) -> Path:
|
|
39
|
+
"""Atomically publish the advisory current transaction state."""
|
|
40
|
+
directory = root / ".engineering" / "status"
|
|
41
|
+
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
42
|
+
path = directory / "current.json"
|
|
43
|
+
checkout = (
|
|
44
|
+
Path(state.genesis_repository_path).expanduser()
|
|
45
|
+
if state.execution_mode == "GENESIS" and state.genesis_repository_path
|
|
46
|
+
else root
|
|
47
|
+
)
|
|
48
|
+
try:
|
|
49
|
+
observed_branch = GitProvider().execute(checkout, "git", "branch", "--show-current").stdout.strip()
|
|
50
|
+
except OSError:
|
|
51
|
+
observed_branch = ""
|
|
52
|
+
try:
|
|
53
|
+
prompt_characters = len(Path(state.prompt_path).read_text(encoding="utf-8"))
|
|
54
|
+
except OSError:
|
|
55
|
+
prompt_characters = None
|
|
56
|
+
previous_reviewers: list[dict[str, object]] = []
|
|
57
|
+
previous_runtime: dict[str, str] = {}
|
|
58
|
+
previous_transient_action: str | None = None
|
|
59
|
+
previous_recovery: dict[str, object] | None = None
|
|
60
|
+
try:
|
|
61
|
+
previous_file = json.loads(path.read_text(encoding="utf-8"))
|
|
62
|
+
if previous_file.get("run_id") == state.run_id and isinstance(previous_file.get("transient_action"), str):
|
|
63
|
+
previous_transient_action = redact_diagnostic(previous_file["transient_action"], limit=160)
|
|
64
|
+
except (OSError, json.JSONDecodeError):
|
|
65
|
+
pass
|
|
66
|
+
try:
|
|
67
|
+
previous = load_projection(root, "live_status") or {}
|
|
68
|
+
except EngineeringStorageError:
|
|
69
|
+
# A live operation must not publish a filesystem-only state if the
|
|
70
|
+
# canonical store is unavailable.
|
|
71
|
+
raise
|
|
72
|
+
reviewer_projection_active = state.phase == _REVIEWER_PROJECTION_PHASE
|
|
73
|
+
if previous.get("run_id") == state.run_id:
|
|
74
|
+
stored_reviewers = previous.get("reviewer_agents")
|
|
75
|
+
if (
|
|
76
|
+
reviewer_projection_active
|
|
77
|
+
and reviewer_agents is None
|
|
78
|
+
and isinstance(stored_reviewers, list)
|
|
79
|
+
):
|
|
80
|
+
previous_reviewers = [item for item in stored_reviewers if isinstance(item, dict)]
|
|
81
|
+
elif not reviewer_projection_active:
|
|
82
|
+
# Once the review is wholly successful, retain its compact,
|
|
83
|
+
# completed result as historical evidence for this live run. A
|
|
84
|
+
# partial, failed or malformed projection is never carried into a
|
|
85
|
+
# later lifecycle phase.
|
|
86
|
+
previous_reviewers = _successful_reviewer_agents(stored_reviewers)
|
|
87
|
+
if runtime_metadata is None and isinstance(previous.get("runtime_metadata"), dict):
|
|
88
|
+
previous_runtime = {
|
|
89
|
+
key: value[:120]
|
|
90
|
+
for key, value in previous["runtime_metadata"].items()
|
|
91
|
+
if key in {"runtime_provider", "model", "reasoning_profile", "configuration_profile", "codex_cli_installation_path"}
|
|
92
|
+
and isinstance(value, str)
|
|
93
|
+
}
|
|
94
|
+
if isinstance(previous.get("workspace_recovery"), dict):
|
|
95
|
+
previous_recovery = dict(previous["workspace_recovery"])
|
|
96
|
+
safe_runtime = previous_runtime if runtime_metadata is None else {
|
|
97
|
+
key: value[:120]
|
|
98
|
+
for key, value in runtime_metadata.items()
|
|
99
|
+
if key in {"runtime_provider", "model", "reasoning_profile", "configuration_profile", "codex_cli_installation_path"}
|
|
100
|
+
and isinstance(value, str)
|
|
101
|
+
}
|
|
102
|
+
previous_progress = previous.get("workspace_progress") if previous.get("run_id") == state.run_id else None
|
|
103
|
+
safe_progress = previous_progress if workspace_progress is None else workspace_progress
|
|
104
|
+
if not isinstance(safe_progress, Mapping):
|
|
105
|
+
safe_progress = {"modified": 0, "created": 0, "deleted": 0}
|
|
106
|
+
safe_progress = {
|
|
107
|
+
key: max(0, int(safe_progress.get(key, 0)))
|
|
108
|
+
for key in ("modified", "created", "deleted", "codex_commands_executed")
|
|
109
|
+
if isinstance(safe_progress.get(key, 0), int)
|
|
110
|
+
}
|
|
111
|
+
if previous_recovery is None:
|
|
112
|
+
try:
|
|
113
|
+
provider = GitProvider()
|
|
114
|
+
head_result = provider.execute(checkout, "git", "rev-parse", "HEAD")
|
|
115
|
+
status_result = provider.execute(checkout, "git", "status", "--porcelain", "--untracked-files=all")
|
|
116
|
+
branches_result = provider.execute(checkout, "git", "for-each-ref", "--format=%(refname:short)", "refs/heads")
|
|
117
|
+
if head_result.returncode or status_result.returncode or branches_result.returncode:
|
|
118
|
+
raise OSError("Git recovery baseline is unavailable")
|
|
119
|
+
baseline_head = head_result.stdout.strip()
|
|
120
|
+
baseline_status = status_result.stdout.strip()
|
|
121
|
+
branches = branches_result.stdout.splitlines()
|
|
122
|
+
previous_recovery = {
|
|
123
|
+
"baseline_branch": observed_branch,
|
|
124
|
+
"baseline_head": baseline_head,
|
|
125
|
+
"baseline_clean": not baseline_status,
|
|
126
|
+
"preexisting_branches": [branch for branch in branches if branch],
|
|
127
|
+
}
|
|
128
|
+
except OSError:
|
|
129
|
+
previous_recovery = {"baseline_clean": False}
|
|
130
|
+
terminal_phase = state.phase in {"COMPLETE", "BLOCKED", "FAILED"}
|
|
131
|
+
# SQLite recovery evidence is authoritative across host restarts. The
|
|
132
|
+
# checkpoint ledger is retained only as a compatibility projection.
|
|
133
|
+
try:
|
|
134
|
+
from .provider_recovery import load_recovery_state
|
|
135
|
+
recovery_record = load_recovery_state(root, state.run_id)
|
|
136
|
+
except EngineeringStorageError:
|
|
137
|
+
recovery_record = None
|
|
138
|
+
if recovery_record is None:
|
|
139
|
+
recovery_record = state.provider_recovery_attempts[0] if state.provider_recovery_attempts else None
|
|
140
|
+
recovery_result = recovery_record.get("result") if isinstance(recovery_record, dict) else None
|
|
141
|
+
recovery_state = recovery_record.get("state") if isinstance(recovery_record, dict) else None
|
|
142
|
+
provider_recovery = {
|
|
143
|
+
"state": (
|
|
144
|
+
"RECOVERING" if recovery_state in {"RECOVERY_AVAILABLE", "RECOVERY_STARTING", "RECOVERY_IN_PROGRESS"} or recovery_result == "ACTIVE" else
|
|
145
|
+
"RECOVERED" if recovery_state == "RECOVERED" or recovery_result == "RECOVERED" else
|
|
146
|
+
"EXHAUSTED" if recovery_state == "EXHAUSTED" or recovery_result == "INTERRUPTED_AGAIN" else
|
|
147
|
+
"NOT_APPLICABLE"
|
|
148
|
+
),
|
|
149
|
+
"automatic_recovery_attempt": "1/1" if recovery_record else "0/1",
|
|
150
|
+
"trigger": (recovery_record.get("classification") or "provider_turn_interrupted") if isinstance(recovery_record, dict) else None,
|
|
151
|
+
"original_provider_invocation": (recovery_record.get("triggering_invocation_id") or recovery_record.get("original_invocation_id")) if isinstance(recovery_record, dict) else None,
|
|
152
|
+
"replacement_provider_invocation": recovery_record.get("replacement_invocation_id") if isinstance(recovery_record, dict) else None,
|
|
153
|
+
"result": recovery_result,
|
|
154
|
+
}
|
|
155
|
+
payload = {
|
|
156
|
+
"run_id": state.run_id,
|
|
157
|
+
"phase": state.phase,
|
|
158
|
+
"current_action": redact_diagnostic(action),
|
|
159
|
+
"objective": state.prompt_path,
|
|
160
|
+
"implementation_pr": state.implementation_pull_request,
|
|
161
|
+
"finalization_pr": state.finalization_pull_request,
|
|
162
|
+
"reconciliation_pr": state.reconciliation_pull_request,
|
|
163
|
+
"pull_request": state.pull_request,
|
|
164
|
+
"waiting_for_merge_since": state.waiting_for_merge_since,
|
|
165
|
+
"repair_iteration": state.repair_iterations,
|
|
166
|
+
"repository_state": "MERGED_RECONCILED" if state.phase == "COMPLETE" else state.phase if terminal_phase else "ACTIVE",
|
|
167
|
+
"workspace_state": "WORKSPACE_READY" if state.phase == "COMPLETE" else "TERMINAL" if terminal_phase else "ACTIVE",
|
|
168
|
+
"last_update": datetime.now(timezone.utc).isoformat(),
|
|
169
|
+
"elapsed_seconds": 0,
|
|
170
|
+
"prompt_characters": prompt_characters,
|
|
171
|
+
"diagnostic": state.diagnostic,
|
|
172
|
+
"resume_command": None if terminal_phase else f"engineering-execution-host {state.prompt_path} --run-id {state.run_id} --resume",
|
|
173
|
+
"execution_mode": state.execution_mode,
|
|
174
|
+
"target_repository": checkout.name if state.execution_mode == "GENESIS" else state.repository,
|
|
175
|
+
"checkout_path": str(checkout),
|
|
176
|
+
"active_branch": observed_branch or state.branch or "unavailable",
|
|
177
|
+
# Reviewer progress is phase-scoped, while a fully successful review
|
|
178
|
+
# remains compact historical evidence during the rest of this active
|
|
179
|
+
# run. The dashboard renders that retained list as completed, never as
|
|
180
|
+
# live work.
|
|
181
|
+
"reviewer_agents": (
|
|
182
|
+
reviewer_agents if reviewer_projection_active and reviewer_agents is not None
|
|
183
|
+
else previous_reviewers
|
|
184
|
+
),
|
|
185
|
+
"runtime_metadata": safe_runtime,
|
|
186
|
+
"workspace_progress": safe_progress,
|
|
187
|
+
# This is intentionally absent from a terminal receipt/report. It is
|
|
188
|
+
# a current dirty-worktree observation, not delivery evidence.
|
|
189
|
+
"live_worktree_snapshot": None if terminal_phase else live_worktree_snapshot(checkout),
|
|
190
|
+
"cumulative_activity": cumulative_activity(root, state.run_id),
|
|
191
|
+
# A recovery baseline is captured once, after admission has proven the
|
|
192
|
+
# workspace clean. It allows the emergency control to fail closed when
|
|
193
|
+
# a run has commits, a pre-existing branch, or an unknown base.
|
|
194
|
+
"workspace_recovery": previous_recovery,
|
|
195
|
+
"provider_recovery": provider_recovery,
|
|
196
|
+
}
|
|
197
|
+
transient = None if state.terminal or terminal_phase else transient_action or previous_transient_action
|
|
198
|
+
try:
|
|
199
|
+
payload["execution_context"] = load_execution_context_snapshot(root, state.run_id)
|
|
200
|
+
payload["forge_governance_handoff"] = load_forge_governance_handoff_snapshot(root, state.run_id)
|
|
201
|
+
except EngineeringStorageError:
|
|
202
|
+
payload["execution_context"] = None
|
|
203
|
+
payload["forge_governance_handoff"] = None
|
|
204
|
+
connection = open_storage(root)
|
|
205
|
+
try:
|
|
206
|
+
store_projection(connection, "live_status", payload)
|
|
207
|
+
finally:
|
|
208
|
+
connection.close()
|
|
209
|
+
# This action title is intentionally absent from the stored projection.
|
|
210
|
+
# It is a short-lived UI hint sourced from Codex reasoning metadata only.
|
|
211
|
+
file_payload = dict(payload)
|
|
212
|
+
if transient:
|
|
213
|
+
file_payload["transient_action"] = transient
|
|
214
|
+
descriptor, temporary = tempfile.mkstemp(prefix=".current.", suffix=".tmp", dir=directory)
|
|
215
|
+
try:
|
|
216
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
217
|
+
json.dump(file_payload, handle, indent=2, sort_keys=True)
|
|
218
|
+
handle.write("\n")
|
|
219
|
+
handle.flush()
|
|
220
|
+
os.fsync(handle.fileno())
|
|
221
|
+
os.replace(temporary, path)
|
|
222
|
+
finally:
|
|
223
|
+
Path(temporary).unlink(missing_ok=True)
|
|
224
|
+
return path
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def print_live_status(root: Path) -> int:
|
|
228
|
+
try:
|
|
229
|
+
current = load_projection(root, "live_status")
|
|
230
|
+
except EngineeringStorageError:
|
|
231
|
+
print("Current engineering status is unavailable.")
|
|
232
|
+
return 2
|
|
233
|
+
if current is None:
|
|
234
|
+
print("No active engineering status is available.")
|
|
235
|
+
return 1
|
|
236
|
+
print(
|
|
237
|
+
f"Run:\n{current['run_id']}\n\nCurrent Phase:\n{current['phase']}\n\nImplementation PR:\n{current['implementation_pr']}\n\nRepair Iteration:\n{current['repair_iteration']}\n\nCurrent Action:\n{current['current_action']}\n\nElapsed:\n{current['elapsed_seconds']}s"
|
|
238
|
+
)
|
|
239
|
+
return 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def write_runner_process(root: Path, run_id: str, process: Mapping[str, int] | None) -> None:
|
|
243
|
+
"""Atomically record only the Execution Host-owned Codex process group."""
|
|
244
|
+
directory = root / ".engineering" / "status"
|
|
245
|
+
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
246
|
+
path = directory / "runner_process.json"
|
|
247
|
+
if process is None:
|
|
248
|
+
try:
|
|
249
|
+
recorded = json.loads(path.read_text(encoding="utf-8"))
|
|
250
|
+
except (OSError, json.JSONDecodeError):
|
|
251
|
+
recorded = {}
|
|
252
|
+
if recorded.get("run_id") == run_id:
|
|
253
|
+
path.unlink(missing_ok=True)
|
|
254
|
+
return
|
|
255
|
+
pid, process_group = process.get("pid"), process.get("process_group")
|
|
256
|
+
if not isinstance(pid, int) or pid <= 0 or not isinstance(process_group, int) or process_group <= 0:
|
|
257
|
+
return
|
|
258
|
+
descriptor, temporary = tempfile.mkstemp(prefix=".runner-process.", suffix=".tmp", dir=directory)
|
|
259
|
+
try:
|
|
260
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
261
|
+
json.dump({"run_id": run_id, "pid": pid, "process_group": process_group}, handle, sort_keys=True)
|
|
262
|
+
handle.write("\n")
|
|
263
|
+
handle.flush()
|
|
264
|
+
os.fsync(handle.fileno())
|
|
265
|
+
os.replace(temporary, path)
|
|
266
|
+
finally:
|
|
267
|
+
Path(temporary).unlink(missing_ok=True)
|