Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
1607b9d
Add the MCP tool-surface eval harness
dheeru0198 Aug 12, 2026
936c072
Split the driver monolith into a package
dheeru0198 Aug 12, 2026
6af196c
Replace the Anthropic-only SDK path with a generic API driver
dheeru0198 Aug 12, 2026
4afe655
Report response-token cost for every driver, and label estimates
dheeru0198 Aug 12, 2026
c735b1c
Split the task catalog into one module per task class
dheeru0198 Aug 12, 2026
19cd92f
Separate the CLI from the run loop
dheeru0198 Aug 12, 2026
adf6534
Reconcile the eval docs with the code
dheeru0198 Aug 12, 2026
ea50b74
Make the API driver provider-neutral and the model tiers vendor-free
dheeru0198 Aug 13, 2026
b8f4617
Collapse the four CLI driver bodies into one template
dheeru0198 Aug 13, 2026
3ddebae
Declare the result schema instead of assembling it by hand
dheeru0198 Aug 13, 2026
de9190c
Grade answers by contract, not by prose style
dheeru0198 Aug 13, 2026
9d7c759
Aggregate repetitions instead of showing the last one
dheeru0198 Aug 13, 2026
87fb16e
Split the fixture builders by Plane object
dheeru0198 Aug 13, 2026
38e1991
Split the report by job and read rows only through the schema
dheeru0198 Aug 13, 2026
5dbdbff
Group the run loop into a package and place helpers with their users
dheeru0198 Aug 13, 2026
ba60db7
Drop the abandoned v2 surface vocabulary from the eval harness
dheeru0198 Aug 13, 2026
0700a96
Give the CLI drivers the same package shape as the API driver
dheeru0198 Aug 13, 2026
22ddca4
Stop treating a local Plane instance as an eval prerequisite
dheeru0198 Aug 13, 2026
128a877
Put the two drivers at the top of the drivers package
dheeru0198 Aug 13, 2026
694ae76
Split the task package by concern
dheeru0198 Aug 13, 2026
6feffb7
Give the report summary a type instead of a magic key
dheeru0198 Aug 13, 2026
f35b498
Break the task repetition into named stages, behind a pinned taxonomy
dheeru0198 Aug 13, 2026
6ad1b1c
Group the tests the way the harness is grouped
dheeru0198 Aug 13, 2026
b082b06
Report progress while a battery runs
dheeru0198 Aug 13, 2026
1a1a07a
Make a feature exclusion mean the feature is off
dheeru0198 Aug 14, 2026
79ac46d
Require a plan refusal to say so before skipping a task
dheeru0198 Aug 14, 2026
6e6bacf
Skip a task whose capability the plan excludes, instead of erroring
dheeru0198 Aug 14, 2026
cd30fda
Add W11: log work against a project with time tracking switched off
dheeru0198 Aug 14, 2026
7064dd9
Stop documenting the flag server as a prerequisite
dheeru0198 Aug 14, 2026
8d37e00
Halve the eval test suite by folding duplicated cases together
dheeru0198 Aug 14, 2026
acd6e3d
Tighten the long docstrings without dropping what they know
dheeru0198 Aug 14, 2026
ef82281
Make fixture seeding and teardown own what they touch
dheeru0198 Aug 15, 2026
b4378c0
Stop verifiers passing tasks they could not verify
dheeru0198 Aug 15, 2026
05debe9
Report what a run actually measured, not what it attempted
dheeru0198 Aug 15, 2026
7fe80ec
Keep the call trace honest about what it dropped
dheeru0198 Aug 15, 2026
cd900bc
Restore per-case test reporting, and document what changed
dheeru0198 Aug 15, 2026
12933c7
Bind read provenance to the target entity, and stop shipping the secret
dheeru0198 Aug 15, 2026
8177441
Count what the proxy lost, and identify the surface it recorded
dheeru0198 Aug 15, 2026
0f7392f
Refuse comparisons the persisted identity cannot establish
dheeru0198 Aug 15, 2026
031037f
Give each repetition its own fixture seed, and record it
dheeru0198 Aug 15, 2026
da12a35
Make trace loss visible in the run verdict, and validate the exact ke…
dheeru0198 Aug 15, 2026
b9eb079
Record a trace the CLI can actually produce
dheeru0198 Aug 15, 2026
619e937
Stop leaking the answers, and give each proxy its own file
dheeru0198 Aug 16, 2026
c232656
Measure off-surface work instead of asserting it cannot happen
dheeru0198 Aug 16, 2026
9feaf78
Close the reporting gaps, and say only what the tests establish
dheeru0198 Aug 16, 2026
99166e5
Count the calls that failed, not just the calls
dheeru0198 Aug 16, 2026
dbcc6a5
Say what the fixture seed actually guarantees
dheeru0198 Aug 16, 2026
054cf72
Let the isolated Codex home actually make tool calls
dheeru0198 Aug 16, 2026
974c097
Stop demanding a tool manifest from rows that never reached the surface
dheeru0198 Aug 16, 2026
70d3c4b
Merge remote-tracking branch 'origin/main' into feat/mcp-eval-harness
dheeru0198 Aug 17, 2026
74b4372
Read the attachment list the shape the SDK returns it in
dheeru0198 Aug 17, 2026
5dbffca
Ask L2 for evidence Plane can actually give
dheeru0198 Aug 17, 2026
f190185
Let a count be evidence at the seed gate too
dheeru0198 Aug 17, 2026
8da1d6c
Bind R1 and I2 evidence to the state that carries the answer
dheeru0198 Aug 17, 2026
181840c
Let R6 be proven by a count per project
dheeru0198 Aug 17, 2026
275683a
Make provenance a property instead of a list of accepted routes
dheeru0198 Aug 17, 2026
e980b69
Stop charging infrastructure failures to the model
dheeru0198 Aug 17, 2026
12f9538
Close the holes the regrade found in the new provenance rule
dheeru0198 Aug 17, 2026
0e0a355
Skip the opencode config readback instead of failing on it
dheeru0198 Aug 17, 2026
a508129
Fix two fixtures the smoke run caught
dheeru0198 Aug 17, 2026
2f65d15
Keep the refusal that caused a skip
dheeru0198 Aug 17, 2026
e34a877
Give the plan-gate policy and the server env their own modules
dheeru0198 Aug 17, 2026
55d9842
Report the run that was executed, not the checkout
dheeru0198 Aug 17, 2026
27d589f
Give each driver surface a base its vendors implement
dheeru0198 Aug 17, 2026
95b2010
Load only the driver surface the run asked for
dheeru0198 Aug 17, 2026
fdd9714
Give the shared floor of the package a name
dheeru0198 Aug 18, 2026
637d2a7
Test the five verifiers that could pass a wrong answer
dheeru0198 Aug 18, 2026
bd4b7cd
Stop a parked stdin read from discarding a whole trace
dheeru0198 Aug 19, 2026
c3d3a71
Stop handing the agent a shell to route around the surface
dheeru0198 Aug 19, 2026
4ddc39c
Score the surface, not the database
dheeru0198 Aug 19, 2026
5c88274
Give seeded projects names with nothing id-shaped in them
dheeru0198 Aug 19, 2026
56372c8
Make the Antigravity driver actually reach agy
dheeru0198 Aug 19, 2026
ff9e4b3
Split the errored-call count by what kind of "no" a call received
dheeru0198 Aug 20, 2026
ce2158a
Document the errored-call split in DESIGN and README
dheeru0198 Aug 20, 2026
85708d9
Count a refusal the server reports as a successful result
dheeru0198 Aug 20, 2026
8646581
Survive a project-name collision, and stop reporting a dirty workspac…
dheeru0198 Aug 20, 2026
d5cb995
Record the request beside a recorded result
dheeru0198 Aug 20, 2026
8978f2a
Adopt the project an ambiguous create leaves behind
dheeru0198 Aug 21, 2026
824ef67
Document the success-flagged refusal count and recorded request args
dheeru0198 Aug 21, 2026
8e1681e
Declare the OpenAI eval provider's package
dheeru0198 Aug 21, 2026
16b0e80
Move the OpenAI eval backend to /v1/responses
dheeru0198 Aug 24, 2026
75da834
Record the cause inside an ExceptionGroup, not its sub-exception count
dheeru0198 Aug 24, 2026
fd09347
Ask Anthropic for prompt caching
dheeru0198 Aug 24, 2026
3c786b6
Make the agent-loop iteration cap settable
dheeru0198 Aug 24, 2026
f2a41b6
feat(evals): normalise token accounting and price runs
dheeru0198 Aug 24, 2026
ac2fcfc
feat(evals): report cost, input, result volume and latency in the A/B…
dheeru0198 Aug 24, 2026
27e3d13
feat(evals): classify failures by kind, not just pass/fail
dheeru0198 Aug 24, 2026
e610d15
feat(evals): warn when per-task verdicts are underpowered
dheeru0198 Aug 24, 2026
979b0cd
fix(evals): eight defects from the adversarial review of the pricing …
dheeru0198 Aug 24, 2026
7352003
feat(evals): measure redundant lookups, and record call args on every…
dheeru0198 Aug 24, 2026
ed7c8a6
docs(evals): describe cost, failure kinds, redundant lookups and power
dheeru0198 Aug 24, 2026
dec701e
docs(evals): README no longer ties request args to payload recording
dheeru0198 Aug 24, 2026
3d7f0e8
fix(evals): ten defects from the second adversarial review
dheeru0198 Aug 24, 2026
35e91b9
fix(tests): bind SDK signatures that Python 3.14 will not evaluate
dheeru0198 Aug 25, 2026
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
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ htmlcov/
.tox/
.hypothesis/

