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") {