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.
Files changed (37) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +9 -8
  3. package/bin/delimit-cli.js +162 -1
  4. package/bin/delimit-setup.js +46 -6
  5. package/gateway/ai/_compile_status.py +154 -0
  6. package/gateway/ai/agent_dispatch.py +36 -0
  7. package/gateway/ai/backends/tools_infra.py +150 -10
  8. package/gateway/ai/daemon.py +10 -0
  9. package/gateway/ai/daily_digest.py +1 -2
  10. package/gateway/ai/delimit_daemon.py +67 -0
  11. package/gateway/ai/dispatch_gate.py +399 -0
  12. package/gateway/ai/hot_reload.py +1 -2
  13. package/gateway/ai/led193_daemon/executor.py +9 -0
  14. package/gateway/ai/ledger_manager.py +9 -0
  15. package/gateway/ai/license_core.cpython-310-x86_64-linux-gnu.so +0 -0
  16. package/gateway/ai/notify.py +39 -0
  17. package/gateway/ai/outreach_substantive.py +676 -0
  18. package/gateway/ai/reaper.py +70 -0
  19. package/gateway/ai/reddit_scanner.py +10 -5
  20. package/gateway/ai/sensing/schema.py +1 -1
  21. package/gateway/ai/sensing/signal_store.py +0 -1
  22. package/gateway/ai/server.py +5171 -1462
  23. package/gateway/ai/social_capability/fit_floor.py +114 -12
  24. package/gateway/ai/tdqs_lint.py +611 -0
  25. package/gateway/ai/usage_allowlist.py +198 -0
  26. package/gateway/ai/workers/base.py +2 -2
  27. package/gateway/ai/workers/executor.py +32 -3
  28. package/gateway/ai/workers/outreach_drafter.py +0 -1
  29. package/gateway/ai/workers/pr_drafter.py +0 -1
  30. package/gateway/ai/x_ranker.py +12 -2
  31. package/gateway/core/json_schema_diff.py +25 -1
  32. package/lib/auth-signin.js +136 -0
  33. package/lib/auth-signout.js +169 -0
  34. package/lib/delimit-template.js +11 -0
  35. package/lib/migration-2092-banner.js +213 -0
  36. package/package.json +2 -2
  37. package/server.json +4 -4
