# --- 4. Detached delayed waker (200ms) — re-foregrounds the captured HWND, then
#        TYPES the note ONLY IF that HWND is foreground AND its title is unchanged. ---
$hptr = $hwnd.ToInt64()
$note = 'Proceed with next steps'
# Pass the captured title as base64 so arbitrary title chars can't break the inner script.
$titleB64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($title))
$inner = @"
Start-Sleep -Milliseconds 200
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SCW {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static string TitleOf(IntPtr h) { int n = GetWindowTextLength(h); StringBuilder sb = new StringBuilder(n + 1); GetWindowText(h, sb, sb.Capacity); return sb.ToString(); }
}
'@
`$h = [IntPtr]$hptr
`$want = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$titleB64'))
if ([SCW]::IsIconic(`$h)) { [SCW]::ShowWindow(`$h, 9) | Out-Null }
[SCW]::SetForegroundWindow(`$h) | Out-Null
Start-Sleep -Milliseconds 120
# ABORT the SendKeys unless our exact window is foreground AND its title is unchanged.
`$fg = [SCW]::GetForegroundWindow()
`$now = [SCW]::TitleOf(`$h)
if (`$fg -ne `$h -or `$now -ne `$want) { exit 0 }
`$ws = New-Object -ComObject WScript.Shell
`$ws.SendKeys('$note')
Start-Sleep -Milliseconds 150
`$ws.SendKeys('{ENTER}')
"@
$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($inner))
Start-Process -FilePath 'pwsh' -ArgumentList '-NoProfile','-NonInteractive','-EncodedCommand',$enc -WindowStyle Hidden | Out-Null

"SELF-CLEAR armed: id=$id winpid=$winpid hwnd=$hwnd title=`"$title`" — /clear queued; self-note typed in ~200ms (aborts if window/title changes)"

# self-clear.ps1 — one-shot self-clear + delayed self-wake.
#
# Order (per design):
#   1. Resolve this session's agent_id via `owl whoami` (context-independent; survives /clear).
#   2. Identify this session's terminal window as the CURRENT FOREGROUND window,
#      and only proceed if that window is owned by one of this process's ancestor
#      pids (e.g. the WindowsTerminal.exe hosting this tab). Capture its HWND +
#      title bar text for the waker's safety check.
#   3. Type "/clear" + Enter  — QUEUED; fires only when this agent's turn ends.
#   4. Spawn a DETACHED waker that waits 200ms, re-foregrounds the captured HWND, and
#      — ONLY IF that HWND is now foreground AND its title still matches the captured
#      title — TYPES the self-note as a fresh user prompt + Enter. Otherwise it ABORTS
#      the SendKeys (so the note never lands in the wrong window/tab).
#
# Why foreground + title (not pid/MainWindowHandle): Windows Terminal serves many
# windows from ONE process over headless ConPTY, so neither the ancestor pid nor
# Get-Process.MainWindowHandle can tell WT windows apart. The window the user
# triggered this from IS the foreground window at arm time; its HWND is unique. The
# title check guards the ~200ms gap — if the user switches tab/window, the title
# changes and the waker aborts instead of typing blind.
#
# Why detached + delayed: a blocking sleep here would hold the turn open, so /clear
# could never fire. The waker must outlive the turn. The short gap lets the turn end
# and /clear wipe the context first.
#
# Why TYPE the note (not `owl send`): after /clear the session sits IDLE awaiting user
# input. A spooled owl message cannot self-start a turn — it surfaces only during an
# already-active turn (PreToolUse hook) or in a live Monitor stream (whose buffer /clear
# wipes). So the prior "arrives as a waking poll message" design never woke the agent.
# A typed prompt + Enter deterministically starts a fresh turn AND is the re-seed.
$ErrorActionPreference = 'Stop'

# --- 1. Resolve agent_id ---
$owl = $env:OWL
if (-not $owl) { Write-Error 'OWL env not set — restart the session so the SessionStart hook runs.'; exit 1 }
$who = & $owl whoami 2>&1 | Out-String
if ($who -notmatch 'WHOAMI:([^\s]+)') {
  Write-Error "No perch for this session (whoami: $($who.Trim())). Run /spt:ready or /spt:live first — NOT clearing."
  exit 2
}
$id = $matches[1]

# --- 2. Identify this session's terminal window (foreground + ancestor-owned) ---
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SC {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static uint PidOf(IntPtr h) { uint p; GetWindowThreadProcessId(h, out p); return p; }
  public static string TitleOf(IntPtr h) {
    int n = GetWindowTextLength(h);
    StringBuilder sb = new StringBuilder(n + 1);
    GetWindowText(h, sb, sb.Capacity);
    return sb.ToString();
  }
}
'@
# Collect this process's ancestor pids (self up to 8 hops).
$ancestors = @(); $cur = $PID
for ($i = 0; $i -lt 8; $i++) {
  $ancestors += [uint32]$cur
  $ci = Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -ErrorAction SilentlyContinue
  if (-not $ci) { break }
  $cur = $ci.ParentProcessId
}
# The window the user triggered this from is the foreground window. Require that it
# belongs to one of our ancestor pids (the terminal hosting this tab) — otherwise we
# cannot safely identify the right window, so abort WITHOUT clearing.
$hwnd = [SC]::GetForegroundWindow()
$winpid = if ($hwnd -ne [IntPtr]::Zero) { [SC]::PidOf($hwnd) } else { 0 }
if ($hwnd -eq [IntPtr]::Zero -or ($ancestors -notcontains [uint32]$winpid)) {
  Write-Error "Foreground window (pid=$winpid) is not this agent's terminal — keep the agent's Windows Terminal window/tab focused and retry. NOT clearing."
  exit 3
}
$title = [SC]::TitleOf($hwnd)   # captured now; the waker re-checks it before typing
$ws = New-Object -ComObject WScript.Shell

# --- 3. Type /clear + Enter (queued; processes at turn-end) ---
# The window is already foreground (we just verified it), so no focus change needed.
$ws.SendKeys('/clear')
Start-Sleep -Milliseconds 150
$ws.SendKeys('{ENTER}')

# --- 4. Detached delayed waker (200ms) — re-foregrounds + TYPES the note as a fresh prompt ---
$hptr = $hwnd.ToInt64()
$note = 'Proceed with next steps'
$inner = @"
Start-Sleep -Milliseconds 200
Add-Type @'
using System;
using System.Runtime.InteropServices;
public class SCW {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
}
'@
`$h = [IntPtr]$hptr
if ([SCW]::IsIconic(`$h)) { [SCW]::ShowWindow(`$h, 9) | Out-Null }
[SCW]::SetForegroundWindow(`$h) | Out-Null
`$ws = New-Object -ComObject WScript.Shell
try { `$ws.AppActivate($winpid) | Out-Null } catch {}
Start-Sleep -Milliseconds 400
`$ws.SendKeys('$note')
Start-Sleep -Milliseconds 150
`$ws.SendKeys('{ENTER}')
"@
$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($inner))
Start-Process -FilePath 'pwsh' -ArgumentList '-NoProfile','-NonInteractive','-EncodedCommand',$enc -WindowStyle Hidden | Out-Null

"SELF-CLEAR armed: id=$id winpid=$winpid hwnd=$hwnd — /clear queued; self-note typed in ~200ms"

-# --- 4. Detached delayed waker (200ms) — re-foregrounds + TYPES the note as a fresh prompt ---

+# --- 4. Detached delayed waker (200ms) — re-foregrounds the captured HWND, then

+#        TYPES the note ONLY IF that HWND is foreground AND its title is unchanged. ---

+# Pass the captured title as base64 so arbitrary title chars can't break the inner script.

+  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();

+  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);

+  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);

+  public static string TitleOf(IntPtr h) { int n = GetWindowTextLength(h); StringBuilder sb = new StringBuilder(n + 1); GetWindowText(h, sb, sb.Capacity); return sb.ToString(); }

+# ABORT the SendKeys unless our exact window is foreground AND its title is unchanged.

+if (`$fg -ne `$h -or `$now -ne `$want) { exit 0 }

-try { `$ws.AppActivate($winpid) | Out-Null } catch {}

-"SELF-CLEAR armed: id=$id winpid=$winpid hwnd=$hwnd — /clear queued; self-note typed in ~200ms"

+"SELF-CLEAR armed: id=$id winpid=$winpid hwnd=$hwnd title=`"$title`" — /clear queued; self-note typed in ~200ms (aborts if window/title changes)"


Validating: parse outer + compile both C# classes + parse the generated inner waker script (with sample values) + title base64 round-trip.


PowerShell

$f='C:\Users\decid\.claude\skills\self-clear\scripts\self-clear.ps1'
$e=$null;[System.Management.Automation.Language.Parser]::ParseFile($f,[ref]$null,[ref]$e)|Out-Null
if($e){"OUTER PARSE ERRORS:";$e|%{$_.Message}}else{"OUTER PARSE OK"}

# Compile SC (arm) + SCW (waker) C# exactly as in file
$sc=@'
using System;using System.Text;using System.Runtime.InteropServices;
public class SCa {
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static uint PidOf(IntPtr h){uint p;GetWindowThreadProcessId(h,out p);return p;}
  public static string TitleOf(IntPtr h){int n=GetWindowTextLength(h);StringBuilder sb=new StringBuilder(n+1);GetWindowText(h,sb,sb.Capacity);return sb.ToString();}
}
'@
try{Add-Type $sc -EA Stop;"SC COMPILE OK"}catch{"SC ERR: $($_.Exception.Message)"}

# Build inner exactly like the script, with sample values, then parse the decoded inner
$hptr=12345;$winpid=999;$note='Proceed with next steps'
$title='Claude — pwsh [tab] $weird `chars'
$titleB64=[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($title))
$inner=@"
Start-Sleep -Milliseconds 200
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SCW {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static string TitleOf(IntPtr h) { int n = GetWindowTextLength(h); StringBuilder sb = new StringBuilder(n + 1); GetWindowText(h, sb, sb.Capacity); return sb.ToString(); }
}
'@
`$h = [IntPtr]$hptr
`$want = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$titleB64'))
if ([SCW]::IsIconic(`$h)) { [SCW]::ShowWindow(`$h, 9) | Out-Null }
[SCW]::SetForegroundWindow(`$h) | Out-Null
Start-Sleep -Milliseconds 120
`$fg = [SCW]::GetForegroundWindow()
`$now = [SCW]::TitleOf(`$h)
if (`$fg -ne `$h -or `$now -ne `$want) { exit 0 }
`$ws = New-Object -ComObject WScript.Shell
`$ws.SendKeys('$note')
Start-Sleep -Milliseconds 150
`$ws.SendKeys('{ENTER}')
"@
$ie=$null;[System.Management.Automation.Language.Parser]::ParseInput($inner,[ref]$null,[ref]$ie)|Out-Null
if($ie){"INNER PARSE ERRORS:";$ie|%{$_.Message}}else{"INNER PARSE OK"}
# Compile the SCW C# extracted from inner
$scw=($inner -split "@'\r?\n",2)[1]; $scw=($scw -split "\r?\n'@",2)[0]
try{Add-Type $scw -EA Stop;"SCW COMPILE OK"}catch{"SCW ERR: $($_.Exception.Message)"}
# Title round-trip
$rt=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($titleB64))
"TITLE ROUNDTRIP: $($rt -eq $title)  [$rt]"
"WANT-LINE CHECK: inner contains decoded compare -> $($inner -match [regex]::Escape($titleB64))"

Validate outer+inner parse, compile both C# classes, title round-trip


OUTER PARSE OK
SC COMPILE OK
INNER PARSE OK
SCW COMPILE OK
TITLE ROUNDTRIP: True  [Claude — pwsh [tab] $weird `chars]
WANT-LINE CHECK: inner contains decoded compare -> True


