delimit-cli 4.5.1 → 4.5.2

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +2 -2
  3. package/bin/delimit-cli.js +109 -24
  4. package/gateway/ai/content_engine.py +3 -4
  5. package/gateway/ai/inbox_classifier.py +215 -0
  6. package/gateway/ai/integrations/opensage_wrapper.py +4 -1
  7. package/gateway/ai/ledger_manager.py +218 -38
  8. package/gateway/ai/license.py +26 -0
  9. package/gateway/ai/notify.py +68 -3
  10. package/gateway/ai/reddit_proxy.py +93 -15
  11. package/gateway/ai/reddit_scanner.py +36 -18
  12. package/gateway/ai/server.py +128 -6
  13. package/gateway/ai/social_capability/__init__.py +6 -0
  14. package/gateway/ai/social_capability/capability_validator.py +273 -0
  15. package/gateway/ai/social_capability/current_capabilities.yaml +95 -0
  16. package/gateway/ai/social_queue.py +307 -0
  17. package/gateway/ai/supabase_sync.py +14 -2
  18. package/gateway/ai/swarm.py +29 -11
  19. package/gateway/ai/tui.py +6 -2
  20. package/gateway/ai/x_ranker.py +276 -0
  21. package/lib/attest-mcp.js +487 -0
  22. package/lib/attest-telemetry.js +48 -0
  23. package/lib/delimit-home.js +35 -0
  24. package/lib/delimit-template.js +14 -0
  25. package/package.json +8 -2
  26. package/scripts/postinstall.js +89 -40
  27. package/gateway/ai/content_grounding/__init__.py +0 -98
  28. package/gateway/ai/content_grounding/build.py +0 -350
  29. package/gateway/ai/content_grounding/consume.py +0 -280
  30. package/gateway/ai/content_grounding/features.py +0 -218
  31. package/gateway/ai/content_grounding/fixtures/fail/01_missing_evidence.json +0 -9
  32. package/gateway/ai/content_grounding/fixtures/fail/02_unknown_evidence_prefix.json +0 -9
  33. package/gateway/ai/content_grounding/fixtures/fail/03_banned_comparative.json +0 -17
  34. package/gateway/ai/content_grounding/fixtures/fail/04_banned_adoption.json +0 -17
  35. package/gateway/ai/content_grounding/fixtures/fail/05_aggregate_no_numeric.json +0 -17
  36. package/gateway/ai/content_grounding/fixtures/fail/06_unversioned_inference_rule.json +0 -18
  37. package/gateway/ai/content_grounding/fixtures/pass/01_feature_shipped.json +0 -18
  38. package/gateway/ai/content_grounding/fixtures/pass/02_aggregate_claim.json +0 -23
  39. package/gateway/ai/content_grounding/fixtures/pass/03_attestation.json +0 -16
  40. package/gateway/ai/content_grounding/schemas/claim.schema.json +0 -40
  41. package/gateway/ai/content_grounding/schemas/event.schema.json +0 -23
  42. package/gateway/ai/content_grounding/schemas.py +0 -276
  43. package/gateway/ai/content_grounding/telemetry.py +0 -221
  44. package/gateway/ai/inbox_drafts/__init__.py +0 -61
  45. package/gateway/ai/inbox_drafts/registry.py +0 -412
  46. package/gateway/ai/inbox_drafts/schema.py +0 -374
  47. package/gateway/ai/inbox_executor.py +0 -565
@@ -7302,7 +7302,8 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
7302
7302
  Categories: tip, changelog, insight, engagement.
7303
7303
  Leave text empty to auto-generate from templates.
7304
7304
  Every post provides value - tips, insights, governance wisdom.
7305
- Max 2 posts per day to stay authentic.
7305
+ Rate cap: 2 original posts per hour, 24 per day (founder-approved
7306
+ 2026-04-30). Override via DELIMIT_HOURLY_TWEETS / DELIMIT_DAILY_TWEETS.
7306
7307
 
7307
7308
  IMPORTANT - Platform tone rules (these are DIFFERENT per platform):
7308
7309
  - Twitter: confident technical brand. Direct, professional, ALWAYS POSITIVE.
@@ -7323,10 +7324,10 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
7323
7324
  draft: If True, save as draft for approval instead of posting immediately.
7324
7325
  context: WHY this post should be made. Strategic reasoning shown in the approval email.
