delimit-cli 3.5.0 → 3.6.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.
@@ -183,6 +183,29 @@ async function main() {
183
183
  }
184
184
  }
185
185
 
186
+ // Step 3d: Configure Gemini CLI (if installed)
187
+ const GEMINI_CONFIG = path.join(os.homedir(), '.gemini', 'settings.json');
188
+ if (fs.existsSync(GEMINI_CONFIG)) {
189
+ try {
190
+ let geminiConfig = JSON.parse(fs.readFileSync(GEMINI_CONFIG, 'utf-8'));
191
+ if (!geminiConfig.mcpServers) geminiConfig.mcpServers = {};
192
+ if (geminiConfig.mcpServers.delimit) {
193
+ log(` ${green('✓')} Delimit already in Gemini CLI config`);
194
+ } else {
195
+ geminiConfig.mcpServers.delimit = {
196
+ command: python,
197
+ args: [actualServer],
198
+ cwd: path.join(DELIMIT_HOME, 'server'),
199
+ env: { PYTHONPATH: path.join(DELIMIT_HOME, 'server') }
200
+ };
201
+ fs.writeFileSync(GEMINI_CONFIG, JSON.stringify(geminiConfig, null, 2));
202
+ log(` ${green('✓')} Added delimit to Gemini CLI (${GEMINI_CONFIG})`);
203
+ }
204
+ } catch (e) {
205
+ log(` ${yellow('!')} Could not configure Gemini CLI: ${e.message}`);
206
+ }
207
+ }
208
+
186
209
  // Step 4: Install default agents
187
210
  step(4, 'Installing governance agents...');
188
211
 
