delimit-cli 4.5.12 → 4.6.0
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/CHANGELOG.md +45 -0
- package/README.md +9 -8
- package/bin/delimit-cli.js +162 -1
- package/bin/delimit-setup.js +46 -6
- package/gateway/ai/_compile_status.py +154 -0
- package/gateway/ai/agent_dispatch.py +36 -0
- package/gateway/ai/backends/tools_infra.py +150 -10
- package/gateway/ai/daemon.py +10 -0
- package/gateway/ai/daily_digest.py +1 -2
- package/gateway/ai/delimit_daemon.py +67 -0
- package/gateway/ai/dispatch_gate.py +399 -0
- package/gateway/ai/hot_reload.py +1 -2
- package/gateway/ai/led193_daemon/executor.py +9 -0
- package/gateway/ai/ledger_manager.py +9 -0
- package/gateway/ai/license_core.cpython-310-x86_64-linux-gnu.so +0 -0
- package/gateway/ai/license_core.pyi +17 -19
- package/gateway/ai/notify.py +39 -0
- package/gateway/ai/outreach_substantive.py +676 -0
- package/gateway/ai/reaper.py +70 -0
- package/gateway/ai/reddit_scanner.py +10 -5
- package/gateway/ai/sensing/schema.py +1 -1
- package/gateway/ai/sensing/signal_store.py +0 -1
- package/gateway/ai/server.py +5171 -1462
- package/gateway/ai/social_capability/fit_floor.py +114 -12
- package/gateway/ai/tdqs_lint.py +611 -0
- package/gateway/ai/usage_allowlist.py +198 -0
- package/gateway/ai/workers/base.py +2 -2
- package/gateway/ai/workers/executor.py +32 -3
- package/gateway/ai/workers/outreach_drafter.py +0 -1
- package/gateway/ai/workers/pr_drafter.py +0 -1
- package/gateway/ai/x_ranker.py +12 -2
- package/gateway/core/json_schema_diff.py +25 -1
- package/lib/auth-signin.js +136 -0
- package/lib/auth-signout.js +169 -0
- package/lib/delimit-template.js +11 -0
- package/lib/migration-2092-banner.js +213 -0
- package/package.json +3 -3
- package/server.json +4 -4
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""LED-1279: dispatcher anti-duplicate gate.
|
|
2
|
+
|
|
3
|
+
Before creating a new agent task tagged with an LED ID, check whether any
|
|
4
|
+
local repository's git history already contains a commit referencing that
|
|
5
|
+
LED. If it does, refuse the dispatch and auto-close the LED — yesterday's
|
|
6
|
+
AGT-65A61AD5 wasted three subagent cycles on LEDs already shipped in
|
|
7
|
+
commit 014fb5c (PR #106) on 2026-05-03.
|
|
8
|
+
|
|
9
|
+
Cost model: each duplicate dispatch burns 5-30 minutes of subagent + orchestrator
|
|
10
|
+
attention. This gate pays for itself within 1-2 future dispatches.
|
|
11
|
+
|
|
12
|
+
Design notes:
|
|
13
|
+
- LED-id parsing is conservative: r"LED-\\d+" only. AGT-... and STR-...
|
|
14
|
+
do not trigger the gate — only operational ledger items.
|
|
15
|
+
- Repo discovery: prefer caller-supplied list, then fall back to a small
|
|
16
|
+
static list of canonical Delimit / wire-report / livetube / dv repos.
|
|
17
|
+
A missing repo logs a warning and is skipped (don't fail dispatch on
|
|
18
|
+
infra issues — that's a worse failure mode than a false negative).
|
|
19
|
+
- Match scope: only commits on the *first-parent* line of `main` count
|
|
20
|
+
as "shipped" so feature-branch WIP that mentions an LED but never
|
|
21
|
+
merged doesn't trigger a false positive. PR-merge commits qualify.
|
|
22
|
+
- Time window: only commits with author/commit date >= LED.created_at
|
|
23
|
+
are considered. An LED can't have been "shipped" before it existed.
|
|
24
|
+
- Multiple matches: the FIRST match (oldest commit since created_at) wins.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
import re
|
|
31
|
+
import subprocess
|
|
32
|
+
import time
|
|
33
|
+
from datetime import datetime, timezone
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Iterable, Optional
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
# Default repos to search. Discovered at runtime first via the venture
|
|
40
|
+
# registry; this list is the safety net if discovery returns nothing.
|
|
41
|
+
DEFAULT_REPOS = (
|
|
42
|
+
"/home/delimit/delimit-gateway",
|
|
43
|
+
"/home/delimit/delimit-action",
|
|
44
|
+
"/home/delimit/npm-delimit",
|
|
45
|
+
"/home/delimit/delimit-ui",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Conservative LED-id matcher. Plain LED-NNN format.
|
|
49
|
+
LED_ID_RE = re.compile(r"\bLED-(\d+)\b", re.IGNORECASE)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def extract_led_id(*texts: str) -> Optional[str]:
|
|
53
|
+
"""Return the first LED-NNN id found in any of the supplied strings.
|
|
54
|
+
|
|
55
|
+
Used by the dispatcher to grab the LED tag from title/description/context
|
|
56
|
+
without forcing callers to plumb it as a separate parameter.
|
|
57
|
+
"""
|
|
58
|
+
for text in texts:
|
|
59
|
+
if not text:
|
|
60
|
+
continue
|
|
61
|
+
m = LED_ID_RE.search(text)
|
|
62
|
+
if m:
|
|
63
|
+
return f"LED-{m.group(1)}"
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def discover_repos() -> list[str]:
|
|
68
|
+
"""Return the list of repo paths to search.
|
|
69
|
+
|
|
70
|
+
Reads the venture registry (~/.delimit/ventures.json) and unions in the
|
|
71
|
+
DEFAULT_REPOS safety net. Filters to existing directories that actually
|
|
72
|
+
contain a .git subdir — pseudo-ventures pointing at /tmp paths or stale
|
|
73
|
+
entries get dropped silently.
|
|
74
|
+
"""
|
|
75
|
+
seen: list[str] = []
|
|
76
|
+
seen_set: set[str] = set()
|
|
77
|
+
|
|
78
|
+
def _add(path: str) -> None:
|
|
79
|
+
if not path:
|
|
80
|
+
return
|
|
81
|
+
p = Path(path).resolve()
|
|
82
|
+
s = str(p)
|
|
83
|
+
if s in seen_set:
|
|
84
|
+
return
|
|
85
|
+
if not (p / ".git").exists():
|
|
86
|
+
return
|
|
87
|
+
seen_set.add(s)
|
|
88
|
+
seen.append(s)
|
|
89
|
+
|
|
90
|
+
# 1. Venture registry — ~/.delimit/ventures.json
|
|
91
|
+
try:
|
|
92
|
+
from ai.ledger_manager import VENTURES_FILE # late import: tests stub VENTURES_FILE
|
|
93
|
+
import json
|
|
94
|
+
|
|
95
|
+
if VENTURES_FILE.exists():
|
|
96
|
+
ventures = json.loads(VENTURES_FILE.read_text())
|
|
97
|
+
for info in ventures.values():
|
|
98
|
+
_add(info.get("path", ""))
|
|
99
|
+
except Exception as e: # pragma: no cover — best effort
|
|
100
|
+
logger.debug("dispatch_gate: venture registry read failed: %s", e)
|
|
101
|
+
|
|
102
|
+
# 2. Safety net defaults
|
|
103
|
+
for path in DEFAULT_REPOS:
|
|
104
|
+
_add(path)
|
|
105
|
+
|
|
106
|
+
return seen
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parse_iso_z(value: str) -> Optional[datetime]:
|
|
110
|
+
"""Parse an ISO-8601 timestamp (with optional trailing Z) to a tz-aware UTC datetime."""
|
|
111
|
+
if not value:
|
|
112
|
+
return None
|
|
113
|
+
v = value.strip()
|
|
114
|
+
if v.endswith("Z"):
|
|
115
|
+
v = v[:-1] + "+00:00"
|
|
116
|
+
try:
|
|
117
|
+
dt = datetime.fromisoformat(v)
|
|
118
|
+
except ValueError:
|
|
119
|
+
return None
|
|
120
|
+
if dt.tzinfo is None:
|
|
121
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
122
|
+
return dt.astimezone(timezone.utc)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _git_log_first_parent_grep(
|
|
126
|
+
repo: str,
|
|
127
|
+
led_id: str,
|
|
128
|
+
since_iso: Optional[str],
|
|
129
|
+
timeout_sec: float = 8.0,
|
|
130
|
+
) -> list[tuple[str, str, str]]:
|
|
131
|
+
"""Run `git log --first-parent main` filtered by --grep=<led_id>.
|
|
132
|
+
|
|
133
|
+
Returns a list of (sha, iso_date, subject) tuples, oldest first. Empty
|
|
134
|
+
list if the repo isn't a git checkout, the ref doesn't exist, or git
|
|
135
|
+
times out / errors. Errors are logged at DEBUG; we never raise — a
|
|
136
|
+
missing repo must NOT fail dispatch.
|
|
137
|
+
"""
|
|
138
|
+
repo_path = Path(repo)
|
|
139
|
+
if not (repo_path / ".git").exists():
|
|
140
|
+
logger.debug("dispatch_gate: repo missing or not a git checkout: %s", repo)
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
cmd = [
|
|
144
|
+
"git",
|
|
145
|
+
"-C",
|
|
146
|
+
str(repo_path),
|
|
147
|
+
"log",
|
|
148
|
+
"--first-parent",
|
|
149
|
+
"main",
|
|
150
|
+
f"--grep={led_id}",
|
|
151
|
+
"-i", # case-insensitive grep — match LED-1208 / led-1208 / Led-1208
|
|
152
|
+
"--pretty=%H%x09%cI%x09%s",
|
|
153
|
+
"--reverse", # oldest first so [0] is the FIRST match
|
|
154
|
+
]
|
|
155
|
+
if since_iso:
|
|
156
|
+
cmd.append(f"--since={since_iso}")
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
result = subprocess.run(
|
|
160
|
+
cmd,
|
|
161
|
+
capture_output=True,
|
|
162
|
+
text=True,
|
|
163
|
+
timeout=timeout_sec,
|
|
164
|
+
check=False,
|
|
165
|
+
)
|
|
166
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
167
|
+
logger.warning("dispatch_gate: git log failed for %s: %s", repo, e)
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
if result.returncode != 0:
|
|
171
|
+
# Try `master` as fallback for repos that haven't renamed yet, but
|
|
172
|
+
# only for the specific "unknown revision" failure — anything else
|
|
173
|
+
# is a real error we should silently skip.
|
|
174
|
+
stderr = result.stderr or ""
|
|
175
|
+
if "unknown revision" in stderr.lower() or "ambiguous argument 'main'" in stderr.lower():
|
|
176
|
+
cmd[5] = "master"
|
|
177
|
+
try:
|
|
178
|
+
result = subprocess.run(
|
|
179
|
+
cmd, capture_output=True, text=True, timeout=timeout_sec, check=False
|
|
180
|
+
)
|
|
181
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
182
|
+
logger.warning("dispatch_gate: git log master fallback failed for %s: %s", repo, e)
|
|
183
|
+
return []
|
|
184
|
+
if result.returncode != 0:
|
|
185
|
+
logger.debug("dispatch_gate: git log non-zero for %s: %s", repo, result.stderr)
|
|
186
|
+
return []
|
|
187
|
+
else:
|
|
188
|
+
logger.debug("dispatch_gate: git log non-zero for %s: %s", repo, result.stderr)
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
matches: list[tuple[str, str, str]] = []
|
|
192
|
+
for line in (result.stdout or "").splitlines():
|
|
193
|
+
if not line:
|
|
194
|
+
continue
|
|
195
|
+
parts = line.split("\t", 2)
|
|
196
|
+
if len(parts) < 3:
|
|
197
|
+
continue
|
|
198
|
+
matches.append((parts[0], parts[1], parts[2]))
|
|
199
|
+
return matches
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def is_led_already_shipped(
|
|
203
|
+
led_id: str,
|
|
204
|
+
created_at: str = "",
|
|
205
|
+
repos: Optional[Iterable[str]] = None,
|
|
206
|
+
) -> tuple[bool, Optional[dict]]:
|
|
207
|
+
"""Search the given repos for a commit on `main`'s first-parent line that
|
|
208
|
+
mentions ``led_id`` and was committed at or after ``created_at``.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
led_id: e.g. "LED-1208" — must be the bare ID, not "LED-1208 fix something".
|
|
212
|
+
created_at: ISO-8601 timestamp of when the LED was opened. Commits
|
|
213
|
+
older than this are treated as unrelated mentions and ignored.
|
|
214
|
+
Empty string disables the time filter (used by the sweep, which
|
|
215
|
+
is willing to accept older matches at the cost of false positives).
|
|
216
|
+
repos: optional iterable of repo paths. Defaults to discover_repos().
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
(shipped: bool, details: dict | None)
|
|
220
|
+
details, when shipped, is:
|
|
221
|
+
{
|
|
222
|
+
"repo": "/home/delimit/delimit-gateway",
|
|
223
|
+
"sha": "014fb5c...",
|
|
224
|
+
"short_sha": "014fb5c",
|
|
225
|
+
"date": "2026-05-03T17:13:45-04:00",
|
|
226
|
+
"subject": "fix(self-repair): ...",
|
|
227
|
+
}
|
|
228
|
+
"""
|
|
229
|
+
if not led_id or not LED_ID_RE.fullmatch(led_id):
|
|
230
|
+
return False, None
|
|
231
|
+
|
|
232
|
+
# Normalize LED ID to canonical "LED-NNN" so the grep is consistent.
|
|
233
|
+
norm_id = led_id.upper()
|
|
234
|
+
|
|
235
|
+
repo_list = list(repos) if repos is not None else discover_repos()
|
|
236
|
+
if not repo_list:
|
|
237
|
+
return False, None
|
|
238
|
+
|
|
239
|
+
since_iso: Optional[str] = None
|
|
240
|
+
since_dt = _parse_iso_z(created_at) if created_at else None
|
|
241
|
+
if since_dt is not None:
|
|
242
|
+
since_iso = since_dt.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
243
|
+
|
|
244
|
+
# Collect first match per repo, then pick the globally-oldest.
|
|
245
|
+
candidates: list[tuple[str, str, str, str]] = []
|
|
246
|
+
for repo in repo_list:
|
|
247
|
+
rows = _git_log_first_parent_grep(repo, norm_id, since_iso)
|
|
248
|
+
if not rows:
|
|
249
|
+
continue
|
|
250
|
+
# rows are oldest-first; defensively re-check the bare-id token is
|
|
251
|
+
# in the subject — `git log --grep` would already match but a stray
|
|
252
|
+
# "LED-12080" could match LED-1208's pattern if regex was loose.
|
|
253
|
+
# Our LED_ID_RE uses \b boundaries so we're safe; double-check anyway.
|
|
254
|
+
for sha, date_iso, subject in rows:
|
|
255
|
+
if not LED_ID_RE.search(subject):
|
|
256
|
+
continue
|
|
257
|
+
if since_dt is not None:
|
|
258
|
+
commit_dt = _parse_iso_z(date_iso)
|
|
259
|
+
if commit_dt is not None and commit_dt < since_dt:
|
|
260
|
+
continue
|
|
261
|
+
# Verify the LED token in the subject actually equals our target.
|
|
262
|
+
tokens = {f"LED-{m.group(1)}" for m in LED_ID_RE.finditer(subject)}
|
|
263
|
+
if norm_id not in tokens:
|
|
264
|
+
continue
|
|
265
|
+
candidates.append((repo, sha, date_iso, subject))
|
|
266
|
+
break # first match per repo
|
|
267
|
+
|
|
268
|
+
if not candidates:
|
|
269
|
+
return False, None
|
|
270
|
+
|
|
271
|
+
# Pick the oldest (smallest commit date) across all repos.
|
|
272
|
+
candidates.sort(key=lambda t: t[2])
|
|
273
|
+
repo, sha, date_iso, subject = candidates[0]
|
|
274
|
+
return True, {
|
|
275
|
+
"repo": repo,
|
|
276
|
+
"sha": sha,
|
|
277
|
+
"short_sha": sha[:7],
|
|
278
|
+
"date": date_iso,
|
|
279
|
+
"subject": subject,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def auto_close_shipped_led(led_id: str, details: dict) -> dict:
|
|
284
|
+
"""Mark an already-shipped LED as done with a note pointing to the commit.
|
|
285
|
+
|
|
286
|
+
Wraps ``ai.ledger_manager.update_item`` so the dispatcher's refusal path
|
|
287
|
+
has a single call site. Errors here MUST NOT propagate — if ledger update
|
|
288
|
+
fails for whatever reason (stale registry, missing file), we still want
|
|
289
|
+
to refuse the dispatch.
|
|
290
|
+
"""
|
|
291
|
+
try:
|
|
292
|
+
from ai.ledger_manager import update_item
|
|
293
|
+
|
|
294
|
+
note = (
|
|
295
|
+
f"Auto-closed by dispatcher gate (LED-1279): shipped in "
|
|
296
|
+
f"{details.get('repo','?')}@{details.get('short_sha','?')} on "
|
|
297
|
+
f"{details.get('date','?')}. "
|
|
298
|
+
f"Refused duplicate AGT dispatch."
|
|
299
|
+
)
|
|
300
|
+
result = update_item(
|
|
301
|
+
item_id=led_id,
|
|
302
|
+
status="done",
|
|
303
|
+
note=note,
|
|
304
|
+
worked_by="dispatcher-gate",
|
|
305
|
+
)
|
|
306
|
+
return {"updated": True, "result": result}
|
|
307
|
+
except Exception as e: # pragma: no cover — defensive
|
|
308
|
+
logger.warning("dispatch_gate: auto-close of %s failed: %s", led_id, e)
|
|
309
|
+
return {"updated": False, "error": str(e)}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def evaluate_dispatch(
|
|
313
|
+
title: str,
|
|
314
|
+
description: str = "",
|
|
315
|
+
context: str = "",
|
|
316
|
+
led_created_at: str = "",
|
|
317
|
+
repos: Optional[Iterable[str]] = None,
|
|
318
|
+
) -> Optional[dict]:
|
|
319
|
+
"""Run the gate: extract an LED id, check if shipped, return refusal payload or None.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
- None when dispatch should proceed (no LED tag, or LED not shipped).
|
|
323
|
+
- A refusal dict when dispatch should be blocked:
|
|
324
|
+
{
|
|
325
|
+
"status": "refused",
|
|
326
|
+
"reason": "led_already_shipped",
|
|
327
|
+
"led_id": "LED-1208",
|
|
328
|
+
"shipped_in": {"repo": ..., "sha": ..., "short_sha": ..., "date": ..., "subject": ...},
|
|
329
|
+
"auto_close": {...},
|
|
330
|
+
"message": "...",
|
|
331
|
+
}
|
|
332
|
+
"""
|
|
333
|
+
led_id = extract_led_id(title, description, context)
|
|
334
|
+
if not led_id:
|
|
335
|
+
return None # No LED tag — orchestrator may dispatch generic work.
|
|
336
|
+
|
|
337
|
+
shipped, details = is_led_already_shipped(
|
|
338
|
+
led_id, created_at=led_created_at, repos=repos
|
|
339
|
+
)
|
|
340
|
+
if not shipped or not details:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
auto_close = auto_close_shipped_led(led_id, details)
|
|
344
|
+
return {
|
|
345
|
+
"status": "refused",
|
|
346
|
+
"reason": "led_already_shipped",
|
|
347
|
+
"led_id": led_id,
|
|
348
|
+
"shipped_in": details,
|
|
349
|
+
"auto_close": auto_close,
|
|
350
|
+
"message": (
|
|
351
|
+
f"Refused: {led_id} already shipped in "
|
|
352
|
+
f"{details['repo']}@{details['short_sha']} on {details['date'][:10]} "
|
|
353
|
+
f"({details['subject'][:80]}). LED auto-closed."
|
|
354
|
+
),
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def lookup_led_created_at(led_id: str) -> str:
|
|
359
|
+
"""Look up the LED's created_at across all known ledger files.
|
|
360
|
+
|
|
361
|
+
Returns the original-create timestamp (genesis row, not the latest update).
|
|
362
|
+
Empty string when the LED isn't found anywhere — the gate then runs without
|
|
363
|
+
the time filter, accepting the small false-positive risk.
|
|
364
|
+
"""
|
|
365
|
+
if not led_id:
|
|
366
|
+
return ""
|
|
367
|
+
norm = led_id.upper()
|
|
368
|
+
try:
|
|
369
|
+
from ai.ledger_manager import LEDGER_V2_DIR
|
|
370
|
+
except Exception:
|
|
371
|
+
return ""
|
|
372
|
+
|
|
373
|
+
if not LEDGER_V2_DIR.exists():
|
|
374
|
+
return ""
|
|
375
|
+
|
|
376
|
+
import json
|
|
377
|
+
|
|
378
|
+
# Walk every ledger file under ledger-v2/* looking for the genesis row.
|
|
379
|
+
for jsonl in LEDGER_V2_DIR.rglob("*.jsonl"):
|
|
380
|
+
try:
|
|
381
|
+
with open(jsonl, "r") as f:
|
|
382
|
+
for line in f:
|
|
383
|
+
line = line.strip()
|
|
384
|
+
if not line:
|
|
385
|
+
continue
|
|
386
|
+
try:
|
|
387
|
+
row = json.loads(line)
|
|
388
|
+
except json.JSONDecodeError:
|
|
389
|
+
continue
|
|
390
|
+
if row.get("id") != norm:
|
|
391
|
+
continue
|
|
392
|
+
if row.get("type") == "update":
|
|
393
|
+
continue # only the genesis row carries created_at-of-LED
|
|
394
|
+
ts = row.get("created_at", "")
|
|
395
|
+
if ts:
|
|
396
|
+
return ts
|
|
397
|
+
except OSError:
|
|
398
|
+
continue
|
|
399
|
+
return ""
|
package/gateway/ai/hot_reload.py
CHANGED
|
@@ -35,11 +35,10 @@ import logging
|
|
|
35
35
|
import os
|
|
36
36
|
import sys
|
|
37
37
|
import threading
|
|
38
|
-
import time
|
|
39
38
|
import traceback
|
|
40
39
|
from datetime import datetime, timezone
|
|
41
40
|
from pathlib import Path
|
|
42
|
-
from typing import Any,
|
|
41
|
+
from typing import Any, Dict, List, Optional, Set
|
|
43
42
|
|
|
44
43
|
logger = logging.getLogger("delimit.ai.hot_reload")
|
|
45
44
|
|
|
@@ -346,11 +346,17 @@ def execute_format_fix(
|
|
|
346
346
|
return out
|
|
347
347
|
ok, reason = _ensure_clean_worktree(repo_path, runner=runner)
|
|
348
348
|
if not ok:
|
|
349
|
+
# Precondition mismatch (worktree dirty), not a profile failure.
|
|
350
|
+
# Mark skipped so the consecutive-failure breaker doesn't trip.
|
|
351
|
+
out.result = "skipped"
|
|
349
352
|
out.reason = reason
|
|
350
353
|
return out
|
|
351
354
|
|
|
352
355
|
cmd = _detect_format_command(repo_path)
|
|
353
356
|
if cmd is None:
|
|
357
|
+
# Repo has no formatter config — daemon can't run, but this is
|
|
358
|
+
# a setup gap not an executor failure. Skip rather than fail.
|
|
359
|
+
out.result = "skipped"
|
|
354
360
|
out.reason = "no_formatter_detected"
|
|
355
361
|
return out
|
|
356
362
|
|
|
@@ -429,11 +435,13 @@ def execute_lockfile_refresh(
|
|
|
429
435
|
return out
|
|
430
436
|
ok, reason = _ensure_clean_worktree(repo_path, runner=runner)
|
|
431
437
|
if not ok:
|
|
438
|
+
out.result = "skipped"
|
|
432
439
|
out.reason = reason
|
|
433
440
|
return out
|
|
434
441
|
|
|
435
442
|
detected = _detect_lockfile_command(repo_path)
|
|
436
443
|
if detected is None:
|
|
444
|
+
out.result = "skipped"
|
|
437
445
|
out.reason = "no_lockfile_or_manager_detected"
|
|
438
446
|
return out
|
|
439
447
|
cmd, _lockfile = detected
|
|
@@ -551,6 +559,7 @@ def execute_docs_typo(
|
|
|
551
559
|
return out
|
|
552
560
|
ok, reason = _ensure_clean_worktree(repo_path, runner=runner)
|
|
553
561
|
if not ok:
|
|
562
|
+
out.result = "skipped"
|
|
554
563
|
out.reason = reason
|
|
555
564
|
return out
|
|
556
565
|
|
|
@@ -619,7 +619,16 @@ def update_item(
|
|
|
619
619
|
if due_date:
|
|
620
620
|
update["due_date"] = due_date
|
|
621
621
|
if labels is not None:
|
|
622
|
+
# LED-2221: write to both `labels` and `tags`. The list_items
|
|
623
|
+
# reconstruction (around line ~870) merges update events into
|
|
624
|
+
# current state by checking the `tags` key only. Writing only
|
|
625
|
+
# `labels` silently drops the update at read time, which in
|
|
626
|
+
# particular meant the build daemon's `autonomous-build` tag
|
|
627
|
+
# check could never see tags written through the MCP. Keeping
|
|
628
|
+
# `labels` for any external consumer that reads the raw event
|
|
629
|
+
# stream; adding `tags` so the live state aggregator picks it up.
|
|
622
630
|
update["labels"] = labels
|
|
631
|
+
update["tags"] = labels
|
|
623
632
|
if blocked_by:
|
|
624
633
|
update["blocked_by"] = blocked_by
|
|
625
634
|
if blocks:
|
|
Binary file
|
|
@@ -1,42 +1,38 @@
|
|
|
1
1
|
# This file was generated by Nuitka
|
|
2
2
|
|
|
3
3
|
# Stubs included by default
|
|
4
|
-
|
|
4
|
+
import time
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
import hashlib
|
|
7
|
-
import json
|
|
8
|
-
import time
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
PRO_TOOLS = frozenset({'delimit_gov_evaluate', 'delimit_gov_policy', 'delimit_gov_run', 'delimit_gov_verify', 'delimit_os_plan', 'delimit_os_status', 'delimit_os_gates', 'delimit_deploy_plan', 'delimit_deploy_build', 'delimit_deploy_publish', 'delimit_deploy_verify', 'delimit_deploy_rollback', 'delimit_deploy_status', 'delimit_deploy_site', 'delimit_deploy_npm', 'delimit_memory_search', 'delimit_vault_search', 'delimit_vault_snapshot', 'delimit_vault_health', 'delimit_evidence_collect', 'delimit_evidence_verify', 'delimit_deliberate', 'delimit_models', 'delimit_obs_metrics', 'delimit_obs_logs', 'delimit_obs_status', 'delimit_release_plan', 'delimit_release_status', 'delimit_release_sync', 'delimit_cost_analyze', 'delimit_cost_optimize', 'delimit_cost_alert', 'delimit_social_post', 'delimit_social_generate', 'delimit_social_history', 'delimit_screen_record', 'delimit_screenshot', 'delimit_notify', 'delimit_agent_dispatch', 'delimit_agent_status', 'delimit_agent_complete', 'delimit_agent_handoff', 'delimit_executor'})
|
|
11
|
+
def needs_revalidation(data: dict) -> bool:
|
|
12
|
+
...
|
|
13
|
+
def revalidate_license(data: dict) -> dict:
|
|
14
|
+
...
|
|
15
|
+
def is_license_valid(data: dict) -> bool:
|
|
16
|
+
...
|
|
17
|
+
def _write_license(data: dict) -> None:
|
|
18
|
+
...
|
|
19
|
+
def _call_lemon_squeezy(data: dict) -> bool | None:
|
|
20
|
+
...
|
|
18
21
|
def load_license() -> dict:
|
|
19
22
|
...
|
|
20
|
-
|
|
21
23
|
def check_premium() -> bool:
|
|
22
24
|
...
|
|
23
|
-
|
|
24
25
|
def gate_tool(tool_name: str) -> dict | None:
|
|
25
26
|
...
|
|
26
|
-
|
|
27
27
|
def activate(key: str) -> dict:
|
|
28
28
|
...
|
|
29
|
-
|
|
30
29
|
def _revalidate(data: dict) -> dict:
|
|
31
30
|
...
|
|
32
|
-
|
|
33
31
|
def _get_monthly_usage(tool_name: str) -> int:
|
|
34
32
|
...
|
|
35
|
-
|
|
36
33
|
def _increment_usage(tool_name: str) -> int:
|
|
37
34
|
...
|
|
38
35
|
|
|
39
|
-
|
|
40
36
|
__name__ = ...
|
|
41
37
|
|
|
42
38
|
|
|
@@ -44,7 +40,9 @@ __name__ = ...
|
|
|
44
40
|
# Modules used internally, to allow implicit dependencies to be seen:
|
|
45
41
|
import hashlib
|
|
46
42
|
import json
|
|
43
|
+
import os
|
|
47
44
|
import time
|
|
48
45
|
import pathlib
|
|
49
46
|
import urllib
|
|
50
|
-
import urllib.request
|
|
47
|
+
import urllib.request
|
|
48
|
+
import re
|
package/gateway/ai/notify.py
CHANGED
|
@@ -919,6 +919,45 @@ def send_email(
|
|
|
919
919
|
"intent_logged": True,
|
|
920
920
|
}
|
|
921
921
|
|
|
922
|
+
# RFC 2606 reserved test domains — never relay. A test fixture in
|
|
923
|
+
# tests/test_notify_routing.py once passed email_to="lead@example.com"
|
|
924
|
+
# through to a real SMTP send, generating 220 spam-bounces against
|
|
925
|
+
# pro@delimit.ai before the fixture was patched. Refuse here so any
|
|
926
|
+
# future regression fails fast instead of silently poisoning sender rep.
|
|
927
|
+
# Tests that mock smtplib can set DELIMIT_ALLOW_TEST_RECIPIENTS=1 to
|
|
928
|
+
# bypass this guard since their mocked transport doesn't actually relay.
|
|
929
|
+
_RFC2606_TEST_DOMAINS = (
|
|
930
|
+
"example.com", "example.net", "example.org",
|
|
931
|
+
"test", "invalid", "localhost",
|
|
932
|
+
)
|
|
933
|
+
recipient_domain = smtp_to.rsplit("@", 1)[-1].strip().lower()
|
|
934
|
+
is_reserved = (
|
|
935
|
+
recipient_domain in _RFC2606_TEST_DOMAINS
|
|
936
|
+
or recipient_domain.endswith(".example")
|
|
937
|
+
or recipient_domain.endswith(".test")
|
|
938
|
+
or recipient_domain.endswith(".invalid")
|
|
939
|
+
or recipient_domain.endswith(".localhost")
|
|
940
|
+
)
|
|
941
|
+
if is_reserved and not os.environ.get("DELIMIT_ALLOW_TEST_RECIPIENTS"):
|
|
942
|
+
record = {
|
|
943
|
+
"channel": "email",
|
|
944
|
+
"event_type": event_type,
|
|
945
|
+
"to": smtp_to,
|
|
946
|
+
"from": smtp_from,
|
|
947
|
+
"subject": subject,
|
|
948
|
+
"timestamp": timestamp,
|
|
949
|
+
"success": False,
|
|
950
|
+
"reason": "reserved_test_domain",
|
|
951
|
+
}
|
|
952
|
+
_record_notification(record)
|
|
953
|
+
return {
|
|
954
|
+
"channel": "email",
|
|
955
|
+
"delivered": False,
|
|
956
|
+
"timestamp": timestamp,
|
|
957
|
+
"error": f"Refusing to send: '{smtp_to}' is an RFC 2606 reserved test domain.",
|
|
958
|
+
"intent_logged": True,
|
|
959
|
+
}
|
|
960
|
+
|
|
922
961
|
subj = subject or f"Delimit: {event_type or 'Notification'}"
|
|
923
962
|
html_body = _render_html_email(subj, email_body, event_type)
|
|
924
963
|
|