delimit-cli 4.1.0 → 4.1.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/CHANGELOG.md +578 -0
- package/bin/delimit-cli.js +91 -0
- package/gateway/ai/backends/gateway_core.py +1012 -0
- package/gateway/ai/github_scanner.py +1 -1
- package/gateway/ai/screen_record.py +1 -1
- package/gateway/ai/server.py +594 -138
- package/gateway/ai/swarm.py +1 -1
- package/gateway/core/diff_engine_v2.py +5 -0
- package/gateway/core/spec_health.py +624 -0
- package/package.json +1 -1
package/gateway/ai/server.py
CHANGED
|
@@ -60,13 +60,13 @@ from fastmcp import FastMCP
|
|
|
60
60
|
logger = logging.getLogger("delimit.ai")
|
|
61
61
|
|
|
62
62
|
# ═══════════════════════════════════════════════════════════════════════
|
|
63
|
-
# STR-046: Agent Identity
|
|
63
|
+
# STR-046: Agent Identity - session tracking for every tool call
|
|
64
64
|
# ═══════════════════════════════════════════════════════════════════════
|
|
65
65
|
|
|
66
66
|
_current_session_id = os.environ.get("DELIMIT_SESSION_ID", "")
|
|
67
67
|
|
|
68
68
|
# ═══════════════════════════════════════════════════════════════════════
|
|
69
|
-
# STR-053: Distributed Tracing
|
|
69
|
+
# STR-053: Distributed Tracing - trace ID + span counter for every call
|
|
70
70
|
# ═══════════════════════════════════════════════════════════════════════
|
|
71
71
|
|
|
72
72
|
_trace_id = os.environ.get("DELIMIT_TRACE_ID", str(uuid.uuid4())[:8])
|
|
@@ -257,11 +257,11 @@ 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
|
|
260
|
+
# Social/outreach - drafts are fine, but posting/approving requires gate
|
|
261
261
|
'social_approve', 'content_publish',
|
|
262
|
-
# Agent dispatch
|
|
262
|
+
# Agent dispatch - spawns autonomous work, must be gated
|
|
263
263
|
'agent_dispatch', 'daemon_run',
|
|
264
|
-
# Deliberation
|
|
264
|
+
# Deliberation - burns model quota
|
|
265
265
|
'deliberate',
|
|
266
266
|
}
|
|
267
267
|
|
|
@@ -269,7 +269,7 @@ CRITICAL_RISK_TOOLS = {
|
|
|
269
269
|
'deploy_rollback', 'data_migrate',
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
# Rate limits per tool per hour
|
|
272
|
+
# Rate limits per tool per hour - prevents runaway loops
|
|
273
273
|
_TOOL_RATE_LIMITS = {
|
|
274
274
|
'social_post': 10,
|
|
275
275
|
'social_target': 20,
|
|
@@ -320,7 +320,7 @@ def _classify_risk(tool_name: str) -> str:
|
|
|
320
320
|
|
|
321
321
|
|
|
322
322
|
# ═══════════════════════════════════════════════════════════════════════
|
|
323
|
-
# STR-052: Policy Kernel
|
|
323
|
+
# STR-052: Policy Kernel - Inline Enforcement
|
|
324
324
|
# Checks policy BEFORE/AFTER tool execution to block high-risk actions.
|
|
325
325
|
# ═══════════════════════════════════════════════════════════════════════
|
|
326
326
|
|
|
@@ -372,7 +372,7 @@ def _check_policy_gate(tool_name: str, kwargs: dict) -> Optional[Dict]:
|
|
|
372
372
|
"action": "Switch to guarded mode or request approval",
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
# LED-173: Deploy gating
|
|
375
|
+
# LED-173: Deploy gating - block deploys when unresolved critical findings exist
|
|
376
376
|
DEPLOY_TOOLS = {"deploy_publish", "deploy_npm", "deploy_site", "deploy_build"}
|
|
377
377
|
clean = tool_name.replace("delimit_", "")
|
|
378
378
|
if clean in DEPLOY_TOOLS and mode != "advisory":
|
|
@@ -493,7 +493,7 @@ def _emit_policy_event(tool_name: str, status: str, reason: str) -> None:
|
|
|
493
493
|
|
|
494
494
|
mcp = FastMCP("delimit")
|
|
495
495
|
mcp.description = (
|
|
496
|
-
"Delimit
|
|
496
|
+
"Delimit - One workspace for every AI coding assistant. "
|
|
497
497
|
"On session start, call delimit_ledger_context to check for open tasks. "
|
|
498
498
|
"Use delimit_scan on new projects. Track all work via the ledger."
|
|
499
499
|
)
|
|
@@ -542,12 +542,12 @@ def _experimental_tool():
|
|
|
542
542
|
return _tier_tool("experimental")
|
|
543
543
|
|
|
544
544
|
|
|
545
|
-
# Pro tools
|
|
545
|
+
# Pro tools - single source of truth is license_core.py
|
|
546
546
|
# Import at module level; fallback to license.py shim if core unavailable
|
|
547
547
|
from ai.license import PRO_TOOLS
|
|
548
548
|
from ai.rate_limiter import limiter, create_cost_controls_response
|
|
549
549
|
|
|
550
|
-
# Free tools
|
|
550
|
+
# Free tools - everything NOT in PRO_TOOLS
|
|
551
551
|
# security_audit, security_scan, test_generate, test_smoke, activate, license_status
|
|
552
552
|
|
|
553
553
|
|
|
@@ -644,7 +644,7 @@ def _count_critical_findings(audit_result: Dict[str, Any]) -> int:
|
|
|
644
644
|
|
|
645
645
|
|
|
646
646
|
# ═══════════════════════════════════════════════════════════════════════
|
|
647
|
-
# CONSENSUS 096: Tool Cohesion
|
|
647
|
+
# CONSENSUS 096: Tool Cohesion - next_steps in every response
|
|
648
648
|
# ═══════════════════════════════════════════════════════════════════════
|
|
649
649
|
|
|
650
650
|
NEXT_STEPS_REGISTRY: Dict[str, List[Dict[str, Any]]] = {
|
|
@@ -885,6 +885,9 @@ NEXT_STEPS_REGISTRY: Dict[str, List[Dict[str, Any]]] = {
|
|
|
885
885
|
],
|
|
886
886
|
# --- Sensing ---
|
|
887
887
|
"sensor_github_issue": [],
|
|
888
|
+
"sensor_github_migrations": [
|
|
889
|
+
{"tool": "delimit_ledger_add", "reason": "Add high-value migration signals to the strategy ledger", "suggested_args": {}, "is_premium": False},
|
|
890
|
+
],
|
|
888
891
|
# --- Context Filesystem (STR-048) ---
|
|
889
892
|
"context_init": [
|
|
890
893
|
{"tool": "delimit_context_write", "reason": "Write an artifact to the new context", "suggested_args": {}, "is_premium": False},
|
|
@@ -1205,13 +1208,13 @@ def _detect_environment() -> Dict[str, Any]:
|
|
|
1205
1208
|
_inbox_daemon_autostarted = False
|
|
1206
1209
|
_toolcard_cache_autoregistered = False
|
|
1207
1210
|
|
|
1208
|
-
# MCP response size cap
|
|
1211
|
+
# MCP response size cap - prevents Node.js heap OOM on all clients (Gemini CLI, Cursor, etc.)
|
|
1209
1212
|
# FastMCP serializes responses to JSON over stdio; large payloads crash Node's default 1.5GB heap.
|
|
1210
1213
|
# Cap is set high enough that all normal tool responses (deliberation, audit, ledger) pass through
|
|
1211
1214
|
# untouched. Only pathological cases (e.g. 910-item scan dumps) get trimmed.
|
|
1212
1215
|
_MCP_RESPONSE_SIZE_LIMIT = 200_000 # 200KB hard ceiling
|
|
1213
1216
|
|
|
1214
|
-
# Fields within list items that are safe to truncate
|
|
1217
|
+
# Fields within list items that are safe to truncate - display text, not structured data
|
|
1215
1218
|
_ITEM_TEXT_FIELDS = {"content_snippet", "body", "text", "rationale", "full_text", "description", "summary"}
|
|
1216
1219
|
_ITEM_TEXT_MAX = 300 # chars per field within a list item
|
|
1217
1220
|
|
|
@@ -1221,14 +1224,14 @@ def _cap_response(result: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1221
1224
|
Strategy (least destructive first):
|
|
1222
1225
|
1. Trim known text-only fields within list items (content_snippet, body, etc.)
|
|
1223
1226
|
2. If still over limit, truncate lists to first 20 items
|
|
1224
|
-
3. If still over limit, add a note
|
|
1227
|
+
3. If still over limit, add a note - structural data is never silently dropped
|
|
1225
1228
|
"""
|
|
1226
1229
|
import json as _json, copy as _copy
|
|
1227
1230
|
if len(_json.dumps(result)) <= _MCP_RESPONSE_SIZE_LIMIT:
|
|
1228
1231
|
return result
|
|
1229
1232
|
r = _copy.deepcopy(result)
|
|
1230
1233
|
|
|
1231
|
-
# Pass 1: trim display-text fields inside list items (safe
|
|
1234
|
+
# Pass 1: trim display-text fields inside list items (safe - these are human-readable snippets)
|
|
1232
1235
|
for k, v in r.items():
|
|
1233
1236
|
if isinstance(v, list):
|
|
1234
1237
|
for item in v:
|
|
@@ -1248,7 +1251,7 @@ def _cap_response(result: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1248
1251
|
if len(_json.dumps(r)) <= _MCP_RESPONSE_SIZE_LIMIT:
|
|
1249
1252
|
return r
|
|
1250
1253
|
|
|
1251
|
-
# Pass 3: last resort
|
|
1254
|
+
# Pass 3: last resort - note that response is large but return it anyway
|
|
1252
1255
|
# Better to let the client decide than silently drop structured data
|
|
1253
1256
|
r["_size_warning"] = f"Response exceeds {_MCP_RESPONSE_SIZE_LIMIT // 1000}KB. Use limit= or action='list' to reduce payload."
|
|
1254
1257
|
return r
|
|
@@ -1267,7 +1270,7 @@ def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1267
1270
|
6. Auto-create ledger items for failures/warnings
|
|
1268
1271
|
7. Route back to delimit_ledger_context (the loop continues)
|
|
1269
1272
|
"""
|
|
1270
|
-
# Auto-start inbox daemon on first tool call
|
|
1273
|
+
# Auto-start inbox daemon on first tool call - works for ALL models
|
|
1271
1274
|
global _inbox_daemon_autostarted
|
|
1272
1275
|
if not _inbox_daemon_autostarted:
|
|
1273
1276
|
_inbox_daemon_autostarted = True
|
|
@@ -1303,7 +1306,7 @@ def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1303
1306
|
except Exception:
|
|
1304
1307
|
pass
|
|
1305
1308
|
|
|
1306
|
-
# Voice doctrine check
|
|
1309
|
+
# Voice doctrine check - flag hype words in outgoing text
|
|
1307
1310
|
if isinstance(result, dict):
|
|
1308
1311
|
_text_fields = [result.get("text", ""), result.get("message", ""),
|
|
1309
1312
|
result.get("explanation", ""), result.get("changelog", ""),
|
|
@@ -1316,7 +1319,7 @@ def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1316
1319
|
f"Rewrite with concrete mechanisms, not vague benefits."
|
|
1317
1320
|
)
|
|
1318
1321
|
|
|
1319
|
-
# Rate limit check
|
|
1322
|
+
# Rate limit check - prevents runaway loops from any model
|
|
1320
1323
|
rate_gate = _check_rate_limit(tool_name)
|
|
1321
1324
|
if rate_gate:
|
|
1322
1325
|
_emit_event(tool_name, rate_gate)
|
|
@@ -1338,7 +1341,7 @@ def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1338
1341
|
if injection:
|
|
1339
1342
|
result["_security_warning"] = injection
|
|
1340
1343
|
|
|
1341
|
-
# Pro license gate
|
|
1344
|
+
# Pro license gate - blocks execution for premium tools
|
|
1342
1345
|
full_name = f"delimit_{tool_name}" if not tool_name.startswith("delimit_") else tool_name
|
|
1343
1346
|
gate = _check_pro(full_name)
|
|
1344
1347
|
if gate:
|
|
@@ -1356,25 +1359,41 @@ def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1356
1359
|
|
|
1357
1360
|
|
|
1358
1361
|
# ═══════════════════════════════════════════════════════════════════════
|
|
1359
|
-
# TIER 1: CORE
|
|
1362
|
+
# TIER 1: CORE - API Lint Engine
|
|
1360
1363
|
# ═══════════════════════════════════════════════════════════════════════
|
|
1361
1364
|
|
|
1362
1365
|
|
|
1363
1366
|
@mcp.tool()
|
|
1364
|
-
def delimit_lint(old_spec: str, new_spec: str, policy_file: Optional[str] = None) -> Dict[str, Any]:
|
|
1367
|
+
def delimit_lint(old_spec: str, new_spec: str, policy_file: Optional[str] = None, dry_run: bool = False) -> Dict[str, Any]:
|
|
1365
1368
|
"""Lint two OpenAPI specs for breaking changes and policy violations.
|
|
1366
1369
|
Primary CI integration point. Combines diff + policy into pass/fail.
|
|
1367
1370
|
Auto-chains: semver classification, governance evaluation on breaking changes.
|
|
1368
1371
|
|
|
1372
|
+
When dry_run=True, returns violations and semver classification without
|
|
1373
|
+
recording evidence, triggering notifications, or enforcing governance.
|
|
1374
|
+
Useful for CI preview comments ("what would block") without side effects.
|
|
1375
|
+
|
|
1369
1376
|
Args:
|
|
1370
1377
|
old_spec: Path to the old (baseline) OpenAPI spec file.
|
|
1371
1378
|
new_spec: Path to the new (proposed) OpenAPI spec file.
|
|
1372
1379
|
policy_file: Optional path to a .delimit/policies.yml file.
|
|
1380
|
+
dry_run: If True, return violations without side effects (no evidence, no chains).
|
|
1373
1381
|
"""
|
|
1374
1382
|
from backends.gateway_core import run_lint, run_semver
|
|
1375
1383
|
|
|
1376
1384
|
# Step 1: Core lint
|
|
1377
1385
|
lint_result = _safe_call(run_lint, old_spec=old_spec, new_spec=new_spec, policy_file=policy_file)
|
|
1386
|
+
|
|
1387
|
+
# Dry-run mode: return raw lint + semver, skip all chains and governance
|
|
1388
|
+
if dry_run:
|
|
1389
|
+
lint_result["dry_run"] = True
|
|
1390
|
+
lint_result["simulated"] = True
|
|
1391
|
+
# Still classify semver (informational, no side effects)
|
|
1392
|
+
semver_result = _safe_call(run_semver, old_spec=old_spec, new_spec=new_spec)
|
|
1393
|
+
if not semver_result.get("error"):
|
|
1394
|
+
lint_result["semver"] = semver_result
|
|
1395
|
+
return lint_result
|
|
1396
|
+
|
|
1378
1397
|
chain: Dict[str, Any] = {"id": "lint_chain", "steps": []}
|
|
1379
1398
|
|
|
1380
1399
|
if lint_result.get("error"):
|
|
@@ -1475,13 +1494,93 @@ def delimit_diff(old_spec: str, new_spec: str) -> Dict[str, Any]:
|
|
|
1475
1494
|
|
|
1476
1495
|
|
|
1477
1496
|
@mcp.tool()
|
|
1478
|
-
def
|
|
1479
|
-
|
|
1497
|
+
def delimit_diff_report(
|
|
1498
|
+
old_spec: str,
|
|
1499
|
+
new_spec: str,
|
|
1500
|
+
output_format: str = "html",
|
|
1501
|
+
output_file: Optional[str] = None,
|
|
1502
|
+
policy_file: Optional[str] = None,
|
|
1503
|
+
) -> Dict[str, Any]:
|
|
1504
|
+
"""Generate a shareable API diff report with full analysis.
|
|
1505
|
+
|
|
1506
|
+
Runs the complete pipeline (diff, policy evaluation, semver
|
|
1507
|
+
classification, spec health scoring, migration guide) and produces
|
|
1508
|
+
a self-contained HTML report or structured JSON. The HTML has inline
|
|
1509
|
+
CSS with no external dependencies -- open it in any browser.
|
|
1510
|
+
|
|
1511
|
+
Use this when teams need a shareable artifact for API review, PR
|
|
1512
|
+
comments, or compliance records.
|
|
1513
|
+
|
|
1514
|
+
Args:
|
|
1515
|
+
old_spec: Path to the old (baseline) OpenAPI spec file.
|
|
1516
|
+
new_spec: Path to the new (proposed) OpenAPI spec file.
|
|
1517
|
+
output_format: "html" for a standalone HTML report, "json" for structured data.
|
|
1518
|
+
output_file: Optional file path to write the report to disk.
|
|
1519
|
+
policy_file: Optional path to a .delimit/policies.yml file.
|
|
1520
|
+
"""
|
|
1521
|
+
from backends.gateway_core import run_diff_report
|
|
1522
|
+
return _with_next_steps(
|
|
1523
|
+
"diff_report",
|
|
1524
|
+
_safe_call(
|
|
1525
|
+
run_diff_report,
|
|
1526
|
+
old_spec=old_spec,
|
|
1527
|
+
new_spec=new_spec,
|
|
1528
|
+
fmt=output_format,
|
|
1529
|
+
output_file=output_file,
|
|
1530
|
+
policy_file=policy_file,
|
|
1531
|
+
),
|
|
1532
|
+
)
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
@mcp.tool()
|
|
1536
|
+
def delimit_spec_health(spec: str) -> Dict[str, Any]:
|
|
1537
|
+
"""Score an OpenAPI spec on quality dimensions. Instant health grade.
|
|
1538
|
+
|
|
1539
|
+
Evaluates completeness, security, consistency, documentation, and best
|
|
1540
|
+
practices. Returns an overall score (0-100), letter grade (A-F),
|
|
1541
|
+
per-dimension breakdowns, and specific recommendations for improvement.
|
|
1542
|
+
|
|
1543
|
+
Use this for quick spec quality checks during onboarding or code review.
|
|
1544
|
+
Works on any valid OpenAPI 3.x or Swagger 2.0 spec.
|
|
1545
|
+
|
|
1546
|
+
Args:
|
|
1547
|
+
spec: Path to an OpenAPI spec file (YAML or JSON).
|
|
1548
|
+
"""
|
|
1549
|
+
from backends.gateway_core import run_spec_health
|
|
1550
|
+
return _with_next_steps("spec_health", _safe_call(run_spec_health, spec_path=spec))
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
@mcp.tool()
|
|
1554
|
+
def delimit_policy(
|
|
1555
|
+
spec_files: List[str],
|
|
1556
|
+
policy_file: Optional[str] = None,
|
|
1557
|
+
action: str = "inspect",
|
|
1558
|
+
old_spec: Optional[str] = None,
|
|
1559
|
+
new_spec: Optional[str] = None,
|
|
1560
|
+
) -> Dict[str, Any]:
|
|
1561
|
+
"""Inspect, validate, or simulate governance policy configuration.
|
|
1562
|
+
|
|
1563
|
+
Actions:
|
|
1564
|
+
- "inspect" (default): Show loaded rules and template.
|
|
1565
|
+
- "simulate": Dry-run lint+policy across all presets (strict, default,
|
|
1566
|
+
relaxed) plus optional custom policy. Shows what WOULD pass/fail
|
|
1567
|
+
without enforcing anything. Requires old_spec and new_spec.
|
|
1480
1568
|
|
|
1481
1569
|
Args:
|
|
1482
1570
|
spec_files: List of spec file paths.
|
|
1483
1571
|
policy_file: Optional custom policy file path.
|
|
1484
|
-
|
|
1572
|
+
action: "inspect" or "simulate".
|
|
1573
|
+
old_spec: Path to baseline spec (required for simulate).
|
|
1574
|
+
new_spec: Path to proposed spec (required for simulate).
|
|
1575
|
+
"""
|
|
1576
|
+
if action == "simulate":
|
|
1577
|
+
if not old_spec or not new_spec:
|
|
1578
|
+
return {"error": "missing_specs", "message": "simulate action requires old_spec and new_spec parameters."}
|
|
1579
|
+
from backends.gateway_core import simulate_policy
|
|
1580
|
+
result = _safe_call(simulate_policy, old_spec=old_spec, new_spec=new_spec, policy_file=policy_file)
|
|
1581
|
+
# Simulation results bypass governance chains -- nothing is enforced
|
|
1582
|
+
return result
|
|
1583
|
+
|
|
1485
1584
|
from backends.gateway_core import run_policy
|
|
1486
1585
|
return _with_next_steps("policy", _safe_call(run_policy, spec_files=spec_files, policy_file=policy_file))
|
|
1487
1586
|
|
|
@@ -1583,7 +1682,7 @@ def delimit_init(
|
|
|
1583
1682
|
|
|
1584
1683
|
Args:
|
|
1585
1684
|
project_path: Project root directory.
|
|
1586
|
-
preset: Policy preset
|
|
1685
|
+
preset: Policy preset - strict, default, or relaxed.
|
|
1587
1686
|
"""
|
|
1588
1687
|
VALID_PRESETS = ("strict", "default", "relaxed")
|
|
1589
1688
|
if preset not in VALID_PRESETS:
|
|
@@ -1673,7 +1772,7 @@ def delimit_init(
|
|
|
1673
1772
|
})
|
|
1674
1773
|
|
|
1675
1774
|
# ═══════════════════════════════════════════════════════════════════════
|
|
1676
|
-
# TIER 2: PLATFORM
|
|
1775
|
+
# TIER 2: PLATFORM - OS, Governance, Memory, Vault
|
|
1677
1776
|
# ═══════════════════════════════════════════════════════════════════════
|
|
1678
1777
|
|
|
1679
1778
|
|
|
@@ -1969,7 +2068,7 @@ def delimit_vault_snapshot() -> Dict[str, Any]:
|
|
|
1969
2068
|
|
|
1970
2069
|
|
|
1971
2070
|
# ═══════════════════════════════════════════════════════════════════════
|
|
1972
|
-
# TIER 3: EXTENDED
|
|
2071
|
+
# TIER 3: EXTENDED - Deploy, Intel, Generate, Repo, Security, Evidence
|
|
1973
2072
|
# ═══════════════════════════════════════════════════════════════════════
|
|
1974
2073
|
|
|
1975
2074
|
|
|
@@ -2413,7 +2512,7 @@ def delimit_security_ingest(
|
|
|
2413
2512
|
or CodeQL. Normalizes findings into a canonical schema, tracks in the
|
|
2414
2513
|
ledger, and enables deploy gating on unresolved criticals.
|
|
2415
2514
|
|
|
2416
|
-
This is the orchestrator model
|
|
2515
|
+
This is the orchestrator model - Delimit doesn't run the scanner,
|
|
2417
2516
|
it adds intelligence on top of results you already have.
|
|
2418
2517
|
|
|
2419
2518
|
Args:
|
|
@@ -2436,7 +2535,7 @@ def delimit_security_ingest(
|
|
|
2436
2535
|
"error": f"Unsupported tool '{tool}'. Supported: {', '.join(SUPPORTED_TOOLS)}",
|
|
2437
2536
|
})
|
|
2438
2537
|
|
|
2439
|
-
# Parse results
|
|
2538
|
+
# Parse results - accept JSON string or file path
|
|
2440
2539
|
raw_data = None
|
|
2441
2540
|
if results.strip().startswith(("{", "[")):
|
|
2442
2541
|
try:
|
|
@@ -2657,7 +2756,7 @@ def delimit_security_deliberate(
|
|
|
2657
2756
|
Args:
|
|
2658
2757
|
findings: JSON string of findings to triage, or empty to pull from ledger.
|
|
2659
2758
|
repo: Repository context for the triage.
|
|
2660
|
-
focus: Which findings to triage
|
|
2759
|
+
focus: Which findings to triage - "critical", "high", "all". Default: critical.
|
|
2661
2760
|
"""
|
|
2662
2761
|
from ai.license import require_premium
|
|
2663
2762
|
gate = require_premium("security_deliberate")
|
|
@@ -2675,7 +2774,7 @@ def delimit_security_deliberate(
|
|
|
2675
2774
|
except json.JSONDecodeError:
|
|
2676
2775
|
return _with_next_steps("security_deliberate", {"error": "Invalid JSON in findings"})
|
|
2677
2776
|
else:
|
|
2678
|
-
# Pull from ledger
|
|
2777
|
+
# Pull from ledger - find open security items
|
|
2679
2778
|
try:
|
|
2680
2779
|
from ai.ledger_manager import list_items
|
|
2681
2780
|
ledger_data = list_items(status="open")
|
|
@@ -2769,7 +2868,7 @@ def delimit_security_deliberate(
|
|
|
2769
2868
|
def delimit_siem(action: str = "status", integration: str = "",
|
|
2770
2869
|
settings: str = "", enabled: str = "",
|
|
2771
2870
|
event: str = "") -> Dict[str, Any]:
|
|
2772
|
-
"""Manage SIEM streaming
|
|
2871
|
+
"""Manage SIEM streaming - forward audit events to Splunk, Datadog, EventBridge, or webhooks.
|
|
2773
2872
|
|
|
2774
2873
|
Actions:
|
|
2775
2874
|
status: Show all SIEM integrations and delivery stats
|
|
@@ -2915,7 +3014,7 @@ def delimit_evidence_verify(bundle_id: Optional[str] = None, bundle_path: Option
|
|
|
2915
3014
|
|
|
2916
3015
|
|
|
2917
3016
|
# ═══════════════════════════════════════════════════════════════════════
|
|
2918
|
-
# TIER 4: OPS / UI
|
|
3017
|
+
# TIER 4: OPS / UI - Governance Primitives + UI Tooling
|
|
2919
3018
|
# ═══════════════════════════════════════════════════════════════════════
|
|
2920
3019
|
|
|
2921
3020
|
|
|
@@ -3455,7 +3554,7 @@ def delimit_story_accessibility(project_path: str, standards: str = "WCAG2AA") -
|
|
|
3455
3554
|
return _with_next_steps("story_accessibility", _safe_call(story_accessibility_test, project_path=project_path, standards=standards))
|
|
3456
3555
|
|
|
3457
3556
|
|
|
3458
|
-
# ─── TestSmith (Testing
|
|
3557
|
+
# ─── TestSmith (Testing - Real implementations) ──────────────────────
|
|
3459
3558
|
|
|
3460
3559
|
@mcp.tool()
|
|
3461
3560
|
def delimit_test_generate(project_path: str, source_files: Optional[List[str]] = None, framework: str = "jest") -> Dict[str, Any]:
|
|
@@ -3667,6 +3766,61 @@ async def delimit_sensor_github_issue(
|
|
|
3667
3766
|
return _with_next_steps("sensor_github_issue", {"error": str(e), "has_new_activity": False})
|
|
3668
3767
|
|
|
3669
3768
|
|
|
3769
|
+
# --- STR-062: Migration Pattern Detector ---
|
|
3770
|
+
|
|
3771
|
+
@mcp.tool()
|
|
3772
|
+
def delimit_sensor_github_migrations(
|
|
3773
|
+
repos: List[str],
|
|
3774
|
+
limit: int = 20,
|
|
3775
|
+
) -> Dict[str, Any]:
|
|
3776
|
+
"""Scan GitHub issues/PRs for migration patterns across target repos.
|
|
3777
|
+
|
|
3778
|
+
Detects language like "migrated from X to Y", "switched to Y",
|
|
3779
|
+
"replaced X with Y", "no longer using X" etc. Returns structured
|
|
3780
|
+
migration signals with source/target tools, sentiment, and strength.
|
|
3781
|
+
|
|
3782
|
+
Useful for competitive intelligence: see what tools repos are moving
|
|
3783
|
+
away from and what they are adopting.
|
|
3784
|
+
|
|
3785
|
+
Args:
|
|
3786
|
+
repos: List of GitHub repos in owner/repo format (e.g. ["chatwoot/chatwoot", "cal-com/cal.com"]).
|
|
3787
|
+
limit: Max migration signals per repo. Default 20.
|
|
3788
|
+
"""
|
|
3789
|
+
try:
|
|
3790
|
+
from ai.social_target import scan_github_migrations
|
|
3791
|
+
signals = scan_github_migrations(repos=repos, limit=limit)
|
|
3792
|
+
|
|
3793
|
+
# Separate errors from valid signals
|
|
3794
|
+
errors = [s for s in signals if s.get("error")]
|
|
3795
|
+
valid = [s for s in signals if not s.get("error")]
|
|
3796
|
+
|
|
3797
|
+
# Summary stats
|
|
3798
|
+
from_tools: Dict[str, int] = {}
|
|
3799
|
+
to_tools: Dict[str, int] = {}
|
|
3800
|
+
for s in valid:
|
|
3801
|
+
ft = s.get("from_tool", "")
|
|
3802
|
+
tt = s.get("to_tool", "")
|
|
3803
|
+
if ft:
|
|
3804
|
+
from_tools[ft] = from_tools.get(ft, 0) + 1
|
|
3805
|
+
if tt:
|
|
3806
|
+
to_tools[tt] = to_tools.get(tt, 0) + 1
|
|
3807
|
+
|
|
3808
|
+
return _with_next_steps("sensor_github_migrations", {
|
|
3809
|
+
"total_signals": len(valid),
|
|
3810
|
+
"errors": errors,
|
|
3811
|
+
"signals": valid,
|
|
3812
|
+
"summary": {
|
|
3813
|
+
"migrating_from": from_tools,
|
|
3814
|
+
"migrating_to": to_tools,
|
|
3815
|
+
"repos_scanned": len(repos),
|
|
3816
|
+
"repos_with_signals": len(set(s.get("repo") for s in valid)),
|
|
3817
|
+
},
|
|
3818
|
+
})
|
|
3819
|
+
except Exception as e:
|
|
3820
|
+
logger.error("Migration sensor error: %s\n%s", e, traceback.format_exc())
|
|
3821
|
+
return _with_next_steps("sensor_github_migrations", {"error": str(e), "total_signals": 0})
|
|
3822
|
+
|
|
3823
|
+
|
|
3670
3824
|
# ═══════════════════════════════════════════════════════════════════════
|
|
3671
3825
|
# META
|
|
3672
3826
|
# ═══════════════════════════════════════════════════════════════════════
|
|
@@ -3677,7 +3831,7 @@ def _count_registered_tools() -> int:
|
|
|
3677
3831
|
try:
|
|
3678
3832
|
return len(mcp._tool_manager._tools)
|
|
3679
3833
|
except AttributeError:
|
|
3680
|
-
# FastMCP version without _tool_manager
|
|
3834
|
+
# FastMCP version without _tool_manager - count via module introspection
|
|
3681
3835
|
import ai.server as _self
|
|
3682
3836
|
return len([n for n in dir(_self) if n.startswith("delimit_")])
|
|
3683
3837
|
|
|
@@ -3709,14 +3863,15 @@ def delimit_version() -> Dict[str, Any]:
|
|
|
3709
3863
|
TOOL_HELP = {
|
|
3710
3864
|
"init": {"desc": "Initialize governance for a project", "example": "delimit_init(project_path='.', preset='default')", "params": "project_path (str), preset (strict|default|relaxed)"},
|
|
3711
3865
|
"lint": {"desc": "Diff two OpenAPI specs and check policy violations", "example": "delimit_lint(old_spec='base.yaml', new_spec='new.yaml')", "params": "old_spec (path), new_spec (path), policy_file (optional path)"},
|
|
3712
|
-
"diff": {"desc": "Pure diff between two specs
|
|
3866
|
+
"diff": {"desc": "Pure diff between two specs - no policy, just changes", "example": "delimit_diff(old_spec='base.yaml', new_spec='new.yaml')", "params": "old_spec (path), new_spec (path)"},
|
|
3713
3867
|
"semver": {"desc": "Classify the semver bump for a spec change", "example": "delimit_semver(old_spec='base.yaml', new_spec='new.yaml', current_version='1.2.3')", "params": "old_spec, new_spec, current_version (optional)"},
|
|
3714
3868
|
"explain": {"desc": "Human-readable explanation of API changes", "example": "delimit_explain(old_spec='base.yaml', new_spec='new.yaml', template='pr_comment')", "params": "old_spec, new_spec, template (developer|pr_comment|migration|changelog)"},
|
|
3715
|
-
"gov_health": {"desc": "Check governance status
|
|
3869
|
+
"gov_health": {"desc": "Check governance status - is the project initialized?", "example": "delimit_gov_health(repo='.')", "params": "repo (path, default '.')"},
|
|
3716
3870
|
"test_coverage": {"desc": "Measure test coverage for a project", "example": "delimit_test_coverage(project_path='.', threshold=80)", "params": "project_path, threshold (default 80)"},
|
|
3717
|
-
"repo_analyze": {"desc": "Full repo health report
|
|
3871
|
+
"repo_analyze": {"desc": "Full repo health report - code quality, security, dependencies", "example": "delimit_repo_analyze(target='.')", "params": "target (path)"},
|
|
3718
3872
|
"zero_spec": {"desc": "Extract OpenAPI spec from source code (FastAPI, Express, NestJS)", "example": "delimit_zero_spec(project_dir='.')", "params": "project_dir (path)"},
|
|
3719
3873
|
"sensor_github_issue": {"desc": "Monitor a GitHub issue for new comments", "example": "delimit_sensor_github_issue(repo='owner/repo', issue_number=123)", "params": "repo (owner/name), issue_number (int)"},
|
|
3874
|
+
"sensor_github_migrations": {"desc": "Scan repos for migration patterns (migrated from X to Y)", "example": "delimit_sensor_github_migrations(repos=['chatwoot/chatwoot'])", "params": "repos (list of owner/repo), limit (int, default 20)"},
|
|
3720
3875
|
"quickstart": {"desc": "60-second guided first-run experience", "example": "delimit_quickstart(project_path='.')", "params": "project_path (str, default '.')"},
|
|
3721
3876
|
}
|
|
3722
3877
|
|
|
@@ -3736,7 +3891,7 @@ STANDARD_WORKFLOWS = [
|
|
|
3736
3891
|
},
|
|
3737
3892
|
{
|
|
3738
3893
|
"name": "Remember Across Models",
|
|
3739
|
-
"pain": "Every new session starts from zero
|
|
3894
|
+
"pain": "Every new session starts from zero - your agent forgot everything",
|
|
3740
3895
|
"fix": "Store and recall context across any AI assistant",
|
|
3741
3896
|
"steps": ["delimit_memory_store", "delimit_memory_search", "delimit_session_handoff"],
|
|
3742
3897
|
},
|
|
@@ -3760,7 +3915,7 @@ def delimit_swarm(action: str = "status", venture: str = "",
|
|
|
3760
3915
|
agent_id: str = "", repo_path: str = "",
|
|
3761
3916
|
deploy_target: str = "", target_path: str = "",
|
|
3762
3917
|
access_action: str = "read") -> Dict[str, Any]:
|
|
3763
|
-
"""Manage the agent swarm
|
|
3918
|
+
"""Manage the agent swarm - ventures, personas, namespace isolation.
|
|
3764
3919
|
|
|
3765
3920
|
Each venture gets 5 AI agent roles (Architect, Senior Dev, Reviewer, QA, Ops)
|
|
3766
3921
|
with namespace isolation and model binding per Agent Swarm Standard v1.2.
|
|
@@ -3788,7 +3943,7 @@ def delimit_swarm(action: str = "status", venture: str = "",
|
|
|
3788
3943
|
repo_path: Repo path, description, or reason depending on action.
|
|
3789
3944
|
deploy_target: Deploy target for venture registration.
|
|
3790
3945
|
target_path: File path, tool name, or role name depending on action.
|
|
3791
|
-
access_action: Action name
|
|
3946
|
+
access_action: Action name - for check: "read"/"write"/"deploy". For approve: "deploy_production"/"deploy_staging"/"social_post" etc.
|
|
3792
3947
|
"""
|
|
3793
3948
|
from ai.swarm import (register_venture, get_venture, get_agent,
|
|
3794
3949
|
check_namespace_access, get_swarm_status,
|
|
@@ -3978,7 +4133,7 @@ def delimit_redact(action: str = "scan", text: str = "",
|
|
|
3978
4133
|
|
|
3979
4134
|
if action == "redact":
|
|
3980
4135
|
result = pii_redact(text, categories=cat_list)
|
|
3981
|
-
# Never expose token_map through MCP
|
|
4136
|
+
# Never expose token_map through MCP - keep it local
|
|
3982
4137
|
return _with_next_steps("redact", {
|
|
3983
4138
|
"redacted": result["redacted"],
|
|
3984
4139
|
"findings": result["findings"],
|
|
@@ -3992,7 +4147,7 @@ def delimit_redact(action: str = "scan", text: str = "",
|
|
|
3992
4147
|
def delimit_prompt_drift(action: str = "check", prompt: str = "",
|
|
3993
4148
|
model: str = "", result_summary: str = "",
|
|
3994
4149
|
success: str = "true", task_type: str = "") -> Dict[str, Any]:
|
|
3995
|
-
"""Detect prompt drift
|
|
4150
|
+
"""Detect prompt drift - when the same task behaves differently across models.
|
|
3996
4151
|
|
|
3997
4152
|
Track how prompts perform across Claude, Codex, and Gemini.
|
|
3998
4153
|
Find which model is best for each task type on YOUR codebase.
|
|
@@ -4099,7 +4254,7 @@ def delimit_project_config(action: str = "load", project_path: str = ".",
|
|
|
4099
4254
|
def delimit_playbook(action: str = "list", name: str = "", prompt: str = "",
|
|
4100
4255
|
description: str = "", variables: str = "",
|
|
4101
4256
|
model_hint: str = "", tags: str = "") -> Dict[str, Any]:
|
|
4102
|
-
"""Manage reusable prompt templates
|
|
4257
|
+
"""Manage reusable prompt templates - save, run, list, delete.
|
|
4103
4258
|
|
|
4104
4259
|
Save your best prompts as named commands. Use {{variables}} for dynamic parts.
|
|
4105
4260
|
Works across all AI assistants through the shared MCP workspace.
|
|
@@ -4151,7 +4306,7 @@ def delimit_playbook(action: str = "list", name: str = "", prompt: str = "",
|
|
|
4151
4306
|
|
|
4152
4307
|
@mcp.tool()
|
|
4153
4308
|
def delimit_help(tool_name: str = "") -> Dict[str, Any]:
|
|
4154
|
-
"""Get help for a Delimit tool
|
|
4309
|
+
"""Get help for a Delimit tool - what it does, parameters, and examples.
|
|
4155
4310
|
|
|
4156
4311
|
Args:
|
|
4157
4312
|
tool_name: Tool name (e.g. 'lint', 'gov_health'). Leave empty for overview.
|
|
@@ -4164,7 +4319,7 @@ def delimit_help(tool_name: str = "") -> Dict[str, Any]:
|
|
|
4164
4319
|
{"name": w["name"], "pain": w["pain"], "start_with": w["steps"][0]}
|
|
4165
4320
|
for w in STANDARD_WORKFLOWS
|
|
4166
4321
|
],
|
|
4167
|
-
"tip": "Tell me what you're trying to do
|
|
4322
|
+
"tip": "Tell me what you're trying to do - I'll suggest the right workflow.",
|
|
4168
4323
|
"total_tools": total,
|
|
4169
4324
|
})
|
|
4170
4325
|
|
|
@@ -4178,22 +4333,81 @@ def delimit_help(tool_name: str = "") -> Dict[str, Any]:
|
|
|
4178
4333
|
|
|
4179
4334
|
@mcp.tool()
|
|
4180
4335
|
def delimit_diagnose(project_path: str = ".") -> Dict[str, Any]:
|
|
4181
|
-
"""
|
|
4336
|
+
"""Comprehensive health check of your Delimit installation (delimit doctor).
|
|
4182
4337
|
|
|
4183
|
-
Universal
|
|
4184
|
-
|
|
4338
|
+
Universal debugging tool. Runs 10 checks covering MCP connectivity,
|
|
4339
|
+
dependencies, governance state, AI assistants, permissions, API keys,
|
|
4340
|
+
network, version, daemons, and disk usage. Each item reports PASS, FAIL,
|
|
4341
|
+
or SKIP with actionable fixes.
|
|
4185
4342
|
|
|
4186
4343
|
Args:
|
|
4187
4344
|
project_path: Project to diagnose.
|
|
4188
4345
|
"""
|
|
4189
|
-
issues = []
|
|
4190
|
-
checks = {}
|
|
4191
|
-
|
|
4192
|
-
# Python version
|
|
4193
4346
|
import sys
|
|
4194
|
-
|
|
4347
|
+
import urllib.request
|
|
4348
|
+
import urllib.error
|
|
4349
|
+
|
|
4350
|
+
issues: List[Dict[str, str]] = []
|
|
4351
|
+
checks: Dict[str, Any] = {}
|
|
4352
|
+
checklist: List[Dict[str, str]] = []
|
|
4353
|
+
home = Path.home()
|
|
4354
|
+
|
|
4355
|
+
def _record(name: str, status: str, detail: str = "", fix: str = ""):
|
|
4356
|
+
"""Record a checklist item. status is PASS, FAIL, or SKIP."""
|
|
4357
|
+
entry = {"check": name, "status": status, "detail": detail}
|
|
4358
|
+
checklist.append(entry)
|
|
4359
|
+
if status == "FAIL" and fix:
|
|
4360
|
+
issues.append({"issue": f"{name}: {detail}", "fix": fix})
|
|
4195
4361
|
|
|
4196
|
-
#
|
|
4362
|
+
# ── 1. MCP Server Connectivity ───────────────────────────────────────
|
|
4363
|
+
try:
|
|
4364
|
+
tool_count = _count_registered_tools()
|
|
4365
|
+
if tool_count > 0:
|
|
4366
|
+
_record("MCP Server", "PASS", f"Running, {tool_count} tools registered")
|
|
4367
|
+
checks["mcp_server"] = {"reachable": True, "tools": tool_count}
|
|
4368
|
+
else:
|
|
4369
|
+
_record("MCP Server", "FAIL", "Server running but 0 tools registered",
|
|
4370
|
+
"Restart the MCP server -- possible import error")
|
|
4371
|
+
checks["mcp_server"] = {"reachable": True, "tools": 0}
|
|
4372
|
+
except Exception as exc:
|
|
4373
|
+
_record("MCP Server", "FAIL", f"Cannot query tool registry: {exc}",
|
|
4374
|
+
"Restart the MCP server process")
|
|
4375
|
+
checks["mcp_server"] = {"reachable": False, "error": str(exc)}
|
|
4376
|
+
|
|
4377
|
+
# ── 2. Python Dependencies ───────────────────────────────────────────
|
|
4378
|
+
checks["python"] = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
4379
|
+
dep_results = {}
|
|
4380
|
+
required_deps = {
|
|
4381
|
+
"yaml": "pyyaml",
|
|
4382
|
+
"pydantic": "pydantic",
|
|
4383
|
+
"packaging": "packaging",
|
|
4384
|
+
"fastmcp": "fastmcp",
|
|
4385
|
+
}
|
|
4386
|
+
all_deps_ok = True
|
|
4387
|
+
for import_name, pip_name in required_deps.items():
|
|
4388
|
+
try:
|
|
4389
|
+
mod = __import__(import_name)
|
|
4390
|
+
ver = getattr(mod, "__version__", getattr(mod, "VERSION", "installed"))
|
|
4391
|
+
dep_results[import_name] = str(ver)
|
|
4392
|
+
except ImportError:
|
|
4393
|
+
dep_results[import_name] = "MISSING"
|
|
4394
|
+
all_deps_ok = False
|
|
4395
|
+
if all_deps_ok:
|
|
4396
|
+
_record("Python Dependencies", "PASS",
|
|
4397
|
+
f"Python {checks['python']}, all {len(required_deps)} packages present")
|
|
4398
|
+
else:
|
|
4399
|
+
missing = [k for k, v in dep_results.items() if v == "MISSING"]
|
|
4400
|
+
_record("Python Dependencies", "FAIL",
|
|
4401
|
+
f"Missing: {', '.join(missing)}",
|
|
4402
|
+
f"pip install {' '.join(required_deps[m] for m in missing)}")
|
|
4403
|
+
for m in missing:
|
|
4404
|
+
issues.append({"issue": f"Missing Python package: {m}", "fix": f"pip install {required_deps[m]}"})
|
|
4405
|
+
checks["dependencies"] = dep_results
|
|
4406
|
+
# Backward compat keys
|
|
4407
|
+
for import_name, pip_name in required_deps.items():
|
|
4408
|
+
checks[f"dep_{import_name}" if import_name != "fastmcp" else "fastmcp"] = dep_results[import_name] != "MISSING"
|
|
4409
|
+
|
|
4410
|
+
# ── 3. Governance State ──────────────────────────────────────────────
|
|
4197
4411
|
p = Path(project_path).resolve()
|
|
4198
4412
|
delimit_dir = p / ".delimit"
|
|
4199
4413
|
policies = delimit_dir / "policies.yml"
|
|
@@ -4204,37 +4418,38 @@ def delimit_diagnose(project_path: str = ".") -> Dict[str, Any]:
|
|
|
4204
4418
|
checks["policies_file"] = policies.is_file()
|
|
4205
4419
|
checks["ledger_file"] = ledger.is_file()
|
|
4206
4420
|
|
|
4207
|
-
if
|
|
4421
|
+
if delimit_dir.is_dir() and policies.is_file() and ledger.is_file():
|
|
4422
|
+
gov_parts = [".delimit/ exists", "policies.yml present", "ledger present"]
|
|
4423
|
+
try:
|
|
4424
|
+
entry_count = sum(1 for line in ledger.read_text().splitlines() if line.strip())
|
|
4425
|
+
gov_parts.append(f"{entry_count} ledger entries")
|
|
4426
|
+
except Exception:
|
|
4427
|
+
pass
|
|
4428
|
+
_record("Governance State", "PASS", "; ".join(gov_parts))
|
|
4429
|
+
elif not delimit_dir.is_dir():
|
|
4430
|
+
_record("Governance State", "FAIL", "Project not initialized",
|
|
4431
|
+
"Run delimit_init(project_path='.') or say 'initialize governance for this project'")
|
|
4208
4432
|
issues.append({
|
|
4209
4433
|
"issue": "Project not initialized",
|
|
4210
4434
|
"fix": "Run delimit_init(project_path='.') or say 'initialize governance for this project'",
|
|
4211
4435
|
})
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
"
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
# Check fastmcp
|
|
4228
|
-
try:
|
|
4229
|
-
import fastmcp
|
|
4230
|
-
checks["fastmcp"] = True
|
|
4231
|
-
except ImportError:
|
|
4232
|
-
checks["fastmcp"] = False
|
|
4233
|
-
issues.append({"issue": "FastMCP not installed", "fix": "pip install fastmcp"})
|
|
4436
|
+
else:
|
|
4437
|
+
missing_parts = []
|
|
4438
|
+
if not policies.is_file():
|
|
4439
|
+
missing_parts.append("policies.yml")
|
|
4440
|
+
if not ledger.is_file():
|
|
4441
|
+
missing_parts.append("ledger")
|
|
4442
|
+
_record("Governance State", "FAIL",
|
|
4443
|
+
f".delimit/ exists but missing: {', '.join(missing_parts)}",
|
|
4444
|
+
"Run delimit_init(project_path='.', preset='default')")
|
|
4445
|
+
if not policies.is_file():
|
|
4446
|
+
issues.append({
|
|
4447
|
+
"issue": "Missing policies.yml",
|
|
4448
|
+
"fix": "Run delimit_init(project_path='.', preset='default')",
|
|
4449
|
+
})
|
|
4234
4450
|
|
|
4235
|
-
#
|
|
4451
|
+
# ── 4. AI Assistant Detection ────────────────────────────────────────
|
|
4236
4452
|
config_sync = {}
|
|
4237
|
-
home = Path.home()
|
|
4238
4453
|
assistant_configs = {
|
|
4239
4454
|
"claude_code": home / ".mcp.json",
|
|
4240
4455
|
"codex_toml": home / ".codex" / "config.toml",
|
|
@@ -4264,7 +4479,215 @@ def delimit_diagnose(project_path: str = ".") -> Dict[str, Any]:
|
|
|
4264
4479
|
checks["assistant_configs"] = config_sync
|
|
4265
4480
|
checks["assistants_configured"] = f"{configured_count}/{installed_count}"
|
|
4266
4481
|
|
|
4267
|
-
|
|
4482
|
+
if installed_count == 0:
|
|
4483
|
+
_record("AI Assistants", "SKIP", "No AI assistant configs found")
|
|
4484
|
+
elif configured_count == installed_count:
|
|
4485
|
+
_record("AI Assistants", "PASS",
|
|
4486
|
+
f"{configured_count}/{installed_count} assistants have Delimit configured")
|
|
4487
|
+
else:
|
|
4488
|
+
unconfigured = [k for k, v in config_sync.items() if v == "missing_delimit"]
|
|
4489
|
+
_record("AI Assistants", "FAIL",
|
|
4490
|
+
f"{configured_count}/{installed_count} configured; missing in: {', '.join(unconfigured)}",
|
|
4491
|
+
"Run: npx delimit-cli setup")
|
|
4492
|
+
|
|
4493
|
+
# ── 5. Permission Status ─────────────────────────────────────────────
|
|
4494
|
+
permission_info = {}
|
|
4495
|
+
claude_settings = home / ".claude.json"
|
|
4496
|
+
if claude_settings.exists():
|
|
4497
|
+
try:
|
|
4498
|
+
claude_data = json.loads(claude_settings.read_text())
|
|
4499
|
+
allowed = claude_data.get("allowedTools",
|
|
4500
|
+
claude_data.get("permissions", {}).get("allow", []))
|
|
4501
|
+
if isinstance(allowed, list):
|
|
4502
|
+
delimit_allowed = [t for t in allowed if "delimit" in str(t).lower()]
|
|
4503
|
+
permission_info["claude_code"] = {
|
|
4504
|
+
"auto_approved": len(delimit_allowed) > 0,
|
|
4505
|
+
"count": len(delimit_allowed),
|
|
4506
|
+
}
|
|
4507
|
+
else:
|
|
4508
|
+
permission_info["claude_code"] = {"auto_approved": False, "count": 0}
|
|
4509
|
+
except Exception:
|
|
4510
|
+
permission_info["claude_code"] = {"status": "read_error"}
|
|
4511
|
+
project_claude = p / ".claude" / "settings.json"
|
|
4512
|
+
if project_claude.exists():
|
|
4513
|
+
try:
|
|
4514
|
+
pdata = json.loads(project_claude.read_text())
|
|
4515
|
+
proj_allowed = pdata.get("allowedTools",
|
|
4516
|
+
pdata.get("permissions", {}).get("allow", []))
|
|
4517
|
+
if isinstance(proj_allowed, list):
|
|
4518
|
+
delimit_proj = [t for t in proj_allowed if "delimit" in str(t).lower()]
|
|
4519
|
+
permission_info["claude_code_project"] = {
|
|
4520
|
+
"auto_approved": len(delimit_proj) > 0,
|
|
4521
|
+
"count": len(delimit_proj),
|
|
4522
|
+
}
|
|
4523
|
+
except Exception:
|
|
4524
|
+
pass
|
|
4525
|
+
checks["permissions"] = permission_info
|
|
4526
|
+
|
|
4527
|
+
if not permission_info:
|
|
4528
|
+
_record("Permissions", "SKIP", "No permission config files found")
|
|
4529
|
+
else:
|
|
4530
|
+
any_approved = any(
|
|
4531
|
+
v.get("auto_approved", False) for v in permission_info.values()
|
|
4532
|
+
if isinstance(v, dict)
|
|
4533
|
+
)
|
|
4534
|
+
if any_approved:
|
|
4535
|
+
_record("Permissions", "PASS", "Delimit tools are auto-approved")
|
|
4536
|
+
else:
|
|
4537
|
+
_record("Permissions", "FAIL",
|
|
4538
|
+
"Delimit tools require manual approval on each call",
|
|
4539
|
+
"Add 'mcp__delimit__*' to allowedTools in .claude.json or project settings")
|
|
4540
|
+
|
|
4541
|
+
# ── 6. API Keys ──────────────────────────────────────────────────────
|
|
4542
|
+
environment = _detect_environment()
|
|
4543
|
+
api_keys = environment.get("api_keys", {})
|
|
4544
|
+
checks["api_keys"] = {k: "configured" for k in api_keys}
|
|
4545
|
+
|
|
4546
|
+
if api_keys:
|
|
4547
|
+
_record("API Keys", "PASS",
|
|
4548
|
+
f"{len(api_keys)} configured: {', '.join(sorted(api_keys.keys()))}")
|
|
4549
|
+
else:
|
|
4550
|
+
_record("API Keys", "SKIP",
|
|
4551
|
+
"No API keys detected (set ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)")
|
|
4552
|
+
|
|
4553
|
+
# ── 7. Network Connectivity ──────────────────────────────────────────
|
|
4554
|
+
network_checks = {}
|
|
4555
|
+
|
|
4556
|
+
def _check_url(label: str, url: str) -> bool:
|
|
4557
|
+
try:
|
|
4558
|
+
req = urllib.request.Request(url, method="HEAD")
|
|
4559
|
+
resp = urllib.request.urlopen(req, timeout=5)
|
|
4560
|
+
network_checks[label] = {"reachable": True, "status": resp.status}
|
|
4561
|
+
return True
|
|
4562
|
+
except Exception as exc:
|
|
4563
|
+
network_checks[label] = {"reachable": False, "error": str(exc)[:120]}
|
|
4564
|
+
return False
|
|
4565
|
+
|
|
4566
|
+
github_ok = _check_url("github_api", "https://api.github.com")
|
|
4567
|
+
npm_ok = _check_url("npm_registry", "https://registry.npmjs.org/delimit-cli")
|
|
4568
|
+
checks["network"] = network_checks
|
|
4569
|
+
|
|
4570
|
+
if github_ok and npm_ok:
|
|
4571
|
+
_record("Network", "PASS", "GitHub API and npm registry reachable")
|
|
4572
|
+
elif not github_ok and not npm_ok:
|
|
4573
|
+
_record("Network", "FAIL", "Cannot reach GitHub API or npm registry",
|
|
4574
|
+
"Check internet connection and firewall/proxy settings")
|
|
4575
|
+
else:
|
|
4576
|
+
parts = []
|
|
4577
|
+
if not github_ok:
|
|
4578
|
+
parts.append("GitHub API unreachable")
|
|
4579
|
+
if not npm_ok:
|
|
4580
|
+
parts.append("npm registry unreachable")
|
|
4581
|
+
_record("Network", "FAIL", "; ".join(parts),
|
|
4582
|
+
"Check internet connection and firewall/proxy settings")
|
|
4583
|
+
|
|
4584
|
+
# ── 8. Version Check ─────────────────────────────────────────────────
|
|
4585
|
+
checks["version"] = VERSION
|
|
4586
|
+
latest_version = None
|
|
4587
|
+
try:
|
|
4588
|
+
req = urllib.request.Request("https://registry.npmjs.org/delimit-cli/latest")
|
|
4589
|
+
resp = urllib.request.urlopen(req, timeout=5)
|
|
4590
|
+
npm_data = json.loads(resp.read().decode())
|
|
4591
|
+
latest_version = npm_data.get("version")
|
|
4592
|
+
checks["latest_npm_version"] = latest_version
|
|
4593
|
+
if latest_version and latest_version != VERSION:
|
|
4594
|
+
_record("Version", "FAIL",
|
|
4595
|
+
f"Running {VERSION}, latest npm is {latest_version}",
|
|
4596
|
+
f"npm update -g delimit-cli (or npx delimit-cli@{latest_version})")
|
|
4597
|
+
elif latest_version:
|
|
4598
|
+
_record("Version", "PASS", f"Running {VERSION} (latest)")
|
|
4599
|
+
else:
|
|
4600
|
+
_record("Version", "SKIP", f"Running {VERSION}, could not parse latest from npm")
|
|
4601
|
+
except Exception:
|
|
4602
|
+
_record("Version", "SKIP", f"Running {VERSION}, npm check unavailable")
|
|
4603
|
+
checks["latest_npm_version"] = None
|
|
4604
|
+
|
|
4605
|
+
# ── 9. Daemon Status ─────────────────────────────────────────────────
|
|
4606
|
+
daemon_info = {}
|
|
4607
|
+
|
|
4608
|
+
# Inbox daemon
|
|
4609
|
+
try:
|
|
4610
|
+
from ai.inbox_daemon import get_daemon_status as _inbox_status
|
|
4611
|
+
inbox_st = _inbox_status()
|
|
4612
|
+
running = inbox_st.get("running", inbox_st.get("status") == "running")
|
|
4613
|
+
daemon_info["inbox"] = {"running": bool(running)}
|
|
4614
|
+
except Exception:
|
|
4615
|
+
daemon_info["inbox"] = {"running": False, "note": "module_unavailable"}
|
|
4616
|
+
|
|
4617
|
+
# Social daemon
|
|
4618
|
+
try:
|
|
4619
|
+
from ai.social_daemon import get_status as _social_status
|
|
4620
|
+
social_st = _social_status()
|
|
4621
|
+
running = social_st.get("running", social_st.get("status") == "running")
|
|
4622
|
+
daemon_info["social"] = {"running": bool(running)}
|
|
4623
|
+
except Exception:
|
|
4624
|
+
daemon_info["social"] = {"running": False, "note": "module_unavailable"}
|
|
4625
|
+
|
|
4626
|
+
# Autonomous daemon
|
|
4627
|
+
try:
|
|
4628
|
+
from ai.daemon import get_daemon_status as _gen_status
|
|
4629
|
+
gen_st = _gen_status()
|
|
4630
|
+
running = gen_st.get("running", gen_st.get("status") == "running")
|
|
4631
|
+
daemon_info["autonomous"] = {"running": bool(running)}
|
|
4632
|
+
except Exception:
|
|
4633
|
+
daemon_info["autonomous"] = {"running": False, "note": "module_unavailable"}
|
|
4634
|
+
|
|
4635
|
+
checks["daemons"] = daemon_info
|
|
4636
|
+
running_daemons = [k for k, v in daemon_info.items() if v.get("running")]
|
|
4637
|
+
stopped_daemons = [k for k, v in daemon_info.items() if not v.get("running")]
|
|
4638
|
+
|
|
4639
|
+
if running_daemons:
|
|
4640
|
+
detail = f"Running: {', '.join(running_daemons)}"
|
|
4641
|
+
if stopped_daemons:
|
|
4642
|
+
detail += f"; stopped: {', '.join(stopped_daemons)}"
|
|
4643
|
+
_record("Daemons", "PASS", detail)
|
|
4644
|
+
else:
|
|
4645
|
+
_record("Daemons", "SKIP",
|
|
4646
|
+
"No daemons running (start with delimit_inbox_daemon or delimit_daemon_run)")
|
|
4647
|
+
|
|
4648
|
+
# ── 10. Disk Usage ───────────────────────────────────────────────────
|
|
4649
|
+
delimit_home = home / ".delimit"
|
|
4650
|
+
if delimit_home.exists():
|
|
4651
|
+
try:
|
|
4652
|
+
total_bytes = 0
|
|
4653
|
+
file_count = 0
|
|
4654
|
+
for f in delimit_home.rglob("*"):
|
|
4655
|
+
if f.is_file():
|
|
4656
|
+
try:
|
|
4657
|
+
total_bytes += f.stat().st_size
|
|
4658
|
+
file_count += 1
|
|
4659
|
+
except OSError:
|
|
4660
|
+
pass
|
|
4661
|
+
if total_bytes < 1024:
|
|
4662
|
+
size_str = f"{total_bytes} B"
|
|
4663
|
+
elif total_bytes < 1024 * 1024:
|
|
4664
|
+
size_str = f"{total_bytes / 1024:.1f} KB"
|
|
4665
|
+
elif total_bytes < 1024 * 1024 * 1024:
|
|
4666
|
+
size_str = f"{total_bytes / (1024 * 1024):.1f} MB"
|
|
4667
|
+
else:
|
|
4668
|
+
size_str = f"{total_bytes / (1024 * 1024 * 1024):.2f} GB"
|
|
4669
|
+
|
|
4670
|
+
checks["disk"] = {
|
|
4671
|
+
"path": str(delimit_home),
|
|
4672
|
+
"size_bytes": total_bytes,
|
|
4673
|
+
"size_human": size_str,
|
|
4674
|
+
"file_count": file_count,
|
|
4675
|
+
}
|
|
4676
|
+
if total_bytes > 500 * 1024 * 1024:
|
|
4677
|
+
_record("Disk Usage", "FAIL",
|
|
4678
|
+
f"~/.delimit/ is {size_str} ({file_count} files) -- consider cleanup",
|
|
4679
|
+
"Remove old ledger entries or run: du -sh ~/.delimit/*/")
|
|
4680
|
+
else:
|
|
4681
|
+
_record("Disk Usage", "PASS",
|
|
4682
|
+
f"~/.delimit/ is {size_str} ({file_count} files)")
|
|
4683
|
+
except Exception as exc:
|
|
4684
|
+
_record("Disk Usage", "SKIP", f"Could not measure: {exc}")
|
|
4685
|
+
checks["disk"] = {"error": str(exc)}
|
|
4686
|
+
else:
|
|
4687
|
+
_record("Disk Usage", "SKIP", "~/.delimit/ does not exist")
|
|
4688
|
+
checks["disk"] = {"path": str(delimit_home), "exists": False}
|
|
4689
|
+
|
|
4690
|
+
# ── MCP Security Warnings (LED-192) ──────────────────────────────────
|
|
4268
4691
|
mcp_warnings = []
|
|
4269
4692
|
mcp_config_path = home / ".mcp.json"
|
|
4270
4693
|
if mcp_config_path.exists():
|
|
@@ -4273,7 +4696,6 @@ def delimit_diagnose(project_path: str = ".") -> Dict[str, Any]:
|
|
|
4273
4696
|
for server_name, server_cfg in mcp_data.get("mcpServers", {}).items():
|
|
4274
4697
|
cmd = server_cfg.get("command", "")
|
|
4275
4698
|
args = server_cfg.get("args", [])
|
|
4276
|
-
# Check for risky patterns
|
|
4277
4699
|
if "curl" in cmd or "wget" in cmd:
|
|
4278
4700
|
mcp_warnings.append(f"{server_name}: command uses curl/wget (potential remote code execution)")
|
|
4279
4701
|
if any("--no-sandbox" in str(a) for a in args):
|
|
@@ -4287,21 +4709,31 @@ def delimit_diagnose(project_path: str = ".") -> Dict[str, Any]:
|
|
|
4287
4709
|
for w in mcp_warnings:
|
|
4288
4710
|
issues.append({"issue": f"MCP security: {w}", "fix": "Review server configuration"})
|
|
4289
4711
|
|
|
4290
|
-
# Summary
|
|
4291
|
-
|
|
4712
|
+
# ── Build Summary ────────────────────────────────────────────────────
|
|
4713
|
+
pass_count = sum(1 for c in checklist if c["status"] == "PASS")
|
|
4714
|
+
fail_count = sum(1 for c in checklist if c["status"] == "FAIL")
|
|
4715
|
+
skip_count = sum(1 for c in checklist if c["status"] == "SKIP")
|
|
4716
|
+
total_count = len(checklist)
|
|
4717
|
+
|
|
4718
|
+
status = "healthy" if fail_count == 0 else "issues_found"
|
|
4292
4719
|
result = {
|
|
4293
4720
|
"status": status,
|
|
4721
|
+
"summary": f"{pass_count}/{total_count} checks passed, {fail_count} failed, {skip_count} skipped",
|
|
4722
|
+
"checklist": checklist,
|
|
4294
4723
|
"checks": checks,
|
|
4295
4724
|
"issues": issues,
|
|
4296
4725
|
"issue_count": len(issues),
|
|
4297
|
-
"tip": "If everything looks good but tools aren't working, try restarting
|
|
4726
|
+
"tip": "If everything looks good but tools aren't working, try restarting your AI assistant."
|
|
4727
|
+
" Run delimit_diagnose again after making fixes.",
|
|
4298
4728
|
}
|
|
4299
|
-
# Dynamic next_steps
|
|
4729
|
+
# Dynamic next_steps
|
|
4300
4730
|
diagnose_next = []
|
|
4301
4731
|
if not delimit_dir.is_dir():
|
|
4302
4732
|
diagnose_next.append({"tool": "delimit_init", "reason": "Initialize governance for this project", "suggested_args": {"preset": "default"}, "is_premium": False})
|
|
4303
4733
|
if any(v == "missing_delimit" for v in config_sync.values()):
|
|
4304
4734
|
diagnose_next.append({"tool": "delimit_quickstart", "reason": "Re-run setup to configure missing assistants", "is_premium": False})
|
|
4735
|
+
if fail_count > 0:
|
|
4736
|
+
diagnose_next.append({"tool": "delimit_help", "reason": "Get help on specific tools", "suggested_args": {"tool_name": "diagnose"}, "is_premium": False})
|
|
4305
4737
|
result["next_steps"] = diagnose_next
|
|
4306
4738
|
return result
|
|
4307
4739
|
|
|
@@ -4347,7 +4779,7 @@ def delimit_deploy_site(
|
|
|
4347
4779
|
project_path: str = ".",
|
|
4348
4780
|
message: str = "",
|
|
4349
4781
|
) -> Dict[str, Any]:
|
|
4350
|
-
"""Deploy a site
|
|
4782
|
+
"""Deploy a site - git commit, push, Vercel build, deploy (Pro)."""
|
|
4351
4783
|
return _delimit_deploy_impl(action="site", project_path=project_path, message=message)
|
|
4352
4784
|
|
|
4353
4785
|
|
|
@@ -4463,8 +4895,8 @@ def delimit_ledger_update(
|
|
|
4463
4895
|
Args:
|
|
4464
4896
|
item_id: The item ID (e.g. LED-001 or STR-001).
|
|
4465
4897
|
venture: Project name or path. Auto-detects if empty.
|
|
4466
|
-
status: New status
|
|
4467
|
-
priority: New priority
|
|
4898
|
+
status: New status - "open", "in_progress", "blocked", "done".
|
|
4899
|
+
priority: New priority - "P0", "P1", "P2".
|
|
4468
4900
|
title: New title.
|
|
4469
4901
|
description: New description.
|
|
4470
4902
|
note: Add a note/comment to the item.
|
|
@@ -4518,8 +4950,8 @@ def delimit_ledger_list(
|
|
|
4518
4950
|
Args:
|
|
4519
4951
|
venture: Project name or path. Auto-detects if empty.
|
|
4520
4952
|
ledger: "ops", "strategy", or "both".
|
|
4521
|
-
status: Filter by status
|
|
4522
|
-
priority: Filter by priority
|
|
4953
|
+
status: Filter by status - "open", "done", "in_progress", or empty for all.
|
|
4954
|
+
priority: Filter by priority - "P0", "P1", "P2", or empty for all.
|
|
4523
4955
|
limit: Max items to return.
|
|
4524
4956
|
"""
|
|
4525
4957
|
from ai.ledger_manager import list_items
|
|
@@ -4584,7 +5016,7 @@ def delimit_ledger_link(
|
|
|
4584
5016
|
Args:
|
|
4585
5017
|
from_id: Source item ID (e.g. "LED-025").
|
|
4586
5018
|
to_id: Target item ID (e.g. "STR-005").
|
|
4587
|
-
link_type: Relationship type
|
|
5019
|
+
link_type: Relationship type - "blocks", "blocked_by", "parent", "child", "relates_to", "duplicates".
|
|
4588
5020
|
note: Optional note explaining the relationship.
|
|
4589
5021
|
venture: Project name or path. Auto-detects if empty.
|
|
4590
5022
|
"""
|
|
@@ -4689,7 +5121,7 @@ def delimit_ventures() -> Dict[str, Any]:
|
|
|
4689
5121
|
|
|
4690
5122
|
|
|
4691
5123
|
# ═══════════════════════════════════════════════════════════════════════
|
|
4692
|
-
# SESSION PHOENIX
|
|
5124
|
+
# SESSION PHOENIX - Cross-Model Resurrection (LED-218)
|
|
4693
5125
|
# ═══════════════════════════════════════════════════════════════════════
|
|
4694
5126
|
|
|
4695
5127
|
|
|
@@ -5355,7 +5787,7 @@ def delimit_quickstart(project_path: str = ".") -> Dict[str, Any]:
|
|
|
5355
5787
|
|
|
5356
5788
|
|
|
5357
5789
|
# ═══════════════════════════════════════════════════════════════════════
|
|
5358
|
-
# STR-049: SECRETS BROKER
|
|
5790
|
+
# STR-049: SECRETS BROKER - JIT credential access with audit
|
|
5359
5791
|
# ═══════════════════════════════════════════════════════════════════════
|
|
5360
5792
|
|
|
5361
5793
|
|
|
@@ -5455,7 +5887,7 @@ def delimit_secret_access_log(name: str = "") -> Dict[str, Any]:
|
|
|
5455
5887
|
|
|
5456
5888
|
|
|
5457
5889
|
# ═══════════════════════════════════════════════════════════════════════
|
|
5458
|
-
# STR-048: Context Filesystem
|
|
5890
|
+
# STR-048: Context Filesystem - versioned namespace for agent state
|
|
5459
5891
|
# ═══════════════════════════════════════════════════════════════════════
|
|
5460
5892
|
|
|
5461
5893
|
# Consensus 082 Phase 2: Unified context tool with action parameter
|
|
@@ -5573,7 +6005,7 @@ def delimit_context_branch(venture: str, action: str = "list", branch_name: str
|
|
|
5573
6005
|
|
|
5574
6006
|
|
|
5575
6007
|
# ═══════════════════════════════════════════════════════════════════════
|
|
5576
|
-
# STR-050: DATA/ACTION PLANE
|
|
6008
|
+
# STR-050: DATA/ACTION PLANE - External systems as typed mounted resources
|
|
5577
6009
|
# ═══════════════════════════════════════════════════════════════════════
|
|
5578
6010
|
|
|
5579
6011
|
|
|
@@ -5677,7 +6109,7 @@ def delimit_tracker_sync(
|
|
|
5677
6109
|
) -> Dict[str, Any]:
|
|
5678
6110
|
"""Pull open issues from GitHub into the Delimit ledger as context.
|
|
5679
6111
|
|
|
5680
|
-
Read-only sync
|
|
6112
|
+
Read-only sync - enriches your ledger with external issue context.
|
|
5681
6113
|
Does NOT write back to GitHub.
|
|
5682
6114
|
|
|
5683
6115
|
Args:
|
|
@@ -5836,7 +6268,7 @@ def delimit_webhook_manage(
|
|
|
5836
6268
|
|
|
5837
6269
|
|
|
5838
6270
|
# ═══════════════════════════════════════════════════════════════════════
|
|
5839
|
-
# SOCIAL MEDIA
|
|
6271
|
+
# SOCIAL MEDIA - Authentic engagement at scale (Pro)
|
|
5840
6272
|
# ═══════════════════════════════════════════════════════════════════════
|
|
5841
6273
|
|
|
5842
6274
|
|
|
@@ -5847,18 +6279,18 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
5847
6279
|
context: str = "") -> Dict[str, Any]:
|
|
5848
6280
|
"""Post to social media (Pro).
|
|
5849
6281
|
|
|
5850
|
-
IMPORTANT
|
|
6282
|
+
IMPORTANT - AUTO-TRIGGER RULE:
|
|
5851
6283
|
When the AI drafts a social post, it MUST call this tool immediately.
|
|
5852
|
-
Never ask "want me to send this?"
|
|
6284
|
+
Never ask "want me to send this?" - just call the tool. The email-based
|
|
5853
6285
|
approval flow handles human review. The AI's job is to draft and send,
|
|
5854
6286
|
the founder's job is to approve/reject via email reply.
|
|
5855
6287
|
|
|
5856
6288
|
Categories: tip, changelog, insight, engagement.
|
|
5857
6289
|
Leave text empty to auto-generate from templates.
|
|
5858
|
-
Every post provides value
|
|
6290
|
+
Every post provides value - tips, insights, governance wisdom.
|
|
5859
6291
|
Max 2 posts per day to stay authentic.
|
|
5860
6292
|
|
|
5861
|
-
IMPORTANT
|
|
6293
|
+
IMPORTANT - Platform tone rules (these are DIFFERENT per platform):
|
|
5862
6294
|
- Twitter: confident technical brand. Direct, professional, ALWAYS POSITIVE.
|
|
5863
6295
|
Celebrate wins and progress. Never complain or air gaps publicly.
|
|
5864
6296
|
No em dashes or en dashes. Default to insight-first with no CTA unless source-grounded.
|
|
@@ -5884,12 +6316,12 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
5884
6316
|
|
|
5885
6317
|
post = generate_post(category, text)
|
|
5886
6318
|
|
|
5887
|
-
# ALL platforms go through email approval
|
|
6319
|
+
# ALL platforms go through email approval - no direct posting.
|
|
5888
6320
|
# Founder reviews and posts manually from their device.
|
|
5889
6321
|
if platform not in ("twitter", "reddit"):
|
|
5890
6322
|
return {"error": f"Platform '{platform}' not supported yet", "supported": ["twitter", "reddit"]}
|
|
5891
6323
|
|
|
5892
|
-
# ── Draft quality gate
|
|
6324
|
+
# ── Draft quality gate - reject template stubs and platform mismatches ──
|
|
5893
6325
|
_draft_text = text or post.get("text", "")
|
|
5894
6326
|
_stub_patterns = [
|
|
5895
6327
|
"[DRAFT - needs human writing]",
|
|
@@ -5969,7 +6401,7 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
5969
6401
|
_lines.append(f"Platform: REDDIT as u/{_acct}")
|
|
5970
6402
|
_lines.append("Owner action: Open the thread and reply using the draft below.")
|
|
5971
6403
|
else:
|
|
5972
|
-
# New Reddit post
|
|
6404
|
+
# New Reddit post - extract title from first line of text
|
|
5973
6405
|
_post_text = post["text"]
|
|
5974
6406
|
_first_newline = _post_text.find("\n")
|
|
5975
6407
|
if _first_newline > 0 and _first_newline < 200:
|
|
@@ -5981,13 +6413,11 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
5981
6413
|
_lines.append(f"Platform: REDDIT as u/{_acct}")
|
|
5982
6414
|
_lines.append("Owner action: Navigate to the subreddit and create a new post.")
|
|
5983
6415
|
_lines.append("")
|
|
5984
|
-
_lines.append("
|
|
5985
|
-
_lines.append("Tap and hold inside this block to copy.")
|
|
6416
|
+
_lines.append("--- TITLE (paste in title field) ---")
|
|
5986
6417
|
_lines.append(_reddit_title)
|
|
5987
|
-
_lines.append("")
|
|
5988
|
-
_lines.append("Post Body")
|
|
5989
|
-
_lines.append("Tap and hold inside this block to copy.")
|
|
6418
|
+
_lines.append("--- BODY (paste in body field) ---")
|
|
5990
6419
|
_lines.append(_reddit_body)
|
|
6420
|
+
_lines.append("--- END COPY ---")
|
|
5991
6421
|
_lines.append("")
|
|
5992
6422
|
_lines.append(f"Draft ID: {entry['draft_id']}")
|
|
5993
6423
|
if entry.get("tone_warnings"):
|
|
@@ -6020,9 +6450,9 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
6020
6450
|
_lines.append("Owner action: Open X, compose a new post, paste the draft below.")
|
|
6021
6451
|
|
|
6022
6452
|
_lines.append("")
|
|
6023
|
-
_lines.append("
|
|
6024
|
-
_lines.append("Tap and hold inside this block to copy.")
|
|
6453
|
+
_lines.append("--- COPY BELOW THIS LINE ---")
|
|
6025
6454
|
_lines.append(post["text"])
|
|
6455
|
+
_lines.append("--- END COPY ---")
|
|
6026
6456
|
_lines.append("")
|
|
6027
6457
|
_lines.append(f"Draft ID: {entry['draft_id']}")
|
|
6028
6458
|
if entry.get("tone_warnings"):
|
|
@@ -6096,7 +6526,7 @@ def delimit_social_history(limit: int = 20, platform: str = "",
|
|
|
6096
6526
|
|
|
6097
6527
|
Args:
|
|
6098
6528
|
limit: Max entries to return.
|
|
6099
|
-
platform: Filter by platform
|
|
6529
|
+
platform: Filter by platform - "twitter" or "reddit".
|
|
6100
6530
|
user: Filter by Reddit user we interacted with (e.g. "coolinjapan001").
|
|
6101
6531
|
subreddit: Filter by subreddit (e.g. "r/vibecoding").
|
|
6102
6532
|
"""
|
|
@@ -6151,7 +6581,7 @@ def delimit_social_target(
|
|
|
6151
6581
|
) -> Dict[str, Any]:
|
|
6152
6582
|
"""Discover engagement opportunities across platforms (Pro).
|
|
6153
6583
|
|
|
6154
|
-
IMPORTANT
|
|
6584
|
+
IMPORTANT - TOOL CHAINING RULE:
|
|
6155
6585
|
After scanning, the AI MUST immediately process results:
|
|
6156
6586
|
1. For "reply" targets: draft a reply via delimit_social_post or delimit_notify
|
|
6157
6587
|
2. For "strategic" targets: create a ledger item via delimit_ledger_add
|
|
@@ -6297,7 +6727,7 @@ def delimit_github_scan(
|
|
|
6297
6727
|
|
|
6298
6728
|
|
|
6299
6729
|
# ═══════════════════════════════════════════════════════════════════════
|
|
6300
|
-
# CONTENT ENGINE
|
|
6730
|
+
# CONTENT ENGINE - Autonomous video + tweet pipeline (Pro)
|
|
6301
6731
|
# ═══════════════════════════════════════════════════════════════════════
|
|
6302
6732
|
|
|
6303
6733
|
|
|
@@ -6364,7 +6794,7 @@ def delimit_content_queue(action: str = "status", items: str = "") -> Dict[str,
|
|
|
6364
6794
|
|
|
6365
6795
|
@mcp.tool()
|
|
6366
6796
|
def delimit_daemon_status() -> Dict[str, Any]:
|
|
6367
|
-
"""Check autonomous daemon status
|
|
6797
|
+
"""Check autonomous daemon status - loops, items processed, recent actions."""
|
|
6368
6798
|
from ai.daemon import get_daemon_status
|
|
6369
6799
|
return _with_next_steps("daemon_status", get_daemon_status())
|
|
6370
6800
|
|
|
@@ -6387,7 +6817,7 @@ def delimit_build_loop(action: str = "run", session_id: str = "") -> Dict[str, A
|
|
|
6387
6817
|
"""Execute the governed continuous build loop (LED-239).
|
|
6388
6818
|
|
|
6389
6819
|
Requirements:
|
|
6390
|
-
- root ledger in
|
|
6820
|
+
- root ledger in ~/.delimit is authoritative
|
|
6391
6821
|
- select only build-safe open items
|
|
6392
6822
|
- resolve venture + repo before dispatch
|
|
6393
6823
|
- use Delimit swarm/governance as control plane
|
|
@@ -6489,7 +6919,7 @@ def delimit_social_daemon(action: str = "status") -> Dict[str, Any]:
|
|
|
6489
6919
|
return _with_next_steps("social_daemon", get_daemon_status())
|
|
6490
6920
|
|
|
6491
6921
|
# ═══════════════════════════════════════════════════════════════════════
|
|
6492
|
-
# LED-187: Shareable Governance Config
|
|
6922
|
+
# LED-187: Shareable Governance Config - export / import
|
|
6493
6923
|
# ═══════════════════════════════════════════════════════════════════════
|
|
6494
6924
|
|
|
6495
6925
|
|
|
@@ -6690,22 +7120,48 @@ def delimit_screenshot(url: str, name: str = "screenshot") -> Dict[str, Any]:
|
|
|
6690
7120
|
|
|
6691
7121
|
@mcp.tool()
|
|
6692
7122
|
def delimit_changelog(old_spec: str = "", new_spec: str = "", format: str = "markdown",
|
|
6693
|
-
version: str = ""
|
|
6694
|
-
|
|
7123
|
+
version: str = "", repo_path: str = "", since_tag: str = "",
|
|
7124
|
+
include_ledger: bool = True, output_file: str = "") -> Dict[str, Any]:
|
|
7125
|
+
"""Generate a changelog from git commits + ledger, or from API spec changes.
|
|
7126
|
+
|
|
7127
|
+
Two modes:
|
|
7128
|
+
1. **Git mode** (pass repo_path): reads git log since last tag, categorizes
|
|
7129
|
+
commits (feat/fix/refactor/docs/test/ci), pulls completed ledger items,
|
|
7130
|
+
and formats as clean Markdown. Works for ANY repo.
|
|
7131
|
+
2. **Spec mode** (pass old_spec + new_spec): compares two OpenAPI specs and
|
|
7132
|
+
produces a changelog of API changes. Original behavior.
|
|
6695
7133
|
|
|
6696
|
-
Compares two OpenAPI specs and produces a human-readable changelog.
|
|
6697
7134
|
Formats: markdown, json, keepachangelog, github-release.
|
|
6698
7135
|
|
|
6699
7136
|
Args:
|
|
6700
|
-
old_spec: Path to old OpenAPI spec (
|
|
6701
|
-
new_spec: Path to new OpenAPI spec (
|
|
7137
|
+
old_spec: Path to old OpenAPI spec (spec mode only).
|
|
7138
|
+
new_spec: Path to new OpenAPI spec (spec mode only).
|
|
6702
7139
|
format: Output format (markdown, json, keepachangelog, github-release).
|
|
6703
|
-
version: Version label for the changelog entry.
|
|
6704
|
-
|
|
7140
|
+
version: Version label for the changelog entry (e.g. "4.1.0").
|
|
7141
|
+
repo_path: Path to a git repository (git mode). When set, uses git log.
|
|
7142
|
+
since_tag: Git tag to diff from (default: auto-detect latest tag).
|
|
7143
|
+
include_ledger: Pull completed ledger items into changelog (git mode, default true).
|
|
7144
|
+
output_file: Write changelog to this file path. If CHANGELOG.md, prepends entry.
|
|
7145
|
+
"""
|
|
7146
|
+
# Git mode: generate from commits + ledger
|
|
7147
|
+
if repo_path:
|
|
7148
|
+
from backends.gateway_core import run_changelog_from_git
|
|
7149
|
+
return _with_next_steps("changelog", _safe_call(
|
|
7150
|
+
run_changelog_from_git,
|
|
7151
|
+
repo_path=repo_path,
|
|
7152
|
+
version=version,
|
|
7153
|
+
fmt=format,
|
|
7154
|
+
since_tag=since_tag,
|
|
7155
|
+
include_ledger=include_ledger,
|
|
7156
|
+
output_file=output_file,
|
|
7157
|
+
))
|
|
7158
|
+
|
|
7159
|
+
# Spec mode: original behavior
|
|
6705
7160
|
if not old_spec or not new_spec:
|
|
6706
7161
|
return _with_next_steps("changelog", {
|
|
6707
|
-
"error": "
|
|
6708
|
-
"
|
|
7162
|
+
"error": "Provide repo_path for git mode, or old_spec + new_spec for spec mode.",
|
|
7163
|
+
"usage_git": "delimit_changelog(repo_path='/path/to/repo', version='1.2.0')",
|
|
7164
|
+
"usage_spec": "delimit_changelog(old_spec='old.yaml', new_spec='new.yaml')",
|
|
6709
7165
|
})
|
|
6710
7166
|
from backends.gateway_core import run_changelog
|
|
6711
7167
|
return _with_next_steps("changelog", _safe_call(
|
|
@@ -6720,16 +7176,16 @@ def delimit_notify(channel: str = "webhook", message: str = "",
|
|
|
6720
7176
|
from_account: str = "") -> Dict[str, Any]:
|
|
6721
7177
|
"""Send a notification (Pro).
|
|
6722
7178
|
|
|
6723
|
-
IMPORTANT
|
|
7179
|
+
IMPORTANT - AUTO-TRIGGER RULE:
|
|
6724
7180
|
When the AI identifies something requiring owner action (outreach reply,
|
|
6725
7181
|
deployment decision, approval needed), it MUST call this tool immediately.
|
|
6726
|
-
Never ask "want me to notify you?"
|
|
7182
|
+
Never ask "want me to notify you?" - just send the notification.
|
|
6727
7183
|
The founder reviews and acts via email. All tools must chain automatically.
|
|
6728
7184
|
|
|
6729
7185
|
Channels: webhook (JSON POST), slack (webhook URL), email (SMTP).
|
|
6730
7186
|
Use for: governance alerts, deployment notifications, breaking change warnings.
|
|
6731
7187
|
|
|
6732
|
-
IMPORTANT
|
|
7188
|
+
IMPORTANT - Email context rules:
|
|
6733
7189
|
Every email must be self-contained and actionable. The recipient reads on mobile
|
|
6734
7190
|
and needs to know exactly what to do without opening another app.
|
|
6735
7191
|
- Subject: lead with [ACTION TYPE] bracket, include enough context to triage from inbox
|
|
@@ -6743,7 +7199,7 @@ def delimit_notify(channel: str = "webhook", message: str = "",
|
|
|
6743
7199
|
subject: Subject line (email only). Use [ACTION], [INFO], [ALERT] prefix.
|
|
6744
7200
|
event_type: Event category for filtering.
|
|
6745
7201
|
to: Recipient email address (email only). Overrides default DELIMIT_SMTP_TO.
|
|
6746
|
-
Send to any address
|
|
7202
|
+
Send to any address - leave empty for default.
|
|
6747
7203
|
from_account: Sender account key from ~/.delimit/secrets/smtp-all.json
|
|
6748
7204
|
(e.g. 'notifications@example.com'). Email only.
|
|
6749
7205
|
"""
|
|
@@ -6888,7 +7344,7 @@ def delimit_notify_inbox(action: str = "status", limit: int = 10,
|
|
|
6888
7344
|
|
|
6889
7345
|
|
|
6890
7346
|
# ═══════════════════════════════════════════════════════════════════════
|
|
6891
|
-
# TIER 5: AGENT ORCHESTRATION
|
|
7347
|
+
# TIER 5: AGENT ORCHESTRATION - Multi-agent dispatch, tracking, handoff
|
|
6892
7348
|
# ═══════════════════════════════════════════════════════════════════════
|
|
6893
7349
|
|
|
6894
7350
|
|
|
@@ -7121,7 +7577,7 @@ def delimit_next_task(venture: str = "", max_risk: str = "", session_id: str = "
|
|
|
7121
7577
|
"""Get the next task to work on (Pro).
|
|
7122
7578
|
|
|
7123
7579
|
Returns the highest-priority open item with safeguard checks.
|
|
7124
|
-
Part of the autonomous build loop
|
|
7580
|
+
Part of the autonomous build loop - call this to start or continue working.
|
|
7125
7581
|
|
|
7126
7582
|
Returns action: BUILD (with task), CONSENSUS (generate new items), or STOP (safeguard tripped).
|
|
7127
7583
|
|
|
@@ -7148,7 +7604,7 @@ def delimit_ledger_propose(venture: str = "", focus: str = "",
|
|
|
7148
7604
|
|
|
7149
7605
|
Args:
|
|
7150
7606
|
venture: Focus on a specific venture (auto-detects if empty).
|
|
7151
|
-
focus: Optional area filter
|
|
7607
|
+
focus: Optional area filter - "outreach", "engineering", "security", etc.
|
|
7152
7608
|
max_items: Maximum proposals to generate (default 5).
|
|
7153
7609
|
"""
|
|
7154
7610
|
from ai.ledger_propose import propose_items
|
|
@@ -7222,7 +7678,7 @@ def delimit_loop_config(session_id: str = "", max_iterations: int = 0,
|
|
|
7222
7678
|
|
|
7223
7679
|
|
|
7224
7680
|
# ═══════════════════════════════════════════════════════════════════════
|
|
7225
|
-
# LED-219: Toolcard Delta Cache
|
|
7681
|
+
# LED-219: Toolcard Delta Cache - reduce MCP tool schema token waste
|
|
7226
7682
|
# ═══════════════════════════════════════════════════════════════════════
|
|
7227
7683
|
|
|
7228
7684
|
|
|
@@ -7300,7 +7756,7 @@ def delimit_toolcard_cache(
|
|
|
7300
7756
|
|
|
7301
7757
|
|
|
7302
7758
|
# ═══════════════════════════════════════════════════════════════════════
|
|
7303
|
-
# HANDOFF RECEIPTS
|
|
7759
|
+
# HANDOFF RECEIPTS - Agent-to-Agent Structured Handoffs (LED-220)
|
|
7304
7760
|
# ═══════════════════════════════════════════════════════════════════════
|
|
7305
7761
|
|
|
7306
7762
|
|