@@ -0,0 +1,254 @@
1
+ """
2
+ Delimit Governance Layer — the loop that keeps AI agents on track.
3
+
4
+ Every tool flows through governance. Governance:
5
+ 1. Logs what happened (evidence)
6
+ 2. Checks result against rules (thresholds, policies)
7
+ 3. Auto-creates ledger items for failures/warnings
8
+ 4. Suggests next steps (loops back to keep building)
9
+
10
+ This replaces _with_next_steps — governance IS the next step system.
11
+ """
12
+
13
+ import json
14
+ import logging
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ logger = logging.getLogger("delimit.governance")
20
+
21
+
22
+ # Governance rules — what triggers auto-ledger-creation
23
+ RULES = {
24
+ "test_coverage": {
25
+ "threshold_key": "line_coverage",
26
+ "threshold": 80,
27
+ "comparison": "below",
28
+ "ledger_title": "Test coverage below {threshold}% — currently {value}%",
29
+ "ledger_type": "fix",
30
+ "ledger_priority": "P1",
31
+ },
32
+ "security_audit": {
33
+ "trigger_key": "vulnerabilities",
34
+ "trigger_if_nonempty": True,
35
+ "ledger_title": "Security: {count} vulnerabilities found",
36
+ "ledger_type": "fix",
37
+ "ledger_priority": "P0",
38
+ },
39
+ "security_scan": {
40
+ "trigger_key": "vulnerabilities",
41
+ "trigger_if_nonempty": True,
42
+ "ledger_title": "Security scan: {count} issues detected",
43
+ "ledger_type": "fix",
44
+ "ledger_priority": "P0",
45
+ },
46
+ "lint": {
47
+ "trigger_key": "violations",
48
+ "trigger_if_nonempty": True,
49
+ "ledger_title": "API lint: {count} violations found",
50
+ "ledger_type": "fix",
51
+ "ledger_priority": "P1",
52
+ },
53
+ "deliberate": {
54
+ "trigger_key": "unanimous",
55
+ "trigger_if_true": True,
56
+ "extract_actions": True,
57
+ "ledger_title": "Deliberation consensus reached — action items pending",
58
+ "ledger_type": "strategy",
59
+ "ledger_priority": "P1",
60
+ },
61
+ "gov_health": {
62
+ "trigger_key": "status",
63
+ "trigger_values": ["not_initialized", "degraded"],
64
+ "ledger_title": "Governance health: {value} — needs attention",
65
+ "ledger_type": "fix",
66
+ "ledger_priority": "P1",
67
+ },
68
+ "docs_validate": {
69
+ "threshold_key": "coverage_percent",
70
+ "threshold": 50,
71
+ "comparison": "below",
72
+ "ledger_title": "Documentation coverage below {threshold}% — currently {value}%",
73
+ "ledger_type": "task",
74
+ "ledger_priority": "P2",
75
+ },
76
+ }
77
+
78
+ # Next steps registry — what to do after each tool
79
+ NEXT_STEPS = {
80
+ "lint": [
81
+ {"tool": "delimit_explain", "reason": "Get migration guide for violations", "premium": False},
82
+ {"tool": "delimit_semver", "reason": "Classify the version bump", "premium": False},
83
+ ],
84
+ "diff": [
85
+ {"tool": "delimit_semver", "reason": "Classify changes as MAJOR/MINOR/PATCH", "premium": False},
86
+ {"tool": "delimit_policy", "reason": "Check against governance policies", "premium": False},
87
+ ],
88
+ "semver": [
89
+ {"tool": "delimit_explain", "reason": "Generate human-readable changelog", "premium": False},
90
+ ],
91
+ "init": [
92
+ {"tool": "delimit_gov_health", "reason": "Verify governance is set up correctly", "premium": True},
93
+ {"tool": "delimit_diagnose", "reason": "Check for any issues", "premium": False},
94
+ ],
95
+ "test_coverage": [
96
+ {"tool": "delimit_test_generate", "reason": "Generate tests for uncovered files", "premium": False},
97
+ ],
98
+ "security_audit": [
99
+ {"tool": "delimit_evidence_collect", "reason": "Collect evidence of findings", "premium": True},
100
+ ],
101
+ "gov_health": [
102
+ {"tool": "delimit_gov_status", "reason": "See detailed governance status", "premium": True},
103
+ {"tool": "delimit_repo_analyze", "reason": "Full repo health report", "premium": True},
104
+ ],
105
+ "deploy_plan": [
106
+ {"tool": "delimit_deploy_build", "reason": "Build the deployment", "premium": True},
107
+ ],
108
+ "deploy_build": [
109
+ {"tool": "delimit_deploy_publish", "reason": "Publish the build", "premium": True},
110
+ ],
111
+ "deploy_publish": [
112
+ {"tool": "delimit_deploy_verify", "reason": "Verify the deployment", "premium": True},
113
+ ],
114
+ "deploy_verify": [
115
+ {"tool": "delimit_deploy_rollback", "reason": "Rollback if unhealthy", "premium": True},
116
+ ],
117
+ "repo_analyze": [
118
+ {"tool": "delimit_security_audit", "reason": "Scan for security issues", "premium": False},
119
+ {"tool": "delimit_gov_health", "reason": "Check governance status", "premium": True},
120
+ ],
121
+ "deliberate": [
122
+ {"tool": "delimit_ledger_context", "reason": "Review what's on the ledger after consensus", "premium": False},
123
+ ],
124
+ "ledger_add": [
125
+ {"tool": "delimit_ledger_context", "reason": "See updated ledger state", "premium": False},
126
+ ],
127
+ "diagnose": [
128
+ {"tool": "delimit_init", "reason": "Initialize governance if not set up", "premium": False},
129
+ ],
130
+ }
131
+
132
+
133
+ def govern(tool_name: str, result: Dict[str, Any], project_path: str = ".") -> Dict[str, Any]:
134
+ """
135
+ Run governance on a tool's result. This is the central loop.
136
+
137
+ 1. Check result against rules
138
+ 2. Auto-create ledger items if thresholds breached
139
+ 3. Add next_steps for the AI to continue
140
+ 4. Return enriched result
141
+
142
+ Every tool should call this before returning.
143
+ """
144
+ # Strip "delimit_" prefix for rule matching
145
+ clean_name = tool_name.replace("delimit_", "")
146
+
147
+ governed_result = dict(result)
148
+
149
+ # 1. Check governance rules
150
+ rule = RULES.get(clean_name)
151
+ auto_items = []
152
+
153
+ if rule:
154
+ triggered = False
155
+ context = {}
156
+
157
+ # Threshold check (e.g., coverage < 80%)
158
+ if "threshold_key" in rule:
159
+ value = _deep_get(result, rule["threshold_key"])
160
+ if value is not None:
161
+ threshold = rule["threshold"]
162
+ if rule.get("comparison") == "below" and value < threshold:
163
+ triggered = True
164
+ context = {"value": f"{value:.1f}" if isinstance(value, float) else str(value), "threshold": str(threshold)}
165
+
166
+ # Non-empty list check (e.g., vulnerabilities found)
167
+ if "trigger_key" in rule and "trigger_if_nonempty" in rule:
168
+ items = _deep_get(result, rule["trigger_key"])
169
+ if items and isinstance(items, list) and len(items) > 0:
170
+ triggered = True
171
+ context = {"count": str(len(items))}
172
+
173
+ # Value match check (e.g., status == "degraded")
174
+ if "trigger_key" in rule and "trigger_values" in rule:
175
+ value = _deep_get(result, rule["trigger_key"])
176
+ if value in rule["trigger_values"]:
177
+ triggered = True
178
+ context = {"value": str(value)}
179
+
180
+ # Boolean check (e.g., unanimous == True)
181
+ if "trigger_key" in rule and "trigger_if_true" in rule:
182
+ value = _deep_get(result, rule["trigger_key"])
183
+ if value:
184
+ triggered = True
185
+
186
+ if triggered:
187
+ title = rule["ledger_title"].format(**context) if context else rule["ledger_title"]
188
+ auto_items.append({
189
+ "title": title,
190
+ "type": rule.get("ledger_type", "task"),
191
+ "priority": rule.get("ledger_priority", "P1"),
192
+ "source": f"governance:{clean_name}",
193
+ })
194
+
195
+ # 2. Auto-create ledger items
196
+ if auto_items:
197
+ try:
198
+ from ai.ledger_manager import add_item
199
+ created = []
200
+ for item in auto_items:
201
+ entry = add_item(
202
+ title=item["title"],
203
+ type=item["type"],
204
+ priority=item["priority"],
205
+ source=item["source"],
206
+ project_path=project_path,
207
+ )
208
+ created.append(entry.get("added", {}).get("id", ""))
209
+ governed_result["governance"] = {
210
+ "action": "ledger_items_created",
211
+ "items": created,
212
+ "reason": "Governance rule triggered by tool result",
213
+ }
214
+ except Exception as e:
215
+ logger.warning("Governance auto-ledger failed: %s", e)
216
+
217
+ # 3. Add next steps
218
+ steps = NEXT_STEPS.get(clean_name, [])
219
+ if steps:
220
+ governed_result["next_steps"] = steps
221
+
222
+ # 4. Always suggest checking the ledger
223
+ if clean_name not in ("ledger_add", "ledger_done", "ledger_list", "ledger_context", "ventures", "version", "help", "diagnose", "activate", "license_status", "models"):
224
+ if "next_steps" not in governed_result:
225
+ governed_result["next_steps"] = []
226
+ # Don't duplicate
227
+ existing = {s.get("tool") for s in governed_result.get("next_steps", [])}
228
+ if "delimit_ledger_context" not in existing:
229
+ governed_result["next_steps"].append({
230
+ "tool": "delimit_ledger_context",
231
+ "reason": "Check ledger for what's next",
232
+ "premium": False,
233
+ })
234
+
235
+ return governed_result
236
+
237
+
238
+ def _deep_get(d: Dict, key: str) -> Any:
239
+ """Get a value from a dict, supporting nested keys with dots."""
240
+ if "." in key:
241
+ parts = key.split(".", 1)
242
+ sub = d.get(parts[0])
243
+ if isinstance(sub, dict):
244
+ return _deep_get(sub, parts[1])
245
+ return None
246
+
247
+ # Check top-level and common nested locations
248
+ if key in d:
249
+ return d[key]
250
+ # Check inside 'data', 'result', 'overall_coverage'
251
+ for wrapper in ["data", "result", "overall_coverage", "summary"]:
252
+ if isinstance(d.get(wrapper), dict) and key in d[wrapper]:
253
+ return d[wrapper][key]
254
+ return None
@@ -1,35 +1,119 @@
1
1
  """
2
2
  Delimit Ledger Manager — Strategy + Operational ledger as first-class MCP tools.
3
3
 
4
- Two ledgers:
4
+ Two ledgers per project:
5
5
  - Strategy: consensus decisions, positioning, pricing, product direction
6
6
  - Operational: tasks, bugs, features — the "keep building" items
7
7
 
8
- Both are append-only JSONL at ~/.delimit/ledger/
8
+ Ledger lives at {project}/.delimit/ledger/ (project-local).
9
+ Ventures auto-registered at ~/.delimit/ventures.json on first use.
9
10
  """
