Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
642bbb4
docs: specify UI and execution revamp
aksOps Jul 10, 2026
7400122
docs: add UI execution implementation plan
aksOps Jul 10, 2026
421c0c9
chore: ignore local worktrees
aksOps Jul 10, 2026
0a4f344
docs: fix cross-platform plan examples
aksOps Jul 10, 2026
63402db
docs: specify Task 1 pure contracts
aksOps Jul 10, 2026
11791ce
docs: resolve Preview key precedence
aksOps Jul 10, 2026
dd74890
feat: add revamp core contracts
aksOps Jul 10, 2026
29cf8b5
docs: correct contrast reference ratios
aksOps Jul 10, 2026
6623059
feat: persist settings and register the default hotkey
aksOps Jul 10, 2026
2412d46
fix: make settings persistence transactional
aksOps Jul 10, 2026
f083336
refactor: serialize capture execution
aksOps Jul 10, 2026
becf578
fix: close capture ownership races
aksOps Jul 10, 2026
08e6722
fix: resume capture after dispatch failure
aksOps Jul 10, 2026
13984bf
feat: unify SnipIT utility surfaces
aksOps Jul 12, 2026
f7c7440
fix: preserve utility native state
aksOps Jul 12, 2026
75040ca
fix: style nested tray surfaces
aksOps Jul 12, 2026
2debc03
fix: remove stock tray check chrome
aksOps Jul 12, 2026
79c5979
feat: add per-monitor Smart capture overlay
aksOps Jul 12, 2026
4350864
fix: harden overlay targeting
aksOps Jul 12, 2026
2624902
feat: build the Floating Studio preview shell
aksOps Jul 12, 2026
125b941
fix: harden Floating Studio interaction paths
aksOps Jul 12, 2026
2086c9c
fix: close Floating Studio edge cases
aksOps Jul 12, 2026
7ec9bd2
fix: finish Floating Studio cleanup
aksOps Jul 12, 2026
65ca017
feat: add selection and non-destructive crop
aksOps Jul 12, 2026
943e778
docs: define modular SnipIT development architecture
aksOps Jul 12, 2026
f1a89cc
docs: plan modular SnipIT migration
aksOps Jul 12, 2026
e05bec4
test: freeze modular migration parity
aksOps Jul 12, 2026
3ecd751
build: add modular source launcher
aksOps Jul 12, 2026
fac8de6
refactor: extract SnipIT XAML sources
aksOps Jul 12, 2026
ce368e2
refactor: split SnipIT development sources
aksOps Jul 12, 2026
df537c0
fix: install standalone SnipIT payload
aksOps Jul 12, 2026
3567eae
build: generate the portable SnipIT release
aksOps Jul 12, 2026
c148f9d
fix: reject linked build sources
aksOps Jul 12, 2026
f222ba5
ci: pin PowerShell 7.5 runtime
aksOps Jul 12, 2026
d8d8e0d
ci: include large PowerShell files in duplication scan
aksOps Jul 12, 2026
fb832fb
fix: keep development XAML lookup out of release
aksOps Jul 12, 2026
1fb6865
test: harden runtime and XAML precedence gates
aksOps Jul 12, 2026
8ee8c81
fix: preflight modular development inputs
aksOps Jul 12, 2026
3ed50f5
fix: validate development modules before loading
aksOps Jul 12, 2026
026f70d
test: keep Windows runtime probes on Windows
aksOps Jul 13, 2026
12ce062
ci: restore protected check contexts
aksOps Jul 13, 2026
cf7790f
docs: remove internal development discussions
aksOps Jul 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.ps1 text eol=lf
*.xaml text eol=lf
*.json text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
59 changes: 42 additions & 17 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ name: Security (OSS-CLI)
# - Semgrep packs: `p/security-audit` + `p/owasp-top-ten` only. Semgrep
# has no first-party PowerShell pack today; the language-specific gate
# is PSScriptAnalyzer (added below) — codeiq-equivalent of `p/java`.
# - jscpd format set to `powershell`, scoped to the three .ps1 files.
# - jscpd format set to `powershell`, with generated release and authoritative
# development sources scanned separately to exclude intentional parity.
# - Added `psscriptanalyzer` job — language lint per
# `shared/runbooks/engineering-standards.md` (PowerShell variant).
on:
Expand Down Expand Up @@ -68,7 +69,8 @@ jobs:
--config p/security-audit \
--config p/owasp-top-ten \
--severity ERROR \
--metrics off
--metrics off \
./SnipIT.ps1 ./Build-SnipIT.ps1 ./SnipIT.Dev.ps1 ./src

