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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- 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
|