ltcai 9.0.0 → 9.2.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 (189) hide show
  1. package/README.md +88 -46
  2. package/auto_setup.py +7 -853
  3. package/desktop/electron/README.md +9 -0
  4. package/desktop/electron/main.cjs +5 -3
  5. package/docs/CHANGELOG.md +178 -2
  6. package/docs/COMMUNITY_AND_PLUGINS.md +4 -2
  7. package/docs/DEVELOPMENT.md +53 -14
  8. package/docs/LEGACY_COMPATIBILITY.md +4 -4
  9. package/docs/ONBOARDING.md +10 -2
  10. package/docs/OPERATIONS.md +8 -4
  11. package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
  12. package/docs/TRUST_MODEL.md +5 -2
  13. package/docs/WHY_LATTICE.md +15 -10
  14. package/docs/WORKFLOW_DESIGNER.md +22 -0
  15. package/docs/kg-schema.md +13 -2
  16. package/docs/mcp-tools.md +17 -6
  17. package/docs/privacy.md +19 -3
  18. package/docs/public-deploy.md +32 -3
  19. package/docs/security-model.md +46 -11
  20. package/lattice_brain/__init__.py +1 -1
  21. package/lattice_brain/archive.py +1 -6
  22. package/lattice_brain/context.py +66 -9
  23. package/lattice_brain/graph/_kg_fsutil.py +1 -5
  24. package/lattice_brain/graph/discovery_index.py +174 -20
  25. package/lattice_brain/graph/documents.py +44 -9
  26. package/lattice_brain/graph/ingest.py +47 -20
  27. package/lattice_brain/graph/provenance.py +13 -6
  28. package/lattice_brain/graph/retrieval.py +140 -39
  29. package/lattice_brain/graph/retrieval_docgen.py +37 -4
  30. package/lattice_brain/ingestion.py +4 -7
  31. package/lattice_brain/portability.py +28 -14
  32. package/lattice_brain/runtime/agent_runtime.py +5 -9
  33. package/lattice_brain/runtime/hooks.py +30 -13
  34. package/lattice_brain/runtime/multi_agent.py +27 -8
  35. package/lattice_brain/runtime/statuses.py +10 -0
  36. package/lattice_brain/utils.py +20 -2
  37. package/lattice_brain/workflow.py +1 -5
  38. package/latticeai/__init__.py +1 -1
  39. package/latticeai/api/admin.py +1 -1
  40. package/latticeai/api/agent_registry.py +10 -8
  41. package/latticeai/api/agents.py +22 -3
  42. package/latticeai/api/auth.py +78 -16
  43. package/latticeai/api/browser.py +303 -34
  44. package/latticeai/api/chat.py +320 -850
  45. package/latticeai/api/chat_agent_http.py +356 -0
  46. package/latticeai/api/chat_contracts.py +54 -0
  47. package/latticeai/api/chat_documents.py +256 -0
  48. package/latticeai/api/chat_helpers.py +28 -1
  49. package/latticeai/api/chat_history.py +99 -0
  50. package/latticeai/api/chat_intents.py +316 -0
  51. package/latticeai/api/chat_stream.py +115 -0
  52. package/latticeai/api/computer_use.py +62 -9
  53. package/latticeai/api/health.py +9 -3
  54. package/latticeai/api/hooks.py +17 -6
  55. package/latticeai/api/knowledge_graph.py +78 -14
  56. package/latticeai/api/local_files.py +7 -2
  57. package/latticeai/api/mcp.py +102 -23
  58. package/latticeai/api/models.py +72 -33
  59. package/latticeai/api/network.py +5 -5
  60. package/latticeai/api/permissions.py +67 -26
  61. package/latticeai/api/plugins.py +21 -7
  62. package/latticeai/api/portability.py +5 -5
  63. package/latticeai/api/realtime.py +33 -5
  64. package/latticeai/api/setup.py +2 -2
  65. package/latticeai/api/static_routes.py +123 -24
  66. package/latticeai/api/tools.py +96 -14
  67. package/latticeai/api/workflow_designer.py +37 -0
  68. package/latticeai/api/workspace.py +2 -1
  69. package/latticeai/app_factory.py +110 -52
  70. package/latticeai/core/agent.py +50 -13
  71. package/latticeai/core/agent_prompts.py +5 -0
  72. package/latticeai/core/agent_registry.py +1 -5
  73. package/latticeai/core/config.py +23 -3
  74. package/latticeai/core/context_builder.py +21 -3
  75. package/latticeai/core/file_generation.py +451 -0
  76. package/latticeai/core/invitations.py +1 -4
  77. package/latticeai/core/io_utils.py +9 -1
  78. package/latticeai/core/legacy_compatibility.py +24 -3
  79. package/latticeai/core/marketplace.py +1 -1
  80. package/latticeai/core/plugins.py +15 -0
  81. package/latticeai/core/policy.py +1 -1
  82. package/latticeai/core/realtime.py +38 -15
  83. package/latticeai/core/sessions.py +7 -2
  84. package/latticeai/core/timeutil.py +10 -0
  85. package/latticeai/core/tool_registry.py +90 -18
  86. package/latticeai/core/users.py +1 -5
  87. package/latticeai/core/workspace_graph_trace.py +31 -5
  88. package/latticeai/core/workspace_memory.py +3 -1
  89. package/latticeai/core/workspace_os.py +18 -7
  90. package/latticeai/core/workspace_os_utils.py +0 -5
  91. package/latticeai/core/workspace_permissions.py +2 -1
  92. package/latticeai/core/workspace_plugins.py +1 -1
  93. package/latticeai/core/workspace_runs.py +14 -5
  94. package/latticeai/core/workspace_skills.py +1 -1
  95. package/latticeai/core/workspace_snapshots.py +2 -1
  96. package/latticeai/core/workspace_timeline.py +2 -1
  97. package/latticeai/integrations/telegram_bot.py +94 -43
  98. package/latticeai/models/router.py +130 -36
  99. package/latticeai/runtime/access_runtime.py +62 -4
  100. package/latticeai/runtime/automation_runtime.py +13 -7
  101. package/latticeai/runtime/brain_runtime.py +19 -7
  102. package/latticeai/runtime/chat_wiring.py +2 -0
  103. package/latticeai/runtime/config_runtime.py +58 -26
  104. package/latticeai/runtime/context_runtime.py +16 -4
  105. package/latticeai/runtime/hooks_runtime.py +1 -1
  106. package/latticeai/runtime/model_wiring.py +33 -5
  107. package/latticeai/runtime/namespace_runtime.py +62 -72
  108. package/latticeai/runtime/platform_runtime_wiring.py +4 -3
  109. package/latticeai/runtime/review_wiring.py +1 -1
  110. package/latticeai/runtime/router_registration.py +34 -1
  111. package/latticeai/runtime/security_runtime.py +110 -13
  112. package/latticeai/runtime/stages.py +27 -0
  113. package/latticeai/server_app.py +5 -1
  114. package/latticeai/services/architecture_readiness.py +74 -5
  115. package/latticeai/services/brain_automation.py +3 -26
  116. package/latticeai/services/chat_service.py +205 -15
  117. package/latticeai/services/local_knowledge.py +423 -0
  118. package/latticeai/services/memory_service.py +55 -21
  119. package/latticeai/services/model_engines.py +48 -33
  120. package/latticeai/services/model_errors.py +17 -0
  121. package/latticeai/services/model_loading.py +28 -25
  122. package/latticeai/services/model_runtime.py +228 -162
  123. package/latticeai/services/p_reinforce.py +22 -3
  124. package/latticeai/services/platform_runtime.py +83 -23
  125. package/latticeai/services/product_readiness.py +19 -17
  126. package/latticeai/services/review_queue.py +12 -0
  127. package/latticeai/services/router_context.py +1 -0
  128. package/latticeai/services/run_executor.py +4 -9
  129. package/latticeai/services/search_service.py +203 -28
  130. package/latticeai/services/tool_dispatch.py +28 -5
  131. package/latticeai/services/triggers.py +53 -4
  132. package/latticeai/services/upload_service.py +13 -2
  133. package/latticeai/setup/__init__.py +25 -0
  134. package/latticeai/setup/auto_setup.py +857 -0
  135. package/latticeai/setup/wizard.py +1264 -0
  136. package/latticeai/tools/__init__.py +278 -0
  137. package/{tools → latticeai/tools}/commands.py +67 -7
  138. package/{tools → latticeai/tools}/computer.py +1 -1
  139. package/{tools → latticeai/tools}/documents.py +1 -1
  140. package/{tools → latticeai/tools}/filesystem.py +3 -3
  141. package/latticeai/tools/knowledge.py +178 -0
  142. package/{tools → latticeai/tools}/local_files.py +1 -1
  143. package/{tools → latticeai/tools}/network.py +0 -1
  144. package/local_knowledge_api.py +4 -342
  145. package/package.json +22 -2
  146. package/scripts/brain_quality_eval.py +3 -1
  147. package/scripts/bump_version.py +3 -0
  148. package/scripts/capture_release_evidence.mjs +180 -0
  149. package/scripts/check_current_release_docs.mjs +142 -0
  150. package/scripts/check_i18n_literals.mjs +107 -31
  151. package/scripts/check_openapi_drift.mjs +56 -0
  152. package/scripts/export_openapi.py +67 -7
  153. package/scripts/i18n_literal_allowlist.json +22 -24
  154. package/scripts/run_integration_tests.mjs +72 -10
  155. package/scripts/validate_release_artifacts.py +49 -7
  156. package/scripts/wheel_smoke.py +5 -0
  157. package/setup_wizard.py +3 -1260
  158. package/src-tauri/Cargo.lock +1 -1
  159. package/src-tauri/Cargo.toml +1 -1
  160. package/src-tauri/tauri.conf.json +1 -1
  161. package/static/app/asset-manifest.json +11 -11
  162. package/static/app/assets/Act-C3dBrWE-.js +1 -0
  163. package/static/app/assets/Brain-DBYgdcjt.js +321 -0
  164. package/static/app/assets/Capture-Cf3hqRtN.js +1 -0
  165. package/static/app/assets/Library-CFfkNn3s.js +1 -0
  166. package/static/app/assets/System-BOurbT-v.js +1 -0
  167. package/static/app/assets/index-A3M9sElj.js +17 -0
  168. package/static/app/assets/index-Bmx9rzTc.css +2 -0
  169. package/static/app/assets/primitives-DcUUmhdC.js +1 -0
  170. package/static/app/assets/textarea-BklR6zN4.js +1 -0
  171. package/static/app/index.html +2 -2
  172. package/static/sw.js +1 -1
  173. package/tools/__init__.py +21 -269
  174. package/docs/CODE_REVIEW_2026-07-06.md +0 -764
  175. package/latticeai/runtime/app_context_runtime.py +0 -13
  176. package/latticeai/runtime/tail_wiring.py +0 -21
  177. package/scripts/com.pts.claudecode.discord.plist +0 -31
  178. package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
  179. package/scripts/start-pts-claudecode-discord.sh +0 -51
  180. package/static/app/assets/Act-21lIXx2E.js +0 -1
  181. package/static/app/assets/Brain-BqUd5UJJ.js +0 -321
  182. package/static/app/assets/Capture-BA7Z2Q1u.js +0 -1
  183. package/static/app/assets/Library-bFMtyni3.js +0 -1
  184. package/static/app/assets/System-K6krGCqn.js +0 -1
  185. package/static/app/assets/index-C4R3ws30.js +0 -17
  186. package/static/app/assets/index-ChSeOB02.css +0 -2
  187. package/static/app/assets/primitives-sQU3it5I.js +0 -1
  188. package/static/app/assets/textarea-DK3Fd_lR.js +0 -1
  189. package/tools/knowledge.py +0 -95
