Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:28:42 +08:00
commit 8a4be47b6e
43 changed files with 10867 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
#!/usr/bin/env -S uv run
# /// script
# dependencies = ["python-dotenv", "pydantic"]
# ///
"""
ADW Plan Build Test Review Iso - Compositional workflow for isolated planning, building, testing, and reviewing
Usage: uv run adw_plan_build_test_review_iso.py <issue-number> [adw-id] [--skip-e2e] [--skip-resolution]
This script runs:
1. adw_plan_iso.py - Planning phase (isolated)
2. adw_build_iso.py - Implementation phase (isolated)
3. adw_test_iso.py - Testing phase (isolated)
4. adw_review_iso.py - Review phase (isolated)
The scripts are chained together via persistent state (adw_state.json).
"""
import subprocess
import sys
import os
# Add the parent directory to Python path to import modules
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from adw_modules.workflow_ops import ensure_adw_id
def main():
"""Main entry point."""
# Check for flags
skip_e2e = "--skip-e2e" in sys.argv
skip_resolution = "--skip-resolution" in sys.argv
# Remove flags from argv
if skip_e2e:
sys.argv.remove("--skip-e2e")
if skip_resolution:
sys.argv.remove("--skip-resolution")
if len(sys.argv) < 2:
print("Usage: uv run adw_plan_build_test_review_iso.py <issue-number> [adw-id] [--skip-e2e] [--skip-resolution]")
print("\nThis runs the isolated plan, build, test, and review workflow:")
print(" 1. Plan (isolated)")
print(" 2. Build (isolated)")
print(" 3. Test (isolated)")
print(" 4. Review (isolated)")
sys.exit(1)
issue_number = sys.argv[1]
adw_id = sys.argv[2] if len(sys.argv) > 2 else None
# Ensure ADW ID exists with initialized state
adw_id = ensure_adw_id(issue_number, adw_id)
print(f"Using ADW ID: {adw_id}")
# Get the directory where this script is located
script_dir = os.path.dirname(os.path.abspath(__file__))
# Run isolated plan with the ADW ID
plan_cmd = [
"uv",
"run",
os.path.join(script_dir, "adw_plan_iso.py"),
issue_number,
adw_id,
]
print(f"\n=== ISOLATED PLAN PHASE ===")
print(f"Running: {' '.join(plan_cmd)}")
plan = subprocess.run(plan_cmd)
if plan.returncode != 0:
print("Isolated plan phase failed")
sys.exit(1)
# Run isolated build with the ADW ID
build_cmd = [
"uv",
"run",
os.path.join(script_dir, "adw_build_iso.py"),
issue_number,
adw_id,
]
print(f"\n=== ISOLATED BUILD PHASE ===")
print(f"Running: {' '.join(build_cmd)}")
build = subprocess.run(build_cmd)
if build.returncode != 0:
print("Isolated build phase failed")
sys.exit(1)
# Run isolated test with the ADW ID
test_cmd = [
"uv",
"run",
os.path.join(script_dir, "adw_test_iso.py"),
issue_number,
adw_id,
]
if skip_e2e:
test_cmd.append("--skip-e2e")
print(f"\n=== ISOLATED TEST PHASE ===")
print(f"Running: {' '.join(test_cmd)}")
test = subprocess.run(test_cmd)
if test.returncode != 0:
print("Isolated test phase failed")
sys.exit(1)
# Run isolated review with the ADW ID
review_cmd = [
"uv",
"run",
os.path.join(script_dir, "adw_review_iso.py"),
issue_number,
adw_id,
]
if skip_resolution:
review_cmd.append("--skip-resolution")
print(f"\n=== ISOLATED REVIEW PHASE ===")
print(f"Running: {' '.join(review_cmd)}")
review = subprocess.run(review_cmd)
if review.returncode != 0:
print("Isolated review phase failed")
sys.exit(1)
print(f"\n=== ISOLATED WORKFLOW COMPLETED ===")
print(f"ADW ID: {adw_id}")
print(f"All phases completed successfully!")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,152 @@
#!/usr/bin/env -S uv run
# /// script
# dependencies = ["python-dotenv", "pydantic"]
# ///
"""
ADW SDLC Iso - Complete Software Development Life Cycle workflow with isolation
Usage: uv run adw_sdlc_iso.py <issue-number> [adw-id] [--skip-e2e] [--skip-resolution]
This script runs the complete ADW SDLC pipeline in isolation:
1. adw_plan_iso.py - Planning phase (isolated)
2. adw_build_iso.py - Implementation phase (isolated)
3. adw_test_iso.py - Testing phase (isolated)
4. adw_review_iso.py - Review phase (isolated)
5. adw_document_iso.py - Documentation phase (isolated)
The scripts are chained together via persistent state (adw_state.json).
Each phase runs in its own git worktree with dedicated ports.
"""
import subprocess
import sys
import os
# Add the parent directory to Python path to import modules
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from adw_modules.workflow_ops import ensure_adw_id
def main():
"""Main entry point."""
# Check for flags
skip_e2e = "--skip-e2e" in sys.argv
skip_resolution = "--skip-resolution" in sys.argv
# Remove flags from argv
if skip_e2e:
sys.argv.remove("--skip-e2e")
if skip_resolution:
sys.argv.remove("--skip-resolution")
if len(sys.argv) < 2:
print("Usage: uv run adw_sdlc_iso.py <issue-number> [adw-id] [--skip-e2e] [--skip-resolution]")
print("\nThis runs the complete isolated Software Development Life Cycle:")
print(" 1. Plan (isolated)")
print(" 2. Build (isolated)")
print(" 3. Test (isolated)")
print(" 4. Review (isolated)")
print(" 5. Document (isolated)")
sys.exit(1)
issue_number = sys.argv[1]
adw_id = sys.argv[2] if len(sys.argv) > 2 else None
# Ensure ADW ID exists with initialized state
adw_id = ensure_adw_id(issue_number, adw_id)
print(f"Using ADW ID: {adw_id}")
# Get the directory where this script is located
script_dir = os.path.dirname(os.path.abspath(__file__))
# Run isolated plan with the ADW ID
plan_cmd = [
"uv",
"run",
os.path.join(script_dir, "adw_plan_iso.py"),
issue_number,
adw_id,
]
print(f"\n=== ISOLATED PLAN PHASE ===")
print(f"Running: {' '.join(plan_cmd)}")
plan = subprocess.run(plan_cmd)
if plan.returncode != 0:
print("Isolated plan phase failed")
sys.exit(1)
# Run isolated build with the ADW ID
build_cmd = [
"uv",
"run",
os.path.join(script_dir, "adw_build_iso.py"),
issue_number,
adw_id,
]
print(f"\n=== ISOLATED BUILD PHASE ===")
print(f"Running: {' '.join(build_cmd)}")
build = subprocess.run(build_cmd)
if build.returncode != 0:
print("Isolated build phase failed")
sys.exit(1)
# Run isolated test with the ADW ID
test_cmd = [
"uv",
"run",
os.path.join(script_dir, "adw_test_iso.py"),
issue_number,
adw_id,
"--skip-e2e", # Always skip E2E tests in SDLC workflows
]
print(f"\n=== ISOLATED TEST PHASE ===")
print(f"Running: {' '.join(test_cmd)}")
test = subprocess.run(test_cmd)
if test.returncode != 0:
print("Isolated test phase failed")
# Note: Continue anyway as some tests might be flaky
print("WARNING: Test phase failed but continuing with review")
# Run isolated review with the ADW ID
review_cmd = [
"uv",
"run",
os.path.join(script_dir, "adw_review_iso.py"),
issue_number,
adw_id,
]
if skip_resolution:
review_cmd.append("--skip-resolution")
print(f"\n=== ISOLATED REVIEW PHASE ===")
print(f"Running: {' '.join(review_cmd)}")
review = subprocess.run(review_cmd)
if review.returncode != 0:
print("Isolated review phase failed")
sys.exit(1)
# Run isolated documentation with the ADW ID
document_cmd = [
"uv",
"run",
os.path.join(script_dir, "adw_document_iso.py"),
issue_number,
adw_id,
]
print(f"\n=== ISOLATED DOCUMENTATION PHASE ===")
print(f"Running: {' '.join(document_cmd)}")
document = subprocess.run(document_cmd)
if document.returncode != 0:
print("Isolated documentation phase failed")
sys.exit(1)
print(f"\n=== ISOLATED SDLC COMPLETED ===")
print(f"ADW ID: {adw_id}")
print(f"All phases completed successfully!")
print(f"\nWorktree location: trees/{adw_id}/")
print(f"To clean up: ./scripts/purge_tree.sh {adw_id}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,340 @@
#!/usr/bin/env -S uv run
# /// script
# dependencies = ["python-dotenv", "pydantic"]
# ///
"""
ADW Ship Iso - AI Developer Workflow for shipping (merging) to main
Usage:
uv run adw_ship_iso.py <issue-number> <adw-id>
Workflow:
1. Load state and validate worktree exists
2. Validate ALL state fields are populated (not None)
3. Perform manual git merge in main repository:
- Fetch latest from origin
- Checkout main
- Merge feature branch
- Push to origin/main
4. Post success message to issue
This workflow REQUIRES that all previous workflows have been run and that
every field in ADWState has a value. This is our final approval step.
Note: Merge operations happen in the main repository root, not in the worktree,
to preserve the worktree's state.
"""
import sys
import os
import logging
import json
import subprocess
from typing import Optional, Dict, Any, Tuple
from dotenv import load_dotenv
from adw_modules.state import ADWState
from adw_modules.github import (
make_issue_comment,
get_repo_url,
extract_repo_path,
)
from adw_modules.beads_integration import is_beads_issue, close_beads_issue
from adw_modules.workflow_ops import format_issue_message
from adw_modules.worktree_ops import validate_worktree
from adw_modules.data_types import ADWStateData
# Setup logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# Agent name constant
AGENT_SHIPPER = "shipper"
def get_main_repo_root() -> str:
"""Get the main repository root directory (parent of adws)."""
# This script is in adws/, so go up one level to get repo root
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def manual_merge_to_main(branch_name: str, logger: logging.Logger) -> Tuple[bool, Optional[str]]:
"""Manually merge a branch to main using git commands.
This runs in the main repository root, not in a worktree.
Args:
branch_name: The feature branch to merge
logger: Logger instance
Returns:
Tuple of (success, error_message)
"""
repo_root = get_main_repo_root()
logger.info(f"Performing manual merge in main repository: {repo_root}")
try:
# Save current branch to restore later
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True, text=True, cwd=repo_root
)
original_branch = result.stdout.strip()
logger.debug(f"Original branch: {original_branch}")
# Step 1: Fetch latest from origin
logger.info("Fetching latest from origin...")
result = subprocess.run(
["git", "fetch", "origin"],
capture_output=True, text=True, cwd=repo_root
)
if result.returncode != 0:
return False, f"Failed to fetch from origin: {result.stderr}"
# Step 2: Checkout main
logger.info("Checking out main branch...")
result = subprocess.run(
["git", "checkout", "main"],
capture_output=True, text=True, cwd=repo_root
)
if result.returncode != 0:
return False, f"Failed to checkout main: {result.stderr}"
# Step 3: Pull latest main
logger.info("Pulling latest main...")
result = subprocess.run(
["git", "pull", "origin", "main"],
capture_output=True, text=True, cwd=repo_root
)
if result.returncode != 0:
# Try to restore original branch
subprocess.run(["git", "checkout", original_branch], cwd=repo_root)
return False, f"Failed to pull latest main: {result.stderr}"
# Step 4: Merge the feature branch (no-ff to preserve all commits)
logger.info(f"Merging branch {branch_name} (no-ff to preserve all commits)...")
result = subprocess.run(
["git", "merge", branch_name, "--no-ff", "-m", f"Merge branch '{branch_name}' via ADW Ship workflow"],
capture_output=True, text=True, cwd=repo_root
)
if result.returncode != 0:
# Try to restore original branch
subprocess.run(["git", "checkout", original_branch], cwd=repo_root)
return False, f"Failed to merge {branch_name}: {result.stderr}"
# Step 5: Push to origin/main
logger.info("Pushing to origin/main...")
result = subprocess.run(
["git", "push", "origin", "main"],
capture_output=True, text=True, cwd=repo_root
)
if result.returncode != 0:
# Try to restore original branch
subprocess.run(["git", "checkout", original_branch], cwd=repo_root)
return False, f"Failed to push to origin/main: {result.stderr}"
# Step 6: Restore original branch
logger.info(f"Restoring original branch: {original_branch}")
subprocess.run(["git", "checkout", original_branch], cwd=repo_root)
logger.info("✅ Successfully merged and pushed to main!")
return True, None
except Exception as e:
logger.error(f"Unexpected error during merge: {e}")
# Try to restore original branch
try:
subprocess.run(["git", "checkout", original_branch], cwd=repo_root)
except:
pass
return False, str(e)
def validate_state_completeness(state: ADWState, logger: logging.Logger) -> tuple[bool, list[str]]:
"""Validate that all fields in ADWState have values (not None).
Returns:
tuple of (is_valid, missing_fields)
"""
# Get the expected fields from ADWStateData model
expected_fields = {
"adw_id",
"issue_number",
"branch_name",
"plan_file",
"issue_class",
"worktree_path",
"backend_port",
"frontend_port",
}
missing_fields = []
for field in expected_fields:
value = state.get(field)
if value is None:
missing_fields.append(field)
logger.warning(f"Missing required field: {field}")
else:
logger.debug(f"{field}: {value}")
return len(missing_fields) == 0, missing_fields
def main():
"""Main entry point."""
# Load environment variables
load_dotenv()
# Parse command line args
# INTENTIONAL: adw-id is REQUIRED - we need it to find the worktree and state
if len(sys.argv) < 3:
print("Usage: uv run adw_ship_iso.py <issue-number> <adw-id>")
print("\nError: Both issue-number and adw-id are required")
print("Run the complete SDLC workflow before shipping")
sys.exit(1)
issue_number = sys.argv[1]
adw_id = sys.argv[2]
# Try to load existing state
state = ADWState.load(adw_id, logger)
if not state:
# No existing state found
logger.error(f"No state found for ADW ID: {adw_id}")
logger.error("Run the complete SDLC workflow before shipping")
print(f"\nError: No state found for ADW ID: {adw_id}")
print("Run the complete SDLC workflow before shipping")
sys.exit(1)
# Update issue number from state if available
issue_number = state.get("issue_number", issue_number)
# Track that this ADW workflow has run
state.append_adw_id("adw_ship_iso")
logger.info(f"ADW Ship Iso starting - ID: {adw_id}, Issue: {issue_number}")
# Check if this is a beads issue
is_beads = is_beads_issue(issue_number)
logger.info(f"Issue type: {'beads' if is_beads else 'GitHub'}")
# Post initial status (only for GitHub issues)
if not is_beads:
make_issue_comment(
issue_number,
format_issue_message(adw_id, "ops", f"🚢 Starting ship workflow\n"
f"📋 Validating state completeness...")
)
# Step 1: Validate state completeness
logger.info("Validating state completeness...")
is_valid, missing_fields = validate_state_completeness(state, logger)
if not is_valid:
error_msg = f"State validation failed. Missing fields: {', '.join(missing_fields)}"
logger.error(error_msg)
if not is_beads:
make_issue_comment(
issue_number,
format_issue_message(adw_id, AGENT_SHIPPER, f"{error_msg}\n\n"
"Please ensure all workflows have been run:\n"
"- adw_plan_iso.py (creates plan_file, branch_name, issue_class)\n"
"- adw_build_iso.py (implements the plan)\n"
"- adw_test_iso.py (runs tests)\n"
"- adw_review_iso.py (reviews implementation)\n"
"- adw_document_iso.py (generates docs)")
)
sys.exit(1)
logger.info("✅ State validation passed - all fields have values")
# Step 2: Validate worktree exists
valid, error = validate_worktree(adw_id, state)
if not valid:
logger.error(f"Worktree validation failed: {error}")
if not is_beads:
make_issue_comment(
issue_number,
format_issue_message(adw_id, AGENT_SHIPPER, f"❌ Worktree validation failed: {error}")
)
sys.exit(1)
worktree_path = state.get("worktree_path")
logger.info(f"✅ Worktree validated at: {worktree_path}")
# Step 3: Get branch name
branch_name = state.get("branch_name")
logger.info(f"Preparing to merge branch: {branch_name}")
if not is_beads:
make_issue_comment(
issue_number,
format_issue_message(adw_id, AGENT_SHIPPER, f"📋 State validation complete\n"
f"🔍 Preparing to merge branch: {branch_name}")
)
# Step 4: Perform manual merge
logger.info(f"Starting manual merge of {branch_name} to main...")
if not is_beads:
make_issue_comment(
issue_number,
format_issue_message(adw_id, AGENT_SHIPPER, f"🔀 Merging {branch_name} to main...\n"
"Using manual git operations in main repository")
)
success, error = manual_merge_to_main(branch_name, logger)
if not success:
logger.error(f"Failed to merge: {error}")
if not is_beads:
make_issue_comment(
issue_number,
format_issue_message(adw_id, AGENT_SHIPPER, f"❌ Failed to merge: {error}")
)
sys.exit(1)
logger.info(f"✅ Successfully merged {branch_name} to main")
# Step 5: Close beads issue if applicable
if is_beads:
logger.info(f"Closing beads issue: {issue_number}")
success, error = close_beads_issue(
issue_number,
f"Completed via ADW {adw_id} - merged to main"
)
if not success:
logger.warning(f"Failed to close beads issue: {error}")
else:
logger.info(f"✅ Closed beads issue: {issue_number}")
# Step 6: Post success message (only for GitHub issues)
if not is_beads:
make_issue_comment(
issue_number,
format_issue_message(adw_id, AGENT_SHIPPER,
f"🎉 **Successfully shipped!**\n\n"
f"✅ Validated all state fields\n"
f"✅ Merged branch `{branch_name}` to main\n"
f"✅ Pushed to origin/main\n\n"
f"🚢 Code has been deployed to production!")
)
# Save final state
state.save("adw_ship_iso")
# Post final state summary (only for GitHub issues)
if not is_beads:
make_issue_comment(
issue_number,
f"{adw_id}_ops: 📋 Final ship state:\n```json\n{json.dumps(state.data, indent=2)}\n```"
)
logger.info("Ship workflow completed successfully")
if __name__ == "__main__":
main()