10
11
 
11
12
  import json
12
13
  import hashlib
14
+ import os
15
+ import subprocess
13
16
  import time
14
- import uuid
15
17
  from pathlib import Path
16
18
  from typing import Any, Dict, List, Optional
17
19
 
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]:
20
+ GLOBAL_DIR = Path.home() / ".delimit"
21
+ VENTURES_FILE = GLOBAL_DIR / "ventures.json"
22
+
23
+
24
+ def _detect_venture(project_path: str = ".") -> Dict[str, str]:
25
+ """Auto-detect venture/project info from the directory."""
26
+ p = Path(project_path).resolve()
27
+ info = {"name": p.name, "path": str(p)}
28
+
29
+ # Try package.json
30
+ pkg = p / "package.json"
31
+ if pkg.exists():
32
+ try:
33
+ d = json.loads(pkg.read_text())
34
+ info["name"] = d.get("name", p.name)
35
+ info["type"] = "node"
36
+ except Exception:
37
+ pass
38
+
39
+ # Try pyproject.toml
40
+ pyproj = p / "pyproject.toml"
41
+ if pyproj.exists():
42
+ try:
43
+ text = pyproj.read_text()
44
+ for line in text.splitlines():
45
+ if line.strip().startswith("name"):
46
+ name = line.split("=", 1)[1].strip().strip('"').strip("'")
47
+ if name:
48
+ info["name"] = name
49
+ info["type"] = "python"
50
+ break
51
+ except Exception:
52
+ pass
53
+
54
+ # Try git remote
55
+ try:
56
+ remote = subprocess.run(
57
+ ["git", "remote", "get-url", "origin"],
58
+ capture_output=True, text=True, timeout=3, cwd=str(p)
59
+ )
60
+ if remote.returncode == 0:
61
+ url = remote.stdout.strip()
62
+ # Extract repo name from URL
63
+ repo = url.rstrip("/").split("/")[-1].replace(".git", "")
64
+ info["repo"] = url
65
+ if not info.get("type"):
66
+ info["name"] = repo
67
+ except Exception:
68
+ pass
69
+
70
+ return info
71
+
72
+
73
+ def _register_venture(info: Dict[str, str]):
74
+ """Silently register a venture in the global registry."""
75
+ GLOBAL_DIR.mkdir(parents=True, exist_ok=True)
76
+ ventures = {}
77
+ if VENTURES_FILE.exists():
78
+ try:
79
+ ventures = json.loads(VENTURES_FILE.read_text())
80
+ except Exception:
81
+ pass
82
+
83
+ name = info["name"]
84
+ if name not in ventures:
85
+ ventures[name] = {
86
+ "path": info.get("path", ""),
87
+ "repo": info.get("repo", ""),
88
+ "type": info.get("type", ""),
89
+ "registered_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
90
+ }
91
+ VENTURES_FILE.write_text(json.dumps(ventures, indent=2))
92
+
93
+
94
+ def _project_ledger_dir(project_path: str = ".") -> Path:
95
+ """Get the ledger directory for the current project."""
96
+ p = Path(project_path).resolve()
97
+ return p / ".delimit" / "ledger"
98
+
99
+
100
+ def _ensure(project_path: str = "."):
101
+ ledger_dir = _project_ledger_dir(project_path)
102
+ ledger_dir.mkdir(parents=True, exist_ok=True)
103
+ for name in ["strategy.jsonl", "operations.jsonl"]:
104
+ f = ledger_dir / name
26
105
  if not f.exists():
