Skip to content

chore(vehicle): rest consolidation train — #17107, #17119, #17122, #17117, #17129 in one CI run (#17128) - #17133

Merged
mrveiss merged 122 commits into
mainfrom
vehicle-v090-2026-09-19-rest
Sep 19, 2026
Merged

mrveiss merged 122 commits into
mainfrom
vehicle-v090-2026-09-19-rest

Conversation

@mrveiss

@mrveiss mrveiss commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Thinking Path

The owner, 2026-09-19: "fix all red, review and merge, check if any are consolidation candidates", with "merge the existing merge trains before producing new ones". After #17048 turned main green, the approved non-security trains had to land. Landing them one at a time would cost one CI cycle each through the FIFO python-suite lanes, so they are merged server-side into this one branch instead: one CI run proves the union. The migrations (#17072, then #17125) stay separate as their own risk class. The security trains ride vehicle-v090-2026-09-19-security2.

What Changed

This branch was cut from main after #17048. Each member was merged server-side at its carried head, and all five merges were clean.

PR Branch Carried head Closes Review
#17107 vehicle-v090-2026-09-19-7b 74dc31be94 #16947, #16949, #16975, #16986 approve@74dc31be9
#17119 vehicle-v090-2026-09-19-77 e1533c96c1 #15603, #16020, #16711, #16712, #16722, #17006, #17038, #17039 approve@e178c020e
#17122 vehicle-v090-2026-09-19-bb 4ecfcd132e #13859 approve@45fa6886f
#17117 issue-16310-unshallow 9fc53562aa approve@dcc111fda
#17129 batching-check-advisory 75878df3df #17128 approve@bf46304b3

