adversarial-workflow 0.6.3__tar.gz → 0.6.4__tar.gz

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.
Files changed (54) hide show
  1. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/PKG-INFO +2 -2
  2. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/__init__.py +1 -1
  3. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/cli.py +49 -131
  4. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/evaluators/discovery.py +5 -15
  5. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/evaluators/runner.py +2 -7
  6. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/utils/config.py +1 -3
  7. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/utils/file_splitter.py +2 -6
  8. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/utils/validation.py +1 -3
  9. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow.egg-info/PKG-INFO +2 -2
  10. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/pyproject.toml +2 -2
  11. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_cli_dynamic_commands.py +4 -12
  12. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_env_loading.py +1 -3
  13. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_evaluate.py +3 -9
  14. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_evaluator_discovery.py +1 -3
  15. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_list_evaluators.py +2 -6
  16. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/LICENSE +0 -0
  17. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/README.md +0 -0
  18. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/__main__.py +0 -0
  19. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/evaluators/__init__.py +0 -0
  20. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/evaluators/builtins.py +0 -0
  21. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/evaluators/config.py +0 -0
  22. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/.aider.conf.yml.template +0 -0
  23. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/.env.example.template +0 -0
  24. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/README.template +0 -0
  25. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/agent-context/AGENT-SYSTEM-GUIDE.md +0 -0
  26. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/agent-context/README.md.template +0 -0
  27. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/agent-context/agent-handoffs-minimal.json.template +0 -0
  28. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/agent-context/agent-handoffs.json.template +0 -0
  29. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/agent-context/current-state.json.template +0 -0
  30. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/config.yml.template +0 -0
  31. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/evaluate_plan.sh.template +0 -0
  32. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/example-task.md.template +0 -0
  33. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/proofread_content.sh.template +0 -0
  34. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/review_implementation.sh.template +0 -0
  35. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/validate_tests.sh.template +0 -0
  36. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/utils/__init__.py +0 -0
  37. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow/utils/colors.py +0 -0
  38. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow.egg-info/SOURCES.txt +0 -0
  39. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow.egg-info/dependency_links.txt +0 -0
  40. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow.egg-info/entry_points.txt +0 -0
  41. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow.egg-info/requires.txt +0 -0
  42. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/adversarial_workflow.egg-info/top_level.txt +0 -0
  43. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/setup.cfg +0 -0
  44. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/setup.py +0 -0
  45. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_cli.py +0 -0
  46. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_config.py +0 -0
  47. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_evaluator_config.py +0 -0
  48. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_evaluator_runner.py +0 -0
  49. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_file_splitter.py +0 -0
  50. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_python_version.py +0 -0
  51. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_scripts_project.py +0 -0
  52. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_split_command.py +0 -0
  53. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_timeout_integration.py +0 -0
  54. {adversarial_workflow-0.6.3 → adversarial_workflow-0.6.4}/tests/test_utils_validation.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adversarial-workflow
3
- Version: 0.6.3
4
- Summary: Multi-stage AI code review system preventing phantom work - Author/Evaluator pattern
3
+ Version: 0.6.4
4
+ Summary: Multi-stage AI evaluation system for task plans, code review, and test validation
5
5
  Author: Fredrik Matheson
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/movito/adversarial-workflow
@@ -12,7 +12,7 @@ Usage:
12
12
  adversarial validate "pytest"