27
106
  f.write_text("")
28
107
 
108
+ # Auto-register venture on first use
109
+ info = _detect_venture(project_path)
110
+ _register_venture(info)
111
+
29
112
 
30
113
  def _read_ledger(path: Path) -> List[Dict]:
31
- _ensure()
32
114
  items = []
115
+ if not path.exists():
116
+ return items
33
117
  for line in path.read_text().splitlines():
34
118
  line = line.strip()
35
119
  if line:
@@ -41,8 +125,9 @@ def _read_ledger(path: Path) -> List[Dict]:
41
125
 
42
126
 
43
127
  def _append(path: Path, entry: Dict) -> Dict:
44
- _ensure()
45
- # Add hash chain
128
+ path.parent.mkdir(parents=True, exist_ok=True)
129
+ if not path.exists():
130
+ path.write_text("")
46
131
  items = _read_ledger(path)
47
132
  prev_hash = items[-1].get("hash", "genesis") if items else "genesis"
48
133
  entry["hash"] = hashlib.sha256(f"{prev_hash}{json.dumps(entry, sort_keys=True)}".encode()).hexdigest()[:16]
@@ -60,17 +145,19 @@ def add_item(
60
145
  priority: str = "P1",
61
146
  description: str = "",
62
147
  source: str = "session",
148
+ project_path: str = ".",
63
149
  tags: Optional[List[str]] = None,
64
150
  ) -> Dict[str, Any]:
65
- """Add a new item to the strategy or operational ledger."""
66
- path = STRATEGY_LEDGER if ledger == "strategy" else OPS_LEDGER
151
+ """Add a new item to the project's strategy or operational ledger."""
152
+ _ensure(project_path)
153
+ venture = _detect_venture(project_path)
154
+ ledger_dir = _project_ledger_dir(project_path)
155
+ path = ledger_dir / ("strategy.jsonl" if ledger == "strategy" else "operations.jsonl")
67
156
 
68
- # Auto-generate ID
69
157
  items = _read_ledger(path)
70
158
  prefix = "STR" if ledger == "strategy" else "LED"
71
- existing_ids = [i.get("id", "") for i in items]
159
+ existing_ids = [i.get("id", "") for i in items if i.get("type") != "update"]
72
160
  num = len(existing_ids) + 1
73
- # Find next available number
74
161
  while f"{prefix}-{num:03d}" in existing_ids:
75
162
  num += 1
76
163
  item_id = f"{prefix}-{num:03d}"
@@ -82,6 +169,7 @@ def add_item(
82
169
  "priority": priority,
83
170
  "description": description,
84
171
  "source": source,
172
+ "venture": venture["name"],
85
173
  "status": "open",
86
174
  "tags": tags or [],
87
175
  }
