delimit-cli 4.1.0 → 4.1.2

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