velune-cli 0.9.0__py3-none-any.whl

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 (279) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +208 -0
  5. velune/cli/autocomplete.py +80 -0
  6. velune/cli/banner.py +60 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +175 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +228 -0
  11. velune/cli/commands/config.py +224 -0
  12. velune/cli/commands/daemon.py +88 -0
  13. velune/cli/commands/doctor.py +721 -0
  14. velune/cli/commands/init.py +170 -0
  15. velune/cli/commands/mcp.py +82 -0
  16. velune/cli/commands/memory.py +293 -0
  17. velune/cli/commands/models.py +683 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +270 -0
  20. velune/cli/commands/setup.py +184 -0
  21. velune/cli/commands/workspace.py +249 -0
  22. velune/cli/context.py +36 -0
  23. velune/cli/councilmodel_ui.py +199 -0
  24. velune/cli/display/council_view.py +254 -0
  25. velune/cli/display/memory_view.py +126 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +25 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +51 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +123 -0
  33. velune/cli/registry.py +80 -0
  34. velune/cli/rendering/__init__.py +5 -0
  35. velune/cli/rendering/error_panel.py +79 -0
  36. velune/cli/rendering/markdown.py +63 -0
  37. velune/cli/repl.py +1855 -0
  38. velune/cli/session_manager.py +71 -0
  39. velune/cli/slash_commands.py +37 -0
  40. velune/cli/theme.py +8 -0
  41. velune/cognition/__init__.py +23 -0
  42. velune/cognition/agents/__init__.py +7 -0
  43. velune/cognition/agents/coder.py +209 -0
  44. velune/cognition/agents/planner.py +156 -0
  45. velune/cognition/agents/reviewer.py +195 -0
  46. velune/cognition/arbitrator.py +220 -0
  47. velune/cognition/architecture.py +415 -0
  48. velune/cognition/budget.py +65 -0
  49. velune/cognition/council/__init__.py +47 -0
  50. velune/cognition/council/base.py +217 -0
  51. velune/cognition/council/challenger.py +74 -0
  52. velune/cognition/council/coder.py +79 -0
  53. velune/cognition/council/critic_agent.py +43 -0
  54. velune/cognition/council/critic_configs.py +111 -0
  55. velune/cognition/council/critics.py +41 -0
  56. velune/cognition/council/debate.py +46 -0
  57. velune/cognition/council/factory.py +140 -0
  58. velune/cognition/council/messages.py +56 -0
  59. velune/cognition/council/planner.py +124 -0
  60. velune/cognition/council/reviewer.py +74 -0
  61. velune/cognition/council/synthesizer.py +67 -0
  62. velune/cognition/council/tiers.py +188 -0
  63. velune/cognition/council_orchestrator.py +282 -0
  64. velune/cognition/firewall.py +354 -0
  65. velune/cognition/module.py +46 -0
  66. velune/cognition/orchestrator.py +1205 -0
  67. velune/cognition/personality.py +238 -0
  68. velune/cognition/state.py +104 -0
  69. velune/cognition/style_resolver.py +64 -0
  70. velune/cognition/verification.py +205 -0
  71. velune/context/__init__.py +28 -0
  72. velune/context/assembler.py +240 -0
  73. velune/context/budget.py +97 -0
  74. velune/context/extractive.py +95 -0
  75. velune/context/prompt_adaptation.py +480 -0
  76. velune/context/sections.py +99 -0
  77. velune/context/token_counter.py +134 -0
  78. velune/context/utilization.py +33 -0
  79. velune/context/window.py +63 -0
  80. velune/core/__init__.py +89 -0
  81. velune/core/background.py +5 -0
  82. velune/core/config/__init__.py +37 -0
  83. velune/core/errors/__init__.py +90 -0
  84. velune/core/errors/catalog.py +188 -0
  85. velune/core/errors/execution.py +31 -0
  86. velune/core/errors/memory.py +25 -0
  87. velune/core/errors/orchestration.py +31 -0
  88. velune/core/errors/provider.py +37 -0
  89. velune/core/event_loop.py +35 -0
  90. velune/core/logging.py +83 -0
  91. velune/core/paths.py +165 -0
  92. velune/core/runtime.py +113 -0
  93. velune/core/startup_profiler.py +56 -0
  94. velune/core/task_registry.py +117 -0
  95. velune/core/trace.py +83 -0
  96. velune/core/types/__init__.py +48 -0
  97. velune/core/types/agent.py +53 -0
  98. velune/core/types/context.py +42 -0
  99. velune/core/types/inference.py +38 -0
  100. velune/core/types/memory.py +42 -0
  101. velune/core/types/model.py +70 -0
  102. velune/core/types/provider.py +62 -0
  103. velune/core/types/repository.py +38 -0
  104. velune/core/types/task.py +61 -0
  105. velune/core/types/workspace.py +28 -0
  106. velune/daemon/client.py +13 -0
  107. velune/daemon/server.py +127 -0
  108. velune/daemon/transport.py +179 -0
  109. velune/events.py +204 -0
  110. velune/execution/__init__.py +22 -0
  111. velune/execution/benchmarker.py +315 -0
  112. velune/execution/cancellation.py +53 -0
  113. velune/execution/checkpointer.py +130 -0
  114. velune/execution/command_spec.py +165 -0
  115. velune/execution/diff_preview.py +197 -0
  116. velune/execution/executor.py +181 -0
  117. velune/execution/module.py +18 -0
  118. velune/execution/multi_diff.py +67 -0
  119. velune/execution/path_guard.py +74 -0
  120. velune/execution/planner.py +91 -0
  121. velune/execution/rollback.py +89 -0
  122. velune/execution/sandbox.py +268 -0
  123. velune/execution/validator.py +115 -0
  124. velune/hardware/__init__.py +1 -0
  125. velune/hardware/detector.py +192 -0
  126. velune/kernel/__init__.py +55 -0
  127. velune/kernel/bootstrap.py +125 -0
  128. velune/kernel/config.py +426 -0
  129. velune/kernel/entrypoint.py +78 -0
  130. velune/kernel/health.py +54 -0
  131. velune/kernel/lifecycle.py +143 -0
  132. velune/kernel/module.py +17 -0
  133. velune/kernel/modules.py +23 -0
  134. velune/kernel/registry.py +96 -0
  135. velune/kernel/schemas.py +28 -0
  136. velune/main.py +9 -0
  137. velune/mcp/__init__.py +9 -0
  138. velune/mcp/client.py +115 -0
  139. velune/mcp/config.py +19 -0
  140. velune/mcp/server.py +624 -0
  141. velune/memory/__init__.py +32 -0
  142. velune/memory/compaction.py +506 -0
  143. velune/memory/embedding_pipeline.py +241 -0
  144. velune/memory/lifecycle.py +680 -0
  145. velune/memory/module.py +218 -0
  146. velune/memory/prioritizer.py +67 -0
  147. velune/memory/storage/episodic_schema.sql +53 -0
  148. velune/memory/storage/lancedb_store.py +282 -0
  149. velune/memory/storage/sqlite_manager.py +369 -0
  150. velune/memory/storage/sqlite_pool.py +149 -0
  151. velune/memory/tiers/episodic.py +588 -0
  152. velune/memory/tiers/graph.py +378 -0
  153. velune/memory/tiers/lineage.py +416 -0
  154. velune/memory/tiers/semantic.py +475 -0
  155. velune/memory/tiers/working.py +168 -0
  156. velune/memory/vitality.py +132 -0
  157. velune/models/__init__.py +15 -0
  158. velune/models/family.py +76 -0
  159. velune/models/module.py +20 -0
  160. velune/models/probes.py +192 -0
  161. velune/models/profile_cache.py +84 -0
  162. velune/models/profiler.py +108 -0
  163. velune/models/registry.py +251 -0
  164. velune/models/scorer.py +233 -0
  165. velune/models/specializations.py +205 -0
  166. velune/orchestration/__init__.py +19 -0
  167. velune/orchestration/engine.py +239 -0
  168. velune/orchestration/module.py +15 -0
  169. velune/orchestration/role_assignments.py +82 -0
  170. velune/orchestration/schemas.py +98 -0
  171. velune/plugins/__init__.py +20 -0
  172. velune/plugins/hooks.py +50 -0
  173. velune/plugins/loader.py +161 -0
  174. velune/plugins/registry.py +56 -0
  175. velune/plugins/schemas.py +21 -0
  176. velune/providers/__init__.py +23 -0
  177. velune/providers/adapters/anthropic.py +257 -0
  178. velune/providers/adapters/fireworks.py +115 -0
  179. velune/providers/adapters/google.py +234 -0
  180. velune/providers/adapters/groq.py +151 -0
  181. velune/providers/adapters/huggingface.py +210 -0
  182. velune/providers/adapters/llamacpp.py +208 -0
  183. velune/providers/adapters/lmstudio.py +175 -0
  184. velune/providers/adapters/ollama.py +233 -0
  185. velune/providers/adapters/openai.py +213 -0
  186. velune/providers/adapters/openrouter.py +81 -0
  187. velune/providers/adapters/together.py +134 -0
  188. velune/providers/adapters/xai.py +60 -0
  189. velune/providers/base.py +86 -0
  190. velune/providers/benchmarker.py +138 -0
  191. velune/providers/discovery/__init__.py +33 -0
  192. velune/providers/discovery/anthropic.py +79 -0
  193. velune/providers/discovery/benchmarks.py +44 -0
  194. velune/providers/discovery/classifier.py +69 -0
  195. velune/providers/discovery/fireworks.py +95 -0
  196. velune/providers/discovery/gguf.py +88 -0
  197. velune/providers/discovery/google.py +95 -0
  198. velune/providers/discovery/gpu.py +117 -0
  199. velune/providers/discovery/groq.py +21 -0
  200. velune/providers/discovery/huggingface.py +67 -0
  201. velune/providers/discovery/lmstudio.py +80 -0
  202. velune/providers/discovery/ollama.py +162 -0
  203. velune/providers/discovery/openai.py +96 -0
  204. velune/providers/discovery/openrouter.py +113 -0
  205. velune/providers/discovery/scanner.py +115 -0
  206. velune/providers/discovery/together.py +114 -0
  207. velune/providers/discovery/xai.py +57 -0
  208. velune/providers/health.py +67 -0
  209. velune/providers/health_monitor.py +169 -0
  210. velune/providers/keystore.py +142 -0
  211. velune/providers/local_paths.py +49 -0
  212. velune/providers/local_resolver.py +229 -0
  213. velune/providers/module.py +51 -0
  214. velune/providers/ollama_manager.py +193 -0
  215. velune/providers/registry.py +220 -0
  216. velune/providers/router.py +255 -0
  217. velune/providers/task_classifier.py +288 -0
  218. velune/py.typed +0 -0
  219. velune/repository/__init__.py +33 -0
  220. velune/repository/analyzer.py +127 -0
  221. velune/repository/ast_parser.py +822 -0
  222. velune/repository/blast_radius.py +298 -0
  223. velune/repository/boundary_classifier.py +295 -0
  224. velune/repository/cognition.py +316 -0
  225. velune/repository/grapher.py +179 -0
  226. velune/repository/import_graph.py +263 -0
  227. velune/repository/incremental_indexer.py +275 -0
  228. velune/repository/index_state.py +96 -0
  229. velune/repository/indexer.py +243 -0
  230. velune/repository/module.py +17 -0
  231. velune/repository/parser.py +474 -0
  232. velune/repository/project_type.py +300 -0
  233. velune/repository/rename_journal.py +287 -0
  234. velune/repository/scanner.py +193 -0
  235. velune/repository/schemas.py +102 -0
  236. velune/repository/symbol_registry.py +365 -0
  237. velune/repository/tracker.py +252 -0
  238. velune/retrieval/__init__.py +27 -0
  239. velune/retrieval/cache.py +110 -0
  240. velune/retrieval/fast_path.py +391 -0
  241. velune/retrieval/graph.py +124 -0
  242. velune/retrieval/hybrid.py +271 -0
  243. velune/retrieval/keyword.py +131 -0
  244. velune/retrieval/module.py +26 -0
  245. velune/retrieval/pipeline.py +303 -0
  246. velune/retrieval/reranker.py +102 -0
  247. velune/retrieval/schemas.py +59 -0
  248. velune/retrieval/slow_path.py +364 -0
  249. velune/retrieval/vector.py +203 -0
  250. velune/telemetry/__init__.py +59 -0
  251. velune/telemetry/cognition.py +267 -0
  252. velune/telemetry/cost_estimator.py +92 -0
  253. velune/telemetry/debug.py +304 -0
  254. velune/telemetry/doctor.py +244 -0
  255. velune/telemetry/logging.py +286 -0
  256. velune/telemetry/spans.py +277 -0
  257. velune/telemetry/token_tracker.py +140 -0
  258. velune/telemetry/usage_tracker.py +340 -0
  259. velune/tools/__init__.py +41 -0
  260. velune/tools/base/registry.py +87 -0
  261. velune/tools/base/tool.py +63 -0
  262. velune/tools/code/navigate.py +116 -0
  263. velune/tools/code/search.py +123 -0
  264. velune/tools/filesystem/read.py +75 -0
  265. velune/tools/filesystem/search.py +136 -0
  266. velune/tools/filesystem/write.py +163 -0
  267. velune/tools/git/history.py +177 -0
  268. velune/tools/git/operations.py +122 -0
  269. velune/tools/git/state.py +121 -0
  270. velune/tools/module.py +81 -0
  271. velune/tools/terminal/execute.py +72 -0
  272. velune/tools/terminal/history.py +47 -0
  273. velune/tools/web/fetch.py +55 -0
  274. velune/tools/web/validator.py +122 -0
  275. velune_cli-0.9.0.dist-info/METADATA +518 -0
  276. velune_cli-0.9.0.dist-info/RECORD +279 -0
  277. velune_cli-0.9.0.dist-info/WHEEL +4 -0
  278. velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
  279. velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,415 @@
