delimit-cli 4.7.3 → 4.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/delimit-cli.js +152 -1
- package/bin/delimit-setup.js +88 -6
- package/bin/delimit.js +10 -25
- package/gateway/ai/backends/governance_bridge.py +52 -0
- package/gateway/ai/backends/repo_bridge.py +12 -0
- package/gateway/ai/backends/tools_infra.py +43 -1
- package/gateway/ai/cli_contract.py +12 -0
- package/gateway/ai/custom_gemini_repl.py +80 -0
- package/gateway/ai/delimit_daemon.py +8 -0
- package/gateway/ai/gemini_vertex_shim.py +38 -0
- package/gateway/ai/license_core.cpython-310-x86_64-linux-gnu.so +0 -0
- package/gateway/ai/release_sync.py +43 -8
- package/gateway/ai/route_daemon.py +98 -0
- package/gateway/ai/server.py +71 -1
- package/gateway/ai/session_phoenix.py +101 -136
- package/gateway/ai/supabase_sync.py +58 -0
- package/gateway/ai/swarm.py +2 -0
- package/gateway/ai/tui.py +81 -0
- package/gateway/core/ci_formatter.py +89 -61
- package/gateway/core/diff_engine_v2.py +208 -627
- package/gateway/core/explainer.py +67 -34
- package/lib/ai-sbom-engine.js +1 -0
- package/lib/auth-setup.js +10 -1
- package/lib/chat-repl.js +244 -0
- package/lib/cross-model-hooks.js +111 -0
- package/lib/timeline-engine.js +60 -0
- package/lib/wrap-engine.js +67 -11
- package/package.json +1 -1
|
@@ -159,6 +159,64 @@ def _http_patch(table: str, query: str, data: dict) -> bool:
|
|
|
159
159
|
return False
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
def _http_upload_storage(
|
|
163
|
+
bucket: str,
|
|
164
|
+
object_path: str,
|
|
165
|
+
body: bytes,
|
|
166
|
+
content_type: str = "application/json",
|
|
167
|
+
) -> bool:
|
|
168
|
+
"""Upload one object to Supabase Storage using the REST API."""
|
|
169
|
+
import urllib.parse
|
|
170
|
+
import urllib.request
|
|
171
|
+
try:
|
|
172
|
+
safe_bucket = urllib.parse.quote(bucket.strip("/"), safe="")
|
|
173
|
+
safe_object = urllib.parse.quote(object_path.strip("/"), safe="/")
|
|
174
|
+
url = f"{SUPABASE_URL.rstrip('/')}/storage/v1/object/{safe_bucket}/{safe_object}"
|
|
175
|
+
req = urllib.request.Request(url, data=body, method="POST")
|
|
176
|
+
req.add_header("Content-Type", content_type)
|
|
177
|
+
req.add_header("Cache-Control", "3600")
|
|
178
|
+
req.add_header("apikey", SUPABASE_KEY)
|
|
179
|
+
req.add_header("Authorization", f"Bearer {SUPABASE_KEY}")
|
|
180
|
+
req.add_header("x-upsert", "true")
|
|
181
|
+
urllib.request.urlopen(req, timeout=5)
|
|
182
|
+
return True
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.debug(f"Supabase Storage upload failed for {bucket}/{object_path}: {e}")
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def sync_attestation_bundle(
|
|
189
|
+
bundle_path: str,
|
|
190
|
+
attestation_id: str = "",
|
|
191
|
+
bucket: str = "",
|
|
192
|
+
) -> bool:
|
|
193
|
+
"""Best-effort mirror of a signed attestation JSON bundle to Supabase Storage.
|
|
194
|
+
|
|
195
|
+
Local files remain the source of truth. This only makes /att/<id> and the
|
|
196
|
+
dashboard index able to discover bundles without committing static JSON.
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
client = _get_client()
|
|
200
|
+
if client is None:
|
|
201
|
+
return False
|
|
202
|
+
if not bundle_path:
|
|
203
|
+
return False
|
|
204
|
+
path = Path(bundle_path)
|
|
205
|
+
if not path.exists() or not path.is_file():
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
object_id = attestation_id or path.stem
|
|
209
|
+
object_path = f"{object_id}.json" if not object_id.endswith(".json") else object_id
|
|
210
|
+
storage_bucket = bucket or os.environ.get(
|
|
211
|
+
"DELIMIT_ATTESTATION_BUCKET",
|
|
212
|
+
"attestations",
|
|
213
|
+
)
|
|
214
|
+
return _http_upload_storage(storage_bucket, object_path, path.read_bytes())
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.debug(f"Attestation bundle sync failed: {e}")
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
|
|
162
220
|
def sync_event(event: dict):
|
|
163
221
|
"""Sync an event to Supabase (fire-and-forget).
|
|
164
222
|
|
package/gateway/ai/swarm.py
CHANGED
|
@@ -964,6 +964,8 @@ def hot_reload(reason: str = "update") -> Dict[str, Any]:
|
|
|
964
964
|
"ai.reddit_scanner",
|
|
965
965
|
"ai.ledger_manager",
|
|
966
966
|
"ai.backends.repo_bridge",
|
|
967
|
+
"ai.backends.governance_bridge",
|
|
968
|
+
"backends.governance_bridge",
|
|
967
969
|
"ai.backends.tools_infra",
|
|
968
970
|
"backends.repo_bridge", # alias used by server.py lazy imports
|
|
969
971
|
"ai.social_target", # depends on ai.social
|
package/gateway/ai/tui.py
CHANGED
|
@@ -24,6 +24,7 @@ from textual.binding import Binding
|
|
|
24
24
|
import json
|
|
25
25
|
import os
|
|
26
26
|
import subprocess
|
|
27
|
+
import sqlite3
|
|
27
28
|
import time
|
|
28
29
|
from datetime import datetime, timezone
|
|
29
30
|
from pathlib import Path
|
|
@@ -136,6 +137,51 @@ def _load_daemon_state() -> Dict[str, Any]:
|
|
|
136
137
|
return {"status": "unknown"}
|
|
137
138
|
|
|
138
139
|
|
|
140
|
+
|
|
141
|
+
def _load_pending_approvals(limit: int = 20) -> List[Dict]:
|
|
142
|
+
"""Load pending drafts from the SQLite registry (LED-1129)."""
|
|
143
|
+
db_path = DELIMIT_HOME / "drafts.db"
|
|
144
|
+
if not db_path.exists():
|
|
145
|
+
return []
|
|
146
|
+
|
|
147
|
+
approvals = []
|
|
148
|
+
try:
|
|
149
|
+
# Connect read-only to avoid locking issues with the daemon
|
|
150
|
+
with sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) as conn:
|
|
151
|
+
conn.row_factory = sqlite3.Row
|
|
152
|
+
cursor = conn.execute(
|
|
153
|
+
"SELECT * FROM drafts WHERE status IN ('pending', 'waiting_for_approval') "
|
|
154
|
+
"ORDER BY created_at DESC LIMIT ?",
|
|
155
|
+
(limit,)
|
|
156
|
+
)
|
|
157
|
+
for row in cursor:
|
|
158
|
+
d = dict(row)
|
|
159
|
+
# Parse target_json for a summary
|
|
160
|
+
try:
|
|
161
|
+
target = json.loads(d.get("target_json", "{}"))
|
|
162
|
+
d["target_summary"] = target.get("repo", target.get("venture", "unknown"))
|
|
163
|
+
if "issue" in target:
|
|
164
|
+
d["target_summary"] += f" #{target['issue']}"
|
|
165
|
+
except:
|
|
166
|
+
d["target_summary"] = "unknown"
|
|
167
|
+
|
|
168
|
+
# Calculate age
|
|
169
|
+
created_at = d.get("created_at", 0)
|
|
170
|
+
if created_at:
|
|
171
|
+
diff = int(time.time()) - created_at
|
|
172
|
+
if diff < 60: d["age_str"] = f"{diff}s"
|
|
173
|
+
elif diff < 3600: d["age_str"] = f"{diff//60}m"
|
|
174
|
+
elif diff < 86400: d["age_str"] = f"{diff//3600}h"
|
|
175
|
+
else: d["age_str"] = f"{diff//86400}d"
|
|
176
|
+
else:
|
|
177
|
+
d["age_str"] = "n/a"
|
|
178
|
+
|
|
179
|
+
approvals.append(d)
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
return approvals
|
|
183
|
+
|
|
184
|
+
|
|
139
185
|
def _load_process_list() -> List[Dict[str, Any]]:
|
|
140
186
|
"""Build a list of known daemons with status from state files and alerts."""
|
|
141
187
|
processes = []
|
|
@@ -488,6 +534,33 @@ def _channel_color(channel: str) -> str:
|
|
|
488
534
|
return colors.get(channel, "white")
|
|
489
535
|
|
|
490
536
|
|
|
537
|
+
|
|
538
|
+
class ApprovalsPanel(Static):
|
|
539
|
+
"""Pending approvals view -- shows items from drafts.db."""
|
|
540
|
+
|
|
541
|
+
def compose(self) -> ComposeResult:
|
|
542
|
+
yield DataTable(id="approvals-table")
|
|
543
|
+
|
|
544
|
+
def on_mount(self) -> None:
|
|
545
|
+
table = self.query_one("#approvals-table", DataTable)
|
|
546
|
+
table.add_columns("ID", "Kind", "Target", "Status", "Age")
|
|
547
|
+
table.cursor_type = "row"
|
|
548
|
+
self._refresh_data()
|
|
549
|
+
self.set_interval(10, self._refresh_data)
|
|
550
|
+
|
|
551
|
+
def _refresh_data(self) -> None:
|
|
552
|
+
table = self.query_one("#approvals-table", DataTable)
|
|
553
|
+
table.clear()
|
|
554
|
+
for item in _load_pending_approvals(25):
|
|
555
|
+
table.add_row(
|
|
556
|
+
item.get("draft_id", "")[:12],
|
|
557
|
+
item.get("draft_kind", ""),
|
|
558
|
+
item.get("target_summary", "")[:40],
|
|
559
|
+
item.get("status", ""),
|
|
560
|
+
item.get("age_str", ""),
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
|
|
491
564
|
class FilesystemPanel(Static):
|
|
492
565
|
"""Filesystem browser -- navigate .delimit/ directory tree."""
|
|
493
566
|
|
|
@@ -774,6 +847,7 @@ class DelimitOS(App):
|
|
|
774
847
|
BINDINGS = [
|
|
775
848
|
Binding("q", "quit", "Quit", key_display="Q"),
|
|
776
849
|
Binding("l", "focus_ledger", "Ledger", key_display="L"),
|
|
850
|
+
Binding("a", "focus_approvals", "Approvals", key_display="A"),
|
|
777
851
|
Binding("s", "focus_swarm", "Swarm", key_display="S"),
|
|
778
852
|
Binding("n", "focus_notifications", "Notifications", key_display="N"),
|
|
779
853
|
Binding("f", "focus_files", "Files", key_display="F"),
|
|
@@ -788,6 +862,8 @@ class DelimitOS(App):
|
|
|
788
862
|
def compose(self) -> ComposeResult:
|
|
789
863
|
yield GovernanceBar()
|
|
790
864
|
with TabbedContent():
|
|
865
|
+
with TabPane("Approvals", id="tab-approvals"):
|
|
866
|
+
yield ApprovalsPanel()
|
|
791
867
|
with TabPane("Ledger", id="tab-ledger"):
|
|
792
868
|
yield LedgerPanel()
|
|
793
869
|
with TabPane("Swarm", id="tab-swarm"):
|
|
@@ -806,6 +882,9 @@ class DelimitOS(App):
|
|
|
806
882
|
|
|
807
883
|
# -- Tab focus actions -----------------------------------------------------
|
|
808
884
|
|
|
885
|
+
def action_focus_approvals(self) -> None:
|
|
886
|
+
self.query_one(TabbedContent).active = "tab-approvals"
|
|
887
|
+
|
|
809
888
|
def action_focus_ledger(self) -> None:
|
|
810
889
|
self.query_one(TabbedContent).active = "tab-ledger"
|
|
811
890
|
|
|
@@ -831,6 +910,8 @@ class DelimitOS(App):
|
|
|
831
910
|
|
|
832
911
|
def action_refresh(self) -> None:
|
|
833
912
|
"""Refresh all panels."""
|
|
913
|
+
for panel in self.query(ApprovalsPanel):
|
|
914
|
+
panel._refresh_data()
|
|
834
915
|
for panel in self.query(LedgerPanel):
|
|
835
916
|
panel._refresh_data()
|
|
836
917
|
for panel in self.query(SwarmPanel):
|
|
@@ -100,89 +100,117 @@ class CIFormatter:
|
|
|
100
100
|
decision = result.get("decision", "unknown")
|
|
101
101
|
violations = result.get("violations", [])
|
|
102
102
|
summary = result.get("summary", {})
|
|
103
|
-
semver = result.get("semver")
|
|
103
|
+
semver = result.get("semver")
|
|
104
|
+
all_changes = result.get("all_changes", [])
|
|
105
|
+
migration = result.get("migration")
|
|
104
106
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
bump = semver.get("bump", "unknown")
|
|
109
|
-
bump_badge = {"major": " `MAJOR`", "minor": " `MINOR`", "patch": " `PATCH`", "none": ""}.get(bump, "")
|
|
107
|
+
bc = summary.get("breaking_changes", 0)
|
|
108
|
+
total = summary.get("total_changes", 0)
|
|
109
|
+
additive = total - bc
|
|
110
110
|
|
|
111
|
-
if
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
errors = [v for v in violations if v.get("severity") == "error"]
|
|
112
|
+
warnings = [v for v in violations if v.get("severity") == "warning"]
|
|
113
|
+
|
|
114
|
+
if bc == 0:
|
|
115
|
+
# ── GREEN PATH ──
|
|
116
|
+
bump_label = "NONE"
|
|
117
|
+
if semver:
|
|
118
|
+
bump_label = semver.get("bump", "none").upper()
|
|
119
|
+
lines.append("\U0001f6e1\ufe0f **Governance Passed**\n")
|
|
120
|
+
if total > 0:
|
|
121
|
+
lines.append(
|
|
122
|
+
f"> **No breaking API changes detected.** "
|
|
123
|
+
f"{additive} additive change{'s' if additive != 1 else ''} "
|
|
124
|
+
f"found \u2014 Semver: **{bump_label}**\n"
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
lines.append("> **No breaking API changes detected.**\n")
|
|
128
|
+
|
|
129
|
+
# Additive changes
|
|
130
|
+
safe_changes = [c for c in all_changes if not c.get("is_breaking")]
|
|
131
|
+
if safe_changes and len(safe_changes) <= 15:
|
|
132
|
+
lines.append("<details>")
|
|
133
|
+
lines.append(f"<summary>\u2705 New additions ({len(safe_changes)})</summary>\n")
|
|
134
|
+
for c in safe_changes:
|
|
135
|
+
lines.append(f"- `{c.get('path', '')}` \u2014 {c.get('message', '')}")
|
|
136
|
+
lines.append("</details>\n")
|
|
115
137
|
else:
|
|
116
|
-
|
|
138
|
+
# ── RED PATH ──
|
|
139
|
+
lines.append("\U0001f6e1\ufe0f **Breaking API Changes Detected**\n")
|
|
117
140
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
lines.append(f"| Total changes | {summary.get('total_changes', 0)} |")
|
|
126
|
-
lines.append(f"| Breaking | {summary.get('breaking_changes', 0)} |")
|
|
127
|
-
if summary.get("violations", 0) > 0:
|
|
128
|
-
lines.append(f"| Policy violations | {summary['violations']} |")
|
|
129
|
-
lines.append("")
|
|
141
|
+
# Summary card
|
|
142
|
+
parts = [f"\U0001f534 **{bc} breaking change{'s' if bc != 1 else ''}**"]
|
|
143
|
+
parts.append("Semver: **MAJOR**")
|
|
144
|
+
if semver and semver.get("next_version"):
|
|
145
|
+
parts.append(f"Next: `{semver['next_version']}`")
|
|
146
|
+
separator = " \u00b7 "
|
|
147
|
+
lines.append(f"> {separator.join(parts)}\n")
|
|
130
148
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
149
|
+
# Stats table
|
|
150
|
+
lines.append("| | Count |")
|
|
151
|
+
lines.append("|---|---|")
|
|
152
|
+
lines.append(f"| Total changes | {total} |")
|
|
153
|
+
lines.append(f"| Breaking | {bc} |")
|
|
154
|
+
lines.append(f"| Additive | {additive} |")
|
|
155
|
+
if len(warnings) > 0:
|
|
156
|
+
lines.append(f"| Warnings | {len(warnings)} |")
|
|
157
|
+
if summary.get("violations", 0) > 0:
|
|
158
|
+
lines.append(f"| Policy violations | {summary['violations']} |")
|
|
159
|
+
lines.append("")
|
|
135
160
|
|
|
161
|
+
# Violations table
|
|
136
162
|
if errors or warnings:
|
|
137
|
-
lines.append("###
|
|
138
|
-
lines.append("| Severity |
|
|
139
|
-
lines.append("
|
|
163
|
+
lines.append("### Breaking Changes\n")
|
|
164
|
+
lines.append("| Severity | Change | Location |")
|
|
165
|
+
lines.append("|----------|--------|----------|")
|
|
140
166
|
|
|
141
167
|
for v in errors:
|
|
142
|
-
rule = v.get("name", v.get("rule", "Unknown"))
|
|
143
168
|
desc = v.get("message", "Unknown violation")
|
|
144
169
|
location = v.get("path", "-")
|
|
145
|
-
lines.append(f"|
|
|
170
|
+
lines.append(f"| \U0001f534 Critical | {desc} | `{location}` |")
|
|
146
171
|
|
|
147
172
|
for v in warnings:
|
|
148
|
-
rule = v.get("name", v.get("rule", "Unknown"))
|
|
149
173
|
desc = v.get("message", "Unknown warning")
|
|
150
174
|
location = v.get("path", "-")
|
|
151
|
-
lines.append(f"|
|
|
175
|
+
lines.append(f"| \U0001f7e1 Warning | {desc} | `{location}` |")
|
|
152
176
|
|
|
153
177
|
lines.append("")
|
|
154
178
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
lines.append(
|
|
164
|
-
|
|
165
|
-
|
|
179
|
+
# Migration guidance
|
|
180
|
+
if migration and decision == "fail":
|
|
181
|
+
lines.append("<details>")
|
|
182
|
+
lines.append("<summary>\U0001f4cb Migration guide</summary>\n")
|
|
183
|
+
lines.append(migration)
|
|
184
|
+
lines.append("\n</details>\n")
|
|
185
|
+
elif errors and decision == "fail":
|
|
186
|
+
lines.append("<details>")
|
|
187
|
+
lines.append("<summary>\U0001f4cb Migration guide</summary>\n")
|
|
188
|
+
lines.append("1. **Restore removed endpoints** \u2014 deprecate before removing")
|
|
189
|
+
lines.append("2. **Make parameters optional** \u2014 don't add required params")
|
|
190
|
+
lines.append("3. **Use versioning** \u2014 create `/v2/` for breaking changes")
|
|
191
|
+
lines.append("4. **Gradual migration** \u2014 provide guides and time")
|
|
192
|
+
lines.append("\n</details>\n")
|
|
166
193
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
194
|
+
# Additive changes
|
|
195
|
+
safe_changes = [c for c in all_changes if not c.get("is_breaking")]
|
|
196
|
+
if safe_changes and len(safe_changes) <= 15:
|
|
197
|
+
lines.append("<details>")
|
|
198
|
+
lines.append(f"<summary>\u2705 New additions ({len(safe_changes)})</summary>\n")
|
|
199
|
+
for c in safe_changes:
|
|
200
|
+
lines.append(f"- `{c.get('path', '')}` \u2014 {c.get('message', '')}")
|
|
201
|
+
lines.append("</details>\n")
|
|
174
202
|
|
|
175
|
-
|
|
176
|
-
if violations and decision == "fail" and not migration:
|
|
177
|
-
lines.append("### 💡 How to Fix\n")
|
|
178
|
-
lines.append("1. **Restore removed endpoints** — deprecate before removing")
|
|
179
|
-
lines.append("2. **Make parameters optional** — don't add required params")
|
|
180
|
-
lines.append("3. **Use versioning** — create `/v2/` for breaking changes")
|
|
181
|
-
lines.append("4. **Gradual migration** — provide guides and time")
|
|
182
|
-
lines.append("")
|
|
203
|
+
lines.append("> **Fix locally:** `npx delimit-cli lint`\n")
|
|
183
204
|
|
|
184
205
|
lines.append("---")
|
|
185
|
-
lines.append(
|
|
206
|
+
lines.append(
|
|
207
|
+
"Powered by [Delimit](https://delimit.ai) \u00b7 "
|
|
208
|
+
"[Docs](https://delimit.ai/docs) \u00b7 "
|
|
209
|
+
"[Install](https://github.com/marketplace/actions/delimit-api-governance)"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if bc == 0:
|
|
213
|
+
lines.append("\nKeep Building.")
|
|
186
214
|
|
|
187
215
|
return "\n".join(lines)
|
|
188
216
|
|