diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 26609aa..47d5791 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,8 +44,33 @@ jobs: 'import site; print(site.getsitepackages()[0])') python -m pip_audit --path "$product_site" + harness-assurance: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - profile: langgraph + package: "langchain-core>=1,<2" + - profile: crewai + package: "crewai>=1,<2" + - profile: openai-agents + package: "openai-agents>=0.19,<1" + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.13" + cache: pip + - run: python -m pip install --upgrade pip + - run: python -m pip install -e '.[dev]' '${{ matrix.package }}' + - name: Exercise current public harness objects + env: + MENDMARK_HARNESS: ${{ matrix.profile }} + run: python -m pytest tests/test_harness_live_compatibility.py + build: - needs: assurance + needs: [assurance, harness-assurance] runs-on: ubuntu-latest outputs: version: ${{ steps.package-version.outputs.version }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7b396b8..049ee77 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,6 +2,7 @@ name: tests on: push: + branches: [main] pull_request: workflow_dispatch: @@ -92,6 +93,31 @@ jobs: run: python -m pip install -e '.[deepeval,rubric,dev]' - run: python -m pytest tests/test_deepeval.py tests/test_rubric_example.py + harness-compatibility: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - profile: langgraph + package: "langchain-core>=1,<2" + - profile: crewai + package: "crewai>=1,<2" + - profile: openai-agents + package: "openai-agents>=0.19,<1" + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.13" + cache: pip + - run: python -m pip install --upgrade pip + - run: python -m pip install -e '.[dev]' '${{ matrix.package }}' + - name: Exercise current public harness objects + env: + MENDMARK_HARNESS: ${{ matrix.profile }} + run: python -m pytest tests/test_harness_live_compatibility.py + distribution-assurance: runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index e26415c..60f2ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,24 @@ All notable changes to Mendmark are documented here. The project follows Semantic Versioning for its Python and JSON contracts. -## Unreleased +## 0.6.0 - 2026-08-10 + +### Added + +- Dependency-light, public-object adapters for LangChain/LangGraph messages, + CrewAI events, and OpenAI Agents SDK run items, including tool-schema and + side-effect metadata conversion. +- `mendmark equip` for bounded harness detection, non-destructive local + scaffolding, an offline evaluator, an inactive pinned CI template, and a + copyable coding-agent self-equip prompt. +- Live compatibility assurance against current releases of all three harness + paths, plus explicit human approval before observed traces can become + expected behavior. +- A fluent causal-case builder for reviewed multi-agent delegation, parallel + dependencies, tool authority, state changes, results, and aggregation without + hand-authoring schema 2.0 JSON. + +## 0.5.0 - 2026-08-06 ### Added diff --git a/README.md b/README.md index f394f05..f3efeb7 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@
Quick start ·
+ Harnesses ·
Golden set ·
Multi-agent ·
How it works ·
@@ -63,6 +64,31 @@ mendmark audit-json examples/multi_agent_suite.json \
--evaluator-command "python3 examples/multi_agent_evaluator.py"
```
+## Equip an agent harness
+
+Mendmark has dependency-light adapters for LangChain/LangGraph, CrewAI, and the
+OpenAI Agents SDK. In an existing agent repository:
+
+```bash
+python -m pip install 'mendmark-evals==0.6.0'
+mendmark equip --framework auto
+```
+
+The command detects bounded dependency files and creates a reviewed capture
+guide, offline evaluator, and inactive CI template under `.mendmark/`. It does
+not edit application code, upload a trace, overwrite existing work, enable CI,
+or accept a baseline.
+
+Want the repository's coding agent to perform the integration?
+
+```bash
+mendmark equip --print-agent-prompt
+```
+
+See the [agent harness integration guide](docs/harness-integrations.md) for the
+direct Python APIs, explicit trace-approval boundary, framework compatibility,
+and multi-agent guidance.
+
## See the blind spot in two minutes
**[▶ Watch the narrated weak-eval demonstration (original v1 fault inventory)](docs/assets/mendmark-weak-eval-demo.mp4)**
@@ -346,7 +372,7 @@ and the [ML evaluation card](https://github.com/danielgaskins/mendmark/blob/main
## Current boundary
-Version 0.5 is a local, open-source engine. It does not yet provide a hosted
+Version 0.6 is a local, open-source engine. It does not yet provide a hosted
dashboard, team accounts, remote trace ingestion, or a secrets service. The
planned control plane is described in [the product design](https://github.com/danielgaskins/mendmark/blob/main/docs/product.md).
diff --git a/docs/assurance.md b/docs/assurance.md
index 2073e1b..060a8d6 100644
--- a/docs/assurance.md
+++ b/docs/assurance.md
@@ -10,6 +10,11 @@ The automated assurance suite checks that:
- `mendmark --version`, `--help`, packaged tasks, packaged schemas, and the
complete single- and multi-agent JSON audit journeys work from that clean
installation.
+- Harness converters are tested with privacy-safe facsimiles and current public
+ objects from LangChain Core, CrewAI, and OpenAI Agents SDK. `mendmark equip`
+ is tested for idempotency, bounded detection, conflict refusal, symlink
+ containment, explicit expectation approval, and a complete generated
+ evaluator journey.
- Reports, console output, JUnit, and SARIF do not expose canary values placed in
prompts, answers, metadata, tags, tool arguments, tool outputs, or descriptions.
- Repeated audits preserve mutation IDs, ordering, decisions, JUnit, SARIF, and
diff --git a/docs/compatibility.md b/docs/compatibility.md
index 7e9eaa1..f3d2658 100644
--- a/docs/compatibility.md
+++ b/docs/compatibility.md
@@ -18,9 +18,14 @@ Mendmark 0.x is evolving, but automation still needs predictable contracts.
- Historical benchmarks select immutable `agent-eval-v1` or `multi-agent-v1`
mutation profiles. Ordinary audits use `current`; adding a new operator never
rewrites an older golden-set contract.
+- Harness adapters are dependency-light and use documented public object
+ fields. Hosted CI exercises LangChain Core 1.5.3, CrewAI 1.15.14, and OpenAI
+ Agents SDK 0.19.4 as the 2026-08-10 compatibility snapshot. Newer supported
+ releases are tested through the latest-version harness matrix; Mendmark does
+ not install or pin a harness in customer environments.
- A custom operator name is globally unique within an audit and is part of the
customer's accepted baseline contract.
-- `audit`, `audit-json`, `prepare`, `grade`, and `show` exit with 0 for success,
+- `audit`, `audit-json`, `equip`, `prepare`, `grade`, and `show` exit with 0 for success,
1 for a failed product gate, and 2 for invalid input or infrastructure failure.
The JSON Schemas under `mendmark/schemas` are the machine-readable contract.
diff --git a/docs/harness-integrations.md b/docs/harness-integrations.md
new file mode 100644
index 0000000..752331a
--- /dev/null
+++ b/docs/harness-integrations.md
@@ -0,0 +1,224 @@
+# Agent harness integrations
+
+Mendmark converts public harness traces into its stable local JSON contract. It
+does not import a harness at package import time, constrain the harness version,
+or send trace content to a hosted service.
+
+## Supported first-class paths
+
+The initial targets were selected from active Python agent projects on
+2026-08-10. GitHub stars are an imperfect adoption signal, so activity and a
+stable tool/trace interface were considered as well.
+
+| Harness path | Adoption signal at selection | Mendmark input |
+| --- | ---: | --- |
+| LangChain / LangGraph | 143,910 / 39,385 GitHub stars | `AIMessage` and `ToolMessage` history |
+| CrewAI | 56,908 GitHub stars | Public tool-usage and completion events |
+| OpenAI Agents SDK | 28,542 GitHub stars | Public `RunResult.new_items` |
+
+The project sources are the [LangChain](https://github.com/langchain-ai/langchain),
+[LangGraph](https://github.com/langchain-ai/langgraph),
+[CrewAI](https://github.com/crewAIInc/crewAI), and
+[OpenAI Agents SDK](https://github.com/openai/openai-agents-python)
+repositories. Mendmark's compatibility job exercises current real objects from
+each framework instead of relying only on local facsimiles.
+
+These paths follow the frameworks' documented structures: LangGraph exposes
+tool lifecycle events and correlated message tool calls, CrewAI exposes an
+event bus with tool-completion events, and the OpenAI Agents SDK exposes public
+run items and trace processors. See the official [LangGraph event-streaming
+guide](https://docs.langchain.com/oss/python/langgraph/event-streaming),
+[CrewAI documentation](https://docs.crewai.com/), and [OpenAI Agents SDK tracing
+guide](https://openai.github.io/openai-agents-python/tracing/).
+
+## One-command setup
+
+From the agent application repository:
+
+```bash
+python -m pip install 'mendmark-evals==0.6.0'
+mendmark equip --framework auto
+```
+
+Detection reads only bounded dependency files; it does not import or execute the
+application. The command creates five reviewable files under `.mendmark/`:
+
+- `agent-setup.md`: harness-specific capture code and acceptance criteria.
+- `evaluator.py`: a deterministic offline evaluator for reviewed snapshots.
+- `mendmark-ci.yml`: an inactive, pinned CI template.
+- `config.json`: detected integration metadata.
+- `.gitignore`: excludes generated report artifacts.
+
+It never edits application code, activates CI, overwrites an existing differing
+file, or accepts a baseline. Running it again is idempotent. Use `--dry-run` to
+preview its targets.
+
+## Let a coding agent self-equip the repository
+
+Print a prompt that works with repository-capable coding agents:
+
+```bash
+mendmark equip --print-agent-prompt
+```
+
+The prompt instructs the agent to run detection, read the generated setup file,
+capture a real tool-using case, pass the audit, and meet every review criterion.
+It explicitly forbids uploading trace content or silently treating observed
+production behavior as correct.
+
+The short prompt can also be copied directly:
+
+> Run `mendmark equip --framework auto`, read `.mendmark/agent-setup.md`
+> completely, integrate the detected harness, capture at least one reviewed
+> tool-using case, run the local audit, and satisfy every acceptance criterion
+> before enabling CI. Do not upload trace content or approve observed behavior
+> without human review.
+
+## Direct Python API
+
+### LangChain and LangGraph
+
+```python
+from mendmark.integrations import write_suite
+from mendmark.integrations.langchain import case_from_messages, tool_specs
+
+case = case_from_messages(
+ result["messages"],
+ case_id="refund-reviewed",
+ input=test_input,
+ expected_output=expected_output,
+ approve_observed=True,
+)
+write_suite(
+ ".mendmark/suite.json",
+ [case],
+ tool_specs(tools, side_effecting=["refund_order"]),
+)
+```
+
+Tool calls are joined to `ToolMessage` results by call ID. Both framework
+objects and their documented dictionary forms are supported.
+
+### OpenAI Agents SDK
+
+```python
+from mendmark.integrations import write_suite
+from mendmark.integrations.openai_agents import case_from_result, tool_specs
+
+result = Runner.run_sync(agent, test_input)
+case = case_from_result(
+ result,
+ case_id="refund-reviewed",
+ input=test_input,
+ expected_output=expected_output,
+ approve_observed=True,
+)
+write_suite(
+ ".mendmark/suite.json",
+ [case],
+ tool_specs(agent.tools, side_effecting=["refund_order"]),
+)
+```
+
+The adapter pairs public `ToolCallItem` and `ToolCallOutputItem` objects using
+their call ID. No OpenAI API call is made by the adapter.
+
+### CrewAI
+
+```python
+from mendmark.integrations import write_suite
+from mendmark.integrations.crewai import CrewAIRecorder, tool_specs
+
+recorder = CrewAIRecorder().attach()
+result = crew.kickoff(inputs=test_inputs)
+case = recorder.case(
+ case_id="refund-reviewed",
+ input=test_input,
+ expected_output=expected_output,
+ approve_observed=True,
+)
+write_suite(
+ ".mendmark/suite.json",
+ [case],
+ tool_specs(all_tools, side_effecting=["refund_order"]),
+)
+```
+
+Attach one recorder in an isolated capture/test process and clear it between
+cases. CrewAI's event bus is process-global, so concurrent crews should capture
+in separate processes or pass their already-separated event sequences directly
+to `case_from_events`.
+
+## Approval boundary
+
+All converters require `expected_tools` unless the caller explicitly supplies
+`approve_observed=True`. That escape hatch exists for a one-off, human-reviewed
+snapshot. It must not be applied automatically to arbitrary production traces.
+
+For maintained capture code, build and pass an explicit sequence of
+`ToolCallRecord` expectations. Mark side-effecting tools explicitly: duplicate
+payment, refund, email, write, and deployment calls are otherwise treated as
+ordinary calls and receive weaker mutation severity.
+
+The adapters above produce the stable ordered-trace schema `1.0`. If agent
+identity, delegation, parallel branches, shared state, or causal dependencies
+matter, use `CausalCaseBuilder`, which emits Mendmark's native [multi-agent
+schema 2.0](multi-agent.md). Do not flatten a coordination test and then claim
+coverage of coordination failures.
+
+```python
+from mendmark.integrations import CausalCaseBuilder, write_suite
+
+case = (
+ CausalCaseBuilder(
+ case_id="parallel-review",
+ input=test_input,
+ root_agent_id="supervisor",
+ )
+ .agent("supervisor")
+ .agent("billing", allowed_tools=["lookup_order", "refund_order"])
+ .agent("risk", allowed_tools=["assess_risk"])
+ .delegation("delegate-billing", "supervisor", "billing")
+ .delegation("delegate-risk", "supervisor", "risk")
+ .tool_call(
+ "lookup",
+ "billing",
+ "lookup_order",
+ input_parameters={"order_id": "104"},
+ output={"status": "paid"},
+ depends_on=["delegate-billing"],
+ )
+ .result("billing-result", "billing", "supervisor", depends_on=["lookup"])
+ .result("risk-result", "risk", "supervisor", depends_on=["delegate-risk"])
+ .message(
+ "aggregate",
+ "supervisor",
+ depends_on=["billing-result", "risk-result"],
+ )
+ .build(
+ actual_output=actual_output,
+ expected_output=expected_output,
+ approve_observed=True,
+ )
+)
+write_suite(".mendmark/multi-agent-suite.json", [case], tool_contracts)
+```
+
+The builder validates agent identities, tool authority, unique event IDs,
+dependency targets, and acyclicity. Dependencies remain explicit; it never
+turns unreliable wall-clock ordering into a causal claim.
+
+## Run and activate CI
+
+```bash
+mendmark audit-json .mendmark/suite.json \
+ --evaluator-command "python .mendmark/evaluator.py" \
+ --output .mendmark/report.json \
+ --junit .mendmark/report.xml \
+ --sarif .mendmark/report.sarif
+```
+
+Review survivors and create an accepted baseline only from a passing audit.
+Then copy `.mendmark/mendmark-ci.yml` to
+`.github/workflows/mendmark.yml`, open a pull request, and require its
+`agent-eval-assurance` check after the first successful run.
diff --git a/pyproject.toml b/pyproject.toml
index e6457b2..34e4cc9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "mendmark-evals"
-version = "0.5.0"
+version = "0.6.0"
description = "Mutation testing for agent evaluation suites"
readme = "README.md"
requires-python = ">=3.10"
diff --git a/scripts/assure_distribution.py b/scripts/assure_distribution.py
index b8eb4e0..c7ea910 100644
--- a/scripts/assure_distribution.py
+++ b/scripts/assure_distribution.py
@@ -94,6 +94,7 @@ def main() -> int:
("scripts", "assure_distribution.py"),
(".github", "allowed_signers"),
("docs", "assurance.md"),
+ ("docs", "harness-integrations.md"),
("golden", "agent-eval-v1", "manifest.json"),
("golden", "agent-eval-v1", "suite.json"),
("golden", "agent-eval-v1", "results.json"),
@@ -119,6 +120,53 @@ def main() -> int:
help_text = run([str(mendmark), "--help"], cwd=workspace, env=clean_env)
if "mutation-test" not in help_text.lower() or "audit-json" not in help_text:
raise RuntimeError("installed CLI help is missing the primary product journey")
+ prompt = run(
+ [str(mendmark), "equip", "--print-agent-prompt"],
+ cwd=workspace,
+ env=clean_env,
+ )
+ if "agent-setup.md" not in prompt or "human review" not in prompt:
+ raise RuntimeError("installed wheel did not expose safe agent self-equip guidance")
+ integration_api = run(
+ [
+ str(python),
+ "-c",
+ (
+ "from mendmark.integrations import CausalCaseBuilder, write_suite; "
+ "from mendmark.integrations.langchain import case_from_messages; "
+ "from mendmark.integrations.crewai import CrewAIRecorder; "
+ "from mendmark.integrations.openai_agents import case_from_result; "
+ "print('harness adapters ready')"
+ ),
+ ],
+ cwd=workspace,
+ env=clean_env,
+ )
+ if integration_api.strip() != "harness adapters ready":
+ raise RuntimeError("installed wheel omitted the harness integration API")
+ equip_output = run(
+ [
+ str(mendmark),
+ "equip",
+ "--framework",
+ "langgraph",
+ "--project-root",
+ str(workspace),
+ ],
+ cwd=workspace,
+ env=clean_env,
+ )
+ if "Created: .mendmark/evaluator.py" not in equip_output:
+ raise RuntimeError("installed wheel did not scaffold a harness integration")
+ for generated in (
+ "evaluator.py",
+ "agent-setup.md",
+ "mendmark-ci.yml",
+ "config.json",
+ ".gitignore",
+ ):
+ if not (workspace / ".mendmark" / generated).is_file():
+ raise RuntimeError(f"installed wheel omitted equip asset: {generated}")
tasks = run([str(mendmark), "tasks"], cwd=workspace, env=clean_env)
if len([line for line in tasks.splitlines() if line.strip()]) != 5:
raise RuntimeError("installed wheel did not expose all five ML integrity tasks")
diff --git a/src/mendmark/__init__.py b/src/mendmark/__init__.py
index d7b84fa..ff14598 100644
--- a/src/mendmark/__init__.py
+++ b/src/mendmark/__init__.py
@@ -2,7 +2,7 @@
from .agent_cases import AgentCase, AgentEvent, AgentSpec, ToolCallRecord, ToolSpec
-__version__ = "0.5.0"
+__version__ = "0.6.0"
__all__ = [
"AgentCase",
diff --git a/src/mendmark/cli.py b/src/mendmark/cli.py
index 68f6c00..d86c685 100644
--- a/src/mendmark/cli.py
+++ b/src/mendmark/cli.py
@@ -82,6 +82,25 @@ def build_parser() -> argparse.ArgumentParser:
subparsers.add_parser("tasks", help="list and validate available tasks")
+ equip = subparsers.add_parser(
+ "equip", help="scaffold a reviewed agent-harness integration"
+ )
+ equip.add_argument(
+ "--framework",
+ choices=("auto", "langgraph", "crewai", "openai-agents", "generic"),
+ default="auto",
+ help="detect from dependency files or select an integration (default: auto)",
+ )
+ equip.add_argument("--project-root", default=".", help="project to equip")
+ equip.add_argument(
+ "--dry-run", action="store_true", help="show files without writing them"
+ )
+ equip.add_argument(
+ "--print-agent-prompt",
+ action="store_true",
+ help="print a prompt for a repository coding agent without writing files",
+ )
+
prepare = subparsers.add_parser("prepare", help="create a public agent workspace")
prepare.add_argument("task_id")
prepare.add_argument("--runs-root", default="runs")
@@ -426,6 +445,27 @@ def main(argv: list[str] | None = None) -> int:
)
return 0
+ if args.command == "equip":
+ from .equip import agent_prompt, equip_project
+
+ if args.print_agent_prompt:
+ print(agent_prompt(args.project_root))
+ return 0
+ frameworks, created, unchanged = equip_project(
+ args.project_root,
+ framework=args.framework,
+ dry_run=args.dry_run,
+ )
+ action = "Would create" if args.dry_run else "Created"
+ print("Detected: " + ", ".join(frameworks))
+ for path in created:
+ print(f"{action}: {path}")
+ for path in unchanged:
+ print(f"Unchanged: {path}")
+ if not args.dry_run:
+ print("Next: ask your coding agent to complete .mendmark/agent-setup.md")
+ return 0
+
if args.command == "prepare":
task = _task(tasks_root, args.task_id)
run_dir = prepare_run(
diff --git a/src/mendmark/equip.py b/src/mendmark/equip.py
new file mode 100644
index 0000000..236150a
--- /dev/null
+++ b/src/mendmark/equip.py
@@ -0,0 +1,345 @@
+"""Safe, review-first scaffolding for agent harness integrations."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Final
+
+from . import __version__
+
+
+class EquipError(ValueError):
+ """Raised when a project cannot be equipped without unsafe assumptions."""
+
+
+SUPPORTED_FRAMEWORKS: Final = ("langgraph", "crewai", "openai-agents", "generic")
+_MARKER = "Generated by Mendmark equip; safe to regenerate."
+_DEPENDENCY_FILES = (
+ "pyproject.toml",
+ "requirements.txt",
+ "requirements-dev.txt",
+ "uv.lock",
+ "poetry.lock",
+ "pdm.lock",
+)
+
+
+def detect_frameworks(project_root: Path) -> tuple[str, ...]:
+ """Detect supported harness names from bounded dependency-file reads."""
+ corpus: list[str] = []
+ for name in _DEPENDENCY_FILES:
+ path = project_root / name
+ if path.is_file() and path.stat().st_size <= 2_000_000:
+ corpus.append(path.read_text(encoding="utf-8", errors="replace").lower())
+ text = "\n".join(corpus)
+ found: list[str] = []
+ if "langgraph" in text or "langchain" in text:
+ found.append("langgraph")
+ if "crewai" in text:
+ found.append("crewai")
+ if "openai-agents" in text or "openai_agents" in text:
+ found.append("openai-agents")
+ return tuple(found or ("generic",))
+
+
+def agent_prompt(project_root: str = ".") -> str:
+ """Return the short prompt a user can give any repository coding agent."""
+ return (
+ f"In {project_root}, run `mendmark equip --framework auto`, then read "
+ "`.mendmark/agent-setup.md` completely. Integrate the detected harness, "
+ "capture at least one reviewed tool-using case, run the local audit, and "
+ "satisfy every acceptance criterion before enabling the generated CI "
+ "workflow. Do not upload trace content or approve observed behavior "
+ "without human review."
+ )
+
+
+def _evaluator() -> str:
+ return f'''#!/usr/bin/env python3
+"""{_MARKER}
+
+Offline exact evaluator for reviewed Mendmark trace snapshots.
+"""
+from __future__ import annotations
+
+import json
+import sys
+
+
+def main() -> int:
+ request = json.load(sys.stdin)
+ evaluations = []
+ for requested in request["evaluations"]:
+ case = requested["case"]
+ tools_match = case.get("tools_called", []) == case.get("expected_tools", [])
+ output_matches = (
+ case.get("expected_output") is None
+ or case.get("actual_output") == case.get("expected_output")
+ )
+ events_match = case.get("events", []) == case.get("expected_events", [])
+ is_multi = "events" in case or "expected_events" in case
+ results = [
+ {{
+ "name": "Reviewed tool trace",
+ "score": float(tools_match),
+ "passed": tools_match,
+ "reason": "Actual tool calls match the reviewed expected calls.",
+ }},
+ {{
+ "name": "Reviewed outcome",
+ "score": float(output_matches),
+ "passed": output_matches,
+ "reason": "The outcome matches when an expected outcome is configured.",
+ }},
+ ]
+ if is_multi:
+ results.append(
+ {{
+ "name": "Reviewed coordination graph",
+ "score": float(events_match),
+ "passed": events_match,
+ "reason": "Actual causal events match the reviewed graph.",
+ }}
+ )
+ evaluations.append(
+ {{"evaluation_id": requested["evaluation_id"], "results": results}}
+ )
+ json.dump(
+ {{"schema_version": request["schema_version"], "evaluations": evaluations}},
+ sys.stdout,
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
+'''
+
+
+def _adapter_example(framework: str) -> str:
+ if framework == "langgraph":
+ return '''```python
+from mendmark.integrations.langchain import case_from_messages, tool_specs
+from mendmark.integrations import write_suite
+
+# `result` is the graph/agent state and `tools` is the configured tool list.
+case = case_from_messages(
+ result["messages"],
+ case_id="reviewed-order-flow",
+ input=test_input,
+ expected_output=reviewed_expected_output,
+ approve_observed=True, # Only in the one-off capture after human review.
+)
+write_suite(
+ ".mendmark/suite.json",
+ [case],
+ tool_specs(tools, side_effecting=["refund_order"]),
+)
+```'''
+ if framework == "openai-agents":
+ return '''```python
+from mendmark.integrations.openai_agents import case_from_result, tool_specs
+from mendmark.integrations import write_suite
+
+# `result` is returned by Runner.run/run_sync and `agent` is the configured Agent.
+case = case_from_result(
+ result,
+ case_id="reviewed-order-flow",
+ input=test_input,
+ expected_output=reviewed_expected_output,
+ approve_observed=True, # Only in the one-off capture after human review.
+)
+write_suite(
+ ".mendmark/suite.json",
+ [case],
+ tool_specs(agent.tools, side_effecting=["refund_order"]),
+)
+```'''
+ if framework == "crewai":
+ return '''```python
+from mendmark.integrations.crewai import CrewAIRecorder, tool_specs
+from mendmark.integrations import write_suite
+
+recorder = CrewAIRecorder().attach() # Attach before crew.kickoff().
+result = crew.kickoff(inputs=test_inputs)
+case = recorder.case(
+ case_id="reviewed-order-flow",
+ input=test_input,
+ expected_output=reviewed_expected_output,
+ approve_observed=True, # Only in the one-off capture after human review.
+)
+write_suite(
+ ".mendmark/suite.json",
+ [case],
+ tool_specs(all_tools, side_effecting=["refund_order"]),
+)
+```'''
+ return '''Use the framework-neutral JSON contract documented at
+`https://github.com/danielgaskins/mendmark/blob/main/docs/json-adapter.md`.
+Create a reviewed suite at `.mendmark/suite.json`; do not send trace content to
+Mendmark or any hosted service.'''
+
+
+def _instructions(frameworks: tuple[str, ...]) -> str:
+ detected = ", ".join(frameworks)
+ examples = "\n\n".join(
+ f"### {framework}\n\n{_adapter_example(framework)}" for framework in frameworks
+ )
+ return f'''
+# Equip this repository with Mendmark
+
+Detected integration: **{detected}**.
+
+Mendmark mutation-tests the evaluator, not the agent. Keep all trace content
+local. The generated exact evaluator is a safe starting point for reviewed
+snapshots; replace or supplement it with the project's real evaluator when
+available.
+
+## Capture a reviewed case
+
+{examples}
+
+`approve_observed=True` is deliberately explicit. Inspect tool names,
+arguments, results, ordering, side effects, and the expected outcome before
+running the capture. Prefer passing an explicit `expected_tools` sequence in a
+maintained capture test. Never turn arbitrary production traces into expected
+behavior automatically.
+
+## Run locally
+
+```bash
+mendmark audit-json .mendmark/suite.json \\
+ --evaluator-command "python .mendmark/evaluator.py" \\
+ --output .mendmark/report.json \\
+ --junit .mendmark/report.xml \\
+ --sarif .mendmark/report.sarif
+```
+
+Review every survivor. When the suite is accepted, create its baseline once
+with `--write-baseline`, review `.mendmark/baseline.json`, and commit it. Copy
+`.mendmark/mendmark-ci.yml` to `.github/workflows/mendmark.yml` only after the
+local command passes.
+
+## Acceptance criteria
+
+- [ ] At least one consequential, tool-using case is captured.
+- [ ] A human reviewed expected calls, arguments, outputs, order, and outcome.
+- [ ] Every side-effecting tool is declared with `side_effecting=True`.
+- [ ] The original case passes before mutation.
+- [ ] Critical survivors were fixed or explicitly documented and accepted.
+- [ ] No prompt, arguments, result content, credentials, or PII appears in the report.
+- [ ] The baseline was generated only from a passing audit and reviewed in diff.
+- [ ] The generated CI workflow passes on a pull request before becoming required.
+
+For native multi-agent coordination mutation testing, model the causal graph
+with `mendmark.integrations.CausalCaseBuilder` instead of flattening it into an
+ordered tool trace. See `docs/multi-agent.md` in Mendmark's repository.
+'''
+
+
+def _workflow() -> str:
+ return f'''# {_MARKER}
+name: mendmark
+
+on:
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ security-events: write
+
+jobs:
+ agent-eval-assurance:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
+ with:
+ python-version: "3.13"
+ cache: pip
+ - run: python -m pip install . 'mendmark-evals=={__version__}'
+ - name: Mutation-test agent evals
+ run: >-
+ mendmark audit-json .mendmark/suite.json
+ --evaluator-command "python .mendmark/evaluator.py"
+ --baseline .mendmark/baseline.json
+ --output .mendmark/report.json
+ --junit .mendmark/report.xml
+ --sarif .mendmark/report.sarif
+ --maximum-mutants 10000
+ - uses: github/codeql-action/upload-sarif@b374143c1149a911a7c303ebe6abb5c19149a01c # v3
+ if: always() && hashFiles('.mendmark/report.sarif') != ''
+ continue-on-error: true
+ with:
+ sarif_file: .mendmark/report.sarif
+'''
+
+
+def _files(frameworks: tuple[str, ...]) -> dict[str, str]:
+ return {
+ ".mendmark/evaluator.py": _evaluator(),
+ ".mendmark/agent-setup.md": _instructions(frameworks),
+ ".mendmark/mendmark-ci.yml": _workflow(),
+ ".mendmark/config.json": json.dumps(
+ {"schema_version": "1.0", "frameworks": list(frameworks)},
+ indent=2,
+ sort_keys=True,
+ )
+ + "\n",
+ ".mendmark/.gitignore": (
+ f"# {_MARKER}\nreport.json\nreport.xml\nreport.sarif\n"
+ ),
+ }
+
+
+def equip_project(
+ project_root: str | Path,
+ *,
+ framework: str = "auto",
+ dry_run: bool = False,
+) -> tuple[tuple[str, ...], tuple[str, ...], tuple[str, ...]]:
+ """Create non-destructive integration assets.
+
+ Returns ``(frameworks, created, unchanged)``. Existing differing files are
+ never overwritten; the user must review and resolve them manually.
+ """
+ root = Path(project_root).expanduser().resolve()
+ if not root.is_dir():
+ raise EquipError(f"project root is not a directory: {root}")
+ if framework != "auto" and framework not in SUPPORTED_FRAMEWORKS:
+ raise EquipError(f"unsupported framework: {framework}")
+ frameworks = detect_frameworks(root) if framework == "auto" else (framework,)
+ generated = _files(frameworks)
+ created: list[str] = []
+ unchanged: list[str] = []
+ conflicts: list[str] = []
+ for relative, content in generated.items():
+ destination = root / relative
+ resolved_destination = destination.resolve()
+ if root not in resolved_destination.parents:
+ conflicts.append(relative)
+ continue
+ if destination.exists():
+ if destination.is_symlink() or not destination.is_file():
+ conflicts.append(relative)
+ elif destination.read_text(encoding="utf-8") == content:
+ unchanged.append(relative)
+ else:
+ conflicts.append(relative)
+ else:
+ created.append(relative)
+ if conflicts:
+ raise EquipError(
+ "refusing to overwrite existing integration file(s): "
+ + ", ".join(conflicts)
+ )
+ if not dry_run:
+ for relative in created:
+ destination = root / relative
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ if root not in destination.resolve().parents:
+ raise EquipError(f"integration path escapes project root: {relative}")
+ destination.write_text(generated[relative], encoding="utf-8")
+ return frameworks, tuple(created), tuple(unchanged)
diff --git a/src/mendmark/integrations/__init__.py b/src/mendmark/integrations/__init__.py
new file mode 100644
index 0000000..2989cd1
--- /dev/null
+++ b/src/mendmark/integrations/__init__.py
@@ -0,0 +1,15 @@
+"""Dependency-light adapters for popular Python agent harnesses.
+
+The adapters use public, duck-typed harness objects so importing Mendmark never
+imports or constrains the customer's agent framework.
+"""
+
+from .common import HarnessIntegrationError, suite_to_json, write_suite
+from .multi_agent import CausalCaseBuilder
+
+__all__ = [
+ "HarnessIntegrationError",
+ "CausalCaseBuilder",
+ "suite_to_json",
+ "write_suite",
+]
diff --git a/src/mendmark/integrations/common.py b/src/mendmark/integrations/common.py
new file mode 100644
index 0000000..a6f42b1
--- /dev/null
+++ b/src/mendmark/integrations/common.py
@@ -0,0 +1,206 @@
+"""Shared helpers for converting harness traces to Mendmark suites."""
+
+from __future__ import annotations
+
+import json
+import os
+import tempfile
+from collections.abc import Mapping, Sequence
+from dataclasses import asdict, is_dataclass
+from pathlib import Path
+from typing import Any
+
+from ..agent_cases import AgentCase, ToolCallRecord, ToolSpec
+from ..json_adapter import case_to_json, load_json_suite
+
+
+class HarnessIntegrationError(ValueError):
+ """Raised when a harness object cannot be converted without guessing."""
+
+
+def member(value: Any, name: str, default: Any = None) -> Any:
+ """Read a public field from either an object or mapping."""
+ if isinstance(value, Mapping):
+ return value.get(name, default)
+ return getattr(value, name, default)
+
+
+def json_value(value: Any, *, _depth: int = 0) -> Any:
+ """Convert common harness/Pydantic values to deterministic JSON values."""
+ if _depth > 32:
+ raise HarnessIntegrationError("harness value nesting exceeds 32 levels")
+ if value is None or isinstance(value, (str, int, float, bool)):
+ return value
+ if hasattr(value, "model_dump"):
+ value = value.model_dump(mode="json")
+ elif is_dataclass(value) and not isinstance(value, type):
+ value = asdict(value)
+ if isinstance(value, Mapping):
+ return {
+ str(key): json_value(item, _depth=_depth + 1)
+ for key, item in value.items()
+ }
+ if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
+ return [json_value(item, _depth=_depth + 1) for item in value]
+ return str(value)
+
+
+def json_arguments(value: Any, *, location: str) -> dict[str, Any]:
+ """Normalize the JSON-object arguments used by agent tool calls."""
+ if value is None or value == "":
+ return {}
+ if isinstance(value, str):
+ try:
+ value = json.loads(value)
+ except json.JSONDecodeError as error:
+ raise HarnessIntegrationError(
+ f"{location} contains invalid JSON arguments"
+ ) from error
+ normalized = json_value(value)
+ if not isinstance(normalized, dict):
+ raise HarnessIntegrationError(f"{location} arguments must be a JSON object")
+ return normalized
+
+
+def text_output(value: Any) -> str:
+ """Normalize a harness final output while preserving structured content."""
+ if value is None:
+ return ""
+ if isinstance(value, str):
+ return value
+ normalized = json_value(value)
+ if isinstance(normalized, str):
+ return normalized
+ return json.dumps(normalized, sort_keys=True, separators=(",", ":"))
+
+
+def schema_from_tool(tool: Any, *, framework: str) -> dict[str, Any]:
+ """Extract a public JSON input schema from common harness tool objects."""
+ for attribute in ("params_json_schema", "args_schema", "tool_call_schema"):
+ candidate = member(tool, attribute)
+ if candidate is None:
+ continue
+ if callable(candidate) and not isinstance(candidate, type):
+ candidate = candidate()
+ if isinstance(candidate, type):
+ if hasattr(candidate, "model_json_schema"):
+ candidate = candidate.model_json_schema()
+ elif hasattr(candidate, "schema"):
+ candidate = candidate.schema()
+ if hasattr(candidate, "model_json_schema"):
+ candidate = candidate.model_json_schema()
+ if isinstance(candidate, Mapping):
+ return json_value(candidate)
+ raise HarnessIntegrationError(
+ f"{framework} tool {member(tool, 'name', '