Skip to content

feat(tools): share the native-command guard, and report which Copilot reviews are outstanding - #81

Merged
MikeGrier merged 11 commits into
mainfrom
mikegrier/tools-native-guard
Sep 8, 2026
Merged

feat(tools): share the native-command guard, and report which Copilot reviews are outstanding#81
MikeGrier merged 11 commits into
mainfrom
mikegrier/tools-native-guard

Conversation

@MikeGrier

Copy link
Copy Markdown
Owner

Stage 3 of peeling #56 apart, after
#79 (the probe report sink) and
#80 (the long-path probe pair).

Nine files, no crate touched. Everything here is tools/, its CI job, and the design note that
records the two decisions.

1. The native-command guard, shared

Under Windows PowerShell 5.1, a native command that writes to stderr while
$ErrorActionPreference is Stop raises a terminating error when its stderr is redirected with
2>&1. PowerShell 7 does not -- which is why this class of defect survives review and CI.

run-numa-spikes.ps1 had two such captures and no guard. The shape of that failure is what makes it
worth a PR rather than a one-line fix:

cargo --quiet writes nothing to stderr on a clean build, so under 5.1 the script ran to
completion for as long as every spike was healthy. It threw only when cargo did write there
-- a warning, or a failed compile -- which is exactly the case the script exists to report. The
throw landed before $buildExit was assigned, so the broken-instrument branch never ran: no
transcript, no summary, and the one artifact somebody downloads to diagnose a rotted spike was the
one case that never produced it.

Measured both ways against a deliberately uncompilable crate:

host unguarded guarded
Windows PowerShell 5.1 threw RemoteException, captured nothing exit 101, all 7 diagnostic lines
PowerShell 7 fine fine

Why it is dot-sourced and not a module

The guard already existed as three hand-copied copies, so this shares it -- and how it is shared
is a correctness requirement.

A scriptblock carries the session state it was created in. Invoke-Native { cargo build } builds
that scriptblock in the caller's scope, so & $Command runs it there; a .psm1 copy sets
$ErrorActionPreference in the module's scope and the flip never reaches the call. Measured with
the identical body in a module: under 5.1 seven of eight cases failed, while all eight passed under
PowerShell 7.

A module can be made to work via $PSCmdlet.SessionState.PSVariable.Set(...), and that was
measured working on both hosts. It is rejected because the guard would then rest on a subtlety that
looks removable -- anyone simplifying it back to a plain assignment reintroduces a defect that still
passes on PowerShell 7 and in CI.

test-common.ps1 runs its eight cases in the invoking host, re-invokes itself in the other, and
treats a missing host as a failure rather than a skip -- a single-host pass is not the claim it
exists to make. The sabotage job now runs under both pwsh and powershell, because it ran
pwsh-only, which is precisely why this was invisible.

run-mutants.ps1 was checked for the same defect and needs no change -- it redirects nothing,
confirmed by experiment on both hosts.

2. The Copilot review scanner

PR #56 has 197 Copilot reviews and nothing said which were dealt with. Findings arrive in two shapes
and only one has state:

  • Inline comments become threads that can be resolved -- durable, visible, queryable.
  • Suppressed comments exist only as prose in the review body's <details> block and create no
    thread
    . A review is not a reactable object either: POST /pulls/{n}/reviews/{id}/reactions
    returns 404 while the same call on an inline comment succeeds. So nothing anywhere records that
    a suppressed finding was read.

-MarkProcessed closes that gap with a marker comment:

<!-- copilot-review-processed: 5136043258 -->

An HTML comment, so it does not render; on the pull request rather than in a file or a session, so it
survives a new machine, contributor, or agent session; read back by the script, so a handled review
stops being reported. A -Summary is required alongside it, because a marker with no account of
what was done is a claim with no evidence.

Exercised end to end on #80: six threads resolved, one suppressed-only review marked, re-scan clean
at exit 0.

Deliberately not included

  • Write-Report is not consolidated. Six scripts define one and they are not duplicates --
    they differ in level vocabulary (warn/warning, bad/error, plus good, note, detail,
    heading) and in rendering, with two emitting GitHub Actions annotations and four emitting console
    colours. Merging them would change six tools' output to remove a duplication that is only apparent.
  • The M34.4 checklist bookkeeping. main has no M34 milestone -- it lives in the source branch's
    documentation cluster -- so the stub and its archive entry travel with that stage rather than being
    half-landed here.

Verification

Both hosts, run rather than reasoned about:

  • test-common.ps1 -- 8 cases, passing on 7 and 5.1.
  • test-run-sabotage.ps1 -- all shards pass under both hosts.
  • run-numa-spikes.ps1 -- completes on both.
  • Sabotage-verified: delivering the guard as a module turns the new suite red on 5.1 (7 of 8) while
    staying green on 7, so the suite detects the regression it was written for.
  • check-encoding.ps1 615 files clean; check-workflow-refs.ps1 56 references resolve.

… reviews are outstanding

Two pieces of tooling, peeled off the long-running deferred-namespace-ops
branch so they can be reviewed on their own. Neither touches any crate.

## The native-command guard

Under Windows PowerShell 5.1 a native command that writes to stderr while
`$ErrorActionPreference` is `Stop` raises a TERMINATING error when its stderr
is redirected with `2>&1`. PowerShell 7 does not, which is why this class
survives review and CI.

