Skip to content

Commit 3bef784

Browse files
Merge pull request #147 from open-coder-ai/claude/r3-note-and-probe-harness-wiring
Record the Cursor event gap (R3), and wire harness + conformance into the probe
2 parents f6ae973 + 0c04e62 commit 3bef784

12 files changed

Lines changed: 461 additions & 56 deletions

docs/witness-skeleton.json

Lines changed: 51 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,53 @@
11
{
2-
"_note": "Generated by the armed initiative, wave 1 (W1, 2026-09-10). Each entry in `gaps` names one coverage gap the committed witness recording (src/agentseam/data/recordings/claude_code@2.1.263.json) does not close, the exact command that closes it, and the evidence-report shape (src/agentseam/evidence_report.py) that command's --report output takes. Copy one `report` object, run its `command`, and replace the blank fields with the real --report output verbatim -- do not hand-type a basis, date or version. tests/test_coverage_skeleton.py asserts every `report` below, exactly as committed, is REJECTED by evidence_report.validate() -- an unfilled block must never be mistaken for a witnessed one.",
3-
"gaps": [
4-
{
5-
"gate": "pre_tool",
6-
"trial": "escalate",
7-
"why": "claude_code@2.1.263 covers every pre_tool trial except escalate -- the one PreToolUse reading (permissionDecision: \"ask\") no vendor doc settles, so the reference driver refuses to guess it (src/agentseam/probe/reference_agent.py raises Undocumented there).",
8-
"command": "agentseam probe run --agent claude_code --event pre_tool --trial escalate --driver \"<your headless claude_code invocation, containing {prompt}>\" --agent-version <version> --record --report --reporter @yourhandle > escalate-pre_tool-report.json",
9-
"report": {
10-
"report_version": 1,
11-
"agent": "claude_code",
12-
"basis": "",
13-
"date": "",
14-
"driver": "real-agent",
15-
"event": "pre_tool",
16-
"version": "",
17-
"experiments": {}
18-
}
19-
},
20-
{
21-
"gate": "prompt_submit",
22-
"trial": null,
23-
"why": "No trial has ever been witnessed at prompt_submit for any agent -- claude_code's block/fail_mode/etc. claims for this gate rest on vendor-docs only.",
24-
"command": "agentseam probe run --agent claude_code --event prompt_submit --driver \"<your headless claude_code invocation, containing {prompt}>\" --agent-version <version> --record --report --reporter @yourhandle > prompt_submit-report.json",
25-
"report": {
26-
"report_version": 1,
27-
"agent": "claude_code",
28-
"basis": "",
29-
"date": "",
30-
"driver": "real-agent",
31-
"event": "prompt_submit",
32-
"version": "",
33-
"experiments": {}
34-
}
35-
},
36-
{
37-
"gate": "stop",
38-
"trial": null,
39-
"why": "No trial has ever been witnessed at stop for any agent -- including the sentinel re-fire asymmetry (experiment.py's own _blocked()) that has so far only been exercised through the reference driver, never a real one.",
40-
"command": "agentseam probe run --agent claude_code --event stop --driver \"<your headless claude_code invocation, containing {prompt}>\" --agent-version <version> --record --report --reporter @yourhandle > stop-report.json",
41-
"report": {
42-
"report_version": 1,
43-
"agent": "claude_code",
44-
"basis": "",
45-
"date": "",
46-
"driver": "real-agent",
47-
"event": "stop",
48-
"version": "",
49-
"experiments": {}
50-
}
51-
}
52-
]
2+
"_note": "Generated by the armed initiative, wave 1 (W1, 2026-09-10). Each entry in `gaps` names one coverage gap the committed witness recording (src/agentseam/data/recordings/claude_code@2.1.263.json) does not close, the exact command that closes it, and the evidence-report shape (src/agentseam/evidence_report.py) that command's --report output takes. Copy one `report` object, run its `command`, and replace the blank fields with the real --report output verbatim. `--driver harness` reads the invocation agentseam.harness records for the agent, so there is no driver string to write by hand; pass an explicit --driver instead if your install needs different flags -- do not hand-type a basis, date or version. tests/test_coverage_skeleton.py asserts every `report` below, exactly as committed, is REJECTED by evidence_report.validate() -- an unfilled block must never be mistaken for a witnessed one.",
3+
"gaps": [
4+
{
5+
"gate": "pre_tool",
6+
"trial": "escalate",
7+
"why": "claude_code@2.1.263 covers every pre_tool trial except escalate -- the one PreToolUse reading (permissionDecision: \"ask\") no vendor doc settles, so the reference driver refuses to guess it (src/agentseam/probe/reference_agent.py raises Undocumented there).",
8+
"command": "agentseam probe run --agent claude_code --event pre_tool --trial escalate --driver harness --agent-version <version> --record --report --reporter @yourhandle > escalate-pre_tool-report.json",
9+
"report": {
10+
"report_version": 1,
11+
"agent": "claude_code",
12+
"basis": "",
13+
"date": "",
14+
"driver": "real-agent",
15+
"event": "pre_tool",
16+
"version": "",
17+
"experiments": {}
18+
}
19+
},
20+
{
21+
"gate": "prompt_submit",
22+
"trial": null,
23+
"why": "No trial has ever been witnessed at prompt_submit for any agent -- claude_code's block/fail_mode/etc. claims for this gate rest on vendor-docs only.",
24+
"command": "agentseam probe run --agent claude_code --event prompt_submit --driver harness --agent-version <version> --record --report --reporter @yourhandle > prompt_submit-report.json",
25+
"report": {
26+
"report_version": 1,
27+
"agent": "claude_code",
28+
"basis": "",
29+
"date": "",
30+
"driver": "real-agent",
31+
"event": "prompt_submit",
32+
"version": "",
33+
"experiments": {}
34+
}
35+
},
36+
{
37+
"gate": "stop",
38+
"trial": null,
39+
"why": "No trial has ever been witnessed at stop for any agent -- including the sentinel re-fire asymmetry (experiment.py's own _blocked()) that has so far only been exercised through the reference driver, never a real one.",
40+
"command": "agentseam probe run --agent claude_code --event stop --driver harness --agent-version <version> --record --report --reporter @yourhandle > stop-report.json",
41+
"report": {
42+
"report_version": 1,
43+
"agent": "claude_code",
44+
"basis": "",
45+
"date": "",
46+
"driver": "real-agent",
47+
"event": "stop",
48+
"version": "",
49+
"experiments": {}
50+
}
51+
}
52+
]
5353
}

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ exclude = ["*.py.tmpl"]
132132
"src/agentseam/probe/experiment.py" = ["PLR0913"]
133133
"src/agentseam/probe/experiment_cli.py" = ["T201"]
134134
"src/agentseam/probe/experiment_report.py" = ["T201"]
135+
"src/agentseam/probe/conformance_report.py" = ["T201"]
135136
"src/agentseam/probe/recorded_driver.py" = ["T201", "PLR0913", "N818"]
136137
"src/agentseam/probe/reference_agent.py" = ["PLR0913", "N818"]
137138
# TODO(lint-adoption): this wave's Sonar/Checkstyle/FindBugs baseline (coding-standards.md

