Repository navigation
feat(context, repo): consistency of repeated runs; pass^k and path stability in repo compare - #155
Conversation
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #155 +/- ##
==========================================
+ Coverage 93.21% 93.25% +0.04%
==========================================
Files 270 272 +2
Lines 34278 35117 +839
Branches 5766 5964 +198
==========================================
+ Hits 31952 32750 +798
- Misses 1526 1548 +22
- Partials 800 819 +19
🚀 New features to boost your workflow:
|
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 8b8bd09. The metrics (pass@k, pass^k, JSD, order distance, the C(c,K)/C(n,K) form in repo compare) are right, and the default repo compare output is unchanged. The problems are in the shell-step parsing, in how empty or unreadable input is reported, and in the strength of the tests. Inline comments cover the first ones; the test gap is below.
What I ran, on a Linux review host at 8b8bd09:
make ci-local: 24 passed, 2 failed, 6 skipped. 6759 tests passed, 1 failed:test_unbalanced_quote_in_hook_completes_quickly(1.25 s against a 1 s limit, under 8-worker load).pip-auditreports virtualenv 21.14.7 (CVE-2026-113722) from the existing locks, fixed onmainby #152. Neither comes from this PR. The changed-line coverage gate passed.- Reproductions for the code findings, with the LLM transport mocked (none of this code calls a model).
- A mutation run over the two new test files: 49 mutants applied, 21 pass the full suite.
Tests do not pin several rules. tests/test_context_consistency.py and tests/test_repo_compare.py survive these one-line mutants:
forbids:ok = not achanged took = Truepasses. The test contract hasforbids: push, but no fixture run pushes.before/after_last:a[0] < b[0]changed to<=, anda[-1]changed toa[0], both pass.passed_but_nonconforming: dropping ther.passed andcondition passes.- Metric denominators:
success_rateandpass^koverninstead ofjudgedpass, because no fixture has an unjudged run. run_from_events:rc != 0->Falseanddenied->Falseboth pass, so failure marking for--path-stabilityis untested.repo compare:if pass_k and not score_taskchanged toif pass_kpasses (no score-task test);order_distance(runs[:2])instead of all runs passes (the tests use two cells per arm);except ValueError: continuechanged toraisepasses.
Add a fixture per rule where the rule is violated, one arm with an unjudged run, and a three-cell path case.
Checked, no issues
- Metrics:
pass_at/pass_hatare guarded byk <= judged, JSD stays in 0..1, and the single-linkage grouping and CV are safe on zero or one sample. - Holm: the new measures are outside
PRIMARY, so the family size is unchanged. With the flags absent noevents.jsonlis read. - Repo rules: the new page is in
mkdocs.ymlnav and the docs index, the changelog fragment has the scriv format, no schema orSCHEMA_VERSIONSentry applies, and no generated files are committed. - Docs vs code: every flag and metric name in the docs matches the CLI help and the code.
|
@napetrov It passed at 8b8bd09 because Suggestion: in |
|
Review at 8b8bd09, test gaps, all pinned in 911a507 (each checked by applying the one-line mutant and seeing a test fail):
|
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 911a507. The fixes for the earlier threads hold: the "no runs" verdict, the clean error for malformed --outcomes, the refusal of a shared outcome key, the empty-trace and nested-line cases in repo compare, the max_docs bump and the --reps wording. The shell-step parser is still the weak part, and the new shell-edit rule has a side effect. Six new points below, plus one thread still open.
What I ran, on a Linux review host at 911a507:
make ci-local: 27 passed, 1 failed, 7 skipped. 7220 tests passed. The one failure ispip-audit(gitpython 3.2.0, CVE-2026-114145, fixed in 3.2.1) from the existing locks; the PR changes no lock file. The changed-line coverage gate passed.- Reproductions for the code findings, with no model involved (none of this code calls one).
- A mutation run over the two new test files: 72 mutants, 29 pass the full suite, 6 of those equivalent. The ones that matter are in point 2 below.
Still open from my earlier review:
dokimi/dokimi/context/consistency.py:207: the program name is cleaned, but the step's action andcommandstill carry the syntax words.for f in a; do black x; donegives the actionBash:black(do black x), a plainblack xgivesBash:black(black x), andseq_distancebetween them is 1.0. Soaction_distancestill splits identical strategies, and a matchercommand: ^blackdoes not match the loop step ([False]against[True]for the bare command). The same holds forif pytest -q; then ...; fi(Bash:pytest(if pytest -q)) and(cd x && pytest -q)(Bash:pytest(pytest))). Suggestion: build the action andcommandtext from the words after_strip_syntax, not from the raw segment.
Findings that cannot be anchored inline:
-
repo compare --pass-k/--path-stabilityfloodunpairedwith entries that are not unpaired tasks.compare.py:292-296adds an entry for every task that lacks the measure: score tasks (pass^K is gate-only), tasks with fewer than K cells, and tasks with fewer than two traced cells. The reason reads "no pass^2 in baseline or treatment", which is wrong for a score task.repo rundefaults to--reps 1, so--path-stabilityon a default batch lists every task. Repro: 15 tasks with one cell per arm plus one task only in the baseline;compare(..., pass_k=2, path_stability=True)returns 31 unpaired entries, and the text shows ten "no pass^2 in baseline or treatment" lines and "and 21 more". Without the flags the same batch prints one line,zz_only_base (m-1, no cells in treatment), the only real one. Suggestion: keep a task out ofunpairedwhen the measure does not apply to it, and report the count of tasks without the measure in one line. -
Tests still do not pin several rules added in the fixes. Mutation check, one-line mutants that pass both new test files (140 tests):
Matcher.__call__: droppingand set(self.tools) & _WRITE_TOOLS(sotool: Readalso matches a shell write) passes.parse_run:except (ValueError, RecursionError)changed toexcept ValueErrorpasses; only_cell_pathhas the nested-line test. A trace line of"[" * 100000still abortscontext consistency._written: droppingprev != "1"or the(?<!\S)lookbehind in_fd_redirect, removing">|"from the operators,len(files) >= 2changed to>= 1(sosed -i 's/a/b/'with no file reports a file), andteewithout the pipe-or-start condition (echo tee out.txtbecomes a write) all pass.apply_outcomes: removing the stem match passes.load_outcomes: acceptingintvalues passes.Run.shape:if not out or out[-1] != kchanged toif Truepasses, so nothing checks that repeats collapse.
Add one case per line:_written("echo tee out.txt") == "",_written("sed -i 's/a/b/'") == "",_written("echo x >| f") == "f", a Read matcher against ased -istep, a stem key fordir/run1.jsonl,load_outcomeson{"a": 1},parse_runon a 200000-bracket line, and runsRead Read Read Edit/Read Editas one strategy.
|
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 5a11251. The fixes since 911a507 hold: a saved test log (pytest > test.log, git diff > f.patch) is no longer an edit, and a trace given twice counts once. Four of the earlier threads are still open, and three new points below.
What I ran, on a Linux review host at 5a11251:
make ci-local: 27 passed, 1 failed, 7 skipped. 7229 tests passed. The failure ispip-auditon the existing lock files, which the PR does not touch.- Reproductions for the code findings, with no model involved (none of this code calls one).
- Mutants from my last review re-applied to the head:
perl/tee/>|/lookbehind/len(files) >= 2/RecursionError/intoutcome/Run.shapestill pass both test files (see point 1).
Still open from my earlier review:
dokimi/dokimi/context/consistency.py:227:_segmentsstill mis-splits\;,$(...),&andcase.find . -name '*.pyc' -exec rm {} \; -printgivesBash:find,Bash:-print;echo don\'t; lsis one step;make & pytestlosespytest;case $x in a) make ;; b) ls ;; esacgives onlyBash:b).consistency.py:207: the step's action andcommandkeep the syntax words.for f in a; do black x; donegivesBash:black(do black x)againstBash:black(black x)forblack x,action_distance1.0, andcommand: ^blackis[False]against[True].if pytest -q; then make; figivesif pytest -q/then make;(cd x && pytest -q)leavespytest -q).consistency.py:441and:427:parse_contractstill leaksTypeErrorforrequires: 5,requires: [[a]]andtool: 5, and the CLI prints a traceback.requires: {a: 1}is accepted as[a].consistency.py:569: a trace with tool calls and noresultevent stays unjudged and drops out ofsuccess_rate,pass@kandpass^k. The text table showsrunsbut notjudged, and nothing warns.
Findings that cannot be anchored inline:
- Tests still do not pin several rules. Re-applied one-line mutants that pass
test_context_consistency.pyandtest_repo_compare.py(149 tests):
except (ValueError, RecursionError)inparse_runchanged toexcept ValueError;(?<!\S)removed from_fd_redirect, and">|"removed from the operators;len(files) >= 2changed to>= 1(_written("sed -i s/a/b/")then returnss/a/b/);- the
teepipe-or-start condition madeTrue(_written("echo tee x")returnsx); load_outcomesaccepting{"a": 1};Run.shapewithif True(repeats no longer collapse);perlremoved from_SHELL_WRITERS, since no test usesperl -pi.
Add one case per line:_written("echo tee x") == "",_written("sed -i s/a/b/") == "",_written("echo x >| out.txt") == "out.txt",_written("echo a2>/dev/null 2 > f") == "f",load_outcomeson{"a": 1}raising,parse_runon a 200000-bracket line, runsRead Read EditandRead Editas one shape, and aperl -pi -e s/a/b/ src/a.pyedit.
I agree with @xaleryb on the growth ratchet and on the timeout / nohup / nice wrapper steps. New evidence: ruff --select C901 at max 10 reports cmd_context_consistency 17 and parse_run 14, and timeout 300 pytest gives the step Bash:timeout.
|
Review-body items at 5a11251 and the open points of @xaleryb, mapped to commits (merge of main in bba5f9c; the rest pushed as 97f9581 and f4fa3ce):
|
CLI interface change detectedThis PR changes dokimi's user-facing CLI surface (commands and/or options): + added command `dokimi context consistency`
+ argument `traces`
+ option `--arm`
+ option `--contract`
+ option `--help/-h`
+ option `--json`
+ option `--outcomes`
+ option `--shell`
+ option `--threshold`
~ `dokimi repo compare` parameters changed:
+ added option `--pass-k`
+ added option `--path-stability`Updated on every push; the |
antonwolfy
left a comment
There was a problem hiding this comment.
Review at e696966. All six open threads are fixed: the compound-syntax and wrapper handling, the scanner, parse_contract errors, a run with no result event, the unpaired count and the prefixed writes. The remaining problems are in the new shell scanner, which still yields wrong steps for a few common forms, and one test gap. Three bugs and two risks are inline.
What I ran, on a Linux review host at e696966:
make ci-local: 28 passed, 1 failed, 7 skipped. 7,895 tests passed. The changed-line coverage gate passed. The one failure ispip-audit(filelock 3.32.7, setuptools 84.0.0) on the existing lock files; this PR changes no lock file.- Reproductions for the code findings, with no model involved (none of this code calls one).
- A mutation run over the four new test files; the survivor I re-ran and kept is the
run_from_eventsmapping below.
Checked, no issues
- Metrics:
pass^KisC(c,K)/C(n,K)and is skipped when a task has fewer than K cells;path_distanceis the mean over all pairs of cells (three cells give 2/3); empty, garbage and deeply nestedevents.jsonllines are skipped, not raised. - Unpaired: score tasks are left out for
pass^K, and tasks with too few cells are one line with a count. - Repo rules: the docs page is in
mkdocs.ymlnav and the docs index, the changelog fragments are present, no schema applies, and no generated files are committed. - Scanner:
\;,$(...), backticks, a lone&,caselabels,for/if/while/(...)syntax words,sudo/env/uv run/timeoutprefixes,>|, and heredoc bodies all give the right steps.
There was a problem hiding this comment.
Review at a22b8c5. My five earlier threads are fixed and pinned: the { } group, the wrapper value options, $# in a word, the &> / >& writes with multi-file sed -i, and the read/edit mapping. Three points remain: two bugs in the new forms and one test gap.
What I ran, on a Linux review host at a22b8c5:
make ci-local: 29 passed, 0 failed, 7 skipped. 8,371 tests passed, 2 skipped. Changed-line coverage 96% (734 lines).- Reproductions for the code findings, with no model involved (none of this code calls one).
- A mutation run over the changed shell code: 33 mutants, 8 survive the full suite; the ones that matter are in the last inline comment.
Checked, no issues
- Threads:
{ ruff check .; pytest; },{ make; }and{ a; b; } > f.txtgive no}step.echo $# > out.txtandecho ${#x} > out.txtwriteout.txt,echo ${#x} | grep 1is masked, and a real# commentis still cut.echo hi &> f,&>>and>& fwritef, while>&2,2>&1and>&-do not. - Read/edit:
tool = "Read"for every edit event, and a read/edit swap, both failtest_a_read_and_an_edit_are_different_steps_for_path_stability. - Calls vs steps: a multi-file
sed -igives one step per file, andtool_calls_*/failed_calls_meanstill count calls.
|
Thanks for the review at a22b8c5. Each open item -> sha:
Ready for another look. |
| before = text | ||
| text = re.sub(rf"^(?:\(+|{_KEYWORDS}(?=\s|$))\s*", "", text) | ||
| text = re.sub(r"^case\b.*?\bin\b\s*", "", text) if text.startswith("case ") else text | ||
| text = re.sub(r"^\(?[^\s()]+\)(?=\s+\S)\s*", "", text) # a `case` label followed by its command |
There was a problem hiding this comment.
This case-label pattern also matches a one-word command that closes a subshell when a redirect or pipe follows it. (cd build && make) 2>&1 | tail -5 gives one step Bash:2>&1 with the command 2>&1 | tail -5, so make is lost, and (cd sub; pytest) > log.txt gives Bash:>. A contract requires: {tool: Bash:make} then reports that the run never built. Please peel a word) label only inside a case ... in block, and add these two lines to the tests.
There was a problem hiding this comment.
Fixed in 70da7a6: a word) is a case label only inside case ... in (after the header or ;;); an unopened ) elsewhere closes a subshell. (cd build && make) 2>&1 | tail -5 gives Bash:make, (cd sub; pytest) > log.txt gives Bash:pytest; both lines plus nested/after-esac cases are in test_context_shell.py.
There was a problem hiding this comment.
Fixed on the branch at e8f0174: a word) is a case label only inside a case ... in block, and elsewhere an unopened ) closes a subshell. The two lines from the review now give make and pytest, and both are in test_context_shell.py.
| raise ValueError(f"{where}: action {name!r} is not a mapping") | ||
| tools = _names(spec.get("tool"), f"{where}: action {name!r}: `tool`") | ||
| try: | ||
| patterns = {str(k): re.compile(str(v)) for k, v in spec.items() if k != "tool"} |
There was a problem hiding this comment.
A pattern value is compiled with str(v), so a list becomes a regex character class: command: [pytest, tox] compiles to ['pytest', 'tox'], which matches almost any command, and requires: test is met by git status. tool already refuses anything but a name or a list of names. Please refuse a non-string pattern with a ValueError, or join a list of strings into one alternation.
There was a problem hiding this comment.
Fixed in 70da7a6: a list of strings is joined into one alternation (command: [pytest, tox] no longer matches git status); a number, mapping or empty list is a ValueError. Tests added.
There was a problem hiding this comment.
Fixed on the branch at e8f0174: a list of strings is one alternation, so command: [pytest, tox] no longer matches git status, and a number, mapping or empty list is refused with a ValueError. Both cases have tests.
| prog = shell.program(seg) | ||
| if not prog: | ||
| continue | ||
| sep = sep if any(shell.clean(later) for later, _ in parts[i + 1:]) else "" # `done`, `fi` close it |
There was a problem hiding this comment.
Clearing the separator when only done/fi follow also clears it where the status is not the command's. pytest -q & always reports 0, but its step is not masked, so last_ok: test passes. In for f in a b; do pytest $f; done the status is the last iteration's, so a failure in the first one reads as a pass. Please keep a trailing & as hiding, and mark a step inside a for/while/until body as masked.
There was a problem hiding this comment.
Fixed in 70da7a6: a trailing & keeps hiding, and a step inside a for/while/until/select body is masked (not under set -e). Loop headers are read as shell words, so quoted for or $(for ...) does not mask. Rows added to the hidden-status test.
There was a problem hiding this comment.
Fixed on the branch at e8f0174: a trailing & keeps hiding, and a step inside a for, while, until or select body is masked unless set -e is on. Loop headers are read as shell words, so a quoted or substituted for does not mask. The rows are in the hidden-status test.
| raw_usage = message.get("usage") | ||
| usage: dict[str, Any] = raw_usage if isinstance(raw_usage, dict) else {} | ||
| mid = str(message.get("id") or id(rec)) | ||
| seen_messages[mid] = max(seen_messages.get(mid, 0), int(usage.get("output_tokens") or 0)) # one call, many blocks |
There was a problem hiding this comment.
int(usage.get("output_tokens") or 0) raises on a value that is not a number: "n/a" gives ValueError and Infinity (which json.loads accepts) gives OverflowError. parse_run has no guard for it and the command catches only TraceError, so context consistency ends in a traceback. Please skip a count that is not a finite number, or catch these errors per line as context/trace.py does.
There was a problem hiding this comment.
Fixed in 70da7a6: a count that is not an int or a finite float ("n/a", Infinity, NaN, bool, list) is skipped; a huge int is still counted. Tests added.
There was a problem hiding this comment.
Fixed on the branch at e8f0174: a usage count that is not an int or a finite float is skipped, so n/a, Infinity and NaN no longer raise, and a huge integer is still counted. The tests cover each value.
| "tool_calls_mean": _mean([r.calls() for r in runs]), | ||
| "tool_calls_cv": _cv([r.calls() for r in runs]), | ||
| "turns_cv": _cv([r.num_turns for r in runs if r.num_turns is not None]), | ||
| "output_tokens_cv": _cv([r.output_tokens for r in runs]), |
There was a problem hiding this comment.
output_tokens starts at 0, so traces without usage give output_tokens_cv == 0.0: a token spread nobody measured reads as perfectly stable. Please keep output_tokens as None when no message had a usage count, and leave such runs out of the CV, as turns_cv does.
There was a problem hiding this comment.
Fixed in 70da7a6: output_tokens is None when no message had a usage count, and such runs stay out of output_tokens_cv, as turns_cv does.
There was a problem hiding this comment.
Fixed on the branch at e8f0174: output_tokens is None when no message had a usage count, and such runs are left out of output_tokens_cv, as turns_cv does. test_traces_without_usage_have_no_token_spread pins it.
| groups.append({ | ||
| "runs": [r.name for r in rs], "passed": ps, "judged": len(js), | ||
| "verdict": _verdict(ps, len(js)), | ||
| "inner_distance": _mean([seq_distance(a.shape(), b.shape()) for a, b in combinations(rs, 2)]) or 0.0, |
There was a problem hiding this comment.
_mean(...) or 0.0 gives a one-run strategy an inner_distance of 0.0, which reads as a tight group, but nothing was measured. Together with _summary_verdict, two passing runs on different paths read as "2 strategies, all stable success (valid diversity)" although each strategy has one run. Please keep None for a one-run strategy, and do not count "single run, passed" as stable success in the summary verdict.
There was a problem hiding this comment.
Fixed in 70da7a6: a one-run strategy has inner_distance: null, and only stable success groups make "valid diversity"; "single run, passed" no longer does. Documented in run-consistency.md.
There was a problem hiding this comment.
Fixed on the branch at e8f0174: a one-run strategy has inner_distance null, and single run, passed no longer counts as stable success for valid diversity. run-consistency.md says so, and a test pins the verdict.
|
|
||
| def _separator(command: str, i: int) -> str: | ||
| ch, nxt, prev = command[i], command[i + 1:i + 2], command[i - 1:i] if i else "" | ||
| if ch in ";\n": |
There was a problem hiding this comment.
&& and ; inside [[ ... ]] and (( ... )) split the line. [[ -n $x && -f y ]] && pytest gives a step Bash:-f, and for ((i=0; i<3; i++)); do pytest; done gives Bash:i<3 and Bash:i++. These fake steps change order_distance, action_distance and mix_jsd. Please track [[/]] and ((/)) in the scanner like $(...), and add both lines to the tests.
There was a problem hiding this comment.
Fixed in 70da7a6: the scanner tracks [[/]] and ((/)) like $(...); (( ... )) alone is no step. Both lines are in the tests.
There was a problem hiding this comment.
Fixed on the branch at e8f0174: the scanner tracks [[ ]] and (( )) like $( ), so && and ; inside them no longer split a line. [[ -n $x && -f y ]] && pytest and the C-style for loop now give only pytest, and both lines are in the tests.
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 2e11ee6. The three threads from my last review are fixed and pinned, but eight points from @vlad-perevezentsev's review at a22b8c5 still reproduce at this head, so this is not ready to merge. One nit inline.
I agree with @vlad-perevezentsev's eight points from a22b8c5; each still reproduces at this head.
What I ran, on a Linux review host at 2e11ee6:
make ci-local: 28 passed, 1 failed, 7 skipped. 8,572 tests passed, 1 failed:test_unbalanced_quote_in_hook_completes_quickly(1.15 s against a 1 s limit under 8-worker load; it passes alone in 0.73 s, and the PR does not touch that code). Changed-line coverage 96% (738 lines).- Reproductions for the code findings, with no model involved (none of this code calls one).
- A mutation run over the changed shell code: 38 mutants, 29 killed. The four mutants from my thread are all killed; the survivors are in the inline nit.
Checked, no issues
- Wrapper options:
uv run --extra dev pytest,--group dev,sudo --user bob make,env --unset FOO make,timeout --signal KILL 5 makeandnice --adjustment 5 makegivepytest/makeas the program. sed -i:--expression=s/a/b/ a.py,-es/a/b/ a.py,-fa.sed, BSD-i '',-i s/a/b/ f > log.txtand2>err.logeach give the one real file.
antonwolfy
left a comment
There was a problem hiding this comment.
I pushed a commit per nit, up to d2e8f51, which fix the nit from my review:
d2e8f51:dokimi/tests/test_context_shell.py:92: added the two sed script-flag rows and the five sudo/env/time/timeout prefix rows from the suggestion.
make ci-local at d2e8f51: 29 passed, 0 failed, 7 skipped.
Not approving yet. Still open: @vlad-perevezentsev on shell.py: command -v pytest is read as running pytest (Bash:pytest step, satisfies requires/last_ok); @vlad-perevezentsev on shell.py:167: (cd build && make) 2>&1 | tail -5 gives only Bash:2>&1 and (cd sub; pytest) > log.txt gives Bash:>; make/pytest lost; @vlad-perevezentsev on shell.py: [[ -n $x && -f y ]] && pytest gives Bash:-f; for ((i=0; i<3; i++)) gives Bash:i<3 and Bash:i++; @vlad-perevezentsev on shell.py: find -exec sed -i ... {} + and xargs sed -i give no file_path, so requires: edit reports a missing edit; not documented as a limit; @vlad-perevezentsev on consistency.py:257: pytest -q & and a for ...; do pytest; done loop are not masked, so last_ok: test passes; @vlad-perevezentsev on consistency.py:161: non-numeric output_tokens ("n/a", Infinity) raises and ends context consistency in a traceback; @vlad-perevezentsev on consistency.py:513/526: no-usage traces give output_tokens_cv 0.0; one-run strategies give inner_distance 0.0 and read as 'all stable success (valid diversity)'; @vlad-perevezentsev on consistency.py:359: a list pattern value is compiled via str(v) into a character class; command: [pytest, tox] matches git status.
|
@antonwolfy the open list in your review at d2e8f51, mapped to commits (head now a8572f0):
Also merged intel/main (bcbb0d9) and raised |
antonwolfy
left a comment
There was a problem hiding this comment.
Review at a8572f0. The fixes since d2e8f51 hold: each point from the last round now gives the right step or value, and I found no crash in the new code. Two things are worth fixing before merge: the trailing-& fix covers only the command right before the &, and the new masking and bracket code has several lines no test pins.
What I ran, on Linux at a8572f0:
make ci-local: 26 passed, 7 skipped, 2 failed. 9,214 tests passed. Changed-line coverage 96% (802 lines). The two failures areInstall gitleaks(the download of the binary returned HTTP 504) andScan commits, which needs that binary; neither is caused by the PR.gitleaks detectoverorigin/main..HEADby hand reports no leaks.- Reproductions for the code findings, with no model involved (none of this code calls one).
- A mutation run over the four test files for the new code: 78 one-line mutants, 35 survive, 15 of those are equivalent. The ones that matter are in the inline test comment.
Checked, no issues
- Delta:
command -v pytestis aBash:commandstep;(cd build && make) 2>&1 | tail -5and(cd sub; pytest) > log.txtgivemake/pytest;[[ -n $x && -f y ]] && pytestandfor ((i=0; i<3; i++))give no fake step;command: [pytest, tox]no longer matchesgit status;output_tokensof"n/a",Infinity,NaN,trueis skipped; a one-run strategy hasinner_distancenull and is not "valid diversity". - Output:
context consistencyin text and--jsonruns on traces without usage and on one-run arms;nullis printed, nothing crashes. - Repo rules: the docs page is in the
mkdocs.ymlnav, the changelog fragments are present,test_growth_ratchet.pypasses, no schema applies, no generated files are committed.
|
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 94c521b. The && list forms from the last round are fixed: pytest -q && echo ok &, pytest -q && echo PASS || echo FAIL and the set -e / pipefail reading now give the right result. Two of my earlier threads are still open (below), so this is not ready to merge. One nit inline.
What I ran, on Linux at 94c521b:
make ci-local: 26 passed, 2 failed, 7 skipped. 9,231 tests passed, 2 skipped. The changed-line coverage gate passed. The two failures areInstall gitleaks(the proxy returned 504 on the download) andScan commits, which needs that binary; the PR does not cause either.gitleaks detectoverorigin/main..HEADby hand reports no leaks.- Reproductions with
_bash_events, no model involved (none of this code calls one). - A mutation run over the four test files for the new code: 126 one-line mutants, 66 survive, 18 of those equivalent.
Still open from my earlier review:
dokimi/dokimi/context/consistency.py:293: a trailing&after a group closer still hides only the closer.{ pytest -q; } &givesBash:pytestmasked=Falseat both grains, andif x; then pytest -q; fi &givesBash:pytestmasked=False, though the shell reports 0 for both.last_ok: testpasses on them. The&&form (pytest -q && echo ok &) is fixed and pinned. Left: a}orfi/donedirectly before&must mask its whole group, with the two rows added to thelast_oktest.dokimi/tests/test_context_shell.py:166: the delta pins the&&/||list rules andsetparsing from a heredoc, comment orecho, but these one-line mutants still pass all four test files (361 tests):_maskswithoutloops -= ... done(for f in a; do pytest $f; done; makehidesmake);any->allin the separator-clearing line (pytest -q;becomes hidden);_opens_loopwithoutselect,until,(,!,elseordo(select x in a b; do pytest; done,(for f in a; do pytest; done),! while read f; do pytest $f; done);_set_optionswith"e" in w->w[1] == "e"(set -ue; pytest -q; echo donereads as hidden), without thesetguard (grep -e foo x; pytest -q; echo doneturns errexit on), and[a-zA-Z]->[a-z](set -Eeuo pipefail; pytest -q | tail -5; echo done);"\n"removed from the errexit check (set -e+ newline);_list_hideswithj = i + 1orany->all;bracketwithout(in the start set (([[ -f a && -f b ]]) && pytest -qgivesBash:-f);command -V pytestandcommand pytest -vwith the flag scan widened;_verdict(0, 1); the float token guard (9.0) andmax(...)for a message id seen twice (5 then 9, 9 then 5).
Checked, no issues
- Delta:
_list_hidesand_set_optionsgive the right result forpytest -q && echo ok || echo bad,pytest -q && git diff; echo done,cd x && pytest -q && git status(not hidden),set -euo pipefail,set -o errexit,set +e, andsetwords in a heredoc body, a comment,echo 'set -e'andgrep -r pipefail. - Repo rules: the changelog fragment keeps its
### Fixedform,test_growth_ratchet.pypasses, no new doc page needs a nav entry, no generated files are committed, the new tests run offline.
…s for pass^K Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…tance's too-few-cells line Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…for mypy Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
`_NO_COMMAND` needed a word boundary after `}`, which a lone brace never has, so
`{ ruff check .; pytest; }` gave a step named `}` and `{ make; }` hid the status
of `make` behind it. A `}` at the start of a segment now closes the group.
Thread: PRRT_kwDOVBg7g86wCjHA
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
`sudo -u bob make`, `env -u FOO make` and `uv run --with rich pytest` named `bob`, `FOO` and `--with` as the program. `time` and `command` took one word only, so `time -p pytest` named `-p`. Options that take a value skip it, and `time` / `command` skip their flags like the other wrappers. Thread: PRRT_kwDOVBg7g86wCjHF Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
`tokens` left shlex's default comment character on, so `echo $# > out.txt`
and `echo ${#x} | grep 1` lost the redirect or the pipe after `$#` and `${#x}`,
and a pipe that hid the status was not seen. The scanner already removes
comments before this point, so the lexer does not need its own.
Thread: PRRT_kwDOVBg7g86wCjHL
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
`run_from_events` maps an `edit` event to `Edit` and a `read` to `Read`, and no test built its events from anything but commands. The mapping passed the suite with `Edit` turned into `Read`; this pins it: the same two steps in another order are one reordering apart, and a read and an edit are different steps. Thread: PRRT_kwDOVBg7g86wCjHQ Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…ach file `echo hi &> out.txt` had no `file_path`: `&>`, `&>>` and `>&` were operators but not write redirects. `sed -i s/a/b/ a.py b.py` recorded only `b.py`. A multi-file edit now gives one step per file, the script of `sed` / `perl` is not a file, and `>&2` / `>&-` (a duplicated stream) are not files. `make &> all.log` was pinned to no file; it redirects the output to all.log as `make > all.log` does, so the pin now says all.log (the matcher still does not count `make` as an edit). docs/run-consistency.md names `&>` and the step per file. Thread: PRRT_kwDOVBg7g86wCjHW Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…scripts The wrapper readers took only the short option forms, so the value of a long option became the program: `uv run --extra dev pytest` ran `dev`, `sudo --user bob make` ran `bob`, `timeout --signal KILL 5 make` ran `KILL`. The sed reader did not see a script given as `--expression=...` or `-es/.../` and then recorded the real file as the script, and the BSD `sed -i '' s/a/b/ f` recorded the empty suffix and the script as files. Long forms join the value-option sets (sudo, env, time, uv, nice, timeout); an attached script counts as the script; an empty word after `-i` is the suffix, not a file. Tests: rows in test_control_syntax_and_wrappers_are_not_steps and a new test_written_files_of_an_in_place_edit (the new sed rows fail before this change; the pins for `> log.txt`, `2>err.log`, `-f a.sed -f b.sed` and `time -f` pass before and after). Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
- `[[ ... ]]` and `(( ... ))` are tracked by the scanner, so `&&` and `;` inside them no longer split a line into fake steps. - A `word)` is a case label only inside a `case ... in` block; elsewhere it closes a subshell, so `(cd build && make) 2>&1 | tail -5` is a make step. - `command -v pytest` is a `Bash:command` step, not a test run. - A trailing `&` and a loop body (`for`/`while`/`until`, not under set -e) hide the status for `last_ok`; the rule reads "(status hidden)". - A contract regex given as a list is an alternation; a non-string value is refused instead of being compiled via str(). - A token count that is not a finite number is skipped; a trace without usage has output_tokens None and stays out of output_tokens_cv. - A one-run strategy has inner_distance None and several of them are not "valid diversity". - Document that `find -exec sed -i` / `xargs sed -i` give no written file. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…ncy.md on top of answer-formats.md) Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
`pytest -q && echo PASS || echo FAIL` and `pytest -q && git diff; echo done` end with status 0 when pytest fails, yet `last_ok: test` passed. A command that is not the last of its `&&` list is now masked when a later `||`, or a `;`, newline or `&` after the list follows; `set -e` does not exempt it. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
The errexit and pipefail flags were a regex over the whole raw line, so a heredoc body (`set -euo pipefail` in a written script), a comment, an echo, a grep or a later `set -e` stopped `pytest -q | tail -5` and `pytest -q; echo done` from reading as hidden, and last_ok passed. Read them per `set` command. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…ules
- A trailing `&` backgrounds the whole `&&` / `||` list, `{ ... }`,
`( ... )` or `if` / `for` / `while` group it ends, so every command in
it has a status that is not the call's: `{ pytest -q; } &`,
`if x; then pytest -q; fi &` and `make && { pytest -q; } &` now hide
`pytest`. A command after the `&` is its own.
- Pin with rows the loop-body, `set -e` / `set -o pipefail`, `[[`
inside a subshell, `command -v` / `-V`, unopened `)`, token-count and
one-run verdict branches that the mutation check showed no test pinned.
- run-consistency.md: a trailing `&` hides the group it ends.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
ec59f3e to
e8f0174
Compare
…olution Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…roup
`set -e; { pytest -q; echo ")"; } &` read `pytest` as unmasked, so a
passing `last_ok` was reported for a status the shell sets to 0: the
group was cut at the quoted `)`. Count parens outside quotes and escapes.
Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
`(set -e; make); pytest -q; echo done` and `(set -o pipefail); pytest -q | tail -5` read as a passing `last_ok`: the options leaked out of the subshell. Restore them at its `)`; the separator after it is the outer shell's. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
With the `set` check in `_set_options` dropped, `grep -e foo x` turned errexit on and `grep -o pipefail x` pipefail; with `until` dropped from `_opens_loop`, an `until` body was unmasked. `last_ok` then passed on a hidden status, and every test still passed. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
|
Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> intel#271 raised max_docs to 34 for its two pages and this branch to 33 for run-consistency.md; with all three docs the selection is 35.
…ability in repo compare (intel#155) * feat(context): consistency — result, path and contract stability of repeated runs context consistency reads N stream-json traces of one task (or several arms) and reports three separate axes instead of one determinism score: result (success, pass@k, pass^k), path (tool-mix JSD, order/action edit distance, resource CV, strategy groups with per-group verdicts) and conformance to an optional YAML process contract (requires, forbids, before, after_last, last_ok). No model call. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * feat(context): consistency reads shell steps and blocked Stop hooks One Bash call becomes one step per chained command (quote- and heredoc-aware), and shell writes (sed -i, >, tee) carry their file, so an edit or check done through the shell counts. A blocked Stop hook is a failed Hook:Stop step. --shell call keeps the per-tool-call grain. Docs: contract equivalence and fault exposure. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * fix(context): consistency blames a failed chain on all its commands, distrusts piped checks A failed shell call marked only its last chained command as failed, so a failed `make check && git diff` passed last_ok. Every command of a failed call is now failed: the trace does not say which one was. `pytest | tail` reports tail's status. A step piped without pipefail is now masked, and last_ok does not accept a masked step as a passing check. _written tokenises with shlex punctuation_chars, so a `>` or `|` inside quotes is no longer a write or a pipe. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * feat(repo): compare pass^k and path stability of repeated cells `repo compare --pass-k K` adds pass^K per task, C(c,K)/C(n,K) over its cells, so an arm that makes a task pass every time is told apart from one that only moves the rate. `--path-stability` adds path_distance, the mean order distance of the cells' tool paths read from events.jsonl through context.consistency (run_from_events, order_distance). Both are descriptive and stay out of Holm's family. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * chore(context): CI fixes for consistency: header, nav, max_docs, fragment Add the license header and the mkdocs nav entry of run-consistency.md, raise the benchmark's max_docs to 31 for it, add the changelog fragment, and rename the trace-grain parameter from shell to grain so bandit does not read shell="call" as a subprocess call. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * Say repeated cells come from repo run --reps in compare docs Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(context): consistency parses shell syntax, matches shell edits, refuses ambiguous outcomes; compare skips empty traces Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * fix(context): a saved test log is not a shell edit Since 911a507 any Bash step with a `>` or `tee` target matched an edit action, so `pytest -q > test.log` was the last edit and broke `after_last: [edit, test]`, and `ls > /tmp/f` satisfied `requires: edit`. Count a shell write as an edit only for sed, perl, echo, printf, cat, tee. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> * fix(context): count a trace given twice to one arm once A repeated --arm or overlapping globs loaded the same trace twice: runs, success_rate and pass^k counted it twice while the contract's per-run table, keyed by name, counted it once. Keep the first spelling of each path and warn. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> * test(context): pin outcome keys, verdicts and the 911a507 shell guards No test reached a file-name or stem outcome key, the list form of --outcomes, the stable-failure and unjudged verdicts, a bare `(`, a `#` inside a word, `1>` or a Bash write against a Read matcher; mutants of each passed the suite. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> * fix(context): read shell lines for the commands they run; failed runs count; contract errors are clean Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * fix(repo): compare lists too-few-cell tasks once and skips score tasks for pass^K Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * test(repo): the score task has no trace, so it counts toward path_distance's too-few-cells line Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * fix(context): type the message, content and usage of a stream record for mypy Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * fix(context): a lone `}` closing a group is not a step `_NO_COMMAND` needed a word boundary after `}`, which a lone brace never has, so `{ ruff check .; pytest; }` gave a step named `}` and `{ make; }` hid the status of `make` behind it. A `}` at the start of a segment now closes the group. Thread: PRRT_kwDOVBg7g86wCjHA Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * fix(context): wrapper options that take a value do not name the program `sudo -u bob make`, `env -u FOO make` and `uv run --with rich pytest` named `bob`, `FOO` and `--with` as the program. `time` and `command` took one word only, so `time -p pytest` named `-p`. Options that take a value skip it, and `time` / `command` skip their flags like the other wrappers. Thread: PRRT_kwDOVBg7g86wCjHF Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * fix(context): a `#` inside a word does not cut the line `tokens` left shlex's default comment character on, so `echo $# > out.txt` and `echo ${#x} | grep 1` lost the redirect or the pipe after `$#` and `${#x}`, and a pipe that hid the status was not seen. The scanner already removes comments before this point, so the lexer does not need its own. Thread: PRRT_kwDOVBg7g86wCjHL Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * test(repo): pin the read and edit steps of path stability `run_from_events` maps an `edit` event to `Edit` and a `read` to `Read`, and no test built its events from anything but commands. The mapping passed the suite with `Edit` turned into `Read`; this pins it: the same two steps in another order are one reordering apart, and a read and an edit are different steps. Thread: PRRT_kwDOVBg7g86wCjHQ Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * fix(context): `&>` and `>&` write a file; a multi-file edit records each file `echo hi &> out.txt` had no `file_path`: `&>`, `&>>` and `>&` were operators but not write redirects. `sed -i s/a/b/ a.py b.py` recorded only `b.py`. A multi-file edit now gives one step per file, the script of `sed` / `perl` is not a file, and `>&2` / `>&-` (a duplicated stream) are not files. `make &> all.log` was pinned to no file; it redirects the output to all.log as `make > all.log` does, so the pin now says all.log (the matcher still does not count `make` as an edit). docs/run-consistency.md names `&>` and the step per file. Thread: PRRT_kwDOVBg7g86wCjHW Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * fix(context): long-form wrapper options and attached or BSD `sed -i` scripts The wrapper readers took only the short option forms, so the value of a long option became the program: `uv run --extra dev pytest` ran `dev`, `sudo --user bob make` ran `bob`, `timeout --signal KILL 5 make` ran `KILL`. The sed reader did not see a script given as `--expression=...` or `-es/.../` and then recorded the real file as the script, and the BSD `sed -i '' s/a/b/ f` recorded the empty suffix and the script as files. Long forms join the value-option sets (sudo, env, time, uv, nice, timeout); an attached script counts as the script; an empty word after `-i` is the suffix, not a file. Tests: rows in test_control_syntax_and_wrappers_are_not_steps and a new test_written_files_of_an_in_place_edit (the new sed rows fail before this change; the pins for `> log.txt`, `2>err.log`, `-f a.sed -f b.sed` and `time -f` pass before and after). Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * test(context): pin long wrapper options and sed script flags Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(context): shell reading and consistency edge cases from review - `[[ ... ]]` and `(( ... ))` are tracked by the scanner, so `&&` and `;` inside them no longer split a line into fake steps. - A `word)` is a case label only inside a `case ... in` block; elsewhere it closes a subshell, so `(cd build && make) 2>&1 | tail -5` is a make step. - `command -v pytest` is a `Bash:command` step, not a test run. - A trailing `&` and a loop body (`for`/`while`/`until`, not under set -e) hide the status for `last_ok`; the rule reads "(status hidden)". - A contract regex given as a list is an alternation; a non-string value is refused instead of being compiled via str(). - A token count that is not a finite number is skipped; a trace without usage has output_tokens None and stays out of output_tokens_cv. - A one-run strategy has inner_distance None and several of them are not "valid diversity". - Document that `find -exec sed -i` / `xargs sed -i` give no written file. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * chore(benchmarks): max_docs 32 -> 33 after merging main (run-consistency.md on top of answer-formats.md) Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * fix(context): a later `||` or `;` hides the status inside an `&&` list `pytest -q && echo PASS || echo FAIL` and `pytest -q && git diff; echo done` end with status 0 when pytest fails, yet `last_ok: test` passed. A command that is not the last of its `&&` list is now masked when a later `||`, or a `;`, newline or `&` after the list follows; `set -e` does not exempt it. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> * fix(context): set -e and pipefail count only from a set command The errexit and pipefail flags were a regex over the whole raw line, so a heredoc body (`set -euo pipefail` in a written script), a comment, an echo, a grep or a later `set -e` stopped `pytest -q | tail -5` and `pytest -q; echo done` from reading as hidden, and last_ok passed. Read them per `set` command. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> * docs(context): say set -e hides only pytest in a list before ; Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(context): a trailing & hides the group it ends; pin the masking rules - A trailing `&` backgrounds the whole `&&` / `||` list, `{ ... }`, `( ... )` or `if` / `for` / `while` group it ends, so every command in it has a status that is not the call's: `{ pytest -q; } &`, `if x; then pytest -q; fi &` and `make && { pytest -q; } &` now hide `pytest`. A command after the `&` is its own. - Pin with rows the loop-body, `set -e` / `set -o pipefail`, `[[` inside a subshell, `command -v` / `-V`, unopened `)`, token-count and one-run verdict branches that the mutation check showed no test pinned. - run-consistency.md: a trailing `&` hides the group it ends. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * fix(repo): drop the duplicate _measure_pairs left by the conflict resolution Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> * fix(context): a quoted or escaped paren does not end a backgrounded group `set -e; { pytest -q; echo ")"; } &` read `pytest` as unmasked, so a passing `last_ok` was reported for a status the shell sets to 0: the group was cut at the quoted `)`. Count parens outside quotes and escapes. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> * fix(context): a `set` inside `( ... )` ends with the subshell `(set -e; make); pytest -q; echo done` and `(set -o pipefail); pytest -q | tail -5` read as a passing `last_ok`: the options leaked out of the subshell. Restore them at its `)`; the separator after it is the outer shell's. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> * test(context): pin the `set` guard of errexit and pipefail, and `until` With the `set` check in `_set_options` dropped, `grep -e foo x` turned errexit on and `grep -o pipefail x` pipefail; with `until` dropped from `_opens_loop`, an `until` body was unmasked. `last_ok` then passed on a hidden status, and every test still passed. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> --------- Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> Signed-off-by: Anton Volkov <antonwolfy@gmail.com> Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> Co-authored-by: Anton Volkov <antonwolfy@gmail.com> Co-authored-by: Alexander Rybkin <alexander.rybkin@intel.com>
Settles three review points that merged PRs left open. #254 (review body B1, item 3): `dokimi.cli_flags.CANONICAL` and `BANNED` were built at import by `_index()` and read by nothing; the flag contract test builds its own index from `CONCEPTS`, so a mutant that emptied them survived. The two tables, `_index()` and their `__all__` entries are removed, with a Removed changelog fragment. #155 (review body B3, item 1): no test had an Edit matcher meet a `perl -pi` Bash step, so dropping `perl` from `_SHELL_WRITERS` left the suite green. `test_perl_in_place_is_an_edit_of_its_file` checks that `perl -pi -e 's/a/b/' src/a.py` satisfies an Edit action on `src/a.py` and that a `perl` run without `-i` does not. #238 (thread T22): the accepted costs in `test_a_cost_at_the_bound_is_accepted` were built from `_SAFE_MAGNITUDE` and moved with it, so lowering the bound to 1e6 passed. A fixed cost of 1e100 is now accepted too. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Summary
New
context consistency: reads repeated Claude Codestream-jsontraces of one task and answers three questions apart. No model calls.success_rate,pass@k,pass^kmix_jsd,order_distance,action_distance, CV of calls/turns/tokensconformance_rate, violations per rule,passed_but_nonconformingThird commit fixes three bugs found while using it:
Fourth commit wires it into
repo compare(item 4 of NEXT_STEPS), opt-in, descriptive, outside Holm:events.jsonlis harness-agnostic, so path stability works for any harnessrepo runconverts (consistency.run_from_events).Docs:
docs/run-consistency.md,docs/repo-evaluation-contract.md.Evidence
sededits that call grain hid.make check && git difffailing →check_contract == [];pytest -q | tail -5→[].After:
["last_ok: test"];["last_ok: test (status hidden by a pipe)"]; withset -o pipefail→[].repo comparetest: t1 2/3→2/3, t2 2/3→3/3: gate delta moves one task by 1/3;pass^3shows baseline 0.0 vs treatment 0.5 (t2 became reliable). Path test: baseline cellsgrep,pytestvsgit,make→ 1.0; treatmentgrep && pytestvsgrep; pytest→ 0.0.test_reach_timeout_kills_the_harness_process_tree[orphan]failure under-n autoload; passes alone and on main.)Merge Danger
Door: two-way
New subcommand and module;
repo comparegains two opt-in flags, default output unchanged.Blast Radius: small
🤖 Generated with Claude Code