@@ -64,12 +64,16 @@ _CREDENTIAL_FALSE_POSITIVES = re.compile(
64
64
  r"sk-ant-demo|sk-demo|AIza-demo|xai-demo|demo[_-]?(?:key|secret|token)|"
65
65
  r"-demo['\"]|"
66
66
  # Function-call RHS (reading from parsed JSON, env, getters, slicing strings)
67
- r"json\.loads|\.read_text\(|\.slice\(|"
67
+ r"json\.loads|\.read_text\(|\.slice\(|\.split\(|"
68
68
  r"\w+\.get\(|token\s*=\s*_make_token|"
69
69
  # RHS that is a parameter reference like token=tokens.get("access_token"...
70
70
  r"=\s*\w+\.get\(|"
71
71
  # Dict index dereference: token_data["token"], result["secret"], etc.
72
72
  r"_data\[|_result\[|"
73
+ # LED-1278 (b): function-call RHS with leading underscore (e.g. _load_token())
74
+ r"=\s*_\w+\(|"
75
+ # LED-1278 (b): documentation/example placeholders in angle brackets
76
+ r"<[^>]*?(?:long|same|random|your|placeholder|example|secret|token|key)[^>]*?>|"
73
77
  # Bare `if not <var>:` and similar control-flow lines that mention
74
78
  # the credential variable name but contain no value.
75
79
  r"if\s+not\s+\w+:|"
@@ -98,6 +102,82 @@ SCAN_EXTENSIONS = {".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rb", ".java", "
98
102
  # Skip directories
99
103
  SKIP_DIRS = {"node_modules", ".git", "__pycache__", ".venv", "venv", ".tox", "dist", "build", ".next", ".nuxt", "vendor"}
100
104
 
105
+ # LED-1278 (a): test-tree path patterns excluded by default. The scanner walks # nosec
106
+ # test directories with prod rules, so test fixtures (placeholder tokens, # nosec
107
+ # trivial JWT bodies, code-injection demos) get surfaced as critical findings # nosec
108
+ # on every audit. Default behavior now skips these; callers can pass # nosec
109
+ # include_tests=True to scan everything. # nosec
110
+ TEST_PATH_PATTERNS = (
111
+ re.compile(r"(?:^|[\\/])tests?[\\/]"), # tests/ or test/ as a path component
112
+ re.compile(r"(?:^|[\\/])__tests__[\\/]"), # JS __tests__/
113
+ re.compile(r"(?:^|[\\/])spec[\\/]"), # spec/
114
+ re.compile(r"(?:^|[\\/])fixtures?[\\/]"), # fixtures/ or fixture/
115
+ re.compile(r"(?:^|[\\/])test_[^\\/]+\.py$"), # test_*.py
116
+ re.compile(r"_test\.(?:py|go|rb|java)$"), # *_test.py / *_test.go
117
+ re.compile(r"\.(?:test|spec)\.(?:js|jsx|ts|tsx|mjs|cjs)$"), # *.test.js, *.spec.tsx
118
+ )
119
+
120
+
121
+ def _is_test_path(path: str) -> bool:
122
+ """Return True if path looks like a test file/dir per TEST_PATH_PATTERNS."""
123
+ s = str(path)
124
+ return any(pat.search(s) for pat in TEST_PATH_PATTERNS)
125
+
126
+
127
+ # LED-1278 (b): well-known dummy / fixture values. Even when include_tests=True
128
+ # (or when production code intentionally embeds canonical placeholders in
129
+ # docs/examples), these specific shapes should be suppressed as `info` log
130
+ # lines, not raised as critical findings.
131
+ #
132
+ # Each entry: (regex applied to the matched secret text, human label).
133
+ KNOWN_DUMMY_PATTERNS = [
134
+ # AWS canonical dummy from official AWS documentation.
135
+ (re.compile(r"AKIAIOSFODNN7EXAMPLE"), "aws_doc_dummy"),
136
+ # GitHub token placeholders that use the printable-alphabet pattern.
137
+ (re.compile(r"^gh[pousr]_ABCDEFGHIJKLMNOPQRSTUVWXYZ", re.IGNORECASE), "github_alphabet_dummy"),
138
+ # Slack tokens with the leading 1234567890 sequence.
139
+ (re.compile(r"^xox[baprs]-1234567890-"), "slack_seq_dummy"),
140
+ # JWT with the unsigned-HS256 header + trivial body. We match the literal
141
+ # eyJhbGciOiJIUzI1NiJ9 header and check the payload separately below.
142
+ (re.compile(r"^eyJhbGciOiJIUzI1NiJ9\."), "jwt_hs256_trivial"),
143
+ # Generic dict-credential placeholder values: fake/test/dummy/example/etc.
144
+ (re.compile(r"['\"](?:fake|test|dummy|example|placeholder|stale|from-)[A-Za-z0-9_\-]*['\"]\s*$", re.IGNORECASE),
145
+ "generic_placeholder_value"),
146
+ # Provider test-key shapes: xai-key-123, google-key-7, claude-key-2 etc.
147
+ (re.compile(r"['\"](?:xai|google|claude|gem|grok|codex|ollama)[-_]?key[-_]?\d+['\"]\s*$", re.IGNORECASE),
148
+ "provider_test_key"),
149
+ ]
150
+
151
+
152
+ def _looks_like_known_dummy(secret_name: str, matched_text: str) -> Optional[str]:
153
+ """Return a label if matched_text is a known-dummy/fixture value, else None.
154
+
155
+ Used by the secret scanner to convert what would otherwise be a critical
156
+ finding into an `info`-level suppressed entry. Keeps the audit-trail
157
+ visible (so a future regression in the allowlist is detectable) while
158
+ eliminating the false-positive-storm noise.
159
+
160
+ For JWT, additionally checks that the body is the trivial `sub:1234567890`
161
+ payload — we don't want to suppress real signed JWTs that happen to use
162
+ HS256.
163
+ """
164
+ for pattern, label in KNOWN_DUMMY_PATTERNS:
165
+ if pattern.search(matched_text):
166
+ if label == "jwt_hs256_trivial":
167
+ # Only treat as dummy if the payload is the canonical demo
168
+ # body (`sub: "1234567890"` or trivial abc123 segment).
169
+ # The JWT pattern produces something like:
170
+ # eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc123def456ghi789
171
+ # The middle segment base64-decodes to {"sub":"1234567890"}.
172
+ if (
173
+ "eyJzdWIiOiIxMjM0NTY3ODkwIn0" in matched_text
174
+ or re.search(r"\.[A-Za-z0-9_-]*abc123[A-Za-z0-9_-]*$", matched_text)
175
+ ):
176
+ return label
177
+ continue
178
+ return label
179
+ return None
180
+
101
181
 
102
182
  def _run_cmd(cmd: List[str], timeout: int = 30, cwd: Optional[str] = None) -> Dict[str, Any]:
103
183
  """Run a command and return stdout, stderr, returncode.
@@ -144,8 +224,13 @@ def _bump_semver(version: str, bump: str) -> str:
144
224
  return f"{major}.{minor}.{patch}"
145
225
 
146
226
 
147
- def _scan_files(target: str) -> List[Path]:
148
- """Collect scannable source files under target."""
227
+ def _scan_files(target: str, include_tests: bool = False) -> List[Path]:
228
+ """Collect scannable source files under target.
229
+
230
+ LED-1278 (a): when include_tests=False (the new default), skip files that
231
+ match TEST_PATH_PATTERNS so test fixtures do not surface as findings.
232
+ Single-file targets are always scanned regardless (caller asked explicitly).
233
+ """
149
234
  root = Path(target).resolve()
150
235
  files = []
151
236
  if root.is_file():
@@ -154,10 +239,25 @@ def _scan_files(target: str) -> List[Path]:
154
239
  return []
155
240
  for dirpath, dirnames, filenames in os.walk(root, onerror=lambda _err: None):
156
241
  dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
242
+ if not include_tests:
243
+ # Prune obvious test directory names before recursing so we don't
244
+ # walk huge __tests__/ trees just to discard them later.
245
+ dirnames[:] = [
246
+ d for d in dirnames
247
+ if d not in ("tests", "test", "__tests__", "spec", "fixtures", "fixture")
248
+ ]
157
249
  for filename in filenames:
158
250
  p = Path(dirpath) / filename
159
- if p.suffix in SCAN_EXTENSIONS:
160
- files.append(p)
251
+ if p.suffix not in SCAN_EXTENSIONS:
252
+ continue
253
+ if not include_tests:
254
+ try:
255
+ rel = str(p.relative_to(root))
256
+ except ValueError:
257
+ rel = str(p)
258
+ if _is_test_path(rel):
259
+ continue
260
+ files.append(p)
161
261
  # Cap to avoid scanning massive repos
162
262
  if len(files) >= 5000:
163
263
  return files
@@ -166,11 +266,26 @@ def _scan_files(target: str) -> List[Path]:
166
266
 
167
267
  # ─── 5. security_audit ──────────────────────────────────────────────────
168
268
 
169
- def security_audit(target: str = ".") -> Dict[str, Any]:
269
+ def security_audit(target: str = ".", include_tests: bool = False) -> Dict[str, Any]:
170
270
  """Audit security: dependency vulnerabilities + anti-patterns + secret detection.
171
271
 
172
272
  Default: runs pip-audit/npm-audit, regex scans for secrets and dangerous patterns.
173
273
  Optional upgrade: set SNYK_TOKEN or TRIVY_PATH for enhanced scanning.
274
+
275
+ LED-1278 fixes:
276
+ (a) include_tests defaults to False — test directories (tests/, __tests__/,
277
+ spec/, fixtures/, *_test.py, *.test.tsx, etc.) are skipped so
278
+ test fixtures don't get raised as critical production findings.
279
+ Pass include_tests=True to scan everything (legacy behavior).
280
+ (b) Well-known dummy/placeholder values (AWS canonical example,
281
+ alphabet-pattern GitHub tokens, leading-1234567890 Slack tokens,
282
+ trivial JWT, fake/test/dummy/placeholder dict values, provider
283
+ test-key shapes) are suppressed and recorded as `info`-severity
284
+ allowlist hits in `suppressed_findings` for audit visibility.
285
+
286
+ Args:
287
+ target: Repository or file path to audit.
288
+ include_tests: When True, scan test directories (default False).
174
289
  """
175
290
  target_path = Path(target).resolve()
176
291
  if not target_path.exists():
@@ -179,6 +294,7 @@ def security_audit(target: str = ".") -> Dict[str, Any]:
179
294
  vulnerabilities = []
180
295
  anti_patterns_found = []
181
296
  secrets_found = []
297
+ suppressed_findings: List[Dict[str, Any]] = [] # LED-1278 (b): allowlist log
182
298
  tools_used = []
183
299
  severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
184
300
 
@@ -284,8 +400,10 @@ def security_audit(target: str = ".") -> Dict[str, Any]:
284
400
  pass
285
401
 
286
402
  # --- 2. Anti-pattern scan ---
287
- files = _scan_files(target)
288
- tools_used.append(f"pattern-scanner ({len(files)} files)")
403
+ files = _scan_files(target, include_tests=include_tests)
404
+ scan_label = f"pattern-scanner ({len(files)} files"
405
+ scan_label += ", include_tests=True" if include_tests else ", tests excluded"
406
+ tools_used.append(scan_label + ")")
289
407
 
290
408
  for fpath in files:
291
409
  try:
@@ -305,6 +423,25 @@ def security_audit(target: str = ".") -> Dict[str, Any]:
305
423
  if secret_name in _FP_FILTERED and _CREDENTIAL_FALSE_POSITIVES.search(matched_text):
306
424
  continue
307
425
  line_num = content[:match.start()].count("\n") + 1
426
+ # LED-1278 (b): well-known dummy/placeholder values get
427
+ # suppressed to info-level rather than raised as critical.
428
+ # Logged in suppressed_findings so a future regression in the
429
+ # allowlist (e.g. real key matching by accident) is auditable.
430
+ dummy_label = _looks_like_known_dummy(secret_name, matched_text)
431
+ if dummy_label:
432
+ suppressed_findings.append({
433
+ "file": rel,
434
+ "line": line_num,
435
+ "type": secret_name,
436
+ "reason": dummy_label,
437
+ "severity": "info",
438
+ })
439
+ severity_counts["info"] += 1
440
+ logger.info(
441
+ "security_audit: suppressed known-dummy %s (%s) in %s:%d",
442
+ secret_name, dummy_label, rel, line_num,
443
+ )
444
+ continue
308
445
  # Redact actual secret values in snippet output
309
446
  snippet_raw = content[max(0, match.start() - 10):match.end() + 10].strip()[:80]
310
447
  secrets_found.append({
@@ -358,6 +495,9 @@ def security_audit(target: str = ".") -> Dict[str, Any]:
358
495
  "anti_patterns": anti_patterns_found,
359
496
  "secrets_detected": len(secrets_found),
360
497
  "secrets": secrets_found[:20], # Cap output to avoid huge responses
498
+ "suppressed_findings": suppressed_findings[:20], # LED-1278 (b): allowlist audit log
499
+ "suppressed_count": len(suppressed_findings),
500
+ "include_tests": include_tests, # LED-1278 (a): expose scan scope
361
501
  "env_in_git": env_in_git,
362
502
  "severity_summary": severity_counts,
363
503
  "tools_used": tools_used,
@@ -765,9 +905,9 @@ def release_plan(environment: str = "production", version: str = "", repository:
765
905
 
766
906
  # Commits since last tag
767
907
  if last_tag:
768
- r = _run_cmd(["git", "log", f"{last_tag}..HEAD", "--oneline", "--no-decorate"], cwd=cwd)
908
+ r = _run_cmd(["git", "log", f"{last_tag}..HEAD", "--format=%s"], cwd=cwd)
769
909
  else:
770
- r = _run_cmd(["git", "log", "--oneline", "--no-decorate", "-50"], cwd=cwd)
910
+ r = _run_cmd(["git", "log", "--format=%s", "-50"], cwd=cwd)
771
911
  commits = [line.strip() for line in r["stdout"].strip().split("\n") if line.strip()] if r["stdout"].strip() else []
772
912
  result["commits_since_last_tag"] = len(commits)
773
913
  result["commits"] = commits[:30] # Cap
@@ -75,6 +75,7 @@ AUTO_PATTERNS = {
75
75
  "test": ["test", "coverage", "smoke"],
76
76
  "docs": ["docs", "documentation", "readme"],
77
77
  "governance": ["governance", "policy", "compliance"],
78
+ "build": ["feat", "fix", "task", "implementation"],
78
79
  }
79
80
 
80
81
 
@@ -263,6 +264,14 @@ def get_next_automatable_item(
263
264
  return None
264
265
 
265
266
 
267
+
268
+ def _run_build(item_id: str, venture: str = "") -> dict:
269
+ """Run the governed build loop for a specific item (LED-1146)."""
270
+ from ai.loop_engine import run_governed_iteration
271
+ # Use a persistent session for the daemon
272
+ session_id = "daemon-build-loop"
273
+ return run_governed_iteration(session_id=session_id)
274
+
266
275
  def process_item(item: dict, log_path: Optional[Path] = None) -> dict:
267
276
  """Process a single ledger item by running the suggested tool.
268
277
 
@@ -293,6 +302,7 @@ def process_item(item: dict, log_path: Optional[Path] = None) -> dict:
293
302
  "test": _run_test,
294
303
  "governance": _run_governance,
295
304
  "docs": _run_docs,
305
+ "build": _run_build,
296
306
  }
297
307
 
298
308
  runner = tool_map.get(tool)
@@ -20,11 +20,10 @@ Call via MCP: delimit_digest(action="run") or scheduled cron.
20
20
  from __future__ import annotations
21
21
 
22
22
  import json
23
- import time
24
23
  from collections import Counter
25
24
  from datetime import datetime, timedelta, timezone
26
25
  from pathlib import Path
27
- from typing import Any, Dict, List, Optional
26
+ from typing import Any, Dict
28
27
 
29
28
  DIGEST_DIR = Path.home() / ".delimit" / "digest"
30
29
  LEDGER_DIR = Path.home() / ".delimit" / "ledger"
@@ -0,0 +1,67 @@
1
+ """
2
+ Unified Delimit Daemon (LED-193).
3
+
4
+ Consolidates three long-running daemons into a single process:
5
+ - inbox_daemon (5m cadence)
6
+ - social_daemon (15m cadence)
7
+ - self_repair_daemon (1h cadence)
8
+
9
+ Retains the individual modules' internal state files and thread-level
10
+ encapsulation to minimize blast radius and ensure existing MCP interfaces
11
+ (status checks) continue to work without modification.
12
+ """
13
+
14
+ import time
15
+ import logging
16
+ import signal
17
+ import sys
18
+
19
+ from ai.inbox_daemon import start_daemon as start_inbox, stop_daemon as stop_inbox
20
+ from ai.social_daemon import start_daemon as start_social, stop_daemon as stop_social
21
+ from ai.self_repair_daemon import start_daemon as start_self_repair, stop_daemon as stop_self_repair
22
+
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
26
+ )
27
+ logger = logging.getLogger("delimit.daemon_runner")
28
+
29
+ def _handle_sigterm(signum, frame):
30
+ logger.info("Received SIGTERM, shutting down all daemons...")
31
+ try:
32
+ stop_inbox()
33
+ except Exception as e:
34
+ logger.error(f"Error stopping inbox: {e}")
35
+ try:
36
+ stop_social()
37
+ except Exception as e:
38
+ logger.error(f"Error stopping social: {e}")
39
+ try:
40
+ stop_self_repair()
41
+ except Exception as e:
42
+ logger.error(f"Error stopping self_repair: {e}")
43
+ sys.exit(0)
44
+
45
+ def main():
46
+ signal.signal(signal.SIGTERM, _handle_sigterm)
47
+ signal.signal(signal.SIGINT, _handle_sigterm)
48
+
49
+ logger.info("Starting unified delimit_daemon (LED-193)...")
50
+
51
+ inbox_res = start_inbox()
52
+ logger.info(f"Inbox daemon: {inbox_res.get('status')}")
53
+
54
+ social_res = start_social()
55
+ logger.info(f"Social daemon: {social_res.get('status')}")
56
+
57
+ repair_res = start_self_repair()
58
+ logger.info(f"Self-repair daemon: {repair_res.get('status')}")
59
+
60
+ try:
61
+ while True:
62
+ time.sleep(60)
63
+ except KeyboardInterrupt:
64
+ _handle_sigterm(None, None)
65
+
66
+ if __name__ == "__main__":
67
+ main()