Skip to content

feat(machine-health): add workstation health-audit plugin#95

Merged
kyle-sexton merged 8 commits into
mainfrom
feat/publish-machine-health
Jul 12, 2026
Merged

feat(machine-health): add workstation health-audit plugin#95
kyle-sexton merged 8 commits into
mainfrom
feat/publish-machine-health

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Refs melodic-software/medley#1378

Ships the machine-health plugin (category: monitoring) from medley's .claude/skills/machine-health/: 17 Windows checks + orchestrator (trend-aware severity, approval-gated remediations, CISA KEV correlation), macOS/Linux honest-UNKNOWN stubs, Pester suite bundled.

Plugin-form changes vs the in-repo skill

  • State/report split via contract seams — new -StateBase orchestrator parameter (explicit > CLAUDE_PLUGIN_DATA > OutputBase) holds state/, logs/, proposals; the report directory is a report_dir userConfig option (default Documents\MachineHealth).
  • Machine-local catalog overlay<StateBase>/catalog/checks.local.jsonc, merged by id (new Merge-CatalogOverlay lib + unit tests). Replaces the in-repo skill's in-place catalog self-edits, which cache isolation would lose on plugin update; custom checks resolve against the state base with the same scripts/<os>/checks/ shape (no validator change).
  • /machine-health:setup — new skill managing the overlay, custom checks, and remediation approvals (nothing approved by default).
  • Decoupling — bundled the Pester workflow-annotation helper; dropped the medley-coupled Apply-DefenderDevExclusions.ps1 (onboard-owned, hardcodes ~/.local/state/medley/ + a repo-root schema reach-out, never dispatched by the orchestrator — it stays in medley); swept medley paths/doc cites; scrubbed example PII from approvals.md.
  • Added artifacts/ to .gitignore (Pester runner output when run from a repo clone).

Validation evidence

  • claude plugin validate ./plugins/machine-health and claude plugin validate --strict . — both PASS
  • Pester: 339 passed / 0 failed / 1 skipped (includes new Merge-CatalogOverlay tests)
  • Dry-run smoke (-RunMode first-run -DryRun with split roots + a battery-disabling overlay): report under the report root, state/logs under the state root, catalog_overlay_merged logged, battery not dispatched (15/16 checks ran)
  • claude --plugin-dir headless load from a non-source directory: both skills register (machine-health:machine-health, machine-health:setup)
  • markdownlint-cli2: 0 errors; PSScriptAnalyzer clean on authored/edited scripts

The medley-side removal is cutover issue melodic-software/medley#1383 (blocked on this publish). Note for that cutover: Apply-DefenderDevExclusions.ps1 must stay in medley (the onboard skill consumes it).


Note

Medium Risk
New local automation can restart services and delete aged temp files when explicitly approved, and performs allowlisted outbound fetches; guardrails (dry first-run, no elevation prompts, schema validation) limit blast radius but the footprint is large.

Overview
Adds the machine-health marketplace plugin: a Windows-first workstation audit that runs a versioned check catalog, adjusts severity using history, optionally applies approval-gated remediations, correlates findings (including CISA KEV), and writes dated markdown reports plus append-only state under plugin data paths.

Marketplace wiring: registers the plugin in .claude-plugin/marketplace.json, documents it in the root README.md, and ignores Pester artifacts/ in .gitignore.

Plugin packaging vs an in-repo skill: splits report root (report_dir user option, default Documents\MachineHealth) from state root (-StateBase / ${CLAUDE_PLUGIN_DATA} for history, logs, approvals.json, and catalog/checks.local.jsonc). The shipped checks.jsonc stays read-only; machine tuning merges via overlay. Skills /machine-health:machine-health and /machine-health:setup drive runs and local configuration. macOS/Linux are explicit NOT_IMPLEMENTED stubs (UNKNOWN report, no Windows script execution).

Implementation surface: JSON schemas, shared reference docs, Invoke-MachineHealthCheck.ps1 orchestrator (timeouts, trend/correlation, remediation dispatch, report render), many Test-*.ps1 checks, narrow remediations (Restart-StoppedService, Clear-TempFiles), allowlisted egress, and a bundled Windows Pester suite.

Reviewed by Cursor Bugbot for commit 7b78b74. Bugbot is set up for automated code reviews on this repo. Configure here.

Port the machine-health skill to a marketplace plugin (category: monitoring):
17 Windows checks + orchestrator with trend-aware severity, approval-gated
remediations, and CISA KEV correlation; macOS/Linux stay honest-UNKNOWN stubs.

Plugin-form changes vs the in-repo skill:
- Split state from reports: new -StateBase (explicit > CLAUDE_PLUGIN_DATA >
  OutputBase) holds state/, logs/, proposals; report dir is a report_dir
  userConfig option (default Documents\MachineHealth).
- Machine-local catalog overlay (checks.local.jsonc under the state base,
  merged by id via new Merge-CatalogOverlay lib + tests) replaces in-place
  catalog self-edits, which a plugin update would overwrite.
- New /machine-health:setup skill manages the overlay, custom checks, and
  remediation approvals.
- Bundle the Pester workflow-annotation helper; drop the medley-coupled
  Apply-DefenderDevExclusions remediation (onboard-owned, never dispatched
  by the orchestrator) and medley path/doc references; scrub example PII.

Validation: claude plugin validate (plugin + --strict catalog) clean; Pester
339 passed / 0 failed; dry-run smoke verifies the report/state split and
overlay merge; --plugin-dir load registers both skills.
@cursor

cursor Bot commented Jul 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_702c17f5-e5c2-4019-82ea-98a683e5175f)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Risk: high. Did not approve: Cursor Bugbot completed as skipped (usage limit reached) with no successful automated review, and this large new plugin adds workstation PowerShell execution and remediation with meaningful security blast radius. Human review is required before merge.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@cursor