# Eval harness output
evals/output/
# Keep earlier local runs ignored after the default directory rename.
evals/results/
evals/.env-pids
evals/.api_runserver.log
evals/.mock_flags.log

# Booting a local Plane to run evals against is each developer's own setup,
# not part of this repo. The harness itself only needs the EVAL_PLANE_* vars.
localdev/

# Mypy
.mypy_cache/
.dmypy.json
Expand Down
461 changes: 461 additions & 0 deletions evals/DESIGN.md

Large diffs are not rendered by default.

356 changes: 356 additions & 0 deletions evals/README.md

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions evals/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Plane MCP tool-surface eval harness."""

from pathlib import Path

# Repository root: the harness launches this repo's MCP server and resolves
# task working directories against it.
REPO_ROOT = Path(__file__).resolve().parent.parent

__all__ = ["REPO_ROOT"]
6 changes: 6 additions & 0 deletions evals/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Command entry point: python -m evals"""

from evals.cli import main

if __name__ == "__main__":
raise SystemExit(main())
285 changes: 285 additions & 0 deletions evals/cleanup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
"""Delete leftover eval projects or fixed-name workspace sentinels.

``python -m evals.cleanup [--prefix "EVAL " | --sentinels] [--yes]`` — dry-run lists only;
``--yes`` is required before anything is deleted. Credentials come from EVAL_PLANE_*.
"""