src/agentseam/adapters/_hook_json.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,19 @@
2222

2323

2424
def hj_reverse(cfg):
25-
"""Canonical event -> wire name: the naive inverse, then the entry's pinned overrides."""
25+
"""Canonical event -> wire name: the naive inverse, then the entry's pinned overrides.
26+
27+
One wire name per canonical event, which is a real limit and not an oversight (R3, gap 4).
28+
Cursor is where it bites: `pre_tool` pins to `preToolUse`, and Cursor honours `ask` only at
29+
`beforeShellExecution` / `beforeMCPExecution`. So installing at `pre_tool` forecloses `ask`
30+
before dispatch is ever reached -- the runtime degrade to `deny` is honest about it, but the
31+
install already chose. Deny-style policies, which is all that ships today, are unaffected.
32+
33+
The day an ask-style Cursor policy exists, this map has to select by decision dialect
34+
(deny -> `preToolUse`, ask -> `beforeShellExecution`) and become one-to-many. Recorded here
35+
rather than built, so the resolution is not re-litigated from scratch; the three facts it
36+
rests on are pinned in tests/test_cursor_ask_dialect.py.
37+
"""
2638
reverse = {}
2739
for name, canonical in cfg["events"].items():
2840
if canonical != UNKNOWN:

src/agentseam/conformance.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
#: Raised as a message constant so the text is not a literal inside the raise (TRY003).
5353
_NOTHING_TO_COMPARE = "no verdicts to compare: a policy that ran nowhere has no conformance"
5454

