skills cleanup, minor fixes

removed frameworks standardized directories cleanup of prompt injection fix of chat reset

frdel committed Feb 2, 2026 at 19:03 UTC 724e06ae9d843a84d4c5cdc652813570d280e41a
22 files changed +83 -142
.dockerignore
+2 -3
@@ -2,15 +2,14 @@
2 # Project‑specific exclusions / re‑includes
3 ###############################################################################
4
5 -# Large / generated data
5 +# Obsolete
6 memory/**
7 +instruments/**
8
9 # Logs, tmp, usr
10 logs/*
11 tmp/*
12 usr/*
12 -!usr/skills/
13 -!usr/skills/**
13
14 # Knowledge directory – keep only default/
15 knowledge/**
prompts/agent.system.skills.md
+4 -3
@@ -1,5 +1,6 @@
1 -# Relevant Skills (SKILL.md)
2 -- The following Skills may be useful for the current task.
3 -- Use `skills_tool` to list/search/load skills and progressively read supporting files/scripts.
1 +# Available skills
2 +- skills in "**name** description" format
3 +- use skills_tool to load with **skill_name** when relevant
4 +
5
6 {{skills}}
python/api/api_reset_chat.py
+1
@@ -47,6 +47,7 @@ class ApiResetChat(ApiHandler):
47 context.reset()
48 # Save the reset context to persist the changes
49 persist_chat.save_tmp_chat(context)
50 + persist_chat.remove_msg_files(context_id)
51
52 # Log the reset
53 PrintStyle(
python/extensions/message_loop_prompts_after/_55_recall_skills.py deleted
-53
@@ -1,53 +0,0 @@
1 -from python.helpers.extension import Extension
2 -from agent import LoopData
3 -from python.helpers import skills as skills_helper
4 -from python.helpers import projects
5 -
6 -
7 -class RecallSkills(Extension):
8 - """
9 - Surface relevant SKILL.md-based Skills into the prompt (token-efficient).
10 -
11 - Uses lightweight lexical matching and injects a compact
12 - "relevant skills" list into extras for the current user message.
13 - """
14 -
15 - async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
16 - # Only on the first iteration of the message loop (new user instruction)
17 - if loop_data.iteration != 0:
18 - return
19 -
20 - # Determine query from current user message
21 - user_instruction = (
22 - loop_data.user_message.output_text() if loop_data.user_message else ""
23 - ).strip()
24 - if not user_instruction or len(user_instruction) < 8:
25 - return
26 -
27 - # Get active project for scoped discovery
28 - project_name = (
29 - projects.get_context_project_name(self.agent.context)
30 - if self.agent.context
31 - else None
32 - )
33 -
34 - matches = skills_helper.search_skills(
35 - user_instruction,
36 - limit=6,
37 - project_name=project_name,
38 - )
39 - if not matches:
40 - return
41 -
42 - lines = []
43 - for s in matches:
44 - desc = (s.description or "").strip() or "(no description)"
45 - if len(desc) > 220:
46 - desc = desc[:220].rstrip() + "…"
47 - lines.append(f"- {s.name} [{s.source}]: {desc}")
48 -
49 - skills_block = "\n".join(lines)
50 - loop_data.extras_temporary["skills"] = self.agent.parse_prompt(
51 - "agent.system.skills.md", skills=skills_block
52 - )
53 -
python/extensions/system_prompt/_10_system_prompt.py
+14 -2
@@ -3,7 +3,7 @@ from python.helpers.extension import Extension
3 from python.helpers.mcp_handler import MCPConfig
4 from agent import Agent, LoopData
5 from python.helpers.settings import get_settings
6 -from python.helpers import projects
6 +from python.helpers import projects, skills
7
8
9 class SystemPrompt(Extension):
@@ -18,6 +18,7 @@ class SystemPrompt(Extension):
18 main = get_main_prompt(self.agent)
19 tools = get_tools_prompt(self.agent)
20 mcp_tools = get_mcp_tools_prompt(self.agent)
21 + skills = get_skills_prompt(self.agent)
22 secrets_prompt = get_secrets_prompt(self.agent)
23 project_prompt = get_project_prompt(self.agent)
24
@@ -25,11 +26,13 @@ class SystemPrompt(Extension):
26 system_prompt.append(tools)
27 if mcp_tools:
28 system_prompt.append(mcp_tools)
29 + if skills:
30 + system_prompt.append(skills)
31 if secrets_prompt:
32 system_prompt.append(secrets_prompt)
33 if project_prompt:
34 system_prompt.append(project_prompt)
32 -
35 +
36
37 def get_main_prompt(agent: Agent):
38 return agent.read_prompt("agent.system.main.md")
@@ -81,4 +84,13 @@ def get_project_prompt(agent: Agent):
84 result += "\n\n" + agent.read_prompt("agent.system.projects.inactive.md")
85 return result
86
87 +def get_skills_prompt(agent: Agent):
88 + available = skills.list_skills(agent)
89 + result = []
90 + for skill in available:
91 + name = skill.name.strip().replace("\n", " ")[:100]
92 + descr = skill.description.replace("\n", " ")[:500]
93 + result.append(f"**{name}** {descr}")
94
95 + if result:
96 + return agent.read_prompt("agent.system.skills.md", skills="\n".join(result))
python/helpers/files.py
+13 -3
@@ -8,7 +8,7 @@ import re
8 import base64
9 import shutil
10 import tempfile
11 -from typing import Any
11 +from typing import Any, Literal
12 import zipfile
13 import importlib
14 import importlib.util
@@ -352,7 +352,7 @@ def find_file_in_dirs(_filename: str, _directories: list[str]):
352 )
353
354
355 -def get_unique_filenames_in_dirs(dir_paths: list[str], pattern: str = "*"):
355 +def get_unique_filenames_in_dirs(dir_paths: list[str], type: Literal["file", "dir", "any"] = "file", pattern: str = "*"):
356 # returns absolute paths for unique filenames, priority by order in dir_paths
357 seen = set()
358 result = []
@@ -360,7 +360,7 @@ def get_unique_filenames_in_dirs(dir_paths: list[str], pattern: str = "*"):
360 full_dir = get_abs_path(dir_path)
361 for file_path in glob.glob(os.path.join(full_dir, pattern)):
362 fname = os.path.basename(file_path)
363 - if fname not in seen and os.path.isfile(file_path):
363 + if fname not in seen and (type == "any" or (type == "file" and os.path.isfile(file_path)) or (type == "dir" and os.path.isdir(file_path))):
364 seen.add(fname)
365 result.append(get_abs_path(file_path))
366 # sort by filename (basename), not the full path
@@ -368,6 +368,16 @@ def get_unique_filenames_in_dirs(dir_paths: list[str], pattern: str = "*"):
368 return result
369
370
371 +def find_existing_paths_by_pattern(pattern: str):
372 + if not pattern:
373 + return []
374 +
375 + search_pattern = get_abs_path(pattern)
376 + matches = glob.glob(search_pattern, recursive=True)
377 + matches.sort()
378 + return matches
379 +
380 +
381 def remove_code_fences(text):
382 # Pattern to match code fences with optional language specifier
383 pattern = r"(```|~~~)(.*?\n)(.*?)(\1)"
python/helpers/migration.py
-6
@@ -19,7 +19,6 @@ def migrate_user_data() -> None:
19 _move_dir("tmp/downloads", "usr/downloads")
20 _move_dir("tmp/email", "usr/email")
21 _move_dir("knowledge/custom", "usr/knowledge", overwrite=True)
22 - _move_dir("skills/custom", "usr/skills/custom", overwrite=True)
22
23 # --- Migrate Files -------------------------------------------------------------
24 # Move specific configuration files to usr/
@@ -37,8 +36,6 @@ def migrate_user_data() -> None:
36 # We use _merge_dir_contents because we want to move the *contents* of default/
37 # into the parent directory, not move the default directory itself.
38 _merge_dir_contents("knowledge/default", "knowledge")
40 - _merge_dir_contents("skills/default", "usr/skills/default")
41 - _merge_dir_contents("skills/builtin", "usr/skills/default")
39
40 # --- Cleanup -------------------------------------------------------------------
41
@@ -105,9 +102,6 @@ def _cleanup_obsolete() -> None:
102 """
103 to_remove = [
104 "knowledge/default",
108 - "skills/default",
109 - "skills/builtin",
110 - "skills",
105 "memory"
106 ]
107 for path in to_remove:
python/helpers/skills.py
+36 -57
@@ -4,9 +4,12 @@ import os
4 import re
5 from dataclasses import dataclass, field
6 from pathlib import Path
7 -from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple
7 +from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple, TYPE_CHECKING
8
9 -from python.helpers import files
9 +from python.helpers import files, subagents
10 +
11 +if TYPE_CHECKING:
12 + from agent import Agent
13
14 try:
15 import yaml # type: ignore
@@ -14,17 +17,12 @@ except Exception: # pragma: no cover
17 yaml = None # type: ignore
18
19
17 -SkillSource = Literal["custom", "default", "project"]
18 -
19 -
20 @dataclass(slots=True)
21 class Skill:
22 name: str
23 description: str
24 path: Path
25 skill_md_path: Path
26 - source: SkillSource
27 -
26 version: str = ""
27 author: str = ""
28 tags: List[str] = field(default_factory=list)
@@ -44,24 +42,20 @@ def get_skills_base_dir() -> Path:
42
43
44 def get_skill_roots(
47 - order: Optional[List[SkillSource]] = None,
48 - project_name: Optional[str] = None,
49 -) -> List[Tuple[SkillSource, Path]]:
50 - base = get_skills_base_dir()
51 - order = order or ["custom", "default"]
52 - roots: List[Tuple[SkillSource, Path]] = [(src, base / src) for src in order]
53 -
54 - # Include project-scoped skills if a project is active
55 - if project_name:
56 - try:
57 - from python.helpers.skills_import import get_project_skills_folder
58 - project_skills = get_project_skills_folder(project_name)
59 - if project_skills.exists():
60 - roots.insert(0, ("project", project_skills))
61 - except Exception:
62 - pass
45 + agent: Agent|None=None,
46 +) -> List[str]:
47 +
48 + if agent:
49 + # skill roots available to agent
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
55 + 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
64 - return roots
58 + return paths
59
60
61 def _is_hidden_path(path: Path) -> bool:
@@ -223,7 +217,6 @@ def parse_frontmatter(frontmatter_text: str) -> Tuple[Dict[str, Any], List[str]]
217
218 def skill_from_markdown(
219 skill_md_path: Path,
226 - source: SkillSource,
220 *,
221 include_content: bool = False,
222 validate: bool = True,
@@ -272,7 +265,6 @@ def skill_from_markdown(
265 description=description,
266 path=skill_dir,
267 skill_md_path=skill_md_path,
275 - source=source,
268 version=version,
269 author=author,
270 tags=tags,
@@ -292,25 +284,21 @@ def skill_from_markdown(
284
285
286 def list_skills(
295 - *,
287 + agent:Agent|None=None,
288 include_content: bool = False,
297 - dedupe: bool = True,
298 - root_order: Optional[List[SkillSource]] = None,
299 - project_name: Optional[str] = None,
289 ) -> List[Skill]:
290 skills: List[Skill] = []
291
303 - roots = get_skill_roots(
304 - order=root_order,
305 - project_name=project_name,
306 - )
307 - for source, root in roots:
308 - for skill_md in discover_skill_md_files(root):
309 - s = skill_from_markdown(skill_md, source, include_content=include_content)
292 + roots = get_skill_roots(agent)
293 +
294 + for root in roots:
295 + for skill_md in discover_skill_md_files(Path(root)):
296 + s = skill_from_markdown(skill_md, include_content=include_content)
297 if s:
298 skills.append(s)
299
313 - if not dedupe:
300 + # no deduplication for global skills
301 + if not agent:
302 return skills
303
304 # Dedupe by normalized name, preserving root_order priority (earlier wins)
@@ -324,22 +312,18 @@ def list_skills(
312
313 def find_skill(
314 skill_name: str,
327 - *,
315 + agent:Agent|None=None,
316 include_content: bool = False,
329 - root_order: Optional[List[SkillSource]] = None,
330 - project_name: Optional[str] = None,
317 ) -> Optional[Skill]:
318 target = _normalize_name(skill_name)
319 if not target:
320 return None
321
336 - roots = get_skill_roots(
337 - order=root_order,
338 - project_name=project_name,
339 - )
340 - for source, root in roots:
341 - for skill_md in discover_skill_md_files(root):
342 - s = skill_from_markdown(skill_md, source, include_content=include_content)
322 + roots = get_skill_roots(agent)
323 +
324 + for root in roots:
325 + for skill_md in discover_skill_md_files(Path(root)):
326 + s = skill_from_markdown(skill_md, include_content=include_content)
327 if not s:
328 continue
329 if _normalize_name(s.name) == target or _normalize_name(s.path.name) == target:
@@ -349,20 +333,15 @@ def find_skill(
333
334 def search_skills(
335 query: str,
352 - *,
336 limit: int = 25,
354 - project_name: Optional[str] = None,
337 + agent: Agent|None=None,
338 ) -> List[Skill]:
339 q = (query or "").strip().lower()
340 if not q:
341 return []
342
343 terms = [t for t in re.split(r"\s+", q) if t]
361 - candidates = list_skills(
362 - include_content=False,
363 - dedupe=True,
364 - project_name=project_name,
365 - )
344 + candidates = list_skills(agent)
345
346 scored: List[Tuple[int, Skill]] = []
347 for s in candidates:
@@ -419,7 +398,7 @@ def validate_skill(skill: Skill) -> List[str]:
398 return issues
399
400
422 -def validate_skill_md(skill_md_path: Path, source: SkillSource) -> List[str]:
401 +def validate_skill_md(skill_md_path: Path) -> List[str]:
402 try:
403 text = _read_text(skill_md_path)
404 except Exception:
@@ -430,7 +409,7 @@ def validate_skill_md(skill_md_path: Path, source: SkillSource) -> List[str]:
409 return fm_errors
410
411 skill = skill_from_markdown(
433 - skill_md_path, source, include_content=False, validate=False
412 + skill_md_path, include_content=False, validate=False
413 )
414 if not skill:
415 return ["Unable to parse SKILL.md frontmatter"]
python/tools/skills_tool.py
+13 -14
@@ -22,10 +22,6 @@ class SkillsTool(Tool):
22 Script execution is handled by code_execution_tool directly.
23 """
24
25 - def _get_project_name(self) -> str | None:
26 - ctx = getattr(self.agent, "context", None)
27 - return projects.get_context_project_name(ctx) if ctx else None
28 -
25 async def execute(self, **kwargs) -> Response:
26 method = (
27 (kwargs.get("method") or self.args.get("method") or self.method or "")
@@ -60,8 +56,7 @@ class SkillsTool(Tool):
56 def _list(self) -> str:
57 skills = skills_helper.list_skills(
58 include_content=False,
63 - dedupe=True,
64 - project_name=self._get_project_name(),
59 + agent=self.agent,
60 )
61 if not skills:
62 return (
@@ -70,7 +65,7 @@ class SkillsTool(Tool):
65 )
66
67 # Stable output: sort by name
73 - skills_sorted = sorted(skills, key=lambda s: (s.name.lower(), s.source))
68 + skills_sorted = sorted(skills, key=lambda s: s.name.lower())
69
70 lines: List[str] = []
71 lines.append(f"Available skills ({len(skills_sorted)}):")
@@ -80,7 +75,7 @@ class SkillsTool(Tool):
75 desc = (s.description or "").strip()
76 if len(desc) > 200:
77 desc = desc[:200].rstrip() + "…"
83 - lines.append(f"- {s.name}{ver} [{s.source}]{tags}: {desc}")
78 + lines.append(f"- {s.name}{ver}{tags}: {desc}")
79 lines.append("")
80 lines.append("Tip: use skills_tool method=search or method=load for details.")
81 return "\n".join(lines)
@@ -92,7 +87,7 @@ class SkillsTool(Tool):
87 results = skills_helper.search_skills(
88 query,
89 limit=25,
95 - project_name=self._get_project_name(),
90 + agent=self.agent,
91 )
92 if not results:
93 return f"No skills matched query: {query!r}"
@@ -103,19 +98,24 @@ class SkillsTool(Tool):
98 desc = (s.description or "").strip()
99 if len(desc) > 200:
100 desc = desc[:200].rstrip() + "…"
106 - lines.append(f"- {s.name} [{s.source}]: {desc}")
101 + lines.append(f"- {s.name}: {desc}")
102 lines.append("")
103 lines.append("Tip: use skills_tool method=load skill_name=<name> to load full instructions.")
104 return "\n".join(lines)
105
106 def _load(self, skill_name: str) -> str:
107 +
108 + skill_name = skill_name.strip()
109 + if skill_name.startswith("**") and skill_name.endswith("**"):
110 + skill_name = skill_name[2:-2] # remove markdown bold markers if used by agent
111 +
112 if not skill_name:
113 - return "Error: 'skill_name' is required for method=load."
113 + return "Error: 'skill_name' is required for method=load."
114
115 skill = skills_helper.find_skill(
116 skill_name,
117 include_content=True,
118 - project_name=self._get_project_name(),
118 + agent=self.agent,
119 )
120 if not skill:
121 return f"Error: skill not found: {skill_name!r}. Try skills_tool method=list or method=search."
@@ -130,7 +130,6 @@ class SkillsTool(Tool):
130
131 lines: List[str] = []
132 lines.append(f"Skill: {skill.name}")
133 - lines.append(f"Source: {skill.source}")
133 lines.append(f"Path: {rel_skill_dir}")
134 lines.append(f"Runtime path: {runtime_path}")
135 if skill.version:
@@ -176,7 +175,7 @@ class SkillsTool(Tool):
175 skill = skills_helper.find_skill(
176 skill_name,
177 include_content=False,
179 - project_name=self._get_project_name(),
178 + agent=self.agent,
179 )
180 if not skill:
181 return f"Error: skill not found: {skill_name!r}."
skills/api-development/SKILL.md renamed
skills/brainstorming/SKILL.md renamed
skills/code-review/SKILL.md renamed
skills/create-skill/SKILL.md renamed
skills/database-design/SKILL.md renamed
skills/debugging/SKILL.md renamed
skills/docker-devops/SKILL.md renamed
skills/git-workflow/SKILL.md renamed
skills/prompt-engineering/SKILL.md renamed
skills/security-audit/SKILL.md renamed
skills/tdd/SKILL.md renamed
usr/skills/.gitkeep renamed
webui/index.js
-1
@@ -317,7 +317,6 @@ export async function applySnapshot(snapshot, options = {}) {
317 if (lastLogGuid) {
318 const chatHistoryEl = document.getElementById("chat-history");
319 if (chatHistoryEl) chatHistoryEl.innerHTML = "";
320 - msgs.resetProcessGroups(); // Reset process groups on chat reset
320 lastLogVersion = 0;
321 lastLogGuid = snapshot.log_guid;
322 if (typeof onLogGuidReset === "function") {