From efa3d48a9be192bb35658fea985cfeb70ba1705b Mon Sep 17 00:00:00 2001 From: weichen Date: Sat, 17 Oct 2026 08:52:32 +0000 Subject: [PATCH] Freeze the adoption case script and record the r1 fail-stop Co-authored-by: Cursor --- .task/feature-governance-v2-closeout.md | 3 +- .../plans/evidence/governance-adopt-cases.py | 160 ++++++++++++++++++ 2 files changed, 162 insertions(+), 1 deletion(-) create mode 100755 docs/superpowers/plans/evidence/governance-adopt-cases.py diff --git a/.task/feature-governance-v2-closeout.md b/.task/feature-governance-v2-closeout.md index e1eb4dcb..dbc21160 100644 --- a/.task/feature-governance-v2-closeout.md +++ b/.task/feature-governance-v2-closeout.md @@ -32,7 +32,8 @@ | 单元 ID | 预期范围 | 配置摘要 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 | |---|---|---|---|---|---|---|---|---|---|---| -| 待定义 | — | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | — | +| (无效启动) | — | — | implementation | — | 无 attempt | — | invalid-config(单元包路径尚未写出即启动,执行器读取失败;无 attempt、无副作用) | 无 | 否 | /tmp/gov-receipt-Agent-System-Dev-r0-invalid-unit-path.json | +| governance-adoption-Agent-System-Dev-r1 | 7 个允许路径(DM gitlink、scripts/dm、scripts/task、卡片模板、采纳记录、ci.yaml、PROJECT.md) | — | implementation | cursor→antigravity→pi | run a3db419d(attempts=1) | cursor / not-observable | failed-unknown(canonical checkout mutated:运行期间 host 向本 worktree 写入 host 豁免文件 docs/superpowers/plans/evidence/governance-adopt-cases.py,违反运行期不触碰规则;对账:HEAD 仍 5de0713,唯一变化即该文件,worker exit 0 且改动均在允许路径内但按 fail-stop 不采用;先提交该文件后以全新 r2 重跑) | 无 | 否 | /tmp/gov-receipt-Agent-System-Dev-r1.json | ## 关联方向与规划 diff --git a/docs/superpowers/plans/evidence/governance-adopt-cases.py b/docs/superpowers/plans/evidence/governance-adopt-cases.py new file mode 100755 index 00000000..b2058253 --- /dev/null +++ b/docs/superpowers/plans/evidence/governance-adopt-cases.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Adoption positive/negative cases through a repository's own scripts/agent-exec. + +Usage: adopt_cases.py [] +Clones the committed HEAD of into a temporary directory, +materializes deps/development-manager at its gitlink, points every profile at a +fake CLI and runs: positive run+apply, missing config, bad receipt, out-of-scope +path and unknown side effect. Prints one line per case and a verdict. +""" +import json +import os +import re +import shutil +import stat +import subprocess +import sys +import tempfile +from pathlib import Path + +SRC = Path(sys.argv[1]).resolve() +DM_SOURCE = sys.argv[2] if len(sys.argv) > 2 else str(Path.home() / "projects/Development-Manager") +TERMINAL_OK = json.dumps({"terminal_status": "completed", "summary": "done", "changed_files": ["adoption-probe.txt"], "evidence": ["wrote probe"], "scope_expansion": None}, separators=(",", ":")) +HELP = "--print\n--output-format\nstream-json\n--force\n--trust\n--workspace\n--sandbox\n--model\n-p\n" +ENVELOPE = 'printf \'%s\\n\' "{\\"type\\":\\"result\\",\\"subtype\\":\\"success\\",\\"is_error\\":false,\\"result\\":$(python3 -c \'import json,sys;print(json.dumps(sys.stdin.read()))\' < "$R")}"' + + +def sh(*args, cwd=None, env=None, check=True): + return subprocess.run(list(args), cwd=cwd, env=env, capture_output=True, text=True, check=check, timeout=900) + + +def fake_cli(bindir: Path, body: str) -> None: + for name in ("fake-cli",): + p = bindir / name + p.write_text( + "#!/bin/sh\n" + 'case "$1" in --version|-v|-V) echo 0.0.1-test; exit 0;; --help|-h) cat <<\'HELP\'\n' + HELP + "HELP\nexit 0;; esac\n" + 'R=$(mktemp)\n' + body + "\nrc=$?\n" + ENVELOPE + '\nrm -f "$R"\nexit $rc\n', + encoding="utf-8", + ) + p.chmod(p.stat().st_mode | stat.S_IEXEC) + + +def prepare(root: Path, drop_config=False) -> Path: + repo = root / "repo" + sh("git", "clone", "-q", "--no-recurse-submodules", str(SRC), str(repo)) + sh("git", "-C", str(repo), "config", "user.email", "adopt@example.invalid") + sh("git", "-C", str(repo), "config", "user.name", "adopt") + pin = sh("git", "-C", str(repo), "ls-tree", "HEAD", "deps/development-manager").stdout.split() + if len(pin) >= 3 and pin[0] == "160000": + dm = repo / "deps/development-manager" + if dm.exists(): + shutil.rmtree(dm) + sh("git", "clone", "-q", "--no-checkout", DM_SOURCE, str(dm)) + sh("git", "-C", str(dm), "checkout", "-q", "--detach", pin[2]) + elif not (repo / "cmd/dm-server").is_dir(): + raise SystemExit("deps/development-manager is not a gitlink at HEAD") + cfg = repo / "AGENT-EXECUTION.md" + text = cfg.read_text(encoding="utf-8") + text = re.sub(r"(?m)^(\s+executable:\s*).*$", r"\1fake-cli", text) + text = re.sub(r"(?m)^(\s+adapter:\s*)\S+$", r"\1cursor", text) + cfg.write_text(text, encoding="utf-8") + if drop_config: + cfg.unlink() + sh("git", "-C", str(repo), "add", "-A", "--", "AGENT-EXECUTION.md") + sh("git", "-C", str(repo), "commit", "-q", "--no-verify", "-m", "adoption case fixture") + sh("git", "-C", str(repo), "checkout", "-q", "-b", "feature/adoption-case") + return repo + + +def unit(root: Path) -> Path: + p = root / "unit.json" + p.write_text(json.dumps({"unit_id": "adoption-case", "goal": "Create adoption-probe.txt", "allowed_paths": ["adoption-probe.txt"], "verification": ["test -f adoption-probe.txt"], "constraints": ["leave uncommitted"], "network": False, "external_cost": False, "timeout_seconds": 120})) + return p + + +def run(repo: Path, root: Path, body: str): + bindir = root / "bin" + bindir.mkdir(exist_ok=True) + fake_cli(bindir, body) + rec = root / "receipt.json" + env = dict(os.environ, PATH=f"{bindir}:{os.environ['PATH']}", AGENT_EXEC_CACHE=str(Path.home() / ".cache/adoption-cases")) + p = sh(str(repo / "scripts/agent-exec"), "run", "--repo", str(repo), "--role", "implementation", "--unit", str(unit(root)), "--receipt", str(rec), cwd=repo, env=env, check=False) + d = json.loads(rec.read_text()) if rec.exists() else {} + return p, d, rec, env + + +WRITE_OK = 'echo probe > adoption-probe.txt; printf "%s" \'' + TERMINAL_OK + '\' > "$R"' +results = [] + + +def case(name, fn): + root = Path(tempfile.mkdtemp(prefix=f"adopt-{name}-")) + try: + ok, detail = fn(root) + except Exception as exc: # noqa: BLE001 + ok, detail = False, f"error: {exc}" + finally: + shutil.rmtree(root, ignore_errors=True) + results.append(ok) + print(f"{'PASS' if ok else 'FAIL'} {name}: {detail}") + + +def positive(root): + repo = prepare(root) + p, d, rec, env = run(repo, root, WRITE_OK) + if d.get("terminal_status") != "success" or not d.get("transport"): + return False, f"run exit={p.returncode} terminal={d.get('terminal_status')} reason={d.get('reason')}" + before = sh("git", "-C", str(repo), "rev-parse", "HEAD").stdout.strip() + a = sh(str(repo / "scripts/agent-exec"), "apply", "--repo", str(repo), "--receipt", str(rec), cwd=repo, env=env, check=False) + after = sh("git", "-C", str(repo), "rev-parse", "HEAD").stdout.strip() + ok = a.returncode == 0 and after == d["transport"]["commit"] and before != after + return ok, f"run exit={p.returncode} terminal=success apply exit={a.returncode} head={after[:12]}" + + +def missing_config(root): + repo = prepare(root, drop_config=True) + p, d, _, env = run(repo, root, WRITE_OK) + v = sh(str(repo / "scripts/agent-exec"), "validate", "--repo", str(repo), cwd=repo, env=env, check=False) + ok = p.returncode != 0 and d.get("terminal_status") == "invalid-config" and not d.get("transport") and v.returncode != 0 + return ok, f"run exit={p.returncode} terminal={d.get('terminal_status')} validate exit={v.returncode}" + + +def bad_receipt(root): + repo = prepare(root) + p, d, rec, env = run(repo, root, WRITE_OK) + if d.get("terminal_status") != "success": + return False, f"setup run terminal={d.get('terminal_status')}" + d["changed_paths"] = sorted(set((d.get("changed_paths") or []) + ["src/injected.txt"])) + rec.write_text(json.dumps(d)) + before = sh("git", "-C", str(repo), "rev-parse", "HEAD").stdout.strip() + a = sh(str(repo / "scripts/agent-exec"), "apply", "--repo", str(repo), "--receipt", str(rec), cwd=repo, env=env, check=False) + after = sh("git", "-C", str(repo), "rev-parse", "HEAD").stdout.strip() + ok = a.returncode != 0 and before == after + return ok, f"apply exit={a.returncode} head_unchanged={before == after} msg={(a.stderr or a.stdout).strip()[:100]}" + + +def out_of_scope(root): + repo = prepare(root) + p, d, _, _ = run(repo, root, 'echo x > outside-scope.txt; ' + WRITE_OK) + ok = p.returncode != 0 and d.get("terminal_status") == "scope-expansion" and not d.get("transport") + return ok, f"run exit={p.returncode} terminal={d.get('terminal_status')} transport={bool(d.get('transport'))}" + + +def unknown_side_effect(root): + repo = prepare(root) + p, d, _, _ = run(repo, root, 'echo probe > adoption-probe.txt; printf "done, no record" > "$R"') + ok = p.returncode != 0 and d.get("terminal_status") == "failed-unknown" and not d.get("transport") + return ok, f"run exit={p.returncode} terminal={d.get('terminal_status')} transport={bool(d.get('transport'))}" + + +if __name__ == "__main__": + head = sh("git", "-C", str(SRC), "rev-parse", "HEAD").stdout.strip() + print(f"[object] {SRC.name} HEAD={head}") + case("positive-run-apply", positive) + case("missing-config", missing_config) + case("bad-receipt", bad_receipt) + case("out-of-scope-path", out_of_scope) + case("unknown-side-effect", unknown_side_effect) + print(f"[verdict] {'ALL PASS' if all(results) else 'FAILURES'} cases={len(results)}") + sys.exit(0 if all(results) else 1)