diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index eb421d3..0000000 --- a/.coveragerc +++ /dev/null @@ -1,33 +0,0 @@ -# Coverage configuration for the Orchestrator. -# -# WHY parallel mode is the whole point. This project's primary test mechanism is a per-module -# `--selftest`, and verify.py runs each one as a SUBPROCESS -# (`subprocess.run([sys.executable, f"{mod}.py", "--selftest"])`). A coverage run that instruments -# only the pytest process therefore cannot see any of it — and that is most of the codebase: 78 -# modules' sole test is a selftest, ~85,500 lines, 79.6% of non-test root Python (measured -# 2026-08-23). The reported 48.45% was that blind spot, not missing tests: sampling four of the -# modules the report named as worst showed outcomes.py at 62%, adversarial.py 88%, -# gh_capacity.py 86%, keepalive_outcomes.py 78% under their own selftests, all exiting 0. -# -# `parallel = true` makes every instrumented process write its own `.coverage...` -# file; `coverage combine` merges them. verify.py --coverage does both. -[run] -parallel = true -source = . -branch = false -omit = - .venv/* - venv/* - */site-packages/* - test_*.py - conftest.py - verify.py - -[report] -# A missing data file means the run did not happen, which must not read as 0% or as success. -skip_empty = false -precision = 1 -exclude_lines = - pragma: no cover - if __name__ == .__main__.: - raise NotImplementedError diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 330c758..e25c11d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: ORCH_LOCAL_RUNTIME: ${{ runner.temp }}/orch-runtime run: | mkdir -p "$ORCH_STATE_DIR" "$ORCH_LOCAL_RUNTIME" - python3 verify.py + python3 src/verify.py # verify.py already fails on: any pytest failure, ZERO tests collected, a collection count # below the recorded floor, passed+skipped dropping below the floor, MORE SKIPS THAN THE @@ -151,7 +151,7 @@ jobs: ORCH_LOCAL_RUNTIME: ${{ runner.temp }}/orch-runtime run: | mkdir -p "$ORCH_STATE_DIR" "$ORCH_LOCAL_RUNTIME" - python3 verify.py --reconcile-floor + python3 src/verify.py --reconcile-floor - name: Commit the reconciled floor run: | diff --git a/.github/workflows/pr-00-gate.yml b/.github/workflows/pr-00-gate.yml index 26cb8a2..5beb994 100644 --- a/.github/workflows/pr-00-gate.yml +++ b/.github/workflows/pr-00-gate.yml @@ -130,23 +130,26 @@ jobs: lint = format_check = cache = RUN_CORE pytest_markers = 'not quarantine and not slow' - # typecheck (and its deprecated `run-mypy` alias) -- OFF. - # blocking: 608 mypy errors across 89 of 189 files (measured 2026-08-23 on 24cb115 with - # `python3 scripts/ci_lint_baseline.py`, which runs - # `mypy --exclude .workflows-lib .` under the pinned mypy 2.3.1). - # THIS NUMBER DRIFTS: it moved 601 -> 604 -> 607 -> 608 during - # 2026-08-23 as typed code landed, and nothing couples it to a - # measurement, so re-run that script rather than trusting it. - # drainable: 0 per PR. Every one is a real annotation or logic change in a distinct - # module; there is no formatter and no `--fix` for this. - # drains by: typed modules landing incrementally. Turn this on the same day the count - # reaches 0, not before -- the 15 commonest of the 19 codes cover 603 - # of the 608, so a `disable_error_code` list would make the job green - # while checking essentially nothing, the defect verify.py exists to stop. - # note: mypy.ini is committed even though the check is off, because without it - # `mypy .` aborts on a duplicate-module setup error and the count above would - # be unverifiable prose instead of a number anyone can regenerate. - typecheck = False + # typecheck (and its deprecated `run-mypy` alias) -- ON, over a BOUNDED scope. + # It was OFF because 608 errors across the whole tree were drainable 0 per PR: an + # all-or-nothing check over code that cannot be fixed in one change is a gate whose + # clear path is blocked by the thing it measures, and it stayed shut. + # Two changes made it openable, both real rather than cosmetic: + # * the src/ move scoped the Gate's `target="src"` to the 99 modules, 608 -> 467; + # * pyproject.toml's [[tool.mypy.overrides]] exempts the 66 modules that still have + # findings BY NAME, so the 33 already-clean ones are checked TODAY. + # blocking: 0. The check passes as configured -- `mypy --config-file pyproject.toml + # --exclude .workflows-lib src` reports "no issues found in 99 source files". + # drainable: 66 modules, one at a time, each by typing it and deleting its line from the + # override list. That is a real mechanism, not "someone notices". + # drains by: typed modules landing incrementally. Unlike the old OFF state this now + # RATCHETS: `.verify-floor.json`'s `mypy_exempt_max` fails if the list grows, + # so new untyped code in a clean module is a red, and verify.py prints the + # remaining count on every run so green can never mean "checks nothing". + # NOT a suppression: no error code is disabled anywhere. The 467 findings stay visible + # (`python3 scripts/ci_lint_baseline.py`) and counted; only their MODULES are + # scoped out, by name, from a list that can only shrink. + typecheck = RUN_CORE # coverage (and the soft gate that reads its artifacts) -- ON since 2026-08-23. # was: OFF until 2026-08-23. `reusable-10-ci-python.yml` appended diff --git a/.verify-floor.json b/.verify-floor.json index 92d54bc..1e8ede6 100644 --- a/.verify-floor.json +++ b/.verify-floor.json @@ -4,5 +4,6 @@ "skipped_max": 26, "selftest_skipped_max": 7, "gate_skipped_max": 2, - "note": "Recorded by verify.py --update-floor, except the *_max ceilings, which are edited BY HAND and never re-measured. `collected` catches tests that stopped being collected; `passed` is compared against passed+skipped, so a check may move between passing and consciously-skipped but the two together may never shrink. The *_max ceilings bound the skipped side: 24/7/2 is exactly what a machine with none of this instance's local prerequisites skips (a GitHub runner: no agent CLIs, no ~/.codex/skills, no /Applications/ChatGPT.app, no populated capability ledger), measured 2026-08-21. On the owner's machine all prerequisites exist and nothing skips at all. Raising a ceiling is a deliberate act: it means agreeing that one more thing is allowed to go unchecked, so say which and why in the commit. LOWERED 26 -> 24 on 2026-08-22, reverting the raise made earlier the same day. The two kill-switch exemption tests no longer need to skip on a bare runner: their declarations moved out of the running instance's ledger and into capabilities.KNOWN_DECLARATIONS, so they assert code-derived truth and run everywhere. Moving a test back below the ceiling is the preferred way to lower it -- fix what made it machine-dependent, rather than agreeing to check less. FLOOR 345 -> 353 on 2026-08-22: 345 was measured on a branch cut before #13 (research panels/rounds/domain studies) merged, so the recorded floor sat 8 tests BELOW what main actually collects. A floor below reality is the permissive direction -- those 8 could have silently stopped being collected and still cleared the check, which is exactly the hole this file exists to close. Measure the floor on the merge result, not on the branch. Raised again on 2026-08-22 by the producer-identity-scope branch, which adds tests on top of the 353 recorded by #15; re-measured after rebasing rather than assumed. NOTE: `verify.py --update-floor` REPLACES this note with a generic one, so it must be restored by hand after every use \u2014 the ceiling rationale is the only record of which prerequisite justifies each skip. FLOOR 365 -> 366 on 2026-08-22 (heartbeat-ordering work, PR #18): exactly one new test, test_capabilities.test_no_tick_producer_runs_above_the_heartbeat_export. No ceiling moved and nothing new is skipped -- it reads source files rather than a populated ledger, so it runs on any machine. The branch recorded 354 because it was cut before #16 merged; re-measured on the MERGE RESULT per the rule above, which is exactly the mistake that put the floor 8 below reality last time. FLOOR 366 -> 368 on 2026-08-23: main collected 368 while this file recorded 366, drift left by #34 (evidence-acquisition landed, +1) and #37 (tick capability evidence, +1) whose authors each measured against a branch cut before the other merged. A floor BELOW reality is the permissive direction this file exists to close -- those two could have silently stopped being collected and still cleared the check. Measured on the merge result per the rule above: 368 passed, 0 failed, 0 skipped, 83/83 selftests, 43/43 can-fire, 5/5 gates. CEILING 24 -> 26 and FLOOR 368 -> 387 on 2026-08-23 (profiles/provenance branch, PR #42). This file CONFLICTED with #50, which raised the floor 366 -> 368 on main while this branch raised it to 387; resolved as the UNION rather than by taking a side -- #50's rationale is retained above and the count was RE-MEASURED on the new merge result instead of keeping either number. 368 (main) + 19 (this branch's net new tests) = 387; #50 corrected recorded drift rather than adding coverage, which is why 387 is unchanged from the pre-conflict measurement. Measured in a runner sandbox reproducing CI exactly (361 passed, 26 skipped, 387 collected) AND on the owner's machine (387 passed, 0 skipped, 5/5 gates). The two new skips are drift detectors against a REAL installed agent runtime, so neither can be moved below the ceiling -- the preferred way to lower one: (1) agy advertised-models cache absent, since comparing declared model ids against the catalogue agy actually advertises needs that catalogue, and a fixture would exercise the comparison while detecting no real drift; (2) vibe config absent (~/.vibe/config.toml), since active_model cannot be read to check for drift when there is no config to read. Both name their missing prerequisite, so a green run still states what it did not check. A third candidate skip was REFUSED: dispatcher's per-run agy-log assertion failed on a bare runner because adapters.advertised_models shells out to `agy models` when its disk cache is cold, and that probe landed inside a monkeypatched subprocess.run and overwrote the captured command. That is a stub leak, so it was fixed by ISOLATING the double rather than by skipping -- which makes CI run MORE. FLOOR 387 -> 391 on 2026-08-23 (improvement-log accessor, PR #59): exactly four new tests, all in test_improvement_log.py -- three read tracked files in the tree (the pointer's size and content, and that CLAUDE.md 0 step 3 and 5 name the accessor rather than a bare path) and one runs the accessor as a subprocess against a path that cannot exist. None reads a populated ledger, an agent CLI or ~/.codex, so all four RUN on a bare runner and NO ceiling moved: nothing new is skipped. Measured on the MERGE RESULT after rebasing onto origin/main af6654d, which collected 387 -- not on the branch base, per the rule above. FLOOR 391 -> 402 on 2026-08-23 (Gate python-ci configuration, the PR that adds the missing .github/workflows/autofix-versions.env): exactly 11 new tests, all in test_ci_gate_config.py, which read committed files only -- the pin file, ruff.toml, mypy.ini, pr-00-gate.yml's toggle annotations and docs/CI_LINT_BASELINE.md. NO ceiling moved. On any CHECKOUT -- CI, the owner's tree, a second instance -- all 11 run: they need no installed linter and no populated ledger. In the EXEC-MIRROR layout all 11 skip with one named reason, because orch-sync-mirror.sh copies root-level *.py only, so .github/workflows, docs/ and scripts/ are genuinely absent there (env_prereq.repo_files_absent). That lands at 11/26 on a machine that otherwise skips nothing, and CI stays at 26/26, so no ceiling needed raising. The skip gate is the presence of those DIRECTORIES, never of the pin file itself -- gating on the file would have made the test that checks for it unable to fail. Measured on the merge result, twice: the branch was rebuilt on origin/main after #42 and #59 merged, and re-measured after #61 merged and was merged in -- 393 passed + 9 skipped = 402 collected both times, so #61 added no collected tests and this floor is not sitting below reality. #61 itself left main's floor at 391, which is exactly main-without-these-11, so there is no inherited drift to correct. RULE CHANGE 2026-08-23: `collected` is now an EQUALITY, not a minimum. Every floor entry above this one records the number being found BELOW reality and hand-raised after the fact -- 21 low at the worst, then 8, then 1, then 2 -- because nothing ever required a test-adding PR to touch this file, so the permissive direction was silent by construction and the rule 'measure on the merge result' had to be restated three times with nothing enforcing it. verify.py now FAILS when collected exceeds the floor, printing the two integers to write. That also makes the concurrency case self-enforcing: once every test-adding branch must edit these same two lines, two concurrent branches CONFLICT IN GIT, so the second cannot merge without rebasing onto the first and re-measuring on the actual merge result. Demonstrated repeatedly on the change itself: six merges landed on main in the two hours it took to write, moving this file 368 -> 387 -> 391 -> 402, and every one would have left the floor below reality under the old one-directional rule. `passed` deliberately stays a MINIMUM on passed+skipped: only collection is machine-invariant (a skipped test is still collected), measured across machines at 391 collected on both, with pass/skip splits of 365/26 on CI against 391/0 locally. The *_max ceilings are untouched by this change and nothing new is skipped. `--update-floor` also stops REPLACING this note -- it appends -- so the warning above about restoring it by hand no longer applies; and drift does NOT block --update-floor, since a gate that forbade its own only remedy would be a deadlock (the first draft was exactly that). FLOOR 402 -> 407 on 2026-08-23 (findability admission requirement). (findability admission requirement). (findability admission requirement). (findability admission requirement). Exactly five new pytest tests, all in test_capability_admission.py: test_findability_distinguishes_its_three_sub_causes, test_findability_blocks_new_capabilities_and_reports_older_ones_as_debt, test_unreadable_reach_is_not_evaluated_and_never_a_failure, test_findability_exemption_is_declared_in_code_not_in_a_live_ledger, test_consult_sites_are_falsifiable_claims_about_real_callers. NO CEILING MOVED and nothing new skips: all five build synthetic ledgers in a tempdir or read committed tables, so none needs a populated capability ledger, an agent CLI or ~/.claude/skills. The one machine-dependent thing they touch -- an external consult site declared in capability_advisor.CONSULT_SITES whose skill prompt is not on this machine -- is reported as UNVERIFIED rather than skipped, because absence of the caller is not refutation of the claim; the in-tree site (tick) is asserted verified on every machine so the check can never degrade into 'everything unverified, nothing tested'. Measured on the merge result per the rule above: this file CONFLICTED three times while the branch was open, as main went 387 -> 391 -> 402 (#61, #64, #65, #60). Each time it was resolved as the UNION rather than by taking a side, and the count was RE-MEASURED on the new merge result rather than either number being carried forward: 402 (main at bd6da2e) + 5 (this branch's new tests) = 407. That is the rule this file already states -- measure the floor on the merge result, not on the branch -- and it mattered here, because #60 both deleted test_ci_gate_config.py and added more than it removed, so guessing in either direction would have been wrong. -> re-measured on 2026-08-23 (PR #62, the four deferred #42 review findings): three new tests, all machine-independent (each builds its own tmp_path Brain and manifests), so NO CEILING MOVED and nothing new is skipped. Fourth conflict for this branch, and the first one under the EQUALITY -- which is the point: the equality's own rationale says git conflict detection is what enforces 'measure on the merge result', and that is exactly what happened here. Under the old minimum the three earlier conflicts could each have been resolved by keeping the larger number; under the equality the count MUST be measured, and it was. RESOLVED AGAINST #68 (findability admission requirement) on 2026-08-23: taken as the UNION per the rule this file states -- #68's five-test entry is retained above and this branch's three-test entry beside it -- and the count RE-MEASURED on the merge result rather than keeping either side's number. main fc1fd42 collects 407; this branch adds 3; 410 measured with `pytest --collect-only -q` on the merge result, not assumed. Ceilings untouched at 26/7/2 and nothing new is skipped. Also resolved in the same merge: langsmith-fleet-worker-attempt.json, a CI-emitted `langsmith-fleet/v1` worker-attempt record whose two sides differed only in `emitted_at` and `pr_number` (62 here, 68 on main). Main's NEWER record was kept rather than this branch's older one -- discarding a newer provenance observation to win a merge would corrupt exactly the causal-provenance evidence CLAUDE.md 2 protects, and this branch's own run re-emits its record anyway. FLOOR 410 -> 411 on 2026-08-23 (CodeRabbit follow-up on PR #42, thread 3837879039; re-measured again after #56 made `collected` an EQUALITY, which makes an assumed number a hard RED rather than a quiet pass -- main stayed at 402 across #56, and the merge result measures 403, so #56 added no collected tests and this is main's 402 plus this branch's one): exactly one new test, test_feedback_model_provenance.test_late_sweep_completes_terminal_attempts_never_one_in_flight, which pins that ledger_reconcile.resolve_unresolved_worker_attempts completes only TERMINAL unresolved worker attempts and never one still in flight. No ceiling moved and nothing new is skipped -- the test builds its own tmp ledger and codex rollout fixture and monkeypatches adapters.CODEX_SESSIONS, so it needs no agent CLI and no populated capability ledger and runs on a bare runner. RESOLVED AGAINST #59 (improvement-log accessor), which raised the floor 387 -> 391 on main while this branch raised it to 388: taken as the UNION -- #59's rationale is retained above and the count was RE-MEASURED on the new merge result rather than keeping either number, which is the rule this file states and the mistake that once put the floor 8 below reality. 391 (main, incl. #59's four tests) + 1 (this branch's one new test) = 392 measured, not assumed: 392 passed, 0 failed, 0 skipped, 83/83 selftests, 43/43 can-fire, 5/5 gates. Three sibling follow-up branches are in flight against this same main (CI/ruff config, arm-attribution + durability, adapters label->ID); if this file conflicts with one of them, resolve as the UNION and RE-MEASURE on the new merge result rather than taking either number -- that is what #42 and #50 did, and taking a side is what put the floor 8 below reality earlier. RESOLVED AGAINST #68 (findability admission requirement) on 2026-08-23: taken as the UNION per the rule this file states -- #68's five-test entry is retained above and this branch's one-test entry beside it -- and the count RE-MEASURED on the merge result. main fc1fd42 collects 407; this branch adds 1; 408 measured with `pytest --collect-only -q` on the merge result, not assumed. Ceilings untouched at 26/7/2 and nothing new is skipped -- the one new test builds its own tmp ledger and codex rollout fixture, so it runs on a bare runner. Also resolved in the same merge: langsmith-fleet-worker-attempt.json, a CI-emitted `langsmith-fleet/v1` worker-attempt record differing only in `emitted_at` and `pr_number`; main's NEWER record was kept, since discarding a newer provenance observation to win a merge would corrupt the causal-provenance evidence CLAUDE.md 2 protects. FLOOR 411 -> 415 on 2026-08-23 (PR #70 diagnostics salvage): four new collected tests in test_capability_set_coverage.py from the PR #43 salvage plus CodeRabbit follow-ups on PR #51/#70 \u2014 union/missing-candidate fetch command, truncation after six modules, AST-scoped gate-call audit, and entrypoint-diagnosis coverage. NO CEILING MOVED and nothing new is skipped; all inject synthetic ledgers or read committed source. Measured on the merge result at 91d37fa: 389 passed + 26 skipped = 415 collected on CI, not assumed. FLOOR 415 -> 416 on 2026-08-23 (the dangling-citation follow-up, PR #74): exactly ONE new test, test_ci_gate_config.test_every_cited_repo_path_resolves, which reads the two committed config files this repo OWNS (the pin file and ruff.toml) and asserts every repo-relative path they cite exists. It exists because the pin file shipped citing docs/ci/LINT_BASELINE.md when the real path was docs/CI_LINT_BASELINE.md: the sibling checks read that file's CONTENTS thoroughly and its PROSE not at all, and the prose is the only pointer telling a reader where to re-measure before bumping a pin. Scoped to the two owned files deliberately -- scanning pr-00-gate.yml yields six findings that are all correct as written (guarded by hashFiles or a .agents check, or upstream paths), and a test that cries wolf gets waived. NO ceiling moved. RE-MEASURED SIX TIMES as the base moved under this ONE-LINE change: bd6da2e 402 -> ddb0928 402 -> fc1fd42 407 -> 0d661e3 407 -> 0593eeb 411 -> 6fed4ad 415, each +1 with this test, and the branch was rebuilt on each rather than the number carried forward. THIS BRANCH IS THE WORKED EXAMPLE of the equality's concurrency cost, so record it rather than rediscover it: main moved EIGHT times in the ~2.5 hours a one-line comment fix was open (#56, #68, #73, #69, #62, #70 and two direct commits), the floor line conflicted THREE separate times, and two merges overlapped the change directly -- #73 landed a byte-identical copy of the backplane-conformance.yml guard this branch also carried (dropped as redundant), and #69 edited this very test file in a neighbouring region. The equality is still the right call and should stay: every entry above this one records the floor being found BELOW reality, which is the permissive direction. But no amount of author care wins this race, because the correct value is only knowable on the merge result. The durable fix is CI running `verify.py --update-floor` on the merge commit, which keeps the equality and removes the race; until then a test-adding PR must be merged promptly after going green, because it re-conflicts on roughly every subsequent merge. FLOOR 416 -> 427 on 2026-08-23 (PR #72 hygiene untrack, rebased after #71 merged): exactly 11 new tests from test_repo_artifact_hygiene.py with root-anchored gitignore patterns. NO ceiling moved. Measured on merge result after #71 landed on main: 416 (main) + 11 = 427 collected via pytest --collect-only -q, not assumed. #71's simpler untrack landed first; this branch carries the full hygiene test suite and corrected root-anchored patterns. FLOOR 427 -> 428 on 2026-08-23 (PR salvaging #34/#42 remnants): exactly one new test, test_feedback_model_provenance.test_gemini_provenance_reads_the_per_run_log_before_the_conversation_store, recovered from #42's post-merge commit 4e0d6ae along with the adapters catalog work it exercises. No ceiling moved and nothing new is skipped -- it seeds adapters._ADVERTISED_MEMO instead of letting the catalog probe shell out, so it runs on any machine and adds no prerequisite. `passed` is 428 rather than the 426 verify.py suggested on this machine: two test_capabilities liveness tests (test_gate_blocks_execution_is_opt_in_and_narrow, test_evidence_gate_kind_is_not_blanket_observer) currently fail HERE on pristine main as well, because the hourly fleet tick mutated the machine-local ledger and range-lane-rollout now classifies matched_not_invoked rather than deliberately_gated. That is ledger STATE, not this branch and not the code -- CI bootstraps an empty ledger and counts 428/428. Recording 426 would have baked a local environment failure into the floor as though it were the expected result. FLOOR 428 -> 441 on 2026-08-23 (coverage measures what actually runs): exactly 12 new tests, all in test_verify_coverage_mode.py. They read committed files and verify.py's own source, and monkeypatch verify.COVERAGE in-process -- no populated ledger, no agent CLI, no ~/.codex, and no coverage RUN -- so all 12 execute on any machine and NO ceiling moved. The change itself is a measurement fix, not a gate: `verify.py --coverage` wraps each child in `coverage run --parallel-mode` and combines, because the per-module --selftest is a SUBPROCESS and a pytest-only coverage run cannot see it. That blind spot was most of the codebase -- 78 modules have no test_*.py at all, ~85,500 lines, 79.6% of non-test root Python -- so the reported 48.45% was measuring the gap in the instrument, not a gap in the tests. Combined: 76.1% (45,049 statements, 10,774 missed). Twelve of the twelve modules the old report named as worst were selftest-only; outcomes.py reported 9.0% and measures 61.9%, watch.py 9.6% -> 86.9%. Coverage is OFF by default and deliberately never touches the exit code -- one of the 12 tests pins that, because enforcing a threshold here would reward pytest wrappers around already-tested modules: metric up, assurance flat. Measured on the MERGE RESULT: branched from origin/main bcc68cd (floor 427), then REBASED onto f5f1c39 when it landed underneath and re-measured on the new merge result rather than carrying the old number: 428 + 13 = 441. +1 on 2026-08-23 (440 -> 441 after the rebase), same branch: test_the_cli_help_actually_renders. It exists because this branch BROKE `verify.py --help` and its own twelve tests did not notice. argparse interpolates help strings with `% params`, so the literal `~80%` in the --coverage help was read as an `%o` octal conversion and --help died with 'badly formed help string'. All twelve original tests passed: every one inspected source text or monkeypatched a flag, and not one RENDERED the help -- a construction-time test suite that never exercised the constructed thing, which is this repo's founding defect one layer up. CI's verify.py gate caught it, which is the check of last resort working as intended. The new test runs `verify.py --help` as a subprocess and asserts it exits 0, so it runs anywhere and NO ceiling moved. Rendering rather than grepping for `%` is deliberate: a grep would flag the legitimate `%(default)s`. FLOOR 441 -> 442 on 2026-08-23 (matched_not_invoked yields to observers and declared gates): exactly one new test, test_capabilities.test_matched_not_invoked_yields_to_observers_and_declared_gates. It is SYNTHETIC on purpose and that is the point of it: the two tests that caught this bug in the wild read the LIVE ledger, so they skipped with a named reason on the empty ledger ci.yml bootstraps -- the defect was red on every populated machine and green on CI for as long as it existed. A synthetic row asks the same question everywhere, so this one RUNS on a bare runner and NO ceiling moved. The fix itself moves matched_not_invoked below `observing` and below the DECLARED deliberately_gated check in classify_liveness: it was the first check, which made it the fourth instance of the unescapable label the comments in that function exist to fix. Audited before committing -- 12 of 43 live rows reclassify (ten observers to observing, two declared gates to deliberately_gated) and ZERO move for any other reason, so nothing is reclassified by inference and the weaker gate_reason-only branch is untouched. Measured on origin/main 0d9c3a7, whose recorded floor is 441, so 441 + 1 = 442. FLOOR RECORDED by verify.py --update-floor on 2026-08-23: collected=442, passed=442. Ceilings preserved, never re-measured \u2014 they are edited by hand. FLOOR RECORDED by verify.py --update-floor on 2026-08-24: collected=442, passed=442. Ceilings preserved, never re-measured \u2014 they are edited by hand. FLOOR RECORDED by verify.py --update-floor on 2026-08-24: collected=442, passed=442. Ceilings preserved, never re-measured \u2014 they are edited by hand." + "note": "Recorded by verify.py --update-floor, except the *_max ceilings, which are edited BY HAND and never re-measured. `collected` catches tests that stopped being collected; `passed` is compared against passed+skipped, so a check may move between passing and consciously-skipped but the two together may never shrink. The *_max ceilings bound the skipped side: 24/7/2 is exactly what a machine with none of this instance's local prerequisites skips (a GitHub runner: no agent CLIs, no ~/.codex/skills, no /Applications/ChatGPT.app, no populated capability ledger), measured 2026-08-21. On the owner's machine all prerequisites exist and nothing skips at all. Raising a ceiling is a deliberate act: it means agreeing that one more thing is allowed to go unchecked, so say which and why in the commit. LOWERED 26 -> 24 on 2026-08-22, reverting the raise made earlier the same day. The two kill-switch exemption tests no longer need to skip on a bare runner: their declarations moved out of the running instance's ledger and into capabilities.KNOWN_DECLARATIONS, so they assert code-derived truth and run everywhere. Moving a test back below the ceiling is the preferred way to lower it -- fix what made it machine-dependent, rather than agreeing to check less. FLOOR 345 -> 353 on 2026-08-22: 345 was measured on a branch cut before #13 (research panels/rounds/domain studies) merged, so the recorded floor sat 8 tests BELOW what main actually collects. A floor below reality is the permissive direction -- those 8 could have silently stopped being collected and still cleared the check, which is exactly the hole this file exists to close. Measure the floor on the merge result, not on the branch. Raised again on 2026-08-22 by the producer-identity-scope branch, which adds tests on top of the 353 recorded by #15; re-measured after rebasing rather than assumed. NOTE: `verify.py --update-floor` REPLACES this note with a generic one, so it must be restored by hand after every use \u2014 the ceiling rationale is the only record of which prerequisite justifies each skip. FLOOR 365 -> 366 on 2026-08-22 (heartbeat-ordering work, PR #18): exactly one new test, test_capabilities.test_no_tick_producer_runs_above_the_heartbeat_export. No ceiling moved and nothing new is skipped -- it reads source files rather than a populated ledger, so it runs on any machine. The branch recorded 354 because it was cut before #16 merged; re-measured on the MERGE RESULT per the rule above, which is exactly the mistake that put the floor 8 below reality last time. FLOOR 366 -> 368 on 2026-08-23: main collected 368 while this file recorded 366, drift left by #34 (evidence-acquisition landed, +1) and #37 (tick capability evidence, +1) whose authors each measured against a branch cut before the other merged. A floor BELOW reality is the permissive direction this file exists to close -- those two could have silently stopped being collected and still cleared the check. Measured on the merge result per the rule above: 368 passed, 0 failed, 0 skipped, 83/83 selftests, 43/43 can-fire, 5/5 gates. CEILING 24 -> 26 and FLOOR 368 -> 387 on 2026-08-23 (profiles/provenance branch, PR #42). This file CONFLICTED with #50, which raised the floor 366 -> 368 on main while this branch raised it to 387; resolved as the UNION rather than by taking a side -- #50's rationale is retained above and the count was RE-MEASURED on the new merge result instead of keeping either number. 368 (main) + 19 (this branch's net new tests) = 387; #50 corrected recorded drift rather than adding coverage, which is why 387 is unchanged from the pre-conflict measurement. Measured in a runner sandbox reproducing CI exactly (361 passed, 26 skipped, 387 collected) AND on the owner's machine (387 passed, 0 skipped, 5/5 gates). The two new skips are drift detectors against a REAL installed agent runtime, so neither can be moved below the ceiling -- the preferred way to lower one: (1) agy advertised-models cache absent, since comparing declared model ids against the catalogue agy actually advertises needs that catalogue, and a fixture would exercise the comparison while detecting no real drift; (2) vibe config absent (~/.vibe/config.toml), since active_model cannot be read to check for drift when there is no config to read. Both name their missing prerequisite, so a green run still states what it did not check. A third candidate skip was REFUSED: dispatcher's per-run agy-log assertion failed on a bare runner because adapters.advertised_models shells out to `agy models` when its disk cache is cold, and that probe landed inside a monkeypatched subprocess.run and overwrote the captured command. That is a stub leak, so it was fixed by ISOLATING the double rather than by skipping -- which makes CI run MORE. FLOOR 387 -> 391 on 2026-08-23 (improvement-log accessor, PR #59): exactly four new tests, all in test_improvement_log.py -- three read tracked files in the tree (the pointer's size and content, and that CLAUDE.md 0 step 3 and 5 name the accessor rather than a bare path) and one runs the accessor as a subprocess against a path that cannot exist. None reads a populated ledger, an agent CLI or ~/.codex, so all four RUN on a bare runner and NO ceiling moved: nothing new is skipped. Measured on the MERGE RESULT after rebasing onto origin/main af6654d, which collected 387 -- not on the branch base, per the rule above. FLOOR 391 -> 402 on 2026-08-23 (Gate python-ci configuration, the PR that adds the missing .github/workflows/autofix-versions.env): exactly 11 new tests, all in test_ci_gate_config.py, which read committed files only -- the pin file, ruff.toml, mypy.ini, pr-00-gate.yml's toggle annotations and docs/CI_LINT_BASELINE.md. NO ceiling moved. On any CHECKOUT -- CI, the owner's tree, a second instance -- all 11 run: they need no installed linter and no populated ledger. In the EXEC-MIRROR layout all 11 skip with one named reason, because orch-sync-mirror.sh copies root-level *.py only, so .github/workflows, docs/ and scripts/ are genuinely absent there (env_prereq.repo_files_absent). That lands at 11/26 on a machine that otherwise skips nothing, and CI stays at 26/26, so no ceiling needed raising. The skip gate is the presence of those DIRECTORIES, never of the pin file itself -- gating on the file would have made the test that checks for it unable to fail. Measured on the merge result, twice: the branch was rebuilt on origin/main after #42 and #59 merged, and re-measured after #61 merged and was merged in -- 393 passed + 9 skipped = 402 collected both times, so #61 added no collected tests and this floor is not sitting below reality. #61 itself left main's floor at 391, which is exactly main-without-these-11, so there is no inherited drift to correct. RULE CHANGE 2026-08-23: `collected` is now an EQUALITY, not a minimum. Every floor entry above this one records the number being found BELOW reality and hand-raised after the fact -- 21 low at the worst, then 8, then 1, then 2 -- because nothing ever required a test-adding PR to touch this file, so the permissive direction was silent by construction and the rule 'measure on the merge result' had to be restated three times with nothing enforcing it. verify.py now FAILS when collected exceeds the floor, printing the two integers to write. That also makes the concurrency case self-enforcing: once every test-adding branch must edit these same two lines, two concurrent branches CONFLICT IN GIT, so the second cannot merge without rebasing onto the first and re-measuring on the actual merge result. Demonstrated repeatedly on the change itself: six merges landed on main in the two hours it took to write, moving this file 368 -> 387 -> 391 -> 402, and every one would have left the floor below reality under the old one-directional rule. `passed` deliberately stays a MINIMUM on passed+skipped: only collection is machine-invariant (a skipped test is still collected), measured across machines at 391 collected on both, with pass/skip splits of 365/26 on CI against 391/0 locally. The *_max ceilings are untouched by this change and nothing new is skipped. `--update-floor` also stops REPLACING this note -- it appends -- so the warning above about restoring it by hand no longer applies; and drift does NOT block --update-floor, since a gate that forbade its own only remedy would be a deadlock (the first draft was exactly that). FLOOR 402 -> 407 on 2026-08-23 (findability admission requirement). (findability admission requirement). (findability admission requirement). (findability admission requirement). Exactly five new pytest tests, all in test_capability_admission.py: test_findability_distinguishes_its_three_sub_causes, test_findability_blocks_new_capabilities_and_reports_older_ones_as_debt, test_unreadable_reach_is_not_evaluated_and_never_a_failure, test_findability_exemption_is_declared_in_code_not_in_a_live_ledger, test_consult_sites_are_falsifiable_claims_about_real_callers. NO CEILING MOVED and nothing new skips: all five build synthetic ledgers in a tempdir or read committed tables, so none needs a populated capability ledger, an agent CLI or ~/.claude/skills. The one machine-dependent thing they touch -- an external consult site declared in capability_advisor.CONSULT_SITES whose skill prompt is not on this machine -- is reported as UNVERIFIED rather than skipped, because absence of the caller is not refutation of the claim; the in-tree site (tick) is asserted verified on every machine so the check can never degrade into 'everything unverified, nothing tested'. Measured on the merge result per the rule above: this file CONFLICTED three times while the branch was open, as main went 387 -> 391 -> 402 (#61, #64, #65, #60). Each time it was resolved as the UNION rather than by taking a side, and the count was RE-MEASURED on the new merge result rather than either number being carried forward: 402 (main at bd6da2e) + 5 (this branch's new tests) = 407. That is the rule this file already states -- measure the floor on the merge result, not on the branch -- and it mattered here, because #60 both deleted test_ci_gate_config.py and added more than it removed, so guessing in either direction would have been wrong. -> re-measured on 2026-08-23 (PR #62, the four deferred #42 review findings): three new tests, all machine-independent (each builds its own tmp_path Brain and manifests), so NO CEILING MOVED and nothing new is skipped. Fourth conflict for this branch, and the first one under the EQUALITY -- which is the point: the equality's own rationale says git conflict detection is what enforces 'measure on the merge result', and that is exactly what happened here. Under the old minimum the three earlier conflicts could each have been resolved by keeping the larger number; under the equality the count MUST be measured, and it was. RESOLVED AGAINST #68 (findability admission requirement) on 2026-08-23: taken as the UNION per the rule this file states -- #68's five-test entry is retained above and this branch's three-test entry beside it -- and the count RE-MEASURED on the merge result rather than keeping either side's number. main fc1fd42 collects 407; this branch adds 3; 410 measured with `pytest --collect-only -q` on the merge result, not assumed. Ceilings untouched at 26/7/2 and nothing new is skipped. Also resolved in the same merge: langsmith-fleet-worker-attempt.json, a CI-emitted `langsmith-fleet/v1` worker-attempt record whose two sides differed only in `emitted_at` and `pr_number` (62 here, 68 on main). Main's NEWER record was kept rather than this branch's older one -- discarding a newer provenance observation to win a merge would corrupt exactly the causal-provenance evidence CLAUDE.md 2 protects, and this branch's own run re-emits its record anyway. FLOOR 410 -> 411 on 2026-08-23 (CodeRabbit follow-up on PR #42, thread 3837879039; re-measured again after #56 made `collected` an EQUALITY, which makes an assumed number a hard RED rather than a quiet pass -- main stayed at 402 across #56, and the merge result measures 403, so #56 added no collected tests and this is main's 402 plus this branch's one): exactly one new test, test_feedback_model_provenance.test_late_sweep_completes_terminal_attempts_never_one_in_flight, which pins that ledger_reconcile.resolve_unresolved_worker_attempts completes only TERMINAL unresolved worker attempts and never one still in flight. No ceiling moved and nothing new is skipped -- the test builds its own tmp ledger and codex rollout fixture and monkeypatches adapters.CODEX_SESSIONS, so it needs no agent CLI and no populated capability ledger and runs on a bare runner. RESOLVED AGAINST #59 (improvement-log accessor), which raised the floor 387 -> 391 on main while this branch raised it to 388: taken as the UNION -- #59's rationale is retained above and the count was RE-MEASURED on the new merge result rather than keeping either number, which is the rule this file states and the mistake that once put the floor 8 below reality. 391 (main, incl. #59's four tests) + 1 (this branch's one new test) = 392 measured, not assumed: 392 passed, 0 failed, 0 skipped, 83/83 selftests, 43/43 can-fire, 5/5 gates. Three sibling follow-up branches are in flight against this same main (CI/ruff config, arm-attribution + durability, adapters label->ID); if this file conflicts with one of them, resolve as the UNION and RE-MEASURE on the new merge result rather than taking either number -- that is what #42 and #50 did, and taking a side is what put the floor 8 below reality earlier. RESOLVED AGAINST #68 (findability admission requirement) on 2026-08-23: taken as the UNION per the rule this file states -- #68's five-test entry is retained above and this branch's one-test entry beside it -- and the count RE-MEASURED on the merge result. main fc1fd42 collects 407; this branch adds 1; 408 measured with `pytest --collect-only -q` on the merge result, not assumed. Ceilings untouched at 26/7/2 and nothing new is skipped -- the one new test builds its own tmp ledger and codex rollout fixture, so it runs on a bare runner. Also resolved in the same merge: langsmith-fleet-worker-attempt.json, a CI-emitted `langsmith-fleet/v1` worker-attempt record differing only in `emitted_at` and `pr_number`; main's NEWER record was kept, since discarding a newer provenance observation to win a merge would corrupt the causal-provenance evidence CLAUDE.md 2 protects. FLOOR 411 -> 415 on 2026-08-23 (PR #70 diagnostics salvage): four new collected tests in test_capability_set_coverage.py from the PR #43 salvage plus CodeRabbit follow-ups on PR #51/#70 \u2014 union/missing-candidate fetch command, truncation after six modules, AST-scoped gate-call audit, and entrypoint-diagnosis coverage. NO CEILING MOVED and nothing new is skipped; all inject synthetic ledgers or read committed source. Measured on the merge result at 91d37fa: 389 passed + 26 skipped = 415 collected on CI, not assumed. FLOOR 415 -> 416 on 2026-08-23 (the dangling-citation follow-up, PR #74): exactly ONE new test, test_ci_gate_config.test_every_cited_repo_path_resolves, which reads the two committed config files this repo OWNS (the pin file and ruff.toml) and asserts every repo-relative path they cite exists. It exists because the pin file shipped citing docs/ci/LINT_BASELINE.md when the real path was docs/CI_LINT_BASELINE.md: the sibling checks read that file's CONTENTS thoroughly and its PROSE not at all, and the prose is the only pointer telling a reader where to re-measure before bumping a pin. Scoped to the two owned files deliberately -- scanning pr-00-gate.yml yields six findings that are all correct as written (guarded by hashFiles or a .agents check, or upstream paths), and a test that cries wolf gets waived. NO ceiling moved. RE-MEASURED SIX TIMES as the base moved under this ONE-LINE change: bd6da2e 402 -> ddb0928 402 -> fc1fd42 407 -> 0d661e3 407 -> 0593eeb 411 -> 6fed4ad 415, each +1 with this test, and the branch was rebuilt on each rather than the number carried forward. THIS BRANCH IS THE WORKED EXAMPLE of the equality's concurrency cost, so record it rather than rediscover it: main moved EIGHT times in the ~2.5 hours a one-line comment fix was open (#56, #68, #73, #69, #62, #70 and two direct commits), the floor line conflicted THREE separate times, and two merges overlapped the change directly -- #73 landed a byte-identical copy of the backplane-conformance.yml guard this branch also carried (dropped as redundant), and #69 edited this very test file in a neighbouring region. The equality is still the right call and should stay: every entry above this one records the floor being found BELOW reality, which is the permissive direction. But no amount of author care wins this race, because the correct value is only knowable on the merge result. The durable fix is CI running `verify.py --update-floor` on the merge commit, which keeps the equality and removes the race; until then a test-adding PR must be merged promptly after going green, because it re-conflicts on roughly every subsequent merge. FLOOR 416 -> 427 on 2026-08-23 (PR #72 hygiene untrack, rebased after #71 merged): exactly 11 new tests from test_repo_artifact_hygiene.py with root-anchored gitignore patterns. NO ceiling moved. Measured on merge result after #71 landed on main: 416 (main) + 11 = 427 collected via pytest --collect-only -q, not assumed. #71's simpler untrack landed first; this branch carries the full hygiene test suite and corrected root-anchored patterns. FLOOR 427 -> 428 on 2026-08-23 (PR salvaging #34/#42 remnants): exactly one new test, test_feedback_model_provenance.test_gemini_provenance_reads_the_per_run_log_before_the_conversation_store, recovered from #42's post-merge commit 4e0d6ae along with the adapters catalog work it exercises. No ceiling moved and nothing new is skipped -- it seeds adapters._ADVERTISED_MEMO instead of letting the catalog probe shell out, so it runs on any machine and adds no prerequisite. `passed` is 428 rather than the 426 verify.py suggested on this machine: two test_capabilities liveness tests (test_gate_blocks_execution_is_opt_in_and_narrow, test_evidence_gate_kind_is_not_blanket_observer) currently fail HERE on pristine main as well, because the hourly fleet tick mutated the machine-local ledger and range-lane-rollout now classifies matched_not_invoked rather than deliberately_gated. That is ledger STATE, not this branch and not the code -- CI bootstraps an empty ledger and counts 428/428. Recording 426 would have baked a local environment failure into the floor as though it were the expected result. FLOOR 428 -> 441 on 2026-08-23 (coverage measures what actually runs): exactly 12 new tests, all in test_verify_coverage_mode.py. They read committed files and verify.py's own source, and monkeypatch verify.COVERAGE in-process -- no populated ledger, no agent CLI, no ~/.codex, and no coverage RUN -- so all 12 execute on any machine and NO ceiling moved. The change itself is a measurement fix, not a gate: `verify.py --coverage` wraps each child in `coverage run --parallel-mode` and combines, because the per-module --selftest is a SUBPROCESS and a pytest-only coverage run cannot see it. That blind spot was most of the codebase -- 78 modules have no test_*.py at all, ~85,500 lines, 79.6% of non-test root Python -- so the reported 48.45% was measuring the gap in the instrument, not a gap in the tests. Combined: 76.1% (45,049 statements, 10,774 missed). Twelve of the twelve modules the old report named as worst were selftest-only; outcomes.py reported 9.0% and measures 61.9%, watch.py 9.6% -> 86.9%. Coverage is OFF by default and deliberately never touches the exit code -- one of the 12 tests pins that, because enforcing a threshold here would reward pytest wrappers around already-tested modules: metric up, assurance flat. Measured on the MERGE RESULT: branched from origin/main bcc68cd (floor 427), then REBASED onto f5f1c39 when it landed underneath and re-measured on the new merge result rather than carrying the old number: 428 + 13 = 441. +1 on 2026-08-23 (440 -> 441 after the rebase), same branch: test_the_cli_help_actually_renders. It exists because this branch BROKE `verify.py --help` and its own twelve tests did not notice. argparse interpolates help strings with `% params`, so the literal `~80%` in the --coverage help was read as an `%o` octal conversion and --help died with 'badly formed help string'. All twelve original tests passed: every one inspected source text or monkeypatched a flag, and not one RENDERED the help -- a construction-time test suite that never exercised the constructed thing, which is this repo's founding defect one layer up. CI's verify.py gate caught it, which is the check of last resort working as intended. The new test runs `verify.py --help` as a subprocess and asserts it exits 0, so it runs anywhere and NO ceiling moved. Rendering rather than grepping for `%` is deliberate: a grep would flag the legitimate `%(default)s`. FLOOR 441 -> 442 on 2026-08-23 (matched_not_invoked yields to observers and declared gates): exactly one new test, test_capabilities.test_matched_not_invoked_yields_to_observers_and_declared_gates. It is SYNTHETIC on purpose and that is the point of it: the two tests that caught this bug in the wild read the LIVE ledger, so they skipped with a named reason on the empty ledger ci.yml bootstraps -- the defect was red on every populated machine and green on CI for as long as it existed. A synthetic row asks the same question everywhere, so this one RUNS on a bare runner and NO ceiling moved. The fix itself moves matched_not_invoked below `observing` and below the DECLARED deliberately_gated check in classify_liveness: it was the first check, which made it the fourth instance of the unescapable label the comments in that function exist to fix. Audited before committing -- 12 of 43 live rows reclassify (ten observers to observing, two declared gates to deliberately_gated) and ZERO move for any other reason, so nothing is reclassified by inference and the weaker gate_reason-only branch is untouched. Measured on origin/main 0d9c3a7, whose recorded floor is 441, so 441 + 1 = 442. FLOOR RECORDED by verify.py --update-floor on 2026-08-23: collected=442, passed=442. Ceilings preserved, never re-measured \u2014 they are edited by hand. FLOOR RECORDED by verify.py --update-floor on 2026-08-24: collected=442, passed=442. Ceilings preserved, never re-measured \u2014 they are edited by hand. FLOOR RECORDED by verify.py --update-floor on 2026-08-24: collected=442, passed=442. Ceilings preserved, never re-measured \u2014 they are edited by hand. MYPY_EXEMPT_MAX introduced at 64 on 2026-08-23, the same change that flipped `typecheck` ON in pr-00-gate.yml. It was OFF because 608 whole-tree errors were drainable 0 per PR -- a gate whose clear path is blocked by the thing it measures. Two real changes opened it: the src/ move scoped the Gate's `target=src` to the 99 modules (608 -> 467), and pyproject.toml's [[tool.mypy.overrides]] exempts the modules that still have findings BY NAME so the 35 already-clean ones are checked today. 12 var-annotated findings were then drained (467 -> 455, 66 -> 64 modules) to prove the drain works rather than promise it. This ceiling may only ever be LOWERED, by typing a module and deleting its line -- raising it means agreeing one more module goes unchecked, so say which and why. NOT a suppression: no error code is disabled anywhere and the 455 stay visible via `python3 scripts/ci_lint_baseline.py`. Edited BY HAND like the other *_max values; --update-floor never re-measures it.", + "mypy_exempt_max": 64 } diff --git a/ADDING_CAPABILITIES.md b/ADDING_CAPABILITIES.md index d242e96..252b0d0 100644 --- a/ADDING_CAPABILITIES.md +++ b/ADDING_CAPABILITIES.md @@ -25,7 +25,7 @@ in a table nothing wrote for it. The dated evidence for all nine failure modes is deliberately **not** committed — it names this instance's repositories, PRs, spend and working constraints. It lives in `ADDING_CAPABILITIES.local.md` (see `LOCAL_POLICY.md`), and the item-by-item status history lives in -the machine-local improvement log, reached with `python3 improvement_log.py search `. +the machine-local improvement log, reached with `python3 src/improvement_log.py search `. The governing lesson survives the split, and it is the reason this file has a test file rather than only prose: **a rule that lives only in a document does not survive the next session.** `CLAUDE.md` @@ -38,7 +38,7 @@ them. Enforced by `capability_admission.py` + `test_capability_admission.py`. Run **before** writing code: ```bash -python3 capability_admission.py --preflight '{"capability_id":"capability:my-thing", ...}' +python3 src/capability_admission.py --preflight '{"capability_id":"capability:my-thing", ...}' ``` `preflight` answers the six declarable requirements immediately — including findability, which is the diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 23f4ea9..682f097 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -656,16 +656,16 @@ unattended agent isn't caught until a bad outcome hours later), and the scaffold ### CLI ```bash -python3 roles.py --selftest # offline contract checks -python3 roles.py route --role redirect # show the router-chosen backend -python3 roles.py redirect --report-json r.json --ac "" \ +python3 src/roles.py --selftest # offline contract checks +python3 src/roles.py route --role redirect # show the router-chosen backend +python3 src/roles.py redirect --report-json r.json --ac "" \ [--proposal-json p.json] # replay a captured proposal (offline) -python3 roles.py redirect --report-json r.json --ac "..." --dispatch # live offload to the backend -python3 redirect_shadow.py record --report-json r.json --ac "..." --dispatch -python3 redirect_shadow.py summarize -python3 redirect_shadow.py historical-candidates -python3 redirect_shadow.py link-outcome --role-run-id RID --influenced-run-id DOWNSTREAM_RID -python3 roles.py link-outcome --role-run-id RID --influenced-run-id DOWNSTREAM_RID +python3 src/roles.py redirect --report-json r.json --ac "..." --dispatch # live offload to the backend +python3 src/redirect_shadow.py record --report-json r.json --ac "..." --dispatch +python3 src/redirect_shadow.py summarize +python3 src/redirect_shadow.py historical-candidates +python3 src/redirect_shadow.py link-outcome --role-run-id RID --influenced-run-id DOWNSTREAM_RID +python3 src/roles.py link-outcome --role-run-id RID --influenced-run-id DOWNSTREAM_RID ``` All `redirect` invocations print a dry-run plan and a SHADOW banner; none mutate state. Live dispatches @@ -701,10 +701,10 @@ boundaries, risks, and confidence. ### CLI ```bash -python3 roles.py route --role prompt -python3 roles.py prompt --target owner/repo#N --goal "..." --task-type implement \ +python3 src/roles.py route --role prompt +python3 src/roles.py prompt --target owner/repo#N --goal "..." --task-type implement \ --target-detail "issue body or PR context" [--proposal-json p.json] -python3 roles.py prompt --target owner/repo#N --goal "..." --task-type implement --dispatch +python3 src/roles.py prompt --target owner/repo#N --goal "..." --task-type implement --dispatch ``` ## DecomposerAgent — the third role (built 2026-06-20) @@ -724,10 +724,10 @@ verification, and re-decomposition triggers. ### CLI ```bash -python3 roles.py route --role decomposer -python3 roles.py decompose --goal "..." --repo owner/repo --target owner/repo#N \ +python3 src/roles.py route --role decomposer +python3 src/roles.py decompose --goal "..." --repo owner/repo --target owner/repo#N \ [--subtask-count 3] [--proposal-json plan.json] -python3 roles.py decompose --goal "..." --repo owner/repo --dispatch +python3 src/roles.py decompose --goal "..." --repo owner/repo --dispatch ``` ## TriageAgent — the fourth role (built 2026-06-20) @@ -750,9 +750,9 @@ into advisory recommendations: work now, defer, needs scope, skip, monitor, and ### CLI ```bash -python3 roles.py route --role triage -python3 roles.py triage --backlog-json ~/.codex/handoff/backlog.json [--proposal-json triage.json] -python3 roles.py triage --backlog-json ~/.codex/handoff/backlog.json --dispatch +python3 src/roles.py route --role triage +python3 src/roles.py triage --backlog-json ~/.codex/handoff/backlog.json [--proposal-json triage.json] +python3 src/roles.py triage --backlog-json ~/.codex/handoff/backlog.json --dispatch ``` ## AdjudicatorAgent — the fifth role (built 2026-06-20) @@ -775,7 +775,7 @@ evidence. ### CLI ```bash -python3 roles.py route --role adjudicator -python3 roles.py adjudicate --case-json case.json [--proposal-json adjudication.json] -python3 roles.py adjudicate --case-json case.json --dispatch +python3 src/roles.py route --role adjudicator +python3 src/roles.py adjudicate --case-json case.json [--proposal-json adjudication.json] +python3 src/roles.py adjudicate --case-json case.json --dispatch ``` diff --git a/CLAUDE.md b/CLAUDE.md index 41ff980..4118733 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -156,8 +156,8 @@ back. Adding a "new" feature that already exists is the easy mistake here. "calibrat", "adversar", "drain" before building routing/recovery/calibration/review/quota work). 2. Read the historical dormancy inventory: `Code/Audits/Orchestrator/2026-07-08-dormancy-rescan.md`, then generate current activation truth - with `python3 capabilities.py inventory`. Feature maturity is not activation evidence. -3. Search the improvement log — `python3 improvement_log.py search `. Items carry status + with `python3 src/capabilities.py inventory`. Feature maturity is not activation evidence. +3. Search the improvement log — `python3 src/improvement_log.py search `. Items carry status notes and many "ideas" are already DONE. The log is machine-local evidence living outside the tree, so **use the accessor, never a path**: it resolves `$ORCH_LOCAL_RUNTIME` for you, prints each hit under the item that owns it, and — when the log is not on this machine — names what is @@ -173,7 +173,7 @@ or "exists at file:line, dormant behind FLAG; activating." **Record it in the ca after it was written. **`ADDING_CAPABILITIES.md` is the procedure, and it is ENFORCED.** Run -`python3 capability_admission.py --preflight ''` before writing code: a capability must +`python3 src/capability_admission.py --preflight ''` before writing code: a capability must arrive with a dedup finding, a caller, a heartbeat, a recurrence fixture, an outcome path, a kill switch, a rollback, an expiry-or-cadence, **and a surface that can offer it**. `test_capability_admission.py` fails the suite otherwise, and also fails on a citation to a dated @@ -204,16 +204,30 @@ Do not create a second event log, model registry, or capability inventory. (default `~/.codex/orchestrator`), the machine-local state, which is never committed. `cmp`-clean is not agreement: re-run the verdict FROM THE MIRROR, since a path resolved relative to a module's own directory is right in one tree and wrong in the other. +- **The modules live in `src/`, the tests in `tests/`, and the CHECKOUT ROOT IS NOT THE MODULE + DIRECTORY.** Those were the same directory until 2026-08-23, and every path in the tree was + derived from that accident. Two questions with two answers now: sibling modules resolve from + `paths.MODULE_DIR`, while `orchestrate.sh`, `.verify-floor.json`, `pyproject.toml` and the docs + resolve from `paths.REPO_ROOT`. **Never write `Path(__file__).resolve().parent` for a repo-root + file, and never hardcode `parent.parent` for it either** — `paths.checkout_root(module_dir)` + applies the rule, and the rule is DETECTED (module dir named `src` ⇒ checkout is its parent, else + they coincide) because THE MIRROR IS FLAT. A hardcoded prefix is right in one tree and wrong in + the other, which is the failure `capability_activation_audit._fleet_roots` already documents. + `orchestrate.sh` does the same detection in shell for `$ORCH`. Verify with + `python3 src/verify.py`. - **A remote merge is inert until the mirror is synced.** Keep that gap manual. It is the only circuit breaker between an agent's change and the dispatcher that dispatches those agents. + **The `src/` move needs a one-time patch to `orch-sync-mirror.sh`, which lives outside the repo: + its `cp "$SRC"/*.py` now matches nothing.** The patch and how to confirm it are in + `docs/MIRROR_SYNC_PATCH.md`. Until it is applied the mirror has no modules. - Run the touched module's `--selftest` (the project's test suite). Add a selftest case for new behavior, including a deliberate-break→revert demonstration for correctness-critical logic. - Register or update lifecycle state in `capabilities.py` for any new/wired capability. Run - `python3 capabilities.py --selftest` and `python3 capabilities.py --json validate`. Never mark a + `python3 src/capabilities.py --selftest` and `python3 src/capabilities.py --json validate`. Never mark a capability active from code existence, a passing selftest, or a feature-registry maturity alone; activation requires executable producer, consumer, outcome, expiry, kill-switch, and rollback evidence. -- **Verify with `python3 verify.py`, never with `for t in test_*.py; do python3 "$t"; done`.** +- **Verify with `python3 src/verify.py`, never with `for t in test_*.py; do python3 "$t"; done`.** Most test files are pytest-only: run directly they define their tests, execute nothing, and exit 0 — which is how 9 failures and a two-month-old broken selftest went unnoticed. `verify.py` runs real pytest, reads the COUNTS rather than the exit status, enforces a collection floor so tests @@ -334,7 +348,7 @@ and update the gated-features list in README.md + the dormancy inventory. When you activate a dormant feature, un-gate a flag, or add a subsystem: update its lifecycle record, regenerate the capability inventory, update README.md's functionality section if the topology changed, and record a status note on the relevant improvement-log item with -`python3 improvement_log.py append ""`. Use the accessor rather than editing a +`python3 src/improvement_log.py append ""`. Use the accessor rather than editing a file: the log is machine-local (outside the tree), the accessor finds the item and places the dated note inside it, and it REFUSES on an ambiguous or unknown ref rather than guessing — a note filed against the wrong item corrupts the record it exists to improve. Do not duplicate lifecycle verdicts diff --git a/IMPROVEMENT_BACKLOG.md b/IMPROVEMENT_BACKLOG.md index a976791..7247a7c 100644 --- a/IMPROVEMENT_BACKLOG.md +++ b/IMPROVEMENT_BACKLOG.md @@ -7,9 +7,9 @@ is this instance's EVIDENCE, not the tool, so it lives outside the tree with the accessor, which resolves the path for you: ```bash -python3 improvement_log.py search # CLAUDE.md §0 step 3 — is this already DONE? -python3 improvement_log.py append "" # CLAUDE.md §5 — record a status note -python3 improvement_log.py path # where it resolved to, and whether it is here +python3 src/improvement_log.py search # CLAUDE.md §0 step 3 — is this already DONE? +python3 src/improvement_log.py append "" # CLAUDE.md §5 — record a status note +python3 src/improvement_log.py path # where it resolved to, and whether it is here ``` `search` prints each hit under the item heading that owns it, plus the number of lines and sections it diff --git a/ORCHESTRATOR.md b/ORCHESTRATOR.md index a9ef9c2..fa044fd 100644 --- a/ORCHESTRATOR.md +++ b/ORCHESTRATOR.md @@ -45,9 +45,9 @@ All under `~/.codex/orchestrator/`. Run with the homebrew/local PATH exported (c cursor-agent live outside the default PATH): `export PATH="/opt/homebrew/bin:$HOME/.local/bin:$HOME/.cursor/bin:$PATH"` -- **Assess capacity** — `python3 capacity.py` → per-agent `{ok|warn|shed}`. Who has headroom right +- **Assess capacity** — `python3 src/capacity.py` → per-agent `{ok|warn|shed}`. Who has headroom right now? (codex/claude default OK + 429-shed; cursor=free/unlimited; vibe=subscription; aider=paygo (LOCAL_POLICY.md).) -- **Observe fleet health** — `python3 observability_dashboard.py [--json] [--write-markdown path]` +- **Observe fleet health** — `python3 src/observability_dashboard.py [--json] [--write-markdown path]` builds a read-only productivity/quality dashboard from the feedback DB plus a live capacity snapshot: outcome coverage, merged/durable-success rates, durability failures, capacity warnings, learned top-agent-by-task, process-improvement signals, keepalive-supervisor gate status, production-flow @@ -56,18 +56,18 @@ cursor-agent live outside the default PATH): that split alerts into immediate operator work, data-gated waits, and informational status. The weekly `orchestrate.sh` cadence writes `$ORCH_STATE_DIR/observability-dashboard.json` and `.md` alongside `periodic-report.json`. -- **Discover work** — `python3 backlog.py --dry-run` → actionable items `{target, task_type, lane}` +- **Discover work** — `python3 src/backlog.py --dry-run` → actionable items `{target, task_type, lane}` (ready issues + in-flight agent PRs across the fleet; scope-blocked excluded). `--live` refreshes. -- **Consult the rule-based prior (OPTIONAL)** — `python3 router.py --dry-run` → what a deterministic +- **Consult the rule-based prior (OPTIONAL)** — `python3 src/router.py --dry-run` → what a deterministic planner *would* do. A second opinion to weigh, not an instruction. Ignore it when your read differs. -- **Delegate one task** — `python3 dispatcher.py delegate --agent +- **Delegate one task** — `python3 src/dispatcher.py delegate --agent --target --lane (--prompt "" | --prompt-file )` → `{pid, log, worktree}`. This claims the target, provisions a LOCAL-disk worktree, and spawns the agent **detached** with your prompt (auth + PATH + claim-release-on-exit all handled). **You write the prompt** — that's where your judgment goes. Use inline `--prompt` for compact one-off prompts and `--prompt-file` only when the brief is large or reusable. -- **Offload one synchronous read/proposal** — `python3 dispatcher.py offload --agent +- **Offload one synchronous read/proposal** — `python3 src/dispatcher.py offload --agent --cwd --prompt "" [--isolate]` → cheap-agent result printed back to stdout, with no claim, commit, push, or PR. Prefer inline `--prompt` for bounded read/review/proposal work so the workflow does not create throwaway prompt documents; `--prompt-file` remains available for large reusable briefs. @@ -98,7 +98,7 @@ cursor-agent live outside the default PATH): the ambient proxy/CA/`NODE_OPTIONS` vars so the culprit is legible. (Diagnosed via per-session evidence: one session's codex+gemini offloads were 6/6 hung while a concurrent session's were 0/6 — inherited env, not contention/auth/desktop-app/concurrency, all of which were ruled out.) -- **Review a large corpus in bounded partitions** — use `python3 dispatcher.py review-corpus prepare +- **Review a large corpus in bounded partitions** — use `python3 src/dispatcher.py review-corpus prepare --corpus corpus.json --plan plan.json`, then `review-corpus run --plan plan.json --results-dir --agent --cwd [--timeout N]`, then `review-corpus synthesize --plan plan.json --results-dir --output synthesis.json [--adjudicator-agent ]`. The corpus groups items with @@ -110,9 +110,9 @@ cursor-agent live outside the default PATH): run/model/log/timeout provenance (the bounded default is 300 seconds). Synthesis reads the expected partition IDs from the hashed plan and returns `INCOMPLETE` when any partition is missing, failed, stale, or invalid; it never infers completeness from the files that happen to exist. Optional adjudication is advisory and also uses `dispatcher.offload`. -- **Concurrency** — `python3 claims.py` (who's working what) · `claims.py release ` · +- **Concurrency** — `python3 src/claims.py` (who's working what) · `claims.py release ` · `claims.py reap` (clear stale at the start of a cycle). -- **Monitor** — `python3 watch.py --agent --target --pid --log --worktree +- **Monitor** — `python3 src/watch.py --agent --target --pid --log --worktree [--lane ] [--task-type ] [--base-ref origin/] [--expected-path ] [--attempt-history-json prior.json] [--stale-seconds 600] [--json]` (conservative running/progress/stalled/exited + semantic drift hints + @@ -122,52 +122,52 @@ cursor-agent live outside the default PATH): paths outside that scope ask the orchestrator to inspect before redirecting. `policy_decision` can escalate repeated stalls/drift to `decompose`, and `redirect_plan` shows the concrete stop/release/redelegate sequence with mutating steps marked as requiring confirmation. -- **Automatic local watch sweep (NEW)** — `python3 redirect_sweep.py [--write path] [--json]` scans +- **Automatic local watch sweep (NEW)** — `python3 src/redirect_sweep.py [--write path] [--json]` scans active local claims, reconstructs watch inputs from dispatcher-stamped metadata, and emits a shadow-only advisory report. `orchestrate.sh` writes `$ORCH_STATE_DIR/redirect-sweep.json` every tick. By default it never kills processes, releases claims, delegates, dispatches RedirectAgent, or runs `redirect_plan --apply`; use it as the automatic detector and inspect/apply separately. For measurement-only evidence, add `--record-corpus --dispatch-redirect-agent` to append capped, deduped RedirectAgent shadow proposals for eligible actions (default `redirect,decompose`) without applying any recovery plan. The cron hook only - enables this when `ORCH_REDIRECT_SWEEP_RECORD_CORPUS=1`. Use `python3 redirect_sweep.py --doctor + enables this when `ORCH_REDIRECT_SWEEP_RECORD_CORPUS=1`. Use `python3 src/redirect_sweep.py --doctor [--json]` to verify cadence wiring, last report freshness, current actionable claim count, corpus readiness, and that autonomous redirect remains disabled. -- **Plan redirect/decompose (NEW)** — `python3 redirect_plan.py --report-json watch-report.json +- **Plan redirect/decompose (NEW)** — `python3 src/redirect_plan.py --report-json watch-report.json [--next-agent ] [--lane ] [--task-type ] [--prompt-file prompt.md] [--json]` converts a watch report into a dry-run recovery plan. It emits inspection commands, optional kill/claim-release commands, and a retry/decomposition prompt. Add `--apply --confirm-target ` only after inspecting the plan; apply writes the prompt first, refuses placeholder commands, skips `kill` if the PID is already gone, releases the claim, then runs the delegated retry/decomposed slice. -- **Run an agent-role in shadow (NEW)** — `python3 roles.py route --role redirect` shows the - router-chosen backend for a role; `python3 roles.py redirect --report-json .json +- **Run an agent-role in shadow (NEW)** — `python3 src/roles.py route --role redirect` shows the + router-chosen backend for a role; `python3 src/roles.py redirect --report-json .json --ac "" [--proposal-json p.json | --dispatch]` runs **RedirectAgent**: it routes a backend (via `route_role` — same capacity + learned weights, claude reserved), authors a corrected prompt, and PROPOSES a dry-run plan by feeding `redirect_plan.py` (its prompt rides the new `prompt_override`). It NEVER mutates — apply stays the human/seat-gated `redirect_plan.py --apply --confirm-target`. Live dispatch returns a `role_run_id`; after the role's plan is accepted/applied and the downstream run has an outcome, link it with - `python3 roles.py link-outcome --role-run-id RID --influenced-run-id DOWNSTREAM_RID` so the learner can + `python3 src/roles.py link-outcome --role-run-id RID --influenced-run-id DOWNSTREAM_RID` so the learner can update `role:` backend fit. Roles are typed contracts with swappable backends; see - [`ARCHITECTURE.md`](ARCHITECTURE.md). Selftest: `python3 roles.py --selftest`. + [`ARCHITECTURE.md`](ARCHITECTURE.md). Selftest: `python3 src/roles.py --selftest`. - **Measure RedirectAgent before promoting it (NEW)** — use - `python3 redirect_shadow.py record --report-json .json --ac "" --dispatch` + `python3 src/redirect_shadow.py record --report-json .json --ac "" --dispatch` on real stalls to append a local, shadow-only proposal event; use - `python3 redirect_shadow.py summarize` / `python3 roles.py summarize-proposals` to review proposal + `python3 src/redirect_shadow.py summarize` / `python3 src/roles.py summarize-proposals` to review proposal validity, baseline disagreement, linked outcomes, and `ready_for_supervised_apply`. If an accepted role plan is actually applied, link it with - `python3 redirect_shadow.py link-outcome --role-run-id RID --influenced-run-id DOWNSTREAM_RID`. - Use `python3 redirect_shadow.py historical-candidates` to find old keepalive-shadow PRs worth replaying + `python3 src/redirect_shadow.py link-outcome --role-run-id RID --influenced-run-id DOWNSTREAM_RID`. + Use `python3 src/redirect_shadow.py historical-candidates` to find old keepalive-shadow PRs worth replaying through RedirectAgent. Those rows are candidates only: they are not counted as RedirectAgent proposal evidence until a fresh/blinded RedirectAgent proposal is recorded and later linked to an outcome. `redirect_sweep.py --record-corpus --dispatch-redirect-agent` is the automatic watch-sweep bridge for collecting these fresh proposal rows; entries are tagged `source=redirect-sweep-live`. Autonomous redirect remains OFF until enough synced outcome evidence exists. -- **Plan post-escalation keepalive supervision (NEW)** — `python3 keepalive_supervisor.py --list-live +- **Plan post-escalation keepalive supervision (NEW)** — `python3 src/keepalive_supervisor.py --list-live [--write-report-dir DIR] [--json]` finds only open `agents:keepalive` PRs already escalated with `needs-human` or `agent:needs-attention`, then emits eligibility, blockers, a RedirectAgent report JSON, a review-only proposal command, a Stage 2 corpus-record command, and an outcome-link template. It is Stage 1 only: no labels, claim release, delegation, or redirect-plan apply. Use - `python3 keepalive_supervisor.py --stage2-plan [--stage2-backend cursor] [--historical-backend cursor] + `python3 src/keepalive_supervisor.py --stage2-plan [--stage2-backend cursor] [--historical-backend cursor] [--json]` for the Stage 2 acquisition loop: it writes runnable local report artifacts, de-dupes already-recorded **valid** live Stage 2 targets, emits live record commands when unrecorded post-escalation targets exist, otherwise emits a bounded `redirect_shadow.py collect-historical @@ -177,31 +177,31 @@ cursor-agent live outside the default PATH): (for example while Gemini/AGY is unhealthy). Invalid live dispatches remain retryable and keep bounded backend diagnostics in the RedirectAgent corpus. The periodic report and dashboard summarize the Stage 2 proposal corpus so promotion waits on linked proposal evidence. -- **Run PromptAgent in shadow (NEW)** — `python3 roles.py route --role prompt` shows the router-chosen - backend for prompt authoring; `python3 roles.py prompt --target owner/repo#N --goal "..." --task-type +- **Run PromptAgent in shadow (NEW)** — `python3 src/roles.py route --role prompt` shows the router-chosen + backend for prompt authoring; `python3 src/roles.py prompt --target owner/repo#N --goal "..." --task-type implement --target-detail "" [--proposal-json p.json | --dispatch]` returns a dispatch-ready prompt with definition-of-done, acceptance criteria, validation, expected paths, out-of-scope boundaries, and risks. It NEVER delegates or mutates. The emitted `task_type` must match the deterministic input task type; PromptAgent cannot replace router selection or dispatcher execution. -- **Run DecomposerAgent in shadow (NEW)** — `python3 roles.py route --role decomposer` shows the - router-chosen backend for decomposition; `python3 roles.py decompose --goal "..." --repo owner/repo +- **Run DecomposerAgent in shadow (NEW)** — `python3 src/roles.py route --role decomposer` shows the + router-chosen backend for decomposition; `python3 src/roles.py decompose --goal "..." --repo owner/repo [--target owner/repo#N] [--subtask-count N] [--proposal-json plan.json | --dispatch]` returns a validated `epic_lane.py` plan plus dispatch-prompt records. It NEVER delegates or mutates. Invalid plans fall back to the deterministic planner prompt only; no placeholder subtask plan is emitted. -- **Run TriageAgent in shadow (NEW)** — `python3 roles.py route --role triage` shows the router-chosen - backend for backlog triage; `python3 roles.py triage --backlog-json ~/.codex/handoff/backlog.json +- **Run TriageAgent in shadow (NEW)** — `python3 src/roles.py route --role triage` shows the router-chosen + backend for backlog triage; `python3 src/roles.py triage --backlog-json ~/.codex/handoff/backlog.json [--proposal-json triage.json | --dispatch]` returns advisory work-now/defer/needs-scope/skip/monitor recommendations and optional batches. It NEVER selects worker agents, changes task types/lanes, claims, labels, delegates, or mutates state. Invalid plans fall back to deterministic backlog order. -- **Run AdjudicatorAgent in shadow (NEW)** — `python3 roles.py route --role adjudicator` shows the - router-chosen backend for disputed-blocker adjudication; `python3 roles.py adjudicate --case-json +- **Run AdjudicatorAgent in shadow (NEW)** — `python3 src/roles.py route --role adjudicator` shows the + router-chosen backend for disputed-blocker adjudication; `python3 src/roles.py adjudicate --case-json case.json [--proposal-json adjudication.json | --dispatch]` returns advisory `uphold_blocker`/`reject_blocker`/`needs_more_evidence` guidance tied to supplied ground-truth refs. It NEVER emits terminal `PASS`/`FAIL`/`BLOCKED` verdicts, overrides `runtime_ac_panel.py` or `adversarial.py` aggregation math, merges, labels, claims, delegates, or mutates state. - **Redirect** — `kill ` → `claims.py release ` → re-`delegate` with a different agent or a sharper prompt. -- **Verify frontend (NEW)** — `python3 frontend_verify.py --url --assert "text:" +- **Verify frontend (NEW)** — `python3 src/frontend_verify.py --url --assert "text:" --assert "role:[=]" [--click-text --then-text ] [--browser-endpoint http://127.0.0.1:9222]` → VISION-FREE UI verification via the ARIA snapshot (token-cheap, deterministic; opt-in `--screenshot` for canvas/SVG). Lets ANY lane — @@ -211,12 +211,12 @@ cursor-agent live outside the default PATH): problems. If the macOS sandbox blocks direct Chromium launch, start an authorized Chrome/Chromium with remote debugging outside the sandbox and pass `--browser-endpoint`, or set `ORCH_FRONTEND_VERIFY_BROWSER_ENDPOINT`. Runtime AC specs can set `runtime_context.browser_endpoint`. - Use `python3 frontend_verify.py --doctor [--require-browser-endpoint] [--json]` before cron/sandboxed + Use `python3 src/frontend_verify.py --doctor [--require-browser-endpoint] [--json]` before cron/sandboxed checks to verify the local helper, Node runtime, and CDP endpoint readiness; it emits structured JSON and launch commands for an authorized browser when the endpoint is absent or unreachable. The Trip Planner live exercise passed `/health`, `/login`, and login→signup click-flow assertions. - (backlog #2 / `BRIEF_expand_range.md` #1) -- **Gate generated tests (NEW)** — `python3 testgen_gate.py --repo --source + (backlog #2 / `docs/briefs/BRIEF_expand_range.md` #1) +- **Gate generated tests (NEW)** — `python3 src/testgen_gate.py --repo --source --baseline-pytest-args "" --candidate-pytest-args ""` → assured-acceptance gate for LLM-generated pytest tests: collection/import, baseline non-regression, repeated candidate reliability, and coverage covered-lines delta. Hand this to a test-generation lane @@ -224,55 +224,55 @@ cursor-agent live outside the default PATH): the generated tests on large suites. Coverage JSON generation forces `--fail-under=0`, so repo-level coverage thresholds do not override the gate's own delta verdict. Live exercise: Inv-Man-Intake `workflow_validation` via isolated Gemini offload passed with +20 covered lines and 3/3 reliability. - (backlog #2 / `BRIEF_expand_range.md` #2) -- **Build a test-generation lane prompt (NEW)** — `python3 testgen_lane.py --repo --source + (backlog #2 / `docs/briefs/BRIEF_expand_range.md` #2) +- **Build a test-generation lane prompt (NEW)** — `python3 src/testgen_lane.py --repo --source --baseline-pytest-args "" --candidate-pytest-args "" [--target owner/repo#N] [--context-file brief.md]` → writes a delegation prompt that instructs an agent to generate pytest tests, run the exact `testgen_gate.py` acceptance command, iterate until it passes, and only then commit/push/open a PR. Backlog labels `tests`/`coverage` now classify to `task_type=testgen`; route defaults avoid Claude and start with codex/cursor/vibe before Gemini. -- **Build or validate an epic decomposition plan (NEW)** — `python3 epic_lane.py --goal "" +- **Build or validate an epic decomposition plan (NEW)** — `python3 src/epic_lane.py --goal "" [--repo owner/repo] [--target owner/repo#N] [--context-file brief.md] [--subtask-count N]` emits the - strict planner prompt for a large/vague goal; `python3 epic_lane.py --validate plan.json --json + strict planner prompt for a large/vague goal; `python3 src/epic_lane.py --validate plan.json --json --emit-dispatch-prompts` validates an agent-produced plan and extracts dispatch-ready prompt records. The schema requires epic metadata, dispatchable subtasks, integration order, and re-decomposition triggers. Backlog labels `epic`/`planning`/`decomposition`/`multi-issue`/`roadmap` classify to `task_type=epic`; route defaults avoid Claude and spend Gemini/AGY first unless its capacity policy says to reserve it. -- **Build or validate a codemod/refactor campaign (NEW)** — `python3 codemod_lane.py --goal "" +- **Build or validate a codemod/refactor campaign (NEW)** — `python3 src/codemod_lane.py --goal "" [--repo owner/repo] [--target owner/repo#N] [--context-file brief.md]` emits a strict campaign-authoring - prompt; `python3 codemod_lane.py --validate campaign.json` validates agent-produced campaign JSON; - `python3 codemod_lane.py --plan campaign.json --json [--emit-delegation-prompt]` produces a dry-run plan + prompt; `python3 src/codemod_lane.py --validate campaign.json` validates agent-produced campaign JSON; + `python3 src/codemod_lane.py --plan campaign.json --json [--emit-delegation-prompt]` produces a dry-run plan with review-before-run commands for ast-grep/Comby/jscodeshift/OpenRewrite/custom when enough fields are present. This increment never auto-applies codemods or opens batched PRs. Backlog labels `codemod`/`refactor`/`refactoring`/`structural`/`bulk-change`/`campaign` classify to `task_type=codemod`; route defaults avoid Claude and start with cursor/vibe/codex. -- **Build or validate a cross-repo coordinated-change plan (NEW)** — `python3 cross_repo_lane.py --goal +- **Build or validate a cross-repo coordinated-change plan (NEW)** — `python3 src/cross_repo_lane.py --goal "" [--source-repo owner/repo] [--consumer owner/repo] [--target owner/repo#N] [--context-file brief.md]` emits a strict coordination-authoring prompt; - `python3 cross_repo_lane.py --validate coordination.json` validates source/consumer rollout JSON; - `python3 cross_repo_lane.py --plan coordination.json --json [--emit-dispatch-prompts]` produces a dry-run + `python3 src/cross_repo_lane.py --validate coordination.json` validates source/consumer rollout JSON; + `python3 src/cross_repo_lane.py --plan coordination.json --json [--emit-dispatch-prompts]` produces a dry-run rollout plan with source/consumer work items, dependency/barrier ordering, and dispatch-ready prompts. This increment never creates branches, labels, issues, PRs, or merges. Backlog labels `cross-repo`/`multi-repo`/`coordinated-change`/`consumer-sync`/`sync-manifest`/`dependency-graph`/ `contract-change` classify to `task_type=cross_repo`; route defaults avoid Claude and prioritize Gemini/AGY for this planning-heavy lane. - **Ingest consumer-sync evidence without consumer writes (NEW)** — - `python3 consumer_sync_artifact_ingest.py preview` validates the latest successful + `python3 src/consumer_sync_artifact_ingest.py preview` validates the latest successful `health-69-consumer-sync-shadow-evidence.yml` artifact, its run/attempt-bound handoff, and up to five registered consumers' downloaded default-branch snapshots without writing state. `ingest` records idempotent local capability evidence behind a lock. The active-only daily cadence runs a bounded human-on-exception phase through 2026-07-25, then returns to shadow evidence. Neither mode has a consumer/GitHub mutation path; inspect `consumer-sync-artifact-ingest-report.json` for exceptions. -- **Build or validate a runtime AC verification plan (NEW)** — `python3 runtime_ac.py --goal "" +- **Build or validate a runtime AC verification plan (NEW)** — `python3 src/runtime_ac.py --goal "" [--repo owner/repo] [--target owner/repo#N] [--context-file brief.md]` emits a strict - acceptance-criteria evidence-authoring prompt; `python3 runtime_ac.py --validate spec.json` validates - AC-bound verification JSON; `python3 runtime_ac.py --plan spec.json --json [--emit-commands]` produces + acceptance-criteria evidence-authoring prompt; `python3 src/runtime_ac.py --validate spec.json` validates + AC-bound verification JSON; `python3 src/runtime_ac.py --plan spec.json --json [--emit-commands]` produces a dry-run plan with review-before-run commands for `frontend_verify.py`, `local_verify.py`, command - checks, and manual evidence. `python3 runtime_ac.py --run spec.json --confirm-run` executes selected + checks, and manual evidence. `python3 src/runtime_ac.py --run spec.json --confirm-run` executes selected verifier/tool checks and gates results as `PASS`/`FAIL`/`NEEDS_REVIEW`; command/non-regression checks require the additional `--allow-command-checks` flag, and shell-control commands are refused. Use - `python3 runtime_ac.py --results spec.json --result-json results.json` to gate externally collected + `python3 src/runtime_ac.py --results spec.json --result-json results.json` to gate externally collected evidence, and `--record-run-id ` to patch `outcomes.verifier_verdict`. For closer PRs, `tick.py` treats those same labels, or a spec at `~/.codex/orchestrator/runtime-ac/.json`, as a required runtime-AC gate: dry-runs report it under `runtime_ac_gates`; active ticks block progression @@ -281,16 +281,16 @@ cursor-agent live outside the default PATH): timeout. This is a **hard opt-in machine gate**, not an advisory review: only explicitly labeled or target-spec-backed closer work is eligible, but eligible active work fails closed. The adversarial reviewer/panel path remains advisory and is adjudicated against ground truth. Use - `python3 runtime_ac_gate.py --scan-backlog [--json]` to inspect the current backlog for + `python3 src/runtime_ac_gate.py --scan-backlog [--json]` to inspect the current backlog for closer PRs that would require the gate, missing specs, and active-execution blockers without running any verification checks. Backlog labels `runtime-ac`/`runtime-verification`/`acceptance-criteria`/`verification-spec`/ `verification-plan`/`ac-checks`/`runtime-checks` classify to `task_type=runtime_ac`; route defaults avoid Claude and prioritize Gemini/AGY for this planning-heavy lane. A finished range-lane spec is not gate input until - `python3 runtime_ac_gate.py --materialize-range-spec spec.json --target owner/repo#N --json` + `python3 src/runtime_ac_gate.py --materialize-range-spec spec.json --target owner/repo#N --json` validates exact target/repo attribution and atomically installs it at the same path/hash consumed by the closer gate. Invalid, mismatched, or unwritable artifacts record a terminal non-installed reason. -- **Roll out range lanes from backlog (NEW)** — `python3 range_lane_rollout.py [--json] +- **Roll out range lanes from backlog (NEW)** — `python3 src/range_lane_rollout.py [--json] [--task-type testgen|epic|codemod|cross_repo|runtime_ac] [--max-dispatches N]` previews first-class opener dispatches for specialized range-lane work only. It reads live backlog state by default without writing the handoff cache; pass `--cached-backlog` only when intentionally inspecting the last @@ -300,52 +300,52 @@ cursor-agent live outside the default PATH): `--apply --confirm-rollout` plus `ORCH_RANGE_LANE_ROLLOUT=1`; it still refuses closer/non-range work and backup/paygo assignments. This is the rollout/apply layer over the range helpers, not a replacement for their strict JSON validation and gate commands. -- **Guard a terminal merge with runtime AC (NEW)** — `python3 merge_guard.py owner/repo#N` dry-runs the - merge command and reports whether runtime AC is required. `python3 merge_guard.py owner/repo#N +- **Guard a terminal merge with runtime AC (NEW)** — `python3 src/merge_guard.py owner/repo#N` dry-runs the + merge command and reports whether runtime AC is required. `python3 src/merge_guard.py owner/repo#N --confirm-merge [--method squash|merge|rebase]` is the terminal merge path when a human or local orchestrator action would otherwise call `gh pr merge` directly. It fails closed if PR metadata cannot be read, draft/non-open PRs are supplied, a required runtime-AC spec is missing, `ORCH_RUN_RUNTIME_AC=1` is absent, or the gate verdict is not `PASS`. It uses the shared `runtime_ac_gate.py` helper, so tick and - terminal merges enforce the same policy. Use `python3 runtime_ac_gate.py --exercise [--json]` for a + terminal merges enforce the same policy. Use `python3 src/runtime_ac_gate.py --exercise [--json]` for a non-mutating active-gate smoke when no live backlog closer currently requires runtime AC; it writes a temporary command spec, runs the real gate executor, and removes the spec without patching feedback. Use - `python3 runtime_ac_flow_monitor.py --json` for current truth: firing comes from structured + `python3 src/runtime_ac_flow_monitor.py --json` for current truth: firing comes from structured `runtime_ac_gate` completion events and the denominator is required active gate events, not all closer proxies. The daily cadence writes `runtime-ac-flow-monitor.json` and exposes failures/backoff in the dashboard. `runtime_ac_gate.py --scan-history` remains an archival inspection helper; cron text is not used for live target/spec attribution or alerts. -- **Adjudicate runtime AC with a panel (NEW)** — `python3 runtime_ac_panel.py --prompt spec.json --gate +- **Adjudicate runtime AC with a panel (NEW)** — `python3 src/runtime_ac_panel.py --prompt spec.json --gate gate.json --reviewer gemini` builds a strict JSON-only judge prompt from a runtime AC spec and gate result; send those prompts through offload/review lanes with inline `dispatcher.py offload --prompt` when the work deserves multiple eyes. Collect the returned reviewer JSON into `reviews.json`, then run - `python3 runtime_ac_panel.py --adjudicate spec.json + `python3 src/runtime_ac_panel.py --adjudicate spec.json --gate gate.json --reviews reviews.json [--record-run-id RUN]`. The adjudicator requires enough reviewers, treats automated gate failure as failure, requires corroborated high-severity fail vetoes before returning `FAIL`, returns `NEEDS_REVIEW` for lone evidence-backed vetoes/disagreement, does not let a bare unsubstantiated `FAIL` label defeat a strong passing panel, and records reviewer evidence gaps when - patching feedback. To run the reviewer panel directly, use `python3 runtime_ac_panel.py --dispatch + patching feedback. To run the reviewer panel directly, use `python3 src/runtime_ac_panel.py --dispatch spec.json --gate gate.json --reviewers vibe,gemini,cursor [--cwd ] [--record-run-id RUN]`; it sends inline offload prompts, parses fenced or plain JSON reviewer output, synthesizes `NEEDS_REVIEW` records for failed/unparseable reviewers, and adjudicates the collected panel in one command. -- **Run a local deliberate-break gate (NEW)** — `python3 local_verify.py --worktree +- **Run a local deliberate-break gate (NEW)** — `python3 src/local_verify.py --worktree --base-ref --test-cmd "" --test-path ` → verifies that candidate tests pass in the live worktree but fail when run against the base implementation in a temporary copy. Verdicts are `PASS`, `FAIL_BROKEN`, or `FAIL_HOLLOW`. The live worktree is not mutated. Add `--record-run-id ` to patch `outcomes.verifier_verdict`; `FAIL_HOLLOW`/`FAIL_BROKEN` count against relearn even if a PR otherwise looks successful. -- **Ingest LangSmith trace artifacts (NEW)** — `python3 langsmith_pull.py --ndjson +- **Ingest LangSmith trace artifacts (NEW)** — `python3 src/langsmith_pull.py --ndjson [--dry-run] [--json]` → joins Workflows `langsmith-fleet/v1` NDJSON records to known Orchestrator runs by exact `run_id`, then by `github_pr`/`github_issue` + `domain.agent` when the trace run_id is LangSmith's own ID. It retains trace refs/provider/model/status in `execution_traces` and aggregates token/$/latency rows into `costs` with `source=langsmith`. Use `--dry-run` first; unmatched or ambiguous refs are skipped by default so fleet artifacts cannot pollute the learner. -- **Pull direct LangSmith API telemetry (NEW)** — `python3 langsmith_direct.py --dry-run --json` - previews, and `python3 langsmith_direct.py --ingest [--json]` writes, `workflows-agents` telemetry from +- **Pull direct LangSmith API telemetry (NEW)** — `python3 src/langsmith_direct.py --dry-run --json` + previews, and `python3 src/langsmith_direct.py --ingest [--json]` writes, `workflows-agents` telemetry from LangSmith's API into the same `langsmith-fleet/v1` join path used by `langsmith_pull.py`. It uses the official SDK when installed and otherwise falls back to stdlib HTTP (`/sessions` + `/runs/query`), so the daily cadence does not depend on a global package install. The API key comes from `LANGSMITH_API_KEY`, `LANGCHAIN_API_KEY`, or `~/.codex/credentials/langsmith_api_key`. -- **Fetch LangSmith fleet artifacts (NEW)** — `python3 langsmith_fetch.py --ingest [--json]` reads the +- **Fetch LangSmith fleet artifacts (NEW)** — `python3 src/langsmith_fetch.py --ingest [--json]` reads the Workflows fleet registry, downloads each repo's latest `langsmith-fleet.ndjson` GitHub Actions artifact, writes `~/.codex/orchestrator/langsmith-artifacts/combined-fleet.ndjson`, then calls `langsmith_pull.py`. Use `--dry-run --json` to verify artifact availability without downloading. The registry name remains the @@ -368,17 +368,17 @@ cursor-agent live outside the default PATH): Workflows repo needs a deeper rollup search than the default. The periodic report and dashboard treat this as GitHub artifact distribution health, separate from the durable LangSmith telemetry rows populated by direct/API/sink paths. -- **Reconcile local execution ledger (NEW)** — `python3 ledger_reconcile.py reconcile [--dry-run] [--json]` +- **Reconcile local execution ledger (NEW)** — `python3 src/ledger_reconcile.py reconcile [--dry-run] [--json]` joins local delegate start/complete rows and JSON usage events in dispatch logs into `costs(source=ledger)`. It skips unknown `run_id`s and never overwrites a richer `source=langsmith` or `source=ccusage` cost row. -- **Attribute ccusage sessions to runs (NEW)** — `python3 ccusage_reconcile.py reconcile --dry-run --json` - previews, and `python3 ccusage_reconcile.py reconcile [--json]` writes, per-run Codex/Claude usage rows +- **Attribute ccusage sessions to runs (NEW)** — `python3 src/ccusage_reconcile.py reconcile --dry-run --json` + previews, and `python3 src/ccusage_reconcile.py reconcile [--json]` writes, per-run Codex/Claude usage rows into `costs(source=ccusage)`. It joins ccusage `session` totals to dispatcher start/complete windows only when `metadata.lastActivity` lands inside exactly one completed same-agent run window, so active, unsupported, unmatched, and ambiguous sessions are skipped instead of guessed. Codex sessions with a parseable rollout timestamp must also have that timestamp inside the run window; this prevents copied or touched session files with misleading `lastActivity` from inflating the wrong run. -- **Ingest keepalive process outcomes (NEW)** — `python3 keepalive_outcomes.py --include-non-agent` +- **Ingest keepalive process outcomes (NEW)** — `python3 src/keepalive_outcomes.py --include-non-agent` records terminal non-agent bot/human/unlabeled PRs as `source=keepalive`, `assignment=none`, `agent=none`, and classified `work_type`. These rows are for repo/process signals, not per-agent causal learning; `relearn_quality()` ignores them because it only learns from `assignment='experimental'`. @@ -387,17 +387,17 @@ cursor-agent live outside the default PATH): Closed issue-target failures that have been inspected can carry `issue_review=` in outcome notes; reports retain them under `reviewed_issue_failures` and keep only unreviewed non-durable issue rows in the active failure-focused queue. -- **Inject repo playbooks (NEW)** — `python3 repo_knowledge.py [task_type] [lane]` previews +- **Inject repo playbooks (NEW)** — `python3 src/repo_knowledge.py [task_type] [lane]` previews the concise per-repo `REPO PLAYBOOK` block auto-appended to delegated prompts. The registry lives at `experiments/repo_knowledge.json` by default; set `ORCH_REPO_KNOWLEDGE_PATH` to review or test a separate registry. It captures recurring definition-of-done rules and gotchas such as Trend phase-3, Counter_Risk formatting, LMS Postgres migrations, and Workflows sync/doc surfaces. - Use `python3 repo_knowledge.py --search owner/repo[#N] [--query TEXT] [--task-type T] [--lane L]` + Use `python3 src/repo_knowledge.py --search owner/repo[#N] [--query TEXT] [--task-type T] [--lane L]` to retrieve approved playbook entries plus retained run/outcome notes for prompt authoring or triage. Search is read-only and never auto-injects unapproved feedback text. It expands compound/path terms such as `sync-manifest` and `docs/ci/WORKFLOWS.md`, ranks with local TF-IDF plus section boosts, and returns `matched_terms`/`coverage` so the retrieved memory can be adjudicated instead of blindly trusted. - Use `python3 repo_knowledge.py --suggest-from-snapshot data/feedback-snapshot.json` to surface candidate + Use `python3 src/repo_knowledge.py --suggest-from-snapshot data/feedback-snapshot.json` to surface candidate new playbook rules from retained outcome notes; this is a review queue, not automatic prompt injection. Use `--suggest-from-docs [--repo owner/repo] [--include-root-docs]`, `--suggest-from-review-json comments.json --repo owner/repo`, or `--suggest-from-pr owner/repo#N` to mine @@ -415,7 +415,7 @@ cursor-agent live outside the default PATH): labels/title metadata and reports a planned advisory refute-mode panel. Active ticks only run it when `ORCH_RUN_ADVERSARIAL_REVIEW=1`; reviewers default to `codex,vibe,gemini` and can be set with `ORCH_ADVERSARIAL_REVIEWERS`. The result is evidence for adjudication, not an automatic block. -- **Periodic dataset report (NEW)** — `python3 periodic_report.py [--json] [--window-days N] +- **Periodic dataset report (NEW)** — `python3 src/periodic_report.py [--json] [--window-days N] [--min-gap-recurrence N] [--snapshot-json data/feedback-snapshot.json] [--approve-evidence-type NAME --from-gap GAP [--rationale TEXT] [--apply]]` → read-only review of the feedback store: table counts, learned route weights vs the hand-set prior and previous version, @@ -425,20 +425,20 @@ cursor-agent live outside the default PATH): maturity/promotion candidates, process-improvement rollups/signals for non-agent maintenance work, the deferred live keepalive-supervisor trigger gate, LangSmith artifact-distribution vs durable telemetry-sink health, hypothesis status, and optional evidence-type approval. - `python3 dry_seam_audit.py [--json]` is the + `python3 src/dry_seam_audit.py [--json]` is the standalone sink-liveness audit used by the report; its outcome-gap summary reports total no-outcome rows, actionable production-ingest candidates, advisory/expected-unlinked rows, and top categories. Approval is preview-only unless `--apply` is passed, and `--apply` is rejected with `--snapshot-json`, so snapshot review stays read-only. Unlike `relearn_report.py`, it does not run the learner or write `route_weights`. `orchestrate.sh` writes the weekly JSON report to `$ORCH_STATE_DIR/periodic-report.json` (default `~/.codex/orchestrator/`). -- **Record task-end feature reflection (NEW)** — `python3 features.py record --name +- **Record task-end feature reflection (NEW)** — `python3 src/features.py record --name --where --problem "" [--module module.py] [--maturity ad-hoc|reused|hardened]` - logs reusable structures into `experiments/features.json`. Use `python3 features.py summary --json` for - maturity counts and top reused structures, `python3 features.py candidates` for rule-of-three promotion - candidates, and `python3 features.py harden --name --module ` when a pattern becomes a + logs reusable structures into `experiments/features.json`. Use `python3 src/features.py summary --json` for + maturity counts and top reused structures, `python3 src/features.py candidates` for rule-of-three promotion + candidates, and `python3 src/features.py harden --name --module ` when a pattern becomes a selftested module. `periodic_report.py` reads this registry without creating or mutating it. -- **Check judge reliability (NEW)** — `python3 judge_reliability.py [--json]` reports data-gated evaluator +- **Check judge reliability (NEW)** — `python3 src/judge_reliability.py [--json]` reports data-gated evaluator weights from cross-eval agreement plus optional human score anchors. `exp_abcd` uses ready weights for winner synthesis; not-ready judges stay neutral except for legacy fallbacks, so thin data cannot swing synthesis through learned weights. Threshold-ready evidence can still move synthesis, so adjudicate @@ -452,22 +452,22 @@ cursor-agent live outside the default PATH): runtime-AC, and allowlisted repo gates before writing exactly one canonical `synthesis-delivery-candidate.{json,md}`. The candidate preserves experiment, arm, member, evaluator, synthesis, profile, shared-capacity, and accepted influence lineage. It is candidate-only: - use `python3 synthesis_promotion.py link-delivery --run-id RUN --ref owner/repo#N` + use `python3 src/synthesis_promotion.py link-delivery --run-id RUN --ref owner/repo#N` only after the existing Workflows auto-pilot/Keepalive workflow has created the delivery record. Repeated followup is idempotent; merge/durability outcomes mirror to the synthesis/source evidence, while failed verification, expiry, or reversion retires the candidate without remote mutation. -- **Check human calibration readiness (NEW)** — `python3 human_calibration.py [--json]` parses structured +- **Check human calibration readiness (NEW)** — `python3 src/human_calibration.py [--json]` parses structured human score anchors from `human_calibration`, joins them to evaluator proxy scores, and fits a simple proxy-score→human-score regression only after enough matched pairs exist. Until then it reports the missing anchor/pair counts and does not change learner weights. -- **Cluster evidence gaps into schema candidates (NEW)** — `python3 evidence_schema.py [--json]` groups +- **Cluster evidence gaps into schema candidates (NEW)** — `python3 src/evidence_schema.py [--json]` groups recurring free-text `evidence_gaps` into approval-ready evidence-type candidates such as `error_recovery_evidence` and `upload_flow_evidence`. It is read-only by default. Active approval is - explicit and guarded: `python3 evidence_schema.py --apply NAME --confirm-type NAME`; approval records the + explicit and guarded: `python3 src/evidence_schema.py --apply NAME --confirm-type NAME`; approval records the evidence type and marks matching open gaps approved. The report also reviews active evidence types for age, influence, and prune-candidate status. A/B evaluators and runtime-AC panel reviewers now return `cited_evidence_types`; only known active names increment influence. -- **Ingest delegated outcomes (NEW)** — `python3 outcomes.py --mode remote|local|both [--dry-run]` +- **Ingest delegated outcomes (NEW)** — `python3 src/outcomes.py --mode remote|local|both [--dry-run]` records PR state for delegated runs that lack outcomes. `remote` reads the target PR directly; `local` resolves the deterministic `orchestrator/issue-N` branch opened by local delegates. The daily cadence runs local ingest fail-open so dry-seam reports surface only runs whose PR state is still unavailable/open. @@ -539,26 +539,26 @@ prefers least-observed eligible agents; set `ORCH_EXPLORATION_MODE=thompson-hybr sampling challenger selector for a bounded review. **Supervised exploration evidence windows (built 2026-06-22):** use -`python3 exploration_evidence_plan.py` to inspect remaining ε-greedy / Thompson-hybrid evidence deficits, -then `python3 exploration_collection.py` to dry-run a bounded collection window. Active dispatch is never -implicit: it requires `ORCH_EXPLORATION_EVIDENCE=1 python3 exploration_collection.py --apply +`python3 src/exploration_evidence_plan.py` to inspect remaining ε-greedy / Thompson-hybrid evidence deficits, +then `python3 src/exploration_collection.py` to dry-run a bounded collection window. Active dispatch is never +implicit: it requires `ORCH_EXPLORATION_EVIDENCE=1 python3 src/exploration_collection.py --apply --confirm-window`. The command filters to low-risk opener work, caps the temporary exploration rate, and rejects late/paygo, backup, closer, and merge-critical assignments. It does not change the router default. -**Route-coverage backfill (built 2026-06-22):** use `python3 exploration_backfill.py` to inspect missing +**Route-coverage backfill (built 2026-06-22):** use `python3 src/exploration_backfill.py` to inspect missing `(task_type, agent)` cells that keep route-weight coverage gates from passing. It plans targeted `exp_abcd` A/B jobs only for real, unclaimed opener subjects and is read-only by default. Active launch is -guarded: `ORCH_EXPLORATION_BACKFILL=1 python3 exploration_backfill.py --apply --confirm-backfill +guarded: `ORCH_EXPLORATION_BACKFILL=1 python3 src/exploration_backfill.py --apply --confirm-backfill --target owner/repo#N --agents a,b[,c]`. A launched backfill counts nothing by itself; run `exp_abcd.py collect` and `exp_abcd.py evaluate` so real evaluations enter the feedback DB, or let normal production outcomes flow before treating any cell as covered. -**Strategy-value experiments (H4/H5, built 2026-06-23):** use `python3 strategy_experiment.py --hypothesis +**Strategy-value experiments (H4/H5, built 2026-06-23):** use `python3 src/strategy_experiment.py --hypothesis H4 --repo owner/repo --spec-file spec.md --exp-id id --json` to plan a strategy-arm comparison such as single high-cost agent vs high+low parallel+synthesis. The planner expands the strategy arms into the unique implementation agents that `exp_abcd.py prepare` can launch and records the intended `experiments//strategy.json` metadata path. Active prepare is deliberately supervised: -`ORCH_STRATEGY_EXPERIMENT=1 python3 strategy_experiment.py ... --prepare --confirm-strategy`. The normal +`ORCH_STRATEGY_EXPERIMENT=1 python3 src/strategy_experiment.py ... --prepare --confirm-strategy`. The normal research tick still refuses to auto-launch strategy arms; after a guarded launch, run the normal `exp_abcd` status/collect/evaluate/synthesize phases and attribute quality/cost at the strategy-arm level. diff --git a/PLANNING.md b/PLANNING.md index 8f5bfba..5483472 100644 --- a/PLANNING.md +++ b/PLANNING.md @@ -179,7 +179,7 @@ running experiments opportunistically (the research scheduler) and refreshing th the capacity ledger does not retain start-only runs. - [x] **Periodic report generator.** Human-facing review of the dataset: current learned weights vs prior, route-weights version diffs (did a change help?), proposed schema changes, hypothesis status. Reads - the snapshot/DB (`python3 periodic_report.py [--json] [--snapshot-json PATH]`). Read-only by default. + the snapshot/DB (`python3 src/periodic_report.py [--json] [--snapshot-json PATH]`). Read-only by default. This is the human's window into the loop. - [x] **Feature reflection CLI + report surface.** `features.py record` now records task-end reusable structures with optional module/maturity metadata; `summary`, `candidates`, and `harden` expose the diff --git a/README.md b/README.md index 25d1e5e..3bca0cf 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ production duels. > range-lane slot, ship-gate, redirect-corpus intake, and more). The dedup-before-develop check in > CLAUDE.md exists to stop that recurring. The historical dormancy scan is > `Code/Audits/Orchestrator/2026-07-08-dormancy-rescan.md`; current activation truth is generated -> from the local capability ledger with `python3 capabilities.py inventory`. +> from the local capability ledger with `python3 src/capabilities.py inventory`. ## How it runs (execution topology) @@ -40,7 +40,7 @@ HANDOFF: ~/.codex/handoff/ (heartbeat orchestrator.json — legacy lan canonical tree — so canonical edits are yours alone, but always re-sync so the schedule sees them. - **Every module has a `--selftest`.** Run it after editing that module; it is the project's test suite (there is no separate pytest tree). `python3 .py --selftest`. -- **`python3 verify.py` is the whole verdict.** Real pytest plus every module selftest plus the five +- **`python3 src/verify.py` is the whole verdict.** Real pytest plus every module selftest plus the five capability gates, judged on the COUNTS rather than exit codes, against a recorded floor in `.verify-floor.json`. It also bounds SKIPPING: a check needing something only a running instance has (the populated capability ledger, an installed agent CLI, `~/.codex/skills`) skips with the @@ -285,11 +285,11 @@ safety switch, not dead code. no-learning instrumentation. Provider-resolved identity remains null and unclaimed; the canary remains ineligible for Brain ingestion, quality-weight updates, and promotion. Provider-attested finalization is unchanged. Instrumentation completion events are excluded from Pattern Miner input. Run - `python3 model_profile_trial_bridge.py selftest` before preparing a canary. + `python3 src/model_profile_trial_bridge.py selftest` before preparing a canary. - **Pattern-to-capability compiler**: `pattern_miner.py` consumes those seven-phase completion envelopes and emits candidate-only capability IR. It is intentionally non-dispatching: inspect `~/.codex/orchestrator/pattern-miner-status.json`, `pattern-miner-inventory.json`, and - `pattern-miner-state.json` after the daily cadence (or run `python3 pattern_miner.py status` + `pattern-miner-state.json` after the daily cadence (or run `python3 src/pattern_miner.py status` and `inventory`). A useful first check-in is after 7 daily runs or 20 accepted episodes, whichever comes first; review candidate evidence, counterexamples, and expiry before promoting anything. Deterministic candidates can then be dry-compiled by `capability_compiler.py`; its reference rail @@ -299,7 +299,7 @@ safety switch, not dead code. emits only read-only create/update/remove/skip/no-change proposals, and `runner_effect_bridge.py` validates provider-neutral runner effect evidence before recording idempotent outcomes or counterexamples in the existing capability ledger. Run - `python3 consumer_sync_shadow.py dashboard` to see distinct effects, harms, reduced-supervision + `python3 src/consumer_sync_shadow.py dashboard` to see distinct effects, harms, reduced-supervision evidence, expiry/kill-switch state, and explicit promotion blockers. The rail remains shadow-only; no consumer writes, dispatch, merge, or promotion authority is exposed. `consumer_sync_artifact_ingest.py preview` validates the latest successful producer artifact and @@ -317,7 +317,7 @@ safety switch, not dead code. `ingest` for at most one artifact and five registered consumers, records only local evidence, and runs a self-expiring human-on-exception phase through 2026-07-25. It has no consumer or GitHub write path; exceptions fail the cadence and remain visible in the local report/state. - For a concise check-in, run `python3 periodic_report.py --json --window-days 7` and inspect the + For a concise check-in, run `python3 src/periodic_report.py --json --window-days 7` and inspect the `model_profile_trial`, `model_profile_transport_qualification`, `role_activation`, `pattern_miner`, and `dataset` sections alongside the generated status/inventory artifacts. @@ -423,7 +423,7 @@ with its causes and its drainable count, and does not fail the suite. Rationale ## Capability activation inventory -`python3 capabilities.py usage` answers the question the inventory cannot: **why** a capability is +`python3 src/capabilities.py usage` answers the question the inventory cannot: **why** a capability is not being used, and what would change that. It reports invocations/week, `evidence_debt` (how many further independent durable reuses the promotion policy still wants), and one next action per capability, rolled up into READY TO LIFT / PROMOTABLE / MEASUREMENT GAPS / WORTH FEEDING / RETIRE @@ -440,7 +440,7 @@ and unrecognised criteria all block readiness, so silence cannot read as a pass. readiness; lifting a gate stays a deliberate act (see the safety-switch policy in CLAUDE.md). Do not maintain a second static list of supposedly active or gated features here. Generate the -current inventory with `python3 capabilities.py inventory` (or inspect +current inventory with `python3 src/capabilities.py inventory` (or inspect `~/.codex/orchestrator/capability-inventory.md` after an active tick). It distinguishes deliberate gates, canaries, no matching work, matched-but-not-invoked seams, missing outcomes, and stale active capabilities from ordinary code maturity. diff --git a/docs/MIRROR_SYNC_PATCH.md b/docs/MIRROR_SYNC_PATCH.md new file mode 100644 index 0000000..a8eecae --- /dev/null +++ b/docs/MIRROR_SYNC_PATCH.md @@ -0,0 +1,79 @@ +# `orch-sync-mirror.sh` must be patched for the `src/` layout — BEFORE the next sync + +**This is the one external dependency of the `src/` move, and it is the owner's file.** +`~/.codex/bin/orch-sync-mirror.sh` lives outside the repository, so this change cannot land it. It +must be applied by hand, and the deliberate manual mirror sync (`CLAUDE.md` §1: *"the only circuit +breaker between an agent's change and the dispatcher"*) is the natural gate for doing so. + +## What breaks without it + +Line 25 copies the modules **flat**: + +```bash +cp "$SRC"/*.py "$SRC"/orchestrate.sh "$MIRROR"/ +``` + +After the move there are no `.py` files at `$SRC` — they are all in `$SRC/src/`. The glob matches +nothing, `cp` fails, and launchd's hourly `orchestrate.sh --active` runs against a mirror with no +modules. The script's closing line would report `synced 0 .py`, so the failure is visible rather +than silent — but only to someone reading the output. + +## The patch + +Replace line 24–25: + +```bash +find "$MIRROR" -maxdepth 1 \( -name '*.py' -o -name '*.sh' \) -delete 2>/dev/null || true +cp "$SRC"/*.py "$SRC"/orchestrate.sh "$MIRROR"/ +``` + +with: + +```bash +# Modules moved to src/ (2026-08-23). The mirror stays FLAT on purpose: everything that resolves +# paths here — paths.py in Python, $ORCH in orchestrate.sh — detects the layout rather than +# assuming it, so a flat mirror and a src/ checkout are both correct. Keeping the mirror flat also +# means the delete-then-copy below needs no new directory handling. +find "$MIRROR" -maxdepth 1 \( -name '*.py' -o -name '*.sh' \) -delete 2>/dev/null || true +MODSRC="$SRC/src" +[[ -d "$MODSRC" ]] || MODSRC="$SRC" # tolerate a pre-move checkout +cp "$MODSRC"/*.py "$SRC"/orchestrate.sh "$MIRROR"/ +``` + +and update the closing summary line to count from `$MODSRC` if you want the number to stay +meaningful. + +## Why the mirror stays flat rather than gaining a `src/` + +Both shapes work, because every path resolver detects the layout instead of assuming it: + +| resolver | rule | +|---|---| +| `src/paths.py` | module dir named `src` ⇒ checkout is its parent; else the two coincide | +| `orchestrate.sh` | `ORCH="$ORCH_REPO/src"`, falling back to `$ORCH_REPO` when that directory is absent | + +A flat mirror therefore needs no further change, and it keeps the existing delete-then-copy +one-liner. That symmetry is deliberate: the alternative — hardcoding `parent.parent` somewhere — +is the failure `capability_activation_audit._fleet_roots` already documents, where byte-identical +code scored 37 of 37 in the canonical tree and 36 of 37 in the mirror. + +## Files the sync already copies by root path, and which are unaffected + +`orchestrate.sh`, `.verify-floor.json`, `CLAUDE.md`, `IMPROVEMENT_BACKLOG.md`, +`experiments/*.json`, `data/feedback-snapshot.json`, and the Workflows registry all stay at the +checkout root, so their lines need no edit. + +**One line does need attention:** the sync copies `.coveragerc`, which this change **deleted** — +the coverage settings moved into `pyproject.toml` because CI passes `--cov-config=pyproject.toml` +whenever that file exists, which would have made a surviving `.coveragerc` invisible. Change that +copy to `pyproject.toml`, or `test_verify_coverage_mode.py` will skip in the mirror for a missing +prerequisite rather than assert. + +## How to confirm it worked + +```bash +bash ~/.codex/bin/orch-sync-mirror.sh && ls ~/.codex/orchestrator-mirror/*.py | wc -l +``` + +Expect ~99, and then `cd ~/.codex/orchestrator-mirror && python3 verify.py` should be green — note +that in the FLAT mirror the command keeps its old form, with no `src/` prefix. diff --git a/BRIEF_expand_range.md b/docs/briefs/BRIEF_expand_range.md similarity index 100% rename from BRIEF_expand_range.md rename to docs/briefs/BRIEF_expand_range.md diff --git a/BRIEF_keepalive_transfers.md b/docs/briefs/BRIEF_keepalive_transfers.md similarity index 100% rename from BRIEF_keepalive_transfers.md rename to docs/briefs/BRIEF_keepalive_transfers.md diff --git a/BRIEF_process_improvement.md b/docs/briefs/BRIEF_process_improvement.md similarity index 100% rename from BRIEF_process_improvement.md rename to docs/briefs/BRIEF_process_improvement.md diff --git a/CODEX_BRIEF.md b/docs/briefs/CODEX_BRIEF.md similarity index 99% rename from CODEX_BRIEF.md rename to docs/briefs/CODEX_BRIEF.md index 97302ba..06bbe74 100644 --- a/CODEX_BRIEF.md +++ b/docs/briefs/CODEX_BRIEF.md @@ -106,7 +106,7 @@ the deliverable is the comparison + the synthesized best. **Strategy experiments (H4/H5):** use `strategy_experiment.py` when the arm is a strategy rather than a single agent (for example, `single(claude)` vs `parallel(claude+cursor+synth)`). It is read-only by -default: `python3 strategy_experiment.py --hypothesis H4 --repo owner/repo --spec-file spec.md --exp-id +default: `python3 src/strategy_experiment.py --hypothesis H4 --repo owner/repo --spec-file spec.md --exp-id id --json` normalizes arms, expands the unique implementation agents for `exp_abcd`, and points to the `strategy.json` metadata path. Active prepare is guarded by both `--prepare --confirm-strategy` and `ORCH_STRATEGY_EXPERIMENT=1`. The cron research tick still auto-launches only simple single-agent A/B/C/D diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 6b0f3c9..0000000 --- a/mypy.ini +++ /dev/null @@ -1,24 +0,0 @@ -; Mypy configuration. The type check is currently OFF in the Gate — see pr-00-gate.yml's -; "Compute Python CI toggles" step for the blocking and drainable counts. This file exists anyway, -; and for one specific reason: WITHOUT IT `mypy .` CANNOT RUN HERE AT ALL, so the recorded count -; would be unverifiable prose rather than a number anyone can regenerate. -; -; What it fixes: `scripts/langchain/` has no `__init__.py`, so mypy resolves -; scripts/langchain/_llm_client.py as both `_llm_client` and `scripts.langchain._llm_client` and -; aborts with "Source file found twice under different module names ... (errors prevented further -; checking)". That single setup error is what a reader of the failed `typecheck-mypy` job saw — -; it masked the real number completely. `explicit_package_bases` is the resolution mypy's own error -; message recommends, and it is why docs/CI_LINT_BASELINE.md can state 601 rather than "unknown". -; -; NOT a suppression file, deliberately. Nothing here disables an error code or narrows the check: -; a mypy.ini that silenced 588 findings would make the Gate green while checking nothing, which is -; the failure this repo's verify.py exists to stop. The findings stay visible and counted, and the -; check stays honestly off until they are drained. -; -; It must be mypy.ini and NOT pyproject.toml or setup.cfg. `reusable-10-ci-python.yml` adds -; `-e '.[app,dev]'` to its install whenever pyproject.toml / setup.cfg / setup.py exists, and this -; repo is 129 flat root modules with no build backend, so that install fails. mypy discovers -; mypy.ini on its own (no `--config-file` is passed when there is no pyproject.toml), and no other -; tool in the pipeline reads this filename. Re-measure with `python3 scripts/ci_lint_baseline.py`. -[mypy] -explicit_package_bases = True diff --git a/orchestrate-seat.sh b/orchestrate-seat.sh index e931f12..b58c9af 100644 --- a/orchestrate-seat.sh +++ b/orchestrate-seat.sh @@ -8,7 +8,9 @@ # orchestrate-seat.sh --agent codex ["instruction"] # rotate the seat # ORCH_SEAT_DRYRUN=1 orchestrate-seat.sh # print the assembled prompt + exit (no seat) set -euo pipefail -ORCH="${ORCH_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" # self-locating (code on Dropbox; runtime LOCAL) +ORCH="${ORCH_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +# This script reads a repo-root DOC, not a module, so it keeps the checkout root. The distinction +# matters since the modules moved under src/: `$ORCH/ORCHESTRATOR.md` would resolve to nothing. # self-locating (code on Dropbox; runtime LOCAL) export PATH="/opt/homebrew/bin:$HOME/.local/bin:$HOME/.cursor/bin:$PATH" SEAT="claude" diff --git a/orchestrate.sh b/orchestrate.sh index 7f973cb..d3b26c6 100644 --- a/orchestrate.sh +++ b/orchestrate.sh @@ -14,7 +14,13 @@ set -euo pipefail # Self-locating: code lives in Code/Orchestrator (Dropbox); git checkouts + feedback DB stay LOCAL # (defaults baked into provision.py/feedback.py). Override with ORCH_DIR. -ORCH="${ORCH_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +ORCH_REPO="${ORCH_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +# ORCH points at the MODULES, which are not the checkout root any more: a checkout keeps them under +# src/, while the exec mirror is FLAT (orch-sync-mirror.sh copies root-level .py only). Detected, +# never assumed — the same rule paths.py applies in Python, for the same reason: a hardcoded path +# would be right in one tree and wrong in the other, and the mirror is the one launchd runs. +ORCH="$ORCH_REPO/src" +[[ -d "$ORCH" ]] || ORCH="$ORCH_REPO" # Tools the tick shells out to live outside the default cron/sandbox PATH: ccusage/npx/node # in homebrew, vibe/cursor-agent in ~/.local|.cursor/bin. Without this, capacity.py can't see # ccusage → codex/claude read 'unknown' and never get routed. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8a76bca --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,185 @@ +# pyproject.toml — tool configuration for a NON-PACKAGE repo, and both halves of that matter. +# +# WHY IT CAN EXIST NOW, AND COULD NOT BEFORE. `reusable-10-ci-python.yml` used to append +# `-e '.[app,dev]'` to its install whenever a pyproject.toml existed, on eight separate code paths, +# gated on nothing but the file's presence. This repo has no build backend, so merely creating this +# file broke all five Python jobs at the install step — which is why the Ruff and mypy settings +# originally landed as `ruff.toml` + `mypy.ini` instead. Upstream now guards that with +# `pyproject_declares_distribution()`: the editable install fires only when a project declares +# `[project]`, `[build-system]` or `[tool.poetry]`. +# +# SO THIS FILE DELIBERATELY DECLARES NONE OF THOSE. It is tool configuration, not a distribution. +# Adding `[project]` here would demand a build backend and an explicit list of ~99 top-level +# modules — a hand-maintained list that goes stale on every rename, which is the drift `ruff.toml` +# already refuses for `known-first-party`. Packaging is a separate decision; this is not it. +# +# WHY THE COVERAGE AND MYPY SETTINGS HAD TO MOVE IN HERE. They were not optional migrations. CI +# passes `--cov-config=pyproject.toml` and `--config-file pyproject.toml` WHENEVER this file +# exists, so a `.coveragerc` or `mypy.ini` sitting beside it would be silently ignored: coverage +# would lose `parallel = true` and report whichever subprocess finished last, and mypy would lose +# `explicit_package_bases` and abort on a setup error. Two configs where the tool reads one is +# drift with a delay on it, so those files are deleted rather than kept as decoration. + +[tool.pytest.ini_options] +# THE ONE LINE THE `src/` LAYOUT NEEDS. Tests live in `tests/` and the modules in `src/`, so the +# import path has to say so; without it every `import capabilities` in the suite fails at +# collection. Modules invoked directly (`python3 src/tick.py`) do not need it — Python puts the +# script's own directory on `sys.path` — which is why the move required no import rewrites at all. +pythonpath = ["src"] +# No `testpaths`. `verify.py` enforces a collection floor as an EQUALITY, so anything here that +# narrows discovery would read as tests silently ceasing to run. Discovery stays as it was. +minversion = "7.0" +markers = [ + "quarantine: excluded from the PR gate by the fleet's marker expression", + "slow: excluded from the PR gate by the fleet's marker expression", +] + +# --------------------------------------------------------------------------------- coverage +# Migrated verbatim in intent from `.coveragerc` (deleted in the same commit — see the header). +# +# WHY parallel mode is the whole point. This project's primary test mechanism is a per-module +# `--selftest`, and verify.py runs each one as a SUBPROCESS. A coverage run that instruments only +# the pytest process therefore cannot see any of it — and that is most of the codebase: 78 modules' +# sole test is a selftest, ~85,500 lines, 79.6% of non-test Python (measured 2026-08-23). The +# reported 48.45% was that blind spot, not missing tests: sampling four of the modules the report +# named as worst showed outcomes.py at 62%, adversarial.py 88%, gh_capacity.py 86%, +# keepalive_outcomes.py 78% under their own selftests, all exiting 0. +# +# `parallel = true` makes every instrumented process write its own `.coverage...` +# file; `coverage combine` merges them. `verify.py --coverage` does both. +[tool.coverage.run] +parallel = true +# `src`, not `.`: the modules moved, and pointing at the checkout root would measure the tests and +# the tooling instead of the code under test. +source = ["src"] +branch = false +omit = [ + ".venv/*", + "venv/*", + "*/site-packages/*", + "tests/*", + "conftest.py", + "src/verify.py", +] + +[tool.coverage.report] +# A missing data file means the run did not happen, which must not read as 0% or as success. +skip_empty = false +precision = 1 +exclude_lines = [ + "pragma: no cover", + "if __name__ == .__main__.:", + "raise NotImplementedError", +] + +# --------------------------------------------------------------------------------- mypy +# Migrated from `mypy.ini` (deleted in the same commit). The type check is currently OFF in the +# Gate — see pr-00-gate.yml's "Compute Python CI toggles" step for the blocking and drainable +# counts. This config exists anyway, and for one specific reason: WITHOUT IT `mypy` CANNOT RUN HERE +# AT ALL, so the recorded count would be unverifiable prose rather than a number anyone can +# regenerate with `python3 scripts/ci_lint_baseline.py`. +# +# What it fixes: `scripts/langchain/` has no `__init__.py`, so mypy resolves +# `scripts/langchain/_llm_client.py` as both `_llm_client` and `scripts.langchain._llm_client` and +# aborts with "Source file found twice under different module names (errors prevented further +# checking)". That single setup error is what a reader of the failed `typecheck-mypy` job saw — it +# masked the real number completely. `explicit_package_bases` is the resolution mypy's own error +# message recommends. +# +# NOT a suppression file, deliberately. Nothing here disables an error code or narrows the check: a +# config that silenced the findings would make the Gate green while checking nothing, which is the +# failure `verify.py` exists to stop. The findings stay visible and counted, and the check stays +# honestly off until they are drained. +[tool.mypy] +explicit_package_bases = true +# `mypy_path` is what lets mypy resolve this repo's FLAT sibling imports (`import capabilities`) +# now that the modules live under `src/`. Without it every cross-module import reports +# `import-not-found` and the recorded baseline inflates with errors that are an artefact of the +# config rather than the code — 769 instead of 601 when first measured, all of the difference +# spurious. The same reason `pythonpath = ["src"]` exists for pytest above: one relocation, two +# tools that each need telling. +mypy_path = ["src"] + +# THE RATCHET, and the reason the check can be ON at all. 35 of the 99 modules are already clean; +# the 64 below are not, and each is exempt BY NAME so the check can run TODAY over the +# clean two-thirds instead of being off over everything. A blanket `ignore_errors` or a +# `disable_error_code` list was rejected: both make the job green while checking nothing, which is +# the defect verify.py exists to stop, and neither has anything to count. +# +# The mandatory latched-gate questions, answered in writing because this IS a gate: +# 1. What decrements it? Typing a module and deleting its line here. Not "time passes". +# 2. Can that run while the gate is CLOSED? Yes — the check being green never blocks typing work, +# and shortening a plain list needs no permission from the gate it drains. +# 3. Does the measuring window equal the draining window? Yes: ONE list, counted by verify.py and +# drained by editing these same lines. A second copy of this population would drift. +# It fails toward motion, not silence: verify.py prints the remaining count every run, and +# `.verify-floor.json`'s `mypy_exempt_max` FAILS if the list grows — so new untyped code in a clean +# module is now a red, and a module can never quietly rejoin the exempt set. +[[tool.mypy.overrides]] +module = [ + "adversarial", + "agent_auth_check", + "backlog", + "cadence_registry", + "capabilities", + "capability_activation_audit", + "capability_admission", + "capability_advisor", + "capability_compiler", + "capability_effectiveness", + "capability_opportunity", + "capability_outcome_bridge", + "capability_propensity", + "capability_recurrence_check", + "capability_targets", + "ccusage_reconcile", + "claims", + "codemod_lane", + "consumer_sync_artifact_ingest", + "consumer_sync_shadow", + "cross_repo_lane", + "dispatcher", + "durability_sweep", + "epic_lane", + "evidence_acquisition", + "execution_profiles", + "exp_abcd", + "exploration_backfill", + "exploration_collection", + "exploration_evidence_plan", + "exploration_review", + "features", + "feedback", + "gh_capacity", + "human_calibration", + "issue_quality", + "issue_readiness", + "judge_reliability", + "keepalive_evidence", + "keepalive_outcomes", + "keepalive_shadow", + "ledger_reconcile", + "mcp_server", + "observability_dashboard", + "partitioned_review", + "pattern_miner", + "range_lane_rollout", + "redirect_apply", + "redirect_shadow", + "redirect_sweep", + "repo_knowledge", + "research_scheduler", + "research_subjects", + "roles", + "router", + "runtime_ac", + "runtime_ac_gate", + "strategy_experiment", + "switch_review", + "synthesis_promotion", + "tick", + "ux_review", + "verify", + "watch", +] +ignore_errors = true diff --git a/ruff.toml b/ruff.toml index cd02cc1..7e7904c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -46,7 +46,7 @@ extend-exclude = [".workflows-lib"] # would mean a list that silently goes stale every time a module is added or renamed — the drift # this file exists to prevent. Both surfaces read it, since Autofix's `--select I` on the command # line overrides `select` but never the isort settings. -src = ["."] +src = ["src"] [lint] # E4/E7/E9/F is exactly the set the Gate was already applying, now owned here. `I` is added because diff --git a/scripts/ci_lint_baseline.py b/scripts/ci_lint_baseline.py index 6d2f99b..98f716b 100644 --- a/scripts/ci_lint_baseline.py +++ b/scripts/ci_lint_baseline.py @@ -192,8 +192,17 @@ def measure_black() -> dict: def measure_mypy() -> dict: - """`mypy --exclude .workflows-lib .` -- verbatim from the Gate's typecheck job.""" - cmd = ["mypy", "--exclude", GATE_EXCLUDE_RUFF, "."] + """`mypy --config-file pyproject.toml --exclude .workflows-lib src` -- the Gate's own command. + + THE TARGET IS `src`, NOT `.`, and copying the Gate exactly is the whole point of this script. + `reusable-10-ci-python.yml` runs `target="src"; [ -d "$target" ] || target="."`, so once the + modules moved the Gate began checking `src` while this script still measured the whole tree — + 613 against the Gate's 467, with 30 of the difference `import-not-found` artefacts from + resolving the tests and tooling without `mypy_path`. A baseline that does not match the command + it claims to record is worse than no baseline: it reads as measured. + """ + target = "src" if (REPO / "src").is_dir() else "." + cmd = ["mypy", "--config-file", "pyproject.toml", "--exclude", GATE_EXCLUDE_RUFF, target] out = _run(cmd).stdout codes: dict[str, int] = {} for match in re.finditer(r"\[([a-z][a-z-]+)\]\s*$", out, re.M): diff --git a/adapters.py b/src/adapters.py similarity index 100% rename from adapters.py rename to src/adapters.py diff --git a/adversarial.py b/src/adversarial.py similarity index 100% rename from adversarial.py rename to src/adversarial.py diff --git a/agent_auth_check.py b/src/agent_auth_check.py similarity index 100% rename from agent_auth_check.py rename to src/agent_auth_check.py diff --git a/backlog.py b/src/backlog.py similarity index 99% rename from backlog.py rename to src/backlog.py index d83ad29..7551436 100644 --- a/backlog.py +++ b/src/backlog.py @@ -232,7 +232,7 @@ def raise_expired_blocker_questions(now: int | None = None) -> list[dict]: change failure this whole area keeps producing. Only `await_human` blockers are raised; a machine-reason block lapsing needs no decision from the owner. """ - raised = [] + raised: list[Any] = [] try: import feedback except Exception: diff --git a/cadence_registry.py b/src/cadence_registry.py similarity index 100% rename from cadence_registry.py rename to src/cadence_registry.py diff --git a/capabilities.py b/src/capabilities.py similarity index 100% rename from capabilities.py rename to src/capabilities.py diff --git a/capability_activation_audit.py b/src/capability_activation_audit.py similarity index 98% rename from capability_activation_audit.py rename to src/capability_activation_audit.py index ef2dd05..526491e 100644 --- a/capability_activation_audit.py +++ b/src/capability_activation_audit.py @@ -51,6 +51,7 @@ import backlog import capabilities +import paths HERE = Path(__file__).resolve().parent STATE_DIR = Path(os.environ.get("ORCH_STATE_DIR", Path.home() / ".codex" / "orchestrator")) @@ -297,11 +298,19 @@ def _entrypoint_files(cap: dict) -> list[Path]: ENTRYPOINT_UNDECLARED = "undeclared" +def _driver_path(driver: str) -> Path: + """Where a DRIVER_MODULES entry actually lives: shell drivers at the checkout root, modules + beside their siblings. One accident kept these the same directory until `src/` separated them. + """ + return (paths.checkout_root(HERE) if driver.endswith(".sh") else HERE) / driver + + def _repo_root() -> Path: """The checkout that `.claude/worktrees/*` hangs off, whether we are IN it or in a worktree.""" - if HERE.parent.name == "worktrees" and HERE.parent.parent.name == ".claude": - return HERE.parent.parent.parent - return HERE + base = paths.checkout_root(HERE) + if base.parent.name == "worktrees" and base.parent.parent.name == ".claude": + return base.parent.parent.parent + return base def sibling_checkouts() -> list[tuple[Path, str]]: @@ -633,7 +642,10 @@ def _callers_of(module_stem: str, func_names: set[str]) -> list[str]: """Driver modules that call `module_stem.` for any func, or run it as a CLI.""" found = [] for driver in DRIVER_MODULES: - dpath = HERE / driver + # Shell drivers sit at the CHECKOUT root, python drivers beside the modules — the whole + # reason `_driver_path` exists. `HERE / driver` silently found NOTHING for orchestrate.sh + # after the move, which reported two live capabilities as having no caller. + dpath = _driver_path(driver) if not dpath.exists(): continue text = dpath.read_text(errors="ignore") @@ -731,7 +743,9 @@ def _fleet_roots() -> list[tuple[pathlib.Path, str]]: env = os.environ.get("ORCH_FLEET_ROOT") if env: roots.append((pathlib.Path(env).expanduser(), "ORCH_FLEET_ROOT")) - roots.append((HERE.parent, "sibling-of-module")) + # From HERE, not the module constant: the selftest patches HERE to build synthetic trees, + # and a constant would ignore the tree the caller believes it is inspecting. + roots.append((paths.fleet_root(HERE), "sibling-of-checkout")) roots.append( ( pathlib.Path.home() / "Library/CloudStorage/Dropbox/Learning/Code", @@ -951,7 +965,9 @@ def heartbeat_env_gate(*, here: Path | None = None) -> dict: `suppressed_modules` is the defect set. `invocations_after` is published alongside it so a zero can be read: 0 suppressed of 40 invocations is a correct ordering, 0 of 0 is a broken parse. """ - root = here or HERE + # Shell drivers live at the CHECKOUT root, not beside the modules. `here` is still honoured so + # the selftest can point this at a synthetic tree. + root = here or paths.checkout_root(HERE) out = { "flag": HEARTBEAT_ENV_FLAG, "anchor": HEARTBEAT_EXPORT_ANCHOR, diff --git a/capability_admission.py b/src/capability_admission.py similarity index 98% rename from capability_admission.py rename to src/capability_admission.py index 5d633b1..6cbf347 100644 --- a/capability_admission.py +++ b/src/capability_admission.py @@ -49,6 +49,7 @@ import capabilities import env_prereq +import paths HERE = pathlib.Path(__file__).resolve().parent @@ -70,7 +71,7 @@ def _audits_dir() -> pathlib.Path: if os.environ.get("ORCH_FLEET_ROOT") else None ), - HERE.parent, + paths.FLEET_ROOT, pathlib.Path.home() / "Library/CloudStorage/Dropbox/Learning/Code", ] for root in candidates: @@ -403,10 +404,11 @@ def known_controls() -> set[str]: delegating a control and asserting one. """ controls: set[str] = set() - here = pathlib.Path(__file__).resolve().parent for name in ("orchestrate.sh", "dispatcher.py", "repo_knowledge.py", "router.py"): + # orchestrate.sh is at the checkout root; the modules sit beside this one. + base = paths.REPO_ROOT if name.endswith(".sh") else paths.MODULE_DIR try: - text = (here / name).read_text() + text = (base / name).read_text() except OSError: continue controls |= set(re.findall(r"ORCH_[A-Z0-9_]+", text)) @@ -439,9 +441,20 @@ def _findability_context(capability_ids, *, path: pathlib.Path | None = None) -> def _context(path: pathlib.Path | None = None) -> dict: - import capability_activation_audit as audit + # The recurrence-fixture roster lives with the tests, and this gate genuinely needs it. Since + # the suite moved to `tests/` that directory is not importable by default, so it is added + # EXPLICITLY rather than left to a sys.path accident — an accident is how this dependency would + # rot into a silent skip. + import sys + + import paths + + if str(paths.TESTS_DIR) not in sys.path: + sys.path.insert(0, str(paths.TESTS_DIR)) import test_capability_set_coverage as coverage + import capability_activation_audit as audit + rows = {r["capability_id"]: r for r in audit.audit(use_cache=True)["rows"]} ledger = capabilities.load(path or capabilities.REG) return { @@ -951,7 +964,7 @@ def _selftest() -> None: gaps: list[str] = [] # Every requirement must be able to FAIL. A predicate that always passes is decoration. - ctx = {"audit_rows": {}, "fixtures": set()} + ctx: dict[str, Any] = {"audit_rows": {}, "fixtures": set()} empty = capabilities._blank_capability("capability:nothing-declared") empty["event_history"] = [{"timestamp": _now(), "type": "migrated"}] # not grandfathered for name, fn in REQUIREMENTS: diff --git a/capability_advisor.py b/src/capability_advisor.py similarity index 99% rename from capability_advisor.py rename to src/capability_advisor.py index 2fd0235..bad6adb 100644 --- a/capability_advisor.py +++ b/src/capability_advisor.py @@ -1908,7 +1908,7 @@ def evaluate_precondition( declared = applies_to(capability_id) needs = required_repo_fact(capability_id) target = consult_target(repository) - out = { + out: list[Any] = { "applies_to": declared, "scope_target": target, "scope_match": None, diff --git a/capability_compiler.py b/src/capability_compiler.py similarity index 99% rename from capability_compiler.py rename to src/capability_compiler.py index a89262c..8813ec7 100644 --- a/capability_compiler.py +++ b/src/capability_compiler.py @@ -393,7 +393,7 @@ def compile_workflow_rail(source: dict[str, Any]) -> dict[str, Any]: rollback_spec = ENTRYPOINTS.get(rollback_entrypoint) if rollback_entrypoint not in spec["rollbacks"] or rollback_spec is None: errors.append(f"invalid rollback: {step_id}") - typed_rollback_inputs = {} + typed_rollback_inputs: dict[str, Any] = {} else: typed_rollback_inputs, rollback_errors = _typed_values( rollback.get("inputs"), rollback_spec["inputs"], f"{path}.rollback.inputs" diff --git a/capability_effectiveness.py b/src/capability_effectiveness.py similarity index 100% rename from capability_effectiveness.py rename to src/capability_effectiveness.py diff --git a/capability_firing_monitor.py b/src/capability_firing_monitor.py similarity index 100% rename from capability_firing_monitor.py rename to src/capability_firing_monitor.py diff --git a/capability_ir.py b/src/capability_ir.py similarity index 100% rename from capability_ir.py rename to src/capability_ir.py diff --git a/capability_lifecycle.py b/src/capability_lifecycle.py similarity index 100% rename from capability_lifecycle.py rename to src/capability_lifecycle.py diff --git a/capability_matcher_proposals.py b/src/capability_matcher_proposals.py similarity index 100% rename from capability_matcher_proposals.py rename to src/capability_matcher_proposals.py diff --git a/capability_opportunity.py b/src/capability_opportunity.py similarity index 100% rename from capability_opportunity.py rename to src/capability_opportunity.py diff --git a/capability_outcome_bridge.py b/src/capability_outcome_bridge.py similarity index 100% rename from capability_outcome_bridge.py rename to src/capability_outcome_bridge.py diff --git a/capability_propensity.py b/src/capability_propensity.py similarity index 99% rename from capability_propensity.py rename to src/capability_propensity.py index 4338441..eb607e2 100644 --- a/capability_propensity.py +++ b/src/capability_propensity.py @@ -179,6 +179,7 @@ import time import capabilities +import paths # KILL SWITCH. Off means the advisor stops ranking by propensity and falls back to its previous # order; the recording edges still work, so turning this off never destroys evidence -- it only @@ -1939,7 +1940,7 @@ def _selftest_tick_evidence() -> None: # catches it for THIS subcommand specifically, and catches (a), which nothing else does. # Resolved relative to THIS module's own directory on purpose: the check must verify the driver # in the same tree as the code, which is right in both the repo and the exec mirror. - driver = pathlib.Path(__file__).resolve().parent / "orchestrate.sh" + driver = paths.orchestrate_sh() if driver.exists(): text = driver.read_text(errors="ignore") call = text.find('capability_propensity.py" tick-evidence') diff --git a/capability_recurrence_check.py b/src/capability_recurrence_check.py similarity index 99% rename from capability_recurrence_check.py rename to src/capability_recurrence_check.py index 64d1529..571043d 100644 --- a/capability_recurrence_check.py +++ b/src/capability_recurrence_check.py @@ -41,6 +41,7 @@ import backlog import capabilities import env_prereq +import paths # --------------------------------------------------------------------------- fixtures # Each entry: the capability under test, the real instance, and how to decide if it would fire. @@ -176,7 +177,8 @@ def _predicate_heartbeat(capability_id: str) -> dict: return {"fires": False, "detail": {"error": str(exc)[:90]}} -ORCHESTRATE = pathlib.Path(__file__).resolve().parent / "orchestrate.sh" +# The tick driver sits at the CHECKOUT root, which is only the module dir on a flat tree. +ORCHESTRATE = paths.orchestrate_sh() _TICK_ENV: dict[str, str] | None = None _TICK_ENV_DIAG: dict | None = None @@ -1240,12 +1242,8 @@ def _selftest() -> None: _line_citation = re.compile(r"\b[A-Za-z0-9_]+\.(?:py|sh|json|md):\d+") _anchor = re.compile(r"ORCH-ANCHOR: ([a-z0-9-]+)") _tree_text = "\n".join( - p.read_text(errors="ignore") - for p in sorted(pathlib.Path(__file__).resolve().parent.glob("*.py")) - ) + "\n".join( - p.read_text(errors="ignore") - for p in sorted(pathlib.Path(__file__).resolve().parent.glob("*.sh")) - ) + p.read_text(errors="ignore") for p in sorted(paths.MODULE_DIR.glob("*.py")) + ) + "\n".join(p.read_text(errors="ignore") for p in sorted(paths.REPO_ROOT.glob("*.sh"))) _anchors_cited = 0 for flag, criterion in SWITCH_ON_CRITERIA.items(): bad = _line_citation.findall(criterion) diff --git a/capability_targets.py b/src/capability_targets.py similarity index 99% rename from capability_targets.py rename to src/capability_targets.py index ad76527..6dbce58 100644 --- a/capability_targets.py +++ b/src/capability_targets.py @@ -168,7 +168,7 @@ def register_target( elif kind == "skill": context["skill_dir"] = str(Path(artifact).resolve()) - record = { + record: dict[str, Any] = { "schema": "orchestrator.capability-target-binding", "version": VERSION, "kind": kind, diff --git a/capacity.py b/src/capacity.py similarity index 100% rename from capacity.py rename to src/capacity.py diff --git a/ccusage_reconcile.py b/src/ccusage_reconcile.py similarity index 100% rename from ccusage_reconcile.py rename to src/ccusage_reconcile.py diff --git a/claims.py b/src/claims.py similarity index 100% rename from claims.py rename to src/claims.py diff --git a/codemod_lane.py b/src/codemod_lane.py similarity index 100% rename from codemod_lane.py rename to src/codemod_lane.py diff --git a/completion_event_adapter.py b/src/completion_event_adapter.py similarity index 100% rename from completion_event_adapter.py rename to src/completion_event_adapter.py diff --git a/consumer_sync_artifact_ingest.py b/src/consumer_sync_artifact_ingest.py similarity index 100% rename from consumer_sync_artifact_ingest.py rename to src/consumer_sync_artifact_ingest.py diff --git a/consumer_sync_shadow.py b/src/consumer_sync_shadow.py similarity index 100% rename from consumer_sync_shadow.py rename to src/consumer_sync_shadow.py diff --git a/cross_repo_lane.py b/src/cross_repo_lane.py similarity index 100% rename from cross_repo_lane.py rename to src/cross_repo_lane.py diff --git a/dispatcher.py b/src/dispatcher.py similarity index 100% rename from dispatcher.py rename to src/dispatcher.py diff --git a/dry_seam_audit.py b/src/dry_seam_audit.py similarity index 99% rename from dry_seam_audit.py rename to src/dry_seam_audit.py index fc92c30..31c8de1 100644 --- a/dry_seam_audit.py +++ b/src/dry_seam_audit.py @@ -453,7 +453,7 @@ def audit_dry_seams( recommendation="Schedule exploration/production runs for high-value zero-observation cells before trusting learned order.", ) - capability_lifecycle = { + capability_lifecycle: dict[str, Any] = { "path": str(capabilities_path) if capabilities_path else None, "total": 0, "counts_by_status": {}, diff --git a/durability_sweep.py b/src/durability_sweep.py similarity index 100% rename from durability_sweep.py rename to src/durability_sweep.py diff --git a/env_prereq.py b/src/env_prereq.py similarity index 97% rename from env_prereq.py rename to src/env_prereq.py index a561163..87b5081 100644 --- a/env_prereq.py +++ b/src/env_prereq.py @@ -214,7 +214,12 @@ def repo_files_absent(*relative_paths: str) -> str | None: Detects the FILE, never the context: no `$CI`, no "am I in the mirror" heuristic. """ - here = Path(__file__).resolve().parent + # The CHECKOUT root, not this module's directory: `.github/`, `docs/` and `scripts/` live at + # the repo root while the modules live under `src/`. Resolved through the shared rule, so the + # mirror — which is flat, and where the two coincide — still detects their absence correctly. + import paths + + here = paths.checkout_root(Path(__file__).resolve().parent) missing = [rel for rel in relative_paths if not (here / rel).exists()] if not missing: return None @@ -249,7 +254,9 @@ def git_repo_absent() -> str | None: "git is not on PATH — this check asks git whether a path is ignored or tracked, since " "reimplementing gitignore precedence in a test would only agree with itself" ) - here = Path(__file__).resolve().parent + import paths + + here = paths.checkout_root(Path(__file__).resolve().parent) try: proc = subprocess.run( ["git", "rev-parse", "--git-dir"], @@ -486,7 +493,10 @@ def _selftest() -> None: # green, which is worse than the red it replaced. absent = repo_files_absent("definitely-not-a-file-here") assert absent and "definitely-not-a-file-here" in absent, absent - assert repo_files_absent("env_prereq.py") is None, "a file that IS here must not skip" + assert repo_files_absent("pyproject.toml") is None, "a file that IS here must not skip" + # A MODULE is not a repo-root path any more: this detector answers "is this + # checkout complete", and the modules live under src/. + assert repo_files_absent("env_prereq.py") is not None, "a module is not a root path" assert repo_files_absent("env_prereq.py", "definitely-not-a-file-here"), "one missing is enough" # A skipped selftest must SPEAK, and its line must carry the shared mark verify.py greps diff --git a/epic_lane.py b/src/epic_lane.py similarity index 100% rename from epic_lane.py rename to src/epic_lane.py diff --git a/evidence_acquisition.py b/src/evidence_acquisition.py similarity index 100% rename from evidence_acquisition.py rename to src/evidence_acquisition.py diff --git a/evidence_schema.py b/src/evidence_schema.py similarity index 100% rename from evidence_schema.py rename to src/evidence_schema.py diff --git a/execution_profiles.py b/src/execution_profiles.py similarity index 100% rename from execution_profiles.py rename to src/execution_profiles.py diff --git a/exp_abcd.py b/src/exp_abcd.py similarity index 100% rename from exp_abcd.py rename to src/exp_abcd.py diff --git a/experiment_recovery.py b/src/experiment_recovery.py similarity index 100% rename from experiment_recovery.py rename to src/experiment_recovery.py diff --git a/exploration_backfill.py b/src/exploration_backfill.py similarity index 100% rename from exploration_backfill.py rename to src/exploration_backfill.py diff --git a/exploration_collection.py b/src/exploration_collection.py similarity index 100% rename from exploration_collection.py rename to src/exploration_collection.py diff --git a/exploration_evidence_plan.py b/src/exploration_evidence_plan.py similarity index 100% rename from exploration_evidence_plan.py rename to src/exploration_evidence_plan.py diff --git a/exploration_review.py b/src/exploration_review.py similarity index 100% rename from exploration_review.py rename to src/exploration_review.py diff --git a/feature_scan.py b/src/feature_scan.py similarity index 100% rename from feature_scan.py rename to src/feature_scan.py diff --git a/features.py b/src/features.py similarity index 99% rename from features.py rename to src/features.py index 7f2daf9..d28e5ca 100644 --- a/features.py +++ b/src/features.py @@ -237,7 +237,7 @@ def summary(path: Path = REG, *, create: bool = True) -> dict: maturity = item.get("maturity", "ad-hoc") counts[maturity] = counts.get(maturity, 0) + 1 candidates = promotion_candidates(path, create=create) - lifecycle = { + lifecycle: dict[str, Any] = { "path": None, "total": 0, "counts_by_status": {}, diff --git a/feedback.py b/src/feedback.py similarity index 100% rename from feedback.py rename to src/feedback.py diff --git a/frontend_verify.py b/src/frontend_verify.py similarity index 99% rename from frontend_verify.py rename to src/frontend_verify.py index 96a6a1a..58422c9 100644 --- a/frontend_verify.py +++ b/src/frontend_verify.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """frontend_verify.py — orchestrator capability: VISION-FREE frontend/UI verification. -Backlog #2 / BRIEF_expand_range.md #1. Expands the fleet's RANGE: today agents can edit TS/React but +Backlog #2 / docs/briefs/BRIEF_expand_range.md #1. Expands the fleet's RANGE: today agents can edit TS/React but cannot run + observe a UI, so frontend PRs ship unverified. This drives chromium via a LOCAL Playwright (node) helper and asserts against the ACCESSIBILITY TREE — token-cheap, deterministic, no multimodal model (Playwright-MCP-style; research: microsoft/playwright-mcp). Any fleet lane (incl. text-only Codex / diff --git a/gh_capacity.py b/src/gh_capacity.py similarity index 100% rename from gh_capacity.py rename to src/gh_capacity.py diff --git a/human_calibration.py b/src/human_calibration.py similarity index 100% rename from human_calibration.py rename to src/human_calibration.py diff --git a/improvement_log.py b/src/improvement_log.py similarity index 100% rename from improvement_log.py rename to src/improvement_log.py diff --git a/issue_quality.py b/src/issue_quality.py similarity index 100% rename from issue_quality.py rename to src/issue_quality.py diff --git a/issue_readiness.py b/src/issue_readiness.py similarity index 100% rename from issue_readiness.py rename to src/issue_readiness.py diff --git a/judge_reliability.py b/src/judge_reliability.py similarity index 99% rename from judge_reliability.py rename to src/judge_reliability.py index 597ed52..4108f14 100644 --- a/judge_reliability.py +++ b/src/judge_reliability.py @@ -131,7 +131,7 @@ def compute( raw_counts[evaluator] += 1 anchors = _human_anchor_rows(human_rows) - stats = { + stats: dict[str, Any] = { evaluator: { "consensus_errors": [], "human_errors": [], diff --git a/keepalive_evidence.py b/src/keepalive_evidence.py similarity index 100% rename from keepalive_evidence.py rename to src/keepalive_evidence.py diff --git a/keepalive_outcomes.py b/src/keepalive_outcomes.py similarity index 100% rename from keepalive_outcomes.py rename to src/keepalive_outcomes.py diff --git a/keepalive_shadow.py b/src/keepalive_shadow.py similarity index 99% rename from keepalive_shadow.py rename to src/keepalive_shadow.py index 38862de..983eabf 100644 --- a/keepalive_shadow.py +++ b/src/keepalive_shadow.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """keepalive_shadow.py - shadow-mode corpus builder for keepalive PR supervision. -STAGE 2 of the BRIEF_keepalive_transfers.md "#6 staged path": before any LIVE +STAGE 2 of the docs/briefs/BRIEF_keepalive_transfers.md "#6 staged path": before any LIVE supervisor is justified, accumulate evidence. For a keepalive-driven PR, this module reads the keepalive state, reconstructs a `watch.py`-style report, asks the existing advisory `redirect_policy.decide()` what it WOULD recommend, compares diff --git a/keepalive_supervisor.py b/src/keepalive_supervisor.py similarity index 100% rename from keepalive_supervisor.py rename to src/keepalive_supervisor.py diff --git a/langsmith_direct.py b/src/langsmith_direct.py similarity index 100% rename from langsmith_direct.py rename to src/langsmith_direct.py diff --git a/langsmith_fetch.py b/src/langsmith_fetch.py similarity index 100% rename from langsmith_fetch.py rename to src/langsmith_fetch.py diff --git a/langsmith_pull.py b/src/langsmith_pull.py similarity index 100% rename from langsmith_pull.py rename to src/langsmith_pull.py diff --git a/ledger_reconcile.py b/src/ledger_reconcile.py similarity index 100% rename from ledger_reconcile.py rename to src/ledger_reconcile.py diff --git a/local_verify.py b/src/local_verify.py similarity index 100% rename from local_verify.py rename to src/local_verify.py diff --git a/mcp_server.py b/src/mcp_server.py similarity index 100% rename from mcp_server.py rename to src/mcp_server.py diff --git a/merge_guard.py b/src/merge_guard.py similarity index 100% rename from merge_guard.py rename to src/merge_guard.py diff --git a/model_profile_trial.py b/src/model_profile_trial.py similarity index 99% rename from model_profile_trial.py rename to src/model_profile_trial.py index 60e4dce..fe788fb 100644 --- a/model_profile_trial.py +++ b/src/model_profile_trial.py @@ -185,7 +185,7 @@ def source_manifest(root: Path) -> dict[str, Any]: root = root.resolve() if not root.is_dir(): raise ValueError(f"source root is not a directory: {root}") - entries = [] + entries: list[Any] = [] total_bytes = 0 # Prune derived/vendor directories before walking them. A plain rglob # still descends through .git and virtualenv trees on CloudStorage even diff --git a/model_profile_trial_bridge.py b/src/model_profile_trial_bridge.py similarity index 100% rename from model_profile_trial_bridge.py rename to src/model_profile_trial_bridge.py diff --git a/objective_anchor.py b/src/objective_anchor.py similarity index 100% rename from objective_anchor.py rename to src/objective_anchor.py diff --git a/observability_dashboard.py b/src/observability_dashboard.py similarity index 100% rename from observability_dashboard.py rename to src/observability_dashboard.py diff --git a/outcomes.py b/src/outcomes.py similarity index 100% rename from outcomes.py rename to src/outcomes.py diff --git a/partitioned_review.py b/src/partitioned_review.py similarity index 100% rename from partitioned_review.py rename to src/partitioned_review.py diff --git a/src/paths.py b/src/paths.py new file mode 100644 index 0000000..6abe1da --- /dev/null +++ b/src/paths.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""paths.py — where the modules live, and where the CHECKOUT root is. Two things, not one. + +WHY THIS EXISTS. Until 2026-08-23 every module derived both answers from the same expression, +`Path(__file__).resolve().parent`, because they happened to be the same directory: the modules sat +at the repo root. They are not the same thing, and conflating them is what made a `src/` layout +look like a 126-file rewrite. Two distinct questions were being answered by one accident: + + * "where is my sibling MODULE?" — `capabilities.py`, `dispatcher.py`. Answer: MODULE_DIR. + * "where is the CHECKOUT?" — `orchestrate.sh`, `.verify-floor.json`, `.coveragerc`, the sibling + fleet repos two levels up. Answer: REPO_ROOT. + +DETECTED, NOT HARDCODED, and that is the load-bearing part. `orch-sync-mirror.sh` copies modules +into the mirror that launchd actually runs, and it may copy them FLAT (module dir == repo root) or +under `src/`. A hardcoded `parent.parent` would be right in one tree and wrong in the other — the +exact failure `capability_activation_audit._fleet_roots` documents, where byte-identical code scored +37 of 37 in the canonical tree and 36 of 37 in the mirror. So the layout is *observed*: if the +module directory is named `src`, the checkout is its parent; otherwise the two coincide. + +That makes this module a no-op on a flat tree, which is deliberate — it lands and is verified +BEFORE any file moves, so the move itself changes no behaviour here. + +Deliberately dependency-free (pathlib only). Every module may import it without risking a cycle: +`capabilities` imports `feedback`, and `feedback`'s own selftest imports `env_prereq`, so anything +placed in those modules instead would close a loop for somebody. +""" + +from __future__ import annotations + +import os +from pathlib import Path + +# The directory holding the orchestrator's modules — this file's own directory, by construction. +MODULE_DIR = Path(__file__).resolve().parent + +# The checkout root: the directory holding `orchestrate.sh`, `.verify-floor.json`, `.coveragerc`, +# `pyproject.toml` and the docs. Equal to MODULE_DIR on a flat tree; its parent under `src/`. +REPO_ROOT = ( + MODULE_DIR.parent if MODULE_DIR.name == "src" else MODULE_DIR +) # == checkout_root(MODULE_DIR) + +# The directory holding the SIBLING FLEET repos (`Workflows`, `Counter_Risk`, ...). One level above +# the checkout, and never above the module dir — that distinction is the whole point of this file. +# `$ORCH_FLEET_ROOT` still wins where a caller consults it; this is only the derived default. +FLEET_ROOT = REPO_ROOT.parent + + +# Where the test suite lives. Named here because ONE module legitimately reaches into it — +# `capability_admission` reads the recurrence-fixture roster from `test_capability_set_coverage` — +# and an implicit `sys.path` accident is how that dependency would rot silently. +TESTS_DIR = REPO_ROOT / "tests" + + +def checkout_root(module_dir: Path) -> Path: + """Apply the rule to an ARBITRARY module dir, not just this file's. + + The constants above are the common case, but a caller that resolves paths relative to its OWN + module directory needs the rule applied to that — and its tests patch that directory to build + synthetic trees. Reading a module-level constant would make those tests un-patchable and, worse, + would silently ignore the tree the caller thinks it is looking at. One rule, applied wherever + asked; that is what stops the module dir and the checkout root drifting apart again. + """ + module_dir = Path(module_dir) + return module_dir.parent if module_dir.name == "src" else module_dir + + +def fleet_root(module_dir: Path) -> Path: + """Where the SIBLING FLEET repos sit, relative to an arbitrary module dir. One above the + CHECKOUT — never one above the module dir, which is the same thing only on a flat tree.""" + return checkout_root(module_dir).parent + + +def orchestrate_sh() -> Path: + """The tick driver, which lives at the CHECKOUT root rather than beside the modules. + + A function rather than a constant because several callers want to know whether it exists, and + a constant would invite `if ORCHESTRATE:` — always true for a Path, existing or not. + """ + return REPO_ROOT / "orchestrate.sh" + + +def _selftest() -> None: + import tempfile + + # FLAT tree: the two roots coincide, so this module is inert before the move. + assert MODULE_DIR == Path(__file__).resolve().parent + if MODULE_DIR.name != "src": + assert REPO_ROOT == MODULE_DIR, (REPO_ROOT, MODULE_DIR) + + # The detection itself, exercised on both shapes rather than on whichever one we are in — + # the mirror may be flat while the checkout is not, and one run can only be in one of them. + resolve = checkout_root # the REAL rule, not a copy of it — a copy could pass while it drifts + + with tempfile.TemporaryDirectory(prefix="paths-selftest-") as td: + root = Path(td) + (root / "src").mkdir() + assert resolve(root / "src") == root, "src/ layout must resolve the checkout to its parent" + assert resolve(root) == root, "a flat tree must resolve the checkout to itself" + # A directory that merely CONTAINS a src/ is not itself src/ — the name is the signal. + assert resolve(root / "lib") == root / "lib" + + # The fleet root is one above the CHECKOUT, never one above the module dir. Under `src/` those + # differ by a level, and getting it wrong is what put the mirror one capability short. + assert FLEET_ROOT == REPO_ROOT.parent == fleet_root(MODULE_DIR) + assert checkout_root(MODULE_DIR) == REPO_ROOT, "constants must come from the rule" + assert orchestrate_sh().parent == REPO_ROOT, orchestrate_sh() + # $ORCH_FLEET_ROOT is a caller's override, not this module's business; assert we do not read it. + assert "ORCH_FLEET_ROOT" not in os.environ or FLEET_ROOT == REPO_ROOT.parent + + print( + f"paths.py selftest: OK (module dir vs checkout root distinguished, layout DETECTED not " + f"hardcoded for both flat and src/ shapes, fleet root anchored to the checkout; " + f"here MODULE_DIR={MODULE_DIR.name!r} REPO_ROOT={REPO_ROOT.name!r})" + ) + + +def main(argv: list[str]) -> int: + if "--selftest" in argv: + _selftest() + return 0 + print(f"MODULE_DIR = {MODULE_DIR}") + print(f"REPO_ROOT = {REPO_ROOT}") + print(f"FLEET_ROOT = {FLEET_ROOT}") + print(f"orchestrate.sh = {orchestrate_sh()} (exists: {orchestrate_sh().is_file()})") + return 0 + + +if __name__ == "__main__": + import sys + + raise SystemExit(main(sys.argv[1:])) diff --git a/pattern_miner.py b/src/pattern_miner.py similarity index 100% rename from pattern_miner.py rename to src/pattern_miner.py diff --git a/periodic_report.py b/src/periodic_report.py similarity index 100% rename from periodic_report.py rename to src/periodic_report.py diff --git a/provision.py b/src/provision.py similarity index 100% rename from provision.py rename to src/provision.py diff --git a/range_lane_rollout.py b/src/range_lane_rollout.py similarity index 100% rename from range_lane_rollout.py rename to src/range_lane_rollout.py diff --git a/redirect_apply.py b/src/redirect_apply.py similarity index 100% rename from redirect_apply.py rename to src/redirect_apply.py diff --git a/redirect_plan.py b/src/redirect_plan.py similarity index 100% rename from redirect_plan.py rename to src/redirect_plan.py diff --git a/redirect_policy.py b/src/redirect_policy.py similarity index 100% rename from redirect_policy.py rename to src/redirect_policy.py diff --git a/redirect_shadow.py b/src/redirect_shadow.py similarity index 100% rename from redirect_shadow.py rename to src/redirect_shadow.py diff --git a/redirect_sweep.py b/src/redirect_sweep.py similarity index 100% rename from redirect_sweep.py rename to src/redirect_sweep.py diff --git a/relearn_report.py b/src/relearn_report.py similarity index 100% rename from relearn_report.py rename to src/relearn_report.py diff --git a/repo_knowledge.py b/src/repo_knowledge.py similarity index 99% rename from repo_knowledge.py rename to src/repo_knowledge.py index be83720..a11a4c5 100644 --- a/repo_knowledge.py +++ b/src/repo_knowledge.py @@ -154,7 +154,7 @@ DOC_DIRS = {"docs", ".github"} DOC_SKIP_DIRS = {".git", "node_modules", ".venv", "venv", "dist", "build", ".next", "__pycache__"} DOC_SKIP_FILE_PREFIXES = ("BRIEF_",) -DOC_SKIP_FILE_NAMES = {"CODEX_BRIEF.md"} +DOC_SKIP_FILE_NAMES = {"docs/briefs/CODEX_BRIEF.md"} # v2 (2026-08-23): corrects a factually wrong Trend summary, unscopes seeded invariants, and adds # the `contraindications` section. Bumping this runs `_migrate()` once against an existing registry. diff --git a/research_scheduler.py b/src/research_scheduler.py similarity index 100% rename from research_scheduler.py rename to src/research_scheduler.py diff --git a/research_subjects.py b/src/research_subjects.py similarity index 100% rename from research_subjects.py rename to src/research_subjects.py diff --git a/roles.py b/src/roles.py similarity index 99% rename from roles.py rename to src/roles.py index 3d964b0..ade4cfb 100644 --- a/roles.py +++ b/src/roles.py @@ -2395,7 +2395,7 @@ def run_triage_agent( if dispatch and backend_name: role_run_id = f"role:triage:{backend_name}:{time.time_ns()}" try: - actions = {} + actions: dict[str, Any] = {} for rec in advisory_plan.get("recommendations") or []: action = rec.get("action") actions[action] = actions.get(action, 0) + 1 diff --git a/router.py b/src/router.py similarity index 100% rename from router.py rename to src/router.py diff --git a/runner_effect_bridge.py b/src/runner_effect_bridge.py similarity index 100% rename from runner_effect_bridge.py rename to src/runner_effect_bridge.py diff --git a/runtime_ac.py b/src/runtime_ac.py similarity index 100% rename from runtime_ac.py rename to src/runtime_ac.py diff --git a/runtime_ac_flow_monitor.py b/src/runtime_ac_flow_monitor.py similarity index 100% rename from runtime_ac_flow_monitor.py rename to src/runtime_ac_flow_monitor.py diff --git a/runtime_ac_gate.py b/src/runtime_ac_gate.py similarity index 100% rename from runtime_ac_gate.py rename to src/runtime_ac_gate.py diff --git a/runtime_ac_panel.py b/src/runtime_ac_panel.py similarity index 100% rename from runtime_ac_panel.py rename to src/runtime_ac_panel.py diff --git a/strategy_experiment.py b/src/strategy_experiment.py similarity index 100% rename from strategy_experiment.py rename to src/strategy_experiment.py diff --git a/switch_review.py b/src/switch_review.py similarity index 100% rename from switch_review.py rename to src/switch_review.py diff --git a/synthesis_promotion.py b/src/synthesis_promotion.py similarity index 99% rename from synthesis_promotion.py rename to src/synthesis_promotion.py index d1498ca..0e317ce 100644 --- a/synthesis_promotion.py +++ b/src/synthesis_promotion.py @@ -309,7 +309,7 @@ def ensure_evaluated_state( initial_event = ( "promotion:" + hashlib.sha256(f"{exp_id}|evaluated".encode()).hexdigest()[:24] ) - state = { + state: dict[str, Any] = { "schema_version": SCHEMA_VERSION, "experiment_id": exp_id, "repo": meta.get("repo"), diff --git a/testgen_gate.py b/src/testgen_gate.py similarity index 99% rename from testgen_gate.py rename to src/testgen_gate.py index fa27ce5..b2cabc3 100644 --- a/testgen_gate.py +++ b/src/testgen_gate.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """testgen_gate.py - assured-acceptance gate for generated tests. -BRIEF_expand_range.md option #2. This widens the fleet from "implement the +docs/briefs/BRIEF_expand_range.md option #2. This widens the fleet from "implement the given issue" to "propose coverage-raising tests, then accept only the tests that survive a gate." The gate follows the transferable TestGen-LLM pattern: collect/import -> non-regression -> repeated reliability -> coverage delta. diff --git a/testgen_lane.py b/src/testgen_lane.py similarity index 99% rename from testgen_lane.py rename to src/testgen_lane.py index 03fa7cc..a2d825f 100644 --- a/testgen_lane.py +++ b/src/testgen_lane.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """testgen_lane.py - build gate-backed prompts for generated-test work. -BRIEF_expand_range.md option #2 already supplied `testgen_gate.py`. This helper +docs/briefs/BRIEF_expand_range.md option #2 already supplied `testgen_gate.py`. This helper wires that gate into an actual orchestrator lane: the seat can generate one prompt file for a delegated agent, and the prompt includes the exact acceptance gate command that must pass before commit/PR. diff --git a/tick.py b/src/tick.py similarity index 100% rename from tick.py rename to src/tick.py diff --git a/ux_review.py b/src/ux_review.py similarity index 100% rename from ux_review.py rename to src/ux_review.py diff --git a/verify.py b/src/verify.py similarity index 87% rename from verify.py rename to src/verify.py index 2f02765..b6aecf4 100644 --- a/verify.py +++ b/src/verify.py @@ -50,13 +50,24 @@ import argparse import datetime as _dt import json +import os import pathlib import re import subprocess import sys +# TWO ROOTS, and verify.py is the module that most needs them separated: it DISCOVERS modules +# (beside itself) and it READS repo files — the floor, the coverage artifacts — and RUNS pytest, +# all of which belong to the checkout. They are the same directory only on a flat tree. HERE = pathlib.Path(__file__).resolve().parent -FLOOR = HERE / ".verify-floor.json" +try: + import paths + + MODULES = paths.MODULE_DIR + ROOT = paths.checkout_root(HERE) +except Exception: # noqa: BLE001 — verify.py must run even if a sibling module is broken + MODULES = ROOT = HERE +FLOOR = ROOT / ".verify-floor.json" # --- coverage instrumentation, OFF unless asked for ------------------------------------------- # Every runner below spawns a SUBPROCESS. That is deliberate (a selftest must be exercised the way @@ -89,7 +100,7 @@ def child_argv(argv: list[str]) -> list[str]: def coverage_reset() -> None: """Delete stale parallel data files so a combine cannot mix runs.""" - for stale in list(HERE.glob(".coverage.*")) + [HERE / ".coverage"]: + for stale in list(ROOT.glob(".coverage.*")) + [ROOT / ".coverage"]: try: stale.unlink() except OSError: @@ -103,18 +114,18 @@ def coverage_combine_and_report() -> str: number nobody produced must not be mistaken for a coverage number that is bad, and neither may look like success. That is the same rule the selftest three-way split follows. """ - files = sorted(HERE.glob(".coverage.*")) + files = sorted(ROOT.glob(".coverage.*")) if not files: return "coverage: NO DATA — no instrumented child wrote a data file (did any test run?)\n" subprocess.run( [sys.executable, "-m", "coverage", "combine", "--quiet"], - cwd=HERE, + cwd=ROOT, capture_output=True, text=True, ) proc = subprocess.run( [sys.executable, "-m", "coverage", "report"], - cwd=HERE, + cwd=ROOT, capture_output=True, text=True, ) @@ -149,7 +160,7 @@ def run_pytest(*, extra: list[str] | None = None) -> dict: [sys.executable, "-m", "pytest", "-q", "-rfEs", "-p", "no:cacheprovider", "--no-header"] ) cmd += extra or [] - proc = subprocess.run(cmd, cwd=HERE, capture_output=True, text=True) + proc = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True) tail = (proc.stdout or "") + (proc.stderr or "") counts: dict[str, int] = {} for n, kind in COUNT_RE.findall(tail): @@ -188,7 +199,7 @@ def run_pytest(*, extra: list[str] | None = None) -> dict: def selftest_modules() -> list[str]: """Discover modules exposing --selftest instead of hardcoding a list that goes stale.""" found = [] - for path in sorted(HERE.glob("*.py")): + for path in sorted(MODULES.glob("*.py")): if path.name.startswith("test_") or path.name == "verify.py": continue try: @@ -213,8 +224,8 @@ def run_selftests(modules: list[str]) -> dict: ok, bad, skipped = [], {}, {} for mod in modules: proc = subprocess.run( - child_argv([sys.executable, f"{mod}.py", "--selftest"]), - cwd=HERE, + child_argv([sys.executable, str(MODULES / f"{mod}.py"), "--selftest"]), + cwd=ROOT, capture_output=True, text=True, ) @@ -239,6 +250,9 @@ def run_selftests(modules: list[str]) -> dict: return {"ok": ok, "failed": bad, "skipped": skipped} +# Two of the five gates are TEST files and three are modules, so they no longer live in one +# directory. Named by bare filename and resolved below — a hardcoded prefix per entry would be a +# second place for the layout to be recorded, and those drift. GATES = ( ("activation audit", ["capability_activation_audit.py", "--no-cache"]), ("recurrence replay", ["capability_recurrence_check.py"]), @@ -248,11 +262,40 @@ def run_selftests(modules: list[str]) -> dict: ) +def _gate_script(name: str) -> str: + """Absolute path to a gate's script, whether it is a module or a test file.""" + for base in (MODULES, ROOT / "tests"): + candidate = base / name + if candidate.is_file(): + return str(candidate) + # Absent is reported by the runner as a failure, never silently skipped — so return the + # module-dir guess and let the "cannot open file" surface with the name in it. + return str(MODULES / name) + + +def _child_env() -> dict: + """`src` on PYTHONPATH, because two of the gates are TEST files. + + Running `python3 tests/test_capability_admission.py` puts `tests/` on `sys.path`, not `src/`, so + its `import capabilities` fails — the gate reported `ModuleNotFoundError` rather than a verdict. + pytest gets this from `pythonpath = ["src"]` in pyproject.toml; a bare subprocess has to be told. + Prepended rather than replacing any inherited PYTHONPATH. + """ + env = dict(os.environ) + existing = env.get("PYTHONPATH") + env["PYTHONPATH"] = f"{MODULES}{os.pathsep}{existing}" if existing else str(MODULES) + return env + + def run_gates() -> dict: out = {} for name, argv in GATES: proc = subprocess.run( - child_argv([sys.executable, *argv]), cwd=HERE, capture_output=True, text=True + child_argv([sys.executable, _gate_script(argv[0]), *argv[1:]]), + cwd=ROOT, + capture_output=True, + text=True, + env=_child_env(), ) text = (proc.stdout or "") + (proc.stderr or "") # Same three-way split as the selftests: a gate that could not judge here says so with @@ -456,9 +499,51 @@ def _appended_note(prior: str | None, collected: int, passed: int, today: str) - ("skipped_max", "skipped test(s)"), ("selftest_skipped_max", "skipped selftest(s)"), ("gate_skipped_max", "skipped gate(s)"), + # The mypy exempt list is bounded exactly like skipping, and for the same reason: a list of + # type-check exemptions that can only GROW is an amnesty with a deadline nobody set. ONE + # constant, defined once in the floor file and consumed by both the count and the bound — a + # matching pair of literals would drift, a shared name cannot. + ("mypy_exempt_max", "module(s) exempt from mypy"), ) +def mypy_exempt_modules() -> list[str] | None: + """Modules on `[[tool.mypy.overrides]] ignore_errors` — the ratchet's blocking quantity. + + None means the question could not be answered here (no pyproject.toml, unreadable, no override). + REPORTED, never treated as zero: a ratchet that stops being counted is indistinguishable from + one that emptied, and only one of those is good news. + """ + try: + import tomllib + + data = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + except Exception: # noqa: BLE001 + return None + for override in data.get("tool", {}).get("mypy", {}).get("overrides") or []: + if override.get("ignore_errors"): + mods = override.get("module") + return sorted(mods) if isinstance(mods, list) else [str(mods)] + return None + + +def _format_mypy_exempt_line(mods: list[str] | None, limit: int | None, total: int = 99) -> str: + """One line carrying BOTH numbers, per the runtime rule in CLAUDE.md. PURE. + + `typecheck: on` alone reads as "typed". `66/66 max of 99 modules exempt` reads as "typed where + it is checked, and here is exactly how much is not" — the difference between a ratchet and an + amnesty. + """ + if mods is None: + return " mypy ratchet: NOT COUNTED (no readable ignore_errors override in pyproject.toml)" + bound = f"/{limit} max" if limit is not None else " (no ceiling set)" + tail = " — type a module, delete its line" if mods else " — fully drained" + return ( + f" mypy ratchet: {len(mods)}{bound} of {total} module(s) exempt, " + f"{total - len(mods)} checked{tail}" + ) + + def _floor_problems(floor: dict, py: dict) -> list[str]: """Is the amount of CHECKING wrong in either direction? Pure, so the selftest exercises the real rule. @@ -535,8 +620,11 @@ def _ceiling_problems(floor: dict, actual: dict) -> list[str]: continue if actual.get(key, 0) > int(limit): problems.append( - f"SKIP CEILING exceeded: {actual[key]} {label} > agreed maximum {limit}. " - f"Skipping is bounded on purpose — either the new skip is wrong, or raise " + # `CEILING`, not `SKIP CEILING`: the mypy exempt list is bounded by this same + # machinery and is not a skip, so the old wording sent a reader hunting for a skip + # that does not exist. The label already names WHICH population overflowed. + f"CEILING exceeded: {actual[key]} {label} > agreed maximum {limit}. " + f"This is bounded on purpose — either the new one is wrong, or raise " f"`{key}` in .verify-floor.json deliberately and say why." ) return problems @@ -570,10 +658,13 @@ def verify(*, update_floor: bool = False, forgive_healed_drift: bool = False) -> # so a future change cannot quietly convert a red into a skip. Each ceiling reports its own # value against the limit in the same breath, per the house rule that a gate must always say # both numbers — `24/24` alone reads as "fine", `24/24 (ceiling)` reads as "at the limit". + exempt = mypy_exempt_modules() actual = { "skipped_max": py["skipped"], "selftest_skipped_max": len(st["skipped"]), "gate_skipped_max": sum(1 for r in gates.values() if r["skipped"]), + # None (uncountable) must not read as 0 — that would let the ceiling pass by being blind. + "mypy_exempt_max": len(exempt) if exempt is not None else 0, } problems += _ceiling_problems(floor, actual) @@ -619,6 +710,9 @@ def _cap(key: str) -> str: entrypoints = absent_entrypoint_line() if entrypoints: lines.append(entrypoints) + # ALWAYS printed: a drained ratchet is news, and a ratchet that stopped being counted is + # exactly what this line exists to expose. + lines.append(_format_mypy_exempt_line(exempt, floor.get("mypy_exempt_max"))) # WHAT DID NOT RUN, always — under a green verdict as much as a red one. A number of skips # with no reasons beside it is how "green" quietly stops meaning "checked". @@ -735,7 +829,7 @@ def _selftest() -> None: # does precisely that and confirm it is classified as failed, not ok. import tempfile - saved = globals()["HERE"] + saved, saved_mods = globals()["HERE"], globals()["MODULES"] with tempfile.TemporaryDirectory(prefix="verify-") as td: (pathlib.Path(td) / "silent_mod.py").write_text( 'import sys\nif "--selftest" in sys.argv:\n sys.exit(0)\n' @@ -752,10 +846,10 @@ def _selftest() -> None: f' print("skipping_mod selftest: {PREREQ_ABSENT_MARK} the widget is not installed")\n' ) try: - globals()["HERE"] = pathlib.Path(td) + globals()["HERE"] = globals()["MODULES"] = pathlib.Path(td) got = run_selftests(["silent_mod", "loud_mod", "skipping_mod"]) finally: - globals()["HERE"] = saved + globals()["HERE"], globals()["MODULES"] = saved, saved_mods assert "silent_mod" in got["failed"], f"a silent zero-exit must FAIL: {got}" assert "did it run?" in got["failed"]["silent_mod"], got assert got["ok"] == ["loud_mod"], f"a skipped selftest must not be counted as ok: {got}" @@ -788,7 +882,7 @@ def _selftest() -> None: over = _ceiling_problems( {"skipped_max": 24}, {"skipped_max": 25, "selftest_skipped_max": 0, "gate_skipped_max": 0} ) - assert len(over) == 1 and "SKIP CEILING exceeded" in over[0], over + assert len(over) == 1 and "CEILING exceeded" in over[0], over assert "skipped_max" in over[0], "the failure must name the key to raise deliberately" # Each ceiling is independent — one slipping must not be masked by the others holding. for key in ("selftest_skipped_max", "gate_skipped_max"): @@ -991,6 +1085,37 @@ def _selftest() -> None: ) assert "not found in any sibling checkout" in nowhere, nowhere + # THE TWO ROOTS. On a flat tree they coincide, which is exactly why an assertion is needed: + # without one, code that re-merged them would pass here and only fail after the layout moved. + assert ROOT == paths.checkout_root(HERE), (ROOT, HERE) + assert MODULES == HERE or MODULES.name == "src", (MODULES, HERE) + assert FLOOR.parent == ROOT, "the floor belongs to the checkout, not the module dir" + + # ---- the mypy ratchet --------------------------------------------------------------------- + # An exempt list that can only GROW is an amnesty with a deadline nobody set. Two properties + # make it a ratchet: the count is always PRINTED with its bound, and it is CEILINGED by the + # same generic machinery as the skips. + assert "64/64 max of 99" in _format_mypy_exempt_line(["m"] * 64, 64) + assert "35 checked" in _format_mypy_exempt_line(["m"] * 64, 64) + assert "type a module, delete its line" in _format_mypy_exempt_line(["m"], 1) + # A DRAINED ratchet must say so, not print a bare 0 — the drain finishing is news. + assert "fully drained" in _format_mypy_exempt_line([], 64) + # UNCOUNTABLE must never render as zero: a ratchet that stopped being counted looks identical + # to one that emptied, and only one of those is good news. + nc = _format_mypy_exempt_line(None, 64) + assert "NOT COUNTED" in nc and " 0" not in nc, nc + # It is not a skip, so it must not carry the mark that spends skip-ceiling headroom. + assert PREREQ_ABSENT_MARK not in _format_mypy_exempt_line(["m"], 64) + # Bounded by the SAME `_ceiling_problems` used for skips — one mechanism, so the measuring and + # draining windows cannot drift apart. + assert ("mypy_exempt_max", "module(s) exempt from mypy") in CEILINGS + _zero = {k: 0 for k, _ in CEILINGS} + assert _ceiling_problems({"mypy_exempt_max": 64}, {**_zero, "mypy_exempt_max": 64}) == [] + _over = _ceiling_problems({"mypy_exempt_max": 64}, {**_zero, "mypy_exempt_max": 65}) + assert len(_over) == 1 and "mypy_exempt_max" in _over[0], _over + # And the real file must be readable, or the line would silently report NOT COUNTED forever. + assert mypy_exempt_modules(), "pyproject.toml's ignore_errors override is unreadable" + print( "verify.py selftest: OK (count parsing, selftest discovery, silent-zero-exit is a " "FAILURE, a loud skip is not a pass, skip ceiling fails when exceeded and holds when " @@ -999,7 +1124,7 @@ def _selftest() -> None: "clobbering the ceiling rationale, a no-op floor write is skipped entirely, " "--reconcile-floor forgives ONLY a drift it " "healed and never an unwritten one, absent-module line is silent when clean and is " - "never counted as a skip)" + "never counted as a skip, mypy ratchet prints both numbers and its ceiling can fail)" ) diff --git a/watch.py b/src/watch.py similarity index 99% rename from watch.py rename to src/watch.py index e906ea6..44ec26c 100644 --- a/watch.py +++ b/src/watch.py @@ -510,7 +510,7 @@ def classify_lane( # evidence of its own usefulness, and eventually reads as dead code. _capability_heartbeat() if pid is None and not log and not worktree: - report = { + report: dict[str, Any] = { "agent": agent, "target": target, "lane": lane, diff --git a/test_capabilities.py b/tests/test_capabilities.py similarity index 99% rename from test_capabilities.py rename to tests/test_capabilities.py index 7bce34f..ad44a96 100644 --- a/test_capabilities.py +++ b/tests/test_capabilities.py @@ -1,5 +1,4 @@ import json -import pathlib import time from concurrent.futures import ThreadPoolExecutor @@ -580,7 +579,10 @@ def test_verifying_the_system_never_writes_the_live_ledger(): assert forbidden.search("x = capabilities.load(" + "REG)") assert not forbidden.search("x = capabilities.load_declared(" + "capabilities.REG)") - root = pathlib.Path(__file__).resolve().parent + import paths + + # MODULE dir: this walks the orchestrator's modules, not the checkout. + root = paths.MODULE_DIR offenders = [] for path in sorted(root.glob("*.py")): text = path.read_text(encoding="utf-8", errors="replace") diff --git a/test_capability_admission.py b/tests/test_capability_admission.py similarity index 100% rename from test_capability_admission.py rename to tests/test_capability_admission.py diff --git a/test_capability_causal_core.py b/tests/test_capability_causal_core.py similarity index 100% rename from test_capability_causal_core.py rename to tests/test_capability_causal_core.py diff --git a/test_capability_epic.py b/tests/test_capability_epic.py similarity index 100% rename from test_capability_epic.py rename to tests/test_capability_epic.py diff --git a/test_capability_lifecycle_e2e.py b/tests/test_capability_lifecycle_e2e.py similarity index 100% rename from test_capability_lifecycle_e2e.py rename to tests/test_capability_lifecycle_e2e.py index 78f3b77..8162a43 100644 --- a/test_capability_lifecycle_e2e.py +++ b/tests/test_capability_lifecycle_e2e.py @@ -6,6 +6,12 @@ from pathlib import Path import pytest +from test_evidence_contract_compiler import _plan as evidence_contract_plan +from test_evidence_contract_compiler import gap_rows as evidence_gap_rows +from test_playbook_compiler import REPO as PLAYBOOK_REPO +from test_playbook_compiler import _candidate as playbook_candidate +from test_playbook_compiler import _write_registry as write_playbook_registry +from test_role_compiler import _candidate as role_candidate import capabilities import capability_compiler as compiler @@ -14,12 +20,6 @@ import env_prereq import feedback import roles -from test_evidence_contract_compiler import _plan as evidence_contract_plan -from test_evidence_contract_compiler import gap_rows as evidence_gap_rows -from test_playbook_compiler import REPO as PLAYBOOK_REPO -from test_playbook_compiler import _candidate as playbook_candidate -from test_playbook_compiler import _write_registry as write_playbook_registry -from test_role_compiler import _candidate as role_candidate def _active_predecessor(capability_id: str, now: int) -> dict: diff --git a/test_capability_set_coverage.py b/tests/test_capability_set_coverage.py similarity index 100% rename from test_capability_set_coverage.py rename to tests/test_capability_set_coverage.py diff --git a/test_capacity_profiles.py b/tests/test_capacity_profiles.py similarity index 100% rename from test_capacity_profiles.py rename to tests/test_capacity_profiles.py diff --git a/test_ci_gate_config.py b/tests/test_ci_gate_config.py similarity index 62% rename from test_ci_gate_config.py rename to tests/test_ci_gate_config.py index 5eaf42a..91f3886 100644 --- a/test_ci_gate_config.py +++ b/tests/test_ci_gate_config.py @@ -24,7 +24,8 @@ `needs.detect.outputs.*`, so a second hardcoded `false` would be a literal that can drift. 6. **The recorded baseline moves with the pins.** The counts are version-specific, so bumping a pin without re-measuring must go red rather than quietly re-describing a toolchain nobody runs. -7. **`mypy.ini` never silences an error code.** Fifteen `disable_error_code` entries would cover +7. **The mypy config never silences an error code.** It lives in `pyproject.toml` now; CI + passes `--config-file pyproject.toml` whenever that file exists. Fifteen `disable_error_code` entries would cover 603 of the 608 findings and produce a green job that checks nothing. 8. **A citation names a file that is there.** These configs are prose-heavy on purpose: each tells the next reader where to re-measure before touching a pin. The pin file shipped citing @@ -45,19 +46,23 @@ from __future__ import annotations +import json import re import tomllib -from pathlib import Path import pytest import env_prereq -HERE = Path(__file__).resolve().parent +# Repo-root files, resolved through the shared rule rather than a local `parent.parent`: +# these tests live in `tests/` while the things they assert on live at the checkout root. +import paths + +HERE = paths.REPO_ROOT GATE = HERE / ".github" / "workflows" / "pr-00-gate.yml" PINS = HERE / ".github" / "workflows" / "autofix-versions.env" RUFF_TOML = HERE / "ruff.toml" -MYPY_INI = HERE / "mypy.ini" +PYPROJECT = HERE / "pyproject.toml" # mypy config moved here; CI reads only this when it exists BASELINE_DOC = HERE / "docs" / "CI_LINT_BASELINE.md" BASELINE_SCRIPT = HERE / "scripts" / "ci_lint_baseline.py" @@ -215,21 +220,42 @@ def test_ruff_config_declares_an_explicit_selection(): ) -def test_every_disabled_toggle_states_blocking_and_drainable(): - """A gate that cannot say what would clear it is already defective.""" +def test_every_bounded_or_disabled_toggle_states_blocking_and_drainable(): + """A gate that cannot say what would clear it is already defective. + + ALL FIVE TOGGLES ARE NOW ON. `typecheck` was the last one forced off, and it went on when the + src/ move scoped the Gate to 99 modules and pyproject.toml's per-module override list made the + remaining findings exempt BY NAME — so the check runs over the clean modules today instead of + over nothing. That does not retire this expectation, it moves it: a toggle that is ON but + BOUNDED owes exactly the same three fields as one that is off, because "on" over a scoped + subset can hide as much as "off" if the scope is not stated. So the annotation requirement now + binds on any toggle that is disabled OR whose comment describes a bound, and the test asserts + at least one such toggle exists — otherwise it would pass vacuously the moment someone deleted + every annotation. + """ require_checkout() script = toggles_script() disabled = re.findall(r"^\s*([a-z_]+) = False\s*$", script, re.M) - assert disabled, ( - "no toggle is forced off in the `Compute Python CI toggles` step. If every check is now on, " - "delete this test's expectation along with the annotations it guards." + # A toggle whose annotation block claims a bound is held to the same standard as a disabled one. + bounded = [ + name + for name in re.findall(r"^\s*([a-z_]+) = RUN_CORE\s*$", script, re.M) + if "drainable:" in script.split(f"{name} = RUN_CORE")[0].rsplit("\n\n", 1)[-1] + ] + annotated = disabled + bounded + assert annotated, ( + "no toggle in the `Compute Python CI toggles` step is either forced off or annotated with a " + "bound. Every check being unconditionally on is a legitimate end state — but then the " + "blocking/drainable annotations have been deleted, and this test is the only thing that " + "required them, so re-read the step before deleting this expectation." ) - for name in disabled: + for name in annotated: + marker = " = False" if name in disabled else " = RUN_CORE" # The annotation block for a toggle is the comment run immediately above its assignment. - block = script.split(f"{name} = False")[0].rsplit("\n\n", 1)[-1] + block = script.split(f"{name}{marker}")[0].rsplit("\n\n", 1)[-1] for field in ("blocking:", "drainable:", "drains by:"): assert field in block, ( - f"the `{name} = False` toggle does not state `{field}` in the comment above it. " + f"the `{name}{marker}` toggle does not state `{field}` in the comment above it. " "Both quantities belong in the same place: an error count alone reads as " "be-patient, the same count with 'drainable 0 per PR' beside it reads as the " "deadlock it is. See " @@ -287,39 +313,132 @@ def test_baseline_was_measured_with_the_pinned_versions(): ) -def test_mypy_config_silences_nothing(): - """An OFF check is honest. A green check that examines nothing is not.""" +def test_mypy_config_silences_nothing_by_error_code(): + """An OFF check is honest. A green check that examines nothing is not. But SCOPE is not SILENCE. + + THE DISTINCTION THIS TEST NOW DRAWS, because the old version forbade both and the difference is + the whole design: + + * `disable_error_code` / `follow_imports = skip` make the check blind to a CLASS of error + across every module, permanently, with nothing to count and no mechanism that removes it. + Fifteen codes would have covered 603 of 608 findings. Still forbidden. + * per-module `ignore_errors` names the modules that are not clean yet. Every finding stays + discoverable (`scripts/ci_lint_baseline.py`), deleting a name restores that module's errors + immediately, and `.verify-floor.json`'s `mypy_exempt_max` FAILS if the list grows. That is + scoping with a drain and a bound, which is what let `typecheck` go from OFF over everything + to ON over the clean 35. + + So the list is permitted and its BOUND is asserted — because an exempt list nobody counts is + the same amnesty by a different route. + + Parsed from the TOML, never grepped from the text: the previous version substring-matched the + file and tripped on a COMMENT explaining why `disable_error_code` was rejected. A check that + fails on prose about itself teaches people to weaken it. + """ require_checkout() - assert MYPY_INI.is_file(), ( - "mypy.ini is missing. Without it `mypy .` aborts on 'Source file found twice under " - "different module names' and the recorded count becomes unverifiable prose." + assert PYPROJECT.is_file(), ( + "pyproject.toml is missing. Without its [tool.mypy] section `mypy` aborts on 'Source " + "file found twice under different module names' and the recorded count becomes " + "unverifiable prose." ) - text = MYPY_INI.read_text(encoding="utf-8") - for forbidden in ("disable_error_code", "ignore_errors", "follow_imports = skip"): - assert forbidden not in text, ( - f"mypy.ini sets `{forbidden}`. Fifteen disabled codes would cover 603 of the 608 " - "findings and make typecheck-mypy green while checking nothing — the exact defect " - "verify.py exists to stop. Leave the check OFF and drain the findings instead." + import tomllib + + with PYPROJECT.open("rb") as fh: + mypy_cfg = tomllib.load(fh).get("tool", {}).get("mypy", {}) + sections = [mypy_cfg] + list(mypy_cfg.get("overrides") or []) + for section in sections: + for forbidden in ("disable_error_code", "follow_imports"): + assert forbidden not in section, ( + f"the mypy config sets `{forbidden}`, which makes the check blind to a CLASS of " + "error across modules — nothing to count, and no mechanism that removes it. " + "Fifteen disabled codes would cover 603 of the 608 findings and make " + "typecheck-mypy green while checking nothing, the exact defect verify.py exists to " + "stop. Scope by MODULE with a counted, ceilinged list instead." + ) + # Top-level `ignore_errors` would exempt everything at once, which has no drain either. + assert "ignore_errors" not in mypy_cfg, ( + "[tool.mypy] sets a top-level `ignore_errors`, which exempts every module in one keyword. " + "Use a per-module override list, which can be counted and bounded." + ) + exempt = [ + m + for o in (mypy_cfg.get("overrides") or []) + if o.get("ignore_errors") + for m in (o.get("module") if isinstance(o.get("module"), list) else [o.get("module")]) + ] + if exempt: + floor = json.loads((HERE / ".verify-floor.json").read_text(encoding="utf-8")) + limit = floor.get("mypy_exempt_max") + assert limit is not None, ( + f"{len(exempt)} module(s) are exempt from mypy but `.verify-floor.json` records no " + "`mypy_exempt_max`. An exemption list nobody counts can only grow — that is an amnesty, " + "not a ratchet. Record the bound." + ) + assert len(exempt) <= int(limit), ( + f"{len(exempt)} modules are exempt from mypy but the agreed maximum is {limit}. The " + "list may only ever shrink: type a module and delete its line, or raise the ceiling " + "deliberately and say which module and why." ) -@pytest.mark.parametrize("name", ["pyproject.toml", "setup.cfg", "setup.py"]) -def test_no_packaging_file_appears_without_making_this_repo_installable(name): - """A packaging file here must mean a REAL installable package, not a config parking spot.""" +@pytest.mark.parametrize("name", ["setup.cfg", "setup.py"]) +def test_no_filename_triggered_packaging_file_appears(name): + """`setup.cfg` / `setup.py` still trigger the editable install BY FILENAME, so they stay out. + + `reusable-10-ci-python.yml` appends `-e '.[app,dev]'` to its install when it sees either of + these, with no metadata check — unlike pyproject.toml, which stranske/Workflows#3202 made + metadata-based. This repo has ~99 flat modules and no build backend, so that install fails and + both runtime jobs go red before a single test runs. + """ require_checkout() assert not (HERE / name).is_file(), ( f"{name} exists at the repo root. reusable-10-ci-python.yml appends `-e '.[app,dev]'` to " - "its install when it sees setup.cfg / setup.py, or a pyproject.toml that declares a " - "distribution ([project], [build-system] or [tool.poetry] — stranske/Workflows#3202 made " - "that gate metadata-based rather than filename-based). 129 flat root modules with no build " - "backend cannot be installed that way, so the tests job would fail for both runtimes. That " - "is why the Ruff and mypy configuration lives in ruff.toml and mypy.ini instead. Adding one " - "is fine, but it has to be a REAL installable package with `app` and `dev` extras. Then " - "delete this expectation.\n\n" - "Note: since #3202 a pyproject.toml carrying ONLY tool configuration no longer triggers the " - "install, so relaxing this for that case is a legitimate change — but make it deliberately, " - "and keep setup.cfg / setup.py forbidden, because those still do." + f"its install on the mere PRESENCE of {name} — no metadata check, unlike pyproject.toml. " + "With no build backend the tests job fails for both runtimes. Either make this a real " + "installable package with `app` and `dev` extras, or keep tool configuration in " + "pyproject.toml, which is metadata-gated." + ) + + +def test_pyproject_carries_tool_config_only_and_not_a_distribution(): + """THE DELIBERATE RELAXATION this file's previous expectation asked for, and its replacement. + + Until stranske/Workflows#3202 the Gate appended `-e '.[app,dev]'` on the mere existence of a + pyproject.toml, so this repo could not have one at all and the Ruff/mypy settings lived in + `ruff.toml` + `mypy.ini`. #3202 made that gate metadata-based via + `pyproject_declares_distribution()`, and the old expectation here explicitly invited relaxing + it for the tool-config-only case — "but make it deliberately". This is that change. + + What still has to hold, and why it is asserted rather than trusted: the moment this file grows + `[project]`, `[build-system]` or `[tool.poetry]`, the Gate WILL try to install the repo. That + is correct for a real package and fatal for ~99 flat modules with no backend, so the boundary + gets a test rather than a comment. Adding a genuine package is fine — declare the backend and + the `app`/`dev` extras, verify the install, and then change this test on purpose. + """ + require_checkout() + assert PYPROJECT.is_file(), "pyproject.toml is where the pytest, coverage and mypy config lives" + import tomllib + + with PYPROJECT.open("rb") as fh: + data = tomllib.load(fh) + declares = sorted(k for k in ("project", "build-system") if k in data) + if "poetry" in data.get("tool", {}): + declares.append("tool.poetry") + assert not declares, ( + f"pyproject.toml now declares {declares}, which makes `pyproject_declares_distribution()` " + "true upstream and adds `-e '.[app,dev]'` to the install on all five Python jobs. This " + "repo has ~99 flat modules under src/ and no build backend, so that install fails and the " + "jobs go red before any test runs. If a real package is intended, add the backend and the " + "app/dev extras, prove `pip install -e '.[app,dev]'` succeeds, and update this test." ) + # And the tool sections that had to move here must actually BE here — a pyproject.toml that + # exists but omits them silently overrides .coveragerc / mypy.ini with nothing. + for section in ("pytest", "coverage", "mypy"): + assert section in data.get("tool", {}), ( + f"[tool.{section}] is missing. CI passes --cov-config/--config-file pointing at THIS " + f"file whenever it exists, so an absent section is not a default — it is a silently " + f"dropped setting." + ) def test_every_cited_repo_path_resolves(): diff --git a/test_completion_events.py b/tests/test_completion_events.py similarity index 100% rename from test_completion_events.py rename to tests/test_completion_events.py diff --git a/test_consumer_sync_artifact_ingest.py b/tests/test_consumer_sync_artifact_ingest.py similarity index 100% rename from test_consumer_sync_artifact_ingest.py rename to tests/test_consumer_sync_artifact_ingest.py index 590fd75..976865c 100644 --- a/test_consumer_sync_artifact_ingest.py +++ b/tests/test_consumer_sync_artifact_ingest.py @@ -7,12 +7,12 @@ from pathlib import Path import pytest +from test_consumer_sync_shadow import stable_hash, valid_handoff, valid_plan import capabilities import capability_outcome_bridge import consumer_sync_artifact_ingest import consumer_sync_shadow -from test_consumer_sync_shadow import stable_hash, valid_handoff, valid_plan def make_zip(files_dict: dict[str, str | bytes]) -> bytes: diff --git a/test_consumer_sync_shadow.py b/tests/test_consumer_sync_shadow.py similarity index 100% rename from test_consumer_sync_shadow.py rename to tests/test_consumer_sync_shadow.py diff --git a/test_evidence_contract_compiler.py b/tests/test_evidence_contract_compiler.py similarity index 100% rename from test_evidence_contract_compiler.py rename to tests/test_evidence_contract_compiler.py diff --git a/test_experiment_arm_identity.py b/tests/test_experiment_arm_identity.py similarity index 100% rename from test_experiment_arm_identity.py rename to tests/test_experiment_arm_identity.py diff --git a/test_feedback_model_provenance.py b/tests/test_feedback_model_provenance.py similarity index 100% rename from test_feedback_model_provenance.py rename to tests/test_feedback_model_provenance.py diff --git a/test_improvement_log.py b/tests/test_improvement_log.py similarity index 89% rename from test_improvement_log.py rename to tests/test_improvement_log.py index a82d03c..28fa9fc 100644 --- a/test_improvement_log.py +++ b/tests/test_improvement_log.py @@ -22,15 +22,23 @@ from __future__ import annotations -import pathlib import re import subprocess import sys -HERE = pathlib.Path(__file__).resolve().parent +# Repo-root files, resolved through the shared rule rather than a local `parent.parent`: +# these tests live in `tests/` while the things they assert on live at the checkout root. +import paths + +HERE = paths.REPO_ROOT POINTER = HERE / "IMPROVEMENT_BACKLOG.md" CLAUDE_MD = HERE / "CLAUDE.md" +# TWO NAMES, because there are two questions. The docs cite the accessor the way a reader types it +# (`improvement_log.py ...`), while INVOKING it needs the real path — the modules moved under src/ +# and the tests no longer share their directory. Conflating them made the doc assertions look for a +# machine-specific absolute path inside CLAUDE.md. ACCESSOR = "improvement_log.py" +ACCESSOR_PATH = str(paths.MODULE_DIR / ACCESSOR) # A pointer is a paragraph and three commands. The real log is ~480 KB / 6,000 lines, so anything in # between is someone having started to use this file as the log. The gap between the two is three @@ -101,7 +109,7 @@ def test_accessor_reports_a_named_absence_to_a_caller(): missing = HERE / "no-such-dir-for-tests" / "IMPROVEMENT_BACKLOG.md" assert not missing.exists() proc = subprocess.run( - [sys.executable, str(HERE / ACCESSOR), "search", "anything"], + [sys.executable, ACCESSOR_PATH, "search", "anything"], capture_output=True, text=True, cwd=str(HERE), diff --git a/test_model_profile_trial.py b/tests/test_model_profile_trial.py similarity index 100% rename from test_model_profile_trial.py rename to tests/test_model_profile_trial.py diff --git a/test_model_profile_trial_bridge.py b/tests/test_model_profile_trial_bridge.py similarity index 100% rename from test_model_profile_trial_bridge.py rename to tests/test_model_profile_trial_bridge.py diff --git a/test_model_tier_resolution.py b/tests/test_model_tier_resolution.py similarity index 100% rename from test_model_tier_resolution.py rename to tests/test_model_tier_resolution.py diff --git a/test_observability_activation.py b/tests/test_observability_activation.py similarity index 100% rename from test_observability_activation.py rename to tests/test_observability_activation.py diff --git a/test_partitioned_review.py b/tests/test_partitioned_review.py similarity index 100% rename from test_partitioned_review.py rename to tests/test_partitioned_review.py diff --git a/test_pattern_miner.py b/tests/test_pattern_miner.py similarity index 100% rename from test_pattern_miner.py rename to tests/test_pattern_miner.py diff --git a/test_playbook_compiler.py b/tests/test_playbook_compiler.py similarity index 100% rename from test_playbook_compiler.py rename to tests/test_playbook_compiler.py diff --git a/test_repo_artifact_hygiene.py b/tests/test_repo_artifact_hygiene.py similarity index 97% rename from test_repo_artifact_hygiene.py rename to tests/test_repo_artifact_hygiene.py index 505fe35..e8f5db5 100644 --- a/test_repo_artifact_hygiene.py +++ b/tests/test_repo_artifact_hygiene.py @@ -48,13 +48,16 @@ from __future__ import annotations import subprocess -from pathlib import Path import pytest import env_prereq -HERE = Path(__file__).resolve().parent +# Repo-root files, resolved through the shared rule rather than a local `parent.parent`: +# these tests live in `tests/` while the things they assert on live at the checkout root. +import paths + +HERE = paths.REPO_ROOT # What the `langsmith-fleet/v1` emitters can leave in a working tree. The first entry is the file # that actually shipped on main; the rest are the sibling names the same producers already use diff --git a/test_research_control.py b/tests/test_research_control.py similarity index 100% rename from test_research_control.py rename to tests/test_research_control.py diff --git a/test_role_compiler.py b/tests/test_role_compiler.py similarity index 100% rename from test_role_compiler.py rename to tests/test_role_compiler.py diff --git a/test_roles_lineage.py b/tests/test_roles_lineage.py similarity index 100% rename from test_roles_lineage.py rename to tests/test_roles_lineage.py diff --git a/test_runner_effect_bridge.py b/tests/test_runner_effect_bridge.py similarity index 100% rename from test_runner_effect_bridge.py rename to tests/test_runner_effect_bridge.py diff --git a/test_runtime_ac_flow_monitor.py b/tests/test_runtime_ac_flow_monitor.py similarity index 100% rename from test_runtime_ac_flow_monitor.py rename to tests/test_runtime_ac_flow_monitor.py diff --git a/test_skill_compiler.py b/tests/test_skill_compiler.py similarity index 100% rename from test_skill_compiler.py rename to tests/test_skill_compiler.py diff --git a/test_synthesis_promotion.py b/tests/test_synthesis_promotion.py similarity index 100% rename from test_synthesis_promotion.py rename to tests/test_synthesis_promotion.py diff --git a/test_ux_review.py b/tests/test_ux_review.py similarity index 100% rename from test_ux_review.py rename to tests/test_ux_review.py diff --git a/test_verify_coverage_mode.py b/tests/test_verify_coverage_mode.py similarity index 75% rename from test_verify_coverage_mode.py rename to tests/test_verify_coverage_mode.py index 6ad08bf..5e71a27 100644 --- a/test_verify_coverage_mode.py +++ b/tests/test_verify_coverage_mode.py @@ -28,28 +28,35 @@ would enforce a threshold nobody agreed to, against a number that was wrong until this change — and would reward writing pytest wrappers around already-tested modules, which raises the metric and adds no assurance. -6. **`.coveragerc` keeps `parallel = true`.** Without it every child overwrites one data file and +6. **The coverage config keeps `parallel = true`.** It lives in `pyproject.toml` since that + file was added — CI passes `--cov-config=pyproject.toml` whenever it exists, so a `.coveragerc` + beside it would be read by nobody. Without it every child overwrites one data file and the combine is meaningless — the failure would look like a plausible-but-wrong number, which is worse than no number. -DELIBERATE BREAK -> REVERT, performed 2026-08-23: setting `parallel = false` in `.coveragerc` failed +DELIBERATE BREAK -> REVERT, performed 2026-08-23: setting `parallel = false` in the coverage config failed `test_coveragerc_enables_parallel_mode`; removing `child_argv` from the selftest runner failed `test_all_three_runners_are_instrumented[selftests]`. Both reverted byte-identical and passed again. """ from __future__ import annotations -import configparser import re -from pathlib import Path import pytest +# Repo-root files, resolved through the shared rule rather than a local `parent.parent`: +# these tests live in `tests/` while the things they assert on live at the checkout root. +import paths import verify -HERE = Path(__file__).resolve().parent -COVERAGERC = HERE / ".coveragerc" -VERIFY_SRC = (HERE / "verify.py").read_text(encoding="utf-8") +HERE = paths.REPO_ROOT +# The coverage settings moved into pyproject.toml when that file was added: CI passes +# `--cov-config=pyproject.toml` whenever it exists, so a `.coveragerc` beside it would be read by +# nobody. Asserting on the file that the tool ACTUALLY reads is the whole point of this check. +PYPROJECT = HERE / "pyproject.toml" +# verify.py is a module, so it lives with the modules — not at the checkout root. +VERIFY_SRC = (paths.MODULE_DIR / "verify.py").read_text(encoding="utf-8") def test_coverage_is_off_by_default(): @@ -100,8 +107,13 @@ def test_child_argv_inserts_coverage_after_the_interpreter(monkeypatch, argv, ex "runner,needle", [ ("pytest", 'cmd=child_argv([sys.executable,"-m","pytest"'), - ("selftests", 'child_argv([sys.executable,f"{mod}.py","--selftest"])'), - ("gates", "child_argv([sys.executable,*argv])"), + # The module is named by PATH, not by bare filename: `verify.py` runs with cwd at the + # CHECKOUT root while the modules may live under `src/`, so a bare `{mod}.py` would resolve + # against the wrong directory. The needle tracks the real call so instrumentation and + # layout cannot drift apart silently. + ("selftests", 'child_argv([sys.executable,str(MODULES/f"{mod}.py"),"--selftest"])'), + # Gates are resolved by path too — two of the five are test files, not modules. + ("gates", "child_argv([sys.executable,_gate_script(argv[0]),*argv[1:]])"), ], ) def test_all_three_runners_are_instrumented(runner, needle): @@ -112,22 +124,38 @@ def test_all_three_runners_are_instrumented(runner, needle): ) -def test_coveragerc_enables_parallel_mode(): - assert COVERAGERC.is_file(), ".coveragerc is missing; without it `parallel` defaults to off" - cfg = configparser.ConfigParser() - cfg.read(COVERAGERC) - assert cfg.getboolean("run", "parallel", fallback=False), ( +def test_coverage_config_enables_parallel_mode(): + """Asserted against the file the tool actually reads, which is now pyproject.toml. + + When `pyproject.toml` exists, CI passes `--cov-config=pyproject.toml` unconditionally, so a + `.coveragerc` left beside it is silently ignored — and `parallel` would quietly revert to its + default of OFF. That failure mode reports whichever subprocess finished last as if it were the + whole run: a plausible-but-wrong number, which is worse than no number. + """ + assert PYPROJECT.is_file(), "pyproject.toml is missing; coverage config has nowhere to live" + assert not (HERE / ".coveragerc").exists(), ( + ".coveragerc is back alongside pyproject.toml. CI reads only the latter, so the two would " + "disagree with nothing to say so — delete one." + ) + import tomllib + + with PYPROJECT.open("rb") as fh: + run_cfg = tomllib.load(fh).get("tool", {}).get("coverage", {}).get("run", {}) + assert run_cfg.get("parallel") is True, ( "`parallel = true` is load-bearing. Without it every instrumented child overwrites the same " "data file and the combine silently reports whichever process finished last — a " "plausible-but-wrong number, which is worse than no number." ) -def test_coveragerc_omits_the_tests_themselves(): - cfg = configparser.ConfigParser() - cfg.read(COVERAGERC) - omit = cfg.get("run", "omit", fallback="") - assert "test_*.py" in omit, ( +def test_coverage_config_omits_the_tests_themselves(): + import tomllib + + with PYPROJECT.open("rb") as fh: + omit = tomllib.load(fh)["tool"]["coverage"]["run"]["omit"] + # `tests/*` since the move — the pattern has to name where the tests actually are, and + # `test_*.py` would now match nothing at all while still looking like a rule. + assert any(pat.startswith("tests/") for pat in omit), ( "test files must be omitted from the measurement. Counting the tests as covered source " "inflates the number with the one thing guaranteed to be executed." ) @@ -194,7 +222,7 @@ def test_the_cli_help_actually_renders(): import sys proc = subprocess.run( - [sys.executable, "verify.py", "--help"], + [sys.executable, str(paths.MODULE_DIR / "verify.py"), "--help"], cwd=HERE, capture_output=True, text=True, diff --git a/test_workflow_compiler.py b/tests/test_workflow_compiler.py similarity index 100% rename from test_workflow_compiler.py rename to tests/test_workflow_compiler.py