13
13
  """
14
14
 
15
- __version__ = "0.6.2"
15
+ __version__ = "0.6.4"
16
16
  __author__ = "Fredrik Matheson"
17
17
  __license__ = "MIT"
18
18
 
@@ -29,7 +29,7 @@ from typing import Dict, List, Optional, Tuple
29
29
  import yaml
30
30
  from dotenv import dotenv_values, load_dotenv
31
31
 
32
- __version__ = "0.6.2"
32
+ __version__ = "0.6.4"
33
33
 
34
34
  # ANSI color codes for better output
35
35
  RESET = "\033[0m"
@@ -180,7 +180,9 @@ def create_env_file_interactive(
180
180
  env_content += "# DO NOT COMMIT THIS FILE\n\n"
181
181
 
182
182
  if anthropic_key:
183
- env_content += f"# Anthropic API Key (Claude 3.5 Sonnet)\nANTHROPIC_API_KEY={anthropic_key}\n\n"
183
+ env_content += (
184
+ f"# Anthropic API Key (Claude 3.5 Sonnet)\nANTHROPIC_API_KEY={anthropic_key}\n\n"
185
+ )
184
186
 
185
187
  if openai_key:
186
188
  env_content += f"# OpenAI API Key (GPT-4o)\nOPENAI_API_KEY={openai_key}\n\n"
@@ -255,9 +257,7 @@ def init_interactive(project_path: str = ".") -> int:
255
257
  ],
256
258
  )
257
259
 
258
- anthropic_key = prompt_user(
259
- "Paste your Anthropic API key (or Enter to skip)", secret=True
260
- )
260
+ anthropic_key = prompt_user("Paste your Anthropic API key (or Enter to skip)", secret=True)
261
261
 
262
262
  if anthropic_key:
263
263
  is_valid, message = validate_api_key(anthropic_key, "anthropic")
@@ -281,9 +281,7 @@ def init_interactive(project_path: str = ".") -> int:
281
281
  ],
282
282
  )
283
283
 
284
- openai_key = prompt_user(
285
- "Paste your OpenAI API key (or Enter to skip)", secret=True
286
- )
284
+ openai_key = prompt_user("Paste your OpenAI API key (or Enter to skip)", secret=True)
287
285
 
288
286
  if openai_key:
289
287
  is_valid, message = validate_api_key(openai_key, "openai")
@@ -613,9 +611,7 @@ def init(project_path: str = ".", interactive: bool = True) -> int:
613
611
  print(" 3. Then run: adversarial init")
614
612
  print()
615
613
  print(f"{BOLD}HELP:{RESET}")
616
- print(
617
- " New to git? https://git-scm.com/book/en/v2/Getting-Started-Installing-Git"
618
- )
614
+ print(" New to git? https://git-scm.com/book/en/v2/Getting-Started-Installing-Git")
619
615
  return 1
620
616
 
621
617
  # Pre-flight validation: Check package integrity
@@ -649,9 +645,7 @@ def init(project_path: str = ".", interactive: bool = True) -> int:
649
645
  print(f" • {template}")
650
646
  print()
651
647
  print(f"{BOLD}FIX:{RESET}")
652
- print(
653
- " 1. Report this issue: https://github.com/movito/adversarial-workflow/issues"
654
- )
648
+ print(" 1. Report this issue: https://github.com/movito/adversarial-workflow/issues")
655
649
  print(
656
650
  " 2. Or try reinstalling: pip install --upgrade --force-reinstall adversarial-workflow"
657
651
  )
@@ -1030,13 +1024,9 @@ def check() -> int:
1030
1024
  else:
1031
1025
  status_parts = []
1032
1026
  if error_count > 0:
1033
- status_parts.append(
1034
- f"{error_count} error" + ("s" if error_count != 1 else "")
1035
- )
1027
+ status_parts.append(f"{error_count} error" + ("s" if error_count != 1 else ""))
1036
1028
  if warning_count > 0:
1037
- status_parts.append(
1038
- f"{warning_count} warning" + ("s" if warning_count != 1 else "")
1039
- )
1029
+ status_parts.append(f"{warning_count} warning" + ("s" if warning_count != 1 else ""))
1040
1030
  if info_count > 0:
1041
1031
  status_parts.append(f"{info_count} info")
1042
1032
 
@@ -1088,20 +1078,14 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1088
1078
  # Helper functions for tracking check results
1089
1079
  def check_pass(category: str, message: str, detail: str = None):
1090
1080
  nonlocal passed
1091
- results[category].append(
1092
- {"status": "pass", "message": message, "detail": detail}
1093
- )
1081
+ results[category].append({"status": "pass", "message": message, "detail": detail})
1094
1082
  if not json_output:
1095
1083
  print(f" {GREEN}✅{RESET} {message}")
1096
1084
  passed += 1
1097
1085
 
1098
- def check_warn(
1099
- category: str, message: str, detail: str = None, recommendation: str = None
1100
- ):
1086
+ def check_warn(category: str, message: str, detail: str = None, recommendation: str = None):
1101
1087
  nonlocal warnings
1102
- results[category].append(
1103
- {"status": "warn", "message": message, "detail": detail}
1104
- )
1088
+ results[category].append({"status": "warn", "message": message, "detail": detail})
1105
1089
  if not json_output:
1106
1090
  print(f" {YELLOW}⚠️{RESET} {message}")
1107
1091
  if detail and verbose:
@@ -1110,9 +1094,7 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1110
1094
  recommendations.append(recommendation)
1111
1095
  warnings += 1
1112
1096
 
1113
- def check_fail(
1114
- category: str, message: str, fix: str = None, recommendation: str = None
1115
- ):
1097
+ def check_fail(category: str, message: str, fix: str = None, recommendation: str = None):
1116
1098
  nonlocal errors
1117
1099
  results[category].append({"status": "fail", "message": message, "fix": fix})
1118
1100
  if not json_output:
@@ -1124,9 +1106,7 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1124
1106
  errors += 1
1125
1107
 
1126
1108
  def check_info(category: str, message: str, detail: str = None):
1127
- results[category].append(
1128
- {"status": "info", "message": message, "detail": detail}
1129
- )
1109
+ results[category].append({"status": "info", "message": message, "detail": detail})
1130
1110
  if not json_output:
1131
1111
  print(f" {CYAN}ℹ️{RESET} {message}")
1132
1112
  if detail and verbose:
@@ -1258,23 +1238,13 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1258
1238
  )
1259
1239
  if git_status.returncode == 0:
1260
1240
  modified = len(
1261
- [
1262
- l
1263
- for l in git_status.stdout.splitlines()
1264
- if l.startswith(" M")
1265
- ]
1241
+ [l for l in git_status.stdout.splitlines() if l.startswith(" M")]
1266
1242
  )
1267
1243
  untracked = len(
1268
- [
1269
- l
1270
- for l in git_status.stdout.splitlines()
1271
- if l.startswith("??")
1272
- ]
1244
+ [l for l in git_status.stdout.splitlines() if l.startswith("??")]
1273
1245
  )
1274
1246
  if modified == 0 and untracked == 0:
1275
- check_pass(
1276
- "dependencies", f"Git: {version} (working tree clean)"
1277
- )
1247
+ check_pass("dependencies", f"Git: {version} (working tree clean)")
1278
1248
  else:
1279
1249
  check_info(
1280
1250
  "dependencies",
@@ -1311,11 +1281,7 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1311
1281
  aider_version = subprocess.run(
1312
1282
  ["aider", "--version"], capture_output=True, text=True, timeout=2
1313
1283
  )
1314
- version = (
1315
- aider_version.stdout.strip()
1316
- if aider_version.returncode == 0
1317
- else "unknown"
1318
- )
1284
+ version = aider_version.stdout.strip() if aider_version.returncode == 0 else "unknown"
1319
1285
  check_pass("dependencies", f"Aider: {version} (functional)")
1320
1286
  except:
1321
1287
  check_pass("dependencies", "Aider: installed")
@@ -1459,9 +1425,7 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1459
1425
  json.load(f)
1460
1426
  check_pass("agent_coordination", "current-state.json - Valid JSON")
1461
1427
  except json.JSONDecodeError as e:
1462
- check_fail(
1463
- "agent_coordination", f"current-state.json - Invalid JSON: {e}"
1464
- )
1428
+ check_fail("agent_coordination", f"current-state.json - Invalid JSON: {e}")
1465
1429
  else:
1466
1430
  check_info("agent_coordination", "current-state.json not found (optional)")
1467
1431
 
@@ -1505,9 +1469,7 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1505
1469
  with open(script_path) as f:
1506
1470
  content = f.read()
1507
1471
  if "#!/bin/bash" in content or "#!/usr/bin/env bash" in content:
1508
- check_pass(
1509
- "workflow_scripts", f"{script_name} - Executable, valid"
1510
- )
1472
+ check_pass("workflow_scripts", f"{script_name} - Executable, valid")
1511
1473
  else:
1512
1474
  check_warn(
1513
1475
  "workflow_scripts",
@@ -1781,9 +1743,7 @@ def verify_token_count(task_file: str, log_file: str) -> None:
1781
1743
  f" Difference: {expected_tokens - actual_tokens:,} tokens ({100 - int(actual_tokens/expected_tokens*100)}% less)"
1782
1744
  )
1783
1745
  print()
1784
- print(
1785
- f"{BOLD}Note:{RESET} Large files may not be fully processed by evaluator."
1786
- )
1746
+ print(f"{BOLD}Note:{RESET} Large files may not be fully processed by evaluator.")
1787
1747
  print(f" Consider splitting into smaller documents (<1,000 lines).")
1788
1748
  print()
1789
1749
 
@@ -1964,9 +1924,7 @@ def evaluate(task_file: str) -> int:
1964
1924
  print(f"{RED}❌ ERROR: OpenAI rate limit exceeded{RESET}")
1965
1925
  print()
1966
1926
  print(f"{BOLD}WHY:{RESET}")
1967
- print(
1968
- " Your task file is too large for your OpenAI organization's rate limit"
1969
- )
1927
+ print(" Your task file is too large for your OpenAI organization's rate limit")
1970
1928
  print()
1971
1929
 
1972
1930
  # Extract file size for helpful message
@@ -2013,9 +1971,7 @@ def evaluate(task_file: str) -> int:
2013
1971
  print()
2014
1972
  print(f"{BOLD}FIX:{RESET}")
2015
1973
  print(" Option 1 (RECOMMENDED): Use WSL (Windows Subsystem for Linux)")
2016
- print(
2017
- " 1. Install WSL: https://learn.microsoft.com/windows/wsl/install"
2018
- )
1974
+ print(" 1. Install WSL: https://learn.microsoft.com/windows/wsl/install")
2019
1975
  print(" 2. Open WSL terminal")
2020
1976
  print(" 3. Reinstall package in WSL: pip install adversarial-workflow")
2021
1977
  print()
@@ -2186,9 +2142,7 @@ def validate(test_command: Optional[str] = None) -> int:
2186
2142
  return 1
2187
2143
 
2188
2144
  try:
2189
- result = subprocess.run(
2190
- [script, test_command], timeout=600
2191
- ) # 10 minutes for tests
2145
+ result = subprocess.run([script, test_command], timeout=600) # 10 minutes for tests
2192
2146
  except subprocess.TimeoutExpired:
2193
2147
  print(f"{RED}❌ ERROR: Test validation timed out (>10 minutes){RESET}")
2194
2148
  return 1
@@ -2241,9 +2195,7 @@ def select_agent_template() -> Dict[str, str]:
2241
2195
  elif choice == "3":
2242
2196
  print()
2243
2197
  print(f"{CYAN}Custom Template URL:{RESET}")
2244
- print(
2245
- " Example: https://raw.githubusercontent.com/user/repo/main/agent-handoffs.json"
2246
- )
2198
+ print(" Example: https://raw.githubusercontent.com/user/repo/main/agent-handoffs.json")
2247
2199
  print()
2248
2200
  url = prompt_user("Template URL")
2249
2201
  if url:
@@ -2283,14 +2235,10 @@ def fetch_agent_template(url: str, template_type: str = "standard") -> Optional[
2283
2235
  with open(template_path, "r") as f:
2284
2236
  return f.read()
2285
2237
  except Exception as e:
2286
- print(
2287
- f"{RED}❌ ERROR: Could not read {template_type} template: {e}{RESET}"
2288
- )
2238
+ print(f"{RED}❌ ERROR: Could not read {template_type} template: {e}{RESET}")
2289
2239
  return None
2290
2240
  else:
2291
- print(
2292
- f"{RED}❌ ERROR: {template_type} template not found in package{RESET}"
2293
- )
2241
+ print(f"{RED}❌ ERROR: {template_type} template not found in package{RESET}")
2294
2242
  return None
2295
2243
 
2296
2244
  elif template_type == "custom" and url:
@@ -2390,9 +2338,11 @@ def agent_onboard(project_path: str = ".") -> int:
2390
2338
  return 0
2391
2339
 
2392
2340
  # 3. Interactive questions (4 max)
2393
- use_delegation = prompt_user(
2394
- "Use delegation/tasks/ structure? (recommended)", "Y"
2395
- ).lower() in ["y", "yes", ""]
2341
+ use_delegation = prompt_user("Use delegation/tasks/ structure? (recommended)", "Y").lower() in [
2342
+ "y",
2343
+ "yes",
2344
+ "",
2345
+ ]
2396
2346
 
2397
2347
  organize_docs = prompt_user("Organize root docs into docs/?", "n").lower() in [
2398
2348
  "y",
@@ -2469,9 +2419,7 @@ def agent_onboard(project_path: str = ".") -> int:
2469
2419
  print(
2470
2420
  f" {CYAN}ℹ️{RESET} Original tasks/ preserved (remove manually if desired)"
2471
2421
  )
2472
- print(
2473
- f" {CYAN}ℹ️{RESET} Rollback: rm -rf tasks && mv tasks.backup tasks"
2474
- )
2422
+ print(f" {CYAN}ℹ️{RESET} Rollback: rm -rf tasks && mv tasks.backup tasks")
2475
2423
 
2476
2424
  except Exception as e:
2477
2425
  print(f" {RED}❌{RESET} Migration failed: {e}")
@@ -2485,9 +2433,7 @@ def agent_onboard(project_path: str = ".") -> int:
2485
2433
  print(f"{BOLD}Documentation Organization:{RESET}")
2486
2434
 
2487
2435
  # Find markdown files in root
2488
- root_docs = [
2489
- f for f in os.listdir(".") if f.endswith(".md") and not f.startswith(".")
2490
- ]
2436
+ root_docs = [f for f in os.listdir(".") if f.endswith(".md") and not f.startswith(".")]
2491
2437
 
2492
2438
  if len(root_docs) > 0:
2493
2439
  print(f" Found {len(root_docs)} markdown file(s) in root")
@@ -2507,9 +2453,7 @@ def agent_onboard(project_path: str = ".") -> int:
2507
2453
  moved_count += 1
2508
2454
 
2509
2455
  if moved_count > 0:
2510
- print(
2511
- f" {GREEN}✅{RESET} Organized {moved_count} doc(s) into docs/"
2512
- )
2456
+ print(f" {GREEN}✅{RESET} Organized {moved_count} doc(s) into docs/")
2513
2457
  else:
2514
2458
  print(f" {CYAN}ℹ️{RESET} No docs needed organizing")
2515
2459
 
@@ -2562,9 +2506,7 @@ def agent_onboard(project_path: str = ".") -> int:
2562
2506
  print(f" {RED}❌{RESET} Failed to fetch agent template")
2563
2507
  return 1
2564
2508
  else:
2565
- print(
2566
- f" {CYAN}ℹ️{RESET} Skipped agent-handoffs.json (manual setup requested)"
2567
- )
2509
+ print(f" {CYAN}ℹ️{RESET} Skipped agent-handoffs.json (manual setup requested)")
2568
2510
 
2569
2511
  # Render current-state.json
2570
2512
  current_state_template = templates_dir / "current-state.json.template"
@@ -2579,9 +2521,7 @@ def agent_onboard(project_path: str = ".") -> int:
2579
2521
  # Render README.md
2580
2522
  readme_template = templates_dir / "README.md.template"
2581
2523
  if readme_template.exists():
2582
- render_template(
2583
- str(readme_template), ".agent-context/README.md", template_vars
2584
- )
2524
+ render_template(str(readme_template), ".agent-context/README.md", template_vars)
2585
2525
  print(f" {GREEN}✅{RESET} Created .agent-context/README.md")
2586
2526
 
2587
2527
  # Copy AGENT-SYSTEM-GUIDE.md if it exists and isn't already there
@@ -2620,9 +2560,7 @@ def agent_onboard(project_path: str = ".") -> int:
2620
2560
 
2621
2561
  except Exception as e:
2622
2562
  print(f" {YELLOW}⚠️{RESET} Could not update config: {e}")
2623
- print(
2624
- f" Manually set task_directory: delegation/tasks/ in .adversarial/config.yml"
2625
- )
2563
+ print(f" Manually set task_directory: delegation/tasks/ in .adversarial/config.yml")
2626
2564
 
2627
2565
  # 9. Update .gitignore
2628
2566
  print()
@@ -2676,9 +2614,7 @@ def agent_onboard(project_path: str = ".") -> int:
2676
2614
  verification_checks.append((f"current-state.json invalid: {e}", False))
2677
2615
 
2678
2616
  # Check directories exist
2679
- verification_checks.append(
2680
- (".agent-context/ exists", os.path.exists(".agent-context"))
2681
- )
2617
+ verification_checks.append((".agent-context/ exists", os.path.exists(".agent-context")))
2682
2618
 
2683
2619
  if use_delegation:
2684
2620
  verification_checks.append(
@@ -2783,9 +2719,7 @@ def split(
2783
2719
 
2784
2720
  # Check if splitting is recommended
2785
2721
  if lines <= max_lines:
2786
- print(
2787
- f"{GREEN}✅ File is under recommended limit ({max_lines} lines){RESET}"
2788
- )
2722
+ print(f"{GREEN}✅ File is under recommended limit ({max_lines} lines){RESET}")
2789
2723
  print("No splitting needed.")
2790
2724
  return 0
2791
2725
 
@@ -2803,9 +2737,7 @@ def split(
2803
2737
  splits = split_by_phases(content)
2804
2738
  print(f"\n💡 Suggested splits (by phases):")
2805
2739
  else:
2806
- print(
2807
- f"{RED}Error: Unknown strategy '{strategy}'. Use 'sections' or 'phases'.{RESET}"
2808
- )
2740
+ print(f"{RED}Error: Unknown strategy '{strategy}'. Use 'sections' or 'phases'.{RESET}")
2809
2741
  return 1
2810
2742
 
2811
2743
  # Display split preview
@@ -2977,26 +2909,18 @@ For more information: https://github.com/movito/adversarial-workflow
2977
2909
  subparsers.add_parser("doctor", help="Alias for 'check'")
2978
2910
 
2979
2911
  # health command
2980
- health_parser = subparsers.add_parser(
2981
- "health", help="Comprehensive system health check"
2982
- )
2912
+ health_parser = subparsers.add_parser("health", help="Comprehensive system health check")
2983
2913
  health_parser.add_argument(
2984
2914
  "--verbose", "-v", action="store_true", help="Show detailed diagnostics"
2985
2915
  )
2986
- health_parser.add_argument(
2987
- "--json", action="store_true", help="Output in JSON format"
2988
- )
2916
+ health_parser.add_argument("--json", action="store_true", help="Output in JSON format")
2989
2917
 
2990
2918
  # agent command (with subcommands)
2991
2919
  agent_parser = subparsers.add_parser("agent", help="Agent coordination commands")
2992
- agent_subparsers = agent_parser.add_subparsers(
2993
- dest="agent_subcommand", help="Agent subcommand"
2994
- )
2920
+ agent_subparsers = agent_parser.add_subparsers(dest="agent_subcommand", help="Agent subcommand")
2995
2921
 
2996
2922
  # agent onboard subcommand
2997
- onboard_parser = agent_subparsers.add_parser(
2998
- "onboard", help="Set up agent coordination system"
2999
- )
2923
+ onboard_parser = agent_subparsers.add_parser("onboard", help="Set up agent coordination system")
3000
2924
  onboard_parser.add_argument(
3001
2925
  "--path", default=".", help="Project path (default: current directory)"
3002
2926
  )
@@ -3005,12 +2929,8 @@ For more information: https://github.com/movito/adversarial-workflow
3005
2929
  subparsers.add_parser("review", help="Run Phase 3: Code review")
3006
2930
 
3007
2931
  # validate command
3008
- validate_parser = subparsers.add_parser(
3009
- "validate", help="Run Phase 4: Test validation"
3010
- )
3011
- validate_parser.add_argument(
3012
- "test_command", nargs="?", help="Test command to run (optional)"
3013
- )
2932
+ validate_parser = subparsers.add_parser("validate", help="Run Phase 4: Test validation")
2933
+ validate_parser.add_argument("test_command", nargs="?", help="Test command to run (optional)")
3014
2934
 
3015
2935
  # split command
3016
2936
  split_parser = subparsers.add_parser(
@@ -3056,9 +2976,7 @@ For more information: https://github.com/movito/adversarial-workflow
3056
2976
  # Only warn for user-defined evaluators, not built-ins
3057
2977
  # Built-in conflicts are intentional (e.g., 'review' command vs 'review' evaluator)
3058
2978
  if getattr(config, "source", None) != "builtin":
3059
- logger.warning(
3060
- "Evaluator '%s' conflicts with CLI command; skipping", name
3061
- )
2979
+ logger.warning("Evaluator '%s' conflicts with CLI command; skipping", name)
3062
2980
  # Mark as registered to prevent alias re-registration attempts
3063
2981
  registered_configs.add(id(config))
3064
2982
  continue
@@ -51,9 +51,7 @@ def parse_evaluator_yaml(yml_file: Path) -> EvaluatorConfig:
51
51
 
52
52
  # Ensure parsed data is a dict (YAML can parse scalars, lists, etc.)
53
53
  if not isinstance(data, dict):
54
- raise EvaluatorParseError(
55
- f"YAML must be a mapping, got {type(data).__name__}: {yml_file}"
56
- )
54
+ raise EvaluatorParseError(f"YAML must be a mapping, got {type(data).__name__}: {yml_file}")
57
55
 
58
56
  # Validate required fields exist
59
57
  required = [
@@ -91,9 +89,7 @@ def parse_evaluator_yaml(yml_file: Path) -> EvaluatorConfig:
91
89
  elif isinstance(aliases, str):
92
90
  data["aliases"] = [aliases]
93
91
  elif not isinstance(aliases, list):
94
- raise EvaluatorParseError(
95
- f"aliases must be string or list, got {type(aliases).__name__}"
96
- )
92
+ raise EvaluatorParseError(f"aliases must be string or list, got {type(aliases).__name__}")
97
93
 
98
94
  # Validate alias names - must be strings with valid format
99
95
  for alias in data.get("aliases", []):
@@ -131,18 +127,14 @@ def parse_evaluator_yaml(yml_file: Path) -> EvaluatorConfig:
131
127
  # Check for bool before int (bool is subclass of int in Python)
132
128
  # YAML parses 'yes'/'true' as True, 'no'/'false' as False
133
129
  if isinstance(timeout, bool):
134
- raise EvaluatorParseError(
135
- f"Field 'timeout' must be an integer, got bool: {timeout!r}"
136
- )
130
+ raise EvaluatorParseError(f"Field 'timeout' must be an integer, got bool: {timeout!r}")
137
131
  if not isinstance(timeout, int):
138
132
  raise EvaluatorParseError(
139
133
  f"Field 'timeout' must be an integer, got {type(timeout).__name__}: {timeout!r}"
140
134
  )
141
135
  # timeout=0 is invalid (does not disable timeout - use a large value instead)
142
136
  if timeout <= 0:
143
- raise EvaluatorParseError(
144
- f"Field 'timeout' must be positive (> 0), got {timeout}"
145
- )
137
+ raise EvaluatorParseError(f"Field 'timeout' must be positive (> 0), got {timeout}")
146
138
  if timeout > 600:
147
139
  logger.warning(
148
140
  "Timeout %ds exceeds maximum (600s), clamping to 600s in %s",
@@ -167,9 +159,7 @@ def parse_evaluator_yaml(yml_file: Path) -> EvaluatorConfig:
167
159
  }
168
160
  unknown = set(data.keys()) - known_fields
169
161
  if unknown:
170
- logger.warning(
171
- "Unknown fields in %s: %s", yml_file.name, ", ".join(sorted(unknown))
172
- )
162
+ logger.warning("Unknown fields in %s: %s", yml_file.name, ", ".join(sorted(unknown)))
173
163
 
174
164
  # Build filtered data dict
175
165
  filtered_data = {k: v for k, v in data.items() if k in known_fields}
@@ -227,10 +227,7 @@ def _execute_script(
227
227
 
228
228
  # Validate output
229
229
  file_basename = Path(file_path).stem
230
- log_file = (
231
- Path(project_config["log_directory"])
232
- / f"{file_basename}-{config.output_suffix}.md"
233
- )
230
+ log_file = Path(project_config["log_directory"]) / f"{file_basename}-{config.output_suffix}.md"
234
231
 
235
232
  is_valid, verdict, message = validate_evaluation_output(str(log_file))
236
233
 
@@ -241,9 +238,7 @@ def _execute_script(
241
238
  return _report_verdict(verdict, log_file, config)
242
239
 
243
240
 
244
- def _report_verdict(
245
- verdict: str | None, log_file: Path, config: EvaluatorConfig
246
- ) -> int:
241
+ def _report_verdict(verdict: str | None, log_file: Path, config: EvaluatorConfig) -> int:
247
242
  """Report the evaluation verdict to terminal."""
248
243
  print()
249
244
  if verdict == "APPROVED":
@@ -24,9 +24,7 @@ def load_config(config_path: str = ".adversarial/config.yml") -> dict[str, Any]:
24
24
  with open(config_path) as f:
25
25
  file_config = yaml.safe_load(f) or {}
26
26
  if not isinstance(file_config, dict):
27
- raise ValueError(
28
- f"Config file must be a mapping, got {type(file_config).__name__}"
29
- )
27
+ raise ValueError(f"Config file must be a mapping, got {type(file_config).__name__}")
30
28
  config.update(file_config)
31
29
 
32
30
  # Override with environment variables
@@ -141,9 +141,7 @@ def split_by_sections(content: str, max_lines: int = 500) -> List[Dict[str, Any]
141
141
  is_section_boundary = re.match(r"^#+\s+", line.strip())
142
142
  approaching_limit = len(current_split_lines) >= max_lines * 0.8
143
143
 
144
- if len(current_split_lines) >= max_lines or (
145
- is_section_boundary and approaching_limit
146
- ):
144
+ if len(current_split_lines) >= max_lines or (is_section_boundary and approaching_limit):
147
145
  # Create split
148
146
  split_content = "\n".join(current_split_lines)
149
147
  splits.append(
@@ -318,9 +316,7 @@ def split_at_lines(content: str, line_numbers: List[int]) -> List[Dict[str, Any]
318
316
  return splits
319
317
 
320
318
 
321
- def generate_split_files(
322
- original: str, splits: List[Dict[str, Any]], output_dir: str
323
- ) -> List[str]:
319
+ def generate_split_files(original: str, splits: List[Dict[str, Any]], output_dir: str) -> List[str]:
324
320
  """Generate split files with metadata and cross-references.
