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
|
@@ -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
|
-
|
|
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
|
-
|
|
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", "--
|
|
908
|
+
r = _run_cmd(["git", "log", f"{last_tag}..HEAD", "--format=%s"], cwd=cwd)
|
|
769
909
|
else:
|
|
770
|
-
r = _run_cmd(["git", "log", "--
|
|
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
|
package/gateway/ai/daemon.py
CHANGED
|
@@ -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
|
|
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()
|