Skip to content

feat(continuous-ops): fail loudly on what's required: one preflight, a lean config (XWING-2440) - #339

Merged
smith-xyz merged 11 commits into
traust-security:developmentfrom
smith-xyz:feat/fail-loudly-on-what-s-required-one-preflight-a-l
Nov 2, 2026
Merged

smith-xyz merged 11 commits into
traust-security:developmentfrom
smith-xyz:feat/fail-loudly-on-what-s-required-one-preflight-a-l

Conversation

@smith-xyz

@smith-xyz smith-xyz commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Plan: plans/traust-standardization/0-continuous-ops/in-progress/22-shaun-fail-loud-preflight.md

Started by Shaun. Rebases onto merged development bdb72e9; Michele’s review is requested for the changed tests, behavior and security gates. Do not merge before that review.

What

The traust half of fail-loudly-on-what's-required: one preflight, the traust rows' decisions applied, adoption stays lean.

  • Pin: traust-core at reachable merged development e01d4202 (replacing the base branch’s orphaned 282ebbee and the old feature pin); it carries Context.preflight, checkout and never-audited observation behavior. uv.lock is regenerated.
  • traust admin doctor reports core's preflight. It runs Context.preflight on the security pack's enabled assets — the same door the engine's start gate refuses on — and every finding is a FAIL row that names its fix (a required gap was a WARN the report carried on past), exiting non-zero. The convention paths the plan keeps soft are shown as defaults in use (the feeds-cache chain, GIT_HOSTS, bicep, zero-config traust.yaml via each value's source row). The pack/engine does not need forge CLIs; the manual GitHub rescan worklist still needs gh for GitHub rows, while GitLab uses Forge REST. Its missing-gh guard fails up front, but doctor does not check gh authentication.
  • scripts/install_traust --doctor delegates to traust admin doctor; its own shell check path (file parsing, retired files, load smoke, completeness, toolchain doctor) is gone — one code path. traust admin toolchain doctor remains for per-profile scanner checks, traust config migrate for old multi-file homes.
  • The model registry is required: traust registry models spend refuses (exit 1, naming the fix) when the config home has no model-registry.yaml. The HARNESS_REQUIRE_SPEND_LEDGER env switch and the PENDING marker go.
  • The rescan worklist fails loud: gitleaks is required up front — the run refuses (exit 2, fix named, no worklist written) before any stage runs, core's tool preflight with requires: tools: semantics, never scanner_unavailable mid-run. The GitLab stages read through core's forge REST (ForgeApi via the context facade): no glab CLI on the runner, the host a known forge (forge_hosts, plus $GIT_HOSTS), its token the forge_tokens env var named for it, sent only as a header. Per-repo statuses keep their meaning (401/403 → no-credentials, transport give-up → unreachable).
  • No traust init: adoption stays copy traust.example.yaml, fill in, run doctor. A fresh home from the example fails preflight naming exactly locations.workspace, locations.analysis_results, locations.progress_tracker and ledger.public_key_path (no facts — traust's composition root registers the pack's defaults) and passes once they are set.

