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:
scawful
2025-10-01 18:18:48 -04:00
parent 04a4d04f4e
commit 02c6985201
13 changed files with 1373 additions and 72 deletions

View File

@@ -93,11 +93,18 @@ The generative workflow has been refined to incorporate more detailed planning a
- **Project Scaffolding**: Implemented.
### Phase 4: Agentic Framework & Generative AI (In Progress)
- **`z3ed agent` command**: Implemented with `run`, `plan`, `diff`, `test`, `commit`, `revert`, and `learn` subcommands.
- **`z3ed agent` command**: Implemented with `run`, `plan`, `diff`, `test`, `commit`, `revert`, `describe`, `learn`, and `list` subcommands.
- **Resource Catalog System**: ✅ Complete - comprehensive schema for all CLI commands with effects and returns metadata.
- **Agent Describe Command**: ✅ Fully operational - exports command catalog in JSON/YAML formats for AI consumption.
- **Agent List Command**: ✅ Complete - enumerates all proposals with status and metadata.
- **Agent Diff Enhancement**: ✅ Complete - reads proposals from registry, supports `--proposal-id` flag, displays execution logs and metadata.
- **Machine-Readable API**: ✅ `docs/api/z3ed-resources.yaml` generated and maintained for automation.
- **AI Model Interaction**: In progress, with `MockAIService` and `GeminiAIService` (conditional) implemented.
- **Execution Loop (MCP)**: In progress, with command parsing and execution logic.
- **Leveraging `ImGuiTestEngine`**: In progress, with `agent test` subcommand.
- **Granular Data Commands**: Not started, but planned.
- **Leveraging `ImGuiTestEngine`**: In progress, with `agent test` subcommand for GUI verification.
- **Sandbox ROM Management**: ✅ Complete - `RomSandboxManager` operational with full lifecycle management.
- **Proposal Tracking**: ✅ Complete - `ProposalRegistry` implemented with metadata, diffs, logs, and lifecycle management.
- **Granular Data Commands**: Partially complete - rom, palette, overworld, dungeon commands operational.
- **SpriteBuilder CLI**: Deprioritized.
### Phase 5: Code Structure & UX Improvements (Completed)
@@ -108,6 +115,27 @@ The generative workflow has been refined to incorporate more detailed planning a
- **Build System**: Streamlined CMake configuration with proper dependency management and conditional compilation.
- **Code Quality**: Resolved linting errors and improved code maintainability through better header organization and forward declarations.
### Phase 6: Resource Catalogue & API Documentation (✅ Completed - Oct 1, 2025)
- **Resource Schema System**: ✅ Comprehensive schema definitions for all CLI resources (ROM, Patch, Palette, Overworld, Dungeon, Agent).
- **Metadata Annotations**: ✅ All commands annotated with arguments, effects, returns, and stability levels.
- **Serialization Framework**: ✅ Dual-format export (JSON compact, YAML human-readable) with resource filtering.
- **Agent Describe Command**: ✅ Full implementation with `--format`, `--resource`, `--output`, `--version` flags.
- **API Documentation Generation**: ✅ Automated generation of `docs/api/z3ed-resources.yaml` for AI/tooling consumption.
- **Flag-Based Dispatch**: ✅ Hardened command routing - all ROM commands use `FLAGS_rom` consistently.
- **ROM Info Fix**: ✅ Created dedicated `RomInfo` handler, resolving segfault issue.
**Key Achievements**:
- Machine-readable API catalog enables LLM integration for automated ROM hacking workflows
- Comprehensive command documentation with argument types, effects, and return schemas
- Stable foundation for AI agents to discover and invoke CLI commands programmatically
- Validation layer for ensuring command compatibility and argument correctness
**Testing Coverage**:
- ✅ All ROM commands tested: `info`, `validate`, `diff`, `generate-golden`
- ✅ Agent describe tested: YAML output, JSON output, resource filtering, file generation
- ✅ Help system integration verified with updated command listings
- ✅ Build system validated on macOS (arm64) with no critical warnings
## 8. Agentic Framework Architecture - Advanced Dive
The agentic framework is designed to allow an AI agent to make edits to the ROM based on high-level natural language prompts. The framework is built around the `z3ed` CLI and the `ImGuiTestEngine`. This section provides a more advanced look into its architecture and future development.
@@ -118,10 +146,12 @@ The `z3ed agent` command is the main entry point for the agent. It has the follo
- `run --prompt "..."`: Executes a prompt by generating and running a sequence of `z3ed` commands.
- `plan --prompt "..."`: Shows the sequence of `z3ed` commands the AI plans to execute.
- `diff`: Shows a diff of the changes made to the ROM after running a prompt.
- `diff [--proposal-id <id>]`: Shows a diff of the changes made to the ROM after running a prompt. Displays the latest pending proposal by default, or a specific proposal if ID is provided.
- `list`: Lists all proposals with their status, creation time, prompt, and execution statistics.
- `test --prompt "..."`: Generates changes and then runs an `ImGuiTestEngine` test to verify them.
- `commit`: Saves the modified ROM and any new assets to the project.
- `revert`: Reverts the changes made by the agent.
- `describe [--resource <name>]`: Returns machine-readable schemas for CLI commands, enabling AI/LLM integration.
- `learn --description "..."`: Records a sequence of user actions (CLI commands and GUI interactions) and associates them with a natural language description, allowing the agent to learn new workflows.
### 8.2. The Agentic Loop (MCP) - Detailed Workflow
@@ -414,6 +444,37 @@ Allowing an LLM to drive the ImGui UI safely requires a structured bridge betwee
- **Synchronization Primitives**: Provide `WaitForIdle`, `WaitForCondition`, and `Delay` primitives so LLMs can coordinate with frame updates. Each primitive enforces timeouts and returns explicit success/failure statuses.
- **State Queries**: Implement reflection endpoints retrieving ImGui widget hierarchy, enabling the agent to confirm UI states before issuing the next action—mirroring how `ImGuiTestEngine` DSL scripts work today.
#### 13.1.1. Transport & Envelope
- **Session bootstrap**: `yaze_test --automation=<socket path>` spins up the harness and prints a connection URI. The CLI or external agent opens a persistent stream (Unix domain socket on macOS/Linux, named pipe + overlapped IO on Windows). TLS is out-of-scope; trust is derived from local IPC.
- **Message format**: Each frame is a length-prefixed JSON envelope with optional binary attachments. Core fields:
```json
{
"id": "req-42",
"type": "event" | "query" | "expect" | "control",
"payload": { /* type-specific body */ },
"attachments": [
{ "slot": 0, "mime": "image/png" }
]
}
```
Binary blobs (e.g., screenshots) follow immediately after the JSON payload in the same frame to avoid out-of-band coordination.
- **Streaming semantics**: Responses reuse the `id` field and include `status`, `error`, and optional attachments. Long-running operations (`WaitForCondition`) stream periodic `progress` updates before returning `status: "ok"` or `status: "timeout"`.
#### 13.1.2. Harness Runtime Lifecycle
1. **Attach**: Agent sends a `control` message (`{"command":"attach"}`) to lock in a session. Harness responds with negotiated capabilities (available input devices, screenshot formats, rate limits).
2. **Activate context**: Agent issues an `event` to focus a specific ImGui context (e.g., "main", "palette_editor"). Harness binds to the corresponding `ImGuiTestEngine` backend fixture.
3. **Execute actions**: Agent streams `event` objects (`click`, `drag`, `keystroke`, `text_input`). Harness feeds them into the ImGui event queue at the start of the next frame, waits for the frame to settle, then replies.
4. **Query & assert**: Agent interleaves `query` messages (`get_widget_tree`, `capture_screenshot`, `read_value`) and `expect` messages (`assert_property`, `assert_pixel`). Harness routes these to existing ImGuiTestEngine inspectors, lifting the results into structured JSON.
5. **Detach**: Agent issues `{"command":"detach"}` (or connection closes). Harness flushes pending frames, releases sandbox locks, and tears down the socket.
#### 13.1.3. Integration with `z3ed agent`
- **Plan annotation**: The CLI plan schema gains a new step kind `imgui_action` with fields `harness_uri`, `actions[]`, and optional `expect[]`. During execution `z3ed agent run` opens the harness stream, feeds each action, and short-circuits on first failure.
- **Sandbox awareness**: Harness sessions inherit the active sandbox ROM path from `RomSandboxManager`, ensuring UI assertions operate on the same data snapshot as CLI mutations.
- **Telemetry hooks**: Every harness response is appended to the proposal timeline (see §12) with thumbnails for screenshots. Failures bubble up as structured errors with hints (`"missing_widget": "Palette/Cell[12]"`).
### 13.2. Safety & Sandboxing
- **Read-Only Default**: Harness sessions start in read-only mode; mutation commands must explicitly request escalation after presenting a plan (triggering a UI prompt for the user to authorize). Without authorization, only `capture` and `assert` operations succeed.

