delimit-cli 3.14.28 → 3.14.29

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.
Files changed (47) hide show
  1. package/gateway/ai/backends/deploy_bridge.py +56 -2
  2. package/gateway/ai/backends/gateway_core.py +212 -1
  3. package/gateway/ai/backends/generate_bridge.py +84 -13
  4. package/gateway/ai/backends/governance_bridge.py +63 -16
  5. package/gateway/ai/backends/memory_bridge.py +77 -76
  6. package/gateway/ai/backends/ops_bridge.py +76 -6
  7. package/gateway/ai/backends/os_bridge.py +23 -3
  8. package/gateway/ai/backends/repo_bridge.py +156 -17
  9. package/gateway/ai/backends/tools_design.py +116 -9
  10. package/gateway/ai/backends/tools_infra.py +200 -72
  11. package/gateway/ai/backends/tools_real.py +8 -0
  12. package/gateway/ai/backends/ui_bridge.py +115 -5
  13. package/gateway/ai/backends/vault_bridge.py +69 -114
  14. package/gateway/ai/content_engine.py +1276 -0
  15. package/gateway/ai/context_fs.py +193 -0
  16. package/gateway/ai/daemon.py +500 -0
  17. package/gateway/ai/data_plane.py +291 -0
  18. package/gateway/ai/deliberation.py +1033 -6
  19. package/gateway/ai/events.py +39 -0
  20. package/gateway/ai/founding_users.py +162 -0
  21. package/gateway/ai/governance.py +698 -4
  22. package/gateway/ai/inbox_daemon.py +78 -17
  23. package/gateway/ai/integrations/__init__.py +1 -0
  24. package/gateway/ai/integrations/opensage_wrapper.py +288 -0
  25. package/gateway/ai/key_resolver.py +95 -0
  26. package/gateway/ai/ledger_manager.py +289 -1
  27. package/gateway/ai/license.py +62 -4
  28. package/gateway/ai/license_core.py +208 -7
  29. package/gateway/ai/local_server.py +215 -0
  30. package/gateway/ai/loop_engine.py +408 -0
  31. package/gateway/ai/mcp_bridge.py +178 -0
  32. package/gateway/ai/release_sync.py +2 -2
  33. package/gateway/ai/screen_record.py +374 -0
  34. package/gateway/ai/secrets_broker.py +235 -0
  35. package/gateway/ai/social.py +189 -27
  36. package/gateway/ai/social_target.py +1635 -0
  37. package/gateway/ai/supabase_sync.py +190 -0
  38. package/gateway/ai/tracing.py +195 -0
  39. package/gateway/core/contract_ledger.py +1 -1
  40. package/gateway/core/dependency_graph.py +1 -1
  41. package/gateway/core/dependency_manifest.py +1 -1
  42. package/gateway/core/diff_engine_v2.py +272 -78
  43. package/gateway/core/event_backbone.py +2 -2
  44. package/gateway/core/event_schema.py +1 -1
  45. package/gateway/core/impact_analyzer.py +1 -1
  46. package/gateway/core/policy_engine.py +4 -0
  47. package/package.json +1 -1