On top of that there is one commit, 760c54bd0b (#17128): the owner rule "finish what you started — append before you open" added to CLAUDE.md, next to the same-file rule.

#17122 wrote its closing keywords in the comma form (Closes #13859, #16784, #16985), which links only the first issue. The two it missed are listed here on their own lines; their ACs were reviewed on #17103 (#16784) and #17016 (#16985).

Closes #13859
Closes #15603
Closes #16020
Closes #16711
Closes #16712
Closes #16722
Closes #16947
Closes #16949
Closes #16975
Closes #16986
Closes #17006
Closes #17038
Closes #17039
Closes #17128
Closes #16784
Closes #16985
Refs #16948
Refs #16429
Refs #16310

Verification

  • Each member carries a ledger review verdict (table). Deltas since an approved head are main-merges only, except chore(vehicle): v0.9.0 2026-09-19 -- deploy/reconciler/orphan-detector/pre-push batch (#77) #17119 (the conflict resolution with main at e1533c96c1, plus the sandbox fix in pre_push_open_pr_cap_17006_test.py) and the CLAUDE.md commit. Those get their own review on this PR.
  • The pre-push hook passed on push with the CI-parity interpreter.
  • The CI of this PR is the proof of the union.

Model Used

Claude Opus 5 (coordinator)

mrveiss and others added 30 commits September 12, 2026 11:58
…ecretsManager (#16429)

SecretAuditLog.vue, SecretVault.vue and ShareSecretDialog.vue had zero
callers outside their own stories files. Triaged each individually rather
than deleting on sight:

- SecretAuditLog: genuine gap, not a duplicate. useSecretsAuditApi reads a
  real GET /api/audit/logs (admin-only, api/audit.py), which api/secrets.py
  already writes to on every secret op. Wired into the Secrets page as a
  new admin-only tab (views/secrets/AuditLogView.vue, secrets-audit-log
  route), matching the LLM API Keys tab's existing admin gate.

- SecretVault: same backend as SecretsManager.vue (secretsApiClient, same
  /api/secrets/ routes) -- a superseded parallel implementation. Its one
  net-new capability, virtual scrolling for 100+ items (#4037), is ported
  into SecretsManager.vue's list view (grid view keeps plain rendering --
  CSS grid's auto-fill column count depends on runtime container width,
  which useVirtualList's fixed-row model doesn't represent, and SecretVault
  itself never had a grid mode to port a solution from). Retired in this
  PR now that parity is shown.

- ShareSecretDialog: depends on useSessionCollaboration, which has 5
  sibling components in components/collaboration/ that are themselves
  unwired -- a full real-time collaborative-session UI (#608 phases 5-7,
  closed / #874 phase 6) built end-to-end and never connected to any view.
  Filed as its own implementation gap, #16443 (sub-issue of #16425,
  blocked_by edge from #16429). Left unticked here.

Also fixed while mapping SecretVault's scope vocabulary onto the backend's
canonical values: the create/edit form's Scope select offered user/session/
shared options that api/schemas_system.py's SecretCreateRequest.scope
(typed ChatSecretScope) would reject with a 422 -- only general/chat were
ever valid. Filed #16450 for a larger, separate finding from the same
investigation: the Visibility/Organization/Team/Shared-With controls in the
same form have no server-side effect at all (SecretCreateRequest doesn't
carry them onto the persisted model).
…est never re-imports the real package over it (#16722)
…tes, so Package.setup doesn't read an invented pytest_plugins (#16722)

CI at 9251f0d failed shards 5 and 7 with 59 setup errors across the services/ tests, all "pytest.UsageError: Plugins may be specified as a sequence ... Got: <MagicMock name='mock.pytest_plugins'>".

The cause: now that the stub is kept, Package.setup imports services/__init__.py through importtestmodule, and importlib mode returns the stub already in sys.modules. pytest then reads pytest_plugins (via consider_module) and setUpModule, setup_module, tearDownModule and teardown_module off it, and MagicMock invents all five. The stub now carries what a plain module without them gives: [] and None. The regression test drives importtestmodule itself.
…k with a __path__ (#16722)

Code review of 52a3638 (HIGH): a MagicMock given a __path__ still invents every attribute. So `from services import manifest_loader`, `jwks_verifier`, `frontend_bundle_health` and the rest bound invented mocks instead of importing the real submodule. And tests/services/conftest.py's _ensure_real_pkg guard now short-circuited on that __path__, so the hollow-package swap it used to do never happened. The package-level UsageError in the last red run hid those tests.

The root conftest now installs a hollow types.ModuleType("services") over the real directory, the shape tests/services/conftest.py already uses, and binds each child stub onto it. It invents nothing, so pytest's Package.setup attribute reads and `from services import` both behave as they would on a real package, and the explicit pytest_plugins and xunit-hook attributes are no longer needed. The regression test checks the shape instead of MagicMock identity: tests/services modules install their own hollow package at import time. It also checks that `from services import purge_playbook`, an unstubbed submodule, loads the real file.
…gisters onto the parent, so the two never diverge (#16722)
…hem (#16784)

`extract_document` dispatched pdf and docx and fell through to
`extract_plain_text` for everything else, so a spreadsheet, presentation or
OpenDocument file was decoded as if its ZIP bytes were text. The result was
binary noise presented as document text, flowing onward into the knowledge base
and into prompts through retrieval. Pre-existing rather than a regression:
before #16773 these formats were detected as "text" and took the same branch,
which is why nothing looked wrong.

`utils/document_parser.py` already owns working parsers for all six formats,
so this delegates rather than forking a second implementation. The import is
function-local on purpose: that module imports FROM this one, so a module-level
import would be a cycle.

Failure raises rather than returning empty text, so the pipeline's existing
handling names the format instead of reporting success with nothing in it. A
missing parser library is checked with find_spec BEFORE delegating, because the
shared sync parser catches every exception per candidate and reports one
"extraction failed" string — without the check, a deployment gap would be
reported as a bad upload, the exact conflation the pipeline's two error paths
exist to prevent.

The tests assert a NEGATIVE property: for these formats extract_document must
never return format="text". A suite covering only healthy documents would
still pass if the fall-through came back, so every case uses a file that cannot
be parsed and asserts it raises rather than degrades.

extraction.py is left at 590 of its 600-line limit; six lines came back by
reflowing over-wrapped comments so the file is not parked at the edge.
…t kinds (#16947)

Discovery today is four unconnected registries (#6828), none of them a live
named "who is here and are they busy" view: AgentHealthRegistry tracks
AI-stack health not activity, DistributedAgentManager tracks distributed
task assignment, agent-terminal's SessionManager holds live sessions with
no shared identity shape, and AgentOrgNode's heartbeat state is a poll, not
live. Sessions had no registry at all.

- protocols/agent_kind.py: AgentKind (COMPANY_OS, AI_STACK, SESSION,
  EXTERNAL), split from agent_communication.py to avoid growing a file
  already at its size-ratchet ceiling, and to avoid a circular import with
  the new presence module.
- protocols/agent_communication.py: AgentIdentity gains kind/name/tenant_id
  (the shared identity model from #16946) -- additive, defaults preserve
  every existing caller's behavior exactly. Net zero growth against the
  805-line ratchet ceiling: three restating-the-obvious comments removed
  (two in the CLI demo block, one duplicate `# noqa: E402`) to offset the
  new fields.
- protocols/agent_presence.py: AgentPresenceRegistry, in-memory and
  TTL-bounded (AUTOBOT_AGENT_PRESENCE_TTL_SECONDS, default 90s) --
  (kind, tenant_id, name) -> busy/idle. report() rejects a different
  instance_id claiming a live name (squat protection, #16946 §2) but lets
  the same instance heartbeat freely, and lets a new instance reclaim a
  name once the old one goes stale. EXTERNAL is refused outright -- owner
  decision 3: external A2A peers get identity for attribution only, never
  discovery.
- protocols/agent_presence_feeds.py: pull adapters for each kind's own
  authoritative source (never a caller-declared blob, per #16946 §2) --
  Company OS from AgentOrgNode + latest heartbeat run status (mirrors
  llc/api/agents.py's own join), AI-stack from AgentHealthRegistry (busy
  honestly reported as unknown/idle -- DistributedAgentManager's
  active_tasks would refine this, left as a follow-up rather than guessed),
  sessions from agent-terminal's SessionManager via has_running_task().
- 19 new tests: collision rejection, same-instance heartbeat, stale-name
  reclaim, TTL expiry (entries actually pruned from the live table, not
  just filtered at read time), EXTERNAL refusal, deregister ownership, and
  each feed adapter against a fake of its own source.

Lands the identity type and the registry interface first, as instructed --
#16948 and #16949 (both blocked_by this) can now build against a real
interface. The AI-stack busy signal and any push-based (non-pull) feed
wiring are explicitly left as follow-ups, not silently dropped.

Refs #16947
#16947)

Review found tenant_id=None doing two jobs: shared infrastructure (correct
for AI-stack) and unknown (wrong for a Company OS node with no company_id,
and for every session unconditionally) -- an agent whose tenant couldn't be
determined was becoming visible to every tenant's query.

- New UNKNOWN_TENANT sentinel, distinct from None. Never returned by
  list_live() under any argument, including the sentinel itself passed
  directly -- fails closed rather than becoming a backdoor to unknowns.
- sync_company_os_presence: a null company_id is UNKNOWN_TENANT, not shared.
- sync_session_presence: UNKNOWN_TENANT unconditionally. Investigated the
  conversation_id -> owner -> org path asked for and it does not exist in
  code -- chat_history's session/conversation mixins (base.py, session.py,
  security.py) carry no user_id/org_id anywhere, and the owner username
  threaded through SessionManager.create_session is never persisted onto
  AgentTerminalSession for a later lookup to recover. Reporting this back
  rather than picking something else, per the review's own instruction.
  The real fix is threading tenant through at session-creation time, not
  deriving it after the fact from a field that cannot carry it.
- list_live(tenant_id=None) -- returns {tenant_id} union {shared}, never
  unknown. No unscoped "every tenant" query on purpose, per the review:
  a caller that needed one would be reimplementing the leak this replaces.
- Logged both rejection paths (collision, EXTERNAL) and the TTL env
  fallback -- were silent before.
- Module docstring now states report()/deregister() enforce no caller
  identity of their own; the authoritative-source guarantee holds only
  because the three feed adapters are the sole intended callers.
- 6 new tests: the negative control (tenant X doesn't see tenant Y), shared
  entries included in a tenant query, no-arg defaults to shared-only, an
  UNKNOWN_TENANT entry never surfaces under any query, a null-company_id
  node is unknown not shared, sessions are UNKNOWN_TENANT and never listed.

Refs #16947
A terminal session had no tenant, and none could be recovered afterwards --
presence (#16947) had to report every session as UNKNOWN_TENANT. The route
that looked like it might work does not exist: conversation_id is a log-
linkage id only, chat_history's session/conversation stores carry no
user/tenant field, and the owner username threaded through
SessionManager.create_session was never persisted for a later lookup to
recover.

- AgentTerminalSession gains an explicit tenant_id field.
- Threaded through SessionManager.create_session ->
  AgentTerminalService.create_session -> the one real call site with
  authenticated context: POST /api/agent-terminal/sessions, which already
  resolves `owner` from current_user.get("username") for the WebSocket
  ownership gate (#14989/#14960) -- tenant_id is current_user.get("org_id")
  alongside it, the JWT claim only. TerminalCreateSessionRequest has no
  org_id/tenant_id field, so a caller cannot supply one through this route.
- sync_session_presence now reads session.tenant_id, falling back to
  UNKNOWN_TENANT only when it's unset -- a pre-#16975 session, or one
  created via a path with no authenticated context (e.g. the chat-tool
  internal session creation in tools/terminal_tool.py, which passes neither
  owner nor tenant_id today and is left alone: no code touches those paths
  in this PR, so they correctly keep reporting UNKNOWN_TENANT rather than
  a guess).
- 6 new tests: SessionManager.create_session threads tenant_id onto the
  session (and leaves it unset without one); presence reports a session
  under its real tenant and it is invisible to a different tenant's query
  (the AC's own discoverability negative control); a session with no
  tenant stays UNKNOWN_TENANT and is never listed.
- service.py's ratchet lowered 962 -> 959 (landed below ceiling after
  trimming its own docstring to make room for the new parameter).

Refs #16975
Review found to_persist_dict() left tenant_id out, and get_session()'s
Redis-reload branch rebuilt AgentTerminalSession without it -- a session
stamped with its tenant at creation lost the stamp on any reload (a
restart, an eviction, another worker) and reappeared as UNKNOWN_TENANT.
Fails closed, so nothing leaked, but it broke AC1 across a reload.

- to_persist_dict() now includes tenant_id.
- get_session()'s Redis-reload branch restores it via
  session_data.get("tenant_id").
- The pending-approval rebuild path is left as UNKNOWN_TENANT deliberately
  -- it predates this PR and never guessing is correct there.
- New test drives the real persist -> drop from memory -> get_session
  round trip (not the two methods in isolation), then confirms the
  reloaded session is discoverable through the real presence registry in
  its own tenant only.

Refs #16975, #16978 review
…16947)

AC2 says busy/idle is live -- both stubs were not. has_running_task()
always False (running_command_task was declared but never assigned
anywhere outside models.py), and sync_ai_stack_presence hard-coded
busy=False and called it a follow-up.

- services/agent_terminal/service.py: new _run_tracked() helper wraps both
  PTY-execution call sites (_execute_auto_approved_command,
  _run_approved_command_body) so running_command_task is actually set for
  the command's real span and cleared in a finally, success or exception.
- chat_workflow/manager.py: ChatWorkflowManager gains _active_streams
  (incremented/decremented around process_message_stream's real try/
  finally, not a copy of it) and is_processing(). No persistent per-role
  AI-stack process exists, so this is "any stream in flight right now" for
  the whole registry, not attributed to one role -- documented as the
  honest simplification, not a guess.
- protocols/agent_presence_feeds.py: sync_ai_stack_presence takes an
  optional chat_workflow_manager and reports its is_processing() as busy
  for every role; omitting it reports idle, same as before.
- AUTOBOT_AGENT_PRESENCE_TTL_SECONDS registered in
  autobot_shared/env_registry_agent_runtime.py -- the code-quality red:
  it was documented by hand in ENV_VARS.md but never registered, so
  check_env_var_registry.py flagged it as read-but-unregistered.
  docs/developer/ENV_VARS.md regenerated from the registry (byte-identical
  to the hand-written version once regenerated).
- 7 new tests: is_processing() true only for a stream's real span, across
  overlapping streams, and clearing on exception; has_running_task() true
  only for a command's real span and clearing on exception.
- File-size ratchet: manager.py and service.py both landed below their
  ceiling (redundant-comment trims offsetting the new lines) and are
  lowered to match, not left with slack.

Refs #16947
#16947)

duplication-guard: sync_company_os_presence's docstring said it "mirrors
llc/api/agents.py::list_agents's own latest-run-per-agent join" -- it did,
literally, 15 lines of the exact same SQLAlchemy query pasted into both
files. jscpd correctly caught it (9 lines over the whole-tree pin; main
alone measures 11718, 5 under it, so this diff's own +14 was the entire
overage).

agent_org_nodes_with_latest_heartbeat(company_id) in llc/api/agents.py is
now the one definition; list_agents and sync_company_os_presence both
call it. Behavior-preserving -- same subquery, same outer joins, same
where clause; list_agents adds its own .order_by(AgentOrgNode.name) after.

Verified with the same jscpd invocation CI runs
(autobot-backend + autobot-frontend/src, -k 70 -l 8 --skip-comments):
11718 duplicated lines, exactly main's own clean baseline, no longer 11732.

Refs #16947
…esult

Merging issue-16947-agent-presence into this branch landed
services/agent_terminal/service.py at 956 lines, under both branches' own
959 ceiling -- the ratchet only turns down, so lowering it here rather
than leaving the merge with unclaimed slack.

Refs #16947, #16975
@mrveiss
mrveiss merged commit ad0a500 into main Sep 19, 2026
86 checks passed
@mrveiss
mrveiss deleted the vehicle-v090-2026-09-19-rest branch September 19, 2026 22:10
@mrveiss mrveiss removed the land-next Landing set: CI capacity goes to these PRs first (owner direction 2026-09-18, #15397) label Sep 19, 2026
This was referenced Sep 19, 2026
mrveiss added a commit that referenced this pull request Sep 24, 2026
…y are real reads (#17329)

python_filter_covers_its_guards_test.py went red at 46 against
MAX_UNCOVERED_READS = 42: the two guards this branch adds name four paths the
python filter does not cover. They are not the same kind of thing, and the
file's own trade-off rule says to treat them differently.

.github/filters/frontend-paths.yml is a REAL read -- frontend_path_filter_
mirror_test.py parses it and compares it to frontend-test.yml's push paths, so
its verdict changes when that file changes. Covered in the filter, which is
what makes the guard re-run when its input moves.

The other three -- autobot-frontend/vitest.config.ts,
autobot-slm-frontend/vitest.config.ts and autobot-slm-frontend/src/App.vue --
are PARAMETERS to hv_path_in_scan_dirs, a pure string predicate. Nothing opens
them and no content of theirs can change the verdict. Recorded as bypasses and
MAX raised 42 -> 45 with the reason, which is exactly the precedent this file
already carries for CLAUDE.md (#17129), README.md (#17133) and SSHTerminal.vue
(#17020): "covering the real path in the filter would run twelve shards on
every edit for a guard that does not depend on its content. Recorded, not
covered, per this file's own trade-off rule."

Raising is allowed for a new bypass and forbidden for a denominator correction
-- the three existing RAISED entries say so in those words. This is the former:
the count went up because this branch introduced three new literals, not
because anything was excluded to silence a failure.

Recursive, and worth noting: the branch widens the hardcoded-value scope to the
frontend config roots, adds a guard asserting that new scope, and that guard
names frontend config paths -- which a different ratchet counts. The widening
is what makes the guard name them.

13 passed on the ratchet, 35 across it and the three guards it judges.
mrveiss added a commit that referenced this pull request Sep 24, 2026
…ory scope (#17329) (#17330)

* fix(hooks): give the hardcoded-values hook and scan one shared directory scope (#17329)

The two entry points share a rule set and disagreed about which files it
applies to. detect-hardcoded-values.sh walked a list it owned privately; the
pre-commit hook scanned whatever was staged and never saw that list. So any
staged file outside those trees was blocked on commit, invisible to the scan,
and impossible to baseline -- the baseline is keyed to what the scan finds and
--audit-baseline fails on an entry the scan cannot reproduce. The only way to
commit such a file was --no-verify, which switches off every other hook to get
past one that was never going to pass.

The list moves to scripts/lib/hardcoded-value-rules.sh as HV_SCAN_DIRS, beside
the rules both callers already share, with hv_path_in_scan_dirs() as the
predicate. The scan consumes it instead of its own literal, and the hook
applies it alongside hv_file_in_scope. Same six directories, byte-identical --
verified by diffing the new array against the old literal from origin/main --
so nothing the scan covered yesterday is uncovered today.

What this does NOT do is add coverage: each frontend's project-root configs sit
outside */src and remain unscanned by both sides. The comments at both the
predicate and the hook's filter say so and name the issue, because a gate that
is silent about what it does not look at is the failure this repo keeps
closing. Option 2 -- widening the list -- is measured on #17329 rather than
guessed at: 24 findings across 8 files, mostly dev/test ports (:8001, :5173,
:3000) in e2e and playwright configs plus documentation URLs in comments.

Verification:

- the file that could not be committed now passes. Staging #17324's
  vitest.config.ts change and running the hook exactly as pre-commit does:
  "No in-scope files staged for commit", exit 0.
- repo_tests/hardcoded_values_scope_agreement_test.py, 14 cases, pins both
  halves: the list is defined once, each caller reads it, and the predicate
  agrees with the declared directories.
- negative controls, both run: delete the hook's scope call -> 1 failed;
  re-fork a literal list into the scan -> 1 failed; restored -> 14 passed.
- the guard's own first version was WRONG and the control caught it. It
  asserted `"hv_path_in_scan_dirs" in body`, which still matched the comment
  describing the call after the call itself was deleted -- green on the exact
  regression it exists to catch. It now strips comment lines before any
  containment check, and that reasoning is recorded in the helper's docstring.

* fix(ci): restore the two frontend path-filter mirrors the hook had made uneditable (#16260)

#15002 needed `autobot-plugins/terminal/**` in the frontend path lists so a
plugin-only edit still runs the redactor drift guard. The entry landed in
.github/filters/frontend-paths.yml -- the authoritative PR gate -- and was
DROPPED from both mirrors, because staging either workflow file tripped the
hardcoded-values hook on three pre-existing, unbaselinable URLs. The commit in
this branch removes that blocker, so the mirrors can be restored:

- frontend-test.yml `on.push.paths`: post-merge runs. Without the entry a
  plugin-only push did not run the frontend suite after merge.
- ci.yml's inline `frontend` filter: gates that workflow's own frontend-tests
  job (ci.yml:545), which was skipped for the same changes.

The note in frontend-paths.yml saying the entry was "not yet mirrored" is
corrected rather than left behind -- a stale comment asserting a gap that no
longer exists is its own small defect.

repo_tests/frontend_path_filter_mirror_test.py pins the mirror that declares
itself one: frontend-test.yml's push paths must equal the authoritative list
exactly, in both directions. Drift here is silent by construction -- both files
parse, both workflows run, and the only symptom is a suite that quietly does
not execute -- which is why it survived months. Negative control run: drop the
restored entry and it fails with "A change matching these patterns would NOT
run the suite after merge: ['autobot-plugins/terminal/**']".

Verified end-to-end rather than by reading: staging ci.yml, frontend-test.yml
and the filter file and running the hook exactly as pre-commit does now reports
"No in-scope files staged for commit". Both workflow files and the embedded
filter block parse as YAML, and the parsed filter now carries the entry.

Not touched: ci.yml's inline list holds 3 of the authoritative 8 patterns. That
is a narrower gate for a different job, so whether it should mirror the full
list is a coverage decision rather than drift, and it is filed separately with
the exact missing set instead of being changed here.

* fix(repo-tests): make the new guard's nosec annotations reach bandit (#17329, #13521)

I wrote `# nosec B404 - fixed argv, no shell`. Bandit reads every word after
the directive as a test id, so `-`, `fixed`, `argv` and `no` became ids it does
not know and the suppression silenced nothing -- an annotation that looks like
a considered decision and has no effect, which is the exact shape #13521's
guard exists to catch. It caught mine.

Correct form, `# nosec <IDS>  # <prose>`, on both sites. Verified twice rather
than by eye: scripts/check_nosec_format.py now reports nothing across the
repository, and bandit on the file reports "Total potential issues skipped due
to specifically being disabled (e.g., #nosec BXXX): 3" -- B404, B603 and B607
are actually attributed and suppressed. The 8 remaining B101 assert_used
findings are ordinary for a test module.

* fix(tests): point the changed-lines fixtures at a scanned directory (#17329)

My own change broke these. TestChangedLinesOnly plants its fixture at the temp
repo ROOT, and this branch makes the hardcoded-values hook judge staged paths
against HV_SCAN_DIRS, so the fixture fell out of scope and the hook looked at
nothing: a pre-existing violation read as "hidden rather than reported" and a
newly added one "passed" -- the same symptom as the guard being switched off.

Verified as mine rather than assumed, same command both sides: 11 passed on
base, 3 failed on this branch.

The fixture path becomes autobot-backend/m.py, which is inside the scanned set
and says so by being that path. Deliberately line-neutral: this file sits at
its recorded size ceiling of 747, and a grandfathered file may not grow (#14236)
-- so the explanation belongs in this message and the PR body rather than in a
comment block that would raise it.

The scope reduction itself is asserted in repo_tests/hardcoded_values_scope_
agreement_test.py, which pins hv_path_in_scan_dirs against paths inside and
outside the scanned set, including project-root configs. An end-to-end version
at this hook's level needs the harness that lives here, so it waits for this
file to be split rather than growing it.

* feat(hooks): widen the hardcoded-value scope to the config roots and scripts/ (#17329)

Option 2, measured rather than guessed, and triaged rather than seeded. The
previous commits made the hook and the scan agree on scope; this one buys back
the coverage that agreement gave up, for the three directories where the gap
was costing something.

HV_SCAN_DIRS gains each frontend's project ROOT in place of its */src, plus
scripts/. Those trees held files that could be neither committed through the
hook nor absorbed by the baseline -- the catch-22 this issue exists for, with
three known victims: autobot-frontend/vitest.config.ts (#17324),
.github/workflows (#16260, already unblocked here) and scripts/, which blocked
the correct fix for #16816 and forced a worse one.

The walk prunes node_modules and friends. _HV_EXCLUDE_RE already dropped those
paths from the findings, but with the project roots in scope the grep would
read every matching file inside a ~790MB tree on the way to discarding it.

Widening surfaced 34 findings. They are not 34 baseline rows:

  3  stopped being findings. _HV_COMMENT_RE lists `#`, `//` and a continuation
     `*` and had simply missed the block OPENER `/*`, so one-line doc comments
     were scanned as code. Measured: zero stranded baseline entries.
  2  are FIXED here. emergency-rollback.sh's REMOTE_ENV_FILE and
     sync-to-admin.sh's REMOTE_PATH now name their remote root through an env
     var with today's value as the default. Deliberately NOT AUTOBOT_BASE_DIR:
     both scripts already resolve a LOCAL root, and pointing a remote path at a
     locally-overridden one conflates two roots that must differ.
 29  are baselined, each with its own `# reviewed: #17329 <why>` line.

One of those reasons matters more than the others. scripts/check_ansible_file_
references.py's "/opt/autobot/src/" LOOKS like the class this gate exists for
and is not: :200 tests whether a resolved ansible VALUE starts with it and :204
strips it to recover a repo path, so it describes what this repo's ansible
files say. Deriving it from AUTOBOT_BASE_DIR would make every real deployed
path fail that startswith() wherever the var is set, and the checker would
report clean while checking nothing. Baselined with that reasoning rather than
"fixed" into a silent guard.

A refinement was measured and REJECTED: adding `process.env` to _HV_CONFIG_RE
would have dropped 34 findings to 21, and would have stranded 10 tracked
entries including two internal IPs, because `process.env.X || 'http://<ip>'`
reads config AND hardcodes a fallback. That is #14198's defect exactly, so the
playwright fallbacks stay findings with reasons instead.

Verified: detect-hardcoded-values.sh reports status=pass, 0 violations, and
--audit-baseline reports no stale entries. 65 tests pass across the scope
guard, the mirror guard and the pre-commit hook suite.

* fix(tests): source HV_SCAN_DIRS instead of text-parsing the wrapper (#17329)

_scan_dirs() read `SCAN_DIRS=(` out of detect-hardcoded-values.sh and split it
by hand. This branch changed that line to `SCAN_DIRS=("${HV_SCAN_DIRS[@]}")`
when the list moved to the shared rule library, so the parse found exactly one
"directory": the literal ${HV_SCAN_DIRS[@]}. Four tests failed with
"SCAN_DIRS parse found ['${HV_SCAN_DIRS[@]}']".

The function's own docstring said it read the list "from the script itself, so
this file cannot drift from it" -- the intent was right and the location moved
underneath it. That is the same shape as a test selected by a name that
resembles the failing one: A TEXT PARSE READS WHERE A VALUE IS WRITTEN, and the
value moved.

Sourcing the library and reading the array asks the same question the script
asks, so a future move cannot silently answer it wrong -- it fails to source
instead of finding a plausible wrong answer. 4 passed.

* fix(tests): repair the two sibling suites this branch's scope change broke (#17329)

Review found the same defect class I had already fixed twice on this branch,
in two files I had not touched: 35 failed, 71 passed before this commit.

pre-commit-hardcoded-values_test.py (67 tests): its fixtures staged files at
`src/...` and bare root names, none of which falls under any HV_SCAN_DIRS
prefix, so the hook filtered every one out, printed "No in-scope files staged
for commit" and exited 0 -- roughly forty tests asserting returncode != 0 got
0. Every fixture now stages inside a scanned tree, line-neutral: the path
itself carries the requirement.

The four workflow cases needed judgement rather than a prefix. This branch
deliberately makes .github/ unscanned -- that is what unblocked #16260 -- so
two of them asserted behaviour this PR removes and two would have passed
VACUOUSLY, which is worse than failing. They now stage under
autobot-infrastructure/.github/workflows/, which the exemption's own pattern
(`*/.github/workflows/*`) still matches, so the vendor-URL rule stays under
test inside a scanned tree. A new case pins the scope change itself: an IP
that would be blocked inside a scanned tree is not judged at the repo root.

detect-hardcoded-values_test.py (25 tests): its _SCAN_DIRS was the stale
six-entry list and its drift guard TEXT-PARSED `SCAN_DIRS=(`, which now reads
`SCAN_DIRS=("${HV_SCAN_DIRS[@]}")` -- so the parse returned that literal as its
one directory, while _hermetic_repo never created `scripts/` and left the
script FATALing on a directory it could not find. The list is sourced from the
library now, and the drift guard asserts the script still DELEGATES rather than
re-declaring, because "a second list exists at all" is the drift that matters.

One assertion widened honestly: `scripts` is both a scan directory and where
the rule library lives, so removing it trips the "cannot load" FATAL rather
than the missing-directory check. Both are the same refusal, and the test now
asserts the refusal rather than which guard reached it first.

Written to fit, not past, the gate: that file is grandfathered at 653 lines and
this leaves it at exactly 653, so the reasoning above lives here rather than in
comments that would have raised a ceiling.

68 passed and 41 passed respectively.

* fix(guards): split this branch's four new guard inputs by whether they are real reads (#17329)

python_filter_covers_its_guards_test.py went red at 46 against
MAX_UNCOVERED_READS = 42: the two guards this branch adds name four paths the
python filter does not cover. They are not the same kind of thing, and the
file's own trade-off rule says to treat them differently.

.github/filters/frontend-paths.yml is a REAL read -- frontend_path_filter_
mirror_test.py parses it and compares it to frontend-test.yml's push paths, so
its verdict changes when that file changes. Covered in the filter, which is
what makes the guard re-run when its input moves.

The other three -- autobot-frontend/vitest.config.ts,
autobot-slm-frontend/vitest.config.ts and autobot-slm-frontend/src/App.vue --
are PARAMETERS to hv_path_in_scan_dirs, a pure string predicate. Nothing opens
them and no content of theirs can change the verdict. Recorded as bypasses and
MAX raised 42 -> 45 with the reason, which is exactly the precedent this file
already carries for CLAUDE.md (#17129), README.md (#17133) and SSHTerminal.vue
(#17020): "covering the real path in the filter would run twelve shards on
every edit for a guard that does not depend on its content. Recorded, not
covered, per this file's own trade-off rule."

Raising is allowed for a new bypass and forbidden for a denominator correction
-- the three existing RAISED entries say so in those words. This is the former:
the count went up because this branch introduced three new literals, not
because anything was excluded to silence a failure.

Recursive, and worth noting: the branch widens the hardcoded-value scope to the
frontend config roots, adds a guard asserting that new scope, and that guard
names frontend config paths -- which a different ratchet counts. The widening
is what makes the guard name them.

13 passed on the ratchet, 35 across it and the three guards it judges.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
mrveiss added a commit that referenced this pull request Oct 4, 2026
…the wrong population (#17930)

Four defects found in review of the guard added earlier on this branch, all confirmed
with a control before being fixed.

1. FALSE POSITIVE ON COMMENT BLOCKS. `undefined_tags("{% comment %}{% citation %}
   {% endcomment %}")` returned `['citation']`. Liquid's `comment` block overrides
   `unknown_tag` to do nothing, so Jekyll accepts that Markdown and the guard rejected
   it. Comment regions are now stripped like raw regions -- AFTER them, because a
   `{% comment %}` quoted inside a raw block would otherwise open a region that
   swallows every violation up to an unrelated `{% endcomment %}`. Fixtures cover both
   directions and the ordering.

2. HOLLOW ON BALANCE. `{% endif %}` alone, `{% else %}` alone, `{% endfor %}` alone, an
   unclosed `{% if %}` and an unclosed `{% raw %}` all returned `[]`, and every one is a
   Liquid::SyntaxError built entirely from WHITELISTED names -- checking names could not
   see any of them. `unbalanced_tags` adds a block/end stack over the same stripped text,
   with a fixture per case and seven well-formed contrasts so it cannot degenerate into
   rejecting all templating. The sibling fix riding this branch (#17932) is itself a
   stray `{% endif %}`.

3. THE POPULATION WAS MISLABELLED (#17844). `what="Jekyll-processed Markdown docs"` was
   false: Jekyll runs Liquid only on files WITH front matter, and 131 of the 739 scanned
   files have it. The other 608 are copied verbatim, can never break the build, and could
   only ever yield false positives -- and the floor was sized over them. Verified first
   that `docs/_config.yml` does not load jekyll-optional-front-matter (its `plugins:` has
   one entry, docs/Gemfile three gems, pages.yml builds with plain `jekyll build`), and a
   new test pins that premise in both places it could change.

   The floor is RE-MEASURED, not carried across: 125 with growth 50, against a live 131.
   Both numbers come from counting the front-matter population at past revisions of main
   -- 102 at 120d, 114 at 110d/100d/80d, 116 at 70d, 120 at 50d, 119 at 40d, 117 at 20d,
   131 now, i.e. +0.24 files/day with a largest drawdown of 3. floor sits 6 below today
   (twice that drawdown); the ceiling at 175 is 44 above it, ~180 days of headroom. The
   previous pair (650/150) left 61, about six weeks.

   The `{% raw %}` wrap added to docs/superpowers/plans/2026-05-29-resource-policy.md is
   reverted: that file has no front matter, is never rendered, and the wrap only added
   literal noise visible on GitHub. The research doc that actually broke the build does
   have front matter and keeps its fix.

   min_fraction was reconsidered rather than dismissed, because the earlier note was too
   strong -- `reference=` is overridable, and against the non-excluded docs count the
   ratio is stable to within 0.165..0.182 across nine revisions. Not taken, for a measured
   reason recorded in the file: 0.15 of 739 is 110, so a fraction tolerates losing 21
   files where floor=125 fires on the 7th, and the staleness it buys does not apply to a
   population growing at a quarter of a file a day.

4. THE GUARD WAS NOT COVERED, AND THE FIRST ATTEMPT REVERSED A RULING. `.github/filters/
   python-paths.yml` reached only `docs/research/**` and one guide, so a docs-only PR
   elsewhere never ran repo_tests. That was "fixed" by appending to
   `repo_tests/_glob_declared_uncovered.py`, whose own header forbids exactly that
   ("Never add one to make a new uncovered dependency pass"). Reverted. The filter now
   covers `docs/**/*.md` and `docs/_config.yml`, following the #17893 widening beside it,
   and the guard declares `docs/**/*.md` instead of a bare `*.md` so the coverage checker
   can match the two. The walk stays rooted at `docs/`: a `**` glob from the repository
   root is what repo_root_walks_use_git_15955_test exists to reject.

   Ripple, stated because it reverses a second recorded trade: seven docs entries in
   `python_filter_uncovered_reads.py` are no longer bypasses, so MAX_UNCOVERED_READS
   drops 45 -> 38. One of them (#17133's GITHUB_FILING_CREDENTIAL_ROTATION.md) was
   accepted on the grounds that covering it ALONE would cost twelve shards; the tree is
   now covered for a stronger reason, so that trade has nothing left to buy.

Mutation-proved, each in a fresh interpreter because `_reach._MEASURED` memoises the
population and an in-process mutation would appear to survive: removing the comment strip
fails 2, swapping the strip order fails 1, removing the raw strip fails 5, a clean
`unbalanced_tags` fails 6, dropping the stray-end check 2, the unclosed check 2, the
inner-tag check 1; `has_front_matter` forced true fails 3 and false fails 6, dropping the
front-matter filter fails 3, floor=1 fails 1 and floor=900 fails 5, enabling
optional-front-matter in _config.yml fails 1. Planted in a real page: a foreign tag, a
stray `{% endif %}`, an unclosed `{% if %}` and reverting the research doc's raw guard
each fail the tree assertion; the same foreign tag planted in a front-matter-less file
correctly does not.

36 pass in this guard, 526 across it plus every reach and filter meta-test.

Refs #17930, #17844
mrveiss added a commit that referenced this pull request Oct 4, 2026
… the python filter (#17930)

Owner decision on the CI-spend question the previous commit flagged: narrow it.

`docs/**/*.md` and `docs/_config.yml` are OUT of `.github/filters/python-paths.yml`
again, so a documentation edit no longer starts the twelve-shard python suite.
`repo_tests/python_filter_uncovered_reads.py` is restored byte for byte --
MAX_UNCOVERED_READS back to 45 and the seven docs entries back, including
`docs/developer/GITHUB_FILING_CREDENTIAL_ROTATION.md`, whose #17133 trade is left
intact rather than reversed by a widening it never asked for.

THE GUARD STILL HAS TO RUN, and `pages.yml` cannot be the thing that runs it: it is
`push: branches: [main]` only, so it reports a dark site after the merge that darkened
it. The whole point is to fail on the pull request. So
`.github/workflows/docs-liquid-tags.yml` -- `pull_request` + `push: main`, gated on
`docs/**/*.md` and `docs/_config.yml`, one job, one test file. Modelled on
`ansible-file-references.yml`, which is the lightest workflow of this shape here.

The dependency set is MEASURED, not assumed: `pyyaml pytest pytest-asyncio` in a bare
venv runs this guard from the repo rootdir -- root conftest, both its plugins and
`repo_tests/conftest.py` included -- in 0.45s, 36 passed. Same three packages
`ansible-file-references.yml` already proves in CI.

NOT A `_glob_declared_uncovered` ENTRY, which is the part worth reading. That record's
header has always had two exits: an entry leaves when the filter covers its tree, "or
when a cheaper route runs that guard on its own trigger". The second exit had no
mechanism, so taking it would have meant writing a sentence -- and a record whose
entries are sentences is the exemption the same header forbids. There is no existing
registry for "runs on its own trigger"; I looked, and say so rather than implying one
was reused.

The smallest honest mechanism instead: a second record,
`GLOB_RUN_BY_A_DEDICATED_WORKFLOW`, every entry of which is VERIFIED against the
workflow's parsed YAML rather than believed --

  * the named workflow exists;
  * its `pull_request` `paths:` fire on the tree the glob names, checked with the SAME
    matcher the python filter is checked with, so the two cannot drift;
  * a `run:` step in it invokes each guard recorded against the glob;
  * and the staleness direction: an entry whose tree the filter now covers, or that no
    guard declares any more, FAILS rather than lapsing into decoration.

Parsed, never grepped. A comment naming the guard, a `name:` quoting it or a path
mentioned in prose satisfies a text search while running nothing, and that exact shape
-- a check keyed on a sentence that resembles the thing it looks for -- bit this branch
twice today. `test_a_workflow_that_only_MENTIONS_the_guard_does_not_count` is the
in-suite control for it, and `test_the_trigger_block_is_read_through_yamls_boolean_on_key`
is the control for the other way this check could be vacuous: YAML 1.1 folds a bare
`on:` key to the boolean True, so a helper looking up the string finds nothing and every
path assertion downstream passes while asserting about `{}`.

ONE filter line is added, and it is the workflow file itself --
`.github/workflows/docs-liquid-tags.yml` -- because the record now names it by concrete
literal and a change confined to it is precisely the change that can falsify the record's
claim. Without that line MAX_UNCOVERED_READS goes to 46 and the equality fails; with it
the count is 45, unchanged. Same shape as the `auto-fix-generated-types.yml` and
`stylelint-tokens.yml` entries beside it.

`DOCS_GLOB = "docs/**/*.md"` and the `docs.rglob(leaf)` walk are unchanged: the glob has
to be the one the workflow's `paths:` are compared against, and the walk stays rooted at
`docs/` because `repo_root_walks_use_git_15955_test.py:136` rejects a `**` glob taken
from the repository root.

NOT A REQUIRED STATUS CONTEXT. New workflows are not required by default and branch
protection is an owner-only change, so this reports and does not block. Promoting it is
a separate, deliberate step.

Mutation-proved, eight ways, all killing
`test_a_dedicated_workflow_entry_really_runs_that_guard`: dropping `docs/**/*.md` from
the workflow's paths, deleting the pytest step, downgrading that step to a comment plus
a matching `name:` (the case above), removing the `pull_request` trigger entirely so it
takes `pages.yml`'s shape, pointing the record at a nonexistent workflow, naming a guard
that does not declare the glob, renaming the glob so no guard declares it (also fails
`test_every_uncovered_glob_declaration_is_recorded`), and widening the filter to
`docs/**/*.md` after all (also fails `test_the_uncovered_record_only_shrinks`).

85 pass across the Liquid guard and the two filter meta-tests; 219 across the workflow,
required-context and CI-wiring guards.

Also filed: #17940, a sub-issue of #15826 -- `excluded_dirs()` reads `repo_root()`
rather than the `root` its caller was handed, so the empty-tree mutation passes for the
wrong reason. Not fixed here: the obvious repair introduces a silent-empty path in which
an unreadable `docs/_config.yml` means "nothing is excluded". Seven candidate guards
listed there, one confirmed.

Refs #17930, #17844, #17940
mrveiss added a commit that referenced this pull request Oct 4, 2026
…is backup produced nothing for ~125 days, and Jekyll broke on quoted template syntax (#17897, #17932, #17930)

* fix(ansible): presence detection counted apt sources apt never reads (#17897)

A clean-machine install died at step 4 of 7 with "No package matching
'grafana' is available". Two defects, one cause and its missing detector.

The cause. The failing node carried
/etc/apt/sources.list.d/grafana.list.distUpgrade -- correct content, correct
keyring, and inert: sources.list(5) has apt parse only names ending .list or
.sources. do-release-upgrade renames third-party sources that way and the
suffix had been baked into the device image. The helper's recursive grep found
the match in that inert file, reported PRESENT, preserved a repository apt had
never read, and the canonical grafana.list was never written. The same host
showed the contrast that proves this is the suffix and not the content:
nodesource.list.distUpgrade was equally inert, nodesource.sources sat beside
it, and nodesource installed fine. Detection is now scoped to the two suffixes
apt parses; matching semantics, directory and reach are otherwise unchanged.

The detector. The verdict short-circuits to 'usable' when no caller names a
package -- defensible per call site, wrong as a fleet property. Six of seven
call sites inherited that untested trust; only postgresql named a package.
Each of the six now names the package its own role installs after adding the
repo, derived from the role rather than guessed:

  slm_manager/grafana      grafana                              (its own apt task)
  monitoring/grafana       grafana                              (its own apt task)
  docker/main              docker-ce                            (the engine; the
                           distribution archive ships docker.io, never docker-ce)
  redis/main               redis-stack-server                   (its own apt task)
  python_interpreter/main  python_interpreter_packages | first  (derived, not
                           restated, so check and install cannot disagree)
  agent_config/openvino    openvino-{{ openvino_version }}      (the only
                           unconditional install from that repo)

Either fix alone unblocks the install; together the detection is honest and the
trust is declared. A new guard turns the default into a decision: every
include_tasks of the helper must pass a non-empty apt_repo_verify_package, with
a documented shrink-only exemption list (empty, ceiling 0) for a role that
genuinely cannot name one. The guard fails if it resolves zero call sites, so
"found nothing" cannot read as "all compliant", and the detection tests execute
the real shell against a fixture rebuilt from the failing host.

Refs #17897

* fix(ansible): the nightly Redis backup produced no archive for 125 days (#17932)

Refs #17932

`roles/redis/templates/redis-backup.sh.j2` has written zero `.tar.gz` since at
least 2026-05-31 while logging a success line on every run. Two defects, plus the
property that let them survive.

1. trim_blocks. Ansible renders with `trim_blocks=True`, which eats the newline
   after a block tag. The template ended the `tar` line with a block tag and began
   the next line with `rm`, so the deployed script carried one fused command with
   `-f` twice; tar refused with "Multiple archive files require '-M'" and `set -e`
   aborted before the retention sweep, which has therefore never run.

   Fixed structurally rather than with another newline: the template now contains
   no block tags at all. Persistence is passed in as a rendered value and every
   condition is decided in shell, so no whitespace setting can move a command.

2. The AOF layout. The guard tested `$REDIS_DATA_DIR/appendonly.aof`, which Redis 7
   does not have -- append-only data lives in `appendonlydir/` (the `appenddirname`
   default). It was false on every run, so no AOF was ever archived despite
   `appendonly yes`. Both layouts are now handled, directory first.

   Same premise found in `ansible/utils/backup.sh`, fixed here as one class rather
   than one instance, and a third time in `playbooks/data-migration.yml`, whose
   source Redis version cannot be established from this repository -- filed as
   #17933 with a documented, shrink-only exemption in the guard.

3. Silent success. The first line the script logs is a success line, so an abort
   read as a completed backup. An EXIT trap now reports a non-zero status and a
   final FAILED line; the completion line states what the archive holds
   (`aof=appendonlydir|appendonly.aof|missing|disabled`). The archive's existence
   is checked rather than assumed, and the BGSAVE wait now watches LASTSAVE advance
   instead of comparing it to `date +%s`, which exited on its first iteration and
   allowed a stale dump into the archive. That wait is bounded by an env-var-backed
   constant, and the install task gains `validate: /bin/bash -n %s` -- which would
   NOT have caught this defect, since the fused line parsed fine, but stops a future
   render that does not parse from reaching /usr/local/bin.

The orphaned `dump-*.rdb` backlog on affected nodes is deliberately NOT swept: the
retention sweep stays scoped to this script's own archives, because disposing of
accumulated data needs an owner decision, not a script.

Guard: `repo_tests/redis_backup_template_renders_runnable_17932_test.py` renders
the template with Ansible's settings and executes it against a fake Redis host.
Its central assertion is differential -- rendering with and without `trim_blocks`
must produce identical output -- so it names no command pair and catches the class.
10 of its 14 tests fail against the template as it stands on main.

* fix(docs): a Liquid tag quoted as prose broke the site build, and nothing caught it (#17930)

The docs site stopped building at `125c083ca0` and nobody noticed for days:

  Liquid syntax error (line 64): Unknown tag 'citation' (Liquid::SyntaxError)

Neither file was making a templating mistake. Both QUOTE another system's tag syntax as content:
`docs/research/private-tenant-chat-reference-app.md:71` describes Markdoc's `citation` tag in prose,
and `docs/superpowers/plans/2026-05-29-resource-policy.md:512` shows an Ansible/Jinja2 template in a
fenced block. Jekyll runs Liquid over the raw Markdown BEFORE rendering, so neither a code span nor
a fence protects the literal -- Liquid reaches it and fails on a tag it does not define.

TWO files, not one. The reported failure named only `citation`, because Jekyll stops at the first
error. Enumerating every `{% tag %}` in the tree found `set`, `macro` and `endmacro` as well --
Jinja2 spellings Liquid has no equivalent for. `macro`/`endmacro` are in `docs/archives/`, which
`docs/_config.yml` excludes from processing, so they are inert; `set` is in `docs/superpowers/`,
which is NOT excluded, so it was the next failure waiting behind this one. Fixing only the reported
file would have produced a second identical red.

Both wrapped in a raw guard. The two obvious alternatives are both wrong and are recorded in the
issue: defining `citation` as a Liquid plugin would invent a tag to satisfy a line quoting a
different templating system -- and there are zero files under `docs/_plugins` -- while deleting the
literal would delete the content, since the syntax is what the paragraph is about.

The explanatory comment names those tags in PROSE rather than writing them out, because an HTML
comment is not protected either. My first version of it spelled them literally and would have
reintroduced the defect inside its own explanation.

The guard is the half that matters. `repo_tests/docs_liquid_tags_are_defined_17930_test.py` compares
every tag in the processed tree against the set Liquid and Jekyll define. Three design points:

* It strips raw-guarded regions FIRST. Without that it would reject the fix for this very defect --
  mutation-proved: removing the strip fails 3 tests including the raw-guarded case.
* Its contrast case accepts DEFINED tags (`if`, `for`, `assign`, `highlight`, `post_url`, `raw`), so
  it cannot degenerate into "no `{% %}` in docs" -- which would pass this issue's acceptance test
  while forbidding all legitimate templating.
* `EXCLUDED_DIRS` is read from `docs/_config.yml`'s own `exclude:` rather than hand-copied, with a
  test pinning that `archives` is still excluded -- a hand-copied list drifts from the renderer's,
  and `archives` is the live case where text and build disagree.

Reach floor at 200 processed docs, because this one discovers its inputs and a glob that stops
matching would otherwise report clean having scanned nothing.

Why it hid for days: `build` runs only when `docs/**` is in the changeset, so it is absent from most
PRs and present on docs PRs. Five consecutive `main` commits showed it failing on three and absent
on two, which reads as flake and is a path filter.

Mutation-proved four ways: reverting either file's raw guard fails the tree assertion, removing the
raw-stripping fails 3, and an unsatisfiable reach floor fails. 18 pass.

Closes #17930
Refs #17835

* test(guards): register the Liquid guard's *.md glob in the declared-reads record (#17930)

glob_declared_reads_15900_test asserts the record names exactly the guards
declaring each glob. The new docs guard declares *.md and was absent, so the
set comparison failed on push -- the record is hand-kept by design, which is
what makes it catch a new declarer rather than drift with one.

Refs #17930

* test(docs): migrate the Liquid-tag guard's reach floor to _reach.declare (#17930)

The hand-rolled MIN_DOCS_SCANNED = 200 sat 3.7x below the real population of
739, so the floor would have gone on passing after the glob lost three quarters
of the tree -- which is why reach_floor_migration_test refuses a carried-across
constant (#15928). Re-measured rather than converted.

Deliberately absolute, not a min_fraction against all *.md under docs/ (846,
ratio 0.874): archives/ is excluded and only ever grows, so every archived doc
leaves the numerator and stays in the denominator, walking the fraction down
into a false fire. The window is wide for the same reason -- archiving shrinks
this population without the glob breaking.

* fix(guards): the Liquid guard was hollow on structure and sized over the wrong population (#17930)

Four defects found in review of the guard added earlier on this branch, all confirmed
with a control before being fixed.

1. FALSE POSITIVE ON COMMENT BLOCKS. `undefined_tags("{% comment %}{% citation %}
   {% endcomment %}")` returned `['citation']`. Liquid's `comment` block overrides
   `unknown_tag` to do nothing, so Jekyll accepts that Markdown and the guard rejected
   it. Comment regions are now stripped like raw regions -- AFTER them, because a
   `{% comment %}` quoted inside a raw block would otherwise open a region that
   swallows every violation up to an unrelated `{% endcomment %}`. Fixtures cover both
   directions and the ordering.

2. HOLLOW ON BALANCE. `{% endif %}` alone, `{% else %}` alone, `{% endfor %}` alone, an
   unclosed `{% if %}` and an unclosed `{% raw %}` all returned `[]`, and every one is a
   Liquid::SyntaxError built entirely from WHITELISTED names -- checking names could not
   see any of them. `unbalanced_tags` adds a block/end stack over the same stripped text,
   with a fixture per case and seven well-formed contrasts so it cannot degenerate into
   rejecting all templating. The sibling fix riding this branch (#17932) is itself a
   stray `{% endif %}`.

3. THE POPULATION WAS MISLABELLED (#17844). `what="Jekyll-processed Markdown docs"` was
   false: Jekyll runs Liquid only on files WITH front matter, and 131 of the 739 scanned
   files have it. The other 608 are copied verbatim, can never break the build, and could
   only ever yield false positives -- and the floor was sized over them. Verified first
   that `docs/_config.yml` does not load jekyll-optional-front-matter (its `plugins:` has
   one entry, docs/Gemfile three gems, pages.yml builds with plain `jekyll build`), and a
   new test pins that premise in both places it could change.

   The floor is RE-MEASURED, not carried across: 125 with growth 50, against a live 131.
   Both numbers come from counting the front-matter population at past revisions of main
   -- 102 at 120d, 114 at 110d/100d/80d, 116 at 70d, 120 at 50d, 119 at 40d, 117 at 20d,
   131 now, i.e. +0.24 files/day with a largest drawdown of 3. floor sits 6 below today
   (twice that drawdown); the ceiling at 175 is 44 above it, ~180 days of headroom. The
   previous pair (650/150) left 61, about six weeks.

   The `{% raw %}` wrap added to docs/superpowers/plans/2026-05-29-resource-policy.md is
   reverted: that file has no front matter, is never rendered, and the wrap only added
   literal noise visible on GitHub. The research doc that actually broke the build does
   have front matter and keeps its fix.

   min_fraction was reconsidered rather than dismissed, because the earlier note was too
   strong -- `reference=` is overridable, and against the non-excluded docs count the
   ratio is stable to within 0.165..0.182 across nine revisions. Not taken, for a measured
   reason recorded in the file: 0.15 of 739 is 110, so a fraction tolerates losing 21
   files where floor=125 fires on the 7th, and the staleness it buys does not apply to a
   population growing at a quarter of a file a day.

4. THE GUARD WAS NOT COVERED, AND THE FIRST ATTEMPT REVERSED A RULING. `.github/filters/
   python-paths.yml` reached only `docs/research/**` and one guide, so a docs-only PR
   elsewhere never ran repo_tests. That was "fixed" by appending to
   `repo_tests/_glob_declared_uncovered.py`, whose own header forbids exactly that
   ("Never add one to make a new uncovered dependency pass"). Reverted. The filter now
   covers `docs/**/*.md` and `docs/_config.yml`, following the #17893 widening beside it,
   and the guard declares `docs/**/*.md` instead of a bare `*.md` so the coverage checker
   can match the two. The walk stays rooted at `docs/`: a `**` glob from the repository
   root is what repo_root_walks_use_git_15955_test exists to reject.

   Ripple, stated because it reverses a second recorded trade: seven docs entries in
   `python_filter_uncovered_reads.py` are no longer bypasses, so MAX_UNCOVERED_READS
   drops 45 -> 38. One of them (#17133's GITHUB_FILING_CREDENTIAL_ROTATION.md) was
   accepted on the grounds that covering it ALONE would cost twelve shards; the tree is
   now covered for a stronger reason, so that trade has nothing left to buy.

Mutation-proved, each in a fresh interpreter because `_reach._MEASURED` memoises the
population and an in-process mutation would appear to survive: removing the comment strip
fails 2, swapping the strip order fails 1, removing the raw strip fails 5, a clean
`unbalanced_tags` fails 6, dropping the stray-end check 2, the unclosed check 2, the
inner-tag check 1; `has_front_matter` forced true fails 3 and false fails 6, dropping the
front-matter filter fails 3, floor=1 fails 1 and floor=900 fails 5, enabling
optional-front-matter in _config.yml fails 1. Planted in a real page: a foreign tag, a
stray `{% endif %}`, an unclosed `{% if %}` and reverting the research doc's raw guard
each fail the tree assertion; the same foreign tag planted in a front-matter-less file
correctly does not.

36 pass in this guard, 526 across it plus every reach and filter meta-test.

Refs #17930, #17844

* fix(redis): the nightly backup sent no credential, so an error reply read as a timestamp (#17932)

CodeRabbit's finding on redis-backup.sh.j2:66-69, confirmed from the codebase (no
production access was used or attempted).

`roles/redis/templates/redis-stack.conf.j2` gives this role's own server a `requirepass`
directive whenever the role has a password, so an auth-enabled host is a rendered
configuration of this very role rather than a hypothetical. The backup script's three
`redis-cli` calls sent nothing at all -- no password, no username, no environment
variable -- while the role's own readiness probe in tasks/main.yml has always sent both.

The reason this is not a nit is the exit code. Without `-e`, redis-cli PRINTS a server
error reply on stdout and still exits 0, so `LASTSAVE` returned the literal string
`NOAUTH Authentication required.`; the wait loop then compared that string against
itself, and the run either spun out its five-minute budget or archived a stale dump.rdb.
That is the same "logs a success line, produces no usable archive" failure #17932 exists
to end, arriving by a second route -- so closing the issue on the current fix would have
closed it on a fleet that still produced zero archives.

TWO FIXES, NOT ONE.

* The credential is SENT. `REDISCLI_AUTH` from the environment rather than `-a`, so it
  never reaches the process list, plus `--user` to mirror the readiness probe (the
  adopted SLM credential authenticates as a username, and a bare password is refused).
* An error reply is REFUSED. `LASTSAVE` is validated by shape -- a reply that is not a
  bare integer ends the run naming what came back. Shape rather than `-e`: the flag
  exists in current redis-cli, but this repository does not establish the fleet's
  version, and a shape check is version-independent and strictly broader. The BGSAVE
  reply is logged, not gated: Redis answers an in-progress save with an error, and
  rejecting that would abort a run about to succeed.

WHERE THE CREDENTIAL LIVES. Not in the script, and that is a ruling rather than a style
choice. The script is deployed 0755 and is re-rendered on the code-only update path,
where `redis_password` resolves from `vault_redis_password` -- which no inventory here
defines. The "DELIBERATELY EXCLUDED" note at the top of roles/redis/tasks/code_only.yml
keeps every artifact whose content depends on that variable off the update path for
exactly that reason, so substituting it into the script would have reversed that ruling
twice: a secret in a world-readable file, and a secret an update silently empties. The
script therefore carries only the PATH; `redis-backup.env.j2` carries the value, 0600
root, written from the provisioning path only, and parsed with sed rather than sourced
so a credential is never executed. The wait loop is also an explicit assignment now: a
command substitution inside a `while` condition is one of the places `set -e` does not
abort, so a LASTSAVE that started failing mid-wait would have been re-reported every two
seconds instead of ending the run.

ALSO, the same review's second finding: the "Move aside" task still used a bare
`grep -rlsZ` while #17897 scoped the DETECT step to `*.list`/`*.sources`, so an
`unusable` verdict displaced the inert `.distUpgrade`/`.save`/`.bak` siblings too --
files outside the population the verdict was computed over. Scoped identically. While
there, the detect step gained `-F`, which the move step always had: `apt_repo_match` is a
URL whose dots are regex wildcards, and two steps in one decision must not read the
pattern in two dialects.

TESTS. The existing e2e tests shadow redis-cli, so they could not see any of this; the
fake now records argv and REDISCLI_AUTH per call and can answer with a server error.
Six new assertions, in a new guard because the old file reached the 600-line ceiling
(#5060) -- the harness is split out to repo_tests/_redis_backup_harness.py so both guards
drive one render rather than two that can diverge. The harness also pins the BGSAVE
budget short, so a run that WAITS instead of failing is observable rather than killing
the subprocess.

Mutation-proved: reverting the LASTSAVE calls to bare fails 2, the BGSAVE call 2,
exporting REDISCLI_AUTH blank 1, always sending --user 1, removing the shape check 1
(and it then takes the full wait budget), never reading the credential file 1,
substituting the credential into the script 14, `-a` at a call site 1, `-a` added to the
shared argument array 2, and an env template that emits no password 2. On the apt helper:
reverting the move to a recursive grep, dropping its `.sources` name test, and removing
`-F` from detect each fail the new agreement test.

21 pass in the two redis guards, 18 in the apt helper's, 5150 across repo_tests (the
three failures and 59 errors are this box's Python 3.10 against a tree that needs 3.11+,
all pre-existing and none in a file this branch touches).

Refs #17932, #17897

* fix(ci): run the Liquid guard on a docs-gated job instead of widening the python filter (#17930)

Owner decision on the CI-spend question the previous commit flagged: narrow it.

`docs/**/*.md` and `docs/_config.yml` are OUT of `.github/filters/python-paths.yml`
again, so a documentation edit no longer starts the twelve-shard python suite.
`repo_tests/python_filter_uncovered_reads.py` is restored byte for byte --
MAX_UNCOVERED_READS back to 45 and the seven docs entries back, including
`docs/developer/GITHUB_FILING_CREDENTIAL_ROTATION.md`, whose #17133 trade is left
intact rather than reversed by a widening it never asked for.

THE GUARD STILL HAS TO RUN, and `pages.yml` cannot be the thing that runs it: it is
`push: branches: [main]` only, so it reports a dark site after the merge that darkened
it. The whole point is to fail on the pull request. So
`.github/workflows/docs-liquid-tags.yml` -- `pull_request` + `push: main`, gated on
`docs/**/*.md` and `docs/_config.yml`, one job, one test file. Modelled on
`ansible-file-references.yml`, which is the lightest workflow of this shape here.

The dependency set is MEASURED, not assumed: `pyyaml pytest pytest-asyncio` in a bare
venv runs this guard from the repo rootdir -- root conftest, both its plugins and
`repo_tests/conftest.py` included -- in 0.45s, 36 passed. Same three packages
`ansible-file-references.yml` already proves in CI.

NOT A `_glob_declared_uncovered` ENTRY, which is the part worth reading. That record's
header has always had two exits: an entry leaves when the filter covers its tree, "or
when a cheaper route runs that guard on its own trigger". The second exit had no
mechanism, so taking it would have meant writing a sentence -- and a record whose
entries are sentences is the exemption the same header forbids. There is no existing
registry for "runs on its own trigger"; I looked, and say so rather than implying one
was reused.

The smallest honest mechanism instead: a second record,
`GLOB_RUN_BY_A_DEDICATED_WORKFLOW`, every entry of which is VERIFIED against the
workflow's parsed YAML rather than believed --

  * the named workflow exists;
  * its `pull_request` `paths:` fire on the tree the glob names, checked with the SAME
    matcher the python filter is checked with, so the two cannot drift;
  * a `run:` step in it invokes each guard recorded against the glob;
  * and the staleness direction: an entry whose tree the filter now covers, or that no
    guard declares any more, FAILS rather than lapsing into decoration.

Parsed, never grepped. A comment naming the guard, a `name:` quoting it or a path
mentioned in prose satisfies a text search while running nothing, and that exact shape
-- a check keyed on a sentence that resembles the thing it looks for -- bit this branch
twice today. `test_a_workflow_that_only_MENTIONS_the_guard_does_not_count` is the
in-suite control for it, and `test_the_trigger_block_is_read_through_yamls_boolean_on_key`
is the control for the other way this check could be vacuous: YAML 1.1 folds a bare
`on:` key to the boolean True, so a helper looking up the string finds nothing and every
path assertion downstream passes while asserting about `{}`.

ONE filter line is added, and it is the workflow file itself --
`.github/workflows/docs-liquid-tags.yml` -- because the record now names it by concrete
literal and a change confined to it is precisely the change that can falsify the record's
claim. Without that line MAX_UNCOVERED_READS goes to 46 and the equality fails; with it
the count is 45, unchanged. Same shape as the `auto-fix-generated-types.yml` and
`stylelint-tokens.yml` entries beside it.

`DOCS_GLOB = "docs/**/*.md"` and the `docs.rglob(leaf)` walk are unchanged: the glob has
to be the one the workflow's `paths:` are compared against, and the walk stays rooted at
`docs/` because `repo_root_walks_use_git_15955_test.py:136` rejects a `**` glob taken
from the repository root.

NOT A REQUIRED STATUS CONTEXT. New workflows are not required by default and branch
protection is an owner-only change, so this reports and does not block. Promoting it is
a separate, deliberate step.

Mutation-proved, eight ways, all killing
`test_a_dedicated_workflow_entry_really_runs_that_guard`: dropping `docs/**/*.md` from
the workflow's paths, deleting the pytest step, downgrading that step to a comment plus
a matching `name:` (the case above), removing the `pull_request` trigger entirely so it
takes `pages.yml`'s shape, pointing the record at a nonexistent workflow, naming a guard
that does not declare the glob, renaming the glob so no guard declares it (also fails
`test_every_uncovered_glob_declaration_is_recorded`), and widening the filter to
`docs/**/*.md` after all (also fails `test_the_uncovered_record_only_shrinks`).

85 pass across the Liquid guard and the two filter meta-tests; 219 across the workflow,
required-context and CI-wiring guards.

Also filed: #17940, a sub-issue of #15826 -- `excluded_dirs()` reads `repo_root()`
rather than the `root` its caller was handed, so the empty-tree mutation passes for the
wrong reason. Not fixed here: the obvious repair introduces a silent-empty path in which
an unreadable `docs/_config.yml` means "nothing is excluded". Seven candidate guards
listed there, one confirmed.

Refs #17930, #17844, #17940

* fix(guards): the credential guard read a comment EXPLAINING the ruling as a breach of it (#17932)

test_code_only_renders_nothing_that_carries_the_redis_password matched
"redis_password" anywhere in a rendered template. redis-backup.sh.j2 documents,
in a shell comment, that the credential is deliberately NOT substituted into it
-- so the guard failed CI for documenting its own fix. The #15771 shape,
catalogued as a class in #17941; sixth witness today.

The question it should ask is whether rendering can EMIT the value, not whether
the file mentions the name. Only a {{ ... }} or {% ... %} can, so that is what
it now matches, after stripping {# ... #} regions.

Shell comments are deliberately NOT stripped, which is the half that is easy to
get wrong: Jinja has no notion of shell syntax, so "# {{ redis_password }}"
still renders and lands the credential in the file. Stripping them would have
traded a false positive for a real hole.

Four controls pin the distinction, including that redis-stack.conf.j2 -- a
genuine offender -- is still caught, so the narrowing did not cost the guard
its subjects.

* test(redis): the bare-call detector read physical lines, so a continued call was invisible (#17932)

`_command_lines` splits on newlines, so `redis-cli\` followed by its arguments
produces no line containing `redis-cli `. The detector walked past it while the
remaining calls still cleared the count floor -- a number reported, the miss
invisible. It was measuring the lines it could see, not the calls that run.

Backslash-newline is now joined first, which is what the shell does before
executing. Latent today: no call in the template is continued. Fixed anyway,
because what the guard is wrong about is an unauthenticated call shipping
unnoticed, which is the defect this whole issue is.

Adds the contrast pair that was missing -- bare and authenticated, each in
plain and continued form. Without the continued cases the detector cannot be
shown to catch anything the line-oriented version missed, and that version
passed the real template happily while blind to exactly that shape.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment