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.
- package/.agent/.shared/ui-ux-pro-max/README.md +3 -3
- package/.agent/ARCHITECTURE.md +205 -10
- package/.agent/GEMINI.md +37 -7
- package/.agent/agents/accessibility-reviewer.md +134 -0
- package/.agent/agents/ai-code-reviewer.md +129 -0
- package/.agent/agents/frontend-specialist.md +3 -0
- package/.agent/agents/game-developer.md +21 -21
- package/.agent/agents/logic-reviewer.md +12 -0
- package/.agent/agents/mobile-reviewer.md +79 -0
- package/.agent/agents/orchestrator.md +56 -26
- package/.agent/agents/performance-reviewer.md +36 -0
- package/.agent/agents/supervisor-agent.md +156 -0
- package/.agent/agents/swarm-worker-contracts.md +166 -0
- package/.agent/agents/swarm-worker-registry.md +92 -0
- package/.agent/rules/GEMINI.md +134 -5
- package/.agent/scripts/bundle_analyzer.py +259 -0
- package/.agent/scripts/dependency_analyzer.py +247 -0
- package/.agent/scripts/lint_runner.py +188 -0
- package/.agent/scripts/patch_skills_meta.py +177 -0
- package/.agent/scripts/patch_skills_output.py +285 -0
- package/.agent/scripts/schema_validator.py +279 -0
- package/.agent/scripts/security_scan.py +224 -0
- package/.agent/scripts/session_manager.py +144 -3
- package/.agent/scripts/skill_integrator.py +234 -0
- package/.agent/scripts/strengthen_skills.py +220 -0
- package/.agent/scripts/swarm_dispatcher.py +317 -0
- package/.agent/scripts/test_runner.py +192 -0
- package/.agent/scripts/test_swarm_dispatcher.py +163 -0
- package/.agent/skills/agent-organizer/SKILL.md +132 -0
- package/.agent/skills/agentic-patterns/SKILL.md +335 -0
- package/.agent/skills/api-patterns/SKILL.md +226 -50
- package/.agent/skills/app-builder/SKILL.md +215 -52
- package/.agent/skills/architecture/SKILL.md +176 -31
- package/.agent/skills/bash-linux/SKILL.md +150 -134
- package/.agent/skills/behavioral-modes/SKILL.md +152 -160
- package/.agent/skills/brainstorming/SKILL.md +148 -101
- package/.agent/skills/brainstorming/dynamic-questioning.md +10 -0
- package/.agent/skills/clean-code/SKILL.md +139 -134
- package/.agent/skills/code-review-checklist/SKILL.md +177 -80
- package/.agent/skills/config-validator/SKILL.md +165 -0
- package/.agent/skills/csharp-developer/SKILL.md +107 -0
- package/.agent/skills/database-design/SKILL.md +252 -29
- package/.agent/skills/deployment-procedures/SKILL.md +122 -175
- package/.agent/skills/devops-engineer/SKILL.md +134 -0
- package/.agent/skills/devops-incident-responder/SKILL.md +98 -0
- package/.agent/skills/documentation-templates/SKILL.md +175 -121
- package/.agent/skills/dotnet-core-expert/SKILL.md +103 -0
- package/.agent/skills/edge-computing/SKILL.md +213 -0
- package/.agent/skills/frontend-design/SKILL.md +76 -0
- package/.agent/skills/frontend-design/color-system.md +18 -0
- package/.agent/skills/frontend-design/typography-system.md +18 -0
- package/.agent/skills/game-development/SKILL.md +69 -0
- package/.agent/skills/geo-fundamentals/SKILL.md +158 -99
- package/.agent/skills/github-operations/SKILL.md +354 -0
- package/.agent/skills/i18n-localization/SKILL.md +158 -96
- package/.agent/skills/intelligent-routing/SKILL.md +89 -285
- package/.agent/skills/intelligent-routing/router-manifest.md +65 -0
- package/.agent/skills/lint-and-validate/SKILL.md +229 -27
- package/.agent/skills/llm-engineering/SKILL.md +258 -0
- package/.agent/skills/local-first/SKILL.md +203 -0
- package/.agent/skills/mcp-builder/SKILL.md +159 -111
- package/.agent/skills/mobile-design/SKILL.md +102 -282
- package/.agent/skills/nextjs-react-expert/SKILL.md +143 -227
- package/.agent/skills/nodejs-best-practices/SKILL.md +201 -254
- package/.agent/skills/observability/SKILL.md +285 -0
- package/.agent/skills/parallel-agents/SKILL.md +124 -118
- package/.agent/skills/performance-profiling/SKILL.md +143 -89
- package/.agent/skills/plan-writing/SKILL.md +133 -97
- package/.agent/skills/platform-engineer/SKILL.md +135 -0
- package/.agent/skills/powershell-windows/SKILL.md +167 -104
- package/.agent/skills/python-patterns/SKILL.md +149 -361
- package/.agent/skills/python-pro/SKILL.md +114 -0
- package/.agent/skills/react-specialist/SKILL.md +107 -0
- package/.agent/skills/readme-builder/SKILL.md +270 -0
- package/.agent/skills/realtime-patterns/SKILL.md +296 -0
- package/.agent/skills/red-team-tactics/SKILL.md +136 -134
- package/.agent/skills/rust-pro/SKILL.md +237 -173
- package/.agent/skills/seo-fundamentals/SKILL.md +134 -82
- package/.agent/skills/server-management/SKILL.md +155 -104
- package/.agent/skills/sql-pro/SKILL.md +104 -0
- package/.agent/skills/systematic-debugging/SKILL.md +156 -79
- package/.agent/skills/tailwind-patterns/SKILL.md +163 -205
- package/.agent/skills/tdd-workflow/SKILL.md +148 -88
- package/.agent/skills/test-result-analyzer/SKILL.md +299 -0
- package/.agent/skills/testing-patterns/SKILL.md +141 -114
- package/.agent/skills/trend-researcher/SKILL.md +228 -0
- package/.agent/skills/ui-ux-pro-max/SKILL.md +107 -0
- package/.agent/skills/ui-ux-researcher/SKILL.md +234 -0
- package/.agent/skills/vue-expert/SKILL.md +118 -0
- package/.agent/skills/vulnerability-scanner/SKILL.md +228 -188
- package/.agent/skills/web-design-guidelines/SKILL.md +148 -33
- package/.agent/skills/webapp-testing/SKILL.md +171 -122
- package/.agent/skills/whimsy-injector/SKILL.md +349 -0
- package/.agent/skills/workflow-optimizer/SKILL.md +219 -0
- package/.agent/workflows/api-tester.md +279 -0
- package/.agent/workflows/audit.md +168 -0
- package/.agent/workflows/brainstorm.md +65 -19
- package/.agent/workflows/changelog.md +144 -0
- package/.agent/workflows/create.md +67 -14
- package/.agent/workflows/debug.md +122 -30
- package/.agent/workflows/deploy.md +82 -31
- package/.agent/workflows/enhance.md +59 -27
- package/.agent/workflows/fix.md +143 -0
- package/.agent/workflows/generate.md +84 -20
- package/.agent/workflows/migrate.md +163 -0
- package/.agent/workflows/orchestrate.md +66 -17
- package/.agent/workflows/performance-benchmarker.md +305 -0
- package/.agent/workflows/plan.md +76 -33
- package/.agent/workflows/preview.md +73 -17
- package/.agent/workflows/refactor.md +153 -0
- package/.agent/workflows/review-ai.md +140 -0
- package/.agent/workflows/review.md +83 -16
- package/.agent/workflows/session.md +154 -0
- package/.agent/workflows/status.md +74 -18
- package/.agent/workflows/strengthen-skills.md +99 -0
- package/.agent/workflows/swarm.md +194 -0
- package/.agent/workflows/test.md +80 -31
- package/.agent/workflows/tribunal-backend.md +55 -13
- package/.agent/workflows/tribunal-database.md +62 -18
- package/.agent/workflows/tribunal-frontend.md +58 -12
- package/.agent/workflows/tribunal-full.md +70 -11
- package/.agent/workflows/tribunal-mobile.md +123 -0
- package/.agent/workflows/tribunal-performance.md +152 -0
- package/.agent/workflows/ui-ux-pro-max.md +100 -82
- package/README.md +117 -62
- package/bin/tribunal-kit.js +542 -288
- 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]
|