delimit-cli 3.14.16 → 3.14.17
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/bin/delimit-cli.js +3 -0
- package/gateway/ai/agent_dispatch.py +458 -0
- package/gateway/ai/agent_policy.py +230 -0
- package/gateway/ai/drift_monitor.py +246 -0
- package/gateway/ai/server.py +791 -38
- package/lib/cross-model-hooks.js +95 -3
- package/package.json +1 -1
package/gateway/ai/server.py
CHANGED
|
@@ -651,6 +651,23 @@ NEXT_STEPS_REGISTRY: Dict[str, List[Dict[str, Any]]] = {
|
|
|
651
651
|
"agent_handoff": [
|
|
652
652
|
{"tool": "delimit_agent_status", "reason": "Verify the handoff was recorded", "suggested_args": {}, "is_premium": True},
|
|
653
653
|
],
|
|
654
|
+
"agent_link": [
|
|
655
|
+
{"tool": "delimit_agent_dashboard", "reason": "View the updated agent dashboard", "suggested_args": {}, "is_premium": True},
|
|
656
|
+
],
|
|
657
|
+
"agent_dashboard": [
|
|
658
|
+
{"tool": "delimit_agent_dispatch", "reason": "Dispatch a new task to an agent", "suggested_args": {}, "is_premium": True},
|
|
659
|
+
],
|
|
660
|
+
"agent_policy": [
|
|
661
|
+
{"tool": "delimit_agent_check", "reason": "Verify a model's permission for an action", "suggested_args": {}, "is_premium": True},
|
|
662
|
+
{"tool": "delimit_agent_dashboard", "reason": "View agent orchestration status", "suggested_args": {}, "is_premium": True},
|
|
663
|
+
],
|
|
664
|
+
"agent_check": [
|
|
665
|
+
{"tool": "delimit_agent_policy", "reason": "Update the model's policy if needed", "suggested_args": {}, "is_premium": True},
|
|
666
|
+
],
|
|
667
|
+
"drift_check": [
|
|
668
|
+
{"tool": "delimit_lint", "reason": "Run lint to review detected drift", "suggested_args": {}, "is_premium": False},
|
|
669
|
+
{"tool": "delimit_notify", "reason": "Alert team about detected drift", "suggested_args": {}, "is_premium": True},
|
|
670
|
+
],
|
|
654
671
|
# --- Autonomous Build Loop (Pro) ---
|
|
655
672
|
"next_task": [
|
|
656
673
|
{"tool": "delimit_task_complete", "reason": "Mark the task done when finished", "suggested_args": {}, "is_premium": True},
|
|
@@ -834,6 +851,11 @@ NEXT_STEPS_REGISTRY: Dict[str, List[Dict[str, Any]]] = {
|
|
|
834
851
|
"social_approve": [
|
|
835
852
|
{"tool": "delimit_social_history", "reason": "Review post history after approval", "suggested_args": {"limit": 5}, "is_premium": True},
|
|
836
853
|
],
|
|
854
|
+
"social_target": [
|
|
855
|
+
{"tool": "delimit_social_post", "reason": "Draft a reply for a discovered target", "suggested_args": {"draft": True}, "is_premium": True},
|
|
856
|
+
{"tool": "delimit_social_target", "reason": "Re-scan for new targets", "suggested_args": {"action": "scan"}, "is_premium": True},
|
|
857
|
+
{"tool": "delimit_social_target", "reason": "View target stats", "suggested_args": {"action": "stats"}, "is_premium": True},
|
|
858
|
+
],
|
|
837
859
|
# --- Content Engine ---
|
|
838
860
|
"content_schedule": [
|
|
839
861
|
{"tool": "delimit_content_publish", "reason": "Publish next queued content", "suggested_args": {"content_type": "tweet"}, "is_premium": True},
|
|
@@ -1108,17 +1130,32 @@ def _detect_environment() -> Dict[str, Any]:
|
|
|
1108
1130
|
}
|
|
1109
1131
|
|
|
1110
1132
|
|
|
1133
|
+
_inbox_daemon_autostarted = False
|
|
1134
|
+
|
|
1135
|
+
|
|
1111
1136
|
def _with_next_steps(tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
1112
1137
|
"""Route every tool result through governance. This IS the loop.
|
|
1113
1138
|
|
|
1114
1139
|
The governance loop:
|
|
1115
|
-
1.
|
|
1116
|
-
2.
|
|
1117
|
-
3.
|
|
1118
|
-
4. Check
|
|
1119
|
-
5.
|
|
1120
|
-
6.
|
|
1140
|
+
1. Auto-start inbox daemon on first tool call (model-agnostic)
|
|
1141
|
+
2. Emit event for dashboard tracking
|
|
1142
|
+
3. STR-052: Policy kernel gate (blocks high-risk actions without approval)
|
|
1143
|
+
4. Check Pro license gate (blocks if not authorized)
|
|
1144
|
+
5. Check result against rules (thresholds, policies)
|
|
1145
|
+
6. Auto-create ledger items for failures/warnings
|
|
1146
|
+
7. Route back to delimit_ledger_context (the loop continues)
|
|
1121
1147
|
"""
|
|
1148
|
+
# Auto-start inbox daemon on first tool call — works for ALL models
|
|
1149
|
+
global _inbox_daemon_autostarted
|
|
1150
|
+
if not _inbox_daemon_autostarted:
|
|
1151
|
+
_inbox_daemon_autostarted = True
|
|
1152
|
+
try:
|
|
1153
|
+
from ai.inbox_daemon import start_daemon
|
|
1154
|
+
start_daemon()
|
|
1155
|
+
logger.info("Inbox daemon auto-started on first tool call")
|
|
1156
|
+
except Exception as e:
|
|
1157
|
+
logger.warning("Inbox daemon auto-start failed: %s", e)
|
|
1158
|
+
|
|
1122
1159
|
# Emit event for real-time dashboard
|
|
1123
1160
|
_emit_event(tool_name, result)
|
|
1124
1161
|
|
|
@@ -1191,6 +1228,18 @@ def delimit_lint(old_spec: str, new_spec: str, policy_file: Optional[str] = None
|
|
|
1191
1228
|
|
|
1192
1229
|
bump = str(semver_result.get("bump", "")).upper()
|
|
1193
1230
|
|
|
1231
|
+
# Step 2b: Impact-based notification routing (LED-233, non-blocking)
|
|
1232
|
+
try:
|
|
1233
|
+
from ai.notify import route_by_impact
|
|
1234
|
+
all_changes = lint_result.get("all_changes", lint_result.get("violations", []))
|
|
1235
|
+
if all_changes:
|
|
1236
|
+
routing_result = route_by_impact(all_changes, dry_run=False)
|
|
1237
|
+
chain["steps"].append({"step": "impact_routing", "ok": True})
|
|
1238
|
+
lint_result["impact_routing"] = routing_result
|
|
1239
|
+
except Exception as e:
|
|
1240
|
+
logger.debug("Impact routing non-fatal error: %s", e)
|
|
1241
|
+
chain["steps"].append({"step": "impact_routing", "ok": False, "error": str(e)})
|
|
1242
|
+
|
|
1194
1243
|
if bump != "MAJOR":
|
|
1195
1244
|
chain["status"] = f"complete_{bump.lower() or 'none'}"
|
|
1196
1245
|
lint_result["chain"] = chain
|
|
@@ -1962,6 +2011,7 @@ def delimit_deploy_publish(app: str = "", git_ref: Optional[str] = None) -> Dict
|
|
|
1962
2011
|
|
|
1963
2012
|
|
|
1964
2013
|
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2014
|
+
@mcp.tool()
|
|
1965
2015
|
def delimit_deploy_verify(app: str = "", env: str = "", git_ref: Optional[str] = None) -> Dict[str, Any]:
|
|
1966
2016
|
"""Verify deployment health (experimental) (Pro)."""
|
|
1967
2017
|
return _delimit_deploy_impl(action="verify", app=app, env=env, git_ref=git_ref)
|
|
@@ -2064,6 +2114,7 @@ def delimit_intel_query(
|
|
|
2064
2114
|
# ─── Generate ───────────────────────────────────────────────────────────
|
|
2065
2115
|
|
|
2066
2116
|
@_internal_tool()
|
|
2117
|
+
@mcp.tool()
|
|
2067
2118
|
def delimit_generate_template(
|
|
2068
2119
|
template_type: str,
|
|
2069
2120
|
name: str,
|
|
@@ -2090,6 +2141,7 @@ def delimit_generate_template(
|
|
|
2090
2141
|
|
|
2091
2142
|
|
|
2092
2143
|
@_internal_tool()
|
|
2144
|
+
@mcp.tool()
|
|
2093
2145
|
def delimit_generate_scaffold(
|
|
2094
2146
|
project_type: str,
|
|
2095
2147
|
name: str,
|
|
@@ -2113,6 +2165,7 @@ def delimit_generate_scaffold(
|
|
|
2113
2165
|
# ─── Repo (RepoDoctor + ConfigSentry) ──────────────────────────────────
|
|
2114
2166
|
|
|
2115
2167
|
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2168
|
+
@mcp.tool()
|
|
2116
2169
|
def delimit_repo_diagnose(target: str = ".") -> Dict[str, Any]:
|
|
2117
2170
|
"""Diagnose repository health issues (experimental) (Pro).
|
|
2118
2171
|
|
|
@@ -2128,6 +2181,7 @@ def delimit_repo_diagnose(target: str = ".") -> Dict[str, Any]:
|
|
|
2128
2181
|
|
|
2129
2182
|
|
|
2130
2183
|
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2184
|
+
@mcp.tool()
|
|
2131
2185
|
def delimit_repo_analyze(target: str = ".") -> Dict[str, Any]:
|
|
2132
2186
|
"""Analyze repository structure and quality (experimental) (Pro).
|
|
2133
2187
|
|
|
@@ -2143,6 +2197,7 @@ def delimit_repo_analyze(target: str = ".") -> Dict[str, Any]:
|
|
|
2143
2197
|
|
|
2144
2198
|
|
|
2145
2199
|
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2200
|
+
@mcp.tool()
|
|
2146
2201
|
def delimit_repo_config_validate(target: str = ".") -> Dict[str, Any]:
|
|
2147
2202
|
"""Validate configuration files (experimental) (Pro).
|
|
2148
2203
|
|
|
@@ -2158,6 +2213,7 @@ def delimit_repo_config_validate(target: str = ".") -> Dict[str, Any]:
|
|
|
2158
2213
|
|
|
2159
2214
|
|
|
2160
2215
|
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2216
|
+
@mcp.tool()
|
|
2161
2217
|
def delimit_repo_config_audit(target: str = ".") -> Dict[str, Any]:
|
|
2162
2218
|
"""Audit configuration compliance (experimental) (Pro).
|
|
2163
2219
|
|
|
@@ -2803,12 +2859,14 @@ def delimit_release_status(environment: str = "production") -> Dict[str, Any]:
|
|
|
2803
2859
|
|
|
2804
2860
|
|
|
2805
2861
|
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2862
|
+
@mcp.tool()
|
|
2806
2863
|
def delimit_release_rollback(environment: str, version: str, to_version: str) -> Dict[str, Any]:
|
|
2807
2864
|
"""Rollback deployment to previous version (experimental)."""
|
|
2808
2865
|
return _delimit_release_impl(action="rollback", environment=environment, version=version, to_version=to_version)
|
|
2809
2866
|
|
|
2810
2867
|
|
|
2811
2868
|
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
2869
|
+
@mcp.tool()
|
|
2812
2870
|
def delimit_release_history(environment: str, limit: int = 10) -> Dict[str, Any]:
|
|
2813
2871
|
"""Show release history (experimental)."""
|
|
2814
2872
|
return _delimit_release_impl(action="history", environment=environment, limit=limit)
|
|
@@ -2912,6 +2970,7 @@ def delimit_cost_controls(
|
|
|
2912
2970
|
# ─── DataSteward (Governance Primitive) ────────────────────────────────
|
|
2913
2971
|
|
|
2914
2972
|
@_internal_tool()
|
|
2973
|
+
@mcp.tool()
|
|
2915
2974
|
def delimit_data_validate(target: str = ".") -> Dict[str, Any]:
|
|
2916
2975
|
"""Validate data files: JSON parse, CSV structure, SQLite integrity check.
|
|
2917
2976
|
|
|
@@ -2923,6 +2982,7 @@ def delimit_data_validate(target: str = ".") -> Dict[str, Any]:
|
|
|
2923
2982
|
|
|
2924
2983
|
|
|
2925
2984
|
@_internal_tool()
|
|
2985
|
+
@mcp.tool()
|
|
2926
2986
|
def delimit_data_migrate(target: str = ".") -> Dict[str, Any]:
|
|
2927
2987
|
"""Check for migration files (alembic, Django, Prisma, Knex) and report status.
|
|
2928
2988
|
|
|
@@ -2934,6 +2994,7 @@ def delimit_data_migrate(target: str = ".") -> Dict[str, Any]:
|
|
|
2934
2994
|
|
|
2935
2995
|
|
|
2936
2996
|
@_internal_tool()
|
|
2997
|
+
@mcp.tool()
|
|
2937
2998
|
def delimit_data_backup(target: str = ".") -> Dict[str, Any]:
|
|
2938
2999
|
"""Back up SQLite and JSON data files to ~/.delimit/backups/ with timestamp.
|
|
2939
3000
|
|
|
@@ -3024,6 +3085,7 @@ def delimit_obs_logs(query: str, time_range: str = "1h", source: Optional[str] =
|
|
|
3024
3085
|
|
|
3025
3086
|
|
|
3026
3087
|
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
3088
|
+
@mcp.tool()
|
|
3027
3089
|
def delimit_obs_alerts(action: str, alert_rule: Optional[Dict[str, Any]] = None, rule_id: Optional[str] = None) -> Dict[str, Any]:
|
|
3028
3090
|
"""Manage alerting rules (experimental)."""
|
|
3029
3091
|
return _delimit_obs_impl(action="alerts", alert_action=action, alert_rule=alert_rule, rule_id=rule_id)
|
|
@@ -3038,6 +3100,7 @@ def delimit_obs_status() -> Dict[str, Any]:
|
|
|
3038
3100
|
# ─── DesignSystem (UI Tooling) ──────────────────────────────────────────
|
|
3039
3101
|
|
|
3040
3102
|
@_internal_tool()
|
|
3103
|
+
@mcp.tool()
|
|
3041
3104
|
def delimit_design_extract_tokens(
|
|
3042
3105
|
figma_file_key: Optional[str] = None,
|
|
3043
3106
|
token_types: Optional[Union[str, List[str]]] = None,
|
|
@@ -3063,6 +3126,7 @@ def delimit_design_extract_tokens(
|
|
|
3063
3126
|
|
|
3064
3127
|
|
|
3065
3128
|
@_internal_tool()
|
|
3129
|
+
@mcp.tool()
|
|
3066
3130
|
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]:
|
|
3067
3131
|
"""Generate a React/Next.js component skeleton with props interface and Tailwind support.
|
|
3068
3132
|
|
|
@@ -3077,6 +3141,7 @@ def delimit_design_generate_component(component_name: str, figma_node_id: Option
|
|
|
3077
3141
|
|
|
3078
3142
|
|
|
3079
3143
|
@_internal_tool()
|
|
3144
|
+
@mcp.tool()
|
|
3080
3145
|
def delimit_design_generate_tailwind(figma_file_key: Optional[str] = None, output_path: Optional[str] = None, project_path: Optional[str] = None) -> Dict[str, Any]:
|
|
3081
3146
|
"""Read existing tailwind.config or generate one from detected CSS tokens.
|
|
3082
3147
|
|
|
@@ -3090,6 +3155,7 @@ def delimit_design_generate_tailwind(figma_file_key: Optional[str] = None, outpu
|
|
|
3090
3155
|
|
|
3091
3156
|
|
|
3092
3157
|
@_ops_pack_tool()
|
|
3158
|
+
@mcp.tool()
|
|
3093
3159
|
def delimit_design_validate_responsive(
|
|
3094
3160
|
project_path: str,
|
|
3095
3161
|
check_types: Optional[Union[str, List[str]]] = None,
|
|
@@ -3111,6 +3177,7 @@ def delimit_design_validate_responsive(
|
|
|
3111
3177
|
|
|
3112
3178
|
|
|
3113
3179
|
@_internal_tool()
|
|
3180
|
+
@mcp.tool()
|
|
3114
3181
|
def delimit_design_component_library(project_path: str, output_format: str = "json") -> Dict[str, Any]:
|
|
3115
3182
|
"""Scan for React/Vue/Svelte components and generate a component catalog.
|
|
3116
3183
|
|
|
@@ -3125,6 +3192,7 @@ def delimit_design_component_library(project_path: str, output_format: str = "js
|
|
|
3125
3192
|
# ─── Story (Component Stories + Visual/A11y Testing) ────────────────────
|
|
3126
3193
|
|
|
3127
3194
|
@_internal_tool()
|
|
3195
|
+
@mcp.tool()
|
|
3128
3196
|
def delimit_story_generate(
|
|
3129
3197
|
component_path: str,
|
|
3130
3198
|
story_name: Optional[str] = None,
|
|
@@ -3146,6 +3214,7 @@ def delimit_story_generate(
|
|
|
3146
3214
|
|
|
3147
3215
|
|
|
3148
3216
|
@_internal_tool()
|
|
3217
|
+
@mcp.tool()
|
|
3149
3218
|
def delimit_story_visual_test(url: str, project_path: Optional[str] = None, threshold: float = 0.05) -> Dict[str, Any]:
|
|
3150
3219
|
"""Run visual regression test -- screenshot and compare to baseline.
|
|
3151
3220
|
|
|
@@ -3163,6 +3232,7 @@ def delimit_story_visual_test(url: str, project_path: Optional[str] = None, thre
|
|
|
3163
3232
|
|
|
3164
3233
|
|
|
3165
3234
|
@_internal_tool() # Was experimental (LED-044), promoted to internal (Consensus 120)
|
|
3235
|
+
@mcp.tool()
|
|
3166
3236
|
def delimit_story_build(project_path: str, output_dir: Optional[str] = None) -> Dict[str, Any]:
|
|
3167
3237
|
"""Build Storybook static site.
|
|
3168
3238
|
|
|
@@ -3178,6 +3248,7 @@ def delimit_story_build(project_path: str, output_dir: Optional[str] = None) ->
|
|
|
3178
3248
|
|
|
3179
3249
|
|
|
3180
3250
|
@_internal_tool()
|
|
3251
|
+
@mcp.tool()
|
|
3181
3252
|
def delimit_story_accessibility(project_path: str, standards: str = "WCAG2AA") -> Dict[str, Any]:
|
|
3182
3253
|
"""Run WCAG accessibility checks by scanning HTML/JSX/TSX for common issues.
|
|
3183
3254
|
|
|
@@ -3210,6 +3281,7 @@ def delimit_test_generate(project_path: str, source_files: Optional[List[str]] =
|
|
|
3210
3281
|
|
|
3211
3282
|
|
|
3212
3283
|
@_experimental_tool() # HIDDEN: stub/pass-through (LED-044)
|
|
3284
|
+
@mcp.tool()
|
|
3213
3285
|
def delimit_test_coverage(project_path: str, threshold: int = 80) -> Dict[str, Any]:
|
|
3214
3286
|
"""Analyze test coverage (experimental) (Pro).
|
|
3215
3287
|
|
|
@@ -3243,6 +3315,7 @@ def delimit_test_smoke(project_path: str, test_suite: Optional[str] = None) -> D
|
|
|
3243
3315
|
# ─── Docs (Real implementations) ─────────────────────────────────────
|
|
3244
3316
|
|
|
3245
3317
|
@_ops_pack_tool()
|
|
3318
|
+
@mcp.tool()
|
|
3246
3319
|
def delimit_docs_generate(target: str = ".") -> Dict[str, Any]:
|
|
3247
3320
|
"""Generate API reference documentation for a project.
|
|
3248
3321
|
|
|
@@ -3768,6 +3841,56 @@ def delimit_ledger_add(
|
|
|
3768
3841
|
return _with_next_steps("ledger_add", result)
|
|
3769
3842
|
|
|
3770
3843
|
|
|
3844
|
+
@mcp.tool()
|
|
3845
|
+
def delimit_ledger_update(
|
|
3846
|
+
item_id: str,
|
|
3847
|
+
venture: str = "",
|
|
3848
|
+
status: str = "",
|
|
3849
|
+
priority: str = "",
|
|
3850
|
+
title: str = "",
|
|
3851
|
+
description: str = "",
|
|
3852
|
+
note: str = "",
|
|
3853
|
+
assignee: str = "",
|
|
3854
|
+
due_date: str = "",
|
|
3855
|
+
labels: Optional[Union[str, List[str]]] = None,
|
|
3856
|
+
blocked_by: str = "",
|
|
3857
|
+
blocks: str = "",
|
|
3858
|
+
) -> Dict[str, Any]:
|
|
3859
|
+
"""Update any field on a ledger item.
|
|
3860
|
+
|
|
3861
|
+
Supports: status, priority, title, description, assignee, due date, labels,
|
|
3862
|
+
and dependency links (blocked_by, blocks). Pass only the fields you want to change.
|
|
3863
|
+
|
|
3864
|
+
Args:
|
|
3865
|
+
item_id: The item ID (e.g. LED-001 or STR-001).
|
|
3866
|
+
venture: Project name or path. Auto-detects if empty.
|
|
3867
|
+
status: New status — "open", "in_progress", "blocked", "done".
|
|
3868
|
+
priority: New priority — "P0", "P1", "P2".
|
|
3869
|
+
title: New title.
|
|
3870
|
+
description: New description.
|
|
3871
|
+
note: Add a note/comment to the item.
|
|
3872
|
+
assignee: Assign to a person or agent (e.g. "founder", "claude", "codex").
|
|
3873
|
+
due_date: Due date in ISO format (e.g. "2026-04-01").
|
|
3874
|
+
labels: Labels/tags (e.g. ["dashboard", "ux"] or "dashboard,ux").
|
|
3875
|
+
blocked_by: Item ID that blocks this item (e.g. "LED-025").
|
|
3876
|
+
blocks: Item ID that this item blocks (e.g. "STR-005").
|
|
3877
|
+
"""
|
|
3878
|
+
try:
|
|
3879
|
+
labels = _coerce_list_arg(labels, "labels") if labels else None
|
|
3880
|
+
except ValueError:
|
|
3881
|
+
labels = None
|
|
3882
|
+
from ai.ledger_manager import update_item
|
|
3883
|
+
project = _resolve_venture(venture)
|
|
3884
|
+
result = update_item(
|
|
3885
|
+
item_id=item_id, status=status or None, priority=priority or None,
|
|
3886
|
+
title=title or None, description=description or None, note=note or None,
|
|
3887
|
+
assignee=assignee or None, due_date=due_date or None, labels=labels,
|
|
3888
|
+
blocked_by=blocked_by or None, blocks=blocks or None,
|
|
3889
|
+
project_path=project,
|
|
3890
|
+
)
|
|
3891
|
+
return _with_next_steps("ledger_update", result)
|
|
3892
|
+
|
|
3893
|
+
|
|
3771
3894
|
@mcp.tool()
|
|
3772
3895
|
def delimit_ledger_done(item_id: str, note: str = "", venture: str = "") -> Dict[str, Any]:
|
|
3773
3896
|
"""Mark a ledger item as done.
|
|
@@ -3822,6 +3945,140 @@ def delimit_ledger_context(venture: str = "") -> Dict[str, Any]:
|
|
|
3822
3945
|
return _with_next_steps("ledger_context", result)
|
|
3823
3946
|
|
|
3824
3947
|
|
|
3948
|
+
@mcp.tool()
|
|
3949
|
+
def delimit_ledger_query(
|
|
3950
|
+
query: str,
|
|
3951
|
+
venture: str = "",
|
|
3952
|
+
) -> Dict[str, Any]:
|
|
3953
|
+
"""Ask natural language questions about the ledger (ChatOps 2.0).
|
|
3954
|
+
|
|
3955
|
+
Examples:
|
|
3956
|
+
"what shipped this week?"
|
|
3957
|
+
"what's blocked?"
|
|
3958
|
+
"show me all P0s"
|
|
3959
|
+
"how many items total?"
|
|
3960
|
+
"what should I work on next?"
|
|
3961
|
+
"search for dashboard"
|
|
3962
|
+
|
|
3963
|
+
Args:
|
|
3964
|
+
query: Natural language question about the ledger.
|
|
3965
|
+
venture: Project name or path. Auto-detects if empty.
|
|
3966
|
+
"""
|
|
3967
|
+
from ai.ledger_manager import query_ledger
|
|
3968
|
+
project = _resolve_venture(venture)
|
|
3969
|
+
return query_ledger(query=query, project_path=project)
|
|
3970
|
+
|
|
3971
|
+
|
|
3972
|
+
@mcp.tool()
|
|
3973
|
+
def delimit_ledger_link(
|
|
3974
|
+
from_id: str,
|
|
3975
|
+
to_id: str,
|
|
3976
|
+
link_type: str = "blocks",
|
|
3977
|
+
note: str = "",
|
|
3978
|
+
venture: str = "",
|
|
3979
|
+
) -> Dict[str, Any]:
|
|
3980
|
+
"""Create a relationship between two ledger items.
|
|
3981
|
+
|
|
3982
|
+
Supports: blocks/blocked_by (auto-creates reverse), parent/child,
|
|
3983
|
+
relates_to, duplicates. Use to track dependencies and sub-tasks.
|
|
3984
|
+
|
|
3985
|
+
Args:
|
|
3986
|
+
from_id: Source item ID (e.g. "LED-025").
|
|
3987
|
+
to_id: Target item ID (e.g. "STR-005").
|
|
3988
|
+
link_type: Relationship type — "blocks", "blocked_by", "parent", "child", "relates_to", "duplicates".
|
|
3989
|
+
note: Optional note explaining the relationship.
|
|
3990
|
+
venture: Project name or path. Auto-detects if empty.
|
|
3991
|
+
"""
|
|
3992
|
+
from ai.ledger_manager import link_items
|
|
3993
|
+
project = _resolve_venture(venture)
|
|
3994
|
+
return link_items(from_id=from_id, to_id=to_id, link_type=link_type, note=note, project_path=project)
|
|
3995
|
+
|
|
3996
|
+
|
|
3997
|
+
@mcp.tool()
|
|
3998
|
+
def delimit_ledger_links(
|
|
3999
|
+
item_id: str,
|
|
4000
|
+
venture: str = "",
|
|
4001
|
+
) -> Dict[str, Any]:
|
|
4002
|
+
"""Get all relationships/dependencies for a ledger item.
|
|
4003
|
+
|
|
4004
|
+
Returns all links where this item is either the source or target.
|
|
4005
|
+
Shows: blocks, blocked_by, parent, child, relates_to, duplicates.
|
|
4006
|
+
|
|
4007
|
+
Args:
|
|
4008
|
+
item_id: The item ID to look up links for.
|
|
4009
|
+
venture: Project name or path. Auto-detects if empty.
|
|
4010
|
+
"""
|
|
4011
|
+
from ai.ledger_manager import get_links
|
|
4012
|
+
project = _resolve_venture(venture)
|
|
4013
|
+
return get_links(item_id=item_id, project_path=project)
|
|
4014
|
+
|
|
4015
|
+
|
|
4016
|
+
@mcp.tool()
|
|
4017
|
+
def delimit_session_handoff(
|
|
4018
|
+
summary: str,
|
|
4019
|
+
items_completed: Optional[Union[str, List[str]]] = None,
|
|
4020
|
+
items_added: Optional[Union[str, List[str]]] = None,
|
|
4021
|
+
key_decisions: Optional[Union[str, List[str]]] = None,
|
|
4022
|
+
blockers: Optional[Union[str, List[str]]] = None,
|
|
4023
|
+
files_changed: Optional[Union[str, List[str]]] = None,
|
|
4024
|
+
venture: str = "",
|
|
4025
|
+
) -> Dict[str, Any]:
|
|
4026
|
+
"""Save a session summary for cross-session continuity.
|
|
4027
|
+
|
|
4028
|
+
Call at the end of a productive session so the next session can recover context.
|
|
4029
|
+
Stores: what was completed, what was added, key decisions, blockers, and files changed.
|
|
4030
|
+
|
|
4031
|
+
Args:
|
|
4032
|
+
summary: 2-3 sentence summary of what happened this session.
|
|
4033
|
+
items_completed: List of completed ledger item IDs (e.g. ["LED-164", "LED-165"]).
|
|
4034
|
+
items_added: List of newly added item IDs.
|
|
4035
|
+
key_decisions: Key decisions or consensus results.
|
|
4036
|
+
blockers: What's blocked and why.
|
|
4037
|
+
files_changed: Key files that were modified.
|
|
4038
|
+
venture: Venture context. Auto-detects if empty.
|
|
4039
|
+
"""
|
|
4040
|
+
try:
|
|
4041
|
+
items_completed = _coerce_list_arg(items_completed, "items_completed") if items_completed else None
|
|
4042
|
+
except ValueError:
|
|
4043
|
+
items_completed = None
|
|
4044
|
+
try:
|
|
4045
|
+
items_added = _coerce_list_arg(items_added, "items_added") if items_added else None
|
|
4046
|
+
except ValueError:
|
|
4047
|
+
items_added = None
|
|
4048
|
+
try:
|
|
4049
|
+
key_decisions = _coerce_list_arg(key_decisions, "key_decisions") if key_decisions else None
|
|
4050
|
+
except ValueError:
|
|
4051
|
+
key_decisions = None
|
|
4052
|
+
try:
|
|
4053
|
+
blockers = _coerce_list_arg(blockers, "blockers") if blockers else None
|
|
4054
|
+
except ValueError:
|
|
4055
|
+
blockers = None
|
|
4056
|
+
try:
|
|
4057
|
+
files_changed = _coerce_list_arg(files_changed, "files_changed") if files_changed else None
|
|
4058
|
+
except ValueError:
|
|
4059
|
+
files_changed = None
|
|
4060
|
+
from ai.ledger_manager import session_handoff
|
|
4061
|
+
return session_handoff(
|
|
4062
|
+
summary=summary, items_completed=items_completed, items_added=items_added,
|
|
4063
|
+
key_decisions=key_decisions, blockers=blockers, files_changed=files_changed,
|
|
4064
|
+
venture=venture,
|
|
4065
|
+
)
|
|
4066
|
+
|
|
4067
|
+
|
|
4068
|
+
@mcp.tool()
|
|
4069
|
+
def delimit_session_history(limit: int = 5) -> Dict[str, Any]:
|
|
4070
|
+
"""Load recent session handoffs for context recovery.
|
|
4071
|
+
|
|
4072
|
+
Call at the start of a new session to see what happened previously.
|
|
4073
|
+
Returns the last N session summaries with items completed, decisions, and blockers.
|
|
4074
|
+
|
|
4075
|
+
Args:
|
|
4076
|
+
limit: Number of recent sessions to return (default 5).
|
|
4077
|
+
"""
|
|
4078
|
+
from ai.ledger_manager import session_history
|
|
4079
|
+
return session_history(limit=limit)
|
|
4080
|
+
|
|
4081
|
+
|
|
3825
4082
|
@mcp.tool()
|
|
3826
4083
|
def delimit_ventures() -> Dict[str, Any]:
|
|
3827
4084
|
"""List all registered ventures/projects that Delimit has been used with.
|
|
@@ -3949,6 +4206,18 @@ def delimit_models(
|
|
|
3949
4206
|
return {"error": f"Unknown action '{action}'. Use: list, detect, add, remove"}
|
|
3950
4207
|
|
|
3951
4208
|
|
|
4209
|
+
@mcp.tool()
|
|
4210
|
+
def delimit_deliberation_status() -> Dict[str, Any]:
|
|
4211
|
+
"""Check your deliberation usage and mode (hosted free tier vs BYOK).
|
|
4212
|
+
|
|
4213
|
+
Returns how many free deliberations you have used and remaining,
|
|
4214
|
+
whether you are in hosted (free) or BYOK (bring your own keys) mode,
|
|
4215
|
+
and your total deliberation count.
|
|
4216
|
+
"""
|
|
4217
|
+
from ai.deliberation import get_deliberation_status
|
|
4218
|
+
return get_deliberation_status()
|
|
4219
|
+
|
|
4220
|
+
|
|
3952
4221
|
@mcp.tool()
|
|
3953
4222
|
def delimit_deliberate(
|
|
3954
4223
|
question: str,
|
|
@@ -3960,6 +4229,8 @@ def delimit_deliberate(
|
|
|
3960
4229
|
"""Run multi-model consensus via real AI-to-AI deliberation (Pro).
|
|
3961
4230
|
|
|
3962
4231
|
Models (Grok 4, Gemini, Codex) debate each other directly until unanimous agreement.
|
|
4232
|
+
Free tier: 3 deliberations using hosted keys, no setup required.
|
|
4233
|
+
BYOK: configure your own API keys in ~/.delimit/models.json for unlimited use.
|
|
3963
4234
|
|
|
3964
4235
|
Args:
|
|
3965
4236
|
question: The question to reach consensus on.
|
|
@@ -4087,6 +4358,36 @@ def delimit_release_sync(action: str = "audit") -> Dict[str, Any]:
|
|
|
4087
4358
|
return _delimit_release_impl(action="sync", sync_action=action)
|
|
4088
4359
|
|
|
4089
4360
|
|
|
4361
|
+
@mcp.tool()
|
|
4362
|
+
def delimit_drift_check(spec_path: str = "", project_path: str = ".",
|
|
4363
|
+
staleness_days: int = 7) -> Dict[str, Any]:
|
|
4364
|
+
"""Check for API spec drift since last governance review.
|
|
4365
|
+
|
|
4366
|
+
Detects: spec changed without lint, stale baseline, missing policy.
|
|
4367
|
+
Run on a schedule (cron) for continuous compliance monitoring.
|
|
4368
|
+
|
|
4369
|
+
Args:
|
|
4370
|
+
spec_path: Path to OpenAPI spec. Auto-detects if empty.
|
|
4371
|
+
project_path: Project root. Defaults to current directory.
|
|
4372
|
+
staleness_days: Alert if baseline older than this (default 7).
|
|
4373
|
+
"""
|
|
4374
|
+
from ai.drift_monitor import check_drift
|
|
4375
|
+
result = _safe_call(check_drift, spec_path=spec_path, project_path=project_path,
|
|
4376
|
+
staleness_days=staleness_days)
|
|
4377
|
+
return _with_next_steps("drift_check", result)
|
|
4378
|
+
|
|
4379
|
+
|
|
4380
|
+
@mcp.tool()
|
|
4381
|
+
def delimit_drift_history(limit: int = 20) -> Dict[str, Any]:
|
|
4382
|
+
"""View recent drift check history.
|
|
4383
|
+
|
|
4384
|
+
Args:
|
|
4385
|
+
limit: Max entries to return.
|
|
4386
|
+
"""
|
|
4387
|
+
from ai.drift_monitor import get_drift_history
|
|
4388
|
+
return _safe_call(get_drift_history, limit=limit)
|
|
4389
|
+
|
|
4390
|
+
|
|
4090
4391
|
@mcp.tool()
|
|
4091
4392
|
def delimit_scan(project_path: str = ".") -> Dict[str, Any]:
|
|
4092
4393
|
"""Scan a project and show what Delimit can do for it.
|
|
@@ -4808,16 +5109,33 @@ def delimit_webhook_manage(
|
|
|
4808
5109
|
|
|
4809
5110
|
|
|
4810
5111
|
@_ops_pack_tool()
|
|
5112
|
+
@mcp.tool()
|
|
4811
5113
|
def delimit_social_post(text: str = "", category: str = "", platform: str = "twitter",
|
|
4812
5114
|
account: str = "", quote_tweet_id: str = "",
|
|
4813
|
-
reply_to_id: str = "", draft: bool = False
|
|
5115
|
+
reply_to_id: str = "", draft: bool = False,
|
|
5116
|
+
context: str = "") -> Dict[str, Any]:
|
|
4814
5117
|
"""Post to social media (Pro).
|
|
4815
5118
|
|
|
5119
|
+
IMPORTANT — AUTO-TRIGGER RULE:
|
|
5120
|
+
When the AI drafts a social post, it MUST call this tool immediately.
|
|
5121
|
+
Never ask "want me to send this?" — just call the tool. The email-based
|
|
5122
|
+
approval flow handles human review. The AI's job is to draft and send,
|
|
5123
|
+
the founder's job is to approve/reject via email reply.
|
|
5124
|
+
|
|
4816
5125
|
Categories: tip, changelog, insight, engagement.
|
|
4817
5126
|
Leave text empty to auto-generate from templates.
|
|
4818
5127
|
Every post provides value — tips, insights, governance wisdom.
|
|
4819
5128
|
Max 2 posts per day to stay authentic.
|
|
4820
5129
|
|
|
5130
|
+
IMPORTANT — Platform tone rules (these are DIFFERENT per platform):
|
|
5131
|
+
- Twitter: confident technical brand. Direct, professional, ALWAYS POSITIVE.
|
|
5132
|
+
Celebrate wins and progress. Never complain or air gaps publicly.
|
|
5133
|
+
No em dashes or en dashes. Include install commands when relevant.
|
|
5134
|
+
- Reddit: proud builder posting as u/delimitdev. Casual, typed-on-phone energy.
|
|
5135
|
+
ALWAYS POSITIVE. Mention Delimit ONLY when genuinely helpful.
|
|
5136
|
+
NO bullet points/lists/bold/em dashes. 2-3 sentences max.
|
|
5137
|
+
- LinkedIn: professional hook + insight + CTA
|
|
5138
|
+
|
|
4821
5139
|
Args:
|
|
4822
5140
|
text: Tweet text. Leave empty to auto-generate.
|
|
4823
5141
|
category: Content category for auto-generation.
|
|
@@ -4826,6 +5144,7 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
4826
5144
|
quote_tweet_id: Tweet ID to quote (creates a quote tweet).
|
|
4827
5145
|
reply_to_id: Tweet ID to reply to (creates a reply).
|
|
4828
5146
|
draft: If True, save as draft for approval instead of posting immediately.
|
|
5147
|
+
context: WHY this post should be made. Strategic reasoning shown in the approval email.
|
|
4829
5148
|
"""
|
|
4830
5149
|
from ai.social import generate_post, post_tweet, should_post_today, save_draft
|
|
4831
5150
|
|
|
@@ -4834,35 +5153,150 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
|
|
|
4834
5153
|
|
|
4835
5154
|
post = generate_post(category, text)
|
|
4836
5155
|
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
5156
|
+
# ALL platforms go through email approval — no direct posting.
|
|
5157
|
+
# Founder reviews and posts manually from their device.
|
|
5158
|
+
if platform not in ("twitter", "reddit"):
|
|
5159
|
+
return {"error": f"Platform '{platform}' not supported yet", "supported": ["twitter", "reddit"]}
|
|
5160
|
+
|
|
5161
|
+
draft = True # Always draft, never auto-post
|
|
5162
|
+
entry = save_draft(
|
|
5163
|
+
post["text"], platform=platform, account=account,
|
|
5164
|
+
quote_tweet_id=quote_tweet_id, reply_to_id=reply_to_id,
|
|
5165
|
+
context=context,
|
|
5166
|
+
)
|
|
5167
|
+
# Send draft notification via email and store Message-ID for
|
|
5168
|
+
# In-Reply-To matching in the inbox daemon (Consensus 116)
|
|
5169
|
+
try:
|
|
5170
|
+
from ai.notify import send_email
|
|
5171
|
+
from ai.social import store_draft_message_id
|
|
5172
|
+
|
|
5173
|
+
# Build contextual email body so the founder knows exactly what to do
|
|
5174
|
+
_acct = account or ("delimitdev" if platform == "reddit" else "delimit_ai")
|
|
5175
|
+
_lines = []
|
|
5176
|
+
|
|
5177
|
+
if platform == "reddit":
|
|
5178
|
+
_lines.append(f"POST FROM: u/{_acct} on REDDIT")
|
|
5179
|
+
else:
|
|
5180
|
+
_lines.append(f"POST FROM: @{_acct} on TWITTER")
|
|
5181
|
+
_lines.append("")
|
|
5182
|
+
|
|
5183
|
+
# WHY should this be posted?
|
|
5184
|
+
_context = entry.get("context", "")
|
|
5185
|
+
if _context:
|
|
5186
|
+
_lines.append(f"WHY: {_context}")
|
|
5187
|
+
_lines.append("")
|
|
5188
|
+
|
|
5189
|
+
# What type of post is this?
|
|
5190
|
+
if platform == "reddit":
|
|
5191
|
+
_thread_url = entry.get("thread_url", "")
|
|
5192
|
+
# Fallback: extract reddit URL from context field
|
|
5193
|
+
if not _thread_url and not reply_to_id:
|
|
5194
|
+
import re as _re
|
|
5195
|
+
_ctx = entry.get("context", "")
|
|
5196
|
+
_url_match = _re.search(r'https?://(?:www\.)?reddit\.com/r/\S+', _ctx)
|
|
5197
|
+
if _url_match:
|
|
5198
|
+
_thread_url = _url_match.group(0)
|
|
5199
|
+
if reply_to_id or _thread_url:
|
|
5200
|
+
_url = _thread_url or reply_to_id
|
|
5201
|
+
_lines.append(f"ACTION: Reply in thread {_url}")
|
|
5202
|
+
_lines.append("Open the thread, find the comment to reply to, paste the text below.")
|
|
5203
|
+
else:
|
|
5204
|
+
# New Reddit post — extract title from first line of text
|
|
5205
|
+
_post_text = post["text"]
|
|
5206
|
+
_first_newline = _post_text.find("\n")
|
|
5207
|
+
if _first_newline > 0 and _first_newline < 200:
|
|
5208
|
+
_reddit_title = _post_text[:_first_newline].strip()
|
|
5209
|
+
_reddit_body = _post_text[_first_newline:].strip()
|
|
5210
|
+
else:
|
|
5211
|
+
_reddit_title = _post_text[:100].strip()
|
|
5212
|
+
_reddit_body = _post_text
|
|
5213
|
+
_lines.append("ACTION: New Reddit post")
|
|
5214
|
+
_lines.append("Navigate to the subreddit, create a new post.")
|
|
5215
|
+
_lines.append("")
|
|
5216
|
+
_lines.append("--- TITLE (paste in title field) ---")
|
|
5217
|
+
_lines.append("")
|
|
5218
|
+
_lines.append(_reddit_title)
|
|
5219
|
+
_lines.append("")
|
|
5220
|
+
_lines.append("--- BODY (paste in body field) ---")
|
|
5221
|
+
_lines.append("")
|
|
5222
|
+
_lines.append(_reddit_body)
|
|
5223
|
+
_lines.append("")
|
|
5224
|
+
_lines.append("--- END ---")
|
|
5225
|
+
_lines.append("")
|
|
5226
|
+
_lines.append(f"Draft ID: {entry['draft_id']}")
|
|
5227
|
+
if entry.get("tone_warnings"):
|
|
5228
|
+
_lines.append("")
|
|
5229
|
+
_lines.append("WARNINGS:")
|
|
5230
|
+
for w in entry["tone_warnings"]:
|
|
5231
|
+
_lines.append(f" - {w}")
|
|
5232
|
+
_lines.append("")
|
|
5233
|
+
_lines.append("Reply APPROVED to approve, CANCEL to reject.")
|
|
5234
|
+
|
|
5235
|
+
_handle = f"u/{_acct}"
|
|
5236
|
+
email_result = send_email(
|
|
5237
|
+
message="\n".join(_lines),
|
|
5238
|
+
subject=f"[Reddit Post] {_handle}: {_reddit_title[:60]}...",
|
|
4849
5239
|
event_type="social_draft",
|
|
4850
5240
|
)
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
5241
|
+
if email_result.get("delivered") and email_result.get("message_id"):
|
|
5242
|
+
store_draft_message_id(entry["draft_id"], email_result["message_id"])
|
|
5243
|
+
entry["category"] = post["category"]
|
|
5244
|
+
entry["mode"] = "draft"
|
|
5245
|
+
return _with_next_steps("social_post", entry)
|
|
5246
|
+
elif reply_to_id:
|
|
5247
|
+
_lines.append(f"ACTION: Reply to https://x.com/i/status/{reply_to_id}")
|
|
5248
|
+
_lines.append("Open the link above, click Reply, paste the text below.")
|
|
5249
|
+
elif quote_tweet_id:
|
|
5250
|
+
_lines.append(f"ACTION: Quote tweet https://x.com/i/status/{quote_tweet_id}")
|
|
5251
|
+
_lines.append("Open the link above, click Repost > Quote, paste the text below.")
|
|
5252
|
+
else:
|
|
5253
|
+
_lines.append("ACTION: New standalone tweet")
|
|
5254
|
+
_lines.append("Open X, compose a new tweet, paste the text below.")
|
|
5255
|
+
|
|
5256
|
+
_lines.append("")
|
|
5257
|
+
_lines.append("--- COPY BELOW THIS LINE ---")
|
|
5258
|
+
_lines.append("")
|
|
5259
|
+
_lines.append(post["text"])
|
|
5260
|
+
_lines.append("")
|
|
5261
|
+
_lines.append("--- END ---")
|
|
5262
|
+
_lines.append("")
|
|
5263
|
+
_lines.append(f"Draft ID: {entry['draft_id']}")
|
|
5264
|
+
if entry.get("tone_warnings"):
|
|
5265
|
+
_lines.append("")
|
|
5266
|
+
_lines.append("WARNINGS:")
|
|
5267
|
+
for w in entry["tone_warnings"]:
|
|
5268
|
+
_lines.append(f" - {w}")
|
|
5269
|
+
_lines.append("")
|
|
5270
|
+
_lines.append("Reply APPROVED to approve, CANCEL to reject.")
|
|
5271
|
+
|
|
5272
|
+
if platform == "reddit":
|
|
5273
|
+
_subject_type = "Reddit"
|
|
5274
|
+
elif reply_to_id:
|
|
5275
|
+
_subject_type = "Reply"
|
|
5276
|
+
elif quote_tweet_id:
|
|
5277
|
+
_subject_type = "Quote"
|
|
5278
|
+
else:
|
|
5279
|
+
_subject_type = "Tweet"
|
|
4861
5280
|
|
|
4862
|
-
|
|
5281
|
+
_handle = f"u/{_acct}" if platform == "reddit" else f"@{_acct}"
|
|
5282
|
+
email_result = send_email(
|
|
5283
|
+
message="\n".join(_lines),
|
|
5284
|
+
subject=f"[{_subject_type}] {_handle}: {post['text'][:60]}...",
|
|
5285
|
+
event_type="social_draft",
|
|
5286
|
+
)
|
|
5287
|
+
# Store the outbound Message-ID on the draft record so the
|
|
5288
|
+
# inbox daemon can match approval replies via In-Reply-To header
|
|
5289
|
+
if email_result.get("delivered") and email_result.get("message_id"):
|
|
5290
|
+
store_draft_message_id(entry["draft_id"], email_result["message_id"])
|
|
5291
|
+
except Exception as e:
|
|
5292
|
+
logger.warning("Failed to send draft notification email: %s", e)
|
|
5293
|
+
entry["category"] = post["category"]
|
|
5294
|
+
entry["mode"] = "draft"
|
|
5295
|
+
return _with_next_steps("social_post", entry)
|
|
4863
5296
|
|
|
4864
5297
|
|
|
4865
5298
|
@_ops_pack_tool()
|
|
5299
|
+
@mcp.tool()
|
|
4866
5300
|
def delimit_social_generate(category: str = "tip") -> Dict[str, Any]:
|
|
4867
5301
|
"""Generate a social media post without posting (Pro).
|
|
4868
5302
|
|
|
@@ -4876,6 +5310,7 @@ def delimit_social_generate(category: str = "tip") -> Dict[str, Any]:
|
|
|
4876
5310
|
|
|
4877
5311
|
|
|
4878
5312
|
@_internal_tool()
|
|
5313
|
+
@mcp.tool()
|
|
4879
5314
|
def delimit_social_accounts() -> Dict[str, Any]:
|
|
4880
5315
|
"""List configured social media accounts (Pro).
|
|
4881
5316
|
|
|
@@ -4889,14 +5324,29 @@ def delimit_social_accounts() -> Dict[str, Any]:
|
|
|
4889
5324
|
|
|
4890
5325
|
|
|
4891
5326
|
@_internal_tool()
|
|
4892
|
-
|
|
4893
|
-
|
|
5327
|
+
@mcp.tool()
|
|
5328
|
+
def delimit_social_history(limit: int = 20, platform: str = "",
|
|
5329
|
+
user: str = "", subreddit: str = "") -> Dict[str, Any]:
|
|
5330
|
+
"""View recent social media post history (Pro).
|
|
5331
|
+
|
|
5332
|
+
Filter by platform, Reddit user, or subreddit to recall prior conversations.
|
|
5333
|
+
Reddit comments include thread context (subreddit, thread_url, replying_to_user)
|
|
5334
|
+
so you can reference what was said when drafting follow-ups or DM replies.
|
|
5335
|
+
|
|
5336
|
+
Args:
|
|
5337
|
+
limit: Max entries to return.
|
|
5338
|
+
platform: Filter by platform — "twitter" or "reddit".
|
|
5339
|
+
user: Filter by Reddit user we interacted with (e.g. "coolinjapan001").
|
|
5340
|
+
subreddit: Filter by subreddit (e.g. "r/vibecoding").
|
|
5341
|
+
"""
|
|
4894
5342
|
from ai.social import get_post_history
|
|
4895
5343
|
|
|
4896
|
-
|
|
5344
|
+
posts = get_post_history(limit, platform=platform, user=user, subreddit=subreddit)
|
|
5345
|
+
return _with_next_steps("social_history", {"posts": posts})
|
|
4897
5346
|
|
|
4898
5347
|
|
|
4899
5348
|
@_ops_pack_tool()
|
|
5349
|
+
@mcp.tool()
|
|
4900
5350
|
def delimit_social_approve(action: str = "list", draft_id: str = "") -> Dict[str, Any]:
|
|
4901
5351
|
"""Manage social media drafts: list, approve, or reject (Pro).
|
|
4902
5352
|
|
|
@@ -4929,12 +5379,111 @@ def delimit_social_approve(action: str = "list", draft_id: str = "") -> Dict[str
|
|
|
4929
5379
|
return {"error": f"Unknown action: {action}. Supported: list, approve, reject"}
|
|
4930
5380
|
|
|
4931
5381
|
|
|
5382
|
+
@mcp.tool()
|
|
5383
|
+
def delimit_social_target(
|
|
5384
|
+
action: str = "scan",
|
|
5385
|
+
platforms: str = "x,hn,devto,reddit,github",
|
|
5386
|
+
ventures: str = "",
|
|
5387
|
+
keywords: str = "",
|
|
5388
|
+
limit: int = 10,
|
|
5389
|
+
draft_replies: bool = False,
|
|
5390
|
+
create_ledger: bool = False,
|
|
5391
|
+
) -> Dict[str, Any]:
|
|
5392
|
+
"""Discover engagement opportunities across platforms (Pro).
|
|
5393
|
+
|
|
5394
|
+
IMPORTANT — TOOL CHAINING RULE:
|
|
5395
|
+
After scanning, the AI MUST immediately process results:
|
|
5396
|
+
1. For "reply" targets: draft a reply via delimit_social_post or delimit_notify
|
|
5397
|
+
2. For "strategic" targets: create a ledger item via delimit_ledger_add
|
|
5398
|
+
3. For "both": do both
|
|
5399
|
+
Never just return targets and stop. Always chain to the next action.
|
|
5400
|
+
|
|
5401
|
+
Scans X, Reddit (RapidAPI), HN, Dev.to, GitHub for posts where ventures can engage.
|
|
5402
|
+
NamePros flagged as manual_check_needed (no API).
|
|
5403
|
+
|
|
5404
|
+
Run in a /loop for continuous discovery. Deduplicates across runs.
|
|
5405
|
+
Targets are classified as: reply (social engagement), strategic (ledger item), or both.
|
|
5406
|
+
|
|
5407
|
+
Args:
|
|
5408
|
+
action: "scan" to discover targets, "list" to show recent, "stats" to show counts.
|
|
5409
|
+
platforms: Comma-separated platforms to scan (x, hn, devto, reddit, github, namepros).
|
|
5410
|
+
ventures: Comma-separated ventures to scan for. Empty = all.
|
|
5411
|
+
keywords: Extra keywords to search for beyond venture topics.
|
|
5412
|
+
limit: Max targets per platform.
|
|
5413
|
+
draft_replies: If True, auto-draft social posts for "reply" targets.
|
|
5414
|
+
create_ledger: If True, create ledger items for "strategic" targets.
|
|
5415
|
+
"""
|
|
5416
|
+
from ai.social_target import scan_targets, process_targets, list_targets, get_stats
|
|
5417
|
+
|
|
5418
|
+
if action == "scan":
|
|
5419
|
+
platform_list = [p.strip() for p in platforms.split(",")]
|
|
5420
|
+
venture_list = [v.strip() for v in ventures.split(",") if v.strip()] or None
|
|
5421
|
+
keyword_list = [k.strip() for k in keywords.split(",") if k.strip()] or None
|
|
5422
|
+
targets = scan_targets(platform_list, venture_list, keyword_list, limit)
|
|
5423
|
+
result = {"action": "scan", "targets_found": len(targets), "targets": targets}
|
|
5424
|
+
if draft_replies or create_ledger:
|
|
5425
|
+
processed = process_targets(targets, draft_replies, create_ledger)
|
|
5426
|
+
result["processed"] = processed
|
|
5427
|
+
return _with_next_steps("social_target", result)
|
|
5428
|
+
elif action == "list":
|
|
5429
|
+
return _with_next_steps("social_target", list_targets(limit))
|
|
5430
|
+
elif action == "stats":
|
|
5431
|
+
return _with_next_steps("social_target", get_stats())
|
|
5432
|
+
return {"error": f"Unknown action: {action}. Supported: scan, list, stats"}
|
|
5433
|
+
|
|
5434
|
+
|
|
5435
|
+
@mcp.tool()
|
|
5436
|
+
def delimit_social_target_config(
|
|
5437
|
+
action: str = "status",
|
|
5438
|
+
platform: str = "",
|
|
5439
|
+
enabled: bool = True,
|
|
5440
|
+
provider: str = "",
|
|
5441
|
+
subreddits: str = "",
|
|
5442
|
+
) -> Dict[str, Any]:
|
|
5443
|
+
"""Configure social target scanning platforms (Pro).
|
|
5444
|
+
|
|
5445
|
+
Actions:
|
|
5446
|
+
status: Show current config and which platforms are available
|
|
5447
|
+
detect: Auto-detect available platforms from configured API keys
|
|
5448
|
+
update: Update a platform's config (enable/disable, change provider)
|
|
5449
|
+
add_subreddits: Add subreddits to scan for a venture
|
|
5450
|
+
|
|
5451
|
+
Args:
|
|
5452
|
+
action: status, detect, update, add_subreddits
|
|
5453
|
+
platform: Platform to configure (x, reddit, github, hn, devto, namepros)
|
|
5454
|
+
enabled: Enable/disable the platform (used with update action)
|
|
5455
|
+
provider: Provider to use, e.g. twttr241, xai, proxy, gh_cli (used with update action)
|
|
5456
|
+
subreddits: Comma-separated subreddits to add (with add_subreddits action)
|
|
5457
|
+
"""
|
|
5458
|
+
from ai.social_target import (
|
|
5459
|
+
get_config_status, _detect_available_platforms,
|
|
5460
|
+
update_platform_config, add_subreddits as add_subs,
|
|
5461
|
+
)
|
|
5462
|
+
|
|
5463
|
+
if action == "status":
|
|
5464
|
+
return get_config_status()
|
|
5465
|
+
elif action == "detect":
|
|
5466
|
+
detection = _detect_available_platforms()
|
|
5467
|
+
return {"platforms": detection}
|
|
5468
|
+
elif action == "update":
|
|
5469
|
+
if not platform:
|
|
5470
|
+
return {"error": "Platform name is required for update action"}
|
|
5471
|
+
return update_platform_config(platform, enabled=enabled, provider=provider or None)
|
|
5472
|
+
elif action == "add_subreddits":
|
|
5473
|
+
if not platform or not subreddits:
|
|
5474
|
+
return {"error": "Platform (as venture name) and subreddits are required"}
|
|
5475
|
+
sub_list = [s.strip() for s in subreddits.split(",") if s.strip()]
|
|
5476
|
+
return add_subs(platform, sub_list)
|
|
5477
|
+
return {"error": f"Unknown action: {action}. Supported: status, detect, update, add_subreddits"}
|
|
5478
|
+
|
|
5479
|
+
|
|
4932
5480
|
# ═══════════════════════════════════════════════════════════════════════
|
|
4933
5481
|
# CONTENT ENGINE — Autonomous video + tweet pipeline (Pro)
|
|
4934
5482
|
# ═══════════════════════════════════════════════════════════════════════
|
|
4935
5483
|
|
|
4936
5484
|
|
|
4937
5485
|
@_internal_tool()
|
|
5486
|
+
@mcp.tool()
|
|
4938
5487
|
def delimit_content_schedule() -> Dict[str, Any]:
|
|
4939
5488
|
"""View the upcoming content schedule: queued tweets, pending videos, recent activity (Pro)."""
|
|
4940
5489
|
from ai.content_engine import get_content_schedule
|
|
@@ -4943,6 +5492,7 @@ def delimit_content_schedule() -> Dict[str, Any]:
|
|
|
4943
5492
|
|
|
4944
5493
|
|
|
4945
5494
|
@_internal_tool()
|
|
5495
|
+
@mcp.tool()
|
|
4946
5496
|
def delimit_content_publish(content_type: str = "tweet") -> Dict[str, Any]:
|
|
4947
5497
|
"""Manually trigger a content publish: tweet or youtube video (Pro).
|
|
4948
5498
|
|
|
@@ -4961,6 +5511,7 @@ def delimit_content_publish(content_type: str = "tweet") -> Dict[str, Any]:
|
|
|
4961
5511
|
|
|
4962
5512
|
|
|
4963
5513
|
@_internal_tool()
|
|
5514
|
+
@mcp.tool()
|
|
4964
5515
|
def delimit_content_queue(action: str = "status", items: str = "") -> Dict[str, Any]:
|
|
4965
5516
|
"""Manage the tweet and video content queues (Pro).
|
|
4966
5517
|
|
|
@@ -5055,12 +5606,13 @@ def delimit_daemon_classify(item_id: str = "") -> Dict[str, Any]:
|
|
|
5055
5606
|
|
|
5056
5607
|
|
|
5057
5608
|
@_internal_tool()
|
|
5609
|
+
@mcp.tool()
|
|
5058
5610
|
def delimit_inbox_daemon(action: str = "status") -> Dict[str, Any]:
|
|
5059
5611
|
"""Control the inbox polling daemon for email governance (Pro).
|
|
5060
5612
|
|
|
5061
5613
|
Polls pro@delimit.ai every 5 minutes, classifies emails, forwards
|
|
5062
|
-
owner-action items, and handles draft approval via email replies
|
|
5063
|
-
|
|
5614
|
+
owner-action items, and handles draft approval via email replies.
|
|
5615
|
+
Auto-posting is disabled - approved drafts are emailed for manual posting.
|
|
5064
5616
|
|
|
5065
5617
|
Args:
|
|
5066
5618
|
action: 'start' (begin polling), 'stop' (halt polling),
|
|
@@ -5205,6 +5757,7 @@ def delimit_config_import(
|
|
|
5205
5757
|
|
|
5206
5758
|
|
|
5207
5759
|
@_ops_pack_tool()
|
|
5760
|
+
@mcp.tool()
|
|
5208
5761
|
def delimit_screen_record(mode: str = "browser", url: str = "", name: str = "recording",
|
|
5209
5762
|
duration: int = 30, script: str = "") -> Dict[str, Any]:
|
|
5210
5763
|
"""Record a screen capture (Pro).
|
|
@@ -5249,6 +5802,7 @@ def delimit_screen_record(mode: str = "browser", url: str = "", name: str = "rec
|
|
|
5249
5802
|
|
|
5250
5803
|
|
|
5251
5804
|
@_ops_pack_tool()
|
|
5805
|
+
@mcp.tool()
|
|
5252
5806
|
def delimit_screenshot(url: str, name: str = "screenshot") -> Dict[str, Any]:
|
|
5253
5807
|
"""Take a screenshot of a URL using headless Chromium (Pro).
|
|
5254
5808
|
|
|
@@ -5302,20 +5856,34 @@ def delimit_changelog(old_spec: str = "", new_spec: str = "", format: str = "mar
|
|
|
5302
5856
|
|
|
5303
5857
|
|
|
5304
5858
|
@_ops_pack_tool()
|
|
5859
|
+
@mcp.tool()
|
|
5305
5860
|
def delimit_notify(channel: str = "webhook", message: str = "",
|
|
5306
5861
|
webhook_url: str = "", subject: str = "",
|
|
5307
5862
|
event_type: str = "", to: str = "",
|
|
5308
5863
|
from_account: str = "") -> Dict[str, Any]:
|
|
5309
5864
|
"""Send a notification (Pro).
|
|
5310
5865
|
|
|
5866
|
+
IMPORTANT — AUTO-TRIGGER RULE:
|
|
5867
|
+
When the AI identifies something requiring owner action (outreach reply,
|
|
5868
|
+
deployment decision, approval needed), it MUST call this tool immediately.
|
|
5869
|
+
Never ask "want me to notify you?" — just send the notification.
|
|
5870
|
+
The founder reviews and acts via email. All tools must chain automatically.
|
|
5871
|
+
|
|
5311
5872
|
Channels: webhook (JSON POST), slack (webhook URL), email (SMTP).
|
|
5312
5873
|
Use for: governance alerts, deployment notifications, breaking change warnings.
|
|
5313
5874
|
|
|
5875
|
+
IMPORTANT — Email context rules:
|
|
5876
|
+
Every email must be self-contained and actionable. The recipient reads on mobile
|
|
5877
|
+
and needs to know exactly what to do without opening another app.
|
|
5878
|
+
- Subject: lead with [ACTION TYPE] bracket, include enough context to triage from inbox
|
|
5879
|
+
- Body: include WHAT happened, WHY it matters, WHAT to do next, and relevant links
|
|
5880
|
+
- Never send bare IDs or technical state without human-readable context
|
|
5881
|
+
|
|
5314
5882
|
Args:
|
|
5315
5883
|
channel: webhook, slack, or email.
|
|
5316
|
-
message: Notification body.
|
|
5884
|
+
message: Notification body. Must include full context (see rules above).
|
|
5317
5885
|
webhook_url: URL for webhook/slack channels.
|
|
5318
|
-
subject: Subject line (email only).
|
|
5886
|
+
subject: Subject line (email only). Use [ACTION], [INFO], [ALERT] prefix.
|
|
5319
5887
|
event_type: Event category for filtering.
|
|
5320
5888
|
to: Recipient email address (email only). Overrides default DELIMIT_SMTP_TO.
|
|
5321
5889
|
Send to any address — leave empty for default (jamsonsholdings@gmail.com).
|
|
@@ -5335,9 +5903,94 @@ def delimit_notify(channel: str = "webhook", message: str = "",
|
|
|
5335
5903
|
))
|
|
5336
5904
|
|
|
5337
5905
|
|
|
5906
|
+
@mcp.tool()
|
|
5907
|
+
def delimit_notify_routing(
|
|
5908
|
+
action: str = "status",
|
|
5909
|
+
config: str = "",
|
|
5910
|
+
webhook_url: str = "",
|
|
5911
|
+
email_to: str = "",
|
|
5912
|
+
from_account: str = "",
|
|
5913
|
+
) -> Dict[str, Any]:
|
|
5914
|
+
"""Manage impact-based notification routing (LED-233).
|
|
5915
|
+
|
|
5916
|
+
Routes change alerts by severity: breaking changes send urgent notifications,
|
|
5917
|
+
non-breaking goes to standard channels, cosmetic changes are suppressed.
|
|
5918
|
+
|
|
5919
|
+
Args:
|
|
5920
|
+
action: 'status' (show current config), 'configure' (update routing rules),
|
|
5921
|
+
'test' (send test notifications at each severity level).
|
|
5922
|
+
config: JSON string with routing config for action='configure'.
|
|
5923
|
+
Example: {"routing": {"critical": {"channels": ["email","webhook"],
|
|
5924
|
+
"email_subject_prefix": "[URGENT]", "webhook_priority": "high"},
|
|
5925
|
+
"warning": {"channels": ["webhook"], "webhook_priority": "normal"},
|
|
5926
|
+
"info": {"channels": [], "digest": true}}}
|
|
5927
|
+
webhook_url: Webhook URL for test notifications.
|
|
5928
|
+
email_to: Email recipient for test notifications.
|
|
5929
|
+
from_account: Sender account key for test email delivery.
|
|
5930
|
+
"""
|
|
5931
|
+
from ai.notify import (
|
|
5932
|
+
load_routing_config,
|
|
5933
|
+
save_routing_config,
|
|
5934
|
+
route_by_impact,
|
|
5935
|
+
DEFAULT_ROUTING_CONFIG,
|
|
5936
|
+
)
|
|
5937
|
+
|
|
5938
|
+
if action == "status":
|
|
5939
|
+
current = load_routing_config()
|
|
5940
|
+
return _with_next_steps("notify_routing", {
|
|
5941
|
+
"action": "status",
|
|
5942
|
+
"config": current,
|
|
5943
|
+
"config_file": str(Path.home() / ".delimit" / "notify_routing.yaml"),
|
|
5944
|
+
"using_defaults": current == DEFAULT_ROUTING_CONFIG,
|
|
5945
|
+
})
|
|
5946
|
+
|
|
5947
|
+
elif action == "configure":
|
|
5948
|
+
if not config:
|
|
5949
|
+
return _with_next_steps("notify_routing", {
|
|
5950
|
+
"error": "config parameter required for action='configure'.",
|
|
5951
|
+
"usage": 'Pass a JSON string with routing rules, e.g. {"routing": {"critical": {"channels": ["email"]}}}',
|
|
5952
|
+
})
|
|
5953
|
+
try:
|
|
5954
|
+
parsed = json.loads(config) if isinstance(config, str) else config
|
|
5955
|
+
except json.JSONDecodeError as e:
|
|
5956
|
+
return _with_next_steps("notify_routing", {
|
|
5957
|
+
"error": f"Invalid JSON in config: {e}",
|
|
5958
|
+
})
|
|
5959
|
+
result = save_routing_config(parsed)
|
|
5960
|
+
return _with_next_steps("notify_routing", {
|
|
5961
|
+
"action": "configure",
|
|
5962
|
+
**result,
|
|
5963
|
+
})
|
|
5964
|
+
|
|
5965
|
+
elif action == "test":
|
|
5966
|
+
# Generate synthetic changes at each severity level
|
|
5967
|
+
test_changes = [
|
|
5968
|
+
{"type": "endpoint_removed", "path": "/test/critical", "message": "Test critical: endpoint removed", "is_breaking": True},
|
|
5969
|
+
{"type": "parameter_added", "path": "/test/warning", "message": "Test warning: optional parameter added", "is_breaking": False},
|
|
5970
|
+
{"type": "description_changed", "path": "/test/info", "message": "Test info: description updated", "severity": "info"},
|
|
5971
|
+
]
|
|
5972
|
+
result = route_by_impact(
|
|
5973
|
+
test_changes,
|
|
5974
|
+
webhook_url=webhook_url,
|
|
5975
|
+
email_to=email_to,
|
|
5976
|
+
from_account=from_account,
|
|
5977
|
+
dry_run=not (webhook_url or email_to),
|
|
5978
|
+
)
|
|
5979
|
+
return _with_next_steps("notify_routing", {
|
|
5980
|
+
"action": "test",
|
|
5981
|
+
**result,
|
|
5982
|
+
})
|
|
5983
|
+
|
|
5984
|
+
else:
|
|
5985
|
+
return _with_next_steps("notify_routing", {
|
|
5986
|
+
"error": f"Unknown action: {action}. Supported: status, configure, test.",
|
|
5987
|
+
})
|
|
5988
|
+
|
|
5989
|
+
|
|
5338
5990
|
@_internal_tool()
|
|
5991
|
+
@mcp.tool()
|
|
5339
5992
|
def delimit_notify_inbox(action: str = "status", limit: int = 10,
|
|
5340
|
-
process: bool =
|
|
5993
|
+
process: bool = True) -> Dict[str, Any]:
|
|
5341
5994
|
"""Check inbound email inbox, classify, and route (Pro).
|
|
5342
5995
|
|
|
5343
5996
|
Polls pro@delimit.ai via IMAP. Classifies emails as owner-action
|
|
@@ -5502,6 +6155,106 @@ def delimit_agent_handoff(task_id: str, to_model: str,
|
|
|
5502
6155
|
return _delimit_agent_impl(action="handoff", task_id=task_id, to_model=to_model, context=context)
|
|
5503
6156
|
|
|
5504
6157
|
|
|
6158
|
+
@mcp.tool()
|
|
6159
|
+
def delimit_agent_link(task_id: str, ledger_item_id: str) -> Dict[str, Any]:
|
|
6160
|
+
"""Link an agent task to a ledger item (LED-xxx or STR-xxx).
|
|
6161
|
+
|
|
6162
|
+
Creates a relationship so the dashboard shows which agent is working on which ledger item.
|
|
6163
|
+
|
|
6164
|
+
Args:
|
|
6165
|
+
task_id: Agent task ID (AGT-xxx).
|
|
6166
|
+
ledger_item_id: Ledger item ID (LED-xxx or STR-xxx).
|
|
6167
|
+
"""
|
|
6168
|
+
from ai.agent_dispatch import link_ledger_item
|
|
6169
|
+
return _with_next_steps("agent_link", _safe_call(
|
|
6170
|
+
link_ledger_item, task_id=task_id, ledger_item_id=ledger_item_id,
|
|
6171
|
+
))
|
|
6172
|
+
|
|
6173
|
+
|
|
6174
|
+
@mcp.tool()
|
|
6175
|
+
def delimit_agent_dashboard() -> Dict[str, Any]:
|
|
6176
|
+
"""View the multi-agent orchestration dashboard (Pro).
|
|
6177
|
+
|
|
6178
|
+
Shows all agent tasks grouped by assignee and status, handoff history,
|
|
6179
|
+
linked ledger items, and recent audit trail.
|
|
6180
|
+
"""
|
|
6181
|
+
from ai.agent_dispatch import get_agent_dashboard
|
|
6182
|
+
return _with_next_steps("agent_dashboard", _safe_call(get_agent_dashboard))
|
|
6183
|
+
|
|
6184
|
+
|
|
6185
|
+
@mcp.tool()
|
|
6186
|
+
def delimit_agent_policy(model: str = "", ledger: str = "", memory: str = "",
|
|
6187
|
+
deploy: str = "", evidence: str = "",
|
|
6188
|
+
secrets: str = "", custom_constraints: str = "") -> Dict[str, Any]:
|
|
6189
|
+
"""Set or view per-model governance permissions (Pro).
|
|
6190
|
+
|
|
6191
|
+
Controls what each AI model can do. Without arguments, shows all policies.
|
|
6192
|
+
With a model name, shows or updates that model's policy.
|
|
6193
|
+
|
|
6194
|
+
Examples:
|
|
6195
|
+
- delimit_agent_policy() -> show all model permissions
|
|
6196
|
+
- delimit_agent_policy(model="codex", ledger="read-only", deploy="false")
|
|
6197
|
+
- delimit_agent_policy(model="gemini", memory="none", secrets="false")
|
|
6198
|
+
|
|
6199
|
+
Access levels for ledger/memory/evidence: "read-only", "read-write", "none"
|
|
6200
|
+
Boolean flags for deploy/secrets: "true" or "false"
|
|
6201
|
+
|
|
6202
|
+
Args:
|
|
6203
|
+
model: AI model name (claude, codex, gemini, cursor). Empty = show all.
|
|
6204
|
+
ledger: Ledger access level (read-only, read-write, none).
|
|
6205
|
+
memory: Memory access level (read-only, read-write, none).
|
|
6206
|
+
deploy: Allow deploys (true/false).
|
|
6207
|
+
evidence: Evidence access level (read-only, read-write, none).
|
|
6208
|
+
secrets: Allow secret access (true/false).
|
|
6209
|
+
custom_constraints: Comma-separated constraints (e.g. "no-deploy,no-publish").
|
|
6210
|
+
"""
|
|
6211
|
+
from ai.agent_policy import set_agent_policy, get_agent_policy
|
|
6212
|
+
|
|
6213
|
+
if not model or not model.strip():
|
|
6214
|
+
return _with_next_steps("agent_policy", _safe_call(get_agent_policy, model=""))
|
|
6215
|
+
|
|
6216
|
+
# If only model provided with no changes, just show the policy
|
|
6217
|
+
has_changes = any([ledger, memory, deploy, evidence, secrets, custom_constraints])
|
|
6218
|
+
if not has_changes:
|
|
6219
|
+
return _with_next_steps("agent_policy", _safe_call(get_agent_policy, model=model))
|
|
6220
|
+
|
|
6221
|
+
# Parse boolean strings
|
|
6222
|
+
deploy_bool = None
|
|
6223
|
+
if deploy:
|
|
6224
|
+
deploy_bool = deploy.lower().strip() in ("true", "1", "yes")
|
|
6225
|
+
secrets_bool = None
|
|
6226
|
+
if secrets:
|
|
6227
|
+
secrets_bool = secrets.lower().strip() in ("true", "1", "yes")
|
|
6228
|
+
constraints_list = None
|
|
6229
|
+
if custom_constraints:
|
|
6230
|
+
constraints_list = [c.strip() for c in custom_constraints.split(",") if c.strip()]
|
|
6231
|
+
|
|
6232
|
+
return _with_next_steps("agent_policy", _safe_call(
|
|
6233
|
+
set_agent_policy, model=model, ledger=ledger, memory=memory,
|
|
6234
|
+
deploy=deploy_bool, evidence=evidence, secrets=secrets_bool,
|
|
6235
|
+
custom_constraints=constraints_list,
|
|
6236
|
+
))
|
|
6237
|
+
|
|
6238
|
+
|
|
6239
|
+
@mcp.tool()
|
|
6240
|
+
def delimit_agent_check(model: str, action: str) -> Dict[str, Any]:
|
|
6241
|
+
"""Check if a model is allowed to perform an action (Pro).
|
|
6242
|
+
|
|
6243
|
+
Use before executing sensitive operations to verify the agent has permission.
|
|
6244
|
+
|
|
6245
|
+
Actions: ledger_write, ledger_read, memory_write, memory_read,
|
|
6246
|
+
deploy, lint, deliberate, security_audit, evidence_write, secrets_read.
|
|
6247
|
+
|
|
6248
|
+
Args:
|
|
6249
|
+
model: AI model name (claude, codex, gemini, cursor).
|
|
6250
|
+
action: Action to check (e.g. "ledger_write", "deploy").
|
|
6251
|
+
"""
|
|
6252
|
+
from ai.agent_policy import check_agent_permission
|
|
6253
|
+
return _with_next_steps("agent_check", _safe_call(
|
|
6254
|
+
check_agent_permission, model=model, action=action,
|
|
6255
|
+
))
|
|
6256
|
+
|
|
6257
|
+
|
|
5505
6258
|
# ═══════════════════════════════════════════════════════════════════════
|
|
5506
6259
|
# STR-026: AUTONOMOUS BUILD LOOP
|
|
5507
6260
|
# ═══════════════════════════════════════════════════════════════════════
|