@@ -20,7 +20,9 @@ from fastapi import HTTPException, Request
20
20
  from lattice_brain.runtime.hooks import dispatch_tool
21
21
  from lattice_brain.runtime.multi_agent import MultiAgentOrchestrator, default_role_runner, llm_role_runner
22
22
  from lattice_brain.workflow import ApprovalRequired, WorkflowEngine
23
- from tools import execute_tool
23
+ from latticeai.core.tool_registry import SCOPED_KNOWLEDGE_TOOLS
24
+ from latticeai.services.tool_dispatch import enforce_tool_policy
25
+ from latticeai.tools import execute_tool
24
26
 
25
27
 
26
28
  class PlatformRuntime:
@@ -38,6 +40,7 @@ class PlatformRuntime:
38
40
  llm_generate: Optional[Callable[..., str]] = None,
39
41
  llm_available: Optional[Callable[[], bool]] = None,
40
42
  agent_registry: Any = None,
43
+ memory_recall: Optional[Callable[..., Dict[str, Any]]] = None,
41
44
  ):
42
45
  self.store = store
43
46
  self.svc = workspace_service
@@ -55,6 +58,7 @@ class PlatformRuntime:
55
58
  self.llm_generate = llm_generate
56
59
  self.llm_available = llm_available or (lambda: False)
