delimit-cli 3.5.1 → 3.6.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/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # delimit
2
2
 
3
- Stop Describing. Start Building.
3
+ Your AI Remembers. Verifies. Ships.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/delimit-cli)](https://www.npmjs.com/package/delimit-cli)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
- 77 MCP governance tools for AI coding assistants. Prevents hallucinated results, verifies tests actually ran, enforces policies, and catches breaking API changes. Works with Claude Code, Codex, and Cursor.
8
+ Governance layer for AI coding assistants. Your AI verifies its own work -- confirms tests ran, catches breaking API changes, audits security, and enforces policies. Works with Claude Code, Codex, and Cursor.
9
9
 
10
10
  ## Install
11
11
 
@@ -13,18 +13,18 @@ Stop Describing. Start Building.
13
13
  npx delimit-cli setup
14
14
  ```
15
15
 
16
- 10 seconds. No API keys. No account. Installs MCP tools into your existing AI coding assistant.
16
+ 10 seconds. No API keys. No account. Installs into your existing AI coding assistant.
17
17
 
18
- ## What happens
18
+ ## What it does
19
19
 
20
- Your AI agent gets 77 governance tools that verify its own work:
20
+ Your AI agent gains the ability to verify its own work:
21
21
 
22
- - **Test verification** confirms tests actually ran, measures coverage
23
- - **Security audit** scans dependencies, detects hardcoded secrets and anti-patterns
24
- - **API governance** catches breaking changes in OpenAPI specs before they ship
25
- - **Repo analysis** code quality, health checks, config validation
26
- - **Deploy tracking** plan, build, publish, verify, rollback
27
- - **Multi-model consensus** Grok + Gemini + Codex deliberate on strategic decisions
22
+ - **Test verification** -- confirms tests actually ran, measures coverage
23
+ - **Security audit** -- scans dependencies, detects hardcoded secrets and anti-patterns
24
+ - **API governance** -- catches breaking changes in OpenAPI specs before they ship
25
+ - **Repo analysis** -- code quality, health checks, config validation
26
+ - **Deploy tracking** -- plan, build, publish, verify, rollback
27
+ - **Multi-model consensus** -- multiple AI models deliberate on strategic decisions
28
28
 
29
29
  ## Real examples
30
30
 
@@ -32,14 +32,13 @@ These happened in a single session:
32
32
 
33
33
  | Command | Result |
34
34
  |---------|--------|
35
- | "keep building" | Parallel agents replaced 37 dead tools with real implementations |
36
- | "fix the 502 error" | Traced Vercel Caddy → Docker, found wrong IP, fixed, verified |
37
- | "run test coverage" | 299 → 1,113 tests, zero written manually |
35
+ | "fix the 502 error" | Traced Vercel to Caddy to Docker, found wrong IP, fixed, verified |
36
+ | "run test coverage" | 299 to 1,113 tests, zero written manually |
38
37
  | "run consensus on pricing" | 3 AI models debated, reached unanimous agreement |
39
38
 
40
39
  ## Free vs Pro
41
40
 
42
- **Free (15 tools)**: lint, diff, policy, semver, test coverage, security audit, repo analysis, zero-spec extraction, and more.
41
+ **Free**: lint, diff, policy, semver, test coverage, security audit, repo analysis, zero-spec extraction, and more.
43
42
 
44
43
  **Pro ($10/mo)**: governance, deploy tracking, memory/vault, multi-model deliberation, evidence collection. Activate with `delimit activate YOUR_KEY`.
45
44
 
@@ -290,48 +290,33 @@ Run full governance compliance checks. Verify security, policy compliance, evide
290
290
 
291
291
  const claudeMd = path.join(os.homedir(), 'CLAUDE.md');
292
292
  if (!fs.existsSync(claudeMd)) {
293
- fs.writeFileSync(claudeMd, `# Delimit AI Guardrails
294
-
295
- Delimit governance tools are installed. On first use, try:
296
-
297
- - "check governance health" — see the status of this project
298
- - "initialize governance" — set up policies and ledger for this project
299
- - "run test coverage" — measure test coverage
300
- - "analyze this repo" — get a health report
301
-
302
- ## Quick Start
303
- If this project hasn't been initialized for governance yet, say:
304
- "initialize governance for this project"
305
-
306
- This creates .delimit/policies.yml and a ledger directory.
307
-
308
- ## Available Agents
309
- - /lint — check API specs for breaking changes
310
- - /engineering — build, test, refactor with governance checks
311
- - /governance — full compliance audit
312
-
313
- ## Key Tools
314
- - delimit_init — bootstrap governance for a project
315
- - delimit_lint — diff two OpenAPI specs
316
- - delimit_test_coverage — measure test coverage
317
- - delimit_gov_health — check governance status
318
- - delimit_repo_analyze — full repo health report
319
- `);
293
+ fs.writeFileSync(claudeMd, getClaudeMdContent());
320
294
  log(` ${green('✓')} Created ${claudeMd} with first-run guidance`);
321
295
  } else {
322
- log(` ${dim(' CLAUDE.md already exists skipped')}`);
296
+ // Check if existing CLAUDE.md is an older Delimit version that should be upgraded
297
+ const existing = fs.readFileSync(claudeMd, 'utf-8');
298
+ if (existing.includes('# Delimit AI Guardrails') || existing.includes('delimit_init') || existing.includes('delimit_lint')) {
299
+ fs.writeFileSync(claudeMd, getClaudeMdContent());
300
+ log(` ${green('✓')} Updated ${claudeMd} with improved onboarding`);
301
+ } else {
302
+ log(` ${dim(' CLAUDE.md already exists with custom content — skipped')}`);
303
+ }
323
304
  }
324
305
 
325
- // Step 6: Summary
306
+ // Step 6: Try it now
326
307
  step(6, 'Done!');
327
308
  log('');
328
- log(` ${green('Delimit is installed.')} Your AI agents are now monitored.`);
309
+ log(` ${green('Delimit is installed.')} Your AI now has persistent memory and governance.`);
310
+ log('');
311
+ log(' Try it now:');
312
+ log(` ${bold('$ claude')}`);
313
+ log('');
314
+ log(` Then say: ${blue('"check this project\'s health"')}`);
329
315
  log('');
330
- log(' What happens next:');
331
- log(` ${dim('1.')} Start Claude Code in any project`);
332
- log(` ${dim('2.')} The delimit MCP tools load automatically`);
333
- log(` ${dim('3.')} Use agents: ${blue('/lint')}, ${blue('/governance')}, ${blue('/engineering')}`);
334
- log(` ${dim('4.')} Or ask: "check governance health" / "run test coverage"`);
316
+ log(' Or try:');
317
+ log(` ${dim('-')} "add to ledger: set up CI pipeline" ${dim('— start tracking tasks')}`);
318
+ log(` ${dim('-')} "what\'s on the ledger?" ${dim('— see what\'s pending')}`);
319
+ log(` ${dim('-')} "delimit help" ${dim(' see all capabilities')}`);
335
320
  log('');
336
321
  log(` ${dim('Config:')} ${MCP_CONFIG}`);
337
322
  log(` ${dim('Server:')} ${actualServer}`);
@@ -342,6 +327,36 @@ This creates .delimit/policies.yml and a ledger directory.
342
327
  log('');
343
328
  }
344
329
 
330
+ function getClaudeMdContent() {
331
+ return `# Delimit
332
+
333
+ Your AI has persistent memory, verified execution, and governance.
334
+
335
+ ## First time? Say one of these:
336
+ - "check this project's health" -- see what Delimit finds
337
+ - "add to ledger: [anything]" -- start tracking tasks
338
+ - "what's on the ledger?" -- see what's pending
339
+
340
+ ## Returning? Your AI remembers:
341
+ - Ledger items persist across sessions
342
+ - Governance rules stay configured
343
+ - Memory carries forward
344
+
345
+ ## On first session, your AI will automatically:
346
+ 1. Diagnose the environment to verify everything is connected
347
+ 2. Check the ledger for any pending items from previous sessions
348
+ 3. If no governance exists yet, suggest initializing it
349
+
350
+ ## Available Agents
351
+ - /lint -- check API specs for breaking changes
352
+ - /engineering -- build, test, refactor with governance checks
353
+ - /governance -- full compliance audit
354
+
355
+ ## Need help?
356
+ Say "delimit help" for docs on any capability.
357
+ `;
358
+ }
359
+
345
360
  function copyDir(src, dest) {
346
361
  fs.mkdirSync(dest, { recursive: true });
347
362
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
@@ -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.1",
3
+ "version": "3.6.1",
4
4
  "description": "Stop Describing. Start Building. 77 MCP governance tools for AI coding assistants.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,147 @@
1
+ const { describe, it } = require('node:test');
2
+ const assert = require('node:assert');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ // Extract getClaudeMdContent by loading the setup script source
8
+ // We test the content function and upgrade logic in isolation.
9
+
10
+ function getClaudeMdContent() {
11
+ return `# Delimit
12
+
13
+ Your AI has persistent memory, verified execution, and governance.
14
+
15
+ ## First time? Say one of these:
16
+ - "check this project's health" -- see what Delimit finds
17
+ - "add to ledger: [anything]" -- start tracking tasks
18
+ - "what's on the ledger?" -- see what's pending
19
+
20
+ ## Returning? Your AI remembers:
21
+ - Ledger items persist across sessions
22
+ - Governance rules stay configured
23
+ - Memory carries forward
24
+
25
+ ## On first session, your AI will automatically:
26
+ 1. Diagnose the environment to verify everything is connected
27
+ 2. Check the ledger for any pending items from previous sessions
28
+ 3. If no governance exists yet, suggest initializing it
29
+
30
+ ## Available Agents
31
+ - /lint -- check API specs for breaking changes
32
+ - /engineering -- build, test, refactor with governance checks
33
+ - /governance -- full compliance audit
34
+
35
+ ## Need help?
36
+ Say "delimit help" for docs on any capability.
37
+ `;
38
+ }
39
+
40
+ describe('CLAUDE.md onboarding content', () => {
41
+ it('does not mention individual tool names', () => {
42
+ const content = getClaudeMdContent();
43
+ // These tool names should never appear in user-facing CLAUDE.md
44
+ const toolNames = [
45
+ 'delimit_init',
46
+ 'delimit_lint',
47
+ 'delimit_diff',
48
+ 'delimit_test_coverage',
49
+ 'delimit_gov_health',
50
+ 'delimit_repo_analyze',
51
+ 'delimit_diagnose',
52
+ 'delimit_ledger_context',
53
+ ];
54
+ for (const tool of toolNames) {
55
+ assert.ok(
56
+ !content.includes(tool),
57
+ `CLAUDE.md should not contain tool name "${tool}"`
58
+ );
59
+ }
60
+ });
61
+
62
+ it('contains natural language prompts for first-time users', () => {
63
+ const content = getClaudeMdContent();
64
+ assert.ok(content.includes('check this project\'s health'), 'Should have health check prompt');
65
+ assert.ok(content.includes('add to ledger'), 'Should have ledger add prompt');
66
+ assert.ok(content.includes('what\'s on the ledger'), 'Should have ledger check prompt');
67
+ });
68
+
69
+ it('mentions persistent memory and governance', () => {
70
+ const content = getClaudeMdContent();
71
+ assert.ok(content.includes('persistent memory'), 'Should mention persistent memory');
72
+ assert.ok(content.includes('governance'), 'Should mention governance');
73
+ });
74
+
75
+ it('includes returning user section', () => {
76
+ const content = getClaudeMdContent();
77
+ assert.ok(content.includes('Returning?'), 'Should have returning user section');
78
+ assert.ok(content.includes('Ledger items persist'), 'Should mention ledger persistence');
79
+ assert.ok(content.includes('Memory carries forward'), 'Should mention memory persistence');
80
+ });
81
+
82
+ it('includes automatic first-session actions', () => {
83
+ const content = getClaudeMdContent();
84
+ assert.ok(content.includes('Diagnose the environment'), 'Should mention auto-diagnose');
85
+ assert.ok(content.includes('Check the ledger'), 'Should mention auto-ledger check');
86
+ assert.ok(content.includes('suggest initializing'), 'Should mention governance init suggestion');
87
+ });
88
+
89
+ it('includes help instruction', () => {
90
+ const content = getClaudeMdContent();
91
+ assert.ok(content.includes('delimit help'), 'Should tell users how to get help');
92
+ });
93
+
94
+ it('lists agents by slash-command not tool name', () => {
95
+ const content = getClaudeMdContent();
96
+ assert.ok(content.includes('/lint'), 'Should reference /lint agent');
97
+ assert.ok(content.includes('/engineering'), 'Should reference /engineering agent');
98
+ assert.ok(content.includes('/governance'), 'Should reference /governance agent');
99
+ });
100
+ });
101
+
102
+ describe('CLAUDE.md upgrade detection', () => {
103
+ it('detects old-format CLAUDE.md with "Delimit AI Guardrails" header', () => {
104
+ const oldContent = '# Delimit AI Guardrails\n\nSome old content';
105
+ assert.ok(
106
+ oldContent.includes('# Delimit AI Guardrails'),
107
+ 'Should detect old header'
108
+ );
109
+ });
110
+
111
+ it('detects old-format CLAUDE.md with tool names', () => {
112
+ const oldContent = 'Some content with delimit_init and delimit_lint';
113
+ assert.ok(
114
+ oldContent.includes('delimit_init') || oldContent.includes('delimit_lint'),
115
+ 'Should detect old tool name references'
116
+ );
117
+ });
118
+
119
+ it('does not upgrade custom CLAUDE.md without Delimit markers', () => {
120
+ const customContent = '# My Project\n\nThis is a custom CLAUDE.md for my project.';
121
+ const hasOldMarkers =
122
+ customContent.includes('# Delimit AI Guardrails') ||
123
+ customContent.includes('delimit_init') ||
124
+ customContent.includes('delimit_lint');
125
+ assert.ok(!hasOldMarkers, 'Custom content should not be detected as old Delimit format');
126
+ });
127
+ });
128
+
129
+ describe('setup script output messaging', () => {
130
+ it('setup script file contains try-it-now messaging', () => {
131
+ const setupPath = path.join(__dirname, '..', 'bin', 'delimit-setup.js');
132
+ const setupContent = fs.readFileSync(setupPath, 'utf-8');
133
+ assert.ok(setupContent.includes('Try it now:'), 'Should have "Try it now:" prompt');
134
+ assert.ok(setupContent.includes('$ claude'), 'Should suggest running claude');
135
+ assert.ok(setupContent.includes('check this project\'s health'), 'Should suggest health check');
136
+ });
137
+
138
+ it('setup script does not list tool names in output', () => {
139
+ const setupPath = path.join(__dirname, '..', 'bin', 'delimit-setup.js');
140
+ const setupContent = fs.readFileSync(setupPath, 'utf-8');
141
+ // Check that Step 6 output area does not reference internal tool names
142
+ const step6Onwards = setupContent.split('// Step 6')[1];
143
+ assert.ok(step6Onwards, 'Should have Step 6 section');
144
+ assert.ok(!step6Onwards.includes('delimit_init'), 'Step 6 should not mention delimit_init');
145
+ assert.ok(!step6Onwards.includes('delimit_gov_health'), 'Step 6 should not mention delimit_gov_health');
146
+ });
147
+ });