55+
#: Below this, there is no cross-vendor question to answer.
56+
_MIN_VENDORS = 2
57+
5558

5659
class UnrecordedVendorError(LookupError):
5760
"""A vendor in the comparison has no matrix row, so its divergence cannot be judged."""
@@ -85,6 +88,21 @@ def classify(verdicts, event, *, needs=NEEDS_BLOCK):
8588
raise ValueError(_NOTHING_TO_COMPARE)
8689
groups = _groups(verdicts)
8790

91+
# One vendor is not a comparison. It will always "agree", and reporting that as AGREED
92+
# claims a cross-vendor check that never happened -- the same mistake as reading unanimity
93+
# among incapable vendors as agreement, one row further out. Note this is about the number
94+
# of vendors ASKED, not how many can enforce: one capable vendor beside one excused one is
95+
# a real vendor-limit finding and still classifies as one.
96+
if len(verdicts) < _MIN_VENDORS:
97+
return _result(
98+
UNDECIDABLE,
99+
groups,
100+
event,
101+
(),
102+
reason="only %s was asked: conformance is a comparison, and one vendor cannot differ "
103+
"from anything" % next(iter(verdicts)),
104+
)
105+
88106
try:
89107
excused = tuple(sorted(a for a in verdicts if not capable(a, event, needs=needs)))
90108
except UnrecordedVendorError as exc:

src/agentseam/harness.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323

2424
from __future__ import annotations
2525

26+
import os as _os
27+
import shlex as _shlex
28+
import subprocess as _subprocess
29+
2630
from ._data import load
2731

2832
_DATA = load("harness.json")
@@ -39,6 +43,10 @@
3943
#: makes a run a measurement of the instructions rather than of the policy.
4044
INSTRUCTION_MARKERS = tuple(_DATA["instruction_markers"])
4145

46+
#: The token the probe's driver template carries where the prompt goes. Named by the probe,
47+
#: not by us -- experiment_driver.drive_real() substitutes a JSON-quoted string for it.
48+
DRIVER_PROMPT_SLOT = "{prompt}"
49+
4250
PROMPT_PLACEHOLDER = "__PROMPT__"
4351
MODEL_PLACEHOLDER = "__MODEL__"
4452

@@ -128,3 +136,32 @@ def isolation(agent):
128136
def creates_files_as(agent):
129137
"""Recorded prose on file ownership of what the agent creates, or None."""
130138
return row(agent).get("creates_files_as")
139+
140+
141+
def driver_command(agent, *, model=None, hooked=False):
142+
"""The recorded invocation as a probe driver template, with `{prompt}` where the prompt goes.
143+
144+
This is what removes the hand-typed driver string from a witness run: the registry already
145+
knows how to reach the vendor, so the operator names the agent and nothing else.
146+
147+
Quoted for the platform actually running it. `shlex.quote` is POSIX-only, and these runs
148+
happen on a Windows machine as often as not -- a POSIX-quoted argument there is not a
149+
slightly-wrong command line, it is a different one. The prompt slot is left unquoted on
150+
purpose: `drive_real` substitutes an already-quoted JSON string for it.
151+
"""
152+
slot = "\x00agentseam-prompt\x00"
153+
parts = argv(agent, slot, model=model, hooked=hooked)
154+
rendered = [DRIVER_PROMPT_SLOT if part == slot else _quote_one(part) for part in parts]
155+
if any(slot in part for part in rendered):
156+
raise NoHarnessError(
157+
"%r builds its prompt inside a larger argument, which this template cannot "
158+
"express; drive it with an explicit --driver string." % agent
159+
)
160+
return " ".join(rendered)
161+
162+
163+
def _quote_one(part):
164+
"""Shell-quote one argument the way the running platform's shell reads it."""
165+
if _os.name == "nt":
166+
return _subprocess.list2cmdline([part])
167+
return _shlex.quote(part)

