107 return absl::OkStatus();
110 for (
const auto& entry :
116 if (!entry.is_directory()) {
120 const std::string proposal_id = entry.path().filename().string();
126 bool metadata_loaded =
false;
127 const std::filesystem::path metadata_path = entry.path() /
"metadata.json";
129 if (std::filesystem::exists(metadata_path, ec) && !ec) {
130 std::ifstream metadata_file(metadata_path);
131 if (metadata_file.is_open()) {
133 nlohmann::json metadata_json;
134 metadata_file >> metadata_json;
136 metadata.
id = metadata_json.value(
"id", proposal_id);
137 if (metadata.
id.empty()) {
138 metadata.
id = proposal_id;
140 metadata.
sandbox_id = metadata_json.value(
"sandbox_id",
"");
142 if (metadata_json.contains(
"sandbox_directory") &&
143 metadata_json[
"sandbox_directory"].is_string()) {
145 metadata_json[
"sandbox_directory"].get<std::string>());
150 if (metadata_json.contains(
"sandbox_rom_path") &&
151 metadata_json[
"sandbox_rom_path"].is_string()) {
153 metadata_json[
"sandbox_rom_path"].get<std::string>());
158 metadata.
description = metadata_json.value(
"description",
"");
159 metadata.
prompt = metadata_json.value(
"prompt",
"");
161 ParseStatus(metadata_json.value(
"status",
"pending"));
163 int64_t created_at_millis = metadata_json.value<int64_t>(
164 "created_at_millis", TimeToMillis(absl::Now()));
165 metadata.
created_at = absl::FromUnixMillis(created_at_millis);
167 int64_t reviewed_at_millis =
168 metadata_json.value<int64_t>(
"reviewed_at_millis", 0);
169 metadata.
reviewed_at = OptionalTimeFromMillis(reviewed_at_millis);
171 std::string diff_path =
172 metadata_json.value(
"diff_path", std::string(
"diff.txt"));
173 std::string log_path =
174 metadata_json.value(
"log_path", std::string(
"execution.log"));
175 metadata.
diff_path = entry.path() / diff_path;
176 metadata.
log_path = entry.path() / log_path;
178 metadata.
bytes_changed = metadata_json.value(
"bytes_changed", 0);
180 metadata_json.value(
"commands_executed", 0);
183 if (metadata_json.contains(
"screenshots") &&
184 metadata_json[
"screenshots"].is_array()) {
185 for (
const auto& screenshot : metadata_json[
"screenshots"]) {
186 if (screenshot.is_string()) {
188 entry.path() / screenshot.get<std::string>());
199 metadata_loaded =
true;
200 }
catch (
const std::exception& ex) {
201 std::cerr <<
"Warning: Failed to parse metadata for proposal "
202 << proposal_id <<
": " << ex.what() <<
"\n";
207 if (!metadata_loaded) {
208 std::filesystem::path log_path = entry.path() /
"execution.log";
209 if (!std::filesystem::exists(log_path, ec) || ec) {
213 std::filesystem::path diff_path = entry.path() /
"diff.txt";
215 absl::Time created_at = absl::Now();
216 if (proposal_id.starts_with(
"proposal-")) {
217 std::string time_str = proposal_id.substr(9, 15);
219 if (absl::ParseTime(
"%Y%m%dT%H%M%S", time_str, &created_at, &error)) {
224 auto ftime = std::filesystem::last_write_time(log_path, ec);
227 std::chrono::time_point_cast<std::chrono::system_clock::duration>(
228 ftime - std::filesystem::file_time_type::clock::now() +
229 std::chrono::system_clock::now());
230 auto time_t_value = std::chrono::system_clock::to_time_t(sctp);
231 created_at = absl::FromTimeT(time_t_value);
237 .sandbox_directory = std::filesystem::path(),
238 .sandbox_rom_path = std::filesystem::path(),
239 .description =
"Loaded from disk",
242 .created_at = created_at,
243 .reviewed_at = std::nullopt,
244 .diff_path = diff_path,
245 .log_path = log_path,
248 .commands_executed = 0,
251 if (std::filesystem::exists(diff_path, ec) && !ec) {
253 static_cast<int>(std::filesystem::file_size(diff_path, ec));
256 for (
const auto& file :
257 std::filesystem::directory_iterator(entry.path(), ec)) {
261 if (file.path().extension() ==
".png" ||
262 file.path().extension() ==
".jpg" ||
263 file.path().extension() ==
".jpeg") {
270 if (!write_status.ok()) {
271 std::cerr <<
"Warning: Failed to persist metadata for legacy proposal "
272 << proposal_id <<
": " << write_status.message() <<
"\n";
279 return absl::OkStatus();
297 const std::filesystem::path& sandbox_rom_path,
298 absl::string_view prompt,
299 absl::string_view description) {
300 std::unique_lock<std::mutex> lock(
mutex_);
308 if (!std::filesystem::create_directories(proposal_dir, ec) && ec) {
309 return absl::InternalError(
310 absl::StrCat(
"Failed to create proposal directory at ",
311 proposal_dir.string(),
": ", ec.message()));
317 .sandbox_id = std::string(sandbox_id),
318 .sandbox_directory = sandbox_rom_path.empty()
319 ? std::filesystem::path()
320 : sandbox_rom_path.parent_path(),
321 .sandbox_rom_path = sandbox_rom_path,
322 .description = std::string(description),
323 .prompt = std::string(prompt),
325 .created_at = absl::Now(),
326 .reviewed_at = std::nullopt,
327 .diff_path = proposal_dir /
"diff.txt",
328 .log_path = proposal_dir /
"execution.log",
331 .commands_executed = 0,
340 absl::string_view diff_content) {
341 std::lock_guard<std::mutex> lock(
mutex_);
344 return absl::NotFoundError(
345 absl::StrCat(
"Proposal not found: ", proposal_id));
348 std::ofstream diff_file(it->second.diff_path, std::ios::out);
349 if (!diff_file.is_open()) {
350 return absl::InternalError(absl::StrCat(
"Failed to open diff file: ",
351 it->second.diff_path.string()));
354 diff_file << diff_content;
358 it->second.bytes_changed =
static_cast<int>(diff_content.size());
362 return absl::OkStatus();
366 absl::string_view log_entry) {
367 std::lock_guard<std::mutex> lock(
mutex_);
370 return absl::NotFoundError(
371 absl::StrCat(
"Proposal not found: ", proposal_id));
374 std::ofstream log_file(it->second.log_path, std::ios::out | std::ios::app);
375 if (!log_file.is_open()) {
376 return absl::InternalError(absl::StrCat(
"Failed to open log file: ",
377 it->second.log_path.string()));
380 log_file << absl::FormatTime(
"[%Y-%m-%d %H:%M:%S] ", absl::Now(),
381 absl::LocalTimeZone())
382 << log_entry <<
"\n";
385 return absl::OkStatus();
451 std::optional<ProposalStatus> filter_status)
const {
452 std::unique_lock<std::mutex> lock(
mutex_);
461 std::cerr <<
"Warning: Failed to load proposals from disk: "
462 << status.message() <<
"\n";
466 std::vector<ProposalMetadata> result;
468 for (
const auto& [
id, metadata] :
proposals_) {
469 if (!filter_status.has_value() || metadata.status == *filter_status) {
470 result.push_back(metadata);
475 std::sort(result.begin(), result.end(),
477 return a.created_at > b.created_at;
507 if (!std::filesystem::exists(proposal_dir, ec) || ec) {
508 return absl::NotFoundError(
509 absl::StrCat(
"Proposal directory missing for ", metadata.
id));
512 auto relative_to_proposal = [&](
const std::filesystem::path& path) {
514 return std::string();
516 std::error_code relative_error;
518 std::filesystem::relative(path, proposal_dir, relative_error);
519 if (!relative_error) {
520 return relative_path.generic_string();
522 return path.generic_string();
525 nlohmann::json metadata_json;
526 metadata_json[
"id"] = metadata.
id;
527 metadata_json[
"sandbox_id"] = metadata.
sandbox_id;
529 metadata_json[
"sandbox_directory"] =
533 metadata_json[
"sandbox_rom_path"] =
536 metadata_json[
"description"] = metadata.
description;
537 metadata_json[
"prompt"] = metadata.
prompt;
538 metadata_json[
"status"] = StatusToString(metadata.
status);
539 metadata_json[
"created_at_millis"] = TimeToMillis(metadata.
created_at);
540 metadata_json[
"reviewed_at_millis"] =
543 metadata_json[
"diff_path"] = relative_to_proposal(metadata.
diff_path);
544 metadata_json[
"log_path"] = relative_to_proposal(metadata.
log_path);
548 nlohmann::json screenshots_json = nlohmann::json::array();
549 for (
const auto& screenshot : metadata.
screenshots) {
550 screenshots_json.push_back(relative_to_proposal(screenshot));
552 metadata_json[
"screenshots"] = std::move(screenshots_json);
554 std::ofstream metadata_file(proposal_dir /
"metadata.json", std::ios::out);
555 if (!metadata_file.is_open()) {
556 return absl::InternalError(absl::StrCat(
557 "Failed to write metadata file for proposal ", metadata.
id));
560 metadata_file << metadata_json.dump(2);
561 metadata_file.close();
562 if (!metadata_file) {
563 return absl::InternalError(absl::StrCat(
564 "Failed to flush metadata file for proposal ", metadata.
id));
567 return absl::OkStatus();