The plan for sameeralam3127.linux_vitals from October 2026 to the end of
January 2027, built from the open issue backlog. Every line links to the issue
that tracks it -- this document orders and groups work, it never describes
work that has no issue.
Board: LinuxVitals Roadmap -- the same issues as cards. The board is the thing you drag cards around on; this file is where the reasoning lives.
The plan ends in January on purpose. It was a twelve-month plan until September 2026, and a year-long plan for a project with one maintainer is mostly a list of things that will not happen in the order written. Four months is short enough to be honest about. What does not fit is not spread thinner across the months -- it is listed under Not before February 2027, with the reason, and revisited then.
The ordering within each month is a dependency order, not a priority order: where a cheap enabler unblocks several larger items, it comes first even when its own priority label is lower. The ordering between months follows one rule:
correctness -> scalability -> detection coverage -> integrations -> compliance -> contributors
New dashboard features must not outrun the correctness of the health model underneath them. A check that is wrong is worth less than no check at all, because a missing check is visible in the report and a wrong one is not; and a check that is right but cannot run across a real fleet is a demo. Everything below is an application of that ordering. It is also why the cut falls where it does: integrations and compliance are the last two steps, so they are what waits for February.
| Month | Theme | Release | Issues |
|---|---|---|---|
| October 2026 | Ship what is done; the finding contract | 2.1.0 (released 10 October) | 3 |
| November 2026 | Finish Trust: scale and safety | 2.2.0 -- Trust (end of November) | 1 |
| December 2026 | Artefacts and the task tree | -- | 6 |
| January 2027 | Coverage and adoption | 2.3.0 -- Coverage (end of January) | 3 |
| Any month | good first issues, for whoever picks them up |
next release | 0 |
| Not before February 2027 | Integrations, exports, compliance, further breadth | -- | 5 |
| Release | When | What it is |
|---|---|---|
| 2.1.0 | released 10 October | CI on the ansible-core 2.16 floor and the two 2.16 fixes it found, configurable report modes, one shared service-status task, golden-file tests for every finding, the opt-in fleet health gate, the check-mode refusal, an explicit fact cache timeout, and Debian 12 and Amazon Linux 2023 in CI. |
| 2.2.0 -- Trust | end of November | The report can be believed. Nothing new on the dashboard except filesystem capacity. |
| 2.3.0 -- Coverage | end of January | The report is more complete, the task tree is one a contributor can navigate, and the fleet-size claim is measured. |
2.1.0 shipped rather than waiting for Trust because it carried two fixes
users on the declared floor needed -- on ansible-core 2.16 the generic
webhook had never sent at all
(#85) -- and
because #61 and
#41 added
variables, which made it a minor release by semver
(#112 records why
it is not 3.0.0). Trust therefore moves from 2.1 to 2.2. The constraint from 2.0.0 is unchanged:
#62's check status
and #23's journal
aggregate must change the schema additively, so that Trust ships as 2.2 and
not 3.0. Both land in 2.2.0, so schema_version moves once.
If a month slips, the rule is fixed in advance rather than argued at the time:
- Trust items never drop. If one is late, 2.2.0 moves; it does not ship without it. A release that claims the report can be believed while one of the known false-assurance bugs is still open would be worse than a late one.
- Coverage items drop from the bottom of January into Not before February 2027, in this order: #33, #32. 2.3.0 ships at the end of January with whatever has landed.
The tool's core claim is that its report is true. October and November are the cases where the report can currently be confidently wrong, or where a run cannot be trusted to be safe. October starts with the one that everything else builds on.
| Item | Issue | Priority |
|---|---|---|
| #62 | done -- ships in 2.2.0 | |
Failed-login check reports a clean host when lastb cannot run |
#39 | high |
Race between service restart and service_facts can report a dead service as "Fixed" |
#24 | high |
| Add filesystem capacity and inode checks for all mounts | #30 | high |
Ordering notes:
- 2.1.0 went out first, on 10 October, because what was merged already fixed users on the declared floor (see Releases).
- #47 is done, and it earned its place at the top of the old plan the first time it ran: the suite had never executed on ansible-core 2.16, and doing so found two more total failures (#81, #85) and a dependency that does not support the floor at all (#86). Every change from here is tested on 2.16 as well as the newest core.
- #98 is done, and it came before everything that changes a finding. It is tests only -- no role file changes -- and it records what every finding produces today for a fixed host state. Each item below then updates those golden files on purpose, so the diff in its pull request is the behaviour change a reviewer checks.
- #62 is done. It
led the product changes because the two items under it are both consumers
of it. A finding existed only when something was wrong and
final_statuswasPassorFail, so there was nowhere to record that a check could not run -- which is why a missinglastbreads as a clean host. Fixing that per-check leaves the schema with the same hole and the next check free to fall into it; fixing it in the finding object makes "unknown" expressible once, for every check, and gives contributors a rule to follow. It keptfinal_statustoPass/Failand addedunknown_countbeside it, so the schema change is additive (2.1).docs/compatibility.md-- what counts as a breaking change, and the deprecation policy -- landed with it, because #62 was the first schema change that had to be checked against one. - #24 and
#39 are the two
places where the report actively asserts something false -- a dead service
shown as auto-fixed, and an unchecked security control shown as clean. Those
are worse than a missing check, because a missing check is visible. Both are
the detection- and remediation-side applications of
#62:
FIXED/STILL_FAILED/UNKNOWNon one side,pass/fail/unknownon the other, and neither can express its third state until the contract lands. - #30 is the one
breadth item in the Trust release. A full filesystem is the most common way
a Linux host falls over, the data is already in
ansible_facts['mounts'], and the collection currently uses that data for/bootalone. Scope note: capacity is not the whole check. Inode exhaustion, a root filesystem remounted read-only, and a missing expected mount are each a total failure that a percent-full threshold reports as healthy. Per-mount findings must carry the mount assubject, or the baseline/postcheck comparison cannot tell a newly full/homefrom an already full/var(#77).
The rest of Trust: the run must be safe to point at a real fleet. 2.2.0 -- Trust ships at the end of the month.
| Item | Issue | Priority |
|---|---|---|
| Journal scans are unbounded and ship whole-window logs to the control node | #23 | high |
Ordering notes:
- #23 is the
fleet-scale ceiling; it lands before any month that adds per-host data to
the result object. The fix is structural rather than a tighter filter: the
scan should return an aggregate (
error_count,critical_count,top_units) with raw evidence opt-in and capped, so per-host fact size is roughly constant instead of scaling with log volume. A quiet host and a host in a crash loop should not cost different amounts to scan. It ships in the same release as #62, soschema_versionmoves once.
December is short, so it carries nothing that changes the report's schema and nothing a release waits on. It is the maintainability work that makes January's detection items cheaper to add and easier for someone else to review.
| Item | Issue | Priority |
|---|---|---|
| Concurrent runs sharing an output directory can corrupt reports and snapshots | #26 | low |
Split the 978-line discovery.yml into focused task files |
#27 | medium |
| Static analysis raises ~40 findings; most are intentional and nothing records why | #72 | medium |
| Decide whether CI toolchain uses hashed lockfiles | #74 | low |
| Report all failed systemd units; make the required-service list configurable | #32 | medium |
notify.yml send path, skip conditions, and .env precedence are untested |
#29 | medium |
Ordering notes:
- #26 was planned
to land before the operator runbook
(#6) so the
runbook would document the final concurrency contract. The runbook shipped
first, in 2.0.0, so it currently describes the undefined behaviour. #26
therefore includes updating
docs/runbook.mdwith the contract it establishes -- a second run waits or fails clearly, and reports and snapshots are written atomically. - #27 comes before
January deliberately. Both of January's detection items add tasks to
discovery.yml; doing them first would mean splitting a larger file, and the tag-coverage bug that issue describes gets likelier with every task added. Therender.ymlsplit (363 lines, with its archive/prune sequence written twice) is the second half of the same refactor and is tracked on that issue rather than separately. It is in scope here only if it stays cheap; otherwise it waits for February. - #72 records a
decision for each static-analysis finding, so the next scan does not
re-raise ones that were intentional. It pairs with
#74
(hashed-lockfile decision for the CI toolchain; the
--only-binaryhalf shipped in 2.1.0). - #32 builds on
#28, which
replaced the two copies of the required-service map with one shared
roles/vitals_scan/tasks/services.yml. The configurable list is one change to that task instead of two. - #29 moved here
from November on 2026-10-13, out of 2.2.0 and into 2.3.0. It is no longer a
Trust item, so the rule that Trust items never drop does not hold it. It is
still the only path in the collection that handles secrets. The redaction
tests from #37
prove what it does not print; this proves what it sends -- the send path,
the skip conditions, and
.envprecedence.
The report becomes more complete, the tested platforms wider, and the fleet-size claim measured rather than asserted. 2.3.0 -- Coverage ships at the end of the month with whatever has landed.
| Item | Issue | Priority |
|---|---|---|
| Report pending package updates and pending security updates, per distro | #31 | high |
| Check time-sync quality (offset, synchronised state), not just daemon liveness | #33 | medium |
Fill in docs/performance.md with measured fleet-size results |
#84 | medium |
Ordering notes:
- #31 is the
largest item in the plan and leads the month: "is this host patched?" is
one of the first questions an evaluator asks, and the baseline/postcheck
workflow is built around patch windows. Stale package metadata must read as
unknown, not as zero updates -- the #62 contract applies. - #33: a running time-sync daemon that is not synchronised passes today, and clock skew is exactly what breaks Kerberos, TLS and log correlation after a maintenance window changes firewall or NTP configuration.
- #84 runs after #23, which is expected to change journal cost the most, so the numbers describe the collection users will actually run. It is also what allows the README to state a tested fleet size at all. A real-fleet data point is the kind of contribution a user can make without writing code.
- #64 shipped early, in 2.1.0, so the Debian 12 and Amazon Linux 2023 scenarios are already in place when #31 adds a package-manager branch per distribution -- exactly the kind of change they exist to catch.
Labelled good first issue, each with a comment listing the files to change
and the test to add. They ship in whichever release is next. Both outside
contributions before these came from issues labelled this way, so the aim is
to keep two to four open at all times rather than to schedule them.
| Item | Issue | Size |
|---|---|---|
| None open; the last two shipped in 2.1.0 | -- | -- |
These are real and worth doing, but they are the integration and compliance end of the ordering rule, and each is medium to large. Keeping them out is what keeps the January date honest. They are revisited in February, in this order.
| Item | Issue | Priority |
|---|---|---|
| Export the fleet report as OpenMetrics and CSV | #35 | medium |
| Extend security posture: firewall, SSH hardening, kernel taint, deleted libraries | #36 | medium |
| Add CPU saturation, PSI pressure, and swap activity checks | #34 | medium |
| Upload reports and snapshots to object storage; PagerDuty/Opsgenie payloads | #65 | low |
| Map findings to CIS or STIG rule identifiers for audit evidence | #43 | low |
Notes for February:
- #35 is first in line. OpenMetrics is the higher-value export by some distance -- it turns a run from "produces an HTML file" into trending and alerting in a stack the team already has. It pairs with #41, which shipped in 2.1.0: together they make the collection usable from a pipeline rather than from a terminal.
- #34 is detection breadth, but after #31 and #33: pending updates and clock skew are the gaps evaluators notice first.
- #65 sits behind #35 deliberately. Object storage is for the teams that need the artefact itself as evidence, plus the case #35 does not cover: baseline and postcheck run from two different ephemeral control nodes, where the baseline snapshot has to outlive the pod that wrote it.
- #43 comes last
because it extends the finding object
#8 introduced --
adding a
rule_idalongside the existingidis an additive change -- and because the CSV export is where rule identifiers become useful. - Whether reports and snapshots should default to
0640/0750rather than0644/0755-- the open half of #61 -- is a breaking change, and is decided underdocs/compatibility.mdonce #62 has written it.
Closed since this roadmap was first written, in release order.
| Item | Issue | Released |
|---|---|---|
Add meta/argument_specs.yml for ansible-doc and runtime validation |
#40 | 1.3.0 |
| Pin an upper bound on collection dependencies | #42 | 1.3.0 |
Scan failed at Build per-host report object on ansible-core 2.16 |
#45 | 1.3.0 |
vitals_report failed on ansible-core 2.16: .env regexes unparseable |
#49 | 1.3.1 |
| Add severity-based finding classification and alert thresholds | #8 | 2.0.0 |
Add vitals_certs role: TLS certificate expiry and hardening checks |
#15 | 2.0.0 |
| Add production-grade examples, screenshots, and an operator runbook | #6 | 2.0.0 |
| Render the Slack summary with Block Kit instead of one plain-text blob | #52 | 2.0.0 |
vitals_certs has no role README, unlike the other three roles |
#63 | 2.0.0 |
| Postcheck comparison hid newly failed services and mis-diffed 1.x baselines | #77 | 2.0.0 |
| CI never tests the declared minimum ansible-core | #47 | 2.1.0 |
| Comparison RAM fields were strings on ansible-core 2.16 | #81 | 2.1.0 |
| Generic webhook never sent on ansible-core 2.16 | #85 | 2.1.0 |
| De-duplicate the required-service status map | #28 | 2.1.0 |
| Make report and snapshot file modes configurable | #61 | 2.1.0 |
Golden-file tests for every vitals_scan finding |
#98 | 2.1.0 |
| Fact cache has no explicit expiry, so a run can report stale facts | #25 | 2.1.0 |
Running with --check fails immediately on every host |
#38 | 2.1.0 |
| Add an opt-in non-zero exit so an unhealthy fleet can gate a pipeline | #41 | 2.1.0 |
| Add Molecule scenarios for Debian 12 and Amazon Linux 2023 | #64 | 2.1.0 |
Tell users on ansible-core 2.16 to use community.general 11 |
#86 | 2.1.0 |
Install CI tooling with --only-binary (the lockfile half is in December) |
#74 | 2.1.0 |
Both 2.16 failures (#45, #49) are the evidence behind #47: each was a total failure on a supported version, each passed CI, and each was found by a user rather than a test.
Items marked done in the month tables above move here when the release that carries them is published.
Kept because each records a trade-off worth remembering the next time it comes up.
-
#52 is done, out of quarter and ahead of everything above it. It was supposed to come before #8 and did not, and the cost that note predicted was paid exactly as predicted: the severity line and the "Needs attention first" list had been built as plain text in
slack_message.txt.j2, and #52 rewrote them rather than wrapping them.The debt was one template, which is what the original note judged it would be, so the call to let #8 go first was the right one -- it unblocked three other items, and redoing one template cost less than reworking all three against a finding shape that had not landed.
Worth recording for the next time this trade-off comes up: the rewrite also turned out to be where a real bug was found. The old template looped over every host uncapped, so a fleet of roughly 175+ produced a message past Slack's 40,000-character limit, refused with a bare HTTP 400 that
no_logmade nearly undiagnosable. That was a live defect nobody had filed, found only because the formatting work forced someone to read the loop. -
#8 is done. It changed the shape of a finding to
{id, message, severity}, and theidis the join key the three items that consume it (#35, #36, #43) were waiting for. Doing it last would have meant reworking all three. -
#63 is done, and is the first change in this project written by someone other than the maintainer. It was filed as a documentation gap --
vitals_certsshipped in #15 without the README the other three roles have -- and labelledgood first issueon the theory that it was a reasonable first pull request for someone new.That theory held, with one correction worth keeping: the submitted page documented a variable name that does not exist, which review caught before it merged. A wrong variable name in a doc fails the same way a wrong health check does -- Ansible silently accepts an undefined variable, the real default stays in force, and nothing errors -- so it is the class of documentation bug this project should treat as a bug, not a typo.
This file and the board
are maintained by hand, not generated. When an issue closes, mark it done in
its month here and move it to Done on the board in the same pull request;
when its release is published, move it to Shipped. When a new issue is
opened, add it to a month or to
Not before February 2027 -- an open issue that
appears nowhere in this file is the signal that the roadmap has drifted.
#82 and
#83 duplicate
#31 and
#33 and are
tracked there.
The same applies in the other direction, and it has bitten once already:
vitals_certs shipped as a fourth role and docs/architecture.md went on
describing three of them, down to a diagram labelled with a superseded
schema_version. When a change adds a role, a finding field, or a schema
version, docs/architecture.md is part of that change, not a follow-up.
In February, this plan is replaced by the next one rather than extended: what is left over is re-ordered against whatever has been learned by then.