psscriptanalyzer:
name: PSScriptAnalyzer (Error severity gate)
Expand All @@ -84,19 +86,41 @@ jobs:
shell: pwsh
run: |
Import-Module PSScriptAnalyzer
$w = Invoke-ScriptAnalyzer -Path ./SnipIT.ps1 -Severity Warning
$paths = @(
'./SnipIT.ps1'
'./Build-SnipIT.ps1'
'./SnipIT.Dev.ps1'
'./scripts/Export-SnipITModules.ps1'
) + @(Get-ChildItem -LiteralPath ./src -Filter *.ps1 -File -Recurse |
Sort-Object FullName | ForEach-Object FullName)
$w = foreach ($path in $paths) {
Invoke-ScriptAnalyzer -Path $path -Severity Warning | ForEach-Object {
$_ | Add-Member -NotePropertyName SourcePath -NotePropertyValue $path -PassThru
}
}
Write-Host "Warning count: $((@($w)).Count)"
$w | Group-Object RuleName | Sort-Object Count -Descending |
Select-Object Count, Name | Format-Table -AutoSize | Out-String | Write-Host
$w | Format-Table -AutoSize SourcePath, RuleName, Line, Message |
Out-String | Write-Host
- name: Fail on Error-severity findings
shell: pwsh
run: |
Import-Module PSScriptAnalyzer
$errs = Invoke-ScriptAnalyzer -Path ./SnipIT.ps1 -Severity Error
$paths = @(
'./SnipIT.ps1'
'./Build-SnipIT.ps1'
'./SnipIT.Dev.ps1'
'./scripts/Export-SnipITModules.ps1'
) + @(Get-ChildItem -LiteralPath ./src -Filter *.ps1 -File -Recurse |
Sort-Object FullName | ForEach-Object FullName)
$errs = foreach ($path in $paths) {
Invoke-ScriptAnalyzer -Path $path -Severity Error | ForEach-Object {
$_ | Add-Member -NotePropertyName SourcePath -NotePropertyValue $path -PassThru
}
}
$count = (@($errs)).Count
Write-Host "Error count: $count"
if ($count -gt 0) {
$errs | Format-Table -AutoSize Severity, RuleName, Line, Message |
$errs | Format-Table -AutoSize SourcePath, Severity, RuleName, Line, Message |
Out-String | Write-Host
exit 1
}
Expand Down Expand Up @@ -139,15 +163,6 @@ jobs:
with:
node-version: '20'
- run: |
# snipIT is three PowerShell files at repo root:
# - SnipIT.ps1 (production)
# - Test-SnipIT.ps1 (headless tests)
# - Test-SnipIT-Interactive.ps1 (interactive tests)
#
# Production-only scope per AC interpretation (matches codeiq
# convention where tests are excluded from the dup gate). Tests
# share fixture / Assert-* shape by design.
#
# `--min-tokens 100` is calibrated to PowerShell's medium verbosity
# floor (Add-Type/[CmdletBinding] blocks, `param()` headers, P/Invoke
# signatures). The Java floor of 200 over-suppresses; the jscpd
Expand All @@ -161,7 +176,17 @@ jobs:
--reporters consoleFull \
--format "powershell" \
--ignore "**/Test-SnipIT*.ps1,**/docs/**" \
./SnipIT.ps1
--max-size 1mb --max-lines 20000 ./SnipIT.ps1
# Scan authoritative development inputs separately so their
# intentional parity with the generated release is not a clone.
npx --yes jscpd@4 \
--threshold 3 \
--min-tokens 100 \
--reporters consoleFull \
--format "powershell" \
--ignore "**/Test-SnipIT*.ps1,**/docs/**" \
--max-size 1mb --max-lines 20000 \
./src ./Build-SnipIT.ps1 ./SnipIT.Dev.ps1