View File

@@ -1,6 +1,6 @@
# z3ed Agentic Workflow Implementation Plan
_Last updated: 2025-10-01 (afternoon update)_
_Last updated: 2025-10-01 (final update - Phase 6 + AW-02 complete)_
This plan decomposes the design additions (Sections 1115 of `E6-z3ed-cli-design.md`) into actionable engineering tasks. Each workstream contains milestones, owners (TBD), blocking dependencies, and expected deliverables.
@@ -14,18 +14,37 @@ This plan decomposes the design additions (Sections 1115 of `E6-z3ed-cli-desi
| Verification Pipeline | Build layered testing + CI coverage. | Phase 6+ | Integrates with harness + CLI suites. |
| Telemetry & Learning | Capture signals to improve prompts + heuristics. | Phase 8 | Optional/opt-in features. |
### Progress snapshot — 2025-10-01 final update (Phase 6 + Agent Diff & List Complete)
- ✅ CLI global flag passthrough now preserves subcommand options, letting `agent describe` and palette routines accept both space-separated and `--flag=value` styles alongside the updated help text.
-`agent describe --format yaml` writes catalog data end-to-end; JSON format also working correctly.
- ✅ Expanded `ImGuiTestHarness` design with concrete transport, message envelope, and lifecycle details to unblock IT-01 spike.
- ✅ Fixed `rom info` segfault by creating dedicated `RomInfo` handler that properly uses the `--rom` flag instead of positional arguments. Command now works correctly with flag-based dispatch.
- ✅ Added `rom info` action to resource catalog with proper schema documentation including return values (title, size, filename).
- ✅ Generated and committed `docs/api/z3ed-resources.yaml` as authoritative machine-readable API reference for CLI automation (RC-02 complete).
-**Implemented `ProposalRegistry` service** for tracking agent-generated ROM modifications with metadata, diffs, logs, and screenshots.
-**Integrated ProposalRegistry into `agent run` workflow** - all command executions are now logged and tracked.
-**RomSandboxManager fully operational** with lifecycle management for proposal tracking.
-**Enhanced `agent diff` command** to read and display proposal diffs from ProposalRegistry with detailed metadata, execution logs, and next-step guidance.
-**Added `--proposal-id` flag to `agent diff`** for viewing specific proposals (not just latest pending).
-**Implemented `agent list` command** to enumerate all proposals with status filtering and metadata display.
-**Updated resource catalog** with agent list and diff actions including comprehensive argument and return schemas.
-**Regenerated API documentation** (`docs/api/z3ed-resources.yaml`) with all new agent commands.
## 2. Task Backlog
| ID | Task | Workstream | Type | Status | Dependencies |
|----|------|------------|------|--------|--------------|
| RC-01 | Define schema for `ResourceCatalog` entries and implement serialization helpers. | Resource Catalogue | Code | In Progress | Palette/Overworld/ROM/Patch/Dungeon actions annotated with effects/returns; serialization covered by unit tests; CLI wiring ongoing |
| RC-02 | Auto-generate `docs/api/z3ed-resources.yaml` from command annotations. | Resource Catalogue | Tooling | Blocked | RC-01 |
| RC-03 | Implement `z3ed agent describe` CLI surface returning JSON schemas. | Resource Catalogue | Code | Prototype | `agent describe` outputs JSON; broaden resource coverage next |
| RC-01 | Define schema for `ResourceCatalog` entries and implement serialization helpers. | Resource Catalogue | Code | Done | Schema system complete with all resource types documented |
| RC-02 | Auto-generate `docs/api/z3ed-resources.yaml` from command annotations. | Resource Catalogue | Tooling | Done | Generated and committed to docs/api/ |
| RC-03 | Implement `z3ed agent describe` CLI surface returning JSON schemas. | Resource Catalogue | Code | Done | Both YAML and JSON output formats working |
| RC-04 | Integrate schema export with TUI command palette + help overlays. | Resource Catalogue | UX | Planned | RC-03 |
| AW-01 | Implement sandbox ROM cloning and tracking (`RomSandboxManager`). | Acceptance Workflow | Code | Prototype | ROM snapshot infra |
| AW-02 | Build proposal registry service storing diffs, logs, screenshots. | Acceptance Workflow | Code | Planned | AW-01 |
| RC-05 | Harden CLI command routing/flag parsing to unblock agent automation. | Resource Catalogue | Code | Done | Fixed rom info handler to use FLAGS_rom |
| AW-01 | Implement sandbox ROM cloning and tracking (`RomSandboxManager`). | Acceptance Workflow | Code | Done | ROM sandbox manager operational with lifecycle management |
| AW-02 | Build proposal registry service storing diffs, logs, screenshots. | Acceptance Workflow | Code | Done | ProposalRegistry implemented and integrated with agent run workflow |
| AW-03 | Add ImGui drawer for proposals with accept/reject controls. | Acceptance Workflow | UX | Planned | AW-02 |
| AW-04 | Implement policy evaluation for gating accept buttons. | Acceptance Workflow | Code | Planned | AW-03 |
| AW-05 | Draft `.z3ed-diff` hybrid schema (binary deltas + JSON metadata). | Acceptance Workflow | Design | Planned | AW-01 |
| IT-01 | Create `ImGuiTestHarness` IPC service embedded in `yaze_test`. | ImGuiTest Bridge | Code | Planned | Harness transport decision |
| IT-02 | Implement CLI agent step translation (`imgui_action` → harness call). | ImGuiTest Bridge | Code | Planned | IT-01 |
| IT-03 | Provide synchronization primitives (`WaitForIdle`, etc.). | ImGuiTest Bridge | Code | Planned | IT-01 |
@@ -39,18 +58,152 @@ _Status Legend: Prototype · In Progress · Planned · Blocked · Done_
## 3. Immediate Next Steps
1. Automate catalog export into `docs/api/z3ed-resources.yaml` and snapshot `agent describe` outputs for regression coverage (`RC-02`, `RC-03`).
2. Wire `RomSandboxManager` into proposal lifecycle (metadata persistence, cleanup) and validate with agent diff flow (`AW-01`).
3. Spike IPC options for `ImGuiTestHarness` (socket vs. HTTP vs. shared memory) and document findings.
1.**COMPLETED**: Automated catalog export into `docs/api/z3ed-resources.yaml` - both JSON and YAML formats work correctly (RC-02, RC-03).
2. **COMPLETED**: Fixed `rom info` crash - created dedicated `RomInfo` handler that uses `FLAGS_rom` instead of positional arguments (RC-05).
3. **COMPLETED**: Wired `RomSandboxManager` and `ProposalRegistry` into agent run workflow with full logging and metadata tracking (AW-01, AW-02).
4.**COMPLETED**: Enhanced `agent diff` command to read and display proposal diffs from ProposalRegistry with formatted output, execution logs, and next-step guidance.
5.**COMPLETED**: Added `agent list` command to enumerate all proposals with status filtering.
6.**COMPLETED**: Added `--proposal-id` flag to `agent diff` for viewing specific proposals.
7.**COMPLETED**: Updated resource catalog with agent list and diff actions including arguments and return schemas.
8. **PLANNED**: Add ImGui drawer for proposals with accept/reject controls (AW-03).
9. **PLANNED**: Spike IPC options for `ImGuiTestHarness` (socket vs. HTTP vs. shared memory) and document findings (IT-01).
10. **PLANNED**: Integrate schema export with TUI command palette + help overlays (RC-04).
## 4. Open Questions
- What serialization format should the proposal registry adopt for diff payloads (binary vs. textual vs. hybrid)?
- How should the harness authenticate escalation requests for mutation actions?
- Can we reuse existing regression test infrastructure for nightly ImGui runs or should we spin up a dedicated binary?
- What serialization format should the proposal registry adopt for diff payloads (binary vs. textual vs. hybrid)? \
➤ Decision: pursue a hybrid package (`.z3ed-diff`) that wraps binary tile/object deltas alongside a JSON metadata envelope (identifiers, texture descriptors, preview palette info). Capture format draft under RC/AW backlog.
- How should the harness authenticate escalation requests for mutation actions? \
➤ Still open—evaluate shared-secret vs. interactive user prompt in the harness spike (IT-01).
- Can we reuse existing regression test infrastructure for nightly ImGui runs or should we spin up a dedicated binary? \
➤ Investigate during the ImGuiTestHarness spike; compare extending `yaze_test` jobs versus introducing a lightweight automation runner.
## 5. References
## 5. Completed Work Summary
- `docs/E6-z3ed-cli-design.md`
- `docs/api/z3ed-resources.yaml`
- `src/cli/service/resource_catalog.h` (prototype)
### Resource Catalogue Workstream (RC) - ✅ COMPLETE
The Resource Catalogue workstream has been successfully completed, providing a foundation for AI-driven automation:
**Implementation Details**:
- Created comprehensive schema system in `src/cli/service/resource_catalog.{h,cc}`
- Implemented resource catalog for: ROM, Patch, Palette, Overworld, Dungeon, and Agent commands
- Each resource includes: name, description, actions, arguments, effects, and return values
- Built dual-format serialization: JSON (compact) and YAML (human-readable)
**Key Fixes**:
- Fixed `rom info` segfault by creating dedicated `RomInfo` handler using `FLAGS_rom`
- Added `rom info` action to resource schema with proper metadata
- Ensured all ROM commands consistently use flag-based dispatch
**Generated Artifacts**:
- `docs/api/z3ed-resources.yaml` - Authoritative machine-readable API reference
- Both JSON and YAML output formats validated and working
- Resource filtering capability (`--resource <name>`) operational
**Command Examples**:
```bash
# View all resources in YAML
z3ed agent describe --format yaml
# Get specific resource as JSON
z3ed agent describe --format json --resource rom
# Generate documentation file
z3ed agent describe --format yaml --output docs/api/z3ed-resources.yaml
```
**Testing Results**:
All commands tested and verified working:
-`z3ed rom info --rom=zelda3.sfc` - displays title, size, filename
-`z3ed rom validate --rom=zelda3.sfc` - verifies checksum and header
-`z3ed agent describe --format yaml` - outputs complete catalog
-`z3ed agent describe --format json --resource rom` - filters by resource
### Acceptance Workflow (AW-01, AW-02) - ✅ CORE COMPLETE
The foundational infrastructure for proposal tracking and review is now operational:
**RomSandboxManager Implementation** (AW-01):
- Singleton service managing isolated ROM copies for agent proposals
- Sandboxes created in `YAZE_SANDBOX_ROOT` (env var) or system temp directory
- Automatic directory creation and ROM file cloning
- Active sandbox tracking for current agent session
- Cleanup utilities for removing old sandboxes
**ProposalRegistry Implementation** (AW-02):
- Comprehensive tracking of agent-generated ROM modifications
- Stores proposal metadata: ID, sandbox ID, prompt, description, timestamps
- Records execution diffs in `diff.txt` within proposal directory
- Appends command execution logs to `execution.log` with timestamps
- Support for screenshot attachments (path tracking)
- Proposal lifecycle: Pending → Accepted/Rejected
- Query capabilities: get by ID, list all, filter by status, find latest pending
**Agent Run Integration**:
- `agent run` now creates sandbox + proposal automatically
- All command executions logged with timestamps and status
- Success/failure outcomes captured in proposal logs
- User feedback includes proposal ID and sandbox path for review
- Foundation ready for `agent diff`, `agent commit`, `agent revert` enhancements
**Agent Diff Enhancement** (Completed Oct 1, 2025):
- Reads proposal diffs from ProposalRegistry automatically
- Displays detailed metadata: proposal ID, status, timestamps, command count
- Shows diff content from proposal directory
- Displays execution log (first 50 lines, with truncation for long logs)
- Provides next-step guidance (commit/revert/GUI review)
- Supports `--proposal-id` flag to view specific proposals
- Fallback to legacy diff behavior if no proposals found
**Agent List Command** (New - Oct 1, 2025):
- Enumerates all proposals in the registry
- Shows proposal ID, status, creation time, prompt, and stats
- Indicates pending/accepted/rejected status for each proposal
- Provides guidance on using `agent diff` to view details
- Empty state message guides users to create proposals with `agent run`
**Resource Catalog Updates**:
- Added `agent list` action with returns schema
- Added `agent diff` action with arguments (`--proposal-id`) and returns schema
- Updated agent resource description to include listing and diffing capabilities
- Regenerated `docs/api/z3ed-resources.yaml` with new agent actions
**Architecture Benefits**:
- Clean separation: RomSandboxManager (file ops) ↔ ProposalRegistry (metadata)
- Thread-safe with mutex protection for concurrent access
- Extensible design ready for ImGui review UI (AW-03)
- Proposal persistence enables post-session review and auditing
- Proposal-centric workflow enables human-in-the-loop review
**Next Steps for AW Workstream**:
- AW-03: ImGui drawer with accept/reject controls
- AW-04: Policy evaluation for gating mutations
- AW-05: `.z3ed-diff` hybrid format design
### Files Modified/Created
**Phase 6 (Resource Catalogue)**:
1. `src/cli/handlers/rom.cc` - Added `RomInfo::Run` implementation
2. `src/cli/z3ed.h` - Added `RomInfo` class declaration
3. `src/cli/modern_cli.cc` - Updated `HandleRomInfoCommand` routing
4. `src/cli/service/resource_catalog.cc` - Added `rom info` schema entry
5. `docs/api/z3ed-resources.yaml` - Generated comprehensive API catalog
**AW-01 & AW-02 (Proposal Tracking)**:
6. `src/cli/service/proposal_registry.h` - New proposal tracking service interface
7. `src/cli/service/proposal_registry.cc` - Implementation with full lifecycle management
8. `src/cli/handlers/agent.cc` - Integrated ProposalRegistry into agent run workflow
9. `src/cli/z3ed.cmake` - Added proposal_registry.cc to build
10. `docs/E6-z3ed-implementation-plan.md` - Updated progress and task statuses
**Agent Diff & List (Oct 1, 2025)**:
11. `src/cli/handlers/agent.cc` - Enhanced `HandleDiffCommand` with proposal reading, added `HandleListCommand`
12. `src/cli/service/resource_catalog.cc` - Added agent list and diff actions to schema
13. `docs/api/z3ed-resources.yaml` - Regenerated with new agent commands
## 6. References
- `docs/E6-z3ed-cli-design.md` - Overall CLI design and architecture
- `docs/api/z3ed-resources.yaml` - Machine-readable API reference (generated)
- `src/cli/service/resource_catalog.h` - Resource catalog implementation
- `src/cli/service/resource_catalog.cc` - Schema definitions and serialization

View File

@@ -0,0 +1,304 @@
# Auto-generated resource catalogue
version: "0.1.0"
last_updated: "2025-10-01"
resources:
- name: "rom"
description: "ROM validation, diffing, and snapshot helpers."
actions:
- name: "info"
synopsis: "z3ed rom info --rom <file>"
stability: "stable"
args:
- flag: "--rom"
type: "path"
required: true
description: "Path to ROM file configured via global flag."
effects:
- "Reads ROM from disk and displays basic information (title, size, filename)."
returns:
- field: "title"
type: "string"
description: "ROM internal title from header."
- field: "size"
type: "integer"
description: "ROM file size in bytes."
- field: "filename"
type: "string"
description: "Full path to the ROM file."
- name: "validate"
synopsis: "z3ed rom validate --rom <file>"
stability: "stable"
args:
- flag: "--rom"
type: "path"
required: true
description: "Path to ROM file configured via global flag."
effects:
- "Reads ROM from disk, verifies checksum, and reports header status."
returns:
- field: "report"
type: "object"
description: "Structured validation summary with checksum and header results."
- name: "diff"
synopsis: "z3ed rom diff <rom_a> <rom_b>"
stability: "beta"
args:
- flag: "rom_a"
type: "path"
required: true
description: "Reference ROM path."
- flag: "rom_b"
type: "path"
required: true
description: "Candidate ROM path."
effects:
- "Reads two ROM images, compares bytes, and streams differences to stdout."
returns:
- field: "differences"
type: "integer"
description: "Count of mismatched bytes between ROMs."
- name: "generate-golden"
synopsis: "z3ed rom generate-golden <rom_file> <golden_file>"
stability: "experimental"
args:
- flag: "rom_file"
type: "path"
required: true
description: "Source ROM to snapshot."
- flag: "golden_file"
type: "path"
required: true
description: "Output path for golden image."
effects:
- "Writes out exact ROM image for tooling baselines and diff workflows."
returns:
- field: "artifact"
type: "path"
description: "Absolute path to the generated golden image."
- name: "patch"
description: "Patch authoring and application commands covering BPS and Asar flows."
actions:
- name: "apply"
synopsis: "z3ed patch apply <rom_file> <bps_patch>"
stability: "beta"
args:
- flag: "rom_file"
type: "path"
required: true
description: "Source ROM image that will receive the patch."
- flag: "bps_patch"
type: "path"
required: true
description: "BPS patch to apply to the ROM."
effects:
- "Loads ROM from disk, applies a BPS patch, and writes `patched.sfc`."
returns:
- field: "artifact"
type: "path"
description: "Absolute path to the patched ROM image produced on success."
- name: "apply-asar"
synopsis: "z3ed patch apply-asar <patch.asm>"
stability: "prototype"
args:
- flag: "patch.asm"
type: "path"
required: true
description: "Assembly patch consumed by the bundled Asar runtime."
- flag: "--rom"
type: "path"
required: false
description: "ROM path supplied via global --rom flag."
effects:
- "Invokes Asar against the active ROM buffer and applies assembled changes."
returns:
- field: "log"
type: "string"
description: "Assembler diagnostics emitted during application."
- name: "create"
synopsis: "z3ed patch create --source <rom> --target <rom> --out <patch.bps>"
stability: "experimental"
args:
- flag: "--source"
type: "path"
required: true
description: "Baseline ROM used when computing the patch."
- flag: "--target"
type: "path"
required: true
description: "Modified ROM to diff against the baseline."
- flag: "--out"
type: "path"
required: true
description: "Output path for the generated BPS patch."
effects:
- "Compares source and target images to synthesize a distributable BPS patch."
returns:
- field: "artifact"
type: "path"
description: "File system path to the generated patch."
- name: "palette"
description: "Palette manipulation commands covering export, import, and color editing."
actions:
- name: "export"
synopsis: "z3ed palette export --group <group> --id <id> --to <file>"
stability: "experimental"
args:
- flag: "--group"
type: "integer"
required: true
description: "Palette group id (0-31)."
- flag: "--id"
type: "integer"
required: true
description: "Palette index inside the group."
- flag: "--to"
type: "path"
required: true
description: "Destination file path for binary export."
effects:
- "Reads ROM palette buffer and writes binary palette data to disk."
returns: []
- name: "import"
synopsis: "z3ed palette import --group <group> --id <id> --from <file>"
stability: "experimental"
args:
- flag: "--group"
type: "integer"
required: true
description: "Palette group id (0-31)."
- flag: "--id"
type: "integer"
required: true
description: "Palette index inside the group."
- flag: "--from"
type: "path"
required: true
description: "Source binary palette file."
effects:
- "Writes imported palette bytes into ROM buffer and marks project dirty."
returns: []
- name: "overworld"
description: "Overworld tile inspection and manipulation commands."
actions:
- name: "get-tile"
synopsis: "z3ed overworld get-tile --map <map_id> --x <x> --y <y>"
stability: "stable"
args:
- flag: "--map"
type: "integer"
required: true
description: "Overworld map identifier (0-63)."
- flag: "--x"
type: "integer"
required: true
description: "Tile x coordinate."
- flag: "--y"
type: "integer"
required: true
description: "Tile y coordinate."
effects: []
returns:
- field: "tile"
type: "integer"
description: "Tile id located at the supplied coordinates."
- name: "set-tile"
synopsis: "z3ed overworld set-tile --map <map_id> --x <x> --y <y> --tile <tile_id>"
stability: "experimental"
args:
- flag: "--map"
type: "integer"
required: true
description: "Overworld map identifier (0-63)."
- flag: "--x"
type: "integer"
required: true
description: "Tile x coordinate."
- flag: "--y"
type: "integer"
required: true
description: "Tile y coordinate."
- flag: "--tile"
type: "integer"
required: true
description: "Tile id to write."
effects:
- "Mutates overworld tile map and enqueues render invalidation."
returns: []
- name: "dungeon"
description: "Dungeon room export and inspection utilities."
actions:
- name: "export"
synopsis: "z3ed dungeon export <room_id>"
stability: "prototype"
args:
- flag: "room_id"
type: "integer"
required: true
description: "Dungeon room identifier to inspect."
effects:
- "Loads the active ROM via --rom and prints metadata for the requested room."
returns:
- field: "metadata"
type: "object"
description: "Structured room summary including blockset, spriteset, palette, and layout."
- name: "list-objects"
synopsis: "z3ed dungeon list-objects <room_id>"
stability: "prototype"
args:
- flag: "room_id"
type: "integer"
required: true
description: "Dungeon room identifier whose objects should be listed."
effects:
- "Streams parsed dungeon object records for the requested room to stdout."
returns:
- field: "objects"
type: "array"
description: "Collection of tile object records with ids, coordinates, and layers."
- name: "agent"
description: "Agent workflow helpers including planning, diffing, listing, and schema discovery."
actions:
- name: "describe"
synopsis: "z3ed agent describe --resource <name>"
stability: "prototype"
args:
- flag: "--resource"
type: "string"
required: false
description: "Optional resource name to filter results."
effects: []
returns:
- field: "schema"
type: "object"
description: "JSON schema describing resource arguments and semantics."
- name: "list"
synopsis: "z3ed agent list"
stability: "prototype"
args: []
effects:
- "reads"
returns:
- field: "proposals"
type: "array"
description: "List of all proposals with ID, status, prompt, and metadata."
- name: "diff"
synopsis: "z3ed agent diff [--proposal-id <id>]"
stability: "prototype"
args:
- flag: "--proposal-id"
type: "string"
required: false
description: "Optional proposal ID to view specific proposal. Defaults to latest pending."
effects:
- "reads"
- "readsproposal_registryList of all proposals with ID, status, prompt, and metadata.z3ed agent diff [--proposal-id <id>]Optional proposal ID to view specific proposal. Defaults to latest pending."
returns:
- field: "diff"
type: "string"
description: "Unified diff showing changes to ROM."
- field: "log"
type: "string"
description: "Execution log of commands run."
- field: "metadata"
type: "object"
description: "Proposal metadata including status and timestamps."

View File

@@ -31,6 +31,42 @@ namespace yaze {
using core::Renderer;
constexpr int Uncompressed3BPPSize = 0x0600;
namespace {
constexpr size_t kBaseRomSize = 1048576; // 1MB
constexpr size_t kHeaderSize = 0x200; // 512 bytes
void MaybeStripSmcHeader(std::vector<uint8_t> &rom_data, unsigned long &size) {
if (size % kBaseRomSize == kHeaderSize && size >= kHeaderSize) {
rom_data.erase(rom_data.begin(), rom_data.begin() + kHeaderSize);
size -= kHeaderSize;
}
}
} // namespace
RomLoadOptions RomLoadOptions::AppDefaults() { return RomLoadOptions{}; }
RomLoadOptions RomLoadOptions::CliDefaults() {
RomLoadOptions options;
options.populate_palettes = false;
options.populate_gfx_groups = false;
options.expand_to_full_image = false;
options.load_resource_labels = false;
return options;
}
RomLoadOptions RomLoadOptions::RawDataOnly() {
RomLoadOptions options;
options.load_zelda3_content = false;
options.strip_header = false;
options.populate_metadata = false;
options.populate_palettes = false;
options.populate_gfx_groups = false;
options.expand_to_full_image = false;
options.load_resource_labels = false;
return options;
}
uint32_t GetGraphicsAddress(const uint8_t *data, uint8_t addr, uint32_t ptr1,
uint32_t ptr2, uint32_t ptr3) {
return SnesToPc(AddressFromBytes(data[ptr1 + addr], data[ptr2 + addr],
@@ -69,7 +105,9 @@ absl::StatusOr<std::array<gfx::Bitmap, kNumLinkSheets>> LoadLinkGraphics(
link_graphics[i].Create(gfx::kTilesheetWidth, gfx::kTilesheetHeight,
gfx::kTilesheetDepth, link_sheet_8bpp);
link_graphics[i].SetPalette(rom.palette_group().armors[0]);
Renderer::Get().RenderBitmap(&link_graphics[i]);
if (SDL_Renderer *renderer = Renderer::Get().renderer(); renderer != nullptr) {
Renderer::Get().RenderBitmap(&link_graphics[i]);
}
}
return link_graphics;
}
@@ -137,6 +175,8 @@ absl::StatusOr<std::array<gfx::Bitmap, kNumGfxSheets>> LoadAllGraphicsData(
std::array<gfx::Bitmap, kNumGfxSheets> graphics_sheets;
std::vector<uint8_t> sheet;
bool bpp3 = false;
SDL_Renderer *renderer = Renderer::Get().renderer();
const bool renderer_ready = renderer != nullptr;
for (uint32_t i = 0; i < kNumGfxSheets; i++) {
if (i >= 115 && i <= 126) { // uncompressed sheets
@@ -174,8 +214,8 @@ absl::StatusOr<std::array<gfx::Bitmap, kNumGfxSheets>> LoadAllGraphicsData(
}
}
if (!defer_render) {
graphics_sheets[i].CreateTexture(Renderer::Get().renderer());
if (!defer_render && renderer_ready) {
graphics_sheets[i].CreateTexture(renderer);
}
for (int j = 0; j < graphics_sheets[i].size(); ++j) {
@@ -228,6 +268,13 @@ absl::Status SaveAllGraphicsData(
}
absl::Status Rom::LoadFromFile(const std::string &filename, bool z3_load) {
return LoadFromFile(
filename, z3_load ? RomLoadOptions::AppDefaults()
: RomLoadOptions::RawDataOnly());
}
absl::Status Rom::LoadFromFile(const std::string &filename,
const RomLoadOptions &options) {
if (filename.empty()) {
return absl::InvalidArgumentError(
"Could not load ROM: parameter `filename` is empty.");
@@ -259,59 +306,108 @@ absl::Status Rom::LoadFromFile(const std::string &filename, bool z3_load) {
file.read(reinterpret_cast<char *>(rom_data_.data()), size_);
file.close();
if (z3_load) {
RETURN_IF_ERROR(LoadZelda3());
resource_label_manager_.LoadLabels(absl::StrFormat("%s.labels", filename));
if (!options.load_zelda3_content) {
if (options.strip_header) {
MaybeStripSmcHeader(rom_data_, size_);
}
size_ = rom_data_.size();
} else {
RETURN_IF_ERROR(LoadZelda3(options));
}
if (options.load_resource_labels) {
resource_label_manager_.LoadLabels(
absl::StrFormat("%s.labels", filename));
}
return absl::OkStatus();
}
absl::Status Rom::LoadFromData(const std::vector<uint8_t> &data, bool z3_load) {
return LoadFromData(
data, z3_load ? RomLoadOptions::AppDefaults()
: RomLoadOptions::RawDataOnly());
}
absl::Status Rom::LoadFromData(const std::vector<uint8_t> &data,
const RomLoadOptions &options) {
if (data.empty()) {
return absl::InvalidArgumentError(
"Could not load ROM: parameter `data` is empty.");
}
rom_data_ = data;
size_ = data.size();
if (z3_load) {
RETURN_IF_ERROR(LoadZelda3());
if (!options.load_zelda3_content) {
if (options.strip_header) {
MaybeStripSmcHeader(rom_data_, size_);
}
size_ = rom_data_.size();
} else {
RETURN_IF_ERROR(LoadZelda3(options));
}
return absl::OkStatus();
}
absl::Status Rom::LoadZelda3() {
// Check if the ROM has a header
constexpr size_t kBaseRomSize = 1048576; // 1MB
constexpr size_t kHeaderSize = 0x200; // 512 bytes
if (size_ % kBaseRomSize == kHeaderSize) {
auto header = std::vector<uint8_t>(rom_data_.begin(),
rom_data_.begin() + kHeaderSize);
rom_data_.erase(rom_data_.begin(), rom_data_.begin() + kHeaderSize);
size_ -= 0x200;
return LoadZelda3(RomLoadOptions::AppDefaults());
}
absl::Status Rom::LoadZelda3(const RomLoadOptions &options) {
if (rom_data_.empty()) {
return absl::FailedPreconditionError("ROM data is empty");
}
// Copy ROM title
if (options.strip_header) {
MaybeStripSmcHeader(rom_data_, size_);
}
size_ = rom_data_.size();
constexpr uint32_t kTitleStringOffset = 0x7FC0;
constexpr uint32_t kTitleStringLength = 20;
title_.resize(kTitleStringLength);
std::copy(rom_data_.begin() + kTitleStringOffset,
rom_data_.begin() + kTitleStringOffset + kTitleStringLength,
title_.begin());
if (rom_data_[kTitleStringOffset + 0x19] == 0) {
version_ = zelda3_version::JP;
} else {
version_ = zelda3_version::US;
constexpr uint32_t kTitleStringOffsetWithHeader = 0x81C0;
if (options.populate_metadata) {
uint32_t offset = options.strip_header ? kTitleStringOffset
: kTitleStringOffsetWithHeader;
if (offset + kTitleStringLength > rom_data_.size()) {
return absl::OutOfRangeError(
"ROM image is too small to contain title metadata.");
}
title_.assign(rom_data_.begin() + offset,
rom_data_.begin() + offset + kTitleStringLength);
if (rom_data_[offset + 0x19] == 0) {
version_ = zelda3_version::JP;
} else {
version_ = zelda3_version::US;
}
}
// Load additional resources
RETURN_IF_ERROR(gfx::LoadAllPalettes(rom_data_, palette_groups_));
// TODO Load gfx groups or expanded ZS values
RETURN_IF_ERROR(LoadGfxGroups());
if (options.populate_palettes) {
palette_groups_.clear();
RETURN_IF_ERROR(gfx::LoadAllPalettes(rom_data_, palette_groups_));
} else {
palette_groups_.clear();
}
// Expand the ROM data to 2MB without changing the data in the first 1MB
rom_data_.resize(kBaseRomSize * 2);
size_ = kBaseRomSize * 2;
if (options.populate_gfx_groups) {
RETURN_IF_ERROR(LoadGfxGroups());
} else {
main_blockset_ids = {};
room_blockset_ids = {};
spriteset_ids = {};
paletteset_ids = {};
}
if (options.expand_to_full_image) {
if (rom_data_.size() < kBaseRomSize * 2) {
rom_data_.resize(kBaseRomSize * 2);
}
}
size_ = rom_data_.size();
return absl::OkStatus();
}

View File

@@ -40,6 +40,20 @@ constexpr uint32_t kNumPalettesets = 72;
constexpr uint32_t kEntranceGfxGroup = 0x5D97;
constexpr uint32_t kMaxGraphics = 0x0C3FFF; // 0xC3FB5
struct RomLoadOptions {
bool load_zelda3_content = true;
bool strip_header = true;
bool populate_metadata = true;
bool populate_palettes = true;
bool populate_gfx_groups = true;
bool expand_to_full_image = true;
bool load_resource_labels = true;
static RomLoadOptions AppDefaults();
static RomLoadOptions CliDefaults();
static RomLoadOptions RawDataOnly();
};
/**
* @brief A map of version constants for each version of the game.
*/
@@ -64,9 +78,14 @@ class Rom {
};
absl::Status LoadFromFile(const std::string& filename, bool z3_load = true);
absl::Status LoadFromFile(const std::string& filename,
const RomLoadOptions& options);
absl::Status LoadFromData(const std::vector<uint8_t>& data,
bool z3_load = true);
absl::Status LoadFromData(const std::vector<uint8_t>& data,
const RomLoadOptions& options);
absl::Status LoadZelda3();
absl::Status LoadZelda3(const RomLoadOptions& options);
absl::Status LoadGfxGroups();
absl::Status SaveGfxGroups();

View File

@@ -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") {

View File

@@ -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;
}

View File

@@ -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);
}

View 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

View 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_

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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();