diff --git a/architecture/features/agent-integration.md b/architecture/features/agent-integration.md index cc08ddf4..f2fdcd47 100644 --- a/architecture/features/agent-integration.md +++ b/architecture/features/agent-integration.md @@ -149,7 +149,8 @@ Without this feature, users would need to manually create and maintain agent-spe 2. - `p1` - **IF** `--agent` flag provided, filter to single agent - `inst-if-filter` 3. - `p1` - **RETURN** list of agents to generate for - `inst-return-agents` 4. [x] - `p1` - Resolve config/kits/ directory and registered kit dirs from core.toml for workflow/skill discovery - `inst-resolve-kits` -5. [x] - `p1` - Parse CLI arguments, resolve project root, studio root, load agent config (shared context for agents commands) - `inst-resolve-context` +5. [x] - `p1` - Registered kit path resolution honors register-mode entries persisted as `..` when the resolved path stays inside the project root, so same-project canonical kits remain discoverable to `generate-agents` - `inst-resolve-register-project-root-kit` +6. [x] - `p1` - Parse CLI arguments, resolve project root, studio root, load agent config (shared context for agents commands) - `inst-resolve-context` **Supporting**: - [x] - `p1` - Module-level constant for all recognized agent names - `inst-define-registry-const` diff --git a/skills/studio/scripts/studio/commands/agents.py b/skills/studio/scripts/studio/commands/agents.py index 5fb39de5..94c4be3f 100644 --- a/skills/studio/scripts/studio/commands/agents.py +++ b/skills/studio/scripts/studio/commands/agents.py @@ -2028,6 +2028,7 @@ def _compute_workflow_skill_id(wf_name: str, kit_slug: Optional[str], prefix: st # @cpt-end:cpt-studio-algo-agent-integration-list-workflows:p1:inst-scan-core-workflows +# @cpt-begin:cpt-studio-algo-agent-integration-discover-agents:p1:inst-resolve-register-project-root-kit # @cpt-begin:cpt-studio-algo-agent-integration-discover-agents:p1:inst-resolve-kits def _resolve_registered_project_relative_path( project_root: Path, @@ -2039,7 +2040,6 @@ def _resolve_registered_project_relative_path( not normalized or path_obj.is_absolute() or PureWindowsPath(normalized).is_absolute() - or ".." in path_obj.parts ): return None resolved = (project_root / Path(normalized)).resolve() @@ -2049,8 +2049,10 @@ def _resolve_registered_project_relative_path( return None return resolved # @cpt-end:cpt-studio-algo-agent-integration-discover-agents:p1:inst-resolve-kits +# @cpt-end:cpt-studio-algo-agent-integration-discover-agents:p1:inst-resolve-register-project-root-kit +# @cpt-begin:cpt-studio-algo-agent-integration-discover-agents:p1:inst-resolve-register-project-root-kit # @cpt-begin:cpt-studio-algo-agent-integration-discover-agents:p1:inst-resolve-kits def _resolve_registered_legacy_studio_path( studio_root: Path, @@ -2059,11 +2061,12 @@ def _resolve_registered_legacy_studio_path( ) -> Optional[Path]: normalized = PurePosixPath(raw_path.strip().replace("\\", "/")).as_posix() path_obj = PurePosixPath(normalized) + has_parent_traversal = ".." in path_obj.parts if ( not normalized or path_obj.is_absolute() or PureWindowsPath(normalized).is_absolute() - or ".." in path_obj.parts + or (has_parent_traversal and normalized != "..") ): return None resolved = (studio_root / Path(normalized)).resolve() @@ -2072,6 +2075,7 @@ def _resolve_registered_legacy_studio_path( except ValueError: return None return resolved +# @cpt-end:cpt-studio-algo-agent-integration-discover-agents:p1:inst-resolve-register-project-root-kit def _registered_kit_dirs(project_root: Optional[Path]) -> Set[str]: diff --git a/tests/test_agents_coverage.py b/tests/test_agents_coverage.py index b59c9551..8e23d1f0 100644 --- a/tests/test_agents_coverage.py +++ b/tests/test_agents_coverage.py @@ -421,6 +421,140 @@ def test_generate_agents_uses_kitmodel_public_skill_and_agent_but_not_rule(self) self.assertFalse((root / ".cursor" / "agents" / "cf-pubkit-codexonly.mdc").exists()) self.assertFalse((root / ".cursor" / "agents" / "cf-pubkit-codex-auditor.mdc").exists()) + def test_generate_agents_supports_register_mode_project_root_kit_path(self): + from studio.commands.agents import _default_agents_config, _process_single_agent + + with TemporaryDirectory() as td: + root = Path(td) / "project" + root.mkdir() + (root / ".git").mkdir() + (root / "AGENTS.md").write_text( + '\n```toml\ncf-studio-path = ".cf-studio"\n```\n\n', + encoding="utf-8", + ) + studio_root = root / ".cf-studio" + (studio_root / ".core" / "skills" / "studio").mkdir(parents=True) + (studio_root / ".core" / "skills" / "studio" / "SKILL.md").write_text( + "---\nname: cf\ndescription: Constructor Studio\n---\nCore skill\n", + encoding="utf-8", + ) + (studio_root / ".core" / "workflows").mkdir(parents=True) + (studio_root / "config").mkdir(parents=True) + (studio_root / "config" / "core.toml").write_text( + "\n".join([ + 'version = "1.0"', + 'project_root = ".."', + "", + "[kits.gears]", + 'format = "CFS"', + 'path = ".."', + 'version = "0.1.0"', + 'install_mode = "register"', + ]) + "\n", + encoding="utf-8", + ) + (root / ".cf-studio-kit.toml").write_text( + "\n".join([ + 'manifest_version = "1.0"', + "", + "[[kits]]", + 'slug = "gears"', + 'name = "Gears"', + 'version = "0.1.0"', + "", + "[[kits.resources]]", + 'id = "workflow_doc_prd"', + 'kind = "skill"', + 'source = "studio-kit-gears/workflows/doc-prd.md"', + 'type = "file"', + "public = true", + ]) + "\n", + encoding="utf-8", + ) + workflow_source = root / "studio-kit-gears" / "workflows" / "doc-prd.md" + workflow_source.parent.mkdir(parents=True) + workflow_source.write_text( + "---\nname: doc-prd\ndescription: Draft PRDs\n---\n# Doc PRD\n", + encoding="utf-8", + ) + + result = _process_single_agent( + "cursor", + root, + studio_root, + _default_agents_config(), + None, + dry_run=False, + ) + + self.assertEqual(result["status"], "PASS") + generated_skill = root / ".agents" / "skills" / "cf-gears-doc-prd" / "SKILL.md" + self.assertTrue(generated_skill.is_file()) + generated_content = generated_skill.read_text(encoding="utf-8") + self.assertIn("cf-gears-doc-prd", generated_content) + self.assertIn("studio-kit-gears/workflows/doc-prd.md", generated_content) + + def test_list_public_components_supports_register_mode_project_root_kit_path(self): + from studio.commands.agents import _list_public_components + + with TemporaryDirectory() as td: + root = Path(td) / "project" + root.mkdir() + (root / ".git").mkdir() + (root / "AGENTS.md").write_text( + '\n```toml\ncf-studio-path = ".cf-studio"\n```\n\n', + encoding="utf-8", + ) + studio_root = root / ".cf-studio" + (studio_root / "config").mkdir(parents=True) + (studio_root / "config" / "core.toml").write_text( + "\n".join([ + 'version = "1.0"', + 'project_root = ".."', + "", + "[kits.gears]", + 'format = "CFS"', + 'path = ".."', + 'version = "0.1.0"', + 'install_mode = "register"', + ]) + "\n", + encoding="utf-8", + ) + (root / ".cf-studio-kit.toml").write_text( + "\n".join([ + 'manifest_version = "1.0"', + "", + "[[kits]]", + 'slug = "gears"', + 'name = "Gears"', + 'version = "0.1.0"', + "", + "[[kits.resources]]", + 'id = "workflow_doc_prd"', + 'kind = "skill"', + 'source = "studio-kit-gears/workflows/doc-prd.md"', + 'type = "file"', + "public = true", + ]) + "\n", + encoding="utf-8", + ) + workflow_source = root / "studio-kit-gears" / "workflows" / "doc-prd.md" + workflow_source.parent.mkdir(parents=True) + workflow_source.write_text( + "---\nname: doc-prd\ndescription: Draft PRDs\n---\n# Doc PRD\n", + encoding="utf-8", + ) + + components, manifest_backed = _list_public_components(studio_root, root, "cursor") + + self.assertEqual(manifest_backed, {"gears"}) + self.assertEqual(len(components), 1) + kit_slug, component, source_path, kit_root, _kit_entry = components[0] + self.assertEqual(kit_slug, "gears") + self.assertEqual(getattr(component, "generated_name", ""), "cf-gears-doc-prd") + self.assertEqual(source_path, workflow_source.resolve()) + self.assertEqual(kit_root, root.resolve()) + def test_nested_public_subagent_uses_registered_resource_binding(self): from studio.commands.agents import _default_agents_config, _process_single_agent