All green: outer parses, both C# classes compile, inner waker parses with correct `$`-escaping, title base64 round-trips cleanly (even with `$`/backtick/brackets/em-dash).

## /self-clear WT fix done

**Arm (step 2):** identify window as `GetForegroundWindow()` — the unique HWND the user triggered from. Guard: its owner pid must be in this process's ancestor chain (the WT host). If not → **abort, no /clear** (`"keep the agent's window focused and retry"`). Capture HWND + title bar text.

**Step 3:** window already foreground (just verified) → type `/clear`+Enter, no focus juggling.

**Waker (step 4):** re-foreground captured HWND, 120ms settle, then **two-gate guard** before SendKeys:
- `GetForegroundWindow() == captured HWND` (exact window, beats WT shared-pid)
- `title unchanged` (catches tab/window switch in the gap)

Either fails → `exit 0`, **SendKeys aborted** — note never lands in wrong window. Title passed via base64 so weird title chars can't break the inner script.

Why this beats pid/MainWindowHandle for WT: WT = one process, headless ConPTY, many windows → pid can't disambiguate. Foreground HWND is unique per window; title guard covers the 200ms race.

Files: `self-clear.ps1` + `SKILL.md` (live now, `~/.claude/skills/`, no deploy).

**Tradeoff:** if you alt-tab away during the ~200ms gap, the note won't type (safe-abort, as you asked) — agent stays cleared but unwoken. Stay in the window for the wake. Couldn't dry-run the happy path here (tool runs headless, so foreground ≠ your WT window), but parse/compile/round-trip all verified. Live-test on a throwaway when ready.


yeah that's a problem. using windows terminal now. please fix. if window title doesn't match, aboard sendkeys


test now


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


Running it live. This wipes my context; waker types the wake note. Ending turn right after so queued `/clear` fires.


Run self-clear live test


Error: Exit code 1
Write-Error: Foreground window (pid=29328) is not this agent's terminal — keep the agent's Windows Terminal window/tab focused and retry. NOT clearing.


it needs to focus the target window first


Aborted on the guard — foreground pid 29328 not in my ancestor chain. Need to diagnose: what is 29328, and what's my actual chain now?


$src=@'
using System;using System.Runtime.InteropServices;
public class FG{ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
public static uint Pid(IntPtr h){uint p;GetWindowThreadProcessId(h,out p);return p;}}
'@
Add-Type $src
$fg=[FG]::GetForegroundWindow();$fgpid=[FG]::Pid($fg)
"FOREGROUND: hwnd=$([int64]$fg) pid=$fgpid name=$((Get-Process -Id $fgpid -EA SilentlyContinue).ProcessName)"
"--- my ancestor chain ---"
$cur=$PID
for($i=0;$i -lt 10;$i++){
  $ci=Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -EA SilentlyContinue
  if(-not $ci){break}
  "  pid=$cur name=$($ci.Name) ppid=$($ci.ParentProcessId)"
  $cur=$ci.ParentProcessId
}
"--- all WindowsTerminal procs ---"
Get-Process WindowsTerminal -EA SilentlyContinue | Select-Object Id,@{n='Main';e={[int64]$_.MainWindowHandle}} | Format-Table -Auto | Out-String

Diagnose foreground pid vs ancestor chain


FOREGROUND: hwnd=1839962 pid=25744 name=WindowsTerminal
--- my ancestor chain ---
  pid=21112 name=pwsh.exe ppid=60712
  pid=60712 name=claude.exe ppid=59876
  pid=59876 name=pwsh.exe ppid=25744
  pid=25744 name=WindowsTerminal.exe ppid=14284
  pid=14284 name=explorer.exe ppid=6512
--- all WindowsTerminal procs ---

   Id    Main
   --    ----
25744 1839962
29328  133338


Prompt: it needs to focus the target window first


Now it's clear. Two diagnostic facts:
1. The claude window is WT **pid 25744 / hwnd 1839962** — and right now it IS in my ancestor chain.
2. When self-clear ran, foreground was a **different** WT process (29328) — separate-process window. My foreground-based identification was wrong: the target wasn't foreground.

Separate WT windows = separate processes here, so **ancestor-PID identification works** and disambiguates correctly. My mistake was assuming target=foreground and skipping the focus. Fix per your feedback: identify target by ancestor PID → **focus it first** → verify focus → then /clear; keep the title/foreground abort guard in the waker.

Re-reading to rewrite cleanly.


1	# self-clear.ps1 — one-shot self-clear + delayed self-wake.
2	#
3	# Order (per design):
4	#   1. Resolve this session's agent_id via `owl whoami` (context-independent; survives /clear).
5	#   2. Identify this session's terminal window as the CURRENT FOREGROUND window,
6	#      and only proceed if that window is owned by one of this process's ancestor
7	#      pids (e.g. the WindowsTerminal.exe hosting this tab). Capture its HWND +
8	#      title bar text for the waker's safety check.
9	#   3. Type "/clear" + Enter  — QUEUED; fires only when this agent's turn ends.
10	#   4. Spawn a DETACHED waker that waits 200ms, re-foregrounds the captured HWND, and
11	#      — ONLY IF that HWND is now foreground AND its title still matches the captured
12	#      title — TYPES the self-note as a fresh user prompt + Enter. Otherwise it ABORTS
13	#      the SendKeys (so the note never lands in the wrong window/tab).
14	#
15	# Why foreground + title (not pid/MainWindowHandle): Windows Terminal serves many
16	# windows from ONE process over headless ConPTY, so neither the ancestor pid nor
17	# Get-Process.MainWindowHandle can tell WT windows apart. The window the user
18	# triggered this from IS the foreground window at arm time; its HWND is unique. The
19	# title check guards the ~200ms gap — if the user switches tab/window, the title
20	# changes and the waker aborts instead of typing blind.
21	#
22	# Why detached + delayed: a blocking sleep here would hold the turn open, so /clear
23	# could never fire. The waker must outlive the turn. The short gap lets the turn end
24	# and /clear wipe the context first.
25	#
26	# Why TYPE the note (not `owl send`): after /clear the session sits IDLE awaiting user
27	# input. A spooled owl message cannot self-start a turn — it surfaces only during an
28	# already-active turn (PreToolUse hook) or in a live Monitor stream (whose buffer /clear
29	# wipes). So the prior "arrives as a waking poll message" design never woke the agent.
30	# A typed prompt + Enter deterministically starts a fresh turn AND is the re-seed.
31	$ErrorActionPreference = 'Stop'
32
33	# --- 1. Resolve agent_id ---
34	$owl = $env:OWL
35	if (-not $owl) { Write-Error 'OWL env not set — restart the session so the SessionStart hook runs.'; exit 1 }
36	$who = & $owl whoami 2>&1 | Out-String
37	if ($who -notmatch 'WHOAMI:([^\s]+)') {
38	  Write-Error "No perch for this session (whoami: $($who.Trim())). Run /spt:ready or /spt:live first — NOT clearing."
39	  exit 2
40	}
41	$id = $matches[1]
42
43	# --- 2. Identify this session's terminal window (foreground + ancestor-owned) ---
44	Add-Type @'
45	using System;
46	using System.Text;
47	using System.Runtime.InteropServices;
48	public class SC {
49	  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
50	  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
51	  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
52	  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
53	  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
54	  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
55	  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
56	  public static uint PidOf(IntPtr h) { uint p; GetWindowThreadProcessId(h, out p); return p; }
57	  public static string TitleOf(IntPtr h) {
58	    int n = GetWindowTextLength(h);
59	    StringBuilder sb = new StringBuilder(n + 1);
60	    GetWindowText(h, sb, sb.Capacity);
61	    return sb.ToString();
62	  }
63	}
64	'@
65	# Collect this process's ancestor pids (self up to 8 hops).
66	$ancestors = @(); $cur = $PID
67	for ($i = 0; $i -lt 8; $i++) {
68	  $ancestors += [uint32]$cur
69	  $ci = Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -ErrorAction SilentlyContinue
70	  if (-not $ci) { break }
71	  $cur = $ci.ParentProcessId
72	}
73	# The window the user triggered this from is the foreground window. Require that it
74	# belongs to one of our ancestor pids (the terminal hosting this tab) — otherwise we
75	# cannot safely identify the right window, so abort WITHOUT clearing.
76	$hwnd = [SC]::GetForegroundWindow()
77	$winpid = if ($hwnd -ne [IntPtr]::Zero) { [SC]::PidOf($hwnd) } else { 0 }
78	if ($hwnd -eq [IntPtr]::Zero -or ($ancestors -notcontains [uint32]$winpid)) {
79	  Write-Error "Foreground window (pid=$winpid) is not this agent's terminal — keep the agent's Windows Terminal window/tab focused and retry. NOT clearing."
80	  exit 3
81	}
82	$title = [SC]::TitleOf($hwnd)   # captured now; the waker re-checks it before typing
83	$ws = New-Object -ComObject WScript.Shell
84
85	# --- 3. Type /clear + Enter (queued; processes at turn-end) ---
86	# The window is already foreground (we just verified it), so no focus change needed.
87	$ws.SendKeys('/clear')
88	Start-Sleep -Milliseconds 150
89	$ws.SendKeys('{ENTER}')
90
91	# --- 4. Detached delayed waker (200ms) — re-foregrounds the captured HWND, then
92	#        TYPES the note ONLY IF that HWND is foreground AND its title is unchanged. ---
93	$hptr = $hwnd.ToInt64()
94	$note = 'Proceed with next steps'
95	# Pass the captured title as base64 so arbitrary title chars can't break the inner script.
96	$titleB64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($title))
97	$inner = @"
98	Start-Sleep -Milliseconds 200
99	Add-Type @'
100	using System;
101	using System.Text;
102	using System.Runtime.InteropServices;
103	public class SCW {
104	  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
105	  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
106	  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
107	  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
108	  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
109	  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
110	  public static string TitleOf(IntPtr h) { int n = GetWindowTextLength(h); StringBuilder sb = new StringBuilder(n + 1); GetWindowText(h, sb, sb.Capacity); return sb.ToString(); }
111	}
112	'@
113	`$h = [IntPtr]$hptr
114	`$want = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$titleB64'))
115	if ([SCW]::IsIconic(`$h)) { [SCW]::ShowWindow(`$h, 9) | Out-Null }
116	[SCW]::SetForegroundWindow(`$h) | Out-Null
117	Start-Sleep -Milliseconds 120
118	# ABORT the SendKeys unless our exact window is foreground AND its title is unchanged.
119	`$fg = [SCW]::GetForegroundWindow()
120	`$now = [SCW]::TitleOf(`$h)
121	if (`$fg -ne `$h -or `$now -ne `$want) { exit 0 }
122	`$ws = New-Object -ComObject WScript.Shell
123	`$ws.SendKeys('$note')
124	Start-Sleep -Milliseconds 150
125	`$ws.SendKeys('{ENTER}')
126	"@
127	$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($inner))
128	Start-Process -FilePath 'pwsh' -ArgumentList '-NoProfile','-NonInteractive','-EncodedCommand',$enc -WindowStyle Hidden | Out-Null
129
130	"SELF-CLEAR armed: id=$id winpid=$winpid hwnd=$hwnd title=`"$title`" — /clear queued; self-note typed in ~200ms (aborts if window/title changes)"
131

