@oswaldzsh/devhive 0.1.0
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/README.md +91 -0
- package/__init__.py +0 -0
- package/agents/__init__.py +0 -0
- package/agents/base.py +118 -0
- package/agents/execute.py +150 -0
- package/agents/verifier_dynamic.py +164 -0
- package/agents/verifier_semantic.py +84 -0
- package/agents/verifier_static.py +153 -0
- package/bin/dh +77 -0
- package/config.yaml +71 -0
- package/control_plane/__init__.py +0 -0
- package/control_plane/cli.py +596 -0
- package/control_plane/dashboard.py +57 -0
- package/control_plane/notifications.py +54 -0
- package/control_plane/tui.py +352 -0
- package/install.sh +67 -0
- package/orchestrator/__init__.py +0 -0
- package/orchestrator/agent_pool.py +107 -0
- package/orchestrator/convergence_gate.py +133 -0
- package/orchestrator/engine.py +353 -0
- package/orchestrator/event_bus.py +58 -0
- package/orchestrator/task_queue.py +59 -0
- package/package.json +50 -0
- package/protocol/__init__.py +0 -0
- package/protocol/schemas.py +222 -0
- package/setup.py +44 -0
- package/signature/__init__.py +0 -0
- package/signature/engine.py +211 -0
- package/signature/extractor.py +156 -0
- package/signature/learner.py +75 -0
- package/signature/src/matcher.c +263 -0
- package/signature/src/matcher.h +135 -0
- package/signatures/seed_signatures.json +174 -0
- package/storage/__init__.py +0 -0
- package/storage/checkpoint.py +153 -0
- package/storage/signature_db.py +62 -0
- package/tools/__init__.py +0 -0
- package/tools/api_client.py +101 -0
- package/tools/git.py +75 -0
- package/tools/sandbox.py +79 -0
- package/verification/__init__.py +0 -0
- package/verification/diagnostic.py +124 -0
- package/verification/patterns/api_breaking.yaml +25 -0
- package/verification/patterns/code_quality.yaml +41 -0
- package/verification/patterns/security.yaml +41 -0
- package/verification/pipeline.py +61 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Convergence Gate — determines whether to pass, retry, or escalate."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from collections import deque
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from protocol.schemas import (
|
|
10
|
+
Task, ExecutionHandoff, Verdict, SemanticVerdict, ConvergenceDecision,
|
|
11
|
+
ConcurrencyAction, EscalationReport,
|
|
12
|
+
)
|
|
13
|
+
from verification.diagnostic import DiagnosticAggregator, AggregatorResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class StageState:
|
|
18
|
+
stage: str
|
|
19
|
+
attempt: int
|
|
20
|
+
fingerprint: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConvergenceGate:
|
|
24
|
+
"""Implements L1/L2 convergence checks with escalation rules."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: dict = None):
|
|
27
|
+
self.config = config or {}
|
|
28
|
+
self.max_l1_retries = config.get("l1_max_retries", 3)
|
|
29
|
+
self.max_l2_retries = config.get("l2_max_retries", 2)
|
|
30
|
+
self.loop_window = config.get("loop_detection_window", 5)
|
|
31
|
+
self.loop_threshold = config.get("loop_similarity_threshold", 0.8)
|
|
32
|
+
self.aggregator = DiagnosticAggregator(config)
|
|
33
|
+
self._stage_history: dict[str, deque[StageState]] = {}
|
|
34
|
+
|
|
35
|
+
def evaluate_l1(self, task: Task, static: Verdict, dynamic: Verdict,
|
|
36
|
+
attempt: int) -> ConvergenceDecision:
|
|
37
|
+
"""Evaluate L1 convergence."""
|
|
38
|
+
result = self.aggregator.aggregate_l1(static, dynamic, task.id)
|
|
39
|
+
|
|
40
|
+
# Check retry limit
|
|
41
|
+
if result.action == ConcurrencyAction.FIX and attempt >= self.max_l1_retries:
|
|
42
|
+
return ConvergenceDecision(
|
|
43
|
+
action=ConcurrencyAction.ESCALATE,
|
|
44
|
+
reason=f"L1 retry limit ({self.max_l1_retries}) exceeded",
|
|
45
|
+
escalation=self._build_escalation(task, result),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Check for loop detection
|
|
49
|
+
if self._detect_loop(task.id, "L1"):
|
|
50
|
+
return ConvergenceDecision(
|
|
51
|
+
action=ConcurrencyAction.ESCALATE,
|
|
52
|
+
reason="Loop detected in L1 verification",
|
|
53
|
+
escalation=self._build_escalation(task, result),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return ConvergenceDecision(
|
|
57
|
+
action=result.action,
|
|
58
|
+
reason=result.reason,
|
|
59
|
+
fix_strategy=result.fix_strategy,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def evaluate_l2(self, task: Task, static: Verdict, dynamic: Verdict,
|
|
63
|
+
semantic: SemanticVerdict, mutation: Optional[Verdict],
|
|
64
|
+
attempt: int) -> ConvergenceDecision:
|
|
65
|
+
"""Evaluate L2 convergence."""
|
|
66
|
+
result = self.aggregator.aggregate_l2(
|
|
67
|
+
static, dynamic, semantic, mutation, task.id
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if result.action == ConcurrencyAction.FIX and attempt >= self.max_l2_retries:
|
|
71
|
+
return ConvergenceDecision(
|
|
72
|
+
action=ConcurrencyAction.ESCALATE,
|
|
73
|
+
reason=f"L2 retry limit ({self.max_l2_retries}) exceeded",
|
|
74
|
+
escalation=self._build_escalation(task, result),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return ConvergenceDecision(
|
|
78
|
+
action=result.action,
|
|
79
|
+
reason=result.reason,
|
|
80
|
+
fix_strategy=result.fix_strategy,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def check_sensitive_modules(self, task: Task) -> bool:
|
|
84
|
+
"""Check if the task touches modules that require human approval."""
|
|
85
|
+
sensitive = set(self.config.get("escalation", {}).get(
|
|
86
|
+
"require_approval_for", []))
|
|
87
|
+
task_modules = set(task.spec.sensitive_modules)
|
|
88
|
+
return bool(sensitive & task_modules)
|
|
89
|
+
|
|
90
|
+
def _detect_loop(self, task_id: str, stage: str) -> bool:
|
|
91
|
+
"""Check if recent stage fingerprints indicate a loop."""
|
|
92
|
+
if task_id not in self._stage_history:
|
|
93
|
+
self._stage_history[task_id] = deque(maxlen=self.loop_window)
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
history = self._stage_history[task_id]
|
|
97
|
+
if len(history) < self.loop_window:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# Count similar fingerprints in the window
|
|
101
|
+
fingerprints = [h.fingerprint for h in history]
|
|
102
|
+
unique = len(set(fingerprints))
|
|
103
|
+
similarity = 1.0 - (unique / len(fingerprints))
|
|
104
|
+
return similarity >= self.loop_threshold
|
|
105
|
+
|
|
106
|
+
def record_state(self, task_id: str, stage: str, attempt: int,
|
|
107
|
+
handoff: dict = None, verdict: dict = None):
|
|
108
|
+
"""Record stage state for loop detection."""
|
|
109
|
+
if task_id not in self._stage_history:
|
|
110
|
+
self._stage_history[task_id] = deque(maxlen=self.loop_window)
|
|
111
|
+
|
|
112
|
+
# Build a fingerprint
|
|
113
|
+
data = json.dumps({
|
|
114
|
+
"stage": stage,
|
|
115
|
+
"handoff_keys": sorted(handoff.keys()) if handoff else [],
|
|
116
|
+
"verdict_overall": verdict.get("overall") if verdict else None,
|
|
117
|
+
}, sort_keys=True)
|
|
118
|
+
fingerprint = hashlib.sha256(data.encode()).hexdigest()[:16]
|
|
119
|
+
|
|
120
|
+
self._stage_history[task_id].append(StageState(
|
|
121
|
+
stage=stage, attempt=attempt, fingerprint=fingerprint,
|
|
122
|
+
))
|
|
123
|
+
|
|
124
|
+
def _build_escalation(self, task: Task,
|
|
125
|
+
result: AggregatorResult) -> EscalationReport:
|
|
126
|
+
from datetime import datetime, timezone
|
|
127
|
+
return EscalationReport(
|
|
128
|
+
escalation_id=f"esc-{task.id}-{datetime.now(timezone.utc).timestamp()}",
|
|
129
|
+
task_id=task.id,
|
|
130
|
+
triggered_by=result.reason,
|
|
131
|
+
current_state={"stage": task.current_stage},
|
|
132
|
+
suggested_human_action=result.reason,
|
|
133
|
+
)
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Orchestrator Engine — the central event loop for DevHive."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from protocol.schemas import (
|
|
10
|
+
Task, ExecutionHandoff, Verdict, SemanticVerdict, DevHiveEvent,
|
|
11
|
+
VerdictOverall, ConcurrencyAction, TaskSpec, Priority,
|
|
12
|
+
)
|
|
13
|
+
from orchestrator.event_bus import EventBus
|
|
14
|
+
from orchestrator.task_queue import TaskQueue
|
|
15
|
+
from orchestrator.agent_pool import AgentPool, AgentType
|
|
16
|
+
from orchestrator.convergence_gate import ConvergenceGate
|
|
17
|
+
from verification.pipeline import VerificationPipeline
|
|
18
|
+
from verification.diagnostic import DiagnosticAggregator
|
|
19
|
+
from storage.checkpoint import CheckpointStore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Orchestrator:
|
|
23
|
+
"""Central orchestrator for the DevHive multi-agent system.
|
|
24
|
+
|
|
25
|
+
Routes events between agents, manages task lifecycle,
|
|
26
|
+
and enforces convergence gates.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: dict = None):
|
|
30
|
+
self.config = config or {}
|
|
31
|
+
self.event_bus = EventBus()
|
|
32
|
+
self.task_queue = TaskQueue()
|
|
33
|
+
self.agent_pool = AgentPool(config)
|
|
34
|
+
self.verification = VerificationPipeline(config.get("verification", {}))
|
|
35
|
+
self.convergence = ConvergenceGate(config.get("convergence", {}))
|
|
36
|
+
self.checkpoint = CheckpointStore(config.get("checkpoint_db",
|
|
37
|
+
"storage/devhive.db"))
|
|
38
|
+
self.diagnostic = DiagnosticAggregator(config)
|
|
39
|
+
self._task_attempts: dict[str, dict[str, int]] = {}
|
|
40
|
+
|
|
41
|
+
async def start(self, agent_counts: dict[str, int] = None):
|
|
42
|
+
"""Start the orchestrator and all agent processes."""
|
|
43
|
+
counts = agent_counts or {
|
|
44
|
+
"execute": 1,
|
|
45
|
+
"static_verifier": 1,
|
|
46
|
+
"dynamic_verifier": 1,
|
|
47
|
+
"semantic_verifier": 1,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Start agents
|
|
51
|
+
agent_type_map = {
|
|
52
|
+
"execute": AgentType.EXECUTE,
|
|
53
|
+
"static_verifier": AgentType.STATIC_VERIFIER,
|
|
54
|
+
"dynamic_verifier": AgentType.DYNAMIC_VERIFIER,
|
|
55
|
+
"semantic_verifier": AgentType.SEMANTIC_VERIFIER,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for name, count in counts.items():
|
|
59
|
+
atype = agent_type_map.get(name)
|
|
60
|
+
if atype:
|
|
61
|
+
for _ in range(count):
|
|
62
|
+
self.agent_pool.start_agent(atype)
|
|
63
|
+
|
|
64
|
+
# Subscribe to events
|
|
65
|
+
self.event_bus.subscribe("task.created", self._on_task_created)
|
|
66
|
+
self.event_bus.subscribe("agent.idle", self._on_agent_idle)
|
|
67
|
+
self.event_bus.subscribe("handoff.emitted", self._on_handoff)
|
|
68
|
+
self.event_bus.subscribe("verdict.ready", self._on_verdict)
|
|
69
|
+
self.event_bus.subscribe("escalation.needed", self._on_escalation)
|
|
70
|
+
|
|
71
|
+
# Start event processing
|
|
72
|
+
await self.event_bus.start()
|
|
73
|
+
|
|
74
|
+
# Start result collection loop
|
|
75
|
+
asyncio.create_task(self._collect_results())
|
|
76
|
+
|
|
77
|
+
async def stop(self):
|
|
78
|
+
"""Stop the orchestrator and all agents."""
|
|
79
|
+
self.agent_pool.stop_all()
|
|
80
|
+
await self.event_bus.stop()
|
|
81
|
+
|
|
82
|
+
async def submit_task(self, spec: TaskSpec, branch: str = "main",
|
|
83
|
+
base_commit: str = "HEAD") -> str:
|
|
84
|
+
"""Submit a new task to the system."""
|
|
85
|
+
import uuid
|
|
86
|
+
task_id = f"task-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}-{uuid.uuid4().hex[:6]}"
|
|
87
|
+
|
|
88
|
+
task = Task(
|
|
89
|
+
id=task_id,
|
|
90
|
+
spec=spec,
|
|
91
|
+
branch=branch,
|
|
92
|
+
base_commit=base_commit,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Persist
|
|
96
|
+
self.checkpoint.save_task(task_id, json.dumps(spec.model_dump()),
|
|
97
|
+
branch, base_commit)
|
|
98
|
+
|
|
99
|
+
# Enqueue
|
|
100
|
+
self.task_queue.enqueue(task)
|
|
101
|
+
|
|
102
|
+
# Publish event
|
|
103
|
+
await self.event_bus.publish(DevHiveEvent(
|
|
104
|
+
event_type="task.created",
|
|
105
|
+
task_id=task_id,
|
|
106
|
+
payload={"task": task.model_dump()},
|
|
107
|
+
))
|
|
108
|
+
|
|
109
|
+
return task_id
|
|
110
|
+
|
|
111
|
+
async def _on_task_created(self, event: DevHiveEvent):
|
|
112
|
+
"""A new task was created — try to dispatch it."""
|
|
113
|
+
task = self.task_queue.peek(event.task_id)
|
|
114
|
+
if task:
|
|
115
|
+
await self._try_dispatch(task)
|
|
116
|
+
|
|
117
|
+
async def _on_agent_idle(self, event: DevHiveEvent):
|
|
118
|
+
"""An agent became idle — try to dispatch pending tasks."""
|
|
119
|
+
agent_type_str = event.payload.get("agent_type", "")
|
|
120
|
+
agent_map = {
|
|
121
|
+
"execute": AgentType.EXECUTE,
|
|
122
|
+
"static_verifier": AgentType.STATIC_VERIFIER,
|
|
123
|
+
"dynamic_verifier": AgentType.DYNAMIC_VERIFIER,
|
|
124
|
+
"semantic_verifier": AgentType.SEMANTIC_VERIFIER,
|
|
125
|
+
}
|
|
126
|
+
atype = agent_map.get(agent_type_str)
|
|
127
|
+
if atype:
|
|
128
|
+
task = self.task_queue.next_pending(agent_type_str)
|
|
129
|
+
if task:
|
|
130
|
+
handle = self.agent_pool.get_idle(atype)
|
|
131
|
+
if handle:
|
|
132
|
+
self.agent_pool.dispatch(handle, task)
|
|
133
|
+
self._increment_attempt(task.id, "execute")
|
|
134
|
+
self.checkpoint.update_task_stage(task.id, "EXECUTE")
|
|
135
|
+
|
|
136
|
+
async def _on_handoff(self, event: DevHiveEvent):
|
|
137
|
+
"""Execute Agent produced a Handoff → trigger L1 verification."""
|
|
138
|
+
task_id = event.task_id
|
|
139
|
+
handoff_data = event.payload.get("handoff", {})
|
|
140
|
+
task = self.task_queue.peek(task_id)
|
|
141
|
+
|
|
142
|
+
if not task:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# Save checkpoint
|
|
146
|
+
self.checkpoint.save_checkpoint(
|
|
147
|
+
checkpoint_id=f"{task_id}-execute",
|
|
148
|
+
task_id=task_id,
|
|
149
|
+
stage="EXECUTE",
|
|
150
|
+
agent_id=event.payload.get("agent_id", ""),
|
|
151
|
+
handoff_json=json.dumps(handoff_data),
|
|
152
|
+
outcome="COMPLETED",
|
|
153
|
+
)
|
|
154
|
+
self.checkpoint.update_task_stage(task_id, "VERIFY_L1")
|
|
155
|
+
|
|
156
|
+
# Run L1 verification
|
|
157
|
+
static, dynamic = await self.verification.run_l1(task)
|
|
158
|
+
|
|
159
|
+
# Evaluate convergence
|
|
160
|
+
attempt = self._get_attempt(task_id, "l1")
|
|
161
|
+
decision = self.convergence.evaluate_l1(task, static, dynamic, attempt)
|
|
162
|
+
|
|
163
|
+
# Save checkpoints
|
|
164
|
+
self.checkpoint.save_checkpoint(
|
|
165
|
+
checkpoint_id=f"{task_id}-static",
|
|
166
|
+
task_id=task_id, stage="VERIFY_L1",
|
|
167
|
+
agent_id="static-verifier",
|
|
168
|
+
verdict_json=json.dumps(static.model_dump()),
|
|
169
|
+
outcome=static.overall.value,
|
|
170
|
+
)
|
|
171
|
+
self.checkpoint.save_checkpoint(
|
|
172
|
+
checkpoint_id=f"{task_id}-dynamic",
|
|
173
|
+
task_id=task_id, stage="VERIFY_L1",
|
|
174
|
+
agent_id="dynamic-verifier",
|
|
175
|
+
verdict_json=json.dumps(dynamic.model_dump()),
|
|
176
|
+
outcome=dynamic.overall.value,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Act on decision
|
|
180
|
+
await self._handle_convergence(task_id, decision)
|
|
181
|
+
|
|
182
|
+
async def _on_verdict(self, event: DevHiveEvent):
|
|
183
|
+
"""Handle manual verdict submission."""
|
|
184
|
+
pass # Reserved for human-in-the-loop verdict input
|
|
185
|
+
|
|
186
|
+
async def _on_escalation(self, event: DevHiveEvent):
|
|
187
|
+
"""An escalation was triggered — notify human operators."""
|
|
188
|
+
task_id = event.task_id
|
|
189
|
+
report = event.payload.get("report", {})
|
|
190
|
+
self.checkpoint.save_escalation(
|
|
191
|
+
escalation_id=report.get("escalation_id", f"esc-{task_id}"),
|
|
192
|
+
task_id=task_id,
|
|
193
|
+
report_json=json.dumps(report),
|
|
194
|
+
)
|
|
195
|
+
self._notify_human(task_id, report)
|
|
196
|
+
|
|
197
|
+
async def _handle_convergence(self, task_id: str,
|
|
198
|
+
decision) -> None:
|
|
199
|
+
"""Process a convergence decision."""
|
|
200
|
+
task = self.task_queue.peek(task_id)
|
|
201
|
+
|
|
202
|
+
if decision.action == ConcurrencyAction.PASS:
|
|
203
|
+
# L1 passed → run L2
|
|
204
|
+
self._increment_attempt(task_id, "l2")
|
|
205
|
+
self.checkpoint.update_task_stage(task_id, "VERIFY_L2")
|
|
206
|
+
semantic = await self.verification.run_l2(task)
|
|
207
|
+
mutation = await self.verification.run_mutation(task)
|
|
208
|
+
|
|
209
|
+
l2_decision = self.convergence.evaluate_l2(
|
|
210
|
+
task, Verdict(verifier_type="static", task_id=task_id,
|
|
211
|
+
overall=VerdictOverall.PASS, findings=[]),
|
|
212
|
+
Verdict(verifier_type="dynamic", task_id=task_id,
|
|
213
|
+
overall=VerdictOverall.PASS, findings=[]),
|
|
214
|
+
semantic, mutation,
|
|
215
|
+
self._get_attempt(task_id, "l2"),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
self.checkpoint.save_checkpoint(
|
|
219
|
+
checkpoint_id=f"{task_id}-semantic",
|
|
220
|
+
task_id=task_id, stage="VERIFY_L2",
|
|
221
|
+
agent_id="semantic-verifier",
|
|
222
|
+
verdict_json=json.dumps(semantic.model_dump()),
|
|
223
|
+
outcome=semantic.alignment.value,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if l2_decision.action == ConcurrencyAction.PASS:
|
|
227
|
+
self.checkpoint.update_task_stage(task_id, "MERGE")
|
|
228
|
+
await self.event_bus.publish(DevHiveEvent(
|
|
229
|
+
event_type="convergence.reached",
|
|
230
|
+
task_id=task_id,
|
|
231
|
+
payload={"status": "ready_to_merge"},
|
|
232
|
+
))
|
|
233
|
+
else:
|
|
234
|
+
await self._escalate(task_id, l2_decision)
|
|
235
|
+
|
|
236
|
+
elif decision.action == ConcurrencyAction.FIX:
|
|
237
|
+
# Retry with fix strategy
|
|
238
|
+
await self._retry_task(task_id, decision.fix_strategy)
|
|
239
|
+
|
|
240
|
+
elif decision.action == ConcurrencyAction.ESCALATE:
|
|
241
|
+
await self._escalate(task_id, decision)
|
|
242
|
+
|
|
243
|
+
elif decision.action == ConcurrencyAction.CONFLICT:
|
|
244
|
+
await self._escalate(task_id, decision)
|
|
245
|
+
|
|
246
|
+
async def _retry_task(self, task_id: str, fix_strategy: str = None):
|
|
247
|
+
"""Retry execution with a fix strategy."""
|
|
248
|
+
task = self.task_queue.peek(task_id)
|
|
249
|
+
if task:
|
|
250
|
+
self.task_queue.enqueue(task)
|
|
251
|
+
await self._try_dispatch(task)
|
|
252
|
+
|
|
253
|
+
async def _escalate(self, task_id: str, decision) -> None:
|
|
254
|
+
"""Escalate to human operators."""
|
|
255
|
+
escalation = decision.escalation or {
|
|
256
|
+
"escalation_id": f"esc-{task_id}",
|
|
257
|
+
"task_id": task_id,
|
|
258
|
+
"triggered_by": decision.reason,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await self.event_bus.publish(DevHiveEvent(
|
|
262
|
+
event_type="escalation.needed",
|
|
263
|
+
task_id=task_id,
|
|
264
|
+
payload={"report": escalation},
|
|
265
|
+
))
|
|
266
|
+
|
|
267
|
+
async def _try_dispatch(self, task: Task):
|
|
268
|
+
"""Try to dispatch a task to an idle Execute Agent."""
|
|
269
|
+
handle = self.agent_pool.get_idle(AgentType.EXECUTE)
|
|
270
|
+
if handle:
|
|
271
|
+
self.agent_pool.dispatch(handle, task)
|
|
272
|
+
self._increment_attempt(task.id, "execute")
|
|
273
|
+
self.checkpoint.update_task_stage(task.id, "EXECUTE")
|
|
274
|
+
|
|
275
|
+
async def _collect_results(self):
|
|
276
|
+
"""Background task: collect results from agent processes."""
|
|
277
|
+
while True:
|
|
278
|
+
await asyncio.sleep(0.5)
|
|
279
|
+
for agent_id, handle in list(self.agent_pool.agents.items()):
|
|
280
|
+
try:
|
|
281
|
+
while not handle.result_queue.empty():
|
|
282
|
+
result = handle.result_queue.get_nowait()
|
|
283
|
+
await self._process_agent_result(agent_id, result)
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
async def _process_agent_result(self, agent_id: str, result: dict):
|
|
288
|
+
"""Process a result from an agent process."""
|
|
289
|
+
result_type = result.get("type", "")
|
|
290
|
+
|
|
291
|
+
if result_type == "agent.idle":
|
|
292
|
+
self.agent_pool.mark_idle(agent_id)
|
|
293
|
+
await self.event_bus.publish(DevHiveEvent(
|
|
294
|
+
event_type="agent.idle",
|
|
295
|
+
task_id="",
|
|
296
|
+
payload={"agent_id": agent_id,
|
|
297
|
+
"agent_type": result.get("agent_type", "")},
|
|
298
|
+
))
|
|
299
|
+
|
|
300
|
+
elif result_type == "success":
|
|
301
|
+
task_id = result.get("task_id", "")
|
|
302
|
+
self.agent_pool.mark_idle(agent_id)
|
|
303
|
+
|
|
304
|
+
if "handoff" in result.get("result", {}):
|
|
305
|
+
# Execute agent produced a handoff
|
|
306
|
+
await self.event_bus.publish(DevHiveEvent(
|
|
307
|
+
event_type="handoff.emitted",
|
|
308
|
+
task_id=task_id,
|
|
309
|
+
payload={"handoff": result["result"]["handoff"],
|
|
310
|
+
"agent_id": agent_id},
|
|
311
|
+
))
|
|
312
|
+
|
|
313
|
+
elif result_type == "stuck":
|
|
314
|
+
task_id = result.get("task_id", "")
|
|
315
|
+
self.agent_pool.mark_idle(agent_id)
|
|
316
|
+
await self.event_bus.publish(DevHiveEvent(
|
|
317
|
+
event_type="escalation.needed",
|
|
318
|
+
task_id=task_id,
|
|
319
|
+
payload={"reason": "agent_stuck",
|
|
320
|
+
"error": result.get("error", ""),
|
|
321
|
+
"agent_id": agent_id},
|
|
322
|
+
))
|
|
323
|
+
|
|
324
|
+
elif result_type == "error":
|
|
325
|
+
task_id = result.get("task_id", "")
|
|
326
|
+
self.agent_pool.mark_idle(agent_id)
|
|
327
|
+
await self.event_bus.publish(DevHiveEvent(
|
|
328
|
+
event_type="escalation.needed",
|
|
329
|
+
task_id=task_id,
|
|
330
|
+
payload={"reason": "agent_error",
|
|
331
|
+
"error": result.get("error", ""),
|
|
332
|
+
"agent_id": agent_id},
|
|
333
|
+
))
|
|
334
|
+
|
|
335
|
+
def _increment_attempt(self, task_id: str, stage: str):
|
|
336
|
+
if task_id not in self._task_attempts:
|
|
337
|
+
self._task_attempts[task_id] = {}
|
|
338
|
+
self._task_attempts[task_id][stage] = \
|
|
339
|
+
self._task_attempts[task_id].get(stage, 0) + 1
|
|
340
|
+
|
|
341
|
+
def _get_attempt(self, task_id: str, stage: str) -> int:
|
|
342
|
+
return self._task_attempts.get(task_id, {}).get(stage, 1)
|
|
343
|
+
|
|
344
|
+
def _notify_human(self, task_id: str, report: dict):
|
|
345
|
+
"""Send desktop notification for escalations."""
|
|
346
|
+
msg = f"[DevHive] Task {task_id} needs attention\n{report.get('triggered_by', '')}"
|
|
347
|
+
try:
|
|
348
|
+
import subprocess
|
|
349
|
+
subprocess.run(["notify-send", "DevHive Escalation", msg],
|
|
350
|
+
timeout=5)
|
|
351
|
+
except Exception:
|
|
352
|
+
pass # notify-send not available
|
|
353
|
+
print(f"\n{'='*60}\nESCALATION: {task_id}\n{msg}\n{'='*60}\n")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Event Bus — async publish/subscribe for agent communication."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from typing import Callable, Awaitable
|
|
6
|
+
|
|
7
|
+
from protocol.schemas import DevHiveEvent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
EventHandler = Callable[[DevHiveEvent], Awaitable[None]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EventBus:
|
|
14
|
+
"""Lightweight pub/sub event bus for orchestrator-agent communication."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self._subscribers: dict[str, list[EventHandler]] = defaultdict(list)
|
|
18
|
+
self._queue: asyncio.Queue[DevHiveEvent] = asyncio.Queue()
|
|
19
|
+
self._running = False
|
|
20
|
+
self._task: asyncio.Task | None = None
|
|
21
|
+
|
|
22
|
+
def subscribe(self, event_type: str, handler: EventHandler):
|
|
23
|
+
"""Register a handler for a specific event type."""
|
|
24
|
+
self._subscribers[event_type].append(handler)
|
|
25
|
+
|
|
26
|
+
async def publish(self, event: DevHiveEvent):
|
|
27
|
+
"""Publish an event to the bus. Non-blocking."""
|
|
28
|
+
await self._queue.put(event)
|
|
29
|
+
|
|
30
|
+
async def start(self):
|
|
31
|
+
"""Start the event processing loop."""
|
|
32
|
+
self._running = True
|
|
33
|
+
self._task = asyncio.create_task(self._process_events())
|
|
34
|
+
|
|
35
|
+
async def stop(self):
|
|
36
|
+
"""Stop the event processing loop."""
|
|
37
|
+
self._running = False
|
|
38
|
+
if self._task:
|
|
39
|
+
self._task.cancel()
|
|
40
|
+
try:
|
|
41
|
+
await self._task
|
|
42
|
+
except asyncio.CancelledError:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
async def _process_events(self):
|
|
46
|
+
"""Main event processing loop."""
|
|
47
|
+
while self._running:
|
|
48
|
+
try:
|
|
49
|
+
event = await asyncio.wait_for(self._queue.get(), timeout=1.0)
|
|
50
|
+
handlers = self._subscribers.get(event.event_type, [])
|
|
51
|
+
# Dispatch to all handlers concurrently
|
|
52
|
+
tasks = [handler(event) for handler in handlers]
|
|
53
|
+
if tasks:
|
|
54
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
55
|
+
except asyncio.TimeoutError:
|
|
56
|
+
continue
|
|
57
|
+
except Exception:
|
|
58
|
+
continue
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Task Queue — priority-based task scheduling."""
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from protocol.schemas import Task, Priority
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class QueueEntry:
|
|
12
|
+
task: Task
|
|
13
|
+
enqueued_at: float
|
|
14
|
+
priority: Priority = Priority.MEDIUM
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TaskQueue:
|
|
18
|
+
"""Simple priority-based task queue with FIFO within each priority level."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self._queues: dict[Priority, deque[QueueEntry]] = {
|
|
22
|
+
Priority.CRITICAL: deque(),
|
|
23
|
+
Priority.HIGH: deque(),
|
|
24
|
+
Priority.MEDIUM: deque(),
|
|
25
|
+
Priority.LOW: deque(),
|
|
26
|
+
}
|
|
27
|
+
self._all_tasks: dict[str, Task] = {}
|
|
28
|
+
|
|
29
|
+
def enqueue(self, task: Task) -> QueueEntry:
|
|
30
|
+
import time
|
|
31
|
+
entry = QueueEntry(task=task, enqueued_at=time.monotonic(),
|
|
32
|
+
priority=task.spec.priority)
|
|
33
|
+
self._queues[task.spec.priority].append(entry)
|
|
34
|
+
self._all_tasks[task.id] = task
|
|
35
|
+
return entry
|
|
36
|
+
|
|
37
|
+
def next_pending(self, agent_type: str = None) -> Optional[Task]:
|
|
38
|
+
"""Get the next pending task by priority."""
|
|
39
|
+
for priority in (Priority.CRITICAL, Priority.HIGH,
|
|
40
|
+
Priority.MEDIUM, Priority.LOW):
|
|
41
|
+
if self._queues[priority]:
|
|
42
|
+
entry = self._queues[priority].popleft()
|
|
43
|
+
return entry.task
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
def peek(self, task_id: str) -> Optional[Task]:
|
|
47
|
+
return self._all_tasks.get(task_id)
|
|
48
|
+
|
|
49
|
+
def pending_count(self) -> int:
|
|
50
|
+
return sum(len(q) for q in self._queues.values())
|
|
51
|
+
|
|
52
|
+
def remove(self, task_id: str):
|
|
53
|
+
"""Remove a task from the queue."""
|
|
54
|
+
for q in self._queues.values():
|
|
55
|
+
for i, entry in enumerate(q):
|
|
56
|
+
if entry.task.id == task_id:
|
|
57
|
+
del q[i]
|
|
58
|
+
break
|
|
59
|
+
self._all_tasks.pop(task_id, None)
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oswaldzsh/devhive",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Multi-Agent Software Development System — autonomous coding with verify-specialized agents, failure signatures, and structured handoff protocols.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"agent",
|
|
8
|
+
"coding",
|
|
9
|
+
"devtool",
|
|
10
|
+
"multi-agent",
|
|
11
|
+
"claude",
|
|
12
|
+
"orchestration",
|
|
13
|
+
"verification",
|
|
14
|
+
"autonomous"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/lejurobot/devhive",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"bin": {
|
|
19
|
+
"dh": "bin/dh"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"bin/",
|
|
23
|
+
"control_plane/",
|
|
24
|
+
"agents/",
|
|
25
|
+
"orchestrator/",
|
|
26
|
+
"protocol/",
|
|
27
|
+
"verification/",
|
|
28
|
+
"signature/",
|
|
29
|
+
"storage/",
|
|
30
|
+
"tools/",
|
|
31
|
+
"signatures/",
|
|
32
|
+
"config.yaml",
|
|
33
|
+
"setup.py",
|
|
34
|
+
"install.sh",
|
|
35
|
+
"__init__.py"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"postinstall": "bash install.sh || echo 'DevHive: Python deps not installed. Run: pip3 install httpx pydantic pyyaml rich'",
|
|
39
|
+
"uninstall": "rm -rf ~/.devhive"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/lejurobot/devhive"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=16.0.0"
|
|
47
|
+
},
|
|
48
|
+
"os": ["darwin", "linux"],
|
|
49
|
+
"preferGlobal": true
|
|
50
|
+
}
|
|
File without changes
|