106 return absl::OkStatus();
109 for (
const auto& entry :
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 metadata_json[
"sandbox_directory"].get<std::string>());
149 if (metadata_json.contains(
"sandbox_rom_path") &&
150 metadata_json[
"sandbox_rom_path"].is_string()) {
152 metadata_json[
"sandbox_rom_path"].get<std::string>());
157 metadata.
description = metadata_json.value(
"description",
"");
158 metadata.
prompt = metadata_json.value(
"prompt",
"");
160 ParseStatus(metadata_json.value(
"status",
"pending"));
162 int64_t created_at_millis = metadata_json.value<int64_t>(
163 "created_at_millis", TimeToMillis(absl::Now()));
164 metadata.
created_at = absl::FromUnixMillis(created_at_millis);
166 int64_t reviewed_at_millis =
167 metadata_json.value<int64_t>(
"reviewed_at_millis", 0);
168 metadata.
reviewed_at = OptionalTimeFromMillis(reviewed_at_millis);
170 std::string diff_path =
171 metadata_json.value(
"diff_path", std::string(
"diff.txt"));
172 std::string log_path =
173 metadata_json.value(
"log_path", std::string(
"execution.log"));
174 metadata.
diff_path = entry.path() / diff_path;
175 metadata.
log_path = entry.path() / log_path;
177 metadata.
bytes_changed = metadata_json.value(
"bytes_changed", 0);
179 metadata_json.value(
"commands_executed", 0);
182 if (metadata_json.contains(
"screenshots") &&
183 metadata_json[
"screenshots"].is_array()) {
184 for (
const auto& screenshot : metadata_json[
"screenshots"]) {
185 if (screenshot.is_string()) {
187 entry.path() / screenshot.get<std::string>());
198 metadata_loaded =
true;
199 }
catch (
const std::exception& ex) {
200 std::cerr <<
"Warning: Failed to parse metadata for proposal "
201 << proposal_id <<
": " << ex.what() <<
"\n";
206 if (!metadata_loaded) {
207 std::filesystem::path log_path = entry.path() /
"execution.log";
208 if (!std::filesystem::exists(log_path, ec) || ec) {
212 std::filesystem::path diff_path = entry.path() /
"diff.txt";
214 absl::Time created_at = absl::Now();
215 if (proposal_id.starts_with(
"proposal-")) {
216 std::string time_str = proposal_id.substr(9, 15);
218 if (absl::ParseTime(
"%Y%m%dT%H%M%S", time_str, &created_at, &error)) {
223 auto ftime = std::filesystem::last_write_time(log_path, ec);
226 std::chrono::time_point_cast<std::chrono::system_clock::duration>(
227 ftime - std::filesystem::file_time_type::clock::now() +
228 std::chrono::system_clock::now());
229 auto time_t_value = std::chrono::system_clock::to_time_t(sctp);
230 created_at = absl::FromTimeT(time_t_value);
236 .sandbox_directory = std::filesystem::path(),
237 .sandbox_rom_path = std::filesystem::path(),
238 .description =
"Loaded from disk",
241 .created_at = created_at,
242 .reviewed_at = std::nullopt,
243 .diff_path = diff_path,
244 .log_path = log_path,
247 .commands_executed = 0,
250 if (std::filesystem::exists(diff_path, ec) && !ec) {
252 static_cast<int>(std::filesystem::file_size(diff_path, ec));
255 for (
const auto& file :
256 std::filesystem::directory_iterator(entry.path(), ec)) {
260 if (file.path().extension() ==
".png" ||
261 file.path().extension() ==
".jpg" ||
262 file.path().extension() ==
".jpeg") {
269 if (!write_status.ok()) {
270 std::cerr <<
"Warning: Failed to persist metadata for legacy proposal "
271 << proposal_id <<
": " << write_status.message() <<
"\n";
278 return absl::OkStatus();
296 const std::filesystem::path& sandbox_rom_path,
297 absl::string_view prompt,
298 absl::string_view description) {
299 std::unique_lock<std::mutex> lock(
mutex_);
307 if (!std::filesystem::create_directories(proposal_dir, ec) && ec) {
308 return absl::InternalError(
309 absl::StrCat(
"Failed to create proposal directory at ",
310 proposal_dir.string(),
": ", ec.message()));
316 .sandbox_id = std::string(sandbox_id),
317 .sandbox_directory = sandbox_rom_path.empty()
318 ? std::filesystem::path()
319 : sandbox_rom_path.parent_path(),
320 .sandbox_rom_path = sandbox_rom_path,
321 .description = std::string(description),
322 .prompt = std::string(prompt),
324 .created_at = absl::Now(),
325 .reviewed_at = std::nullopt,
326 .diff_path = proposal_dir /
"diff.txt",
327 .log_path = proposal_dir /
"execution.log",
330 .commands_executed = 0,
339 absl::string_view diff_content) {
340 std::lock_guard<std::mutex> lock(
mutex_);
343 return absl::NotFoundError(
344 absl::StrCat(
"Proposal not found: ", proposal_id));
347 std::ofstream diff_file(it->second.diff_path, std::ios::out);
348 if (!diff_file.is_open()) {
349 return absl::InternalError(absl::StrCat(
"Failed to open diff file: ",
350 it->second.diff_path.string()));
353 diff_file << diff_content;
357 it->second.bytes_changed =
static_cast<int>(diff_content.size());
361 return absl::OkStatus();
365 absl::string_view log_entry) {
366 std::lock_guard<std::mutex> lock(
mutex_);
369 return absl::NotFoundError(
370 absl::StrCat(
"Proposal not found: ", proposal_id));
373 std::ofstream log_file(it->second.log_path, std::ios::out | std::ios::app);
374 if (!log_file.is_open()) {
375 return absl::InternalError(absl::StrCat(
"Failed to open log file: ",
376 it->second.log_path.string()));
379 log_file << absl::FormatTime(
"[%Y-%m-%d %H:%M:%S] ", absl::Now(),
380 absl::LocalTimeZone())
381 << log_entry <<
"\n";
384 return absl::OkStatus();
450 std::optional<ProposalStatus> filter_status)
const {
451 std::unique_lock<std::mutex> lock(
mutex_);
460 std::cerr <<
"Warning: Failed to load proposals from disk: "
461 << status.message() <<
"\n";
465 std::vector<ProposalMetadata> result;
467 for (
const auto& [
id, metadata] :
proposals_) {
468 if (!filter_status.has_value() || metadata.status == *filter_status) {
469 result.push_back(metadata);
474 std::sort(result.begin(), result.end(),
476 return a.created_at > b.created_at;
506 if (!std::filesystem::exists(proposal_dir, ec) || ec) {
507 return absl::NotFoundError(
508 absl::StrCat(
"Proposal directory missing for ", metadata.
id));
511 auto relative_to_proposal = [&](
const std::filesystem::path& path) {
513 return std::string();
515 std::error_code relative_error;
517 std::filesystem::relative(path, proposal_dir, relative_error);
518 if (!relative_error) {
519 return relative_path.generic_string();
521 return path.generic_string();
524 nlohmann::json metadata_json;
525 metadata_json[
"id"] = metadata.
id;
526 metadata_json[
"sandbox_id"] = metadata.
sandbox_id;
528 metadata_json[
"sandbox_directory"] =
532 metadata_json[
"sandbox_rom_path"] =
535 metadata_json[
"description"] = metadata.
description;
536 metadata_json[
"prompt"] = metadata.
prompt;
537 metadata_json[
"status"] = StatusToString(metadata.
status);
538 metadata_json[
"created_at_millis"] = TimeToMillis(metadata.
created_at);
539 metadata_json[
"reviewed_at_millis"] =
542 metadata_json[
"diff_path"] = relative_to_proposal(metadata.
diff_path);
543 metadata_json[
"log_path"] = relative_to_proposal(metadata.
log_path);
547 nlohmann::json screenshots_json = nlohmann::json::array();
548 for (
const auto& screenshot : metadata.
screenshots) {
549 screenshots_json.push_back(relative_to_proposal(screenshot));
551 metadata_json[
"screenshots"] = std::move(screenshots_json);
553 std::ofstream metadata_file(proposal_dir /
"metadata.json", std::ios::out);
554 if (!metadata_file.is_open()) {
555 return absl::InternalError(absl::StrCat(
556 "Failed to write metadata file for proposal ", metadata.
id));
559 metadata_file << metadata_json.dump(2);
560 metadata_file.close();
561 if (!metadata_file) {
562 return absl::InternalError(absl::StrCat(
563 "Failed to flush metadata file for proposal ", metadata.
id));
566 return absl::OkStatus();