1
+ """Architecture Cognition Agent and Technical Debt Ledger."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import json
7
+ import logging
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ logger = logging.getLogger("velune.cognition.architecture")
13
+
14
+
15
+ class ArchitectureDriftAlarm(Exception):
16
+ """Exception raised when an architectural layering boundary rule is violated."""
17
+
18
+ pass
19
+
20
+
21
+ class LCOMVisitor(ast.NodeVisitor):
22
+ """AST visitor to calculate Lack of Cohesion of Methods (LCOM4/LCOM)."""
23
+
24
+ def __init__(self) -> None:
25
+ self.current_method: str | None = None
26
+ self.method_accessed_attributes: dict[str, set[str]] = {}
27
+ self.class_name: str | None = None
28
+
29
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
30
+ self.class_name = node.name
31
+ # Process the body of the class
32
+ for subnode in node.body:
33
+ if isinstance(subnode, ast.FunctionDef):
34
+ self.current_method = subnode.name
35
+ self.method_accessed_attributes[subnode.name] = set()
36
+ self.generic_visit(subnode)
37
+ self.current_method = None
38
+ # Do not recurse into nested classes to avoid confusion
39
+
40
+ def visit_Attribute(self, node: ast.Attribute) -> None:
41
+ # Check if attribute access is on 'self' (e.g. self.attr)
42
+ if self.current_method and isinstance(node.value, ast.Name) and node.value.id == "self":
43
+ self.method_accessed_attributes[self.current_method].add(node.attr)
44
+ self.generic_visit(node)
45
+
46
+
47
+ class CognitiveDebtLedger:
48
+ """Manages the persistent registry of technical debt, coupling, and modularity violations."""
49
+
50
+ def __init__(self, ledger_path: str | Path | None = None) -> None:
51
+ if ledger_path is None:
52
+ self.ledger_path = Path(".velune") / "debt_ledger.json"
53
+ else:
54
+ self.ledger_path = Path(ledger_path)
55
+
56
+ self.ledger_path.parent.mkdir(parents=True, exist_ok=True)
57
+ self._load_ledger()
58
+
59
+ def _load_ledger(self) -> None:
60
+ if self.ledger_path.exists():
61
+ try:
62
+ with open(self.ledger_path, encoding="utf-8") as f:
63
+ self.data = json.load(f)
64
+ except Exception:
65
+ self.data = {"debt_items": [], "total_severity": 0.0}
66
+ else:
67
+ self.data = {"debt_items": [], "total_severity": 0.0}
68
+
69
+ def _save_ledger(self) -> None:
70
+ try:
71
+ with open(self.ledger_path, "w", encoding="utf-8") as f:
72
+ json.dump(self.data, f, indent=2)
73
+ except Exception as e:
74
+ logger.error("Failed to write to technical debt ledger: %s", e)
75
+
76
+ def add_debt_item(
77
+ self, file_path: str, category: str, description: str, severity: float
78
+ ) -> None:
79
+ """Add or update an item in the debt ledger."""
80
+ # Check if duplicate exists
81
+ for item in self.data["debt_items"]:
82
+ if (
83
+ item["file_path"] == file_path
84
+ and item["category"] == category
85
+ and item["description"] == description
86
+ ):
87
+ item["severity"] = severity
88
+ item["updated_at"] = (
89
+ os.path.getmtime(str(self.ledger_path)) if self.ledger_path.exists() else 0.0
90
+ )
91
+ break
92
+ else:
93
+ self.data["debt_items"].append(
94
+ {
95
+ "file_path": file_path,
96
+ "category": category,
97
+ "description": description,
98
+ "severity": severity,
99
+ }
100
+ )
101
+
102
+ # Recompute total severity
103
+ self.data["total_severity"] = sum(item["severity"] for item in self.data["debt_items"])
104
+ self._save_ledger()
105
+
106
+ def clear_file_debt(self, file_path: str) -> None:
107
+ """Clear all debt items registered for a specific file."""
108
+ self.data["debt_items"] = [
109
+ item for item in self.data["debt_items"] if item["file_path"] != file_path
110
+ ]
111
+ self.data["total_severity"] = sum(item["severity"] for item in self.data["debt_items"])
112
+ self._save_ledger()
113
+
114
+ def get_items(self) -> list[dict[str, Any]]:
115
+ return self.data["debt_items"]
116
+
117
+
118
+ class ArchitectureCognitionAgent:
119
+ """
120
+ Analyzes repository structure, calculates cohesion metrics (LCOM),
121
+ verifies architectural boundary coupling, and records technical debt.
122
+ """
123
+
124
+ def __init__(
125
+ self, workspace_root: str | None = None, ledger: CognitiveDebtLedger | None = None
126
+ ) -> None:
127
+ self.workspace_root = workspace_root or os.getcwd()
128
+ self.ledger = ledger or CognitiveDebtLedger()
129
+
130
+ # Layering Rules: maps prefix -> list of disallowed module prefixes
131
+ self.layering_rules: list[tuple[str, list[str]]] = [
132
+ ("velune/core", ["velune/execution", "velune/cognition", "velune/cli"]),
133
+ ("velune/kernel", ["velune/execution", "velune/cognition", "velune/cli"]),
134
+ ("velune/models", ["velune/providers", "velune/orchestration", "velune/cli"]),
135
+ ]
136
+
137
+ def calculate_lcom(self, code: str) -> dict[str, int]:
138
+ """
139
+ Calculates Lack of Cohesion of Methods (LCOM) for each class in the code.
140
+ Returns a dictionary mapping class name to LCOM score.
141
+ """
142
+ scores: dict[str, int] = {}
143
+ try:
144
+ tree = ast.parse(code)
145
+ except SyntaxError:
146
+ return scores
147
+
148
+ for node in ast.walk(tree):
149
+ if isinstance(node, ast.ClassDef):
150
+ visitor = LCOMVisitor()
151
+ visitor.visit(node)
152
+
153
+ methods = list(visitor.method_accessed_attributes.keys())
154
+ # Exclude constructor __init__ from scoring if preferred, or keep all.
155
+ # Let's count LCOM based on standard formula.
156
+ p = 0
157
+ q = 0
158
+
159
+ n = len(methods)
160
+ if n <= 1:
161
+ scores[node.name] = 0
162
+ continue
163
+
164
+ for i in range(n):
165
+ for j in range(i + 1, n):
166
+ m1 = methods[i]
167
+ m2 = methods[j]
168
+ s1 = visitor.method_accessed_attributes[m1]
169
+ s2 = visitor.method_accessed_attributes[m2]
170
+ if s1.isdisjoint(s2):
171
+ p += 1
172
+ else:
173
+ q += 1
174
+
175
+ lcom = p - q if p > q else 0
176
+ scores[node.name] = lcom
177
+
178
+ return scores
179
+
180
+ def calculate_coupling_ratio(self, directory: str) -> float:
181
+ """
182
+ Calculates coupling ratio recursively over a directory's python files:
183
+ external_project_imports / (internal_project_imports + external_project_imports).
184
+ Project imports are identified by a 'velune' module prefix.
185
+ """
186
+ abs_dir = os.path.abspath(directory)
187
+ parts = Path(abs_dir).parts
188
+ if "velune" in parts:
189
+ idx = parts.index("velune")
190
+ rel_parts = parts[idx:]
191
+ prefix = ".".join(rel_parts)
192
+ else:
193
+ prefix = "velune." + os.path.basename(abs_dir)
194
+
195
+ internal_count = 0
196
+ external_count = 0
197
+
198
+ for root, _, files in os.walk(directory):
199
+ for file in files:
200
+ if file.endswith(".py"):
201
+ file_path = os.path.join(root, file)
202
+ try:
203
+ with open(file_path, encoding="utf-8", errors="ignore") as f:
204
+ content = f.read()
205
+ tree = ast.parse(content)
206
+ for node in ast.walk(tree):
207
+ imported_mods = []
208
+ if isinstance(node, ast.Import):
209
+ for name in node.names:
210
+ imported_mods.append(name.name)
211
+ elif isinstance(node, ast.ImportFrom) and node.module:
212
+ imported_mods.append(node.module)
213
+
214
+ for imported_mod in imported_mods:
215
+ if imported_mod == "velune" or imported_mod.startswith("velune."):
216
+ if imported_mod.startswith(prefix):
217
+ internal_count += 1
218
+ else:
219
+ external_count += 1
220
+ except Exception:
221
+ pass
222
+
223
+ total = internal_count + external_count
224
+ if total == 0:
225
+ return 0.0
226
+ return round(external_count / total, 3)
227
+
228
+ def calculate_shi(self, directory: str) -> float:
229
+ """
230
+ Calculates the Subsystem Health Index (SHI) for the given directory.
231
+ SHI(M) = max(0.0, min(1.0, 1.0 * Cohesion - 0.5 * Coupling - 0.2 * DebtPenalty))
232
+ """
233
+ # Calculate Average LCOM
234
+ lcom_scores = []
235
+ for root, _, files in os.walk(directory):
236
+ for file in files:
237
+ if file.endswith(".py"):
238
+ file_path = os.path.join(root, file)
239
+ try:
240
+ with open(file_path, encoding="utf-8", errors="ignore") as f:
241
+ content = f.read()
242
+ scores = self.calculate_lcom(content)
243
+ lcom_scores.extend(scores.values())
244
+ except Exception:
245
+ pass
246
+
247
+ if lcom_scores:
248
+ avg_lcom = sum(lcom_scores) / len(lcom_scores)
249
+ cohesion = 1.0 - min(avg_lcom / 10.0, 1.0)
250
+ else:
251
+ cohesion = 1.0
252
+
253
+ coupling = self.calculate_coupling_ratio(directory)
254
+
255
+ # Calculate Debt Penalty
256
+ norm_dir = os.path.abspath(directory)
257
+ sum_severity = 0.0
258
+ for item in self.ledger.get_items():
259
+ item_path = os.path.abspath(item["file_path"])
260
+ if item_path.startswith(norm_dir):
261
+ sum_severity += item.get("severity", 0.0)
262
+
263
+ debt_penalty = min(sum_severity * 0.1, 1.0)
264
+
265
+ shi = 1.0 * cohesion - 0.5 * coupling - 0.2 * debt_penalty
266
+ return max(0.0, min(1.0, round(shi, 3)))
267
+
268
+ def propose_refactoring(self, directory: str) -> str | None:
269
+ """Generates proactive refactoring suggestions in markdown when SHI < 0.60."""
270
+ shi = self.calculate_shi(directory)
271
+ if shi >= 0.60:
272
+ return None
273
+
274
+ proposal = f"# Proactive Refactoring Proposal: `{os.path.basename(directory)}`\n\n"
275
+ proposal += f"The subsystem health index (**SHI: {shi:.2f}**) has fallen below the acceptable threshold (0.60).\n"
276
+ proposal += "This module is currently classified as an **Architectural Sink** and requires immediate refactoring.\n\n"
277
+
278
+ proposal += "## Diagnostic Summary\n"
279
+ coupling = self.calculate_coupling_ratio(directory)
280
+ proposal += f"- **Coupling Ratio**: {coupling:.2f} (Target: < 0.3)\n"
281
+
282
+ # Cohesion (LCOM) diagnostic
283
+ lcom_scores = []
284
+ for root, _, files in os.walk(directory):
285
+ for file in files:
286
+ if file.endswith(".py"):
287
+ file_path = os.path.join(root, file)
288
+ try:
289
+ with open(file_path, encoding="utf-8", errors="ignore") as f:
290
+ scores = self.calculate_lcom(f.read())
291
+ for cls, score in scores.items():
292
+ lcom_scores.append((cls, score, file))
293
+ except Exception:
294
+ pass
295
+ if lcom_scores:
296
+ avg_lcom = sum(s[1] for s in lcom_scores) / len(lcom_scores)
297
+ cohesion = 1.0 - min(avg_lcom / 10.0, 1.0)
298
+ proposal += f"- **Cohesion Score**: {cohesion:.2f} (Average LCOM: {avg_lcom:.1f})\n"
299
+ high_lcom = [s for s in lcom_scores if s[1] > 5]
300
+ if high_lcom:
301
+ proposal += "\n### High Lack of Cohesion of Methods (LCOM) Classes:\n"
302
+ for cls, score, file in high_lcom:
303
+ proposal += f" - Class `{cls}` in `{file}` has LCOM score of {score}.\n"
304
+ else:
305
+ proposal += "- **Cohesion Score**: 1.00 (No classes found)\n"
306
+
307
+ # Debt diagnostics
308
+ norm_dir = os.path.abspath(directory)
309
+ debt_items = []
310
+ for item in self.ledger.get_items():
311
+ item_path = os.path.abspath(item["file_path"])
312
+ if item_path.startswith(norm_dir):
313
+ debt_items.append(item)
314
+ if debt_items:
315
+ proposal += "\n## Outstanding Technical Debt Items\n"
316
+ for item in debt_items:
317
+ proposal += f"- **[{item['category'].upper()}]** `{os.path.basename(item['file_path'])}`: {item['description']} (Severity: {item['severity']})\n"
318
+
319
+ proposal += "\n## Action Plan\n"
320
+ proposal += "1. **Split High LCOM Classes**: Refactor classes with high cohesion issues into smaller, more focused modules.\n"
321
+ proposal += "2. **Reduce Module Coupling**: Minimize external dependencies and enforce clean interface boundaries.\n"
322
+ proposal += "3. **Clear Technical Debt**: Address the boundary layering violations and resolve critical warnings immediately.\n"
323
+ return proposal
324
+
325
+ def verify_boundaries(
326
+ self, proposed_code: str, file_path: str, raise_on_violation: bool = False
327
+ ) -> list[str]:
328
+ """
329
+ Checks proposed Python code for clean architecture layering and modular boundary violations.
330
+ Returns a list of violations found.
331
+ Raises ArchitectureDriftAlarm if raise_on_violation is True and violations exist.
332
+ """
333
+ violations = []
334
+ # Standardize path slashes for matching
335
+ norm_path = file_path.replace("\\", "/")
336
+
337
+ # Identify if target file has active layering rules
338
+ disallowed_prefixes: list[str] = []
339
+ for prefix, rules in self.layering_rules:
340
+ if norm_path.startswith(prefix):
341
+ disallowed_prefixes.extend(rules)
342
+
343
+ if not disallowed_prefixes:
344
+ return violations
345
+
346
+ try:
347
+ tree = ast.parse(proposed_code)
348
+ except SyntaxError:
349
+ return violations
350
+
351
+ for node in ast.walk(tree):
352
+ imported_mod: str | None = None
353
+ if isinstance(node, ast.Import):
354
+ for name in node.names:
355
+ imported_mod = name.name
356
+ elif isinstance(node, ast.ImportFrom) and node.module:
357
+ imported_mod = node.module
358
+
359
+ if imported_mod:
360
+ norm_import = imported_mod.replace(".", "/")
361
+ for disallowed in disallowed_prefixes:
362
+ if norm_import.startswith(disallowed):
363
+ violations.append(
364
+ f"Layering violation: '{file_path}' imports '{imported_mod}' which is banned."
365
+ )
366
+
367
+ if raise_on_violation and violations:
368
+ raise ArchitectureDriftAlarm("\n".join(violations))
369
+
370
+ return violations
371
+
372
+ def audit_architecture(self, file_path: str, code: str) -> dict[str, Any]:
373
+ """
374
+ Performs full cohesion and architectural boundary audit.
375
+ Records violations and high LCOM classes in the Technical Debt Ledger.
376
+ """
377
+ self.ledger.clear_file_debt(file_path)
378
+
379
+ lcom_scores = self.calculate_lcom(code)
380
+ boundary_violations = self.verify_boundaries(code, file_path)
381
+
382
+ # Log LCOM high scores as debt (LCOM > 5 is considered high)
383
+ for class_name, score in lcom_scores.items():
384
+ if score > 5:
385
+ self.ledger.add_debt_item(
386
+ file_path=file_path,
387
+ category="cohesion",
388
+ description=f"Class '{class_name}' has high LCOM score ({score}). Consider splitting its responsibilities.",
389
+ severity=round(score * 0.1, 2),
390
+ )
391
+
392
+ # Log boundary violations as high-severity debt
393
+ for violation in boundary_violations:
394
+ self.ledger.add_debt_item(
395
+ file_path=file_path,
396
+ category="layering",
397
+ description=violation,
398
+ severity=1.5,
399
+ )
400
+
401
+ dir_path = os.path.dirname(os.path.abspath(file_path))
402
+ coupling = self.calculate_coupling_ratio(dir_path)
403
+ shi = self.calculate_shi(dir_path)
404
+ proposal = self.propose_refactoring(dir_path)
405
+
406
+ passed = len(boundary_violations) == 0
407
+ return {
408
+ "passed": passed,
409
+ "lcom_scores": lcom_scores,
410
+ "violations": boundary_violations,
411
+ "total_debt": len(boundary_violations) + sum(1 for s in lcom_scores.values() if s > 5),
412
+ "coupling_ratio": coupling,
413
+ "shi": shi,
414
+ "refactoring_proposal": proposal,
415
+ }
@@ -0,0 +1,65 @@
1
+ """Council execution budget — hard time and cycle guards for agent deliberation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from velune.cli.modes import SessionMode
10
+
11
+
12
+ @dataclass
13
+ class CouncilExecutionBudget:
14
+ """Hard budget applied to every council deliberation run.
15
+
16
+ All ``*_seconds`` values are wall-clock limits. When passed to
17
+ :meth:`CouncilOrchestrator.execute_task`, these values override the
18
+ instance-level ``max_wall_time_seconds`` and inject per-agent
19
+ ``asyncio.wait_for`` guards that prevent hangs from unresponsive models.
20
+
21
+ Review cycle cap
22
+ ----------------
23
+ ``max_review_cycles`` is the binding cap on the debate loop. Even when
24
+ :func:`calculate_max_debate_turns` returns a higher number (e.g. 4 for a
25
+ security failure), the debate stops after ``max_review_cycles`` turns so a
26
+ single stubborn critic cannot loop a session indefinitely.
27
+ """
28
+
29
+ max_wall_time_seconds: int = 120
30
+ max_tokens_per_agent: int = 4096
31
+ max_review_cycles: int = 2
32
+ planner_timeout_seconds: int = 30
33
+ coder_timeout_seconds: int = 60
34
+ reviewer_timeout_seconds: int = 30
35
+
36
+ @classmethod
37
+ def from_session_mode(cls, mode: SessionMode) -> CouncilExecutionBudget:
38
+ """Return a budget calibrated to the given session mode.
39
+
40
+ * ``OPTIMUS`` — tightest budgets; speed over depth, 1 review cycle.
41
+ * ``NORMAL`` — balanced defaults (2 review cycles, 120s wall).
42
+ * ``GODLY`` — relaxed but still bounded; depth over speed, 3 cycles.
43
+ """
44
+ from velune.cli.modes import SessionMode
45
+
46
+ if mode == SessionMode.OPTIMUS:
47
+ return cls(
48
+ max_wall_time_seconds=60,
49
+ max_tokens_per_agent=2048,
50
+ max_review_cycles=1,
51
+ planner_timeout_seconds=15,
52
+ coder_timeout_seconds=30,
53
+ reviewer_timeout_seconds=15,
54
+ )
55
+ if mode == SessionMode.GODLY:
56
+ return cls(
57
+ max_wall_time_seconds=300,
58
+ max_tokens_per_agent=8192,
59
+ max_review_cycles=3,
60
+ planner_timeout_seconds=60,
61
+ coder_timeout_seconds=120,
62
+ reviewer_timeout_seconds=60,
63
+ )
64
+ # SessionMode.NORMAL — default values
65
+ return cls()
@@ -0,0 +1,47 @@
1
+ """Reasoning Council agents package."""
2
+
3
+ from velune.cognition.council.base import BaseCouncilAgent
4
+ from velune.cognition.council.challenger import ChallengerAgent
5
+ from velune.cognition.council.coder import CoderAgent
6
+ from velune.cognition.council.critic_agent import CriticAgent
7
+ from velune.cognition.council.critic_configs import (
8
+ MAINTAINABILITY_CONFIG,
9
+ PERFORMANCE_CONFIG,
10
+ SCALABILITY_CONFIG,
11
+ SECURITY_CONFIG,
12
+ CriticConfig,
13
+ )
14
+ from velune.cognition.council.critics import (
15
+ MaintainabilityCritic,
16
+ PerformanceCritic,
17
+ ScalabilityCritic,
18
+ SecurityCritic,
19
+ )
20
+ from velune.cognition.council.debate import (
21
+ DebateConfig,
22
+ calculate_max_debate_turns,
23
+ )
24
+ from velune.cognition.council.planner import PlannerAgent
25
+ from velune.cognition.council.reviewer import ReviewerAgent
26
+ from velune.cognition.council.synthesizer import SynthesizerAgent
27
+
28
+ __all__ = [
29
+ "BaseCouncilAgent",
30
+ "PlannerAgent",
31
+ "CoderAgent",
32
+ "ReviewerAgent",
33
+ "ChallengerAgent",
34
+ "SynthesizerAgent",
35
+ "CriticAgent",
36
+ "CriticConfig",
37
+ "SCALABILITY_CONFIG",
38
+ "SECURITY_CONFIG",
39
+ "PERFORMANCE_CONFIG",
40
+ "MAINTAINABILITY_CONFIG",
41
+ "ScalabilityCritic",
42
+ "SecurityCritic",
43
+ "PerformanceCritic",
44
+ "MaintainabilityCritic",
45
+ "DebateConfig",
46
+ "calculate_max_debate_turns",
47
+ ]