feat: implement Moss CI evaluation platform (Tasks 1-16)#1
Open
1-ztc wants to merge 44 commits into
Open
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
- CLIBackend now tokenizes moss_command, so MOSS_CLI_COMMAND can point at an interpreter + script (e.g. 'node /abs/cli.js --config-file ...') not just a PATH-resolved binary. - CLI `run` now invokes a real MossRunner by default; add --mock to keep scaffold ([mock]) behavior when no Moss instance is available. - Add examples/moss_smoke.yaml: 4-test smoke suite (contains evals) used to verify the real Moss end-to-end path (Task 16 Step 10). Verified against a real Moss CLI: 4 passed, 0 failed via MOSS_CLI_COMMAND='node .../dist/cli.js --config-file .../config.json' moss-ci run examples/moss_smoke.yaml --no-fail-fast Co-Authored-By: Claude <noreply@anthropic.com>
Moss writes session logs (.moss/sessions/*.jsonl) into the cwd when invoked. These are runtime artifacts (and were accidentally committed in the previous commit). Add .moss/ to .gitignore and remove them from the tree. Co-Authored-By: Claude <noreply@anthropic.com>
1-ztc
force-pushed
the
feat/implementation
branch
from
July 12, 2026 09:26
d978495 to
88cee3b
Compare
- examples/moss_capabilities.yaml: a real-value test suite where every contains target is something Moss can only answer by actually invoking tools or reasoning — e.g. a random token (ZETA-7741-NOVA) planted in examples/fixtures/secret_config.json that Moss must read_file to know. Covers: file-read+extract, code-bug detection (regex, case-insensitive), multi-step counting, and a flake-detection stability test. - Fix infinite recursion in flake detection: FlakeDetector.detect called executor._execute_test, which re-entered the flake branch for the same plan. Split _execute_test into a flake gate + _run_test_once (single non-flake run); FlakeDetector now calls _run_test_once so re-runs don't recurse. - Verified end-to-end against real Moss: 5 passed, 0 failed (4 read/tool tests + 1 stability test run 3x, all pass). Co-Authored-By: Claude <noreply@anthropic.com>
- `run` now persists each PipelineResult to the RunRepository (SQLite by
default) and prints the run_id, so a run is comparable later.
- `history` lists persisted runs (id, pipeline, status, summary).
- `status <run_id>` shows per-test pass/fail for a run.
- `diff <prev> <curr>` runs DiffEngine.compare and prints new_failures /
fixed / improved / degraded — the core CI value: not "did it pass" but
"did it regress since last run".
- `logs <run_id>` shows stored Moss output per test.
Previously these commands were scaffold stubs ("not available — API not
running"). The CLI is now a usable local CI: run → diff → catch regressions.
Co-Authored-By: Claude <noreply@anthropic.com>
A reference workflow for Moss forks (D-Robotics/moss): on push/PR, install moss-ci from 1-ztc/ci_test, build Moss, and run the capability suite (moss_capabilities.yaml) against the freshly-built Moss. Fails the PR if a capability regresses. Key mechanics, validated locally: - moss-ci ships the CLI but not the example suites, so the workflow also fetches the examples/ tree from the same ref and stages it into the checkout (the suite uses workdir: ./examples/fixtures, resolved against the moss-ci cwd = the Moss checkout root). - Moss ignores env vars for model settings, so a temporary config.json is written at runtime from the MOSS_API_KEY repo secret; the file lives only on the one-shot runner. To activate in a Moss fork: copy this file to <moss-fork>/.github/workflows/moss-ci.yml and add the MOSS_API_KEY secret. Note: this gates on pass/fail. Cross-run regression DIFF (new_failure/fixed) needs a shared result store across runners — a follow-up. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
shlex.split() with posix=True (the default) eats backslashes as shell escapes: 'C:\Users\tongchun.zhao\...\config.json' becomes 'C:Userstongchun.zhao...config.json', so Moss can't read --config-file and exits 3 with "needs a model configuration". Use posix=False on Windows (os.name == 'nt') for the command-prefix split so Windows paths survive. Forward-slash paths (Linux CI runners) were already fine; this fixes local Windows runs. Prompt tokenization is unchanged (still posix=True) so the bash -c test stays green. Co-Authored-By: Claude <noreply@anthropic.com>
CI failed with "ModuleNotFoundError: No module named 'aiosqlite'" because aiosqlite was only in the dev extras — the CLI's run/history/status/diff persist to SQLite at runtime, so it must be a runtime dependency. Move it to [project].dependencies. Also collapse the CLI `run` command's two asyncio.run() calls (run tests, then save) into one async function. The second asyncio.run() raised "cannot be called from a running event loop" on some platforms and left a stale loop; a single run_and_save fixes it. Persistence is wrapped in try/except so a DB hiccup never masks the actual test results. Co-Authored-By: Claude <noreply@anthropic.com>
The CLI now lets a run result travel outside the SQLite DB so CI can diff consecutive runs (the core CI value: not "did it pass" but "did it regress since last run"). CLI: - `moss-ci export [run_id] -o <file.json>` — write a run (or the latest run if run_id omitted) to JSON. Omit run_id is for CI where the id isn't easily captured. - `moss-ci diff-files <prev.json> <curr.json>` — read two exported runs and report new_failures/fixed/improved/degraded. - Refactored the DB-backed `diff` command's print into `_print_diff`, shared with `diff-files` so both produce identical output. CI workflow (reference at .github/workflows/moss-ci.yml, copied to the Moss fork): after running the capability suite, restore the last run's JSON from a per-branch cache, export the current run, `diff-files` the two into $GITHUB_STEP_SUMMARY (visible on the PR Checks tab), then cache the current run as next run's baseline. First run on a branch establishes a baseline; subsequent runs report the regression diff. Verified end-to-end locally: baseline (5 pass) → tightened eval (1 fail) reports "1 new failure"; reversing reports "1 fixed". Co-Authored-By: Claude <noreply@anthropic.com>
…ce/tool_args
The tool_sequence and tool_args evaluators were untested against real Moss
because CLIBackend only captured stdout text — moss_result.tool_calls was
always empty. Now CLIBackend reads Moss's session jsonl (the one Moss writes
to <cwd>/.moss/sessions/ per invocation) and extracts Anthropic-style
tool_use blocks, converting {"name","input"} to moss-ci's {"tool","args"}.
Snapshot existing sessions before the run, then parse only the NEW session
file(s) so stale tool-call history can't leak into the current run's
result. Robust to missing/empty/malformed sessions (returns []).
moss_capabilities.yaml gains a "工具调用-顺序与参数" test using
tool_sequence (read_file, strict) + tool_args (path contains secret_config),
verified passing against real Moss: Moss called read_file with
path=secret_config.json and both evaluators passed.
Co-Authored-By: Claude <noreply@anthropic.com>
Covers: quickstart (env vars + run + diff), CI activation in a Moss fork, cross-run regression diff via cache, custom Anthropic-protocol endpoints (e.g. horizon — needs the Bearer-auth support in moss fork 2026_07_08), the 5 evaluators, tool-call extraction from Moss sessions, and how to write capability tests that Moss can only pass by actually working. No secrets — key handling points at GitHub Secrets / local files only. Co-Authored-By: Claude <noreply@anthropic.com>
The extract-token/field prompts previously contained the expected answer in the prompt text itself, so Moss could pass by echoing the prompt without actually reading the file — masking real read_file regressions. Reword prompts to ask for the field VALUE without revealing it, so Moss must truly read_file to answer. (Used to verify a real read_file regression is caught — see moss fork 2026_07_08.) Co-Authored-By: Claude <noreply@anthropic.com>
Upgrade the capability suite from 6 baseline tests to a 13-test matrix covering Moss's core agent capabilities, split into CI-cost-controlled layers. CLIBackend (moss_ci/runner/cli_backend.py): - Extract files_modified from the Moss session (write_file/edit_file/ apply_patch/move_file tool_use input.path) → MossResult.files_modified, so the side_effect evaluator (file_modified) works against real Moss. - Insert `--` before the prompt argv so prompt text like "python -m pytest" isn't parsed as Moss flags (-m → --model → HTTP 400). env-injection test rewritten to a fixture script (bash -c no longer viable with --). CLI (moss_ci/cli/main.py): - `run --tag <tag>` filters tests by TestConfig.tags, enabling layer split: push/PR runs `--tag quick`, manual/nightly runs the full layer. Suite (examples/moss_capabilities.yaml, v2.0): 13 tests across A. tool calls (read/write/multi-step sequence + tool_args) B. file side effects (write/modify, side_effect file_modified) C. multi-step end-to-end (autonomous bug-fix via task:, tests_pass) D. code understanding (division-by-zero, empty-list, count) E. stability (flake_detection runs=3 on key tests) F. quality (llm_judge code review) Fixtures: buggy.py + test_buggy.py (C1), to_transform.txt (C2). Workflow (.github/workflows/moss-ci.yml): two layers — quick on push/PR, full on workflow_dispatch + nightly schedule; per-layer cache keys so diffs compare within the same layer; MOSS_CI_JUDGE_API_URL secret wired into the run step. Verified locally: quick layer (9 tests) all pass against real Moss (deepseek-v4-pro); files_modified + side_effect confirmed; tool_sequence strict ordering confirmed. C1 end-to-end depends on Moss autonomous maxTurns config (full layer, manual/nightly). Co-Authored-By: Claude <noreply@anthropic.com>
…-insensitive)
C1 (autonomous bug-fix) was failing even when Moss fixed the bug and ran
the tests, because tests_pass checked for the exact uppercase pytest
banner "PASSED"/"OK" — but Moss reports results in natural language
("All 4 tests pass", "tests passed"). Match "pass" case-insensitively
(covers pass/passed/passing). Drop the bare "ok" match — it's a substring
of "broken" and caused false positives. tests_fail similarly matches
"fail"/"error" case-insensitively.
This was the real blocker for the C1 end-to-end test, not maxTurns (Moss
completes the fix in ~5 tool calls, well under the 64-turn default).
Verified: Moss reads buggy.py → edits it → runs pytest → reports "All 4
tests pass" → tests_pass eval now passes.
Co-Authored-By: Claude <noreply@anthropic.com>
Rewrite the capability-suite section for v2.0: 13 tests across 6 dimensions split into quick (push/PR) and full (manual/nightly) layers, all 5 evaluators in use, tool_calls + files_modified extraction, and the local end-to-end verification result (quick 9 pass, full 4 pass incl. C1 autonomous bug-fix 3/3). Records the C1 fix (tests_pass case-insensitive, maxTurns wasn't the blocker) and adds guidance on writing tests (answer-source-proof prompts, tags, flake_detection). Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The llm_judge evaluator was inert: it returned a fake score 3.0 when no endpoint was configured (F1 always "passed") and called an OpenAI /chat/completions shape that didn't match the real endpoint. - Rewrite _call_judge for Anthropic /v1/messages + Bearer auth, reading MOSS_CI_JUDGE_API_URL + MOSS_CI_JUDGE_API_KEY from env. Host-based auth detection mirrors the Moss fork: Bearer for non-anthropic gateways (horizon at llmapi.horizon.auto), x-api-key for api.anthropic.com. - Unconfigured judge now marks the eval error (score=None) instead of faking 3.0 — no silent pass, no phantom score polluting the diff engine. - Robust score parsing: strip ```json fences, JSON, then regex fallback for prose like "score is 4". - Fix dimension threshold bug: min==0 (default) dimensions are recorded but don't gate pass, instead of letting any score through. - Default judge_model -> HORIZON-GLM; F1/llm_judge.yaml updated. - Add 8 real tests (success/fence/prose/http-error/bearer/apikey/dim). Co-Authored-By: Claude <noreply@anthropic.com>
Three independent bugs found while wiring up judge end-to-end: - flake_runs were never persisted (RunRepository skipped the field), so `status`/`logs` showed "flake" with no per-run breakdown. Add a JSON column + serialize/deserialize + a lightweight ALTER TABLE migration so existing dev DBs get the column. - CLI --concurrency defaulted to 10, ignoring each suite's YAML max_concurrency. The inner per-suite semaphore masked it, but the semantics were muddled and fragile. Default is now None (no global cap); the suite's own max_concurrency is the real limit, --concurrency N caps it. - Moss wrote into the source fixtures dir (hello.txt etc. residue), so the next run's create-file test saw the file exist and skipped -> failed. Executor now stages a throwaway copy of workdir per run (ignoring .moss/__pycache__), so source fixtures stay clean across runs. - logs/status now render flake per-run verdicts (+ judge score) since the data is finally available. Co-Authored-By: Claude <noreply@anthropic.com>
`moss-ci run` exits 1 when tests fail, which skipped the Export/diff steps — so the regression-diff report (CI's core value) never ran exactly when tests failed, and the baseline cache got corrupted copying a missing file. Add if: always() to Restore/Export/diff; guard Save with || true and emit a has_baseline flag; cache only when a baseline exists. Also inject MOSS_CI_JUDGE_API_KEY (URL already wired) so F1 can really score on CI. Update USAGE/PROJECT_STATE: judge now uses Anthropic protocol + Bearer at https://llmapi.horizon.auto (HORIZON-GLM); unconfigured = error, not fake 3.0. Co-Authored-By: Claude <noreply@anthropic.com>
On CI the GitHub runner can't reach llmapi.horizon.auto (ConnectTimeout — Chinese IP, runner is US-west), so F1's judge failed and blocked the PR behind branch protection. The judge code is correct (scores 5.0 when reachable), so failing the whole suite on a network gap was wrong. - Distinguish an unreachable endpoint (network) from a real error. A network failure (httpx TransportError/ConnectTimeout/etc) now SKIPS the eval — it's environmental, auto-recovers when reachable. Config missing / HTTP 401 / unparseable score still hard-fail (never fake a pass). - EvalResult.skipped flag; executor excludes skipped evals from the pass verdict (all-skipped -> test "skipped"); flake detector treats an all-skipped set as skipped. - Short connect timeout (10s) so an unreachable host fails fast instead of burning 120s × 3 flake runs. - Readable error: httpx ConnectTimeout has empty str(), so lead with the type name instead of an unreadable error="". - Persist skipped (in details JSON, no new migration). - status/logs render skipped judge runs with the reason. Local (reachable) still scores 5.0/3; CI (unreachable) will skip F1 and the suite stays green. Swap in a public-reachable endpoint to re-enable. Co-Authored-By: Claude <noreply@anthropic.com>
Aligns moss-ci with the upstream Moss repo (D-Robotics/moss, MIT) so the tool can live alongside or within the organization without a license ambiguity blocking compliance review. Without an explicit license the code defaulted to all-rights-reserved. Co-Authored-By: Claude <noreply@anthropic.com>
The tool and test suites now live at D-Robotics/moss-ci (main), so the upstream Moss CI no longer depends on a personal fork (1-ztc/ci_test). Updates the workflow's pip install + git clone sources and the docs to match. Branch ref changes from feat/implementation to main. Co-Authored-By: Claude <noreply@anthropic.com>
The current.json (per-test moss_output + moss_tool_calls) was only written to the runner's /tmp and lost when the job ended, leaving no way to see what Moss actually returned when tests fail. Upload it as a short-lived artifact so failures can be diagnosed without re-running. Marked with a comment so it can be removed once the suite is green. Co-Authored-By: Claude <noreply@anthropic.com>
When tests come back with empty moss_output, this pinpoints the failing layer: L1 TCP reachability to the gateway, and L2 an authenticated /v1/chat/completions call (HTTP status only). Key read from env, never echoed. Distinguishes network-blocked (GitHub IP not allowlisted) from auth/path errors. Remove once the suite is green. Co-Authored-By: Claude <noreply@anthropic.com>
F1's llm_judge eval keeps timing out from the runner. Add a parallel L1 (TCP) + L2 (authenticated /v1/messages, host-based Bearer/x-api-key) probe for MOSS_CI_JUDGE_API_URL so we can see whether it's network blocked (GitHub IP not allowlisted) vs auth/path. Key never echoed. Co-Authored-By: Claude <noreply@anthropic.com>
The llm_judge (F1) eval skips cleanly when its endpoint is unreachable from the runner (internal/VPC gateway GitHub hosted runners can't reach) — it's not an error and doesn't turn CI red. Two clarity fixes: - Update the secret comment to say "skipped, not error" instead of the old "marked error" wording, and note unreachable endpoints auto-recover. - In the job summary, detect skipped llm_judge evals in the export and print a one-line "llm_judge skipped" note with the reason, so it's visible in PR Checks instead of buried in step logs. No behavior change to pass/fail — F1 still skips, just no longer a mystery to anyone reading the CI output. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
完整实现 Moss CI —— 面向 Moss 智能体的多维度回归评测平台。涵盖数据模型、YAML 解析、Pipeline 引擎、Moss Runner(CLI/API/SDK 三后端)、5 种评估器、持久化存储、FastAPI 服务、CLI、Flake 检测、回归分析、React Dashboard,以及从 mock 到真实 Moss 的切换。
本 PR 一次性落地实现计划的全部 16 个 Task / 8 个 Phase,测试 68 passed。
架构
分层架构:Pydantic 数据模型 → YAML 解析 → Pipeline 引擎(DAG 调度 + 并行执行)→ Moss Runner → Evaluator → FastAPI + CLI + React Dashboard。PostgreSQL 存元数据(开发用 SQLite),MinIO 存日志/产物,Redis 做队列和缓存。
技术栈:Python 3.11+ / Pydantic v2 / FastAPI / Typer / SQLAlchemy async / asyncpg / Redis / MinIO / PyYAML / NetworkX / Structlog / Tenacity / SSE-starlette / React + TypeScript + Recharts + Vite
实现清单(16 Task / 8 Phase)
测试
68 passed, 0 failed(38 个 deprecation warnings 均为
datetime.utcnow(),非本 PR 引入)。测试覆盖各模块:models / parser / engine / runner(CLI+API+SDK)/ evaluator / storage(db+ext)/ api(core+advanced)/ cli / flake / diff / switchover。
过程中处理的实现计划缺陷
实现计划(spec)自身的测试与实现代码有若干对不上之处,已逐一修正:
bash -c 'echo $X'被当成单个 argv,测试挂。改用shlex.split(prompt)拆分。patch(return_value=AsyncMock)对异步方法返回非 awaitable;AsyncMock.json()返回 coroutine。改用MagicMockresponse(.json()同步)+AsyncMockpost。get/list访问嵌套关系触发 lazy load 崩(MissingGreenlet)。加selectinload链式预加载 suites→tests→evals。ttl=-1在set里被else 0压成「永不过期」。重写 expiry 逻辑:>0未来、==0永不、<0已过期。flake,测试期望pass。保留 flake 状态(区分不稳定通过),修正测试期望为flake。_repo()为 async 但调用处未 await。统一为repo = await _repo()。_execute_suite的 fail_fast 遍历裸 coroutine 调.done()崩(Task 4 遗留,被真实 Moss 失败路径暴露)。用asyncio.Task+asyncio.wait(return_when=FIRST_COMPLETED)重写,实现真正的 fail_fast 取消。ASGITransport(测试)不触发 FastAPI lifespan,DB 不初始化。Database.init()改为幂等,_repo()兜底 ensure。已知待办(计划内明确推迟)
以下功能在实现计划「Scope Notes」中标注为有意推迟,留作后续 spec:
{{ model }}替换)、suite 继承、条件执行、matrix 策略端到端冒烟(需配环境)
Task 16 Step 10 的真实 Moss 冒烟需运行环境提供 Moss 后端之一 + judge 端点:
🤖 Generated with Claude Code