adversarial-workflow 0.6.2__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.2 → adversarial_workflow-0.6.4}/PKG-INFO +26 -4
  2. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/README.md +24 -2
  3. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/__init__.py +1 -1
  4. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/__main__.py +1 -0
  5. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/cli.py +85 -137
  6. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/evaluators/config.py +2 -0
  7. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/evaluators/discovery.py +29 -9
  8. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/evaluators/runner.py +2 -7
  9. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/utils/config.py +1 -3
  10. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/utils/file_splitter.py +2 -6
  11. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/utils/validation.py +1 -3
  12. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow.egg-info/PKG-INFO +26 -4
  13. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow.egg-info/SOURCES.txt +1 -0
  14. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/pyproject.toml +2 -2
  15. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/tests/test_cli_dynamic_commands.py +50 -3
  16. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/tests/test_config.py +20 -14
  17. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/tests/test_env_loading.py +1 -3
  18. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/tests/test_evaluate.py +35 -30
  19. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/tests/test_evaluator_discovery.py +204 -3
  20. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/tests/test_file_splitter.py +1 -0
  21. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/tests/test_python_version.py +1 -0
  22. adversarial_workflow-0.6.4/tests/test_timeout_integration.py +406 -0
  23. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/LICENSE +0 -0
  24. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/evaluators/__init__.py +0 -0
  25. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/evaluators/builtins.py +0 -0
  26. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/.aider.conf.yml.template +0 -0
  27. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/.env.example.template +0 -0
  28. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/README.template +0 -0
  29. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/agent-context/AGENT-SYSTEM-GUIDE.md +0 -0
  30. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/agent-context/README.md.template +0 -0
  31. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/agent-context/agent-handoffs-minimal.json.template +0 -0
  32. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/agent-context/agent-handoffs.json.template +0 -0
  33. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/agent-context/current-state.json.template +0 -0
  34. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/config.yml.template +0 -0
  35. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/evaluate_plan.sh.template +0 -0
  36. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/example-task.md.template +0 -0
  37. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/proofread_content.sh.template +0 -0
  38. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/review_implementation.sh.template +0 -0
  39. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/templates/validate_tests.sh.template +0 -0
  40. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/utils/__init__.py +0 -0
  41. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow/utils/colors.py +0 -0
  42. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow.egg-info/dependency_links.txt +0 -0
  43. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow.egg-info/entry_points.txt +0 -0
  44. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow.egg-info/requires.txt +0 -0
  45. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/adversarial_workflow.egg-info/top_level.txt +0 -0
  46. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/setup.cfg +0 -0
  47. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/setup.py +0 -0
  48. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/tests/test_cli.py +0 -0
  49. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/tests/test_evaluator_config.py +0 -0
  50. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/tests/test_evaluator_runner.py +0 -0
  51. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/tests/test_list_evaluators.py +0 -0
  52. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/tests/test_scripts_project.py +0 -0
  53. {adversarial_workflow-0.6.2 → adversarial_workflow-0.6.4}/tests/test_split_command.py +0 -0
  54. {adversarial_workflow-0.6.2 → 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.2
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
@@ -55,9 +55,30 @@ Evaluate proposals, sort out ideas, and prevent "phantom work" (AI claiming to i
55
55
  - 🎯 **Tool-agnostic**: Use with Claude Code, Cursor, Aider, manual coding, or any workflow
56
56
  - ✨ **Interactive onboarding**: Guided setup wizard gets you started in <5 minutes
57
57
 
58
- ## What's New in v0.6.0
58
+ ## What's New in v0.6.3
59
59
 
60
- 🔌 **Plugin Architecture** - Define custom evaluators without modifying the package:
60
+ ### Upgrade
61
+
62
+ ```bash
63
+ pip install --upgrade adversarial-workflow
64
+ ```
65
+
66
+ ### v0.6.3 - Configurable Timeouts
67
+
68
+ - **Per-evaluator timeout**: Add `timeout: 300` to evaluator YAML for slow models like Mistral Large
69
+ - **CLI override**: Use `--timeout 400` to override YAML config on-the-fly
70
+ - **Timeout logging**: See which timeout source is used (CLI/YAML/default)
71
+ - **Safety limits**: Maximum 600 seconds to prevent runaway processes
72
+
73
+ ### v0.6.2 - .env Loading & Stability
74
+
75
+ - **Automatic .env loading**: API keys in `.env` files are now loaded at CLI startup
76
+ - **Custom evaluator support**: Evaluators using `api_key_env: GEMINI_API_KEY` (or other keys) now work with `.env` files
77
+ - **Better diagnostics**: `adversarial check` correctly reports the number of variables loaded from `.env`
78
+
79
+ ### v0.6.0 - Plugin Architecture
80
+
81
+ 🔌 **Custom Evaluators** - Define your own evaluators without modifying the package:
61
82
 
62
83
  ```bash
63
84
  # Create a custom evaluator
@@ -459,6 +480,7 @@ Starting with v0.6.0, you can define project-specific evaluators without modifyi
459
480
  | `aliases` | No | Alternative command names |
460
481
  | `log_prefix` | No | CLI output prefix |
461
482
  | `fallback_model` | No | Fallback model if primary fails |
483
+ | `timeout` | No | Timeout in seconds (default: 180, max: 600) |
462
484
  | `version` | No | Evaluator version (default: 1.0.0) |
463
485
 
464
486
  ### Listing Available Evaluators
@@ -20,9 +20,30 @@ Evaluate proposals, sort out ideas, and prevent "phantom work" (AI claiming to i
20
20
  - 🎯 **Tool-agnostic**: Use with Claude Code, Cursor, Aider, manual coding, or any workflow
21
21
  - ✨ **Interactive onboarding**: Guided setup wizard gets you started in <5 minutes
22
22
 
23
- ## What's New in v0.6.0
23
+ ## What's New in v0.6.3
24
24
 
25
- 🔌 **Plugin Architecture** - Define custom evaluators without modifying the package:
25
+ ### Upgrade
26
+
27
+ ```bash
28
+ pip install --upgrade adversarial-workflow
29
+ ```
30
+
31
+ ### v0.6.3 - Configurable Timeouts
32
+
33
+ - **Per-evaluator timeout**: Add `timeout: 300` to evaluator YAML for slow models like Mistral Large
34
+ - **CLI override**: Use `--timeout 400` to override YAML config on-the-fly
35
+ - **Timeout logging**: See which timeout source is used (CLI/YAML/default)
36
+ - **Safety limits**: Maximum 600 seconds to prevent runaway processes
37
+
38
+ ### v0.6.2 - .env Loading & Stability
39
+
40
+ - **Automatic .env loading**: API keys in `.env` files are now loaded at CLI startup
41
+ - **Custom evaluator support**: Evaluators using `api_key_env: GEMINI_API_KEY` (or other keys) now work with `.env` files
42
+ - **Better diagnostics**: `adversarial check` correctly reports the number of variables loaded from `.env`
43
+
44
+ ### v0.6.0 - Plugin Architecture
45
+
46
+ 🔌 **Custom Evaluators** - Define your own evaluators without modifying the package:
26
47
 
27
48
  ```bash
28
49
  # Create a custom evaluator
@@ -424,6 +445,7 @@ Starting with v0.6.0, you can define project-specific evaluators without modifyi
424
445
  | `aliases` | No | Alternative command names |
425
446
  | `log_prefix` | No | CLI output prefix |
426
447
  | `fallback_model` | No | Fallback model if primary fails |
448
+ | `timeout` | No | Timeout in seconds (default: 180, max: 600) |
427
449
  | `version` | No | Evaluator version (default: 1.0.0) |
428
450
 
429
451
  ### Listing Available Evaluators
@@ -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
 
@@ -1,4 +1,5 @@
1
1
  """Allow execution via python -m adversarial_workflow."""
2
+
2
3
  from .cli import main
3
4
 
4
5
  if __name__ == "__main__":
@@ -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")
@@ -322,16 +320,20 @@ def init_interactive(project_path: str = ".") -> int:
322
320
  f"{GREEN}✅ Setup Complete!{RESET}",
323
321
  [
324
322
  "Created:",
325
- " ✓ .env (with your API keys - added to .gitignore)"
326
- if (anthropic_key or openai_key)
327
- else " ⚠️ .env (skipped - no API keys provided)",
323
+ (
324
+ " ✓ .env (with your API keys - added to .gitignore)"
325
+ if (anthropic_key or openai_key)
326
+ else " ⚠️ .env (skipped - no API keys provided)"
327
+ ),
328
328
  " ✓ .adversarial/config.yml",
329
329
  " ✓ .adversarial/scripts/ (3 workflow scripts)",
330
330
  " ✓ .aider.conf.yml (aider configuration)",
331
331
  "",
332
- "Your configuration:"
333
- if (anthropic_key or openai_key)
334
- else "Configuration (no API keys yet):",
332
+ (
333
+ "Your configuration:"
334
+ if (anthropic_key or openai_key)
335
+ else "Configuration (no API keys yet):"
336
+ ),
335
337
  f" Author (implementation): {'Claude 3.5 Sonnet (Anthropic)' if anthropic_key else 'GPT-4o (OpenAI)' if openai_key else 'Not configured'}",
336
338
  f" Evaluator: {'GPT-4o (OpenAI)' if openai_key else 'Claude 3.5 Sonnet (Anthropic)' if anthropic_key else 'Not configured'}",
337
339
  f" Cost per workflow: {'~$0.02-0.10' if (anthropic_key and openai_key) else '~$0.05-0.15' if (anthropic_key or openai_key) else 'N/A'}",
@@ -609,9 +611,7 @@ def init(project_path: str = ".", interactive: bool = True) -> int:
609
611
  print(" 3. Then run: adversarial init")
610
612
  print()
611
613
  print(f"{BOLD}HELP:{RESET}")
612
- print(
613
- " New to git? https://git-scm.com/book/en/v2/Getting-Started-Installing-Git"
614
- )
614
+ print(" New to git? https://git-scm.com/book/en/v2/Getting-Started-Installing-Git")
615
615
  return 1
616
616
 
617
617
  # Pre-flight validation: Check package integrity
@@ -645,9 +645,7 @@ def init(project_path: str = ".", interactive: bool = True) -> int:
645
645
  print(f" • {template}")
646
646
  print()
647
647
  print(f"{BOLD}FIX:{RESET}")
648
- print(
649
- " 1. Report this issue: https://github.com/movito/adversarial-workflow/issues"
650
- )
648
+ print(" 1. Report this issue: https://github.com/movito/adversarial-workflow/issues")
651
649
  print(
652
650
  " 2. Or try reinstalling: pip install --upgrade --force-reinstall adversarial-workflow"
653
651
  )
@@ -1026,13 +1024,9 @@ def check() -> int:
1026
1024
  else:
1027
1025
  status_parts = []
1028
1026
  if error_count > 0:
1029
- status_parts.append(
1030
- f"{error_count} error" + ("s" if error_count != 1 else "")
1031
- )
1027
+ status_parts.append(f"{error_count} error" + ("s" if error_count != 1 else ""))
1032
1028
  if warning_count > 0:
1033
- status_parts.append(
1034
- f"{warning_count} warning" + ("s" if warning_count != 1 else "")
1035
- )
1029
+ status_parts.append(f"{warning_count} warning" + ("s" if warning_count != 1 else ""))
1036
1030
  if info_count > 0:
1037
1031
  status_parts.append(f"{info_count} info")
1038
1032
 
@@ -1084,20 +1078,14 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1084
1078
  # Helper functions for tracking check results
1085
1079
  def check_pass(category: str, message: str, detail: str = None):
1086
1080
  nonlocal passed
1087
- results[category].append(
1088
- {"status": "pass", "message": message, "detail": detail}
1089
- )
1081
+ results[category].append({"status": "pass", "message": message, "detail": detail})
1090
1082
  if not json_output:
1091
1083
  print(f" {GREEN}✅{RESET} {message}")
1092
1084
  passed += 1
1093
1085
 
1094
- def check_warn(
1095
- category: str, message: str, detail: str = None, recommendation: str = None
1096
- ):
1086
+ def check_warn(category: str, message: str, detail: str = None, recommendation: str = None):
1097
1087
  nonlocal warnings
1098
- results[category].append(
1099
- {"status": "warn", "message": message, "detail": detail}
1100
- )
1088
+ results[category].append({"status": "warn", "message": message, "detail": detail})
1101
1089
  if not json_output:
1102
1090
  print(f" {YELLOW}⚠️{RESET} {message}")
1103
1091
  if detail and verbose:
@@ -1106,9 +1094,7 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1106
1094
  recommendations.append(recommendation)
1107
1095
  warnings += 1
1108
1096
 
1109
- def check_fail(
1110
- category: str, message: str, fix: str = None, recommendation: str = None
1111
- ):
1097
+ def check_fail(category: str, message: str, fix: str = None, recommendation: str = None):
1112
1098
  nonlocal errors
1113
1099
  results[category].append({"status": "fail", "message": message, "fix": fix})
1114
1100
  if not json_output:
@@ -1120,9 +1106,7 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1120
1106
  errors += 1
1121
1107
 
1122
1108
  def check_info(category: str, message: str, detail: str = None):
1123
- results[category].append(
1124
- {"status": "info", "message": message, "detail": detail}
1125
- )
1109
+ results[category].append({"status": "info", "message": message, "detail": detail})
1126
1110
  if not json_output:
1127
1111
  print(f" {CYAN}ℹ️{RESET} {message}")
1128
1112
  if detail and verbose:
@@ -1254,23 +1238,13 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1254
1238
  )
1255
1239
  if git_status.returncode == 0:
1256
1240
  modified = len(
1257
- [
1258
- l
1259
- for l in git_status.stdout.splitlines()
1260
- if l.startswith(" M")
1261
- ]
1241
+ [l for l in git_status.stdout.splitlines() if l.startswith(" M")]
1262
1242
  )
1263
1243
  untracked = len(
1264
- [
1265
- l
1266
- for l in git_status.stdout.splitlines()
1267
- if l.startswith("??")
1268
- ]
1244
+ [l for l in git_status.stdout.splitlines() if l.startswith("??")]
1269
1245
  )
1270
1246
  if modified == 0 and untracked == 0:
1271
- check_pass(
1272
- "dependencies", f"Git: {version} (working tree clean)"
1273
- )
1247
+ check_pass("dependencies", f"Git: {version} (working tree clean)")
1274
1248
  else:
1275
1249
  check_info(
1276
1250
  "dependencies",
@@ -1307,11 +1281,7 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1307
1281
  aider_version = subprocess.run(
1308
1282
  ["aider", "--version"], capture_output=True, text=True, timeout=2
1309
1283
  )
1310
- version = (
1311
- aider_version.stdout.strip()
1312
- if aider_version.returncode == 0
1313
- else "unknown"
1314
- )
1284
+ version = aider_version.stdout.strip() if aider_version.returncode == 0 else "unknown"
1315
1285
  check_pass("dependencies", f"Aider: {version} (functional)")
1316
1286
  except:
1317
1287
  check_pass("dependencies", "Aider: installed")
@@ -1455,9 +1425,7 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1455
1425
  json.load(f)
1456
1426
  check_pass("agent_coordination", "current-state.json - Valid JSON")
1457
1427
  except json.JSONDecodeError as e:
1458
- check_fail(
1459
- "agent_coordination", f"current-state.json - Invalid JSON: {e}"
1460
- )
1428
+ check_fail("agent_coordination", f"current-state.json - Invalid JSON: {e}")
1461
1429
  else:
1462
1430
  check_info("agent_coordination", "current-state.json not found (optional)")
1463
1431
 
@@ -1501,9 +1469,7 @@ def health(verbose: bool = False, json_output: bool = False) -> int:
1501
1469
  with open(script_path) as f:
1502
1470
  content = f.read()
1503
1471
  if "#!/bin/bash" in content or "#!/usr/bin/env bash" in content:
1504
- check_pass(
1505
- "workflow_scripts", f"{script_name} - Executable, valid"
1506
- )
1472
+ check_pass("workflow_scripts", f"{script_name} - Executable, valid")
1507
1473
  else:
1508
1474
  check_warn(
1509
1475
  "workflow_scripts",
@@ -1777,9 +1743,7 @@ def verify_token_count(task_file: str, log_file: str) -> None:
1777
1743
  f" Difference: {expected_tokens - actual_tokens:,} tokens ({100 - int(actual_tokens/expected_tokens*100)}% less)"
1778
1744
  )
1779
1745
  print()
1780
- print(
1781
- f"{BOLD}Note:{RESET} Large files may not be fully processed by evaluator."
1782
- )
1746
+ print(f"{BOLD}Note:{RESET} Large files may not be fully processed by evaluator.")
1783
1747
  print(f" Consider splitting into smaller documents (<1,000 lines).")
1784
1748
  print()
1785
1749
 
@@ -1960,9 +1924,7 @@ def evaluate(task_file: str) -> int:
1960
1924
  print(f"{RED}❌ ERROR: OpenAI rate limit exceeded{RESET}")
1961
1925
  print()
1962
1926
  print(f"{BOLD}WHY:{RESET}")
1963
- print(
1964
- " Your task file is too large for your OpenAI organization's rate limit"
1965
- )
1927
+ print(" Your task file is too large for your OpenAI organization's rate limit")
1966
1928
  print()
1967
1929
 
1968
1930
  # Extract file size for helpful message
@@ -2009,9 +1971,7 @@ def evaluate(task_file: str) -> int:
2009
1971
  print()
2010
1972
  print(f"{BOLD}FIX:{RESET}")
2011
1973
  print(" Option 1 (RECOMMENDED): Use WSL (Windows Subsystem for Linux)")
2012
- print(
2013
- " 1. Install WSL: https://learn.microsoft.com/windows/wsl/install"
2014
- )
1974
+ print(" 1. Install WSL: https://learn.microsoft.com/windows/wsl/install")
2015
1975
  print(" 2. Open WSL terminal")
2016
1976
  print(" 3. Reinstall package in WSL: pip install adversarial-workflow")
2017
1977
  print()
@@ -2182,9 +2142,7 @@ def validate(test_command: Optional[str] = None) -> int:
2182
2142
  return 1
2183
2143
 
2184
2144
  try:
2185
- result = subprocess.run(
2186
- [script, test_command], timeout=600
2187
- ) # 10 minutes for tests
2145
+ result = subprocess.run([script, test_command], timeout=600) # 10 minutes for tests
2188
2146
  except subprocess.TimeoutExpired:
2189
2147
  print(f"{RED}❌ ERROR: Test validation timed out (>10 minutes){RESET}")
2190
2148
  return 1
@@ -2237,9 +2195,7 @@ def select_agent_template() -> Dict[str, str]:
2237
2195
  elif choice == "3":
2238
2196
  print()
2239
2197
  print(f"{CYAN}Custom Template URL:{RESET}")
2240
- print(
2241
- " Example: https://raw.githubusercontent.com/user/repo/main/agent-handoffs.json"
2242
- )
2198
+ print(" Example: https://raw.githubusercontent.com/user/repo/main/agent-handoffs.json")
2243
2199
  print()
2244
2200
  url = prompt_user("Template URL")
2245
2201
  if url:
@@ -2279,9 +2235,7 @@ def fetch_agent_template(url: str, template_type: str = "standard") -> Optional[
2279
2235
  with open(template_path, "r") as f:
2280
2236
  return f.read()
2281
2237
  except Exception as e:
2282
- print(
2283
- f"{RED}❌ ERROR: Could not read {template_type} template: {e}{RESET}"
2284
- )
2238
+ print(f"{RED}❌ ERROR: Could not read {template_type} template: {e}{RESET}")
2285
2239
  return None
2286
2240
  else:
2287
2241
  print(f"{RED}❌ ERROR: {template_type} template not found in package{RESET}")
@@ -2384,9 +2338,11 @@ def agent_onboard(project_path: str = ".") -> int:
2384
2338
  return 0
2385
2339
 
2386
2340
  # 3. Interactive questions (4 max)
2387
- use_delegation = prompt_user(
2388
- "Use delegation/tasks/ structure? (recommended)", "Y"
2389
- ).lower() in ["y", "yes", ""]
2341
+ use_delegation = prompt_user("Use delegation/tasks/ structure? (recommended)", "Y").lower() in [
2342
+ "y",
2343
+ "yes",
2344
+ "",
2345
+ ]
2390
2346
 
2391
2347
  organize_docs = prompt_user("Organize root docs into docs/?", "n").lower() in [
2392
2348
  "y",
@@ -2463,9 +2419,7 @@ def agent_onboard(project_path: str = ".") -> int:
2463
2419
  print(
2464
2420
  f" {CYAN}ℹ️{RESET} Original tasks/ preserved (remove manually if desired)"
2465
2421
  )
2466
- print(
2467
- f" {CYAN}ℹ️{RESET} Rollback: rm -rf tasks && mv tasks.backup tasks"
2468
- )
2422
+ print(f" {CYAN}ℹ️{RESET} Rollback: rm -rf tasks && mv tasks.backup tasks")
2469
2423
 
2470
2424
  except Exception as e:
2471
2425
  print(f" {RED}❌{RESET} Migration failed: {e}")
@@ -2479,9 +2433,7 @@ def agent_onboard(project_path: str = ".") -> int:
2479
2433
  print(f"{BOLD}Documentation Organization:{RESET}")
2480
2434
 
2481
2435
  # Find markdown files in root
2482
- root_docs = [
2483
- f for f in os.listdir(".") if f.endswith(".md") and not f.startswith(".")
2484
- ]
2436
+ root_docs = [f for f in os.listdir(".") if f.endswith(".md") and not f.startswith(".")]
2485
2437
 
2486
2438
  if len(root_docs) > 0:
2487
2439
  print(f" Found {len(root_docs)} markdown file(s) in root")
@@ -2501,9 +2453,7 @@ def agent_onboard(project_path: str = ".") -> int:
2501
2453
  moved_count += 1
2502
2454
 
2503
2455
  if moved_count > 0:
2504
- print(
2505
- f" {GREEN}✅{RESET} Organized {moved_count} doc(s) into docs/"
2506
- )
2456
+ print(f" {GREEN}✅{RESET} Organized {moved_count} doc(s) into docs/")
2507
2457
  else:
2508
2458
  print(f" {CYAN}ℹ️{RESET} No docs needed organizing")
2509
2459
 
@@ -2556,9 +2506,7 @@ def agent_onboard(project_path: str = ".") -> int:
2556
2506
  print(f" {RED}❌{RESET} Failed to fetch agent template")
2557
2507
  return 1
2558
2508
  else:
2559
- print(
2560
- f" {CYAN}ℹ️{RESET} Skipped agent-handoffs.json (manual setup requested)"
2561
- )
2509
+ print(f" {CYAN}ℹ️{RESET} Skipped agent-handoffs.json (manual setup requested)")
2562
2510
 
2563
2511
  # Render current-state.json
2564
2512
  current_state_template = templates_dir / "current-state.json.template"
@@ -2573,9 +2521,7 @@ def agent_onboard(project_path: str = ".") -> int:
2573
2521
  # Render README.md
2574
2522
  readme_template = templates_dir / "README.md.template"
2575
2523
  if readme_template.exists():
2576
- render_template(
2577
- str(readme_template), ".agent-context/README.md", template_vars
2578
- )
2524
+ render_template(str(readme_template), ".agent-context/README.md", template_vars)
2579
2525
  print(f" {GREEN}✅{RESET} Created .agent-context/README.md")
2580
2526
 
2581
2527
  # Copy AGENT-SYSTEM-GUIDE.md if it exists and isn't already there
@@ -2614,9 +2560,7 @@ def agent_onboard(project_path: str = ".") -> int:
2614
2560
 
2615
2561
  except Exception as e:
2616
2562
  print(f" {YELLOW}⚠️{RESET} Could not update config: {e}")
2617
- print(
2618
- f" Manually set task_directory: delegation/tasks/ in .adversarial/config.yml"
2619
- )
2563
+ print(f" Manually set task_directory: delegation/tasks/ in .adversarial/config.yml")
2620
2564
 
2621
2565
  # 9. Update .gitignore
2622
2566
  print()
@@ -2670,9 +2614,7 @@ def agent_onboard(project_path: str = ".") -> int:
2670
2614
  verification_checks.append((f"current-state.json invalid: {e}", False))
2671
2615
 
2672
2616
  # Check directories exist
2673
- verification_checks.append(
2674
- (".agent-context/ exists", os.path.exists(".agent-context"))
2675
- )
2617
+ verification_checks.append((".agent-context/ exists", os.path.exists(".agent-context")))
2676
2618
 
2677
2619
  if use_delegation:
2678
2620
  verification_checks.append(
@@ -2777,9 +2719,7 @@ def split(
2777
2719
 
2778
2720
  # Check if splitting is recommended
2779
2721
  if lines <= max_lines:
2780
- print(
2781
- f"{GREEN}✅ File is under recommended limit ({max_lines} lines){RESET}"
2782
- )
2722
+ print(f"{GREEN}✅ File is under recommended limit ({max_lines} lines){RESET}")
2783
2723
  print("No splitting needed.")
2784
2724
  return 0
2785
2725
 
@@ -2797,9 +2737,7 @@ def split(
2797
2737
  splits = split_by_phases(content)
2798
2738
  print(f"\n💡 Suggested splits (by phases):")
2799
2739
  else:
2800
- print(
2801
- f"{RED}Error: Unknown strategy '{strategy}'. Use 'sections' or 'phases'.{RESET}"
2802
- )
2740
+ print(f"{RED}Error: Unknown strategy '{strategy}'. Use 'sections' or 'phases'.{RESET}")
2803
2741
  return 1
2804
2742
 
2805
2743
  # Display split preview
@@ -2971,26 +2909,18 @@ For more information: https://github.com/movito/adversarial-workflow
2971
2909
  subparsers.add_parser("doctor", help="Alias for 'check'")
2972
2910
 
2973
2911
  # health command
2974
- health_parser = subparsers.add_parser(
2975
- "health", help="Comprehensive system health check"
2976
- )
2912
+ health_parser = subparsers.add_parser("health", help="Comprehensive system health check")
2977
2913
  health_parser.add_argument(
2978
2914
  "--verbose", "-v", action="store_true", help="Show detailed diagnostics"
2979
2915
  )
2980
- health_parser.add_argument(
2981
- "--json", action="store_true", help="Output in JSON format"
2982
- )
2916
+ health_parser.add_argument("--json", action="store_true", help="Output in JSON format")
2983
2917
 
2984
2918
  # agent command (with subcommands)
2985
2919
  agent_parser = subparsers.add_parser("agent", help="Agent coordination commands")
2986
- agent_subparsers = agent_parser.add_subparsers(
2987
- dest="agent_subcommand", help="Agent subcommand"
2988
- )
2920
+ agent_subparsers = agent_parser.add_subparsers(dest="agent_subcommand", help="Agent subcommand")
2989
2921
 
2990
2922
  # agent onboard subcommand
2991
- onboard_parser = agent_subparsers.add_parser(
2992
- "onboard", help="Set up agent coordination system"
2993
- )
2923
+ onboard_parser = agent_subparsers.add_parser("onboard", help="Set up agent coordination system")
2994
2924
  onboard_parser.add_argument(
2995
2925
  "--path", default=".", help="Project path (default: current directory)"
2996
2926
  )
@@ -2999,12 +2929,8 @@ For more information: https://github.com/movito/adversarial-workflow
2999
2929
  subparsers.add_parser("review", help="Run Phase 3: Code review")
3000
2930
 
3001
2931
  # validate command
3002
- validate_parser = subparsers.add_parser(
3003
- "validate", help="Run Phase 4: Test validation"
3004
- )
3005
- validate_parser.add_argument(
3006
- "test_command", nargs="?", help="Test command to run (optional)"
3007
- )
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)")
3008
2934
 
3009
2935
  # split command
3010
2936
  split_parser = subparsers.add_parser(
@@ -3050,9 +2976,7 @@ For more information: https://github.com/movito/adversarial-workflow
3050
2976
  # Only warn for user-defined evaluators, not built-ins
3051
2977
  # Built-in conflicts are intentional (e.g., 'review' command vs 'review' evaluator)
3052
2978
  if getattr(config, "source", None) != "builtin":
3053
- logger.warning(
3054
- "Evaluator '%s' conflicts with CLI command; skipping", name
3055
- )
2979
+ logger.warning("Evaluator '%s' conflicts with CLI command; skipping", name)
3056
2980
  # Mark as registered to prevent alias re-registration attempts
3057
2981
  registered_configs.add(id(config))
3058
2982
  continue
@@ -3082,8 +3006,8 @@ For more information: https://github.com/movito/adversarial-workflow
3082
3006
  "--timeout",
3083
3007
  "-t",
3084
3008
  type=int,
3085
- default=180,
3086
- help="Timeout in seconds (default: 180)",
3009
+ default=None,
3010
+ help="Timeout in seconds (default: from evaluator config or 180, max: 600)",
3087
3011
  )
3088
3012
  # Store config for later execution
3089
3013
  eval_parser.set_defaults(evaluator_config=config)
@@ -3096,10 +3020,34 @@ For more information: https://github.com/movito/adversarial-workflow
3096
3020
 
3097
3021
  # Check for evaluator command first (has evaluator_config attribute)
3098
3022
  if hasattr(args, "evaluator_config"):
3023
+ # Determine timeout: CLI flag > YAML config > default (180s)
3024
+ if args.timeout is not None:
3025
+ timeout = args.timeout
3026
+ source = "CLI override"
3027
+ elif args.evaluator_config.timeout != 180:
3028
+ timeout = args.evaluator_config.timeout
3029
+ source = "evaluator config"
3030
+ else:
3031
+ timeout = args.evaluator_config.timeout # 180 (default)
3032
+ source = "default"
3033
+
3034
+ # Validate CLI timeout (consistent with YAML validation)
3035
+ if timeout <= 0:
3036
+ print(f"{RED}Error: Timeout must be positive (> 0), got {timeout}{RESET}")
3037
+ return 1
3038
+ if timeout > 600:
3039
+ print(
3040
+ f"{YELLOW}Warning: Timeout {timeout}s exceeds maximum (600s), clamping to 600s{RESET}"
3041
+ )
3042
+ timeout = 600
3043
+
3044
+ # Log actual timeout and source
3045
+ print(f"Using timeout: {timeout}s ({source})")
3046
+
3099
3047
  return run_evaluator(
3100
3048
  args.evaluator_config,
3101
3049
  args.file,
3102
- timeout=args.timeout,
3050
+ timeout=timeout,
3103
3051
  )
3104
3052
 
3105
3053
  # Execute static commands
@@ -26,6 +26,7 @@ class EvaluatorConfig:
26
26
  fallback_model: Fallback model if primary fails
27
27
  aliases: Alternative command names
28
28
  version: Evaluator version
29
+ timeout: Timeout in seconds (default: 180, max: 600)
29
30
  source: "builtin" or "local" (set internally)
30
31
  config_file: Path to YAML file if local (set internally)
31
32
  """
@@ -43,6 +44,7 @@ class EvaluatorConfig:
43
44
  fallback_model: str | None = None
44
45
  aliases: list[str] = field(default_factory=list)
45
46
  version: str = "1.0.0"
47
+ timeout: int = 180 # Timeout in seconds (default: 180, max: 600)
46
48
 
47
49
  # Metadata (set internally during discovery, not from YAML)
48
50
  source: str = "builtin"