src/agentseam/probe/cli.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import json
1111

12-
from . import experiment, experiment_probe, experiment_report, recorded_driver
12+
from . import conformance_report, experiment, experiment_probe, experiment_report, recorded_driver
1313

1414

1515
def cmd_list(_args):
@@ -48,6 +48,11 @@ def cmd_run(args):
4848
return experiment_report.render(results, agent=args.agent, event=args.event, driver=driver)
4949

5050

51+
def cmd_conformance(args):
52+
"""`agentseam probe conformance`: compare the recorded agents, gate on seam gaps."""
53+
return conformance_report.render(args.event)
54+
55+
5156
def add_subparser(sub, *, default_event):
5257
"""Wire `probe list`/`probe run` onto `sub` -- agentseam.cli's own top-level subparsers."""
5358
pr = sub.add_parser("probe", help="measure what an agent's hooks actually enforce, against a driver")
@@ -70,3 +75,7 @@ def add_subparser(sub, *, default_event):
7075
run.add_argument("--agent-version", help="the agent build these trials ran against")
7176
run.add_argument("--reporter", help="how you want crediting, e.g. @handle")
7277
run.set_defaults(fn=cmd_run, _parser=run)
78+
79+
conf = psub.add_parser("conformance", help="compare the recorded agents; exit non-zero on a seam gap")
80+
conf.add_argument("--event", default=None, choices=experiment.EVENTS, help="one gate (default: all recorded)")
81+
conf.set_defaults(fn=cmd_conformance)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Cross-vendor conformance over the committed recordings: where a witnessed run lands.
2+
3+
Each new witnessed run has been worth something only in the abstract -- one more row moving
4+
off vendor-docs. This is the concrete thing it buys: with two agents recorded at the same
5+
gate, every trial becomes a comparison, and `conformance.classify` says whether a difference
6+
is a true vendor property or this layer leaking a dialect.
7+
8+
Deliberately honest about the current state rather than impressive. One agent is recorded, so
9+
every comparison here reports `undecidable` -- one vendor cannot differ from anything. That
10+
reads as an empty result and is the correct one; the moment a second recording lands the same
11+
command starts answering. Nothing is fabricated to fill the table in the meantime.
12+
13+
Replay, not launch: `recorded_driver.run_trial` runs each recorded trial back through the real
14+
classifier without starting a process, so this is free to run in CI and measures exactly what
15+
a live run measured.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
from .. import conformance, recordings
21+
from . import recorded_driver
22+
23+
24+
def _measured(agent, trial, event):
25+
"""The one value `agent`'s recording measured for `trial` at `event`, or None."""
26+
try:
27+
result = recorded_driver.run_trial(agent, trial, event=event)
28+
except recorded_driver.NoRecording:
29+
return None
30+
measured = result.get("measured") or {}
31+
return next(iter(measured.values()), None) if measured else None
32+
33+
34+
def events_recorded():
35+
"""Every gate at least one recording covers, sorted."""
36+
found = set()
37+
for agent in recordings.agents():
38+
body = recordings.load_recording(agent) or {}
39+
found.update(body.get("events") or {})
40+
return sorted(found)
41+
42+
43+
def trials_recorded(event):
44+
"""Every trial at least one recording covers at `event`, sorted."""
45+
found = set()
46+
for agent in recordings.agents():
47+
body = recordings.load_recording(agent) or {}
48+
found.update(((body.get("events") or {}).get(event) or {}).get("trials") or {})
49+
return sorted(found)
50+
51+
52+
def compare(event):
53+
"""One row per trial at `event`: the per-agent verdicts and what their difference means."""
54+
rows = []
55+
for trial in trials_recorded(event):
56+
verdicts = {}
57+
for agent in recordings.agents():
58+
value = _measured(agent, trial, event)
59+
if value is not None:
60+
verdicts[agent] = value
61+
if not verdicts:
62+
continue
63+
rows.append({"trial": trial, "verdicts": verdicts, "result": conformance.classify(verdicts, event)})
64+
return rows
65+
66+
67+
def gaps(event=None):
68+
"""Every comparison that condemns the seam. Empty is the answer a green CI gate wants."""
69+
events = [event] if event else events_recorded()
70+
return [
71+
{"event": ev, **row} for ev in events for row in compare(ev) if row["result"]["call"] == conformance.SEAM_GAP
72+
]
73+
74+
75+
def render(event=None):
76+
"""Print the comparison and return an exit code: non-zero only for a seam gap."""
77+
witnessed = recordings.agents()
78+
print("recorded agents: %s" % (", ".join(witnessed) or "(none)"))
79+
if len(witnessed) < 2: # noqa: PLR2004 -- conformance._MIN_VENDORS, stated where it is read
80+
print(
81+
"a comparison needs two: with %d recorded, every trial below reads undecidable, which\n"
82+
"is the honest answer rather than an empty one. Witness a second agent at the same gate\n"
83+
"and these rows start deciding." % len(witnessed)
84+
)
85+
events = [event] if event else events_recorded()
86+
found = 0
87+
for ev in events:
88+
rows = compare(ev)
89+
if not rows:
90+
continue
91+
print("\n%s" % ev)
92+
for row in rows:
93+
verdicts = ", ".join("%s=%s" % (a, v) for a, v in sorted(row["verdicts"].items()))
94+
print(" %-10s %-12s %s" % (row["trial"], row["result"]["call"], verdicts))
95+
if row["result"]["call"] == conformance.SEAM_GAP:
96+
found += 1
97+
print(" %s" % row["result"]["reason"])
98+
if found:
99+
print("\n%d seam gap(s): vendors the matrix says are equally able measured differently." % found)
100+
return 1
101+
return 0

