delimit-cli 4.7.3 → 4.7.5
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 +87 -118
- 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 +143 -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 +247 -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
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,91 @@ 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
|
+
BINDINGS = [
|
|
542
|
+
Binding("y", "approve", "Approve", key_display="Y"),
|
|
543
|
+
Binding("n", "reject", "Reject", key_display="N"),
|
|
544
|
+
]
|
|
545
|
+
|
|
546
|
+
def compose(self) -> ComposeResult:
|
|
547
|
+
yield DataTable(id="approvals-table")
|
|
548
|
+
|
|
549
|
+
def on_mount(self) -> None:
|
|
550
|
+
table = self.query_one("#approvals-table", DataTable)
|
|
551
|
+
table.add_columns("ID", "Kind", "Target", "Status", "Age")
|
|
552
|
+
table.cursor_type = "row"
|
|
553
|
+
self._refresh_data()
|
|
554
|
+
self.set_interval(10, self._refresh_data)
|
|
555
|
+
|
|
556
|
+
def _refresh_data(self) -> None:
|
|
557
|
+
table = self.query_one("#approvals-table", DataTable)
|
|
558
|
+
table.clear()
|
|
559
|
+
self.items = _load_pending_approvals(25)
|
|
560
|
+
for item in self.items:
|
|
561
|
+
table.add_row(
|
|
562
|
+
item.get("draft_id", "")[:12],
|
|
563
|
+
item.get("draft_kind", ""),
|
|
564
|
+
item.get("target_summary", "")[:40],
|
|
565
|
+
item.get("status", ""),
|
|
566
|
+
item.get("age_str", ""),
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
def action_approve(self) -> None:
|
|
570
|
+
self._handle_action("approve")
|
|
571
|
+
|
|
572
|
+
def action_reject(self) -> None:
|
|
573
|
+
self._handle_action("reject")
|
|
574
|
+
|
|
575
|
+
def _handle_action(self, action: str) -> None:
|
|
576
|
+
table = self.query_one("#approvals-table", DataTable)
|
|
577
|
+
cursor_row = table.cursor_row
|
|
578
|
+
if cursor_row is None or not hasattr(self, "items") or cursor_row >= len(self.items):
|
|
579
|
+
self.app.notify("No draft selected.", severity="warning")
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
item = self.items[cursor_row]
|
|
583
|
+
draft_id = item.get("draft_id")
|
|
584
|
+
current_status = item.get("status")
|
|
585
|
+
|
|
586
|
+
if not draft_id or not current_status:
|
|
587
|
+
self.app.notify("Invalid draft data selected.", severity="error")
|
|
588
|
+
return
|
|
589
|
+
|
|
590
|
+
from .inbox_drafts import transition
|
|
591
|
+
db_path = DELIMIT_HOME / "drafts.db"
|
|
592
|
+
new_status = "approved" if action == "approve" else "cancelled"
|
|
593
|
+
|
|
594
|
+
try:
|
|
595
|
+
success = transition(
|
|
596
|
+
draft_id,
|
|
597
|
+
expected=current_status,
|
|
598
|
+
new=new_status,
|
|
599
|
+
db_path=db_path
|
|
600
|
+
)
|
|
601
|
+
if success:
|
|
602
|
+
self.app.notify(
|
|
603
|
+
f"Draft {draft_id[:12]} {action}d successfully!",
|
|
604
|
+
title="Action Succeeded",
|
|
605
|
+
severity="information"
|
|
606
|
+
)
|
|
607
|
+
self._refresh_data()
|
|
608
|
+
else:
|
|
609
|
+
self.app.notify(
|
|
610
|
+
f"Failed to {action} draft {draft_id[:12]}: state mismatch.",
|
|
611
|
+
title="Action Failed",
|
|
612
|
+
severity="warning"
|
|
613
|
+
)
|
|
614
|
+
except Exception as e:
|
|
615
|
+
self.app.notify(
|
|
616
|
+
f"Error performing {action}: {e}",
|
|
617
|
+
title="System Error",
|
|
618
|
+
severity="error"
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
|
|
491
622
|
class FilesystemPanel(Static):
|
|
492
623
|
"""Filesystem browser -- navigate .delimit/ directory tree."""
|
|
493
624
|
|
|
@@ -774,6 +905,7 @@ class DelimitOS(App):
|
|
|
774
905
|
BINDINGS = [
|
|
775
906
|
Binding("q", "quit", "Quit", key_display="Q"),
|
|
776
907
|
Binding("l", "focus_ledger", "Ledger", key_display="L"),
|
|
908
|
+
Binding("a", "focus_approvals", "Approvals", key_display="A"),
|
|
777
909
|
Binding("s", "focus_swarm", "Swarm", key_display="S"),
|
|
778
910
|
Binding("n", "focus_notifications", "Notifications", key_display="N"),
|
|
779
911
|
Binding("f", "focus_files", "Files", key_display="F"),
|
|
@@ -788,6 +920,8 @@ class DelimitOS(App):
|
|
|
788
920
|
def compose(self) -> ComposeResult:
|
|
789
921
|
yield GovernanceBar()
|
|
790
922
|
with TabbedContent():
|
|
923
|
+
with TabPane("Approvals", id="tab-approvals"):
|
|
924
|
+
yield ApprovalsPanel()
|
|
791
925
|
with TabPane("Ledger", id="tab-ledger"):
|
|
792
926
|
yield LedgerPanel()
|
|
793
927
|
with TabPane("Swarm", id="tab-swarm"):
|
|
@@ -806,6 +940,13 @@ class DelimitOS(App):
|
|
|
806
940
|
|
|
807
941
|
# -- Tab focus actions -----------------------------------------------------
|
|
808
942
|
|
|
943
|
+
def action_focus_approvals(self) -> None:
|
|
944
|
+
self.query_one(TabbedContent).active = "tab-approvals"
|
|
945
|
+
try:
|
|
946
|
+
self.query_one("#approvals-table", DataTable).focus()
|
|
947
|
+
except Exception:
|
|
948
|
+
pass
|
|
949
|
+
|
|
809
950
|
def action_focus_ledger(self) -> None:
|
|
810
951
|
self.query_one(TabbedContent).active = "tab-ledger"
|
|
811
952
|
|
|
@@ -831,6 +972,8 @@ class DelimitOS(App):
|
|
|
831
972
|
|
|
832
973
|
def action_refresh(self) -> None:
|
|
833
974
|
"""Refresh all panels."""
|
|
975
|
+
for panel in self.query(ApprovalsPanel):
|
|
976
|
+
panel._refresh_data()
|
|
834
977
|
for panel in self.query(LedgerPanel):
|
|
835
978
|
panel._refresh_data()
|
|
836
979
|
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
|
|