delimit-cli 3.15.1 → 3.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gateway/ai/playbook.py +187 -0
- package/gateway/ai/server.py +603 -29
- package/package.json +1 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Prompt Playbook — versioned, reusable prompt templates (STR-048).
|
|
2
|
+
|
|
3
|
+
Save prompts as named commands that work across any AI assistant.
|
|
4
|
+
Share them with your team. Version them per model.
|
|
5
|
+
|
|
6
|
+
Storage: ~/.delimit/playbooks/
|
|
7
|
+
Format: YAML files with name, prompt, variables, model hints.
|
|
8
|
+
|
|
9
|
+
Focus group origin: "Prompt management is a total disaster.
|
|
10
|
+
Slack channels, Notion docs, personal text files."
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import time
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
PLAYBOOKS_DIR = Path.home() / ".delimit" / "playbooks"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _ensure_dir():
|
|
23
|
+
PLAYBOOKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _playbook_path(name: str) -> Path:
|
|
27
|
+
safe = re.sub(r'[^a-zA-Z0-9_-]', '_', name.lower().strip())
|
|
28
|
+
return PLAYBOOKS_DIR / f"{safe}.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def save_playbook(
|
|
32
|
+
name: str,
|
|
33
|
+
prompt: str,
|
|
34
|
+
description: str = "",
|
|
35
|
+
variables: Optional[List[str]] = None,
|
|
36
|
+
model_hint: str = "",
|
|
37
|
+
tags: Optional[List[str]] = None,
|
|
38
|
+
) -> Dict[str, Any]:
|
|
39
|
+
"""Save a named prompt template.
|
|
40
|
+
|
|
41
|
+
Variables use {{var_name}} syntax in the prompt text.
|
|
42
|
+
Example: "Generate tests for {{file_path}} using {{framework}}"
|
|
43
|
+
"""
|
|
44
|
+
if not name or not name.strip():
|
|
45
|
+
return {"error": "name is required"}
|
|
46
|
+
if not prompt or not prompt.strip():
|
|
47
|
+
return {"error": "prompt is required"}
|
|
48
|
+
|
|
49
|
+
name = name.strip()
|
|
50
|
+
_ensure_dir()
|
|
51
|
+
|
|
52
|
+
# Auto-detect variables from {{var}} patterns
|
|
53
|
+
detected_vars = re.findall(r'\{\{(\w+)\}\}', prompt)
|
|
54
|
+
all_vars = list(set((variables or []) + detected_vars))
|
|
55
|
+
|
|
56
|
+
playbook = {
|
|
57
|
+
"name": name,
|
|
58
|
+
"prompt": prompt,
|
|
59
|
+
"description": description or f"Playbook: {name}",
|
|
60
|
+
"variables": all_vars,
|
|
61
|
+
"model_hint": model_hint,
|
|
62
|
+
"tags": tags or [],
|
|
63
|
+
"version": 1,
|
|
64
|
+
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
65
|
+
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pb_path = _playbook_path(name)
|
|
69
|
+
|
|
70
|
+
# If exists, increment version
|
|
71
|
+
if pb_path.exists():
|
|
72
|
+
try:
|
|
73
|
+
existing = json.loads(pb_path.read_text())
|
|
74
|
+
playbook["version"] = existing.get("version", 0) + 1
|
|
75
|
+
playbook["created_at"] = existing.get("created_at", playbook["created_at"])
|
|
76
|
+
except (json.JSONDecodeError, OSError):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
pb_path.write_text(json.dumps(playbook, indent=2))
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"status": "saved",
|
|
83
|
+
"name": name,
|
|
84
|
+
"version": playbook["version"],
|
|
85
|
+
"variables": all_vars,
|
|
86
|
+
"path": str(pb_path),
|
|
87
|
+
"message": f"Playbook '{name}' saved (v{playbook['version']})",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def run_playbook(
|
|
92
|
+
name: str,
|
|
93
|
+
variables: Optional[Dict[str, str]] = None,
|
|
94
|
+
) -> Dict[str, Any]:
|
|
95
|
+
"""Load and render a named playbook with variables filled in.
|
|
96
|
+
|
|
97
|
+
Returns the rendered prompt ready to send to an AI model.
|
|
98
|
+
"""
|
|
99
|
+
if not name or not name.strip():
|
|
100
|
+
return {"error": "name is required"}
|
|
101
|
+
|
|
102
|
+
pb_path = _playbook_path(name.strip())
|
|
103
|
+
if not pb_path.exists():
|
|
104
|
+
# Try fuzzy match
|
|
105
|
+
matches = list(PLAYBOOKS_DIR.glob("*.json"))
|
|
106
|
+
suggestions = []
|
|
107
|
+
for m in matches:
|
|
108
|
+
try:
|
|
109
|
+
pb = json.loads(m.read_text())
|
|
110
|
+
suggestions.append(pb["name"])
|
|
111
|
+
except:
|
|
112
|
+
pass
|
|
113
|
+
return {
|
|
114
|
+
"error": f"Playbook '{name}' not found",
|
|
115
|
+
"available": suggestions[:10],
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
playbook = json.loads(pb_path.read_text())
|
|
120
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
121
|
+
return {"error": f"Failed to read playbook: {e}"}
|
|
122
|
+
|
|
123
|
+
prompt = playbook["prompt"]
|
|
124
|
+
vars_used = variables or {}
|
|
125
|
+
|
|
126
|
+
# Fill in variables
|
|
127
|
+
missing = []
|
|
128
|
+
for var in playbook.get("variables", []):
|
|
129
|
+
if var in vars_used:
|
|
130
|
+
prompt = prompt.replace(f"{{{{{var}}}}}", vars_used[var])
|
|
131
|
+
else:
|
|
132
|
+
missing.append(var)
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"status": "ready",
|
|
136
|
+
"name": playbook["name"],
|
|
137
|
+
"version": playbook["version"],
|
|
138
|
+
"rendered_prompt": prompt,
|
|
139
|
+
"model_hint": playbook.get("model_hint", ""),
|
|
140
|
+
"missing_variables": missing,
|
|
141
|
+
"message": f"Playbook '{name}' ready" + (f" (missing: {', '.join(missing)})" if missing else ""),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def list_playbooks(tag: str = "") -> Dict[str, Any]:
|
|
146
|
+
"""List all saved playbooks, optionally filtered by tag."""
|
|
147
|
+
_ensure_dir()
|
|
148
|
+
playbooks = []
|
|
149
|
+
|
|
150
|
+
for pb_file in sorted(PLAYBOOKS_DIR.glob("*.json")):
|
|
151
|
+
try:
|
|
152
|
+
pb = json.loads(pb_file.read_text())
|
|
153
|
+
if tag and tag not in pb.get("tags", []):
|
|
154
|
+
continue
|
|
155
|
+
playbooks.append({
|
|
156
|
+
"name": pb["name"],
|
|
157
|
+
"description": pb.get("description", ""),
|
|
158
|
+
"version": pb.get("version", 1),
|
|
159
|
+
"variables": pb.get("variables", []),
|
|
160
|
+
"model_hint": pb.get("model_hint", ""),
|
|
161
|
+
"tags": pb.get("tags", []),
|
|
162
|
+
})
|
|
163
|
+
except (json.JSONDecodeError, OSError):
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
"status": "ok",
|
|
168
|
+
"playbooks": playbooks,
|
|
169
|
+
"total": len(playbooks),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def delete_playbook(name: str) -> Dict[str, Any]:
|
|
174
|
+
"""Delete a named playbook."""
|
|
175
|
+
if not name:
|
|
176
|
+
return {"error": "name is required"}
|
|
177
|
+
|
|
178
|
+
pb_path = _playbook_path(name.strip())
|
|
179
|
+
if not pb_path.exists():
|
|
180
|
+
return {"error": f"Playbook '{name}' not found"}
|
|
181
|
+
|
|
182
|
+
pb_path.unlink()
|
|
183
|
+
return {
|
|
184
|
+
"status": "deleted",
|
|
185
|
+
"name": name,
|
|
186
|
+
"message": f"Playbook '{name}' deleted",
|
|
187
|
+
}
|
package/gateway/ai/server.py
CHANGED
|
@@ -19,6 +19,30 @@ All tools follow the Adapter Boundary Contract v1.0:
|
|
|
19
19
|
- Stateless between calls
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
+
# ── Founder Voice Doctrine ──────────────────────────────────────────────
|
|
23
|
+
# Applies to ALL outward-facing text generated by any tool in this server.
|
|
24
|
+
# Full doctrine: /home/delimit/delimit-private/style/FOUNDER_VOICE_DOCTRINE.md
|
|
25
|
+
#
|
|
26
|
+
# Core: serious builder/operator, not a marketer. Credibility over persuasion.
|
|
27
|
+
# Truth over excitement. Concrete mechanisms, not vague benefits.
|
|
28
|
+
# No hype words (revolutionary, seamless, unlock, supercharge, game-changing).
|
|
29
|
+
# Sound earned. Respect the reader. Acknowledge tradeoffs.
|
|
30
|
+
# "Measured conviction" not "sales energy."
|
|
31
|
+
# "Serious founder/architect/controller" not "visionary hype founder."
|
|
32
|
+
#
|
|
33
|
+
# Quality bar: Does this sound like a real operator with consequences
|
|
34
|
+
# attached to decisions? Would this still read well a year from now?
|
|
35
|
+
# ────────────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
FOUNDER_VOICE_HYPE_WORDS = {
|
|
38
|
+
"revolutionary", "game-changing", "world-class", "cutting-edge",
|
|
39
|
+
"best-in-class", "seamless", "unlock", "supercharge", "next-generation",
|
|
40
|
+
"magical", "delightful", "effortless", "frictionless", "transformative",
|
|
41
|
+
"paradigm shift", "visionary", "category-defining", "industry-leading",
|
|
42
|
+
"innovative", "reimagine", "future of", "changing the game",
|
|
43
|
+
"empowering teams", "built for everyone",
|
|
44
|
+
}
|
|
45
|
+
|
|
22
46
|
import json
|
|
23
47
|
import logging
|
|
24
48
|
import os
|
|
@@ -899,7 +923,10 @@ NEXT_STEPS_REGISTRY: Dict[str, List[Dict[str, Any]]] = {
|
|
|
899
923
|
"version": [],
|
|
900
924
|
"help": [],
|
|
901
925
|
"diagnose": [],
|
|
902
|
-
"activate": [
|
|
926
|
+
"activate": [
|
|
927
|
+
{"tool": "delimit_init", "reason": "Initialize governance if not set up", "suggested_args": {"preset": "default"}, "is_premium": False},
|
|
928
|
+
{"tool": "delimit_diagnose", "reason": "Deep-dive into any failing checks", "suggested_args": {}, "is_premium": False},
|
|
929
|
+
],
|
|
903
930
|
"license_status": [],
|
|
904
931
|
}
|
|
905
932
|
|
|
@@ -1131,6 +1158,7 @@ def _detect_environment() -> Dict[str, Any]:
|
|
|
1131
1158
|
|
|
1132
1159
|
|
|
1133
1160
|
_inbox_daemon_autostarted = False
|
|
1161
|
+
_toolcard_cache_autoregistered = False
|
|
1134
1162
|
|
|
1135
1163
|
|
|
1136
1164
|
def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -1138,6 +1166,7 @@ def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1138
1166
|
|
|
1139
1167
|
The governance loop:
|
|
1140
1168
|
1. Auto-start inbox daemon on first tool call (model-agnostic)
|
|
1169
|
+
1b. Auto-register tool schemas with toolcard cache (LED-219)
|
|
1141
1170
|
2. Emit event for dashboard tracking
|
|
1142
1171
|
3. STR-052: Policy kernel gate (blocks high-risk actions without approval)
|
|
1143
1172
|
4. Check Pro license gate (blocks if not authorized)
|
|
@@ -1156,6 +1185,44 @@ def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1156
1185
|
except Exception as e:
|
|
1157
1186
|
logger.warning("Inbox daemon auto-start failed: %s", e)
|
|
1158
1187
|
|
|
1188
|
+
# LED-219: Auto-register tool schemas with toolcard cache on first call
|
|
1189
|
+
global _toolcard_cache_autoregistered
|
|
1190
|
+
if not _toolcard_cache_autoregistered:
|
|
1191
|
+
_toolcard_cache_autoregistered = True
|
|
1192
|
+
try:
|
|
1193
|
+
from ai.toolcard_cache import get_cache
|
|
1194
|
+
_tc = get_cache()
|
|
1195
|
+
# Build schema list from mcp's registered tools
|
|
1196
|
+
_tool_schemas = []
|
|
1197
|
+
for _tname, _tfn in getattr(mcp, '_tool_manager', {}).items() if hasattr(mcp, '_tool_manager') else []:
|
|
1198
|
+
_tool_schemas.append({"name": _tname})
|
|
1199
|
+
if not _tool_schemas:
|
|
1200
|
+
# Fallback: just record the current tool call
|
|
1201
|
+
_tc.record_call(tool_name)
|
|
1202
|
+
logger.info("Toolcard cache auto-registered on first tool call")
|
|
1203
|
+
except Exception as e:
|
|
1204
|
+
logger.warning("Toolcard cache auto-register failed: %s", e)
|
|
1205
|
+
|
|
1206
|
+
# LED-219: Track every tool call for session analytics
|
|
1207
|
+
try:
|
|
1208
|
+
from ai.toolcard_cache import get_cache as _get_tc
|
|
1209
|
+
_get_tc().record_call(tool_name)
|
|
1210
|
+
except Exception:
|
|
1211
|
+
pass
|
|
1212
|
+
|
|
1213
|
+
# Voice doctrine check — flag hype words in outgoing text
|
|
1214
|
+
if isinstance(result, dict):
|
|
1215
|
+
_text_fields = [result.get("text", ""), result.get("message", ""),
|
|
1216
|
+
result.get("explanation", ""), result.get("changelog", ""),
|
|
1217
|
+
result.get("content", "")]
|
|
1218
|
+
_all_text = " ".join(str(f) for f in _text_fields if f).lower()
|
|
1219
|
+
_found_hype = [w for w in FOUNDER_VOICE_HYPE_WORDS if w in _all_text]
|
|
1220
|
+
if _found_hype:
|
|
1221
|
+
result.setdefault("voice_warnings", []).append(
|
|
1222
|
+
f"VOICE DOCTRINE: Hype words detected: {', '.join(_found_hype)}. "
|
|
1223
|
+
f"Rewrite with concrete mechanisms, not vague benefits."
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1159
1226
|
# Emit event for real-time dashboard
|
|
1160
1227
|
_emit_event(tool_name, result)
|
|
1161
1228
|
|
|
@@ -3532,33 +3599,92 @@ TOOL_HELP = {
|
|
|
3532
3599
|
|
|
3533
3600
|
STANDARD_WORKFLOWS = [
|
|
3534
3601
|
{
|
|
3535
|
-
"name": "
|
|
3536
|
-
"
|
|
3537
|
-
"
|
|
3602
|
+
"name": "Resume Work",
|
|
3603
|
+
"pain": "You switched models or sessions and lost all context",
|
|
3604
|
+
"fix": "Pick up exactly where you left off",
|
|
3605
|
+
"steps": ["delimit_session_history", "delimit_ledger_context", "delimit_memory_search"],
|
|
3538
3606
|
},
|
|
3539
3607
|
{
|
|
3540
|
-
"name": "
|
|
3541
|
-
"
|
|
3542
|
-
"
|
|
3608
|
+
"name": "Catch Breaking Changes",
|
|
3609
|
+
"pain": "Your AI agent deployed a breaking API change and nobody caught it",
|
|
3610
|
+
"fix": "Detect and block breaking changes before merge",
|
|
3611
|
+
"steps": ["delimit_lint", "delimit_diff", "delimit_semver"],
|
|
3543
3612
|
},
|
|
3544
3613
|
{
|
|
3545
|
-
"name": "
|
|
3546
|
-
"
|
|
3547
|
-
"
|
|
3614
|
+
"name": "Remember Across Models",
|
|
3615
|
+
"pain": "Every new session starts from zero — your agent forgot everything",
|
|
3616
|
+
"fix": "Store and recall context across any AI assistant",
|
|
3617
|
+
"steps": ["delimit_memory_store", "delimit_memory_search", "delimit_session_handoff"],
|
|
3548
3618
|
},
|
|
3549
3619
|
{
|
|
3550
|
-
"name": "
|
|
3551
|
-
"
|
|
3552
|
-
"
|
|
3620
|
+
"name": "Track What Needs Doing",
|
|
3621
|
+
"pain": "Tasks get lost when context windows fill up",
|
|
3622
|
+
"fix": "Persistent ledger that survives session resets",
|
|
3623
|
+
"steps": ["delimit_ledger_add", "delimit_ledger_context", "delimit_ledger_done"],
|
|
3553
3624
|
},
|
|
3554
3625
|
{
|
|
3555
|
-
"name": "
|
|
3556
|
-
"
|
|
3557
|
-
"
|
|
3626
|
+
"name": "Watch for Drift",
|
|
3627
|
+
"pain": "Your API spec changed without governance review",
|
|
3628
|
+
"fix": "Continuous monitoring with alerts on drift",
|
|
3629
|
+
"steps": ["delimit_drift_check", "delimit_scan", "delimit_gov_health"],
|
|
3558
3630
|
},
|
|
3559
3631
|
]
|
|
3560
3632
|
|
|
3561
3633
|
|
|
3634
|
+
@mcp.tool()
|
|
3635
|
+
def delimit_playbook(action: str = "list", name: str = "", prompt: str = "",
|
|
3636
|
+
description: str = "", variables: str = "",
|
|
3637
|
+
model_hint: str = "", tags: str = "") -> Dict[str, Any]:
|
|
3638
|
+
"""Manage reusable prompt templates — save, run, list, delete.
|
|
3639
|
+
|
|
3640
|
+
Save your best prompts as named commands. Use {{variables}} for dynamic parts.
|
|
3641
|
+
Works across all AI assistants through the shared MCP workspace.
|
|
3642
|
+
|
|
3643
|
+
Examples:
|
|
3644
|
+
Save: delimit_playbook(action="save", name="test-gen", prompt="Generate Jest tests for {{file}}")
|
|
3645
|
+
Run: delimit_playbook(action="run", name="test-gen", variables="file=src/auth.ts")
|
|
3646
|
+
List: delimit_playbook(action="list")
|
|
3647
|
+
|
|
3648
|
+
Args:
|
|
3649
|
+
action: "save", "run", "list", or "delete".
|
|
3650
|
+
name: Playbook name (required for save/run/delete).
|
|
3651
|
+
prompt: Prompt template with {{variable}} placeholders (save only).
|
|
3652
|
+
description: Short description of what this playbook does.
|
|
3653
|
+
variables: For run: comma-separated key=value pairs. For save: comma-separated variable names.
|
|
3654
|
+
model_hint: Suggested model (e.g. "claude-opus" for complex tasks).
|
|
3655
|
+
tags: Comma-separated tags for organization.
|
|
3656
|
+
"""
|
|
3657
|
+
from ai.playbook import save_playbook, run_playbook, list_playbooks, delete_playbook
|
|
3658
|
+
|
|
3659
|
+
action = action.lower().strip()
|
|
3660
|
+
|
|
3661
|
+
if action == "save":
|
|
3662
|
+
tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else None
|
|
3663
|
+
var_list = [v.strip() for v in variables.split(",") if v.strip()] if variables else None
|
|
3664
|
+
return _with_next_steps("playbook", _safe_call(
|
|
3665
|
+
save_playbook, name=name, prompt=prompt, description=description,
|
|
3666
|
+
variables=var_list, model_hint=model_hint, tags=tag_list,
|
|
3667
|
+
))
|
|
3668
|
+
|
|
3669
|
+
if action == "run":
|
|
3670
|
+
var_dict = {}
|
|
3671
|
+
if variables:
|
|
3672
|
+
for pair in variables.split(","):
|
|
3673
|
+
if "=" in pair:
|
|
3674
|
+
k, v = pair.split("=", 1)
|
|
3675
|
+
var_dict[k.strip()] = v.strip()
|
|
3676
|
+
return _with_next_steps("playbook", _safe_call(
|
|
3677
|
+
run_playbook, name=name, variables=var_dict,
|
|
3678
|
+
))
|
|
3679
|
+
|
|
3680
|
+
if action == "delete":
|
|
3681
|
+
return _with_next_steps("playbook", _safe_call(delete_playbook, name=name))
|
|
3682
|
+
|
|
3683
|
+
# Default: list
|
|
3684
|
+
tag_filter = tags.strip() if tags else ""
|
|
3685
|
+
return _with_next_steps("playbook", _safe_call(list_playbooks, tag=tag_filter))
|
|
3686
|
+
|
|
3687
|
+
|
|
3562
3688
|
@mcp.tool()
|
|
3563
3689
|
def delimit_help(tool_name: str = "") -> Dict[str, Any]:
|
|
3564
3690
|
"""Get help for a Delimit tool — what it does, parameters, and examples.
|
|
@@ -3569,12 +3695,13 @@ def delimit_help(tool_name: str = "") -> Dict[str, Any]:
|
|
|
3569
3695
|
if not tool_name:
|
|
3570
3696
|
total = _count_registered_tools()
|
|
3571
3697
|
return _with_next_steps("help", {
|
|
3572
|
-
"message":
|
|
3698
|
+
"message": "What problem are you solving?",
|
|
3699
|
+
"workflows": [
|
|
3700
|
+
{"name": w["name"], "pain": w["pain"], "start_with": w["steps"][0]}
|
|
3701
|
+
for w in STANDARD_WORKFLOWS
|
|
3702
|
+
],
|
|
3703
|
+
"tip": "Tell me what you're trying to do — I'll suggest the right workflow.",
|
|
3573
3704
|
"total_tools": total,
|
|
3574
|
-
"essential_tools": {k: v["desc"] for k, v in TOOL_HELP.items()},
|
|
3575
|
-
"workflows": STANDARD_WORKFLOWS,
|
|
3576
|
-
"tip": "Run delimit_help(tool_name='lint') for detailed help on a specific tool.",
|
|
3577
|
-
"all_tools": "Run delimit_version() for the complete list.",
|
|
3578
3705
|
})
|
|
3579
3706
|
|
|
3580
3707
|
# Normalize name
|
|
@@ -3721,14 +3848,22 @@ def delimit_diagnose(project_path: str = ".") -> Dict[str, Any]:
|
|
|
3721
3848
|
|
|
3722
3849
|
|
|
3723
3850
|
@mcp.tool()
|
|
3724
|
-
def delimit_activate(license_key: str) -> Dict[str, Any]:
|
|
3725
|
-
"""Activate
|
|
3851
|
+
def delimit_activate(license_key: str = "", project_path: str = ".", auto_permissions: bool = True) -> Dict[str, Any]:
|
|
3852
|
+
"""Activate Delimit and run a readiness checklist.
|
|
3853
|
+
|
|
3854
|
+
Performs a comprehensive activation check: license validation, MCP server
|
|
3855
|
+
status, governance init, test smoke, permission auto-config, and premium
|
|
3856
|
+
feature availability. Skipped checks (premium on free tier, no test
|
|
3857
|
+
framework) do NOT count against the score.
|
|
3726
3858
|
|
|
3727
3859
|
Args:
|
|
3728
|
-
license_key:
|
|
3860
|
+
license_key: Optional license key to activate Pro (e.g. DELIMIT-XXXX-XXXX-XXXX). Leave empty to check free-tier readiness.
|
|
3861
|
+
project_path: Project directory to check.
|
|
3862
|
+
auto_permissions: Auto-configure AI assistant permissions for Delimit tools (default True).
|
|
3729
3863
|
"""
|
|
3730
|
-
from ai.
|
|
3731
|
-
|
|
3864
|
+
from ai.activate_helpers import build_checklist
|
|
3865
|
+
result = build_checklist(license_key=license_key, project_path=project_path, auto_permissions=auto_permissions)
|
|
3866
|
+
return _with_next_steps("activate", result)
|
|
3732
3867
|
|
|
3733
3868
|
|
|
3734
3869
|
@mcp.tool()
|
|
@@ -4089,6 +4224,85 @@ def delimit_ventures() -> Dict[str, Any]:
|
|
|
4089
4224
|
return list_ventures()
|
|
4090
4225
|
|
|
4091
4226
|
|
|
4227
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
4228
|
+
# SESSION PHOENIX — Cross-Model Resurrection (LED-218)
|
|
4229
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
4230
|
+
|
|
4231
|
+
|
|
4232
|
+
@mcp.tool()
|
|
4233
|
+
def delimit_soul_capture(
|
|
4234
|
+
active_task: str = "",
|
|
4235
|
+
decisions: str = "",
|
|
4236
|
+
key_context: str = "",
|
|
4237
|
+
blockers: str = "",
|
|
4238
|
+
next_steps: str = "",
|
|
4239
|
+
task_status: str = "in_progress",
|
|
4240
|
+
tokens_used: int = 0,
|
|
4241
|
+
context_fullness: float = 0.0,
|
|
4242
|
+
) -> Dict[str, Any]:
|
|
4243
|
+
"""Capture current session state as a 'soul' for cross-model resurrection (Pro).
|
|
4244
|
+
|
|
4245
|
+
Save what you're working on so the next session (in any model) picks up
|
|
4246
|
+
where you left off. Auto-detects git state and files changed.
|
|
4247
|
+
|
|
4248
|
+
Args:
|
|
4249
|
+
active_task: What you're currently working on (one line).
|
|
4250
|
+
decisions: Comma-separated key decisions made this session.
|
|
4251
|
+
key_context: Comma-separated important context for next session.
|
|
4252
|
+
blockers: Comma-separated blockers.
|
|
4253
|
+
next_steps: Comma-separated next steps.
|
|
4254
|
+
task_status: in_progress, blocked, or almost_done.
|
|
4255
|
+
tokens_used: Estimated tokens consumed this session.
|
|
4256
|
+
context_fullness: 0.0-1.0 how full the context window is.
|
|
4257
|
+
"""
|
|
4258
|
+
from ai.session_phoenix import capture_soul as _capture
|
|
4259
|
+
|
|
4260
|
+
def _split(val: str) -> List[str]:
|
|
4261
|
+
if not val or not val.strip():
|
|
4262
|
+
return []
|
|
4263
|
+
return [s.strip() for s in val.split(",") if s.strip()]
|
|
4264
|
+
|
|
4265
|
+
soul = _capture(
|
|
4266
|
+
active_task=active_task,
|
|
4267
|
+
decisions=_split(decisions),
|
|
4268
|
+
key_context=_split(key_context),
|
|
4269
|
+
blockers=_split(blockers),
|
|
4270
|
+
next_steps=_split(next_steps),
|
|
4271
|
+
source_model=_detect_model(),
|
|
4272
|
+
task_status=task_status,
|
|
4273
|
+
tokens_used=tokens_used,
|
|
4274
|
+
context_fullness=context_fullness,
|
|
4275
|
+
)
|
|
4276
|
+
|
|
4277
|
+
from dataclasses import asdict
|
|
4278
|
+
return _with_next_steps("soul_capture", {
|
|
4279
|
+
"status": "captured",
|
|
4280
|
+
"soul_id": soul.soul_id,
|
|
4281
|
+
"project": soul.project_path,
|
|
4282
|
+
"active_task": soul.active_task,
|
|
4283
|
+
"files_modified": len(soul.files_modified),
|
|
4284
|
+
"files_created": len(soul.files_created),
|
|
4285
|
+
"uncommitted_changes": soul.uncommitted_changes,
|
|
4286
|
+
"message": f"Soul {soul.soul_id} captured. Run delimit_revive in any model to restore.",
|
|
4287
|
+
})
|
|
4288
|
+
|
|
4289
|
+
|
|
4290
|
+
@mcp.tool()
|
|
4291
|
+
def delimit_revive(project_path: str = "", soul_id: str = "") -> Dict[str, Any]:
|
|
4292
|
+
"""Revive the last session's state in any model (Pro).
|
|
4293
|
+
|
|
4294
|
+
Run this at the start of a new session to pick up where you left off.
|
|
4295
|
+
Works across Claude Code, Codex, Gemini CLI, and Cursor.
|
|
4296
|
+
|
|
4297
|
+
Args:
|
|
4298
|
+
project_path: Project to revive. Auto-detects from cwd.
|
|
4299
|
+
soul_id: Specific soul to revive. Empty = latest.
|
|
4300
|
+
"""
|
|
4301
|
+
from ai.session_phoenix import revive as _revive
|
|
4302
|
+
result = _revive(project_path=project_path, soul_id=soul_id)
|
|
4303
|
+
return _with_next_steps("revive", result)
|
|
4304
|
+
|
|
4305
|
+
|
|
4092
4306
|
# ═══════════════════════════════════════════════════════════════════════
|
|
4093
4307
|
# DELIBERATION (Multi-Round Consensus)
|
|
4094
4308
|
# ═══════════════════════════════════════════════════════════════════════
|
|
@@ -4352,6 +4566,59 @@ def _extract_deliberation_actions(result: Dict, question: str) -> List[Dict[str,
|
|
|
4352
4566
|
return actions[:10]
|
|
4353
4567
|
|
|
4354
4568
|
|
|
4569
|
+
@mcp.tool()
|
|
4570
|
+
def delimit_audit(
|
|
4571
|
+
target: str = "",
|
|
4572
|
+
target_type: str = "file",
|
|
4573
|
+
lenses: str = "",
|
|
4574
|
+
) -> Dict[str, Any]:
|
|
4575
|
+
"""Cross-model code audit -- 3 models, 3 lenses, synthesized findings (Pro).
|
|
4576
|
+
|
|
4577
|
+
Run security, correctness, and governance reviews through different AI models
|
|
4578
|
+
simultaneously. Agreements are high-confidence. Disagreements surface tradeoffs.
|
|
4579
|
+
|
|
4580
|
+
"Trust through triangulation."
|
|
4581
|
+
|
|
4582
|
+
Args:
|
|
4583
|
+
target: File path, git diff output, or code snippet to audit.
|
|
4584
|
+
target_type: "file" (reads file), "diff" (git diff text), "snippet" (inline code).
|
|
4585
|
+
lenses: Comma-separated lenses to apply (security, correctness, governance). Default: all.
|
|
4586
|
+
"""
|
|
4587
|
+
from ai.license import require_premium
|
|
4588
|
+
gate = require_premium("audit")
|
|
4589
|
+
if gate:
|
|
4590
|
+
return gate
|
|
4591
|
+
|
|
4592
|
+
if not target.strip():
|
|
4593
|
+
return {"status": "error", "error": "No target provided. Pass a file path, diff, or code snippet."}
|
|
4594
|
+
|
|
4595
|
+
from ai.cross_model_audit import audit as run_audit
|
|
4596
|
+
|
|
4597
|
+
lens_list = [l.strip() for l in lenses.split(",") if l.strip()] if lenses else None
|
|
4598
|
+
|
|
4599
|
+
result = run_audit(
|
|
4600
|
+
target=target,
|
|
4601
|
+
target_type=target_type,
|
|
4602
|
+
lenses=lens_list,
|
|
4603
|
+
)
|
|
4604
|
+
|
|
4605
|
+
if result.get("status") == "error":
|
|
4606
|
+
return result
|
|
4607
|
+
|
|
4608
|
+
synthesis = result.get("synthesis", {})
|
|
4609
|
+
return _with_next_steps("audit", {
|
|
4610
|
+
"status": "ok",
|
|
4611
|
+
"formatted_output": result.get("formatted", ""),
|
|
4612
|
+
"agreements": len(synthesis.get("agreements", [])),
|
|
4613
|
+
"unique_findings": len(synthesis.get("unique_findings", [])),
|
|
4614
|
+
"disagreements": len(synthesis.get("disagreements", [])),
|
|
4615
|
+
"summary": synthesis.get("summary", ""),
|
|
4616
|
+
"saved_to": result.get("saved_to", ""),
|
|
4617
|
+
"elapsed_seconds": result.get("elapsed_seconds", 0),
|
|
4618
|
+
"note": "Review the formatted output above. Agreements are high-confidence. Unique findings may be blind-spot catches.",
|
|
4619
|
+
})
|
|
4620
|
+
|
|
4621
|
+
|
|
4355
4622
|
@mcp.tool()
|
|
4356
4623
|
def delimit_release_sync(action: str = "audit") -> Dict[str, Any]:
|
|
4357
4624
|
"""Audit or sync all public surfaces for consistency (Pro)."""
|
|
@@ -5477,6 +5744,66 @@ def delimit_social_target_config(
|
|
|
5477
5744
|
return {"error": f"Unknown action: {action}. Supported: status, detect, update, add_subreddits"}
|
|
5478
5745
|
|
|
5479
5746
|
|
|
5747
|
+
@mcp.tool()
|
|
5748
|
+
def delimit_reddit_scan(sort: str = "hot", limit: int = 10) -> Dict[str, Any]:
|
|
5749
|
+
"""Bulk scan 25+ subreddits for outreach targets (Pro).
|
|
5750
|
+
|
|
5751
|
+
Scans via residential proxy. Rate limited to 1 req/2sec.
|
|
5752
|
+
Returns ranked, categorized targets ready for comment drafting.
|
|
5753
|
+
|
|
5754
|
+
IMPORTANT -- TOOL CHAINING RULE:
|
|
5755
|
+
After scanning, the AI MUST process high_priority targets:
|
|
5756
|
+
1. Draft replies via delimit_social_post or delimit_notify
|
|
5757
|
+
2. Create ledger items for strategic opportunities via delimit_ledger_add
|
|
5758
|
+
Never just return targets and stop. Always chain to the next action.
|
|
5759
|
+
|
|
5760
|
+
Args:
|
|
5761
|
+
sort: Reddit sort order (hot, new, top).
|
|
5762
|
+
limit: Posts per subreddit (default 10, max 25).
|
|
5763
|
+
"""
|
|
5764
|
+
from ai.reddit_scanner import scan_all
|
|
5765
|
+
|
|
5766
|
+
capped_limit = min(max(1, limit), 25)
|
|
5767
|
+
if sort not in ("hot", "new", "top", "rising"):
|
|
5768
|
+
sort = "hot"
|
|
5769
|
+
|
|
5770
|
+
result = scan_all(limit_per_sub=capped_limit, sort=sort)
|
|
5771
|
+
return _with_next_steps("social_target", result)
|
|
5772
|
+
|
|
5773
|
+
|
|
5774
|
+
@_internal_tool()
|
|
5775
|
+
@mcp.tool()
|
|
5776
|
+
def delimit_github_scan(
|
|
5777
|
+
cadence: str = "pulse",
|
|
5778
|
+
limit: int = 20,
|
|
5779
|
+
) -> Dict[str, Any]:
|
|
5780
|
+
"""Scan GitHub for adoption leads, competitive intel, and repo health (Pro).
|
|
5781
|
+
|
|
5782
|
+
Three cadences:
|
|
5783
|
+
pulse: Own repo health (stars, forks, issues, traffic). Fast, run often.
|
|
5784
|
+
hunter: Competitor users, adoption leads, pain threads. Medium, run hourly.
|
|
5785
|
+
deep: Full ecosystem intel. Slow, run daily.
|
|
5786
|
+
|
|
5787
|
+
IMPORTANT -- TOOL CHAINING RULE:
|
|
5788
|
+
After scanning, the AI MUST process high-score findings:
|
|
5789
|
+
1. Auto-ledger items (score >= 75 competitor users) via delimit_ledger_add
|
|
5790
|
+
2. Pain threads with existing_feature relevance via delimit_notify
|
|
5791
|
+
Never just return findings and stop. Always chain to the next action.
|
|
5792
|
+
|
|
5793
|
+
Args:
|
|
5794
|
+
cadence: pulse, hunter, or deep.
|
|
5795
|
+
limit: Max results per search query (default 20, max 30).
|
|
5796
|
+
"""
|
|
5797
|
+
from ai.github_scanner import scan
|
|
5798
|
+
|
|
5799
|
+
if cadence not in ("pulse", "hunter", "deep"):
|
|
5800
|
+
cadence = "pulse"
|
|
5801
|
+
capped_limit = min(max(1, limit), 30)
|
|
5802
|
+
|
|
5803
|
+
result = scan(cadence=cadence, limit=capped_limit)
|
|
5804
|
+
return _with_next_steps("github_scan", result)
|
|
5805
|
+
|
|
5806
|
+
|
|
5480
5807
|
# ═══════════════════════════════════════════════════════════════════════
|
|
5481
5808
|
# CONTENT ENGINE — Autonomous video + tweet pipeline (Pro)
|
|
5482
5809
|
# ═══════════════════════════════════════════════════════════════════════
|
|
@@ -5886,9 +6213,9 @@ def delimit_notify(channel: str = "webhook", message: str = "",
|
|
|
5886
6213
|
subject: Subject line (email only). Use [ACTION], [INFO], [ALERT] prefix.
|
|
5887
6214
|
event_type: Event category for filtering.
|
|
5888
6215
|
to: Recipient email address (email only). Overrides default DELIMIT_SMTP_TO.
|
|
5889
|
-
Send to any address — leave empty for default (
|
|
6216
|
+
Send to any address — leave empty for default (owner@example.com).
|
|
5890
6217
|
from_account: Sender account key from ~/.delimit/secrets/smtp-all.json
|
|
5891
|
-
(e.g. 'pro@
|
|
6218
|
+
(e.g. 'pro@example.com', 'admin@example.com'). Email only.
|
|
5892
6219
|
"""
|
|
5893
6220
|
from ai.notify import send_notification
|
|
5894
6221
|
return _with_next_steps("notify", _safe_call(
|
|
@@ -5994,7 +6321,7 @@ def delimit_notify_inbox(action: str = "status", limit: int = 10,
|
|
|
5994
6321
|
"""Check inbound email inbox, classify, and route (Pro).
|
|
5995
6322
|
|
|
5996
6323
|
Polls pro@delimit.ai via IMAP. Classifies emails as owner-action
|
|
5997
|
-
(forwards to
|
|
6324
|
+
(forwards to owner@example.com) or non-owner (stays in inbox).
|
|
5998
6325
|
|
|
5999
6326
|
Args:
|
|
6000
6327
|
action: 'status' (show inbox state), 'poll' (classify and optionally forward),
|
|
@@ -6344,6 +6671,253 @@ def delimit_loop_config(session_id: str = "", max_iterations: int = 0,
|
|
|
6344
6671
|
return _with_next_steps("loop_config", r)
|
|
6345
6672
|
|
|
6346
6673
|
|
|
6674
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
6675
|
+
# LED-219: Toolcard Delta Cache — reduce MCP tool schema token waste
|
|
6676
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
6677
|
+
|
|
6678
|
+
|
|
6679
|
+
@mcp.tool()
|
|
6680
|
+
def delimit_toolcard_cache(
|
|
6681
|
+
action: str = "status",
|
|
6682
|
+
tool_schemas: Optional[str] = None,
|
|
6683
|
+
tool_names: Optional[str] = None,
|
|
6684
|
+
) -> Dict[str, Any]:
|
|
6685
|
+
"""Manage the tool schema cache to reduce token waste (Pro).
|
|
6686
|
+
|
|
6687
|
+
MCP servers dump full tool definitions every session. This cache
|
|
6688
|
+
stores schemas and sends only diffs, cutting context bloat.
|
|
6689
|
+
|
|
6690
|
+
Actions:
|
|
6691
|
+
status: Show cache stats (tools cached, hit rate, token savings)
|
|
6692
|
+
register: Register tool schemas (JSON array string). Auto-runs on first call.
|
|
6693
|
+
delta: Get only changed/new tool names since last session (comma-separated names)
|
|
6694
|
+
clear: Clear the cache (forces full schema send next session)
|
|
6695
|
+
estimate: Estimate token savings for a tool set (JSON array string)
|
|
6696
|
+
flush: Write session stats to disk log
|
|
6697
|
+
|
|
6698
|
+
Args:
|
|
6699
|
+
action: One of: status, register, delta, clear, estimate, flush
|
|
6700
|
+
tool_schemas: JSON array of tool schema objects (for register/estimate)
|
|
6701
|
+
tool_names: Comma-separated tool names (for delta)
|
|
6702
|
+
"""
|
|
6703
|
+
from ai.license import require_premium
|
|
6704
|
+
gate = require_premium("toolcard_cache")
|
|
6705
|
+
if gate:
|
|
6706
|
+
return gate
|
|
6707
|
+
|
|
6708
|
+
from ai.toolcard_cache import get_cache
|
|
6709
|
+
cache = get_cache()
|
|
6710
|
+
|
|
6711
|
+
if action == "status":
|
|
6712
|
+
r = cache.get_stats()
|
|
6713
|
+
elif action == "register":
|
|
6714
|
+
if not tool_schemas:
|
|
6715
|
+
return _with_next_steps("toolcard_cache", {
|
|
6716
|
+
"error": "missing_param",
|
|
6717
|
+
"message": "register action requires tool_schemas (JSON array of tool schema objects)"
|
|
6718
|
+
})
|
|
6719
|
+
try:
|
|
6720
|
+
schemas = json.loads(tool_schemas)
|
|
6721
|
+
except json.JSONDecodeError as e:
|
|
6722
|
+
return _with_next_steps("toolcard_cache", {
|
|
6723
|
+
"error": "invalid_json", "message": str(e)
|
|
6724
|
+
})
|
|
6725
|
+
r = cache.register_tools(schemas)
|
|
6726
|
+
elif action == "delta":
|
|
6727
|
+
names = [n.strip() for n in (tool_names or "").split(",") if n.strip()]
|
|
6728
|
+
r = cache.get_delta(names)
|
|
6729
|
+
elif action == "clear":
|
|
6730
|
+
r = cache.clear()
|
|
6731
|
+
elif action == "estimate":
|
|
6732
|
+
if not tool_schemas:
|
|
6733
|
+
return _with_next_steps("toolcard_cache", {
|
|
6734
|
+
"error": "missing_param",
|
|
6735
|
+
"message": "estimate action requires tool_schemas (JSON array of tool schema objects)"
|
|
6736
|
+
})
|
|
6737
|
+
try:
|
|
6738
|
+
schemas = json.loads(tool_schemas)
|
|
6739
|
+
except json.JSONDecodeError as e:
|
|
6740
|
+
return _with_next_steps("toolcard_cache", {
|
|
6741
|
+
"error": "invalid_json", "message": str(e)
|
|
6742
|
+
})
|
|
6743
|
+
r = cache.estimate_savings(schemas)
|
|
6744
|
+
elif action == "flush":
|
|
6745
|
+
r = cache.flush_session()
|
|
6746
|
+
else:
|
|
6747
|
+
r = {"error": "unknown_action", "message": f"Unknown action: {action}. Use: status, register, delta, clear, estimate, flush"}
|
|
6748
|
+
|
|
6749
|
+
return _with_next_steps("toolcard_cache", r)
|
|
6750
|
+
|
|
6751
|
+
|
|
6752
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
6753
|
+
# HANDOFF RECEIPTS — Agent-to-Agent Structured Handoffs (LED-220)
|
|
6754
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
6755
|
+
|
|
6756
|
+
|
|
6757
|
+
@mcp.tool()
|
|
6758
|
+
def delimit_handoff_create(
|
|
6759
|
+
task_description: str = "",
|
|
6760
|
+
completed: str = "",
|
|
6761
|
+
not_completed: str = "",
|
|
6762
|
+
assumptions: str = "",
|
|
6763
|
+
blockers: str = "",
|
|
6764
|
+
files_modified: str = "",
|
|
6765
|
+
in_scope: str = "",
|
|
6766
|
+
out_of_scope: str = "",
|
|
6767
|
+
next_action: str = "",
|
|
6768
|
+
priority: str = "P1",
|
|
6769
|
+
to_model: str = "any",
|
|
6770
|
+
) -> Dict[str, Any]:
|
|
6771
|
+
"""Create a handoff receipt when transitioning between agents/sessions (Pro).
|
|
6772
|
+
|
|
6773
|
+
Documents what was done, what wasn't, and what the next agent should do.
|
|
6774
|
+
The receiving agent should acknowledge before starting work.
|
|
6775
|
+
|
|
6776
|
+
Args:
|
|
6777
|
+
task_description: What the task was (one line).
|
|
6778
|
+
completed: Comma-separated list of completed items.
|
|
6779
|
+
not_completed: Comma-separated list of items not completed (with reasons).
|
|
6780
|
+
assumptions: Comma-separated assumptions made during work.
|
|
6781
|
+
blockers: Comma-separated blockers encountered.
|
|
6782
|
+
files_modified: JSON list of {path, change_type, summary} dicts, or empty for auto-detect.
|
|
6783
|
+
in_scope: Comma-separated items that were in scope.
|
|
6784
|
+
out_of_scope: Comma-separated items explicitly excluded.
|
|
6785
|
+
next_action: What the receiving agent should do first.
|
|
6786
|
+
priority: P0/P1/P2.
|
|
6787
|
+
to_model: Target model (or "any").
|
|
6788
|
+
"""
|
|
6789
|
+
from ai.handoff_receipts import create_receipt as _create, format_receipt
|
|
6790
|
+
|
|
6791
|
+
def _split(val: str) -> List[str]:
|
|
6792
|
+
if not val or not val.strip():
|
|
6793
|
+
return []
|
|
6794
|
+
return [s.strip() for s in val.split(",") if s.strip()]
|
|
6795
|
+
|
|
6796
|
+
# Parse files_modified as JSON if provided
|
|
6797
|
+
parsed_files = None
|
|
6798
|
+
if files_modified and files_modified.strip():
|
|
6799
|
+
try:
|
|
6800
|
+
parsed_files = json.loads(files_modified)
|
|
6801
|
+
if not isinstance(parsed_files, list):
|
|
6802
|
+
parsed_files = None
|
|
6803
|
+
except json.JSONDecodeError:
|
|
6804
|
+
parsed_files = None
|
|
6805
|
+
|
|
6806
|
+
receipt = _create(
|
|
6807
|
+
task_description=task_description,
|
|
6808
|
+
completed=_split(completed),
|
|
6809
|
+
not_completed=_split(not_completed),
|
|
6810
|
+
assumptions=_split(assumptions),
|
|
6811
|
+
blockers=_split(blockers),
|
|
6812
|
+
files_modified=parsed_files,
|
|
6813
|
+
in_scope=_split(in_scope),
|
|
6814
|
+
out_of_scope=_split(out_of_scope),
|
|
6815
|
+
next_action=next_action,
|
|
6816
|
+
priority=priority,
|
|
6817
|
+
from_model=_detect_model(),
|
|
6818
|
+
to_model=to_model,
|
|
6819
|
+
)
|
|
6820
|
+
|
|
6821
|
+
formatted = format_receipt(receipt)
|
|
6822
|
+
return _with_next_steps("handoff_create", {
|
|
6823
|
+
"status": "created",
|
|
6824
|
+
"receipt_id": receipt.receipt_id,
|
|
6825
|
+
"project": receipt.project_path,
|
|
6826
|
+
"task_description": receipt.task_description,
|
|
6827
|
+
"completed_count": len(receipt.completed),
|
|
6828
|
+
"not_completed_count": len(receipt.not_completed),
|
|
6829
|
+
"files_count": len(receipt.files_modified),
|
|
6830
|
+
"next_action": receipt.next_action,
|
|
6831
|
+
"priority": receipt.priority,
|
|
6832
|
+
"formatted": formatted,
|
|
6833
|
+
"message": f"Handoff receipt {receipt.receipt_id} created. Receiving agent should run delimit_handoff_acknowledge(receipt_id=\"{receipt.receipt_id}\").",
|
|
6834
|
+
})
|
|
6835
|
+
|
|
6836
|
+
|
|
6837
|
+
@mcp.tool()
|
|
6838
|
+
def delimit_handoff_acknowledge(
|
|
6839
|
+
receipt_id: str = "",
|
|
6840
|
+
notes: str = "",
|
|
6841
|
+
) -> Dict[str, Any]:
|
|
6842
|
+
"""Acknowledge a handoff receipt before starting work (Pro).
|
|
6843
|
+
|
|
6844
|
+
Run this at the start of a session if there are pending handoff receipts.
|
|
6845
|
+
|
|
6846
|
+
Args:
|
|
6847
|
+
receipt_id: The receipt ID to acknowledge.
|
|
6848
|
+
notes: Optional notes from the receiving agent.
|
|
6849
|
+
"""
|
|
6850
|
+
from ai.handoff_receipts import acknowledge_receipt as _ack
|
|
6851
|
+
|
|
6852
|
+
if not receipt_id or not receipt_id.strip():
|
|
6853
|
+
return _with_next_steps("handoff_acknowledge", {
|
|
6854
|
+
"status": "error",
|
|
6855
|
+
"message": "receipt_id is required.",
|
|
6856
|
+
})
|
|
6857
|
+
|
|
6858
|
+
result = _ack(
|
|
6859
|
+
receipt_id=receipt_id.strip(),
|
|
6860
|
+
model=_detect_model(),
|
|
6861
|
+
notes=notes,
|
|
6862
|
+
)
|
|
6863
|
+
return _with_next_steps("handoff_acknowledge", result)
|
|
6864
|
+
|
|
6865
|
+
|
|
6866
|
+
@mcp.tool()
|
|
6867
|
+
def delimit_handoff_list(
|
|
6868
|
+
status: str = "pending",
|
|
6869
|
+
) -> Dict[str, Any]:
|
|
6870
|
+
"""List handoff receipts (Pro).
|
|
6871
|
+
|
|
6872
|
+
Args:
|
|
6873
|
+
status: "pending" (unacknowledged), "acknowledged", or "all".
|
|
6874
|
+
"""
|
|
6875
|
+
from ai.handoff_receipts import get_receipts, format_receipt
|
|
6876
|
+
from dataclasses import asdict
|
|
6877
|
+
|
|
6878
|
+
if status not in ("pending", "acknowledged", "all"):
|
|
6879
|
+
status = "pending"
|
|
6880
|
+
|
|
6881
|
+
receipts = get_receipts(status=status)
|
|
6882
|
+
|
|
6883
|
+
if not receipts:
|
|
6884
|
+
return _with_next_steps("handoff_list", {
|
|
6885
|
+
"status": "empty",
|
|
6886
|
+
"filter": status,
|
|
6887
|
+
"count": 0,
|
|
6888
|
+
"message": f"No {status} handoff receipts found.",
|
|
6889
|
+
})
|
|
6890
|
+
|
|
6891
|
+
formatted_list = []
|
|
6892
|
+
for r in receipts:
|
|
6893
|
+
formatted_list.append({
|
|
6894
|
+
"receipt_id": r.receipt_id,
|
|
6895
|
+
"created_at": r.created_at,
|
|
6896
|
+
"task_description": r.task_description,
|
|
6897
|
+
"from_model": r.from_model,
|
|
6898
|
+
"to_model": r.to_model,
|
|
6899
|
+
"priority": r.priority,
|
|
6900
|
+
"acknowledged": r.acknowledged,
|
|
6901
|
+
"completed_count": len(r.completed),
|
|
6902
|
+
"not_completed_count": len(r.not_completed),
|
|
6903
|
+
"next_action": r.next_action,
|
|
6904
|
+
})
|
|
6905
|
+
|
|
6906
|
+
# Format the first pending receipt in full for immediate context
|
|
6907
|
+
display = ""
|
|
6908
|
+
if status == "pending" and receipts:
|
|
6909
|
+
display = format_receipt(receipts[0])
|
|
6910
|
+
|
|
6911
|
+
return _with_next_steps("handoff_list", {
|
|
6912
|
+
"status": "ok",
|
|
6913
|
+
"filter": status,
|
|
6914
|
+
"count": len(receipts),
|
|
6915
|
+
"receipts": formatted_list,
|
|
6916
|
+
"display": display,
|
|
6917
|
+
"message": f"{len(receipts)} {status} receipt(s) found.",
|
|
6918
|
+
})
|
|
6919
|
+
|
|
6920
|
+
|
|
6347
6921
|
# ═══════════════════════════════════════════════════════════════════════
|
|
6348
6922
|
# ENTRY POINT
|
|
6349
6923
|
# ═══════════════════════════════════════════════════════════════════════
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "delimit-cli",
|
|
3
3
|
"mcpName": "io.github.delimit-ai/delimit-mcp-server",
|
|
4
|
-
"version": "3.15.
|
|
4
|
+
"version": "3.15.2",
|
|
5
5
|
"description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|