@@ -0,0 +1,190 @@
1
+ """Supabase sync -- writes gateway data to cloud for dashboard access.
2
+
3
+ Writes are fire-and-forget (never blocks tool execution).
4
+ If Supabase is unreachable, data stays in local files (always the source of truth).
5
+ """
6
+ import json
7
+ import os
8
+ import logging
9
+ import uuid
10
+ from pathlib import Path
11
+ from typing import Dict, Optional
12
+
13
+ logger = logging.getLogger("delimit.supabase_sync")
14
+
15
+ _client = None
16
+ _init_attempted = False
17
+ SUPABASE_URL = os.environ.get("SUPABASE_URL", "")
18
+ SUPABASE_KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY", "")
19
+
20
+ # Also check local secrets file
21
+ if not SUPABASE_URL:
22
+ secrets_file = Path.home() / ".delimit" / "secrets" / "supabase.json"
23
+ if secrets_file.exists():
24
+ try:
25
+ creds = json.loads(secrets_file.read_text())
26
+ SUPABASE_URL = creds.get("url", "")
27
+ SUPABASE_KEY = creds.get("service_role_key", "")
28
+ except Exception:
29
+ pass
30
+
31
+
32
+ def _get_client():
33
+ """Lazy-init Supabase client. Returns the SDK client, 'http' for fallback, or None."""
34
+ global _client, _init_attempted
35
+ if _client is not None:
36
+ return _client
37
+ if _init_attempted:
38
+ return _client # Already tried and failed, return cached result (may be None or "http")
39
+ _init_attempted = True
40
+ if not SUPABASE_URL or not SUPABASE_KEY:
41
+ return None
42
+ try:
43
+ from supabase import create_client
44
+ _client = create_client(SUPABASE_URL, SUPABASE_KEY)
45
+ return _client
46
+ except ImportError:
47
+ logger.debug("supabase-py not installed, using HTTP fallback")
48
+ _client = "http"
49
+ return _client
50
+ except Exception as e:
51
+ logger.warning(f"Supabase init failed: {e}")
52
+ _client = "http" # Fall back to HTTP rather than giving up entirely
53
+ return _client
54
+
55
+
56
+ def _http_post(table: str, data: dict, headers_extra: Optional[Dict] = None) -> bool:
57
+ """POST to Supabase REST API without the SDK."""
58
+ import urllib.request
59
+ try:
60
+ url = f"{SUPABASE_URL}/rest/v1/{table}"
61
+ body = json.dumps(data).encode()
62
+ req = urllib.request.Request(url, data=body, method="POST")
63
+ req.add_header("Content-Type", "application/json")
64
+ req.add_header("apikey", SUPABASE_KEY)
65
+ req.add_header("Authorization", f"Bearer {SUPABASE_KEY}")
66
+ req.add_header("Prefer", "return=minimal")
67
+ if headers_extra:
68
+ for k, v in headers_extra.items():
69
+ req.add_header(k, v)
70
+ urllib.request.urlopen(req, timeout=5)
71
+ return True
72
+ except Exception as e:
73
+ logger.debug(f"Supabase HTTP POST to {table} failed: {e}")
74
+ return False
75
+
76
+
77
+ def _http_patch(table: str, query: str, data: dict) -> bool:
78
+ """PATCH to Supabase REST API without the SDK."""
79
+ import urllib.request
80
+ try:
81
+ url = f"{SUPABASE_URL}/rest/v1/{table}?{query}"
82
+ body = json.dumps(data).encode()
83
+ req = urllib.request.Request(url, data=body, method="PATCH")
84
+ req.add_header("Content-Type", "application/json")
85
+ req.add_header("apikey", SUPABASE_KEY)
86
+ req.add_header("Authorization", f"Bearer {SUPABASE_KEY}")
87
+ req.add_header("Prefer", "return=minimal")
88
+ urllib.request.urlopen(req, timeout=5)
89
+ return True
90
+ except Exception as e:
91
+ logger.debug(f"Supabase HTTP PATCH to {table} failed: {e}")
92
+ return False
93
+
94
+
95
+ def sync_event(event: dict):
96
+ """Sync an event to Supabase (fire-and-forget).
97
+
98
+ Maps the gateway event dict to the Supabase events table schema:
99
+ id (uuid, required), type (text, required), tool (text, required),
100
+ ts, model, status, venture, detail, user_id, session_id
101
+ """
102
+ try:
103
+ client = _get_client()
104
+ if client is None:
105
+ return
106
+ row = {
107
+ "id": str(uuid.uuid4()),
108
+ "type": event.get("type", "tool_call"),
109
+ "tool": event.get("tool", "unknown"),
110
+ "ts": event.get("ts", ""),
111
+ "model": event.get("model", ""),
112
+ "status": event.get("status", "ok"),
113
+ "venture": event.get("venture", ""),
114
+ "session_id": event.get("session_id", ""),
115
+ "user_id": event.get("user_id", ""),
116
+ }
117
+ # Include risk_level and trace info in detail field
118
+ detail_parts = []
119
+ if event.get("risk_level"):
120
+ detail_parts.append(f"risk={event['risk_level']}")
121
+ if event.get("trace_id"):
122
+ detail_parts.append(f"trace={event['trace_id']}")
123
+ if event.get("span_id"):
124
+ detail_parts.append(f"span={event['span_id']}")
125
+ if detail_parts:
126
+ row["detail"] = " ".join(detail_parts)
127
+
128
+ if client == "http":
129
+ _http_post("events", row)
130
+ else:
131
+ client.table("events").insert(row).execute()
132
+ except Exception as e:
133
+ logger.debug(f"Event sync failed: {e}")
134
+
135
+
136
+ def sync_ledger_item(item: dict):
137
+ """Sync a ledger item to Supabase (upsert).
138
+
139
+ Maps the gateway ledger item to the Supabase ledger_items table schema:
140
+ id (text, required), title (text, required), priority, venture,
141
+ status, description, source, note, assignee
142
+ """
143
+ try:
144
+ client = _get_client()
145
+ if client is None:
146
+ return
147
+ row = {
148
+ "id": item.get("id", ""),
149
+ "title": item.get("title", ""),
150
+ "priority": item.get("priority", "P1"),
151
+ "venture": item.get("venture", ""),
152
+ "status": item.get("status", "open"),
153
+ "description": item.get("description", ""),
154
+ "source": item.get("source", "mcp"),
155
+ }
156
+ if not row["id"] or not row["title"]:
157
+ return # Required fields missing
158
+ if client == "http":
159
+ _http_post(
160
+ "ledger_items",
161
+ row,
162
+ headers_extra={
163
+ "Prefer": "resolution=merge-duplicates,return=minimal",
164
+ },
165
+ )
166
+ else:
167
+ client.table("ledger_items").upsert(row).execute()
168
+ except Exception as e:
169
+ logger.debug(f"Ledger item sync failed: {e}")
170
+
171
+
172
+ def sync_ledger_update(item_id: str, status: str, note: str = ""):
173
+ """Sync a ledger status update to Supabase."""
174
+ try:
175
+ client = _get_client()
176
+ if client is None:
177
+ return
178
+ update = {"status": status}
179
+ if note:
180
+ update["note"] = note
181
+ if status == "done":
182
+ from datetime import datetime, timezone
183
+ update["completed_at"] = datetime.now(timezone.utc).isoformat()
184
+
185
+ if client == "http":
186
+ _http_patch("ledger_items", f"id=eq.{item_id}", update)
187
+ else:
188
+ client.table("ledger_items").update(update).eq("id", item_id).execute()
189
+ except Exception as e:
190
+ logger.debug(f"Ledger update sync failed: {e}")
@@ -0,0 +1,195 @@
1
+ """Distributed tracing for agent tool calls.
2
+
3
+ STR-053: Every tool call is part of a trace -- from prompt to tool to artifact to outcome.
4
+ Traces are stored as JSONL files in ~/.delimit/traces/ for local-first observability.
5
+ """
6
+ import json
7
+ import time
8
+ from pathlib import Path
9
+ from datetime import datetime, timezone
10
+
11
+ TRACES_DIR = Path.home() / ".delimit" / "traces"
12
+
13
+ _span_seq = 0
14
+
15
+
16
+ def start_span(trace_id: str, tool: str, args: dict = None) -> dict:
17
+ """Start a trace span."""
18
+ global _span_seq
19
+ _span_seq += 1
20
+ TRACES_DIR.mkdir(parents=True, exist_ok=True)
21
+ span = {
22
+ "trace_id": trace_id,
23
+ "span_id": f"{trace_id}-{int(time.time()*1000) % 10000:04d}-{_span_seq}",
24
+ "tool": tool,
25
+ "started_at": datetime.now(timezone.utc).isoformat(),
26
+ "status": "running",
27
+ "args_summary": str(args)[:200] if args else "",
28
+ }
29
+ trace_file = TRACES_DIR / f"trace-{trace_id}.jsonl"
30
+ with open(trace_file, "a") as f:
31
+ f.write(json.dumps(span) + "\n")
32
+ return span
33
+
34
+
35
+ def end_span(trace_id: str, span_id: str, status: str = "ok", result_summary: str = ""):
36
+ """End a trace span."""
37
+ span = {
38
+ "trace_id": trace_id,
39
+ "span_id": span_id,
40
+ "ended_at": datetime.now(timezone.utc).isoformat(),
41
+ "status": status,
42
+ "result_summary": result_summary[:200],
43
+ }
44
+ trace_file = TRACES_DIR / f"trace-{trace_id}.jsonl"
45
+ with open(trace_file, "a") as f:
46
+ f.write(json.dumps(span) + "\n")
47
+
48
+
49
+ def get_trace(trace_id: str) -> list:
50
+ """Get all spans for a trace, merging start/end records by span_id."""
51
+ trace_file = TRACES_DIR / f"trace-{trace_id}.jsonl"
52
+ if not trace_file.exists():
53
+ return []
54
+ spans = {}
55
+ for line in trace_file.read_text().splitlines():
56
+ try:
57
+ s = json.loads(line)
58
+ sid = s.get("span_id", "")
59
+ if sid in spans:
60
+ spans[sid].update(s)
61
+ else:
62
+ spans[sid] = s
63
+ except Exception:
64
+ pass
65
+ return sorted(spans.values(), key=lambda s: s.get("started_at", ""))
66
+
67
+
68
+ def list_traces(limit: int = 20) -> list:
69
+ """List recent traces."""
70
+ if not TRACES_DIR.exists():
71
+ return []
72
+ traces = []
73
+ for f in sorted(TRACES_DIR.glob("trace-*.jsonl"), reverse=True)[:limit]:
74
+ try:
75
+ lines = f.read_text().splitlines()
76
+ first = json.loads(lines[0]) if lines else {}
77
+ span_count = len(set(
78
+ json.loads(l).get("span_id", "") for l in lines if l.strip()
79
+ ))
80
+ traces.append({
81
+ "trace_id": first.get("trace_id", f.stem.replace("trace-", "")),
82
+ "started_at": first.get("started_at", ""),
83
+ "span_count": span_count,
84
+ "first_tool": first.get("tool", ""),
85
+ })
86
+ except Exception:
87
+ pass
88
+ return traces
89
+
90
+
91
+ def write_demo_traces() -> list:
92
+ """Generate demo trace data for UI development. Returns trace IDs created."""
93
+ TRACES_DIR.mkdir(parents=True, exist_ok=True)
94
+ now = time.time()
95
+ demo_traces = []
96
+
97
+ # Trace 1: Successful lint + diff + ledger workflow
98
+ t1 = "demo-a1b2"
99
+ spans_1 = [
100
+ {"tool": "delimit_lint", "duration_ms": 120, "status": "ok", "args": "old=api_v1.yaml new=api_v2.yaml", "result": "2 breaking changes found"},
101
+ {"tool": "delimit_diff", "duration_ms": 85, "status": "ok", "args": "old=api_v1.yaml new=api_v2.yaml", "result": "5 changes: 2 breaking, 1 deprecation, 2 additions"},
102
+ {"tool": "delimit_ledger_add", "duration_ms": 45, "status": "ok", "args": "title=Fix breaking changes priority=high", "result": "Created DLM-105"},
103
+ {"tool": "delimit_deliberate", "duration_ms": 3200, "status": "ok", "args": "question=Should we ship v2 with breaks?", "result": "Consensus: delay release, add migration guide"},
104
+ {"tool": "delimit_explain", "duration_ms": 210, "status": "ok", "args": "template=migration", "result": "Migration guide generated for 2 endpoints"},
105
+ ]
106
+ trace_file = TRACES_DIR / f"trace-{t1}.jsonl"
107
+ with open(trace_file, "w") as f:
108
+ for i, s in enumerate(spans_1):
109
+ offset = sum(sp["duration_ms"] for sp in spans_1[:i]) / 1000
110
+ started = now - 300 + offset
111
+ span_id = f"{t1}-{i:04d}"
112
+ start_record = {
113
+ "trace_id": t1,
114
+ "span_id": span_id,
115
+ "tool": s["tool"],
116
+ "started_at": datetime.fromtimestamp(started, tz=timezone.utc).isoformat(),
117
+ "status": "running",
118
+ "args_summary": s["args"],
119
+ }
120
+ end_record = {
121
+ "trace_id": t1,
122
+ "span_id": span_id,
123
+ "ended_at": datetime.fromtimestamp(started + s["duration_ms"] / 1000, tz=timezone.utc).isoformat(),
124
+ "status": s["status"],
125
+ "result_summary": s["result"],
126
+ }
127
+ f.write(json.dumps(start_record) + "\n")
128
+ f.write(json.dumps(end_record) + "\n")
129
+ demo_traces.append(t1)
130
+
131
+ # Trace 2: Deploy workflow with a blocked step
132
+ t2 = "demo-c3d4"
133
+ spans_2 = [
134
+ {"tool": "delimit_deploy_plan", "duration_ms": 150, "status": "ok", "args": "service=gateway version=3.3.0", "result": "Plan: build, test, publish"},
135
+ {"tool": "delimit_deploy_build", "duration_ms": 890, "status": "warn", "args": "service=gateway", "result": "Built with 2 warnings (deprecated deps)"},
136
+ {"tool": "delimit_deploy_publish", "duration_ms": 12, "status": "blocked", "args": "service=gateway target=prod", "result": "Blocked: requires approval (high risk)"},
137
+ ]
138
+ trace_file = TRACES_DIR / f"trace-{t2}.jsonl"
139
+ with open(trace_file, "w") as f:
140
+ for i, s in enumerate(spans_2):
141
+ offset = sum(sp["duration_ms"] for sp in spans_2[:i]) / 1000
142
+ started = now - 600 + offset
143
+ span_id = f"{t2}-{i:04d}"
144
+ start_record = {
145
+ "trace_id": t2,
146
+ "span_id": span_id,
147
+ "tool": s["tool"],
148
+ "started_at": datetime.fromtimestamp(started, tz=timezone.utc).isoformat(),
149
+ "status": "running",
150
+ "args_summary": s["args"],
151
+ }
152
+ end_record = {
153
+ "trace_id": t2,
154
+ "span_id": span_id,
155
+ "ended_at": datetime.fromtimestamp(started + s["duration_ms"] / 1000, tz=timezone.utc).isoformat(),
156
+ "status": s["status"],
157
+ "result_summary": s["result"],
158
+ }
159
+ f.write(json.dumps(start_record) + "\n")
160
+ f.write(json.dumps(end_record) + "\n")
161
+ demo_traces.append(t2)
162
+
163
+ # Trace 3: Security scan workflow
164
+ t3 = "demo-e5f6"
165
+ spans_3 = [
166
+ {"tool": "delimit_scan", "duration_ms": 340, "status": "ok", "args": "path=./project", "result": "Project scanned: FastAPI, 76 tools, OpenAPI spec found"},
167
+ {"tool": "delimit_security_scan", "duration_ms": 1200, "status": "warn", "args": "path=./project", "result": "1 high severity: outdated cryptography package"},
168
+ {"tool": "delimit_ledger_add", "duration_ms": 38, "status": "ok", "args": "title=Upgrade cryptography package priority=high", "result": "Created SEC-001"},
169
+ ]
170
+ trace_file = TRACES_DIR / f"trace-{t3}.jsonl"
171
+ with open(trace_file, "w") as f:
172
+ for i, s in enumerate(spans_3):
173
+ offset = sum(sp["duration_ms"] for sp in spans_3[:i]) / 1000
174
+ started = now - 900 + offset
175
+ span_id = f"{t3}-{i:04d}"
176
+ start_record = {
177
+ "trace_id": t3,
178
+ "span_id": span_id,
179
+ "tool": s["tool"],
180
+ "started_at": datetime.fromtimestamp(started, tz=timezone.utc).isoformat(),
181
+ "status": "running",
182
+ "args_summary": s["args"],
183
+ }
184
+ end_record = {
185
+ "trace_id": t3,
186
+ "span_id": span_id,
187
+ "ended_at": datetime.fromtimestamp(started + s["duration_ms"] / 1000, tz=timezone.utc).isoformat(),
188
+ "status": s["status"],
189
+ "result_summary": s["result"],
190
+ }
191
+ f.write(json.dumps(start_record) + "\n")
192
+ f.write(json.dumps(end_record) + "\n")
193
+ demo_traces.append(t3)
194
+
195
+ return demo_traces
@@ -3,7 +3,7 @@ Delimit Contract Ledger
3
3
  Reads, validates, and queries the append-only JSONL event ledger.
4
4
  Optional SQLite index for fast lookups (never required for CI).
5
5
 
6
- Per Delimit architecture:
6
+ Per Jamsons Doctrine:
7
7
  - Deterministic outputs
8
8
  - Append-only artifacts
9
9
  - SQLite index is optional, not required for CI
@@ -5,7 +5,7 @@ Constructs a deterministic service dependency graph from manifests.
5
5
  The graph maps each API/service to its downstream consumers,
6
6
  enabling impact analysis when an API contract changes.
7
7
 
8
- Per Delimit architecture:
8
+ Per Jamsons Doctrine:
9
9
  - Deterministic outputs (sorted, reproducible)
10
10
  - No telemetry
11
11
  - Graceful degradation when manifests are missing
@@ -2,7 +2,7 @@
2
2
  Delimit Dependency Manifest
3
3
  Parses and validates .delimit/dependencies.yaml service dependency declarations.
4
4
 
5
- Per Delimit architecture:
5
+ Per Jamsons Doctrine:
6
6
  - Deterministic outputs
7
7
  - No credential discovery
8
8
  - No telemetry