from __future__ import annotations

import argparse
import sys
from typing import Any

from plane.models.query_params import PaginatedQueryParams

from evals.seed.customers import (
EVALUATION_CUSTOMER_PROPERTY_NAME,
is_evaluation_customer_name,
)
from evals.seed.item_types import (
BUG_TYPE_NAME,
FIXTURE_WORK_ITEM_TYPE_NAMES,
is_severity_property,
is_work_item_type_named,
list_workspace_properties_for_type,
list_workspace_work_item_types,
)
from evals.seed.releases import EVALUATION_RELEASE_TAG_VERSION
from evals.seed.workspace import list_workspace_rows


def list_projects_with_prefix(plane: Any, workspace_slug: str, prefix: str) -> list[Any]:
"""Return projects whose name starts with ``prefix`` (paginated list).

Matches the SDK contract used elsewhere in the repo: pass
``params=PaginatedQueryParams(...)`` and stop when ``not page.next_page_results``.
Do not fall back on ``next_cursor`` alone — the SDK always populates it.
"""
matches: list[Any] = []
cursor = None
while True:
params = PaginatedQueryParams(per_page=100, cursor=cursor)
page = plane.projects.list(workspace_slug=workspace_slug, params=params)
results = page.results if hasattr(page, "results") else page
for proj in results or []:
# Prefix may include a trailing space (default "EVAL ") so "EVALUATION" is excluded.
name = getattr(proj, "name", None) or ""
if name.startswith(prefix):
matches.append(proj)
if not getattr(page, "next_page_results", False):
break
cursor = page.next_cursor
return matches


