delimit-cli 4.5.2 → 4.5.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.
@@ -1,4 +1,4 @@
1
- """X engagement ranker (LED-216 Phase 2, Q4 of the 2026-05-02 distribution panel).
1
+ """X engagement ranker (LED-216 Phase 2 + LED-1240 part B selectivity bar).
2
2
 
3
3
  Filters and orders X (Twitter) candidate posts so we reply to the highest-
4
4
  yield originals only. The score formula and the 7-day author dedupe are the
@@ -15,7 +15,19 @@ Filter pipeline applied in order:
15
15
  2. ``lang == 'en'`` → drop non-English (US/UK builder community is the wedge)
16
16
  3. ``is_retweet == False`` → drop retweets
17
17
  4. dedupe authors we replied to in the last ``replied_authors_window_hours``
18
- 5. sort by score DESC
18
+ 5. **Engagement-score floor** (LED-1240 part B) — drop low-yield threads
19
+ outright. Configured via ``MIN_ENGAGEMENT_SCORE`` (default 1.5, was 0).
20
+ 6. **Delimit-fit floor** (LED-1240 part B) — drop threads that don't
21
+ match Delimit-domain or orbit-with-context signals. High-engagement
22
+ threads above ``HIGH_ENGAGEMENT_OPPORTUNITY_COST`` pass through but
23
+ are flagged ``_human_only=True`` so callers do NOT auto-draft.
24
+ 7. **Topic-coverage cooldown** (LED-1240 part B) — drop threads whose
25
+ matched signals overlap topics we drafted on inside the last 7 days.
26
+ 8. sort by score DESC
27
+
28
+ Rejected candidates are logged to ``~/.delimit/x_rejected_targets.jsonl``
29
+ (append-only, one JSON line per rejection) so we can audit the bar over
30
+ time without changing the return shape.
19
31
 
20
32
  Tolerant defaults: any field missing on a target is treated as "do not drop"
21
33
  so partial Twttr241 payloads still rank instead of being silently filtered.
@@ -32,9 +44,29 @@ logger = logging.getLogger(__name__)
32
44
 
33
45
 
34
46
  SOCIAL_LOG = Path.home() / ".delimit" / "social_log.jsonl"
47
+ REJECTED_TARGETS_LOG = Path.home() / ".delimit" / "x_rejected_targets.jsonl"
35
48
 
36
49
  DEFAULT_WINDOW_HOURS = 24 * 7 # 7 days, per founder's anti-spammy directive
37
50
 
51
+ # LED-1240 part B selectivity bar.
52
+ # Founder directive 2026-05-05: raise the engagement floor and add a fit
53
+ # floor BEFORE we waste LLM tokens drafting for noise-grade threads.
54
+ #
55
+ # Raising MIN_ENGAGEMENT_SCORE from 0 → 1.5 cuts the long tail of
56
+ # ``_rank_score=0.0`` candidates we observed in the recent X target log
57
+ # (every recent X target with 0 likes / 0 retweets had score=0.0). The
58
+ # threshold is conservative — a single-hour-old post with 1.5 likes still
59
+ # passes — but eliminates the zero-engagement floor.
60
+ MIN_ENGAGEMENT_SCORE = 1.5
61
+
62
+ # Threads above this score pass the Delimit-fit floor even without a
63
+ # keyword match, but are flagged ``_human_only=True`` so the caller does
64
+ # NOT auto-draft. The orchestrator decides whether to surface them.
65
+ HIGH_ENGAGEMENT_OPPORTUNITY_COST = 50.0
66
+
67
+ # Topic-coverage cooldown — same window as author dedupe (7 days).
68
+ DEFAULT_COOLDOWN_DAYS = 7
69
+
38
70
 
39
71
  # ---------------------------------------------------------------------------
40
72
  # Helpers
@@ -213,10 +245,44 @@ def _is_retweet(target: Dict[str, Any]) -> bool:
213
245
  return False
214
246
 
215
247
 
