delimit-cli 3.14.16 → 3.14.18

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.
@@ -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. Emit event for dashboard tracking
1116
- 2. STR-052: Policy kernel gate (blocks high-risk actions without approval)
1117
- 3. Check Pro license gate (blocks if not authorized)
1118
- 4. Check result against rules (thresholds, policies)
1119
- 5. Auto-create ledger items for failures/warnings
1120
- 6. Route back to delimit_ledger_context (the loop continues)
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) -> Dict[str, Any]:
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
- if platform == "twitter":
4838
- if draft:
4839
- entry = save_draft(
4840
- post["text"], platform=platform, account=account,
4841
- quote_tweet_id=quote_tweet_id, reply_to_id=reply_to_id,
4842
- )
4843
- # Send draft notification via email
4844
- try:
4845
- from ai.notify import send_email
4846
- send_email(
4847
- message=f"Social draft pending approval:\n\n{post['text']}\n\nDraft ID: {entry['draft_id']}\nPlatform: {platform}\nAccount: {account or 'default'}",
4848
- subject=f"Social Draft: {entry['draft_id']}",
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
- except Exception as e:
4852
- logger.warning("Failed to send draft notification email: %s", e)
4853
- entry["category"] = post["category"]
4854
- entry["mode"] = "draft"
4855
- return _with_next_steps("social_post", entry)
4856
-
4857
- result = post_tweet(post["text"], account=account,
4858
- quote_tweet_id=quote_tweet_id, reply_to_id=reply_to_id)
4859
- result["category"] = post["category"]
4860
- return _with_next_steps("social_post", result)
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
- return {"error": f"Platform '{platform}' not supported yet", "supported": ["twitter"]}
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
- def delimit_social_history(limit: int = 20) -> Dict[str, Any]:
4893
- """View recent social media post history (Pro)."""
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
- return _with_next_steps("social_history", {"posts": get_post_history(limit)})
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
- with a 10-minute cancel window.
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 = False) -> Dict[str, Any]:
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
  # ═══════════════════════════════════════════════════════════════════════