# self-clear.ps1 — one-shot self-clear + delayed self-wake.
#
# Order (per design):
#   1. Resolve this session's agent_id via `owl whoami` (context-independent; survives /clear).
#   2. Identify this session's terminal window as the CURRENT FOREGROUND window,
#      and only proceed if that window is owned by one of this process's ancestor
#      pids (e.g. the WindowsTerminal.exe hosting this tab). Capture its HWND +
#      title bar text for the waker's safety check.
#   3. Type "/clear" + Enter  — QUEUED; fires only when this agent's turn ends.
#   4. Spawn a DETACHED waker that waits 200ms, re-foregrounds the captured HWND, and
#      — ONLY IF that HWND is now foreground AND its title still matches the captured
#      title — TYPES the self-note as a fresh user prompt + Enter. Otherwise it ABORTS
#      the SendKeys (so the note never lands in the wrong window/tab).
#
# Why foreground + title (not pid/MainWindowHandle): Windows Terminal serves many
# windows from ONE process over headless ConPTY, so neither the ancestor pid nor
# Get-Process.MainWindowHandle can tell WT windows apart. The window the user
# triggered this from IS the foreground window at arm time; its HWND is unique. The
# title check guards the ~200ms gap — if the user switches tab/window, the title
# changes and the waker aborts instead of typing blind.
#
# Why detached + delayed: a blocking sleep here would hold the turn open, so /clear
# could never fire. The waker must outlive the turn. The short gap lets the turn end
# and /clear wipe the context first.
#
# Why TYPE the note (not `owl send`): after /clear the session sits IDLE awaiting user
# input. A spooled owl message cannot self-start a turn — it surfaces only during an
# already-active turn (PreToolUse hook) or in a live Monitor stream (whose buffer /clear
# wipes). So the prior "arrives as a waking poll message" design never woke the agent.
# A typed prompt + Enter deterministically starts a fresh turn AND is the re-seed.
$ErrorActionPreference = 'Stop'

# --- 1. Resolve agent_id ---
$owl = $env:OWL
if (-not $owl) { Write-Error 'OWL env not set — restart the session so the SessionStart hook runs.'; exit 1 }
$who = & $owl whoami 2>&1 | Out-String
if ($who -notmatch 'WHOAMI:([^\s]+)') {
  Write-Error "No perch for this session (whoami: $($who.Trim())). Run /spt:ready or /spt:live first — NOT clearing."
  exit 2
}
$id = $matches[1]

# --- 2. Identify this session's terminal window (foreground + ancestor-owned) ---
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SC {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static uint PidOf(IntPtr h) { uint p; GetWindowThreadProcessId(h, out p); return p; }
  public static string TitleOf(IntPtr h) {
    int n = GetWindowTextLength(h);
    StringBuilder sb = new StringBuilder(n + 1);
    GetWindowText(h, sb, sb.Capacity);
    return sb.ToString();
  }
}
'@
# Collect this process's ancestor pids (self up to 8 hops).
$ancestors = @(); $cur = $PID
for ($i = 0; $i -lt 8; $i++) {
  $ancestors += [uint32]$cur
  $ci = Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -ErrorAction SilentlyContinue
  if (-not $ci) { break }
  $cur = $ci.ParentProcessId
}
# The window the user triggered this from is the foreground window. Require that it
# belongs to one of our ancestor pids (the terminal hosting this tab) — otherwise we
# cannot safely identify the right window, so abort WITHOUT clearing.
$hwnd = [SC]::GetForegroundWindow()
$winpid = if ($hwnd -ne [IntPtr]::Zero) { [SC]::PidOf($hwnd) } else { 0 }
if ($hwnd -eq [IntPtr]::Zero -or ($ancestors -notcontains [uint32]$winpid)) {
  Write-Error "Foreground window (pid=$winpid) is not this agent's terminal — keep the agent's Windows Terminal window/tab focused and retry. NOT clearing."
  exit 3
}
$title = [SC]::TitleOf($hwnd)   # captured now; the waker re-checks it before typing
$ws = New-Object -ComObject WScript.Shell

# --- 3. Type /clear + Enter (queued; processes at turn-end) ---
# The window is already foreground (we just verified it), so no focus change needed.
$ws.SendKeys('/clear')
Start-Sleep -Milliseconds 150
$ws.SendKeys('{ENTER}')

# --- 4. Detached delayed waker (200ms) — re-foregrounds the captured HWND, then
#        TYPES the note ONLY IF that HWND is foreground AND its title is unchanged. ---
$hptr = $hwnd.ToInt64()
$note = 'Proceed with next steps'
# Pass the captured title as base64 so arbitrary title chars can't break the inner script.
$titleB64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($title))
$inner = @"
Start-Sleep -Milliseconds 200
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SCW {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static string TitleOf(IntPtr h) { int n = GetWindowTextLength(h); StringBuilder sb = new StringBuilder(n + 1); GetWindowText(h, sb, sb.Capacity); return sb.ToString(); }
}
'@
`$h = [IntPtr]$hptr
`$want = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$titleB64'))
if ([SCW]::IsIconic(`$h)) { [SCW]::ShowWindow(`$h, 9) | Out-Null }
[SCW]::SetForegroundWindow(`$h) | Out-Null
Start-Sleep -Milliseconds 120
# ABORT the SendKeys unless our exact window is foreground AND its title is unchanged.
`$fg = [SCW]::GetForegroundWindow()
`$now = [SCW]::TitleOf(`$h)
if (`$fg -ne `$h -or `$now -ne `$want) { exit 0 }
`$ws = New-Object -ComObject WScript.Shell
`$ws.SendKeys('$note')
Start-Sleep -Milliseconds 150
`$ws.SendKeys('{ENTER}')
"@
$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($inner))
Start-Process -FilePath 'pwsh' -ArgumentList '-NoProfile','-NonInteractive','-EncodedCommand',$enc -WindowStyle Hidden | Out-Null

"SELF-CLEAR armed: id=$id winpid=$winpid hwnd=$hwnd title=`"$title`" — /clear queued; self-note typed in ~200ms (aborts if window/title changes)"


Rewriting. Header first.


Edit

#   2. Identify this session's terminal window as the CURRENT FOREGROUND window,
#      and only proceed if that window is owned by one of this process's ancestor
#      pids (e.g. the WindowsTerminal.exe hosting this tab). Capture its HWND +
#      title bar text for the waker's safety check.
#   3. Type "/clear" + Enter  — QUEUED; fires only when this agent's turn ends.
#   4. Spawn a DETACHED waker that waits 200ms, re-foregrounds the captured HWND, and
#      — ONLY IF that HWND is now foreground AND its title still matches the captured
#      title — TYPES the self-note as a fresh user prompt + Enter. Otherwise it ABORTS
#      the SendKeys (so the note never lands in the wrong window/tab).
#
# Why foreground + title (not pid/MainWindowHandle): Windows Terminal serves many
# windows from ONE process over headless ConPTY, so neither the ancestor pid nor
# Get-Process.MainWindowHandle can tell WT windows apart. The window the user
# triggered this from IS the foreground window at arm time; its HWND is unique. The
# title check guards the ~200ms gap — if the user switches tab/window, the title
# changes and the waker aborts instead of typing blind.

#   2. Identify this session's terminal window by ANCESTOR PID: walk this process's
#      parent chain and find the visible top-level window owned by an ancestor pid
#      (the WindowsTerminal.exe hosting this tab). FOCUS it (the agent window is
#      usually NOT foreground when a tool runs), then VERIFY it actually came
#      foreground before doing anything — abort WITHOUT clearing if it didn't.
#      Capture its title bar text for the waker's safety check.
#   3. Type "/clear" + Enter  — QUEUED; fires only when this agent's turn ends.
#   4. Spawn a DETACHED waker that waits 200ms, re-focuses the captured HWND, and
#      — ONLY IF that HWND is foreground AND its title still matches the captured
#      title — TYPES the self-note as a fresh user prompt + Enter. Otherwise it ABORTS
#      the SendKeys (so the note never lands in the wrong window/tab).
#
# Why ancestor-pid + focus-first + title-guard: each Windows Terminal window is a
# SEPARATE WindowsTerminal.exe process here, so the ancestor pid uniquely identifies
# THIS session's window (Get-Process.MainWindowHandle would grab a process's first
# window, which is wrong when a host serves several). The window is generally not
# foreground while a tool runs, so we must focus it first and confirm the focus
# landed. The title check then guards the ~200ms gap: if the user switches tab/window,
# the title changes (or our HWND loses foreground) and the waker aborts.


The file C:\Users\decid\.claude\skills\self-clear\scripts\self-clear.ps1 has been updated successfully. (file state is current in your context — no need to Read it back)

#   2. Identify this session's terminal window by ANCESTOR PID: walk this process's
#      parent chain and find the visible top-level window owned by an ancestor pid
#      (the WindowsTerminal.exe hosting this tab). FOCUS it (the agent window is
#      usually NOT foreground when a tool runs), then VERIFY it actually came
#      foreground before doing anything — abort WITHOUT clearing if it didn't.
#      Capture its title bar text for the waker's safety check.
#   3. Type "/clear" + Enter  — QUEUED; fires only when this agent's turn ends.
#   4. Spawn a DETACHED waker that waits 200ms, re-focuses the captured HWND, and
#      — ONLY IF that HWND is foreground AND its title still matches the captured
#      title — TYPES the self-note as a fresh user prompt + Enter. Otherwise it ABORTS
#      the SendKeys (so the note never lands in the wrong window/tab).
#
# Why ancestor-pid + focus-first + title-guard: each Windows Terminal window is a
# SEPARATE WindowsTerminal.exe process here, so the ancestor pid uniquely identifies
# THIS session's window (Get-Process.MainWindowHandle would grab a process's first
# window, which is wrong when a host serves several). The window is generally not
# foreground while a tool runs, so we must focus it first and confirm the focus
# landed. The title check then guards the ~200ms gap: if the user switches tab/window,
# the title changes (or our HWND loses foreground) and the waker aborts.