cursor Bot commented Jul 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_4b1bbd7a-d870-4880-8db8-f44d0738017f)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Risk: high. Did not approve: Cursor Bugbot completed as skipped (usage limit reached), so automated review did not finish successfully. Human review is required for this large new plugin with PowerShell execution and remediation.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7befbce4b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…health

# Conflicts:
#	.claude-plugin/marketplace.json
#	README.md
@cursor

cursor Bot commented Jul 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_c186eebb-1c33-435d-935f-7fca6987bddc)

@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 26s —— View job


PR Review: machine-health plugin

Tasks

  • Gather context and understand request
  • Review plugin manifest and marketplace registration
  • Review orchestrator (Invoke-MachineHealthCheck.ps1)
  • Review check scripts and library functions
  • Review remediations and security controls
  • Review test suite structure
  • Review documentation and CLAUDE.md alignment
  • Compile and post findings

Summary

The plugin is well-architected with several good security-posture choices (fail-safe approvals, reparse-point-safe temp deletion, atomic KEV download, Start-Job isolation, overlay merge without caller mutation). CLAUDE.md requirements are met: repo-agnostic via CLAUDE_PLUGIN_DATA / userConfig, no PII, plugin.json versioned at 0.1.0, security surface (egress allowlist, approval gate, dry-run default) documented.

Four bugs were found — three confirmed from the Codex review and one additional. None are security-critical in the current scope, but one (battery) is a functional gap and one (event log) is a correctness issue that could suppress real CRIT findings.


Bug 1 — Battery capacity analysis always skipped on laptops (correctness)

Files: Invoke-MachineHealthCheck.ps1:287 · Test-Battery.ps1:84

Invoke-CheckWithTimeout invokes every check as & $ScriptToRun with no arguments. Test-Battery.ps1 only runs powercfg /batteryreport and computes full_capacity_pct when -ReportPath is supplied. Without that parameter:

# Test-Battery.ps1 line 84 — this branch never executes in a normal audit
if ($ReportPath) {   # always false; orchestrator never passes it
    ...
    $fullPct = [math]::Round(($capacity.full_mwh / $capacity.design_mwh) * 100, 1)
}

On a laptop, the check produces severity = OK, summary = "Battery present.", and notes = "No ReportPath provided; capacity analysis skipped." — with no wear signal even when the battery is at 30% of design capacity.

The orchestrator needs to pass a report path for battery, but the current check-dispatch model (Invoke-CheckWithTimeout) has no mechanism to supply per-check parameters. One approach: the battery check reads a well-known env var (e.g., $env:MH_BATTERY_REPORT_PATH) that the orchestrator sets before dispatching; child jobs inherit process env vars. Fix this →


Bug 2 — Test-EventLogErrors queries 500 events before filtering to 7-day window (correctness)

File: Test-EventLogErrors.ps1:51

# Gets the 500 most-recent System events first, then filters by level, then filters by date
$events = Get-WinEvent -LogName System -MaxEvents 500 -ErrorAction Stop |
    Where-Object { $_.LevelDisplayName -in 'Error', 'Critical' }

$windowEvents = @($events | Where-Object { $_.TimeCreated -ge $cutoff })

On a system with high event volume (>500 events since the oldest 7-day Error/Critical event), Get-WinEvent -MaxEvents 500 fills its window with recent informational/verbose events, leaving BugCheck, Kernel-Power 41, or disk-source events within the 7-day window unread. The check can report OK despite a CRIT-level event in scope.

Fix: move the level and time filter into the query:

# Level 1 = Critical, Level 2 = Error
$events = Get-WinEvent -FilterHashtable @{
    LogName   = 'System'
    Level     = @(1, 2)
    StartTime = $cutoff
} -ErrorAction Stop

This also eliminates the post-filter noise-allowlist Get-WinEvent result set re-scan cost. Fix this →


Bug 3 — CISA KEV egress from winget check is not logged (auditability gap)

Files: Test-WingetUpgrades.ps1:82 · Get-CisaKevCache.ps1:93

Get-CisaKevCache is called without -LogPath:

$kev = Get-CisaKevCache -CachePath $kevPath -MaxAgeDays 7   # no -LogPath

Inside Get-CisaKevCache, the egress log line is guarded by if (-not $LogPath) { return }, so the GET https://www.cisa.gov/... fetch is silently omitted from the run log. This makes it invisible in the snapshot's urls_called array.

Compounding this: checks run inside Start-Job, which is a separate PowerShell process. The orchestrator's $PSDefaultParameterValues['Write-MachineHealthLog:LogPath'] binding is not inherited across the job boundary, so there is no ambient default to fall back on.

The CISA KEV fetch is one of the most security-relevant egress events and the PR description explicitly lists auditability as a design goal. Fix this →


Finding 4 — #Requires -Version 7.4 makes the PS 5.1 degrade path dead code (doc/code mismatch)

File: Invoke-MachineHealthCheck.ps1:1 vs. line 157

The docblock (line 7) and SKILL.md both describe a PS 5.1 fallback path:

"Primary target: PowerShell 7.x. Degrades to 5.1 where possible; checks that require newer cmdlets return UNKNOWN cleanly on 5.1."

But #Requires -Version 7.4 on line 1 causes PowerShell to terminate the script before the body is parsed. The degrade check at line 157 can never execute on PS 5.1 or PS 7.0–7.3:

$psMajor = $PSVersionTable.PSVersion.Major
if ($psMajor -lt 7) {           # dead on PS 5.1: script was aborted at line 1
    Write-MachineHealthLog ...
}

All individual check scripts also carry #Requires -Version 7.4, so even if the orchestrator's directive were removed, no checks could run on PS 5.1 (Start-Job for each check would fail at #Requires). The effective minimum is PS 7.4 across the board.

Recommendation: remove the if ($psMajor -lt 7) dead block and update the docblock to say "Requires PowerShell 7.4+". Fix this →


Design note — Get-CisaKevCache bypasses Invoke-AllowlistedWeb (low severity)

File: Get-CisaKevCache.ps1:104

