adversarial-workflow 0.6.0__py3-none-any.whl → 0.6.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adversarial_workflow/__init__.py +1 -1
- adversarial_workflow/cli.py +111 -58
- adversarial_workflow/evaluators/__init__.py +3 -2
- adversarial_workflow/evaluators/discovery.py +9 -4
- adversarial_workflow/evaluators/runner.py +16 -8
- adversarial_workflow/utils/file_splitter.py +218 -184
- adversarial_workflow/utils/validation.py +3 -1
- {adversarial_workflow-0.6.0.dist-info → adversarial_workflow-0.6.2.dist-info}/METADATA +34 -4
- {adversarial_workflow-0.6.0.dist-info → adversarial_workflow-0.6.2.dist-info}/RECORD +13 -13
- {adversarial_workflow-0.6.0.dist-info → adversarial_workflow-0.6.2.dist-info}/WHEEL +1 -1
- {adversarial_workflow-0.6.0.dist-info → adversarial_workflow-0.6.2.dist-info}/entry_points.txt +0 -0
- {adversarial_workflow-0.6.0.dist-info → adversarial_workflow-0.6.2.dist-info}/licenses/LICENSE +0 -0
- {adversarial_workflow-0.6.0.dist-info → adversarial_workflow-0.6.2.dist-info}/top_level.txt +0 -0
adversarial_workflow/__init__.py
CHANGED
adversarial_workflow/cli.py
CHANGED
|
@@ -27,9 +27,9 @@ from pathlib import Path
|
|
|
27
27
|
from typing import Dict, List, Optional, Tuple
|
|
28
28
|
|
|
29
29
|
import yaml
|
|
30
|
-
from dotenv import load_dotenv
|
|
30
|
+
from dotenv import dotenv_values, load_dotenv
|
|
31
31
|
|
|
32
|
-
__version__ = "0.6.
|
|
32
|
+
__version__ = "0.6.2"
|
|
33
33
|
|
|
34
34
|
# ANSI color codes for better output
|
|
35
35
|
RESET = "\033[0m"
|
|
@@ -800,26 +800,36 @@ def check() -> int:
|
|
|
800
800
|
issues: List[Dict] = []
|
|
801
801
|
good_checks: List[str] = []
|
|
802
802
|
|
|
803
|
-
# Check for .env file
|
|
803
|
+
# Check for .env file (note: already loaded by main() at startup)
|
|
804
804
|
env_file = Path(".env")
|
|
805
805
|
env_loaded = False
|
|
806
|
-
env_keys_before = set(os.environ.keys())
|
|
807
806
|
|
|
808
807
|
if env_file.exists():
|
|
809
808
|
try:
|
|
809
|
+
# Count variables by reading file directly (works even if already loaded)
|
|
810
|
+
env_vars = dotenv_values(env_file)
|
|
811
|
+
var_count = len([k for k, v in env_vars.items() if v is not None])
|
|
812
|
+
|
|
813
|
+
# Still load to ensure environment is set
|
|
810
814
|
load_dotenv(env_file)
|
|
811
|
-
env_keys_after = set(os.environ.keys())
|
|
812
|
-
new_keys = env_keys_after - env_keys_before
|
|
813
815
|
env_loaded = True
|
|
814
|
-
good_checks.append(
|
|
815
|
-
|
|
816
|
+
good_checks.append(f".env file found and loaded ({var_count} variables)")
|
|
817
|
+
except (FileNotFoundError, PermissionError) as e:
|
|
818
|
+
# File access errors
|
|
819
|
+
issues.append(
|
|
820
|
+
{
|
|
821
|
+
"severity": "WARNING",
|
|
822
|
+
"message": f".env file found but could not be read: {e}",
|
|
823
|
+
"fix": "Check .env file permissions",
|
|
824
|
+
}
|
|
816
825
|
)
|
|
817
|
-
except
|
|
826
|
+
except (OSError, ValueError) as e:
|
|
827
|
+
# Covers UnicodeDecodeError (ValueError subclass) and other OS errors
|
|
818
828
|
issues.append(
|
|
819
829
|
{
|
|
820
830
|
"severity": "WARNING",
|
|
821
|
-
"message": f".env file found but could not be
|
|
822
|
-
"fix": "Check .env file
|
|
831
|
+
"message": f".env file found but could not be parsed: {e}",
|
|
832
|
+
"fix": "Check .env file encoding (should be UTF-8)",
|
|
823
833
|
}
|
|
824
834
|
)
|
|
825
835
|
else:
|
|
@@ -2086,10 +2096,6 @@ def evaluate(task_file: str) -> int:
|
|
|
2086
2096
|
return 0
|
|
2087
2097
|
|
|
2088
2098
|
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
2099
|
def review() -> int:
|
|
2094
2100
|
"""Run Phase 3: Code review."""
|
|
2095
2101
|
|
|
@@ -2728,54 +2734,61 @@ def agent_onboard(project_path: str = ".") -> int:
|
|
|
2728
2734
|
return 0
|
|
2729
2735
|
|
|
2730
2736
|
|
|
2731
|
-
def split(
|
|
2737
|
+
def split(
|
|
2738
|
+
task_file: str,
|
|
2739
|
+
strategy: str = "sections",
|
|
2740
|
+
max_lines: int = 500,
|
|
2741
|
+
dry_run: bool = False,
|
|
2742
|
+
):
|
|
2732
2743
|
"""Split large task files into smaller evaluable chunks.
|
|
2733
|
-
|
|
2744
|
+
|
|
2734
2745
|
Args:
|
|
2735
2746
|
task_file: Path to the task file to split
|
|
2736
2747
|
strategy: Split strategy ('sections', 'phases', or 'manual')
|
|
2737
2748
|
max_lines: Maximum lines per split (default: 500)
|
|
2738
2749
|
dry_run: Preview splits without creating files
|
|
2739
|
-
|
|
2750
|
+
|
|
2740
2751
|
Returns:
|
|
2741
2752
|
Exit code (0 for success, 1 for error)
|
|
2742
2753
|
"""
|
|
2743
2754
|
from .utils.file_splitter import (
|
|
2744
|
-
analyze_task_file,
|
|
2745
|
-
|
|
2746
|
-
split_by_phases,
|
|
2747
|
-
|
|
2755
|
+
analyze_task_file,
|
|
2756
|
+
generate_split_files,
|
|
2757
|
+
split_by_phases,
|
|
2758
|
+
split_by_sections,
|
|
2748
2759
|
)
|
|
2749
|
-
|
|
2760
|
+
|
|
2750
2761
|
try:
|
|
2751
2762
|
print_box("File Splitting Utility", CYAN)
|
|
2752
|
-
|
|
2763
|
+
|
|
2753
2764
|
# Validate file exists
|
|
2754
2765
|
if not os.path.exists(task_file):
|
|
2755
2766
|
print(f"{RED}Error: File not found: {task_file}{RESET}")
|
|
2756
2767
|
return 1
|
|
2757
|
-
|
|
2768
|
+
|
|
2758
2769
|
# Analyze file
|
|
2759
2770
|
print(f"📄 Analyzing task file: {task_file}")
|
|
2760
2771
|
analysis = analyze_task_file(task_file)
|
|
2761
|
-
|
|
2762
|
-
lines = analysis[
|
|
2763
|
-
tokens = analysis[
|
|
2772
|
+
|
|
2773
|
+
lines = analysis["total_lines"]
|
|
2774
|
+
tokens = analysis["estimated_tokens"]
|
|
2764
2775
|
print(f" Lines: {lines}")
|
|
2765
2776
|
print(f" Estimated tokens: ~{tokens:,}")
|
|
2766
|
-
|
|
2777
|
+
|
|
2767
2778
|
# Check if splitting is recommended
|
|
2768
2779
|
if lines <= max_lines:
|
|
2769
|
-
print(
|
|
2780
|
+
print(
|
|
2781
|
+
f"{GREEN}✅ File is under recommended limit ({max_lines} lines){RESET}"
|
|
2782
|
+
)
|
|
2770
2783
|
print("No splitting needed.")
|
|
2771
2784
|
return 0
|
|
2772
|
-
|
|
2785
|
+
|
|
2773
2786
|
print(f"{YELLOW}⚠️ File exceeds recommended limit ({max_lines} lines){RESET}")
|
|
2774
|
-
|
|
2787
|
+
|
|
2775
2788
|
# Read file content for splitting
|
|
2776
|
-
with open(task_file,
|
|
2789
|
+
with open(task_file, "r", encoding="utf-8") as f:
|
|
2777
2790
|
content = f.read()
|
|
2778
|
-
|
|
2791
|
+
|
|
2779
2792
|
# Apply split strategy
|
|
2780
2793
|
if strategy == "sections":
|
|
2781
2794
|
splits = split_by_sections(content, max_lines=max_lines)
|
|
@@ -2784,42 +2797,44 @@ def split(task_file: str, strategy: str = "sections", max_lines: int = 500, dry_
|
|
|
2784
2797
|
splits = split_by_phases(content)
|
|
2785
2798
|
print(f"\n💡 Suggested splits (by phases):")
|
|
2786
2799
|
else:
|
|
2787
|
-
print(
|
|
2800
|
+
print(
|
|
2801
|
+
f"{RED}Error: Unknown strategy '{strategy}'. Use 'sections' or 'phases'.{RESET}"
|
|
2802
|
+
)
|
|
2788
2803
|
return 1
|
|
2789
|
-
|
|
2804
|
+
|
|
2790
2805
|
# Display split preview
|
|
2791
2806
|
for i, split in enumerate(splits, 1):
|
|
2792
2807
|
filename = f"{Path(task_file).stem}-part{i}{Path(task_file).suffix}"
|
|
2793
2808
|
print(f" - {filename} ({split['line_count']} lines)")
|
|
2794
|
-
|
|
2809
|
+
|
|
2795
2810
|
# Dry run mode
|
|
2796
2811
|
if dry_run:
|
|
2797
2812
|
print(f"\n{CYAN}📋 Dry run mode - no files created{RESET}")
|
|
2798
2813
|
return 0
|
|
2799
|
-
|
|
2814
|
+
|
|
2800
2815
|
# Prompt user for confirmation
|
|
2801
2816
|
create_files = prompt_user(f"\nCreate {len(splits)} files?", default="n")
|
|
2802
|
-
|
|
2803
|
-
if create_files.lower() in [
|
|
2817
|
+
|
|
2818
|
+
if create_files.lower() in ["y", "yes"]:
|
|
2804
2819
|
# Create output directory
|
|
2805
2820
|
output_dir = os.path.join(os.path.dirname(task_file), "splits")
|
|
2806
|
-
|
|
2821
|
+
|
|
2807
2822
|
# Generate split files
|
|
2808
2823
|
created_files = generate_split_files(task_file, splits, output_dir)
|
|
2809
|
-
|
|
2824
|
+
|
|
2810
2825
|
print(f"{GREEN}✅ Created {len(created_files)} files:{RESET}")
|
|
2811
2826
|
for file_path in created_files:
|
|
2812
2827
|
print(f" {file_path}")
|
|
2813
|
-
|
|
2828
|
+
|
|
2814
2829
|
print(f"\n{CYAN}💡 Tip: Evaluate each split file independently:{RESET}")
|
|
2815
2830
|
for file_path in created_files:
|
|
2816
2831
|
rel_path = os.path.relpath(file_path)
|
|
2817
2832
|
print(f" adversarial evaluate {rel_path}")
|
|
2818
2833
|
else:
|
|
2819
2834
|
print("Cancelled - no files created.")
|
|
2820
|
-
|
|
2835
|
+
|
|
2821
2836
|
return 0
|
|
2822
|
-
|
|
2837
|
+
|
|
2823
2838
|
except Exception as e:
|
|
2824
2839
|
print(f"{RED}Error during file splitting: {e}{RESET}")
|
|
2825
2840
|
return 1
|
|
@@ -2865,14 +2880,33 @@ def list_evaluators() -> int:
|
|
|
2865
2880
|
|
|
2866
2881
|
return 0
|
|
2867
2882
|
|
|
2883
|
+
|
|
2868
2884
|
def main():
|
|
2869
2885
|
"""Main CLI entry point."""
|
|
2870
2886
|
import logging
|
|
2887
|
+
import sys
|
|
2888
|
+
|
|
2889
|
+
# Load .env file before any commands run
|
|
2890
|
+
# Wrapped in try/except so CLI remains usable even with malformed .env
|
|
2891
|
+
try:
|
|
2892
|
+
load_dotenv()
|
|
2893
|
+
except Exception as e:
|
|
2894
|
+
print(f"Warning: Could not load .env file: {e}", file=sys.stderr)
|
|
2895
|
+
|
|
2896
|
+
# Load .env file before any commands run
|
|
2897
|
+
# Use explicit path to ensure we find .env in current working directory
|
|
2898
|
+
# (load_dotenv() without args can fail to find .env in some contexts)
|
|
2899
|
+
env_file = Path.cwd() / ".env"
|
|
2900
|
+
if env_file.exists():
|
|
2901
|
+
try:
|
|
2902
|
+
load_dotenv(env_file)
|
|
2903
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
2904
|
+
print(f"Warning: Could not load .env file: {e}", file=sys.stderr)
|
|
2871
2905
|
|
|
2872
2906
|
from adversarial_workflow.evaluators import (
|
|
2907
|
+
BUILTIN_EVALUATORS,
|
|
2873
2908
|
get_all_evaluators,
|
|
2874
2909
|
run_evaluator,
|
|
2875
|
-
BUILTIN_EVALUATORS,
|
|
2876
2910
|
)
|
|
2877
2911
|
|
|
2878
2912
|
logger = logging.getLogger(__name__)
|
|
@@ -2880,8 +2914,16 @@ def main():
|
|
|
2880
2914
|
# Commands that cannot be overridden by evaluators
|
|
2881
2915
|
# Note: 'review' is special - it reviews git changes without a file argument
|
|
2882
2916
|
STATIC_COMMANDS = {
|
|
2883
|
-
"init",
|
|
2884
|
-
"
|
|
2917
|
+
"init",
|
|
2918
|
+
"check",
|
|
2919
|
+
"doctor",
|
|
2920
|
+
"health",
|
|
2921
|
+
"quickstart",
|
|
2922
|
+
"agent",
|
|
2923
|
+
"split",
|
|
2924
|
+
"validate",
|
|
2925
|
+
"review",
|
|
2926
|
+
"list-evaluators",
|
|
2885
2927
|
}
|
|
2886
2928
|
|
|
2887
2929
|
parser = argparse.ArgumentParser(
|
|
@@ -2970,16 +3012,21 @@ For more information: https://github.com/movito/adversarial-workflow
|
|
|
2970
3012
|
)
|
|
2971
3013
|
split_parser.add_argument("task_file", help="Task file to split")
|
|
2972
3014
|
split_parser.add_argument(
|
|
2973
|
-
"--strategy",
|
|
2974
|
-
|
|
3015
|
+
"--strategy",
|
|
3016
|
+
"-s",
|
|
3017
|
+
choices=["sections", "phases"],
|
|
3018
|
+
default="sections",
|
|
3019
|
+
help="Split strategy: 'sections' (default) or 'phases'",
|
|
2975
3020
|
)
|
|
2976
3021
|
split_parser.add_argument(
|
|
2977
|
-
"--max-lines",
|
|
2978
|
-
|
|
3022
|
+
"--max-lines",
|
|
3023
|
+
"-m",
|
|
3024
|
+
type=int,
|
|
3025
|
+
default=500,
|
|
3026
|
+
help="Maximum lines per split (default: 500)",
|
|
2979
3027
|
)
|
|
2980
3028
|
split_parser.add_argument(
|
|
2981
|
-
"--dry-run", action="store_true",
|
|
2982
|
-
help="Preview splits without creating files"
|
|
3029
|
+
"--dry-run", action="store_true", help="Preview splits without creating files"
|
|
2983
3030
|
)
|
|
2984
3031
|
|
|
2985
3032
|
# list-evaluators command
|
|
@@ -3000,7 +3047,12 @@ For more information: https://github.com/movito/adversarial-workflow
|
|
|
3000
3047
|
for name, config in evaluators.items():
|
|
3001
3048
|
# Skip if name conflicts with static command
|
|
3002
3049
|
if name in STATIC_COMMANDS:
|
|
3003
|
-
|
|
3050
|
+
# Only warn for user-defined evaluators, not built-ins
|
|
3051
|
+
# Built-in conflicts are intentional (e.g., 'review' command vs 'review' evaluator)
|
|
3052
|
+
if getattr(config, "source", None) != "builtin":
|
|
3053
|
+
logger.warning(
|
|
3054
|
+
"Evaluator '%s' conflicts with CLI command; skipping", name
|
|
3055
|
+
)
|
|
3004
3056
|
# Mark as registered to prevent alias re-registration attempts
|
|
3005
3057
|
registered_configs.add(id(config))
|
|
3006
3058
|
continue
|
|
@@ -3027,10 +3079,11 @@ For more information: https://github.com/movito/adversarial-workflow
|
|
|
3027
3079
|
)
|
|
3028
3080
|
eval_parser.add_argument("file", help="File to evaluate")
|
|
3029
3081
|
eval_parser.add_argument(
|
|
3030
|
-
"--timeout",
|
|
3082
|
+
"--timeout",
|
|
3083
|
+
"-t",
|
|
3031
3084
|
type=int,
|
|
3032
3085
|
default=180,
|
|
3033
|
-
help="Timeout in seconds (default: 180)"
|
|
3086
|
+
help="Timeout in seconds (default: 180)",
|
|
3034
3087
|
)
|
|
3035
3088
|
# Store config for later execution
|
|
3036
3089
|
eval_parser.set_defaults(evaluator_config=config)
|
|
@@ -3078,7 +3131,7 @@ For more information: https://github.com/movito/adversarial-workflow
|
|
|
3078
3131
|
args.task_file,
|
|
3079
3132
|
strategy=args.strategy,
|
|
3080
3133
|
max_lines=args.max_lines,
|
|
3081
|
-
dry_run=args.dry_run
|
|
3134
|
+
dry_run=args.dry_run,
|
|
3082
3135
|
)
|
|
3083
3136
|
elif args.command == "list-evaluators":
|
|
3084
3137
|
return list_evaluators()
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Evaluators module for adversarial-workflow plugin architecture."""
|
|
2
2
|
|
|
3
|
+
from .builtins import BUILTIN_EVALUATORS
|
|
3
4
|
from .config import EvaluatorConfig
|
|
4
5
|
from .discovery import (
|
|
6
|
+
EvaluatorParseError,
|
|
5
7
|
discover_local_evaluators,
|
|
6
8
|
parse_evaluator_yaml,
|
|
7
|
-
EvaluatorParseError,
|
|
8
9
|
)
|
|
9
10
|
from .runner import run_evaluator
|
|
10
|
-
from .builtins import BUILTIN_EVALUATORS
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def get_all_evaluators() -> dict[str, EvaluatorConfig]:
|
|
@@ -17,6 +17,7 @@ def get_all_evaluators() -> dict[str, EvaluatorConfig]:
|
|
|
17
17
|
Aliases from local evaluators are also included in the returned dictionary.
|
|
18
18
|
"""
|
|
19
19
|
import logging
|
|
20
|
+
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
22
23
|
evaluators: dict[str, EvaluatorConfig] = {}
|
|
@@ -40,9 +40,7 @@ def parse_evaluator_yaml(yml_file: Path) -> EvaluatorConfig:
|
|
|
40
40
|
try:
|
|
41
41
|
content = yml_file.read_text(encoding="utf-8")
|
|
42
42
|
except UnicodeDecodeError as e:
|
|
43
|
-
raise EvaluatorParseError(
|
|
44
|
-
f"File encoding error (not UTF-8): {yml_file}"
|
|
45
|
-
) from e
|
|
43
|
+
raise EvaluatorParseError(f"File encoding error (not UTF-8): {yml_file}") from e
|
|
46
44
|
|
|
47
45
|
# Parse YAML
|
|
48
46
|
data = yaml.safe_load(content)
|
|
@@ -58,7 +56,14 @@ def parse_evaluator_yaml(yml_file: Path) -> EvaluatorConfig:
|
|
|
58
56
|
)
|
|
59
57
|
|
|
60
58
|
# Validate required fields exist
|
|
61
|
-
required = [
|
|
59
|
+
required = [
|
|
60
|
+
"name",
|
|
61
|
+
"description",
|
|
62
|
+
"model",
|
|
63
|
+
"api_key_env",
|
|
64
|
+
"prompt",
|
|
65
|
+
"output_suffix",
|
|
66
|
+
]
|
|
62
67
|
missing = [f for f in required if f not in data]
|
|
63
68
|
if missing:
|
|
64
69
|
raise EvaluatorParseError(f"Missing required fields: {', '.join(missing)}")
|
|
@@ -10,10 +10,10 @@ import tempfile
|
|
|
10
10
|
from datetime import datetime, timezone
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
-
from .
|
|
14
|
-
from ..utils.colors import RESET, BOLD, GREEN, YELLOW, RED
|
|
13
|
+
from ..utils.colors import BOLD, GREEN, RED, RESET, YELLOW
|
|
15
14
|
from ..utils.config import load_config
|
|
16
15
|
from ..utils.validation import validate_evaluation_output
|
|
16
|
+
from .config import EvaluatorConfig
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def run_evaluator(config: EvaluatorConfig, file_path: str, timeout: int = 180) -> int:
|
|
@@ -124,7 +124,7 @@ def _run_custom_evaluator(
|
|
|
124
124
|
"""
|
|
125
125
|
|
|
126
126
|
# Create temp file for prompt
|
|
127
|
-
with tempfile.NamedTemporaryFile(mode=
|
|
127
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
|
|
128
128
|
f.write(full_prompt)
|
|
129
129
|
prompt_file = f.name
|
|
130
130
|
|
|
@@ -136,12 +136,15 @@ def _run_custom_evaluator(
|
|
|
136
136
|
# Build aider command
|
|
137
137
|
cmd = [
|
|
138
138
|
"aider",
|
|
139
|
-
"--model",
|
|
139
|
+
"--model",
|
|
140
|
+
config.model,
|
|
140
141
|
"--yes",
|
|
141
142
|
"--no-git",
|
|
142
143
|
"--no-auto-commits",
|
|
143
|
-
"--message-file",
|
|
144
|
-
|
|
144
|
+
"--message-file",
|
|
145
|
+
prompt_file,
|
|
146
|
+
"--read",
|
|
147
|
+
file_path,
|
|
145
148
|
]
|
|
146
149
|
|
|
147
150
|
result = subprocess.run(
|
|
@@ -224,7 +227,10 @@ def _execute_script(
|
|
|
224
227
|
|
|
225
228
|
# Validate output
|
|
226
229
|
file_basename = Path(file_path).stem
|
|
227
|
-
log_file =
|
|
230
|
+
log_file = (
|
|
231
|
+
Path(project_config["log_directory"])
|
|
232
|
+
/ f"{file_basename}-{config.output_suffix}.md"
|
|
233
|
+
)
|
|
228
234
|
|
|
229
235
|
is_valid, verdict, message = validate_evaluation_output(str(log_file))
|
|
230
236
|
|
|
@@ -235,7 +241,9 @@ def _execute_script(
|
|
|
235
241
|
return _report_verdict(verdict, log_file, config)
|
|
236
242
|
|
|
237
243
|
|
|
238
|
-
def _report_verdict(
|
|
244
|
+
def _report_verdict(
|
|
245
|
+
verdict: str | None, log_file: Path, config: EvaluatorConfig
|
|
246
|
+
) -> int:
|
|
239
247
|
"""Report the evaluation verdict to terminal."""
|
|
240
248
|
print()
|
|
241
249
|
if verdict == "APPROVED":
|
|
@@ -4,360 +4,392 @@ This module provides functionality to split large markdown files into smaller,
|
|
|
4
4
|
independently evaluable chunks to work around OpenAI's rate limits.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import re
|
|
8
7
|
import os
|
|
8
|
+
import re
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import
|
|
10
|
+
from typing import Any, Dict, List
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def analyze_task_file(file_path: str) -> Dict[str, Any]:
|
|
14
14
|
"""Analyze file structure and suggest split points.
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
Args:
|
|
17
17
|
file_path: Path to the markdown file to analyze
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
Returns:
|
|
20
20
|
Dict containing:
|
|
21
21
|
- total_lines: Total number of lines
|
|
22
22
|
- sections: List of detected sections with metadata
|
|
23
23
|
- estimated_tokens: Rough token estimate (lines * 4)
|
|
24
24
|
- suggested_splits: List of suggested split points
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
Raises:
|
|
27
27
|
FileNotFoundError: If file doesn't exist
|
|
28
28
|
ValueError: If file is empty or too small
|
|
29
29
|
"""
|
|
30
30
|
if not os.path.exists(file_path):
|
|
31
31
|
raise FileNotFoundError(f"File not found: {file_path}")
|
|
32
|
-
|
|
33
|
-
with open(file_path,
|
|
32
|
+
|
|
33
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
34
34
|
content = f.read()
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
if not content.strip():
|
|
37
37
|
raise ValueError("File is empty or too small")
|
|
38
|
-
|
|
39
|
-
lines = content.split(
|
|
38
|
+
|
|
39
|
+
lines = content.split("\n")
|
|
40
40
|
total_lines = len(lines)
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
# Detect markdown sections
|
|
43
43
|
sections = []
|
|
44
44
|
current_section = None
|
|
45
45
|
current_start = 1
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
for i, line in enumerate(lines, 1):
|
|
48
48
|
# Check for markdown headings (# or ##)
|
|
49
|
-
if re.match(r
|
|
49
|
+
if re.match(r"^#+\s+", line.strip()):
|
|
50
50
|
# Close previous section
|
|
51
51
|
if current_section is not None:
|
|
52
|
-
current_section[
|
|
53
|
-
current_section[
|
|
52
|
+
current_section["end_line"] = i - 1
|
|
53
|
+
current_section["line_count"] = (
|
|
54
|
+
current_section["end_line"] - current_section["start_line"] + 1
|
|
55
|
+
)
|
|
54
56
|
sections.append(current_section)
|
|
55
|
-
|
|
57
|
+
|
|
56
58
|
# Start new section
|
|
57
59
|
heading_level = len(line.lstrip().split()[0]) # Count # characters
|
|
58
|
-
title = re.sub(r
|
|
60
|
+
title = re.sub(r"^#+\s+", "", line.strip())
|
|
59
61
|
current_section = {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
"title": title,
|
|
63
|
+
"heading_level": heading_level,
|
|
64
|
+
"start_line": i,
|
|
65
|
+
"end_line": None,
|
|
66
|
+
"line_count": 0,
|
|
65
67
|
}
|
|
66
68
|
current_start = i
|
|
67
|
-
|
|
69
|
+
|
|
68
70
|
# Close final section
|
|
69
71
|
if current_section is not None:
|
|
70
|
-
current_section[
|
|
71
|
-
current_section[
|
|
72
|
+
current_section["end_line"] = total_lines
|
|
73
|
+
current_section["line_count"] = (
|
|
74
|
+
current_section["end_line"] - current_section["start_line"] + 1
|
|
75
|
+
)
|
|
72
76
|
sections.append(current_section)
|
|
73
|
-
|
|
77
|
+
|
|
74
78
|
# If no sections found, treat entire file as one section
|
|
75
79
|
if not sections:
|
|
76
|
-
sections = [
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
sections = [
|
|
81
|
+
{
|
|
82
|
+
"title": "Full Document",
|
|
83
|
+
"heading_level": 1,
|
|
84
|
+
"start_line": 1,
|
|
85
|
+
"end_line": total_lines,
|
|
86
|
+
"line_count": total_lines,
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
|
|
84
90
|
# Estimate tokens (rough approximation: 1 line ≈ 4 tokens)
|
|
85
91
|
estimated_tokens = total_lines * 4
|
|
86
|
-
|
|
92
|
+
|
|
87
93
|
# Suggest splits if file is large
|
|
88
94
|
suggested_splits = []
|
|
89
95
|
if total_lines > 500:
|
|
90
96
|
# Suggest section-based splits
|
|
91
97
|
suggested_splits = _suggest_section_splits(sections, max_lines=500)
|
|
92
|
-
|
|
98
|
+
|
|
93
99
|
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
"total_lines": total_lines,
|
|
101
|
+
"sections": sections,
|
|
102
|
+
"estimated_tokens": estimated_tokens,
|
|
103
|
+
"suggested_splits": suggested_splits,
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
|
|
101
107
|
def split_by_sections(content: str, max_lines: int = 500) -> List[Dict[str, Any]]:
|
|
102
108
|
"""Split file by markdown sections.
|
|
103
|
-
|
|
109
|
+
|
|
104
110
|
Args:
|
|
105
111
|
content: The markdown content to split
|
|
106
112
|
max_lines: Maximum lines per split
|
|
107
|
-
|
|
113
|
+
|
|
108
114
|
Returns:
|
|
109
115
|
List of split dictionaries with metadata
|
|
110
116
|
"""
|
|
111
|
-
lines = content.split(
|
|
117
|
+
lines = content.split("\n")
|
|
112
118
|
total_lines = len(lines)
|
|
113
|
-
|
|
119
|
+
|
|
114
120
|
if total_lines <= max_lines:
|
|
115
|
-
return [
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
return [
|
|
122
|
+
{
|
|
123
|
+
"content": content,
|
|
124
|
+
"title": "Full Document",
|
|
125
|
+
"start_line": 1,
|
|
126
|
+
"end_line": total_lines,
|
|
127
|
+
"line_count": total_lines,
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
|
|
123
131
|
splits = []
|
|
124
132
|
current_split_lines = []
|
|
125
133
|
current_start = 1
|
|
126
134
|
current_title = "Part"
|
|
127
135
|
split_count = 1
|
|
128
|
-
|
|
136
|
+
|
|
129
137
|
for i, line in enumerate(lines, 1):
|
|
130
138
|
current_split_lines.append(line)
|
|
131
|
-
|
|
139
|
+
|
|
132
140
|
# Check if we hit a section boundary and are near limit
|
|
133
|
-
is_section_boundary = re.match(r
|
|
141
|
+
is_section_boundary = re.match(r"^#+\s+", line.strip())
|
|
134
142
|
approaching_limit = len(current_split_lines) >= max_lines * 0.8
|
|
135
|
-
|
|
136
|
-
if len(current_split_lines) >= max_lines or (
|
|
143
|
+
|
|
144
|
+
if len(current_split_lines) >= max_lines or (
|
|
145
|
+
is_section_boundary and approaching_limit
|
|
146
|
+
):
|
|
137
147
|
# Create split
|
|
138
|
-
split_content =
|
|
139
|
-
splits.append(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
148
|
+
split_content = "\n".join(current_split_lines)
|
|
149
|
+
splits.append(
|
|
150
|
+
{
|
|
151
|
+
"content": split_content,
|
|
152
|
+
"title": f"Part {split_count}",
|
|
153
|
+
"start_line": current_start,
|
|
154
|
+
"end_line": i,
|
|
155
|
+
"line_count": len(current_split_lines),
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
147
159
|
# Reset for next split
|
|
148
160
|
current_split_lines = []
|
|
149
161
|
current_start = i + 1
|
|
150
162
|
split_count += 1
|
|
151
|
-
|
|
163
|
+
|
|
152
164
|
# Handle remaining lines
|
|
153
165
|
if current_split_lines:
|
|
154
|
-
split_content =
|
|
155
|
-
splits.append(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
166
|
+
split_content = "\n".join(current_split_lines)
|
|
167
|
+
splits.append(
|
|
168
|
+
{
|
|
169
|
+
"content": split_content,
|
|
170
|
+
"title": f"Part {split_count}",
|
|
171
|
+
"start_line": current_start,
|
|
172
|
+
"end_line": total_lines,
|
|
173
|
+
"line_count": len(current_split_lines),
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
|
|
163
177
|
return splits
|
|
164
178
|
|
|
165
179
|
|
|
166
180
|
def split_by_phases(content: str) -> List[Dict[str, Any]]:
|
|
167
181
|
"""Split file by implementation phases.
|
|
168
|
-
|
|
182
|
+
|
|
169
183
|
Args:
|
|
170
184
|
content: The markdown content to split
|
|
171
|
-
|
|
185
|
+
|
|
172
186
|
Returns:
|
|
173
187
|
List of split dictionaries, one per phase
|
|
174
188
|
"""
|
|
175
|
-
lines = content.split(
|
|
189
|
+
lines = content.split("\n")
|
|
176
190
|
splits = []
|
|
177
191
|
current_split_lines = []
|
|
178
192
|
current_phase = None
|
|
179
193
|
current_start = 1
|
|
180
|
-
|
|
194
|
+
|
|
181
195
|
for i, line in enumerate(lines, 1):
|
|
182
196
|
# Check for phase markers
|
|
183
|
-
phase_match = re.search(r
|
|
184
|
-
|
|
197
|
+
phase_match = re.search(r"#+\s+Phase\s+(\d+)", line, re.IGNORECASE)
|
|
198
|
+
|
|
185
199
|
if phase_match:
|
|
186
200
|
# Close previous split
|
|
187
201
|
if current_split_lines:
|
|
188
|
-
split_content =
|
|
202
|
+
split_content = "\n".join(current_split_lines)
|
|
189
203
|
title = f"Phase {current_phase}" if current_phase else "Overview"
|
|
190
|
-
splits.append(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
204
|
+
splits.append(
|
|
205
|
+
{
|
|
206
|
+
"content": split_content,
|
|
207
|
+
"title": title,
|
|
208
|
+
"phase_number": current_phase,
|
|
209
|
+
"start_line": current_start,
|
|
210
|
+
"end_line": i - 1,
|
|
211
|
+
"line_count": len(current_split_lines),
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
|
|
199
215
|
# Start new split
|
|
200
216
|
current_phase = int(phase_match.group(1))
|
|
201
217
|
current_split_lines = [line]
|
|
202
218
|
current_start = i
|
|
203
219
|
else:
|
|
204
220
|
current_split_lines.append(line)
|
|
205
|
-
|
|
221
|
+
|
|
206
222
|
# Handle final split
|
|
207
223
|
if current_split_lines:
|
|
208
|
-
split_content =
|
|
224
|
+
split_content = "\n".join(current_split_lines)
|
|
209
225
|
title = f"Phase {current_phase}" if current_phase else "Full Document"
|
|
210
|
-
phase_info = {
|
|
211
|
-
splits.append(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
226
|
+
phase_info = {"phase_number": current_phase} if current_phase else {}
|
|
227
|
+
splits.append(
|
|
228
|
+
{
|
|
229
|
+
"content": split_content,
|
|
230
|
+
"title": title,
|
|
231
|
+
"start_line": current_start,
|
|
232
|
+
"end_line": len(lines),
|
|
233
|
+
"line_count": len(current_split_lines),
|
|
234
|
+
**phase_info,
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
|
|
220
238
|
# If no phases found, return entire content
|
|
221
239
|
if not splits:
|
|
222
|
-
splits = [
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
240
|
+
splits = [
|
|
241
|
+
{
|
|
242
|
+
"content": content,
|
|
243
|
+
"title": "Full Document",
|
|
244
|
+
"start_line": 1,
|
|
245
|
+
"end_line": len(lines),
|
|
246
|
+
"line_count": len(lines),
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
|
|
230
250
|
return splits
|
|
231
251
|
|
|
232
252
|
|
|
233
253
|
def split_at_lines(content: str, line_numbers: List[int]) -> List[Dict[str, Any]]:
|
|
234
254
|
"""Split at specified line numbers.
|
|
235
|
-
|
|
255
|
+
|
|
236
256
|
Args:
|
|
237
257
|
content: The content to split
|
|
238
258
|
line_numbers: Line numbers where splits should occur
|
|
239
|
-
|
|
259
|
+
|
|
240
260
|
Returns:
|
|
241
261
|
List of split dictionaries
|
|
242
262
|
"""
|
|
243
|
-
lines = content.split(
|
|
263
|
+
lines = content.split("\n")
|
|
244
264
|
total_lines = len(lines)
|
|
245
|
-
|
|
265
|
+
|
|
246
266
|
if not line_numbers:
|
|
247
|
-
return [
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
267
|
+
return [
|
|
268
|
+
{
|
|
269
|
+
"content": content,
|
|
270
|
+
"title": "Full Document",
|
|
271
|
+
"start_line": 1,
|
|
272
|
+
"end_line": total_lines,
|
|
273
|
+
"line_count": total_lines,
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
|
|
255
277
|
# Sort and deduplicate line numbers
|
|
256
278
|
split_points = sorted(set(line_numbers))
|
|
257
|
-
|
|
279
|
+
|
|
258
280
|
splits = []
|
|
259
281
|
current_start = 1
|
|
260
|
-
|
|
282
|
+
|
|
261
283
|
for split_line in split_points:
|
|
262
284
|
if split_line >= total_lines:
|
|
263
285
|
continue
|
|
264
|
-
|
|
286
|
+
|
|
265
287
|
# Create split from current_start to split_line
|
|
266
|
-
split_lines = lines[current_start - 1:split_line]
|
|
267
|
-
split_content =
|
|
268
|
-
|
|
269
|
-
splits.append(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
288
|
+
split_lines = lines[current_start - 1 : split_line]
|
|
289
|
+
split_content = "\n".join(split_lines)
|
|
290
|
+
|
|
291
|
+
splits.append(
|
|
292
|
+
{
|
|
293
|
+
"content": split_content,
|
|
294
|
+
"title": f"Lines {current_start}-{split_line}",
|
|
295
|
+
"start_line": current_start,
|
|
296
|
+
"end_line": split_line,
|
|
297
|
+
"line_count": len(split_lines),
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
|
|
277
301
|
current_start = split_line + 1
|
|
278
|
-
|
|
302
|
+
|
|
279
303
|
# Handle remaining lines after final split
|
|
280
304
|
if current_start <= total_lines:
|
|
281
|
-
remaining_lines = lines[current_start - 1:]
|
|
282
|
-
split_content =
|
|
283
|
-
|
|
284
|
-
splits.append(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
305
|
+
remaining_lines = lines[current_start - 1 :]
|
|
306
|
+
split_content = "\n".join(remaining_lines)
|
|
307
|
+
|
|
308
|
+
splits.append(
|
|
309
|
+
{
|
|
310
|
+
"content": split_content,
|
|
311
|
+
"title": f"Lines {current_start}-{total_lines}",
|
|
312
|
+
"start_line": current_start,
|
|
313
|
+
"end_line": total_lines,
|
|
314
|
+
"line_count": len(remaining_lines),
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
|
|
292
318
|
return splits
|
|
293
319
|
|
|
294
320
|
|
|
295
|
-
def generate_split_files(
|
|
321
|
+
def generate_split_files(
|
|
322
|
+
original: str, splits: List[Dict[str, Any]], output_dir: str
|
|
323
|
+
) -> List[str]:
|
|
296
324
|
"""Generate split files with metadata and cross-references.
|
|
297
|
-
|
|
325
|
+
|
|
298
326
|
Args:
|
|
299
327
|
original: Original filename
|
|
300
328
|
splits: List of split dictionaries
|
|
301
329
|
output_dir: Directory to write split files
|
|
302
|
-
|
|
330
|
+
|
|
303
331
|
Returns:
|
|
304
332
|
List of created file paths
|
|
305
333
|
"""
|
|
306
334
|
os.makedirs(output_dir, exist_ok=True)
|
|
307
|
-
|
|
335
|
+
|
|
308
336
|
created_files = []
|
|
309
337
|
original_name = Path(original).stem
|
|
310
338
|
original_ext = Path(original).suffix
|
|
311
|
-
|
|
339
|
+
|
|
312
340
|
for i, split in enumerate(splits, 1):
|
|
313
341
|
# Generate filename
|
|
314
342
|
filename = f"{original_name}-part{i}{original_ext}"
|
|
315
343
|
file_path = os.path.join(output_dir, filename)
|
|
316
|
-
|
|
344
|
+
|
|
317
345
|
# Create content with metadata header
|
|
318
346
|
metadata_header = f"""<!-- Split from {original} -->
|
|
319
347
|
<!-- Part {i} of {len(splits)} -->
|
|
320
348
|
<!-- Lines {split['start_line']}-{split['end_line']} ({split['line_count']} lines) -->
|
|
321
349
|
|
|
322
350
|
"""
|
|
323
|
-
|
|
324
|
-
full_content = metadata_header + split[
|
|
325
|
-
|
|
351
|
+
|
|
352
|
+
full_content = metadata_header + split["content"]
|
|
353
|
+
|
|
326
354
|
# Write file
|
|
327
|
-
with open(file_path,
|
|
355
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
328
356
|
f.write(full_content)
|
|
329
|
-
|
|
357
|
+
|
|
330
358
|
created_files.append(file_path)
|
|
331
|
-
|
|
359
|
+
|
|
332
360
|
return created_files
|
|
333
361
|
|
|
334
362
|
|
|
335
|
-
def _suggest_section_splits(
|
|
363
|
+
def _suggest_section_splits(
|
|
364
|
+
sections: List[Dict[str, Any]], max_lines: int = 500
|
|
365
|
+
) -> List[Dict[str, Any]]:
|
|
336
366
|
"""Suggest optimal split points based on sections.
|
|
337
|
-
|
|
367
|
+
|
|
338
368
|
Args:
|
|
339
369
|
sections: List of section metadata
|
|
340
370
|
max_lines: Maximum lines per split
|
|
341
|
-
|
|
371
|
+
|
|
342
372
|
Returns:
|
|
343
373
|
List of suggested split configurations
|
|
344
374
|
"""
|
|
345
375
|
suggestions = []
|
|
346
376
|
current_chunk_lines = 0
|
|
347
377
|
current_chunk_sections = []
|
|
348
|
-
|
|
378
|
+
|
|
349
379
|
for section in sections:
|
|
350
|
-
section_lines = section[
|
|
351
|
-
|
|
380
|
+
section_lines = section["line_count"]
|
|
381
|
+
|
|
352
382
|
# If adding this section would exceed limit, finish current chunk
|
|
353
383
|
if current_chunk_lines + section_lines > max_lines and current_chunk_sections:
|
|
354
|
-
suggestions.append(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
384
|
+
suggestions.append(
|
|
385
|
+
{
|
|
386
|
+
"sections": current_chunk_sections.copy(),
|
|
387
|
+
"total_lines": current_chunk_lines,
|
|
388
|
+
"start_line": current_chunk_sections[0]["start_line"],
|
|
389
|
+
"end_line": current_chunk_sections[-1]["end_line"],
|
|
390
|
+
}
|
|
391
|
+
)
|
|
392
|
+
|
|
361
393
|
# Start new chunk
|
|
362
394
|
current_chunk_sections = [section]
|
|
363
395
|
current_chunk_lines = section_lines
|
|
@@ -365,14 +397,16 @@ def _suggest_section_splits(sections: List[Dict[str, Any]], max_lines: int = 500
|
|
|
365
397
|
# Add section to current chunk
|
|
366
398
|
current_chunk_sections.append(section)
|
|
367
399
|
current_chunk_lines += section_lines
|
|
368
|
-
|
|
400
|
+
|
|
369
401
|
# Add final chunk
|
|
370
402
|
if current_chunk_sections:
|
|
371
|
-
suggestions.append(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
403
|
+
suggestions.append(
|
|
404
|
+
{
|
|
405
|
+
"sections": current_chunk_sections,
|
|
406
|
+
"total_lines": current_chunk_lines,
|
|
407
|
+
"start_line": current_chunk_sections[0]["start_line"],
|
|
408
|
+
"end_line": current_chunk_sections[-1]["end_line"],
|
|
409
|
+
}
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return suggestions
|
|
@@ -47,7 +47,9 @@ def validate_evaluation_output(
|
|
|
47
47
|
"concerns",
|
|
48
48
|
]
|
|
49
49
|
|
|
50
|
-
has_evaluation_content = any(
|
|
50
|
+
has_evaluation_content = any(
|
|
51
|
+
marker in content_lower for marker in evaluation_markers
|
|
52
|
+
)
|
|
51
53
|
if not has_evaluation_content:
|
|
52
54
|
return (
|
|
53
55
|
False,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adversarial-workflow
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Multi-stage AI code review system preventing phantom work - Author/Evaluator pattern
|
|
5
5
|
Author: Fredrik Matheson
|
|
6
6
|
License: MIT
|
|
@@ -35,6 +35,10 @@ Dynamic: license-file
|
|
|
35
35
|
|
|
36
36
|
# Adversarial Workflow
|
|
37
37
|
|
|
38
|
+
[](https://pypi.org/project/adversarial-workflow/)
|
|
39
|
+
[](https://www.python.org/downloads/)
|
|
40
|
+
[](https://opensource.org/licenses/MIT)
|
|
41
|
+
|
|
38
42
|
**A multi-stage AI code review system that makes your code better**
|
|
39
43
|
|
|
40
44
|
Evaluate proposals, sort out ideas, and prevent "phantom work" (AI claiming to implement but not delivering) through adversarial verification using independent review stages. A battle-tested workflow from the [thematic-cuts](https://github.com/movito/thematic-cuts) project that achieved 96.9% test pass rate improvement.
|
|
@@ -51,6 +55,31 @@ Evaluate proposals, sort out ideas, and prevent "phantom work" (AI claiming to i
|
|
|
51
55
|
- 🎯 **Tool-agnostic**: Use with Claude Code, Cursor, Aider, manual coding, or any workflow
|
|
52
56
|
- ✨ **Interactive onboarding**: Guided setup wizard gets you started in <5 minutes
|
|
53
57
|
|
|
58
|
+
## What's New in v0.6.0
|
|
59
|
+
|
|
60
|
+
🔌 **Plugin Architecture** - Define custom evaluators without modifying the package:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Create a custom evaluator
|
|
64
|
+
mkdir -p .adversarial/evaluators
|
|
65
|
+
cat > .adversarial/evaluators/athena.yml << 'EOF'
|
|
66
|
+
name: athena
|
|
67
|
+
description: Knowledge evaluation using Gemini 2.5 Pro
|
|
68
|
+
model: gemini-2.5-pro
|
|
69
|
+
api_key_env: GEMINI_API_KEY
|
|
70
|
+
prompt: |
|
|
71
|
+
You are Athena, a knowledge evaluation specialist...
|
|
72
|
+
EOF
|
|
73
|
+
|
|
74
|
+
# Use it immediately
|
|
75
|
+
adversarial athena docs/research-plan.md
|
|
76
|
+
|
|
77
|
+
# List all available evaluators
|
|
78
|
+
adversarial list-evaluators
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
See [Custom Evaluators](#custom-evaluators) for full documentation, or check the [CHANGELOG](CHANGELOG.md) for complete release history.
|
|
82
|
+
|
|
54
83
|
## Prerequisites
|
|
55
84
|
|
|
56
85
|
Before installing, ensure you have:
|
|
@@ -856,12 +885,13 @@ From the [thematic-cuts](https://github.com/movito/thematic-cuts) project:
|
|
|
856
885
|
|
|
857
886
|
## Documentation
|
|
858
887
|
|
|
859
|
-
- **
|
|
888
|
+
- **[Custom Evaluators Guide](docs/CUSTOM_EVALUATORS.md)**: Create project-specific evaluators
|
|
889
|
+
- **[Integration Guide](docs/INTEGRATION-GUIDE.md)**: Detailed integration strategies
|
|
890
|
+
- **[CHANGELOG](CHANGELOG.md)**: Release history and version notes
|
|
891
|
+
- **Interaction Patterns**: How Author-Evaluator collaboration works
|
|
860
892
|
- **Token Optimization**: Detailed Aider configuration guide
|
|
861
893
|
- **Workflow Phases**: Step-by-step guide for each phase
|
|
862
894
|
- **Troubleshooting**: Common issues and solutions
|
|
863
|
-
- **Examples**: Real integration scenarios
|
|
864
|
-
- **Terminology**: Official standards for Author/Reviewer concepts
|
|
865
895
|
|
|
866
896
|
See `docs/` directory for comprehensive guides.
|
|
867
897
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
adversarial_workflow/__init__.py,sha256=
|
|
1
|
+
adversarial_workflow/__init__.py,sha256=moTEp6nKU5F4B1YnJaSBmwhptkDP0ST5n--2hak9PRc,596
|
|
2
2
|
adversarial_workflow/__main__.py,sha256=Ibb0CngDCh4mpCe8Zxnf3kyKnMddBxQy2JAk_kfTUMQ,119
|
|
3
|
-
adversarial_workflow/cli.py,sha256=
|
|
4
|
-
adversarial_workflow/evaluators/__init__.py,sha256=
|
|
3
|
+
adversarial_workflow/cli.py,sha256=I9LM6MnfW-m1kXbF202l50-SeidLCyyF0Sk0sR-UFqk,110743
|
|
4
|
+
adversarial_workflow/evaluators/__init__.py,sha256=A9ZKUmjSMfyvEu6jDzYAFLxfkt_OQ4RGA10Bv_eO2i4,1267
|
|
5
5
|
adversarial_workflow/evaluators/builtins.py,sha256=u5LokYLe8ruEW2tunhOQaNSkpcZ9Ee2IeTkaC0dZDSY,1102
|
|
6
6
|
adversarial_workflow/evaluators/config.py,sha256=05qYPIiIpCxXBVJzs70WQQLxi8I7MedfhE_oydXEcq0,1520
|
|
7
|
-
adversarial_workflow/evaluators/discovery.py,sha256=
|
|
8
|
-
adversarial_workflow/evaluators/runner.py,sha256=
|
|
7
|
+
adversarial_workflow/evaluators/discovery.py,sha256=a8qTUsuJRdPUcVbt1zzEgLixmvJbHA7WfnYSNm8V5OY,6834
|
|
8
|
+
adversarial_workflow/evaluators/runner.py,sha256=JPVeigjGF2fRDVJLcGyDEuy9pCIp-LjmVAZyucMbdCU,9310
|
|
9
9
|
adversarial_workflow/templates/.aider.conf.yml.template,sha256=jT2jWIgsnmS3HLhoQWMTO3GV07bUcsT2keYw60jqiDw,183
|
|
10
10
|
adversarial_workflow/templates/.env.example.template,sha256=TmTlcgz44uZqIbqgXqdfHMl-0vVn96F_EGNohClFkb8,1821
|
|
11
11
|
adversarial_workflow/templates/README.template,sha256=FQAMPO99eIt_kgQfwhGHcrK736rm_MEvWSbPnqBSjAE,1349
|
|
@@ -23,11 +23,11 @@ adversarial_workflow/templates/agent-context/current-state.json.template,sha256=
|
|
|
23
23
|
adversarial_workflow/utils/__init__.py,sha256=Pnm-a_jqoMVOxHdvVWXeVrL0IKI-zkY7EAdbQmZAkSI,352
|
|
24
24
|
adversarial_workflow/utils/colors.py,sha256=uRrG6KfIDBLo0F5_vPwms9NCm9-x8YXBiyZ4naCr868,160
|
|
25
25
|
adversarial_workflow/utils/config.py,sha256=NBoC_-YYukEVo6BgpX2cDyeqV-3tnn_sHNU9L1AuSLQ,1341
|
|
26
|
-
adversarial_workflow/utils/file_splitter.py,sha256
|
|
27
|
-
adversarial_workflow/utils/validation.py,sha256=
|
|
28
|
-
adversarial_workflow-0.6.
|
|
29
|
-
adversarial_workflow-0.6.
|
|
30
|
-
adversarial_workflow-0.6.
|
|
31
|
-
adversarial_workflow-0.6.
|
|
32
|
-
adversarial_workflow-0.6.
|
|
33
|
-
adversarial_workflow-0.6.
|
|
26
|
+
adversarial_workflow/utils/file_splitter.py,sha256=-zSWgAZ71DfX6dBu15Y4M84NBbJzq-0ENktbBEp9zvQ,12409
|
|
27
|
+
adversarial_workflow/utils/validation.py,sha256=ZiJxtm03kJXicfFTt0QZwpc9V_D8PkDOVYrJEDsafQI,2202
|
|
28
|
+
adversarial_workflow-0.6.2.dist-info/licenses/LICENSE,sha256=M-dOQlre-NmicyPa55hYOJUW8roGpCKEgtq-z0z1KCA,1073
|
|
29
|
+
adversarial_workflow-0.6.2.dist-info/METADATA,sha256=zdFc4h-9XPcgg6mn0SCnJNg3VFhYl7XyxI0egYhU2fY,29955
|
|
30
|
+
adversarial_workflow-0.6.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
31
|
+
adversarial_workflow-0.6.2.dist-info/entry_points.txt,sha256=9H-iZ-yF1uKZ8P0G1suc6kWR0NvK7uPZJbhN7nvt1sE,62
|
|
32
|
+
adversarial_workflow-0.6.2.dist-info/top_level.txt,sha256=8irutNxLRjUbTlzfAibIpz7_ovkkF2h8ES69NQpv24c,21
|
|
33
|
+
adversarial_workflow-0.6.2.dist-info/RECORD,,
|
{adversarial_workflow-0.6.0.dist-info → adversarial_workflow-0.6.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{adversarial_workflow-0.6.0.dist-info → adversarial_workflow-0.6.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|