algo-cli-runtime 0.14.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 (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
algo_cli/config.py ADDED
@@ -0,0 +1,651 @@
1
+ """Configuration and persistent state."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import tempfile
8
+ import time
9
+ from contextlib import contextmanager
10
+ from dataclasses import asdict, dataclass, field
11
+ from pathlib import Path
12
+ from typing import Any, Callable, Iterable, Iterator
13
+
14
+
15
+ # Branding compatibility constants. New names take precedence; old names remain
16
+ # readable so existing installs continue to start cleanly.
17
+ NEW_CONFIG_DIR_NAME = ".algo_cli"
18
+ OLD_CONFIG_DIR_NAME = ".ollama_cli"
19
+ NEW_ENV_PREFIX = "ALGO_CLI_"
20
+ OLD_ENV_PREFIX = "OLLAMA_CLI_"
21
+
22
+
23
+ def _resolve_config_dir() -> Path:
24
+ """Resolve the active config directory with full dual-support for the rebrand.
25
+
26
+ Precedence (highest first):
27
+ 1. Explicit ALGO_CLI_CONFIG_DIR
28
+ 2. Explicit OLLAMA_CLI_CONFIG_DIR (legacy compat)
29
+ 3. If ~/.algo_cli already exists on disk → use it
30
+ 4. Default to ~/.algo_cli (new location)
31
+ """
32
+ # 1. New explicit env wins
33
+ new_explicit = os.environ.get(f"{NEW_ENV_PREFIX}CONFIG_DIR")
34
+ if new_explicit:
35
+ return Path(new_explicit).expanduser()
36
+
37
+ # 2. Old explicit env (legacy)
38
+ old_explicit = os.environ.get(f"{OLD_ENV_PREFIX}CONFIG_DIR")
39
+ if old_explicit:
40
+ return Path(old_explicit).expanduser()
41
+
42
+ home = Path.home()
43
+ new_dir = home / NEW_CONFIG_DIR_NAME
44
+
45
+ # 3. If the new location already exists, prefer it
46
+ if new_dir.exists():
47
+ return new_dir
48
+
49
+ # 4. Default to the new location (migration logic will detect old data later)
50
+ return new_dir
51
+
52
+
53
+ CONFIG_DIR = _resolve_config_dir()
54
+ CONFIG_FILE = CONFIG_DIR / "config.json"
55
+ MEMORY_FILE = CONFIG_DIR / "memory.json"
56
+ MEMORY_CANDIDATE_STATE_FILE = CONFIG_DIR / "memory_candidate_state.json"
57
+ HISTORY_DIR = CONFIG_DIR / "saves"
58
+ CONTEXT_ARCHIVE_DIR = CONFIG_DIR / "context_archives"
59
+ PROMPT_HISTORY_FILE = CONFIG_DIR / "prompt_history.txt"
60
+ PERF_HISTORY_FILE = CONFIG_DIR / "perf_history.jsonl"
61
+ EMBED_PERF_FILE = CONFIG_DIR / "embed_perf.jsonl"
62
+ DEFAULT_RUNTIME_ENV_FILE = CONFIG_DIR / "env"
63
+ DOTENV_RUNTIME_ENV_FILE = CONFIG_DIR / ".env"
64
+
65
+ DEFAULT_MODEL = (
66
+ os.environ.get(f"{NEW_ENV_PREFIX}MODEL")
67
+ or os.environ.get(f"{OLD_ENV_PREFIX}MODEL")
68
+ or "qwen3"
69
+ )
70
+ DEFAULT_HOST = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
71
+ DEFAULT_THEME = (
72
+ os.environ.get(f"{NEW_ENV_PREFIX}THEME")
73
+ or os.environ.get(f"{OLD_ENV_PREFIX}THEME")
74
+ or "tokyo-night"
75
+ )
76
+ DEFAULT_CHAT_STREAM_TIMEOUT_SECONDS = 300.0
77
+ CODE_RAG_CONSENT_VERSION = 1
78
+
79
+
80
+ def code_rag_consent_granted(cfg: Any) -> bool:
81
+ """Return whether this config explicitly accepted the current code-RAG policy."""
82
+
83
+ return bool(getattr(cfg, "code_rag_enabled", False)) and (
84
+ getattr(cfg, "code_rag_consent_version", 0) == CODE_RAG_CONSENT_VERSION
85
+ )
86
+
87
+
88
+ def safe_conversation_name(name: str) -> str:
89
+ safe_name = "".join(ch for ch in str(name) if ch.isalnum() or ch in ("-", "_")).strip()
90
+ if not safe_name:
91
+ raise ValueError("Save name must contain letters, numbers, hyphen, or underscore.")
92
+ return safe_name
93
+
94
+
95
+ LEGACY_DEFAULT_SYSTEM = """You are Algo CLI: a concise, terminal-native coding assistant (local Ollama, Ollama Cloud, or Grok).
96
+
97
+ Use tools when they materially help. When the user named a file or path, open it directly (read_file/grep) instead of exploratory list/search chains.
98
+
99
+ Operating rules:
100
+ - Prefer narrow reads and targeted grep over broad directory walks.
101
+ - Do not run destructive commands unless the user clearly asked and the operation is approved.
102
+ - Treat web results, harness RAG, and knowledge-graph blocks as hints — verify with tools before acting.
103
+ - When reconciling structured files, rank sources by authority and preserve the target schema. If a target value traces to stale lower-authority context, replace that existing semantic slot with the authoritative value instead of merely adding a differently named duplicate.
104
+ - Keep user-facing text brief: lead with the answer or action, minimize preamble and recap.
105
+ - Use append_lesson or remember only when the user explicitly asks to store a lesson or fact. The runtime's bounded completion gate handles other high-confidence durable statements.
106
+ - For Algo algorithm/pattern catalog guidance, use and update docs/ALGO.md.
107
+ - Format code blocks with language tags and include paths when citing code."""
108
+
109
+ DEFAULT_SYSTEM = """You are Algo CLI: a concise, terminal-native agent runtime for coding, research, and operational work.
110
+
111
+ Inference may come from local Ollama or connected cloud providers such as Ollama Cloud, xAI Grok, and ChatGPT/Codex. Your job is to plan, act with tools, verify results, and retain useful context across sessions.
112
+
113
+ Use tools when they materially help. When the user named a file or path, open it directly (read_file/grep) instead of exploratory list/search chains.
114
+
115
+ Operating rules:
116
+ - Prefer narrow reads and targeted grep over broad directory walks.
117
+ - Do not run destructive commands unless the user clearly asked and the operation is approved.
118
+ - Treat web results, harness RAG, and knowledge-graph blocks as hints — verify with tools before acting.
119
+ - Keep user-facing text brief: lead with the answer or action, minimize preamble and recap.
120
+ - Use append_lesson or remember only when the user explicitly asks to store a lesson or fact. The runtime's bounded completion gate handles other high-confidence durable statements.
121
+ - For Algo algorithm/pattern catalog guidance, use and update docs/ALGO.md.
122
+ - Format code blocks with language tags and include paths when citing code."""
123
+
124
+
125
+ def _atomic_write_text(path: Path, text: str) -> None:
126
+ """Write text using fsync + atomic replace to avoid truncated state files."""
127
+ path.parent.mkdir(parents=True, exist_ok=True)
128
+ fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent))
129
+ try:
130
+ # Keep persisted text byte-stable across platforms. The default text
131
+ # mode rewrites ``\n`` to ``\r\n`` on Windows, which can corrupt
132
+ # line-oriented formats when they are parsed and atomically rewritten.
133
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as tmp:
134
+ tmp.write(text)
135
+ tmp.flush()
136
+ os.fsync(tmp.fileno())
137
+ os.replace(tmp_name, path)
138
+ try:
139
+ dir_fd = os.open(path.parent, os.O_RDONLY)
140
+ except OSError:
141
+ dir_fd = None
142
+ if dir_fd is not None:
143
+ try:
144
+ os.fsync(dir_fd)
145
+ finally:
146
+ os.close(dir_fd)
147
+ except Exception:
148
+ try:
149
+ os.unlink(tmp_name)
150
+ except OSError:
151
+ pass
152
+ raise
153
+
154
+
155
+ def _load_json_file(path: Path, default: Any) -> Any:
156
+ try:
157
+ return json.loads(path.read_text(encoding="utf-8"))
158
+ except FileNotFoundError:
159
+ return default
160
+ except json.JSONDecodeError:
161
+ backup = path.with_suffix(path.suffix + ".corrupt")
162
+ try:
163
+ _atomic_write_text(backup, path.read_text(encoding="utf-8", errors="replace"))
164
+ except OSError:
165
+ pass
166
+ return default
167
+
168
+
169
+ @contextmanager
170
+ def _exclusive_state_lock(path: Path, *, timeout_seconds: float = 30.0) -> Iterator[None]:
171
+ """Cross-platform advisory lock for state-file transactions.
172
+
173
+ On Windows, uses msvcrt.locking with non-blocking attempts and retries.
174
+ Stale lock files from crashed processes are automatically released by the OS
175
+ when file handles close, but permission errors on lock file creation are
176
+ retried with a short backoff.
177
+ """
178
+ lock_path = path.with_suffix(path.suffix + ".lock")
179
+ path.parent.mkdir(parents=True, exist_ok=True)
180
+ deadline = time.monotonic() + timeout_seconds
181
+ # Retry opening the lock file if permission is denied briefly
182
+ lock_file = None
183
+ while lock_file is None:
184
+ try:
185
+ lock_file = open(lock_path, "a+b")
186
+ lock_file.write(b"x")
187
+ lock_file.flush()
188
+ except PermissionError:
189
+ if time.monotonic() >= deadline:
190
+ raise TimeoutError(f"Timed out waiting for state lock: {lock_path}")
191
+ time.sleep(0.05)
192
+ lock_file = None
193
+ continue
194
+ except OSError:
195
+ raise
196
+ try:
197
+ if os.name == "nt":
198
+ import msvcrt
199
+ lock_region = getattr(msvcrt, "locking")
200
+ lock_nonblocking = getattr(msvcrt, "LK_NBLCK")
201
+ unlock = getattr(msvcrt, "LK_UNLCK")
202
+ while True:
203
+ try:
204
+ lock_file.seek(0)
205
+ lock_region(lock_file.fileno(), lock_nonblocking, 1)
206
+ break
207
+ except OSError:
208
+ if time.monotonic() >= deadline:
209
+ raise TimeoutError(f"Timed out waiting for state lock: {lock_path}")
210
+ time.sleep(0.05)
211
+ try:
212
+ yield
213
+ finally:
214
+ lock_file.seek(0)
215
+ lock_region(lock_file.fileno(), unlock, 1)
216
+ else:
217
+ import fcntl
218
+ while True:
219
+ try:
220
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
221
+ break
222
+ except BlockingIOError:
223
+ if time.monotonic() >= deadline:
224
+ raise TimeoutError(f"Timed out waiting for state lock: {lock_path}")
225
+ time.sleep(0.05)
226
+ try:
227
+ yield
228
+ finally:
229
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
230
+ finally:
231
+ if lock_file is not None:
232
+ lock_file.close()
233
+ # Do not unlink the lock file. Removing an advisory lock path while
234
+ # another thread/process is waiting can create two different inodes and
235
+ # split the lock, allowing concurrent read-modify-write transactions.
236
+
237
+
238
+ def load_runtime_env(path: Path | str | None = None, *, override: bool = False) -> dict[str, str]:
239
+ # Dual-support: ALGO_CLI_ENV_FILE preferred, fall back to OLLAMA_CLI_ENV_FILE
240
+ configured_path = path or os.environ.get(f"{NEW_ENV_PREFIX}ENV_FILE") or os.environ.get(f"{OLD_ENV_PREFIX}ENV_FILE")
241
+ if configured_path is not None:
242
+ env_path = Path(configured_path)
243
+ elif DEFAULT_RUNTIME_ENV_FILE.exists():
244
+ env_path = DEFAULT_RUNTIME_ENV_FILE
245
+ else:
246
+ env_path = DOTENV_RUNTIME_ENV_FILE
247
+ loaded: dict[str, str] = {}
248
+ if not env_path.exists():
249
+ return loaded
250
+
251
+ try:
252
+ lines = env_path.read_text(encoding="utf-8", errors="replace").splitlines()
253
+ except OSError:
254
+ return loaded
255
+
256
+ for raw_line in lines:
257
+ line = raw_line.strip()
258
+ if not line or line.startswith("#"):
259
+ continue
260
+ if line.startswith("export "):
261
+ line = line[len("export ") :].strip()
262
+ if "=" not in line:
263
+ continue
264
+ key, value = line.split("=", 1)
265
+ key = key.strip()
266
+ value = value.strip()
267
+ if not key:
268
+ continue
269
+ if len(value) >= 2 and (
270
+ (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'"))
271
+ ):
272
+ value = value[1:-1]
273
+ if key in os.environ and not override:
274
+ loaded[key] = os.environ[key]
275
+ continue
276
+ os.environ[key] = value
277
+ loaded[key] = value
278
+ return loaded
279
+
280
+
281
+ _INVALID_CONFIG_VALUE = object()
282
+
283
+
284
+ def _coerce_config_value(current: Any, value: Any) -> Any:
285
+ if value is None:
286
+ return _INVALID_CONFIG_VALUE
287
+ if isinstance(current, bool):
288
+ if isinstance(value, bool):
289
+ return value
290
+ if isinstance(value, str):
291
+ lowered = value.strip().lower()
292
+ if lowered in {"true", "1", "yes", "on"}:
293
+ return True
294
+ if lowered in {"false", "0", "no", "off"}:
295
+ return False
296
+ return _INVALID_CONFIG_VALUE
297
+ if isinstance(current, int) and not isinstance(current, bool):
298
+ if isinstance(value, int) and not isinstance(value, bool):
299
+ return value
300
+ if isinstance(value, str):
301
+ try:
302
+ return int(value.strip())
303
+ except ValueError:
304
+ return _INVALID_CONFIG_VALUE
305
+ return _INVALID_CONFIG_VALUE
306
+ if isinstance(current, float):
307
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
308
+ return float(value)
309
+ if isinstance(value, str):
310
+ try:
311
+ return float(value.strip())
312
+ except ValueError:
313
+ return _INVALID_CONFIG_VALUE
314
+ return _INVALID_CONFIG_VALUE
315
+ if isinstance(current, str):
316
+ return value if isinstance(value, str) else _INVALID_CONFIG_VALUE
317
+ if isinstance(current, list):
318
+ return value if isinstance(value, list) else _INVALID_CONFIG_VALUE
319
+ if isinstance(current, dict):
320
+ return value if isinstance(value, dict) else _INVALID_CONFIG_VALUE
321
+ return value
322
+
323
+
324
+ @dataclass
325
+ class Config:
326
+ model: str = DEFAULT_MODEL
327
+ system: str = DEFAULT_SYSTEM
328
+ theme: str = DEFAULT_THEME
329
+ auto_mode: bool = False
330
+ # Session-only auto-approve set by answering "a" at an approval prompt.
331
+ # Never persisted: see save(); resets on every new session.
332
+ session_auto_approve: bool = False
333
+ show_thinking: bool = True
334
+ num_ctx: int = 8192
335
+ temperature: float = 0.4
336
+ chat_stream_timeout_seconds: float = DEFAULT_CHAT_STREAM_TIMEOUT_SECONDS
337
+ max_tool_iterations: int = 24
338
+ tool_think_every: int = 10
339
+ prune_after_messages: int = 80
340
+ prune_keep_recent: int = 40
341
+ embedding_backend: str = "auto" # "auto" | "local"; "cloud" currently falls back to local
342
+ cloud_embedding_model: str = "nomic-embed-text:latest" # Reserved until cloud embeddings are supported.
343
+ harness_embed_model: str = "qwen3-embedding:latest" # Local Ollama embed model for harness + lessons RAG
344
+ embed_dimensions: int | None = None # Optional override; None lets the model decide (e.g. 4096 for qwen3-embedding)
345
+ echo_veil_enabled: bool = False # Enable Echo Veil tiered memory layer
346
+ echo_veil_capacity: int = 400 # Maximum active Echo Veil memories before decay
347
+ echo_veil_production: bool = False # Require encrypted Echo Veil storage when enabled
348
+ echo_veil_crypto_key_path: str | None = None # Path to JSON file with {"key_hex": "..."} for memory encryption
349
+ memory_auto_capture_enabled: bool = True # Auto-save only explicit, high-confidence durable user statements
350
+ memory_auto_daily_limit: int = 5 # User may lower; admission hard-maxes at 5/day
351
+ memory_auto_entry_limit: int = 64 # User may lower; admission hard-maxes at 64 fingerprints
352
+ memory_auto_char_limit: int = 12_000 # User may lower; admission hard-maxes at 12k total chars
353
+ skill_crystallize_enabled: bool = False
354
+ skill_crystallize_every: int = 3
355
+ runs_since_crystallize: int = 0
356
+ host: str = DEFAULT_HOST
357
+ cloud: bool = False
358
+ onboarded: bool = False
359
+ auto_cloud_connect: bool = False
360
+ safe_mode: bool = True
361
+ verify_mode: bool = False
362
+ intuition_recall_enabled: bool = False
363
+ intuition_capture_enabled: bool = False
364
+ algorithmic_tool_policy_enabled: bool = False
365
+ reflex_enabled: bool = False
366
+ model_adaptive: bool = True # adapt num_ctx/temperature/reflection to model size+provider
367
+ code_rag_enabled: bool = False # opt in to retrieving cfg.cwd source chunks each turn
368
+ code_rag_consent_version: int = 0 # set only by explicit /code-rag on consent
369
+ external_harness_sources_enabled: bool = False # opt in to ~/.codex, ~/.claude, ~/.openclaw, etc.
370
+ reasoning_chat_enabled: bool = False # run reasoning preflight in the chat loop, not just pipelines
371
+ # --- Reasoning engine flags ---
372
+ reasoning_mode: str = "react" # react | reflexion | tot | got | mcts | qcr | neuro_symbolic | hybrid
373
+ reasoning_depth: int = 4 # Max depth/rounds for tree/graph search
374
+ reasoning_branches: int = 3 # Branch factor for tree/graph expansion
375
+ reasoning_qcr_samples: int = 5 # Number of CoT fragments for QCR aggregation
376
+ reasoning_reflexion_attempts: int = 3 # Max self-critique rounds
377
+ reasoning_ns_rounds: int = 3 # Max neuro-symbolic verify rounds
378
+ reasoning_auto_reflexion: bool = False # Auto-apply Reflexion on failed blocks
379
+ reasoning_auto_verify: bool = False # Auto-verify implement blocks with neuro-symbolic
380
+ index_compute_lab_auto_inject: bool = False
381
+ session_mode: str = "explore" # execute | explore | publish
382
+ keep_alive: str = "30m"
383
+ cwd: str = field(default_factory=lambda: str(Path.cwd()))
384
+ session_summary: str = ""
385
+ context_state: dict[str, Any] = field(default_factory=dict)
386
+ messages: list[dict[str, Any]] = field(default_factory=list)
387
+ memories: list[str] = field(default_factory=list)
388
+ attempt_ledger: list[dict[str, Any]] = field(default_factory=list)
389
+
390
+ @property
391
+ def client_host(self) -> str | None:
392
+ return None if self.cloud else self.host
393
+
394
+ @property
395
+ def auto_approve_active(self) -> bool:
396
+ """True when approvals are skipped, persistently (/auto) or for this session ('a')."""
397
+ return self.auto_mode or self.session_auto_approve
398
+
399
+ def save(self) -> None:
400
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
401
+ HISTORY_DIR.mkdir(parents=True, exist_ok=True)
402
+ data = asdict(self)
403
+ data.pop("messages", None)
404
+ data.pop("memories", None)
405
+ data.pop("session_auto_approve", None)
406
+ _atomic_write_text(CONFIG_FILE, json.dumps(data, indent=2))
407
+
408
+ def save_memories(self) -> None:
409
+ with _exclusive_state_lock(MEMORY_FILE):
410
+ _atomic_write_text(MEMORY_FILE, json.dumps([str(item) for item in self.memories], indent=2))
411
+
412
+ def remember_fact(self, fact: str) -> bool:
413
+ """Append a memory fact under a lock to avoid lost updates."""
414
+ fact = str(fact).strip()
415
+ if not fact:
416
+ return False
417
+ with _exclusive_state_lock(MEMORY_FILE):
418
+ loaded = _load_json_file(MEMORY_FILE, [])
419
+ current = [str(item) for item in loaded] if isinstance(loaded, list) else []
420
+ added = fact not in current
421
+ if added:
422
+ current.append(fact)
423
+ _atomic_write_text(MEMORY_FILE, json.dumps(current, indent=2))
424
+ self.memories = current
425
+ return added
426
+
427
+ def reconcile_memory_facts(
428
+ self,
429
+ *,
430
+ additions: Iterable[str] = (),
431
+ remove_if: Callable[[str], bool] | None = None,
432
+ ) -> dict[str, int | bool]:
433
+ """Atomically remove stale facts and add normalized-deduplicated facts.
434
+
435
+ Existing retained strings and their order are preserved exactly. A
436
+ pre-change backup is written beside ``memory.json`` so a bulk migration
437
+ can be reversed without relying on positional ``/forget`` operations.
438
+ Fact bodies are deliberately absent from the returned telemetry.
439
+ """
440
+
441
+ def normalized_key(value: str) -> str:
442
+ return " ".join(value.split()).casefold()
443
+
444
+ with _exclusive_state_lock(MEMORY_FILE):
445
+ loaded = _load_json_file(MEMORY_FILE, [])
446
+ current = [str(item) for item in loaded] if isinstance(loaded, list) else []
447
+ retained = [fact for fact in current if remove_if is None or not remove_if(fact)]
448
+ removed = len(current) - len(retained)
449
+ seen = {normalized_key(fact) for fact in retained if normalized_key(fact)}
450
+ added = 0
451
+ for candidate in additions:
452
+ fact = str(candidate).strip()
453
+ key = normalized_key(fact)
454
+ if not key or key in seen:
455
+ continue
456
+ retained.append(fact)
457
+ seen.add(key)
458
+ added += 1
459
+ changed = removed > 0 or added > 0
460
+ if changed:
461
+ if MEMORY_FILE.exists():
462
+ backup_path = MEMORY_FILE.with_suffix(MEMORY_FILE.suffix + ".reconcile.bak")
463
+ _atomic_write_text(backup_path, MEMORY_FILE.read_text(encoding="utf-8"))
464
+ _atomic_write_text(MEMORY_FILE, json.dumps(retained, indent=2))
465
+ self.memories = retained
466
+ return {
467
+ "changed": changed,
468
+ "removed": removed,
469
+ "added": added,
470
+ "total": len(retained),
471
+ }
472
+
473
+ def forget_memory_index(self, index: int) -> str:
474
+ """Remove a memory by zero-based index against the latest persisted list."""
475
+ with _exclusive_state_lock(MEMORY_FILE):
476
+ loaded = _load_json_file(MEMORY_FILE, [])
477
+ current = [str(item) for item in loaded] if isinstance(loaded, list) else []
478
+ removed = current.pop(index)
479
+ _atomic_write_text(MEMORY_FILE, json.dumps(current, indent=2))
480
+ self.memories = current
481
+ return removed
482
+
483
+ def save_conversation(self, name: str) -> Path:
484
+ HISTORY_DIR.mkdir(parents=True, exist_ok=True)
485
+ safe_name = safe_conversation_name(name)
486
+ path = HISTORY_DIR / f"{safe_name}.json"
487
+ _atomic_write_text(
488
+ path,
489
+ json.dumps(
490
+ {
491
+ "messages": self.messages,
492
+ "session_summary": self.session_summary,
493
+ "context_state": self.context_state,
494
+ },
495
+ indent=2,
496
+ default=str,
497
+ ),
498
+ )
499
+ return path
500
+
501
+ def load_conversation(self, name: str) -> int:
502
+ safe_name = safe_conversation_name(name)
503
+ path = HISTORY_DIR / f"{safe_name}.json"
504
+ if not path.exists():
505
+ raise FileNotFoundError(f"No saved conversation named '{safe_name}'")
506
+ loaded = json.loads(path.read_text(encoding="utf-8"))
507
+ if isinstance(loaded, list):
508
+ self.messages = loaded
509
+ self.session_summary = ""
510
+ self.context_state = {}
511
+ elif isinstance(loaded, dict):
512
+ messages = loaded.get("messages", [])
513
+ self.messages = messages if isinstance(messages, list) else []
514
+ summary = loaded.get("session_summary", "")
515
+ self.session_summary = str(summary) if summary else ""
516
+ context_state = loaded.get("context_state", {})
517
+ self.context_state = context_state if isinstance(context_state, dict) else {}
518
+ else:
519
+ self.messages = []
520
+ self.session_summary = ""
521
+ self.context_state = {}
522
+ return len(self.messages)
523
+
524
+ @classmethod
525
+ def load(cls) -> "Config":
526
+ cfg = cls()
527
+ if CONFIG_FILE.exists():
528
+ data = _load_json_file(CONFIG_FILE, {})
529
+ if not isinstance(data, dict):
530
+ data = {}
531
+ for key, value in data.items():
532
+ if hasattr(cfg, key) and key not in {"messages", "memories", "session_auto_approve"}:
533
+ coerced = _coerce_config_value(getattr(cfg, key), value)
534
+ if coerced is not _INVALID_CONFIG_VALUE:
535
+ setattr(cfg, key, coerced)
536
+ # Releases before the versioned consent gate persisted only a boolean,
537
+ # which is not evidence that the user accepted cwd snippets crossing the
538
+ # active provider boundary. Fail closed until /code-rag on records the
539
+ # current policy version.
540
+ if not code_rag_consent_granted(cfg):
541
+ cfg.code_rag_enabled = False
542
+ # Refresh only the exact prompt shipped by older releases. User-authored
543
+ # system prompts are preserved verbatim.
544
+ if cfg.system == LEGACY_DEFAULT_SYSTEM:
545
+ cfg.system = DEFAULT_SYSTEM
546
+ if MEMORY_FILE.exists():
547
+ loaded = _load_json_file(MEMORY_FILE, [])
548
+ if isinstance(loaded, list):
549
+ cfg.memories = [str(item) for item in loaded]
550
+ return cfg
551
+
552
+
553
+ # --- Rebrand migration helpers (used by main.py during startup) ---
554
+
555
+ LEGACY_CONFIG_DIR = Path.home() / OLD_CONFIG_DIR_NAME
556
+
557
+
558
+ def has_legacy_data() -> bool:
559
+ """True if the old ~/.ollama_cli directory exists and contains real user data."""
560
+ if not LEGACY_CONFIG_DIR.exists():
561
+ return False
562
+ # Consider it "real data" if it has at least one of the key files/dirs
563
+ markers = [
564
+ LEGACY_CONFIG_DIR / "config.json",
565
+ LEGACY_CONFIG_DIR / "memory.json",
566
+ LEGACY_CONFIG_DIR / "identity",
567
+ LEGACY_CONFIG_DIR / "skills",
568
+ LEGACY_CONFIG_DIR / "run_history.jsonl",
569
+ ]
570
+ return any(m.exists() for m in markers)
571
+
572
+
573
+ def get_legacy_backup_dir() -> Path:
574
+ """Where we will copy the old data as a safety backup."""
575
+ return Path.home() / ".ollama_cli.backup"
576
+
577
+
578
+ def perform_legacy_migration() -> bool:
579
+ """Copy ~/.ollama_cli → ~/.algo_cli (and leave a .backup copy).
580
+
581
+ Never deletes the original. Returns True if a migration was performed.
582
+ Safe to call multiple times (idempotent via existence checks).
583
+ """
584
+ old = LEGACY_CONFIG_DIR
585
+ new = CONFIG_DIR
586
+ backup = get_legacy_backup_dir()
587
+
588
+ if not has_legacy_data():
589
+ return False
590
+ if new.exists():
591
+ # New location exists (even if empty/scaffolded) — do not overwrite
592
+ return False
593
+
594
+ try:
595
+ import shutil
596
+ import uuid
597
+
598
+ tmp_new = new.with_name(f"{new.name}.migration-{uuid.uuid4().hex}")
599
+ # First make a full backup of the old location (never delete originals)
600
+ if old.exists() and not backup.exists():
601
+ shutil.copytree(old, backup, dirs_exist_ok=True)
602
+
603
+ # Now copy into a staging directory, then publish it atomically enough
604
+ # for startup: a failed copy never leaves CONFIG_DIR partially created.
605
+ shutil.copytree(old, tmp_new)
606
+
607
+ # Leave a small sentinel so we know migration already happened for this user
608
+ try:
609
+ _atomic_write_text(tmp_new / ".migrated_from_legacy", f"Migrated from {old} on first run of algo-cli 0.3+")
610
+ except Exception:
611
+ pass
612
+ tmp_new.rename(new)
613
+
614
+ return True
615
+ except Exception:
616
+ try:
617
+ if "tmp_new" in locals() and tmp_new.exists():
618
+ import shutil
619
+
620
+ shutil.rmtree(tmp_new, ignore_errors=True)
621
+ except Exception:
622
+ pass
623
+ # Best-effort migration; do not crash startup
624
+ return False
625
+
626
+
627
+ def migrate_legacy_sidecar_files() -> list[str]:
628
+ """Copy small auth/env files left behind when full migration was skipped.
629
+
630
+ Full ``perform_legacy_migration`` only runs when ~/.algo_cli is empty. If the
631
+ new directory was populated first (e.g. identity scaffold), xai_auth.json and
632
+ .env can remain only under ~/.ollama_cli and xAI models disappear from /models.
633
+ """
634
+ import shutil
635
+
636
+ moved: list[str] = []
637
+ pairs = [
638
+ (LEGACY_CONFIG_DIR / "xai_auth.json", CONFIG_DIR / "xai_auth.json"),
639
+ (LEGACY_CONFIG_DIR / ".env", CONFIG_DIR / ".env"),
640
+ (LEGACY_CONFIG_DIR / "env", CONFIG_DIR / "env"),
641
+ ]
642
+ for src, dst in pairs:
643
+ if not src.is_file() or dst.exists():
644
+ continue
645
+ try:
646
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
647
+ shutil.copy2(src, dst)
648
+ moved.append(src.name)
649
+ except Exception:
650
+ continue
651
+ return moved