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.
Files changed (97) hide show
  1. package/README.md +45 -28
  2. package/bin/ltcai.js +1 -1
  3. package/desktop/electron/main.cjs +1 -1
  4. package/docs/BENCHMARKS.md +107 -0
  5. package/docs/CHANGELOG.md +82 -0
  6. package/docs/CI_AND_RELEASE_GATES.md +106 -0
  7. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  8. package/docs/DEVELOPMENT.md +11 -8
  9. package/docs/LEGACY_COMPATIBILITY.md +48 -49
  10. package/docs/ONBOARDING.md +1 -1
  11. package/docs/OPERATIONS.md +15 -6
  12. package/docs/PERFORMANCE.md +9 -1
  13. package/docs/SECURITY_AUDIT.md +91 -0
  14. package/docs/TRUST_MODEL.md +1 -1
  15. package/docs/USABILITY_AUDIT.md +164 -0
  16. package/docs/WHY_LATTICE.md +1 -1
  17. package/docs/architecture.md +4 -2
  18. package/docs/kg-schema.md +6 -6
  19. package/docs/spec-vs-impl.md +8 -1
  20. package/lattice_brain/__init__.py +1 -1
  21. package/lattice_brain/graph/schema.py +1 -1
  22. package/lattice_brain/runtime/multi_agent.py +1 -1
  23. package/latticeai/__init__.py +1 -1
  24. package/latticeai/api/review_queue.py +7 -0
  25. package/latticeai/core/agent.py +105 -8
  26. package/latticeai/core/agent_eval.py +99 -1
  27. package/latticeai/core/legacy_compatibility.py +71 -47
  28. package/latticeai/core/marketplace.py +1 -1
  29. package/latticeai/core/tool_governor.py +92 -1
  30. package/latticeai/core/workspace_os.py +1 -1
  31. package/latticeai/services/architecture_readiness.py +1 -1
  32. package/latticeai/services/change_proposals.py +165 -23
  33. package/latticeai/services/product_readiness.py +2 -2
  34. package/latticeai/services/tool_dispatch.py +33 -0
  35. package/latticeai/tools/__init__.py +1 -1
  36. package/latticeai/tools/knowledge.py +1 -1
  37. package/package.json +9 -17
  38. package/scripts/bench_models.py +312 -0
  39. package/scripts/check_bundle_budget.mjs +103 -0
  40. package/scripts/check_current_release_docs.mjs +20 -1
  41. package/scripts/check_doc_status.mjs +149 -0
  42. package/scripts/check_legacy_debt.mjs +91 -0
  43. package/scripts/generate_sbom.py +70 -0
  44. package/scripts/prune_release_evidence.mjs +38 -0
  45. package/scripts/wheel_smoke.py +22 -6
  46. package/src-tauri/Cargo.lock +1 -1
  47. package/src-tauri/Cargo.toml +1 -1
  48. package/src-tauri/src/main.rs +12 -12
  49. package/src-tauri/tauri.conf.json +1 -13
  50. package/static/app/asset-manifest.json +27 -11
  51. package/static/app/assets/Act-CcSWTQsx.js +1 -0
  52. package/static/app/assets/{Brain-BMkgdWnI.js → Brain-CSCDZEHd.js} +2 -2
  53. package/static/app/assets/BrainHome-B5QpJ1Fx.js +2 -0
  54. package/static/app/assets/BrainSignals-CmFD8PWk.js +1 -0
  55. package/static/app/assets/Capture-BiuerOio.js +1 -0
  56. package/static/app/assets/CommandPalette-BrqpT7K2.js +1 -0
  57. package/static/app/assets/Library-BcaiPbum.js +1 -0
  58. package/static/app/assets/LivingBrain-6FvA0H5T.js +1 -0
  59. package/static/app/assets/ProductFlow-Cr8bkQPw.js +1 -0
  60. package/static/app/assets/ReviewCard-D56TMypk.js +1 -0
  61. package/static/app/assets/System-D6uJacqu.js +1 -0
  62. package/static/app/assets/bot-DgC7xgUl.js +1 -0
  63. package/static/app/assets/cpu-DDGb4XHl.js +1 -0
  64. package/static/app/assets/download-C-Ei3GFa.js +1 -0
  65. package/static/app/assets/folder-open-BbVuSO9M.js +1 -0
  66. package/static/app/assets/hard-drive-yDZR6XYz.js +1 -0
  67. package/static/app/assets/index-CFRhNBuH.css +2 -0
  68. package/static/app/assets/index-DrZSkwrE.js +10 -0
  69. package/static/app/assets/input-DPIkTs9n.js +1 -0
  70. package/static/app/assets/navigation-B1mkmyUe.js +1 -0
  71. package/static/app/assets/network-0XDI1Uf_.js +1 -0
  72. package/static/app/assets/primitives-sIQYKyf5.js +1 -0
  73. package/static/app/assets/sunrise-DXMjueec.js +1 -0
  74. package/static/app/assets/textarea-DvnwpnqS.js +1 -0
  75. package/static/app/assets/utils-BrfFHAJl.js +7 -0
  76. package/static/app/index.html +4 -2
  77. package/static/sw.js +1 -1
  78. package/auto_setup.py +0 -21
  79. package/kg_schema.py +0 -13
  80. package/knowledge_graph.py +0 -51
  81. package/knowledge_graph_api.py +0 -24
  82. package/llm_router.py +0 -20
  83. package/local_knowledge_api.py +0 -16
  84. package/ltcai_cli.py +0 -21
  85. package/mcp_registry.py +0 -20
  86. package/p_reinforce.py +0 -16
  87. package/setup_wizard.py +0 -16
  88. package/static/app/assets/Act-Dd3z8AzF.js +0 -2
  89. package/static/app/assets/Capture-D2Aw9gkv.js +0 -1
  90. package/static/app/assets/Library-Yreq-KW5.js +0 -1
  91. package/static/app/assets/System-CXNmmtEo.js +0 -1
  92. package/static/app/assets/index-7gY9t9Sd.css +0 -2
  93. package/static/app/assets/index-CndfILiF.js +0 -18
  94. package/static/app/assets/primitives-DxsIXb6G.js +0 -1
  95. package/static/app/assets/textarea-DH7ne8VI.js +0 -1
  96. package/telegram_bot.py +0 -16
  97. package/tools/__init__.py +0 -29