def delete_projects(
plane: Any,
workspace_slug: str,
projects: list[Any],
*,
yes: bool,
) -> tuple[int, int]:
"""Delete projects when yes=True. Returns (deleted, failed). Dry-run: (0, 0)."""
if not yes:
return 0, 0
deleted = failed = 0
for proj in projects:
pid = getattr(proj, "id", None)
name = getattr(proj, "name", pid)
try:
plane.projects.delete(workspace_slug=workspace_slug, project_id=pid)
deleted += 1
print(f" deleted {name!r} ({pid})")
except Exception as exc:
failed += 1
print(f" FAILED {name!r} ({pid}): {exc}", file=sys.stderr)
return deleted, failed


def list_sentinel_workspace_artifacts(plane: Any, workspace_slug: str) -> list[dict[str, Any]]:
"""Return fixed-name workspace fixtures that can false-pass eval tasks."""
customers = plane.customers
specs = (
(
"customer",
customers,
lambda row: is_evaluation_customer_name(getattr(row, "name", None)),
lambda row: (getattr(row, "name", None) or "").strip(),
),
(
"release_tag",
plane.releases.tags,
lambda row: (getattr(row, "version", None) or "").strip() == EVALUATION_RELEASE_TAG_VERSION,
lambda row: (getattr(row, "version", None) or "").strip(),
),
(
"customer_property",
customers.properties,
lambda row: (
(getattr(row, "display_name", None) or getattr(row, "name", None) or "").strip().casefold()
== EVALUATION_CUSTOMER_PROPERTY_NAME.casefold()
),
lambda row: (getattr(row, "display_name", None) or getattr(row, "name", None) or "").strip(),
),
)
artifacts: list[dict[str, Any]] = []
for kind, api, matches, display_name in specs:
for row in list_workspace_rows(api, workspace_slug):
object_id = getattr(row, "id", None)
if object_id is not None and matches(row):
artifacts.append({"kind": kind, "id": object_id, "name": display_name(row)})

type_api = getattr(plane, "workspace_work_item_types", None)
if callable(getattr(type_api, "list", None)):
for row in list_workspace_work_item_types(plane, workspace_slug):
object_id = getattr(row, "id", None)
if object_id is None:
continue
# Every fixture name, not just Incident. Bug used to be reachable only as the type
# whose Severity property gets removed, so leftover Bug types were both undeletable
# by this tool and counted as "nothing to delete" -- a workspace reported clean while
# holding types that skew any task reading the workspace-level list. Duplicates of
# one name each match, so a double-seeded type is fully removed.
matched = next(
(name for name in FIXTURE_WORK_ITEM_TYPE_NAMES if is_work_item_type_named(row, name)),
None,
)
if matched is not None:
artifacts.append({"kind": "work_item_type", "id": object_id, "name": matched})

property_api = getattr(plane, "workspace_work_item_properties", None)
links_api = getattr(type_api, "properties", None)
if callable(getattr(property_api, "list", None)) and callable(getattr(links_api, "list", None)):
for row in list_workspace_properties_for_type(plane, workspace_slug, BUG_TYPE_NAME):
object_id = getattr(row, "id", None)
if object_id is not None and is_severity_property(row):
display = getattr(row, "display_name", None) or getattr(row, "name", None) or ""
artifacts.append({"kind": "work_item_property", "id": object_id, "name": display.strip()})
return artifacts


def list_unowned_workspace_work_item_types(plane: Any, workspace_slug: str) -> list[dict[str, Any]]:
"""Return workspace-level work item types this harness never creates.

Reported rather than deleted by default: a type the harness did not create may be a real
workspace's configuration, and this runs against instances it does not own. They still
have to be *visible*, because a workspace holding types another workspace lacks skews
every task that reads the workspace-level list, and silence there reads as clean.
"""
type_api = getattr(plane, "workspace_work_item_types", None)
if not callable(getattr(type_api, "list", None)):
return []
unowned: list[dict[str, Any]] = []
for row in list_workspace_work_item_types(plane, workspace_slug):
object_id = getattr(row, "id", None)
if object_id is None:
continue
if any(is_work_item_type_named(row, name) for name in FIXTURE_WORK_ITEM_TYPE_NAMES):
continue
unowned.append({"kind": "work_item_type", "id": object_id, "name": (getattr(row, "name", "") or "").strip()})
return unowned


def _sentinel_description(artifact: dict[str, Any]) -> str:
kind = str(artifact["kind"]).replace("_", " ")
return f"{kind} {artifact['name']!r} ({artifact['id']})"