`run-numa-spikes.ps1` had two such captures and no guard, and the shape of
that failure is what makes it dangerous. `cargo --quiet` writes nothing to
stderr on a clean build, so under 5.1 the script ran to completion for as long
as every spike was healthy. It threw only when cargo did write there -- a
warning, or a failed compile -- which is exactly the case the script exists to
report. The throw landed before `$buildExit` was assigned, so the
broken-instrument branch never ran: no transcript, no summary, and the one
artifact somebody downloads to diagnose a rotted spike was the one case that
never produced it. Measured both ways against a deliberately uncompilable
crate: unguarded, 5.1 threw and captured nothing; guarded, exit 101 with all
seven diagnostic lines.

The guard already existed as three hand-copied copies, so this shares it --
and HOW it is shared is a correctness requirement, not a style choice. A
scriptblock carries the session state it was created in, so
`Invoke-Native { cargo build }` runs in the CALLER's scope while a `.psm1`
copy flips the preference in the MODULE's scope; the flip never reaches the
call. Measured with the identical body in a module: under 5.1 seven of eight
cases failed while all eight passed under PowerShell 7. Dot-sourcing puts the
function in the caller's own scope, where the plain assignment does reach it.

A module can be made to work through `$PSCmdlet.SessionState.PSVariable.Set`,
measured working on both hosts, and is rejected: the guard would rest on a
subtlety that looks removable, and simplifying it back reintroduces a defect
that still passes on PowerShell 7 and in CI.

`test-common.ps1` covers it on BOTH hosts -- it runs its cases in the invoking
host, re-invokes itself in the other, and treats a missing host as a failure
rather than a skip. The sabotage job now runs under `pwsh` and `powershell`
for the same reason: it ran pwsh-only, which is precisely why this was
invisible.

`run-mutants.ps1` was checked for the same defect and needs no change -- it
redirects nothing, confirmed by experiment on both hosts.

## The review scanner

PR #56 has accumulated 197 Copilot reviews and nothing said which had been
dealt with. Findings arrive in two shapes and only one has state: inline
comments become threads that can be RESOLVED, while suppressed comments exist
only as prose in the review body and create no thread. A review is not a
reactable object either -- `POST /pulls/{n}/reviews/{id}/reactions` returns
404 while the same call on an inline comment succeeds -- so nothing anywhere
records that a suppressed finding was read.

`scan-pr-reviews.ps1` reports both, and `-MarkProcessed` closes the gap with a
marker comment on the pull request:

    <!-- copilot-review-processed: 5136043258 -->

An HTML comment, so it does not render; on the pull request rather than in a
file or a session, so it survives a new machine, contributor, or agent
session; read back by the script, so a handled review stops being reported. A
`-Summary` is required alongside it, because a marker with no account of what
was done is a claim with no evidence.

Exercised end to end on PR #80: six threads resolved, one suppressed-only
review marked, re-scan clean at exit 0.

## Deliberately not included

The `Write-Report` functions are NOT consolidated. Six scripts define one and
they are not duplicates -- they differ in level vocabulary and in rendering,
with two emitting GitHub Actions annotations and four emitting console
colours. Merging them would change six tools' output to remove a duplication
that is only apparent.

The M34.4 checklist bookkeeping is not here either: `main` has no M34
milestone, which lives in the source branch's documentation cluster, so the
stub and its archive entry travel with that stage rather than being
half-landed here.

Verified on both hosts: `test-common.ps1` passes, the sabotage suite passes
under 7 and 5.1, `run-numa-spikes.ps1` completes, and the encoding and
workflow-reference checks pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 8, 2026 01:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The changes are self-contained to tooling/CI, add cross-host test coverage for the 5.1-specific failure mode, and do not introduce correctness or safety issues in the reviewed diffs.

Pull request overview

This PR improves the reliability and maintainability of the repository’s PowerShell tooling by centralizing the Windows PowerShell 5.1 native-stderr capture guard (via dot-sourced shared support) and adding a script that reports which GitHub Copilot review findings remain outstanding, including suppressed-only reviews that otherwise have no durable “handled” state.

Changes:

  • Introduce tools/common.ps1 as the shared, dot-sourced home for Invoke-Native / ConvertTo-OutputLines, and update multiple tools to use it for native command capture under Windows PowerShell 5.1.
  • Add tools/test-common.ps1 and wire CI to validate the guard across both PowerShell hosts (pwsh + Windows PowerShell 5.1), plus run the sabotage harness tests under both shells.
  • Add tools/scan-pr-reviews.ps1 and corresponding design notes to track unresolved Copilot review threads and to mark suppressed-only review bodies as processed via PR comments.
File summaries
File Description
tools/common.ps1 New shared, dot-sourced helper implementing the 5.1 native stderr capture guard and output normalization.
tools/test-common.ps1 New cross-host test suite validating Invoke-Native behavior and refusing to pass if the other host is missing.
tools/test-run-sabotage.ps1 Dot-sources common.ps1 and routes native git/shell capture through Invoke-Native to avoid 5.1 terminating errors.
tools/soak-flush-barrier.ps1 Removes in-file guard copies and dot-sources common.ps1 to eliminate drift across capture sites.
tools/run-sabotage.ps1 Dot-sources common.ps1 and uses Invoke-Native for git calls that redirect stderr under Stop.
tools/run-numa-spikes.ps1 Dot-sources common.ps1 and uses Invoke-Native for cargo build/run captures to behave consistently on 5.1.
tools/scan-pr-reviews.ps1 New tool that scans Copilot reviews/threads and supports durable “processed” markers for suppressed-only findings.
DESIGN-NOTES.md Records the dot-sourcing requirement for shared tools support and the Copilot review ledger convention.
.github/workflows/ci.yml Adds test-common.ps1 and runs the sabotage harness tests under both pwsh and Windows PowerShell 5.1.
Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