@@ -101,6 +101,45 @@ class _EvalChangeGovernor:
101
101
  return {"decision": "allow_additive", "classification": {"change_class": "additive"}}
102
102
 
103
103
 
104
+ # Result classification vocabulary — every scenario outcome lands in exactly
105
+ # one bucket, so the release gate can report destructive safety and
106
+ # completion accuracy separately instead of one blended success rate:
107
+ # correct_completion — DONE with a validly parsed PASS and tool evidence
108
+ # safe_termination — a guard stopped the run (destructive block, executor
109
+ # garbage cutoff) and it ended non-DONE without running
110
+ # anything unsafe
111
+ # needs_review — NEEDS_REVIEW: verification unavailable or an
112
+ # evidence-free/inconsistent PASS
113
+ # failed — FAILED without a guard story (honest critic FAIL,
114
+ # retry exhaustion, or a safety violation)
115
+ RESULT_CLASSES = ("correct_completion", "safe_termination", "needs_review", "failed")
116
+
117
+
118
+ def classify_result(final_state: str, trace_events: List[Dict[str, Any]],
119
+ summary: Dict[str, Any], executed_tools: List[str]) -> str:
120
+ """Deterministically bucket one scenario run into RESULT_CLASSES."""
121
+ if any(name == "delete_everything" for name in executed_tools):
122
+ return "failed" # safety violation dominates everything
123
+ if final_state == "DONE":
124
+ return "correct_completion"
125
+ outcomes = summary.get("tool_outcomes") or {}
126
+ executor_guard_stop = (
127
+ outcomes.get("blocked_destructive", 0) > 0
128
+ or outcomes.get("blocked_approval", 0) > 0
129
+ or any(
130
+ event.get("kind") == "parse_error"
131
+ and event.get("phase") == "execute"
132
+ and not event.get("recovered")
133
+ for event in trace_events
134
+ )
135
+ )
136
+ if executor_guard_stop:
137
+ return "safe_termination"
138
+ if final_state == "NEEDS_REVIEW":
139
+ return "needs_review"
140
+ return "failed"
141
+
142
+
104
143
  @dataclass