src/agentseam/probe/recorded_driver.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import sys
2626
from datetime import date
2727

28-
from .. import recordings
28+
from .. import harness, recordings
2929
from ..evidence_report import RECORDED_DRIVER, REFERENCE_DRIVER
3030

3131
#: The driver name tools/experiment.py dispatches on.
@@ -35,6 +35,11 @@
3535
#: evidence -- the same rule evidence_report.py enforces for a submitted report.
3636
NON_LIVE_DRIVERS = (REFERENCE_DRIVER, DRIVER_NAME)
3737

38+
#: Asks for the invocation `agentseam.harness` records for this agent, instead of a template the
39+
#: operator types. Opt-in by name rather than a new default: the resolution order below is what
40+
#: decides whether a run is evidence, and it should not change because a registry gained a row.
41+
HARNESS_DRIVER = "harness"
42+
3843

3944
class NoRecording(Exception):
4045
"""No recording covers the (agent, version, event, trial) asked for."""
@@ -54,8 +59,9 @@ def add_cli_args(run_parser):
5459
run_parser.add_argument(
5560
"--driver",
5661
default=None,
57-
help="'reference', 'recorded', or a shell template containing {prompt}; "
58-
"default: 'recorded' if a recording covers --agent at --event, else 'reference'",
62+
help="'reference', 'recorded', 'harness' (the invocation agentseam.harness records "
63+
"for --agent), or a shell template containing {prompt}; default: 'recorded' if a "
64+
"recording covers --agent at --event, else 'reference'",
5965
)
6066
run_parser.add_argument(
6167
"--record", action="store_true", help="freeze this run into data/recordings/<agent>@<agent-version>.json"
@@ -66,6 +72,12 @@ def resolve_driver(agent, driver, event=None, version=None):
6672
"""`driver` if given, else 'recorded' when a recording covers `agent` at `event`, else the
6773
reference. Per gate, not per agent: claude_code@2.1.263 recorded pre_tool only, and a run
6874
at stop must fall back to the reference rather than fail on a recording that never saw it."""
75+
if driver == HARNESS_DRIVER:
76+
# Expanded here, not carried as a token: everything downstream -- check_record_args,
77+
# the rendered table, the report's `driver` field -- then sees a real command line and
78+
# treats the run as live, which it is. A token would have had to be special-cased in
79+
# each of those places to avoid being mistaken for a non-live driver.
80+
return harness.driver_command(agent)
6981
if driver is not None:
7082
return driver
7183
return DRIVER_NAME if has_recording(agent, event, version) else REFERENCE_DRIVER

0 commit comments

Comments
 (0)