From 98217bd43bd5e0193c5f0243daad8f7cd95fa3aa Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 12:57:37 +0000 Subject: [PATCH 1/6] Adopt the squad from Squad-Spec-Repository-Template The squad, its skills for Claude, Codex/GPT and Copilot and the shared agent rules now come from the template (dotnet profile); fix-issue, publish-pr and rereview-pr are replaced by squad-issue, create-pr and review-pr. Project knowledge moved into .squad/stack.md, .squad/project.md and the project blocks of the instruction files, docs/CONTRIBUTING.md, docs/ARCHITECTURE.md (moved from the repository root), the bug report and the pull request template. Three process decision records are added, the coverage gate excludes tests and generated migrations, and SonarQube's coverage measure excludes the squad tooling. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014xZzHGWbMaKfnCWeA4Lgj1 --- .agents/skills/create-pr/SKILL.md | 151 +++++++++ .agents/skills/fix-issue/SKILL.md | 76 ----- .agents/skills/publish-pr/SKILL.md | 45 --- .agents/skills/rereview-pr/SKILL.md | 102 ------ .agents/skills/review-pr/SKILL.md | 202 ++++++------ .agents/skills/squad-issue/SKILL.md | 176 +++++++++++ .agents/skills/squad-spec/SKILL.md | 31 ++ .claude/agents/squad-code-officer.md | 28 ++ .claude/agents/squad-dev.md | 36 +++ .claude/agents/squad-devils-advocate.md | 40 +++ .claude/agents/squad-lead.md | 84 +++++ .claude/agents/squad-reviewer.md | 197 ++++++++++++ .claude/agents/squad-security.md | 31 ++ .claude/agents/squad-tester.md | 34 ++ .claude/hooks/session-start.sh | 33 ++ .claude/settings.json | 14 + .claude/skills/create-pr/SKILL.md | 151 +++++++++ .claude/skills/fix-issue/SKILL.md | 76 ----- .claude/skills/publish-pr/SKILL.md | 45 --- .claude/skills/rereview-pr/SKILL.md | 102 ------ .claude/skills/review-pr/SKILL.md | 202 ++++++------ .claude/skills/squad-issue/SKILL.md | 176 +++++++++++ .claude/skills/squad-spec/SKILL.md | 31 ++ .github/ISSUE_TEMPLATE/bug_report.md | 32 +- .github/ISSUE_TEMPLATE/feature_request.md | 6 +- .github/copilot-instructions.md | 284 ++++++++++++----- ...T_TEMPLATE.md => pull_request_template.md} | 18 +- .github/skills/create-pr/SKILL.md | 151 +++++++++ .github/skills/review-pr/SKILL.md | 117 +++++++ .github/skills/squad-issue/SKILL.md | 176 +++++++++++ .github/skills/squad-spec/SKILL.md | 31 ++ .github/workflows/ci.yml | 3 +- .squad/agents/code-officer/charter.md | 20 ++ .squad/agents/code-officer/history.md | 4 + .squad/agents/dev/charter.md | 21 ++ .squad/agents/dev/history.md | 4 + .squad/agents/devils-advocate/charter.md | 12 + .squad/agents/devils-advocate/history.md | 4 + .squad/agents/lead/charter.md | 37 +++ .squad/agents/lead/history.md | 4 + .squad/agents/reviewer/charter.md | 10 + .squad/agents/reviewer/history.md | 4 + .squad/agents/security/charter.md | 11 + .squad/agents/security/history.md | 4 + .squad/agents/tester/charter.md | 18 ++ .squad/agents/tester/history.md | 4 + .squad/decisions.md | 10 + .squad/project.md | 85 +++++ .squad/routing.md | 160 ++++++++++ .squad/stack.md | 101 ++++++ .squad/team.md | 47 +++ .squad/template.json | 5 + .squad/tools/.gitignore | 1 + .squad/tools/analyzer-check.py | 118 +++++++ .squad/tools/config-check.py | 172 ++++++++++ .squad/tools/coverage-check.py | 184 +++++++++++ .squad/tools/squad_settings.py | 11 + AGENTS.md | 293 +++++++++++++----- CLAUDE.md | 293 +++++++++++++----- ARCHITECTURE.md => docs/ARCHITECTURE.md | 53 +++- docs/CONTRIBUTING.md | 66 +++- docs/UNIT_TESTS.md | 22 +- ...1-quality-gates-before-the-pull-request.md | 38 +++ .../0002-squash-merge-pull-requests.md | 31 ++ .../0003-squad-working-records-off-main.md | 35 +++ docs/decisions/README.md | 33 ++ docs/decisions/_template.md | 25 ++ specs/README.md | 13 + specs/_template/log.md | 6 + specs/_template/plan.md | 57 ++++ specs/_template/spec.md | 19 ++ specs/_template/tasks.md | 7 + 72 files changed, 4001 insertions(+), 922 deletions(-) create mode 100644 .agents/skills/create-pr/SKILL.md delete mode 100644 .agents/skills/fix-issue/SKILL.md delete mode 100644 .agents/skills/publish-pr/SKILL.md delete mode 100644 .agents/skills/rereview-pr/SKILL.md create mode 100644 .agents/skills/squad-issue/SKILL.md create mode 100644 .agents/skills/squad-spec/SKILL.md create mode 100644 .claude/agents/squad-code-officer.md create mode 100644 .claude/agents/squad-dev.md create mode 100644 .claude/agents/squad-devils-advocate.md create mode 100644 .claude/agents/squad-lead.md create mode 100644 .claude/agents/squad-reviewer.md create mode 100644 .claude/agents/squad-security.md create mode 100644 .claude/agents/squad-tester.md create mode 100755 .claude/hooks/session-start.sh create mode 100644 .claude/settings.json create mode 100644 .claude/skills/create-pr/SKILL.md delete mode 100644 .claude/skills/fix-issue/SKILL.md delete mode 100644 .claude/skills/publish-pr/SKILL.md delete mode 100644 .claude/skills/rereview-pr/SKILL.md create mode 100644 .claude/skills/squad-issue/SKILL.md create mode 100644 .claude/skills/squad-spec/SKILL.md rename .github/{PULL_REQUEST_TEMPLATE.md => pull_request_template.md} (65%) create mode 100644 .github/skills/create-pr/SKILL.md create mode 100644 .github/skills/review-pr/SKILL.md create mode 100644 .github/skills/squad-issue/SKILL.md create mode 100644 .github/skills/squad-spec/SKILL.md create mode 100644 .squad/agents/code-officer/charter.md create mode 100644 .squad/agents/code-officer/history.md create mode 100644 .squad/agents/dev/charter.md create mode 100644 .squad/agents/dev/history.md create mode 100644 .squad/agents/devils-advocate/charter.md create mode 100644 .squad/agents/devils-advocate/history.md create mode 100644 .squad/agents/lead/charter.md create mode 100644 .squad/agents/lead/history.md create mode 100644 .squad/agents/reviewer/charter.md create mode 100644 .squad/agents/reviewer/history.md create mode 100644 .squad/agents/security/charter.md create mode 100644 .squad/agents/security/history.md create mode 100644 .squad/agents/tester/charter.md create mode 100644 .squad/agents/tester/history.md create mode 100644 .squad/decisions.md create mode 100644 .squad/project.md create mode 100644 .squad/routing.md create mode 100644 .squad/stack.md create mode 100644 .squad/team.md create mode 100644 .squad/template.json create mode 100644 .squad/tools/.gitignore create mode 100644 .squad/tools/analyzer-check.py create mode 100644 .squad/tools/config-check.py create mode 100644 .squad/tools/coverage-check.py create mode 100644 .squad/tools/squad_settings.py rename ARCHITECTURE.md => docs/ARCHITECTURE.md (91%) create mode 100644 docs/decisions/0001-quality-gates-before-the-pull-request.md create mode 100644 docs/decisions/0002-squash-merge-pull-requests.md create mode 100644 docs/decisions/0003-squad-working-records-off-main.md create mode 100644 docs/decisions/README.md create mode 100644 docs/decisions/_template.md create mode 100644 specs/README.md create mode 100644 specs/_template/log.md create mode 100644 specs/_template/plan.md create mode 100644 specs/_template/spec.md create mode 100644 specs/_template/tasks.md diff --git a/.agents/skills/create-pr/SKILL.md b/.agents/skills/create-pr/SKILL.md new file mode 100644 index 0000000..4e48dce --- /dev/null +++ b/.agents/skills/create-pr/SKILL.md @@ -0,0 +1,151 @@ +--- +name: create-pr +description: Use when the user asks to open/create a pull request for changes on this branch. Runs local verification (format, build, test), reviews the change with the squad-reviewer subagent, then pushes the branch and opens a PR following this repo's pull request template. +--- + +# Create PR + +Use this skill to prepare and open a pull request for changes made in this +repository. + +All user-facing output you create — branch name, commit message, PR title and +body — is written in **English**, regardless of the language the user wrote +in. + +## Steps + +1. **Verify the working tree**: run `git status --short --branch` and + `git diff` to confirm what will be included, and confirm the `origin` + remote exists. Do not include unrelated or uncommitted work the user + didn't ask for. If there are no relevant local changes and no unpushed + commits, stop and say so plainly. +2. **Create a branch** if you are still on `main` (or another base branch) — + never commit directly to it. Derive a short kebab-case name from the work + (e.g. `add-season-aggregates`, `fix-path-mapping`), or use the name the + user supplied. If you are already on a feature branch, stay on it. +3. **Verify tests exist** for what the diff changes. Per + [`UNIT_TESTS.md`](../../../docs/UNIT_TESTS.md), unit tests are mandatory for + new/changed behavior, not optional — if the diff adds or changes logic + without a corresponding test, write one before proceeding (following + `UNIT_TESTS.md`'s naming, test-double and assert-message conventions) + rather than opening the PR without coverage. +4. **Run local verification** before pushing, from the repository root, + with the commands from [`.squad/stack.md`](../../../.squad/stack.md): + - *Restore* (if the stack has one) and *Format* + - *Build* — it must finish without errors and without the warnings + `stack.md` lists as forbidden + - *Analyzer gate* — no analyzer diagnostic of any severity in a changed + file; treat each as a failure + - *Test with coverage* and *Coverage gate* — at least 80 % line coverage + on new/changed production code and overall + - `python3 .squad/tools/config-check.py` when the diff touches `.claude/`, + `.github/skills/`, `.agents/skills/` or an instruction file — Claude Code + silently drops an agent or skill whose front matter does not parse, and + the skill copies and instruction files must match + Fix any failures before proceeding — do not open a PR with failing checks, + unformatted code or outstanding analyzer diagnostics. This step is the gate + before the PR; CI is not meant to find anything here. +5. **Commit** with a subject line of at most 80 characters, not written in + the first person and without a trailing period, and a body of 3–5 + sentences explaining *what* changed and *why* if it is not obvious from + the diff. Stage only the files that belong to this task. +6. **Run the internal review loop** (see below) and resolve what it finds. + This happens *before* the push, so the pull request opens on a reviewed + change instead of collecting review rounds afterwards. +7. **Push** the branch: `git push -u origin `. +8. **Open the PR** using the repository's template at + `.github/pull_request_template.md`: + - base branch `main`, unless the user explicitly requests a different base + - title `[area] Description` per + [`CONTRIBUTING.md`](../../../docs/CONTRIBUTING.md) — area is one of the + areas CONTRIBUTING lists, capitalized, no period at the end, no issue number + in the description, under 70 characters + - fill in Description, Issues (link the related issue if one exists, with + `Closes #`), Reviewer Notes and Test Plan, and check off the + checklist items that are actually true (don't check items you haven't + verified) — including the unit-test, formatting, analyzer, coverage, + documentation and dependency items, not just the general ones + - wrap the body in a HEREDOC so the formatting survives +9. Report the branch name and the PR URL back to the user. + +## What the pull request says — and what it doesn't + +The pull request documents **the change**, not how the change was produced. + +- Reviewer Notes tell a reviewer where to look and why the approach was + chosen: the components touched, any guarantee from + [`ARCHITECTURE.md`](../../../docs/ARCHITECTURE.md) the change comes near, + and a smoke test if one is worth running. +- Do **not** mention the internal review loop anywhere in the PR — not how + many passes ran, not what they found, not which commits resolved their + findings. That loop is a working step inside this session, not part of the + change's history, and a reader of the PR has no use for it. +- Describe the finished state of the change, not the sequence of corrections + that got there. + +## The internal review loop + +The review happens here, in this session, against the local branch — not as +a round trip through pull request comments. Each pass is delegated to the +`squad-reviewer` subagent, which runs on Opus with a fresh +context and the repository's full review checklist. That checklist lives in +`.claude/agents/squad-reviewer.md`; an agent without subagent +support follows the same file inline, so the review is the same either way. + +1. **Pass 1** — launch `squad-reviewer` (subagent_type + `squad-reviewer`, model `opus`). Tell it the base ref, the + head to review, and that this is round 1. +2. **Act on the verdict**: + - `APPROVE` → done, go push. + - Blocking findings → fix each one minimally and commit. Do not widen the + change beyond what the finding requires. + - Non-blocking findings → **do not open another round for them**. Fix one + if it is trivial and already in scope. Otherwise open a GitHub issue for + it **now**, in this session, and link that issue under the PR's Next + Steps — a note that only exists in this conversation is lost the moment + the session ends, so it is not a way to carry a finding forward. +3. **Pass n+1** — launch a fresh `squad-reviewer` and give it + the round number, the previous round's findings, and the commits that + fixed them. It reviews the delta only, per its own instructions. +4. **Stop** at the first pass that reports no blocking findings. Cap the loop + at **three passes**: if blocking findings remain after the third, stop and + report the open findings to the user rather than continuing to iterate — + at that point the change needs a decision, not another round. + +Two rules keep this loop finite, and they are the point of the whole +arrangement: + +- **Later passes review the delta, never the whole diff again.** A fresh full + review of unchanged code always finds something new. +- **Only blocking findings start a new pass.** Non-blocking findings are + resolved or turned into an issue, not iterated on. + +## Findings that arrive after the push + +If a review lands on the pull request after it is open — from a human +reviewer, from an automated code review, or from the `review-pr` skill — work +those findings in this session, in this pull request. Do not defer a posted +finding to "the next change that touches this code": there is no such change +on the horizon, and the session holding the context needed to act on it will +not exist later. `review-pr` describes how to answer and close out each +posted comment. + +## Notes + +- Prefer non-interactive commands only. +- Do not amend existing commits unless the user explicitly asks. +- If a PR already exists for the branch, push the new commits and report the + existing URL instead of opening a duplicate. +- If push or PR creation fails, stop and report the failure clearly instead + of continuing as if it succeeded. +- Never force-push over another contributor's commits without explicit + confirmation. +- If the change touches a guarantee, security area or integration-surface + entry in [`.squad/project.md`](../../../.squad/project.md), a configuration + key, or the Docker/CI setup, make + sure the corresponding documentation — [`README.md`](../../../README.md), + [`ARCHITECTURE.md`](../../../docs/ARCHITECTURE.md), + [`SECURITY.md`](../../../SECURITY.md) — was updated in the same PR (see the + template checklist). See + [`CONTRIBUTING.md`](../../../docs/CONTRIBUTING.md) for the full workflow and + stability policy this skill follows. \ No newline at end of file diff --git a/.agents/skills/fix-issue/SKILL.md b/.agents/skills/fix-issue/SKILL.md deleted file mode 100644 index 1931b5b..0000000 --- a/.agents/skills/fix-issue/SKILL.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -name: fix-issue -description: Takes a GitHub issue number, fixes the issue in the codebase, creates a branch, opens a pull request that closes the issue, and switches back to main. Use this whenever the user wants an issue resolved end-to-end, e.g. "fix issue 42", "work on #42", or passes a bare issue number to be handled. ---- - -Use this skill when the user gives you a GitHub issue number (e.g. "fix issue 42", "#42", or just "42") and wants it resolved end-to-end: understand the issue, implement the fix, and publish it as a pull request. - -Repository: `LarsLaskowski/DockerUpdateGuard`. - -All user-facing output you create — branch name, commit message, PR title and body, and any code comments or XML docs — must be written in **English**, regardless of the language the user wrote in. Never mention Codex, Anthropic, or any AI/assistant tooling in the commit message or PR, and do not add any `Co-Authored-By` trailer, "Generated with" footer, session link, or other note attributing the work to an AI (see the "Pull requests" section in `AGENTS.md`). - -## Workflow - -### 1. Read and understand the issue - -- Confirm the issue number from the user's request. If no number was given, stop and ask for one. -- Fetch the issue with the GitHub MCP tool: `mcp__github__issue_read` (`method: get`, `owner: LarsLaskowski`, `repo: DockerUpdateGuard`, `issue_number: `), and `method: get_comments` for the discussion. -- Read the title, body, and comments to understand what is actually being asked. If the issue is already `closed`, stop and report that instead of starting work. -- If the issue is ambiguous, underspecified, or could be solved several materially different ways, ask the user a focused clarifying question before writing code. Do not guess on decisions that are expensive to reverse. - -### 2. Prepare a clean starting point - -- Verify the working tree is clean with `git status --short --branch`. If there are unrelated uncommitted changes, stop and report them — do not bundle them into this fix. -- Make sure you start from an up-to-date base branch (`main` unless the user says otherwise): switch to it and `git pull` so the branch and PR are based on current code. -- Confirm the `origin` remote exists. - -### 3. Create the branch - -- Derive the branch type from the issue labels and content: use `fix/` for bugs, `feat/` for new functionality, `chore/`/`docs/`/`build/` where appropriate. -- Name the branch `/-`, e.g. `fix/42-registry-token-cache`. If the user supplied a branch name, use theirs. -- Create and switch to the branch from the base branch. - -### 4. Implement the fix - -- Solve the issue following the conventions in `AGENTS.md` and `.github/instructions/csharp.instructions.md` (binding C# style: naming, regions, XML docs, null handling, no `this.`, no primary constructors). -- Keep changes small and targeted; reuse existing helpers before adding abstractions. -- Respect the project layering: web startup and DI wiring stay in `src\DockerUpdateGuard`, persistence in `src\DockerUpdateGuard.Data`, observability in `src\DockerUpdateGuard.Telemetry`. -- Add or update tests under `src\Tests` (MSTest, `{Class}{Scenario}{ExpectedResult}` naming, assertion messages required) for the behavior you change. -- Read the surrounding code and match its style, naming, and comment density. - -### 5. Validate - -- Run `reihitsu-format ./` after making source changes. -- Run `dotnet build DockerUpdateGuard.slnx -c Release --no-restore` (run `dotnet restore DockerUpdateGuard.slnx` first if needed). -- Run the relevant tests, e.g. `dotnet test src\Tests\DockerUpdateGuard.Tests\DockerUpdateGuard.Tests.csproj -c Release --no-build` (and/or the `.Data.Tests` project) for the layer you touched. -- If validation fails, fix the cause before continuing — do not push broken code. If you cannot make it pass, stop and report clearly. - -### 6. Commit - -- Stage only the files relevant to this fix. Do not include unrelated changes. -- Write a commit message following `AGENTS.md`: one-line summary under 80 characters, no trailing period, not first person, and a body of 3–5 sentences depending on the number of changes. Reference the issue number. -- Do not add any `Co-Authored-By` trailer or any other note attributing the work to an AI/assistant. - -### 7. Push and open the pull request - -- Push the branch to `origin` with upstream tracking (`git push -u origin `). -- Open the pull request with `mcp__github__create_pull_request`: - - `owner: LarsLaskowski`, `repo: DockerUpdateGuard` - - `base`: `main`, unless the user requested a different base - - `head`: the branch created in step 3 - - `title`: concise English summary of the fix - - `body`: a short English summary of the problem and the fix, and a line `Closes #` so the issue auto-closes on merge. If a PR template exists in the repository, structure the body to match it. - - Do not add any attribution, "Generated with" footer, session link, or other note referencing an AI/assistant in the PR title or body. - -### 8. Finish - -- After the PR is created, switch back to the base branch (`main`). -- Report the issue number, branch name, and pull request URL clearly. - -## Rules - -- Prefer non-interactive commands only. -- If push or PR creation fails, stop and report the failure clearly — do not continue as if it succeeded. -- Do not amend existing commits unless the user explicitly asks. -- If switching back to `main` would discard or conflict with uncommitted work, stop and explain the blocker. -- Never close the issue manually; let `Closes #` in the PR body do it on merge. diff --git a/.agents/skills/publish-pr/SKILL.md b/.agents/skills/publish-pr/SKILL.md deleted file mode 100644 index f4fa9f2..0000000 --- a/.agents/skills/publish-pr/SKILL.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: publish-pr -description: Creates a branch, commits the current changes, pushes the branch, opens a pull request, and switches back to main. Use this when asked to publish local changes as a pull request. ---- - -Use this skill when the user wants the current local changes published to GitHub as a pull request. - -Repository: `LarsLaskowski/DockerUpdateGuard`. - -All user-facing output you create — branch name, commit message, PR title and body — must be written in **English**, regardless of the language the user wrote in. Never mention Codex, Anthropic, or any other AI/assistant tooling in the PR title or body, and do not add any `Co-Authored-By` trailer, "Generated with" footer, session link, or other note attributing the work to an AI (see the "Pull requests" section in `AGENTS.md`). - -Follow this workflow: - -1. Inspect the repository state first with non-interactive Git commands: - - confirm the current branch - - review `git status --short --branch` - - confirm the `origin` remote exists -2. If there are no relevant local changes to publish, stop and say so plainly. -3. Validate the changes before publishing: - - Run `reihitsu-format ./` if any source files changed. - - Run `dotnet build DockerUpdateGuard.slnx -c Release --no-restore` (restore first if needed). - - Run the tests for any affected project(s) under `src\Tests`. - - If validation fails, fix the cause before continuing — do not publish broken code. If you cannot make it pass, stop and report clearly. -4. Choose or confirm a branch name based on the change. If the user already provided one, use it. Otherwise derive a short kebab-case branch name (e.g. `fix/…`, `feat/…`, `chore/…`) from the work. -5. Create and switch to the branch from the current base branch. -6. Stage only the files relevant to this change. Do not include unrelated changes. -7. Create a non-interactive Git commit following `AGENTS.md`: one-line summary under 80 characters, no trailing period, not first person, and a body of 3–5 sentences depending on the number of changes. -8. Push the branch to `origin` and set upstream tracking (`git push -u origin `). -9. Create a pull request with `mcp__github__create_pull_request`: - - `owner: LarsLaskowski`, `repo: DockerUpdateGuard` - - `base`: `main`, unless the user explicitly requests a different base - - `head`: the branch created in step 5 - - `title`: concise English summary of the change - - `body`: short English summary of what changed. If a PR template exists in the repository, structure the body to match it. - - Do not add any attribution, "Generated with" footer, session link, or other note referencing an AI/assistant in the PR title or body. -10. After the pull request is created, switch back to the `main` branch. -11. Report the branch name and pull request URL clearly. - -Additional rules: - -- Prefer non-interactive commands only. -- Do not amend existing commits unless the user explicitly asks. -- Do not include unrelated modified files in the commit. -- If push or PR creation fails, stop and report the failure clearly instead of continuing as if it succeeded. -- If switching back to `main` would discard or conflict with uncommitted work, stop and explain the blocker. diff --git a/.agents/skills/rereview-pr/SKILL.md b/.agents/skills/rereview-pr/SKILL.md deleted file mode 100644 index 118f976..0000000 --- a/.agents/skills/rereview-pr/SKILL.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -name: rereview-pr -description: Re-reviews a GitHub pull request after review feedback was addressed, focusing only on what changed since the previous review, without changing any code. Use this whenever the user wants a follow-up look at a PR after fixes were pushed, e.g. "rereview PR 42", "re-review #42 after the fixes", "check if the review comments on 42 were addressed". ---- - -Use this skill when the user gives you a GitHub pull request number and wants it re-reviewed after earlier review feedback (from this skill's `review-pr` sibling, a human reviewer, or GitHub review comments) was supposedly addressed. - -Repository: `LarsLaskowski/DockerUpdateGuard`. - -This skill is **read-only**, exactly like `review-pr`. Its job is to check whether the previous review's findings were actually resolved and whether the newest commits introduced anything new worth flagging. It must **not** modify any code, commit, push, change the PR, or check out the branch in a way that alters the working tree beyond what is needed to inspect the diff. - -Write all output in **English**: your summary to the user, your recommendations, and — only if the user asks for it — any review comment posted to GitHub. This holds regardless of the language the user wrote in. - -## Scope - -Branch protection on this repository blocks a PR from merging on its own — a review is always required first. That is just background context for why re-reviews happen; it is not a finding to restate in your output. - -Keep the review itself narrow. Only evaluate: - -1. Whether each finding from the previous review was actually resolved by the new commits. -2. The code changes made since the previous review (not the whole PR from scratch, unless nothing was reviewed before). -3. Whether the PR description still matches what the diff now does. -4. The SonarQube Cloud check status — at most. - -Anything else about the PR (other CI checks such as build/test/CodeQL runs, labels, assignees, unrelated discussion) is out of scope and should not be reported on. - -## Workflow - -### 1. Find the previous review - -- Confirm the PR number from the user's request. If none was given, stop and ask for one. -- Fetch metadata with `mcp__github__pull_request_read` (`method: get`, `owner: LarsLaskowski`, `repo: DockerUpdateGuard`, `pullNumber: `). -- Find what was reviewed before: - - Look at prior review submissions and review comments with `mcp__github__pull_request_read` (`method: get_reviews` and `method: get_review_comments`). - - If the user pasted or referenced a previous `review-pr` report in the conversation, use that as the list of findings instead of (or in addition to) GitHub review comments. -- If you cannot find any previous review or findings at all, say so and ask the user whether to proceed as a full `review-pr`-style review instead. - -### 2. Isolate what changed since that review - -- Identify the commit (or timestamp) the previous review was based on — the latest commit reviewed, or the time of the last review submission. -- Fetch the current diff with `method: get_diff` and the changed files with `method: get_files`. -- Where possible, scope your reading to the commits/files touched **since** the previous review point, rather than re-reading the entire PR diff from scratch. Use the full diff only for context when a finding can't be judged from the incremental change alone. -- Fetch check runs with `method: get_check_runs` and read only the SonarQube Cloud check's conclusion. Ignore every other check. - -### 3. Check each previous finding - -For every finding from the previous review: - -- Mark it **Resolved**, **Not resolved**, or **Partially resolved**, with a one-line reason pointing at the relevant `file:line`. -- If a finding was a **Blocking** item and is not resolved, it stays blocking. -- If the fix introduces a new problem (regression, incomplete fix, new edge case), report that as a new finding. - -### 4. Check the description and new code - -- Compare the current PR description to the current diff; if it no longer matches (e.g. the fix changed scope but the description wasn't updated), raise it as a finding. -- Review any newly added or changed code (since the previous review) against the same criteria `review-pr` uses: correctness (bugs, edge cases, null handling, concurrency, async/`.ConfigureAwait(false)`, EF Core navigation assumptions), convention adherence (`AGENTS.md` and `.github/instructions/csharp.instructions.md`), scope/size, tests (`src\Tests`, `{Class}{Scenario}{ExpectedResult}` naming, MSTest assertions with messages), and clarity. - -### 5. Report findings - -Present the re-review to the user in this structure: - -``` -## Re-review of PR # — - -**Verdict:** <Approve / Approve with comments / Request changes / Needs discussion> - -### Summary -<1–3 sentences on whether the previous feedback was addressed and the PR is now in better shape.> - -**Description match:** <Does the PR description accurately reflect the current diff? Yes/No and why.> -**SonarQube Cloud:** <Pass / Fail / Warnings / Not run — no other checks.> - -### Previous findings -- **[Resolved|Not resolved|Partially resolved] <file:line>** — <what changed, or why it's still open.> -- ... - -### New findings -- **[Blocking|Suggestion|Nit] <file:line>** — <what and why, with a recommended action.> -- ... - -### Recommendations -<Concrete next steps the author should take.> -``` - -- Reference exact `file:line` locations so findings are easy to act on. -- If everything was resolved and nothing new came up, say so plainly rather than inventing issues. - -### 6. Optional: post a review comment - -- Only post anything to GitHub if the user explicitly asks for it. By default, just report back in the chat. -- If asked, use `mcp__github__pull_request_review_write`: - - `method: create` with `event: COMMENT` for a neutral English review comment, or `event: APPROVE` / `event: REQUEST_CHANGES` only when the user explicitly chooses that action. - - For line-specific comments, create a pending review (`method: create` without `event`), add comments with `mcp__github__add_comment_to_pending_review`, then submit with `method: submit_pending`. -- Do not add any attribution, "Generated with" footer, or other note referencing an AI/assistant. - -## Rules - -- Never modify code, commit, push, or change the PR contents — this skill only reviews. -- Prefer non-interactive commands only. -- Do not post any comment or review to GitHub unless the user explicitly requests it. -- Base your verdict on evidence from the diff and code; if something is uncertain, say so instead of guessing. -- Stay inside the scope defined above: previous-finding resolution, the changes since the last review, the description-vs-diff match, and the SonarQube Cloud check. Do not comment on other checks, labels, or metadata. diff --git a/.agents/skills/review-pr/SKILL.md b/.agents/skills/review-pr/SKILL.md index d5a6d20..b69f7cb 100644 --- a/.agents/skills/review-pr/SKILL.md +++ b/.agents/skills/review-pr/SKILL.md @@ -1,95 +1,117 @@ --- name: review-pr -description: Reviews a GitHub pull request by number and reports findings and actionable recommendations, without changing any code. Use this whenever the user wants a pull request examined, e.g. "review PR 42", "check #42", or passes a bare PR number for review. Posting a review comment is optional and only happens on explicit request. +description: Use when the user asks to review a pull request of this repository on GitHub. Checks out the PR, runs the build and tests, reviews it with the squad-reviewer subagent against this project's stack, analyzer, security and unit-test conventions, and posts the findings with an explicit verdict. --- -Use this skill when the user gives you a GitHub pull request number (e.g. "review PR 42", "#42", or just "42") and wants it reviewed. - -Repository: `LarsLaskowski/DockerUpdateGuard`. - -This skill is **read-only**. Its job is to understand the PR and give the user findings and actionable recommendations. It must **not** modify any code, commit, push, change the PR, or check out the branch in a way that alters the working tree beyond what is needed to inspect the diff. The output is a review, not a fix. - -Write all output in **English**: your summary to the user, your recommendations, and — only if the user asks for it — any review comment posted to GitHub. This holds regardless of the language the user wrote in. - -## Scope - -Branch protection on this repository blocks a PR from merging on its own — a review is always required first. That is just how merging works here; it is background context for why this skill exists, not a finding to restate in your output. - -Keep the review itself narrow. Only evaluate: - -1. The code changes in the diff. -2. Whether the PR description matches what the diff actually does. -3. The SonarQube Cloud check status — at most. - -Anything else about the PR (other CI checks such as build/test/CodeQL runs, labels, assignees, comment history, unrelated discussion) is out of scope and should not be reported on. - -## Workflow - -### 1. Load the pull request - -- Confirm the PR number from the user's request. If none was given, stop and ask for one. -- Fetch metadata with `mcp__github__pull_request_read` (`method: get`, `owner: LarsLaskowski`, `repo: DockerUpdateGuard`, `pullNumber: <number>`). -- Fetch the diff with `method: get_diff`, and the changed files with `method: get_files` if you need per-file granularity. -- Fetch check runs with `method: get_check_runs` and read only the SonarQube Cloud check's conclusion (pass, fail, or warnings). Ignore every other check. -- If the PR is already merged or closed, say so and ask whether the user still wants a review before continuing. - -### 2. Check the description against the diff - -- Read the PR title and body to understand what the change claims to do. -- If the PR references an issue (e.g. `Closes #N`), read that issue with `mcp__github__issue_read` (`method: get`) so you can judge whether the change actually solves the stated problem. -- Compare the description to the actual diff. If the description is inaccurate, incomplete, or overstates/understates the change, raise it as a finding (see step 4). - -### 3. Review the diff - -Evaluate the change against what matters for this project. Focus on: - -- **Correctness** — bugs, edge cases, error handling, null handling, concurrency issues. Pay attention to Docker/registry API interaction, async/await usage (missing `.ConfigureAwait(false)` in service/data-access code), and EF Core query/navigation assumptions. -- **Convention adherence** (`AGENTS.md` and `.github/instructions/csharp.instructions.md`) — naming, region layout, file-scoped namespaces, XML documentation on public/internal/private members, no `this.`, no primary constructors, `== false` instead of `!`, layering (`.Data` for persistence, `.Telemetry` for observability, main host for web/DI wiring). -- **Scope and size** — unrelated changes bundled in, accidental file inclusions, debug leftovers. -- **Tests** — whether tests were added or updated under `src\Tests`, whether they follow the `{Class}{Scenario}{ExpectedResult}` naming and MSTest `Assert`/`CollectionAssert` conventions, and whether assertion messages are present. -- **Clarity** — naming, dead code, needless complexity, missing or misleading comments/docs. - -Do not run builds that modify files unnecessarily; reading the diff and the surrounding code is usually enough. You may read any file in the repo for context. - -### 4. Report findings - -Present the review to the user in this structure: - -``` -## PR #<number> — <title> - -**Verdict:** <Approve / Approve with comments / Request changes / Needs discussion> - -### Summary -<1–3 sentences on what the PR does and whether it achieves its goal.> - -**Description match:** <Does the PR description accurately reflect the diff? Yes/No and why.> -**SonarQube Cloud:** <Pass / Fail / Warnings / Not run — no other checks.> - -### Findings -- **[Blocking|Suggestion|Nit] <file:line>** — <what and why, with a recommended action.> -- ... - -### Recommendations -<Concrete next steps the author should take.> -``` - -- Classify each finding as **Blocking** (must fix before merge), **Suggestion** (worth doing), or **Nit** (minor/optional). -- Reference exact `file:line` locations so findings are easy to act on. -- If you find nothing wrong, say so plainly rather than inventing issues. - -### 5. Optional: post a review comment - -- Only post anything to GitHub if the user explicitly asks for it. By default, just report back in the chat. -- If asked, use `mcp__github__pull_request_review_write`: - - `method: create` with `event: COMMENT` for a neutral English review comment, or `event: APPROVE` / `event: REQUEST_CHANGES` only when the user explicitly chooses that action. - - For line-specific comments, create a pending review (`method: create` without `event`), add comments with `mcp__github__add_comment_to_pending_review`, then submit with `method: submit_pending`. -- Do not add any attribution, "Generated with" footer, or other note referencing an AI/assistant. - -## Rules - -- Never modify code, commit, push, or change the PR contents — this skill only reviews. +# Review PR + +Use this skill to review a pull request on GitHub — someone else's, or your +own when you deliberately want a second opinion after it is open. + +For a change that has not been pushed yet, do not use this skill: the +internal review loop in `create-pr` reviews the local branch before the pull +request exists, which is cheaper and does not fill the PR with comment +threads. + +Write everything in **English** — the summary to the user, the findings, and +anything posted to GitHub — regardless of the language the user wrote in. + +## Steps + +1. Fetch and check out the PR (or read the diff directly if a checkout isn't + necessary). Read the PR title and body to understand the intent, and read + any issue it references so you can judge whether the change actually + solves the stated problem. If the PR is already merged or closed, say so + and ask whether the user still wants a review. +2. Delegate the review itself to the `squad-reviewer` subagent + (subagent_type `squad-reviewer`, model `opus`). Give it the + base ref, the head SHA, and the round number — round 1 for a first review, + and for a re-review the previous round's findings plus the commits that + were meant to fix them. The review checklist, the integration-surface + sweep, the severity model and the round semantics all live in that agent's + definition (`.claude/agents/squad-reviewer.md`), so they stay + identical whether the review runs before or after the push; an agent + without subagent support follows that same file inline. +3. Post the result: + - Inline comments for findings anchored to a line, otherwise one review + comment. + - **Only genuine findings.** No positive remarks, no confirmation that + checklist items pass, no "looks good" filler, no formatting + the formatter already fixes. + - Lead the review body with the verdict line the subagent produced + (`APPROVE`, or the blocking/non-blocking counts), so the author can see + whether anything is required of them without reading every thread. + - Mark each finding `blocking` or `non-blocking` explicitly. +4. If the review produces no findings, post nothing beyond a short approving + verdict — and if the previous round already said the same, post nothing at + all. + +## Every posted finding gets worked + +A finding that has been posted as a review comment is work, not a note. This +holds for **every** posted finding — blocking and non-blocking alike, whether +it came from this skill, from a human reviewer, or from an automated code +review on the pull request. + +- Resolve it in the pull request it was posted on, while the session that can + act on it is still running. +- Do not defer a posted finding to "the next change that touches this code" + or "the next substantive commit". No such change is scheduled, and the + session holding the context needed to act on the comment will not exist + later — the deferral is a way of dropping the finding, not of carrying it + forward. +- If a posted finding genuinely should not be acted on in this PR, it gets + one of two concrete outcomes, never an implied one: a reply explaining why + the code stays as it is, or a GitHub issue opened **now** and linked from + the reply. Either way the thread is answered and resolved before the PR is + considered done. +- Non-blocking is about whether a finding gates the merge, not about whether + anyone will ever deal with it. + +## Keeping the loop finite + +A pull request review can always produce one more finding. These rules make +it converge, without leaving posted findings unhandled: + +- **Round 1 reviews the whole diff. Every later round reviews only the + delta**: does each fix resolve its finding, and did the fix commits break + something — including in prose they wrote to fix a documentation finding? + Never re-review untouched code; that is what turns three findings into four + rounds. +- **Only blocking findings justify another review round.** A non-blocking + finding is still worked per the section above, but working it does not earn + a new round of review. +- **Two consecutive rounds without a blocking finding means done.** Say so + plainly instead of leaving the review open-ended. +- **At most two rounds on GitHub.** If blocking findings survive that, the + change needs a decision from the author, not another review pass — say what + is still blocking and stop. +- The number of rounds is capped; the number of posted findings that get + handled is not. Every open thread is answered before the PR is done, even + when no further round runs. + +## Answering findings on your own PR + +When acting as the author of a PR under review: + +- Fix the finding, push, then keep the reply to one line: + `Fixed in <sha>: <what changed>`. The reasoning belongs in the commit + message, where it stays with the code; the reviewer verifies the commit, + not the reply. +- Re-run *Format*, *Build*, the *Analyzer gate* (no diagnostic in a changed + file), *Test with coverage* and the *Coverage gate* from `.squad/stack.md` + before each push — a fix that turns CI red costs more + than the finding did. +- Resolve the thread once it is answered. One summary comment per round beats + one essay per thread. +- Work through every open thread before calling the PR done, including the + non-blocking ones, as described above. + +## Notes + +- This skill reviews; it does not silently rewrite the PR. Fixing findings on + your own PR is the author's step above, and it is explicit — never edit + someone else's branch without being asked. - Prefer non-interactive commands only. -- Do not post any comment or review to GitHub unless the user explicitly requests it. -- Base your verdict on evidence from the diff and code; if something is uncertain, say so instead of guessing. -- Stay inside the scope defined above: the diff, the description-vs-diff match, and the SonarQube Cloud check. Do not comment on other checks, labels, or metadata. +- Base the verdict on evidence from the diff and the code; if something is + uncertain, say so instead of guessing. \ No newline at end of file diff --git a/.agents/skills/squad-issue/SKILL.md b/.agents/skills/squad-issue/SKILL.md new file mode 100644 index 0000000..adf45dc --- /dev/null +++ b/.agents/skills/squad-issue/SKILL.md @@ -0,0 +1,176 @@ +--- +name: squad-issue +description: Use when the user asks to fix a specific GitHub issue in this repository. Runs the squad pipeline — Lead plans and picks a tier, the Devil's Advocate challenges the plan, Security reviews security-relevant plans, Tester writes failing tests first, Dev implements to 80% coverage, Code Officer clears format and analyzer findings, Reviewer (+ Security) review, Lead approves — and opens a PR referencing the issue. +--- + +# Squad Issue + +Fix a reported GitHub issue with the squad defined in `.squad/`. You are the **orchestrator**: you launch +the members as subagents, pass their outputs on (they cannot talk to each other), enforce the tiers and +loop limits from [`.squad/routing.md`](../../../.squad/routing.md), and perform every Git and GitHub +action yourself — including follow-up issues the Lead decides on. + +- **You never do a member's work.** You do not edit production code, tests or documentation the plan + assigns to the Dev, do not run the formatter, and do not fix analyzer findings — not even a one-line `sed`. + Whatever a check of yours finds goes to its owner (production code → `squad-dev`, tests → + `squad-tester`, formatting/analyzer-only edits → `squad-code-officer`) and through the steps that follow + it. Besides read-only checks (`--check`, the analyzer and coverage scripts, tests) you only write the + squad's bookkeeping: `log.md` and `tasks.md` check marks (features). Never production code, tests or `docs/`. +- **The squad does not change itself in a product PR.** An issue or feature PR never touches `.squad/` + (including `history.md` and `decisions.md`), `.claude/`, `.github/skills/`, `.agents/skills/`, `CLAUDE.md`, `AGENTS.md` + or `.github/copilot-instructions.md`. Lessons about the squad are filed in step 12 as `.squad/routing.md`, + *Squad lessons*, says — template-managed files in the template repository, project knowledge here. If the + change itself genuinely needs one of those files (e.g. a new build command every contributor must know), + the Lead escalates instead and the Product Manager decides: `.squad/stack.md` and `.squad/project.md` may + change in the product PR (*Scope of a product PR*); a template-managed file is changed in the template + repository; any other squad or instruction file (e.g. a project block) goes into a separate + squad-maintenance PR. +- **Working records stay off `main`.** `specs/<folder>/` exists only on the work branch, so it survives a + crashed session. Before the PR (step 10) its content is posted as a comment and the folder is removed; + the lasting reasoning lives in `docs/decisions/`. +- **One build at a time.** Never run two members that build or test (`squad-dev`, `squad-tester`, + `squad-code-officer`, the reviewers' verification runs) in parallel: they share `bin/` and `obj/` and + break each other (`.squad/routing.md`). Launch them one after another; only `squad-reviewer` and + `squad-security` may run together in step 8, because both are read-only and the reviewer's own build + happens in a scratch copy. +- **Commits and pushes** to the work branch are always allowed (`CLAUDE.md`, golden rules): commit and + push `specs/<folder>/log.md` right after intake (step 1), so a stop hook or a crashed session finds no + untracked files, and after every further completed step. PRs are merged with *Squash and merge*, so only + the PR title and description reach `main`; intermediate commit messages may name the step, but never + contain secrets. Interim work-in-progress commits — e.g. demanded by a stop hook while a member is still + working — are fine for the same reason. Stage with plain `git add -A`: ignored paths such as `TestResults/` + are skipped anyway, and an exclusion pathspec for an ignored path makes `git add` fail and stage + nothing. Never commit to `main`. +- **GitHub access:** use the GitHub MCP tools (`mcp__github__*`) for issues, comments, labels and pull + requests. In these sessions the `gh` CLI only works as `gh api repos/<owner>/<repo>/...`; `gh issue`, + `gh pr` and `gh search` fail (GraphQL is blocked and search is not scoped to the repository). +- **Pull request:** invoking this skill is the user's approval for opening the PR in step 10, once the + Lead has approved it (tier `docs`: once the latest review round is clean). +- **Product Manager:** the user is only contacted when the Lead returns `RESULT: ESCALATE` (relay the + question verbatim with its options and wait) or for confirming a public issue comment on + `RESULT: NO CHANGE`. +- Everything that ends up in the repository or on GitHub is written in **English**. + +## Steps + +1. **Intake.** Read the issue in full, including comments; note the reported environment (versions of the + software involved, image tag or release, host OS, configuration). If it is closed, stop and report that. Start + from a clean working tree on a new branch off the latest `main`, e.g. + `fix-issue-<number>-<short-slug>` (or the branch the session prescribes). Create + `specs/issue-<number>/log.md` from `specs/_template/log.md`, commit and push it; append one table row + per step. Only you + (the orchestrator) write `log.md`, one row per append, each row ending in the file's line ending — subagents report and + you record, so rows never merge or end up with mixed line endings. +2. **Plan.** Launch `squad-lead` in mode `plan` with the issue text and the work folder. It returns one of: + - `RESULT: DONE` — for tier **`docs`** (see its definition in `.squad/routing.md`), a short result (tier, the files and lines to change, acceptance + criteria) that you record as the first plan row in `log.md`, then continue with step 6 (Dev), the + read-only check from the `docs` row in `.squad/routing.md`, one review round in step 8, and step 10 + directly — no Security, skeleton, tests, coverage, Code Officer or Lead approval. Otherwise: + `plan.md` with the **tier** (`trivial` / `standard` / `security`), acceptance + criteria, the signatures of new or changed API, required documentation updates (`README.md`, + `docs/`), and `Proposed` decision records. Continue with the steps the tier requires. + - `RESULT: NO CHANGE` — show the proposed issue comment to the user, post it only after confirmation + (append the log as a collapsed "Squad working record" block), remove the work folder with a commit + and push, and stop. No PR; the branch stays as it is, and you tell the user so. + - `RESULT: ESCALATE` — ask the user, then relaunch the Lead with the answer. + + **Plan challenge** (`standard` and `security` only, once). Launch `squad-devils-advocate` with the issue + text and the work folder. On `VERDICT: OBJECTIONS …`, launch `squad-lead` in mode `revise` with the + objections; it answers each one in the plan's *Challenge* section (accepted and the plan revised, or + rejected with a reason) and may narrow the scope, raise the tier or switch to `RESULT: NO CHANGE`. + `RESULT: NO CHANGE` and `RESULT: ESCALATE` are handled as above; after a raised tier, continue with that + tier's steps. There is no second challenge round and no veto. Record the verdict (also a clean + `NO OBJECTIONS`) and the Lead's answer in `log.md`. +3. **Plan security review** (`security` tier only). Launch `squad-security` in mode `plan`. On + `CHANGES_REQUIRED`, launch `squad-lead` in mode `revise` and repeat. After the **2nd** rejection launch + `squad-lead` in mode `decide` (scope down, split into issues, abort, or escalate). +4. **Skeleton** (only if the plan adds or changes API). Launch `squad-dev` in mode `skeleton`: the planned + signatures built as *Skeleton* in `.squad/stack.md` describes (bodies fail when called), plus the existing + test call sites the plan assigns to the Dev for an incompatible signature change, so the tests of step 5 + compile. +5. **Tests first** (skipped for `trivial`). Launch `squad-tester` in mode `tests-first`. Confirm yourself + that the new tests compile and fail on the current code (unless the Tester justified why one cannot). + A fix without a reproducing test is only acceptable when the bug genuinely needs a live external + system — then the PR says so. +6. **Implement and cover.** Launch `squad-dev` in mode `implement` with the plan and the test names; it + also makes the documentation updates the plan lists. If the Dev disputes a test, launch `squad-lead` + in mode `decide`; the Tester changes a test only if the Lead says so. Then launch `squad-tester` in + mode `coverage`; repeat Dev/Tester until the *Coverage gate* (after *Test with coverage*, both in + `.squad/stack.md`) passes (≥ 80 % on new/changed production code and overall). Lines reported as not unit-testable go to + `squad-lead` in mode `decide`; an accepted gap is recorded in `log.md`. +7. **Code check.** Launch `squad-code-officer` with the base ref — the only member that runs + the formatter and clears analyzer diagnostics. Then verify yourself, without formatting, with the + commands from `.squad/stack.md`: *Format check* exits 0, the *Analyzer gate* passes (no diagnostic of + any severity in a changed file), *Test* is green with the same tests, and the *Coverage gate* still + passes. Structural items handed back go to `squad-dev` (or + `squad-tester`), followed by another code check. This is the gate before the PR; CI is not meant to find anything here. +8. **Review.** Launch `squad-reviewer` (round 1, full) and — for `standard` and `security` — + `squad-security` in mode `diff`, in parallel, against the base ref. Pass both the work folder + (`specs/<folder>/`) so they check the plan's acceptance criteria and tier (tier `docs`: the first + `log.md` row, since there is no `plan.md`); either may raise the tier. Tier `docs`: a blocking finding + goes to `squad-dev`, then the read-only check and a delta round, then step 10. Blocking + findings → their owner fixes them (`squad-dev` for production code, `squad-tester` for tests) → steps 6 + (coverage) and 7 again → **a new review round on the delta is mandatory** before step 9; never go from + a blocking finding straight to PR approval. The same holds for a non-blocking finding the Lead decides + to fix now: any change to production code, tests or `docs/` after a review round needs a delta round. At most **2 fix rounds** after round 1; then `squad-lead` + in mode `decide`. Non-blocking + findings: the Lead decides per finding — fix now, or you open a linked GitHub issue now. +9. **PR approval.** Launch `squad-lead` in mode `approve-pr` with the base ref, the build/test/coverage + output and the review outcome — including the result of the **latest** review round, which must have + no blocking finding that is not covered by a recorded Lead decision, and must cover every change to + production code, tests and `docs/` since it ran (only `specs/` bookkeeping and the Lead's own approval edits — + record status, the index, a link from `docs/ARCHITECTURE.md` — may follow it; a fix for a blocking + finding always needs a delta round, also in a decision record). `NOT APPROVED` → back to step 6 or 8 (counting against the review loop + limit) or let the Lead decide/escalate. On `APPROVED`, the decision records are `Accepted` and indexed + in `docs/decisions/README.md`. +10. **Pull request** (Dev role, performed by you). First move the working record off the branch: post + `plan.md` (none for tier `docs`) and `log.md` as one comment on the issue (each inside a collapsed `<details>` block, headed + "Squad working record"), then `git rm -r specs/issue-<number>/`, commit ("Remove squad working + record"), and push. Later log rows (steps 11–12) are appended by editing that comment. Then open the + PR from + [`.github/pull_request_template.md`](../../../.github/pull_request_template.md): title per + `docs/CONTRIBUTING.md` — `[area] Description`, where `area` is one of the areas + CONTRIBUTING lists, capitalized — not a lowercase class or file name. It becomes the squash + commit subject on `main`. Body describing the bug, the fix and the + reproducing test, `Closes #<number>` under Issues, links to the decision records. Next Steps lists + **only linked GitHub issues** (create them now) or "None" — never an unlinked "revisit later". Follow the `create-pr` skill's template rules, but + do **not** run its internal review loop — step 8 replaced it. If the fix is not fully verifiable + without a real external system, say so. +11. **After the PR.** Subscribe to the PR's activity right after opening it (`subscribe_pr_activity` when + available; otherwise check the CI and code-analysis (e.g. SonarQube Cloud) results yourself before finishing) — a session + that ends with an unwatched PR has not completed this step. Stay with the PR until CI is green and the + code-analysis quality gate (e.g. SonarQube Cloud) passes: + - code-analysis findings → `squad-code-officer` (structural ones → `squad-dev`); + - failing build or tests → `squad-dev` (test defects → `squad-tester`); + - review comments (human, automated, `review-pr`) → `squad-dev`, worked in this PR, blocking or not. + + Each fix goes through steps 7–8 again (delta review), with at most 2 fix rounds per failure before the + Lead decides. The work folder is gone by now: give the Reviewer, Security and the Lead the plan (tier + `docs`: the first log row; features: + also `spec.md` and `tasks.md`) from the "Squad working record" comment, or via + `git show <commit-before-removal>:specs/<folder>/<file>`, and record each log row by editing that + comment. Never skip, disable or weaken a test to get green. +12. **Wrap-up (mandatory).** Collect what this run taught about the squad itself (a rule that was + unclear or contradictory, a tool that misbehaved, an agent that could not be launched, a step that + had to be improvised), each with the role it concerns and a concrete proposal, and file them as + `.squad/routing.md`, *Squad lessons*, says: lessons about template-managed files as **one** issue + labelled `squad` in the template repository named in `.squad/template.json` (attach that repository to the session if + needed; without access, file it here with the label `squad-upstream`), lessons about project knowledge + as **one** issue labelled `squad` in this repository (create the labels if missing). Link the issues + from the working record comment. Do **not** edit `.squad/`, `.claude/` or the instruction files. + Report the branch, the PR URL, the tier, the `squad` issues (or "no lessons") and any escalation or + Lead decision to the user. If there is genuinely nothing to learn, + append a `| <date> | 12 Wrap-up | Orchestrator | no lessons |` row to the working record comment + instead of opening an issue — the step itself is never skipped. + +## What the pull request says — and what it doesn't + +The PR title and description document the change, not how it was produced: the bug, the fix and the test +that pins it down. Plan revisions, review rounds and their findings never appear there. +(The working record is the "Squad working record" comment on the issue; the lasting reasoning is in +`docs/decisions/`.) + +## Notes + +- Prefer non-interactive commands only. If push or PR creation fails, stop and report it. +- Never close the issue manually; `Closes #<number>` closes it on merge. diff --git a/.agents/skills/squad-spec/SKILL.md b/.agents/skills/squad-spec/SKILL.md new file mode 100644 index 0000000..847ce65 --- /dev/null +++ b/.agents/skills/squad-spec/SKILL.md @@ -0,0 +1,31 @@ +--- +name: squad-spec +description: Use when the user wants to develop a new feature in this repository spec-driven with the squad. Lead writes spec, plan and tasks and picks a tier, the Devil's Advocate challenges them, Security reviews security-relevant plans, Tester writes failing tests first, Dev implements to 80% coverage, Code Officer clears format and analyzer findings, Reviewer (+ Security) review, Lead approves, then a PR is opened. +--- + +# Squad Spec + +Build a feature with the squad defined in `.squad/`. Tiers, pipeline, loop limits, escalation rules, +commit/push rules and the orchestrator role are identical to the `squad-issue` skill — follow its steps +1–12 with these changes: + +- **Step 1 — work folder and branch:** `specs/feature-<short-slug>/` with `log.md` from + `specs/_template/log.md`; branch `feature-<short-slug>` off the latest `main` (or the branch the session + prescribes). +- **Step 2 — plan:** `squad-lead` in mode `plan` writes `spec.md` (behavior, acceptance criteria, out of + scope), `plan.md` and `tasks.md`. A feature is never `docs` and rarely `trivial`. It is more likely than a bug + fix to need a product decision — the Lead escalates whenever the request does not settle user-visible + behavior. `RESULT: NO CHANGE` means the feature already exists or contradicts an accepted decision; report + that to the user instead of commenting on an issue. The plan challenge covers `spec.md`, `plan.md` and + `tasks.md` together. +- **Decision records:** features usually involve real design choices, so expect at least one record in + `docs/decisions/`; the Lead also updates `docs/ARCHITECTURE.md` when the feature changes a flow or + guarantee. +- **Step 3** reviews `spec.md` and `plan.md` together. +- **Steps 4–6** run per task or group of tasks from `tasks.md`; tick tasks off as they are done. Run + Tester and Dev one after another, never in parallel (see *Concurrency* in `.squad/routing.md`). +- **Step 10 — pull request:** the working record (`spec.md`, `plan.md`, `tasks.md`, `log.md`) is posted + to the feature request issue if one exists (`Closes #<n>` in the PR), otherwise as the first comment on + the PR right after opening it; the folder is removed before the PR as in `squad-issue`. Behavior that + must stay documented belongs in `README.md`, `docs/ARCHITECTURE.md` or a decision record, not in + `spec.md`. diff --git a/.claude/agents/squad-code-officer.md b/.claude/agents/squad-code-officer.md new file mode 100644 index 0000000..89da820 --- /dev/null +++ b/.claude/agents/squad-code-officer.md @@ -0,0 +1,28 @@ +--- +name: squad-code-officer +description: "Squad Code Officer. The only squad member that runs the formatter and owns a clean analyzer gate: no analyzer diagnostic of any severity in changed files, as defined in .squad/stack.md. Applies style and analyzer fixes to the changed files without structural or behavioral change, so nothing is left for CI or SonarQube Cloud to find." +model: sonnet +--- + +# Squad Code Officer + +Read first: `.squad/agents/code-officer/charter.md`, `.squad/agents/code-officer/history.md`, +`.squad/stack.md` (commands, *Analyzer gate*, *Writing code*, *Known pitfalls*), `CLAUDE.md` (code style) +and the formatter/analyzer configuration files `stack.md` names. + +1. Determine the changed files (`git status --short` and `git diff --name-only <base>`); touch only those. +2. Run *Format* from `stack.md` (non-interactive) and confirm with *Format check* (exit code 0). Never + skip this step. If the formatter fails for environment reasons, check *Known pitfalls* in `stack.md`. +3. Run the *Analyzer gate*. It lists every diagnostic in a changed file, at every severity — including + ones a plain build never prints. Do not rely on grepping console build output. Fix every listed + diagnostic within the charter's limits; re-run *Format* and the gate until it passes. +4. Run the full test suite (*Test*); the same tests must pass as before your pass. + +After the PR is open you may also receive SonarQube Cloud (or other CI analysis) findings; treat them like +findings of the analyzer gate, and find out why the local gate missed them (report it in your result so +the orchestrator files it in the step-12 `squad` issue — never edit `.squad/` in a product PR). + +A new guard, branch, early return, null check or a changed assertion counts as structural. If a +diagnostic can only be fixed by a structural change, do not make it — hand it back with file, line and +rule id. Never suppress a rule on your own and never run Git write operations. Report: files touched, +kinds of edits, analyzer gate output (must pass), build/test result, items handed back. diff --git a/.claude/agents/squad-dev.md b/.claude/agents/squad-dev.md new file mode 100644 index 0000000..1cdc96e --- /dev/null +++ b/.claude/agents/squad-dev.md @@ -0,0 +1,36 @@ +--- +name: squad-dev +description: Squad Dev. Implements the approved squad plan in production code until the Tester's tests and the full suite are green and new/changed code reaches at least 80% line coverage, and fixes blocking review findings. Does not edit tests, does not run the formatter, no Git write operations. +model: sonnet +--- + +# Squad Dev + +Read first: `.squad/agents/dev/charter.md`, `.squad/agents/dev/history.md`, `.squad/stack.md`, +`.squad/project.md`, `CLAUDE.md`, the approved plan (and spec/tasks for features), the Tester's tests, and +the relevant parts of `docs/`. + +The orchestrator tells you which **mode** to run: + +- `skeleton` — add exactly the signatures listed in the plan, built as *Skeleton* in `stack.md` describes + (the bodies fail when called), nothing else, and make sure *Build* passes. If the plan assigns you + existing test call sites of a signature it changes incompatibly, adapt exactly those sites to the new + signature here (mechanically, no assertion touched), so the suite builds. This lets the Tester's tests + compile and fail before the implementation exists. +- `implement` — steps 1–3 below. +- `fix` — fix the findings, CI failures or handed-back items you are given, then steps 2–3. + +1. Implement the plan minimally, in the style of the surrounding code and *Writing code* in `stack.md` + from the start, and make the documentation updates the plan lists (`README.md`, `docs/`). +2. Run *Build*, *Test with coverage* and the *Coverage gate* from `stack.md`. Report the coverage result; + uncovered changed lines go to the Tester (or are made testable by you). +3. In the review loop you receive findings: fix the blocking ones, the non-blocking ones the Lead assigned + to this change, and structural items the Code Officer hands back. + +Do not run *Format* and do not chase style diagnostics unless the Code Officer hands one back — the Code +Officer owns them. Before handing over, run the *Analyzer gate* once and fix the findings in your +production files that need a code change, so they do not come back later as a structural hand-back. +Never edit tests (a test you believe is wrong goes back as a report for the Lead) — the only exception +is the skeleton-mode adaptation of existing test call sites that the plan explicitly assigns to you; never deviate from the +plan silently, never run Git write operations. Report: changed files, build/test/coverage result, plan +deviations. diff --git a/.claude/agents/squad-devils-advocate.md b/.claude/agents/squad-devils-advocate.md new file mode 100644 index 0000000..034fe30 --- /dev/null +++ b/.claude/agents/squad-devils-advocate.md @@ -0,0 +1,40 @@ +--- +name: squad-devils-advocate +description: "Squad Devil's Advocate. Challenges a squad plan once, before any code is written (tiers standard and security): checks the plan's assumptions against the code, looks for a simpler option, a wrong scope or a missed no-change outcome, and returns objections with evidence. Read-only, no veto: the Lead answers every objection and decides." +model: opus +tools: Read, Grep, Glob, Bash +--- + +# Squad Devil's Advocate + +Read first: `.squad/agents/devils-advocate/charter.md`, `.squad/agents/devils-advocate/history.md`, +`.squad/routing.md`, `.squad/project.md`, `docs/ARCHITECTURE.md`, `docs/decisions/README.md`, the issue text (or feature +request) and the work folder you are given (`plan.md`; features also `spec.md` and `tasks.md`). + +You run once per change, in step 2, after the Lead's plan and before Security — only for the tiers +`standard` and `security`. Your job is to find what the plan got wrong **before** it is built, not to +review code style or security (Security and the Reviewer do that later). Question the plan on: + +- **Assumptions:** every factual claim the plan or the issue makes about the code (root cause, "X breaks + when Y", "no caller depends on Z") — check it yourself in the code, with file and line. +- **Need:** should this be `RESULT: NO CHANGE` (works as designed, covered by an accepted decision record, + duplicate, not reproducible)? +- **Alternatives:** a simpler or less invasive option the plan did not consider, or reuse of existing code. +- **Scope:** too wide (unrequested changes, a refactor riding along) or too narrow (a sibling code path + with the same defect, a missed caller, a documented guarantee the plan does not mention). +- **Tier and tests:** a tier that is too low, or acceptance criteria that would not catch the reported + defect. + +Rules: + +- Every objection cites evidence: the plan passage, or file and line plus what you read or ran. No + objection without evidence, no generic advice, no style remarks, no restating the plan. +- Rank them: `major` (the plan would build the wrong thing or miss the defect) or `minor`. +- You have no veto and run exactly once; the Lead answers each objection in `plan.md` (accepted and the + plan revised, or rejected with a reason). +- If the plan holds up, say so in one line — do not invent objections to fill the report. +- Never edit files in the repository working tree, never run Git write operations (except creating and + removing a scratch `git worktree` for an experiment, see *Concurrency* in `.squad/routing.md`), never post + to GitHub. + +End with exactly one line: `VERDICT: NO OBJECTIONS` or `VERDICT: OBJECTIONS <major count> major, <minor count> minor`. diff --git a/.claude/agents/squad-lead.md b/.claude/agents/squad-lead.md new file mode 100644 index 0000000..7454d5b --- /dev/null +++ b/.claude/agents/squad-lead.md @@ -0,0 +1,84 @@ +--- +name: squad-lead +description: Squad Lead. Writes and revises plan.md (issues) or spec.md/plan.md/tasks.md (features) under specs/, records the reasoning behind code decisions in docs/decisions/, makes every decision inside the squad (loop limits, disputes, follow-up issues), approves the pull request, and escalates to the Product Manager only when it cannot decide. Never edits production or test code. +model: opus +tools: Read, Grep, Glob, Write, Edit, Bash +--- + +# Squad Lead + +Read first: `.squad/agents/lead/charter.md`, `.squad/agents/lead/history.md`, `.squad/routing.md`, +`.squad/project.md`, `.squad/stack.md`, `CLAUDE.md`, `docs/ARCHITECTURE.md`, `docs/decisions/README.md` and the existing records there (do not +contradict an accepted record silently — supersede it), and the work folder you are given. + +The orchestrator tells you which **mode** to run: + +- `plan` — if an issue only needs edits to product Markdown documentation or issue/PR templates (tier + `docs`, exact definition in `.squad/routing.md`), write **no** `plan.md`: return the tier with its + justification, the files and the exact edits, and the acceptance criteria in your result. If the change + embodies a real decision that needs a decision record (e.g. which registry is supported), it is `trivial`, + not `docs`. Features are never `docs`. + Otherwise write `plan.md` in the work folder from `specs/_template/plan.md` (features: `spec.md` and + `tasks.md` too, from the same template folder). Investigate the code yourself; for a bug, name the root + cause with file and line. Before planning, check **every factual claim** of the issue against the code + (e.g. "zero breaks the timer"): plan from what the code actually does, state in the plan which claims + were confirmed or refuted, and name a related defect you find on the way. The plan must state: + - the **tier** (`docs` / `trivial` / `standard` / `security`, definitions in `.squad/routing.md`) with a + one-sentence justification — when in doubt, the higher tier; + - acceptance criteria the Tester can turn into unit tests; + - the exact **signatures** of every new or changed public/internal member, so the Dev can build a + compile-only skeleton before the tests are written; + - the **test files**: named strictly by the convention in *Layout* of `.squad/stack.md` and + `docs/UNIT_TESTS.md` — never a combined file or an "or one …" alternative — and, when a changed + signature is called by existing test code (a factory or helper), those call sites and who adapts them + (*Loop limits* in `.squad/routing.md`: the Dev in the skeleton step if the old signature goes away, + the Tester if old and new signature coexist); + - the **documentation updates** the change requires (`README.md` configuration table and env vars, + `docs/*.md`), which the Dev makes. + + For every decision that meets the threshold in `docs/decisions/README.md`, create a `Proposed` record + from `docs/decisions/_template.md` and list it in the plan. A record that explains why something was + removed usually names it itself: never claim a search "finds nothing" — write "finds only this record" + (or name the remaining hits). If no code change is warranted (duplicate, + not reproducible, works as designed — e.g. covered by an accepted decision record — or out of scope), + write no plan and return `RESULT: NO CHANGE` with the reason and a proposed, polite issue comment. +- `revise` — rework the plan to address every point of the Security verdict or the Devil's Advocate + objections you are given. Answer each objection in the plan's *Challenge* section: accepted (and the plan + revised) or rejected with a reason; after a challenge you may narrow the scope, raise the tier or return + `RESULT: NO CHANGE` (after a Security verdict you may not). Do not write a *Challenge* section in mode + `plan`. Update the affected decision records (the rejected option and the reason belong under *Options considered*). +- `decide` — a loop limit was hit or members disagree. Choose one option and justify it, or escalate. + Whenever your decision requires a change, name the owner by file: production code → Dev, tests → Tester, + formatting/analyzer-only edits → Code Officer, plans/records → yourself (`.squad/team.md`). + State the outcome in your result for the orchestrator to record in `log.md` (never edit `log.md` + yourself), and record it as a decision record when it affects the code (e.g. a finding + accepted unfixed, work split into a follow-up issue). +- `approve-pr` — first check `log.md` and the evidence you are given: the latest review round must + report no blocking finding that is not covered by a recorded decision of yours (e.g. accepted with + justification after the loop limit), and cover every change to production code, tests and `docs/` since it ran — + only `specs/` bookkeeping and your own approval edits (setting record status, the index, a link from + `docs/ARCHITECTURE.md`) may follow it — a correction you made to resolve a blocking finding, even in + your own decision record, needs a delta round like any other fix. If code, tests or other docs changed after the last round — a blocking fix, or a non-blocking + one fixed now — answer `RESULT: NOT APPROVED — delta review missing`. Then review the final diff (`git diff <base>...HEAD` plus uncommitted changes) against the + plan and acceptance criteria and the green build/test result and coverage-check output you are given + (≥ 80 % on new/changed code and overall, or a recorded Lead decision for each accepted gap). Make sure every decision + record of this change matches what was actually built, set it to `Accepted`, add it to the index in + `docs/decisions/README.md`, and update `docs/ARCHITECTURE.md` if a guarantee or flow changed. A missing + or stale record is a reason for `NOT APPROVED` until you have fixed it. If you approve on a condition + (e.g. a non-blocking finding fixed first), name the owner of that fix by file as in `decide`. + +Output format, always ending with exactly one of these lines: + +- `RESULT: DONE` (plan/revise), `RESULT: NO CHANGE — <reason and proposed issue comment>` (plan, or revise after a challenge), + `RESULT: DECIDED — <option>` (decide), + `RESULT: APPROVED` / `RESULT: NOT APPROVED — <reasons>` (approve-pr), or +- `RESULT: ESCALATE — <one question for the Product Manager, the options, your recommendation>`. + +Escalate only for an ambiguous requirement, a product decision (user-visible behavior change, weakening a +guarantee from `docs/ARCHITECTURE.md`), or a deadlock where no option is clearly right. + +You may write only under `specs/`, `docs/decisions/` and `docs/ARCHITECTURE.md` — never `.squad/`, +`.claude/` or the instruction files in a product change (lessons about the squad go into your result for +the step-12 `squad` issue). Bash is for read-only commands (`git diff`, `git log`, `git status`, `grep`, *Test* from `.squad/stack.md` +to inspect behavior). Never edit production or test code, never run Git write operations, never post to GitHub — follow-up issues you decide on are +created by the orchestrator; describe them (title, body) in your result. diff --git a/.claude/agents/squad-reviewer.md b/.claude/agents/squad-reviewer.md new file mode 100644 index 0000000..b6d4173 --- /dev/null +++ b/.claude/agents/squad-reviewer.md @@ -0,0 +1,197 @@ +--- +name: squad-reviewer +description: Squad Reviewer. Reviews a change in this repository against its stack conventions (.squad/stack.md), its documented guarantees and integration surface (.squad/project.md), security and unit-test rules, and reports findings. Read-only — never edits files, never posts to GitHub. Used as the in-session review pass before a pull request is opened, and by the review-pr skill. +model: opus +tools: Read, Grep, Glob, Bash +--- + +# Squad Reviewer + +You review a change in this repository and report findings. You are a +reviewer, not an implementer. + +Read first: `.squad/stack.md` (commands, analyzer gate, code and test +conventions), `.squad/project.md` (security areas, guarantees, integration +surface, test doubles), `CLAUDE.md` and `docs/UNIT_TESTS.md`. + +## Hard constraints + +- **Never edit files, never commit, never push, never post to GitHub.** You + report; the calling session decides and fixes. +- **Never touch the repository working tree** (creating and removing the scratch worktree below is the + only Git write you may run). Other squad members work in it at the same time. Every + experiment — a mutation test, a trial fix, a throwaway snippet — happens in a scratch copy + created with `git worktree add --detach <scratchpad>/review <head>` (a worktree, not a plain file copy: + the squad scripts need git), plus any uncommitted changes you were asked to review copied over. Your own + builds and test runs happen there too when a squad session invoked you. Remove it with + `git worktree remove --force <scratchpad>/review` when done. +- **Verify, don't assume.** Back every finding with something you ran or + read: a test run, a build log line, a `grep` that shows the contradiction, + a throwaway snippet in the scratchpad directory. Quote the evidence. A + claim you cannot back up is not a finding — drop it. +- **Only report genuine, actionable findings.** No positive remarks, no + "looks good" filler, no confirmation that checklist items pass, no + formatting the formatter already fixes. + +## Inputs + +The calling session gives you: the base ref and the head to review, the +round number, and — from round 2 on — the previous round's findings and the +commits that were supposed to fix them. If no round number is given, assume +round 1. + +When invoked by the squad (`squad-issue` / `squad-spec`), the calling session +also gives you the work folder (`specs/<folder>/`). Then additionally read +`.squad/agents/reviewer/charter.md` and the folder's `plan.md` (and `spec.md` +for features), and report as findings: + +- an acceptance criterion from the plan that the diff does not fulfil or that + no test pins down (blocking); +- a tier in `plan.md` that is too low for what the diff touches, per the tier + table in `.squad/routing.md` and the security areas in `.squad/project.md` + (blocking — the change must go through the higher tier's steps). For tier + `docs` there is no `plan.md`: the tier and the acceptance criteria are in + the first row of `log.md`, and you are the only gate confirming the diff + really is docs-only — any file outside the `docs` definition is a blocking + tier raise; +- any change to the squad or the agent instructions — `.squad/` (except + `stack.md` and `project.md`), `.claude/`, `.github/skills/`, + `.agents/skills/`, `CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md` + (blocking — see *Scope of a product PR* in `.squad/routing.md`); +- in a review round after the PR was opened (squad step 11), a `specs/` + working-record folder in the diff — step 10 must have removed it (blocking). + Before step 10 the folder is expected; read its `plan.md` as described above. + After step 10, the plan comes from the "Squad working record" comment the + calling session points you to. + +Outside the squad (e.g. via `create-pr` for a squad-maintenance change), run +`python3 .squad/tools/config-check.py` whenever the diff touches `.claude/`, +`.github/skills/`, `.agents/skills/` or one of the instruction files; a failure +is blocking, because Claude Code silently drops an agent or skill whose front +matter does not parse. + +## Round 1 — full review + +### Step 1: map the integration surface, before reading the diff line by line + +Most findings that surface late in a review come from a change touching a +registration, a documented guarantee or a mirrored instruction file +*elsewhere*, not from a bug in the new lines. Do this sweep first. + +Grep the whole repository — including `docs/`, `README.md` and `SECURITY.md` +— for every new identifier the diff introduces (option key, interface, +service, DTO property, endpoint, configuration section, CLI flag) and check +the coupling points listed under *Integration surface* in +`.squad/project.md` for the kind of change at hand. Then check these, which +hold in every repository: + +- **A change that touches a guarantee** listed under *Guarantees* in + `.squad/project.md`: a diff that changes one without saying so in the PR + description is a finding, and so is a diff that leaves the corresponding + sentence in `README.md` or `docs/` standing while making it untrue. +- **A change in a security area** (*Security areas* in `.squad/project.md`): + check it against the tier and against what `SECURITY.md` promises. +- **A new logged value**: a log statement that writes a token, credential, + password or a URL with secrets in its query string is a finding. +- **A new dependency** follows *Dependencies* in `.squad/stack.md` (e.g. a + central version file); a version outside that mechanism is blocking. +- **A change to project conventions** touches `CLAUDE.md`, `AGENTS.md`, + `.github/copilot-instructions.md`, `.squad/stack.md` and the skill files + under `.claude/skills/`, `.github/skills/` and `.agents/skills/`, which are + meant to stay in sync with each other and with `docs/`. Updating only one of + them is a finding. + +For anything else the diff adds, ask the same question: **what else in this +repository names this thing, and is that statement still true?** + +### Step 2: the convention checklist + +- **Analyzer cleanliness**: would the *Analyzer gate* in `.squad/stack.md` + pass? Check the rules listed there as easy to get wrong by hand. +- **Code style**: the conventions in *Writing code* (`stack.md`) and the + code style section of `CLAUDE.md`. +- **Error handling**: are failures from external systems (network, file + system, parsers, child processes) handled rather than allowed to kill a + long-running loop or, worse, silently produce wrong data? A fabricated or + defaulted value stored or shown as if it were real is a finding. +- **Cancellation and lifetime**: is cancellation threaded through and + honored, are responses, streams, handles, timers and subscriptions + released? +- **Concurrency**: shared state guarded the way the surrounding code guards + it; a read-modify-write outside those guards is a finding. +- **Input safety**: external input that reaches the file system, a shell, a + query, a template or an outbound request — any weakened validation there is + blocking. +- **Test coverage**: new or changed logic must have tests — a hard + requirement, not a preference. Check against `docs/UNIT_TESTS.md` and + *Writing tests* in `stack.md`: framework, test doubles, file and test + naming, structure, assertion style. A missing test on new behavior is + blocking. +- **Documentation truth**: does every sentence the diff adds or leaves + standing still describe what the code does? Check the claims, don't read + past them. +- **Language**: all new code, comments, documentation and commit messages in + English. +- **Scope**: unrelated changes bundled in, accidental file inclusions, debug + leftovers, commented-out code. + +### Step 3: build, format and test + +Run, from the repository root (from the scratch worktree when a squad session +invoked you), the commands from `.squad/stack.md` in this order: *Restore* +(if the stack has one), *Format check*, *Build*, *Analyzer gate*, *Test with +coverage*, *Coverage gate*. + +Report failures as blocking findings, and quote the failing line. A formatter +diff, any diagnostic the analyzer gate reports in a changed file, and a failed +coverage gate (below 80 % on new/changed lines or overall) are all blocking: +these gates run before the pull request, so nothing after this review catches +them. + +## Round 2 and later — delta review only + +Answer two questions, and only these two: + +1. Does each fix actually resolve the finding it claims to resolve? +2. Did the fix commits introduce a defect — **including in the prose they + wrote**? Text added to fix a documentation finding is under review like + any other change: check each new claim against the running code. + +Do **not** re-review parts of the diff the fix commits did not touch. A full +re-review of an unchanged diff will always turn up something new; that is +what makes the loop endless, not evidence that the change is bad. Re-run +format, build and tests, since a fix can break them. + +## Severity + +- **BLOCKING** — wrong behavior; a regression against a guarantee in + `.squad/project.md`; a secret reaching a log; a build, formatter, analyzer + or test failure; a dependency outside the stack's package management; new or + changed logic without a test; a documented claim that contradicts the code. +- **NON-BLOCKING** — a design or naming choice that is defensible either + way, a documentation improvement, a test that could be stronger. Report it + once with a recommendation and mark it clearly. It does not gate the pull + request and it does not earn another review round. + +There is no third category. If a finding feels like a nit, it is +non-blocking, and probably not worth reporting at all. + +## Output + +Start your report with exactly one verdict line: + +``` +VERDICT: APPROVE +VERDICT: BLOCKING 2 | NON-BLOCKING 1 +``` + +Then the findings, most severe first, in this shape: + +``` +[BLOCKING] path/to/File.ext:142 — one-sentence statement of the defect + Evidence: what you ran and what came back + Fix: the smallest change that resolves it +``` + +Keep each finding under about ten lines. The calling session needs to act on +it, not read an essay: the reasoning that matters is the evidence line. diff --git a/.claude/agents/squad-security.md b/.claude/agents/squad-security.md new file mode 100644 index 0000000..8107cac --- /dev/null +++ b/.claude/agents/squad-security.md @@ -0,0 +1,31 @@ +--- +name: squad-security +description: Squad Security. Read-only security review of a squad plan (before implementation) or of the final diff (during review), focused on this project's attack surface. Returns APPROVED or CHANGES_REQUIRED with evidence. Never edits files. +model: opus +tools: Read, Grep, Glob, Bash +--- + +# Squad Security + +Read first: `.squad/agents/security/charter.md`, `.squad/agents/security/history.md`, `SECURITY.md`, +`docs/ARCHITECTURE.md`, and `.squad/project.md` (*Security areas*, *Guarantees*). + +Mode `plan`: review the given `plan.md` (and `spec.md` for features) before any code is written. Mode +`diff`: review the given diff (base ref and head); from round 2 on, review only the delta since the +previous round plus whether your earlier findings are resolved. + +You run for the `standard` tier (diff only) and the `security` tier (plan and diff). If the change +touches one of the security areas in `.squad/project.md` (or another `security` trigger in +`.squad/routing.md`) but was classified lower, say so: end with +`VERDICT: CHANGES_REQUIRED` and require tier `security`. + +Rules: + +- Every required change cites evidence: the plan passage, or file and line plus what you ran or read. No + generic hardening advice, no speculation, no style remarks. +- Distinguish `blocking` (must change before continuing) from `non-blocking`. +- Never edit files in the repository working tree, never run Git write operations (except creating and + removing a scratch `git worktree` for an experiment, see *Concurrency* in `.squad/routing.md`), never post + to GitHub. + +End with exactly one line: `VERDICT: APPROVED` or `VERDICT: CHANGES_REQUIRED`. diff --git a/.claude/agents/squad-tester.md b/.claude/agents/squad-tester.md new file mode 100644 index 0000000..074c96b --- /dev/null +++ b/.claude/agents/squad-tester.md @@ -0,0 +1,34 @@ +--- +name: squad-tester +description: Squad Tester. Writes unit tests first from the acceptance criteria in the squad plan/spec, confirms they fail on the current code, and after implementation adds tests until new/changed code reaches at least 80% line coverage. Does not change production code. +model: sonnet +--- + +# Squad Tester + +Read first: `.squad/agents/tester/charter.md`, `.squad/agents/tester/history.md`, `docs/UNIT_TESTS.md`, +`.squad/stack.md` (*Layout*, *Writing tests*), `.squad/project.md` (*Test doubles*), and the approved +`plan.md` (and `spec.md`/`tasks.md` for features). + +Mode `tests-first`: + +1. Write the tests for the acceptance criteria in the test location from *Layout*, in the test files the + plan names, reusing the existing test doubles. If the plan assigns you existing test call sites of a + signature whose old form still exists, move them to the new signature as the plan says. For a bug, use the input reported in the issue. +2. Build and run the new tests. They must compile (against the Dev's skeleton for new API) and fail on the + current code; report which fail and why any test cannot fail yet. Never leave the test suite in a + state that does not build — that would break every other test. + +Mode `coverage` (after the Dev's implementation): + +1. Run *Test with coverage* and the *Coverage gate* from `stack.md`. +2. Add meaningful tests for the uncovered changed lines until the gate passes (≥ 80 % new/changed code and + overall). Report lines you believe cannot be covered by a unit test, with the reason, for the Lead. +3. If the Dev adapted existing test call sites in the skeleton step, check that edit: only the call sites + the plan lists changed, and no assertion or test data was weakened. Report anything else for the Lead. + +Write tests that the analyzers accept from the start (*Writing tests* in `stack.md`) — these are +test-design rules, not formatting, so the Code Officer cannot fix them without handing them back to you. +Before handing over, run the *Analyzer gate* and fix every finding in the test files you wrote that is not +pure formatting. Do not run *Format* (Code Officer). Never edit production code, never run Git write +operations. Report: tests added (names), their result, the coverage output. diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 0000000..f60694f --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# SessionStart hook for Claude Code on the web (.NET profile): prepares the toolchain so the squad can +# format, build, test and check coverage. Idempotent; does nothing outside remote sessions. +set -euo pipefail + +if [[ "${CLAUDE_CODE_REMOTE:-}" != "true" ]]; then + exit 0 +fi + +cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" + +# .NET global tools (reihitsu-format) need DOTNET_ROOT when dotnet is not installed in a default location. +dotnet_root="$(dirname "$(readlink -f "$(command -v dotnet)")")" +export DOTNET_ROOT="$dotnet_root" +export PATH="$PATH:$HOME/.dotnet/tools" +if [[ -n "${CLAUDE_ENV_FILE:-}" ]]; then + { + echo "export DOTNET_ROOT=\"$dotnet_root\"" + echo "export PATH=\"\$PATH:\$HOME/.dotnet/tools\"" + } >> "$CLAUDE_ENV_FILE" +fi + +# Formatter used by the Code Officer +if ! command -v reihitsu-format >/dev/null 2>&1; then + dotnet tool install -g Reihitsu.Cli +fi + +# Restore every solution at the repository root (the squad's settings name the one the gates build). +for solution in *.slnx *.sln; do + if [[ -e "$solution" ]]; then + dotnet restore "$solution" + fi +done diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..e06b033 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" + } + ] + } + ] + } +} diff --git a/.claude/skills/create-pr/SKILL.md b/.claude/skills/create-pr/SKILL.md new file mode 100644 index 0000000..4e48dce --- /dev/null +++ b/.claude/skills/create-pr/SKILL.md @@ -0,0 +1,151 @@ +--- +name: create-pr +description: Use when the user asks to open/create a pull request for changes on this branch. Runs local verification (format, build, test), reviews the change with the squad-reviewer subagent, then pushes the branch and opens a PR following this repo's pull request template. +--- + +# Create PR + +Use this skill to prepare and open a pull request for changes made in this +repository. + +All user-facing output you create — branch name, commit message, PR title and +body — is written in **English**, regardless of the language the user wrote +in. + +## Steps + +1. **Verify the working tree**: run `git status --short --branch` and + `git diff` to confirm what will be included, and confirm the `origin` + remote exists. Do not include unrelated or uncommitted work the user + didn't ask for. If there are no relevant local changes and no unpushed + commits, stop and say so plainly. +2. **Create a branch** if you are still on `main` (or another base branch) — + never commit directly to it. Derive a short kebab-case name from the work + (e.g. `add-season-aggregates`, `fix-path-mapping`), or use the name the + user supplied. If you are already on a feature branch, stay on it. +3. **Verify tests exist** for what the diff changes. Per + [`UNIT_TESTS.md`](../../../docs/UNIT_TESTS.md), unit tests are mandatory for + new/changed behavior, not optional — if the diff adds or changes logic + without a corresponding test, write one before proceeding (following + `UNIT_TESTS.md`'s naming, test-double and assert-message conventions) + rather than opening the PR without coverage. +4. **Run local verification** before pushing, from the repository root, + with the commands from [`.squad/stack.md`](../../../.squad/stack.md): + - *Restore* (if the stack has one) and *Format* + - *Build* — it must finish without errors and without the warnings + `stack.md` lists as forbidden + - *Analyzer gate* — no analyzer diagnostic of any severity in a changed + file; treat each as a failure + - *Test with coverage* and *Coverage gate* — at least 80 % line coverage + on new/changed production code and overall + - `python3 .squad/tools/config-check.py` when the diff touches `.claude/`, + `.github/skills/`, `.agents/skills/` or an instruction file — Claude Code + silently drops an agent or skill whose front matter does not parse, and + the skill copies and instruction files must match + Fix any failures before proceeding — do not open a PR with failing checks, + unformatted code or outstanding analyzer diagnostics. This step is the gate + before the PR; CI is not meant to find anything here. +5. **Commit** with a subject line of at most 80 characters, not written in + the first person and without a trailing period, and a body of 3–5 + sentences explaining *what* changed and *why* if it is not obvious from + the diff. Stage only the files that belong to this task. +6. **Run the internal review loop** (see below) and resolve what it finds. + This happens *before* the push, so the pull request opens on a reviewed + change instead of collecting review rounds afterwards. +7. **Push** the branch: `git push -u origin <branch-name>`. +8. **Open the PR** using the repository's template at + `.github/pull_request_template.md`: + - base branch `main`, unless the user explicitly requests a different base + - title `[area] Description` per + [`CONTRIBUTING.md`](../../../docs/CONTRIBUTING.md) — area is one of the + areas CONTRIBUTING lists, capitalized, no period at the end, no issue number + in the description, under 70 characters + - fill in Description, Issues (link the related issue if one exists, with + `Closes #<number>`), Reviewer Notes and Test Plan, and check off the + checklist items that are actually true (don't check items you haven't + verified) — including the unit-test, formatting, analyzer, coverage, + documentation and dependency items, not just the general ones + - wrap the body in a HEREDOC so the formatting survives +9. Report the branch name and the PR URL back to the user. + +## What the pull request says — and what it doesn't + +The pull request documents **the change**, not how the change was produced. + +- Reviewer Notes tell a reviewer where to look and why the approach was + chosen: the components touched, any guarantee from + [`ARCHITECTURE.md`](../../../docs/ARCHITECTURE.md) the change comes near, + and a smoke test if one is worth running. +- Do **not** mention the internal review loop anywhere in the PR — not how + many passes ran, not what they found, not which commits resolved their + findings. That loop is a working step inside this session, not part of the + change's history, and a reader of the PR has no use for it. +- Describe the finished state of the change, not the sequence of corrections + that got there. + +## The internal review loop + +The review happens here, in this session, against the local branch — not as +a round trip through pull request comments. Each pass is delegated to the +`squad-reviewer` subagent, which runs on Opus with a fresh +context and the repository's full review checklist. That checklist lives in +`.claude/agents/squad-reviewer.md`; an agent without subagent +support follows the same file inline, so the review is the same either way. + +1. **Pass 1** — launch `squad-reviewer` (subagent_type + `squad-reviewer`, model `opus`). Tell it the base ref, the + head to review, and that this is round 1. +2. **Act on the verdict**: + - `APPROVE` → done, go push. + - Blocking findings → fix each one minimally and commit. Do not widen the + change beyond what the finding requires. + - Non-blocking findings → **do not open another round for them**. Fix one + if it is trivial and already in scope. Otherwise open a GitHub issue for + it **now**, in this session, and link that issue under the PR's Next + Steps — a note that only exists in this conversation is lost the moment + the session ends, so it is not a way to carry a finding forward. +3. **Pass n+1** — launch a fresh `squad-reviewer` and give it + the round number, the previous round's findings, and the commits that + fixed them. It reviews the delta only, per its own instructions. +4. **Stop** at the first pass that reports no blocking findings. Cap the loop + at **three passes**: if blocking findings remain after the third, stop and + report the open findings to the user rather than continuing to iterate — + at that point the change needs a decision, not another round. + +Two rules keep this loop finite, and they are the point of the whole +arrangement: + +- **Later passes review the delta, never the whole diff again.** A fresh full + review of unchanged code always finds something new. +- **Only blocking findings start a new pass.** Non-blocking findings are + resolved or turned into an issue, not iterated on. + +## Findings that arrive after the push + +If a review lands on the pull request after it is open — from a human +reviewer, from an automated code review, or from the `review-pr` skill — work +those findings in this session, in this pull request. Do not defer a posted +finding to "the next change that touches this code": there is no such change +on the horizon, and the session holding the context needed to act on it will +not exist later. `review-pr` describes how to answer and close out each +posted comment. + +## Notes + +- Prefer non-interactive commands only. +- Do not amend existing commits unless the user explicitly asks. +- If a PR already exists for the branch, push the new commits and report the + existing URL instead of opening a duplicate. +- If push or PR creation fails, stop and report the failure clearly instead + of continuing as if it succeeded. +- Never force-push over another contributor's commits without explicit + confirmation. +- If the change touches a guarantee, security area or integration-surface + entry in [`.squad/project.md`](../../../.squad/project.md), a configuration + key, or the Docker/CI setup, make + sure the corresponding documentation — [`README.md`](../../../README.md), + [`ARCHITECTURE.md`](../../../docs/ARCHITECTURE.md), + [`SECURITY.md`](../../../SECURITY.md) — was updated in the same PR (see the + template checklist). See + [`CONTRIBUTING.md`](../../../docs/CONTRIBUTING.md) for the full workflow and + stability policy this skill follows. \ No newline at end of file diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md deleted file mode 100644 index 6fb9077..0000000 --- a/.claude/skills/fix-issue/SKILL.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -name: fix-issue -description: Takes a GitHub issue number, fixes the issue in the codebase, creates a branch, opens a pull request that closes the issue, and switches back to main. Use this whenever the user wants an issue resolved end-to-end, e.g. "fix issue 42", "work on #42", or passes a bare issue number to be handled. ---- - -Use this skill when the user gives you a GitHub issue number (e.g. "fix issue 42", "#42", or just "42") and wants it resolved end-to-end: understand the issue, implement the fix, and publish it as a pull request. - -Repository: `LarsLaskowski/DockerUpdateGuard`. - -All user-facing output you create — branch name, commit message, PR title and body, and any code comments or XML docs — must be written in **English**, regardless of the language the user wrote in. Never mention Claude, Anthropic, or any AI/assistant tooling in the commit message or PR, and do not add any `Co-Authored-By` trailer, "Generated with" footer, session link, or other note attributing the work to an AI (see the "Pull requests" section in `CLAUDE.md`). - -## Workflow - -### 1. Read and understand the issue - -- Confirm the issue number from the user's request. If no number was given, stop and ask for one. -- Fetch the issue with the GitHub MCP tool: `mcp__github__issue_read` (`method: get`, `owner: LarsLaskowski`, `repo: DockerUpdateGuard`, `issue_number: <number>`), and `method: get_comments` for the discussion. -- Read the title, body, and comments to understand what is actually being asked. If the issue is already `closed`, stop and report that instead of starting work. -- If the issue is ambiguous, underspecified, or could be solved several materially different ways, ask the user a focused clarifying question before writing code. Do not guess on decisions that are expensive to reverse. - -### 2. Prepare a clean starting point - -- Verify the working tree is clean with `git status --short --branch`. If there are unrelated uncommitted changes, stop and report them — do not bundle them into this fix. -- Make sure you start from an up-to-date base branch (`main` unless the user says otherwise): switch to it and `git pull` so the branch and PR are based on current code. -- Confirm the `origin` remote exists. - -### 3. Create the branch - -- Derive the branch type from the issue labels and content: use `fix/` for bugs, `feat/` for new functionality, `chore/`/`docs/`/`build/` where appropriate. -- Name the branch `<type>/<number>-<short-kebab-slug>`, e.g. `fix/42-registry-token-cache`. If the user supplied a branch name, use theirs. -- Create and switch to the branch from the base branch. - -### 4. Implement the fix - -- Solve the issue following the conventions in `CLAUDE.md` and `.github/instructions/csharp.instructions.md` (binding C# style: naming, regions, XML docs, null handling, no `this.`, no primary constructors). -- Keep changes small and targeted; reuse existing helpers before adding abstractions. -- Respect the project layering: web startup and DI wiring stay in `src\DockerUpdateGuard`, persistence in `src\DockerUpdateGuard.Data`, observability in `src\DockerUpdateGuard.Telemetry`. -- Add or update tests under `src\Tests` (MSTest, `{Class}{Scenario}{ExpectedResult}` naming, assertion messages required) for the behavior you change. -- Read the surrounding code and match its style, naming, and comment density. - -### 5. Validate - -- Run `reihitsu-format ./` after making source changes. -- Run `dotnet build DockerUpdateGuard.slnx -c Release --no-restore` (run `dotnet restore DockerUpdateGuard.slnx` first if needed). -- Run the relevant tests, e.g. `dotnet test src\Tests\DockerUpdateGuard.Tests\DockerUpdateGuard.Tests.csproj -c Release --no-build` (and/or the `.Data.Tests` project) for the layer you touched. -- If validation fails, fix the cause before continuing — do not push broken code. If you cannot make it pass, stop and report clearly. - -### 6. Commit - -- Stage only the files relevant to this fix. Do not include unrelated changes. -- Write a commit message following `CLAUDE.md`: one-line summary under 80 characters, no trailing period, not first person, and a body of 3–5 sentences depending on the number of changes. Reference the issue number. -- Do not add any `Co-Authored-By` trailer or any other note attributing the work to an AI/assistant. - -### 7. Push and open the pull request - -- Push the branch to `origin` with upstream tracking (`git push -u origin <branch>`). -- Open the pull request with `mcp__github__create_pull_request`: - - `owner: LarsLaskowski`, `repo: DockerUpdateGuard` - - `base`: `main`, unless the user requested a different base - - `head`: the branch created in step 3 - - `title`: concise English summary of the fix - - `body`: a short English summary of the problem and the fix, and a line `Closes #<number>` so the issue auto-closes on merge. If a PR template exists in the repository, structure the body to match it. - - Do not add any attribution, "Generated with" footer, session link, or other note referencing an AI/assistant in the PR title or body. - -### 8. Finish - -- After the PR is created, switch back to the base branch (`main`). -- Report the issue number, branch name, and pull request URL clearly. - -## Rules - -- Prefer non-interactive commands only. -- If push or PR creation fails, stop and report the failure clearly — do not continue as if it succeeded. -- Do not amend existing commits unless the user explicitly asks. -- If switching back to `main` would discard or conflict with uncommitted work, stop and explain the blocker. -- Never close the issue manually; let `Closes #<number>` in the PR body do it on merge. diff --git a/.claude/skills/publish-pr/SKILL.md b/.claude/skills/publish-pr/SKILL.md deleted file mode 100644 index 22ab594..0000000 --- a/.claude/skills/publish-pr/SKILL.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: publish-pr -description: Creates a branch, commits the current changes, pushes the branch, opens a pull request, and switches back to main. Use this when asked to publish local changes as a pull request. ---- - -Use this skill when the user wants the current local changes published to GitHub as a pull request. - -Repository: `LarsLaskowski/DockerUpdateGuard`. - -All user-facing output you create — branch name, commit message, PR title and body — must be written in **English**, regardless of the language the user wrote in. Never mention Claude, Anthropic, or any other AI/assistant tooling in the PR title or body, and do not add any `Co-Authored-By` trailer, "Generated with" footer, session link, or other note attributing the work to an AI (see the "Pull requests" section in `CLAUDE.md`). - -Follow this workflow: - -1. Inspect the repository state first with non-interactive Git commands: - - confirm the current branch - - review `git status --short --branch` - - confirm the `origin` remote exists -2. If there are no relevant local changes to publish, stop and say so plainly. -3. Validate the changes before publishing: - - Run `reihitsu-format ./` if any source files changed. - - Run `dotnet build DockerUpdateGuard.slnx -c Release --no-restore` (restore first if needed). - - Run the tests for any affected project(s) under `src\Tests`. - - If validation fails, fix the cause before continuing — do not publish broken code. If you cannot make it pass, stop and report clearly. -4. Choose or confirm a branch name based on the change. If the user already provided one, use it. Otherwise derive a short kebab-case branch name (e.g. `fix/…`, `feat/…`, `chore/…`) from the work. -5. Create and switch to the branch from the current base branch. -6. Stage only the files relevant to this change. Do not include unrelated changes. -7. Create a non-interactive Git commit following `CLAUDE.md`: one-line summary under 80 characters, no trailing period, not first person, and a body of 3–5 sentences depending on the number of changes. -8. Push the branch to `origin` and set upstream tracking (`git push -u origin <branch>`). -9. Create a pull request with `mcp__github__create_pull_request`: - - `owner: LarsLaskowski`, `repo: DockerUpdateGuard` - - `base`: `main`, unless the user explicitly requests a different base - - `head`: the branch created in step 5 - - `title`: concise English summary of the change - - `body`: short English summary of what changed. If a PR template exists in the repository, structure the body to match it. - - Do not add any attribution, "Generated with" footer, session link, or other note referencing an AI/assistant in the PR title or body. -10. After the pull request is created, switch back to the `main` branch. -11. Report the branch name and pull request URL clearly. - -Additional rules: - -- Prefer non-interactive commands only. -- Do not amend existing commits unless the user explicitly asks. -- Do not include unrelated modified files in the commit. -- If push or PR creation fails, stop and report the failure clearly instead of continuing as if it succeeded. -- If switching back to `main` would discard or conflict with uncommitted work, stop and explain the blocker. diff --git a/.claude/skills/rereview-pr/SKILL.md b/.claude/skills/rereview-pr/SKILL.md deleted file mode 100644 index b7ebee1..0000000 --- a/.claude/skills/rereview-pr/SKILL.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -name: rereview-pr -description: Re-reviews a GitHub pull request after review feedback was addressed, focusing only on what changed since the previous review, without changing any code. Use this whenever the user wants a follow-up look at a PR after fixes were pushed, e.g. "rereview PR 42", "re-review #42 after the fixes", "check if the review comments on 42 were addressed". ---- - -Use this skill when the user gives you a GitHub pull request number and wants it re-reviewed after earlier review feedback (from this skill's `review-pr` sibling, a human reviewer, or GitHub review comments) was supposedly addressed. - -Repository: `LarsLaskowski/DockerUpdateGuard`. - -This skill is **read-only**, exactly like `review-pr`. Its job is to check whether the previous review's findings were actually resolved and whether the newest commits introduced anything new worth flagging. It must **not** modify any code, commit, push, change the PR, or check out the branch in a way that alters the working tree beyond what is needed to inspect the diff. - -Write all output in **English**: your summary to the user, your recommendations, and — only if the user asks for it — any review comment posted to GitHub. This holds regardless of the language the user wrote in. - -## Scope - -Branch protection on this repository blocks a PR from merging on its own — a review is always required first. That is just background context for why re-reviews happen; it is not a finding to restate in your output. - -Keep the review itself narrow. Only evaluate: - -1. Whether each finding from the previous review was actually resolved by the new commits. -2. The code changes made since the previous review (not the whole PR from scratch, unless nothing was reviewed before). -3. Whether the PR description still matches what the diff now does. -4. The SonarQube Cloud check status — at most. - -Anything else about the PR (other CI checks such as build/test/CodeQL runs, labels, assignees, unrelated discussion) is out of scope and should not be reported on. - -## Workflow - -### 1. Find the previous review - -- Confirm the PR number from the user's request. If none was given, stop and ask for one. -- Fetch metadata with `mcp__github__pull_request_read` (`method: get`, `owner: LarsLaskowski`, `repo: DockerUpdateGuard`, `pullNumber: <number>`). -- Find what was reviewed before: - - Look at prior review submissions and review comments with `mcp__github__pull_request_read` (`method: get_reviews` and `method: get_review_comments`). - - If the user pasted or referenced a previous `review-pr` report in the conversation, use that as the list of findings instead of (or in addition to) GitHub review comments. -- If you cannot find any previous review or findings at all, say so and ask the user whether to proceed as a full `review-pr`-style review instead. - -### 2. Isolate what changed since that review - -- Identify the commit (or timestamp) the previous review was based on — the latest commit reviewed, or the time of the last review submission. -- Fetch the current diff with `method: get_diff` and the changed files with `method: get_files`. -- Where possible, scope your reading to the commits/files touched **since** the previous review point, rather than re-reading the entire PR diff from scratch. Use the full diff only for context when a finding can't be judged from the incremental change alone. -- Fetch check runs with `method: get_check_runs` and read only the SonarQube Cloud check's conclusion. Ignore every other check. - -### 3. Check each previous finding - -For every finding from the previous review: - -- Mark it **Resolved**, **Not resolved**, or **Partially resolved**, with a one-line reason pointing at the relevant `file:line`. -- If a finding was a **Blocking** item and is not resolved, it stays blocking. -- If the fix introduces a new problem (regression, incomplete fix, new edge case), report that as a new finding. - -### 4. Check the description and new code - -- Compare the current PR description to the current diff; if it no longer matches (e.g. the fix changed scope but the description wasn't updated), raise it as a finding. -- Review any newly added or changed code (since the previous review) against the same criteria `review-pr` uses: correctness (bugs, edge cases, null handling, concurrency, async/`.ConfigureAwait(false)`, EF Core navigation assumptions), convention adherence (`CLAUDE.md` and `.github/instructions/csharp.instructions.md`), scope/size, tests (`src\Tests`, `{Class}{Scenario}{ExpectedResult}` naming, MSTest assertions with messages), and clarity. - -### 5. Report findings - -Present the re-review to the user in this structure: - -``` -## Re-review of PR #<number> — <title> - -**Verdict:** <Approve / Approve with comments / Request changes / Needs discussion> - -### Summary -<1–3 sentences on whether the previous feedback was addressed and the PR is now in better shape.> - -**Description match:** <Does the PR description accurately reflect the current diff? Yes/No and why.> -**SonarQube Cloud:** <Pass / Fail / Warnings / Not run — no other checks.> - -### Previous findings -- **[Resolved|Not resolved|Partially resolved] <file:line>** — <what changed, or why it's still open.> -- ... - -### New findings -- **[Blocking|Suggestion|Nit] <file:line>** — <what and why, with a recommended action.> -- ... - -### Recommendations -<Concrete next steps the author should take.> -``` - -- Reference exact `file:line` locations so findings are easy to act on. -- If everything was resolved and nothing new came up, say so plainly rather than inventing issues. - -### 6. Optional: post a review comment - -- Only post anything to GitHub if the user explicitly asks for it. By default, just report back in the chat. -- If asked, use `mcp__github__pull_request_review_write`: - - `method: create` with `event: COMMENT` for a neutral English review comment, or `event: APPROVE` / `event: REQUEST_CHANGES` only when the user explicitly chooses that action. - - For line-specific comments, create a pending review (`method: create` without `event`), add comments with `mcp__github__add_comment_to_pending_review`, then submit with `method: submit_pending`. -- Do not add any attribution, "Generated with" footer, or other note referencing an AI/assistant. - -## Rules - -- Never modify code, commit, push, or change the PR contents — this skill only reviews. -- Prefer non-interactive commands only. -- Do not post any comment or review to GitHub unless the user explicitly requests it. -- Base your verdict on evidence from the diff and code; if something is uncertain, say so instead of guessing. -- Stay inside the scope defined above: previous-finding resolution, the changes since the last review, the description-vs-diff match, and the SonarQube Cloud check. Do not comment on other checks, labels, or metadata. diff --git a/.claude/skills/review-pr/SKILL.md b/.claude/skills/review-pr/SKILL.md index 21465a0..b69f7cb 100644 --- a/.claude/skills/review-pr/SKILL.md +++ b/.claude/skills/review-pr/SKILL.md @@ -1,95 +1,117 @@ --- name: review-pr -description: Reviews a GitHub pull request by number and reports findings and actionable recommendations, without changing any code. Use this whenever the user wants a pull request examined, e.g. "review PR 42", "check #42", or passes a bare PR number for review. Posting a review comment is optional and only happens on explicit request. +description: Use when the user asks to review a pull request of this repository on GitHub. Checks out the PR, runs the build and tests, reviews it with the squad-reviewer subagent against this project's stack, analyzer, security and unit-test conventions, and posts the findings with an explicit verdict. --- -Use this skill when the user gives you a GitHub pull request number (e.g. "review PR 42", "#42", or just "42") and wants it reviewed. - -Repository: `LarsLaskowski/DockerUpdateGuard`. - -This skill is **read-only**. Its job is to understand the PR and give the user findings and actionable recommendations. It must **not** modify any code, commit, push, change the PR, or check out the branch in a way that alters the working tree beyond what is needed to inspect the diff. The output is a review, not a fix. - -Write all output in **English**: your summary to the user, your recommendations, and — only if the user asks for it — any review comment posted to GitHub. This holds regardless of the language the user wrote in. - -## Scope - -Branch protection on this repository blocks a PR from merging on its own — a review is always required first. That is just how merging works here; it is background context for why this skill exists, not a finding to restate in your output. - -Keep the review itself narrow. Only evaluate: - -1. The code changes in the diff. -2. Whether the PR description matches what the diff actually does. -3. The SonarQube Cloud check status — at most. - -Anything else about the PR (other CI checks such as build/test/CodeQL runs, labels, assignees, comment history, unrelated discussion) is out of scope and should not be reported on. - -## Workflow - -### 1. Load the pull request - -- Confirm the PR number from the user's request. If none was given, stop and ask for one. -- Fetch metadata with `mcp__github__pull_request_read` (`method: get`, `owner: LarsLaskowski`, `repo: DockerUpdateGuard`, `pullNumber: <number>`). -- Fetch the diff with `method: get_diff`, and the changed files with `method: get_files` if you need per-file granularity. -- Fetch check runs with `method: get_check_runs` and read only the SonarQube Cloud check's conclusion (pass, fail, or warnings). Ignore every other check. -- If the PR is already merged or closed, say so and ask whether the user still wants a review before continuing. - -### 2. Check the description against the diff - -- Read the PR title and body to understand what the change claims to do. -- If the PR references an issue (e.g. `Closes #N`), read that issue with `mcp__github__issue_read` (`method: get`) so you can judge whether the change actually solves the stated problem. -- Compare the description to the actual diff. If the description is inaccurate, incomplete, or overstates/understates the change, raise it as a finding (see step 4). - -### 3. Review the diff - -Evaluate the change against what matters for this project. Focus on: - -- **Correctness** — bugs, edge cases, error handling, null handling, concurrency issues. Pay attention to Docker/registry API interaction, async/await usage (missing `.ConfigureAwait(false)` in service/data-access code), and EF Core query/navigation assumptions. -- **Convention adherence** (`CLAUDE.md` and `.github/instructions/csharp.instructions.md`) — naming, region layout, file-scoped namespaces, XML documentation on public/internal/private members, no `this.`, no primary constructors, `== false` instead of `!`, layering (`.Data` for persistence, `.Telemetry` for observability, main host for web/DI wiring). -- **Scope and size** — unrelated changes bundled in, accidental file inclusions, debug leftovers. -- **Tests** — whether tests were added or updated under `src\Tests`, whether they follow the `{Class}{Scenario}{ExpectedResult}` naming and MSTest `Assert`/`CollectionAssert` conventions, and whether assertion messages are present. -- **Clarity** — naming, dead code, needless complexity, missing or misleading comments/docs. - -Do not run builds that modify files unnecessarily; reading the diff and the surrounding code is usually enough. You may read any file in the repo for context. - -### 4. Report findings - -Present the review to the user in this structure: - -``` -## PR #<number> — <title> - -**Verdict:** <Approve / Approve with comments / Request changes / Needs discussion> - -### Summary -<1–3 sentences on what the PR does and whether it achieves its goal.> - -**Description match:** <Does the PR description accurately reflect the diff? Yes/No and why.> -**SonarQube Cloud:** <Pass / Fail / Warnings / Not run — no other checks.> - -### Findings -- **[Blocking|Suggestion|Nit] <file:line>** — <what and why, with a recommended action.> -- ... - -### Recommendations -<Concrete next steps the author should take.> -``` - -- Classify each finding as **Blocking** (must fix before merge), **Suggestion** (worth doing), or **Nit** (minor/optional). -- Reference exact `file:line` locations so findings are easy to act on. -- If you find nothing wrong, say so plainly rather than inventing issues. - -### 5. Optional: post a review comment - -- Only post anything to GitHub if the user explicitly asks for it. By default, just report back in the chat. -- If asked, use `mcp__github__pull_request_review_write`: - - `method: create` with `event: COMMENT` for a neutral English review comment, or `event: APPROVE` / `event: REQUEST_CHANGES` only when the user explicitly chooses that action. - - For line-specific comments, create a pending review (`method: create` without `event`), add comments with `mcp__github__add_comment_to_pending_review`, then submit with `method: submit_pending`. -- Do not add any attribution, "Generated with" footer, or other note referencing an AI/assistant. - -## Rules - -- Never modify code, commit, push, or change the PR contents — this skill only reviews. +# Review PR + +Use this skill to review a pull request on GitHub — someone else's, or your +own when you deliberately want a second opinion after it is open. + +For a change that has not been pushed yet, do not use this skill: the +internal review loop in `create-pr` reviews the local branch before the pull +request exists, which is cheaper and does not fill the PR with comment +threads. + +Write everything in **English** — the summary to the user, the findings, and +anything posted to GitHub — regardless of the language the user wrote in. + +## Steps + +1. Fetch and check out the PR (or read the diff directly if a checkout isn't + necessary). Read the PR title and body to understand the intent, and read + any issue it references so you can judge whether the change actually + solves the stated problem. If the PR is already merged or closed, say so + and ask whether the user still wants a review. +2. Delegate the review itself to the `squad-reviewer` subagent + (subagent_type `squad-reviewer`, model `opus`). Give it the + base ref, the head SHA, and the round number — round 1 for a first review, + and for a re-review the previous round's findings plus the commits that + were meant to fix them. The review checklist, the integration-surface + sweep, the severity model and the round semantics all live in that agent's + definition (`.claude/agents/squad-reviewer.md`), so they stay + identical whether the review runs before or after the push; an agent + without subagent support follows that same file inline. +3. Post the result: + - Inline comments for findings anchored to a line, otherwise one review + comment. + - **Only genuine findings.** No positive remarks, no confirmation that + checklist items pass, no "looks good" filler, no formatting + the formatter already fixes. + - Lead the review body with the verdict line the subagent produced + (`APPROVE`, or the blocking/non-blocking counts), so the author can see + whether anything is required of them without reading every thread. + - Mark each finding `blocking` or `non-blocking` explicitly. +4. If the review produces no findings, post nothing beyond a short approving + verdict — and if the previous round already said the same, post nothing at + all. + +## Every posted finding gets worked + +A finding that has been posted as a review comment is work, not a note. This +holds for **every** posted finding — blocking and non-blocking alike, whether +it came from this skill, from a human reviewer, or from an automated code +review on the pull request. + +- Resolve it in the pull request it was posted on, while the session that can + act on it is still running. +- Do not defer a posted finding to "the next change that touches this code" + or "the next substantive commit". No such change is scheduled, and the + session holding the context needed to act on the comment will not exist + later — the deferral is a way of dropping the finding, not of carrying it + forward. +- If a posted finding genuinely should not be acted on in this PR, it gets + one of two concrete outcomes, never an implied one: a reply explaining why + the code stays as it is, or a GitHub issue opened **now** and linked from + the reply. Either way the thread is answered and resolved before the PR is + considered done. +- Non-blocking is about whether a finding gates the merge, not about whether + anyone will ever deal with it. + +## Keeping the loop finite + +A pull request review can always produce one more finding. These rules make +it converge, without leaving posted findings unhandled: + +- **Round 1 reviews the whole diff. Every later round reviews only the + delta**: does each fix resolve its finding, and did the fix commits break + something — including in prose they wrote to fix a documentation finding? + Never re-review untouched code; that is what turns three findings into four + rounds. +- **Only blocking findings justify another review round.** A non-blocking + finding is still worked per the section above, but working it does not earn + a new round of review. +- **Two consecutive rounds without a blocking finding means done.** Say so + plainly instead of leaving the review open-ended. +- **At most two rounds on GitHub.** If blocking findings survive that, the + change needs a decision from the author, not another review pass — say what + is still blocking and stop. +- The number of rounds is capped; the number of posted findings that get + handled is not. Every open thread is answered before the PR is done, even + when no further round runs. + +## Answering findings on your own PR + +When acting as the author of a PR under review: + +- Fix the finding, push, then keep the reply to one line: + `Fixed in <sha>: <what changed>`. The reasoning belongs in the commit + message, where it stays with the code; the reviewer verifies the commit, + not the reply. +- Re-run *Format*, *Build*, the *Analyzer gate* (no diagnostic in a changed + file), *Test with coverage* and the *Coverage gate* from `.squad/stack.md` + before each push — a fix that turns CI red costs more + than the finding did. +- Resolve the thread once it is answered. One summary comment per round beats + one essay per thread. +- Work through every open thread before calling the PR done, including the + non-blocking ones, as described above. + +## Notes + +- This skill reviews; it does not silently rewrite the PR. Fixing findings on + your own PR is the author's step above, and it is explicit — never edit + someone else's branch without being asked. - Prefer non-interactive commands only. -- Do not post any comment or review to GitHub unless the user explicitly requests it. -- Base your verdict on evidence from the diff and code; if something is uncertain, say so instead of guessing. -- Stay inside the scope defined above: the diff, the description-vs-diff match, and the SonarQube Cloud check. Do not comment on other checks, labels, or metadata. +- Base the verdict on evidence from the diff and the code; if something is + uncertain, say so instead of guessing. \ No newline at end of file diff --git a/.claude/skills/squad-issue/SKILL.md b/.claude/skills/squad-issue/SKILL.md new file mode 100644 index 0000000..adf45dc --- /dev/null +++ b/.claude/skills/squad-issue/SKILL.md @@ -0,0 +1,176 @@ +--- +name: squad-issue +description: Use when the user asks to fix a specific GitHub issue in this repository. Runs the squad pipeline — Lead plans and picks a tier, the Devil's Advocate challenges the plan, Security reviews security-relevant plans, Tester writes failing tests first, Dev implements to 80% coverage, Code Officer clears format and analyzer findings, Reviewer (+ Security) review, Lead approves — and opens a PR referencing the issue. +--- + +# Squad Issue + +Fix a reported GitHub issue with the squad defined in `.squad/`. You are the **orchestrator**: you launch +the members as subagents, pass their outputs on (they cannot talk to each other), enforce the tiers and +loop limits from [`.squad/routing.md`](../../../.squad/routing.md), and perform every Git and GitHub +action yourself — including follow-up issues the Lead decides on. + +- **You never do a member's work.** You do not edit production code, tests or documentation the plan + assigns to the Dev, do not run the formatter, and do not fix analyzer findings — not even a one-line `sed`. + Whatever a check of yours finds goes to its owner (production code → `squad-dev`, tests → + `squad-tester`, formatting/analyzer-only edits → `squad-code-officer`) and through the steps that follow + it. Besides read-only checks (`--check`, the analyzer and coverage scripts, tests) you only write the + squad's bookkeeping: `log.md` and `tasks.md` check marks (features). Never production code, tests or `docs/`. +- **The squad does not change itself in a product PR.** An issue or feature PR never touches `.squad/` + (including `history.md` and `decisions.md`), `.claude/`, `.github/skills/`, `.agents/skills/`, `CLAUDE.md`, `AGENTS.md` + or `.github/copilot-instructions.md`. Lessons about the squad are filed in step 12 as `.squad/routing.md`, + *Squad lessons*, says — template-managed files in the template repository, project knowledge here. If the + change itself genuinely needs one of those files (e.g. a new build command every contributor must know), + the Lead escalates instead and the Product Manager decides: `.squad/stack.md` and `.squad/project.md` may + change in the product PR (*Scope of a product PR*); a template-managed file is changed in the template + repository; any other squad or instruction file (e.g. a project block) goes into a separate + squad-maintenance PR. +- **Working records stay off `main`.** `specs/<folder>/` exists only on the work branch, so it survives a + crashed session. Before the PR (step 10) its content is posted as a comment and the folder is removed; + the lasting reasoning lives in `docs/decisions/`. +- **One build at a time.** Never run two members that build or test (`squad-dev`, `squad-tester`, + `squad-code-officer`, the reviewers' verification runs) in parallel: they share `bin/` and `obj/` and + break each other (`.squad/routing.md`). Launch them one after another; only `squad-reviewer` and + `squad-security` may run together in step 8, because both are read-only and the reviewer's own build + happens in a scratch copy. +- **Commits and pushes** to the work branch are always allowed (`CLAUDE.md`, golden rules): commit and + push `specs/<folder>/log.md` right after intake (step 1), so a stop hook or a crashed session finds no + untracked files, and after every further completed step. PRs are merged with *Squash and merge*, so only + the PR title and description reach `main`; intermediate commit messages may name the step, but never + contain secrets. Interim work-in-progress commits — e.g. demanded by a stop hook while a member is still + working — are fine for the same reason. Stage with plain `git add -A`: ignored paths such as `TestResults/` + are skipped anyway, and an exclusion pathspec for an ignored path makes `git add` fail and stage + nothing. Never commit to `main`. +- **GitHub access:** use the GitHub MCP tools (`mcp__github__*`) for issues, comments, labels and pull + requests. In these sessions the `gh` CLI only works as `gh api repos/<owner>/<repo>/...`; `gh issue`, + `gh pr` and `gh search` fail (GraphQL is blocked and search is not scoped to the repository). +- **Pull request:** invoking this skill is the user's approval for opening the PR in step 10, once the + Lead has approved it (tier `docs`: once the latest review round is clean). +- **Product Manager:** the user is only contacted when the Lead returns `RESULT: ESCALATE` (relay the + question verbatim with its options and wait) or for confirming a public issue comment on + `RESULT: NO CHANGE`. +- Everything that ends up in the repository or on GitHub is written in **English**. + +## Steps + +1. **Intake.** Read the issue in full, including comments; note the reported environment (versions of the + software involved, image tag or release, host OS, configuration). If it is closed, stop and report that. Start + from a clean working tree on a new branch off the latest `main`, e.g. + `fix-issue-<number>-<short-slug>` (or the branch the session prescribes). Create + `specs/issue-<number>/log.md` from `specs/_template/log.md`, commit and push it; append one table row + per step. Only you + (the orchestrator) write `log.md`, one row per append, each row ending in the file's line ending — subagents report and + you record, so rows never merge or end up with mixed line endings. +2. **Plan.** Launch `squad-lead` in mode `plan` with the issue text and the work folder. It returns one of: + - `RESULT: DONE` — for tier **`docs`** (see its definition in `.squad/routing.md`), a short result (tier, the files and lines to change, acceptance + criteria) that you record as the first plan row in `log.md`, then continue with step 6 (Dev), the + read-only check from the `docs` row in `.squad/routing.md`, one review round in step 8, and step 10 + directly — no Security, skeleton, tests, coverage, Code Officer or Lead approval. Otherwise: + `plan.md` with the **tier** (`trivial` / `standard` / `security`), acceptance + criteria, the signatures of new or changed API, required documentation updates (`README.md`, + `docs/`), and `Proposed` decision records. Continue with the steps the tier requires. + - `RESULT: NO CHANGE` — show the proposed issue comment to the user, post it only after confirmation + (append the log as a collapsed "Squad working record" block), remove the work folder with a commit + and push, and stop. No PR; the branch stays as it is, and you tell the user so. + - `RESULT: ESCALATE` — ask the user, then relaunch the Lead with the answer. + + **Plan challenge** (`standard` and `security` only, once). Launch `squad-devils-advocate` with the issue + text and the work folder. On `VERDICT: OBJECTIONS …`, launch `squad-lead` in mode `revise` with the + objections; it answers each one in the plan's *Challenge* section (accepted and the plan revised, or + rejected with a reason) and may narrow the scope, raise the tier or switch to `RESULT: NO CHANGE`. + `RESULT: NO CHANGE` and `RESULT: ESCALATE` are handled as above; after a raised tier, continue with that + tier's steps. There is no second challenge round and no veto. Record the verdict (also a clean + `NO OBJECTIONS`) and the Lead's answer in `log.md`. +3. **Plan security review** (`security` tier only). Launch `squad-security` in mode `plan`. On + `CHANGES_REQUIRED`, launch `squad-lead` in mode `revise` and repeat. After the **2nd** rejection launch + `squad-lead` in mode `decide` (scope down, split into issues, abort, or escalate). +4. **Skeleton** (only if the plan adds or changes API). Launch `squad-dev` in mode `skeleton`: the planned + signatures built as *Skeleton* in `.squad/stack.md` describes (bodies fail when called), plus the existing + test call sites the plan assigns to the Dev for an incompatible signature change, so the tests of step 5 + compile. +5. **Tests first** (skipped for `trivial`). Launch `squad-tester` in mode `tests-first`. Confirm yourself + that the new tests compile and fail on the current code (unless the Tester justified why one cannot). + A fix without a reproducing test is only acceptable when the bug genuinely needs a live external + system — then the PR says so. +6. **Implement and cover.** Launch `squad-dev` in mode `implement` with the plan and the test names; it + also makes the documentation updates the plan lists. If the Dev disputes a test, launch `squad-lead` + in mode `decide`; the Tester changes a test only if the Lead says so. Then launch `squad-tester` in + mode `coverage`; repeat Dev/Tester until the *Coverage gate* (after *Test with coverage*, both in + `.squad/stack.md`) passes (≥ 80 % on new/changed production code and overall). Lines reported as not unit-testable go to + `squad-lead` in mode `decide`; an accepted gap is recorded in `log.md`. +7. **Code check.** Launch `squad-code-officer` with the base ref — the only member that runs + the formatter and clears analyzer diagnostics. Then verify yourself, without formatting, with the + commands from `.squad/stack.md`: *Format check* exits 0, the *Analyzer gate* passes (no diagnostic of + any severity in a changed file), *Test* is green with the same tests, and the *Coverage gate* still + passes. Structural items handed back go to `squad-dev` (or + `squad-tester`), followed by another code check. This is the gate before the PR; CI is not meant to find anything here. +8. **Review.** Launch `squad-reviewer` (round 1, full) and — for `standard` and `security` — + `squad-security` in mode `diff`, in parallel, against the base ref. Pass both the work folder + (`specs/<folder>/`) so they check the plan's acceptance criteria and tier (tier `docs`: the first + `log.md` row, since there is no `plan.md`); either may raise the tier. Tier `docs`: a blocking finding + goes to `squad-dev`, then the read-only check and a delta round, then step 10. Blocking + findings → their owner fixes them (`squad-dev` for production code, `squad-tester` for tests) → steps 6 + (coverage) and 7 again → **a new review round on the delta is mandatory** before step 9; never go from + a blocking finding straight to PR approval. The same holds for a non-blocking finding the Lead decides + to fix now: any change to production code, tests or `docs/` after a review round needs a delta round. At most **2 fix rounds** after round 1; then `squad-lead` + in mode `decide`. Non-blocking + findings: the Lead decides per finding — fix now, or you open a linked GitHub issue now. +9. **PR approval.** Launch `squad-lead` in mode `approve-pr` with the base ref, the build/test/coverage + output and the review outcome — including the result of the **latest** review round, which must have + no blocking finding that is not covered by a recorded Lead decision, and must cover every change to + production code, tests and `docs/` since it ran (only `specs/` bookkeeping and the Lead's own approval edits — + record status, the index, a link from `docs/ARCHITECTURE.md` — may follow it; a fix for a blocking + finding always needs a delta round, also in a decision record). `NOT APPROVED` → back to step 6 or 8 (counting against the review loop + limit) or let the Lead decide/escalate. On `APPROVED`, the decision records are `Accepted` and indexed + in `docs/decisions/README.md`. +10. **Pull request** (Dev role, performed by you). First move the working record off the branch: post + `plan.md` (none for tier `docs`) and `log.md` as one comment on the issue (each inside a collapsed `<details>` block, headed + "Squad working record"), then `git rm -r specs/issue-<number>/`, commit ("Remove squad working + record"), and push. Later log rows (steps 11–12) are appended by editing that comment. Then open the + PR from + [`.github/pull_request_template.md`](../../../.github/pull_request_template.md): title per + `docs/CONTRIBUTING.md` — `[area] Description`, where `area` is one of the areas + CONTRIBUTING lists, capitalized — not a lowercase class or file name. It becomes the squash + commit subject on `main`. Body describing the bug, the fix and the + reproducing test, `Closes #<number>` under Issues, links to the decision records. Next Steps lists + **only linked GitHub issues** (create them now) or "None" — never an unlinked "revisit later". Follow the `create-pr` skill's template rules, but + do **not** run its internal review loop — step 8 replaced it. If the fix is not fully verifiable + without a real external system, say so. +11. **After the PR.** Subscribe to the PR's activity right after opening it (`subscribe_pr_activity` when + available; otherwise check the CI and code-analysis (e.g. SonarQube Cloud) results yourself before finishing) — a session + that ends with an unwatched PR has not completed this step. Stay with the PR until CI is green and the + code-analysis quality gate (e.g. SonarQube Cloud) passes: + - code-analysis findings → `squad-code-officer` (structural ones → `squad-dev`); + - failing build or tests → `squad-dev` (test defects → `squad-tester`); + - review comments (human, automated, `review-pr`) → `squad-dev`, worked in this PR, blocking or not. + + Each fix goes through steps 7–8 again (delta review), with at most 2 fix rounds per failure before the + Lead decides. The work folder is gone by now: give the Reviewer, Security and the Lead the plan (tier + `docs`: the first log row; features: + also `spec.md` and `tasks.md`) from the "Squad working record" comment, or via + `git show <commit-before-removal>:specs/<folder>/<file>`, and record each log row by editing that + comment. Never skip, disable or weaken a test to get green. +12. **Wrap-up (mandatory).** Collect what this run taught about the squad itself (a rule that was + unclear or contradictory, a tool that misbehaved, an agent that could not be launched, a step that + had to be improvised), each with the role it concerns and a concrete proposal, and file them as + `.squad/routing.md`, *Squad lessons*, says: lessons about template-managed files as **one** issue + labelled `squad` in the template repository named in `.squad/template.json` (attach that repository to the session if + needed; without access, file it here with the label `squad-upstream`), lessons about project knowledge + as **one** issue labelled `squad` in this repository (create the labels if missing). Link the issues + from the working record comment. Do **not** edit `.squad/`, `.claude/` or the instruction files. + Report the branch, the PR URL, the tier, the `squad` issues (or "no lessons") and any escalation or + Lead decision to the user. If there is genuinely nothing to learn, + append a `| <date> | 12 Wrap-up | Orchestrator | no lessons |` row to the working record comment + instead of opening an issue — the step itself is never skipped. + +## What the pull request says — and what it doesn't + +The PR title and description document the change, not how it was produced: the bug, the fix and the test +that pins it down. Plan revisions, review rounds and their findings never appear there. +(The working record is the "Squad working record" comment on the issue; the lasting reasoning is in +`docs/decisions/`.) + +## Notes + +- Prefer non-interactive commands only. If push or PR creation fails, stop and report it. +- Never close the issue manually; `Closes #<number>` closes it on merge. diff --git a/.claude/skills/squad-spec/SKILL.md b/.claude/skills/squad-spec/SKILL.md new file mode 100644 index 0000000..847ce65 --- /dev/null +++ b/.claude/skills/squad-spec/SKILL.md @@ -0,0 +1,31 @@ +--- +name: squad-spec +description: Use when the user wants to develop a new feature in this repository spec-driven with the squad. Lead writes spec, plan and tasks and picks a tier, the Devil's Advocate challenges them, Security reviews security-relevant plans, Tester writes failing tests first, Dev implements to 80% coverage, Code Officer clears format and analyzer findings, Reviewer (+ Security) review, Lead approves, then a PR is opened. +--- + +# Squad Spec + +Build a feature with the squad defined in `.squad/`. Tiers, pipeline, loop limits, escalation rules, +commit/push rules and the orchestrator role are identical to the `squad-issue` skill — follow its steps +1–12 with these changes: + +- **Step 1 — work folder and branch:** `specs/feature-<short-slug>/` with `log.md` from + `specs/_template/log.md`; branch `feature-<short-slug>` off the latest `main` (or the branch the session + prescribes). +- **Step 2 — plan:** `squad-lead` in mode `plan` writes `spec.md` (behavior, acceptance criteria, out of + scope), `plan.md` and `tasks.md`. A feature is never `docs` and rarely `trivial`. It is more likely than a bug + fix to need a product decision — the Lead escalates whenever the request does not settle user-visible + behavior. `RESULT: NO CHANGE` means the feature already exists or contradicts an accepted decision; report + that to the user instead of commenting on an issue. The plan challenge covers `spec.md`, `plan.md` and + `tasks.md` together. +- **Decision records:** features usually involve real design choices, so expect at least one record in + `docs/decisions/`; the Lead also updates `docs/ARCHITECTURE.md` when the feature changes a flow or + guarantee. +- **Step 3** reviews `spec.md` and `plan.md` together. +- **Steps 4–6** run per task or group of tasks from `tasks.md`; tick tasks off as they are done. Run + Tester and Dev one after another, never in parallel (see *Concurrency* in `.squad/routing.md`). +- **Step 10 — pull request:** the working record (`spec.md`, `plan.md`, `tasks.md`, `log.md`) is posted + to the feature request issue if one exists (`Closes #<n>` in the PR), otherwise as the first comment on + the PR right after opening it; the folder is removed before the PR as in `squad-issue`. Behavior that + must stay documented belongs in `README.md`, `docs/ARCHITECTURE.md` or a decision record, not in + `spec.md`. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5d3171a..fd35961 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,32 +1,42 @@ --- name: Bug report -about: Create a report to help us improve -title: "[BUG]" +about: Report a problem +title: "[BUG] " labels: bug assignees: '' --- **Describe the bug** -A clear and concise description of what the bug is. +A clear and concise description of what went wrong. **To Reproduce** Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error +1. ... +2. ... **Expected behavior** A clear and concise description of what you expected to happen. +<!-- project:begin environment --> **Screenshots** If applicable, add screenshots to help explain your problem. -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] +**Environment** + - Version / image tag: [e.g. `1.4.0` or `latest`] + - Deployment tier: [minimal / trivy / full, see `INSTALL.md`] + - Docker / Portainer / Trivy versions involved: + - Browser: [e.g. Chrome, Firefox] +<!-- project:end environment --> + +**Logs** +<!-- project:begin logs --> +Paste the relevant log excerpt (redact tokens, passwords and other secrets). +<!-- project:end logs --> + +``` +paste logs here +``` **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index ad53fad..2d8e250 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,14 +1,14 @@ --- name: Feature request about: Suggest an idea for this project -title: "[Feature]" +title: "[Feature] " labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. +A clear and concise description of what the problem is, e.g. "I'm frustrated when [...]" **Describe the solution you'd like** A clear and concise description of what you want to happen. @@ -17,4 +17,4 @@ A clear and concise description of what you want to happen. A clear and concise description of any alternative solutions or features you've considered. **Additional context** -Add any other context or screenshots about the feature request here. +Add any other context, configuration snippets, or dashboard screenshots about the feature request here. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b78f072..23c6850 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,86 +1,224 @@ -# DockerUpdateGuard Project Instructions - -This file describes project-specific conventions and configuration for DockerUpdateGuard. -Copilot and other AI assistants must follow these guidelines when working in this repository. +# Copilot instructions + +Project guidance for GitHub Copilot when working in this repository. These rules mirror `CLAUDE.md` and `AGENTS.md`; keep all three in sync — +everything from the first `##` heading on is identical in all three files. This file is a summary; the +binding, detailed references are [`ARCHITECTURE.md`](/docs/ARCHITECTURE.md) (how the system is put +together and why), [`CONTRIBUTING.md`](/docs/CONTRIBUTING.md) (workflow, PR conventions, versioning), +[`UNIT_TESTS.md`](/docs/UNIT_TESTS.md) (test conventions — **unit tests are mandatory for new code**) and +[`.squad/stack.md`](/.squad/stack.md) (toolchain and commands). Read them before making a non-trivial +change; when this file and one of them appear to disagree, treat that as a sync bug to fix, not as +license to pick either one. + +## What this project is + +<!-- project:begin overview --> +DockerUpdateGuard is an ASP.NET Core Razor Components (Blazor Server) web application that tracks what is +actually running in Docker, compares it with registry metadata, and shows where updates, vulnerabilities and +shared base-image dependencies need attention. It runs as a Docker container against PostgreSQL. + +[`ARCHITECTURE.md`](/docs/ARCHITECTURE.md) is the binding architecture reference: solution layout, composition +root and startup sequence, configuration model, data layer, integration clients, the background scan engine, +the UI layer, telemetry, security posture, and the CI/deployment pipeline. Read it before making structural +changes (new projects, new background jobs, new integration clients, changes to the composition root or entity +model) and update it in the same change whenever it goes out of date — it must never contain open questions +or stale claims. +<!-- project:end overview --> + +## Golden rules + +- **Never commit or push to `main`** — no one, not even with approval. Every change goes through a + separate branch and a pull request. +- **Commits and pushes to a feature branch are always allowed** without asking: commit finished work and + push it to the current feature branch (creating that branch off `main` if needed), so nothing is lost + when a session ends. Force-pushing or otherwise rewriting published history, deleting branches, and + creating tags (a `v*` tag may trigger a release) still need explicit user approval. +- **Pull requests are only opened by the squad or by the user.** The `squad-issue` and `squad-spec` + skills open a PR after the Lead's approval (tier `docs`: after a clean review); outside the squad, a PR + is opened only when the user explicitly asks for one (e.g. by running the `create-pr` skill). Never open + a PR on your own initiative. +- Run *Format* from [`.squad/stack.md`](/.squad/stack.md) after editing code and before building; CI is + not meant to find formatting issues. In the squad skills only the Code Officer runs it. +- A changed file may not carry **any analyzer diagnostic of any severity**, including info-level ones that + never show up as build warnings but that the CI code analysis (e.g. SonarQube Cloud) reports. Check with + the *Analyzer gate* from `.squad/stack.md` and fix every finding before considering the work done (in + the squad skills, the Code Officer owns this). +- New or changed production code needs **at least 80 % line coverage**, and overall coverage must stay + at least 80 % (*Coverage gate* in `.squad/stack.md`, see [`UNIT_TESTS.md`](/docs/UNIT_TESTS.md#code-coverage)). +<!-- stack:begin golden-rules --> +- Add new packages via **Central Package Management** (`Directory.Packages.props`); do not put version + numbers in individual `.csproj` files. +- Every C# project uses the **Reihitsu.Analyzer** and the **SonarAnalyzer.CSharp** rules, so SonarQube + issues surface in the local build, not first in the CI analysis. A build must finish with **zero + Reihitsu (`RH####`) warnings and errors**. +- Wrap every type's members in `#region` blocks **as you write the code** — never leave a type + un-regioned and never add the regions only after an analyzer warning. +<!-- stack:end golden-rules --> ## Commit messages -- The first line should be a one-line summary of no more than 80 characters -- Do not end the subject line with a period -- Do not write the text in the first person -- Keep the main body to a maximum of 3–5 sentences, depending on the number of changes - -## Git workflow - -- Never run `git commit` or `git push` without explicit user approval. -- Read-only Git commands are fine. -- Keep commit subjects to a single line under 80 characters and do not end them with a period. +- Keep the subject line to a single summary of **no more than 80 characters** and do not end it with a + period. +- Do not write the message in the first person. +- Keep the body to **3–5 sentences**, depending on the number of changes. ## Pull requests -- Write the PR title and description in English, regardless of the language used in the conversation. -- Do not mention Claude, Anthropic, Copilot, or any AI assistant in the PR title or description. -- Do not include AI session links, "Co-Authored-By" trailers for AI assistants, "Generated with ..." notices, or any other reference indicating the PR was created with AI assistance. -- Follow the PR template in `.github/PULL_REQUEST_TEMPLATE.md`. Use its sections and do not add extra sections beyond it. -- Do not add a "Validation", "Verification", "Testing", or similar section that lists `reihitsu-format`, `dotnet build`, `dotnet test`, or other build/test commands. Build and tests run automatically as PR checks, so restating them in the description is unnecessary. - -## Build, test, and lint +- Title and description are always written in **English**, regardless of the language used in the + conversation. -Use the solution file at the repository root: +## Commands -- Restore: `dotnet restore DockerUpdateGuard.slnx` -- Format source: `reihitsu-format ./` -- Build: `dotnet build DockerUpdateGuard.slnx -c Release --no-restore` -- Run all tests: `dotnet test src\Tests\**\*.csproj -c Release --no-build --logger trx --collect:"XPlat Code Coverage"` -- Run one test project: `dotnet test src\Tests\DockerUpdateGuard.Tests\DockerUpdateGuard.Tests.csproj -c Release --no-build` -- Run one test method: `dotnet test src\Tests\DockerUpdateGuard.Tests\DockerUpdateGuard.Tests.csproj --filter "FullyQualifiedName~Namespace.ClassName.MethodName"` +<!-- stack:begin commands --> +Run from the repository root, where the solution file lives (exact commands, with the solution name, in +`.squad/stack.md`): -Run `reihitsu-format ./` after source changes and before running a build. The command is available as a .NET tool and can be installed with `dotnet tool install -g Reihitsu.Cli` if it is missing. +```bash +dotnet restore +reihitsu-format ./ # dotnet tool install -g Reihitsu.Cli +dotnet build -c Release --no-restore +dotnet test -c Release --no-build +python3 .squad/tools/analyzer-check.py # analyzer gate +python3 .squad/tools/coverage-check.py # coverage gate, after a coverage run +``` +<!-- stack:end commands --> -There is no dedicated lint command at the moment beyond formatting. Static analysis runs during build through the configured rulesets and analyzers. +All commands, with what each one checks, are listed in [`.squad/stack.md`](/.squad/stack.md). ## Architecture -[ARCHITECTURE.md](../ARCHITECTURE.md) is the binding architecture reference for -this repository: solution layout, composition root and startup sequence, -configuration model, data layer, integration clients, the background scan -engine, the UI layer, telemetry, security posture, and the CI/deployment -pipeline. Read it before making structural changes (new projects, new -background jobs, new integration clients, changes to the composition root -or entity model) and update it in the same change whenever it goes out of -date — it must never contain open questions or stale claims. - -The repository contains a complete implementation: Docker/DockerHub/Portainer clients, an EF Core data layer with migrations, a Blazor UI, and a telemetry layer. - -| Path | Role | -| --- | --- | -| `src\DockerUpdateGuard` | Main ASP.NET Core host (`Microsoft.NET.Sdk.Web`); composition root; references the data and telemetry projects | -| `src\DockerUpdateGuard.Data` | Data-access layer; EF Core with PostgreSQL via `Npgsql.EntityFrameworkCore.PostgreSQL` | -| `src\DockerUpdateGuard.Telemetry` | Shared observability layer; OpenTelemetry hosting, OTLP export, ASP.NET Core, HTTP, and runtime instrumentation | -| `src\Tests\DockerUpdateGuard.Tests` | Tests for the main host/application layer; references the web project and uses EF Core InMemory plus NSubstitute | -| `src\Tests\DockerUpdateGuard.Data.Tests` | Tests for the data layer; references the data project and uses EF Core SQLite | - -Web startup and dependency wiring stay in the main host project; persistence stays in `.Data`; observability stays in `.Telemetry`. For everything beyond this table, see [ARCHITECTURE.md](../ARCHITECTURE.md). - -## Key conventions - -- The repository uses the XML-based `.slnx` solution format. -- Runtime projects target `net10.0`, enable nullable reference types, implicit usings, and XML documentation files. -- Runtime projects disable generated assembly info and instead link `SharedAssemblyInfo.cs` from the repository root. -- Runtime and test projects also use per-configuration rulesets from `rules\DockerUpdateGuard.Debug.ruleset` and `rules\DockerUpdateGuard.Release.ruleset`. -- `Reihitsu.Analyzer` are part of the standard project setup. -- Tests are under `src\Tests`, not a top-level `tests` folder. Keep new test projects there. -- The current test stack is MSTest with `coverlet.collector`. -- Detailed C# formatting and style rules live in `.github\instructions\csharp.instructions.md`. Follow that file for naming, region layout, XML docs, and null-handling preferences. -- Prefer MSTest's `Assert` and `CollectionAssert` APIs directly instead of FluentAssertions. -- Name test classes `{Feature}Tests` and test methods `{Class}{Scenario}{ExpectedResult}`. -- Always include assertion messages in tests. - -## EF Core migrations - -The project already has EF Core migrations; follow the same pattern for new ones: - -- first migration: `InitialCreate` -- later migrations: `Update1`, `Update2`, `Update3`, ... -- rename migration files to remove the timestamp prefix -- keep the generated `[Migration("yyyyMMddHHmmss_Name")]` attribute unchanged +<!-- project:begin architecture --> +- `src/DockerUpdateGuard` — main ASP.NET Core host (`Microsoft.NET.Sdk.Web`); composition root; references the + data and telemetry projects. Web startup and dependency wiring stay here. +- `src/DockerUpdateGuard.Data` — data-access layer; EF Core with PostgreSQL (`Npgsql.EntityFrameworkCore.PostgreSQL`). +- `src/DockerUpdateGuard.Telemetry` — shared observability layer; OpenTelemetry hosting, OTLP export, ASP.NET + Core / HTTP / runtime instrumentation. +- `src/Tests/DockerUpdateGuard.Tests` — tests for the host/application layer; EF Core InMemory + NSubstitute. +- `src/Tests/DockerUpdateGuard.Data.Tests` — tests for the data layer; EF Core SQLite. +<!-- project:end architecture --> + +## Project configuration + +<!-- stack:begin configuration --> +- **Target framework** as set in the project files (see `.squad/stack.md`); **nullable reference types**, **implicit usings**, and + **documentation XML** generation are all enabled. +- **Central Package Management** via `Directory.Packages.props`; never put versions in individual + `.csproj` files. +- **Reihitsu.Analyzer** and **SonarAnalyzer.CSharp** are dev dependencies in every project (via + `Directory.Build.props`). +- **Solution format** is `.slnx` (XML-based) at the repository root. +<!-- stack:end configuration --> +<!-- project:begin configuration --> +- **Target framework** `net10.0` in the runtime projects; one solution, `DockerUpdateGuard.slnx`, at the + repository root. +- Runtime projects disable generated assembly info and link `SharedAssemblyInfo.cs` from the repository root. +- Runtime and test projects use per-configuration rulesets from `rules/DockerUpdateGuard.Debug.ruleset` and + `rules/DockerUpdateGuard.Release.ruleset`; rules SonarAnalyzer.CSharp disables by default (e.g. `S3776`) are + enabled there. +- SonarCloud rule suppressions are centralized in the shared `src/GlobalSuppressions.cs` (linked into each + project), never scattered per file. +- Tests live under `src/Tests`, not a top-level `tests` folder; keep new test projects there. +- EF Core migrations follow the SeriesOverwatch pattern: the first migration is `InitialCreate`, later ones + `Update1`, `Update2`, …; migration files are renamed to drop the timestamp prefix, while the generated + `[Migration("yyyyMMddHHmmss_Name")]` attribute stays unchanged. +<!-- project:end configuration --> + +## Code style + +<!-- stack:begin code-style --> +File-scoped namespaces; one top-level type per file; `using` outside namespace (System first); Allman +braces, always required; 4-space indent; `var` preferred; language keywords over BCL types; LINQ method +syntax only; `== false` instead of `!`; `is null` / `is not null`; no primary constructors; constructor +injection with `_camelCase` readonly fields; `#region` blocks grouped by member kind (an interface's +region named after the interface, its description not ending in "implementation"); XML docs on all +members (English, no `<remarks>`); `.ConfigureAwait(false)` in library/service code. +<!-- stack:end code-style --> +<!-- project:begin code-style --> +The detailed C# code-style rules (naming, regions, formatting, XML docs, null handling, suppressed analyzer +rules) in [`.github/instructions/csharp.instructions.md`](/.github/instructions/csharp.instructions.md) are +binding and take precedence over the summary above: + +@.github/instructions/csharp.instructions.md + +- CRLF line endings and no final newline (`.editorconfig`). +- Tests: MSTest's `Assert` / `CollectionAssert` (no FluentAssertions); **NSubstitute** is the mocking library + here, used only where a collaborator crosses an infrastructure boundary; EF Core InMemory / SQLite for data + tests (`docs/UNIT_TESTS.md`). Test classes `{TypeUnderTest}Tests`, methods + `{TypeUnderTest}{Scenario}{ExpectedResult}`, always with assertion messages. +<!-- project:end code-style --> + +## Testing + +<!-- stack:begin testing --> +**Unit tests are mandatory for newly written code.** MSTest only (no FluentAssertions, no mocking +library — use real objects or the hand-written fakes/stubs). Classes `{TypeUnderTest}Tests`, methods +`{Class}{Scenario}{ExpectedResult}` in PascalCase **without underscores**; always pass an assert message. +<!-- stack:end testing --> +Full conventions, including the project's test doubles and the checklist to run before committing a new +test, are in [`UNIT_TESTS.md`](/docs/UNIT_TESTS.md). + +## Related skills + +Project-specific workflow skills live under `.claude/skills/`, mirrored identically under +`.agents/skills/` (Codex/GPT) and `.github/skills/` (GitHub Copilot): + +- `create-pr` — verify (format, build, tests, analyzer and coverage gates), review the change locally, + then open a PR following [`.github/pull_request_template.md`](/.github/pull_request_template.md). +- `squad-issue` — fix a GitHub issue with the squad: the Lead plans and picks a tier + (`docs` / `trivial` / `standard` / `security`), the Devil's Advocate challenges `standard`/`security` + plans once, Security reviews security-relevant plans, the Tester writes failing tests first, the Dev + implements to ≥ 80 % coverage, the Code Officer clears format and analyzer diagnostics, Reviewer and + Security review the diff, the Lead approves, then a PR referencing the issue is opened. +- `squad-spec` — the same squad pipeline for a new feature, planned as `spec.md`, `plan.md` and + `tasks.md` in a working folder under `specs/`. +- `review-pr` — review an open pull request against this project's stack, analyzer, security and + unit-test conventions, and post the findings with an explicit verdict. + +Review runs as a subagent defined in `.claude/agents/squad-reviewer.md` (read-only, pinned to Opus, fresh +context). `create-pr` and the squad skills call it *before* pushing, so a change is reviewed while it is +still local; `review-pr` calls the same agent for a pull request that is already open. The review +checklist, the integration-surface sweep, the blocking/non-blocking severity model and the "round 1 is a +full review, later rounds review only the delta" rule live in that one file, so they are identical either +way. An agent without subagent support follows the same file inline. + +The squad skills run a multi-role pipeline defined in [`.squad/`](/.squad/team.md) — Lead (plan, decisions, +PR approval), Devil's Advocate (one plan challenge), Security (plan and diff), Tester (tests first, +coverage), Dev, Code Officer (format, analyzers) and Reviewer — as subagents under +`.claude/agents/squad-*.md`, with the loop limits and escalation rules in +[`.squad/routing.md`](/.squad/routing.md). Stack commands live in [`.squad/stack.md`](/.squad/stack.md), +the project's guarantees, security areas and integration surface in +[`.squad/project.md`](/.squad/project.md). Their working records (`plan.md`, `log.md`, for features also +`spec.md` and `tasks.md`) live under `specs/` on the work branch only; before the PR they are posted as a +comment on the issue and removed, so `main` keeps no working records. An issue or feature PR never changes +the squad or these instructions (`.squad/` except `stack.md` and `project.md`, `.claude/`, +`.github/skills/`, `.agents/skills/`, `CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`): squad +lessons are filed as GitHub issues labelled `squad` and never fixed in a product PR. The squad and these +rules come from the template repository named in `.squad/template.json`: a lesson about a template-managed +file becomes an issue there and is rolled out with its `adopt-template` skill; a lesson about project +knowledge (`.squad/stack.md`, `.squad/project.md`, a project block) becomes an issue here and is worked in +a squad-maintenance PR checked with `python3 .squad/tools/config-check.py` (`.squad/routing.md`, +*Squad lessons*). The user acts as Product Manager +and is only asked when the Lead escalates. Pull requests are merged with *Squash and merge*, so only the +PR title and description reach `main`. + +The reasoning behind code decisions — why something was built the way it was — is recorded by the Lead +as one decision record per decision in [`docs/decisions/`](/docs/decisions/README.md) (append-only, +superseded rather than rewritten), not in `ARCHITECTURE.md`. Read the relevant records before changing +code they cover, and do not contradict an accepted record without superseding it. + +Two rules these skills enforce that are easy to get wrong: + +- **A pull request documents the change, not how it was produced.** The internal review loop — its + pass count, its findings, the commits that resolved them — never appears in the PR title, body or + commit messages. +- **A finding posted as a review comment gets worked in that pull request**, blocking or not. It is + never deferred to "the next change that touches this code": no such change is scheduled, and the + session holding the context to act on it will not exist later. If it really should not be fixed + here, reply with the reason or open a linked issue now — then resolve the thread. + +## Pull requests, contributing and architecture + +Follow [`CONTRIBUTING.md`](/docs/CONTRIBUTING.md) for branch/PR naming (`[area] Description`), the PR +checklist in [`.github/pull_request_template.md`](/.github/pull_request_template.md), and the +stability policy. Consult [`ARCHITECTURE.md`](/docs/ARCHITECTURE.md) before changing the behavior it +describes — the guarantees listed in [`.squad/project.md`](/.squad/project.md) are deliberate, not +incidental behavior. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/pull_request_template.md similarity index 65% rename from .github/PULL_REQUEST_TEMPLATE.md rename to .github/pull_request_template.md index 6440003..c39630d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/pull_request_template.md @@ -2,8 +2,6 @@ Thanks for filing a pull request! Before you submit, please read the following: Search open/closed issues before submitting. Someone may have pushed the same thing before! - -Provide a summary of your changes in the title field above. --> # Pull Request @@ -39,24 +37,28 @@ Please provide a summary of the tests affected by this work and any unique strat ## ✅ Checklist -### General - <!--- Review the list and put an x in the boxes that apply. --> -- [ ] I have added tests for my changes. +- [ ] I have added or updated [Unit Tests](../docs/UNIT_TESTS.md) for the change. - [ ] I have tested my changes. -- [ ] I have updated the project documentation to reflect my changes. -- [ ] I have read the [CONTRIBUTING](../docs/CONTRIBUTING.md) documentation and followed the project's [code style guidelines](../.github/instructions/csharp.instructions.md). +- [ ] I have run *Format* and *Build* from [`.squad/stack.md`](../.squad/stack.md), and the *Analyzer gate* reports no diagnostic in a changed file. +- [ ] New or changed production code has at least 80 % line coverage, and overall coverage is at least 80 % (*Coverage gate* in `.squad/stack.md`). +- [ ] I have updated the project documentation ([`README.md`](../README.md), [`ARCHITECTURE.md`](../docs/ARCHITECTURE.md)) to reflect my changes. +- [ ] I have read the [CONTRIBUTING](../docs/CONTRIBUTING.md) documentation and followed the project's code style guidelines. +- [ ] New dependencies, if any, were added the way *Dependencies* in `.squad/stack.md` prescribes. + +<!-- project:begin checklist --> +- [ ] I have followed the project's [code style guidelines](../.github/instructions/csharp.instructions.md). ### UI-specific -<!--- Review the list and put an x in the boxes that apply. --> <!--- Remove this section if not applicable. --> - [ ] I have added a new Blazor page/component. - [ ] I have added [Unit Tests](../docs/UNIT_TESTS.md) for the new page/component. - [ ] I have modified an existing Blazor page/component. - [ ] I have updated the [Unit Tests](../docs/UNIT_TESTS.md) for the modified page/component. +<!-- project:end checklist --> ## ⏭ Next Steps diff --git a/.github/skills/create-pr/SKILL.md b/.github/skills/create-pr/SKILL.md new file mode 100644 index 0000000..4e48dce --- /dev/null +++ b/.github/skills/create-pr/SKILL.md @@ -0,0 +1,151 @@ +--- +name: create-pr +description: Use when the user asks to open/create a pull request for changes on this branch. Runs local verification (format, build, test), reviews the change with the squad-reviewer subagent, then pushes the branch and opens a PR following this repo's pull request template. +--- + +# Create PR + +Use this skill to prepare and open a pull request for changes made in this +repository. + +All user-facing output you create — branch name, commit message, PR title and +body — is written in **English**, regardless of the language the user wrote +in. + +## Steps + +1. **Verify the working tree**: run `git status --short --branch` and + `git diff` to confirm what will be included, and confirm the `origin` + remote exists. Do not include unrelated or uncommitted work the user + didn't ask for. If there are no relevant local changes and no unpushed + commits, stop and say so plainly. +2. **Create a branch** if you are still on `main` (or another base branch) — + never commit directly to it. Derive a short kebab-case name from the work + (e.g. `add-season-aggregates`, `fix-path-mapping`), or use the name the + user supplied. If you are already on a feature branch, stay on it. +3. **Verify tests exist** for what the diff changes. Per + [`UNIT_TESTS.md`](../../../docs/UNIT_TESTS.md), unit tests are mandatory for + new/changed behavior, not optional — if the diff adds or changes logic + without a corresponding test, write one before proceeding (following + `UNIT_TESTS.md`'s naming, test-double and assert-message conventions) + rather than opening the PR without coverage. +4. **Run local verification** before pushing, from the repository root, + with the commands from [`.squad/stack.md`](../../../.squad/stack.md): + - *Restore* (if the stack has one) and *Format* + - *Build* — it must finish without errors and without the warnings + `stack.md` lists as forbidden + - *Analyzer gate* — no analyzer diagnostic of any severity in a changed + file; treat each as a failure + - *Test with coverage* and *Coverage gate* — at least 80 % line coverage + on new/changed production code and overall + - `python3 .squad/tools/config-check.py` when the diff touches `.claude/`, + `.github/skills/`, `.agents/skills/` or an instruction file — Claude Code + silently drops an agent or skill whose front matter does not parse, and + the skill copies and instruction files must match + Fix any failures before proceeding — do not open a PR with failing checks, + unformatted code or outstanding analyzer diagnostics. This step is the gate + before the PR; CI is not meant to find anything here. +5. **Commit** with a subject line of at most 80 characters, not written in + the first person and without a trailing period, and a body of 3–5 + sentences explaining *what* changed and *why* if it is not obvious from + the diff. Stage only the files that belong to this task. +6. **Run the internal review loop** (see below) and resolve what it finds. + This happens *before* the push, so the pull request opens on a reviewed + change instead of collecting review rounds afterwards. +7. **Push** the branch: `git push -u origin <branch-name>`. +8. **Open the PR** using the repository's template at + `.github/pull_request_template.md`: + - base branch `main`, unless the user explicitly requests a different base + - title `[area] Description` per + [`CONTRIBUTING.md`](../../../docs/CONTRIBUTING.md) — area is one of the + areas CONTRIBUTING lists, capitalized, no period at the end, no issue number + in the description, under 70 characters + - fill in Description, Issues (link the related issue if one exists, with + `Closes #<number>`), Reviewer Notes and Test Plan, and check off the + checklist items that are actually true (don't check items you haven't + verified) — including the unit-test, formatting, analyzer, coverage, + documentation and dependency items, not just the general ones + - wrap the body in a HEREDOC so the formatting survives +9. Report the branch name and the PR URL back to the user. + +## What the pull request says — and what it doesn't + +The pull request documents **the change**, not how the change was produced. + +- Reviewer Notes tell a reviewer where to look and why the approach was + chosen: the components touched, any guarantee from + [`ARCHITECTURE.md`](../../../docs/ARCHITECTURE.md) the change comes near, + and a smoke test if one is worth running. +- Do **not** mention the internal review loop anywhere in the PR — not how + many passes ran, not what they found, not which commits resolved their + findings. That loop is a working step inside this session, not part of the + change's history, and a reader of the PR has no use for it. +- Describe the finished state of the change, not the sequence of corrections + that got there. + +## The internal review loop + +The review happens here, in this session, against the local branch — not as +a round trip through pull request comments. Each pass is delegated to the +`squad-reviewer` subagent, which runs on Opus with a fresh +context and the repository's full review checklist. That checklist lives in +`.claude/agents/squad-reviewer.md`; an agent without subagent +support follows the same file inline, so the review is the same either way. + +1. **Pass 1** — launch `squad-reviewer` (subagent_type + `squad-reviewer`, model `opus`). Tell it the base ref, the + head to review, and that this is round 1. +2. **Act on the verdict**: + - `APPROVE` → done, go push. + - Blocking findings → fix each one minimally and commit. Do not widen the + change beyond what the finding requires. + - Non-blocking findings → **do not open another round for them**. Fix one + if it is trivial and already in scope. Otherwise open a GitHub issue for + it **now**, in this session, and link that issue under the PR's Next + Steps — a note that only exists in this conversation is lost the moment + the session ends, so it is not a way to carry a finding forward. +3. **Pass n+1** — launch a fresh `squad-reviewer` and give it + the round number, the previous round's findings, and the commits that + fixed them. It reviews the delta only, per its own instructions. +4. **Stop** at the first pass that reports no blocking findings. Cap the loop + at **three passes**: if blocking findings remain after the third, stop and + report the open findings to the user rather than continuing to iterate — + at that point the change needs a decision, not another round. + +Two rules keep this loop finite, and they are the point of the whole +arrangement: + +- **Later passes review the delta, never the whole diff again.** A fresh full + review of unchanged code always finds something new. +- **Only blocking findings start a new pass.** Non-blocking findings are + resolved or turned into an issue, not iterated on. + +## Findings that arrive after the push + +If a review lands on the pull request after it is open — from a human +reviewer, from an automated code review, or from the `review-pr` skill — work +those findings in this session, in this pull request. Do not defer a posted +finding to "the next change that touches this code": there is no such change +on the horizon, and the session holding the context needed to act on it will +not exist later. `review-pr` describes how to answer and close out each +posted comment. + +## Notes + +- Prefer non-interactive commands only. +- Do not amend existing commits unless the user explicitly asks. +- If a PR already exists for the branch, push the new commits and report the + existing URL instead of opening a duplicate. +- If push or PR creation fails, stop and report the failure clearly instead + of continuing as if it succeeded. +- Never force-push over another contributor's commits without explicit + confirmation. +- If the change touches a guarantee, security area or integration-surface + entry in [`.squad/project.md`](../../../.squad/project.md), a configuration + key, or the Docker/CI setup, make + sure the corresponding documentation — [`README.md`](../../../README.md), + [`ARCHITECTURE.md`](../../../docs/ARCHITECTURE.md), + [`SECURITY.md`](../../../SECURITY.md) — was updated in the same PR (see the + template checklist). See + [`CONTRIBUTING.md`](../../../docs/CONTRIBUTING.md) for the full workflow and + stability policy this skill follows. \ No newline at end of file diff --git a/.github/skills/review-pr/SKILL.md b/.github/skills/review-pr/SKILL.md new file mode 100644 index 0000000..b69f7cb --- /dev/null +++ b/.github/skills/review-pr/SKILL.md @@ -0,0 +1,117 @@ +--- +name: review-pr +description: Use when the user asks to review a pull request of this repository on GitHub. Checks out the PR, runs the build and tests, reviews it with the squad-reviewer subagent against this project's stack, analyzer, security and unit-test conventions, and posts the findings with an explicit verdict. +--- + +# Review PR + +Use this skill to review a pull request on GitHub — someone else's, or your +own when you deliberately want a second opinion after it is open. + +For a change that has not been pushed yet, do not use this skill: the +internal review loop in `create-pr` reviews the local branch before the pull +request exists, which is cheaper and does not fill the PR with comment +threads. + +Write everything in **English** — the summary to the user, the findings, and +anything posted to GitHub — regardless of the language the user wrote in. + +## Steps + +1. Fetch and check out the PR (or read the diff directly if a checkout isn't + necessary). Read the PR title and body to understand the intent, and read + any issue it references so you can judge whether the change actually + solves the stated problem. If the PR is already merged or closed, say so + and ask whether the user still wants a review. +2. Delegate the review itself to the `squad-reviewer` subagent + (subagent_type `squad-reviewer`, model `opus`). Give it the + base ref, the head SHA, and the round number — round 1 for a first review, + and for a re-review the previous round's findings plus the commits that + were meant to fix them. The review checklist, the integration-surface + sweep, the severity model and the round semantics all live in that agent's + definition (`.claude/agents/squad-reviewer.md`), so they stay + identical whether the review runs before or after the push; an agent + without subagent support follows that same file inline. +3. Post the result: + - Inline comments for findings anchored to a line, otherwise one review + comment. + - **Only genuine findings.** No positive remarks, no confirmation that + checklist items pass, no "looks good" filler, no formatting + the formatter already fixes. + - Lead the review body with the verdict line the subagent produced + (`APPROVE`, or the blocking/non-blocking counts), so the author can see + whether anything is required of them without reading every thread. + - Mark each finding `blocking` or `non-blocking` explicitly. +4. If the review produces no findings, post nothing beyond a short approving + verdict — and if the previous round already said the same, post nothing at + all. + +## Every posted finding gets worked + +A finding that has been posted as a review comment is work, not a note. This +holds for **every** posted finding — blocking and non-blocking alike, whether +it came from this skill, from a human reviewer, or from an automated code +review on the pull request. + +- Resolve it in the pull request it was posted on, while the session that can + act on it is still running. +- Do not defer a posted finding to "the next change that touches this code" + or "the next substantive commit". No such change is scheduled, and the + session holding the context needed to act on the comment will not exist + later — the deferral is a way of dropping the finding, not of carrying it + forward. +- If a posted finding genuinely should not be acted on in this PR, it gets + one of two concrete outcomes, never an implied one: a reply explaining why + the code stays as it is, or a GitHub issue opened **now** and linked from + the reply. Either way the thread is answered and resolved before the PR is + considered done. +- Non-blocking is about whether a finding gates the merge, not about whether + anyone will ever deal with it. + +## Keeping the loop finite + +A pull request review can always produce one more finding. These rules make +it converge, without leaving posted findings unhandled: + +- **Round 1 reviews the whole diff. Every later round reviews only the + delta**: does each fix resolve its finding, and did the fix commits break + something — including in prose they wrote to fix a documentation finding? + Never re-review untouched code; that is what turns three findings into four + rounds. +- **Only blocking findings justify another review round.** A non-blocking + finding is still worked per the section above, but working it does not earn + a new round of review. +- **Two consecutive rounds without a blocking finding means done.** Say so + plainly instead of leaving the review open-ended. +- **At most two rounds on GitHub.** If blocking findings survive that, the + change needs a decision from the author, not another review pass — say what + is still blocking and stop. +- The number of rounds is capped; the number of posted findings that get + handled is not. Every open thread is answered before the PR is done, even + when no further round runs. + +## Answering findings on your own PR + +When acting as the author of a PR under review: + +- Fix the finding, push, then keep the reply to one line: + `Fixed in <sha>: <what changed>`. The reasoning belongs in the commit + message, where it stays with the code; the reviewer verifies the commit, + not the reply. +- Re-run *Format*, *Build*, the *Analyzer gate* (no diagnostic in a changed + file), *Test with coverage* and the *Coverage gate* from `.squad/stack.md` + before each push — a fix that turns CI red costs more + than the finding did. +- Resolve the thread once it is answered. One summary comment per round beats + one essay per thread. +- Work through every open thread before calling the PR done, including the + non-blocking ones, as described above. + +## Notes + +- This skill reviews; it does not silently rewrite the PR. Fixing findings on + your own PR is the author's step above, and it is explicit — never edit + someone else's branch without being asked. +- Prefer non-interactive commands only. +- Base the verdict on evidence from the diff and the code; if something is + uncertain, say so instead of guessing. \ No newline at end of file diff --git a/.github/skills/squad-issue/SKILL.md b/.github/skills/squad-issue/SKILL.md new file mode 100644 index 0000000..adf45dc --- /dev/null +++ b/.github/skills/squad-issue/SKILL.md @@ -0,0 +1,176 @@ +--- +name: squad-issue +description: Use when the user asks to fix a specific GitHub issue in this repository. Runs the squad pipeline — Lead plans and picks a tier, the Devil's Advocate challenges the plan, Security reviews security-relevant plans, Tester writes failing tests first, Dev implements to 80% coverage, Code Officer clears format and analyzer findings, Reviewer (+ Security) review, Lead approves — and opens a PR referencing the issue. +--- + +# Squad Issue + +Fix a reported GitHub issue with the squad defined in `.squad/`. You are the **orchestrator**: you launch +the members as subagents, pass their outputs on (they cannot talk to each other), enforce the tiers and +loop limits from [`.squad/routing.md`](../../../.squad/routing.md), and perform every Git and GitHub +action yourself — including follow-up issues the Lead decides on. + +- **You never do a member's work.** You do not edit production code, tests or documentation the plan + assigns to the Dev, do not run the formatter, and do not fix analyzer findings — not even a one-line `sed`. + Whatever a check of yours finds goes to its owner (production code → `squad-dev`, tests → + `squad-tester`, formatting/analyzer-only edits → `squad-code-officer`) and through the steps that follow + it. Besides read-only checks (`--check`, the analyzer and coverage scripts, tests) you only write the + squad's bookkeeping: `log.md` and `tasks.md` check marks (features). Never production code, tests or `docs/`. +- **The squad does not change itself in a product PR.** An issue or feature PR never touches `.squad/` + (including `history.md` and `decisions.md`), `.claude/`, `.github/skills/`, `.agents/skills/`, `CLAUDE.md`, `AGENTS.md` + or `.github/copilot-instructions.md`. Lessons about the squad are filed in step 12 as `.squad/routing.md`, + *Squad lessons*, says — template-managed files in the template repository, project knowledge here. If the + change itself genuinely needs one of those files (e.g. a new build command every contributor must know), + the Lead escalates instead and the Product Manager decides: `.squad/stack.md` and `.squad/project.md` may + change in the product PR (*Scope of a product PR*); a template-managed file is changed in the template + repository; any other squad or instruction file (e.g. a project block) goes into a separate + squad-maintenance PR. +- **Working records stay off `main`.** `specs/<folder>/` exists only on the work branch, so it survives a + crashed session. Before the PR (step 10) its content is posted as a comment and the folder is removed; + the lasting reasoning lives in `docs/decisions/`. +- **One build at a time.** Never run two members that build or test (`squad-dev`, `squad-tester`, + `squad-code-officer`, the reviewers' verification runs) in parallel: they share `bin/` and `obj/` and + break each other (`.squad/routing.md`). Launch them one after another; only `squad-reviewer` and + `squad-security` may run together in step 8, because both are read-only and the reviewer's own build + happens in a scratch copy. +- **Commits and pushes** to the work branch are always allowed (`CLAUDE.md`, golden rules): commit and + push `specs/<folder>/log.md` right after intake (step 1), so a stop hook or a crashed session finds no + untracked files, and after every further completed step. PRs are merged with *Squash and merge*, so only + the PR title and description reach `main`; intermediate commit messages may name the step, but never + contain secrets. Interim work-in-progress commits — e.g. demanded by a stop hook while a member is still + working — are fine for the same reason. Stage with plain `git add -A`: ignored paths such as `TestResults/` + are skipped anyway, and an exclusion pathspec for an ignored path makes `git add` fail and stage + nothing. Never commit to `main`. +- **GitHub access:** use the GitHub MCP tools (`mcp__github__*`) for issues, comments, labels and pull + requests. In these sessions the `gh` CLI only works as `gh api repos/<owner>/<repo>/...`; `gh issue`, + `gh pr` and `gh search` fail (GraphQL is blocked and search is not scoped to the repository). +- **Pull request:** invoking this skill is the user's approval for opening the PR in step 10, once the + Lead has approved it (tier `docs`: once the latest review round is clean). +- **Product Manager:** the user is only contacted when the Lead returns `RESULT: ESCALATE` (relay the + question verbatim with its options and wait) or for confirming a public issue comment on + `RESULT: NO CHANGE`. +- Everything that ends up in the repository or on GitHub is written in **English**. + +## Steps + +1. **Intake.** Read the issue in full, including comments; note the reported environment (versions of the + software involved, image tag or release, host OS, configuration). If it is closed, stop and report that. Start + from a clean working tree on a new branch off the latest `main`, e.g. + `fix-issue-<number>-<short-slug>` (or the branch the session prescribes). Create + `specs/issue-<number>/log.md` from `specs/_template/log.md`, commit and push it; append one table row + per step. Only you + (the orchestrator) write `log.md`, one row per append, each row ending in the file's line ending — subagents report and + you record, so rows never merge or end up with mixed line endings. +2. **Plan.** Launch `squad-lead` in mode `plan` with the issue text and the work folder. It returns one of: + - `RESULT: DONE` — for tier **`docs`** (see its definition in `.squad/routing.md`), a short result (tier, the files and lines to change, acceptance + criteria) that you record as the first plan row in `log.md`, then continue with step 6 (Dev), the + read-only check from the `docs` row in `.squad/routing.md`, one review round in step 8, and step 10 + directly — no Security, skeleton, tests, coverage, Code Officer or Lead approval. Otherwise: + `plan.md` with the **tier** (`trivial` / `standard` / `security`), acceptance + criteria, the signatures of new or changed API, required documentation updates (`README.md`, + `docs/`), and `Proposed` decision records. Continue with the steps the tier requires. + - `RESULT: NO CHANGE` — show the proposed issue comment to the user, post it only after confirmation + (append the log as a collapsed "Squad working record" block), remove the work folder with a commit + and push, and stop. No PR; the branch stays as it is, and you tell the user so. + - `RESULT: ESCALATE` — ask the user, then relaunch the Lead with the answer. + + **Plan challenge** (`standard` and `security` only, once). Launch `squad-devils-advocate` with the issue + text and the work folder. On `VERDICT: OBJECTIONS …`, launch `squad-lead` in mode `revise` with the + objections; it answers each one in the plan's *Challenge* section (accepted and the plan revised, or + rejected with a reason) and may narrow the scope, raise the tier or switch to `RESULT: NO CHANGE`. + `RESULT: NO CHANGE` and `RESULT: ESCALATE` are handled as above; after a raised tier, continue with that + tier's steps. There is no second challenge round and no veto. Record the verdict (also a clean + `NO OBJECTIONS`) and the Lead's answer in `log.md`. +3. **Plan security review** (`security` tier only). Launch `squad-security` in mode `plan`. On + `CHANGES_REQUIRED`, launch `squad-lead` in mode `revise` and repeat. After the **2nd** rejection launch + `squad-lead` in mode `decide` (scope down, split into issues, abort, or escalate). +4. **Skeleton** (only if the plan adds or changes API). Launch `squad-dev` in mode `skeleton`: the planned + signatures built as *Skeleton* in `.squad/stack.md` describes (bodies fail when called), plus the existing + test call sites the plan assigns to the Dev for an incompatible signature change, so the tests of step 5 + compile. +5. **Tests first** (skipped for `trivial`). Launch `squad-tester` in mode `tests-first`. Confirm yourself + that the new tests compile and fail on the current code (unless the Tester justified why one cannot). + A fix without a reproducing test is only acceptable when the bug genuinely needs a live external + system — then the PR says so. +6. **Implement and cover.** Launch `squad-dev` in mode `implement` with the plan and the test names; it + also makes the documentation updates the plan lists. If the Dev disputes a test, launch `squad-lead` + in mode `decide`; the Tester changes a test only if the Lead says so. Then launch `squad-tester` in + mode `coverage`; repeat Dev/Tester until the *Coverage gate* (after *Test with coverage*, both in + `.squad/stack.md`) passes (≥ 80 % on new/changed production code and overall). Lines reported as not unit-testable go to + `squad-lead` in mode `decide`; an accepted gap is recorded in `log.md`. +7. **Code check.** Launch `squad-code-officer` with the base ref — the only member that runs + the formatter and clears analyzer diagnostics. Then verify yourself, without formatting, with the + commands from `.squad/stack.md`: *Format check* exits 0, the *Analyzer gate* passes (no diagnostic of + any severity in a changed file), *Test* is green with the same tests, and the *Coverage gate* still + passes. Structural items handed back go to `squad-dev` (or + `squad-tester`), followed by another code check. This is the gate before the PR; CI is not meant to find anything here. +8. **Review.** Launch `squad-reviewer` (round 1, full) and — for `standard` and `security` — + `squad-security` in mode `diff`, in parallel, against the base ref. Pass both the work folder + (`specs/<folder>/`) so they check the plan's acceptance criteria and tier (tier `docs`: the first + `log.md` row, since there is no `plan.md`); either may raise the tier. Tier `docs`: a blocking finding + goes to `squad-dev`, then the read-only check and a delta round, then step 10. Blocking + findings → their owner fixes them (`squad-dev` for production code, `squad-tester` for tests) → steps 6 + (coverage) and 7 again → **a new review round on the delta is mandatory** before step 9; never go from + a blocking finding straight to PR approval. The same holds for a non-blocking finding the Lead decides + to fix now: any change to production code, tests or `docs/` after a review round needs a delta round. At most **2 fix rounds** after round 1; then `squad-lead` + in mode `decide`. Non-blocking + findings: the Lead decides per finding — fix now, or you open a linked GitHub issue now. +9. **PR approval.** Launch `squad-lead` in mode `approve-pr` with the base ref, the build/test/coverage + output and the review outcome — including the result of the **latest** review round, which must have + no blocking finding that is not covered by a recorded Lead decision, and must cover every change to + production code, tests and `docs/` since it ran (only `specs/` bookkeeping and the Lead's own approval edits — + record status, the index, a link from `docs/ARCHITECTURE.md` — may follow it; a fix for a blocking + finding always needs a delta round, also in a decision record). `NOT APPROVED` → back to step 6 or 8 (counting against the review loop + limit) or let the Lead decide/escalate. On `APPROVED`, the decision records are `Accepted` and indexed + in `docs/decisions/README.md`. +10. **Pull request** (Dev role, performed by you). First move the working record off the branch: post + `plan.md` (none for tier `docs`) and `log.md` as one comment on the issue (each inside a collapsed `<details>` block, headed + "Squad working record"), then `git rm -r specs/issue-<number>/`, commit ("Remove squad working + record"), and push. Later log rows (steps 11–12) are appended by editing that comment. Then open the + PR from + [`.github/pull_request_template.md`](../../../.github/pull_request_template.md): title per + `docs/CONTRIBUTING.md` — `[area] Description`, where `area` is one of the areas + CONTRIBUTING lists, capitalized — not a lowercase class or file name. It becomes the squash + commit subject on `main`. Body describing the bug, the fix and the + reproducing test, `Closes #<number>` under Issues, links to the decision records. Next Steps lists + **only linked GitHub issues** (create them now) or "None" — never an unlinked "revisit later". Follow the `create-pr` skill's template rules, but + do **not** run its internal review loop — step 8 replaced it. If the fix is not fully verifiable + without a real external system, say so. +11. **After the PR.** Subscribe to the PR's activity right after opening it (`subscribe_pr_activity` when + available; otherwise check the CI and code-analysis (e.g. SonarQube Cloud) results yourself before finishing) — a session + that ends with an unwatched PR has not completed this step. Stay with the PR until CI is green and the + code-analysis quality gate (e.g. SonarQube Cloud) passes: + - code-analysis findings → `squad-code-officer` (structural ones → `squad-dev`); + - failing build or tests → `squad-dev` (test defects → `squad-tester`); + - review comments (human, automated, `review-pr`) → `squad-dev`, worked in this PR, blocking or not. + + Each fix goes through steps 7–8 again (delta review), with at most 2 fix rounds per failure before the + Lead decides. The work folder is gone by now: give the Reviewer, Security and the Lead the plan (tier + `docs`: the first log row; features: + also `spec.md` and `tasks.md`) from the "Squad working record" comment, or via + `git show <commit-before-removal>:specs/<folder>/<file>`, and record each log row by editing that + comment. Never skip, disable or weaken a test to get green. +12. **Wrap-up (mandatory).** Collect what this run taught about the squad itself (a rule that was + unclear or contradictory, a tool that misbehaved, an agent that could not be launched, a step that + had to be improvised), each with the role it concerns and a concrete proposal, and file them as + `.squad/routing.md`, *Squad lessons*, says: lessons about template-managed files as **one** issue + labelled `squad` in the template repository named in `.squad/template.json` (attach that repository to the session if + needed; without access, file it here with the label `squad-upstream`), lessons about project knowledge + as **one** issue labelled `squad` in this repository (create the labels if missing). Link the issues + from the working record comment. Do **not** edit `.squad/`, `.claude/` or the instruction files. + Report the branch, the PR URL, the tier, the `squad` issues (or "no lessons") and any escalation or + Lead decision to the user. If there is genuinely nothing to learn, + append a `| <date> | 12 Wrap-up | Orchestrator | no lessons |` row to the working record comment + instead of opening an issue — the step itself is never skipped. + +## What the pull request says — and what it doesn't + +The PR title and description document the change, not how it was produced: the bug, the fix and the test +that pins it down. Plan revisions, review rounds and their findings never appear there. +(The working record is the "Squad working record" comment on the issue; the lasting reasoning is in +`docs/decisions/`.) + +## Notes + +- Prefer non-interactive commands only. If push or PR creation fails, stop and report it. +- Never close the issue manually; `Closes #<number>` closes it on merge. diff --git a/.github/skills/squad-spec/SKILL.md b/.github/skills/squad-spec/SKILL.md new file mode 100644 index 0000000..847ce65 --- /dev/null +++ b/.github/skills/squad-spec/SKILL.md @@ -0,0 +1,31 @@ +--- +name: squad-spec +description: Use when the user wants to develop a new feature in this repository spec-driven with the squad. Lead writes spec, plan and tasks and picks a tier, the Devil's Advocate challenges them, Security reviews security-relevant plans, Tester writes failing tests first, Dev implements to 80% coverage, Code Officer clears format and analyzer findings, Reviewer (+ Security) review, Lead approves, then a PR is opened. +--- + +# Squad Spec + +Build a feature with the squad defined in `.squad/`. Tiers, pipeline, loop limits, escalation rules, +commit/push rules and the orchestrator role are identical to the `squad-issue` skill — follow its steps +1–12 with these changes: + +- **Step 1 — work folder and branch:** `specs/feature-<short-slug>/` with `log.md` from + `specs/_template/log.md`; branch `feature-<short-slug>` off the latest `main` (or the branch the session + prescribes). +- **Step 2 — plan:** `squad-lead` in mode `plan` writes `spec.md` (behavior, acceptance criteria, out of + scope), `plan.md` and `tasks.md`. A feature is never `docs` and rarely `trivial`. It is more likely than a bug + fix to need a product decision — the Lead escalates whenever the request does not settle user-visible + behavior. `RESULT: NO CHANGE` means the feature already exists or contradicts an accepted decision; report + that to the user instead of commenting on an issue. The plan challenge covers `spec.md`, `plan.md` and + `tasks.md` together. +- **Decision records:** features usually involve real design choices, so expect at least one record in + `docs/decisions/`; the Lead also updates `docs/ARCHITECTURE.md` when the feature changes a flow or + guarantee. +- **Step 3** reviews `spec.md` and `plan.md` together. +- **Steps 4–6** run per task or group of tasks from `tasks.md`; tick tasks off as they are done. Run + Tester and Dev one after another, never in parallel (see *Concurrency* in `.squad/routing.md`). +- **Step 10 — pull request:** the working record (`spec.md`, `plan.md`, `tasks.md`, `log.md`) is posted + to the feature request issue if one exists (`Closes #<n>` in the PR), otherwise as the first comment on + the PR right after opening it; the folder is removed before the PR as in `squad-issue`. Behavior that + must stay documented belongs in `README.md`, `docs/ARCHITECTURE.md` or a decision record, not in + `spec.md`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a0eeef..a042747 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,8 @@ jobs: /k:"LarsLaskowski_DockerUpdateGuard" \ /o:"networlddev" \ /d:sonar.token="$SONAR_TOKEN" \ - /d:sonar.cs.opencover.reportsPaths="${{ runner.temp }}/TestResults/**/coverage.opencover.xml" + /d:sonar.cs.opencover.reportsPaths="${{ runner.temp }}/TestResults/**/coverage.opencover.xml" \ + /d:sonar.coverage.exclusions=".squad/**,.claude/**" - name: Build run: dotnet build DockerUpdateGuard.slnx -c Release --no-restore diff --git a/.squad/agents/code-officer/charter.md b/.squad/agents/code-officer/charter.md new file mode 100644 index 0000000..920117a --- /dev/null +++ b/.squad/agents/code-officer/charter.md @@ -0,0 +1,20 @@ +# Code Officer + +**Owns:** code quality and style of the change, after implementation and before the review. The Code +Officer is the **only** squad member that runs the formatter (*Format* in `.squad/stack.md`) and the one +responsible for a passing *Analyzer gate* (no diagnostic of any severity in a changed file). CI does not +replace this step, so nothing the Code Officer lets through is caught before the pull request. + +- **Format:** run *Format* and confirm with *Format check*. +- **Analyzers:** run the *Analyzer gate* and clear everything it lists in changed files. Local analyzers + are configured to report what SonarQube Cloud (or the repository's CI analysis) would report; where the + stack has gaps, `stack.md` says which findings can still arrive after the push (squad step 11). A rule + that must not apply gets a justified, narrowly scoped suppression only with the Lead's approval + (recorded in a decision record) — never a blanket suppression. +- **Style:** the code conventions in `stack.md` and the project's code style section in `CLAUDE.md`. +- **Not allowed:** changing behavior, signatures used across files, control flow, test assertions or test + data. Control flow includes adding a guard, branch, null check or early return to satisfy a rule, and + replacing an assertion with another one; those go to the Dev (production code) or Tester (tests) with + the exact diagnostic. +- Only touches files already in the diff. Afterwards the build and full test suite are green with the same + set of passing tests. diff --git a/.squad/agents/code-officer/history.md b/.squad/agents/code-officer/history.md new file mode 100644 index 0000000..9467077 --- /dev/null +++ b/.squad/agents/code-officer/history.md @@ -0,0 +1,4 @@ +# History + +Learnings worth keeping across sessions (conventions discovered, pitfalls, decisions that affected this +role). Append short dated entries; do not log routine work. diff --git a/.squad/agents/dev/charter.md b/.squad/agents/dev/charter.md new file mode 100644 index 0000000..34b87a2 --- /dev/null +++ b/.squad/agents/dev/charter.md @@ -0,0 +1,21 @@ +# Dev + +**Owns:** production code (*Layout* in `.squad/stack.md`), and creating the pull request at the end +(performed by the orchestrator, which holds the Git and GitHub tools). + +- Builds a compile-only skeleton of new/changed API first when the plan requires one (*Skeleton* in + `stack.md`), so tests can be written before the implementation. +- Implements the approved plan minimally, including the documentation updates the plan lists, until the + Tester's tests and the full suite are green. No unrelated refactoring, no scope creep. +- Writes code in the project style from the start (*Writing code* in `stack.md`, code style in + `CLAUDE.md`) — but does **not** run the formatter and does not chase style diagnostics; that is the Code + Officer's job. Analyzer findings in its own files that need a code change (not just style) are fixed by + the Dev before handing over. +- Works with the Tester until **at least 80 % line coverage on new/changed production code** and at least + 80 % overall are reached (*Coverage gate*). Code that is hard to test is a design signal for the Dev + (seams, injected dependencies), not a reason to skip coverage; a genuinely untestable line (e.g. process + startup glue) needs a Lead decision. +- Does not edit tests — except, in the skeleton step, the existing test call sites of an incompatibly + changed signature that the plan assigns to the Dev (`.squad/routing.md`, *Loop limits*). If a test looks wrong, or the plan does not work, report to the Lead instead of + deviating. +- Fixes blocking review findings and structural items the Code Officer hands back. diff --git a/.squad/agents/dev/history.md b/.squad/agents/dev/history.md new file mode 100644 index 0000000..9467077 --- /dev/null +++ b/.squad/agents/dev/history.md @@ -0,0 +1,4 @@ +# History + +Learnings worth keeping across sessions (conventions discovered, pitfalls, decisions that affected this +role). Append short dated entries; do not log routine work. diff --git a/.squad/agents/devils-advocate/charter.md b/.squad/agents/devils-advocate/charter.md new file mode 100644 index 0000000..5a6d173 --- /dev/null +++ b/.squad/agents/devils-advocate/charter.md @@ -0,0 +1,12 @@ +# Devil's Advocate + +**Owns:** one challenge of the plan in step 2 (tiers `standard` and `security`), before Security and +before any code is written. + +Questions the plan's assumptions against the code, whether a change is needed at all, simpler +alternatives, the scope (too wide or too narrow) and whether tier and acceptance criteria fit the +reported problem. Every objection carries evidence (file and line, or the plan passage) and is ranked +`major` or `minor`. + +No veto and exactly one round: the Lead answers every objection in `plan.md` — accepted (plan revised) +or rejected with a reason — and decides. Read-only; never edits files. diff --git a/.squad/agents/devils-advocate/history.md b/.squad/agents/devils-advocate/history.md new file mode 100644 index 0000000..9467077 --- /dev/null +++ b/.squad/agents/devils-advocate/history.md @@ -0,0 +1,4 @@ +# History + +Learnings worth keeping across sessions (conventions discovered, pitfalls, decisions that affected this +role). Append short dated entries; do not log routine work. diff --git a/.squad/agents/lead/charter.md b/.squad/agents/lead/charter.md new file mode 100644 index 0000000..0b80b9a --- /dev/null +++ b/.squad/agents/lead/charter.md @@ -0,0 +1,37 @@ +# Lead + +**Owns:** `plan.md` (issues), `spec.md` / `plan.md` / `tasks.md` (features), the decision records in +`docs/decisions/`, every decision inside the squad, and the PR approval. (`.squad/decisions.md` changes only +in squad-maintenance PRs, never in a product PR.) + +- **Plan:** first check every factual claim of the issue against the code and plan from what the code + actually does. Classify the tier (`.squad/routing.md`), state the root cause (issue) or the behavior + (feature), the acceptance criteria the Tester will turn into tests, the files/types to change, the test + files (named per *Layout* in `.squad/stack.md` and `docs/UNIT_TESTS.md`), the + signatures of new/changed API (for the Dev's skeleton), the documentation updates, and an architecture check against + `docs/ARCHITECTURE.md` — the deliberate guarantees listed in `.squad/project.md` may not be weakened + without the Product Manager. +- **Revise** the plan on a Security `CHANGES_REQUIRED`, addressing every point, and answer every Devil's + Advocate objection in the plan's *Challenge* section (accepted and revised, or rejected with a reason). +- **Decide** when a loop limit is hit or members disagree: accept with justification, split into a + separate issue, narrow the scope, or abort. State the decision in your result — the orchestrator records it in `log.md`. A decision about the squad + itself that outlives this change goes into the step-12 `squad` issue, not into `.squad/`. +- **Record the why:** every decision about the code that a reader months later could not reconstruct + from the diff alone gets a decision record in `docs/decisions/` (rules and threshold in + `docs/decisions/README.md`): context, options considered, decision, consequences, and links to the + issue (whose "Squad working record" comment replaces the removed `specs/` folder). Draft it as `Proposed` with the plan, update it when Security, review or a + Lead decision changes the outcome, and set it to `Accepted` with the PR approval. Never rewrite an + accepted record — supersede it. If an architectural guarantee or flow changes, update + `docs/ARCHITECTURE.md` too and link the record from it. +- **Approve the PR:** confirm the latest review round has no blocking finding that is not covered by a + recorded decision of yours, and covers every change to production code, tests and `docs/` since it ran except + `specs/` bookkeeping and your own approval edits (record status, the index, a link from + `docs/ARCHITECTURE.md`) — a correction that resolves a blocking finding needs a delta round, even in + your own record; otherwise a delta review is missing; check the final diff + against the plan and acceptance criteria, confirm build/tests are green, confirm coverage meets 80 % on new/changed code and overall (or + each gap has a recorded decision), confirm the decision records for this change exist and match what was built, then answer `APPROVED` or + `NOT APPROVED` with reasons. +- **No change:** if an issue needs no code change (duplicate, not reproducible, works as designed, out of + scope), say so with a proposed issue comment instead of planning a fix. +- **Escalate** to the Product Manager only as defined in `.squad/routing.md`. +- Never edits production or test code, never runs Git write operations. diff --git a/.squad/agents/lead/history.md b/.squad/agents/lead/history.md new file mode 100644 index 0000000..9467077 --- /dev/null +++ b/.squad/agents/lead/history.md @@ -0,0 +1,4 @@ +# History + +Learnings worth keeping across sessions (conventions discovered, pitfalls, decisions that affected this +role). Append short dated entries; do not log routine work. diff --git a/.squad/agents/reviewer/charter.md b/.squad/agents/reviewer/charter.md new file mode 100644 index 0000000..c4dfc66 --- /dev/null +++ b/.squad/agents/reviewer/charter.md @@ -0,0 +1,10 @@ +# Reviewer + +**Owns:** the code review (step 8), together with Security. Implemented by the read-only subagent +`.claude/agents/squad-reviewer.md` (round 1 full review, later rounds delta only, blocking/non-blocking +severity model), which sweeps the integration surface listed in `.squad/project.md`. + +- Additionally checks the diff against the plan's acceptance criteria, and flags (blocking) a change + whose tier is too low for what it touches (`.squad/routing.md`). For tier `docs` the tier and criteria + are in the first `log.md` row, and any file outside the `docs` definition is a blocking tier raise. +- Never edits files, never commits or posts; reports findings to the orchestrator. diff --git a/.squad/agents/reviewer/history.md b/.squad/agents/reviewer/history.md new file mode 100644 index 0000000..9467077 --- /dev/null +++ b/.squad/agents/reviewer/history.md @@ -0,0 +1,4 @@ +# History + +Learnings worth keeping across sessions (conventions discovered, pitfalls, decisions that affected this +role). Append short dated entries; do not log routine work. diff --git a/.squad/agents/security/charter.md b/.squad/agents/security/charter.md new file mode 100644 index 0000000..d2f7b6a --- /dev/null +++ b/.squad/agents/security/charter.md @@ -0,0 +1,11 @@ +# Security + +**Owns:** the security verdict on the plan (step 3, `security` tier) and on the diff (step 8, `standard` +and `security` tiers). + +Focus areas: the *Security areas* in `.squad/project.md` (this project's attack surface — secrets, +authentication, file writes, parsing of external input, outbound calls, logging of external data, …), +CI/Docker/build configuration defaults, and new or updated dependencies. + +Answer with `APPROVED` or `CHANGES_REQUIRED`, each required change concrete and backed by evidence (file +and line, or the plan passage). No speculative or generic advice. diff --git a/.squad/agents/security/history.md b/.squad/agents/security/history.md new file mode 100644 index 0000000..9467077 --- /dev/null +++ b/.squad/agents/security/history.md @@ -0,0 +1,4 @@ +# History + +Learnings worth keeping across sessions (conventions discovered, pitfalls, decisions that affected this +role). Append short dated entries; do not log routine work. diff --git a/.squad/agents/tester/charter.md b/.squad/agents/tester/charter.md new file mode 100644 index 0000000..5acc18d --- /dev/null +++ b/.squad/agents/tester/charter.md @@ -0,0 +1,18 @@ +# Tester + +**Owns:** unit tests (*Layout* in `.squad/stack.md`), and the coverage figure of the change. + +- **Tests first:** derive tests from the acceptance criteria in the plan/spec, before the Dev touches + production code. For a bug, reproduce it with the input the issue reports. The new tests must compile + (or load) and fail against the current code; new API is available as a compile-only skeleton from the + Dev (*Skeleton* in `stack.md`). +- **Coverage:** after the Dev's implementation, run *Test with coverage* and the *Coverage gate* and add + tests until **at least 80 % line coverage on new/changed production code** and at least 80 % overall + are reached. Tests that only execute lines without asserting behavior do not count. If the Dev adapted + existing test call sites in the skeleton step (`.squad/routing.md`, *Loop limits*), check that only the + planned call sites changed and no assertion was weakened. +- Follow `docs/UNIT_TESTS.md` and *Writing tests* in `stack.md` (framework, test doubles from + `.squad/project.md`, naming, one assertion message per assertion where the framework supports it). Run + the *Analyzer gate* before handing over and fix the findings in your test files that are test-design + issues. Does not run the formatter; the Code Officer does. +- Never weaken a test to make it pass — a test the Dev disputes goes to the Lead. diff --git a/.squad/agents/tester/history.md b/.squad/agents/tester/history.md new file mode 100644 index 0000000..9467077 --- /dev/null +++ b/.squad/agents/tester/history.md @@ -0,0 +1,4 @@ +# History + +Learnings worth keeping across sessions (conventions discovered, pitfalls, decisions that affected this +role). Append short dated entries; do not log routine work. diff --git a/.squad/decisions.md b/.squad/decisions.md new file mode 100644 index 0000000..26fb494 --- /dev/null +++ b/.squad/decisions.md @@ -0,0 +1,10 @@ +# Squad Decisions + +Process decisions about how the squad works in this repository — not decisions about the product code +(those are decision records in `docs/decisions/`). Append-only: add a dated entry, never rewrite an old +one. Changed only in squad-maintenance PRs. + +- 2026-10-03 — Squad adopted from Squad-Spec-Repository-Template (`adopt-template`), stack profile + `dotnet`. Reason: one shared, stack-neutral squad and rule set across all repositories; project + knowledge lives in `.squad/stack.md`, `.squad/project.md` and the `<!-- project:… -->` sections of the + instruction files. diff --git a/.squad/project.md b/.squad/project.md new file mode 100644 index 0000000..7331933 --- /dev/null +++ b/.squad/project.md @@ -0,0 +1,85 @@ +# Project + +What the squad needs to know about this project that is not stack-specific. Read by the Lead, the Devil's +Advocate, Security, the Tester and the Reviewer. Not template-managed: `adopt-template` creates it once +and never overwrites it. A product PR updates it when the change makes an entry untrue +(`.squad/routing.md`, *Scope of a product PR*). + +## Security areas + +A change that touches one of these is tier `security` (`.squad/routing.md`). Details in +`docs/ARCHITECTURE.md`, *Security posture and operating assumptions*. + +- **Secrets in configuration:** Docker Hub PAT, Portainer credentials/API token, database password — never + logged, never shown in the UI, never in exception messages or telemetry attributes. +- **TLS and transport relaxations:** `SkipCertificateValidation` (Docker instances) and `AllowInsecureHttp` + (Portainer) — must stay opt-in and keep their runtime warnings. +- **Docker Engine access:** `DockerInstanceClient` (HTTP, TCP, Unix socket, named pipe transports); the + mounted Docker socket is root-equivalent on the host. +- **Container actions:** `PortainerClient` restart/redeploy/update actions and the UI that triggers them. +- **External process execution:** `TrivyVulnerabilityProvider` via `IProcessRunner` (argument construction, + untrusted image references, JSON parsing of stdout). +- **Registry and Docker Hub calls:** `DockerHubClient`, `OciRegistryClient`, `DockerScoutVulnerabilityProvider` + (untrusted JSON, pagination caps, token caching), `TransientHttpRetryHandler`. +- **Database and migrations:** EF Core queries built from user input, the PostgreSQL advisory lock around + migrations. +- **Container image and runtime user:** `src/DockerUpdateGuard/Dockerfile` (non-root UID 64000, GID 0). +- **Logging and telemetry of external data:** OpenTelemetry attributes and log messages that carry + registry, container or credential data. + +## Guarantees + +Deliberate behavior that must not change without the Product Manager. Each one is described in +`docs/ARCHITECTURE.md` (*Key design decisions* and the sections it points to). + +- No authentication or authorization in the app itself: it assumes a trusted network perimeter (reverse + proxy, VPN, private network). +- Integration clients never throw for expected failures; they return `ExternalOperationResult<T>` + (`NotConfigured`, `Unsupported`, `NotFound`, `Failed`, `Unknown`) and orchestrators branch on the status. +- Update detection (`IUpdateDetectionService`) and tag comparison (`VersionTagResolutionHelper`) stay pure + and I/O-free, including the conservative major-version-upgrade gating. +- `IImageCatalogRepository` is the single entry point for the registry/image-version catalog (get-or-create + with dedup and race retry); other entities are written by the orchestrator that owns them. +- Migrations are serialized by a PostgreSQL advisory lock at startup; the scan engine assumes a single + instance (duplicate scans are wasteful, never incorrect). +- Layering: web startup and DI wiring in `src/DockerUpdateGuard`, persistence in `.Data`, observability in + `.Telemetry`. + +## Integration surface + +What the Reviewer checks when the diff introduces or changes a thing of this kind: every place that must +change with it. + +**A new or changed configuration option** touches: +- its options class and the binding/validation in the host's composition root +- `appsettings*.json` and the configuration reference in `README.md` (key, environment variable, default) +- the deployment tiers in `docs/docker-compose.*.yml` and `INSTALL.md` when operators must set it +- `docs/ARCHITECTURE.md`, *Configuration model* + +**A new or changed service, background job or orchestrator** touches: +- its interface and its registration *and lifetime* in the composition root +- the background job engine section of `docs/ARCHITECTURE.md` (schedule, overlap rules) for a job +- the tests in `src/Tests/DockerUpdateGuard.Tests` + +**A new integration client or external call** touches: +- the client behind a narrow interface, returning `ExternalOperationResult<T>` +- `TransientHttpRetryHandler` attached to its `HttpClient` +- `RegistryMetadataService` dispatch (`CanHandle`) for a new registry type +- the client table in `docs/ARCHITECTURE.md`, *Integration clients* + +**A change to the entity model** touches: +- `DockerUpdateGuardDbContext`, a migration named per the convention in `CLAUDE.md` (*Project configuration*) +- `src/Tests/DockerUpdateGuard.Data.Tests` +- `docs/ARCHITECTURE.md`, *Data layer* + +**A new Blazor page or component** touches the UI tests (`…RenderTests`, `…PersistentStateTests`) and the +UI section of `docs/ARCHITECTURE.md`. + +**Async code in service and data-access code** uses `.ConfigureAwait(false)`; EF Core navigation and query +assumptions (included navigations, tracking) are checked against the query that loads the entity. + +## Test doubles + +MSTest with **NSubstitute** (`Substitute.For<T>()`) for collaborators that cross an infrastructure boundary +(HTTP, process, clock); real objects otherwise. Data tests use EF Core InMemory (host tests) or SQLite +(`DockerUpdateGuard.Data.Tests`). Details and helpers in `docs/UNIT_TESTS.md`. diff --git a/.squad/routing.md b/.squad/routing.md new file mode 100644 index 0000000..2478753 --- /dev/null +++ b/.squad/routing.md @@ -0,0 +1,160 @@ +# Routing + +The pipeline is the same for issues and features; only the input differs (a GitHub issue for +`squad-issue`, a feature idea plus `spec.md` for `squad-spec`). The orchestrator — the session that runs +the skill — launches the members, passes documents between them (subagents cannot talk to each other +directly), performs every Git and GitHub action (including follow-up issues the Lead decides on) and +records every step in the work folder's `log.md` (after step 10, in the "Squad working record" comment +that replaces it). Step numbers below are the ones the skills use. + +## Work folder + +- Issue: `specs/issue-<number>/` — `plan.md`, `log.md` +- Feature: `specs/feature-<short-slug>/` — `spec.md`, `plan.md`, `tasks.md`, `log.md` +- The work folder lives **only on the work branch**: it is committed and pushed after every step so a + crashed session can resume, and in step 10 its content is posted as a "Squad working record" comment + (on the issue, or on the PR for a feature without issue) and the folder is removed before the PR opens. + `main` never contains `specs/` working records. +- Decision records (both): `docs/decisions/NNNN-title.md` — the lasting *why*, written by the Lead; this + is what a reader months later looks at. + +## Scope of a product PR + +An issue or feature PR changes the product and its documentation only. It never touches the squad or the +agent instructions: `.squad/` (charters, `history.md`, `decisions.md`, tools), `.claude/`, +`.github/skills/`, `.agents/skills/`, `CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`. +Lessons about the squad are collected in step 12 and filed where they can be fixed (*Squad lessons* +below), never fixed in the product PR. The Reviewer reports any such file in a product PR as a blocking +finding. + +One exception: `.squad/stack.md` and `.squad/project.md` describe the product, not the squad. A product PR +updates them when the change itself makes them untrue — a new build command, a new security area, a new +guarantee or coupling point — and the Reviewer treats a stale entry there like stale documentation. + +## Template-managed files + +Many squad, instruction and documentation files come from the Squad-Spec-Repository-Template repository and +are refreshed from there with its `adopt-template` skill (`.squad/template.json` records the template +repository, its commit and the stack profile). Three kinds: + +- **Managed** — overwritten on every refresh: `.squad/team.md`, `.squad/routing.md`, the charters in + `.squad/agents/*/charter.md`, `.squad/tools/*.py` except `squad_settings.py`, `.squad/tools/.gitignore`, + `.claude/agents/squad-*.md`, `.claude/hooks/session-start.sh`, the template's skills under + `.claude/skills/`, `.agents/skills/` and `.github/skills/`, + `.github/ISSUE_TEMPLATE/feature_request.md`, `docs/decisions/_template.md`, `specs/README.md` and + `specs/_template/`. +- **Marked** — rebuilt on every refresh, keeping the repository's content inside + `<!-- project:… -->` blocks: `CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`, + `docs/CONTRIBUTING.md`, `docs/ARCHITECTURE.md`, `.github/ISSUE_TEMPLATE/bug_report.md`, + `.github/pull_request_template.md` and `docs/decisions/README.md`. Text outside the project blocks is the template's. +- **Seeded** — created once and owned by the repository from then on: `.squad/stack.md`, + `.squad/project.md`, `.squad/decisions.md`, the `history.md` files, `.squad/tools/squad_settings.py`, + `.claude/settings.json`, `SECURITY.md`, `docs/UNIT_TESTS.md` and the stack profile's CI, CodeQL, Dependabot and tool + configuration files. + +## Squad lessons + +Every lesson from step 12 is filed once, where it can actually be fixed: + +| The lesson concerns | Filed as | Fixed by | +| ------------------- | -------- | -------- | +| a **managed** file, or the template part of a **marked** file (squad rules, charters, agents, skills, tools, the shared sections of the instruction files) | an issue labelled `squad` in the template repository named in `.squad/template.json` (`repository`), titled `[Squad] <lesson> (from <this repository>#<issue>)` and linking the run | a PR in the template repository, then a refresh of every repository that uses the template (`adopt-template`) — never a local edit, which the next refresh would overwrite | +| **project knowledge**: `.squad/stack.md`, `.squad/project.md`, `.squad/tools/squad_settings.py`, a `<!-- project:… -->` block, another seeded file | an issue labelled `squad` in this repository | a small squad-maintenance PR in this repository, checked with `.squad/tools/config-check.py` | + +One run's lessons go into at most one issue per destination. If the session cannot create an issue in the +template repository (no access), it files that issue in this repository with the label `squad-upstream` +and tells the user, who moves it to the template repository; it is never worked here. + +## Tiers + +In step 2 the Lead classifies the change and justifies the tier in `plan.md` (for `docs`, in its result). When in doubt, the higher +tier applies; Security or the Reviewer may raise the tier at any point (never lower it). + +| Tier | When | Pipeline | +| ---- | ---- | -------- | +| `docs` | Issues only (never a feature). Only product Markdown documentation changes — `README.md`, `docs/` except `docs/decisions/`, `SECURITY.md`, and inside their `<!-- project:… -->` blocks `docs/CONTRIBUTING.md`, `docs/ARCHITECTURE.md`, the bug report and the pull request template — and no other file at all (template-managed files such as the feature-request template are fixed in Squad-Spec-Repository-Template, see *Template-managed files*): not even a comment in production or test code (that is `trivial`), no build, CI, Docker or config file, and never squad or instruction files (`.squad/`, `.claude/`, `.github/skills/`, `.agents/skills/`, `CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`). A change that needs a decision record is not `docs` either. | Lead plans briefly (no `plan.md`: tier, change list and acceptance criteria go into its result, recorded as the first `log.md` row); steps 3–7 and 9 skipped; the Dev makes the edits; the orchestrator verifies read-only (*Format check* in `.squad/stack.md`); one Reviewer round, which checks the diff against the first `log.md` row — a blocking finding goes to the Dev, then the read-only check and a delta round; the orchestrator opens the PR once the latest round is clean. | +| `trivial` | Documentation that does not qualify as `docs`, code comments, log or UI wording, configuration defaults, or a documentation change that needs a decision record — no change to behavior or control flow | Steps 3, 4 and 5 skipped (no Security, no tests-first); code check, Reviewer and Lead approval still run. Tests and coverage are still required if production code changes. | +| `standard` | A behavior change that touches none of the security areas below | Plan challenge in step 2; step 3 skipped; Security reviews only the diff (step 8) | +| `security` | Touches one of the security areas listed in `.squad/project.md` (*Security areas*), Docker/CI or build configuration, or adds/updates a dependency | Full pipeline, including the plan challenge in step 2 | + +## Pipeline + +| # | Step | Owner | Exit condition | +| - | ---- | ----- | -------------- | +| 1 | Intake | Orchestrator | Branch off `main`, work folder and `log.md` created, committed and pushed | +| 2 | Plan | Lead, Devil's Advocate | `plan.md` with tier, acceptance criteria, signatures of new/changed API, doc updates; decision records `Proposed`. Or outcome **no change** (see below). `standard`/`security`: one plan challenge by the Devil's Advocate, every objection answered by the Lead in the plan's *Challenge* section | +| 3 | Plan security review | Security | `APPROVED` → 4; `CHANGES_REQUIRED` → Lead revises, back to 3 (`security` tier only) | +| 4 | Skeleton | Dev | Only when the plan adds or changes API: compile-only signatures built as *Skeleton* in `.squad/stack.md` describes, *Build* passes | +| 5 | Tests first | Tester | Tests for every acceptance criterion; they compile and **fail** on the current code | +| 6 | Implementation + coverage | Dev, Tester | All tests green; ≥ 80 % line coverage on new/changed code and overall (*Coverage gate* in `.squad/stack.md`); doc updates from the plan done | +| 7 | Code check | Code Officer | *Format* and *Analyzer gate* from `.squad/stack.md` pass (no diagnostic of any severity in changed files); same tests green; no structural change | +| 8 | Review | Reviewer + Security | No blocking findings → 9; blocking → owner fixes (Dev: code, Tester: tests), back to 6, then a mandatory delta round (Security only for `standard`/`security`) | +| 9 | PR approval | Lead | Latest review round without a blocking finding not covered by a recorded Lead decision, and covering every change to production code, tests and `docs/` except `specs/` bookkeeping and the Lead's own approval edits (record status, index, `docs/ARCHITECTURE.md` link); plan fulfilled, coverage met, decision records `Accepted` and indexed → `APPROVED` → 10 | +| 10 | Pull request | Dev (via orchestrator) | Working record posted as comment, `specs/<folder>/` removed, PR opened (merged later with *Squash and merge*) | +| 11 | After the PR | Dev, Code Officer, Reviewer | CI green, SonarQube Cloud quality gate passed, review comments worked | +| 12 | Wrap-up | Orchestrator | Squad lessons filed as one issue per destination (*Squad lessons*), or "no lessons" logged; user informed | + +Commits and pushes to the work branch happen right after intake (`specs/<folder>/log.md`, so a stop hook +or a crashed session finds no untracked files) and after every further completed step; with *Squash and +merge* only the PR title and description reach `main`, so intermediate commits may describe the step. +They never contain secrets. + +## Concurrency + +Only one member that builds or runs tests may work at a time: concurrent builds and test runs share +build output and caches and break each other (see *Concurrency* in `.squad/stack.md` for what this stack +shares). In step 8, `squad-reviewer` and `squad-security` may run together because +both are read-only and the reviewer builds in a scratch copy. No member experiments (mutation tests, +trial edits) in the repository working tree — use a scratch `git worktree` instead. + +## Outcome "no change" + +If the Lead concludes in step 2 (also after answering the Devil's Advocate) that no code change is needed — duplicate, cannot be reproduced, works as +designed (e.g. covered by an accepted decision record), or out of scope — it returns +`RESULT: NO CHANGE` with a proposed issue comment. The orchestrator shows the comment to the Product +Manager and posts it only after confirmation (a public statement on the issue). The orchestrator removes +the work folder with a commit and pushes; no PR is opened, the branch stays without one, and the user is +told so. + +## Loop limits + +- **Plan challenge (step 2):** exactly one Devil's Advocate round, no veto. The Lead answers every + objection (accept and revise, or reject with a reason); a rejected objection is not raised again. +- **Plan ↔ Security (steps 2–3):** at most 2 rejections. After the 2nd `CHANGES_REQUIRED` the Lead + decides: narrow the scope, split into separate issues, or escalate to the Product Manager. +- **Review ↔ Dev (steps 6–8):** review pass 1 is a full review; at most **2 further fix-and-review + rounds**, each reviewing only the delta. Blocking findings still open after that go to the Lead, who + decides: accept with justification, split into a follow-up issue, abort, or escalate. +- **Existing tests affected by a signature change:** when the plan changes a constructor or another + signature that existing test code calls (a test factory or helper), the plan lists those call sites and + who adapts them. If the old signature is removed or changed incompatibly, the existing tests stop + building as soon as the skeleton exists, so the **Dev** adapts exactly the listed call sites in step 4 + (*Skeleton*) — mechanically, to the new signature, without touching an assertion — so *Build* passes + before the Tester starts. If old and new signature coexist (e.g. an added overload), nothing breaks, and + the **Tester** moves the listed call sites to the new signature in step 5 where the plan asks for it. + This is the only case in which the Dev edits test code; the Tester checks the Dev's edit of those call + sites in its coverage step (only those sites, no assertion weakened). +- **Dev ↔ Tester disagreements:** if the Dev believes a step-5 test is wrong, the Lead decides (the test + is not changed silently). Not counted against a loop limit. +- **Code check needs a structural change** (or breaks build/tests): the Code Officer's edit is reverted + and the item goes to the Dev (or Tester for tests), then the code check runs again. Not counted + against a loop limit. +- **Coverage below 80 %:** Dev and Tester iterate; lines that cannot be covered by a unit test go to the + Lead, whose decision is recorded in `log.md`. +- **After the PR (step 11):** CI or quality-gate failures and review comments are fixed on the same + branch and go through steps 7–8 again (delta review). The same limit of 2 fix rounds applies per + failure; after that the Lead decides. + +## Escalation to the Product Manager + +The Lead escalates only when it cannot decide responsibly on its own: the requirement is ambiguous, the +fix needs a product decision (behavior change visible to users, breaking a documented guarantee in +`docs/ARCHITECTURE.md` or an accepted decision record), or a loop limit was hit and none of the Lead's +options is clearly right. The escalation is one concise question with the options and the Lead's +recommendation. Follow-up GitHub issues the Lead decides on are created by the orchestrator and listed in +the PR under Next Steps. + +## Non-blocking findings + +Fixed in the same change or opened as a linked GitHub issue now (Lead decides which) — never deferred to +"a later change". diff --git a/.squad/stack.md b/.squad/stack.md new file mode 100644 index 0000000..ead3d9a --- /dev/null +++ b/.squad/stack.md @@ -0,0 +1,101 @@ +# Stack: .NET + +The toolchain and the exact commands of this repository. Every squad member, skill and instruction file +refers to the entries below by their *italic name*. Seeded from the Squad-Spec-Repository-Template `dotnet` +profile and owned by this repository: keep it true when the build changes. + +## Toolchain + +- .NET SDK `10.0.x` (target framework `net10.0`), solution `DockerUpdateGuard.slnx`. +- Formatter: `reihitsu-format` (`dotnet tool install -g Reihitsu.Cli`), on the same release line as the + **Reihitsu.Analyzer** version pinned in `Directory.Packages.props`; otherwise the formatter can revert + code the analyzer considers correct. +- Analyzers: **SonarAnalyzer.CSharp** in every project via `Directory.Build.props` (the Sonar C# rules locally, + so SonarQube issues surface before the push), **Reihitsu.Analyzer** referenced by each project file, both + configured by the rulesets in `rules/`; the MSTest analyzers come with the MSTest package in the test projects. +- The SessionStart hook `.claude/hooks/session-start.sh` installs the formatter, sets `DOTNET_ROOT` and + restores the solution in remote sessions. + +## Layout + +- Production code: `src/` — `DockerUpdateGuard` (host, composition root, UI, scan engine), + `DockerUpdateGuard.Data` (EF Core / PostgreSQL), `DockerUpdateGuard.Telemetry` (OpenTelemetry); + `src/GlobalSuppressions.cs` holds the centralized suppressions, `rules/` the per-configuration rulesets. +- Tests: `src/Tests/DockerUpdateGuard.Tests/` (host/application layer) and + `src/Tests/DockerUpdateGuard.Data.Tests/` (data layer), one test class per type under test in + `{TypeUnderTest}Tests.cs` (or `…RenderTests` / `…PersistentStateTests` when split, `docs/UNIT_TESTS.md`). + +## Commands + +| Name | Command | +| ---- | ------- | +| *Restore* | `dotnet restore DockerUpdateGuard.slnx` | +| *Format* (Code Officer only in the squad) | `reihitsu-format --force ./` | +| *Format check* | `reihitsu-format --check ./` | +| *Build* | `dotnet build DockerUpdateGuard.slnx -c Release --no-restore` | +| *Test* | `dotnet test DockerUpdateGuard.slnx -c Release --no-build` | +| *Single test* | `dotnet test src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuard.Tests.csproj --filter "FullyQualifiedName~ClassName.MethodName"` | +| *Test with coverage* | `dotnet test DockerUpdateGuard.slnx -c Release --no-build --collect:"XPlat Code Coverage" --results-directory ./TestResults` | +| *Coverage gate* | `python3 .squad/tools/coverage-check.py` | +| *Analyzer gate* | `python3 .squad/tools/analyzer-check.py` | + +## Analyzer gate + +`analyzer-check.py` runs a full, non-incremental Release build with a SARIF error log per project and +reports every non-suppressed Roslyn diagnostic — `RH####`, `S####`, `MSTEST####`, `CA####`, at **any +severity**, including info-level ones that never appear as build warnings but that SonarQube Cloud +imports — located in a file changed since the merge base with `origin/main`. Its solution comes from +`.squad/tools/squad_settings.py`. In addition the build must show **zero `RH####` warnings and errors** +anywhere. Do not grep console build output instead: an incremental build prints no warnings at all. + +Easy to get wrong by hand: `#region` blocks on every type, XML documentation on every member, no +underscores in member names, a region for an interface implementation named after the interface. + +SonarQube Cloud's own quality profile can still report `S####` rules the local default profile does not, +and its non-Roslyn checks (duplication, hotspots, taint analysis) only run in CI — such findings arrive in +squad step 11. + +## Writing code + +- File-scoped namespaces; one top-level type per file; `using` outside the namespace (System first). +- Wrap every type's members in `#region` blocks **as you write the code**, grouped by member kind + (`Constants`, `Fields`, `Constructors`, `Properties`, `Events`, `Methods`, …); a region that groups an + interface implementation is named after the interface (e.g. `#region IParser`) and its description + does not end with the word "implementation". +- XML documentation on all members (English, no `<remarks>`). +- Nullable reference types, `var`, language keywords over BCL types, LINQ method syntax only, + `== false` instead of `!`, `is null` / `is not null`, no primary constructors, constructor injection + with `_camelCase` readonly fields, `.ConfigureAwait(false)` in library/service code. +- Guards an analyzer asks for (e.g. `if (_logger.IsEnabled(...))` for CA1873) are written by the Dev, + not added later by the Code Officer. + +## Writing tests + +See `docs/UNIT_TESTS.md` — MSTest with **NSubstitute** for collaborators that cross an infrastructure boundary, +EF Core InMemory / SQLite for data tests. The rules the MSTest and Sonar analyzers enforce and the Code Officer cannot fix +without handing back: pass `TestContext.CancellationToken` to every call that accepts a token +(MSTEST0049 / S8949), and use the specific `Assert` member instead of `Assert.IsTrue(...)` or +`StringAssert` (MSTEST0037 / MSTEST0046). + +## Skeleton + +New members with their full signature, XML docs and `#region` blocks, bodies +`throw new NotImplementedException();`, so the solution builds and the tests compile and fail. + +## Dependencies + +NuGet packages through **Central Package Management**: the version goes into `Directory.Packages.props`, +never into a `.csproj`. SonarAnalyzer.CSharp is referenced for every project in `Directory.Build.props`. + +## Concurrency + +Builds and test runs share `bin/` and `obj/`; two at once break each other. `analyzer-check.py` serializes +itself with a lock (`obj/analyzer-check.lock`), plain builds do not. + +## Known pitfalls + +- `reihitsu-format` fails with ".NET location: Not found" when `DOTNET_ROOT` is unset — the SessionStart + hook sets it; otherwise prefix the command with + `DOTNET_ROOT="$(dirname "$(readlink -f "$(command -v dotnet)")")"`. +- `reihitsu-format` asks for confirmation for more than 25 files; `--force` skips the prompt, which a + non-interactive session cannot answer. diff --git a/.squad/team.md b/.squad/team.md new file mode 100644 index 0000000..e50dee2 --- /dev/null +++ b/.squad/team.md @@ -0,0 +1,47 @@ +# Team + +Squad for this repository, used for both GitHub issues (`squad-issue` skill) and new features +(`squad-spec` skill). The layout follows [bradygaster/squad](https://github.com/bradygaster/squad) +(`team.md`, `routing.md`, `decisions.md`, `agents/{name}/charter.md` + `history.md`, all changed only in +squad-maintenance PRs); in Claude Code the roles run as subagents under `.claude/agents/`, driven by the +invoking session (the orchestrator). An agent without subagent support runs each role inline by following +the same agent file. + +The squad files are stack-neutral and come from the Squad-Spec-Repository-Template repository. Everything +specific to this repository lives in two files the members read first: + +- [`stack.md`](stack.md) — toolchain, layout and the exact commands: format, build, test, coverage, the + analyzer gate, how to write code and tests that pass it, and how to build a compile-only skeleton. +- [`project.md`](project.md) — what this project guarantees: the security areas that decide the + `security` tier, deliberate guarantees, the integration surface the Reviewer sweeps, and the test doubles. + +## Members + +| Role | Charter | Claude subagent | Model | Writes | +| ---------------- | --------------------------------------------- | ----------------------- | ------ | ------------------------------------------------------- | +| Lead | [charter](agents/lead/charter.md) | `squad-lead` | Opus | plans, decisions | +| Devil's Advocate | [charter](agents/devils-advocate/charter.md) | `squad-devils-advocate` | Opus | nothing (read-only) | +| Security | [charter](agents/security/charter.md) | `squad-security` | Opus | nothing (read-only) | +| Tester | [charter](agents/tester/charter.md) | `squad-tester` | Sonnet | test code | +| Dev | [charter](agents/dev/charter.md) | `squad-dev` | Sonnet | production code | +| Code Officer | [charter](agents/code-officer/charter.md) | `squad-code-officer` | Sonnet | production and test code (format, analyzer and style fixes only) | +| Reviewer | [charter](agents/reviewer/charter.md) | `squad-reviewer` | Opus | nothing (read-only) | +| Product Manager | — | the human user | — | answers escalations | + +Where production and test code live is defined in `stack.md` (*Layout*). + +The **Lead** decides everything inside the squad, including approving plans and approving the pull +request. The **Product Manager** is only involved when the Lead escalates: an unclear requirement, a +product decision that cannot be derived from the issue or the existing documentation, or a deadlock the +Lead cannot resolve. + +## Shared rules (apply to every member) + +`CLAUDE.md`, `docs/ARCHITECTURE.md`, `docs/CONTRIBUTING.md`, `docs/UNIT_TESTS.md`, `.squad/stack.md` and +`.squad/project.md` are binding: unit tests for all new code with at least 80 % line coverage on +new/changed production code, the code conventions from `stack.md` while writing, English for everything +that ends up in the repository or on GitHub. Inside the squad, only the Code Officer runs the formatter +(*Format* in `stack.md`) and owns a clean analyzer gate. Subagents never run Git write operations, except +creating and removing a scratch `git worktree` for experiments (`.squad/routing.md`, *Concurrency*); the +orchestrator commits and pushes to the work branch at any time (see `CLAUDE.md`, golden rules) and opens +the pull request only after the Lead's approval. diff --git a/.squad/template.json b/.squad/template.json new file mode 100644 index 0000000..c35f5e2 --- /dev/null +++ b/.squad/template.json @@ -0,0 +1,5 @@ +{ + "repository": "LarsLaskowski/Squad-Spec-Repository-Template", + "commit": "cf659f2de8fe0a5332448a587e0fa2f789321095", + "profile": "dotnet" +} diff --git a/.squad/tools/.gitignore b/.squad/tools/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.squad/tools/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/.squad/tools/analyzer-check.py b/.squad/tools/analyzer-check.py new file mode 100644 index 0000000..afb85dd --- /dev/null +++ b/.squad/tools/analyzer-check.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Analyzer gate: every Roslyn diagnostic in a changed file, at any severity, as SonarQube Cloud sees it. + +SonarQube Cloud imports *all* Roslyn diagnostics from the build's SARIF error log - including info-level +ones such as the MSTest analyzer rules (MSTEST####), which never appear as build warnings. A console +build therefore hides issues that the quality gate later reports. This script runs a full, non-incremental +Release build with an SARIF error log per project and reports every non-suppressed diagnostic (RH####, +S####, MSTEST####, CA####, ...) located in a file changed since the merge base with origin/main +(working tree and untracked files included). + +The build command (solution from `.squad/tools/squad_settings.py`), the base (origin/main) and the log +location are fixed on purpose: the script takes no arguments, so nothing user-supplied reaches the shell, +git or the filesystem. + +Usage, from the repository root (after *Restore* from `.squad/stack.md`): + python3 .squad/tools/analyzer-check.py + +Runs are serialized with a lock file (obj/analyzer-check.lock), because two concurrent full builds of the +solution break each other. The check fails if the build fails or if any project produced no SARIF log. + +Exit code 0 when no changed file has a diagnostic, 1 otherwise. +""" +import fcntl +import glob +import json +import os +import subprocess +import sys +from urllib.parse import unquote, urlparse + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import squad_settings as settings # noqa: E402 (per-repository settings next to this script) + +BASE_REF = "origin/main" +SARIF_NAME = os.path.join("obj", "roslyn.sarif") +LOCK_FILE = os.path.join("obj", "analyzer-check.lock") +BUILD = ["dotnet", "build", settings.SOLUTION, "-c", "Release", "--no-restore", "--no-incremental", + "-p:ErrorLog=" + SARIF_NAME + "%2Cversion=2.1"] + + +def git(*args): + return subprocess.run(["git", *args], capture_output=True, text=True, check=True).stdout + + +def changed_files(): + merge_base = git("merge-base", BASE_REF, "HEAD").strip() + names = git("diff", "--name-only", merge_base).splitlines() + names += git("ls-files", "--others", "--exclude-standard").splitlines() + return {name.strip() for name in names if name.strip()} + + +def to_repo_path(uri, root): + parsed = urlparse(uri) + path = unquote(parsed.path) if parsed.scheme == "file" else unquote(uri) + return os.path.relpath(os.path.realpath(path), root).replace(os.sep, "/") + + +def diagnostics(root): + for log in glob.glob(os.path.join("**", SARIF_NAME), recursive=True): + with open(log, encoding="utf-8-sig") as handle: + data = json.load(handle) + for run in data.get("runs", []): + for result in run.get("results", []): + if result.get("suppressions"): + continue + locations = result.get("locations") or [] + if not locations: + continue + physical = locations[0].get("physicalLocation", {}) + path = to_repo_path(physical.get("artifactLocation", {}).get("uri", ""), root) + line = physical.get("region", {}).get("startLine", 0) + yield path, line, result.get("ruleId", "?"), result.get("level", "warning"), \ + result.get("message", {}).get("text", "") + + +def project_dirs(): + return sorted(os.path.dirname(project) for project in glob.glob(os.path.join("**", "*.csproj"), recursive=True) + if os.sep + "bin" + os.sep not in project and os.sep + "obj" + os.sep not in project) + + +def main(): + os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")) + os.makedirs("obj", exist_ok=True) + with open(LOCK_FILE, "w", encoding="utf-8") as lock: + fcntl.flock(lock, fcntl.LOCK_EX) + return run_check() + + +def run_check(): + root = os.path.realpath(os.getcwd()) + for stale in glob.glob(os.path.join("**", SARIF_NAME), recursive=True): + os.remove(stale) + build = subprocess.run(BUILD, capture_output=True, text=True, check=False) + if build.returncode != 0: + print(build.stdout[-4000:]) + print("Build failed.") + return 1 + missing = [d for d in project_dirs() if not os.path.isfile(os.path.join(d, SARIF_NAME))] + if missing: + print("No SARIF log produced for: " + ", ".join(missing)) + print("FAIL") + return 1 + + changed = changed_files() + found = sorted(set(diagnostics(root))) + in_changed = [d for d in found if d[0] in changed] + elsewhere = len(found) - len(in_changed) + + for path, line, rule, level, message in in_changed: + print(f"{path}({line}): {level} {rule}: {message}") + print(f"\nDiagnostics in changed files: {len(in_changed)}") + print(f"Diagnostics in unchanged files (not gating): {elsewhere}") + print("PASS" if not in_changed else "FAIL") + return 0 if not in_changed else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.squad/tools/config-check.py b/.squad/tools/config-check.py new file mode 100644 index 0000000..961d174 --- /dev/null +++ b/.squad/tools/config-check.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Config gate for the squad: agent and skill definitions must load, mirrors must match, and the +per-repository squad files must exist and be filled in. + +Claude Code silently drops an agent or skill whose YAML front matter does not parse (for example an +unquoted description containing ": "), so a broken file only shows up when a squad run tries to launch +it. This script checks, without arguments: + +- every `.claude/agents/*.md` and every `SKILL.md` under `.claude/skills/`, `.agents/skills/` and + `.github/skills/` has front matter that parses as YAML, with a non-empty `name` and `description`; +- an agent's `name` equals its file name, a skill's `name` equals its folder name; +- the three skill folders contain the same skills with identical content; +- `CLAUDE.md`, `AGENTS.md` and `.github/copilot-instructions.md` are identical from their first `## ` + heading on (only the title and introduction may differ); +- `.squad/template.json` names the template repository (where lessons about template-managed files are + filed); +- `.squad/stack.md`, `.squad/project.md` and `.squad/tools/squad_settings.py` exist, and no file the + template seeded or rebuilt still contains a template placeholder (`{{TODO: …}}` — a marker that + ordinary Go templates, `docker --format` strings or GitHub Actions expressions never contain). + +Usage, from anywhere inside the repository: + python3 .squad/tools/config-check.py + +Exit code 0 when everything is valid, 1 otherwise. Requires PyYAML (`pip install pyyaml`). +""" +import glob +import json +import os +import re +import sys + +CLAUDE_DIR = ".claude" +GITHUB_DIR = ".github" +SQUAD_DIR = ".squad" +AGENTS_DIR = os.path.join(CLAUDE_DIR, "agents") +SKILL_ROOTS = [os.path.join(CLAUDE_DIR, "skills"), os.path.join(".agents", "skills"), os.path.join(GITHUB_DIR, "skills")] +INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md", os.path.join(GITHUB_DIR, "copilot-instructions.md")] +REQUIRED_FILES = [os.path.join(SQUAD_DIR, "stack.md"), os.path.join(SQUAD_DIR, "project.md"), + os.path.join(SQUAD_DIR, "tools", "squad_settings.py")] +# No quantifier overlaps another one, so matching stays linear (no backtracking). +PLACEHOLDER = re.compile(r"\{\{TODO:([^{}]*)\}\}") +PLACEHOLDER_GLOBS = INSTRUCTION_FILES + REQUIRED_FILES + [ + "SECURITY.md", "sonar-project.properties", + os.path.join(SQUAD_DIR, "**", "*.md"), os.path.join("docs", "**", "*.md"), + os.path.join(GITHUB_DIR, "**", "*.md"), os.path.join(GITHUB_DIR, "**", "*.yml"), +] + +try: + import yaml +except ImportError: + sys.exit("PyYAML is required: pip install pyyaml") + + +def read_text(path): + with open(path, encoding="utf-8-sig") as handle: + return handle.read().replace("\r\n", "\n") + + +def front_matter(path): + text = read_text(path) + if not text.startswith("---\n"): + raise ValueError("no front matter") + end = text.find("\n---", 4) + if end < 0: + raise ValueError("unterminated front matter") + data = yaml.safe_load(text[4:end]) + if not isinstance(data, dict): + raise ValueError("front matter is not a mapping") + return data + + +def check(path, expected_name, errors): + try: + data = front_matter(path) + except (ValueError, yaml.YAMLError) as error: + errors.append(f"{path}: {str(error).splitlines()[0]}") + return + for key in ("name", "description"): + if not str(data.get(key) or "").strip(): + errors.append(f"{path}: missing '{key}'") + if data.get("name") and data["name"] != expected_name: + errors.append(f"{path}: name '{data['name']}' does not match '{expected_name}'") + + +def check_skills(errors): + skills = {} + for root in SKILL_ROOTS: + found = {} + for path in sorted(glob.glob(os.path.join(root, "*", "SKILL.md"))): + name = os.path.basename(os.path.dirname(path)) + check(path, name, errors) + with open(path, "rb") as handle: + found[name] = handle.read().replace(b"\r\n", b"\n") + skills[root] = found + reference = skills[SKILL_ROOTS[0]] + for root in SKILL_ROOTS[1:]: + other = skills[root] + for name in sorted(set(reference) | set(other)): + if name not in reference or name not in other: + errors.append(f"skill '{name}' exists in only one of {SKILL_ROOTS[0]} and {root}") + elif reference[name] != other[name]: + errors.append(f"skill '{name}' differs between {SKILL_ROOTS[0]} and {root}") + return reference + + +def body(path): + text = read_text(path) + start = text.find("\n## ") + return text[start:] if start >= 0 else "" + + +def check_instructions(errors): + missing = [path for path in INSTRUCTION_FILES if not os.path.isfile(path)] + for path in missing: + errors.append(f"{path} is missing") + present = [path for path in INSTRUCTION_FILES if path not in missing] + if len(present) < 2: + return + reference = body(present[0]) + for path in present[1:]: + if body(path) != reference: + errors.append(f"{path} differs from {present[0]} after the first '## ' heading") + + +def check_template_record(errors): + path = os.path.join(SQUAD_DIR, "template.json") + try: + with open(path, encoding="utf-8") as handle: + record = json.load(handle) + except (OSError, ValueError) as error: + errors.append(f"{path}: {error} (written by adopt-template)") + return + if not isinstance(record, dict) or not str(record.get("repository") or "").strip(): + errors.append(f"{path}: no 'repository' - refresh the squad with adopt-template") + + +def check_project_files(errors): + for path in REQUIRED_FILES: + if not os.path.isfile(path): + errors.append(f"{path} is missing (seeded by adopt-template)") + paths = sorted({p for pattern in PLACEHOLDER_GLOBS for p in glob.glob(pattern, recursive=True) + if os.path.isfile(p)}) + for path in paths: + text = read_text(path) + for match in PLACEHOLDER.finditer(text): + line = text.count("\n", 0, match.start()) + 1 + first = " ".join(match.group(1).split())[:60] + errors.append(f"{path}:{line}: template placeholder '{{{{TODO: {first}}}}}' not filled in") + + +def main(): + # Resolve paths from the repository root, whatever the current directory is. + os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")) + errors = [] + agents = sorted(glob.glob(os.path.join(AGENTS_DIR, "*.md"))) + for path in agents: + check(path, os.path.splitext(os.path.basename(path))[0], errors) + skills = check_skills(errors) + check_instructions(errors) + check_template_record(errors) + check_project_files(errors) + + if not agents or not skills: + errors.append("no agents or skills found - has the squad been adopted in this repository?") + for error in errors: + print(error) + print(f"\nChecked {len(agents)} agents and {len(skills)} skills: {'PASS' if not errors else 'FAIL'}") + return 0 if not errors else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.squad/tools/coverage-check.py b/.squad/tools/coverage-check.py new file mode 100644 index 0000000..1c7ae57 --- /dev/null +++ b/.squad/tools/coverage-check.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +"""Coverage gate for the squad: line coverage of new/changed production lines (like SonarQube's +"coverage on new code") and overall line coverage, merged from the coverage reports of the latest test run (one per test project). + +Supported report formats (set COVERAGE_FORMAT in `.squad/tools/squad_settings.py`): +- `cobertura` — e.g. coverlet's `coverage.cobertura.xml` (.NET), or any Cobertura XML +- `lcov` — `lcov.info` (Node: c8, Node's test runner, Jest, Vitest, ...) +- `go` — a `go test -coverprofile` file + +The base (origin/main), the report location and the production-code paths are fixed in +`squad_settings.py`, not taken from the command line, so nothing user-supplied reaches git or the +filesystem. + +Usage, from the repository root, after *Test with coverage* from `.squad/stack.md`: + python3 .squad/tools/coverage-check.py [--threshold 80] + +Exit code 0 when both values reach the threshold, 1 otherwise. +""" +import argparse +import glob +import os +import re +import subprocess +import sys +import xml.etree.ElementTree as ET + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import squad_settings as settings # noqa: E402 (per-repository settings next to this script) + +BASE_REF = "origin/main" + + +def repo_path(path): + """Normalize a report path to a repository-relative path with forward slashes.""" + return os.path.relpath(os.path.abspath(path)).replace(os.sep, "/") + + +def changed_lines(): + """Return {repo-relative path: set(line numbers)} of lines added or changed since the merge base with + origin/main (working tree included) in the production paths from squad_settings.""" + merge_base = subprocess.run( + ["git", "merge-base", BASE_REF, "HEAD"], capture_output=True, text=True, check=True).stdout.strip() + pathspecs = list(settings.COVERAGE_PATHSPECS) + [f":(exclude){p}" for p in settings.COVERAGE_EXCLUDES] + diff = subprocess.run( + ["git", "diff", "-U0", merge_base, "--", *pathspecs], + capture_output=True, text=True, check=True).stdout + result, current = {}, None + for line in diff.splitlines(): + if line.startswith("+++ "): + path = line[4:] + current = path[2:] if path.startswith("b/") else None + if current: + result.setdefault(current, set()) + elif line.startswith("@@") and current: + match = re.search(r"\+(\d+)(?:,(\d+))?", line) + start, count = int(match.group(1)), int(match.group(2) or "1") + result[current].update(range(start, start + count)) + return result + + +# Reports written within this many seconds of the newest one belong to the same test run (one report per +# test project); older ones are leftovers of earlier runs and are ignored. +RUN_WINDOW_SECONDS = 600 + + +def latest_run_reports(): + reports = glob.glob(settings.COVERAGE_REPORT_GLOB, recursive=True) + if not reports: + sys.exit(f"No coverage report matches {settings.COVERAGE_REPORT_GLOB} - run *Test with coverage* first") + newest = max(os.path.getmtime(report) for report in reports) + return sorted(report for report in reports if newest - os.path.getmtime(report) <= RUN_WINDOW_SECONDS) + + +def add_hit(hits, path, number, count): + lines = hits.setdefault(path, {}) + lines[number] = max(lines.get(number, 0), count) + + +def load_cobertura(report): + # Local report from the repo's own test run; the tool stays stdlib-only, so no defusedxml. + root = ET.parse(report).getroot() # noqa: S314 # nosec B314 + sources = [s.text.rstrip("/\\") for s in root.iter("source") if s.text] + hits = {} + for cls in root.iter("class"): + filename = cls.get("filename") + for src in sources: + candidate = os.path.join(src, filename) + if os.path.exists(candidate): + filename = candidate + break + path = repo_path(filename) + for ln in cls.iter("line"): + add_hit(hits, path, int(ln.get("number")), int(ln.get("hits"))) + return hits + + +def load_lcov(report): + hits, path = {}, None + with open(report, encoding="utf-8") as handle: + for raw in handle: + line = raw.strip() + if line.startswith("SF:"): + path = repo_path(line[3:]) + elif line.startswith("DA:") and path: + number, count = line[3:].split(",")[:2] + add_hit(hits, path, int(number), int(float(count))) + elif line == "end_of_record": + path = None + return hits + + +def go_module(): + with open("go.mod", encoding="utf-8") as handle: + for line in handle: + if line.startswith("module "): + return line.split()[1].strip() + sys.exit("go.mod has no module line") + + +def load_go(report): + module, hits = go_module(), {} + block = re.compile(r"^(.+):(\d+)\.\d+,(\d+)\.\d+ (\d+) (\d+)$") + with open(report, encoding="utf-8") as handle: + for raw in handle: + match = block.match(raw.strip()) + if not match: + continue + name, start, end, statements, count = match.groups() + if int(statements) == 0: + continue + path = name[len(module) + 1:] if name.startswith(module + "/") else name + for number in range(int(start), int(end) + 1): + add_hit(hits, path, number, int(count)) + return hits + + +LOADERS = {"cobertura": load_cobertura, "lcov": load_lcov, "go": load_go} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--threshold", type=float, default=80.0) + args = parser.parse_args() + + os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")) + loader = LOADERS.get(settings.COVERAGE_FORMAT) + if loader is None: + sys.exit(f"Unknown COVERAGE_FORMAT '{settings.COVERAGE_FORMAT}' in squad_settings.py") + hits = {} + for report in latest_run_reports(): + for path, lines in loader(report).items(): + for number, count in lines.items(): + add_hit(hits, path, number, count) + + tracked = set(subprocess.run( + ["git", "ls-files", "--", *settings.COVERAGE_PATHSPECS, + *[f":(exclude){p}" for p in settings.COVERAGE_EXCLUDES]], + capture_output=True, text=True, check=True).stdout.splitlines()) + production = {path: lines for path, lines in hits.items() if path in tracked} + total = sum(len(lines) for lines in production.values()) + total_hit = sum(1 for lines in production.values() for count in lines.values() if count > 0) + overall = total_hit / total * 100 if total else 100.0 + + covered = coverable = 0 + print("Changed production files (coverable changed lines):") + for path, lines in sorted(changed_lines().items()): + file_hits = hits.get(path, {}) + relevant = [n for n in lines if n in file_hits] + hit = sum(1 for n in relevant if file_hits[n] > 0) + covered, coverable = covered + hit, coverable + len(relevant) + missed = sorted(n for n in relevant if file_hits[n] == 0) + rate = f"{hit / len(relevant) * 100:5.1f}%" if relevant else " n/a " + print(f" {rate} {hit}/{len(relevant)} {path}" + (f" uncovered: {missed}" if missed else "")) + + new_code = covered / coverable * 100 if coverable else 100.0 + print(f"\nNew/changed code: {new_code:.1f}% ({covered}/{coverable} lines)") + print(f"Overall: {overall:.1f}% ({total_hit}/{total} lines)") + ok = new_code >= args.threshold and overall >= args.threshold + print(f"Threshold {args.threshold:.0f}%: {'PASS' if ok else 'FAIL'}") + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.squad/tools/squad_settings.py b/.squad/tools/squad_settings.py new file mode 100644 index 0000000..9f34cf7 --- /dev/null +++ b/.squad/tools/squad_settings.py @@ -0,0 +1,11 @@ +"""Per-repository settings for the squad tools (.NET profile). Seeded once by adopt-template and kept on +later refreshes; the scripts that import it are template-managed.""" + +# Solution or project file the analyzer gate builds. +SOLUTION = "DockerUpdateGuard.slnx" + +# Coverage gate (.squad/tools/coverage-check.py) +COVERAGE_FORMAT = "cobertura" +COVERAGE_REPORT_GLOB = "TestResults/**/coverage.cobertura.xml" +COVERAGE_PATHSPECS = ["src/*.cs", "src/*.razor"] +COVERAGE_EXCLUDES = ["src/Tests/*", "src/DockerUpdateGuard.Data/Migrations/*"] # tests and generated EF Core migrations diff --git a/AGENTS.md b/AGENTS.md index dc45f2d..a4df4f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,97 +1,224 @@ -# DockerUpdateGuard — Codex Instructions - -This file describes project-specific conventions for DockerUpdateGuard. -Codex must follow these guidelines when working in this repository. - -The detailed C# code-style rules (naming, regions, formatting, XML docs, -null handling, suppressed analyzer rules) are imported below and are -**binding**: - -@.github/instructions/csharp.instructions.md - -## Architecture - -[ARCHITECTURE.md](ARCHITECTURE.md) is the binding architecture reference for -this repository: solution layout, composition root and startup sequence, -configuration model, data layer, integration clients, the background scan -engine, the UI layer, telemetry, security posture, and the CI/deployment -pipeline. Read it before making structural changes (new projects, new -background jobs, new integration clients, changes to the composition root -or entity model) and update it in the same change whenever it goes out of -date — it must never contain open questions or stale claims. - -## Git workflow - -- Never run `git commit` or `git push` without explicit user approval. -- Read-only Git commands are fine. +# AGENTS.md + +Project guidance for Codex/GPT and other agents that read `AGENTS.md` when working in this repository. These rules mirror `CLAUDE.md` and `.github/copilot-instructions.md`; keep all three in sync — +everything from the first `##` heading on is identical in all three files. This file is a summary; the +binding, detailed references are [`ARCHITECTURE.md`](/docs/ARCHITECTURE.md) (how the system is put +together and why), [`CONTRIBUTING.md`](/docs/CONTRIBUTING.md) (workflow, PR conventions, versioning), +[`UNIT_TESTS.md`](/docs/UNIT_TESTS.md) (test conventions — **unit tests are mandatory for new code**) and +[`.squad/stack.md`](/.squad/stack.md) (toolchain and commands). Read them before making a non-trivial +change; when this file and one of them appear to disagree, treat that as a sync bug to fix, not as +license to pick either one. + +## What this project is + +<!-- project:begin overview --> +DockerUpdateGuard is an ASP.NET Core Razor Components (Blazor Server) web application that tracks what is +actually running in Docker, compares it with registry metadata, and shows where updates, vulnerabilities and +shared base-image dependencies need attention. It runs as a Docker container against PostgreSQL. + +[`ARCHITECTURE.md`](/docs/ARCHITECTURE.md) is the binding architecture reference: solution layout, composition +root and startup sequence, configuration model, data layer, integration clients, the background scan engine, +the UI layer, telemetry, security posture, and the CI/deployment pipeline. Read it before making structural +changes (new projects, new background jobs, new integration clients, changes to the composition root or entity +model) and update it in the same change whenever it goes out of date — it must never contain open questions +or stale claims. +<!-- project:end overview --> + +## Golden rules + +- **Never commit or push to `main`** — no one, not even with approval. Every change goes through a + separate branch and a pull request. +- **Commits and pushes to a feature branch are always allowed** without asking: commit finished work and + push it to the current feature branch (creating that branch off `main` if needed), so nothing is lost + when a session ends. Force-pushing or otherwise rewriting published history, deleting branches, and + creating tags (a `v*` tag may trigger a release) still need explicit user approval. +- **Pull requests are only opened by the squad or by the user.** The `squad-issue` and `squad-spec` + skills open a PR after the Lead's approval (tier `docs`: after a clean review); outside the squad, a PR + is opened only when the user explicitly asks for one (e.g. by running the `create-pr` skill). Never open + a PR on your own initiative. +- Run *Format* from [`.squad/stack.md`](/.squad/stack.md) after editing code and before building; CI is + not meant to find formatting issues. In the squad skills only the Code Officer runs it. +- A changed file may not carry **any analyzer diagnostic of any severity**, including info-level ones that + never show up as build warnings but that the CI code analysis (e.g. SonarQube Cloud) reports. Check with + the *Analyzer gate* from `.squad/stack.md` and fix every finding before considering the work done (in + the squad skills, the Code Officer owns this). +- New or changed production code needs **at least 80 % line coverage**, and overall coverage must stay + at least 80 % (*Coverage gate* in `.squad/stack.md`, see [`UNIT_TESTS.md`](/docs/UNIT_TESTS.md#code-coverage)). +<!-- stack:begin golden-rules --> +- Add new packages via **Central Package Management** (`Directory.Packages.props`); do not put version + numbers in individual `.csproj` files. +- Every C# project uses the **Reihitsu.Analyzer** and the **SonarAnalyzer.CSharp** rules, so SonarQube + issues surface in the local build, not first in the CI analysis. A build must finish with **zero + Reihitsu (`RH####`) warnings and errors**. +- Wrap every type's members in `#region` blocks **as you write the code** — never leave a type + un-regioned and never add the regions only after an analyzer warning. +<!-- stack:end golden-rules --> ## Commit messages -- First line: one-line summary of no more than 80 characters. -- Do not end the subject line with a period. -- Do not write in the first person. -- Keep the body to a maximum of 3–5 sentences, depending on the number of changes. +- Keep the subject line to a single summary of **no more than 80 characters** and do not end it with a + period. +- Do not write the message in the first person. +- Keep the body to **3–5 sentences**, depending on the number of changes. ## Pull requests -- Write the PR title and description in English, regardless of the language used in the conversation. -- Do not mention Codex, Anthropic, or any AI assistant in the PR title or description. -- Do not include Codex session links, "Co-Authored-By: Codex" trailers, "Generated with Codex" notices, or any other reference indicating the PR was created with AI assistance. -- Follow the PR template in `.github/PULL_REQUEST_TEMPLATE.md`. Use its sections and do not add extra sections beyond it. -- Do not add a "Validation", "Verification", "Testing", or similar section that lists `reihitsu-format`, `dotnet build`, `dotnet test`, or other build/test commands. Build and tests run automatically as PR checks, so restating them in the description is unnecessary. - -## Build, test, and format +- Title and description are always written in **English**, regardless of the language used in the + conversation. -Use the solution file at the repository root (`DockerUpdateGuard.slnx`): +## Commands -- Restore: `dotnet restore DockerUpdateGuard.slnx` -- Format source: `reihitsu-format ./` -- Build: `dotnet build DockerUpdateGuard.slnx -c Release --no-restore` -- Run all tests: `dotnet test src\Tests\**\*.csproj -c Release --no-build --logger trx --collect:"XPlat Code Coverage"` -- Run one test project: `dotnet test src\Tests\DockerUpdateGuard.Tests\DockerUpdateGuard.Tests.csproj -c Release --no-build` -- Run one test method: `dotnet test src\Tests\DockerUpdateGuard.Tests\DockerUpdateGuard.Tests.csproj --filter "FullyQualifiedName~Namespace.ClassName.MethodName"` +<!-- stack:begin commands --> +Run from the repository root, where the solution file lives (exact commands, with the solution name, in +`.squad/stack.md`): -Run `reihitsu-format ./` after source changes and before building. The command -is a .NET tool installable with `dotnet tool install -g Reihitsu.Cli` if missing. +```bash +dotnet restore +reihitsu-format ./ # dotnet tool install -g Reihitsu.Cli +dotnet build -c Release --no-restore +dotnet test -c Release --no-build +python3 .squad/tools/analyzer-check.py # analyzer gate +python3 .squad/tools/coverage-check.py # coverage gate, after a coverage run +``` +<!-- stack:end commands --> -There is no separate lint command beyond formatting. Static analysis runs during -build via the configured rulesets and analyzers. +All commands, with what each one checks, are listed in [`.squad/stack.md`](/.squad/stack.md). -## High-level architecture - -| Path | Role | -| --- | --- | -| `src\DockerUpdateGuard` | Main ASP.NET Core host (`Microsoft.NET.Sdk.Web`); composition root; references the data and telemetry projects | -| `src\DockerUpdateGuard.Data` | Data-access layer; EF Core with PostgreSQL via `Npgsql.EntityFrameworkCore.PostgreSQL` | -| `src\DockerUpdateGuard.Telemetry` | Shared observability layer; OpenTelemetry hosting, OTLP export, ASP.NET Core / HTTP / runtime instrumentation | -| `src\Tests\DockerUpdateGuard.Tests` | Tests for the host/application layer; references the web project; EF Core InMemory + NSubstitute | -| `src\Tests\DockerUpdateGuard.Data.Tests` | Tests for the data layer; references the data project; EF Core SQLite | +## Architecture -Web startup and dependency wiring stay in the main host project; persistence -stays in `.Data`; observability stays in `.Telemetry`. For everything beyond -this table — composition root, background jobs, integration clients, data -model, UI, telemetry, security posture — see [ARCHITECTURE.md](ARCHITECTURE.md). +<!-- project:begin architecture --> +- `src/DockerUpdateGuard` — main ASP.NET Core host (`Microsoft.NET.Sdk.Web`); composition root; references the + data and telemetry projects. Web startup and dependency wiring stay here. +- `src/DockerUpdateGuard.Data` — data-access layer; EF Core with PostgreSQL (`Npgsql.EntityFrameworkCore.PostgreSQL`). +- `src/DockerUpdateGuard.Telemetry` — shared observability layer; OpenTelemetry hosting, OTLP export, ASP.NET + Core / HTTP / runtime instrumentation. +- `src/Tests/DockerUpdateGuard.Tests` — tests for the host/application layer; EF Core InMemory + NSubstitute. +- `src/Tests/DockerUpdateGuard.Data.Tests` — tests for the data layer; EF Core SQLite. +<!-- project:end architecture --> + +## Project configuration + +<!-- stack:begin configuration --> +- **Target framework** as set in the project files (see `.squad/stack.md`); **nullable reference types**, **implicit usings**, and + **documentation XML** generation are all enabled. +- **Central Package Management** via `Directory.Packages.props`; never put versions in individual + `.csproj` files. +- **Reihitsu.Analyzer** and **SonarAnalyzer.CSharp** are dev dependencies in every project (via + `Directory.Build.props`). +- **Solution format** is `.slnx` (XML-based) at the repository root. +<!-- stack:end configuration --> +<!-- project:begin configuration --> +- **Target framework** `net10.0` in the runtime projects; one solution, `DockerUpdateGuard.slnx`, at the + repository root. +- Runtime projects disable generated assembly info and link `SharedAssemblyInfo.cs` from the repository root. +- Runtime and test projects use per-configuration rulesets from `rules/DockerUpdateGuard.Debug.ruleset` and + `rules/DockerUpdateGuard.Release.ruleset`; rules SonarAnalyzer.CSharp disables by default (e.g. `S3776`) are + enabled there. +- SonarCloud rule suppressions are centralized in the shared `src/GlobalSuppressions.cs` (linked into each + project), never scattered per file. +- Tests live under `src/Tests`, not a top-level `tests` folder; keep new test projects there. +- EF Core migrations follow the SeriesOverwatch pattern: the first migration is `InitialCreate`, later ones + `Update1`, `Update2`, …; migration files are renamed to drop the timestamp prefix, while the generated + `[Migration("yyyyMMddHHmmss_Name")]` attribute stays unchanged. +<!-- project:end configuration --> + +## Code style + +<!-- stack:begin code-style --> +File-scoped namespaces; one top-level type per file; `using` outside namespace (System first); Allman +braces, always required; 4-space indent; `var` preferred; language keywords over BCL types; LINQ method +syntax only; `== false` instead of `!`; `is null` / `is not null`; no primary constructors; constructor +injection with `_camelCase` readonly fields; `#region` blocks grouped by member kind (an interface's +region named after the interface, its description not ending in "implementation"); XML docs on all +members (English, no `<remarks>`); `.ConfigureAwait(false)` in library/service code. +<!-- stack:end code-style --> +<!-- project:begin code-style --> +The detailed C# code-style rules (naming, regions, formatting, XML docs, null handling, suppressed analyzer +rules) in [`.github/instructions/csharp.instructions.md`](/.github/instructions/csharp.instructions.md) are +binding and take precedence over the summary above: -## Key conventions +@.github/instructions/csharp.instructions.md -- The repository uses the XML-based `.slnx` solution format. -- Runtime projects target `net10.0`, enable nullable reference types, implicit usings, and XML documentation files. -- Runtime projects disable generated assembly info and link `SharedAssemblyInfo.cs` from the repository root. -- Runtime and test projects use per-configuration rulesets from `rules\DockerUpdateGuard.Debug.ruleset` and `rules\DockerUpdateGuard.Release.ruleset`. -- `Reihitsu.Analyzer` is part of the standard project setup. -- `SonarAnalyzer.CSharp` is referenced from `Directory.Build.props` and therefore applies to every project, so SonarCloud findings surface at build time. Rules that the package disables by default (for example `S3776`) are enabled in the rulesets under `rules\`. -- Tests live under `src\Tests`, not a top-level `tests` folder. Keep new test projects there. -- The test stack is MSTest with `coverlet.collector`. -- Prefer MSTest's `Assert` and `CollectionAssert` APIs directly instead of FluentAssertions. -- Name test classes `{Feature}Tests` and test methods `{Class}{Scenario}{ExpectedResult}`. -- Always include assertion messages in tests. -- Centralize SonarCloud rule suppressions in the shared `src\GlobalSuppressions.cs` (linked into each project) rather than scattering per-file suppressions. - -## EF Core migrations - -If the project adds EF Core migrations, follow the SeriesOverwatch pattern: - -- first migration: `InitialCreate` -- later migrations: `Update1`, `Update2`, `Update3`, ... -- rename migration files to remove the timestamp prefix -- keep the generated `[Migration("yyyyMMddHHmmss_Name")]` attribute unchanged +- CRLF line endings and no final newline (`.editorconfig`). +- Tests: MSTest's `Assert` / `CollectionAssert` (no FluentAssertions); **NSubstitute** is the mocking library + here, used only where a collaborator crosses an infrastructure boundary; EF Core InMemory / SQLite for data + tests (`docs/UNIT_TESTS.md`). Test classes `{TypeUnderTest}Tests`, methods + `{TypeUnderTest}{Scenario}{ExpectedResult}`, always with assertion messages. +<!-- project:end code-style --> + +## Testing + +<!-- stack:begin testing --> +**Unit tests are mandatory for newly written code.** MSTest only (no FluentAssertions, no mocking +library — use real objects or the hand-written fakes/stubs). Classes `{TypeUnderTest}Tests`, methods +`{Class}{Scenario}{ExpectedResult}` in PascalCase **without underscores**; always pass an assert message. +<!-- stack:end testing --> +Full conventions, including the project's test doubles and the checklist to run before committing a new +test, are in [`UNIT_TESTS.md`](/docs/UNIT_TESTS.md). + +## Related skills + +Project-specific workflow skills live under `.claude/skills/`, mirrored identically under +`.agents/skills/` (Codex/GPT) and `.github/skills/` (GitHub Copilot): + +- `create-pr` — verify (format, build, tests, analyzer and coverage gates), review the change locally, + then open a PR following [`.github/pull_request_template.md`](/.github/pull_request_template.md). +- `squad-issue` — fix a GitHub issue with the squad: the Lead plans and picks a tier + (`docs` / `trivial` / `standard` / `security`), the Devil's Advocate challenges `standard`/`security` + plans once, Security reviews security-relevant plans, the Tester writes failing tests first, the Dev + implements to ≥ 80 % coverage, the Code Officer clears format and analyzer diagnostics, Reviewer and + Security review the diff, the Lead approves, then a PR referencing the issue is opened. +- `squad-spec` — the same squad pipeline for a new feature, planned as `spec.md`, `plan.md` and + `tasks.md` in a working folder under `specs/`. +- `review-pr` — review an open pull request against this project's stack, analyzer, security and + unit-test conventions, and post the findings with an explicit verdict. + +Review runs as a subagent defined in `.claude/agents/squad-reviewer.md` (read-only, pinned to Opus, fresh +context). `create-pr` and the squad skills call it *before* pushing, so a change is reviewed while it is +still local; `review-pr` calls the same agent for a pull request that is already open. The review +checklist, the integration-surface sweep, the blocking/non-blocking severity model and the "round 1 is a +full review, later rounds review only the delta" rule live in that one file, so they are identical either +way. An agent without subagent support follows the same file inline. + +The squad skills run a multi-role pipeline defined in [`.squad/`](/.squad/team.md) — Lead (plan, decisions, +PR approval), Devil's Advocate (one plan challenge), Security (plan and diff), Tester (tests first, +coverage), Dev, Code Officer (format, analyzers) and Reviewer — as subagents under +`.claude/agents/squad-*.md`, with the loop limits and escalation rules in +[`.squad/routing.md`](/.squad/routing.md). Stack commands live in [`.squad/stack.md`](/.squad/stack.md), +the project's guarantees, security areas and integration surface in +[`.squad/project.md`](/.squad/project.md). Their working records (`plan.md`, `log.md`, for features also +`spec.md` and `tasks.md`) live under `specs/` on the work branch only; before the PR they are posted as a +comment on the issue and removed, so `main` keeps no working records. An issue or feature PR never changes +the squad or these instructions (`.squad/` except `stack.md` and `project.md`, `.claude/`, +`.github/skills/`, `.agents/skills/`, `CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`): squad +lessons are filed as GitHub issues labelled `squad` and never fixed in a product PR. The squad and these +rules come from the template repository named in `.squad/template.json`: a lesson about a template-managed +file becomes an issue there and is rolled out with its `adopt-template` skill; a lesson about project +knowledge (`.squad/stack.md`, `.squad/project.md`, a project block) becomes an issue here and is worked in +a squad-maintenance PR checked with `python3 .squad/tools/config-check.py` (`.squad/routing.md`, +*Squad lessons*). The user acts as Product Manager +and is only asked when the Lead escalates. Pull requests are merged with *Squash and merge*, so only the +PR title and description reach `main`. + +The reasoning behind code decisions — why something was built the way it was — is recorded by the Lead +as one decision record per decision in [`docs/decisions/`](/docs/decisions/README.md) (append-only, +superseded rather than rewritten), not in `ARCHITECTURE.md`. Read the relevant records before changing +code they cover, and do not contradict an accepted record without superseding it. + +Two rules these skills enforce that are easy to get wrong: + +- **A pull request documents the change, not how it was produced.** The internal review loop — its + pass count, its findings, the commits that resolved them — never appears in the PR title, body or + commit messages. +- **A finding posted as a review comment gets worked in that pull request**, blocking or not. It is + never deferred to "the next change that touches this code": no such change is scheduled, and the + session holding the context to act on it will not exist later. If it really should not be fixed + here, reply with the reason or open a linked issue now — then resolve the thread. + +## Pull requests, contributing and architecture + +Follow [`CONTRIBUTING.md`](/docs/CONTRIBUTING.md) for branch/PR naming (`[area] Description`), the PR +checklist in [`.github/pull_request_template.md`](/.github/pull_request_template.md), and the +stability policy. Consult [`ARCHITECTURE.md`](/docs/ARCHITECTURE.md) before changing the behavior it +describes — the guarantees listed in [`.squad/project.md`](/.squad/project.md) are deliberate, not +incidental behavior. diff --git a/CLAUDE.md b/CLAUDE.md index 480caa4..9c8e4f3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,97 +1,224 @@ -# DockerUpdateGuard — Claude Instructions - -This file describes project-specific conventions for DockerUpdateGuard. -Claude must follow these guidelines when working in this repository. - -The detailed C# code-style rules (naming, regions, formatting, XML docs, -null handling, suppressed analyzer rules) are imported below and are -**binding**: - -@.github/instructions/csharp.instructions.md - -## Architecture - -[ARCHITECTURE.md](ARCHITECTURE.md) is the binding architecture reference for -this repository: solution layout, composition root and startup sequence, -configuration model, data layer, integration clients, the background scan -engine, the UI layer, telemetry, security posture, and the CI/deployment -pipeline. Read it before making structural changes (new projects, new -background jobs, new integration clients, changes to the composition root -or entity model) and update it in the same change whenever it goes out of -date — it must never contain open questions or stale claims. - -## Git workflow - -- Never run `git commit` or `git push` without explicit user approval. -- Read-only Git commands are fine. +# CLAUDE.md + +Project guidance for Claude when working in this repository. These rules mirror `AGENTS.md` and `.github/copilot-instructions.md`; keep all three in sync — +everything from the first `##` heading on is identical in all three files. This file is a summary; the +binding, detailed references are [`ARCHITECTURE.md`](/docs/ARCHITECTURE.md) (how the system is put +together and why), [`CONTRIBUTING.md`](/docs/CONTRIBUTING.md) (workflow, PR conventions, versioning), +[`UNIT_TESTS.md`](/docs/UNIT_TESTS.md) (test conventions — **unit tests are mandatory for new code**) and +[`.squad/stack.md`](/.squad/stack.md) (toolchain and commands). Read them before making a non-trivial +change; when this file and one of them appear to disagree, treat that as a sync bug to fix, not as +license to pick either one. + +## What this project is + +<!-- project:begin overview --> +DockerUpdateGuard is an ASP.NET Core Razor Components (Blazor Server) web application that tracks what is +actually running in Docker, compares it with registry metadata, and shows where updates, vulnerabilities and +shared base-image dependencies need attention. It runs as a Docker container against PostgreSQL. + +[`ARCHITECTURE.md`](/docs/ARCHITECTURE.md) is the binding architecture reference: solution layout, composition +root and startup sequence, configuration model, data layer, integration clients, the background scan engine, +the UI layer, telemetry, security posture, and the CI/deployment pipeline. Read it before making structural +changes (new projects, new background jobs, new integration clients, changes to the composition root or entity +model) and update it in the same change whenever it goes out of date — it must never contain open questions +or stale claims. +<!-- project:end overview --> + +## Golden rules + +- **Never commit or push to `main`** — no one, not even with approval. Every change goes through a + separate branch and a pull request. +- **Commits and pushes to a feature branch are always allowed** without asking: commit finished work and + push it to the current feature branch (creating that branch off `main` if needed), so nothing is lost + when a session ends. Force-pushing or otherwise rewriting published history, deleting branches, and + creating tags (a `v*` tag may trigger a release) still need explicit user approval. +- **Pull requests are only opened by the squad or by the user.** The `squad-issue` and `squad-spec` + skills open a PR after the Lead's approval (tier `docs`: after a clean review); outside the squad, a PR + is opened only when the user explicitly asks for one (e.g. by running the `create-pr` skill). Never open + a PR on your own initiative. +- Run *Format* from [`.squad/stack.md`](/.squad/stack.md) after editing code and before building; CI is + not meant to find formatting issues. In the squad skills only the Code Officer runs it. +- A changed file may not carry **any analyzer diagnostic of any severity**, including info-level ones that + never show up as build warnings but that the CI code analysis (e.g. SonarQube Cloud) reports. Check with + the *Analyzer gate* from `.squad/stack.md` and fix every finding before considering the work done (in + the squad skills, the Code Officer owns this). +- New or changed production code needs **at least 80 % line coverage**, and overall coverage must stay + at least 80 % (*Coverage gate* in `.squad/stack.md`, see [`UNIT_TESTS.md`](/docs/UNIT_TESTS.md#code-coverage)). +<!-- stack:begin golden-rules --> +- Add new packages via **Central Package Management** (`Directory.Packages.props`); do not put version + numbers in individual `.csproj` files. +- Every C# project uses the **Reihitsu.Analyzer** and the **SonarAnalyzer.CSharp** rules, so SonarQube + issues surface in the local build, not first in the CI analysis. A build must finish with **zero + Reihitsu (`RH####`) warnings and errors**. +- Wrap every type's members in `#region` blocks **as you write the code** — never leave a type + un-regioned and never add the regions only after an analyzer warning. +<!-- stack:end golden-rules --> ## Commit messages -- First line: one-line summary of no more than 80 characters. -- Do not end the subject line with a period. -- Do not write in the first person. -- Keep the body to a maximum of 3–5 sentences, depending on the number of changes. +- Keep the subject line to a single summary of **no more than 80 characters** and do not end it with a + period. +- Do not write the message in the first person. +- Keep the body to **3–5 sentences**, depending on the number of changes. ## Pull requests -- Write the PR title and description in English, regardless of the language used in the conversation. -- Do not mention Claude, Anthropic, or any AI assistant in the PR title or description. -- Do not include Claude session links, "Co-Authored-By: Claude" trailers, "Generated with Claude" notices, or any other reference indicating the PR was created with AI assistance. -- Follow the PR template in `.github/PULL_REQUEST_TEMPLATE.md`. Use its sections and do not add extra sections beyond it. -- Do not add a "Validation", "Verification", "Testing", or similar section that lists `reihitsu-format`, `dotnet build`, `dotnet test`, or other build/test commands. Build and tests run automatically as PR checks, so restating them in the description is unnecessary. - -## Build, test, and format +- Title and description are always written in **English**, regardless of the language used in the + conversation. -Use the solution file at the repository root (`DockerUpdateGuard.slnx`): +## Commands -- Restore: `dotnet restore DockerUpdateGuard.slnx` -- Format source: `reihitsu-format ./` -- Build: `dotnet build DockerUpdateGuard.slnx -c Release --no-restore` -- Run all tests: `dotnet test src\Tests\**\*.csproj -c Release --no-build --logger trx --collect:"XPlat Code Coverage"` -- Run one test project: `dotnet test src\Tests\DockerUpdateGuard.Tests\DockerUpdateGuard.Tests.csproj -c Release --no-build` -- Run one test method: `dotnet test src\Tests\DockerUpdateGuard.Tests\DockerUpdateGuard.Tests.csproj --filter "FullyQualifiedName~Namespace.ClassName.MethodName"` +<!-- stack:begin commands --> +Run from the repository root, where the solution file lives (exact commands, with the solution name, in +`.squad/stack.md`): -Run `reihitsu-format ./` after source changes and before building. The command -is a .NET tool installable with `dotnet tool install -g Reihitsu.Cli` if missing. +```bash +dotnet restore +reihitsu-format ./ # dotnet tool install -g Reihitsu.Cli +dotnet build -c Release --no-restore +dotnet test -c Release --no-build +python3 .squad/tools/analyzer-check.py # analyzer gate +python3 .squad/tools/coverage-check.py # coverage gate, after a coverage run +``` +<!-- stack:end commands --> -There is no separate lint command beyond formatting. Static analysis runs during -build via the configured rulesets and analyzers. +All commands, with what each one checks, are listed in [`.squad/stack.md`](/.squad/stack.md). -## High-level architecture - -| Path | Role | -| --- | --- | -| `src\DockerUpdateGuard` | Main ASP.NET Core host (`Microsoft.NET.Sdk.Web`); composition root; references the data and telemetry projects | -| `src\DockerUpdateGuard.Data` | Data-access layer; EF Core with PostgreSQL via `Npgsql.EntityFrameworkCore.PostgreSQL` | -| `src\DockerUpdateGuard.Telemetry` | Shared observability layer; OpenTelemetry hosting, OTLP export, ASP.NET Core / HTTP / runtime instrumentation | -| `src\Tests\DockerUpdateGuard.Tests` | Tests for the host/application layer; references the web project; EF Core InMemory + NSubstitute | -| `src\Tests\DockerUpdateGuard.Data.Tests` | Tests for the data layer; references the data project; EF Core SQLite | +## Architecture -Web startup and dependency wiring stay in the main host project; persistence -stays in `.Data`; observability stays in `.Telemetry`. For everything beyond -this table — composition root, background jobs, integration clients, data -model, UI, telemetry, security posture — see [ARCHITECTURE.md](ARCHITECTURE.md). +<!-- project:begin architecture --> +- `src/DockerUpdateGuard` — main ASP.NET Core host (`Microsoft.NET.Sdk.Web`); composition root; references the + data and telemetry projects. Web startup and dependency wiring stay here. +- `src/DockerUpdateGuard.Data` — data-access layer; EF Core with PostgreSQL (`Npgsql.EntityFrameworkCore.PostgreSQL`). +- `src/DockerUpdateGuard.Telemetry` — shared observability layer; OpenTelemetry hosting, OTLP export, ASP.NET + Core / HTTP / runtime instrumentation. +- `src/Tests/DockerUpdateGuard.Tests` — tests for the host/application layer; EF Core InMemory + NSubstitute. +- `src/Tests/DockerUpdateGuard.Data.Tests` — tests for the data layer; EF Core SQLite. +<!-- project:end architecture --> + +## Project configuration + +<!-- stack:begin configuration --> +- **Target framework** as set in the project files (see `.squad/stack.md`); **nullable reference types**, **implicit usings**, and + **documentation XML** generation are all enabled. +- **Central Package Management** via `Directory.Packages.props`; never put versions in individual + `.csproj` files. +- **Reihitsu.Analyzer** and **SonarAnalyzer.CSharp** are dev dependencies in every project (via + `Directory.Build.props`). +- **Solution format** is `.slnx` (XML-based) at the repository root. +<!-- stack:end configuration --> +<!-- project:begin configuration --> +- **Target framework** `net10.0` in the runtime projects; one solution, `DockerUpdateGuard.slnx`, at the + repository root. +- Runtime projects disable generated assembly info and link `SharedAssemblyInfo.cs` from the repository root. +- Runtime and test projects use per-configuration rulesets from `rules/DockerUpdateGuard.Debug.ruleset` and + `rules/DockerUpdateGuard.Release.ruleset`; rules SonarAnalyzer.CSharp disables by default (e.g. `S3776`) are + enabled there. +- SonarCloud rule suppressions are centralized in the shared `src/GlobalSuppressions.cs` (linked into each + project), never scattered per file. +- Tests live under `src/Tests`, not a top-level `tests` folder; keep new test projects there. +- EF Core migrations follow the SeriesOverwatch pattern: the first migration is `InitialCreate`, later ones + `Update1`, `Update2`, …; migration files are renamed to drop the timestamp prefix, while the generated + `[Migration("yyyyMMddHHmmss_Name")]` attribute stays unchanged. +<!-- project:end configuration --> + +## Code style + +<!-- stack:begin code-style --> +File-scoped namespaces; one top-level type per file; `using` outside namespace (System first); Allman +braces, always required; 4-space indent; `var` preferred; language keywords over BCL types; LINQ method +syntax only; `== false` instead of `!`; `is null` / `is not null`; no primary constructors; constructor +injection with `_camelCase` readonly fields; `#region` blocks grouped by member kind (an interface's +region named after the interface, its description not ending in "implementation"); XML docs on all +members (English, no `<remarks>`); `.ConfigureAwait(false)` in library/service code. +<!-- stack:end code-style --> +<!-- project:begin code-style --> +The detailed C# code-style rules (naming, regions, formatting, XML docs, null handling, suppressed analyzer +rules) in [`.github/instructions/csharp.instructions.md`](/.github/instructions/csharp.instructions.md) are +binding and take precedence over the summary above: -## Key conventions +@.github/instructions/csharp.instructions.md -- The repository uses the XML-based `.slnx` solution format. -- Runtime projects target `net10.0`, enable nullable reference types, implicit usings, and XML documentation files. -- Runtime projects disable generated assembly info and link `SharedAssemblyInfo.cs` from the repository root. -- Runtime and test projects use per-configuration rulesets from `rules\DockerUpdateGuard.Debug.ruleset` and `rules\DockerUpdateGuard.Release.ruleset`. -- `Reihitsu.Analyzer` is part of the standard project setup. -- `SonarAnalyzer.CSharp` is referenced from `Directory.Build.props` and therefore applies to every project, so SonarCloud findings surface at build time. Rules that the package disables by default (for example `S3776`) are enabled in the rulesets under `rules\`. -- Tests live under `src\Tests`, not a top-level `tests` folder. Keep new test projects there. -- The test stack is MSTest with `coverlet.collector`. -- Prefer MSTest's `Assert` and `CollectionAssert` APIs directly instead of FluentAssertions. -- Name test classes `{Feature}Tests` and test methods `{Class}{Scenario}{ExpectedResult}`. -- Always include assertion messages in tests. -- Centralize SonarCloud rule suppressions in the shared `src\GlobalSuppressions.cs` (linked into each project) rather than scattering per-file suppressions. - -## EF Core migrations - -If the project adds EF Core migrations, follow the SeriesOverwatch pattern: - -- first migration: `InitialCreate` -- later migrations: `Update1`, `Update2`, `Update3`, ... -- rename migration files to remove the timestamp prefix -- keep the generated `[Migration("yyyyMMddHHmmss_Name")]` attribute unchanged +- CRLF line endings and no final newline (`.editorconfig`). +- Tests: MSTest's `Assert` / `CollectionAssert` (no FluentAssertions); **NSubstitute** is the mocking library + here, used only where a collaborator crosses an infrastructure boundary; EF Core InMemory / SQLite for data + tests (`docs/UNIT_TESTS.md`). Test classes `{TypeUnderTest}Tests`, methods + `{TypeUnderTest}{Scenario}{ExpectedResult}`, always with assertion messages. +<!-- project:end code-style --> + +## Testing + +<!-- stack:begin testing --> +**Unit tests are mandatory for newly written code.** MSTest only (no FluentAssertions, no mocking +library — use real objects or the hand-written fakes/stubs). Classes `{TypeUnderTest}Tests`, methods +`{Class}{Scenario}{ExpectedResult}` in PascalCase **without underscores**; always pass an assert message. +<!-- stack:end testing --> +Full conventions, including the project's test doubles and the checklist to run before committing a new +test, are in [`UNIT_TESTS.md`](/docs/UNIT_TESTS.md). + +## Related skills + +Project-specific workflow skills live under `.claude/skills/`, mirrored identically under +`.agents/skills/` (Codex/GPT) and `.github/skills/` (GitHub Copilot): + +- `create-pr` — verify (format, build, tests, analyzer and coverage gates), review the change locally, + then open a PR following [`.github/pull_request_template.md`](/.github/pull_request_template.md). +- `squad-issue` — fix a GitHub issue with the squad: the Lead plans and picks a tier + (`docs` / `trivial` / `standard` / `security`), the Devil's Advocate challenges `standard`/`security` + plans once, Security reviews security-relevant plans, the Tester writes failing tests first, the Dev + implements to ≥ 80 % coverage, the Code Officer clears format and analyzer diagnostics, Reviewer and + Security review the diff, the Lead approves, then a PR referencing the issue is opened. +- `squad-spec` — the same squad pipeline for a new feature, planned as `spec.md`, `plan.md` and + `tasks.md` in a working folder under `specs/`. +- `review-pr` — review an open pull request against this project's stack, analyzer, security and + unit-test conventions, and post the findings with an explicit verdict. + +Review runs as a subagent defined in `.claude/agents/squad-reviewer.md` (read-only, pinned to Opus, fresh +context). `create-pr` and the squad skills call it *before* pushing, so a change is reviewed while it is +still local; `review-pr` calls the same agent for a pull request that is already open. The review +checklist, the integration-surface sweep, the blocking/non-blocking severity model and the "round 1 is a +full review, later rounds review only the delta" rule live in that one file, so they are identical either +way. An agent without subagent support follows the same file inline. + +The squad skills run a multi-role pipeline defined in [`.squad/`](/.squad/team.md) — Lead (plan, decisions, +PR approval), Devil's Advocate (one plan challenge), Security (plan and diff), Tester (tests first, +coverage), Dev, Code Officer (format, analyzers) and Reviewer — as subagents under +`.claude/agents/squad-*.md`, with the loop limits and escalation rules in +[`.squad/routing.md`](/.squad/routing.md). Stack commands live in [`.squad/stack.md`](/.squad/stack.md), +the project's guarantees, security areas and integration surface in +[`.squad/project.md`](/.squad/project.md). Their working records (`plan.md`, `log.md`, for features also +`spec.md` and `tasks.md`) live under `specs/` on the work branch only; before the PR they are posted as a +comment on the issue and removed, so `main` keeps no working records. An issue or feature PR never changes +the squad or these instructions (`.squad/` except `stack.md` and `project.md`, `.claude/`, +`.github/skills/`, `.agents/skills/`, `CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`): squad +lessons are filed as GitHub issues labelled `squad` and never fixed in a product PR. The squad and these +rules come from the template repository named in `.squad/template.json`: a lesson about a template-managed +file becomes an issue there and is rolled out with its `adopt-template` skill; a lesson about project +knowledge (`.squad/stack.md`, `.squad/project.md`, a project block) becomes an issue here and is worked in +a squad-maintenance PR checked with `python3 .squad/tools/config-check.py` (`.squad/routing.md`, +*Squad lessons*). The user acts as Product Manager +and is only asked when the Lead escalates. Pull requests are merged with *Squash and merge*, so only the +PR title and description reach `main`. + +The reasoning behind code decisions — why something was built the way it was — is recorded by the Lead +as one decision record per decision in [`docs/decisions/`](/docs/decisions/README.md) (append-only, +superseded rather than rewritten), not in `ARCHITECTURE.md`. Read the relevant records before changing +code they cover, and do not contradict an accepted record without superseding it. + +Two rules these skills enforce that are easy to get wrong: + +- **A pull request documents the change, not how it was produced.** The internal review loop — its + pass count, its findings, the commits that resolved them — never appears in the PR title, body or + commit messages. +- **A finding posted as a review comment gets worked in that pull request**, blocking or not. It is + never deferred to "the next change that touches this code": no such change is scheduled, and the + session holding the context to act on it will not exist later. If it really should not be fixed + here, reply with the reason or open a linked issue now — then resolve the thread. + +## Pull requests, contributing and architecture + +Follow [`CONTRIBUTING.md`](/docs/CONTRIBUTING.md) for branch/PR naming (`[area] Description`), the PR +checklist in [`.github/pull_request_template.md`](/.github/pull_request_template.md), and the +stability policy. Consult [`ARCHITECTURE.md`](/docs/ARCHITECTURE.md) before changing the behavior it +describes — the guarantees listed in [`.squad/project.md`](/.squad/project.md) are deliberate, not +incidental behavior. diff --git a/ARCHITECTURE.md b/docs/ARCHITECTURE.md similarity index 91% rename from ARCHITECTURE.md rename to docs/ARCHITECTURE.md index 7c43f84..3c415d7 100644 --- a/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,12 +1,13 @@ # Architecture +<!-- project:begin architecture --> This document describes the architecture of DockerUpdateGuard as currently implemented. It is a technical reference for contributors and AI coding agents working in this repository; it complements, and does not repeat, -[README.md](README.md) (product scope, configuration reference), the -[docs/](docs) folder (contribution, testing, deployment tiers), and -[CLAUDE.md](CLAUDE.md) / [AGENTS.md](AGENTS.md) / -[.github/copilot-instructions.md](.github/copilot-instructions.md) (coding +[README.md](../README.md) (product scope, configuration reference), the +[docs/](.) folder (contribution, testing, deployment tiers), and +[CLAUDE.md](../CLAUDE.md) / [AGENTS.md](../AGENTS.md) / +[.github/copilot-instructions.md](../.github/copilot-instructions.md) (coding conventions). ## 1. Purpose and shape of the system @@ -65,7 +66,7 @@ observability stays in `.Telemetry`; the host project owns composition. Solution-wide conventions (`.slnx` format, `net10.0`, nullable/implicit usings, `SharedAssemblyInfo.cs`, per-configuration rulesets, centralized `GlobalSuppressions.cs`, `Reihitsu.Analyzer` + `SonarAnalyzer.CSharp`) are -documented in [CLAUDE.md](CLAUDE.md) and not repeated here. +documented in [CLAUDE.md](../CLAUDE.md) and not repeated here. ## 3. Host composition and startup sequence @@ -131,7 +132,7 @@ see §11 for the resulting security posture. Configuration is bound from the `DockerUpdateGuard` and `Telemetry` sections via standard ASP.NET Core configuration (files, command-line, secret stores). The full key-by-key reference (defaults, valid ranges, required -combinations) lives in [README.md](README.md#configuration-reference) and +combinations) lives in [README.md](../README.md#configuration-reference) and is not duplicated here. Architecturally relevant points: @@ -206,7 +207,7 @@ read/write intent, not by entity: Current migrations, in order: `InitialCreate` → `Update1` → … → `Update7` (latest). New migrations follow the `Update{N}` naming convention described -in [CLAUDE.md](CLAUDE.md#ef-core-migrations); `Design\ +in [CLAUDE.md](../CLAUDE.md#project-configuration); `Design\ DockerUpdateGuardDbContextFactory` provides the design-time `DbContext` for `dotnet ef` tooling. @@ -440,7 +441,7 @@ aspirational guarantees: behind network-level access control (private network, VPN, or a reverse proxy that terminates auth) rather than exposed directly — this is a deployment responsibility, not something the app enforces itself. See - [SECURITY.md](SECURITY.md) for the vulnerability-reporting process. + [SECURITY.md](../SECURITY.md) for the vulnerability-reporting process. - **No health-check or rate-limiting endpoints** are registered. - **Single-instance assumption for the scan engine.** The PostgreSQL advisory lock (§3) serializes *migrations* across concurrently starting @@ -454,7 +455,7 @@ aspirational guarantees: the root group, not root user) so a group-owned, root:root Docker socket bind-mount stays readable (e.g. on Synology DSM) without running the process as root; see the comment block in - [src/DockerUpdateGuard/Dockerfile](src/DockerUpdateGuard/Dockerfile). + [src/DockerUpdateGuard/Dockerfile](../src/DockerUpdateGuard/Dockerfile). Mounting the host's Docker socket is nonetheless a high-trust operation (equivalent to root on the host) and is inherent to the feature, not a bug. @@ -472,8 +473,8 @@ aspirational guarantees: Test framework, project structure, and conventions (MSTest, NSubstitute, bUnit + MudBlazor via `BlazorTestContextFactory`, EF Core InMemory vs. SQLite, naming, `[DataRow]`-over-branching, mandatory assertion messages) -are fully specified in [docs/UNIT_TESTS.md](docs/UNIT_TESTS.md) and -[docs/CONTRIBUTING.md](docs/CONTRIBUTING.md); the two test projects mirror +are fully specified in [docs/UNIT_TESTS.md](UNIT_TESTS.md) and +[docs/CONTRIBUTING.md](CONTRIBUTING.md); the two test projects mirror the two production projects that matter for this split: - `DockerUpdateGuard.Tests` — host/UI/domain layer, EF Core **InMemory**. @@ -485,7 +486,7 @@ the two production projects that matter for this split: ## 13. Build, CI, and deployment - **Formatting/build/test**: `reihitsu-format`, `dotnet build`, `dotnet - test` as documented in [CLAUDE.md](CLAUDE.md#build-test-and-format). + test` as listed in [`.squad/stack.md`](../.squad/stack.md). - **CI** (`.github/workflows/ci.yml`): on every push to `main` and every pull request — restore, format check, build, test with coverage, and (when `SONAR_TOKEN` is available, i.e. not on forked/Dependabot PRs) @@ -504,7 +505,7 @@ the two production projects that matter for this split: a separately-run Trivy *server*), and an Alpine ASP.NET runtime stage running as the non-root user described in §11. Listens on `:8080`. - **Deployment tiers** (`docs/docker-compose.{minimal,trivy,full}.yml`, - walked through in [INSTALL.md](INSTALL.md)): minimal + walked through in [INSTALL.md](../INSTALL.md)): minimal (app + PostgreSQL), + Trivy server, or + a full Grafana OTel-LGTM stack (collector + Grafana + Loki + Tempo + Prometheus/Mimir) for the OTLP telemetry described in §10. All three tiers use the same published @@ -524,3 +525,29 @@ Summarized here for quick reference; details are in the sections above. | One shared `IImageCatalogRepository` for the `RegistryRepository`/`ImageVersion` catalog, but no repository for any other entity | Only the catalog has a get-or-create-with-dedup concern that benefits from a single enforced entry point (§5.2); other entities are written directly by the orchestrator that owns them. | | PostgreSQL advisory lock around migrations, but no distributed lock around scanning | Migrations are unsafe to run concurrently and are cheap to serialize once at startup; duplicate concurrent scans are wasteful but not unsafe (§11), so they were not made distributed. | | No authentication/authorization in the app itself | The app assumes a trusted network perimeter (reverse proxy, VPN, private network) rather than implementing its own auth layer (§11). | +<!-- project:end architecture --> + +## Development process + +This repository is developed with AI agents (Claude Code, Codex/GPT, GitHub Copilot) that follow the same +rules: `CLAUDE.md`, `AGENTS.md` and `.github/copilot-instructions.md` hold one shared rule set, and the +skills under `.claude/skills/`, `.agents/skills/` and `.github/skills/` are identical copies. Every pull +request is reviewed before it is opened by the read-only reviewer in `.claude/agents/squad-reviewer.md` +— round 1 is a full review, every later round looks only at the delta, and only blocking findings earn +another round, because a fresh full re-review of unchanged code always finds something new. + +The squad skills (`squad-issue`, `squad-spec`) wrap that review in a larger, bounded pipeline described in +[`.squad/routing.md`](../.squad/routing.md): an Opus Lead plans, classifies the change into a tier (`docs`, +`trivial`, `standard`, `security`) that decides how much of the pipeline runs, and owns every decision +including PR approval; for `standard` and `security` a Devil's Advocate challenges the plan once (no veto) +before Security sees it; a Security member reviews the plan (tier `security`) and the diff; tests are +written first and new/changed code reaches at least 80 % line coverage; a Code Officer clears formatting +and analyzer diagnostics *before* the review so the reviewed code is the merged code; and the review loop +is one full pass plus at most two delta rounds. Every limit ends in a Lead decision, and only a decision +the Lead cannot make reaches the human. The stack-specific commands live in +[`.squad/stack.md`](../.squad/stack.md), the project's guarantees and attack surface in +[`.squad/project.md`](../.squad/project.md). + +The reasoning behind individual choices is kept out of this document and recorded instead as decision +records in [`docs/decisions/`](decisions/README.md); this document describes how the system works and +links a record where a guarantee or flow is the result of one. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index f4d0290..3ccd95f 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,5 +1,6 @@ # Contributing +<!-- project:begin getting-started --> ## Getting started ### Machine setup @@ -48,42 +49,77 @@ dotnet build DockerUpdateGuard.slnx -c Release --no-restore ### Running tests ```shell -dotnet test src\Tests\**\*.csproj -c Release --no-build --logger trx --collect:"XPlat Code Coverage" +dotnet test DockerUpdateGuard.slnx -c Release --no-build ``` -To run a single test project or method, see the commands in `README.md` and `CLAUDE.md`. +Coverage, single-test and analyzer commands are listed in [`.squad/stack.md`](../.squad/stack.md). For detailed rules on how unit tests should be structured and named, see [`UNIT_TESTS.md`](UNIT_TESTS.md). +<!-- project:end getting-started --> -### Submitting a pull request +## Submitting a pull request -If you'd like to contribute by fixing a bug, implementing a feature, or even correcting typos in the documentation, you'll need to submit a pull request. +Nothing is ever committed or pushed directly to `main` — every change goes through a separate branch and +a pull request. -Before submitting a pull request, be sure to [rebase](https://www.atlassian.com/git/tutorials/merging-vs-rebasing) your branch onto the current `main`. Do not use `git merge` or the *merge* button provided by GitHub. +Pull requests are merged with **Squash and merge**: the PR title becomes the single commit subject on +`main` and the description its body, so the commits on the branch are working history and need not be +curated. Keep the branch up to date by merging the current `main` into it (no force-push needed); do not +use the plain *Create a merge commit* or *Rebase and merge* buttons (see the decision record on +squash-merging in [`decisions/`](decisions/README.md)). For PR naming use the following convention: `[area] Description` (no period at the end). -- For the area, use the affected project or feature (for example `Data`, `Telemetry`, `UI`, `Scanning`). -- For the description, do not reference an issue number in there. A clear, short summary of what the change entails is enough; there is room to elaborate in the description. +- For the area, use one of the areas listed below, capitalized. +- For the description, do not reference an issue number in there. A clear, short summary of what + the change entails is enough; there is room to elaborate in the description. -When a PR is related to an issue, use the `Closes #issuenumber` syntax so the issue links to the PR automatically and closes when the PR is merged. +<!-- project:begin areas --> +Areas: `Data`, `Telemetry`, `UI`, `Scanning`, `Host`, `Tests`, `Docker`, `CI`, `Docs` — the affected project or +feature. Use before/after screenshots in the PR description when a change affects the UI. +<!-- project:end areas --> -Use before/after screenshots in the PR description when a change affects the UI. +When a PR is related to an issue, use the `Closes #issuenumber` syntax so the issue links to the +PR automatically and closes when the PR is merged. -Follow the PR template in [`.github/PULL_REQUEST_TEMPLATE.md`](../.github/PULL_REQUEST_TEMPLATE.md). +Follow the PR template in [`.github/pull_request_template.md`](../.github/pull_request_template.md). -## Code style +## Quality gates -Detailed C# code-style rules (naming, regions, formatting, XML docs, null handling) are documented in [`.github/instructions/csharp.instructions.md`](../.github/instructions/csharp.instructions.md) and are binding for all contributions. Run `reihitsu-format ./` before opening a pull request. +Code-style rules are documented in [`CLAUDE.md`](../CLAUDE.md) (mirrored in `AGENTS.md` and +[`.github/copilot-instructions.md`](../.github/copilot-instructions.md)) and in +[`.squad/stack.md`](../.squad/stack.md), and are binding for all contributions. Before opening a pull +request, run the commands from `stack.md`: *Format*, *Build*, the *Analyzer gate* (no analyzer diagnostic +of any severity in a changed file) and the *Coverage gate* (at least 80 % line coverage on new or changed +production code and overall, see [`UNIT_TESTS.md`](UNIT_TESTS.md#code-coverage)). A pull request is +expected to arrive clean (see the decision record on quality gates in [`decisions/`](decisions/README.md)). +<!-- project:begin releases --> +## Versioning and releases + +Releases are cut by pushing a `v*.*.*` tag on `main` (with explicit approval): `.github/workflows/release.yml` +builds and tests, pushes the Docker image (version and `latest`) to Docker Hub +(`networlddev/dockerupdateguard`) and creates a GitHub Release with generated notes. Merging a PR by itself +never publishes a release (see [`ARCHITECTURE.md`](ARCHITECTURE.md), *Build, CI, and deployment*). +<!-- project:end releases --> + +<!-- project:begin stability --> ## Stability policy -An essential consideration in every pull request is its impact on the system. Avoid introducing unnecessary breaking changes, performance or functional regressions, or negative impacts on usability. +An essential consideration in every pull request is its impact on the system. Avoid introducing unnecessary +breaking changes, performance or functional regressions, or negative impacts on usability. In particular, +preserve the guarantees listed in [`.squad/project.md`](../.squad/project.md) (*Guarantees*) and described in +[`ARCHITECTURE.md`](ARCHITECTURE.md) unless a change explicitly intends to alter one. +<!-- project:end stability --> ## Reporting security issues -Do not report security vulnerabilities through public GitHub issues. See [`SECURITY.md`](../SECURITY.md) for the private reporting process. +Do not report security vulnerabilities through public GitHub issues. See +[`SECURITY.md`](../SECURITY.md) for the private reporting process. ## License -By contributing to this project, you agree that your contributions will be licensed under the same [MIT License](../LICENSE.md) that covers the project. +<!-- project:begin license --> +By contributing to this project, you agree that your contributions will be licensed under the same +[MIT License](../LICENSE.md) that covers the project. +<!-- project:end license --> diff --git a/docs/UNIT_TESTS.md b/docs/UNIT_TESTS.md index 3ff2891..f0cc53b 100644 --- a/docs/UNIT_TESTS.md +++ b/docs/UNIT_TESTS.md @@ -293,15 +293,20 @@ Code coverage is collected with `coverlet.collector`, already referenced by both test projects — no separate installation is needed to collect coverage during `dotnet test`. -Run the full suite with coverage collection, as documented in `README.md` and -`CLAUDE.md`: +**Threshold: at least 80 % line coverage on new or changed production code, and at least 80 % overall** — +the same measure as SonarQube's "coverage on new code". Check it locally before a push with *Test with +coverage* and the *Coverage gate* from [`.squad/stack.md`](../.squad/stack.md): ```shell -dotnet test src\Tests\**\*.csproj -c Release --no-build --logger trx --collect:"XPlat Code Coverage" +dotnet test DockerUpdateGuard.slnx -c Release --no-build --collect:"XPlat Code Coverage" --results-directory ./TestResults +python3 .squad/tools/coverage-check.py ``` -This produces a `coverage.cobertura.xml` file per test project under its -`TestResults` folder. To turn those into a browsable HTML report locally, +The gate lists every changed production file with its covered/coverable changed lines and the uncovered line +numbers. Lines that genuinely cannot be covered by a unit test (for example host startup glue) need an +explicit, recorded decision — they are not silently accepted. + +This produces a `coverage.cobertura.xml` file per test project under `TestResults`. To turn those into a browsable HTML report locally, install [ReportGenerator](https://reportgenerator.io) once: ```shell @@ -311,11 +316,16 @@ dotnet tool install --global dotnet-reportgenerator-globaltool and merge the reports: ```shell -reportgenerator "-reports:src\Tests\**\TestResults\**\coverage.cobertura.xml" "-targetdir:coverage-report" -reporttypes:Html +reportgenerator "-reports:TestResults/**/coverage.cobertura.xml" "-targetdir:coverage-report" -reporttypes:Html ``` ## Checklist for new tests +- [ ] New production code has accompanying unit tests — mandatory, not optional. +- [ ] The *Analyzer gate* (`python3 .squad/tools/analyzer-check.py`) reports no diagnostic in a changed test + file (MSTest analyzer rules are info-level and only visible there or in SonarQube Cloud). +- [ ] At least 80 % line coverage on new/changed production code and overall (*Coverage gate*). + - [ ] Test class named `{TypeUnderTest}Tests` (or `...RenderTests` / `...PersistentStateTests` when split), in the matching test project. - [ ] Test method named `{TypeUnderTest}{Scenario}{ExpectedResult}` (PascalCase, diff --git a/docs/decisions/0001-quality-gates-before-the-pull-request.md b/docs/decisions/0001-quality-gates-before-the-pull-request.md new file mode 100644 index 0000000..006e0b9 --- /dev/null +++ b/docs/decisions/0001-quality-gates-before-the-pull-request.md @@ -0,0 +1,38 @@ +# 0001: Quality gates before the pull request + +- **Status:** Accepted +- **Date:** 2026-10-03 +- **Source:** Squad adopted from Squad-Spec-Repository-Template +- **Supersedes:** — + +## Context + +Formatting slips, analyzer findings and missing tests that only surface in CI or in the SonarQube Cloud +analysis of the pull request cost an extra fix-and-push round each, and every round costs reviewer +attention. The squad has a Code Officer whose sole job is to leave nothing of that kind behind, and the +`create-pr` skill runs the same checks outside the squad. + +## Options considered + +1. **Keep everything in CI** — a safety net for every contributor, but findings keep arriving late. +2. **Check locally and in CI** — earliest feedback plus a safety net; some checks run twice. +3. **Check locally first, CI as the system of record** — format, analyzer gate and coverage gate run + before the push (`.squad/stack.md`); CI keeps the build, the tests and the code analysis. + +## Decision + +Option 3. Before a push: *Format*, the *Analyzer gate* (no diagnostic of any severity in a changed file) +and the *Coverage gate* (at least 80 % line coverage on new/changed production code and overall) from +`.squad/stack.md` pass. CI still builds, tests and runs the code analysis (e.g. SonarQube Cloud), which +stays the system of record for the quality gate. Squad and agent tooling (`.squad/**`, `.claude/**`) is +excluded from the coverage measure in CI; it is developer tooling, not production code. + +## Consequences + +- Findings appear while coding; the Code Officer clears them before the review. +- A push that skips the local gates is only caught by what CI still checks. The PR template checklist and + the `create-pr` skill require the local gates; adding a CI step back is the remedy if unchecked code + starts reaching `main`. +- The local analyzers may differ from the CI quality profile, and some checks (duplication, security + hotspots, taint analysis) only run in CI; such findings arrive in squad step 11. +- The 80 % rule applies to new or changed lines, so it does not force retroactive tests on old code. diff --git a/docs/decisions/0002-squash-merge-pull-requests.md b/docs/decisions/0002-squash-merge-pull-requests.md new file mode 100644 index 0000000..972befc --- /dev/null +++ b/docs/decisions/0002-squash-merge-pull-requests.md @@ -0,0 +1,31 @@ +# 0002: Squash-merge pull requests + +- **Status:** Accepted +- **Date:** 2026-10-03 +- **Source:** Squad adopted from Squad-Spec-Repository-Template +- **Supersedes:** — + +## Context + +The squad commits and pushes after every pipeline step to secure its work, and the internal process (plan +revisions, review rounds) must not appear in the history of `main`. Rewriting a pushed branch to tidy it +up would need a force-push, which requires explicit approval. + +## Options considered + +1. **Rebase merges, commit only at curated milestones** — linear, granular history on `main`; work + between milestones is not secured, and review fixes have to be folded into earlier commits. +2. **Squash and merge** — one commit per PR on `main`, built from the PR title and description; branch + commits can be frequent and unpolished; per-commit granularity inside a PR is lost on `main`. + +## Decision + +Option 2. Pull requests are merged with *Squash and merge*. The PR title (`[area] Description`) becomes +the commit subject on `main`, the description its body. Branches are kept current by merging `main` into +them instead of rebasing. + +## Consequences + +- The PR title and description are the permanent record of a change and are written for that purpose. +- Branch commits may name pipeline steps; they never reach `main`. +- The repository settings allow only *Squash and merge* (or at least default to it). diff --git a/docs/decisions/0003-squad-working-records-off-main.md b/docs/decisions/0003-squad-working-records-off-main.md new file mode 100644 index 0000000..e720dd5 --- /dev/null +++ b/docs/decisions/0003-squad-working-records-off-main.md @@ -0,0 +1,35 @@ +# 0003: Squad working records stay off main, and product PRs never change the squad + +- **Status:** Accepted +- **Date:** 2026-10-03 +- **Source:** Squad adopted from Squad-Spec-Repository-Template +- **Supersedes:** — + +## Context + +The squad writes a plan and a log per issue or feature under `specs/`. Kept on `main`, these records +accumulate without being read again, while the lasting reasoning already lives in decision records. Squad +lessons fixed inside a product PR mix two unrelated changes and are easy to lose in a later template +refresh. + +## Options considered + +1. **Keep `specs/` on `main`** — full history in the repository; grows with every change and duplicates + the decision records. +2. **Working records only on the work branch** — posted as a "Squad working record" comment on the issue + (or the PR) before the PR opens, then removed; `main` keeps only `specs/README.md` and the templates. + +## Decision + +Option 2. In addition, an issue or feature PR never changes the squad or the agent instructions +(`.squad/` except `stack.md` and `project.md`, `.claude/`, `.github/skills/`, `.agents/skills/`, +`CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`). Lessons about the squad become a GitHub issue +labelled `squad`: lessons about template-managed files in the template repository +(LarsLaskowski/Squad-Spec-Repository-Template), fixed there and rolled out with `adopt-template`; lessons +about project knowledge in the product repository, worked in a separate squad-maintenance PR. + +## Consequences + +- `main` stays free of per-change working records; the issue comment keeps them findable. +- A crashed session can still resume, because the work folder is committed on the work branch. +- Squad fixes need their own PR, even when they are small. diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 0000000..6d08346 --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1,33 @@ +# Decision records + +Why the code is the way it is. Each file records one decision — its context, the options considered, what +was chosen and the consequences — so that months later the reasoning is still available without the +pull request, the issue thread or the session that produced it. + +`docs/ARCHITECTURE.md` describes *how* the system works today; these records explain *why* individual +choices were made. When a decision changes the architecture, `ARCHITECTURE.md` is updated as well and +links the record. + +## Rules + +- One decision per file: `NNNN-short-title.md` (four digits, next free number), created from + [`_template.md`](_template.md). +- Written by the squad Lead (see [`.squad/agents/lead/charter.md`](../../.squad/agents/lead/charter.md)); + anyone may add one for a change made outside the squad. +- Committed together with the change it explains. +- Records are **append-only**: an accepted record is never rewritten. A changed decision gets a new record + that names the old one under *Supersedes*, and the old record's status becomes + `Superseded by NNNN` (the only edit allowed). +- Not for routine changes: a record is needed when a choice between real alternatives was made, a + trade-off or limitation was accepted, a review finding was deliberately not fixed, a documented + guarantee was touched, a dependency was added or removed, or work was split into a follow-up issue. + +## Index + +<!-- project:begin index --> +| # | Title | Status | Date | +| ---- | ----- | ------ | ---- | +| [0001](0001-quality-gates-before-the-pull-request.md) | Quality gates before the pull request | Accepted | 2026-10-03 | +| [0002](0002-squash-merge-pull-requests.md) | Squash-merge pull requests | Accepted | 2026-10-03 | +| [0003](0003-squad-working-records-off-main.md) | Squad working records stay off main, and product PRs never change the squad | Accepted | 2026-10-03 | +<!-- project:end index --> diff --git a/docs/decisions/_template.md b/docs/decisions/_template.md new file mode 100644 index 0000000..54063d4 --- /dev/null +++ b/docs/decisions/_template.md @@ -0,0 +1,25 @@ +# NNNN: <short title> + +- **Status:** Proposed | Accepted | Superseded by NNNN +- **Date:** YYYY-MM-DD +- **Source:** Issue #<number> / PR #<number> +- **Supersedes:** — | NNNN + +## Context + +The situation and the forces at play: the problem, constraints, relevant guarantees from +`docs/ARCHITECTURE.md`, what the issue reporter observed. + +## Options considered + +1. **<option>** — pros / cons. +2. **<option>** — pros / cons. + +## Decision + +What was chosen, stated so it can be checked against the code. + +## Consequences + +What becomes easier or harder, accepted limitations, follow-up issues, what would have to change to revisit +this decision. diff --git a/specs/README.md b/specs/README.md new file mode 100644 index 0000000..315a99f --- /dev/null +++ b/specs/README.md @@ -0,0 +1,13 @@ +# Specs + +Working records of the squad (see [`.squad/`](../.squad/team.md) and +[`.squad/routing.md`](../.squad/routing.md)). They live **only on a work branch**: + +- `specs/issue-<number>/` — `plan.md`, `log.md` (created by the `squad-issue` skill) +- `specs/feature-<short-slug>/` — `spec.md`, `plan.md`, `tasks.md`, `log.md` (created by the + `squad-spec` skill) + +Before the pull request is opened, the content is posted as a "Squad working record" comment on the issue +(or the PR) and the folder is removed, so `main` only holds this README and the templates in +[`_template/`](_template). The lasting reasoning behind a change is recorded in +[`docs/decisions/`](../docs/decisions/README.md). \ No newline at end of file diff --git a/specs/_template/log.md b/specs/_template/log.md new file mode 100644 index 0000000..324219c --- /dev/null +++ b/specs/_template/log.md @@ -0,0 +1,6 @@ +# Log: <issue #number | feature name> + +One line per pipeline step: date, step, member, result. Lead decisions and escalations are quoted in full. + +| Date | Step | Member | Result | +| ---- | ---- | ------ | ------ | diff --git a/specs/_template/plan.md b/specs/_template/plan.md new file mode 100644 index 0000000..f442f8f --- /dev/null +++ b/specs/_template/plan.md @@ -0,0 +1,57 @@ +# Plan: <title> + +Source: Issue #<number> | [spec.md](spec.md) +Status: Draft | Revised (n) | Approved by Security +Tier: trivial | standard | security (tier `docs` uses no plan.md) — <one-sentence justification> + +## Problem / root cause + +For a bug: the cause, with file and line. For a feature: a summary of `spec.md`. +Each factual claim of the issue, checked against the code: confirmed or refuted. + +## Acceptance criteria + +- [ ] AC1: ... (the Tester turns each one into at least one unit test) + +## Approach + +## Affected projects and types + +| Project | Type / file | Change | +| ------- | ----------- | ------ | + +## Signatures (for the Dev's skeleton) + +Every new or changed member, with its full signature — or "none". + +## Test files + +Named strictly by the convention in *Layout* of [`.squad/stack.md`](../../.squad/stack.md) and +[UNIT_TESTS.md](../../docs/UNIT_TESTS.md) — no combined file, no alternatives. + +Existing test code that calls a changed signature (factories, helpers): the call sites, and who adapts them — +the Dev in step 4 (skeleton) when the old signature goes away, so the suite keeps building; the Tester in +step 5 when old and new signature coexist (`.squad/routing.md`, *Loop limits*). "None" if no existing +test is affected. + +## Documentation updates + +`README.md` (configuration table, env vars), `docs/*.md` — or "none". + +## Architecture check + +Which guarantees from `docs/ARCHITECTURE.md` are touched and how they are preserved. + +## Security considerations + +## Decision records + +- `docs/decisions/NNNN-title.md` (Proposed) — or "none: no decision beyond the obvious fix" + +## Challenge + +Left out when the plan is written. The Lead adds it in mode `revise` only after Devil's Advocate +objections: each objection and the answer (accepted — what changed; or rejected — why). A clean challenge +(`NO OBJECTIONS`) is recorded only in `log.md`. + +## Out of scope / follow-ups diff --git a/specs/_template/spec.md b/specs/_template/spec.md new file mode 100644 index 0000000..d93ceb4 --- /dev/null +++ b/specs/_template/spec.md @@ -0,0 +1,19 @@ +# Spec: <title> + +Status: Draft | Approved by Lead + +## Problem / motivation + +## Behavior + +What the user or the system does differently. No implementation details. + +## Acceptance criteria + +- [ ] AC1: ... + +## Out of scope + +## Open questions + +Questions the Lead could not settle go to the Product Manager (escalation). diff --git a/specs/_template/tasks.md b/specs/_template/tasks.md new file mode 100644 index 0000000..84d9239 --- /dev/null +++ b/specs/_template/tasks.md @@ -0,0 +1,7 @@ +# Tasks: <title> + +Plan: [plan.md](plan.md) — Status: Draft | Approved + +| # | Task | Files | Tests (AC) | Owner | Done | +| - | ---- | ----- | ---------- | ----- | ---- | +| 1 | ... | ... | AC1 | Dev / Tester | [ ] | From f22c0e47e78b61a08281244004a9ea3301e4d84c Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 3 Oct 2026 12:58:49 +0000 Subject: [PATCH 2/6] Clear earlier coverage runs before measuring The coverage gate now merges every report it finds, one per test project, so Test with coverage removes TestResults first and the unit-test guide shows the same commands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014xZzHGWbMaKfnCWeA4Lgj1 --- .squad/stack.md | 2 +- .squad/tools/coverage-check.py | 21 ++++++++++----------- docs/UNIT_TESTS.md | 1 + 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.squad/stack.md b/.squad/stack.md index ead3d9a..92a43bd 100644 --- a/.squad/stack.md +++ b/.squad/stack.md @@ -35,7 +35,7 @@ profile and owned by this repository: keep it true when the build changes. | *Build* | `dotnet build DockerUpdateGuard.slnx -c Release --no-restore` | | *Test* | `dotnet test DockerUpdateGuard.slnx -c Release --no-build` | | *Single test* | `dotnet test src/Tests/DockerUpdateGuard.Tests/DockerUpdateGuard.Tests.csproj --filter "FullyQualifiedName~ClassName.MethodName"` | -| *Test with coverage* | `dotnet test DockerUpdateGuard.slnx -c Release --no-build --collect:"XPlat Code Coverage" --results-directory ./TestResults` | +| *Test with coverage* | `rm -rf TestResults && dotnet test DockerUpdateGuard.slnx -c Release --no-build --collect:"XPlat Code Coverage" --results-directory ./TestResults` (clears earlier runs: the coverage gate merges every report it finds) | | *Coverage gate* | `python3 .squad/tools/coverage-check.py` | | *Analyzer gate* | `python3 .squad/tools/analyzer-check.py` | diff --git a/.squad/tools/coverage-check.py b/.squad/tools/coverage-check.py index 1c7ae57..f120ea9 100644 --- a/.squad/tools/coverage-check.py +++ b/.squad/tools/coverage-check.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """Coverage gate for the squad: line coverage of new/changed production lines (like SonarQube's -"coverage on new code") and overall line coverage, merged from the coverage reports of the latest test run (one per test project). +"coverage on new code") and overall line coverage, merged from all coverage reports +(one per test project) of the latest test run; *Test with coverage* clears the results directory first. Supported report formats (set COVERAGE_FORMAT in `.squad/tools/squad_settings.py`): - `cobertura` — e.g. coverlet's `coverage.cobertura.xml` (.NET), or any Cobertura XML @@ -58,17 +59,13 @@ def changed_lines(): return result -# Reports written within this many seconds of the newest one belong to the same test run (one report per -# test project); older ones are leftovers of earlier runs and are ignored. -RUN_WINDOW_SECONDS = 600 - - -def latest_run_reports(): - reports = glob.glob(settings.COVERAGE_REPORT_GLOB, recursive=True) +def run_reports(): + """Every report the glob matches: one per test project of the latest run. *Test with coverage* clears the + results directory first, so no report of an earlier run can be among them.""" + reports = sorted(glob.glob(settings.COVERAGE_REPORT_GLOB, recursive=True)) if not reports: sys.exit(f"No coverage report matches {settings.COVERAGE_REPORT_GLOB} - run *Test with coverage* first") - newest = max(os.path.getmtime(report) for report in reports) - return sorted(report for report in reports if newest - os.path.getmtime(report) <= RUN_WINDOW_SECONDS) + return reports def add_hit(hits, path, number, count): @@ -147,7 +144,9 @@ def main(): if loader is None: sys.exit(f"Unknown COVERAGE_FORMAT '{settings.COVERAGE_FORMAT}' in squad_settings.py") hits = {} - for report in latest_run_reports(): + reports = run_reports() + print(f"Coverage reports merged: {len(reports)}") + for report in reports: for path, lines in loader(report).items(): for number, count in lines.items(): add_hit(hits, path, number, count) diff --git a/docs/UNIT_TESTS.md b/docs/UNIT_TESTS.md index f0cc53b..7a11cff 100644 --- a/docs/UNIT_TESTS.md +++ b/docs/UNIT_TESTS.md @@ -298,6 +298,7 @@ the same measure as SonarQube's "coverage on new code". Check it locally before coverage* and the *Coverage gate* from [`.squad/stack.md`](../.squad/stack.md): ```shell +rm -rf TestResults dotnet test DockerUpdateGuard.slnx -c Release --no-build --collect:"XPlat Code Coverage" --results-directory ./TestResults python3 .squad/tools/coverage-check.py ``` From 39edd33fe3d3626c09b35f0099188b71634cb317 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 3 Oct 2026 13:03:35 +0000 Subject: [PATCH 3/6] Align the adoption with Sonar, the C# rules and the Serena memories SonarQube's coverage measure now excludes the generated EF Core migrations like the local coverage gate does, so both gates measure the same code. The C# instructions name interface regions after the interface as the code and stack.md already do, the testing rules defer to the project's test doubles (NSubstitute here), and the Serena memories point to docs/ARCHITECTURE.md and .squad/stack.md. Reviews compare the PR description with the diff and read the SonarQube result, as the former review skill did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014xZzHGWbMaKfnCWeA4Lgj1 --- .github/copilot-instructions.md | 11 +++++++---- .github/instructions/csharp.instructions.md | 6 +++--- .github/pull_request_template.md | 2 +- .github/workflows/ci.yml | 2 +- .serena/memories/project_overview.md | 2 +- .serena/memories/suggested_commands.md | 2 +- .serena/memories/task_completion.md | 2 +- .squad/project.md | 3 +++ .squad/template.json | 2 +- AGENTS.md | 11 +++++++---- CLAUDE.md | 11 +++++++---- docs/UNIT_TESTS.md | 1 - 12 files changed, 33 insertions(+), 22 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 23c6850..3677aa2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -135,7 +135,8 @@ members (English, no `<remarks>`); `.ConfigureAwait(false)` in library/service c <!-- project:begin code-style --> The detailed C# code-style rules (naming, regions, formatting, XML docs, null handling, suppressed analyzer rules) in [`.github/instructions/csharp.instructions.md`](/.github/instructions/csharp.instructions.md) are -binding and take precedence over the summary above: +binding; together with *Writing code* in `.squad/stack.md` they describe one set of rules (a conflict between +them is a sync bug to fix): @.github/instructions/csharp.instructions.md @@ -149,9 +150,11 @@ binding and take precedence over the summary above: ## Testing <!-- stack:begin testing --> -**Unit tests are mandatory for newly written code.** MSTest only (no FluentAssertions, no mocking -library — use real objects or the hand-written fakes/stubs). Classes `{TypeUnderTest}Tests`, methods -`{Class}{Scenario}{ExpectedResult}` in PascalCase **without underscores**; always pass an assert message. +**Unit tests are mandatory for newly written code.** MSTest with its own `Assert` / `CollectionAssert` (no +FluentAssertions); test doubles as `.squad/project.md` (*Test doubles*) and `docs/UNIT_TESTS.md` prescribe — +real objects and hand-written fakes/stubs unless the project names a mocking library. Classes +`{TypeUnderTest}Tests`, methods `{Class}{Scenario}{ExpectedResult}` in PascalCase **without underscores**; +always pass an assert message. <!-- stack:end testing --> Full conventions, including the project's test doubles and the checklist to run before committing a new test, are in [`UNIT_TESTS.md`](/docs/UNIT_TESTS.md). diff --git a/.github/instructions/csharp.instructions.md b/.github/instructions/csharp.instructions.md index 0c7c77f..451ce4d 100644 --- a/.github/instructions/csharp.instructions.md +++ b/.github/instructions/csharp.instructions.md @@ -122,7 +122,7 @@ public void DoSomething() 5. `Static methods` 6. `Methods` 7. `Controller methods` (for controllers) -8. `IDisposable` / `IDisposable implementation` +8. `IDisposable` (named after the interface; the description never ends with "implementation") 9. Interface implementations (e.g. `IAppMetrics properties`, `IAppMetrics methods`) - Within the same visibility level, place **static methods before instance methods** (for example, `public static` before `public`, and `private static` before `private`) @@ -492,7 +492,7 @@ public enum TracingTarget ## IDisposable Pattern ```csharp -#region IDisposable implementation +#region IDisposable /// <summary> /// Releases the resources used by the current instance of the class @@ -503,7 +503,7 @@ public void Dispose() _resource = null; } -#endregion // IDisposable implementation +#endregion // IDisposable ``` For more complex resources: Flush → Shutdown → Dispose → set to null. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c39630d..27281dc 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -48,7 +48,7 @@ Please provide a summary of the tests affected by this work and any unique strat - [ ] New dependencies, if any, were added the way *Dependencies* in `.squad/stack.md` prescribes. <!-- project:begin checklist --> -- [ ] I have followed the project's [code style guidelines](../.github/instructions/csharp.instructions.md). +- [ ] The C# rules in [`csharp.instructions.md`](../.github/instructions/csharp.instructions.md) are followed. ### UI-specific diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a042747..70c378b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,7 @@ jobs: /o:"networlddev" \ /d:sonar.token="$SONAR_TOKEN" \ /d:sonar.cs.opencover.reportsPaths="${{ runner.temp }}/TestResults/**/coverage.opencover.xml" \ - /d:sonar.coverage.exclusions=".squad/**,.claude/**" + /d:sonar.coverage.exclusions=".squad/**,.claude/**,src/DockerUpdateGuard.Data/Migrations/**" - name: Build run: dotnet build DockerUpdateGuard.slnx -c Release --no-restore diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md index d1f7bcf..b9d78dc 100644 --- a/.serena/memories/project_overview.md +++ b/.serena/memories/project_overview.md @@ -1 +1 @@ -DockerUpdateGuard is an ASP.NET Core Razor Components web app for monitoring Docker runtime containers, observed images, update findings, vulnerabilities, scan history, and shared base-image dependencies. Main projects: src\\DockerUpdateGuard (web host/UI), src\\DockerUpdateGuard.Data (EF Core/PostgreSQL data layer), src\\DockerUpdateGuard.Telemetry (OpenTelemetry setup), src\\Tests\\DockerUpdateGuard.Tests and src\\Tests\\DockerUpdateGuard.Data.Tests (MSTest). Full binding architecture reference (composition root, background scan engine, integration clients, data model, UI, telemetry, security posture): ARCHITECTURE.md at the repo root — read it before structural changes and keep it current. \ No newline at end of file +DockerUpdateGuard is an ASP.NET Core Razor Components web app for monitoring Docker runtime containers, observed images, update findings, vulnerabilities, scan history, and shared base-image dependencies. Main projects: src\\DockerUpdateGuard (web host/UI), src\\DockerUpdateGuard.Data (EF Core/PostgreSQL data layer), src\\DockerUpdateGuard.Telemetry (OpenTelemetry setup), src\\Tests\\DockerUpdateGuard.Tests and src\\Tests\\DockerUpdateGuard.Data.Tests (MSTest). Full binding architecture reference: docs/ARCHITECTURE.md — read it before structural changes and keep it current. Squad, guarantees and integration surface: .squad/project.md. \ No newline at end of file diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md index a301302..d7be348 100644 --- a/.serena/memories/suggested_commands.md +++ b/.serena/memories/suggested_commands.md @@ -1 +1 @@ -Windows development commands: dotnet restore DockerUpdateGuard.slnx ; reihitsu-format ./ ; dotnet build DockerUpdateGuard.slnx -c Release --no-restore ; Get-ChildItem -Path src\\Tests -Recurse -Filter *.csproj | ForEach-Object { dotnet test $_.FullName -c Release --no-build --logger trx --collect:\"XPlat Code Coverage\" } ; dotnet test src\\Tests\\DockerUpdateGuard.Tests\\DockerUpdateGuard.Tests.csproj -c Release --no-build ; dotnet test src\\Tests\\DockerUpdateGuard.Tests\\DockerUpdateGuard.Tests.csproj --filter \"FullyQualifiedName~Namespace.ClassName.MethodName\" ; git --no-pager status ; git --no-pager diff --stat \ No newline at end of file +All development commands (restore, format, build, test, single test, test with coverage, coverage gate, analyzer gate) are listed in .squad/stack.md. On Windows, run the Python gates with `python .squad/tools/analyzer-check.py` and `python .squad/tools/coverage-check.py`; `git --no-pager status` / `git --no-pager diff --stat` for a quick look. \ No newline at end of file diff --git a/.serena/memories/task_completion.md b/.serena/memories/task_completion.md index cd25995..7d70018 100644 --- a/.serena/memories/task_completion.md +++ b/.serena/memories/task_completion.md @@ -1 +1 @@ -After source changes in DockerUpdateGuard, run reihitsu-format ./ before build. Then run dotnet build DockerUpdateGuard.slnx -c Release --no-restore and execute the existing MSTest projects under src\\Tests. Prefer the PowerShell Get-ChildItem expansion for running all test projects on Windows because the shell does not expand src\\Tests\\**\\*.csproj automatically. \ No newline at end of file +After source changes in DockerUpdateGuard, run Format, Build, the Analyzer gate, Test with coverage and the Coverage gate from .squad/stack.md (at least 80 % line coverage on new/changed code and overall), and keep docs/ARCHITECTURE.md and .squad/project.md true. \ No newline at end of file diff --git a/.squad/project.md b/.squad/project.md index 7331933..0664fb3 100644 --- a/.squad/project.md +++ b/.squad/project.md @@ -75,6 +75,9 @@ change with it. **A new Blazor page or component** touches the UI tests (`…RenderTests`, `…PersistentStateTests`) and the UI section of `docs/ARCHITECTURE.md`. +**Every review** also compares the PR description with the diff and reads the SonarQube Cloud result of the +PR; a description that overstates or misses part of the change, or a failing quality gate, is a finding. + **Async code in service and data-access code** uses `.ConfigureAwait(false)`; EF Core navigation and query assumptions (included navigations, tracking) are checked against the query that loads the entity. diff --git a/.squad/template.json b/.squad/template.json index c35f5e2..27926c7 100644 --- a/.squad/template.json +++ b/.squad/template.json @@ -1,5 +1,5 @@ { "repository": "LarsLaskowski/Squad-Spec-Repository-Template", - "commit": "cf659f2de8fe0a5332448a587e0fa2f789321095", + "commit": "3cf21ba30f6acdaae5fd9898f3347669020aa8e1", "profile": "dotnet" } diff --git a/AGENTS.md b/AGENTS.md index a4df4f9..b82a0d7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -135,7 +135,8 @@ members (English, no `<remarks>`); `.ConfigureAwait(false)` in library/service c <!-- project:begin code-style --> The detailed C# code-style rules (naming, regions, formatting, XML docs, null handling, suppressed analyzer rules) in [`.github/instructions/csharp.instructions.md`](/.github/instructions/csharp.instructions.md) are -binding and take precedence over the summary above: +binding; together with *Writing code* in `.squad/stack.md` they describe one set of rules (a conflict between +them is a sync bug to fix): @.github/instructions/csharp.instructions.md @@ -149,9 +150,11 @@ binding and take precedence over the summary above: ## Testing <!-- stack:begin testing --> -**Unit tests are mandatory for newly written code.** MSTest only (no FluentAssertions, no mocking -library — use real objects or the hand-written fakes/stubs). Classes `{TypeUnderTest}Tests`, methods -`{Class}{Scenario}{ExpectedResult}` in PascalCase **without underscores**; always pass an assert message. +**Unit tests are mandatory for newly written code.** MSTest with its own `Assert` / `CollectionAssert` (no +FluentAssertions); test doubles as `.squad/project.md` (*Test doubles*) and `docs/UNIT_TESTS.md` prescribe — +real objects and hand-written fakes/stubs unless the project names a mocking library. Classes +`{TypeUnderTest}Tests`, methods `{Class}{Scenario}{ExpectedResult}` in PascalCase **without underscores**; +always pass an assert message. <!-- stack:end testing --> Full conventions, including the project's test doubles and the checklist to run before committing a new test, are in [`UNIT_TESTS.md`](/docs/UNIT_TESTS.md). diff --git a/CLAUDE.md b/CLAUDE.md index 9c8e4f3..b65a9a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -135,7 +135,8 @@ members (English, no `<remarks>`); `.ConfigureAwait(false)` in library/service c <!-- project:begin code-style --> The detailed C# code-style rules (naming, regions, formatting, XML docs, null handling, suppressed analyzer rules) in [`.github/instructions/csharp.instructions.md`](/.github/instructions/csharp.instructions.md) are -binding and take precedence over the summary above: +binding; together with *Writing code* in `.squad/stack.md` they describe one set of rules (a conflict between +them is a sync bug to fix): @.github/instructions/csharp.instructions.md @@ -149,9 +150,11 @@ binding and take precedence over the summary above: ## Testing <!-- stack:begin testing --> -**Unit tests are mandatory for newly written code.** MSTest only (no FluentAssertions, no mocking -library — use real objects or the hand-written fakes/stubs). Classes `{TypeUnderTest}Tests`, methods -`{Class}{Scenario}{ExpectedResult}` in PascalCase **without underscores**; always pass an assert message. +**Unit tests are mandatory for newly written code.** MSTest with its own `Assert` / `CollectionAssert` (no +FluentAssertions); test doubles as `.squad/project.md` (*Test doubles*) and `docs/UNIT_TESTS.md` prescribe — +real objects and hand-written fakes/stubs unless the project names a mocking library. Classes +`{TypeUnderTest}Tests`, methods `{Class}{Scenario}{ExpectedResult}` in PascalCase **without underscores**; +always pass an assert message. <!-- stack:end testing --> Full conventions, including the project's test doubles and the checklist to run before committing a new test, are in [`UNIT_TESTS.md`](/docs/UNIT_TESTS.md). diff --git a/docs/UNIT_TESTS.md b/docs/UNIT_TESTS.md index 7a11cff..7a0f2ee 100644 --- a/docs/UNIT_TESTS.md +++ b/docs/UNIT_TESTS.md @@ -326,7 +326,6 @@ reportgenerator "-reports:TestResults/**/coverage.cobertura.xml" "-targetdir:cov - [ ] The *Analyzer gate* (`python3 .squad/tools/analyzer-check.py`) reports no diagnostic in a changed test file (MSTest analyzer rules are info-level and only visible there or in SonarQube Cloud). - [ ] At least 80 % line coverage on new/changed production code and overall (*Coverage gate*). - - [ ] Test class named `{TypeUnderTest}Tests` (or `...RenderTests` / `...PersistentStateTests` when split), in the matching test project. - [ ] Test method named `{TypeUnderTest}{Scenario}{ExpectedResult}` (PascalCase, From f85cb655f4f64a8b2473780122a5fc86d108b68e Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 3 Oct 2026 13:04:34 +0000 Subject: [PATCH 4/6] Scope the Sonar review check to open PRs and add the Windows coverage command Pre-push reviews run before a PR or Sonar result exists, so project.md now asks only reviews of an open pull request to compare the description and read the SonarQube result. The Serena memory gives the PowerShell form of Test with coverage, because the stack.md command uses rm -rf and &&. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014xZzHGWbMaKfnCWeA4Lgj1 --- .serena/memories/suggested_commands.md | 2 +- .squad/project.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md index d7be348..977e03d 100644 --- a/.serena/memories/suggested_commands.md +++ b/.serena/memories/suggested_commands.md @@ -1 +1 @@ -All development commands (restore, format, build, test, single test, test with coverage, coverage gate, analyzer gate) are listed in .squad/stack.md. On Windows, run the Python gates with `python .squad/tools/analyzer-check.py` and `python .squad/tools/coverage-check.py`; `git --no-pager status` / `git --no-pager diff --stat` for a quick look. \ No newline at end of file +All development commands (restore, format, build, test, single test, test with coverage, coverage gate, analyzer gate) are listed in .squad/stack.md. On Windows, run the Python gates with `python .squad/tools/analyzer-check.py` and `python .squad/tools/coverage-check.py`; `git --no-pager status` / `git --no-pager diff --stat` for a quick look. On Windows PowerShell, run Test with coverage as `Remove-Item -Recurse -Force TestResults -ErrorAction SilentlyContinue; dotnet test DockerUpdateGuard.slnx -c Release --no-build --collect:"XPlat Code Coverage" --results-directory ./TestResults` (the stack.md form uses `rm -rf` and `&&`). \ No newline at end of file diff --git a/.squad/project.md b/.squad/project.md index 0664fb3..6bacafe 100644 --- a/.squad/project.md +++ b/.squad/project.md @@ -75,8 +75,8 @@ change with it. **A new Blazor page or component** touches the UI tests (`…RenderTests`, `…PersistentStateTests`) and the UI section of `docs/ARCHITECTURE.md`. -**Every review** also compares the PR description with the diff and reads the SonarQube Cloud result of the -PR; a description that overstates or misses part of the change, or a failing quality gate, is a finding. +**A review of an open pull request** (`review-pr`, squad step 11) also compares the PR description with the +diff and reads the SonarQube Cloud result of the PR; a description that overstates or misses part of the change, or a failing quality gate, is a finding. **Async code in service and data-access code** uses `.ConfigureAwait(false)`; EF Core navigation and query assumptions (included navigations, tracking) are checked against the query that loads the entity. From 4d8ceb4cacc6e89704b4899fec066903f7db4481 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 3 Oct 2026 13:10:45 +0000 Subject: [PATCH 5/6] Record the template's main commit in template.json The template changes this adoption needed are merged, so .squad/template.json points at the template's main commit that the next refresh starts from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014xZzHGWbMaKfnCWeA4Lgj1 --- .squad/template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.squad/template.json b/.squad/template.json index 27926c7..11aa744 100644 --- a/.squad/template.json +++ b/.squad/template.json @@ -1,5 +1,5 @@ { "repository": "LarsLaskowski/Squad-Spec-Repository-Template", - "commit": "3cf21ba30f6acdaae5fd9898f3347669020aa8e1", + "commit": "d369d65437216cca48888a32e632de16e4873793", "profile": "dotnet" } From ebc2ddc4b4af917a0baa2ecb7c57609ae3ef588a Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 3 Oct 2026 13:20:23 +0000 Subject: [PATCH 6/6] Refresh the squad from the template to lower coverage gate complexity The coverage gate's main() is split into helpers in the template, which clears SonarQube Cloud's python:S3776 finding on coverage-check.py. The gate's output is unchanged. The template record now points to the template commit carrying the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014xZzHGWbMaKfnCWeA4Lgj1 --- .squad/template.json | 2 +- .squad/tools/coverage-check.py | 37 +++++++++++++++++++++++----------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/.squad/template.json b/.squad/template.json index 11aa744..a6d8a21 100644 --- a/.squad/template.json +++ b/.squad/template.json @@ -1,5 +1,5 @@ { "repository": "LarsLaskowski/Squad-Spec-Repository-Template", - "commit": "d369d65437216cca48888a32e632de16e4873793", + "commit": "9b733d05141c9d77adfebab0fbc51643dd3b4169", "profile": "dotnet" } diff --git a/.squad/tools/coverage-check.py b/.squad/tools/coverage-check.py index f120ea9..f845c0b 100644 --- a/.squad/tools/coverage-check.py +++ b/.squad/tools/coverage-check.py @@ -134,15 +134,8 @@ def load_go(report): LOADERS = {"cobertura": load_cobertura, "lcov": load_lcov, "go": load_go} -def main(): - parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument("--threshold", type=float, default=80.0) - args = parser.parse_args() - - os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")) - loader = LOADERS.get(settings.COVERAGE_FORMAT) - if loader is None: - sys.exit(f"Unknown COVERAGE_FORMAT '{settings.COVERAGE_FORMAT}' in squad_settings.py") +def merged_hits(loader): + """Merge every report matching COVERAGE_REPORT_GLOB into one map of hits per file and line.""" hits = {} reports = run_reports() print(f"Coverage reports merged: {len(reports)}") @@ -150,7 +143,11 @@ def main(): for path, lines in loader(report).items(): for number, count in lines.items(): add_hit(hits, path, number, count) + return hits + +def overall_coverage(hits): + """Return (percent, hit lines, coverable lines) over the tracked production files.""" tracked = set(subprocess.run( ["git", "ls-files", "--", *settings.COVERAGE_PATHSPECS, *[f":(exclude){p}" for p in settings.COVERAGE_EXCLUDES]], @@ -158,8 +155,11 @@ def main(): production = {path: lines for path, lines in hits.items() if path in tracked} total = sum(len(lines) for lines in production.values()) total_hit = sum(1 for lines in production.values() for count in lines.values() if count > 0) - overall = total_hit / total * 100 if total else 100.0 + return (total_hit / total * 100 if total else 100.0), total_hit, total + +def new_code_coverage(hits): + """Print the changed production files and returns (percent, hit lines, coverable lines).""" covered = coverable = 0 print("Changed production files (coverable changed lines):") for path, lines in sorted(changed_lines().items()): @@ -170,14 +170,27 @@ def main(): missed = sorted(n for n in relevant if file_hits[n] == 0) rate = f"{hit / len(relevant) * 100:5.1f}%" if relevant else " n/a " print(f" {rate} {hit}/{len(relevant)} {path}" + (f" uncovered: {missed}" if missed else "")) + return (covered / coverable * 100 if coverable else 100.0), covered, coverable + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--threshold", type=float, default=80.0) + args = parser.parse_args() + + os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")) + loader = LOADERS.get(settings.COVERAGE_FORMAT) + if loader is None: + sys.exit(f"Unknown COVERAGE_FORMAT '{settings.COVERAGE_FORMAT}' in squad_settings.py") + hits = merged_hits(loader) + overall, total_hit, total = overall_coverage(hits) + new_code, covered, coverable = new_code_coverage(hits) - new_code = covered / coverable * 100 if coverable else 100.0 print(f"\nNew/changed code: {new_code:.1f}% ({covered}/{coverable} lines)") print(f"Overall: {overall:.1f}% ({total_hit}/{total} lines)") ok = new_code >= args.threshold and overall >= args.threshold print(f"Threshold {args.threshold:.0f}%: {'PASS' if ok else 'FAIL'}") return 0 if ok else 1 - if __name__ == "__main__": sys.exit(main())