fixing skill roots

frdel committed Feb 5, 2026 at 19:05 UTC 84c06592c76a8a533d586c43446a73eb22f15ae4
3 files changed +75 -51
python/helpers/projects.py
+59 -41
@@ -1,5 +1,5 @@
1 import os
2 -from typing import Literal, TypedDict, TYPE_CHECKING
2 +from typing import Literal, TypedDict, TYPE_CHECKING, cast
3
4 from python.helpers import files, dirty_json, persist_chat, file_tree
5 from python.helpers.print_style import PrintStyle
@@ -106,7 +106,7 @@ def clone_git_project(name: str, git_url: str, git_token: str, data: BasicProjec
106 meta_path = os.path.join(abs_path, PROJECT_META_DIR, PROJECT_HEADER_FILE)
107 if os.path.exists(meta_path):
108 # Merge: keep cloned content, override only user-specified fields
109 - cloned_header = dirty_json.parse(files.read_file(meta_path))
109 + cloned_header: BasicProjectData = dirty_json.parse(files.read_file(meta_path)) # type: ignore
110 cloned_header["title"] = data.get("title") or cloned_header.get("title", "")
111 cloned_header["color"] = data.get("color") or cloned_header.get("color", "")
112 cloned_header["git_url"] = clean_url
@@ -151,48 +151,63 @@ def _default_file_structure_settings():
151 )
152
153
154 -def _normalizeBasicData(data: BasicProjectData):
155 - return BasicProjectData(
156 - title=data.get("title", ""),
157 - description=data.get("description", ""),
158 - instructions=data.get("instructions", ""),
159 - color=data.get("color", ""),
160 - git_url=data.get("git_url", ""),
161 - memory=data.get("memory", "own"),
162 - file_structure=data.get(
154 +def _normalizeBasicData(data: BasicProjectData) -> BasicProjectData:
155 + return {
156 + "title": data.get("title", ""),
157 + "description": data.get("description", ""),
158 + "instructions": data.get("instructions", ""),
159 + "color": data.get("color", ""),
160 + "git_url": data.get("git_url", ""),
161 + "memory": data.get("memory", "own"),
162 + "file_structure": data.get(
163 "file_structure",
164 _default_file_structure_settings(),
165 ),
166 - )
166 + }
167
168
169 -def _normalizeEditData(data: EditProjectData):
170 - return EditProjectData(
171 - name=data.get("name", ""),
172 - title=data.get("title", ""),
173 - description=data.get("description", ""),
174 - instructions=data.get("instructions", ""),
175 - variables=data.get("variables", ""),
176 - color=data.get("color", ""),
177 - git_status=data.get("git_status", {"is_git_repo": False}),
178 - instruction_files_count=data.get("instruction_files_count", 0),
179 - knowledge_files_count=data.get("knowledge_files_count", 0),
180 - secrets=data.get("secrets", ""),
181 - memory=data.get("memory", "own"),
182 - file_structure=data.get(
169 +def _normalizeEditData(data: EditProjectData) -> EditProjectData:
170 + normalized: EditProjectData = {
171 + "name": data.get("name", ""),
172 + "title": data.get("title", ""),
173 + "description": data.get("description", ""),
174 + "instructions": data.get("instructions", ""),
175 + "variables": data.get("variables", ""),
176 + "color": data.get("color", ""),
177 + "git_url": data.get("git_url", ""),
178 + "git_status": data.get("git_status", {"is_git_repo": False}),
179 + "instruction_files_count": data.get("instruction_files_count", 0),
180 + "knowledge_files_count": data.get("knowledge_files_count", 0),
181 + "secrets": data.get("secrets", ""),
182 + "memory": data.get("memory", "own"),
183 + "file_structure": data.get(
184 "file_structure",
185 _default_file_structure_settings(),
186 ),
186 - subagents=data.get("subagents", {}),
187 - )
187 + "subagents": data.get("subagents", {}),
188 + }
189 + return normalized
190
191
192 def _edit_data_to_basic_data(data: EditProjectData):
193 return _normalizeBasicData(data)
194
195
194 -def _basic_data_to_edit_data(data: BasicProjectData):
195 - return _normalizeEditData(data) # type: ignore
196 +def _basic_data_to_edit_data(data: BasicProjectData) -> EditProjectData:
197 + base: EditProjectData = cast(
198 + EditProjectData,
199 + {
200 + **data,
201 + "name": "",
202 + "instruction_files_count": 0,
203 + "knowledge_files_count": 0,
204 + "variables": "",
205 + "secrets": "",
206 + "subagents": {},
207 + "git_status": {"is_git_repo": False},
208 + },
209 + )
210 + return _normalizeEditData(base)
211
212
213 def update_project(name: str, data: EditProjectData):
@@ -215,7 +230,7 @@ def update_project(name: str, data: EditProjectData):
230
231
232 def load_basic_project_data(name: str) -> BasicProjectData:
218 - data = BasicProjectData(**load_project_header(name))
233 + data = cast(BasicProjectData, load_project_header(name))
234 normalized = _normalizeBasicData(data)
235 return normalized
236
@@ -229,17 +244,20 @@ def load_edit_project_data(name: str) -> EditProjectData:
244 secrets = load_project_secrets_masked(name)
245 subagents = load_project_subagents(name)
246 knowledge_files_count = get_knowledge_files_count(name)
232 - git_status = git.get_repo_status(get_project_folder(name))
247 + git_status = cast(GitStatusData, git.get_repo_status(get_project_folder(name)))
248
234 - data = EditProjectData(
235 - **data,
236 - name=name,
237 - instruction_files_count=len(additional_instructions),
238 - knowledge_files_count=knowledge_files_count,
239 - variables=variables,
240 - secrets=secrets,
241 - subagents=subagents,
242 - git_status=git_status,
249 + data = cast(
250 + EditProjectData,
251 + {
252 + **data,
253 + "name": name,
254 + "instruction_files_count": len(additional_instructions),
255 + "knowledge_files_count": knowledge_files_count,
256 + "variables": variables,
257 + "secrets": secrets,
258 + "subagents": subagents,
259 + "git_status": git_status,
260 + },
261 )
262 data = _normalizeEditData(data)
263 return data
python/helpers/skills.py
+13 -7
@@ -50,11 +50,18 @@ def get_skill_roots(
50 paths = subagents.get_paths(agent, "skills")
51 else:
52 # skill roots available globally
53 - pr_ag = files.find_existing_paths_by_pattern("projects/*/.a0proj/agents/*/skills") # agents in projects
54 - projects = files.find_existing_paths_by_pattern("projects/*/.a0proj/skills") # projects
53 + project_agents = files.find_existing_paths_by_pattern("usr/projects/*/.a0proj/agents/*/skills") # agents in projects
54 + projects = files.find_existing_paths_by_pattern("usr/projects/*/.a0proj/skills") # projects
55 + usr_agents = files.find_existing_paths_by_pattern("usr/agents/*/skills") # agents
56 agents = files.find_existing_paths_by_pattern("agents/*/skills") # agents
56 - paths = [files.get_abs_path("skills"), files.get_abs_path("usr/skills")] + pr_ag + projects + agents # full scope
57 -
57 + paths = [
58 + files.get_abs_path("skills"),
59 + files.get_abs_path("usr/skills"),
60 + *project_agents,
61 + *projects,
62 + *usr_agents,
63 + *agents
64 + ]
65 return paths
66
67
@@ -364,10 +371,9 @@ def load_skill_for_agent(
371 return f"Error: skill '{skill_name}' not found"
372
373 # Get runtime path
367 - if agent and agent.config.code_exec_ssh_enabled:
374 + runtime_path = str(skill.path)
375 + if runtime.is_development():
376 runtime_path = files.normalize_a0_path(str(skill.path))
369 - else:
370 - runtime_path = str(skill.path)
377
378 lines = [f"Skill: {skill.name}", f"Path: {runtime_path}"]
379
webui/components/settings/skills/list.html
+3 -3
@@ -18,10 +18,10 @@
18 <div class="skills-list-controls">
19 <div class="field">
20 <div class="field-label">
21 - <div class="field-title">Project scope</div>
22 - <div class="field-description">
21 + <div class="field-title">Filter by project</div>
22 + <!-- <div class="field-description">
23 Includes global skills plus the selected project.
24 - </div>
24 + </div> -->
25 </div>
26 <div class="field-control">
27 <select x-model="$store.skillsListStore.projectName"