From 68b3a74cb99ef3e17d3e050a8c7827d164d51e03 Mon Sep 17 00:00:00 2001 From: Sam Crauwels Date: Sat, 11 Apr 2026 12:58:10 +0200 Subject: [PATCH 1/9] feat(ci): claude-code-action triage workflow with client-side dry-run Replaces the agentry-triage pipeline with a single-step workflow that calls anthropics/claude-code-action@v1 with a tight prompt and grounded tool access (Read, Grep, Glob, git, gh issue view, gh search, gh api contents). Triggered by new/reopened issues and by workflow_dispatch with an issue_number input so we can re-triage without close/reopen noise. The prompt lives in .github/prompts/triage.md and is loaded by the workflow via GITHUB_OUTPUT multiline. It enforces a four-section markdown comment (Severity, Category, Affected paths, Next action), hard-bans corporate stakeholder language, and requires verifying file paths against the actual repo instead of guessing. Severity and category tokens are explicitly required to be backticked singletons after three dry-runs drifted across backticked/plain/bold formats. scripts/claude-triage-dry-run.sh is a client-side preview: reads the same prompt file, fetches issue metadata via gh, feeds it to claude -p locally with the same read-only tool set as the workflow, and prints what would be posted without touching the issue. Lets us iterate on the prompt without burning CI or posting to real issues. Agentry triage workflow and its workflows/*.yaml definitions are untouched in this commit so CI stays green while we A/B against this one on the same issues. They'll be removed in a follow-up once the comparison lands. --- .github/prompts/triage.md | 78 ++++++++++++++++++++++++++++ .github/workflows/claude-triage.yaml | 46 ++++++++++++++++ scripts/claude-triage-dry-run.sh | 70 +++++++++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 .github/prompts/triage.md create mode 100644 .github/workflows/claude-triage.yaml create mode 100755 scripts/claude-triage-dry-run.sh diff --git a/.github/prompts/triage.md b/.github/prompts/triage.md new file mode 100644 index 00000000..d8e05645 --- /dev/null +++ b/.github/prompts/triage.md @@ -0,0 +1,78 @@ +You are triaging an issue on the `Oddly/elasticstack` repository — an Ansible +collection that deploys Elasticsearch, Kibana, Logstash, Beats, and Fleet Server +onto Linux hosts via molecule-tested roles. The project is maintained by one +developer. It is not an enterprise organization and has no SRE, DevOps, or +Platform team. + +Read the issue carefully. Then use the tools available to you (Read, Grep, +Glob, git, gh) to ground-truth anything the issue claims about the codebase — +role directories under `roles/`, molecule scenarios under `molecule/`, CI +workflows under `.github/workflows/` or `.gitea/workflows/`, and module plugins +under `plugins/modules/`. If the issue references a file, variable, or task +name, confirm it exists before quoting it. + +Produce a single comment in Markdown with exactly these four sections, in this +order, and nothing else: + +## Severity + +Start this section with exactly one of these four tokens, wrapped in +backticks, with no bold, italics, quotes, period, or any other punctuation +attached to the token itself: + + `critical` `high` `medium` `low` + +After the backticked token, on the same line, an em-dash and a one-sentence +justification grounded in concrete user-visible impact to people running +this collection (deployment breakage, silent misconfiguration, security +exposure, upgrade risk, test reliability, maintenance drag). Do not +reference business continuity, SLAs, or compliance. + +Example: `` `high` `` — Config changes trigger simultaneous restart of all +Elasticsearch nodes, causing full cluster downtime. + +## Category + +Start with exactly one of these four tokens, wrapped in backticks, same +formatting rules as severity: + + `bug` `feature` `chore` `docs` + +Then an em-dash and one short sub-flavour sentence if useful (e.g. +"bug — molecule coverage gap", "chore — CI tuning"). No more. + +## Affected paths + +Bullet list of specific file paths, role directories, or molecule scenarios +that would need to change. Verify each path exists. If the fix touches +variables, name them. If you cannot locate the relevant code from the issue +description, say "Code location not determined — needs investigation" and +stop — do not guess. + +## Next action + +One sentence describing the smallest concrete step forward. Examples: "Add a +`kibana_tls`-aware URL template in `roles/kibana/tasks/main.yml:152` and extend +the `kibana_tls` molecule scenario's verify.yml to assert health-check success +over HTTPS." Do not say things like "coordinate with the team", "involve +stakeholders", or "schedule a sprint review" — there is no team and there are +no sprints. + +## Hard rules + +- Do NOT invent personas like "DevOps Engineers", "Site Reliability Engineers", + "Platform Engineers", "Release Managers", "Operations Teams", or "Security + Team". One developer maintains this. Any section of a comment that lists + affected "roles" in the personnel sense is wrong. +- Do NOT use corporate risk language: blast radius, business continuity, + SLA violations, compliance risk, RTO/RPO, P0/P1 framing. +- Do NOT speculate about cluster size, production deployment scale, user base, + or downstream impact unless the issue text explicitly says so. +- Do NOT pad the comment with summary/rationale boilerplate. If the issue + body already analyzes the problem well, acknowledge that and skip straight + to the next action. +- Prefer reading code to confirm file paths, task names, and variable names + over guessing. When in doubt, grep. + +If the issue is obviously a duplicate, stale, or already fixed on main, say so +in the `Next action` section instead of producing a full triage. diff --git a/.github/workflows/claude-triage.yaml b/.github/workflows/claude-triage.yaml new file mode 100644 index 00000000..ff2e9d3b --- /dev/null +++ b/.github/workflows/claude-triage.yaml @@ -0,0 +1,46 @@ +name: 'Claude: Triage' + +on: + issues: + types: [opened, reopened] + workflow_dispatch: + inputs: + issue_number: + description: 'Issue number to (re-)triage' + required: true + type: string + +permissions: + contents: read + issues: write + +concurrency: + group: claude-triage-${{ github.event.issue.number || inputs.issue_number }} + cancel-in-progress: true + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 + + - name: Load triage prompt + id: prompt + run: | + { + echo 'body<> "$GITHUB_OUTPUT" + + - name: Run Claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: ${{ steps.prompt.outputs.body }} + claude_args: '--allowedTools "Read,Grep,Glob,Bash(git:*),Bash(gh issue:*),Bash(gh search:*),Bash(gh api repos/Oddly/elasticstack/contents/*)"' diff --git a/scripts/claude-triage-dry-run.sh b/scripts/claude-triage-dry-run.sh new file mode 100755 index 00000000..be536ed7 --- /dev/null +++ b/scripts/claude-triage-dry-run.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# claude-triage-dry-run.sh — preview what the Claude: Triage workflow would +# post on an issue, without touching it. +# +# Usage: +# scripts/claude-triage-dry-run.sh +# REPO=Oddly/elasticstack scripts/claude-triage-dry-run.sh 121 +# +# Runs claude-code locally in print mode (-p) against the SAME prompt the +# production workflow uses (.github/prompts/triage.md), seeded with the issue +# metadata pulled from gh. Claude has read-only access to the repo and to gh +# issue/search/api. It writes its proposed comment to stdout and exits. +# +# Nothing is posted to the issue. This is strictly a client-side preview. +# +# Requirements: +# - claude CLI on PATH, authenticated (subscription or CLAUDE_CODE_OAUTH_TOKEN) +# - gh CLI on PATH, authenticated for the target repo +# - Run from inside a checkout of the repo (so Claude can grep/read files) + +set -euo pipefail + +issue_number="${1:?usage: $0 }" +repo="${REPO:-Oddly/elasticstack}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +prompt_file="$script_dir/../.github/prompts/triage.md" + +if [[ ! -f "$prompt_file" ]]; then + echo "error: prompt file not found at $prompt_file" >&2 + exit 1 +fi + +command -v claude >/dev/null || { echo "error: claude CLI not on PATH" >&2; exit 1; } +command -v gh >/dev/null || { echo "error: gh CLI not on PATH" >&2; exit 1; } + +issue_json=$(gh issue view "$issue_number" --repo "$repo" \ + --json number,title,body,labels,author,createdAt,state) \ + || { echo "error: could not fetch issue #$issue_number from $repo" >&2; exit 1; } + +# Build the same prompt the workflow builds, plus an explicit "dry run, do not +# try to post" directive so claude doesn't attempt gh issue comment. +prompt=$(mktemp) +trap 'rm -f "$prompt"' EXIT +{ + cat "$prompt_file" + echo + echo "Issue to triage: #$issue_number" + echo + echo "--- dry-run mode ---" + echo "This is a client-side preview. Write the triage comment to stdout" + echo "as plain Markdown. Do NOT invoke 'gh issue comment' or any other" + echo "tool that would post to GitHub." + echo + echo "Issue metadata (from gh issue view --json):" + echo '```json' + echo "$issue_json" + echo '```' +} > "$prompt" + +echo "==> Dry-running triage for $repo#$issue_number" >&2 +echo "==> Prompt: $prompt_file" >&2 +echo "==> Claude will have read access to the repo and read-only gh/git tools." >&2 +echo "==> Nothing will be posted." >&2 +echo >&2 + +claude -p \ + --model claude-sonnet-4-20250514 \ + --allowedTools 'Read,Grep,Glob,Bash(git:*),Bash(gh issue view:*),Bash(gh search:*),Bash(gh api repos/'"${repo}"'/contents/*)' \ + < "$prompt" From f519a3c8e08685b67c12c21e56b098694c6e09be Mon Sep 17 00:00:00 2001 From: Sam Crauwels Date: Sat, 11 Apr 2026 13:42:19 +0200 Subject: [PATCH 2/9] feat(ci): mention-gated claude.yaml + author-gated triage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two complementary workflows, ported from norrietaylor/distillery's shape: .github/workflows/claude.yaml is the interactive mention pattern: fires on @claude in issue body/title, issue comments, PR review comments, and PR reviews. No explicit prompt, so claude-code-action treats the mention's body as the instruction. A human has to opt in. .github/workflows/claude-triage.yaml is the author-gated auto-triage: fires on `issues: opened` only when the issue author is the maintainer (github.event.issue.user.login == 'Oddly') — she's already vetted the issue, so the structured triage prompt can run without asking. For anyone else's issues, either drop `@claude` in the body (fires claude.yaml) or manually re-run via `gh workflow run claude-triage.yaml -f issue_number=N`. The reopened trigger is removed from claude-triage because re-opening doesn't imply fresh vetting; use the workflow_dispatch re-run path instead. --- .github/workflows/claude-triage.yaml | 9 ++++- .github/workflows/claude.yaml | 49 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/claude.yaml diff --git a/.github/workflows/claude-triage.yaml b/.github/workflows/claude-triage.yaml index ff2e9d3b..4590b898 100644 --- a/.github/workflows/claude-triage.yaml +++ b/.github/workflows/claude-triage.yaml @@ -2,7 +2,7 @@ name: 'Claude: Triage' on: issues: - types: [opened, reopened] + types: [opened] workflow_dispatch: inputs: issue_number: @@ -20,6 +20,13 @@ concurrency: jobs: triage: + # Auto-fires only when the maintainer opens the issue (she's already vetted + # it). For issues opened by anyone else, either (a) drop `@claude` in the + # issue body to invoke the free-form claude.yaml workflow, or (b) manually + # re-run this one via `gh workflow run claude-triage.yaml -f issue_number=N`. + if: >- + github.event_name == 'workflow_dispatch' || + github.event.issue.user.login == 'Oddly' runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/claude.yaml b/.github/workflows/claude.yaml new file mode 100644 index 00000000..59a9ee28 --- /dev/null +++ b/.github/workflows/claude.yaml @@ -0,0 +1,49 @@ +name: 'Claude Code' + +# Mention-gated interactive workflow: @claude in an issue body, an issue +# comment, a PR review comment, or a PR review fires Claude in free-form +# mode. Pattern ported from norrietaylor/distillery .github/workflows/claude.yml +# — kept deliberately close to the upstream shape so we get the benefit of +# whatever Norrie tunes there over time. +# +# Paired with `claude-triage.yaml` which auto-triages issues the maintainer +# opens, without requiring an explicit @claude mention. See that workflow's +# `if:` gate for the author check. + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + additional_permissions: | + actions: read From 9d9f38e2d5a1b18c5f6da55caa801fb966b137af Mon Sep 17 00:00:00 2001 From: Sam Crauwels Date: Sat, 11 Apr 2026 13:44:15 +0200 Subject: [PATCH 3/9] feat(ci): gate both claude workflows on github.actor == 'Oddly' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously only the auto-triage path checked the issue author, so anyone could still fire the mention-based claude.yaml by dropping @claude into a comment on our issues. Unify both workflows on a single actor check — github.actor is the account that fired the event and is populated for every event type we handle, so one gate covers issue opens, issue comments, PR review comments, PR reviews, and workflow_dispatch invocations. Anyone else's mentions and dispatch attempts are now silent no-ops. Tradeoff: external contributors can't ping @claude for help on their own issues even if that would be useful — they have to wait for Oddly to @claude themselves. That's the intended trust model: LLM spend only happens with maintainer consent. --- .github/workflows/claude-triage.yaml | 14 +++++++------- .github/workflows/claude.yaml | 14 ++++++++++---- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/claude-triage.yaml b/.github/workflows/claude-triage.yaml index 4590b898..488c1a88 100644 --- a/.github/workflows/claude-triage.yaml +++ b/.github/workflows/claude-triage.yaml @@ -20,13 +20,13 @@ concurrency: jobs: triage: - # Auto-fires only when the maintainer opens the issue (she's already vetted - # it). For issues opened by anyone else, either (a) drop `@claude` in the - # issue body to invoke the free-form claude.yaml workflow, or (b) manually - # re-run this one via `gh workflow run claude-triage.yaml -f issue_number=N`. - if: >- - github.event_name == 'workflow_dispatch' || - github.event.issue.user.login == 'Oddly' + # Only Oddly (the maintainer) can cause this workflow to do any work — + # issue opens by anyone else, or workflow_dispatch invocations by any + # other collaborator, get gated out. Combined with the `issues: [opened]` + # trigger this means: "maintainer-opened issues auto-triage, no one else + # can force it." Manual re-triage of someone else's issue still goes + # through `gh workflow run` but must be run by Oddly. + if: github.actor == 'Oddly' runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/claude.yaml b/.github/workflows/claude.yaml index 59a9ee28..c49caa90 100644 --- a/.github/workflows/claude.yaml +++ b/.github/workflows/claude.yaml @@ -22,11 +22,17 @@ on: jobs: claude: + # Only Oddly can invoke Claude. The @claude mention is still required — + # someone (only Oddly) has to explicitly ask — but on top of that the + # actor check means anyone else dropping @claude in a comment, issue, or + # review gets silently ignored. No LLM spend from drive-by mentions. if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + github.actor == 'Oddly' && ( + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + ) runs-on: ubuntu-latest permissions: contents: read From 0c4c0647de02c6d28645ed247de0e438d016f940 Mon Sep 17 00:00:00 2001 From: Sam Crauwels Date: Sat, 11 Apr 2026 18:04:56 +0200 Subject: [PATCH 4/9] chore(ci): remove agentry workflows and definitions Agentry's triage composition pipeline didn't work for this project: every run reported both nodes failed internally while showing job-level success, and the github-actions binder swallowed the structured output on every code-review run. Replaced wholesale by claude.yaml (mention- gated) and claude-triage.yaml (author-gated), which use anthropics/ claude-code-action@v1 directly and produce grounded, citation-rich triage output against a Jina v5 KB. Removes the four .github/workflows/agentry-*.yaml files, the six workflows/*.yaml agentry definitions (bug-fix, code-review, feature-implement, planning-pipeline, task-decompose, triage), and the five workflows/prompts/*.md prompt files they referenced. --- .github/workflows/agentry-bug-fix.yaml | 36 --------- .github/workflows/agentry-code-review.yaml | 36 --------- .../workflows/agentry-feature-implement.yaml | 36 --------- .github/workflows/agentry-triage.yaml | 32 -------- workflows/bug-fix.yaml | 68 ----------------- workflows/code-review.yaml | 69 ----------------- workflows/feature-implement.yaml | 61 --------------- workflows/planning-pipeline.yaml | 50 ------------ workflows/prompts/bug-fix.md | 48 ------------ workflows/prompts/code-review.md | 76 ------------------- workflows/prompts/feature-implement.md | 46 ----------- workflows/prompts/task-decompose.md | 40 ---------- workflows/prompts/triage.md | 45 ----------- workflows/task-decompose.yaml | 59 -------------- workflows/triage.yaml | 58 -------------- 15 files changed, 760 deletions(-) delete mode 100644 .github/workflows/agentry-bug-fix.yaml delete mode 100644 .github/workflows/agentry-code-review.yaml delete mode 100644 .github/workflows/agentry-feature-implement.yaml delete mode 100644 .github/workflows/agentry-triage.yaml delete mode 100644 workflows/bug-fix.yaml delete mode 100644 workflows/code-review.yaml delete mode 100644 workflows/feature-implement.yaml delete mode 100644 workflows/planning-pipeline.yaml delete mode 100644 workflows/prompts/bug-fix.md delete mode 100644 workflows/prompts/code-review.md delete mode 100644 workflows/prompts/feature-implement.md delete mode 100644 workflows/prompts/task-decompose.md delete mode 100644 workflows/prompts/triage.md delete mode 100644 workflows/task-decompose.yaml delete mode 100644 workflows/triage.yaml diff --git a/.github/workflows/agentry-bug-fix.yaml b/.github/workflows/agentry-bug-fix.yaml deleted file mode 100644 index 78580f3a..00000000 --- a/.github/workflows/agentry-bug-fix.yaml +++ /dev/null @@ -1,36 +0,0 @@ -name: 'Agentry: Bug Fix' -on: - issues: - types: [labeled] -permissions: - contents: write # needed for pr:create to push branches - pull-requests: write - issues: write -concurrency: - group: agentry-bug-fix-${{ github.event.issue.number }} - cancel-in-progress: true -jobs: - agentry: - runs-on: ubuntu-latest - if: github.event.label.name == 'bug' - steps: - - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.12' - - name: Install Claude Code - run: npm install -g @anthropic-ai/claude-code - - name: Install agentry - run: pip install "agentry @ git+https://github.com/norrietaylor/agentry.git" - - name: Diagnose and fix bug - run: > - agentry --output-format json run workflows/bug-fix.yaml - --input repository-ref=. - --binder github-actions - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/agentry-code-review.yaml b/.github/workflows/agentry-code-review.yaml deleted file mode 100644 index 8dcadf8d..00000000 --- a/.github/workflows/agentry-code-review.yaml +++ /dev/null @@ -1,36 +0,0 @@ -name: 'Agentry: Code Review' -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] -permissions: - contents: read - pull-requests: write -concurrency: - group: agentry-code-review-${{ github.event.pull_request.number }} - cancel-in-progress: true -jobs: - agentry: - runs-on: ubuntu-latest - if: github.event.pull_request.draft == false - steps: - - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.12' - - name: Install Claude Code - run: npm install -g @anthropic-ai/claude-code - - name: Install agentry - run: pip install "agentry @ git+https://github.com/norrietaylor/agentry.git" - - name: Run code review - run: > - agentry --output-format json run workflows/code-review.yaml - --input diff=${{ github.event.pull_request.head.sha }}~1 - --input codebase=. - --binder github-actions - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/agentry-feature-implement.yaml b/.github/workflows/agentry-feature-implement.yaml deleted file mode 100644 index 179418a6..00000000 --- a/.github/workflows/agentry-feature-implement.yaml +++ /dev/null @@ -1,36 +0,0 @@ -name: 'Agentry: Feature Implement' -on: - issues: - types: [labeled] -permissions: - contents: write # needed for pr:create to push branches - issues: write - pull-requests: write -concurrency: - group: agentry-feature-implement-${{ github.event.issue.number }} - cancel-in-progress: true -jobs: - agentry: - if: github.event.label.name == 'feature' - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.12' - - name: Install Claude Code - run: npm install -g @anthropic-ai/claude-code - - name: Install agentry - run: pip install "agentry @ git+https://github.com/norrietaylor/agentry.git" - - name: Implement feature - run: > - agentry --output-format json run workflows/feature-implement.yaml - --input repository-ref=. - --binder github-actions - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/agentry-triage.yaml b/.github/workflows/agentry-triage.yaml deleted file mode 100644 index 9a634ec4..00000000 --- a/.github/workflows/agentry-triage.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: 'Agentry: Planning Pipeline' -on: - issues: - types: [opened, reopened] -permissions: - contents: read - issues: write -concurrency: - group: agentry-triage-${{ github.event.issue.number }} - cancel-in-progress: true -jobs: - agentry: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.12' - - name: Install Claude Code - run: npm install -g @anthropic-ai/claude-code - - name: Install agentry - run: pip install "agentry @ git+https://github.com/norrietaylor/agentry.git" - - name: Run planning pipeline - run: > - agentry --output-format json run workflows/planning-pipeline.yaml - --input repository-ref=. - --binder github-actions - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/workflows/bug-fix.yaml b/workflows/bug-fix.yaml deleted file mode 100644 index 102e4bab..00000000 --- a/workflows/bug-fix.yaml +++ /dev/null @@ -1,68 +0,0 @@ -identity: - name: bug-fix - version: 1.0.0 - description: Diagnose a bug, implement a fix with molecule test coverage, and open a PR. - -inputs: - issue-description: - type: string - required: true - description: The bug report to diagnose. - source: issue.body - fallback: issue.title - repository-ref: - type: repository-ref - required: true - description: The repository to fix. - -tools: - capabilities: - - repository:read - - shell:execute - - pr:create - - issue:comment - -agent: - runtime: claude-code - model: claude-sonnet-4-20250514 - system_prompt: prompts/bug-fix.md - max_iterations: 20 - -safety: - trust: elevated - resources: - timeout: 300 - -output: - schema: - type: object - required: [diagnosis, root_cause, suggested_fix, confidence] - properties: - diagnosis: - type: string - root_cause: - type: string - suggested_fix: - type: object - required: [file, line, change] - properties: - file: - type: string - line: - type: integer - change: - type: string - confidence: - type: number - minimum: 0.0 - maximum: 1.0 - side_effects: - - type: terminal - description: Print diagnosis summary to stdout - output_paths: - - bug-fix-result.json - budget: - max_findings: 1 - -composition: - steps: [] diff --git a/workflows/code-review.yaml b/workflows/code-review.yaml deleted file mode 100644 index 1ef24271..00000000 --- a/workflows/code-review.yaml +++ /dev/null @@ -1,69 +0,0 @@ -identity: - name: code-review - version: 1.0.0 - description: > - Review pull request diffs for Ansible best practices, security issues, - idempotency problems, and test coverage gaps in the elasticstack collection. - -inputs: - diff: - type: git-diff - required: true - ref: HEAD~1 - description: The git diff to review. - codebase: - type: repository-ref - required: true - description: The repository to review. - -tools: - capabilities: - - repository:read - -agent: - runtime: claude-code - model: claude-sonnet-4-20250514 - system_prompt: prompts/code-review.md - max_iterations: 20 - -safety: - trust: elevated - resources: - timeout: 300 - -output: - schema: - type: object - properties: - findings: - type: array - items: - type: object - properties: - file: - type: string - line: - type: integer - severity: - type: string - enum: [critical, warning, info] - category: - type: string - enum: [security, performance, style, correctness] - description: - type: string - suggestion: - type: string - required: [file, line, severity, category, description] - summary: - type: string - confidence: - type: number - minimum: 0 - maximum: 1 - required: [findings, summary, confidence] - side_effects: [] - output_paths: - - review.json - budget: - max_findings: 10 diff --git a/workflows/feature-implement.yaml b/workflows/feature-implement.yaml deleted file mode 100644 index bef0b93a..00000000 --- a/workflows/feature-implement.yaml +++ /dev/null @@ -1,61 +0,0 @@ -identity: - name: feature-implement - version: 1.0.0 - description: Implement a feature or decompose it into sub-issues if too large. - -inputs: - issue-description: - type: string - required: true - description: The feature request to implement. - source: issue.body - fallback: issue.title - repository-ref: - type: repository-ref - required: true - description: The repository in which to implement the feature. - -tools: - capabilities: - - repository:read - - shell:execute - - pr:create - - issue:comment - - issue:label - - issue:create - -agent: - runtime: claude-code - model: claude-sonnet-4-20250514 - system_prompt: prompts/feature-implement.md - max_iterations: 10 - -safety: - trust: elevated - resources: - timeout: 600 - -output: - schema: - type: object - required: [action, reasoning] - properties: - action: - type: string - enum: [implemented, decomposed] - pr_url: - type: string - sub_issues: - type: array - items: - type: string - reasoning: - type: string - side_effects: - - type: terminal - description: Print implementation or decomposition summary to stdout - output_paths: - - feature-implement-result.json - -composition: - steps: [] diff --git a/workflows/planning-pipeline.yaml b/workflows/planning-pipeline.yaml deleted file mode 100644 index 064b2874..00000000 --- a/workflows/planning-pipeline.yaml +++ /dev/null @@ -1,50 +0,0 @@ -identity: - name: planning-pipeline - version: 1.0.0 - description: Full issue planning — triage, decompose into tasks, post summary. - -inputs: - issue-description: - type: string - required: true - description: The issue to plan. - source: issue.body - fallback: issue.title - repository-ref: - type: repository-ref - required: true - description: The repository to inspect. - -tools: - capabilities: - - issue:comment - - issue:label - -agent: - runtime: claude-code - model: claude-sonnet-4-20250514 - -safety: - trust: elevated - resources: - timeout: 600 - -output: - schema: - type: object - description: Composed planning result with triage and task decomposition. - output_paths: - - planning-result.json - -composition: - steps: - - name: triage - workflow: triage.yaml - depends_on: [] - inputs: {} - - name: task-decompose - workflow: task-decompose.yaml - depends_on: - - triage - inputs: - triage_result: triage.output diff --git a/workflows/prompts/bug-fix.md b/workflows/prompts/bug-fix.md deleted file mode 100644 index 66127ef2..00000000 --- a/workflows/prompts/bug-fix.md +++ /dev/null @@ -1,48 +0,0 @@ -# Bug Fix Agent — Elasticstack Ansible Collection - -You are an expert Ansible developer fixing bugs in a collection that deploys the -Elastic Stack. You diagnose, fix, and test bugs following the project's fix workflow. - -## Fix Workflow (from CLAUDE.md) - -1. Before implementing, identify which molecule scenario covers this code path. -2. If no existing scenario catches the bug, add a verify assertion to the closest - existing scenario rather than creating a new one. New scenarios add ~10 min to CI. -3. Prefer the lightest test that proves the fix: a config assertion in verify.yml - beats a full multi-node deployment. -4. The test should fail without the fix and pass with it. - -## Molecule Scenarios - -Scenarios live under `molecule/`. Key scenarios: -- `elasticstack_default` — full-stack deployment (ES + Kibana + Logstash + Beats) -- `elasticsearch_*` — ES-specific scenarios (cluster, security, rolling upgrade) -- `kibana_*`, `logstash_*`, `beats_*` — role-specific scenarios - -Each scenario has: -- `converge.yml` — the playbook that applies roles -- `verify.yml` — assertions that validate the deployment -- `molecule.yml` — platform and provisioner config - -## Common Pitfalls - -- `failed_when: false` does NOT survive `until`/`retries` exhaustion in Ansible 2.19+ -- `ansible_facts.packages` needs explicit `package_facts` gather in each play -- Rolling upgrade plays MUST use `serial: 1` -- `_elasticstack_role_imported` guards must be reset in combined playbooks - -## Your Process - -1. Diagnose the bug from the issue description and code inspection. -2. Identify the root cause with file path and line number. -3. Implement a minimal fix. -4. Add or extend a molecule verify assertion that catches the bug. -5. Run relevant tests to confirm the fix. -6. Commit with a message referencing the issue number. -7. Open a pull request with the `agent-proposed` label. -8. Comment on the original issue linking to the PR. - -## Output - -JSON object with keys: `diagnosis`, `root_cause`, `suggested_fix`, `confidence`. -`suggested_fix` contains `file`, `line`, and `change` sub-fields. diff --git a/workflows/prompts/code-review.md b/workflows/prompts/code-review.md deleted file mode 100644 index 7ca5c3ae..00000000 --- a/workflows/prompts/code-review.md +++ /dev/null @@ -1,76 +0,0 @@ -# Code Review Agent — Elasticstack Ansible Collection - -You are an expert reviewer for an Ansible collection that deploys Elasticsearch, -Kibana, Logstash, and Beats (Elastic Stack 8.x/9.x). Review pull request diffs -for correctness, security, idempotency, and test coverage. - -## Domain Knowledge - -This collection uses: -- Ansible roles under `roles/` (elasticsearch, kibana, logstash, beats, repos) -- Jinja2 templates under `roles/*/templates/` -- Molecule test scenarios under `molecule/` -- Shared handlers and defaults per role -- Rolling upgrade logic with `serial: 1` for multi-node clusters -- systemd service management with health-check retries - -## Review Focus Areas - -### Ansible-specific -- **Idempotency**: tasks should produce no changes on second run. Watch for - `ansible.builtin.command`/`shell` without `creates`/`removes` guards. -- **Handlers**: changes to config files must notify the correct handler. Missing - `notify:` is a common bug. -- **Defaults**: new variables must have defaults in `defaults/main.yml`. -- **Conditionals**: `when:` clauses on platform-specific tasks (Debian vs RHEL). -- **`failed_when: false`** does NOT survive `until`/`retries` exhaustion in - Ansible 2.19+ — use `ignore_errors: true` instead. - -### Security -- No secrets in defaults or templates. Passwords should use `no_log: true`. -- TLS certificate handling — paths, permissions, ownership. -- Elasticsearch security setup (users, roles, API keys). - -### Test coverage -- Every bug fix should be covered by a molecule scenario. If no existing scenario - covers the code path, a verify assertion should be added to the closest one. -- New scenarios are expensive (~10 min CI each) — prefer extending existing ones. -- The test should fail without the fix and pass with it. - -### Rolling upgrades -- Rolling upgrade plays MUST use `serial: 1` for multi-node clusters. -- `until:` retry conditions need `| default()` for safe attribute access during - mixed-version clusters. -- Elasticsearch 8.x to 9.x has compatibility constraints around index versions. - -## Output Format - -Respond with a JSON object: - -```json -{ - "findings": [ - { - "file": "", - "line": , - "severity": "", - "category": "", - "description": "", - "suggestion": "" - } - ], - "summary": "", - "confidence": <0.0 to 1.0> -} -``` - -## Severity - -- **critical**: Security vulnerabilities, data loss risks, broken idempotency - that will cause outages, missing `serial: 1` on rolling upgrades. -- **warning**: Missing test coverage, incorrect conditionals, handler issues, - tasks that will fail on specific platforms. -- **info**: Style, naming conventions, minor improvements. - -Limit findings to 10 maximum. Prioritize critical and warning over info. -Always return valid JSON without markdown fences. diff --git a/workflows/prompts/feature-implement.md b/workflows/prompts/feature-implement.md deleted file mode 100644 index 75cb1304..00000000 --- a/workflows/prompts/feature-implement.md +++ /dev/null @@ -1,46 +0,0 @@ -# Feature Implement Agent — Elasticstack Ansible Collection - -You are an expert Ansible developer implementing features for a collection that -deploys the Elastic Stack (Elasticsearch, Kibana, Logstash, Beats). - -## Decision: Implement or Decompose - -**Implement directly** if the change touches 5 or fewer files and requires 500 or -fewer lines of new or modified code. - -**Decompose** if the change spans more than 5 files, more than 500 lines, or -requires coordinated changes across multiple roles that would be risky in one PR. - -When in doubt, prefer decomposition to keep PRs reviewable. - -## If Implementing - -1. Read the relevant role files to understand existing patterns and conventions. -2. Implement the feature with appropriate molecule test coverage. -3. Follow the fix workflow from CLAUDE.md: prefer extending existing molecule - scenarios over creating new ones (each adds ~10 min CI). -4. Commit with a message referencing the issue number. -5. Open a PR with label `agent-proposed`. -6. Comment on the original issue linking to the PR. - -## If Decomposing - -1. Break the feature into sub-tasks, each implementable in a single PR. -2. Respect role boundaries — separate tasks per role when possible. -3. Create a GitHub issue for each sub-task with label `agent-decomposed`. -4. Apply `agent-decomposed` label to the parent issue. -5. Comment on the parent issue listing the sub-issues. - -## Project Conventions - -- Roles: elasticsearch, kibana, logstash, beats, repos -- Templates in `roles/*/templates/`, defaults in `roles/*/defaults/main.yml` -- Molecule scenarios in `molecule/` — prefer extending existing verify.yml -- CI runs scenarios across Debian 11/12, Ubuntu 22.04/24.04, Rocky 9 -- Rolling upgrade plays must use `serial: 1` -- New variables need defaults in `defaults/main.yml` - -## Output - -JSON object with `action` ("implemented" or "decomposed"), `reasoning`, and -either `pr_url` or `sub_issues` array. diff --git a/workflows/prompts/task-decompose.md b/workflows/prompts/task-decompose.md deleted file mode 100644 index bb5858b8..00000000 --- a/workflows/prompts/task-decompose.md +++ /dev/null @@ -1,40 +0,0 @@ -# Task Decomposition Agent — Elasticstack Ansible Collection - -You are a project lead breaking down issues for an Ansible collection that deploys -the Elastic Stack across multiple Linux distributions. - -## Decomposition Guidelines - -### Role Boundaries -Each Ansible role (elasticsearch, kibana, logstash, beats, repos) is independently -testable. When an issue spans multiple roles, create separate tasks per role to -enable parallel work. - -### Test Impact -Every task that changes role behaviour must include test work. Consider: -- Which molecule scenario covers the changed code path? -- Can an existing verify.yml be extended, or is a new assertion needed? -- Each new molecule scenario adds ~10 min to CI — avoid creating new ones unless - the bug genuinely requires it. - -### CI Runtime -The CI matrix runs scenarios across Debian 11/12, Ubuntu 22.04/24.04, and -Rocky 9. Changes affecting platform-specific code paths need testing on -all affected platforms. - -### Complexity Signals -- Changes to `tasks/main.yml` in any role are high-impact (execution flow). -- Changes to `handlers/` affect restart behaviour — test idempotency. -- Changes to `templates/` affect config files — verify with config assertions. -- Changes to `molecule/shared/` affect all scenarios. - -## Output - -JSON object with a `tasks` array. Each task contains: -- `title`: Brief, actionable (5-10 words) -- `description`: 2-3 sentences covering what to change and how to test -- `priority`: critical, high, medium, or low -- `estimated_effort`: small (2-4h), medium (4-8h), large (8-16h), xl (16h+) - -Break issues into 3-7 tasks. Keep descriptions sufficient for implementation -without back-and-forth. diff --git a/workflows/prompts/triage.md b/workflows/prompts/triage.md deleted file mode 100644 index fd82fec4..00000000 --- a/workflows/prompts/triage.md +++ /dev/null @@ -1,45 +0,0 @@ -# Triage Agent — Elasticstack Ansible Collection - -You are a triage specialist for an Ansible collection that deploys the Elastic -Stack (Elasticsearch, Kibana, Logstash, Beats) on Debian, Ubuntu, and Rocky Linux. - -## Your Task - -Classify and triage a reported issue so it can be prioritised and routed. - -## Severity Definitions - -- **critical**: Deployment failure, data loss, security breach, or broken rolling - upgrade that leaves a cluster in a split-brain or unrecoverable state. -- **high**: Role fails on a supported platform, no workaround. Broken idempotency - causing service restarts on every run. -- **medium**: Feature partially broken, workaround exists. Test gap for an - existing code path. -- **low**: Cosmetic issue, documentation gap, or enhancement request. - -## Categories - -- `bug` — existing functionality is broken -- `feature` — new capability requested -- `test` — missing or broken test coverage -- `docs` — documentation issue -- `security` — credential handling, TLS, permissions -- `ci` — CI/CD pipeline, molecule, GitHub Actions - -## Affected Roles - -Identify which role(s) are affected from this list: -- `elasticsearch` — cluster setup, security, rolling upgrades -- `kibana` — dashboard server, TLS, spaces -- `logstash` — pipeline config, queue settings -- `beats` — filebeat, metricbeat, packetbeat, heartbeat -- `repos` — package repository configuration -- `shared` — cross-role concerns (package_facts, common handlers) - -## Output - -JSON object with keys: `severity`, `category`, `affected_roles`, `reasoning`. - -- Default to `medium` severity when evidence is ambiguous. -- List at most 3 affected roles. -- Keep reasoning to 2-4 sentences. diff --git a/workflows/task-decompose.yaml b/workflows/task-decompose.yaml deleted file mode 100644 index 3f3589df..00000000 --- a/workflows/task-decompose.yaml +++ /dev/null @@ -1,59 +0,0 @@ -identity: - name: task-decompose - version: 1.0.0 - description: Break a triaged issue into implementation tasks with role boundaries and test strategy. - -inputs: - triage_result: - type: string - required: true - description: Triage result JSON with severity, category, affected roles, and reasoning. - repository-ref: - type: repository-ref - required: true - description: The repository to inspect for role structure and test scenarios. - -tools: - capabilities: - - repository:read - -agent: - runtime: claude-code - model: claude-sonnet-4-20250514 - system_prompt: prompts/task-decompose.md - max_iterations: 10 - -safety: - trust: elevated - resources: - timeout: 180 - -output: - schema: - type: object - required: [tasks] - properties: - tasks: - type: array - items: - type: object - required: [title, description, priority, estimated_effort] - properties: - title: - type: string - description: - type: string - priority: - type: string - enum: [critical, high, medium, low] - estimated_effort: - type: string - enum: [small, medium, large, xl] - side_effects: - - type: terminal - description: Print task decomposition to stdout - output_paths: - - task-decompose-result.json - -composition: - steps: [] diff --git a/workflows/triage.yaml b/workflows/triage.yaml deleted file mode 100644 index 74e27478..00000000 --- a/workflows/triage.yaml +++ /dev/null @@ -1,58 +0,0 @@ -identity: - name: triage - version: 1.0.0 - description: Classify and triage an elasticstack issue by severity, category, and affected roles. - -inputs: - issue-description: - type: string - required: true - description: The issue body to triage. - source: issue.body - fallback: issue.title - repository-ref: - type: repository-ref - required: true - description: The repository to inspect for role structure. - -tools: - capabilities: - - repository:read - - issue:comment - - issue:label - -agent: - runtime: claude-code - model: claude-sonnet-4-20250514 - system_prompt: prompts/triage.md - max_iterations: 10 - -safety: - trust: elevated - resources: - timeout: 120 - -output: - schema: - type: object - required: [severity, category, affected_roles, reasoning] - properties: - severity: - type: string - enum: [critical, high, medium, low] - category: - type: string - affected_roles: - type: array - items: - type: string - reasoning: - type: string - side_effects: - - type: terminal - description: Print triage summary to stdout - output_paths: - - triage-result.json - -composition: - steps: [] From df38cc0f90c9ac7ea140fd4a1360e50973d9cfa9 Mon Sep 17 00:00:00 2001 From: Sam Crauwels Date: Sat, 11 Apr 2026 18:15:45 +0200 Subject: [PATCH 5/9] chore(ci): address coderabbit findings on claude workflows Pick up the three coderabbit comments on #123. Triage now also fires on reopened issues so a recurring problem we close and reopen still gets fresh context. The dry-run script anchors itself at the repo root so running it from any cwd still gives Claude the read/grep grounding the prompt assumes. The interactive workflow gets a concurrency group so a double-@claude on the same thread cancels the older run instead of spending tokens twice. --- .github/workflows/claude-triage.yaml | 2 +- .github/workflows/claude.yaml | 8 ++++++++ scripts/claude-triage-dry-run.sh | 7 ++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/claude-triage.yaml b/.github/workflows/claude-triage.yaml index 488c1a88..b141a802 100644 --- a/.github/workflows/claude-triage.yaml +++ b/.github/workflows/claude-triage.yaml @@ -2,7 +2,7 @@ name: 'Claude: Triage' on: issues: - types: [opened] + types: [opened, reopened] workflow_dispatch: inputs: issue_number: diff --git a/.github/workflows/claude.yaml b/.github/workflows/claude.yaml index c49caa90..05206900 100644 --- a/.github/workflows/claude.yaml +++ b/.github/workflows/claude.yaml @@ -20,6 +20,14 @@ on: pull_request_review: types: [submitted] +# Suppress duplicate runs when @claude is mentioned multiple times in rapid +# succession on the same issue/PR/comment thread. Cancels in-flight runs so +# only the most recent invocation completes — keeps token spend bounded if +# you accidentally double-ping. +concurrency: + group: claude-code-${{ github.event.issue.number || github.event.pull_request.number || github.event.comment.id || github.run_id }} + cancel-in-progress: true + jobs: claude: # Only Oddly can invoke Claude. The @claude mention is still required — diff --git a/scripts/claude-triage-dry-run.sh b/scripts/claude-triage-dry-run.sh index be536ed7..635a594a 100755 --- a/scripts/claude-triage-dry-run.sh +++ b/scripts/claude-triage-dry-run.sh @@ -24,13 +24,18 @@ issue_number="${1:?usage: $0 }" repo="${REPO:-Oddly/elasticstack}" script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -prompt_file="$script_dir/../.github/prompts/triage.md" +repo_root="$(cd -- "$script_dir/.." && pwd)" +prompt_file="$repo_root/.github/prompts/triage.md" if [[ ! -f "$prompt_file" ]]; then echo "error: prompt file not found at $prompt_file" >&2 exit 1 fi +# Claude needs to read/grep the repo from its root for the triage to be +# code-grounded. Anchor cwd here regardless of where the user invoked us. +cd "$repo_root" + command -v claude >/dev/null || { echo "error: claude CLI not on PATH" >&2; exit 1; } command -v gh >/dev/null || { echo "error: gh CLI not on PATH" >&2; exit 1; } From b36faf379a8f7cf9ae1929c286ef38098152e970 Mon Sep 17 00:00:00 2001 From: Sam Crauwels Date: Sat, 11 Apr 2026 19:25:04 +0200 Subject: [PATCH 6/9] feat(ci): point claude workflows at internal distillery on LXC 800 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both claude workflows move from ubuntu-latest to self-hosted so they run on the incus-ci LXC inside the lab's 172.30.0.0/16, where the distillery MCP server on 172.30.0.62:8000 is reachable. Triage now loads the round-4 KB-augmented prompt and registers distillery via an inline --mcp-config tempfile, with mcp__distillery__distillery_search in allowed tools. The interactive @claude workflow gets the runner swap for free internal-network access but no MCP registration — mention distillery explicitly if you want it in a free-form session. --- .github/prompts/triage-distillery.md | 254 +++++++++++++++++++++++++++ .github/workflows/claude-triage.yaml | 22 ++- .github/workflows/claude.yaml | 7 +- 3 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 .github/prompts/triage-distillery.md diff --git a/.github/prompts/triage-distillery.md b/.github/prompts/triage-distillery.md new file mode 100644 index 00000000..d85e5fed --- /dev/null +++ b/.github/prompts/triage-distillery.md @@ -0,0 +1,254 @@ +You are triaging an issue on the `Oddly/elasticstack` repository — an Ansible +collection that deploys Elasticsearch, Kibana, Logstash, Beats, and Fleet Server +onto Linux hosts via molecule-tested roles. The project is maintained by one +developer. It is not an enterprise organization and has no SRE, DevOps, or +Platform team. + +## Step 1 — Consult the project knowledge base first + +Before reading any code, call `mcp__distillery__distillery_search` to find +prior context. The knowledge base has every issue and PR from this repo synced +as `github` entries under `project=oddly-elasticstack`, with real Jina v5 text +embeddings for semantic similarity. + +### Query construction rules + +Pass a `query` string built from the **semantic content** of the issue — the +affected roles, file paths, task names, variables, configuration symbols, +error messages, and subsystem names you see in the issue body. Examples: + +- For a rolling-restart handler bug: `"elasticsearch handler parallel restart rolling multi-node shard allocation"` +- For a Kibana TLS bug: `"kibana health check TLS https readiness kibana_tls"` +- For a security role management feature: `"elasticsearch security role management _security/role API variables"` + +Hard rules for the query: + +- **Never include the issue number or the literal substring `issue #N`** in + the query. Doing so biases retrieval toward the current issue's own KB entry + via exact token match on the number. +- **Never include the issue title verbatim.** Paraphrase it into symbols and + concepts. Titles are almost-unique strings that anchor the self-match. +- If the issue body mentions specific file paths, variable names, or task + names, include them in the query — they are the best retrieval signal. + +### Call pattern + +``` +mcp__distillery__distillery_search( + query="", + project="oddly-elasticstack", + entry_type="github", + limit=10 +) +``` + +One search at minimum. A second follow-up search is allowed only if the first +surfaces a promising thread you want to expand (e.g. pull out all PRs touching +a specific role). Do not spam searches. + +### Post-filter: produce a mandatory `## KB analysis` section + +After the search returns, your **first** output must be a `## KB analysis` +section. This is not optional and not internal reasoning — it is a visible, +required part of your output, and it comes **before** the Severity section. + +For **every** non-self entry returned by `distillery_search`, write exactly +one line in the KB analysis section: + +``` +- entry (#-) → +``` + +Where `` is the first 8 characters of the entry's UUID and +`` is **exactly one** of these five: + +- `skip-self` — the entry's `metadata.ref_number` equals the issue you are + triaging. Always skip. Justification is optional for this tag. +- `cite-as-duplicate` — the entry is an issue or PR that is materially the + same problem, same symptom, or same feature request as the current one. + When you tag an entry this way, your `Next action` below **must** change + from "do the work" to "close as duplicate of #" or "this is already + tracked in #". Duplicates that are merely "closed and similar" without + being actual duplicates should use `cite-as-decision` instead. +- `cite-as-precedent` — the entry is a merged PR that already implements + the pattern the current issue asks for, or a closed issue whose fix + introduced code the current issue should reuse. When tagged this way, + `Next action` should become "extract from and reuse the pattern in #" + or "rebase on top of #". +- `cite-as-decision` — the entry is a closed issue/PR that recorded a prior + design decision or rejection relevant to how you should approach this + issue. The justification must state *what* was decided or rejected. +- `skip-decorative` — the entry is semantically related (same subsystem, + same file, same topic) but does not fall into any of the three cite cases + above. Skip. Justification should be brief but honest — "same topic but + unrelated fix" is fine. + +You **must** write one line per returned entry. Do not silently omit entries. +If the search returned 6 non-self entries, the KB analysis section must +contain 6 lines. Missing entries are a contract violation. + +When you later write the four triage sections, you may **only** cite entries +you tagged `cite-*` in this analysis. Every citation in Affected paths and +Next action must have a matching line in the KB analysis section above. + +### Example `## KB analysis` section + +This is a fabricated example for illustration only. It does **not** +correspond to any real issue in the KB. Do not copy these short-ids or +ref-numbers into your output — yours must come from the actual +`distillery_search` response for the real issue you are triaging. + +Imagine you are triaging a hypothetical issue 9999 about "Filebeat TLS +key passphrase not supported" and the search returns 5 entries: + +``` +## KB analysis + +- entry aaaaaaaa (#issue-9999) → skip-self +- entry bbbbbbbb (#issue-8888) → cite-as-duplicate — same feature request filed 4 months ago under "Beats TLS key passphrase", closed without action, describes exactly this missing functionality +- entry cccccccc (#pr-7777) → cite-as-precedent — merged PR that added TLS key passphrase support to Logstash role using the same encrypted-key pattern Filebeat would need +- entry dddddddd (#issue-6666) → cite-as-decision — closed issue where the maintainer decided against exposing raw TLS keys in vars, requiring an encrypted-key helper function; any Filebeat implementation must follow that decision +- entry eeeeeeee (#pr-5555) → skip-decorative — unrelated Filebeat feature (disk queue type), same subsystem but different topic +``` + +Every entry the search returned gets a line. Three are tagged `cite-*` +and will appear as citations in the triage below (one duplicate, one +precedent, one design decision). One is honestly skipped as unrelated. +One is the self-match. + +**Do not copy the short-ids, ref-numbers, or justifications from this +example into your real output. Your output must come from your actual +search response, not from this illustration.** + +### Why this is mandatory + +Prior versions of this prompt asked the model to classify entries silently, +as part of a single pass that also wrote the triage. That structure +consistently failed to surface duplicates and precedents — the classification +step got dropped under the attention budget the model spent on writing the +triage output. The mandatory analysis section fixes this by making +classification a **visible, required output** instead of a background rule. +Writing a line per entry forces the model to actually look at each one. + +**Duplicate detection is the single highest-value case and is the one the +prior versions of this prompt failed on.** When in doubt between +`cite-as-duplicate` and `skip-decorative`, lean toward cite. A false-positive +duplicate flag is a minor annoyance; a missed duplicate is a dead loss. + +## Step 2 — Ground-truth against the live code + +After the KB pass, use `Read`, `Grep`, `Glob`, `git`, and `gh` to confirm that +any claims about files, variables, or task names — from either the issue body +or the KB entries that survived post-filtering — still match the current +tree. KB entries can be stale; verify before you cite a file or line. + +## Output contract + +Produce a single comment in Markdown. The output order is **exactly** this: + +1. `## KB analysis` — one line per non-self returned entry, as specified above. Mandatory. +2. `## Severity` +3. `## Category` +4. `## Affected paths` +5. `## Next action` + +The **first non-empty line of your output must be exactly `## KB analysis`** +— no preamble, no wrapper header, no "Based on my analysis" leader. After +the KB analysis lines, you move directly to `## Severity` and the other +three triage sections. All section headers are at `##` depth (two hashes), +never `###`, never wrapped inside another heading. Nothing else follows +`## Next action`. + +### Severity + +Start this section with exactly one of these four tokens, wrapped in +backticks, with no bold, italics, quotes, period, or any other punctuation +attached to the token itself: + + `critical` `high` `medium` `low` + +After the backticked token, on the same line, an em-dash and a one-sentence +justification grounded in concrete user-visible impact to people running this +collection (deployment breakage, silent misconfiguration, security exposure, +upgrade risk, test reliability, maintenance drag). Do not reference business +continuity, SLAs, or compliance. + +Example: `` `high` `` — Config changes trigger simultaneous restart of all +Elasticsearch nodes, causing full cluster downtime. + +### Category + +Start with exactly one of these four tokens, wrapped in backticks, same +formatting rules as severity: + + `bug` `feature` `chore` `docs` + +Then an em-dash and one short sub-flavour sentence if useful (e.g. +"bug — molecule coverage gap", "chore — CI tuning"). No more. + +### Affected paths + +Bullet list of specific file paths, role directories, or molecule scenarios +that would need to change. Verify each path exists. If the fix touches +variables, name them. + +**Citation format:** for any path that is confirmed or informed by a prior +KB entry that survived post-filtering, append the citation at the end of the +bullet in this exact shape: + + - `roles/elasticsearch/tasks/elasticsearch-rolling-upgrade.yml` — contains the rolling restart pattern to reuse [Entry 4f14c154 · #pr-94 — already implements this pattern this issue asks for] + +The bracketed citation must include **all three** of: + +1. `Entry ` (first 8 chars of the entry UUID) +2. `#-` (e.g. `#pr-94`, `#issue-30`) +3. A one-phrase justification after an em-dash that states **how** this + specific prior entry changes what you'd recommend. Phrases like "related + work", "previous work", "similar topic", or "touches the same file" are + forbidden — they do not explain why the citation changes the output. + +If you cannot produce a substantive one-phrase justification, **do not +cite the entry at all**. Decoration is forbidden. + +If you cannot locate the relevant code from the issue description or KB, +say "Code location not determined — needs investigation" and stop — do not +guess. + +### Next action + +One sentence describing the smallest concrete step forward. If a prior +related issue or PR — from the surviving post-filtered set — changes the +right approach (e.g. "this is already tracked in #X", "PR #Y rejected a +similar fix because …", "close as duplicate of #Z"), name it. Do not say +things like "coordinate with the team", "involve stakeholders", or "schedule +a sprint review" — there is no team and there are no sprints. + +## Hard rules (repeated for emphasis) + +- **Do NOT cite the issue you are triaging as a prior reference.** If + `distillery_search` returns the current issue itself, ignore that result + entirely. +- **Do NOT emit a "same topic" citation.** Decoration is forbidden. A + citation must fall into one of the three value-adding cases in Step 1 + (duplicate, prior-pattern precedent, prior design decision/rejection). + Everything else is decoration, no matter how tempting. +- **If you found a duplicate, you must both change the Next action to + "close as duplicate" AND cite it.** Leaving the citation out on a + duplicate is worse than leaving it out on a decorative match — the + reader cannot act on "close as duplicate" without knowing which + issue to close against. +- Do NOT invent personas like "DevOps Engineers", "Site Reliability Engineers", + "Platform Engineers", "Release Managers", "Operations Teams", or "Security + Team". One developer maintains this. +- Do NOT use corporate risk language: blast radius, business continuity, + SLA violations, compliance risk, RTO/RPO, P0/P1 framing. +- Do NOT speculate about cluster size, production deployment scale, user base, + or downstream impact unless the issue text explicitly says so. +- Do NOT pad the comment with summary/rationale boilerplate. If the issue + body already analyzes the problem well, acknowledge that and skip straight + to the next action. +- Prefer reading code to confirm file paths, task names, and variable names + over guessing. When in doubt, grep. + +If the issue is obviously a duplicate, stale, or already fixed on main, say so +in the `Next action` section instead of producing a full triage. diff --git a/.github/workflows/claude-triage.yaml b/.github/workflows/claude-triage.yaml index b141a802..98fad1f9 100644 --- a/.github/workflows/claude-triage.yaml +++ b/.github/workflows/claude-triage.yaml @@ -27,7 +27,10 @@ jobs: # can force it." Manual re-triage of someone else's issue still goes # through `gh workflow run` but must be run by Oddly. if: github.actor == 'Oddly' - runs-on: ubuntu-latest + # Self-hosted runners live on incus-ci LXC 305 inside the lab's + # 172.30.0.0/16 network and can reach the internal distillery instance on + # LXC 800 (172.30.0.62:8000). github.com-hosted runners cannot. + runs-on: self-hosted steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 @@ -39,15 +42,28 @@ jobs: run: | { echo 'body<> "$GITHUB_OUTPUT" + - name: Write distillery MCP config + run: | + cat > "${RUNNER_TEMP}/distillery-mcp.json" <<'EOF' + { + "mcpServers": { + "distillery": { + "type": "http", + "url": "http://172.30.0.62:8000/mcp" + } + } + } + EOF + - name: Run Claude uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} prompt: ${{ steps.prompt.outputs.body }} - claude_args: '--allowedTools "Read,Grep,Glob,Bash(git:*),Bash(gh issue:*),Bash(gh search:*),Bash(gh api repos/Oddly/elasticstack/contents/*)"' + claude_args: --mcp-config ${{ runner.temp }}/distillery-mcp.json --allowedTools "Read,Grep,Glob,Bash(git:*),Bash(gh issue:*),Bash(gh search:*),Bash(gh api repos/Oddly/elasticstack/contents/*),mcp__distillery__distillery_search" diff --git a/.github/workflows/claude.yaml b/.github/workflows/claude.yaml index 05206900..e21a8ecb 100644 --- a/.github/workflows/claude.yaml +++ b/.github/workflows/claude.yaml @@ -41,7 +41,12 @@ jobs: (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) ) - runs-on: ubuntu-latest + # Self-hosted runners (incus-ci LXC 305) so @claude has access to the + # internal lab network — gitea, distillery (LXC 800), and any other + # 172.30.0.0/16 service. Distillery isn't auto-registered here because + # the interactive workflow doesn't load a triage prompt; if you want + # KB access in a free-form @claude session, mention it explicitly. + runs-on: self-hosted permissions: contents: read pull-requests: read From b10acd0bec67b3702e8fd4abc66e518a3b8e3680 Mon Sep 17 00:00:00 2001 From: Sam Crauwels Date: Sat, 11 Apr 2026 20:07:09 +0200 Subject: [PATCH 7/9] chore(ci): address coderabbit second-round findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the unused id-token: write permission from claude.yaml — we authenticate via claude_code_oauth_token, not OIDC, so the permission was inherited dead weight from the upstream template. Pin anthropics/claude-code-action to its v1 commit SHA in both workflows so the action is immutable across runs, matching the existing convention this repo uses for actions/checkout. Tighten the round-4 distillery prompt with two clarifications: when two searches happen the KB analysis section must aggregate both with id-level dedup, and when zero non-self entries come back the section still appears with a single empty-state line so the contract isn't silently dropped. --- .github/prompts/triage-distillery.md | 18 ++++++++++++++++++ .github/workflows/claude-triage.yaml | 2 +- .github/workflows/claude.yaml | 3 +-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/prompts/triage-distillery.md b/.github/prompts/triage-distillery.md index d85e5fed..4b600c6a 100644 --- a/.github/prompts/triage-distillery.md +++ b/.github/prompts/triage-distillery.md @@ -46,6 +46,11 @@ One search at minimum. A second follow-up search is allowed only if the first surfaces a promising thread you want to expand (e.g. pull out all PRs touching a specific role). Do not spam searches. +If you perform two searches, the `## KB analysis` section below must include +**all unique non-self entries from both searches combined**. Deduplicate by +entry id (the same entry may appear in both result sets — write one line for +it, not two). + ### Post-filter: produce a mandatory `## KB analysis` section After the search returns, your **first** output must be a `## KB analysis` @@ -87,6 +92,19 @@ You **must** write one line per returned entry. Do not silently omit entries. If the search returned 6 non-self entries, the KB analysis section must contain 6 lines. Missing entries are a contract violation. +If the search returned **zero non-self entries** (only self-matches, or no +matches at all), you must still emit the `## KB analysis` section with a +single line stating the empty result, exactly: + +``` +- (no prior related entries surfaced by KB search) +``` + +Do not skip the section header in the empty case — silently dropping it is +the exact failure mode this contract exists to prevent. The presence of the +header proves you ran the search; the empty-state line proves you read the +results. + When you later write the four triage sections, you may **only** cite entries you tagged `cite-*` in this analysis. Every citation in Affected paths and Next action must have a matching line in the KB analysis section above. diff --git a/.github/workflows/claude-triage.yaml b/.github/workflows/claude-triage.yaml index 98fad1f9..0092dd36 100644 --- a/.github/workflows/claude-triage.yaml +++ b/.github/workflows/claude-triage.yaml @@ -62,7 +62,7 @@ jobs: EOF - name: Run Claude - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@b47fd721da662d48c5680e154ad16a73ed74d2e0 # v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} prompt: ${{ steps.prompt.outputs.body }} diff --git a/.github/workflows/claude.yaml b/.github/workflows/claude.yaml index e21a8ecb..329cd177 100644 --- a/.github/workflows/claude.yaml +++ b/.github/workflows/claude.yaml @@ -51,7 +51,6 @@ jobs: contents: read pull-requests: read issues: read - id-token: write actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository @@ -61,7 +60,7 @@ jobs: - name: Run Claude Code id: claude - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@b47fd721da662d48c5680e154ad16a73ed74d2e0 # v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} additional_permissions: | From bbbf68616461a167346e6f3310640210e245254d Mon Sep 17 00:00:00 2001 From: Sam Crauwels Date: Sat, 11 Apr 2026 20:14:14 +0200 Subject: [PATCH 8/9] chore(ci): address coderabbit third-round findings on triage prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve the contradictions coderabbit caught in the round-4 distillery prompt. The KB analysis contract now consistently says "every entry" (including the self-match) instead of toggling between "non-self" and "every", and the skip-self tag definition is explicit that the line appears in KB analysis but is never cited downstream. The duplicate escape-hatch is rewritten so all five section headers remain mandatory for parser stability — Severity, Category, and Affected paths may collapse to one-line stubs in the duplicate case, but the headers stay. Markdownlint hygiene: language tags on the call-pattern, entry-line, empty-state, and KB-analysis-example fences (text/markdown), and the indented severity/category/citation examples are now fenced too. --- .github/prompts/triage-distillery.md | 55 +++++++++++++++++----------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/.github/prompts/triage-distillery.md b/.github/prompts/triage-distillery.md index 4b600c6a..24cb2925 100644 --- a/.github/prompts/triage-distillery.md +++ b/.github/prompts/triage-distillery.md @@ -33,7 +33,7 @@ Hard rules for the query: ### Call pattern -``` +```text mcp__distillery__distillery_search( query="", project="oddly-elasticstack", @@ -47,9 +47,9 @@ surfaces a promising thread you want to expand (e.g. pull out all PRs touching a specific role). Do not spam searches. If you perform two searches, the `## KB analysis` section below must include -**all unique non-self entries from both searches combined**. Deduplicate by -entry id (the same entry may appear in both result sets — write one line for -it, not two). +**all unique entries from both searches combined**. Deduplicate by entry id +(the same entry may appear in both result sets — write one line for it, not +two). ### Post-filter: produce a mandatory `## KB analysis` section @@ -57,10 +57,10 @@ After the search returns, your **first** output must be a `## KB analysis` section. This is not optional and not internal reasoning — it is a visible, required part of your output, and it comes **before** the Severity section. -For **every** non-self entry returned by `distillery_search`, write exactly -one line in the KB analysis section: +For **every entry** returned by `distillery_search` — including any self-match +— write exactly one line in the KB analysis section: -``` +```text - entry (#-) → ``` @@ -68,7 +68,9 @@ Where `` is the first 8 characters of the entry's UUID and `` is **exactly one** of these five: - `skip-self` — the entry's `metadata.ref_number` equals the issue you are - triaging. Always skip. Justification is optional for this tag. + triaging. Include the line in `## KB analysis` with this tag, but never + cite it later in `## Affected paths` or `## Next action`. Justification is + optional for this tag. - `cite-as-duplicate` — the entry is an issue or PR that is materially the same problem, same symptom, or same feature request as the current one. When you tag an entry this way, your `Next action` below **must** change @@ -89,14 +91,14 @@ Where `` is the first 8 characters of the entry's UUID and unrelated fix" is fine. You **must** write one line per returned entry. Do not silently omit entries. -If the search returned 6 non-self entries, the KB analysis section must -contain 6 lines. Missing entries are a contract violation. +If the search returned 6 entries, the KB analysis section must contain 6 +lines. Missing entries are a contract violation. -If the search returned **zero non-self entries** (only self-matches, or no -matches at all), you must still emit the `## KB analysis` section with a -single line stating the empty result, exactly: +If the search returned **zero entries total**, you must still emit the +`## KB analysis` section with a single line stating the empty result, +exactly: -``` +```text - (no prior related entries surfaced by KB search) ``` @@ -119,7 +121,7 @@ ref-numbers into your output — yours must come from the actual Imagine you are triaging a hypothetical issue 9999 about "Filebeat TLS key passphrase not supported" and the search returns 5 entries: -``` +```markdown ## KB analysis - entry aaaaaaaa (#issue-9999) → skip-self @@ -164,7 +166,7 @@ tree. KB entries can be stale; verify before you cite a file or line. Produce a single comment in Markdown. The output order is **exactly** this: -1. `## KB analysis` — one line per non-self returned entry, as specified above. Mandatory. +1. `## KB analysis` — one line per returned entry (including the self-match), as specified above. Mandatory. 2. `## Severity` 3. `## Category` 4. `## Affected paths` @@ -183,7 +185,9 @@ Start this section with exactly one of these four tokens, wrapped in backticks, with no bold, italics, quotes, period, or any other punctuation attached to the token itself: - `critical` `high` `medium` `low` +```text +`critical` `high` `medium` `low` +``` After the backticked token, on the same line, an em-dash and a one-sentence justification grounded in concrete user-visible impact to people running this @@ -199,7 +203,9 @@ Elasticsearch nodes, causing full cluster downtime. Start with exactly one of these four tokens, wrapped in backticks, same formatting rules as severity: - `bug` `feature` `chore` `docs` +```text +`bug` `feature` `chore` `docs` +``` Then an em-dash and one short sub-flavour sentence if useful (e.g. "bug — molecule coverage gap", "chore — CI tuning"). No more. @@ -214,7 +220,9 @@ variables, name them. KB entry that survived post-filtering, append the citation at the end of the bullet in this exact shape: - - `roles/elasticsearch/tasks/elasticsearch-rolling-upgrade.yml` — contains the rolling restart pattern to reuse [Entry 4f14c154 · #pr-94 — already implements this pattern this issue asks for] +```markdown +- `roles/elasticsearch/tasks/elasticsearch-rolling-upgrade.yml` — contains the rolling restart pattern to reuse [Entry 4f14c154 · #pr-94 — already implements this pattern this issue asks for] +``` The bracketed citation must include **all three** of: @@ -268,5 +276,10 @@ a sprint review" — there is no team and there are no sprints. - Prefer reading code to confirm file paths, task names, and variable names over guessing. When in doubt, grep. -If the issue is obviously a duplicate, stale, or already fixed on main, say so -in the `Next action` section instead of producing a full triage. +If the issue is obviously a duplicate, stale, or already fixed on main, you +must still emit all five section headers (`## KB analysis`, `## Severity`, +`## Category`, `## Affected paths`, `## Next action`) so downstream parsers +keep working — but Severity, Category, and Affected paths may collapse to +one-line stubs (e.g. Severity → `` `low` `` — already fixed; Affected paths +→ "n/a, already addressed in #X"). Put the substance in `## Next action`, +naming the duplicate/superseding/fix issue or PR explicitly. From da0fbf0d722102c85fe8cc9dd6da46e44bb70d64 Mon Sep 17 00:00:00 2001 From: Sam Crauwels Date: Sat, 11 Apr 2026 20:21:14 +0200 Subject: [PATCH 9/9] chore(ci): address coderabbit fourth-round prompt contradictions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups to the round-3 fix that I missed in the Hard rules section. The "do not cite the issue you are triaging" rule now matches the rest of the contract: self-matches go in KB analysis as skip-self, they just never appear in Affected paths or Next action. The "acknowledge and skip straight to next action" rule is rewritten so all five section headers stay present — Severity, Category, and Affected paths can collapse to one-line stubs but the structure is non-negotiable. --- .github/prompts/triage-distillery.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/prompts/triage-distillery.md b/.github/prompts/triage-distillery.md index 24cb2925..950378da 100644 --- a/.github/prompts/triage-distillery.md +++ b/.github/prompts/triage-distillery.md @@ -251,9 +251,10 @@ a sprint review" — there is no team and there are no sprints. ## Hard rules (repeated for emphasis) -- **Do NOT cite the issue you are triaging as a prior reference.** If - `distillery_search` returns the current issue itself, ignore that result - entirely. +- **Do NOT cite the issue you are triaging in the triage body.** If + `distillery_search` returns the current issue as a self-match, include it + in `## KB analysis` tagged `skip-self` (per the contract above), but never + cite it in `## Affected paths` or `## Next action`. - **Do NOT emit a "same topic" citation.** Decoration is forbidden. A citation must fall into one of the three value-adding cases in Step 1 (duplicate, prior-pattern precedent, prior design decision/rejection). @@ -271,8 +272,10 @@ a sprint review" — there is no team and there are no sprints. - Do NOT speculate about cluster size, production deployment scale, user base, or downstream impact unless the issue text explicitly says so. - Do NOT pad the comment with summary/rationale boilerplate. If the issue - body already analyzes the problem well, acknowledge that and skip straight - to the next action. + body already analyzes the problem well, keep `## Severity`, `## Category`, + and `## Affected paths` minimal (one-line stubs are fine) and put the + substantive guidance in `## Next action`. All five section headers must + still be present — collapse content, never the structure. - Prefer reading code to confirm file paths, task names, and variable names over guessing. When in doubt, grep.