delimit-cli 3.4.0 → 3.5.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/gateway/ai/backends/tools_data.py +830 -0
- package/gateway/ai/backends/tools_design.py +921 -0
- package/gateway/ai/backends/tools_infra.py +866 -0
- package/gateway/ai/backends/tools_real.py +766 -0
- package/gateway/ai/backends/ui_bridge.py +26 -49
- package/gateway/ai/deliberation.py +387 -0
- package/gateway/ai/ledger_manager.py +207 -0
- package/gateway/ai/server.py +630 -216
- package/package.json +1 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Delimit Ledger Manager — Strategy + Operational ledger as first-class MCP tools.
|
|
3
|
+
|
|
4
|
+
Two ledgers:
|
|
5
|
+
- Strategy: consensus decisions, positioning, pricing, product direction
|
|
6
|
+
- Operational: tasks, bugs, features — the "keep building" items
|
|
7
|
+
|
|
8
|
+
Both are append-only JSONL at ~/.delimit/ledger/
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import hashlib
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
LEDGER_DIR = Path.home() / ".delimit" / "ledger"
|
|
19
|
+
STRATEGY_LEDGER = LEDGER_DIR / "strategy.jsonl"
|
|
20
|
+
OPS_LEDGER = LEDGER_DIR / "operations.jsonl"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ensure():
|
|
24
|
+
LEDGER_DIR.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
for f in [STRATEGY_LEDGER, OPS_LEDGER]:
|
|
26
|
+
if not f.exists():
|
|
27
|
+
f.write_text("")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _read_ledger(path: Path) -> List[Dict]:
|
|
31
|
+
_ensure()
|
|
32
|
+
items = []
|
|
33
|
+
for line in path.read_text().splitlines():
|
|
34
|
+
line = line.strip()
|
|
35
|
+
if line:
|
|
36
|
+
try:
|
|
37
|
+
items.append(json.loads(line))
|
|
38
|
+
except json.JSONDecodeError:
|
|
39
|
+
continue
|
|
40
|
+
return items
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _append(path: Path, entry: Dict) -> Dict:
|
|
44
|
+
_ensure()
|
|
45
|
+
# Add hash chain
|
|
46
|
+
items = _read_ledger(path)
|
|
47
|
+
prev_hash = items[-1].get("hash", "genesis") if items else "genesis"
|
|
48
|
+
entry["hash"] = hashlib.sha256(f"{prev_hash}{json.dumps(entry, sort_keys=True)}".encode()).hexdigest()[:16]
|
|
49
|
+
entry["created_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
50
|
+
|
|
51
|
+
with open(path, "a") as f:
|
|
52
|
+
f.write(json.dumps(entry) + "\n")
|
|
53
|
+
return entry
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def add_item(
|
|
57
|
+
title: str,
|
|
58
|
+
ledger: str = "ops",
|
|
59
|
+
type: str = "task",
|
|
60
|
+
priority: str = "P1",
|
|
61
|
+
description: str = "",
|
|
62
|
+
source: str = "session",
|
|
63
|
+
tags: Optional[List[str]] = None,
|
|
64
|
+
) -> Dict[str, Any]:
|
|
65
|
+
"""Add a new item to the strategy or operational ledger."""
|
|
66
|
+
path = STRATEGY_LEDGER if ledger == "strategy" else OPS_LEDGER
|
|
67
|
+
|
|
68
|
+
# Auto-generate ID
|
|
69
|
+
items = _read_ledger(path)
|
|
70
|
+
prefix = "STR" if ledger == "strategy" else "LED"
|
|
71
|
+
existing_ids = [i.get("id", "") for i in items]
|
|
72
|
+
num = len(existing_ids) + 1
|
|
73
|
+
# Find next available number
|
|
74
|
+
while f"{prefix}-{num:03d}" in existing_ids:
|
|
75
|
+
num += 1
|
|
76
|
+
item_id = f"{prefix}-{num:03d}"
|
|
77
|
+
|
|
78
|
+
entry = {
|
|
79
|
+
"id": item_id,
|
|
80
|
+
"title": title,
|
|
81
|
+
"type": type,
|
|
82
|
+
"priority": priority,
|
|
83
|
+
"description": description,
|
|
84
|
+
"source": source,
|
|
85
|
+
"status": "open",
|
|
86
|
+
"tags": tags or [],
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
result = _append(path, entry)
|
|
90
|
+
return {
|
|
91
|
+
"added": result,
|
|
92
|
+
"ledger": ledger,
|
|
93
|
+
"total_items": len(_read_ledger(path)),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def update_item(
|
|
98
|
+
item_id: str,
|
|
99
|
+
status: Optional[str] = None,
|
|
100
|
+
note: Optional[str] = None,
|
|
101
|
+
priority: Optional[str] = None,
|
|
102
|
+
) -> Dict[str, Any]:
|
|
103
|
+
"""Update an existing ledger item's status, priority, or add a note."""
|
|
104
|
+
# Search both ledgers
|
|
105
|
+
for ledger_name, path in [("ops", OPS_LEDGER), ("strategy", STRATEGY_LEDGER)]:
|
|
106
|
+
items = _read_ledger(path)
|
|
107
|
+
for item in items:
|
|
108
|
+
if item.get("id") == item_id:
|
|
109
|
+
# Append an update event
|
|
110
|
+
update = {
|
|
111
|
+
"id": item_id,
|
|
112
|
+
"type": "update",
|
|
113
|
+
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
114
|
+
}
|
|
115
|
+
if status:
|
|
116
|
+
update["status"] = status
|
|
117
|
+
if note:
|
|
118
|
+
update["note"] = note
|
|
119
|
+
if priority:
|
|
120
|
+
update["priority"] = priority
|
|
121
|
+
_append(path, update)
|
|
122
|
+
return {"updated": item_id, "changes": update, "ledger": ledger_name}
|
|
123
|
+
|
|
124
|
+
return {"error": f"Item {item_id} not found in either ledger"}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def list_items(
|
|
128
|
+
ledger: str = "both",
|
|
129
|
+
status: Optional[str] = None,
|
|
130
|
+
priority: Optional[str] = None,
|
|
131
|
+
limit: int = 50,
|
|
132
|
+
) -> Dict[str, Any]:
|
|
133
|
+
"""List ledger items with optional filters."""
|
|
134
|
+
results = {}
|
|
135
|
+
|
|
136
|
+
for ledger_name, path in [("ops", OPS_LEDGER), ("strategy", STRATEGY_LEDGER)]:
|
|
137
|
+
if ledger not in ("both", ledger_name):
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
items = _read_ledger(path)
|
|
141
|
+
|
|
142
|
+
# Build current state by replaying events
|
|
143
|
+
state = {}
|
|
144
|
+
for item in items:
|
|
145
|
+
item_id = item.get("id", "")
|
|
146
|
+
if item.get("type") == "update":
|
|
147
|
+
if item_id in state:
|
|
148
|
+
if "status" in item:
|
|
149
|
+
state[item_id]["status"] = item["status"]
|
|
150
|
+
if "note" in item:
|
|
151
|
+
state[item_id]["last_note"] = item["note"]
|
|
152
|
+
if "priority" in item:
|
|
153
|
+
state[item_id]["priority"] = item["priority"]
|
|
154
|
+
state[item_id]["updated_at"] = item.get("updated_at")
|
|
155
|
+
else:
|
|
156
|
+
state[item_id] = {**item}
|
|
157
|
+
|
|
158
|
+
# Filter
|
|
159
|
+
filtered = list(state.values())
|
|
160
|
+
if status:
|
|
161
|
+
filtered = [i for i in filtered if i.get("status") == status]
|
|
162
|
+
if priority:
|
|
163
|
+
filtered = [i for i in filtered if i.get("priority") == priority]
|
|
164
|
+
|
|
165
|
+
# Sort by priority then created_at
|
|
166
|
+
priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
|
|
167
|
+
filtered.sort(key=lambda x: (priority_order.get(x.get("priority", "P2"), 9), x.get("created_at", "")))
|
|
168
|
+
|
|
169
|
+
results[ledger_name] = filtered[:limit]
|
|
170
|
+
|
|
171
|
+
# Summary
|
|
172
|
+
all_items = []
|
|
173
|
+
for v in results.values():
|
|
174
|
+
all_items.extend(v)
|
|
175
|
+
|
|
176
|
+
open_count = sum(1 for i in all_items if i.get("status") == "open")
|
|
177
|
+
done_count = sum(1 for i in all_items if i.get("status") == "done")
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
"items": results,
|
|
181
|
+
"summary": {
|
|
182
|
+
"total": len(all_items),
|
|
183
|
+
"open": open_count,
|
|
184
|
+
"done": done_count,
|
|
185
|
+
"in_progress": sum(1 for i in all_items if i.get("status") == "in_progress"),
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_context() -> Dict[str, Any]:
|
|
191
|
+
"""Get a concise ledger summary for AI context — what's open, what's next."""
|
|
192
|
+
result = list_items(status="open")
|
|
193
|
+
open_items = []
|
|
194
|
+
for ledger_items in result["items"].values():
|
|
195
|
+
open_items.extend(ledger_items)
|
|
196
|
+
|
|
197
|
+
# Sort by priority
|
|
198
|
+
priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
|
|
199
|
+
open_items.sort(key=lambda x: priority_order.get(x.get("priority", "P2"), 9))
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
"open_items": len(open_items),
|
|
203
|
+
"next_up": [{"id": i["id"], "title": i["title"], "priority": i["priority"]}
|
|
204
|
+
for i in open_items[:5]],
|
|
205
|
+
"summary": result["summary"],
|
|
206
|
+
"tip": "Use delimit_ledger_add to add new items, delimit_ledger_done to mark complete.",
|
|
207
|
+
}
|