Skip to content

Add AI skills: analyze-debug-files, find-related-github-issues, trace-code-introduction - #2584

Draft
David Paulson (dpaulson45) wants to merge 13 commits into
mainfrom
dpaul-AISkill
Draft

Add AI skills: analyze-debug-files, find-related-github-issues, trace-code-introduction#2584
David Paulson (dpaulson45) wants to merge 13 commits into
mainfrom
dpaul-AISkill

Conversation

@dpaulson45

Copy link
Copy Markdown
Member

Summary

Adds three new Copilot AI skills for CSS-Exchange debug-log triage, plus a supporting extension to the existing find-release-tag-for-script-version skill.

New skills

  • analyze-debug-files — end-to-end pipeline that ingests a CSS-Exchange debug log, identifies the source script + release-tag baseline, walks the dependency graph via .build/Build.ps1 output, and writes a root-cause report (DebugAnalysis-<timestamp>.md) into the caller-supplied directory.
  • find-related-github-issues — searches microsoft/CSS-Exchange issues for prior reports of a given exception. Invocable directly during triage or as a Step 7 sub-invocation of analyze-debug-files.
  • trace-code-introduction — resolves a specific source line + range at a pinned SHA to the commit that introduced it (via git log -L and git blame). Used by analyze-debug-files to attribute unhandled exceptions to their originating change.

Extension

  • find-release-tag-for-script-version now emits a ConfirmedCommitSha field alongside ConfirmedTag. analyze-debug-files needs the 40-character SHA to build a scratch worktree deterministically and to key its per-SHA dependency cache; a tag alone is ambiguous once tags are re-pointed.

Notable design decisions

  • Per-SHA dependency cache at %LOCALAPPDATA%\CSS-Exchange\dependency-cache\<sha>\. On cache hit, analyze-debug-files skips the ~107 s .build/Build.ps1 invocation (measured ~74× speedup). The cache is worktree-independent — XML keys are normalized from absolute worktree paths to repo-relative paths so Steps 6-8 can read source with git show <sha>:<path> regardless of which run produced the cache.
  • Report fidelity guardrails — Step 8's template enforces strict source-slice rules (# L<n> trailing annotations, no ellipses, consecutive-line check) with a post-render regex assertion so cited source cannot be silently truncated or paraphrased.
  • Trust model — personal-machine, no ownership preflight; the caller's supplied output directory is trusted, and the report is written directly there.
  • Input validation — all three new skills validate gh CLI arguments, git pathspecs, and shell-metacharacter surfaces before invoking subprocesses (see Get-SafeQueryPhrase, allow-list regexes in Trace-CodeIntroduction.ps1).

Testing

  • Pre-commit hooks (PSScriptAnalyzer + formatter) pass on all 4 commits.
  • .build\SpellCheck.ps1 — clean (0 issues, 810 files).
  • Manual end-to-end validated against a real HealthChecker debug log (PS 4.0 / Server 2012 R2 / -ForceLegacy scenario). Cache miss + cache hit both verified; normalized XML keys resolve via git show <sha>:<path>.

Commits

  1. Extend find-release-tag-for-script-version with ConfirmedCommitSha + cspell dictionary additions.
  2. Add find-related-github-issues skill.
  3. Add trace-code-introduction skill.
  4. Add analyze-debug-files skill.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Adds a Copilot skill that maps a CSS-Exchange script name + version stamp
(YY.MM.DD.HHMM) back to the earliest GitHub release tag that shipped that
build, by walking releases in ascending tag-date order and matching the
File + Version pair in each release's ScriptVersions.csv.

Includes:
- Find-ReleaseTagForScriptVersion.ps1 helper with structured result
  (Script, Version, ConfirmedTag, SHA256Hash, Status, WindowExhausted,
  EarlierGaps, Tried[]) and status enum distinguishing match-earliest,
  match-possibly-not-earliest, not-found-complete, not-found-inconclusive,
  and not-found-no-candidates.
- SKILL.md documenting the workflow, status semantics, per-candidate
  Tried statuses, and WorkFolder rejection rules.

Security posture:
- Repository pinned to github.com/<owner>/<repo> so an inherited GH_HOST
  cannot redirect requests or leak an ambient enterprise token.
- WorkFolder rejects UNC/extended-UNC/provider-qualified paths,
  non-FileSystem PSDrives, network/CD-ROM/unknown drive types,
  SUBST/raw-DOS-device aliases (via QueryDosDevice), and paths whose
  volume root or any existing ancestor is a filesystem reparse point.
- Downloaded CSVs go through strict header, per-row, and per-field
  validation; File comparison is ordinal, and Version/SHA256 formats
  are regex-checked before any value is surfaced.
- All caller-visible strings from downloaded content are size-capped
  and stripped of control characters.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a Get-CommitShaForTag helper that resolves the matched release tag
to its target commit SHA via the GitHub API, and surfaces it on the
result object as ConfirmedCommitSha alongside ConfirmedTag. The resolver
pins to github.com and validates the SHA shape before returning; any
failure yields $null rather than throwing so the primary tag match is
never blocked. Downstream skills (analyze-debug-files) require a
40-character SHA to build a scratch worktree deterministically and to
key the per-SHA dependency cache; a tag alone is ambiguous once tags
are re-pointed.

Also adds five cspell dictionary entries (metacharacters, misattributed,
misrouting, triaging, worktree) used across the new skills.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New Copilot skill that, given an exception message and script name,
searches microsoft/CSS-Exchange issues for prior reports of the same
failure. Invoked directly when triaging a fresh exception, or as a
Step 7 sub-invocation of analyze-debug-files after each unhandled
finding.

Provides:
- SKILL.md contract: input shape, output shape, scoring criteria,
  hard cap on results returned to the caller.
- Find-RelatedGitHubIssues.ps1: sanitizes user-controlled exception
  content against gh CLI argument injection (Get-SafeQueryPhrase
  strips quotes, search-syntax metacharacters, backticks, newlines),
  runs 3-4 randomized query variants, and deduplicates by issue
  number before returning ranked candidates.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New Copilot skill that traces a specific source line back to the commit
that introduced it. Used by analyze-debug-files Step 7 to determine
whether an unhandled exception was authored inside the failing script's
own history (own-repo introduction) or inherited from a Shared/ file.

Provides:
- SKILL.md contract: input (repo-relative path + line range + SHA),
  output shape, caveat that git log -L reports the whole enclosing
  function/block so sibling statements added later can be
  misattributed as the introducing commit.
- Trace-CodeIntroduction.ps1: validates repository and path against
  a strict allow-list (no shell metacharacters, no traversal, no
  absolute or UNC paths), invokes git log -L and git blame against
  the pinned worktree, and returns a structured record with commit
  SHA, author, date, and message.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New Copilot skill that analyzes CSS-Exchange debug log files, identifies
the source script + release-tag baseline, and produces a root-cause
report for unhandled exceptions.

Pipeline:
- Step 1a: streaming inventory (Get-DebugFileMetadata.ps1) — detects
  CSS-Exchange output shape; hard-stops if the file is not a
  recognized script log.
- Step 2-4: identifies the script + version and confirms the release
  tag + commit SHA via find-release-tag-for-script-version.
- Step 5: obtains the dependency graph. Loads from a per-SHA cache at
  $env:LOCALAPPDATA\CSS-Exchange\dependency-cache\<sha>\ when
  available; otherwise materializes a scratch worktree, runs
  .build/Build.ps1, and populates the cache. Cache hit avoids the
  ~107-second Build.ps1 run (measured 74x speedup on the primed
  path). XML keys are normalized from absolute worktree paths to
  repo-relative form so Steps 6-8 can read source with
  git show <sha>:<path> regardless of which branch produced the XML.
- Step 6-7: per-finding source reads and BFS across the dependency
  graph, with optional sub-invocations of trace-code-introduction and
  find-related-github-issues.
- Step 8: renders DebugAnalysis-<timestamp>.md into the caller-supplied
  directory. Includes STRICT source-slice fidelity rules and a
  post-render assertion (regex-enforced format, forbidden ellipses,
  consecutive-line check) so cited source cannot be silently truncated
  or paraphrased.

Trust model: personal machine, no ownership preflight, report written
directly to the caller-supplied directory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dpaulson45

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

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

Unresolved critical and moderate findings remain.

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

Pull request overview

Adds three Copilot skills for CSS-Exchange debug-log triage, GitHub issue correlation, and code-introduction tracing, plus confirmed commit-SHA support for release lookup.

Changes:

  • Adds debug-log metadata parsing, dependency caching, and report generation.
  • Adds related GitHub issue search and source-history tracing.
  • Extends release resolution with ConfirmedCommitSha and updates spelling data.