Notes:

  • Rebase: merged traust#336/feat(continuous-ops): repository checkout service: one door for a commit's files, with a mirror cache (XWING-2440) #337/feat(continuous-ops): skills check out through the service (XWING-2440) #338 and fix(pack): class_sweep declares its pending state #346 are in the base; the branch keeps only its eight unique commits plus the post-rebase observation regression. The Core pin is reachable merged e01d4202.
  • The engine side of the same gate is traust-engine#133. The doctor/engine parity test coordinates via the pack fixture — both consume the same assets.yaml and call the same Context.preflight; no engine import (the architecture test forbids one).
  • The security pack's assets.yaml carries its first requires: row — repo_state: {tools: [gitleaks]} (core's observation scans each change's patch for secrets and fails without the scanner), landed here so the doctor's FAIL row has something to fire on; doctor reads the rows generically (validated against contracts' pack schema, enabled: false skipped, merged).
  • Core's audit message for a missing model-registry.yaml names config/model-registry.example.yaml; traust ships config/model-registry.yaml (no .example infix). A core-side wording wart, noted here rather than forked.
  • Doctor's preflight registers the shipped feeds, so a requires: feeds: entry is checkable once the pack declares one.

Reviewer checks (from the plan)

  • No new except: return default or log.warning followed by carrying on, for anything required.
  • Optional means declared optional in config, never inferred from absence.
  • One preflight code path: doctor, engine and doors all call it.
  • Every error names its fix.

Quality checklist

  • 1. Comments only for the non-trivial
  • 2. Docstrings don't bloat
  • 3. Don't reinvent
  • 4. Shared logic is a helper, in one place
  • 5. Tests cover the edges
  • 6. Integration tests are thorough and use real data, sanitized
  • 7. Minimal, clean code
  • 8. Queries are low latency, and you checked
  • 9. Tests change only on purpose, and a person said so

Legacy files

Each file this plan replaces: reuse (core already does it), retire (the model made it obsolete) or port.

File Verdict Where it went / why it's gone
scripts/install_traust do_doctor (the shell check path) retire one code path: --doctor runs traust admin doctor (core's preflight). The duplicate shell parsing/load/completeness checks are gone; explicit --toolchain PROFILE also runs traust admin toolchain doctor against that same config home. traust config migrate remains separate
src/traust/cli/groups/registry.py _declare_spend's declaration-skip path (HARNESS_REQUIRE_SPEND_LEDGER, PENDING marker) retire the model registry is required: models spend exits 1 naming the fix, and core's completeness audit names a missing model-registry.yaml at preflight
src/traust/cli/build_rescan_worklist.py _glab_api (the glab subprocess transport) port onto core's forge REST (ForgeApi): same per-repo status kinds, token from forge_tokens as a header (never argv, never glab's auth store), hosts from forge_hosts ∪ $GIT_HOSTS
src/traust/cli/doctor.py check_tools' gh/glab rows and check_auth retire pack preflight does not use forge CLIs; the manual GitHub worklist still uses gh and fails up front when missing. Doctor does not verify GitHub CLI auth; see the partially addressed Qodo finding below

Changed tests

Test Old → new Why
tests/test_doctor.py home fixture bare {"home": …} traust.yaml → a complete home (locations, ledger key, the shipped model registry) doctor now FAILs on preflight findings; the exit-by-failures test needs a home the completeness audit accepts
tests/test_doctor.py::test_an_unreachable_store_fails_and_skips_the_catalog also asserts check_core returns None the loaded Context is handed on to the preflight/defaults checks
tests/test_doctor.py::test_main_reports_the_core_rows_and_exits_by_failures same assertions through the new main wiring the preflight and defaults-in-use rows replaced the auth section; --quick no longer skips auth
tests/test_install_traust.py::test_doctor_smoke_on_a_fresh_install → test_doctor_delegates_to_traust_admin_doctor asserted the shell doctor's own sections ("traust configuration loads", retired files) --doctor delegates: the test now asserts admin doctor's report and rc 1 on a fresh home naming the unset keys
tests/test_install_traust.py::test_doctor_names_retired_files_and_the_migrate_command deleted the retired-files warning went with the shell check path; traust config migrate remains the fold path
tests/test_install_traust.py::test_doctor_passes_on_the_operational_config shell-doctor assertions → delegated-report assertions same reason
tests/test_registry_models_spend.py::test_without_a_model_registry_the_declaration_is_skipped → …_is_refused parametrized skip/env-switch (rc 0 or 1) → one refusal the registry is required: exit 1 naming the fix, nothing recorded
tests/test_build_rescan_worklist.py::TestGitlabCompare → TestGitlabRest _glab_api monkeypatching → a ForgeApi fake; the glab-missing case → REST failure mapping (401/403 → no-credentials, transport give-up → unreachable, other HTTP → error) the transport is core's forge REST
tests/test_build_rescan_worklist.py::TestInjectionGuards::test_gitlab_rejects_traversal_segment gitlab_repo_info(host, path) → gitlab_repo_info(None, host, path) the fetchers take the forge client first
tests/test_build_rescan_worklist.py::TestMainEndToEnd::_patch_network gitlab fake takes the client argument; preflight_failures stubbed gitleaks-required is covered by its own refusal tests, not silently by the e2e fakes
tests/test_build_rescan_worklist.py::TestTripwire::test_scanner_missing_records_and_skips deleted, replaced by test_gitleaks_missing_refuses_before_the_worklist_builds + test_no_tripwire_skips_the_gitleaks_requirement the scanner is required up front (rc 2, no output), not recorded as skipped mid-run
tests/test_build_rescan_worklist.py::TestTripwire (hit / no-hits / quota-short) dropped the inert rw.shutil.which patches the module no longer imports shutil; availability is preflighted up front
tests/test_build_rescan_worklist.py::TestBanBreaker::test_stage1_aborts_after_streak stage1(entries, jobs=1, …) → stage1(entries, 1, None, …) stage1 takes the forge client
tests/test_security_router.py::test_tripwire_matches_the_legacy_router dropped the rw.shutil.which patch same reason as the tripwire tests
tests/test_traust_example.py::test_the_example_as_copied_is_the_zero_config_install Config(...) == Config() → model_dump() comparison the pinned core's Config records which keys the file set (provided, for preflight), so model equality no longer holds for a copied file
tests/test_portfolio_observe.py (home fixture / forge_api helper) the fake forge answers the compare patch (stream); a clean gitleaks scanner is registered; the never-audited test now expects repository and default-branch reads merged Core observes the first head of a never-audited repository, so the old “no forge contact” assertion was obsolete; the runner image’s scanner is not this test’s subject
tests/test_impact_doors.py::test_sbom_without_a_document_reads_the_commits_manifests (re-pin to core#312's head) the stubbed archive checkout built a Workspace without a provider the literal carries provider="forge-archive"
tests/test_manifest_sbom_lane.py::test_the_cli_records_the_observed_heads_sbom_once (re-pin) same stubbed Workspace literal without a provider same fix, provider="forge-archive"
tests/test_doctor.py::test_main_reports_the_core_rows_and_exits_by_failures, ::test_a_required_gap_is_a_fail_row_and_a_nonzero_exit, ::test_a_fresh_home_from_traust_example_yaml_names_exactly_what_is_still_needed new gitleaks_on_path fixture parameter (a stubbed-PATH scanner that passes the pin); assertions unchanged repo_state now requires gitleaks, so the config-row assertions must not depend on the machine's scanners
tests/test_build_rescan_worklist.py::TestMainEndToEnd._patch_network (and its subclass helpers) also puts a stub gh executable on a private PATH gh is required up front now; the e2e fakes never ran a real CLI
tests/test_build_rescan_worklist.py's gh fetcher fakes (fake_gh_info, fake_gh_cmp, failing_gh, the tripwire gh_patch / preroute tree_count lambdas — tests/test_security_router.py's too) take (project, sha) / (p, r) → also the row's host (keyword-defaulted host="github.com") every gh api call names its host now; the fakes mirror the fetcher signatures
tests/test_build_rescan_worklist.py::TestPreroute / TestTripwire _rows_and_keys fixtures unchanged assertions (the github rows stay without a host key) a hand-built row without a host is github.com's (_gh_host), the default the quota gates — the fixtures keep proving that fallback

New tests (Michele’s review required): doctor's required-gap FAIL/exit, doctor/engine same-findings parity over one pack fixture, the fresh-home lean-adoption test, the defaults-in-use rows, the worklist's forge-client hosts/tokens and gitleaks refusal tests; the GHE-host routing/quota tests (--hostname on every gh call, github.com's quota never gating an enterprise row); the doctor's chosen-home tests (the toolchain check on the forwarded home, a relative home resolved against the caller's cwd).

  • A person reviewed each changed test and the behaviour change is intended

Verification after rebase

  • make lint-fix: passed; make test: 2,757 passed, 86 skipped (TMPDIR=/tmp); commit and push hooks passed.
  • CI on rebased d5e9adf is running. Michele’s review remains requested.

Qodo findings addressed

Finding Verdict What changed
Doctor passes on a runner without gitleaks real repo_state declares requires: {tools: [gitleaks]} — the engine's start gate and doctor refuse on it before any work (a FAIL row naming the install pin, never a WARN); a test proves the missing-scanner FAIL row on a complete home
Doctor checks the wrong configuration home real (the --config-home flag) install_traust --doctor forwards --config-home to the delegated traust admin doctor (the env/default paths already saw the same home the old checks did); a test proves the flag wins over TRAUST_CONFIG_HOME
--doctor no longer refuses a missing config home not real both missing-home variants still exit 1 through core's completeness audit ("no config home: set $TRAUST_CONFIG_HOME or run scripts/install_traust"); the existing fail-closed test passes unchanged
CLI doctor misses scanner failures real (the ignored --toolchain flag) install_traust --doctor --toolchain PROFILE runs the profile's scanner doctor (traust admin toolchain doctor) beside the preflight again, so a degraded profile cannot pass silently; by default scanner requirements come from the pack's requires: rows (the repo_state row above is the first)
Doctor stops checking gh, which the router still needs partially addressed; leave review thread open the manual worklist refuses up front (exit 2 with installation/auth guidance) if GitHub rows need gh but it is missing, while GitLab-only runs do not require it. Doctor still does not verify gh/authentication and unauthenticated GitHub calls can fail per row; do not claim the complete operator-readiness finding fixed
Configured GitLab hosts stay unsupported real split_repo_url takes the known-host mapping and main threads _known_hosts(core) — the same merge the forge client gets (traust.yaml forge_hosts ∪ $GIT_HOSTS) — through stage 1, the quota estimate and the no-network branch, so a host named only in config routes instead of landing unsupported-host
Host comment wrongly says the env list can't widen reach real (informational) the $GIT_HOSTS comment now says what the merge does: the hosts feed the forge client's set, so they widen reach, not just recognition
GitHub Enterprise repos queried on github.com real a GHE host classified github-kind ran hostless gh api calls: every fetcher (stage 1/2, the tree count, the tripwire patch) now takes the row's host and names it (gh api --hostname), so a GHE repo reads its own API, never github.com's data for the same org/name; the quota reads are github.com's, so its gates (the preflight estimate, stage-2 deferral, the tripwire and preroute budgets) now count only github.com rows — an enterprise host runs its own limits
Scanner check ignores the chosen config home real the --toolchain PROFILE doctor forwards --config-home like the preflight call above it, so the scanner check loads the home the wrapper was given, not TRAUST_CONFIG_HOME/the default (which could exit 2 on a home that doesn't resolve while the chosen one is fine)
Doctor checks a relative home in the wrong folder real do_doctor resolves the home once at the wrapper's boundary — ~ expanded, a relative path anchored to the caller's directory — before the subshells cd into the repo root, so --doctor --config-home ./myhome run from outside the repo audits the caller's home (do_install absolutizes the same way)

Cross-repo coordination

Track: development. Use these explicit related-repository targets rather than assuming the connected repository's main is the matching overhaul baseline.

Review targets

Integration and rollout

Adopt the Core preflight door and reconcile its pin with the other application adoption PRs. The Engine companion shares the pack/preflight contract; it is not a new application-to-Engine Python import.

Branch URLs are compatibility baselines; PR URLs are proposed companion code. Neither proves that a manifest/lockfile pin already contains the changes or that the target is deployed. Honor the prerequisites above, reconcile compatible dependency pins before rollout, and do not dismiss a real compatibility gap merely because another PR is planned.

@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Oct 30, 2026
@openshift-ci

openshift-ci Bot commented Oct 30, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: smith-xyz

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Oct 30, 2026
@smith-xyz
smith-xyz marked this pull request as ready for review October 30, 2026 18:47
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Oct 30, 2026
@qodo-for-traust-security

Copy link
Copy Markdown

PR Summary by Qodo

Unify required preflight checks and fail closed in continuous operations

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes


AI Description

• Make doctor and installation report the requirements that gate security-pack execution.
• Require the model registry and gitleaks instead of silently skipping spend or tripwire checks.
• Move GitLab worklist requests to core’s forge REST client, removing the runner’s glab dependency.
Diagram

graph TD
  Installer["Install doctor"] --> Doctor["Admin doctor"] --> Preflight["Core preflight"]
  Pack["Enabled assets"] --> Preflight
  Router["Worklist builder"] --> Gate{"Gitleaks ready?"} --> Worklist["Rescan worklist"]
  Forge["Forge REST"] --> Worklist
  Preflight -. "shared tool checks" .-> Gate
Loading
High-Level Assessment

Keep the shared core checks and forge client. Retaining a separate shell doctor or glab transport would preserve divergent failure and credential-handling paths; neither offers a clear advantage for this change.

Files changed (20) +632 / -369

Enhancement (2) +209 / -130
build_rescan_worklist.pyPreflight gitleaks and use forge REST for GitLab +70/-69

Preflight gitleaks and use forge REST for GitLab

• Refuses networked tripwire runs without gitleaks before worklist stages begin. Replaces glab calls with core’s ForgeApi and maps REST authentication and transport failures to per-repository statuses.

src/traust/cli/build_rescan_worklist.py

doctor.pyReport enabled-pack preflight findings as failures +139/-61

Report enabled-pack preflight findings as failures

• Checks enabled security-pack requirements through core, reports every finding as FAIL, and makes --config-home apply to the preflight audit. Shows convention defaults and removes forge CLI authentication probes.

src/traust/cli/doctor.py

Bug fix (1) +5 / -10
registry.pyRefuse spend declarations without a registry +5/-10

Refuse spend declarations without a registry

• Makes a missing model-registry.yaml an exit-1 failure with a copy instruction. Removes the optional HARNESS_REQUIRE_SPEND_LEDGER behavior.

src/traust/cli/groups/registry.py

Refactor (1) +11 / -116
install_traustDelegate installation doctor to the CLI +11/-116

Delegate installation doctor to the CLI

• Removes the separate shell checks for files, configuration, completeness, and toolchain health. Doctor mode now invokes traust admin doctor using the selected Python interpreter.

scripts/install_traust

Tests (7) +371 / -77
test_build_rescan_worklist.pyCover GitLab REST statuses and upfront scanner refusal +113/-26

Cover GitLab REST statuses and upfront scanner refusal

• Tests GitLab payload normalization, forge host and token wiring, and per-repository REST failures. Verifies that missing gitleaks prevents output and that --no-tripwire skips that requirement.

tests/test_build_rescan_worklist.py

test_doctor.pyCompare doctor failures with the engine’s start gate +185/-7

Compare doctor failures with the engine’s start gate

• Tests matching findings for the same pack and config home, including disabled assets. Covers fresh-example onboarding, required-key failures, and visible convention defaults.

tests/test_doctor.py

test_install_traust.pyTest delegated installer doctor behavior +13/-27

Test delegated installer doctor behavior

• Replaces shell-doctor expectations with a check that a fresh install reports CLI preflight failures and exits nonzero.

tests/test_install_traust.py

test_portfolio_observe.pyAdapt observation fixtures to core’s scanner requirement +48/-2

Adapt observation fixtures to core’s scanner requirement

• Supplies a compare patch and a clean gitleaks provider so changed-repository observation tests exercise the pinned core without depending on a local scanner.

tests/test_portfolio_observe.py

test_registry_models_spend.pyAssert missing registries refuse spend +9/-13

Assert missing registries refuse spend

• Replaces optional-skip cases with an exit-1 assertion, the prescribed fix, and confirmation that no spend is recorded.

tests/test_registry_models_spend.py

test_security_router.pyRemove an obsolete tripwire tool mock +0/-1

Remove an obsolete tripwire tool mock

• Stops mocking the former mid-run gitleaks availability check in router decision tests.

tests/test_security_router.py

test_traust_example.pyCompare effective example configuration values +3/-1

Compare effective example configuration values

• Compares serialized configuration values because the pinned core also tracks which keys the file supplied.

tests/test_traust_example.py

Documentation (7) +30 / -30
AGENTS.mdIdentify admin doctor as the installation check +2/-1

Identify admin doctor as the installation check

• Clarifies that install_traust --doctor runs the same check as traust admin doctor, while config migration remains separate.

AGENTS.md

continuous-operations.mdDocument REST-based GitLab routing and required tripwire +4/-4

Document REST-based GitLab routing and required tripwire

• Updates the worklist transport and credential guidance for GitLab. States that a run requiring the tripwire refuses before building a worklist if gitleaks is absent.

docs/continuous-operations.md

external-dependencies.mdRemove the rescan router from glab consumers +1/-1

Remove the rescan router from glab consumers

• Retains glab for MR automation but no longer lists it as a rescan-router dependency.

docs/external-dependencies.md

model-classes.mdDescribe ownership of the required model registry +6/-12

Describe ownership of the required model registry

• Replaces the installer’s divergence-warning guidance with guidance for maintaining a deployment copy and diagnosing a missing registry through doctor.

docs/model-classes.md

model-routing.mdDocument mandatory spend registry +4/-4

Document mandatory spend registry

• Removes the embedded-environment skip policy and explains that spend declarations refuse a missing model registry.

docs/model-routing.md

setup.mdDocument one doctor path for onboarding +8/-7

Document one doctor path for onboarding

• Explains that the installer delegates to admin doctor and that unmet preflight requirements fail rather than remain warnings.

docs/setup.md

admin.pyRefresh admin doctor command help +5/-1

Refresh admin doctor command help

• Describes doctor in terms of pack requirements, configuration, tools, network, and gates rather than forge CLI authentication.

src/traust/cli/groups/admin.py

Other (2) +6 / -6
pyproject.tomlPin core to its preflight revision +1/-1

Pin core to its preflight revision

• Advances the traust-core Git pin to the revision supplying preflight and requirement support.

pyproject.toml

uv.lockLock updated core and contracts revisions +5/-5

Lock updated core and contracts revisions

• Records the new traust-core pin and its transitive traust-contracts revision.

uv.lock

@qodo-for-traust-security

qodo-for-traust-security Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Code Review by Qodo


View medium (4)
Remediation recommended
1. One forge outage skips other repositories 🐞 Bug
Description
stage1 uses one _BanBreaker for every GitHub host, even though its requests now target separate
enterprise and public hosts. After a failure streak on one host trips the breaker, repositories on
another host are marked ban-suspected without a request and cannot enter the compare stage.
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Stage 1 shares its GitHub failure breaker across independent hosts, so an outage on one host suppresses requests to another.

## Fix Focus Areas
- src/traust/cli/build_rescan_worklist.py[1992-2013]
- tests/test_build_rescan_worklist.py[424-465]

## Recommended Fix
Keep a separate breaker for each GitHub hostname and check and record against the row's host. Add a regression test where one host trips its breaker while a repository on another host is still fetched.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

2. Doctor passes a home with no model registry 🐞 Bug
Description
check_config only adds the new model-registry.yaml missing FAIL when found is true, meaning a
traust.yaml exists in the home. A zero-config home (the directory exists but has no traust.yaml)
that also has no registry therefore gets no FAIL from doctor, yet traust registry models spend now
exits 1 on exactly that home. The shell doctor this PR removes failed on a missing registry whether
or not traust.yaml was there.
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Doctor reports a missing model-registry.yaml only when traust.yaml exists. A zero-config home without a registry passes doctor, but `traust registry models spend` refuses on it.

## Fix Focus Areas
- src/traust/cli/doctor.py[229-232]

## Recommended Fix
Change the condition from `if found and not (home / "model-registry.yaml").is_file()` to `if home is not None and home.is_dir() and not (home / "model-registry.yaml").is_file()`. Add a test in tests/test_doctor.py for a home that has neither traust.yaml nor model-registry.yaml and assert the FAIL row appears.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

3. Malformed packs prevent doctor reports 🐞 Bug
Description
check_preflight calls pack_requirements() without handling failures from reading, parsing,
validating, or constructing requirements from the security pack. If that pack is missing or
malformed, traust admin doctor --json raises before it can emit its failure rows or JSON report.
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A missing or invalid security pack escapes the new preflight check, preventing doctor from returning its normal failure report.

## Fix Focus Areas
- src/traust/cli/doctor.py[127-148]
- src/traust/cli/doctor.py[383-409]
- tests/test_doctor.py[171-207]

## Recommended Fix
Catch expected pack read, YAML, and validation errors at the preflight-check boundary and append a FAIL row with a safe, actionable reason. Add a regression test asserting a nonzero exit and valid JSON for an invalid pack.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

4. Enterprise quota outages spoil worklists 🐞 Bug
Description
main checks only github.com's remaining quota before stage 1, while the new enterprise-host path
proceeds without checking that host's quota. When an enterprise host has too little quota for its
repositories, stage-1 failures become per-repository errors rather than an up-front refusal, and
those repositories never reach change comparison.
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The stage-1 quota guard excludes GitHub Enterprise hosts even though the router now queries them, allowing a quota-starved host to produce an incomplete worklist.

## Fix Focus Areas
- src/traust/cli/build_rescan_worklist.py[533-550]
- src/traust/cli/build_rescan_worklist.py[2238-2268]
- tests/test_build_rescan_worklist.py[424-465]

## Recommended Fix
Read remaining quota for each GitHub host represented in the population and compare it with that host's stage-1 demand before starting the sweep. Preserve independent budgets in later quota-aware stages, and test a quota-starved enterprise host alongside a healthy github.com host.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


Comment thread scripts/install_traust Resolved
Comment thread src/traust/cli/build_rescan_worklist.py Resolved
Comment thread src/traust/cli/doctor.py
Comment on lines -50 to -51
("gh", False),
("glab", False),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Medium

3. Doctor stops checking gh, which the router still needs 🐞 Bug ≡ Correctness

The PR removes ("gh", False) from TOOLS and deletes check_auth, on the grounds that no forge
CLI is required any more. But build_rescan_worklist still runs gh api for the GitHub rate-limit
check, stage 1, stage 2, the tree count and the tripwire patch fetch, so a runner with no gh or an
unauthenticated one now passes doctor with no row at all and then marks every GitHub repo as an
error mid-run.
Agent Prompt
## Issue description
Doctor no longer reports `gh` presence or auth, but the rescan router's GitHub stages still call `gh api`.

## Fix Focus Areas
- src/traust/cli/doctor.py[57-70]
- src/traust/cli/doctor.py[305-310]

## Recommended Fix
Put `("gh", False)` back in `TOOLS` and restore a gh-only auth probe (`gh auth status`, or a check that `GITHUB_TOKEN` is set) under the non-`--quick` path. Leave `glab` out, since it is no longer used by the router.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially addressed; leaving this thread open. The manual worklist now refuses before stage 1 (exit 2 with install/auth guidance) if GitHub rows need gh but the CLI is missing; GitLab-only/pack runs do not require it. However traust admin doctor still does not check gh or its authentication, and unauthenticated GitHub worklist calls can still fail per row. I corrected the PR description rather than claim full doctor readiness. A conditional auth/readiness policy is still needed before this finding is fully resolved.

Comment thread src/traust/cli/doctor.py Resolved
Comment thread scripts/install_traust Resolved
Comment thread src/traust/cli/build_rescan_worklist.py Resolved
Comment thread scripts/install_traust Resolved
@smith-xyz

Copy link
Copy Markdown
Contributor Author

Qodo round 2 — the three new findings, resolved (commit 9d44f66):

  1. "GitHub Enterprise repos queried on github.com" — real, fixed. Every gh api fetcher (gh_repo_info, gh_compare, gh_tree_fp_count, gh_compare_patch) now takes the row's host and names it with gh api --hostname <host> when it isn't github.com, so a GHE repo (classified github-kind via $GIT_HOSTS / traust.yaml forge_hosts) reads its own API — never github.com's data for the same org/name, and never a false error. github.com's own calls stay flagless (gh's default target), so nothing changes for it. The quota reads (gh api rate_limit) are github.com's, so its gates — the up-front preflight estimate, stage-2 deferral, the tripwire and preroute budgets — now count only github.com rows; an enterprise host runs its own limits. Tests: test_a_ghe_host_routes_its_own_api_not_github_com (argv-level --hostname on all four fetchers + stage 1 host threading) and test_github_com_quota_never_gates_an_enterprise_row.

  2. "Scanner check ignores the chosen config home" — real, fixed. The --toolchain PROFILE doctor in do_doctor now forwards --config-home exactly like the preflight call above it, so traust admin toolchain doctor loads the home the wrapper was given instead of TRAUST_CONFIG_HOME/the default (which could report on a different installation, or exit 2 on a home that doesn't resolve while the chosen one is fine). Test: test_doctor_with_toolchain_checks_the_config_home_the_wrapper_was_given (a broken TRAUST_CONFIG_HOME beside a fine chosen home).

  3. "Doctor checks a relative home in the wrong folder" — real, fixed. do_doctor resolves the home once at the wrapper's boundary — ~ expanded, a relative path anchored to the caller's directory — before the subshells cd into $REPO_ROOT, so scripts/install_traust --doctor --config-home ./myhome run from outside the repo audits the caller's home, not a $REPO_ROOT-shaped path that doesn't exist (do_install absolutizes the same way, with cd && pwd). Test: test_doctor_resolves_a_relative_config_home_against_the_callers_cwd (run from a directory outside the repo; the report names the caller's home).

Gates: make lint-fix, make test (2725 passed, 86 skipped), consumer ratchet clean.

@openshift-ci openshift-ci Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Oct 31, 2026
@smith-xyz

Copy link
Copy Markdown
Contributor Author

checking if this is resolved

@smith-xyz
smith-xyz requested a review from chubirka October 31, 2026 14:09
core#312 (e8ab152) carries Context.preflight, PreflightError and the
requires: vocabulary (contracts#102 at 55b69a9, transitively). Two tests
adapt to the pinned core: Config now records which keys traust.yaml set
(the example compares by model_dump), and a changed repository's
observation scans its patch for secrets (gitleaks required), so the
observe fixture answers the compare patch and registers a clean scanner.
… delegates (XWING-2440)

traust admin doctor now runs Context.preflight on the security pack's
enabled assets — the same door the engine's start gate refuses on — and
every finding is a FAIL row naming its fix (a required gap was a WARN
before). The convention paths the plan keeps soft are shown as defaults
in use (feeds cache chain, GIT_HOSTS, bicep), the stale gh/glab PATH and
auth checks go (no forge CLI is required anymore), and --config-home
wins for the env-resolved audit too. scripts/install_traust --doctor
calls traust admin doctor instead of running its own shell checks; the
separate check path (file parsing, retired files, load smoke,
completeness, toolchain doctor) is gone.
…y (XWING-2440)

The registry is required: models spend exits 1 naming the fix (copy
config/model-registry.yaml into the config home) instead of skipping the
declaration. The HARNESS_REQUIRE_SPEND_LEDGER env switch and the PENDING
marker go.
…up front (XWING-2440)

The GitLab stages read through core's ForgeApi (no glab CLI on the
runner): the host must be a known forge (traust.yaml forge_hosts plus
$GIT_HOSTS) and its token comes from the forge_tokens env var named for
it, sent only as a header. gitleaks is required — the run refuses before
the worklist builds (core's tool preflight, requires: tools semantics),
never scanner_unavailable mid-run.
…2440)

Qodo findings on the delegation and the forge REST move:

- install_traust --doctor forwards --config-home (the wrapper's flag
  named a home the delegated doctor ignored, auditing
  TRAUST_CONFIG_HOME/default instead) and honours --toolchain PROFILE
  again: the profile's scanner doctor (traust admin toolchain doctor)
  runs beside the preflight, so a degraded profile cannot pass silently.
- A GitLab host named only in traust.yaml's forge_hosts now classifies:
  split_repo_url takes the known-host mapping, and main threads
  _known_hosts(core) — the same merge the forge client gets — through
  stage 1, the quota estimate and the no-network branch, so a configured
  host routes instead of landing unsupported-host.
- The $GIT_HOSTS comment tells the truth: it widens reach, not just
  recognition (the hosts feed the forge client's set).
…nt (XWING-2440)

The two Qodo findings the doctor PR owns:

- repo_state declares `requires: {tools: [gitleaks]}`: core's
  observation scans each change's patch for secrets and fails without
  the scanner, so the engine's start gate and doctor now refuse on it
  before any work (a FAIL row naming the install pin, never a WARN).
  Doctor's tests pin the scanner side with a stubbed-PATH fixture so
  the config-row assertions stay machine-independent, and one test
  proves the missing-gitleaks FAIL row on a complete home.
- The rescan worklist refuses up front (exit 2, fix named) when GitHub
  repos are in the population and gh is missing: the router is a
  hand-run lane, not a pack asset, so there is no requires row to hang
  the CLI on — the door-level refusal is its home, the same convention
  as the gitleaks preflight. Without GitHub repos the run proceeds
  (GitLab hosts go through core's forge REST).
…XWING-2440)

The three new Qodo findings on the doctor PR:

- A GitHub Enterprise host classified github-kind queried github.com:
  every gh fetcher (stage 1/2, the tree count, the tripwire patch) now
  takes the row's host and names it (gh api --hostname), so a GHE repo
  reads its own API, never github.com's data for the same org/name;
  github.com's calls stay flagless (gh's default target). The quota
  reads are github.com's, so its gates (the preflight estimate,
  stage-2 deferral, the tripwire and preroute budgets) count only
  github.com rows — an enterprise host runs its own limits.
- The delegated doctor's --toolchain check loads the chosen home too:
  the scanner doctor forwards --config-home like the preflight above
  it, so it never reports on TRAUST_CONFIG_HOME instead (or exit 2 on
  a home that doesn't resolve while the chosen one is fine).
- A relative --config-home resolves against the caller's directory,
  not the repo root the doctor's subshell cds into: the wrapper
  absolutizes it once at its boundary (do_install absolutizes the same
  way), so the CI gate audits the home the caller named.
@smith-xyz
smith-xyz force-pushed the feat/fail-loudly-on-what-s-required-one-preflight-a-l branch from 6d4a012 to d5e9adf Compare November 2, 2026 02:04
@smith-xyz

Copy link
Copy Markdown
Contributor Author

Rebase/comment follow-up for d5e9adf (onto upstream development bdb72e9): the eight unique PR commits remain, the Core pin now points to reachable merged e01d4202, and the never-audited observation test follows Core’s merged first-head behavior. make lint-fix, full make test (2,757 passed, 86 skipped), commit/push hooks passed; CI for this head is running.

Relevant Qodo dispositions, with direct replies on the inline threads:

  • Fixed: chosen/relative config home and requested scanner-profile doctor are both forwarded to the same home; configured GitLab hosts share classifier and ForgeApi mapping; enabled repo_state now requires gitleaks, making missing-scanner preflight a FAIL; the $GIT_HOSTS comment now states that it widens REST reach. Each has a focused regression and its inline explanation; those five verified bot threads are resolved.
  • Fixed (summary-only): GitHub Enterprise fetchers use gh api --hostname for the repository’s host, and github.com quota checks exclude Enterprise rows (test_a_ghe_host_routes_its_own_api_not_github_com, test_github_com_quota_never_gates_an_enterprise_row). The selected-home scanner check and relative-home issue are covered by the wrapper tests and inline replies.
  • Not a fix / dismissed: the missing-config-home report is incorrect: the delegated doctor still fails with the install guidance, and test_doctor_fails_closed_when_unset_and_default_absent passes. Reintroducing a second shell check would duplicate the shared preflight.
  • Partially addressed; left open: the manual worklist now fails before stage 1 if GitHub rows need gh and it is missing, but traust admin doctor still does not verify gh/auth and unauthenticated GitHub calls may still fail per row. See the unresolved doctor/gh thread. I did not treat Qodo’s automated “implemented” label as proof of full doctor readiness.

Michele’s review of the changed tests, behavior and security gates is still requested; no merge/auto-merge was enabled.

@qodo-code-review

qodo-code-review Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. An unchanged doctor test fails in CI ✓ Resolved
Description
do_doctor no longer emits the install instruction when both TRAUST_CONFIG_HOME and the default
home are absent, but test_doctor_fails_closed_when_unset_and_default_absent still requires that
exact text. The test invokes this changed wrapper path and asserts that its stdout contains `run
scripts/install_traust`, so the assertion fails regardless of whether the delegated preflight exits
nonzero.
Code

scripts/install_traust[R220-221]

+  (cd "$REPO_ROOT" && "$py" -m traust.cli admin doctor \
+      ${home:+--config-home "$home"}) || rc=1
Evidence
The changed wrapper directly delegates to the admin command instead of running the removed location
check. The unchanged test still asserts the old location-check phrase, which does not appear in the
delegated doctor implementation.

scripts/install_traust[200-227]
tests/test_install_traust.py[53-60]
src/traust/cli/doctor.py[354-386]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The doctor wrapper removed its missing-home instruction, while an unchanged test still requires that instruction in stdout.
## Fix Focus Areas
- scripts/install_traust[216-227]
- tests/test_install_traust.py[53-60]
## Recommended Fix
Decide the intended missing-home diagnostic and keep the wrapper and test consistent. If delegation is intended, update the test to assert the delegated doctor’s nonzero result and actionable preflight findings rather than the removed shell message.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Relative config homes inspect wrong files ✓ Resolved
Description
do_doctor absolutizes CONFIG_HOME but leaves a relative TRAUST_CONFIG_HOME environment value
unchanged before changing to REPO_ROOT. When a caller sets TRAUST_CONFIG_HOME=cfg outside the
repository and omits --config-home, both doctor commands inspect the repository-relative cfg
instead of the caller’s configuration.
Code

scripts/install_traust[R216-218]

+  local home="${CONFIG_HOME/#\~/$HOME}"
+  if [[ -n "$home" && "$home" != /* ]]; then
+    home="$PWD/$home"
Evidence
Only the flag-derived CONFIG_HOME is normalized, and the child commands run after `cd
"$REPO_ROOT"`. With no explicit flag, the CLI receives no home argument and resolves the unchanged
environment value from that new working directory; the existing relative-path test covers only the
flag.

scripts/install_traust[212-225]
src/traust/cli/doctor.py[354-365]
src/traust/context.py[100-121]
tests/test_install_traust.py[126-142]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The doctor wrapper changes directory before resolving a relative config home supplied through the environment, so it can inspect a different installation.
## Fix Focus Areas
- scripts/install_traust[212-225]
- tests/test_install_traust.py[126-142]
## Recommended Fix
At the wrapper boundary, select the explicit flag or the environment home and resolve either relative path against the caller’s working directory before invoking both commands. Add a wrapper test that sets a relative TRAUST_CONFIG_HOME from outside the repository without passing --config-home.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Enterprise quota failures skip rescan work ✓ Resolved
Description
The new Enterprise-host exclusions in main and stage2 rely on gh_rate_remaining(), which
queries only GitHub.com, without checking the Enterprise host’s quota. When that host exhausts its
quota, stage-two compare failures become ordinary error statuses and decide() can assign rule 7
with no lane to a repository whose push was detected.
Code

src/traust/cli/build_rescan_worklist.py[R2043-2045]

            i
            for i, e in enumerate(candidates)
-            if e["host_kind"] == "github" and e.get("pinned_sha")
+            if e["host_kind"] == "github" and _gh_host(e) == "github.com" and e.get("pinned_sha")
Evidence
The quota lookup has no host argument and calls gh api rate_limit without --hostname. The
changed candidate filter excludes Enterprise rows from deferral, while a failed compare leaves
change metrics unset; the decision table’s catch-all assigns no lane when those metrics are absent.

src/traust/cli/build_rescan_worklist.py[533-550]
src/traust/cli/build_rescan_worklist.py[2036-2073]
src/traust/cli/build_rescan_worklist.py[1398-1444]
src/traust/cli/build_rescan_worklist.py[2446-2465]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Enterprise compares bypass quota deferral without checking the Enterprise host’s own quota; a failed compare can leave a changed repository without a rescan lane.
## Fix Focus Areas
- src/traust/cli/build_rescan_worklist.py[533-550]
- src/traust/cli/build_rescan_worklist.py[2036-2065]
- src/traust/cli/build_rescan_worklist.py[2249-2256]
- tests/test_build_rescan_worklist.py[443-507]
## Recommended Fix
Query quota for each GitHub host and apply the initial gate and compare deferral against that host’s budget. Add a regression case with an exhausted Enterprise quota and an available GitHub.com quota; verify the changed Enterprise row is deferred rather than routed as no qualifying change.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Doctor passes on runners where the rescan router refuses for missing gh ✓ Resolved
Description
This PR removes ("gh", False) from doctor's TOOLS list and drops the gh auth status probe
(check_auth), so traust admin doctor no longer reports the GitHub CLI at all. At the same time,
build_rescan_worklist.main now refuses with exit 2 whenever the population has a GitHub repo and
shutil.which("gh") fails. A runner that doctor calls "environment ready" can therefore fail the
daily router on its first run.
Code

src/traust/cli/doctor.py[L50-51]

-    ("gh", False),
-    ("glab", False),
Evidence
Doctor's tool list no longer has gh, and check_auth is deleted. The router now treats gh as
required up front (new lines 2234-2242). docs/continuous-operations.md line 51 still names gh as
the stage-1/2 transport, so the claim that no forge CLI is required holds only for GitLab.

src/traust/cli/build_rescan_worklist.py[2229-2242]
src/traust/cli/doctor.py[56-70]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`traust admin doctor` no longer checks `gh`, but `build rescan-worklist` now refuses (exit 2) without it when any GitHub repo is in the population. Doctor can say the environment is ready on a runner the router will refuse.

## Fix Focus Areas
- src/traust/cli/doctor.py[56-70]
- src/traust/cli/build_rescan_worklist.py[2229-2242]

## Recommended Fix
Put `gh` back in doctor's checks, labelled as required by the rescan router for github-kind hosts. For example, keep it in `TOOLS` (WARN when absent) and add a short `gh auth status` probe outside `--quick`. Alternatively, state clearly in the doctor's output and in the docs that gh is a router prerequisite that doctor does not check.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
5. Missing model registry may no longer be caught by the doctor ✓ Resolved
Description
The removed shell do_doctor FAILed when $home/model-registry.yaml was missing. The `traust admin
doctor` it now delegates to has no check of its own for the registry: there is no reference to
model-registry/model_catalog in doctor.py, and every new doctor test copies the registry into
the home before running.
Code

scripts/install_traust[L237-239]

-    else
-      fail "$f missing — run install_traust to copy the shipped one"; failures=$((failures+1))
-    fi
Evidence
docs/model-classes.md and docs/setup.md (both changed here) say doctor names a missing registry as a
FAIL. Nothing in the traust repo implements that claim, and no test exercises it. It holds only if
the pinned core's preflight happens to check the registry, which I could not verify because core
isn't in the workspace. Meanwhile traust registry models spend now returns 1 without the registry,
so the gap would surface only at the end of a skill run.

src/traust/cli/doctor.py[139-147]
src/traust/cli/groups/registry.py[79-87]
tests/test_doctor.py[63-82]
docs/model-classes.md[173-178]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The shell doctor's missing-`model-registry.yaml` FAIL was removed. `traust admin doctor` has no equivalent check in this repo, but the docs now promise one.

## Fix Focus Areas
- src/traust/cli/doctor.py[139-147]
- tests/test_doctor.py[233-270]

## Recommended Fix
In `check_preflight` (or a sibling check), add `(FAIL, "model-registry.yaml missing — copy config/model-registry.yaml into the config home")` when `core.model_catalog is None`. Add a doctor test that omits the registry and asserts the FAIL row and the non-zero exit. If core's preflight already reports this, add a test that proves it instead.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

6. Forge-hosts row hides hosts set in traust.yaml ✓ Resolved
Description
check_defaults builds its 'extra forges' row only from $GIT_HOSTS. When that is empty it prints
'none — github.com/gitlab.com only', even though the router now reaches `{**core.config.forge_hosts,
**GIT_HOSTS} (_known_hosts). An operator who added a host to forge_hosts` in traust.yaml is told
no extra forge is in use.
Code

src/traust/cli/doctor.py[R166-172]

+    extra = [h.strip() for h in os.environ.get("GIT_HOSTS", "").split(",") if h.strip()]
+    results.append(
+        (
+            OK,
+            "extra forges (GIT_HOSTS): "
+            + (", ".join(extra) if extra else "none — github.com/gitlab.com only"),
+        )
Evidence
The router merges the config hosts with the env hosts (_known_hosts), but the doctor row reads
only the env variable.

src/traust/cli/build_rescan_worklist.py[245-254]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Doctor's forge row ignores `forge_hosts` from traust.yaml.

## Fix Focus Areas
- src/traust/cli/doctor.py[166-173]

## Recommended Fix
List `core.config.forge_hosts` (source: traust.yaml) and the `$GIT_HOSTS` entries (source: env) together. Print 'github.com/gitlab.com only' only when both are empty.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Old multi-file homes are no longer told to migrate ✓ Resolved
Description
Deleting RETIRED_FILES and the 'Retired files' section removes the only check that warned when
core.yaml, locations.yaml and the other retired files were still in the home. `traust admin
doctor` has no equivalent, so an un-migrated home now just shows unset-location FAILs and never
mentions traust config migrate.
Code

scripts/install_traust[L242-249]

-  info "3. Retired files"
-  local retired=""
-  for f in $RETIRED_FILES; do [[ -f "$home/$f" ]] && retired="$retired $f"; done
-  if [[ -n "$retired" ]]; then
-    warn "no longer read:$retired — fold them into traust.yaml with: traust config migrate $home"
-  else
-    ok "none left in the home"
-  fi
Evidence
doctor.py contains no reference to retired files or migrate. The removed shell block was the only
place that pointed an adopter with ignored legacy files to traust config migrate <home>.

src/traust/cli/doctor.py[346-386]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The retired-file check was dropped along with the shell doctor, and `traust admin doctor` doesn't replace it.

## Fix Focus Areas
- src/traust/cli/doctor.py[150-193]

## Recommended Fix
Add a WARN row in `check_defaults` (or `check_config`) that lists any retired file present in the resolved config home and suggests `traust config migrate <home>`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Linked PRs:
  🌐 PR #346
Review mode: Auto: 🚀 Fast: Localized test assertion update; review deletion and replacement in one path.
✅ Fix-loop round: fix loop

Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 4d6765b

Results up to commit d5e9adf 🧠 Deep


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. An unchanged doctor test fails in CI ✓ Resolved
Description
do_doctor no longer emits the install instruction when both TRAUST_CONFIG_HOME and the default
home are absent, but test_doctor_fails_closed_when_unset_and_default_absent still requires that
exact text. The test invokes this changed wrapper path and asserts that its stdout contains `run
scripts/install_traust`, so the assertion fails regardless of whether the delegated preflight exits
nonzero.
Code

scripts/install_traust[R220-221]

+  (cd "$REPO_ROOT" && "$py" -m traust.cli admin doctor \
+      ${home:+--config-home "$home"}) || rc=1
Evidence
The changed wrapper directly delegates to the admin command instead of running the removed location
check. The unchanged test still asserts the old location-check phrase, which does not appear in the
delegated doctor implementation.

scripts/install_traust[200-227]
tests/test_install_traust.py[53-60]
src/traust/cli/doctor.py[354-386]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The doctor wrapper removed its missing-home instruction, while an unchanged test still requires that instruction in stdout.
## Fix Focus Areas
- scripts/install_traust[216-227]
- tests/test_install_traust.py[53-60]
## Recommended Fix
Decide the intended missing-home diagnostic and keep the wrapper and test consistent. If delegation is intended, update the test to assert the delegated doctor’s nonzero result and actionable preflight findings rather than the removed shell message.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. Enterprise quota failures skip rescan work ✓ Resolved
Description
The new Enterprise-host exclusions in main and stage2 rely on gh_rate_remaining(), which
queries only GitHub.com, without checking the Enterprise host’s quota. When that host exhausts its
quota, stage-two compare failures become ordinary error statuses and decide() can assign rule 7
with no lane to a repository whose push was detected.
Code

src/traust/cli/build_rescan_worklist.py[R2043-2045]

            i
            for i, e in enumerate(candidates)
-            if e["host_kind"] == "github" and e.get("pinned_sha")
+            if e["host_kind"] == "github" and _gh_host(e) == "github.com" and e.get("pinned_sha")
Evidence
The quota lookup has no host argument and calls gh api rate_limit without --hostname. The
changed candidate filter excludes Enterprise rows from deferral, while a failed compare leaves
change metrics unset; the decision table’s catch-all assigns no lane when those metrics are absent.

src/traust/cli/build_rescan_worklist.py[533-550]
src/traust/cli/build_rescan_worklist.py[2036-2073]
src/traust/cli/build_rescan_worklist.py[1398-1444]
src/traust/cli/build_rescan_worklist.py[2446-2465]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Enterprise compares bypass quota deferral without checking the Enterprise host’s own quota; a failed compare can leave a changed repository without a rescan lane.
## Fix Focus Areas
- src/traust/cli/build_rescan_worklist.py[533-550]
- src/traust/cli/build_rescan_worklist.py[2036-2065]
- src/traust/cli/build_rescan_worklist.py[2249-2256]
- tests/test_build_rescan_worklist.py[443-507]
## Recommended Fix
Query quota for each GitHub host and apply the initial gate and compare deferral against that host’s budget. Add a regression case with an exhausted Enterprise quota and an available GitHub.com quota; verify the changed Enterprise row is deferred rather than routed as no qualifying change.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Relative config homes inspect wrong files ✓ Resolved
Description
do_doctor absolutizes CONFIG_HOME but leaves a relative TRAUST_CONFIG_HOME environment value
unchanged before changing to REPO_ROOT. When a caller sets TRAUST_CONFIG_HOME=cfg outside the
repository and omits --config-home, both doctor commands inspect the repository-relative cfg
instead of the caller’s configuration.
Code

scripts/install_traust[R216-218]

+  local home="${CONFIG_HOME/#\~/$HOME}"
+  if [[ -n "$home" && "$home" != /* ]]; then
+    home="$PWD/$home"
Evidence
Only the flag-derived CONFIG_HOME is normalized, and the child commands run after `cd
"$REPO_ROOT"`. With no explicit flag, the CLI receives no home argument and resolves the unchanged
environment value from that new working directory; the existing relative-path test covers only the
flag.

scripts/install_traust[212-225]
src/traust/cli/doctor.py[354-365]
src/traust/context.py[100-121]
tests/test_install_traust.py[126-142]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The doctor wrapper changes directory before resolving a relative config home supplied through the environment, so it can inspect a different installation.
## Fix Focus Areas
- scripts/install_traust[212-225]
- tests/test_install_traust.py[126-142]
## Recommended Fix
At the wrapper boundary, select the explicit flag or the environment home and resolve either relative path against the caller’s working directory before invoking both commands. Add a wrapper test that sets a relative TRAUST_CONFIG_HOME from outside the repository without passing --config-home.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Doctor passes on runners where the rescan router refuses for missing gh ✓ Resolved
Description
This PR removes ("gh", False) from doctor's TOOLS list and drops the gh auth status probe
(check_auth), so traust admin doctor no longer reports the GitHub CLI at all. At the same time,
build_rescan_worklist.main now refuses with exit 2 whenever the population has a GitHub repo and
shutil.which("gh") fails. A runner that doctor calls "environment ready" can therefore fail the
daily router on its first run.
Code

src/traust/cli/doctor.py[L50-51]

-    ("gh", False),
-    ("glab", False),
Evidence
Doctor's tool list no longer has gh, and check_auth is deleted. The router now treats gh as
required up front (new lines 2234-2242). docs/continuous-operations.md line 51 still names gh as
the stage-1/2 transport, so the claim that no forge CLI is required holds only for GitLab.

src/traust/cli/build_rescan_worklist.py[2229-2242]
src/traust/cli/doctor.py[56-70]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`traust admin doctor` no longer checks `gh`, but `build rescan-worklist` now refuses (exit 2) without it when any GitHub repo is in the population. Doctor can say the environment is ready on a runner the router will refuse.

## Fix Focus Areas
- src/traust/cli/doctor.py[56-70]
- src/traust/cli/build_rescan_worklist.py[2229-2242]

## Recommended Fix
Put `gh` back in doctor's checks, labelled as required by the rescan router for github-kind hosts. For example, keep it in `TOOLS` (WARN when absent) and add a short `gh auth status` probe outside `--quick`. Alternatively, state clearly in the doctor's output and in the docs that gh is a router prerequisite that doctor does not check.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
5. Missing model registry may no longer be caught by the doctor ✓ Resolved
Description
The removed shell do_doctor FAILed when $home/model-registry.yaml was missing. The `traust admin
doctor` it now delegates to has no check of its own for the registry: there is no reference to
model-registry/model_catalog in doctor.py, and every new doctor test copies the registry into
the home before running.
Code

scripts/install_traust[L237-239]

-    else
-      fail "$f missing — run install_traust to copy the shipped one"; failures=$((failures+1))
-    fi
Evidence
docs/model-classes.md and docs/setup.md (both changed here) say doctor names a missing registry as a
FAIL. Nothing in the traust repo implements that claim, and no test exercises it. It holds only if
the pinned core's preflight happens to check the registry, which I could not verify because core
isn't in the workspace. Meanwhile traust registry models spend now returns 1 without the registry,
so the gap would surface only at the end of a skill run.

src/traust/cli/doctor.py[139-147]
src/traust/cli/groups/registry.py[79-87]
tests/test_doctor.py[63-82]
docs/model-classes.md[173-178]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The shell doctor's missing-`model-registry.yaml` FAIL was removed. `traust admin doctor` has no equivalent check in this repo, but the docs now promise one.

## Fix Focus Areas
- src/traust/cli/doctor.py[139-147]
- tests/test_doctor.py[233-270]

## Recommended Fix
In `check_preflight` (or a sibling check), add `(FAIL, "model-registry.yaml missing — copy config/model-registry.yaml into the config home")` when `core.model_catalog is None`. Add a doctor test that omits the registry and asserts the FAIL row and the non-zero exit. If core's preflight already reports this, add a test that proves it instead.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational
6. Forge-hosts row hides hosts set in traust.yaml ✓ Resolved
Description
check_defaults builds its 'extra forges' row only from $GIT_HOSTS. When that is empty it prints
'none — github.com/gitlab.com only', even though the router now reaches `{**core.config.forge_hosts,
**GIT_HOSTS} (_known_hosts). An operator who added a host to forge_hosts` in traust.yaml is told
no extra forge is in use.
Code

src/traust/cli/doctor.py[R166-172]

+    extra = [h.strip() for h in os.environ.get("GIT_HOSTS", "").split(",") if h.strip()]
+    results.append(
+        (
+            OK,
+            "extra forges (GIT_HOSTS): "
+            + (", ".join(extra) if extra else "none — github.com/gitlab.com only"),
+        )
Evidence
The router merges the config hosts with the env hosts (_known_hosts), but the doctor row reads
only the env variable.

src/traust/cli/build_rescan_worklist.py[245-254]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Doctor's forge row ignores `forge_hosts` from traust.yaml.

## Fix Focus Areas
- src/traust/cli/doctor.py[166-173]

## Recommended Fix
List `core.config.forge_hosts` (source: traust.yaml) and the `$GIT_HOSTS` entries (source: env) together. Print 'github.com/gitlab.com only' only when both are empty.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Old multi-file homes are no longer told to migrate ✓ Resolved
Description
Deleting RETIRED_FILES and the 'Retired files' section removes the only check that warned when
core.yaml, locations.yaml and the other retired files were still in the home. `traust admin
doctor` has no equivalent, so an un-migrated home now just shows unset-location FAILs and never
mentions traust config migrate.
Code

scripts/install_traust[L242-249]

-  info "3. Retired files"
-  local retired=""
-  for f in $RETIRED_FILES; do [[ -f "$home/$f" ]] && retired="$retired $f"; done
-  if [[ -n "$retired" ]]; then
-    warn "no longer read:$retired — fold them into traust.yaml with: traust config migrate $home"
-  else
-    ok "none left in the home"
-  fi
Evidence
doctor.py contains no reference to retired files or migrate. The removed shell block was the only
place that pointed an adopter with ignored legacy files to traust config migrate <home>.

src/traust/cli/doctor.py[346-386]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The retired-file check was dropped along with the shell doctor, and `traust admin doctor` doesn't replace it.

## Fix Focus Areas
- src/traust/cli/doctor.py[150-193]

## Recommended Fix
Add a WARN row in `check_defaults` (or `check_config`) that lists any retired file present in the resolved config home and suggests `traust config migrate <home>`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 36e6e0b

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 4d6765b

@smith-xyz
smith-xyz merged commit f352cfa into traust-security:development Nov 2, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant