delimit-cli 4.0.6 → 4.1.1
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/CHANGELOG.md +578 -0
- package/README.md +2 -2
- package/bin/delimit-cli.js +33 -0
- package/gateway/ai/agent_dispatch.py +2 -34
- package/gateway/ai/backends/gateway_core.py +1012 -0
- package/gateway/ai/github_scanner.py +1 -1
- package/gateway/ai/ledger_manager.py +3 -13
- package/gateway/ai/loop_engine.py +372 -175
- package/gateway/ai/notify.py +2 -1662
- package/gateway/ai/reddit_scanner.py +0 -34
- package/gateway/ai/screen_record.py +1 -1
- package/gateway/ai/server.py +598 -141
- package/gateway/ai/swarm.py +1 -1
- package/gateway/ai/tui.py +87 -6
- package/gateway/core/diff_engine_v2.py +5 -0
- package/gateway/core/spec_health.py +624 -0
- package/lib/delimit-template.js +0 -5
- package/package.json +2 -2
- package/scripts/security-check.sh +0 -12
package/gateway/ai/swarm.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Implements Agent Swarm Standard v1.2 (4-party consent achieved 2026-03-30).
|
|
4
4
|
Each venture gets 5 agent roles bound to AI models with namespace isolation.
|
|
5
5
|
|
|
6
|
-
Config: ~/.delimit/
|
|
6
|
+
Config: ~/.delimit/governance/AGENT_SWARM_STANDARD.yml
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import json
|
package/gateway/ai/tui.py
CHANGED
|
@@ -34,18 +34,21 @@ SESSIONS_DIR = Path.home() / ".delimit" / "sessions"
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def _load_ledger_items(status: str = "open", limit: int = 20) -> List[Dict]:
|
|
37
|
-
|
|
37
|
+
# Deduplicate by ID — last entry wins (append-only JSONL)
|
|
38
|
+
by_id: Dict[str, Dict] = {}
|
|
38
39
|
for fname in ("operations.jsonl", "strategy.jsonl"):
|
|
39
40
|
path = LEDGER_DIR / fname
|
|
40
41
|
if not path.exists():
|
|
41
42
|
continue
|
|
42
|
-
for line in path.read_text().strip().split("\n")
|
|
43
|
+
for line in path.read_text().strip().split("\n"):
|
|
43
44
|
try:
|
|
44
45
|
d = json.loads(line)
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
item_id = d.get("id", "")
|
|
47
|
+
if item_id:
|
|
48
|
+
by_id[item_id] = d
|
|
47
49
|
except json.JSONDecodeError:
|
|
48
50
|
continue
|
|
51
|
+
items = [d for d in by_id.values() if d.get("status") == status]
|
|
49
52
|
items.sort(key=lambda x: (0 if x.get("priority") == "P0" else 1 if x.get("priority") == "P1" else 2))
|
|
50
53
|
return items[:limit]
|
|
51
54
|
|
|
@@ -154,6 +157,48 @@ class SessionPanel(Static):
|
|
|
154
157
|
content.update("\n".join(lines))
|
|
155
158
|
|
|
156
159
|
|
|
160
|
+
class VenturesPanel(Static):
|
|
161
|
+
"""Ventures as app tiles — each venture is an 'app' in the OS."""
|
|
162
|
+
|
|
163
|
+
def compose(self) -> ComposeResult:
|
|
164
|
+
yield Static(id="ventures-content")
|
|
165
|
+
|
|
166
|
+
def on_mount(self) -> None:
|
|
167
|
+
self._refresh_data()
|
|
168
|
+
self.set_interval(30, self._refresh_data)
|
|
169
|
+
|
|
170
|
+
def _refresh_data(self) -> None:
|
|
171
|
+
content = self.query_one("#ventures-content", Static)
|
|
172
|
+
swarm = _load_swarm_status()
|
|
173
|
+
by_venture = swarm.get("by_venture", {})
|
|
174
|
+
|
|
175
|
+
if not by_venture:
|
|
176
|
+
content.update("[dim]No ventures registered. Run delimit_swarm(action='register').[/]")
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# Count open items per venture
|
|
180
|
+
all_items = _load_ledger_items("open", 999)
|
|
181
|
+
venture_items = {}
|
|
182
|
+
for item in all_items:
|
|
183
|
+
v = item.get("venture", "root")
|
|
184
|
+
venture_items[v] = venture_items.get(v, 0) + 1
|
|
185
|
+
|
|
186
|
+
lines = [
|
|
187
|
+
"[bold]Ventures[/] — each venture is an app in Delimit OS\n",
|
|
188
|
+
]
|
|
189
|
+
for venture, agent_count in sorted(by_venture.items()):
|
|
190
|
+
open_count = venture_items.get(venture, venture_items.get(f"{venture}-mcp", 0))
|
|
191
|
+
status_icon = "[green]●[/]" if agent_count > 0 else "[red]○[/]"
|
|
192
|
+
lines.append(
|
|
193
|
+
f" {status_icon} [bold cyan]{venture}[/]"
|
|
194
|
+
f" | {agent_count} agents"
|
|
195
|
+
f" | {open_count} open items"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
lines.append(f"\n[dim]Total: {len(by_venture)} ventures, {swarm['agents']} agents[/]")
|
|
199
|
+
content.update("\n".join(lines))
|
|
200
|
+
|
|
201
|
+
|
|
157
202
|
class GovernanceBar(Static):
|
|
158
203
|
"""Top status bar — governance health at a glance."""
|
|
159
204
|
|
|
@@ -213,6 +258,7 @@ class DelimitOS(App):
|
|
|
213
258
|
("q", "quit", "Quit"),
|
|
214
259
|
("l", "focus_ledger", "Ledger"),
|
|
215
260
|
("s", "focus_swarm", "Swarm"),
|
|
261
|
+
("v", "focus_ventures", "Ventures"),
|
|
216
262
|
("h", "focus_sessions", "History"),
|
|
217
263
|
("r", "refresh", "Refresh"),
|
|
218
264
|
("t", "think", "Think"),
|
|
@@ -226,6 +272,8 @@ class DelimitOS(App):
|
|
|
226
272
|
yield LedgerPanel()
|
|
227
273
|
with TabPane("Swarm", id="tab-swarm"):
|
|
228
274
|
yield SwarmPanel()
|
|
275
|
+
with TabPane("Ventures", id="tab-ventures"):
|
|
276
|
+
yield VenturesPanel()
|
|
229
277
|
with TabPane("Sessions", id="tab-sessions"):
|
|
230
278
|
yield SessionPanel()
|
|
231
279
|
yield Footer()
|
|
@@ -236,6 +284,9 @@ class DelimitOS(App):
|
|
|
236
284
|
def action_focus_swarm(self) -> None:
|
|
237
285
|
self.query_one(TabbedContent).active = "tab-swarm"
|
|
238
286
|
|
|
287
|
+
def action_focus_ventures(self) -> None:
|
|
288
|
+
self.query_one(TabbedContent).active = "tab-ventures"
|
|
289
|
+
|
|
239
290
|
def action_focus_sessions(self) -> None:
|
|
240
291
|
self.query_one(TabbedContent).active = "tab-sessions"
|
|
241
292
|
|
|
@@ -248,11 +299,41 @@ class DelimitOS(App):
|
|
|
248
299
|
panel._refresh_data()
|
|
249
300
|
self.query_one(GovernanceBar)._refresh()
|
|
250
301
|
|
|
302
|
+
@work(thread=True)
|
|
251
303
|
def action_think(self) -> None:
|
|
252
|
-
|
|
304
|
+
"""Trigger deliberation in background thread."""
|
|
305
|
+
self.notify("Deliberation starting...", title="Think")
|
|
306
|
+
try:
|
|
307
|
+
from ai.deliberation import deliberate
|
|
308
|
+
result = deliberate(
|
|
309
|
+
"Based on the current ledger and recent signals, what should the swarm build next?",
|
|
310
|
+
mode="dialogue",
|
|
311
|
+
max_rounds=2,
|
|
312
|
+
)
|
|
313
|
+
if result.get("mode") == "single_model_reflection":
|
|
314
|
+
verdict = result.get("synthesis", "No synthesis")[:200]
|
|
315
|
+
else:
|
|
316
|
+
verdict = result.get("final_verdict", "No consensus")
|
|
317
|
+
if isinstance(verdict, str):
|
|
318
|
+
verdict = verdict[:200]
|
|
319
|
+
else:
|
|
320
|
+
verdict = str(verdict)[:200]
|
|
321
|
+
self.notify(verdict, title="Think Result", timeout=15)
|
|
322
|
+
except Exception as e:
|
|
323
|
+
self.notify(f"Deliberation failed: {e}", title="Think Error", severity="error")
|
|
253
324
|
|
|
254
325
|
def action_build(self) -> None:
|
|
255
|
-
|
|
326
|
+
"""Show next buildable item from ledger."""
|
|
327
|
+
items = _load_ledger_items("open", 5)
|
|
328
|
+
if items:
|
|
329
|
+
top = items[0]
|
|
330
|
+
self.notify(
|
|
331
|
+
f"{top.get('id', '?')} [{top.get('priority', '?')}]: {top.get('title', '?')[:60]}",
|
|
332
|
+
title="Next Build Item",
|
|
333
|
+
timeout=10,
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
self.notify("Ledger is clear — nothing to build!", title="Build")
|
|
256
337
|
|
|
257
338
|
|
|
258
339
|
def main():
|
|
@@ -367,6 +367,11 @@ class OpenAPIDiffEngine:
|
|
|
367
367
|
|
|
368
368
|
def _compare_schema_deep(self, path: str, old_schema: Dict, new_schema: Dict, required_fields: Optional[Set[str]] = None):
|
|
369
369
|
"""Deep comparison of schemas including nested objects."""
|
|
370
|
+
# Guard against None schemas
|
|
371
|
+
if old_schema is None:
|
|
372
|
+
old_schema = {}
|
|
373
|
+
if new_schema is None:
|
|
374
|
+
new_schema = {}
|
|
370
375
|
|
|
371
376
|
# Handle references
|
|
372
377
|
if "$ref" in old_schema or "$ref" in new_schema:
|