codex-rescue 0.1.0a1__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.
- codex_rescue/__init__.py +3 -0
- codex_rescue/artifacts.py +51 -0
- codex_rescue/cli.py +178 -0
- codex_rescue/discovery.py +473 -0
- codex_rescue/doctor.py +67 -0
- codex_rescue/fixtures.py +163 -0
- codex_rescue/gitstate.py +132 -0
- codex_rescue/harness.py +75 -0
- codex_rescue/hooks.py +125 -0
- codex_rescue/journal.py +83 -0
- codex_rescue/reconstruct.py +260 -0
- codex_rescue/salvage.py +90 -0
- codex_rescue/transcript.py +216 -0
- codex_rescue/verify.py +162 -0
- codex_rescue-0.1.0a1.dist-info/METADATA +200 -0
- codex_rescue-0.1.0a1.dist-info/RECORD +20 -0
- codex_rescue-0.1.0a1.dist-info/WHEEL +5 -0
- codex_rescue-0.1.0a1.dist-info/entry_points.txt +2 -0
- codex_rescue-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- codex_rescue-0.1.0a1.dist-info/top_level.txt +1 -0
codex_rescue/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def canonical_json(data: Any) -> bytes:
|
|
11
|
+
return json.dumps(data, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def atomic_write(path: Path, data: bytes) -> None:
|
|
15
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
temp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
17
|
+
fd = os.open(temp, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
18
|
+
try:
|
|
19
|
+
offset = 0
|
|
20
|
+
while offset < len(data):
|
|
21
|
+
offset += os.write(fd, data[offset:])
|
|
22
|
+
os.fsync(fd)
|
|
23
|
+
finally:
|
|
24
|
+
os.close(fd)
|
|
25
|
+
os.replace(temp, path)
|
|
26
|
+
if os.name != "nt":
|
|
27
|
+
dir_fd = os.open(path.parent, os.O_RDONLY)
|
|
28
|
+
try:
|
|
29
|
+
os.fsync(dir_fd)
|
|
30
|
+
finally:
|
|
31
|
+
os.close(dir_fd)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_rescue(root: Path, handoff: dict[str, Any], brief: str, continuation: str) -> tuple[str, Path]:
|
|
35
|
+
handoff_bytes = canonical_json(handoff)
|
|
36
|
+
rescue_id = hashlib.sha256(handoff_bytes).hexdigest()[:24]
|
|
37
|
+
rescue_dir = root / "rescues" / rescue_id
|
|
38
|
+
atomic_write(rescue_dir / "handoff.v1.json", handoff_bytes + b"\n")
|
|
39
|
+
atomic_write(rescue_dir / "RECOVERY_BRIEF.md", brief.encode("utf-8"))
|
|
40
|
+
atomic_write(rescue_dir / "CONTINUATION_PROMPT.md", continuation.encode("utf-8"))
|
|
41
|
+
return rescue_id, rescue_dir
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_handoff(root: Path, rescue_id: str) -> dict[str, Any]:
|
|
45
|
+
path = root / "rescues" / rescue_id / "handoff.v1.json"
|
|
46
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
47
|
+
actual_id = hashlib.sha256(canonical_json(data)).hexdigest()[:24]
|
|
48
|
+
if actual_id != rescue_id:
|
|
49
|
+
raise ValueError(f"handoff hash mismatch: expected {rescue_id}, actual {actual_id}")
|
|
50
|
+
return data
|
|
51
|
+
|
codex_rescue/cli.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .salvage import salvage_session
|
|
10
|
+
from .verify import verify_rescue
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _json(data: object) -> None:
|
|
14
|
+
print(json.dumps({"schema_version": 1, "data": data}, indent=2, ensure_ascii=False, sort_keys=True))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _doctor(path: Path, oversized_threshold: int = 1_000_000):
|
|
18
|
+
from .doctor import doctor_session
|
|
19
|
+
|
|
20
|
+
result = doctor_session(path, oversized_threshold=oversized_threshold)
|
|
21
|
+
return result
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _to_dict(value: object) -> dict[str, object]:
|
|
25
|
+
if hasattr(value, "to_dict"):
|
|
26
|
+
return value.to_dict() # type: ignore[no-any-return,attr-defined]
|
|
27
|
+
if isinstance(value, dict):
|
|
28
|
+
return value
|
|
29
|
+
return value.__dict__.copy() # type: ignore[attr-defined]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parsed_from_doctor(result: object):
|
|
33
|
+
for name in ("transcript", "parse_result", "parsed"):
|
|
34
|
+
parsed = getattr(result, name, None)
|
|
35
|
+
if parsed is not None:
|
|
36
|
+
return parsed
|
|
37
|
+
if isinstance(result, dict):
|
|
38
|
+
for name in ("transcript", "parse_result", "parsed"):
|
|
39
|
+
if result.get(name) is not None:
|
|
40
|
+
return result[name]
|
|
41
|
+
raise RuntimeError("doctor result does not expose transcript parse result")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _print_doctor(result: object) -> None:
|
|
45
|
+
data = _to_dict(result)
|
|
46
|
+
print(f"Doctor: {data.get('status', 'UNKNOWN_CORRUPTION')}")
|
|
47
|
+
session = data.get("session")
|
|
48
|
+
if session:
|
|
49
|
+
print(f"Session: {session}")
|
|
50
|
+
findings = list(data.get("findings") or [])
|
|
51
|
+
if findings:
|
|
52
|
+
print(f"Findings: {', '.join(str(item) for item in findings)}")
|
|
53
|
+
repository = data.get("repository")
|
|
54
|
+
if isinstance(repository, dict):
|
|
55
|
+
cwd = repository.get("cwd")
|
|
56
|
+
head = repository.get("head_sha")
|
|
57
|
+
if cwd or head:
|
|
58
|
+
print(f"Repository: {cwd or 'unknown'} (HEAD {head or 'unknown'})")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _print_salvage(result: object) -> None:
|
|
62
|
+
data = _to_dict(result)
|
|
63
|
+
print(f"Salvage: {data.get('rescue_id', 'unknown')}")
|
|
64
|
+
untouched = data.get("original_untouched")
|
|
65
|
+
print(f"Original session untouched: {'yes' if untouched else 'no'}")
|
|
66
|
+
if data.get("rescue_dir"):
|
|
67
|
+
print(f"Rescue directory: {data['rescue_dir']}")
|
|
68
|
+
if data.get("continuation_command"):
|
|
69
|
+
print(f"Continue: {data['continuation_command']}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _print_verify(result: object) -> None:
|
|
73
|
+
data = _to_dict(result)
|
|
74
|
+
print(f"Verify: {data.get('status', 'REVIEW_REQUIRED')}")
|
|
75
|
+
conflicts = list(data.get("conflicts") or [])
|
|
76
|
+
reasons = list(data.get("review_reasons") or [])
|
|
77
|
+
for label, values in (("Conflict", conflicts), ("Review", reasons)):
|
|
78
|
+
for value in values:
|
|
79
|
+
print(f"{label}: {value}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main(argv: list[str] | None = None) -> int:
|
|
83
|
+
parser = argparse.ArgumentParser(prog="codex-rescue")
|
|
84
|
+
parser.add_argument("--version", action="version", version=f"codex-rescue {__version__}")
|
|
85
|
+
subs = parser.add_subparsers(dest="command")
|
|
86
|
+
|
|
87
|
+
sessions = subs.add_parser("sessions")
|
|
88
|
+
sessions.add_argument("--codex-home", type=Path)
|
|
89
|
+
sessions.add_argument("--limit", type=int, default=20)
|
|
90
|
+
sessions.add_argument("--latest", action="store_true")
|
|
91
|
+
sessions.add_argument("--json", action="store_true")
|
|
92
|
+
|
|
93
|
+
doctor = subs.add_parser("doctor")
|
|
94
|
+
doctor.add_argument("session", nargs="?", type=Path)
|
|
95
|
+
doctor.add_argument("--latest", action="store_true")
|
|
96
|
+
doctor.add_argument("--codex-home", type=Path)
|
|
97
|
+
doctor.add_argument("--json", action="store_true")
|
|
98
|
+
doctor.add_argument("--oversized-threshold", type=int, default=1_000_000)
|
|
99
|
+
|
|
100
|
+
salvage = subs.add_parser("salvage")
|
|
101
|
+
salvage.add_argument("session", nargs="?", type=Path)
|
|
102
|
+
salvage.add_argument("--latest", action="store_true")
|
|
103
|
+
salvage.add_argument("--codex-home", type=Path)
|
|
104
|
+
salvage.add_argument("--json", action="store_true")
|
|
105
|
+
salvage.add_argument("--oversized-threshold", type=int, default=1_000_000)
|
|
106
|
+
salvage.add_argument("--fork", action="store_true", required=True)
|
|
107
|
+
salvage.add_argument("--rescue-root", type=Path, default=Path(".codex-rescue"))
|
|
108
|
+
|
|
109
|
+
verify = subs.add_parser("verify")
|
|
110
|
+
verify.add_argument("rescue_id")
|
|
111
|
+
verify.add_argument("--rescue-root", type=Path, default=Path(".codex-rescue"))
|
|
112
|
+
verify.add_argument("--json", action="store_true")
|
|
113
|
+
|
|
114
|
+
args = parser.parse_args(argv)
|
|
115
|
+
if args.command in (None, "sessions"):
|
|
116
|
+
from .discovery import discover_sessions
|
|
117
|
+
|
|
118
|
+
limit = 1 if getattr(args, "latest", False) else getattr(args, "limit", 20)
|
|
119
|
+
summaries = discover_sessions(getattr(args, "codex_home", None), limit=limit)
|
|
120
|
+
data = [item.to_dict() for item in summaries]
|
|
121
|
+
if getattr(args, "json", False):
|
|
122
|
+
_json(data)
|
|
123
|
+
else:
|
|
124
|
+
print("Recent Codex sessions\n")
|
|
125
|
+
for index, item in enumerate(summaries, 1):
|
|
126
|
+
print(f"{index}. {item.modified_at} {item.status}")
|
|
127
|
+
print(f" repo: {item.repo or item.cwd or 'unknown'}")
|
|
128
|
+
print(f" prompt: {item.prompt_preview or 'unavailable'}")
|
|
129
|
+
if item.reason:
|
|
130
|
+
print(f" reason: {item.reason}")
|
|
131
|
+
return 0
|
|
132
|
+
if args.command == "doctor":
|
|
133
|
+
if args.latest:
|
|
134
|
+
from .discovery import resolve_latest
|
|
135
|
+
args.session = resolve_latest(args.codex_home)
|
|
136
|
+
if args.session is None:
|
|
137
|
+
parser.error("doctor requires SESSION or --latest (no session discovered)")
|
|
138
|
+
result = _doctor(args.session, args.oversized_threshold)
|
|
139
|
+
if args.json:
|
|
140
|
+
_json(_to_dict(result))
|
|
141
|
+
else:
|
|
142
|
+
_print_doctor(result)
|
|
143
|
+
return 0
|
|
144
|
+
if args.command == "salvage":
|
|
145
|
+
if args.latest:
|
|
146
|
+
from .discovery import resolve_latest
|
|
147
|
+
args.session = resolve_latest(args.codex_home)
|
|
148
|
+
if args.session is None:
|
|
149
|
+
parser.error("salvage requires SESSION or --latest (no session discovered)")
|
|
150
|
+
result = _doctor(args.session, args.oversized_threshold)
|
|
151
|
+
data = _to_dict(result)
|
|
152
|
+
status = str(data.get("status", "UNKNOWN_CORRUPTION"))
|
|
153
|
+
findings = list(data.get("findings") or [status])
|
|
154
|
+
salvage_result = salvage_session(
|
|
155
|
+
args.session,
|
|
156
|
+
_parsed_from_doctor(result),
|
|
157
|
+
status,
|
|
158
|
+
findings,
|
|
159
|
+
args.rescue_root,
|
|
160
|
+
args.fork,
|
|
161
|
+
)
|
|
162
|
+
if args.json:
|
|
163
|
+
_json(salvage_result.to_dict())
|
|
164
|
+
else:
|
|
165
|
+
_print_salvage(salvage_result)
|
|
166
|
+
return 0 if salvage_result.original_untouched else 2
|
|
167
|
+
if args.command == "verify":
|
|
168
|
+
result = verify_rescue(args.rescue_root, args.rescue_id)
|
|
169
|
+
if args.json:
|
|
170
|
+
_json(result.to_dict())
|
|
171
|
+
else:
|
|
172
|
+
_print_verify(result)
|
|
173
|
+
return 0 if result.status == "SAFE_TO_CONTINUE" else 3
|
|
174
|
+
return 2
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
raise SystemExit(main())
|