tribunal-kit 1.0.0 → 2.4.2

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 (127) hide show
  1. package/.agent/.shared/ui-ux-pro-max/README.md +3 -3
  2. package/.agent/ARCHITECTURE.md +205 -10
  3. package/.agent/GEMINI.md +37 -7
  4. package/.agent/agents/accessibility-reviewer.md +134 -0
  5. package/.agent/agents/ai-code-reviewer.md +129 -0
  6. package/.agent/agents/frontend-specialist.md +3 -0
  7. package/.agent/agents/game-developer.md +21 -21
  8. package/.agent/agents/logic-reviewer.md +12 -0
  9. package/.agent/agents/mobile-reviewer.md +79 -0
  10. package/.agent/agents/orchestrator.md +56 -26
  11. package/.agent/agents/performance-reviewer.md +36 -0
  12. package/.agent/agents/supervisor-agent.md +156 -0
  13. package/.agent/agents/swarm-worker-contracts.md +166 -0
  14. package/.agent/agents/swarm-worker-registry.md +92 -0
  15. package/.agent/rules/GEMINI.md +134 -5
  16. package/.agent/scripts/bundle_analyzer.py +259 -0
  17. package/.agent/scripts/dependency_analyzer.py +247 -0
  18. package/.agent/scripts/lint_runner.py +188 -0
  19. package/.agent/scripts/patch_skills_meta.py +177 -0
  20. package/.agent/scripts/patch_skills_output.py +285 -0
  21. package/.agent/scripts/schema_validator.py +279 -0
  22. package/.agent/scripts/security_scan.py +224 -0
  23. package/.agent/scripts/session_manager.py +144 -3
  24. package/.agent/scripts/skill_integrator.py +234 -0
  25. package/.agent/scripts/strengthen_skills.py +220 -0
  26. package/.agent/scripts/swarm_dispatcher.py +317 -0
  27. package/.agent/scripts/test_runner.py +192 -0
  28. package/.agent/scripts/test_swarm_dispatcher.py +163 -0
  29. package/.agent/skills/agent-organizer/SKILL.md +132 -0
  30. package/.agent/skills/agentic-patterns/SKILL.md +335 -0
  31. package/.agent/skills/api-patterns/SKILL.md +226 -50
  32. package/.agent/skills/app-builder/SKILL.md +215 -52
  33. package/.agent/skills/architecture/SKILL.md +176 -31
  34. package/.agent/skills/bash-linux/SKILL.md +150 -134
  35. package/.agent/skills/behavioral-modes/SKILL.md +152 -160
  36. package/.agent/skills/brainstorming/SKILL.md +148 -101
  37. package/.agent/skills/brainstorming/dynamic-questioning.md +10 -0
  38. package/.agent/skills/clean-code/SKILL.md +139 -134
  39. package/.agent/skills/code-review-checklist/SKILL.md +177 -80
  40. package/.agent/skills/config-validator/SKILL.md +165 -0
  41. package/.agent/skills/csharp-developer/SKILL.md +107 -0
  42. package/.agent/skills/database-design/SKILL.md +252 -29
  43. package/.agent/skills/deployment-procedures/SKILL.md +122 -175
  44. package/.agent/skills/devops-engineer/SKILL.md +134 -0
  45. package/.agent/skills/devops-incident-responder/SKILL.md +98 -0
  46. package/.agent/skills/documentation-templates/SKILL.md +175 -121
  47. package/.agent/skills/dotnet-core-expert/SKILL.md +103 -0
  48. package/.agent/skills/edge-computing/SKILL.md +213 -0
  49. package/.agent/skills/frontend-design/SKILL.md +76 -0
  50. package/.agent/skills/frontend-design/color-system.md +18 -0
  51. package/.agent/skills/frontend-design/typography-system.md +18 -0
  52. package/.agent/skills/game-development/SKILL.md +69 -0
  53. package/.agent/skills/geo-fundamentals/SKILL.md +158 -99
  54. package/.agent/skills/github-operations/SKILL.md +354 -0
  55. package/.agent/skills/i18n-localization/SKILL.md +158 -96
  56. package/.agent/skills/intelligent-routing/SKILL.md +89 -285
  57. package/.agent/skills/intelligent-routing/router-manifest.md +65 -0
  58. package/.agent/skills/lint-and-validate/SKILL.md +229 -27
  59. package/.agent/skills/llm-engineering/SKILL.md +258 -0
  60. package/.agent/skills/local-first/SKILL.md +203 -0
  61. package/.agent/skills/mcp-builder/SKILL.md +159 -111
  62. package/.agent/skills/mobile-design/SKILL.md +102 -282
  63. package/.agent/skills/nextjs-react-expert/SKILL.md +143 -227
  64. package/.agent/skills/nodejs-best-practices/SKILL.md +201 -254
  65. package/.agent/skills/observability/SKILL.md +285 -0
  66. package/.agent/skills/parallel-agents/SKILL.md +124 -118
  67. package/.agent/skills/performance-profiling/SKILL.md +143 -89
  68. package/.agent/skills/plan-writing/SKILL.md +133 -97
  69. package/.agent/skills/platform-engineer/SKILL.md +135 -0
  70. package/.agent/skills/powershell-windows/SKILL.md +167 -104
  71. package/.agent/skills/python-patterns/SKILL.md +149 -361
  72. package/.agent/skills/python-pro/SKILL.md +114 -0
  73. package/.agent/skills/react-specialist/SKILL.md +107 -0
  74. package/.agent/skills/readme-builder/SKILL.md +270 -0
  75. package/.agent/skills/realtime-patterns/SKILL.md +296 -0
  76. package/.agent/skills/red-team-tactics/SKILL.md +136 -134
  77. package/.agent/skills/rust-pro/SKILL.md +237 -173
  78. package/.agent/skills/seo-fundamentals/SKILL.md +134 -82
  79. package/.agent/skills/server-management/SKILL.md +155 -104
  80. package/.agent/skills/sql-pro/SKILL.md +104 -0
  81. package/.agent/skills/systematic-debugging/SKILL.md +156 -79
  82. package/.agent/skills/tailwind-patterns/SKILL.md +163 -205
  83. package/.agent/skills/tdd-workflow/SKILL.md +148 -88
  84. package/.agent/skills/test-result-analyzer/SKILL.md +299 -0
  85. package/.agent/skills/testing-patterns/SKILL.md +141 -114
  86. package/.agent/skills/trend-researcher/SKILL.md +228 -0
  87. package/.agent/skills/ui-ux-pro-max/SKILL.md +107 -0
  88. package/.agent/skills/ui-ux-researcher/SKILL.md +234 -0
  89. package/.agent/skills/vue-expert/SKILL.md +118 -0
  90. package/.agent/skills/vulnerability-scanner/SKILL.md +228 -188
  91. package/.agent/skills/web-design-guidelines/SKILL.md +148 -33
  92. package/.agent/skills/webapp-testing/SKILL.md +171 -122
  93. package/.agent/skills/whimsy-injector/SKILL.md +349 -0
  94. package/.agent/skills/workflow-optimizer/SKILL.md +219 -0
  95. package/.agent/workflows/api-tester.md +279 -0
  96. package/.agent/workflows/audit.md +168 -0
  97. package/.agent/workflows/brainstorm.md +65 -19
  98. package/.agent/workflows/changelog.md +144 -0
  99. package/.agent/workflows/create.md +67 -14
  100. package/.agent/workflows/debug.md +122 -30
  101. package/.agent/workflows/deploy.md +82 -31
  102. package/.agent/workflows/enhance.md +59 -27
  103. package/.agent/workflows/fix.md +143 -0
  104. package/.agent/workflows/generate.md +84 -20
  105. package/.agent/workflows/migrate.md +163 -0
  106. package/.agent/workflows/orchestrate.md +66 -17
  107. package/.agent/workflows/performance-benchmarker.md +305 -0
  108. package/.agent/workflows/plan.md +76 -33
  109. package/.agent/workflows/preview.md +73 -17
  110. package/.agent/workflows/refactor.md +153 -0
  111. package/.agent/workflows/review-ai.md +140 -0
  112. package/.agent/workflows/review.md +83 -16
  113. package/.agent/workflows/session.md +154 -0
  114. package/.agent/workflows/status.md +74 -18
  115. package/.agent/workflows/strengthen-skills.md +99 -0
  116. package/.agent/workflows/swarm.md +194 -0
  117. package/.agent/workflows/test.md +80 -31
  118. package/.agent/workflows/tribunal-backend.md +55 -13
  119. package/.agent/workflows/tribunal-database.md +62 -18
  120. package/.agent/workflows/tribunal-frontend.md +58 -12
  121. package/.agent/workflows/tribunal-full.md +70 -11
  122. package/.agent/workflows/tribunal-mobile.md +123 -0
  123. package/.agent/workflows/tribunal-performance.md +152 -0
  124. package/.agent/workflows/ui-ux-pro-max.md +100 -82
  125. package/README.md +117 -62
  126. package/bin/tribunal-kit.js +542 -288
  127. package/package.json +10 -6
