delimit-cli 4.5.13 → 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/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 +2 -2
- package/server.json +4 -4
|
@@ -45,10 +45,19 @@ from typing import Any, Dict, Iterable, Optional, Set, Tuple
|
|
|
45
45
|
|
|
46
46
|
logger = logging.getLogger(__name__)
|
|
47
47
|
|
|
48
|
-
# Default cooldown window for topic coverage.
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
48
|
+
# Default cooldown window for topic coverage.
|
|
49
|
+
# Original founder directive (2026-05-05): 7 days to avoid spam-pattern detection.
|
|
50
|
+
# Revised 2026-05-12 (LED-1356, panel-unanimous): 7 days × cross-platform global
|
|
51
|
+
# scope was producing 57% abstention rate (115/200 recent abstentions =
|
|
52
|
+
# topic_cooldown) and effectively blocked every Delimit-relevant topic for a
|
|
53
|
+
# full week after one draft. Reduced to 48h AND scoped per-platform/per-subreddit
|
|
54
|
+
# so a Reddit/r/mcp draft no longer cools an HN draft on MCP, and a Reddit/r/mcp
|
|
55
|
+
# draft no longer cools a Reddit/r/programming draft on MCP. Different audiences,
|
|
56
|
+
# different scans.
|
|
57
|
+
DEFAULT_COOLDOWN_HOURS = 48
|
|
58
|
+
# Backwards-compat alias for any callers that still pass `cooldown_days=` —
|
|
59
|
+
# converted to hours internally. Don't add new callers using this name.
|
|
60
|
+
DEFAULT_COOLDOWN_DAYS = DEFAULT_COOLDOWN_HOURS / 24
|
|
52
61
|
|
|
53
62
|
# Default high-engagement opportunity-cost threshold. Threads above this score
|
|
54
63
|
# pass the fit floor even without keyword match, but with ``human_only=True``
|
|
@@ -179,18 +188,63 @@ def _parse_iso(value: Optional[str]) -> Optional[datetime]:
|
|
|
179
188
|
return dt
|
|
180
189
|
|
|
181
190
|
|
|
191
|
+
def _log_entry_scope_key(entry: dict) -> str:
|
|
192
|
+
"""LED-1356 platform-scoped cooldown key. Reddit cooldowns are per-subreddit
|
|
193
|
+
(different subreddits = different audiences); HN/devto/X are global within
|
|
194
|
+
the platform. Cross-platform doesn't cool.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
- "reddit:r/<name>" for Reddit entries with a subreddit field
|
|
198
|
+
- "reddit" for Reddit entries without (legacy fallback)
|
|
199
|
+
- The platform name itself for hn / devto / x / twitter
|
|
200
|
+
- "unknown" when the entry doesn't carry a platform
|
|
201
|
+
"""
|
|
202
|
+
platform = (entry.get("platform") or "").lower()
|
|
203
|
+
if not platform:
|
|
204
|
+
return "unknown"
|
|
205
|
+
if platform == "reddit":
|
|
206
|
+
sub = (entry.get("subreddit") or "").lower()
|
|
207
|
+
if sub:
|
|
208
|
+
sub = sub.lstrip("r/")
|
|
209
|
+
return f"reddit:r/{sub}"
|
|
210
|
+
return "reddit"
|
|
211
|
+
# twitter and x are aliases for the same audience
|
|
212
|
+
if platform == "twitter":
|
|
213
|
+
return "x"
|
|
214
|
+
return platform
|
|
215
|
+
|
|
216
|
+
|
|
182
217
|
def _recent_topic_fingerprints(
|
|
183
|
-
|
|
218
|
+
cooldown_hours: float = DEFAULT_COOLDOWN_HOURS,
|
|
184
219
|
log_path: Optional[Path] = None,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
220
|
+
cooldown_days: Optional[float] = None,
|
|
221
|
+
) -> Dict[str, Set[str]]:
|
|
222
|
+
"""Return platform-scoped topic fingerprints from ``social_log.jsonl``
|
|
223
|
+
within the cooldown window.
|
|
224
|
+
|
|
225
|
+
LED-1356 changes (2026-05-12, panel-unanimous):
|
|
226
|
+
- Window default 7d → 48h.
|
|
227
|
+
- Return shape Set[str] → Dict[scope_key, Set[str]] so callers can ask
|
|
228
|
+
"what topics has THIS audience seen recently" instead of the prior
|
|
229
|
+
"global union of every topic across every platform."
|
|
230
|
+
- `cooldown_days` is preserved as backwards-compat kwarg; converted to
|
|
231
|
+
hours internally.
|
|
232
|
+
|
|
233
|
+
Callers should look up the relevant set via the dict and pass it to
|
|
234
|
+
``evaluate_fit(..., recent_topics=...)``. A target's scope is computed
|
|
235
|
+
via :func:`_log_entry_scope_key` on the target dict.
|
|
236
|
+
|
|
237
|
+
Returns dict mapping scope_key → set of canonical signal fingerprints
|
|
238
|
+
drafted on for that scope within the window.
|
|
188
239
|
"""
|
|
189
240
|
p = log_path or _social_log_path()
|
|
190
241
|
if not p.exists():
|
|
191
|
-
return
|
|
192
|
-
|
|
193
|
-
|
|
242
|
+
return {}
|
|
243
|
+
# Backwards compatibility: if cooldown_days passed, convert to hours.
|
|
244
|
+
if cooldown_days is not None:
|
|
245
|
+
cooldown_hours = float(cooldown_days) * 24.0
|
|
246
|
+
cutoff = datetime.now(timezone.utc) - timedelta(hours=cooldown_hours)
|
|
247
|
+
seen: Dict[str, Set[str]] = {}
|
|
194
248
|
try:
|
|
195
249
|
with open(p, "r", encoding="utf-8") as fh:
|
|
196
250
|
for line in fh:
|
|
@@ -204,6 +258,7 @@ def _recent_topic_fingerprints(
|
|
|
204
258
|
ts = _parse_iso(entry.get("ts"))
|
|
205
259
|
if ts is None or ts < cutoff:
|
|
206
260
|
continue
|
|
261
|
+
scope_key = _log_entry_scope_key(entry)
|
|
207
262
|
# Use the post body + thread title (when present) as the
|
|
208
263
|
# topic surface. Reddit entries log the thread title
|
|
209
264
|
# separately; X entries don't have one but the post body
|
|
@@ -214,12 +269,55 @@ def _recent_topic_fingerprints(
|
|
|
214
269
|
entry.get("thread_title") or "",
|
|
215
270
|
]
|
|
216
271
|
)
|
|
217
|
-
|
|
272
|
+
fps = _extract_topic_fingerprint(blob)
|
|
273
|
+
if not fps:
|
|
274
|
+
continue
|
|
275
|
+
seen.setdefault(scope_key, set()).update(fps)
|
|
218
276
|
except OSError as exc:
|
|
219
277
|
logger.warning("fit_floor: failed to read %s: %s", p, exc)
|
|
220
278
|
return seen
|
|
221
279
|
|
|
222
280
|
|
|
281
|
+
def topics_for_scope(
|
|
282
|
+
recent_topics_dict: Dict[str, Set[str]],
|
|
283
|
+
scope_key: str,
|
|
284
|
+
) -> Set[str]:
|
|
285
|
+
"""Convenience helper: look up the topic set for a given scope.
|
|
286
|
+
|
|
287
|
+
Returns empty set if the scope has no recent activity in the window.
|
|
288
|
+
Callers should pass the result as ``evaluate_fit(..., recent_topics=...)``.
|
|
289
|
+
"""
|
|
290
|
+
return recent_topics_dict.get(scope_key, set())
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def target_scope_key(target: dict) -> str:
|
|
294
|
+
"""Compute the scope key for a target dict (for cooldown lookup).
|
|
295
|
+
|
|
296
|
+
Mirror of :func:`_log_entry_scope_key` for the inbound target side.
|
|
297
|
+
Targets may carry `platform` plus (for Reddit) `subreddit` extracted
|
|
298
|
+
from the thread URL.
|
|
299
|
+
"""
|
|
300
|
+
platform = (target.get("platform") or "").lower()
|
|
301
|
+
if not platform:
|
|
302
|
+
return "unknown"
|
|
303
|
+
if platform == "reddit":
|
|
304
|
+
sub = (target.get("subreddit") or "").lower()
|
|
305
|
+
if not sub:
|
|
306
|
+
# Try to extract from canonical_url / thread_url
|
|
307
|
+
url = target.get("canonical_url") or target.get("thread_url") or ""
|
|
308
|
+
import re as _re
|
|
309
|
+
m = _re.search(r"/r/([A-Za-z0-9_]+)", url)
|
|
310
|
+
if m:
|
|
311
|
+
sub = m.group(1).lower()
|
|
312
|
+
if sub:
|
|
313
|
+
sub = sub.lstrip("r/")
|
|
314
|
+
return f"reddit:r/{sub}"
|
|
315
|
+
return "reddit"
|
|
316
|
+
if platform == "twitter":
|
|
317
|
+
return "x"
|
|
318
|
+
return platform
|
|
319
|
+
|
|
320
|
+
|
|
223
321
|
# ── Fit-floor decision ──────────────────────────────────────────────
|
|
224
322
|
|
|
225
323
|
|
|
@@ -348,6 +446,7 @@ def append_jsonl(path: Path, payload: Dict[str, Any]) -> None:
|
|
|
348
446
|
|
|
349
447
|
__all__ = [
|
|
350
448
|
"DEFAULT_COOLDOWN_DAYS",
|
|
449
|
+
"DEFAULT_COOLDOWN_HOURS",
|
|
351
450
|
"DEFAULT_HIGH_ENGAGEMENT_FLOOR",
|
|
352
451
|
"SOCIAL_LOG",
|
|
353
452
|
"DELIMIT_DOMAIN_SIGNALS",
|
|
@@ -357,4 +456,7 @@ __all__ = [
|
|
|
357
456
|
"append_jsonl",
|
|
358
457
|
"_extract_topic_fingerprint",
|
|
359
458
|
"_recent_topic_fingerprints",
|
|
459
|
+
"_log_entry_scope_key",
|
|
460
|
+
"target_scope_key",
|
|
461
|
+
"topics_for_scope",
|
|
360
462
|
]
|