Get-CisaKevCache calls Invoke-WebRequest directly rather than routing through Invoke-AllowlistedWeb. The CISA URL is hardcoded as a constant ($script:CisaKevUrl) and the host is on the allowlist, so the current effective security is identical. However, the design intent of Invoke-AllowlistedWeb is centralized allowlist enforcement — future changes that add a second URL to this function would bypass the guardrail silently. Consider routing through Invoke-AllowlistedWeb for consistency.


What's working well

  • Overlay security: Assert-CatalogEntry validates the script field against ^scripts/(linux|macos|windows)/checks/[A-Za-z0-9_-]+\.ps1$ (line 67), preventing overlay-injected path traversal. Entry cloning in Merge-CatalogOverlay prevents caller-state mutation.
  • Remediation safety: Approval gating returns safe defaults on any error (fail-safe posture). Restart-StoppedService is gated to severity == WARN only, not CRIT; Clear-TempFiles only runs on disk-space findings.
  • Junction-safe temp deletion: Get-NonReparseFile in Clear-TempFiles.ps1 skips reparse-point directories at descent time (not post-enumeration), preventing junction-based traversal to system paths.
  • Atomic KEV refresh: temp-file-then-move pattern in Invoke-KevFetch prevents partial downloads from corrupting the cache.
  • Start-Job isolation: check failures are contained; the orchestrator catches reason = timeout | no_output | invalid_json and continues.
  • Contamination guard: the seed-KEV size check with exit 2 on non-first-run modes prevents silently running an audit against stale in-source data.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Risk: high. Did not approve: Cursor Bugbot completed as skipped (usage limit reached), so automated review did not finish successfully. Human review is required for this large new plugin with PowerShell execution and remediation.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f52e0d524a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Align 'unparseable' to 'unparsable', reword one 'mis-detect' comment, and
silence the official Win32_PnP* CIM class names via the config-blessed
inline spellchecker directives (this repo's _typos.toml is a managed
standards materialization — allow-word additions go upstream, and inline
directives rank first in the false-positive hierarchy anyway).
@cursor

cursor Bot commented Jul 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_3ca52a3b-a890-4fdd-81ee-d5c08f6e0af4)

@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 36s —— View job


PR Review: machine-health plugin

Tasks

  • Gather context and understand request
  • Diff new commit against prior review baseline
  • Verify status of previously flagged bugs
  • Review additional Codex findings
  • Compile and post updated findings

The latest commit (73a0fe6) is a typos-lane fixup only — spelling in docblocks, comments, and one log-string. None of the functional issues from the prior review are addressed. Updated status below.


Bug 1 — Battery capacity analysis always skipped on laptops (OPEN)

Files: Invoke-MachineHealthCheck.ps1:287 · Test-Battery.ps1:84

Invoke-CheckWithTimeout dispatches every check as & $ScriptToRun with no arguments. Test-Battery.ps1 only runs powercfg /batteryreport and computes full_capacity_pct when -ReportPath is supplied. On every normal audit dispatch, $ReportPath is absent → battery wear is never measured.

Fix: set $env:MH_BATTERY_REPORT_PATH to "$stateDir/battery-report.html" in the orchestrator before dispatching the battery check; read that env var as the fallback inside Test-Battery.ps1 when -ReportPath is not supplied. Child Start-Job processes inherit env vars but not $PSDefaultParameterValues. Fix this →


Bug 2 — Test-EventLogErrors misses in-window events on high-volume systems (OPEN)

File: Test-EventLogErrors.ps1:51

$events = Get-WinEvent -LogName System -MaxEvents 500 -ErrorAction Stop |
    Where-Object { $_.LevelDisplayName -in 'Error', 'Critical' }

Two compounding bugs:

a) Volume cap: Get-WinEvent -MaxEvents 500 fills its window with the 500 most-recent events of any level. On a busy system these are all informational; a BugCheck or Kernel-Power 41 within the 7-day window but older than those 500 records is never read, and the check reports OK despite an in-scope CRIT event.

b) Localization: LevelDisplayName is the locale-translated display name. On a non-English Windows installation these labels are not "Error" / "Critical", so every Error/Critical event is dropped before the bugcheck/disk-source severity logic runs.

Fix: move both filters into the query so Windows does the work at source:

# Level 1 = Critical, Level 2 = Error; numeric values are locale-invariant
$events = @()
try {
    $events = Get-WinEvent -FilterHashtable @{
        LogName   = 'System'
        Level     = @(1, 2)
        StartTime = $cutoff
    } -ErrorAction Stop
} catch {
    if ($_.Exception.Message -notmatch 'No events were found') { throw }
}

The post-query Where-Object filters for LevelDisplayName and TimeCreated become redundant and can be removed. Fix this →


Bug 3 — CISA KEV egress never appears in urls_called (two-part, OPEN)

Files: Test-WingetUpgrades.ps1:82 · Get-CisaKevCache.ps1:92

Part 1 — -LogPath never supplied: Get-CisaKevCache is called without -LogPath, so Write-MachineHealthLog returns immediately (if (-not $LogPath) { return }). The fetch is not logged at all. Checks run inside Start-Job so the orchestrator's $PSDefaultParameterValues cannot fill this gap.

Part 2 — Double-timestamp breaks the parser even when -LogPath is supplied: Get-CisaKevCache passes a pre-stamped message to Write-MachineHealthLog:

$stamp = (Get-Date).ToString('o')
Write-MachineHealthLog -LogPath $LogPath -Message "$stamp egress GET $CisaKevUrl"

Write-MachineHealthLog prepends its own timestamp, producing:

2026-07-12T06:00:01+00:00 2026-07-12T06:00:01+00:00 egress GET https://...

Read-EgressLog's regex is ^(?<ts>\S+)\s+egress\s+(?<kind>GET|FAIL|DENY)\s+(?<uri>\S+) — after parsing ts it expects the literal egress, but instead finds a second timestamp. The line is silently skipped and urls_called stays empty.

