Hooks/scripts/Add-SessionContext.ps1

<#
.SYNOPSIS
    SessionStart hook that injects the deterministic part of Pre-flight.
.DESCRIPTION
    Reads the VS Code SessionStart hook payload from standard input, resolves the
    session working directory, and returns a short block of additional context on
    standard output: the current UTC timestamp and an authoritative statement of
    whether a Memory Bank exists in the workspace.
 
    The workspace summary supplied at session start omits dotfile folders, which
    is the recurring cause of agents concluding that no Memory Bank exists. This
    hook probes the filesystem instead of relying on the model to remember to.
.PARAMETER InputJson
    Hook payload as JSON. Defaults to reading standard input. Tests pass the
    payload directly so they do not depend on redirected input.
.NOTES
    Emits the SessionStart output contract shared by VS Code, Copilot CLI, and
    Claude Code. Always exits 0 so a probe failure never blocks a session.
#>


[CmdletBinding()]
param(
    [Parameter()]
    [AllowEmptyString()]
    [AllowNull()]
    [string]$InputJson
)

if ([string]::IsNullOrEmpty($InputJson)) {
    # Decode explicitly: Windows PowerShell would otherwise use the console input
    # encoding, which mangles non-ASCII payloads that pwsh reads as UTF-8.
    $reader = [IO.StreamReader]::new([Console]::OpenStandardInput(), [Text.UTF8Encoding]::new($false))
    try {
        $InputJson = $reader.ReadToEnd()
    } finally {
        $reader.Dispose()
    }
}

$workingDirectory = $null
if (-not [string]::IsNullOrWhiteSpace($InputJson)) {
    try {
        $workingDirectory = [string]($InputJson | ConvertFrom-Json -ErrorAction Stop).cwd
    } catch {
        $workingDirectory = $null
    }
}

if ([string]::IsNullOrWhiteSpace($workingDirectory)) {
    $workingDirectory = (Get-Location).Path
}

# The path is interpolated into the instruction channel, so strip control
# characters that a hostile workspace name could use to inject extra lines.
$workingDirectory = $workingDirectory -replace '[\p{Cc}]', ' '

<#
    The payload supplies this path, so it is untrusted and may name a drive or a
    shape this host cannot resolve. Combine and probe it through .NET rather than
    the PowerShell provider: a provider that cannot resolve the path writes to
    standard error, and the caller merges the streams, which would corrupt the
    JSON contract on standard output. [System.IO.File]::Exists returns false for
    any path it cannot read and never throws.
#>

$memoryBankExists = $false
$memoryBankIndex = $workingDirectory

try {
    $memoryBankIndex = [System.IO.Path]::Combine($workingDirectory, '.memory-bank', 'index.md')
    $memoryBankExists = [System.IO.File]::Exists($memoryBankIndex)
} catch {
    $memoryBankExists = $false
}

if ($memoryBankExists) {
    $memoryBankState = "A Memory Bank exists at $memoryBankIndex. This probe is authoritative: " +
    'do not conclude that the Memory Bank is absent. Read the index and apply its routing table ' +
    'before the first tool call.'
} else {
    $memoryBankState = "No Memory Bank exists under $workingDirectory. This probe is authoritative. " +
    'Create one only before a durable repository write, using the memory-bank Skill.'
}

$line = @(
    "Session started at $((Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm')) UTC."
    $memoryBankState
    'Open the reply with that UTC timestamp and a one-line PRE-FLIGHT acknowledgment.'
    'Never push or otherwise mutate a git remote unless the user asks in the current turn.'
)

$output = [ordered]@{
    continue = $true
    hookSpecificOutput = [ordered]@{
        hookEventName = 'SessionStart'
        additionalContext = ($line -join ' ')
    }
}

$output | ConvertTo-Json -Depth 5 -Compress
exit 0