delimit-cli 4.0.5 → 4.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 +2 -2
- package/bin/delimit-cli.js +33 -0
- package/bin/delimit-os.sh +105 -0
- package/gateway/ai/agent_dispatch.py +2 -34
- 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/server.py +4 -3
- package/gateway/ai/tui.py +377 -0
- package/lib/delimit-template.js +0 -5
- package/package.json +2 -2
- package/scripts/security-check.sh +0 -12
|
@@ -560,37 +560,3 @@ def _save_scan(result: Dict[str, Any], scan_time: datetime) -> Path:
|
|
|
560
560
|
path.write_text(json.dumps(result, indent=2, default=str))
|
|
561
561
|
logger.info("Scan saved to %s", path)
|
|
562
562
|
return path
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
def fetch_thread(thread_id: str, *, proxy_url: str = PROXY_URL) -> Optional[Dict[str, Any]]:
|
|
566
|
-
"""Fetch a single Reddit thread by ID via the residential proxy."""
|
|
567
|
-
import urllib.parse
|
|
568
|
-
import urllib.request
|
|
569
|
-
reddit_url = f"https://www.reddit.com/comments/{thread_id}.json?raw_json=1"
|
|
570
|
-
fetch_url = f"{proxy_url}?url={urllib.parse.quote(reddit_url, safe='')}"
|
|
571
|
-
|
|
572
|
-
req = urllib.request.Request(
|
|
573
|
-
fetch_url,
|
|
574
|
-
headers={"User-Agent": "delimit-scanner/1.0", "Accept": "application/json"},
|
|
575
|
-
)
|
|
576
|
-
|
|
577
|
-
try:
|
|
578
|
-
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
579
|
-
data = json.loads(resp.read().decode())
|
|
580
|
-
if isinstance(data, list) and len(data) > 0:
|
|
581
|
-
post_data = data[0].get("data", {}).get("children", [{}])[0].get("data", {})
|
|
582
|
-
if post_data:
|
|
583
|
-
return {
|
|
584
|
-
"id": post_data.get("id", ""),
|
|
585
|
-
"title": post_data.get("title", ""),
|
|
586
|
-
"author": post_data.get("author", ""),
|
|
587
|
-
"score": post_data.get("score", 0),
|
|
588
|
-
"num_comments": post_data.get("num_comments", 0),
|
|
589
|
-
"subreddit": post_data.get("subreddit", ""),
|
|
590
|
-
"permalink": post_data.get("permalink", ""),
|
|
591
|
-
"selftext": post_data.get("selftext", ""),
|
|
592
|
-
"created_utc": post_data.get("created_utc", 0),
|
|
593
|
-
}
|
|
594
|
-
except Exception as exc:
|
|
595
|
-
logger.warning("Failed to fetch thread %s: %s", thread_id, exc)
|
|
596
|
-
return None
|
package/gateway/ai/server.py
CHANGED
|
@@ -4906,9 +4906,10 @@ def delimit_deliberate(
|
|
|
4906
4906
|
) -> Dict[str, Any]:
|
|
4907
4907
|
"""Run multi-model consensus via real AI-to-AI deliberation (Pro).
|
|
4908
4908
|
|
|
4909
|
-
Models
|
|
4910
|
-
Free tier: 3 deliberations using
|
|
4911
|
-
BYOK: configure your own API keys in ~/.delimit/models.json for unlimited use
|
|
4909
|
+
Models debate each other directly until unanimous agreement.
|
|
4910
|
+
Free tier: 3 deliberations using Gemini Flash + GPT-4o-mini, no setup required.
|
|
4911
|
+
BYOK: configure your own API keys in ~/.delimit/models.json for unlimited use
|
|
4912
|
+
with any models (Grok, Claude, Gemini Pro, GPT-4o, etc).
|
|
4912
4913
|
|
|
4913
4914
|
Args:
|
|
4914
4915
|
question: The question to reach consensus on.
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Delimit TUI — Terminal User Interface (Phase 2 of Delimit OS).
|
|
2
|
+
|
|
3
|
+
The proprietary terminal experience. Type 'delimit' and get an OS-like
|
|
4
|
+
environment with panels for ledger, swarm, memory, and live logs.
|
|
5
|
+
|
|
6
|
+
Enterprise-ready: zero JS, pure Python, works over SSH, sub-2s boot.
|
|
7
|
+
Designed for devs who hate browser-based tools.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python -m ai.tui # Full TUI
|
|
11
|
+
python -m ai.tui --quick # Quick status (no interactive mode)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from textual.app import App, ComposeResult
|
|
15
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
16
|
+
from textual.widgets import (
|
|
17
|
+
Header, Footer, Static, DataTable, Log, TabbedContent, TabPane,
|
|
18
|
+
Label, ProgressBar, Button, Input,
|
|
19
|
+
)
|
|
20
|
+
from textual.timer import Timer
|
|
21
|
+
from textual import work
|
|
22
|
+
import json
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Dict, List
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── Data loaders ─────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
LEDGER_DIR = Path.home() / ".delimit" / "ledger"
|
|
31
|
+
SWARM_DIR = Path.home() / ".delimit" / "swarm"
|
|
32
|
+
MEMORY_DIR = Path.home() / ".delimit" / "memory"
|
|
33
|
+
SESSIONS_DIR = Path.home() / ".delimit" / "sessions"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_ledger_items(status: str = "open", limit: int = 20) -> List[Dict]:
|
|
37
|
+
# Deduplicate by ID — last entry wins (append-only JSONL)
|
|
38
|
+
by_id: Dict[str, Dict] = {}
|
|
39
|
+
for fname in ("operations.jsonl", "strategy.jsonl"):
|
|
40
|
+
path = LEDGER_DIR / fname
|
|
41
|
+
if not path.exists():
|
|
42
|
+
continue
|
|
43
|
+
for line in path.read_text().strip().split("\n"):
|
|
44
|
+
try:
|
|
45
|
+
d = json.loads(line)
|
|
46
|
+
item_id = d.get("id", "")
|
|
47
|
+
if item_id:
|
|
48
|
+
by_id[item_id] = d
|
|
49
|
+
except json.JSONDecodeError:
|
|
50
|
+
continue
|
|
51
|
+
items = [d for d in by_id.values() if d.get("status") == status]
|
|
52
|
+
items.sort(key=lambda x: (0 if x.get("priority") == "P0" else 1 if x.get("priority") == "P1" else 2))
|
|
53
|
+
return items[:limit]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _load_swarm_status() -> Dict[str, Any]:
|
|
57
|
+
registry = SWARM_DIR / "agent_registry.json"
|
|
58
|
+
if not registry.exists():
|
|
59
|
+
return {"agents": 0, "ventures": 0}
|
|
60
|
+
try:
|
|
61
|
+
data = json.loads(registry.read_text())
|
|
62
|
+
agents = data.get("agents", {})
|
|
63
|
+
ventures = set(a.get("venture", "") for a in agents.values())
|
|
64
|
+
return {
|
|
65
|
+
"agents": len(agents),
|
|
66
|
+
"ventures": len(ventures),
|
|
67
|
+
"by_venture": {v: sum(1 for a in agents.values() if a.get("venture") == v) for v in ventures},
|
|
68
|
+
}
|
|
69
|
+
except (json.JSONDecodeError, KeyError):
|
|
70
|
+
return {"agents": 0, "ventures": 0}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _load_recent_sessions(limit: int = 5) -> List[Dict]:
|
|
74
|
+
if not SESSIONS_DIR.exists():
|
|
75
|
+
return []
|
|
76
|
+
sessions = []
|
|
77
|
+
for f in sorted(SESSIONS_DIR.glob("*.json"), reverse=True)[:limit]:
|
|
78
|
+
try:
|
|
79
|
+
sessions.append(json.loads(f.read_text()))
|
|
80
|
+
except (json.JSONDecodeError, KeyError):
|
|
81
|
+
continue
|
|
82
|
+
return sessions
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── Widgets ──────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
class LedgerPanel(Static):
|
|
88
|
+
"""Live ledger view — shows open items sorted by priority."""
|
|
89
|
+
|
|
90
|
+
def compose(self) -> ComposeResult:
|
|
91
|
+
yield DataTable(id="ledger-table")
|
|
92
|
+
|
|
93
|
+
def on_mount(self) -> None:
|
|
94
|
+
table = self.query_one("#ledger-table", DataTable)
|
|
95
|
+
table.add_columns("ID", "P", "Title", "Venture", "Type")
|
|
96
|
+
self._refresh_data()
|
|
97
|
+
self.set_interval(30, self._refresh_data)
|
|
98
|
+
|
|
99
|
+
def _refresh_data(self) -> None:
|
|
100
|
+
table = self.query_one("#ledger-table", DataTable)
|
|
101
|
+
table.clear()
|
|
102
|
+
for item in _load_ledger_items("open", 25):
|
|
103
|
+
table.add_row(
|
|
104
|
+
item.get("id", ""),
|
|
105
|
+
item.get("priority", ""),
|
|
106
|
+
item.get("title", "")[:60],
|
|
107
|
+
item.get("venture", "")[:15],
|
|
108
|
+
item.get("type", ""),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class SwarmPanel(Static):
|
|
113
|
+
"""Swarm status — agents, ventures, health."""
|
|
114
|
+
|
|
115
|
+
def compose(self) -> ComposeResult:
|
|
116
|
+
yield Static(id="swarm-content")
|
|
117
|
+
|
|
118
|
+
def on_mount(self) -> None:
|
|
119
|
+
self._refresh_data()
|
|
120
|
+
self.set_interval(15, self._refresh_data)
|
|
121
|
+
|
|
122
|
+
def _refresh_data(self) -> None:
|
|
123
|
+
content = self.query_one("#swarm-content", Static)
|
|
124
|
+
swarm = _load_swarm_status()
|
|
125
|
+
lines = [
|
|
126
|
+
f"[bold cyan]Agents:[/] {swarm['agents']} | [bold cyan]Ventures:[/] {swarm['ventures']}",
|
|
127
|
+
"",
|
|
128
|
+
]
|
|
129
|
+
for venture, count in swarm.get("by_venture", {}).items():
|
|
130
|
+
lines.append(f" [green]{venture}[/]: {count} agents")
|
|
131
|
+
content.update("\n".join(lines))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class SessionPanel(Static):
|
|
135
|
+
"""Recent sessions — handoff history."""
|
|
136
|
+
|
|
137
|
+
def compose(self) -> ComposeResult:
|
|
138
|
+
yield Static(id="session-content")
|
|
139
|
+
|
|
140
|
+
def on_mount(self) -> None:
|
|
141
|
+
self._refresh_data()
|
|
142
|
+
|
|
143
|
+
def _refresh_data(self) -> None:
|
|
144
|
+
content = self.query_one("#session-content", Static)
|
|
145
|
+
sessions = _load_recent_sessions(5)
|
|
146
|
+
if not sessions:
|
|
147
|
+
content.update("[dim]No sessions recorded yet.[/]")
|
|
148
|
+
return
|
|
149
|
+
lines = []
|
|
150
|
+
for s in sessions:
|
|
151
|
+
ts = s.get("timestamp", s.get("closed_at", ""))[:16]
|
|
152
|
+
summary = s.get("summary", "")[:80]
|
|
153
|
+
completed = len(s.get("items_completed", []))
|
|
154
|
+
lines.append(f"[dim]{ts}[/] — {summary}")
|
|
155
|
+
if completed:
|
|
156
|
+
lines.append(f" [green]✓ {completed} items completed[/]")
|
|
157
|
+
content.update("\n".join(lines))
|
|
158
|
+
|
|
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
|
+
|
|
202
|
+
class GovernanceBar(Static):
|
|
203
|
+
"""Top status bar — governance health at a glance."""
|
|
204
|
+
|
|
205
|
+
def compose(self) -> ComposeResult:
|
|
206
|
+
yield Static(id="gov-bar")
|
|
207
|
+
|
|
208
|
+
def on_mount(self) -> None:
|
|
209
|
+
self._refresh()
|
|
210
|
+
self.set_interval(60, self._refresh)
|
|
211
|
+
|
|
212
|
+
def _refresh(self) -> None:
|
|
213
|
+
bar = self.query_one("#gov-bar", Static)
|
|
214
|
+
ledger_count = len(_load_ledger_items("open", 999))
|
|
215
|
+
swarm = _load_swarm_status()
|
|
216
|
+
mode_file = Path.home() / ".delimit" / "enforcement_mode"
|
|
217
|
+
mode = mode_file.read_text().strip() if mode_file.exists() else "default"
|
|
218
|
+
|
|
219
|
+
bar.update(
|
|
220
|
+
f" [bold magenta]</>[/] [bold]Delimit OS[/] | "
|
|
221
|
+
f"[cyan]Ledger:[/] {ledger_count} open | "
|
|
222
|
+
f"[cyan]Swarm:[/] {swarm['agents']} agents / {swarm['ventures']} ventures | "
|
|
223
|
+
f"[cyan]Mode:[/] {mode} | "
|
|
224
|
+
f"[dim]{time.strftime('%H:%M')}[/]"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ── Main App ─────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
class DelimitOS(App):
|
|
231
|
+
"""Delimit OS — the AI developer operating system."""
|
|
232
|
+
|
|
233
|
+
CSS = """
|
|
234
|
+
Screen {
|
|
235
|
+
background: $surface;
|
|
236
|
+
}
|
|
237
|
+
#gov-bar {
|
|
238
|
+
height: 1;
|
|
239
|
+
background: $primary-background;
|
|
240
|
+
color: $text;
|
|
241
|
+
padding: 0 1;
|
|
242
|
+
}
|
|
243
|
+
TabbedContent {
|
|
244
|
+
height: 1fr;
|
|
245
|
+
}
|
|
246
|
+
DataTable {
|
|
247
|
+
height: 1fr;
|
|
248
|
+
}
|
|
249
|
+
#swarm-content, #session-content {
|
|
250
|
+
padding: 1;
|
|
251
|
+
}
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
TITLE = "Delimit OS"
|
|
255
|
+
SUB_TITLE = "AI Developer Operating System"
|
|
256
|
+
|
|
257
|
+
BINDINGS = [
|
|
258
|
+
("q", "quit", "Quit"),
|
|
259
|
+
("l", "focus_ledger", "Ledger"),
|
|
260
|
+
("s", "focus_swarm", "Swarm"),
|
|
261
|
+
("v", "focus_ventures", "Ventures"),
|
|
262
|
+
("h", "focus_sessions", "History"),
|
|
263
|
+
("r", "refresh", "Refresh"),
|
|
264
|
+
("t", "think", "Think"),
|
|
265
|
+
("b", "build", "Build"),
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
def compose(self) -> ComposeResult:
|
|
269
|
+
yield GovernanceBar()
|
|
270
|
+
with TabbedContent():
|
|
271
|
+
with TabPane("Ledger", id="tab-ledger"):
|
|
272
|
+
yield LedgerPanel()
|
|
273
|
+
with TabPane("Swarm", id="tab-swarm"):
|
|
274
|
+
yield SwarmPanel()
|
|
275
|
+
with TabPane("Ventures", id="tab-ventures"):
|
|
276
|
+
yield VenturesPanel()
|
|
277
|
+
with TabPane("Sessions", id="tab-sessions"):
|
|
278
|
+
yield SessionPanel()
|
|
279
|
+
yield Footer()
|
|
280
|
+
|
|
281
|
+
def action_focus_ledger(self) -> None:
|
|
282
|
+
self.query_one(TabbedContent).active = "tab-ledger"
|
|
283
|
+
|
|
284
|
+
def action_focus_swarm(self) -> None:
|
|
285
|
+
self.query_one(TabbedContent).active = "tab-swarm"
|
|
286
|
+
|
|
287
|
+
def action_focus_ventures(self) -> None:
|
|
288
|
+
self.query_one(TabbedContent).active = "tab-ventures"
|
|
289
|
+
|
|
290
|
+
def action_focus_sessions(self) -> None:
|
|
291
|
+
self.query_one(TabbedContent).active = "tab-sessions"
|
|
292
|
+
|
|
293
|
+
def action_refresh(self) -> None:
|
|
294
|
+
for panel in self.query(LedgerPanel):
|
|
295
|
+
panel._refresh_data()
|
|
296
|
+
for panel in self.query(SwarmPanel):
|
|
297
|
+
panel._refresh_data()
|
|
298
|
+
for panel in self.query(SessionPanel):
|
|
299
|
+
panel._refresh_data()
|
|
300
|
+
self.query_one(GovernanceBar)._refresh()
|
|
301
|
+
|
|
302
|
+
@work(thread=True)
|
|
303
|
+
def action_think(self) -> None:
|
|
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")
|
|
324
|
+
|
|
325
|
+
def action_build(self) -> None:
|
|
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")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def main():
|
|
340
|
+
"""Entry point for 'delimit' command."""
|
|
341
|
+
import sys
|
|
342
|
+
if "--quick" in sys.argv:
|
|
343
|
+
# Quick status mode — no interactive TUI
|
|
344
|
+
from rich.console import Console
|
|
345
|
+
from rich.table import Table
|
|
346
|
+
|
|
347
|
+
console = Console()
|
|
348
|
+
console.print("\n[bold magenta]</>[/] [bold]Delimit OS[/]\n")
|
|
349
|
+
|
|
350
|
+
swarm = _load_swarm_status()
|
|
351
|
+
items = _load_ledger_items("open", 10)
|
|
352
|
+
|
|
353
|
+
console.print(f"[cyan]Swarm:[/] {swarm['agents']} agents across {swarm['ventures']} ventures")
|
|
354
|
+
console.print(f"[cyan]Ledger:[/] {len(items)} open items\n")
|
|
355
|
+
|
|
356
|
+
if items:
|
|
357
|
+
table = Table(title="Open Items")
|
|
358
|
+
table.add_column("ID", style="dim")
|
|
359
|
+
table.add_column("P", style="bold")
|
|
360
|
+
table.add_column("Title")
|
|
361
|
+
table.add_column("Venture", style="green")
|
|
362
|
+
for item in items[:10]:
|
|
363
|
+
table.add_row(
|
|
364
|
+
item.get("id", ""),
|
|
365
|
+
item.get("priority", ""),
|
|
366
|
+
item.get("title", "")[:60],
|
|
367
|
+
item.get("venture", "")[:15],
|
|
368
|
+
)
|
|
369
|
+
console.print(table)
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
app = DelimitOS()
|
|
373
|
+
app.run()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
if __name__ == "__main__":
|
|
377
|
+
main()
|
package/lib/delimit-template.js
CHANGED
|
@@ -31,11 +31,6 @@ The founder reviews and approves via email. The AI's job is to draft, send, and
|
|
|
31
31
|
- **Session end or idle**: call \`delimit_session_handoff\` to preserve context for next session
|
|
32
32
|
- If returning user, summarize what's pending. If new project, call \`delimit_scan\`.
|
|
33
33
|
|
|
34
|
-
### Natural Language Triggers
|
|
35
|
-
- \`think and build\`, \`keep building\`, \`resume building\`, \`run the swarm\`: route through native Delimit session/build behavior, equivalent to \`delimit session --build\`
|
|
36
|
-
- \`ask delimit\`, \`what's next\`, \`check the ledger\`: route through native Delimit inspect behavior, equivalent to \`delimit session --inspect\` or \`delimit ask\`
|
|
37
|
-
- Bootstrap behavior must be identical across Codex, Claude Code, Gemini, Grok, and Cursor: resolve user + venture + repo, load continuity/session state, check ledger, check daemon/swarm status, then either inspect or resume/launch the governed loop
|
|
38
|
-
|
|
39
34
|
### Code Development (debounced per edit batch)
|
|
40
35
|
- After editing UI/CSS: call \`delimit_design_validate_responsive\`
|
|
41
36
|
- After editing API specs: call \`delimit_lint\` + \`delimit_drift_check\`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "delimit-cli",
|
|
3
3
|
"mcpName": "io.github.delimit-ai/delimit-mcp-server",
|
|
4
|
-
"version": "4.0
|
|
4
|
+
"version": "4.1.0",
|
|
5
5
|
"description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"url": "https://github.com/delimit-ai/delimit-mcp-server.git"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"axios": "
|
|
71
|
+
"axios": "1.13.6",
|
|
72
72
|
"chalk": "^4.1.2",
|
|
73
73
|
"commander": "^12.1.0",
|
|
74
74
|
"express": "^4.18.0",
|
|
@@ -52,18 +52,6 @@ else
|
|
|
52
52
|
echo "✅ clean"
|
|
53
53
|
fi
|
|
54
54
|
|
|
55
|
-
# 5. Continuity state must never ship
|
|
56
|
-
echo -n " Continuity state... "
|
|
57
|
-
if find "$TMPDIR/package/" \( -path "*/.delimit/*" -o -path "*/continuity/*" -o -name "*.jsonl" -o -name "session_*.json" -o -name "handoff_*.json" \) | grep -v "package-lock.json" 2>/dev/null; then
|
|
58
|
-
echo "❌ CONTINUITY ARTIFACTS IN PACKAGE"
|
|
59
|
-
FAIL=1
|
|
60
|
-
elif grep -rEi "/root/\.delimit|/home/[^/]+/\.delimit|/Users/[^/]+/\.delimit|C:\\Users\\[^\]+\\.delimit" "$TMPDIR/package/" --include="*.js" --include="*.json" 2>/dev/null | grep -v "security-scan-ignore"; then
|
|
61
|
-
echo "❌ CONTINUITY PATHS LEAKED INTO PACKAGE"
|
|
62
|
-
FAIL=1
|
|
63
|
-
else
|
|
64
|
-
echo "✅ clean"
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
55
|
# Cleanup
|
|
68
56
|
rm -rf "$TMPDIR"
|
|
69
57
|
|