7325
7326
  """
7326
- from ai.social import generate_post, post_tweet, should_post_today, save_draft
7327
+ from ai.social import generate_post, post_tweet, should_post_now, save_draft
7327
7328
 
7328
- if not draft and not should_post_today():
7329
- return {"status": "skipped", "reason": "Already posted twice today. Authenticity > volume."}
7329
+ if not draft and not should_post_now():
7330
+ return {"status": "skipped", "reason": "Rate cap hit (2/hr or 24/day). Wait or pass draft=True for email-approval flow."}
7330
7331
 
7331
7332
  post = generate_post(category, text)
7332
7333
 
@@ -7443,9 +7444,16 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
7443
7444
  _lines.append("Reply APPROVED to approve, CANCEL to reject.")
7444
7445
 
7445
7446
  _handle = f"u/{_acct}"
7447
+ # LED-1129 Phase 2 — append [draft_id:<8>] token to subject so
7448
+ # the inbox daemon's draft_id fallback can match the approval
7449
+ # reply even when no LED/STR token is present.
7450
+ _reddit_subject = f"[Reddit Post] {_handle}: {_reddit_title[:60]}..."
7451
+ _reg_id = entry.get("registry_draft_id")
7452
+ if _reg_id:
7453
+ _reddit_subject = f"{_reddit_subject} [draft_id:{_reg_id[:8]}]"
7446
7454
  email_result = send_email(
7447
7455
  message="\n".join(_lines),
7448
- subject=f"[Reddit Post] {_handle}: {_reddit_title[:60]}...",
7456
+ subject=_reddit_subject,
7449
7457
  event_type="social_draft",
7450
7458
  )
7451
7459
  if email_result.get("delivered") and email_result.get("message_id"):
@@ -7487,9 +7495,16 @@ def delimit_social_post(text: str = "", category: str = "", platform: str = "twi
7487
7495
  _subject_type = "Tweet"
7488
7496
 
7489
7497
  _handle = f"u/{_acct}" if platform == "reddit" else f"@{_acct}"
7498
+ # LED-1129 Phase 2 — append [draft_id:<8>] token to subject so the
7499
+ # inbox daemon's draft_id fallback can match the approval reply even
7500
+ # when no LED/STR token is present.
7501
+ _social_subject = f"[{_subject_type}] {_handle}: {post['text'][:60]}..."
7502
+ _reg_id = entry.get("registry_draft_id")
7503
+ if _reg_id:
7504
+ _social_subject = f"{_social_subject} [draft_id:{_reg_id[:8]}]"
7490
7505
  email_result = send_email(
7491
7506
  message="\n".join(_lines),
7492
- subject=f"[{_subject_type}] {_handle}: {post['text'][:60]}...",
7507
+ subject=_social_subject,
7493
7508
  event_type="social_draft",
7494
7509
  )
7495
7510
  # Store the outbound Message-ID on the draft record so the
@@ -7529,6 +7544,55 @@ def delimit_social_accounts() -> Dict[str, Any]:
7529
7544
  return _with_next_steps("social_accounts", {"accounts": accounts, "count": len(accounts)})
7530
7545
 
7531
7546
 
7547
+ @mcp.tool()
7548
+ def delimit_x_fetch(id_or_url: str = "", ids: str = "") -> Dict[str, Any]:
7549
+ """LED-825: fetch tweets from X by id or URL via twttr241 (RapidAPI).
7550
+
7551
+ Inherits the LRU + SQLite cache + budget gate already wired for the
7552
+ social-target scanner, so repeated reads of the same tweet are free.
7553
+
7554
+ Args:
7555
+ id_or_url: A single status id ("2048825010371039648") OR a full
7556
+ x.com / twitter.com URL — the id is extracted automatically.
7557
+ Mutually exclusive with `ids`.
7558
+ ids: Comma-separated list of status ids OR URLs for a batch fetch.
7559
+ Each is normalized to a status id and fetched independently.
7560
+
7561
+ Returns:
7562
+ Single-fetch shape: {id, text, author, author_name, created_at,
7563
+ metrics: {favorite_count, retweet_count, reply_count,
7564
+ quote_count, bookmark_count, view_count}, url, from_cache}
7565
+ Batch shape: {tweets: [<single-shape>, ...], count}
7566
+ Errors: {error: <reason>}
7567
+
7568
+ Why this exists: WebFetch hits 402 on x.com (auth-walled), and going
7569
+ around to tweepy + the X API direct creds skips the cache + budget
7570
+ gate. This tool is the cheap, cached, governable read path.
7571
+ """
7572
+ from ai.social_target import fetch_tweet_by_id, fetch_tweets_by_ids, extract_status_id
7573
+
7574
+ if ids:
7575
+ # Batch mode — accept commas, newlines, or whitespace as separators
7576
+ raw = [r.strip() for r in ids.replace("\n", ",").split(",") if r.strip()]
7577
+ normalized: List[str] = []
7578
+ for item in raw:
7579
+ sid = extract_status_id(item)
7580
+ if sid:
7581
+ normalized.append(sid)
7582
+ if not normalized:
7583
+ return _with_next_steps("x_fetch", {"error": "no valid ids/URLs in `ids`"})
7584
+ results = fetch_tweets_by_ids(normalized)
7585
+ return _with_next_steps("x_fetch", {"tweets": results, "count": len(results)})
7586
+
7587
+ if not id_or_url:
7588
+ return _with_next_steps("x_fetch", {"error": "provide either id_or_url or ids"})
7589
+
7590
+ sid = extract_status_id(id_or_url)
7591
+ if not sid:
7592
+ return _with_next_steps("x_fetch", {"error": f"could not parse status id from {id_or_url!r}"})
7593
+ return _with_next_steps("x_fetch", fetch_tweet_by_id(sid))
7594
+
7595
+
7532
7596
  @mcp.tool()
7533
7597
  def delimit_social_history(limit: int = 20, platform: str = "",
7534
7598
  user: str = "", subreddit: str = "") -> Dict[str, Any]:
@@ -7985,6 +8049,64 @@ def delimit_social_daemon(action: str = "status") -> Dict[str, Any]:
7985
8049
  else:
7986
8050
  return _with_next_steps("social_daemon", get_daemon_status())
7987
8051
 
8052
+
8053
+ @mcp.tool()
8054
+ def delimit_self_repair_daemon(action: str = "status") -> Dict[str, Any]:
8055
+ """Control the self-repair watcher daemon (LED-191, internal).
8056
+
8057
+ Polls function KPIs on a cadence (default 1h) and emits founder
8058
+ alerts on fresh breaches. Higher modes (diagnose/deliberate/apply/
8059
+ verify) chain through the watcher when configured per-function in
8060
+ ~/.delimit/self_repair.yaml.
8061
+
8062
+ Idempotent start; circuit-breakered stop after 3 consecutive
8063
+ pass failures. Honors DELIMIT_SELF_REPAIR_PAUSE=1 at every pass
8064
+ without requiring a daemon restart.
8065
+
8066
+ Args:
8067
+ action: 'start' (begin polling), 'stop' (halt polling),
8068
+ 'status' (running / last_pass / breaches_emitted /
8069
+ consecutive_failures).
8070
+ """
8071
+ from ai.self_repair_daemon import (
8072
+ start_daemon as _sr_start,
8073
+ stop_daemon as _sr_stop,
8074
+ get_daemon_status as _sr_status,
8075
+ )
8076
+
8077
+ if action == "start":
8078
+ return _with_next_steps("self_repair_daemon", _sr_start())
8079
+ elif action == "stop":
8080
+ return _with_next_steps("self_repair_daemon", _sr_stop())
8081
+ else:
8082
+ return _with_next_steps("self_repair_daemon", _sr_status())
8083
+
8084
+
8085
+ # ═══════════════════════════════════════════════════════════════════════
8086
+ # LED-189: Corp dashboard — single-call session-start synthesis
8087
+ # ═══════════════════════════════════════════════════════════════════════
8088
+
8089
+
8090
+ @mcp.tool()
8091
+ def delimit_corp_dashboard() -> Dict[str, Any]:
8092
+ """One-call corp status — replaces the 6-call session-start ritual (LED-189).
8093
+
8094
+ Returns daemon states (systemd + in-process), self-repair status,
8095
+ social/inbox activity, ledger pending counts, agent queue (audit-only),
8096
+ latest session, and a synthesized one-line summary like:
8097
+
8098
+ "Corp status: 3 daemons active (self-repair, inbox, social),
8099
+ 12 ledger open, 2 approvals waiting, 4 breaches in 24h."
8100
+
8101
+ Every sub-section is failure-isolated: a partial failure returns
8102
+ {"error": "..."} for that key only and never crashes the whole call.
8103
+ Gateway-only — not shipped in the npm bundle.
8104
+ """
8105
+ from ai.corp_dashboard import get_corp_dashboard
8106
+ result = get_corp_dashboard()
8107
+ return _with_next_steps("corp_dashboard", result)
8108
+
8109
+
7988
8110
  # ═══════════════════════════════════════════════════════════════════════
7989
8111
  # LED-187: Shareable Governance Config - export / import
7990
8112
  # ═══════════════════════════════════════════════════════════════════════
@@ -0,0 +1,6 @@
1
+ """Social outreach capability-currency module (LED-216 Phase 1).
2
+
3
+ Curated capability inventory + draft-emit validator. Lives outside
4
+ ``ai/social.py`` so the existing module stays focused on draft generation
5
+ and Python doesn't get a package/module name collision.
6
+ """
@@ -0,0 +1,273 @@
1
+ """Capability-currency validator (LED-216 Phase 1).
2
+
3
+ Validates social drafts against ``current_capabilities.yaml``. The validator
4
+ hard-fails any draft that names a banned surface (literal or regex), and
5
+ warns when a draft references the Delimit product but no canonical phrase
6
+ appears.
7
+
8
+ Wiring: ``ai.social.save_draft`` calls :func:`validate_draft` after the
9
+ existing tone/length checks but BEFORE the file is appended. On hard-fail
10
+ the draft's ``quality`` is overridden to ``"rejected_capability_drift"`` and
11
+ the entry MUST NOT be enqueued for notify. On warn the quality becomes
12
+ ``"ready_with_warnings"``. Both outcomes are logged to
13
+ ``~/.delimit/social_drafts_validation.jsonl`` for audit / replay.
14
+
15
+ Governance: the underlying ``current_capabilities.yaml`` is gated by the
16
+ LED-1037 banned-vocabulary contract — edits require a unanimous
17
+ ``delimit_deliberate`` verdict. This module only consumes the file; it does
18
+ not mutate it.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import logging
25
+ import re
26
+ from datetime import datetime, timezone
27
+ from pathlib import Path
28
+ from typing import Any, Dict, List, Optional
29
+
30
+ try:
31
+ import yaml as _yaml
32
+ except ImportError: # pragma: no cover
33
+ _yaml = None # type: ignore[assignment]
34
+
35
+ logger = logging.getLogger("delimit.ai.social_capability.capability_validator")
36
+
37
+ # Default capabilities path — co-located with this module so the file ships
38
+ # (or doesn't) with the same npm exclusion rule as the rest of
39
+ # ai/social_capability/. The package name is `social_capability` rather
40
+ # than `social` because `ai/social.py` already exists as a top-level
41
+ # module and Python forbids a package with the same name as a sibling
42
+ # module.
43
+ DEFAULT_CAPABILITIES_PATH = Path(__file__).parent / "current_capabilities.yaml"
44
+
45
+ # Audit log for every validator decision (pass / warn / fail). One JSONL
46
+ # line per draft. Used by self-repair to detect over- or under-firing.
47
+ VALIDATION_LOG = Path.home() / ".delimit" / "social_drafts_validation.jsonl"
48
+
49
+ # ── product-mention detection ────────────────────────────────────────
50
+
51
+ # Case-insensitive match for "Delimit" as a standalone word OR the
52
+ # @delimit_ai twitter handle. Avoid false positives on hostnames like
53
+ # "delimit.ai/methodology/..." by NOT requiring word boundaries — the URL
54
+ # itself is a product reference, which is exactly the case where we want
55
+ # to nudge the canonical phrase.
56
+ _PRODUCT_MENTION_RE = re.compile(
57
+ r"(?:\bDelimit\b|@delimit_ai|delimit\.ai)",
58
+ re.IGNORECASE,
59
+ )
60
+
61
+
62
+ def _load_capabilities(path: Path) -> Dict[str, Any]:
63
+ """Load and parse the capabilities YAML.
64
+
65
+ Returns an empty config (no banned, no claims) if the file is missing
66
+ or YAML is unavailable, so the validator fails open in degraded
67
+ environments rather than crashing draft generation. The fail-open is
68
+ intentional: a missing capability file should NOT block legitimate
69
+ drafts; the worst outcome is the draft passes through without
70
+ capability-currency enforcement, which we'll catch via the audit log.
71
+ """
72
+ if _yaml is None:
73
+ logger.warning(
74
+ "capability_validator: PyYAML not available; validator is a no-op"
75
+ )
76
+ return {}
77
+ if not path.exists():
78
+ logger.warning(
79
+ "capability_validator: capabilities file missing at %s; "
80
+ "validator is a no-op",
81
+ path,
82
+ )
83
+ return {}
84
+ try:
85
+ with open(path, "r", encoding="utf-8") as f:
86
+ data = _yaml.safe_load(f) or {}
87
+ except Exception as exc: # pragma: no cover — corrupt yaml
88
+ logger.error(
89
+ "capability_validator: failed to load %s: %s", path, exc
90
+ )
91
+ return {}
92
+ if not isinstance(data, dict):
93
+ logger.error(
94
+ "capability_validator: %s did not parse to a mapping", path
95
+ )
96
+ return {}
97
+ return data
98
+
99
+
100
+ def _matched_claims(text: str, claims: List[Dict[str, Any]]) -> List[str]:
101
+ """Return IDs of allowed_claims whose surface_name appears in text."""
102
+ out: List[str] = []
103
+ lower = text.lower()
104
+ for claim in claims:
105
+ if not isinstance(claim, dict):
106
+ continue
107
+ surface = (claim.get("surface_name") or "").strip()
108
+ cid = (claim.get("id") or "").strip()
109
+ if not surface or not cid:
110
+ continue
111
+ if surface.lower() in lower:
112
+ out.append(cid)
113
+ return out
114
+
115
+
116
+ def _matched_banned_literal(
117
+ text: str, banned: List[str]
118
+ ) -> List[str]:
119
+ """Return banned surface literals (case-insensitive) found in text."""
120
+ lower = text.lower()
121
+ return [b for b in banned if isinstance(b, str) and b and b.lower() in lower]
122
+
123
+
124
+ def _matched_banned_pattern(
125
+ text: str, patterns: List[str]
126
+ ) -> List[str]:
127
+ """Return banned regex patterns that match somewhere in text.
128
+
129
+ Compilation errors on individual patterns are logged and the pattern is
130
+ skipped so one bad regex does not break the whole validator.
131
+ """
132
+ out: List[str] = []
133
+ for pat in patterns:
134
+ if not isinstance(pat, str) or not pat:
135
+ continue
136
+ try:
137
+ if re.search(pat, text, flags=re.IGNORECASE):
138
+ out.append(pat)
139
+ except re.error as exc:
140
+ logger.warning(
141
+ "capability_validator: bad banned_surface_pattern %r: %s",
142
+ pat, exc,
143
+ )
144
+ continue
145
+ return out
146
+
147
+
148
+ def _has_canonical_phrase(text: str, phrases: List[str]) -> bool:
149
+ lower = text.lower()
150
+ for p in phrases:
151
+ if isinstance(p, str) and p and p.lower() in lower:
152
+ return True
153
+ return False
154
+
155
+
156
+ def _mentions_product(text: str) -> bool:
157
+ return bool(_PRODUCT_MENTION_RE.search(text or ""))
158
+
159
+
160
+ def _append_audit(record: Dict[str, Any]) -> None:
161
+ """Append a validation decision to the audit log. Best-effort."""
162
+ try:
163
+ VALIDATION_LOG.parent.mkdir(parents=True, exist_ok=True)
164
+ with open(VALIDATION_LOG, "a", encoding="utf-8") as f:
165
+ f.write(json.dumps(record) + "\n")
166
+ except Exception as exc: # pragma: no cover — disk full, etc.
167
+ logger.debug("capability_validator: audit write failed: %s", exc)
168
+
169
+
170
+ def validate_draft(
171
+ text: str,
172
+ capabilities_path: Optional[Path] = None,
173
+ *,
174
+ audit_meta: Optional[Dict[str, Any]] = None,
175
+ log: bool = True,
176
+ ) -> Dict[str, Any]:
177
+ """Validate a social draft against ``current_capabilities.yaml``.
178
+
179
+ Args:
180
+ text: The candidate draft text.
181
+ capabilities_path: Override path to the capabilities YAML. Defaults
182
+ to the bundled ``current_capabilities.yaml`` next to this
183
+ module.
184
+ audit_meta: Optional fields to embed in the audit log entry
185
+ (e.g. ``{"draft_id": ..., "platform": ...}``). Never required
186
+ for validation logic.
187
+ log: When False, skip the audit log write. Used by tests.
188
+
189
+ Returns:
190
+ Dict with:
191
+ - ``ok`` (bool): False iff a banned surface (literal or pattern)
192
+ appeared.
193
+ - ``errors`` (list[str]): Hard-fail reasons.
194
+ - ``warnings`` (list[str]): Soft-fail reasons (e.g. product
195
+ mentioned without canonical phrase).
196
+ - ``matched_claims`` (list[str]): IDs of allowed_claims found.
197
+ - ``matched_banned`` (list[str]): Banned surfaces / patterns hit.
198
+ - ``mentions_product`` (bool): Whether the draft references
199
+ Delimit by name or handle.
200
+ - ``has_canonical_phrase`` (bool): Whether at least one canonical
201
+ phrase appears.
202
+ """
203
+ text = text or ""
204
+ path = capabilities_path or DEFAULT_CAPABILITIES_PATH
205
+ cfg = _load_capabilities(path)
206
+
207
+ allowed_claims = cfg.get("allowed_claims") or []
208
+ banned_literals = cfg.get("banned_surfaces") or []
209
+ banned_patterns = cfg.get("banned_surface_patterns") or []
210
+ required_phrases = cfg.get("required_canonical_phrases") or []
211
+
212
+ matched_claims = _matched_claims(text, allowed_claims)
213
+ matched_literal = _matched_banned_literal(text, banned_literals)
214
+ matched_patterns = _matched_banned_pattern(text, banned_patterns)
215
+ matched_banned = matched_literal + matched_patterns
216
+
217
+ errors: List[str] = []
218
+ for hit in matched_literal:
219
+ errors.append(
220
+ f"banned surface literal: {hit!r} — see ai/social_capability/current_capabilities.yaml"
221
+ )
222
+ for pat in matched_patterns:
223
+ errors.append(
224
+ f"banned surface pattern matched: {pat!r} (tool-count hero "
225
+ f"language is forbidden in social copy)"
226
+ )
227
+
228
+ mentions_product = _mentions_product(text)
229
+ has_canonical = _has_canonical_phrase(text, required_phrases)
230
+
231
+ warnings: List[str] = []
232
+ if mentions_product and not has_canonical and required_phrases:
233
+ warnings.append(
234
+ "draft mentions Delimit but does not include a canonical phrase "
235
+ "(merge gate / signed, replayable attestation / AI-written code "
236
+ "/ AI-assisted merge). Founder review recommended."
237
+ )
238
+
239
+ ok = not errors
240
+
241
+ result: Dict[str, Any] = {
242
+ "ok": ok,
243
+ "errors": errors,
244
+ "warnings": warnings,
245
+ "matched_claims": matched_claims,
246
+ "matched_banned": matched_banned,
247
+ "mentions_product": mentions_product,
248
+ "has_canonical_phrase": has_canonical,
249
+ }
250
+
251
+ if log:
252
+ record = {
253
+ "ts": datetime.now(timezone.utc).isoformat(),
254
+ "ok": ok,
255
+ "errors": errors,
256
+ "warnings": warnings,
257
+ "matched_claims": matched_claims,
258
+ "matched_banned": matched_banned,
259
+ "mentions_product": mentions_product,
260
+ "has_canonical_phrase": has_canonical,
261
+ "text_len": len(text),
262
+ "capabilities_path": str(path),
263
+ }
264
+ if audit_meta:
265
+ # Don't let audit_meta clobber computed fields.
266
+ for k, v in audit_meta.items():
267
+ record.setdefault(k, v)
268
+ _append_audit(record)
269
+
270
+ return result
271
+
272
+
273
+ __all__ = ["validate_draft", "DEFAULT_CAPABILITIES_PATH", "VALIDATION_LOG"]
@@ -0,0 +1,95 @@
1
+ # LED-216 Phase 1 — Capability Currency
2
+ #
3
+ # This file is the single source of truth for what Delimit is allowed to
4
+ # claim in any social draft. The capability_validator.py module reads it
5
+ # at draft emit time and hard-fails any draft that:
6
+ #
7
+ # * names a banned surface (literal substring), OR
8
+ # * matches a banned surface regex (e.g. tool-count hero language).
9
+ #
10
+ # Drafts that mention "Delimit" / "@delimit_ai" but reference NO canonical
11
+ # phrase are warned (not failed) so the founder can still review.
12
+ #
13
+ # ──────────────────────────────────────────────────────────────────────
14
+ # GOVERNANCE (mirror of LED-1037 banned-vocabulary gate)
15
+ # ──────────────────────────────────────────────────────────────────────
16
+ # Edits to this file MUST be gated by `delimit_deliberate` unanimous
17
+ # verdict. Same governance contract as the LED-1037 banned-vocabulary
18
+ # list: any change requires a multi-model panel sign-off (Claude +
19
+ # Gemini + Codex + Grok) before it lands. Silent edits are a policy
20
+ # violation and will be reverted.
21
+ #
22
+ # 30-day review cadence: capabilities rot fast. The `last_reviewed`
23
+ # field below MUST be bumped on every panel-approved edit, and the
24
+ # orchestrator should re-deliberate if `now - last_reviewed >
25
+ # review_cadence_days`.
26
+ # ──────────────────────────────────────────────────────────────────────
27
+
28
+ version: 1
29
+ last_reviewed: 2026-05-02
30
+ review_cadence_days: 30
31
+
32
+ allowed_claims:
33
+ # Each entry: {id, surface_name, description, since_version, evidence_link}
34
+ - id: attest_mcp
35
+ surface_name: "delimit attest mcp"
36
+ description: "Local CLI subcommand that runs the 5-check methodology preview."
37
+ since_version: 4.5.0
38
+ evidence_link: https://delimit.ai/methodology/mcp-attestation
39
+ - id: self_repair_v1
40
+ surface_name: "self-repair v1 closed loop"
41
+ description: "Sensor → diagnose → deliberate → apply → verify with founder approval gate."
42
+ since_version: gateway-2026-05-01
43
+ evidence_link: https://delimit.ai/methodology/mcp-attestation
44
+ - id: merge_gate
45
+ surface_name: "merge gate for AI-written code"
46
+ description: "Signed, replayable attestation for every AI-assisted merge."
47
+ since_version: "1.0"
48
+ evidence_link: https://delimit.ai
49
+ - id: multi_model_deliberation
50
+ surface_name: "multi-model deliberation panel"
51
+ description: "delimit_deliberate runs Claude + Gemini + Codex + Grok to consensus."
52
+ since_version: "1.0"
53
+ - id: persistent_context
54
+ surface_name: "persistent context across sessions"
55
+ description: "Memory + ledger survive across sessions and models via delimit_revive / delimit_session_handoff."
56
+ - id: governance_kernel
57
+ surface_name: "fail-closed governance kernel"
58
+ description: "Audit trail + evidence_collect at every gate."
59
+ - id: diff_engine
60
+ surface_name: "27 breaking-change types"
61
+ description: "Deterministic diff engine for OpenAPI spec changes."
62
+ - id: github_action
63
+ surface_name: "delimit-ai/delimit-action GitHub Action"
64
+ description: "On Marketplace, breaking-change detection on PRs."
65
+ - id: corp_dashboard
66
+ surface_name: "delimit_corp_dashboard"
67
+ description: "Single-call corp status synthesis."
68
+ since_version: gateway-2026-05-01
69
+ - id: methodology_v1
70
+ surface_name: "MCP attestation methodology v1"
71
+ description: "Public methodology document at delimit.ai/methodology/mcp-attestation."
72
+
73
+ banned_surfaces:
74
+ # Strings that MUST NOT appear in any social draft. Validator hard-fails.
75
+ - "AI OS"
76
+ - "governance platform"
77
+ - "one workspace for every AI coding assistant"
78
+ - "5 capability domains"
79
+ - "Jamsons OS"
80
+ - "Delimit OS"
81
+ - "186 tools"
82
+
83
+ banned_surface_patterns:
84
+ # Tool-count hero language patterns — match by regex in validator.
85
+ - "\\d+ tools"
86
+ - "\\d+ capability domains"
87
+
88
+ required_canonical_phrases:
89
+ # At least one of these MUST appear in any draft that mentions Delimit's
90
+ # product. If draft mentions Delimit by name and references no canonical
91
+ # phrase, validator warns (does not hard-fail).
92
+ - "merge gate"
93
+ - "signed, replayable attestation"
94
+ - "AI-written code"
95
+ - "AI-assisted merge"