@@ -90,6 +178,7 @@ def add_item(
90
178
  return {
91
179
  "added": result,
92
180
  "ledger": ledger,
181
+ "venture": venture["name"],
93
182
  "total_items": len(_read_ledger(path)),
94
183
  }
95
184
 
@@ -99,14 +188,17 @@ def update_item(
99
188
  status: Optional[str] = None,
100
189
  note: Optional[str] = None,
101
190
  priority: Optional[str] = None,
191
+ project_path: str = ".",
102
192
  ) -> Dict[str, Any]:
103
193
  """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)]:
194
+ _ensure(project_path)
195
+ ledger_dir = _project_ledger_dir(project_path)
196
+
197
+ for ledger_name, filename in [("ops", "operations.jsonl"), ("strategy", "strategy.jsonl")]:
198
+ path = ledger_dir / filename
106
199
  items = _read_ledger(path)
107
200
  for item in items:
108
- if item.get("id") == item_id:
109
- # Append an update event
201
+ if item.get("id") == item_id and item.get("type") != "update":
110
202
  update = {
111
203
  "id": item_id,
112
204
  "type": "update",
@@ -121,7 +213,7 @@ def update_item(
121
213
  _append(path, update)
122
214
  return {"updated": item_id, "changes": update, "ledger": ledger_name}
123
215
 
124
- return {"error": f"Item {item_id} not found in either ledger"}
216
+ return {"error": f"Item {item_id} not found in project ledger"}
125
217
 
126
218
 
127
219
  def list_items(
@@ -129,14 +221,19 @@ def list_items(
129
221
  status: Optional[str] = None,
130
222
  priority: Optional[str] = None,
131
223
  limit: int = 50,
224
+ project_path: str = ".",
132
225
  ) -> Dict[str, Any]:
133
226
  """List ledger items with optional filters."""
227
+ _ensure(project_path)
228
+ ledger_dir = _project_ledger_dir(project_path)
229
+ venture = _detect_venture(project_path)
134
230
  results = {}
135
231
 
136
- for ledger_name, path in [("ops", OPS_LEDGER), ("strategy", STRATEGY_LEDGER)]:
232
+ for ledger_name, filename in [("ops", "operations.jsonl"), ("strategy", "strategy.jsonl")]:
137
233
  if ledger not in ("both", ledger_name):
138
234
  continue
139
235
 
236
+ path = ledger_dir / filename
140
237
  items = _read_ledger(path)
141
238
 
142
239
  # Build current state by replaying events
@@ -155,53 +252,59 @@ def list_items(
155
252
  else:
156
253
  state[item_id] = {**item}
157
254
 
158
- # Filter
159
255
  filtered = list(state.values())
160
256
  if status:
161
257
  filtered = [i for i in filtered if i.get("status") == status]
162
258
  if priority:
163
259
  filtered = [i for i in filtered if i.get("priority") == priority]
164
260
 
165
- # Sort by priority then created_at
166
261
  priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
167
262
  filtered.sort(key=lambda x: (priority_order.get(x.get("priority", "P2"), 9), x.get("created_at", "")))
168
263
 
169
264
  results[ledger_name] = filtered[:limit]
170
265
 
171
- # Summary
172
266
  all_items = []
173
267
  for v in results.values():
174
268
  all_items.extend(v)
175
269
 
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
270
  return {
271
+ "venture": venture["name"],
180
272
  "items": results,
181
273
  "summary": {
182
274
  "total": len(all_items),
183
- "open": open_count,
184
- "done": done_count,
275
+ "open": sum(1 for i in all_items if i.get("status") == "open"),
276
+ "done": sum(1 for i in all_items if i.get("status") == "done"),
185
277
  "in_progress": sum(1 for i in all_items if i.get("status") == "in_progress"),
186
278
  },
187
279
  }
188
280
 
189
281
 
190
- def get_context() -> Dict[str, Any]:
282
+ def get_context(project_path: str = ".") -> Dict[str, Any]:
191
283
  """Get a concise ledger summary for AI context — what's open, what's next."""
192
- result = list_items(status="open")
284
+ venture = _detect_venture(project_path)
285
+ result = list_items(status="open", project_path=project_path)
193
286
  open_items = []
194
287
  for ledger_items in result["items"].values():
195
288
  open_items.extend(ledger_items)
196
289
 
197
- # Sort by priority
198
290
  priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
199
291
  open_items.sort(key=lambda x: priority_order.get(x.get("priority", "P2"), 9))
200
292
 
201
293
  return {
294
+ "venture": venture["name"],
202
295
  "open_items": len(open_items),
203
296
  "next_up": [{"id": i["id"], "title": i["title"], "priority": i["priority"]}
204
297
  for i in open_items[:5]],
205
298
  "summary": result["summary"],
206
- "tip": "Use delimit_ledger_add to add new items, delimit_ledger_done to mark complete.",
207
299
  }
300
+
301
+
302
+ def list_ventures() -> Dict[str, Any]:
303
+ """List all registered ventures/projects."""
304
+ if not VENTURES_FILE.exists():
305
+ return {"ventures": {}, "count": 0}
306
+ try:
307
+ ventures = json.loads(VENTURES_FILE.read_text())
308
+ return {"ventures": ventures, "count": len(ventures)}
309
+ except Exception:
310
+ return {"ventures": {}, "count": 0}
@@ -246,10 +246,22 @@ NEXT_STEPS_REGISTRY: Dict[str, List[Dict[str, Any]]] = {
246
246
 
247
247
 
248
248
  def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
249
- """Attach next_steps metadata to a tool response (Consensus 096)."""
250
- steps = NEXT_STEPS_REGISTRY.get(tool_name, [])
251
- result["next_steps"] = steps
252
- return result
249
+ """Route every tool result through governance (replaces simple next_steps).
250
+
251
+ Governance:
252
+ 1. Checks result against rules (thresholds, policies)
253
+ 2. Auto-creates ledger items for failures/warnings
254
+ 3. Adds next_steps to keep the AI building
255
+ 4. Loops back to governance via ledger_context suggestion
256
+ """
257
+ try:
258
+ from ai.governance import govern
259
+ return govern(tool_name, result)
260
+ except Exception:
261
+ # Fallback: just add next_steps from registry
262
+ steps = NEXT_STEPS_REGISTRY.get(tool_name, [])
263
+ result["next_steps"] = steps
264
+ return result
253
265
 
254
266
 
255
267
  # ═══════════════════════════════════════════════════════════════════════
@@ -1799,22 +1811,45 @@ def delimit_license_status() -> Dict[str, Any]:
1799
1811
  # ═══════════════════════════════════════════════════════════════════════
1800
1812
 
1801
1813
 
1814
+ def _resolve_venture(venture: str) -> str:
1815
+ """Resolve a venture name or path to an actual directory path."""
1816
+ if not venture:
1817
+ return "."
1818
+ # If it's already a path
1819
+ if venture.startswith("/") or venture.startswith("~"):
1820
+ return str(Path(venture).expanduser())
1821
+ # Check registered ventures
1822
+ from ai.ledger_manager import list_ventures
1823
+ v = list_ventures()
1824
+ for name, info in v.get("ventures", {}).items():
1825
+ if name == venture or venture in name:
1826
+ return info.get("path", ".")
1827
+ # Fallback: assume it's a directory name under common roots
1828
+ for root in ["/home/delimit", "/home/jamsons/ventures", "/home"]:
1829
+ candidate = Path(root) / venture
1830
+ if candidate.exists():
1831
+ return str(candidate)
1832
+ return "."
1833
+
1834
+
1802
1835
  @mcp.tool()
1803
1836
  def delimit_ledger_add(
1804
1837
  title: str,
1838
+ venture: str = "",
1805
1839
  ledger: str = "ops",
1806
1840
  type: str = "task",
1807
1841
  priority: str = "P1",
1808
1842
  description: str = "",
1809
1843
  source: str = "session",
1810
1844
  ) -> Dict[str, Any]:
1811
- """Add a new item to the strategy or operational ledger.
1845
+ """Add a new item to a project's ledger.
1812
1846
 
1813
- The ledger tracks what needs to be done across sessions. Use "ops" for
1814
- tasks/bugs/features, "strategy" for consensus decisions and direction.
1847
+ The ledger tracks what needs to be done across sessions. Specify the venture/project
1848
+ name or path. If empty, auto-detects from current directory.
1815
1849
 
1816
1850
  Args:
1817
1851
  title: What needs to be done.
1852
+ venture: Project name or path (e.g. "delimit-gateway", "/home/delimit/delimit-gateway"). Auto-detects if empty.
1818
1853
  ledger: "ops" (tasks, bugs, features) or "strategy" (decisions, direction).
1819
1854
  type: task, fix, feat, strategy, consensus.
1820
1855
  priority: P0 (urgent), P1 (important), P2 (nice to have).
@@ -1822,49 +1857,70 @@ def delimit_ledger_add(
1822
1857
  source: Where this came from (session, consensus, focus-group, etc).
1823
1858
  """
1824
1859
  from ai.ledger_manager import add_item
1860
+ project = _resolve_venture(venture)
1825
1861
  return add_item(title=title, ledger=ledger, type=type, priority=priority,
1826
- description=description, source=source)
1862
+ description=description, source=source, project_path=project)
1827
1863
 