By contrast, Invoke-AllowlistedWeb calls Write-EgressLogLine (line 118) which writes "<ts> egress GET <uri>" directly — matching the parser.

Fix: (a) pass -LogPath sourced from $env:MH_RUN_LOG_PATH (set by the orchestrator before dispatch); (b) replace the two Write-MachineHealthLog calls in Invoke-KevFetch with Write-EgressLogLine -LogPath $LogPath -Kind GET/FAIL -Uri $CisaKevUrl. Fix this →


Bug 4 — Invoke-TrendAnalysis injects last_severity but schema forbids it (OPEN)

Files: Invoke-TrendAnalysis.ps1:102 · check-result.schema.json:71

Invoke-TrendAnalysis builds:

$trend = [ordered]@{
    last_severity = $lastSeverity   # ← NOT in the schema
    delta         = $deltaText
}

The check-result.schema.json defines trend with "additionalProperties": false and allows only last_run, delta, and adjusted_from. last_severity is not permitted; last_run is missing. Every run that has history data produces trend-annotated check results that fail external schema validation. The adjusted_from field — intended to record the pre-upgrade severity — is also never populated when a WARN→CRIT upgrade fires.

Additionally, Invoke-TrendAnalysis's own docblock (line 12) documents the intended shape as { last_run, last_severity, delta }, confirming the schema and the code diverged.

Fix: rename last_severitylast_run (populating it with the timestamp of the last run from history), move severity information to adjusted_from when an upgrade occurs. Fix this →


Finding 5 — #Requires -Version 7.4 makes the PS 5.1 degrade path dead code (OPEN)

File: Invoke-MachineHealthCheck.ps1:1 vs line 157

#Requires -Version 7.4 aborts the script before parse on PS 5.1 or 7.0–7.3. The if ($psMajor -lt 7) block at line 157 and the DESCRIPTION docblock's "Degrades to 5.1 where possible" claim are unreachable dead documentation. All individual check scripts also carry #Requires -Version 7.4.

Fix: remove the dead if ($psMajor -lt 7) block; update the .DESCRIPTION to "Requires PowerShell 7.4+". Fix this →


Finding 6 — Disk used_pct not available as scalar for trend tracking (OPEN)

File: Test-DiskHealth.ps1:157

ConvertTo-TopMetric (which feeds history.jsonl) only captures scalar leaf values from detail; arrays are skipped. Test-DiskHealth stores used_pct only inside detail.volumes[], so no disk-space.used_pct entry is ever written to history. Invoke-TrendAnalysis looks up disk-space.used_pct to compute the week-over-week delta, finds nothing, and skips trend adjustment for disk findings entirely.

Fix: add a top-level scalar detail.worst_used_pct (the worst-volume value already computed for the summary) so ConvertTo-TopMetric can capture it and trend analysis can track week-over-week disk fill rate. Fix this →


Design note — Get-CisaKevCache bypasses Invoke-AllowlistedWeb (low severity)

File: Get-CisaKevCache.ps1:104

Get-CisaKevCache calls Invoke-WebRequest directly. The CISA URL is hardcoded and on the allowlist, so current behavior is identical. However, routing through Invoke-AllowlistedWeb would centralize allowlist enforcement and be consistent with all other egress callers — future additions to this function wouldn't silently bypass the guardrail.


What's working well

  • Overlay security: Assert-CatalogEntry validates the script field against ^scripts/(linux|macos|windows)/checks/[A-Za-z0-9_-]+\.ps1$, preventing overlay-injected path traversal. Entry cloning in Merge-CatalogOverlay prevents caller-state mutation.
  • Remediation safety: Approval gating fails safe on any error. Restart-StoppedService is gated to severity == WARN only; Clear-TempFiles only runs on disk-space findings.
  • Junction-safe temp deletion: Get-NonReparseFile in Clear-TempFiles.ps1 skips reparse-point directories at descent time, preventing junction-based traversal to system paths.
  • Atomic KEV refresh: temp-file-then-move in Invoke-KevFetch prevents partial downloads from corrupting the cache.
  • Start-Job isolation: check failures are contained; orchestrator catches timeout | no_output | invalid_json and continues.
  • CLAUDE.md alignment: repo-agnostic via CLAUDE_PLUGIN_DATA / userConfig, no PII, plugin.json versioned at 0.1.0, security surface documented in PR body.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Risk: high. Did not approve: Cursor Bugbot completed as skipped (usage limit reached), so automated review did not finish successfully. Human review is required for this large new plugin with PowerShell execution and remediation; no reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73a0fe673d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Codex review finding on the new state/report split: the elevation banner's
printed rerun command carried only -OutputBase, and an elevated terminal
usually lacks CLAUDE_PLUGIN_DATA — the rerun would fall back to OutputBase
and split state across two roots. Pass the resolved StateBase through and
print it in the command.
@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 3s —— View job


PR Review: machine-health plugin (commit 87cbc4f)

Tasks

  • Gather context and understand request
  • Diff new commit (87cbc4f) against prior review baseline (73a0fe6)
  • Verify which prior findings are now fixed
  • Assess correctness of the elevation banner fix
  • Compile and post updated findings

The commit 87cbc4f pins -StateBase in the elevated rerun command shown in the non-elevated banner. One finding is now fixed; six from prior reviews remain open.


Elevation Banner Fix — FIXED ✓

Files: Write-ElevationBanner.ps1 · Invoke-MachineHealthCheck.ps1:184

The fix is correct. $StateBase is fully resolved (absolute path via GetFullPath) before Write-ElevationBanner is called. The new parameter is optional ([string] without Mandatory) so existing callers don't break, and the if ($StateBase) guard cleanly handles the null/empty case. The inline comment explains why the pin is necessary (elevated terminals typically lack CLAUDE_PLUGIN_DATA), which is good signal density.