…tests able to fail

Code review found that `Invoke-Native`'s `try/finally` restoration was dead
code and that three cases in `test-common.ps1` claimed to cover it while being
unable to fail. Both hold, and verifying it turned up a second defect the
review did not reach.

**The restoration was a no-op.** `$ErrorActionPreference = 'Continue'` assigns
to a FUNCTION-LOCAL variable -- PowerShell assignment always writes to the
current scope -- so the caller's value was never modified and the local one is
discarded on return. `& $Command` still inherits `Continue` because the
scriptblock runs in a child of that scope, which is the reach the guard needs.
Measured on both hosts: a variant with the `try/finally` deleted outright
leaves the caller reading `Stop` immediately after the call, identically to the
version that had it. Removed, with the scoping rule stated where the
assignment is.

**The three cases were aimed at the wrong property.** They are not worthless,
which is worth being precise about: a mutant that writes
`$script:ErrorActionPreference` instead leaves the CALLER running under
`Continue` for everything afterwards, and that is the mutation actually worth
guarding. They are now described as testing that the flip does not ESCAPE,
which is what they establish.

**The second defect: they could be defeated by their own contamination.** Each
captured `$before = $ErrorActionPreference` rather than establishing a known
value. Under a `$script:`-scoped mutant the leak happens on the FIRST call, so
a later case captured `Continue`, compared it against `Continue` afterwards,
and passed -- the contamination hiding itself. Measured before the fix: that
mutant was caught by only ONE of the three, the behavioural case that observes
`Stop` through a failing cmdlet rather than through the variable. Each case now
sets `$script:ErrorActionPreference = 'Stop'` first, and all three catch it on
both hosts.

**And a real gap the review's finding exposed: PowerShell 7 had no coverage at
all that the guard did anything.** Removing the `Continue` flip fails loudly on
5.1, but 7 captures stderr either way, so every case passed there against a
guard that did nothing. Added `the flip reaches the scriptblock it is handed`,
which observes the preference from inside the passed scriptblock and is
therefore host-independent. It is the only case that fails on 7 against a
removed flip.

Sabotage-verified, both mutants on both hosts:

  escape to $script:   7   : 3 cases fail (was 1 before the isolation fix)
  escape to $script:   5.1 : 3 cases fail
  flip removed         7   : 1 case fails  (was 0 before the new case)
  flip removed         5.1 : 8 cases fail

Nine cases now, passing on both hosts. The sabotage suite passes on both, the
NUMA spike runner completes, and the encoding check is clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 02:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new documentation in tools/common.ps1 and DESIGN-NOTES.md contradicts the intended "no restoration is needed/attempted" behavior and should be corrected to match the implementation.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread DESIGN-NOTES.md Outdated
Comment thread tools/common.ps1
…st markers from write access

Three defects from a review round on this branch. Two were the reviewer's; the
third was found by running the tool this branch adds against its own pull
request, which reported it.

**scan-pr-reviews.ps1 exited 1 for both "there are findings" and "the tool
broke".** Every error path was an unhandled `throw`, and an unhandled throw
from a script also exits 1, so a caller gating on the code could not tell
"2 unresolved threads" from "gh is not authenticated" or "no such pull
request". That is the instrument-versus-finding distinction `run-numa-spikes`
and `run-sabotage` already make by exit code. Error paths now leave through
`Exit-Broken` with code 2; 1 stays the finding. A `ConvertFrom-Json` failure is
covered too, which is the realistic shape of a proxy returning an HTML error
page to a `gh` that still exited 0. Verified on both hosts: nonexistent PR -> 2,
missing -Summary -> 2, PR with open threads -> 1, clean PR -> 0.

**Processed markers were honoured from any comment author.** This repository is
public, so anyone able to comment could post
`<!-- copilot-review-processed: N -->` and permanently retire a review from
every future scan. Because a suppressed-only review has no state anywhere else
-- the whole reason the tool exists -- that would silently delete the only
record that a finding was never read, and the summary would just report a
smaller number. Only OWNER, MEMBER and COLLABORATOR are now honoured;
CONTRIBUTOR means merely "has had a pull request merged" and is not enough.
`author_association` arrives on every comment in the same request, so this
costs no extra call. Markers from other authors are counted and reported rather
than dropped in silence, because such a marker is either an honest mistake or
an attempt to retire a finding and both are worth seeing. Verified on both
hosts across all six association values, including a missing field, which fails
closed; the real OWNER marker on PR #80 is still honoured, so the legitimate
path is unaffected.

**Restatement drift: two places still said the guard "restores" the
preference.** The previous commit removed that restoration as dead code and
corrected the implementation comment, but left the claim standing in
`common.ps1`'s own summary paragraph and in DESIGN-NOTES. Both now state what
the code does -- the flip is function-local rather than restored -- and why the
property worth testing is that it does not escape. Swept `restor` across
`tools/` and DESIGN-NOTES: 49 matches, 2 stale and corrected, the rest about
file restoration in the sabotage harness and unrelated crate behaviour.