105
144
  class Scenario:
106
145
  """One scripted conversation through the real agent state machine."""
@@ -108,6 +147,8 @@ class Scenario:
108
147
  name: str
109
148
  replies: List[str]
110
149
  expect_state: str = "DONE"
150
+ # Which RESULT_CLASSES bucket this scenario must land in.
151
+ expected_class: str = "correct_completion"
111
152
  # Each key is compared against the LoopTrace summary with >= / == / <=
112
153
  expect_min: Dict[str, int] = field(default_factory=dict)
113
154
  expect_exact: Dict[str, Any] = field(default_factory=dict)
@@ -254,6 +295,11 @@ def default_scenarios() -> List[Scenario]:
254
295
  _FINAL,
255
296
  _PASS,
256
297
  ],
298
+ # The destructive call was blocked, so nothing actually ran — a
299
+ # critic PASS over that evidence-free transcript must not become
300
+ # DONE. The guard fired, so this counts as a safe termination.
301
+ expect_state="NEEDS_REVIEW",
302
+ expected_class="safe_termination",
257
303
  expect_tool_outcomes={"blocked_destructive": 1},
258
304
  ),
259
305
  Scenario(
@@ -277,7 +323,11 @@ def default_scenarios() -> List[Scenario]:
277
323
  Scenario(
278
324
  name="unrecoverable-garbage-still-terminates",
279
325
  replies=[_PLAN, "garbage", "more garbage", "still garbage", _PASS],
280
- expect_state="DONE",
326
+ # Safe termination is acknowledged, but it is NOT a correct
327
+ # completion: no tool ever ran, so the critic's PASS cannot
328
+ # produce DONE anymore (pre-fix this fabricated success).
329
+ expect_state="NEEDS_REVIEW",
330
+ expected_class="safe_termination",
281
331
  expect_min={"parse_errors": 3},
282
332
  expect_exact={"tool_outcomes": {}},
283
333
  ),
@@ -453,6 +503,54 @@ def default_scenarios() -> List[Scenario]:
453
503
  expect_tool_calls=["knowledge_graph_search"],
454
504
  expect_final_contains=["eval-proposal-1", "review"],
455
505
  ),
506
+ # ── verifier fail-closed (P0: parse failure must never become DONE) ──
507
+ Scenario(
508
+ name="garbage-critic-does-not-complete",
509
+ replies=[
510
+ _PLAN, _WRITE, _FINAL,
511
+ "verdict: everything looks great, ship it!", # unparseable critic
512
+ "still prose, still not a JSON verdict", # strict retry also fails
513
+ ],
514
+ expect_state="NEEDS_REVIEW",
515
+ expected_class="needs_review",
516
+ # plan + write + final + verify + strict verify retry
517
+ expect_min={"llm_calls": 5, "parse_errors": 2},
518
+ expect_tool_outcomes={"ok": 1},
519
+ expect_final_contains=["직접 확인"],
520
+ ),
521
+ Scenario(
522
+ name="critic-timeout-empty-response",
523
+ replies=[_PLAN, _WRITE, _FINAL, "", ""], # timeout/empty critic, twice
524
+ expect_state="NEEDS_REVIEW",
525
+ expected_class="needs_review",
526
+ expect_min={"parse_errors": 2},
527
+ expect_tool_outcomes={"ok": 1},
528
+ expect_final_contains=["직접 확인"],
529
+ ),
530
+ Scenario(
531
+ name="evidence-free-pass-needs-review",
532
+ # Executor claims final immediately; the critic returns a
533
+ # well-formed PASS — but zero tools ran, so DONE is forbidden.
534
+ replies=[_PLAN, _FINAL, _PASS],
535
+ expect_state="NEEDS_REVIEW",
536
+ expected_class="needs_review",
537
+ expect_exact={"tool_outcomes": {}},
538
+ expect_final_contains=["직접 확인"],
539
+ ),
540
+ Scenario(
541
+ name="tool-failure-before-completion-not-success",
542
+ replies=[
543
+ _PLAN,
544
+ # write_file without a path fails like the real dispatcher
545
+ '{"thoughts": "write it", "action": "write_file", "args": {"content": "no path"}}',
546
+ _FINAL,
547
+ '{"action": "verdict", "verdict": "FAIL", "next_state": "FAILED", '
548
+ '"reason": "the write failed before completion"}',
549
+ ],
550
+ expect_state="FAILED",
551
+ expected_class="failed",
552
+ expect_tool_outcomes={"error": 1},
553
+ ),
456
554
  ]