def delete_sentinel_workspace_artifacts(
plane: Any,
workspace_slug: str,
artifacts: list[dict[str, Any]],
*,
yes: bool,
) -> tuple[int, int]:
"""Delete explicitly selected sentinel artifacts. Returns (deleted, failed)."""
if not yes:
return 0, 0
deleted = failed = 0
for artifact in artifacts:
try:
if artifact["kind"] == "customer":
plane.customers.delete(workspace_slug=workspace_slug, customer_id=artifact["id"])
elif artifact["kind"] == "release_tag":
plane.releases.tags.delete(workspace_slug=workspace_slug, tag_id=artifact["id"])
elif artifact["kind"] == "customer_property":
plane.customers.properties.delete(workspace_slug=workspace_slug, property_id=artifact["id"])
elif artifact["kind"] == "work_item_type":
plane.workspace_work_item_types.delete(workspace_slug=workspace_slug, type_id=artifact["id"])
elif artifact["kind"] == "work_item_property":
plane.workspace_work_item_properties.delete(
workspace_slug=workspace_slug,
property_id=artifact["id"],
)
else:
raise ValueError(f"unknown sentinel kind: {artifact['kind']}")
deleted += 1
print(f" deleted sentinel {_sentinel_description(artifact)}")
except Exception as exc:
failed += 1
print(f" FAILED sentinel {_sentinel_description(artifact)}: {exc}", file=sys.stderr)
return deleted, failed


def _cleanup_sentinels(plane: Any, workspace_slug: str, *, yes: bool, unowned: bool = False) -> int:
artifacts = list_sentinel_workspace_artifacts(plane, workspace_slug)
others = list_unowned_workspace_work_item_types(plane, workspace_slug)
if unowned:
artifacts = artifacts + others
print(f"workspace={workspace_slug} sentinel_matches={len(artifacts)}")
if others and not unowned:
# Never let a zero match count imply a clean workspace while these sit here.
print(f"note: {len(others)} workspace work item type(s) present that this tool did not create:")
for artifact in others:
print(f" {_sentinel_description(artifact)}")
print(" add --unowned to delete them too")
if not artifacts:
print("nothing to delete")
return 0
if not yes:
for artifact in artifacts:
print(f" would delete sentinel {_sentinel_description(artifact)}")
print("dry-run: re-run with --sentinels --yes to delete these sentinel fixture(s)")
return 0
deleted, failed = delete_sentinel_workspace_artifacts(plane, workspace_slug, artifacts, yes=True)
print(f"summary: deleted={deleted} failed={failed} matched={len(artifacts)}")
return 1 if failed else 0


def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(description="Clean up leftover eval fixtures (dry-run by default)")
p.add_argument("--prefix", type=str, default="EVAL ", help='Project name prefix (default: "EVAL ")')
p.add_argument(
"--sentinels",
action="store_true",
help="Clean fixed-name workspace sentinels instead of projects",
)
p.add_argument(
"--unowned",
action="store_true",
help="With --sentinels, also delete workspace work item types this harness never creates",
)
p.add_argument(
"--yes",
action="store_true",
help="Actually delete matched objects (default is dry-run list only)",
)
args = p.parse_args(argv)

from evals.seed import make_plane_client

try:
plane, workspace_slug = make_plane_client()
except RuntimeError as exc:
print(f"error: {exc}", file=sys.stderr)
return 2

if args.sentinels:
return _cleanup_sentinels(plane, workspace_slug, yes=args.yes, unowned=args.unowned)
if args.unowned:
print("error: --unowned only applies with --sentinels", file=sys.stderr)
return 2

projects = list_projects_with_prefix(plane, workspace_slug, args.prefix)
print(f"workspace={workspace_slug} prefix={args.prefix!r} matches={len(projects)}")
for proj in projects:
pid = getattr(proj, "id", "?")
name = getattr(proj, "name", "?")
ident = getattr(proj, "identifier", "")
print(f" {name!r} id={pid} identifier={ident}")

if not projects:
print("nothing to delete")
return 0

if not args.yes:
print(f"dry-run: would delete {len(projects)} project(s); re-run with --yes to delete")
return 0

deleted, failed = delete_projects(plane, workspace_slug, projects, yes=True)
print(f"summary: deleted={deleted} failed={failed} matched={len(projects)}")
return 1 if failed else 0


if __name__ == "__main__":
raise SystemExit(main())
Loading