Minor test gap (non-blocking): Write-ElevationBanner.Tests.ps1 only asserts on the elevated and quiet paths. There's no test that captures banner output for a non-elevated, non-quiet run and verifies -StateBase appears in the rerun command. Consider adding a case that calls with -Elevated $false -StateBase 'C:\state' and asserts the stderr output contains -StateBase 'C:\state'. Add test →


Remaining Open Findings

All bugs/findings from the prior two review passes remain unaddressed:

Bug 1 — Battery capacity analysis always skipped on laptops (OPEN)

Files: Invoke-MachineHealthCheck.ps1:287 · Test-Battery.ps1:84

Invoke-CheckWithTimeout dispatches every check as & $ScriptToRun with no arguments. Test-Battery.ps1 only runs powercfg /batteryreport and computes full_capacity_pct when -ReportPath is supplied — which it never is via normal dispatch. Battery wear is never measured on laptops.

Fix: set $env:MH_BATTERY_REPORT_PATH = "$stateDir/battery-report.html" in the orchestrator before dispatching the battery check; read that env var as the fallback inside Test-Battery.ps1. Child Start-Job processes inherit env vars but not $PSDefaultParameterValues. Fix this →


Bug 2 — Test-EventLogErrors misses in-window events on high-volume / non-English systems (OPEN)

File: Test-EventLogErrors.ps1:51

Two compounding issues: (a) Get-WinEvent -MaxEvents 500 fills its window with the 500 most-recent events of any level — on a busy system a BugCheck within the 7-day window but older than those 500 records is never read; (b) filtering on LevelDisplayName -in 'Error', 'Critical' uses locale-translated strings that are not "Error" / "Critical" on non-English Windows, silently dropping all in-scope events.

Fix: move both filters into the query using locale-invariant numeric Level values:

# Level 1 = Critical, Level 2 = Error; numeric values are locale-invariant
$events = @()
try {
    $events = Get-WinEvent -FilterHashtable @{
        LogName   = 'System'
        Level     = @(1, 2)
        StartTime = $cutoff
    } -ErrorAction Stop
} catch {
    if ($_.Exception.Message -notmatch 'No events were found') { throw }
}

Fix this →


Bug 3 — CISA KEV egress never appears in urls_called (two-part, OPEN)

Files: Test-WingetUpgrades.ps1:82 · Get-CisaKevCache.ps1:92

Part 1 — -LogPath never supplied: Get-CisaKevCache is called without -LogPath, so Write-MachineHealthLog returns immediately. The fetch is not logged. Checks run inside Start-Job so $PSDefaultParameterValues cannot fill this.

Part 2 — Double-timestamp breaks the parser even when -LogPath is supplied: Get-CisaKevCache passes a pre-stamped message to Write-MachineHealthLog, which prepends its own timestamp — producing <ts> <ts> egress GET .... Read-EgressLog's regex expects <ts> egress ... so the line is silently skipped and urls_called stays empty.

Fix: (a) have the orchestrator set $env:MH_RUN_LOG_PATH and pass it as -LogPath; (b) replace both Write-MachineHealthLog calls in Invoke-KevFetch with Write-EgressLogLine -LogPath $LogPath -Kind GET/FAIL -Uri $CisaKevUrl (the same helper that Invoke-AllowlistedWeb uses). Fix this →


Bug 4 — Invoke-TrendAnalysis injects last_severity but schema forbids it (OPEN)

Files: Invoke-TrendAnalysis.ps1:102 · check-result.schema.json:71

Invoke-TrendAnalysis builds $trend with last_severity and delta. The schema defines trend with "additionalProperties": false permitting only last_run, delta, and adjusted_from. Every trended check result fails external schema validation. last_run is never populated; adjusted_from is never set on WARN→CRIT upgrades.

Fix: rename last_severitylast_run (populate from the last history entry's ran_at timestamp); set adjusted_from = 'WARN' when an upgrade fires. Update the .DESCRIPTION docblock at line 12 to match. Fix this →


Finding 5 — #Requires -Version 7.4 makes the PS 5.1 degrade path dead code (OPEN)

File: Invoke-MachineHealthCheck.ps1:1 vs line 157

#Requires -Version 7.4 aborts the script before parse on PS < 7.4. The if ($psMajor -lt 7) block at line 157 and the DESCRIPTION's "Degrades to 5.1 where possible" claim are unreachable. All check scripts also carry #Requires -Version 7.4.

Fix: remove the dead if ($psMajor -lt 7) block; update .DESCRIPTION to "Requires PowerShell 7.4+". Fix this →


Finding 6 — Disk used_pct not available as a scalar for trend tracking (OPEN)

File: Test-DiskHealth.ps1:157

ConvertTo-TopMetric skips array values; used_pct only exists inside detail.volumes[], so disk-space.used_pct is never written to history and Invoke-TrendAnalysis cannot produce week-over-week deltas for disk findings.

Fix: add a top-level scalar detail.worst_used_pct using the already-computed worst-volume value. Fix this →


Design note — Get-CisaKevCache bypasses Invoke-AllowlistedWeb (low severity, OPEN)

File: Get-CisaKevCache.ps1:104

Get-CisaKevCache calls Invoke-WebRequest directly rather than routing through Invoke-AllowlistedWeb. The CISA URL is hardcoded and on the allowlist, so the current behavior is identical. Routing through Invoke-AllowlistedWeb would centralize allowlist enforcement and keep all egress callers consistent — future additions to this function wouldn't silently bypass the guardrail.


Summary

Finding Status
Elevation banner missing -StateBase ✅ FIXED (87cbc4f)
Battery capacity always skipped 🔴 OPEN
Event log misses events (volume cap + localization) 🔴 OPEN
CISA KEV egress not logged (missing -LogPath + double-timestamp) 🔴 OPEN
Invoke-TrendAnalysis emits last_severity (schema violation) 🔴 OPEN
PS 5.1 degrade path dead code 🟡 OPEN (doc correctness)
Disk used_pct not scalar for trend tracking 🟡 OPEN
Get-CisaKevCache bypasses Invoke-AllowlistedWeb 🟡 OPEN (low severity)
Elevation banner test doesn't cover non-elevated path 🟡 OPEN (new, minor)

Bugs 1–4 are the blocking items before merge. Findings 5–6 and the design note are clean-up candidates.

@cursor

cursor Bot commented Jul 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_b17bffc6-5cd7-4faa-8acf-9d17784248d7)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Risk: high. Did not approve: Cursor Bugbot completed as skipped (usage limit reached), so automated review did not finish successfully. Human review is required for this large new plugin with PowerShell execution and remediation; no reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Codex review disposition:

  • Elevation-banner rerun command missing -StateBase — genuine finding against this PR's new state/report split; fixed in 87cbc4f (banner now pins the resolved state root in the printed command).
  • The other nine findings (PS 7.4 requires-header vs documented 5.1 degrade, battery -ReportPath never passed by the dispatcher, KEV egress not logged from the Start-Job context + double-timestamp format, event-log 500-record cap and localized LevelDisplayName filtering, disk-space trend scalar never persisted, trend object vs shipped schema drift, Windows Update degraded-query severity) are pre-existing behaviors of the upstream in-repo skill, ported as-is — this PR is a behavior-preserving publish. They are being filed as a retrofit issue in the migration tracker so they get fixed as a versioned plugin update rather than smuggled into the port.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87cbc4f6d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cursor

