107 return absl::OkStatus();
110 for (
const auto& entry : std::filesystem::directory_iterator(
root_directory_, ec)) {
115 if (!entry.is_directory()) {
119 const std::string proposal_id = entry.path().filename().string();
125 bool metadata_loaded =
false;
126 const std::filesystem::path metadata_path = entry.path() /
"metadata.json";
128 if (std::filesystem::exists(metadata_path, ec) && !ec) {
129 std::ifstream metadata_file(metadata_path);
130 if (metadata_file.is_open()) {
132 nlohmann::json metadata_json;
133 metadata_file >> metadata_json;
135 metadata.
id = metadata_json.value(
"id", proposal_id);
136 if (metadata.
id.empty()) {
137 metadata.
id = proposal_id;
139 metadata.
sandbox_id = metadata_json.value(
"sandbox_id",
"");
141 if (metadata_json.contains(
"sandbox_directory") &&
142 metadata_json[
"sandbox_directory"].is_string()) {
144 std::filesystem::path(metadata_json[
"sandbox_directory"].get<std::string>());
149 if (metadata_json.contains(
"sandbox_rom_path") &&
150 metadata_json[
"sandbox_rom_path"].is_string()) {
152 std::filesystem::path(metadata_json[
"sandbox_rom_path"].get<std::string>());
157 metadata.
description = metadata_json.value(
"description",
"");
158 metadata.
prompt = metadata_json.value(
"prompt",
"");
159 metadata.
status = ParseStatus(metadata_json.value(
"status",
"pending"));
161 int64_t created_at_millis = metadata_json.value<int64_t>(
162 "created_at_millis", TimeToMillis(absl::Now()));
163 metadata.
created_at = absl::FromUnixMillis(created_at_millis);
165 int64_t reviewed_at_millis = metadata_json.value<int64_t>(
166 "reviewed_at_millis", 0);
167 metadata.
reviewed_at = OptionalTimeFromMillis(reviewed_at_millis);
169 std::string diff_path = metadata_json.value(
"diff_path", std::string(
"diff.txt"));
170 std::string log_path = metadata_json.value(
"log_path", std::string(
"execution.log"));
171 metadata.
diff_path = entry.path() / diff_path;
172 metadata.
log_path = entry.path() / log_path;
174 metadata.
bytes_changed = metadata_json.value(
"bytes_changed", 0);
178 if (metadata_json.contains(
"screenshots") &&
179 metadata_json[
"screenshots"].is_array()) {
180 for (
const auto& screenshot : metadata_json[
"screenshots"]) {
181 if (screenshot.is_string()) {
183 screenshot.get<std::string>());
193 metadata_loaded =
true;
194 }
catch (
const std::exception& ex) {
195 std::cerr <<
"Warning: Failed to parse metadata for proposal "
196 << proposal_id <<
": " << ex.what() <<
"\n";
201 if (!metadata_loaded) {
202 std::filesystem::path log_path = entry.path() /
"execution.log";
203 if (!std::filesystem::exists(log_path, ec) || ec) {
207 std::filesystem::path diff_path = entry.path() /
"diff.txt";
209 absl::Time created_at = absl::Now();
210 if (proposal_id.starts_with(
"proposal-")) {
211 std::string time_str = proposal_id.substr(9, 15);
213 if (absl::ParseTime(
"%Y%m%dT%H%M%S", time_str, &created_at, &error)) {
218 auto ftime = std::filesystem::last_write_time(log_path, ec);
220 auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
221 ftime - std::filesystem::file_time_type::clock::now() +
222 std::chrono::system_clock::now());
223 auto time_t_value = std::chrono::system_clock::to_time_t(sctp);
224 created_at = absl::FromTimeT(time_t_value);
230 .sandbox_directory = std::filesystem::path(),
231 .sandbox_rom_path = std::filesystem::path(),
232 .description =
"Loaded from disk",
235 .created_at = created_at,
236 .reviewed_at = std::nullopt,
237 .diff_path = diff_path,
238 .log_path = log_path,
241 .commands_executed = 0,
244 if (std::filesystem::exists(diff_path, ec) && !ec) {
246 std::filesystem::file_size(diff_path, ec));
249 for (
const auto& file : std::filesystem::directory_iterator(entry.path(), ec)) {
253 if (file.path().extension() ==
".png" || file.path().extension() ==
".jpg" ||
254 file.path().extension() ==
".jpeg") {
261 if (!write_status.ok()) {
262 std::cerr <<
"Warning: Failed to persist metadata for legacy proposal "
263 << proposal_id <<
": " << write_status.message() <<
"\n";
270 return absl::OkStatus();
288 const std::filesystem::path& sandbox_rom_path,
289 absl::string_view prompt,
290 absl::string_view description) {
291 std::unique_lock<std::mutex> lock(
mutex_);
299 if (!std::filesystem::create_directories(proposal_dir, ec) && ec) {
300 return absl::InternalError(absl::StrCat(
301 "Failed to create proposal directory at ", proposal_dir.string(),
302 ": ", ec.message()));
308 .sandbox_id = std::string(sandbox_id),
309 .sandbox_directory = sandbox_rom_path.empty()
310 ? std::filesystem::path()
311 : sandbox_rom_path.parent_path(),
312 .sandbox_rom_path = sandbox_rom_path,
313 .description = std::string(description),
314 .prompt = std::string(prompt),
316 .created_at = absl::Now(),
317 .reviewed_at = std::nullopt,
318 .diff_path = proposal_dir /
"diff.txt",
319 .log_path = proposal_dir /
"execution.log",
322 .commands_executed = 0,
331 absl::string_view diff_content) {
332 std::lock_guard<std::mutex> lock(
mutex_);
335 return absl::NotFoundError(
336 absl::StrCat(
"Proposal not found: ", proposal_id));
339 std::ofstream diff_file(it->second.diff_path, std::ios::out);
340 if (!diff_file.is_open()) {
341 return absl::InternalError(absl::StrCat(
342 "Failed to open diff file: ", it->second.diff_path.string()));
345 diff_file << diff_content;
349 it->second.bytes_changed =
static_cast<int>(diff_content.size());
353 return absl::OkStatus();
357 absl::string_view log_entry) {
358 std::lock_guard<std::mutex> lock(
mutex_);
361 return absl::NotFoundError(
362 absl::StrCat(
"Proposal not found: ", proposal_id));
365 std::ofstream log_file(it->second.log_path,
366 std::ios::out | std::ios::app);
367 if (!log_file.is_open()) {
368 return absl::InternalError(absl::StrCat(
369 "Failed to open log file: ", it->second.log_path.string()));
372 log_file << absl::FormatTime(
"[%Y-%m-%d %H:%M:%S] ", absl::Now(),
373 absl::LocalTimeZone())
374 << log_entry <<
"\n";
377 return absl::OkStatus();
499 if (!std::filesystem::exists(proposal_dir, ec) || ec) {
500 return absl::NotFoundError(
501 absl::StrCat(
"Proposal directory missing for ", metadata.
id));
504 auto relative_to_proposal = [&](
const std::filesystem::path& path) {
506 return std::string();
508 std::error_code relative_error;
509 auto relative_path = std::filesystem::relative(path, proposal_dir, relative_error);
510 if (!relative_error) {
511 return relative_path.generic_string();
513 return path.generic_string();
516 nlohmann::json metadata_json;
517 metadata_json[
"id"] = metadata.
id;
518 metadata_json[
"sandbox_id"] = metadata.
sandbox_id;
520 metadata_json[
"sandbox_directory"] = metadata.
sandbox_directory.generic_string();
523 metadata_json[
"sandbox_rom_path"] = metadata.
sandbox_rom_path.generic_string();
525 metadata_json[
"description"] = metadata.
description;
526 metadata_json[
"prompt"] = metadata.
prompt;
527 metadata_json[
"status"] = StatusToString(metadata.
status);
528 metadata_json[
"created_at_millis"] = TimeToMillis(metadata.
created_at);
529 metadata_json[
"reviewed_at_millis"] = metadata.
reviewed_at.has_value()
532 metadata_json[
"diff_path"] = relative_to_proposal(metadata.
diff_path);
533 metadata_json[
"log_path"] = relative_to_proposal(metadata.
log_path);
537 nlohmann::json screenshots_json = nlohmann::json::array();
538 for (
const auto& screenshot : metadata.
screenshots) {
539 screenshots_json.push_back(relative_to_proposal(screenshot));
541 metadata_json[
"screenshots"] = std::move(screenshots_json);
543 std::ofstream metadata_file(proposal_dir /
"metadata.json", std::ios::out);
544 if (!metadata_file.is_open()) {
545 return absl::InternalError(absl::StrCat(
546 "Failed to write metadata file for proposal ", metadata.
id));
549 metadata_file << metadata_json.dump(2);
550 metadata_file.close();
551 if (!metadata_file) {
552 return absl::InternalError(absl::StrCat(
553 "Failed to flush metadata file for proposal ", metadata.
id));
556 return absl::OkStatus();