1828
1864
 
1829
1865
  @mcp.tool()
1830
- def delimit_ledger_done(item_id: str, note: str = "") -> Dict[str, Any]:
1866
+ def delimit_ledger_done(item_id: str, note: str = "", venture: str = "") -> Dict[str, Any]:
1831
1867
  """Mark a ledger item as done.
1832
1868
 
1833
1869
  Args:
1834
1870
  item_id: The item ID (e.g. LED-001 or STR-001).
1835
1871
  note: Optional completion note.
1872
+ venture: Project name or path. Auto-detects if empty.
1836
1873
  """
1837
1874
  from ai.ledger_manager import update_item
1838
- return update_item(item_id=item_id, status="done", note=note)
1875
+ project = _resolve_venture(venture)
1876
+ return update_item(item_id=item_id, status="done", note=note, project_path=project)
1839
1877
 
1840
1878
 
1841
1879
  @mcp.tool()
1842
1880
  def delimit_ledger_list(
1881
+ venture: str = "",
1843
1882
  ledger: str = "both",
1844
1883
  status: str = "",
1845
1884
  priority: str = "",
1846
1885
  limit: int = 20,
1847
1886
  ) -> Dict[str, Any]:
1848
- """List ledger items see what's open, done, or in progress.
1887
+ """List ledger items for a venture/project.
1849
1888
 
