
Winui Session Report
- 91 installs
- 373 repo stars
- Updated August 3, 2026
- microsoft/win-dev-skills
winui-session-report is an agent skill that runs Analyze-Session.ps1 to produce diagnostic reports for Copilot CLI or Claude Code sessions.
About
The winui-session-report skill generates a diagnostic report for agent coding sessions by running the bundled Analyze-Session.ps1 script. It auto-detects whether the session came from GitHub Copilot CLI or Claude Code using environment variables and on-disk transcript formats, then parses turns, token usage, skill invocations, build attempts, stuck patterns, and tooling issues into session-report.md. Detection prefers explicit COPILOT_AGENT_SESSION_ID or CLAUDE_SESSION_ID over most-recently-modified files so parallel terminals do not shadow the active session. The agent must surface privacy guidance because reports embed unredacted transcripts including file paths, prompts, secrets, and local environment values. Users may request high-level metric summaries instead of sharing the full file. Report sections cover overview, prompt, turn breakdown, skills, subagents for Claude Code, build analysis, stuck patterns, tooling issues, and per-turn detail. After script output the agent adds observations on app quality, missing work, and WinUI tooling improvements. Triggers include session feedback, debugging agent behavior, or reviewing what happened during a build session.
- Auto-detects Copilot CLI or Claude Code session transcripts.
- Analyze-Session.ps1 produces structured session-report.md output.
- Covers turns, tokens, skills, builds, stuck patterns, and tooling issues.
- Mandatory privacy warning for unredacted transcript content.
- Supports explicit session ID, events file, and subagent skip options.
Winui Session Report by the numbers
- 91 all-time installs (skills.sh)
- +9 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #250 of 596 Debugging skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
winui-session-report capabilities & compatibility
- Capabilities
- harness auto detection for copilot cli and claud · analyze session.ps1 report generation · turn, token, and skill usage breakdown · build failure and stuck pattern detection · privacy guidance for unredacted transcript shari
- Use cases
- debugging · planning
npx skills add https://github.com/microsoft/win-dev-skills --skill winui-session-reportAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 91 |
|---|---|
| repo stars | ★ 373 |
| Last updated | August 3, 2026 |
| Repository | microsoft/win-dev-skills ↗ |
What happened during my agent session and where did builds or tooling get stuck?
Analyze a GitHub Copilot CLI or Claude Code agent session and generate a diagnostic session-report.md with turns, skills, builds, and stuck patterns.
Who is it for?
WinUI developers reviewing Copilot CLI or Claude Code session quality and tooling gaps.
Skip if: Skip for non-agent sessions, live debugging without transcripts, or non-Windows harnesses.
When should I use this skill?
User asks for session feedback, a session report, or review of recent agent build behavior.
What you get
A session-report.md with turn metrics, skill usage, build analysis, stuck patterns, and agent observations.
Files
Session Analysis Report
Generate a diagnostic report for an agent session by running the Analyze-Session.ps1 script included with this skill. The script auto-detects whether the current session was produced by GitHub Copilot CLI or Claude Code from environment variables and on-disk file format, and dispatches to the appropriate parser. If neither harness can be detected, the script exits with a clear error.
Privacy and sensitivity — surface this guidance to the user
Analyze-Session.ps1 always:
1. Embeds a "Privacy and sensitivity" section at the top of the generated `session-report.md` (right above the Overview table), and 2. Prints a yellow PRIVACY NOTICE banner to the console when it finishes writing the file.
You (the agent) must surface this guidance to the user in your response — do not let it stay buried in script output the user might not have read. When you finish running the script and reporting the findings, include a short privacy reminder in your reply to the user, in plain second-person language. Use this template, adapting wording as needed:
⚠️ Heads-up before you share `session-report.md` — this file contains your unredacted session transcript: file contents and paths the agent read or edited, your prompts verbatim (including any secrets you may have pasted), tool output, environment values, and local paths under C:\Users\<you>\…. You're responsible for what you share — please open the file in your editor and read it end-to-end before attaching it to a public issue, posting it in chat, or sending it outside your organization. Redact anything sensitive. If you only need to share the high-level metrics, ask me to summarize the file instead of attaching it.If the user only wants the high-level metrics (turn counts, skill usage, build success rate) without the per-turn detail, summarize the report and share the summary instead of the file — and tell the user that's what you're doing so they don't have to read it themselves to confirm.
Steps
1. Run the analysis script to generate the report:
# Analyze the most recent session (auto-detects harness) and save report
.\Analyze-Session.ps1 -OutputFile session-report.md
# Or analyze a specific session by ID (searched in both harness locations)
.\Analyze-Session.ps1 -SessionId "<session-id>" -OutputFile session-report.md
# Or analyze a transcript file directly (format sniffed from content)
.\Analyze-Session.ps1 -EventsFile <path-to-transcript.jsonl> -OutputFile session-report.md
# Force a specific format if auto-detection picks the wrong harness
.\Analyze-Session.ps1 -Format ClaudeCode -OutputFile session-report.md
# Skip subagent transcripts (Claude Code only) for a parent-only view
.\Analyze-Session.ps1 -SkipSubagents -OutputFile session-report.mdDetection rules:
- The current session is preferred when an explicit ID is available:
COPILOT_AGENT_SESSION_ID(Copilot CLI) orCLAUDE_SESSION_ID(Claude Code) take priority over "most recently modified" so a parallel session in another terminal can't shadow the one the skill was invoked from. - Environment first:
CLAUDECODE=1orCLAUDE_CODE_ENTRYPOINT-> Claude Code;COPILOT_*env vars -> Copilot. - For Claude Code, the most-recent JSONL whose
cwdmatches the current working directory is preferred. - For an explicit
-EventsFile, the format is sniffed from the first events. - If neither harness is detected, the script exits with a non-zero status and a message naming both supported locations.
2. Review the generated report — read session-report.md and summarize key findings for the user:
- How many turns, how long, token usage
- What skills were loaded and when
- Build success/failure pattern
- Any stuck patterns or tooling issues detected
3. Add your own observations — append a section to the report with any additional context:
- Was the final app working? What's missing?
- Quality assessment of the generated code
- Suggestions specific to what went wrong
4. Include any tooling improvements or recommendations based on the analysis.
- Are there rules that need to be added to the Roslyn analyzer to prevent common mistakes detected during the session?
- Were there bugs or issues with winapp run or the BuildAndRun.ps1 script?
- Are there features that could be added to lower the number of turns required to complete a task?
What the Report Covers
| Section | Details |
|---|---|
| Overview | Harness, session ID, model, duration, turns, tokens (incl. cache tokens for Claude Code) |
| Prompt | The original user request |
| Turn Breakdown | Turns and tokens by category (building, coding, exploring, subagent dispatch, etc.) |
| Skills | Which were invoked and when, including from inside subagent transcripts |
| Subagents | (Claude Code only) Per-agent breakdown of dispatched subagents and their work |
| Build Analysis | Build attempts, failures, errors, whether BuildAndRun.ps1 was used |
| Stuck Patterns | Build loops, repeated file reads, obj/ clean cycles |
| Tooling Issues | Auto-detected improvement opportunities |
| Turn Detail | Every turn with tools used and errors flagged, parent and subagent transcripts shown separately |
When to Use
- When the user asks for a session report to understand what happened during an agent session.
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Analyzes an agentic session (GitHub Copilot CLI or Claude Code) from its
on-disk transcript and outputs a structured markdown report.
.DESCRIPTION
Auto-detects the harness from the environment and the on-disk format:
- Copilot CLI: ~/.copilot/session-state/<id>/events.jsonl
- Claude Code: ~/.claude/projects/<encoded-cwd>/<id>.jsonl
For Claude Code, subagent transcripts under <id>/subagents/agent-*.jsonl
are also analyzed and rolled into the parent session's report.
.PARAMETER SessionId
Session ID (UUID) to analyze. The script searches both harness locations.
.PARAMETER EventsFile
Direct path to a session transcript file. Format is sniffed from content.
.PARAMETER OutputFile
Path to write the markdown report. If omitted, writes to stdout.
.PARAMETER Format
Override harness detection. One of: Copilot, ClaudeCode.
.PARAMETER SkipSubagents
For Claude Code sessions, do not include subagent activity in the report.
.EXAMPLE
.\Analyze-Session.ps1
.\Analyze-Session.ps1 -SessionId "f116c51e-a9d1-4636-b250-1e00c746705e"
.\Analyze-Session.ps1 -EventsFile .\transcript.jsonl -OutputFile session-report.md
#>
param(
[string]$SessionId,
[string]$EventsFile,
[string]$OutputFile,
[ValidateSet('Copilot', 'ClaudeCode')]
[string]$Format,
[switch]$SkipSubagents
)
$ErrorActionPreference = 'Stop'
# -----------------------------------------------------------------------------
# Harness detection
# -----------------------------------------------------------------------------
function Get-CopilotSessionRoot {
Join-Path $env:USERPROFILE ".copilot\session-state"
}
function Get-ClaudeProjectsRoot {
Join-Path $env:USERPROFILE ".claude\projects"
}
function Test-CopilotEnvironment {
if ($env:COPILOT_AGENT_SESSION_ID -or $env:COPILOT_CLI -or $env:COPILOT_HOME -or $env:COPILOT_MODEL -or $env:COPILOT_GITHUB_TOKEN) { return $true }
return $false
}
function Test-ClaudeEnvironment {
if ($env:CLAUDECODE -eq '1') { return $true }
if ($env:CLAUDE_CODE_ENTRYPOINT) { return $true }
return $false
}
function Get-EventFormatFromContent {
param([string]$Path)
if (-not (Test-Path $Path)) { return $null }
$sniffed = 0
foreach ($line in Get-Content $Path -Encoding UTF8) {
if (-not $line.Trim()) { continue }
try { $obj = $line | ConvertFrom-Json } catch { continue }
$type = $obj.type
if ($type -in 'user.message', 'assistant.turn_start', 'assistant.message',
'tool.execution_start', 'tool.execution_complete',
'session.skills_loaded', 'session.tools_updated', 'result') {
return 'Copilot'
}
if ($type -in 'assistant', 'user', 'system', 'attachment',
'permission-mode', 'file-history-snapshot', 'queue-operation',
'ai-title', 'last-prompt') {
return 'ClaudeCode'
}
if (++$sniffed -ge 10) { break }
}
return $null
}
function Find-LatestCopilotSession {
$root = Get-CopilotSessionRoot
if (-not (Test-Path $root)) { return $null }
$latest = Get-ChildItem $root -Directory -ErrorAction SilentlyContinue |
Where-Object { Test-Path (Join-Path $_.FullName "events.jsonl") } |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if (-not $latest) { return $null }
return [pscustomobject]@{
SessionId = $latest.Name
Path = Join-Path $latest.FullName "events.jsonl"
Modified = $latest.LastWriteTime
}
}
function Get-ClaudeSessionCwd {
param([string]$Path)
foreach ($line in Get-Content $Path -Encoding UTF8 -TotalCount 50) {
if (-not $line.Trim()) { continue }
try { $obj = $line | ConvertFrom-Json } catch { continue }
if ($obj.cwd) { return [string]$obj.cwd }
}
return $null
}
function Find-LatestClaudeSession {
param([string]$PreferCwd)
$root = Get-ClaudeProjectsRoot
if (-not (Test-Path $root)) { return $null }
$candidates = Get-ChildItem $root -Recurse -Filter '*.jsonl' -File -ErrorAction SilentlyContinue |
Where-Object {
$_.Directory.Name -ne 'subagents' -and
$_.Directory.Parent.Name -eq 'projects'
} |
Sort-Object LastWriteTime -Descending
if (-not $candidates) { return $null }
if ($PreferCwd) {
foreach ($c in $candidates) {
$firstCwd = Get-ClaudeSessionCwd -Path $c.FullName
if ($firstCwd -and ($firstCwd.TrimEnd('\') -ieq $PreferCwd.TrimEnd('\'))) {
return [pscustomobject]@{
SessionId = [System.IO.Path]::GetFileNameWithoutExtension($c.Name)
Path = $c.FullName
Modified = $c.LastWriteTime
}
}
}
}
$top = $candidates | Select-Object -First 1
return [pscustomobject]@{
SessionId = [System.IO.Path]::GetFileNameWithoutExtension($top.Name)
Path = $top.FullName
Modified = $top.LastWriteTime
}
}
function Find-ClaudeSessionById {
param([string]$Id)
$root = Get-ClaudeProjectsRoot
if (-not (Test-Path $root)) { return $null }
$hit = Get-ChildItem $root -Recurse -Filter "$Id.jsonl" -File -ErrorAction SilentlyContinue |
Where-Object { $_.Directory.Name -ne 'subagents' } |
Select-Object -First 1
if (-not $hit) { return $null }
return [pscustomobject]@{
SessionId = $Id
Path = $hit.FullName
Modified = $hit.LastWriteTime
}
}
function Find-CopilotSessionById {
param([string]$Id)
$p = Join-Path (Get-CopilotSessionRoot) "$Id\events.jsonl"
if (-not (Test-Path $p)) { return $null }
return [pscustomobject]@{
SessionId = $Id
Path = $p
Modified = (Get-Item $p).LastWriteTime
}
}
function Write-UnsupportedHarnessError {
param([string]$ExtraContext)
$msg = @"
Analyze-Session: could not detect a supported session format.
Looked for:
- GitHub Copilot CLI: $((Get-CopilotSessionRoot))\<id>\events.jsonl
- Claude Code: $((Get-ClaudeProjectsRoot))\<encoded-cwd>\<id>.jsonl
$ExtraContext
If you're running a different agentic harness (Cursor, Aider, Cline, etc.),
this skill does not currently support it. Please file an issue at
https://github.com/microsoft/win-dev-skills/issues with:
- The harness name and version
- A sample session file (redacted)
- Where the harness stores session transcripts on disk
"@
Write-Error $msg -ErrorAction Stop
}
# -----------------------------------------------------------------------------
# Tool name normalization
# -----------------------------------------------------------------------------
$script:ToolNameMap = @{
'Read' = 'view'
'Edit' = 'edit'
'Write' = 'create'
'Glob' = 'glob'
'Grep' = 'grep'
'Bash' = 'shell'
'PowerShell' = 'powershell'
'Skill' = 'skill'
'Agent' = 'agent'
'WebFetch' = 'web_fetch'
'WebSearch' = 'web_search'
'ToolSearch' = 'tool_search'
'Monitor' = 'monitor'
'NotebookEdit' = 'edit'
'ScheduleWakeup' = 'schedule_wakeup'
'TaskCreate' = 'task_create'
'TaskUpdate' = 'task_update'
'TaskList' = 'task_list'
'TaskGet' = 'task_get'
'TaskOutput' = 'task_output'
'TaskStop' = 'task_stop'
'PushNotification' = 'push_notification'
'AskUserQuestion' = 'ask_user'
'EnterPlanMode' = 'plan_mode'
'ExitPlanMode' = 'plan_mode'
'EnterWorktree' = 'worktree'
'ExitWorktree' = 'worktree'
'CronCreate' = 'cron'
'CronDelete' = 'cron'
'CronList' = 'cron'
}
function Get-NormalizedToolName {
param([string]$Name)
if ($script:ToolNameMap.ContainsKey($Name)) { return $script:ToolNameMap[$Name] }
return $Name.ToLowerInvariant()
}
# -----------------------------------------------------------------------------
# Shared helpers
# -----------------------------------------------------------------------------
function New-NormalizedTurn {
param([int]$TurnNum, [string]$Timestamp)
return @{
TurnNum = $TurnNum
Timestamp = $Timestamp
Tools = @()
OutputTokens = 0
CacheReadTokens = 0
CacheCreateTokens = 0
TextSnippets = @()
SkillInvocations = @()
AgentSpawns = @()
Category = 'other'
}
}
function Get-ErrorSummary {
param([string]$ResultText)
if (-not $ResultText) { return @{ HasError = $false; Summary = @() } }
$hasErr = $ResultText -match 'error|FAILED|SyntaxError'
if (-not $hasErr) { return @{ HasError = $false; Summary = @() } }
$errLines = ($ResultText -split "`n") |
Where-Object { $_ -match 'error|FAILED|SyntaxError' } |
Select-Object -First 5
$errSummary = @()
foreach ($errLine in $errLines) {
$cleaned = $errLine.Trim()
if ($cleaned -match 'error (CS\d+|XLS\d+|XDG\d+|MSB\d+):\s*(.+?)(\s*\[|$)') {
$errSummary += "$($Matches[1]): $($Matches[2].Trim())"
} elseif ($cleaned -match 'SyntaxError:\s*(.+)') {
$errSummary += "SyntaxError: $($Matches[1].Trim())"
} elseif ($cleaned -match 'error (MSB\d+)') {
if ($cleaned -match 'error (MSB\d+):.*exited with code (\d+)') {
$errSummary += "$($Matches[1]): XamlCompiler.exe exited with code $($Matches[2])"
} else {
$errSummary += $Matches[1]
}
} elseif ($cleaned -match 'BUILD FAILED') {
$errSummary += "BUILD FAILED"
} else {
$errSummary += $cleaned.Substring(0, [Math]::Min($cleaned.Length, 120))
}
}
return @{ HasError = $true; Summary = ($errSummary | Select-Object -Unique) }
}
function Get-TurnCategory {
param($Turn)
$toolNames = $Turn.Tools | ForEach-Object { $_.Name }
$hasSkill = $Turn.SkillInvocations.Count -gt 0
$shellCmd = { param($t) ($t.Name -in 'powershell', 'shell') }
$hasBuild = $Turn.Tools | Where-Object { (& $shellCmd $_) -and ($_.Args.command -match 'dotnet build|MSBuild|BuildAndRun|msbuild') }
$hasRun = $Turn.Tools | Where-Object { (& $shellCmd $_) -and ($_.Args.command -match 'winapp run|BuildAndRun(?!.*-SkipRun)') }
$hasGit = $Turn.Tools | Where-Object { (& $shellCmd $_) -and ($_.Args.command -match '\bgit\b') }
$hasBuildError = $Turn.Tools | Where-Object { $_.HasError -and (& $shellCmd $_) }
$hasScaffold = $Turn.Tools | Where-Object { $_.Args.command -match 'dotnet new|New-Item.*Directory' }
$isDiagnosing = $Turn.Tools | Where-Object {
(& $shellCmd $_) -and ($_.Args.command -match 'XamlCompiler|output\.json|input\.json|-v d\b|-v:d|-verbosity|Select-String.*error|obj\\|temp_output|Remove-Item.*obj|Get-Content.*log|Get-Process')
}
$hasCreate = 'create' -in $toolNames
$hasEdit = 'edit' -in $toolNames
$hasView = 'view' -in $toolNames
$hasAgent = 'agent' -in $toolNames
if ($hasSkill -and $toolNames.Count -le 2) { return 'skill-load' }
if ($hasGit -and -not $hasBuild) { return 'git' }
if ($hasBuild -and $hasBuildError) { return 'build-fix' }
if ($hasBuild -and -not $hasBuildError) { return 'build-ok' }
if ($hasRun) { return 'run' }
if ($isDiagnosing -and -not $hasEdit) { return 'diagnosing' }
if ($hasScaffold) { return 'scaffold' }
if ($hasAgent) { return 'subagent' }
if ($hasCreate -and -not $hasEdit) { return 'code-create' }
if ($hasEdit) { return 'code-edit' }
if ($hasView -and -not $hasEdit -and -not $hasCreate) { return 'explore' }
if ($toolNames.Count -eq 0) { return 'thinking' }
return 'other'
}
function Format-ToolList {
param($Turn)
$parts = $Turn.Tools | ForEach-Object {
$err = if ($_.HasError) { " :x:" } else { "" }
$summary = ""
if ($_.Name -in 'powershell', 'shell') { $summary = ($_.Args.command -split "`n")[0]; if ($summary.Length -gt 60) { $summary = $summary.Substring(0, 60) } }
elseif ($_.Args.path) { $summary = Split-Path $_.Args.path -Leaf }
elseif ($_.Args.file_path) { $summary = Split-Path $_.Args.file_path -Leaf }
elseif ($_.Name -eq 'skill'){ $summary = $_.Args.skill }
elseif ($_.Name -eq 'agent'){ $summary = $_.Args.subagent_type }
elseif ($_.Args.pattern) { $summary = $_.Args.pattern }
if ($summary) { "$($_.Name)($summary)$err" } else { "$($_.Name)$err" }
}
$skills = if ($Turn.SkillInvocations.Count -gt 0) { " [skill: $($Turn.SkillInvocations -join ',')]" } else { "" }
return ($parts -join ', ') + $skills
}
# -----------------------------------------------------------------------------
# Copilot parser
# -----------------------------------------------------------------------------
function Parse-CopilotEvents {
param([string]$Path)
$lines = Get-Content $Path -Encoding UTF8
$events = @()
foreach ($line in $lines) {
if (-not $line.Trim()) { continue }
try { $events += ($line | ConvertFrom-Json) } catch { }
}
if ($events.Count -eq 0) {
Write-Error "No events found in $Path"
}
$userMsg = $events | Where-Object { $_.type -eq 'user.message' } | Select-Object -First 1
$resultEvent = $events | Where-Object { $_.type -eq 'result' } | Select-Object -First 1
$skillsLoaded = $events | Where-Object { $_.type -eq 'session.skills_loaded' } | Select-Object -First 1
$modelEvent = $events | Where-Object { $_.type -eq 'session.tools_updated' } | Select-Object -First 1
$prompt = if ($userMsg.data.content) { $userMsg.data.content } else { "(no prompt found)" }
$sid = if ($resultEvent.sessionId) { $resultEvent.sessionId } else { "(unknown)" }
$model = if ($modelEvent.data.model) { $modelEvent.data.model } else { "(unknown)" }
$exitCode = $resultEvent.exitCode
$usage = if ($resultEvent.usage) { $resultEvent.usage } else { @{} }
$firstTs = if ($events[0].timestamp) { [DateTime]::Parse($events[0].timestamp) } else { $null }
$lastTs = if ($events[-1].timestamp) { [DateTime]::Parse($events[-1].timestamp) } else { $null }
$durationMin = if ($firstTs -and $lastTs) { [math]::Round(($lastTs - $firstTs).TotalMinutes, 1) } else { 0 }
$availableSkills = @()
if ($skillsLoaded.data.skills) {
$availableSkills = $skillsLoaded.data.skills | ForEach-Object { $_.name }
}
$turns = @()
$currentTurn = $null
$toolStarts = @{}
foreach ($ev in $events) {
switch ($ev.type) {
'assistant.turn_start' {
$currentTurn = New-NormalizedTurn -TurnNum ($turns.Count + 1) -Timestamp $ev.timestamp
}
'assistant.message' {
if ($currentTurn) {
$currentTurn.OutputTokens = if ($ev.data.outputTokens) { $ev.data.outputTokens } else { 0 }
if ($ev.data.toolRequests) {
foreach ($tr in $ev.data.toolRequests) {
$callId = if ($tr.id) { $tr.id } else { $tr.toolCallId }
$currentTurn.Tools += @{
Name = (Get-NormalizedToolName $tr.name)
RawName = $tr.name
Args = $tr.arguments
CallId = $callId
HasError = $false
ErrorSummary = @()
}
}
}
}
}
'assistant.message_delta' {
if ($currentTurn -and $ev.data.deltaContent) {
$currentTurn.TextSnippets += $ev.data.deltaContent
}
}
'tool.execution_start' {
$toolStarts[$ev.data.toolCallId] = @{
Name = $ev.data.toolName
Args = $ev.data.arguments
}
}
'tool.execution_complete' {
$start = $toolStarts[$ev.data.toolCallId]
if ($start -and $currentTurn) {
$resultText = ""
$r = $ev.data.result
if ($r -is [string]) { $resultText = $r }
elseif ($r.textResultForLlm) { $resultText = $r.textResultForLlm }
elseif ($r.content) { $resultText = $r.content }
else { $resultText = ($r | ConvertTo-Json -Depth 3 -Compress) }
if ((Get-NormalizedToolName $start.Name) -eq 'skill' -and $start.Args.skill) {
$currentTurn.SkillInvocations += $start.Args.skill
}
$errInfo = Get-ErrorSummary -ResultText $resultText
foreach ($t in $currentTurn.Tools) {
if ($t.CallId -eq $ev.data.toolCallId) {
$t.HasError = $errInfo.HasError
if ($errInfo.HasError) { $t.ErrorSummary = $errInfo.Summary }
break
}
}
}
}
'assistant.turn_end' {
if ($currentTurn) {
$turns += [PSCustomObject]$currentTurn
$currentTurn = $null
}
}
}
}
return [pscustomobject]@{
Format = 'Copilot'
SessionId = $sid
Model = $model
DurationMin = $durationMin
Prompt = $prompt
ExitCode = $exitCode
Usage = $usage
Turns = $turns
AvailableSkills = $availableSkills
Subagents = @()
}
}
# -----------------------------------------------------------------------------
# Claude Code parser
# -----------------------------------------------------------------------------
function Parse-ClaudeEvents {
param(
[string]$Path,
[bool]$IsSubagent = $false
)
$events = @()
foreach ($line in (Get-Content $Path -Encoding UTF8)) {
if (-not $line.Trim()) { continue }
try { $events += ($line | ConvertFrom-Json) } catch { }
}
if ($events.Count -eq 0) {
Write-Error "No events found in $Path"
}
# Find the first "real" user prompt: not a meta event, not a sidechain
# event, not a tool_result, and not a slash-command/local-command wrapper.
$prompt = "(no prompt found)"
foreach ($ev in $events) {
if ($ev.type -ne 'user') { continue }
if ($ev.isMeta -or $ev.isSidechain) { continue }
$content = $ev.message.content
$candidate = $null
if ($content -is [string]) {
$candidate = $content
} elseif ($content -is [array]) {
$hasToolResult = $false
$textParts = @()
foreach ($block in $content) {
if ($block.type -eq 'tool_result') { $hasToolResult = $true; break }
if ($block.type -eq 'text' -and $block.text) { $textParts += $block.text }
}
if (-not $hasToolResult -and $textParts.Count -gt 0) {
$candidate = $textParts -join "`n"
}
}
if (-not $candidate) { continue }
# Skip slash-command stdout/caveat wrappers (they aren't user intent).
$stripped = $candidate.Trim()
if ($stripped -match '^<(local-)?command-(name|message|args|stdout)>') { continue }
if ($stripped -match '^<local-command-caveat>') { continue }
$prompt = $candidate
break
}
$sid = if ($events[0].sessionId) { $events[0].sessionId } else {
[System.IO.Path]::GetFileNameWithoutExtension($Path)
}
$firstAssistant = $events | Where-Object { $_.type -eq 'assistant' } | Select-Object -First 1
$model = if ($firstAssistant.message.model) { $firstAssistant.message.model } else { "(unknown)" }
$firstTs = $null; $lastTs = $null
foreach ($ev in $events) {
if ($ev.timestamp) {
try {
$ts = [DateTime]::Parse($ev.timestamp)
if (-not $firstTs) { $firstTs = $ts }
$lastTs = $ts
} catch { }
}
}
$durationMin = if ($firstTs -and $lastTs) { [math]::Round(($lastTs - $firstTs).TotalMinutes, 1) } else { 0 }
$toolResults = @{}
foreach ($ev in $events) {
if ($ev.type -eq 'user' -and $ev.message.content -is [array]) {
foreach ($block in $ev.message.content) {
if ($block.type -eq 'tool_result') {
$resultText = ""
$c = $block.content
if ($c -is [string]) { $resultText = $c }
elseif ($c -is [array]) {
foreach ($item in $c) {
if ($item.type -eq 'text' -and $item.text) {
$resultText += "`n" + $item.text
}
}
}
$isError = ($block.is_error -eq $true)
$toolResults[[string]$block.tool_use_id] = @{
Text = $resultText
IsErrorFlag = $isError
}
}
}
}
}
$turns = @()
$turnNum = 0
foreach ($ev in $events) {
if ($ev.type -ne 'assistant') { continue }
# Parent transcripts: skip sidechain events (those belong to subagents
# and live in their own files). Subagent transcripts: keep them - the
# subagent's own activity is stored as sidechain from the parent's
# perspective.
if (-not $IsSubagent -and $ev.isSidechain) { continue }
$turnNum++
$turn = New-NormalizedTurn -TurnNum $turnNum -Timestamp $ev.timestamp
$usage = $ev.message.usage
if ($usage) {
$turn.OutputTokens = if ($usage.output_tokens) { [int]$usage.output_tokens } else { 0 }
$turn.CacheReadTokens = if ($usage.cache_read_input_tokens) { [int]$usage.cache_read_input_tokens } else { 0 }
$turn.CacheCreateTokens = if ($usage.cache_creation_input_tokens) { [int]$usage.cache_creation_input_tokens } else { 0 }
}
$content = $ev.message.content
if ($content -is [array]) {
foreach ($block in $content) {
switch ($block.type) {
'text' {
if ($block.text) { $turn.TextSnippets += $block.text }
}
'tool_use' {
$callId = [string]$block.id
$args = $block.input
$normName = Get-NormalizedToolName $block.name
$tool = @{
Name = $normName
RawName = $block.name
Args = $args
CallId = $callId
HasError = $false
ErrorSummary = @()
}
if ($normName -eq 'skill' -and $args.skill) {
$turn.SkillInvocations += [string]$args.skill
}
if ($normName -eq 'agent') {
$turn.AgentSpawns += @{
ToolUseId = $callId
AgentType = if ($args.subagent_type) { [string]$args.subagent_type } else { 'general-purpose' }
Description = if ($args.description) { [string]$args.description } else { '' }
}
}
$tr = $toolResults[$callId]
if ($tr) {
$errInfo = Get-ErrorSummary -ResultText $tr.Text
if ($tr.IsErrorFlag -or $errInfo.HasError) {
$tool.HasError = $true
$tool.ErrorSummary = $errInfo.Summary
}
}
$turn.Tools += $tool
}
}
}
}
$turns += [PSCustomObject]$turn
}
$subagents = @()
if (-not $IsSubagent) {
$sessionDir = Join-Path (Split-Path $Path) ([System.IO.Path]::GetFileNameWithoutExtension($Path))
$subagentsDir = Join-Path $sessionDir 'subagents'
if (Test-Path $subagentsDir) {
$agentFiles = Get-ChildItem $subagentsDir -Filter 'agent-*.jsonl' -ErrorAction SilentlyContinue
foreach ($af in $agentFiles) {
$agentId = $af.BaseName -replace '^agent-', ''
$metaPath = Join-Path $af.DirectoryName "agent-$agentId.meta.json"
$meta = $null
if (Test-Path $metaPath) {
try { $meta = Get-Content $metaPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { }
}
$subResult = Parse-ClaudeEvents -Path $af.FullName -IsSubagent $true
$subagents += [pscustomobject]@{
AgentId = $agentId
AgentType = if ($meta.agentType) { $meta.agentType } else { '(unknown)' }
Description = if ($meta.description) { $meta.description } else { '' }
Turns = $subResult.Turns
DurationMin = $subResult.DurationMin
Model = $subResult.Model
}
}
}
}
return [pscustomobject]@{
Format = 'ClaudeCode'
SessionId = $sid
Model = $model
DurationMin = $durationMin
Prompt = $prompt
ExitCode = $null
Usage = @{}
Turns = $turns
AvailableSkills = @()
Subagents = $subagents
}
}
# -----------------------------------------------------------------------------
# Resolve which session to analyze
# -----------------------------------------------------------------------------
$session = $null
$detectedFormat = $Format
if ($EventsFile) {
if (-not (Test-Path $EventsFile)) {
Write-Error "Events file not found: $EventsFile"
exit 1
}
if (-not $detectedFormat) {
$detectedFormat = Get-EventFormatFromContent -Path $EventsFile
if (-not $detectedFormat) {
Write-UnsupportedHarnessError -ExtraContext "The file at '$EventsFile' did not match either known format. Use -Format to override."
}
}
$session = [pscustomobject]@{
SessionId = [System.IO.Path]::GetFileNameWithoutExtension($EventsFile)
Path = (Resolve-Path $EventsFile).Path
Modified = (Get-Item $EventsFile).LastWriteTime
}
} elseif ($SessionId) {
$session = Find-CopilotSessionById -Id $SessionId
if ($session) {
if (-not $detectedFormat) { $detectedFormat = 'Copilot' }
} else {
$session = Find-ClaudeSessionById -Id $SessionId
if ($session -and -not $detectedFormat) { $detectedFormat = 'ClaudeCode' }
}
if (-not $session) {
Write-UnsupportedHarnessError -ExtraContext "Session ID '$SessionId' was not found in either harness location."
}
} else {
# Auto-detect: prefer the *current* session via harness-provided env vars
# (COPILOT_AGENT_SESSION_ID / CLAUDE_SESSION_ID) so we don't accidentally
# pick up a more-recently-modified parallel session from another terminal.
if (-not $detectedFormat) {
if (Test-ClaudeEnvironment) { $detectedFormat = 'ClaudeCode' }
elseif (Test-CopilotEnvironment) { $detectedFormat = 'Copilot' }
}
$envSessionId = $null
switch ($detectedFormat) {
'Copilot' { if ($env:COPILOT_AGENT_SESSION_ID) { $envSessionId = $env:COPILOT_AGENT_SESSION_ID } }
'ClaudeCode' { if ($env:CLAUDE_SESSION_ID) { $envSessionId = $env:CLAUDE_SESSION_ID } }
}
if ($envSessionId) {
switch ($detectedFormat) {
'Copilot' { $session = Find-CopilotSessionById -Id $envSessionId }
'ClaudeCode' { $session = Find-ClaudeSessionById -Id $envSessionId }
}
if (-not $session) {
Write-Warning "Current session ID '$envSessionId' from $detectedFormat env var was not found on disk; falling back to most recent."
}
}
if (-not $session) {
switch ($detectedFormat) {
'ClaudeCode' { $session = Find-LatestClaudeSession -PreferCwd $PWD.Path }
'Copilot' { $session = Find-LatestCopilotSession }
default {
$cop = Find-LatestCopilotSession
$cla = Find-LatestClaudeSession -PreferCwd $PWD.Path
if ($cop -and $cla) {
if ($cop.Modified -ge $cla.Modified) { $session = $cop; $detectedFormat = 'Copilot' }
else { $session = $cla; $detectedFormat = 'ClaudeCode' }
} elseif ($cop) { $session = $cop; $detectedFormat = 'Copilot' }
elseif ($cla) { $session = $cla; $detectedFormat = 'ClaudeCode' }
}
}
}
if (-not $session) {
Write-UnsupportedHarnessError -ExtraContext "No sessions were found in either harness's storage directory."
}
}
# -----------------------------------------------------------------------------
# Parse the session
# -----------------------------------------------------------------------------
switch ($detectedFormat) {
'Copilot' { $parsed = Parse-CopilotEvents -Path $session.Path }
'ClaudeCode' { $parsed = Parse-ClaudeEvents -Path $session.Path }
default { Write-UnsupportedHarnessError -ExtraContext "Format '$detectedFormat' is not supported." }
}
# Fall back to the session ID we resolved during discovery if the parser
# couldn't recover one from the events (e.g., Copilot transcripts that were
# truncated before the `result` event was written).
if ($parsed.SessionId -eq '(unknown)' -and $session.SessionId) {
$parsed.SessionId = $session.SessionId
}
foreach ($t in $parsed.Turns) { $t.Category = Get-TurnCategory -Turn $t }
foreach ($sa in $parsed.Subagents) {
foreach ($t in $sa.Turns) { $t.Category = Get-TurnCategory -Turn $t }
}
# -----------------------------------------------------------------------------
# Aggregate analysis
# -----------------------------------------------------------------------------
$includeSubagents = -not $SkipSubagents -and $parsed.Subagents.Count -gt 0
$allTurns = @($parsed.Turns)
if ($includeSubagents) {
foreach ($sa in $parsed.Subagents) { $allTurns += $sa.Turns }
}
$buildAttempts = ($allTurns | Where-Object { $_.Category -in 'build-ok', 'build-fix' }).Count
$buildSuccesses = ($allTurns | Where-Object { $_.Category -eq 'build-ok' }).Count
$buildFailures = ($allTurns | Where-Object { $_.Category -eq 'build-fix' }).Count
$buildErrors = @()
foreach ($t in $allTurns) {
foreach ($tool in $t.Tools) {
if ($tool.HasError -and $tool.Name -in 'powershell', 'shell' -and $tool.Args.command -match '\bdotnet build\b|\bmsbuild\b|BuildAndRun\.ps1|BuildAndRun ' -and $tool.ErrorSummary.Count -gt 0) {
$buildErrors += @{ Turn = $t.TurnNum; Errors = $tool.ErrorSummary }
}
}
}
$buildAndRunUsed = $allTurns | Where-Object {
$_.Tools | Where-Object { $_.Name -in 'powershell', 'shell' -and $_.Args.command -match 'BuildAndRun' }
}
$rawDotnetBuilds = $allTurns | Where-Object {
$_.Tools | Where-Object { $_.Name -in 'powershell', 'shell' -and $_.Args.command -match 'dotnet build' -and $_.Args.command -notmatch 'BuildAndRun' }
}
if ($buildAndRunUsed -and -not $rawDotnetBuilds) {
$buildScriptStatus = "Used BuildAndRun.ps1 for all builds"
} elseif ($buildAndRunUsed -and $rawDotnetBuilds) {
$buildScriptStatus = "Mixed: raw 'dotnet build' $($rawDotnetBuilds.Count)x, BuildAndRun.ps1 $($buildAndRunUsed.Count)x"
} elseif ($rawDotnetBuilds) {
$buildScriptStatus = "NOT USED: raw 'dotnet build' $($rawDotnetBuilds.Count)x, never used BuildAndRun.ps1"
} else {
$buildScriptStatus = "No build commands detected"
}
$skillTimeline = @()
foreach ($t in $parsed.Turns) {
foreach ($skill in $t.SkillInvocations) {
$skillTimeline += @{ Turn = $t.TurnNum; Skill = $skill; Origin = 'parent' }
}
}
foreach ($sa in $parsed.Subagents) {
foreach ($t in $sa.Turns) {
foreach ($skill in $t.SkillInvocations) {
$skillTimeline += @{ Turn = $t.TurnNum; Skill = $skill; Origin = "subagent:$($sa.AgentType)" }
}
}
}
$invokedSkills = $skillTimeline | ForEach-Object { $_.Skill } | Select-Object -Unique
$notInvoked = $parsed.AvailableSkills | Where-Object { $_ -notin $invokedSkills -and $_ -ne 'customize-cloud-agent' }
$totalOutputTokens = ($allTurns | Measure-Object -Property OutputTokens -Sum).Sum
$totalCacheReadTokens = ($allTurns | Measure-Object -Property CacheReadTokens -Sum).Sum
$totalCacheCreateTokens = ($allTurns | Measure-Object -Property CacheCreateTokens -Sum).Sum
$categoryGroups = $allTurns | Group-Object -Property Category
$categoryTable = $categoryGroups | ForEach-Object {
$tokens = ($_.Group | Measure-Object -Property OutputTokens -Sum).Sum
@{ Category = $_.Name; Turns = $_.Count; Tokens = $tokens }
} | Sort-Object { $_.Turns } -Descending
$stuckPatterns = @()
$fileReads = @{}
foreach ($t in $allTurns) {
foreach ($tool in $t.Tools) {
if ($tool.Name -eq 'view' -and ($tool.Args.path -or $tool.Args.file_path)) {
$p = if ($tool.Args.path) { $tool.Args.path } else { $tool.Args.file_path }
$file = Split-Path $p -Leaf
$fileReads[$file] = $(if ($fileReads[$file]) { $fileReads[$file] } else { 0 }) + 1
}
}
}
$excessiveReads = $fileReads.GetEnumerator() | Where-Object { $_.Value -ge 3 }
if ($excessiveReads) {
$detail = ($excessiveReads | ForEach-Object { "$($_.Key) ($($_.Value)x)" }) -join ', '
$stuckPatterns += "Repeated file reads: $detail"
}
$consecutive = 0; $maxConsec = 0
foreach ($t in $allTurns) {
if ($t.Category -eq 'build-fix') { $consecutive++; $maxConsec = [Math]::Max($maxConsec, $consecutive) }
elseif ($t.Category -eq 'build-ok') { $consecutive = 0 }
}
if ($maxConsec -ge 3) {
$stuckPatterns += "Build loop: $maxConsec consecutive build failures before success"
}
$objCleans = ($allTurns | Where-Object {
$_.Tools | Where-Object { $_.Args.command -match 'Remove-Item.*obj' }
}).Count
if ($objCleans -ge 2) {
$stuckPatterns += "Cleaned obj/ directory ${objCleans}x (suggests stale XAML compiler state)"
}
$toolingIssues = @()
if ($rawDotnetBuilds -and $rawDotnetBuilds.Count -gt 0) {
$toolingIssues += @{
Area = "BuildAndRun.ps1"
Issue = $buildScriptStatus
Suggestion = "Agent should use BuildAndRun.ps1 for builds - it includes the Roslyn analyzer, auto-detects platform, and handles common errors."
}
}
if ($buildErrors | Where-Object { $_.Errors -match 'MSB3073' }) {
$toolingIssues += @{
Area = "XAML Compiler"
Issue = "XamlCompiler.exe crashed (MSB3073) - agent could not diagnose from error output"
Suggestion = "Clean obj/ first when MSB3073 occurs. CS0103 errors for x:Name elements are a side-effect of XAML compiler failure - fix XAML before C#."
}
}
$devWorkflowEntry = $skillTimeline | Where-Object { $_.Skill -match 'winui-dev-workflow' } | Select-Object -First 1
$firstBuildTurn = ($allTurns | Where-Object { $_.Category -in 'build-ok', 'build-fix' } | Select-Object -First 1).TurnNum
if ($devWorkflowEntry -and $rawDotnetBuilds -and $devWorkflowEntry.Turn -gt $firstBuildTurn) {
$toolingIssues += @{
Area = "Skill timing"
Issue = "dev-workflow skill loaded at turn $($devWorkflowEntry.Turn) but first build was turn $firstBuildTurn"
Suggestion = "Agent should load dev-workflow before its first build attempt."
}
}
# -----------------------------------------------------------------------------
# Render markdown report
# -----------------------------------------------------------------------------
$categoryLabels = @{
'skill-load' = 'Skill loading'; 'explore' = 'Reading/exploring'; 'scaffold' = 'Scaffolding'
'code-create' = 'Creating files'; 'code-edit' = 'Editing code'; 'build-ok' = 'Build (success)'
'build-fix' = 'Build (failed)'; 'run' = 'Running app'; 'git' = 'Git operations'
'thinking' = 'Thinking (no tools)'; 'diagnosing' = 'Diagnosing errors'; 'subagent' = 'Subagent dispatch'
'other' = 'Other'
}
$md = @()
$md += "# Session Analysis Report"
$md += ""
$md += "## Overview"
$md += ""
$md += "| Field | Value |"
$md += "|-------|-------|"
$md += "| Harness | $($parsed.Format) |"
$md += "| Session ID | " + '`' + $parsed.SessionId + '`' + " |"
$md += "| Model | $($parsed.Model) |"
$md += "| Duration | $($parsed.DurationMin) min |"
$md += "| Turns (parent) | $($parsed.Turns.Count) |"
if ($includeSubagents) {
$subTurnTotal = ($parsed.Subagents | ForEach-Object { $_.Turns.Count } | Measure-Object -Sum).Sum
$md += "| Subagents | $($parsed.Subagents.Count) ($subTurnTotal turns) |"
}
$md += "| Output tokens (combined) | $($totalOutputTokens.ToString('N0')) |"
if ($parsed.Format -eq 'ClaudeCode') {
$md += "| Cache read tokens | $($totalCacheReadTokens.ToString('N0')) |"
$md += "| Cache create tokens | $($totalCacheCreateTokens.ToString('N0')) |"
}
if ($parsed.Format -eq 'Copilot') {
$md += "| Premium requests | $(if ($parsed.Usage.premiumRequests) { $parsed.Usage.premiumRequests } else { 'N/A' }) |"
$md += "| Exit code | $($parsed.ExitCode) |"
$md += "| Lines added | $(if ($parsed.Usage.codeChanges.linesAdded) { $parsed.Usage.codeChanges.linesAdded } else { 'N/A' }) |"
$md += "| Files modified | $(if ($parsed.Usage.codeChanges.filesModified) { $parsed.Usage.codeChanges.filesModified.Count } else { 'N/A' }) |"
}
$md += ""
$md += "## Prompt"
$md += ""
$promptDisplay = if ($parsed.Prompt.Length -gt 500) { $parsed.Prompt.Substring(0, 500) + "..." } else { $parsed.Prompt }
$md += '```'
$md += $promptDisplay
$md += '```'
$md += ""
$md += "## Turn Breakdown"
$md += ""
if ($includeSubagents) {
$md += "_Combined parent + $($parsed.Subagents.Count) subagent transcript(s)._"
$md += ""
}
$md += "| Category | Turns | Output Tokens |"
$md += "|----------|------:|--------------:|"
foreach ($cat in $categoryTable) {
$label = if ($categoryLabels[$cat.Category]) { $categoryLabels[$cat.Category] } else { $cat.Category }
$md += "| $label | $($cat.Turns) | $($cat.Tokens.ToString('N0')) |"
}
$md += ""
$md += "## Skills"
$md += ""
if ($skillTimeline.Count -gt 0) {
$md += "**Invoked:**"
foreach ($s in $skillTimeline) {
$origin = if ($s.Origin -eq 'parent') { '' } else { " _(in $($s.Origin))_" }
$md += "- Turn $($s.Turn): " + '`' + $s.Skill + '`' + $origin
}
} else {
$md += "_No skills were invoked during this session._"
}
$md += ""
if ($parsed.AvailableSkills.Count -gt 0 -and $notInvoked.Count -gt 0) {
$notInvokedStr = ($notInvoked | ForEach-Object { '`' + $_ + '`' }) -join ', '
$md += "**Available but not invoked:** $notInvokedStr"
$md += ""
} elseif ($parsed.Format -eq 'ClaudeCode') {
$md += "_Available-skill enumeration is not yet supported for Claude Code transcripts._"
$md += ""
}
if ($includeSubagents) {
$md += "## Subagents"
$md += ""
$md += "| Agent ID | Type | Turns | Duration | Description |"
$md += "|---|---|---:|---:|---|"
foreach ($sa in $parsed.Subagents) {
$desc = if ($sa.Description.Length -gt 60) { $sa.Description.Substring(0, 60) + '...' } else { $sa.Description }
$md += "| ``$($sa.AgentId)`` | $($sa.AgentType) | $($sa.Turns.Count) | $($sa.DurationMin) min | $desc |"
}
$md += ""
}
$md += "## Build Analysis"
$md += ""
$md += "- **Attempts:** $buildAttempts ($buildSuccesses success, $buildFailures failed)"
$md += "- **BuildAndRun.ps1:** $buildScriptStatus"
$md += ""
if ($buildErrors.Count -gt 0) {
$md += "**Build errors encountered:**"
$md += ""
foreach ($be in $buildErrors) {
$md += "Turn $($be.Turn):"
foreach ($err in $be.Errors) {
$md += "- " + '`' + $err + '`'
}
}
$md += ""
}
if ($stuckPatterns.Count -gt 0) {
$md += "## Stuck Patterns"
$md += ""
foreach ($sp in $stuckPatterns) { $md += "- $sp" }
$md += ""
}
if ($toolingIssues.Count -gt 0) {
$md += "## Tooling Improvement Opportunities"
$md += ""
foreach ($ti in $toolingIssues) {
$md += "### $($ti.Area)"
$md += "- **Issue:** $($ti.Issue)"
$md += "- **Suggestion:** $($ti.Suggestion)"
$md += ""
}
}
$md += "## Turn Detail"
$md += ""
$md += "_Parent session._"
$md += ""
$md += "| # | Category | Tokens | Tools |"
$md += "|--:|----------|-------:|-------|"
foreach ($t in $parsed.Turns) {
$md += "| $($t.TurnNum) | $($t.Category) | $($t.OutputTokens.ToString('N0')) | $(Format-ToolList $t) |"
}
$md += ""
if ($includeSubagents) {
foreach ($sa in $parsed.Subagents) {
$md += "_Subagent " + '`' + $sa.AgentType + '`' + " (id ``$($sa.AgentId)``)._"
$md += ""
$md += "| # | Category | Tokens | Tools |"
$md += "|--:|----------|-------:|-------|"
foreach ($t in $sa.Turns) {
$md += "| $($t.TurnNum) | $($t.Category) | $($t.OutputTokens.ToString('N0')) | $(Format-ToolList $t) |"
}
$md += ""
}
}
$report = $md -join "`n"
# -----------------------------------------------------------------------------
# Privacy notice (kept in sync with SKILL.md)
# -----------------------------------------------------------------------------
$privacyHeading = "## Privacy and sensitivity — read before sharing this file"
$privacyBody = @(
""
"**This report was generated from your live agent session and was NOT redacted.** It can include any of the following, depending on what your session involved:"
""
"- File contents and paths the agent read or edited (code, configuration, secrets accidentally pasted into prompts, internal URLs, customer data, anything you asked the agent about)."
"- Your prompts verbatim — including any credentials, tokens, identifiers, or proprietary information you typed."
"- Tool output — ``git`` history, environment variables echoed by failing commands, build logs containing machine names and ``C:\Users\<you>\…`` paths, etc."
"- Error messages quoting source code or stack traces from third-party libraries."
""
"**You are responsible for the contents of this file.** Open it in your editor and read it end-to-end before attaching it to a public issue, posting it in chat, or sending it outside your organization. Redact anything sensitive (paths, names, secrets, business logic). When in doubt, share excerpts rather than the whole file, or ask the agent to summarize the metrics instead."
""
)
$privacySection = @($privacyHeading) + $privacyBody -join "`n"
$reportLines = $report -split "`n"
$insertAt = 1
for ($i = 0; $i -lt $reportLines.Count; $i++) {
if ($reportLines[$i] -match '^## Overview') { $insertAt = $i; break }
}
$reportWithNotice = (
$reportLines[0..($insertAt - 1)] +
@($privacySection, "") +
$reportLines[$insertAt..($reportLines.Count - 1)]
) -join "`n"
if ($OutputFile) {
Set-Content -Path $OutputFile -Value $reportWithNotice -Encoding UTF8
Write-Host ""
Write-Host "Report saved to: $OutputFile" -ForegroundColor Green
Write-Host ""
Write-Host "================================================================" -ForegroundColor Yellow
Write-Host " PRIVACY NOTICE - READ BEFORE SHARING $OutputFile" -ForegroundColor Yellow
Write-Host "================================================================" -ForegroundColor Yellow
Write-Host " This report contains your unredacted session transcript:" -ForegroundColor Yellow
Write-Host " * file contents and paths the agent read or edited" -ForegroundColor Yellow
Write-Host " * your prompts verbatim (including any secrets you pasted)" -ForegroundColor Yellow
Write-Host " * tool output, error messages, local paths, env values" -ForegroundColor Yellow
Write-Host "" -ForegroundColor Yellow
Write-Host " You are responsible for what you share. Open the file and" -ForegroundColor Yellow
Write-Host " read it end-to-end before posting it publicly or sending it" -ForegroundColor Yellow
Write-Host " outside your organization. Redact anything sensitive." -ForegroundColor Yellow
Write-Host "================================================================" -ForegroundColor Yellow
Write-Host ""
} else {
Write-Output $reportWithNotice
}
Related skills
FAQ
What does winui-session-report produce?
A session-report.md with turns, tokens, skills, build results, stuck patterns, and tooling issues.
When should I use winui-session-report?
When reviewing a Copilot CLI or Claude Code session to understand agent behavior and failures.
Is winui-session-report safe to install?
Review the Security Audits panel on this page before installing in production.