Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ms_agent/agent_hub/_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,8 @@ def _is_private_file(product: str, path: str) -> bool:
'openclaw': 'MEMORY.md',
'qwenpaw': 'MEMORY.md',
'hermes': 'memories/MEMORY.md',
'openhuman': 'MEMORY.md'
'openhuman': 'MEMORY.md',
'qoder': 'memory/MEMORY.md'
},
{
'openclaw': 'IDENTITY.md',
Expand Down
14 changes: 13 additions & 1 deletion ms_agent/agent_hub/frameworks/qoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class QoderWorkspace(WorkspaceSpec):
Qoder keeps user-level config at ``~/.qoder`` (project-level config lives in
a project's ``.qoder/`` directory; point ``--local_dir`` at it to upload
that instead). A sub-agent is one Markdown file ``agents/<name>.md``; skills,
commands, rules and ``AGENTS.md`` are shared across sub-agents.
commands, rules, ``AGENTS.md`` and the user-level auto memory (``memory/``)
are shared across sub-agents -- Qoder memory is scoped per user / per
project, never per agent.
"""

@property
Expand All @@ -30,11 +32,21 @@ def default_root(self) -> Path:

@property
def patterns(self) -> list[str]:
# ``memory/`` is the Qoder CLI user-level auto-memory root
# (``MEMORY.md`` index + topic ``.md`` files, per the official CLI
# docs). The project-level auto memory under
# ``projects/<encoded-workspace-path>/memory/`` is deliberately NOT
# collected: the directory name is a machine-specific encoding of the
# workspace path, and several projects each carry their own
# ``MEMORY.md`` -- flattening them into the single cross-framework
# memory slot would overwrite one project's memory with another's.
return [
'AGENTS.md',
'agents/{name}.md',
'commands/*.md',
'rules/*.md',
'memory/MEMORY.md',
'memory/*.md',
'skills/*/SKILL.md',
'skills/*/scripts/*',
'skills/*/references/*',
Expand Down
4 changes: 3 additions & 1 deletion tests/agent_hub/test_agent_frameworks.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ def _to_bytes(files: dict) -> dict:
"agents/code-reviewer.md": "# Code Reviewer\nReview code for bugs and style.\n",
"commands/review.md": "# /review\nTrigger a code review on the current file.\n",
"rules/style-guide.md": "# Style Guide\nUse 4-space indentation for Python.\n",
"memory/MEMORY.md": "# Memory Index\n\n- [User language](user-language.md) — respond in Chinese\n",
"memory/user-language.md": "---\nname: user-language\nmetadata:\n type: user\n---\n\nUser speaks Chinese.\n",
"skills/lint/SKILL.md": "# Lint\nRun linters on the codebase.\n",
"skills/lint/scripts/run_lint.sh": "# lint runner\nrun flake8 on all project files\n",
}
Expand Down Expand Up @@ -452,7 +454,7 @@ def test_23_framework_structure(self):
"qwenpaw": ["PROFILE.md", "BOOTSTRAP.md", "memory/story-notes.md"],
"hermes": ["memories/USER.md"],
"openhuman": ["SOUL.md", "IDENTITY.md", "HEARTBEAT.md", "wiki/interests.md"],
"qoder": ["agents/code-reviewer.md", "commands/review.md", "rules/style-guide.md"],
"qoder": ["agents/code-reviewer.md", "commands/review.md", "rules/style-guide.md", "memory/MEMORY.md"],
}

for fw, markers in framework_markers.items():
Expand Down
34 changes: 34 additions & 0 deletions tests/agent_hub/test_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,20 @@ def test_cross_product_user_md(self):

def test_cross_product_memory_md(self):
self.assertEqual(_resolve_target_path("nanobot", "memory/MEMORY.md", "openclaw"), "MEMORY.md")
# qoder user-level auto memory joins the same MEMORY.md group.
self.assertEqual(
_resolve_target_path("qoder", "memory/MEMORY.md", "hermes"),
"memories/MEMORY.md")
self.assertEqual(
_resolve_target_path("qoder", "memory/MEMORY.md", "openclaw"),
"MEMORY.md")
self.assertEqual(
_resolve_target_path("hermes", "memories/MEMORY.md", "qoder"),
"memory/MEMORY.md")
# ms-agent has no memory slot, so qoder memory has no semantic target
# there either (folds into the catch-all instead).
self.assertIsNone(
_resolve_target_path("qoder", "memory/MEMORY.md", "ms-agent"))

def test_cross_product_ms_agent_profile(self):
# ms-agent PROFILE.md -> qwenpaw maps to memory/USER.md (USER group).
Expand Down Expand Up @@ -303,6 +317,26 @@ def test_same_product_imports_directly(self):
self.assertIn("SOUL.md", result.merged_files)
self.assertEqual(result.merged_files["SOUL.md"], "my soul")

def test_qoder_memory_maps_and_topic_files_pass_through(self):
"""Cross-framework, qoder's user-level memory index maps onto the
target's MEMORY.md slot while topic files keep their original path
(kept only if the target spec accepts it); both travel verbatim --
memory is user data, never rebased onto a target template."""
result = merge_resources(
incoming={
"memory/MEMORY.md": "# Memory Index\n\n- [t](t.md) — x\n",
"memory/t.md": "---\nname: t\n---\n\ntopic body\n",
},
source_product="qoder",
target_product="openclaw",
source_defaults={},
target_defaults={},
)
self.assertEqual(result.merged_files["MEMORY.md"],
"# Memory Index\n\n- [t](t.md) — x\n")
self.assertEqual(result.merged_files["memory/t.md"],
"---\nname: t\n---\n\ntopic body\n")

def test_fills_missing_from_target_defaults(self):
"""merge_resources fills target defaults for absent source files."""
result = merge_resources(
Expand Down
22 changes: 22 additions & 0 deletions tests/agent_hub/test_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,28 @@ def test_qoder_collects_named_agent_plus_shared(self):
self.assertIn("skills/x/SKILL.md", collected)
self.assertNotIn("agents/other.md", collected)

def test_qoder_collects_user_level_memory_not_project_level(self):
"""Qoder CLI auto memory is user-level (``memory/``) plus
project-level (``projects/<encoded-workspace>/memory/``). Only the
user-level root is portable: the project directory name is a
machine-specific encoding of the workspace path and several projects
each carry a ``MEMORY.md`` that would overwrite one another in the
single cross-framework memory slot."""
(self.root / "memory").mkdir()
(self.root / "memory" / "MEMORY.md").write_text("# Memory Index\n")
(self.root / "memory" / "user-language.md").write_text("topic\n")
proj_mem = self.root / "projects" / "-Users-test-demo" / "memory"
proj_mem.mkdir(parents=True)
(proj_mem / "MEMORY.md").write_text("# Project Memory Index\n")

spec = QoderWorkspace(agent_name="default", local_dir=self.root)
collected = spec.collect()

self.assertIn("memory/MEMORY.md", collected)
self.assertIn("memory/user-language.md", collected)
self.assertNotIn(
"projects/-Users-test-demo/memory/MEMORY.md", collected)

def test_hermes_excludes_framework_skills_keeps_user_skills(self):
"""hermes collect drops bundled/framework skills (identified by a
builtin_skill_version / metadata.copaw frontmatter marker or a
Expand Down
Loading