1850
1889
  Args:
1890
+ venture: Project name or path. Auto-detects if empty.
1851
1891
  ledger: "ops", "strategy", or "both".
1852
1892
  status: Filter by status — "open", "done", "in_progress", or empty for all.
1853
1893
  priority: Filter by priority — "P0", "P1", "P2", or empty for all.
1854
1894
  limit: Max items to return.
1855
1895
  """
1856
1896
  from ai.ledger_manager import list_items
1857
- return list_items(ledger=ledger, status=status or None, priority=priority or None, limit=limit)
1897
+ project = _resolve_venture(venture)
1898
+ return list_items(ledger=ledger, status=status or None, priority=priority or None, limit=limit, project_path=project)
1858
1899
 
1859
1900
 
1860
1901
  @mcp.tool()
1861
- def delimit_ledger_context() -> Dict[str, Any]:
1862
- """Get a quick summary of what's open in the ledger — use at session start.
1902
+ def delimit_ledger_context(venture: str = "") -> Dict[str, Any]:
1903
+ """Get a quick summary of what's open in the ledger.
1863
1904
 
1905
+ Auto-detects the venture from context. Pass a venture name to check a specific project.
1864
1906
  Returns the top 5 open items by priority so the AI knows what to work on.
1907
+
1908
+ Args:
1909
+ venture: Project name or path. Auto-detects if empty.
1865
1910
  """
1866
1911
  from ai.ledger_manager import get_context
1867
- return get_context()
1912
+ project = _resolve_venture(venture) if venture else "."
1913
+ return get_context(project_path=project)
1914
+
1915
+
1916
+ @mcp.tool()
1917
+ def delimit_ventures() -> Dict[str, Any]:
1918
+ """List all registered ventures/projects that Delimit has been used with.
1919
+
1920
+ Ventures are auto-registered when you use any Delimit tool in a project directory.
1921
+ """
1922
+ from ai.ledger_manager import list_ventures
1923
+ return list_ventures()
1868
1924
 
1869
1925
 
1870
1926
  # ═══════════════════════════════════════════════════════════════════════
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "Stop Describing. Start Building. 77 MCP governance tools for AI coding assistants.",
5
5
  "main": "index.js",
6
6
  "bin": {