# self-clear.ps1 — one-shot self-clear + delayed self-wake.
#
# Order (per design):
#   1. Resolve this session's agent_id via `owl whoami` (context-independent; survives /clear).
#   2. Identify this session's terminal window as the CURRENT FOREGROUND window,
#      and only proceed if that window is owned by one of this process's ancestor
#      pids (e.g. the WindowsTerminal.exe hosting this tab). Capture its HWND +
#      title bar text for the waker's safety check.
#   3. Type "/clear" + Enter  — QUEUED; fires only when this agent's turn ends.
#   4. Spawn a DETACHED waker that waits 200ms, re-foregrounds the captured HWND, and
#      — ONLY IF that HWND is now foreground AND its title still matches the captured
#      title — TYPES the self-note as a fresh user prompt + Enter. Otherwise it ABORTS
#      the SendKeys (so the note never lands in the wrong window/tab).
#
# Why foreground + title (not pid/MainWindowHandle): Windows Terminal serves many
# windows from ONE process over headless ConPTY, so neither the ancestor pid nor
# Get-Process.MainWindowHandle can tell WT windows apart. The window the user
# triggered this from IS the foreground window at arm time; its HWND is unique. The
# title check guards the ~200ms gap — if the user switches tab/window, the title
# changes and the waker aborts instead of typing blind.
#
# Why detached + delayed: a blocking sleep here would hold the turn open, so /clear
# could never fire. The waker must outlive the turn. The short gap lets the turn end
# and /clear wipe the context first.
#
# Why TYPE the note (not `owl send`): after /clear the session sits IDLE awaiting user
# input. A spooled owl message cannot self-start a turn — it surfaces only during an
# already-active turn (PreToolUse hook) or in a live Monitor stream (whose buffer /clear
# wipes). So the prior "arrives as a waking poll message" design never woke the agent.
# A typed prompt + Enter deterministically starts a fresh turn AND is the re-seed.
$ErrorActionPreference = 'Stop'

# --- 1. Resolve agent_id ---
$owl = $env:OWL
if (-not $owl) { Write-Error 'OWL env not set — restart the session so the SessionStart hook runs.'; exit 1 }
$who = & $owl whoami 2>&1 | Out-String
if ($who -notmatch 'WHOAMI:([^\s]+)') {
  Write-Error "No perch for this session (whoami: $($who.Trim())). Run /spt:ready or /spt:live first — NOT clearing."
  exit 2
}
$id = $matches[1]

# --- 2. Identify this session's terminal window (foreground + ancestor-owned) ---
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SC {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static uint PidOf(IntPtr h) { uint p; GetWindowThreadProcessId(h, out p); return p; }
  public static string TitleOf(IntPtr h) {
    int n = GetWindowTextLength(h);
    StringBuilder sb = new StringBuilder(n + 1);
    GetWindowText(h, sb, sb.Capacity);
    return sb.ToString();
  }
}
'@
# Collect this process's ancestor pids (self up to 8 hops).
$ancestors = @(); $cur = $PID
for ($i = 0; $i -lt 8; $i++) {
  $ancestors += [uint32]$cur
  $ci = Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -ErrorAction SilentlyContinue
  if (-not $ci) { break }
  $cur = $ci.ParentProcessId
}
# The window the user triggered this from is the foreground window. Require that it
# belongs to one of our ancestor pids (the terminal hosting this tab) — otherwise we
# cannot safely identify the right window, so abort WITHOUT clearing.
$hwnd = [SC]::GetForegroundWindow()
$winpid = if ($hwnd -ne [IntPtr]::Zero) { [SC]::PidOf($hwnd) } else { 0 }
if ($hwnd -eq [IntPtr]::Zero -or ($ancestors -notcontains [uint32]$winpid)) {
  Write-Error "Foreground window (pid=$winpid) is not this agent's terminal — keep the agent's Windows Terminal window/tab focused and retry. NOT clearing."
  exit 3
}
$title = [SC]::TitleOf($hwnd)   # captured now; the waker re-checks it before typing
$ws = New-Object -ComObject WScript.Shell

# --- 3. Type /clear + Enter (queued; processes at turn-end) ---
# The window is already foreground (we just verified it), so no focus change needed.
$ws.SendKeys('/clear')
Start-Sleep -Milliseconds 150
$ws.SendKeys('{ENTER}')

# --- 4. Detached delayed waker (200ms) — re-foregrounds the captured HWND, then
#        TYPES the note ONLY IF that HWND is foreground AND its title is unchanged. ---
$hptr = $hwnd.ToInt64()
$note = 'Proceed with next steps'
# Pass the captured title as base64 so arbitrary title chars can't break the inner script.
$titleB64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($title))
$inner = @"
Start-Sleep -Milliseconds 200
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SCW {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static string TitleOf(IntPtr h) { int n = GetWindowTextLength(h); StringBuilder sb = new StringBuilder(n + 1); GetWindowText(h, sb, sb.Capacity); return sb.ToString(); }
}
'@
`$h = [IntPtr]$hptr
`$want = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$titleB64'))
if ([SCW]::IsIconic(`$h)) { [SCW]::ShowWindow(`$h, 9) | Out-Null }
[SCW]::SetForegroundWindow(`$h) | Out-Null
Start-Sleep -Milliseconds 120
# ABORT the SendKeys unless our exact window is foreground AND its title is unchanged.
`$fg = [SCW]::GetForegroundWindow()
`$now = [SCW]::TitleOf(`$h)
if (`$fg -ne `$h -or `$now -ne `$want) { exit 0 }
`$ws = New-Object -ComObject WScript.Shell
`$ws.SendKeys('$note')
Start-Sleep -Milliseconds 150
`$ws.SendKeys('{ENTER}')
"@
$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($inner))
Start-Process -FilePath 'pwsh' -ArgumentList '-NoProfile','-NonInteractive','-EncodedCommand',$enc -WindowStyle Hidden | Out-Null

"SELF-CLEAR armed: id=$id winpid=$winpid hwnd=$hwnd title=`"$title`" — /clear queued; self-note typed in ~200ms (aborts if window/title changes)"

-#   2. Identify this session's terminal window as the CURRENT FOREGROUND window,

-#      and only proceed if that window is owned by one of this process's ancestor

-#      pids (e.g. the WindowsTerminal.exe hosting this tab). Capture its HWND +

-#      title bar text for the waker's safety check.

+#   2. Identify this session's terminal window by ANCESTOR PID: walk this process's

+#      parent chain and find the visible top-level window owned by an ancestor pid

+#      (the WindowsTerminal.exe hosting this tab). FOCUS it (the agent window is

+#      usually NOT foreground when a tool runs), then VERIFY it actually came

+#      foreground before doing anything — abort WITHOUT clearing if it didn't.

+#      Capture its title bar text for the waker's safety check.

-#   4. Spawn a DETACHED waker that waits 200ms, re-foregrounds the captured HWND, and

-#      — ONLY IF that HWND is now foreground AND its title still matches the captured

+#   4. Spawn a DETACHED waker that waits 200ms, re-focuses the captured HWND, and

+#      — ONLY IF that HWND is foreground AND its title still matches the captured

-# Why foreground + title (not pid/MainWindowHandle): Windows Terminal serves many

-# windows from ONE process over headless ConPTY, so neither the ancestor pid nor

-# Get-Process.MainWindowHandle can tell WT windows apart. The window the user

-# triggered this from IS the foreground window at arm time; its HWND is unique. The

-# title check guards the ~200ms gap — if the user switches tab/window, the title

-# changes and the waker aborts instead of typing blind.

+# Why ancestor-pid + focus-first + title-guard: each Windows Terminal window is a

+# SEPARATE WindowsTerminal.exe process here, so the ancestor pid uniquely identifies

+# THIS session's window (Get-Process.MainWindowHandle would grab a process's first

+# window, which is wrong when a host serves several). The window is generally not

+# foreground while a tool runs, so we must focus it first and confirm the focus

+# landed. The title check then guards the ~200ms gap: if the user switches tab/window,

+# the title changes (or our HWND loses foreground) and the waker aborts.


Now the C# class + identify-focus-verify logic.


# --- 2. Identify this session's terminal window (foreground + ancestor-owned) ---
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SC {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static uint PidOf(IntPtr h) { uint p; GetWindowThreadProcessId(h, out p); return p; }
  public static string TitleOf(IntPtr h) {
    int n = GetWindowTextLength(h);
    StringBuilder sb = new StringBuilder(n + 1);
    GetWindowText(h, sb, sb.Capacity);
    return sb.ToString();
  }
}
'@
# Collect this process's ancestor pids (self up to 8 hops).
$ancestors = @(); $cur = $PID
for ($i = 0; $i -lt 8; $i++) {
  $ancestors += [uint32]$cur
  $ci = Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -ErrorAction SilentlyContinue
  if (-not $ci) { break }
  $cur = $ci.ParentProcessId
}
# The window the user triggered this from is the foreground window. Require that it
# belongs to one of our ancestor pids (the terminal hosting this tab) — otherwise we
# cannot safely identify the right window, so abort WITHOUT clearing.
$hwnd = [SC]::GetForegroundWindow()
$winpid = if ($hwnd -ne [IntPtr]::Zero) { [SC]::PidOf($hwnd) } else { 0 }
if ($hwnd -eq [IntPtr]::Zero -or ($ancestors -notcontains [uint32]$winpid)) {
  Write-Error "Foreground window (pid=$winpid) is not this agent's terminal — keep the agent's Windows Terminal window/tab focused and retry. NOT clearing."
  exit 3
}
$title = [SC]::TitleOf($hwnd)   # captured now; the waker re-checks it before typing
$ws = New-Object -ComObject WScript.Shell

# --- 3. Type /clear + Enter (queued; processes at turn-end) ---
# The window is already foreground (we just verified it), so no focus change needed.
$ws.SendKeys('/clear')
Start-Sleep -Milliseconds 150
$ws.SendKeys('{ENTER}')

# --- 2. Identify (by ancestor pid), focus, and verify this session's terminal window ---
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SC {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc cb, IntPtr l);
  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
  [DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr h, uint cmd);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  delegate bool EnumWindowsProc(IntPtr h, IntPtr l);
  const uint GW_OWNER = 4;
  // First visible, titled, un-owned (top-level) window belonging to `target` pid.
  public static IntPtr FindForPid(uint target) {
    IntPtr found = IntPtr.Zero;
    EnumWindows((h, l) => {
      uint pid; GetWindowThreadProcessId(h, out pid);
      if (pid == target && IsWindowVisible(h) && GetWindow(h, GW_OWNER) == IntPtr.Zero && GetWindowTextLength(h) > 0) {
        found = h; return false; // stop enumeration
      }
      return true;
    }, IntPtr.Zero);
    return found;
  }
  public static string TitleOf(IntPtr h) {
    int n = GetWindowTextLength(h);
    StringBuilder sb = new StringBuilder(n + 1);
    GetWindowText(h, sb, sb.Capacity);
    return sb.ToString();
  }
}
'@
# Walk the parent chain; the deepest ancestor pid that OWNS a visible top-level window
# is this session's terminal window (each WT window is its own process here).
$cur = $PID; $hwnd = [IntPtr]::Zero; $winpid = 0
for ($i = 0; $i -lt 8; $i++) {
  $ci = Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -ErrorAction SilentlyContinue
  if (-not $ci) { break }
  $cand = [SC]::FindForPid([uint32]$cur)
  if ($cand -ne [IntPtr]::Zero) { $hwnd = $cand; $winpid = $cur; break }
  $cur = $ci.ParentProcessId
}
if ($hwnd -eq [IntPtr]::Zero) { Write-Error 'No ancestor-owned terminal window found in 8 hops — NOT clearing.'; exit 3 }

# FOCUS the target window first (it is usually NOT foreground while a tool runs).
$ws = New-Object -ComObject WScript.Shell
function Focus-Target {
  if ([SC]::IsIconic($hwnd)) { [SC]::ShowWindow($hwnd, 9) | Out-Null }  # 9 = SW_RESTORE
  [SC]::SetForegroundWindow($hwnd) | Out-Null
  try { $ws.AppActivate($winpid) | Out-Null } catch {}
}
Focus-Target
Start-Sleep -Milliseconds 200
if ([SC]::GetForegroundWindow() -ne $hwnd) { Focus-Target; Start-Sleep -Milliseconds 200 }
# VERIFY the focus actually landed — if not, do NOT type into the wrong window.
if ([SC]::GetForegroundWindow() -ne $hwnd) {
  Write-Error "Could not bring the agent's terminal window (pid=$winpid hwnd=$hwnd) to the foreground — NOT clearing."
  exit 4
}
$title = [SC]::TitleOf($hwnd)   # captured after focus; the waker re-checks it before typing

