From 24058cf1c48bf72dbe66e758cb1a07924c9253f3 Mon Sep 17 00:00:00 2001 From: Snowson <15168246+Snowson@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:37:33 +0800 Subject: [PATCH 1/4] =?UTF-8?q?harness=EF=BC=9A=E8=A3=85=E5=85=A5=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E5=BC=95=E6=93=8E=20delivery-harness=200.1.0=EF=BC=8C?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=B9=B3=E9=93=BA=E7=9A=84=20harness/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .harness/engine(内置副本)与 engine.lock(版本、引擎提交、目录树哈希)。 - 本项目的规则与配置移到 .harness/config(rules.toml、autonomy.toml,新增 checks.toml: Agent 身份、运行时、源码与语言、verify 检查清单、发版版本文件、变异目标、回放套件、周报机器人); 棘轮状态移到 .harness/state,事故回放用例移到 .harness/project。 - 新入口 bin/harness。 Task: T008 Co-Authored-By: Claude Opus 5.5 --- {harness => .harness/config}/autonomy.toml | 0 .harness/config/checks.toml | 113 +++++++++ {harness => .harness/config}/rules.toml | 21 +- .harness/engine.lock | 6 + .harness/engine/__init__.py | 3 + .harness/engine/agents/__init__.py | 0 .../engine/agents}/dispatch.py | 65 +++-- .../engine/agents}/dispatch_host.py | 0 .harness/engine/agents/identity.py | 26 ++ {harness => .harness/engine/agents}/review.py | 38 +-- .../engine/agents}/review_pack.py | 12 +- .harness/engine/checks/__init__.py | 0 .../engine/checks}/acceptance.py | 13 +- .../engine/checks}/base_tests.py | 12 +- .../engine/checks}/base_tests_runner.py | 0 .../engine/checks}/docs_check.py | 11 +- .../engine/checks}/evidence.py | 18 +- .../engine/checks}/hygiene.py | 8 +- .harness/engine/checks/integrity.py | 74 ++++++ {harness => .harness/engine/checks}/mutate.py | 64 ++--- .harness/engine/checks/quality.py | 133 +++++++++++ .../engine/checks}/r1_checks.py | 83 ++----- .../engine/checks}/release_check.py | 44 ++-- {harness => .harness/engine/checks}/replay.py | 43 ++-- .../engine/checks}/taskbook.py | 17 +- {harness => .harness/engine/checks}/verify.py | 142 +++++------ .harness/engine/cli.py | 71 ++++++ .harness/engine/core/__init__.py | 0 .harness/engine/core/cases.py | 59 +++++ {harness => .harness/engine/core}/common.py | 74 +++++- .harness/engine/core/install.py | 107 +++++++++ .../engine/core}/shell_structure.py | 10 +- .harness/engine/guards/__init__.py | 0 .../engine/guards}/command_guard.py | 24 +- .../engine/guards}/git_guard.py | 37 ++- .harness/engine/lang/__init__.py | 20 ++ .harness/engine/lang/python.py | 45 ++++ .harness/engine/lang/swift.py | 135 +++++++++++ .../engine/prompts}/dispatch_prompt.md | 2 +- .../engine/prompts}/review_prompt.md | 2 +- .harness/engine/reports/__init__.py | 0 .../engine/reports}/metrics.py | 17 +- .../engine/reports}/weekly.py | 25 +- .harness/engine/routing/__init__.py | 0 .../engine/routing}/policy.py | 25 +- {harness => .harness/engine/routing}/risk.py | 29 ++- .../engine/routing}/run_check.py | 9 +- {harness => .harness/project}/replay_cases.py | 61 ++--- .../state}/acceptance-gaps.txt | 0 .../state}/mutation-baseline.json | 0 .../state}/quality-baseline.json | 0 .../state}/taskbook-exempt.txt | 0 bin/harness | 12 + harness/README.md | 94 -------- harness/quality.py | 222 ------------------ 55 files changed, 1251 insertions(+), 775 deletions(-) rename {harness => .harness/config}/autonomy.toml (100%) create mode 100644 .harness/config/checks.toml rename {harness => .harness/config}/rules.toml (80%) create mode 100644 .harness/engine.lock create mode 100644 .harness/engine/__init__.py create mode 100644 .harness/engine/agents/__init__.py rename {harness => .harness/engine/agents}/dispatch.py (92%) rename {harness => .harness/engine/agents}/dispatch_host.py (100%) create mode 100644 .harness/engine/agents/identity.py rename {harness => .harness/engine/agents}/review.py (94%) rename {harness => .harness/engine/agents}/review_pack.py (94%) create mode 100644 .harness/engine/checks/__init__.py rename {harness => .harness/engine/checks}/acceptance.py (94%) rename {harness => .harness/engine/checks}/base_tests.py (92%) rename {harness => .harness/engine/checks}/base_tests_runner.py (100%) rename {harness => .harness/engine/checks}/docs_check.py (94%) rename {harness => .harness/engine/checks}/evidence.py (97%) rename {harness => .harness/engine/checks}/hygiene.py (96%) create mode 100644 .harness/engine/checks/integrity.py rename {harness => .harness/engine/checks}/mutate.py (80%) create mode 100644 .harness/engine/checks/quality.py rename {harness => .harness/engine/checks}/r1_checks.py (51%) rename {harness => .harness/engine/checks}/release_check.py (56%) rename {harness => .harness/engine/checks}/replay.py (81%) rename {harness => .harness/engine/checks}/taskbook.py (96%) rename {harness => .harness/engine/checks}/verify.py (64%) create mode 100644 .harness/engine/cli.py create mode 100644 .harness/engine/core/__init__.py create mode 100644 .harness/engine/core/cases.py rename {harness => .harness/engine/core}/common.py (63%) create mode 100644 .harness/engine/core/install.py rename {harness => .harness/engine/core}/shell_structure.py (97%) create mode 100644 .harness/engine/guards/__init__.py rename {harness => .harness/engine/guards}/command_guard.py (93%) rename {harness => .harness/engine/guards}/git_guard.py (87%) create mode 100644 .harness/engine/lang/__init__.py create mode 100644 .harness/engine/lang/python.py create mode 100644 .harness/engine/lang/swift.py rename {harness => .harness/engine/prompts}/dispatch_prompt.md (92%) rename {harness => .harness/engine/prompts}/review_prompt.md (97%) create mode 100644 .harness/engine/reports/__init__.py rename {harness => .harness/engine/reports}/metrics.py (93%) rename {harness => .harness/engine/reports}/weekly.py (96%) create mode 100644 .harness/engine/routing/__init__.py rename {harness => .harness/engine/routing}/policy.py (95%) rename {harness => .harness/engine/routing}/risk.py (93%) rename {harness => .harness/engine/routing}/run_check.py (97%) rename {harness => .harness/project}/replay_cases.py (93%) rename {harness => .harness/state}/acceptance-gaps.txt (100%) rename {harness => .harness/state}/mutation-baseline.json (100%) rename {harness => .harness/state}/quality-baseline.json (100%) rename {harness => .harness/state}/taskbook-exempt.txt (100%) create mode 100755 bin/harness delete mode 100644 harness/README.md delete mode 100644 harness/quality.py diff --git a/harness/autonomy.toml b/.harness/config/autonomy.toml similarity index 100% rename from harness/autonomy.toml rename to .harness/config/autonomy.toml diff --git a/.harness/config/checks.toml b/.harness/config/checks.toml new file mode 100644 index 0000000..faf2aba --- /dev/null +++ b/.harness/config/checks.toml @@ -0,0 +1,113 @@ +# delivery-harness 项目配置:检查清单、源码与语言、运行时、目录约定、身份。 +# 改动本文件属于 R3(护栏本身),由用户批准。 +schema = 1 + +[identity] +# 执行方与 Agent 推送、开 PR、评论使用的 GitHub 账号(bin/as-agent、派发的提交身份)。 +agent_login = "Snowson" +agent_email = "15168246+Snowson@users.noreply.github.com" + +[runtime] +# 开发包运行时 venv:bin/* 与 git 钩子优先用它,没有时退回 PATH 上的 python3。 +python = "scratch/iterm-probe-venv/bin/python" +# 派发槽位清理未跟踪文件时保留、并以符号链接指向主目录的路径。 +preserve = ["scratch/iterm-probe-venv"] + +[sources] +# 产品代码(R1 判定:签名不变、无新依赖、无迁移)。 +code = ["scripts/**", "native/**"] +# UI 代码:policy 对 UI 改动不自动合并。 +ui = ["native/**"] +# 质量棘轮:Python 用 ruff C901 与体量,Swift 用轻量解析。 +python_dirs = ["scripts"] +swift_dirs = ["native"] +# 数据迁移语句的检查只看产品代码。 +migrations = "scripts/migrations.py" + +[release] +# 发版前核对:tag 名 = "v" + 版本号;两处版本号必须同步。 +version_file = "scripts/build_inbox_app.py" +short_version_var = "BUNDLE_SHORT_VERSION" +build_version_var = "BUNDLE_VERSION" + +[[mutation.targets]] +name = "价格转换与防护" +file = "scripts/pricing_fetch.py" +functions = ["transform", "_price_jump", "_drop_conflicting_aliases"] +tests = ["test_pricing_fetch", "test_properties.PricingTransformProperties"] + +[[mutation.targets]] +name = "热力金额分级" +file = "scripts/daily_report.py" +functions = ["cost_thresholds", "cost_level"] +tests = ["test_properties.CostThresholdProperties", "test_daily_report"] + +[[mutation.targets]] +name = "过去日报告合并" +file = "scripts/daily_report.py" +functions = ["_merge_day_tasks", "_task_total"] +tests = ["test_properties.MergeNoDowngradeProperties", "test_daily_report"] + +[[mutation.targets]] +name = "模型规范名" +file = "scripts/model_names.py" +functions = ["canonical"] +tests = ["test_model_names"] + +[[mutation.targets]] +name = "金额计算" +file = "scripts/usage_cost.py" +functions = ["price_for_day", "model_cost", "cost_for_models", "merge_cost", "convert_display"] +tests = ["test_usage_cost"] + +[[mutation.targets]] +name = "用量汇总" +file = "scripts/usage_report.py" +functions = ["period_bounds", "previous_anchor", "_merge_models", "_dimension_rows"] +tests = ["test_usage_report", "test_golden"] + +[verify] +requirements = "requirements-dev.txt" +pinned_tools = ["ruff"] +order = [ + "tools", "integrity", "lint", "hygiene", "quality", "docs", "acceptance", "taskbook", + "python-tests", "swift-policy", "swift-runner", "zcode-selftest", "replay", +] + +[[verify.checks]] +name = "lint" +tiers = ["quick", "default", "full"] +command = ["{python}", "-m", "ruff", "check", "scripts", "tests"] + +[[verify.checks]] +name = "python-tests" +command = ["{python}", "-W", "error::ResourceWarning", "-m", "unittest", "discover", "-s", "tests"] + +[[verify.checks]] +name = "swift-policy" +shell = "mkdir -p build && xcrun swiftc -parse-as-library native/InboxPolicy.swift native/Shared/WidgetSnapshot.swift tests/InboxPolicyTests.swift -o build/policy-tests && ./build/policy-tests" +requires = "macos" +why_skipped = "非 macOS,Swift 检查由 macOS CI 负责" + +[[verify.checks]] +name = "swift-runner" +shell = "mkdir -p build && xcrun swiftc -parse-as-library native/ProcessRunner.swift tests/ProcessRunnerTests.swift -o build/runner-tests && ./build/runner-tests" +requires = "macos" +why_skipped = "非 macOS,Swift 检查由 macOS CI 负责" + +[[verify.checks]] +# AGENTS.md 要求的无 UI 自检(身份比对、搜索框结构、点击状态机、run loop)。 +name = "zcode-selftest" +shell = "mkdir -p build && xcrun swiftc native/ZcodeFocus.swift -o build/zcode-focus && build/zcode-focus --self-test" +requires = "macos" +why_skipped = "非 macOS,Swift 检查由 macOS CI 负责" + +[replay] +# 回放用例中 tests 写 "swift-policy" 时运行的命令(macOS)。 +[replay.suites.swift-policy] +shell = "mkdir -p build && xcrun swiftc -parse-as-library native/InboxPolicy.swift native/Shared/WidgetSnapshot.swift tests/InboxPolicyTests.swift -o build/policy-tests && ./build/policy-tests" +requires = "macos" + +[reports] +# 自动合并用的批准 App 的机器人账号(周报把它合并或批准的 PR 算作自动合并)。 +bots = ["snowsonz-auto-merge"] diff --git a/harness/rules.toml b/.harness/config/rules.toml similarity index 80% rename from harness/rules.toml rename to .harness/config/rules.toml index 431d870..d199495 100644 --- a/harness/rules.toml +++ b/.harness/config/rules.toml @@ -1,5 +1,5 @@ # 可验证交付 harness 的规则(docs/plans/verifiable-delivery.md)。 -# 改动本文件、harness/ 下任何代码或 .githooks/ 都属于 R3(护栏本身),必须由用户批准。 +# 改动本文件、.harness/ 下任何文件(引擎、配置、棘轮状态、回放用例)或 .githooks/ 都属于 R3(护栏本身),必须由用户批准。 [hygiene] # 已跟踪文件或暂存区命中即失败(v0.8.0 R1:真实快照与编译产物进入公开仓库)。 @@ -47,7 +47,7 @@ r3 = [ ".pi/**", ".zcode/**", ".codex/**", - "harness/**", + ".harness/**", "bin/**", "evals/review/samples.json", "requirements*.txt", @@ -86,17 +86,17 @@ contracts = [ "docs/templates/**", "docs/plans/backlog.md", ] -# 任务书按类别判定(2026-09-28 决定 1,harness/taskbook.py):K7、K8 与 architecture: true 的任务书、 +# 任务书按类别判定(2026-09-28 决定 1,.harness/engine/checks/taskbook.py):K7、K8 与 architecture: true 的任务书、 # 头部不合格或删除任务书为 R2;其余(业务修改、缺陷修复等)为 R0,准入由 CI 中的 verify 检查。 # 类别写低不影响安全:实现 PR 仍按实际改动路径判级。 taskbooks = ["docs/plans/task-*.md"] # 只能缩减的清单:只删不增时按 R0(关闭缺口是期望的方向);有新增行仍按上面的规则判定。 -shrink_only = ["harness/acceptance-gaps.txt", "harness/taskbook-exempt.txt"] +shrink_only = [".harness/state/acceptance-gaps.txt", ".harness/state/taskbook-exempt.txt"] # 黄金快照:任何改动都意味着行为变化,不能算 R1。 golden = ["tests/golden/**"] [taskbook] -# 任务书准入检查(harness/taskbook.py)对「步骤与提交顺序」中列出的文件做交叉核对。 +# 任务书准入检查(.harness/engine/checks/taskbook.py)对「步骤与提交顺序」中列出的文件做交叉核对。 # 触及这些路径的任务属护栏与流程类(K7)。 guard_paths = [ ".github/**", @@ -106,10 +106,11 @@ guard_paths = [ ".pi/**", ".zcode/**", ".codex/**", - "harness/**", + ".harness/**", "bin/verify", "bin/as-agent", "bin/dispatch", + "bin/harness", "ruff.toml", ] # 架构级:数据迁移、新增依赖、公共接口(CLI 输出由黄金快照固定,快照格式)。 @@ -126,13 +127,13 @@ native = ["native/**"] cli = ["bin/session-manager"] [dispatch] -# 派发脚本(harness/dispatch.py,bin/dispatch):仓库同级的固定槽位数、卡死判定与等待 CI 的上限。 +# 派发脚本(bin/dispatch):仓库同级的固定槽位数、卡死判定与等待 CI 的上限。 slots = 3 stall_minutes = 15 ci_timeout_minutes = 60 [review] -# 独立评审(harness/review.py):默认评审方与各评审方的模型、超时。 +# 独立评审(bin/dispatch review):默认评审方与各评审方的模型、超时。 # 用户 2026-09-28 指定:默认由 OpenCode(glm-5.3,只读的 plan 代理)评审,避开执行方 Pi;Pi、Codex(gpt-6-sol、max)可选。 # 换评审方、模型或推理强度后须在校准集上重跑 TPR、TNR 再比较(设计 8.3)。 reviewer = "opencode" @@ -143,10 +144,12 @@ codex_reasoning_effort = "max" timeout_minutes = 30 [guard] -# git 层守卫(.githooks → harness/git_guard.py)。以下操作被拒绝,除非人显式设置覆盖变量: +# git 层守卫(.githooks → bin/harness guard-git)。以下操作被拒绝,除非人显式设置覆盖变量: # 在保护分支上提交、推送保护分支 HARNESS_ALLOW_MAIN=1 # 推送 tag(发版由用户执行) HARNESS_ALLOW_TAG=1 # 本地改写或删除保护分支、移动或删除 tag、强制推送 HARNESS_ALLOW_REWRITE=1 # pre-push 前跳过 verify HARNESS_SKIP_VERIFY=1 # Agent 层守卫会拒绝设置这些变量的命令:覆盖只属于人。 protected_branches = ["main"] +# 执行方(--role implementer)另外不能编辑的项目文件(引擎固定保护 .github、.githooks、.harness 与各 Agent 配置目录)。 +implementer_protected = ["requirements-dev.txt", "ruff.toml"] diff --git a/.harness/engine.lock b/.harness/engine.lock new file mode 100644 index 0000000..f79029a --- /dev/null +++ b/.harness/engine.lock @@ -0,0 +1,6 @@ +{ + "engine": "delivery-harness", + "version": "0.1.0", + "commit": "95d7f67b0d325af53b0bd0e6ec3ab5e13a2f90dc", + "tree": "sha256:10e1808e6f2aa91f3d86be1218a641154e3e42594b3c1feacf8080eeb03041f3" +} diff --git a/.harness/engine/__init__.py b/.harness/engine/__init__.py new file mode 100644 index 0000000..f8d91ff --- /dev/null +++ b/.harness/engine/__init__.py @@ -0,0 +1,3 @@ +"""delivery-harness:AI 编码 Agent 的可验证交付引擎。""" + +__version__ = "0.1.0" diff --git a/.harness/engine/agents/__init__.py b/.harness/engine/agents/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/harness/dispatch.py b/.harness/engine/agents/dispatch.py similarity index 92% rename from harness/dispatch.py rename to .harness/engine/agents/dispatch.py index e4cca26..ab1a256 100644 --- a/harness/dispatch.py +++ b/.harness/engine/agents/dispatch.py @@ -19,7 +19,7 @@ bin/dispatch run docs/plans/task-005-x.md [--resume] [--model M] bin/dispatch status bin/dispatch stop --all - bin/dispatch review [--reviewer opencode|pi|codex|claude-code] 独立评审(harness/review.py) + bin/dispatch review [--reviewer opencode|pi|codex|claude-code] 独立评审(engine/agents/review.py) bin/dispatch review --pending | --watch [--interval 5] 评审全部待评审的 PR(后台常驻用 --watch) """ @@ -38,19 +38,26 @@ from dataclasses import dataclass, field from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) +from engine.agents import dispatch_host +from engine.checks import taskbook +from engine.core.common import ENGINE_DIR, ROOT, git, load_rules, setting -import dispatch_host -import taskbook -from common import ROOT, git, load_rules - -PROMPT_TEMPLATE = Path(__file__).resolve().parent / "dispatch_prompt.md" -AGENT_LOGIN = os.environ.get("AGENT_LOGIN", "Snowson") -AGENT_EMAIL = os.environ.get("AGENT_EMAIL", "15168246+Snowson@users.noreply.github.com") -VENV = "scratch/iterm-probe-venv" +PROMPT_TEMPLATE = ENGINE_DIR / "prompts" / "dispatch_prompt.md" ESCALATION_FILE = "build/dispatch/escalation.md" +def agent_identity() -> tuple[str, str]: + """执行方的提交身份(checks.toml [identity])。邮箱缺省时用 GitHub 的 noreply 形式 `@users.noreply.github.com`。""" + login = os.environ.get("AGENT_LOGIN") or setting("identity", "agent_login", required=True) + email = os.environ.get("AGENT_EMAIL") or setting("identity", "agent_email", f"{login}@users.noreply.github.com") + return login, email + + +def preserved_paths() -> list[str]: + """槽位清理未跟踪文件时保留、并以符号链接指向主目录的路径(checks.toml [runtime] preserve,如本地 venv)。""" + return list(setting("runtime", "preserve", [])) + + class Stop(Exception): """派发在确定性步骤中止(准入、认领、无槽位、守卫预检)。""" @@ -107,7 +114,7 @@ def admit(path: str, root: Path = ROOT, require_on_main: bool = True) -> Task: if not reports: raise Stop(f"{rel} 不是 docs/plans/task-*.md 下的任务书") report = reports[0] - if rel in taskbook.load_exempt(root / "harness" / "taskbook-exempt.txt"): + if rel in taskbook.load_exempt(root / ".harness" / "state" / "taskbook-exempt.txt"): raise Stop(f"{rel} 是登记豁免的历史任务书,只检查头部,不能派发") if require_on_main: problem = taskbook.on_main(rel, root) @@ -187,11 +194,13 @@ def prepare_slot(root: Path, slot: Path, branch: str, resume: bool) -> None: git("fetch", "--quiet", "origin", cwd=slot) start = f"origin/{branch}" if resume else "origin/main" git("checkout", "--quiet", "--force", "-B", branch, start, cwd=slot) - git("clean", "-ffdxq", "-e", VENV, cwd=slot) - venv = root / VENV - if venv.exists() and not (slot / VENV).exists(): - (slot / VENV).parent.mkdir(parents=True, exist_ok=True) - (slot / VENV).symlink_to(venv) + keep = preserved_paths() + git("clean", "-ffdxq", *[arg for path in keep for arg in ("-e", path)], cwd=slot) + for path in keep: + source = root / path + if source.exists() and not (slot / path).exists(): + (slot / path).parent.mkdir(parents=True, exist_ok=True) + (slot / path).symlink_to(source) # ---- 4 守卫 ---- @@ -200,15 +209,20 @@ def prepare_guard(root: Path) -> tuple[Path, str]: """把 origin/main 上的守卫导出到本机目录并确认可运行;返回 (扩展路径, main 的提交)。""" ref = git("rev-parse", "origin/main", cwd=root) bundle = state_dir(root) / "guard" / ref - if not (bundle / "harness" / "command_guard.py").exists(): + # 守卫与它读取的规则都取自 origin/main(.harness/ 与 .pi/);main 还是旧布局(harness/ 平铺)时按旧布局导出。 + legacy = not git("ls-tree", "--name-only", ref, ".harness/engine/cli.py", cwd=root, check=False) + guard_cmd = [str(bundle / "harness" / "command_guard.py")] if legacy \ + else [str(bundle / ".harness" / "engine" / "cli.py"), "guard-command"] + if not Path(guard_cmd[0]).exists(): bundle.mkdir(parents=True, exist_ok=True) - archive = subprocess.run(["git", "archive", ref, "harness", ".pi"], cwd=root, capture_output=True, check=False) + paths = ["harness", ".pi"] if legacy else [".harness", ".pi"] + archive = subprocess.run(["git", "archive", ref, *paths], cwd=root, capture_output=True, check=False) if archive.returncode != 0: raise Stop(f"无法从 origin/main 导出守卫:{archive.stderr.decode(errors='replace').strip()}") subprocess.run(["tar", "-x", "-C", str(bundle)], input=archive.stdout, check=True) extension = bundle / ".pi" / "extensions" / "harness-guard.ts" probe = subprocess.run( - [sys.executable, str(bundle / "harness" / "command_guard.py"), "--format", "json", "--role", "implementer"], + [sys.executable, *guard_cmd, "--format", "json", "--role", "implementer"], input=json.dumps({"command": "gh issue close 1"}), capture_output=True, text=True, check=False, ) if not extension.exists() or probe.returncode != 2: @@ -256,10 +270,13 @@ def worktree_problem(slot: Path, base: str) -> str | None: class Dispatcher: def __init__(self, root: Path, config: Config, github, host, identity: dict[str, str] | None = None): self.root, self.config, self.github, self.host = root, config, github, host - self.identity = identity or { - "GIT_AUTHOR_NAME": AGENT_LOGIN, "GIT_AUTHOR_EMAIL": AGENT_EMAIL, - "GIT_COMMITTER_NAME": AGENT_LOGIN, "GIT_COMMITTER_EMAIL": AGENT_EMAIL, - } + if identity is None: + login, email = agent_identity() + identity = { + "GIT_AUTHOR_NAME": login, "GIT_AUTHOR_EMAIL": email, + "GIT_COMMITTER_NAME": login, "GIT_COMMITTER_EMAIL": email, + } + self.identity = identity self.used_seconds = 0.0 def run_executor(self, task: Task, slot: Path, guard: Path, prompt: str, events: Path) -> dispatch_host.RunResult: @@ -448,7 +465,7 @@ def pr_body(task: Task, attempt: Attempt, number: int, prompt_sha: str) -> str: "- CI 运行(当前 head):见本 PR checks", "- 风险等级与修复证据:见 harness job summary(机器生成)", "", "## 需要人工验收的部分", "", - "见任务书验收表中的人工条目(`python3 harness/acceptance.py --manual`)。", "", + "见任务书验收表中的人工条目(`bin/harness acceptance --manual`)。", "", "🤖 Dispatched by bin/dispatch", ]) + "\n" diff --git a/harness/dispatch_host.py b/.harness/engine/agents/dispatch_host.py similarity index 100% rename from harness/dispatch_host.py rename to .harness/engine/agents/dispatch_host.py diff --git a/.harness/engine/agents/identity.py b/.harness/engine/agents/identity.py new file mode 100644 index 0000000..5b7a60b --- /dev/null +++ b/.harness/engine/agents/identity.py @@ -0,0 +1,26 @@ +"""打印 Agent 的 GitHub 身份(checks.toml [identity]),供 bin/as-agent 等 shell 入口读取。 + + bin/harness identity 输出一行: +""" + +from __future__ import annotations + +import sys + +from engine.core.common import ConfigError + + +def main(argv: list[str] | None = None) -> int: + from engine.agents.dispatch import agent_identity + + try: + login, email = agent_identity() + except ConfigError as error: + print(error, file=sys.stderr) + return 1 + print(f"{login} {email}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/harness/review.py b/.harness/engine/agents/review.py similarity index 94% rename from harness/review.py rename to .harness/engine/agents/review.py index 0ddda88..09dcc93 100644 --- a/harness/review.py +++ b/.harness/engine/agents/review.py @@ -1,8 +1,8 @@ """独立评审:R2 及以上的 PR 由非任务设计方的 Agent 评审,结论写回 PR 供用户参考(目标态设计 8.3,决定 5)。 分离规则:评审方不是该任务的设计方(任务书头部 designer;没有任务书时按提交的 Co-Authored-By 判断); -也不看执行方轨迹,只看任务书、PR 描述、diff 与证据包。默认评审方由 rules.toml [review] reviewer 指定 -(用户 2026-09-28 定为 Pi + glm-5.3);评审方与执行方同一宿主时在评论中标出,供用户判断独立性。 +也不看执行方轨迹,只看任务书、PR 描述、diff 与证据包。默认评审方与各评审方的模型由 rules.toml [review] +指定(不配模型时沿用评审方工具自己的默认设置);评审方与执行方同一宿主时在评论中标出,供用户判断独立性。 结构上保证只读:评审在单独的 worktree(<仓库名>-review)里进行,Pi 只开放 read、grep、find、ls 且不加载 项目文件,Codex 以 `-s read-only` 沙箱运行,Claude Code 只开放 Read、Grep、Glob;评审方的环境没有 GitHub @@ -13,7 +13,7 @@ 分别统计抓住问题的比例(TPR)与放过好改动的比例(TNR),结果写入 evals/review/results/。 bin/dispatch review [--reviewer pi|codex|claude-code] - python3 harness/review.py calibrate --reviewer pi [--limit N] [--jobs N] [--resume evals/review/results/<文件>] + bin/harness review calibrate --reviewer pi [--limit N] [--jobs N] [--resume evals/review/results/<文件>] """ from __future__ import annotations @@ -32,14 +32,11 @@ from queue import Queue from typing import ClassVar -sys.path.insert(0, str(Path(__file__).resolve().parent)) +from engine.agents import dispatch, dispatch_host +from engine.core.common import ENGINE_DIR, ROOT, changed_files, commit_field, git, load_rules +from engine.routing import run_check -import dispatch -import dispatch_host -import run_check -from common import ROOT, changed_files, commit_field, git, load_rules - -PROMPT = Path(__file__).resolve().parent / "review_prompt.md" +PROMPT = ENGINE_DIR / "prompts" / "review_prompt.md" EVALS = ROOT / "evals" / "review" VERDICTS = ("通过", "不通过", "需用户验收") OTHER = {"claude-code": "codex", "codex": "claude-code"} @@ -95,16 +92,19 @@ def read(self, stdout: str, output: Path) -> tuple[str, str]: class CodexReviewer(Reviewer): name = "codex" - def __init__(self, model: str, effort: str | None = None): + def __init__(self, model: str | None, effort: str | None = None): self.model, self.effort = model, effort def argv(self, prompt, workspace, output): + # 没配置模型时沿用 Codex 自己的默认设置。 + model = ["-m", self.model] if self.model else [] effort = ["-c", f'model_reasoning_effort="{self.effort}"'] if self.effort else [] - return ["codex", "exec", "-s", "read-only", "-m", self.model, *effort, "-C", str(workspace), + return ["codex", "exec", "-s", "read-only", *model, *effort, "-C", str(workspace), "--skip-git-repo-check", "-o", str(output), prompt] def read(self, stdout, output): - label = f"{self.model} {self.effort}" if self.effort else self.model + base = self.model or "codex 默认模型" + label = f"{base} {self.effort}" if self.effort else base return (output.read_text(encoding="utf-8") if output.exists() else stdout), label @@ -199,7 +199,7 @@ def make_reviewer(name: str) -> Reviewer: if name == "pi": return PiReviewer(config.get("pi_model")) if name == "codex": - return CodexReviewer(config.get("codex_model", "gpt-6-sol"), config.get("codex_reasoning_effort")) + return CodexReviewer(config.get("codex_model"), config.get("codex_reasoning_effort")) if name == "claude-code": return ClaudeReviewer(config.get("claude_model")) raise SystemExit(f"未知的评审方:{name}") @@ -260,7 +260,7 @@ def review_workspace(root: Path, index: int = 0) -> Path: def checkout(workspace: Path, ref: str) -> None: git("checkout", "--quiet", "--force", "--detach", ref, cwd=workspace) - git("clean", "-ffdxq", "-e", dispatch.VENV, cwd=workspace) + git("clean", "-ffdxq", *[arg for path in dispatch.preserved_paths() for arg in ("-e", path)], cwd=workspace) def review_base(pr: dict, workspace: Path) -> str: @@ -289,7 +289,10 @@ def write_materials(workspace: Path, base: str, pr_text: str, task_text: str, ci (folder / "ci.md").write_text(ci_text or "(校准样本没有 CI 运行)", encoding="utf-8") diff = git("diff", "--no-color", f"{base}...HEAD", cwd=workspace) (folder / "diff.patch").write_text(diff[:200_000], encoding="utf-8") - pack = subprocess.run([sys.executable, str(workspace / "harness" / "review_pack.py"), "--base", base, "--no-run"], + # 评审材料用被评审提交自己的引擎生成;迁移前的旧布局提交里引擎在 harness/ 平铺。 + entry = workspace / ".harness" / "engine" / "cli.py" + command = [str(entry), "review-pack"] if entry.exists() else [str(workspace / "harness" / "review_pack.py")] + pack = subprocess.run([sys.executable, *command, "--base", base, "--no-run"], cwd=workspace, capture_output=True, text=True, check=False) (folder / "pack.md").write_text(pack.stdout or pack.stderr, encoding="utf-8") @@ -434,8 +437,9 @@ def watch(interval_minutes: float = 5, reviewer_name: str | None = None, root: P def calibration_samples(root: Path = ROOT) -> list[dict]: """已知有问题:事故回放用例(在 main 上重新注入);已知良好:manifest 中列出的已合并 PR。""" - from replay_cases import CASES + from engine.core.cases import load_project_cases + CASES = load_project_cases().CASES manifest = json.loads((EVALS / "samples.json").read_text(encoding="utf-8")) bad = [{"kind": "bad", "id": f"{case.defect}:{index}", "title": manifest["bad_title"], "file": case.file, "find": case.find, "replace": case.replace} diff --git a/harness/review_pack.py b/.harness/engine/agents/review_pack.py similarity index 94% rename from harness/review_pack.py rename to .harness/engine/agents/review_pack.py index 59209b8..149d02f 100644 --- a/harness/review_pack.py +++ b/.harness/engine/agents/review_pack.py @@ -1,6 +1,6 @@ """评审证据包:把评审需要的机器结论汇成一页,评审方的时间花在清单中机器判定不了的部分。 - python3 harness/review_pack.py --base origin/main [--head HEAD] [--output build/review/pack.md] [--no-run] + bin/harness review-pack --base origin/main [--head HEAD] [--output build/review/pack.md] [--no-run] 内容:改动概要、风险等级(risk.py)、修复证据(evidence.py)、本地 verify 汇总(仅当与当前 head 一致)、 本次改动涉及的验收编号及其中的人工项、评审清单入口(docs/templates/review-checklist.md)。 @@ -11,15 +11,11 @@ import argparse import json import re -import sys from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -import acceptance -import evidence -import risk -from common import ROOT, added_lines, git +from engine.checks import acceptance, evidence +from engine.core.common import ROOT, added_lines, git +from engine.routing import risk VERIFY_SUMMARY = ROOT / "build" / "verify" / "summary.json" diff --git a/.harness/engine/checks/__init__.py b/.harness/engine/checks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/harness/acceptance.py b/.harness/engine/checks/acceptance.py similarity index 94% rename from harness/acceptance.py rename to .harness/engine/checks/acceptance.py index 062ef75..1791422 100644 --- a/harness/acceptance.py +++ b/.harness/engine/checks/acceptance.py @@ -13,11 +13,11 @@ `test_mod`、`test_mod.Class`、`test_mod.Class.test_x` tests/ 下的 Python 测试(按 AST 核对存在) `路径#片段` 文件存在且包含该片段(Swift 断言、CI 步骤) -暂时没有测试的可自动化条目登记在 harness/acceptance-gaps.txt(带原因);补上测试后必须从清单删除, +暂时没有测试的可自动化条目登记在 .harness/state/acceptance-gaps.txt(带原因);补上测试后必须从清单删除, 清单只能缩减。 - python3 harness/acceptance.py 检查(verify 默认档运行) - python3 harness/acceptance.py --manual 列出人工验收清单 + bin/harness acceptance 检查(verify 默认档运行) + bin/harness acceptance --manual 列出人工验收清单 """ from __future__ import annotations @@ -25,17 +25,14 @@ import argparse import ast import re -import sys from dataclasses import dataclass, field from functools import cache from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -from common import ROOT +from engine.core.common import ROOT, STATE_DIR SPECS_DIR = ROOT / "docs" / "specs" -GAPS_FILE = ROOT / "harness" / "acceptance-gaps.txt" +GAPS_FILE = STATE_DIR / "acceptance-gaps.txt" # 与 AGENTS.md「验证边界」一致:单元测试、模拟协议、真实 PTY 可自动化;用户 UI 验收属人工。 AUTOMATABLE = {"单测", "夹具", "性质", "快照", "架构", "模拟协议", "真实 PTY", "CI 断言"} MANUAL = {"真实数据", "真机 UI", "人工"} diff --git a/harness/base_tests.py b/.harness/engine/checks/base_tests.py similarity index 92% rename from harness/base_tests.py rename to .harness/engine/checks/base_tests.py index 92d52ee..82bd818 100644 --- a/harness/base_tests.py +++ b/.harness/engine/checks/base_tests.py @@ -8,10 +8,10 @@ - PR 有这类改动(risk.py 已标为 R2,交评审)时:base 测试预期可能失败,只报告结果。 - 测试 import 的是 head 的产品代码,产品模块可在 import 时篡改 unittest(PR7-R7):测试经 `base_tests_runner.py` 运行,框架被篡改时一律失败,不论是否有意改动了测试。 -- 测试读取的验收数据(`docs/specs/`、`harness/acceptance-gaps.txt`)一并换回 base 版本(H0926-4): +- 测试读取的验收数据(`docs/specs/`、`.harness/state/acceptance-gaps.txt`)一并换回 base 版本(H0926-4): head 在验收表里引用新测试是补测试的正常做法,与 base 测试放在一起会被误判为失败。 - python3 harness/base_tests.py --base origin/main [--head HEAD] [--repo 路径] + bin/harness base-tests --base origin/main [--head HEAD] [--repo 路径] """ from __future__ import annotations @@ -23,16 +23,14 @@ import tempfile from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -import risk -from common import ROOT, clean_git_env, git +from engine.core.common import ROOT, clean_git_env, git +from engine.routing import risk INTENDED = ("改动已有测试", "删除已有测试", "黄金快照改动") RUNNER = Path(__file__).resolve().parent / "base_tests_runner.py" # 判定器的输入整体按 base 版本:测试本身,以及测试读取的验收数据。只还原 tests/ 时, # head 在规格验收表里引用新测试(补测试的正常做法)会让 base 的验收映射测试失败(H0926-4)。 -JUDGE_INPUTS = ("tests", "docs/specs", "harness/acceptance-gaps.txt") +JUDGE_INPUTS = ("tests", "docs/specs", ".harness/state/acceptance-gaps.txt") def _restore_base(judged: str, base: str, worktree: Path, cwd: Path) -> None: diff --git a/harness/base_tests_runner.py b/.harness/engine/checks/base_tests_runner.py similarity index 100% rename from harness/base_tests_runner.py rename to .harness/engine/checks/base_tests_runner.py diff --git a/harness/docs_check.py b/.harness/engine/checks/docs_check.py similarity index 94% rename from harness/docs_check.py rename to .harness/engine/checks/docs_check.py index fdbe9bf..aba64e0 100644 --- a/harness/docs_check.py +++ b/.harness/engine/checks/docs_check.py @@ -1,6 +1,6 @@ """文档熵治理:已跟踪 Markdown 的相对链接必须指向存在的文件;报告状态陈旧的文档。 -范围:仓库根的 README*.md 与 AGENTS.md、docs/ 下全部 .md、harness/README.md(只看 git 已跟踪的文件)。 +范围:仓库根的 README*.md 与 AGENTS.md、docs/ 下全部 .md(只看 git 已跟踪的文件)。 断链(失败) 行内链接 `[文字](目标)`、图片与引用式定义 `[名字]: 目标` 中的相对路径,按所在文件的目录解析 (以 / 开头的按仓库根解析),目标文件或目录不存在即失败。http(s)、mailto 等带协议的链接与纯锚点 @@ -8,23 +8,20 @@ 陈旧状态(只报告) 「状态:」一行写着待执行、进行中、草案等未完成字样,且该文件最近一次 git 提交早于 30 天。 它提示文档可能已与实际脱节,由评审方收尾时核对(待办清单「定期核对」),不阻塞合并。 - python3 harness/docs_check.py + bin/harness docs """ from __future__ import annotations import argparse import re -import sys import time from pathlib import Path from urllib.parse import unquote -sys.path.insert(0, str(Path(__file__).resolve().parent)) +from engine.core.common import ROOT, git -from common import ROOT, git - -PATTERNS = ("README*.md", "AGENTS.md", "docs/*.md", "harness/README.md") # git 路径通配里 * 可跨目录 +PATTERNS = ("README*.md", "AGENTS.md", "docs/*.md") # git 路径通配里 * 可跨目录 STALE_DAYS = 30 UNFINISHED = ("待执行", "进行中", "草案", "待实施", "实施中", "待派发", "未开始", "待评审") diff --git a/harness/evidence.py b/.harness/engine/checks/evidence.py similarity index 97% rename from harness/evidence.py rename to .harness/engine/checks/evidence.py index b6193ba..064696d 100644 --- a/harness/evidence.py +++ b/.harness/engine/checks/evidence.py @@ -12,11 +12,11 @@ (修复前失败才能证明测试真的在检查这个缺陷,v0.8.0 X4)。Python 按方法运行;Swift 按引用它的 verify 检查整组编译运行(只在 macOS,--swift;编译失败算出错,运行失败算失败) - 4. 回放强制(审计 G2,设计 8.2):非 doc 类、未撤销的编号,head 上的 harness/replay_cases.py 必须有其一: + 4. 回放强制(审计 G2,设计 8.2):非 doc 类、未撤销的编号,head 上的 .harness/project/replay_cases.py 必须有其一: 注入用例(CASES)、守卫测试(GUARDED)、写明原因的暂缓项(DEFERRED)。回放用例是判定器,由评审方在同一 PR 中加入(执行方在 PR 里写明注入点);是否真的失败由 `verify --full` 的回放检查核对。 - python3 harness/evidence.py --base origin/main [--head HEAD] [--ids A,B] [--markdown out.md] + bin/harness evidence --base origin/main [--head HEAD] [--ids A,B] [--markdown out.md] [--swift] """ @@ -32,11 +32,9 @@ from dataclasses import dataclass, field from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -import replay_cases -import verify -from common import ROOT, clean_git_env, commit_field, git, path_matches +from engine.checks import verify +from engine.core.cases import load_project_cases +from engine.core.common import ROOT, clean_git_env, commit_field, git, path_matches ID_PATTERN = r"[A-Z][A-Z0-9]*-[A-Z]*\d+" TRAILER_RE = re.compile(rf"^({ID_PATTERN})(?:\s+(doc))?\s*$") @@ -325,7 +323,7 @@ def analyse( def replay_coverage(evidences: dict[str, DefectEvidence], registry=None) -> dict[str, tuple[bool, str]]: """每个需要回放的编号 → (是否满足, 说明)。doc 类与已撤销的编号不要求回放。""" - registry = registry or replay_cases + registry = registry or load_project_cases() injected = {case.defect for case in registry.CASES} coverage = {} for defect, evidence in evidences.items(): @@ -341,7 +339,7 @@ def replay_coverage(evidences: dict[str, DefectEvidence], registry=None) -> dict coverage[defect] = ( False, ( - "harness/replay_cases.py 中没有注入用例、守卫测试或写明原因的暂缓项;" + ".harness/project/replay_cases.py 中没有注入用例、守卫测试或写明原因的暂缓项;" "执行方在 PR 里写明注入点,由评审方在本 PR 中加入" ), ) @@ -361,7 +359,7 @@ def render_markdown(evidences: dict[str, DefectEvidence], base: str, head: str, head_sha = git("rev-parse", "--short", head, cwd=cwd) label = {"pass": "✓ 通过", "fail": "✗ 失败", "error": "✗ 出错", "": "—"} lines = [ - f"### 修复证据(`{base_sha}` → `{head_sha}`,由 `harness/evidence.py` 生成)", + f"### 修复证据(`{base_sha}` → `{head_sha}`,由 `engine/checks/evidence.py` 生成)", "", ] if not evidences: diff --git a/harness/hygiene.py b/.harness/engine/checks/hygiene.py similarity index 96% rename from harness/hygiene.py rename to .harness/engine/checks/hygiene.py index 1f7f9dd..56c287f 100644 --- a/harness/hygiene.py +++ b/.harness/engine/checks/hygiene.py @@ -17,9 +17,7 @@ from dataclasses import dataclass from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -from common import ROOT, git, load_rules, parse_added_lines, path_matches +from engine.core.common import ROOT, git, load_rules, parse_added_lines, path_matches @dataclass(frozen=True) @@ -61,7 +59,7 @@ def check_added_lines(added: dict[str, list[tuple[int, str]]], rules: dict) -> l secrets, home = _compiled(rules) violations = [] for path, lines in added.items(): - if path == "harness/rules.toml": # 规则文件本身写着这些模式 + if path == ".harness/config/rules.toml": # 规则文件本身写着这些模式 continue for line_no, text in lines: if any(pattern.search(text) for pattern in secrets): @@ -77,7 +75,7 @@ def check_secrets_in_files(paths: list[str], rules: dict, cwd: Path) -> list[Vio secrets, _ = _compiled(rules) violations = [] for path in paths: - if path == "harness/rules.toml": + if path == ".harness/config/rules.toml": continue full = cwd / path try: diff --git a/.harness/engine/checks/integrity.py b/.harness/engine/checks/integrity.py new file mode 100644 index 0000000..48a9dad --- /dev/null +++ b/.harness/engine/checks/integrity.py @@ -0,0 +1,74 @@ +"""引擎完整性:业务仓库里的 .harness/engine/ 必须与 .harness/engine.lock 记录的发布版本逐字节一致。 + +引擎以内置副本的形式装进业务仓库(经用户批准合并到 main,守卫与规则都从 main 读取)。 +锁文件记录版本、引擎仓库提交与目录树哈希;任何人(包括执行方)改了引擎文件而没有走 upgrade(在引擎仓库的检出中运行), +哈希就对不上,verify 失败。升级引擎只能由 upgrade 同时改引擎与锁文件,属于 R3,由用户批准。 + + bin/harness integrity 核对(verify 各档运行) + bin/harness integrity --print 打印当前目录树哈希 +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path + +from engine.core.common import ENGINE_DIR, HARNESS_DIR + +LOCK_FILE = HARNESS_DIR / "engine.lock" +IGNORED_PARTS = {"__pycache__"} +IGNORED_SUFFIXES = {".pyc", ".pyo"} + + +def tree_hash(engine_dir: Path = ENGINE_DIR) -> str: + """按相对路径排序,对「路径 NUL 内容哈希」逐行求 sha256;忽略字节码缓存。""" + digest = hashlib.sha256() + for path in sorted(p for p in engine_dir.rglob("*") if p.is_file()): + rel = path.relative_to(engine_dir) + if IGNORED_PARTS & set(rel.parts) or path.suffix in IGNORED_SUFFIXES: + continue + digest.update(rel.as_posix().encode() + b"\0" + hashlib.sha256(path.read_bytes()).hexdigest().encode() + b"\n") + return f"sha256:{digest.hexdigest()}" + + +def read_lock(path: Path = LOCK_FILE) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def write_lock(path: Path, version: str, commit: str, tree: str) -> None: + data = {"engine": "delivery-harness", "version": version, "commit": commit, "tree": tree} + path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def check(engine_dir: Path = ENGINE_DIR, lock_file: Path = LOCK_FILE) -> tuple[bool, str]: + if engine_dir.parent != lock_file.parent: + return True, "引擎不在业务仓库的 .harness/ 下(引擎仓库自身),无需核对" + if not lock_file.exists(): + return False, f"缺少 {lock_file.name}:在引擎仓库的检出中运行 `python3 engine/cli.py upgrade --target <本仓库>`,不要手工复制" + lock = read_lock(lock_file) + actual = tree_hash(engine_dir) + if actual != lock.get("tree"): + return False, ( + f"引擎文件与锁定的 {lock.get('version')}({str(lock.get('commit', ''))[:12]})不一致:\n" + f" 锁定 {lock.get('tree')}\n 实际 {actual}\n" + "引擎只能在引擎仓库的检出中用 `python3 engine/cli.py upgrade --target <本仓库>` 整体替换(R3,由用户批准);本地修改请撤销" + ) + return True, f"引擎 {lock.get('version')}({str(lock.get('commit', ''))[:12]})与锁文件一致 ✓" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--print", action="store_true", help="打印当前目录树哈希") + args = parser.parse_args(argv) + if args.print: + print(tree_hash()) + return 0 + ok, message = check() + print(message) + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/harness/mutate.py b/.harness/engine/checks/mutate.py similarity index 80% rename from harness/mutate.py rename to .harness/engine/checks/mutate.py index b24ce79..c83e5dc 100644 --- a/harness/mutate.py +++ b/.harness/engine/checks/mutate.py @@ -1,12 +1,12 @@ """定向变异测试:衡量关键函数的测试强度(判定器本身被检验)。 -对 TARGETS 中列出的函数逐个施加小的 AST 变异(比较符、算术符、布尔运算、常量、删除调用语句), +对 checks.toml [[mutation.targets]] 中列出的函数逐个施加小的 AST 变异(比较符、算术符、布尔运算、常量、删除调用语句), 在工作区副本上运行对应测试:测试失败 = 变异被杀死;仍通过 = 存活(测试的盲区线索,也可能是等价变异)。 - python3 harness/mutate.py 运行全部目标,打印得分与存活变异 - python3 harness/mutate.py --check 与 harness/mutation-baseline.json 比较,任一目标得分下降即失败 - python3 harness/mutate.py --update 运行后写回基线(得分提高后执行,基线只升不降) - python3 harness/mutate.py --check --changed-since BASE 只运行 BASE...HEAD 改到其文件的目标(R1 的 PR) + bin/harness mutate 运行全部目标,打印得分与存活变异 + bin/harness mutate --check 与 .harness/state/mutation-baseline.json 比较,任一目标得分下降即失败 + bin/harness mutate --update 运行后写回基线(得分提高后执行,基线只升不降) + bin/harness mutate --check --changed-since BASE 只运行 BASE...HEAD 改到其文件的目标(R1 的 PR) 较慢(每个变异跑一次测试),不在每次提交时运行;由定期的 quality workflow 与人工触发。 """ @@ -24,12 +24,10 @@ from dataclasses import dataclass from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) +from engine.checks.replay import copy_worktree +from engine.core.common import ROOT, STATE_DIR, changed_files, clean_git_env, setting -from common import ROOT, changed_files, clean_git_env -from replay import copy_worktree - -BASELINE = ROOT / "harness" / "mutation-baseline.json" +BASELINE = STATE_DIR / "mutation-baseline.json" @dataclass(frozen=True) @@ -40,39 +38,12 @@ class Target: tests: tuple[str, ...] -TARGETS = [ - Target( - "价格转换与防护", - "scripts/pricing_fetch.py", - ("transform", "_price_jump", "_drop_conflicting_aliases"), - ("test_pricing_fetch", "test_properties.PricingTransformProperties"), - ), - Target( - "热力金额分级", - "scripts/daily_report.py", - ("cost_thresholds", "cost_level"), - ("test_properties.CostThresholdProperties", "test_daily_report"), - ), - Target( - "过去日报告合并", - "scripts/daily_report.py", - ("_merge_day_tasks", "_task_total"), - ("test_properties.MergeNoDowngradeProperties", "test_daily_report"), - ), - Target("模型规范名", "scripts/model_names.py", ("canonical",), ("test_model_names",)), - Target( - "金额计算", - "scripts/usage_cost.py", - ("price_for_day", "model_cost", "cost_for_models", "merge_cost", "convert_display"), - ("test_usage_cost",), - ), - Target( - "用量汇总", - "scripts/usage_report.py", - ("period_bounds", "previous_anchor", "_merge_models", "_dimension_rows"), - ("test_usage_report", "test_golden"), - ), -] +def load_targets() -> list[Target]: + """checks.toml 的 [[mutation.targets]]:name、file(Python 源文件)、functions、tests(在 tests/ 下运行)。""" + return [ + Target(item["name"], item["file"], tuple(item["functions"]), tuple(item["tests"])) + for item in setting("mutation", "targets", []) + ] SWAPS = { ast.Lt: ast.LtE, @@ -212,10 +183,13 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--changed-since", metavar="BASE", help="只运行 BASE...HEAD 改到其文件的目标") args = parser.parse_args(argv) - targets = TARGETS + targets = load_targets() + if not targets: + print("checks.toml 没有登记变异目标([[mutation.targets]]),无需运行") + return 0 if args.only: wanted = set(args.only.split(",")) - targets = [target for target in TARGETS if target.name in wanted] + targets = [target for target in targets if target.name in wanted] if args.changed_since: changed = {path for _, path in changed_files(args.changed_since)} targets = [target for target in targets if target.file in changed] diff --git a/.harness/engine/checks/quality.py b/.harness/engine/checks/quality.py new file mode 100644 index 0000000..1966875 --- /dev/null +++ b/.harness/engine/checks/quality.py @@ -0,0 +1,133 @@ +"""熵治理:产品代码的复杂度与体量只降不升(报告 11.4;He 等 MSR 2026:复杂度持续上升是后期变慢的主因)。 + +指标(产品代码目录见 checks.toml [sources] python_dirs 与 swift_dirs): + complex_functions ruff C901 超标函数数(Python,圈复杂度 > 10) 只降不升 + files_over_800 超过 800 行的源文件数 只降不升 + largest_file_lines 最大源文件行数 只报告(往大文件里加小修复不应被拦) + swift_files_over_500 超过 500 行的 Swift 文件数 只降不升 + swift_long_functions 函数体超过 60 行的 Swift 函数数 只降不升 + swift_deep_functions 大括号嵌套深度超过 6 的 Swift 函数数 只降不升 + +Swift 没有 ruff 这样的现成工具,这里用轻量解析(只用标准库):先把注释(含嵌套块注释)与字符串字面量 +(普通、多行三引号、带 # 的原始字符串)抹成空格,再配对大括号。「函数」指 func、init、deinit 与计算属性 +(`var x: T {`,SwiftUI 的 body 即此类);函数体行数从 `{` 所在行数到匹配的 `}` 所在行,嵌套深度以函数体 +自身为 1,闭包与控制流都计入。解析不求完美(如 #if 分支、正则字面量不处理),只需对同一份代码稳定。 + +阈值取自一个 SwiftUI 项目 2026-09-28 的分布(332 个函数):函数体行数中位 6、P90 29、P95 48、P99 88, +取 60 行约为前 3%;嵌套深度 1–4 占 92%,SwiftUI 视图常见 5–6 层,超过 6 层只剩 13 个,都是可以 +拆出子视图的大 body。文件 500 行:现有 Swift 文件多在 400 行上下,超过 500 行的只有 2 个。 + + bin/harness quality 打印指标并与 .harness/state/quality-baseline.json 比较,棘轮指标上升即失败 + bin/harness quality --update 指标下降后写回基线(上调基线须直接改文件,属 R3,由评审确认) +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +from engine.core.common import ROOT, STATE_DIR, setting +from engine.lang.swift import functions as swift_functions + +BASELINE = STATE_DIR / "quality-baseline.json" +LONG_FILE = 800 +SWIFT_LONG_FILE = 500 +SWIFT_LONG_FUNCTION = 60 +SWIFT_DEEP_NESTING = 6 +RATCHETED = ( + "complex_functions", + "files_over_800", + "swift_files_over_500", + "swift_long_functions", + "swift_deep_functions", +) + +def python_dirs() -> list[str]: + return list(setting("sources", "python_dirs", [])) + + +def swift_dirs() -> list[str]: + return list(setting("sources", "swift_dirs", [])) + + +def sources() -> list[tuple[str, str]]: + """(目录, 通配):Python 按 [sources] python_glob(缺省只看目录本层的 *.py),Swift 递归。""" + python_glob = setting("sources", "python_glob", "*.py") + return [(d, python_glob) for d in python_dirs()] + [(d, "**/*.swift") for d in swift_dirs()] + + +def measure_swift(root: Path = ROOT) -> dict[str, int]: + files = long_functions = deep_functions = 0 + paths = sorted(path for directory in swift_dirs() for path in (root / directory).glob("**/*.swift")) + for path in paths: + source = path.read_text(encoding="utf-8") + files += len(source.splitlines()) > SWIFT_LONG_FILE + for _name, _line, body_lines, deepest in swift_functions(source): + long_functions += body_lines > SWIFT_LONG_FUNCTION + deep_functions += deepest > SWIFT_DEEP_NESTING + return { + "swift_files_over_500": files, + "swift_long_functions": long_functions, + "swift_deep_functions": deep_functions, + } + + +def measure(root: Path = ROOT) -> dict[str, int]: + complex_functions = 0 + if python_dirs(): + completed = subprocess.run( + [sys.executable, "-m", "ruff", "check", *python_dirs(), "--select", "C901", "--output-format", "concise", + "--no-cache"], + cwd=root, + capture_output=True, + text=True, + check=False, + ) + complex_functions = sum(1 for line in completed.stdout.splitlines() if " C901 " in line) + sizes = [] + for directory, pattern in sources(): + for path in (root / directory).glob(pattern): + sizes.append(len(path.read_text(encoding="utf-8").splitlines())) + return { + "complex_functions": complex_functions, + "largest_file_lines": max(sizes, default=0), + "files_over_800": sum(size > LONG_FILE for size in sizes), + **measure_swift(root), + } + + +def compare(current: dict[str, int], baseline: dict[str, int]) -> list[str]: + return [ + f"{name} 从 {baseline[name]} 升到 {current[name]}" + for name in RATCHETED + if name in baseline and current[name] > baseline[name] + ] + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--update", action="store_true", help="指标不高于基线时写回基线") + args = parser.parse_args(argv) + current = measure() + baseline = json.loads(BASELINE.read_text()) if BASELINE.exists() else {} + for name, value in current.items(): + kind = "棘轮" if name in RATCHETED else "报告" + print(f"{name:<20} {value:>6}(基线 {baseline.get(name, '—')},{kind})") + regressions = compare(current, baseline) + if args.update: + if regressions: + print("✗ 指标高于基线,不能用 --update 上调:" + ";".join(regressions)) + return 1 + BASELINE.write_text(json.dumps(current, indent=2, sort_keys=True) + "\n") + print(f"基线已写入 {BASELINE.relative_to(ROOT)}") + return 0 + for regression in regressions: + print(f"✗ {regression}:先拆分或简化;确需上调基线请改 .harness/state/quality-baseline.json 并说明理由(R3)") + return 1 if regressions else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/harness/r1_checks.py b/.harness/engine/checks/r1_checks.py similarity index 51% rename from harness/r1_checks.py rename to .harness/engine/checks/r1_checks.py index 091a376..3c89026 100644 --- a/harness/r1_checks.py +++ b/.harness/engine/checks/r1_checks.py @@ -3,32 +3,22 @@ 2026-09-28 决定 2:R1 保持自动合并,但以下任一不满足即降为 R2(risk.py 调用): - 被改函数签名不变:base 中已有的函数(Python 按 AST、Swift 按声明文本)签名不变、未被删除;新增函数不限 - 无新依赖:新增的 Python import 只能是标准库或仓库内模块;Swift 不新增 import - - 无数据迁移:产品代码不新增建表、改表、删表语句(scripts/migrations.py 本身属 R3) - - 不超规模阈值:增删行数不超过 harness/autonomy.toml [size] max_lines + - 无数据迁移:产品代码不新增建表、改表、删表语句(迁移脚本本身按 rules.toml 判 R3) + - 不超规模阈值:增删行数不超过 .harness/config/autonomy.toml [size] max_lines 已有测试零改动、黄金快照零差异由 risk.py 的标记核对;变异得分不降由 build 的 harness job 在声明 R1 时运行 `mutate.py --check --changed-since`(要执行代码,放在 PR 自己的 CI 里,下降即失败)。 """ from __future__ import annotations -import ast import re -import sys import tomllib from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) +from engine import lang +from engine.core.common import CONFIG_DIR, ROOT, added_lines, changed_files, git, path_matches, setting -from common import ROOT, added_lines, changed_files, git, path_matches - -AUTONOMY_PATH = ROOT / "harness" / "autonomy.toml" -CODE_PATTERNS = ["scripts/**", "native/**"] -SWIFT_FUNC = re.compile( - r"\bfunc\s+(?P[A-Za-z_]\w*)\s*(?P<[^>{]*>)?\s*\((?P[^)]*)\)" - r"(?P[^{\n]*)" -) -PY_IMPORT = re.compile(r"^\s*(?:from\s+(?P[\w.]+)\s+import\b|import\s+(?P[\w., ]+))") -SWIFT_IMPORT = re.compile(r"^\s*(?:@\w+\s+)?import\s+(\w+)") +AUTONOMY_PATH = CONFIG_DIR / "autonomy.toml" SCHEMA = re.compile(r"(?i)\b(CREATE|ALTER|DROP)\s+(TABLE|INDEX|VIEW|TRIGGER)\b") @@ -37,43 +27,15 @@ def load_autonomy(path: Path = AUTONOMY_PATH) -> dict: return tomllib.load(handle) -def python_signatures(source: str) -> dict[str, str]: - """模块与类中的函数 → 参数与返回注解的文本。语法错误时返回空(由测试与 lint 拦住)。""" - try: - tree = ast.parse(source) - except SyntaxError: - return {} - result: dict[str, str] = {} - - def visit(body: list[ast.stmt], prefix: str) -> None: - for node in body: - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): - returns = f" -> {ast.unparse(node.returns)}" if node.returns else "" - kind = "async " if isinstance(node, ast.AsyncFunctionDef) else "" - result[prefix + node.name] = f"{kind}({ast.unparse(node.args)}){returns}" - elif isinstance(node, ast.ClassDef): - visit(node.body, f"{prefix}{node.name}.") - - visit(tree.body, "") - return result - - -def swift_signatures(source: str) -> dict[str, list[str]]: - """函数名 → 该名下全部声明(重载按多个签名计)。""" - result: dict[str, list[str]] = {} - for match in SWIFT_FUNC.finditer(source): - text = " ".join(f"{match['generic'] or ''}({match['params']}){match['tail']}".split()) - result.setdefault(match["name"], []).append(text) - return {name: sorted(items) for name, items in result.items()} +python_signatures = lang.python.signatures +swift_signatures = lang.swift.signatures def signature_changes(path: str, before: str, after: str) -> list[str]: - if path.endswith(".py"): - old, new = python_signatures(before), python_signatures(after) - elif path.endswith(".swift"): - old, new = swift_signatures(before), swift_signatures(after) - else: + plugin = lang.for_path(path) + if plugin is None: return [] + old, new = plugin.signatures(before), plugin.signatures(after) changes = [] for name, signature in old.items(): if name not in new: @@ -84,27 +46,16 @@ def signature_changes(path: str, before: str, after: str) -> list[str]: def _local_modules(head: str, cwd: Path) -> set[str]: - names = git("ls-tree", "--name-only", f"{head}:scripts", cwd=cwd, check=False).split() + """仓库内模块名:checks.toml [sources] python_dirs 下的文件与目录名(新增对它们的 import 不算新依赖)。""" + names = [] + for directory in setting("sources", "python_dirs", []): + names += git("ls-tree", "--name-only", f"{head}:{directory}", cwd=cwd, check=False).split() return {Path(name).stem for name in names} def new_dependencies(path: str, lines: list[str], local: set[str]) -> list[str]: - found = [] - for text in lines: - if path.endswith(".py"): - match = PY_IMPORT.match(text) - if not match: - continue - modules = [match["from"]] if match["from"] else [part.split()[0] for part in match["names"].split(",") if part.strip()] - for module in modules: - top = module.split(".")[0] - if top and top not in sys.stdlib_module_names and top not in local and top != "__future__": - found.append(f"`{path}` 新增了对 `{top}` 的依赖") - elif path.endswith(".swift"): - match = SWIFT_IMPORT.match(text) - if match: - found.append(f"`{path}` 新增了 `import {match[1]}`") - return found + plugin = lang.for_path(path) + return plugin.new_dependencies(path, lines, local) if plugin else [] def changed_line_count(base: str, head: str, cwd: Path, exclude: list[str]) -> int: @@ -125,7 +76,7 @@ def violations(base: str, head: str, cwd: Path = ROOT, autonomy: dict | None = N added = added_lines(base, head, cwd) local = _local_modules(head, cwd) for status, path in changed_files(base, head, cwd): - if not path_matches(path, CODE_PATTERNS): + if not path_matches(path, setting("sources", "code", [])): continue if status in {"M", "D"}: before = git("show", f"{merge_base}:{path}", cwd=cwd) diff --git a/harness/release_check.py b/.harness/engine/checks/release_check.py similarity index 56% rename from harness/release_check.py rename to .harness/engine/checks/release_check.py index 65d1ced..dba1737 100644 --- a/harness/release_check.py +++ b/.harness/engine/checks/release_check.py @@ -1,12 +1,13 @@ -"""发版前核对(推 v* tag 时由 CI 的 harness job 运行)。把 AGENTS.md 的发版规则变成机器检查: +"""发版前核对(推 v* tag 时由 CI 的 harness job 运行)。把发版规则变成机器检查: - 1. tag 名 = "v" + BUNDLE_SHORT_VERSION(scripts/build_inbox_app.py) - 2. BUNDLE_VERSION 是整数,且大于上一个 v* tag 的值(两处版本号不能只改其一) + 1. tag 名 = "v" + 版本号(checks.toml [release] version_file 中 short_version_var 的值) + 2. 构建号(build_version_var,可不配)是整数,且大于上一个 v* tag 的值(两处版本号不能只改其一) 3. tag 指向的提交在 main 上(发版只从合入 main 的代码打) 版本号本身必须先与用户确认;release job 另有 environment 审批,由用户放行。 +版本文件目前支持 Python 源文件里的模块级常量赋值。 - python3 harness/release_check.py --tag v0.8.1 [--commit HEAD] [--main origin/main] + bin/harness release-check --tag v0.8.1 [--commit HEAD] [--main origin/main] """ from __future__ import annotations @@ -15,23 +16,29 @@ import ast import re import subprocess -import sys from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) +from engine.core.common import ROOT, git, setting -from common import ROOT, git -BUILD_SCRIPT = "scripts/build_inbox_app.py" +def version_file() -> str: + return setting("release", "version_file", required=True) + + +def version_vars() -> tuple[str, str | None]: + return setting("release", "short_version_var", "VERSION"), setting("release", "build_version_var") def read_versions(source: str) -> dict[str, str]: + """返回 {"short": 版本号, "build": 构建号}(缺的不出现)。""" + short_var, build_var = version_vars() + names = {short_var: "short", **({build_var: "build"} if build_var else {})} versions = {} for node in ast.parse(source).body: if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name): name = node.targets[0].id - if name in ("BUNDLE_SHORT_VERSION", "BUNDLE_VERSION") and isinstance(node.value, ast.Constant): - versions[name] = str(node.value.value) + if name in names and isinstance(node.value, ast.Constant): + versions[names[name]] = str(node.value.value) return versions @@ -45,18 +52,19 @@ def previous_tag(tag: str, commit: str, cwd: Path) -> str | None: def check(tag: str, commit: str = "HEAD", main: str = "origin/main", cwd: Path = ROOT) -> list[str]: problems = [] - current = read_versions(git("show", f"{commit}:{BUILD_SCRIPT}", cwd=cwd)) - short, build = current.get("BUNDLE_SHORT_VERSION"), current.get("BUNDLE_VERSION") + source, (short_var, build_var) = version_file(), version_vars() + current = read_versions(git("show", f"{commit}:{source}", cwd=cwd)) + short, build = current.get("short"), current.get("build") if tag != f"v{short}": - problems.append(f"tag {tag} 与 BUNDLE_SHORT_VERSION {short!r} 不一致") - if not build or not re.fullmatch(r"\d+", build): - problems.append(f"BUNDLE_VERSION {build!r} 不是整数") - else: + problems.append(f"tag {tag} 与 {short_var} {short!r} 不一致") + if build_var and (not build or not re.fullmatch(r"\d+", build)): + problems.append(f"{build_var} {build!r} 不是整数") + elif build_var: prev = previous_tag(tag, commit, cwd) if prev: - old = read_versions(git("show", f"{prev}:{BUILD_SCRIPT}", cwd=cwd, check=False)).get("BUNDLE_VERSION") + old = read_versions(git("show", f"{prev}:{source}", cwd=cwd, check=False)).get("build") if old and old.isdigit() and int(build) <= int(old): - problems.append(f"BUNDLE_VERSION {build} 没有大于上一个 tag {prev} 的 {old}(两处版本号需同步)") + problems.append(f"{build_var} {build} 没有大于上一个 tag {prev} 的 {old}(两处版本号需同步)") on_main = subprocess.run( ["git", "merge-base", "--is-ancestor", commit, main], cwd=cwd, capture_output=True, check=False ) diff --git a/harness/replay.py b/.harness/engine/checks/replay.py similarity index 81% rename from harness/replay.py rename to .harness/engine/checks/replay.py index 9850fcc..cd7e282 100644 --- a/harness/replay.py +++ b/.harness/engine/checks/replay.py @@ -1,15 +1,15 @@ -"""事故回放:把历史缺陷注入工作区副本,证明对应检查真的会失败(用例见 harness/replay_cases.py)。 +"""事故回放:把历史缺陷注入工作区副本,证明对应检查真的会失败(用例见 .harness/project/replay_cases.py)。 - python3 harness/replay.py 运行全部用例(Swift 用例仅在 macOS 上运行) - python3 harness/replay.py --strict 被跳过的用例算失败(macOS CI 用) - python3 harness/replay.py --list 只列出覆盖情况,不运行 + bin/harness replay 运行全部用例(Swift 用例仅在 macOS 上运行) + bin/harness replay --strict 被跳过的用例算失败(macOS CI 用) + bin/harness replay --list 只列出覆盖情况,不运行 结论: 拦住 注入后检查失败(期望结果) 漏过 注入后检查仍通过:这条回归测试守不住这个缺陷 过期 find 在文件中不存在或不唯一:用例需随代码更新 坏底 未注入的副本上检查就已失败:结论不可信 -另外检查基线(docs/review/2026-09-25-harness-baseline.md)每一条都有注入用例、守卫测试或写明原因的暂缓项。 +另外检查项目登记的基线(BASELINE)每一条都有注入用例、守卫测试或写明原因的暂缓项。 """ from __future__ import annotations @@ -22,15 +22,8 @@ from dataclasses import dataclass from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -from common import ROOT, clean_git_env, git -from replay_cases import BASELINE, CASES, DEFERRED, GUARDED, Case - -SWIFT_POLICY = ( - "mkdir -p build && xcrun swiftc -parse-as-library native/InboxPolicy.swift native/Shared/WidgetSnapshot.swift " - "tests/InboxPolicyTests.swift -o build/policy-tests && ./build/policy-tests" -) +from engine.core.cases import Case, load_project_cases, suites +from engine.core.common import ROOT, clean_git_env, git @dataclass @@ -58,7 +51,8 @@ def copy_worktree(target: Path) -> None: def run_tests(copy: Path, tests: tuple[str, ...]) -> tuple[bool, str]: """返回 (是否通过, 输出摘要)。""" - python_tests = [test for test in tests if test != "swift-policy"] + registered = suites() + python_tests = [test for test in tests if test not in registered] outputs = [] passed = True env = clean_git_env({"PYTHONDONTWRITEBYTECODE": "1"}) @@ -74,12 +68,12 @@ def run_tests(copy: Path, tests: tuple[str, ...]) -> tuple[bool, str]: passed = passed and completed.returncode == 0 lines = completed.stderr.strip().splitlines() outputs.append(lines[-1] if lines else "") - if "swift-policy" in tests: + for name in (test for test in tests if test in registered): completed = subprocess.run( - SWIFT_POLICY, shell=True, cwd=copy, capture_output=True, text=True, env=env, check=False + registered[name]["shell"], shell=True, cwd=copy, capture_output=True, text=True, env=env, check=False ) passed = passed and completed.returncode == 0 - outputs.append("swift-policy 通过" if completed.returncode == 0 else "swift-policy 失败") + outputs.append(f"{name} 通过" if completed.returncode == 0 else f"{name} 失败") return passed, ";".join(filter(None, outputs)) @@ -93,7 +87,7 @@ def replay(cases: list[Case], strict: bool = False) -> list[Outcome]: for case in cases: if case.platform == "macos" and not _is_macos(): verdict = "坏底" if strict else "跳过" - outcomes.append(Outcome(case, verdict, "需要 macOS(Swift)")) + outcomes.append(Outcome(case, verdict, "需要 macOS")) continue target = copy / case.file original = target.read_text(encoding="utf-8") @@ -121,7 +115,7 @@ def replay(cases: list[Case], strict: bool = False) -> list[Outcome]: def guarded_outcomes() -> list[tuple[str, str, bool, str]]: """守卫测试:逐个运行,必须存在且通过。""" results = [] - for defect, tests in GUARDED.items(): + for defect, tests in load_project_cases().GUARDED.items(): completed = subprocess.run( [sys.executable, "-m", "unittest", *tests], cwd=ROOT / "tests", @@ -136,8 +130,9 @@ def guarded_outcomes() -> list[tuple[str, str, bool, str]]: def coverage_gaps() -> list[str]: - covered = {case.defect for case in CASES} | set(GUARDED) | set(DEFERRED) - return [defect for defect in BASELINE if defect not in covered] + project = load_project_cases() + covered = {case.defect for case in project.CASES} | set(project.GUARDED) | set(project.DEFERRED) + return [defect for defect in project.BASELINE if defect not in covered] def main(argv: list[str] | None = None) -> int: @@ -147,6 +142,8 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--only", help="只运行这些缺陷编号(逗号分隔)") args = parser.parse_args(argv) + project = load_project_cases() + BASELINE, CASES, GUARDED, DEFERRED = project.BASELINE, project.CASES, project.GUARDED, project.DEFERRED gaps = coverage_gaps() if args.list: for defect in BASELINE: @@ -175,7 +172,7 @@ def main(argv: list[str] | None = None) -> int: for defect, names, ok, summary in guarded_outcomes(): failed = failed or not ok print(f" {'✓' if ok else '✗'} {defect:<9} {names} [{summary}]") - print("暂缓(原因见 harness/replay_cases.py DEFERRED):" + "、".join(DEFERRED)) + print("暂缓(原因见 .harness/project/replay_cases.py DEFERRED):" + "、".join(DEFERRED)) if gaps: failed = True print(f"✗ 基线未覆盖:{'、'.join(gaps)}") diff --git a/harness/taskbook.py b/.harness/engine/checks/taskbook.py similarity index 96% rename from harness/taskbook.py rename to .harness/engine/checks/taskbook.py index b0d4557..e4752ec 100644 --- a/harness/taskbook.py +++ b/.harness/engine/checks/taskbook.py @@ -26,27 +26,24 @@ - 中、大任务有「非目标」「前置条件」「步骤与提交顺序」。 - 与「步骤与提交顺序」中列出的文件交叉核对:预期风险不低于这些路径的风险;触及护栏路径须为 K7; 跨两个以上模块或触及架构级路径须声明 architecture: true。只是交叉核对:实现 PR 仍按实际改动判级。 - - harness/taskbook-exempt.txt 登记的历史任务只检查头部可解析、字段齐全(清单只能缩减)。 + - .harness/state/taskbook-exempt.txt 登记的历史任务只检查头部可解析、字段齐全(清单只能缩减)。 - python3 harness/taskbook.py 检查全部任务书(verify 各档运行) - python3 harness/taskbook.py docs/plans/task-005-x.md --on-main 派发前:另要求与 origin/main 上的版本一致 + bin/harness taskbook 检查全部任务书(verify 各档运行) + bin/harness taskbook docs/plans/task-005-x.md --on-main 派发前:另要求与 origin/main 上的版本一致 """ from __future__ import annotations import argparse import re -import sys from dataclasses import dataclass, field from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -import acceptance -from common import ROOT, git, load_rules, path_matches +from engine.checks import acceptance +from engine.core.common import ROOT, STATE_DIR, git, load_rules, path_matches TASK_GLOB = "task-*.md" -EXEMPT_FILE = ROOT / "harness" / "taskbook-exempt.txt" +EXEMPT_FILE = STATE_DIR / "taskbook-exempt.txt" # 类别 → 允许的预期风险(设计文档第十五节 15.1)。 CLASS_RISKS = { "K1": {"R0"}, @@ -340,7 +337,7 @@ def spec_ids(root: Path = ROOT) -> dict[str, str]: def check_all(root: Path = ROOT, exempt: dict[str, str] | None = None, rules: dict | None = None) -> list[Report]: - exempt = load_exempt(root / "harness" / "taskbook-exempt.txt") if exempt is None else exempt + exempt = load_exempt(root / ".harness" / "state" / "taskbook-exempt.txt") if exempt is None else exempt ids = spec_ids(root) reports = [] tasks = sorted((root / "docs" / "plans").glob(TASK_GLOB)) diff --git a/harness/verify.py b/.harness/engine/checks/verify.py similarity index 64% rename from harness/verify.py rename to .harness/engine/checks/verify.py index 1112bc6..9e7e0bf 100644 --- a/harness/verify.py +++ b/.harness/engine/checks/verify.py @@ -1,9 +1,9 @@ """统一验证入口:本机、云端、CI 用同一条命令、同一组检查。 - bin/verify 默认档:工具版本、lint、仓库卫生、质量棘轮、文档链接、Python 测试、Swift 测试(仅 macOS) - bin/verify --quick 快速档:工具版本、lint、仓库卫生(pre-commit 用) + bin/verify 默认档:工具版本、仓库卫生、质量棘轮、文档链接、验收映射、任务书准入与项目检查(lint、测试等) + bin/verify --quick 快速档:工具版本、仓库卫生与标为 quick 的项目检查(pre-commit 用) bin/verify --full 完整档:默认档 + 事故回放(注入历史缺陷,对应测试必须失败) - bin/verify --strict 任何被跳过的检查都算失败(macOS CI 用,防止 Swift 检查被静默跳过) + bin/verify --strict 任何被跳过的检查都算失败(防止平台相关检查被静默跳过) 终端只打印每项结论;完整输出写到 build/verify/<检查名>.log,汇总写到 build/verify/summary.json。 通过与否以本命令的退出码和 CI 上当前 head 的运行为准,不以任何人的自述为准。 @@ -21,14 +21,10 @@ import sys import time from dataclasses import dataclass, field -from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -from common import ROOT, clean_git_env, git +from engine.core.common import CLI, ENGINE_REL, ROOT, clean_git_env, git, setting LOG_DIR = ROOT / "build" / "verify" -DEV_REQUIREMENTS = ROOT / "requirements-dev.txt" MIN_PYTHON = (3, 11) TAIL_LINES = 30 @@ -55,9 +51,10 @@ class Result: def pinned_version(package: str) -> str | None: - if not DEV_REQUIREMENTS.exists(): + requirements = ROOT / setting("verify", "requirements", "requirements-dev.txt") + if not requirements.exists(): return None - for line in DEV_REQUIREMENTS.read_text().splitlines(): + for line in requirements.read_text().splitlines(): match = re.match(rf"\s*{re.escape(package)}==([\w.]+)", line) if match: return match.group(1) @@ -72,25 +69,28 @@ def check_tools() -> tuple[bool, str]: lines.append(f"Python {platform.python_version()} < {'.'.join(map(str, MIN_PYTHON))}") else: lines.append(f"Python {platform.python_version()} ✓") - want = pinned_version("ruff") - result = subprocess.run([sys.executable, "-m", "ruff", "--version"], capture_output=True, text=True, check=False) - have = result.stdout.strip().removeprefix("ruff ") if result.returncode == 0 else None - if want is None: - ok = False - lines.append("requirements-dev.txt 未锁定 ruff 版本") - elif have != want: - ok = False - lines.append( - f"ruff {have or '未安装'} ≠ 锁定的 {want};执行 `{sys.executable} -m pip install -r requirements-dev.txt`" - ) - else: - lines.append(f"ruff {have} ✓") + # 锁定版本的 Python 工具(checks.toml [verify] pinned_tools),与 CI 同一版本。 + requirements = setting("verify", "requirements", "requirements-dev.txt") + for tool in setting("verify", "pinned_tools", []): + want = pinned_version(tool) + result = subprocess.run([sys.executable, "-m", tool, "--version"], capture_output=True, text=True, check=False) + have = result.stdout.strip().removeprefix(f"{tool} ") if result.returncode == 0 else None + if want is None: + ok = False + lines.append(f"{requirements} 未锁定 {tool} 版本") + elif have != want: + ok = False + lines.append(f"{tool} {have or '未安装'} ≠ 锁定的 {want};执行 `{sys.executable} -m pip install -r {requirements}`") + else: + lines.append(f"{tool} {have} ✓") # 每个开发与 Agent 执行环境都必须装上 git 守卫;CI 是全新检出,不需要。 if os.environ.get("CI") != "true": hooks = git("config", "--local", "--get", "core.hooksPath", check=False) if hooks != ".githooks": ok = False - lines.append(f"git 守卫未安装(core.hooksPath = {hooks or '未设置'});执行 `python3 harness/git_guard.py install`") + lines.append( + f"git 守卫未安装(core.hooksPath = {hooks or '未设置'});执行 `python3 {ENGINE_REL}/cli.py guard-git install`" + ) else: lines.append("git 守卫已安装 ✓") return ok, "\n".join(lines) @@ -100,57 +100,59 @@ def _is_macos() -> bool: return sys.platform == "darwin" and shutil.which("xcrun") is not None -def build_checks(strict: bool = False) -> list[Check]: +ALL_TIERS = ("quick", "default", "full") + + +def builtin_checks(strict: bool = False) -> list[Check]: + """引擎自带的检查。项目检查(lint、测试、平台相关的编译运行)写在 checks.toml [[verify.checks]]。""" py = sys.executable - replay = [py, "harness/replay.py", *(["--strict"] if strict else [])] + cli = [py, str(CLI)] return [ - Check("tools", ("quick", "default", "full"), func=check_tools), - Check("lint", ("quick", "default", "full"), command=[py, "-m", "ruff", "check", "scripts", "tests", "harness"]), - Check("hygiene", ("quick", "default", "full"), command=[py, "harness/hygiene.py", "--tracked"]), - # 熵治理棘轮(harness/quality.py):复杂度超标函数数与超长文件数只降不升。 - Check("quality", ("default", "full"), command=[py, "harness/quality.py"]), - # 文档熵治理(harness/docs_check.py):已跟踪 Markdown 的相对链接断链即失败;陈旧状态只报告。 - Check("docs", ("default", "full"), command=[py, "harness/docs_check.py"]), - # 规格验收编号 ↔ 测试映射(harness/acceptance.py):引用失效或新增无测试条目即失败。 - Check("acceptance", ("quick", "default", "full"), command=[py, "harness/acceptance.py"]), - # 任务书准入(harness/taskbook.py):头部、类别与风险、验收挂规格编号、步骤交叉核对。 - Check("taskbook", ("quick", "default", "full"), command=[py, "harness/taskbook.py"]), - Check( - "python-tests", - ("default", "full"), - command=[py, "-W", "error::ResourceWarning", "-m", "unittest", "discover", "-s", "tests"], - ), - Check( - "swift-policy", - ("default", "full"), - shell="mkdir -p build && xcrun swiftc -parse-as-library native/InboxPolicy.swift " - "native/Shared/WidgetSnapshot.swift tests/InboxPolicyTests.swift -o build/policy-tests " - "&& ./build/policy-tests", - requires="macos", - why_skipped="非 macOS,Swift 检查由 macOS CI 负责", - ), - Check( - "swift-runner", - ("default", "full"), - shell="mkdir -p build && xcrun swiftc -parse-as-library native/ProcessRunner.swift " - "tests/ProcessRunnerTests.swift -o build/runner-tests && ./build/runner-tests", - requires="macos", - why_skipped="非 macOS,Swift 检查由 macOS CI 负责", - ), - Check( - "zcode-selftest", - ("default", "full"), - # AGENTS.md 要求的无 UI 自检(身份比对、搜索框结构、点击状态机、run loop),此前 CI 只编译未运行。 - shell="mkdir -p build && xcrun swiftc native/ZcodeFocus.swift -o build/zcode-focus " - "&& build/zcode-focus --self-test", - requires="macos", - why_skipped="非 macOS,Swift 检查由 macOS CI 负责", - ), - # 事故回放:注入历史缺陷,对应测试必须失败(harness/replay_cases.py)。 - Check("replay", ("full",), command=replay), + Check("tools", ALL_TIERS, func=check_tools), + # 引擎完整性:.harness/engine/ 与锁文件一致(引擎只能经 upgrade 整体替换)。 + Check("integrity", ALL_TIERS, command=[*cli, "integrity"]), + Check("hygiene", ALL_TIERS, command=[*cli, "hygiene", "--tracked"]), + # 熵治理棘轮(quality):复杂度超标函数数与超长文件数只降不升。 + Check("quality", ("default", "full"), command=[*cli, "quality"]), + # 文档熵治理(docs):已跟踪 Markdown 的相对链接断链即失败;陈旧状态只报告。 + Check("docs", ("default", "full"), command=[*cli, "docs"]), + # 规格验收编号 ↔ 测试映射(acceptance):引用失效或新增无测试条目即失败。 + Check("acceptance", ALL_TIERS, command=[*cli, "acceptance"]), + # 任务书准入(taskbook):头部、类别与风险、验收挂规格编号、步骤交叉核对。 + Check("taskbook", ALL_TIERS, command=[*cli, "taskbook"]), + # 事故回放:注入历史缺陷,对应测试必须失败(.harness/project/replay_cases.py)。 + Check("replay", ("full",), command=[*cli, "replay", *(["--strict"] if strict else [])]), ] +def project_checks() -> list[Check]: + """checks.toml 的 [[verify.checks]]:name、tiers、command(列表,`{python}` 换成当前解释器)或 shell、 + requires = "macos"、why_skipped。""" + checks = [] + for item in setting("verify", "checks", []): + command = [sys.executable if part == "{python}" else part for part in item["command"]] if "command" in item else None + checks.append( + Check( + item["name"], + tuple(item.get("tiers", ("default", "full"))), + command=command, + shell=item.get("shell"), + requires=item.get("requires"), + why_skipped=item.get("why_skipped", ""), + ) + ) + return checks + + +def build_checks(strict: bool = False) -> list[Check]: + """顺序取 checks.toml [verify] order(引擎检查与项目检查的名字混排);没写的检查依次排在后面,回放最后。""" + available = {check.name: check for check in [*builtin_checks(strict), *project_checks()]} + order = [name for name in setting("verify", "order", []) if name in available] + rest = [name for name in available if name not in order and name != "replay"] + names = order + rest + ([] if "replay" in order else ["replay"]) + return [available[name] for name in names] + + def run_check(check: Check) -> Result: if check.requires == "macos" and not _is_macos(): return Result(check.name, "skip", note=check.why_skipped) diff --git a/.harness/engine/cli.py b/.harness/engine/cli.py new file mode 100644 index 0000000..ea8dbd9 --- /dev/null +++ b/.harness/engine/cli.py @@ -0,0 +1,71 @@ +"""delivery-harness 唯一入口:python3 <引擎目录>/cli.py <子命令> [参数…] + +CI、git 钩子、Agent 宿主钩子与引擎内部的子进程调用都经这里启动,各子命令的参数见对应模块的说明。 +""" + +from __future__ import annotations + +import importlib +import sys +from pathlib import Path + +# 让 `engine` 包可导入:引擎目录的上一级(业务仓库的 .harness/,或引擎仓库根)。 +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +COMMANDS = { + # 判定器 + "verify": "engine.checks.verify", + "hygiene": "engine.checks.hygiene", + "acceptance": "engine.checks.acceptance", + "taskbook": "engine.checks.taskbook", + "docs": "engine.checks.docs_check", + "base-tests": "engine.checks.base_tests", + "evidence": "engine.checks.evidence", + "r1": "engine.checks.r1_checks", + "replay": "engine.checks.replay", + "mutate": "engine.checks.mutate", + "quality": "engine.checks.quality", + "release-check": "engine.checks.release_check", + "integrity": "engine.checks.integrity", + # 判级与合并路由 + "risk": "engine.routing.risk", + "policy": "engine.routing.policy", + "run-check": "engine.routing.run_check", + # 护栏 + "guard-command": "engine.guards.command_guard", + "guard-git": "engine.guards.git_guard", + # 派发与评审 + "dispatch": "engine.agents.dispatch", + "review": "engine.agents.review", + "review-pack": "engine.agents.review_pack", + "identity": "engine.agents.identity", + # 报告 + "metrics": "engine.reports.metrics", + "weekly": "engine.reports.weekly", + # 安装与升级(只能从引擎仓库的检出运行) + "install": "engine.core.install", + "upgrade": "engine.core.install:upgrade_main", +} + + +def usage() -> str: + return "用法:cli.py <子命令> [参数…]\n子命令:" + "、".join(COMMANDS) + + +def main(argv: list[str] | None = None) -> int: + args = sys.argv[1:] if argv is None else argv + if not args or args[0] in {"-h", "--help"}: + print(usage()) + return 0 if args else 2 + name, rest = args[0], args[1:] + if name not in COMMANDS: + print(f"未知子命令:{name}\n{usage()}", file=sys.stderr) + return 2 + module_name, _, function = COMMANDS[name].partition(":") + module = importlib.import_module(module_name) + sys.argv = [f"cli.py {name}", *rest] + return getattr(module, function or "main")(rest) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.harness/engine/core/__init__.py b/.harness/engine/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.harness/engine/core/cases.py b/.harness/engine/core/cases.py new file mode 100644 index 0000000..7b185df --- /dev/null +++ b/.harness/engine/core/cases.py @@ -0,0 +1,59 @@ +"""事故回放用例的数据结构与项目用例的加载。 + +项目在 `.harness/project/replay_cases.py` 中登记自己的历史缺陷(引擎不带任何项目的用例): + + from engine.core.cases import Case + + BASELINE = ["D1", ...] # 必须有覆盖的缺陷编号(注入、守卫测试或写明原因的暂缓) + CASES = [Case("D1", "标题", "src/mod.py", "原文(恰好出现一次)", "注入后的文字", ("test_mod",)), ...] + GUARDED = {"D2": ("test_guard.GuardTest.test_x",), ...} # 过程类失败由守卫测试覆盖 + DEFERRED = {"D3": "暂缓原因", ...} + +Case.tests 里是在 tests/ 下运行的 Python 测试 id,或 checks.toml [replay.suites.<名字>] 登记的测试套件名。 +""" + +from __future__ import annotations + +import importlib.util +from dataclasses import dataclass +from types import SimpleNamespace + +from engine.core.common import PROJECT_DIR, setting + + +def suites() -> dict[str, dict]: + """checks.toml [replay.suites.<名字>]:shell 命令与 requires(如 "macos")。""" + return dict(setting("replay", "suites", {})) + + +@dataclass(frozen=True) +class Case: + defect: str + title: str + file: str + find: str + replace: str + tests: tuple[str, ...] + + @property + def platform(self) -> str: + registered = suites() + needs = {registered[name].get("requires", "any") for name in self.tests if name in registered} + return "macos" if "macos" in needs else "any" + + +def load_project_cases() -> SimpleNamespace: + """读取项目的回放用例;项目没有登记时返回空集合。""" + path = PROJECT_DIR / "replay_cases.py" + empty = SimpleNamespace(BASELINE=[], CASES=[], GUARDED={}, DEFERRED={}) + if not path.exists(): + return empty + spec = importlib.util.spec_from_file_location("harness_project_replay_cases", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return SimpleNamespace( + BASELINE=list(getattr(module, "BASELINE", [])), + CASES=list(getattr(module, "CASES", [])), + GUARDED=dict(getattr(module, "GUARDED", {})), + DEFERRED=dict(getattr(module, "DEFERRED", {})), + ) diff --git a/harness/common.py b/.harness/engine/core/common.py similarity index 63% rename from harness/common.py rename to .harness/engine/core/common.py index 55f2ac2..f588516 100644 --- a/harness/common.py +++ b/.harness/engine/core/common.py @@ -1,6 +1,13 @@ -"""harness 公共工具:仓库根、git 调用、规则加载、路径通配。 +"""harness 公共工具:项目根与配置位置、git 调用、配置加载、路径通配。 -harness 不进发布包(scripts/ 会被整体拷进 App,harness/ 不会),只依赖标准库。 +引擎只依赖标准库。安装在业务仓库时位于 `<项目根>/.harness/engine/`;在引擎仓库自身开发时位于 +`<引擎仓库>/engine/`。项目自己的配置、棘轮状态与扩展点都在 `<项目根>/.harness/` 下: + + .harness/config/ rules.toml、autonomy.toml、checks.toml + .harness/state/ 只升不降或只减不增的基线与清单 + .harness/project/ replay_cases.py(本项目的事故回放用例) + +引擎不带任何项目或使用者自己的默认值(账号、模型、目录布局之外的路径):缺配置时明确报错。 """ from __future__ import annotations @@ -12,10 +19,34 @@ from functools import cache from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] -RULES_PATH = ROOT / "harness" / "rules.toml" +ENGINE_DIR = Path(__file__).resolve().parents[1] + + +def _find_root(engine_dir: Path) -> Path: + if engine_dir.parent.name == ".harness": + return engine_dir.parent.parent + return engine_dir.parent + + +ROOT = _find_root(ENGINE_DIR) +HARNESS_DIR = ROOT / ".harness" +CONFIG_DIR = HARNESS_DIR / "config" +STATE_DIR = HARNESS_DIR / "state" +PROJECT_DIR = HARNESS_DIR / "project" +RULES_PATH = CONFIG_DIR / "rules.toml" +# 引擎入口:CI、钩子与引擎内部的子进程调用都经它启动。 +CLI = ENGINE_DIR / "cli.py" +# 相对项目根的位置(写进 git 命令与提示)。 +ENGINE_REL = ENGINE_DIR.relative_to(ROOT).as_posix() +RULES_REL = ".harness/config/rules.toml" +# 迁移过渡:origin/main 上还是旧布局(harness/ 平铺)时从这里读规则;全部使用方迁移后删除。 +LEGACY_RULES_REL = "harness/rules.toml" ZERO_SHA = "0" * 40 + +class ConfigError(RuntimeError): + """缺少必填配置。信息里写明该在哪个文件的哪一节补什么。""" + # git 在执行钩子时会注入这些变量;对其他仓库或工作树调用 git 时必须清掉, # 否则命令会落到钩子所在的仓库上。 # 与 `git rev-parse --local-env-vars` 一致:这些变量把 git 命令绑定到某个具体仓库。 @@ -61,11 +92,44 @@ def git(*args: str, cwd: Path | str = ROOT, check: bool = True, isolate: bool = @cache -def load_rules(path: Path = RULES_PATH) -> dict: +def _load_toml(path: Path) -> dict: with open(path, "rb") as handle: return tomllib.load(handle) +def load_rules(path: Path | None = None) -> dict: + return _load_toml(path or RULES_PATH) + + +def load_checks() -> dict: + """`.harness/config/checks.toml`:verify 检查清单、源码与语言、运行时、目录约定等。缺文件按空配置处理。""" + path = CONFIG_DIR / "checks.toml" + return _load_toml(path) if path.exists() else {} + + +def setting(section: str, key: str, default=None, *, required: bool = False, source: str = "checks"): + """读 `.toml` 中 `[section] key`(section 可用点号表示子表)。required 时缺失即报 ConfigError。""" + if source == "checks": + data = load_checks() + else: + data = load_rules() if RULES_PATH.exists() else {} + node = data + for part in section.split("."): + node = node.get(part, {}) if isinstance(node, dict) else {} + if isinstance(node, dict) and key in node: + return node[key] + if required: + raise ConfigError(f"缺少配置:.harness/config/{source}.toml 的 [{section}] {key}") + return default + + +# 目录约定(与项目无关的通用布局,写在引擎 README): +# docs/specs/ 现役规格与验收表 docs/plans/task-*.md 任务书 +# docs/runs/ 运行记录 docs/templates/ 模板 +# tests/ 测试 evals/review/ 评审校准集 +# 可配置的目录约定排在后续版本(待办),当前以约定为准。 + + @cache def _glob_regex(pattern: str) -> re.Pattern[str]: """把 `**`、`*`、`?` 通配转成正则。`**/` 可匹配零层目录,`*` 不跨目录。""" diff --git a/.harness/engine/core/install.py b/.harness/engine/core/install.py new file mode 100644 index 0000000..bc88ca1 --- /dev/null +++ b/.harness/engine/core/install.py @@ -0,0 +1,107 @@ +"""安装与升级:把引擎以内置副本的形式装进业务仓库,并写锁文件(只能从引擎仓库的检出运行)。 + + python3 <引擎仓库>/engine/cli.py install --target <业务仓库> 首次安装:引擎、锁文件、配置骨架与模板 + python3 <引擎仓库>/engine/cli.py upgrade --target <业务仓库> 升级:只整体替换引擎与锁文件 + +模板(bin/、.githooks/、各 Agent 的守卫钩子、.harness/ 的配置骨架)只在目标文件不存在时写入,已有文件一律不覆盖, +结束时列出跳过的文件,由接入方按需合并。锁文件记录版本、引擎仓库提交与目录树哈希,verify 的 integrity 检查据此 +拒绝任何绕过升级流程的引擎改动。安装与升级的结果都应经 PR 合并(R3,由用户批准)。 +""" + +from __future__ import annotations + +import argparse +import shutil +import subprocess +from pathlib import Path + +from engine import __version__ +from engine.checks.integrity import tree_hash, write_lock +from engine.core.common import ENGINE_DIR + +TEMPLATES = ENGINE_DIR.parent / "templates" +IGNORE = shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo") + + +def _git(*args: str, cwd: Path) -> subprocess.CompletedProcess: + return subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, check=False) + + +def engine_commit(allow_dirty: bool) -> str: + """引擎仓库当前提交;engine/ 有未提交改动时拒绝(锁文件必须对应一个真实提交)。""" + repo = ENGINE_DIR.parent + head = _git("rev-parse", "HEAD", cwd=repo) + if head.returncode != 0: + raise SystemExit("只能从引擎仓库的 git 检出运行 install/upgrade") + dirty = _git("status", "--porcelain", "--", "engine", "templates", cwd=repo).stdout.strip() + if dirty and not allow_dirty: + raise SystemExit("引擎仓库的 engine/ 或 templates/ 有未提交改动:先提交或检出一个发布 tag") + return head.stdout.strip() + ("-dirty" if dirty else "") + + +def copy_engine(target: Path) -> Path: + destination = target / ".harness" / "engine" + if destination.resolve() == ENGINE_DIR.resolve(): + raise SystemExit("不能把引擎装到它自己所在的位置") + if destination.exists(): + shutil.rmtree(destination) + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(ENGINE_DIR, destination, ignore=IGNORE) + return destination + + +def copy_templates(target: Path) -> tuple[list[str], list[str]]: + """返回 (新写入, 已存在而跳过)。可执行位随模板保留。""" + written, skipped = [], [] + for source in sorted(p for p in TEMPLATES.rglob("*") if p.is_file()): + rel = source.relative_to(TEMPLATES) + if "__pycache__" in rel.parts: + continue + destination = target / rel + if destination.exists(): + skipped.append(rel.as_posix()) + continue + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + written.append(rel.as_posix()) + return written, skipped + + +def install(target: Path, templates: bool, allow_dirty: bool) -> int: + if not (target / ".git").exists(): + raise SystemExit(f"{target} 不是 git 仓库的根目录") + lock = target / ".harness" / "engine.lock" + previous = lock.read_text(encoding="utf-8") if lock.exists() else "" + commit = engine_commit(allow_dirty) + destination = copy_engine(target) + write_lock(lock, __version__, commit, tree_hash(destination)) + print(f"引擎 {__version__}({commit[:12]})已装到 {destination.relative_to(target)},锁文件已写入") + if previous: + print(" 升级前的锁文件:" + " ".join(previous.split())) + if templates: + written, skipped = copy_templates(target) + for rel in written: + print(f" + {rel}") + if skipped: + print("已存在、未覆盖(按需手工合并):") + for rel in skipped: + print(f" = {rel}") + print("下一步:填 .harness/config/checks.toml 的 [identity] 与 [sources]、[verify];" + "bin/harness guard-git install;bin/harness verify;经 PR 合并(R3,由用户批准)") + return 0 + + +def main(argv: list[str] | None = None, command: str = "install") -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--target", required=True, help="业务仓库根目录") + parser.add_argument("--allow-dirty", action="store_true", help="允许引擎仓库有未提交改动(仅限开发调试,锁文件会标 -dirty)") + args = parser.parse_args(argv) + return install(Path(args.target).resolve(), templates=command == "install", allow_dirty=args.allow_dirty) + + +def upgrade_main(argv: list[str] | None = None) -> int: + return main(argv, command="upgrade") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/harness/shell_structure.py b/.harness/engine/core/shell_structure.py similarity index 97% rename from harness/shell_structure.py rename to .harness/engine/core/shell_structure.py index dc778c4..3399d7a 100644 --- a/harness/shell_structure.py +++ b/.harness/engine/core/shell_structure.py @@ -17,6 +17,8 @@ import shlex from collections.abc import Callable +from engine.core.common import setting + FILTER = "改写历史(v0.8.0 曾因此改写 main 与 18 个 tag)" FORCE = "强制推送:已推送的历史不改写" FORCE_REFSPEC = "强制推送(+refspec):已推送的历史不改写" @@ -30,7 +32,7 @@ HOOKS_PATH = "修改 core.hooksPath 会绕过 git 守卫" OVERRIDE = "覆盖变量只供人使用,Agent 不能自行放开守卫" RESET_HARD = "git reset --hard 会丢弃未提交的改动(v0.8.0 X1 的修复就这样丢失);先提交或 stash" -CLEAN_X = "git clean -x 会删除 scratch/iterm-probe-venv(AGENTS.md);加 -e scratch/iterm-probe-venv" +CLEAN_X = "git clean -x 会删除项目登记要保留的本地运行时(checks.toml [runtime] preserve);用 -e 逐个排除:{paths}" RM_OUTSIDE = "递归删除工作区以外的路径(临时目录 /tmp 除外)" RELEASE = "gh release(list、view 以外):发版由用户执行" ISSUE_DELETE = "删除议题会销毁逃逸、抽审与升级登记;已关闭的议题照样计数" @@ -336,8 +338,10 @@ def _git_clean(rest: list[str]) -> list[str]: excluded.append(rest[index + 1]) elif arg.startswith("--exclude="): excluded.append(arg.split("=", 1)[1]) - if removes_ignored and not any(path.rstrip("/") == "scratch/iterm-probe-venv" for path in excluded): - return [CLEAN_X] + kept = {path.rstrip("/") for path in excluded} + missing = [path for path in setting("runtime", "preserve", []) if path.rstrip("/") not in kept] + if removes_ignored and missing: + return [CLEAN_X.format(paths="、".join(f"-e {path}" for path in missing))] return [] diff --git a/.harness/engine/guards/__init__.py b/.harness/engine/guards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/harness/command_guard.py b/.harness/engine/guards/command_guard.py similarity index 93% rename from harness/command_guard.py rename to .harness/engine/guards/command_guard.py index cf85cae..e263bb9 100644 --- a/harness/command_guard.py +++ b/.harness/engine/guards/command_guard.py @@ -22,10 +22,8 @@ import sys from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -import shell_structure -from common import path_matches +from engine.core import shell_structure +from engine.core.common import path_matches, setting # (模式, 理由)。按整条命令匹配,`&&`、`;`、管道串起来的命令同样生效。 COMMAND_RULES: list[tuple[str, str]] = [ @@ -54,7 +52,8 @@ # 覆盖变量:出现变量名或其后半截即拒(拼接如 ${P}_ALLOW_TAG 也能命中;评审 PR7-R6)。 (r"HARNESS_|_ALLOW_(MAIN|TAG|REWRITE)\b|_SKIP_VERIFY\b", "覆盖变量只供人使用,Agent 不能自行放开守卫"), (r"\bgit\s+reset\s+[^;&|]*--hard\b", "git reset --hard 会丢弃未提交的改动(v0.8.0 X1 的修复就这样丢失);先提交或 stash"), - (r"\bgit\s+clean\b(?=[^;&|]*\s-\w*[xX])(?![^;&|]*-e\s+scratch/iterm-probe-venv)", "git clean -x 会删除 scratch/iterm-probe-venv(AGENTS.md);加 -e scratch/iterm-probe-venv"), + # 解析失败时的兜底(宁可误报):-x 且没有任何 -e 排除即拒;能解析时由 shell_structure 按 [runtime] preserve 精确判断。 + (r"\bgit\s+clean\b(?=[^;&|]*\s-\w*[xX])(?![^;&|]*\s-e\s)", "git clean -x 会删除本地运行时等被忽略的文件;用 -e 排除 checks.toml [runtime] preserve 登记的路径"), ( r"\brm\s+-[a-zA-Z]*[rR][a-zA-Z]*\b[^;&|]*?\s(/(?!tmp/|private/tmp/|var/folders/)|~|\$HOME|\.\.)", "递归删除工作区以外的路径(临时目录 /tmp 除外)", @@ -121,19 +120,22 @@ PROTECTED_FOR_IMPLEMENTER = [ ".github/**", ".githooks/**", - "harness/**", + ".harness/**", ".claude/**", ".opencode/**", ".pi/**", ".zcode/**", ".codex/**", - "requirements-dev.txt", - "ruff.toml", ] + +def protected_for_implementer() -> list[str]: + """引擎固定的护栏路径,加上项目在 rules.toml [guard] implementer_protected 中登记的(如锁定工具版本的文件)。""" + return PROTECTED_FOR_IMPLEMENTER + list(setting("guard", "implementer_protected", [], source="rules")) + # 受保护目录里执行方按规范必须更新的数据文件(逐个列出,H0926-2)。其单调性由 CI 兜底: # 缺口清单只能缩减,新增条目 risk.py 判 R3(rules.toml [risk] shrink_only)。 -IMPLEMENTER_EDITABLE = ["harness/acceptance-gaps.txt"] +IMPLEMENTER_EDITABLE = [".harness/state/acceptance-gaps.txt"] # 合同对执行方只读(设计文档 4.1):任务书与现役规格是判定器的一部分。 CONTRACTS_FOR_IMPLEMENTER = ["docs/plans/task-*.md", "docs/specs/**"] @@ -187,7 +189,7 @@ def check_edit(path: str, role: str, root: Path) -> list[str]: if role != "implementer": return [] relative = _relative(path, root) - pattern = path_matches(relative, PROTECTED_FOR_IMPLEMENTER) + pattern = path_matches(relative, protected_for_implementer()) if pattern and relative not in IMPLEMENTER_EDITABLE: return [f"执行者不能编辑判定器与护栏({relative} 命中 {pattern});需要改动请升级给评审方"] pattern = path_matches(relative, CONTRACTS_FOR_IMPLEMENTER) @@ -245,7 +247,7 @@ def main(argv: list[str] | None = None) -> int: print("harness 守卫拒绝了这次操作:", file=sys.stderr) for reason in dict.fromkeys(reasons): print(f" - {reason}", file=sys.stderr) - print(" 如确需执行,请把原因和命令交给用户决定(docs/specs/delivery-harness.md)。", file=sys.stderr) + print(" 如确需执行,请把原因和命令交给用户决定(规则见 .harness/config/rules.toml)。", file=sys.stderr) return 2 diff --git a/harness/git_guard.py b/.harness/engine/guards/git_guard.py similarity index 87% rename from harness/git_guard.py rename to .harness/engine/guards/git_guard.py index 40c6a21..63619a8 100644 --- a/harness/git_guard.py +++ b/.harness/engine/guards/git_guard.py @@ -1,14 +1,14 @@ """git 层守卫:与使用哪家 Agent 无关,所有经 git 的操作都会经过这里。 - python3 harness/git_guard.py install 把 core.hooksPath 指向 .githooks(幂等) - python3 harness/git_guard.py status 查看安装状态 + bin/harness guard-git install 把 core.hooksPath 指向 .githooks(幂等) + bin/harness guard-git status 查看安装状态 .githooks/pre-commit → pre-commit 保护分支上禁止提交;暂存区卫生;快速 verify .githooks/pre-push → pre-push 禁推保护分支与 tag、禁强制推送;推送前 verify .githooks/reference-transaction → reference-transaction 禁止本地改写/删除保护分支、移动/删除 tag 对应 v0.8.0 X3:filter-repo 改写了本地 main 与 18 个 tag,并删除了 origin。 -覆盖变量见 harness/rules.toml [guard];只供人使用。 +覆盖变量见 .harness/config/rules.toml [guard];只供人使用。 """ from __future__ import annotations @@ -19,10 +19,8 @@ import tomllib from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -import hygiene -from common import ROOT, ZERO_SHA, git, load_rules +from engine.checks import hygiene +from engine.core.common import LEGACY_RULES_REL, ROOT, RULES_REL, ZERO_SHA, git, load_rules HOOKS_DIR = ".githooks" @@ -30,16 +28,17 @@ def trusted_rules(repo: Path) -> dict: """守卫规则以 origin/main 上的版本为准(评审 PR7-R3)。 - 工作区里的 harness/rules.toml 执行者可写:改掉 protected_branches 就能让本机守卫失效。 + 工作区里的 .harness/config/rules.toml 执行者可写:改掉 protected_branches 就能让本机守卫失效。 origin/main 受服务端 ruleset 保护,改它必须经 PR;取不到(尚未合入、没有 origin)时退回工作区版本。""" - shown = subprocess.run( - ["git", "show", "origin/main:harness/rules.toml"], cwd=repo, capture_output=True, text=True, check=False - ) - if shown.returncode != 0: + for rel in (RULES_REL, LEGACY_RULES_REL): # main 还是旧布局时读旧位置(迁移过渡) + shown = subprocess.run(["git", "show", f"origin/main:{rel}"], cwd=repo, capture_output=True, text=True, check=False) + if shown.returncode == 0: + break + else: return load_rules() - local = repo / "harness" / "rules.toml" + local = repo / RULES_REL if local.is_file() and local.read_text(encoding="utf-8") != shown.stdout: - print("harness:工作区的 harness/rules.toml 与 origin/main 不一致,git 守卫按 origin/main 的规则执行。", file=sys.stderr) + print("harness:工作区的 .harness/config/rules.toml 与 origin/main 不一致,git 守卫按 origin/main 的规则执行。", file=sys.stderr) return tomllib.loads(shown.stdout) @@ -159,19 +158,19 @@ def _report(title: str, problems: list[str]) -> int: print(f"harness:{title}被拒绝:", file=sys.stderr) for problem in problems: print(f" - {problem}", file=sys.stderr) - print(" 规则与覆盖方式见 harness/rules.toml [guard](覆盖只供人使用)。", file=sys.stderr) + print(" 规则与覆盖方式见 .harness/config/rules.toml [guard](覆盖只供人使用)。", file=sys.stderr) return 1 def _run_verify(repo: Path, *args: str) -> int: - """跑钩子所在仓库自己的 verify;仓库里没有 harness(如测试用的临时仓库)则跳过。""" + """跑钩子所在仓库自己的 verify;仓库里没有引擎(如测试用的临时仓库)则跳过。""" if _allowed("HARNESS_SKIP_VERIFY"): print("harness:HARNESS_SKIP_VERIFY=1,跳过 verify。", file=sys.stderr) return 0 - entry = repo / "harness" / "verify.py" + entry = repo / ".harness" / "engine" / "cli.py" if not entry.is_file(): return 0 - return subprocess.run([sys.executable, str(entry), *args], cwd=repo, check=False).returncode + return subprocess.run([sys.executable, str(entry), "verify", *args], cwd=repo, check=False).returncode def cmd_pre_commit(repo: Path) -> int: @@ -254,7 +253,7 @@ def main(argv: list[str] | None = None) -> int: return cmd_install() if command == "status": installed = hooks_path() == HOOKS_DIR - print("已安装" if installed else "未安装:python3 harness/git_guard.py install") + print("已安装" if installed else "未安装:bin/harness guard-git install") return 0 if installed else 1 repo = Path.cwd() # git 在仓库(工作树)根目录执行钩子 if command == "pre-commit": diff --git a/.harness/engine/lang/__init__.py b/.harness/engine/lang/__init__.py new file mode 100644 index 0000000..80de96b --- /dev/null +++ b/.harness/engine/lang/__init__.py @@ -0,0 +1,20 @@ +"""语言插件:每种语言提供签名比对(R1)、新增依赖识别(R1),可选提供函数体量与嵌套深度(质量棘轮)。 + +插件接口(模块级): + SUFFIXES: tuple[str, ...] + signatures(source) -> dict[str, object] 函数 → 签名(base 中已有函数的签名不能变) + new_dependencies(path, lines, local) -> list[str] 新增行里引入的外部依赖(理由文本) + functions(source) -> [(名称, 行号, 函数体行数, 嵌套深度)] 可选 +""" + +from __future__ import annotations + +from types import ModuleType + +from engine.lang import python, swift + +PLUGINS: tuple[ModuleType, ...] = (python, swift) + + +def for_path(path: str) -> ModuleType | None: + return next((plugin for plugin in PLUGINS if path.endswith(plugin.SUFFIXES)), None) diff --git a/.harness/engine/lang/python.py b/.harness/engine/lang/python.py new file mode 100644 index 0000000..1e6f2b0 --- /dev/null +++ b/.harness/engine/lang/python.py @@ -0,0 +1,45 @@ +"""Python 语言插件:按 AST 取函数签名;新增 import 只能是标准库或仓库内模块。""" + +from __future__ import annotations + +import ast +import re +import sys + +SUFFIXES = (".py",) +IMPORT = re.compile(r"^\s*(?:from\s+(?P[\w.]+)\s+import\b|import\s+(?P[\w., ]+))") + + +def signatures(source: str) -> dict[str, str]: + """模块与类中的函数 → 参数与返回注解的文本。语法错误时返回空(由测试与 lint 拦住)。""" + try: + tree = ast.parse(source) + except SyntaxError: + return {} + result: dict[str, str] = {} + + def visit(body: list[ast.stmt], prefix: str) -> None: + for node in body: + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + returns = f" -> {ast.unparse(node.returns)}" if node.returns else "" + kind = "async " if isinstance(node, ast.AsyncFunctionDef) else "" + result[prefix + node.name] = f"{kind}({ast.unparse(node.args)}){returns}" + elif isinstance(node, ast.ClassDef): + visit(node.body, f"{prefix}{node.name}.") + + visit(tree.body, "") + return result + + +def new_dependencies(path: str, lines: list[str], local: set[str]) -> list[str]: + found = [] + for text in lines: + match = IMPORT.match(text) + if not match: + continue + modules = [match["from"]] if match["from"] else [part.split()[0] for part in match["names"].split(",") if part.strip()] + for module in modules: + top = module.split(".")[0] + if top and top not in sys.stdlib_module_names and top not in local and top != "__future__": + found.append(f"`{path}` 新增了对 `{top}` 的依赖") + return found diff --git a/.harness/engine/lang/swift.py b/.harness/engine/lang/swift.py new file mode 100644 index 0000000..8c8e465 --- /dev/null +++ b/.harness/engine/lang/swift.py @@ -0,0 +1,135 @@ +"""Swift 语言插件(只用标准库的轻量解析)。 + +函数体量与嵌套深度:先把注释(含嵌套块注释)与字符串字面量(普通、多行三引号、带 # 的原始字符串)抹成空格, +再配对大括号。「函数」指 func、init、deinit 与计算属性(`var x: T {`,SwiftUI 的 body 即此类);函数体行数从 +`{` 所在行数到匹配的 `}` 所在行,嵌套深度以函数体自身为 1,闭包与控制流都计入。解析不求完美(如 #if 分支、 +正则字面量不处理),只需对同一份代码稳定。签名按声明文本比对,重载按多个签名计;新增 import 即算新依赖。 +""" + +from __future__ import annotations + +import re + +SUFFIXES = (".swift",) + +FUNC = re.compile( + r"\bfunc\s+(?P[A-Za-z_]\w*)\s*(?P<[^>{]*>)?\s*\((?P[^)]*)\)" + r"(?P[^{\n]*)" +) +IMPORT = re.compile(r"^\s*(?:@\w+\s+)?import\s+(\w+)") + +_STRING_START = re.compile(r'(#*)("""|")') +_SWIFT_DECL = re.compile( + r"(?[^\s(<]+)|(?Pinit|deinit)\b[?!]?|var\s+(?P\w+)\s*:\s*[^=\n{};]+?\{)" +) +_DECL_KEYWORD = re.compile(r"(? str: + """把注释与字符串字面量抹成空格(保留换行,行号不变),其余字符原样保留。""" + out: list[str] = [] + i, n = 0, len(source) + block_depth = 0 + while i < n: + ch = source[i] + if block_depth: # Swift 的块注释可以嵌套 + if source.startswith("/*", i) or source.startswith("*/", i): + block_depth += 1 if source[i] == "/" else -1 + out.append(" ") + i += 2 + else: + out.append("\n" if ch == "\n" else " ") + i += 1 + continue + if source.startswith("//", i): + end = source.find("\n", i) + end = n if end < 0 else end + out.append(" " * (end - i)) + i = end + continue + if source.startswith("/*", i): + block_depth = 1 + out.append(" ") + i += 2 + continue + match = _STRING_START.match(source, i) + if match and (match.group(1) or ch == '"'): + hashes, quote = match.group(1), match.group(2) + closing = quote + hashes + j = match.end() + while j < n: + if not hashes and source[j] == "\\": + j += 2 + continue + if source.startswith(closing, j): + j += len(closing) + break + if quote == '"' and source[j] == "\n": + break + j += 1 + out.append("".join("\n" if c == "\n" else " " for c in source[i:j])) + i = j + continue + out.append(ch) + i += 1 + return "".join(out) + + +def _body_open(text: str, index: int) -> int | None: + """从声明名之后找函数体的 `{`:跳过参数表与泛型;先遇到 `}`、`=` 或下一个声明说明没有函数体(协议要求)。""" + parens = 0 + while index < len(text): + ch = text[index] + if ch in "([": + parens += 1 + elif ch in ")]": + parens -= 1 + elif parens == 0: + if ch == "{": + return index + if ch in "};=" or _DECL_KEYWORD.match(text, index): + return None + index += 1 + return None + + +def functions(source: str) -> list[tuple[str, int, int, int]]: + """返回 (名字, 声明行号, 函数体行数, 最大嵌套深度)。""" + text = strip(source) + closing: dict[int, int] = {} + stack: list[int] = [] + for index, ch in enumerate(text): + if ch == "{": + stack.append(index) + elif ch == "}" and stack: + closing[stack.pop()] = index + functions = [] + for decl in _SWIFT_DECL.finditer(text): + open_at = decl.end() - 1 if decl.group("var") else _body_open(text, decl.end()) + if open_at is None or open_at not in closing: + continue + close_at = closing[open_at] + depth = deepest = 0 + for ch in text[open_at : close_at + 1]: + if ch == "{": + depth += 1 + deepest = max(deepest, depth) + elif ch == "}": + depth -= 1 + name = decl.group("func") or decl.group("init") or decl.group("var") + body_lines = text.count("\n", open_at, close_at) + 1 + functions.append((name, text.count("\n", 0, decl.start()) + 1, body_lines, deepest)) + return functions + + +def signatures(source: str) -> dict[str, list[str]]: + """函数名 → 该名下全部声明(重载按多个签名计)。""" + result: dict[str, list[str]] = {} + for match in FUNC.finditer(source): + text = " ".join(f"{match['generic'] or ''}({match['params']}){match['tail']}".split()) + result.setdefault(match["name"], []).append(text) + return {name: sorted(items) for name, items in result.items()} + + +def new_dependencies(path: str, lines: list[str], local: set[str]) -> list[str]: + return [f"`{path}` 新增了 `import {match[1]}`" for text in lines if (match := IMPORT.match(text))] diff --git a/harness/dispatch_prompt.md b/.harness/engine/prompts/dispatch_prompt.md similarity index 92% rename from harness/dispatch_prompt.md rename to .harness/engine/prompts/dispatch_prompt.md index f8449b3..cef6cba 100644 --- a/harness/dispatch_prompt.md +++ b/.harness/engine/prompts/dispatch_prompt.md @@ -14,7 +14,7 @@ ## 不要做 - 不推送、不开 PR、不评论、不合并:推送与开 PR 由派发脚本完成。 -- 不编辑任务书、规格、`harness/`、`docs/runs/`、CI 与 Agent 配置;需要改时按下面的方式升级。 +- 不编辑任务书、规格、`.harness/`、`docs/runs/`、CI 与 Agent 配置;需要改时按下面的方式升级。 - 不改写已提交的历史,不设置任何 `HARNESS_` 开头的变量。 - 守卫拒绝某个操作时,不要换一种写法重试同一件事。 diff --git a/harness/review_prompt.md b/.harness/engine/prompts/review_prompt.md similarity index 97% rename from harness/review_prompt.md rename to .harness/engine/prompts/review_prompt.md index c7fd2bc..d1acfae 100644 --- a/harness/review_prompt.md +++ b/.harness/engine/prompts/review_prompt.md @@ -8,7 +8,7 @@ - `build/review/pack.md`:机器生成的证据包(风险等级、修复证据的提交与测试映射、涉及的验收编号) - `build/review/ci.md`:当前 head 的 CI 检查结论与链接(核对 PR 描述中「CI 通过」一类说法以它为准) - 材料为空或与 PR 描述明显不符时,结论为「需用户验收」并在发现里写明,不要在空材料上给出通过 -- 评审清单:`docs/templates/review-checklist.md`;仓库规则:`AGENTS.md`、`docs/specs/delivery-harness.md` +- 评审清单:`docs/templates/review-checklist.md`;仓库规则:`AGENTS.md`(如有) ## 怎么评 diff --git a/.harness/engine/reports/__init__.py b/.harness/engine/reports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/harness/metrics.py b/.harness/engine/reports/metrics.py similarity index 93% rename from harness/metrics.py rename to .harness/engine/reports/metrics.py index f7c5ab6..a627528 100644 --- a/harness/metrics.py +++ b/.harness/engine/reports/metrics.py @@ -1,6 +1,6 @@ """交付度量:统计一次交付(base..head)中机器可数的指标,并与 v0.8.0 基线并列。 - python3 harness/metrics.py --base origin/main [--head HEAD] [--github] [--json] + bin/harness metrics --base origin/main [--head HEAD] [--github] [--json] --github 时用 GITHUB_TOKEN 与 GITHUB_REPOSITORY 查询该分支的 build workflow 运行(CI 轮次、失败轮次); 没有凭据时标为「不可用」,不猜。评审轮次、自述不实、人工介入由人记录在交付说明中(见 @@ -12,18 +12,14 @@ import argparse import json import os -import sys import urllib.parse import urllib.request from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -import evidence -import risk -import run_check -from common import ROOT, added_lines, git -from replay_cases import CASES, DEFERRED, GUARDED +from engine.checks import evidence +from engine.core.cases import load_project_cases +from engine.core.common import ROOT, added_lines, git +from engine.routing import risk, run_check # docs/review/2026-09-25-harness-baseline.md §2 V080_BASELINE = { @@ -44,7 +40,8 @@ def added_test_functions(base: str, head: str, cwd: Path = ROOT) -> int: def replay_coverage(defects: list[str]) -> tuple[int, list[str]]: - covered = {case.defect for case in CASES} | set(GUARDED) | set(DEFERRED) + project = load_project_cases() + covered = {case.defect for case in project.CASES} | set(project.GUARDED) | set(project.DEFERRED) missing = [defect for defect in defects if defect not in covered] return len(defects) - len(missing), missing diff --git a/harness/weekly.py b/.harness/engine/reports/weekly.py similarity index 96% rename from harness/weekly.py rename to .harness/engine/reports/weekly.py index ee2c8ca..fff6e0d 100644 --- a/harness/weekly.py +++ b/.harness/engine/reports/weekly.py @@ -6,8 +6,8 @@ 与前 4 周的中位数比较,以下信号翻倍即在顶部标红(设计 10.3):重试与 CI 轮次、守卫拒绝、升级数、被改动的已有测试、 逃逸登记。每周的原始数据以隐藏注释存在当周评论里,下周据此比较。 - python3 harness/weekly.py 打印本周报告 - python3 harness/weekly.py --publish 更新「每周质量报告」议题(正文为最新一期,另发一条当周评论) + bin/harness weekly 打印本周报告 + bin/harness weekly --publish 更新「每周质量报告」议题(正文为最新一期,另发一条当周评论) """ from __future__ import annotations @@ -18,19 +18,15 @@ import re import statistics import subprocess -import sys from dataclasses import dataclass, field from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) +from engine.checks import r1_checks +from engine.core.common import ROOT, git, setting +from engine.reports import metrics +from engine.routing import policy, risk -import metrics -import policy -import r1_checks -import risk -from common import ROOT, git - -REPORT_TITLE = "每周质量报告" +REPORT_TITLE = setting("reports", "weekly_title", "每周质量报告") REPORT_LABEL = "weekly-report" DATA_MARK = re.compile(r"", re.DOTALL) SIGNALS = { @@ -42,7 +38,8 @@ } # 人工时间的标准分钟(设计 14.1;每月由用户校正)。 MINUTES = {"approve": 3, "change_request": 15, "escalation": 10, "audit": 5} -BOTS = {"github-actions", "app/github-actions", "snowsonz-auto-merge"} +# 自动合并与批准的机器人账号:GitHub Actions,加上项目的批准 App(checks.toml [reports] bots)。 +BOTS = {"github-actions", "app/github-actions", *setting("reports", "bots", [])} LABELS = { "escape": ("d93f0b", "合并后才发现的逃逸缺陷,正文写明引入的 PR"), "audit": ("fbca04", "抽样审计"), @@ -383,8 +380,8 @@ def publish(text: str, gh=_gh) -> int: def build(end: dt.datetime, gh=_gh, cwd: Path = ROOT, comments: list[dict] | None = None) -> str: week = collect(end, gh, cwd) - quality = json.loads((cwd / "harness" / "quality-baseline.json").read_text()) - mutation = json.loads((cwd / "harness" / "mutation-baseline.json").read_text()) + quality = json.loads((cwd / ".harness" / "state" / "quality-baseline.json").read_text()) + mutation = json.loads((cwd / ".harness" / "state" / "mutation-baseline.json").read_text()) data = compute(week, quality, mutation) history = history_from_comments(comments or [], week.key) history_map = json.loads((cwd / "docs" / "runs" / "history.json").read_text()) \ diff --git a/.harness/engine/routing/__init__.py b/.harness/engine/routing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/harness/policy.py b/.harness/engine/routing/policy.py similarity index 95% rename from harness/policy.py rename to .harness/engine/routing/policy.py index 3be7e2c..d4fa74b 100644 --- a/harness/policy.py +++ b/.harness/engine/routing/policy.py @@ -15,7 +15,7 @@ K3 按 PR 编号哈希每 audit_every 个抽 1 个,合并后由工作流开 audit 议题。 - python3 harness/policy.py --base origin/main --head --pr <编号> --branch <分支> [--github] + bin/harness policy --base origin/main --head --pr <编号> --branch <分支> [--github] """ from __future__ import annotations @@ -26,20 +26,21 @@ import os import re import subprocess -import sys from dataclasses import dataclass, field from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -import r1_checks -import risk -import run_check -import taskbook -from common import ROOT, commit_field, git, path_matches +from engine.checks import r1_checks, taskbook +from engine.core.common import ROOT, commit_field, git, path_matches, setting +from engine.routing import risk, run_check CONTRACT_PATTERNS = ["docs/plans/task-*.md", "docs/templates/**", "docs/plans/backlog.md", "docs/specs/**"] -UI_PATTERNS = ["native/**"] + + +def ui_patterns() -> list[str]: + """UI 代码(checks.toml [sources] ui):改动按 K6 归类,不自动合并。""" + return list(setting("sources", "ui", [])) + + BUDGET_LABEL = "budget-exceeded" ESCAPE_LABEL = "escape" @@ -76,7 +77,7 @@ def machine_class(report: risk.RiskReport, defects: bool) -> str: return "K0" if defects: return "K4" - return "K6" if any(path_matches(path, UI_PATTERNS) for path in paths) else "K5" + return "K6" if any(path_matches(path, ui_patterns()) for path in paths) else "K5" if any(path_matches(path, ["tests/**"]) for path in paths): return "K2" if any(path_matches(path, ["docs/plans/task-*.md"]) for path in paths): @@ -146,7 +147,7 @@ def render(rules: list[Rule], facts: Facts, audit: bool) -> str: lines += ["", "R1 加强判定未通过:"] + [f"- {reason}" for reason in facts.risk.r1_violations] if audit: lines += ["", f"本 PR 被抽中审计({facts.machine_class}),合并后开 audit 议题。"] - lines += ["", "停机:Actions → auto-merge → Disable workflow(docs/specs/delivery-harness.md §8)。"] + lines += ["", "停机:Actions → auto-merge → Disable workflow。"] return "\n".join(lines) + "\n" diff --git a/harness/risk.py b/.harness/engine/routing/risk.py similarity index 93% rename from harness/risk.py rename to .harness/engine/routing/risk.py index 88492f5..0ea614b 100644 --- a/harness/risk.py +++ b/.harness/engine/routing/risk.py @@ -1,18 +1,18 @@ -"""按改动路径判定风险等级(R0–R3),不由执行者自报。规则在 harness/rules.toml [risk]。 +"""按改动路径判定风险等级(R0–R3),不由执行者自报。规则在 .harness/config/rules.toml [risk]。 R0 说明性文档、只新增测试 门禁全绿即可自动合并 R1 声明为行为不变的重构并通过机器核对 自动合并 + 抽样审计 R2 产品代码、现役规格、改动已有测试 评审方评审 + 用户看证据包后合并 R3 护栏、CI 与发布、迁移、隐私、用户配置 必须由用户批准 -文档中,模板与待办清单是合同,按 R2;任务书按头部的类别判定(harness/taskbook.py):护栏与流程(K7)、 +文档中,模板与待办清单是合同,按 R2;任务书按头部的类别判定(engine/checks/taskbook.py):护栏与流程(K7)、 发版(K8)、architecture: true 的任务书按 R2,其余按 R0(准入由 CI 中的 verify 检查)。 R1 需要同时满足:范围内每个提交都带 `Risk: R1` trailer;最高等级只来自产品代码路径; 已有测试与黄金快照零改动;以及 r1_checks.py 的加强判定(被改函数签名不变、无新依赖与迁移、 不超规模阈值,2026-09-28 决定 2)。任何一条不满足都按 R2。变异得分不降由 build 的 harness job 核对。 - python3 harness/risk.py --base origin/main [--head HEAD] [--github] + bin/harness risk --base origin/main [--head HEAD] [--github] """ from __future__ import annotations @@ -20,15 +20,11 @@ import argparse import os import re -import sys from dataclasses import dataclass, field from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -import r1_checks -import taskbook -from common import ( +from engine.checks import r1_checks, taskbook +from engine.core.common import ( ROOT, added_line_count, added_lines, @@ -38,6 +34,7 @@ load_rules, path_matches, removed_line_count, + setting, ) POLICY = { @@ -46,7 +43,13 @@ 2: "评审方评审 + 用户看证据包后合并", 3: "必须由用户批准后合并", } -CODE_PATTERNS = ["scripts/**", "native/**"] + + +def code_patterns() -> list[str]: + """产品代码(checks.toml [sources] code)。""" + return list(setting("sources", "code", [])) + + # 产品代码不该引用测试框架:在 import 时替换断言即可让已有测试失效(评审 PR7-R7)。 TEST_FRAMEWORK = re.compile(r"\b(unittest|TestCase|pytest|XCTest)\b") @@ -161,7 +164,7 @@ def classify(base: str, head: str = "HEAD", cwd: Path = ROOT, rules: dict | None touching = sorted( path for path, lines in added_lines(base, head, cwd).items() - if path_matches(path, CODE_PATTERNS) and any(TEST_FRAMEWORK.search(text) for _, text in lines) + if path_matches(path, code_patterns()) and any(TEST_FRAMEWORK.search(text) for _, text in lines) ) if touching: report.flags.append( @@ -172,12 +175,12 @@ def classify(base: str, head: str = "HEAD", cwd: Path = ROOT, rules: dict | None if appended: report.notes.append( "追加到已有测试文件:" + "、".join(f"`{path}`" for path in appended) - + "。已有测试另按 base 版本在 head 代码上运行(`harness/base_tests.py`),追加的代码影响不到判定(PR7-R2)" + + "。已有测试另按 base 版本在 head 代码上运行(`engine/checks/base_tests.py`),追加的代码影响不到判定(PR7-R2)" ) report.claimed_r1 = commits_claim_r1(base, head, cwd) if report.claimed_r1: r2_files = [item for item in report.files if item.level == 2] - only_code = all(path_matches(item.path, CODE_PATTERNS) for item in r2_files) + only_code = all(path_matches(item.path, code_patterns()) for item in r2_files) eligible = report.level == 2 and only_code and not report.flags strengthened = r1_checks.violations(base, head, cwd) if eligible else [] if eligible and not strengthened: diff --git a/harness/run_check.py b/.harness/engine/routing/run_check.py similarity index 97% rename from harness/run_check.py rename to .harness/engine/routing/run_check.py index 4dc8796..5d02ad7 100644 --- a/harness/run_check.py +++ b/.harness/engine/routing/run_check.py @@ -13,7 +13,7 @@ 权威判定在 auto-merge 的 policy.py(main 上的代码,只读 PR 数据);build 的 harness job 只把结果写进 summary。 - python3 harness/run_check.py --base origin/main [--head HEAD] [--branch task/005-x] [--github] + bin/harness run-check --base origin/main [--head HEAD] [--branch task/005-x] [--github] """ from __future__ import annotations @@ -24,14 +24,11 @@ import os import re import subprocess -import sys from dataclasses import dataclass from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -import taskbook -from common import ROOT, changed_files, commit_field, git +from engine.checks import taskbook +from engine.core.common import ROOT, changed_files, commit_field, git RECORD_FIELDS = ( "task", "class", "attempt", "branch", "gen_ai.agent.name", "host_version", "gen_ai.request.model", diff --git a/harness/replay_cases.py b/.harness/project/replay_cases.py similarity index 93% rename from harness/replay_cases.py rename to .harness/project/replay_cases.py index be27f76..182762f 100644 --- a/harness/replay_cases.py +++ b/.harness/project/replay_cases.py @@ -1,6 +1,7 @@ """事故回放集:把 v0.8.0 的缺陷重新注入产品代码,对应检查必须失败。 -每条注入用例:在 file 中把恰好出现一次的 find 换成 replace(模拟缺陷回来了),然后运行 tests, +每条注入用例:在 file 中把恰好出现一次的 find 换成 replace(模拟缺陷回来了),然后运行 tests(Python 测试 id, +或 checks.toml [replay.suites] 登记的套件名), 期望失败。find 找不到或不唯一时用例算「过期」,必须随代码一起维护。 过程类失败(历史改写、声称已修等)不适合源码注入,由 GUARDED 中列出的守卫测试覆盖; @@ -10,21 +11,7 @@ from __future__ import annotations -from dataclasses import dataclass - - -@dataclass(frozen=True) -class Case: - defect: str - title: str - file: str - find: str - replace: str - tests: tuple[str, ...] # Python 测试 id(在 tests/ 下运行);"swift-policy" 表示 Swift 策略测试 - - @property - def platform(self) -> str: - return "macos" if "swift-policy" in self.tests else "any" +from engine.core.cases import Case CASES: list[Case] = [ @@ -214,7 +201,7 @@ def platform(self) -> str: Case( "H0925-5", "证据检查只认 git trailer(Defect 与 Co-Authored-By 隔空行即被忽略)", - "harness/common.py", + ".harness/engine/core/common.py", ' message = git("log", "-1", "--format=%B", sha, cwd=cwd)', ' message = git("log", "-1", "--format=%(trailers:only,unfold)", sha, cwd=cwd)', ( @@ -226,7 +213,7 @@ def platform(self) -> str: Case( "PR7-R1", "doc 类修复改了代码也不查", - "harness/evidence.py", + ".harness/engine/checks/evidence.py", " if evidence.code_files:\n # doc 类修复免于测试核对", " if False:\n # doc 类修复免于测试核对", ("test_harness.EvidenceTest.test_doc_defect_that_changes_code_is_rejected",), @@ -234,7 +221,7 @@ def platform(self) -> str: Case( "PR7-R2", "已有测试不按 base 版本运行(追加的 monkeypatch 生效)", - "harness/base_tests.py", + ".harness/engine/checks/base_tests.py", " _restore_base(judged, base, worktree, cwd)", " pass", ("test_harness.BaseTestsTest",), @@ -242,15 +229,15 @@ def platform(self) -> str: Case( "H0926-4", "base_tests 只还原 tests/,head 规格引用新测试时 base 的验收映射测试误判失败", - "harness/base_tests.py", - 'JUDGE_INPUTS = ("tests", "docs/specs", "harness/acceptance-gaps.txt")', + ".harness/engine/checks/base_tests.py", + 'JUDGE_INPUTS = ("tests", "docs/specs", ".harness/state/acceptance-gaps.txt")', 'JUDGE_INPUTS = ("tests",)', ("test_harness.BaseTestsTest.test_new_tests_referenced_in_specs_are_not_a_regression",), ), Case( "H0926-5", "reference-transaction 把 pack-refs 删除散文件当作删 tag 拒绝", - "harness/git_guard.py", + ".harness/engine/guards/git_guard.py", " if new == ZERO_SHA and old != ZERO_SHA and _packing_loose_ref(ref, old, cwd):", " if False:", ("test_harness_guard.GitGuardTest.test_packing_refs_is_not_a_tag_deletion",), @@ -258,7 +245,7 @@ def platform(self) -> str: Case( "H0926-5", "只凭 packed-refs 放行,指定旧值的 update-ref -d 可删掉 tag", - "harness/git_guard.py", + ".harness/engine/guards/git_guard.py", ' return _transaction_subcommand() == "pack-refs"', " return True", ("test_harness_guard.GitGuardTest.test_packing_refs_is_not_a_tag_deletion",), @@ -266,15 +253,15 @@ def platform(self) -> str: Case( "PR7-R3", "git 守卫读工作区的规则文件", - "harness/git_guard.py", - " if shown.returncode != 0:\n return load_rules()", - " if True:\n return load_rules()", + ".harness/engine/guards/git_guard.py", + " if shown.returncode == 0:\n break", + " if True:\n return load_rules()", ("test_harness_guard.TamperedRulesTest",), ), Case( "PR7-R4", "修复前以出错结束只记警告", - "harness/evidence.py", + ".harness/engine/checks/evidence.py", ' evidence.problems.append(\n f"修复退回后测试以出错而非断言失败结束', ' evidence.warnings.append(\n f"修复退回后测试以出错而非断言失败结束', ("test_harness.EvidenceTest.test_error_before_fix_is_not_proof",), @@ -282,7 +269,7 @@ def platform(self) -> str: Case( "PR7-R5", "整文件退回,连带撤掉后续无关提交", - "harness/evidence.py", + ".harness/engine/checks/evidence.py", " if not _reverse_apply_fix(evidence, worktree, cwd):", " if True:", ("test_harness.EvidenceTest.test_revert_only_the_fix_not_later_commits",), @@ -290,7 +277,7 @@ def platform(self) -> str: Case( "PR7-R6", "不按 shell 规则去掉引号(拆写的覆盖变量;E3 后由 shlex posix 分词负责)", - "harness/shell_structure.py", + ".harness/engine/core/shell_structure.py", ' lexer = shlex.shlex(text, posix=True, punctuation_chars=";&|()<>\\n")', ' lexer = shlex.shlex(text, posix=False, punctuation_chars=";&|()<>\\n")', ("test_harness_guard.CommandGuardTest.test_remote_writes_and_split_override_variables",), @@ -298,7 +285,7 @@ def platform(self) -> str: Case( "PR7-R6", "只拦 GitHub API 的 DELETE", - "harness/shell_structure.py", + ".harness/engine/core/shell_structure.py", " if method in WRITE_METHODS or fields:", ' if method == "DELETE":', ("test_harness_guard.CommandGuardTest.test_remote_writes_and_split_override_variables",), @@ -306,7 +293,7 @@ def platform(self) -> str: Case( "PR7-R7", "base_tests 不核对测试框架是否被产品代码篡改", - "harness/base_tests_runner.py", + ".harness/engine/checks/base_tests_runner.py", " if changed or caught != 3:", " if False:", ("test_harness.BaseTestsTest.test_product_code_cannot_disable_assertions_on_import",), @@ -314,7 +301,7 @@ def platform(self) -> str: Case( "PR7-R7", "有意改动测试时,框架被篡改也只报告", - "harness/base_tests.py", + ".harness/engine/checks/base_tests.py", " return completed.returncode == 0, not intended or completed.returncode == TAMPERED, summary", " return completed.returncode == 0, not intended, summary", ("test_harness.BaseTestsTest.test_tampering_is_enforced_even_when_tests_changed_on_purpose",), @@ -322,7 +309,7 @@ def platform(self) -> str: Case( "PR7-R7", "产品代码引用测试框架不产生标记,仍可声明 R1", - "harness/risk.py", + ".harness/engine/routing/risk.py", " if touching:", " if False:", ("test_harness.RiskTest.test_product_code_touching_test_framework_is_flagged_and_not_r1",), @@ -330,7 +317,7 @@ def platform(self) -> str: Case( "PR7-R9", "curl 写请求只认分开写的方法(-XPOST、--request=put 连写漏判)", - "harness/shell_structure.py", + ".harness/engine/core/shell_structure.py", ' method = _method(args, ("-X", "--request"), ("-X", "--request="))', ' method = _method(args, ("-X", "--request"), ())', ("test_harness_guard.CommandGuardTest.test_curl_writes_in_any_argument_order",), @@ -338,7 +325,7 @@ def platform(self) -> str: Case( "H0926-3", "verify 的检查子进程继承钩子注入的 GIT_DIR(linked worktree 中指向真实仓库)", - "harness/verify.py", + ".harness/engine/checks/verify.py", ' env=clean_git_env({"PYTHONDONTWRITEBYTECODE": "1"}),', ' env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},', ("test_harness.VerifyTest.test_checks_do_not_inherit_hook_git_dir",), @@ -346,7 +333,7 @@ def platform(self) -> str: Case( "H0926-2", "执行方守卫把缺口清单也一并禁改,规范要求的删行做不了", - "harness/command_guard.py", + ".harness/engine/guards/command_guard.py", " if pattern and relative not in IMPLEMENTER_EDITABLE:", " if pattern:", ("test_harness_guard.CommandGuardTest.test_implementer_may_shrink_gap_list_but_not_edit_other_verifiers",), @@ -354,7 +341,7 @@ def platform(self) -> str: Case( "H0926-5", "只读的 gh api graphql 查询因 -f 传参被当成写请求拒绝", - "harness/shell_structure.py", + ".harness/engine/core/shell_structure.py", ' if action == "graphql" and _graphql_read_only(args):', " if False:", ("test_harness_guard.CommandGuardTest.test_remote_writes_and_split_override_variables",), diff --git a/harness/acceptance-gaps.txt b/.harness/state/acceptance-gaps.txt similarity index 100% rename from harness/acceptance-gaps.txt rename to .harness/state/acceptance-gaps.txt diff --git a/harness/mutation-baseline.json b/.harness/state/mutation-baseline.json similarity index 100% rename from harness/mutation-baseline.json rename to .harness/state/mutation-baseline.json diff --git a/harness/quality-baseline.json b/.harness/state/quality-baseline.json similarity index 100% rename from harness/quality-baseline.json rename to .harness/state/quality-baseline.json diff --git a/harness/taskbook-exempt.txt b/.harness/state/taskbook-exempt.txt similarity index 100% rename from harness/taskbook-exempt.txt rename to .harness/state/taskbook-exempt.txt diff --git a/bin/harness b/bin/harness new file mode 100755 index 0000000..50c993f --- /dev/null +++ b/bin/harness @@ -0,0 +1,12 @@ +#!/bin/sh +# delivery-harness 入口:bin/harness <子命令> [参数…](子命令见 bin/harness --help)。 +# 解释器与 git 钩子相同:优先 .harness/config/checks.toml [runtime] python,没有时用 PATH 上的 python3。 +set -eu +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +runtime=$(sed -n '/^\[runtime\]/,/^\[/s/^python *= *"\(.*\)".*/\1/p' "$root/.harness/config/checks.toml" 2>/dev/null | head -n 1) +if [ -n "$runtime" ] && [ -x "$root/$runtime" ]; then + runtime="$root/$runtime" +else + runtime=$(command -v python3) +fi +exec "$runtime" "$root/.harness/engine/cli.py" "$@" diff --git a/harness/README.md b/harness/README.md deleted file mode 100644 index 91c11ad..0000000 --- a/harness/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# harness:可验证交付的判定器与护栏 - -本目录把「AI Agent 在低人工参与下持续、稳定、高质量地交付」落成机器可执行的规则。本项目是这套做法的标杆:目标是**有效**(有数据证明)且**可复制**(脚本、模板和文档能被其他项目照搬)。 - -## 愿景 - -每一类改动的「完成」,都从「Agent 说做完了、人去复验」变成: - -- 开工前就有可执行的判定; -- 完工后由 Agent 之外的机器重跑判定,并留下证据; -- 人只做机器做不了的事:定义意图、设计约束、审证据、处理例外。 - -自治程度不靠信任,而是按「可验证性 × 风险」逐级放开:判定器越可靠、风险越低,放得越多(R0/R1 自动合并,R2 以上由用户审批)。 - -## 理念来源 - -- 调研报告《低人工干预下 AI Agent 持续高质量交付:理论与全链路最佳实践》(2026-09-23):入库快照 [docs/research/2026-09-23-agent-delivery-theory.md](../docs/research/2026-09-23-agent-delivery-theory.md),编辑源为 [Claude 文档](https://claude.ai/code/artifact/487213ef-a1a8-44fd-9399-2e37f91c8b0c)。先读它的「核心结论」和「3.2 八条设计原则」。 -- 开工时的落地计划:[2026-09-25 落地交接说明](../docs/plans/2026-09-25-harness-rollout-handoff.md)(原为报告最后一节:基线、失败分类、可验证性地图、P0–P6 路线),历史文档。 -- 目标态设计:[Agent-Notification 可验证交付 Harness 目标态设计](https://claude.ai/code/artifact/a7646759-f838-49a8-aa1a-175b329ea1ed)(2026-09-27,第十七节为 2026-09-28 的决定,第十六节为分阶段实施路线)。 -- **现状(2026-09-29)**:阶段一(P1–P3)与阶段二(P4–P8)全部落地;成熟度为 M1 完整、M2 的机制基本建齐但准入数据未满足;下一阶段是阶段三「放权」,以真实使用积累数据。对照报告与设计的逐项复核见 [2026-09-29 现状复核](../docs/review/2026-09-29-harness-status.md)。 -- 落到本仓库的方案与决定:[可验证交付方案](../docs/plans/verifiable-delivery.md)(§1 目标、§2「可验证」的操作定义、§10 决定记录)。 -- 本项目自己的失败样本:v0.8.0 交付中的 R1–R19 与 X1–X6,分类见 [2026-09-25 基线评审](../docs/review/2026-09-25-harness-baseline.md)。每条护栏都对应其中至少一种失败。 - -## 原则与落点 - -报告的八条设计原则,在本仓库分别由这些部分承担: - -| 原则(报告 3.2) | 本仓库的落点 | -|---|---| -| 1. 先有判定器,再放权 | `verify.py`(本机、云端、CI 同一条命令);`acceptance.py`(规格验收编号必须有测试或登记缺口);`taskbook.py`(任务书不合格不派发,验收必须挂规格编号);只有判定可靠的 R0/R1 才自动合并 | -| 2. 确定性优先 | 风险等级由 `risk.py` 按改动路径判定,不由执行者自报;修复证据由 `evidence.py` 生成;评审证据包由 `review_pack.py` 汇总 | -| 3. 生成与验证分离 | 角色分工:Codex、Claude Code 设计与评审,Zcode、OpenCode、Pi 执行;判定在 CI 中重跑;`auto-merge.yml` 执行 main 上的定义;Agent 用单独账号,合并须由非推送者批准 | -| 4. 结构防护胜过提示词 | 三层护栏(见下);执行方不能改判定器与合同(任务书、规格);已有测试按 base 版本运行(`base_tests.py`);判定器本身被检验(修复前必须失败、变异测试、事故回放) | -| 5. 小批量、可回滚 | 一个任务一个 PR,按 R0–R3 分级;回滚方式写进任务书 | -| 6. 状态外置、上下文从简 | 任务书(机器可读的 YAML 头部)与模板(`docs/templates/`)、[待办清单](../docs/plans/backlog.md)、决定记录;中断后从文件恢复 | -| 7. 自治度按「任务类别 × 风险」 | `policy.py` 按 `autonomy.toml` 的类别自治等级与误差预算路由:满足条件的 R0/R1 由 App 批准后自动合并,超预算自动停该类,其余请用户评审;停机用 Disable workflow(规范 §8) | -| 8. 每次失败都沉淀为 harness 改进 | 缺陷编号全局唯一,修复带 `Defect:` 与回归测试;`replay_cases.py` 事故回放集;变异与质量基线只升不降(质量含 Swift),文档断链由 `docs_check.py` 拦下;每周周报与错误分析把异常沉淀为待办 | - -## 组成 - -**判定器**(回答「做对了吗」,都由 `verify.py` 或 CI 调用) - -| 文件 | 作用 | -|---|---| -| `verify.py` | 统一验证入口:lint、卫生、验收映射、Python 与 Swift 测试、回放 | -| `acceptance.py`、`acceptance-gaps.txt` | 规格验收编号 ↔ 测试的映射;暂缺的登记在缺口清单,只能缩减 | -| `taskbook.py`、`taskbook-exempt.txt` | 任务书准入:头部、类别与风险、验收挂规格编号(复用 `acceptance.py` 的解析)、章节、步骤交叉核对;历史任务登记豁免,清单只能缩减 | -| `evidence.py` | 修复证据:退回带 `Defect:` 的提交,引用该编号的测试必须以断言失败结束,恢复后通过;每个非文档类编号须有回放(注入、守卫测试或写明原因的暂缓) | -| `base_tests.py`、`base_tests_runner.py` | 已有测试按 base 版本在新代码上运行,防止执行方改测试迁就代码 | -| `replay.py`、`replay_cases.py` | 事故回放:把历史缺陷注入代码副本,对应检查必须失败 | -| `mutate.py`、`mutation-baseline.json` | 关键函数的变异测试,得分只升不降 | -| `quality.py`、`quality-baseline.json` | 熵治理:Python 复杂度(ruff C901)与体量、Swift 超长文件、超长函数与嵌套过深函数(标准库轻量解析)只降不升 | -| `docs_check.py` | 文档熵治理:已跟踪 Markdown 的相对链接断链即失败;未完成状态超过 30 天未更新的文档只报告 | -| `hygiene.py` | 禁止提交的路径、超大文件、凭据、本机真实路径 | -| `risk.py`、`r1_checks.py` | 按改动路径判定 R0–R3;任务书按头部类别判定,模板与待办清单为 R2;声明 R1 时另核对签名不变、无新依赖与迁移、不超规模 | -| `policy.py`、`autonomy.toml` | 合并路由:风险、类别自治等级、误差预算、规模、任务 PR 的运行记录逐条判定并写理由;K3 三抽一抽审;放权只改 `autonomy.toml`(R3) | -| `run_check.py` | 任务 PR 的 CI 侧复核:`Task:` 归属、运行记录格式与一致性、CI 轮次对预算 | -| `weekly.py` | 周报:交付指标、突增标红、误差预算与抽审进度、试跑记录汇总,写进「每周质量报告」议题 | -| `review.py`、`review_prompt.md`(`bin/dispatch review`,后台 `--watch`) | 独立评审:非设计方、只读、只看任务书与 diff 与证据包,结论评论到 PR;`calibrate` 在 `evals/review/` 校准集上统计 TPR、TNR | -| `release_check.py` | 发版前核对版本号与 tag | -| `metrics.py` | 交付度量,与 v0.8.0 基线并列 | -| `review_pack.py` | 评审证据包:把机器结论汇成一页 | - -**派发**(回答「怎样让执行方在限定范围内做完」) - -| 文件 | 作用 | -|---|---| -| `dispatch.py`(`bin/dispatch`) | 准入、认领、槽位、守卫预检、本地判定、运行记录、开 PR、等 CI、升级与停机 | -| `dispatch_host.py` | 启动执行方(Pi),监视时长与卡死,最小权限环境,解析 token 与守卫拒绝 | -| `dispatch_prompt.md` | 执行方提示词模板,只由任务书渲染,随运行记录入库 | - -**三层护栏**(回答「最坏会怎样」) - -| 层 | 组成 | 挡住什么 | -|---|---|---| -| Agent 层 | `command_guard.py` + `shell_structure.py`,由各宿主的钩子调用:`.claude/settings.json`、`.codex/hooks.json`、`.opencode/plugin/`、`.pi/extensions/`、`.zcode/config.json` | 执行前拒绝改写历史、推 tag、强推 main、合并或批准 PR、设置覆盖变量、删除议题与撤登记标签;执行方不能编辑判定器、合同与运行记录,不能改动议题 | -| git 层 | `git_guard.py` + `.githooks/`,规则读 origin/main 上的 `rules.toml` | 与用哪家 Agent 无关:保护分支上提交、改写、推送卫生 | -| 服务端 | ruleset(`.github/rulesets/`)、`build.yml`、`auto-merge.yml`、单独的 Agent 账号与批准 App | 本机两层都被绕过时的兜底:必须经 PR、必需检查、非推送者批准 | - -**配置**:`rules.toml`(风险、卫生、守卫规则)、`common.py`(公共工具)。 - -## 从哪里读起 - -1. 本文件。 -2. 报告的「核心结论」与「3.2 八条设计原则」。 -3. [现役规范 delivery-harness.md](../docs/specs/delivery-harness.md):命令、约定、一次性设置、验证状态、已知边界。 -4. [待办清单](../docs/plans/backlog.md):还没做完的事。 -5. 需要追溯决定时读 [可验证交付方案](../docs/plans/verifiable-delivery.md) §10,追溯失败样本时读 [基线评审](../docs/review/2026-09-25-harness-baseline.md)。 - -## 维护 - -- 新增或删除 harness 组件时,同步更新本文件的「组成」与「原则与落点」。 -- 本目录由评审方维护,执行方不能编辑(唯一例外:`acceptance-gaps.txt` 只能删行);改动属于 R3,由用户批准。 -- 报告有实质更新时,评审方重新导出快照,覆盖 `docs/research/2026-09-23-agent-delivery-theory.md` 并更新其中的导出日期与版本。 diff --git a/harness/quality.py b/harness/quality.py deleted file mode 100644 index 99c6478..0000000 --- a/harness/quality.py +++ /dev/null @@ -1,222 +0,0 @@ -"""熵治理:产品代码的复杂度与体量只降不升(报告 11.4;He 等 MSR 2026:复杂度持续上升是后期变慢的主因)。 - -指标(scripts/ 与 native/): - complex_functions ruff C901 超标函数数(Python,圈复杂度 > 10) 只降不升 - files_over_800 超过 800 行的源文件数 只降不升 - largest_file_lines 最大源文件行数 只报告(往大文件里加小修复不应被拦) - swift_files_over_500 native/ 下超过 500 行的 Swift 文件数 只降不升 - swift_long_functions 函数体超过 60 行的 Swift 函数数 只降不升 - swift_deep_functions 大括号嵌套深度超过 6 的 Swift 函数数 只降不升 - -Swift 没有 ruff 这样的现成工具,这里用轻量解析(只用标准库):先把注释(含嵌套块注释)与字符串字面量 -(普通、多行三引号、带 # 的原始字符串)抹成空格,再配对大括号。「函数」指 func、init、deinit 与计算属性 -(`var x: T {`,SwiftUI 的 body 即此类);函数体行数从 `{` 所在行数到匹配的 `}` 所在行,嵌套深度以函数体 -自身为 1,闭包与控制流都计入。解析不求完美(如 #if 分支、正则字面量不处理),只需对同一份代码稳定。 - -阈值取自 2026-09-28 native/ 的分布(332 个函数):函数体行数中位 6、P90 29、P95 48、P99 88, -取 60 行约为前 3%;嵌套深度 1–4 占 92%,SwiftUI 视图常见 5–6 层,超过 6 层只剩 13 个,都是可以 -拆出子视图的大 body。文件 500 行:现有 Swift 文件多在 400 行上下,超过 500 行的只有 2 个。 - - python3 harness/quality.py 打印指标并与 harness/quality-baseline.json 比较,棘轮指标上升即失败 - python3 harness/quality.py --update 指标下降后写回基线(上调基线须直接改文件,属 R3,由评审确认) -""" - -from __future__ import annotations - -import argparse -import json -import re -import subprocess -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -from common import ROOT - -BASELINE = ROOT / "harness" / "quality-baseline.json" -SOURCES = [("scripts", "*.py"), ("native", "**/*.swift")] -LONG_FILE = 800 -SWIFT_LONG_FILE = 500 -SWIFT_LONG_FUNCTION = 60 -SWIFT_DEEP_NESTING = 6 -RATCHETED = ( - "complex_functions", - "files_over_800", - "swift_files_over_500", - "swift_long_functions", - "swift_deep_functions", -) - -_STRING_START = re.compile(r'(#*)("""|")') -_SWIFT_DECL = re.compile( - r"(?[^\s(<]+)|(?Pinit|deinit)\b[?!]?|var\s+(?P\w+)\s*:\s*[^=\n{};]+?\{)" -) -_DECL_KEYWORD = re.compile(r"(? str: - """把注释与字符串字面量抹成空格(保留换行,行号不变),其余字符原样保留。""" - out: list[str] = [] - i, n = 0, len(source) - block_depth = 0 - while i < n: - ch = source[i] - if block_depth: # Swift 的块注释可以嵌套 - if source.startswith("/*", i) or source.startswith("*/", i): - block_depth += 1 if source[i] == "/" else -1 - out.append(" ") - i += 2 - else: - out.append("\n" if ch == "\n" else " ") - i += 1 - continue - if source.startswith("//", i): - end = source.find("\n", i) - end = n if end < 0 else end - out.append(" " * (end - i)) - i = end - continue - if source.startswith("/*", i): - block_depth = 1 - out.append(" ") - i += 2 - continue - match = _STRING_START.match(source, i) - if match and (match.group(1) or ch == '"'): - hashes, quote = match.group(1), match.group(2) - closing = quote + hashes - j = match.end() - while j < n: - if not hashes and source[j] == "\\": - j += 2 - continue - if source.startswith(closing, j): - j += len(closing) - break - if quote == '"' and source[j] == "\n": - break - j += 1 - out.append("".join("\n" if c == "\n" else " " for c in source[i:j])) - i = j - continue - out.append(ch) - i += 1 - return "".join(out) - - -def _body_open(text: str, index: int) -> int | None: - """从声明名之后找函数体的 `{`:跳过参数表与泛型;先遇到 `}`、`=` 或下一个声明说明没有函数体(协议要求)。""" - parens = 0 - while index < len(text): - ch = text[index] - if ch in "([": - parens += 1 - elif ch in ")]": - parens -= 1 - elif parens == 0: - if ch == "{": - return index - if ch in "};=" or _DECL_KEYWORD.match(text, index): - return None - index += 1 - return None - - -def swift_functions(source: str) -> list[tuple[str, int, int, int]]: - """返回 (名字, 声明行号, 函数体行数, 最大嵌套深度)。""" - text = strip_swift(source) - closing: dict[int, int] = {} - stack: list[int] = [] - for index, ch in enumerate(text): - if ch == "{": - stack.append(index) - elif ch == "}" and stack: - closing[stack.pop()] = index - functions = [] - for decl in _SWIFT_DECL.finditer(text): - open_at = decl.end() - 1 if decl.group("var") else _body_open(text, decl.end()) - if open_at is None or open_at not in closing: - continue - close_at = closing[open_at] - depth = deepest = 0 - for ch in text[open_at : close_at + 1]: - if ch == "{": - depth += 1 - deepest = max(deepest, depth) - elif ch == "}": - depth -= 1 - name = decl.group("func") or decl.group("init") or decl.group("var") - body_lines = text.count("\n", open_at, close_at) + 1 - functions.append((name, text.count("\n", 0, decl.start()) + 1, body_lines, deepest)) - return functions - - -def measure_swift(root: Path = ROOT) -> dict[str, int]: - files = long_functions = deep_functions = 0 - for path in sorted((root / "native").glob("**/*.swift")): - source = path.read_text(encoding="utf-8") - files += len(source.splitlines()) > SWIFT_LONG_FILE - for _name, _line, body_lines, deepest in swift_functions(source): - long_functions += body_lines > SWIFT_LONG_FUNCTION - deep_functions += deepest > SWIFT_DEEP_NESTING - return { - "swift_files_over_500": files, - "swift_long_functions": long_functions, - "swift_deep_functions": deep_functions, - } - - -def measure(root: Path = ROOT) -> dict[str, int]: - completed = subprocess.run( - [sys.executable, "-m", "ruff", "check", "scripts", "--select", "C901", "--output-format", "concise", "--no-cache"], - cwd=root, - capture_output=True, - text=True, - check=False, - ) - complex_functions = sum(1 for line in completed.stdout.splitlines() if " C901 " in line) - sizes = [] - for directory, pattern in SOURCES: - for path in (root / directory).glob(pattern): - sizes.append(len(path.read_text(encoding="utf-8").splitlines())) - return { - "complex_functions": complex_functions, - "largest_file_lines": max(sizes, default=0), - "files_over_800": sum(size > LONG_FILE for size in sizes), - **measure_swift(root), - } - - -def compare(current: dict[str, int], baseline: dict[str, int]) -> list[str]: - return [ - f"{name} 从 {baseline[name]} 升到 {current[name]}" - for name in RATCHETED - if name in baseline and current[name] > baseline[name] - ] - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument("--update", action="store_true", help="指标不高于基线时写回基线") - args = parser.parse_args(argv) - current = measure() - baseline = json.loads(BASELINE.read_text()) if BASELINE.exists() else {} - for name, value in current.items(): - kind = "棘轮" if name in RATCHETED else "报告" - print(f"{name:<20} {value:>6}(基线 {baseline.get(name, '—')},{kind})") - regressions = compare(current, baseline) - if args.update: - if regressions: - print("✗ 指标高于基线,不能用 --update 上调:" + ";".join(regressions)) - return 1 - BASELINE.write_text(json.dumps(current, indent=2, sort_keys=True) + "\n") - print(f"基线已写入 {BASELINE.relative_to(ROOT)}") - return 0 - for regression in regressions: - print(f"✗ {regression}:先拆分或简化;确需上调基线请改 harness/quality-baseline.json 并说明理由(R3)") - return 1 if regressions else 0 - - -if __name__ == "__main__": - raise SystemExit(main()) From 678e8c89ca991892ee6f670fd1a7c7f77e90c41b Mon Sep 17 00:00:00 2001 From: Snowson <15168246+Snowson@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:37:42 +0800 Subject: [PATCH 2/4] =?UTF-8?q?harness=EF=BC=9A=E5=85=A5=E5=8F=A3=E3=80=81?= =?UTF-8?q?git=20=E9=92=A9=E5=AD=90=E3=80=81Agent=20=E5=AE=88=E5=8D=AB?= =?UTF-8?q?=E9=92=A9=E5=AD=90=E4=B8=8E=20CI=20=E6=94=B9=E8=B0=83=20.harnes?= =?UTF-8?q?s/engine/cli.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bin/verify、bin/dispatch 转到 bin/harness;bin/as-agent 的身份改读 checks.toml [identity]; git 钩子与 bin/harness 的解释器改读 [runtime] python;Claude Code、Codex、Zcode、OpenCode、Pi 的守卫 与 build、quality、auto-merge 工作流改调新入口。 Task: T008 Co-Authored-By: Claude Opus 5.5 --- .claude/settings.json | 4 ++-- .codex/hooks.json | 2 +- .githooks/_runtime | 14 +++++++++----- .githooks/pre-commit | 4 ++-- .githooks/pre-push | 4 ++-- .githooks/reference-transaction | 4 ++-- .github/workflows/auto-merge.yml | 6 +++--- .github/workflows/build.yml | 22 +++++++++++----------- .github/workflows/quality.yml | 10 +++++----- .opencode/plugin/harness-guard.js | 4 ++-- .pi/extensions/harness-guard.ts | 4 ++-- .zcode/config.json | 2 +- bin/as-agent | 12 ++++++++---- bin/dispatch | 11 ++--------- bin/verify | 11 ++--------- 15 files changed, 54 insertions(+), 60 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 9c180b6..23bf87f 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "python3 \"$CLAUDE_PROJECT_DIR/harness/command_guard.py\" --format claude --role designer" + "command": "python3 \"$CLAUDE_PROJECT_DIR/.harness/engine/cli.py\" guard-command --format claude --role designer" } ] }, @@ -15,7 +15,7 @@ "hooks": [ { "type": "command", - "command": "python3 \"$CLAUDE_PROJECT_DIR/harness/command_guard.py\" --format claude --role designer" + "command": "python3 \"$CLAUDE_PROJECT_DIR/.harness/engine/cli.py\" guard-command --format claude --role designer" } ] } diff --git a/.codex/hooks.json b/.codex/hooks.json index c25a8e7..ea6615c 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -5,7 +5,7 @@ "hooks": [ { "type": "command", - "command": "sh -c 'exec python3 \"$(git rev-parse --show-toplevel)/harness/command_guard.py\" --format claude --role designer'", + "command": "sh -c 'exec python3 \"$(git rev-parse --show-toplevel)/.harness/engine/cli.py\" guard-command --format claude --role designer'", "timeout": 30 } ] diff --git a/.githooks/_runtime b/.githooks/_runtime index bcdbd0f..7c69f3f 100644 --- a/.githooks/_runtime +++ b/.githooks/_runtime @@ -1,8 +1,13 @@ -# harness 钩子公共部分(由各钩子 source):定位 harness 与解释器。 -# 解释器优先用开发包运行时 venv,与 bin/verify 一致;找不到时按 HARNESS_FAIL_OPEN 决定放行或拒绝。 +# delivery-harness 钩子公共部分(由各钩子 source):定位引擎与解释器。 +# 解释器优先用 .harness/config/checks.toml [runtime] python(相对仓库根),与 bin/harness 一致; +# 没有配置或不可执行时用 PATH 上的 python3;都找不到时按 HARNESS_FAIL_OPEN 决定放行或拒绝。 HARNESS_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -HARNESS_PY="$HARNESS_ROOT/scratch/iterm-probe-venv/bin/python" -if [ ! -x "$HARNESS_PY" ]; then +HARNESS_CLI="$HARNESS_ROOT/.harness/engine/cli.py" +HARNESS_PY=$(sed -n '/^\[runtime\]/,/^\[/s/^python *= *"\(.*\)".*/\1/p' "$HARNESS_ROOT/.harness/config/checks.toml" 2>/dev/null | head -n 1) +if [ -n "$HARNESS_PY" ]; then + HARNESS_PY="$HARNESS_ROOT/$HARNESS_PY" +fi +if [ -z "$HARNESS_PY" ] || [ ! -x "$HARNESS_PY" ]; then HARNESS_PY=$(command -v python3 || true) fi if [ -z "$HARNESS_PY" ]; then @@ -10,4 +15,3 @@ if [ -z "$HARNESS_PY" ]; then if [ "${HARNESS_FAIL_OPEN:-0}" = 1 ]; then exit 0; fi exit 1 fi -HARNESS_GUARD="$HARNESS_ROOT/harness/git_guard.py" diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 29cf4a1..4ef63f1 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh -# 保护分支上禁止提交;暂存区卫生;快速 verify(harness/git_guard.py)。 +# 保护分支上禁止提交;暂存区卫生;快速 verify(.harness/engine/guards/git_guard.py)。 . "$(dirname -- "$0")/_runtime" -exec "$HARNESS_PY" "$HARNESS_GUARD" pre-commit +exec "$HARNESS_PY" "$HARNESS_CLI" guard-git pre-commit diff --git a/.githooks/pre-push b/.githooks/pre-push index 46b2409..4638db1 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,4 +1,4 @@ #!/bin/sh -# 禁推保护分支与 tag、禁强制推送;推送前完整 verify(harness/git_guard.py)。 +# 禁推保护分支与 tag、禁强制推送;推送前完整 verify(.harness/engine/guards/git_guard.py)。 . "$(dirname -- "$0")/_runtime" -exec "$HARNESS_PY" "$HARNESS_GUARD" pre-push "$@" +exec "$HARNESS_PY" "$HARNESS_CLI" guard-git pre-push "$@" diff --git a/.githooks/reference-transaction b/.githooks/reference-transaction index c2a3ee9..1d44290 100755 --- a/.githooks/reference-transaction +++ b/.githooks/reference-transaction @@ -1,5 +1,5 @@ #!/bin/sh -# 禁止本地改写/删除保护分支、移动/删除 tag(harness/git_guard.py)。 +# 禁止本地改写/删除保护分支、移动/删除 tag(.harness/engine/guards/git_guard.py)。 # 每次引用更新都会调用本钩子:只在 prepared 阶段、且涉及分支或 tag 时才启动 Python。 [ "$1" = prepared ] || exit 0 input=$(cat) @@ -10,4 +10,4 @@ esac # 解释器缺失时放行并告警:引用守卫失效不能让整个仓库无法提交。 HARNESS_FAIL_OPEN=1 . "$(dirname -- "$0")/_runtime" -printf '%s\n' "$input" | exec "$HARNESS_PY" "$HARNESS_GUARD" reference-transaction prepared +printf '%s\n' "$input" | exec "$HARNESS_PY" "$HARNESS_CLI" guard-git reference-transaction prepared diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index c004dff..20334eb 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -1,6 +1,6 @@ name: auto-merge -# L4 自治:build 与 harness 全绿后,由 harness/policy.py 判定能否自动合并(目标态设计 9.3): +# L4 自治:build 与 harness 全绿后,由 .harness/engine/cli.py policy 判定能否自动合并(目标态设计 9.3): # 风险 R0/R1、类别在 autonomy.toml 中为 L4、未超误差预算、不超规模阈值、任务 PR 的运行记录与 CI 轮次合格, # 每条理由写进 job summary。 # 停机:Actions → auto-merge → Disable workflow;恢复:Enable workflow(docs/specs/delivery-harness.md §8)。 @@ -60,7 +60,7 @@ jobs: HEAD_SHA: ${{ github.event.workflow_run.head_sha }} PR: ${{ github.event.workflow_run.pull_requests[0].number }} HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} - run: python harness/policy.py --base origin/main --head "$HEAD_SHA" ${PR:+--pr "$PR"} --branch "$HEAD_BRANCH" --github + run: python .harness/engine/cli.py policy --base origin/main --head "$HEAD_SHA" ${PR:+--pr "$PR"} --branch "$HEAD_BRANCH" --github # 不自动合并的:build 与 harness 通过后打类别标签并请仓库所有者评审,PR 出现在其待评审列表里。 # 用 GITHUB_TOKEN 只「请求评审」,不批准(仓库已关闭 Actions 批准 PR)。 @@ -145,5 +145,5 @@ jobs: gh label create audit --repo "$GITHUB_REPOSITORY" --color fbca04 --force >/dev/null gh issue create --repo "$GITHUB_REPOSITORY" --label audit --label "class:$CLASS" \ --title "抽审:PR #$PR($CLASS)" \ - --body "PR #$PR 按 harness/autonomy.toml 的抽样规则被抽中($RISK $CLASS,$HEAD_SHA)。评审方对照任务书、diff 与证据包审计;发现问题时另开带 escape 与 class:$CLASS 标签的议题,写明引入的 PR #$PR。" + --body "PR #$PR 按 .harness/config/autonomy.toml 的抽样规则被抽中($RISK $CLASS,$HEAD_SHA)。评审方对照任务书、diff 与证据包审计;发现问题时另开带 escape 与 class:$CLASS 标签的议题,写明引入的 PR #$PR。" fi diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24ed3bb..d4ef46e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,45 +42,45 @@ jobs: fi - name: Verify (quick) - run: python harness/verify.py --quick + run: python .harness/engine/cli.py verify --quick - name: Hygiene of changes - run: python harness/hygiene.py --range "${{ steps.base.outputs.sha }}" + run: python .harness/engine/cli.py hygiene --range "${{ steps.base.outputs.sha }}" # 已有判定器以 base 为准:追加或新增的测试代码不能让已有测试失效(评审 PR7-R2)。 - name: Existing tests (base version) on head - run: python harness/base_tests.py --base "${{ steps.base.outputs.sha }}" + run: python .harness/engine/cli.py base-tests --base "${{ steps.base.outputs.sha }}" # 前面的检查失败时,风险等级与证据仍要生成,评审才看得到全貌。 - name: Risk level id: risk if: ${{ !cancelled() }} - run: python harness/risk.py --base "${{ steps.base.outputs.sha }}" --github + run: python .harness/engine/cli.py risk --base "${{ steps.base.outputs.sha }}" --github # R1 加强判定中要执行代码的一项(2026-09-28 决定 2):声明 R1 时,改到的变异目标得分不得低于基线。 # 下降即失败;要按 R2 走的重构去掉 Risk: R1 即可(加一个不带它的提交)。 - name: Mutation score of changed targets (R1 claims) if: ${{ !cancelled() && steps.risk.outputs.claimed_r1 == 'true' }} - run: python harness/mutate.py --check --changed-since "${{ steps.base.outputs.sha }}" + run: python .harness/engine/cli.py mutate --check --changed-since "${{ steps.base.outputs.sha }}" # 实现任务书的 PR:归属 trailer 与运行记录(只报告;是否自动合并由 auto-merge 的 policy.py 判定)。 - name: Run record (task PRs) if: ${{ !cancelled() }} - run: python harness/run_check.py --base "${{ steps.base.outputs.sha }}" --github + run: python .harness/engine/cli.py run-check --base "${{ steps.base.outputs.sha }}" --github - name: Fix evidence if: ${{ !cancelled() }} - run: python harness/evidence.py --base "${{ steps.base.outputs.sha }}" --markdown "$GITHUB_STEP_SUMMARY" + run: python .harness/engine/cli.py evidence --base "${{ steps.base.outputs.sha }}" --markdown "$GITHUB_STEP_SUMMARY" - name: Delivery metrics if: ${{ !cancelled() }} env: GITHUB_TOKEN: ${{ github.token }} - run: python harness/metrics.py --base "${{ steps.base.outputs.sha }}" --github --markdown "$GITHUB_STEP_SUMMARY" + run: python .harness/engine/cli.py metrics --base "${{ steps.base.outputs.sha }}" --github --markdown "$GITHUB_STEP_SUMMARY" - name: Release check (tag = build version, on main) if: startsWith(github.ref, 'refs/tags/v') - run: python harness/release_check.py --tag "$GITHUB_REF_NAME" --commit "$GITHUB_SHA" + run: python .harness/engine/cli.py release-check --tag "$GITHUB_REF_NAME" --commit "$GITHUB_SHA" build: # macos-26 自带 Xcode 26:build 脚本用 actool 把 native/AppIcon.icon 编成 Liquid Glass 分层图标。 @@ -105,7 +105,7 @@ jobs: - name: Verify (lint, hygiene, Python and Swift tests, incident replay) # 与本机、云端同一入口(docs/plans/verifiable-delivery.md P1);--strict 不允许 Swift 检查被跳过。 - run: scratch/iterm-probe-venv/bin/python harness/verify.py --strict --full + run: scratch/iterm-probe-venv/bin/python .harness/engine/cli.py verify --strict --full # Swift 测试的「修复前失败、修复后通过」只能在 macOS 上跑(harness job 在 Linux 只做 Python,方案 §13 E4)。 - name: Fix evidence with Swift (pull requests) @@ -114,7 +114,7 @@ jobs: BASE: ${{ github.event.pull_request.base.sha }} run: | git fetch --no-tags --unshallow origin || git fetch --no-tags origin - scratch/iterm-probe-venv/bin/python harness/evidence.py --base "$BASE" --swift --markdown "$GITHUB_STEP_SUMMARY" + scratch/iterm-probe-venv/bin/python .harness/engine/cli.py evidence --base "$BASE" --swift --markdown "$GITHUB_STEP_SUMMARY" - name: Build Agent Notification.app run: python3 scripts/build_inbox_app.py --standalone diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 1e4dda9..2afea10 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -1,7 +1,7 @@ name: quality # 每周的测试强度与熵治理检查(docs/specs/delivery-harness.md):较慢,不放在每次提交的门禁里。 -# 周报(harness/weekly.py)在检查之后汇总交付指标,写进固定的「每周质量报告」议题;检查失败也照样出周报。 +# 周报(.harness/engine/cli.py weekly)在检查之后汇总交付指标,写进固定的「每周质量报告」议题;检查失败也照样出周报。 on: schedule: - cron: '17 2 * * 1' # 每周一 02:17 UTC @@ -34,19 +34,19 @@ jobs: run: python -m pip install -r requirements-dev.txt - name: Mutation score not below baseline - run: python harness/mutate.py --check + run: python .harness/engine/cli.py mutate --check - name: Incident replay if: ${{ !cancelled() }} - run: python harness/replay.py + run: python .harness/engine/cli.py replay - name: Complexity ratchet if: ${{ !cancelled() }} - run: python harness/quality.py + run: python .harness/engine/cli.py quality - name: Weekly report if: ${{ !cancelled() }} env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} - run: python harness/weekly.py --publish | tee -a "$GITHUB_STEP_SUMMARY" + run: python .harness/engine/cli.py weekly --publish | tee -a "$GITHUB_STEP_SUMMARY" diff --git a/.opencode/plugin/harness-guard.js b/.opencode/plugin/harness-guard.js index 3e721f2..c487fb8 100644 --- a/.opencode/plugin/harness-guard.js +++ b/.opencode/plugin/harness-guard.js @@ -1,7 +1,7 @@ // 可验证交付 harness:OpenCode(执行者)的工具调用守卫。 // // 项目级插件,只在本仓库内运行的 OpenCode 会话加载,不改用户全局配置。每次 bash / 编辑类工具 -// 调用前(以及合并、评审、议题、标签类 MCP 工具)交给 harness/command_guard.py 判定(--role implementer:另外禁止编辑判定器与护栏); +// 调用前(以及合并、评审、议题、标签类 MCP 工具)交给 .harness/engine/cli.py guard-command 判定(--role implementer:另外禁止编辑判定器与护栏); // 拒绝时抛错,OpenCode 会中止这次调用并把理由反馈给模型。 // 真实 OpenCode 中的加载与拦截效果待实测(docs/specs/delivery-harness.md「验证状态」)。 import { spawnSync } from "node:child_process"; @@ -13,7 +13,7 @@ const GUARDED_TOOLS = /merge|review|approve|issue|label/i; export function guard(root, payload) { const result = spawnSync( "python3", - [path.join(root, "harness", "command_guard.py"), "--format", "json", "--role", "implementer"], + [path.join(root, ".harness", "engine", "cli.py"), "guard-command", "--format", "json", "--role", "implementer"], { input: JSON.stringify(payload), encoding: "utf8" }, ); if (result.error) return; // 没有 python3:交给 git 与服务端两层兜底 diff --git a/.pi/extensions/harness-guard.ts b/.pi/extensions/harness-guard.ts index fbcada5..c20c117 100644 --- a/.pi/extensions/harness-guard.ts +++ b/.pi/extensions/harness-guard.ts @@ -2,7 +2,7 @@ // // 项目级扩展(.pi/extensions/),只在本仓库内、且项目已被信任时加载:未信任的项目不会加载它, // 执行时需先信任本项目或加 `pi -a`(docs/specs/delivery-harness.md §4)。每次 bash / 写入类工具 -// 以及合并、评审、议题、标签类工具调用前交给 harness/command_guard.py 判定(--role implementer:另外禁止 +// 以及合并、评审、议题、标签类工具调用前交给 .harness/engine/cli.py guard-command 判定(--role implementer:另外禁止 // 编辑判定器与护栏);拒绝时返回 block,Pi 中止这次调用并把理由反馈给模型。 import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; @@ -17,7 +17,7 @@ type Verdict = { block: true; reason: string } | undefined; export function guard(root: string, payload: Record): Verdict { const result = spawnSync( "python3", - [path.join(root, "harness", "command_guard.py"), "--format", "json", "--role", "implementer"], + [path.join(root, ".harness", "engine", "cli.py"), "guard-command", "--format", "json", "--role", "implementer"], { input: JSON.stringify(payload), encoding: "utf8" }, ); if (result.error) return undefined; // 没有 python3:交给 git 与服务端两层兜底 diff --git a/.zcode/config.json b/.zcode/config.json index e3e015b..9338848 100644 --- a/.zcode/config.json +++ b/.zcode/config.json @@ -8,7 +8,7 @@ "hooks": [ { "type": "command", - "command": "python3 \"${ZCODE_PROJECT_DIR}/harness/command_guard.py\" --format claude --role implementer", + "command": "python3 \"${ZCODE_PROJECT_DIR}/.harness/engine/cli.py\" guard-command --format claude --role implementer", "timeout": 30, "statusMessage": "harness 守卫" } diff --git a/bin/as-agent b/bin/as-agent index 90de2cd..2afdd11 100755 --- a/bin/as-agent +++ b/bin/as-agent @@ -1,5 +1,5 @@ #!/bin/sh -# 以 Agent 的 GitHub 身份运行一条命令(方案 §13 D3,规范 §5 第 8 步)。 +# 以 Agent 的 GitHub 身份运行一条命令。 # 用法:bin/as-agent opencode run "$(cat build/task-prompt.txt)" # bin/as-agent git push -u origin task/xxx # bin/as-agent gh pr create ... @@ -7,8 +7,12 @@ # 不改 gh 的当前账号,也不改仓库或全局 git 配置。 set -eu -AGENT_LOGIN=${AGENT_LOGIN:-Snowson} -AGENT_EMAIL=${AGENT_EMAIL:-15168246+Snowson@users.noreply.github.com} +# 身份取自 .harness/config/checks.toml [identity];环境变量 AGENT_LOGIN、AGENT_EMAIL 可临时覆盖。 +if [ -z "${AGENT_LOGIN:-}" ] || [ -z "${AGENT_EMAIL:-}" ]; then + identity=$("$(dirname -- "$0")/harness" identity) || exit 1 + AGENT_LOGIN=${AGENT_LOGIN:-${identity%% *}} + AGENT_EMAIL=${AGENT_EMAIL:-${identity#* }} +fi if [ "$#" -eq 0 ]; then echo "用法:bin/as-agent <命令> [参数…]" >&2 @@ -16,7 +20,7 @@ if [ "$#" -eq 0 ]; then fi if ! token=$(gh auth token --hostname github.com --user "$AGENT_LOGIN" 2>/dev/null) || [ -z "$token" ]; then - echo "gh 里没有 $AGENT_LOGIN 的登录。由用户执行一次(规范 §5 第 8 步):" >&2 + echo "gh 里没有 $AGENT_LOGIN 的登录。由用户执行一次:" >&2 echo " gh auth login --hostname github.com # 以 $AGENT_LOGIN 登录" >&2 echo " gh auth switch --user <你自己的账号> # 切回自己的账号" >&2 exit 1 diff --git a/bin/dispatch b/bin/dispatch index 43f756d..2f6f7e1 100755 --- a/bin/dispatch +++ b/bin/dispatch @@ -1,10 +1,3 @@ #!/bin/sh -# 派发脚本入口(harness/dispatch.py):把已合并的任务书交给执行方,在槽位 worktree 中完成并开 PR。 -# 解释器与 bin/verify 相同:优先开发包运行时 venv,没有时退回 PATH 上的 python3。 -set -eu -root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -runtime="$root/scratch/iterm-probe-venv/bin/python" -if [ ! -x "$runtime" ]; then - runtime=$(command -v python3) -fi -exec "$runtime" "$root/harness/dispatch.py" "$@" +# 派发与独立评审入口(bin/harness dispatch):把已合并的任务书交给执行方,在槽位 worktree 中完成并开 PR。 +exec "$(dirname -- "$0")/harness" dispatch "$@" diff --git a/bin/verify b/bin/verify index 1886bcf..f52f4bf 100755 --- a/bin/verify +++ b/bin/verify @@ -1,10 +1,3 @@ #!/bin/sh -# 统一验证入口(harness/verify.py)。解释器优先用开发包运行时 venv,与 App/CLI 同一环境; -# 没有 venv 的环境(云端、新克隆)退回 PATH 上的 python3。 -set -eu -root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -runtime="$root/scratch/iterm-probe-venv/bin/python" -if [ ! -x "$runtime" ]; then - runtime=$(command -v python3) -fi -exec "$runtime" "$root/harness/verify.py" "$@" +# 统一验证入口(bin/harness verify),本机、CI 与派发同一条命令。 +exec "$(dirname -- "$0")/harness" verify "$@" From 60a65fedeb9e37e27fffa12605dd290546eb9dee Mon Sep 17 00:00:00 2001 From: Snowson <15168246+Snowson@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:37:43 +0800 Subject: [PATCH 3/4] =?UTF-8?q?harness=EF=BC=9A=E6=B5=8B=E8=AF=95=E4=B8=8E?= =?UTF-8?q?=E7=8E=B0=E5=BD=B9=E6=96=87=E6=A1=A3=E6=94=B9=E5=88=B0=E6=96=B0?= =?UTF-8?q?=E5=B8=83=E5=B1=80=EF=BC=9B=E4=BB=BB=E5=8A=A1=E4=B9=A6=E3=80=81?= =?UTF-8?q?=E5=86=B3=E7=AD=96=E8=AE=B0=E5=BD=95=E4=B8=8E=E5=BE=85=E5=8A=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tests/test_harness*.py 改为从 engine 包导入、按新路径构造夹具;作为引擎在本项目上的契约测试暂留本仓库(B42)。 - 规范 delivery-harness.md、AGENTS.md、README 索引、模板与规格覆盖引用改到新布局;指向 harness/README.md 的链接改到引擎仓库。 - 任务书 T008(方案 B);决策记录 0001;待办新增 B42–B47。 Task: T008 Co-Authored-By: Claude Opus 5.5 --- .github/pull_request_template.md | 2 +- AGENTS.md | 4 +- README.md | 2 +- README.zh-CN.md | 7 +- docs/decisions/0001-harness-extraction.md | 31 ++++++++ .../2026-09-25-harness-rollout-handoff.md | 2 +- docs/plans/backlog.md | 8 +- docs/plans/task-008-harness-extract.md | 58 ++++++++++++++ docs/plans/verifiable-delivery.md | 2 +- .../2026-09-23-agent-delivery-theory.md | 2 +- docs/review/2026-09-27-harness-audit.md | 2 +- docs/specs/delivery-harness.md | 78 +++++++++---------- docs/specs/usage-cost.md | 2 +- docs/specs/zcode-native-navigation.md | 2 +- docs/templates/review-checklist.md | 2 +- docs/templates/review-prompt.md | 2 +- docs/templates/task.md | 8 +- tests/test_harness.py | 22 +++--- tests/test_harness_acceptance.py | 6 +- tests/test_harness_dispatch.py | 19 +++-- tests/test_harness_entropy.py | 5 +- tests/test_harness_guard.py | 38 +++++---- tests/test_harness_metrics.py | 6 +- tests/test_harness_p3.py | 12 +-- tests/test_harness_policy.py | 11 ++- tests/test_harness_release.py | 12 +-- tests/test_harness_replay.py | 14 ++-- tests/test_harness_review.py | 4 +- tests/test_harness_review_independent.py | 8 +- tests/test_harness_run_check.py | 11 ++- tests/test_harness_taskbook.py | 22 +++--- tests/test_harness_weekly.py | 10 +-- 32 files changed, 254 insertions(+), 160 deletions(-) create mode 100644 docs/decisions/0001-harness-extraction.md create mode 100644 docs/plans/task-008-harness-extract.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9c78132..c2d4695 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -24,4 +24,4 @@ ## 需要人工验收的部分 + `bin/harness acceptance --manual` 列出现役人工验收清单,从中挑出本 PR 涉及的编号。 --> diff --git a/AGENTS.md b/AGENTS.md index d79d98b..3f52d3d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,8 +5,8 @@ ## 开发入口 - Python 后端和适配器在 `scripts/`,Swift 原生界面与 Zcode 导航在 `native/`,统一 CLI 在 `bin/session-manager`。 -- 统一验证:`bin/verify`(lint、仓库卫生、Python 测试、macOS 上的 Swift 测试,与 CI 同口径)。修复提交带 `Defect: <编号>` trailer,通过与否只认 verify 与 CI 输出,不手写。约定与护栏见 docs/specs/delivery-harness.md;harness 的愿景、理念出处与组件对照见 harness/README.md。 -- 新环境准备由 Agent 自行完成,不交给人(均幂等,可重复执行):`scratch/iterm-probe-venv` 缺失时按 README.zh-CN.md「快速上手」重建;`scratch/iterm-probe-venv/bin/python -m pip install -r requirements-dev.txt`;`python3 harness/git_guard.py install`;最后 `bin/verify`,它会检查前两步。 +- 统一验证:`bin/verify`(lint、仓库卫生、Python 测试、macOS 上的 Swift 测试,与 CI 同口径)。修复提交带 `Defect: <编号>` trailer,通过与否只认 verify 与 CI 输出,不手写。约定与护栏见 docs/specs/delivery-harness.md;harness 引擎是独立项目 delivery-harness(装在 `.harness/engine/`,只能经 `upgrade` 整体替换),愿景与组件对照见其 README。 +- 新环境准备由 Agent 自行完成,不交给人(均幂等,可重复执行):`scratch/iterm-probe-venv` 缺失时按 README.zh-CN.md「快速上手」重建;`scratch/iterm-probe-venv/bin/python -m pip install -r requirements-dev.txt`;`bin/harness guard-git install`;最后 `bin/verify`,它会检查前两步。 - Agent 工具的项目守卫需要人确认一次信任,Agent 不得代为授权(自授信任等于执行者给自己放行):Agent 不得替执行方信任槽位或仓库里执行方能改的项目文件。`bin/dispatch` 派发 Pi 时用 `-na` 忽略槽位中的全部项目文件、`-e` 显式加载从 origin/main 导出的守卫,守卫来自经用户批准合并的 harness,不需要也不构成对槽位的信任(用户 2026-09-28 确认)。开工时自查,未信任就把命令交给用户:Zcode 用 `zcode hooks trust status --workspace <仓库根>` 查看,按其提示由用户 `grant`;Pi 由用户在仓库根启动 `pi` 执行 `/trust`;Claude Code 首次启动时由用户确认项目钩子;OpenCode 无需确认。未信任时 Agent 层守卫不生效,只剩 git 与服务端两层。 - 派发与评审:执行方任务先写任务书(`docs/templates/task.md`,合并后)再用 `bin/dispatch run <任务书>` 派发,不在主目录运行执行方;设计方自己实现的工作也写任务书,与实现同一个 PR。R2 以上 PR 的独立评审用 `bin/dispatch review`(本机常驻 `--watch`)。现状与下一阶段见 docs/review/2026-09-29-harness-status.md。 - Python 检查:`python3 -W error::ResourceWarning -m unittest discover -s tests -v`(ResourceWarning 按错误对待,与 CI 同口径)。 diff --git a/README.md b/README.md index 88595a0..c90064e 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ python3 -W error::ResourceWarning -m unittest discover -s tests -v ## Documentation -The complete documentation (specifications, design plans, dated research records, and reviews) is available in Chinese under [`docs/`](docs/). Start with [the unified inbox specification](docs/specs/unified-inbox.md); the latest cross-project health review is the [2026-09-21 project assessment](docs/review/2026-09-21-project-assessment.md). For the verifiable-delivery harness, start with [harness/README.md](harness/README.md) (vision, sources, and how each design principle maps to a component). The underlying theory is the research report [Low-intervention, continuous, high-quality delivery with AI agents](docs/research/2026-09-23-agent-delivery-theory.md); the plan and decision log are in [docs/plans/verifiable-delivery.md](docs/plans/verifiable-delivery.md), the baseline in the [2026-09-25 harness baseline review](docs/review/2026-09-25-harness-baseline.md), and open items in the [backlog](docs/plans/backlog.md). +The complete documentation (specifications, design plans, dated research records, and reviews) is available in Chinese under [`docs/`](docs/). Start with [the unified inbox specification](docs/specs/unified-inbox.md); the latest cross-project health review is the [2026-09-21 project assessment](docs/review/2026-09-21-project-assessment.md). For the verifiable-delivery harness, start with [delivery-harness](https://github.com/SnowsonZ/delivery-harness/blob/main/README.zh-CN.md) (vision, sources, and how each design principle maps to a component). The underlying theory is the research report [Low-intervention, continuous, high-quality delivery with AI agents](docs/research/2026-09-23-agent-delivery-theory.md); the plan and decision log are in [docs/plans/verifiable-delivery.md](docs/plans/verifiable-delivery.md), the baseline in the [2026-09-25 harness baseline review](docs/review/2026-09-25-harness-baseline.md), and open items in the [backlog](docs/plans/backlog.md). ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index d99c029..3324989 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -129,15 +129,16 @@ python3 -W error::ResourceWarning -m unittest discover -s tests -v | [系统 Terminal 备选](docs/plans/terminal-migration.md) | 设计方案 | 无额外依赖的备用终端探针(暂缓) | | [用量金额与桌面组件执行计划](docs/plans/usage-cost-widgets-execution.md) | 设计方案 | 工作包、里程碑评审关口与交付要求 | | [可验证交付 harness](docs/specs/delivery-harness.md) | 规范 | bin/verify、任务书准入、风险等级与合并路由、派发(bin/dispatch)与运行记录、独立评审、周报、三层护栏、停机与恢复、一次性设置与验证状态 | -| [任务模板](docs/templates/task.md) | 模板 | 任务书即合同:YAML 头部(类别、风险、设计方、预算)、终态、非目标、挂规格编号的验收、步骤与提交顺序、升级包;`harness/taskbook.py` 准入。[计划模板](docs/templates/archive/plan.md)已归档(并入任务书) | -| [评审清单](docs/templates/review-checklist.md)、[评审提示词](docs/templates/review-prompt.md) | 模板 | 由失败分类生成的评审清单与评审方工作方式,配合 `harness/review_pack.py` 证据包 | +| [任务模板](docs/templates/task.md) | 模板 | 任务书即合同:YAML 头部(类别、风险、设计方、预算)、终态、非目标、挂规格编号的验收、步骤与提交顺序、升级包;`.harness/engine/checks/taskbook.py` 准入。[计划模板](docs/templates/archive/plan.md)已归档(并入任务书) | +| [评审清单](docs/templates/review-checklist.md)、[评审提示词](docs/templates/review-prompt.md) | 模板 | 由失败分类生成的评审清单与评审方工作方式,配合 `.harness/engine/agents/review_pack.py` 证据包 | | [试跑任务 001:补 DR14 测试](docs/plans/trial-001-dr14.md) | 设计方案 | 可验证交付首个真实任务试跑:任务说明、预算与试跑记录表 | | [任务 002:补齐测试缺口](docs/plans/task-002-test-gaps.md) | 设计方案 | 变异测试暴露的 9 处缺口与 V080-R7 回归测试(已完成,PR #30) | | [任务 003:今日用量传入写入器](docs/plans/task-003-r9-today-usage.md) | 设计方案 | V080-R9:去掉默认参数、抽出用量键与查找的纯函数,交执行方 | | [任务 004:今日 token 从未显示](docs/plans/task-004-today-tokens.md) | 设计方案 | H0926-7 会话 ID 解码失败、H0926-8 签名不含用量,交执行方 | | [任务 005:金额热力分级边界测试](docs/plans/task-005-cost-level-boundaries.md) | 设计方案 | K2:杀死 `cost_level` 的两处存活变异;`bin/dispatch` 端到端验收(待办 B21) | | [待办清单](docs/plans/backlog.md) | 设计方案 | 未关闭事项的唯一清单(有日期或触发条件、可做未排期、持续或暂缓、用户本机环境) | -| [harness 说明](harness/README.md) | 设计方案 | 可验证交付的愿景、理念出处、原则与组件对照、阅读顺序 | +| [delivery-harness 引擎](https://github.com/SnowsonZ/delivery-harness/blob/main/README.zh-CN.md) | 外部项目 | 从本仓库抽出的可验证交付引擎:愿景、理念出处、原则与组件对照;本仓库以内置副本装在 `.harness/engine/` | +| [0001 harness 抽成独立引擎](docs/decisions/0001-harness-extraction.md) | 决策记录 | 以带锁文件的内置副本安装、引擎不带使用者默认值、放弃 pip/submodule/可复用 workflow 的理由 | | [2026-09-25 落地交接说明](docs/plans/2026-09-25-harness-rollout-handoff.md) | 设计方案(历史) | harness 开工时的基线、失败分类、可验证性地图与 P0–P6 路线,原为调研报告最后一节 | | [可验证交付方案](docs/plans/verifiable-delivery.md) | 设计方案 | 以本项目为标杆的全链路 harness:可验证定义、角色、风险等级、护栏分层与 P0–P6 阶段 | | [日报与桌面组件重设计](docs/plans/2026-09-24-report-widget-redesign.md) | 设计决策记录 | 口径决定(金额伴随/USD-only/官方图标)、实施与已知边界 | diff --git a/docs/decisions/0001-harness-extraction.md b/docs/decisions/0001-harness-extraction.md new file mode 100644 index 0000000..d902c58 --- /dev/null +++ b/docs/decisions/0001-harness-extraction.md @@ -0,0 +1,31 @@ +# 0001 harness 抽成独立引擎,以带锁文件的内置副本安装 + +状态:已采纳(2026-09-29,用户决定)。 + +## 背景 + +harness 在本仓库建成并验证(阶段一、二),用户要开发第二个项目,并希望这一层面向开源、可装到任意项目。原先的 `harness/` 目录把通用机制与本项目数据(规则、基线、回放用例、Swift 检查、账号与模型)混在一起。 + +## 决定 + +1. 引擎抽到独立公开仓库 [delivery-harness](https://github.com/SnowsonZ/delivery-harness),用 `git subtree split` 保留历史(推送前逐个提交扫描凭据与本机路径,0 命中)。 +2. **安装形态为带锁文件的内置副本**:`.harness/engine/` + `.harness/engine.lock`(版本、引擎提交、目录树哈希),`verify` 的 integrity 检查拒绝绕过 `upgrade` 的改动,`.harness/**` 判 R3。 +3. 引擎不带任何使用者自己的默认值;本项目的值写在 `.harness/config/`。 +4. 本仓库先迁(T008),行为不变作为验收;第二个项目等引擎 v0.2(带可观测性与 `adopt`)再装。 + +## 理由与放弃的方案 + +守卫的可信来源是「经用户批准合并到 origin/main 的代码」:git 守卫读 origin/main 的规则,派发从 origin/main 导出守卫给执行方,自动合并执行 main 上的定义。内置副本与此完全同构。 + +| 方案 | 放弃原因 | +|---|---| +| pip/uv 装进本机环境 | 守卫在执行方可写的环境里,且不在 origin/main 上,信任链断 | +| git submodule | `git archive` 不含子模块,派发槽位还要逐个初始化;指针变更的审查不直观 | +| 可复用 workflow(`uses: …@v1`) | CI 跑引擎仓库的代码、本机跑另一份,形成两个来源 | + +代价:每个业务仓库多约 5,500 行不需自己维护的代码;lint、质量棘轮、变异测试不覆盖该目录(由引擎仓库负责)。 + +## 影响 + +- 升级引擎:在 delivery-harness 检出中运行 `python3 engine/cli.py upgrade --target <本仓库>`,经 PR 由用户批准。 +- 迁移过渡期内,git 守卫与派发在 origin/main 仍为旧布局时读 `harness/`,全部使用方迁移后删除(待办 B43)。 diff --git a/docs/plans/2026-09-25-harness-rollout-handoff.md b/docs/plans/2026-09-25-harness-rollout-handoff.md index f515cf0..037ac7a 100644 --- a/docs/plans/2026-09-25-harness-rollout-handoff.md +++ b/docs/plans/2026-09-25-harness-rollout-handoff.md @@ -1,6 +1,6 @@ # 落地交接说明:Agent-Notification 标杆项目(2026-09-25) -> 历史文档。原为调研报告《低人工干预下 AI Agent 持续高质量交付:理论与全链路最佳实践》([入库快照](../research/2026-09-23-agent-delivery-theory.md))的最后一节,2026-09-26 拆出单独保存,报告原文已删去该节。它记录 2026-09-25 开工时的基线、失败分类、可验证性地图与 P0–P6 路线。现行做法见 [可验证交付方案](verifiable-delivery.md) 与 [harness 说明](../../harness/README.md),未关闭事项见 [待办清单](backlog.md)。 +> 历史文档。原为调研报告《低人工干预下 AI Agent 持续高质量交付:理论与全链路最佳实践》([入库快照](../research/2026-09-23-agent-delivery-theory.md))的最后一节,2026-09-26 拆出单独保存,报告原文已删去该节。它记录 2026-09-25 开工时的基线、失败分类、可验证性地图与 P0–P6 路线。现行做法见 [可验证交付方案](verifiable-delivery.md) 与 [harness 说明](https://github.com/SnowsonZ/delivery-harness/blob/main/README.zh-CN.md),未关闭事项见 [待办清单](backlog.md)。 **当前状态(2026-09-26)**:P0–P6 已完成并合并;已有四次执行方试跑(trial-001、任务 002–004)。下方计划保留作历史记录;现役规则见仓库 `docs/specs/delivery-harness.md`,未关闭事项以 `docs/plans/backlog.md`(待办清单)为准。 diff --git a/docs/plans/backlog.md b/docs/plans/backlog.md index 9f96664..79df41e 100644 --- a/docs/plans/backlog.md +++ b/docs/plans/backlog.md @@ -1,6 +1,6 @@ # 待办清单 -更新:2026-09-28。这里是本项目未关闭事项的唯一清单;[可验证交付方案](verifiable-delivery.md) §13 保留已完成事项的历史,未完成项指向这里。 +更新:2026-09-29。这里是本项目未关闭事项的唯一清单;[可验证交付方案](verifiable-delivery.md) §13 保留已完成事项的历史,未完成项指向这里。 ## 使用规则 @@ -41,6 +41,12 @@ | B39 | 月度熵治理:按周报的复杂度热点生成 K3 行为不变重构任务并派发 | 小;随真实使用 | 评审方 | 设计 11.5 | | B40 | 本机完整事件流(git 公共目录下 `dispatch/runs/`)保留 30 天后清理 | 小;R3 | 评审方 | 设计 10.1 | | B41 | 用本 App 采集的执行方用量核对运行记录的 token 与费用,偏差 10% 以内即采用(可选) | 中 | 评审方 | 设计 10.1 | +| B42 | harness 测试(`tests/test_harness*.py`)移植到引擎仓库:改为基于夹具项目,引擎仓库单独即可验证;本仓库只留消费方契约测试 | 中;引擎仓库 | 评审方 | T008(2026-09-29) | +| B43 | 删除迁移过渡回退:git 守卫与派发读旧布局 `harness/`、评审材料的旧入口;全部使用方迁到新布局后随引擎 v0.1.1 | 小;引擎仓库,本仓库随升级 R3 | 评审方 | T008 | +| B44 | 引擎仓库 CI 加消费方契约测试:检出本仓库 main,用待测引擎 upgrade 后跑其 harness 测试与 `verify --quick` | 小;T008 合并后 | 评审方 | T008 | +| B45 | 第二个项目接入前:`adopt`(探测语言、源码目录、测试与 lint 命令,渐进档位,已有配置幂等合并,AGENTS.md 受管块)与 TypeScript 语言插件 | 大;引擎仓库 | 评审方 | 用户 2026-09-29(面向开源、接入无感) | +| B46 | 可观测性(引擎 v0.2):统一事件格式与贯穿全程的追踪 ID、各环节埋点、`bin/harness trace`、即时告警;设计先交用户审,收掉 B36、B38、B40 | 大;引擎仓库 | 评审方设计,用户审 | 用户 2026-09-29 | +| B47 | 引擎开源化的其余项:界面与提示词国际化(en、zh-CN)、可配置的目录约定与默认分支、执行方宿主适配器(Pi 之外)、`platform` 一键平台设置与 App manifest | 中;引擎仓库 | 评审方 | 用户 2026-09-29 | ## 3. 持续进行或暂缓 diff --git a/docs/plans/task-008-harness-extract.md b/docs/plans/task-008-harness-extract.md new file mode 100644 index 0000000..3d86046 --- /dev/null +++ b/docs/plans/task-008-harness-extract.md @@ -0,0 +1,58 @@ +--- +task: T008 +class: K7 +risk: R3 +designer: claude-code +size: large +architecture: true +spec_refs: [] +no_spec_reason: 护栏与流程改动(harness 迁到独立引擎),验收写在 docs/specs/delivery-harness.md 与本任务书中,没有产品规格验收编号 +budget: + wall_clock_min: 600 + ci_rounds: 3 + retries: 2 + tokens: null +rollback: git revert(回到 harness/ 平铺布局;引擎仓库不受影响) +--- + +# 任务:harness 迁到独立引擎 delivery-harness(v0.1.0) + +状态:设计评审方自行实现(2026-09-29,方案 B)。来源:用户 2026-09-29 决定把 harness 抽成独立开源仓库 [delivery-harness](https://github.com/SnowsonZ/delivery-harness),先让本仓库作为第一个使用方迁过去,再装到第二个项目;安装形态为带锁文件的内置副本,引擎不带任何使用者自己的默认值。 + +## 目标终态 + +- `harness/` 删除;引擎以内置副本装在 `.harness/engine/`,`.harness/engine.lock` 记录版本、引擎提交与目录树哈希,`bin/verify` 的 integrity 检查拒绝绕过升级的改动。 +- 本项目的规则与配置在 `.harness/config/`(`rules.toml`、`autonomy.toml`、新增 `checks.toml`:Agent 身份、运行时、源码与语言、verify 检查清单、发版版本文件、变异目标、回放套件、周报机器人),棘轮状态在 `.harness/state/`,事故回放用例在 `.harness/project/replay_cases.py`。 +- 所有入口经 `bin/harness <子命令>`:`bin/verify`、`bin/dispatch`、`bin/as-agent`、git 钩子、5 个 Agent 工具的守卫钩子与 CI 工作流都改为调用 `.harness/engine/cli.py`。 +- 行为不变:harness 测试数、质量指标、变异得分与迁移前完全相同。 + +## 非目标与禁止动作 + +- 不改判定逻辑与阈值;不放宽任何规则。 +- 不在本任务中做 `adopt`(探测接入)、可观测性、国际化、可配置目录约定、TypeScript 插件(登记为待办)。 +- harness 测试(`tests/test_harness*.py`)暂留本仓库,作为引擎在真实项目上的契约测试;移植到引擎仓库另行处理。 + +## 前置条件(不满足就停下报告) + +- 引擎仓库 `restructure/v0.1` 分支的提交已推送,锁文件中的提交能在引擎仓库找到。 + +## 验收 + +| 编号 | 验收内容 | 证据类型 | 覆盖(测试名或验证步骤) | +|---|---|---|---| +| 不挂规格:护栏 | harness 测试在新布局下全部通过,数量与迁移前相同(248 个,跳过 1 个) | 单测 | `tests.test_harness` | +| 不挂规格:护栏 | 守卫从 origin/main 的新布局读规则、导出守卫;main 仍为旧布局时回退 | 单测 | `tests.test_harness_guard.TamperedRulesTest`、`tests.test_harness_dispatch.GuardBundleTest` | +| 不挂规格:护栏 | 质量指标与变异得分与迁移前相同 | CI 断言 | `bin/harness quality`、`bin/harness mutate --check` | +| 不挂规格:护栏 | 引擎完整性:改动 `.harness/engine/` 任一文件后 integrity 失败 | 人工 | `bin/harness integrity` | + +## 步骤与提交顺序 + +| # | 改动 | 涉及文件 | 验证方式 | 对应验收 | +|---|---|---|---|---| +| 1 | 装入引擎与配置,删除 `harness/` | `.harness/engine.lock`、`.harness/config/checks.toml`、`bin/harness` | `bin/verify --full` | 第 1、3、4 行 | +| 2 | 入口、钩子与工作流改调新入口 | `bin/verify`、`bin/dispatch`、`bin/as-agent`、`.githooks/pre-commit`、`.claude/settings.json`、`.github/workflows/build.yml` | `bin/verify` | 第 2 行 | +| 3 | 测试与现役文档改到新布局 | `tests/test_harness.py`、`docs/specs/delivery-harness.md`、`AGENTS.md`、`docs/plans/backlog.md` | `bin/verify` | 第 1 行 | + +## 升级包 + +需要改合同或遇到行为差异时停下,写明差异与证据交用户决定。 diff --git a/docs/plans/verifiable-delivery.md b/docs/plans/verifiable-delivery.md index 5dc38c8..5578f12 100644 --- a/docs/plans/verifiable-delivery.md +++ b/docs/plans/verifiable-delivery.md @@ -2,7 +2,7 @@ 状态:**历史文档**(2026-09-29):已由[目标态设计](https://claude.ai/code/artifact/a7646759-f838-49a8-aa1a-175b329ea1ed)取代,当前进展见 [2026-09-29 现状复核](../review/2026-09-29-harness-status.md),未关闭事项只在[待办清单](backlog.md)。§10 决定记录与 §13 已完成事项保留作追溯。以下为原状态:**P0–P5 完成,P6 的工具与试跑任务就绪,真实试跑待执行方执行**(2026-09-25)。基线评审见 [2026-09-25 可验证交付基线](../review/2026-09-25-harness-baseline.md),现役规则与命令见 [可验证交付 harness](../specs/delivery-harness.md)。 -理论来源:用户的调研报告《低人工干预下 AI Agent 持续高质量交付:理论与全链路最佳实践》(2026-09-23,Claude 文档;入库快照见 [docs/research/2026-09-23-agent-delivery-theory.md](../research/2026-09-23-agent-delivery-theory.md),组件对照见 [harness/README.md](../../harness/README.md))。本文只保留落到本仓库的部分;报告中面向大团队的做法按单人项目裁剪。 +理论来源:用户的调研报告《低人工干预下 AI Agent 持续高质量交付:理论与全链路最佳实践》(2026-09-23,Claude 文档;入库快照见 [docs/research/2026-09-23-agent-delivery-theory.md](../research/2026-09-23-agent-delivery-theory.md),组件对照见 [harness/README.md](https://github.com/SnowsonZ/delivery-harness/blob/main/README.zh-CN.md))。本文只保留落到本仓库的部分;报告中面向大团队的做法按单人项目裁剪。 ## 1. 目标 diff --git a/docs/research/2026-09-23-agent-delivery-theory.md b/docs/research/2026-09-23-agent-delivery-theory.md index b503314..aa3c32c 100644 --- a/docs/research/2026-09-23-agent-delivery-theory.md +++ b/docs/research/2026-09-23-agent-delivery-theory.md @@ -2,7 +2,7 @@ Sep 23, 2026 · @SnowsonZ -> 本文件是调研报告的入库快照,供本地阅读和引用。编辑源是 Claude 文档 (导出于 2026-09-26,文档版本 rev 47;原文与本快照均已删去曾附在末尾的「落地交接说明」,该节是本项目的落地计划,另存于 [docs/plans/2026-09-25-harness-rollout-handoff.md](../plans/2026-09-25-harness-rollout-handoff.md),与 rev 48 内容一致)。报告有实质更新时,由评审方重新导出、覆盖本文件,并更新这里的日期与版本。落到本仓库的做法见 [harness/README.md](../../harness/README.md) 与 [可验证交付方案](../plans/verifiable-delivery.md)。 +> 本文件是调研报告的入库快照,供本地阅读和引用。编辑源是 Claude 文档 (导出于 2026-09-26,文档版本 rev 47;原文与本快照均已删去曾附在末尾的「落地交接说明」,该节是本项目的落地计划,另存于 [docs/plans/2026-09-25-harness-rollout-handoff.md](../plans/2026-09-25-harness-rollout-handoff.md),与 rev 48 内容一致)。报告有实质更新时,由评审方重新导出、覆盖本文件,并更新这里的日期与版本。落到本仓库的做法见 [harness/README.md](https://github.com/SnowsonZ/delivery-harness/blob/main/README.zh-CN.md) 与 [可验证交付方案](../plans/verifiable-delivery.md)。 ## 核心结论 diff --git a/docs/review/2026-09-27-harness-audit.md b/docs/review/2026-09-27-harness-audit.md index eccf754..4c464bf 100644 --- a/docs/review/2026-09-27-harness-audit.md +++ b/docs/review/2026-09-27-harness-audit.md @@ -40,7 +40,7 @@ | 节点 | 当前实现 | 差距 | 可实现性 | |---|---|---|---| -| 5.1 仓库即知识库 | AGENTS.md 51 行只做地图;规格、方案、调研、评审、决定分目录;[待办清单](../plans/backlog.md)为唯一未关闭清单;[harness 说明](../../harness/README.md)给出理念与组件对照 | 无 | — | +| 5.1 仓库即知识库 | AGENTS.md 51 行只做地图;规格、方案、调研、评审、决定分目录;[待办清单](../plans/backlog.md)为唯一未关闭清单;[harness 说明](https://github.com/SnowsonZ/delivery-harness/blob/main/README.zh-CN.md)给出理念与组件对照 | 无 | — | | 5.2 系统对 Agent 可读 | 一条命令验证(`bin/verify`),输出只打结论,细节写日志 | 无 | — | | 5.3 上下文预算 | 状态外置到文件与 git,中断可从 AGENTS.md 与待办清单恢复 | 无机制检查过时文档:Codex 发现方案开头仍写「P6 待试跑」;H0925-4、H0926-1 的状态曾过时到 2026-09-26 才改 | **G8**:链接与状态新鲜度可做轻量检查(小);内容正确性靠收尾核对 | | 5.4 工具少而准 | 统一 CLI、verify、as-agent;守卫按命令结构判断 | 守卫把只读的 `gh release list` 也拒绝(对 `gh release` 一律拦) | 小修(G11) | diff --git a/docs/specs/delivery-harness.md b/docs/specs/delivery-harness.md index 397c54d..7c4cd93 100644 --- a/docs/specs/delivery-harness.md +++ b/docs/specs/delivery-harness.md @@ -2,7 +2,7 @@ 状态:现役(2026-09-25 起)。目标态与分阶段路线见[目标态设计](https://claude.ai/code/artifact/a7646759-f838-49a8-aa1a-175b329ea1ed),当前进展与下一阶段见 [2026-09-29 现状复核](../review/2026-09-29-harness-status.md);基线见 [2026-09-25 基线评审](../review/2026-09-25-harness-baseline.md)。早期的[可验证交付方案](../plans/verifiable-delivery.md)为历史文档,只保留 §10 决定记录与 §13 已完成事项。 -harness 只依赖 Python 标准库,放在仓库顶层 `harness/`,不进发布包(构建只拷 `scripts/` 与 `bin/session-manager`)。改动 `harness/`、`.githooks/`、`.github/`、`.claude/`、`.opencode/`、`.pi/`、`.zcode/` 属于 R3,必须由用户批准。 +harness 引擎是独立的开源项目 [delivery-harness](https://github.com/SnowsonZ/delivery-harness)(2026-09-29 从本仓库抽出,保留历史),以带锁文件的内置副本装在 `.harness/engine/`(版本、引擎提交与目录树哈希见 `.harness/engine.lock`,`bin/verify` 的 integrity 检查拒绝任何绕过升级的改动),只依赖 Python 标准库,不进发布包(构建只拷 `scripts/` 与 `bin/session-manager`)。本项目自己的规则与配置在 `.harness/config/`(`rules.toml`、`autonomy.toml`、`checks.toml`),棘轮状态在 `.harness/state/`,事故回放用例在 `.harness/project/replay_cases.py`。所有命令经 `bin/harness <子命令>`(`bin/verify`、`bin/dispatch` 为快捷入口)。升级引擎:在 delivery-harness 的检出中运行 `python3 engine/cli.py upgrade --target <本仓库>`,经 PR 合并。改动 `.harness/`、`bin/`、`.githooks/`、`.github/`、`.claude/`、`.opencode/`、`.pi/`、`.zcode/` 属于 R3,必须由用户批准。 ## 1. 命令 @@ -12,29 +12,29 @@ harness 只依赖 Python 标准库,放在仓库顶层 `harness/`,不进发 | `bin/verify --quick` | 工具版本、lint、仓库卫生、验收映射、任务书准入 | pre-commit 自动运行 | | `bin/verify --full` | 默认档 + 事故回放 | macOS CI(`--strict --full`);改动测试或产品逻辑后 | | `bin/verify --strict` | 被跳过的检查算失败 | macOS CI | -| `python3 harness/acceptance.py [--manual]` | 规格验收编号 ↔ 测试映射检查;列出人工验收清单 | verify 各档;写 PR 的人工验收部分时 | -| `python3 harness/taskbook.py [任务书 --on-main]` | 任务书准入:YAML 头部(类别、风险、设计方、规模、架构级、规格编号、预算、回滚)、类别与风险相容、验收每行挂规格编号、中大任务的必备章节、按「步骤与提交顺序」所列文件交叉核对;`--on-main` 另要求任务书已合并到 main(派发前) | verify 各档;派发前 | -| `python3 harness/review_pack.py --base origin/main` | 评审证据包:风险等级、修复证据、verify、涉及的验收编号 | 评审方评审前 | -| `python3 harness/quality.py [--update]` | 熵治理棘轮:Python 复杂度超标函数数、超长文件数只降不升;Swift(`native/`,轻量解析)超过 500 行的文件数、函数体超过 60 行的函数数、大括号嵌套超过 6 层的函数数只降不升 | verify 默认档;每周 quality workflow | -| `python3 harness/docs_check.py` | 文档熵治理:已跟踪的 README*.md、AGENTS.md、docs/、harness/README.md 中相对链接指向的文件必须存在(外链、纯锚点不查,`文件#锚点` 只查文件),断链即失败;「状态:」写着待执行、进行中、草案等且超过 30 天未提交的文档只报告 | verify 默认档与完整档 | -| `python3 harness/metrics.py --base origin/main [--github]` | 交付度量:修复数、声称已修、修复带回放比例、新增测试、CI 轮次,并列 v0.8.0 基线 | CI harness job;试跑记录 | -| `python3 harness/replay.py [--list]` | 把历史缺陷注入工作区副本,对应测试必须失败;列出基线覆盖 | `verify --full` | -| `python3 harness/mutate.py [--check / --update]` | 定向变异测试,得分与 `harness/mutation-baseline.json` 比较(只升不降) | 每周 quality workflow;补测试后 | -| `python3 harness/evidence.py --base origin/main [--swift]` | 按 `Defect:` trailer 生成修复证据,验证修复前测试失败、修复后通过;`--swift` 同时编译运行引用编号的 Swift 测试(macOS,编译失败算出错);回放覆盖:非 doc 类、未撤销的编号在 head 的 `replay_cases.py` 中须有注入用例、守卫测试或写明原因的暂缓项,否则失败 | CI harness job(Python)与 macOS build job(含 Swift);实现方自查 | -| `python3 harness/risk.py --base origin/main` | 按改动路径判定 R0–R3;声明 R1 时另跑 `r1_checks.py` 的加强判定 | CI harness job | -| `python3 harness/policy.py --base origin/main --head --pr <编号> --branch <分支>` | 合并路由:风险、类别(`autonomy.toml` 中为 L4,任务书声明与机器判定一致)、误差预算(escape 议题、`budget-exceeded` 标签)、规模(400 行)、任务 PR 的运行记录与 CI 轮次,逐条写理由;K3 按 PR 编号哈希三抽一 | auto-merge 的判定步骤(main 上的代码) | -| `python3 harness/run_check.py --base origin/main [--branch <分支>]` | 实现任务书的 PR(分支为 `task/<名字>` 且有对应任务书,或提交带 `Task:`):每个提交带 `Task: <编号>`;有格式完整、与任务书一致、exit 为 ok 的运行记录,提示词 sha256 一致;已完成的 build 轮次不超过 `budget.ci_rounds` | CI harness job(只报告);合并路由第 5 条(权威) | -| `python3 harness/weekly.py [--publish]` | 周报:14.1 全部指标(样本不足时只报计数或标「样本不足」)、前 4 周对比的突增标红、各类误差预算与 K3 抽审进度、试跑记录自动汇总(含任务 002–004 的历史 PR,`docs/runs/history.json`);`--publish` 更新固定的「每周质量报告」议题并发当周评论 | 每周一 quality 工作流;需要时手动 | -| `python3 harness/mutate.py --check --changed-since ` | 只核对改到其文件的变异目标,得分不得低于基线 | CI harness job,声明 R1 时 | -| `python3 harness/base_tests.py --base origin/main` | 用 base 版本的已有测试在 head 上重跑:追加进已有测试文件的代码禁用不了判定器(有意改动已有测试的 PR 按 R2 评审,此项只报告);测试经 `harness/base_tests_runner.py` 运行,产品代码在运行中篡改 unittest 时一律失败 | CI harness job | -| `python3 harness/hygiene.py --staged / --range BASE` | 禁止路径、超大文件、凭据、新增行中的本机路径 | pre-commit、pre-push、CI | -| `python3 harness/git_guard.py install` | 把 `core.hooksPath` 指向 `.githooks`(幂等,不覆盖已有设置) | 每个新环境一次 | +| `bin/harness acceptance [--manual]` | 规格验收编号 ↔ 测试映射检查;列出人工验收清单 | verify 各档;写 PR 的人工验收部分时 | +| `bin/harness taskbook [任务书 --on-main]` | 任务书准入:YAML 头部(类别、风险、设计方、规模、架构级、规格编号、预算、回滚)、类别与风险相容、验收每行挂规格编号、中大任务的必备章节、按「步骤与提交顺序」所列文件交叉核对;`--on-main` 另要求任务书已合并到 main(派发前) | verify 各档;派发前 | +| `bin/harness review-pack --base origin/main` | 评审证据包:风险等级、修复证据、verify、涉及的验收编号 | 评审方评审前 | +| `bin/harness quality [--update]` | 熵治理棘轮:Python 复杂度超标函数数、超长文件数只降不升;Swift(`native/`,轻量解析)超过 500 行的文件数、函数体超过 60 行的函数数、大括号嵌套超过 6 层的函数数只降不升 | verify 默认档;每周 quality workflow | +| `bin/harness docs` | 文档熵治理:已跟踪的 README*.md、AGENTS.md、docs/、harness/README.md 中相对链接指向的文件必须存在(外链、纯锚点不查,`文件#锚点` 只查文件),断链即失败;「状态:」写着待执行、进行中、草案等且超过 30 天未提交的文档只报告 | verify 默认档与完整档 | +| `bin/harness metrics --base origin/main [--github]` | 交付度量:修复数、声称已修、修复带回放比例、新增测试、CI 轮次,并列 v0.8.0 基线 | CI harness job;试跑记录 | +| `bin/harness replay [--list]` | 把历史缺陷注入工作区副本,对应测试必须失败;列出基线覆盖 | `verify --full` | +| `bin/harness mutate [--check / --update]` | 定向变异测试,得分与 `.harness/state/mutation-baseline.json` 比较(只升不降) | 每周 quality workflow;补测试后 | +| `bin/harness evidence --base origin/main [--swift]` | 按 `Defect:` trailer 生成修复证据,验证修复前测试失败、修复后通过;`--swift` 同时编译运行引用编号的 Swift 测试(macOS,编译失败算出错);回放覆盖:非 doc 类、未撤销的编号在 head 的 `replay_cases.py` 中须有注入用例、守卫测试或写明原因的暂缓项,否则失败 | CI harness job(Python)与 macOS build job(含 Swift);实现方自查 | +| `bin/harness risk --base origin/main` | 按改动路径判定 R0–R3;声明 R1 时另跑 `r1_checks.py` 的加强判定 | CI harness job | +| `bin/harness policy --base origin/main --head --pr <编号> --branch <分支>` | 合并路由:风险、类别(`autonomy.toml` 中为 L4,任务书声明与机器判定一致)、误差预算(escape 议题、`budget-exceeded` 标签)、规模(400 行)、任务 PR 的运行记录与 CI 轮次,逐条写理由;K3 按 PR 编号哈希三抽一 | auto-merge 的判定步骤(main 上的代码) | +| `bin/harness run-check --base origin/main [--branch <分支>]` | 实现任务书的 PR(分支为 `task/<名字>` 且有对应任务书,或提交带 `Task:`):每个提交带 `Task: <编号>`;有格式完整、与任务书一致、exit 为 ok 的运行记录,提示词 sha256 一致;已完成的 build 轮次不超过 `budget.ci_rounds` | CI harness job(只报告);合并路由第 5 条(权威) | +| `bin/harness weekly [--publish]` | 周报:14.1 全部指标(样本不足时只报计数或标「样本不足」)、前 4 周对比的突增标红、各类误差预算与 K3 抽审进度、试跑记录自动汇总(含任务 002–004 的历史 PR,`docs/runs/history.json`);`--publish` 更新固定的「每周质量报告」议题并发当周评论 | 每周一 quality 工作流;需要时手动 | +| `bin/harness mutate --check --changed-since ` | 只核对改到其文件的变异目标,得分不得低于基线 | CI harness job,声明 R1 时 | +| `bin/harness base-tests --base origin/main` | 用 base 版本的已有测试在 head 上重跑:追加进已有测试文件的代码禁用不了判定器(有意改动已有测试的 PR 按 R2 评审,此项只报告);测试经 `.harness/engine/checks/base_tests_runner.py` 运行,产品代码在运行中篡改 unittest 时一律失败 | CI harness job | +| `bin/harness hygiene --staged / --range BASE` | 禁止路径、超大文件、凭据、新增行中的本机路径 | pre-commit、pre-push、CI | +| `bin/harness guard-git install` | 把 `core.hooksPath` 指向 `.githooks`(幂等,不覆盖已有设置) | 每个新环境一次 | | `bin/dispatch run <任务书> [--resume] [--model M]` | 派发:准入(含已在 origin/main)→ 认领(推送 `task/<编号>-<名字>`,远端已有即停)→ 取槽位 → 守卫预检 → Pi 执行 → 执行方之外跑 `bin/verify`(失败带摘要重试,同一失败签名连续两次即打转)→ 提交运行记录、推送、开 PR → 等 CI(失败带摘要重试,超 `ci_rounds` 打 `budget-exceeded`);不能完成时升级(PR 评论或议题,`escalation` 标签) | 设计评审方,任务书合并后 | | `bin/dispatch status` / `bin/dispatch stop --all` | 查看槽位;停机:终止所有正在运行的执行方(§8) | 设计评审方、用户 | | `bin/dispatch review [--reviewer opencode\|pi\|codex\|claude-code]` | 独立评审:在只读的评审工作区(`<仓库名>-review`)中由非设计方评审任务书、PR 描述、diff、证据包与 CI 结论,结论以固定格式评论到 PR,并去掉 `needs-independent-review` 标签 | 设计评审方;通常由后台评审自动运行 | | `bin/dispatch review --pending` / `--watch [--interval 5]` | 后台评审:逐个评审带 `needs-independent-review`、CI 已全部通过、当前 head 尚无独立评审结论的开着的 PR;`--watch` 每隔若干分钟一轮,停机标记存在即退出。不并行(多个 OpenCode 同时运行会冲突) | 本机常驻(终端中运行 `--watch`) | -| `python3 harness/review.py calibrate --reviewer <评审方> [--limit N]` | 评审校准:回放用例重新注入的已知问题与已知良好的 PR,统计 TPR、TNR,结果写入 `evals/review/results/` | 改评审提示词或换评审模型后 | -| `python3 harness/release_check.py --tag vX.Y.Z` | tag 与构建版本一致、构建号递增、tag 在 main 上 | 推 tag 时 CI 自动运行 | +| `bin/harness review calibrate --reviewer <评审方> [--limit N]` | 评审校准:回放用例重新注入的已知问题与已知良好的 PR,统计 TPR、TNR,结果写入 `evals/review/results/` | 改评审提示词或换评审模型后 | +| `bin/harness release-check --tag vX.Y.Z` | tag 与构建版本一致、构建号递增、tag 在 main 上 | 推 tag 时 CI 自动运行 | `verify` 终端只打印结论,完整输出在 `build/verify/<检查名>.log`,汇总在 `build/verify/summary.json`。判断通过只认退出码和 CI 上当前 head 的运行。 @@ -45,27 +45,27 @@ harness 只依赖 Python 标准库,放在仓库顶层 `harness/`,不进发 - **编号写错时撤销,不改写历史**:在 PR 的后续提交里加一行 `Defect-Withdrawn: <编号>`,evidence 不再核对该编号,证据摘要里标「已撤销」,评审可见。只能撤销本 PR 中更早声明过的编号;撤销后又重新声明的,仍按修复核对。 - **PR 正文与评论用文件传入**:`gh pr create --body-file`、`gh pr comment --body-file`,不在双引号里写 Markdown。双引号内的反引号是 shell 命令替换,会真的执行其中的命令(2026-09-26 评审方把 `` `gh pr merge` `` 写进 `--body "…"`,被命令守卫拦下)。 - **缺陷编号全局唯一**:`<来源>-<序号>`,例如 `V080-R17`(v0.8.0 交付评审第 17 项)、`REV0921-R2`(2026-09-21 评估第 2 项)、`H0925-4`(2026-09-25 harness 建设中的发现)。不再使用裸 `R17`。 -- **每个修复都要能被回放**(2026-09-28 起由 evidence.py 强制,审计 G2):PR 中每个非 doc 类、未撤销的 `Defect` 编号,head 上的 `harness/replay_cases.py` 必须有注入用例、守卫测试(`GUARDED`)或写明原因的暂缓项(`DEFERRED`),否则 harness job 与 macOS build job 失败;v0.8.0 基线之后的新缺陷一视同仁。回放用例是判定器:执行方在 PR 里写明注入点,由评审方在**同一个 PR** 里加入(H0926-2);该 PR 因此改到 `harness/`,按 R3 由用户批准。注入后是否真的失败由 `verify --full` 的回放检查核对。 -- **执行方可编辑的 harness 数据**:`harness/**` 对执行方整体禁改,唯一例外是 `harness/acceptance-gaps.txt`(补完测试后删行)。它只能缩减,新增条目 risk.py 判 R3。 +- **每个修复都要能被回放**(2026-09-28 起由 evidence.py 强制,审计 G2):PR 中每个非 doc 类、未撤销的 `Defect` 编号,head 上的 `.harness/project/replay_cases.py` 必须有注入用例、守卫测试(`GUARDED`)或写明原因的暂缓项(`DEFERRED`),否则 harness job 与 macOS build job 失败;v0.8.0 基线之后的新缺陷一视同仁。回放用例是判定器:执行方在 PR 里写明注入点,由评审方在**同一个 PR** 里加入(H0926-2);该 PR 因此改到 `harness/`,按 R3 由用户批准。注入后是否真的失败由 `verify --full` 的回放检查核对。 +- **执行方可编辑的 harness 数据**:`harness/**` 对执行方整体禁改,唯一例外是 `.harness/state/acceptance-gaps.txt`(补完测试后删行)。它只能缩减,新增条目 risk.py 判 R3。 - **已知缺陷登记**:发现但暂不修的缺陷写成确定性测试并标 `@unittest.expectedFailure`,说明里写编号与待决事项;修好后它会「意外通过」并报错,逼着移除登记。 -- **验收编号**:每份规格的验收表含「证据类型」「覆盖」两列,编号前缀按规格区分——W(桌面组件)、U(用量金额)、DR(工作日报)、IN(统一收件箱)、CB(CLI 会话绑定)、ZN(Zcode 导航)。可自动化条目必须有真实存在的测试;暂缺的登记在 `harness/acceptance-gaps.txt`(带原因,只能缩减)。 -- **任务书即合同**(2026-09-28 起):任务按 [task.md](../templates/task.md) 写,开头是 YAML 头部(`task`、`class`、`risk`、`designer`、`size`、`architecture`、`spec_refs` 或 `no_spec_reason`、`budget`、`rollback`),正文含终态、非目标、前置条件、验收、步骤与提交顺序、升级包;由 `harness/taskbook.py` 准入。计划并入任务书的「步骤与提交顺序」,不再单独交计划(计划模板已归档到 `docs/templates/archive/`)。验收表每行的编号是现役规格中的编号、`新增:<规格文件>#<编号>`(同一改动在规格里新增)或 `不挂规格:<原因>`,新需求先进规格再进任务书。执行方对任务书与规格只读(守卫拒绝),需要改合同时写升级包(含澄清)。历史任务 002–004 登记在 `harness/taskbook-exempt.txt`,只检查头部,清单只能缩减。 +- **验收编号**:每份规格的验收表含「证据类型」「覆盖」两列,编号前缀按规格区分——W(桌面组件)、U(用量金额)、DR(工作日报)、IN(统一收件箱)、CB(CLI 会话绑定)、ZN(Zcode 导航)。可自动化条目必须有真实存在的测试;暂缺的登记在 `.harness/state/acceptance-gaps.txt`(带原因,只能缩减)。 +- **任务书即合同**(2026-09-28 起):任务按 [task.md](../templates/task.md) 写,开头是 YAML 头部(`task`、`class`、`risk`、`designer`、`size`、`architecture`、`spec_refs` 或 `no_spec_reason`、`budget`、`rollback`),正文含终态、非目标、前置条件、验收、步骤与提交顺序、升级包;由 `.harness/engine/checks/taskbook.py` 准入。计划并入任务书的「步骤与提交顺序」,不再单独交计划(计划模板已归档到 `docs/templates/archive/`)。验收表每行的编号是现役规格中的编号、`新增:<规格文件>#<编号>`(同一改动在规格里新增)或 `不挂规格:<原因>`,新需求先进规格再进任务书。执行方对任务书与规格只读(守卫拒绝),需要改合同时写升级包(含澄清)。历史任务 002–004 登记在 `.harness/state/taskbook-exempt.txt`,只检查头部,清单只能缩减。 - **设计方自行实现也要有任务书**(用户 2026-09-28 决定,方案 B):设计评审方自己实现的工作(如护栏改动)同样先写任务书,与实现放在同一个 PR 中,照常过准入检查,提交带 `Task: <编号>`;独立评审以它为对照。这类 PR 不经派发脚本,不要求运行记录(`run_check.py` 按「任务书随本 PR 提交」识别),本就由用户审批。派给执行方的任务仍须先合并任务书再派发。 - **任务书的审查按类别**(2026-09-28 决定 1):K7 护栏与流程、K8 发版、`architecture: true`(跨两个以上模块、新增或修改公共接口如 CLI 输出与数据格式、数据迁移、新增依赖)的任务书由用户审(R2);其余类别(业务修改、缺陷修复等)的任务书通过准入检查即自动合并(R0)。模板与待办清单由用户审(R2)。类别写低不影响安全:实现 PR 仍按实际改动路径判级。 -- **独立评审**(试行,2026-09-28 决定 5;设计 8.3):R2 及以上的 PR 在 CI 通过后由 auto-merge 打上 `needs-independent-review`,本机的后台评审(`bin/dispatch review --watch`)在 CI 通过后自动评审,不占用用户等待时间;PR 有新推送时 auto-merge 在 build 通过后重新打标签,自动重评。评审方不能是设计方:有任务书时看头部 `designer`,没有时看提交的 `Co-Authored-By`。默认评审方由 `rules.toml [review] reviewer` 指定:用户 2026-09-28 定为 OpenCode(`zai-coding-plan/glm-5.3`,只读的 plan 代理,避开执行方 Pi);Pi、Codex(`gpt-6-sol`、推理强度 max)与 Claude Code 可用 `--reviewer` 选。评审方与该 PR 的执行方同一宿主时,评论里会标出独立性较弱。评审方只看任务书、PR 描述、diff 与 `review_pack.py` 证据包,不看执行过程;结构上只读(OpenCode 用 plan 代理、`--pure` 不加载插件,并以配置关掉 bash、子代理、网页与写入类工具:plan 代理本身只禁止编辑,2026-09-28 实测它曾在评审中运行 PR 的测试与 `gh`;Pi 只开放 read、grep、find、ls 且不加载项目文件,Codex `-s read-only`,Claude Code 只开放 Read、Grep、Glob),环境里没有 GitHub 凭据,结论由派发脚本评论到 PR。评审方本身失败(报错、超时、额度用尽)记为「评审失败」:不评论为结论、保留待评审标签,校准时不计入 TPR、TNR;校准逐个样本落盘,`--resume` 续跑,连续失败 3 次即停。结论只是给用户的输入,不替代用户审批。先试行 3 个 R2 PR,由用户决定是否常态化;30 个 R2 PR 中若没有发现用户未发现的问题,降为抽样。评审提示词 `harness/review_prompt.md` 与校准集 `evals/review/samples.json` 属 R3。人工评审仍可按 [review-prompt.md](../templates/review-prompt.md) 与 [review-checklist.md](../templates/review-checklist.md) 进行。 +- **独立评审**(试行,2026-09-28 决定 5;设计 8.3):R2 及以上的 PR 在 CI 通过后由 auto-merge 打上 `needs-independent-review`,本机的后台评审(`bin/dispatch review --watch`)在 CI 通过后自动评审,不占用用户等待时间;PR 有新推送时 auto-merge 在 build 通过后重新打标签,自动重评。评审方不能是设计方:有任务书时看头部 `designer`,没有时看提交的 `Co-Authored-By`。默认评审方由 `rules.toml [review] reviewer` 指定:用户 2026-09-28 定为 OpenCode(`zai-coding-plan/glm-5.3`,只读的 plan 代理,避开执行方 Pi);Pi、Codex(`gpt-6-sol`、推理强度 max)与 Claude Code 可用 `--reviewer` 选。评审方与该 PR 的执行方同一宿主时,评论里会标出独立性较弱。评审方只看任务书、PR 描述、diff 与 `review_pack.py` 证据包,不看执行过程;结构上只读(OpenCode 用 plan 代理、`--pure` 不加载插件,并以配置关掉 bash、子代理、网页与写入类工具:plan 代理本身只禁止编辑,2026-09-28 实测它曾在评审中运行 PR 的测试与 `gh`;Pi 只开放 read、grep、find、ls 且不加载项目文件,Codex `-s read-only`,Claude Code 只开放 Read、Grep、Glob),环境里没有 GitHub 凭据,结论由派发脚本评论到 PR。评审方本身失败(报错、超时、额度用尽)记为「评审失败」:不评论为结论、保留待评审标签,校准时不计入 TPR、TNR;校准逐个样本落盘,`--resume` 续跑,连续失败 3 次即停。结论只是给用户的输入,不替代用户审批。先试行 3 个 R2 PR,由用户决定是否常态化;30 个 R2 PR 中若没有发现用户未发现的问题,降为抽样。评审提示词 `.harness/engine/prompts/review_prompt.md` 与校准集 `evals/review/samples.json` 属 R3。人工评审仍可按 [review-prompt.md](../templates/review-prompt.md) 与 [review-checklist.md](../templates/review-checklist.md) 进行。 - **测试的几种形态**:种子固定的性质测试(`tests/test_properties.py`,`PROPTEST_SEEDS=N` 放大搜索)、架构适应度(`tests/test_architecture.py`,同一口径只实现一次)、CLI 黄金快照(`tests/test_golden.py`,有意改变时 `UPDATE_GOLDEN=1` 重新生成,按 R2 评审)。 -- **护栏规则先合并**:本机 git 守卫按 origin/main 上的 `harness/rules.toml` 执行(评审 PR7-R3),改规则的 PR 合并前,依赖新规则的文件在本机提交会被拒。放宽类规则(如新增 `[hygiene] allowed` 例外)与依赖它的文件分两个 PR:先合并规则,再提交文件。 +- **护栏规则先合并**:本机 git 守卫按 origin/main 上的 `.harness/config/rules.toml` 执行(评审 PR7-R3),改规则的 PR 合并前,依赖新规则的文件在本机提交会被拒。放宽类规则(如新增 `[hygiene] allowed` 例外)与依赖它的文件分两个 PR:先合并规则,再提交文件。 - **行为不变的重构**:每个提交带 `Risk: R1`;risk.py 核对只改产品代码、已有测试与黄金快照零改动,另由 `r1_checks.py` 核对被改函数签名不变(Python 按 AST,Swift 按声明文本;新增函数不限)、无新依赖(Python 只能新增标准库与仓库内模块,Swift 不新增 import)、无建表改表语句、增删不超过 400 行,任一不满足按 R2(2026-09-28 决定 2)。变异得分不降要执行代码,由 build 的 harness job 在声明 R1 时运行,下降即失败;想按 R2 走的重构加一个不带 `Risk: R1` 的提交即可。 - **逃逸与抽审登记**(2026-09-28 决定 9):合并后才发现、本应被门禁或评审拦住的缺陷,任何人(含 Agent)开议题,加 `escape` 与 `class:<类别>` 标签,正文写明「引入:#」;不写会话正文。已关闭的议题照样计数。K3 被抽中的 PR 合并后由 auto-merge 开 `audit` 议题,评审方审完关闭,发现问题另开 escape 议题。PR 的类别标签 `class:<类别>` 由 auto-merge 打上,是误差预算窗口的依据。逃逸用议题模板「逃逸登记」(`.github/ISSUE_TEMPLATE/escape.yml`)。升级(`escalation`)由处理人处理后加 `escalation:needed` 或 `escalation:unneeded`,周报据此算升级精度与处理时长。 - **每周错误分析**(设计 11.1):周一周报出来后,由不是本周主要设计方的评审方看本周全部异常(失败或超预算的运行、升级、CI 失败、守卫拒绝、评审发现、逃逸),逐条写问题、归入基线评审的失败分类,决定沉淀形式(能自动检查的写成守卫、lint 或测试);沉淀项进待办清单,结论存 `docs/review/weekly/<年-周>.md`(R0)。 - **不手写通过状态**:PR 与交付说明里的「测试通过」「CI 通过」「已修复」一律由 CI 的 harness job summary 与 run 链接代替。 - **派发**(2026-09-28 起,设计 6.2):执行方任务经 `bin/dispatch` 派发,不在主目录运行执行方;主目录只留给用户,设计评审方也在自己的 worktree 或槽位中工作。槽位是仓库同级的固定目录 `<仓库名>-slot-`(`rules.toml [dispatch]`,默认 3 个),每次派发从 origin/main 重建分支并清理未跟踪文件(保留 `scratch/iterm-probe-venv`,指向主目录的运行时)。执行方以 `Snowson` 的提交身份工作,但**拿不到任何 GitHub 凭据**:继承的令牌被去掉、gh 指向空配置目录、git 凭据助手清空;推送、开 PR、评论由派发脚本经 `bin/as-agent` 完成。预算取自任务书头部:`wall_clock_min` 是执行方的累计时长,`retries` 是本地重试,`ci_rounds` 是 CI 轮次;15 分钟既无输出也无文件变化判为卡死。执行方卡住时写 `build/dispatch/escalation.md`(可选方案、需要决定的问题),派发脚本补上状态、已尝试方案与证据后升级。 -- **运行记录**:每次推送带一份 `docs/runs/<任务书名>/<序号>.json`(R0)与渲染后的提示词 `<序号>.prompt.md`:宿主、版本、模型、提示词 sha256、守卫取自的 main 提交、时长与退出方式、重试与失败签名、守卫拒绝次数(按理由计)、token 与费用。只存结构化摘要;完整事件流留在本机 git 公共目录下的 `dispatch/runs/`,不入库。执行方不能编辑 `docs/runs/**`。提示词模板 `harness/dispatch_prompt.md` 属 R3。CI 侧由 `run_check.py` 复核(合并路由第 5 条):即使绕过派发脚本手工派发,缺记录、缺 `Task:` 或 CI 轮次超预算的 PR 也不会被自动合并;交付度量另在 job summary 中列出「CI 轮次 / 预算」。 +- **运行记录**:每次推送带一份 `docs/runs/<任务书名>/<序号>.json`(R0)与渲染后的提示词 `<序号>.prompt.md`:宿主、版本、模型、提示词 sha256、守卫取自的 main 提交、时长与退出方式、重试与失败签名、守卫拒绝次数(按理由计)、token 与费用。只存结构化摘要;完整事件流留在本机 git 公共目录下的 `dispatch/runs/`,不入库。执行方不能编辑 `docs/runs/**`。提示词模板 `.harness/engine/prompts/dispatch_prompt.md` 属 R3。CI 侧由 `run_check.py` 复核(合并路由第 5 条):即使绕过派发脚本手工派发,缺记录、缺 `Task:` 或 CI 轮次超预算的 PR 也不会被自动合并;交付度量另在 job summary 中列出「CI 轮次 / 预算」。 - **减少审批次数**(用户 2026-09-28 决定,先执行、阶段二结束后按数据复核):判级规则不放宽,靠攒批减少 PR 数。①记录类改动(规范 §6 验证状态、待办关闭与状态更新、实测结果)不单独开 PR,并入下一个实质性 PR;没有合适的 PR 时,当天的记录攒成一个。②相关的 R3 小改动按主题合成一个 PR(设计 16 章),不拆成零碎的护栏 PR。 ## 3. 风险等级 -| 等级 | 判定(`harness/rules.toml [risk]`) | 合并 | +| 等级 | 判定(`.harness/config/rules.toml [risk]`) | 合并 | |---|---|---| | R0 | 说明性文档;业务修改与缺陷修复等类别的任务书(通过准入);只新增测试 | 门禁全绿即可自动合并 | | R1 | 声明 `Risk: R1` 且机器核对通过 | 自动合并 + 抽样审计 | @@ -77,12 +77,12 @@ harness 只依赖 Python 标准库,放在仓库顶层 `harness/`,不进发 | 判定(按顺序) | 满足条件 | 不满足时 | |---|---|---| | 风险 | R0 或 R1 | 转用户评审 | -| 类别 | 机器判定的类别(K0 合同、K1 说明、K2 补测试、K3 重构、K4 修复、K5 功能、K6 界面、K7 护栏)在 `harness/autonomy.toml` 中为 L4;提交带 `Task: T<编号>` 时,main 上该任务书声明的类别与机器判定一致 | 转用户评审(声明不一致按「未分类」) | +| 类别 | 机器判定的类别(K0 合同、K1 说明、K2 补测试、K3 重构、K4 修复、K5 功能、K6 界面、K7 护栏)在 `.harness/config/autonomy.toml` 中为 L4;提交带 `Task: T<编号>` 时,main 上该任务书声明的类别与机器判定一致 | 转用户评审(声明不一致按「未分类」) | | 预算 | 该类最近 N 次合并的 escape 议题不超预算(K0–K2:20 次内 ≤ 1;K3:10 次内 = 0);PR 不带 `budget-exceeded` 标签;读不到标签或议题时按不满足 | 该类自动合并暂停,直到用户改 `autonomy.toml` 恢复 | | 规模 | 增删行数(不计 `tests/golden/**`、`docs/runs/**`)≤ 400 | 转用户评审,提示拆分 | | 运行记录 | 实现任务书的 PR:提交都带 `Task:`,有合格的运行记录(exit 为 ok),已完成的 build 轮次(按不同 head 提交计)≤ 任务书 `ci_rounds`;读不到 build 运行按不满足;不是在实现任务书的 PR 不要求 | 转用户评审:缺记录说明未经 `bin/dispatch` 或记录不全,超预算说明反复试错 | -放权只能由用户改 `harness/autonomy.toml`(R3),降级自动生效。K3 自动合并的 PR 按编号哈希每 3 个抽 1 个开 audit 议题,满 10 个抽审无问题后由用户调低比例。判定放在这里而不是 PR 自己的 CI 里,是因为 `pull_request` 事件执行的是 PR 分支里的 workflow 定义。 +放权只能由用户改 `.harness/config/autonomy.toml`(R3),降级自动生效。K3 自动合并的 PR 按编号哈希每 3 个抽 1 个开 audit 议题,满 10 个抽审无问题后由用户调低比例。判定放在这里而不是 PR 自己的 CI 里,是因为 `pull_request` 事件执行的是 PR 分支里的 workflow 定义。 ## 4. 三层护栏 @@ -90,7 +90,7 @@ harness 只依赖 Python 标准库,放在仓库顶层 `harness/`,不进发 |---|---|---| | 服务端 | `.github/rulesets/main.json`:main 禁删除与改写,必须经 PR,`build` 与 `harness` 检查必须通过,且 PR 须基于最新 main(严格模式);`release-tags.json`:`v*` tag 禁移动与删除;release job 走 environment `release`,由用户批准 | 本机 Agent 无法绕过 | | git | `.githooks/pre-commit`(保护分支上禁止提交、暂存区卫生、快速 verify);`pre-push`(禁推 main 与 tag、禁强制推送、本次推送的改动卫生、完整 verify);`reference-transaction`(禁本地改写或删除 main、移动或删除 tag,含 filter-repo) | 防误操作,不防有意绕过:`--no-verify`、改 `core.hooksPath`、改守卫代码或运行时解释器都能绕过(规则文件已改为读 origin/main);Agent 层拒绝其中能识别的命令 | -| Agent | `harness/command_guard.py`:拒绝改写历史、强推、推 main 与 tag、建删 tag、跳过钩子、设置覆盖变量、`reset --hard`、不带 venv 排除的 `git clean -x`、删除工作区外路径、`gh release`(只读的 `list`、`view` 放行)与删除 CI 记录、GitHub API 写请求(`gh api` 写方法与对 api.github.com 的 curl 写请求);删除议题,撤下或删改登记标签(`escape`、`audit`、`escalation`、`budget-exceeded`、`class:K*`,含 MCP 的删除议题与标签工具);引号或反斜杠拆写的命令另按去掉引号的形式再查一遍;Agent 自行合并 PR(`gh pr merge` 与 MCP 的合并、开启自动合并工具,用户决定 D4);`--role implementer` 另禁编辑判定器与护栏、合同(`docs/plans/task-*.md`、`docs/specs/**`)与运行记录(`docs/runs/**`),以及关闭、重开、改动议题和标签(`gh issue close/edit…`、`gh pr edit --add/remove-label`、`gh label`、MCP 的议题写工具)。设计评审方可以关闭审完的 audit 议题 | 取决于各家 hook 能力 | +| Agent | `.harness/engine/guards/command_guard.py`:拒绝改写历史、强推、推 main 与 tag、建删 tag、跳过钩子、设置覆盖变量、`reset --hard`、不带 venv 排除的 `git clean -x`、删除工作区外路径、`gh release`(只读的 `list`、`view` 放行)与删除 CI 记录、GitHub API 写请求(`gh api` 写方法与对 api.github.com 的 curl 写请求);删除议题,撤下或删改登记标签(`escape`、`audit`、`escalation`、`budget-exceeded`、`class:K*`,含 MCP 的删除议题与标签工具);引号或反斜杠拆写的命令另按去掉引号的形式再查一遍;Agent 自行合并 PR(`gh pr merge` 与 MCP 的合并、开启自动合并工具,用户决定 D4);`--role implementer` 另禁编辑判定器与护栏、合同(`docs/plans/task-*.md`、`docs/specs/**`)与运行记录(`docs/runs/**`),以及关闭、重开、改动议题和标签(`gh issue close/edit…`、`gh pr edit --add/remove-label`、`gh label`、MCP 的议题写工具)。设计评审方可以关闭审完的 audit 议题 | 取决于各家 hook 能力 | 覆盖变量只供人使用(Agent 层会拒绝设置它们的命令): @@ -114,9 +114,9 @@ Agent 层按角色接入: ## 5. 一次性设置(用户) 1. GitHub ruleset(先做:它是唯一不能被本机绕过的一层):仓库 Settings → Rules → Rulesets → New ruleset → Import a ruleset,依次导入 `.github/rulesets/main.json` 与 `.github/rulesets/release-tags.json`。 -2. 本机环境:由 Agent 按 AGENTS.md「新环境准备」自行完成(重建 venv、装开发依赖、`python3 harness/git_guard.py install`、`bin/verify`),不需要人执行。 +2. 本机环境:由 Agent 按 AGENTS.md「新环境准备」自行完成(重建 venv、装开发依赖、`bin/harness guard-git install`、`bin/verify`),不需要人执行。 3. 发版审批:Settings → Environments → New environment,名称 `release`,勾选 Required reviewers 并加上自己;只有一个维护者时不要勾选 Prevent self-review。 -4. Codex(用 Codex 时必做,`.codex/hooks.json` 改动后需重做):在仓库根启动交互式 `codex`,出现「Hooks need review」时先选 Review hooks,确认命令是调用 `harness/command_guard.py --role designer`,再信任。也可以启动后执行 `/hooks` 信任。信任只能由用户做;`--dangerously-bypass-hook-trust` 不用于本仓库。 +4. Codex(用 Codex 时必做,`.codex/hooks.json` 改动后需重做):在仓库根启动交互式 `codex`,出现「Hooks need review」时先选 Review hooks,确认命令是调用 `.harness/engine/guards/command_guard.py --role designer`,再信任。也可以启动后执行 `/hooks` 信任。信任只能由用户做;`--dangerously-bypass-hook-trust` 不用于本仓库。 5. Pi(执行方用 Pi 时必做):在仓库根目录启动 `pi`,执行 `/trust` 保存对本项目的信任(写入用户级 `~/.pi/agent/trust.json`,由用户自行执行),重启 pi 后项目扩展才会加载;或每次运行都加 `-a`。 6. Zcode(执行方用 Zcode 时必做,每个克隆一次,`.zcode/config.json` 改动后需重做):`zcode hooks trust status --workspace <仓库根>` 查看,确认声明内容后由用户执行它提示的 `zcode hooks trust grant --workspace <仓库根> --hook-digest `。 7. 设置后自检(只看、不改):Rulesets 列表中两条规则均为 Active;任一 PR 页面上 `build` 与 `harness` 标为 Required。不要用真实推送 main 的方式测试:规则没生效时会真的改掉 main。 @@ -144,9 +144,9 @@ Agent 层按角色接入: | CI harness job | 首次运行即拦下测试数据里的真实用户目录(run 36148783352) | ✅ | | git 钩子 | 临时仓库真实 git 回放:main 上提交、amend、移动与删除 tag、filter-repo、推 main、推 tag、强推、推送卫生(`tests/test_harness_guard.py`) | ✅ Linux;macOS 由 CI 覆盖 | | Claude Code PreToolUse | 2026-09-25 本仓库会话中真实拦截 3 次(均为命令文本含危险字样的误报,拦截本身生效) | ✅ 真实会话 | -| OpenCode 插件 | node 加载插件的单测;2026-09-26 真实 OpenCode(`opencode run`)中实测:设置覆盖变量的命令被拒、编辑 `harness/rules.toml` 被拒且文件未改 | ✅ 真实会话 | -| Pi 扩展 | node 加载扩展的单测(`PiExtensionTest`);2026-09-26 真实 Pi(`pi -a -p`)中实测:设置覆盖变量的命令被拒、编辑 `harness/rules.toml` 被拒且文件未改;**未信任项目时(`pi -p` 不带 `-a`)同一命令照常执行**,扩展未加载 | ✅ 已信任时;⚠️ 依赖用户按 §5 第 5 步信任项目 | -| Zcode 钩子 | `.zcode/config.json` 按声明运行钩子命令的单测(`ZcodeHookConfigTest`)。2026-09-26 真实 Zcode 0.16.9 实测:CLI(`zcode -p`)识别到声明并可授予信任,但**信任后两项探针仍放行**。日志为 `workspace_hook.feature_disabled`:源码中只有协议服务端(桌面 ZCode.app 等宿主)显式打开工作区钩子(`workspaceHookTrustEnabled: true`,注释称灰度开关),CLI 与 TUI 未打开 2026-09-26 桌面版 ZCode.app 实测(主仓库授信任后由用户发两项探针):设置覆盖变量的命令、编辑 `harness/rules.toml` 均被「harness 守卫」拒绝,文件未改。CLI 没有启用该功能的参数:开关只由协议服务端注入,不读项目配置或环境变量 | ✅ 桌面版;❌ CLI 与 TUI(只剩 git 与服务端两层) | +| OpenCode 插件 | node 加载插件的单测;2026-09-26 真实 OpenCode(`opencode run`)中实测:设置覆盖变量的命令被拒、编辑 `.harness/config/rules.toml` 被拒且文件未改 | ✅ 真实会话 | +| Pi 扩展 | node 加载扩展的单测(`PiExtensionTest`);2026-09-26 真实 Pi(`pi -a -p`)中实测:设置覆盖变量的命令被拒、编辑 `.harness/config/rules.toml` 被拒且文件未改;**未信任项目时(`pi -p` 不带 `-a`)同一命令照常执行**,扩展未加载 | ✅ 已信任时;⚠️ 依赖用户按 §5 第 5 步信任项目 | +| Zcode 钩子 | `.zcode/config.json` 按声明运行钩子命令的单测(`ZcodeHookConfigTest`)。2026-09-26 真实 Zcode 0.16.9 实测:CLI(`zcode -p`)识别到声明并可授予信任,但**信任后两项探针仍放行**。日志为 `workspace_hook.feature_disabled`:源码中只有协议服务端(桌面 ZCode.app 等宿主)显式打开工作区钩子(`workspaceHookTrustEnabled: true`,注释称灰度开关),CLI 与 TUI 未打开 2026-09-26 桌面版 ZCode.app 实测(主仓库授信任后由用户发两项探针):设置覆盖变量的命令、编辑 `.harness/config/rules.toml` 均被「harness 守卫」拒绝,文件未改。CLI 没有启用该功能的参数:开关只由协议服务端注入,不读项目配置或环境变量 | ✅ 桌面版;❌ CLI 与 TUI(只剩 git 与服务端两层) | | Codex hooks | 载荷解析单测(含列表形式命令),`CodexHookConfigTest` 按 `.codex/hooks.json` 的声明运行钩子。2026-09-26 Codex 0.157.1 在临时仓库实测:项目级 hooks.json 被读取;未信任时 `codex exec` 静默不运行、命令照常执行;用户信任后,探针钩子以 JSON 拒绝拦下命令(`Command blocked by PreToolUse hook`);载荷为 `tool_name: Bash`、`tool_input.command` 字符串,与守卫的 Claude 格式一致。同日用户信任本仓库钩子后,在仓库内实测 `codex exec`:`git status` 放行,`gh pr merge` 被守卫以退出码 2 拒绝,Codex 报 `Command blocked by PreToolUse hook: harness 守卫拒绝了这次操作…`,拒绝理由原样回给模型。2026-09-28 Codex 桌面版实测(用户在本仓库执行):`git status` 放行,`gh pr merge 999999` 被 PreToolUse 钩子拦截、未执行,桌面版同样执行项目级 `.codex/hooks.json` | ✅ 真实会话(`codex exec` 与桌面版) | | ruleset | 配置文件与 workflow 一致性单测(`tests/test_harness_release.py`);2026-09-25 导入后经 GitHub API 核对:两条均 Active、规则与文件一致(GitHub 为 PR 规则补了默认参数)、无绕过名单,main 上生效的规则为禁删、禁强推、必须经 PR、`build` 与 `harness` 必须通过 | ✅ | | 必过检查严格模式、密钥扫描(B15、B26) | 2026-09-28 用户在仓库设置中修改后,经 GitHub API 只读核对:main 规则(Active)`strict_required_status_checks_policy: true`,`build` 与 `harness` 必过、无绕过名单,其余规则不变;`secret_scanning` 与 `secret_scanning_push_protection` 均为 `enabled`。`.github/rulesets/main.json` 同步为严格模式 | ✅ 配置;推送被拦的实际效果待首次命中 | @@ -180,11 +180,11 @@ Agent 层按角色接入: - 残余:Agent 与用户在同一个 macOS 用户下运行,技术上能读到用户在 gh 里的登录凭据,再以用户身份批准。这一步只靠 Agent 层守卫拦(拒绝 `gh pr review --approve` 与 MCP 评审工具);彻底隔离要给 Agent 单独的系统用户。 - 用户自己开的 PR 同样需要别人批准,由 `Snowson` 账号批准,或临时调整 ruleset。 - **PR 在自己的 workflow 里合并自己**:`pull_request` 事件执行 PR 分支里的 workflow 定义。D3 之后合并需要非推送者批准,`GITHUB_TOKEN` 既不能批准(仓库已关闭「允许 Actions 批准 PR」),也取不到 environment `auto-merge` 中的 App 凭据,因此 PR 不能再合并自己。仍依赖的前提:environment 的分支限制只允许 main;App 只装在本仓库,只有 Contents 与 Pull requests 写权限。App 的 Contents 写权限是批准被计入的前提(H0926-6),它的令牌因此也能推送;令牌只在 main 上的 auto-merge、判定为 R0/R1 之后取得。 -- **命令守卫按命令结构判断**(2026-09-26,方案 §13 E3,`harness/shell_structure.py`):把命令拆成简单命令,只对会执行的程序(git、rm、gh、curl 与覆盖变量的赋值)按参数判断;echo、grep 模式、提交说明、Agent 提示词、heredoc 正文、注释里的文字不再误拒。会执行但看不到结构的地方退回字符串规则:`python -c` 等解释器代码、交给 shell 或解释器执行的 stdin(heredoc 与 here-string 只判断被执行的那段正文,管道整段判断)、无法解析的命令(引号不配对);`bash -c`、`eval`、`$(...)`、反引号递归按结构判断。仍然识别不了的:脚本文件内部(`bash x.sh`、`python3 x.py`)、`xargs` 从 stdin 补的参数、变量间接展开出的命令名。实测探针不要再用 `echo 覆盖变量=1`(现在按数据放行),改用会真实执行、被放过也无害的命令,例如对不存在的分支强推。此前字符串匹配阶段记录了 6 次误报。 +- **命令守卫按命令结构判断**(2026-09-26,方案 §13 E3,`.harness/engine/core/shell_structure.py`):把命令拆成简单命令,只对会执行的程序(git、rm、gh、curl 与覆盖变量的赋值)按参数判断;echo、grep 模式、提交说明、Agent 提示词、heredoc 正文、注释里的文字不再误拒。会执行但看不到结构的地方退回字符串规则:`python -c` 等解释器代码、交给 shell 或解释器执行的 stdin(heredoc 与 here-string 只判断被执行的那段正文,管道整段判断)、无法解析的命令(引号不配对);`bash -c`、`eval`、`$(...)`、反引号递归按结构判断。仍然识别不了的:脚本文件内部(`bash x.sh`、`python3 x.py`)、`xargs` 从 stdin 补的参数、变量间接展开出的命令名。实测探针不要再用 `echo 覆盖变量=1`(现在按数据放行),改用会真实执行、被放过也无害的命令,例如对不存在的分支强推。此前字符串匹配阶段记录了 6 次误报。 - **Zcode 与 Pi 的拦截依赖用户信任**:Zcode 的工作区钩子未经信任不执行,Pi 的项目扩展在未信任项目时不加载。此时它们设置覆盖变量或使用 `--no-verify`,本机两层都挡不住,只有服务端兜底。 -- **本机护栏的信任根在可写路径**(评审 PR7-R3):git 钩子的规则文件曾在执行者可写的工作区,改掉 `protected_branches` 即可让本机改写 main 不受拦截。现在钩子按 origin/main 上的 `harness/rules.toml` 执行,工作区版本不一致时告警;但守卫代码(`harness/git_guard.py`、`.githooks/`)与运行时解释器(`scratch/iterm-probe-venv`)仍可被改。本机两层定位为防误操作,防有意绕过只能靠服务端 ruleset 与发版审批,所以一次性设置里 ruleset 排在第一步。 +- **本机护栏的信任根在可写路径**(评审 PR7-R3):git 钩子的规则文件曾在执行者可写的工作区,改掉 `protected_branches` 即可让本机改写 main 不受拦截。现在钩子按 origin/main 上的 `.harness/config/rules.toml` 执行,工作区版本不一致时告警;但守卫代码(`.harness/engine/guards/git_guard.py`、`.githooks/`)与运行时解释器(`scratch/iterm-probe-venv`)仍可被改。本机两层定位为防误操作,防有意绕过只能靠服务端 ruleset 与发版审批,所以一次性设置里 ruleset 排在第一步。 - 「读 origin/main」只在本地远端引用可信时成立(评审 PR7-R8):本地的 `refs/remotes/origin/main` 不受保护,把它改指伪造的提交,或把 remote 指向伪造的源再 fetch,守卫就会读到伪造的规则并照常告警「按 origin/main 的规则执行」。 - - 本 PR 合并前 main 上没有 `harness/rules.toml`,钩子退回读工作区版本;更根本的是 main 上还没有 `.githooks/`,检出 main 时本机没有任何 git 守卫(2026-09-26 评审报告直接提交到 main 即因此未被拦截,见基线评审 H0926-1)。 + - 本 PR 合并前 main 上没有 `.harness/config/rules.toml`,钩子退回读工作区版本;更根本的是 main 上还没有 `.githooks/`,检出 main 时本机没有任何 git 守卫(2026-09-26 评审报告直接提交到 main 即因此未被拦截,见基线评审 H0926-1)。 - **base_tests 的篡改检查在同一进程内**(评审 PR7-R7):base 版本的测试 import 的是 head 的产品代码。`base_tests_runner.py` 在运行前后比对 unittest 各类与模块的属性,并跑一组必然失败的哨兵用例;risk.py 对产品代码新增的测试框架引用加标记(取消 R1)。专门针对这两处检查的篡改(例如识别哨兵用例名、改后在检查前复原)仍可能漏过,由评审兜底;接上 L4 自动合并(方案 §13 E1)前需复核这一残余。 ## 8. 停机与恢复 diff --git a/docs/specs/usage-cost.md b/docs/specs/usage-cost.md index ae4c026..757e258 100644 --- a/docs/specs/usage-cost.md +++ b/docs/specs/usage-cost.md @@ -269,7 +269,7 @@ bin/session-manager inbox pricing show [MODEL] | check [--days 30] | update [--a | U8 | `money_text` 双端一致 | Python 与 Swift 跑同一组边界值 | 单测 | `test_usage_cost.MoneyTextTest`、`tests/InboxPolicyTests.swift#moneyText(0.005, currency: "USD")` | | U9 | 周期边界 | ISO 周跨年、闰年二月、月末时区边界 | 单测 | `test_usage_report.PeriodBoundsTest` | | U10 | 界面 | 真实数据下日/周/月三屏截图(`scratch/u10-*.png`)核对:编排与统一设计语言一致、金额为 USD 伴随指标、悬浮卡数字与 `inbox usage` 一致(2026-09-24 重设计后重验收) | 真机 UI | 用户截图核对 | -| U11 | 全量测试 | `python3 -W error::ResourceWarning -m unittest discover -s tests -v`、`ruff check scripts tests`、Swift 策略测试全部通过 | CI 断言 | `.github/workflows/build.yml#harness/verify.py --strict --full` | +| U11 | 全量测试 | `python3 -W error::ResourceWarning -m unittest discover -s tests -v`、`ruff check scripts tests`、Swift 策略测试全部通过 | CI 断言 | `.github/workflows/build.yml#cli.py verify --strict --full` | ## 9. 已知边界 diff --git a/docs/specs/zcode-native-navigation.md b/docs/specs/zcode-native-navigation.md index ad4e6da..9dab89d 100644 --- a/docs/specs/zcode-native-navigation.md +++ b/docs/specs/zcode-native-navigation.md @@ -85,7 +85,7 @@ python3 -m unittest discover -s tests -v | ZN1 | 按 task ID 读本地索引取标题与工作区;归档、缺失、歧义及含控制字符的标题在界面操作前拒绝 | 单测 | `test_zcode_focus.ZcodeDescriptorTests.test_archived_and_ambiguous_not_opened`、`test_zcode_focus.ZcodeDescriptorTests.test_query_cannot_contain_newline_or_control_character`、`test_zcode_task_state.TaskIndexTests.test_duplicate_task_id_is_ambiguous`、`test_zcode_task_state.TaskIndexTests.test_id_injection_is_not_a_query` | | ZN2 | 只有复制出的任务路径与索引中的 workspace + task ID 一致才算精确定位;逻辑路径不要求文件存在;打开工作区不算精确定位 | 单测 | `test_zcode_task_state.TaskIndexTests.test_copied_ui_path_must_match_task_and_workspace`、`test_zcode_task_state.TaskIndexTests.test_logical_copied_path_does_not_require_a_legacy_file`、`test_zcode_task_state.TaskIndexTests.test_workspace_link_is_not_claimed_as_exact_navigation` | | ZN3 | 只有导航元数据跨越原生边界;原生失败写日志、终端不刷屏 | 单测 | `test_zcode_focus.ZcodeDescriptorTests.test_only_navigation_metadata_crosses_native_boundary`、`test_zcode_focus.ZcodeDescriptorTests.test_native_failure_is_logged_without_cluttering_terminal_with_traces` | -| ZN4 | 原生无 UI 自检:搜索框结构匹配、打开面板的点击状态机(已打开不重复切换)、等待服务主 run loop | 单测 | `harness/verify.py#--self-test` | +| ZN4 | 原生无 UI 自检:搜索框结构匹配、打开面板的点击状态机(已打开不重复切换)、等待服务主 run loop | 单测 | `.harness/config/checks.toml#--self-test` | | ZN5 | 剪贴板只用于复制任务路径:原内容保存在内存、用户未改动时恢复;不打印不落盘;必须产生新的 changeCount | 人工 | 代码审读(原生实现无自动测试) | | ZN6 | 真实 Zcode 中打开目标任务并返回 status=focused、selection_identity_matches=true | 真机 UI | 2026-09-14 真实运行记录(本地日志,不入库) | | ZN7 | 同名多候选、冷启动、多显示器、英文界面 | 真机 UI | 未完成(见下方边界) | diff --git a/docs/templates/review-checklist.md b/docs/templates/review-checklist.md index b055f3a..9589ed9 100644 --- a/docs/templates/review-checklist.md +++ b/docs/templates/review-checklist.md @@ -5,7 +5,7 @@ 不重复劳动)和必须由评审方亲自看的部分。tests/test_harness_review.py 检查本清单覆盖全部失败类型。 --> -评审前先生成证据包:`python3 harness/review_pack.py --base origin/main`。证据包里已有的结论(风险等级、修复证据、verify、验收映射)只需核对与当前 head 一致;评审的时间花在下表「评审必须看」一列。 +评审前先生成证据包:`bin/harness review-pack --base origin/main`。证据包里已有的结论(风险等级、修复证据、verify、验收映射)只需核对与当前 head 一致;评审的时间花在下表「评审必须看」一列。 | 类型 | 机器已判定 | 评审必须看 | |---|---|---| diff --git a/docs/templates/review-prompt.md b/docs/templates/review-prompt.md index 3ea32ef..5bd61f4 100644 --- a/docs/templates/review-prompt.md +++ b/docs/templates/review-prompt.md @@ -6,7 +6,7 @@ ## 步骤 -1. `git fetch origin`,然后 `python3 harness/review_pack.py --base origin/main --output build/review/pack.md` 生成证据包。 +1. `git fetch origin`,然后 `bin/harness review-pack --base origin/main --output build/review/pack.md` 生成证据包。 2. 确认当前 head 的 CI(`build` 与 `harness` 两个 job)已通过,记录 run 链接;未通过时直接给出「不通过」,不做其余评审。 3. 对照 `docs/templates/review-checklist.md`,按本次 diff 涉及的失败类型逐条看「评审必须看」一列;「机器已判定」一列只核对证据包结论与 head 一致。 4. 对证据包列出的验收编号:可自动化条目打开对应测试,确认它断言的是这条验收、并且调用的是产品代码;人工条目整理成给用户的验收步骤。 diff --git a/docs/templates/task.md b/docs/templates/task.md index 69f554b..e0703a7 100644 --- a/docs/templates/task.md +++ b/docs/templates/task.md @@ -20,7 +20,7 @@ rollback: git revert # 回滚方式