refactor(layout): src/ + tests/ + pyproject.toml, and a decluttered root - #90
Conversation
Structure evaluation, step one of two. The repo root carried 15 tracked markdown files, of which
four are transient handoff briefs rather than standing documentation: `BRIEF_expand_range.md`,
`BRIEF_keepalive_transfers.md`, `BRIEF_process_improvement.md` and `CODEX_BRIEF.md`. They now live
in `docs/briefs/`, and their six inbound references are rewritten in the same change so no link
goes stale.
WHAT DELIBERATELY DID NOT MOVE, because the move would break a contract rather than tidy one:
* `README.md`, `CLAUDE.md`, `AGENTS.md` — root is where tooling and readers look for them.
* `ARCHITECTURE.md` — wired as a contract from `PLANNING.md` and `ORCHESTRATOR.md` (CLAUDE.md 5).
* `IMPROVEMENT_BACKLOG.md` — a tracked pointer `test_improvement_log.py` asserts on, and
`orch-sync-mirror.sh` copies it to the mirror BY ROOT PATH.
* `CLAUDE.md` for the same mirror-sync reason.
* `ORCHESTRATOR.md`, `PLANNING.md`, `ADDING_CAPABILITIES.md`, `EVAL_AND_TESTING.md`,
`FEEDBACK_LOOP.md`, `WORKFLOW_USER_GUIDE.md` — standing docs with live inbound references.
The stray runtime artifacts that used to sit at the root (`gh-rate-ledger.ndjson`, `__pycache__`,
`.coverage`) are already gitignored as of #71/#72, so there was nothing left to untrack.
Verified: 442 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…o-op, on purpose
PREREQUISITE for a `src/` layout, landed and verified BEFORE anything moves so the move itself
changes no behaviour here. On a flat tree the two roots are the same directory, so this commit is
provably inert; that is exactly why it goes first.
THE ACCIDENT THIS UNPICKS. Every module derived two different answers from one expression,
`Path(__file__).resolve().parent`, because the modules sat at the repo root and the two questions
happened to have the same answer:
* "where is my sibling MODULE?" -> `capabilities.py`, `dispatcher.py` -> MODULE_DIR
* "where is the CHECKOUT?" -> `orchestrate.sh`, `.verify-floor.json`,
`.coverage*`, the sibling fleet repos -> REPO_ROOT
Conflating them is what made a `src/` move look like a 126-file rewrite. Separated, it is this
commit plus a `git mv`.
`paths.py` is dependency-free (pathlib only) so any module can import it without closing a cycle —
`capabilities` imports `feedback`, and `feedback`'s selftest imports `env_prereq`, so putting this
in an existing module would have created a loop for somebody.
DETECTED, NOT HARDCODED, and that is the load-bearing part. `orch-sync-mirror.sh` copies modules
into the mirror launchd actually runs, and it may copy them FLAT or under `src/`. A hardcoded
`parent.parent` would be right in one tree and wrong in the other — precisely 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. So the layout is OBSERVED: module dir named
`src` => checkout is its parent; otherwise the two coincide. Both mirror shapes stay correct.
THE RULE IS A FUNCTION, not just a constant, and that mattered. The first attempt used module-level
constants and broke `capability_activation_audit`'s selftest, which patches its own `HERE` to build
synthetic trees — a constant ignored the tree the caller believed it was inspecting. `checkout_root(
module_dir)` / `fleet_root(module_dir)` apply the rule wherever asked; the constants are defined
through the same function so there is one implementation, not a matching pair that can drift.
Rewired: `capability_recurrence_check` (ORCHESTRATE, the `*.sh` glob — while its `*.py` glob moves
to MODULE_DIR), `capability_propensity` (driver), `capability_activation_audit` (`_driver_path` for
shell-vs-module drivers, `_repo_root`, `_fleet_roots`), `capability_admission` (`known_controls`
reads orchestrate.sh from the checkout and modules from the module dir; `_audits_dir` anchors to
FLEET_ROOT), and `verify.py` — the module that needed it most, since it DISCOVERS modules beside
itself while READING repo files and running pytest from the checkout.
`verify.py` now also invokes each selftest BY PATH (`MODULES / f"{mod}.py"`) rather than by bare
filename, because its cwd is the checkout and a bare name would resolve against the wrong
directory once the modules move. `test_verify_coverage_mode`'s instrumentation needle tracks the
real call, so instrumentation and layout cannot drift apart silently — deliberate break -> revert:
removing `child_argv` from that runner still fails the test, reverted byte-identical.
`verify.py`'s own selftest now patches BOTH `HERE` and `MODULES` and asserts the two roots stay
distinguished. On a flat tree they are equal, which is why the assertion is needed: without it,
code that re-merged them would pass here and only fail after the layout moved.
Verified: 442 passed, 0 failed, 0 skipped, 85/85 selftests, 43/43 can-fire, 5/5 gates. Floor
unchanged at 442 — no test added or removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nd add pyproject.toml The reorganisation the previous commit made cheap. 99 modules -> `src/`, 33 test files -> `tests/`. The repo root goes from 131 tracked `.py` files to zero. NO IMPORT REWRITES, and that is why this is a `git mv` rather than a 126-file edit. `python3 src/tick.py` puts `src/` on `sys.path` itself, so every `import capabilities` keeps resolving unchanged; pytest gets the same via `pythonpath = ["src"]`. Both mechanics were proved on a scratch tree before a single file moved. WHY A FLAT `src/` AND NOT `src/orchestrator/`. A package would rewrite 115 import statements AND break the capability system's static analysis, which is built end-to-end on basename resolution from a known root — `DRIVER_MODULES`, `_callers_of`, `heartbeat_reachable`, `emits_heartbeat`, `shell_heartbeat_gate`, plus 31 modules that shell out to a sibling by filename. A flat `src/` gives the two things actually wanted — mypy scoping and a clean root — at a fraction of the risk. Namespace de-collision would need the package; it is a separate decision. pyproject.toml carries TOOL CONFIG ONLY, deliberately: no `[project]`, no `[build-system]`. It can exist at all only because stranske/Workflows#3202 made the editable-install gate metadata-based instead of filename-based. `.coveragerc` and `mypy.ini` are DELETED into it — not tidying: CI passes `--cov-config=pyproject.toml` and `--config-file pyproject.toml` whenever the file exists, so leaving them beside it would have silently dropped `parallel = true` (reporting whichever subprocess finished last) and `explicit_package_bases` (aborting mypy on a setup error). `test_ci_gate_config` now asserts the boundary that matters — the moment this file declares a distribution, five jobs start attempting `-e '.[app,dev]'` — replacing the old blanket ban, which its own docstring invited relaxing "deliberately". MEASURED WIN: mypy drops 608 -> 467 errors, purely from scoping. The Gate does `target="src"; [ -d "$target" ] || target="."`, so it now checks the 99 modules instead of the whole tree. `scripts/ci_lint_baseline.py` was measuring `.` and would have recorded 613 against the Gate's 467 — a baseline that does not match the command it claims to record reads as measured, so it now runs the Gate's exact command. The toggle comment carries the new number and says why. THE TAIL, each found by the suite rather than reasoned about, and each a repo-root path derived from the module directory: * `verify.py` — gates live in TWO directories now (three modules, two test files), so each is resolved by where its file actually is; and the two test-file gates needed `src` on PYTHONPATH, because `python3 tests/x.py` puts `tests/` on `sys.path`, not `src/`. Both reported `ModuleNotFoundError` instead of a verdict. * `capability_activation_audit._callers_of` — `HERE / driver` silently found NOTHING for orchestrate.sh, reporting two LIVE capabilities as having no caller. `heartbeat_env_gate` the same. * `env_prereq.repo_files_absent` — looked for `.github/`, `docs/`, `scripts/` beside the modules, so 12 real assertions became skips. Skips are ceiling-bounded here, which is what surfaced it. * `capability_admission` — reaches into the test suite for the recurrence-fixture roster; that directory is now added to `sys.path` EXPLICITLY rather than by accident. * `test_improvement_log` — conflated the accessor's DOC spelling with its invocation path, so the doc assertions started hunting for an absolute path inside CLAUDE.md. `orchestrate.sh` needed one line: `$ORCH` points at the modules, detected (`$ORCH_REPO/src`, falling back when absent) because THE MIRROR IS FLAT. All 47 of its `$ORCH/...` uses are modules, so nothing else changed. 133 doc invocations gained their `src/` prefix, matched against the real module list so no generic `.py` word was rewritten. EXTERNAL DEPENDENCY, NOT LANDED HERE: `~/.codex/bin/orch-sync-mirror.sh` copies `"$SRC"/*.py` flat and now matches nothing, so the mirror launchd runs would have no modules. It is outside the repo and owner-owned; the patch, the reason the mirror stays flat, and how to confirm it are in `docs/MIRROR_SYNC_PATCH.md`, and CLAUDE.md §1 now points at it. Verified: 442 passed, 0 failed, 0 skipped, 85/85 selftests, 43/43 can-fire, 5/5 gates. Floor unchanged at 442 — nothing added or removed. `ruff check .` and `black --check --line-length 100` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 154 files, which is 4 over the limit of 150. To get a review, reduce the PR to 150 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to Pro+ to raise the limit. Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (154)
You can disable this status message by setting the Comment |
Workflow source neededPR #90 needs either a linked GitHub issue or one valid non-issue Workflow Source before PR metadata automation can manage it safely. Please do one of:
Once a valid source is present, this warning will not be reposted. |
…t a ratchet
ANSWER TO "has mypy addressed all the typing issues": NO, and it could not have been. There are 455
real findings across 64 modules — `arg-type` 109, `index` 104, `assignment` 69, `union-attr` 43 —
each a distinct annotation or narrowing decision in a distinct module, with no `mypy --fix` in
existence. That is what the toggle's old `drainable: 0 per PR` recorded, and it is why the check had
been OFF: an all-or-nothing gate over code nobody can fix in one change is a gate whose clear path
is blocked by the thing it measures, so it stayed shut and checked NOTHING.
So this changes the shape of the problem rather than pretending to finish it. The check is now ON.
WHAT MADE IT OPENABLE:
* the src/ move scoped the Gate's `target="src"` to the 99 modules — 608 -> 467 errors;
* `[[tool.mypy.overrides]]` exempts the modules that still have findings BY NAME, so the 35
already-clean ones are checked TODAY instead of nothing being checked at all.
`mypy --config-file pyproject.toml --exclude .workflows-lib src` reports "no issues found in 99
source files", and new untyped code in a clean module is now a RED.
SCOPE IS NOT SILENCE, and the distinction is enforced, not asserted in prose.
`test_mypy_config_silences_nothing_by_error_code` still forbids `disable_error_code` and
`follow_imports` — those blind the check to a CLASS of error everywhere, with nothing to count and
no mechanism that removes them; fifteen codes would have covered 603 of 608. A per-module list is
different in kind: every finding stays discoverable via `scripts/ci_lint_baseline.py`, and deleting
a name restores that module's errors instantly (verified: removing `dispatcher` surfaced its 65).
A top-level `ignore_errors` is forbidden too, because it exempts everything in one keyword.
THE RATCHET, with the mandatory latched-gate questions answered in pyproject.toml itself:
1. What decrements it? Typing a module and deleting its line. Not "time passes".
2. Can that run while the gate is CLOSED? Yes — green never blocks typing work.
3. Same measuring and draining window? Yes: ONE list, counted by verify.py, drained by editing
those same lines.
`.verify-floor.json`'s `mypy_exempt_max` (64) FAILS if the list grows, so a module can never
quietly rejoin the exempt set, and `verify.py` prints `64/64 max of 99 module(s) exempt, 35 checked`
on EVERY run — "mypy on" can never come to mean "mypy checks nothing".
AND IT IS DRAINED, not just mechanised: 12 `var-annotated` findings fixed with real annotations
(467 -> 455, 66 -> 64 modules). Purely additive, no runtime behaviour touched, every module
re-imported. Five more were left alone deliberately — they are tuple-unpacking or loop targets where
the annotation is not a one-line edit, and guessing at those is how a "typing" commit becomes a
behaviour change.
Two tests needed real changes rather than relaxation:
* `test_every_disabled_toggle_states_blocking_and_drainable` asserted that SOME toggle is forced
off; none is now. Generalised rather than deleted: a toggle that is ON but BOUNDED owes the same
blocking/drainable/drains-by annotation, because "on over a scoped subset" hides as much as
"off" if the scope is unstated. It still refuses to pass vacuously.
* `test_mypy_config_silences_nothing` substring-matched the config text and tripped on a COMMENT
explaining why `disable_error_code` was rejected. It now parses the TOML — a check that fails on
prose about itself teaches people to weaken it.
Deliberate break -> revert, each caught then reverted byte-identically: adding
`disable_error_code`; adding a module to the exempt list without raising the ceiling; deleting the
ceiling while the list exists; removing `dispatcher` from the list (65 errors returned).
Verified: 442 passed, 0 failed, 0 skipped, 85/85 selftests, 43/43 can-fire, 5/5 gates.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…bound assertion (#92) Two regressions from #90, both found by running verify FROM THE MIRROR rather than reasoning about it. The tests stopped travelling: one cp glob used to carry modules and tests together when both were root-level .py. A mirror run collected zero tests and reported 'floor 442 NOT MET', which reads as a broken change rather than a broken sync. pyproject.toml now carries pythonpath = ['src', '.'] so one list is right in both the src/ checkout and the flat mirror; the sync script (outside the repo) copies tests/ and fails loudly on either half being empty, and removes the .coveragerc that pyproject.toml replaced. And an assertion added in #90 was true only under src/ — it claimed a module name is absent from the checkout root, which is false in the flat mirror where those are the same directory. Removed rather than made conditional: a test that holds in one layout makes the other look broken. Verified in both trees: repo 442 passed / 0 skipped; mirror 419 passed / 23 skipped of 442 collected, floor met, 5/5 gates in each.
THREE THINGS, and the third is why this is one PR rather than three. 1. --floor-may-lag, on PULL REQUESTS ONLY. .verify-floor.json's collected is an EQUALITY, which is right -- a floor behind reality is permissive by exactly the gap. But the correct number is only knowable on the MERGE RESULT: one one-line change needed SIX re-measurements in two hours while main moved eight times, and that file's own note records the floor being found behind reality four separate times before that. On a PR a LAGGING floor is now reported and not fatal; a collection DROP stays fatal. On main the equality is enforced by the same job WITHOUT the flag, and the reconcile job heals a lag within one run, so main's invariant is unchanged. The lag still PRINTS -- forgiven is not hidden. This is the third consumer of _blocks_floor_update, which is what makes it safe: one predicate distinguishes "you added tests" from "tests vanished", so the flag structurally cannot widen into the dangerous direction. 2. The wiring-pin convention, in CLAUDE.md. Three source-literal pins fired on entirely correct changes on 2026-08-23/24 -- the pin file's baseline citation, test_verify_coverage_mode's verify() call, and verify.py's own --update-floor guard -- because each pinned a WHOLE LINE. A test that calls a reformat a regression gets waived, and a waived test protects nothing. The rule: pin the smallest fragment that would be ABSENT if the wiring were removed; nothing a formatter owns; prefer a behavioural assertion where one is cheap. All three instances are already fragment-form; this stops the regression. 3. Six F821 "Undefined name Any" on main, from #90. Not a runtime break -- from __future__ import annotations means those annotations never evaluate, and all six modules import cleanly -- but they fail lint-ruff on every future PR, including this one. WHY THEY GOT IN, because the mechanism matters more than the fix: the Gate runs on pull_request only, #90's Gate run was held at action_required with 0 jobs (the suspicious-workflow review, because #90 edits workflow files), and pushes to main do not trigger it. So #90 merged with ZERO lint/format/typecheck verification. That is this repo's founding failure -- merging past a gate that never ran -- recurring through a different mechanism than the missing pin file that started it. Verified on the merge result after rebasing through #90's 82 renames: 442 passed, 0 failed, 0 skipped, floor 442, 85/85 selftests, 5 of 5 gates, mypy Success (99 files, 35 checked / 64 exempt), ruff clean, black clean. Break -> revert: widening --floor-may-lag to `return []` fails the collection-drop assertion; reverted byte-identical. Co-authored-by: Tim Stranske <tim@stranskemo.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
THE DEFECT. main has no branch protection, and `gh pr checks` lists what DID report -- so a check that never started is not red, it is missing, and a PR with no Gate reads exactly like a PR whose Gate passed. Silence indistinguishable from success: this repo's founding defect, twice over. 2026-08-23, five python-ci jobs died at a shared install step and #61/#64/#65 merged with all five red. 2026-08-24, #90's Gate run was held at `action_required` with ZERO jobs and merged with no lint, no format and no typecheck, landing six F821s found only because somebody ran ruff by hand. THE CAUSE WILL BE DIFFERENT NEXT TIME, so nothing here models holds. A check can vanish to a hold, a cancellation, a deleted or renamed workflow, a rate limit, a mistaken path filter or a GitHub incident. All present identically to whoever is merging. scripts/check_checks_reported.py asks only: did every check that NORMALLY reports also report here. TWO SIMPLER DESIGNS WERE TRIED AND REJECTED BY REAL DATA, both recorded in the file because the next person will reach for them: * one reference PR -- failed on the actual incident. #90 had no Gate, and the newest merged PR (#93) had no Gate checks either, so #90 was declared healthy. The hold had already swallowed the yardstick. * the union across recent merges -- caught #90 (21 absent) but reported 25-26 absences on entirely healthy PRs, sweeping in event-driven checks. A test that cries wolf 25 times gets waived. Frequency (>=75% of 12 merged PRs) discriminates: #90 exit 1 with 10 absent, be a second copy of the CI topology. NOT BRANCH PROTECTION, deliberately. A required check that is HELD never reports, so the PR could never merge -- the clear path blocked by the very thing the gate measures. On a solo-maintained repo "unverified but movable" beats "permanently stuck". pr-00-gate.yml also gains `push: [main]`. A held PR run cannot be fixed from inside CI, but the silence AFTER the merge can: #90's F821s would have gone red on main within one run instead of never. Fail toward noise. docs/ABSENT_CHECK_LANE_WIRING.md carries the one step this repo cannot land -- the closer's pre-merge call and the prerun `--sweep` line, since the lane TOMLs live outside any repository. Same shape as docs/MIRROR_SYNC_PATCH.md. Already found a live one: --sweep flags open PR #91 with 10 absent checks, which I would have merged on a green-looking list. Verified: 446 passed, 0 failed, 0 skipped, floor 446, 85/85 selftests, 5 of 5 gates, mypy Success, ruff and black clean. Break -> revert: removing the max(2, ...) threshold floor fails test_the_threshold_never_falls_to_one; reverted byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(ci): an absent check is a red, whatever made it absent THE DEFECT. main has no branch protection, and `gh pr checks` lists what DID report -- so a check that never started is not red, it is missing, and a PR with no Gate reads exactly like a PR whose Gate passed. Silence indistinguishable from success: this repo's founding defect, twice over. 2026-08-23, five python-ci jobs died at a shared install step and #61/#64/#65 merged with all five red. 2026-08-24, #90's Gate run was held at `action_required` with ZERO jobs and merged with no lint, no format and no typecheck, landing six F821s found only because somebody ran ruff by hand. THE CAUSE WILL BE DIFFERENT NEXT TIME, so nothing here models holds. A check can vanish to a hold, a cancellation, a deleted or renamed workflow, a rate limit, a mistaken path filter or a GitHub incident. All present identically to whoever is merging. scripts/check_checks_reported.py asks only: did every check that NORMALLY reports also report here. TWO SIMPLER DESIGNS WERE TRIED AND REJECTED BY REAL DATA, both recorded in the file because the next person will reach for them: * one reference PR -- failed on the actual incident. #90 had no Gate, and the newest merged PR (#93) had no Gate checks either, so #90 was declared healthy. The hold had already swallowed the yardstick. * the union across recent merges -- caught #90 (21 absent) but reported 25-26 absences on entirely healthy PRs, sweeping in event-driven checks. A test that cries wolf 25 times gets waived. Frequency (>=75% of 12 merged PRs) discriminates: #90 exit 1 with 10 absent, be a second copy of the CI topology. NOT BRANCH PROTECTION, deliberately. A required check that is HELD never reports, so the PR could never merge -- the clear path blocked by the very thing the gate measures. On a solo-maintained repo "unverified but movable" beats "permanently stuck". pr-00-gate.yml also gains `push: [main]`. A held PR run cannot be fixed from inside CI, but the silence AFTER the merge can: #90's F821s would have gone red on main within one run instead of never. Fail toward noise. docs/ABSENT_CHECK_LANE_WIRING.md carries the one step this repo cannot land -- the closer's pre-merge call and the prerun `--sweep` line, since the lane TOMLs live outside any repository. Same shape as docs/MIRROR_SYNC_PATCH.md. Already found a live one: --sweep flags open PR #91 with 10 absent checks, which I would have merged on a green-looking list. Verified: 446 passed, 0 failed, 0 skipped, floor 446, 85/85 selftests, 5 of 5 gates, mypy Success, ruff and black clean. Break -> revert: removing the max(2, ...) threshold floor fails test_the_threshold_never_falls_to_one; reverted byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): degrade the absent-check sweep gracefully on a rate limit The sweep runs every lane round from handoff-prerun.sh. Dumping GitHub's full rate-limit paragraph hourly would train the reader to skip the whole section, which is how a health report stops being read. One line instead, and it says UNKNOWN rather than implying clean -- a reporter that cannot report must not read as a clean bill of health. Found by testing the prerun block while genuinely rate-limited, which is the degraded path I would otherwise have had to simulate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): ratchet the expected-check set so a sustained outage cannot erode it FOUND BY DOGFOODING, not by review. Running --pr 91 before merging it, the check PASSED -- and should not have. While pr-00-gate.yml sat held, every newly merged PR merged WITHOUT the Gate, so after twelve such merges the Gate's checks no longer appeared on 75% of the reference window, stopped counting as "normally reporting", and their absence stopped being flagged. The expected set fell 23 -> 14 names and #91 was pronounced clean by the tool written to catch exactly that. A SUSTAINED outage is the case that matters most, and it was the one case the frequency rule could not see. The erosion test already in this file covered only PRs that reported NOTHING; a PR reporting some checks but not the Gate's slid straight through. config/expected-checks.json is now the high-water mark, seeded from PRs #87/#89 whose Gate demonstrably ran (33 names, 8 of them python-ci). A name that has ever been expected stays expected until somebody DELETES ITS LINE -- a visible act in a diff. Same shape as pyproject.toml's mypy exempt ratchet, and the same reason: an automatic downward move is indistinguishable from the defect. --update-ratchet raises it and never lowers it. With the ratchet, PR 91's head correctly reports 19 absences including the whole python-ci set. PR 89 (Gate ran) reports one, "guard", which is a TRUE positive: agents-guard.yml was already held by then. Break -> revert: returning the observed set instead of its union with ratchet_names() fails test_the_ratchet_is_wired_into_the_expected_set; byte-identical after. 448 passed, floor 448, 85/85 selftests, 5 of 5 gates, mypy Success, ruff and black clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Tim Stranske <tim@stranskemo.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…wanted, plus .json (#110) Maint Coverage Guard failed the moment it was unheld, on both runs (#3 scheduled, #4 manual), with: error: Coverage verification could not find required coverage artifacts on a successful Gate run. warn: Unable to locate a recent successful Gate workflow run with required coverage artifacts: gate-coverage-trend, gate-coverage-trend-history, gate-coverage. maint-coverage-guard.yml downloads exactly three artifact NAMES. The Gate produced the first two under those names and the third as "gate-coverage.json", so the guard probed ten successful runs, found two of three every time, and reported no usable run at all. Measured rather than inferred: Gate run #241 (main, success) uploaded gate-coverage-summary.md, gate-coverage.json, gate-summary.md, gate-coverage-3.13-1, gate-coverage-trend-history, gate-coverage-trend, gate-coverage-summary, gate-coverage-3.12-1 -- trend and trend-history present, "gate-coverage" absent. An artifact NAME and the FILE inside it are different things, and this step conflated them. Renaming the artifact is safe and was checked, not assumed: "gate-coverage.json" as an artifact name appears nowhere else in this repo, and the only download-artifact steps in any workflow here are the guard's three. The file inside is still gate-coverage.json, so anything reading contents is unaffected. A paired literal in two files that disagreed -- this repo's signature defect -- and it stayed invisible for as long as the guard was held. That is the second thing the holds were concealing, after PR #90's six F821s. pr-00-gate.yml is create_only in the Workflows sync manifest with Orchestrator absent from overwrite_repos, so this local fix persists. The same mismatch likely affects every consumer whose Gate emits coverage, which is worth raising upstream separately. 448 collected, ruff and black clean, test_ci_gate_config 12 passed. Co-authored-by: Tim Stranske <tim@stranskemo.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sets up the
src/layout, addspyproject.toml, and declutters the repo root. Three commits, in dependency order — read them separately, the middle one is what makes the third cheap.462d8e4docs/briefs/98eaf02c573065git mv99 modules →src/, 33 tests →tests/, addpyproject.tomlThe structure evaluation you asked for
Tracked root before: 131
.py(98 modules + 33 tests), 15.md, plus config. After: zero.py..pyfilessrc/andtests/docs/briefs/, 6 inbound refs rewrittengh-rate-ledger.ndjson,__pycache__,.coverage).coveragerc+mypy.inialongside a newpyproject.tomlDeliberately not moved, because moving would break a contract rather than tidy one:
README.md,CLAUDE.md,AGENTS.md,ARCHITECTURE.md(a contract fromPLANNING.md/ORCHESTRATOR.md), andIMPROVEMENT_BACKLOG.md+CLAUDE.md(whichorch-sync-mirror.shcopies by root path).Why this was a
git mvand not a 126-file rewriteThe middle commit is the whole trick. Every path in the tree came from
Path(__file__).resolve().parent, because two different questions had the same answer while the modules sat at the root:MODULE_DIRorchestrate.sh,.verify-floor.json, the fleet repos) →REPO_ROOTSeparated in
src/paths.py, the move became a rename. And no imports were rewritten:python3 src/tick.pyputssrc/onsys.pathitself, so everyimport capabilitiesstill resolves; pytest gets the same frompythonpath = ["src"]. Both mechanics were proved on a scratch tree before anything moved.The rule is detected, not hardcoded — module dir named
src⇒ checkout is its parent, else they coincide — because the exec mirror is flat. A hardcodedparent.parentwould be right in one tree and wrong in the other, which is precisely the failure_fleet_rootsalready documents (37 of 37 in the checkout, 36 of 37 in the mirror).orchestrate.shdoes the same detection in shell, which is why all 47 of its$ORCH/...sites needed one line changed.Flat
src/, notsrc/orchestrator/A package would rewrite 115 imports and break the capability system's static analysis, which is built end-to-end on basename resolution from a known root —
DRIVER_MODULES,_callers_of,heartbeat_reachable,emits_heartbeat, plus 31 modules that shell out to a sibling by filename. Flatsrc/buys the two things actually wanted (mypy scoping, clean root) at a fraction of the risk. Namespace de-collision needs the package and is a separate decision.pyproject.toml: tool config only, and the deletions were mandatoryNo
[project], no[build-system]. It can exist at all only because stranske/Workflows#3202 made the editable-install gate metadata-based instead of filename-based..coveragercandmypy.inihad to be absorbed, not kept: CI passes--cov-config=pyproject.tomland--config-file pyproject.tomlwhenever the file exists, so leaving them beside it would have silently droppedparallel = true(reporting whichever subprocess finished last — the "plausible-but-wrong number" its own comment warns about) andexplicit_package_bases(aborting mypy on a setup error).test_ci_gate_confignow asserts the boundary that matters — the moment this file declares a distribution, five jobs start attempting-e '.[app,dev]'— replacing the old blanket ban, whose docstring explicitly invited relaxing it "deliberately".Measured win
mypy 608 → 467 errors, purely from scoping. The Gate does
target="src"; [ -d "$target" ] || target=".", so it now checks the 99 modules instead of the whole tree. This is the peer-parity the earlier evaluation saidsrc/alone would only buy 9% of — the scoping half is real, the other 489 still need typing.scripts/ci_lint_baseline.pywas measuring.and would have recorded 613 against the Gate's 467, 30 of the gap beingimport-not-foundartefacts. A baseline that doesn't match the command it claims to record reads as measured, so it now runs the Gate's exact command. The toggle comment carries the new number and says why it moved.The tail — every item found by the suite, not by reasoning
Each was a repo-root path derived from the module directory:
verify.py— gates now live in two directories (3 modules, 2 test files), so each resolves by where its file is; and the test-file gates neededsrconPYTHONPATH, sincepython3 tests/x.pyputstests/onsys.path. Both had been reportingModuleNotFoundErrorinstead of a verdict._callers_of/heartbeat_env_gate—HERE / driversilently found nothing fororchestrate.sh, reporting two live capabilities as having no caller.env_prereq.repo_files_absent— looked for.github/,docs/,scripts/beside the modules, turning 12 real assertions into skips. Caught because skips are ceiling-bounded here.capability_admission— reaches into the test suite for the fixture roster; that directory is now onsys.pathexplicitly rather than by accident.test_improvement_log— conflated the accessor's doc spelling with its invocation path, so doc assertions started hunting an absolute path insideCLAUDE.md.~/.codex/bin/orch-sync-mirror.shcopies"$SRC"/*.pyflat — after this move that glob matches nothing, and the mirror launchd runs hourly would have no modules. That file is outside the repo and yours. The patch, why the mirror stays flat, and how to confirm it are indocs/MIRROR_SYNC_PATCH.md;CLAUDE.md§1 now points at it. It also needs its.coveragerccopy changed topyproject.toml.Do not sync the mirror until that patch is applied. The manual sync gap is the natural gate.
Test gate
python3 src/verify.py— 442 passed, 0 failed, 0 skipped, 85/85 selftests, 43/43 can-fire, 5/5 gates. Floor unchanged at 442: nothing added or removed.ruff check .andblack --check --line-length 100clean.Note the command itself moved —
python3 src/verify.py— and all 133 doc invocations gained the prefix, matched against the real module list so no generic word ending in.pywas rewritten.🤖 Generated with Claude Code