248
+ def _log_rejection(target: Dict[str, Any], score: float, reason: str,
249
+ extra: Optional[Dict[str, Any]] = None,
250
+ log_path: Optional[Path] = None) -> None:
251
+ """Append a single JSON line to the rejected-targets audit log.
252
+
253
+ Best-effort — never raises into the caller's pipeline.
254
+ """
255
+ p = log_path or REJECTED_TARGETS_LOG
256
+ snippet = (target.get("content_snippet") or target.get("text") or "")[:200]
257
+ payload: Dict[str, Any] = {
258
+ "ts": datetime.now(timezone.utc).isoformat(),
259
+ "tweet_id": target.get("source_id") or target.get("id_str") or target.get("id") or "",
260
+ "fingerprint": target.get("fingerprint") or "",
261
+ "author": target.get("author") or "",
262
+ "score": round(float(score), 4),
263
+ "snippet": snippet,
264
+ "reason": reason,
265
+ }
266
+ if extra:
267
+ payload.update(extra)
268
+ try:
269
+ p.parent.mkdir(parents=True, exist_ok=True)
270
+ with open(p, "a", encoding="utf-8") as fh:
271
+ fh.write(json.dumps(payload, ensure_ascii=False) + "\n")
272
+ except OSError as exc: # pragma: no cover — best-effort logging
273
+ logger.warning("x_ranker: failed to write rejection log %s: %s", p, exc)
274
+
275
+
216
276
  def rank_x_targets(
217
277
  targets: Iterable[Dict[str, Any]],
218
278
  replied_authors_window_hours: int = DEFAULT_WINDOW_HOURS,
219
279
  replied_authors: Optional[Set[str]] = None,
280
+ min_engagement_score: float = MIN_ENGAGEMENT_SCORE,
281
+ high_engagement_floor: float = HIGH_ENGAGEMENT_OPPORTUNITY_COST,
282
+ cooldown_days: int = DEFAULT_COOLDOWN_DAYS,
283
+ recent_topics: Optional[Set[str]] = None,
284
+ enable_fit_floor: bool = True,
285
+ log_rejections: bool = True,
220
286
  ) -> List[Dict[str, Any]]:
221
287
  """Filter and sort X targets by engagement.
222
288
 
@@ -227,18 +293,45 @@ def rank_x_targets(
227
293
  replied_authors: explicit author set (lowercase, no leading ``@``).
228
294
  When ``None``, the set is read from ``~/.delimit/social_log.jsonl``.
229
295
  Tests inject an explicit set to avoid touching disk.
296
+ min_engagement_score: hard floor on the engagement-rate score.
297
+ Threads below this are dropped before fit-floor evaluation.
298
+ Default ``MIN_ENGAGEMENT_SCORE`` (1.5).
299
+ high_engagement_floor: score above which a thread can pass the
300
+ fit floor without a keyword match (flagged ``_human_only=True``).
301
+ cooldown_days: topic-coverage cooldown window in days.
302
+ recent_topics: explicit set of topic fingerprints already drafted on.
303
+ ``None`` reads from ``social_log.jsonl``. Tests inject directly.
304
+ enable_fit_floor: kill switch for the LED-1240 part B gates. False
305
+ preserves the legacy behavior (no fit floor, no min-score, no
306
+ cooldown) — used by tests that target the legacy filter chain
307
+ in isolation. Default True.
308
+ log_rejections: when True, append rejection records to
309
+ ``~/.delimit/x_rejected_targets.jsonl``. Tests disable this to
310
+ keep the founder's audit log clean.
230
311
 
231
312
  Returns:
232
313
  A new list sorted by engagement score DESC. Each item gets a
233
314
  ``_rank_score`` key for downstream observability. Filtered items are
234
315
  dropped (not kept with score=0) so the caller can blindly slice the
235
- first N.
316
+ first N. High-engagement-but-off-topic items survive with
317
+ ``_human_only=True`` so the caller can route them to human review
318
+ without auto-drafting.
236
319
  """
237
320
  if replied_authors is None:
238
321
  replied_authors = _replied_authors_within(replied_authors_window_hours)
239
322
  else:
240
323
  replied_authors = {_normalize_handle(a) for a in replied_authors}
241
324
 
325
+ if enable_fit_floor and recent_topics is None:
326
+ try:
327
+ from ai.social_capability.fit_floor import _recent_topic_fingerprints
328
+ recent_topics = _recent_topic_fingerprints(cooldown_days=cooldown_days)
329
+ except Exception as exc: # pragma: no cover — tolerant fallback
330
+ logger.warning("x_ranker: cooldown bootstrap failed (%s) — proceeding without", exc)
331
+ recent_topics = set()
332
+ elif recent_topics is None:
333
+ recent_topics = set()
334
+
242
335
  survivors: List[Dict[str, Any]] = []
243
336
  for t in targets or []:
244
337
  if not isinstance(t, dict):