File summaries
File Description
.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1 Traces source ranges to introducing commits.
.github/skills/trace-code-introduction/SKILL.md Documents code-introduction tracing.
.github/skills/find-release-tag-for-script-version/SKILL.md Documents confirmed commit-SHA output.
.github/skills/find-release-tag-for-script-version/Find-ReleaseTagForScriptVersion.ps1 Resolves release tags and commit SHAs.
.github/skills/find-related-github-issues/SKILL.md Documents related-issue lookup.
.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1 Searches and classifies related issues.
.github/skills/analyze-debug-files/SKILL.md Defines the end-to-end analysis workflow.
.github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1 Parses and sanitizes debug logs.
.build/cspell-words.txt Adds skill-specific dictionary terms.
Review details

Suppressed comments (12)

.github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1:555

  • The sanitizer removes C0 controls and DEL, but it leaves the C1 range U+0080–U+009F. Those characters can be returned in snippets and violate the report's explicit no-stray-C0/C1 assertion. Extend the character class to cover \x7F-\x9F.
    '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]',

.github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1:672

  • The regex-timeout fallback uses code -ge 0x20 as its printable test, which also accepts C1 controls U+0080–U+009F. A timeout therefore bypasses the normal sanitizer's intended output guarantee and can still place those controls in the report. Use the same printable range as the normal path here.
            if (($code -ge 0x20 -and $code -ne 0x7F) -or $code -eq 0x09) {

.github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1:992

  • HealthChecker's Get-ErrorsThatOccurred also emits ----Errors that occurred that was not handled remotely---- after the normal unhandled footer (see Diagnostics/HealthChecker/Helpers/Get-ErrorsThatOccurred.ps1:37-40). This state machine only recognizes the handled/unhandled headers, so a run with hidden remote errors is still marked SummaryComplete with UnhandledCount=0, causing the analyzer to report a clean run while silently dropping those errors. Add a remote-section signal/count or downgrade completion when that header is present.
                } elseif ($Script:UnhandledSummaryHeaderRegex.IsMatch($line)) {
                    $summaryState = 'unhandled'
                    $summaryUnhandledCount = 0
                    $unhandledHeaderLine = $lineNumber
                    $unhandledHeaderCount++

.github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1:417

  • The logger formats [System.DateTime]::Now with the machine's culture (Shared/LoggerFunctions.ps1:60-63), so the common en-US output includes an AM/PM suffix. This regex and the AcceptedTimestampFormats below reject that form, leaving $ts null on every timestamped line; version candidates, completion signals, summary events, and body-evidence correlation then become unavailable for ordinary HealthChecker logs. Parse the logger's culture-dependent format (or add the 12-hour forms) consistently to the framing regex and accepted formats.
    '\A\s*\[(?<ts>[0-9]{1,2}/[0-9]{1,2}/[0-9]{4}\s+[0-9]{1,2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?)\]',

.github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1:490

  • This explanation cites Write-Host, but Get-ErrorsThatOccurred.ps1 emits both summary headers with Write-Verbose. The headers are untimestamped because the message starts with CRLF and the logger prefixes the timestamp before those newlines; documenting the actual mechanism is important because this comment justifies the anchored header regex.
# cannot forge an authoritative summary. HealthChecker emits these as
# untimestamped lines (the writers in Get-ErrorsThatOccurred.ps1 use
# `Write-Host`, not `Write-Verbose`), so anchor to line start/end.

.github/skills/analyze-debug-files/SKILL.md:576

  • The suffix loop does not actually require a unique match: an absolute key can end with multiple repository paths, and the first hashtable key wins. For example, a key ending in .../Shared/OutputOverrides/Write-Error.ps1 can also match a shorter nested path if one exists, causing the dependency graph to resolve to the wrong source file. Collect suffix matches and accept only the unique/longest path (or reject ambiguity).
            if ($winKey.EndsWith($k, [System.StringComparison]::OrdinalIgnoreCase)) {
                return $Index[$k]

.github/skills/analyze-debug-files/SKILL.md:479

  • A cache miss can be caused by an existing corrupt or incomplete $cacheDir (for example, invalid metadata). This branch deletes the newly built temp directory whenever the final directory exists, leaving the bad entry in place, so every subsequent run rebuilds and remains BuildOnly. Distinguish a valid concurrent winner from a stale invalid entry and replace or quarantine the latter.
                if (Test-Path -LiteralPath $cacheDir -PathType Container) {
                    Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue
                } else {
                    Move-Item -LiteralPath $tempDir -Destination $cacheDir
                    $materializationSource = 'BuildAndCached'

.github/skills/analyze-debug-files/SKILL.md:1041

  • find-related-github-issues defines PartialLookup as usable retained results with incomplete coverage, not as an unavailable lookup. Treating every non-Ok status as unavailable would hide those matches from the report and contradict the sibling skill's contract; render retained results plus an incompleteness note for PartialLookup, reserving "unavailable" for the failure statuses.
**Failure modes.** Both skills return a `Status` field. When
`Status -ne 'Ok'`, render a single-line "lookup unavailable" note in
the corresponding subsection and continue with the report. A failing
provenance lookup MUST NOT abort the report.

.github/skills/analyze-debug-files/SKILL.md:437

  • This invocation is still subject to the caller's $PSNativeCommandUseErrorActionPreference. When that preference is $true and Build.ps1 exits nonzero—the condition this step explicitly says may be cosmetic—PowerShell throws before the XML existence checks, so the documented BuildOnly/cache flow aborts instead of using the generated XML. Disable native-error promotion only around this invocation and restore the caller's value in finally, or invoke the process through an API that captures the exit code without throwing.
            & pwsh -NoProfile -File (Join-Path $worktreeRoot '.build\Build.ps1')

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:88

  • Mandatory does not reject whitespace-only input. In that case normalization produces no query, the final status falls through to Ok with empty results, and callers can incorrectly report that no related issue exists instead of an invalid/failed lookup. Reject an empty or whitespace-only top-level exception before building the query list.
    [string]$TopLevelException,

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:128

  • These replacements create literal placeholder tokens, but the subsequent exact GitHub query is built from the normalized phrase. For Failure at C:\a\b, the query becomes "Failure at path" after delimiter stripping; GitHub does not treat path as a wildcard, so an issue saying Failure at C:\other\d is never returned and normalization cannot provide the advertised stable matching. Build search phrases that omit volatile spans or use unquoted invariant terms, while keeping normalization for local classification.
    $s = [regex]::Replace($s, '[A-Za-z]:\\[^\s"'']+', '<path>')
    # Strip UNC paths.
    $s = [regex]::Replace($s, '\\\\[^\s"'']+', '<unc>')
    # Strip randomized temp module names like tmpEXO_3fzjpepe.o0p
    $s = [regex]::Replace($s, 'tmpEXO_[A-Za-z0-9]+(?:\.[A-Za-z0-9]+)?', 'tmpEXO_<random>')

.github/skills/find-release-tag-for-script-version/Find-ReleaseTagForScriptVersion.ps1:358

  • For a valid empty gh release list response ([]), ConvertFrom-Json can produce $null (especially on Windows PowerShell 5.1). @($parsedReleases) then creates an array containing one null element, so the loop below throws Unexpected release entry shape from gh instead of reaching the not-found-no-candidates result. Preserve $null as an empty array before iterating.
    $releases = @($parsedReleases)
  • Files reviewed: 9/9 changed files
  • Comments generated: 4
  • Review effort level: Lite

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

Comment thread .github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1 Outdated
Comment thread .github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1
Comment thread .github/skills/analyze-debug-files/SKILL.md Outdated
Comment thread .github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1 Outdated
@tweekerz
Rob Whaley (tweekerz) marked this pull request as ready for review September 11, 2026 01:03
@tweekerz
Rob Whaley (tweekerz) requested a review from a team as a code owner September 11, 2026 01:03
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@tweekerz
Rob Whaley (tweekerz) marked this pull request as draft September 11, 2026 01:03
Get-DebugFileMetadata.ps1
- (#1) ControlCharRegex now rejects C1 controls (\x80-\x9F) as well
  as C0/DEL. The report contract forbids both ranges; leaving C1 in
  would let 8-bit terminal sequences reach rendered snippets.
- (#2) Regex-timeout fallback loop matches the same character range
  as the normal path, so a timeout cannot bypass the sanitizer
  guarantee.
- (#3) Recognize HealthChecker's second unhandled-errors section
  (`----Errors that occurred that was not handled remotely----`,
  emitted by Diagnostics/HealthChecker/Helpers/Get-ErrorsThatOccurred.ps1:37-40
  when Test-HiddenJobUnhandledErrors is true). Treated as another
  entry point into the 'unhandled' state so events counted here
  contribute to UnhandledSummaryEvents. New RemoteUnhandledSectionSeen
  flag on the result surfaces the section for auditing.
- (#4) TimestampRegex, SummaryFooterRegex, ErrorIndexRegex, and
  AcceptedTimestampFormats now accept the 12-hour AM/PM shape that
  `[System.DateTime]::Now.ToString()` produces under the default
  en-US culture (LoggerFunctions.ps1:62). Before this fix, every
  timestamped line in an en-US HealthChecker log fell through the
  timestamp path, breaking version-candidate collection, summary
  framing, and body-evidence correlation.
- (#5) Corrected the summary-header comment: Get-ErrorsThatOccurred.ps1
  emits both handled/unhandled headers via `Write-Verbose`, not
  `Write-Host`. The headers arrive untimestamped because the message
  starts with CRLF and the logger prefixes the timestamp before the
  newlines.

analyze-debug-files/SKILL.md
- (#6) Resolve-RepoRelativePath now requires an unambiguous longest
  suffix match and warns-then-drops on ties. The previous
  first-match-wins behavior could resolve to the wrong source file
  when multiple hashtable keys ended with the same repository path.
- (#7) Cache-hit race handling now validates the existing \
  (metadata.json parseable, SchemaVersion=1, BaselineSha matches, both
  XML files present) before accepting the concurrent winner. A stale
  or corrupt entry is quarantined as `<sha>.corrupt.<ts>.<pid>` and
  the new build takes its place, so a bad cache entry cannot pin every
  future run to BuildOnly forever.
- (#8) Distinguish Status='PartialLookup' (render results + note
  incompleteness) from the other non-Ok statuses (render 'lookup
  unavailable'). find-related-github-issues explicitly defines
  PartialLookup as a usable-but-incomplete result; the previous
  instruction lumped it into 'unavailable' and hid retained matches.
- (#9) Save/restore \False around
  the `pwsh Build.ps1` invocation. On PS 7.4+ with the preference
  enabled, Build.ps1's cosmetic non-zero exit was promoted to
  NativeCommandExitException before the XML existence check ran,
  which would break the BuildOnly/BuildAndCached fallback.

find-related-github-issues/Find-RelatedGitHubIssues.ps1
- (#10) Reject empty or whitespace-only \ via
  ValidateScript on the parameter binding. Previously the mandatory
  attribute alone let whitespace-only input reach the query builder,
  which then returned Status='Ok' with zero results and misrepresented
  'no related issue found' to downstream reports.
- (#11) Added ConvertTo-SearchablePhrase, which strips the `<path>`,
  `<unc>`, `<guid>`, `<ts>`, and `tmpEXO_<random>` placeholders
  before the phrase reaches Get-DistinctivePhrase and the `gh` query.
  Those placeholders are useful for local classification but are NOT
  wildcards in GitHub's search grammar — leaving them in a quoted
  exact-substring query silently guaranteed zero matches.

find-release-tag-for-script-version/Find-ReleaseTagForScriptVersion.ps1
- (#12) Coerce ConvertFrom-Json's \ output (Windows PowerShell 5.1
  behavior for a valid empty array `[]`) to an empty array before
  iterating. Wrapping \ with @() produced a single-element array
  containing \, which the shape check below rejected as
  'Unexpected release entry shape from gh' instead of returning the
  clean 'not-found-no-candidates' result.

All changes verified: PSScriptAnalyzer 0 findings, SpellCheck 0 issues,
functional probes confirmed the remote-unhandled state machine addition,
AM/PM timestamp parsing, whitespace rejection, and placeholder stripping.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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

Unresolved critical and moderate findings affect caching, report safety, log parsing, and issue/provenance handling.

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

Review details

Suppressed comments (14)

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

.github/skills/analyze-debug-files/SKILL.md:401

  • Cache validity is decided from metadata.json alone, then the XML files are imported outside that validation try. A truncated or malformed dependency XML therefore aborts the analysis on a cache hit instead of being quarantined/rebuilt, despite the cache-population path explicitly treating malformed entries as recoverable cache misses.

.github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1:641

  • These completion patterns only accept 24-hour timestamps, but the parser explicitly accepts the en-US 12-hour form (h:mm:ss tt) and LoggerFunctions.ps1 emits the current culture's DateTime.ToString(). A normal en-US line such as [9/11/2026 1:14:30 PM] : No errors occurred in the script. therefore produces no completion signal and can be classified as critically incomplete; apply the same optional AM|PM suffix to all three terminal-message patterns.
        Pattern = [regex]::new('\A\s*\[[0-9]{1,2}/[0-9]{1,2}/[0-9]{4}\s+[0-9]{1,2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?\]\s*:\s*No\s+errors\s+occurred\s+in\s+the\s+script\.\s*\z', [System.Text.RegularExpressions.RegexOptions]::Compiled, $Script:RegexTimeout)

.github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1:940

  • The file is checked with Test-IsSafeLocalFile during enumeration, but this later opens it again by pathname. A local replacement between those operations can turn the path into a reparse point or redirect it outside the validated directory, bypassing the local-file trust boundary; keep and read from the validated handle (or revalidate the opened handle's final target) instead of reopening the name.
        $stream = [System.IO.File]::Open($FileInfo.FullName, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)

.github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1:1094

  • This only validates the timestamp's shape, while Get-LineTimestamp returns $null for semantically invalid values such as month/day/hour 99. The branch then creates a SummaryEvent with a null Timestamp, and Step 7 dereferences SummaryEvent.Timestamp.AddSeconds(-60), so a malformed log can abort the analysis instead of being treated as an invalid record.
                } elseif ($Script:ErrorIndexRegex.IsMatch($line)) {

.github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1:1064

  • Setting $summaryState here does not actually parse any remote error: WriteRemoteErrorInformation emits a Remote Error Information block with Exception Message: fields, not an Error Index: header. The event-creation branch below is gated by $ErrorIndexRegex, so remote failures are skipped and UnhandledCount/SummaryEvents omit them; incrementing $unhandledHeaderCount also makes every valid remote section set MultipleSummaryBlocksDetected, causing Step 6 to skip correlation. Add a remote-record parser (or change the producer format) and treat this section as part of the same summary lifecycle.
                    $summaryState = 'unhandled'
                    if ($null -eq $summaryUnhandledCount) { $summaryUnhandledCount = 0 }
                    if ($null -eq $unhandledHeaderLine) { $unhandledHeaderLine = $lineNumber }
                    $unhandledHeaderCount++
                    if ($unhandledHeaderCount -gt 1) { $multipleSummaryBlocksDetected = $true }

.github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1:398

  • Some repository logs include the extension in the base name (ConfigureExchangeHybridApplication.ps1-Debug_<timestamp>.txt, documented at docs/Hybrid/ConfigureExchangeHybridApplication.md:16). When that filename matches, $Matches['name'] already ends in .ps1, so this appends a second extension and returns ConfigureExchangeHybridApplication.ps1.ps1; Step 2 then cannot resolve the release baseline for the log.
            ScriptName      = "$($Matches['name']).ps1"

.github/skills/analyze-debug-files/SKILL.md:1190

  • On the documented cache-hit path $worktreeRoot is intentionally $null and Steps 6-8 read source with git show; therefore this requirement to resolve every permalink against an "alive worktree" cannot be satisfied and may cause Step 8 to reject valid cache-hit reports. State the invariant in terms of the pinned Git tree, with the worktree and git show paths as the two materialization mechanisms.
  Every permalink path resolves against the alive worktree at that

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:169

  • Replacing a normalized placeholder with a space makes an exact phrase cross a gap that cannot exist in a real issue body: Failed to open <path> for user becomes Failed to open for user, while the normalized issue still contains <path>. Because the query is then wrapped in quotes, path/GUID-bearing exceptions are likely to miss their duplicate entirely; preserve searchable fragments or avoid exact-phrase matching when placeholders were removed.
    # Drop every placeholder token; GitHub search cannot use them as
    # wildcards. Each placeholder is replaced with a single space so
    # word boundaries stay intact.
    $s = $s -replace '<path>', ' '

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:488

  • If normalization removes the only exception content (for example, an input consisting only of a path, GUID, or timestamp), $queries remains empty and this branch falls through to Status = 'Ok'. That makes the caller report “no related issue” even though no GitHub search was performed; return Error with an explicit sanitization detail when $queryCount -eq 0.
if ($queryCount -gt 0 -and $failureCount -eq $queryCount) {

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:139

  • The repository logger prefixes lines with culture-dependent [DateTime]::Now.ToString() output; for en-US this is commonly [9/11/2026 1:14:31 PM]. This pattern accepts only two-digit 24-hour timestamps, so ordinary debug exception text keeps volatile timestamps and identical failures across runs do not normalize as documented. Accept one- or two-digit date/time components and the optional AM/PM suffix, as Get-DebugFileMetadata.ps1 already does.
    $s = [regex]::Replace($s, '\[\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}(?:\.\d+)?\]', '<ts>')

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:131

  • The Windows-path pattern stops at the first whitespace, so a common path such as C:\Program Files\Microsoft\Exchange Server\... becomes <path> Files\Microsoft\Exchange Server\... rather than one placeholder. This leaves a machine-dependent tail in the searchable/classification phrase and can leak or mismatch path text; normalize quoted/space-containing paths with a real path boundary instead of [^{\s}]+.
    $s = [regex]::Replace($s, '[A-Za-z]:\\[^\s"'']+', '<path>')

.github/skills/find-release-tag-for-script-version/Find-ReleaseTagForScriptVersion.ps1:266

  • This defense-in-depth check still accepts traversal-shaped repository components such as owner/.. because both components allow only a non-empty character class, not an alphanumeric boundary. Since the value is interpolated into GitHub API paths, use the same start/end-alphanumeric repository pattern enforced by the public related-issues helper.
    if ($Repository -notmatch '\A[A-Za-z0-9._-]+/[A-Za-z0-9._-]+\z') { return $null }

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:350

  • This branch explicitly says the phase-3 diff failed and leaves the regression assessment indeterminate, but still returns Status = 'Ok' with an empty StatusDetail. The caller's contract renders any Ok result normally, so a failed provenance lookup is presented as successful; return a non-Ok status and preserve the failure detail.
            Status               = 'Ok'
            StatusDetail         = ''

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:574

  • A successful git blame can still produce no parseable porcelain attribution (for example, an empty/out-of-range result or boundary-only output). In that case the final else indexes $blameShas[0] under strict mode and throws instead of returning the documented Unavailable provenance state.
            if (-not $blameOk) {
  • Files reviewed: 9/9 changed files
  • Comments generated: 5
  • Review effort level: Lite

Comment thread .github/skills/analyze-debug-files/SKILL.md
Comment thread .github/skills/analyze-debug-files/SKILL.md Outdated
Comment thread .github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1
Comment thread .github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1 Outdated
Comment thread .github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1 Outdated

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

Ten moderate findings remain across the cache, issue-search, release-lookup, and tracing implementations.

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

Review details

Suppressed comments (8)

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:173

  • These replacements leave surrounding quote characters behind. For a normal PowerShell message such as Path 'C:\foo' was not found, normalization produces a query like Path ' ' was not found; the exact-phrase search then looks for a literal blank quoted path, so the corresponding issue with its real path is missed. Remove optional quote delimiters together with each placeholder before collapsing whitespace.
    $s = $s -replace '<path>', ' '
    $s = $s -replace '<unc>', ' '
    $s = $s -replace '<guid>', ' '
    $s = $s -replace '<ts>', ' '
    $s = $s -replace 'tmpEXO_<random>', ' '

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:217

  • Get-SafeQueryPhrase is also used for the non-exact identifier queries, but this filter leaves leading - operators intact. A log-controlled discriminator such as -secret is emitted unquoted by Invoke-GhSearch and changes GitHub's query semantics instead of being searched as data. Neutralize leading hyphens, or quote all untrusted query fragments, before passing them to gh.
    $p = [regex]::Replace($p, '[\(\)\[\]\{\}\<\>:]', ' ')

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:331

  • These variables are later used as the $matchTop/$matchInner criteria for classifying returned issues, but Get-DistinctivePhrase truncates messages longer than 80 characters. That means an issue sharing only the common prefix can be reported as Similar or Duplicate, even when the rest of the exception differs; the latter classification can cause a real failure to be closed as a duplicate. Keep the shortened phrases for GitHub queries, but use the full normalized/searchable phrases for local classification.
$topPhrase = Get-DistinctivePhrase -NormalizedText $searchTop
$innerPhrase = Get-DistinctivePhrase -NormalizedText $searchInner

.github/skills/find-release-tag-for-script-version/Find-ReleaseTagForScriptVersion.ps1:333

  • This helper does not disable or otherwise guard $PSNativeCommandUseErrorActionPreference, unlike the other new native-command helpers. If a caller has that preference enabled together with ErrorActionPreference = 'Stop', a failed gh release list throws before $LASTEXITCODE is inspected, so the documented structured failure result and cleanup path are bypassed. Set the native-command preference to false for the guarded calls (restoring the caller's value afterward) or catch the promoted exception and convert it to the result status.
    $releaseJson = gh release list --repo $qualifiedRepository --limit $releaseListLimit --json $jsonFields 2>&1
    if ($LASTEXITCODE -ne 0) {
        throw "Failed to list releases from ${Repository}: $(ConvertTo-SafeDetail ($releaseJson -join ' '))"

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:384

  • The -eq, -ne, -and, and -or alternatives are not word-bounded, so identifiers such as Invoke-eqThing in an added or removed line are counted as conditional operators after strings and comments are stripped. That can incorrectly produce Intentional or PossibleRegression verdicts; add a word boundary to each operator token.
    $guardPattern = '-eq|-ne|-and|-or|-not\b|\bif\b|\belseif\b|\bIsNullOr(?:Empty|WhiteSpace)\b|\bTest-Path\b|\bthrow\b|\bcontinue\b|\breturn\b'

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:373

  • These predicates discard every diff line whose payload starts with the same marker character: a real added source line +foo is emitted as ++foo, and a removed -foo as --foo. Such lines are skipped along with the +++/--- file headers, so AddedLines/RemovedLines and the guard-keyword heuristic can be wrong for valid source or here-string content. Exclude only the actual file-header forms (or parse hunk metadata) while retaining doubled-marker source lines.
        if ($ln -match '^\+[^+]' -or ($ln -match '^\+$')) {
            $addedLines += $ln.Substring(1)
        } elseif ($ln -match '^-[^-]' -or ($ln -match '^-$')) {
            $removedLines += $ln.Substring(1)

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:392

  • ParseInput is called once per diff line, so it cannot retain state across a multiline here-string. A changed line inside an @"..."@ or @'...'@ body is therefore parsed as executable PowerShell (or handled by the fallback regex), allowing prose such as if or -eq to affect $keywordsAdded/$keywordsRemoved and produce a false regression verdict. Parse the complete hunk or carry here-string state across lines before extracting guard keywords.
            [void][System.Management.Automation.Language.Parser]::ParseInput(
                $line, [ref]$tokens, [ref]$errors)

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:362

  • When Phase 3 diff extraction fails, this branch returns Status = 'Ok' with an empty StatusDetail. The analyze-debug-files contract treats only non-Ok provenance results as unavailable, so a failed diff is rendered as a successful lookup with an Indeterminate verdict and the failure is hidden. Return an error status and preserve the diff failure in StatusDetail.
            Provenance           = $null
            Status               = 'Ok'
            StatusDetail         = ''
  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread .github/skills/analyze-debug-files/SKILL.md Outdated

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

Unresolved moderate and critical findings remain.

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

Review details

Suppressed comments (11)

.github/skills/analyze-debug-files/SKILL.md:1401

  • This requirement conflicts with the cache-hit flow: lines 361-371 state that no worktree is materialized on a cache hit, but this says every permalink must resolve against an alive worktree. An implementation following the requirement literally will either rebuild the worktree unnecessarily or fail all cached runs when $worktreeRoot is null. Define the integrity check in terms of the pinned tree via git show (with the worktree as an optional cache-miss implementation).
  Every permalink path resolves against the alive worktree at that
  SHA. Every source token quoted in a `Cause` narrative appears in
  the linked line slice fetched via
  `git show $baseline.ConfirmedCommitSha:<path>`; convert 1-based

.github/skills/analyze-debug-files/SKILL.md:114

  • Get-EmptyFileResult preserves filename-derived confidence for Empty, Oversize, and Unreadable entries, so a directory containing only an oversized or empty HealthChecker-Debug... file passes this gate despite having no parsed input. Steps 2 and 3 then operate on an empty parsed set and can prompt for a version/baseline; include Status -eq 'Parsed' in this predicate.
Rule: if the inventory produces zero files whose `ScriptNameConfidence`
is `High` or `Medium`, print the following concise message and STOP the

.github/skills/analyze-debug-files/SKILL.md:155

  • This sentence is missing the noun describing what TerminationLineText contains, so the field documentation reads "sanitized text of that belongs" and is unclear to callers.
  belongs to the section and closes the run of errors), whereas
  `NextErrorIndex`, `NextRemoteRecord`, and `SectionHeaderTransition`

.github/skills/analyze-debug-files/SKILL.md:400

  • Step 5 is gated only on PowerShell 7+, but this cache-root construction unconditionally passes $env:LOCALAPPDATA to Join-Path. On PowerShell 7 on Linux/macOS, where that variable is normally unset, the skill aborts before materializing the baseline even though the inventory helper accepts non-Windows absolute paths. Add an explicit Windows prerequisite or choose a platform-appropriate writable cache root.
$cacheRoot = Join-Path $env:LOCALAPPDATA 'CSS-Exchange\dependency-cache'

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:457

  • When the normalized top-level and inner phrases are identical, the query builder suppresses the inner query, but both $matchTop and $matchInner become true for the same single phrase and this branch reports a Duplicate. The documented duplicate contract requires two distinct exception phrases; make the inner match conditional on $innerPhrase -ne $topPhrase.
    $matchInner = $innerPhrase -and $normBody.IndexOf($innerPhrase, [System.StringComparison]::OrdinalIgnoreCase) -ge 0

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:349

  • This query is described as finding script-scoped issues that do not quote the exception, but classification later accepts only a full $topPhrase/$innerPhrase (or a function-only match, which is discarded). A result found solely by <scriptStem> <topToken> is therefore dropped at lines 487–488, so this query cannot provide the advertised coverage. Either remove it or carry query-specific evidence into classification with an explicitly documented similarity rule.
        # A single distinctive token from the top-level phrase, plus the
        # script stem, catches script-scoped issues that do not quote the
        # exception verbatim.
        $topTokens = ($topPhrase -split '\s+' | Where-Object { $_.Length -ge 6 }) | Select-Object -First 1
        if ($topTokens) { $queries += @{ Query = "$scriptStem $topTokens"; ExactPhrase = $false } }

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:457

  • $topPhrase and $innerPhrase are the ≤80-character distinctive search prefixes returned by Get-DistinctivePhrase, but these same prefixes drive local classification. A long exception whose first 80 characters match another issue can therefore satisfy both matchTop and matchInner and be reported as a duplicate even when the remainder differs. Keep the full normalized/searchable phrases for classification and reserve the shortened phrases for GitHub query construction.
    $matchTop = $topPhrase -and $normBody.IndexOf($topPhrase, [System.StringComparison]::OrdinalIgnoreCase) -ge 0
    $matchInner = $innerPhrase -and $normBody.IndexOf($innerPhrase, [System.StringComparison]::OrdinalIgnoreCase) -ge 0

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:224

  • When git log -L finds no history, this branch returns Status = 'Ok' with no introducing commit. The analyze-debug-files caller treats Ok as a successful provenance lookup, so it can render an empty/indeterminate result instead of the documented "code-introduction lookup unavailable" outcome. Return a non-Ok status such as RangeUnavailable and populate StatusDetail here.
            RegressionAssessment = [PSCustomObject]@{ Verdict = 'Indeterminate'; Reasoning = 'git log -L returned no history for the range.' }
            Provenance           = $null
            Status               = 'Ok'
            StatusDetail         = ''

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:374

  • The ^[^+] / ^[^-] tests intentionally skip every line beginning with two identical diff/code characters, not just unified-diff headers. A valid added ++$counter or removed --$counter line is therefore omitted from AddedLines/RemovedLines and from the guard-token heuristic, which can produce incorrect summaries and verdicts. Skip only +++ a|b/path / --- a|b/path header lines, then classify any remaining line starting with + or -.
        if ($ln -match '^\+[^+]' -or ($ln -match '^\+$')) {
            $addedLines += $ln.Substring(1)
        } elseif ($ln -match '^-[^-]' -or ($ln -match '^-$')) {
            $removedLines += $ln.Substring(1)
        }

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:511

  • gh api normally emits formatted, multi-line JSON, so native command output is an array of lines in PowerShell. Piping that array into ConvertFrom-Json parses each line separately; this optional lookup will usually fall into the catch and lose the PR number. Join $apiOut with newlines (or use a raw-output equivalent) before parsing.
                    $apiParsed = $apiOut | ConvertFrom-Json -ErrorAction Stop

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:539

  • The gh pr view --json response has the same multi-line native-output issue: piping $prJson directly to ConvertFrom-Json can parse one line at a time and make the PR details disappear through the optional-data catch. Join the captured lines before deserializing.
                    $prObj = $prJson | ConvertFrom-Json -ErrorAction Stop
  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread .github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1
Comment thread .github/skills/analyze-debug-files/SKILL.md

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

A critical path-validation issue and multiple moderate correctness and cleanup issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (8)

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

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:655

  • gh api normally returns pretty-printed JSON as multiple native-command output lines. Piping $apiOut directly sends those lines separately to ConvertFrom-Json, so a valid PR list falls into the catch block and PullRequest is silently omitted. Join the output before parsing, as the release helper does.

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

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:185

  • The placeholders are removed and the remaining text is submitted as one exact quoted phrase. For example, Cannot open <path> in module becomes "Cannot open in module", which cannot match an issue containing the concrete path between open and in module; internal volatile values therefore cause false-negative searches. Build the query from stable segments/tokens (or otherwise support an internal wildcard) instead of deleting the intervening content while retaining exact-phrase matching.
    $s = $s -replace '<path>', ' '
    $s = $s -replace '<unc>', ' '
    $s = $s -replace '<guid>', ' '
    $s = $s -replace '<ts>', ' '
    $s = $s -replace 'tmpEXO_<random>', ' '

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:483

  • $topPhrase and $innerPhrase are the shortened 80-character query prefixes from Get-DistinctivePhrase, but the skill contract defines Duplicate/Similar using the normalized exception phrases. A long exception whose top and inner prefixes match an unrelated issue can therefore be classified as a duplicate even when the rest of both messages differs. Use the full searchable phrases for local classification and keep the shortened values only for GitHub queries.
    $matchTop = $topPhrase -and $normBody.IndexOf($topPhrase, [System.StringComparison]::OrdinalIgnoreCase) -ge 0
    $matchInner = $innerPhrase -and $normBody.IndexOf($innerPhrase, [System.StringComparison]::OrdinalIgnoreCase) -ge 0

.github/skills/find-release-tag-for-script-version/Find-ReleaseTagForScriptVersion.ps1:200

  • The preflight only checks single-letter drive prefixes before this call. A path such as Foo:\..., HKCU:\..., or a custom provider path still reaches GetUnresolvedProviderPathFromPSPath, which invokes that provider before the later FileSystem check and defeats the documented local-only boundary. Reject longer provider-qualified prefixes lexically before calling Resolve-ProviderPath.
    $resolved = Resolve-ProviderPath -Path $Path

.github/skills/find-release-tag-for-script-version/Find-ReleaseTagForScriptVersion.ps1:333

  • This paragraph says the generated folder is revalidated after CreateDirectory and that the later block catches a leaf reparse point, but the implementation explicitly does no post-create revalidation at lines 369-373. That contradiction overstates the default-path locality guarantee; document the parent-only validation and the intentional same-user-race boundary instead.
    # front, then re-validate the FULL path AFTER `CreateDirectory`
    # succeeds — the existing `Test-Path -PathType Container` +
    # `Test-PathHasReparsePoint` block below catches a reparse-point
    # installed at the leaf between generation and use.

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:683

  • The gh pr view --json result has the same multiline native-output shape as the API response. Passing $prJson directly to ConvertFrom-Json can therefore make the optional PR lookup fail for valid JSON and return no provenance details; parse the joined text instead.
                    $prObj = $prJson | ConvertFrom-Json -ErrorAction Stop

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:491

  • This branch reports Status = 'Ok' with an empty StatusDetail even though the diff extraction failed and DiffSummary, regression evidence, and PR attribution are unavailable. Callers treat any non-Ok status as lookup-unavailable, so this currently presents an incomplete trace as successful; return Error (or a documented partial status) and preserve the failure detail.
            Status               = 'Ok'
            StatusDetail         = ''

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:353

  • When git log -L succeeds but returns no history, this result also uses Status = 'Ok' and an empty detail while IntroducingCommit is $null. The documented caller contract uses non-Ok statuses to render an unavailable lookup, so a missing range should be reported as RangeUnavailable with the existing explanation.
            RegressionAssessment = [PSCustomObject]@{ Verdict = 'Indeterminate'; Reasoning = 'git log -L returned no history for the range.' }
            Provenance           = $null
            Status               = 'Ok'
            StatusDetail         = ''
  • Files reviewed: 10/12 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread .github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1 Outdated

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

Unresolved moderate issues affect snapshot-bounded reading, cache validation, phase widening, and multiline JSON parsing.

Review details

Suppressed comments (2)

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

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:655

  • gh api emits the JSON array as multiple native-output lines, so piping $apiOut directly into ConvertFrom-Json makes PowerShell bind and parse each line separately; the optional PR lookup therefore fails on normal multi-line responses and leaves PullRequest null. Join the native output into one JSON string before parsing, as the release-tag helper does.

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:683

  • The gh pr view --json result is also native-command output and can be multiline. Piping $prJson directly to ConvertFrom-Json causes the PR metadata to be discarded by the catch block instead of populating PullRequest; join the lines before parsing.
                    $prObj = $prJson | ConvertFrom-Json -ErrorAction Stop
  • Files reviewed: 10/12 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Consolidated fixes from 4 review iterations addressing findings raised
by Copilot's automated PR reviewer:

1. Fix producer-culture timestamp parsing in analyze-debug-files
   Get-DebugFileMetadata.ps1: extract a shared \\\
   variable used by 6 regexes; extend \\\
   with culture variants (en-GB, de-DE, ja-JP, ISO) so debug files written
   under non-invariant PowerShell cultures parse correctly.

2. Guard gh search from leading-hyphen queries via -- end-of-options
   Find-RelatedGitHubIssues.ps1: reorder \gh search issues\ invocation to
   place flags BEFORE an explicit \--\ end-of-options marker so a query
   value beginning with \--\ cannot silently be reparsed as an option
   (which would degrade to a full-repo search, producing a silent
   partial-coverage regression).

3. Gate CompletionSignals on successful parse; use accepted snapshot for SizeBytes
   Get-DebugFileMetadata.ps1: add \IsTimestamped\ flag to
   \\\ entries; skip timestamped signals when
   \\ -eq \\ so forged out-of-range values in bracketed shapes
   (e.g. \[99/99/2026 25:99:99]\) cannot fake \NoErrorsMessage\. Also
   pass \-SizeBytes ([int64]\.Value)\ in the
   successful-return path and all three catch branches so
   \Get-EmptyFileResult\ does not fall back to reading \\.Length\
   on a possibly-redirected file handle.

4. Reject drive-relative paths in Test-IsLexicallyLocalPath
   Get-DebugFileMetadata.ps1: reject Windows drive-relative forms like
   \C:relative\ (drive letter + colon NOT followed by \\\\ or \/\).
   These resolve against a per-PSDrive current directory that can differ
   from .NET's \Directory.GetCurrentDirectory()\, creating a
   validation-vs-use split-brain between the reparse walk (.NET) and
   the downstream Test-Path (PowerShell provider). Also tightened
   \^([A-Za-z]):[\\\\/]?\ -> \^([A-Za-z]):[\\\\/]\ in the two
   downstream drive-letter extractors for defense in depth.

All fixes were validated with:
- \.build/Invoke-CodeFormatterOnFiles.ps1\ (clean)
- \.build/SpellCheck.ps1\ (0 issues)
- AST parse (0 errors)
- 4-model code review (scoped + scope-blind, Claude + GPT)
- Rubber-duck review (no blocking issues)
- Empirical verification for the security-relevant regex changes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New Pester test at Diagnostics/HealthChecker/Tests/AnalyzeDebugFilesSync.Tests.ps1
verifies that each HealthChecker producer string the skill regex-matches on is
still emitted verbatim by the expected function, and that each consumer regex
still matches the resulting log-line form. Guards against silent drift when
producer wording is edited (typo fix, grammar cleanup, reformat) without also
updating the skill's regexes.

Coverage (15 tests across 4 contexts):

- Get-ErrorsThatOccurred.ps1 producers -> skill regexes:
  handled banner, unhandled banner, remote-unhandled banner, dashed footer
  (exactly 3), Error Index: marker (exactly 2), No errors message,
  All errors handled message, Writing script debug objects.
- HiddenJobUnhandledErrorFunctions.ps1: Remote Error Information banner —
  with data-flow assertions that the banner is assigned into
  $errorInformation AND that $errorInformation is passed to Write-Verbose.
- Shared/ErrorMonitorFunctions.ps1: Calling: Invoke-CatchActions, Error
  Excluded Count:, Error Count: — all scoped to Invoke-CatchActions.
- CompletionSignals wiring: header signals reference the named regex and
  carry IsTimestamped = $false.

Design notes:

- AST-based extraction: producer literals are extracted by walking each
  file's AST and finding Write-Verbose calls (or, for concatenation-based
  emissions, string literals) scoped to their enclosing function via
  Get-EnclosingFunctionName. Copies of the same marker in other functions
  do not satisfy the assertion, so removing the marker from the expected
  emitter fails loudly.
- Exact-equality assertion (-ceq) on the extracted value catches prefix/
  suffix drift; a producer change like `"prefix -----Errors...----- suffix"`
  fails the count check rather than silently satisfying a Contains() test.
- Consumer regex extractor also walks the skill AST: named regexes,
  CompletionSignals (with variable-reference pattern resolution AND
  IsTimestamped extraction), and BodyEvidenceMarkerRegexes.
- Simulator (Get-LoggedLine) is faithful to the DOCUMENTED logger contract
  (`[<ts>] : <message>` or first non-empty line for multi-line-with-leading-
  newlines). It does not invoke Write-LoggerInstance directly — logger↔skill
  sync is a separate coupling and out of scope for this test.

Follow-up (out of scope for this commit): Shared/Write-ErrorInformation.ps1
emits Position Message:/Script Stack:/Inner Exception:/Error Information
headers that the skill's CriticalFrameRegex and InnerException body-evidence
regex depend on. Add producer/consumer sync coverage for that file when
convenient.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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

Unresolved moderate findings remain in debug metadata parsing and source-tracing behavior.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (4)

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:683

  • gh pr view can also emit formatted JSON over multiple native-command output lines, but $prJson is piped as an array and is parsed line-by-line. This makes the optional PR metadata lookup fail for normal multi-line output and leaves PullRequest null; join the lines before calling ConvertFrom-Json.
                    $prObj = $prJson | ConvertFrom-Json -ErrorAction Stop

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:144

  • This parameter is marked mandatory even though the script help and trace-code-introduction/SKILL.md document -Repository as optional and promise that omitting it skips PR lookup. Direct callers following that contract fail during parameter binding before receiving the structured result; make the parameter optional.
    [ValidatePattern('\A[A-Za-z0-9](?:[A-Za-z0-9._\-]*[A-Za-z0-9])?/[A-Za-z0-9](?:[A-Za-z0-9._\-]*[A-Za-z0-9])?\z')]

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:353

  • When git log -L returns no SHA, this result has no introducing commit but advertises Status = 'Ok'. The documented status contract says a range with no history is unavailable, and analyze-debug-files uses non-Ok status to surface lookup failures; returning Ok can therefore render an empty trace as successful. Return RangeUnavailable and a status detail here.
            Status               = 'Ok'
            StatusDetail         = ''

Diagnostics/HealthChecker/Tests/AnalyzeDebugFilesSync.Tests.ps1:569

  • This sync guard extracts all seven BodyEvidence marker definitions but only exercises InvokeCatchActions, ErrorExcludedCount, and ErrorCount. Removing or changing TryingTo, FailedTo, InnerException, or CompletedNarrative would still leave the test suite green, even though Step 7 explicitly relies on those marker kinds. Add assertions for the remaining kinds (or assert the complete expected Kind set and a representative match for each).
    Context "Shared/ErrorMonitorFunctions.ps1 -> skill HandledMarkerRegex + BodyEvidenceMarkerRegexes" {

        It "Invoke-CatchActions emits 'Calling: `$(`$MyInvocation.MyCommand)' and both HandledMarkerRegex and body-evidence InvokeCatchActions regex match the resolved line" {
            $expected = 'Calling: $($MyInvocation.MyCommand)'
            $records = @($Script:monitorRecords | Where-Object { $_.Function -eq 'Invoke-CatchActions' -and $_.Value -ceq $expected })
            $records.Count | Should -Be 1 -Because "Invoke-CatchActions must still emit exactly one '$expected' Write-Verbose; a copy in another function does not satisfy this check because \$MyInvocation resolves to that other function's name"
            $logged = Get-LoggedLine -RawWriteVerbose 'Calling: Invoke-CatchActions'
            $Script:consumer.Named['HandledMarkerRegex'].IsMatch($logged) | Should -BeTrue -Because "HandledMarkerRegex specifically anchors on 'Calling:\\s*Invoke-CatchActions'"
            $Script:consumer.BodyEvidence['InvokeCatchActions'].IsMatch($logged) | Should -BeTrue -Because "body-evidence InvokeCatchActions regex must match the same line"
        }

        It "Invoke-CatchActions emits 'Error Excluded Count:' and both HandledMarkerRegex and body-evidence ErrorExcludedCount regex match" {
            $expected = 'Error Excluded Count: $($Script:ErrorsExcluded.Count)'
            $records = @($Script:monitorRecords | Where-Object { $_.Function -eq 'Invoke-CatchActions' -and $_.Value -ceq $expected })
            $records.Count | Should -Be 1 -Because "Invoke-CatchActions must still emit exactly one '$expected' Write-Verbose"
            $logged = Get-LoggedLine -RawWriteVerbose 'Error Excluded Count: 0'
            $Script:consumer.Named['HandledMarkerRegex'].IsMatch($logged) | Should -BeTrue
            $Script:consumer.BodyEvidence['ErrorExcludedCount'].IsMatch($logged) | Should -BeTrue
        }

        It "Invoke-CatchActions emits 'Error Count:' and the body-evidence ErrorCount regex matches" {
            $expected = 'Error Count: $($Error.Count)'
            $records = @($Script:monitorRecords | Where-Object { $_.Function -eq 'Invoke-CatchActions' -and $_.Value -ceq $expected })
            $records.Count | Should -Be 1 -Because "Invoke-CatchActions must still emit exactly one '$expected' Write-Verbose"
            $logged = Get-LoggedLine -RawWriteVerbose 'Error Count: 0'
            $Script:consumer.BodyEvidence['ErrorCount'].IsMatch($logged) | Should -BeTrue
        }
  • Files reviewed: 11/13 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread .github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1 Outdated
Comment thread .github/skills/analyze-debug-files/SKILL.md Outdated
Addressed:
- Trace-CodeIntroduction.ps1: change Status='Ok' to 'RangeUnavailable' when
  `git log -L` succeeds but returns no SHA — matches the documented
  .Status contract; a null IntroducingCommit was being mislabeled as a
  successful lookup, which would skip the analyze-debug-files runner's
  unavailable-lookup fallback.
- Trace-CodeIntroduction.ps1: join `gh api` multi-line native-command
  output into a single JSON string before ConvertFrom-Json — Windows
  PowerShell 5.1 pipes each line separately, causing pretty-printed
  responses to fail parse and be swallowed by the optional-lookup catch.
- Trace-CodeIntroduction.ps1: same join-before-parse for `gh pr view --json`.
- SKILL.md (analyze-debug-files) and Get-DebugFileMetadata.ps1 comment:
  correctly list the remote-emitter field labels — the remote producer
  writes `Exception Inner Exception:` (not `Inner Exception:`) and
  also emits `Error Details Script Stack Trace:`. Documented that
  future emitter revisions should be quoted verbatim rather than
  omitting an unrecognized label.
- AnalyzeDebugFilesSync.Tests.ps1: add a new Context that freezes the
  complete seven-Kind expected set for BodyEvidenceMarkerRegexes and
  adds representative-match tests for the four generic kinds
  (TryingTo, FailedTo, InnerException, CompletedNarrative). Also
  covers the remote `Exception Inner Exception:` form and all three
  CompletedNarrative verbs (Completed/Finished/Starting) so an
  alternation edit fails loudly.

Not addressed (dismissed):
- Trace-CodeIntroduction.ps1:144 (suppressed comment) claimed the
  `-Repository` parameter is Mandatory. It is not: line 144 is only
  the `[ValidatePattern]` attribute for the parameter, and no
  `[Parameter(Mandatory=$true)]` is applied — the comment above
  even says `Optional here; when omitted, PR lookup is skipped`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dpaulson45

Copy link
Copy Markdown
Member Author

Follow-up addressing the 4 suppressed comments from Copilot's review of 0b8869b:

  • Trace-CodeIntroduction.ps1:683 (gh pr view multi-line JSON) — Fixed in ce9d01c. Same join-before-parse fix applied at both the gh api and gh pr view call sites; see the reply on the inline comment for line 655.
  • Trace-CodeIntroduction.ps1:144 (-Repository marked mandatory) — Not applicable. Line 144 is only the [ValidatePattern] attribute; no [Parameter(Mandatory = $true)] is applied to -Repository. The comment above the parameter even documents Optional here; when omitted, PR lookup is skipped. Verified callers work when -Repository is omitted.
  • Trace-CodeIntroduction.ps1:353 (range with no history returning Ok) — Fixed in ce9d01c. When git log -L succeeds but returns no SHA matches, Status is now RangeUnavailable with an explanatory StatusDetail, matching the .Status contract documented in the file header. RegressionAssessment.Verdict = 'Indeterminate' is preserved.
  • AnalyzeDebugFilesSync.Tests.ps1:569 (only 3 of 7 marker kinds exercised) — Fixed in ce9d01c. Added a new Context "Consumer wiring: BodyEvidenceMarkerRegexes complete Kind set is present" that freezes the complete seven-Kind expected set and adds representative-match tests for all four generic kinds (TryingTo, FailedTo, InnerException, CompletedNarrative). The InnerException test also covers the remote Exception Inner Exception: label form, and the CompletedNarrative test covers all three verbs (Completed, Finished, Starting) so an alternation edit fails loudly.

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

Unresolved correctness, error-handling, and portability findings must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (6)

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

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:483

  • $topPhrase and $innerPhrase are the truncated 40–80 character search prefixes, not the full normalized exception phrases promised by the Classification section of find-related-github-issues/SKILL.md. A broad query can therefore return an issue containing only a common prefix and this code will incorrectly classify it as Similar or Duplicate even when the rest of the exception differs. Keep these short values for GitHub queries, but use the full placeholder-reduced $searchTop/$searchInner values for local classification.
    .github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:499
  • This branch returns Status = 'Ok' even though DiffSummary is null and the regression assessment is explicitly Indeterminate. The analyze-debug-files caller treats Ok as a successful lookup and only renders an unavailable note for non-Ok statuses, so a failed diff can be reported as a normal provenance result. Return a non-Ok status with the Phase 3 failure in StatusDetail so callers take the documented fallback path.

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:151

  • The normalizer claims to collapse timestamps, but this pattern only accepts two-digit slash-separated dates, 24-hour time, and no AM/PM. Write-LoggerInstance uses DateTime.Now.ToString(), so real lines such as [9/12/2026 2:41:31 PM] and non-US culture formats remain in the phrase, causing equivalent captures to search and classify differently. Accept the producer's culture/width variants (or otherwise strip timestamps using the same parser contract).
    # Strip timestamps [MM/dd/yyyy HH:mm:ss.fffffff].
    $s = [regex]::Replace($s, '\[\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}(?:\.\d+)?\]', '<ts>')

.github/skills/find-release-tag-for-script-version/Find-ReleaseTagForScriptVersion.ps1:63

  • Unlike the other new helpers, this script does not disable $PSNativeCommandUseErrorActionPreference. If a caller has enabled native error promotion, a nonzero gh release list or per-candidate gh release download throws before the $LASTEXITCODE branches run; downloads that should be recorded as download-failed instead abort the structured lookup. Disable and restore that preference before invoking gh.
)

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:525

  • Because git stderr is merged into $diffOut with 2>&1, a warning can produce an ErrorRecord element here. ErrorRecord does not implement StartsWith, so the diff parser can throw before producing the structured result, despite Phase 3 being intended to return an Indeterminate assessment on failure. Convert each element to its text representation before calling StartsWith/Substring.
        if ($ln.StartsWith('+')) {
            $addedLines += $ln.Substring(1)
        } elseif ($ln.StartsWith('-')) {
            $removedLines += $ln.Substring(1)

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:520

  • Matching raw line shape is not sufficient to identify unified-diff headers: an added source line beginning with ++ is rendered as +++ , exactly like the +++ b/path header (and the analogous -- case becomes --- ). Those legitimate lines are skipped here, which corrupts AddedLines/RemovedLines and keyword-based regression classification. Track hunk/header context or otherwise distinguish file headers from hunk content.
    foreach ($ln in $diffOut) {
        if ($ln -match '^\+\+\+ ' -or $ln -match '^--- ') {
            continue
  • Files reviewed: 11/13 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread .github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1
Fixes three pre-existing script blockers surfaced by the second Copilot
review on PR #2584. All three predate the sync-guard commit but are
within scope of the wider AI-skill work.

trace-code-introduction/Trace-CodeIntroduction.ps1 — Phase 3 diff parsing:

- $diffOut can carry ErrorRecord objects when git wrote to stderr
  (we redirect 2>&1). ErrorRecord has no .StartsWith(...) method, so
  the previous loop threw a runtime exception on any git warning and
  aborted the trace mid-parse. Each element is now cast to string
  before regex or String method calls.
- The diff file-header regex is anchored on git's actual header
  form (`+++ b/`, `--- a/`, or `.../dev/null`). The old broad
  `+++ ` / `--- ` pattern silently dropped added / removed source
  lines whose own content began with `++ ` or `-- ` (rendered
  `+++ ` / `--- ` in the unified diff after the leading marker),
  which could omit a guard keyword and produce an incorrect
  regression verdict.
- The narrower header regex would itself regress when a user or
  repo enabled `diff.noprefix=true`, because git then emits real
  headers as `--- path` / `+++ path` with no `a/` or `b/` prefix.
  The `git log` invocation now passes `-c diff.noprefix=false` at
  git-level to force prefix emission regardless of caller config.
  (Rubber-duck-verified: reproduced both the mis-parse and the
  fix by toggling the config across `git log -L`.)

find-release-tag-for-script-version/Find-ReleaseTagForScriptVersion.ps1:

- Added `$PSNativeCommandUseErrorActionPreference = $false` after
  the param block. Without it, callers running PowerShell 7.4+
  with the preference enabled would see NativeCommandExitException
  on any nonzero `gh` exit — bypassing the script's own
  `$LASTEXITCODE` handling and breaking the structured
  `Status = 'GhUnavailable' / 'NotFound' / 'Error'` result
  contract. Matches the pattern already used in the sibling skills
  in this PR.

Dictionary:

- Add `noprefix` to `.build/cspell-words.txt` (git config key
  used in the fix above).

Findings dismissed (not addressed in this commit):

- F-008 (backslash separators in dot-source): apparent false
  positive; no `IsWindows` branch exists in the file and the skill
  is Windows-only by design.
- F-009 (Status='Ok' + Verdict='Indeterminate' on Phase 3 failure):
  deliberate — the trace itself succeeded (IntroducingCommit is
  populated), only classification is indeterminate. Documented in
  the code comment above the return.
- F-012 (2-digit slash timestamp normalizer): heuristic tuned for
  the CSS-Exchange logger format.
- F-013 (truncated phrase used for classification): heuristic
  tradeoff between recall and precision on GitHub-search results.

Verification:
- Formatter: clean on all 3 modified files
- SpellCheck: 0/814 issues
- PSScriptAnalyzer: clean
- Pester (sync-guard): 23/23 pass
- Rubber-duck: clean after diff.noprefix follow-up

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread .build/cspell-words.txt Outdated

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

Unresolved critical and moderate findings affect log completion detection, report fidelity, portability, timestamp matching, and provenance status.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:151

  • This normalizer only removes [MM/dd/yyyy HH:mm:ss(.fraction)] with exactly two-digit components and no AM/PM. Write-LoggerInstance uses DateTime.Now.ToString(), so a normal en-US line such as [9/12/2026 6:11:22 PM], or a de-DE/year-first timestamp accepted by the analyzer, remains in the searchable and classification phrase. Because the phrase is then quoted and compared literally, equivalent reports from another culture fail to match; use the analyzer's full accepted timestamp shape here.
    $s = [regex]::Replace($s, '\[\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}(?:\.\d+)?\]', '<ts>')
  • Files reviewed: 11/13 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread .github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1
Comment thread .github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1 Outdated

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

Critical and moderate issues remain in debug-log parsing and filesystem-safety handling.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:229

  • Get-SafeQueryPhrase removes : before Invoke-GhSearch adds the trusted outer quotes. A normal exception such as System.InvalidOperationException: message is therefore submitted as the exact phrase System.InvalidOperationException message, which does not match the colon-bearing text normally stored in issues, so direct duplicate lookups can return no match. Preserve punctuation for exact-phrase queries (while continuing to neutralize syntax for unquoted identifier queries), or otherwise add a fallback that searches the original exception phrase safely.
    $p = [regex]::Replace($p, '[\"`\r\n\t]', ' ')
    # Strip characters that are search-syntax metacharacters (parens,
    # brackets, colons, angle brackets) — replace with space so token
    # structure is preserved but no operator escapes.
    $p = [regex]::Replace($p, '[\(\)\[\]\{\}\<\>:]', ' ')

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:550

  • Matching any +++ b/... or --- a/... line as a header is not sufficient: an added source line whose content starts with ++ b/... is rendered by unified diff as the identical +++ b/... text (and likewise for removed -- a/...). This loop will drop that real source line from AddedLines/RemovedLines and from keyword extraction, potentially changing the regression verdict. Track whether the parser is in the file-header portion of each diff or use hunk-aware parsing rather than a content-only header regex.
    foreach ($ln in $diffOut) {
        $s = if ($null -eq $ln) { '' } else { [string]$ln }
        if ($s -match '^\+\+\+ (b/|/dev/null)' -or $s -match '^--- (a/|/dev/null)') {
            continue
  • Files reviewed: 11/13 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread .github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1

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

Unresolved moderate findings affect parsing limits, path safety, validation, search coverage, and report correctness.

Review details

Suppressed comments (7)

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

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:151

  • This pattern does not match the logger's actual DateTime.Now.ToString() output: the default en-US form is typically [9/12/2026 6:11:22 PM] (no zero padding or fractional seconds), and other machine cultures use different separators/order. Consequently, volatile timestamps embedded in exception text are not normalized and can make otherwise identical failures produce different GitHub search phrases. Broaden this to the culture-dependent timestamp shapes used by the log parser (or share the parser's normalization routine).

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:302

  • gh search issues excludes pull requests unless --include-prs is supplied. Because this skill promises to search both issues and pull requests, the current command silently misses matching prior reports stored in PRs; add the flag to the search invocation.
    $out = & gh search issues --repo $qualifiedRepo --limit $Limit --json 'number,state,title,url,body' -- $qArg 2>&1

.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:145

  • Both path normalizers stop at whitespace, so a valid path such as C:\Program Files\Module\x.dll becomes <path> Files\Module\x.dll (and the same issue exists for UNC paths). The leftover path text changes the exact search phrase and can prevent matching an issue that contains the same exception. Normalize a complete path token, or otherwise establish an explicit path boundary before replacing it.
    # Strip Windows absolute paths (already redacted by caller, but path
    # tail can still be volatile per-machine).
    $s = [regex]::Replace($s, '[A-Za-z]:\\[^\s"'']+', '<path>')
    # Strip UNC paths.
    $s = [regex]::Replace($s, '\\\\[^\s"'']+', '<unc>')

.github/skills/find-release-tag-for-script-version/Find-ReleaseTagForScriptVersion.ps1:493

  • Release tags may legally contain /, but interpolating $tag into the local output filename makes Join-Path target a nonexistent nested path such as <WorkFolder>\release\...csv; gh release download -O then fails before the matching asset can be inspected. Use a GUID-only filename (or otherwise sanitize path separators) for the temporary CSV.
        $csvPath = Join-Path $WorkFolder "$tag-$([guid]::NewGuid().ToString('N')).ScriptVersions.csv"

.github/skills/find-release-tag-for-script-version/Find-ReleaseTagForScriptVersion.ps1:310

  • The tag validator allows /, but that value is interpolated into the REST path unescaped. For a valid slash-containing ref such as release/2026, gh api can treat the ref as extra URL path segments and fail to resolve the commit, leaving ConfirmedCommitSha null; URL-encode the tag path segment before the lookup.
        $endpoint = "repos/$Repository/commits/$Tag"

.github/skills/find-release-tag-for-script-version/Find-ReleaseTagForScriptVersion.ps1:497

  • A valid Git ref/release tag can begin with -, and this helper's tag validation permits it. Passing $tag before an option terminator lets gh release download parse such a tag as a flag instead of the positional tag, so the matching release is skipped; pass the tag after -- (with all flags first).
            $ghOutput = gh release download $tag --repo $qualifiedRepository -p "ScriptVersions.csv" -O $csvPath 2>&1

.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:550

  • A legitimate added source line whose text begins with ++ b/ is rendered by unified diff as +++ b/..., so it still matches this header predicate and is dropped from AddedLines. That can remove guard keywords and change the regression verdict. Track the file-header position/state (the ---/+++ pair before the hunk) instead of treating every matching line as a header.
        if ($s -match '^\+\+\+ (b/|/dev/null)' -or $s -match '^--- (a/|/dev/null)') {
            continue
  • Files reviewed: 11/13 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Consolidated fixes for two Copilot review passes plus one [CLI]
follow-up on the AI-skills PR. Squashed from commits 112d0f0
and b2bf163.

Files changed:
- .build/cspell-words.txt
- .github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1
- .github/skills/find-release-tag-for-script-version/Find-ReleaseTagForScriptVersion.ps1
- .github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1

Addressed:
- F-001 ([CLI], .build/cspell-words.txt:121): removed lowercase
  `noprefix` and updated Trace-CodeIntroduction.ps1 to use the
  PascalCased `diff.noPrefix=false` per the repo convention. Git
  config keys are case-insensitive so the runtime behavior is
  unchanged.
- F-002 (Copilot, Get-DebugFileMetadata.ps1:1677): the completion-
  signal loop used a first-seen `ContainsKey` early-continue, so
  tampered or appended logs could keep a stale completion event and
  mask later state. Rewrote to overwrite (LAST-seen) with an Iter-25
  rationale comment; updated the sibling variable comment at L1141.
- F-003 (Copilot, Trace-CodeIntroduction.ps1:506): Phase-3 diff-
  failure return path was building a result with `Status='Ok'`,
  masking failure downstream. Set `Status='Error'` and populated
  `StatusDetail` from `RegressionAssessment.Reasoning` while
  keeping `IntroducingCommit` populated for the trace consumer.
- F-005 (Copilot, Find-ReleaseTagForScriptVersion.ps1:353): the
  intake comment promised post-create reparse-point revalidation,
  but the post-create block only ran a container check and stated
  same-user races were out of scope. Added a
  `Test-PathHasReparsePoint` check after `CreateDirectory`; on
  positive detection the code flips ` = $false`
  (so the `finally` block skips the recursive delete that would
  otherwise follow the reparse target) and throws with a message
  directing the caller to inspect the redirection.

Not addressed:
- F-004 (Copilot, Get-DebugFileMetadata.ps1:1323): dismissed as a
  false positive. The state-none banner branch at ~L1305 intentionally
  does not create an event — the first (and every) remote record is
  created by the dedicated `RemoteErrorInformationHeaderRegex` branch
  at ~L1425-1481, which fires on the `----------------Remote Error
  Information----------------` line and increments
  ``. A single-remote-failure log correctly
  yields `UnhandledCount = 1`. Replied on the thread with the trace.

Validated with: Invoke-CodeFormatterOnFiles, SpellCheck.ps1,
PSScriptAnalyzer, AnalyzeDebugFilesSync.Tests.ps1 (23/23 passing),
and two clean Copilot code review passes.

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.

3 participants