From fdc9b571b53d92221b0397b62f251db6cba1dd4f Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 9 Jun 2026 17:42:53 -0600 Subject: [PATCH 1/5] Fix inconsistencies in AI agent instruction files - Local function naming: camelCase -> PascalCase in REFRESH.md and copilot-instructions.md (matches .editorconfig local_functions_rule, AGENTS.md, and event-patterns.md) - Replace stale Tests/UnitTests references with current project names (UnitTestsParallelizable / UnitTests.NonParallelizable) across AGENTS.md, CONTRIBUTING.md, .aider.md, .cursorrules, .windsurfrules, copilot-instructions.md, and .claude workflows/tasks - Replace deprecated --filter "FullyQualifiedName~" syntax with xUnit v3 MTP --filter-method/--filter-class in copilot-instructions.md - Remove machine-local path (D:\s\...) from AGENTS.md planning section - build-app.md: use Accepted (post-event) for fire-and-forget handlers per event-patterns.md; show Accepting only for cancellation; fix v1-style new Button ("OK") to v2 object initializer - build-test-workflow.md: .NET SDK 8.0 -> 10.0.100 per global.json Co-Authored-By: Claude Fable 5 --- .aider.md | 2 +- .claude/REFRESH.md | 2 +- .claude/tasks/build-app.md | 25 +++++++++++++++--------- .claude/tasks/clean-code-review.md | 2 +- .claude/workflows/build-test-workflow.md | 10 +++++----- .claude/workflows/pr-workflow.md | 6 +++--- .cursorrules | 2 +- .github/copilot-instructions.md | 16 +++++++-------- .windsurfrules | 2 +- AGENTS.md | 4 ++-- CONTRIBUTING.md | 2 +- 11 files changed, 40 insertions(+), 33 deletions(-) diff --git a/.aider.md b/.aider.md index 2d4df42afb..e56665fc0c 100644 --- a/.aider.md +++ b/.aider.md @@ -34,7 +34,7 @@ Terminal.Gui v2 is a **complete rewrite**. Pre-2025 training data about Terminal dotnet restore dotnet build --no-restore dotnet test --project Tests/UnitTestsParallelizable --no-build -dotnet test --project Tests/UnitTests --no-build +dotnet test --project Tests/UnitTests.NonParallelizable --no-build ``` --- diff --git a/.claude/REFRESH.md b/.claude/REFRESH.md index 4d6aabc19e..fbf31d0abb 100644 --- a/.claude/REFRESH.md +++ b/.claude/REFRESH.md @@ -9,7 +9,7 @@ 3. **Use `[...]`** not `new () { ... }` for collections 4. **SubView/SuperView** - never say "child", "parent", or "container" 5. **Unused lambda params** - use `_` discard: `(_, _) => { }` -6. **Local functions** - use camelCase: `void myLocalFunc ()` +6. **Local functions** - use PascalCase: `void MyLocalFunc ()` 7. **Backing fields** - place immediately before their property (ReSharper bug, must do manually) 8. **SPACE BEFORE PARENTHESES** - `Method ()` not `Method()`, `array [i]` not `array[i]` (see `formatting.md`) 9. **Braces on next line** - ALL opening braces on next line (Allman style) diff --git a/.claude/tasks/build-app.md b/.claude/tasks/build-app.md index 0ba4512286..7edf368e47 100644 --- a/.claude/tasks/build-app.md +++ b/.claude/tasks/build-app.md @@ -61,10 +61,9 @@ public sealed class MainWindow : Runnable // Add controls here Button button = new () { Text = "Click Me", X = Pos.Center (), Y = Pos.Center () }; - button.Accepting += (_, e) => + button.Accepted += (_, _) => { MessageBox.Query (App!, "Hello", "Button clicked!", "OK"); - e.Handled = true; }; Add (button); @@ -80,11 +79,10 @@ public sealed class LoginWindow : Runnable { // ... setup UI ... - loginButton.Accepting += (_, e) => + loginButton.Accepted += (_, _) => { Result = usernameField.Text; // Set return value App!.RequestStop (); // Close window - e.Handled = true; }; } } @@ -150,10 +148,19 @@ See `.claude/cookbook/common-patterns.md` for recipes including: ### Button Click ```csharp -button.Accepting += (sender, e) => +// Simple side-effect handler — use Accepted (post-event) +button.Accepted += (_, _) => { // Handle the click - e.Handled = true; // Prevent further processing +}; + +// Use Accepting (pre-event) ONLY to inspect or cancel the in-flight action +button.Accepting += (_, e) => +{ + if (!CanProceed ()) + { + e.Handled = true; // Cancel — prevents Accepted from firing + } }; ``` @@ -200,7 +207,7 @@ Dialog dialog = new () Title = "Custom Dialog", Width = 40, Height = 10, - Buttons = [new Button ("OK"), new Button ("Cancel")] + Buttons = [new Button { Text = "OK" }, new Button { Text = "Cancel" }] }; // Add controls to dialog... app.Run (dialog); @@ -241,12 +248,12 @@ Available themes: `Default`, `Dark`, `Light`, `Amber Phosphor`, `Green Phosphor` - [ ] Main window class inheriting from `Runnable` or `Runnable` - [ ] Application lifecycle: Create -> Init -> Run -> Dispose - [ ] Layout using Pos/Dim (not hardcoded positions) -- [ ] Event handlers with `e.Handled = true` when appropriate +- [ ] `-ed` events (`Accepted`) for side effects; `-ing` events (`Accepting`) only to cancel - [ ] Proper cleanup with `Dispose` pattern ## What NOT to Do - Don't use `Application.Init()` / `Application.Shutdown()` (legacy static API) - Don't hardcode sizes - use `Dim.Fill()`, `Dim.Auto()`, `Dim.Percent()` -- Don't forget `e.Handled = true` in Accepting handlers +- Don't use `Accepting` for fire-and-forget side effects - use `Accepted`; reserve `Accepting` (with `e.Handled = true`) for canceling - Don't block the main thread - use `Application.AddTimeout` for async work diff --git a/.claude/tasks/clean-code-review.md b/.claude/tasks/clean-code-review.md index 7aee227d0a..cdd67968fb 100644 --- a/.claude/tasks/clean-code-review.md +++ b/.claude/tasks/clean-code-review.md @@ -51,8 +51,8 @@ Run these for each commit: ```bash dotnet build --no-restore dotnet test --project Tests/IntegrationTests --no-build -dotnet test --project Tests/UnitTests --no-build dotnet test --project Tests/UnitTestsParallelizable --no-build +dotnet test --project Tests/UnitTests.NonParallelizable --no-build ``` ## Terminal.Gui Specific Requirements diff --git a/.claude/workflows/build-test-workflow.md b/.claude/workflows/build-test-workflow.md index 19a6909038..c0d8cf13b9 100644 --- a/.claude/workflows/build-test-workflow.md +++ b/.claude/workflows/build-test-workflow.md @@ -4,8 +4,8 @@ ## Required Tools -- **.NET SDK**: 8.0.0 (see `global.json`) -- **Runtime**: .NET 8.x (latest GA) +- **.NET SDK**: 10.0.100 (see `global.json`) +- **Runtime**: .NET 10.x (latest GA) - **Optional**: ReSharper/Rider for code formatting (honor `.editorconfig` and `Terminal.sln.DotSettings`) ## Build Commands @@ -47,7 +47,7 @@ dotnet build --configuration Release --no-restore **Time:** ~10 min timeout ```bash -dotnet test --project Tests/UnitTests --no-build --verbosity normal +dotnet test --project Tests/UnitTests.NonParallelizable --no-build --verbosity normal ``` - Uses `Application.Init` and static state @@ -75,7 +75,7 @@ dotnet test --project Tests/IntegrationTests --no-build --verbosity normal ### Run All Tests ```bash -dotnet test --project Tests/UnitTests --no-build --verbosity normal && dotnet test --project Tests/UnitTestsParallelizable --no-build --verbosity normal +dotnet test --project Tests/UnitTestsParallelizable --no-build --verbosity normal && dotnet test --project Tests/UnitTests.NonParallelizable --no-build --verbosity normal ``` ## Common Build Issues @@ -94,7 +94,7 @@ dotnet publish ./Tests/NativeAotSmoke/NativeAotSmoke.csproj --configuration Rele **For clean builds, always run in this order:** ```bash -dotnet restore && dotnet build --no-restore && dotnet test --project Tests/UnitTests --no-build && dotnet test --project Tests/UnitTestsParallelizable --no-build +dotnet restore && dotnet build --no-restore && dotnet test --project Tests/UnitTestsParallelizable --no-build && dotnet test --project Tests/UnitTests.NonParallelizable --no-build ``` This ensures: diff --git a/.claude/workflows/pr-workflow.md b/.claude/workflows/pr-workflow.md index 7c8a958693..235bd3d151 100644 --- a/.claude/workflows/pr-workflow.md +++ b/.claude/workflows/pr-workflow.md @@ -27,7 +27,7 @@ Before submitting a PR, ensure: - [ ] **Build passes** locally: `dotnet build --no-restore` -- [ ] **Tests pass** locally: `dotnet test --project Tests/UnitTests --no-build && dotnet test --project Tests/UnitTestsParallelizable --no-build` +- [ ] **Tests pass** locally: `dotnet test --project Tests/UnitTestsParallelizable --no-build && dotnet test --project Tests/UnitTests.NonParallelizable --no-build` ## PR Description Template @@ -70,7 +70,7 @@ dotnet build --configuration Debug --no-restore ### 2. Run Tests ```bash -dotnet test --project Tests/UnitTests --no-build --verbosity normal && dotnet test --project Tests/UnitTestsParallelizable --no-build --verbosity normal +dotnet test --project Tests/UnitTestsParallelizable --no-build --verbosity normal && dotnet test --project Tests/UnitTests.NonParallelizable --no-build --verbosity normal ``` **Expected:** All tests pass @@ -106,7 +106,7 @@ git diff - ❌ Don't modify unrelated code - ❌ Don't remove/edit unrelated tests - ❌ Don't break existing functionality -- ❌ Don't add tests to `UnitTests` if they can be parallelizable +- ❌ Don't add tests to `UnitTests.NonParallelizable` if they can be parallelizable; never add tests to `UnitTests.Legacy` - ❌ Don't decrease code coverage - ❌ Don't introduce new warnings - ❌ Don't include commented-out code without explanation diff --git a/.cursorrules b/.cursorrules index 9e9ee1fd67..a0b2552fa7 100644 --- a/.cursorrules +++ b/.cursorrules @@ -35,7 +35,7 @@ Terminal.Gui v2 is a **complete rewrite**. Pre-2025 training data about Terminal dotnet restore dotnet build --no-restore dotnet test --project Tests/UnitTestsParallelizable --no-build -dotnet test --project Tests/UnitTests --no-build +dotnet test --project Tests/UnitTests.NonParallelizable --no-build ``` --- diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d919fa900e..110e73b118 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -42,16 +42,16 @@ dotnet build --no-restore # Run all tests (two separate projects) dotnet test --project Tests/UnitTestsParallelizable --no-build -dotnet test --project Tests/UnitTests --no-build +dotnet test --project Tests/UnitTests.NonParallelizable --no-build -# Run a single test by fully-qualified name -dotnet test --project Tests/UnitTestsParallelizable --no-build --filter "FullyQualifiedName~MyTestClass.MyTestMethod" +# Run a single test by method name (xUnit v3 / Microsoft Testing Platform) +dotnet test --project Tests/UnitTestsParallelizable --no-build --filter-method "*MyTestMethod" -# Run tests matching a trait or pattern -dotnet test --project Tests/UnitTestsParallelizable --no-build --filter "ClassName~ButtonTests" +# Run all tests in a class +dotnet test --project Tests/UnitTestsParallelizable --no-build --filter-class "*ButtonTests" ``` -New tests go in `Tests/UnitTestsParallelizable` (no static state dependencies). Only use `Tests/UnitTests` when testing `Application.Init`/`Shutdown` or other static state. +New tests go in `Tests/UnitTestsParallelizable` (no static state dependencies). Only use `Tests/UnitTests.NonParallelizable` when testing `Application.Init`/`Shutdown` or other static state. Never add new tests to `Tests/UnitTests.Legacy`. ## Architecture Overview @@ -191,10 +191,10 @@ All opening braces go on the next line. No exceptions. textField.TextChanged += (_, _) => { /* ... */ }; ``` -### Local functions use camelCase +### Local functions use PascalCase ```csharp -void myLocalFunc () { } +void MyLocalFunc () { } ``` ### Backing fields directly above their property diff --git a/.windsurfrules b/.windsurfrules index 726e393bb3..cf64252dd9 100644 --- a/.windsurfrules +++ b/.windsurfrules @@ -35,7 +35,7 @@ Terminal.Gui v2 is a **complete rewrite**. Pre-2025 training data about Terminal dotnet restore dotnet build --no-restore dotnet test --project Tests/UnitTestsParallelizable --no-build -dotnet test --project Tests/UnitTests --no-build +dotnet test --project Tests/UnitTests.NonParallelizable --no-build ``` --- diff --git a/AGENTS.md b/AGENTS.md index d1a9fee611..8afd31bc2a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,7 +84,7 @@ dotnet run **Terminal.Gui** - Cross-platform console UI toolkit for .NET (C# 14, net10.0) **Build:** `dotnet restore && dotnet build --no-restore` -**Test:** `dotnet test --project Tests/UnitTests --no-build && dotnet test --project Tests/UnitTestsParallelizable --no-build` +**Test:** `dotnet test --project Tests/UnitTestsParallelizable --no-build && dotnet test --project Tests/UnitTests.NonParallelizable --no-build` **Details:** [Build & Test Workflow](.claude/workflows/build-test-workflow.md) ### xUnit v3 Test Filtering (Microsoft Testing Platform) @@ -145,7 +145,7 @@ Process guides in `.claude/workflows/`: ## Planning Mode When creating implementation plans: -- **Create plan files in `./plans/`** (relative to repository root: `D:\s\gui-cs\Terminal.Gui\plans\`) +- **Create plan files in `./plans/`** (relative to the repository root) - Use markdown format with clear sections - Include: problem statement, implementation steps, file changes, verification steps - Reference existing patterns and reuse opportunities from exploration diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17d3cacac8..30a97e14ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,7 @@ Welcome! This guide provides everything you need to know to contribute effective 1. **Non-parallel tests** (depend on static state, ~10 min timeout): ```bash - dotnet test --project Tests/UnitTests --no-build --verbosity normal + dotnet test --project Tests/UnitTests.NonParallelizable --no-build --verbosity normal ``` - Uses `Application.Init` and static state - Cannot run in parallel From 65e3a6e74f664e29bde9ed73436d0f61a0d6beb9 Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 9 Jun 2026 17:53:08 -0600 Subject: [PATCH 2/5] Add agent docs lint, tuirec verification guidance, promote local memories - Add Scripts/lint-agent-docs.ps1 + lint-agent-docs.yml CI workflow: fails when known rot patterns reappear in agent instruction files (stale test project names, machine-local paths, camelCase local-function guidance, deprecated FullyQualifiedName~ filter syntax, SDK version drift vs global.json). The lint immediately caught a stale .NET SDK 8.0 claim in CONTRIBUTING.md, now fixed. - Wire tuirec into agent entry points (CLAUDE.md, AGENTS.md, build-app.md): agents can verify TUI behavior by recording with tuirec and reading the asciinema .cast output back as text, per Scripts/tuirec/README.md. - Promote durable guidance from machine-specific .claude/projects/ memory files into shared rules (.claude/rules/logging-tracing.md and fragile-areas.md); untrack .claude/projects/ and gitignore it (the directory name encoded a local user path). Co-Authored-By: Claude Fable 5 --- .../memory/feedback_no_console_writeline.md | 32 ----- .../memory/feedback_textview_fragile.md | 11 -- .claude/rules/fragile-areas.md | 11 ++ .claude/rules/logging-tracing.md | 35 +++++ .claude/tasks/build-app.md | 26 ++++ .github/workflows/lint-agent-docs.yml | 52 +++++++ .gitignore | 2 + AGENTS.md | 6 + CLAUDE.md | 12 ++ CONTRIBUTING.md | 4 +- Scripts/lint-agent-docs.ps1 | 128 ++++++++++++++++++ 11 files changed, 274 insertions(+), 45 deletions(-) delete mode 100644 .claude/projects/C--Users-Tig-s-gui-cs-Terminal-Gui/memory/feedback_no_console_writeline.md delete mode 100644 .claude/projects/C--Users-Tig-s-gui-cs-Terminal-Gui/memory/feedback_textview_fragile.md create mode 100644 .claude/rules/fragile-areas.md create mode 100644 .claude/rules/logging-tracing.md create mode 100644 .github/workflows/lint-agent-docs.yml create mode 100644 Scripts/lint-agent-docs.ps1 diff --git a/.claude/projects/C--Users-Tig-s-gui-cs-Terminal-Gui/memory/feedback_no_console_writeline.md b/.claude/projects/C--Users-Tig-s-gui-cs-Terminal-Gui/memory/feedback_no_console_writeline.md deleted file mode 100644 index 8ef99d694b..0000000000 --- a/.claude/projects/C--Users-Tig-s-gui-cs-Terminal-Gui/memory/feedback_no_console_writeline.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: No Console.WriteLine for debugging -description: Never use Console.Error.WriteLine or Console.WriteLine for debug tracing - use `Terminal.Gui.App.Logging`, `Terminal.Gui.Tests.TestLogging` and `Terminal.Gui.Tracing.Tracing.Trace` instead -type: feedback ---- - -Do not use Console.Error.WriteLine or Console.WriteLine for debug output in Terminal.Gui code. Use project's Logging infrastructure instead: `Terminal.Gui.App.Logging`, `Terminal.Gui.Tests.TestLogging` and `Terminal.Gui.Tracing.Tracing.Trace`. - -**Why:** User has explicitly corrected this twice. Console output interferes with the terminal UI framework. - -**How to apply:** When adding temporary debug tracing, use `TestLogging` and `Tracing.Trace` and the project's logging system. - -`Tracing.Trace` is only available in DEBUG builds; do not use it to validate test results as all tests must pass in RELEASE builds. - -```csharp -using Terminal.Gui.Tests; -using Terminal.Gui.Tracing; - -[Fact] -public void MyTest () -{ - // Enable logging and tracing in one call - using (TestLogging.Verbose (_output, TraceCategory.Command)) - { - // Logs and traces appear in xUnit test output - CheckBox checkbox = new () { Id = "test" }; - checkbox.InvokeCommand (Command.Activate); - } -} -``` - -See `./docfx/docs/logging.md` for full details. \ No newline at end of file diff --git a/.claude/projects/C--Users-Tig-s-gui-cs-Terminal-Gui/memory/feedback_textview_fragile.md b/.claude/projects/C--Users-Tig-s-gui-cs-Terminal-Gui/memory/feedback_textview_fragile.md deleted file mode 100644 index 6dc769d445..0000000000 --- a/.claude/projects/C--Users-Tig-s-gui-cs-Terminal-Gui/memory/feedback_textview_fragile.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: TextView is fragile - don't change -description: TextView EndInit ordering is fragile - changes cause cascading test failures in non-parallel UnitTests. Avoid modifying TextView. -type: feedback ---- - -Do not change TextView's EndInit ordering or initialization flow. Moving `base.EndInit()` before `UpdateContentSize()`/`UpdateScrollBars()` fixes some tests but breaks others in the non-parallel UnitTests project. - -**Why:** TextView has complex initialization dependencies and the non-parallel tests rely on specific ordering. The user explicitly said "TextView is fragile." - -**How to apply:** If TextView ContentSize tests fail, note the root cause (UpdateContentSize runs before IsInitialized is set) but do NOT fix it by reordering EndInit. File a separate issue or let the user decide when to address it. diff --git a/.claude/rules/fragile-areas.md b/.claude/rules/fragile-areas.md new file mode 100644 index 0000000000..d45f6fe7bd --- /dev/null +++ b/.claude/rules/fragile-areas.md @@ -0,0 +1,11 @@ +# Known Fragile Areas + +Areas of the codebase where seemingly-safe refactors cause cascading failures. Do not "fix" these in passing — file a separate issue instead. + +## TextView Initialization Ordering + +Do not change `TextView`'s `EndInit` ordering or initialization flow. Moving `base.EndInit ()` before `UpdateContentSize ()`/`UpdateScrollBars ()` fixes some tests but breaks others in the non-parallel test project. + +**Why:** `TextView` has complex initialization dependencies, and the non-parallel tests rely on specific ordering. + +**How to apply:** If `TextView` ContentSize tests fail, note the root cause (`UpdateContentSize` runs before `IsInitialized` is set) but do NOT fix it by reordering `EndInit`. File a separate issue or let a maintainer decide when to address it. diff --git a/.claude/rules/logging-tracing.md b/.claude/rules/logging-tracing.md new file mode 100644 index 0000000000..76592a08e2 --- /dev/null +++ b/.claude/rules/logging-tracing.md @@ -0,0 +1,35 @@ +# Logging and Tracing + +**Never use `Console.WriteLine` or `Console.Error.WriteLine` for debug output.** Console output interferes with the terminal UI framework. Use the project's logging infrastructure instead: + +| Need | Use | +|------|-----| +| Library logging | `Terminal.Gui.App.Logging` | +| Test output | `Terminal.Gui.Tests.TestLogging` | +| Debug tracing | `Terminal.Gui.Tracing.Trace` | + +## Test Pattern + +```csharp +using Terminal.Gui.Tests; +using Terminal.Gui.Tracing; + +[Fact] +public void MyTest () +{ + // Enable logging and tracing in one call + using (TestLogging.Verbose (_output, TraceCategory.Command)) + { + // Logs and traces appear in xUnit test output + CheckBox checkbox = new () { Id = "test" }; + checkbox.InvokeCommand (Command.Activate); + } +} +``` + +## Rules + +- `Tracing.Trace` is only available in DEBUG builds; do not use it to validate test results — all tests must pass in RELEASE builds. +- Remove temporary debug tracing before committing. + +See `docfx/docs/logging.md` for full details. diff --git a/.claude/tasks/build-app.md b/.claude/tasks/build-app.md index 7edf368e47..5e29ef5655 100644 --- a/.claude/tasks/build-app.md +++ b/.claude/tasks/build-app.md @@ -242,6 +242,31 @@ ConfigurationManager.Enable (ConfigLocations.All); Available themes: `Default`, `Dark`, `Light`, `Amber Phosphor`, `Green Phosphor`, `Blue Phosphor` +## Verify Your App Actually Works (Give Yourself Eyes) + +You cannot see a TUI from a build log. Before declaring an app done, **run it and observe it** with [`tuirec`](https://github.com/gui-cs/tuirec) — it spawns the app in a PTY, injects keystrokes, and records the terminal output: + +```powershell +dotnet build -c Release +$ks = 'wait:1000,Tab,Enter,wait:800,Escape' + +tuirec record ` + --binary dotnet ` + --args "./bin/Release/net10.0/MyApp.dll" ` + --name MyApp ` + --keystrokes $ks ` + --startup-delay 2000 --drain 1500 ` + --cols 120 --rows 30 +``` + +Then **verify the output yourself**: + +1. **Read `artifacts/MyApp.cast`** — it is asciinema v2 JSON (plain text). Inspect the frames to confirm the UI rendered what you expect (controls visible, focus moved, dialog appeared). +2. Grep the cast for failures: `Select-String -Path artifacts/MyApp.cast -Pattern "error|exception|usage:"`. +3. Check `artifacts/MyApp.gif` exists and is > 100KB (a blank recording is typically < 50KB). + +See [Scripts/tuirec/README.md](../../Scripts/tuirec/README.md) for the full keystroke syntax, validation checklist, and troubleshooting table. For in-process assertions (no PTY), use `InputInjector` and `VirtualTimeProvider` — see `docfx/docs/input-injection.md`. + ## Checklist for Building Apps - [ ] Project setup with correct packages @@ -250,6 +275,7 @@ Available themes: `Default`, `Dark`, `Light`, `Amber Phosphor`, `Green Phosphor` - [ ] Layout using Pos/Dim (not hardcoded positions) - [ ] `-ed` events (`Accepted`) for side effects; `-ing` events (`Accepting`) only to cancel - [ ] Proper cleanup with `Dispose` pattern +- [ ] Behavior verified by running the app (tuirec recording or input injection), not just by compiling ## What NOT to Do diff --git a/.github/workflows/lint-agent-docs.yml b/.github/workflows/lint-agent-docs.yml new file mode 100644 index 0000000000..3ae03c0569 --- /dev/null +++ b/.github/workflows/lint-agent-docs.yml @@ -0,0 +1,52 @@ +name: Lint Agent Docs + +# The AI agent instruction files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) drift +# from repo state because they are hand-maintained in parallel. This check fails +# when known rot patterns reappear (stale test project names, machine-local paths, +# contradictory style guidance, deprecated test filter syntax, wrong SDK versions). + +on: + push: + branches: [develop] + paths: + - 'CLAUDE.md' + - 'AGENTS.md' + - 'CONTRIBUTING.md' + - 'ai-v2-primer.md' + - 'llms.txt' + - '.cursorrules' + - '.windsurfrules' + - '.aider.md' + - '.github/copilot-instructions.md' + - '.claude/**' + - 'global.json' + - 'Scripts/lint-agent-docs.ps1' + - '.github/workflows/lint-agent-docs.yml' + pull_request: + paths: + - 'CLAUDE.md' + - 'AGENTS.md' + - 'CONTRIBUTING.md' + - 'ai-v2-primer.md' + - 'llms.txt' + - '.cursorrules' + - '.windsurfrules' + - '.aider.md' + - '.github/copilot-instructions.md' + - '.claude/**' + - 'global.json' + - 'Scripts/lint-agent-docs.ps1' + - '.github/workflows/lint-agent-docs.yml' + +jobs: + lint-agent-docs: + name: Lint Agent Docs + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Run agent docs lint + shell: pwsh + run: ./Scripts/lint-agent-docs.ps1 diff --git a/.gitignore b/.gitignore index 5bc2282ed6..022de0dfcb 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,8 @@ log.* # Claude Code local settings .claude/settings.local.json .claude/worktrees/ +# Session-local agent memory (machine-specific paths) - promote durable guidance to .claude/rules/ instead +.claude/projects/ .mcp.json .env tmpclaude-*-cwd diff --git a/AGENTS.md b/AGENTS.md index 8afd31bc2a..6549da7122 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -134,6 +134,8 @@ Consult these files in `.claude/rules/` before editing code: - [Code Layout](/.claude/rules/code-layout.md) - Member ordering, backing fields - [Testing Patterns](/.claude/rules/testing-patterns.md) - Test writing conventions - [API Documentation](/.claude/rules/api-documentation.md) - XML doc requirements +- [Logging & Tracing](/.claude/rules/logging-tracing.md) - No Console.WriteLine; use Logging/TestLogging/Trace +- [Fragile Areas](/.claude/rules/fragile-areas.md) - Code that must not be refactored in passing ## Workflows @@ -142,6 +144,10 @@ Process guides in `.claude/workflows/`: - [Build & Test Workflow](/.claude/workflows/build-test-workflow.md) - Build, test, and troubleshooting - [PR Workflow](/.claude/workflows/pr-workflow.md) - Submitting pull requests +## Visual Verification (Agent Eyes) + +Don't ship UI changes blind. Use [`tuirec`](https://github.com/gui-cs/tuirec) to run any Terminal.Gui app in a PTY, inject keystrokes, and capture the result — see [Scripts/tuirec/README.md](Scripts/tuirec/README.md). The `.cast` output is asciinema v2 JSON (plain text): read it back to verify what actually rendered. The `.gif` is for humans — attach it to PRs that change visuals. For deterministic in-process assertions, use `InputInjector`/`VirtualTimeProvider` (`docfx/docs/input-injection.md`). + ## Planning Mode When creating implementation plans: diff --git a/CLAUDE.md b/CLAUDE.md index 4b20ed7067..540e52f30b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,7 @@ Terminal.Gui v2 is a **complete rewrite**. Pre-2025 training data is **wrong**. | **"Build me an app that..."** | [.claude/tasks/build-app.md](.claude/tasks/build-app.md) | | **"Add a feature to Terminal.Gui..."** | Continue below (Contributor Guide) | | **"Fix a bug in Terminal.Gui..."** | Continue below (Contributor Guide) | +| **"Record a GIF / verify a UI change..."** | [Scripts/tuirec/README.md](Scripts/tuirec/README.md) | ### App Builder Quick Start ```bash @@ -60,6 +61,8 @@ See `.claude/rules/` for detailed guidance: - `code-layout.md` - Backing fields, member ordering - `api-documentation.md` - XML documentation requirements - `testing-patterns.md` - Test patterns and requirements +- `logging-tracing.md` - **No Console.WriteLine** - use Logging/TestLogging/Trace +- `fragile-areas.md` - Code that must not be refactored in passing (TextView init) ## Task-Specific Guides @@ -109,6 +112,15 @@ dotnet test --project Tests/UnitTestsParallelizable --no-build --filter-class "* See `Tests/README.md` for the full list of test projects (including `IntegrationTests`, `StressTests`, `Benchmarks`) and the static-state classification that determines where a new test belongs. +## Seeing Your Changes (Visual Verification) + +Agents can observe a running Terminal.Gui app — don't ship UI changes blind. Use [`tuirec`](https://github.com/gui-cs/tuirec) to run the app in a PTY, inject keystrokes, and capture the result: + +- **Full guide:** [Scripts/tuirec/README.md](Scripts/tuirec/README.md) — install, keystroke syntax, UICatalog scenario recipes, validation checklist +- The `.cast` output is asciinema v2 JSON (plain text) — **read it back** to verify what actually rendered, frame by frame +- The `.gif` output is for humans — attach it to PRs that change visuals +- For deterministic in-process assertions, use `InputInjector`/`VirtualTimeProvider` (see `docfx/docs/input-injection.md`) and driver `ToString ()` screen captures + ## Key Concepts | Concept | Documentation | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 30a97e14ec..003f768914 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,8 +47,8 @@ Welcome! This guide provides everything you need to know to contribute effective ### Required Tools -- **.NET SDK**: 8.0.0 (see `global.json`) -- **Runtime**: .NET 8.x (latest GA) +- **.NET SDK**: 10.0.100 (see `global.json`) +- **Runtime**: .NET 10.x (latest GA) - **Optional**: ReSharper/Rider for code formatting (honor `.editorconfig` and `Terminal.sln.DotSettings`) ### Build Commands (In Order) diff --git a/Scripts/lint-agent-docs.ps1 b/Scripts/lint-agent-docs.ps1 new file mode 100644 index 0000000000..208b3f9fc3 --- /dev/null +++ b/Scripts/lint-agent-docs.ps1 @@ -0,0 +1,128 @@ +<# +.SYNOPSIS + Lints the AI agent instruction files for staleness and contradictions. + +.DESCRIPTION + The agent instruction files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) are + maintained in parallel and drift from the repo state and from each other. + This script fails CI when known rot patterns reappear: + + 1. References to test projects that no longer exist (bare Tests/UnitTests) + 2. Machine-local absolute paths (D:\..., C:\Users\..., /home/...) + 3. camelCase local-function guidance (the .editorconfig rule is PascalCase) + 4. Deprecated xUnit v2 test filter syntax (--filter "FullyQualifiedName~") + 5. .NET SDK version claims that do not match global.json + +.EXAMPLE + pwsh -File Scripts/lint-agent-docs.ps1 +#> + +$ErrorActionPreference = 'Stop' +$repoRoot = Split-Path -Parent $PSScriptRoot + +# The set of files that instruct AI agents. Keep in sync with the files listed +# at the top of CLAUDE.md and AGENTS.md. +$agentDocPatterns = @( + 'CLAUDE.md' + 'AGENTS.md' + 'CONTRIBUTING.md' + 'ai-v2-primer.md' + 'llms.txt' + '.cursorrules' + '.windsurfrules' + '.aider.md' + '.github/copilot-instructions.md' +) + +$files = [System.Collections.Generic.List[string]]::new() + +foreach ($pattern in $agentDocPatterns) +{ + $path = Join-Path $repoRoot $pattern + + if (Test-Path $path) + { + $files.Add($path) + } +} + +# All markdown under .claude/ (rules, tasks, workflows, cookbook). +Get-ChildItem -Path (Join-Path $repoRoot '.claude') -Filter '*.md' -Recurse -File | + ForEach-Object { $files.Add($_.FullName) } + +$failures = [System.Collections.Generic.List[string]]::new() + +function Add-Failure +{ + param ([string] $File, [int] $Line, [string] $Rule, [string] $Text) + + $relative = [System.IO.Path]::GetRelativePath($repoRoot, $File) + $failures.Add("${relative}:${Line}: [$Rule] $Text") +} + +# Rule 5 needs the pinned SDK version. +$globalJson = Get-Content (Join-Path $repoRoot 'global.json') -Raw | ConvertFrom-Json +$sdkVersion = $globalJson.sdk.version +$sdkMajor = $sdkVersion.Split('.')[0] + +foreach ($file in $files) +{ + $lines = Get-Content $file + $lineNumber = 0 + + foreach ($line in $lines) + { + $lineNumber++ + + # Rule 1: stale test project names. Valid projects: UnitTestsParallelizable, + # UnitTests.NonParallelizable, UnitTests.Legacy. + if ($line -match 'Tests/UnitTests(?!Parallelizable|\.NonParallelizable|\.Legacy)') + { + Add-Failure $file $lineNumber 'stale-test-project' "bare 'Tests/UnitTests' no longer exists; use Tests/UnitTestsParallelizable or Tests/UnitTests.NonParallelizable" + } + + # Rule 2: machine-local absolute paths. + if ($line -match '[A-Za-z]:\\' -or $line -match '(? Date: Tue, 9 Jun 2026 18:30:26 -0600 Subject: [PATCH 3/5] Document Windows ConPTY sixel limitation in tuirec README Discovered while verifying the About Box fire animation: ConPTY strips sixel DCS and the DA1 sixel handshake, so sixel content cannot be captured in tuirec recordings on Windows (apps detect Sixel support: False). Added to the troubleshooting table and validation checklist so the next agent does not burn recordings rediscovering it. Co-Authored-By: Claude Fable 5 --- Scripts/tuirec/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Scripts/tuirec/README.md b/Scripts/tuirec/README.md index 08537e5c03..4bb13ceda0 100644 --- a/Scripts/tuirec/README.md +++ b/Scripts/tuirec/README.md @@ -271,6 +271,11 @@ After every recording, verify: ``` Examples/UICatalog/Scenarios//.gif ``` +- [ ] **Sixel content recorded on Linux/macOS** — sixel DCS cannot be captured + through Windows ConPTY. Confirm sixel made it into the cast with: + ```powershell + Select-String -Path artifacts/.cast -Pattern 'u001bP' | Measure-Object + ``` --- @@ -278,6 +283,7 @@ After every recording, verify: | Problem | Cause | Fix | |---------|-------|-----| +| No sixel output on Windows | **Windows ConPTY strips sixel DCS** and does not pass through the DA1 sixel handshake — the app detects `Sixel support: False` | Record sixel content on Linux/macOS (see `tuirec agent-guide`). On Windows you can still verify the app's sixel code path runs (e.g., via an app-level force flag) by checking redraw activity in the `.cast`, but flame/image pixels will not appear | | Wide glyphs misaligned in GIF | Emoji/CJK chars are 2-cell wide; agg renders per-cell | Avoid emoji/CJK categories; use single-width ranges (Arrows, Box Drawing, etc.) | | Nav keys ignored with `--kitty-keyboard` | tuirec bug [#54](https://github.com/gui-cs/tuirec/issues/54) — sends wrong codepoints | Remove `--kitty-keyboard` | | App doesn't quit | Wrong quit key or key not delivered | Use `Escape` (the default quit key); check `--kitty-keyboard` interaction | From 3e62ecbb00cdc2bd179a581483ae844b1cc41c2c Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 9 Jun 2026 19:36:54 -0600 Subject: [PATCH 4/5] tuirec README: require measuring grid-anchored sixels, not eyeballing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents (including me) recurrently verify the wrong invariant when checking sixel recordings: confirming the sixel appears, or that agg rendered it faithfully at the requested cursor cell, and calling it done. That misses size/position errors — notably the ~4% undersize from tuirec advertising a cell resolution that does not match agg's rendered font cell (tuirec #84). Adds a "Verifying Placement and Size (measure - don't eyeball)" section with the cell-calibration recipe (measure agg's real cell from a known grid reference; reconcile against the resolution the app used; confirm the rendered bbox covers the target region), a checklist item, a troubleshooting row for #84, and rewrites the workflow's "visual confirm" step to require measurement for grid-anchored content. Also states the general principle: verify the invariant the change was meant to satisfy, not a proxy. Co-Authored-By: Claude Fable 5 --- Scripts/tuirec/README.md | 61 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/Scripts/tuirec/README.md b/Scripts/tuirec/README.md index 4bb13ceda0..1c11fb5ac1 100644 --- a/Scripts/tuirec/README.md +++ b/Scripts/tuirec/README.md @@ -252,6 +252,56 @@ tuirec record ` --- +## Verifying Placement and Size (measure — don't eyeball) + +**The recurring trap.** Confirming a sixel *appears* in the GIF — or that agg +rendered it faithfully at the cursor cell the app requested — does **not** prove +it is correct. A pixel-perfect render of a raster that was *built from the wrong +cell size* is still the wrong size on screen. "Looks present" and "pipeline is +faithful" are proxies. The invariant you must actually check is: + +> **Does the rendered sixel cover the cells the app intended — in both position +> and size?** + +Verify that with a measurement, not your eyes. A ~4% size error is invisible by +sight and obvious by arithmetic. + +**Why this bites with tuirec specifically.** tuirec advertises a sixel cell +resolution (e.g. `8×17` px) that does **not** match agg's actual rendered font +cell (~`8.3×18.8` px at the default `--font-size 14`) — see +[#84](https://github.com/gui-cs/tuirec/issues/84). An app that *correctly* sizes +its raster as `cells × reportedResolution` (and fills exactly on a real sixel +terminal) therefore renders ~4% **undersized** under tuirec. Do not "fix" the app +for this; verify it and attribute it correctly. + +**The check — calibrate agg's real cell, then reconcile:** + +1. Extract a frame from the GIF (any decoder; e.g. ImageSharp + `Image.Load(gif).Frames.CloneFrame(i)`). +2. Measure agg's **actual** cell size from a *known* grid reference — e.g. a + border/box that spans a known number of columns: + `cellPx = borderSpanPx / (spannedCells)`. (Don't trust `imageWidth / cols` — + agg adds margins.) +3. Read the resolution the **app** used from the cast: the sixel DCS header + `P…q"asp;asp;WIDTH;HEIGHT` gives raster pixel size; divide by the raster's + cell count to get the app's px-per-cell. +4. **If agg's measured cell ≠ the app's px-per-cell, the sixel is mis-sized — + and that is the tuirec mismatch ([#84](https://github.com/gui-cs/tuirec/issues/84)), + not an app bug.** Then confirm the sixel's rendered bounding box actually spans + the target region (the columns/rows it was meant to cover), not merely that it + exists. + +Run this whenever a sixel is sized or aligned to the text grid (bordered image +views, insets, bottom bands — anything grid-anchored). It turns "I think it looks +right" into a number, which is the only thing that catches sub-cell and +few-percent errors. + +> **General principle (applies beyond sixel):** verify the *invariant the change +> was supposed to satisfy*, measured against the design intent — not that the tool +> ran, the file is non-empty, or the screenshot "has the thing in it." When you've +> just fixed one symptom, the next bug often hides in the dimension you didn't +> measure. + ## Validation Checklist After every recording, verify: @@ -276,6 +326,11 @@ After every recording, verify: ```powershell Select-String -Path artifacts/.cast -Pattern 'u001bP' | Measure-Object ``` +- [ ] **Grid-anchored sixel measured, not eyeballed** — if the sixel is sized or + aligned to the text grid, calibrate agg's real cell and confirm the rendered + bbox covers the target columns/rows (see *Verifying Placement and Size* + above). A ~4% undersize from [#84](https://github.com/gui-cs/tuirec/issues/84) + is invisible by sight. --- @@ -284,6 +339,7 @@ After every recording, verify: | Problem | Cause | Fix | |---------|-------|-----| | No sixel output on Windows | **Windows ConPTY strips sixel DCS** and does not pass through the DA1 sixel handshake — the app detects `Sixel support: False` | Record sixel content on Linux/macOS (see `tuirec agent-guide`). On Windows you can still verify the app's sixel code path runs (e.g., via an app-level force flag) by checking redraw activity in the `.cast`, but flame/image pixels will not appear | +| Sixel renders ~4% too small / short of a border | tuirec advertises a cell resolution that doesn't match agg's rendered font cell ([#84](https://github.com/gui-cs/tuirec/issues/84)) | App is correct (fills on a real terminal). Verify by measurement (see *Verifying Placement and Size*); attribute to tuirec, not the app. Until fixed, only a tuirec-specific over-render hack would close the gap | | Wide glyphs misaligned in GIF | Emoji/CJK chars are 2-cell wide; agg renders per-cell | Avoid emoji/CJK categories; use single-width ranges (Arrows, Box Drawing, etc.) | | Nav keys ignored with `--kitty-keyboard` | tuirec bug [#54](https://github.com/gui-cs/tuirec/issues/54) — sends wrong codepoints | Remove `--kitty-keyboard` | | App doesn't quit | Wrong quit key or key not delivered | Use `Escape` (the default quit key); check `--kitty-keyboard` interaction | @@ -304,7 +360,10 @@ When asked to record a scenario GIF: 3. **Read `GetDemoKeyStrokes()`** — find it in the scenario source file 4. **Compose keystrokes** — translate to tuirec syntax, add waits, keep short 5. **Record** — `tuirec record --binary ... --args "run," --keystrokes $ks ...` -6. **Validate** — error-grep the cast, check GIF file size, visual confirm +6. **Validate** — error-grep the cast, check GIF file size, confirm the interaction + played. For anything sized/aligned to the grid (sixels especially), + **measure** placement and size against the design intent — do not stop at + "the screenshot has the thing in it" (see *Verifying Placement and Size*). 7. **If nav keys fail** — remove `--kitty-keyboard` and retry 8. **Report** — share the output paths and exact command used From 583eeab71790e9d5bfc2b3af03231f155317d059 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 10 Jun 2026 08:12:45 -0600 Subject: [PATCH 5/5] Sync CONTRIBUTING.md TFM to net10.0; lint stale TFMs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review on #5478 flagged that build-test-workflow.md cites CONTRIBUTING.md as its source of truth, but CONTRIBUTING.md still described the project as net8.0 (line 28) even though the Required Tools section was updated to .NET 10 — so the declared source could revert the fix and mislead readers onto the wrong toolchain. Test-first: added Rule 6 to lint-agent-docs.ps1 that derives the expected target-framework moniker from global.json's SDK major and fails on any mismatched `net.0` reference. It flagged CONTRIBUTING.md:28 (net8.0); fixed that to `C# 14 (net10.0)`. Lint now passes (32 files). Co-Authored-By: Claude Fable 5 --- CONTRIBUTING.md | 2 +- Scripts/lint-agent-docs.ps1 | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 003f768914..4977b95e1a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ Welcome! This guide provides everything you need to know to contribute effective **Terminal.Gui** is a cross-platform UI toolkit for creating console-based graphical user interfaces in .NET. It's a large codebase (~1,050 C# files) providing a comprehensive framework for building interactive console applications with support for keyboard and mouse input, customizable views, and a robust event system. **Key characteristics:** -- **Language**: C# (net8.0) +- **Language**: C# 14 (net10.0) - **Platform**: Cross-platform (Windows, macOS, Linux) - **Architecture**: Console UI toolkit with driver-based architecture - **Version**: v2 (Beta), v1 (maintenance mode) diff --git a/Scripts/lint-agent-docs.ps1 b/Scripts/lint-agent-docs.ps1 index 208b3f9fc3..ed42c8be13 100644 --- a/Scripts/lint-agent-docs.ps1 +++ b/Scripts/lint-agent-docs.ps1 @@ -110,6 +110,18 @@ foreach ($file in $files) Add-Failure $file $lineNumber 'sdk-version' "claims .NET SDK $claimed but global.json pins $sdkVersion" } } + + # Rule 6: target framework moniker must match global.json's SDK major. + # Catches stale `net8.0`-style TFMs left behind when the project moved on. + if ($line -match 'net(\d+)\.0(?![\d.])') + { + $tfmMajor = $Matches[1] + + if ($tfmMajor -ne $sdkMajor) + { + Add-Failure $file $lineNumber 'stale-tfm' "references net$tfmMajor.0 but global.json pins .NET $sdkMajor (expected net$sdkMajor.0)" + } + } } }