feat: Enhance ROM loading options and proposal management
- Introduced `RomLoadOptions` struct to manage various loading configurations for ROM files, including options for stripping headers, populating metadata, and loading Zelda 3 content. - Updated `Rom::LoadFromFile` and `Rom::LoadFromData` methods to accept `RomLoadOptions`, allowing for more flexible ROM loading behavior. - Implemented `MaybeStripSmcHeader` function to conditionally remove SMC headers from ROM data. - Added new command handler `RomInfo` to display basic ROM information, including title and size. - Created `ProposalRegistry` class to manage agent-generated proposals, including creation, logging, and status updates. - Enhanced CLI commands to support proposal listing and detailed diff viewing, improving user interaction with agent-generated modifications. - Updated resource catalog to include new actions for ROM info and agent proposal management.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#include "cli/z3ed.h"
|
||||
#include "cli/modern_cli.h"
|
||||
#include "cli/service/ai_service.h"
|
||||
#include "cli/service/proposal_registry.h"
|
||||
#include "cli/service/resource_catalog.h"
|
||||
#include "cli/service/rom_sandbox_manager.h"
|
||||
#include "util/macro.h"
|
||||
@@ -9,6 +10,7 @@
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/ascii.h"
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
@@ -105,23 +107,48 @@ absl::Status HandleRunCommand(const std::vector<std::string>& arg_vec, Rom& rom)
|
||||
std::string prompt = arg_vec[1];
|
||||
|
||||
// Save a sandbox copy of the ROM for proposal tracking.
|
||||
if (rom.is_loaded()) {
|
||||
auto sandbox_or = RomSandboxManager::Instance().CreateSandbox(
|
||||
rom, "agent-run");
|
||||
if (!sandbox_or.ok()) {
|
||||
return sandbox_or.status();
|
||||
}
|
||||
if (!rom.is_loaded()) {
|
||||
return absl::FailedPreconditionError("No ROM loaded");
|
||||
}
|
||||
|
||||
auto sandbox_or = RomSandboxManager::Instance().CreateSandbox(
|
||||
rom, "agent-run");
|
||||
if (!sandbox_or.ok()) {
|
||||
return sandbox_or.status();
|
||||
}
|
||||
auto sandbox = sandbox_or.value();
|
||||
|
||||
// Create a proposal to track this agent run
|
||||
auto proposal_or = ProposalRegistry::Instance().CreateProposal(
|
||||
sandbox.id, prompt, "Agent-generated ROM modifications");
|
||||
if (!proposal_or.ok()) {
|
||||
return proposal_or.status();
|
||||
}
|
||||
auto proposal = proposal_or.value();
|
||||
|
||||
// Log the start of execution
|
||||
RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog(
|
||||
proposal.id, absl::StrCat("Starting agent run with prompt: ", prompt)));
|
||||
|
||||
MockAIService ai_service;
|
||||
auto commands_or = ai_service.GetCommands(prompt);
|
||||
if (!commands_or.ok()) {
|
||||
RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog(
|
||||
proposal.id, absl::StrCat("AI service error: ", commands_or.status().message())));
|
||||
return commands_or.status();
|
||||
}
|
||||
std::vector<std::string> commands = commands_or.value();
|
||||
|
||||
// Log the planned commands
|
||||
RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog(
|
||||
proposal.id, absl::StrCat("Generated ", commands.size(), " commands")));
|
||||
|
||||
ModernCLI cli;
|
||||
int commands_executed = 0;
|
||||
for (const auto& command : commands) {
|
||||
RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog(
|
||||
proposal.id, absl::StrCat("Executing: ", command)));
|
||||
|
||||
std::vector<std::string> command_parts;
|
||||
std::string current_part;
|
||||
bool in_quotes = false;
|
||||
@@ -144,10 +171,30 @@ absl::Status HandleRunCommand(const std::vector<std::string>& arg_vec, Rom& rom)
|
||||
if (it != cli.commands_.end()) {
|
||||
auto status = it->second.handler(cmd_args);
|
||||
if (!status.ok()) {
|
||||
RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog(
|
||||
proposal.id, absl::StrCat("Command failed: ", status.message())));
|
||||
return status;
|
||||
}
|
||||
commands_executed++;
|
||||
RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog(
|
||||
proposal.id, "Command succeeded"));
|
||||
} else {
|
||||
auto error_msg = absl::StrCat("Unknown command: ", cmd_name);
|
||||
RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog(
|
||||
proposal.id, error_msg));
|
||||
return absl::NotFoundError(error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Update proposal with execution stats
|
||||
RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog(
|
||||
proposal.id, absl::StrCat("Completed execution of ", commands_executed, " commands")));
|
||||
|
||||
std::cout << "✅ Agent run completed successfully." << std::endl;
|
||||
std::cout << " Proposal ID: " << proposal.id << std::endl;
|
||||
std::cout << " Sandbox: " << sandbox.rom_path << std::endl;
|
||||
std::cout << " Use 'z3ed agent diff' to review changes" << std::endl;
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
@@ -170,11 +217,110 @@ absl::Status HandlePlanCommand(const std::vector<std::string>& arg_vec) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status HandleDiffCommand(Rom& rom) {
|
||||
absl::Status HandleDiffCommand(Rom& rom, const std::vector<std::string>& args) {
|
||||
// Parse optional --proposal-id flag
|
||||
std::optional<std::string> proposal_id;
|
||||
for (size_t i = 0; i < args.size(); ++i) {
|
||||
const std::string& token = args[i];
|
||||
if (absl::StartsWith(token, "--proposal-id=")) {
|
||||
proposal_id = token.substr(14); // Length of "--proposal-id="
|
||||
} else if (token == "--proposal-id" && i + 1 < args.size()) {
|
||||
proposal_id = args[i + 1];
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
auto& registry = ProposalRegistry::Instance();
|
||||
absl::StatusOr<ProposalRegistry::ProposalMetadata> proposal_or;
|
||||
|
||||
// Get specific proposal or latest pending
|
||||
if (proposal_id.has_value()) {
|
||||
proposal_or = registry.GetProposal(proposal_id.value());
|
||||
} else {
|
||||
proposal_or = registry.GetLatestPendingProposal();
|
||||
}
|
||||
|
||||
if (proposal_or.ok()) {
|
||||
const auto& proposal = proposal_or.value();
|
||||
|
||||
std::cout << "\n=== Proposal Diff ===\n";
|
||||
std::cout << "Proposal ID: " << proposal.id << "\n";
|
||||
std::cout << "Sandbox ID: " << proposal.sandbox_id << "\n";
|
||||
std::cout << "Prompt: " << proposal.prompt << "\n";
|
||||
std::cout << "Description: " << proposal.description << "\n";
|
||||
std::cout << "Status: ";
|
||||
switch (proposal.status) {
|
||||
case ProposalRegistry::ProposalStatus::kPending:
|
||||
std::cout << "Pending";
|
||||
break;
|
||||
case ProposalRegistry::ProposalStatus::kAccepted:
|
||||
std::cout << "Accepted";
|
||||
break;
|
||||
case ProposalRegistry::ProposalStatus::kRejected:
|
||||
std::cout << "Rejected";
|
||||
break;
|
||||
}
|
||||
std::cout << "\n";
|
||||
std::cout << "Created: " << absl::FormatTime(proposal.created_at) << "\n";
|
||||
std::cout << "Commands Executed: " << proposal.commands_executed << "\n";
|
||||
std::cout << "Bytes Changed: " << proposal.bytes_changed << "\n\n";
|
||||
|
||||
// Read and display the diff file
|
||||
if (std::filesystem::exists(proposal.diff_path)) {
|
||||
std::cout << "--- Diff Content ---\n";
|
||||
std::ifstream diff_file(proposal.diff_path);
|
||||
if (diff_file.is_open()) {
|
||||
std::string line;
|
||||
while (std::getline(diff_file, line)) {
|
||||
std::cout << line << "\n";
|
||||
}
|
||||
diff_file.close();
|
||||
} else {
|
||||
std::cout << "(Unable to read diff file)\n";
|
||||
}
|
||||
} else {
|
||||
std::cout << "(No diff file found)\n";
|
||||
}
|
||||
|
||||
// Display execution log summary
|
||||
std::cout << "\n--- Execution Log ---\n";
|
||||
if (std::filesystem::exists(proposal.log_path)) {
|
||||
std::ifstream log_file(proposal.log_path);
|
||||
if (log_file.is_open()) {
|
||||
std::string line;
|
||||
int line_count = 0;
|
||||
while (std::getline(log_file, line)) {
|
||||
std::cout << line << "\n";
|
||||
line_count++;
|
||||
if (line_count > 50) { // Limit output for readability
|
||||
std::cout << "... (log truncated, see " << proposal.log_path << " for full output)\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
log_file.close();
|
||||
} else {
|
||||
std::cout << "(Unable to read log file)\n";
|
||||
}
|
||||
} else {
|
||||
std::cout << "(No log file found)\n";
|
||||
}
|
||||
|
||||
// Display next steps
|
||||
std::cout << "\n=== Next Steps ===\n";
|
||||
std::cout << "To accept changes: z3ed agent commit\n";
|
||||
std::cout << "To reject changes: z3ed agent revert\n";
|
||||
std::cout << "To review in GUI: yaze --proposal=" << proposal.id << "\n";
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Fallback to old behavior if no proposal found
|
||||
if (rom.is_loaded()) {
|
||||
auto sandbox_or = RomSandboxManager::Instance().ActiveSandbox();
|
||||
if (!sandbox_or.ok()) {
|
||||
return sandbox_or.status();
|
||||
return absl::NotFoundError(
|
||||
"No pending proposals found and no active sandbox. "
|
||||
"Run 'z3ed agent run' first.");
|
||||
}
|
||||
RomDiff diff_handler;
|
||||
auto status = diff_handler.Run(
|
||||
@@ -279,6 +425,46 @@ absl::Status HandleLearnCommand() {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status HandleListCommand() {
|
||||
auto& registry = ProposalRegistry::Instance();
|
||||
auto proposals = registry.ListProposals();
|
||||
|
||||
if (proposals.empty()) {
|
||||
std::cout << "No proposals found.\n";
|
||||
std::cout << "Run 'z3ed agent run --prompt \"...\"' to create a proposal.\n";
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
std::cout << "\n=== Agent Proposals ===\n\n";
|
||||
|
||||
for (const auto& proposal : proposals) {
|
||||
std::cout << "ID: " << proposal.id << "\n";
|
||||
std::cout << " Status: ";
|
||||
switch (proposal.status) {
|
||||
case ProposalRegistry::ProposalStatus::kPending:
|
||||
std::cout << "Pending";
|
||||
break;
|
||||
case ProposalRegistry::ProposalStatus::kAccepted:
|
||||
std::cout << "Accepted";
|
||||
break;
|
||||
case ProposalRegistry::ProposalStatus::kRejected:
|
||||
std::cout << "Rejected";
|
||||
break;
|
||||
}
|
||||
std::cout << "\n";
|
||||
std::cout << " Created: " << absl::FormatTime(proposal.created_at) << "\n";
|
||||
std::cout << " Prompt: " << proposal.prompt << "\n";
|
||||
std::cout << " Commands: " << proposal.commands_executed << "\n";
|
||||
std::cout << " Bytes Changed: " << proposal.bytes_changed << "\n";
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
std::cout << "Total: " << proposals.size() << " proposal(s)\n";
|
||||
std::cout << "\nUse 'z3ed agent diff --proposal-id=<id>' to view details.\n";
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status HandleCommitCommand(Rom& rom) {
|
||||
if (rom.is_loaded()) {
|
||||
auto status = rom.SaveToFile({.save_new = false});
|
||||
@@ -367,7 +553,7 @@ absl::Status HandleDescribeCommand(const std::vector<std::string>& arg_vec) {
|
||||
absl::Status Agent::Run(const std::vector<std::string>& arg_vec) {
|
||||
if (arg_vec.empty()) {
|
||||
return absl::InvalidArgumentError(
|
||||
"Usage: agent <run|plan|diff|test|learn|commit|revert|describe> [options]");
|
||||
"Usage: agent <run|plan|diff|test|learn|list|commit|revert|describe> [options]");
|
||||
}
|
||||
|
||||
std::string subcommand = arg_vec[0];
|
||||
@@ -378,11 +564,13 @@ absl::Status Agent::Run(const std::vector<std::string>& arg_vec) {
|
||||
} else if (subcommand == "plan") {
|
||||
return HandlePlanCommand(subcommand_args);
|
||||
} else if (subcommand == "diff") {
|
||||
return HandleDiffCommand(rom_);
|
||||
return HandleDiffCommand(rom_, subcommand_args);
|
||||
} else if (subcommand == "test") {
|
||||
return HandleTestCommand(subcommand_args);
|
||||
} else if (subcommand == "learn") {
|
||||
return HandleLearnCommand();
|
||||
} else if (subcommand == "list") {
|
||||
return HandleListCommand();
|
||||
} else if (subcommand == "commit") {
|
||||
return HandleCommitCommand(rom_);
|
||||
} else if (subcommand == "revert") {
|
||||
|
||||
@@ -8,15 +8,34 @@ ABSL_DECLARE_FLAG(std::string, rom);
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
absl::Status RomInfo::Run(const std::vector<std::string>& arg_vec) {
|
||||
std::string rom_file = absl::GetFlag(FLAGS_rom);
|
||||
if (rom_file.empty()) {
|
||||
return absl::InvalidArgumentError("ROM file must be provided via --rom flag.");
|
||||
}
|
||||
|
||||
RETURN_IF_ERROR(rom_.LoadFromFile(rom_file, RomLoadOptions::CliDefaults()));
|
||||
if (!rom_.is_loaded()) {
|
||||
return absl::AbortedError("Failed to load ROM.");
|
||||
}
|
||||
|
||||
std::cout << "ROM Information:" << std::endl;
|
||||
std::cout << " Title: " << rom_.title() << std::endl;
|
||||
std::cout << " Size: 0x" << std::hex << rom_.size() << " bytes" << std::endl;
|
||||
std::cout << " Filename: " << rom_file << std::endl;
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status RomValidate::Run(const std::vector<std::string>& arg_vec) {
|
||||
std::string rom_file = absl::GetFlag(FLAGS_rom);
|
||||
if (rom_file.empty()) {
|
||||
return absl::InvalidArgumentError("ROM file must be provided via --rom flag.");
|
||||
}
|
||||
|
||||
rom_.LoadFromFile(rom_file);
|
||||
RETURN_IF_ERROR(rom_.LoadFromFile(rom_file, RomLoadOptions::CliDefaults()));
|
||||
if (!rom_.is_loaded()) {
|
||||
return absl::AbortedError("Failed to load ROM.");
|
||||
return absl::AbortedError("Failed to load ROM.");
|
||||
}
|
||||
|
||||
bool all_ok = true;
|
||||
@@ -57,13 +76,13 @@ absl::Status RomDiff::Run(const std::vector<std::string>& arg_vec) {
|
||||
}
|
||||
|
||||
Rom rom_a;
|
||||
auto status_a = rom_a.LoadFromFile(arg_vec[0]);
|
||||
auto status_a = rom_a.LoadFromFile(arg_vec[0], RomLoadOptions::CliDefaults());
|
||||
if (!status_a.ok()) {
|
||||
return status_a;
|
||||
}
|
||||
|
||||
Rom rom_b;
|
||||
auto status_b = rom_b.LoadFromFile(arg_vec[1]);
|
||||
auto status_b = rom_b.LoadFromFile(arg_vec[1], RomLoadOptions::CliDefaults());
|
||||
if (!status_b.ok()) {
|
||||
return status_b;
|
||||
}
|
||||
@@ -95,7 +114,7 @@ absl::Status RomGenerateGolden::Run(const std::vector<std::string>& arg_vec) {
|
||||
}
|
||||
|
||||
Rom rom;
|
||||
auto status = rom.LoadFromFile(arg_vec[0]);
|
||||
auto status = rom.LoadFromFile(arg_vec[0], RomLoadOptions::CliDefaults());
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -318,7 +318,7 @@ absl::Status ModernCLI::HandleBpsPatchCommand(const std::vector<std::string>& ar
|
||||
}
|
||||
|
||||
absl::Status ModernCLI::HandleRomInfoCommand(const std::vector<std::string>& args) {
|
||||
Open handler;
|
||||
RomInfo handler;
|
||||
return handler.Run(args);
|
||||
}
|
||||
|
||||
|
||||
294
src/cli/service/proposal_registry.cc
Normal file
294
src/cli/service/proposal_registry.cc
Normal file
@@ -0,0 +1,294 @@
|
||||
#include "cli/service/proposal_registry.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
#include "util/macro.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path DetermineDefaultRoot() {
|
||||
if (const char* env_root = std::getenv("YAZE_PROPOSAL_ROOT")) {
|
||||
return std::filesystem::path(env_root);
|
||||
}
|
||||
std::error_code ec;
|
||||
auto temp_dir = std::filesystem::temp_directory_path(ec);
|
||||
if (ec) {
|
||||
return std::filesystem::current_path() / "yaze" / "proposals";
|
||||
}
|
||||
return temp_dir / "yaze" / "proposals";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProposalRegistry& ProposalRegistry::Instance() {
|
||||
static ProposalRegistry* instance = new ProposalRegistry();
|
||||
return *instance;
|
||||
}
|
||||
|
||||
ProposalRegistry::ProposalRegistry()
|
||||
: root_directory_(DetermineDefaultRoot()) {}
|
||||
|
||||
void ProposalRegistry::SetRootDirectory(const std::filesystem::path& root) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
root_directory_ = root;
|
||||
(void)EnsureRootExistsLocked();
|
||||
}
|
||||
|
||||
const std::filesystem::path& ProposalRegistry::RootDirectory() const {
|
||||
return root_directory_;
|
||||
}
|
||||
|
||||
absl::Status ProposalRegistry::EnsureRootExistsLocked() {
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(root_directory_, ec)) {
|
||||
if (!std::filesystem::create_directories(root_directory_, ec) && ec) {
|
||||
return absl::InternalError(absl::StrCat(
|
||||
"Failed to create proposal root at ", root_directory_.string(),
|
||||
": ", ec.message()));
|
||||
}
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
std::string ProposalRegistry::GenerateProposalIdLocked() {
|
||||
absl::Time now = absl::Now();
|
||||
std::string time_component = absl::FormatTime("%Y%m%dT%H%M%S", now,
|
||||
absl::LocalTimeZone());
|
||||
++sequence_;
|
||||
return absl::StrCat("proposal-", time_component, "-", sequence_);
|
||||
}
|
||||
|
||||
std::filesystem::path ProposalRegistry::ProposalDirectory(
|
||||
absl::string_view proposal_id) const {
|
||||
return root_directory_ / std::string(proposal_id);
|
||||
}
|
||||
|
||||
absl::StatusOr<ProposalRegistry::ProposalMetadata>
|
||||
ProposalRegistry::CreateProposal(absl::string_view sandbox_id,
|
||||
absl::string_view prompt,
|
||||
absl::string_view description) {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
RETURN_IF_ERROR(EnsureRootExistsLocked());
|
||||
|
||||
std::string id = GenerateProposalIdLocked();
|
||||
std::filesystem::path proposal_dir = ProposalDirectory(id);
|
||||
lock.unlock();
|
||||
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::create_directories(proposal_dir, ec) && ec) {
|
||||
return absl::InternalError(absl::StrCat(
|
||||
"Failed to create proposal directory at ", proposal_dir.string(),
|
||||
": ", ec.message()));
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
proposals_[id] = ProposalMetadata{
|
||||
.id = id,
|
||||
.sandbox_id = std::string(sandbox_id),
|
||||
.description = std::string(description),
|
||||
.prompt = std::string(prompt),
|
||||
.status = ProposalStatus::kPending,
|
||||
.created_at = absl::Now(),
|
||||
.reviewed_at = std::nullopt,
|
||||
.diff_path = proposal_dir / "diff.txt",
|
||||
.log_path = proposal_dir / "execution.log",
|
||||
.screenshots = {},
|
||||
.bytes_changed = 0,
|
||||
.commands_executed = 0,
|
||||
};
|
||||
|
||||
return proposals_.at(id);
|
||||
}
|
||||
|
||||
absl::Status ProposalRegistry::RecordDiff(const std::string& proposal_id,
|
||||
absl::string_view diff_content) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = proposals_.find(proposal_id);
|
||||
if (it == proposals_.end()) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrCat("Proposal not found: ", proposal_id));
|
||||
}
|
||||
|
||||
std::ofstream diff_file(it->second.diff_path, std::ios::out);
|
||||
if (!diff_file.is_open()) {
|
||||
return absl::InternalError(absl::StrCat(
|
||||
"Failed to open diff file: ", it->second.diff_path.string()));
|
||||
}
|
||||
|
||||
diff_file << diff_content;
|
||||
diff_file.close();
|
||||
|
||||
// Update bytes_changed metric (rough estimate based on diff size)
|
||||
it->second.bytes_changed = static_cast<int>(diff_content.size());
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status ProposalRegistry::AppendLog(const std::string& proposal_id,
|
||||
absl::string_view log_entry) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = proposals_.find(proposal_id);
|
||||
if (it == proposals_.end()) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrCat("Proposal not found: ", proposal_id));
|
||||
}
|
||||
|
||||
std::ofstream log_file(it->second.log_path,
|
||||
std::ios::out | std::ios::app);
|
||||
if (!log_file.is_open()) {
|
||||
return absl::InternalError(absl::StrCat(
|
||||
"Failed to open log file: ", it->second.log_path.string()));
|
||||
}
|
||||
|
||||
log_file << absl::FormatTime("[%Y-%m-%d %H:%M:%S] ", absl::Now(),
|
||||
absl::LocalTimeZone())
|
||||
<< log_entry << "\n";
|
||||
log_file.close();
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status ProposalRegistry::AddScreenshot(
|
||||
const std::string& proposal_id,
|
||||
const std::filesystem::path& screenshot_path) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = proposals_.find(proposal_id);
|
||||
if (it == proposals_.end()) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrCat("Proposal not found: ", proposal_id));
|
||||
}
|
||||
|
||||
// Verify screenshot exists
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(screenshot_path, ec)) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrCat("Screenshot file not found: ", screenshot_path.string()));
|
||||
}
|
||||
|
||||
it->second.screenshots.push_back(screenshot_path);
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status ProposalRegistry::UpdateStatus(const std::string& proposal_id,
|
||||
ProposalStatus status) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = proposals_.find(proposal_id);
|
||||
if (it == proposals_.end()) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrCat("Proposal not found: ", proposal_id));
|
||||
}
|
||||
|
||||
it->second.status = status;
|
||||
it->second.reviewed_at = absl::Now();
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::StatusOr<ProposalRegistry::ProposalMetadata>
|
||||
ProposalRegistry::GetProposal(const std::string& proposal_id) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = proposals_.find(proposal_id);
|
||||
if (it == proposals_.end()) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrCat("Proposal not found: ", proposal_id));
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::vector<ProposalRegistry::ProposalMetadata>
|
||||
ProposalRegistry::ListProposals(std::optional<ProposalStatus> filter_status) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::vector<ProposalMetadata> result;
|
||||
|
||||
for (const auto& [id, metadata] : proposals_) {
|
||||
if (!filter_status.has_value() || metadata.status == *filter_status) {
|
||||
result.push_back(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by creation time (newest first)
|
||||
std::sort(result.begin(), result.end(),
|
||||
[](const ProposalMetadata& a, const ProposalMetadata& b) {
|
||||
return a.created_at > b.created_at;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::StatusOr<ProposalRegistry::ProposalMetadata>
|
||||
ProposalRegistry::GetLatestPendingProposal() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
const ProposalMetadata* latest = nullptr;
|
||||
for (const auto& [id, metadata] : proposals_) {
|
||||
if (metadata.status == ProposalStatus::kPending) {
|
||||
if (!latest || metadata.created_at > latest->created_at) {
|
||||
latest = &metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!latest) {
|
||||
return absl::NotFoundError("No pending proposals found");
|
||||
}
|
||||
|
||||
return *latest;
|
||||
}
|
||||
|
||||
absl::Status ProposalRegistry::RemoveProposal(const std::string& proposal_id) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = proposals_.find(proposal_id);
|
||||
if (it == proposals_.end()) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrCat("Proposal not found: ", proposal_id));
|
||||
}
|
||||
|
||||
std::filesystem::path proposal_dir = ProposalDirectory(proposal_id);
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(proposal_dir, ec);
|
||||
if (ec) {
|
||||
return absl::InternalError(absl::StrCat(
|
||||
"Failed to remove proposal directory: ", ec.message()));
|
||||
}
|
||||
|
||||
proposals_.erase(it);
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::StatusOr<int> ProposalRegistry::CleanupOlderThan(absl::Duration max_age) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
absl::Time cutoff = absl::Now() - max_age;
|
||||
int removed_count = 0;
|
||||
|
||||
std::vector<std::string> to_remove;
|
||||
for (const auto& [id, metadata] : proposals_) {
|
||||
if (metadata.created_at < cutoff) {
|
||||
to_remove.push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& id : to_remove) {
|
||||
std::filesystem::path proposal_dir = ProposalDirectory(id);
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(proposal_dir, ec);
|
||||
// Continue even if removal fails
|
||||
proposals_.erase(id);
|
||||
++removed_count;
|
||||
}
|
||||
|
||||
return removed_count;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
123
src/cli/service/proposal_registry.h
Normal file
123
src/cli/service/proposal_registry.h
Normal file
@@ -0,0 +1,123 @@
|
||||
#ifndef YAZE_SRC_CLI_SERVICE_PROPOSAL_REGISTRY_H_
|
||||
#define YAZE_SRC_CLI_SERVICE_PROPOSAL_REGISTRY_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
// ProposalRegistry tracks agent-generated ROM modification proposals,
|
||||
// storing metadata, diffs, execution logs, and optional screenshots for
|
||||
// human review. Each proposal is associated with a sandbox ROM copy.
|
||||
//
|
||||
// Proposals follow a lifecycle:
|
||||
// 1. Created - agent generates a proposal during `agent run`
|
||||
// 2. Reviewed - user inspects via `agent diff` or TUI
|
||||
// 3. Accepted - changes committed to main ROM via `agent commit`
|
||||
// 4. Rejected - sandbox cleaned up via `agent revert`
|
||||
class ProposalRegistry {
|
||||
public:
|
||||
enum class ProposalStatus {
|
||||
kPending, // Created but not yet reviewed
|
||||
kAccepted, // User accepted the changes
|
||||
kRejected, // User rejected the changes
|
||||
};
|
||||
|
||||
struct ProposalMetadata {
|
||||
std::string id;
|
||||
std::string sandbox_id;
|
||||
std::string description;
|
||||
std::string prompt; // Original agent prompt that created this proposal
|
||||
ProposalStatus status;
|
||||
absl::Time created_at;
|
||||
std::optional<absl::Time> reviewed_at;
|
||||
|
||||
// File paths relative to proposal directory
|
||||
std::filesystem::path diff_path;
|
||||
std::filesystem::path log_path;
|
||||
std::vector<std::filesystem::path> screenshots;
|
||||
|
||||
// Statistics
|
||||
int bytes_changed;
|
||||
int commands_executed;
|
||||
};
|
||||
|
||||
static ProposalRegistry& Instance();
|
||||
|
||||
// Set the root directory for storing proposal data. If not set, uses
|
||||
// YAZE_PROPOSAL_ROOT env var or defaults to system temp directory.
|
||||
void SetRootDirectory(const std::filesystem::path& root);
|
||||
|
||||
const std::filesystem::path& RootDirectory() const;
|
||||
|
||||
// Creates a new proposal linked to the given sandbox. The proposal directory
|
||||
// is created under the root, and metadata is initialized.
|
||||
absl::StatusOr<ProposalMetadata> CreateProposal(
|
||||
absl::string_view sandbox_id,
|
||||
absl::string_view prompt,
|
||||
absl::string_view description);
|
||||
|
||||
// Records a diff between original and modified ROM for the proposal.
|
||||
// The diff content is written to a file within the proposal directory.
|
||||
absl::Status RecordDiff(const std::string& proposal_id,
|
||||
absl::string_view diff_content);
|
||||
|
||||
// Appends log output from command execution to the proposal's log file.
|
||||
absl::Status AppendLog(const std::string& proposal_id,
|
||||
absl::string_view log_entry);
|
||||
|
||||
// Adds a screenshot path to the proposal metadata. Screenshots should
|
||||
// be copied into the proposal directory before calling this.
|
||||
absl::Status AddScreenshot(const std::string& proposal_id,
|
||||
const std::filesystem::path& screenshot_path);
|
||||
|
||||
// Updates the proposal status (pending -> accepted/rejected) and sets
|
||||
// the review timestamp.
|
||||
absl::Status UpdateStatus(const std::string& proposal_id,
|
||||
ProposalStatus status);
|
||||
|
||||
// Returns the metadata for a specific proposal.
|
||||
absl::StatusOr<ProposalMetadata> GetProposal(
|
||||
const std::string& proposal_id) const;
|
||||
|
||||
// Lists all proposals, optionally filtered by status.
|
||||
std::vector<ProposalMetadata> ListProposals(
|
||||
std::optional<ProposalStatus> filter_status = std::nullopt) const;
|
||||
|
||||
// Returns the most recently created proposal that is still pending.
|
||||
absl::StatusOr<ProposalMetadata> GetLatestPendingProposal() const;
|
||||
|
||||
// Removes a proposal and its associated files from disk and memory.
|
||||
absl::Status RemoveProposal(const std::string& proposal_id);
|
||||
|
||||
// Deletes all proposals older than the specified age.
|
||||
absl::StatusOr<int> CleanupOlderThan(absl::Duration max_age);
|
||||
|
||||
private:
|
||||
ProposalRegistry();
|
||||
|
||||
absl::Status EnsureRootExistsLocked();
|
||||
std::string GenerateProposalIdLocked();
|
||||
std::filesystem::path ProposalDirectory(absl::string_view proposal_id) const;
|
||||
|
||||
std::filesystem::path root_directory_;
|
||||
mutable std::mutex mutex_;
|
||||
std::unordered_map<std::string, ProposalMetadata> proposals_;
|
||||
int sequence_ = 0;
|
||||
};
|
||||
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_SRC_CLI_SERVICE_PROPOSAL_REGISTRY_H_
|
||||
@@ -51,6 +51,20 @@ ResourceSchema MakeRomSchema() {
|
||||
schema.resource = "rom";
|
||||
schema.description = "ROM validation, diffing, and snapshot helpers.";
|
||||
|
||||
ResourceAction info_action;
|
||||
info_action.name = "info";
|
||||
info_action.synopsis = "z3ed rom info --rom <file>";
|
||||
info_action.stability = "stable";
|
||||
info_action.arguments = {
|
||||
ResourceArgument{"--rom", "path", true, "Path to ROM file configured via global flag."},
|
||||
};
|
||||
info_action.effects = {
|
||||
"Reads ROM from disk and displays basic information (title, size, filename)."};
|
||||
info_action.returns = {
|
||||
{"title", "string", "ROM internal title from header."},
|
||||
{"size", "integer", "ROM file size in bytes."},
|
||||
{"filename", "string", "Full path to the ROM file."}};
|
||||
|
||||
ResourceAction validate_action;
|
||||
validate_action.name = "validate";
|
||||
validate_action.synopsis = "z3ed rom validate --rom <file>";
|
||||
@@ -91,7 +105,7 @@ ResourceSchema MakeRomSchema() {
|
||||
generate_action.returns = {
|
||||
{"artifact", "path", "Absolute path to the generated golden image."}};
|
||||
|
||||
schema.actions = {validate_action, diff_action, generate_action};
|
||||
schema.actions = {info_action, validate_action, diff_action, generate_action};
|
||||
return schema;
|
||||
}
|
||||
|
||||
@@ -231,7 +245,7 @@ ResourceSchema MakeAgentSchema() {
|
||||
ResourceSchema schema;
|
||||
schema.resource = "agent";
|
||||
schema.description =
|
||||
"Agent workflow helpers including planning, diffing, and schema discovery.";
|
||||
"Agent workflow helpers including planning, diffing, listing, and schema discovery.";
|
||||
|
||||
ResourceAction describe_action;
|
||||
describe_action.name = "describe";
|
||||
@@ -245,7 +259,31 @@ ResourceSchema MakeAgentSchema() {
|
||||
{"schema", "object",
|
||||
"JSON schema describing resource arguments and semantics."}};
|
||||
|
||||
schema.actions = {describe_action};
|
||||
ResourceAction list_action;
|
||||
list_action.name = "list";
|
||||
list_action.synopsis = "z3ed agent list";
|
||||
list_action.stability = "prototype";
|
||||
list_action.arguments = {};
|
||||
list_action.effects = {{"reads", "proposal_registry"}};
|
||||
list_action.returns = {
|
||||
{"proposals", "array",
|
||||
"List of all proposals with ID, status, prompt, and metadata."}};
|
||||
|
||||
ResourceAction diff_action;
|
||||
diff_action.name = "diff";
|
||||
diff_action.synopsis = "z3ed agent diff [--proposal-id <id>]";
|
||||
diff_action.stability = "prototype";
|
||||
diff_action.arguments = {
|
||||
ResourceArgument{"--proposal-id", "string", false,
|
||||
"Optional proposal ID to view specific proposal. Defaults to latest pending."},
|
||||
};
|
||||
diff_action.effects = {{"reads", "proposal_registry"}, {"reads", "sandbox"}};
|
||||
diff_action.returns = {
|
||||
{"diff", "string", "Unified diff showing changes to ROM."},
|
||||
{"log", "string", "Execution log of commands run."},
|
||||
{"metadata", "object", "Proposal metadata including status and timestamps."}};
|
||||
|
||||
schema.actions = {describe_action, list_action, diff_action};
|
||||
return schema;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ add_executable(
|
||||
cli/handlers/project.cc
|
||||
cli/handlers/agent.cc
|
||||
cli/service/ai_service.cc
|
||||
cli/service/proposal_registry.cc
|
||||
cli/service/resource_catalog.cc
|
||||
cli/service/rom_sandbox_manager.cc
|
||||
cli/service/gemini_ai_service.cc
|
||||
|
||||
@@ -102,6 +102,11 @@ class DungeonListObjects : public CommandHandler {
|
||||
absl::Status Run(const std::vector<std::string>& arg_vec) override;
|
||||
};
|
||||
|
||||
class RomInfo : public CommandHandler {
|
||||
public:
|
||||
absl::Status Run(const std::vector<std::string>& arg_vec) override;
|
||||
};
|
||||
|
||||
class RomValidate : public CommandHandler {
|
||||
public:
|
||||
absl::Status Run(const std::vector<std::string>& arg_vec) override;
|
||||
@@ -151,7 +156,7 @@ class Open : public CommandHandler {
|
||||
public:
|
||||
absl::Status Run(const std::vector<std::string>& arg_vec) override {
|
||||
auto const& arg = arg_vec[0];
|
||||
RETURN_IF_ERROR(rom_.LoadFromFile(arg))
|
||||
RETURN_IF_ERROR(rom_.LoadFromFile(arg, RomLoadOptions::CliDefaults()))
|
||||
std::cout << "Title: " << rom_.title() << std::endl;
|
||||
std::cout << "Size: 0x" << std::hex << rom_.size() << std::endl;
|
||||
return absl::OkStatus();
|
||||
|
||||
Reference in New Issue
Block a user