ltcai 9.8.0 → 9.9.1
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/README.md +45 -28
- package/bin/ltcai.js +1 -1
- package/desktop/electron/main.cjs +1 -1
- package/docs/BENCHMARKS.md +107 -0
- package/docs/CHANGELOG.md +82 -0
- package/docs/CI_AND_RELEASE_GATES.md +106 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +11 -8
- package/docs/LEGACY_COMPATIBILITY.md +48 -49
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +15 -6
- package/docs/PERFORMANCE.md +9 -1
- package/docs/SECURITY_AUDIT.md +91 -0
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/USABILITY_AUDIT.md +164 -0
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/architecture.md +4 -2
- package/docs/kg-schema.md +6 -6
- package/docs/spec-vs-impl.md +8 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/schema.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/review_queue.py +7 -0
- package/latticeai/core/agent.py +105 -8
- package/latticeai/core/agent_eval.py +99 -1
- package/latticeai/core/legacy_compatibility.py +71 -47
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/tool_governor.py +92 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/change_proposals.py +165 -23
- package/latticeai/services/product_readiness.py +2 -2
- package/latticeai/services/tool_dispatch.py +33 -0
- package/latticeai/tools/__init__.py +1 -1
- package/latticeai/tools/knowledge.py +1 -1
- package/package.json +9 -17
- package/scripts/bench_models.py +312 -0
- package/scripts/check_bundle_budget.mjs +103 -0
- package/scripts/check_current_release_docs.mjs +20 -1
- package/scripts/check_doc_status.mjs +149 -0
- package/scripts/check_legacy_debt.mjs +91 -0
- package/scripts/generate_sbom.py +70 -0
- package/scripts/prune_release_evidence.mjs +38 -0
- package/scripts/wheel_smoke.py +22 -6
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/main.rs +12 -12
- package/src-tauri/tauri.conf.json +1 -13
- package/static/app/asset-manifest.json +27 -11
- package/static/app/assets/Act-CcSWTQsx.js +1 -0
- package/static/app/assets/{Brain-BMkgdWnI.js → Brain-CSCDZEHd.js} +2 -2
- package/static/app/assets/BrainHome-B5QpJ1Fx.js +2 -0
- package/static/app/assets/BrainSignals-CmFD8PWk.js +1 -0
- package/static/app/assets/Capture-BiuerOio.js +1 -0
- package/static/app/assets/CommandPalette-BrqpT7K2.js +1 -0
- package/static/app/assets/Library-BcaiPbum.js +1 -0
- package/static/app/assets/LivingBrain-6FvA0H5T.js +1 -0
- package/static/app/assets/ProductFlow-Cr8bkQPw.js +1 -0
- package/static/app/assets/ReviewCard-D56TMypk.js +1 -0
- package/static/app/assets/System-D6uJacqu.js +1 -0
- package/static/app/assets/bot-DgC7xgUl.js +1 -0
- package/static/app/assets/cpu-DDGb4XHl.js +1 -0
- package/static/app/assets/download-C-Ei3GFa.js +1 -0
- package/static/app/assets/folder-open-BbVuSO9M.js +1 -0
- package/static/app/assets/hard-drive-yDZR6XYz.js +1 -0
- package/static/app/assets/index-CFRhNBuH.css +2 -0
- package/static/app/assets/index-DrZSkwrE.js +10 -0
- package/static/app/assets/input-DPIkTs9n.js +1 -0
- package/static/app/assets/navigation-B1mkmyUe.js +1 -0
- package/static/app/assets/network-0XDI1Uf_.js +1 -0
- package/static/app/assets/primitives-sIQYKyf5.js +1 -0
- package/static/app/assets/sunrise-DXMjueec.js +1 -0
- package/static/app/assets/textarea-DvnwpnqS.js +1 -0
- package/static/app/assets/utils-BrfFHAJl.js +7 -0
- package/static/app/index.html +4 -2
- package/static/sw.js +1 -1
- package/auto_setup.py +0 -21
- package/kg_schema.py +0 -13
- package/knowledge_graph.py +0 -51
- package/knowledge_graph_api.py +0 -24
- package/llm_router.py +0 -20
- package/local_knowledge_api.py +0 -16
- package/ltcai_cli.py +0 -21
- package/mcp_registry.py +0 -20
- package/p_reinforce.py +0 -16
- package/setup_wizard.py +0 -16
- package/static/app/assets/Act-Dd3z8AzF.js +0 -2
- package/static/app/assets/Capture-D2Aw9gkv.js +0 -1
- package/static/app/assets/Library-Yreq-KW5.js +0 -1
- package/static/app/assets/System-CXNmmtEo.js +0 -1
- package/static/app/assets/index-7gY9t9Sd.css +0 -2
- package/static/app/assets/index-CndfILiF.js +0 -18
- package/static/app/assets/primitives-DxsIXb6G.js +0 -1
- package/static/app/assets/textarea-DH7ne8VI.js +0 -1
- package/telegram_bot.py +0 -16
- package/tools/__init__.py +0 -29
|
@@ -21,9 +21,13 @@ change audit log. The classification itself lives in
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
23
|
import difflib
|
|
24
|
+
import hashlib
|
|
24
25
|
import logging
|
|
26
|
+
import os
|
|
27
|
+
import tempfile
|
|
28
|
+
import threading
|
|
25
29
|
from pathlib import Path
|
|
26
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
30
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
27
31
|
|
|
28
32
|
from latticeai.core.tool_governor import classify_tool_call
|
|
29
33
|
|
|
@@ -34,6 +38,55 @@ _MAX_DIFF_LINES = 400
|
|
|
34
38
|
_SMALL_TIER_DIFF_LINES = 40
|
|
35
39
|
|
|
36
40
|
|
|
41
|
+
class ProposalConflictError(ValueError):
|
|
42
|
+
"""The target file changed (or the proposal was already resolved) between
|
|
43
|
+
staging and approval — applying now would silently destroy user edits.
|
|
44
|
+
|
|
45
|
+
Subclasses :class:`ValueError` deliberately: API surfaces that only know
|
|
46
|
+
``except ValueError`` degrade to a 4xx instead of applying or crashing,
|
|
47
|
+
while conflict-aware surfaces catch this class first and answer **409**
|
|
48
|
+
with a rebase hint.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
reason: str,
|
|
55
|
+
path: str,
|
|
56
|
+
kind: str,
|
|
57
|
+
base_sha256: str = "",
|
|
58
|
+
current_sha256: str = "",
|
|
59
|
+
rebase_hint: str = "",
|
|
60
|
+
) -> None:
|
|
61
|
+
self.reason = reason
|
|
62
|
+
self.path = path
|
|
63
|
+
self.kind = kind
|
|
64
|
+
self.base_sha256 = base_sha256
|
|
65
|
+
self.current_sha256 = current_sha256
|
|
66
|
+
self.rebase_hint = rebase_hint or (
|
|
67
|
+
"제안 생성 이후 파일 상태가 바뀌었습니다. 이 제안을 거부하고 "
|
|
68
|
+
"현재 파일 내용을 기준으로 제안을 다시 생성하세요."
|
|
69
|
+
)
|
|
70
|
+
super().__init__(f"change proposal conflict ({reason}): {path}")
|
|
71
|
+
|
|
72
|
+
def to_detail(self) -> Dict[str, Any]:
|
|
73
|
+
"""409 response body — no staged content, just what the UI needs."""
|
|
74
|
+
return {
|
|
75
|
+
"error": "change_proposal_conflict",
|
|
76
|
+
"conflict": True,
|
|
77
|
+
"reason": self.reason,
|
|
78
|
+
"path": self.path,
|
|
79
|
+
"kind": self.kind,
|
|
80
|
+
"base_sha256": self.base_sha256,
|
|
81
|
+
"current_sha256": self.current_sha256,
|
|
82
|
+
"rebase_hint": self.rebase_hint,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _sha256_text(content: str) -> str:
|
|
87
|
+
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
88
|
+
|
|
89
|
+
|
|
37
90
|
def _unified_diff(before: str, after: str, path: str) -> List[str]:
|
|
38
91
|
lines = list(
|
|
39
92
|
difflib.unified_diff(
|
|
@@ -60,6 +113,11 @@ class ChangeProposalService:
|
|
|
60
113
|
self._review_queue = review_queue
|
|
61
114
|
self._resolve_path = resolve_path
|
|
62
115
|
self._audit = audit or (lambda *a, **kw: None)
|
|
116
|
+
# One protected section for status-check → conflict-check → apply →
|
|
117
|
+
# transition. In-process serialization is the right weight here: the
|
|
118
|
+
# review queue lives in this process and the write itself is atomic
|
|
119
|
+
# (temp file + os.replace), so a cross-process file lock adds nothing.
|
|
120
|
+
self._apply_lock = threading.Lock()
|
|
63
121
|
|
|
64
122
|
# ── classification / staging (agent-loop governor port) ─────────────
|
|
65
123
|
|
|
@@ -125,15 +183,24 @@ class ChangeProposalService:
|
|
|
125
183
|
return False
|
|
126
184
|
|
|
127
185
|
def _read_before(self, path: str) -> str:
|
|
186
|
+
return self._snapshot(path)[1]
|
|
187
|
+
|
|
188
|
+
def _snapshot(self, path: str) -> Tuple[bool, str]:
|
|
189
|
+
"""(exists, content) of the target *as the proposal pipeline sees it*.
|
|
190
|
+
|
|
191
|
+
Both staging and the approve-time conflict check go through this one
|
|
192
|
+
normalization (truncate → utf-8 with replacement), so an unchanged
|
|
193
|
+
file always hashes identically at both points in time.
|
|
194
|
+
"""
|
|
128
195
|
try:
|
|
129
196
|
target = self._resolve_path(path)
|
|
130
197
|
if not target.is_file():
|
|
131
|
-
return ""
|
|
198
|
+
return False, ""
|
|
132
199
|
raw = target.read_bytes()[:_MAX_STAGED_BYTES]
|
|
133
|
-
return raw.decode("utf-8", errors="replace")
|
|
200
|
+
return True, raw.decode("utf-8", errors="replace")
|
|
134
201
|
except Exception:
|
|
135
202
|
LOGGER.exception("change proposal read failed")
|
|
136
|
-
return ""
|
|
203
|
+
return False, ""
|
|
137
204
|
|
|
138
205
|
def _staged_content(self, name: str, args: Dict[str, Any]) -> Optional[str]:
|
|
139
206
|
if name == "write_file":
|
|
@@ -164,7 +231,7 @@ class ChangeProposalService:
|
|
|
164
231
|
workspace_id: Optional[str] = None,
|
|
165
232
|
context: Optional[Dict[str, Any]] = None,
|
|
166
233
|
) -> Dict[str, Any]:
|
|
167
|
-
before = self.
|
|
234
|
+
base_exists, before = self._snapshot(path)
|
|
168
235
|
diff = _unified_diff(before, new_content, path)
|
|
169
236
|
tier = "small" if len(diff) <= _SMALL_TIER_DIFF_LINES else "large"
|
|
170
237
|
provenance: Dict[str, Any] = {"proposed_by": proposed_by, "reason": reason}
|
|
@@ -183,6 +250,11 @@ class ChangeProposalService:
|
|
|
183
250
|
"tier": tier,
|
|
184
251
|
"before_bytes": len(before.encode("utf-8")),
|
|
185
252
|
"after_bytes": len(new_content.encode("utf-8")),
|
|
253
|
+
# Base snapshot for the approve-time conflict check: an empty
|
|
254
|
+
# base_sha256 with base_exists=False means "proposed against a
|
|
255
|
+
# missing file", never "hash of the empty string".
|
|
256
|
+
"base_exists": base_exists,
|
|
257
|
+
"base_sha256": _sha256_text(before) if base_exists else "",
|
|
186
258
|
},
|
|
187
259
|
provenance=provenance,
|
|
188
260
|
user_email=user_email,
|
|
@@ -203,7 +275,7 @@ class ChangeProposalService:
|
|
|
203
275
|
user_email: Optional[str] = None,
|
|
204
276
|
workspace_id: Optional[str] = None,
|
|
205
277
|
) -> Dict[str, Any]:
|
|
206
|
-
before = self.
|
|
278
|
+
base_exists, before = self._snapshot(path)
|
|
207
279
|
item = self._review_queue.create(
|
|
208
280
|
title=f"파일 삭제 제안: {path}",
|
|
209
281
|
summary=reason or "기존 파일을 삭제하는 작업이라 검토 후 적용됩니다.",
|
|
@@ -215,6 +287,8 @@ class ChangeProposalService:
|
|
|
215
287
|
"tier": "large",
|
|
216
288
|
"before_bytes": len(before.encode("utf-8")),
|
|
217
289
|
"after_bytes": 0,
|
|
290
|
+
"base_exists": base_exists,
|
|
291
|
+
"base_sha256": _sha256_text(before) if base_exists else "",
|
|
218
292
|
},
|
|
219
293
|
provenance={"proposed_by": proposed_by, "reason": reason},
|
|
220
294
|
user_email=user_email,
|
|
@@ -274,28 +348,96 @@ class ChangeProposalService:
|
|
|
274
348
|
self, item_id: str, *, user_email: Optional[str] = None,
|
|
275
349
|
workspace_id: Optional[str] = None,
|
|
276
350
|
) -> Dict[str, Any]:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
351
|
+
"""Apply the staged change exactly as reviewed — but only if the file
|
|
352
|
+
on disk still matches the base snapshot the reviewer looked at.
|
|
353
|
+
|
|
354
|
+
Raises :class:`ProposalConflictError` (→ HTTP 409) when the target
|
|
355
|
+
drifted since staging or the proposal was already resolved; nothing
|
|
356
|
+
touches disk in that case, so out-of-band user edits are preserved.
|
|
357
|
+
"""
|
|
358
|
+
with self._apply_lock:
|
|
359
|
+
item = self._review_queue.get(item_id, workspace_id=workspace_id)
|
|
360
|
+
if item.get("source") != "change_proposal":
|
|
361
|
+
raise KeyError(f"not a change proposal: {item_id}")
|
|
362
|
+
payload = item.get("payload") or {}
|
|
363
|
+
kind = str(item.get("kind") or "")
|
|
364
|
+
path = str(payload.get("path") or "")
|
|
365
|
+
if kind not in ("file_update", "file_delete"):
|
|
366
|
+
raise ValueError(f"unknown change proposal kind: {kind}")
|
|
367
|
+
|
|
368
|
+
# Duplicate/concurrent approval guard: a resolved proposal must
|
|
369
|
+
# never re-apply (the disk may have moved on since it was applied).
|
|
370
|
+
status = str(item.get("status") or "pending")
|
|
371
|
+
if status not in ("pending", "snoozed"):
|
|
372
|
+
raise ProposalConflictError(
|
|
373
|
+
reason=f"already_{status}", path=path, kind=kind,
|
|
374
|
+
rebase_hint="이미 처리된 제안입니다. 다시 적용할 수 없습니다.",
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
self._check_base_unchanged(payload, path=path, kind=kind)
|
|
378
|
+
|
|
379
|
+
target = self._resolve_path(path)
|
|
380
|
+
if kind == "file_update":
|
|
381
|
+
self._atomic_write(target, str(payload.get("new_content") or ""))
|
|
382
|
+
else: # file_delete — existence re-verified inside the lock
|
|
383
|
+
if target.is_file():
|
|
384
|
+
target.unlink()
|
|
385
|
+
approved = self._review_queue.approve(item_id, workspace_id=workspace_id)
|
|
293
386
|
self._audit(
|
|
294
387
|
"change_proposal_applied", user_email=user_email,
|
|
295
388
|
proposal_id=item_id, path=path, kind=kind,
|
|
296
389
|
)
|
|
297
390
|
return {"item": approved, "applied": True, "path": path, "kind": kind}
|
|
298
391
|
|
|
392
|
+
def _check_base_unchanged(
|
|
393
|
+
self, payload: Dict[str, Any], *, path: str, kind: str
|
|
394
|
+
) -> None:
|
|
395
|
+
"""Compare the disk state *now* against the staged base snapshot."""
|
|
396
|
+
if "base_sha256" not in payload or "base_exists" not in payload:
|
|
397
|
+
# Legacy proposal staged before base snapshots existed — keep the
|
|
398
|
+
# historical apply-as-reviewed behavior rather than rejecting it.
|
|
399
|
+
return
|
|
400
|
+
base_exists = bool(payload.get("base_exists"))
|
|
401
|
+
base_sha256 = str(payload.get("base_sha256") or "")
|
|
402
|
+
current_exists, current_content = self._snapshot(path)
|
|
403
|
+
current_sha256 = _sha256_text(current_content) if current_exists else ""
|
|
404
|
+
if not base_exists:
|
|
405
|
+
if current_exists:
|
|
406
|
+
raise ProposalConflictError(
|
|
407
|
+
reason="file_created_since_proposal", path=path, kind=kind,
|
|
408
|
+
current_sha256=current_sha256,
|
|
409
|
+
)
|
|
410
|
+
return
|
|
411
|
+
if not current_exists:
|
|
412
|
+
raise ProposalConflictError(
|
|
413
|
+
reason="file_deleted_since_proposal", path=path, kind=kind,
|
|
414
|
+
base_sha256=base_sha256,
|
|
415
|
+
)
|
|
416
|
+
if current_sha256 != base_sha256:
|
|
417
|
+
raise ProposalConflictError(
|
|
418
|
+
reason="file_modified_since_proposal", path=path, kind=kind,
|
|
419
|
+
base_sha256=base_sha256, current_sha256=current_sha256,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
@staticmethod
|
|
423
|
+
def _atomic_write(target: Path, content: str) -> None:
|
|
424
|
+
"""Write via a same-directory temp file + ``os.replace`` so readers
|
|
425
|
+
never observe a partially written proposal."""
|
|
426
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
427
|
+
fd, tmp_name = tempfile.mkstemp(
|
|
428
|
+
dir=str(target.parent), prefix=f".{target.name}.", suffix=".staged"
|
|
429
|
+
)
|
|
430
|
+
try:
|
|
431
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
432
|
+
handle.write(content)
|
|
433
|
+
os.replace(tmp_name, target)
|
|
434
|
+
except BaseException:
|
|
435
|
+
try:
|
|
436
|
+
os.unlink(tmp_name)
|
|
437
|
+
except OSError:
|
|
438
|
+
pass
|
|
439
|
+
raise
|
|
440
|
+
|
|
299
441
|
def reject(
|
|
300
442
|
self, item_id: str, *, user_email: Optional[str] = None,
|
|
301
443
|
workspace_id: Optional[str] = None, reason: str = "",
|
|
@@ -319,4 +461,4 @@ class ChangeProposalService:
|
|
|
319
461
|
return {"item": dismissed, "applied": False, "reason": reason}
|
|
320
462
|
|
|
321
463
|
|
|
322
|
-
__all__ = ["ChangeProposalService"]
|
|
464
|
+
__all__ = ["ChangeProposalService", "ProposalConflictError"]
|
|
@@ -18,7 +18,7 @@ from typing import Any, Dict, List
|
|
|
18
18
|
|
|
19
19
|
from latticeai.services.architecture_readiness import architecture_readiness
|
|
20
20
|
|
|
21
|
-
PRODUCT_VERSION_TARGET = "9.
|
|
21
|
+
PRODUCT_VERSION_TARGET = "9.9.1"
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
@dataclass(frozen=True)
|
|
@@ -140,7 +140,7 @@ PRODUCT_GATES: List[ProductGate] = [
|
|
|
140
140
|
evidence=[
|
|
141
141
|
"scripts/brain_quality_eval.py",
|
|
142
142
|
"scripts/product_readiness.py",
|
|
143
|
-
"tests/unit/
|
|
143
|
+
"tests/unit/test_product_readiness.py",
|
|
144
144
|
"tests/visual/v3.spec.js::Brain Chat Home",
|
|
145
145
|
".github/workflows/ci.yml::scripts/product_readiness.py",
|
|
146
146
|
".github/workflows/release.yml::npm run lint",
|
|
@@ -21,6 +21,7 @@ from latticeai.core.agent_prompts import (
|
|
|
21
21
|
PLANNER_PROMPT,
|
|
22
22
|
)
|
|
23
23
|
from latticeai.core.policy import role_has_capability
|
|
24
|
+
from latticeai.core.tool_governor import classify_tool_call
|
|
24
25
|
from latticeai.core.tool_registry import ToolPermission, ToolPolicy
|
|
25
26
|
from latticeai.tools import AGENT_ROOT, DEFAULT_TOOL_REGISTRY, ToolError, ensure_agent_root
|
|
26
27
|
|
|
@@ -122,6 +123,22 @@ class ToolDispatchService:
|
|
|
122
123
|
detail=f"'{tool_name}' 툴에는 '{capability}' capability가 필요합니다.",
|
|
123
124
|
)
|
|
124
125
|
|
|
126
|
+
def _governed_path_exists(self, path: str) -> bool:
|
|
127
|
+
"""Best-effort existence check for governance classification.
|
|
128
|
+
|
|
129
|
+
Resolves workspace-relative paths under ``AGENT_ROOT`` and honors
|
|
130
|
+
absolute paths (home-sandbox writes). Never raises — an unresolvable
|
|
131
|
+
path is treated as non-existent (additive), which the fail-closed
|
|
132
|
+
guard only escalates when the target actually exists.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
candidate = Path(path)
|
|
136
|
+
if not candidate.is_absolute():
|
|
137
|
+
candidate = Path(AGENT_ROOT) / candidate
|
|
138
|
+
return candidate.exists()
|
|
139
|
+
except Exception:
|
|
140
|
+
return False
|
|
141
|
+
|
|
125
142
|
def user_role(self, current_user: str) -> str:
|
|
126
143
|
users = self.load_users()
|
|
127
144
|
try:
|
|
@@ -162,6 +179,22 @@ class ToolDispatchService:
|
|
|
162
179
|
status_code=403,
|
|
163
180
|
detail=f"'{tool_name}' 툴은 파괴적 작업으로 차단되었습니다.",
|
|
164
181
|
)
|
|
182
|
+
# Fail-closed governance: a call that would rewrite existing content but
|
|
183
|
+
# cannot be staged as a reviewable proposal is blocked, never applied
|
|
184
|
+
# silently. New-file (additive) calls are unaffected.
|
|
185
|
+
verdict = classify_tool_call(
|
|
186
|
+
tool_name, args or {},
|
|
187
|
+
policy=dict(policy), path_exists=self._governed_path_exists,
|
|
188
|
+
)
|
|
189
|
+
if verdict.get("fail_closed"):
|
|
190
|
+
raise HTTPException(
|
|
191
|
+
status_code=409,
|
|
192
|
+
detail=(
|
|
193
|
+
f"'{tool_name}' 툴은 기존 콘텐츠를 변경하지만 검토 가능한 제안으로 "
|
|
194
|
+
"적용할 수 없어 차단되었습니다. 새 파일 이름으로 생성하거나 "
|
|
195
|
+
"지원되는 편집 도구(write_file/edit_file)를 사용하세요."
|
|
196
|
+
),
|
|
197
|
+
)
|
|
165
198
|
if (
|
|
166
199
|
require_auto_approval
|
|
167
200
|
and not trusted_admin
|
|
@@ -23,7 +23,7 @@ from pathlib import Path
|
|
|
23
23
|
from typing import Any, Callable, Dict, List, Optional
|
|
24
24
|
|
|
25
25
|
from latticeai.core.tool_registry import ToolRegistry
|
|
26
|
-
from p_reinforce import BRAIN_DIR, STRUCTURE
|
|
26
|
+
from latticeai.services.p_reinforce import BRAIN_DIR, STRUCTURE
|
|
27
27
|
|
|
28
28
|
_PLATFORM = platform.system() # "Darwin" | "Windows" | "Linux"
|
|
29
29
|
|
|
@@ -6,7 +6,7 @@ import hashlib
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, Dict, List, Optional
|
|
8
8
|
|
|
9
|
-
from p_reinforce import BRAIN_DIR, STRUCTURE
|
|
9
|
+
from latticeai.services.p_reinforce import BRAIN_DIR, STRUCTURE
|
|
10
10
|
from latticeai.tools import MAX_FILE_BYTES, ToolError
|
|
11
11
|
|
|
12
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.9.1",
|
|
4
4
|
"description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"start": "LTCAI",
|
|
19
|
-
"dev": "python3
|
|
19
|
+
"dev": "python3 -m latticeai.cli.entrypoint --reload",
|
|
20
20
|
"build": "npm run build:assets && npm run build:python",
|
|
21
21
|
"frontend:dev": "vite --host 127.0.0.1",
|
|
22
22
|
"frontend:openapi": "node scripts/run_python.mjs scripts/export_openapi.py frontend/openapi.json && npx openapi-typescript frontend/openapi.json -o frontend/src/api/openapi.ts",
|
|
@@ -24,10 +24,13 @@
|
|
|
24
24
|
"build:assets": "vite build && node scripts/build_frontend_assets.mjs",
|
|
25
25
|
"build:python": "node scripts/run_python.mjs -m build",
|
|
26
26
|
"check:python": "node scripts/run_python.mjs scripts/check_python.py",
|
|
27
|
-
"lint": "npm run lint:python && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend && npm run frontend:openapi:check && node scripts/check_i18n_literals.mjs && npm run test:browser-extension",
|
|
27
|
+
"lint": "npm run lint:python && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend && npm run frontend:openapi:check && node scripts/check_i18n_literals.mjs && npm run check:bundle && npm run check:legacy-debt && npm run test:browser-extension",
|
|
28
|
+
"check:legacy-debt": "node scripts/check_legacy_debt.mjs",
|
|
28
29
|
"lint:python": "node scripts/run_python.mjs -m ruff check .",
|
|
29
30
|
"lint:frontend": "node scripts/lint_frontend.mjs",
|
|
30
|
-
"
|
|
31
|
+
"check:bundle": "node scripts/check_bundle_budget.mjs",
|
|
32
|
+
"docs:check-links": "node scripts/check_markdown_links.mjs && node scripts/check_doc_status.mjs",
|
|
33
|
+
"docs:check-status": "node scripts/check_doc_status.mjs",
|
|
31
34
|
"docs:check-current": "node scripts/check_current_release_docs.mjs",
|
|
32
35
|
"typecheck": "npm run typecheck:frontend && cd vscode-extension && npm run build",
|
|
33
36
|
"typecheck:frontend": "npx tsc -p tsconfig.json --noEmit",
|
|
@@ -44,7 +47,8 @@
|
|
|
44
47
|
"desktop:tauri:check": "cd src-tauri && cargo check",
|
|
45
48
|
"desktop:electron": "electron desktop/electron/main.cjs",
|
|
46
49
|
"package:vsix": "node scripts/build_vsix.mjs",
|
|
47
|
-
"release:evidence": "node scripts/capture_release_evidence.mjs",
|
|
50
|
+
"release:evidence": "node scripts/capture_release_evidence.mjs && node scripts/prune_release_evidence.mjs",
|
|
51
|
+
"release:evidence:prune": "node scripts/prune_release_evidence.mjs",
|
|
48
52
|
"release:artifacts": "node scripts/clean_release_artifacts.mjs $npm_package_version && npm run build:assets && npm run build:python && npm pack && npm run package:vsix && npm run desktop:tauri:build",
|
|
49
53
|
"release:validate": "node scripts/run_python.mjs scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz --require-dmg",
|
|
50
54
|
"release:smoke": "node scripts/run_python.mjs scripts/release_smoke.py $npm_package_version",
|
|
@@ -74,17 +78,7 @@
|
|
|
74
78
|
"bin/ltcai.js",
|
|
75
79
|
"LICENSE",
|
|
76
80
|
"requirements.txt",
|
|
77
|
-
"ltcai_cli.py",
|
|
78
|
-
"auto_setup.py",
|
|
79
81
|
"server.py",
|
|
80
|
-
"kg_schema.py",
|
|
81
|
-
"knowledge_graph.py",
|
|
82
|
-
"local_knowledge_api.py",
|
|
83
|
-
"llm_router.py",
|
|
84
|
-
"p_reinforce.py",
|
|
85
|
-
"telegram_bot.py",
|
|
86
|
-
"tools/",
|
|
87
|
-
"mcp_registry.py",
|
|
88
82
|
"latticeai/**/*.py",
|
|
89
83
|
"lattice_brain/**/*.py",
|
|
90
84
|
"skills/",
|
|
@@ -111,8 +105,6 @@
|
|
|
111
105
|
"!**/*.pyc",
|
|
112
106
|
"!**/*.map",
|
|
113
107
|
"README.md",
|
|
114
|
-
"setup_wizard.py",
|
|
115
|
-
"knowledge_graph_api.py",
|
|
116
108
|
"static/vendor/",
|
|
117
109
|
"src-tauri/",
|
|
118
110
|
"!src-tauri/gen/",
|