Found by running `scan-pr-reviews.ps1 -Pr 81`, which is the tool this branch
adds; it reported both stale sites as unresolved Copilot threads. The
instrument catching a defect in its own change set is the outcome it was
written for.

Verified: `test-common.ps1` passes on both hosts, the sabotage suite passes on
both, encoding 615 files clean, workflow references resolve.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 02:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

scan-pr-reviews.ps1 should explicitly fail fast when gh is missing to avoid misclassifying a broken instrument as a JSON/parse error.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread tools/scan-pr-reviews.ps1
…e all-clear

Copilot raised that `scan-pr-reviews.ps1` should preflight `gh`. Measuring it
found the consequence is worse than either the report or my own expectation.

The report's stated mechanism was a stale `$LASTEXITCODE` surfacing as a
misleading JSON-parse failure. That is not what happens. `Invoke-Native { gh
... }` with `gh` absent throws CommandNotFoundException on both hosts, and the
script then died on something else entirely: `$LASTEXITCODE` is UNSET until a
native command has run in the session, and under `Set-StrictMode -Version
Latest` reading an unset variable is itself a terminating error. So the failure
was a StrictMode violation about `$LASTEXITCODE`, describing the wreckage
rather than the missing tool.

And the exit code was the real damage. Measured before and after, both hosts,
with `gh` stripped from PATH:

  pre-fix   exit 0     <- "Nothing outstanding."
  post-fix  exit 2     <- broken instrument

Exit 0 is this tool's "nothing outstanding" signal, so a machine without the
GitHub CLI got a silent false all-clear from the one tool whose entire purpose
is to stop findings being lost. That is strictly worse than the exit-1
collision the previous commit fixed for the other error paths, and it is the
same class: a broken instrument reporting as a result.

Two changes, because the preflight alone would leave the landmine for any other
path that reads the variable first:

- `gh` is checked once, up front, and its absence exits through `Exit-Broken`
  with a message naming the cause and the fix.
- `$LASTEXITCODE` is read through `Get-LastExitCode`, which uses `Get-Variable
  -ErrorAction SilentlyContinue` and treats unset as failure rather than
  throwing. Both call sites now distinguish "unset" from "non-zero" and report
  the code they saw.

Verified on both hosts: without `gh`, exit 2 with the intended message; with
`gh`, the contract still holds -- findings 1, clean 0, nonexistent PR 2.
`test-common.ps1` and the sabotage suite pass on both hosts; encoding clean.

