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 +14 -15
- package/bin/delimit-setup.js +50 -35
- package/gateway/ai/governance.py +254 -0
- package/gateway/ai/ledger_manager.py +140 -37
- package/gateway/ai/server.py +71 -15
- package/package.json +1 -1
- package/tests/setup-onboarding.test.js +147 -0
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# delimit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Your AI Remembers. Verifies. Ships.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/delimit-cli)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
|
|
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
|
|
16
|
+
10 seconds. No API keys. No account. Installs into your existing AI coding assistant.
|
|
17
17
|
|
|
18
|
-
## What
|
|
18
|
+
## What it does
|
|
19
19
|
|
|
20
|
-
Your AI agent
|
|
20
|
+
Your AI agent gains the ability to verify its own work:
|
|
21
21
|
|
|
22
|
-
- **Test verification**
|
|
23
|
-
- **Security audit**
|
|
24
|
-
- **API governance**
|
|
25
|
-
- **Repo analysis**
|
|
26
|
-
- **Deploy tracking**
|
|
27
|
-
- **Multi-model consensus**
|
|
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
|
-
| "
|
|
36
|
-
| "
|
|
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
|
|
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
|
|
package/bin/delimit-setup.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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:
|
|
306
|
+
// Step 6: Try it now
|
|
326
307
|
step(6, 'Done!');
|
|
327
308
|
log('');
|
|
328
|
-
log(` ${green('Delimit is installed.')} Your AI
|
|
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('
|
|
331
|
-
log(` ${dim('
|
|
332
|
-
log(` ${dim('
|
|
333
|
-
log(` ${dim('
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
|
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,
|
|
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":
|
|
184
|
-
"done":
|
|
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
|
-
|
|
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}
|
package/gateway/ai/server.py
CHANGED
|
@@ -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
|
-
"""
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
1845
|
+
"""Add a new item to a project's ledger.
|
|
1812
1846
|
|
|
1813
|
-
The ledger tracks what needs to be done across sessions.
|
|
1814
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
@@ -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
|
+
});
|