cursor Bot commented Jul 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_c9f30d83-6991-4ea3-8fd3-199097b45111)

@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 35s —— View job


PR Review: machine-health plugin (merge commit d8ffc7f)

Tasks

  • Gather context and understand request
  • Diff merge commit against prior review baseline (87cbc4f)
  • Verify no new issues introduced in the merge
  • Review author's retrofit disposition
  • Compile and post updated findings

The merge commit (d8ffc7f) pulls in only unrelated changes from origin/main (the knowledge plugin rename from book-distill per PR #93, songwriting plugin updates, marketplace renames field). No machine-health files were modified. All prior findings carry forward unchanged from the 87cbc4f review.


Merge scope — no machine-health changes

git diff 87cbc4f..d8ffc7f -- plugins/machine-health/ returns empty. The merge is clean. The marketplace.json renames field addition (mapping book-distillknowledge) is unrelated to this plugin.


Author's retrofit disposition — accepted

@kyle-sexton confirmed that Bugs 1–4 and Findings 5–6 are pre-existing behaviors of the upstream in-repo skill, ported as-is. The fix path is a versioned follow-up plugin update rather than a change to this port commit. That disposition is reasonable for a behavior-preserving publish:

  • The plugin-form seams added in this PR (state/report split, StateBase, catalog overlay, approvals gate, dry-run first run) are net-new and well-implemented.
  • The functional gaps existed in the upstream skill before this PR and are not regressions introduced by the port.
  • The elevation-banner -StateBase fix (87cbc4f) was the one item genuinely introduced by the plugin-form changes, and it was correctly addressed.

Open findings — tracked for retrofit

All items below are pre-existing upstream behaviors filed for follow-up:

Finding Severity Status
Elevation banner missing -StateBase ✅ FIXED (87cbc4f)
Battery capacity analysis always skipped on laptops 🔴 Bug retrofit
Event log misses in-window events (volume cap + localization) 🔴 Bug retrofit
CISA KEV egress not logged (-LogPath omitted + double-timestamp) 🔴 Bug retrofit
Invoke-TrendAnalysis emits last_severity (schema violation) 🔴 Bug retrofit
PS 5.1 degrade path dead code (#Requires -Version 7.4) 🟡 Doc retrofit
Disk used_pct not scalar for trend tracking 🟡 Logic retrofit
Get-CisaKevCache bypasses Invoke-AllowlistedWeb 🟡 Design retrofit
Windows Update degraded-query severity masks unknown state 🟡 Logic retrofit
Cadence filter ignored in check selection 🟡 Logic retrofit
Same-day report overwrite on back-to-back runs 🟡 Correctness retrofit
Elevation banner test missing non-elevated + -StateBase assertion 🟡 Test retrofit

Merge readiness

This PR is a clean, behavior-preserving publish of the upstream machine-health skill into plugin form. The plugin-form-specific work (StateBase/report split, catalog overlay, approvals gate, dry-run default, PII scrub, no hardcoded paths) is sound. The open findings are inherited behaviors scoped to a follow-up versioned update.

No objections to merge from this review pass.

Codex finding: the orchestrator's check selection ignores cadence (as the
in-repo original did), but the new overlay reference presented the monthly
demotion patch as taking effect. State the advisory status until
cadence-aware selection ships.
@cursor

cursor Bot commented Jul 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_5c142574-6ea1-42e3-845e-925b5060c50f)

@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 31s —— View job


PR Review: machine-health plugin (commit 436ec92)

Tasks

  • Gather context and understand request
  • Diff new commit (436ec92) against prior review baseline (d8ffc7f)
  • Verify status of previously flagged bugs
  • Review new changes introduced in this commit
  • Compile and post updated findings

The commit 436ec92 is a documentation-only change: it adds an advisory notice to catalog-overlay.md clarifying that the cadence field on overlay entries is not yet enforced by the orchestrator. One retrofit finding is now accurately documented; no prior findings are addressed or regressed.


Cadence demotion documentation — ADDRESSED ✓

File: references/shared/catalog-overlay.md:37

The Codex finding "Honor monthly cadence when selecting checks" (Codex review of 87cbc4f) flagged that the orchestrator silently ignores cadence overlay values. Rather than adding cadence-aware dispatch to the port commit, the doc now says:

Advisory today: the orchestrator does not yet skip monthly-cadence checks on weekly runs — the field documents the approved demotion until cadence-aware selection ships in a plugin update.

This is the correct resolution for a behavior-preserving port. The documentation now accurately reflects actual runtime behavior; the feature is tracked for a follow-up versioned update rather than introduced into the port.


Updated findings table