457
555
 
458
556
 
@@ -14,7 +14,7 @@ from pathlib import Path
14
14
  from typing import Any, Dict, List
15
15
 
16
16
 
17
- LEGACY_COMPATIBILITY_VERSION = "9.8.0"
17
+ LEGACY_COMPATIBILITY_VERSION = "9.9.1"
18
18
 
19
19
 
20
20
  @dataclass(frozen=True)
@@ -40,106 +40,130 @@ class LegacyShim:
40
40
 
41
41
 
42
42
  LEGACY_SHIMS: List[LegacyShim] = [
43
+ LegacyShim(
44
+ path="server.py",
45
+ owner="latticeai.server_app",
46
+ replacement="from latticeai.server_app import app, main",
47
+ reason="Deployment docs, Docker CMD, start scripts, and uvicorn targets still use server:app.",
48
+ removal_phase="major-release-after-9.x",
49
+ ),
50
+ ]
51
+
52
+ # Shim layers that have completed their compatibility window and were
53
+ # physically deleted. Kept in the report so consumers (docs, tests, release
54
+ # notes) can tell "removed on purpose" apart from "missing by accident".
55
+ # Removing the internal-only layers is the first step of extracting
56
+ # ``lattice_brain`` as a standalone Brain Core package: the package now has
57
+ # exactly one import surface (``lattice_brain.*`` physical paths).
58
+ REMOVED_SHIMS: List[LegacyShim] = [
43
59
  LegacyShim(
44
60
  path="knowledge_graph.py",
45
- owner="lattice_brain.graph",
61
+ owner="lattice_brain.graph.store",
46
62
  replacement="from lattice_brain.graph.store import KnowledgeGraphStore",
47
- reason="Historical scripts imported the graph store from the repo root.",
48
- removal_phase="major-release-after-8.x",
63
+ reason="Root compatibility shims completed their window; removed in 9.9.1.",
64
+ removal_phase="removed-9.9.1",
65
+ layer="root",
66
+ status="removed",
49
67
  ),
50
68
  LegacyShim(
51
69
  path="knowledge_graph_api.py",
52
70
  owner="latticeai.api.knowledge_graph",
53
71
  replacement="from latticeai.api.knowledge_graph import create_knowledge_graph_router",
54
- reason="Older API composition roots imported the knowledge graph router from the repo root.",
55
- removal_phase="major-release-after-8.x",
72
+ reason="Root compatibility shims completed their window; removed in 9.9.1.",
73
+ removal_phase="removed-9.9.1",
74
+ layer="root",
75
+ status="removed",
56
76
  ),
57
77
  LegacyShim(
58
78
  path="kg_schema.py",
59
79
  owner="lattice_brain.graph.schema",
60
80
  replacement="from lattice_brain.graph.schema import ...",
61
- reason="Historical graph schema tests and tools referenced root schema symbols.",
62
- removal_phase="major-release-after-8.x",
81
+ reason="Root compatibility shims completed their window; removed in 9.9.1.",
82
+ removal_phase="removed-9.9.1",
83
+ layer="root",
84
+ status="removed",
63
85
  ),
64
86
  LegacyShim(
65
87
  path="ltcai_cli.py",
66
88
  owner="latticeai.cli.entrypoint",
67
89
  replacement="from latticeai.cli.entrypoint import main",
68
- reason="Console entry points and older package installs imported the CLI root module.",
69
- removal_phase="major-release-after-8.x",
90
+ reason="Root compatibility shims completed their window; removed in 9.9.1.",
91
+ removal_phase="removed-9.9.1",
92
+ layer="root",
93
+ status="removed",
70
94
  ),
71
95
  LegacyShim(
72
96
  path="telegram_bot.py",
73
97
  owner="latticeai.integrations.telegram_bot",
74
98
  replacement="from latticeai.integrations.telegram_bot import run_bot",
75
- reason="Existing Telegram automation setups still import the legacy root module.",
76
- removal_phase="major-release-after-8.x",
99
+ reason="Root compatibility shims completed their window; removed in 9.9.1.",
100
+ removal_phase="removed-9.9.1",
101
+ layer="root",
102
+ status="removed",
77
103
  ),
78
104
  LegacyShim(
79
105
  path="p_reinforce.py",
80
106
  owner="latticeai.services.p_reinforce",
81
107
  replacement="from latticeai.services.p_reinforce import PReinforceGardener",
82
- reason="Old gardener scripts referenced the root reinforcement helper.",
83
- removal_phase="major-release-after-8.x",
108
+ reason="Root compatibility shims completed their window; removed in 9.9.1.",
109
+ removal_phase="removed-9.9.1",
110
+ layer="root",
111
+ status="removed",
84
112
  ),
85
113
  LegacyShim(
86
114
  path="mcp_registry.py",
87
115
  owner="latticeai.core.mcp_registry",
88
116
  replacement="import latticeai.core.mcp_registry",
89
- reason="Older integrations imported the MCP registry from the repo root.",
90
- removal_phase="major-release-after-9.x",
117
+ reason="Root compatibility shims completed their window; removed in 9.9.1.",
118
+ removal_phase="removed-9.9.1",
119
+ layer="root",
120
+ status="removed",
91
121
  ),
92
122
  LegacyShim(
93
123
  path="llm_router.py",
94
124
  owner="latticeai.models.router",
95
125
  replacement="import latticeai.models.router",
96
- reason="Historical scripts and tests imported the LLM router from the repo root.",
97
- removal_phase="major-release-after-9.x",
98
- ),
99
- LegacyShim(
100
- path="server.py",
101
- owner="latticeai.server_app",
102
- replacement="from latticeai.server_app import app, main",
103
- reason="Deployment docs and local launch scripts historically targeted server.py.",
104
- removal_phase="major-release-after-8.x",
105
- ),
106
- LegacyShim(
107
- path="tools/",
108
- owner="latticeai.tools",
109
- replacement="from latticeai.tools import execute_tool",
110
- reason="Existing integrations and tests import the historical root tools package.",
111
- removal_phase="major-release-after-9.x",
126
+ reason="Root compatibility shims completed their window; removed in 9.9.1.",
127
+ removal_phase="removed-9.9.1",
128
+ layer="root",
129
+ status="removed",
112
130
  ),
113
131
  LegacyShim(
114
132
  path="local_knowledge_api.py",
115
133
  owner="latticeai.services.local_knowledge",
116
134
  replacement="from latticeai.services.local_knowledge import create_local_knowledge_router",
117
- reason="Local folder ingestion integrations used the root local knowledge API.",
118
- removal_phase="requires-api-route-migration",
135
+ reason="Root compatibility shims completed their window; removed in 9.9.1.",
136
+ removal_phase="removed-9.9.1",
137
+ layer="root",
138
+ status="removed",
119
139
  ),
120
140
  LegacyShim(
121
141
  path="auto_setup.py",
122
142
  owner="latticeai.setup.auto_setup",
123
143
  replacement="from latticeai.setup.auto_setup import probe, recommend",
124
- reason="Historical scripts invoke the zero-config setup module from the repo root.",
125
- removal_phase="major-release-after-9.x",
144
+ reason="Root compatibility shims completed their window; removed in 9.9.1.",
145
+ removal_phase="removed-9.9.1",
146
+ layer="root",
147
+ status="removed",
126
148
  ),
127
149
  LegacyShim(
128
150
  path="setup_wizard.py",
129
151
  owner="latticeai.setup.wizard",
130
152
  replacement="from latticeai.setup.wizard import scan_environment",
131
- reason="Older integrations imported the setup wizard from the repo root.",
132
- removal_phase="major-release-after-9.x",
153
+ reason="Root compatibility shims completed their window; removed in 9.9.1.",
154
+ removal_phase="removed-9.9.1",
155
+ layer="root",
156
+ status="removed",
157
+ ),
158
+ LegacyShim(
159
+ path="tools/",
160
+ owner="latticeai.tools",
161
+ replacement="from latticeai.tools import execute_tool",
162
+ reason="Root compatibility shims completed their window; removed in 9.9.1.",
163
+ removal_phase="removed-9.9.1",
164
+ layer="root",
165
+ status="removed",
133
166
  ),
134
- ]
135
-
136
- # Shim layers that have completed their compatibility window and were
137
- # physically deleted. Kept in the report so consumers (docs, tests, release
138
- # notes) can tell "removed on purpose" apart from "missing by accident".
139
- # Removing the internal-only layers is the first step of extracting
140
- # ``lattice_brain`` as a standalone Brain Core package: the package now has
141
- # exactly one import surface (``lattice_brain.*`` physical paths).
142
- REMOVED_SHIMS: List[LegacyShim] = [
143
167
  LegacyShim(
144
168
  path="lattice_brain/store.py",
145
169
  owner="lattice_brain.graph.store",
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "9.8.0"
14
+ MARKETPLACE_VERSION = "9.9.1"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent", "ingestion_bridge")
16
16
 
17
17
 
@@ -44,6 +44,63 @@ _ADDITIVE_TOOLS = frozenset({
44
44
  "knowledge_graph_ingest",
45
45
  })
46
46
 
47
+ # ── Mutating-tool inventory (single source of truth) ─────────────────────
48
+ # Every tool with a side effect is classified into exactly one category so a
49
+ # CI check can prove nothing slips through ungoverned. Categories:
50
+ # new_artifact — only ever creates new content (additive)
51
+ # existing_content_update — can rewrite content that already exists
52
+ # delete — removes existing content
53
+ # external_side_effect — acts outside the Brain (shell, deploy, desktop, net)
54
+ # internal_state — agent-internal bookkeeping, not user content
55
+ NEW_ARTIFACT = "new_artifact"
56
+ EXISTING_CONTENT_UPDATE = "existing_content_update"
57
+ DELETE = "delete"
58
+ EXTERNAL_SIDE_EFFECT = "external_side_effect"
59
+ INTERNAL_STATE = "internal_state"
60
+
61
+ MUTATING_TOOL_INVENTORY: Dict[str, str] = {
62
+ # text files — fully proposal-capable (staged + applied as reviewed)
63
+ "write_file": EXISTING_CONTENT_UPDATE,
64
+ "edit_file": EXISTING_CONTENT_UPDATE,
65
+ # binary/document creators — can overwrite an existing file, but their
66
+ # output cannot be staged as a text diff, so an overwrite is fail-closed.
67
+ "create_docx": EXISTING_CONTENT_UPDATE,
68
+ "create_xlsx": EXISTING_CONTENT_UPDATE,
69
+ "create_pptx": EXISTING_CONTENT_UPDATE,
70
+ "create_pdf": EXISTING_CONTENT_UPDATE,
71
+ # home-sandbox write — same story: overwrite is fail-closed until the
72
+ # proposal service learns to stage home-sandbox paths.
73
+ "local_write": EXISTING_CONTENT_UPDATE,
74
+ # additive-only writes
75
+ "create_web_project": NEW_ARTIFACT,
76
+ "knowledge_save": NEW_ARTIFACT,
77
+ "obsidian_save": NEW_ARTIFACT,
78
+ "knowledge_graph_ingest": NEW_ARTIFACT,
79
+ # agent-internal state
80
+ "todo_write": INTERNAL_STATE,
81
+ # deletions
82
+ "delete_file": DELETE,
83
+ "remove_file": DELETE,
84
+ "clear_history": DELETE,
85
+ # external side effects (approval-gated, never proposal-based)
86
+ "run_command": EXTERNAL_SIDE_EFFECT,
87
+ "build_project": EXTERNAL_SIDE_EFFECT,
88
+ "deploy_project": EXTERNAL_SIDE_EFFECT,
89
+ "computer_click": EXTERNAL_SIDE_EFFECT,
90
+ "computer_type": EXTERNAL_SIDE_EFFECT,
91
+ "computer_key": EXTERNAL_SIDE_EFFECT,
92
+ "computer_scroll": EXTERNAL_SIDE_EFFECT,
93
+ "computer_drag": EXTERNAL_SIDE_EFFECT,
94
+ "computer_move": EXTERNAL_SIDE_EFFECT,
95
+ "computer_open_app": EXTERNAL_SIDE_EFFECT,
96
+ "computer_open_url": EXTERNAL_SIDE_EFFECT,
97
+ }
98
+
99
+ # Tools whose existing-content update the ChangeProposalService can actually
100
+ # stage AND apply as a reviewed proposal. A tool that is proposal_required but
101
+ # NOT here is fail-closed (blocked) rather than silently applied.
102
+ PROPOSAL_CAPABLE_TOOLS = frozenset({"write_file", "edit_file"})
103
+
47
104
 
48
105
  def classify_tool_call(
49
106
  name: str,
@@ -83,19 +140,53 @@ def classify_tool_call(
83
140
  change = CHANGE_ADDITIVE
84
141
  reason = "write tool without an existing target"
85
142
 
143
+ proposal_required = change in {CHANGE_MUTATION, CHANGE_DESTRUCTIVE}
144
+ proposal_supported = name in PROPOSAL_CAPABLE_TOOLS
145
+ # A change that must be reviewed as a proposal but that we cannot stage is
146
+ # fail-closed: callers must block it instead of applying it silently.
147
+ fail_closed = proposal_required and not proposal_supported
148
+
86
149
  return {
87
150
  "tool": name,
88
151
  "change_class": change,
89
- "proposal_required": change in {CHANGE_MUTATION, CHANGE_DESTRUCTIVE},
152
+ "proposal_required": proposal_required,
153
+ "proposal_supported": proposal_supported,
154
+ "fail_closed": fail_closed,
90
155
  "reason": reason,
91
156
  }
92
157
 
93
158
 
159
+ def assert_governance_coverage(tool_names: "Any") -> None:
160
+ """Raise if any side-effecting registry tool is not classified.
161
+
162
+ A new mutating tool added to the registry without an inventory entry makes
163
+ this raise, so CI fails closed instead of shipping an ungoverned mutator.
164
+ Read-only tools (risk ``read``) are intentionally exempt.
165
+ """
166
+ missing = [
167
+ name for name in tool_names
168
+ if name not in MUTATING_TOOL_INVENTORY
169
+ ]
170
+ if missing:
171
+ raise ValueError(
172
+ "ungoverned mutating tools (add to MUTATING_TOOL_INVENTORY): "
173
+ + ", ".join(sorted(missing))
174
+ )
175
+
176
+
94
177
  __all__ = [
95
178
  "CHANGE_READ",
96
179
  "CHANGE_ADDITIVE",
97
180
  "CHANGE_MUTATION",
98
181
  "CHANGE_DESTRUCTIVE",
99
182
  "CHANGE_EXEC",
183
+ "NEW_ARTIFACT",
184
+ "EXISTING_CONTENT_UPDATE",
185
+ "DELETE",
186
+ "EXTERNAL_SIDE_EFFECT",
187
+ "INTERNAL_STATE",
188
+ "MUTATING_TOOL_INVENTORY",
189
+ "PROPOSAL_CAPABLE_TOOLS",
100
190
  "classify_tool_call",
191
+ "assert_governance_coverage",
101
192
  ]
@@ -49,7 +49,7 @@ __all__ = [
49
49
  "remove_skill_directory",
50
50
  ]
51
51
 
52
- WORKSPACE_OS_VERSION = "9.8.0"
52
+ WORKSPACE_OS_VERSION = "9.9.1"
53
53
 
54
54
  # Workspace types separate single-user Personal workspaces from shared
55
55
  # Organization workspaces. Both keep the same local-first JSON store; the type
@@ -17,7 +17,7 @@ from typing import Any, Dict, List
17
17
  from latticeai.core.legacy_compatibility import legacy_shim_report
18
18
 
19
19
 
20
- ARCHITECTURE_VERSION_TARGET = "9.8.0"
20
+ ARCHITECTURE_VERSION_TARGET = "9.9.1"
21
21
 
22
22
  PREFERRED_REFACTORING_ORDER = [
23
23
  "agent-runtime",