A note on measurement, since it bit twice while checking this: `pwsh -Command
"& script.ps1"` does NOT propagate the script's exit code, and `Select-Object
-First N` stops the pipeline in a way that makes `$LASTEXITCODE` unreliable.
Both produced wrong readings that looked plausible. The numbers above come from
`-Command "...; exit $LASTEXITCODE"` with no truncation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 02:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The changes are confined to tools/CI/docs, add cross-host coverage for the 5.1-specific failure mode, and don’t alter crate code or public APIs.

Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

…lds exiting as findings

Two findings from a review round, both confirmed by measurement on both hosts.

**Nullable API fields would have exited 1 -- the "findings" code.** Several
property chains over GraphQL and REST data are nullable BY SCHEMA rather than
by accident: `author` and `pullRequestReview` are null for a deleted account,
REST `user` likewise, `submitted_at` is null on a PENDING review, and a
thread's `comments.nodes` can be empty. Under `Set-StrictMode -Version Latest`
every one of those is a TERMINATING error, and an unhandled throw from a script
exits 1 -- which the exit contract added two commits ago defines as "there are
outstanding findings". A contributor deleting their GitHub account would have
silently converted this tool into a false positive.

Measured on both hosts: `$null.login`, `@()[0]` and `[datetime]$null` all throw
under StrictMode, and a JSON-sourced null behaves identically. Added `Get-Path`,
which walks a property chain yielding `$null` instead of throwing, and routed
every such access through it; a thread that cannot be attributed to a review is
skipped rather than attributed to review 0, and a PENDING review renders as
`pending` rather than being cast.

**The marker-trust check was wider than it claimed.** It trusted
`author_association` in OWNER/MEMBER/COLLABORATOR and described those as "the
ones that imply write access". They are not: GitHub reports `COLLABORATOR` for
anyone invited to collaborate, with no permission qualifier, so a collaborator
with `read` or `triage` could post a marker and permanently retire a
suppressed-only finding that has no other state anywhere -- without even being
counted as ignored.

Replaced with an actual permission lookup against the collaborators endpoint,
accepting only `admin` or `write`. One call per DISTINCT marker author, cached,
and markers are rare, so this is a call or two per scan. It **fails closed**: a
404 for a non-collaborator, a 403 because the account running the scan cannot
query permissions, or any network failure leaves the marker unhonoured. That
direction is deliberate -- over-reporting a handled finding is visible and
recoverable, while wrongly honouring a marker silently deletes the only record
that a finding was never read. The lookup deliberately does not go through
`Invoke-GitHubJson`, because a non-zero exit there is the ordinary answer for a
non-collaborator rather than a broken instrument.

Verified against real accounts on both hosts: the owner may retire, `octocat`
and `torvalds` may not, an empty login may not; and the existing OWNER marker
on PR #80 is still honoured, so the legitimate path is unchanged.

The claim is corrected in both places that stated it -- the script header and
DESIGN-NOTES -- since the old wording described an enforcement the code did not
perform. Swept for the stale phrasing; no occurrences remain.

Verified: `test-common.ps1` passes on both hosts, the sabotage suite passes on
both, exit contract intact (findings 1, clean 0, nonexistent PR 2), encoding
615 files clean, workflow references resolve.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 03:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The changes consistently centralize the measured 5.1-safe native-capture guard, add cross-host validation in both tests and CI, and introduce the review scanner without introducing observable correctness issues in the updated scripts.

Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

… its author

A review round found that the marker-trust check collapsed two different
outcomes into one message, and the message asserted the one the run had not
established. That is the same defect this whole tool exists to prevent, so it
is worth naming precisely.

`GET /repos/{owner}/{repo}/collaborators/{user}/permission` requires the CALLER
to have push access. An account without it gets a flat 403 for every login it
asks about -- including the maintainer who wrote the markers. The old code
mapped every non-zero exit to `$false` and printed:

    ignored markers: N (author lacks write access)

In that case the author has `admin`. A reader following the message goes and
checks the marker author's collaborator role, which is fine, rather than their
own token, which is the actual cause.

Measured against the live API, there are three outcomes and the old code had
two. The third is not an error at all, which is what made the conflation easy:

    exit 0, admin|write   -> allowed
    exit 0, read|none     -> denied        (a statement about the author)
    exit 1, HTTP 403      -> unverifiable  (a statement about the caller)

`octocat` on this repository returns exit 0 with `read`, and
`github-actions[bot]` returns exit 0 with `none` -- both successful answers, not
failures. Only the 403 is a failure, and only it is unverifiable.

`Get-RetireAuthority` now returns those three, counted and reported separately.
The unverifiable message says whose problem it is and that every marked review
is being re-reported as a consequence. All three still fail closed -- only
`allowed` honours a marker -- because over-reporting a handled finding is
visible and recoverable, while wrongly honouring one silently deletes the only
record that a finding was never read.

The bot result is worth its own note, and is now in the script header and
DESIGN-NOTES: a marker posted from a workflow using GITHUB_TOKEN is written by
`github-actions[bot]`, whose permission reads `none`, so it can never be
honoured. Mark from a real account. Measured rather than assumed.

Verified on both hosts against real accounts -- owner allowed, non-collaborator
denied, bot denied, a repository where this account lacks push unverifiable, and
a comment with no author denied -- 5 of 5 correct. Exit contract intact
(PR #80 0, PR #81 0, nonexistent PR 2), `test-common.ps1` and the sabotage suite
pass on both hosts, encoding 615 files clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 03:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The changes are cohesive, well-documented, and the added cross-host CI coverage directly validates the correctness-critical PowerShell 5.1 behavior the refactor is intended to enforce.

Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

… guard

A review round found a native capture this branch had missed. The miss was a
sweep failure on my part, and the correction is a wider sweep rather than one
line: I had grepped for `2>&1` and converted those, but `2>$null` is the same
hazard and I never looked for it.

Measured, both spellings, Windows PowerShell 5.1 under `Stop`:

  git rev-parse ... 2>$null   THREW RemoteException
  git rev-parse ... 2>&1      THREW RemoteException
  git rev-parse ...           OK, LASTEXITCODE=128

**`run-sabotage.ps1`'s `Get-RepoRoot` was the live one, and it inverted this
script's own exit contract.** Its whole failure mode IS stderr -- outside a
working tree git says `fatal: not a git repository` -- so on 5.1 the deliberate
`Exit-WithMessage ... 2` line beneath it was unreachable. The harness died with
NativeCommandError and exited 1, which in that script means "sabotages did not
behave as declared": a broken instrument reported as a finding, which is the
exact separation this branch exists to make. PowerShell 7 reached the intended
path either way, which is why it looked fine.

Nothing tested it, because the suite only ever ran the harness INSIDE a fixture
repository. Added a case that runs it from a non-git directory and asserts exit
2, and sabotage-verified it by reverting the guard:

  guard reverted, PS 7   -> PASS   (the bug is invisible here)
  guard reverted, PS 5.1 -> FAIL

That is the host-specific signal this branch's new `shell: powershell` CI step
exists to surface.

**`check-workflow-refs.ps1`'s `cargo metadata ... 2>$null` was latent**, and is
converted too. Cargo writes to stderr routinely; it happens to stay silent on a
warm workspace, which is what kept this hidden. A cold one printing
"Downloading", or any warning, would have killed the check on 5.1 while passing
on 7. That script does run on 5.1, so the fix is live there.

**`check-encoding.ps1` carries the same shape and is deliberately NOT changed.**
Converting it looked right and would have been theatre: measured on both this
branch and origin/main, the file does not parse under Windows PowerShell 5.1 at
all. It has no BOM and contains UTF-8 mojibake fixture strings, which 5.1 reads
as the ANSI code page, so it fails at parse time long before reaching any git
call. The hazard cannot manifest there. That is a real pre-existing defect -- a
repository check that cannot run on one of the two hosts -- but it is a
different one, in a file this branch does not otherwise touch, and fixing it
means changing that file's encoding rather than its git call.

Verified: the new case passes on both hosts and fails on 5.1 against the
reverted guard; `test-common.ps1` and the full sabotage suite pass on both;
`check-workflow-refs.ps1` passes on both; encoding 615 files clean; the review
scanner self-reports 0 on this PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 04:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The new review-marker mechanism in tools/scan-pr-reviews.ps1 can unintentionally honor quoted/example markers in normal PR comments, which can incorrectly suppress outstanding reviews unless marker parsing is tightened.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

tools/scan-pr-reviews.ps1:186

  • Marker parsing currently honors any <!-- copilot-review-processed: ... --> anywhere in a PR comment body, which can accidentally retire a review if someone with write access quotes a marker as an example. Consider bracketing tool-emitted markers with explicit begin/end sentinels so only intentional marker blocks are recognized.

This issue also appears on line 318 of the same file.

tools/scan-pr-reviews.ps1:319

  • The scan treats any marker substring in an issue comment as authoritative. If a maintainer quotes a marker in normal discussion, it will be honored and suppress the review. After adding begin/end sentinels in the emitted marker comment, restrict recognition to markers inside that sentinel block (and ideally anchored to whole lines) to avoid accidental matches.
    $markers = [regex]::Matches((Get-Text $c.body), '<!--\s*copilot-review-processed:\s*(\d+)\s*-->')
    if ($markers.Count -eq 0) { continue }
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

…s parsed output

The previous commit routed `cargo metadata` through `Invoke-Native` and turned
CI red while passing locally. The cause is a semantic difference between the two
redirects that I had treated as equivalent, and it is worth stating plainly
because the whole point of that commit was that any stderr redirect is a hazard
on 5.1:

  2>$null        DISCARDS stderr
  Invoke-Native  MERGES stderr into the capture (2>&1)

For a transcript, merging is what you want. For output that is PARSED, it is
corruption. `cargo metadata`'s stdout is JSON, and a cold CI runner emits
rustup's `info: syncing channel updates` on stderr, so the capture became
`info: ... {json}` and `ConvertFrom-Json` failed with "Unexpected character
encountered while parsing value: i". A warm workspace writes nothing to stderr,
which is exactly why it passed here and failed there.

Reproduced on both hosts against a stand-in that writes both streams, with the
identical error message, and confirmed the fix on both.

Added `Invoke-NativeStdout`, which keeps the `Continue` flip -- `2>$null` is
still a redirect and still terminating on 5.1 under `Stop` -- while discarding
stderr instead of merging it. Swept every `Invoke-Native` call site and
converted the four whose output is parsed rather than shown:

  check-workflow-refs.ps1  cargo metadata -> JSON
  run-sabotage.ps1         git rev-parse  -> a path
  scan-pr-reviews.ps1      gh api         -> JSON
  scan-pr-reviews.ps1      gh api --jq    -> a permission string

The last two matter beyond tidiness: a notice on `gh`'s stderr would have
corrupted every API response, and in the permission case would have made a
genuine writer read as denied -- the marker-trust control failing in the
direction that looks like a legitimate refusal. The remaining call sites are
transcripts or piped to Out-Null, where merging is intended.

Four cases added to test-common.ps1, including the reduced form of the exact CI
failure: stderr discarded rather than merged, JSON parseable under stderr noise,
the exit code preserved, and no throw on 5.1 despite the redirect. Thirteen
cases now, passing on both hosts.

Verified: check-workflow-refs, the sabotage suite and the review scanner all
pass on both hosts; encoding 615 files clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 04:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

scan-pr-reviews.ps1 currently discards gh stderr even on failure, making broken-instrument errors significantly harder to diagnose.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread tools/scan-pr-reviews.ps1
…r the text appears

Copilot review 5137260817 on PR #81, whose two findings were both suppressed --
so they created no thread and, without this branch's own scanner, nothing would
have surfaced them at all.

Marker recognition matched `<!-- copilot-review-processed: N -->` ANYWHERE in a
comment body. That cannot tell a comment that IS a marker from one that merely
MENTIONS one, so a maintainer quoting a marker in discussion -- or pasting the
example out of this script's own documentation -- would silently retire a
review. Since a suppressed-only review has no other state anywhere, retiring it
deletes the only record that a finding was never read.

Not currently firing: the sole marker in the repository is the legitimate one on
PR #80. But this branch publishes the literal marker text in its own PR body and
DESIGN-NOTES, so the material for an accidental retirement is already written.

Markers are now emitted bracketed:

    <!-- copilot-review-processed:begin -->
    <!-- copilot-review-processed: N -->
    <!-- copilot-review-processed:end -->

and honoured only on a line of their own inside such a block. The sentinels are
defined once so emitter and reader cannot drift, which is the failure this whole
tool is about. Both documentation examples now spell the id as a placeholder
rather than digits, so copying an example verbatim -- sentinels included --
matches nothing.

Verified 9 discrimination cases on both hosts, 9 of 9: a real marker and a
two-id block are honoured; a bare marker, one quoted inline in prose, one inside
a fenced block, the placeholder example, a marker sharing a line with a
sentinel, an unterminated block, and an empty body are all rejected.

The existing marker on PR #80 was bare, so it correctly stopped counting -- the
scan went back to exit 1 there, which is the fail-closed direction. Re-marked in
the new format and confirmed exit 0 again; its finding is unchanged.

Verified: exit contract intact on both hosts (PR #80 0, nonexistent PR 2),
test-common.ps1 13 cases pass on both, the sabotage suite passes on both,
encoding 615 files clean, workflow references resolve.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 04:29
@MikeGrier

Copy link
Copy Markdown
Owner Author

Both suppressed findings addressed in 4b0134d.

They were the same finding at two sites: marker recognition matched the marker text anywhere
in a comment body, which cannot distinguish a comment that IS a marker from one that merely
MENTIONS one. A maintainer quoting a marker in discussion, or pasting the example out of the
script's own documentation, would have silently retired a review -- and since a suppressed-only
review has no other state anywhere, that deletes the only record a finding was never read.

Markers are now emitted inside :begin / :end sentinels and honoured only on a line of their
own within such a block, with the sentinels defined once so emitter and reader cannot drift.
Both documentation examples now spell the id as a placeholder rather than digits, so copying an
example verbatim -- sentinels included -- matches nothing.

Verified 9 discrimination cases on both PowerShell hosts, 9 of 9: real marker and a two-id block
honoured; bare marker, quoted inline, quoted in a fenced block, the placeholder example, a marker
sharing a line with a sentinel, an unterminated block, and an empty body all rejected.

Not firing before the fix -- the only marker in the repository was the legitimate one on PR #80 --
but this branch publishes the literal marker text in its own PR body and DESIGN-NOTES, so the
material for an accidental retirement was already written. That PR #80 marker was bare and
correctly stopped counting after the change; it has been re-marked in the new format.

Worth noting these two findings were suppressed, so they created no review thread. Without the
scanner this pull request adds, nothing would have surfaced them.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The changes consistently apply the shared native-command guard across tools, add host-sensitive tests/CI coverage, and introduce the review-scanning tool without altering crate codepaths.

Review details

Suppressed comments (1)

tools/scan-pr-reviews.ps1:180

  • Invoke-GitHubJson discards gh stderr via Invoke-NativeStdout, so on a failure (non-zero exit) the Exit-Broken message can end up missing the real diagnostic text (which gh typically prints to stderr). Consider capturing merged stdout+stderr only on the failing path so the error message is actionable without compromising the JSON parse on success.
    $text = Invoke-NativeStdout { gh @Arguments }
    $code = Get-LastExitCode
    if ($null -eq $code -or $code -ne 0) {
        Exit-Broken "gh $($Arguments -join ' ') failed (exit $code): $($text -join ' ')"
    }
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

… parsed output

Copilot review 5137310730, whose finding is a regression I introduced two
commits ago: routing `gh` through the stdout-only capture fixed the JSON
corruption and silently emptied every broken-instrument message.

Measured which stream `gh` actually uses, because it is not uniform and the
difference decides the fix:

  REST 404          stdout has the JSON error body, stderr `gh: Not Found`
  network failure   stdout EMPTY, stderr `error connecting to ...`
  usage error       stdout EMPTY, stderr the usage text

So the 404 case still reported -- which is what made this look fine locally --
while a bad host, a bad flag or an auth problem produced `failed (exit 1): `
with nothing after the colon. Blank exactly when the cause is least guessable,
which is the shape a broken-instrument message exists to explain.

Added `Invoke-NativeSplit`, which captures both streams separately: stdout stays
clean for the parse, stderr is available for the report. Both `gh` call sites use
it, preferring stderr and falling back to stdout, so the 404's JSON body is not
lost either. The marker-post path is converted too -- it was merging, so a
successful post could echo a warning as though it were the comment URL.

**The first implementation was wrong on 5.1, and the cross-host suite caught
it.** Redirecting stderr to a file (`2>$path`) looks like the clean way to keep
the streams apart, and on PowerShell 7 it is. Windows PowerShell 5.1 writes the
FORMATTED error record there instead -- `cmd.exe : to-err`, the offending source
line, a caret ruler, CategoryInfo and FullyQualifiedErrorId -- so the diagnostic
would have arrived wrapped in a stack trace of the helper itself. Two of the new
cases failed on 5.1 and passed on 7. The shipped version merges with `2>&1` and
partitions by RECORD TYPE, which is lossless on both: PowerShell wraps native
stderr in ErrorRecords and leaves stdout as plain strings.

Five cases added, including the one that names the defect -- a diagnostic
survives when stdout is empty -- plus the 5.1 redirect hazard and an empty
stderr arriving as `''` rather than `$null`, which would throw at the call
sites under StrictMode. Eighteen cases now, passing on both hosts, and the
`-Pr 999999` message is identical on each.

Verified: exit contract intact on both hosts, sabotage suite passes on both,
encoding 615 files clean, workflow references resolve.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 04:36
@MikeGrier

Copy link
Copy Markdown
Owner Author

Addressed in 2cc009b. The approval's one suppressed finding was a regression I introduced two
commits earlier: routing gh through the stdout-only capture fixed JSON corruption and silently
emptied every broken-instrument message.

Measured which stream gh uses, because it is not uniform and that decides the fix. A REST 404
puts its JSON body on stdout (so that case still reported, which is what made it look fine),
but a network failure or a usage error leaves stdout EMPTY and says everything on stderr --
producing failed (exit 1): with nothing after the colon, blank exactly when the cause is
least guessable.

Invoke-NativeSplit now captures both streams separately: stdout stays clean for the parse,
stderr is available for the report, preferring stderr and falling back to stdout so the 404's
body is not lost either. The marker-post path is converted too -- it was merging, so a successful
post could have echoed a warning as though it were the comment URL.

Worth recording: the first implementation used a file redirect (2>$path) and was wrong on
Windows PowerShell 5.1, which writes the FORMATTED error record there -- source line, caret
ruler, CategoryInfo -- rather than raw stderr. Two of the new cases failed on 5.1 and passed on
7; the cross-host suite this PR adds is what caught it. The shipped version merges and partitions
by record type, which is lossless on both hosts.

Eighteen cases now, passing on both hosts, and the failure message is identical on each.

@MikeGrier

Copy link
Copy Markdown
Owner Author

Already fixed when this review ran. It reviewed 4b0134d and its one suppressed finding -- that
Invoke-GitHubJson discards gh stderr, leaving the Exit-Broken message without the real
diagnostic -- is the same one raised in review 5137310730 and addressed in 2cc009b, which was
pushed just after.

The exact line it quotes no longer exists: Invoke-GitHubJson now uses Invoke-NativeSplit,
which captures both streams separately so stdout stays clean for the JSON parse while stderr is
available for the failure message, preferring stderr and falling back to stdout so a REST 404's
JSON body is not lost either.

Recorded here rather than left open, because a suppressed finding creates no thread and would
otherwise keep being re-reported by this tool with nothing to say it was handled.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The changes are cohesive, well-scoped to tooling/CI/design-notes, and the only issue found is a small comment/name mismatch that doesn’t affect behavior.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

tools/run-sabotage.ps1:219

  • This comment says Get-RepoRoot goes through Invoke-Native, but the code now uses Invoke-NativeStdout. Keeping the helper name accurate matters here because the choice (merge vs stdout-only) is part of the contract being explained.
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Restatement drift, from an approving review. Converting `Get-RepoRoot` to the
stdout-only capture changed the call and left the comment above it still saying
"Through `Invoke-Native`", so the explanation named the merging helper while the
code used the discarding one. That distinction is the contract being explained
-- merge for a transcript, discard for output that is parsed -- so a wrong name
there is worse than no name.

Two comment blocks had also stacked up, the older one first, which is how the
stale name survived being read. Merged into one that says why the call is
guarded at all (any stderr redirect is terminating on 5.1, and this call's
failure mode IS stderr) and separately why it is stdout-only (the output becomes
the repository root, and a git warning merged into it would corrupt the path).

Swept the class rather than the reported line: all 37 `Invoke-Native*` call
sites across `tools/`, comparing the helper each one calls against the helper
named in its own preceding comment block. Exactly one mismatch, the reported
one. The other two converted sites read correctly.

No behaviour change. Verified anyway on both hosts: the outside-a-git-repository
case passes, the full sabotage suite passes, test-common passes, encoding and
workflow-reference checks pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 04:43
@MikeGrier

Copy link
Copy Markdown
Owner Author

Addressed in c40a2cb.

The suppressed finding was restatement drift: converting Get-RepoRoot to the stdout-only
capture changed the call and left the comment above it still saying "Through Invoke-Native".
That distinction is exactly the contract the comment exists to explain -- merge for a
transcript, discard for output that is parsed -- so a wrong name there is worse than no name.
Two comment blocks had also stacked up with the older one first, which is how the stale name
survived being read; they are now one block that says why the call is guarded at all and
separately why it is stdout-only.

Swept the class rather than the reported line: all 37 Invoke-Native* call sites across
tools/, comparing the helper each calls against the helper named in its own preceding comment
block. Exactly one mismatch, the reported one. The other two converted sites read correctly.

No behaviour change; verified on both hosts anyway.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The new review-scanner misclassifies non-collaborator permission checks (HTTP 404) as “unverifiable,” which can emit a misleading “token lacks push access” warning and should be corrected before approval.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

tools/scan-pr-reviews.ps1:347

  • Get-RetireAuthority treats ANY non-zero gh api .../collaborators/<login>/permission result as unverifiable (and later prints the "token has no push access" warning). But the GitHub API returns 404 for a user who is not a collaborator, which is an ordinary "denied" outcome; classifying that as unverifiable will produce a misleading warning whenever a non-collaborator posts a marker comment.
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@MikeGrier
MikeGrier merged commit 26dbea1 into main Sep 8, 2026
26 checks passed
@MikeGrier
MikeGrier deleted the mikegrier/tools-native-guard branch September 8, 2026 04:49
MikeGrier pushed a commit that referenced this pull request Sep 8, 2026
Brings back stage 3 of peeling this branch apart: the shared native-command
guard and the Copilot review scanner (#81). Taken from this branch, then
corrected over ten review rounds on the way through, so main holds the
successor of every file it touches.

Four conflicts, all resolved to main on that basis. Checked rather than
assumed: every line unique to this branch is one those rounds replaced --

- common.ps1: the `try`/`finally` restoration that was dead code, because the
  assignment is function-local and the caller's value was never modified.
- scan-pr-reviews.ps1: `throw`-based error paths that exited 1 and collided
  with the findings code, and the bare marker format that could be retired by
  quoting it in ordinary discussion.
- test-common.ps1: the three cases that asserted the dead restoration and
  could not fail.
- DESIGN-NOTES: the same claims in prose, plus the marker documentation, which
  main now states with the sentinel format and the write-access rule.

Verified on both PowerShell hosts, since that is the whole subject of what
merged: test-common and the sabotage suite pass under 7 and 5.1, the review
scanner runs, encoding checks 636 files clean, workflow references resolve, and
`cargo check --all-targets` is clean with no warnings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

2 participants