57
60
  self.agent_registry = agent_registry
61
+ self.memory_recall = memory_recall
58
62
 
59
63
  # ── request gating ────────────────────────────────────────────────────
60
64
 
@@ -75,7 +79,12 @@ class PlatformRuntime:
75
79
  workspaces = self.svc.list_workspaces(user or None).get("workspaces", [])
76
80
  return {ws.get("workspace_id") for ws in workspaces if ws.get("workspace_id")}
77
81
  except Exception:
78
- return None
82
+ # ``None`` deliberately means "unscoped local/no-auth mode" to
83
+ # search and realtime consumers. Returning it for an authenticated
84
+ # account on a storage/service failure would therefore fail open
85
+ # and expose every workspace. Authenticated failures see nothing;
86
+ # only the explicit anonymous local mode keeps legacy semantics.
87
+ return set() if user else None
79
88
 
80
89
  # ── plugin lifecycle hooks ────────────────────────────────────────────
81
90
 
@@ -86,7 +95,7 @@ class PlatformRuntime:
86
95
 
87
96
  # ── shared node runners ───────────────────────────────────────────────
88
97
 
89
- def _tool_node_runner(self):
98
+ def _tool_node_runner(self, user=None, scope=None):
90
99
  """Workflow tool node: EXECUTES the tool under governance (v4).
91
100
 
92
101
  Auto-approve tools run immediately through the shared dispatch_tool
@@ -99,9 +108,12 @@ class PlatformRuntime:
99
108
  def runner(*, node, context):
100
109
  cfg = node.get("config") or {}
101
110
  name = cfg.get("tool") or ""
102
- args = cfg.get("args") or {}
111
+ args = dict(cfg.get("args") or {})
103
112
  if not name:
104
113
  raise ValueError("tool node has no tool configured")
114
+ if name in SCOPED_KNOWLEDGE_TOOLS:
115
+ args["workspace_id"] = scope or "personal"
116
+ args["user_email"] = user or "local"
105
117
  try:
106
118
  permission = dict(self.get_tool_permission(name, args))
107
119
  except TypeError:
@@ -142,6 +154,31 @@ class PlatformRuntime:
142
154
 
143
155
  def _context_provider(self, user, scope):
144
156
  def provider(goal: str):
157
+ recall = getattr(self, "memory_recall", None)
158
+ if recall is not None:
159
+ try:
160
+ payload = recall(
161
+ goal,
162
+ user_email=user,
163
+ workspace_id=scope,
164
+ limit=8,
165
+ )
166
+ results = payload.get("results") or payload.get("items") or []
167
+ evidence = []
168
+ for item in results[:8]:
169
+ if not isinstance(item, dict):
170
+ continue
171
+ source = str(item.get("source") or "memory")
172
+ title = str(item.get("title") or item.get("kind") or "Memory")
173
+ snippet = str(item.get("snippet") or item.get("content") or "").strip()
174
+ if snippet:
175
+ evidence.append(f"[{source}] {title}: {snippet[:240]}")
176
+ if evidence:
177
+ return evidence
178
+ except Exception:
179
+ # Recall is an enrichment seam; the legacy store fallback
180
+ # keeps agent execution available if it degrades.
181
+ pass
145
182
  try:
146
183
  mems = self.store.search_memories(goal, user_email=user, workspace_id=scope).get("memories", [])
147
184
  ctx = [str(m.get("content") or "")[:180] for m in mems[:6]]
@@ -152,6 +189,14 @@ class PlatformRuntime:
152
189
  ctx = synth + ctx
153
190
  except Exception:
154
191
  pass
192
+ if not ctx:
193
+ try:
194
+ recent = self.store.list_memories(user_email=user, workspace_id=scope).get("memories", [])
195
+ # list_memories is newest-first; automation should ground
196
+ # itself in the knowledge that just entered the Brain.
197
+ ctx = [str(m.get("content") or "")[:180] for m in recent[:8]]
198
+ except Exception:
199
+ pass
155
200
  return ctx[:8]
156
201
  except Exception:
157
202
  return []
@@ -169,15 +214,20 @@ class PlatformRuntime:
169
214
  tool = args.get("tool") or (manifest.provides.get("tools") or [None])[0]
170
215
  if not tool:
171
216
  raise ValueError(f"plugin '{plugin_id}' run_tool needs a tool name")
172
- permission = dict(self.get_tool_permission(tool))
173
- if permission.get("requires_approval"):
174
- raise ApprovalRequired(
175
- f"plugin tool '{tool}' requires explicit approval",
176
- tool=tool, args=args, permission=permission,
177
- )
217
+ args = dict(args)
218
+ if tool in SCOPED_KNOWLEDGE_TOOLS:
219
+ args["workspace_id"] = scope or "personal"
220
+ args["user_email"] = user or "local"
221
+ policy = enforce_tool_policy(
222
+ tool,
223
+ args,
224
+ current_user=user or "",
225
+ source="plugin",
226
+ )
227
+ permission = dict(self.get_tool_permission(tool, args))
178
228
  result = dispatch_tool(self.hooks, tool, args, lambda: execute_tool(tool, args),
179
229
  source=f"plugin:{plugin_id}")
180
- return {"plugin": plugin_id, "tool": tool, "permission": permission,
230
+ return {"plugin": plugin_id, "tool": tool, "permission": permission, "policy": dict(policy),
181
231
  "executed": True, "result": result}
182
232
 
183
233
  def run_workflow(*, plugin_id, action, args, manifest):
@@ -206,8 +256,18 @@ class PlatformRuntime:
206
256
  def _agent_node_runner(self, user, scope):
207
257
  def runner(*, node, context):
208
258
  cfg = node.get("config") or {}
209
- goal = cfg.get("goal") or context.get("goal") or "Run agent"
210
- return self.run_agent(goal, user, scope, with_workflow=False, roles=cfg.get("roles"), inputs=context.get("inputs"))
259
+ goal = cfg.get("goal") or cfg.get("prompt") or context.get("goal") or "Run agent"
260
+ roles = cfg.get("roles")
261
+ if not roles and (cfg.get("mode") == "draft" or cfg.get("prompt")):
262
+ roles = ["researcher", "planner", "executor", "reviewer"]
263
+ return self.run_agent(
264
+ goal,
265
+ user,
266
+ scope,
267
+ with_workflow=False,
268
+ roles=roles,
269
+ inputs=context.get("inputs"),
270
+ )
211
271
  return runner
212
272
 
213
273
  # ── cross-system runs ─────────────────────────────────────────────────
@@ -218,7 +278,7 @@ class PlatformRuntime:
218
278
  except FileNotFoundError:
219
279
  return {"error": f"workflow not found: {workflow_id}"}
220
280
  runners = {
221
- "tool": self._tool_node_runner(),
281
+ "tool": self._tool_node_runner(user, scope),
222
282
  "skill": self._skill_node_runner(),
223
283
  "plugin": self._plugin_node_runner(user, scope),
224
284
  }
@@ -236,12 +296,8 @@ class PlatformRuntime:
236
296
  return {"workflow_run_id": run["id"], "status": result.status}
237
297
 
238
298
  def run_agent(self, goal, user, scope, *, with_workflow: bool, roles=None, inputs=None) -> Dict[str, Any]:
239
- role_runner = default_role_runner(
240
- workflow_runner=(lambda wf_ref, ctx: self.run_workflow_by_id(wf_ref, user, scope, with_agent=False, inputs=ctx.inputs)) if with_workflow else None,
241
- plugin_runner=lambda pid, ctx: self.registry.execute_action(pid, "run_skill", {}, runners=self.plugin_capability_runners(user, scope), workspace_id=scope).as_dict(),
242
- context_provider=self._context_provider(user, scope),
243
- )
244
- result = MultiAgentOrchestrator(role_runner=role_runner).run(
299
+ orchestrator = self.build_orchestrator(user, scope, with_workflow=with_workflow)
300
+ result = orchestrator.run(
245
301
  goal, user_email=user, workspace_id=scope, roles=roles, inputs=inputs or {}
246
302
  )
247
303
  run = self.store.record_agent_run(
@@ -259,14 +315,18 @@ class PlatformRuntime:
259
315
 
260
316
  def build_workflow_runners(self, user, scope) -> Dict[str, Callable[..., Any]]:
261
317
  return {
262
- "tool": self._tool_node_runner(),
318
+ "tool": self._tool_node_runner(user, scope),
263
319
  "skill": self._skill_node_runner(),
264
320
  "plugin": self._plugin_node_runner(user, scope),
265
321
  "agent": self._agent_node_runner(user, scope),
266
322
  }
267
323
 
268
- def build_orchestrator(self, user, scope) -> MultiAgentOrchestrator:
269
- workflow_runner = lambda wf_ref, ctx: self.run_workflow_by_id(wf_ref, user, scope, with_agent=False, inputs=ctx.inputs) # noqa: E731
324
+ def build_orchestrator(self, user, scope, *, with_workflow: bool = True) -> MultiAgentOrchestrator:
325
+ workflow_runner = (
326
+ (lambda wf_ref, ctx: self.run_workflow_by_id(wf_ref, user, scope, with_agent=False, inputs=ctx.inputs))
327
+ if with_workflow
328
+ else None
329
+ )
270
330
  plugin_runner = lambda pid, ctx: self.registry.execute_action(pid, "run_skill", {}, runners=self.plugin_capability_runners(user, scope), workspace_id=scope).as_dict() # noqa: E731
271
331
  context_provider = self._context_provider(user, scope)
272
332
  custom_agents = {}
@@ -1,4 +1,4 @@
1
- """Machine-checkable *product* readiness gates for the 8.4 line.
1
+ """Machine-checkable product readiness gates for the current release.
2
2
 
3
3
  Where ``architecture_readiness`` proves the internal structure is sound, this
4
4
  module answers the product question the 8.4 release exists to settle: *does the
@@ -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.0.0"
21
+ PRODUCT_VERSION_TARGET = "9.2.0"
22
22
 
23
23
 
24
24
  @dataclass(frozen=True)
@@ -37,11 +37,12 @@ PRODUCT_GATES: List[ProductGate] = [
37
37
  evidence=[
38
38
  "docs/ONBOARDING.md::five-minute",
39
39
  "frontend/src/components/ProductFlow.tsx::WakeBrainScreen",
40
- "frontend/src/features/brain/BrainConversation.tsx::ProductCommandCenter",
40
+ "frontend/src/features/brain/BrainConversation.tsx::brain-home-insights",
41
41
  "frontend/src/features/brain/BrainConversation.tsx::BrainBriefPanel",
42
42
  "frontend/src/features/brain/BrainHome.tsx",
43
- "auto_setup.py",
44
- "setup_wizard.py",
43
+ "frontend/src/App.tsx::brain-mobile-nav",
44
+ "latticeai/setup/auto_setup.py",
45
+ "latticeai/setup/wizard.py",
45
46
  ],
46
47
  ),
47
48
  ProductGate(
@@ -57,7 +58,8 @@ PRODUCT_GATES: List[ProductGate] = [
57
58
  title="File action requests create artifacts instead of code-only answers",
58
59
  evidence=[
59
60
  "latticeai/api/chat.py::is_file_action_request",
60
- "latticeai/api/chat.py::direct_write_file",
61
+ "latticeai/api/chat_intents.py::direct_file_action",
62
+ "latticeai/api/chat_intents.py::direct_write_file",
61
63
  "tests/unit/test_chat_telegram_decoupling.py::test_chat_file_creation_intent_writes_real_file",
62
64
  ],
63
65
  ),
@@ -76,10 +78,10 @@ PRODUCT_GATES: List[ProductGate] = [
76
78
  evidence=[
77
79
  "package.json::release:artifacts",
78
80
  "package.json::release:validate",
79
- "README.md::dist/ltcai-9.0.0-py3-none-any.whl",
80
- "README.md::dist/ltcai-9.0.0.tar.gz",
81
- "README.md::dist/ltcai-9.0.0.vsix",
82
- "README.md::ltcai-9.0.0.tgz",
81
+ f"README.md::dist/ltcai-{PRODUCT_VERSION_TARGET}-py3-none-any.whl",
82
+ f"README.md::dist/ltcai-{PRODUCT_VERSION_TARGET}.tar.gz",
83
+ f"README.md::dist/ltcai-{PRODUCT_VERSION_TARGET}.vsix",
84
+ f"README.md::ltcai-{PRODUCT_VERSION_TARGET}.tgz",
83
85
  "scripts/validate_release_artifacts.py",
84
86
  "scripts/release_smoke.py",
85
87
  "Dockerfile",
@@ -95,14 +97,14 @@ PRODUCT_GATES: List[ProductGate] = [
95
97
  title="Release story is documented and honest",
96
98
  evidence=[
97
99
  "README.md",
98
- "README.md::The current release is **9.0.0",
99
- "SECURITY.md::9.0.x (latest)",
100
- "vscode-extension/README.md::**9.0.0",
101
- "docs/CHANGELOG.md::## [9.0.0]",
100
+ f"README.md::The current release is **{PRODUCT_VERSION_TARGET}",
101
+ f"SECURITY.md::{'.'.join(PRODUCT_VERSION_TARGET.split('.')[:2])}.x (latest)",
102
+ f"vscode-extension/README.md::**{PRODUCT_VERSION_TARGET}",
103
+ f"docs/CHANGELOG.md::## [{PRODUCT_VERSION_TARGET}]",
102
104
  "FEATURE_STATUS.md",
103
- "RELEASE_NOTES_v9.0.0.md",
105
+ f"RELEASE_NOTES_v{PRODUCT_VERSION_TARGET}.md",
104
106
  "latticeai/core/agent.py::SingleAgentRuntime",
105
- "latticeai/core/agent.py::AgentRuntime = SingleAgentRuntime",
107
+ "lattice_brain/runtime/agent_runtime.py::class AgentRuntime",
106
108
  "lattice_brain/runtime/contracts.py::runtime-boundary/v1",
107
109
  "lattice_brain/runtime/contracts.py::RuntimeBoundaryProtocol",
108
110
  "lattice_brain/runtime/agent_runtime.py::def boundary",
@@ -115,7 +117,7 @@ PRODUCT_GATES: List[ProductGate] = [
115
117
  id="ecosystem-path",
116
118
  title="Community and plugin growth path is explicit",
117
119
  evidence=[
118
- "docs/COMMUNITY_AND_PLUGINS.md::9.0.0",
120
+ f"docs/COMMUNITY_AND_PLUGINS.md::{PRODUCT_VERSION_TARGET}",
119
121
  "docs/PLUGIN_SDK.md",
120
122
  "plugins/README.md",
121
123
  "plugins/hello-world/plugin.json",
@@ -49,6 +49,14 @@ def review_queue_opted_in(workflow: Dict[str, Any]) -> bool:
49
49
  continue
50
50
  if (node.get("config") or {}).get("review_queue") is True:
51
51
  return True
52
+ metadata = workflow.get("metadata") or {}
53
+ # Compatibility for recipe drafts installed before review_queue became an
54
+ # explicit trigger flag. They are local-only and already consent-gated.
55
+ if (
56
+ metadata.get("created_from") == "brain_automation_recipe"
57
+ and metadata.get("external_actions") is False
58
+ ):
59
+ return True
52
60
  return False
53
61
 
54
62
 
@@ -307,6 +315,10 @@ def _extract_run_id(result: Any) -> Optional[str]:
307
315
  if isinstance(result, dict):
308
316
  if result.get("run_id"):
309
317
  return str(result["run_id"])
318
+ if result.get("workflow_run_id"):
319
+ return str(result["workflow_run_id"])
320
+ if result.get("agent_run_id"):
321
+ return str(result["agent_run_id"])
310
322
  run = result.get("run")
311
323
  if isinstance(run, dict) and run.get("id"):
312
324
  return str(run["id"])
@@ -34,6 +34,7 @@ class ToolRouterContext:
34
34
  install_mcp: Any
35
35
  mcp_public_item: Any
36
36
  hooks: Any = None
37
+ workspace_service: Any = None
37
38
  # Resolves a caller email to their allowed workspace set (None = no scoping,
38
39
  # i.e. single-user / no-auth mode). Threaded to the knowledge-graph router so
39
40
  # its read endpoints enforce the same workspace boundary as /api/search.
@@ -10,18 +10,13 @@ from __future__ import annotations
10
10
 
11
11
  import asyncio
12
12
  from dataclasses import dataclass
13
- from datetime import datetime
14
13
  from typing import Any, Callable, Dict, Optional
15
14
 
15
+ from lattice_brain.runtime.statuses import (
16
+ RUN_ACTIVE_STATUSES as ACTIVE_STATUSES,
17
+ )
16
18
  from lattice_brain.workflow import WorkflowEngine
17
-
18
-
19
- ACTIVE_STATUSES = {"queued", "running", "in_progress", "retrying", "cancelling"}
20
- TERMINAL_STATUSES = {"ok", "retried_ok", "failed", "rejected", "cancelled", "interrupted", "partial"}
21
-
22
-
23
- def _now() -> str:
24
- return datetime.now().isoformat(timespec="seconds")
19
+ from latticeai.core.timeutil import now_iso as _now
25
20
 
26
21
 
27
22
  @dataclass