Add AI skills: analyze-debug-files, find-related-github-issues, trace-code-introduction - #2584
Add AI skills: analyze-debug-files, find-related-github-issues, trace-code-introduction#2584David Paulson (dpaulson45) wants to merge 13 commits into
Conversation
|
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>
3188dce to
dd9382a
Compare
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
🟡 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
ConfirmedCommitShaand 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 0x20as 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-ErrorsThatOccurredalso emits----Errors that occurred that was not handled remotely----after the normal unhandled footer (seeDiagnostics/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 markedSummaryCompletewithUnhandledCount=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]::Nowwith the machine's culture (Shared/LoggerFunctions.ps1:60-63), so the common en-US output includes anAM/PMsuffix. This regex and theAcceptedTimestampFormatsbelow reject that form, leaving$tsnull 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, butGet-ErrorsThatOccurred.ps1emits both summary headers withWrite-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.ps1can 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 remainsBuildOnly. 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-issuesdefinesPartialLookupas usable retained results with incomplete coverage, not as an unavailable lookup. Treating every non-Okstatus as unavailable would hide those matches from the report and contradict the sibling skill's contract; render retained results plus an incompleteness note forPartialLookup, 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$trueandBuild.ps1exits 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 infinally, 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
Mandatorydoes not reject whitespace-only input. In that case normalization produces no query, the final status falls through toOkwith 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 treatpathas a wildcard, so an issue sayingFailure at C:\other\dis 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 listresponse ([]),ConvertFrom-Jsoncan produce$null(especially on Windows PowerShell 5.1).@($parsedReleases)then creates an array containing one null element, so the loop below throwsUnexpected release entry shape from ghinstead of reaching thenot-found-no-candidatesresult. Preserve$nullas 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.
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
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>
There was a problem hiding this comment.
🟡 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.jsonalone, then the XML files are imported outside that validationtry. 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) andLoggerFunctions.ps1emits the current culture'sDateTime.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 optionalAM|PMsuffix 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-IsSafeLocalFileduring 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-LineTimestampreturns$nullfor semantically invalid values such as month/day/hour 99. The branch then creates aSummaryEventwith a nullTimestamp, and Step 7 dereferencesSummaryEvent.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
$summaryStatehere does not actually parse any remote error:WriteRemoteErrorInformationemits aRemote Error Informationblock withException Message:fields, not anError Index:header. The event-creation branch below is gated by$ErrorIndexRegex, so remote failures are skipped andUnhandledCount/SummaryEventsomit them; incrementing$unhandledHeaderCountalso makes every valid remote section setMultipleSummaryBlocksDetected, 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 atdocs/Hybrid/ConfigureExchangeHybridApplication.md:16). When that filename matches,$Matches['name']already ends in.ps1, so this appends a second extension and returnsConfigureExchangeHybridApplication.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
$worktreeRootis intentionally$nulland Steps 6-8 read source withgit 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 andgit showpaths 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 userbecomesFailed 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),
$queriesremains empty and this branch falls through toStatus = 'Ok'. That makes the caller report “no related issue” even though no GitHub search was performed; returnErrorwith 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, asGet-DebugFileMetadata.ps1already 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 emptyStatusDetail. The caller's contract renders anyOkresult 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 blamecan still produce no parseable porcelain attribution (for example, an empty/out-of-range result or boundary-only output). In that case the finalelseindexes$blameShas[0]under strict mode and throws instead of returning the documentedUnavailableprovenance state.
if (-not $blameOk) {
- Files reviewed: 9/9 changed files
- Comments generated: 5
- Review effort level: Lite
There was a problem hiding this comment.
🟡 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 likePath ' ' 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-SafeQueryPhraseis also used for the non-exact identifier queries, but this filter leaves leading-operators intact. A log-controlled discriminator such as-secretis emitted unquoted byInvoke-GhSearchand changes GitHub's query semantics instead of being searched as data. Neutralize leading hyphens, or quote all untrusted query fragments, before passing them togh.
$p = [regex]::Replace($p, '[\(\)\[\]\{\}\<\>:]', ' ')
.github/skills/find-related-github-issues/Find-RelatedGitHubIssues.ps1:331
- These variables are later used as the
$matchTop/$matchInnercriteria for classifying returned issues, butGet-DistinctivePhrasetruncates 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 withErrorActionPreference = 'Stop', a failedgh release listthrows before$LASTEXITCODEis 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-oralternatives are not word-bounded, so identifiers such asInvoke-eqThingin an added or removed line are counted as conditional operators after strings and comments are stripped. That can incorrectly produceIntentionalorPossibleRegressionverdicts; 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
+foois emitted as++foo, and a removed-fooas--foo. Such lines are skipped along with the+++/---file headers, soAddedLines/RemovedLinesand 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
ParseInputis 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 asifor-eqto affect$keywordsAdded/$keywordsRemovedand 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 emptyStatusDetail. The analyze-debug-files contract treats only non-Okprovenance results as unavailable, so a failed diff is rendered as a successful lookup with anIndeterminateverdict and the failure is hidden. Return an error status and preserve the diff failure inStatusDetail.
Provenance = $null
Status = 'Ok'
StatusDetail = ''
- Files reviewed: 9/9 changed files
- Comments generated: 2
- Review effort level: Lite
There was a problem hiding this comment.
🟡 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
$worktreeRootis null. Define the integrity check in terms of the pinned tree viagit 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-EmptyFileResultpreserves filename-derived confidence forEmpty,Oversize, andUnreadableentries, so a directory containing only an oversized or emptyHealthChecker-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; includeStatus -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
TerminationLineTextcontains, 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:LOCALAPPDATAtoJoin-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
$matchTopand$matchInnerbecome true for the same single phrase and this branch reports aDuplicate. 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
$topPhraseand$innerPhraseare the ≤80-character distinctive search prefixes returned byGet-DistinctivePhrase, but these same prefixes drive local classification. A long exception whose first 80 characters match another issue can therefore satisfy bothmatchTopandmatchInnerand 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 -Lfinds no history, this branch returnsStatus = 'Ok'with no introducing commit. The analyze-debug-files caller treatsOkas a successful provenance lookup, so it can render an empty/indeterminate result instead of the documented "code-introduction lookup unavailable" outcome. Return a non-Okstatus such asRangeUnavailableand populateStatusDetailhere.
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++$counteror removed--$counterline is therefore omitted fromAddedLines/RemovedLinesand from the guard-token heuristic, which can produce incorrect summaries and verdicts. Skip only+++ a|b/path/--- a|b/pathheader 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 apinormally emits formatted, multi-line JSON, so native command output is an array of lines in PowerShell. Piping that array intoConvertFrom-Jsonparses each line separately; this optional lookup will usually fall into the catch and lose the PR number. Join$apiOutwith 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 --jsonresponse has the same multi-line native-output issue: piping$prJsondirectly toConvertFrom-Jsoncan 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
There was a problem hiding this comment.
🟡 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 apinormally returns pretty-printed JSON as multiple native-command output lines. Piping$apiOutdirectly sends those lines separately toConvertFrom-Json, so a valid PR list falls into the catch block andPullRequestis 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 modulebecomes"Cannot open in module", which cannot match an issue containing the concrete path betweenopenandin 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
$topPhraseand$innerPhraseare the shortened 80-character query prefixes fromGet-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 reachesGetUnresolvedProviderPathFromPSPath, which invokes that provider before the later FileSystem check and defeats the documented local-only boundary. Reject longer provider-qualified prefixes lexically before callingResolve-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
CreateDirectoryand 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 --jsonresult has the same multiline native-output shape as the API response. Passing$prJsondirectly toConvertFrom-Jsoncan 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 emptyStatusDetaileven though the diff extraction failed andDiffSummary, regression evidence, and PR attribution are unavailable. Callers treat any non-Okstatus as lookup-unavailable, so this currently presents an incomplete trace as successful; returnError(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 -Lsucceeds but returns no history, this result also usesStatus = 'Ok'and an empty detail whileIntroducingCommitis$null. The documented caller contract uses non-Okstatuses to render an unavailable lookup, so a missing range should be reported asRangeUnavailablewith 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
There was a problem hiding this comment.
🔵 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 apiemits the JSON array as multiple native-output lines, so piping$apiOutdirectly intoConvertFrom-Jsonmakes PowerShell bind and parse each line separately; the optional PR lookup therefore fails on normal multi-line responses and leavesPullRequestnull. 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 --jsonresult is also native-command output and can be multiline. Piping$prJsondirectly toConvertFrom-Jsoncauses the PR metadata to be discarded by the catch block instead of populatingPullRequest; 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>
1149203 to
c87eebe
Compare
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>
There was a problem hiding this comment.
🟡 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 viewcan also emit formatted JSON over multiple native-command output lines, but$prJsonis piped as an array and is parsed line-by-line. This makes the optional PR metadata lookup fail for normal multi-line output and leavesPullRequestnull; join the lines before callingConvertFrom-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.mddocument-Repositoryas 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 -Lreturns no SHA, this result has no introducing commit but advertisesStatus = 'Ok'. The documented status contract says a range with no history is unavailable, andanalyze-debug-filesuses non-Okstatus to surface lookup failures; returningOkcan therefore render an empty trace as successful. ReturnRangeUnavailableand a status detail here.
Status = 'Ok'
StatusDetail = ''
Diagnostics/HealthChecker/Tests/AnalyzeDebugFilesSync.Tests.ps1:569
- This sync guard extracts all seven
BodyEvidencemarker definitions but only exercisesInvokeCatchActions,ErrorExcludedCount, andErrorCount. Removing or changingTryingTo,FailedTo,InnerException, orCompletedNarrativewould 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 expectedKindset 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
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>
|
Follow-up addressing the 4 suppressed comments from Copilot's review of 0b8869b:
|
There was a problem hiding this comment.
🟡 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
$topPhraseand$innerPhraseare the truncated 40–80 character search prefixes, not the full normalized exception phrases promised by the Classification section offind-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/$searchInnervalues for local classification.
.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:499- This branch returns
Status = 'Ok'even thoughDiffSummaryis null and the regression assessment is explicitlyIndeterminate. The analyze-debug-files caller treatsOkas a successful lookup and only renders an unavailable note for non-Okstatuses, so a failed diff can be reported as a normal provenance result. Return a non-Okstatus with the Phase 3 failure inStatusDetailso 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-LoggerInstanceusesDateTime.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 nonzerogh release listor per-candidategh release downloadthrows before the$LASTEXITCODEbranches run; downloads that should be recorded asdownload-failedinstead abort the structured lookup. Disable and restore that preference before invokinggh.
)
.github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1:525
- Because
gitstderr is merged into$diffOutwith2>&1, a warning can produce anErrorRecordelement here.ErrorRecorddoes not implementStartsWith, so the diff parser can throw before producing the structured result, despite Phase 3 being intended to return anIndeterminateassessment on failure. Convert each element to its text representation before callingStartsWith/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/pathheader (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
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>
There was a problem hiding this comment.
🟡 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-LoggerInstanceusesDateTime.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
There was a problem hiding this comment.
🟡 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-SafeQueryPhraseremoves:beforeInvoke-GhSearchadds the trusted outer quotes. A normal exception such asSystem.InvalidOperationException: messageis therefore submitted as the exact phraseSystem.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 fromAddedLines/RemovedLinesand 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
There was a problem hiding this comment.
🔵 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 issuesexcludes pull requests unless--include-prsis 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.dllbecomes<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$taginto the local output filename makesJoin-Pathtarget a nonexistent nested path such as<WorkFolder>\release\...csv;gh release download -Othen 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 asrelease/2026,gh apican treat the ref as extra URL path segments and fail to resolve the commit, leavingConfirmedCommitShanull; 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$tagbefore an option terminator letsgh release downloadparse 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 fromAddedLines. 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>
b2bf163 to
27a2aa3
Compare
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-versionskill.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.ps1output, and writes a root-cause report (DebugAnalysis-<timestamp>.md) into the caller-supplied directory.find-related-github-issues— searchesmicrosoft/CSS-Exchangeissues for prior reports of a given exception. Invocable directly during triage or as a Step 7 sub-invocation ofanalyze-debug-files.trace-code-introduction— resolves a specific source line + range at a pinned SHA to the commit that introduced it (viagit log -Landgit blame). Used byanalyze-debug-filesto attribute unhandled exceptions to their originating change.Extension
find-release-tag-for-script-versionnow emits aConfirmedCommitShafield alongsideConfirmedTag.analyze-debug-filesneeds 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
%LOCALAPPDATA%\CSS-Exchange\dependency-cache\<sha>\. On cache hit,analyze-debug-filesskips the ~107 s.build/Build.ps1invocation (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 withgit show <sha>:<path>regardless of which run produced the cache.# L<n>trailing annotations, no ellipses, consecutive-line check) with a post-render regex assertion so cited source cannot be silently truncated or paraphrased.ghCLI arguments, git pathspecs, and shell-metacharacter surfaces before invoking subprocesses (seeGet-SafeQueryPhrase, allow-list regexes inTrace-CodeIntroduction.ps1).Testing
.build\SpellCheck.ps1— clean (0 issues, 810 files).-ForceLegacyscenario). Cache miss + cache hit both verified; normalized XML keys resolve viagit show <sha>:<path>.Commits
find-release-tag-for-script-versionwithConfirmedCommitSha+ cspell dictionary additions.find-related-github-issuesskill.trace-code-introductionskill.analyze-debug-filesskill.