sbom:
name: SBOM (SPDX + CycloneDX)
Expand Down
85 changes: 64 additions & 21 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,29 @@ jobs:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2
- name: Install PowerShell 7.5+ (SnipIT.ps1 requires it)
- name: Install PowerShell 7.5.7
shell: bash
run: |
dotnet tool install --global PowerShell
dotnet tool install --global PowerShell --version 7.5.7
echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
- name: Show pwsh version
shell: bash
run: pwsh --version
- name: Verify PowerShell 7.5 on .NET 9
shell: pwsh
run: |
Write-Host "pwsh executable: $((Get-Process -Id $PID).Path)"
Write-Host "PowerShell: $($PSVersionTable.PSVersion)"
Write-Host ".NET runtime: $([Environment]::Version)"
if (-not ($PSVersionTable.PSVersion -eq [version]'7.5.7')) {
throw "Expected PowerShell 7.5.7, got $($PSVersionTable.PSVersion)"
}
if ([Environment]::Version.Major -ne 9) {
throw "Expected .NET 9, got $([Environment]::Version)"
}
- name: Rebuild generated release and reject stale output
shell: pwsh
run: |
pwsh -NoProfile -File ./Build-SnipIT.ps1
git diff --exit-code -- SnipIT.ps1
pwsh -NoProfile -File ./Test-SnipIT-Build.ps1
- name: Run headless tests
shell: bash
run: pwsh -NoProfile -File ./Test-SnipIT.ps1
Expand All @@ -47,27 +62,55 @@ jobs:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2
- name: Install PowerShell 7.5+ (SnipIT.ps1 requires it)
- name: Install PowerShell 7.5.7
shell: pwsh
run: |
dotnet tool install --global PowerShell
dotnet tool install --global PowerShell --version 7.5.7
"$HOME\.dotnet\tools" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8
- name: Show pwsh version
shell: bash
run: pwsh --version
- name: Parse script (AST parser works with any pwsh version)
shell: bash
- name: Verify PowerShell 7.5 on .NET 9
shell: pwsh
run: |
Write-Host "pwsh executable: $((Get-Process -Id $PID).Path)"
Write-Host "PowerShell: $($PSVersionTable.PSVersion)"
Write-Host ".NET runtime: $([Environment]::Version)"
if (-not ($PSVersionTable.PSVersion -eq [version]'7.5.7')) {
throw "Expected PowerShell 7.5.7, got $($PSVersionTable.PSVersion)"
}
if ([Environment]::Version.Major -ne 9) {
throw "Expected .NET 9, got $([Environment]::Version)"
}
- name: Rebuild generated release and reject stale output
shell: pwsh
run: |
pwsh -NoProfile -Command "
\$errors = \$null
[System.Management.Automation.Language.Parser]::ParseFile(
(Resolve-Path ./SnipIT.ps1), [ref]\$null, [ref]\$errors) | Out-Null
if (\$errors) {
\$errors | ForEach-Object { Write-Host \" \$(\$_.Message) at line \$(\$_.Extent.StartLineNumber)\" }
exit 1
pwsh -NoProfile -File ./Build-SnipIT.ps1
git diff --exit-code -- SnipIT.ps1
pwsh -NoProfile -File ./Test-SnipIT-Build.ps1
- name: Parse every tracked PowerShell script
shell: pwsh
run: |
$parseFailures = [Collections.Generic.List[string]]::new()
$trackedScripts = @(git ls-files -- '*.ps1') | Sort-Object
foreach ($path in $trackedScripts) {
$tokens = $null
$errors = $null
[void][Management.Automation.Language.Parser]::ParseFile(
(Resolve-Path -LiteralPath $path), [ref]$tokens, [ref]$errors)
foreach ($errorRecord in @($errors)) {
$parseFailures.Add(('{0}:{1}: {2}' -f $path,
$errorRecord.Extent.StartLineNumber, $errorRecord.Message))
}
Write-Host 'Parsed cleanly.'
"
}
if ($parseFailures.Count) {
$parseFailures | ForEach-Object { Write-Host $_ }
exit 1
}
Write-Host "Parsed $($trackedScripts.Count) tracked PowerShell scripts cleanly."
- name: Run headless tests on Windows
shell: bash
run: pwsh -NoProfile -File ./Test-SnipIT.ps1
- name: Smoke-test modular development launcher
shell: pwsh
env:
SNIPIT_SCRIPT_UNDER_TEST: ./SnipIT.Dev.ps1
SNIPIT_TEST_GROUP: Floating Studio preview shell
run: pwsh -NoProfile -Sta -File ./Test-SnipIT-Interactive.ps1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Desktop.ini
.vscode/
.vs/
.idea/
.worktrees/
*.swp
*~

Expand Down
114 changes: 114 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# snipIT — Agent brief

Read this at session start. It is the standing context for any agent touching this repo.

## What it is

A **professional snipping tool** for Windows 11 written in **pure PowerShell
7.5+** on **.NET 9**. Smart hover-to-highlight capture, magnifier loupe,
floating widget, system tray, chromeless WPF Fluent preview, and a full
annotation editor — zero external runtime dependencies and no admin elevation.

**Board reversal (accepted 2026-07-12):** modular `src/` PowerShell and
external `xaml/` files are now the authoritative development source.
`SnipIT.ps1` remains the headline single-file distribution and is generated
deterministically. Never edit `SnipIT.ps1` directly.

## Repo layout

```text
src/
├── 00-Core.ps1 pure cross-platform logic and -CoreOnly boundary
├── 10-Bootstrap.ps1 startup, settings, install, XAML loader
├── 20-Native.ps1 Win32 interop and native window services
├── 30-Capture.ps1 capture coordinator, bitmaps, overlays
├── 40-Preview.ps1 preview and annotation editor
├── 50-Tray.ps1 tray, settings/about windows, floating widget
└── 90-Main.ps1 application entry point and cleanup
xaml/ six authoritative WPF surfaces/resources
Build-SnipIT.ps1 sole manifest/order authority and release generator
SnipIT.Dev.ps1 repository development launcher
SnipIT.ps1 generated checked-in standalone distribution
Test-SnipIT-Build.ps1 deterministic-build and failure fixtures
Test-SnipIT.ps1 pure-logic tests (Linux + Windows, no Pester)
Test-SnipIT-Interactive.ps1 WPF integration tests (Windows only)
scripts/Export-SnipITModules.ps1 retained AST/provenance round-trip tool
README.md install, usage, architecture, contributor workflow
SECURITY.md disclosure policy and scope
shared/runbooks/engineering-standards.md PowerShell engineering runbook
```

The generated `SnipIT.ps1` must stay portable: no runtime lookup of `src/`,
`xaml/`, the builder, repository paths, external modules, or network resources.

## Build / test / run

| Action | Command |
|---|---|
| Run the distribution | `pwsh -Sta -File ./SnipIT.ps1` |
| Run development sources | `pwsh -Sta -File ./SnipIT.Dev.ps1` |
| Regenerate distribution | `pwsh -NoProfile -File ./Build-SnipIT.ps1` |
| Reject stale distribution | `git diff --exit-code -- SnipIT.ps1` |
| Build contract tests | `pwsh -NoProfile -File ./Test-SnipIT-Build.ps1` |
| Release pure tests | `pwsh -NoProfile -File ./Test-SnipIT.ps1` |
| Development pure tests | `$env:SNIPIT_SCRIPT_UNDER_TEST='./SnipIT.Dev.ps1'; pwsh -NoProfile -File ./Test-SnipIT.ps1` |
| Release WPF tests | `pwsh -NoProfile -Sta -File ./Test-SnipIT-Interactive.ps1` |
| Development WPF smoke | `$env:SNIPIT_SCRIPT_UNDER_TEST='./SnipIT.Dev.ps1'; $env:SNIPIT_TEST_GROUP='Floating Studio preview shell'; pwsh -NoProfile -Sta -File ./Test-SnipIT-Interactive.ps1` |

Always regenerate after changing `src/` or `xaml/`. Commit the source change and
generated release together. CI rebuilds and rejects any stale `SnipIT.ps1`.

## Conventions

- **PowerShell 7.5+ only.** No PowerShell 5.1 fallbacks.
- Functions use approved `Verb-Noun` PascalCase names. Use `[CmdletBinding()]`
and `param()` for anything with more than one argument.
- Pure helpers belong in `src/00-Core.ps1`; `-CoreOnly` must remain usable on
Linux without loading WPF, WinForms, or other Windows-only assemblies.
- The builder is the only module/XAML order authority. Generated provenance
uses manifest-relative forward-slash paths and never absolute paths.
- Preview mouse handlers are named closures captured by `New-SnipPreviewWindow`;
real WPF handlers remain one-line wrappers. Tests drive these closures through
the `Show-PreviewWindow -TestAction` seam.
- `SNIPIT_SCRIPT_UNDER_TEST` redirects either test harness to the development
launcher. Keep injection step-scoped; release tests must remain release tests.
- `SNIPIT_TEST_MODE=1` suppresses mutex, install, tray, hotkeys, and the main
loop so either launch path can be dot-sourced without side effects.
- All commits on `main` are signed. Run `scripts/setup-git-signed.sh` once in a
fresh worktree; Task commits must use signing as required by the workflow.

## Engineering standards

The repo follows `shared/runbooks/engineering-standards.md`.

- Merge gates: deterministic build plus stale-release diff, tests, AST parse,
**PSScriptAnalyzer Error**, **Trivy HIGH/CRITICAL**, **Semgrep ERROR**,
**Gitleaks**, **jscpd < 3%**, OpenSSF Best Practices `passing`, and signed commits.
- SBOM, OpenSSF Scorecard, and Dependabot are surfaces, not merge gates.
- **OSS-CLI only.** No Sonar, CodeQL, or NVD-direct tools.
- Every GitHub Action reference must remain pinned to a full commit SHA.

OpenSSF Best Practices project: <https://www.bestpractices.dev/en/projects/12647>.
Scorecard workflow is observational; best-effort target is at least 8.0/10 and
material regressions create a security chore rather than blocking a PR.

## Gotchas

- **Capture loop ownership.** `Invoke-CaptureLoop` owns every captured bitmap.
Preview disposes it on close, and `CaptureFactory` creates a fresh bitmap per
iteration. Never dispose inside the factory or reuse a preallocated bitmap.
- **SnipIT-window exclusion.** Register every new top-level SnipIT window through
`Hide-OwnSnipITWindowsForCapture` so it is excluded from captured frames.
- **Per-monitor DPI.** Capture math supports mixed scaling and negative virtual
desktop origins. Never assume `(0,0)` is the virtual desktop top-left.
- **Single instance.** A second launch notifies and exits unless
`SNIPIT_TEST_MODE=1` is set.
- **Generated-file ownership.** Do not patch `SnipIT.ps1`; change authoritative
source and run `Build-SnipIT.ps1`.
- **Pinned dependencies.** Never replace a full action SHA with a tag such as
`actions/checkout@v4`.

## Issue tracker

Paperclip uses prefix `RAN`. Link tickets as `[RAN-XX](/RAN/issues/RAN-XX)` in
PR descriptions and code comments.
Loading
Loading