325
321
 
326
322
  Args:
@@ -47,9 +47,7 @@ def validate_evaluation_output(
47
47
  "concerns",
48
48
  ]
49
49
 
50
- has_evaluation_content = any(
51
- marker in content_lower for marker in evaluation_markers
52
- )
50
+ has_evaluation_content = any(marker in content_lower for marker in evaluation_markers)
53
51
  if not has_evaluation_content:
54
52
  return (
55
53
  False,
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adversarial-workflow
3
- Version: 0.6.3
4
- Summary: Multi-stage AI code review system preventing phantom work - Author/Evaluator pattern
3
+ Version: 0.6.4
4
+ Summary: Multi-stage AI evaluation system for task plans, code review, and test validation
5
5
  Author: Fredrik Matheson
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/movito/adversarial-workflow
@@ -5,9 +5,9 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "adversarial-workflow"
7
7
 
8
- version = "0.6.3"
8
+ version = "0.6.4"
9
9
 
10
- description = "Multi-stage AI code review system preventing phantom work - Author/Evaluator pattern"
10
+ description = "Multi-stage AI evaluation system for task plans, code review, and test validation"
11
11
  readme = "README.md"
12
12
  authors = [
13
13
  {name = "Fredrik Matheson"}
@@ -78,9 +78,7 @@ output_suffix: CUSTOM-TEST
78
78
  monkeypatch.chdir(tmp_path)
79
79
 
80
80
  result = run_cli(["--help"], cwd=tmp_path)
81
- assert (
82
- "custom" in result.stdout
83
- ), f"'custom' not found in help output:\n{result.stdout}"
81
+ assert "custom" in result.stdout, f"'custom' not found in help output:\n{result.stdout}"
84
82
  assert "Custom test evaluator" in result.stdout
85
83
 
86
84
  def test_multiple_local_evaluators_in_help(self, tmp_path, monkeypatch, run_cli):
@@ -351,9 +349,7 @@ class TestBackwardsCompatibility:
351
349
  class TestGracefulDegradation:
352
350
  """Test graceful degradation on errors."""
353
351
 
354
- def test_help_works_without_local_evaluators_dir(
355
- self, tmp_path, monkeypatch, run_cli
356
- ):
352
+ def test_help_works_without_local_evaluators_dir(self, tmp_path, monkeypatch, run_cli):
357
353
  """CLI help works even without .adversarial/evaluators/ directory."""
358
354
  adv_dir = tmp_path / ".adversarial"
359
355
  adv_dir.mkdir(parents=True)
@@ -422,9 +418,7 @@ class TestReviewCommandBackwardsCompatibility:
422
418
  # Review should NOT have --timeout flag (that's for evaluators)
423
419
  assert "--timeout" not in result.stdout
424
420
 
425
- def test_review_command_not_overridden_by_evaluator(
426
- self, tmp_path, monkeypatch, run_cli
427
- ):
421
+ def test_review_command_not_overridden_by_evaluator(self, tmp_path, monkeypatch, run_cli):
428
422
  """Review command cannot be overridden by local evaluator."""
429
423
  adv_dir = tmp_path / ".adversarial"
430
424
  adv_dir.mkdir(parents=True)
@@ -492,9 +486,7 @@ aliases:
492
486
  assert "--path" in result_init.stdout
493
487
  assert "--interactive" in result_init.stdout
494
488
 
495
- def test_evaluator_with_conflicting_name_and_alias(
496
- self, tmp_path, monkeypatch, run_cli
497
- ):
489
+ def test_evaluator_with_conflicting_name_and_alias(self, tmp_path, monkeypatch, run_cli):
498
490
  """Evaluator with conflicting name doesn't crash when alias is processed."""
499
491
  adv_dir = tmp_path / ".adversarial"
500
492
  adv_dir.mkdir(parents=True)
@@ -94,9 +94,7 @@ class TestCheckEnvCount:
94
94
  """
95
95
  # Create .env with 3 variables
96
96
  (tmp_path / ".env").write_text(
97
- "OPENAI_API_KEY=sk-test\n"
98
- "ANTHROPIC_API_KEY=ant-test\n"
99
- "CUSTOM_KEY=custom-value\n"
97
+ "OPENAI_API_KEY=sk-test\n" "ANTHROPIC_API_KEY=ant-test\n" "CUSTOM_KEY=custom-value\n"
100
98
  )
101
99
 
102
100
  # Remove keys from environment to isolate test
@@ -49,9 +49,7 @@ class TestEvaluate:
49
49
 
50
50
  @patch("shutil.which")
51
51
  @patch("adversarial_workflow.cli.load_config")
52
- def test_evaluate_aider_not_found(
53
- self, mock_load_config, mock_which, tmp_path, capsys
54
- ):
52
+ def test_evaluate_aider_not_found(self, mock_load_config, mock_which, tmp_path, capsys):
55
53
  """Test evaluate when aider is not available."""
56
54
  # Create a test file
57
55
  task_file = tmp_path / "test_task.md"
@@ -411,9 +409,7 @@ class TestVerifyTokenCount:
411
409
 
412
410
  @patch("adversarial_workflow.cli.estimate_file_tokens")
413
411
  @patch("adversarial_workflow.cli.extract_token_count_from_log")
414
- def test_verify_token_count_normal(
415
- self, mock_extract, mock_estimate, tmp_path, capsys
416
- ):
412
+ def test_verify_token_count_normal(self, mock_extract, mock_estimate, tmp_path, capsys):
417
413
  """Test normal token count verification."""
418
414
  task_file = tmp_path / "task.md"
419
415
  log_file = tmp_path / "log.md"
@@ -433,9 +429,7 @@ class TestVerifyTokenCount:
433
429
 
434
430
  @patch("adversarial_workflow.cli.estimate_file_tokens")
435
431
  @patch("adversarial_workflow.cli.extract_token_count_from_log")
436
- def test_verify_token_count_low_warning(
437
- self, mock_extract, mock_estimate, tmp_path, capsys
438
- ):
432
+ def test_verify_token_count_low_warning(self, mock_extract, mock_estimate, tmp_path, capsys):
439
433
  """Test token count verification warns on suspiciously low usage."""
440
434
  task_file = tmp_path / "task.md"
441
435
  log_file = tmp_path / "log.md"
@@ -388,9 +388,7 @@ fallback_model: yes
388
388
  """
389
389
  )
390
390
 
391
- with pytest.raises(
392
- EvaluatorParseError, match="'fallback_model' must be a string"
393
- ):
391
+ with pytest.raises(EvaluatorParseError, match="'fallback_model' must be a string"):
394
392
  parse_evaluator_yaml(yml)
395
393
 
396
394
  def test_parse_with_valid_timeout(self, tmp_path):
@@ -53,9 +53,7 @@ aliases:
53
53
  result = run_cli(["--help"])
54
54
  assert "list-evaluators" in result.stdout
55
55
 
56
- def test_list_evaluators_skips_alias_duplicates(
57
- self, tmp_path, monkeypatch, run_cli
58
- ):
56
+ def test_list_evaluators_skips_alias_duplicates(self, tmp_path, monkeypatch, run_cli):
59
57
  """Aliases do not cause duplicate entries in output."""
60
58
  eval_dir = tmp_path / ".adversarial" / "evaluators"
61
59
  eval_dir.mkdir(parents=True)
@@ -80,9 +78,7 @@ aliases:
80
78
  assert result.stdout.count("Knowledge evaluation") == 1
81
79
  assert "aliases: knowledge, research" in result.stdout
82
80
 
83
- def test_list_evaluators_shows_version_if_not_default(
84
- self, tmp_path, monkeypatch, run_cli
85
- ):
81
+ def test_list_evaluators_shows_version_if_not_default(self, tmp_path, monkeypatch, run_cli):
86
82
  """Shows version only when it differs from default 1.0.0."""
87
83
  eval_dir = tmp_path / ".adversarial" / "evaluators"
88
84
  eval_dir.mkdir(parents=True)