Finding Status
Elevation banner missing -StateBase ✅ FIXED (87cbc4f)
Cadence field not enforced by orchestrator ✅ DOC FIXED (436ec92)
Battery capacity analysis always skipped on laptops 🔴 retrofit
Event log misses in-window events (volume cap + localization) 🔴 retrofit
CISA KEV egress not logged (-LogPath omitted + double-timestamp) 🔴 retrofit
Invoke-TrendAnalysis emits last_severity (schema violation) 🔴 retrofit
PS 5.1 degrade path dead code (#Requires -Version 7.4) 🟡 retrofit
Disk used_pct not scalar for trend tracking 🟡 retrofit
Get-CisaKevCache bypasses Invoke-AllowlistedWeb 🟡 retrofit
Windows Update degraded-query severity masks unknown state 🟡 retrofit
Same-day report overwrite on back-to-back runs 🟡 retrofit
Elevation banner test missing non-elevated + -StateBase assertion 🟡 retrofit

All remaining items are pre-existing behaviors from the upstream in-repo skill, accepted by the author as retrofit work (tracked in the migration issue per the @kyle-sexton comment on 87cbc4f).


Merge readiness

No change to the prior conclusion: this is a clean, behavior-preserving publish. The plugin-form-specific work (StateBase/report split, catalog overlay, approvals gate, dry-run default, PII scrub, no hardcoded paths) is sound. The documentation now honestly describes the current state of cadence enforcement. No objections to merge.

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Second-round Codex disposition: the overlay doc's monthly-cadence claim is corrected in 436ec92 (cadence is recorded but advisory — the orchestrator's selection ignores it, exactly as the in-repo original did); the same-day report-overwrite behavior is pre-existing upstream (health-YYYY-MM-DD.md date-keyed filename). Both join the retrofit issue being filed with the first-round findings.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Risk: high. Did not approve: Cursor Bugbot completed as skipped (usage limit reached), so automated review did not finish successfully. Human review is required for this large new plugin with PowerShell execution and remediation; no reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

…health

# Conflicts:
#	.claude-plugin/marketplace.json
#	README.md
@cursor

cursor Bot commented Jul 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_490d3bdf-897f-4f67-87c5-4a9f430b2797)

@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 19s —— View job


PR Review: machine-health plugin (merge commit 7b78b74)

Tasks

  • Gather context and understand request
  • Diff new merge commit (7b78b74) against prior review baseline (436ec92)
  • Check for machine-health changes introduced in this merge
  • Verify status of previously flagged findings
  • Compile and post updated findings

