diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..aeb9f98d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +# CI — the quality gate (issue #23). +# +# Runs `make check` (ruff + mypy + pytest) and `make validate` (offline +# validation of the flagship example) on every pull request and on pushes +# to main. Single Python version per pyproject.toml (requires-python >=3.11); +# no matrix, no caching tuning — see the issue's non-goals. +# +# To make this a required status check, protect `main` in the repo settings +# and require the "check" job (maintainer action, not part of this file). + +name: CI + +on: + pull_request: + push: + branches: [main] + +# Cancel superseded runs of the same PR/branch to save runner minutes. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install (editable + dev extras) + run: make install + + - name: Quality gate (lint + typecheck + test) + run: make check + + - name: Validate flagship example (offline) + run: make validate diff --git a/examples/enterprise-knowledge-assistant/mcps/local_knowledge_mcp/tools.py b/examples/enterprise-knowledge-assistant/mcps/local_knowledge_mcp/tools.py index a4aacaa9..be4b7239 100644 --- a/examples/enterprise-knowledge-assistant/mcps/local_knowledge_mcp/tools.py +++ b/examples/enterprise-knowledge-assistant/mcps/local_knowledge_mcp/tools.py @@ -3,7 +3,11 @@ from __future__ import annotations import hashlib -from typing import TypedDict + +# typing_extensions rather than typing: pydantic (which FastMCP uses to build +# tool schemas from these types) rejects typing.TypedDict on Python < 3.12, +# and this project supports 3.11. +from typing_extensions import TypedDict class Document(TypedDict): diff --git a/src/agent_engine/core/validator.py b/src/agent_engine/core/validator.py index 979270fa..f4fba741 100644 --- a/src/agent_engine/core/validator.py +++ b/src/agent_engine/core/validator.py @@ -4,7 +4,7 @@ from agent_engine.core.errors import ValidationError from agent_engine.core.plugin_stubs import scan_unimplemented_plugins -from agent_engine.core.spec import GraphNode, OrchestratorSpec, SystemSpec +from agent_engine.core.spec import GraphNode, SystemSpec class SystemSpecValidator: diff --git a/src/agent_engine/generate/generator.py b/src/agent_engine/generate/generator.py index 23d06ba6..f2c1da16 100644 --- a/src/agent_engine/generate/generator.py +++ b/src/agent_engine/generate/generator.py @@ -5,7 +5,6 @@ from agent_engine.core.spec import AgentSpec, GraphNode, SystemSpec from agent_engine.generate.manifest import ( - MANIFEST_NAME, ensure_plugins_manifest_exists, manifest_package, update_manifest, @@ -42,7 +41,9 @@ def generate(self, spec: SystemSpec, base_dir: Path) -> GenerateResult: tools_dir = base_dir / "plugins" / "tools" tools_dir.mkdir(parents=True, exist_ok=True) for tool_id, description in tool_ids.items(): - self._write(tools_dir / f"{tool_id}.py", _tool_stub(tool_id, description), result, base_dir) + self._write( + tools_dir / f"{tool_id}.py", _tool_stub(tool_id, description), result, base_dir + ) resolvers_dir = base_dir / "plugins" / "resolvers" resolvers_dir.mkdir(parents=True, exist_ok=True) diff --git a/tests/approvals/test_sanitization.py b/tests/approvals/test_sanitization.py index 7c386eb0..79cf449c 100644 --- a/tests/approvals/test_sanitization.py +++ b/tests/approvals/test_sanitization.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from agent_engine.approvals.sanitization import REDACTED, mask_arguments, mask_sensitive @@ -42,7 +44,7 @@ def test_recursive_masking_in_nested_structures() -> None: def test_original_arguments_unchanged() -> None: - args = {"password": "hunter2", "nested": {"token": "t"}} + args: dict[str, Any] = {"password": "hunter2", "nested": {"token": "t"}} mask_arguments(args) assert args["password"] == "hunter2" assert args["nested"]["token"] == "t" diff --git a/tests/core/test_plugin_stub_scan.py b/tests/core/test_plugin_stub_scan.py index e3f5e47f..8839f5e4 100644 --- a/tests/core/test_plugin_stub_scan.py +++ b/tests/core/test_plugin_stub_scan.py @@ -430,9 +430,10 @@ def test_missing_prompt_file_is_reported_with_hint(tmp_path: Path) -> None: ) errors = scan(spec, tmp_path) assert len(errors) == 1 - assert any( - "a.prompts.system" in e and "Prompt file not found: prompts/a/system.md — run `agentctl generate` to create the stub" in e - for e in errors + expected = ( + "Prompt file not found: prompts/a/system.md — " + "run `agentctl generate` to create the stub" ) + assert any("a.prompts.system" in e and expected in e for e in errors)