release: promote develop → main for v0.37.1 (STX-TQ false-green fix)#437
Merged
Conversation
…-behind + suppress hint) (#389) * rescue: pre-stop autosave scitex-dev@20260720T164109Z * fix(cli): fire version-staleness guard on every invocation (warn-when-behind + suppress hint) The staleness check existed but was wired to NO callsite — dead code. This makes it actually fire, matching the operator's spec. - Wire emit_if_drift() into the root `main` group callback: every scitex-dev CLI invocation now checks whether the install is BEHIND its remote (editable HEAD behind origin/<branch>) or the latest published wheel (local version cache), and warns — or hard-fails when the `staleness_severity` knob is `error` — with a copy-paste-safe `git -C <abs-path> pull --ff-only` / `pip install -U` remedy. Being AHEAD of a tag/remote is never flagged (the original false positive). Emitted via scitex-logging (WARN:/ERRO: prefix). - Append a suppression hint to every emitted line (operator request): SCITEX_DEV_NO_DRIFT_WARN=1 kill-switch + the staleness_severity knob. - Extract the recursive help-rendering helpers (_command_to_dict, _show_recursive_help) to _cli/_root_help.py to keep _root.py under the 512-line limit; re-exported from _root for API/import compatibility. - Fail-safe: the error-severity abort (SystemExit) propagates on purpose; any other internal fault is swallowed so the guard can never break the host CLI. Placed after --version / --help-recursive so a version/help query never trips it; shell-completion + repeat invocations are guarded inside. --------- Co-authored-by: Yusuke Watanabe <ywata1989@gmail.com>
… (PS-220) (#390) New project-audit check PS-220: AST-scans the shippable src/<pkg>/**.py tree for builtin print(...) calls and flags each one. SciTeX code must emit messages through scitex-logging (aligned WARN:/ERRO:/SUCC: prefixes), never print. - _check_no_print.py: check_ps220_no_print + co-located PRINT_FORBIDDEN_RULES (mirrors the _check_no_url_deps PS-216 contract). AST walk of ast.Call whose func is the bare name 'print'; attribute forms (x.print) not matched. - Scope: src/ only; tests/scripts/examples/docs subtrees excluded. Inline '# noqa' opts a line out. - Severity W during ecosystem adoption (promotes to E once compliant). - Registered in _registry.py; wired into _audit.py after PS-216. - 7 tests mirroring test__check_no_url_deps (fires / clean / noqa / attr / multi-count / excluded-subtrees). Full run: 147 passed.
…backends (#388) Faithfully distills the phase-1 proposal from the central-compute dispatcher vision memo (operator TG1503-1525). Policy layer over EXISTING executors (Snakemake/Parsl/Slurm), not a new executor: scitex-dev = policy+auditor, scitex-hpc = backend adapters. Locks 4 interfaces now (container contract, resource-spec, ExecutionBackend submit/status/logs/cancel/collect, receipt schema); ships local + slurm backends only; defers cloud/routing/sharding/cost/ data-governance behind the interfaces. Includes the -n auto -> allocated-cpus (SLURM_CPUS_PER_TASK) fix and the scitex-hpc coordination ask.
…s (anti wrong-tree footgun) (#392) audit-project resolves the tree it grades from several sources (explicit --path, the CWD repo-root, an editable install, or a ~/proj/<pkg> guess). When resolution lands on a tree that is NOT the commit under test, a green "no violations" is a confident lie about source it never read — and a clean result is exactly the one nobody double-checks. Before running any checks, announce the ABSOLUTE resolved path plus the branch and short HEAD sha of the tree about to be graded: human rail — a scitex-logging INFO line "<dist>: auditing <abs-path> (branch <b>, HEAD <sha>)" --json rail — new resolved_path / branch / head fields in the payload (present on the located AND the cannot-locate payloads). Resolution is fail-safe: a non-git tree (sdist extract, tarball) still surfaces its absolute path; branch/head fall back to None ("?" in the human line). The resolver never raises and never blocks the audit. New helper module _resolved_tree.py keeps _audit.py within the line budget; 12 tests cover the pure resolver (None repo, non-git, real checkout), both --json fields, and the human banner.
Root cause: STX-FM001 is a *plugin-provided* rule object. FMChecker._add returns silently when `rules.FM001 is None` — which happens whenever the linter interpreter's figrecipe provides no FM001 rule (absent, or a version whose plugin predates/renames it). `@requires_figrecipe` only proves figrecipe is IMPORTABLE, not that it registers FM001, so on the shared self-hosted runner (figrecipe importable but rule-skewed) FM001 never fires and these 6 tests fail with `_sev_of(...) == None`. This is the exact figrecipe detection-skew the file already tolerates for its plugin-path siblings via `_skip_if_not_emitted` (added after the v0.23.0 release-blocking incident, #277/#25). The FM001 tests simply never got that guard. Apply it to all 6 FM001-presence-requiring tests. A real regression is still caught: if FM001 FIRES but at the wrong severity, the rule is present so `_skip_if_not_emitted` does not skip and the severity assert fails as before. Only genuine "rule absent" skew now skips instead of hard-failing. Unblocks the pytest-matrix failures on #391 and #393; de-flakes develop.
…order tests (supersedes #391, #393) (#395) * fix(audit): consolidate PS-221 [all]-closure rule + de-flake 4 xdist-order tests (supersedes #391, #393) Brings the UNION of PRs #391 and #393 onto one branch, plus root-cause fixes for the 5 deterministic matrix failures they shared. Consolidated from the two PRs: - #391: the PS-221 "[all]-closure on public extras" audit rule (_check_extras_all_closure.py + _audit.py/_registry.py wiring + test). - #393: `scitex-dev[sync]` added to `[all]` in pyproject.toml, and the drift-emission guard in _cli/_root.py that suppresses the boot-path staleness line under PYTEST_CURRENT_TEST (it fired on every CLI invocation and polluted unrelated tests' captured output). Root-cause fixes for the 5 CI failures (identical across py3.11/12/13): 1. test_audit.py::test_audit_all_clean - the new PS-221 rule flagged scitex-dev's OWN pyproject because the `sync` extra (scitex-ssh, scitex-scholar) was missing from `[all]`. Fixed by adding `scitex-dev[sync]` to `[all]` (verified: PS-221 now reports 0 violations on this tree), which both validates the rule and makes the package self-compliant. 2-3. test__resolved_tree.py::test_human_output_{surfaces_resolved_path, banner_says_auditing} - the INFO banner is emitted through scitex_logging.getLogger("scitex_dev.audit"), whose StreamHandler binds the REAL sys.stderr at import time; contextlib.redirect_stderr (swapped afterwards) captured nothing whenever another test imported scitex-logging first (xdist-order flake). Made the test hermetic: _human_output now attaches its own handler to that exact logger, so the banner lands in the buffer deterministically regardless of global handler state. Real assertion (path in banner) kept. 4-5. test_check_editable_drift.py::test_{warn,error}_output_carries_ severity_prefix - same root cause on the "scitex_dev" logger via _log_stale. Replaced redirect_stderr with a dedicated _capture_drift_log handler on that logger (levelname carries the WARN/ERRO prefix) and pass the buffer as the plain fallback stream. Real assertion (WARN:/ERRO: prefix present) kept; prod code unchanged. Control: PR #393's matrix fails exactly on 2-5 (env-common) and NOT on test_audit_all_clean (which is #391-rule-specific), confirming these fixes cover both PRs. * fix(test): pin audit-all to the checkout under test via --path (test_audit_all_clean) The consolidated PS-221 rule flagged violations in CI even after fixing this repo's own `[all]` closure, because `test_audit_all_clean` called `audit_all_for_package('scitex-dev')` WITHOUT a `path=`. Without it, audit-all falls back to a resolve-by-name `~/proj/<pkg>` development guess; on the shared self-hosted (Spartan pooled) runner that guess resolves the operator's `/home/ywatanabe/proj/scitex-dev` develop checkout (HEAD cc599c9) — a DIFFERENT tree and commit than the one under test. So the gate graded stale source whose pyproject still lacked the `scitex-dev[sync]`-in-`[all]` fix, and reported PS-221 §3 for scitex-ssh / scitex-scholar. PR #392's resolved-tree banner surfaced exactly this: `INFO: scitex-dev: auditing /home/ywatanabe/proj/scitex-dev (branch develop, HEAD cc599c9)`. Fix per the documented idiom in `audit_all_for_package`: anchor on the test file's own checkout (`Path(__file__).resolve().parents[2]`, since the file lives at <root>/tests/develop/test_audit.py), threaded through as `--path` so every source-reading sub-auditor grades the commit under test — which carries the `[all]`-closure fix and is PS-221-clean. * fix(test): rename mismatched test files to double-underscore (PS-204/PS-205) With the audit now correctly targeting the checkout under test (previous commit's --path fix), audit-all surfaced 3 pre-existing error-severity violations that the wrong-tree resolution had been masking on develop (introduced by #374 and #385, not by this consolidation): [E] PS-205 test-name-prefix-mismatch — private `_knob_commands.py` must be tested by `test__knob_commands.py` (double underscore). [E] PS-205 test-name-prefix-mismatch — private `_test_execution_plugin.py` must be tested by `test__test_execution_plugin.py`. [E] PS-204 orphan-test-file — `test_knob_commands.py` mapped to a non-existent `knob_commands.py` (the src is private `_knob_commands.py`). Both PS-204 and PS-205 on the knob file are resolved by the single rename to the double-underscore form; the plugin file's PS-205 by its rename. Content is unchanged (both already no-mocks + Arrange/Act/Assert, so no test-quality rule fires), and `test__*.py` still matches pytest's `test_*.py` collection glob. The 6 remaining §12/§13 CLI-naming lines are WARN-severity and do not fail the gate.
…install-integrity primitive (#396) Operator directive 2026-07-21: when a SciTeX tool is invoked, a latest-version check must fire — at ERROR severity by default ("普通は warning ですが、私たちはエラーを選びます"). scitex-dev owns the primitive; consumers (scitex-cards CLI entry + MCP startup) wire it fail-open via `try: from scitex_dev.staleness import ensure_current / except ImportError: pass`. Two halves in ensure_current(dist_name, *, severity=None): 1. INTEGRITY — always runs, purely local, no network: - multiple dist-info dirs claiming the dist in one site dir → "ambiguous metadata"; - any RECORD-listed file missing on disk → "partial install". RECORD is parsed directly, NOT via Distribution.files: Python 3.12 silently filters missing files out of .files, which would hide the exact corruption this gate exists to catch. Motivating incident (2026-07-21): a venv carried 0.16.0 + 0.17.4 dist-infos with 0.17.4 metadata over a 0.16-era file set; _store_backend.py was in RECORD but absent on disk — every version probe lied. Editable installs (PEP 610 editable flag) skip this half; freshness governs them. 2. FRESHNESS — fail-safe (no cache / offline / any error → PASS): - wheel: installed vs cached-latest (per-dist cache file; PyPI is truth; opportunistic detached background refresh with TTL — never a blocking live PyPI call on the invocation path); - editable: HEAD behind tracking remote (reuses the drift git machinery). Severity ladder: explicit arg > $SCITEX_DEV_CURRENCY_SEVERITY > currency_severity knob (config.yaml → knob-state.json) > default error. error raises StalenessError carrying the exact remedy (pip install -U <dist> / git -C <path> pull --ff-only); warn logs via scitex-logging; silent no-ops. SCITEX_DEV_NO_CURRENCY_GATE=1 bypasses both halves but emits a loud WARN — an exercised bypass is never silent. Shared pieces refactored out of check_editable_drift (no duplication): _editable_dir_from_meta extraction, _compute_drift/_resolve_severity parameterization. scitex-dev's own CLI now runs an integrity-only self-check at warn severity next to the existing drift emission. Tests: real dist-info/RECORD fixtures in a tmp site dir (injected via the _search_paths seam), real git repos, no mocks; log capture via a handler on the scitex_dev logger (#395 idiom).
…rrent checkout > registry (#397) Operator directive (2026-07-21): audits must land where the caller aimed (監査が worktrees などでもうまく狙ったところにいくように). Auditing the develop checkout is legitimate in itself; the defect is resolution silently landing on a tree the caller didn't intend. Incident (same day): tests/develop/test_audit.py ran audit_all_for_package('scitex-dev') on a CI runner and the per-target audit CLIs resolved the operator's ~/proj/scitex-dev develop checkout via the ecosystem registry's local_path — grading a different tree than the CI checkout the test lived in. PR #395 pinned that one test with an explicit path; this fixes the resolution itself. Precedence, applied by audit-project / audit-django / audit-python-apis (and therefore ecosystem audit-all, which shells out to them with the caller's cwd): a. explicit --path/--repo — always wins (unchanged) b. the CURRENT checkout — git toplevel of the cwd (works inside linked worktrees) when its pyproject [project].name PEP-503-matches the requested distribution c. the registry's local_path — historical behaviour, still right for cross-package audits run from anywhere Changes: - new shared resolver _cli/audit/_target_tree.py (resolve_target_tree, cwd_checkout_of, PEP-503 normalize/match); the three per-target cmds use it instead of their registry-first block - _project/_discovery: PEP-503 name matching in _looks_like_checkout_of; _resolve_repo_root_with_rule reports WHICH rule resolved (explicit/cwd/import/proj-guess) - the #392 resolved-tree banner now names the winning rule (via cwd / via registry / ...); --json payloads carry resolved_via - line-limit refactor: _project/_audit.py check sequence extracted verbatim into _project/_run_checks.py (pure move, no behaviour change) - tests (no mocks): real tmp git repos + git-worktree-add linked worktrees covering explicit-wins, cwd main checkout, cwd linked worktree outranking an injected registry mapping, registry fallback, PEP-503 variants, and the banner/payload rule token
…rministic audit target-tree + de-flakes (#398)
#400) Root cause of fleet CI drift (inventory 2026-07-21): scitex-dev shipped TWO competing canonical mechanisms — (A) the consolidated pr-ci.yml.tmpl/release-ci.yml.tmpl pair rendered by `ecosystem ci-template apply`, and (B) `ci runner register` copying its own in-SIF ci.yml.template — stranding the fleet across 6 workflow generations. Operator decision (TG, 2026-07-21): converge on ONE shape — the thin REUSABLE caller `ci.yml` delegating every job body to scitex-ai/.github@main (a shared workflow cannot drift per-repo), runner selection via the CI_RUNS_ON Actions Variable with self-hosted default ["self-hosted","Linux","X64","scitex-ci"], never ubuntu-latest (PS-169). What won / what was deleted: - WINNER: `scitex_dev._ecosystem.ci_template` now renders ONE ci.yml.tmpl (caller jobs pytest-matrix / import-smoke / quality-audit / rtd-sphinx-build, `secrets: inherit`, PR+push+nightly+dispatch triggers, PR-only cancel-in-progress). pr-ci.yml.tmpl and release-ci.yml.tmpl are DELETED; apply now also deletes superseded pr-ci.yml/release-ci.yml and newb-docs* (operator-ordered fleet-wide removal) in target repos. cla.yml, auto-merge-to-develop.*, pypi-publish-*, rtd-sphinx-* stay preserved exactly as before. - LOSER→ALIAS: `ci runner register` no longer carries a template body; it delegates workflow deployment to ci_template.apply and only adds the CI_RUNS_ON Actions Variable (canonical default). Its ci.yml.template is DELETED. SCITEX_CI_APPTAINER/SIF vars are no longer set (they configured the deleted in-SIF body only). - Branch-protection gate now expects the workflow_call context shape "<caller-job-id> / <reusable job name>"; tests pin caller-job ids against the gate's expected prefixes so they cannot drift apart. Org-side gaps (NOT changed here; wave-2 work in scitex-ai/.github): - Org reusable workflows pin runs-on [self-hosted, Linux, X64, spartan-cpu] and declare `workflow_call: {}` (no inputs); they must adopt the fromJSON(vars.CI_RUNS_ON || default) expression (vars resolve from the caller repo) for per-repo runner selection to bind. - quality-audit.yml runs a FULL audit (no --new-only PR-incremental gate); pytest-matrix pins its own python matrix; no dep-hygiene / CLI-help smoke equivalents. Tests pin: caller shape (pure uses+secrets jobs), vars.CI_RUNS_ON + self-hosted default present, zero ubuntu-latest in emitted bodies, newb-docs + superseded-file cleanup, exactly one vendored template, and no workflow YAML body left under ci/runner/templates.
…ai (#401) This repo moved to the scitex-ai GitHub org, but its own docs, badges, package metadata and sphinx html_context still pointed at the old ywatanabe1989 org. Rewrite SELF-references only. Cross-repo links to packages still living under ywatanabe1989 are left untouched, as are historical records (ADRs, frozen release notes).
… store (#399) * fix(test): session live-store shield — no test may see the fleet card store Incident 2026-07-21 (third store wipe): the fleet's canonical scitex-cards store (~/.scitex/cards/cards.db, 2170 cards) was mass-DELETEd down to 18 rows. Culprit class: full-suite pytest runs in worktrees executing with the session's AMBIENT environment, where SCITEX_CARDS_DB / SCITEX_TODO_* point at the live store — tests that exercise store-writing code paths then hit production data. The window overlaps this repo's full-suite runs; scitex-cards is adding a write-side shrink guard on their side, and this is scitex-dev's own mechanical barrier. A session-scoped autouse fixture (_shield_live_card_store in tests/conftest.py) now, before any test runs: - FORCE-SETS every store-steering env var — both SCITEX_CARDS_* and SCITEX_TODO_* twins of DB / TASKS / TASKS_YAML / TASKS_YAML_SHARED / INBOX_DB / INDEX_PATH / CI_STATE / BOARD_PIDFILE, plus SCITEX_DIR — to per-session tmp paths (tasks.yaml seeded as an empty valid store); - forces backend selectors local (STORE_BACKEND / INBOX_BACKEND = sqlite, DUAL_WRITE / STORE_GIT_AUTOCOMMIT = 0) and unsets the remote-hub endpoints (HUB_URL / HUB_TOKEN / HUB_TOKEN_FILE); - asserts (defense-in-depth) that the store path scitex_cards would actually resolve is NOT any live path captured from the pre-shield environment — failing LOUD before a single test runs; - restores the original env only at session teardown, never mid-session. REPOINT, never merely unset: with these vars absent, scitex-cards (and scitex-dev's own _task_harvest) fall back to the default $HOME-based paths — i.e. unsetting routes straight back to the live store. Explicit tmp paths cover both the env tier and the HOME-fallback tier of every resolution chain. Both prefixes must be set because scitex_cards._env_compat.mirror_env() copies CARDS_* onto TODO_* with "new name wins" at import time. tests/test_live_store_shield.py proves the barrier: env vars point into tmp during tests, and the leak assertion fires on simulated leaks built from a saved copy of the original env (the session is never unshielded). Verified locally: 1470 passed / 27 skipped across linter + staleness + audit + shield subsets with the shield active. * fix(test): move live-store-shield test under tests/scitex_dev/ (PS-203) * fix(test): move live-store-shield test to tests/develop/ (PS-204 — meta-test has no src mirror)
… truth (#402) Verified all 68 ECOSYSTEM entries against GitHub with 'gh api repos/<owner>/<repo> --jq .full_name' (the API follows renames/transfers, so the returned full_name is canonical even when queried via the old path). Counts: 66 updated to scitex-ai/* (65 owner-only + scitex-todo -> scitex-ai/scitex-cards, an owner+name change), 2 confirmed-unchanged untransferred GitHub archives (scitex-audit, scitex-bridge — verification comments added), 0 unverifiable. Mechanical split for the 512-line cap: the literal table moved to _registry_data_1.py / _registry_data_2.py, concatenated order-preservingly in _registry.py; public surface (ECOSYSTEM, PackageInfo, __all__, _core re-export) unchanged and now pinned by tests. scripts/quality/ecosystem_list.py (which regex-parses the registry source) now reads the part files, with fallbacks for the older layouts. Audit footer: the escalation issues URL in _audit_disclaimer.py is now derived from the registry's own scitex-dev entry (_issues_url()) instead of a second hardcode, so an org/repo rename cannot leave a stale self-reference again.
…, phased adoption (#403) ADR-0004: ONE declarative JobSpec for all job families (tests, CI, experiments, agent/fleet jobs); INTENT (one-shot|periodic|daemon|reactive) orthogonal to MECHANISM (local-exec|slurm|gh-runner|systemd/cron); best-effort allocation onto the shared pool (org runners + Spartan backfill, vars.CI_RUNS_ON as the CI-facing slice); ownership split scitex-dev (primitives+aggregation) / scitex-hpc (Spartan mechanics) / sac (container/daemon lifecycle) per ADR-0003 seams; fail-safe invocation path vs fail-loud gates; phased adoption P1 tests (shipped #385) -> P2 CI (#400 wave in flight) -> P3 fleet jobs (federation), each with exit criteria. Synthesizes docs/design/execution-fabric-phase1.md (#388), the #385 test-execution recipe, the #400 canonical CI caller + drift inventory, and the JobSpec federation contract. Claude-Session: https://claude.ai/code/session_01KEDFF532gvmbxk3jhsgG63
…ames (#404) PS-221's suggestion text recommended making an extra internal by renaming it with a leading underscore (`_dev`, `_ci`) — but PEP 508/685 forbids leading-underscore extra names: setuptools refuses to build, hatchling rejects the metadata, pip/uv reject the request form. Five repos broke on this advice in 2026-07 before reverting. - Both PS-221 violation messages + the co-located rule text now give closure-by-inclusion as the fix (reference every public extra from `all` via self-refs, the shape scitex-dev/scholar/session/logging ship) and explicitly warn the underscore rename is invalid. - Checker docstring: underscore skip documented as defensive-only, new Remedy section; behavior unchanged (all 10 tests pass as-is). - general/05_development/11_dependency-tiers.md reconciled with the enforced rule: removed the stale '[dev] NOT included by [all]' claim; [dev] is a public extra closed into [all] via a self-ref (PS-221 cited); example toml updated to the shipped shape. - Test comments updated to describe the underscore skip as defensive (no assertion changes; none asserted the old suggestion text).
…egistry checkout (#405) * fix(audit): read §6 exemption from the AUDITED tree, not the runner registry checkout On self-hosted CI runners the home often carries a ~/proj/<pkg> checkout at the ecosystem registry local_path; _audited_repo_root preferred it, so a PR declaring mcp_parity_exempt / mcp_tools_allowlist could never turn its own quality-audit green (scitex-orochi #460) — the exemption was read from the stale tree instead of the editable-installed PR checkout. Reorder: import-derived root (the tree actually under audit) first; registry local_path only decides for non-editable site-packages installs where no audited tree exists on disk. Extracted the resolution pair to _mcp_repo_root.py (line budget; names re-exported from _mcp_parity), with the registry resolver injectable for the precedence tests. * chore: drop stray executable bit on _mcp_repo_root.py
…h a structural carve-out (#406) * feat(audit): enforce the no-bare-print mandate — PS-220 to error, with a structural carve-out PS-220 already existed and was already wired, but it could not fail: it shipped at severity "W" in its own rule tuple, and `audit_project` drives the exit code off "E" findings only. The default severity floor is "error", so its findings were not even PRINTED unless someone passed `--severity warning`. It was dormant by SEVERITY, not by exemption. - Promote PS-220 to "E" in its co-located rule tuple. - Fix the severity-table trap: `_patch` (which applies `_SEVERITY_OVERRIDES` and `_SLUGS`) ran BEFORE the co-located/sidecar rule merges, so the override table silently ignored 31 rules. Adding an entry for any of them did nothing, with no error. Apply `_patch` after all merges instead. Verified no rule changes severity or slug today, so this is a pure safety fix. - Add the bucket-five discriminator (`_print_discriminator.py`): stderr always fires (scitex-logging owns stderr), prose to stdout always fires, a serializer payload to stdout is spared, and anything undecidable fires. Machine-readable stdout is spared STRUCTURALLY because a logger would corrupt it — a ban without the carve-out gets worked around rather than followed. - Replace the blanket `# noqa` hatch with per-site `audit.exemptions` entries carrying a MANDATORY reason; a blank reason is rejected and reported. The old hatch keeps working for one release at severity W. - Scope the error severity by project type: E for ecosystem packages, W for research hybrids (the operator has not ruled on research trees), overridable via `audit.enforce-logging`. - Fix `_src_files` matching the exclusion set against the ABSOLUTE path: a checkout under any dir named docs/tests/scripts/examples skipped every file and reported a tree it never looked at. * fix(audit): correct a PS-220 false positive CI caught, and declare scitex-dev's own migration state CI (run 29906963643) failed test_audit_all_clean on all three Python legs — not a py3.11-specific issue. Two distinct causes, both mine. 1. A REAL FALSE POSITIVE in the discriminator. `_assignments_to` collected EVERY assignment to a `file=` name in the enclosing function, and `_resolve_name_stream` is fail-closed on stderr. In the real branch chain at `_cli/_utils.py:35-51`, `out` is rebound three times and only the LAST branch is stderr — so that stderr poisoned the two stdout payload writers above it, and genuine `--json` sites were reported as "writes to stderr". Fixed by resolving to the NEAREST PRECEDING assignment (a standard reaching-definition approximation). Lines 37/42/44 are now correctly spared; 47/49/51 still correctly fire. scitex-dev's count: 135 -> 132. I had claimed these sites "verified spare" in the PR body. That claim was not earned: I tested synthetic snippets reproducing their SHAPE and never ran the discriminator against the real files, then reported the intended conclusion as a measured one. The bug lived exactly in the difference between the fixture (one assignment) and the file (three). Recorded in the survey as a seventh instance of "unearned green". 2. PS-204 orphan test. `test__ps220_exit_code.py` had no src counterpart. PS-204 permits a descriptor suffix (`test__foo_real.py` -> `_foo.py`), which is why `test__registry_severity_overrides.py` passed via `_registry`. Renamed to `test__check_no_print_exit_code.py`. 3. The remaining 132 findings are GENUINE — the rule firing on its own author. They are not migrated here and not given per-site exemptions (132 reasons all reading "not migrated yet" is the reasonless park the design rejects). Instead scitex-dev declares `audit.enforce-logging: warning` in its own config, because converting print -> scitex-logging moves output from STDOUT to STDERR and scitex-dev is a CLI the fleet parses. Severity stays E for every other package. `warning`, never `off` — all 132 stay visible on every run. Retired by scitex-todo card `ps220-selfmigrate-scitex-dev`, named in the config comment. Tests: 1223 passed in tests/scitex_dev/_cli/audit (was 1217; +6 branch-chain regression tests pinning each destination in the three-branch shape).
Two audit gates ran different things and disagreed. The per-repo pytest wrapper honoured its `skip_rules=` kwarg; the org reusable workflow called `scitex-dev ecosystem audit-all` directly and bypassed it. So develop could be green while unified CI was red on identical code (measured in scitex-hub PR #433: 150 violations masked by the wrapper, zero masked by the workflow). Deferring a rule to a named migration campaign is the sanctioned exemption; forcing every in-flight campaign into one commit is not a scheduling policy anyone chose. So audit-all now reads the repo's deferrals itself — and never honours one silently. New `audit.skip-rules` in <repo>/.scitex/dev/config.yaml: audit: skip-rules: PS-139: "TQ-migration campaign — tracked in scitex-hub#412" - A rationale is MANDATORY. A bare list (`- PS-139`) is rejected with exit 2 naming the entry; a deferral that cannot say why is the abandonment the mechanism exists to catch. - A MASKED INVENTORY is ALWAYS emitted — total, rule ids, per-rule masked count, and each written reason — in normal output, not behind a verbosity flag, and in the --json payload too. - The summary states BOTH numbers, for single- and multi-package runs. "0 errors" while 150 are masked is a lie of omission. - The exit code stays driven by unmasked findings; one declared deferral never blanket-silences anything else. - A declared rule matching zero violations is flagged removable. Distinct from legacy `audit.skip` (bare codes, audit-project-only, silent) and from `audit.capabilities`, both untouched. Also fixes the summary defect sac reported: every auditor's CLEAN line named its category ("no project-structure violations") but every FAILURE line did not, so audit-all printed named SUCC lines then a bare `<pkg> (<path>): 3 error(s)`. sac PRs #813/#814 both misread a real violation as a broken gate because of it. Fixed across all five categories. Warning-level findings are now reported separately from errors rather than folded into the error count — the same imprecision one level up. `_cli/audit/_skills/_audit.py` (631 lines, over budget) split into _registry/_violation/_discovery/_checks + facade, mirroring the sibling _project/ package. Pure extraction. Mutation-proven (break it, watch it go red, restore, watch it pass): all 7 behaviours PROVEN. 1504 audit+ecosystem tests pass; audit-all on this worktree exits 0 with every line naming its category.
… + native skip-rules masking (#408)
#409) `scitex-ui --version` and `scitex-app --version` printed `scitex-dev <scitex-dev's version>` — 36 downstream packages reported another package's identity. `scitex_dev/_cli/__init__.py` fast-paths a bare `--version` to skip the expensive `._root` import. The predicate matched on `sys.argv[1:]` alone, never checking WHO was asking. Because `scitex_dev._cli` is a shared primitive that downstream CLIs import (`._completion`, `._root`), the block fired during `import` in any process launched as `<other-cli> --version`: it printed scitex-dev's name and version and raised SystemExit(0) before the downstream CLI's own `main()` ran. Exit code 0, so nothing looked broken; the downstream packages' own `--version` wiring was correct all along. The fast path now additionally requires `sys.argv[0]` to identify scitex-dev's own console script (or `python -m scitex_dev`). An unrecognised argv[0] declines the optimisation and falls through to the real Click group — correct, just slower. No path falls back to printing `scitex-dev`; that fallback was the defect. A library must never read sys.argv or exit at import time. Mutation-proven: dropping the argv[0] gate, blanking the entry-point basenames, and making an unknown argv[0] silently claim scitex-dev each turn the suite red; restoring returns 14 passed.
…o error (#410) 0.35.0 (#406) promoted PS-220 to ERROR ecosystem-wide. The measured blast radius — 44 repos newly FAILING on 1856 findings, top-5 repos carrying 64% of them (GITIGNORED/ps220-blast-radius-20260722.md) — led the operator to restage the rollout (Telegram 1691/1692): 「print に関しては順次やっていきましょうか。 とりあえず warning で、移行できたものから red で」 Staging, not retreat: every finding stays visible on every audit run, and nothing about what the rule DETECTS changed. * Default severity back to W for every project type (_check_no_print._DEFAULT_SEVERITY, the rule tuple, and resolve_ps220_severity). * Per-package opt-in via the EXISTING audit.enforce-logging knob, extended to a mapping that carries a MANDATORY written reason: audit: enforce-logging: level: error reason: "print migration complete (PR #412)" `error` and `off` deviate from the default and must state why; `warning` is accepted bare because it IS the default and changes nothing. A missing/whitespace-only reason is a HARD CONFIG ERROR: the declaration does NOT take effect (falls back to W) AND the rejection is reported at E, so a package can never believe it is gated — or silenced — when it is not. Bare `error`/`off` shorthands (and their YAML 1.1 boolean spellings) are rejected on the same grounds; unrecognised values now reject loudly instead of falling back silently. Parsing lives in the new _config/_enforce_logging.py, keeping _loader.py under the line cap. * Config errors are NOT staged. A rejected exemption entry and a rejected enforce-logging declaration are both reported at E regardless of the project's PS-220 severity. Staging covers migration debt; a malformed override is not debt. (This preserves #406's strength for exemptions, which would otherwise have been demoted to W by the restaging.) * Removed the blanket `# noqa` hatch and its PS-220-noqa-deprecated notice. A sweep of all 118 repos under ~/proj (8956 src/**.py files, 4448 flagged sites) found ZERO users, with a planted-user control confirming the sweep could detect one. audit.exemptions is now the only per-site opt-out. Tests: the registry override test injected "W" for PS-220, which becomes a tautology once W is the registered severity — it now injects "E" and a guard test asserts the injected severity differs from the registered one. Both halves of the staged gate are pinned: a print does NOT fail a non-opted-in package, and the SAME source DOES fail an opted-in one (one variable varied). Mutation-proved: reverting the default to E reds 11 tests; dropping the mandatory-reason rule reds 18.
…t-in, --version entry-point gate (#411) Promotes the accumulated [Unreleased] section to 0.36.0 and documents #409, which landed without a CHANGELOG entry. Minor, not patch: the PS-220 default severity moves E -> W (a behaviour change for every consumer), `audit.enforce-logging` grows a new mapping shape with a mandatory reason, and two opt-outs are REMOVED — the bare `enforce-logging: error` / `off` scalar shorthands and the blanket `# noqa` hatch. Under 0.x that combination is a minor bump.
…t the node (#412) `pytest -n auto` over-subscribes an allocated node. The cause is narrower than "auto reads the machine's core count": pytest-xdist's `pytest_xdist_auto_num_workers` consults psutil FIRST and returns its count immediately (xdist/plugin.py 3.8.0 L26-34), and psutil ignores cgroup/cpuset confinement. Only if psutil is absent does xdist fall through to `sched_getaffinity`, which is correct. Measured on the Spartan CI runner node spartan-bm155, inside the 48-CPU lease cgroup `job_27144058/step_extern`: os.sched_getaffinity(0) 48 <- the allocation os.cpu_count() 128 psutil.cpu_count(logical=False) 128 <- what `-n auto` used psutil.cpu_count() 128 SLURM_CPUS_PER_TASK (empty) SLURM_JOB_CPUS_PER_NODE (empty) i.e. ~128 workers into a 48-CPU allocation, a 2.7x continuous over-subscription (corroborated by worker ids reaching gw121 in a scitex-hub run). Changes: * `_core/test_execution.py`: new `allocated_cpus()` — SLURM_CPUS_PER_TASK, then SLURM_JOB_CPUS_PER_NODE (incl. the "48(x2)" form), then `sched_getaffinity`, then `os.cpu_count()`. No magic cap and no hardcoded core count, so an unconstrained laptop still gets all its cores. `ALLOCATED_CPUS_PY_SNIPPET` carries the same policy as a shell-embeddable one-liner for invocations that must resolve the count on a REMOTE host. * `ecosystem test-remote`: keeps `-n auto` but corrects it at its source by exporting `PYTEST_XDIST_AUTO_NUM_WORKERS`, which xdist consults ahead of psutil. Computed on the remote and echoed into the log. * ADR-0004: P1(b) named `$SLURM_CPUS_PER_TASK` as the source; that variable is empty on the runner (ssh-adopted into the lease cgroup, so it inherits the cpuset but not the step env). Corrected, with the measurement recorded. NOT changed, deliberately: `src/scitex_dev/_ecosystem/ci_template/templates/` contains no `-n auto` at all — it is a thin caller into scitex-ai/.github. The live `-n auto` that produced gw121 is `.github/workflows/pytest-matrix.yml:89` in THAT repo and needs the same one-line export there; it is outside this repo's tree.
`scitex-dev cron list` printed `installed (0): (none)` when it could not
read the crontab AT ALL — cannot-look rendered as looked-and-found-zero.
Reads failed soft (`read_crontab` returned "" on a missing binary) while
writes failed loud; only the read path reported, so the soft failure was
the one that reached an operator.
This nearly caused a false fleet-wide automation-outage escalation: a
peer agent ran `cron list` inside a container with no `crontab` binary,
read "13 jobs registered, NONE installed" — worktree-gc, ci-watch,
creds-rotate-all — and was one step from escalating. The jobs were
running fine on the host; only the container could not look.
Adds a third state at the source (`CrontabRead` / `read_crontab_state`)
and plumbs it through both reporting surfaces:
* read, lines found -> the count, as before
* read, empty -> zero installed, as before (unchanged)
* NOT readable -> UNKNOWN, with the reason
Human: `installed: UNKNOWN ('crontab' not found on PATH)` plus an
explicit "this is NOT zero jobs installed" note; `cron status` gains a
banner and per-job `installed: unknown` instead of a fabricated `no`.
JSON: `cron list` reports `installed: null` (NOT `[]`, so a consumer
calling len() cannot silently read zero) alongside `installed_state`
and `installed_unavailable_reason`; `cron status` gains `crontab_state`
and `crontab_unavailable_reason`.
A non-zero `crontab -l` exit whose message is "no crontab for <user>"
stays READABLE-and-empty — that is an ordinary empty crontab, not a
failure to look.
The write path is untouched: `write_crontab` still raises, which is
correct. `read_crontab` keeps its lossy signature for callers that
cannot act on the difference (install / remove).
Tests are mutation-proved on both arms — collapsing UNKNOWN back into
empty reddens 10 tests; relabelling everything UNKNOWN reddens the 5
control tests that pin "readable and genuinely empty still reports
zero".
Every verification failure recorded across the fleet on 2026-07-22/23 is one shape: a failed measurement rendered as a confident value. The new leaf states that once and organises the defences by claim type — absence claims need a positive control, causal claims need one varied variable and a hunted counter-example, content claims need a second independent reader, artifact claims need the artifact actually in use, peer claims need corroboration independent in kind. Also carries the six-state search-failure taxonomy, the vacuous / inert / layer-blind / sampling control failures, the both-arms rule for fixing a masking bug, the status words that fuse a failed measurement into a clean one (skipped, declared-masked, 0), and the mechanism for commissioned findings. Every rule cites a measured instance. Draft sections with no instance behind them were dropped. 200 lines / 10230 B — under the §4 leaf ceiling. Verified with a positive control: inflating the leaf past the ceiling makes the checker report §4.monolith, so the clean result is not a check that never ran.
…(132 -> 118) (#415) * chore(ps220): migrate stderr prints to scitex-logging (trace_env, linter format/health) First slice of the PS-220 self-migration. scitex-dev authored the no-bare-print mandate but carried 132 findings against it; this migrates part of the STDERR bucket, where scitex-logging provably cannot move the output because it already writes every level to sys.stderr. - trace_env/trace.py: the strace announce -> log.info - linter/_cmd_format.py: not-found / no-files -> log.error / log.warning - linter/_health.py: both one-shot L1/L2 notices -> log.warning, dropping the hand-rolled ANSI now that scitex-logging owns the colouring Stdout prints are deliberately untouched: they need the console API that is still being designed. * chore(ps220): migrate stderr prints in linter completion + dispatch argparse - linter/_cmd_completion.py: both unsupported-shell errors -> log.error. These two sites are dead legacy argparse code: nothing imports the module except a lazy _generate_completion_script import in _cmd_completion_cmds.py, register() is never called, and the live surface is the click group in _cmd_completion_cmds.py. - _core/dispatch/_docs_argparse.py: LookupError report -> log.error. - _core/dispatch/_skills_argparse.py: the three not-found reports -> log.error; the skills-get pair folds into one record rather than two separately-prefixed lines. sys stays imported in the dispatch modules — sys.exit(2) still uses it. * chore(ps220): migrate stderr prints in quality check + cron task-harvest - _cli/quality/_check.py: missing-version error -> log.error. - _cli/cron/_task_harvest.py: both harvest-failure reports -> log.error, dropping the now-redundant ERROR: prefix (the level supplies it) and the explicit flush (scitex-logging flushes its stderr handler). The [task-harvest <timestamp>] tag stays — it is the cron log convention that lets an operator grep one pass out of a shared log. Completes the 14-site stderr slice.
…fig-layout convention (#416) * feat(audit): PS-222 config-layout auditor (check module) * feat(audit): register + wire PS-222 config-layout check * test(audit): PS-222 config-layout tests incl. runtime/ control arm * docs(skills): PS-222 config-layout enforcement rule doc
* docs(doctrine): degrade branches hide hard failures; mispositioned probes cannot fail * docs(doctrine): trim redundant instances to make room for the masking rule * docs(doctrine): tighten §4/§5 prose
* fix(audit): never print SUCC over live warning findings audit-project decided its success banner from the severity-FLOOR-filtered finding list, so at the default --severity error a tree carrying live W findings printed output byte-identical to a genuinely clean tree: SUCC: <pkg> (<root>): no project-structure violations Measured on scitex-agent-container: 53 live PS-220 findings, SUCC, exit 0. The serious consequence is not cosmetic. A mutation proof driven through the CLI could not fail for ANY W-severity rule: planting a bare print() did not change one byte of output, so the check that was supposed to detect it was structurally incapable of doing so. Every 'mutation-proved' green produced that way this week was unearned. It also broke the staged-rollout premise PS-220's E->W move (PR #410) was granted on: findings stay FULLY VISIBLE, they only stop blocking. Fix — reporting only, exit codes untouched: - SUCC iff zero findings at ANY severity (was: zero findings at floor) - headline always carries both counts: N error(s), M warning(s) - counts taken over all surviving findings, not the floor-filtered view - --json gains warnings/infos alongside the existing errors - below-floor findings get a line naming how to list them W/I still never fail CI. Changing the exit code would silently re-break every repo the 0.36.0 restage just unblocked. Reported-by: scitex-agent-container (2026-07-23) * test(audit): pin that the summary never claims success over live findings Asserts on OUTPUT, not only exit codes. The pre-existing PS-220 suite (test__check_no_print_exit_code.py) reads exit codes alone, which is exactly why this bug survived it: the exit code was correct throughout, and only the banner lied. Covers the four required arms plus the control: - zero findings -> SUCC printed, exit 0 - N warnings, 0 errors -> SUCC NOT printed, count shown, exit 0 - N errors -> failure reported, exit 1 (unchanged) - --json -> both errors and warnings present - mutation control -> planting one bare print in an otherwise identical tree moves warnings 0 -> 1 The last pair is the test the previous verification could not perform. Pre-fix, both halves of it produced identical output, so no assertion on the CLI's report could have distinguished them. * test(audit): assert the banner on its logging transport, not on a stream CI was red on all three pytest legs while the same tests passed locally. Measured rather than guessed: `_emit` logs through scitex_logging.getLogger('scitex_dev.audit'), and that logger carries NO handlers of its own with propagate=True. The banner reaches a terminal only via handlers scitex-logging installs on the ROOT logger — ambient state that depends on import order and on whatever else touched logging in the process. capfd saw it in a bare shell and not under the full CI suite, which captured the click-printed epilogue and none of the log. The --json assertions were green in CI throughout, which corroborates the split: that payload really does go through click.echo to stdout. So the banner assertions now read caplog. That is deterministic across environments and strictly STRONGER than the stream version: it also pins the LEVEL of each line (SUCC / WARN / ERRO), so a regression that downgraded the summary to info — invisible in real use, since the audit logger's default level is WARNING — now fails. No assertion was weakened; three were added. Also closes an ordering hazard the rewrite surfaced: naming the audit logger in caplog.set_level() creates it through the stdlib, and logging.getLogger caches by name, so scitex_logging then returns that plain Logger and _emit._logger loses .success. Fixtures set the level on root only, and the module imports _emit up front to bind the logger before anything else can. The same hazard is a latent crash in production — reported separately, not silently absorbed here.
…ries (#419) * feat(ecosystem): distribution-identity enumeration keyed by origin remote Adds _ecosystem/_enumerate.py: resolves checkout directories into DISTRIBUTIONS by origin remote instead of directory name, collapses duplicate checkouts and linked worktrees into reported aliases, cross-references an org listing to surface repos with no local checkout, and labels which tree was measured. Unreadable paths are recorded as errors, never silently dropped. * test(ecosystem): cover distribution-identity enumeration with real git repos Two-armed suite: duplicate checkouts and linked worktrees collapse to one distribution with the extras reported as aliases, while the control arm proves two genuinely distinct repos still count as two. Also covers org-delta surfacing, loud error reporting, and the measured-tree label. * feat(ecosystem): expose distribution enumeration via 'ecosystem list --distributions' Opt-in flag; the default output shape is unchanged so existing callers keep working. Adds --org to cross-reference the GitHub org listing and --scan-root to enumerate a directory tree (the input shape brand-wide sweeps use, where duplicate checkouts appear). Output labels its own source, scope, measured tree, and alias/not-checked-out counts. Orphaned linked worktrees are now diagnosed by name rather than as a generic unreadable path.
…loor included PR #417/#420 fixed the human summary COUNTS so they no longer derive from the severity-floor-filtered `visible` list. The residual half was the --json branch, which still built its `violations` list from `visible`: at the default `error` floor a tree of live W findings emitted `"violations": []` while `warnings` reported a positive count, so any --json mutation proof written at the default floor read an empty list and could not fail. Keep `violations` floor-filtered so --json still honours --severity (control-arm), and add `violations_total` carrying every surviving finding with its severity. Additive: `violations` unchanged, and len([v for violations_total if severity=='W']) == warnings by construction. Applied to _project and _django; _api has no severity floor so it does not share the defect.
docs(skills): correct the void-proofs claim in 04_verification-controls.md (census found 0)
…_total Matched-pair coverage for the residual half of #417/#420 on the machine path, for both audit_project and audit_django: - default (error) floor, W-only tree: violations_total is NOT silently empty and carries the finding with severity (the defect) - CONTROL ARM: default floor, violations (filtered) stays [] — proves the fix did not just 'emit everything' and destroy --severity - warning floor: violations lists the W finding (floor still selects) - exit code identical (0) across floors - violations_total W-count == warnings (ties the list to #417 counts) - premise guard: _check_no_print._DEFAULT_SEVERITY == 'W' Mutation-proven both directions (visible-derived total -> disclosure tests red; violations ignores floor -> control arm red).
Add mutation-provable, function-level tests for W rules flagged by the #429 census as having no rule-ID proof. Each test calls the checker directly (never the CLI summary banner) with a stub Violation, asserting the finding on a triggering fixture and its absence on a clean control. Project (PS): PS-145, PS-147, PS-150, PS-151, PS-157, PS-165, PS-166, PS-167, PS-206b, PS-211, PS-212. Django (DJ): DJ-103, DJ-105, DJ-109, DJ-202, DJ-203 added to the shared conforming-fixture suite; DJ-107 already had a proof.
…ks.py)
The self-audit gate (tests/develop/test_audit.py::test_audit_all_clean)
scans the whole repo tree. The proof file was named
test__check_placeholder_tests_ps206b.py, which has no matching source
module — PS-204 (orphan-test-file, severity E) fired, making audit-all
exit 1. The function under test (_check_placeholder_tests) lives in
_checks.py, so test__checks.py is the mirror-correct name. Verified:
audit_project('scitex-dev') now exits 0 (was 1); the 3 PS-206b tests
stay green.
test(audit): prove 16 unproven W-severity audit rules
…skills docs(skills): split remaining oversize skill files under caps
…untime/ (finish #367 remainders 1+3)
…-creds fix(cron): route federated provider + creds install-cron logs under runtime/ (finish #367 remainders 1+3)
Merged #367+#433 moved cron/log output under $HOME/.scitex/<pkg>/runtime/logs/, but nothing mechanically prevented a regression to the forbidden non-runtime $HOME/.scitex/<pkg>/logs/ path — the directive lived only in a docstring, disconnected from the code it governs. PS-223 closes that gap. Co-located checker _check_logs_path.py AST-scans src/<pkg>/**.py and flags a STRING LITERAL matching .scitex/<pkg>/logs/ (segment after <pkg>/ is 'logs', not 'runtime/logs'). Two structural false-positive guards: docstrings and bare string-statements are skipped by node identity, and only whitespace-free path tokens fire — so description/help prose that merely mentions the path never fires. Severity W (warn-first, matching PS-222 bake-in). Registered in _registry.py as a co-located rule; dispatched from _run_checks.py. Test test__check_logs_path.py (mirrors source module): fires on forbidden literal, clean on runtime/logs control arm, silent on prose docstring/comment/ description; mutation-proven (suppress emit -> RED, restore -> GREEN).
feat(audit): PS-223 — flag non-runtime/ logs-path string literals in src
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pilot run of the tag-driven release flow the operator approved (2026-07-24: 「まず1個丁寧に、良さそうだったら手を離す」). scitex-dev is the pilot repo: it is the package that must ship the fix, and its blast radius is the smallest.
Why now
#436 fixed a FALSE-GREEN in the shared quality audit: STX-TQ (PA-307) located the tests/ tree from the import-resolved package (an installed wheel has no tests/), found 0 candidates, and returned a SILENT 0 that read as a clean pass. figrecipe caught it with a positive control.
Fleet exposure (measured today, this checkout)
ecosystem audit-all.--path; 34 files across 27 repos do not — the same shape that let TQ silently skip. scitex-dev itself is one of them.--no-version-check; 9 pass--new-only(all 9 pass both) — the constitution's named anti-pattern.Caveat stated honestly: missing
--pathis NECESSARY but not SUFFICIENT for a skip — if CI installs the package editable, the import-resolved path lands back in the repo and TQ does run. Per-repo positive controls are the definitive test and are the follow-up.Why this release fixes all of them
resolve_target_treeresolves explicit(--path) → cwd checkout → registry. #436 threads that resolved root into the TQ check, so with the fixed release installed, a CI job running in its own checkout gets the right tree WITHOUT needing --path. Shipping this is what closes the fleet exposure.Promotion of 71 commits accumulated on develop. Merges on real CI green only.