The merge commit 7b78b74 (Merge: 436ec92b7719cc) pulls in the skill-quality plugin from origin/main. No machine-health files were modified. The only touched files are:

  • .claude-plugin/marketplace.json — conflict resolved to include both skill-quality (new from main) and machine-health (this PR's entry); both entries are correct
  • README.md — one-line addition from main
  • plugins/skill-quality/ — the incoming plugin (unrelated to this PR)

The conflict resolution is clean. The machine-health marketplace registration is intact and unaffected.


Open findings — unchanged from prior pass

All items below are pre-existing upstream behaviors, accepted by @kyle-sexton as retrofit work per the comment on 87cbc4f:

Finding Status
Elevation banner missing -StateBase ✅ FIXED (87cbc4f)
Cadence field advisory documentation ✅ DOC FIXED (436ec92)
Battery capacity analysis always skipped on laptops 🔴 retrofit
Event log misses in-window events (volume cap + localization) 🔴 retrofit
CISA KEV egress not logged (-LogPath omitted + double-timestamp) 🔴 retrofit
Invoke-TrendAnalysis emits last_severity (schema violation) 🔴 retrofit
PS 5.1 degrade path dead code (#Requires -Version 7.4) 🟡 retrofit
Disk used_pct not scalar for trend tracking 🟡 retrofit
Get-CisaKevCache bypasses Invoke-AllowlistedWeb 🟡 retrofit
Windows Update degraded-query severity masks unknown state 🟡 retrofit
Same-day report overwrite on back-to-back runs 🟡 retrofit
Elevation banner test missing non-elevated + -StateBase assertion 🟡 retrofit

Merge readiness

No change to the prior conclusion. This merge is clean — the machine-health plugin-form work (StateBase/report split, catalog overlay, approvals gate, dry-run default, PII scrub, no hardcoded paths) is sound and the conflict resolution is correct. No objections to merge.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Risk: high. Did not approve: Cursor Bugbot completed as skipped, so automated review did not finish successfully. Human review is required for this large new plugin with PowerShell execution and remediation; no reviewers were assigned because no eligible reviewers were identifiable outside the author.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@kyle-sexton
kyle-sexton merged commit fc882bf into main Jul 12, 2026
22 checks passed
@kyle-sexton
kyle-sexton deleted the feat/publish-machine-health branch July 12, 2026 07:07

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7b78b74a64

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

$restartApproved = Test-ApprovalGranted -State $approvalState -RemediationId 'restart-stopped-service'
if ($servicesFinding -and $servicesFinding.severity -eq 'WARN' -and $restartApproved) {
$findingJson = $servicesFinding | ConvertTo-Json -Depth 10 -Compress
$out = Invoke-Remediation -Script 'remediations\Restart-StoppedService.ps1' -FindingJson $findingJson

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor per-host service exclusions

When approvals.json includes check_overrides.services.service_exclusions, this path still serializes the entire services finding and the remediation targets every detail.stopped_auto_services entry. In that configured environment, a service the user explicitly excluded is still passed to Start-Service, violating the remediation policy's abort condition; filter the finding or pass the exclusions before invoking the remediation.

Useful? React with 👍 / 👎.


$diskFinding = $checkResults | Where-Object id -EQ 'disk-space' | Select-Object -First 1
$clearApproved = Test-ApprovalGranted -State $approvalState -RemediationId 'clear-temp-files'
if ($diskFinding -and $diskFinding.severity -in @('WARN', 'CRIT') -and $clearApproved) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate temp cleanup on the C: volume

This triggers Clear-TempFiles for any disk-space WARN/CRIT, including SMART wear/temperature warnings or low space on a non-C: volume. The remediation policy authorizes it only when C: free space is below 15% and says to abort once C: is above that threshold, so approved runs can delete temp files even when the system volume is not low; check the C: volume detail before dispatching.

Useful? React with 👍 / 👎.

Comment on lines +49 to +51
$unsignedInStore = @($driverStore | Where-Object {
-not $_.PSObject.Properties['SignerName'] -or
[string]::IsNullOrWhiteSpace($_.SignerName)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat missing SignerName as unknown

On non-English Windows, pnputil /enum-drivers localizes field labels, so parsed records may not contain an English SignerName property at all. This predicate counts that absence as unsigned, causing every driver-store record to be reported as unsigned/WARN on localized systems; only present-but-empty signer values should count as unsigned, while missing signer fields should degrade as unknown.

Useful? React with 👍 / 👎.

kyle-sexton added a commit that referenced this pull request Jul 13, 2026
…or fixes (v0.2.0) (#103)

Retrofit of the `machine-health` plugin addressing the 11 findings
triaged during the publish PR #95 (all dispositioned there as
pre-existing behavior or deferred implementation; threads resolved).
Bumps the plugin to **0.2.0** with a `CHANGELOG.md` entry.

## New capability

- **Cadence-aware check selection.** A `weekly` run now defers a
`cadence: monthly` check that ran within the last ~4 weeks, using a new
per-check `checks_ran` field on each `state/history.jsonl` line and a
pure `Get-CheckSelection` helper. `on-demand`/`first-run` run every
enabled check. The monthly demotion in the catalog overlay is no longer
advisory-only.

## Behavior fixes

- **event-log-errors** — filter the 7-day window (`StartTime`) and
numeric `Level` 1,2 inside the `Get-WinEvent` query, instead of reading
the newest 500 records then filtering (dropped in-window errors on busy
hosts; numeric level is locale-independent, unlike `LevelDisplayName`).
- **disk-space** — emit a scalar worst-volume `used_pct` so the history
flattener persists it; disk trend deltas/escalation now fire.
- **trend object** — emit `{ last_run, delta, adjusted_from }` (was a
non-schema `last_severity`), matching `check-result.schema.json`
(`additionalProperties: false`); `Assert-CheckResult` now enforces the
trend sub-shape. `last_run` is per-check and normalized to ISO 8601.
- **battery** — the dispatcher passes a run-scoped `-ReportPath`, so
`powercfg /batteryreport` runs and wear/capacity are analyzed.
- **winget / CISA KEV** — forward the run `-LogPath` to
`Get-CisaKevCache`, and emit the KEV egress line via the canonical
single-timestamp writer so `Read-EgressLog` parses it; the CISA fetch
now appears in `urls_called`.
- **windows-update** — a failed `Get-WUList` enumeration surfaces as
INFO (degraded), not a misleading OK "no pending updates".
- **PowerShell version** — reconciled docs/header to the real
**PowerShell 7.4+** requirement (the code uses 7.x-only syntax
throughout); removed the unreachable "degrade to 5.1" claim and dead
branch. Windows PowerShell 5.1 is not supported. _(Direction was left
open by the finding; reconciled toward the real 7.4 requirement rather
than porting to 5.1.)_
- **reports** — per-run filename `reports/health-<UTC-timestamp>.md`, so
a same-day rerun no longer overwrites the earlier report.

## Notes

- New dispatcher plumbing splats per-check arguments across `Start-Job`.
New pure, unit-tested helpers: `Get-CheckSelection`,
`Get-CheckArgument`, `Get-CheckLastRun`.
- Cadence-deferred monthly checks are recorded in the run log
(`check_skipped ... reason=cadence`) and simply do not run that week —
they are not rendered in the report.
- Full Pester suite green (365 passing, +25 new); `PSScriptAnalyzer` and
`markdownlint` clean. Verified end-to-end via live orchestrator runs
(cadence skip, per-run report files, battery capacity, `urls_called`
egress, ISO `trend.last_run`).

Refs melodic-software/medley#1419

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches orchestration, history schema (`checks_ran`), trend severity,
and several security-adjacent checks (event log, updates, KEV egress);
behavior changes are well-tested but affect weekly run composition and
report deltas.
> 
> **Overview**
> Bumps **machine-health** to **0.2.0** with a changelog and doc updates
(per-run `health-<UTC-timestamp>.md` reports, PowerShell **7.4+** only).
> 
> **Cadence-aware runs:** Weekly mode now skips `monthly` catalog checks
that already ran within ~27 days, using new `checks_ran` on each
`history.jsonl` line and helpers `Get-CheckSelection`,
`Get-CheckLastRun`, and `Get-CheckArgument`. The orchestrator splats
per-check args into `Start-Job`, reads a deeper history tail for cadence
(100 lines) while trend still uses the last ~8 runs, and the report
delta line notes cadence-deferred checks so fewer totals are not
mistaken for regressions.
> 
> **Trend and history:** `Invoke-TrendAnalysis` emits schema-aligned
`trend` (`last_run`, `delta`, `adjusted_from`); `Assert-CheckResult`
enforces that shape. `disk-space` adds top-level `detail.used_pct` so
disk trends persist. Failed checks are excluded from `checks_ran` so
monthly checks retry instead of being deferred.
> 
> **Check fixes:** Event log query uses `FilterHashtable` with
`StartTime` and numeric levels plus locale-safe empty-result handling.
Windows Update reports INFO when `Get-WUList` fails
(`pending_update_count` null). Winget forwards `-LogPath` to KEV fetch;
egress uses `Write-EgressLogLine` so CISA appears in `urls_called`.
Battery gets a run-scoped `-ReportPath` for `powercfg /batteryreport`.
> 
> Pester coverage expanded for the new libs and changed checks.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9e13086. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant