This is the Tiger project — a CLI tool for managing CI/CD infrastructure.
Work through docs/todo.md one item at a time:
- Implement the change for the current item
- Stop and present the changes for human review — do NOT commit or move on
- Only after explicit approval: commit and proceed to the next item
Rules:
- Do not commit without explicit approval.
- Do not delete
tiger.dbwithout explicit approval.
Read docs/architecture.md for full context on:
- Project structure and naming conventions
- Single-process architecture (CLI + poller + MCP/HTTP server)
- SQLite schema design (multi-org aware)
- Configuration format
- Authentication approach
- Technology stack
Read docs/todo.md for the current work item checklist.
When changing the SQLite database schema (adding/removing/renaming tables or columns) or
adding/changing valid values for existing columns (e.g. new status values), you must
update the corresponding SKILL.md files under src/Tiger/skills/ to stay in sync.
The tiger-data/SKILL.md file documents the full schema and is used by MCP skills to
query the database correctly.
When adding, removing, or renaming tiger azdo or tiger helix CLI subcommands, you
must update src/Tiger/skills/tiger-cli/SKILL.md to keep the command tables in sync.
This skill file is how MCP agents discover available CLI commands for querying live
AzDO and Helix data.
- The
tiger azdoandtiger helixsubcommands are designed for agent consumption, not human use. They must produce structured JSON output (viaJsonSerializer.SerializewithJsonOptions.Indented). Do not use Spectre.Console tables, color markup, or interactive prompts in these commands. - The interactive dashboard (
tiger dashboard) is for human use and uses Spectre.Console for rich UI.
- Projects/namespaces use PascalCase:
Tiger,Tiger.Core - User-facing artifacts use lowercase:
tiger.exe,~/.tiger/ - Target framework:
net10.0 - All SQLite tables are keyed by
(organization, project)for multi-org support - GitHub operations use the user's
ghCLI auth - AzDO auth uses
DefaultAzureCredential
When modifying code that has associated tests (check src/Tiger.Tests/), you must
update or add tests to cover your changes. Run dotnet test --nologo to verify all tests
pass before presenting changes for review.
Do not use Assert.Contains to spot-check fragments of output. Every test must assert
against the complete expected value using Assert.Equal with a raw string literal so the
full behavior is visible and regressions are caught precisely.
- For single-value results:
Assert.Equal("expected value", actual); - For multi-line output: use a raw string literal (
"""...""") as the expected value andAssert.Equal(expected, actual)(withignoreLineEndingDifferences: trueif needed). - For list results: join lines and compare the full string, or assert each element by index.
Never use
Assert.Contains(collection, predicate)to search for a matching element.
This applies to all test classes — not just UI rendering tests.
Tests that validate UI rendering must render through IAnsiConsole (using TestConsole
with .EmitAnsiSequences()) and assert against the actual console output. Do not use
intermediate buffers or capture mechanisms — test what is actually rendered.
Use a single raw string literal for the expected value, written in Spectre markup form
so the formatting intent is visible at a glance ([bold], [red], [dim], etc.). The helper
MarkupToAnsi() converts this readable markup to the ANSI-encoded string that TestConsole
produces, enabling exact comparison.
Use StripChrome() to remove terminal chrome (clear screen, cursor hide/show) and normalize
non-deterministic OSC hyperlink IDs. Use ReplaceLineEndings("\n") since TestConsole uses
\n internally when emitting ANSI sequences.
var console = new TestConsole().EmitAnsiSequences().Width(80).Height(30);
var renderer = new PanelRenderer(console);
var content = new List<string>
{
PanelRenderer.FormatField("Status", "Complete"),
"Build failed due to missing package",
};
renderer.RenderDetailPanel(["Analysis", "Build #123"], null, content, "[blue]Esc[/] Back");
var actual = PanelRendererTests.StripChrome(console.Output).ReplaceLineEndings("\n").Trim();
var expected = """
[dim]╔══════════════════════════════════════════════════════════════════════════════╗[/]
[dim]║[/] [bold orange1]TIGER[/] [dim]>[/] Analysis > Build #123 [dim]║[/]
[dim]╠══════════════════════════════════════════════════════════════════════════════╣[/]
[dim]║[/] [bold]Status:[/] Complete [dim]║[/]
[dim]║[/] Build failed due to missing package [dim]║[/]
[dim]║[/] [dim]║[/]
[dim]╠══════════════════════════════════════════════════════════════════════════════╣[/]
[dim]║[/] [blue]Esc[/] Back [dim]║[/]
[dim]╚══════════════════════════════════════════════════════════════════════════════╝[/]
""";
Assert.Equal(PanelRendererTests.MarkupToAnsi(expected.ReplaceLineEndings("\n").Trim()), actual);Markup conventions in expected strings:
- Border lines (
╔═╗,╠═╣,╚═╝): wrap entire line with[dim]...[/] - Content lines:
[dim]║[/] content padding [dim]║[/] - Empty lines:
[dim]║[/]+ spaces +[dim]║[/] - Title pane:
[dim]║[/] [bold orange1]TIGER[/] [dim]>[/] Breadcrumbs padding [dim]║[/] - Command pane hotkeys:
[dim]║[/] [blue]Esc[/] Back padding [dim]║[/] - Content markup stays as-is:
[bold]Label:[/],[red]X[/],[bold underline]Section[/]
Do not use Assert.Contains for UI tests — always compare the full expected output.
The raw string literal shows exactly what formatting and layout the UI produces.
The dashboard UI is a three-pane grid layout:
+---------------------------------------------------------------+
| TIGER > Builds > Build #123 | <- Title Pane
+---------------------------------------------------------------+
| |
| Status: Failed |
| Branch: main | <- Content Pane
| Duration: 5m 32s |
| |
+---------------------------------------------------------------+
| [B]uilds [T]ests [H]ealth [Esc] Back | <- Command Pane
+---------------------------------------------------------------+
- Title pane: The top row showing the
TIGER ▸ Section ▸ Subsectionbreadcrumb trail. - Content pane: The middle area displaying list selections or detail views.
- Command pane: The bottom row showing hotkey commands and navigation actions.
Use these terms when referring to UI regions in issues, code comments, and discussions.
All screens use PanelLayout for a consistent "command and control" look:
- Title pane:
TIGER ▸ Section ▸ Subsectionbreadcrumb trail - Content pane: List selection or detail view
- Command pane: Bottom bar with hotkey commands, focusable via Tab
The command pane is the standard way to expose actions on any screen. It uses List<CommandBarItem> where each item has a label, hotkey, and return value.
- Hotkey display: Bracket style —
[B]uilds [T]ests [H]ealth— with the bracketed letter rendered in blue. - Focus model: Tab toggles focus between the content pane and the command pane. When the command pane is focused, ←→ moves the highlight and Enter executes. Hotkey letters work regardless of focus.
- Focused item: Shown as
[bold white on blue] Label [/](inverted highlight). - Main menu: Uses
PanelLayout.ShowMainMenu(commands)— displays Figlet ASCII art + TIGER branding in the content pane, navigation only via command pane. - List screens: Use
PanelLayout.SelectInPanel(..., commands)— list in content pane + Tab-focusable command pane. - Detail screens: Use
PanelLayout.RenderDetailPanel(...)with a static hotkey string footer (detail views handle their own key loops).
- Hotkeys use the
[X]bracket format in labels (e.g.,[E]dit filter,[R]efresh) - The bracket letter is highlighted in blue via Spectre markup:
[blue][[X]][/] - Escape always means "go back" or "cancel"
- Tab always switches focus to the command pane (on list screens)
DashboardCommand.cs— main menu (ShowMainMenu)BuildBrowser.cs— build list command bar, empty-list detail, filter menu, filter help, build detail, test failures, timeline issuesTestBrowser.cs— test list command bar, empty-list detail, filter menu, test detailAnalysisBrowser.cs— analysis list command bar, analysis detail menu, full log menuAgentBrowser.cs— agent list command bar, agent detail menuHealthCommand.cs— health list command bar, runs list command bar, state page menu, run detailConfigEditor.cs— config menu (still usesBrowserUI.SelectWithEscape)
- if/try/catch bodies and braces must be on separate lines — never on the same line as the keyword.
- Escape always means "go back" or "cancel" in any interactive context.
- Regular expressions must use
[GeneratedRegex]source generators instead ofnew Regex(...)orRegex.Match(...). This provides compile-time validation, better performance, and avoids runtime compilation. The containing class must bepartialto support the generated code. - Branch names displayed in the UI must be simplified using
BrowserUI.SimplifyBranch()to striprefs/heads/andrefs/pull/prefixes (e.g.refs/heads/main→main).