@@ -0,0 +1,317 @@
1
+ import argparse
2
+ import json
3
+ import logging
4
+ import os
5
+ import sys
6
+ import uuid
7
+ from pathlib import Path
8
+
9
+ # Configure logging
10
+ logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
11
+
12
+ VALID_WORKER_TYPES = {
13
+ "research", "generate_code", "review_code", "debug",
14
+ "plan", "design_schema", "write_docs", "security_audit",
15
+ "optimize", "test"
16
+ }
17
+
18
+ VALID_RESULT_STATUSES = {"success", "failure", "escalate"}
19
+
20
+ MAX_GOAL_LENGTH = 200
21
+ MAX_CONTEXT_LENGTH = 800
22
+ MAX_WORKERS_PER_SWARM = 5
23
+
24
+
25
+ def find_agent_dir(start_path: Path) -> Path:
26
+ current = start_path.resolve()
27
+ while current != current.parent:
28
+ agent_dir = current / '.agent'
29
+ if agent_dir.exists() and agent_dir.is_dir():
30
+ return agent_dir
31
+ current = current.parent
32
+ return None
33
+
34
+
35
+ # ─── Legacy mode: validate orchestrator micro-worker payloads ──────────────────
36
+
37
+ def validate_payload(payload_data: dict, workspace_root: Path, agents_dir: Path) -> bool:
38
+ if "dispatch_micro_workers" not in payload_data:
39
+ logging.error("Payload missing required 'dispatch_micro_workers' array.")
40
+ return False
41
+
42
+ workers = payload_data.get("dispatch_micro_workers", [])
43
+ if not isinstance(workers, list):
44
+ logging.error("'dispatch_micro_workers' must be a list.")
45
+ return False
46
+
47
+ all_valid = True
48
+ for i, worker in enumerate(workers):
49
+ agent_name = worker.get("target_agent")
50
+ if not agent_name:
51
+ logging.error(f"Worker {i}: missing 'target_agent'.")
52
+ all_valid = False
53
+ continue
54
+
55
+ agent_file = agents_dir / f"{agent_name}.md"
56
+ if not agent_file.exists():
57
+ logging.error(f"Worker {i}: target_agent '{agent_name}' not found at {agent_file}.")
58
+ all_valid = False
59
+
60
+ files_attached = worker.get("files_attached", [])
61
+ if not isinstance(files_attached, list):
62
+ logging.error(f"Worker {i}: 'files_attached' must be a list.")
63
+ all_valid = False
64
+ continue
65
+
66
+ for f in files_attached:
67
+ file_path = workspace_root / f
68
+ if not file_path.exists():
69
+ logging.warning(f"Worker {i}: attached file '{f}' does not exist (might be a new file to create).")
70
+
71
+ return all_valid
72
+
73
+
74
+ def build_worker_prompts(payload_data: dict, workspace_root: Path) -> list:
75
+ prompts = []
76
+ workers = payload_data.get("dispatch_micro_workers", [])
77
+ for worker in workers:
78
+ agent = worker.get("target_agent")
79
+ ctx = worker.get("context_summary", "")
80
+ task = worker.get("task_description", "")
81
+ files = worker.get("files_attached", [])
82
+
83
+ prompt = f"--- MICRO-WORKER DISPATCH ---\n"
84
+ prompt += f"Agent: {agent}\n"
85
+ prompt += f"Context: {ctx}\n"
86
+ prompt += f"Task: {task}\n"
87
+ prompt += f"Attached Files: {', '.join(files) if files else 'None'}\n"
88
+ prompt += "-----------------------------"
89
+ prompts.append(prompt)
90
+ return prompts
91
+
92
+
93
+ # ─── Swarm mode: validate WorkerRequest / WorkerResult payloads ───────────────
94
+
95
+ def validate_worker_request(req: dict, index: int, agents_dir: Path) -> list[str]:
96
+ """Validate a single WorkerRequest object. Returns a list of error strings."""
97
+ errors = []
98
+
99
+ # task_id
100
+ task_id = req.get("task_id", "")
101
+ if not task_id or not isinstance(task_id, str):
102
+ errors.append(f"WorkerRequest[{index}]: 'task_id' must be a non-empty string.")
103
+
104
+ # type
105
+ req_type = req.get("type", "")
106
+ if req_type not in VALID_WORKER_TYPES:
107
+ errors.append(
108
+ f"WorkerRequest[{index}]: 'type' must be one of {sorted(VALID_WORKER_TYPES)}, got '{req_type}'."
109
+ )
110
+
111
+ # agent
112
+ agent = req.get("agent", "")
113
+ if not agent or not isinstance(agent, str):
114
+ errors.append(f"WorkerRequest[{index}]: 'agent' must be a non-empty string.")
115
+ else:
116
+ agent_file = agents_dir / f"{agent}.md"
117
+ if not agent_file.exists():
118
+ errors.append(
119
+ f"WorkerRequest[{index}]: agent '{agent}' not found at {agent_file}. "
120
+ f"Only agents that exist in .agent/agents/ are valid."
121
+ )
122
+
123
+ # goal
124
+ goal = req.get("goal", "")
125
+ if not goal or not isinstance(goal, str):
126
+ errors.append(f"WorkerRequest[{index}]: 'goal' must be a non-empty string.")
127
+ elif len(goal) > MAX_GOAL_LENGTH:
128
+ errors.append(
129
+ f"WorkerRequest[{index}]: 'goal' exceeds {MAX_GOAL_LENGTH} characters ({len(goal)} chars). "
130
+ f"Keep it to a single, focused sentence."
131
+ )
132
+
133
+ # context
134
+ context = req.get("context", "")
135
+ if not context or not isinstance(context, str):
136
+ errors.append(f"WorkerRequest[{index}]: 'context' must be a non-empty string.")
137
+ elif len(context) > MAX_CONTEXT_LENGTH:
138
+ errors.append(
139
+ f"WorkerRequest[{index}]: 'context' exceeds {MAX_CONTEXT_LENGTH} characters ({len(context)} chars). "
140
+ f"Trim to minimal required context only."
141
+ )
142
+
143
+ # max_retries
144
+ max_retries = req.get("max_retries")
145
+ if not isinstance(max_retries, int) or not (1 <= max_retries <= 3):
146
+ errors.append(
147
+ f"WorkerRequest[{index}]: 'max_retries' must be an integer between 1 and 3, got '{max_retries}'."
148
+ )
149
+
150
+ return errors
151
+
152
+
153
+ def validate_worker_result(res: dict, index: int) -> list[str]:
154
+ """Validate a single WorkerResult object. Returns a list of error strings."""
155
+ errors = []
156
+
157
+ # task_id
158
+ task_id = res.get("task_id", "")
159
+ if not task_id or not isinstance(task_id, str):
160
+ errors.append(f"WorkerResult[{index}]: 'task_id' must be a non-empty string.")
161
+
162
+ # agent
163
+ agent = res.get("agent", "")
164
+ if not agent or not isinstance(agent, str):
165
+ errors.append(f"WorkerResult[{index}]: 'agent' must be a non-empty string.")
166
+
167
+ # status
168
+ status = res.get("status", "")
169
+ if status not in VALID_RESULT_STATUSES:
170
+ errors.append(
171
+ f"WorkerResult[{index}]: 'status' must be one of {sorted(VALID_RESULT_STATUSES)}, got '{status}'."
172
+ )
173
+
174
+ # output / error rules
175
+ output = res.get("output", "")
176
+ error = res.get("error", "")
177
+ if status == "success" and not output:
178
+ errors.append(f"WorkerResult[{index}]: 'output' is required when status is 'success'.")
179
+ if status in ("failure", "escalate") and not error:
180
+ errors.append(
181
+ f"WorkerResult[{index}]: 'error' is required when status is '{status}'. "
182
+ f"Be specific — 'Something went wrong' is not acceptable."
183
+ )
184
+
185
+ # attempts
186
+ attempts = res.get("attempts")
187
+ if not isinstance(attempts, int) or attempts < 1:
188
+ errors.append(f"WorkerResult[{index}]: 'attempts' must be an integer >= 1, got '{attempts}'.")
189
+
190
+ return errors
191
+
192
+
193
+ def validate_swarm_payload(payload_data, agents_dir: Path) -> bool:
194
+ """
195
+ Validate a Swarm payload. Accepts either:
196
+ - A single WorkerRequest object (dict with 'task_id', 'type', 'agent', ...)
197
+ - A single WorkerResult object (dict with 'task_id', 'agent', 'status', ...)
198
+ - A list of WorkerRequests
199
+ - A list of WorkerResults
200
+ - An object with a top-level 'workers' array of WorkerRequests
201
+ """
202
+ # Normalise to a list
203
+ if isinstance(payload_data, dict):
204
+ if "workers" in payload_data:
205
+ items = payload_data["workers"]
206
+ else:
207
+ items = [payload_data]
208
+ elif isinstance(payload_data, list):
209
+ items = payload_data
210
+ else:
211
+ logging.error("Swarm payload must be a JSON object or array.")
212
+ return False
213
+
214
+ if len(items) > MAX_WORKERS_PER_SWARM:
215
+ logging.error(
216
+ f"Swarm payload contains {len(items)} workers, "
217
+ f"exceeding the maximum of {MAX_WORKERS_PER_SWARM}."
218
+ )
219
+ return False
220
+
221
+ all_errors = []
222
+ for i, item in enumerate(items):
223
+ if not isinstance(item, dict):
224
+ all_errors.append(f"Item[{i}]: must be a JSON object.")
225
+ continue
226
+
227
+ # Detect whether this is a WorkerRequest or WorkerResult by key presence
228
+ if "status" in item and "output" in item:
229
+ # Looks like a WorkerResult
230
+ errors = validate_worker_result(item, i)
231
+ else:
232
+ # Treat as WorkerRequest
233
+ errors = validate_worker_request(item, i, agents_dir)
234
+
235
+ all_errors.extend(errors)
236
+
237
+ if all_errors:
238
+ for err in all_errors:
239
+ logging.error(err)
240
+ return False
241
+
242
+ return True
243
+
244
+
245
+ # ─── Main ─────────────────────────────────────────────────────────────────────
246
+
247
+ def main():
248
+ parser = argparse.ArgumentParser(
249
+ description=(
250
+ "Validate Orchestrator micro-worker payloads (legacy) "
251
+ "and Swarm WorkerRequest/WorkerResult payloads (swarm mode)."
252
+ )
253
+ )
254
+ parser.add_argument("--payload", type=str, help="JSON string of the payload", required=False)
255
+ parser.add_argument("--file", type=str, help="Path to a JSON file containing the payload", required=False)
256
+ parser.add_argument("--workspace", type=str, default=".", help="Workspace root directory")
257
+ parser.add_argument(
258
+ "--mode",
259
+ type=str,
260
+ choices=["legacy", "swarm"],
261
+ default="legacy",
262
+ help=(
263
+ "Validation mode. "
264
+ "'legacy': validate orchestrator dispatch_micro_workers payload (default). "
265
+ "'swarm': validate WorkerRequest or WorkerResult JSON."
266
+ )
267
+ )
268
+
269
+ args = parser.parse_args()
270
+
271
+ if not args.payload and not args.file:
272
+ logging.error("Must provide either --payload or --file")
273
+ sys.exit(1)
274
+
275
+ workspace_root = Path(args.workspace).resolve()
276
+ agent_dir = find_agent_dir(workspace_root)
277
+
278
+ if not agent_dir:
279
+ logging.error(f"Could not find .agent directory starting from {workspace_root}")
280
+ sys.exit(1)
281
+
282
+ agents_dir = agent_dir / "agents"
283
+ if not agents_dir.exists():
284
+ logging.error(f"Could not find 'agents' directory inside {agent_dir}")
285
+ sys.exit(1)
286
+
287
+ try:
288
+ if args.file:
289
+ with open(args.file, 'r', encoding='utf-8') as f:
290
+ payload_data = json.load(f)
291
+ else:
292
+ payload_data = json.loads(args.payload)
293
+ except Exception as e:
294
+ logging.error(f"Failed to parse payload as JSON: {e}")
295
+ sys.exit(1)
296
+
297
+ if args.mode == "swarm":
298
+ if not validate_swarm_payload(payload_data, agents_dir):
299
+ logging.error("Swarm payload validation failed.")
300
+ sys.exit(1)
301
+ logging.info("Swarm payload validation successful.")
302
+ else:
303
+ # Legacy mode
304
+ if not validate_payload(payload_data, workspace_root, agents_dir):
305
+ logging.error("Payload validation failed.")
306
+ sys.exit(1)
307
+
308
+ logging.info("Payload validation successful.")
309
+ prompts = build_worker_prompts(payload_data, workspace_root)
310
+
311
+ for i, p in enumerate(prompts):
312
+ print(f"\n[Worker {i+1} Ready]")
313
+ print(p)
314
+
315
+
316
+ if __name__ == "__main__":
317
+ main()
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ test_runner.py — Standalone test runner for the Tribunal Agent Kit.
4
+
5
+ Detects and runs the project's test framework:
6
+ - npm test (Jest / Vitest / Mocha)
7
+ - pytest (Python)
8
+ - go test (Go)
9
+
10
+ Usage:
11
+ python .agent/scripts/test_runner.py .
12
+ python .agent/scripts/test_runner.py . --coverage
13
+ python .agent/scripts/test_runner.py . --watch
14
+ python .agent/scripts/test_runner.py . --file src/utils.test.ts
15
+ """
16
+
17
+ import os
18
+ import sys
19
+ import subprocess
20
+ import argparse
21
+ from pathlib import Path
22
+
23
+ RED = "\033[91m"
24
+ GREEN = "\033[92m"
25
+ YELLOW = "\033[93m"
26
+ BLUE = "\033[94m"
27
+ BOLD = "\033[1m"
28
+ RESET = "\033[0m"
29
+
30
+
31
+ def header(title: str) -> None:
32
+ print(f"\n{BOLD}{BLUE}━━━ {title} ━━━{RESET}")
33
+
34
+
35
+ def ok(msg: str) -> None:
36
+ print(f" {GREEN}✅ {msg}{RESET}")
37
+
38
+
39
+ def fail(msg: str) -> None:
40
+ print(f" {RED}❌ {msg}{RESET}")
41
+
42
+
43
+ def skip(msg: str) -> None:
44
+ print(f" {YELLOW}⏭️ {msg}{RESET}")
45
+
46
+
47
+ def run_tests(label: str, cmd: list[str], cwd: str) -> bool:
48
+ """Run a test command, streaming output in real-time."""
49
+ try:
50
+ result = subprocess.run(
51
+ cmd, cwd=cwd, capture_output=True, text=True, timeout=300
52
+ )
53
+ output = (result.stdout + result.stderr).strip()
54
+ if output:
55
+ for line in output.split("\n"):
56
+ print(f" {line}")
57
+
58
+ if result.returncode == 0:
59
+ ok(f"{label} — all tests passed")
60
+ return True
61
+ fail(f"{label} — test failures detected")
62
+ return False
63
+ except FileNotFoundError:
64
+ skip(f"{label} — tool not installed")
65
+ return True
66
+ except subprocess.TimeoutExpired:
67
+ fail(f"{label} — timed out after 300s")
68
+ return False
69
+
70
+
71
+ def detect_test_framework(project_root: str) -> str | None:
72
+ """Detect the primary test framework from project files."""
73
+ root = Path(project_root)
74
+
75
+ # Node.js test frameworks
76
+ pkg_json = root / "package.json"
77
+ if pkg_json.exists():
78
+ try:
79
+ import json
80
+ with open(pkg_json) as f:
81
+ pkg = json.load(f)
82
+ deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
83
+ scripts = pkg.get("scripts", {})
84
+
85
+ if "vitest" in deps:
86
+ return "vitest"
87
+ if "jest" in deps:
88
+ return "jest"
89
+ if "mocha" in deps:
90
+ return "mocha"
91
+ if "test" in scripts:
92
+ return "npm-test"
93
+ except Exception:
94
+ if "test" in str(pkg_json.read_text()):
95
+ return "npm-test"
96
+
97
+ # Python
98
+ if (root / "pytest.ini").exists() or (root / "pyproject.toml").exists() or (root / "conftest.py").exists():
99
+ return "pytest"
100
+
101
+ # Go
102
+ go_files = list(root.glob("**/*_test.go"))
103
+ if go_files:
104
+ return "go"
105
+
106
+ return None
107
+
108
+
109
+ def main() -> None:
110
+ parser = argparse.ArgumentParser(
111
+ description="Tribunal test runner — detects and runs the project's test framework"
112
+ )
113
+ parser.add_argument("path", help="Project root directory")
114
+ parser.add_argument("--coverage", action="store_true", help="Run tests with coverage reporting")
115
+ parser.add_argument("--watch", action="store_true", help="Run tests in watch mode")
116
+ parser.add_argument("--file", help="Run tests for a specific file only")
117
+ args = parser.parse_args()
118
+
119
+ project_root = os.path.abspath(args.path)
120
+ if not os.path.isdir(project_root):
121
+ fail(f"Directory not found: {project_root}")
122
+ sys.exit(1)
123
+
124
+ print(f"{BOLD}Tribunal — test_runner.py{RESET}")
125
+ print(f"Project: {project_root}")
126
+
127
+ framework = detect_test_framework(project_root)
128
+ if not framework:
129
+ skip("No test framework detected in this project")
130
+ sys.exit(0)
131
+
132
+ header(f"Running tests ({framework})")
133
+
134
+ cmd: list[str] = []
135
+ passed = True
136
+
137
+ if framework in ("vitest", "jest", "mocha", "npm-test"):
138
+ if framework == "vitest":
139
+ cmd = ["npx", "vitest", "run"]
140
+ if args.coverage:
141
+ cmd.append("--coverage")
142
+ if args.watch:
143
+ cmd = ["npx", "vitest"] # watch is vitest default
144
+ if args.file:
145
+ cmd.append(args.file)
146
+ elif framework == "jest":
147
+ cmd = ["npx", "jest"]
148
+ if args.coverage:
149
+ cmd.append("--coverage")
150
+ if args.watch:
151
+ cmd.append("--watch")
152
+ if args.file:
153
+ cmd.append(args.file)
154
+ else:
155
+ cmd = ["npm", "test", "--", "--passWithNoTests"]
156
+ if args.coverage:
157
+ cmd.append("--coverage")
158
+ if args.file:
159
+ cmd.append(args.file)
160
+ passed = run_tests(framework, cmd, project_root)
161
+
162
+ elif framework == "pytest":
163
+ cmd = ["python", "-m", "pytest", "-v"]
164
+ if args.coverage:
165
+ cmd.extend(["--cov", "--cov-report=term-missing"])
166
+ if args.watch:
167
+ cmd = ["python", "-m", "pytest-watch", "--", "-v"] # pytest-watch
168
+
169
+ if args.file:
170
+ cmd.append(args.file)
171
+ passed = run_tests("pytest", cmd, project_root)
172
+
173
+ elif framework == "go":
174
+ cmd = ["go", "test", "./...", "-v"]
175
+ if args.coverage:
176
+ cmd.append("-cover")
177
+ if args.file:
178
+ cmd = ["go", "test", "-v", "-run", args.file]
179
+ passed = run_tests("go test", cmd, project_root)
180
+
181
+ # Summary
182
+ print(f"\n{BOLD}━━━ Test Summary ━━━{RESET}")
183
+ if passed:
184
+ ok(f"Tests passed ({framework})")
185
+ else:
186
+ fail(f"Tests failed ({framework})")
187
+
188
+ sys.exit(0 if passed else 1)
189
+
190
+
191
+ if __name__ == "__main__":
192
+ main()
@@ -0,0 +1,163 @@
1
+ import pytest
2
+ from pathlib import Path
3
+
4
+ # Adjust the path so we can import swarm_dispatcher directly
5
+ import sys
6
+ import os
7
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
8
+
9
+ from swarm_dispatcher import find_agent_dir, validate_payload, build_worker_prompts
10
+
11
+
12
+ def test_find_agent_dir_found(tmp_path):
13
+ # Setup: Create an .agent directory and a deep subdirectory
14
+ agent_dir = tmp_path / ".agent"
15
+ agent_dir.mkdir()
16
+ sub_dir = tmp_path / "src" / "deep" / "folder"
17
+ sub_dir.mkdir(parents=True)
18
+
19
+ # Act & Assert
20
+ result = find_agent_dir(sub_dir)
21
+ assert result is not None
22
+ assert result.resolve() == agent_dir.resolve()
23
+
24
+ def test_find_agent_dir_not_found(tmp_path):
25
+ # Setup: Directory structure without '.agent'
26
+ sub_dir = tmp_path / "src" / "deep"
27
+ sub_dir.mkdir(parents=True)
28
+
29
+ # Act & Assert
30
+ assert find_agent_dir(sub_dir) is None
31
+
32
+ def test_validate_payload_valid(tmp_path):
33
+ # Setup
34
+ agents_dir = tmp_path / "agents"
35
+ agents_dir.mkdir()
36
+ agent_file = agents_dir / "test_agent.md"
37
+ agent_file.touch()
38
+
39
+ workspace = tmp_path / "workspace"
40
+ workspace.mkdir()
41
+ file_attached = workspace / "file1.txt"
42
+ file_attached.touch()
43
+
44
+ payload = {
45
+ "dispatch_micro_workers": [
46
+ {
47
+ "target_agent": "test_agent",
48
+ "files_attached": ["file1.txt"]
49
+ }
50
+ ]
51
+ }
52
+
53
+ # Act & Assert
54
+ assert validate_payload(payload, workspace, agents_dir) is True
55
+
56
+ def test_validate_payload_missing_workers(tmp_path):
57
+ # Act & Assert
58
+ assert validate_payload({}, tmp_path, tmp_path) is False
59
+
60
+ def test_validate_payload_not_list(tmp_path):
61
+ # Act & Assert
62
+ assert validate_payload({"dispatch_micro_workers": "str"}, tmp_path, tmp_path) is False
63
+
64
+ def test_validate_payload_missing_target_agent(tmp_path):
65
+ # Setup
66
+ payload = {
67
+ "dispatch_micro_workers": [
68
+ {
69
+ "files_attached": []
70
+ }
71
+ ]
72
+ }
73
+
74
+ # Act & Assert
75
+ assert validate_payload(payload, tmp_path, tmp_path) is False
76
+
77
+ def test_validate_payload_agent_not_found(tmp_path):
78
+ # Setup
79
+ agents_dir = tmp_path / "agents"
80
+ agents_dir.mkdir()
81
+
82
+ payload = {
83
+ "dispatch_micro_workers": [
84
+ {
85
+ "target_agent": "missing_agent"
86
+ }
87
+ ]
88
+ }
89
+
90
+ # Act & Assert
91
+ assert validate_payload(payload, tmp_path, agents_dir) is False
92
+
93
+ def test_validate_payload_files_not_a_list(tmp_path):
94
+ agents_dir = tmp_path / "agents"
95
+ agents_dir.mkdir()
96
+ (agents_dir / "test_agent.md").touch()
97
+
98
+ payload = {
99
+ "dispatch_micro_workers": [
100
+ {
101
+ "target_agent": "test_agent",
102
+ "files_attached": "a single file string"
103
+ }
104
+ ]
105
+ }
106
+
107
+ assert validate_payload(payload, tmp_path, agents_dir) is False
108
+
109
+ def test_validate_payload_files_missing_warning(tmp_path, caplog):
110
+ # Tests that the validation still passes if files are missing,
111
+ # but that a warning is logged.
112
+ agents_dir = tmp_path / "agents"
113
+ agents_dir.mkdir()
114
+ (agents_dir / "test_agent.md").touch()
115
+
116
+ workspace = tmp_path / "workspace"
117
+ workspace.mkdir()
118
+
119
+ payload = {
120
+ "dispatch_micro_workers": [
121
+ {
122
+ "target_agent": "test_agent",
123
+ "files_attached": ["nonexistent.txt"]
124
+ }
125
+ ]
126
+ }
127
+
128
+ assert validate_payload(payload, workspace, agents_dir) is True
129
+ assert "attached file 'nonexistent.txt' does not exist" in caplog.text
130
+
131
+ def test_build_worker_prompts():
132
+ payload = {
133
+ "dispatch_micro_workers": [
134
+ {
135
+ "target_agent": "worker1",
136
+ "context_summary": "Initial context value",
137
+ "task_description": "First task to do",
138
+ "files_attached": ["main.py", "utils.py"]
139
+ },
140
+ {
141
+ "target_agent": "worker2",
142
+ "context_summary": "Another context",
143
+ "task_description": "Second task",
144
+ "files_attached": []
145
+ }
146
+ ]
147
+ }
148
+
149
+ prompts = build_worker_prompts(payload, Path("."))
150
+
151
+ assert len(prompts) == 2
152
+
153
+ # Check first prompt
154
+ assert "Agent: worker1" in prompts[0]
155
+ assert "Context: Initial context value" in prompts[0]
156
+ assert "Task: First task to do" in prompts[0]
157
+ assert "Attached Files: main.py, utils.py" in prompts[0]
158
+
159
+ # Check second prompt
160
+ assert "Agent: worker2" in prompts[1]
161
+ assert "Context: Another context" in prompts[1]
162
+ assert "Task: Second task" in prompts[1]
163
+ assert "Attached Files: None" in prompts[1]