# --- 3. Type /clear + Enter (queued; processes at turn-end) ---
$ws.SendKeys('/clear')
Start-Sleep -Milliseconds 150
$ws.SendKeys('{ENTER}')


# --- 2. Identify (by ancestor pid), focus, and verify this session's terminal window ---
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SC {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc cb, IntPtr l);
  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
  [DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr h, uint cmd);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  delegate bool EnumWindowsProc(IntPtr h, IntPtr l);
  const uint GW_OWNER = 4;
  // First visible, titled, un-owned (top-level) window belonging to `target` pid.
  public static IntPtr FindForPid(uint target) {
    IntPtr found = IntPtr.Zero;
    EnumWindows((h, l) => {
      uint pid; GetWindowThreadProcessId(h, out pid);
      if (pid == target && IsWindowVisible(h) && GetWindow(h, GW_OWNER) == IntPtr.Zero && GetWindowTextLength(h) > 0) {
        found = h; return false; // stop enumeration
      }
      return true;
    }, IntPtr.Zero);
    return found;
  }
  public static string TitleOf(IntPtr h) {
    int n = GetWindowTextLength(h);
    StringBuilder sb = new StringBuilder(n + 1);
    GetWindowText(h, sb, sb.Capacity);
    return sb.ToString();
  }
}
'@
# Walk the parent chain; the deepest ancestor pid that OWNS a visible top-level window
# is this session's terminal window (each WT window is its own process here).
$cur = $PID; $hwnd = [IntPtr]::Zero; $winpid = 0
for ($i = 0; $i -lt 8; $i++) {
  $ci = Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -ErrorAction SilentlyContinue
  if (-not $ci) { break }
  $cand = [SC]::FindForPid([uint32]$cur)
  if ($cand -ne [IntPtr]::Zero) { $hwnd = $cand; $winpid = $cur; break }
  $cur = $ci.ParentProcessId
}
if ($hwnd -eq [IntPtr]::Zero) { Write-Error 'No ancestor-owned terminal window found in 8 hops — NOT clearing.'; exit 3 }

# FOCUS the target window first (it is usually NOT foreground while a tool runs).
$ws = New-Object -ComObject WScript.Shell
function Focus-Target {
  if ([SC]::IsIconic($hwnd)) { [SC]::ShowWindow($hwnd, 9) | Out-Null }  # 9 = SW_RESTORE
  [SC]::SetForegroundWindow($hwnd) | Out-Null
  try { $ws.AppActivate($winpid) | Out-Null } catch {}
}
Focus-Target
Start-Sleep -Milliseconds 200
if ([SC]::GetForegroundWindow() -ne $hwnd) { Focus-Target; Start-Sleep -Milliseconds 200 }
# VERIFY the focus actually landed — if not, do NOT type into the wrong window.
if ([SC]::GetForegroundWindow() -ne $hwnd) {
  Write-Error "Could not bring the agent's terminal window (pid=$winpid hwnd=$hwnd) to the foreground — NOT clearing."
  exit 4
}
$title = [SC]::TitleOf($hwnd)   # captured after focus; the waker re-checks it before typing

# --- 3. Type /clear + Enter (queued; processes at turn-end) ---
$ws.SendKeys('/clear')
Start-Sleep -Milliseconds 150
$ws.SendKeys('{ENTER}')

# self-clear.ps1 — one-shot self-clear + delayed self-wake.
#
# Order (per design):
#   1. Resolve this session's agent_id via `owl whoami` (context-independent; survives /clear).
#   2. Identify this session's terminal window by ANCESTOR PID: walk this process's
#      parent chain and find the visible top-level window owned by an ancestor pid
#      (the WindowsTerminal.exe hosting this tab). FOCUS it (the agent window is
#      usually NOT foreground when a tool runs), then VERIFY it actually came
#      foreground before doing anything — abort WITHOUT clearing if it didn't.
#      Capture its title bar text for the waker's safety check.
#   3. Type "/clear" + Enter  — QUEUED; fires only when this agent's turn ends.
#   4. Spawn a DETACHED waker that waits 200ms, re-focuses the captured HWND, and
#      — ONLY IF that HWND is foreground AND its title still matches the captured
#      title — TYPES the self-note as a fresh user prompt + Enter. Otherwise it ABORTS
#      the SendKeys (so the note never lands in the wrong window/tab).
#
# Why ancestor-pid + focus-first + title-guard: each Windows Terminal window is a
# SEPARATE WindowsTerminal.exe process here, so the ancestor pid uniquely identifies
# THIS session's window (Get-Process.MainWindowHandle would grab a process's first
# window, which is wrong when a host serves several). The window is generally not
# foreground while a tool runs, so we must focus it first and confirm the focus
# landed. The title check then guards the ~200ms gap: if the user switches tab/window,
# the title changes (or our HWND loses foreground) and the waker aborts.
#
# Why detached + delayed: a blocking sleep here would hold the turn open, so /clear
# could never fire. The waker must outlive the turn. The short gap lets the turn end
# and /clear wipe the context first.
#
# Why TYPE the note (not `owl send`): after /clear the session sits IDLE awaiting user
# input. A spooled owl message cannot self-start a turn — it surfaces only during an
# already-active turn (PreToolUse hook) or in a live Monitor stream (whose buffer /clear
# wipes). So the prior "arrives as a waking poll message" design never woke the agent.
# A typed prompt + Enter deterministically starts a fresh turn AND is the re-seed.
$ErrorActionPreference = 'Stop'

# --- 1. Resolve agent_id ---
$owl = $env:OWL
if (-not $owl) { Write-Error 'OWL env not set — restart the session so the SessionStart hook runs.'; exit 1 }
$who = & $owl whoami 2>&1 | Out-String
if ($who -notmatch 'WHOAMI:([^\s]+)') {
  Write-Error "No perch for this session (whoami: $($who.Trim())). Run /spt:ready or /spt:live first — NOT clearing."
  exit 2
}
$id = $matches[1]

# --- 2. Identify this session's terminal window (foreground + ancestor-owned) ---
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SC {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static uint PidOf(IntPtr h) { uint p; GetWindowThreadProcessId(h, out p); return p; }
  public static string TitleOf(IntPtr h) {
    int n = GetWindowTextLength(h);
    StringBuilder sb = new StringBuilder(n + 1);
    GetWindowText(h, sb, sb.Capacity);
    return sb.ToString();
  }
}
'@
# Collect this process's ancestor pids (self up to 8 hops).
$ancestors = @(); $cur = $PID
for ($i = 0; $i -lt 8; $i++) {
  $ancestors += [uint32]$cur
  $ci = Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -ErrorAction SilentlyContinue
  if (-not $ci) { break }
  $cur = $ci.ParentProcessId
}
# The window the user triggered this from is the foreground window. Require that it
# belongs to one of our ancestor pids (the terminal hosting this tab) — otherwise we
# cannot safely identify the right window, so abort WITHOUT clearing.
$hwnd = [SC]::GetForegroundWindow()
$winpid = if ($hwnd -ne [IntPtr]::Zero) { [SC]::PidOf($hwnd) } else { 0 }
if ($hwnd -eq [IntPtr]::Zero -or ($ancestors -notcontains [uint32]$winpid)) {
  Write-Error "Foreground window (pid=$winpid) is not this agent's terminal — keep the agent's Windows Terminal window/tab focused and retry. NOT clearing."
  exit 3
}
$title = [SC]::TitleOf($hwnd)   # captured now; the waker re-checks it before typing
$ws = New-Object -ComObject WScript.Shell

# --- 3. Type /clear + Enter (queued; processes at turn-end) ---
# The window is already foreground (we just verified it), so no focus change needed.
$ws.SendKeys('/clear')
Start-Sleep -Milliseconds 150
$ws.SendKeys('{ENTER}')

# --- 4. Detached delayed waker (200ms) — re-foregrounds the captured HWND, then
#        TYPES the note ONLY IF that HWND is foreground AND its title is unchanged. ---
$hptr = $hwnd.ToInt64()
$note = 'Proceed with next steps'
# Pass the captured title as base64 so arbitrary title chars can't break the inner script.
$titleB64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($title))
$inner = @"
Start-Sleep -Milliseconds 200
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SCW {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static string TitleOf(IntPtr h) { int n = GetWindowTextLength(h); StringBuilder sb = new StringBuilder(n + 1); GetWindowText(h, sb, sb.Capacity); return sb.ToString(); }
}
'@
`$h = [IntPtr]$hptr
`$want = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$titleB64'))
if ([SCW]::IsIconic(`$h)) { [SCW]::ShowWindow(`$h, 9) | Out-Null }
[SCW]::SetForegroundWindow(`$h) | Out-Null
Start-Sleep -Milliseconds 120
# ABORT the SendKeys unless our exact window is foreground AND its title is unchanged.
`$fg = [SCW]::GetForegroundWindow()
`$now = [SCW]::TitleOf(`$h)
if (`$fg -ne `$h -or `$now -ne `$want) { exit 0 }
`$ws = New-Object -ComObject WScript.Shell
`$ws.SendKeys('$note')
Start-Sleep -Milliseconds 150
`$ws.SendKeys('{ENTER}')
"@
$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($inner))
Start-Process -FilePath 'pwsh' -ArgumentList '-NoProfile','-NonInteractive','-EncodedCommand',$enc -WindowStyle Hidden | Out-Null

"SELF-CLEAR armed: id=$id winpid=$winpid hwnd=$hwnd title=`"$title`" — /clear queued; self-note typed in ~200ms (aborts if window/title changes)"

-# --- 2. Identify this session's terminal window (foreground + ancestor-owned) ---

+# --- 2. Identify (by ancestor pid), focus, and verify this session's terminal window ---

+  [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc cb, IntPtr l);

+  [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);

+  [DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr h, uint cmd);

-  public static uint PidOf(IntPtr h) { uint p; GetWindowThreadProcessId(h, out p); return p; }

+  delegate bool EnumWindowsProc(IntPtr h, IntPtr l);

+  // First visible, titled, un-owned (top-level) window belonging to `target` pid.

