Compare commits
2 Commits
dhanji/g3-
...
micn/save-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80ff4edfde | ||
|
|
9e41f84b52 |
171
CHANGELOG_REQUIREMENTS_PERSISTENCE.md
Normal file
171
CHANGELOG_REQUIREMENTS_PERSISTENCE.md
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# Changelog: Requirements Persistence Feature
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Enhanced the accumulative autonomous mode (`--auto` / default mode) to automatically persist requirements to a local `.g3/requirements.md` file.
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. Core Implementation (`crates/g3-cli/src/lib.rs`)
|
||||||
|
|
||||||
|
#### New Functions Added:
|
||||||
|
|
||||||
|
- **`ensure_g3_dir(workspace_dir: &Path) -> Result<PathBuf>`**
|
||||||
|
- Creates `.g3` directory in the workspace if it doesn't exist
|
||||||
|
- Returns the path to the `.g3` directory
|
||||||
|
|
||||||
|
- **`load_existing_requirements(workspace_dir: &Path) -> Result<Vec<String>>`**
|
||||||
|
- Loads requirements from `.g3/requirements.md` if the file exists
|
||||||
|
- Parses numbered requirements (format: `1. requirement text`)
|
||||||
|
- Returns empty vector if file doesn't exist
|
||||||
|
|
||||||
|
- **`save_requirements(workspace_dir: &Path, requirements: &[String]) -> Result<()>`**
|
||||||
|
- Saves accumulated requirements to `.g3/requirements.md`
|
||||||
|
- Creates `.g3` directory if needed
|
||||||
|
- Formats as markdown with numbered list
|
||||||
|
|
||||||
|
#### Modified Functions:
|
||||||
|
|
||||||
|
- **`run_accumulative_mode()`**
|
||||||
|
- Now loads existing requirements on startup
|
||||||
|
- Displays loaded requirements to user
|
||||||
|
- Initializes turn number based on existing requirements count
|
||||||
|
- Saves requirements after each new requirement is added
|
||||||
|
- Shows save confirmation message
|
||||||
|
- Updated `/requirements` command to show file location
|
||||||
|
|
||||||
|
### 2. Version Control (`.gitignore`)
|
||||||
|
|
||||||
|
- Added `.g3/` directory to `.gitignore`
|
||||||
|
- Prevents accidental commit of local requirements
|
||||||
|
- Users can opt-in to version control if desired
|
||||||
|
|
||||||
|
### 3. Documentation
|
||||||
|
|
||||||
|
#### New Documentation:
|
||||||
|
|
||||||
|
- **`docs/REQUIREMENTS_PERSISTENCE.md`**
|
||||||
|
- Comprehensive guide to the requirements persistence feature
|
||||||
|
- Usage examples and commands
|
||||||
|
- File format specification
|
||||||
|
- Use cases and best practices
|
||||||
|
- Comparison with traditional autonomous mode
|
||||||
|
|
||||||
|
#### Updated Documentation:
|
||||||
|
|
||||||
|
- **`README.md`**
|
||||||
|
- Added requirements persistence section to "Getting Started"
|
||||||
|
- Highlighted key benefits (resume, review, share)
|
||||||
|
- Added example showing `.g3/requirements.md` usage
|
||||||
|
|
||||||
|
### 4. Testing
|
||||||
|
|
||||||
|
- **`test_requirements.sh`**
|
||||||
|
- Simple test script for manual verification
|
||||||
|
- Creates test directory and provides instructions
|
||||||
|
|
||||||
|
## User-Facing Changes
|
||||||
|
|
||||||
|
### New Behavior
|
||||||
|
|
||||||
|
1. **Automatic Saving**
|
||||||
|
- Every requirement entered is immediately saved to `.g3/requirements.md`
|
||||||
|
- User sees confirmation: `💾 Saved to .g3/requirements.md`
|
||||||
|
|
||||||
|
2. **Automatic Loading**
|
||||||
|
- On startup, G3 checks for existing `.g3/requirements.md`
|
||||||
|
- If found, loads and displays requirements
|
||||||
|
- Shows: `📂 Loaded N existing requirement(s) from .g3/requirements.md`
|
||||||
|
|
||||||
|
3. **Enhanced `/requirements` Command**
|
||||||
|
- Now shows file location in output
|
||||||
|
- Format: `📋 Accumulated Requirements (saved to .g3/requirements.md):`
|
||||||
|
|
||||||
|
4. **Session Resumability**
|
||||||
|
- Users can exit and resume work later
|
||||||
|
- Requirements persist across sessions
|
||||||
|
- Turn numbering continues from previous session
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
my-project/
|
||||||
|
├── .g3/
|
||||||
|
│ └── requirements.md # NEW: Accumulated requirements
|
||||||
|
├── logs/ # Existing: Session logs
|
||||||
|
└── ... (project files)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Requirements File Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Project Requirements
|
||||||
|
|
||||||
|
1. First requirement
|
||||||
|
2. Second requirement
|
||||||
|
3. Third requirement
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
1. **Persistence**: No data loss if G3 crashes or is interrupted
|
||||||
|
2. **Transparency**: Always know what G3 is working on
|
||||||
|
3. **Resumability**: Pick up where you left off
|
||||||
|
4. **Documentation**: Requirements serve as project documentation
|
||||||
|
5. **Collaboration**: Share requirements with team members
|
||||||
|
6. **Auditability**: Track what was requested and when
|
||||||
|
|
||||||
|
## Backward Compatibility
|
||||||
|
|
||||||
|
- ✅ Fully backward compatible
|
||||||
|
- ✅ No breaking changes to existing functionality
|
||||||
|
- ✅ Works seamlessly with existing projects
|
||||||
|
- ✅ Graceful handling of missing `.g3` directory
|
||||||
|
- ✅ Error handling for file I/O issues
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- If `.g3/requirements.md` cannot be read: Shows warning, continues with empty requirements
|
||||||
|
- If `.g3/requirements.md` cannot be written: Shows warning, continues with in-memory requirements
|
||||||
|
- Non-blocking errors don't interrupt workflow
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [x] Build succeeds without errors
|
||||||
|
- [ ] Manual test: Create new requirements in fresh directory
|
||||||
|
- [ ] Manual test: Resume session with existing requirements
|
||||||
|
- [ ] Manual test: `/requirements` command shows file location
|
||||||
|
- [ ] Manual test: Requirements file format is correct
|
||||||
|
- [ ] Manual test: Error handling for permission issues
|
||||||
|
- [ ] Manual test: `.g3` directory is created automatically
|
||||||
|
- [ ] Manual test: `.g3` directory is ignored by git
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential improvements for future versions:
|
||||||
|
|
||||||
|
1. Requirement status tracking (pending, in-progress, completed)
|
||||||
|
2. Requirement dependencies and ordering
|
||||||
|
3. Requirement templates and snippets
|
||||||
|
4. Integration with issue trackers
|
||||||
|
5. Requirement validation and linting
|
||||||
|
6. Export to other formats (JSON, YAML, etc.)
|
||||||
|
7. Requirement search and filtering
|
||||||
|
8. Requirement history and versioning
|
||||||
|
|
||||||
|
## Migration Guide
|
||||||
|
|
||||||
|
No migration needed! The feature works automatically:
|
||||||
|
|
||||||
|
1. Update to the new version
|
||||||
|
2. Run `g3` in any directory
|
||||||
|
3. Enter requirements as usual
|
||||||
|
4. Requirements are automatically saved to `.g3/requirements.md`
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
- `crates/g3-cli/src/lib.rs` - Core implementation
|
||||||
|
- `.gitignore` - Version control exclusion
|
||||||
|
- `docs/REQUIREMENTS_PERSISTENCE.md` - Feature documentation
|
||||||
|
- `README.md` - Updated getting started guide
|
||||||
|
- `test_requirements.sh` - Test script
|
||||||
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -1345,7 +1345,6 @@ dependencies = [
|
|||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
"g3-config",
|
"g3-config",
|
||||||
"g3-core",
|
"g3-core",
|
||||||
"g3-ensembles",
|
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"rustyline",
|
"rustyline",
|
||||||
@@ -1463,23 +1462,6 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "g3-ensembles"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"chrono",
|
|
||||||
"clap",
|
|
||||||
"g3-config",
|
|
||||||
"g3-core",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tempfile",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "g3-execution"
|
name = "g3-execution"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ members = [
|
|||||||
"crates/g3-config",
|
"crates/g3-config",
|
||||||
"crates/g3-execution",
|
"crates/g3-execution",
|
||||||
"crates/g3-computer-control",
|
"crates/g3-computer-control",
|
||||||
"crates/g3-console",
|
"crates/g3-console"
|
||||||
"crates/g3-ensembles"
|
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -96,7 +96,6 @@ These commands give you fine-grained control over context management, allowing y
|
|||||||
- Window listing and identification
|
- Window listing and identification
|
||||||
- **Code Search**: Embedded tree-sitter for syntax-aware code search (Rust, Python, JavaScript, TypeScript, Go, Java, C, C++) - see [Code Search Guide](docs/CODE_SEARCH.md)
|
- **Code Search**: Embedded tree-sitter for syntax-aware code search (Rust, Python, JavaScript, TypeScript, Go, Java, C, C++) - see [Code Search Guide](docs/CODE_SEARCH.md)
|
||||||
- **Final Output**: Formatted result presentation
|
- **Final Output**: Formatted result presentation
|
||||||
- **Flock Mode**: Parallel multi-agent development for large projects - see [Flock Mode Guide](docs/FLOCK_MODE.md)
|
|
||||||
|
|
||||||
### Provider Flexibility
|
### Provider Flexibility
|
||||||
- Support for multiple LLM providers through a unified interface
|
- Support for multiple LLM providers through a unified interface
|
||||||
@@ -130,7 +129,6 @@ G3 is designed for:
|
|||||||
- API integration and testing
|
- API integration and testing
|
||||||
- Documentation generation
|
- Documentation generation
|
||||||
- Complex multi-step workflows
|
- Complex multi-step workflows
|
||||||
- Parallel development of modular architectures
|
|
||||||
- Desktop application automation and testing
|
- Desktop application automation and testing
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
@@ -139,6 +137,11 @@ G3 is designed for:
|
|||||||
|
|
||||||
The default interactive mode now uses **accumulative autonomous mode**, which combines the best of interactive and autonomous workflows:
|
The default interactive mode now uses **accumulative autonomous mode**, which combines the best of interactive and autonomous workflows:
|
||||||
|
|
||||||
|
**Requirements Persistence**: All requirements are automatically saved to `.g3/requirements.md` in your workspace, allowing you to:
|
||||||
|
- Resume work across sessions
|
||||||
|
- Review what you've asked G3 to build
|
||||||
|
- Share requirements with team members
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Simply run g3 in any directory
|
# Simply run g3 in any directory
|
||||||
g3
|
g3
|
||||||
@@ -154,6 +157,9 @@ requirement> create a simple web server in Python with Flask
|
|||||||
# ... autonomous mode runs and implements it ...
|
# ... autonomous mode runs and implements it ...
|
||||||
requirement> add a /health endpoint that returns JSON
|
requirement> add a /health endpoint that returns JSON
|
||||||
# ... autonomous mode runs again with both requirements ...
|
# ... autonomous mode runs again with both requirements ...
|
||||||
|
|
||||||
|
# Requirements are saved to .g3/requirements.md
|
||||||
|
# Use /requirements command to view them
|
||||||
```
|
```
|
||||||
|
|
||||||
### Other Modes
|
### Other Modes
|
||||||
|
|||||||
@@ -15,25 +15,6 @@ max_tokens = 4096 # Per-request output limit (how many tokens the model can gen
|
|||||||
temperature = 0.1
|
temperature = 0.1
|
||||||
use_oauth = true
|
use_oauth = true
|
||||||
|
|
||||||
# Multiple OpenAI-compatible providers can be configured with custom names
|
|
||||||
# Each provider gets its own section under [providers.openai_compatible.<name>]
|
|
||||||
# [providers.openai_compatible.openrouter]
|
|
||||||
# api_key = "your-openrouter-api-key"
|
|
||||||
# model = "anthropic/claude-3.5-sonnet"
|
|
||||||
# base_url = "https://openrouter.ai/api/v1"
|
|
||||||
# max_tokens = 4096
|
|
||||||
# temperature = 0.1
|
|
||||||
|
|
||||||
# [providers.openai_compatible.groq]
|
|
||||||
# api_key = "your-groq-api-key"
|
|
||||||
# model = "llama-3.3-70b-versatile"
|
|
||||||
# base_url = "https://api.groq.com/openai/v1"
|
|
||||||
# max_tokens = 4096
|
|
||||||
# temperature = 0.1
|
|
||||||
|
|
||||||
# To use one of these providers, set default_provider to the name you chose:
|
|
||||||
# default_provider = "openrouter"
|
|
||||||
|
|
||||||
[agent]
|
[agent]
|
||||||
fallback_default_max_tokens = 8192
|
fallback_default_max_tokens = 8192
|
||||||
# max_context_length: Override the context window size for all providers
|
# max_context_length: Override the context window size for all providers
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ description = "CLI interface for G3 AI coding agent"
|
|||||||
g3-core = { path = "../g3-core" }
|
g3-core = { path = "../g3-core" }
|
||||||
g3-config = { path = "../g3-config" }
|
g3-config = { path = "../g3-config" }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
g3-ensembles = { path = "../g3-ensembles" }
|
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
|||||||
@@ -246,36 +246,11 @@ pub struct Cli {
|
|||||||
/// Enable WebDriver browser automation tools
|
/// Enable WebDriver browser automation tools
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub webdriver: bool,
|
pub webdriver: bool,
|
||||||
|
|
||||||
/// Enable flock mode - parallel multi-agent development
|
|
||||||
#[arg(long, requires = "flock_workspace", requires = "segments")]
|
|
||||||
pub project: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Flock workspace directory (where segment copies will be created)
|
|
||||||
#[arg(long, requires = "project")]
|
|
||||||
pub flock_workspace: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Number of segments to partition work into (for flock mode)
|
|
||||||
#[arg(long, requires = "project")]
|
|
||||||
pub segments: Option<usize>,
|
|
||||||
|
|
||||||
/// Maximum turns per segment in flock mode (default: 5)
|
|
||||||
#[arg(long, default_value = "5")]
|
|
||||||
pub flock_max_turns: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run() -> Result<()> {
|
pub async fn run() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
// Check if flock mode is enabled
|
|
||||||
if let (Some(project_dir), Some(flock_workspace), Some(num_segments)) =
|
|
||||||
(&cli.project, &cli.flock_workspace, cli.segments) {
|
|
||||||
// Run flock mode
|
|
||||||
return run_flock_mode(project_dir.clone(), flock_workspace.clone(), num_segments, cli.flock_max_turns).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, continue with normal mode
|
|
||||||
|
|
||||||
// Only initialize logging if not in retro mode
|
// Only initialize logging if not in retro mode
|
||||||
if !cli.machine {
|
if !cli.machine {
|
||||||
// Initialize logging with filtering
|
// Initialize logging with filtering
|
||||||
@@ -464,36 +439,48 @@ pub async fn run() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run flock mode - parallel multi-agent development
|
/// Ensure .g3 directory exists in the workspace
|
||||||
async fn run_flock_mode(
|
fn ensure_g3_dir(workspace_dir: &Path) -> Result<PathBuf> {
|
||||||
project_dir: PathBuf,
|
let g3_dir = workspace_dir.join(".g3");
|
||||||
flock_workspace: PathBuf,
|
if !g3_dir.exists() {
|
||||||
num_segments: usize,
|
std::fs::create_dir_all(&g3_dir)?;
|
||||||
max_turns: usize,
|
}
|
||||||
) -> Result<()> {
|
Ok(g3_dir)
|
||||||
let output = SimpleOutput::new();
|
}
|
||||||
|
|
||||||
output.print("");
|
/// Load existing requirements from .g3/requirements.md if it exists
|
||||||
output.print("🦅 G3 FLOCK MODE - Parallel Multi-Agent Development");
|
fn load_existing_requirements(workspace_dir: &Path) -> Result<Vec<String>> {
|
||||||
output.print("");
|
let g3_dir = workspace_dir.join(".g3");
|
||||||
output.print(&format!("📁 Project: {}", project_dir.display()));
|
let requirements_file = g3_dir.join("requirements.md");
|
||||||
output.print(&format!("🗂️ Workspace: {}", flock_workspace.display()));
|
|
||||||
output.print(&format!("🔢 Segments: {}", num_segments));
|
|
||||||
output.print(&format!("🔄 Max Turns per Segment: {}", max_turns));
|
|
||||||
output.print("");
|
|
||||||
|
|
||||||
// Create flock configuration
|
if !requirements_file.exists() {
|
||||||
let config = g3_ensembles::FlockConfig::new(project_dir, flock_workspace, num_segments)?
|
return Ok(Vec::new());
|
||||||
.with_max_turns(max_turns);
|
|
||||||
|
|
||||||
// Create and run flock mode
|
|
||||||
let mut flock = g3_ensembles::FlockMode::new(config)?;
|
|
||||||
|
|
||||||
match flock.run().await {
|
|
||||||
Ok(_) => output.print("\n✅ Flock mode completed successfully"),
|
|
||||||
Err(e) => output.print(&format!("\n❌ Flock mode failed: {}", e)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(&requirements_file)?;
|
||||||
|
|
||||||
|
// Parse the requirements from the markdown file
|
||||||
|
let mut requirements = Vec::new();
|
||||||
|
for line in content.lines() {
|
||||||
|
// Look for numbered requirements (e.g., "1. requirement text")
|
||||||
|
if let Some(stripped) = line.strip_prefix(|c: char| c.is_ascii_digit()) {
|
||||||
|
if let Some(req) = stripped.strip_prefix(". ") {
|
||||||
|
// Reconstruct the numbered format
|
||||||
|
let num = line.chars().take_while(|c| c.is_ascii_digit()).collect::<String>();
|
||||||
|
requirements.push(format!("{}. {}", num, req));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(requirements)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save accumulated requirements to .g3/requirements.md
|
||||||
|
fn save_requirements(workspace_dir: &Path, requirements: &[String]) -> Result<()> {
|
||||||
|
let g3_dir = ensure_g3_dir(workspace_dir)?;
|
||||||
|
let requirements_file = g3_dir.join("requirements.md");
|
||||||
|
let content = format!("# Project Requirements\n\n{}\n", requirements.join("\n"));
|
||||||
|
std::fs::write(&requirements_file, content)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,9 +519,25 @@ async fn run_accumulative_mode(
|
|||||||
let _ = rl.load_history(history_path);
|
let _ = rl.load_history(history_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accumulated requirements stored in memory
|
// Load existing requirements from .g3/requirements.md if it exists
|
||||||
let mut accumulated_requirements = Vec::new();
|
let mut accumulated_requirements = match load_existing_requirements(&workspace_dir) {
|
||||||
let mut turn_number = 0;
|
Ok(reqs) if !reqs.is_empty() => {
|
||||||
|
output.print("");
|
||||||
|
output.print(&format!("📂 Loaded {} existing requirement(s) from .g3/requirements.md", reqs.len()));
|
||||||
|
output.print("");
|
||||||
|
for req in &reqs {
|
||||||
|
output.print(&format!(" {}", req));
|
||||||
|
}
|
||||||
|
output.print("");
|
||||||
|
reqs
|
||||||
|
}
|
||||||
|
Ok(_) => Vec::new(),
|
||||||
|
Err(e) => {
|
||||||
|
output.print(&format!("⚠️ Warning: Could not load existing requirements: {}", e));
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut turn_number = accumulated_requirements.len();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
output.print(&format!("\n{}", "=".repeat(60)));
|
output.print(&format!("\n{}", "=".repeat(60)));
|
||||||
@@ -577,7 +580,8 @@ async fn run_accumulative_mode(
|
|||||||
if accumulated_requirements.is_empty() {
|
if accumulated_requirements.is_empty() {
|
||||||
output.print("📋 No requirements accumulated yet");
|
output.print("📋 No requirements accumulated yet");
|
||||||
} else {
|
} else {
|
||||||
output.print("📋 Accumulated Requirements:");
|
let req_file = workspace_dir.join(".g3/requirements.md");
|
||||||
|
output.print(&format!("📋 Accumulated Requirements (saved to {}):", req_file.display()));
|
||||||
output.print("");
|
output.print("");
|
||||||
for req in &accumulated_requirements {
|
for req in &accumulated_requirements {
|
||||||
output.print(&format!(" {}", req));
|
output.print(&format!(" {}", req));
|
||||||
@@ -663,6 +667,13 @@ async fn run_accumulative_mode(
|
|||||||
turn_number += 1;
|
turn_number += 1;
|
||||||
accumulated_requirements.push(format!("{}. {}", turn_number, input));
|
accumulated_requirements.push(format!("{}. {}", turn_number, input));
|
||||||
|
|
||||||
|
// Save requirements to .g3/requirements.md
|
||||||
|
if let Err(e) = save_requirements(&workspace_dir, &accumulated_requirements) {
|
||||||
|
output.print(&format!("⚠️ Warning: Could not save requirements to .g3/requirements.md: {}", e));
|
||||||
|
} else {
|
||||||
|
output.print(&format!("💾 Saved to .g3/requirements.md"));
|
||||||
|
}
|
||||||
|
|
||||||
// Build the complete requirements document
|
// Build the complete requirements document
|
||||||
let requirements_doc = format!(
|
let requirements_doc = format!(
|
||||||
"# Project Requirements\n\n\
|
"# Project Requirements\n\n\
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ pub struct Config {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ProvidersConfig {
|
pub struct ProvidersConfig {
|
||||||
pub openai: Option<OpenAIConfig>,
|
pub openai: Option<OpenAIConfig>,
|
||||||
/// Multiple named OpenAI-compatible providers (e.g., openrouter, groq, etc.)
|
|
||||||
#[serde(default)]
|
|
||||||
pub openai_compatible: std::collections::HashMap<String, OpenAIConfig>,
|
|
||||||
pub anthropic: Option<AnthropicConfig>,
|
pub anthropic: Option<AnthropicConfig>,
|
||||||
pub databricks: Option<DatabricksConfig>,
|
pub databricks: Option<DatabricksConfig>,
|
||||||
pub embedded: Option<EmbeddedConfig>,
|
pub embedded: Option<EmbeddedConfig>,
|
||||||
@@ -124,7 +121,6 @@ impl Default for Config {
|
|||||||
Self {
|
Self {
|
||||||
providers: ProvidersConfig {
|
providers: ProvidersConfig {
|
||||||
openai: None,
|
openai: None,
|
||||||
openai_compatible: std::collections::HashMap::new(),
|
|
||||||
anthropic: None,
|
anthropic: None,
|
||||||
databricks: Some(DatabricksConfig {
|
databricks: Some(DatabricksConfig {
|
||||||
host: "https://your-workspace.cloud.databricks.com".to_string(),
|
host: "https://your-workspace.cloud.databricks.com".to_string(),
|
||||||
@@ -243,7 +239,6 @@ impl Config {
|
|||||||
Self {
|
Self {
|
||||||
providers: ProvidersConfig {
|
providers: ProvidersConfig {
|
||||||
openai: None,
|
openai: None,
|
||||||
openai_compatible: std::collections::HashMap::new(),
|
|
||||||
anthropic: None,
|
anthropic: None,
|
||||||
databricks: None,
|
databricks: None,
|
||||||
embedded: Some(EmbeddedConfig {
|
embedded: Some(EmbeddedConfig {
|
||||||
|
|||||||
@@ -875,21 +875,6 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register OpenAI-compatible providers (e.g., OpenRouter, Groq, etc.)
|
|
||||||
for (name, openai_config) in &config.providers.openai_compatible {
|
|
||||||
if providers_to_register.contains(name) {
|
|
||||||
let openai_provider = g3_providers::OpenAIProvider::new_with_name(
|
|
||||||
name.clone(),
|
|
||||||
openai_config.api_key.clone(),
|
|
||||||
Some(openai_config.model.clone()),
|
|
||||||
openai_config.base_url.clone(),
|
|
||||||
openai_config.max_tokens,
|
|
||||||
openai_config.temperature,
|
|
||||||
)?;
|
|
||||||
providers.register(openai_provider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register Anthropic provider if configured AND it's the default provider
|
// Register Anthropic provider if configured AND it's the default provider
|
||||||
if let Some(anthropic_config) = &config.providers.anthropic {
|
if let Some(anthropic_config) = &config.providers.anthropic {
|
||||||
if providers_to_register.contains(&"anthropic".to_string()) {
|
if providers_to_register.contains(&"anthropic".to_string()) {
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "g3-ensembles"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
description = "Multi-agent ensemble functionality for G3"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
g3-core = { path = "../g3-core" }
|
|
||||||
g3-config = { path = "../g3-config" }
|
|
||||||
clap = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
uuid = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = "3.8"
|
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
# G3 Ensembles Testing Documentation
|
|
||||||
|
|
||||||
This document describes the comprehensive test suite for the g3-ensembles crate (Flock Mode).
|
|
||||||
|
|
||||||
## Test Coverage
|
|
||||||
|
|
||||||
### Unit Tests (`src/tests.rs`)
|
|
||||||
|
|
||||||
Unit tests cover the core data structures and logic:
|
|
||||||
|
|
||||||
#### Status Module Tests
|
|
||||||
|
|
||||||
1. **`test_segment_state_display`**
|
|
||||||
- Verifies that `SegmentState` enum displays correctly with emojis
|
|
||||||
- Tests all states: Pending, Running, Completed, Failed, Cancelled
|
|
||||||
|
|
||||||
2. **`test_flock_status_creation`**
|
|
||||||
- Tests creation of `FlockStatus` with correct initial values
|
|
||||||
- Verifies session ID, segment count, and zero metrics
|
|
||||||
|
|
||||||
3. **`test_segment_status_update`**
|
|
||||||
- Tests updating a single segment's status
|
|
||||||
- Verifies metrics are correctly aggregated
|
|
||||||
|
|
||||||
4. **`test_multiple_segment_updates`**
|
|
||||||
- Tests updating multiple segments
|
|
||||||
- Verifies aggregate metrics (tokens, tool calls, errors) are summed correctly
|
|
||||||
|
|
||||||
5. **`test_is_complete`**
|
|
||||||
- Tests the completion detection logic
|
|
||||||
- Verifies that flock is only complete when all segments are in terminal states
|
|
||||||
- Tests various scenarios: no segments, partial completion, full completion
|
|
||||||
|
|
||||||
6. **`test_count_by_state`**
|
|
||||||
- Tests counting segments by their state
|
|
||||||
- Verifies correct counts for each state type
|
|
||||||
|
|
||||||
7. **`test_status_serialization`**
|
|
||||||
- Tests JSON serialization and deserialization
|
|
||||||
- Verifies round-trip conversion preserves all data
|
|
||||||
|
|
||||||
8. **`test_report_generation`**
|
|
||||||
- Tests the comprehensive report generation
|
|
||||||
- Verifies all expected sections are present
|
|
||||||
- Checks that metrics are correctly displayed
|
|
||||||
|
|
||||||
**Run unit tests:**
|
|
||||||
```bash
|
|
||||||
cargo test -p g3-ensembles --lib
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration Tests (`tests/integration_tests.rs`)
|
|
||||||
|
|
||||||
Integration tests verify end-to-end functionality with real file system and git operations:
|
|
||||||
|
|
||||||
#### Configuration Tests
|
|
||||||
|
|
||||||
1. **`test_flock_config_validation`**
|
|
||||||
- Tests validation of project directory requirements
|
|
||||||
- Verifies error messages for:
|
|
||||||
- Non-existent directory
|
|
||||||
- Non-git repository
|
|
||||||
- Missing flock-requirements.md
|
|
||||||
- Verifies successful creation with valid inputs
|
|
||||||
|
|
||||||
2. **`test_flock_config_builder`**
|
|
||||||
- Tests the builder pattern for `FlockConfig`
|
|
||||||
- Verifies `with_max_turns()` and `with_g3_binary()` methods
|
|
||||||
|
|
||||||
3. **`test_workspace_creation`**
|
|
||||||
- Tests creation of `FlockMode` instance
|
|
||||||
- Verifies project structure is valid
|
|
||||||
|
|
||||||
#### Git Operations Tests
|
|
||||||
|
|
||||||
4. **`test_git_clone_functionality`**
|
|
||||||
- Tests git cloning of project repository
|
|
||||||
- Verifies cloned repository structure:
|
|
||||||
- `.git` directory exists
|
|
||||||
- All files are present
|
|
||||||
- Git history is preserved
|
|
||||||
|
|
||||||
5. **`test_multiple_segment_clones`**
|
|
||||||
- Tests cloning multiple segments (2 segments)
|
|
||||||
- Verifies each segment is independent
|
|
||||||
- Tests that modifications in one segment don't affect others
|
|
||||||
|
|
||||||
6. **`test_git_repo_independence`**
|
|
||||||
- Comprehensive test of segment independence
|
|
||||||
- Creates commits in different segments
|
|
||||||
- Verifies git histories diverge correctly
|
|
||||||
- Ensures files in one segment don't appear in others
|
|
||||||
|
|
||||||
#### Segment Management Tests
|
|
||||||
|
|
||||||
7. **`test_segment_requirements_creation`**
|
|
||||||
- Tests creation of `segment-requirements.md` files
|
|
||||||
- Verifies content is written correctly
|
|
||||||
|
|
||||||
8. **`test_requirements_file_content`**
|
|
||||||
- Tests the structure of flock-requirements.md
|
|
||||||
- Verifies content contains expected sections
|
|
||||||
|
|
||||||
#### Status File Tests
|
|
||||||
|
|
||||||
9. **`test_status_file_operations`**
|
|
||||||
- Tests saving and loading `flock-status.json`
|
|
||||||
- Verifies JSON serialization to file
|
|
||||||
- Tests deserialization from file
|
|
||||||
|
|
||||||
#### JSON Processing Tests
|
|
||||||
|
|
||||||
10. **`test_json_extraction`**
|
|
||||||
- Tests extraction of JSON arrays from text output
|
|
||||||
- Verifies handling of various formats:
|
|
||||||
- Plain JSON
|
|
||||||
- JSON in markdown code blocks
|
|
||||||
- JSON with surrounding text
|
|
||||||
- Invalid input (no JSON)
|
|
||||||
|
|
||||||
11. **`test_partition_json_parsing`**
|
|
||||||
- Tests parsing of partition JSON structure
|
|
||||||
- Verifies module names, requirements, and dependencies are extracted correctly
|
|
||||||
|
|
||||||
**Run integration tests:**
|
|
||||||
```bash
|
|
||||||
cargo test -p g3-ensembles --test integration_tests
|
|
||||||
```
|
|
||||||
|
|
||||||
### End-to-End Test Script (`scripts/test-flock-mode.sh`)
|
|
||||||
|
|
||||||
A comprehensive bash script that tests the complete flock mode workflow:
|
|
||||||
|
|
||||||
#### Test Scenarios
|
|
||||||
|
|
||||||
1. **Project Creation**
|
|
||||||
- Creates a temporary test project
|
|
||||||
- Initializes git repository
|
|
||||||
- Creates flock-requirements.md with realistic content
|
|
||||||
- Makes initial commit
|
|
||||||
|
|
||||||
2. **Project Structure Validation**
|
|
||||||
- Verifies `.git` directory exists
|
|
||||||
- Verifies `flock-requirements.md` exists
|
|
||||||
|
|
||||||
3. **Git Operations**
|
|
||||||
- Tests cloning project to segment directories
|
|
||||||
- Verifies cloned repositories are valid
|
|
||||||
- Tests git log to ensure history is preserved
|
|
||||||
|
|
||||||
4. **Segment Independence**
|
|
||||||
- Creates two segments
|
|
||||||
- Modifies one segment
|
|
||||||
- Verifies other segment is unaffected
|
|
||||||
|
|
||||||
5. **Segment Requirements**
|
|
||||||
- Creates `segment-requirements.md` in segments
|
|
||||||
- Verifies content is written correctly
|
|
||||||
|
|
||||||
6. **Status File Operations**
|
|
||||||
- Creates `flock-status.json`
|
|
||||||
- Validates JSON structure (if `jq` is available)
|
|
||||||
|
|
||||||
**Run end-to-end test:**
|
|
||||||
```bash
|
|
||||||
./scripts/test-flock-mode.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Results
|
|
||||||
|
|
||||||
### Current Status
|
|
||||||
|
|
||||||
✅ **All tests passing**
|
|
||||||
|
|
||||||
- **Unit tests**: 8/8 passed
|
|
||||||
- **Integration tests**: 11/11 passed
|
|
||||||
- **End-to-end test**: All scenarios passed
|
|
||||||
|
|
||||||
### Test Execution Time
|
|
||||||
|
|
||||||
- Unit tests: ~0.01s
|
|
||||||
- Integration tests: ~0.35s (includes git operations)
|
|
||||||
- End-to-end test: ~1-2s (includes cleanup)
|
|
||||||
|
|
||||||
## Running All Tests
|
|
||||||
|
|
||||||
### Run all tests for g3-ensembles:
|
|
||||||
```bash
|
|
||||||
cargo test -p g3-ensembles
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run with verbose output:
|
|
||||||
```bash
|
|
||||||
cargo test -p g3-ensembles -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run specific test:
|
|
||||||
```bash
|
|
||||||
cargo test -p g3-ensembles test_git_clone_functionality
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run tests with coverage (requires cargo-tarpaulin):
|
|
||||||
```bash
|
|
||||||
cargo tarpaulin -p g3-ensembles
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Helpers
|
|
||||||
|
|
||||||
### `create_test_project(name: &str) -> TempDir`
|
|
||||||
|
|
||||||
Helper function in integration tests that creates a complete test project:
|
|
||||||
- Initializes git repository
|
|
||||||
- Configures git user
|
|
||||||
- Creates flock-requirements.md with two modules
|
|
||||||
- Creates README.md
|
|
||||||
- Makes initial commit
|
|
||||||
- Returns `TempDir` that auto-cleans on drop
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```rust
|
|
||||||
let project_dir = create_test_project("my-test");
|
|
||||||
// Use project_dir.path() to access the directory
|
|
||||||
// Automatically cleaned up when project_dir goes out of scope
|
|
||||||
```
|
|
||||||
|
|
||||||
### `extract_json_array(output: &str) -> Option<String>`
|
|
||||||
|
|
||||||
Helper function that extracts JSON arrays from text output:
|
|
||||||
- Finds first `[` and last `]`
|
|
||||||
- Returns content between them
|
|
||||||
- Returns `None` if no valid JSON array found
|
|
||||||
|
|
||||||
## Test Data
|
|
||||||
|
|
||||||
### Sample Requirements
|
|
||||||
|
|
||||||
The test suite uses realistic requirements for a calculator project:
|
|
||||||
|
|
||||||
**Module A: Core Library**
|
|
||||||
- Arithmetic operations (add, sub, mul, div)
|
|
||||||
- Error handling for division by zero
|
|
||||||
- Unit tests
|
|
||||||
- Documentation
|
|
||||||
|
|
||||||
**Module B: CLI Application**
|
|
||||||
- Command-line interface using clap
|
|
||||||
- Subcommands for each operation
|
|
||||||
- User-friendly output
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
This structure tests the partitioning logic with:
|
|
||||||
- Clear module boundaries
|
|
||||||
- Dependency relationship (CLI depends on Core)
|
|
||||||
- Realistic implementation requirements
|
|
||||||
|
|
||||||
## Continuous Integration
|
|
||||||
|
|
||||||
To integrate these tests into CI/CD:
|
|
||||||
|
|
||||||
### GitHub Actions Example
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: Test G3 Ensembles
|
|
||||||
|
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
- name: Run unit tests
|
|
||||||
run: cargo test -p g3-ensembles --lib
|
|
||||||
- name: Run integration tests
|
|
||||||
run: cargo test -p g3-ensembles --test integration_tests
|
|
||||||
- name: Run end-to-end test
|
|
||||||
run: ./scripts/test-flock-mode.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Coverage Goals
|
|
||||||
|
|
||||||
### Current Coverage
|
|
||||||
|
|
||||||
- ✅ Status data structures: 100%
|
|
||||||
- ✅ Configuration validation: 100%
|
|
||||||
- ✅ Git operations: 100%
|
|
||||||
- ✅ Segment independence: 100%
|
|
||||||
- ✅ JSON processing: 100%
|
|
||||||
- ⚠️ Full flock execution: Requires LLM access (tested manually)
|
|
||||||
|
|
||||||
### Future Test Additions
|
|
||||||
|
|
||||||
1. **Mock LLM Tests**
|
|
||||||
- Mock the partitioning agent response
|
|
||||||
- Test full flock workflow without real LLM calls
|
|
||||||
|
|
||||||
2. **Performance Tests**
|
|
||||||
- Test with large numbers of segments (10+)
|
|
||||||
- Measure memory usage
|
|
||||||
- Test concurrent segment execution
|
|
||||||
|
|
||||||
3. **Error Handling Tests**
|
|
||||||
- Test behavior when git operations fail
|
|
||||||
- Test behavior when segments fail
|
|
||||||
- Test recovery scenarios
|
|
||||||
|
|
||||||
4. **Edge Cases**
|
|
||||||
- Empty requirements file
|
|
||||||
- Single segment (degenerate case)
|
|
||||||
- Very large requirements file
|
|
||||||
- Binary files in project
|
|
||||||
|
|
||||||
## Debugging Tests
|
|
||||||
|
|
||||||
### Enable debug logging:
|
|
||||||
```bash
|
|
||||||
RUST_LOG=debug cargo test -p g3-ensembles -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
### Keep test artifacts:
|
|
||||||
```bash
|
|
||||||
# Modify test to not cleanup
|
|
||||||
# Or inspect TEST_DIR before cleanup in end-to-end test
|
|
||||||
export TEST_DIR=/tmp/my-test
|
|
||||||
./scripts/test-flock-mode.sh
|
|
||||||
ls -la $TEST_DIR
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run single test with backtrace:
|
|
||||||
```bash
|
|
||||||
RUST_BACKTRACE=1 cargo test -p g3-ensembles test_git_clone_functionality -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contributing Tests
|
|
||||||
|
|
||||||
When adding new features to g3-ensembles:
|
|
||||||
|
|
||||||
1. **Add unit tests** for new data structures and logic
|
|
||||||
2. **Add integration tests** for new file/git operations
|
|
||||||
3. **Update end-to-end test** if workflow changes
|
|
||||||
4. **Document tests** in this file
|
|
||||||
5. **Ensure all tests pass** before submitting PR
|
|
||||||
|
|
||||||
### Test Naming Convention
|
|
||||||
|
|
||||||
- Unit tests: `test_<functionality>`
|
|
||||||
- Integration tests: `test_<feature>_<scenario>`
|
|
||||||
- Use descriptive names that explain what is being tested
|
|
||||||
|
|
||||||
### Test Structure
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[test]
|
|
||||||
fn test_feature_name() {
|
|
||||||
// Arrange: Set up test data
|
|
||||||
let data = create_test_data();
|
|
||||||
|
|
||||||
// Act: Perform the operation
|
|
||||||
let result = perform_operation(data);
|
|
||||||
|
|
||||||
// Assert: Verify the result
|
|
||||||
assert_eq!(result, expected_value);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Tests fail with "git not found"
|
|
||||||
|
|
||||||
**Solution**: Install git:
|
|
||||||
```bash
|
|
||||||
# macOS
|
|
||||||
brew install git
|
|
||||||
|
|
||||||
# Ubuntu/Debian
|
|
||||||
sudo apt-get install git
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
choco install git
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tests fail with permission errors
|
|
||||||
|
|
||||||
**Solution**: Ensure test directories are writable:
|
|
||||||
```bash
|
|
||||||
chmod -R u+w /tmp
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration tests are slow
|
|
||||||
|
|
||||||
**Cause**: Git operations and file I/O take time
|
|
||||||
|
|
||||||
**Solution**: Run only unit tests for quick feedback:
|
|
||||||
```bash
|
|
||||||
cargo test -p g3-ensembles --lib
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test artifacts not cleaned up
|
|
||||||
|
|
||||||
**Cause**: Test panicked before cleanup
|
|
||||||
|
|
||||||
**Solution**: Manually clean temp directories:
|
|
||||||
```bash
|
|
||||||
rm -rf /tmp/tmp.*
|
|
||||||
```
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
The g3-ensembles test suite provides comprehensive coverage of:
|
|
||||||
- ✅ Core data structures and logic
|
|
||||||
- ✅ Configuration validation
|
|
||||||
- ✅ Git repository operations
|
|
||||||
- ✅ Segment independence
|
|
||||||
- ✅ Status tracking and reporting
|
|
||||||
- ✅ JSON processing
|
|
||||||
- ✅ End-to-end workflow
|
|
||||||
|
|
||||||
All tests are automated, fast, and reliable. The test suite ensures that flock mode works correctly across different scenarios and edge cases.
|
|
||||||
@@ -1,647 +0,0 @@
|
|||||||
//! Flock mode implementation - parallel multi-agent development
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use chrono::Utc;
|
|
||||||
use g3_config::Config;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::process::Stdio;
|
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
|
||||||
use tokio::process::Command;
|
|
||||||
use tracing::{debug, error, info, warn};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::status::{FlockStatus, SegmentState, SegmentStatus};
|
|
||||||
|
|
||||||
/// Configuration for flock mode
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct FlockConfig {
|
|
||||||
/// Project directory (must be a git repo with flock-requirements.md)
|
|
||||||
pub project_dir: PathBuf,
|
|
||||||
|
|
||||||
/// Flock workspace directory where segments will be created
|
|
||||||
pub flock_workspace: PathBuf,
|
|
||||||
|
|
||||||
/// Number of segments to partition work into
|
|
||||||
pub num_segments: usize,
|
|
||||||
|
|
||||||
/// Maximum turns per segment (for autonomous mode)
|
|
||||||
pub max_turns: usize,
|
|
||||||
|
|
||||||
/// G3 configuration to use for agents
|
|
||||||
pub g3_config: Config,
|
|
||||||
|
|
||||||
/// Path to g3 binary (defaults to current executable)
|
|
||||||
pub g3_binary: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FlockConfig {
|
|
||||||
/// Create a new flock configuration
|
|
||||||
pub fn new(
|
|
||||||
project_dir: PathBuf,
|
|
||||||
flock_workspace: PathBuf,
|
|
||||||
num_segments: usize,
|
|
||||||
) -> Result<Self> {
|
|
||||||
// Validate project directory
|
|
||||||
if !project_dir.exists() {
|
|
||||||
anyhow::bail!("Project directory does not exist: {}", project_dir.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a git repo
|
|
||||||
if !project_dir.join(".git").exists() {
|
|
||||||
anyhow::bail!("Project directory must be a git repository: {}", project_dir.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for flock-requirements.md
|
|
||||||
let requirements_path = project_dir.join("flock-requirements.md");
|
|
||||||
if !requirements_path.exists() {
|
|
||||||
anyhow::bail!(
|
|
||||||
"Project directory must contain flock-requirements.md: {}",
|
|
||||||
project_dir.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load default config
|
|
||||||
let g3_config = Config::load(None)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
project_dir,
|
|
||||||
flock_workspace,
|
|
||||||
num_segments,
|
|
||||||
max_turns: 5, // Default
|
|
||||||
g3_config,
|
|
||||||
g3_binary: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set maximum turns per segment
|
|
||||||
pub fn with_max_turns(mut self, max_turns: usize) -> Self {
|
|
||||||
self.max_turns = max_turns;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set custom g3 binary path
|
|
||||||
pub fn with_g3_binary(mut self, binary: PathBuf) -> Self {
|
|
||||||
self.g3_binary = Some(binary);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set custom g3 config
|
|
||||||
pub fn with_config(mut self, config: Config) -> Self {
|
|
||||||
self.g3_config = config;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flock mode orchestrator
|
|
||||||
pub struct FlockMode {
|
|
||||||
config: FlockConfig,
|
|
||||||
status: FlockStatus,
|
|
||||||
session_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FlockMode {
|
|
||||||
/// Create a new flock mode instance
|
|
||||||
pub fn new(config: FlockConfig) -> Result<Self> {
|
|
||||||
let session_id = Uuid::new_v4().to_string();
|
|
||||||
|
|
||||||
let status = FlockStatus::new(
|
|
||||||
session_id.clone(),
|
|
||||||
config.project_dir.clone(),
|
|
||||||
config.flock_workspace.clone(),
|
|
||||||
config.num_segments,
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
config,
|
|
||||||
status,
|
|
||||||
session_id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run flock mode
|
|
||||||
pub async fn run(&mut self) -> Result<()> {
|
|
||||||
info!("Starting flock mode with {} segments", self.config.num_segments);
|
|
||||||
|
|
||||||
// Step 1: Partition requirements
|
|
||||||
println!("\n🧠 Step 1: Partitioning requirements into {} segments...", self.config.num_segments);
|
|
||||||
let partitions = self.partition_requirements().await?;
|
|
||||||
|
|
||||||
// Step 2: Create segment workspaces
|
|
||||||
println!("\n📁 Step 2: Creating segment workspaces...");
|
|
||||||
self.create_segment_workspaces(&partitions).await?;
|
|
||||||
|
|
||||||
// Step 3: Run segments in parallel
|
|
||||||
println!("\n🚀 Step 3: Running {} segments in parallel...", self.config.num_segments);
|
|
||||||
self.run_segments_parallel().await?;
|
|
||||||
|
|
||||||
// Step 4: Generate final report
|
|
||||||
println!("\n📊 Step 4: Generating final report...");
|
|
||||||
self.status.completed_at = Some(Utc::now());
|
|
||||||
self.save_status()?;
|
|
||||||
|
|
||||||
let report = self.status.generate_report();
|
|
||||||
println!("{}", report);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Partition requirements using an AI agent
|
|
||||||
async fn partition_requirements(&mut self) -> Result<Vec<String>> {
|
|
||||||
let requirements_path = self.config.project_dir.join("flock-requirements.md");
|
|
||||||
let requirements_content = std::fs::read_to_string(&requirements_path)
|
|
||||||
.context("Failed to read flock-requirements.md")?;
|
|
||||||
|
|
||||||
// Create a temporary workspace for the partitioning agent
|
|
||||||
let partition_workspace = self.config.flock_workspace.join("_partition");
|
|
||||||
std::fs::create_dir_all(&partition_workspace)?;
|
|
||||||
|
|
||||||
// Create the partitioning prompt
|
|
||||||
let partition_prompt = format!(
|
|
||||||
"You are a software architect tasked with partitioning project requirements into {} logical, \
|
|
||||||
largely non-overlapping modules that can grow into separate architectural components \
|
|
||||||
(e.g., crates, services, or packages).\n\n\
|
|
||||||
REQUIREMENTS:\n{}\n\n\
|
|
||||||
INSTRUCTIONS:\n\
|
|
||||||
1. Analyze the requirements carefully\n\
|
|
||||||
2. Identify {} distinct architectural modules that:\n\
|
|
||||||
- Have minimal overlap and dependencies\n\
|
|
||||||
- Can be developed largely independently\n\
|
|
||||||
- Represent logical architectural boundaries\n\
|
|
||||||
- Could eventually become separate crates or services\n\
|
|
||||||
3. For each module, provide:\n\
|
|
||||||
- A clear module name\n\
|
|
||||||
- The specific requirements that belong to this module\n\
|
|
||||||
- Any dependencies on other modules\n\n\
|
|
||||||
4. Use the final_output tool to provide your partitioning as a JSON array of objects, where each object has:\n\
|
|
||||||
- \"module_name\": string\n\
|
|
||||||
- \"requirements\": string (the requirements text for this module)\n\
|
|
||||||
- \"dependencies\": array of strings (names of other modules this depends on)\n\n\
|
|
||||||
Example format:\n\
|
|
||||||
```json\n\
|
|
||||||
[\n\
|
|
||||||
{{\n\
|
|
||||||
\"module_name\": \"core-engine\",\n\
|
|
||||||
\"requirements\": \"Implement the core processing engine...\",\n\
|
|
||||||
\"dependencies\": []\n\
|
|
||||||
}},\n\
|
|
||||||
{{\n\
|
|
||||||
\"module_name\": \"api-server\",\n\
|
|
||||||
\"requirements\": \"Create REST API endpoints...\",\n\
|
|
||||||
\"dependencies\": [\"core-engine\"]\n\
|
|
||||||
}}\n\
|
|
||||||
]\n\
|
|
||||||
```\n\n\
|
|
||||||
Be thoughtful and strategic in your partitioning. The goal is to enable parallel development.",
|
|
||||||
self.config.num_segments,
|
|
||||||
requirements_content,
|
|
||||||
self.config.num_segments
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get g3 binary path
|
|
||||||
let g3_binary = self.get_g3_binary()?;
|
|
||||||
|
|
||||||
// Run g3 in single-shot mode to partition requirements
|
|
||||||
println!(" Analyzing requirements and creating partitions...");
|
|
||||||
let output = Command::new(&g3_binary)
|
|
||||||
.arg("--workspace")
|
|
||||||
.arg(&partition_workspace)
|
|
||||||
.arg("--quiet") // Disable logging for partitioning agent
|
|
||||||
.arg(&partition_prompt)
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.context("Failed to run g3 for partitioning")?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
anyhow::bail!("Partitioning agent failed: {}", stderr);
|
|
||||||
}
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
debug!("Partitioning agent output: {}", stdout);
|
|
||||||
|
|
||||||
// Extract JSON from the output
|
|
||||||
let partitions_json = self.extract_json_from_output(&stdout)
|
|
||||||
.context("Failed to extract partition JSON from agent output")?;
|
|
||||||
|
|
||||||
// Parse the partitions
|
|
||||||
let partitions: Vec<serde_json::Value> = serde_json::from_str(&partitions_json)
|
|
||||||
.context("Failed to parse partition JSON")?;
|
|
||||||
|
|
||||||
if partitions.len() != self.config.num_segments {
|
|
||||||
warn!(
|
|
||||||
"Expected {} partitions but got {}. Adjusting...",
|
|
||||||
self.config.num_segments,
|
|
||||||
partitions.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract requirements text from each partition
|
|
||||||
let mut partition_texts = Vec::new();
|
|
||||||
for (i, partition) in partitions.iter().enumerate() {
|
|
||||||
let default_name = format!("module-{}", i + 1);
|
|
||||||
let module_name = partition["module_name"]
|
|
||||||
.as_str()
|
|
||||||
.unwrap_or(&default_name);
|
|
||||||
let requirements = partition["requirements"]
|
|
||||||
.as_str()
|
|
||||||
.context("Missing requirements field in partition")?;
|
|
||||||
let dependencies = partition["dependencies"]
|
|
||||||
.as_array()
|
|
||||||
.map(|arr| {
|
|
||||||
arr.iter()
|
|
||||||
.filter_map(|v| v.as_str())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let partition_text = format!(
|
|
||||||
"# Module: {}\n\n## Dependencies\n{}\n\n## Requirements\n\n{}",
|
|
||||||
module_name,
|
|
||||||
if dependencies.is_empty() {
|
|
||||||
"None".to_string()
|
|
||||||
} else {
|
|
||||||
dependencies
|
|
||||||
},
|
|
||||||
requirements
|
|
||||||
);
|
|
||||||
|
|
||||||
partition_texts.push(partition_text);
|
|
||||||
println!(" ✓ Created partition {}: {}", i + 1, module_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(partition_texts)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract JSON from agent output (looks for JSON array in output)
|
|
||||||
fn extract_json_from_output(&self, output: &str) -> Result<String> {
|
|
||||||
// Look for JSON array in the output
|
|
||||||
// Try to find content between [ and ]
|
|
||||||
if let Some(start) = output.find('[') {
|
|
||||||
if let Some(end) = output.rfind(']') {
|
|
||||||
if end > start {
|
|
||||||
return Ok(output[start..=end].to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we can't find JSON array markers, try to parse the whole output
|
|
||||||
anyhow::bail!("Could not find JSON array in agent output")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create segment workspaces by copying project directory
|
|
||||||
async fn create_segment_workspaces(&mut self, partitions: &[String]) -> Result<()> {
|
|
||||||
// Ensure flock workspace exists
|
|
||||||
std::fs::create_dir_all(&self.config.flock_workspace)?;
|
|
||||||
|
|
||||||
for (i, partition) in partitions.iter().enumerate() {
|
|
||||||
let segment_id = i + 1;
|
|
||||||
let segment_dir = self.config.flock_workspace.join(format!("segment-{}", segment_id));
|
|
||||||
|
|
||||||
println!(" Creating segment {} workspace...", segment_id);
|
|
||||||
|
|
||||||
// Copy project directory to segment directory
|
|
||||||
self.copy_git_repo(&self.config.project_dir, &segment_dir)
|
|
||||||
.await
|
|
||||||
.context(format!("Failed to copy project to segment {}", segment_id))?;
|
|
||||||
|
|
||||||
// Write segment-requirements.md
|
|
||||||
let requirements_path = segment_dir.join("segment-requirements.md");
|
|
||||||
std::fs::write(&requirements_path, partition)
|
|
||||||
.context(format!("Failed to write requirements for segment {}", segment_id))?;
|
|
||||||
|
|
||||||
println!(" ✓ Segment {} workspace ready at {}", segment_id, segment_dir.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copy a git repository to a new location
|
|
||||||
async fn copy_git_repo(&self, source: &Path, dest: &Path) -> Result<()> {
|
|
||||||
// Use git clone for efficient copying
|
|
||||||
let output = Command::new("git")
|
|
||||||
.arg("clone")
|
|
||||||
.arg(source)
|
|
||||||
.arg(dest)
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.context("Failed to run git clone")?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
anyhow::bail!("Git clone failed: {}", stderr);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run all segments in parallel
|
|
||||||
async fn run_segments_parallel(&mut self) -> Result<()> {
|
|
||||||
let mut handles = Vec::new();
|
|
||||||
|
|
||||||
for segment_id in 1..=self.config.num_segments {
|
|
||||||
let segment_dir = self.config.flock_workspace.join(format!("segment-{}", segment_id));
|
|
||||||
let max_turns = self.config.max_turns;
|
|
||||||
let g3_binary = self.get_g3_binary()?;
|
|
||||||
let status_file = self.get_status_file_path();
|
|
||||||
let session_id = self.session_id.clone();
|
|
||||||
|
|
||||||
// Initialize segment status
|
|
||||||
let segment_status = SegmentStatus {
|
|
||||||
segment_id,
|
|
||||||
workspace: segment_dir.clone(),
|
|
||||||
state: SegmentState::Running,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: None,
|
|
||||||
tokens_used: 0,
|
|
||||||
tool_calls: 0,
|
|
||||||
errors: 0,
|
|
||||||
current_turn: 0,
|
|
||||||
max_turns,
|
|
||||||
last_message: Some("Starting...".to_string()),
|
|
||||||
error_message: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.status.update_segment(segment_id, segment_status);
|
|
||||||
self.save_status()?;
|
|
||||||
|
|
||||||
// Spawn a task for this segment
|
|
||||||
let handle = tokio::spawn(async move {
|
|
||||||
run_segment(
|
|
||||||
segment_id,
|
|
||||||
segment_dir,
|
|
||||||
max_turns,
|
|
||||||
g3_binary,
|
|
||||||
status_file,
|
|
||||||
session_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
});
|
|
||||||
|
|
||||||
handles.push((segment_id, handle));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all segments to complete
|
|
||||||
for (segment_id, handle) in handles {
|
|
||||||
match handle.await {
|
|
||||||
Ok(Ok(final_status)) => {
|
|
||||||
println!("\n✅ Segment {} completed", segment_id);
|
|
||||||
self.status.update_segment(segment_id, final_status);
|
|
||||||
self.save_status()?;
|
|
||||||
}
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
error!("Segment {} failed: {}", segment_id, e);
|
|
||||||
let mut segment_status = self.status.segments.get(&segment_id).cloned()
|
|
||||||
.unwrap_or_else(|| SegmentStatus {
|
|
||||||
segment_id,
|
|
||||||
workspace: self.config.flock_workspace.join(format!("segment-{}", segment_id)),
|
|
||||||
state: SegmentState::Failed,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: Some(Utc::now()),
|
|
||||||
tokens_used: 0,
|
|
||||||
tool_calls: 0,
|
|
||||||
errors: 1,
|
|
||||||
current_turn: 0,
|
|
||||||
max_turns: self.config.max_turns,
|
|
||||||
last_message: None,
|
|
||||||
error_message: Some(e.to_string()),
|
|
||||||
});
|
|
||||||
segment_status.state = SegmentState::Failed;
|
|
||||||
segment_status.completed_at = Some(Utc::now());
|
|
||||||
segment_status.error_message = Some(e.to_string());
|
|
||||||
segment_status.errors += 1;
|
|
||||||
self.status.update_segment(segment_id, segment_status);
|
|
||||||
self.save_status()?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Segment {} task panicked: {}", segment_id, e);
|
|
||||||
let mut segment_status = self.status.segments.get(&segment_id).cloned()
|
|
||||||
.unwrap_or_else(|| SegmentStatus {
|
|
||||||
segment_id,
|
|
||||||
workspace: self.config.flock_workspace.join(format!("segment-{}", segment_id)),
|
|
||||||
state: SegmentState::Failed,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: Some(Utc::now()),
|
|
||||||
tokens_used: 0,
|
|
||||||
tool_calls: 0,
|
|
||||||
errors: 1,
|
|
||||||
current_turn: 0,
|
|
||||||
max_turns: self.config.max_turns,
|
|
||||||
last_message: None,
|
|
||||||
error_message: Some(format!("Task panicked: {}", e)),
|
|
||||||
});
|
|
||||||
segment_status.state = SegmentState::Failed;
|
|
||||||
segment_status.completed_at = Some(Utc::now());
|
|
||||||
segment_status.error_message = Some(format!("Task panicked: {}", e));
|
|
||||||
segment_status.errors += 1;
|
|
||||||
self.status.update_segment(segment_id, segment_status);
|
|
||||||
self.save_status()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the g3 binary path
|
|
||||||
fn get_g3_binary(&self) -> Result<PathBuf> {
|
|
||||||
if let Some(ref binary) = self.config.g3_binary {
|
|
||||||
Ok(binary.clone())
|
|
||||||
} else {
|
|
||||||
// Use current executable
|
|
||||||
std::env::current_exe().context("Failed to get current executable path")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the status file path
|
|
||||||
fn get_status_file_path(&self) -> PathBuf {
|
|
||||||
self.config.flock_workspace.join("flock-status.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save current status to file
|
|
||||||
fn save_status(&self) -> Result<()> {
|
|
||||||
let status_file = self.get_status_file_path();
|
|
||||||
self.status.save_to_file(&status_file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run a single segment worker
|
|
||||||
async fn run_segment(
|
|
||||||
segment_id: usize,
|
|
||||||
segment_dir: PathBuf,
|
|
||||||
max_turns: usize,
|
|
||||||
g3_binary: PathBuf,
|
|
||||||
status_file: PathBuf,
|
|
||||||
session_id: String,
|
|
||||||
) -> Result<SegmentStatus> {
|
|
||||||
info!("Starting segment {} in {}", segment_id, segment_dir.display());
|
|
||||||
|
|
||||||
let mut segment_status = SegmentStatus {
|
|
||||||
segment_id,
|
|
||||||
workspace: segment_dir.clone(),
|
|
||||||
state: SegmentState::Running,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: None,
|
|
||||||
tokens_used: 0,
|
|
||||||
tool_calls: 0,
|
|
||||||
errors: 0,
|
|
||||||
current_turn: 0,
|
|
||||||
max_turns,
|
|
||||||
last_message: Some("Starting autonomous mode...".to_string()),
|
|
||||||
error_message: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run g3 in autonomous mode with segment-requirements.md
|
|
||||||
let mut child = Command::new(&g3_binary)
|
|
||||||
.arg("--workspace")
|
|
||||||
.arg(&segment_dir)
|
|
||||||
.arg("--autonomous")
|
|
||||||
.arg("--max-turns")
|
|
||||||
.arg(max_turns.to_string())
|
|
||||||
.arg("--requirements")
|
|
||||||
.arg(std::fs::read_to_string(segment_dir.join("segment-requirements.md"))?)
|
|
||||||
.arg("--quiet") // Disable session logging for workers
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::piped())
|
|
||||||
.spawn()
|
|
||||||
.context("Failed to spawn g3 process")?;
|
|
||||||
|
|
||||||
// Stream output and update status
|
|
||||||
let stdout = child.stdout.take().context("Failed to get stdout")?;
|
|
||||||
let stderr = child.stderr.take().context("Failed to get stderr")?;
|
|
||||||
|
|
||||||
let stdout_reader = BufReader::new(stdout);
|
|
||||||
let stderr_reader = BufReader::new(stderr);
|
|
||||||
|
|
||||||
let mut stdout_lines = stdout_reader.lines();
|
|
||||||
let mut stderr_lines = stderr_reader.lines();
|
|
||||||
|
|
||||||
// Read output and update status
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
line = stdout_lines.next_line() => {
|
|
||||||
match line {
|
|
||||||
Ok(Some(line)) => {
|
|
||||||
println!("[Segment {}] {}", segment_id, line);
|
|
||||||
|
|
||||||
// Parse output for status updates
|
|
||||||
if line.contains("TURN") {
|
|
||||||
// Extract turn number if possible
|
|
||||||
if let Some(turn_str) = line.split("TURN").nth(1) {
|
|
||||||
if let Ok(turn) = turn_str.trim().split('/').next().unwrap_or("0").parse::<usize>() {
|
|
||||||
segment_status.current_turn = turn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
segment_status.last_message = Some(line);
|
|
||||||
update_status_file(&status_file, &session_id, segment_status.clone())?;
|
|
||||||
}
|
|
||||||
Ok(None) => break,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error reading stdout for segment {}: {}", segment_id, e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
line = stderr_lines.next_line() => {
|
|
||||||
match line {
|
|
||||||
Ok(Some(line)) => {
|
|
||||||
eprintln!("[Segment {} ERROR] {}", segment_id, line);
|
|
||||||
segment_status.errors += 1;
|
|
||||||
update_status_file(&status_file, &session_id, segment_status.clone())?;
|
|
||||||
}
|
|
||||||
Ok(None) => break,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error reading stderr for segment {}: {}", segment_id, e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for process to complete
|
|
||||||
let status = child.wait().await.context("Failed to wait for g3 process")?;
|
|
||||||
|
|
||||||
segment_status.completed_at = Some(Utc::now());
|
|
||||||
|
|
||||||
if status.success() {
|
|
||||||
segment_status.state = SegmentState::Completed;
|
|
||||||
segment_status.last_message = Some("Completed successfully".to_string());
|
|
||||||
} else {
|
|
||||||
segment_status.state = SegmentState::Failed;
|
|
||||||
segment_status.error_message = Some(format!("Process exited with status: {}", status));
|
|
||||||
segment_status.errors += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to extract metrics from session log if available
|
|
||||||
let log_dir = segment_dir.join("logs");
|
|
||||||
if log_dir.exists() {
|
|
||||||
if let Ok(entries) = std::fs::read_dir(&log_dir) {
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path();
|
|
||||||
if path.extension().and_then(|s| s.to_str()) == Some("json") {
|
|
||||||
if let Ok(log_content) = std::fs::read_to_string(&path) {
|
|
||||||
if let Ok(log_json) = serde_json::from_str::<serde_json::Value>(&log_content) {
|
|
||||||
// Extract token usage
|
|
||||||
if let Some(context) = log_json.get("context_window") {
|
|
||||||
if let Some(cumulative) = context.get("cumulative_tokens") {
|
|
||||||
if let Some(tokens) = cumulative.as_u64() {
|
|
||||||
segment_status.tokens_used = tokens;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count tool calls from conversation history
|
|
||||||
if let Some(context) = log_json.get("context_window") {
|
|
||||||
if let Some(history) = context.get("conversation_history") {
|
|
||||||
if let Some(messages) = history.as_array() {
|
|
||||||
let tool_call_count = messages
|
|
||||||
.iter()
|
|
||||||
.filter(|msg| {
|
|
||||||
msg.get("role")
|
|
||||||
.and_then(|r| r.as_str())
|
|
||||||
== Some("tool")
|
|
||||||
})
|
|
||||||
.count();
|
|
||||||
segment_status.tool_calls = tool_call_count as u64;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update_status_file(&status_file, &session_id, segment_status.clone())?;
|
|
||||||
|
|
||||||
Ok(segment_status)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the status file with new segment status
|
|
||||||
fn update_status_file(
|
|
||||||
status_file: &PathBuf,
|
|
||||||
session_id: &str,
|
|
||||||
segment_status: SegmentStatus,
|
|
||||||
) -> Result<()> {
|
|
||||||
// Load existing status or create new one
|
|
||||||
let mut flock_status = if status_file.exists() {
|
|
||||||
FlockStatus::load_from_file(status_file)?
|
|
||||||
} else {
|
|
||||||
// This shouldn't happen, but handle it gracefully
|
|
||||||
FlockStatus::new(
|
|
||||||
session_id.to_string(),
|
|
||||||
PathBuf::new(),
|
|
||||||
PathBuf::new(),
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
flock_status.update_segment(segment_status.segment_id, segment_status);
|
|
||||||
flock_status.save_to_file(status_file)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
//! G3 Ensembles - Multi-agent ensemble functionality
|
|
||||||
//!
|
|
||||||
//! This crate provides functionality for running multiple G3 agents in coordination,
|
|
||||||
//! enabling parallel development across different architectural modules.
|
|
||||||
|
|
||||||
pub mod flock;
|
|
||||||
pub mod status;
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
/// Re-export main types for convenience
|
|
||||||
pub use flock::{FlockConfig, FlockMode};
|
|
||||||
pub use status::{FlockStatus, SegmentStatus};
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
//! Status tracking for flock mode
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// Status of an individual segment worker
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct SegmentStatus {
|
|
||||||
/// Segment number
|
|
||||||
pub segment_id: usize,
|
|
||||||
|
|
||||||
/// Segment workspace directory
|
|
||||||
pub workspace: PathBuf,
|
|
||||||
|
|
||||||
/// Current state of the segment
|
|
||||||
pub state: SegmentState,
|
|
||||||
|
|
||||||
/// Start time
|
|
||||||
pub started_at: DateTime<Utc>,
|
|
||||||
|
|
||||||
/// Completion time (if finished)
|
|
||||||
pub completed_at: Option<DateTime<Utc>>,
|
|
||||||
|
|
||||||
/// Total tokens used
|
|
||||||
pub tokens_used: u64,
|
|
||||||
|
|
||||||
/// Number of tool calls made
|
|
||||||
pub tool_calls: u64,
|
|
||||||
|
|
||||||
/// Number of errors encountered
|
|
||||||
pub errors: u64,
|
|
||||||
|
|
||||||
/// Current turn number (for autonomous mode)
|
|
||||||
pub current_turn: usize,
|
|
||||||
|
|
||||||
/// Maximum turns allowed
|
|
||||||
pub max_turns: usize,
|
|
||||||
|
|
||||||
/// Last status message
|
|
||||||
pub last_message: Option<String>,
|
|
||||||
|
|
||||||
/// Error message (if failed)
|
|
||||||
pub error_message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// State of a segment worker
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub enum SegmentState {
|
|
||||||
/// Waiting to start
|
|
||||||
Pending,
|
|
||||||
|
|
||||||
/// Currently running
|
|
||||||
Running,
|
|
||||||
|
|
||||||
/// Completed successfully
|
|
||||||
Completed,
|
|
||||||
|
|
||||||
/// Failed with error
|
|
||||||
Failed,
|
|
||||||
|
|
||||||
/// Cancelled by user
|
|
||||||
Cancelled,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for SegmentState {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
SegmentState::Pending => write!(f, "⏳ Pending"),
|
|
||||||
SegmentState::Running => write!(f, "🔄 Running"),
|
|
||||||
SegmentState::Completed => write!(f, "✅ Completed"),
|
|
||||||
SegmentState::Failed => write!(f, "❌ Failed"),
|
|
||||||
SegmentState::Cancelled => write!(f, "⚠️ Cancelled"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Overall flock status
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct FlockStatus {
|
|
||||||
/// Flock session ID
|
|
||||||
pub session_id: String,
|
|
||||||
|
|
||||||
/// Project directory
|
|
||||||
pub project_dir: PathBuf,
|
|
||||||
|
|
||||||
/// Flock workspace directory
|
|
||||||
pub flock_workspace: PathBuf,
|
|
||||||
|
|
||||||
/// Number of segments
|
|
||||||
pub num_segments: usize,
|
|
||||||
|
|
||||||
/// Start time
|
|
||||||
pub started_at: DateTime<Utc>,
|
|
||||||
|
|
||||||
/// Completion time (if finished)
|
|
||||||
pub completed_at: Option<DateTime<Utc>>,
|
|
||||||
|
|
||||||
/// Status of each segment
|
|
||||||
pub segments: HashMap<usize, SegmentStatus>,
|
|
||||||
|
|
||||||
/// Total tokens used across all segments
|
|
||||||
pub total_tokens: u64,
|
|
||||||
|
|
||||||
/// Total tool calls across all segments
|
|
||||||
pub total_tool_calls: u64,
|
|
||||||
|
|
||||||
/// Total errors across all segments
|
|
||||||
pub total_errors: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FlockStatus {
|
|
||||||
/// Create a new flock status
|
|
||||||
pub fn new(
|
|
||||||
session_id: String,
|
|
||||||
project_dir: PathBuf,
|
|
||||||
flock_workspace: PathBuf,
|
|
||||||
num_segments: usize,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
session_id,
|
|
||||||
project_dir,
|
|
||||||
flock_workspace,
|
|
||||||
num_segments,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: None,
|
|
||||||
segments: HashMap::new(),
|
|
||||||
total_tokens: 0,
|
|
||||||
total_tool_calls: 0,
|
|
||||||
total_errors: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update segment status
|
|
||||||
pub fn update_segment(&mut self, segment_id: usize, status: SegmentStatus) {
|
|
||||||
self.segments.insert(segment_id, status);
|
|
||||||
self.recalculate_totals();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recalculate total metrics
|
|
||||||
fn recalculate_totals(&mut self) {
|
|
||||||
self.total_tokens = self.segments.values().map(|s| s.tokens_used).sum();
|
|
||||||
self.total_tool_calls = self.segments.values().map(|s| s.tool_calls).sum();
|
|
||||||
self.total_errors = self.segments.values().map(|s| s.errors).sum();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if all segments are complete
|
|
||||||
pub fn is_complete(&self) -> bool {
|
|
||||||
self.segments.len() == self.num_segments
|
|
||||||
&& self.segments.values().all(|s| {
|
|
||||||
matches!(
|
|
||||||
s.state,
|
|
||||||
SegmentState::Completed | SegmentState::Failed | SegmentState::Cancelled
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get count of segments by state
|
|
||||||
pub fn count_by_state(&self, state: SegmentState) -> usize {
|
|
||||||
self.segments.values().filter(|s| s.state == state).count()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save status to file
|
|
||||||
pub fn save_to_file(&self, path: &PathBuf) -> anyhow::Result<()> {
|
|
||||||
let json = serde_json::to_string_pretty(self)?;
|
|
||||||
std::fs::write(path, json)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load status from file
|
|
||||||
pub fn load_from_file(path: &PathBuf) -> anyhow::Result<Self> {
|
|
||||||
let json = std::fs::read_to_string(path)?;
|
|
||||||
let status = serde_json::from_str(&json)?;
|
|
||||||
Ok(status)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate a summary report
|
|
||||||
pub fn generate_report(&self) -> String {
|
|
||||||
let mut report = String::new();
|
|
||||||
|
|
||||||
report.push_str(&format!("\n{}", "=".repeat(80)));
|
|
||||||
report.push_str(&format!("\n📊 FLOCK MODE SESSION REPORT"));
|
|
||||||
report.push_str(&format!("\n{}", "=".repeat(80)));
|
|
||||||
|
|
||||||
report.push_str(&format!("\n\n🆔 Session ID: {}", self.session_id));
|
|
||||||
report.push_str(&format!("\n📁 Project: {}", self.project_dir.display()));
|
|
||||||
report.push_str(&format!("\n🗂️ Workspace: {}", self.flock_workspace.display()));
|
|
||||||
report.push_str(&format!("\n🔢 Segments: {}", self.num_segments));
|
|
||||||
|
|
||||||
let duration = if let Some(completed) = self.completed_at {
|
|
||||||
completed.signed_duration_since(self.started_at)
|
|
||||||
} else {
|
|
||||||
Utc::now().signed_duration_since(self.started_at)
|
|
||||||
};
|
|
||||||
|
|
||||||
report.push_str(&format!("\n⏱️ Duration: {:.2}s", duration.num_milliseconds() as f64 / 1000.0));
|
|
||||||
|
|
||||||
// Segment status summary
|
|
||||||
report.push_str(&format!("\n\n📈 Segment Status:"));
|
|
||||||
report.push_str(&format!("\n • Completed: {}", self.count_by_state(SegmentState::Completed)));
|
|
||||||
report.push_str(&format!("\n • Running: {}", self.count_by_state(SegmentState::Running)));
|
|
||||||
report.push_str(&format!("\n • Failed: {}", self.count_by_state(SegmentState::Failed)));
|
|
||||||
report.push_str(&format!("\n • Pending: {}", self.count_by_state(SegmentState::Pending)));
|
|
||||||
report.push_str(&format!("\n • Cancelled: {}", self.count_by_state(SegmentState::Cancelled)));
|
|
||||||
|
|
||||||
// Metrics
|
|
||||||
report.push_str(&format!("\n\n📊 Aggregate Metrics:"));
|
|
||||||
report.push_str(&format!("\n • Total Tokens: {}", self.total_tokens));
|
|
||||||
report.push_str(&format!("\n • Total Tool Calls: {}", self.total_tool_calls));
|
|
||||||
report.push_str(&format!("\n • Total Errors: {}", self.total_errors));
|
|
||||||
|
|
||||||
// Per-segment details
|
|
||||||
report.push_str(&format!("\n\n🔍 Segment Details:"));
|
|
||||||
let mut segments: Vec<_> = self.segments.iter().collect();
|
|
||||||
segments.sort_by_key(|(id, _)| *id);
|
|
||||||
|
|
||||||
for (id, segment) in segments {
|
|
||||||
report.push_str(&format!("\n\n Segment {}:", id));
|
|
||||||
report.push_str(&format!("\n Status: {}", segment.state));
|
|
||||||
report.push_str(&format!("\n Workspace: {}", segment.workspace.display()));
|
|
||||||
report.push_str(&format!("\n Tokens: {}", segment.tokens_used));
|
|
||||||
report.push_str(&format!("\n Tool Calls: {}", segment.tool_calls));
|
|
||||||
report.push_str(&format!("\n Errors: {}", segment.errors));
|
|
||||||
report.push_str(&format!("\n Turn: {}/{}", segment.current_turn, segment.max_turns));
|
|
||||||
|
|
||||||
if let Some(ref msg) = segment.last_message {
|
|
||||||
report.push_str(&format!("\n Last Message: {}", msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref err) = segment.error_message {
|
|
||||||
report.push_str(&format!("\n Error: {}", err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
report.push_str(&format!("\n\n{}", "=".repeat(80)));
|
|
||||||
|
|
||||||
report
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
//! Unit tests for g3-ensembles
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::status::{FlockStatus, SegmentState, SegmentStatus};
|
|
||||||
use chrono::Utc;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_segment_state_display() {
|
|
||||||
assert_eq!(format!("{}", SegmentState::Pending), "⏳ Pending");
|
|
||||||
assert_eq!(format!("{}", SegmentState::Running), "🔄 Running");
|
|
||||||
assert_eq!(format!("{}", SegmentState::Completed), "✅ Completed");
|
|
||||||
assert_eq!(format!("{}", SegmentState::Failed), "❌ Failed");
|
|
||||||
assert_eq!(format!("{}", SegmentState::Cancelled), "⚠️ Cancelled");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_flock_status_creation() {
|
|
||||||
let status = FlockStatus::new(
|
|
||||||
"test-session".to_string(),
|
|
||||||
PathBuf::from("/test/project"),
|
|
||||||
PathBuf::from("/test/workspace"),
|
|
||||||
3,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(status.session_id, "test-session");
|
|
||||||
assert_eq!(status.num_segments, 3);
|
|
||||||
assert_eq!(status.segments.len(), 0);
|
|
||||||
assert_eq!(status.total_tokens, 0);
|
|
||||||
assert_eq!(status.total_tool_calls, 0);
|
|
||||||
assert_eq!(status.total_errors, 0);
|
|
||||||
assert!(status.completed_at.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_segment_status_update() {
|
|
||||||
let mut status = FlockStatus::new(
|
|
||||||
"test-session".to_string(),
|
|
||||||
PathBuf::from("/test/project"),
|
|
||||||
PathBuf::from("/test/workspace"),
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
|
|
||||||
let segment1 = SegmentStatus {
|
|
||||||
segment_id: 1,
|
|
||||||
workspace: PathBuf::from("/test/workspace/segment-1"),
|
|
||||||
state: SegmentState::Completed,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: Some(Utc::now()),
|
|
||||||
tokens_used: 1000,
|
|
||||||
tool_calls: 50,
|
|
||||||
errors: 2,
|
|
||||||
current_turn: 5,
|
|
||||||
max_turns: 10,
|
|
||||||
last_message: Some("Done".to_string()),
|
|
||||||
error_message: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
status.update_segment(1, segment1);
|
|
||||||
|
|
||||||
assert_eq!(status.segments.len(), 1);
|
|
||||||
assert_eq!(status.total_tokens, 1000);
|
|
||||||
assert_eq!(status.total_tool_calls, 50);
|
|
||||||
assert_eq!(status.total_errors, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multiple_segment_updates() {
|
|
||||||
let mut status = FlockStatus::new(
|
|
||||||
"test-session".to_string(),
|
|
||||||
PathBuf::from("/test/project"),
|
|
||||||
PathBuf::from("/test/workspace"),
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
|
|
||||||
let segment1 = SegmentStatus {
|
|
||||||
segment_id: 1,
|
|
||||||
workspace: PathBuf::from("/test/workspace/segment-1"),
|
|
||||||
state: SegmentState::Completed,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: Some(Utc::now()),
|
|
||||||
tokens_used: 1000,
|
|
||||||
tool_calls: 50,
|
|
||||||
errors: 2,
|
|
||||||
current_turn: 5,
|
|
||||||
max_turns: 10,
|
|
||||||
last_message: Some("Done".to_string()),
|
|
||||||
error_message: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let segment2 = SegmentStatus {
|
|
||||||
segment_id: 2,
|
|
||||||
workspace: PathBuf::from("/test/workspace/segment-2"),
|
|
||||||
state: SegmentState::Failed,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: Some(Utc::now()),
|
|
||||||
tokens_used: 500,
|
|
||||||
tool_calls: 25,
|
|
||||||
errors: 5,
|
|
||||||
current_turn: 3,
|
|
||||||
max_turns: 10,
|
|
||||||
last_message: Some("Error".to_string()),
|
|
||||||
error_message: Some("Test error".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
status.update_segment(1, segment1);
|
|
||||||
status.update_segment(2, segment2);
|
|
||||||
|
|
||||||
assert_eq!(status.segments.len(), 2);
|
|
||||||
assert_eq!(status.total_tokens, 1500);
|
|
||||||
assert_eq!(status.total_tool_calls, 75);
|
|
||||||
assert_eq!(status.total_errors, 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_is_complete() {
|
|
||||||
let mut status = FlockStatus::new(
|
|
||||||
"test-session".to_string(),
|
|
||||||
PathBuf::from("/test/project"),
|
|
||||||
PathBuf::from("/test/workspace"),
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Not complete - no segments
|
|
||||||
assert!(!status.is_complete());
|
|
||||||
|
|
||||||
// Add one completed segment
|
|
||||||
let segment1 = SegmentStatus {
|
|
||||||
segment_id: 1,
|
|
||||||
workspace: PathBuf::from("/test/workspace/segment-1"),
|
|
||||||
state: SegmentState::Completed,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: Some(Utc::now()),
|
|
||||||
tokens_used: 1000,
|
|
||||||
tool_calls: 50,
|
|
||||||
errors: 0,
|
|
||||||
current_turn: 5,
|
|
||||||
max_turns: 10,
|
|
||||||
last_message: None,
|
|
||||||
error_message: None,
|
|
||||||
};
|
|
||||||
status.update_segment(1, segment1);
|
|
||||||
|
|
||||||
// Still not complete - only 1 of 2 segments
|
|
||||||
assert!(!status.is_complete());
|
|
||||||
|
|
||||||
// Add second segment (running)
|
|
||||||
let segment2 = SegmentStatus {
|
|
||||||
segment_id: 2,
|
|
||||||
workspace: PathBuf::from("/test/workspace/segment-2"),
|
|
||||||
state: SegmentState::Running,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: None,
|
|
||||||
tokens_used: 500,
|
|
||||||
tool_calls: 25,
|
|
||||||
errors: 0,
|
|
||||||
current_turn: 3,
|
|
||||||
max_turns: 10,
|
|
||||||
last_message: None,
|
|
||||||
error_message: None,
|
|
||||||
};
|
|
||||||
status.update_segment(2, segment2);
|
|
||||||
|
|
||||||
// Still not complete - segment 2 is running
|
|
||||||
assert!(!status.is_complete());
|
|
||||||
|
|
||||||
// Update segment 2 to completed
|
|
||||||
let segment2_done = SegmentStatus {
|
|
||||||
segment_id: 2,
|
|
||||||
workspace: PathBuf::from("/test/workspace/segment-2"),
|
|
||||||
state: SegmentState::Completed,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: Some(Utc::now()),
|
|
||||||
tokens_used: 500,
|
|
||||||
tool_calls: 25,
|
|
||||||
errors: 0,
|
|
||||||
current_turn: 5,
|
|
||||||
max_turns: 10,
|
|
||||||
last_message: None,
|
|
||||||
error_message: None,
|
|
||||||
};
|
|
||||||
status.update_segment(2, segment2_done);
|
|
||||||
|
|
||||||
// Now complete
|
|
||||||
assert!(status.is_complete());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_count_by_state() {
|
|
||||||
let mut status = FlockStatus::new(
|
|
||||||
"test-session".to_string(),
|
|
||||||
PathBuf::from("/test/project"),
|
|
||||||
PathBuf::from("/test/workspace"),
|
|
||||||
3,
|
|
||||||
);
|
|
||||||
|
|
||||||
let segment1 = SegmentStatus {
|
|
||||||
segment_id: 1,
|
|
||||||
workspace: PathBuf::from("/test/workspace/segment-1"),
|
|
||||||
state: SegmentState::Completed,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: Some(Utc::now()),
|
|
||||||
tokens_used: 1000,
|
|
||||||
tool_calls: 50,
|
|
||||||
errors: 0,
|
|
||||||
current_turn: 5,
|
|
||||||
max_turns: 10,
|
|
||||||
last_message: None,
|
|
||||||
error_message: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let segment2 = SegmentStatus {
|
|
||||||
segment_id: 2,
|
|
||||||
workspace: PathBuf::from("/test/workspace/segment-2"),
|
|
||||||
state: SegmentState::Failed,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: Some(Utc::now()),
|
|
||||||
tokens_used: 500,
|
|
||||||
tool_calls: 25,
|
|
||||||
errors: 5,
|
|
||||||
current_turn: 3,
|
|
||||||
max_turns: 10,
|
|
||||||
last_message: None,
|
|
||||||
error_message: Some("Error".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let segment3 = SegmentStatus {
|
|
||||||
segment_id: 3,
|
|
||||||
workspace: PathBuf::from("/test/workspace/segment-3"),
|
|
||||||
state: SegmentState::Completed,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: Some(Utc::now()),
|
|
||||||
tokens_used: 800,
|
|
||||||
tool_calls: 40,
|
|
||||||
errors: 1,
|
|
||||||
current_turn: 4,
|
|
||||||
max_turns: 10,
|
|
||||||
last_message: None,
|
|
||||||
error_message: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
status.update_segment(1, segment1);
|
|
||||||
status.update_segment(2, segment2);
|
|
||||||
status.update_segment(3, segment3);
|
|
||||||
|
|
||||||
assert_eq!(status.count_by_state(SegmentState::Completed), 2);
|
|
||||||
assert_eq!(status.count_by_state(SegmentState::Failed), 1);
|
|
||||||
assert_eq!(status.count_by_state(SegmentState::Running), 0);
|
|
||||||
assert_eq!(status.count_by_state(SegmentState::Pending), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_status_serialization() {
|
|
||||||
let mut status = FlockStatus::new(
|
|
||||||
"test-session".to_string(),
|
|
||||||
PathBuf::from("/test/project"),
|
|
||||||
PathBuf::from("/test/workspace"),
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
|
|
||||||
let segment1 = SegmentStatus {
|
|
||||||
segment_id: 1,
|
|
||||||
workspace: PathBuf::from("/test/workspace/segment-1"),
|
|
||||||
state: SegmentState::Completed,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: Some(Utc::now()),
|
|
||||||
tokens_used: 1000,
|
|
||||||
tool_calls: 50,
|
|
||||||
errors: 2,
|
|
||||||
current_turn: 5,
|
|
||||||
max_turns: 10,
|
|
||||||
last_message: Some("Done".to_string()),
|
|
||||||
error_message: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
status.update_segment(1, segment1);
|
|
||||||
|
|
||||||
// Serialize to JSON
|
|
||||||
let json = serde_json::to_string(&status).expect("Failed to serialize");
|
|
||||||
assert!(json.contains("test-session"));
|
|
||||||
assert!(json.contains("segment_id"));
|
|
||||||
assert!(json.contains("Completed"));
|
|
||||||
|
|
||||||
// Deserialize back
|
|
||||||
let deserialized: FlockStatus =
|
|
||||||
serde_json::from_str(&json).expect("Failed to deserialize");
|
|
||||||
assert_eq!(deserialized.session_id, "test-session");
|
|
||||||
assert_eq!(deserialized.segments.len(), 1);
|
|
||||||
assert_eq!(deserialized.total_tokens, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_report_generation() {
|
|
||||||
let mut status = FlockStatus::new(
|
|
||||||
"test-session".to_string(),
|
|
||||||
PathBuf::from("/test/project"),
|
|
||||||
PathBuf::from("/test/workspace"),
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
|
|
||||||
let segment1 = SegmentStatus {
|
|
||||||
segment_id: 1,
|
|
||||||
workspace: PathBuf::from("/test/workspace/segment-1"),
|
|
||||||
state: SegmentState::Completed,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
completed_at: Some(Utc::now()),
|
|
||||||
tokens_used: 1000,
|
|
||||||
tool_calls: 50,
|
|
||||||
errors: 2,
|
|
||||||
current_turn: 5,
|
|
||||||
max_turns: 10,
|
|
||||||
last_message: Some("Done".to_string()),
|
|
||||||
error_message: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
status.update_segment(1, segment1);
|
|
||||||
|
|
||||||
let report = status.generate_report();
|
|
||||||
|
|
||||||
// Check that report contains expected sections
|
|
||||||
assert!(report.contains("FLOCK MODE SESSION REPORT"));
|
|
||||||
assert!(report.contains("test-session"));
|
|
||||||
assert!(report.contains("Segment Status:"));
|
|
||||||
assert!(report.contains("Aggregate Metrics:"));
|
|
||||||
assert!(report.contains("Segment Details:"));
|
|
||||||
assert!(report.contains("Total Tokens: 1000"));
|
|
||||||
assert!(report.contains("Total Tool Calls: 50"));
|
|
||||||
assert!(report.contains("Total Errors: 2"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,443 +0,0 @@
|
|||||||
//! Integration tests for g3-ensembles flock mode
|
|
||||||
|
|
||||||
use g3_ensembles::{FlockConfig, FlockMode};
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::process::Command;
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
/// Helper to create a test git repository with flock-requirements.md
|
|
||||||
fn create_test_project(name: &str) -> TempDir {
|
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
||||||
let project_path = temp_dir.path();
|
|
||||||
|
|
||||||
// Initialize git repo
|
|
||||||
let output = Command::new("git")
|
|
||||||
.arg("init")
|
|
||||||
.current_dir(project_path)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to run git init");
|
|
||||||
assert!(output.status.success(), "git init failed");
|
|
||||||
|
|
||||||
// Configure git user (required for commits)
|
|
||||||
Command::new("git")
|
|
||||||
.args(["config", "user.email", "test@example.com"])
|
|
||||||
.current_dir(project_path)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to configure git email");
|
|
||||||
|
|
||||||
Command::new("git")
|
|
||||||
.args(["config", "user.name", "Test User"])
|
|
||||||
.current_dir(project_path)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to configure git name");
|
|
||||||
|
|
||||||
// Create flock-requirements.md
|
|
||||||
let requirements = format!(
|
|
||||||
"# {} Test Project\n\n\
|
|
||||||
## Module A\n\
|
|
||||||
- Create a simple Rust library\n\
|
|
||||||
- Add a function that returns \"Hello from Module A\"\n\
|
|
||||||
- Write a unit test for the function\n\n\
|
|
||||||
## Module B\n\
|
|
||||||
- Create another Rust library\n\
|
|
||||||
- Add a function that returns \"Hello from Module B\"\n\
|
|
||||||
- Write a unit test for the function\n",
|
|
||||||
name
|
|
||||||
);
|
|
||||||
|
|
||||||
fs::write(project_path.join("flock-requirements.md"), requirements)
|
|
||||||
.expect("Failed to write requirements");
|
|
||||||
|
|
||||||
// Create a simple README
|
|
||||||
fs::write(project_path.join("README.md"), format!("# {}\n", name))
|
|
||||||
.expect("Failed to write README");
|
|
||||||
|
|
||||||
// Create initial commit
|
|
||||||
Command::new("git")
|
|
||||||
.args(["add", "."])
|
|
||||||
.current_dir(project_path)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to git add");
|
|
||||||
|
|
||||||
let output = Command::new("git")
|
|
||||||
.args(["commit", "-m", "Initial commit"])
|
|
||||||
.current_dir(project_path)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to git commit");
|
|
||||||
assert!(output.status.success(), "git commit failed");
|
|
||||||
|
|
||||||
temp_dir
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_flock_config_validation() {
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let project_path = temp_dir.path().to_path_buf();
|
|
||||||
let workspace_path = temp_dir.path().join("workspace");
|
|
||||||
|
|
||||||
// Should fail - not a git repo
|
|
||||||
let result = FlockConfig::new(project_path.clone(), workspace_path.clone(), 2);
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result
|
|
||||||
.unwrap_err()
|
|
||||||
.to_string()
|
|
||||||
.contains("must be a git repository"));
|
|
||||||
|
|
||||||
// Initialize git repo
|
|
||||||
Command::new("git")
|
|
||||||
.arg("init")
|
|
||||||
.current_dir(&project_path)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to run git init");
|
|
||||||
|
|
||||||
// Should fail - no flock-requirements.md
|
|
||||||
let result = FlockConfig::new(project_path.clone(), workspace_path.clone(), 2);
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result
|
|
||||||
.unwrap_err()
|
|
||||||
.to_string()
|
|
||||||
.contains("flock-requirements.md"));
|
|
||||||
|
|
||||||
// Create flock-requirements.md
|
|
||||||
fs::write(project_path.join("flock-requirements.md"), "# Test\n")
|
|
||||||
.expect("Failed to write requirements");
|
|
||||||
|
|
||||||
// Should succeed now
|
|
||||||
let result = FlockConfig::new(project_path, workspace_path, 2);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_flock_config_builder() {
|
|
||||||
let project_dir = create_test_project("builder-test");
|
|
||||||
let workspace_dir = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
let config = FlockConfig::new(
|
|
||||||
project_dir.path().to_path_buf(),
|
|
||||||
workspace_dir.path().to_path_buf(),
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
.expect("Failed to create config")
|
|
||||||
.with_max_turns(15)
|
|
||||||
.with_g3_binary(PathBuf::from("/custom/g3"));
|
|
||||||
|
|
||||||
assert_eq!(config.num_segments, 2);
|
|
||||||
assert_eq!(config.max_turns, 15);
|
|
||||||
assert_eq!(config.g3_binary, Some(PathBuf::from("/custom/g3")));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_workspace_creation() {
|
|
||||||
let project_dir = create_test_project("workspace-test");
|
|
||||||
let workspace_dir = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
let config = FlockConfig::new(
|
|
||||||
project_dir.path().to_path_buf(),
|
|
||||||
workspace_dir.path().to_path_buf(),
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
.expect("Failed to create config");
|
|
||||||
|
|
||||||
// Create FlockMode instance
|
|
||||||
let _flock = FlockMode::new(config).expect("Failed to create FlockMode");
|
|
||||||
|
|
||||||
// Verify workspace directory structure will be created
|
|
||||||
// (We can't run the full flock without LLM access, but we can test the setup)
|
|
||||||
assert!(project_dir.path().join(".git").exists());
|
|
||||||
assert!(project_dir.path().join("flock-requirements.md").exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_git_clone_functionality() {
|
|
||||||
let project_dir = create_test_project("clone-test");
|
|
||||||
let workspace_dir = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
// Manually test git cloning (what flock mode does internally)
|
|
||||||
let segment_dir = workspace_dir.path().join("segment-1");
|
|
||||||
|
|
||||||
let output = Command::new("git")
|
|
||||||
.arg("clone")
|
|
||||||
.arg(project_dir.path())
|
|
||||||
.arg(&segment_dir)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to run git clone");
|
|
||||||
|
|
||||||
assert!(output.status.success(), "git clone failed: {:?}", output);
|
|
||||||
|
|
||||||
// Verify the clone
|
|
||||||
assert!(segment_dir.exists());
|
|
||||||
assert!(segment_dir.join(".git").exists());
|
|
||||||
assert!(segment_dir.join("flock-requirements.md").exists());
|
|
||||||
assert!(segment_dir.join("README.md").exists());
|
|
||||||
|
|
||||||
// Verify it's a proper git repo
|
|
||||||
let output = Command::new("git")
|
|
||||||
.args(["log", "--oneline"])
|
|
||||||
.current_dir(&segment_dir)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to run git log");
|
|
||||||
|
|
||||||
assert!(output.status.success());
|
|
||||||
let log = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(log.contains("Initial commit"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multiple_segment_clones() {
|
|
||||||
let project_dir = create_test_project("multi-clone-test");
|
|
||||||
let workspace_dir = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
// Clone multiple segments
|
|
||||||
for i in 1..=2 {
|
|
||||||
let segment_dir = workspace_dir.path().join(format!("segment-{}", i));
|
|
||||||
|
|
||||||
let output = Command::new("git")
|
|
||||||
.arg("clone")
|
|
||||||
.arg(project_dir.path())
|
|
||||||
.arg(&segment_dir)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to run git clone");
|
|
||||||
|
|
||||||
assert!(output.status.success(), "git clone {} failed", i);
|
|
||||||
assert!(segment_dir.exists());
|
|
||||||
assert!(segment_dir.join(".git").exists());
|
|
||||||
assert!(segment_dir.join("flock-requirements.md").exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify both segments exist and are independent
|
|
||||||
let segment1 = workspace_dir.path().join("segment-1");
|
|
||||||
let segment2 = workspace_dir.path().join("segment-2");
|
|
||||||
|
|
||||||
assert!(segment1.exists());
|
|
||||||
assert!(segment2.exists());
|
|
||||||
|
|
||||||
// Modify segment 1
|
|
||||||
fs::write(segment1.join("test.txt"), "segment 1")
|
|
||||||
.expect("Failed to write to segment 1");
|
|
||||||
|
|
||||||
// Verify segment 2 is unaffected
|
|
||||||
assert!(!segment2.join("test.txt").exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_segment_requirements_creation() {
|
|
||||||
let project_dir = create_test_project("segment-req-test");
|
|
||||||
let workspace_dir = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
// Clone a segment
|
|
||||||
let segment_dir = workspace_dir.path().join("segment-1");
|
|
||||||
Command::new("git")
|
|
||||||
.arg("clone")
|
|
||||||
.arg(project_dir.path())
|
|
||||||
.arg(&segment_dir)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to clone");
|
|
||||||
|
|
||||||
// Create segment-requirements.md (what flock mode does)
|
|
||||||
let segment_requirements = "# Module A\n\nImplement module A functionality\n";
|
|
||||||
fs::write(segment_dir.join("segment-requirements.md"), segment_requirements)
|
|
||||||
.expect("Failed to write segment requirements");
|
|
||||||
|
|
||||||
// Verify it was created
|
|
||||||
assert!(segment_dir.join("segment-requirements.md").exists());
|
|
||||||
let content = fs::read_to_string(segment_dir.join("segment-requirements.md"))
|
|
||||||
.expect("Failed to read segment requirements");
|
|
||||||
assert!(content.contains("Module A"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_status_file_operations() {
|
|
||||||
use g3_ensembles::FlockStatus;
|
|
||||||
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let status_file = temp_dir.path().join("flock-status.json");
|
|
||||||
|
|
||||||
// Create a status
|
|
||||||
let status = FlockStatus::new(
|
|
||||||
"test-session".to_string(),
|
|
||||||
PathBuf::from("/test/project"),
|
|
||||||
PathBuf::from("/test/workspace"),
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save to file
|
|
||||||
status
|
|
||||||
.save_to_file(&status_file)
|
|
||||||
.expect("Failed to save status");
|
|
||||||
|
|
||||||
// Verify file exists
|
|
||||||
assert!(status_file.exists());
|
|
||||||
|
|
||||||
// Load from file
|
|
||||||
let loaded = FlockStatus::load_from_file(&status_file).expect("Failed to load status");
|
|
||||||
|
|
||||||
assert_eq!(loaded.session_id, "test-session");
|
|
||||||
assert_eq!(loaded.num_segments, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_json_extraction() {
|
|
||||||
// Test the JSON extraction logic used in partition_requirements
|
|
||||||
let test_cases = vec![
|
|
||||||
(
|
|
||||||
"Here is the result: [{\"module_name\": \"test\"}]",
|
|
||||||
Some("[{\"module_name\": \"test\"}]"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"```json\n[{\"module_name\": \"test\"}]\n```",
|
|
||||||
Some("[{\"module_name\": \"test\"}]"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Some text before\n[{\"a\": 1}, {\"b\": 2}]\nSome text after",
|
|
||||||
Some("[{\"a\": 1}, {\"b\": 2}]"),
|
|
||||||
),
|
|
||||||
("No JSON here", None),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (input, expected) in test_cases {
|
|
||||||
let result = extract_json_array(input);
|
|
||||||
match expected {
|
|
||||||
Some(exp) => {
|
|
||||||
assert!(result.is_some(), "Failed to extract from: {}", input);
|
|
||||||
assert_eq!(result.unwrap(), exp);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
assert!(result.is_none(), "Should not extract from: {}", input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to extract JSON array (mimics the logic in flock.rs)
|
|
||||||
fn extract_json_array(output: &str) -> Option<String> {
|
|
||||||
if let Some(start) = output.find('[') {
|
|
||||||
if let Some(end) = output.rfind(']') {
|
|
||||||
if end > start {
|
|
||||||
return Some(output[start..=end].to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_partition_json_parsing() {
|
|
||||||
// Test parsing of partition JSON
|
|
||||||
let json = r#"[
|
|
||||||
{
|
|
||||||
"module_name": "core-library",
|
|
||||||
"requirements": "Build the core library with basic functionality",
|
|
||||||
"dependencies": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"module_name": "cli-tool",
|
|
||||||
"requirements": "Create a CLI tool that uses the core library",
|
|
||||||
"dependencies": ["core-library"]
|
|
||||||
}
|
|
||||||
]"#;
|
|
||||||
|
|
||||||
let partitions: Vec<serde_json::Value> =
|
|
||||||
serde_json::from_str(json).expect("Failed to parse JSON");
|
|
||||||
|
|
||||||
assert_eq!(partitions.len(), 2);
|
|
||||||
assert_eq!(partitions[0]["module_name"], "core-library");
|
|
||||||
assert_eq!(partitions[1]["module_name"], "cli-tool");
|
|
||||||
assert_eq!(partitions[1]["dependencies"][0], "core-library");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_requirements_file_content() {
|
|
||||||
let project_dir = create_test_project("content-test");
|
|
||||||
|
|
||||||
let requirements_path = project_dir.path().join("flock-requirements.md");
|
|
||||||
let content = fs::read_to_string(&requirements_path).expect("Failed to read requirements");
|
|
||||||
|
|
||||||
// Verify content structure
|
|
||||||
assert!(content.contains("# content-test Test Project"));
|
|
||||||
assert!(content.contains("## Module A"));
|
|
||||||
assert!(content.contains("## Module B"));
|
|
||||||
assert!(content.contains("Hello from Module A"));
|
|
||||||
assert!(content.contains("Hello from Module B"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_git_repo_independence() {
|
|
||||||
let project_dir = create_test_project("independence-test");
|
|
||||||
let workspace_dir = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
// Clone two segments
|
|
||||||
let segment1 = workspace_dir.path().join("segment-1");
|
|
||||||
let segment2 = workspace_dir.path().join("segment-2");
|
|
||||||
|
|
||||||
Command::new("git")
|
|
||||||
.arg("clone")
|
|
||||||
.arg(project_dir.path())
|
|
||||||
.arg(&segment1)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to clone segment 1");
|
|
||||||
|
|
||||||
Command::new("git")
|
|
||||||
.arg("clone")
|
|
||||||
.arg(project_dir.path())
|
|
||||||
.arg(&segment2)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to clone segment 2");
|
|
||||||
|
|
||||||
// Make a commit in segment 1
|
|
||||||
fs::write(segment1.join("file1.txt"), "content 1").expect("Failed to write file1");
|
|
||||||
|
|
||||||
Command::new("git")
|
|
||||||
.args(["add", "file1.txt"])
|
|
||||||
.current_dir(&segment1)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to git add");
|
|
||||||
|
|
||||||
Command::new("git")
|
|
||||||
.args(["commit", "-m", "Add file1"])
|
|
||||||
.current_dir(&segment1)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to commit in segment 1");
|
|
||||||
|
|
||||||
// Make a different commit in segment 2
|
|
||||||
fs::write(segment2.join("file2.txt"), "content 2").expect("Failed to write file2");
|
|
||||||
|
|
||||||
Command::new("git")
|
|
||||||
.args(["add", "file2.txt"])
|
|
||||||
.current_dir(&segment2)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to git add");
|
|
||||||
|
|
||||||
Command::new("git")
|
|
||||||
.args(["commit", "-m", "Add file2"])
|
|
||||||
.current_dir(&segment2)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to commit in segment 2");
|
|
||||||
|
|
||||||
// Verify they have different commits
|
|
||||||
let log1 = Command::new("git")
|
|
||||||
.args(["log", "--oneline"])
|
|
||||||
.current_dir(&segment1)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to get log 1");
|
|
||||||
|
|
||||||
let log2 = Command::new("git")
|
|
||||||
.args(["log", "--oneline"])
|
|
||||||
.current_dir(&segment2)
|
|
||||||
.output()
|
|
||||||
.expect("Failed to get log 2");
|
|
||||||
|
|
||||||
let log1_str = String::from_utf8_lossy(&log1.stdout);
|
|
||||||
let log2_str = String::from_utf8_lossy(&log2.stdout);
|
|
||||||
|
|
||||||
assert!(log1_str.contains("Add file1"));
|
|
||||||
assert!(!log1_str.contains("Add file2"));
|
|
||||||
assert!(log2_str.contains("Add file2"));
|
|
||||||
assert!(!log2_str.contains("Add file1"));
|
|
||||||
|
|
||||||
// Verify files exist only in their respective segments
|
|
||||||
assert!(segment1.join("file1.txt").exists());
|
|
||||||
assert!(!segment1.join("file2.txt").exists());
|
|
||||||
assert!(segment2.join("file2.txt").exists());
|
|
||||||
assert!(!segment2.join("file1.txt").exists());
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,6 @@ pub struct OpenAIProvider {
|
|||||||
base_url: String,
|
base_url: String,
|
||||||
max_tokens: Option<u32>,
|
max_tokens: Option<u32>,
|
||||||
_temperature: Option<f32>,
|
_temperature: Option<f32>,
|
||||||
name: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenAIProvider {
|
impl OpenAIProvider {
|
||||||
@@ -32,24 +31,6 @@ impl OpenAIProvider {
|
|||||||
base_url: Option<String>,
|
base_url: Option<String>,
|
||||||
max_tokens: Option<u32>,
|
max_tokens: Option<u32>,
|
||||||
temperature: Option<f32>,
|
temperature: Option<f32>,
|
||||||
) -> Result<Self> {
|
|
||||||
Self::new_with_name(
|
|
||||||
"openai".to_string(),
|
|
||||||
api_key,
|
|
||||||
model,
|
|
||||||
base_url,
|
|
||||||
max_tokens,
|
|
||||||
temperature,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_with_name(
|
|
||||||
name: String,
|
|
||||||
api_key: String,
|
|
||||||
model: Option<String>,
|
|
||||||
base_url: Option<String>,
|
|
||||||
max_tokens: Option<u32>,
|
|
||||||
temperature: Option<f32>,
|
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
@@ -58,7 +39,6 @@ impl OpenAIProvider {
|
|||||||
base_url: base_url.unwrap_or_else(|| "https://api.openai.com/v1".to_string()),
|
base_url: base_url.unwrap_or_else(|| "https://api.openai.com/v1".to_string()),
|
||||||
max_tokens,
|
max_tokens,
|
||||||
_temperature: temperature,
|
_temperature: temperature,
|
||||||
name,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +353,7 @@ impl LLMProvider for OpenAIProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
&self.name
|
"openai"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn model(&self) -> &str {
|
fn model(&self) -> &str {
|
||||||
|
|||||||
210
docs/REQUIREMENTS_PERSISTENCE.md
Normal file
210
docs/REQUIREMENTS_PERSISTENCE.md
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
# Requirements Persistence in Accumulative Mode
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
In accumulative autonomous mode (`--auto` or default mode), G3 now automatically persists your requirements to a local `.g3/requirements.md` file. This provides several benefits:
|
||||||
|
|
||||||
|
1. **Persistence across sessions**: Your requirements are saved and can be resumed later
|
||||||
|
2. **Version control friendly**: Requirements are stored in a readable markdown format
|
||||||
|
3. **Easy review**: You can view and edit requirements directly in the file
|
||||||
|
4. **Transparency**: Always know what G3 is working on
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Automatic Saving
|
||||||
|
|
||||||
|
When you run G3 in accumulative mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
g3
|
||||||
|
```
|
||||||
|
|
||||||
|
Each requirement you enter is automatically:
|
||||||
|
1. Added to the accumulated requirements list
|
||||||
|
2. Saved to `.g3/requirements.md` in your workspace
|
||||||
|
3. Used for the autonomous implementation run
|
||||||
|
|
||||||
|
### File Format
|
||||||
|
|
||||||
|
The `.g3/requirements.md` file uses a simple numbered list format:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Project Requirements
|
||||||
|
|
||||||
|
1. Create a simple web server in Python with Flask
|
||||||
|
2. Add a /health endpoint that returns JSON
|
||||||
|
3. Add logging for all requests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loading Existing Requirements
|
||||||
|
|
||||||
|
When you start G3 in a directory that already has a `.g3/requirements.md` file, it will:
|
||||||
|
|
||||||
|
1. Automatically load the existing requirements
|
||||||
|
2. Display them on startup
|
||||||
|
3. Continue numbering from where you left off
|
||||||
|
|
||||||
|
Example output:
|
||||||
|
|
||||||
|
```
|
||||||
|
📂 Loaded 3 existing requirement(s) from .g3/requirements.md
|
||||||
|
|
||||||
|
1. Create a simple web server in Python with Flask
|
||||||
|
2. Add a /health endpoint that returns JSON
|
||||||
|
3. Add logging for all requests
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
📝 Turn 4 - What's next? (add more requirements or refinements)
|
||||||
|
============================================================
|
||||||
|
requirement>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### View Requirements
|
||||||
|
|
||||||
|
Use the `/requirements` command to view all accumulated requirements:
|
||||||
|
|
||||||
|
```
|
||||||
|
requirement> /requirements
|
||||||
|
|
||||||
|
📋 Accumulated Requirements (saved to .g3/requirements.md):
|
||||||
|
|
||||||
|
1. Create a simple web server in Python with Flask
|
||||||
|
2. Add a /health endpoint that returns JSON
|
||||||
|
3. Add logging for all requests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Commands
|
||||||
|
|
||||||
|
- `/help` - Show all available commands
|
||||||
|
- `/chat` - Switch to interactive chat mode (preserves requirements context)
|
||||||
|
- `exit` or `quit` - Exit the session
|
||||||
|
|
||||||
|
## File Location
|
||||||
|
|
||||||
|
The requirements file is stored at:
|
||||||
|
|
||||||
|
```
|
||||||
|
<workspace>/.g3/requirements.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `<workspace>` is your current working directory.
|
||||||
|
|
||||||
|
## Version Control
|
||||||
|
|
||||||
|
The `.g3/` directory is automatically added to `.gitignore`, so your requirements won't be committed to version control by default. If you want to track requirements in git, you can:
|
||||||
|
|
||||||
|
1. Remove `.g3/` from `.gitignore`
|
||||||
|
2. Commit the `.g3/requirements.md` file
|
||||||
|
|
||||||
|
This can be useful for:
|
||||||
|
- Sharing requirements with team members
|
||||||
|
- Tracking requirement evolution over time
|
||||||
|
- Documenting project goals
|
||||||
|
|
||||||
|
## Manual Editing
|
||||||
|
|
||||||
|
You can manually edit `.g3/requirements.md` if needed. G3 will parse the file and load any numbered requirements (format: `1. requirement text`).
|
||||||
|
|
||||||
|
**Note**: Make sure to maintain the numbered list format for proper parsing.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
If G3 cannot save or load requirements, it will:
|
||||||
|
|
||||||
|
1. Display a warning message
|
||||||
|
2. Continue operating with in-memory requirements
|
||||||
|
3. Not interrupt your workflow
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ Warning: Could not save requirements to .g3/requirements.md: Permission denied
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### Resuming Work
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Day 1: Start a project
|
||||||
|
cd my-project
|
||||||
|
g3
|
||||||
|
requirement> Create a REST API with user authentication
|
||||||
|
# ... work happens ...
|
||||||
|
exit
|
||||||
|
|
||||||
|
# Day 2: Resume work
|
||||||
|
cd my-project
|
||||||
|
g3
|
||||||
|
# G3 automatically loads previous requirements
|
||||||
|
requirement> Add password reset functionality
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reviewing Progress
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check what you've asked G3 to build
|
||||||
|
cat .g3/requirements.md
|
||||||
|
|
||||||
|
# Or use the command within G3
|
||||||
|
requirement> /requirements
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sharing Requirements
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Share requirements with a team member
|
||||||
|
cp .g3/requirements.md requirements-backup.md
|
||||||
|
# Or commit to version control
|
||||||
|
git add .g3/requirements.md
|
||||||
|
git commit -m "Add project requirements"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
|
||||||
|
- `ensure_g3_dir()` - Creates `.g3` directory if it doesn't exist
|
||||||
|
- `load_existing_requirements()` - Loads requirements from `.g3/requirements.md`
|
||||||
|
- `save_requirements()` - Saves requirements to `.g3/requirements.md`
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
my-project/
|
||||||
|
├── .g3/
|
||||||
|
│ └── requirements.md # Accumulated requirements
|
||||||
|
├── logs/ # Session logs (existing)
|
||||||
|
└── ... (your project files)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
1. **No data loss**: Requirements are persisted even if G3 crashes or is interrupted
|
||||||
|
2. **Transparency**: Always know what G3 is working on
|
||||||
|
3. **Resumability**: Pick up where you left off in any session
|
||||||
|
4. **Documentation**: Requirements serve as project documentation
|
||||||
|
5. **Collaboration**: Share requirements with team members
|
||||||
|
6. **Auditability**: Track what was requested and when
|
||||||
|
|
||||||
|
## Comparison with Traditional Autonomous Mode
|
||||||
|
|
||||||
|
| Feature | Accumulative Mode | Traditional `--autonomous` |
|
||||||
|
|---------|------------------|---------------------------|
|
||||||
|
| Requirements file | `.g3/requirements.md` | `requirements.md` (root) |
|
||||||
|
| Auto-save | ✅ Yes | ❌ No (manual edit) |
|
||||||
|
| Interactive | ✅ Yes | ❌ No |
|
||||||
|
| Incremental | ✅ Yes | ❌ No (one-shot) |
|
||||||
|
| Resume support | ✅ Yes | ⚠️ Manual |
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential future improvements:
|
||||||
|
|
||||||
|
- Requirement status tracking (pending, in-progress, completed)
|
||||||
|
- Requirement dependencies and ordering
|
||||||
|
- Requirement templates and snippets
|
||||||
|
- Integration with issue trackers
|
||||||
|
- Requirement validation and linting
|
||||||
39
test-ai-requirements.sh
Executable file
39
test-ai-requirements.sh
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test script for AI-enhanced interactive requirements mode
|
||||||
|
|
||||||
|
echo "Testing AI-enhanced interactive requirements mode..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create a test workspace
|
||||||
|
TEST_WORKSPACE="/tmp/g3-test-interactive-$(date +%s)"
|
||||||
|
mkdir -p "$TEST_WORKSPACE"
|
||||||
|
|
||||||
|
echo "Test workspace: $TEST_WORKSPACE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create sample brief input
|
||||||
|
BRIEF_INPUT="build a calculator cli in rust with basic operations"
|
||||||
|
|
||||||
|
echo "Brief input:"
|
||||||
|
echo "---"
|
||||||
|
echo "$BRIEF_INPUT"
|
||||||
|
echo "---"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "This will:"
|
||||||
|
echo "1. Send brief input to AI"
|
||||||
|
echo "2. AI generates structured requirements.md"
|
||||||
|
echo "3. Show enhanced requirements"
|
||||||
|
echo "4. Prompt for confirmation (y/e/n)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "To test manually, run:"
|
||||||
|
echo "cargo run -- --autonomous --interactive-requirements --workspace $TEST_WORKSPACE"
|
||||||
|
echo ""
|
||||||
|
echo "Then type: $BRIEF_INPUT"
|
||||||
|
echo "Press Ctrl+D"
|
||||||
|
echo "Review the AI-generated requirements"
|
||||||
|
echo "Choose 'y' to proceed, 'e' to edit, or 'n' to cancel"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Test workspace will be at: $TEST_WORKSPACE"
|
||||||
70
test_anthropic_fix.md
Normal file
70
test_anthropic_fix.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Anthropic max_tokens Error Fix - Test Plan
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. Fixed Context Window Size Detection
|
||||||
|
- **Problem**: Code used hardcoded 200k limit for Anthropic instead of configured max_tokens
|
||||||
|
- **Fix**: Modified `determine_context_length()` to check configured max_tokens first before falling back to defaults
|
||||||
|
- **Files**: `crates/g3-core/src/lib.rs` lines 923-945, 967-985
|
||||||
|
|
||||||
|
### 2. Added Thinning Before Summarization
|
||||||
|
- **Problem**: Code attempted summarization even when context window was nearly full
|
||||||
|
- **Fix**: Added logic to try thinning first when context usage is between 80-90%
|
||||||
|
- **Files**: `crates/g3-core/src/lib.rs` lines 2415-2439
|
||||||
|
|
||||||
|
### 3. Added Capacity Checks Before Summarization
|
||||||
|
- **Problem**: No validation that sufficient tokens remained for summarization
|
||||||
|
- **Fix**: Added capacity checks for all provider types with helpful error messages
|
||||||
|
- **Files**: `crates/g3-core/src/lib.rs` lines 2480-2520
|
||||||
|
|
||||||
|
### 4. Improved Error Messages
|
||||||
|
- **Problem**: Generic errors when summarization failed
|
||||||
|
- **Fix**: Specific error messages suggesting `/thinnify` and `/compact` commands
|
||||||
|
- **Files**: Multiple locations in summarization logic
|
||||||
|
|
||||||
|
### 5. Dynamic Buffer Calculation
|
||||||
|
- **Problem**: Fixed 5k buffer regardless of model size
|
||||||
|
- **Fix**: Proportional buffer (2.5% of model limit, min 1k, max 10k)
|
||||||
|
- **Files**: `crates/g3-core/src/lib.rs` line 2487
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
### Test 1: Configured max_tokens Respected
|
||||||
|
```toml
|
||||||
|
# In g3.toml
|
||||||
|
[providers.anthropic]
|
||||||
|
api_key = "your-key"
|
||||||
|
model = "claude-3-5-sonnet-20241022"
|
||||||
|
max_tokens = 50000 # Should use this instead of 200k default
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2: Thinning Before Summarization
|
||||||
|
- Fill context to 85% capacity
|
||||||
|
- Verify thinning is attempted before summarization
|
||||||
|
- Check that summarization is skipped if thinning resolves the issue
|
||||||
|
|
||||||
|
### Test 3: Capacity Error Handling
|
||||||
|
- Fill context to 98% capacity
|
||||||
|
- Verify helpful error message is shown instead of API error
|
||||||
|
- Check that `/thinnify` and `/compact` commands are suggested
|
||||||
|
|
||||||
|
### Test 4: Provider-Specific Handling
|
||||||
|
- Test with different providers (anthropic, databricks, embedded)
|
||||||
|
- Verify each uses appropriate capacity checks and buffers
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
1. **No more max_tokens API errors** from Anthropic when context window is full
|
||||||
|
2. **Automatic thinning** when approaching capacity (80-90%)
|
||||||
|
3. **Clear error messages** with actionable suggestions when at capacity
|
||||||
|
4. **Respect configured limits** instead of hardcoded defaults
|
||||||
|
5. **Graceful degradation** with helpful user guidance
|
||||||
|
|
||||||
|
## Manual Testing Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test with small max_tokens to trigger the issue quickly
|
||||||
|
g3 --chat
|
||||||
|
# Then paste large amounts of text to fill context window
|
||||||
|
# Verify thinning and error handling work correctly
|
||||||
|
```
|
||||||
36
test_requirements.sh
Executable file
36
test_requirements.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Test script for .g3/requirements.md feature
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Testing .g3/requirements.md feature..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create a test directory
|
||||||
|
TEST_DIR="/tmp/g3_test_$$"
|
||||||
|
mkdir -p "$TEST_DIR"
|
||||||
|
cd "$TEST_DIR"
|
||||||
|
|
||||||
|
echo "Test directory: $TEST_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create a simple test by simulating user input
|
||||||
|
echo "Testing requirement persistence..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if .g3 directory gets created
|
||||||
|
if [ ! -d ".g3" ]; then
|
||||||
|
echo "✅ .g3 directory does not exist yet (expected)"
|
||||||
|
else
|
||||||
|
echo "❌ .g3 directory already exists (unexpected)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Test directory created at: $TEST_DIR"
|
||||||
|
echo "You can manually test by running:"
|
||||||
|
echo " cd $TEST_DIR"
|
||||||
|
echo " g3"
|
||||||
|
echo ""
|
||||||
|
echo "Then enter a requirement and check if .g3/requirements.md is created."
|
||||||
|
echo ""
|
||||||
Reference in New Issue
Block a user