Repository navigation
feat(repo): compare records the start context, filters tasks, and flags inconclusive rows - #296
antonwolfy wants to merge 9 commits into
Conversation
An evaluation of AGENTS.md guidance files (26 review_replay tasks, two arms, three reps) showed three gaps in reading a batch with repo compare. Start context: a batch did not say what each cell started with, so whether an arm's guidance was delivered had to be read by hand from every agent.stdout. The Transcript now carries first_prompt_tokens, the input + cache_creation + cache_read tokens of the first main-thread call, read with dokimi.context.trace (one call per message id, subagent calls apart). repo run writes it as result.json agent.first_prompt_tokens (null until set, and for a trace without such a call), and repo compare compares it as a descriptive measure next to cost_usd and turns. Like a process count, it is compared only when both arms report it, so a batch written before the field has no row rather than a row of zeros. Task selection: repo compare --task ID (repeatable) compares only those task ids. An id no finished cell has is refused through CompareError; the tasks left out are not listed as unpaired nor their cells as missing; the summary records the selection as task_filter and the text header prints "tasks: N of M (--task)". Reading notes, with no number changed: a gate or score row of 2+ tasks whose interval includes 0 gets an "inconclusive" note with mde, the smallest effect 80% power detects at that spread, (1.959964 + 0.841621) * sd / sqrt(n) (mde is in every row of --json); a row whose interval and sign-flip p disagree gets interval_p_disagree and a note naming the two methods, saying for gate and score that p is the one to read; descriptive rows with p < 0.05 are named on one line as uncorrected and outside Holm's family. Contract sections 4 and 9, docs/context-window.md and the compare module docstring describe the field, the flag and the notes. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
CLI interface change detectedThis PR changes dokimi's user-facing CLI surface (commands and/or options): ~ `dokimi repo compare` parameters changed:
+ added option `--task`Updated on every push; the |
|
@exai-bot /review |
exai-bot
left a comment
There was a problem hiding this comment.
Note
AI-generated review of 30030ed1d020, requested by @antonwolfy, using the default review skill. It can be wrong: verify a finding before acting on it. React with a thumbs up or down on a finding to tell the bot whether it helped.
CI on this commit, at review start and at posting: 13 passed, 3 running, 2 skipped.
Model us.anthropic.claude-sonnet-5-5, effort default, 12 of 162 turns, 1.3 of 120 minutes, 528k in / 6k out tokens.
The change records first_prompt_tokens per cell and compares it as a descriptive measure, adds repo compare --task, and prints reading notes (mde, interval/p disagreement, uncorrected descriptive p) without changing any number. I read the diff against context/trace.py, compare.py and the test helpers and found no defects. The parse_trace reuse, the one-arm-reports skip rule, the task-filter handling of missing and unpaired, and the interval_p_disagree condition all behave as the description says. I could not run the tests, because pytest, numpy, ruff and mypy are not installed in the container and there is no network.
No issues found.
For a human reviewer
- Design call: the notes label a gate or score row inconclusive and give an
mdecomputed from the observed spread of the per-task deltas. Confirm that framing is what you want users to read. - Whether the interval/p disagreement note should fire on descriptive rows, since the t interval and the sign-flip p will often differ at small task counts and could add noise.
Repository checks
Each ran as the head of this pull request defines it.
| Check | From | Result | Note |
|---|---|---|---|
python scripts/check_growth.py |
AGENTS.md | not run | no shell call ran this command |
pytest tests/test_repo_compare_reading.py tests/test_repo_compare.py tests/test_import_architecture.py tests/test_repo_reach.py |
AGENTS.md | not run | pytest is not installed in the container and there is no network |
ruff check / mypy dokimi |
.github/workflows/ci.yml | not run | tools are not installed in the container and there is no network |
Not verified
I did not run the new tests (test_repo_compare_reading.py, test_repo_reach.py, test_repo_run.py) or the CI checks. I checked the bootstrap-interval expectations (lo > 0, hi = +0.250) against the code by reading only. python scripts/check_growth.py ran and printed only a note that origin/main has no growth baseline to compare against. I did not confirm compare.py stays under the complexity cap for _reading_notes.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #296 +/- ##
==========================================
+ Coverage 93.48% 93.52% +0.03%
==========================================
Files 272 273 +1
Lines 35363 35616 +253
Branches 6001 6047 +46
==========================================
+ Hits 33060 33310 +250
- Misses 1489 1492 +3
Partials 814 814
🚀 New features to boost your workflow:
|
|
✅ OpenCodeReview: Review complete: 0 finding(s) across 11 selected item(s). Review details
|
Resolve the conflicts with intel#238: cmd_repo_compare passes --task and catches CellFileError too; _task_values reads first_prompt_tokens inside the new reading_cells block; compare.py keeps `import math` for min_detectable. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Review comment 4415626666: the one-arm skip of first_prompt_tokens was tested only with the baseline lacking it. The test now also compares a batch whose treatment lacks it and asserts the same warning and no unpaired entries, so a skip that checks only the baseline arm fails. Review comment 4416413441: the main call's message id now has a third, sparser usage line after the full one, so the first prompt is 19_380 only when the lines merge by maximum; "last line wins" gives 16_319 and fails the test, as "first line wins" already did. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
|
Review round in 00f9b40, tests only. Both OpenCodeReview points held: a mutant survived each at cad52dd, and both are caught now.
On the two design points exai-bot left for a human reviewer:
|
vlad-perevezentsev
left a comment
There was a problem hiding this comment.
Review at 00f9b40. The first_prompt_tokens record and --task are right. The reading notes can still mislead in two cases: mde at task counts where the sign-flip test cannot reach 0.05, and gate and score rows read against the raw p instead of p_holm. Both are in the line comments.
What I ran, at 00f9b40:
make ci-local --offline: 17 checks passed, 0 failed, 13 skipped (network, docker and the other Pythons).paired_summaryon small gate sets:[1, 0]givesmde1.401 at p 1.0, and[1, 1, 0]gives 0.934 at p 0.5.comparewith two treatments and 6 of 7 gate tasks up: p 0.031, p_holm 0.062, interval [+0.571, +1.000], and no note.compare --task fixon a batch with tasksfixandfix__a: a missingfix__acell is still listed.
Checked, no issues
- Start context:
_claude_stream_jsontakes the first main-thread call fromparse_trace, so a message id's usage lines are merged field by field, ascontext tracedoes. A broken line goes intoerrorsthere and does not lose the cell's record. A killed cell writesnull. - Older batches:
first_prompt_tokensgoes through the same one-arm rule as the process counts. The tests now cover both directions and the sparse-later-line merge, as the resolved threads asked. - Numbers: no existing value changes;
mdeandinterval_p_disagreeare new keys, and nothing in the schemas covers the compare JSON. - Docs and changelog: the contract,
context-window.mdand the fragment describe the new fields and--task.
| "p": sign_flip_p(deltas, seed=seed), | ||
| "p": p, | ||
| "p_holm": None, | ||
| "mde": _round(min_detectable(sd, len(deltas))) if sd > EPS else None, |
There was a problem hiding this comment.
mde comes from a normal-approximation power formula, but the p on the row is an exact sign-flip test, which cannot go below 0.05 with 5 or fewer changed tasks.
The smallest p the exact test can give is 2 / 2**k for k changed tasks, so for k <= 5 no effect of any size can reach p < 0.05. The note still names an effect that 80% power would detect. In the slot venv, paired_summary gives:
- gate deltas
[1, 0]: interval [+0.000, +1.000], p 1.0,mde1.401. The note says the smallest detectable effect is about ±1.401 (2 tasks), which is more than any gate delta can be. - gate deltas
[1, 1, 0]: p 0.5,mde0.934. The best p any result could reach here is 0.25.
A reader takes "±0.934 would have been detected" as a planning figure, but at this task count nothing can be detected.
Suggestion: when 2 / 2**k >= ALPHA (or n_tasks is so small that the exact test cannot reach it), say that in the note instead of an mde, e.g. "with 3 changed tasks the sign-flip p cannot go below 0.25; no effect can reach p < 0.05", and leave mde null there (or cap it at the measure's range for gate). A test with [1, 1, 0] would pin it.
| "p": p, | ||
| "p_holm": None, | ||
| "mde": _round(min_detectable(sd, len(deltas))) if sd > EPS else None, | ||
| "interval_p_disagree": lo is not None and hi is not None and p is not None and (lo <= 0 <= hi) == (p < ALPHA), |
There was a problem hiding this comment.
For gate and score, interval_p_disagree and the "read p" note use the raw p, but the p that decides those rows is p_holm.
paired_summary sets the flag before _holm runs, so Holm never enters it. With two treatments and the same gate deltas (6 of 7 tasks up, one tied), each row prints +0.857 [+0.571, +1.000], p 0.031, p_holm 0.062. The interval excludes 0 and the corrected p is not below 0.05, which is exactly the disagreement the note is meant to flag ("for gate and score p is the one to read"), but no note is printed. In the other direction (interval includes 0, raw p < 0.05, p_holm >= 0.05), the note tells the reader to "read p" and quotes the uncorrected p.
Suggestion: set interval_p_disagree for the gate and score rows after _holm(rows), comparing the interval with p_holm, and print p_holm in their note; descriptive rows keep the raw p. A test with two treatments like the one above would pin it.
| unknown = [t for t in dict.fromkeys(tasks) if t not in known] | ||
| if unknown: | ||
| raise CompareError(f"no finished cell of the compared batches has --task {', '.join(unknown)}") | ||
| kept = [m for m in missing if any(m.partition("/")[2].startswith(f"{t}__") for t in tasks)] # BATCH/<task>__... |
There was a problem hiding this comment.
Missing cells are matched to --task by the prefix <task>__ of the cell name, and a task id may itself contain __.
The manifest only rejects whitespace and commas in task ids. With tasks fix and fix__a, and an unfinished cell fix__a__guidance-raw__m-1__r1, compare([b], "guidance:none", tasks=["fix"]) returns missing == ['b/fix__a__guidance-raw__m-1__r1'], and the header says "1 missing", though fix__a was left out.
Suggestion: give each missing cell the longest known task id it starts with (<id>__), and keep it only if that id was selected. Or read the task from the cell's meta.json or the plan when there is one.
| if pass_k is not None and pass_k < 1: | ||
| raise CompareError(f"--pass-k must be at least 1, got {pass_k}") | ||
| cells, missing = load_batches(batch_dirs, metrics, path_stability) | ||
| cells, missing, task_filter = _select_tasks(cells, missing, tasks) |
There was a problem hiding this comment.
Nit: --task filters the cells before the baseline, treatment and --metric checks, so their errors no longer say why nothing matched.
repo compare b --baseline guidance:none --arm guidance:raw --task t3, where guidance:raw ran only t1 and t2, exits with "no finished cell matches the treatment 'guidance:raw'", which reads as a wrong arm name. Without --arm, that arm drops out of the comparison with no word.
Suggestion: add "among the --task selection" to those messages when tasks is set.
| if unknown: | ||
| raise CompareError(f"no finished cell of the compared batches has --task {', '.join(unknown)}") | ||
| kept = [m for m in missing if any(m.partition("/")[2].startswith(f"{t}__") for t in tasks)] # BATCH/<task>__... | ||
| return [c for c in cells if c["task"] in tasks], kept, {"tasks": sorted(set(tasks)), "of": len(known)} |
There was a problem hiding this comment.
Nit: task_filter.of counts only the tasks that have a finished cell, while the contract calls it "the task count of the batches".
A task whose cells all crashed before result.json is left out of of, so tasks: 1 of 8 (--task) can be printed for a batch of 10 tasks. Either count the planned tasks too, or say "tasks with a finished cell" in repo-evaluation-contract.md and the changelog.
|
✅ OpenCodeReview: Review complete: 0 finding(s) across 11 selected item(s). Review details
|
The inconclusive note gave mde with normal quantiles (1.96 + 0.84), but the interval beside it is a t interval at n-1 degrees of freedom. At 4 tasks the note said +-0.639 where the t quantiles give +-0.950; at 26 the gap is small. The contract's formula is updated to match. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…refix A task id may contain '__', so a missing cell of fix__a starts with fix__ and was counted as a cell of fix. The task is now split off the cell name from the right, as the arm, model and rep are the last three parts. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
With --task the arm and metric checks run on the selected cells, so an arm that ran only other tasks read as a wrong name. Those errors now add 'among the --task selection' when a selection is set. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The sentence bolded an orphan 'first'. It now names the first prompt. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
of is the tasks with a finished cell, the only ids --task can name, not the task count of the batches: a task whose cells all crashed before result.json is not in it. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
There was a problem hiding this comment.
🟡 Changes recommended
The detection-limit estimate misrepresents the exact sign-flip test’s small-sample capabilities, and ambiguous cell-name parsing silently omits unfinished cells when filtering by task.
2 open issues
What changed in this PR
2 new must-fix findings.
This PR improves dokimi’s repository A/B comparisons by recording initial prompt size, supporting task subsets, and adding guidance for interpreting uncertain results.
Changes:
- Records and compares first-main-thread prompt tokens, while preserving missing values for older batches.
- Adds repeatable
repo compare --taskfiltering with selection details in text and JSON. - Adds detection-limit estimates and notes for inconclusive results, interval/p-value disagreements, and uncorrected descriptive comparisons.
| File | Description |
|---|---|
dokimi/tests/test_repo_run.py |
Checks recorded prompt tokens and null defaults. |
dokimi/tests/test_repo_reach.py |
Tests first-call selection and merged usage records. |
dokimi/tests/test_repo_compare_reading.py |
Tests task filtering, token comparisons, and statistical notes. |
dokimi/tests/repo_eval_fixture.py |
Adds assistant-call usage to the fake harness. |
dokimi/dokimi/repos/runner.py |
Persists first-prompt token counts. |
dokimi/dokimi/repos/events.py |
Extracts initial prompt size through the shared trace parser. |
dokimi/dokimi/repos/compare.py |
Implements task filtering, token comparisons, and reading notes. |
dokimi/dokimi/commands/repo_run.py |
Exposes and forwards the repeatable --task option. |
dokimi/docs/repo-evaluation-contract.md |
Documents the new fields, filtering, and statistical guidance. |
dokimi/docs/context-window.md |
Documents first-prompt measurement in repository evaluations. |
dokimi/changelog.d/20261029_150000_antonwolfy_compare-reading.md |
Announces the new recording and comparison features. |
Review effort: Balanced
| parts = cell.rsplit("__", 3) | ||
| return parts[0] if len(parts) == 4 else None |
| def min_detectable(sd: float, n: int) -> float: | ||
| """The smallest mean delta a two-sided test at :data:`ALPHA` finds with :data:`POWER` over ``n`` tasks of | ||
| spread ``sd``. The quantiles are t at ``n - 1`` degrees of freedom, as the interval of ``mean_ci`` is.""" | ||
| df = n - 1 | ||
| return (sp_stats.t.ppf(1 - ALPHA / 2, df) + sp_stats.t.ppf(POWER, df)) * sd / math.sqrt(n) |
|
Head at review: 00f9b40 (unchanged on fetch, 10-29). Pushed sha: 525e9d2. Anton, the small fixes for Vlad's threads are on top of 00f9b40 and pushed as 525e9d2 (6 commits, signed off). Each fix has a test that fails on 00f9b40. Two of the threads are design questions, listed at the end, and are not changed. Fixed, with the commits (pushed shas):
Not changed, for you to decide:
Test note for you: |

Summary
Found while evaluating IntelPython/dpnp#3099 (AGENTS.md guidance) with dokimi: 26
review_replaytasks, two arms (guidance:noneandguidance:rev:<sha>), 3 reps, Claude Code, 156 cells. Reading that batch withrepo compareran into three gaps.maingivesagent.stdout: median 19,380 tokens against 16,315 forguidance:none(each ±20 over 78 cells), +3,065. That matches the rootAGENTS.md(7,015 chars) and none of the 7 nested files (21,199 chars), which load only lazily.result.jsonagenthas whole-run token totals only, andrepos/events.pyreads the assistant messages but not theirusage.repo runhas--task;repo comparehas none, so every statistic of the split was recomputed by hand.recall +0.022 [-0.066, +0.100], p 0.62, 26 tasks(1.96 + 0.84) * 0.222 / sqrt(26): the result is inconclusive.metric:n_findings+0.89, t interval [-0.091, +1.860], sign-flip p 0.0497: the report quoted "p = 0.049" for a descriptive row outside Holm's family, one of about a dozen comparisons. The score row over the 6 post-cut tasks no guidance commit named: +0.139, bootstrap [+0.028, +0.250], which excludes 0, while the exact sign-flip p is 0.25 (three non-zero deltas, all positive, so p cannot go below 0.25).Related: #292 asks for an interval that agrees with the sign-flip p by construction; the disagreement note here is the stopgap until then. #290 asks
repo compareto flag tasks a guidance revision may have seen;--taskis how the rest get compared on their own.Changes
repos/events.py:Transcript.first_prompt_tokens, theinput + cache_creation + cache_readtokens of the first main-thread call (parent_tool_use_idnull) of a Claude Codestream-jsontrace. It is read withdokimi.context.trace.parse_trace, so a call is counted once by message id and its blocks merge ascontext tracedoes; there is no second implementation.nullwhen the trace has no such call, and for thetextformat.repos/runner.py:result.jsonagent.first_prompt_tokens(nulluntil set, like the other token fields).repos/compare.py:first_prompt_tokensis a descriptive measure next tocost_usdandturns(per-task mean over the cells that have it). Like aprocesscount it is compared only when both arms report it for some shared task: a batch written before the field has no row, not a row of zeros, and when only one arm has it a warning names it.repo compare --task ID, repeatable (commands/repo_run.py,compare._select_tasks): compares only those task ids. An id no finished cell of the batches has is refused (exit 1,✗ no finished cell of the compared batches has --task t9). The tasks left out are not listed as unpaired and their cells not as missing.--jsonrecords the selection astask_filter({"tasks": [...], "of": <task count of the batches>},nullwithout--task), and the text header printstasks: N of M (--task).compare._reading_notes), after thechanged:and headroom notes. No number and no table column changes:score inconclusive: the interval includes 0; with this spread the smallest effect 80% power detects is about ±0.639 (4 tasks).mde = (1.959964 + 0.841621) * sd / sqrt(n)over the per-task deltas (sample SD) is in every--jsonrow,nullfor one task or deltas with no spread;interval_p_disagree: trueand a note. On the 6-delta case above it printsscore: the interval [+0.028, +0.250] excludes 0 but p 0.25 is ≥ 0.05; they come from two methods, a percentile bootstrap, too narrow over few tasks, and a sign-flip test; read p. A descriptive row names the t interval instead and does not say "read p". On a gate or score row that has this note, the inconclusive note is not printed too;p < 0.05 on turns: descriptive rows, their p uncorrected and outside Holm's family, which covers gate and score only.agent.first_prompt_tokens) and §9 (the measure,--task, the three notes andmde), a line indocs/context-window.md, and thecompare.pymodule docstring.tests/test_repo_compare_reading.py(test_repo_compare.pyis 955 lines, near the 1000-line limit for a test file), one trace test intests/test_repo_reach.py, and the agent-record tests intests/test_repo_run.py. The fake harness intests/repo_eval_fixture.pyputsusageon itsFAKE_USAGEassistant message.Risk
result.jsonagentgains one key; readers that take the dict as it is see one more field. No schema covers this file.repo compare --jsonrows gainmdeandinterval_p_disagree, and the summary gainstask_filter. Every existing key and value is unchanged.first_prompt_tokensrow for batches run from this change on. Older batches print as before apart from the notes.repo runparses each Claude Code trace a second time (incontext.trace) to get the first call. That is linear in the trace size and runs once per cell.repos/compare.pygrows from 696 to 769 lines (limit 800). Open PR fix(model): one same-model rule for baseline, self-judge and repo compare #273 also edits this file. This change edits only the parts above and reformats nothing around them; merged together, the file may need a split to stay under the limit.Testing
On a Linux review host, at
30030ed(onmaine966422):make ci-local: 27 passed, 1 failed, 7 skipped. Skipped: external-catalog-smoke (needs docker), test-latest, docs deploy and pip-audit report (job conditions false on a PR), cli-surface packaging (unknown expression locally), conda-forge-deps (no micromamba), semgrep secrets (not installed). The failure ispip-audit:Found 10 known vulnerabilitiesinfilelock 3.32.7(fixes in 4.0.9 / 4.2.1). That version is pinned inmain's lock, which this branch does not touch; ci(deps): document and ignore ten more filelock 3.32.7 advisories #288 documents and ignores those advisories. 10196 tests passed and 4 were skipped; changed-line coverage 100% (42 lines). The cli-surface step reports the one interface change:dokimi repo compare, added option--task.test_repo_compare_reading.py,test_repo_compare.py,test_repo_compare_paths.py,test_repo_reach.py,test_repo_run.py,test_repo_process_counts.py,test_repo_uptake.py,test_repo_skill_arms.py,test_context_trace.py): 456 passed. Guard tests (test_docs_code_refs.py,test_growth_ratchet.py,test_audit_test_quality.py,test_import_architecture.py): 290 passed.ruff checkon the changed files andmypyon the whole package (278 files) are clean.main, the new tests fail:test_repo_compare_reading.pystops at import (min_detectabledoes not exist), andtest_first_prompt_tokens_is_the_first_main_thread_call_counted_onceand threetest_repo_run.pyagent-record tests fail. All of them pass here.context.trace's block merge dropped;null;first_prompt_tokensdropped from_task_values, or from the both-arms rule;--task: the unknown-id check off, every cell kept, missing cells not filtered, the header line dropped, the CLI not passing the flag;Z_POWERset to 0, orstdevswapped forpstdev;repo runagainst a model, since this host makes no model calls. The dpnp numbers above come from that evaluation's batch, which is not on this host.mainmerged in after fix(cli): run, compare, arms, tasks, explore and repo exit with a message on bad input or output #238 (cad52dd). The conflicts were incmd_repo_compare, which now passes--taskand also catches fix(cli): run, compare, arms, tasks, explore and repo exit with a message on bad input or output #238'sCellFileError, and in_task_values, where thefirst_prompt_tokenslist moved into fix(cli): run, compare, arms, tasks, explore and repo exit with a message on bad input or output #238'sreading_cellsblock.compare.pykeepsimport mathformin_detectableand is 770 lines. On that commit, the compare, run, reach and context-trace tests with the guard tests: 644 passed;ruffandmypyare clean on the merged files.mainf0f95a2with the other PRs from this evaluation (fix(repo): scan says why it is incomplete #295 at4fa7783, feat(repo): compare records the start context, filters tasks, and flags inconclusive rows #296 atcad52dd, feat(repo): replay grades record each credited hit; repo report --hits lists them #297 at0a7539a, fix(novelty): a capped run says how far the cap reached #299 atfa453d8), on a workstation: the full suite has 10482 passed and 57 skipped, and the same 5 failures and 1 collection error asmainalone there (10456 passed). All six come from that machine's setup: nodocker, nohypothesis, and two tests that depend on its git and shell settings.00f9b40(test only):test_first_prompt_tokens_of_an_older_batch_is_no_row_not_zeronow also checks a batch where only the baseline reportsfirst_prompt_tokens, with the same warning and no unpaired entries.test_first_prompt_tokens_is_the_first_main_thread_call_counted_oncegives the main call a third, sparser usage line after the full one. Before the change, two mutants passed these tests: the one-arm skip checking only the baseline arm (if not has[0]), and "last line wins" incontext.trace's merge. Each now fails, and "first line wins" fails as well: 3 of 3 caught. An unmutated copy passes both files (100 tests).make ci-local: 25 passed, 3 failed, 7 skipped. 10528 tests passed and 4 were skipped; changed-line coverage 100% (43 lines). Two of the failures are gitleaks: its download got HTTP 504 four times, so Scan commits had no binary (exit 127). The third ispip-audit, the samefilelock 3.32.7finding as before; that version comes frommain's lock. The skips are the same 7 as before.Revert
git revertof the commit. Cells written in the meantime keepagent.first_prompt_tokensinresult.json; readers before this change ignore the key.Checklist
pytestpassesruff,mypy, and schema checks passAGENTS.md.github/.licenserc.yaml)