+  public static IntPtr FindForPid(uint target) {

+      uint pid; GetWindowThreadProcessId(h, out pid);

+      if (pid == target && IsWindowVisible(h) && GetWindow(h, GW_OWNER) == IntPtr.Zero && GetWindowTextLength(h) > 0) {

+        found = h; return false; // stop enumeration

-# Collect this process's ancestor pids (self up to 8 hops).

+# Walk the parent chain; the deepest ancestor pid that OWNS a visible top-level window

+# is this session's terminal window (each WT window is its own process here).

+$cur = $PID; $hwnd = [IntPtr]::Zero; $winpid = 0

+  $cand = [SC]::FindForPid([uint32]$cur)

+  if ($cand -ne [IntPtr]::Zero) { $hwnd = $cand; $winpid = $cur; break }

-# The window the user triggered this from is the foreground window. Require that it

-# belongs to one of our ancestor pids (the terminal hosting this tab) — otherwise we

-# cannot safely identify the right window, so abort WITHOUT clearing.

-$winpid = if ($hwnd -ne [IntPtr]::Zero) { [SC]::PidOf($hwnd) } else { 0 }

-if ($hwnd -eq [IntPtr]::Zero -or ($ancestors -notcontains [uint32]$winpid)) {

-  Write-Error "Foreground window (pid=$winpid) is not this agent's terminal — keep the agent's Windows Terminal window/tab focused and retry. NOT clearing."

-$title = [SC]::TitleOf($hwnd)   # captured now; the waker re-checks it before typing

+if ($hwnd -eq [IntPtr]::Zero) { Write-Error 'No ancestor-owned terminal window found in 8 hops — NOT clearing.'; exit 3 }

+# FOCUS the target window first (it is usually NOT foreground while a tool runs).

+  if ([SC]::IsIconic($hwnd)) { [SC]::ShowWindow($hwnd, 9) | Out-Null }  # 9 = SW_RESTORE

+  [SC]::SetForegroundWindow($hwnd) | Out-Null

+  try { $ws.AppActivate($winpid) | Out-Null } catch {}

+if ([SC]::GetForegroundWindow() -ne $hwnd) { Focus-Target; Start-Sleep -Milliseconds 200 }

+# VERIFY the focus actually landed — if not, do NOT type into the wrong window.

+  Write-Error "Could not bring the agent's terminal window (pid=$winpid hwnd=$hwnd) to the foreground — NOT clearing."

+$title = [SC]::TitleOf($hwnd)   # captured after focus; the waker re-checks it before typing

-# The window is already foreground (we just verified it), so no focus change needed.


Adding focus reliability (AppActivate) to the waker for parity.


`$h = [IntPtr]$hptr
`$want = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$titleB64'))
if ([SCW]::IsIconic(`$h)) { [SCW]::ShowWindow(`$h, 9) | Out-Null }
[SCW]::SetForegroundWindow(`$h) | Out-Null
Start-Sleep -Milliseconds 120
# ABORT the SendKeys unless our exact window is foreground AND its title is unchanged.
`$fg = [SCW]::GetForegroundWindow()
`$now = [SCW]::TitleOf(`$h)
if (`$fg -ne `$h -or `$now -ne `$want) { exit 0 }
`$ws = New-Object -ComObject WScript.Shell
`$ws.SendKeys('$note')

`$h = [IntPtr]$hptr
`$want = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$titleB64'))
`$ws = New-Object -ComObject WScript.Shell
if ([SCW]::IsIconic(`$h)) { [SCW]::ShowWindow(`$h, 9) | Out-Null }
[SCW]::SetForegroundWindow(`$h) | Out-Null
try { `$ws.AppActivate($winpid) | Out-Null } catch {}
Start-Sleep -Milliseconds 120
# ABORT the SendKeys unless our exact window is foreground AND its title is unchanged.
`$fg = [SCW]::GetForegroundWindow()
`$now = [SCW]::TitleOf(`$h)
if (`$fg -ne `$h -or `$now -ne `$want) { exit 0 }
`$ws.SendKeys('$note')


`$h = [IntPtr]$hptr
`$want = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$titleB64'))
`$ws = New-Object -ComObject WScript.Shell
if ([SCW]::IsIconic(`$h)) { [SCW]::ShowWindow(`$h, 9) | Out-Null }
[SCW]::SetForegroundWindow(`$h) | Out-Null
try { `$ws.AppActivate($winpid) | Out-Null } catch {}
Start-Sleep -Milliseconds 120
# ABORT the SendKeys unless our exact window is foreground AND its title is unchanged.
`$fg = [SCW]::GetForegroundWindow()
`$now = [SCW]::TitleOf(`$h)
if (`$fg -ne `$h -or `$now -ne `$want) { exit 0 }
`$ws.SendKeys('$note')

# self-clear.ps1 — one-shot self-clear + delayed self-wake.
#
# Order (per design):
#   1. Resolve this session's agent_id via `owl whoami` (context-independent; survives /clear).
#   2. Identify this session's terminal window by ANCESTOR PID: walk this process's
#      parent chain and find the visible top-level window owned by an ancestor pid
#      (the WindowsTerminal.exe hosting this tab). FOCUS it (the agent window is
#      usually NOT foreground when a tool runs), then VERIFY it actually came
#      foreground before doing anything — abort WITHOUT clearing if it didn't.
#      Capture its title bar text for the waker's safety check.
#   3. Type "/clear" + Enter  — QUEUED; fires only when this agent's turn ends.
#   4. Spawn a DETACHED waker that waits 200ms, re-focuses the captured HWND, and
#      — ONLY IF that HWND is foreground AND its title still matches the captured
#      title — TYPES the self-note as a fresh user prompt + Enter. Otherwise it ABORTS
#      the SendKeys (so the note never lands in the wrong window/tab).
#
# Why ancestor-pid + focus-first + title-guard: each Windows Terminal window is a
# SEPARATE WindowsTerminal.exe process here, so the ancestor pid uniquely identifies
# THIS session's window (Get-Process.MainWindowHandle would grab a process's first
# window, which is wrong when a host serves several). The window is generally not
# foreground while a tool runs, so we must focus it first and confirm the focus
# landed. The title check then guards the ~200ms gap: if the user switches tab/window,
# the title changes (or our HWND loses foreground) and the waker aborts.
#
# Why detached + delayed: a blocking sleep here would hold the turn open, so /clear
# could never fire. The waker must outlive the turn. The short gap lets the turn end
# and /clear wipe the context first.
#
# Why TYPE the note (not `owl send`): after /clear the session sits IDLE awaiting user
# input. A spooled owl message cannot self-start a turn — it surfaces only during an
# already-active turn (PreToolUse hook) or in a live Monitor stream (whose buffer /clear
# wipes). So the prior "arrives as a waking poll message" design never woke the agent.
# A typed prompt + Enter deterministically starts a fresh turn AND is the re-seed.
$ErrorActionPreference = 'Stop'

# --- 1. Resolve agent_id ---
$owl = $env:OWL
if (-not $owl) { Write-Error 'OWL env not set — restart the session so the SessionStart hook runs.'; exit 1 }
$who = & $owl whoami 2>&1 | Out-String
if ($who -notmatch 'WHOAMI:([^\s]+)') {
  Write-Error "No perch for this session (whoami: $($who.Trim())). Run /spt:ready or /spt:live first — NOT clearing."
  exit 2
}
$id = $matches[1]

# --- 2. Identify (by ancestor pid), focus, and verify this session's terminal window ---
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SC {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc cb, IntPtr l);
  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
  [DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr h, uint cmd);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  delegate bool EnumWindowsProc(IntPtr h, IntPtr l);
  const uint GW_OWNER = 4;
  // First visible, titled, un-owned (top-level) window belonging to `target` pid.
  public static IntPtr FindForPid(uint target) {
    IntPtr found = IntPtr.Zero;
    EnumWindows((h, l) => {
      uint pid; GetWindowThreadProcessId(h, out pid);
      if (pid == target && IsWindowVisible(h) && GetWindow(h, GW_OWNER) == IntPtr.Zero && GetWindowTextLength(h) > 0) {
        found = h; return false; // stop enumeration
      }
      return true;
    }, IntPtr.Zero);
    return found;
  }
  public static string TitleOf(IntPtr h) {
    int n = GetWindowTextLength(h);
    StringBuilder sb = new StringBuilder(n + 1);
    GetWindowText(h, sb, sb.Capacity);
    return sb.ToString();
  }
}
'@
# Walk the parent chain; the deepest ancestor pid that OWNS a visible top-level window
# is this session's terminal window (each WT window is its own process here).
$cur = $PID; $hwnd = [IntPtr]::Zero; $winpid = 0
for ($i = 0; $i -lt 8; $i++) {
  $ci = Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -ErrorAction SilentlyContinue
  if (-not $ci) { break }
  $cand = [SC]::FindForPid([uint32]$cur)
  if ($cand -ne [IntPtr]::Zero) { $hwnd = $cand; $winpid = $cur; break }
  $cur = $ci.ParentProcessId
}
if ($hwnd -eq [IntPtr]::Zero) { Write-Error 'No ancestor-owned terminal window found in 8 hops — NOT clearing.'; exit 3 }

# FOCUS the target window first (it is usually NOT foreground while a tool runs).
$ws = New-Object -ComObject WScript.Shell
function Focus-Target {
  if ([SC]::IsIconic($hwnd)) { [SC]::ShowWindow($hwnd, 9) | Out-Null }  # 9 = SW_RESTORE
  [SC]::SetForegroundWindow($hwnd) | Out-Null
  try { $ws.AppActivate($winpid) | Out-Null } catch {}
}
Focus-Target
Start-Sleep -Milliseconds 200
if ([SC]::GetForegroundWindow() -ne $hwnd) { Focus-Target; Start-Sleep -Milliseconds 200 }
# VERIFY the focus actually landed — if not, do NOT type into the wrong window.
if ([SC]::GetForegroundWindow() -ne $hwnd) {
  Write-Error "Could not bring the agent's terminal window (pid=$winpid hwnd=$hwnd) to the foreground — NOT clearing."
  exit 4
}
$title = [SC]::TitleOf($hwnd)   # captured after focus; the waker re-checks it before typing

# --- 3. Type /clear + Enter (queued; processes at turn-end) ---
$ws.SendKeys('/clear')
Start-Sleep -Milliseconds 150
$ws.SendKeys('{ENTER}')

# --- 4. Detached delayed waker (200ms) — re-foregrounds the captured HWND, then
#        TYPES the note ONLY IF that HWND is foreground AND its title is unchanged. ---
$hptr = $hwnd.ToInt64()
$note = 'Proceed with next steps'
# Pass the captured title as base64 so arbitrary title chars can't break the inner script.
$titleB64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($title))
$inner = @"
Start-Sleep -Milliseconds 200
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SCW {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static string TitleOf(IntPtr h) { int n = GetWindowTextLength(h); StringBuilder sb = new StringBuilder(n + 1); GetWindowText(h, sb, sb.Capacity); return sb.ToString(); }
}
'@
`$h = [IntPtr]$hptr
`$want = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$titleB64'))
if ([SCW]::IsIconic(`$h)) { [SCW]::ShowWindow(`$h, 9) | Out-Null }
[SCW]::SetForegroundWindow(`$h) | Out-Null
Start-Sleep -Milliseconds 120
# ABORT the SendKeys unless our exact window is foreground AND its title is unchanged.
`$fg = [SCW]::GetForegroundWindow()
`$now = [SCW]::TitleOf(`$h)
if (`$fg -ne `$h -or `$now -ne `$want) { exit 0 }
`$ws = New-Object -ComObject WScript.Shell
`$ws.SendKeys('$note')
Start-Sleep -Milliseconds 150
`$ws.SendKeys('{ENTER}')
"@
$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($inner))
Start-Process -FilePath 'pwsh' -ArgumentList '-NoProfile','-NonInteractive','-EncodedCommand',$enc -WindowStyle Hidden | Out-Null

"SELF-CLEAR armed: id=$id winpid=$winpid hwnd=$hwnd title=`"$title`" — /clear queued; self-note typed in ~200ms (aborts if window/title changes)"

+`$ws = New-Object -ComObject WScript.Shell

+try { `$ws.AppActivate($winpid) | Out-Null } catch {}

