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/url_scheme.py ADDED
@@ -0,0 +1,174 @@
1
+ """URL scheme handler for Algo CLI.
2
+
3
+ Handles algo-cli:// deep links, inspired by Ollama's ollama:// URL scheme.
4
+
5
+ Supported routes:
6
+ algo-cli://skill/<name> — load and display a skill
7
+ algo-cli://memory/recall?q=<query> — trigger a memory recall search
8
+ algo-cli://session/new — start a new session
9
+ algo-cli://session/load?name=<n> — load a saved session
10
+ algo-cli://plugin/<name> — show plugin status
11
+ algo-cli://version — show full version manifest
12
+ algo-cli://credential/<helper> — show credential helper status
13
+
14
+ The handler parses the URL and returns a ParsedDeepLink describing the
15
+ action to take. The caller (CLI entry point or GUI companion) is responsible
16
+ for executing the action.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import urllib.parse
22
+ from dataclasses import dataclass, field
23
+ from typing import Any
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ URL_SCHEME = "algo-cli"
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class ParsedDeepLink:
32
+ """A parsed algo-cli:// deep link."""
33
+ route: str
34
+ path: str
35
+ query: dict[str, str] = field(default_factory=dict)
36
+ raw_url: str = ""
37
+
38
+ def as_dict(self) -> dict[str, Any]:
39
+ return {
40
+ "route": self.route,
41
+ "path": self.path,
42
+ "query": self.query,
43
+ "raw_url": self.raw_url,
44
+ }
45
+
46
+
47
+ # Supported routes and their expected path/query parameters
48
+ KNOWN_ROUTES: dict[str, dict[str, Any]] = {
49
+ "skill": {"path_required": True, "description": "Load and display a skill by name"},
50
+ "memory": {"path_required": True, "description": "Memory operations (recall, list)"},
51
+ "session": {"path_required": True, "description": "Session operations (new, load)"},
52
+ "plugin": {"path_required": True, "description": "Show plugin status by name"},
53
+ "version": {"path_required": False, "description": "Show full version manifest"},
54
+ "credential": {"path_required": True, "description": "Show credential helper status"},
55
+ }
56
+
57
+
58
+ def parse_deep_link(url: str) -> ParsedDeepLink | None:
59
+ """Parse an algo-cli:// URL into a ParsedDeepLink.
60
+
61
+ Returns None if the URL is not a valid algo-cli:// link.
62
+ """
63
+ if not url:
64
+ return None
65
+
66
+ # Accept both algo-cli:// and algo-cli: for flexibility
67
+ if url.startswith(f"{URL_SCHEME}://"):
68
+ remainder = url[len(f"{URL_SCHEME}://"):]
69
+ elif url.startswith(f"{URL_SCHEME}:"):
70
+ remainder = url[len(f"{URL_SCHEME}:"):]
71
+ else:
72
+ return None
73
+
74
+ # Strip leading slashes
75
+ remainder = remainder.lstrip("/")
76
+
77
+ # Parse query string if present
78
+ if "?" in remainder:
79
+ path_part, query_part = remainder.split("?", 1)
80
+ query = dict(urllib.parse.parse_qsl(query_part))
81
+ else:
82
+ path_part = remainder
83
+ query = {}
84
+
85
+ path_part = path_part.rstrip("/")
86
+ if not path_part:
87
+ return ParsedDeepLink(route="", path="", query=query, raw_url=url)
88
+
89
+ # Split into route and sub-path
90
+ parts = path_part.split("/", 1)
91
+ route = parts[0].lower()
92
+ sub_path = parts[1] if len(parts) > 1 else ""
93
+
94
+ return ParsedDeepLink(
95
+ route=route,
96
+ path=sub_path,
97
+ query=query,
98
+ raw_url=url,
99
+ )
100
+
101
+
102
+ def validate_deep_link(link: ParsedDeepLink) -> tuple[bool, str]:
103
+ """Validate a parsed deep link. Returns (is_valid, error_message)."""
104
+ if not link.route:
105
+ return False, "Empty route"
106
+
107
+ if link.route not in KNOWN_ROUTES:
108
+ known = ", ".join(sorted(KNOWN_ROUTES.keys()))
109
+ return False, f"Unknown route '{link.route}'. Known routes: {known}"
110
+
111
+ route_info = KNOWN_ROUTES[link.route]
112
+ if route_info["path_required"] and not link.path:
113
+ return False, f"Route '{link.route}' requires a path component"
114
+
115
+ return True, ""
116
+
117
+
118
+ def handle_deep_link(url: str) -> dict[str, Any]:
119
+ """Parse and validate a deep link URL, returning an action descriptor.
120
+
121
+ This is the main entry point. It returns a dict with:
122
+ - valid: bool
123
+ - error: str (if invalid)
124
+ - action: str (the route name)
125
+ - target: str (the path component)
126
+ - query: dict (query parameters)
127
+ - raw_url: str
128
+
129
+ The caller is responsible for executing the action.
130
+ """
131
+ parsed = parse_deep_link(url)
132
+ if parsed is None:
133
+ return {
134
+ "valid": False,
135
+ "error": f"URL does not start with {URL_SCHEME}://",
136
+ "raw_url": url,
137
+ }
138
+
139
+ is_valid, error = validate_deep_link(parsed)
140
+ if not is_valid:
141
+ return {
142
+ "valid": False,
143
+ "error": error,
144
+ "raw_url": url,
145
+ **parsed.as_dict(),
146
+ }
147
+
148
+ return {
149
+ "valid": True,
150
+ "error": "",
151
+ "action": parsed.route,
152
+ "target": parsed.path,
153
+ "query": parsed.query,
154
+ "raw_url": url,
155
+ }
156
+
157
+
158
+ def format_help() -> str:
159
+ """Return a help string listing all supported deep link routes."""
160
+ lines = [f"Algo CLI URL scheme: {URL_SCHEME}://<route>/<path>?<query>"]
161
+ lines.append("")
162
+ lines.append("Supported routes:")
163
+ for route, info in sorted(KNOWN_ROUTES.items()):
164
+ path_req = " <path>" if info["path_required"] else ""
165
+ lines.append(f" {URL_SCHEME}://{route}{path_req}")
166
+ lines.append(f" {info['description']}")
167
+ lines.append("")
168
+ lines.append("Examples:")
169
+ lines.append(f" {URL_SCHEME}://skill/echo-veil-integration")
170
+ lines.append(f" {URL_SCHEME}://memory/recall?q=rebrand")
171
+ lines.append(f" {URL_SCHEME}://session/new")
172
+ lines.append(f" {URL_SCHEME}://session/load?name=my-session")
173
+ lines.append(f" {URL_SCHEME}://version")
174
+ return "\n".join(lines)
algo_cli/verify.py ADDED
@@ -0,0 +1,154 @@
1
+ """Hallucination-prevention layer for small models.
2
+
3
+ Three components wired together:
4
+ 1. parameter_size_billions() — parse model size string to float (lives in model_info)
5
+ 2. extract_claims() — maintenance-model pass decomposing a response into
6
+ individual verifiable factual claims
7
+ 3. ground_claim() — each claim is checked against the harness index;
8
+ ungrounded claims are returned for user visibility
9
+ 4. verify_response() — orchestrates 2+3 and returns a structured report
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ from typing import Any, Callable
16
+
17
+ from . import harness
18
+
19
+ LLMFn = Callable[[str, str], str]
20
+
21
+ _MIN_CLAIM_CHARS = 12
22
+
23
+ _EXTRACT_SYSTEM = (
24
+ "You are a factual claim extractor. Given text, list every verifiable factual claim "
25
+ "as a JSON array under the key 'claims'. A claim is a specific, checkable assertion: "
26
+ "a file path, function name, version number, shell command, URL, configuration key, "
27
+ "or named technical concept. Omit vague statements and subjective opinions. "
28
+ 'Output only valid JSON: {"claims": ["claim1", "claim2", ...]}'
29
+ )
30
+
31
+ _WORD_RE = re.compile(r"[a-zA-Z0-9_\-\.]+")
32
+ _COMMON_WORDS = frozenset(
33
+ {
34
+ "the", "and", "for", "that", "this", "with", "from", "file", "uses",
35
+ "use", "has", "have", "will", "into", "json", "text", "data", "code",
36
+ "line", "lines", "path",
37
+ }
38
+ )
39
+
40
+
41
+ def extract_claims(text: str, llm_fn: LLMFn) -> list[str]:
42
+ """Use a maintenance LLM pass to extract factual claims from model output.
43
+
44
+ Returns an empty list on any failure — never raises.
45
+ """
46
+ if not text or not text.strip():
47
+ return []
48
+ try:
49
+ raw = llm_fn(_EXTRACT_SYSTEM, text)
50
+ data = json.loads(raw)
51
+ claims = data.get("claims", [])
52
+ if not isinstance(claims, list):
53
+ return []
54
+ return [
55
+ str(c).strip()
56
+ for c in claims
57
+ if str(c).strip() and len(str(c).strip()) >= _MIN_CLAIM_CHARS
58
+ ]
59
+ except Exception:
60
+ return []
61
+
62
+
63
+ def ground_claim(claim: str, *, limit: int = 3) -> dict[str, Any]:
64
+ """Check whether a claim has supporting evidence in the harness index.
65
+
66
+ Returns:
67
+ {"claim": str, "grounded": bool, "sources": list[str]}
68
+ """
69
+ if not claim or not claim.strip():
70
+ return {"claim": claim, "grounded": False, "sources": []}
71
+ words = _WORD_RE.findall(claim)
72
+ keywords = [w for w in words if len(w) >= 4 and w.lower() not in _COMMON_WORDS][:8]
73
+ query = " ".join(keywords)
74
+ if not query:
75
+ return {"claim": claim, "grounded": False, "sources": []}
76
+ try:
77
+ results = harness.search_index(query, limit=limit)
78
+ except Exception:
79
+ results = []
80
+ claim_tokens = {w.lower() for w in keywords}
81
+ sources: list[str] = []
82
+ for result in results:
83
+ haystack = " ".join(
84
+ str(result.get(key, ""))
85
+ for key in ("id", "title", "path", "relative_path", "description", "summary", "search_text")
86
+ ).lower()
87
+ if any(token in haystack for token in claim_tokens):
88
+ if result.get("id"):
89
+ sources.append(str(result["id"]))
90
+ if sources:
91
+ return {
92
+ "claim": claim,
93
+ "grounded": True,
94
+ "sources": sources[:limit],
95
+ }
96
+ return {"claim": claim, "grounded": False, "sources": []}
97
+
98
+
99
+ def verify_response(text: str, llm_fn: LLMFn) -> dict[str, Any]:
100
+ """Full claim-extraction + harness-grounding pipeline.
101
+
102
+ Returns:
103
+ {
104
+ "total_claims": int,
105
+ "grounded_count": int,
106
+ "ungrounded_count": int,
107
+ "grounded": [...],
108
+ "ungrounded": [...],
109
+ "confidence": float, # fraction of claims grounded (1.0 if no claims)
110
+ }
111
+ """
112
+ claims = extract_claims(text, llm_fn)
113
+ if not claims:
114
+ return {
115
+ "total_claims": 0,
116
+ "grounded_count": 0,
117
+ "ungrounded_count": 0,
118
+ "grounded": [],
119
+ "ungrounded": [],
120
+ "confidence": 1.0,
121
+ }
122
+ results = [ground_claim(c) for c in claims]
123
+ grounded = [r for r in results if r["grounded"]]
124
+ ungrounded = [r for r in results if not r["grounded"]]
125
+ confidence = len(grounded) / len(results) if results else 1.0
126
+ return {
127
+ "total_claims": len(claims),
128
+ "grounded_count": len(grounded),
129
+ "ungrounded_count": len(ungrounded),
130
+ "grounded": grounded,
131
+ "ungrounded": ungrounded,
132
+ "confidence": round(confidence, 3),
133
+ }
134
+
135
+
136
+ def format_verification_report(result: dict[str, Any]) -> str:
137
+ """Human-readable summary of a verify_response result."""
138
+ total = result.get("total_claims", 0)
139
+ if total == 0:
140
+ return ""
141
+ grounded_count = result["grounded_count"]
142
+ confidence = result["confidence"]
143
+ lines = [
144
+ f"[Verify] {grounded_count}/{total} claims grounded in harness "
145
+ f"(confidence {confidence:.0%})"
146
+ ]
147
+ ungrounded = result.get("ungrounded", [])
148
+ if ungrounded:
149
+ lines.append("Unverified claims (not found in harness index):")
150
+ for item in ungrounded[:5]:
151
+ lines.append(f" · {item['claim']}")
152
+ if len(ungrounded) > 5:
153
+ lines.append(f" · …and {len(ungrounded) - 5} more")
154
+ return "\n".join(lines)
@@ -0,0 +1,178 @@
1
+ """Version manifest for Algo CLI.
2
+
3
+ Tracks the full system state — not just the CLI binary version, but the
4
+ harness index version, embedding model, knowledge graph version, and any
5
+ loaded plugin versions. Inspired by Docker's componentsVersion.json.
6
+
7
+ The manifest may be written to ~/.algo_cli/versions.json. `algo-cli --version`
8
+ only reads state that already exists; it never builds an index or scaffolds files.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import time
14
+ from dataclasses import asdict, dataclass, field
15
+ from typing import Any
16
+
17
+ from .config import CONFIG_DIR
18
+ from . import __version__ as _cli_version
19
+
20
+ VERSIONS_FILE = CONFIG_DIR / "versions.json"
21
+ EXTENSIONS_VERSION_FILE = CONFIG_DIR / "extensions_version.json"
22
+
23
+
24
+ @dataclass
25
+ class VersionManifest:
26
+ """Full system version state."""
27
+ cli_version: str = _cli_version
28
+ python_version: str = ""
29
+ platform: str = ""
30
+ harness_index_version: str = ""
31
+ harness_record_count: int = 0
32
+ harness_embed_model: str = ""
33
+ knowledge_graph_version: str = ""
34
+ knowledge_graph_node_count: int = 0
35
+ plugins: dict[str, str] = field(default_factory=dict)
36
+ config_dir: str = ""
37
+ updated_at: float = 0.0
38
+
39
+ def as_dict(self) -> dict[str, Any]:
40
+ return asdict(self)
41
+
42
+ def to_json(self) -> str:
43
+ return json.dumps(self.as_dict(), indent=2)
44
+
45
+
46
+ def _safe_get_harness_info() -> dict[str, Any]:
47
+ """Read an existing harness index without building or refreshing it."""
48
+ try:
49
+ from . import harness
50
+ if not harness.INDEX_PATH.is_file():
51
+ return {}
52
+ index = json.loads(harness.INDEX_PATH.read_text(encoding="utf-8"))
53
+ record_count = len(index.get("records", []))
54
+ embed_model = index.get("embed_model", "")
55
+ index_version = index.get("version", "")
56
+ return {
57
+ "harness_index_version": index_version,
58
+ "harness_record_count": record_count,
59
+ "harness_embed_model": embed_model,
60
+ }
61
+ except Exception:
62
+ return {}
63
+
64
+
65
+ def _safe_get_kg_info() -> dict[str, Any]:
66
+ """Read existing knowledge-graph metadata without running the graph."""
67
+ try:
68
+ from . import index_compute_lab
69
+ assets = index_compute_lab.lab_assets()
70
+ if assets is not None:
71
+ _root, graph_path, _aliases = assets
72
+ data = json.loads(graph_path.read_text(encoding="utf-8"))
73
+ nodes = data.get("nodes")
74
+ index = data.get("index")
75
+ node_count = len(nodes) if isinstance(nodes, list) else len(index) if isinstance(index, dict) else 0
76
+ version = data.get("version") or data.get("schema_version") or ""
77
+ return {
78
+ "knowledge_graph_version": version,
79
+ "knowledge_graph_node_count": node_count,
80
+ }
81
+ except Exception:
82
+ pass
83
+ return {}
84
+
85
+
86
+ def _safe_get_plugin_versions() -> dict[str, str]:
87
+ """Safely query loaded plugins for their versions."""
88
+ try:
89
+ from . import plugins as plugins_module
90
+ manifests = plugins_module.discover_plugins()
91
+ return {m.name: m.version for m in manifests}
92
+ except Exception:
93
+ return {}
94
+
95
+
96
+ def build_manifest() -> VersionManifest:
97
+ """Build a version manifest from the current system state."""
98
+ import platform as _platform
99
+ import sys
100
+
101
+ harness_info = _safe_get_harness_info()
102
+ kg_info = _safe_get_knowledge_graph_info()
103
+ plugin_versions = _safe_get_plugin_versions()
104
+
105
+ return VersionManifest(
106
+ cli_version=_cli_version,
107
+ python_version=sys.version.split()[0] if sys.version else "",
108
+ platform=f"{_platform.system()} {_platform.machine()}",
109
+ harness_index_version=harness_info.get("harness_index_version", ""),
110
+ harness_record_count=harness_info.get("harness_record_count", 0),
111
+ harness_embed_model=harness_info.get("harness_embed_model", ""),
112
+ knowledge_graph_version=kg_info.get("knowledge_graph_version", ""),
113
+ knowledge_graph_node_count=kg_info.get("knowledge_graph_node_count", 0),
114
+ plugins=plugin_versions,
115
+ config_dir="~/.algo_cli (or ALGO_CLI_CONFIG_DIR)",
116
+ updated_at=time.time(),
117
+ )
118
+
119
+
120
+ def _safe_get_knowledge_graph_info() -> dict[str, Any]:
121
+ """Alias for _safe_get_kg_info to avoid name collision."""
122
+ return _safe_get_kg_info()
123
+
124
+
125
+ def save_manifest() -> VersionManifest:
126
+ """Build and persist the version manifest to disk."""
127
+ manifest = build_manifest()
128
+ try:
129
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
130
+ VERSIONS_FILE.write_text(manifest.to_json(), encoding="utf-8")
131
+ except OSError as exc:
132
+ # Don't crash if we can't write — just log
133
+ import logging
134
+ logging.getLogger(__name__).warning("Could not save version manifest: %s", exc)
135
+ return manifest
136
+
137
+
138
+ def load_manifest() -> VersionManifest | None:
139
+ """Load a previously saved version manifest from disk."""
140
+ try:
141
+ data = json.loads(VERSIONS_FILE.read_text(encoding="utf-8"))
142
+ return VersionManifest(**data)
143
+ except (FileNotFoundError, json.JSONDecodeError, TypeError):
144
+ return None
145
+
146
+
147
+ def format_version_string(manifest: VersionManifest | None = None) -> str:
148
+ """Format a human-readable version string showing full system state."""
149
+ if manifest is None:
150
+ manifest = build_manifest()
151
+
152
+ lines = [
153
+ f"Algo CLI v{manifest.cli_version}",
154
+ f" Python: {manifest.python_version}",
155
+ f" Platform: {manifest.platform}",
156
+ f" Config dir: {manifest.config_dir}",
157
+ ]
158
+
159
+ if manifest.harness_record_count > 0:
160
+ lines.append(
161
+ f" Harness: {manifest.harness_record_count} records"
162
+ f" (index v{manifest.harness_index_version or 'unknown'})"
163
+ )
164
+ if manifest.harness_embed_model:
165
+ lines.append(f" Embed model: {manifest.harness_embed_model}")
166
+
167
+ if manifest.knowledge_graph_node_count > 0:
168
+ lines.append(
169
+ f" KG: {manifest.knowledge_graph_node_count} nodes"
170
+ f" (v{manifest.knowledge_graph_version or 'unknown'})"
171
+ )
172
+
173
+ if manifest.plugins:
174
+ lines.append(" Plugins:")
175
+ for name, version in sorted(manifest.plugins.items()):
176
+ lines.append(f" {name} v{version}")
177
+
178
+ return "\n".join(lines)
@@ -0,0 +1,76 @@
1
+ """Screenshot-as-verification helpers (I6).
2
+
3
+ These helpers do not call a vision model themselves. They validate the textual
4
+ result returned by ``vision_describe`` against expected/forbidden UI evidence,
5
+ so screenshots become structured test artifacts instead of informal notes.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ScreenshotVerification:
14
+ """Structured result for screenshot description verification."""
15
+
16
+ passed: bool
17
+ expected_terms: tuple[str, ...]
18
+ missing_terms: tuple[str, ...]
19
+ forbidden_terms: tuple[str, ...]
20
+ forbidden_hits: tuple[str, ...]
21
+ coverage: float
22
+ summary: str
23
+
24
+ def to_dict(self) -> dict:
25
+ return {
26
+ "passed": self.passed,
27
+ "expected_terms": list(self.expected_terms),
28
+ "missing_terms": list(self.missing_terms),
29
+ "forbidden_terms": list(self.forbidden_terms),
30
+ "forbidden_hits": list(self.forbidden_hits),
31
+ "coverage": round(self.coverage, 3),
32
+ "summary": self.summary,
33
+ }
34
+
35
+
36
+ def _contains_term(text: str, term: str) -> bool:
37
+ return term.strip().lower() in text.lower()
38
+
39
+
40
+ def verify_screenshot_description(
41
+ description: str,
42
+ expected_terms: list[str] | tuple[str, ...] = (),
43
+ forbidden_terms: list[str] | tuple[str, ...] = (),
44
+ ) -> ScreenshotVerification:
45
+ """Verify a screenshot description against expected and forbidden evidence.
46
+
47
+ Args:
48
+ description: Text returned by a vision/OCR model.
49
+ expected_terms: Terms that should appear in the description.
50
+ forbidden_terms: Terms that must not appear (for example, "error").
51
+
52
+ Returns:
53
+ A structured verification result suitable for tests or reports.
54
+ """
55
+ expected = tuple(term.strip() for term in expected_terms if term and term.strip())
56
+ forbidden = tuple(term.strip() for term in forbidden_terms if term and term.strip())
57
+ missing = tuple(term for term in expected if not _contains_term(description or "", term))
58
+ hits = tuple(term for term in forbidden if _contains_term(description or "", term))
59
+ coverage = 1.0 if not expected else (len(expected) - len(missing)) / len(expected)
60
+ passed = not missing and not hits
61
+ summary = (
62
+ f"screenshot verification {'passed' if passed else 'failed'}: "
63
+ f"coverage={coverage:.2f}, missing={len(missing)}, forbidden_hits={len(hits)}"
64
+ )
65
+ return ScreenshotVerification(
66
+ passed=passed,
67
+ expected_terms=expected,
68
+ missing_terms=missing,
69
+ forbidden_terms=forbidden,
70
+ forbidden_hits=hits,
71
+ coverage=coverage,
72
+ summary=summary,
73
+ )
74
+
75
+
76
+ __all__ = ["ScreenshotVerification", "verify_screenshot_description"]
@@ -0,0 +1,68 @@
1
+ """Resolve an agent session cwd from generic project hints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import shlex
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ _WIN_DRIVE_ABS = re.compile(r"^(?:\\\\\?\\)?[A-Za-z]:[\\/]")
12
+
13
+
14
+ def parse_path_arg(arg: str) -> str:
15
+ """Parse a single path from a slash-command argument (handles quoted Windows paths)."""
16
+ text = (arg or "").strip()
17
+ if not text:
18
+ return ""
19
+ # shlex treats backslash as escape (POSIX rules), so a Windows absolute path loses separators.
20
+ if _WIN_DRIVE_ABS.match(text) or (len(text) >= 2 and text[1] == ":" and text[0].isalpha()):
21
+ return text.strip('"').strip("'")
22
+ if text[0] in "\"'":
23
+ try:
24
+ parts = shlex.split(text)
25
+ return parts[0] if parts else ""
26
+ except ValueError:
27
+ return text.strip('"').strip("'")
28
+ try:
29
+ parts = shlex.split(text)
30
+ except ValueError:
31
+ parts = [text.strip('"').strip("'")]
32
+ return parts[0] if parts else ""
33
+
34
+ _ALGO_CLI_PATH = re.compile(r"\balgo_cli/", re.IGNORECASE)
35
+ _ALGO_CLI_NAME = re.compile(r"\b(?:ollama-cli|algo-cli)\b", re.IGNORECASE)
36
+
37
+
38
+ def candidate_workspaces(task: str) -> list[Path]:
39
+ text = task or ""
40
+ candidates: list[Path] = []
41
+ if _ALGO_CLI_PATH.search(text) or _ALGO_CLI_NAME.search(text):
42
+ candidates.append(Path.home() / "algo-cli")
43
+ legacy = Path.home() / "ollama-cli"
44
+ if legacy.is_dir() and legacy != candidates[-1]:
45
+ candidates.append(legacy)
46
+ return candidates
47
+
48
+
49
+ def _is_valid_workspace(root: Path) -> bool:
50
+ if not root.is_dir():
51
+ return False
52
+ return (root / "algo_cli").is_dir()
53
+
54
+
55
+ def resolve_agent_workspace(task: str, cfg: Any) -> bool:
56
+ """Point cfg.cwd at a known project root inferred from the task. Returns True if updated."""
57
+ seen: set[str] = set()
58
+ for root in candidate_workspaces(task):
59
+ key = str(root)
60
+ if key in seen:
61
+ continue
62
+ seen.add(key)
63
+ if _is_valid_workspace(root):
64
+ cfg.cwd = str(root.resolve())
65
+ if hasattr(cfg, "save"):
66
+ cfg.save()
67
+ return True
68
+ return False