delimit-cli 4.0.3 → 4.0.4
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 +9 -242
- package/bin/delimit-cli.js +352 -4
- package/bin/delimit-setup.js +30 -2
- package/gateway/ai/agent_dispatch.py +34 -2
- package/gateway/ai/github_scanner.py +1 -1
- package/gateway/ai/ledger_manager.py +13 -3
- package/gateway/ai/ledger_propose.py +240 -0
- package/gateway/ai/loop_engine.py +175 -372
- package/gateway/ai/notify.py +700 -13
- package/gateway/ai/reddit_proxy.py +106 -0
- package/gateway/ai/reddit_scanner.py +34 -0
- package/gateway/ai/server.py +343 -81
- package/gateway/ai/siem_streaming.py +290 -0
- package/gateway/ai/social_daemon.py +189 -0
- package/gateway/ai/swarm.py +434 -0
- package/lib/continuity-resolver.js +325 -0
- package/lib/cross-model-hooks.js +212 -0
- package/lib/delimit-template.js +5 -0
- package/lib/session-shell.js +655 -0
- package/lib/session-worker.js +479 -0
- package/package.json +1 -1
- package/scripts/security-check.sh +12 -0
package/gateway/ai/server.py
CHANGED
|
@@ -19,9 +19,8 @@ All tools follow the Adapter Boundary Contract v1.0:
|
|
|
19
19
|
- Stateless between calls
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
# ──
|
|
22
|
+
# ── Output Quality Rules ──────────────────────────────────────────────
|
|
23
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
24
|
#
|
|
26
25
|
# Core: serious builder/operator, not a marketer. Credibility over persuasion.
|
|
27
26
|
# Truth over excitement. Concrete mechanisms, not vague benefits.
|
|
@@ -49,6 +48,7 @@ import os
|
|
|
49
48
|
import re
|
|
50
49
|
import shutil
|
|
51
50
|
import subprocess
|
|
51
|
+
import threading
|
|
52
52
|
import traceback
|
|
53
53
|
import uuid
|
|
54
54
|
from datetime import datetime, timezone
|
|
@@ -257,12 +257,57 @@ def _coerce_dict_arg(
|
|
|
257
257
|
HIGH_RISK_TOOLS = {
|
|
258
258
|
'deploy_publish', 'deploy_rollback', 'deploy_npm', 'deploy_site',
|
|
259
259
|
'security_scan', 'data_migrate', 'data_backup',
|
|
260
|
+
# Social/outreach — drafts are fine, but posting/approving requires gate
|
|
261
|
+
'social_approve', 'content_publish',
|
|
262
|
+
# Agent dispatch — spawns autonomous work, must be gated
|
|
263
|
+
'agent_dispatch', 'daemon_run',
|
|
264
|
+
# Deliberation — burns model quota
|
|
265
|
+
'deliberate',
|
|
260
266
|
}
|
|
261
267
|
|
|
262
268
|
CRITICAL_RISK_TOOLS = {
|
|
263
269
|
'deploy_rollback', 'data_migrate',
|
|
264
270
|
}
|
|
265
271
|
|
|
272
|
+
# Rate limits per tool per hour — prevents runaway loops
|
|
273
|
+
_TOOL_RATE_LIMITS = {
|
|
274
|
+
'social_post': 10,
|
|
275
|
+
'social_target': 20,
|
|
276
|
+
'social_approve': 10,
|
|
277
|
+
'notify': 15,
|
|
278
|
+
'deliberate': 5,
|
|
279
|
+
'agent_dispatch': 5,
|
|
280
|
+
'ledger_add': 30,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
_tool_call_counts: Dict[str, list] = {}
|
|
284
|
+
_tool_rate_lock = threading.Lock()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _check_rate_limit(tool_name: str) -> Optional[Dict]:
|
|
288
|
+
"""Enforce per-tool rate limits. Returns error dict if over limit, None if allowed."""
|
|
289
|
+
clean = tool_name.replace('delimit_', '')
|
|
290
|
+
limit = _TOOL_RATE_LIMITS.get(clean)
|
|
291
|
+
if not limit:
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
import time as _rl_time
|
|
295
|
+
now = _rl_time.time()
|
|
296
|
+
with _tool_rate_lock:
|
|
297
|
+
calls = _tool_call_counts.setdefault(clean, [])
|
|
298
|
+
# Prune calls older than 1 hour
|
|
299
|
+
calls[:] = [t for t in calls if now - t < 3600]
|
|
300
|
+
if len(calls) >= limit:
|
|
301
|
+
return {
|
|
302
|
+
"status": "rate_limited",
|
|
303
|
+
"reason": f"Tool '{tool_name}' called {len(calls)} times in the last hour (limit: {limit}). "
|
|
304
|
+
f"This prevents runaway loops. Wait or check your scan configuration.",
|
|
305
|
+
"calls_this_hour": len(calls),
|
|
306
|
+
"limit": limit,
|
|
307
|
+
}
|
|
308
|
+
calls.append(now)
|
|
309
|
+
return None
|
|
310
|
+
|
|
266
311
|
|
|
267
312
|
def _classify_risk(tool_name: str) -> str:
|
|
268
313
|
"""Classify tool risk level for approval gate decisions."""
|
|
@@ -1160,6 +1205,54 @@ def _detect_environment() -> Dict[str, Any]:
|
|
|
1160
1205
|
_inbox_daemon_autostarted = False
|
|
1161
1206
|
_toolcard_cache_autoregistered = False
|
|
1162
1207
|
|
|
1208
|
+
# MCP response size cap — prevents Node.js heap OOM on all clients (Gemini CLI, Cursor, etc.)
|
|
1209
|
+
# FastMCP serializes responses to JSON over stdio; large payloads crash Node's default 1.5GB heap.
|
|
1210
|
+
# Cap is set high enough that all normal tool responses (deliberation, audit, ledger) pass through
|
|
1211
|
+
# untouched. Only pathological cases (e.g. 910-item scan dumps) get trimmed.
|
|
1212
|
+
_MCP_RESPONSE_SIZE_LIMIT = 200_000 # 200KB hard ceiling
|
|
1213
|
+
|
|
1214
|
+
# Fields within list items that are safe to truncate — display text, not structured data
|
|
1215
|
+
_ITEM_TEXT_FIELDS = {"content_snippet", "body", "text", "rationale", "full_text", "description", "summary"}
|
|
1216
|
+
_ITEM_TEXT_MAX = 300 # chars per field within a list item
|
|
1217
|
+
|
|
1218
|
+
def _cap_response(result: Dict[str, Any]) -> Dict[str, Any]:
|
|
1219
|
+
"""Truncate response payload to _MCP_RESPONSE_SIZE_LIMIT bytes.
|
|
1220
|
+
|
|
1221
|
+
Strategy (least destructive first):
|
|
1222
|
+
1. Trim known text-only fields within list items (content_snippet, body, etc.)
|
|
1223
|
+
2. If still over limit, truncate lists to first 20 items
|
|
1224
|
+
3. If still over limit, add a note — structural data is never silently dropped
|
|
1225
|
+
"""
|
|
1226
|
+
import json as _json, copy as _copy
|
|
1227
|
+
if len(_json.dumps(result)) <= _MCP_RESPONSE_SIZE_LIMIT:
|
|
1228
|
+
return result
|
|
1229
|
+
r = _copy.deepcopy(result)
|
|
1230
|
+
|
|
1231
|
+
# Pass 1: trim display-text fields inside list items (safe — these are human-readable snippets)
|
|
1232
|
+
for k, v in r.items():
|
|
1233
|
+
if isinstance(v, list):
|
|
1234
|
+
for item in v:
|
|
1235
|
+
if isinstance(item, dict):
|
|
1236
|
+
for field in _ITEM_TEXT_FIELDS:
|
|
1237
|
+
if field in item and isinstance(item[field], str) and len(item[field]) > _ITEM_TEXT_MAX:
|
|
1238
|
+
item[field] = item[field][:_ITEM_TEXT_MAX] + "…"
|
|
1239
|
+
if len(_json.dumps(r)) <= _MCP_RESPONSE_SIZE_LIMIT:
|
|
1240
|
+
return r
|
|
1241
|
+
|
|
1242
|
+
# Pass 2: truncate lists to first 20 items
|
|
1243
|
+
truncated_keys = [k for k, v in r.items() if isinstance(v, list) and len(v) > 20]
|
|
1244
|
+
for k in truncated_keys:
|
|
1245
|
+
total = len(r[k])
|
|
1246
|
+
r[k] = r[k][:20]
|
|
1247
|
+
r.setdefault("_pagination", {})[k] = {"returned": 20, "total": total, "note": "Use limit= to page"}
|
|
1248
|
+
if len(_json.dumps(r)) <= _MCP_RESPONSE_SIZE_LIMIT:
|
|
1249
|
+
return r
|
|
1250
|
+
|
|
1251
|
+
# Pass 3: last resort — note that response is large but return it anyway
|
|
1252
|
+
# Better to let the client decide than silently drop structured data
|
|
1253
|
+
r["_size_warning"] = f"Response exceeds {_MCP_RESPONSE_SIZE_LIMIT // 1000}KB. Use limit= or action='list' to reduce payload."
|
|
1254
|
+
return r
|
|
1255
|
+
|
|
1163
1256
|
|
|
1164
1257
|
def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
1165
1258
|
"""Route every tool result through governance. This IS the loop.
|
|
@@ -1223,6 +1316,12 @@ def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1223
1316
|
f"Rewrite with concrete mechanisms, not vague benefits."
|
|
1224
1317
|
)
|
|
1225
1318
|
|
|
1319
|
+
# Rate limit check — prevents runaway loops from any model
|
|
1320
|
+
rate_gate = _check_rate_limit(tool_name)
|
|
1321
|
+
if rate_gate:
|
|
1322
|
+
_emit_event(tool_name, rate_gate)
|
|
1323
|
+
return _cap_response(rate_gate)
|
|
1324
|
+
|
|
1226
1325
|
# Emit event for real-time dashboard
|
|
1227
1326
|
_emit_event(tool_name, result)
|
|
1228
1327
|
|
|
@@ -1231,7 +1330,7 @@ def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1231
1330
|
if policy_gate:
|
|
1232
1331
|
policy_gate["original_result"] = result
|
|
1233
1332
|
policy_gate["governance"] = {"action": "policy_blocked", "reason": policy_gate["reason"]}
|
|
1234
|
-
return policy_gate
|
|
1333
|
+
return _cap_response(policy_gate)
|
|
1235
1334
|
|
|
1236
1335
|
# LED-195: Prompt injection detection on tool inputs
|
|
1237
1336
|
if isinstance(result, dict):
|
|
@@ -1248,12 +1347,12 @@ def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1248
1347
|
# Route through governance loop
|
|
1249
1348
|
try:
|
|
1250
1349
|
from ai.governance import govern
|
|
1251
|
-
return govern(tool_name, result)
|
|
1350
|
+
return _cap_response(govern(tool_name, result))
|
|
1252
1351
|
except Exception:
|
|
1253
1352
|
# Fallback: just add next_steps from registry
|
|
1254
1353
|
steps = NEXT_STEPS_REGISTRY.get(tool_name, [])
|
|
1255
1354
|
result["next_steps"] = steps
|
|
1256
|
-
return result
|
|
1355
|
+
return _cap_response(result)
|
|
1257
1356
|
|
|
1258
1357
|
|
|
1259
1358
|
# ═══════════════════════════════════════════════════════════════════════
|
|
@@ -2077,7 +2176,6 @@ def delimit_deploy_publish(app: str = "", git_ref: Optional[str] = None) -> Dict
|
|
|
2077
2176
|
return _delimit_deploy_impl(action="publish", app=app, git_ref=git_ref)
|
|
2078
2177
|
|
|
2079
2178
|
|
|
2080
|
-
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2081
2179
|
@mcp.tool()
|
|
2082
2180
|
def delimit_deploy_verify(app: str = "", env: str = "", git_ref: Optional[str] = None) -> Dict[str, Any]:
|
|
2083
2181
|
"""Verify deployment health (experimental) (Pro)."""
|
|
@@ -2180,7 +2278,6 @@ def delimit_intel_query(
|
|
|
2180
2278
|
|
|
2181
2279
|
# ─── Generate ───────────────────────────────────────────────────────────
|
|
2182
2280
|
|
|
2183
|
-
@_internal_tool()
|
|
2184
2281
|
@mcp.tool()
|
|
2185
2282
|
def delimit_generate_template(
|
|
2186
2283
|
template_type: str,
|
|
@@ -2207,7 +2304,6 @@ def delimit_generate_template(
|
|
|
2207
2304
|
return _with_next_steps("generate_template", _safe_call(template, template_type=template_type, name=name, framework=framework, features=features, target=target))
|
|
2208
2305
|
|
|
2209
2306
|
|
|
2210
|
-
@_internal_tool()
|
|
2211
2307
|
@mcp.tool()
|
|
2212
2308
|
def delimit_generate_scaffold(
|
|
2213
2309
|
project_type: str,
|
|
@@ -2231,7 +2327,6 @@ def delimit_generate_scaffold(
|
|
|
2231
2327
|
|
|
2232
2328
|
# ─── Repo (RepoDoctor + ConfigSentry) ──────────────────────────────────
|
|
2233
2329
|
|
|
2234
|
-
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2235
2330
|
@mcp.tool()
|
|
2236
2331
|
def delimit_repo_diagnose(target: str = ".") -> Dict[str, Any]:
|
|
2237
2332
|
"""Diagnose repository health issues (experimental) (Pro).
|
|
@@ -2247,7 +2342,6 @@ def delimit_repo_diagnose(target: str = ".") -> Dict[str, Any]:
|
|
|
2247
2342
|
return _safe_call(diagnose, target=target)
|
|
2248
2343
|
|
|
2249
2344
|
|
|
2250
|
-
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2251
2345
|
@mcp.tool()
|
|
2252
2346
|
def delimit_repo_analyze(target: str = ".") -> Dict[str, Any]:
|
|
2253
2347
|
"""Analyze repository structure and quality (experimental) (Pro).
|
|
@@ -2263,7 +2357,6 @@ def delimit_repo_analyze(target: str = ".") -> Dict[str, Any]:
|
|
|
2263
2357
|
return _safe_call(analyze, target=target)
|
|
2264
2358
|
|
|
2265
2359
|
|
|
2266
|
-
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2267
2360
|
@mcp.tool()
|
|
2268
2361
|
def delimit_repo_config_validate(target: str = ".") -> Dict[str, Any]:
|
|
2269
2362
|
"""Validate configuration files (experimental) (Pro).
|
|
@@ -2279,7 +2372,6 @@ def delimit_repo_config_validate(target: str = ".") -> Dict[str, Any]:
|
|
|
2279
2372
|
return _safe_call(config_validate, target=target)
|
|
2280
2373
|
|
|
2281
2374
|
|
|
2282
|
-
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2283
2375
|
@mcp.tool()
|
|
2284
2376
|
def delimit_repo_config_audit(target: str = ".") -> Dict[str, Any]:
|
|
2285
2377
|
"""Audit configuration compliance (experimental) (Pro).
|
|
@@ -2673,6 +2765,55 @@ def delimit_security_deliberate(
|
|
|
2673
2765
|
})
|
|
2674
2766
|
|
|
2675
2767
|
|
|
2768
|
+
@mcp.tool()
|
|
2769
|
+
def delimit_siem(action: str = "status", integration: str = "",
|
|
2770
|
+
settings: str = "", enabled: str = "",
|
|
2771
|
+
event: str = "") -> Dict[str, Any]:
|
|
2772
|
+
"""Manage SIEM streaming — forward audit events to Splunk, Datadog, EventBridge, or webhooks.
|
|
2773
|
+
|
|
2774
|
+
Actions:
|
|
2775
|
+
status: Show all SIEM integrations and delivery stats
|
|
2776
|
+
configure: Update integration settings (pass integration name + settings JSON)
|
|
2777
|
+
test: Send a test event to all enabled integrations
|
|
2778
|
+
forward: Forward a specific event (used internally by audit trail)
|
|
2779
|
+
|
|
2780
|
+
Args:
|
|
2781
|
+
action: status, configure, test, or forward
|
|
2782
|
+
integration: splunk, datadog, eventbridge, or webhook (for configure)
|
|
2783
|
+
settings: JSON string of settings to update (for configure)
|
|
2784
|
+
enabled: "true" or "false" to enable/disable (for configure)
|
|
2785
|
+
event: JSON string of event to forward (for forward/test)
|
|
2786
|
+
"""
|
|
2787
|
+
from ai.siem_streaming import configure, get_status, forward_event
|
|
2788
|
+
|
|
2789
|
+
if action == "status":
|
|
2790
|
+
return _with_next_steps("siem", get_status())
|
|
2791
|
+
if action == "configure":
|
|
2792
|
+
parsed_settings = {}
|
|
2793
|
+
if settings:
|
|
2794
|
+
try:
|
|
2795
|
+
parsed_settings = json.loads(settings)
|
|
2796
|
+
except json.JSONDecodeError:
|
|
2797
|
+
return _with_next_steps("siem", {"error": "Invalid JSON in settings"})
|
|
2798
|
+
is_enabled = None
|
|
2799
|
+
if enabled:
|
|
2800
|
+
is_enabled = enabled.lower() in ("true", "1", "yes")
|
|
2801
|
+
return _with_next_steps("siem", configure(
|
|
2802
|
+
integration=integration,
|
|
2803
|
+
settings=parsed_settings if parsed_settings else None,
|
|
2804
|
+
enabled=is_enabled,
|
|
2805
|
+
))
|
|
2806
|
+
if action in ("test", "forward"):
|
|
2807
|
+
test_event = {"type": "test", "timestamp": time.time(), "source": "siem_test"}
|
|
2808
|
+
if event:
|
|
2809
|
+
try:
|
|
2810
|
+
test_event = json.loads(event)
|
|
2811
|
+
except json.JSONDecodeError:
|
|
2812
|
+
pass
|
|
2813
|
+
return _with_next_steps("siem", forward_event(test_event))
|
|
2814
|
+
return _with_next_steps("siem", {"error": f"Unknown action: {action}"})
|
|
2815
|
+
|
|
2816
|
+
|
|
2676
2817
|
@mcp.tool()
|
|
2677
2818
|
def delimit_security_audit(target: str = ".") -> Dict[str, Any]:
|
|
2678
2819
|
"""Audit security: dependency vulnerabilities, anti-patterns, and secret detection.
|
|
@@ -2925,14 +3066,12 @@ def delimit_release_status(environment: str = "production") -> Dict[str, Any]:
|
|
|
2925
3066
|
return _delimit_release_impl(action="status", environment=environment)
|
|
2926
3067
|
|
|
2927
3068
|
|
|
2928
|
-
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2929
3069
|
@mcp.tool()
|
|
2930
3070
|
def delimit_release_rollback(environment: str, version: str, to_version: str) -> Dict[str, Any]:
|
|
2931
3071
|
"""Rollback deployment to previous version (experimental)."""
|
|
2932
3072
|
return _delimit_release_impl(action="rollback", environment=environment, version=version, to_version=to_version)
|
|
2933
3073
|
|
|
2934
3074
|
|
|
2935
|
-
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2936
3075
|
@mcp.tool()
|
|
2937
3076
|
def delimit_release_history(environment: str, limit: int = 10) -> Dict[str, Any]:
|
|
2938
3077
|
"""Show release history (experimental)."""
|
|
@@ -3036,7 +3175,6 @@ def delimit_cost_controls(
|
|
|
3036
3175
|
|
|
3037
3176
|
# ─── DataSteward (Governance Primitive) ────────────────────────────────
|
|
3038
3177
|
|
|
3039
|
-
@_internal_tool()
|
|
3040
3178
|
@mcp.tool()
|
|
3041
3179
|
def delimit_data_validate(target: str = ".") -> Dict[str, Any]:
|
|
3042
3180
|
"""Validate data files: JSON parse, CSV structure, SQLite integrity check.
|
|
@@ -3048,7 +3186,6 @@ def delimit_data_validate(target: str = ".") -> Dict[str, Any]:
|
|
|
3048
3186
|
return _with_next_steps("data_validate", _safe_call(data_validate, target=target))
|
|
3049
3187
|
|
|
3050
3188
|
|
|
3051
|
-
@_internal_tool()
|
|
3052
3189
|
@mcp.tool()
|
|
3053
3190
|
def delimit_data_migrate(target: str = ".") -> Dict[str, Any]:
|
|
3054
3191
|
"""Check for migration files (alembic, Django, Prisma, Knex) and report status.
|
|
@@ -3060,7 +3197,6 @@ def delimit_data_migrate(target: str = ".") -> Dict[str, Any]:
|
|
|
3060
3197
|
return _with_next_steps("data_migrate", _safe_call(data_migrate, target=target))
|
|
3061
3198
|
|
|
3062
3199
|
|
|
3063
|
-
@_internal_tool()
|
|
3064
3200
|
@mcp.tool()
|
|
3065
3201
|
def delimit_data_backup(target: str = ".") -> Dict[str, Any]:
|
|
3066
3202
|
"""Back up SQLite and JSON data files to ~/.delimit/backups/ with timestamp.
|
|
@@ -3151,7 +3287,6 @@ def delimit_obs_logs(query: str, time_range: str = "1h", source: Optional[str] =
|
|
|
3151
3287
|
return _delimit_obs_impl(action="logs", query=query, time_range=time_range, source=source)
|
|
3152
3288
|
|
|
3153
3289
|
|
|
3154
|
-
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
3155
3290
|
@mcp.tool()
|
|
3156
3291
|
def delimit_obs_alerts(action: str, alert_rule: Optional[Dict[str, Any]] = None, rule_id: Optional[str] = None) -> Dict[str, Any]:
|
|
3157
3292
|
"""Manage alerting rules (experimental)."""
|
|
@@ -3166,7 +3301,6 @@ def delimit_obs_status() -> Dict[str, Any]:
|
|
|
3166
3301
|
|
|
3167
3302
|
# ─── DesignSystem (UI Tooling) ──────────────────────────────────────────
|
|
3168
3303
|
|
|
3169
|
-
@_internal_tool()
|
|
3170
3304
|
@mcp.tool()
|
|
3171
3305
|
def delimit_design_extract_tokens(
|
|
3172
3306
|
figma_file_key: Optional[str] = None,
|
|
@@ -3192,7 +3326,6 @@ def delimit_design_extract_tokens(
|
|
|
3192
3326
|
return _with_next_steps("design_extract_tokens", _safe_call(design_extract_tokens, figma_file_key=figma_file_key, token_types=token_types, project_path=project_path))
|
|
3193
3327
|
|
|
3194
3328
|
|
|
3195
|
-
@_internal_tool()
|
|
3196
3329
|
@mcp.tool()
|
|
3197
3330
|
def delimit_design_generate_component(component_name: str, figma_node_id: Optional[str] = None, output_path: Optional[str] = None, project_path: Optional[str] = None) -> Dict[str, Any]:
|
|
3198
3331
|
"""Generate a React/Next.js component skeleton with props interface and Tailwind support.
|
|
@@ -3207,7 +3340,6 @@ def delimit_design_generate_component(component_name: str, figma_node_id: Option
|
|
|
3207
3340
|
return _with_next_steps("design_generate_component", _safe_call(design_generate_component, component_name=component_name, figma_node_id=figma_node_id, output_path=output_path, project_path=project_path))
|
|
3208
3341
|
|
|
3209
3342
|
|
|
3210
|
-
@_internal_tool()
|
|
3211
3343
|
@mcp.tool()
|
|
3212
3344
|
def delimit_design_generate_tailwind(figma_file_key: Optional[str] = None, output_path: Optional[str] = None, project_path: Optional[str] = None) -> Dict[str, Any]:
|
|
3213
3345
|
"""Read existing tailwind.config or generate one from detected CSS tokens.
|
|
@@ -3221,7 +3353,6 @@ def delimit_design_generate_tailwind(figma_file_key: Optional[str] = None, outpu
|
|
|
3221
3353
|
return _with_next_steps("design_generate_tailwind", _safe_call(design_generate_tailwind, figma_file_key=figma_file_key, output_path=output_path, project_path=project_path))
|
|
3222
3354
|
|
|
3223
3355
|
|
|
3224
|
-
@_ops_pack_tool()
|
|
3225
3356
|
@mcp.tool()
|
|
3226
3357
|
def delimit_design_validate_responsive(
|
|
3227
3358
|
project_path: str,
|
|
@@ -3243,7 +3374,6 @@ def delimit_design_validate_responsive(
|
|
|
3243
3374
|
return _with_next_steps("design_validate_responsive", _safe_call(design_validate_responsive, project_path=project_path, check_types=check_types))
|
|
3244
3375
|
|
|
3245
3376
|
|
|
3246
|
-
@_internal_tool()
|
|
3247
3377
|
@mcp.tool()
|
|
3248
3378
|
def delimit_design_component_library(project_path: str, output_format: str = "json") -> Dict[str, Any]:
|
|
3249
3379
|
"""Scan for React/Vue/Svelte components and generate a component catalog.
|
|
@@ -3258,7 +3388,6 @@ def delimit_design_component_library(project_path: str, output_format: str = "js
|
|
|
3258
3388
|
|
|
3259
3389
|
# ─── Story (Component Stories + Visual/A11y Testing) ────────────────────
|
|
3260
3390
|
|
|
3261
|
-
@_internal_tool()
|
|
3262
3391
|
@mcp.tool()
|
|
3263
3392
|
def delimit_story_generate(
|
|
3264
3393
|
component_path: str,
|
|
@@ -3280,7 +3409,6 @@ def delimit_story_generate(
|
|
|
3280
3409
|
return _with_next_steps("story_generate", _safe_call(story_generate, component_path=component_path, story_name=story_name, variants=variants))
|
|
3281
3410
|
|
|
3282
3411
|
|
|
3283
|
-
@_internal_tool()
|
|
3284
3412
|
@mcp.tool()
|
|
3285
3413
|
def delimit_story_visual_test(url: str, project_path: Optional[str] = None, threshold: float = 0.05) -> Dict[str, Any]:
|
|
3286
3414
|
"""Run visual regression test -- screenshot and compare to baseline.
|
|
@@ -3298,7 +3426,6 @@ def delimit_story_visual_test(url: str, project_path: Optional[str] = None, thre
|
|
|
3298
3426
|
return _with_next_steps("story_visual_test", _safe_call(story_visual_test, url=url, project_path=project_path, threshold=threshold))
|
|
3299
3427
|
|
|
3300
3428
|
|
|
3301
|
-
@_internal_tool() # Was experimental (LED-044), promoted to internal (Consensus 120)
|
|
3302
3429
|
@mcp.tool()
|
|
3303
3430
|
def delimit_story_build(project_path: str, output_dir: Optional[str] = None) -> Dict[str, Any]:
|
|
3304
3431
|
"""Build Storybook static site.
|
|
@@ -3314,7 +3441,6 @@ def delimit_story_build(project_path: str, output_dir: Optional[str] = None) ->
|
|
|
3314
3441
|
return _safe_call(story_build, project_path=project_path, output_dir=output_dir)
|
|
3315
3442
|
|
|
3316
3443
|
|
|
3317
|
-
@_internal_tool()
|
|
3318
3444
|
@mcp.tool()
|
|
3319
3445
|
def delimit_story_accessibility(project_path: str, standards: str = "WCAG2AA") -> Dict[str, Any]:
|
|
3320
3446
|
"""Run WCAG accessibility checks by scanning HTML/JSX/TSX for common issues.
|
|
@@ -3347,7 +3473,6 @@ def delimit_test_generate(project_path: str, source_files: Optional[List[str]] =
|
|
|
3347
3473
|
return _with_next_steps("test_generate", _safe_call(test_generate, project_path=project_path, source_files=source_files, framework=framework))
|
|
3348
3474
|
|
|
3349
3475
|
|
|
3350
|
-
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
3351
3476
|
@mcp.tool()
|
|
3352
3477
|
def delimit_test_coverage(project_path: str, threshold: int = 80) -> Dict[str, Any]:
|
|
3353
3478
|
"""Analyze test coverage (experimental) (Pro).
|
|
@@ -3381,7 +3506,6 @@ def delimit_test_smoke(project_path: str, test_suite: Optional[str] = None) -> D
|
|
|
3381
3506
|
|
|
3382
3507
|
# ─── Docs (Real implementations) ─────────────────────────────────────
|
|
3383
3508
|
|
|
3384
|
-
@_ops_pack_tool()
|
|
3385
3509
|
@mcp.tool()
|
|
3386
3510
|
def delimit_docs_generate(target: str = ".") -> Dict[str, Any]:
|
|
3387
3511
|
"""Generate API reference documentation for a project.
|
|
@@ -3438,7 +3562,7 @@ async def delimit_sensor_github_issue(
|
|
|
3438
3562
|
with new comments, issue state, and severity classification.
|
|
3439
3563
|
|
|
3440
3564
|
Args:
|
|
3441
|
-
repo: GitHub repository in owner/repo format (e.g. "
|
|
3565
|
+
repo: GitHub repository in owner/repo format (e.g. "owner/repo").
|
|
3442
3566
|
issue_number: The issue number to monitor.
|
|
3443
3567
|
since_comment_id: Last seen comment ID. Pass 0 to get all comments.
|
|
3444
3568
|
"""
|
|
@@ -3650,14 +3774,20 @@ def delimit_swarm(action: str = "status", venture: str = "",
|
|
|
3650
3774
|
approve: Check approval tier for an action
|
|
3651
3775
|
guide: Get usage documentation
|
|
3652
3776
|
rules: Get escalation rules
|
|
3777
|
+
create_tool: Create a custom MCP tool (architect/senior_dev only)
|
|
3778
|
+
list_tools: List custom tools per venture
|
|
3779
|
+
reload: Hot-reload MCP server to pick up updated modules
|
|
3780
|
+
create_agent: Provision a new specialist agent role (architect only)
|
|
3781
|
+
approve_agent: Activate a pending custom agent (founder only)
|
|
3782
|
+
list_agents: List all agents (built-in + custom)
|
|
3653
3783
|
|
|
3654
3784
|
Args:
|
|
3655
|
-
action:
|
|
3656
|
-
venture: Venture name (for register/venture).
|
|
3657
|
-
agent_id: Agent ID
|
|
3658
|
-
repo_path: Repo path
|
|
3785
|
+
action: See actions above.
|
|
3786
|
+
venture: Venture name (for register/venture/create_agent).
|
|
3787
|
+
agent_id: Agent ID (for agent/check/create_agent/approve_agent).
|
|
3788
|
+
repo_path: Repo path, description, or reason depending on action.
|
|
3659
3789
|
deploy_target: Deploy target for venture registration.
|
|
3660
|
-
target_path: File path
|
|
3790
|
+
target_path: File path, tool name, or role name depending on action.
|
|
3661
3791
|
access_action: Action name — for check: "read"/"write"/"deploy". For approve: "deploy_production"/"deploy_staging"/"social_post" etc.
|
|
3662
3792
|
"""
|
|
3663
3793
|
from ai.swarm import (register_venture, get_venture, get_agent,
|
|
@@ -3705,6 +3835,34 @@ def delimit_swarm(action: str = "status", venture: str = "",
|
|
|
3705
3835
|
if action == "list_tools":
|
|
3706
3836
|
from ai.swarm import list_custom_tools
|
|
3707
3837
|
return _with_next_steps("swarm", _safe_call(list_custom_tools, venture=venture))
|
|
3838
|
+
if action == "reload":
|
|
3839
|
+
from ai.swarm import hot_reload
|
|
3840
|
+
return _with_next_steps("swarm", _safe_call(hot_reload, reason=repo_path or "manual"))
|
|
3841
|
+
if action == "create_agent":
|
|
3842
|
+
from ai.swarm import create_agent
|
|
3843
|
+
return _with_next_steps("swarm", _safe_call(
|
|
3844
|
+
create_agent,
|
|
3845
|
+
venture=venture,
|
|
3846
|
+
role_name=target_path,
|
|
3847
|
+
description=repo_path or "",
|
|
3848
|
+
default_model=agent_id or "claude-opus-4.6",
|
|
3849
|
+
creator_agent_id=agent_id,
|
|
3850
|
+
))
|
|
3851
|
+
if action == "approve_agent":
|
|
3852
|
+
from ai.swarm import approve_agent
|
|
3853
|
+
return _with_next_steps("swarm", _safe_call(approve_agent, agent_id=agent_id))
|
|
3854
|
+
if action == "list_agents":
|
|
3855
|
+
from ai.swarm import list_agents
|
|
3856
|
+
return _with_next_steps("swarm", _safe_call(list_agents, venture=venture))
|
|
3857
|
+
if action == "preflight":
|
|
3858
|
+
from ai.swarm import preflight_check
|
|
3859
|
+
return _with_next_steps("swarm", _safe_call(
|
|
3860
|
+
preflight_check,
|
|
3861
|
+
action=access_action or "general",
|
|
3862
|
+
venture=venture,
|
|
3863
|
+
path=target_path or repo_path or "",
|
|
3864
|
+
agent_id=agent_id,
|
|
3865
|
+
))
|
|
3708
3866
|
return _with_next_steps("swarm", _safe_call(get_swarm_status))
|
|
3709
3867
|
|
|
3710
3868
|
|
|
@@ -5681,7 +5839,6 @@ def delimit_webhook_manage(
|
|
|
5681
5839
|
# ═══════════════════════════════════════════════════════════════════════
|
|
5682
5840
|
|
|
5683
5841
|
|
|
5684
|
-
@_ops_pack_tool()
|
|
5685
5842
|
@mcp.tool()
|
|
5686
5843
|
def delimit_social_post(text: str = "", category: str = "", platform: str = "twitter",
|
|
5687
5844
|
account: str = "", quote_tweet_id: str = "",
|
|
@@ -5703,9 +5860,9 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
5703
5860
|
IMPORTANT — Platform tone rules (these are DIFFERENT per platform):
|
|
5704
5861
|
- Twitter: confident technical brand. Direct, professional, ALWAYS POSITIVE.
|
|
5705
5862
|
Celebrate wins and progress. Never complain or air gaps publicly.
|
|
5706
|
-
No em dashes or en dashes.
|
|
5707
|
-
- Reddit:
|
|
5708
|
-
|
|
5863
|
+
No em dashes or en dashes. Default to insight-first with no CTA unless source-grounded.
|
|
5864
|
+
- Reddit: helpful builder voice. Grounded, concise, never salesy.
|
|
5865
|
+
Default to no Delimit mention unless directly necessary and source-grounded.
|
|
5709
5866
|
NO bullet points/lists/bold/em dashes. 2-3 sentences max.
|
|
5710
5867
|
- LinkedIn: professional hook + insight + CTA
|
|
5711
5868
|
|
|
@@ -5731,6 +5888,36 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
5731
5888
|
if platform not in ("twitter", "reddit"):
|
|
5732
5889
|
return {"error": f"Platform '{platform}' not supported yet", "supported": ["twitter", "reddit"]}
|
|
5733
5890
|
|
|
5891
|
+
# ── Draft quality gate — reject template stubs and platform mismatches ──
|
|
5892
|
+
_draft_text = text or post.get("text", "")
|
|
5893
|
+
_stub_patterns = [
|
|
5894
|
+
"[DRAFT - needs human writing]",
|
|
5895
|
+
"[DRAFT -",
|
|
5896
|
+
"Engagement opportunity for",
|
|
5897
|
+
"needs human writing",
|
|
5898
|
+
]
|
|
5899
|
+
if any(pat in _draft_text for pat in _stub_patterns):
|
|
5900
|
+
return {
|
|
5901
|
+
"error": "Draft rejected: template stub detected. Write an actual reply, not a placeholder.",
|
|
5902
|
+
"rejected_text": _draft_text[:200],
|
|
5903
|
+
"hint": "Read the thread, understand the conversation, then write a genuine 2-3 sentence reply. "
|
|
5904
|
+
"Helpful first, no product pitch, no invented personal story, no generic pep talk.",
|
|
5905
|
+
}
|
|
5906
|
+
|
|
5907
|
+
# Reject drafts shorter than 50 chars (likely not a real reply)
|
|
5908
|
+
if len(_draft_text.strip()) < 50:
|
|
5909
|
+
return {
|
|
5910
|
+
"error": "Draft rejected: too short. Write a substantive reply (50+ characters).",
|
|
5911
|
+
"text_length": len(_draft_text.strip()),
|
|
5912
|
+
}
|
|
5913
|
+
|
|
5914
|
+
# Warn if context field contains a Reddit URL but platform is set to twitter
|
|
5915
|
+
if platform == "twitter" and "reddit.com" in (context or ""):
|
|
5916
|
+
return {
|
|
5917
|
+
"error": "Platform mismatch: context references Reddit but platform is 'twitter'. Set platform='reddit'.",
|
|
5918
|
+
"hint": "When drafting from a Reddit scan target, always set platform='reddit'.",
|
|
5919
|
+
}
|
|
5920
|
+
|
|
5734
5921
|
draft = True # Always draft, never auto-post
|
|
5735
5922
|
entry = save_draft(
|
|
5736
5923
|
post["text"], platform=platform, account=account,
|
|
@@ -5748,18 +5935,26 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
5748
5935
|
_lines = []
|
|
5749
5936
|
|
|
5750
5937
|
if platform == "reddit":
|
|
5751
|
-
_lines.append(
|
|
5938
|
+
_lines.append("WHERE: Reddit")
|
|
5752
5939
|
else:
|
|
5753
|
-
_lines.append(
|
|
5940
|
+
_lines.append("WHERE: X")
|
|
5941
|
+
_where_link = ""
|
|
5942
|
+
if platform == "reddit":
|
|
5943
|
+
_where_link = entry.get("thread_url", "") or reply_to_id
|
|
5944
|
+
elif reply_to_id:
|
|
5945
|
+
_where_link = f"https://x.com/i/status/{reply_to_id}"
|
|
5946
|
+
elif quote_tweet_id:
|
|
5947
|
+
_where_link = f"https://x.com/i/status/{quote_tweet_id}"
|
|
5948
|
+
if _where_link:
|
|
5949
|
+
_lines.append(f"LINK: {_where_link}")
|
|
5754
5950
|
_lines.append("")
|
|
5755
5951
|
|
|
5756
|
-
# WHY should this be posted?
|
|
5757
5952
|
_context = entry.get("context", "")
|
|
5758
5953
|
if _context:
|
|
5759
5954
|
_lines.append(f"WHY: {_context}")
|
|
5760
5955
|
_lines.append("")
|
|
5761
5956
|
|
|
5762
|
-
|
|
5957
|
+
_lines.append("WHAT:")
|
|
5763
5958
|
if platform == "reddit":
|
|
5764
5959
|
_thread_url = entry.get("thread_url", "")
|
|
5765
5960
|
# Fallback: extract reddit URL from context field
|
|
@@ -5770,9 +5965,8 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
5770
5965
|
if _url_match:
|
|
5771
5966
|
_thread_url = _url_match.group(0)
|
|
5772
5967
|
if reply_to_id or _thread_url:
|
|
5773
|
-
|
|
5774
|
-
_lines.append(
|
|
5775
|
-
_lines.append("Open the thread, find the comment to reply to, paste the text below.")
|
|
5968
|
+
_lines.append(f"Platform: REDDIT as u/{_acct}")
|
|
5969
|
+
_lines.append("Owner action: Open the thread and reply using the draft below.")
|
|
5776
5970
|
else:
|
|
5777
5971
|
# New Reddit post — extract title from first line of text
|
|
5778
5972
|
_post_text = post["text"]
|
|
@@ -5783,19 +5977,17 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
5783
5977
|
else:
|
|
5784
5978
|
_reddit_title = _post_text[:100].strip()
|
|
5785
5979
|
_reddit_body = _post_text
|
|
5786
|
-
_lines.append("
|
|
5787
|
-
_lines.append("Navigate to the subreddit
|
|
5788
|
-
_lines.append("")
|
|
5789
|
-
_lines.append("--- TITLE (paste in title field) ---")
|
|
5980
|
+
_lines.append(f"Platform: REDDIT as u/{_acct}")
|
|
5981
|
+
_lines.append("Owner action: Navigate to the subreddit and create a new post.")
|
|
5790
5982
|
_lines.append("")
|
|
5983
|
+
_lines.append("Post Title")
|
|
5984
|
+
_lines.append("Tap and hold inside this block to copy.")
|
|
5791
5985
|
_lines.append(_reddit_title)
|
|
5792
5986
|
_lines.append("")
|
|
5793
|
-
_lines.append("
|
|
5794
|
-
_lines.append("")
|
|
5987
|
+
_lines.append("Post Body")
|
|
5988
|
+
_lines.append("Tap and hold inside this block to copy.")
|
|
5795
5989
|
_lines.append(_reddit_body)
|
|
5796
5990
|
_lines.append("")
|
|
5797
|
-
_lines.append("--- END ---")
|
|
5798
|
-
_lines.append("")
|
|
5799
5991
|
_lines.append(f"Draft ID: {entry['draft_id']}")
|
|
5800
5992
|
if entry.get("tone_warnings"):
|
|
5801
5993
|
_lines.append("")
|
|
@@ -5817,22 +6009,20 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
5817
6009
|
entry["mode"] = "draft"
|
|
5818
6010
|
return _with_next_steps("social_post", entry)
|
|
5819
6011
|
elif reply_to_id:
|
|
5820
|
-
_lines.append(f"
|
|
5821
|
-
_lines.append("Open the link above, click Reply, paste the
|
|
6012
|
+
_lines.append(f"Platform: X as @{_acct}")
|
|
6013
|
+
_lines.append("Owner action: Open the link above, click Reply, paste the draft below.")
|
|
5822
6014
|
elif quote_tweet_id:
|
|
5823
|
-
_lines.append(f"
|
|
5824
|
-
_lines.append("Open the link above, click Repost > Quote, paste the
|
|
6015
|
+
_lines.append(f"Platform: X as @{_acct}")
|
|
6016
|
+
_lines.append("Owner action: Open the link above, click Repost > Quote, paste the draft below.")
|
|
5825
6017
|
else:
|
|
5826
|
-
_lines.append("
|
|
5827
|
-
_lines.append("Open X, compose a new
|
|
6018
|
+
_lines.append(f"Platform: X as @{_acct}")
|
|
6019
|
+
_lines.append("Owner action: Open X, compose a new post, paste the draft below.")
|
|
5828
6020
|
|
|
5829
6021
|
_lines.append("")
|
|
5830
|
-
_lines.append("
|
|
5831
|
-
_lines.append("")
|
|
6022
|
+
_lines.append("Manual Post Text")
|
|
6023
|
+
_lines.append("Tap and hold inside this block to copy.")
|
|
5832
6024
|
_lines.append(post["text"])
|
|
5833
6025
|
_lines.append("")
|
|
5834
|
-
_lines.append("--- END ---")
|
|
5835
|
-
_lines.append("")
|
|
5836
6026
|
_lines.append(f"Draft ID: {entry['draft_id']}")
|
|
5837
6027
|
if entry.get("tone_warnings"):
|
|
5838
6028
|
_lines.append("")
|
|
@@ -5868,7 +6058,6 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
5868
6058
|
return _with_next_steps("social_post", entry)
|
|
5869
6059
|
|
|
5870
6060
|
|
|
5871
|
-
@_ops_pack_tool()
|
|
5872
6061
|
@mcp.tool()
|
|
5873
6062
|
def delimit_social_generate(category: str = "tip") -> Dict[str, Any]:
|
|
5874
6063
|
"""Generate a social media post without posting (Pro).
|
|
@@ -5882,7 +6071,6 @@ def delimit_social_generate(category: str = "tip") -> Dict[str, Any]:
|
|
|
5882
6071
|
return _with_next_steps("social_generate", post)
|
|
5883
6072
|
|
|
5884
6073
|
|
|
5885
|
-
@_internal_tool()
|
|
5886
6074
|
@mcp.tool()
|
|
5887
6075
|
def delimit_social_accounts() -> Dict[str, Any]:
|
|
5888
6076
|
"""List configured social media accounts (Pro).
|
|
@@ -5896,7 +6084,6 @@ def delimit_social_accounts() -> Dict[str, Any]:
|
|
|
5896
6084
|
return _with_next_steps("social_accounts", {"accounts": accounts, "count": len(accounts)})
|
|
5897
6085
|
|
|
5898
6086
|
|
|
5899
|
-
@_internal_tool()
|
|
5900
6087
|
@mcp.tool()
|
|
5901
6088
|
def delimit_social_history(limit: int = 20, platform: str = "",
|
|
5902
6089
|
user: str = "", subreddit: str = "") -> Dict[str, Any]:
|
|
@@ -5918,7 +6105,6 @@ def delimit_social_history(limit: int = 20, platform: str = "",
|
|
|
5918
6105
|
return _with_next_steps("social_history", {"posts": posts})
|
|
5919
6106
|
|
|
5920
6107
|
|
|
5921
|
-
@_ops_pack_tool()
|
|
5922
6108
|
@mcp.tool()
|
|
5923
6109
|
def delimit_social_approve(action: str = "list", draft_id: str = "") -> Dict[str, Any]:
|
|
5924
6110
|
"""Manage social media drafts: list, approve, or reject (Pro).
|
|
@@ -6077,7 +6263,6 @@ def delimit_reddit_scan(sort: str = "hot", limit: int = 10) -> Dict[str, Any]:
|
|
|
6077
6263
|
return _with_next_steps("social_target", result)
|
|
6078
6264
|
|
|
6079
6265
|
|
|
6080
|
-
@_internal_tool()
|
|
6081
6266
|
@mcp.tool()
|
|
6082
6267
|
def delimit_github_scan(
|
|
6083
6268
|
cadence: str = "pulse",
|
|
@@ -6087,12 +6272,12 @@ def delimit_github_scan(
|
|
|
6087
6272
|
|
|
6088
6273
|
Three cadences:
|
|
6089
6274
|
pulse: Own repo health (stars, forks, issues, traffic). Fast, run often.
|
|
6090
|
-
hunter:
|
|
6275
|
+
hunter: Repository signals, engagement threads. Medium, run hourly.
|
|
6091
6276
|
deep: Full ecosystem intel. Slow, run daily.
|
|
6092
6277
|
|
|
6093
6278
|
IMPORTANT -- TOOL CHAINING RULE:
|
|
6094
6279
|
After scanning, the AI MUST process high-score findings:
|
|
6095
|
-
1. Auto-ledger
|
|
6280
|
+
1. Auto-ledger high-score findings via delimit_ledger_add
|
|
6096
6281
|
2. Pain threads with existing_feature relevance via delimit_notify
|
|
6097
6282
|
Never just return findings and stop. Always chain to the next action.
|
|
6098
6283
|
|
|
@@ -6115,7 +6300,6 @@ def delimit_github_scan(
|
|
|
6115
6300
|
# ═══════════════════════════════════════════════════════════════════════
|
|
6116
6301
|
|
|
6117
6302
|
|
|
6118
|
-
@_internal_tool()
|
|
6119
6303
|
@mcp.tool()
|
|
6120
6304
|
def delimit_content_schedule() -> Dict[str, Any]:
|
|
6121
6305
|
"""View the upcoming content schedule: queued tweets, pending videos, recent activity (Pro)."""
|
|
@@ -6124,7 +6308,6 @@ def delimit_content_schedule() -> Dict[str, Any]:
|
|
|
6124
6308
|
return _with_next_steps("content_schedule", get_content_schedule())
|
|
6125
6309
|
|
|
6126
6310
|
|
|
6127
|
-
@_internal_tool()
|
|
6128
6311
|
@mcp.tool()
|
|
6129
6312
|
def delimit_content_publish(content_type: str = "tweet") -> Dict[str, Any]:
|
|
6130
6313
|
"""Manually trigger a content publish: tweet or youtube video (Pro).
|
|
@@ -6143,7 +6326,6 @@ def delimit_content_publish(content_type: str = "tweet") -> Dict[str, Any]:
|
|
|
6143
6326
|
return {"error": f"Unknown content_type: {content_type}", "supported": ["tweet", "youtube"]}
|
|
6144
6327
|
|
|
6145
6328
|
|
|
6146
|
-
@_internal_tool()
|
|
6147
6329
|
@mcp.tool()
|
|
6148
6330
|
def delimit_content_queue(action: str = "status", items: str = "") -> Dict[str, Any]:
|
|
6149
6331
|
"""Manage the tweet and video content queues (Pro).
|
|
@@ -6199,6 +6381,31 @@ def delimit_daemon_run(iterations: int = 1, dry_run: bool = True) -> Dict[str, A
|
|
|
6199
6381
|
max_iterations=iterations, interval_seconds=5, dry_run=dry_run,
|
|
6200
6382
|
))
|
|
6201
6383
|
|
|
6384
|
+
@mcp.tool()
|
|
6385
|
+
def delimit_build_loop(action: str = "run", session_id: str = "") -> Dict[str, Any]:
|
|
6386
|
+
"""Execute the governed continuous build loop (LED-239).
|
|
6387
|
+
|
|
6388
|
+
Requirements:
|
|
6389
|
+
- root ledger in /root/.delimit is authoritative
|
|
6390
|
+
- select only build-safe open items
|
|
6391
|
+
- resolve venture + repo before dispatch
|
|
6392
|
+
- use Delimit swarm/governance as control plane
|
|
6393
|
+
- enforce max-iteration, max-error, and max-cost safeguards
|
|
6394
|
+
|
|
6395
|
+
Args:
|
|
6396
|
+
action: 'init' to start a session, 'run' to execute one iteration.
|
|
6397
|
+
session_id: Optional session ID to continue.
|
|
6398
|
+
"""
|
|
6399
|
+
from ai.loop_engine import create_governed_session, run_governed_iteration
|
|
6400
|
+
|
|
6401
|
+
if action == "init":
|
|
6402
|
+
return _with_next_steps("build_loop", create_governed_session())
|
|
6403
|
+
else:
|
|
6404
|
+
if not session_id:
|
|
6405
|
+
# Try to pick up existing or create new
|
|
6406
|
+
session_id = create_governed_session()["session_id"]
|
|
6407
|
+
return _with_next_steps("build_loop", run_governed_iteration(session_id))
|
|
6408
|
+
|
|
6202
6409
|
|
|
6203
6410
|
@mcp.tool()
|
|
6204
6411
|
def delimit_daemon_classify(item_id: str = "") -> Dict[str, Any]:
|
|
@@ -6238,7 +6445,6 @@ def delimit_daemon_classify(item_id: str = "") -> Dict[str, Any]:
|
|
|
6238
6445
|
# ═══════════════════════════════════════════════════════════════════════
|
|
6239
6446
|
|
|
6240
6447
|
|
|
6241
|
-
@_internal_tool()
|
|
6242
6448
|
@mcp.tool()
|
|
6243
6449
|
def delimit_inbox_daemon(action: str = "status") -> Dict[str, Any]:
|
|
6244
6450
|
"""Control the inbox polling daemon for email governance (Pro).
|
|
@@ -6261,6 +6467,26 @@ def delimit_inbox_daemon(action: str = "status") -> Dict[str, Any]:
|
|
|
6261
6467
|
return _with_next_steps("inbox_daemon", get_daemon_status())
|
|
6262
6468
|
|
|
6263
6469
|
|
|
6470
|
+
@mcp.tool()
|
|
6471
|
+
def delimit_social_daemon(action: str = "status") -> Dict[str, Any]:
|
|
6472
|
+
"""Control the social sensing daemon for Delimit (Pro).
|
|
6473
|
+
|
|
6474
|
+
Runs social discovery scans every 15 minutes, deduplicates findings,
|
|
6475
|
+
and emits HTML draft emails. Also monitors Reddit replies.
|
|
6476
|
+
|
|
6477
|
+
Args:
|
|
6478
|
+
action: 'start' (begin scanning), 'stop' (halt scanning),
|
|
6479
|
+
'status' (show daemon state, last scan, targets found).
|
|
6480
|
+
"""
|
|
6481
|
+
from ai.social_daemon import start_daemon, stop_daemon, get_daemon_status
|
|
6482
|
+
|
|
6483
|
+
if action == "start":
|
|
6484
|
+
return _with_next_steps("social_daemon", start_daemon())
|
|
6485
|
+
elif action == "stop":
|
|
6486
|
+
return _with_next_steps("social_daemon", stop_daemon())
|
|
6487
|
+
else:
|
|
6488
|
+
return _with_next_steps("social_daemon", get_daemon_status())
|
|
6489
|
+
|
|
6264
6490
|
# ═══════════════════════════════════════════════════════════════════════
|
|
6265
6491
|
# LED-187: Shareable Governance Config — export / import
|
|
6266
6492
|
# ═══════════════════════════════════════════════════════════════════════
|
|
@@ -6389,7 +6615,6 @@ def delimit_config_import(
|
|
|
6389
6615
|
# ═══════════════════════════════════════════════════════════════════════
|
|
6390
6616
|
|
|
6391
6617
|
|
|
6392
|
-
@_ops_pack_tool()
|
|
6393
6618
|
@mcp.tool()
|
|
6394
6619
|
def delimit_screen_record(mode: str = "browser", url: str = "", name: str = "recording",
|
|
6395
6620
|
duration: int = 30, script: str = "") -> Dict[str, Any]:
|
|
@@ -6434,7 +6659,6 @@ def delimit_screen_record(mode: str = "browser", url: str = "", name: str = "rec
|
|
|
6434
6659
|
return _with_next_steps("screen_record", result)
|
|
6435
6660
|
|
|
6436
6661
|
|
|
6437
|
-
@_ops_pack_tool()
|
|
6438
6662
|
@mcp.tool()
|
|
6439
6663
|
def delimit_screenshot(url: str, name: str = "screenshot") -> Dict[str, Any]:
|
|
6440
6664
|
"""Take a screenshot of a URL using headless Chromium (Pro).
|
|
@@ -6488,7 +6712,6 @@ def delimit_changelog(old_spec: str = "", new_spec: str = "", format: str = "mar
|
|
|
6488
6712
|
))
|
|
6489
6713
|
|
|
6490
6714
|
|
|
6491
|
-
@_ops_pack_tool()
|
|
6492
6715
|
@mcp.tool()
|
|
6493
6716
|
def delimit_notify(channel: str = "webhook", message: str = "",
|
|
6494
6717
|
webhook_url: str = "", subject: str = "",
|
|
@@ -6519,9 +6742,9 @@ def delimit_notify(channel: str = "webhook", message: str = "",
|
|
|
6519
6742
|
subject: Subject line (email only). Use [ACTION], [INFO], [ALERT] prefix.
|
|
6520
6743
|
event_type: Event category for filtering.
|
|
6521
6744
|
to: Recipient email address (email only). Overrides default DELIMIT_SMTP_TO.
|
|
6522
|
-
Send to any address — leave empty for default
|
|
6745
|
+
Send to any address — leave empty for default.
|
|
6523
6746
|
from_account: Sender account key from ~/.delimit/secrets/smtp-all.json
|
|
6524
|
-
(e.g. '
|
|
6747
|
+
(e.g. 'notifications@example.com'). Email only.
|
|
6525
6748
|
"""
|
|
6526
6749
|
from ai.notify import send_notification
|
|
6527
6750
|
return _with_next_steps("notify", _safe_call(
|
|
@@ -6620,7 +6843,6 @@ def delimit_notify_routing(
|
|
|
6620
6843
|
})
|
|
6621
6844
|
|
|
6622
6845
|
|
|
6623
|
-
@_internal_tool()
|
|
6624
6846
|
@mcp.tool()
|
|
6625
6847
|
def delimit_notify_inbox(action: str = "status", limit: int = 10,
|
|
6626
6848
|
process: bool = True) -> Dict[str, Any]:
|
|
@@ -6912,6 +7134,27 @@ def delimit_next_task(venture: str = "", max_risk: str = "", session_id: str = "
|
|
|
6912
7134
|
return _with_next_steps("next_task", result)
|
|
6913
7135
|
|
|
6914
7136
|
|
|
7137
|
+
@mcp.tool()
|
|
7138
|
+
def delimit_ledger_propose(venture: str = "", focus: str = "",
|
|
7139
|
+
max_items: int = 5) -> Dict[str, Any]:
|
|
7140
|
+
"""Propose new ledger items based on signals, completed work, and gaps.
|
|
7141
|
+
|
|
7142
|
+
Analyzes repo state, sensing signals, and completed work to suggest
|
|
7143
|
+
3-5 new items with rationale. Run at end of build loops or when
|
|
7144
|
+
the queue is empty to keep momentum.
|
|
7145
|
+
|
|
7146
|
+
Works across all AI models via MCP.
|
|
7147
|
+
|
|
7148
|
+
Args:
|
|
7149
|
+
venture: Focus on a specific venture (auto-detects if empty).
|
|
7150
|
+
focus: Optional area filter — "outreach", "engineering", "security", etc.
|
|
7151
|
+
max_items: Maximum proposals to generate (default 5).
|
|
7152
|
+
"""
|
|
7153
|
+
from ai.ledger_propose import propose_items
|
|
7154
|
+
result = _safe_call(propose_items, venture=venture, focus=focus, max_items=max_items)
|
|
7155
|
+
return _with_next_steps("ledger_propose", result)
|
|
7156
|
+
|
|
7157
|
+
|
|
6915
7158
|
@mcp.tool()
|
|
6916
7159
|
def delimit_task_complete(task_id: str, result: str = "", cost_incurred: float = 0.0,
|
|
6917
7160
|
error: str = "", session_id: str = "", venture: str = "") -> Dict[str, Any]:
|
|
@@ -7242,3 +7485,22 @@ def main():
|
|
|
7242
7485
|
"""Entry point for `delimit-mcp` console script."""
|
|
7243
7486
|
import asyncio
|
|
7244
7487
|
asyncio.run(run_mcp_server(mcp))
|
|
7488
|
+
|
|
7489
|
+
@mcp.tool()
|
|
7490
|
+
def delimit_reddit_fetch_thread(thread_id: str) -> Dict[str, Any]:
|
|
7491
|
+
"""Surgically fetch a single Reddit thread by ID (e.g. 'OSKJVH7f35')."""
|
|
7492
|
+
from ai.reddit_scanner import fetch_thread, score_and_classify
|
|
7493
|
+
|
|
7494
|
+
# Strip URL parts if user passed a full link
|
|
7495
|
+
if "comments/" in thread_id:
|
|
7496
|
+
parts = thread_id.split("comments/")[1].split("/")
|
|
7497
|
+
thread_id = parts[0]
|
|
7498
|
+
elif thread_id.startswith("http"):
|
|
7499
|
+
thread_id = thread_id.split("/")[-1].split("?")[0]
|
|
7500
|
+
|
|
7501
|
+
thread = fetch_thread(thread_id)
|
|
7502
|
+
if not thread:
|
|
7503
|
+
return {"error": f"Could not find thread with ID {thread_id}"}
|
|
7504
|
+
|
|
7505
|
+
scored = score_and_classify([thread])
|
|
7506
|
+
return {"thread": scored[0] if scored else thread}
|