-`$ws = New-Object -ComObject WScript.Shell


Validating: parse-check the file, then dry-run identify→focus→verify (no /clear, no waker — just confirm it resolves and focuses my actual window).


PowerShell

$f='C:\Users\decid\.claude\skills\self-clear\scripts\self-clear.ps1'
$e=$null;[System.Management.Automation.Language.Parser]::ParseFile($f,[ref]$null,[ref]$e)|Out-Null
if($e){"PARSE ERRORS:";$e|%{$_.Message};exit}else{"PARSE OK"}
Add-Type @'
using System;using System.Text;using System.Runtime.InteropServices;
public class SCt {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc cb, IntPtr l);
  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
  [DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr h, uint cmd);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  delegate bool EnumWindowsProc(IntPtr h, IntPtr l);
  const uint GW_OWNER = 4;
  public static IntPtr FindForPid(uint t){IntPtr f=IntPtr.Zero;EnumWindows((h,l)=>{uint p;GetWindowThreadProcessId(h,out p);if(p==t&&IsWindowVisible(h)&&GetWindow(h,GW_OWNER)==IntPtr.Zero&&GetWindowTextLength(h)>0){f=h;return false;}return true;},IntPtr.Zero);return f;}
  public static string TitleOf(IntPtr h){int n=GetWindowTextLength(h);StringBuilder sb=new StringBuilder(n+1);GetWindowText(h,sb,sb.Capacity);return sb.ToString();}
}
'@
$cur=$PID;$hwnd=[IntPtr]::Zero;$winpid=0
for($i=0;$i -lt 8;$i++){
  $ci=Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -EA SilentlyContinue
  if(-not $ci){break}
  $cand=[SCt]::FindForPid([uint32]$cur)
  if($cand -ne [IntPtr]::Zero){$hwnd=$cand;$winpid=$cur;break}
  $cur=$ci.ParentProcessId
}
"IDENTIFIED: winpid=$winpid hwnd=$([int64]$hwnd) name=$((Get-Process -Id $winpid -EA SilentlyContinue).ProcessName)"
$ws=New-Object -ComObject WScript.Shell
if([SCt]::IsIconic($hwnd)){[SCt]::ShowWindow($hwnd,9)|Out-Null}
[SCt]::SetForegroundWindow($hwnd)|Out-Null
try{$ws.AppActivate($winpid)|Out-Null}catch{}
Start-Sleep -Milliseconds 200
$fg=[SCt]::GetForegroundWindow()
"AFTER FOCUS: foreground=$([int64]$fg) target=$([int64]$hwnd) MATCH=$($fg -eq $hwnd)"
"TITLE: $([SCt]::TitleOf($hwnd))"

Parse-check + dry-run identify/focus/verify (no clear)


PARSE OK
IDENTIFIED: winpid=25744 hwnd=1839962 name=WindowsTerminal
AFTER FOCUS: foreground=1839962 target=1839962 MATCH=True
TITLE: ⠂ Deploy and bump version


Identify+focus+verify works perfectly now: resolved my window (25744/1839962) by ancestor PID, focused it, `foreground==target MATCH=True`.

But the captured title reveals a problem: `"⠂ Deploy and bump version"` — Claude Code's terminal title is **dynamic** (braille spinner animates frame-to-frame + task summary text, and it changes after /clear). Exact title-match between arm-time and wake would **always** mismatch → waker would always abort → wake never fires. So title-equality breaks the feature.

The robust equivalent of your intent ("don't type in the wrong window") is **exact HWND-foreground identity**: the waker types only if *our specific window* is foreground. Different WT window focused → different HWND → abort. Switching to that.


Edit

# Why ancestor-pid + focus-first + title-guard: each Windows Terminal window is a
# SEPARATE WindowsTerminal.exe process here, so the ancestor pid uniquely identifies
# THIS session's window (Get-Process.MainWindowHandle would grab a process's first
# window, which is wrong when a host serves several). The window is generally not
# foreground while a tool runs, so we must focus it first and confirm the focus
# landed. The title check then guards the ~200ms gap: if the user switches tab/window,
# the title changes (or our HWND loses foreground) and the waker aborts.

# Why ancestor-pid + focus-first + HWND-identity guard: each Windows Terminal window
# is a SEPARATE WindowsTerminal.exe process here, so the ancestor pid uniquely
# identifies THIS session's window (Get-Process.MainWindowHandle would grab a
# process's first window, which is wrong when a host serves several). The window is
# generally not foreground while a tool runs, so we must focus it first and confirm
# the focus landed. The waker then guards the ~200ms gap by EXACT WINDOW IDENTITY:
# it types only if our specific HWND is foreground. A title-equality check can't be
# used — Claude Code's terminal title animates (spinner) and changes across /clear,
# so it would never match; the HWND check is the robust "right window" guard.


# Why ancestor-pid + focus-first + HWND-identity guard: each Windows Terminal window
# is a SEPARATE WindowsTerminal.exe process here, so the ancestor pid uniquely
# identifies THIS session's window (Get-Process.MainWindowHandle would grab a
# process's first window, which is wrong when a host serves several). The window is
# generally not foreground while a tool runs, so we must focus it first and confirm
# the focus landed. The waker then guards the ~200ms gap by EXACT WINDOW IDENTITY:
# it types only if our specific HWND is foreground. A title-equality check can't be
# used — Claude Code's terminal title animates (spinner) and changes across /clear,
# so it would never match; the HWND check is the robust "right window" guard.

# self-clear.ps1 — one-shot self-clear + delayed self-wake.
#
# Order (per design):
#   1. Resolve this session's agent_id via `owl whoami` (context-independent; survives /clear).
#   2. Identify this session's terminal window by ANCESTOR PID: walk this process's
#      parent chain and find the visible top-level window owned by an ancestor pid
#      (the WindowsTerminal.exe hosting this tab). FOCUS it (the agent window is
#      usually NOT foreground when a tool runs), then VERIFY it actually came
#      foreground before doing anything — abort WITHOUT clearing if it didn't.
#      Capture its title bar text for the waker's safety check.
#   3. Type "/clear" + Enter  — QUEUED; fires only when this agent's turn ends.
#   4. Spawn a DETACHED waker that waits 200ms, re-focuses the captured HWND, and
#      — ONLY IF that HWND is foreground AND its title still matches the captured
#      title — TYPES the self-note as a fresh user prompt + Enter. Otherwise it ABORTS
#      the SendKeys (so the note never lands in the wrong window/tab).
#
# Why ancestor-pid + focus-first + title-guard: each Windows Terminal window is a
# SEPARATE WindowsTerminal.exe process here, so the ancestor pid uniquely identifies
# THIS session's window (Get-Process.MainWindowHandle would grab a process's first
# window, which is wrong when a host serves several). The window is generally not
# foreground while a tool runs, so we must focus it first and confirm the focus
# landed. The title check then guards the ~200ms gap: if the user switches tab/window,
# the title changes (or our HWND loses foreground) and the waker aborts.
#
# Why detached + delayed: a blocking sleep here would hold the turn open, so /clear
# could never fire. The waker must outlive the turn. The short gap lets the turn end
# and /clear wipe the context first.
#
# Why TYPE the note (not `owl send`): after /clear the session sits IDLE awaiting user
# input. A spooled owl message cannot self-start a turn — it surfaces only during an
# already-active turn (PreToolUse hook) or in a live Monitor stream (whose buffer /clear
# wipes). So the prior "arrives as a waking poll message" design never woke the agent.
# A typed prompt + Enter deterministically starts a fresh turn AND is the re-seed.
$ErrorActionPreference = 'Stop'

# --- 1. Resolve agent_id ---
$owl = $env:OWL
if (-not $owl) { Write-Error 'OWL env not set — restart the session so the SessionStart hook runs.'; exit 1 }
$who = & $owl whoami 2>&1 | Out-String
if ($who -notmatch 'WHOAMI:([^\s]+)') {
  Write-Error "No perch for this session (whoami: $($who.Trim())). Run /spt:ready or /spt:live first — NOT clearing."
  exit 2
}
$id = $matches[1]

# --- 2. Identify (by ancestor pid), focus, and verify this session's terminal window ---
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SC {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc cb, IntPtr l);
  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
  [DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr h, uint cmd);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  delegate bool EnumWindowsProc(IntPtr h, IntPtr l);
  const uint GW_OWNER = 4;
  // First visible, titled, un-owned (top-level) window belonging to `target` pid.
  public static IntPtr FindForPid(uint target) {
    IntPtr found = IntPtr.Zero;
    EnumWindows((h, l) => {
      uint pid; GetWindowThreadProcessId(h, out pid);
      if (pid == target && IsWindowVisible(h) && GetWindow(h, GW_OWNER) == IntPtr.Zero && GetWindowTextLength(h) > 0) {
        found = h; return false; // stop enumeration
      }
      return true;
    }, IntPtr.Zero);
    return found;
  }
  public static string TitleOf(IntPtr h) {
    int n = GetWindowTextLength(h);
    StringBuilder sb = new StringBuilder(n + 1);
    GetWindowText(h, sb, sb.Capacity);
    return sb.ToString();
  }
}
'@
# Walk the parent chain; the deepest ancestor pid that OWNS a visible top-level window
# is this session's terminal window (each WT window is its own process here).
$cur = $PID; $hwnd = [IntPtr]::Zero; $winpid = 0
for ($i = 0; $i -lt 8; $i++) {
  $ci = Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -ErrorAction SilentlyContinue
  if (-not $ci) { break }
  $cand = [SC]::FindForPid([uint32]$cur)
  if ($cand -ne [IntPtr]::Zero) { $hwnd = $cand; $winpid = $cur; break }
  $cur = $ci.ParentProcessId
}
if ($hwnd -eq [IntPtr]::Zero) { Write-Error 'No ancestor-owned terminal window found in 8 hops — NOT clearing.'; exit 3 }

# FOCUS the target window first (it is usually NOT foreground while a tool runs).
$ws = New-Object -ComObject WScript.Shell
function Focus-Target {
  if ([SC]::IsIconic($hwnd)) { [SC]::ShowWindow($hwnd, 9) | Out-Null }  # 9 = SW_RESTORE
  [SC]::SetForegroundWindow($hwnd) | Out-Null
  try { $ws.AppActivate($winpid) | Out-Null } catch {}
}
Focus-Target
Start-Sleep -Milliseconds 200
if ([SC]::GetForegroundWindow() -ne $hwnd) { Focus-Target; Start-Sleep -Milliseconds 200 }
# VERIFY the focus actually landed — if not, do NOT type into the wrong window.
if ([SC]::GetForegroundWindow() -ne $hwnd) {
  Write-Error "Could not bring the agent's terminal window (pid=$winpid hwnd=$hwnd) to the foreground — NOT clearing."
  exit 4
}
$title = [SC]::TitleOf($hwnd)   # captured after focus; the waker re-checks it before typing

# --- 3. Type /clear + Enter (queued; processes at turn-end) ---
$ws.SendKeys('/clear')
Start-Sleep -Milliseconds 150
$ws.SendKeys('{ENTER}')

# --- 4. Detached delayed waker (200ms) — re-foregrounds the captured HWND, then
#        TYPES the note ONLY IF that HWND is foreground AND its title is unchanged. ---
$hptr = $hwnd.ToInt64()
$note = 'Proceed with next steps'
# Pass the captured title as base64 so arbitrary title chars can't break the inner script.
$titleB64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($title))
$inner = @"
Start-Sleep -Milliseconds 200
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SCW {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static string TitleOf(IntPtr h) { int n = GetWindowTextLength(h); StringBuilder sb = new StringBuilder(n + 1); GetWindowText(h, sb, sb.Capacity); return sb.ToString(); }
}
'@
`$h = [IntPtr]$hptr
`$want = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$titleB64'))
`$ws = New-Object -ComObject WScript.Shell
if ([SCW]::IsIconic(`$h)) { [SCW]::ShowWindow(`$h, 9) | Out-Null }
[SCW]::SetForegroundWindow(`$h) | Out-Null
try { `$ws.AppActivate($winpid) | Out-Null } catch {}
Start-Sleep -Milliseconds 120
# ABORT the SendKeys unless our exact window is foreground AND its title is unchanged.
`$fg = [SCW]::GetForegroundWindow()
`$now = [SCW]::TitleOf(`$h)
if (`$fg -ne `$h -or `$now -ne `$want) { exit 0 }
`$ws.SendKeys('$note')
Start-Sleep -Milliseconds 150
`$ws.SendKeys('{ENTER}')
"@
$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($inner))
Start-Process -FilePath 'pwsh' -ArgumentList '-NoProfile','-NonInteractive','-EncodedCommand',$enc -WindowStyle Hidden | Out-Null

"SELF-CLEAR armed: id=$id winpid=$winpid hwnd=$hwnd title=`"$title`" — /clear queued; self-note typed in ~200ms (aborts if window/title changes)"

-# Why ancestor-pid + focus-first + title-guard: each Windows Terminal window is a

-# SEPARATE WindowsTerminal.exe process here, so the ancestor pid uniquely identifies

-# THIS session's window (Get-Process.MainWindowHandle would grab a process's first

-# window, which is wrong when a host serves several). The window is generally not

-# foreground while a tool runs, so we must focus it first and confirm the focus

-# landed. The title check then guards the ~200ms gap: if the user switches tab/window,

-# the title changes (or our HWND loses foreground) and the waker aborts.

+# Why ancestor-pid + focus-first + HWND-identity guard: each Windows Terminal window

+# is a SEPARATE WindowsTerminal.exe process here, so the ancestor pid uniquely

+# identifies THIS session's window (Get-Process.MainWindowHandle would grab a

+# process's first window, which is wrong when a host serves several). The window is

+# generally not foreground while a tool runs, so we must focus it first and confirm

+# the focus landed. The waker then guards the ~200ms gap by EXACT WINDOW IDENTITY:

+# it types only if our specific HWND is foreground. A title-equality check can't be

+# used — Claude Code's terminal title animates (spinner) and changes across /clear,

+# so it would never match; the HWND check is the robust "right window" guard.


$title = [SC]::TitleOf($hwnd)   # captured after focus; the waker re-checks it before typing

# --- 3. Type /clear + Enter (queued; processes at turn-end) ---

$title = [SC]::TitleOf($hwnd)   # informational only (for the armed message)

# --- 3. Type /clear + Enter (queued; processes at turn-end) ---


$title = [SC]::TitleOf($hwnd)   # informational only (for the armed message)

# --- 3. Type /clear + Enter (queued; processes at turn-end) ---

# self-clear.ps1 — one-shot self-clear + delayed self-wake.
#
# Order (per design):
#   1. Resolve this session's agent_id via `owl whoami` (context-independent; survives /clear).
#   2. Identify this session's terminal window by ANCESTOR PID: walk this process's
#      parent chain and find the visible top-level window owned by an ancestor pid
#      (the WindowsTerminal.exe hosting this tab). FOCUS it (the agent window is
#      usually NOT foreground when a tool runs), then VERIFY it actually came
#      foreground before doing anything — abort WITHOUT clearing if it didn't.
#      Capture its title bar text for the waker's safety check.
#   3. Type "/clear" + Enter  — QUEUED; fires only when this agent's turn ends.
#   4. Spawn a DETACHED waker that waits 200ms, re-focuses the captured HWND, and
#      — ONLY IF that HWND is foreground AND its title still matches the captured
#      title — TYPES the self-note as a fresh user prompt + Enter. Otherwise it ABORTS
#      the SendKeys (so the note never lands in the wrong window/tab).
#
# Why ancestor-pid + focus-first + HWND-identity guard: each Windows Terminal window
# is a SEPARATE WindowsTerminal.exe process here, so the ancestor pid uniquely
# identifies THIS session's window (Get-Process.MainWindowHandle would grab a
# process's first window, which is wrong when a host serves several). The window is
# generally not foreground while a tool runs, so we must focus it first and confirm
# the focus landed. The waker then guards the ~200ms gap by EXACT WINDOW IDENTITY:
# it types only if our specific HWND is foreground. A title-equality check can't be
# used — Claude Code's terminal title animates (spinner) and changes across /clear,
# so it would never match; the HWND check is the robust "right window" guard.
#
# Why detached + delayed: a blocking sleep here would hold the turn open, so /clear
# could never fire. The waker must outlive the turn. The short gap lets the turn end
# and /clear wipe the context first.
#
# Why TYPE the note (not `owl send`): after /clear the session sits IDLE awaiting user
# input. A spooled owl message cannot self-start a turn — it surfaces only during an
# already-active turn (PreToolUse hook) or in a live Monitor stream (whose buffer /clear
# wipes). So the prior "arrives as a waking poll message" design never woke the agent.
# A typed prompt + Enter deterministically starts a fresh turn AND is the re-seed.
$ErrorActionPreference = 'Stop'

# --- 1. Resolve agent_id ---
$owl = $env:OWL
if (-not $owl) { Write-Error 'OWL env not set — restart the session so the SessionStart hook runs.'; exit 1 }
$who = & $owl whoami 2>&1 | Out-String
if ($who -notmatch 'WHOAMI:([^\s]+)') {
  Write-Error "No perch for this session (whoami: $($who.Trim())). Run /spt:ready or /spt:live first — NOT clearing."
  exit 2
}
$id = $matches[1]

# --- 2. Identify (by ancestor pid), focus, and verify this session's terminal window ---
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SC {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc cb, IntPtr l);
  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
  [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
  [DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr h, uint cmd);
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  delegate bool EnumWindowsProc(IntPtr h, IntPtr l);
  const uint GW_OWNER = 4;
  // First visible, titled, un-owned (top-level) window belonging to `target` pid.
  public static IntPtr FindForPid(uint target) {
    IntPtr found = IntPtr.Zero;
    EnumWindows((h, l) => {
      uint pid; GetWindowThreadProcessId(h, out pid);
      if (pid == target && IsWindowVisible(h) && GetWindow(h, GW_OWNER) == IntPtr.Zero && GetWindowTextLength(h) > 0) {
        found = h; return false; // stop enumeration
      }
      return true;
    }, IntPtr.Zero);
    return found;
  }
  public static string TitleOf(IntPtr h) {
    int n = GetWindowTextLength(h);
    StringBuilder sb = new StringBuilder(n + 1);
    GetWindowText(h, sb, sb.Capacity);
    return sb.ToString();
  }
}
'@
# Walk the parent chain; the deepest ancestor pid that OWNS a visible top-level window
# is this session's terminal window (each WT window is its own process here).
$cur = $PID; $hwnd = [IntPtr]::Zero; $winpid = 0
for ($i = 0; $i -lt 8; $i++) {
  $ci = Get-CimInstance Win32_Process -Filter "ProcessId=$cur" -ErrorAction SilentlyContinue
  if (-not $ci) { break }
  $cand = [SC]::FindForPid([uint32]$cur)
  if ($cand -ne [IntPtr]::Zero) { $hwnd = $cand; $winpid = $cur; break }
  $cur = $ci.ParentProcessId
}
if ($hwnd -eq [IntPtr]::Zero) { Write-Error 'No ancestor-owned terminal window found in 8 hops — NOT clearing.'; exit 3 }

# FOCUS the target window first (it is usually NOT foreground while a tool runs).
$ws = New-Object -ComObject WScript.Shell
function Focus-Target {
  if ([SC]::IsIconic($hwnd)) { [SC]::ShowWindow($hwnd, 9) | Out-Null }  # 9 = SW_RESTORE
  [SC]::SetForegroundWindow($hwnd) | Out-Null
  try { $ws.AppActivate($winpid) | Out-Null } catch {}
}
Focus-Target
Start-Sleep -Milliseconds 200
if ([SC]::GetForegroundWindow() -ne $hwnd) { Focus-Target; Start-Sleep -Milliseconds 200 }
# VERIFY the focus actually landed — if not, do NOT type into the wrong window.
if ([SC]::GetForegroundWindow() -ne $hwnd) {
  Write-Error "Could not bring the agent's terminal window (pid=$winpid hwnd=$hwnd) to the foreground — NOT clearing."
  exit 4
}
$title = [SC]::TitleOf($hwnd)   # captured after focus; the waker re-checks it before typing

# --- 3. Type /clear + Enter (queued; processes at turn-end) ---
$ws.SendKeys('/clear')
Start-Sleep -Milliseconds 150
$ws.SendKeys('{ENTER}')

# --- 4. Detached delayed waker (200ms) — re-foregrounds the captured HWND, then
#        TYPES the note ONLY IF that HWND is foreground AND its title is unchanged. ---
$hptr = $hwnd.ToInt64()
$note = 'Proceed with next steps'
# Pass the captured title as base64 so arbitrary title chars can't break the inner script.
$titleB64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($title))
$inner = @"
Start-Sleep -Milliseconds 200
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public class SCW {
  [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
  [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int max);
  [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
  public static string TitleOf(IntPtr h) { int n = GetWindowTextLength(h); StringBuilder sb = new StringBuilder(n + 1); GetWindowText(h, sb, sb.Capacity); return sb.ToString(); }
}
'@
`$h = [IntPtr]$hptr
`$want = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$titleB64'))
`$ws = New-Object -ComObject WScript.Shell
if ([SCW]::IsIconic(`$h)) { [SCW]::ShowWindow(`$h, 9) | Out-Null }
[SCW]::SetForegroundWindow(`$h) | Out-Null
try { `$ws.AppActivate($winpid) | Out-Null } catch {}
Start-Sleep -Milliseconds 120
# ABORT the SendKeys unless our exact window is foreground AND its title is unchanged.
`$fg = [SCW]::GetForegroundWindow()
`$now = [SCW]::TitleOf(`$h)
if (`$fg -ne `$h -or `$now -ne `$want) { exit 0 }
`$ws.SendKeys('$note')
Start-Sleep -Milliseconds 150
`$ws.SendKeys('{ENTER}')
"@
$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($inner))
Start-Process -FilePath 'pwsh' -ArgumentList '-NoProfile','-NonInteractive','-EncodedCommand',$enc -WindowStyle Hidden | Out-Null

"SELF-CLEAR armed: id=$id winpid=$winpid hwnd=$hwnd title=`"$title`" — /clear queued; self-note typed in ~200ms (aborts if window/title changes)"

-$title = [SC]::TitleOf($hwnd)   # captured after focus; the waker re-checks it before typing

+$title = [SC]::TitleOf($hwnd)   # informational only (for the armed message)