@@ -260,17 +353,65 @@ def rank_x_targets(
260
353
  if author_norm and author_norm in replied_authors:
261
354
  continue
262
355
 
356
+ score = round(score_target(t), 4)
357
+
358
+ # 5. engagement-score floor (LED-1240 part B). Off by default for
359
+ # legacy callers that pass enable_fit_floor=False.
360
+ if enable_fit_floor and score < min_engagement_score:
361
+ if log_rejections:
362
+ _log_rejection(
363
+ t, score, "below_engagement_floor",
364
+ extra={"min_score": min_engagement_score},
365
+ )
366
+ continue
367
+
368
+ # 6 + 7. Delimit-fit floor + topic cooldown.
263
369
  scored = dict(t)
264
- scored["_rank_score"] = round(score_target(t), 4)
370
+ scored["_rank_score"] = score
371
+
372
+ if enable_fit_floor:
373
+ try:
374
+ from ai.social_capability.fit_floor import evaluate_fit
375
+ except Exception as exc: # pragma: no cover — tolerant fallback
376
+ logger.warning("x_ranker: fit_floor import failed (%s) — passing through", exc)
377
+ survivors.append(scored)
378
+ continue
379
+
380
+ text = t.get("content_snippet") or t.get("text") or ""
381
+ verdict = evaluate_fit(
382
+ text,
383
+ engagement_score=score,
384
+ high_engagement_floor=high_engagement_floor,
385
+ recent_topics=recent_topics,
386
+ )
387
+ if not verdict["passed"]:
388
+ if log_rejections:
389
+ _log_rejection(
390
+ t, score, verdict["reason"],
391
+ extra={
392
+ "matched_signals": verdict.get("matched_signals", []),
393
+ "topic_fingerprint": verdict.get("topic_fingerprint", []),
394
+ },
395
+ )
396
+ continue
397
+ scored["_fit_reason"] = verdict["reason"]
398
+ scored["_human_only"] = bool(verdict.get("human_only"))
399
+ scored["_matched_signals"] = verdict.get("matched_signals", [])
400
+ scored["_topic_fingerprint"] = verdict.get("topic_fingerprint", [])
401
+
265
402
  survivors.append(scored)
266
403
 
267
- # 5. sort score DESC, stable
404
+ # 8. sort score DESC, stable
268
405
  survivors.sort(key=lambda x: x.get("_rank_score", 0.0), reverse=True)
269
406
  return survivors
270
407
 
271
408
 
272
409
  __all__ = [
273
410
  "DEFAULT_WINDOW_HOURS",
411
+ "DEFAULT_COOLDOWN_DAYS",
412
+ "MIN_ENGAGEMENT_SCORE",
413
+ "HIGH_ENGAGEMENT_OPPORTUNITY_COST",
414
+ "REJECTED_TARGETS_LOG",
274
415
  "score_target",
275
416
  "rank_x_targets",
276
417
  ]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
3
  "mcpName": "io.github.delimit-ai/delimit-mcp-server",
4
- "version": "4.5.2",
4
+ "version": "4.5.4",
5
5
  "description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -85,7 +85,23 @@
85
85
  "gemini-cli",
86
86
  "cursor",
87
87
  "ai-governance",
88
- "ai-agents"
88
+ "ai-agents",
89
+ "ai-code-review",
90
+ "ci-governance",
91
+ "merge-gate",
92
+ "attestation",
93
+ "signed-attestation",
94
+ "sigstore",
95
+ "sbom",
96
+ "supply-chain-security",
97
+ "json-schema",
98
+ "json-schema-diff",
99
+ "policy-as-code",
100
+ "audit-trail",
101
+ "ai-coding-assistant",
102
+ "pull-request",
103
+ "pr-comment",
104
+ "developer-tools"
89
105
  ],
90
106
  "author": "Delimit AI <hello@delimit.ai>",
91
107
  "license": "MIT",
@@ -95,7 +111,7 @@
95
111
  "url": "https://github.com/delimit-ai/delimit-mcp-server.git"
96
112
  },
