harness-agent2 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
harness/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Harness - local-first control plane that professionalizes how coding agents work on a repo."""
2
+
3
+ __version__ = "1.1.0"
harness/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from harness.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
harness/agents.py ADDED
@@ -0,0 +1,75 @@
1
+ """Agent adapters: the same contract lifecycle driven by different coding agents.
2
+
3
+ Enforcement asymmetry, stated honestly: Claude runs under the PreToolUse guard hook
4
+ (.claude/settings.json); Codex has no hook equivalent, so it relies on AGENTS.md rules
5
+ plus its workspace-write sandbox, with the scope gate in `harness verify` catching
6
+ violations after the fact.
7
+ """
8
+
9
+ import json
10
+ import shutil
11
+ import subprocess
12
+ from collections.abc import Callable, Mapping
13
+ from dataclasses import dataclass
14
+ from types import MappingProxyType
15
+
16
+ from harness.policy import die
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class AgentSpec:
21
+ binary: str
22
+ argv: Callable[[str], list[str]]
23
+ enforcement_tier: str
24
+ description: str
25
+
26
+
27
+ AGENT_SPECS: Mapping[str, AgentSpec] = MappingProxyType({
28
+ "claude": AgentSpec("claude", lambda prompt: ["claude", "-p", prompt, "--permission-mode", "acceptEdits"],
29
+ "guard-hook", "Claude Code with the PreToolUse guard hook."),
30
+ "codex": AgentSpec("codex", lambda prompt: ["codex", "exec", "--sandbox", "workspace-write", prompt],
31
+ "sandbox+verify", "Codex with workspace-write sandbox and verify scope gate."),
32
+ })
33
+ AGENTS = tuple(AGENT_SPECS)
34
+
35
+
36
+ def agent_prompt(c) -> str:
37
+ return (f"Goal: {c['goal']}\nAcceptance criteria:\n" + "\n".join(f"- {x}" for x in c["accept"]) +
38
+ "\nFirst write a short plan, then implement. Do not touch protected paths. Before finishing, run: " +
39
+ "; ".join(g["cmd"] for g in c["gates"].values()) + ". Do not claim completion while any fails.")
40
+
41
+
42
+ def build_argv(agent: str, prompt: str) -> list[str]:
43
+ try:
44
+ return AGENT_SPECS[agent].argv(prompt)
45
+ except KeyError:
46
+ die(f"unknown agent: {agent} (supported: {', '.join(AGENT_SPECS)})")
47
+
48
+
49
+ def cmd_agents(args) -> int:
50
+ rows = []
51
+ for name, spec in AGENT_SPECS.items():
52
+ path = shutil.which(spec.binary)
53
+ version = None
54
+ if path:
55
+ for flag in ("--version", "version"):
56
+ try:
57
+ result = subprocess.run([path, flag], capture_output=True, text=True, timeout=5, check=False)
58
+ except (OSError, subprocess.SubprocessError):
59
+ continue
60
+ if result.returncode == 0:
61
+ lines = (result.stdout + result.stderr).strip().splitlines()
62
+ version = lines[0][:90] if lines else "present"
63
+ break
64
+ if version is None:
65
+ version = "present"
66
+ rows.append({"name": name, "binary": spec.binary, "present": path is not None,
67
+ "version": version, "enforcement_tier": spec.enforcement_tier})
68
+ if args.json:
69
+ print(json.dumps(rows))
70
+ else:
71
+ print(f"{'NAME':<12} {'PRESENT':<8} {'VERSION':<24} ENFORCEMENT TIER")
72
+ for row in rows:
73
+ print(f"{row['name']:<12} {'yes' if row['present'] else 'no':<8} "
74
+ f"{(row['version'] or '-'):<24} {row['enforcement_tier']}")
75
+ return 0
harness/bundle.py ADDED
@@ -0,0 +1,110 @@
1
+ """Team policy bundle: the shared files of .ai-engineering/ versioned in a Git repo.
2
+
3
+ Bundle = policy.json + verification.json at .ai-engineering/ in the bundle repo.
4
+ Everything else stays local on purpose: project.json carries machine-specific
5
+ paths, and tasks/evidence/audit.log are per-repo history.
6
+ """
7
+
8
+ import shutil
9
+ import subprocess
10
+ import tempfile
11
+ from pathlib import Path
12
+
13
+ from harness.policy import AE, die, find_root
14
+
15
+ BUNDLE = ("policy.json", "verification.json")
16
+
17
+
18
+ def _git(cwd: Path, *a: str) -> tuple[int, str]:
19
+ p = subprocess.run(["git", *a], cwd=str(cwd), capture_output=True, text=True, check=False)
20
+ return p.returncode, (p.stdout + p.stderr).rstrip()
21
+
22
+
23
+ def _clone(remote: str, branch: str) -> Path:
24
+ tmp = Path(tempfile.mkdtemp(prefix="harness-bundle-"))
25
+ clone = tmp / "repo"
26
+ rc, out = _git(tmp, "clone", remote, "repo")
27
+ if rc:
28
+ shutil.rmtree(tmp, ignore_errors=True)
29
+ die(f"cannot clone bundle remote {remote!r}: {out}")
30
+ if _git(clone, "rev-parse", "--verify", f"origin/{branch}")[0] == 0:
31
+ _git(clone, "checkout", "-B", branch, f"origin/{branch}")
32
+ else:
33
+ # non-empty repo without the branch: new branch off HEAD; empty repo: unborn HEAD
34
+ _git(clone, "checkout", "-B", branch)
35
+ return clone
36
+
37
+
38
+ def _need_local(r: Path) -> None:
39
+ miss = [n for n in BUNDLE if not (r / AE / n).exists()]
40
+ if miss:
41
+ die(f"missing {', '.join(miss)}; run `harness init` first")
42
+
43
+
44
+ def _remote_bundle(clone: Path) -> dict[str, str]:
45
+ out = {}
46
+ for n in BUNDLE:
47
+ p = clone / AE / n
48
+ if p.exists():
49
+ out[n] = p.read_text()
50
+ else:
51
+ die(f"bundle remote has no {AE}/{n}; run `harness policy push` from a member repo first")
52
+ return out
53
+
54
+
55
+ def _push(r: Path, clone: Path, a) -> int:
56
+ _need_local(r)
57
+ dst = clone / AE
58
+ dst.mkdir(parents=True, exist_ok=True)
59
+ for n in BUNDLE:
60
+ shutil.copy2(r / AE / n, dst / n)
61
+ _git(clone, "add", "-A")
62
+ rc, out = _git(clone, "commit", "-m", f"harness policy bundle: {r.name}")
63
+ if "nothing to commit" in out:
64
+ print("Bundle : unchanged (remote already has this state)")
65
+ return 0
66
+ rc, out = _git(clone, "push", "origin", f"HEAD:{a.branch}")
67
+ if rc:
68
+ die(f"push failed: {out}")
69
+ print(f"Pushed : {_git(clone, 'rev-parse', 'HEAD')[1]} -> {a.remote} ({a.branch})")
70
+ return 0
71
+
72
+
73
+ def _status(r: Path, clone: Path, a) -> int:
74
+ _need_local(r)
75
+ remote = _remote_bundle(clone)
76
+ drift = [n for n in BUNDLE if remote[n] != (r / AE / n).read_text()]
77
+ for n in drift:
78
+ print(f"DRIFT : {AE}/{n} differs from {a.remote} ({a.branch})")
79
+ if drift:
80
+ return 1
81
+ print(f"In sync : {', '.join(BUNDLE)} match {a.remote} ({a.branch})")
82
+ return 0
83
+
84
+
85
+ def _pull(r: Path, clone: Path, a) -> int:
86
+ remote = _remote_bundle(clone)
87
+ updated = []
88
+ for n in BUNDLE:
89
+ lp = r / AE / n
90
+ if not lp.exists() or lp.read_text() != remote[n]:
91
+ lp.parent.mkdir(parents=True, exist_ok=True)
92
+ lp.write_text(remote[n])
93
+ updated.append(n)
94
+ print(f"Pulled : {', '.join(updated) if updated else 'nothing (already in sync)'}")
95
+ return 0
96
+
97
+
98
+ def cmd_policy(a) -> int:
99
+ r = find_root(a.repo)
100
+ if not r:
101
+ die("run `harness init` first")
102
+ clone = _clone(a.remote, a.branch)
103
+ try:
104
+ if a.polc == "push":
105
+ return _push(r, clone, a)
106
+ if a.polc == "status":
107
+ return _status(r, clone, a)
108
+ return _pull(r, clone, a)
109
+ finally:
110
+ shutil.rmtree(clone.parent, ignore_errors=True)
harness/ci.py ADDED
@@ -0,0 +1,112 @@
1
+ """CI generator: a GitHub Actions workflow running the readiness floor and the repo's gates.
2
+
3
+ The YAML is generated from the repo's own verification.json, so CI and
4
+ `harness verify` execute the same commands. Re-running `harness ci` updates the
5
+ file in place (marker line); a hand-written workflow of the same name is only
6
+ touched with --force.
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+ from harness.policy import AE, die, find_root, jload
12
+ from harness.scan import scan
13
+
14
+ WORKFLOW = Path(".github") / "workflows" / "harness.yml"
15
+ MARKER = "Generated by `harness ci`"
16
+ LOCKFILES = {"package-lock.json": "npm ci --no-fund --no-audit", "pnpm-lock.yaml": "pnpm install --frozen-lockfile",
17
+ "yarn.lock": "yarn install --frozen-lockfile"}
18
+ SKIP_DIRS = {"node_modules", ".venv", "venv", "dist", "build", "test-results"}
19
+
20
+
21
+ def _manifest_dirs(r: Path) -> list[Path]:
22
+ """Dirs holding a JS package: the root plus depth-1 app dirs (web/, frontend/…)."""
23
+ dirs = [r] if (r / "package.json").exists() else []
24
+ dirs += [p.parent for p in sorted(r.glob("*/package.json")) if p.parent.name not in SKIP_DIRS]
25
+ return dirs
26
+
27
+
28
+ def _install_cmds(r: Path) -> list[str]:
29
+ cmds = []
30
+ if (r / "uv.lock").exists():
31
+ cmds += ["pip install uv", "uv sync --frozen"] # runners have pip, not uv
32
+ for f, c in LOCKFILES.items():
33
+ if (r / f).exists():
34
+ cmds.append(c)
35
+ for d in _manifest_dirs(r):
36
+ if d == r:
37
+ continue
38
+ for f, c in LOCKFILES.items():
39
+ if (d / f).exists():
40
+ cmds.append(f"cd {d.relative_to(r).as_posix()} && {c}")
41
+ if not cmds and (r / "pyproject.toml").exists():
42
+ cmds.append("pip install .")
43
+ return list(dict.fromkeys(cmds))
44
+
45
+
46
+ def _harness_install(r: Path) -> str:
47
+ """Where CI gets the CLI: this repo when it is harness itself, otherwise a git ref
48
+ (the package is not on PyPI; secrets.HARNESS_REPO points at the harness repository)."""
49
+ return "pip install ." if (r / "harness" / "cli.py").exists() else "pip install git+${{ secrets.HARNESS_REPO }}"
50
+
51
+
52
+ def _yaml(r: Path, gates: dict, fail_under: int, extra: list[str], install: str) -> str:
53
+ L = ["# " + MARKER + " — regenerate with `harness ci`; do not edit by hand.",
54
+ "name: harness",
55
+ "on:",
56
+ " pull_request:",
57
+ " push:",
58
+ " branches: [main]",
59
+ "permissions:",
60
+ " contents: read",
61
+ "jobs:",
62
+ " readiness:",
63
+ " runs-on: ubuntu-latest",
64
+ " steps:",
65
+ " - uses: actions/checkout@v4",
66
+ " - uses: actions/setup-python@v5",
67
+ " with:",
68
+ ' python-version: "3.12"',
69
+ f' - run: "{install}"',
70
+ f' - run: "harness scan --fail-under {fail_under}"',
71
+ " gates:",
72
+ " runs-on: ubuntu-latest",
73
+ " steps:",
74
+ " - uses: actions/checkout@v4"]
75
+ if _manifest_dirs(r):
76
+ L += [" - uses: actions/setup-node@v4",
77
+ " with:",
78
+ ' node-version: "22"']
79
+ if (r / "pyproject.toml").exists() or (r / "uv.lock").exists() or (r / "requirements.txt").exists():
80
+ L += [" - uses: actions/setup-python@v5",
81
+ " with:",
82
+ ' python-version: "3.12"']
83
+ for c in _install_cmds(r) + extra:
84
+ L += [f' - run: "{c}"']
85
+ for name, g in gates.items():
86
+ L += [f" - name: gate {name}", f' run: "{g["cmd"]}"']
87
+ if not gates:
88
+ L += [" - name: no gates detected", " run: \"echo 'run harness init to add gates'\""]
89
+ return "\n".join(L) + "\n"
90
+
91
+
92
+ def cmd_ci(a) -> int:
93
+ r = find_root(a.repo)
94
+ if not r:
95
+ die("run `harness init` first")
96
+ v = jload(r / AE / "verification.json")
97
+ floor = a.fail_under
98
+ if floor is None:
99
+ measured = scan(r)["readiness"]
100
+ floor = min(70, measured)
101
+ if measured < 70:
102
+ print(f"Note : repo readiness is {measured}/100 — clamped floor to {floor}; "
103
+ "pass --fail-under explicitly to override")
104
+ dst = r / WORKFLOW
105
+ if dst.exists() and MARKER not in dst.read_text() and not a.force:
106
+ die(f"{WORKFLOW} is hand-written; keep it or re-run with --force")
107
+ dst.parent.mkdir(parents=True, exist_ok=True)
108
+ dst.write_text(_yaml(r, v.get("gates", {}), floor, a.setup or [],
109
+ a.install or _harness_install(r)))
110
+ print(f"Wrote : {WORKFLOW} ({len(v.get('gates', {}))} gates, floor {floor}) — commit it; "
111
+ "GitHub Actions runs it on push/PR")
112
+ return 0
harness/cli.py ADDED
@@ -0,0 +1,134 @@
1
+ """Argparse surface. Exit codes: 0 ok, 1 policy/contract failure or die(), 2 reserved for guard block."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from harness import __version__
7
+ from harness.agents import AGENTS, cmd_agents
8
+ from harness.bundle import cmd_policy
9
+ from harness.ci import cmd_ci
10
+ from harness.contract import cmd_list, cmd_run, cmd_start, cmd_update
11
+ from harness.eval import VARIANTS, cmd_eval
12
+ from harness.guard import cmd_guard
13
+ from harness.policy import LVL, cmd_init
14
+ from harness.pr import cmd_pr
15
+ from harness.scan import cmd_scan
16
+ from harness.verify import cmd_verify
17
+
18
+
19
+ def main(argv=None) -> int:
20
+ p = argparse.ArgumentParser(prog="harness",
21
+ description="Local-first control plane that professionalizes how "
22
+ "coding agents work on a repo: scan -> init -> task start "
23
+ "-> (task run) -> verify -> pr.")
24
+ p.add_argument("--version", action="version", version=f"harness {__version__}")
25
+ p.add_argument("--repo", default=".")
26
+ s = p.add_subparsers(dest="c", required=True)
27
+
28
+ x = s.add_parser("scan", help="detect stacks, gates, protected paths; score agent-readiness")
29
+ x.add_argument("--json", action="store_true")
30
+ x.add_argument("--fail-under", type=int, metavar="N", help="exit 1 when readiness scores below N (CI gate)")
31
+ x.set_defaults(f=cmd_scan)
32
+
33
+ x = s.add_parser("init", help="write .ai-engineering/, CLAUDE.md, AGENTS.md and .claude/settings.json")
34
+ x.add_argument("--force", action="store_true")
35
+ x.set_defaults(f=cmd_init)
36
+
37
+ s.add_parser("guard", help=argparse.SUPPRESS).set_defaults(f=cmd_guard)
38
+
39
+ x = s.add_parser("agents", help="show supported agent binaries and enforcement tiers")
40
+ x.add_argument("--json", action="store_true", help="machine-readable JSON output")
41
+ x.set_defaults(f=cmd_agents)
42
+
43
+ t = s.add_parser("task", help="manage contracts").add_subparsers(dest="t", required=True)
44
+ x = t.add_parser("start")
45
+ x.add_argument("name")
46
+ x.add_argument("--goal", required=True)
47
+ x.add_argument("--accept", action="append")
48
+ x.add_argument("--allow", action="append")
49
+ x.add_argument("--risk", choices=list(LVL), default="medium")
50
+ x.set_defaults(f=cmd_start)
51
+ x = t.add_parser("run", help="optional headless agent run inside the worktree")
52
+ x.add_argument("name")
53
+ x.add_argument("--agent", choices=list(AGENTS), default="claude")
54
+ x.set_defaults(f=cmd_run)
55
+ x = t.add_parser("update", help="amend a contract (goal/accept/allow/risk)")
56
+ x.add_argument("name")
57
+ x.add_argument("--goal")
58
+ x.add_argument("--accept", action="append")
59
+ x.add_argument("--allow", action="append")
60
+ x.add_argument("--risk", choices=list(LVL))
61
+ x.set_defaults(f=cmd_update)
62
+ x = t.add_parser("list")
63
+ x.set_defaults(f=cmd_list)
64
+
65
+ x = s.add_parser("verify", help="run gates + scope + review, emit evidence")
66
+ x.add_argument("name")
67
+ x.set_defaults(f=cmd_verify)
68
+
69
+ x = s.add_parser("pr", help="gh-backed PR operations for task branches")
70
+ prs = x.add_subparsers(dest="prc", required=True)
71
+ y = prs.add_parser("create", help="commit, push, gh pr create (requires PASS evidence)")
72
+ y.add_argument("name")
73
+ y = prs.add_parser("view", help="show the PR opened for the task branch")
74
+ y.add_argument("name")
75
+ prs.add_parser("status", help="gh pr status")
76
+ prs.add_parser("list", help="gh pr list")
77
+ x.set_defaults(f=cmd_pr)
78
+
79
+ x = s.add_parser("policy", help="sync the team policy bundle (policy.json + verification.json) with a Git repo")
80
+ ps = x.add_subparsers(dest="polc", required=True)
81
+ for nm, hp in (("push", "publish local policy + gates to the bundle repo"),
82
+ ("pull", "adopt the bundle as the local team standard"),
83
+ ("status", "exit 1 when the local bundle differs from the remote")):
84
+ y = ps.add_parser(nm, help=hp)
85
+ y.add_argument("remote", help="bundle repo path or URL")
86
+ y.add_argument("--branch", default="main")
87
+ x.set_defaults(f=cmd_policy)
88
+
89
+ x = s.add_parser("ci", help="generate .github/workflows/harness.yml (readiness floor + gates)")
90
+ x.add_argument("--fail-under", type=int, metavar="N",
91
+ help="readiness floor in the workflow (default: min(70, the repo's measured score))")
92
+ x.add_argument("--setup", action="append", metavar="CMD",
93
+ help="extra step the gates need before they can run (repeatable), e.g. "
94
+ '"cd web && npx playwright install --with-deps chromium"')
95
+ x.add_argument("--install", metavar="CMD",
96
+ help="how CI installs the harness CLI (default: this repo when it is harness, "
97
+ "otherwise `pip install harness-agent`)")
98
+ x.add_argument("--force", action="store_true", help="overwrite a hand-written workflow")
99
+ x.set_defaults(f=cmd_ci)
100
+
101
+ x = s.add_parser("eval", help="P7: run the raw-vs-harnessed corpus and report metrics")
102
+ evs = x.add_subparsers(dest="ev", required=True)
103
+ evs.add_parser("list", help="show the task matrix").set_defaults(f=cmd_eval)
104
+ y = evs.add_parser("run", help="run evals (fake agent with --dry; provider calls otherwise)")
105
+ y.add_argument("--task", help="only this task id")
106
+ y.add_argument("--agent", choices=[*AGENTS, "all"], default="all")
107
+ y.add_argument("--variant", choices=[*VARIANTS, "all"], default="all")
108
+ y.add_argument("--dry", action="store_true", help="no provider calls: fake no-op agent, plumbing proof")
109
+ y.add_argument("--keep", action="store_true", help="keep throwaway workdirs for post-mortem")
110
+ y.set_defaults(f=cmd_eval)
111
+ evs.add_parser("report", help="render evals/report.html from evals/results.jsonl").set_defaults(f=cmd_eval)
112
+
113
+
114
+ x = s.add_parser("serve", help="local API server for the dashboard (needs server extra)")
115
+ x.add_argument("--host", default="127.0.0.1")
116
+ x.add_argument("--port", type=int, default=8766)
117
+ x.add_argument("--max-parallel", type=int, default=2, help="concurrent agent/verify runs (plan §4.3)")
118
+ x.set_defaults(f=_serve)
119
+
120
+ a = p.parse_args(argv)
121
+ return a.f(a) or 0
122
+
123
+
124
+ def _serve(a) -> int:
125
+ try:
126
+ from harness.server import cmd_serve
127
+ except ImportError:
128
+ from harness.policy import die
129
+ die("fastapi/uvicorn missing; run `uv sync --extra server` or `uv tool install harness-agent --with fastapi --with uvicorn`")
130
+ return cmd_serve(a)
131
+
132
+
133
+ if __name__ == "__main__":
134
+ sys.exit(main())
harness/contract.py ADDED
@@ -0,0 +1,100 @@
1
+ """Task contracts: start/update/list and the optional headless agent run."""
2
+
3
+ import shutil
4
+ import time
5
+ from pathlib import Path
6
+
7
+ from harness.policy import AE, LVL, die, find_root, jload, jsave
8
+ from harness.shellx import git, sh
9
+
10
+
11
+ def tdir(r: Path) -> Path:
12
+ return r / AE / "tasks"
13
+
14
+
15
+ def load_task(a) -> tuple:
16
+ r = find_root(a.repo)
17
+ if not r:
18
+ die("run `harness init` first")
19
+ p = tdir(r) / f"{a.name}.json"
20
+ if not p.exists():
21
+ die(f"no such task: {a.name}")
22
+ return r, jload(p)
23
+
24
+
25
+ def cmd_start(a) -> int:
26
+ r = find_root(a.repo)
27
+ if not r:
28
+ die("run `harness init` first")
29
+ wt = r.parent / ".harness-worktrees" / f"{r.name}-{a.name}"
30
+ br = f"harness/{a.name}"
31
+ base = git(r, "rev-parse", "HEAD")[1]
32
+ rc, out, _ = git(r, "worktree", "add", "-b", br, str(wt))
33
+ if rc:
34
+ die(out)
35
+ for p in (AE, ".claude", "CLAUDE.md", "AGENTS.md"):
36
+ s, d = r / p, wt / p
37
+ if s.exists() and not d.exists():
38
+ if s.is_dir():
39
+ shutil.copytree(s, d, ignore=shutil.ignore_patterns("tasks", "evidence", "audit.log"))
40
+ else:
41
+ shutil.copy2(s, d)
42
+ v = jload(r / AE / "verification.json")
43
+ pol = jload(r / AE / "policy.json")
44
+ gates = {k: g for k, g in v["gates"].items() if LVL[g["min_risk"]] <= LVL[a.risk]}
45
+ con = {"name": a.name, "goal": a.goal, "accept": a.accept or [], "risk": a.risk, "branch": br,
46
+ "worktree": str(wt), "base": base, "allowed": a.allow or ["**"], "protected": pol["protected"],
47
+ "gates": gates, "review": v.get("review", True) and a.risk != "low", "created": int(time.time())}
48
+ jsave(tdir(r) / f"{a.name}.json", con)
49
+ print(f"Worktree : {wt}\nBranch : {br}\nGates : {', '.join(gates) or 'none (!)'}")
50
+ print(f"Next : harness task run {a.name} | harness verify {a.name}")
51
+ return 0
52
+
53
+
54
+ def cmd_update(a) -> int:
55
+ r, c = load_task(a)
56
+ if a.goal:
57
+ c["goal"] = a.goal
58
+ if a.accept:
59
+ c["accept"] = a.accept
60
+ if a.allow:
61
+ c["allowed"] = a.allow
62
+ if a.risk and a.risk != c["risk"]:
63
+ c["risk"] = a.risk
64
+ v = jload(r / AE / "verification.json")
65
+ c["gates"] = {k: g for k, g in v["gates"].items() if LVL[g["min_risk"]] <= LVL[a.risk]}
66
+ c["review"] = v.get("review", True) and c["risk"] != "low"
67
+ jsave(tdir(r) / f"{c['name']}.json", c)
68
+ print(f"Updated : {c['name']}\nRisk : {c['risk']}\nGates : {', '.join(c['gates']) or 'none (!)'}")
69
+ print(f"Allowed : {', '.join(c['allowed'])}")
70
+ return 0
71
+
72
+
73
+ def cmd_list(a) -> int:
74
+ r = find_root(a.repo)
75
+ if not r:
76
+ die("run `harness init` first")
77
+ d = tdir(r)
78
+ rows = []
79
+ for p in sorted(d.glob("*.json")) if d.is_dir() else []:
80
+ c = jload(p)
81
+ ep = r / AE / "evidence" / f"{c['name']}.json"
82
+ verdict = jload(ep)["verdict"] if ep.exists() else "unverified"
83
+ rows.append(f"{c['name']:<20} {c['risk']:<7} {c['branch']:<28} {verdict}")
84
+ if not rows:
85
+ print("no tasks")
86
+ for row in rows:
87
+ print(row)
88
+ return 0
89
+
90
+
91
+ def cmd_run(a) -> int:
92
+ from harness.agents import agent_prompt, build_argv
93
+
94
+ _, c = load_task(a)
95
+ if not shutil.which(a.agent):
96
+ die(f"`{a.agent}` CLI not found; work in the worktree manually, then `harness verify`.")
97
+ rc, out, _ = sh(build_argv(a.agent, agent_prompt(c)), c["worktree"], timeout=3600)
98
+ if out:
99
+ print(out)
100
+ return rc