97
113
  "dependencies": {
98
- "axios": "1.15.0",
114
+ "axios": "^1.16.0",
99
115
  "chalk": "^4.1.2",
100
116
  "commander": "^12.1.0",
101
117
  "express": "^4.18.0",
@@ -1,64 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Delimit Security Skill for Codex CLI
4
- *
5
- * Validates that Codex-generated code doesn't introduce security anti-patterns.
6
- * Runs as a security validation skill.
7
- */
8
-
9
- const fs = require('fs');
10
- const path = require('path');
11
-
12
- const SECURITY_PATTERNS = [
13
- { pattern: /eval\s*\(/g, severity: 'high', message: 'eval() usage detected — potential code injection' }, // nosec B-eval_usage: regex-pattern DEFINITION string for security scanner
14
- { pattern: /exec\s*\(/g, severity: 'medium', message: 'exec() usage — verify input sanitization' }, // nosec B-exec_usage: regex-pattern DEFINITION string for security scanner
15
- { pattern: /shell\s*=\s*True/g, severity: 'high', message: 'subprocess with shell=True — command injection risk' },
16
- { pattern: /dangerouslySetInnerHTML/g, severity: 'medium', message: 'dangerouslySetInnerHTML — XSS risk' }, // nosec B-dangerous_innerHTML: regex-pattern DEFINITION string
17
- { pattern: /password\s*=\s*["'][^"']+["']/gi, severity: 'high', message: 'Hardcoded password detected' },
18
- { pattern: /api[_-]?key\s*=\s*["'][A-Za-z0-9]{10,}["']/gi, severity: 'high', message: 'Hardcoded API key detected' },
19
- ];
20
-
21
- function checkSecurity(context) {
22
- const code = context.code || context.content || '';
23
- if (!code) return { status: 'clean', findings: [] };
24
-
25
- const findings = [];
26
- for (const { pattern, severity, message } of SECURITY_PATTERNS) {
27
- const matches = code.match(pattern);
28
- if (matches) {
29
- findings.push({ severity, message, count: matches.length });
30
- }
31
- }
32
-
33
- // Audit
34
- try {
35
- const auditDir = path.join(process.env.HOME || '', '.delimit', 'audit');
36
- fs.mkdirSync(auditDir, { recursive: true });
37
- const record = {
38
- timestamp: new Date().toISOString(),
39
- source: 'codex-security',
40
- findings_count: findings.length,
41
- high_count: findings.filter(f => f.severity === 'high').length,
42
- };
43
- const auditFile = path.join(auditDir, `${new Date().toISOString().split('T')[0]}.jsonl`);
44
- fs.appendFileSync(auditFile, JSON.stringify(record) + '\n');
45
- } catch {}
46
-
47
- const hasHigh = findings.some(f => f.severity === 'high');
48
- return {
49
- status: hasHigh ? 'flagged' : findings.length > 0 ? 'warnings' : 'clean',
50
- findings,
51
- };
52
- }
53
-
54
- const context = process.argv[2] ? JSON.parse(process.argv[2]) : {};
55
- const result = checkSecurity(context);
56
-
57
- if (result.status === 'flagged') {
58
- for (const f of result.findings) {
59
- console.error(`[Delimit Security] ${f.severity.toUpperCase()}: ${f.message}`);
60
- }
61
- process.exit(1);
62
- }
63
-
64
- process.exit(0);
@@ -1,78 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Delimit Governance Skill for Codex CLI
4
- *
5
- * Runs as a validation skill triggered on pre-code-generation and pre-suggestion.
6
- * Checks governance state and policy compliance before Codex executes actions.
7
- */
8
-
9
- const fs = require('fs');
10
- const path = require('path');
11
-
12
- const DELIMIT_HOME = path.join(process.env.HOME || '', '.delimit');
13
- const MODE_FILE = path.join(DELIMIT_HOME, 'enforcement_mode');
14
-
15
- function getMode() {
16
- try {
17
- return fs.readFileSync(MODE_FILE, 'utf-8').trim();
18
- } catch {
19
- return 'guarded'; // Default
20
- }
21
- }
22
-
23
- function checkGovernance(context) {
24
- const mode = getMode();
25
- const warnings = [];
26
-
27
- // Check if governance is initialized
28
- const policiesFile = path.join(process.cwd(), '.delimit', 'policies.yml');
29
- if (!fs.existsSync(policiesFile)) {
30
- warnings.push('No .delimit/policies.yml — run: delimit init');
31
- }
32
-
33
- // Check for sensitive file access
34
- const sensitivePatterns = ['.env', 'credentials', '.ssh', 'secrets'];
35
- const target = context.target || context.file || '';
36
- for (const pattern of sensitivePatterns) {
37
- if (target.includes(pattern)) {
38
- if (mode === 'enforce') {
39
- return { status: 'blocked', reason: `Access to sensitive path: ${target}` };
40
- }
41
- warnings.push(`Accessing sensitive path: ${target}`);
42
- }
43
- }
44
-
45
- // Audit log
46
- try {
47
- const auditDir = path.join(DELIMIT_HOME, 'audit');
48
- fs.mkdirSync(auditDir, { recursive: true });
49
- const record = {
50
- timestamp: new Date().toISOString(),
51
- source: 'codex-skill',
52
- mode,
53
- context: typeof context === 'object' ? JSON.stringify(context).slice(0, 200) : String(context).slice(0, 200),
54
- warnings,
55
- };
56
- const auditFile = path.join(auditDir, `${new Date().toISOString().split('T')[0]}.jsonl`);
57
- fs.appendFileSync(auditFile, JSON.stringify(record) + '\n');
58
- } catch {}
59
-
60
- return { status: 'allowed', mode, warnings };
61
- }
62
-
63
- // Entry point — read context from stdin or args
64
- const context = process.argv[2] ? JSON.parse(process.argv[2]) : {};
65
- const result = checkGovernance(context);
66
-
67
- if (result.status === 'blocked') {
68
- console.error(`[Delimit] BLOCKED: ${result.reason}`);
69
- process.exit(1);
70
- }
71
-
72
- if (result.warnings.length > 0) {
73
- for (const w of result.warnings) {
74
- console.error(`[Delimit] Warning: ${w}`);
75
- }
76
- }
77
-
78
- process.exit(0);