{"owner":"line","repo":"armeria","hasSkills":true,"totalSkillsCount":13,"totalTokensCount":26378,"categories":["marketplace","anthropic-skill","claude-rule","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":[".claude-plugin/marketplace.json","tools/claude-plugins/assistant/skills/eventloop-audit/SKILL.md",".claude/skills/release-note/references/style-guide.md",".agents/skills/release-note/references/style-guide.md",".claude/skills/release-note/scripts/lint-release-note.sh","tools/claude-plugins/assistant/.claude-plugin/plugin.json","tools/claude-plugins/assistant/skills/eventloop-audit/references/armeria-thread-pool-guide.md","tools/claude-plugins/assistant/skills/eventloop-audit/references/detection-patterns.md",".agents/skills/release-note/SKILL.md",".claude/skills/release-note/SKILL.md",".claude/skills/upgrade-deps/SKILL.md",".agents/skills/upgrade-deps/SKILL.md","tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md"],"skills":{".claude-plugin/marketplace.json":"{\n  \"name\": \"armeria\",\n  \"owner\": {\n    \"name\": \"armerian\",\n    \"email\": \"dl_armeria@linecorp.com\"\n  },\n  \"metadata\": {\n    \"description\": \"Claude plugin for the Armeria microservice framework\",\n    \"version\": \"0.0.1\"\n  },\n  \"plugins\": [\n    {\n      \"name\": \"assistant\",\n      \"source\": {\n        \"source\": \"git-subdir\",\n        \"url\": \"https://github.com/line/armeria.git\",\n        \"path\": \"tools/claude-plugins/assistant\"\n      }\n    }\n  ]\n}\n","tools/claude-plugins/assistant/skills/eventloop-audit/SKILL.md":"---\nname: eventloop-audit\ndescription: Systematically audits any Armeria-based Java project for event loop blocking issues — discovers project patterns, scans for blocking operations, traces call chains, and produces a fix plan without modifying code. Use when experiencing latency spikes, connection timeouts, deadlocks, or thread starvation in Armeria services, or as a pre-release audit. Invoked as `/eventloop-audit` for the full project or `/eventloop-audit <path>` for a specific directory.\n---\n\n# Event Loop Blocking Audit for Armeria Projects\n\n## Overview\n\nA systematic, multi-phase audit that discovers how a project uses Armeria, scans for all categories of event loop blocking, traces call chains to determine thread context, and produces a structured report with fix plans. **No code is modified** — only analysis and planning.\n\nThis skill is **framework-generic** — it works on any Armeria-based Java project by discovering the project's patterns dynamically, not assuming specific class names or directory structures.\n\n## When to Use\n\n- After setting up a new Armeria-based project and want to verify thread safety\n- Before a release to audit for latent event loop blocking issues\n- When experiencing unexplained latency spikes, connection timeouts, or deadlocks in an Armeria server\n- After integrating a new library that may contain blocking calls\n- As a periodic health check on the codebase's non-blocking discipline\n\n## Thread Model Reference\n\nFor a comprehensive guide on Armeria's thread pools, executor selection, and programmatic usage patterns, see [armeria-thread-pool-guide.md](references/armeria-thread-pool-guide.md).\n\n**The Golden Rule**: If `Thread.currentThread() instanceof NonBlocking` — you **MUST NOT** block. Armeria's event loop threads implement the `NonBlocking` marker interface. Any blocking operation on these threads stalls ALL connections sharing that event loop, causing cascading latency and potential deadlocks.\n\n---\n\n## Phase 1: Discovery — Understand the Project's Armeria Usage\n\nBefore searching for problems, discover HOW the project uses Armeria. Launch an **Explore agent** (or do it yourself) with these specific searches. Record findings as they'll inform all subsequent phases.\n\n### 1.1 Find Armeria Server Configuration\n\n```text\nGrep: Server\\.builder\\(\\)|ServerBuilder\nScope: **/*.java\n```\n\nRead each match and document:\n- Worker group configuration (`workerGroup(n)`, `serviceWorkerGroup(n)`)\n- Blocking executor configuration (`blockingTaskExecutor(...)`)\n- Decorator chain (`.decorate(...)` calls)\n- Request/idle timeout settings\n- Any custom `EventLoopGroup` creation\n\n### 1.2 Find HttpService Implementations\n\n```text\nGrep: implements HttpService|extends AbstractHttpService|extends SimpleDecoratingHttpService\nScope: **/*.java\n```\n\nThese classes have `serve()` methods that run on the event loop. List all of them — they are the primary audit targets.\n\n### 1.3 Find Annotated Services\n\n```text\nGrep: @Get\\(|@Post\\(|@Put\\(|@Delete\\(|@Head\\(|@Patch\\(|@Options\\(\nScope: **/*.java\n```\n\nFor each, check if the method or class has `@Blocking`. Methods without `@Blocking` run on the event loop.\n\n### 1.4 Find gRPC Services\n\n```text\nGrep: extends \\w+ImplBase|GrpcService\\.builder\\(\\)\nScope: **/*.java\n```\n\nCheck whether `useBlockingTaskExecutor(true)` is configured in the `GrpcService.builder()`. If not, all gRPC handler methods run on the event loop.\n\n### 1.5 Find Thrift Services\n\n```text\nGrep: THttpService\\.builder\\(\\)|THttpService\\.of\\(\nScope: **/*.java\n```\n\nSame check — look for `useBlockingTaskExecutor(true)`.\n\n### 1.6 Find Decorators\n\n```text\nGrep: SimpleDecoratingHttpService|DecoratingHttpServiceFunction|\\.decorate\\(\nScope: **/*.java\n```\n\nEvery decorator's `serve()` method runs on the event loop. List them all.\n\n### 1.7 Find Custom Executors and Thread Pools\n\n```text\nGrep: blockingTaskExecutor\\(\\)|ctx\\.eventLoop\\(\\)|Executors\\.new|new ThreadPoolExecutor|new ForkJoinPool|ExecutorService\nScope: **/*.java\n```\n\nDocument all custom thread pools and how they're used. These are the \"safe zones\" where blocking is OK.\n\n### 1.8 Find Offloading Patterns\n\n```text\nGrep: supplyAsync|thenApplyAsync|thenComposeAsync|thenAcceptAsync|ctx\\.blockingTaskExecutor|makeContextAware\nScope: **/*.java\n```\n\nUnderstand the project's conventions for offloading work from the event loop. Look for:\n- **Base class methods** that submit work to a blocking executor (e.g., a `runBlocking(Supplier)` method in a base handler)\n- **Utility classes** that wrap `ctx.blockingTaskExecutor()` with additional logic (cancellation, metrics, tracing)\n- **Project-specific annotations** that trigger blocking executor dispatch (beyond Armeria's `@Blocking`)\n\nA wrapper is \"safe\" if it provably submits the lambda/callable to a non-event-loop executor before the lambda body executes. Read the wrapper's implementation to verify — don't trust method names alone.\n\n### 1.9 Build Architecture Map\n\nSynthesize findings into a concise map:\n- **Event loop entry points**: List of classes/methods where the event loop calls into application code\n- **Safe execution wrappers**: Project-specific methods that offload to blocking executors\n- **Thread pool inventory**: All thread pools, their sizing, and purpose\n- **Decorator chain**: The order in which decorators wrap services\n\n---\n\n## Phase 2: Systematic Detection — 7 Categories\n\nSearch the entire project (or scoped path) for each category of blocking pattern. Exclude test files (`**/test/**`, `**/tests/**`) and generated code (`**/generated/**`, `**/target/**`, `**/build/**`).\n\nSee [detection-patterns.md](references/detection-patterns.md) for the complete search patterns and triage guidance for all 7 categories:\n\n1. **Direct Blocking Calls** — `.join()`, `.get()`, `Thread.sleep()`, `.await()`, `.lock()`, etc.\n2. **Blocking I/O Operations** — File I/O, network, JDBC, serialization, encryption\n3. **Synchronization** — `synchronized`, `ReentrantLock`, `StampedLock` on request paths\n4. **CompletableFuture Continuations on Wrong Executor** — `.thenApply()` without `Async` where completing thread is event loop\n5. **Thread Pool Misconfiguration** — Unbounded pools, missing shutdown, undersized blocking executor\n6. **Missing Armeria Idioms** — Missing `@Blocking`, missing `useBlockingTaskExecutor(true)`, blocking cancel handlers, missing context propagation\n7. **Transitive / Hidden Blocking** — DNS resolution, class loading, synchronous logging appenders\n\n---\n\n## Phase 3: Call Chain Analysis\n\nFor each finding from Phase 2, perform call chain analysis to determine the thread context.\n\n### Analysis Steps\n\n1. **Identify enclosing method** — What method contains the potentially blocking call?\n\n2. **Find all callers** — Grep for the method name across the project:\n   ```text\n   Grep: methodName\\(\n   Scope: **/*.java\n   ```\n   When handling overloaded methods (same name, different signatures), read each call site to confirm it calls the blocking variant. Also check superclass/interface hierarchies — a `serve()` override in a subclass is still called from the event loop if the parent is an `HttpService`.\n\n3. **Trace to entry point** — Follow the call chain upward until you reach one of:\n   - An Armeria entry point (`serve()`, `@Get`/`@Post` handler, gRPC handler, decorator)\n   - A thread pool submission (`blockingTaskExecutor().execute(...)`, `supplyAsync(...)`, etc.)\n   - A framework callback (`whenRequestCancelling`, `setOnCancelHandler`)\n   - A lambda/method reference passed to an executor (`.submit(this::methodName)`)\n\n   **Handling ambiguity**: If a method is called from BOTH event loop paths AND thread pool paths, classify based on the worst case (event loop path). Note both paths in the finding.\n\n4. **Determine thread context** — At the entry point:\n   - Is there a `@Blocking` annotation? → Safe\n   - Is there `useBlockingTaskExecutor(true)`? → Safe\n   - Is it inside a `supplyAsync(..., blockingExecutor)` call? → Safe\n   - Is it inside a project-specific safe wrapper (discovered in Phase 1.8)? → Safe\n   - None of the above? → **Event loop**\n\n5. **Classify severity using this decision tree:**\n\n```text\nIs the blocking call provably inside a blocking executor or thread pool?\n├── YES → SAFE (exclude from report or list separately)\n├── NO, it's provably on the event loop:\n│   ├── Is it an I/O operation, .join(), .get() without timeout, or Thread.sleep()?\n│   │   └── YES → CRITICAL\n│   ├── Is it .get(timeout, unit) or .await(timeout, unit)?\n│   │   └── YES → HIGH (blocks event loop for the timeout duration)\n│   ├── Is it a synchronized block or lock?\n│   │   ├── Can it contend with a thread that holds the lock for >1ms?\n│   │   │   └── YES → HIGH\n│   │   └── Low contention, very fast critical section?\n│   │       └── LOW\n│   └── Is it a very fast operation (System.getProperty, in-memory map lookup)?\n│       └── LOW\n└── UNDETERMINED (e.g., CF continuation where completing thread varies):\n    ├── Could the continuation body block?\n    │   ├── YES → MEDIUM (must verify at runtime or refactor to be safe)\n    │   └── NO (continuation is pure computation) → LOW\n    └── Is the source future always completed by a thread pool?\n        └── YES → SAFE\n```\n\n**Key distinction**: MEDIUM means \"we cannot statically prove the thread context\" — it requires either runtime verification (add logging/assertion) or defensive refactoring (use `*Async` variant with explicit executor).\n\n---\n\n## Phase 4: Fix Plan Templates\n\nFor each non-SAFE finding, recommend one of the fix approaches documented in [fix-templates.md](references/fix-templates.md):\n\n| Template | When to use |\n|----------|-------------|\n| **FP-1** | Replace `.join()`/`.get()` with async chaining |\n| **FP-2** | Offload blocking work to `ctx.blockingTaskExecutor()` |\n| **FP-3** | Add `@Blocking` annotation to annotated service methods |\n| **FP-4** | Enable `useBlockingTaskExecutor(true)` for gRPC/Thrift services |\n| **FP-5** | Use `*Async` CF variant with explicit executor |\n| **FP-6** | Add timeout to unavoidable blocking |\n| **FP-7** | Make thread pool initialization thread-safe |\n| **FP-8** | Wire context propagation with `makeContextAware` |\n\n**Document the recommendation — do not apply it.**\n\n---\n\n## Phase 5: Produce the Report\n\n### Per-Finding Format\n\n```markdown\n## Finding [ID]: [Short descriptive title]\n\n**Severity**: CRITICAL / HIGH / MEDIUM / LOW\n**File**: [relative path]:[line number]\n**Thread context**: Event loop / Blocking executor / Undetermined\n**Detection category**: [1-7 with category name]\n\n### Description\n[What the code does and why it's a blocking risk on the event loop]\n\n### Call chain\n[Armeria entry point (event loop)] → [Method A] → [Method B] → [blocking call at line X]\n\n### Recommended fix\n[Reference FP-1 through FP-8, with specific application to this case:\n which method to change, what the new code should look like, which executor to use]\n\n### Risk assessment\n[What could break when applying the fix, edge cases to consider, tests to run]\n```\n\n### Summary Table\n\nAt the end of all findings, produce a summary:\n\n```markdown\n| # | Severity | File | Line | Category | Blocking Pattern | Fix |\n|---|----------|------|------|----------|-----------------|-----|\n| 1 | CRITICAL | FooService.java | 42 | 1 - Direct blocking | `.join()` | FP-1 |\n| 2 | HIGH | BarHandler.java | 88 | 2 - I/O | FileInputStream | FP-2 |\n| ... | ... | ... | ... | ... | ... | ... |\n```\n\n### Statistics\n\n```markdown\n- Total findings: [N]\n- CRITICAL: [N] | HIGH: [N] | MEDIUM: [N] | LOW: [N] | SAFE (excluded): [N]\n- By category: Cat.1: [N] | Cat.2: [N] | Cat.3: [N] | Cat.4: [N] | Cat.5: [N] | Cat.6: [N] | Cat.7: [N]\n```\n\n### Follow-Up Recommendations\n\nAlways include these recommendations regardless of findings:\n\n1. **Runtime detection**: Consider adding [BlockHound](https://github.com/reactor/BlockHound) to the test suite — it detects blocking calls on non-blocking threads at runtime\n2. **Async logging**: Verify that the logging framework uses async appenders in production to avoid blocking on `log.*()` calls\n3. **Event loop monitoring**: Enable Armeria's built-in event loop blocking detection:\n   ```text\n   -Dcom.linecorp.armeria.reportBlockedEventLoop=true\n   ```\n4. **Code review checklist**: Add \"Does this handler offload blocking work?\" to the team's code review checklist for Armeria services\n\n---\n\n## Execution Checklist\n\nFollow this checklist to ensure nothing is skipped. Check off each item as you complete it.\n\n### Phase 1: Discovery\n- [ ] 1.1 — Found and documented Armeria server configuration(s)\n- [ ] 1.2 — Listed all `HttpService` implementations\n- [ ] 1.3 — Listed all annotated services, noted `@Blocking` presence/absence\n- [ ] 1.4 — Listed all gRPC services, noted `useBlockingTaskExecutor` presence/absence\n- [ ] 1.5 — Listed all Thrift services (if any)\n- [ ] 1.6 — Listed all decorators\n- [ ] 1.7 — Documented all custom executors and thread pools\n- [ ] 1.8 — Identified project-specific offloading patterns\n- [ ] 1.9 — Built architecture map (entry points, safe wrappers, thread pools)\n\n### Phase 2: Detection\n- [ ] Category 1 — Searched for all direct blocking calls\n- [ ] Category 2 — Searched for all blocking I/O operations\n- [ ] Category 3 — Searched for synchronization on request paths\n- [ ] Category 4 — Audited CompletableFuture continuation executors\n- [ ] Category 5 — Audited thread pool configurations\n- [ ] Category 6 — Checked for missing Armeria idioms (@Blocking, useBlockingTaskExecutor, cancel handlers, context propagation)\n- [ ] Category 7 — Searched for transitive/hidden blocking (DNS, class loading, logging config)\n\n### Phase 3: Analysis\n- [ ] Traced call chains for ALL non-obvious findings\n- [ ] Classified every finding by severity\n- [ ] Excluded SAFE findings from the report (or listed them separately)\n\n### Phase 4: Fix Planning\n- [ ] Assigned a fix template (FP-1 through FP-8) to each non-SAFE finding\n- [ ] Documented specific fix details for each finding\n\n### Phase 5: Reporting\n- [ ] Produced per-finding reports\n- [ ] Produced summary table\n- [ ] Produced statistics\n- [ ] Included follow-up recommendations\n\n---\n\n## Common Mistakes to Avoid\n\n- **Stopping at grep hits** — A grep hit is not a finding. You must trace the call chain to determine thread context. Many `.join()` calls are inside thread pool code and are perfectly safe.\n- **Ignoring non-async CF continuations** — `.thenApply()` without `Async` is the most subtle source of event loop blocking. The continuation runs on whatever thread completes the source future.\n- **Missing cancel handlers** — Cancel handler lambdas always run on the event loop. They're easy to overlook but are a common source of blocking.\n- **Assuming `synchronized` is always bad** — It's only a problem if (a) it's on a request path reachable from the event loop AND (b) the lock can be contended by another thread holding it for a long time.\n- **Forgetting transitive blocking** — A clean `serve()` method that calls `helperMethod()` which calls `utilMethod()` which calls `.join()` is still blocking the event loop.\n- **Ignoring logging** — If the logging framework uses synchronous file appenders, every `log.info()` on the event loop is a blocking I/O operation. This is a configuration issue, not a code issue, but still critical.\n",".claude/skills/release-note/references/style-guide.md":"# Release Notes Style Guide\n\nAnnotated examples from real Armeria release notes showing the exact formatting conventions.\n\n---\n\n## Major New Feature Entry\n\nBold title, description explaining value, `[Type](type)` links, code example with `👈👈👈`, PR refs.\n\n```mdx\n- **Standalone Athenz Token Validation**: You can now use [AthenzAuthorizer](type) to validate Athenz tokens\n  outside of Armeria's request pipeline. This allows you to easily integrate Athenz authorization into\n  third-party frameworks like Spring MVC or other servlet-based applications. #6607\n  ```java\n  ZtsBaseClient ztsBaseClient = ...;\n  AthenzAuthorizer authorizer =\n    AthenzAuthorizer.builder(ztsBaseClient)\n                    .policyConfig(new AthenzPolicyConfig(\"your-domain\"))\n                    .build();\n\n  // Validate tokens anywhere\n  AccessCheckStatus status =\n    authorizer.authorize(token, resource, action); // 👈👈👈\n  ```\n```\n\nKey elements:\n- `**Bold Title**:` prefix — only for significant features, not every entry\n- Description explains *what you can now do* and *why it matters*\n- `[AthenzAuthorizer](type)` — Armeria API type link\n- Code block indented under the bullet (2 spaces)\n- `// 👈👈👈` on the line the reader should focus on\n- `#6607` issue/PR references at end of description line (list both the issue and the PR)\n\n---\n\n## Minor New Feature Entry\n\nOne-liner without code example, for smaller additions.\n\n```mdx\n- **Per-Request Client TLS Configuration**: You can now specify [ClientTlsSpec](type) for each request using\n  [RequestOptions#clientTlsSpec()](type) or [ClientRequestContext#clientTlsSpec()](type). #6551\n```\n\nOr even simpler:\n\n```mdx\n- [ConnectionPoolListener](type) now supports ping-related events. #6539 #6552\n```\n\n---\n\n## Improvement Entry\n\nConcise description with type links. No code example unless it changes user-facing API.\n\n```mdx\n- [XdsBootstrap](type) now supports SDS. #6597 #6610 #6654\n- Improved warning logs by adding diagnostic context to frequent HTTP/2 connection errors. #6638\n```\n\n---\n\n## Bug Fix Entry\n\nDescribe the symptom that was fixed, not the internal root cause.\n\n```mdx\n- [GrpcMeterIdPrefixFunction](type) records `grpc-status` correctly for responses which fail with an exception. #6606 #6621\n- `X-Forwarded-For` header values with leading or trailing whitespace around comma-separated addresses\n  (e.g., `\"192.168.1.1 , 10.0.0.1\"`) are now trimmed and parsed correctly. #6615\n- Setting `pingIntervalMillis` to a value greater than 33 seconds no longer throws an exception. #6648\n  - Linux keepalive socket options (`SO_KEEPALIVE` `TCP_KEEPIDLE`, `TCP_KEEPINTVL`) are no longer set by default.\n```\n\nKey patterns:\n- \"[Thing] now [works correctly]\" or \"[Thing] no longer [does wrong thing]\"\n- Sub-bullets for related side effects or additional context\n- Use backticks for config names, error types, and header names\n\n---\n\n## Breaking Change Entry\n\nState what changed and what users must do to migrate.\n\n```mdx\n- A subclass of [AbstractEndpointSelector](type) must implement [AbstractEndpointSelector#doSelectNow(ClientRequestContext)](type)\n  instead of [AbstractEndpointSelector#selectNow(ClientRequestContext)](type), which is now a final method. #6535\n```\n\nFor non-trivial migrations, include before/after code.\n\n---\n\n## Documentation Entry\n\nBrief description with links to new/updated docs.\n\nLink to docs pages with a **site-relative path** (`/docs/...`), never an absolute `https://armeria.dev/...`\nURL — that is the convention across the docs and the newer release notes, and it keeps links working\non local builds and previews.\n\n```mdx\n- New comprehensive documentation on understanding and handling timeouts: #6592\n  - [Understanding timeout and cancellation origins](/docs/advanced/understanding-timeouts)\n  - Handling timeouts for streaming:\n    - [Client-side streaming](/docs/client/timeouts#handling-timeouts-for-streaming-responses)\n    - [Server-side streaming](/docs/server/timeouts#handling-timeouts-for-streaming-requests)\n```\n\n---\n\n## Dependencies Section\n\nFormat: `- LibraryName oldVersion → newVersion`, sorted alphabetically.\n\n```mdx\n- Athenz 1.12.31 → 1.12.33\n- BlockHound 1.0.15 → 1.0.16\n- Jackson 2.20.1 → 2.21.0\n- Kotlin 2.2.21 → 2.3.0\n- Logback 1.5.23 → 1.5.27\n- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3\n- Spring Boot 3.5.8 → 3.5.10, 4.0.1 → 4.0.2\n```\n\nRules:\n- Use `→` (unicode arrow), not `->` or `-->`\n- Use `-` dash bullets, not `*`\n- Use the library's common name, not Maven artifact IDs\n- Group multi-version bumps: `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n- Omit build-only dependencies (anything under the `- Build` section in the raw dependency PR)\n- Omit **test-only** dependencies (e.g., Apache HttpClient, OkHttp, DGS) — only used in\n  `testImplementation` and not in published POMs\n- Omit **shaded** dependencies (e.g., Guava, Bouncy Castle, JCTools) — relocated into Armeria\n  JARs and invisible to users\n- Omit dependencies that are **not transitive** in any published artifact (e.g., Jsoup)\n- When unsure, check `dependencies.toml` for scope and whether the dep appears in a published\n  module's `api` or `implementation` configuration\n- Sort alphabetically (A → Z)\n- Strip trailing release qualifiers from version numbers — `.Final`, `.RELEASE`, `-GA`,\n  `.vYYYYMMDD` and the like. They add no information a reader acts on:\n  - `Netty 4.2.15.Final → 4.2.16.Final` becomes `Netty 4.2.15 → 4.2.16`\n  - `Javassist 3.31.0-GA → 3.32.0-GA` becomes `Javassist 3.31.0 → 3.32.0`\n  - `Jetty 9.4.55.v20240627 → 9.4.58.v20250814` becomes `Jetty 9.4.55 → 9.4.58`\n\n---\n\n## Thank You Section\n\n```mdx\n## 🙇 Thank you\n\n<ThankYou usernames={[\n  'contributor1',\n  'contributor2',\n  'contributor3'\n]} />\n```\n\nRules:\n- Usernames sorted alphabetically\n- Remove bot accounts: `dependabot[bot]`, `CLAassistant`\n- Core maintainers from `.github/CODEOWNERS` must always be included\n- One username per line, single-quoted, comma-separated\n\n---\n\n## What NOT to Include\n\n- **`- N/A` entries**: Remove sections that have no content instead of writing `- N/A`\n- **`🗑 Maybe ignore` section**: Must be fully triaged and removed\n- **Build-only dependencies**: `- Build` sub-section from dependency PRs\n- **Internal refactoring details**: Changes with no user-facing impact\n- **Site/docs dependency bumps**: npm bumps for `site/` or `docs-client/` (e.g., webpack, lodash)\n\n## Type Link Rules\n\nUse `[ClassName](type)` ONLY for Armeria public API references:\n- Classes/interfaces: `[GrpcServiceBuilder](type)`\n- Methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always use the full\n  `ClassName#methodName(ParamType)` form. Never use backtick-only style like\n  `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - **No space after commas** in the parameter list. The lookup key in `gen-src/api-index.json`\n    has none, so `[X#m(A, B)](type)` silently resolves to a dead `type://#` link while\n    `[X#m(A,B)](type)` works. The rendered text hides the difference — it shows `X.m()` either way.\n  - **Never split a method across a class link and a backtick.** `[Foo](type)` `` `.bar()` `` is\n    not a link to `bar()`; write `[Foo#bar(Baz)](type)` as one unit.\n- Annotations: `[@Blocking](type)`\n\nDo NOT use `[Name](type)` for:\n- JDK types (`String`, `Duration`, `CompletableFuture`, `List`)\n- Third-party types (`Mono`, `Flux`, `HttpServletRequest`)\n- Internal/private Armeria classes\n- Generic concepts (use plain text or backticks instead)\n- Method calls inside code blocks — code blocks use plain Java syntax, not `(type)` links\n",".agents/skills/release-note/references/style-guide.md":"# Release Notes Style Guide\n\nAnnotated examples from real Armeria release notes showing the exact formatting conventions.\n\n---\n\n## Major New Feature Entry\n\nBold title, description explaining value, `[Type](type)` links, code example with `👈👈👈`, PR refs.\n\n```mdx\n- **Standalone Athenz Token Validation**: You can now use [AthenzAuthorizer](type) to validate Athenz tokens\n  outside of Armeria's request pipeline. This allows you to easily integrate Athenz authorization into\n  third-party frameworks like Spring MVC or other servlet-based applications. #6607\n  ```java\n  ZtsBaseClient ztsBaseClient = ...;\n  AthenzAuthorizer authorizer =\n    AthenzAuthorizer.builder(ztsBaseClient)\n                    .policyConfig(new AthenzPolicyConfig(\"your-domain\"))\n                    .build();\n\n  // Validate tokens anywhere\n  AccessCheckStatus status =\n    authorizer.authorize(token, resource, action); // 👈👈👈\n  ```\n```\n\nKey elements:\n- `**Bold Title**:` prefix — only for significant features, not every entry\n- Description explains *what you can now do* and *why it matters*\n- `[AthenzAuthorizer](type)` — Armeria API type link\n- Code block indented under the bullet (2 spaces)\n- `// 👈👈👈` on the line the reader should focus on\n- `#6607` issue/PR references at end of description line (list both the issue and the PR)\n\n---\n\n## Minor New Feature Entry\n\nOne-liner without code example, for smaller additions.\n\n```mdx\n- **Per-Request Client TLS Configuration**: You can now specify [ClientTlsSpec](type) for each request using\n  [RequestOptions#clientTlsSpec()](type) or [ClientRequestContext#clientTlsSpec()](type). #6551\n```\n\nOr even simpler:\n\n```mdx\n- [ConnectionPoolListener](type) now supports ping-related events. #6539 #6552\n```\n\n---\n\n## Improvement Entry\n\nConcise description with type links. No code example unless it changes user-facing API.\n\n```mdx\n- [XdsBootstrap](type) now supports SDS. #6597 #6610 #6654\n- Improved warning logs by adding diagnostic context to frequent HTTP/2 connection errors. #6638\n```\n\n---\n\n## Bug Fix Entry\n\nDescribe the symptom that was fixed, not the internal root cause.\n\n```mdx\n- [GrpcMeterIdPrefixFunction](type) records `grpc-status` correctly for responses which fail with an exception. #6606 #6621\n- `X-Forwarded-For` header values with leading or trailing whitespace around comma-separated addresses\n  (e.g., `\"192.168.1.1 , 10.0.0.1\"`) are now trimmed and parsed correctly. #6615\n- Setting `pingIntervalMillis` to a value greater than 33 seconds no longer throws an exception. #6648\n  - Linux keepalive socket options (`SO_KEEPALIVE` `TCP_KEEPIDLE`, `TCP_KEEPINTVL`) are no longer set by default.\n```\n\nKey patterns:\n- \"[Thing] now [works correctly]\" or \"[Thing] no longer [does wrong thing]\"\n- Sub-bullets for related side effects or additional context\n- Use backticks for config names, error types, and header names\n\n---\n\n## Breaking Change Entry\n\nState what changed and what users must do to migrate.\n\n```mdx\n- A subclass of [AbstractEndpointSelector](type) must implement [AbstractEndpointSelector#doSelectNow(ClientRequestContext)](type)\n  instead of [AbstractEndpointSelector#selectNow(ClientRequestContext)](type), which is now a final method. #6535\n```\n\nFor non-trivial migrations, include before/after code.\n\n---\n\n## Documentation Entry\n\nBrief description with links to new/updated docs.\n\nLink to docs pages with a **site-relative path** (`/docs/...`), never an absolute `https://armeria.dev/...`\nURL — that is the convention across the docs and the newer release notes, and it keeps links working\non local builds and previews.\n\n```mdx\n- New comprehensive documentation on understanding and handling timeouts: #6592\n  - [Understanding timeout and cancellation origins](/docs/advanced/understanding-timeouts)\n  - Handling timeouts for streaming:\n    - [Client-side streaming](/docs/client/timeouts#handling-timeouts-for-streaming-responses)\n    - [Server-side streaming](/docs/server/timeouts#handling-timeouts-for-streaming-requests)\n```\n\n---\n\n## Dependencies Section\n\nFormat: `- LibraryName oldVersion → newVersion`, sorted alphabetically.\n\n```mdx\n- Athenz 1.12.31 → 1.12.33\n- BlockHound 1.0.15 → 1.0.16\n- Jackson 2.20.1 → 2.21.0\n- Kotlin 2.2.21 → 2.3.0\n- Logback 1.5.23 → 1.5.27\n- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3\n- Spring Boot 3.5.8 → 3.5.10, 4.0.1 → 4.0.2\n```\n\nRules:\n- Use `→` (unicode arrow), not `->` or `-->`\n- Use `-` dash bullets, not `*`\n- Use the library's common name, not Maven artifact IDs\n- Group multi-version bumps: `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n- Omit build-only dependencies (anything under the `- Build` section in the raw dependency PR)\n- Sort alphabetically (A → Z)\n- Strip trailing release qualifiers from version numbers — `.Final`, `.RELEASE`, `-GA`,\n  `.vYYYYMMDD` and the like. They add no information a reader acts on:\n  - `Netty 4.2.15.Final → 4.2.16.Final` becomes `Netty 4.2.15 → 4.2.16`\n  - `Javassist 3.31.0-GA → 3.32.0-GA` becomes `Javassist 3.31.0 → 3.32.0`\n  - `Jetty 9.4.55.v20240627 → 9.4.58.v20250814` becomes `Jetty 9.4.55 → 9.4.58`\n\n---\n\n## Thank You Section\n\n```mdx\n## 🙇 Thank you\n\n<ThankYou usernames={[\n  'contributor1',\n  'contributor2',\n  'contributor3'\n]} />\n```\n\nRules:\n- Usernames sorted alphabetically\n- Remove bot accounts: `dependabot[bot]`, `CLAassistant`\n- Core maintainers from `.github/CODEOWNERS` must always be included\n- One username per line, single-quoted, comma-separated\n\n---\n\n## What NOT to Include\n\n- **`- N/A` entries**: Remove sections that have no content instead of writing `- N/A`\n- **`🗑 Maybe ignore` section**: Must be fully triaged and removed\n- **Build-only dependencies**: `- Build` sub-section from dependency PRs\n- **Internal refactoring details**: Changes with no user-facing impact\n- **Site/docs dependency bumps**: npm bumps for `site/` or `docs-client/` (e.g., webpack, lodash)\n\n## Type Link Rules\n\nUse `[ClassName](type)` ONLY for Armeria public API references:\n- Classes/interfaces: `[GrpcServiceBuilder](type)`\n- Methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always use the full\n  `ClassName#methodName(ParamType)` form. Never use backtick-only style like\n  `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - **No space after commas** in the parameter list. The lookup key in `gen-src/api-index.json`\n    has none, so `[X#m(A, B)](type)` silently resolves to a dead `type://#` link while\n    `[X#m(A,B)](type)` works. The rendered text hides the difference — it shows `X.m()` either way.\n  - **Never split a method across a class link and a backtick.** `[Foo](type)` `` `.bar()` `` is\n    not a link to `bar()`; write `[Foo#bar(Baz)](type)` as one unit.\n- Annotations: `[@Blocking](type)`\n\nDo NOT use `[Name](type)` for:\n- JDK types (`String`, `Duration`, `CompletableFuture`, `List`)\n- Third-party types (`Mono`, `Flux`, `HttpServletRequest`)\n- Internal/private Armeria classes\n- Generic concepts (use plain text or backticks instead)\n- Method calls inside code blocks — code blocks use plain Java syntax, not `(type)` links\n",".claude/skills/release-note/scripts/lint-release-note.sh":"#!/usr/bin/env bash\n# Mechanical checks for a release note. Prose rules only fire if someone remembers to apply them\n# document-wide; these run. Exits non-zero on any finding.\n#\n#   .claude/skills/release-note/scripts/lint-release-note.sh site/src/content/release-notes/1.41.0.mdx\n\nset -uo pipefail\n\nf=\"${1:?usage: lint-release-note.sh <release-note.mdx>}\"\n[ -f \"$f\" ] || { echo \"no such file: $f\" >&2; exit 2; }\n\nfail=0\n# check <description> <grep-output>; prints each matched line indented, preserving line breaks.\ncheck() {\n  [ -n \"$2\" ] || return 0\n  fail=1\n  printf '\\n✗ %s\\n' \"$1\"\n  printf '%s\\n' \"$2\" | sed 's/^/    /'\n}\n\n# --- type links -------------------------------------------------------------\n# A class link glued to a backticked method is not a link to that method.\nhits=$(grep -n '\\](type)`\\.' \"$f\" || true)\ncheck \"class link + backtick method (write [Foo#bar(Baz)](type) as one unit)\" \"$hits\"\n\n# api-index keys carry no space after commas; a space yields a dead type://# link.\nhits=$(grep -nE '\\[[A-Za-z0-9_.]+#[^]]*, [^]]*\\]\\(type\\)' \"$f\" || true)\ncheck \"space after comma in a method type link (silently dead)\" \"$hits\"\n\n# --- links ------------------------------------------------------------------\nhits=$(grep -n '](https://armeria.dev/' \"$f\" || true)\ncheck \"absolute armeria.dev link (use a site-relative /docs/... path)\" \"$hits\"\n\n# --- leftovers from the generator ------------------------------------------\nhits=$(grep -n '^- N/A' \"$f\" || true)\ncheck \"empty section left as '- N/A' (delete the section)\" \"$hits\"\n\nhits=$(grep -n 'Maybe ignore' \"$f\" || true)\ncheck \"'🗑 Maybe ignore' section must be triaged away\" \"$hits\"\n\nhits=$(grep -nE '^\\s*\\*\\s' \"$f\" || true)\ncheck \"'*' bullet (use '-')\" \"$hits\"\n\n# --- dependencies -----------------------------------------------------------\ndeps=$(sed -n '/^## ⛓ Dependencies/,/^## /p' \"$f\")\nhits=$(printf '%s' \"$deps\" | grep -n ' -> ' || true)\ncheck \"ASCII arrow in Dependencies (use →)\" \"$hits\"\n\n# Strip stable-release metadata only; -RC1/-M1/-SNAPSHOT identify distinct builds and must stay.\nhits=$(printf '%s' \"$deps\" | grep -nE '[0-9](\\.(Final|RELEASE|GA)|-GA|\\.v[0-9]{6,})' || true)\ncheck \"release qualifier left in a version (.Final/.RELEASE/-GA/.vYYYYMMDD)\" \"$hits\"\n\nhits=$(printf '%s' \"$deps\" | grep -n '^- Build' || true)\ncheck \"build-only dependency section leaked in\" \"$hits\"\n\n# --- formatting -------------------------------------------------------------\nhits=$(awk 'length>104 {printf \"%d: (%d chars)\\n\", FNR, length}' \"$f\" || true)\ncheck \"line longer than 104 chars\" \"$hits\"\n\nif [ $fail -eq 0 ]; then\n  echo \"✓ $(basename \"$f\"): all mechanical checks passed\"\nelse\n  printf '\\nSome checks failed. Note that a bad type link never fails the site build —\\n'\n  printf 'it renders as a blank type://# link, so this script is the only thing that catches it.\\n'\nfi\nexit $fail\n","tools/claude-plugins/assistant/.claude-plugin/plugin.json":"{\n  \"name\": \"assistant\"\n}\n","tools/claude-plugins/assistant/skills/eventloop-audit/references/armeria-thread-pool-guide.md":"<!-- A developer-facing version of this guide is published at /docs/advanced/threading-model -->\n\n# Armeria Thread Pool Guide for Developers\n\n## Context\n\nArmeria is built on Netty's non-blocking I/O model. Misusing thread pools — especially blocking the event loop — is the most common cause of performance degradation and deadlocks. This guide provides an architectural overview, recommended configurations, and practical dos/don'ts for developers working with Armeria.\n\n---\n\n## 1. Architectural Overview\n\n### Thread Pool Roles\n\n| Pool | What it does | Thread name pattern | Default size | Daemon? |\n|------|-------------|---------------------|-------------|---------|\n| **bossGroup** | Accepts incoming TCP connections (one per server port) | `armeria-boss-{protocol}-{addr}` where addr is `*:{port}`, `{ip}:{port}`, or `unix:{path}` | **1 per port** (fixed, not configurable) | No |\n| **workerGroup** | Handles all socket I/O (reads/writes) and executes non-blocking service logic | `armeria-common-worker-*` | `2 * CPU cores` (NIO/epoll/kqueue) or `CPU cores` (io_uring) | Yes |\n| **serviceWorkerGroup** | Optional dedicated EventLoopGroup for service method execution, isolating service logic from socket I/O | User-defined | Falls back to workerGroup if not set | Yes |\n| **blockingTaskExecutor** | Runs long-running or blocking operations (DB calls, file I/O, legacy sync APIs) | `armeria-common-blocking-tasks-*` | **200** threads (inspired by Tomcat default) | Yes |\n\n### How a Request Flows Through the Pools\n\n```text\nClient request\n    |\n    v\n[bossGroup]  -- accepts TCP connection, hands channel to workerGroup\n    |\n    v\n[workerGroup]  -- reads bytes, decodes HTTP/2 frames, invokes service\n    |\n    +---> Non-blocking service: runs directly on event loop thread\n    |\n    +---> @Blocking / useBlockingTaskExecutor(true): dispatched to blockingTaskExecutor\n    |\n    +---> serviceWorkerGroup configured: service runs on dedicated event loop\n```\n\n### Key Classes and Files\n\n- `CommonPools` — global shared pools (`core/.../common/CommonPools.java`)\n- `EventLoopGroups` — factory for creating EventLoopGroups (`core/.../common/util/EventLoopGroups.java`)\n- `BlockingTaskExecutor` / `BlockingTaskExecutorBuilder` — blocking pool abstraction (`core/.../common/util/`)\n- `EventLoopThread` — custom thread extending Netty's `FastThreadLocalThread`, implements `NonBlocking` marker (`core/.../internal/common/util/EventLoopThread.java`)\n- `EventLoopCheckingFuture` — warns when `.get()`/`.join()` is called from an event loop (`core/.../common/util/EventLoopCheckingFuture.java`)\n- `DefaultAnnotatedService` — dispatches to event loop or blocking executor based on `@Blocking` (`core/.../internal/server/annotation/DefaultAnnotatedService.java`)\n- `Flags` / `DefaultFlagsProvider` — system-wide defaults and JVM flag overrides (`core/.../common/Flags.java`)\n\n### Client-Side Thread Pools\n\nThe HTTP client (`WebClient`) uses the same `CommonPools.workerGroup()` by default. An `EventLoopScheduler` distributes connections across event loops per endpoint (default: 1 event loop per endpoint, configurable).\n\nKey client config class: `ClientFactoryBuilder` (`core/.../client/ClientFactoryBuilder.java`)\n\n---\n\n## 2. `serviceWorkerGroup` vs `blockingTaskExecutor` — When to Use Which\n\nThese two are the most commonly confused. They solve **different problems** and have fundamentally different runtime characteristics.\n\n### The Core Distinction\n\n| Aspect | `serviceWorkerGroup` | `blockingTaskExecutor` |\n|--------|----------------------|------------------------|\n| **Java type** | `EventLoopGroup` (Netty event loops) | `ScheduledExecutorService` (thread pool) |\n| **Thread type** | Event loop threads (`NonBlocking`) | Regular threads (blocking allowed) |\n| **Can you block in it?** | **NO** — same rules as workerGroup | **YES** — that's its purpose |\n| **Scheduling model** | Single-threaded per loop, run-to-completion | Traditional thread pool, one thread per task |\n| **Typical size** | Small (matches CPU cores) | Large (default 200) |\n\n### How They Work at Runtime\n\n**serviceWorkerGroup** — The service method still runs on an event loop, just a *different* one from the I/O channel. In `HttpServerHandler`, Armeria checks whether the configured serviceWorkerGroup differs from the main workerGroup. If so, it calls `req.subscribeOn(serviceEventLoop)` and invokes the service on that loop. Request reading, service execution, and response emission all happen on the service event loop. Response *writing* to the socket still goes back to the channel's original event loop.\n\n```text\n[workerGroup event loop]  reads bytes from socket\n        |\n        v  (if serviceWorkerGroup != workerGroup)\n[serviceWorkerGroup event loop]  decodes request, runs service, emits response\n        |\n        v  (response writing)\n[workerGroup event loop]  writes bytes to socket\n```\n\n**blockingTaskExecutor** — The service method is submitted via `thenApplyAsync(..., ctx.blockingTaskExecutor())` to a traditional thread pool. The calling event loop is freed immediately. The blocking thread runs the service logic, and when done, the response flows back to the event loop for writing.\n\n```text\n[event loop]  reads bytes, decodes request\n        |\n        v  (thenApplyAsync)\n[blocking thread pool]  runs service method (blocking OK here)\n        |\n        v  (future completes)\n[event loop]  writes response bytes to socket\n```\n\n### Decision Guide\n\n**Use `serviceWorkerGroup` when:**\n\n- Your service does **CPU-intensive but non-blocking** work (e.g., JSON serialization of large payloads, image transformation in memory, complex computation)\n- You want to **prevent service CPU work from starving socket I/O** — if your service burns 50ms of CPU per request, that delays read/write for all other connections sharing that event loop\n- You need **isolation between services** — e.g., a high-throughput service shouldn't compete for event loop time with a latency-sensitive one\n- Your code is already fully async (returns `CompletableFuture`, uses reactive streams) — you just want it on a different event loop\n\n```java\n// Good use case: CPU-heavy serialization isolated from I/O\nServer.builder()\n      .serviceWorkerGroup(4)  // Dedicated event loops for service logic\n      .service(\"/heavy-json\", (ctx, req) -> {\n          // Non-blocking but CPU-intensive: serialize huge object\n          return HttpResponse.of(hugeObjectMapper.writeValueAsBytes(bigData));\n      })\n      .build();\n```\n\n**Use `blockingTaskExecutor` when:**\n\n- Your code calls **synchronous blocking APIs** (JDBC, file I/O, `Thread.sleep`, `future.get()`, legacy HTTP clients, `synchronized` blocks)\n- You're integrating with **libraries that block the calling thread** (most traditional Java libraries)\n- You have **no choice** but to wait for a result synchronously\n\n```java\n// Good use case: JDBC call that blocks the thread\nServer.builder()\n      .service(\"/users\", (ctx, req) ->\n          HttpResponse.of(CompletableFuture.supplyAsync(() -> {\n              // This blocks — JDBC has no async API\n              User user = jdbcTemplate.queryForObject(\"SELECT ...\", User.class);\n              return HttpResponse.of(HttpStatus.OK, MediaType.JSON, toJson(user));\n          }, ctx.blockingTaskExecutor())))\n      .build();\n```\n\n**DON'T use `serviceWorkerGroup` for blocking work** — it's still an event loop. Blocking it has the same catastrophic effect as blocking the main workerGroup. You're just blocking a *different* event loop.\n\n**DON'T use `blockingTaskExecutor` for CPU-bound async work** — it wastes a thread sitting idle during async operations and doesn't benefit from event loop optimizations (FastThreadLocal, zero-copy buffers, etc.).\n\n### When You Need Neither\n\nMost async services need neither. If your service just orchestrates async HTTP calls, reactive streams, or CompletableFuture chains, let it run on the default workerGroup.\n\n---\n\n## 3. Recommended Configuration Settings\n\n### Sizing Guidelines\n\n| Pool | Guideline |\n|------|-----------|\n| **workerGroup** | Default (`2 * CPU`) is usually fine. Increase only if profiling shows event loop saturation on I/O-heavy workloads. |\n| **blockingTaskExecutor** | Default 200 is generous. Size based on expected concurrent blocking operations. Consider: `expected_concurrent_blocking_calls * avg_blocking_duration / target_latency`. |\n| **serviceWorkerGroup** | Only configure if you need to isolate service execution from socket I/O (e.g., CPU-heavy request processing that shouldn't delay I/O). Most services don't need this. |\n\n### JVM Flags\n\n```properties\n-Dcom.linecorp.armeria.numCommonWorkers=<int>           # Override worker group size\n-Dcom.linecorp.armeria.numCommonBlockingTaskThreads=<int> # Override blocking executor size\n-Dcom.linecorp.armeria.reportBlockedEventLoop=true       # Enable event loop blocking warnings (default: true)\n```\n\n### Server Builder Example\n\n```java\nServer.builder()\n      // Custom worker group (only if default is insufficient)\n      .workerGroup(16)\n      // Custom blocking executor for this server\n      .blockingTaskExecutor(BlockingTaskExecutor.builder()\n              .numThreads(100)\n              .threadNamePrefix(\"my-app-blocking\")\n              .build(), true)\n      .service(\"/api\", myService)\n      .build();\n```\n\n### Client Factory Example\n\n```java\nClientFactory.builder()\n      .workerGroup(8)\n      // Distribute HTTP/2 connections across more event loops for high-throughput endpoints\n      .maxNumEventLoopsPerEndpoint(4)\n      .build();\n```\n\n---\n\n## 4. Dos and Don'ts\n\n### DO: Use `@Blocking` for Synchronous Service Methods\n\n```java\n// Annotated service — method level\n@Get(\"/users/{id}\")\n@Blocking\npublic User getUser(@Param int id) {\n    return database.findById(id); // Safe: runs on blocking executor\n}\n\n// Annotated service — class level (all methods become blocking)\n@Blocking\npublic class MyDatabaseService {\n    @Get(\"/items\")\n    public List<Item> list() { return db.listAll(); }\n}\n```\n\n### DO: Use `useBlockingTaskExecutor(true)` for gRPC/Thrift Services\n\n```java\n// gRPC\nServer.builder()\n      .service(GrpcService.builder()\n              .addService(new MyBlockingGrpcService())\n              .useBlockingTaskExecutor(true)\n              .build());\n\n// Thrift\nServer.builder()\n      .service(\"/thrift\", THttpService.builder()\n              .addService(new MyBlockingThriftService())\n              .useBlockingTaskExecutor(true)\n              .build());\n```\n\n### DO: Use `ctx.blockingTaskExecutor()` for Ad-Hoc Blocking Work\n\n```java\n@Get(\"/report\")\npublic CompletableFuture<HttpResponse> generateReport(ServiceRequestContext ctx) {\n    return CompletableFuture.supplyAsync(() -> {\n        byte[] pdf = slowPdfGenerator.generate(); // blocking\n        return HttpResponse.of(HttpStatus.OK, MediaType.PDF, pdf);\n    }, ctx.blockingTaskExecutor());\n}\n```\n\nThe `ctx.blockingTaskExecutor()` returns a context-aware executor — it automatically propagates `ServiceRequestContext` to the blocking thread, so logging, tracing, and `ServiceRequestContext.current()` work correctly.\n\n### DO: Stay Async When Possible\n\n```java\n// Prefer returning async types on the event loop\n@Get(\"/data\")\npublic CompletableFuture<String> getData() {\n    return asyncHttpClient.get(\"/upstream\")\n            .thenApply(response -> response.contentUtf8());\n    // Runs entirely on event loop — no blocking, no thread switch\n}\n```\n\n### DON'T: Block the Event Loop\n\n```java\n// NEVER do this on the event loop\n@Get(\"/bad\")\npublic String bad() {\n    Thread.sleep(1000);                    // Blocks event loop\n    return db.query(\"SELECT ...\");         // Blocks event loop\n    future.get();                          // Blocks event loop\n    future.join();                         // Blocks event loop\n    synchronized(lock) { ... }             // Can block event loop\n    new URL(...).openConnection();         // Blocks event loop\n}\n```\n\nArmeria will log a warning if you call `.get()` or `.join()` on an `EventLoopCheckingFuture` from an event loop thread. Treat these warnings as bugs.\n\n### DON'T: Create Unbounded Thread Pools for Blocking Work\n\n```java\n// Don't do this — use Armeria's blockingTaskExecutor instead\nExecutorService myPool = Executors.newCachedThreadPool();\nmyPool.submit(() -> { ... });\n// Problems: no context propagation, no metrics, no coordinated shutdown\n```\n\n### DON'T: Mix Blocking and Non-Blocking in the Same Service Without Annotation\n\n```java\n// BAD: one blocking method taints the whole event loop experience\npublic class MixedService {\n    @Get(\"/fast\")\n    public String fast() { return \"ok\"; }\n\n    @Get(\"/slow\")\n    public String slow() {\n        return db.query(\"...\"); // Blocks! But no @Blocking annotation\n    }\n}\n```\n\nFix: add `@Blocking` to the slow method, or use `ctx.blockingTaskExecutor()` explicitly.\n\n### DON'T: Assume `serviceWorkerGroup` is Required\n\nMost applications should NOT configure `serviceWorkerGroup`. It adds complexity and is only useful when:\n- Your service does CPU-intensive work (e.g., serialization of large payloads) that could starve socket I/O\n- You want strict isolation between I/O handling and request processing\n\nFor blocking work, use `blockingTaskExecutor` instead — that's what it's for.\n\n### DON'T: Forget Context Propagation When Switching Threads\n\n```java\n// BAD: loses ServiceRequestContext\nsomeOtherExecutor.submit(() -> {\n    ServiceRequestContext.current(); // Throws IllegalStateException!\n});\n\n// GOOD: use context-aware executor\nctx.blockingTaskExecutor().submit(() -> {\n    ServiceRequestContext.current(); // Works\n});\n\n// GOOD: if you must use a custom executor, propagate context manually\nsomeOtherExecutor.submit(ctx.makeContextAware(() -> {\n    ServiceRequestContext.current(); // Works\n}));\n```\n\n---\n\n## 5. Programmatic Usage (Without Annotations)\n\nWhen you implement `HttpService` directly, build decorators, or work with gRPC/Thrift at a lower level, you manage executor dispatch yourself.\n\n### 5.1 Direct `HttpService` with Blocking Work\n\n```java\n// Implement HttpService directly — you control everything\nHttpService myService = (ctx, req) -> {\n    CompletableFuture<HttpResponse> future = new CompletableFuture<>();\n    ctx.blockingTaskExecutor().execute(() -> {\n        try {\n            String result = legacySyncClient.call(); // blocking\n            future.complete(HttpResponse.of(HttpStatus.OK, MediaType.JSON, result));\n        } catch (Exception e) {\n            future.completeExceptionally(e);\n        }\n    });\n    return HttpResponse.of(future);\n};\n\nServer.builder().service(\"/api\", myService).build();\n```\n\n### 5.2 CompletableFuture Chaining with Explicit Executors\n\nUse `thenApplyAsync` / `thenComposeAsync` / `supplyAsync` with the right executor to control where each stage runs.\n\n```java\nHttpService myService = (ctx, req) -> {\n    // Stage 1: blocking DB call on blocking executor\n    CompletableFuture<List<Long>> ids = CompletableFuture.supplyAsync(\n            () -> {\n                assert ServiceRequestContext.current() == ctx;  // context is propagated\n                assert !ctx.eventLoop().inEventLoop();           // not on event loop\n                return jdbcTemplate.queryForList(\"SELECT id FROM items\", Long.class);\n            },\n            ctx.blockingTaskExecutor());\n\n    // Stage 2: async HTTP calls back on event loop\n    CompletableFuture<List<AggregatedHttpResponse>> details = ids.thenComposeAsync(\n            idList -> {\n                assert ctx.eventLoop().inEventLoop();  // back on event loop\n                List<CompletableFuture<AggregatedHttpResponse>> futures = idList.stream()\n                        .map(id -> backendClient.get(\"/items/\" + id).aggregate())\n                        .collect(Collectors.toList());\n                return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))\n                        .thenApply(v -> futures.stream()\n                                // join() is safe here: allOf() guarantees all futures\n                                // are already complete, so join() returns immediately.\n                                .map(CompletableFuture::join)\n                                .collect(Collectors.toList()));\n            },\n            ctx.eventLoop());\n\n    // Stage 3: assemble response (still on event loop)\n    CompletableFuture<HttpResponse> response = details.thenApply(\n            results -> HttpResponse.of(HttpStatus.OK, MediaType.JSON, toJson(results)));\n\n    return HttpResponse.of(response);\n};\n```\n\n### 5.3 Scheduling Delayed Work on the Event Loop\n\nUse `ctx.eventLoop().schedule()` for non-blocking timed operations.\n\n```java\n// gRPC: respond after a delay without blocking a thread\nServiceRequestContext.current().eventLoop().schedule(() -> {\n    responseObserver.onNext(buildReply(request.getName()));\n    responseObserver.onCompleted();\n}, 3, TimeUnit.SECONDS);\n```\n\n### 5.4 gRPC: Submit Blocking Work Manually\n\nWhen you don't want `useBlockingTaskExecutor(true)` for the entire service, submit individual calls.\n\n```java\npublic class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {\n    @Override\n    public void hello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {\n        // This method runs on the event loop by default.\n        // Submit only the blocking part:\n        ServiceRequestContext.current().blockingTaskExecutor().submit(() -> {\n            try {\n                Thread.sleep(3000); // Simulate blocking API\n            } catch (Exception ignored) {}\n            responseObserver.onNext(buildReply(request.getName()));\n            responseObserver.onCompleted();\n        });\n    }\n}\n```\n\n### 5.5 Thrift: Submit Blocking Work Manually\n\nSame pattern as gRPC.\n\n```java\nServiceRequestContext.current().blockingTaskExecutor().execute(() -> {\n    try {\n        Thread.sleep(3000); // Simulate blocking\n    } catch (Exception ignored) {}\n    resultHandler.onComplete(buildReply(request.getName()));\n});\n```\n\n### 5.6 Decorators with Executor Awareness\n\n```java\n// A decorator that logs request duration, running the delegate as-is\nHttpService decorated = myService.decorate((delegate, ctx, req) -> {\n    long start = System.nanoTime();\n    return delegate.serve(ctx, req).mapHeaders(headers -> {\n        long elapsed = System.nanoTime() - start;\n        logger.info(\"{} took {} ms\", ctx.path(), elapsed / 1_000_000);\n        return headers;\n    });\n});\n```\n\n### 5.7 Context Propagation with `makeContextAware`\n\nWhen you must use a third-party executor that Armeria doesn't control, wrap your task.\n\n```java\n// Wrapping a Runnable\nsomeExternalExecutor.submit(ctx.makeContextAware(() -> {\n    ServiceRequestContext.current(); // Works — context is mounted\n    doWork();\n}));\n\n// Wrapping a Callable\nFuture<String> result = someExternalExecutor.submit(ctx.makeContextAware(() -> {\n    return ServiceRequestContext.current().path(); // Works\n}));\n\n// Wrapping a Function (for CompletableFuture chains)\nfuture.thenApply(ctx.makeContextAware(value -> {\n    ServiceRequestContext.current(); // Works\n    return transform(value);\n}));\n```\n\n### 5.8 Reactor Integration\n\n```java\nHttpService myService = (ctx, req) -> {\n    Scheduler blockingScheduler = Schedulers.fromExecutor(ctx.blockingTaskExecutor());\n    Scheduler eventLoopScheduler = Schedulers.fromExecutor(ctx.eventLoop());\n\n    Mono<String> result = Mono.fromCallable(() -> {\n                // Runs on blocking scheduler\n                return database.query(\"SELECT ...\");\n            })\n            .subscribeOn(blockingScheduler)\n            .flatMap(dbResult ->\n                // Switch back to event loop for async HTTP call\n                Mono.fromCompletionStage(backendClient.get(\"/enrich/\" + dbResult).aggregate())\n                    .subscribeOn(eventLoopScheduler)\n                    .map(resp -> resp.contentUtf8())\n            );\n\n    return HttpResponse.of(result.map(body ->\n            HttpResponse.of(HttpStatus.OK, MediaType.JSON, body)).toFuture());\n};\n```\n\n### 5.9 RxJava Integration\n\n```java\nScheduler blockingScheduler = Schedulers.from(ctx.blockingTaskExecutor());\nScheduler eventLoopScheduler = Schedulers.from(ctx.eventLoop());\n\nSingle<String> result = Single.fromCallable(() -> database.query(\"SELECT ...\"))\n        .subscribeOn(blockingScheduler)       // blocking work here\n        .observeOn(eventLoopScheduler)        // switch to event loop\n        .flatMap(dbResult ->\n            Single.fromCompletionStage(backendClient.get(\"/enrich/\" + dbResult).aggregate())\n                  .map(AggregatedHttpResponse::contentUtf8));\n```\n\n---\n\n## Quick Reference: Which Pool to Use\n\n| Scenario | Pool | How |\n|----------|------|-----|\n| HTTP/gRPC request handling (async) | workerGroup (event loop) | Default — just return `CompletableFuture` or reactive types |\n| Database queries, file I/O, sync APIs | blockingTaskExecutor | `@Blocking`, `useBlockingTaskExecutor(true)`, or `ctx.blockingTaskExecutor()` |\n| CPU-heavy non-blocking work that shouldn't starve I/O | serviceWorkerGroup | `ServerBuilder.serviceWorkerGroup(n)` (rare) |\n| Client HTTP calls | workerGroup (event loop) | Default — `WebClient` is async |\n| Legacy synchronous library calls | blockingTaskExecutor | Wrap with `ctx.blockingTaskExecutor().submit(...)` |\n| Delayed/scheduled non-blocking work | event loop | `ctx.eventLoop().schedule(...)` |\n| Reactor blocking operations | blockingTaskExecutor | `.subscribeOn(Schedulers.fromExecutor(ctx.blockingTaskExecutor()))` |\n| Custom third-party executor | context-wrap it | `ctx.makeContextAware(runnable)` |\n","tools/claude-plugins/assistant/skills/eventloop-audit/references/detection-patterns.md":"# Detection Patterns — 7 Categories\n\nSearch the entire project (or scoped path) for each category of blocking pattern. Exclude test files (`**/test/**`, `**/tests/**`) and generated code (`**/generated/**`, `**/target/**`, `**/build/**`).\n\n## Category 1: Direct Blocking Calls\n\nThese are explicit operations that block the calling thread.\n\n**Search patterns:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `\\.join\\(\\)` | `CompletableFuture.join()` — blocks indefinitely |\n| `\\.get\\(\\s*\\)` | `Future.get()` with no args — blocks indefinitely |\n| `\\.get\\(\\s*\\d` | `Future.get(timeout, unit)` — bounded but still blocks |\n| `Thread\\.sleep\\(` | `Thread.sleep()` — deliberate blocking |\n| `\\.await\\(` | `CountDownLatch.await()`, `Condition.await()`, `CyclicBarrier.await()` |\n| `\\.acquire\\(` | `Semaphore.acquire()` — blocks until permit available |\n| `\\.lock\\(\\)` | `ReentrantLock.lock()` — blocks until lock available |\n| `\\.lockInterruptibly\\(\\)` | Same, interruptible variant |\n| `\\.take\\(\\)` | `BlockingQueue.take()` — blocks until element available |\n| `\\.put\\(` | `BlockingQueue.put()` — blocks if queue full (check context) |\n| `\\.wait\\(` | `Object.wait()` — monitor wait |\n\n**Triage for each hit:**\n1. Identify the enclosing method\n2. Trace callers to determine if reachable from an event loop entry point (from Phase 1)\n3. Check if the call is inside a known safe execution wrapper\n4. Classify severity (see Phase 3)\n\n## Category 2: Blocking I/O Operations\n\nI/O operations always block the calling thread. The question is whether they run on the event loop.\n\n**2A — File system operations:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `new FileInputStream` | File reading |\n| `new FileOutputStream` | File writing |\n| `new RandomAccessFile` | File random access |\n| `FileChannel\\.open` | File channel (blocking despite NIO package) |\n| `Files\\.(read\\|write\\|copy\\|move\\|walk\\|list\\|newInputStream\\|newOutputStream)` | NIO file operations (still blocking despite being in `java.nio`) |\n| `ProcessBuilder.*\\.start\\(\\)` | Process execution |\n| `Runtime\\..*exec\\(` | Process execution |\n\n**2B — Network and HTTP operations:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `\\.openConnection\\(\\)` | `URL.openConnection()` — blocking HTTP |\n| `\\.openStream\\(\\)` | `URL.openStream()` — blocking HTTP |\n| `HttpURLConnection` | Legacy blocking HTTP client |\n| `HttpClient.*\\.send\\(` | `java.net.http.HttpClient.send()` — synchronous (`.sendAsync()` is safe) |\n| `new Socket\\(` | TCP socket creation |\n| `new ServerSocket\\(` | Server socket binding |\n| `Pipe\\.open\\(\\)` | NIO pipe creation — blocking despite NIO package |\n\n**2C — Database operations:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `DriverManager\\.getConnection` | JDBC connection creation |\n| `javax\\.sql\\.DataSource` | JDBC data source |\n| `java\\.sql\\.\\(Connection\\|Statement\\|PreparedStatement\\|ResultSet\\)` | JDBC operations |\n\n**2D — Serialization and compression:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `ObjectInputStream\\|ObjectOutputStream` | Java serialization (blocking I/O on streams) |\n| `GZIPInputStream\\|GZIPOutputStream` | GZIP compression (blocking I/O) |\n| `ZipInputStream\\|ZipOutputStream` | ZIP operations (blocking I/O) |\n| `DeflaterInputStream\\|InflaterOutputStream` | Raw deflate/inflate (blocking I/O) |\n\n**2E — Encryption and security:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `KeyStore\\.load\\(` | Keystore loading from disk — blocking file I/O |\n| `SSLContext\\.getInstance` | SSL context initialization — can trigger I/O |\n| `TrustManagerFactory\\.init` | Trust manager init — may load certificates |\n\n## Category 3: Synchronization That Could Contend on Event Loop\n\nLocks are only a problem if they can contend (block waiting) on an event loop thread.\n\n**Search patterns:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `synchronized\\s*\\(` | Synchronized blocks |\n| `\\bsynchronized\\b[^\\n{;]*\\(` | Synchronized methods (modifier in method signature) |\n| `ReentrantLock` | Explicit locking |\n| `ReentrantReadWriteLock` | Read-write locking |\n| `StampedLock` | Stamped locking |\n\n**Triage:**\n- `synchronized` on startup/config-only paths → **SAFE**\n- `synchronized` on request-path data accessed from event loop → **MEDIUM to HIGH** depending on contention probability\n- `synchronized` on a monitor that is also locked by a thread pool thread → **HIGH** (event loop can wait for thread pool)\n\n## Category 4: CompletableFuture Continuations on Wrong Executor\n\nWhen you chain `.thenApply()` (without `Async`), the continuation runs on whichever thread completes the source future. If that's an event loop thread, the continuation is on the event loop.\n\n**Search patterns (non-async variants — potentially dangerous):**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `\\.thenApply\\(` | Synchronous continuation — inherits completing thread |\n| `\\.thenAccept\\(` | Same |\n| `\\.thenRun\\(` | Same |\n| `\\.thenCompose\\(` | Same |\n| `\\.whenComplete\\(` | Same |\n| `\\.handle\\(` | Same |\n| `\\.exceptionally\\(` | Same |\n\n**For each hit, determine:**\n1. What completes the source future? (event loop thread? thread pool thread?)\n2. Does the continuation body do blocking work?\n3. If both answers are problematic → needs `*Async` variant with explicit executor\n\n**Important — `*Async()` without executor is NOT always safe:**\n```regex\n\\.thenApplyAsync\\(\n\\.thenAcceptAsync\\(\n\\.thenComposeAsync\\(\n```\nThe single-argument `*Async()` variants (e.g., `.thenApplyAsync(fn)`) use `ForkJoinPool.commonPool()`, NOT the blocking task executor. This is problematic if:\n- The continuation does blocking I/O → blocks a common pool thread, starving other ForkJoinPool work\n- The project expects `ServiceRequestContext` to be available → it won't be on ForkJoinPool threads\n\nOnly the two-argument variants (e.g., `.thenApplyAsync(fn, executor)`) with an explicit executor are reliably safe. Verify that the second argument is a blocking executor, not just any executor.\n\n## Category 5: Thread Pool Misconfiguration\n\n**Search patterns:**\n\n| Pattern | Risk |\n|---------|------|\n| `Executors\\.newCachedThreadPool` | Unbounded — can exhaust memory under load |\n| `Executors\\.newSingleThreadExecutor` | Single thread — no parallelism, instant bottleneck |\n| `new ThreadPoolExecutor\\(` | Check queue type and max size — unbounded `LinkedBlockingQueue` is risky |\n| `new ForkJoinPool\\(` | Check initialization is thread-safe, check parallelism level |\n\n**Also verify:**\n- Is `blockingTaskExecutor` configured in `ServerBuilder`? (If not, Armeria uses its default 200-thread pool)\n- Is the blocking executor sized for the expected concurrent blocking operations?\n- Are custom executors properly shut down on server close?\n\n## Category 6: Missing Armeria Idioms\n\nThese are patterns where the project doesn't use Armeria's built-in mechanisms for handling blocking work.\n\n**Check for:**\n\n1. **gRPC services doing blocking work without `useBlockingTaskExecutor(true)`**\n   - Find gRPC handler methods (from Phase 1.4)\n   - Check if the method body contains I/O, database calls, or blocking waits\n   - If yes and `useBlockingTaskExecutor(true)` is not set → **CRITICAL**\n\n2. **Annotated services with blocking methods missing `@Blocking`**\n   - Find `@Get`/`@Post`/etc. methods (from Phase 1.3)\n   - Check if the method body contains blocking operations\n   - If yes and `@Blocking` is absent → **CRITICAL**\n\n3. **Custom `HttpService.serve()` that blocks**\n   - Find `HttpService` implementations (from Phase 1.2)\n   - Check if `serve()` body contains blocking operations without offloading to `ctx.blockingTaskExecutor()`\n   - If yes → **CRITICAL**\n\n4. **Cancel handlers that block**\n   ```text\n   Grep: setOnCancelHandler|whenRequestCancelling\n   ```\n   - Cancel handler lambdas run on the event loop\n   - Check if they call `.get()`, `.join()`, I/O, or any blocking operation → **CRITICAL**\n\n5. **Missing context propagation**\n   ```text\n   Grep: \\.submit\\(|\\.execute\\(|\\.schedule\\(\n   ```\n   - If work is submitted to a non-Armeria executor, check for `ctx.makeContextAware()` wrapping\n   - Missing context propagation means `ServiceRequestContext.current()` will fail in the submitted task → **MEDIUM**\n\n## Category 7: Transitive / Hidden Blocking\n\nThese are blocking operations that are easy to overlook.\n\n**Search patterns:**\n\n| Pattern | Risk |\n|---------|------|\n| `InetAddress\\.getByName` | DNS resolution — can block for seconds |\n| `InetAddress\\.getAllByName` | Same |\n| `Class\\.forName` | Class loading — can trigger I/O to read class files |\n| `ServiceLoader\\.load` | Classpath scanning — iterates JARs |\n| `System\\.getProperty` | Usually fast, but can block with SecurityManager |\n\n**Configuration-level checks:**\n- **Logging framework**: Does the project use Logback or Log4j? Check `logback.xml` / `log4j2.xml` for synchronous appenders (`FileAppender`, `ConsoleAppender` without `AsyncAppender` wrapper). Every `log.info()` on the event loop becomes blocking I/O if appenders are synchronous.\n  ```text\n  Grep: <appender|<AsyncAppender|AsyncLogger\n  Scope: **/logback*.xml, **/log4j2*.xml, **/logging*.xml\n  ```\n",".agents/skills/release-note/SKILL.md":"---\nname: release-note\ndescription: Generate and polish release notes for an Armeria version. Runs site/release-note.ts to collect PR data from a GitHub milestone, then rewrites the skeletal output into publication-ready MDX. Invoked as `/release-note <version>` (e.g., `/release-note 1.38.0`).\n---\n\n# Release Note Generator\n\nGenerates skeletal release notes from a GitHub milestone using `site/release-note.ts`,\nthen rewrites them into polished, publication-ready MDX with rich descriptions, code examples,\nand proper formatting.\n\n## Prerequisites\n\n- The `gh` CLI must be authenticated with access to `line/armeria`. Verify with `gh auth status`.\n- A `GITHUB_ACCESS_TOKEN` environment variable is recommended for higher GitHub API rate limits.\n  If not set, the script falls back to anonymous access (lower rate limits).\n- Node.js and npm must be available. The `site/` directory must have dependencies installed\n  (`npm install` in `site/`).\n\n## Invocation\n\n```\n/release-note <version>\n```\n\nExample: `/release-note 1.38.0`\n\n---\n\n## Phase 0: Generate Skeletal Release Notes\n\n1. Verify the GitHub milestone exists for the given version by checking:\n   ```\n   gh api repos/line/armeria/milestones --jq '.[] | select(.title == \"<version>\") | .number'\n   ```\n2. Run the release note generation script:\n   ```\n   cd site && npm run release-note <version>\n   ```\n3. Verify the output file was created at `site/src/content/release-notes/<version>.mdx`.\n4. If the script fails (e.g., milestone not found, network error), report the error and stop.\n\n## Phase 1: Load Draft and Study Style\n\n1. Read the generated draft file at `site/src/content/release-notes/<version>.mdx`.\n2. Read 3-4 recent polished release notes (e.g., `1.36.0.mdx`, `1.37.0.mdx`) to calibrate tone and style.\n3. Read the style guide at `references/style-guide.md` for formatting rules.\n4. Extract all PR/issue references (`#NNNN`) from every line of the draft.\n\n## Phase 2: Gather PR Context from GitHub\n\nFor each unique PR number found in the draft:\n\n1. Fetch PR details:\n   ```\n   gh pr view <number> --repo line/armeria --json title,body,labels,files\n   ```\n2. Parse the PR body to extract **Motivation**, **Modifications**, and **Result** sections.\n3. Fetch PR review comments — these often contain important design decisions, caveats, and\n   scope limitations (e.g., \"this only applies to unary calls\") that are not in the PR description:\n   ```\n   gh api repos/line/armeria/pulls/<number>/comments --jq '.[].body'\n   ```\n4. Extract linked issue numbers from the body (`Closes #NNNN`, `Fixes #NNNN`, `Resolves #NNNN`).\n5. For each linked issue, fetch its context **including comments**, which often contain use cases,\n   edge cases, and design discussions that inform the release note description:\n   ```\n   gh issue view <number> --repo line/armeria --json title,body\n   gh api repos/line/armeria/issues/<number>/comments --jq '.[].body'\n   ```\n6. For PRs in the \"New features\" section that introduce significant new API:\n   - Read key changed source files from the PR's `files` list to understand method signatures.\n   - Look for usage examples in the PR description's Result section first — prefer these over\n     constructing examples from scratch.\n   - Check review comments for scope limitations, caveats, or known constraints that should be\n     mentioned in the release note (e.g., \"only supports unary methods\", \"HTTP/2 only\").\n\n**Rate limiting**: If fetching many PRs, batch requests and pause briefly between them to avoid\nGitHub API rate limits.\n\n## Phase 3: Triage \"Maybe Ignore\" Section\n\nThe script puts PRs without a recognized label into `🗑 Maybe ignore`. For each entry:\n\n1. Check the PR's labels and description.\n2. **Drop** items that are purely internal (CI config, build scripts, test infrastructure,\n   non-user-facing refactoring, site/docs dependency bumps).\n3. **Relocate** user-facing items to the correct section based on their actual impact:\n   - API additions → New features\n   - Performance or usability improvements → Improvements\n   - Bug fixes → Bug fixes\n   - Breaking API changes → Breaking changes\n4. Report triage decisions to the user so they can override if needed.\n\n## Phase 4: Rewrite Each Section\n\nRewrite every entry following the formatting rules in `references/style-guide.md`.\n\n### Ordering\n\n1. **Lead with user interest**: Place the top 3 entries that users would care about most first.\n   Prioritize broadly applicable features (core, gRPC, Kubernetes) over niche modules (Athenz, xDS).\n   Consider the size of the user base affected and how common the use case is.\n2. **Then group by module**: After the top 3, group remaining entries by module/area so that\n   related changes appear together (e.g., Athenz entries adjacent, Kubernetes entries adjacent).\n3. This applies to all sections (New features, Improvements, Bug fixes, etc.).\n\n### Key principles:\n\n### New Features (`🌟 New features`)\n\n- All new feature entries get a bold title prefix: `- **Feature Title**: Description. #NNNN`\n- **Keep descriptions concise** — most entries should fit within 3 lines of prose.\n  Only high-impact features (e.g., a brand-new module or paradigm-shifting API) warrant\n  longer descriptions.\n- Include a Java code example (5-15 lines) whenever possible. It may be omitted if there is\n  no clear usage pattern to show.\n  - Mark the most important line with `// 👈👈👈`\n  - Indent code blocks with 2 spaces under the bullet.\n\n### Improvements (`📈 Improvements`)\n\n- Concise description of what improved and why it matters.\n- Code examples only if the improvement changes how users interact with an API.\n\n### Bug Fixes (`🛠️ Bug fixes`)\n\n- Describe the symptom that was fixed, not the internal cause.\n- Format: \"[What was broken] now [works correctly]. #NNNN\"\n\n### Breaking Changes (`☢️ Breaking changes`)\n\n- State clearly what changed and what users must do to migrate.\n- Include before/after code if the migration is non-trivial.\n\n### Documentation (`📃 Documentation`)\n\n- Brief description with links to the new/updated docs if available.\n\n### Deprecations (`🏚️ Deprecations`)\n\n- State what is deprecated and what to use instead.\n\n### All Sections — Common Rules\n\n- Use `[ClassName](type)` for Armeria API types (classes, interfaces, annotations, methods).\n  - For classes/interfaces: `[GrpcServiceBuilder](type)`\n  - For methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always include the\n    class name, method name, and parameter types. Do NOT use backtick-only style like\n    `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - Do NOT use this syntax for JDK types (`String`, `Duration`, `CompletableFuture`), third-party\n    types, or types that are not part of Armeria's public API.\n- Issue and PR references go at the end of the entry: `#6431 #6691` (list both the issue and the PR)\n- Do NOT copy PR titles verbatim — they are often terse commit-style messages.\n- Do NOT fabricate code examples. Derive them from PR descriptions or actual source code.\n- Keep entries self-contained — a reader should understand the change without clicking the PR link.\n\n## Phase 5: Clean Up Dependencies Section\n\nThe raw script includes the full dependency update PR body, which uses a structured commit message format.\n\n1. **Strip build-only dependencies**: Remove the `- Build` section and all its sub-bullets\n   (these are testImplementation, annotationProcessor, and other non-production deps).\n2. **Format each entry**: `- LibraryName oldVersion → newVersion`\n   - Use the library's common name (e.g., `Jackson`, `Netty`, `gRPC-Java`, not the Maven artifact ID).\n   - Use `→` (unicode arrow), not `->`.\n3. **Group multi-version bumps** on one line when a library has multiple version streams:\n   `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n4. **Sort alphabetically** (A → Z).\n\n## Optional: Validate with a Site Build\n\n`cd site && npm run build` validates the MDX end to end, but requires generated files under\n`site/gen-src/` (`api-index.json`, `versions.json`, `contributors.json`):\n\n- Full generation: `./gradlew :site:generateSiteSources` (api-index needs the aggregated Javadoc,\n  which is slow).\n- Quick validation: copy `api-index.json`/`versions.json` from a previous build and generate\n  `contributors.json` with\n  `gh api --paginate repos/line/armeria/contributors | jq -s '[.[][]] | map({(.login): .avatar_url}) | add'`.\n  A stale `api-index.json` is fine for validation — unknown `[X](type)` names render as blank\n  links (`type://#`) and never fail the build, so type names must be verified against the source\n  either way.\n- `npm run build | tail` masks the exit code — capture it separately.\n\n## Phase 6: Finalize\n\n1. **Remove empty sections**: Delete any section whose only content is `- N/A`.\n2. **Remove \"Maybe ignore\"**: The `🗑 Maybe ignore` section must not appear in the final output.\n3. **Deduplicate and sort contributors**: Ensure `<ThankYou usernames={[...]} />` has\n   alphabetically sorted, deduplicated usernames. Remove bot accounts (`dependabot[bot]`,\n   `CLAassistant`). Core maintainers listed in `.github/CODEOWNERS` must always be included\n   in the Thank You section, even if they don't appear in the PR participant lists.\n4. **Ensure consistent bullet style**: Use `-` (dash) for all bullets, not `*`.\n5. **Write the final file** to `site/src/content/release-notes/<version>.mdx`.\n6. **Show a summary** to the user: list the sections, entry count per section, and any entries\n   flagged as uncertain (where PR context was insufficient to write a confident description).\n\n---\n\n## Phase 7: Lint\n\nRun the mechanical checks and fix everything they report:\n\n```\n.agents/skills/release-note/scripts/lint-release-note.sh site/src/content/release-notes/<version>.mdx\n```\n\nThis is not optional, and reading the style guide is not a substitute for running it. The rules it\nenforces are all written down elsewhere in this skill and have still been broken while the author\nhad them open — a malformed `(type)` link never fails the site build, it just renders as a blank\n`type://#` link, so nothing else catches it.\n\nThe script cannot see two things, so check them by hand:\n\n- **Identifiers copied from a PR body** — metric names, class names, proto fields. PR descriptions\n  go stale; the source does not. Grep the repo for every identifier you quote. (A real case: PR\n  #6840's body advertised `armeria.xds.lb.request`, while the code registered `lb.select`.)\n- **Whether a claim is true**, as opposed to well-formed. Read the code the entry describes.\n\n## Execution Checklist\n\n- [ ] Phase 0 — Ran `npm run release-note <version>` and verified output file exists\n- [ ] Phase 1 — Read draft, recent examples, and style guide\n- [ ] Phase 2 — Fetched PR/issue context for all referenced PRs\n- [ ] Phase 3 — Triaged all \"Maybe ignore\" entries\n- [ ] Phase 4 — Rewrote all entries per style guide\n- [ ] Phase 5 — Cleaned up dependencies section\n- [ ] Phase 6 — Removed empty sections, finalized file, reported summary\n- [ ] Phase 7 — Ran `lint-release-note.sh` and it exited 0\n\n## Common Mistakes to Avoid\n\n- **Copying PR titles as-is**: PR titles like \"Fix NPE in FooBar\" are not user-friendly.\n  Rewrite as \"Fixed a `NullPointerException` in [FooBar](type) when ...\"\n- **Fabricating code examples**: If you cannot find a clear usage pattern from the PR description\n  or source code, write a descriptive sentence instead of guessing at code.\n- **Over-linking types**: Only use `[Name](type)` for Armeria's own public API types, not for\n  JDK classes, third-party libraries, or internal classes.\n- **Including build dependencies**: The dependency update PR body contains `- Build` sub-bullets\n  for test/build-only deps. These must be stripped.\n- **Leaving `- N/A` sections**: The polished output should only contain sections with actual content.\n- **Using `*` bullets**: Standardize on `-` dashes for all bullet points.\n- **Missing `👈👈👈` callouts**: Every code example for a new feature should highlight the key line.\n",".claude/skills/release-note/SKILL.md":"---\nname: release-note\ndescription: Generate and polish release notes for an Armeria version. Runs site/release-note.ts to collect PR data from a GitHub milestone, then rewrites the skeletal output into publication-ready MDX. Invoked as `/release-note <version>` (e.g., `/release-note 1.38.0`).\n---\n\n# Release Note Generator\n\nGenerates skeletal release notes from a GitHub milestone using `site/release-note.ts`,\nthen rewrites them into polished, publication-ready MDX with rich descriptions, code examples,\nand proper formatting.\n\n## Prerequisites\n\n- The `gh` CLI must be authenticated with access to `line/armeria`. Verify with `gh auth status`.\n- A `GITHUB_ACCESS_TOKEN` environment variable is recommended for higher GitHub API rate limits.\n  If not set, the script falls back to anonymous access (lower rate limits).\n- Node.js and npm must be available. The `site/` directory must have dependencies installed\n  (`npm install` in `site/`).\n\n## Invocation\n\n```\n/release-note <version>\n```\n\nExample: `/release-note 1.38.0`\n\n---\n\n## Phase 0: Generate Skeletal Release Notes\n\n1. Verify the GitHub milestone exists for the given version by checking:\n   ```\n   gh api repos/line/armeria/milestones --jq '.[] | select(.title == \"<version>\") | .number'\n   ```\n2. Run the release note generation script:\n   ```\n   cd site && npm run release-note <version>\n   ```\n3. Verify the output file was created at `site/src/content/release-notes/<version>.mdx`.\n4. If the script fails (e.g., milestone not found, network error), report the error and stop.\n\n## Phase 1: Load Draft and Study Style\n\n1. Read the generated draft file at `site/src/content/release-notes/<version>.mdx`.\n2. Read 3-4 recent polished release notes (e.g., `1.36.0.mdx`, `1.37.0.mdx`) to calibrate tone and style.\n3. Read the style guide at `references/style-guide.md` for formatting rules.\n4. Extract all PR/issue references (`#NNNN`) from every line of the draft.\n\n## Phase 2: Gather PR Context from GitHub\n\nFor each unique PR number found in the draft:\n\n1. Fetch PR details:\n   ```\n   gh pr view <number> --repo line/armeria --json title,body,labels,files\n   ```\n2. Parse the PR body to extract **Motivation**, **Modifications**, and **Result** sections.\n3. Fetch PR review comments — these often contain important design decisions, caveats, and\n   scope limitations (e.g., \"this only applies to unary calls\") that are not in the PR description:\n   ```\n   gh api repos/line/armeria/pulls/<number>/comments --jq '.[].body'\n   ```\n4. Extract linked issue numbers from the body (`Closes #NNNN`, `Fixes #NNNN`, `Resolves #NNNN`).\n5. For each linked issue, fetch its context **including comments**, which often contain use cases,\n   edge cases, and design discussions that inform the release note description:\n   ```\n   gh issue view <number> --repo line/armeria --json title,body\n   gh api repos/line/armeria/issues/<number>/comments --jq '.[].body'\n   ```\n6. For PRs in the \"New features\" section that introduce significant new API:\n   - Read key changed source files from the PR's `files` list to understand method signatures.\n   - Look for usage examples in the PR description's Result section first — prefer these over\n     constructing examples from scratch.\n   - Check review comments for scope limitations, caveats, or known constraints that should be\n     mentioned in the release note (e.g., \"only supports unary methods\", \"HTTP/2 only\").\n\n**Rate limiting**: If fetching many PRs, batch requests and pause briefly between them to avoid\nGitHub API rate limits.\n\n## Phase 3: Triage \"Maybe Ignore\" Section\n\nThe script puts PRs without a recognized label into `🗑 Maybe ignore`. For each entry:\n\n1. Check the PR's labels and description.\n2. **Drop** items that are purely internal (CI config, build scripts, test infrastructure,\n   non-user-facing refactoring, site/docs dependency bumps).\n3. **Relocate** user-facing items to the correct section based on their actual impact:\n   - API additions → New features\n   - Performance or usability improvements → Improvements\n   - Bug fixes → Bug fixes\n   - Breaking API changes → Breaking changes\n4. Report triage decisions to the user so they can override if needed.\n\n## Phase 4: Rewrite Each Section\n\nRewrite every entry following the formatting rules in `references/style-guide.md`.\n\n### Ordering\n\n1. **Lead with user interest**: Place the top 3 entries that users would care about most first.\n   Prioritize broadly applicable features (core, gRPC, Kubernetes) over niche modules (Athenz, xDS).\n   Consider the size of the user base affected and how common the use case is.\n2. **Then group by module**: After the top 3, group remaining entries by module/area so that\n   related changes appear together (e.g., Athenz entries adjacent, Kubernetes entries adjacent).\n3. This applies to all sections (New features, Improvements, Bug fixes, etc.).\n\n### Key principles:\n\n### New Features (`🌟 New features`)\n\n- All new feature entries get a bold title prefix: `- **Feature Title**: Description. #NNNN`\n- **Keep descriptions concise** — most entries should fit within 3 lines of prose.\n  Only high-impact features (e.g., a brand-new module or paradigm-shifting API) warrant\n  longer descriptions.\n- Include a Java code example (5-15 lines) whenever possible. It may be omitted if there is\n  no clear usage pattern to show.\n  - Mark the most important line with `// 👈👈👈`\n  - Indent code blocks with 2 spaces under the bullet.\n\n### Improvements (`📈 Improvements`)\n\n- Concise description of what improved and why it matters.\n- Code examples only if the improvement changes how users interact with an API.\n\n### Bug Fixes (`🛠️ Bug fixes`)\n\n- Describe the symptom that was fixed, not the internal cause.\n- Format: \"[What was broken] now [works correctly]. #NNNN\"\n\n### Breaking Changes (`☢️ Breaking changes`)\n\n- State clearly what changed and what users must do to migrate.\n- Include before/after code if the migration is non-trivial.\n\n### Documentation (`📃 Documentation`)\n\n- Brief description with links to the new/updated docs if available.\n\n### Deprecations (`🏚️ Deprecations`)\n\n- State what is deprecated and what to use instead.\n\n### All Sections — Common Rules\n\n- Use `[ClassName](type)` for Armeria API types (classes, interfaces, annotations, methods).\n  - For classes/interfaces: `[GrpcServiceBuilder](type)`\n  - For methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always include the\n    class name, method name, and parameter types. Do NOT use backtick-only style like\n    `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - Do NOT use this syntax for JDK types (`String`, `Duration`, `CompletableFuture`), third-party\n    types, or types that are not part of Armeria's public API.\n- Issue and PR references go at the end of the entry: `#6431 #6691` (list both the issue and the PR)\n- Do NOT copy PR titles verbatim — they are often terse commit-style messages.\n- Do NOT fabricate code examples. Derive them from PR descriptions or actual source code.\n- Keep entries self-contained — a reader should understand the change without clicking the PR link.\n\n## Phase 5: Clean Up Dependencies Section\n\nThe raw script includes the full dependency update PR body, which uses a structured commit message format.\n\n1. **Strip non-user-facing dependencies**:\n   - Remove the `- Build` section and all its sub-bullets\n     (these are testImplementation, annotationProcessor, and other non-production deps).\n   - Remove **test-only** dependencies (e.g., Apache HttpClient, OkHttp, DGS) that are only\n     used in `testImplementation` / `testFixturesImplementation` and never appear in published POMs.\n   - Remove **shaded** dependencies (e.g., Guava, Bouncy Castle, JCTools) that are relocated\n     into Armeria JARs and invisible to users at runtime.\n   - Remove dependencies that are **not transitive** in any published artifact (e.g., Jsoup).\n   - When unsure, check `dependencies.toml` for the dependency's scope (`testImplementation`,\n     `shaded`, etc.) and verify whether it appears in a published module's `api` or\n     `implementation` configuration.\n2. **Format each entry**: `- LibraryName oldVersion → newVersion`\n   - Use the library's common name (e.g., `Jackson`, `Netty`, `gRPC-Java`, not the Maven artifact ID).\n   - Use `→` (unicode arrow), not `->`.\n3. **Group multi-version bumps** on one line when a library has multiple version streams:\n   `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n4. **Sort alphabetically** (A → Z).\n\n## Optional: Validate with a Site Build\n\n`cd site && npm run build` validates the MDX end to end, but requires generated files under\n`site/gen-src/` (`api-index.json`, `versions.json`, `contributors.json`):\n\n- Full generation: `./gradlew :site:generateSiteSources` (api-index needs the aggregated Javadoc,\n  which is slow).\n- Quick validation: copy `api-index.json`/`versions.json` from a previous build and generate\n  `contributors.json` with\n  `gh api --paginate repos/line/armeria/contributors | jq -s '[.[][]] | map({(.login): .avatar_url}) | add'`.\n  A stale `api-index.json` is fine for validation — unknown `[X](type)` names render as blank\n  links (`type://#`) and never fail the build, so type names must be verified against the source\n  either way.\n- `npm run build | tail` masks the exit code — capture it separately.\n\n## Phase 6: Finalize\n\n1. **Remove empty sections**: Delete any section whose only content is `- N/A`.\n2. **Remove \"Maybe ignore\"**: The `🗑 Maybe ignore` section must not appear in the final output.\n3. **Deduplicate and sort contributors**: Ensure `<ThankYou usernames={[...]} />` has\n   alphabetically sorted, deduplicated usernames. Remove bot accounts (`dependabot[bot]`,\n   `CLAassistant`). Core maintainers listed in `.github/CODEOWNERS` must always be included\n   in the Thank You section, even if they don't appear in the PR participant lists.\n4. **Ensure consistent bullet style**: Use `-` (dash) for all bullets, not `*`.\n5. **Write the final file** to `site/src/content/release-notes/<version>.mdx`.\n6. **Show a summary** to the user: list the sections, entry count per section, and any entries\n   flagged as uncertain (where PR context was insufficient to write a confident description).\n\n---\n\n## Phase 7: Lint\n\nRun the mechanical checks and fix everything they report:\n\n```\n.claude/skills/release-note/scripts/lint-release-note.sh site/src/content/release-notes/<version>.mdx\n```\n\nThis is not optional, and reading the style guide is not a substitute for running it. The rules it\nenforces are all written down elsewhere in this skill and have still been broken while the author\nhad them open — a malformed `(type)` link never fails the site build, it just renders as a blank\n`type://#` link, so nothing else catches it.\n\nThe script cannot see two things, so check them by hand:\n\n- **Identifiers copied from a PR body** — metric names, class names, proto fields. PR descriptions\n  go stale; the source does not. Grep the repo for every identifier you quote. (A real case: PR\n  #6840's body advertised `armeria.xds.lb.request`, while the code registered `lb.select`.)\n- **Whether a claim is true**, as opposed to well-formed. Read the code the entry describes.\n\n## Execution Checklist\n\n- [ ] Phase 0 — Ran `npm run release-note <version>` and verified output file exists\n- [ ] Phase 1 — Read draft, recent examples, and style guide\n- [ ] Phase 2 — Fetched PR/issue context for all referenced PRs\n- [ ] Phase 3 — Triaged all \"Maybe ignore\" entries\n- [ ] Phase 4 — Rewrote all entries per style guide\n- [ ] Phase 5 — Cleaned up dependencies section\n- [ ] Phase 6 — Removed empty sections, finalized file, reported summary\n- [ ] Phase 7 — Ran `lint-release-note.sh` and it exited 0\n\n## Common Mistakes to Avoid\n\n- **Copying PR titles as-is**: PR titles like \"Fix NPE in FooBar\" are not user-friendly.\n  Rewrite as \"Fixed a `NullPointerException` in [FooBar](type) when ...\"\n- **Fabricating code examples**: If you cannot find a clear usage pattern from the PR description\n  or source code, write a descriptive sentence instead of guessing at code.\n- **Over-linking types**: Only use `[Name](type)` for Armeria's own public API types, not for\n  JDK classes, third-party libraries, or internal classes.\n- **Including build dependencies**: The dependency update PR body contains `- Build` sub-bullets\n  for test/build-only deps. These must be stripped.\n- **Leaving `- N/A` sections**: The polished output should only contain sections with actual content.\n- **Using `*` bullets**: Standardize on `-` dashes for all bullet points.\n- **Missing `👈👈👈` callouts**: Every code example for a new feature should highlight the key line.\n",".claude/skills/upgrade-deps/SKILL.md":"---\nname: upgrade-deps\ndescription: Upgrade Gradle dependencies and Gradle wrapper version\ndisable-model-invocation: true\n---\n\n# Dependency Upgrade Workflow\n\nThis skill performs a full Gradle dependency upgrade for the Armeria project step by step.\n\n## Background: Minimum Java Version Per Module\n\nArmeria is a multi-module project with different minimum Java version requirements.\nBefore upgrading a dependency, always verify that the new version supports the minimum\nJava version required by the modules that use it.\n\n| Java Version | Modules |\n|-------------|---------|\n| **8**  | `core`, `brave5`, `brave6`, `eureka`, `grpc`, `grpc-kotlin`, `graphql-protocol`, `jetty9`, `junit4`, `junit5`, `kafka`, `kotlin`, `logback`, `logback12`, `logback13`, `micrometer-context`, `oauth2`, `prometheus1`, `protobuf`, `reactor3`, `resteasy`, `retrofit2`, `rxjava2`, `rxjava3`, `sangria`, `scala*`, `spring:boot2-*`, `dropwizard2`, `thrift0.9`–`thrift0.17`, `tomcat8`, `tomcat9`, `xds`, `zookeeper3`, `saml`, `bucket4j`, `consul`, `nacos` |\n| **11** | `athenz`, `graphql`, `jetty10`, `jetty11`, `kubernetes`, `logback14`, `thrift0.18`–`thrift0.22`, `tomcat10` |\n| **17** | `ai:mcp`, `jetty12`, `resilience4j2`, `spring:boot3-*`, `spring:spring6`, `spring:boot4-*`, `spring:spring7` |\n\nWhen in doubt, check `settings.gradle` for the `java`, `java11`, or `java17` flag on each subproject.\n\n**The table above lists published modules only.** Integration-test (`:it:*`) and example (`:examples:*`)\nmodules carry their own flags in `settings.gradle` and are frequently `java17` already. A dependency\nconsumed *only* by those modules is not held back by the Java 8 baseline at all — always resolve the\nactual consumer before assuming a Java constraint applies.\n\n## Step 1: Run dependencyUpdates\n\n```\n./gradlew dependencyUpdates --no-parallel\n```\n\nThe report is saved to `build/dependencyUpdates/report.txt`.\n\n## Step 2: Parse the Report\n\nRead `build/dependencyUpdates/report.txt` to identify all outdated dependencies.\nFor each dependency, note the current version and the available latest version.\n\nIf a version looks ambiguous (unexpected format, major version bump, unusual naming),\nverify on Maven Central before proceeding:\n  https://central.sonatype.com/artifact/{groupId}/{artifactId}\n\n## Step 3: Check Skip Hints and Java Version Compatibility\n\nBefore upgrading any dependency, check `dependencies.toml` for a comment directly above\nits version entry. Many pinned versions have an explicit reason, for example:\n\n```toml\n# Don't upgrade Caffeine to 3.x that requires Java 11.\ncaffeine = \"2.9.3\"\n\n# Upgrade once https://github.com/ronmamo/reflections/issues/279 is fixed.\nreflections = \"0.9.11\"\n\n# Ensure that we use the same ZooKeeper version as what Curator depends on.\nzookeeper = \"3.9.3\"\n```\n\n### Step 3.1: Validate the comment before trusting it\n\nA hold comment is a claim about this repository, and claims go stale. **Never treat a comment as\nproof — re-derive its reason every upgrade.** For each held entry:\n\n1. Find the aliases for the version key in the `[libraries.*]` / `[plugins]` sections.\n2. Grep the build scripts for each alias accessor (`libs.foo.bar` for alias `foo-bar`):\n   `git grep -n 'libs\\.foo\\.bar' -- '*.gradle' '*.gradle.kts'`\n3. Map each hit's build file to its project, and read that project's flags in `settings.gradle`.\n4. Compare the *consumer's* Java level against the new version's requirement — **not** the library's\n   requirement in isolation. \"Library X requires Java 17\" is not a blocker when every consumer is\n   already `java17`.\n\nIf the reason no longer holds, **remove the comment and upgrade**. If it still holds, keep both.\n\nTwo comments were found stale this way (`dgs` and `graphql-kotlin`, both consumed only by `java17`\nmodules), so treat this step as mandatory rather than a spot check.\n\n### Step 3.2: Never write a hold reason you have not verified\n\nWhen you add a comment, the reason must be something you confirmed, with the evidence to hand:\n\n- **Java constraint** — confirmed against the consumer module's flag, per Step 3.1.\n- **Runtime/API breakage** — confirmed by actually building and running the affected module's tests,\n  and the comment names the concrete blocker (e.g. `# Don't upgrade graphql-kotlin to 10.x that\n  migrated to Jackson 3`), not a vague \"major upgrade deferred\".\n- **Version-sync constraint** — confirmed from the other project's POM *and* from this repo's history\n  (`git log -p -- dependencies.toml`). Do not invent a sync rule: check whether the two versions have\n  ever actually moved independently before claiming they must move together.\n\nA plausible-sounding reason that nobody verified is worse than no comment, because the next upgrade\nwill trust it.\n\nIf no comment exists, proceed with the Java version compatibility check:\n1. Find which modules use this dependency in `dependencies.toml`\n2. Determine the minimum Java version required by those modules (see table above)\n3. Check the new library version's minimum Java requirement (Maven Central / release notes)\n4. If the library's required Java > module's minimum Java → **skip the upgrade** and add a\n   comment above the version entry explaining why (e.g. `# X.Y requires Java 11`)\n5. If compatible → proceed with the upgrade\n\n## Step 4: Update dependencies.toml\n\nEdit the `[versions]` section in `dependencies.toml` to update the version strings.\nThe file is located at the root of the repository: `dependencies.toml`.\n\n## Step 5: Cross-check Updated Versions\n\nAfter editing `dependencies.toml`, re-read the full diff (`git diff dependencies.toml`) and\ncross-check every changed entry against the `dependencyUpdates` report:\n\n1. **No entry was accidentally skipped** — compare the report's \"The following dependencies\n   have later milestone versions\" list against your edits. If a dependency appears in the\n   report but not in the diff, either upgrade it or document why it was skipped.\n2. **Versions match the report** — confirm each new version in the diff matches the latest\n   version from the report, not a typo or intermediate version.\n3. **Pinned-version comments were respected** — verify you did not upgrade a dependency whose\n   comment says to skip it, and that you did not remove or contradict an existing comment.\n4. **Linked-version constraints are satisfied** — some dependencies must stay in sync with\n   others (e.g. ZooKeeper with Curator, Protobuf with gRPC). Read the comments above those\n   entries and verify the constraint still holds after the upgrade.\n\n## Step 6: Upgrade Gradle Wrapper\n\nCheck the latest stable Gradle release at https://gradle.org/releases/ and update\n`gradle/wrapper/gradle-wrapper.properties`:\n\n```properties\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-X.Y.Z-all.zip\n```\n\n## Step 7: Verify the Build\n\nRun the full build including tests to catch both compilation errors and runtime regressions:\n\n```\n./gradlew build --no-daemon\n```\n\nIf there are compilation errors, API breaking changes, or test failures caused by the upgrade,\nfix them before proceeding.\n\n## Step 8: Commit\n\nCreate a commit with the **exact** message format below. Follow it strictly — do not add extra\nsections, reorder bullets, or change the structure:\n\n```\nUpdate dependencies\n\n- {library-name} {old-version} -> {new-version}\n- {library-name} {old-version} -> {new-version}\n- Build\n   - {library-name} {old-version} -> {new-version}\n   - {library-name} {old-version} -> {new-version}\n```\n\nRules:\n- Each production dependency (api/implementation scope) gets one bullet: `- {name} {old} -> {new}`\n- The `- Build` bullet groups build-only dependencies (testImplementation, annotationProcessor,\n  relocated libs, non-transitive deps); nest them as sub-bullets with 3-space indent\n- Use the library's official name if one exists (e.g. `gRPC-Java`, `Jackson`, `Netty`, `Kotlin`,\n  `Reactor`, `Logback`, `Micrometer`); otherwise use the key name as-is from `dependencies.toml`\n- Sort entries **alphabetically (A → Z)** within each section\n- If no build-only deps were upgraded, omit the `- Build` section entirely\n- If any dependencies were **not upgraded** (due to Java version constraints, pinned-version\n  comments, or linked-version constraints), list them under a `- Unupdated` section with the\n  reason. This helps reviewers know which upgrades were intentionally skipped.\n\nExample:\n```\nUpdate dependencies\n\n- gRPC-Java 1.63.0 -> 1.64.0\n- Jackson 2.17.0 -> 2.18.0\n- Build\n   - checkstyle 10.14.0 -> 10.17.0\n   - ErrorProne 2.27.0 -> 2.28.0\n- Unupdated\n   - Caffeine 2.9.3 -> 3.2.0 (requires Java 11)\n   - ZooKeeper 3.9.3 (pinned to Curator's version)\n```\n",".agents/skills/upgrade-deps/SKILL.md":"---\nname: upgrade-deps\ndescription: Upgrade Gradle dependencies and Gradle wrapper version\ndisable-model-invocation: true\n---\n\n# Dependency Upgrade Workflow\n\nThis skill performs a full Gradle dependency upgrade for the Armeria project step by step.\n\n## Background: Minimum Java Version Per Module\n\nArmeria is a multi-module project with different minimum Java version requirements.\nBefore upgrading a dependency, always verify that the new version supports the minimum\nJava version required by the modules that use it.\n\n| Java Version | Modules |\n|-------------|---------|\n| **8**  | `core`, `brave5`, `brave6`, `eureka`, `grpc`, `grpc-kotlin`, `graphql-protocol`, `jetty9`, `junit4`, `junit5`, `kafka`, `kotlin`, `logback`, `logback12`, `logback13`, `micrometer-context`, `oauth2`, `prometheus1`, `protobuf`, `reactor3`, `resteasy`, `retrofit2`, `rxjava2`, `rxjava3`, `sangria`, `scala*`, `spring:boot2-*`, `dropwizard2`, `thrift0.9`–`thrift0.17`, `tomcat8`, `tomcat9`, `xds`, `zookeeper3`, `saml`, `bucket4j`, `consul`, `nacos` |\n| **11** | `athenz`, `graphql`, `jetty10`, `jetty11`, `kubernetes`, `logback14`, `thrift0.18`–`thrift0.22`, `tomcat10` |\n| **17** | `ai:mcp`, `jetty12`, `resilience4j2`, `spring:boot3-*`, `spring:spring6`, `spring:boot4-*`, `spring:spring7` |\n\nWhen in doubt, check `settings.gradle` for the `java`, `java11`, or `java17` flag on each subproject.\n\n**The table above lists published modules only.** Integration-test (`:it:*`) and example (`:examples:*`)\nmodules carry their own flags in `settings.gradle` and are frequently `java17` already. A dependency\nconsumed *only* by those modules is not held back by the Java 8 baseline at all — always resolve the\nactual consumer before assuming a Java constraint applies.\n\n## Step 1: Run dependencyUpdates\n\n```\n./gradlew dependencyUpdates --no-parallel\n```\n\nThe report is saved to `build/dependencyUpdates/report.txt`.\n\n## Step 2: Parse the Report\n\nRead `build/dependencyUpdates/report.txt` to identify all outdated dependencies.\nFor each dependency, note the current version and the available latest version.\n\nIf a version looks ambiguous (unexpected format, major version bump, unusual naming),\nverify on Maven Central before proceeding:\n  https://central.sonatype.com/artifact/{groupId}/{artifactId}\n\n## Step 3: Check Skip Hints and Java Version Compatibility\n\nBefore upgrading any dependency, check `dependencies.toml` for a comment directly above\nits version entry. Many pinned versions have an explicit reason, for example:\n\n```toml\n# Don't upgrade Caffeine to 3.x that requires Java 11.\ncaffeine = \"2.9.3\"\n\n# Upgrade once https://github.com/ronmamo/reflections/issues/279 is fixed.\nreflections = \"0.9.11\"\n\n# Ensure that we use the same ZooKeeper version as what Curator depends on.\nzookeeper = \"3.9.3\"\n```\n\n### Step 3.1: Validate the comment before trusting it\n\nA hold comment is a claim about this repository, and claims go stale. **Never treat a comment as\nproof — re-derive its reason every upgrade.** For each held entry:\n\n1. Find the aliases for the version key in the `[libraries.*]` / `[plugins]` sections.\n2. Grep the build scripts for each alias accessor (`libs.foo.bar` for alias `foo-bar`):\n   `git grep -n 'libs\\.foo\\.bar' -- '*.gradle' '*.gradle.kts'`\n3. Map each hit's build file to its project, and read that project's flags in `settings.gradle`.\n4. Compare the *consumer's* Java level against the new version's requirement — **not** the library's\n   requirement in isolation. \"Library X requires Java 17\" is not a blocker when every consumer is\n   already `java17`.\n\nIf the reason no longer holds, **remove the comment and upgrade**. If it still holds, keep both.\n\nTwo comments were found stale this way (`dgs` and `graphql-kotlin`, both consumed only by `java17`\nmodules), so treat this step as mandatory rather than a spot check.\n\n### Step 3.2: Never write a hold reason you have not verified\n\nWhen you add a comment, the reason must be something you confirmed, with the evidence to hand:\n\n- **Java constraint** — confirmed against the consumer module's flag, per Step 3.1.\n- **Runtime/API breakage** — confirmed by actually building and running the affected module's tests,\n  and the comment names the concrete blocker (e.g. `# Don't upgrade graphql-kotlin to 10.x that\n  migrated to Jackson 3`), not a vague \"major upgrade deferred\".\n- **Version-sync constraint** — confirmed from the other project's POM *and* from this repo's history\n  (`git log -p -- dependencies.toml`). Do not invent a sync rule: check whether the two versions have\n  ever actually moved independently before claiming they must move together.\n\nA plausible-sounding reason that nobody verified is worse than no comment, because the next upgrade\nwill trust it.\n\nIf no comment exists, proceed with the Java version compatibility check:\n1. Find which modules use this dependency in `dependencies.toml`\n2. Determine the minimum Java version required by those modules (see table above)\n3. Check the new library version's minimum Java requirement (Maven Central / release notes)\n4. If the library's required Java > module's minimum Java → **skip the upgrade** and add a\n   comment above the version entry explaining why (e.g. `# X.Y requires Java 11`)\n5. If compatible → proceed with the upgrade\n\n## Step 4: Update dependencies.toml\n\nEdit the `[versions]` section in `dependencies.toml` to update the version strings.\nThe file is located at the root of the repository: `dependencies.toml`.\n\n## Step 5: Cross-check Updated Versions\n\nAfter editing `dependencies.toml`, re-read the full diff (`git diff dependencies.toml`) and\ncross-check every changed entry against the `dependencyUpdates` report:\n\n1. **No entry was accidentally skipped** — compare the report's \"The following dependencies\n   have later milestone versions\" list against your edits. If a dependency appears in the\n   report but not in the diff, either upgrade it or document why it was skipped.\n2. **Versions match the report** — confirm each new version in the diff matches the latest\n   version from the report, not a typo or intermediate version.\n3. **Pinned-version comments were respected** — verify you did not upgrade a dependency whose\n   comment says to skip it, and that you did not remove or contradict an existing comment.\n4. **Linked-version constraints are satisfied** — some dependencies must stay in sync with\n   others (e.g. ZooKeeper with Curator, Protobuf with gRPC). Read the comments above those\n   entries and verify the constraint still holds after the upgrade.\n\n## Step 6: Upgrade Gradle Wrapper\n\nCheck the latest stable Gradle release at https://gradle.org/releases/ and update\n`gradle/wrapper/gradle-wrapper.properties`:\n\n```properties\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-X.Y.Z-all.zip\n```\n\n## Step 7: Verify the Build\n\nRun the full build including tests to catch both compilation errors and runtime regressions:\n\n```\n./gradlew build --no-daemon\n```\n\nIf there are compilation errors, API breaking changes, or test failures caused by the upgrade,\nfix them before proceeding.\n\n## Step 8: Commit\n\nCreate a commit with the **exact** message format below. Follow it strictly — do not add extra\nsections, reorder bullets, or change the structure:\n\n```\nUpdate dependencies\n\n- {library-name} {old-version} -> {new-version}\n- {library-name} {old-version} -> {new-version}\n- Build\n   - {library-name} {old-version} -> {new-version}\n   - {library-name} {old-version} -> {new-version}\n```\n\nRules:\n- Each production dependency (api/implementation scope) gets one bullet: `- {name} {old} -> {new}`\n- The `- Build` bullet groups build-only dependencies (testImplementation, annotationProcessor,\n  relocated libs, non-transitive deps); nest them as sub-bullets with 3-space indent\n- Use the library's official name if one exists (e.g. `gRPC-Java`, `Jackson`, `Netty`, `Kotlin`,\n  `Reactor`, `Logback`, `Micrometer`); otherwise use the key name as-is from `dependencies.toml`\n- Sort entries **alphabetically (A → Z)** within each section\n- If no build-only deps were upgraded, omit the `- Build` section entirely\n- If any dependencies were **not upgraded** (due to Java version constraints, pinned-version\n  comments, or linked-version constraints), list them under a `- Unupdated` section with the\n  reason. This helps reviewers know which upgrades were intentionally skipped.\n\nExample:\n```\nUpdate dependencies\n\n- gRPC-Java 1.63.0 -> 1.64.0\n- Jackson 2.17.0 -> 2.18.0\n- Build\n   - checkstyle 10.14.0 -> 10.17.0\n   - ErrorProne 2.27.0 -> 2.28.0\n- Unupdated\n   - Caffeine 2.9.3 -> 3.2.0 (requires Java 11)\n   - ZooKeeper 3.9.3 (pinned to Curator's version)\n```\n","tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md":"# References Skill\n\nPath: `tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/line/armeria/HEAD/tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md)"},"files":{".claude-plugin/marketplace.json":"{\n  \"name\": \"armeria\",\n  \"owner\": {\n    \"name\": \"armerian\",\n    \"email\": \"dl_armeria@linecorp.com\"\n  },\n  \"metadata\": {\n    \"description\": \"Claude plugin for the Armeria microservice framework\",\n    \"version\": \"0.0.1\"\n  },\n  \"plugins\": [\n    {\n      \"name\": \"assistant\",\n      \"source\": {\n        \"source\": \"git-subdir\",\n        \"url\": \"https://github.com/line/armeria.git\",\n        \"path\": \"tools/claude-plugins/assistant\"\n      }\n    }\n  ]\n}\n","tools/claude-plugins/assistant/skills/eventloop-audit/SKILL.md":"---\nname: eventloop-audit\ndescription: Systematically audits any Armeria-based Java project for event loop blocking issues — discovers project patterns, scans for blocking operations, traces call chains, and produces a fix plan without modifying code. Use when experiencing latency spikes, connection timeouts, deadlocks, or thread starvation in Armeria services, or as a pre-release audit. Invoked as `/eventloop-audit` for the full project or `/eventloop-audit <path>` for a specific directory.\n---\n\n# Event Loop Blocking Audit for Armeria Projects\n\n## Overview\n\nA systematic, multi-phase audit that discovers how a project uses Armeria, scans for all categories of event loop blocking, traces call chains to determine thread context, and produces a structured report with fix plans. **No code is modified** — only analysis and planning.\n\nThis skill is **framework-generic** — it works on any Armeria-based Java project by discovering the project's patterns dynamically, not assuming specific class names or directory structures.\n\n## When to Use\n\n- After setting up a new Armeria-based project and want to verify thread safety\n- Before a release to audit for latent event loop blocking issues\n- When experiencing unexplained latency spikes, connection timeouts, or deadlocks in an Armeria server\n- After integrating a new library that may contain blocking calls\n- As a periodic health check on the codebase's non-blocking discipline\n\n## Thread Model Reference\n\nFor a comprehensive guide on Armeria's thread pools, executor selection, and programmatic usage patterns, see [armeria-thread-pool-guide.md](references/armeria-thread-pool-guide.md).\n\n**The Golden Rule**: If `Thread.currentThread() instanceof NonBlocking` — you **MUST NOT** block. Armeria's event loop threads implement the `NonBlocking` marker interface. Any blocking operation on these threads stalls ALL connections sharing that event loop, causing cascading latency and potential deadlocks.\n\n---\n\n## Phase 1: Discovery — Understand the Project's Armeria Usage\n\nBefore searching for problems, discover HOW the project uses Armeria. Launch an **Explore agent** (or do it yourself) with these specific searches. Record findings as they'll inform all subsequent phases.\n\n### 1.1 Find Armeria Server Configuration\n\n```text\nGrep: Server\\.builder\\(\\)|ServerBuilder\nScope: **/*.java\n```\n\nRead each match and document:\n- Worker group configuration (`workerGroup(n)`, `serviceWorkerGroup(n)`)\n- Blocking executor configuration (`blockingTaskExecutor(...)`)\n- Decorator chain (`.decorate(...)` calls)\n- Request/idle timeout settings\n- Any custom `EventLoopGroup` creation\n\n### 1.2 Find HttpService Implementations\n\n```text\nGrep: implements HttpService|extends AbstractHttpService|extends SimpleDecoratingHttpService\nScope: **/*.java\n```\n\nThese classes have `serve()` methods that run on the event loop. List all of them — they are the primary audit targets.\n\n### 1.3 Find Annotated Services\n\n```text\nGrep: @Get\\(|@Post\\(|@Put\\(|@Delete\\(|@Head\\(|@Patch\\(|@Options\\(\nScope: **/*.java\n```\n\nFor each, check if the method or class has `@Blocking`. Methods without `@Blocking` run on the event loop.\n\n### 1.4 Find gRPC Services\n\n```text\nGrep: extends \\w+ImplBase|GrpcService\\.builder\\(\\)\nScope: **/*.java\n```\n\nCheck whether `useBlockingTaskExecutor(true)` is configured in the `GrpcService.builder()`. If not, all gRPC handler methods run on the event loop.\n\n### 1.5 Find Thrift Services\n\n```text\nGrep: THttpService\\.builder\\(\\)|THttpService\\.of\\(\nScope: **/*.java\n```\n\nSame check — look for `useBlockingTaskExecutor(true)`.\n\n### 1.6 Find Decorators\n\n```text\nGrep: SimpleDecoratingHttpService|DecoratingHttpServiceFunction|\\.decorate\\(\nScope: **/*.java\n```\n\nEvery decorator's `serve()` method runs on the event loop. List them all.\n\n### 1.7 Find Custom Executors and Thread Pools\n\n```text\nGrep: blockingTaskExecutor\\(\\)|ctx\\.eventLoop\\(\\)|Executors\\.new|new ThreadPoolExecutor|new ForkJoinPool|ExecutorService\nScope: **/*.java\n```\n\nDocument all custom thread pools and how they're used. These are the \"safe zones\" where blocking is OK.\n\n### 1.8 Find Offloading Patterns\n\n```text\nGrep: supplyAsync|thenApplyAsync|thenComposeAsync|thenAcceptAsync|ctx\\.blockingTaskExecutor|makeContextAware\nScope: **/*.java\n```\n\nUnderstand the project's conventions for offloading work from the event loop. Look for:\n- **Base class methods** that submit work to a blocking executor (e.g., a `runBlocking(Supplier)` method in a base handler)\n- **Utility classes** that wrap `ctx.blockingTaskExecutor()` with additional logic (cancellation, metrics, tracing)\n- **Project-specific annotations** that trigger blocking executor dispatch (beyond Armeria's `@Blocking`)\n\nA wrapper is \"safe\" if it provably submits the lambda/callable to a non-event-loop executor before the lambda body executes. Read the wrapper's implementation to verify — don't trust method names alone.\n\n### 1.9 Build Architecture Map\n\nSynthesize findings into a concise map:\n- **Event loop entry points**: List of classes/methods where the event loop calls into application code\n- **Safe execution wrappers**: Project-specific methods that offload to blocking executors\n- **Thread pool inventory**: All thread pools, their sizing, and purpose\n- **Decorator chain**: The order in which decorators wrap services\n\n---\n\n## Phase 2: Systematic Detection — 7 Categories\n\nSearch the entire project (or scoped path) for each category of blocking pattern. Exclude test files (`**/test/**`, `**/tests/**`) and generated code (`**/generated/**`, `**/target/**`, `**/build/**`).\n\nSee [detection-patterns.md](references/detection-patterns.md) for the complete search patterns and triage guidance for all 7 categories:\n\n1. **Direct Blocking Calls** — `.join()`, `.get()`, `Thread.sleep()`, `.await()`, `.lock()`, etc.\n2. **Blocking I/O Operations** — File I/O, network, JDBC, serialization, encryption\n3. **Synchronization** — `synchronized`, `ReentrantLock`, `StampedLock` on request paths\n4. **CompletableFuture Continuations on Wrong Executor** — `.thenApply()` without `Async` where completing thread is event loop\n5. **Thread Pool Misconfiguration** — Unbounded pools, missing shutdown, undersized blocking executor\n6. **Missing Armeria Idioms** — Missing `@Blocking`, missing `useBlockingTaskExecutor(true)`, blocking cancel handlers, missing context propagation\n7. **Transitive / Hidden Blocking** — DNS resolution, class loading, synchronous logging appenders\n\n---\n\n## Phase 3: Call Chain Analysis\n\nFor each finding from Phase 2, perform call chain analysis to determine the thread context.\n\n### Analysis Steps\n\n1. **Identify enclosing method** — What method contains the potentially blocking call?\n\n2. **Find all callers** — Grep for the method name across the project:\n   ```text\n   Grep: methodName\\(\n   Scope: **/*.java\n   ```\n   When handling overloaded methods (same name, different signatures), read each call site to confirm it calls the blocking variant. Also check superclass/interface hierarchies — a `serve()` override in a subclass is still called from the event loop if the parent is an `HttpService`.\n\n3. **Trace to entry point** — Follow the call chain upward until you reach one of:\n   - An Armeria entry point (`serve()`, `@Get`/`@Post` handler, gRPC handler, decorator)\n   - A thread pool submission (`blockingTaskExecutor().execute(...)`, `supplyAsync(...)`, etc.)\n   - A framework callback (`whenRequestCancelling`, `setOnCancelHandler`)\n   - A lambda/method reference passed to an executor (`.submit(this::methodName)`)\n\n   **Handling ambiguity**: If a method is called from BOTH event loop paths AND thread pool paths, classify based on the worst case (event loop path). Note both paths in the finding.\n\n4. **Determine thread context** — At the entry point:\n   - Is there a `@Blocking` annotation? → Safe\n   - Is there `useBlockingTaskExecutor(true)`? → Safe\n   - Is it inside a `supplyAsync(..., blockingExecutor)` call? → Safe\n   - Is it inside a project-specific safe wrapper (discovered in Phase 1.8)? → Safe\n   - None of the above? → **Event loop**\n\n5. **Classify severity using this decision tree:**\n\n```text\nIs the blocking call provably inside a blocking executor or thread pool?\n├── YES → SAFE (exclude from report or list separately)\n├── NO, it's provably on the event loop:\n│   ├── Is it an I/O operation, .join(), .get() without timeout, or Thread.sleep()?\n│   │   └── YES → CRITICAL\n│   ├── Is it .get(timeout, unit) or .await(timeout, unit)?\n│   │   └── YES → HIGH (blocks event loop for the timeout duration)\n│   ├── Is it a synchronized block or lock?\n│   │   ├── Can it contend with a thread that holds the lock for >1ms?\n│   │   │   └── YES → HIGH\n│   │   └── Low contention, very fast critical section?\n│   │       └── LOW\n│   └── Is it a very fast operation (System.getProperty, in-memory map lookup)?\n│       └── LOW\n└── UNDETERMINED (e.g., CF continuation where completing thread varies):\n    ├── Could the continuation body block?\n    │   ├── YES → MEDIUM (must verify at runtime or refactor to be safe)\n    │   └── NO (continuation is pure computation) → LOW\n    └── Is the source future always completed by a thread pool?\n        └── YES → SAFE\n```\n\n**Key distinction**: MEDIUM means \"we cannot statically prove the thread context\" — it requires either runtime verification (add logging/assertion) or defensive refactoring (use `*Async` variant with explicit executor).\n\n---\n\n## Phase 4: Fix Plan Templates\n\nFor each non-SAFE finding, recommend one of the fix approaches documented in [fix-templates.md](references/fix-templates.md):\n\n| Template | When to use |\n|----------|-------------|\n| **FP-1** | Replace `.join()`/`.get()` with async chaining |\n| **FP-2** | Offload blocking work to `ctx.blockingTaskExecutor()` |\n| **FP-3** | Add `@Blocking` annotation to annotated service methods |\n| **FP-4** | Enable `useBlockingTaskExecutor(true)` for gRPC/Thrift services |\n| **FP-5** | Use `*Async` CF variant with explicit executor |\n| **FP-6** | Add timeout to unavoidable blocking |\n| **FP-7** | Make thread pool initialization thread-safe |\n| **FP-8** | Wire context propagation with `makeContextAware` |\n\n**Document the recommendation — do not apply it.**\n\n---\n\n## Phase 5: Produce the Report\n\n### Per-Finding Format\n\n```markdown\n## Finding [ID]: [Short descriptive title]\n\n**Severity**: CRITICAL / HIGH / MEDIUM / LOW\n**File**: [relative path]:[line number]\n**Thread context**: Event loop / Blocking executor / Undetermined\n**Detection category**: [1-7 with category name]\n\n### Description\n[What the code does and why it's a blocking risk on the event loop]\n\n### Call chain\n[Armeria entry point (event loop)] → [Method A] → [Method B] → [blocking call at line X]\n\n### Recommended fix\n[Reference FP-1 through FP-8, with specific application to this case:\n which method to change, what the new code should look like, which executor to use]\n\n### Risk assessment\n[What could break when applying the fix, edge cases to consider, tests to run]\n```\n\n### Summary Table\n\nAt the end of all findings, produce a summary:\n\n```markdown\n| # | Severity | File | Line | Category | Blocking Pattern | Fix |\n|---|----------|------|------|----------|-----------------|-----|\n| 1 | CRITICAL | FooService.java | 42 | 1 - Direct blocking | `.join()` | FP-1 |\n| 2 | HIGH | BarHandler.java | 88 | 2 - I/O | FileInputStream | FP-2 |\n| ... | ... | ... | ... | ... | ... | ... |\n```\n\n### Statistics\n\n```markdown\n- Total findings: [N]\n- CRITICAL: [N] | HIGH: [N] | MEDIUM: [N] | LOW: [N] | SAFE (excluded): [N]\n- By category: Cat.1: [N] | Cat.2: [N] | Cat.3: [N] | Cat.4: [N] | Cat.5: [N] | Cat.6: [N] | Cat.7: [N]\n```\n\n### Follow-Up Recommendations\n\nAlways include these recommendations regardless of findings:\n\n1. **Runtime detection**: Consider adding [BlockHound](https://github.com/reactor/BlockHound) to the test suite — it detects blocking calls on non-blocking threads at runtime\n2. **Async logging**: Verify that the logging framework uses async appenders in production to avoid blocking on `log.*()` calls\n3. **Event loop monitoring**: Enable Armeria's built-in event loop blocking detection:\n   ```text\n   -Dcom.linecorp.armeria.reportBlockedEventLoop=true\n   ```\n4. **Code review checklist**: Add \"Does this handler offload blocking work?\" to the team's code review checklist for Armeria services\n\n---\n\n## Execution Checklist\n\nFollow this checklist to ensure nothing is skipped. Check off each item as you complete it.\n\n### Phase 1: Discovery\n- [ ] 1.1 — Found and documented Armeria server configuration(s)\n- [ ] 1.2 — Listed all `HttpService` implementations\n- [ ] 1.3 — Listed all annotated services, noted `@Blocking` presence/absence\n- [ ] 1.4 — Listed all gRPC services, noted `useBlockingTaskExecutor` presence/absence\n- [ ] 1.5 — Listed all Thrift services (if any)\n- [ ] 1.6 — Listed all decorators\n- [ ] 1.7 — Documented all custom executors and thread pools\n- [ ] 1.8 — Identified project-specific offloading patterns\n- [ ] 1.9 — Built architecture map (entry points, safe wrappers, thread pools)\n\n### Phase 2: Detection\n- [ ] Category 1 — Searched for all direct blocking calls\n- [ ] Category 2 — Searched for all blocking I/O operations\n- [ ] Category 3 — Searched for synchronization on request paths\n- [ ] Category 4 — Audited CompletableFuture continuation executors\n- [ ] Category 5 — Audited thread pool configurations\n- [ ] Category 6 — Checked for missing Armeria idioms (@Blocking, useBlockingTaskExecutor, cancel handlers, context propagation)\n- [ ] Category 7 — Searched for transitive/hidden blocking (DNS, class loading, logging config)\n\n### Phase 3: Analysis\n- [ ] Traced call chains for ALL non-obvious findings\n- [ ] Classified every finding by severity\n- [ ] Excluded SAFE findings from the report (or listed them separately)\n\n### Phase 4: Fix Planning\n- [ ] Assigned a fix template (FP-1 through FP-8) to each non-SAFE finding\n- [ ] Documented specific fix details for each finding\n\n### Phase 5: Reporting\n- [ ] Produced per-finding reports\n- [ ] Produced summary table\n- [ ] Produced statistics\n- [ ] Included follow-up recommendations\n\n---\n\n## Common Mistakes to Avoid\n\n- **Stopping at grep hits** — A grep hit is not a finding. You must trace the call chain to determine thread context. Many `.join()` calls are inside thread pool code and are perfectly safe.\n- **Ignoring non-async CF continuations** — `.thenApply()` without `Async` is the most subtle source of event loop blocking. The continuation runs on whatever thread completes the source future.\n- **Missing cancel handlers** — Cancel handler lambdas always run on the event loop. They're easy to overlook but are a common source of blocking.\n- **Assuming `synchronized` is always bad** — It's only a problem if (a) it's on a request path reachable from the event loop AND (b) the lock can be contended by another thread holding it for a long time.\n- **Forgetting transitive blocking** — A clean `serve()` method that calls `helperMethod()` which calls `utilMethod()` which calls `.join()` is still blocking the event loop.\n- **Ignoring logging** — If the logging framework uses synchronous file appenders, every `log.info()` on the event loop is a blocking I/O operation. This is a configuration issue, not a code issue, but still critical.\n",".claude/skills/release-note/references/style-guide.md":"# Release Notes Style Guide\n\nAnnotated examples from real Armeria release notes showing the exact formatting conventions.\n\n---\n\n## Major New Feature Entry\n\nBold title, description explaining value, `[Type](type)` links, code example with `👈👈👈`, PR refs.\n\n```mdx\n- **Standalone Athenz Token Validation**: You can now use [AthenzAuthorizer](type) to validate Athenz tokens\n  outside of Armeria's request pipeline. This allows you to easily integrate Athenz authorization into\n  third-party frameworks like Spring MVC or other servlet-based applications. #6607\n  ```java\n  ZtsBaseClient ztsBaseClient = ...;\n  AthenzAuthorizer authorizer =\n    AthenzAuthorizer.builder(ztsBaseClient)\n                    .policyConfig(new AthenzPolicyConfig(\"your-domain\"))\n                    .build();\n\n  // Validate tokens anywhere\n  AccessCheckStatus status =\n    authorizer.authorize(token, resource, action); // 👈👈👈\n  ```\n```\n\nKey elements:\n- `**Bold Title**:` prefix — only for significant features, not every entry\n- Description explains *what you can now do* and *why it matters*\n- `[AthenzAuthorizer](type)` — Armeria API type link\n- Code block indented under the bullet (2 spaces)\n- `// 👈👈👈` on the line the reader should focus on\n- `#6607` issue/PR references at end of description line (list both the issue and the PR)\n\n---\n\n## Minor New Feature Entry\n\nOne-liner without code example, for smaller additions.\n\n```mdx\n- **Per-Request Client TLS Configuration**: You can now specify [ClientTlsSpec](type) for each request using\n  [RequestOptions#clientTlsSpec()](type) or [ClientRequestContext#clientTlsSpec()](type). #6551\n```\n\nOr even simpler:\n\n```mdx\n- [ConnectionPoolListener](type) now supports ping-related events. #6539 #6552\n```\n\n---\n\n## Improvement Entry\n\nConcise description with type links. No code example unless it changes user-facing API.\n\n```mdx\n- [XdsBootstrap](type) now supports SDS. #6597 #6610 #6654\n- Improved warning logs by adding diagnostic context to frequent HTTP/2 connection errors. #6638\n```\n\n---\n\n## Bug Fix Entry\n\nDescribe the symptom that was fixed, not the internal root cause.\n\n```mdx\n- [GrpcMeterIdPrefixFunction](type) records `grpc-status` correctly for responses which fail with an exception. #6606 #6621\n- `X-Forwarded-For` header values with leading or trailing whitespace around comma-separated addresses\n  (e.g., `\"192.168.1.1 , 10.0.0.1\"`) are now trimmed and parsed correctly. #6615\n- Setting `pingIntervalMillis` to a value greater than 33 seconds no longer throws an exception. #6648\n  - Linux keepalive socket options (`SO_KEEPALIVE` `TCP_KEEPIDLE`, `TCP_KEEPINTVL`) are no longer set by default.\n```\n\nKey patterns:\n- \"[Thing] now [works correctly]\" or \"[Thing] no longer [does wrong thing]\"\n- Sub-bullets for related side effects or additional context\n- Use backticks for config names, error types, and header names\n\n---\n\n## Breaking Change Entry\n\nState what changed and what users must do to migrate.\n\n```mdx\n- A subclass of [AbstractEndpointSelector](type) must implement [AbstractEndpointSelector#doSelectNow(ClientRequestContext)](type)\n  instead of [AbstractEndpointSelector#selectNow(ClientRequestContext)](type), which is now a final method. #6535\n```\n\nFor non-trivial migrations, include before/after code.\n\n---\n\n## Documentation Entry\n\nBrief description with links to new/updated docs.\n\nLink to docs pages with a **site-relative path** (`/docs/...`), never an absolute `https://armeria.dev/...`\nURL — that is the convention across the docs and the newer release notes, and it keeps links working\non local builds and previews.\n\n```mdx\n- New comprehensive documentation on understanding and handling timeouts: #6592\n  - [Understanding timeout and cancellation origins](/docs/advanced/understanding-timeouts)\n  - Handling timeouts for streaming:\n    - [Client-side streaming](/docs/client/timeouts#handling-timeouts-for-streaming-responses)\n    - [Server-side streaming](/docs/server/timeouts#handling-timeouts-for-streaming-requests)\n```\n\n---\n\n## Dependencies Section\n\nFormat: `- LibraryName oldVersion → newVersion`, sorted alphabetically.\n\n```mdx\n- Athenz 1.12.31 → 1.12.33\n- BlockHound 1.0.15 → 1.0.16\n- Jackson 2.20.1 → 2.21.0\n- Kotlin 2.2.21 → 2.3.0\n- Logback 1.5.23 → 1.5.27\n- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3\n- Spring Boot 3.5.8 → 3.5.10, 4.0.1 → 4.0.2\n```\n\nRules:\n- Use `→` (unicode arrow), not `->` or `-->`\n- Use `-` dash bullets, not `*`\n- Use the library's common name, not Maven artifact IDs\n- Group multi-version bumps: `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n- Omit build-only dependencies (anything under the `- Build` section in the raw dependency PR)\n- Omit **test-only** dependencies (e.g., Apache HttpClient, OkHttp, DGS) — only used in\n  `testImplementation` and not in published POMs\n- Omit **shaded** dependencies (e.g., Guava, Bouncy Castle, JCTools) — relocated into Armeria\n  JARs and invisible to users\n- Omit dependencies that are **not transitive** in any published artifact (e.g., Jsoup)\n- When unsure, check `dependencies.toml` for scope and whether the dep appears in a published\n  module's `api` or `implementation` configuration\n- Sort alphabetically (A → Z)\n- Strip trailing release qualifiers from version numbers — `.Final`, `.RELEASE`, `-GA`,\n  `.vYYYYMMDD` and the like. They add no information a reader acts on:\n  - `Netty 4.2.15.Final → 4.2.16.Final` becomes `Netty 4.2.15 → 4.2.16`\n  - `Javassist 3.31.0-GA → 3.32.0-GA` becomes `Javassist 3.31.0 → 3.32.0`\n  - `Jetty 9.4.55.v20240627 → 9.4.58.v20250814` becomes `Jetty 9.4.55 → 9.4.58`\n\n---\n\n## Thank You Section\n\n```mdx\n## 🙇 Thank you\n\n<ThankYou usernames={[\n  'contributor1',\n  'contributor2',\n  'contributor3'\n]} />\n```\n\nRules:\n- Usernames sorted alphabetically\n- Remove bot accounts: `dependabot[bot]`, `CLAassistant`\n- Core maintainers from `.github/CODEOWNERS` must always be included\n- One username per line, single-quoted, comma-separated\n\n---\n\n## What NOT to Include\n\n- **`- N/A` entries**: Remove sections that have no content instead of writing `- N/A`\n- **`🗑 Maybe ignore` section**: Must be fully triaged and removed\n- **Build-only dependencies**: `- Build` sub-section from dependency PRs\n- **Internal refactoring details**: Changes with no user-facing impact\n- **Site/docs dependency bumps**: npm bumps for `site/` or `docs-client/` (e.g., webpack, lodash)\n\n## Type Link Rules\n\nUse `[ClassName](type)` ONLY for Armeria public API references:\n- Classes/interfaces: `[GrpcServiceBuilder](type)`\n- Methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always use the full\n  `ClassName#methodName(ParamType)` form. Never use backtick-only style like\n  `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - **No space after commas** in the parameter list. The lookup key in `gen-src/api-index.json`\n    has none, so `[X#m(A, B)](type)` silently resolves to a dead `type://#` link while\n    `[X#m(A,B)](type)` works. The rendered text hides the difference — it shows `X.m()` either way.\n  - **Never split a method across a class link and a backtick.** `[Foo](type)` `` `.bar()` `` is\n    not a link to `bar()`; write `[Foo#bar(Baz)](type)` as one unit.\n- Annotations: `[@Blocking](type)`\n\nDo NOT use `[Name](type)` for:\n- JDK types (`String`, `Duration`, `CompletableFuture`, `List`)\n- Third-party types (`Mono`, `Flux`, `HttpServletRequest`)\n- Internal/private Armeria classes\n- Generic concepts (use plain text or backticks instead)\n- Method calls inside code blocks — code blocks use plain Java syntax, not `(type)` links\n",".agents/skills/release-note/references/style-guide.md":"# Release Notes Style Guide\n\nAnnotated examples from real Armeria release notes showing the exact formatting conventions.\n\n---\n\n## Major New Feature Entry\n\nBold title, description explaining value, `[Type](type)` links, code example with `👈👈👈`, PR refs.\n\n```mdx\n- **Standalone Athenz Token Validation**: You can now use [AthenzAuthorizer](type) to validate Athenz tokens\n  outside of Armeria's request pipeline. This allows you to easily integrate Athenz authorization into\n  third-party frameworks like Spring MVC or other servlet-based applications. #6607\n  ```java\n  ZtsBaseClient ztsBaseClient = ...;\n  AthenzAuthorizer authorizer =\n    AthenzAuthorizer.builder(ztsBaseClient)\n                    .policyConfig(new AthenzPolicyConfig(\"your-domain\"))\n                    .build();\n\n  // Validate tokens anywhere\n  AccessCheckStatus status =\n    authorizer.authorize(token, resource, action); // 👈👈👈\n  ```\n```\n\nKey elements:\n- `**Bold Title**:` prefix — only for significant features, not every entry\n- Description explains *what you can now do* and *why it matters*\n- `[AthenzAuthorizer](type)` — Armeria API type link\n- Code block indented under the bullet (2 spaces)\n- `// 👈👈👈` on the line the reader should focus on\n- `#6607` issue/PR references at end of description line (list both the issue and the PR)\n\n---\n\n## Minor New Feature Entry\n\nOne-liner without code example, for smaller additions.\n\n```mdx\n- **Per-Request Client TLS Configuration**: You can now specify [ClientTlsSpec](type) for each request using\n  [RequestOptions#clientTlsSpec()](type) or [ClientRequestContext#clientTlsSpec()](type). #6551\n```\n\nOr even simpler:\n\n```mdx\n- [ConnectionPoolListener](type) now supports ping-related events. #6539 #6552\n```\n\n---\n\n## Improvement Entry\n\nConcise description with type links. No code example unless it changes user-facing API.\n\n```mdx\n- [XdsBootstrap](type) now supports SDS. #6597 #6610 #6654\n- Improved warning logs by adding diagnostic context to frequent HTTP/2 connection errors. #6638\n```\n\n---\n\n## Bug Fix Entry\n\nDescribe the symptom that was fixed, not the internal root cause.\n\n```mdx\n- [GrpcMeterIdPrefixFunction](type) records `grpc-status` correctly for responses which fail with an exception. #6606 #6621\n- `X-Forwarded-For` header values with leading or trailing whitespace around comma-separated addresses\n  (e.g., `\"192.168.1.1 , 10.0.0.1\"`) are now trimmed and parsed correctly. #6615\n- Setting `pingIntervalMillis` to a value greater than 33 seconds no longer throws an exception. #6648\n  - Linux keepalive socket options (`SO_KEEPALIVE` `TCP_KEEPIDLE`, `TCP_KEEPINTVL`) are no longer set by default.\n```\n\nKey patterns:\n- \"[Thing] now [works correctly]\" or \"[Thing] no longer [does wrong thing]\"\n- Sub-bullets for related side effects or additional context\n- Use backticks for config names, error types, and header names\n\n---\n\n## Breaking Change Entry\n\nState what changed and what users must do to migrate.\n\n```mdx\n- A subclass of [AbstractEndpointSelector](type) must implement [AbstractEndpointSelector#doSelectNow(ClientRequestContext)](type)\n  instead of [AbstractEndpointSelector#selectNow(ClientRequestContext)](type), which is now a final method. #6535\n```\n\nFor non-trivial migrations, include before/after code.\n\n---\n\n## Documentation Entry\n\nBrief description with links to new/updated docs.\n\nLink to docs pages with a **site-relative path** (`/docs/...`), never an absolute `https://armeria.dev/...`\nURL — that is the convention across the docs and the newer release notes, and it keeps links working\non local builds and previews.\n\n```mdx\n- New comprehensive documentation on understanding and handling timeouts: #6592\n  - [Understanding timeout and cancellation origins](/docs/advanced/understanding-timeouts)\n  - Handling timeouts for streaming:\n    - [Client-side streaming](/docs/client/timeouts#handling-timeouts-for-streaming-responses)\n    - [Server-side streaming](/docs/server/timeouts#handling-timeouts-for-streaming-requests)\n```\n\n---\n\n## Dependencies Section\n\nFormat: `- LibraryName oldVersion → newVersion`, sorted alphabetically.\n\n```mdx\n- Athenz 1.12.31 → 1.12.33\n- BlockHound 1.0.15 → 1.0.16\n- Jackson 2.20.1 → 2.21.0\n- Kotlin 2.2.21 → 2.3.0\n- Logback 1.5.23 → 1.5.27\n- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3\n- Spring Boot 3.5.8 → 3.5.10, 4.0.1 → 4.0.2\n```\n\nRules:\n- Use `→` (unicode arrow), not `->` or `-->`\n- Use `-` dash bullets, not `*`\n- Use the library's common name, not Maven artifact IDs\n- Group multi-version bumps: `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n- Omit build-only dependencies (anything under the `- Build` section in the raw dependency PR)\n- Sort alphabetically (A → Z)\n- Strip trailing release qualifiers from version numbers — `.Final`, `.RELEASE`, `-GA`,\n  `.vYYYYMMDD` and the like. They add no information a reader acts on:\n  - `Netty 4.2.15.Final → 4.2.16.Final` becomes `Netty 4.2.15 → 4.2.16`\n  - `Javassist 3.31.0-GA → 3.32.0-GA` becomes `Javassist 3.31.0 → 3.32.0`\n  - `Jetty 9.4.55.v20240627 → 9.4.58.v20250814` becomes `Jetty 9.4.55 → 9.4.58`\n\n---\n\n## Thank You Section\n\n```mdx\n## 🙇 Thank you\n\n<ThankYou usernames={[\n  'contributor1',\n  'contributor2',\n  'contributor3'\n]} />\n```\n\nRules:\n- Usernames sorted alphabetically\n- Remove bot accounts: `dependabot[bot]`, `CLAassistant`\n- Core maintainers from `.github/CODEOWNERS` must always be included\n- One username per line, single-quoted, comma-separated\n\n---\n\n## What NOT to Include\n\n- **`- N/A` entries**: Remove sections that have no content instead of writing `- N/A`\n- **`🗑 Maybe ignore` section**: Must be fully triaged and removed\n- **Build-only dependencies**: `- Build` sub-section from dependency PRs\n- **Internal refactoring details**: Changes with no user-facing impact\n- **Site/docs dependency bumps**: npm bumps for `site/` or `docs-client/` (e.g., webpack, lodash)\n\n## Type Link Rules\n\nUse `[ClassName](type)` ONLY for Armeria public API references:\n- Classes/interfaces: `[GrpcServiceBuilder](type)`\n- Methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always use the full\n  `ClassName#methodName(ParamType)` form. Never use backtick-only style like\n  `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - **No space after commas** in the parameter list. The lookup key in `gen-src/api-index.json`\n    has none, so `[X#m(A, B)](type)` silently resolves to a dead `type://#` link while\n    `[X#m(A,B)](type)` works. The rendered text hides the difference — it shows `X.m()` either way.\n  - **Never split a method across a class link and a backtick.** `[Foo](type)` `` `.bar()` `` is\n    not a link to `bar()`; write `[Foo#bar(Baz)](type)` as one unit.\n- Annotations: `[@Blocking](type)`\n\nDo NOT use `[Name](type)` for:\n- JDK types (`String`, `Duration`, `CompletableFuture`, `List`)\n- Third-party types (`Mono`, `Flux`, `HttpServletRequest`)\n- Internal/private Armeria classes\n- Generic concepts (use plain text or backticks instead)\n- Method calls inside code blocks — code blocks use plain Java syntax, not `(type)` links\n",".claude/skills/release-note/scripts/lint-release-note.sh":"#!/usr/bin/env bash\n# Mechanical checks for a release note. Prose rules only fire if someone remembers to apply them\n# document-wide; these run. Exits non-zero on any finding.\n#\n#   .claude/skills/release-note/scripts/lint-release-note.sh site/src/content/release-notes/1.41.0.mdx\n\nset -uo pipefail\n\nf=\"${1:?usage: lint-release-note.sh <release-note.mdx>}\"\n[ -f \"$f\" ] || { echo \"no such file: $f\" >&2; exit 2; }\n\nfail=0\n# check <description> <grep-output>; prints each matched line indented, preserving line breaks.\ncheck() {\n  [ -n \"$2\" ] || return 0\n  fail=1\n  printf '\\n✗ %s\\n' \"$1\"\n  printf '%s\\n' \"$2\" | sed 's/^/    /'\n}\n\n# --- type links -------------------------------------------------------------\n# A class link glued to a backticked method is not a link to that method.\nhits=$(grep -n '\\](type)`\\.' \"$f\" || true)\ncheck \"class link + backtick method (write [Foo#bar(Baz)](type) as one unit)\" \"$hits\"\n\n# api-index keys carry no space after commas; a space yields a dead type://# link.\nhits=$(grep -nE '\\[[A-Za-z0-9_.]+#[^]]*, [^]]*\\]\\(type\\)' \"$f\" || true)\ncheck \"space after comma in a method type link (silently dead)\" \"$hits\"\n\n# --- links ------------------------------------------------------------------\nhits=$(grep -n '](https://armeria.dev/' \"$f\" || true)\ncheck \"absolute armeria.dev link (use a site-relative /docs/... path)\" \"$hits\"\n\n# --- leftovers from the generator ------------------------------------------\nhits=$(grep -n '^- N/A' \"$f\" || true)\ncheck \"empty section left as '- N/A' (delete the section)\" \"$hits\"\n\nhits=$(grep -n 'Maybe ignore' \"$f\" || true)\ncheck \"'🗑 Maybe ignore' section must be triaged away\" \"$hits\"\n\nhits=$(grep -nE '^\\s*\\*\\s' \"$f\" || true)\ncheck \"'*' bullet (use '-')\" \"$hits\"\n\n# --- dependencies -----------------------------------------------------------\ndeps=$(sed -n '/^## ⛓ Dependencies/,/^## /p' \"$f\")\nhits=$(printf '%s' \"$deps\" | grep -n ' -> ' || true)\ncheck \"ASCII arrow in Dependencies (use →)\" \"$hits\"\n\n# Strip stable-release metadata only; -RC1/-M1/-SNAPSHOT identify distinct builds and must stay.\nhits=$(printf '%s' \"$deps\" | grep -nE '[0-9](\\.(Final|RELEASE|GA)|-GA|\\.v[0-9]{6,})' || true)\ncheck \"release qualifier left in a version (.Final/.RELEASE/-GA/.vYYYYMMDD)\" \"$hits\"\n\nhits=$(printf '%s' \"$deps\" | grep -n '^- Build' || true)\ncheck \"build-only dependency section leaked in\" \"$hits\"\n\n# --- formatting -------------------------------------------------------------\nhits=$(awk 'length>104 {printf \"%d: (%d chars)\\n\", FNR, length}' \"$f\" || true)\ncheck \"line longer than 104 chars\" \"$hits\"\n\nif [ $fail -eq 0 ]; then\n  echo \"✓ $(basename \"$f\"): all mechanical checks passed\"\nelse\n  printf '\\nSome checks failed. Note that a bad type link never fails the site build —\\n'\n  printf 'it renders as a blank type://# link, so this script is the only thing that catches it.\\n'\nfi\nexit $fail\n","tools/claude-plugins/assistant/.claude-plugin/plugin.json":"{\n  \"name\": \"assistant\"\n}\n","tools/claude-plugins/assistant/skills/eventloop-audit/references/armeria-thread-pool-guide.md":"<!-- A developer-facing version of this guide is published at /docs/advanced/threading-model -->\n\n# Armeria Thread Pool Guide for Developers\n\n## Context\n\nArmeria is built on Netty's non-blocking I/O model. Misusing thread pools — especially blocking the event loop — is the most common cause of performance degradation and deadlocks. This guide provides an architectural overview, recommended configurations, and practical dos/don'ts for developers working with Armeria.\n\n---\n\n## 1. Architectural Overview\n\n### Thread Pool Roles\n\n| Pool | What it does | Thread name pattern | Default size | Daemon? |\n|------|-------------|---------------------|-------------|---------|\n| **bossGroup** | Accepts incoming TCP connections (one per server port) | `armeria-boss-{protocol}-{addr}` where addr is `*:{port}`, `{ip}:{port}`, or `unix:{path}` | **1 per port** (fixed, not configurable) | No |\n| **workerGroup** | Handles all socket I/O (reads/writes) and executes non-blocking service logic | `armeria-common-worker-*` | `2 * CPU cores` (NIO/epoll/kqueue) or `CPU cores` (io_uring) | Yes |\n| **serviceWorkerGroup** | Optional dedicated EventLoopGroup for service method execution, isolating service logic from socket I/O | User-defined | Falls back to workerGroup if not set | Yes |\n| **blockingTaskExecutor** | Runs long-running or blocking operations (DB calls, file I/O, legacy sync APIs) | `armeria-common-blocking-tasks-*` | **200** threads (inspired by Tomcat default) | Yes |\n\n### How a Request Flows Through the Pools\n\n```text\nClient request\n    |\n    v\n[bossGroup]  -- accepts TCP connection, hands channel to workerGroup\n    |\n    v\n[workerGroup]  -- reads bytes, decodes HTTP/2 frames, invokes service\n    |\n    +---> Non-blocking service: runs directly on event loop thread\n    |\n    +---> @Blocking / useBlockingTaskExecutor(true): dispatched to blockingTaskExecutor\n    |\n    +---> serviceWorkerGroup configured: service runs on dedicated event loop\n```\n\n### Key Classes and Files\n\n- `CommonPools` — global shared pools (`core/.../common/CommonPools.java`)\n- `EventLoopGroups` — factory for creating EventLoopGroups (`core/.../common/util/EventLoopGroups.java`)\n- `BlockingTaskExecutor` / `BlockingTaskExecutorBuilder` — blocking pool abstraction (`core/.../common/util/`)\n- `EventLoopThread` — custom thread extending Netty's `FastThreadLocalThread`, implements `NonBlocking` marker (`core/.../internal/common/util/EventLoopThread.java`)\n- `EventLoopCheckingFuture` — warns when `.get()`/`.join()` is called from an event loop (`core/.../common/util/EventLoopCheckingFuture.java`)\n- `DefaultAnnotatedService` — dispatches to event loop or blocking executor based on `@Blocking` (`core/.../internal/server/annotation/DefaultAnnotatedService.java`)\n- `Flags` / `DefaultFlagsProvider` — system-wide defaults and JVM flag overrides (`core/.../common/Flags.java`)\n\n### Client-Side Thread Pools\n\nThe HTTP client (`WebClient`) uses the same `CommonPools.workerGroup()` by default. An `EventLoopScheduler` distributes connections across event loops per endpoint (default: 1 event loop per endpoint, configurable).\n\nKey client config class: `ClientFactoryBuilder` (`core/.../client/ClientFactoryBuilder.java`)\n\n---\n\n## 2. `serviceWorkerGroup` vs `blockingTaskExecutor` — When to Use Which\n\nThese two are the most commonly confused. They solve **different problems** and have fundamentally different runtime characteristics.\n\n### The Core Distinction\n\n| Aspect | `serviceWorkerGroup` | `blockingTaskExecutor` |\n|--------|----------------------|------------------------|\n| **Java type** | `EventLoopGroup` (Netty event loops) | `ScheduledExecutorService` (thread pool) |\n| **Thread type** | Event loop threads (`NonBlocking`) | Regular threads (blocking allowed) |\n| **Can you block in it?** | **NO** — same rules as workerGroup | **YES** — that's its purpose |\n| **Scheduling model** | Single-threaded per loop, run-to-completion | Traditional thread pool, one thread per task |\n| **Typical size** | Small (matches CPU cores) | Large (default 200) |\n\n### How They Work at Runtime\n\n**serviceWorkerGroup** — The service method still runs on an event loop, just a *different* one from the I/O channel. In `HttpServerHandler`, Armeria checks whether the configured serviceWorkerGroup differs from the main workerGroup. If so, it calls `req.subscribeOn(serviceEventLoop)` and invokes the service on that loop. Request reading, service execution, and response emission all happen on the service event loop. Response *writing* to the socket still goes back to the channel's original event loop.\n\n```text\n[workerGroup event loop]  reads bytes from socket\n        |\n        v  (if serviceWorkerGroup != workerGroup)\n[serviceWorkerGroup event loop]  decodes request, runs service, emits response\n        |\n        v  (response writing)\n[workerGroup event loop]  writes bytes to socket\n```\n\n**blockingTaskExecutor** — The service method is submitted via `thenApplyAsync(..., ctx.blockingTaskExecutor())` to a traditional thread pool. The calling event loop is freed immediately. The blocking thread runs the service logic, and when done, the response flows back to the event loop for writing.\n\n```text\n[event loop]  reads bytes, decodes request\n        |\n        v  (thenApplyAsync)\n[blocking thread pool]  runs service method (blocking OK here)\n        |\n        v  (future completes)\n[event loop]  writes response bytes to socket\n```\n\n### Decision Guide\n\n**Use `serviceWorkerGroup` when:**\n\n- Your service does **CPU-intensive but non-blocking** work (e.g., JSON serialization of large payloads, image transformation in memory, complex computation)\n- You want to **prevent service CPU work from starving socket I/O** — if your service burns 50ms of CPU per request, that delays read/write for all other connections sharing that event loop\n- You need **isolation between services** — e.g., a high-throughput service shouldn't compete for event loop time with a latency-sensitive one\n- Your code is already fully async (returns `CompletableFuture`, uses reactive streams) — you just want it on a different event loop\n\n```java\n// Good use case: CPU-heavy serialization isolated from I/O\nServer.builder()\n      .serviceWorkerGroup(4)  // Dedicated event loops for service logic\n      .service(\"/heavy-json\", (ctx, req) -> {\n          // Non-blocking but CPU-intensive: serialize huge object\n          return HttpResponse.of(hugeObjectMapper.writeValueAsBytes(bigData));\n      })\n      .build();\n```\n\n**Use `blockingTaskExecutor` when:**\n\n- Your code calls **synchronous blocking APIs** (JDBC, file I/O, `Thread.sleep`, `future.get()`, legacy HTTP clients, `synchronized` blocks)\n- You're integrating with **libraries that block the calling thread** (most traditional Java libraries)\n- You have **no choice** but to wait for a result synchronously\n\n```java\n// Good use case: JDBC call that blocks the thread\nServer.builder()\n      .service(\"/users\", (ctx, req) ->\n          HttpResponse.of(CompletableFuture.supplyAsync(() -> {\n              // This blocks — JDBC has no async API\n              User user = jdbcTemplate.queryForObject(\"SELECT ...\", User.class);\n              return HttpResponse.of(HttpStatus.OK, MediaType.JSON, toJson(user));\n          }, ctx.blockingTaskExecutor())))\n      .build();\n```\n\n**DON'T use `serviceWorkerGroup` for blocking work** — it's still an event loop. Blocking it has the same catastrophic effect as blocking the main workerGroup. You're just blocking a *different* event loop.\n\n**DON'T use `blockingTaskExecutor` for CPU-bound async work** — it wastes a thread sitting idle during async operations and doesn't benefit from event loop optimizations (FastThreadLocal, zero-copy buffers, etc.).\n\n### When You Need Neither\n\nMost async services need neither. If your service just orchestrates async HTTP calls, reactive streams, or CompletableFuture chains, let it run on the default workerGroup.\n\n---\n\n## 3. Recommended Configuration Settings\n\n### Sizing Guidelines\n\n| Pool | Guideline |\n|------|-----------|\n| **workerGroup** | Default (`2 * CPU`) is usually fine. Increase only if profiling shows event loop saturation on I/O-heavy workloads. |\n| **blockingTaskExecutor** | Default 200 is generous. Size based on expected concurrent blocking operations. Consider: `expected_concurrent_blocking_calls * avg_blocking_duration / target_latency`. |\n| **serviceWorkerGroup** | Only configure if you need to isolate service execution from socket I/O (e.g., CPU-heavy request processing that shouldn't delay I/O). Most services don't need this. |\n\n### JVM Flags\n\n```properties\n-Dcom.linecorp.armeria.numCommonWorkers=<int>           # Override worker group size\n-Dcom.linecorp.armeria.numCommonBlockingTaskThreads=<int> # Override blocking executor size\n-Dcom.linecorp.armeria.reportBlockedEventLoop=true       # Enable event loop blocking warnings (default: true)\n```\n\n### Server Builder Example\n\n```java\nServer.builder()\n      // Custom worker group (only if default is insufficient)\n      .workerGroup(16)\n      // Custom blocking executor for this server\n      .blockingTaskExecutor(BlockingTaskExecutor.builder()\n              .numThreads(100)\n              .threadNamePrefix(\"my-app-blocking\")\n              .build(), true)\n      .service(\"/api\", myService)\n      .build();\n```\n\n### Client Factory Example\n\n```java\nClientFactory.builder()\n      .workerGroup(8)\n      // Distribute HTTP/2 connections across more event loops for high-throughput endpoints\n      .maxNumEventLoopsPerEndpoint(4)\n      .build();\n```\n\n---\n\n## 4. Dos and Don'ts\n\n### DO: Use `@Blocking` for Synchronous Service Methods\n\n```java\n// Annotated service — method level\n@Get(\"/users/{id}\")\n@Blocking\npublic User getUser(@Param int id) {\n    return database.findById(id); // Safe: runs on blocking executor\n}\n\n// Annotated service — class level (all methods become blocking)\n@Blocking\npublic class MyDatabaseService {\n    @Get(\"/items\")\n    public List<Item> list() { return db.listAll(); }\n}\n```\n\n### DO: Use `useBlockingTaskExecutor(true)` for gRPC/Thrift Services\n\n```java\n// gRPC\nServer.builder()\n      .service(GrpcService.builder()\n              .addService(new MyBlockingGrpcService())\n              .useBlockingTaskExecutor(true)\n              .build());\n\n// Thrift\nServer.builder()\n      .service(\"/thrift\", THttpService.builder()\n              .addService(new MyBlockingThriftService())\n              .useBlockingTaskExecutor(true)\n              .build());\n```\n\n### DO: Use `ctx.blockingTaskExecutor()` for Ad-Hoc Blocking Work\n\n```java\n@Get(\"/report\")\npublic CompletableFuture<HttpResponse> generateReport(ServiceRequestContext ctx) {\n    return CompletableFuture.supplyAsync(() -> {\n        byte[] pdf = slowPdfGenerator.generate(); // blocking\n        return HttpResponse.of(HttpStatus.OK, MediaType.PDF, pdf);\n    }, ctx.blockingTaskExecutor());\n}\n```\n\nThe `ctx.blockingTaskExecutor()` returns a context-aware executor — it automatically propagates `ServiceRequestContext` to the blocking thread, so logging, tracing, and `ServiceRequestContext.current()` work correctly.\n\n### DO: Stay Async When Possible\n\n```java\n// Prefer returning async types on the event loop\n@Get(\"/data\")\npublic CompletableFuture<String> getData() {\n    return asyncHttpClient.get(\"/upstream\")\n            .thenApply(response -> response.contentUtf8());\n    // Runs entirely on event loop — no blocking, no thread switch\n}\n```\n\n### DON'T: Block the Event Loop\n\n```java\n// NEVER do this on the event loop\n@Get(\"/bad\")\npublic String bad() {\n    Thread.sleep(1000);                    // Blocks event loop\n    return db.query(\"SELECT ...\");         // Blocks event loop\n    future.get();                          // Blocks event loop\n    future.join();                         // Blocks event loop\n    synchronized(lock) { ... }             // Can block event loop\n    new URL(...).openConnection();         // Blocks event loop\n}\n```\n\nArmeria will log a warning if you call `.get()` or `.join()` on an `EventLoopCheckingFuture` from an event loop thread. Treat these warnings as bugs.\n\n### DON'T: Create Unbounded Thread Pools for Blocking Work\n\n```java\n// Don't do this — use Armeria's blockingTaskExecutor instead\nExecutorService myPool = Executors.newCachedThreadPool();\nmyPool.submit(() -> { ... });\n// Problems: no context propagation, no metrics, no coordinated shutdown\n```\n\n### DON'T: Mix Blocking and Non-Blocking in the Same Service Without Annotation\n\n```java\n// BAD: one blocking method taints the whole event loop experience\npublic class MixedService {\n    @Get(\"/fast\")\n    public String fast() { return \"ok\"; }\n\n    @Get(\"/slow\")\n    public String slow() {\n        return db.query(\"...\"); // Blocks! But no @Blocking annotation\n    }\n}\n```\n\nFix: add `@Blocking` to the slow method, or use `ctx.blockingTaskExecutor()` explicitly.\n\n### DON'T: Assume `serviceWorkerGroup` is Required\n\nMost applications should NOT configure `serviceWorkerGroup`. It adds complexity and is only useful when:\n- Your service does CPU-intensive work (e.g., serialization of large payloads) that could starve socket I/O\n- You want strict isolation between I/O handling and request processing\n\nFor blocking work, use `blockingTaskExecutor` instead — that's what it's for.\n\n### DON'T: Forget Context Propagation When Switching Threads\n\n```java\n// BAD: loses ServiceRequestContext\nsomeOtherExecutor.submit(() -> {\n    ServiceRequestContext.current(); // Throws IllegalStateException!\n});\n\n// GOOD: use context-aware executor\nctx.blockingTaskExecutor().submit(() -> {\n    ServiceRequestContext.current(); // Works\n});\n\n// GOOD: if you must use a custom executor, propagate context manually\nsomeOtherExecutor.submit(ctx.makeContextAware(() -> {\n    ServiceRequestContext.current(); // Works\n}));\n```\n\n---\n\n## 5. Programmatic Usage (Without Annotations)\n\nWhen you implement `HttpService` directly, build decorators, or work with gRPC/Thrift at a lower level, you manage executor dispatch yourself.\n\n### 5.1 Direct `HttpService` with Blocking Work\n\n```java\n// Implement HttpService directly — you control everything\nHttpService myService = (ctx, req) -> {\n    CompletableFuture<HttpResponse> future = new CompletableFuture<>();\n    ctx.blockingTaskExecutor().execute(() -> {\n        try {\n            String result = legacySyncClient.call(); // blocking\n            future.complete(HttpResponse.of(HttpStatus.OK, MediaType.JSON, result));\n        } catch (Exception e) {\n            future.completeExceptionally(e);\n        }\n    });\n    return HttpResponse.of(future);\n};\n\nServer.builder().service(\"/api\", myService).build();\n```\n\n### 5.2 CompletableFuture Chaining with Explicit Executors\n\nUse `thenApplyAsync` / `thenComposeAsync` / `supplyAsync` with the right executor to control where each stage runs.\n\n```java\nHttpService myService = (ctx, req) -> {\n    // Stage 1: blocking DB call on blocking executor\n    CompletableFuture<List<Long>> ids = CompletableFuture.supplyAsync(\n            () -> {\n                assert ServiceRequestContext.current() == ctx;  // context is propagated\n                assert !ctx.eventLoop().inEventLoop();           // not on event loop\n                return jdbcTemplate.queryForList(\"SELECT id FROM items\", Long.class);\n            },\n            ctx.blockingTaskExecutor());\n\n    // Stage 2: async HTTP calls back on event loop\n    CompletableFuture<List<AggregatedHttpResponse>> details = ids.thenComposeAsync(\n            idList -> {\n                assert ctx.eventLoop().inEventLoop();  // back on event loop\n                List<CompletableFuture<AggregatedHttpResponse>> futures = idList.stream()\n                        .map(id -> backendClient.get(\"/items/\" + id).aggregate())\n                        .collect(Collectors.toList());\n                return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))\n                        .thenApply(v -> futures.stream()\n                                // join() is safe here: allOf() guarantees all futures\n                                // are already complete, so join() returns immediately.\n                                .map(CompletableFuture::join)\n                                .collect(Collectors.toList()));\n            },\n            ctx.eventLoop());\n\n    // Stage 3: assemble response (still on event loop)\n    CompletableFuture<HttpResponse> response = details.thenApply(\n            results -> HttpResponse.of(HttpStatus.OK, MediaType.JSON, toJson(results)));\n\n    return HttpResponse.of(response);\n};\n```\n\n### 5.3 Scheduling Delayed Work on the Event Loop\n\nUse `ctx.eventLoop().schedule()` for non-blocking timed operations.\n\n```java\n// gRPC: respond after a delay without blocking a thread\nServiceRequestContext.current().eventLoop().schedule(() -> {\n    responseObserver.onNext(buildReply(request.getName()));\n    responseObserver.onCompleted();\n}, 3, TimeUnit.SECONDS);\n```\n\n### 5.4 gRPC: Submit Blocking Work Manually\n\nWhen you don't want `useBlockingTaskExecutor(true)` for the entire service, submit individual calls.\n\n```java\npublic class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {\n    @Override\n    public void hello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {\n        // This method runs on the event loop by default.\n        // Submit only the blocking part:\n        ServiceRequestContext.current().blockingTaskExecutor().submit(() -> {\n            try {\n                Thread.sleep(3000); // Simulate blocking API\n            } catch (Exception ignored) {}\n            responseObserver.onNext(buildReply(request.getName()));\n            responseObserver.onCompleted();\n        });\n    }\n}\n```\n\n### 5.5 Thrift: Submit Blocking Work Manually\n\nSame pattern as gRPC.\n\n```java\nServiceRequestContext.current().blockingTaskExecutor().execute(() -> {\n    try {\n        Thread.sleep(3000); // Simulate blocking\n    } catch (Exception ignored) {}\n    resultHandler.onComplete(buildReply(request.getName()));\n});\n```\n\n### 5.6 Decorators with Executor Awareness\n\n```java\n// A decorator that logs request duration, running the delegate as-is\nHttpService decorated = myService.decorate((delegate, ctx, req) -> {\n    long start = System.nanoTime();\n    return delegate.serve(ctx, req).mapHeaders(headers -> {\n        long elapsed = System.nanoTime() - start;\n        logger.info(\"{} took {} ms\", ctx.path(), elapsed / 1_000_000);\n        return headers;\n    });\n});\n```\n\n### 5.7 Context Propagation with `makeContextAware`\n\nWhen you must use a third-party executor that Armeria doesn't control, wrap your task.\n\n```java\n// Wrapping a Runnable\nsomeExternalExecutor.submit(ctx.makeContextAware(() -> {\n    ServiceRequestContext.current(); // Works — context is mounted\n    doWork();\n}));\n\n// Wrapping a Callable\nFuture<String> result = someExternalExecutor.submit(ctx.makeContextAware(() -> {\n    return ServiceRequestContext.current().path(); // Works\n}));\n\n// Wrapping a Function (for CompletableFuture chains)\nfuture.thenApply(ctx.makeContextAware(value -> {\n    ServiceRequestContext.current(); // Works\n    return transform(value);\n}));\n```\n\n### 5.8 Reactor Integration\n\n```java\nHttpService myService = (ctx, req) -> {\n    Scheduler blockingScheduler = Schedulers.fromExecutor(ctx.blockingTaskExecutor());\n    Scheduler eventLoopScheduler = Schedulers.fromExecutor(ctx.eventLoop());\n\n    Mono<String> result = Mono.fromCallable(() -> {\n                // Runs on blocking scheduler\n                return database.query(\"SELECT ...\");\n            })\n            .subscribeOn(blockingScheduler)\n            .flatMap(dbResult ->\n                // Switch back to event loop for async HTTP call\n                Mono.fromCompletionStage(backendClient.get(\"/enrich/\" + dbResult).aggregate())\n                    .subscribeOn(eventLoopScheduler)\n                    .map(resp -> resp.contentUtf8())\n            );\n\n    return HttpResponse.of(result.map(body ->\n            HttpResponse.of(HttpStatus.OK, MediaType.JSON, body)).toFuture());\n};\n```\n\n### 5.9 RxJava Integration\n\n```java\nScheduler blockingScheduler = Schedulers.from(ctx.blockingTaskExecutor());\nScheduler eventLoopScheduler = Schedulers.from(ctx.eventLoop());\n\nSingle<String> result = Single.fromCallable(() -> database.query(\"SELECT ...\"))\n        .subscribeOn(blockingScheduler)       // blocking work here\n        .observeOn(eventLoopScheduler)        // switch to event loop\n        .flatMap(dbResult ->\n            Single.fromCompletionStage(backendClient.get(\"/enrich/\" + dbResult).aggregate())\n                  .map(AggregatedHttpResponse::contentUtf8));\n```\n\n---\n\n## Quick Reference: Which Pool to Use\n\n| Scenario | Pool | How |\n|----------|------|-----|\n| HTTP/gRPC request handling (async) | workerGroup (event loop) | Default — just return `CompletableFuture` or reactive types |\n| Database queries, file I/O, sync APIs | blockingTaskExecutor | `@Blocking`, `useBlockingTaskExecutor(true)`, or `ctx.blockingTaskExecutor()` |\n| CPU-heavy non-blocking work that shouldn't starve I/O | serviceWorkerGroup | `ServerBuilder.serviceWorkerGroup(n)` (rare) |\n| Client HTTP calls | workerGroup (event loop) | Default — `WebClient` is async |\n| Legacy synchronous library calls | blockingTaskExecutor | Wrap with `ctx.blockingTaskExecutor().submit(...)` |\n| Delayed/scheduled non-blocking work | event loop | `ctx.eventLoop().schedule(...)` |\n| Reactor blocking operations | blockingTaskExecutor | `.subscribeOn(Schedulers.fromExecutor(ctx.blockingTaskExecutor()))` |\n| Custom third-party executor | context-wrap it | `ctx.makeContextAware(runnable)` |\n","tools/claude-plugins/assistant/skills/eventloop-audit/references/detection-patterns.md":"# Detection Patterns — 7 Categories\n\nSearch the entire project (or scoped path) for each category of blocking pattern. Exclude test files (`**/test/**`, `**/tests/**`) and generated code (`**/generated/**`, `**/target/**`, `**/build/**`).\n\n## Category 1: Direct Blocking Calls\n\nThese are explicit operations that block the calling thread.\n\n**Search patterns:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `\\.join\\(\\)` | `CompletableFuture.join()` — blocks indefinitely |\n| `\\.get\\(\\s*\\)` | `Future.get()` with no args — blocks indefinitely |\n| `\\.get\\(\\s*\\d` | `Future.get(timeout, unit)` — bounded but still blocks |\n| `Thread\\.sleep\\(` | `Thread.sleep()` — deliberate blocking |\n| `\\.await\\(` | `CountDownLatch.await()`, `Condition.await()`, `CyclicBarrier.await()` |\n| `\\.acquire\\(` | `Semaphore.acquire()` — blocks until permit available |\n| `\\.lock\\(\\)` | `ReentrantLock.lock()` — blocks until lock available |\n| `\\.lockInterruptibly\\(\\)` | Same, interruptible variant |\n| `\\.take\\(\\)` | `BlockingQueue.take()` — blocks until element available |\n| `\\.put\\(` | `BlockingQueue.put()` — blocks if queue full (check context) |\n| `\\.wait\\(` | `Object.wait()` — monitor wait |\n\n**Triage for each hit:**\n1. Identify the enclosing method\n2. Trace callers to determine if reachable from an event loop entry point (from Phase 1)\n3. Check if the call is inside a known safe execution wrapper\n4. Classify severity (see Phase 3)\n\n## Category 2: Blocking I/O Operations\n\nI/O operations always block the calling thread. The question is whether they run on the event loop.\n\n**2A — File system operations:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `new FileInputStream` | File reading |\n| `new FileOutputStream` | File writing |\n| `new RandomAccessFile` | File random access |\n| `FileChannel\\.open` | File channel (blocking despite NIO package) |\n| `Files\\.(read\\|write\\|copy\\|move\\|walk\\|list\\|newInputStream\\|newOutputStream)` | NIO file operations (still blocking despite being in `java.nio`) |\n| `ProcessBuilder.*\\.start\\(\\)` | Process execution |\n| `Runtime\\..*exec\\(` | Process execution |\n\n**2B — Network and HTTP operations:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `\\.openConnection\\(\\)` | `URL.openConnection()` — blocking HTTP |\n| `\\.openStream\\(\\)` | `URL.openStream()` — blocking HTTP |\n| `HttpURLConnection` | Legacy blocking HTTP client |\n| `HttpClient.*\\.send\\(` | `java.net.http.HttpClient.send()` — synchronous (`.sendAsync()` is safe) |\n| `new Socket\\(` | TCP socket creation |\n| `new ServerSocket\\(` | Server socket binding |\n| `Pipe\\.open\\(\\)` | NIO pipe creation — blocking despite NIO package |\n\n**2C — Database operations:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `DriverManager\\.getConnection` | JDBC connection creation |\n| `javax\\.sql\\.DataSource` | JDBC data source |\n| `java\\.sql\\.\\(Connection\\|Statement\\|PreparedStatement\\|ResultSet\\)` | JDBC operations |\n\n**2D — Serialization and compression:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `ObjectInputStream\\|ObjectOutputStream` | Java serialization (blocking I/O on streams) |\n| `GZIPInputStream\\|GZIPOutputStream` | GZIP compression (blocking I/O) |\n| `ZipInputStream\\|ZipOutputStream` | ZIP operations (blocking I/O) |\n| `DeflaterInputStream\\|InflaterOutputStream` | Raw deflate/inflate (blocking I/O) |\n\n**2E — Encryption and security:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `KeyStore\\.load\\(` | Keystore loading from disk — blocking file I/O |\n| `SSLContext\\.getInstance` | SSL context initialization — can trigger I/O |\n| `TrustManagerFactory\\.init` | Trust manager init — may load certificates |\n\n## Category 3: Synchronization That Could Contend on Event Loop\n\nLocks are only a problem if they can contend (block waiting) on an event loop thread.\n\n**Search patterns:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `synchronized\\s*\\(` | Synchronized blocks |\n| `\\bsynchronized\\b[^\\n{;]*\\(` | Synchronized methods (modifier in method signature) |\n| `ReentrantLock` | Explicit locking |\n| `ReentrantReadWriteLock` | Read-write locking |\n| `StampedLock` | Stamped locking |\n\n**Triage:**\n- `synchronized` on startup/config-only paths → **SAFE**\n- `synchronized` on request-path data accessed from event loop → **MEDIUM to HIGH** depending on contention probability\n- `synchronized` on a monitor that is also locked by a thread pool thread → **HIGH** (event loop can wait for thread pool)\n\n## Category 4: CompletableFuture Continuations on Wrong Executor\n\nWhen you chain `.thenApply()` (without `Async`), the continuation runs on whichever thread completes the source future. If that's an event loop thread, the continuation is on the event loop.\n\n**Search patterns (non-async variants — potentially dangerous):**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `\\.thenApply\\(` | Synchronous continuation — inherits completing thread |\n| `\\.thenAccept\\(` | Same |\n| `\\.thenRun\\(` | Same |\n| `\\.thenCompose\\(` | Same |\n| `\\.whenComplete\\(` | Same |\n| `\\.handle\\(` | Same |\n| `\\.exceptionally\\(` | Same |\n\n**For each hit, determine:**\n1. What completes the source future? (event loop thread? thread pool thread?)\n2. Does the continuation body do blocking work?\n3. If both answers are problematic → needs `*Async` variant with explicit executor\n\n**Important — `*Async()` without executor is NOT always safe:**\n```regex\n\\.thenApplyAsync\\(\n\\.thenAcceptAsync\\(\n\\.thenComposeAsync\\(\n```\nThe single-argument `*Async()` variants (e.g., `.thenApplyAsync(fn)`) use `ForkJoinPool.commonPool()`, NOT the blocking task executor. This is problematic if:\n- The continuation does blocking I/O → blocks a common pool thread, starving other ForkJoinPool work\n- The project expects `ServiceRequestContext` to be available → it won't be on ForkJoinPool threads\n\nOnly the two-argument variants (e.g., `.thenApplyAsync(fn, executor)`) with an explicit executor are reliably safe. Verify that the second argument is a blocking executor, not just any executor.\n\n## Category 5: Thread Pool Misconfiguration\n\n**Search patterns:**\n\n| Pattern | Risk |\n|---------|------|\n| `Executors\\.newCachedThreadPool` | Unbounded — can exhaust memory under load |\n| `Executors\\.newSingleThreadExecutor` | Single thread — no parallelism, instant bottleneck |\n| `new ThreadPoolExecutor\\(` | Check queue type and max size — unbounded `LinkedBlockingQueue` is risky |\n| `new ForkJoinPool\\(` | Check initialization is thread-safe, check parallelism level |\n\n**Also verify:**\n- Is `blockingTaskExecutor` configured in `ServerBuilder`? (If not, Armeria uses its default 200-thread pool)\n- Is the blocking executor sized for the expected concurrent blocking operations?\n- Are custom executors properly shut down on server close?\n\n## Category 6: Missing Armeria Idioms\n\nThese are patterns where the project doesn't use Armeria's built-in mechanisms for handling blocking work.\n\n**Check for:**\n\n1. **gRPC services doing blocking work without `useBlockingTaskExecutor(true)`**\n   - Find gRPC handler methods (from Phase 1.4)\n   - Check if the method body contains I/O, database calls, or blocking waits\n   - If yes and `useBlockingTaskExecutor(true)` is not set → **CRITICAL**\n\n2. **Annotated services with blocking methods missing `@Blocking`**\n   - Find `@Get`/`@Post`/etc. methods (from Phase 1.3)\n   - Check if the method body contains blocking operations\n   - If yes and `@Blocking` is absent → **CRITICAL**\n\n3. **Custom `HttpService.serve()` that blocks**\n   - Find `HttpService` implementations (from Phase 1.2)\n   - Check if `serve()` body contains blocking operations without offloading to `ctx.blockingTaskExecutor()`\n   - If yes → **CRITICAL**\n\n4. **Cancel handlers that block**\n   ```text\n   Grep: setOnCancelHandler|whenRequestCancelling\n   ```\n   - Cancel handler lambdas run on the event loop\n   - Check if they call `.get()`, `.join()`, I/O, or any blocking operation → **CRITICAL**\n\n5. **Missing context propagation**\n   ```text\n   Grep: \\.submit\\(|\\.execute\\(|\\.schedule\\(\n   ```\n   - If work is submitted to a non-Armeria executor, check for `ctx.makeContextAware()` wrapping\n   - Missing context propagation means `ServiceRequestContext.current()` will fail in the submitted task → **MEDIUM**\n\n## Category 7: Transitive / Hidden Blocking\n\nThese are blocking operations that are easy to overlook.\n\n**Search patterns:**\n\n| Pattern | Risk |\n|---------|------|\n| `InetAddress\\.getByName` | DNS resolution — can block for seconds |\n| `InetAddress\\.getAllByName` | Same |\n| `Class\\.forName` | Class loading — can trigger I/O to read class files |\n| `ServiceLoader\\.load` | Classpath scanning — iterates JARs |\n| `System\\.getProperty` | Usually fast, but can block with SecurityManager |\n\n**Configuration-level checks:**\n- **Logging framework**: Does the project use Logback or Log4j? Check `logback.xml` / `log4j2.xml` for synchronous appenders (`FileAppender`, `ConsoleAppender` without `AsyncAppender` wrapper). Every `log.info()` on the event loop becomes blocking I/O if appenders are synchronous.\n  ```text\n  Grep: <appender|<AsyncAppender|AsyncLogger\n  Scope: **/logback*.xml, **/log4j2*.xml, **/logging*.xml\n  ```\n",".agents/skills/release-note/SKILL.md":"---\nname: release-note\ndescription: Generate and polish release notes for an Armeria version. Runs site/release-note.ts to collect PR data from a GitHub milestone, then rewrites the skeletal output into publication-ready MDX. Invoked as `/release-note <version>` (e.g., `/release-note 1.38.0`).\n---\n\n# Release Note Generator\n\nGenerates skeletal release notes from a GitHub milestone using `site/release-note.ts`,\nthen rewrites them into polished, publication-ready MDX with rich descriptions, code examples,\nand proper formatting.\n\n## Prerequisites\n\n- The `gh` CLI must be authenticated with access to `line/armeria`. Verify with `gh auth status`.\n- A `GITHUB_ACCESS_TOKEN` environment variable is recommended for higher GitHub API rate limits.\n  If not set, the script falls back to anonymous access (lower rate limits).\n- Node.js and npm must be available. The `site/` directory must have dependencies installed\n  (`npm install` in `site/`).\n\n## Invocation\n\n```\n/release-note <version>\n```\n\nExample: `/release-note 1.38.0`\n\n---\n\n## Phase 0: Generate Skeletal Release Notes\n\n1. Verify the GitHub milestone exists for the given version by checking:\n   ```\n   gh api repos/line/armeria/milestones --jq '.[] | select(.title == \"<version>\") | .number'\n   ```\n2. Run the release note generation script:\n   ```\n   cd site && npm run release-note <version>\n   ```\n3. Verify the output file was created at `site/src/content/release-notes/<version>.mdx`.\n4. If the script fails (e.g., milestone not found, network error), report the error and stop.\n\n## Phase 1: Load Draft and Study Style\n\n1. Read the generated draft file at `site/src/content/release-notes/<version>.mdx`.\n2. Read 3-4 recent polished release notes (e.g., `1.36.0.mdx`, `1.37.0.mdx`) to calibrate tone and style.\n3. Read the style guide at `references/style-guide.md` for formatting rules.\n4. Extract all PR/issue references (`#NNNN`) from every line of the draft.\n\n## Phase 2: Gather PR Context from GitHub\n\nFor each unique PR number found in the draft:\n\n1. Fetch PR details:\n   ```\n   gh pr view <number> --repo line/armeria --json title,body,labels,files\n   ```\n2. Parse the PR body to extract **Motivation**, **Modifications**, and **Result** sections.\n3. Fetch PR review comments — these often contain important design decisions, caveats, and\n   scope limitations (e.g., \"this only applies to unary calls\") that are not in the PR description:\n   ```\n   gh api repos/line/armeria/pulls/<number>/comments --jq '.[].body'\n   ```\n4. Extract linked issue numbers from the body (`Closes #NNNN`, `Fixes #NNNN`, `Resolves #NNNN`).\n5. For each linked issue, fetch its context **including comments**, which often contain use cases,\n   edge cases, and design discussions that inform the release note description:\n   ```\n   gh issue view <number> --repo line/armeria --json title,body\n   gh api repos/line/armeria/issues/<number>/comments --jq '.[].body'\n   ```\n6. For PRs in the \"New features\" section that introduce significant new API:\n   - Read key changed source files from the PR's `files` list to understand method signatures.\n   - Look for usage examples in the PR description's Result section first — prefer these over\n     constructing examples from scratch.\n   - Check review comments for scope limitations, caveats, or known constraints that should be\n     mentioned in the release note (e.g., \"only supports unary methods\", \"HTTP/2 only\").\n\n**Rate limiting**: If fetching many PRs, batch requests and pause briefly between them to avoid\nGitHub API rate limits.\n\n## Phase 3: Triage \"Maybe Ignore\" Section\n\nThe script puts PRs without a recognized label into `🗑 Maybe ignore`. For each entry:\n\n1. Check the PR's labels and description.\n2. **Drop** items that are purely internal (CI config, build scripts, test infrastructure,\n   non-user-facing refactoring, site/docs dependency bumps).\n3. **Relocate** user-facing items to the correct section based on their actual impact:\n   - API additions → New features\n   - Performance or usability improvements → Improvements\n   - Bug fixes → Bug fixes\n   - Breaking API changes → Breaking changes\n4. Report triage decisions to the user so they can override if needed.\n\n## Phase 4: Rewrite Each Section\n\nRewrite every entry following the formatting rules in `references/style-guide.md`.\n\n### Ordering\n\n1. **Lead with user interest**: Place the top 3 entries that users would care about most first.\n   Prioritize broadly applicable features (core, gRPC, Kubernetes) over niche modules (Athenz, xDS).\n   Consider the size of the user base affected and how common the use case is.\n2. **Then group by module**: After the top 3, group remaining entries by module/area so that\n   related changes appear together (e.g., Athenz entries adjacent, Kubernetes entries adjacent).\n3. This applies to all sections (New features, Improvements, Bug fixes, etc.).\n\n### Key principles:\n\n### New Features (`🌟 New features`)\n\n- All new feature entries get a bold title prefix: `- **Feature Title**: Description. #NNNN`\n- **Keep descriptions concise** — most entries should fit within 3 lines of prose.\n  Only high-impact features (e.g., a brand-new module or paradigm-shifting API) warrant\n  longer descriptions.\n- Include a Java code example (5-15 lines) whenever possible. It may be omitted if there is\n  no clear usage pattern to show.\n  - Mark the most important line with `// 👈👈👈`\n  - Indent code blocks with 2 spaces under the bullet.\n\n### Improvements (`📈 Improvements`)\n\n- Concise description of what improved and why it matters.\n- Code examples only if the improvement changes how users interact with an API.\n\n### Bug Fixes (`🛠️ Bug fixes`)\n\n- Describe the symptom that was fixed, not the internal cause.\n- Format: \"[What was broken] now [works correctly]. #NNNN\"\n\n### Breaking Changes (`☢️ Breaking changes`)\n\n- State clearly what changed and what users must do to migrate.\n- Include before/after code if the migration is non-trivial.\n\n### Documentation (`📃 Documentation`)\n\n- Brief description with links to the new/updated docs if available.\n\n### Deprecations (`🏚️ Deprecations`)\n\n- State what is deprecated and what to use instead.\n\n### All Sections — Common Rules\n\n- Use `[ClassName](type)` for Armeria API types (classes, interfaces, annotations, methods).\n  - For classes/interfaces: `[GrpcServiceBuilder](type)`\n  - For methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always include the\n    class name, method name, and parameter types. Do NOT use backtick-only style like\n    `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - Do NOT use this syntax for JDK types (`String`, `Duration`, `CompletableFuture`), third-party\n    types, or types that are not part of Armeria's public API.\n- Issue and PR references go at the end of the entry: `#6431 #6691` (list both the issue and the PR)\n- Do NOT copy PR titles verbatim — they are often terse commit-style messages.\n- Do NOT fabricate code examples. Derive them from PR descriptions or actual source code.\n- Keep entries self-contained — a reader should understand the change without clicking the PR link.\n\n## Phase 5: Clean Up Dependencies Section\n\nThe raw script includes the full dependency update PR body, which uses a structured commit message format.\n\n1. **Strip build-only dependencies**: Remove the `- Build` section and all its sub-bullets\n   (these are testImplementation, annotationProcessor, and other non-production deps).\n2. **Format each entry**: `- LibraryName oldVersion → newVersion`\n   - Use the library's common name (e.g., `Jackson`, `Netty`, `gRPC-Java`, not the Maven artifact ID).\n   - Use `→` (unicode arrow), not `->`.\n3. **Group multi-version bumps** on one line when a library has multiple version streams:\n   `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n4. **Sort alphabetically** (A → Z).\n\n## Optional: Validate with a Site Build\n\n`cd site && npm run build` validates the MDX end to end, but requires generated files under\n`site/gen-src/` (`api-index.json`, `versions.json`, `contributors.json`):\n\n- Full generation: `./gradlew :site:generateSiteSources` (api-index needs the aggregated Javadoc,\n  which is slow).\n- Quick validation: copy `api-index.json`/`versions.json` from a previous build and generate\n  `contributors.json` with\n  `gh api --paginate repos/line/armeria/contributors | jq -s '[.[][]] | map({(.login): .avatar_url}) | add'`.\n  A stale `api-index.json` is fine for validation — unknown `[X](type)` names render as blank\n  links (`type://#`) and never fail the build, so type names must be verified against the source\n  either way.\n- `npm run build | tail` masks the exit code — capture it separately.\n\n## Phase 6: Finalize\n\n1. **Remove empty sections**: Delete any section whose only content is `- N/A`.\n2. **Remove \"Maybe ignore\"**: The `🗑 Maybe ignore` section must not appear in the final output.\n3. **Deduplicate and sort contributors**: Ensure `<ThankYou usernames={[...]} />` has\n   alphabetically sorted, deduplicated usernames. Remove bot accounts (`dependabot[bot]`,\n   `CLAassistant`). Core maintainers listed in `.github/CODEOWNERS` must always be included\n   in the Thank You section, even if they don't appear in the PR participant lists.\n4. **Ensure consistent bullet style**: Use `-` (dash) for all bullets, not `*`.\n5. **Write the final file** to `site/src/content/release-notes/<version>.mdx`.\n6. **Show a summary** to the user: list the sections, entry count per section, and any entries\n   flagged as uncertain (where PR context was insufficient to write a confident description).\n\n---\n\n## Phase 7: Lint\n\nRun the mechanical checks and fix everything they report:\n\n```\n.agents/skills/release-note/scripts/lint-release-note.sh site/src/content/release-notes/<version>.mdx\n```\n\nThis is not optional, and reading the style guide is not a substitute for running it. The rules it\nenforces are all written down elsewhere in this skill and have still been broken while the author\nhad them open — a malformed `(type)` link never fails the site build, it just renders as a blank\n`type://#` link, so nothing else catches it.\n\nThe script cannot see two things, so check them by hand:\n\n- **Identifiers copied from a PR body** — metric names, class names, proto fields. PR descriptions\n  go stale; the source does not. Grep the repo for every identifier you quote. (A real case: PR\n  #6840's body advertised `armeria.xds.lb.request`, while the code registered `lb.select`.)\n- **Whether a claim is true**, as opposed to well-formed. Read the code the entry describes.\n\n## Execution Checklist\n\n- [ ] Phase 0 — Ran `npm run release-note <version>` and verified output file exists\n- [ ] Phase 1 — Read draft, recent examples, and style guide\n- [ ] Phase 2 — Fetched PR/issue context for all referenced PRs\n- [ ] Phase 3 — Triaged all \"Maybe ignore\" entries\n- [ ] Phase 4 — Rewrote all entries per style guide\n- [ ] Phase 5 — Cleaned up dependencies section\n- [ ] Phase 6 — Removed empty sections, finalized file, reported summary\n- [ ] Phase 7 — Ran `lint-release-note.sh` and it exited 0\n\n## Common Mistakes to Avoid\n\n- **Copying PR titles as-is**: PR titles like \"Fix NPE in FooBar\" are not user-friendly.\n  Rewrite as \"Fixed a `NullPointerException` in [FooBar](type) when ...\"\n- **Fabricating code examples**: If you cannot find a clear usage pattern from the PR description\n  or source code, write a descriptive sentence instead of guessing at code.\n- **Over-linking types**: Only use `[Name](type)` for Armeria's own public API types, not for\n  JDK classes, third-party libraries, or internal classes.\n- **Including build dependencies**: The dependency update PR body contains `- Build` sub-bullets\n  for test/build-only deps. These must be stripped.\n- **Leaving `- N/A` sections**: The polished output should only contain sections with actual content.\n- **Using `*` bullets**: Standardize on `-` dashes for all bullet points.\n- **Missing `👈👈👈` callouts**: Every code example for a new feature should highlight the key line.\n",".claude/skills/release-note/SKILL.md":"---\nname: release-note\ndescription: Generate and polish release notes for an Armeria version. Runs site/release-note.ts to collect PR data from a GitHub milestone, then rewrites the skeletal output into publication-ready MDX. Invoked as `/release-note <version>` (e.g., `/release-note 1.38.0`).\n---\n\n# Release Note Generator\n\nGenerates skeletal release notes from a GitHub milestone using `site/release-note.ts`,\nthen rewrites them into polished, publication-ready MDX with rich descriptions, code examples,\nand proper formatting.\n\n## Prerequisites\n\n- The `gh` CLI must be authenticated with access to `line/armeria`. Verify with `gh auth status`.\n- A `GITHUB_ACCESS_TOKEN` environment variable is recommended for higher GitHub API rate limits.\n  If not set, the script falls back to anonymous access (lower rate limits).\n- Node.js and npm must be available. The `site/` directory must have dependencies installed\n  (`npm install` in `site/`).\n\n## Invocation\n\n```\n/release-note <version>\n```\n\nExample: `/release-note 1.38.0`\n\n---\n\n## Phase 0: Generate Skeletal Release Notes\n\n1. Verify the GitHub milestone exists for the given version by checking:\n   ```\n   gh api repos/line/armeria/milestones --jq '.[] | select(.title == \"<version>\") | .number'\n   ```\n2. Run the release note generation script:\n   ```\n   cd site && npm run release-note <version>\n   ```\n3. Verify the output file was created at `site/src/content/release-notes/<version>.mdx`.\n4. If the script fails (e.g., milestone not found, network error), report the error and stop.\n\n## Phase 1: Load Draft and Study Style\n\n1. Read the generated draft file at `site/src/content/release-notes/<version>.mdx`.\n2. Read 3-4 recent polished release notes (e.g., `1.36.0.mdx`, `1.37.0.mdx`) to calibrate tone and style.\n3. Read the style guide at `references/style-guide.md` for formatting rules.\n4. Extract all PR/issue references (`#NNNN`) from every line of the draft.\n\n## Phase 2: Gather PR Context from GitHub\n\nFor each unique PR number found in the draft:\n\n1. Fetch PR details:\n   ```\n   gh pr view <number> --repo line/armeria --json title,body,labels,files\n   ```\n2. Parse the PR body to extract **Motivation**, **Modifications**, and **Result** sections.\n3. Fetch PR review comments — these often contain important design decisions, caveats, and\n   scope limitations (e.g., \"this only applies to unary calls\") that are not in the PR description:\n   ```\n   gh api repos/line/armeria/pulls/<number>/comments --jq '.[].body'\n   ```\n4. Extract linked issue numbers from the body (`Closes #NNNN`, `Fixes #NNNN`, `Resolves #NNNN`).\n5. For each linked issue, fetch its context **including comments**, which often contain use cases,\n   edge cases, and design discussions that inform the release note description:\n   ```\n   gh issue view <number> --repo line/armeria --json title,body\n   gh api repos/line/armeria/issues/<number>/comments --jq '.[].body'\n   ```\n6. For PRs in the \"New features\" section that introduce significant new API:\n   - Read key changed source files from the PR's `files` list to understand method signatures.\n   - Look for usage examples in the PR description's Result section first — prefer these over\n     constructing examples from scratch.\n   - Check review comments for scope limitations, caveats, or known constraints that should be\n     mentioned in the release note (e.g., \"only supports unary methods\", \"HTTP/2 only\").\n\n**Rate limiting**: If fetching many PRs, batch requests and pause briefly between them to avoid\nGitHub API rate limits.\n\n## Phase 3: Triage \"Maybe Ignore\" Section\n\nThe script puts PRs without a recognized label into `🗑 Maybe ignore`. For each entry:\n\n1. Check the PR's labels and description.\n2. **Drop** items that are purely internal (CI config, build scripts, test infrastructure,\n   non-user-facing refactoring, site/docs dependency bumps).\n3. **Relocate** user-facing items to the correct section based on their actual impact:\n   - API additions → New features\n   - Performance or usability improvements → Improvements\n   - Bug fixes → Bug fixes\n   - Breaking API changes → Breaking changes\n4. Report triage decisions to the user so they can override if needed.\n\n## Phase 4: Rewrite Each Section\n\nRewrite every entry following the formatting rules in `references/style-guide.md`.\n\n### Ordering\n\n1. **Lead with user interest**: Place the top 3 entries that users would care about most first.\n   Prioritize broadly applicable features (core, gRPC, Kubernetes) over niche modules (Athenz, xDS).\n   Consider the size of the user base affected and how common the use case is.\n2. **Then group by module**: After the top 3, group remaining entries by module/area so that\n   related changes appear together (e.g., Athenz entries adjacent, Kubernetes entries adjacent).\n3. This applies to all sections (New features, Improvements, Bug fixes, etc.).\n\n### Key principles:\n\n### New Features (`🌟 New features`)\n\n- All new feature entries get a bold title prefix: `- **Feature Title**: Description. #NNNN`\n- **Keep descriptions concise** — most entries should fit within 3 lines of prose.\n  Only high-impact features (e.g., a brand-new module or paradigm-shifting API) warrant\n  longer descriptions.\n- Include a Java code example (5-15 lines) whenever possible. It may be omitted if there is\n  no clear usage pattern to show.\n  - Mark the most important line with `// 👈👈👈`\n  - Indent code blocks with 2 spaces under the bullet.\n\n### Improvements (`📈 Improvements`)\n\n- Concise description of what improved and why it matters.\n- Code examples only if the improvement changes how users interact with an API.\n\n### Bug Fixes (`🛠️ Bug fixes`)\n\n- Describe the symptom that was fixed, not the internal cause.\n- Format: \"[What was broken] now [works correctly]. #NNNN\"\n\n### Breaking Changes (`☢️ Breaking changes`)\n\n- State clearly what changed and what users must do to migrate.\n- Include before/after code if the migration is non-trivial.\n\n### Documentation (`📃 Documentation`)\n\n- Brief description with links to the new/updated docs if available.\n\n### Deprecations (`🏚️ Deprecations`)\n\n- State what is deprecated and what to use instead.\n\n### All Sections — Common Rules\n\n- Use `[ClassName](type)` for Armeria API types (classes, interfaces, annotations, methods).\n  - For classes/interfaces: `[GrpcServiceBuilder](type)`\n  - For methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always include the\n    class name, method name, and parameter types. Do NOT use backtick-only style like\n    `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - Do NOT use this syntax for JDK types (`String`, `Duration`, `CompletableFuture`), third-party\n    types, or types that are not part of Armeria's public API.\n- Issue and PR references go at the end of the entry: `#6431 #6691` (list both the issue and the PR)\n- Do NOT copy PR titles verbatim — they are often terse commit-style messages.\n- Do NOT fabricate code examples. Derive them from PR descriptions or actual source code.\n- Keep entries self-contained — a reader should understand the change without clicking the PR link.\n\n## Phase 5: Clean Up Dependencies Section\n\nThe raw script includes the full dependency update PR body, which uses a structured commit message format.\n\n1. **Strip non-user-facing dependencies**:\n   - Remove the `- Build` section and all its sub-bullets\n     (these are testImplementation, annotationProcessor, and other non-production deps).\n   - Remove **test-only** dependencies (e.g., Apache HttpClient, OkHttp, DGS) that are only\n     used in `testImplementation` / `testFixturesImplementation` and never appear in published POMs.\n   - Remove **shaded** dependencies (e.g., Guava, Bouncy Castle, JCTools) that are relocated\n     into Armeria JARs and invisible to users at runtime.\n   - Remove dependencies that are **not transitive** in any published artifact (e.g., Jsoup).\n   - When unsure, check `dependencies.toml` for the dependency's scope (`testImplementation`,\n     `shaded`, etc.) and verify whether it appears in a published module's `api` or\n     `implementation` configuration.\n2. **Format each entry**: `- LibraryName oldVersion → newVersion`\n   - Use the library's common name (e.g., `Jackson`, `Netty`, `gRPC-Java`, not the Maven artifact ID).\n   - Use `→` (unicode arrow), not `->`.\n3. **Group multi-version bumps** on one line when a library has multiple version streams:\n   `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n4. **Sort alphabetically** (A → Z).\n\n## Optional: Validate with a Site Build\n\n`cd site && npm run build` validates the MDX end to end, but requires generated files under\n`site/gen-src/` (`api-index.json`, `versions.json`, `contributors.json`):\n\n- Full generation: `./gradlew :site:generateSiteSources` (api-index needs the aggregated Javadoc,\n  which is slow).\n- Quick validation: copy `api-index.json`/`versions.json` from a previous build and generate\n  `contributors.json` with\n  `gh api --paginate repos/line/armeria/contributors | jq -s '[.[][]] | map({(.login): .avatar_url}) | add'`.\n  A stale `api-index.json` is fine for validation — unknown `[X](type)` names render as blank\n  links (`type://#`) and never fail the build, so type names must be verified against the source\n  either way.\n- `npm run build | tail` masks the exit code — capture it separately.\n\n## Phase 6: Finalize\n\n1. **Remove empty sections**: Delete any section whose only content is `- N/A`.\n2. **Remove \"Maybe ignore\"**: The `🗑 Maybe ignore` section must not appear in the final output.\n3. **Deduplicate and sort contributors**: Ensure `<ThankYou usernames={[...]} />` has\n   alphabetically sorted, deduplicated usernames. Remove bot accounts (`dependabot[bot]`,\n   `CLAassistant`). Core maintainers listed in `.github/CODEOWNERS` must always be included\n   in the Thank You section, even if they don't appear in the PR participant lists.\n4. **Ensure consistent bullet style**: Use `-` (dash) for all bullets, not `*`.\n5. **Write the final file** to `site/src/content/release-notes/<version>.mdx`.\n6. **Show a summary** to the user: list the sections, entry count per section, and any entries\n   flagged as uncertain (where PR context was insufficient to write a confident description).\n\n---\n\n## Phase 7: Lint\n\nRun the mechanical checks and fix everything they report:\n\n```\n.claude/skills/release-note/scripts/lint-release-note.sh site/src/content/release-notes/<version>.mdx\n```\n\nThis is not optional, and reading the style guide is not a substitute for running it. The rules it\nenforces are all written down elsewhere in this skill and have still been broken while the author\nhad them open — a malformed `(type)` link never fails the site build, it just renders as a blank\n`type://#` link, so nothing else catches it.\n\nThe script cannot see two things, so check them by hand:\n\n- **Identifiers copied from a PR body** — metric names, class names, proto fields. PR descriptions\n  go stale; the source does not. Grep the repo for every identifier you quote. (A real case: PR\n  #6840's body advertised `armeria.xds.lb.request`, while the code registered `lb.select`.)\n- **Whether a claim is true**, as opposed to well-formed. Read the code the entry describes.\n\n## Execution Checklist\n\n- [ ] Phase 0 — Ran `npm run release-note <version>` and verified output file exists\n- [ ] Phase 1 — Read draft, recent examples, and style guide\n- [ ] Phase 2 — Fetched PR/issue context for all referenced PRs\n- [ ] Phase 3 — Triaged all \"Maybe ignore\" entries\n- [ ] Phase 4 — Rewrote all entries per style guide\n- [ ] Phase 5 — Cleaned up dependencies section\n- [ ] Phase 6 — Removed empty sections, finalized file, reported summary\n- [ ] Phase 7 — Ran `lint-release-note.sh` and it exited 0\n\n## Common Mistakes to Avoid\n\n- **Copying PR titles as-is**: PR titles like \"Fix NPE in FooBar\" are not user-friendly.\n  Rewrite as \"Fixed a `NullPointerException` in [FooBar](type) when ...\"\n- **Fabricating code examples**: If you cannot find a clear usage pattern from the PR description\n  or source code, write a descriptive sentence instead of guessing at code.\n- **Over-linking types**: Only use `[Name](type)` for Armeria's own public API types, not for\n  JDK classes, third-party libraries, or internal classes.\n- **Including build dependencies**: The dependency update PR body contains `- Build` sub-bullets\n  for test/build-only deps. These must be stripped.\n- **Leaving `- N/A` sections**: The polished output should only contain sections with actual content.\n- **Using `*` bullets**: Standardize on `-` dashes for all bullet points.\n- **Missing `👈👈👈` callouts**: Every code example for a new feature should highlight the key line.\n",".claude/skills/upgrade-deps/SKILL.md":"---\nname: upgrade-deps\ndescription: Upgrade Gradle dependencies and Gradle wrapper version\ndisable-model-invocation: true\n---\n\n# Dependency Upgrade Workflow\n\nThis skill performs a full Gradle dependency upgrade for the Armeria project step by step.\n\n## Background: Minimum Java Version Per Module\n\nArmeria is a multi-module project with different minimum Java version requirements.\nBefore upgrading a dependency, always verify that the new version supports the minimum\nJava version required by the modules that use it.\n\n| Java Version | Modules |\n|-------------|---------|\n| **8**  | `core`, `brave5`, `brave6`, `eureka`, `grpc`, `grpc-kotlin`, `graphql-protocol`, `jetty9`, `junit4`, `junit5`, `kafka`, `kotlin`, `logback`, `logback12`, `logback13`, `micrometer-context`, `oauth2`, `prometheus1`, `protobuf`, `reactor3`, `resteasy`, `retrofit2`, `rxjava2`, `rxjava3`, `sangria`, `scala*`, `spring:boot2-*`, `dropwizard2`, `thrift0.9`–`thrift0.17`, `tomcat8`, `tomcat9`, `xds`, `zookeeper3`, `saml`, `bucket4j`, `consul`, `nacos` |\n| **11** | `athenz`, `graphql`, `jetty10`, `jetty11`, `kubernetes`, `logback14`, `thrift0.18`–`thrift0.22`, `tomcat10` |\n| **17** | `ai:mcp`, `jetty12`, `resilience4j2`, `spring:boot3-*`, `spring:spring6`, `spring:boot4-*`, `spring:spring7` |\n\nWhen in doubt, check `settings.gradle` for the `java`, `java11`, or `java17` flag on each subproject.\n\n**The table above lists published modules only.** Integration-test (`:it:*`) and example (`:examples:*`)\nmodules carry their own flags in `settings.gradle` and are frequently `java17` already. A dependency\nconsumed *only* by those modules is not held back by the Java 8 baseline at all — always resolve the\nactual consumer before assuming a Java constraint applies.\n\n## Step 1: Run dependencyUpdates\n\n```\n./gradlew dependencyUpdates --no-parallel\n```\n\nThe report is saved to `build/dependencyUpdates/report.txt`.\n\n## Step 2: Parse the Report\n\nRead `build/dependencyUpdates/report.txt` to identify all outdated dependencies.\nFor each dependency, note the current version and the available latest version.\n\nIf a version looks ambiguous (unexpected format, major version bump, unusual naming),\nverify on Maven Central before proceeding:\n  https://central.sonatype.com/artifact/{groupId}/{artifactId}\n\n## Step 3: Check Skip Hints and Java Version Compatibility\n\nBefore upgrading any dependency, check `dependencies.toml` for a comment directly above\nits version entry. Many pinned versions have an explicit reason, for example:\n\n```toml\n# Don't upgrade Caffeine to 3.x that requires Java 11.\ncaffeine = \"2.9.3\"\n\n# Upgrade once https://github.com/ronmamo/reflections/issues/279 is fixed.\nreflections = \"0.9.11\"\n\n# Ensure that we use the same ZooKeeper version as what Curator depends on.\nzookeeper = \"3.9.3\"\n```\n\n### Step 3.1: Validate the comment before trusting it\n\nA hold comment is a claim about this repository, and claims go stale. **Never treat a comment as\nproof — re-derive its reason every upgrade.** For each held entry:\n\n1. Find the aliases for the version key in the `[libraries.*]` / `[plugins]` sections.\n2. Grep the build scripts for each alias accessor (`libs.foo.bar` for alias `foo-bar`):\n   `git grep -n 'libs\\.foo\\.bar' -- '*.gradle' '*.gradle.kts'`\n3. Map each hit's build file to its project, and read that project's flags in `settings.gradle`.\n4. Compare the *consumer's* Java level against the new version's requirement — **not** the library's\n   requirement in isolation. \"Library X requires Java 17\" is not a blocker when every consumer is\n   already `java17`.\n\nIf the reason no longer holds, **remove the comment and upgrade**. If it still holds, keep both.\n\nTwo comments were found stale this way (`dgs` and `graphql-kotlin`, both consumed only by `java17`\nmodules), so treat this step as mandatory rather than a spot check.\n\n### Step 3.2: Never write a hold reason you have not verified\n\nWhen you add a comment, the reason must be something you confirmed, with the evidence to hand:\n\n- **Java constraint** — confirmed against the consumer module's flag, per Step 3.1.\n- **Runtime/API breakage** — confirmed by actually building and running the affected module's tests,\n  and the comment names the concrete blocker (e.g. `# Don't upgrade graphql-kotlin to 10.x that\n  migrated to Jackson 3`), not a vague \"major upgrade deferred\".\n- **Version-sync constraint** — confirmed from the other project's POM *and* from this repo's history\n  (`git log -p -- dependencies.toml`). Do not invent a sync rule: check whether the two versions have\n  ever actually moved independently before claiming they must move together.\n\nA plausible-sounding reason that nobody verified is worse than no comment, because the next upgrade\nwill trust it.\n\nIf no comment exists, proceed with the Java version compatibility check:\n1. Find which modules use this dependency in `dependencies.toml`\n2. Determine the minimum Java version required by those modules (see table above)\n3. Check the new library version's minimum Java requirement (Maven Central / release notes)\n4. If the library's required Java > module's minimum Java → **skip the upgrade** and add a\n   comment above the version entry explaining why (e.g. `# X.Y requires Java 11`)\n5. If compatible → proceed with the upgrade\n\n## Step 4: Update dependencies.toml\n\nEdit the `[versions]` section in `dependencies.toml` to update the version strings.\nThe file is located at the root of the repository: `dependencies.toml`.\n\n## Step 5: Cross-check Updated Versions\n\nAfter editing `dependencies.toml`, re-read the full diff (`git diff dependencies.toml`) and\ncross-check every changed entry against the `dependencyUpdates` report:\n\n1. **No entry was accidentally skipped** — compare the report's \"The following dependencies\n   have later milestone versions\" list against your edits. If a dependency appears in the\n   report but not in the diff, either upgrade it or document why it was skipped.\n2. **Versions match the report** — confirm each new version in the diff matches the latest\n   version from the report, not a typo or intermediate version.\n3. **Pinned-version comments were respected** — verify you did not upgrade a dependency whose\n   comment says to skip it, and that you did not remove or contradict an existing comment.\n4. **Linked-version constraints are satisfied** — some dependencies must stay in sync with\n   others (e.g. ZooKeeper with Curator, Protobuf with gRPC). Read the comments above those\n   entries and verify the constraint still holds after the upgrade.\n\n## Step 6: Upgrade Gradle Wrapper\n\nCheck the latest stable Gradle release at https://gradle.org/releases/ and update\n`gradle/wrapper/gradle-wrapper.properties`:\n\n```properties\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-X.Y.Z-all.zip\n```\n\n## Step 7: Verify the Build\n\nRun the full build including tests to catch both compilation errors and runtime regressions:\n\n```\n./gradlew build --no-daemon\n```\n\nIf there are compilation errors, API breaking changes, or test failures caused by the upgrade,\nfix them before proceeding.\n\n## Step 8: Commit\n\nCreate a commit with the **exact** message format below. Follow it strictly — do not add extra\nsections, reorder bullets, or change the structure:\n\n```\nUpdate dependencies\n\n- {library-name} {old-version} -> {new-version}\n- {library-name} {old-version} -> {new-version}\n- Build\n   - {library-name} {old-version} -> {new-version}\n   - {library-name} {old-version} -> {new-version}\n```\n\nRules:\n- Each production dependency (api/implementation scope) gets one bullet: `- {name} {old} -> {new}`\n- The `- Build` bullet groups build-only dependencies (testImplementation, annotationProcessor,\n  relocated libs, non-transitive deps); nest them as sub-bullets with 3-space indent\n- Use the library's official name if one exists (e.g. `gRPC-Java`, `Jackson`, `Netty`, `Kotlin`,\n  `Reactor`, `Logback`, `Micrometer`); otherwise use the key name as-is from `dependencies.toml`\n- Sort entries **alphabetically (A → Z)** within each section\n- If no build-only deps were upgraded, omit the `- Build` section entirely\n- If any dependencies were **not upgraded** (due to Java version constraints, pinned-version\n  comments, or linked-version constraints), list them under a `- Unupdated` section with the\n  reason. This helps reviewers know which upgrades were intentionally skipped.\n\nExample:\n```\nUpdate dependencies\n\n- gRPC-Java 1.63.0 -> 1.64.0\n- Jackson 2.17.0 -> 2.18.0\n- Build\n   - checkstyle 10.14.0 -> 10.17.0\n   - ErrorProne 2.27.0 -> 2.28.0\n- Unupdated\n   - Caffeine 2.9.3 -> 3.2.0 (requires Java 11)\n   - ZooKeeper 3.9.3 (pinned to Curator's version)\n```\n",".agents/skills/upgrade-deps/SKILL.md":"---\nname: upgrade-deps\ndescription: Upgrade Gradle dependencies and Gradle wrapper version\ndisable-model-invocation: true\n---\n\n# Dependency Upgrade Workflow\n\nThis skill performs a full Gradle dependency upgrade for the Armeria project step by step.\n\n## Background: Minimum Java Version Per Module\n\nArmeria is a multi-module project with different minimum Java version requirements.\nBefore upgrading a dependency, always verify that the new version supports the minimum\nJava version required by the modules that use it.\n\n| Java Version | Modules |\n|-------------|---------|\n| **8**  | `core`, `brave5`, `brave6`, `eureka`, `grpc`, `grpc-kotlin`, `graphql-protocol`, `jetty9`, `junit4`, `junit5`, `kafka`, `kotlin`, `logback`, `logback12`, `logback13`, `micrometer-context`, `oauth2`, `prometheus1`, `protobuf`, `reactor3`, `resteasy`, `retrofit2`, `rxjava2`, `rxjava3`, `sangria`, `scala*`, `spring:boot2-*`, `dropwizard2`, `thrift0.9`–`thrift0.17`, `tomcat8`, `tomcat9`, `xds`, `zookeeper3`, `saml`, `bucket4j`, `consul`, `nacos` |\n| **11** | `athenz`, `graphql`, `jetty10`, `jetty11`, `kubernetes`, `logback14`, `thrift0.18`–`thrift0.22`, `tomcat10` |\n| **17** | `ai:mcp`, `jetty12`, `resilience4j2`, `spring:boot3-*`, `spring:spring6`, `spring:boot4-*`, `spring:spring7` |\n\nWhen in doubt, check `settings.gradle` for the `java`, `java11`, or `java17` flag on each subproject.\n\n**The table above lists published modules only.** Integration-test (`:it:*`) and example (`:examples:*`)\nmodules carry their own flags in `settings.gradle` and are frequently `java17` already. A dependency\nconsumed *only* by those modules is not held back by the Java 8 baseline at all — always resolve the\nactual consumer before assuming a Java constraint applies.\n\n## Step 1: Run dependencyUpdates\n\n```\n./gradlew dependencyUpdates --no-parallel\n```\n\nThe report is saved to `build/dependencyUpdates/report.txt`.\n\n## Step 2: Parse the Report\n\nRead `build/dependencyUpdates/report.txt` to identify all outdated dependencies.\nFor each dependency, note the current version and the available latest version.\n\nIf a version looks ambiguous (unexpected format, major version bump, unusual naming),\nverify on Maven Central before proceeding:\n  https://central.sonatype.com/artifact/{groupId}/{artifactId}\n\n## Step 3: Check Skip Hints and Java Version Compatibility\n\nBefore upgrading any dependency, check `dependencies.toml` for a comment directly above\nits version entry. Many pinned versions have an explicit reason, for example:\n\n```toml\n# Don't upgrade Caffeine to 3.x that requires Java 11.\ncaffeine = \"2.9.3\"\n\n# Upgrade once https://github.com/ronmamo/reflections/issues/279 is fixed.\nreflections = \"0.9.11\"\n\n# Ensure that we use the same ZooKeeper version as what Curator depends on.\nzookeeper = \"3.9.3\"\n```\n\n### Step 3.1: Validate the comment before trusting it\n\nA hold comment is a claim about this repository, and claims go stale. **Never treat a comment as\nproof — re-derive its reason every upgrade.** For each held entry:\n\n1. Find the aliases for the version key in the `[libraries.*]` / `[plugins]` sections.\n2. Grep the build scripts for each alias accessor (`libs.foo.bar` for alias `foo-bar`):\n   `git grep -n 'libs\\.foo\\.bar' -- '*.gradle' '*.gradle.kts'`\n3. Map each hit's build file to its project, and read that project's flags in `settings.gradle`.\n4. Compare the *consumer's* Java level against the new version's requirement — **not** the library's\n   requirement in isolation. \"Library X requires Java 17\" is not a blocker when every consumer is\n   already `java17`.\n\nIf the reason no longer holds, **remove the comment and upgrade**. If it still holds, keep both.\n\nTwo comments were found stale this way (`dgs` and `graphql-kotlin`, both consumed only by `java17`\nmodules), so treat this step as mandatory rather than a spot check.\n\n### Step 3.2: Never write a hold reason you have not verified\n\nWhen you add a comment, the reason must be something you confirmed, with the evidence to hand:\n\n- **Java constraint** — confirmed against the consumer module's flag, per Step 3.1.\n- **Runtime/API breakage** — confirmed by actually building and running the affected module's tests,\n  and the comment names the concrete blocker (e.g. `# Don't upgrade graphql-kotlin to 10.x that\n  migrated to Jackson 3`), not a vague \"major upgrade deferred\".\n- **Version-sync constraint** — confirmed from the other project's POM *and* from this repo's history\n  (`git log -p -- dependencies.toml`). Do not invent a sync rule: check whether the two versions have\n  ever actually moved independently before claiming they must move together.\n\nA plausible-sounding reason that nobody verified is worse than no comment, because the next upgrade\nwill trust it.\n\nIf no comment exists, proceed with the Java version compatibility check:\n1. Find which modules use this dependency in `dependencies.toml`\n2. Determine the minimum Java version required by those modules (see table above)\n3. Check the new library version's minimum Java requirement (Maven Central / release notes)\n4. If the library's required Java > module's minimum Java → **skip the upgrade** and add a\n   comment above the version entry explaining why (e.g. `# X.Y requires Java 11`)\n5. If compatible → proceed with the upgrade\n\n## Step 4: Update dependencies.toml\n\nEdit the `[versions]` section in `dependencies.toml` to update the version strings.\nThe file is located at the root of the repository: `dependencies.toml`.\n\n## Step 5: Cross-check Updated Versions\n\nAfter editing `dependencies.toml`, re-read the full diff (`git diff dependencies.toml`) and\ncross-check every changed entry against the `dependencyUpdates` report:\n\n1. **No entry was accidentally skipped** — compare the report's \"The following dependencies\n   have later milestone versions\" list against your edits. If a dependency appears in the\n   report but not in the diff, either upgrade it or document why it was skipped.\n2. **Versions match the report** — confirm each new version in the diff matches the latest\n   version from the report, not a typo or intermediate version.\n3. **Pinned-version comments were respected** — verify you did not upgrade a dependency whose\n   comment says to skip it, and that you did not remove or contradict an existing comment.\n4. **Linked-version constraints are satisfied** — some dependencies must stay in sync with\n   others (e.g. ZooKeeper with Curator, Protobuf with gRPC). Read the comments above those\n   entries and verify the constraint still holds after the upgrade.\n\n## Step 6: Upgrade Gradle Wrapper\n\nCheck the latest stable Gradle release at https://gradle.org/releases/ and update\n`gradle/wrapper/gradle-wrapper.properties`:\n\n```properties\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-X.Y.Z-all.zip\n```\n\n## Step 7: Verify the Build\n\nRun the full build including tests to catch both compilation errors and runtime regressions:\n\n```\n./gradlew build --no-daemon\n```\n\nIf there are compilation errors, API breaking changes, or test failures caused by the upgrade,\nfix them before proceeding.\n\n## Step 8: Commit\n\nCreate a commit with the **exact** message format below. Follow it strictly — do not add extra\nsections, reorder bullets, or change the structure:\n\n```\nUpdate dependencies\n\n- {library-name} {old-version} -> {new-version}\n- {library-name} {old-version} -> {new-version}\n- Build\n   - {library-name} {old-version} -> {new-version}\n   - {library-name} {old-version} -> {new-version}\n```\n\nRules:\n- Each production dependency (api/implementation scope) gets one bullet: `- {name} {old} -> {new}`\n- The `- Build` bullet groups build-only dependencies (testImplementation, annotationProcessor,\n  relocated libs, non-transitive deps); nest them as sub-bullets with 3-space indent\n- Use the library's official name if one exists (e.g. `gRPC-Java`, `Jackson`, `Netty`, `Kotlin`,\n  `Reactor`, `Logback`, `Micrometer`); otherwise use the key name as-is from `dependencies.toml`\n- Sort entries **alphabetically (A → Z)** within each section\n- If no build-only deps were upgraded, omit the `- Build` section entirely\n- If any dependencies were **not upgraded** (due to Java version constraints, pinned-version\n  comments, or linked-version constraints), list them under a `- Unupdated` section with the\n  reason. This helps reviewers know which upgrades were intentionally skipped.\n\nExample:\n```\nUpdate dependencies\n\n- gRPC-Java 1.63.0 -> 1.64.0\n- Jackson 2.17.0 -> 2.18.0\n- Build\n   - checkstyle 10.14.0 -> 10.17.0\n   - ErrorProne 2.27.0 -> 2.28.0\n- Unupdated\n   - Caffeine 2.9.3 -> 3.2.0 (requires Java 11)\n   - ZooKeeper 3.9.3 (pinned to Curator's version)\n```\n","tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md":"# References Skill\n\nPath: `tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/line/armeria/HEAD/tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md)"},"items":[{"name":"style-guide.md","path":".agents/skills/release-note/references/style-guide.md","rawUrl":"https://raw.githubusercontent.com/line/armeria/HEAD/.agents/skills/release-note/references/style-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Release Notes Style Guide\n\nAnnotated examples from real Armeria release notes showing the exact formatting conventions.\n\n---\n\n## Major New Feature Entry\n\nBold title, description explaining value, `[Type](type)` links, code example with `👈👈👈`, PR refs.\n\n```mdx\n- **Standalone Athenz Token Validation**: You can now use [AthenzAuthorizer](type) to validate Athenz tokens\n  outside of Armeria's request pipeline. This allows you to easily integrate Athenz authorization into\n  third-party frameworks like Spring MVC or other servlet-based applications. #6607\n  ```java\n  ZtsBaseClient ztsBaseClient = ...;\n  AthenzAuthorizer authorizer =\n    AthenzAuthorizer.builder(ztsBaseClient)\n                    .policyConfig(new AthenzPolicyConfig(\"your-domain\"))\n                    .build();\n\n  // Validate tokens anywhere\n  AccessCheckStatus status =\n    authorizer.authorize(token, resource, action); // 👈👈👈\n  ```\n```\n\nKey elements:\n- `**Bold Title**:` prefix — only for significant features, not every entry\n- Description explains *what you can now do* and *why it matters*\n- `[AthenzAuthorizer](type)` — Armeria API type link\n- Code block indented under the bullet (2 spaces)\n- `// 👈👈👈` on the line the reader should focus on\n- `#6607` issue/PR references at end of description line (list both the issue and the PR)\n\n---\n\n## Minor New Feature Entry\n\nOne-liner without code example, for smaller additions.\n\n```mdx\n- **Per-Request Client TLS Configuration**: You can now specify [ClientTlsSpec](type) for each request using\n  [RequestOptions#clientTlsSpec()](type) or [ClientRequestContext#clientTlsSpec()](type). #6551\n```\n\nOr even simpler:\n\n```mdx\n- [ConnectionPoolListener](type) now supports ping-related events. #6539 #6552\n```\n\n---\n\n## Improvement Entry\n\nConcise description with type links. No code example unless it changes user-facing API.\n\n```mdx\n- [XdsBootstrap](type) now supports SDS. #6597 #6610 #6654\n- Improved warning logs by adding diagnostic context to frequent HTTP/2 connection errors. #6638\n```\n\n---\n\n## Bug Fix Entry\n\nDescribe the symptom that was fixed, not the internal root cause.\n\n```mdx\n- [GrpcMeterIdPrefixFunction](type) records `grpc-status` correctly for responses which fail with an exception. #6606 #6621\n- `X-Forwarded-For` header values with leading or trailing whitespace around comma-separated addresses\n  (e.g., `\"192.168.1.1 , 10.0.0.1\"`) are now trimmed and parsed correctly. #6615\n- Setting `pingIntervalMillis` to a value greater than 33 seconds no longer throws an exception. #6648\n  - Linux keepalive socket options (`SO_KEEPALIVE` `TCP_KEEPIDLE`, `TCP_KEEPINTVL`) are no longer set by default.\n```\n\nKey patterns:\n- \"[Thing] now [works correctly]\" or \"[Thing] no longer [does wrong thing]\"\n- Sub-bullets for related side effects or additional context\n- Use backticks for config names, error types, and header names\n\n---\n\n## Breaking Change Entry\n\nState what changed and what users must do to migrate.\n\n```mdx\n- A subclass of [AbstractEndpointSelector](type) must implement [AbstractEndpointSelector#doSelectNow(ClientRequestContext)](type)\n  instead of [AbstractEndpointSelector#selectNow(ClientRequestContext)](type), which is now a final method. #6535\n```\n\nFor non-trivial migrations, include before/after code.\n\n---\n\n## Documentation Entry\n\nBrief description with links to new/updated docs.\n\nLink to docs pages with a **site-relative path** (`/docs/...`), never an absolute `https://armeria.dev/...`\nURL — that is the convention across the docs and the newer release notes, and it keeps links working\non local builds and previews.\n\n```mdx\n- New comprehensive documentation on understanding and handling timeouts: #6592\n  - [Understanding timeout and cancellation origins](/docs/advanced/understanding-timeouts)\n  - Handling timeouts for streaming:\n    - [Client-side streaming](/docs/client/timeouts#handling-timeouts-for-streaming-responses)\n    - [Server-side streaming](/docs/server/timeouts#handling-timeouts-for-streaming-requests)\n```\n\n---\n\n## Dependencies Section\n\nFormat: `- LibraryName oldVersion → newVersion`, sorted alphabetically.\n\n```mdx\n- Athenz 1.12.31 → 1.12.33\n- BlockHound 1.0.15 → 1.0.16\n- Jackson 2.20.1 → 2.21.0\n- Kotlin 2.2.21 → 2.3.0\n- Logback 1.5.23 → 1.5.27\n- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3\n- Spring Boot 3.5.8 → 3.5.10, 4.0.1 → 4.0.2\n```\n\nRules:\n- Use `→` (unicode arrow), not `->` or `-->`\n- Use `-` dash bullets, not `*`\n- Use the library's common name, not Maven artifact IDs\n- Group multi-version bumps: `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n- Omit build-only dependencies (anything under the `- Build` section in the raw dependency PR)\n- Sort alphabetically (A → Z)\n- Strip trailing release qualifiers from version numbers — `.Final`, `.RELEASE`, `-GA`,\n  `.vYYYYMMDD` and the like. They add no information a reader acts on:\n  - `Netty 4.2.15.Final → 4.2.16.Final` becomes `Netty 4.2.15 → 4.2.16`\n  - `Javassist 3.31.0-GA → 3.32.0-GA` becomes `Javassist 3.31.0 → 3.32.0`\n  - `Jetty 9.4.55.v20240627 → 9.4.58.v20250814` becomes `Jetty 9.4.55 → 9.4.58`\n\n---\n\n## Thank You Section\n\n```mdx\n## 🙇 Thank you\n\n<ThankYou usernames={[\n  'contributor1',\n  'contributor2',\n  'contributor3'\n]} />\n```\n\nRules:\n- Usernames sorted alphabetically\n- Remove bot accounts: `dependabot[bot]`, `CLAassistant`\n- Core maintainers from `.github/CODEOWNERS` must always be included\n- One username per line, single-quoted, comma-separated\n\n---\n\n## What NOT to Include\n\n- **`- N/A` entries**: Remove sections that have no content instead of writing `- N/A`\n- **`🗑 Maybe ignore` section**: Must be fully triaged and removed\n- **Build-only dependencies**: `- Build` sub-section from dependency PRs\n- **Internal refactoring details**: Changes with no user-facing impact\n- **Site/docs dependency bumps**: npm bumps for `site/` or `docs-client/` (e.g., webpack, lodash)\n\n## Type Link Rules\n\nUse `[ClassName](type)` ONLY for Armeria public API references:\n- Classes/interfaces: `[GrpcServiceBuilder](type)`\n- Methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always use the full\n  `ClassName#methodName(ParamType)` form. Never use backtick-only style like\n  `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - **No space after commas** in the parameter list. The lookup key in `gen-src/api-index.json`\n    has none, so `[X#m(A, B)](type)` silently resolves to a dead `type://#` link while\n    `[X#m(A,B)](type)` works. The rendered text hides the difference — it shows `X.m()` either way.\n  - **Never split a method across a class link and a backtick.** `[Foo](type)` `` `.bar()` `` is\n    not a link to `bar()`; write `[Foo#bar(Baz)](type)` as one unit.\n- Annotations: `[@Blocking](type)`\n\nDo NOT use `[Name](type)` for:\n- JDK types (`String`, `Duration`, `CompletableFuture`, `List`)\n- Third-party types (`Mono`, `Flux`, `HttpServletRequest`)\n- Internal/private Armeria classes\n- Generic concepts (use plain text or backticks instead)\n- Method calls inside code blocks — code blocks use plain Java syntax, not `(type)` links\n","isInternal":false,"tokens":1964,"sizeBytes":7120},{"name":"SKILL.md","path":".agents/skills/release-note/SKILL.md","rawUrl":"https://raw.githubusercontent.com/line/armeria/HEAD/.agents/skills/release-note/SKILL.md","title":"release-note","category":"anthropic-skill","format":"markdown","content":"---\nname: release-note\ndescription: Generate and polish release notes for an Armeria version. Runs site/release-note.ts to collect PR data from a GitHub milestone, then rewrites the skeletal output into publication-ready MDX. Invoked as `/release-note <version>` (e.g., `/release-note 1.38.0`).\n---\n\n# Release Note Generator\n\nGenerates skeletal release notes from a GitHub milestone using `site/release-note.ts`,\nthen rewrites them into polished, publication-ready MDX with rich descriptions, code examples,\nand proper formatting.\n\n## Prerequisites\n\n- The `gh` CLI must be authenticated with access to `line/armeria`. Verify with `gh auth status`.\n- A `GITHUB_ACCESS_TOKEN` environment variable is recommended for higher GitHub API rate limits.\n  If not set, the script falls back to anonymous access (lower rate limits).\n- Node.js and npm must be available. The `site/` directory must have dependencies installed\n  (`npm install` in `site/`).\n\n## Invocation\n\n```\n/release-note <version>\n```\n\nExample: `/release-note 1.38.0`\n\n---\n\n## Phase 0: Generate Skeletal Release Notes\n\n1. Verify the GitHub milestone exists for the given version by checking:\n   ```\n   gh api repos/line/armeria/milestones --jq '.[] | select(.title == \"<version>\") | .number'\n   ```\n2. Run the release note generation script:\n   ```\n   cd site && npm run release-note <version>\n   ```\n3. Verify the output file was created at `site/src/content/release-notes/<version>.mdx`.\n4. If the script fails (e.g., milestone not found, network error), report the error and stop.\n\n## Phase 1: Load Draft and Study Style\n\n1. Read the generated draft file at `site/src/content/release-notes/<version>.mdx`.\n2. Read 3-4 recent polished release notes (e.g., `1.36.0.mdx`, `1.37.0.mdx`) to calibrate tone and style.\n3. Read the style guide at `references/style-guide.md` for formatting rules.\n4. Extract all PR/issue references (`#NNNN`) from every line of the draft.\n\n## Phase 2: Gather PR Context from GitHub\n\nFor each unique PR number found in the draft:\n\n1. Fetch PR details:\n   ```\n   gh pr view <number> --repo line/armeria --json title,body,labels,files\n   ```\n2. Parse the PR body to extract **Motivation**, **Modifications**, and **Result** sections.\n3. Fetch PR review comments — these often contain important design decisions, caveats, and\n   scope limitations (e.g., \"this only applies to unary calls\") that are not in the PR description:\n   ```\n   gh api repos/line/armeria/pulls/<number>/comments --jq '.[].body'\n   ```\n4. Extract linked issue numbers from the body (`Closes #NNNN`, `Fixes #NNNN`, `Resolves #NNNN`).\n5. For each linked issue, fetch its context **including comments**, which often contain use cases,\n   edge cases, and design discussions that inform the release note description:\n   ```\n   gh issue view <number> --repo line/armeria --json title,body\n   gh api repos/line/armeria/issues/<number>/comments --jq '.[].body'\n   ```\n6. For PRs in the \"New features\" section that introduce significant new API:\n   - Read key changed source files from the PR's `files` list to understand method signatures.\n   - Look for usage examples in the PR description's Result section first — prefer these over\n     constructing examples from scratch.\n   - Check review comments for scope limitations, caveats, or known constraints that should be\n     mentioned in the release note (e.g., \"only supports unary methods\", \"HTTP/2 only\").\n\n**Rate limiting**: If fetching many PRs, batch requests and pause briefly between them to avoid\nGitHub API rate limits.\n\n## Phase 3: Triage \"Maybe Ignore\" Section\n\nThe script puts PRs without a recognized label into `🗑 Maybe ignore`. For each entry:\n\n1. Check the PR's labels and description.\n2. **Drop** items that are purely internal (CI config, build scripts, test infrastructure,\n   non-user-facing refactoring, site/docs dependency bumps).\n3. **Relocate** user-facing items to the correct section based on their actual impact:\n   - API additions → New features\n   - Performance or usability improvements → Improvements\n   - Bug fixes → Bug fixes\n   - Breaking API changes → Breaking changes\n4. Report triage decisions to the user so they can override if needed.\n\n## Phase 4: Rewrite Each Section\n\nRewrite every entry following the formatting rules in `references/style-guide.md`.\n\n### Ordering\n\n1. **Lead with user interest**: Place the top 3 entries that users would care about most first.\n   Prioritize broadly applicable features (core, gRPC, Kubernetes) over niche modules (Athenz, xDS).\n   Consider the size of the user base affected and how common the use case is.\n2. **Then group by module**: After the top 3, group remaining entries by module/area so that\n   related changes appear together (e.g., Athenz entries adjacent, Kubernetes entries adjacent).\n3. This applies to all sections (New features, Improvements, Bug fixes, etc.).\n\n### Key principles:\n\n### New Features (`🌟 New features`)\n\n- All new feature entries get a bold title prefix: `- **Feature Title**: Description. #NNNN`\n- **Keep descriptions concise** — most entries should fit within 3 lines of prose.\n  Only high-impact features (e.g., a brand-new module or paradigm-shifting API) warrant\n  longer descriptions.\n- Include a Java code example (5-15 lines) whenever possible. It may be omitted if there is\n  no clear usage pattern to show.\n  - Mark the most important line with `// 👈👈👈`\n  - Indent code blocks with 2 spaces under the bullet.\n\n### Improvements (`📈 Improvements`)\n\n- Concise description of what improved and why it matters.\n- Code examples only if the improvement changes how users interact with an API.\n\n### Bug Fixes (`🛠️ Bug fixes`)\n\n- Describe the symptom that was fixed, not the internal cause.\n- Format: \"[What was broken] now [works correctly]. #NNNN\"\n\n### Breaking Changes (`☢️ Breaking changes`)\n\n- State clearly what changed and what users must do to migrate.\n- Include before/after code if the migration is non-trivial.\n\n### Documentation (`📃 Documentation`)\n\n- Brief description with links to the new/updated docs if available.\n\n### Deprecations (`🏚️ Deprecations`)\n\n- State what is deprecated and what to use instead.\n\n### All Sections — Common Rules\n\n- Use `[ClassName](type)` for Armeria API types (classes, interfaces, annotations, methods).\n  - For classes/interfaces: `[GrpcServiceBuilder](type)`\n  - For methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always include the\n    class name, method name, and parameter types. Do NOT use backtick-only style like\n    `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - Do NOT use this syntax for JDK types (`String`, `Duration`, `CompletableFuture`), third-party\n    types, or types that are not part of Armeria's public API.\n- Issue and PR references go at the end of the entry: `#6431 #6691` (list both the issue and the PR)\n- Do NOT copy PR titles verbatim — they are often terse commit-style messages.\n- Do NOT fabricate code examples. Derive them from PR descriptions or actual source code.\n- Keep entries self-contained — a reader should understand the change without clicking the PR link.\n\n## Phase 5: Clean Up Dependencies Section\n\nThe raw script includes the full dependency update PR body, which uses a structured commit message format.\n\n1. **Strip build-only dependencies**: Remove the `- Build` section and all its sub-bullets\n   (these are testImplementation, annotationProcessor, and other non-production deps).\n2. **Format each entry**: `- LibraryName oldVersion → newVersion`\n   - Use the library's common name (e.g., `Jackson`, `Netty`, `gRPC-Java`, not the Maven artifact ID).\n   - Use `→` (unicode arrow), not `->`.\n3. **Group multi-version bumps** on one line when a library has multiple version streams:\n   `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n4. **Sort alphabetically** (A → Z).\n\n## Optional: Validate with a Site Build\n\n`cd site && npm run build` validates the MDX end to end, but requires generated files under\n`site/gen-src/` (`api-index.json`, `versions.json`, `contributors.json`):\n\n- Full generation: `./gradlew :site:generateSiteSources` (api-index needs the aggregated Javadoc,\n  which is slow).\n- Quick validation: copy `api-index.json`/`versions.json` from a previous build and generate\n  `contributors.json` with\n  `gh api --paginate repos/line/armeria/contributors | jq -s '[.[][]] | map({(.login): .avatar_url}) | add'`.\n  A stale `api-index.json` is fine for validation — unknown `[X](type)` names render as blank\n  links (`type://#`) and never fail the build, so type names must be verified against the source\n  either way.\n- `npm run build | tail` masks the exit code — capture it separately.\n\n## Phase 6: Finalize\n\n1. **Remove empty sections**: Delete any section whose only content is `- N/A`.\n2. **Remove \"Maybe ignore\"**: The `🗑 Maybe ignore` section must not appear in the final output.\n3. **Deduplicate and sort contributors**: Ensure `<ThankYou usernames={[...]} />` has\n   alphabetically sorted, deduplicated usernames. Remove bot accounts (`dependabot[bot]`,\n   `CLAassistant`). Core maintainers listed in `.github/CODEOWNERS` must always be included\n   in the Thank You section, even if they don't appear in the PR participant lists.\n4. **Ensure consistent bullet style**: Use `-` (dash) for all bullets, not `*`.\n5. **Write the final file** to `site/src/content/release-notes/<version>.mdx`.\n6. **Show a summary** to the user: list the sections, entry count per section, and any entries\n   flagged as uncertain (where PR context was insufficient to write a confident description).\n\n---\n\n## Phase 7: Lint\n\nRun the mechanical checks and fix everything they report:\n\n```\n.agents/skills/release-note/scripts/lint-release-note.sh site/src/content/release-notes/<version>.mdx\n```\n\nThis is not optional, and reading the style guide is not a substitute for running it. The rules it\nenforces are all written down elsewhere in this skill and have still been broken while the author\nhad them open — a malformed `(type)` link never fails the site build, it just renders as a blank\n`type://#` link, so nothing else catches it.\n\nThe script cannot see two things, so check them by hand:\n\n- **Identifiers copied from a PR body** — metric names, class names, proto fields. PR descriptions\n  go stale; the source does not. Grep the repo for every identifier you quote. (A real case: PR\n  #6840's body advertised `armeria.xds.lb.request`, while the code registered `lb.select`.)\n- **Whether a claim is true**, as opposed to well-formed. Read the code the entry describes.\n\n## Execution Checklist\n\n- [ ] Phase 0 — Ran `npm run release-note <version>` and verified output file exists\n- [ ] Phase 1 — Read draft, recent examples, and style guide\n- [ ] Phase 2 — Fetched PR/issue context for all referenced PRs\n- [ ] Phase 3 — Triaged all \"Maybe ignore\" entries\n- [ ] Phase 4 — Rewrote all entries per style guide\n- [ ] Phase 5 — Cleaned up dependencies section\n- [ ] Phase 6 — Removed empty sections, finalized file, reported summary\n- [ ] Phase 7 — Ran `lint-release-note.sh` and it exited 0\n\n## Common Mistakes to Avoid\n\n- **Copying PR titles as-is**: PR titles like \"Fix NPE in FooBar\" are not user-friendly.\n  Rewrite as \"Fixed a `NullPointerException` in [FooBar](type) when ...\"\n- **Fabricating code examples**: If you cannot find a clear usage pattern from the PR description\n  or source code, write a descriptive sentence instead of guessing at code.\n- **Over-linking types**: Only use `[Name](type)` for Armeria's own public API types, not for\n  JDK classes, third-party libraries, or internal classes.\n- **Including build dependencies**: The dependency update PR body contains `- Build` sub-bullets\n  for test/build-only deps. These must be stripped.\n- **Leaving `- N/A` sections**: The polished output should only contain sections with actual content.\n- **Using `*` bullets**: Standardize on `-` dashes for all bullet points.\n- **Missing `👈👈👈` callouts**: Every code example for a new feature should highlight the key line.\n","frontmatter":{"name":"release-note","description":"Generate and polish release notes for an Armeria version. Runs site/release-note.ts to collect PR data from a GitHub milestone, then rewrites the skeletal output into publication-ready MDX. Invoked as `/release-note <version>` (e.g., `/release-note 1.38.0`)."},"isInternal":false,"tokens":2911,"sizeBytes":12088},{"name":"SKILL.md","path":".agents/skills/upgrade-deps/SKILL.md","rawUrl":"https://raw.githubusercontent.com/line/armeria/HEAD/.agents/skills/upgrade-deps/SKILL.md","title":"upgrade-deps","category":"anthropic-skill","format":"markdown","content":"---\nname: upgrade-deps\ndescription: Upgrade Gradle dependencies and Gradle wrapper version\ndisable-model-invocation: true\n---\n\n# Dependency Upgrade Workflow\n\nThis skill performs a full Gradle dependency upgrade for the Armeria project step by step.\n\n## Background: Minimum Java Version Per Module\n\nArmeria is a multi-module project with different minimum Java version requirements.\nBefore upgrading a dependency, always verify that the new version supports the minimum\nJava version required by the modules that use it.\n\n| Java Version | Modules |\n|-------------|---------|\n| **8**  | `core`, `brave5`, `brave6`, `eureka`, `grpc`, `grpc-kotlin`, `graphql-protocol`, `jetty9`, `junit4`, `junit5`, `kafka`, `kotlin`, `logback`, `logback12`, `logback13`, `micrometer-context`, `oauth2`, `prometheus1`, `protobuf`, `reactor3`, `resteasy`, `retrofit2`, `rxjava2`, `rxjava3`, `sangria`, `scala*`, `spring:boot2-*`, `dropwizard2`, `thrift0.9`–`thrift0.17`, `tomcat8`, `tomcat9`, `xds`, `zookeeper3`, `saml`, `bucket4j`, `consul`, `nacos` |\n| **11** | `athenz`, `graphql`, `jetty10`, `jetty11`, `kubernetes`, `logback14`, `thrift0.18`–`thrift0.22`, `tomcat10` |\n| **17** | `ai:mcp`, `jetty12`, `resilience4j2`, `spring:boot3-*`, `spring:spring6`, `spring:boot4-*`, `spring:spring7` |\n\nWhen in doubt, check `settings.gradle` for the `java`, `java11`, or `java17` flag on each subproject.\n\n**The table above lists published modules only.** Integration-test (`:it:*`) and example (`:examples:*`)\nmodules carry their own flags in `settings.gradle` and are frequently `java17` already. A dependency\nconsumed *only* by those modules is not held back by the Java 8 baseline at all — always resolve the\nactual consumer before assuming a Java constraint applies.\n\n## Step 1: Run dependencyUpdates\n\n```\n./gradlew dependencyUpdates --no-parallel\n```\n\nThe report is saved to `build/dependencyUpdates/report.txt`.\n\n## Step 2: Parse the Report\n\nRead `build/dependencyUpdates/report.txt` to identify all outdated dependencies.\nFor each dependency, note the current version and the available latest version.\n\nIf a version looks ambiguous (unexpected format, major version bump, unusual naming),\nverify on Maven Central before proceeding:\n  https://central.sonatype.com/artifact/{groupId}/{artifactId}\n\n## Step 3: Check Skip Hints and Java Version Compatibility\n\nBefore upgrading any dependency, check `dependencies.toml` for a comment directly above\nits version entry. Many pinned versions have an explicit reason, for example:\n\n```toml\n# Don't upgrade Caffeine to 3.x that requires Java 11.\ncaffeine = \"2.9.3\"\n\n# Upgrade once https://github.com/ronmamo/reflections/issues/279 is fixed.\nreflections = \"0.9.11\"\n\n# Ensure that we use the same ZooKeeper version as what Curator depends on.\nzookeeper = \"3.9.3\"\n```\n\n### Step 3.1: Validate the comment before trusting it\n\nA hold comment is a claim about this repository, and claims go stale. **Never treat a comment as\nproof — re-derive its reason every upgrade.** For each held entry:\n\n1. Find the aliases for the version key in the `[libraries.*]` / `[plugins]` sections.\n2. Grep the build scripts for each alias accessor (`libs.foo.bar` for alias `foo-bar`):\n   `git grep -n 'libs\\.foo\\.bar' -- '*.gradle' '*.gradle.kts'`\n3. Map each hit's build file to its project, and read that project's flags in `settings.gradle`.\n4. Compare the *consumer's* Java level against the new version's requirement — **not** the library's\n   requirement in isolation. \"Library X requires Java 17\" is not a blocker when every consumer is\n   already `java17`.\n\nIf the reason no longer holds, **remove the comment and upgrade**. If it still holds, keep both.\n\nTwo comments were found stale this way (`dgs` and `graphql-kotlin`, both consumed only by `java17`\nmodules), so treat this step as mandatory rather than a spot check.\n\n### Step 3.2: Never write a hold reason you have not verified\n\nWhen you add a comment, the reason must be something you confirmed, with the evidence to hand:\n\n- **Java constraint** — confirmed against the consumer module's flag, per Step 3.1.\n- **Runtime/API breakage** — confirmed by actually building and running the affected module's tests,\n  and the comment names the concrete blocker (e.g. `# Don't upgrade graphql-kotlin to 10.x that\n  migrated to Jackson 3`), not a vague \"major upgrade deferred\".\n- **Version-sync constraint** — confirmed from the other project's POM *and* from this repo's history\n  (`git log -p -- dependencies.toml`). Do not invent a sync rule: check whether the two versions have\n  ever actually moved independently before claiming they must move together.\n\nA plausible-sounding reason that nobody verified is worse than no comment, because the next upgrade\nwill trust it.\n\nIf no comment exists, proceed with the Java version compatibility check:\n1. Find which modules use this dependency in `dependencies.toml`\n2. Determine the minimum Java version required by those modules (see table above)\n3. Check the new library version's minimum Java requirement (Maven Central / release notes)\n4. If the library's required Java > module's minimum Java → **skip the upgrade** and add a\n   comment above the version entry explaining why (e.g. `# X.Y requires Java 11`)\n5. If compatible → proceed with the upgrade\n\n## Step 4: Update dependencies.toml\n\nEdit the `[versions]` section in `dependencies.toml` to update the version strings.\nThe file is located at the root of the repository: `dependencies.toml`.\n\n## Step 5: Cross-check Updated Versions\n\nAfter editing `dependencies.toml`, re-read the full diff (`git diff dependencies.toml`) and\ncross-check every changed entry against the `dependencyUpdates` report:\n\n1. **No entry was accidentally skipped** — compare the report's \"The following dependencies\n   have later milestone versions\" list against your edits. If a dependency appears in the\n   report but not in the diff, either upgrade it or document why it was skipped.\n2. **Versions match the report** — confirm each new version in the diff matches the latest\n   version from the report, not a typo or intermediate version.\n3. **Pinned-version comments were respected** — verify you did not upgrade a dependency whose\n   comment says to skip it, and that you did not remove or contradict an existing comment.\n4. **Linked-version constraints are satisfied** — some dependencies must stay in sync with\n   others (e.g. ZooKeeper with Curator, Protobuf with gRPC). Read the comments above those\n   entries and verify the constraint still holds after the upgrade.\n\n## Step 6: Upgrade Gradle Wrapper\n\nCheck the latest stable Gradle release at https://gradle.org/releases/ and update\n`gradle/wrapper/gradle-wrapper.properties`:\n\n```properties\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-X.Y.Z-all.zip\n```\n\n## Step 7: Verify the Build\n\nRun the full build including tests to catch both compilation errors and runtime regressions:\n\n```\n./gradlew build --no-daemon\n```\n\nIf there are compilation errors, API breaking changes, or test failures caused by the upgrade,\nfix them before proceeding.\n\n## Step 8: Commit\n\nCreate a commit with the **exact** message format below. Follow it strictly — do not add extra\nsections, reorder bullets, or change the structure:\n\n```\nUpdate dependencies\n\n- {library-name} {old-version} -> {new-version}\n- {library-name} {old-version} -> {new-version}\n- Build\n   - {library-name} {old-version} -> {new-version}\n   - {library-name} {old-version} -> {new-version}\n```\n\nRules:\n- Each production dependency (api/implementation scope) gets one bullet: `- {name} {old} -> {new}`\n- The `- Build` bullet groups build-only dependencies (testImplementation, annotationProcessor,\n  relocated libs, non-transitive deps); nest them as sub-bullets with 3-space indent\n- Use the library's official name if one exists (e.g. `gRPC-Java`, `Jackson`, `Netty`, `Kotlin`,\n  `Reactor`, `Logback`, `Micrometer`); otherwise use the key name as-is from `dependencies.toml`\n- Sort entries **alphabetically (A → Z)** within each section\n- If no build-only deps were upgraded, omit the `- Build` section entirely\n- If any dependencies were **not upgraded** (due to Java version constraints, pinned-version\n  comments, or linked-version constraints), list them under a `- Unupdated` section with the\n  reason. This helps reviewers know which upgrades were intentionally skipped.\n\nExample:\n```\nUpdate dependencies\n\n- gRPC-Java 1.63.0 -> 1.64.0\n- Jackson 2.17.0 -> 2.18.0\n- Build\n   - checkstyle 10.14.0 -> 10.17.0\n   - ErrorProne 2.27.0 -> 2.28.0\n- Unupdated\n   - Caffeine 2.9.3 -> 3.2.0 (requires Java 11)\n   - ZooKeeper 3.9.3 (pinned to Curator's version)\n```\n","frontmatter":{"name":"upgrade-deps","description":"Upgrade Gradle dependencies and Gradle wrapper version","disable-model-invocation":true,"disableModelInvocation":true},"isInternal":false,"tokens":2176,"sizeBytes":8661},{"name":"style-guide.md","path":".claude/skills/release-note/references/style-guide.md","rawUrl":"https://raw.githubusercontent.com/line/armeria/HEAD/.claude/skills/release-note/references/style-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Release Notes Style Guide\n\nAnnotated examples from real Armeria release notes showing the exact formatting conventions.\n\n---\n\n## Major New Feature Entry\n\nBold title, description explaining value, `[Type](type)` links, code example with `👈👈👈`, PR refs.\n\n```mdx\n- **Standalone Athenz Token Validation**: You can now use [AthenzAuthorizer](type) to validate Athenz tokens\n  outside of Armeria's request pipeline. This allows you to easily integrate Athenz authorization into\n  third-party frameworks like Spring MVC or other servlet-based applications. #6607\n  ```java\n  ZtsBaseClient ztsBaseClient = ...;\n  AthenzAuthorizer authorizer =\n    AthenzAuthorizer.builder(ztsBaseClient)\n                    .policyConfig(new AthenzPolicyConfig(\"your-domain\"))\n                    .build();\n\n  // Validate tokens anywhere\n  AccessCheckStatus status =\n    authorizer.authorize(token, resource, action); // 👈👈👈\n  ```\n```\n\nKey elements:\n- `**Bold Title**:` prefix — only for significant features, not every entry\n- Description explains *what you can now do* and *why it matters*\n- `[AthenzAuthorizer](type)` — Armeria API type link\n- Code block indented under the bullet (2 spaces)\n- `// 👈👈👈` on the line the reader should focus on\n- `#6607` issue/PR references at end of description line (list both the issue and the PR)\n\n---\n\n## Minor New Feature Entry\n\nOne-liner without code example, for smaller additions.\n\n```mdx\n- **Per-Request Client TLS Configuration**: You can now specify [ClientTlsSpec](type) for each request using\n  [RequestOptions#clientTlsSpec()](type) or [ClientRequestContext#clientTlsSpec()](type). #6551\n```\n\nOr even simpler:\n\n```mdx\n- [ConnectionPoolListener](type) now supports ping-related events. #6539 #6552\n```\n\n---\n\n## Improvement Entry\n\nConcise description with type links. No code example unless it changes user-facing API.\n\n```mdx\n- [XdsBootstrap](type) now supports SDS. #6597 #6610 #6654\n- Improved warning logs by adding diagnostic context to frequent HTTP/2 connection errors. #6638\n```\n\n---\n\n## Bug Fix Entry\n\nDescribe the symptom that was fixed, not the internal root cause.\n\n```mdx\n- [GrpcMeterIdPrefixFunction](type) records `grpc-status` correctly for responses which fail with an exception. #6606 #6621\n- `X-Forwarded-For` header values with leading or trailing whitespace around comma-separated addresses\n  (e.g., `\"192.168.1.1 , 10.0.0.1\"`) are now trimmed and parsed correctly. #6615\n- Setting `pingIntervalMillis` to a value greater than 33 seconds no longer throws an exception. #6648\n  - Linux keepalive socket options (`SO_KEEPALIVE` `TCP_KEEPIDLE`, `TCP_KEEPINTVL`) are no longer set by default.\n```\n\nKey patterns:\n- \"[Thing] now [works correctly]\" or \"[Thing] no longer [does wrong thing]\"\n- Sub-bullets for related side effects or additional context\n- Use backticks for config names, error types, and header names\n\n---\n\n## Breaking Change Entry\n\nState what changed and what users must do to migrate.\n\n```mdx\n- A subclass of [AbstractEndpointSelector](type) must implement [AbstractEndpointSelector#doSelectNow(ClientRequestContext)](type)\n  instead of [AbstractEndpointSelector#selectNow(ClientRequestContext)](type), which is now a final method. #6535\n```\n\nFor non-trivial migrations, include before/after code.\n\n---\n\n## Documentation Entry\n\nBrief description with links to new/updated docs.\n\nLink to docs pages with a **site-relative path** (`/docs/...`), never an absolute `https://armeria.dev/...`\nURL — that is the convention across the docs and the newer release notes, and it keeps links working\non local builds and previews.\n\n```mdx\n- New comprehensive documentation on understanding and handling timeouts: #6592\n  - [Understanding timeout and cancellation origins](/docs/advanced/understanding-timeouts)\n  - Handling timeouts for streaming:\n    - [Client-side streaming](/docs/client/timeouts#handling-timeouts-for-streaming-responses)\n    - [Server-side streaming](/docs/server/timeouts#handling-timeouts-for-streaming-requests)\n```\n\n---\n\n## Dependencies Section\n\nFormat: `- LibraryName oldVersion → newVersion`, sorted alphabetically.\n\n```mdx\n- Athenz 1.12.31 → 1.12.33\n- BlockHound 1.0.15 → 1.0.16\n- Jackson 2.20.1 → 2.21.0\n- Kotlin 2.2.21 → 2.3.0\n- Logback 1.5.23 → 1.5.27\n- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3\n- Spring Boot 3.5.8 → 3.5.10, 4.0.1 → 4.0.2\n```\n\nRules:\n- Use `→` (unicode arrow), not `->` or `-->`\n- Use `-` dash bullets, not `*`\n- Use the library's common name, not Maven artifact IDs\n- Group multi-version bumps: `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n- Omit build-only dependencies (anything under the `- Build` section in the raw dependency PR)\n- Omit **test-only** dependencies (e.g., Apache HttpClient, OkHttp, DGS) — only used in\n  `testImplementation` and not in published POMs\n- Omit **shaded** dependencies (e.g., Guava, Bouncy Castle, JCTools) — relocated into Armeria\n  JARs and invisible to users\n- Omit dependencies that are **not transitive** in any published artifact (e.g., Jsoup)\n- When unsure, check `dependencies.toml` for scope and whether the dep appears in a published\n  module's `api` or `implementation` configuration\n- Sort alphabetically (A → Z)\n- Strip trailing release qualifiers from version numbers — `.Final`, `.RELEASE`, `-GA`,\n  `.vYYYYMMDD` and the like. They add no information a reader acts on:\n  - `Netty 4.2.15.Final → 4.2.16.Final` becomes `Netty 4.2.15 → 4.2.16`\n  - `Javassist 3.31.0-GA → 3.32.0-GA` becomes `Javassist 3.31.0 → 3.32.0`\n  - `Jetty 9.4.55.v20240627 → 9.4.58.v20250814` becomes `Jetty 9.4.55 → 9.4.58`\n\n---\n\n## Thank You Section\n\n```mdx\n## 🙇 Thank you\n\n<ThankYou usernames={[\n  'contributor1',\n  'contributor2',\n  'contributor3'\n]} />\n```\n\nRules:\n- Usernames sorted alphabetically\n- Remove bot accounts: `dependabot[bot]`, `CLAassistant`\n- Core maintainers from `.github/CODEOWNERS` must always be included\n- One username per line, single-quoted, comma-separated\n\n---\n\n## What NOT to Include\n\n- **`- N/A` entries**: Remove sections that have no content instead of writing `- N/A`\n- **`🗑 Maybe ignore` section**: Must be fully triaged and removed\n- **Build-only dependencies**: `- Build` sub-section from dependency PRs\n- **Internal refactoring details**: Changes with no user-facing impact\n- **Site/docs dependency bumps**: npm bumps for `site/` or `docs-client/` (e.g., webpack, lodash)\n\n## Type Link Rules\n\nUse `[ClassName](type)` ONLY for Armeria public API references:\n- Classes/interfaces: `[GrpcServiceBuilder](type)`\n- Methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always use the full\n  `ClassName#methodName(ParamType)` form. Never use backtick-only style like\n  `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - **No space after commas** in the parameter list. The lookup key in `gen-src/api-index.json`\n    has none, so `[X#m(A, B)](type)` silently resolves to a dead `type://#` link while\n    `[X#m(A,B)](type)` works. The rendered text hides the difference — it shows `X.m()` either way.\n  - **Never split a method across a class link and a backtick.** `[Foo](type)` `` `.bar()` `` is\n    not a link to `bar()`; write `[Foo#bar(Baz)](type)` as one unit.\n- Annotations: `[@Blocking](type)`\n\nDo NOT use `[Name](type)` for:\n- JDK types (`String`, `Duration`, `CompletableFuture`, `List`)\n- Third-party types (`Mono`, `Flux`, `HttpServletRequest`)\n- Internal/private Armeria classes\n- Generic concepts (use plain text or backticks instead)\n- Method calls inside code blocks — code blocks use plain Java syntax, not `(type)` links\n","isInternal":false,"tokens":2097,"sizeBytes":7618},{"name":"SKILL.md","path":".claude/skills/release-note/SKILL.md","rawUrl":"https://raw.githubusercontent.com/line/armeria/HEAD/.claude/skills/release-note/SKILL.md","title":"release-note","category":"anthropic-skill","format":"markdown","content":"---\nname: release-note\ndescription: Generate and polish release notes for an Armeria version. Runs site/release-note.ts to collect PR data from a GitHub milestone, then rewrites the skeletal output into publication-ready MDX. Invoked as `/release-note <version>` (e.g., `/release-note 1.38.0`).\n---\n\n# Release Note Generator\n\nGenerates skeletal release notes from a GitHub milestone using `site/release-note.ts`,\nthen rewrites them into polished, publication-ready MDX with rich descriptions, code examples,\nand proper formatting.\n\n## Prerequisites\n\n- The `gh` CLI must be authenticated with access to `line/armeria`. Verify with `gh auth status`.\n- A `GITHUB_ACCESS_TOKEN` environment variable is recommended for higher GitHub API rate limits.\n  If not set, the script falls back to anonymous access (lower rate limits).\n- Node.js and npm must be available. The `site/` directory must have dependencies installed\n  (`npm install` in `site/`).\n\n## Invocation\n\n```\n/release-note <version>\n```\n\nExample: `/release-note 1.38.0`\n\n---\n\n## Phase 0: Generate Skeletal Release Notes\n\n1. Verify the GitHub milestone exists for the given version by checking:\n   ```\n   gh api repos/line/armeria/milestones --jq '.[] | select(.title == \"<version>\") | .number'\n   ```\n2. Run the release note generation script:\n   ```\n   cd site && npm run release-note <version>\n   ```\n3. Verify the output file was created at `site/src/content/release-notes/<version>.mdx`.\n4. If the script fails (e.g., milestone not found, network error), report the error and stop.\n\n## Phase 1: Load Draft and Study Style\n\n1. Read the generated draft file at `site/src/content/release-notes/<version>.mdx`.\n2. Read 3-4 recent polished release notes (e.g., `1.36.0.mdx`, `1.37.0.mdx`) to calibrate tone and style.\n3. Read the style guide at `references/style-guide.md` for formatting rules.\n4. Extract all PR/issue references (`#NNNN`) from every line of the draft.\n\n## Phase 2: Gather PR Context from GitHub\n\nFor each unique PR number found in the draft:\n\n1. Fetch PR details:\n   ```\n   gh pr view <number> --repo line/armeria --json title,body,labels,files\n   ```\n2. Parse the PR body to extract **Motivation**, **Modifications**, and **Result** sections.\n3. Fetch PR review comments — these often contain important design decisions, caveats, and\n   scope limitations (e.g., \"this only applies to unary calls\") that are not in the PR description:\n   ```\n   gh api repos/line/armeria/pulls/<number>/comments --jq '.[].body'\n   ```\n4. Extract linked issue numbers from the body (`Closes #NNNN`, `Fixes #NNNN`, `Resolves #NNNN`).\n5. For each linked issue, fetch its context **including comments**, which often contain use cases,\n   edge cases, and design discussions that inform the release note description:\n   ```\n   gh issue view <number> --repo line/armeria --json title,body\n   gh api repos/line/armeria/issues/<number>/comments --jq '.[].body'\n   ```\n6. For PRs in the \"New features\" section that introduce significant new API:\n   - Read key changed source files from the PR's `files` list to understand method signatures.\n   - Look for usage examples in the PR description's Result section first — prefer these over\n     constructing examples from scratch.\n   - Check review comments for scope limitations, caveats, or known constraints that should be\n     mentioned in the release note (e.g., \"only supports unary methods\", \"HTTP/2 only\").\n\n**Rate limiting**: If fetching many PRs, batch requests and pause briefly between them to avoid\nGitHub API rate limits.\n\n## Phase 3: Triage \"Maybe Ignore\" Section\n\nThe script puts PRs without a recognized label into `🗑 Maybe ignore`. For each entry:\n\n1. Check the PR's labels and description.\n2. **Drop** items that are purely internal (CI config, build scripts, test infrastructure,\n   non-user-facing refactoring, site/docs dependency bumps).\n3. **Relocate** user-facing items to the correct section based on their actual impact:\n   - API additions → New features\n   - Performance or usability improvements → Improvements\n   - Bug fixes → Bug fixes\n   - Breaking API changes → Breaking changes\n4. Report triage decisions to the user so they can override if needed.\n\n## Phase 4: Rewrite Each Section\n\nRewrite every entry following the formatting rules in `references/style-guide.md`.\n\n### Ordering\n\n1. **Lead with user interest**: Place the top 3 entries that users would care about most first.\n   Prioritize broadly applicable features (core, gRPC, Kubernetes) over niche modules (Athenz, xDS).\n   Consider the size of the user base affected and how common the use case is.\n2. **Then group by module**: After the top 3, group remaining entries by module/area so that\n   related changes appear together (e.g., Athenz entries adjacent, Kubernetes entries adjacent).\n3. This applies to all sections (New features, Improvements, Bug fixes, etc.).\n\n### Key principles:\n\n### New Features (`🌟 New features`)\n\n- All new feature entries get a bold title prefix: `- **Feature Title**: Description. #NNNN`\n- **Keep descriptions concise** — most entries should fit within 3 lines of prose.\n  Only high-impact features (e.g., a brand-new module or paradigm-shifting API) warrant\n  longer descriptions.\n- Include a Java code example (5-15 lines) whenever possible. It may be omitted if there is\n  no clear usage pattern to show.\n  - Mark the most important line with `// 👈👈👈`\n  - Indent code blocks with 2 spaces under the bullet.\n\n### Improvements (`📈 Improvements`)\n\n- Concise description of what improved and why it matters.\n- Code examples only if the improvement changes how users interact with an API.\n\n### Bug Fixes (`🛠️ Bug fixes`)\n\n- Describe the symptom that was fixed, not the internal cause.\n- Format: \"[What was broken] now [works correctly]. #NNNN\"\n\n### Breaking Changes (`☢️ Breaking changes`)\n\n- State clearly what changed and what users must do to migrate.\n- Include before/after code if the migration is non-trivial.\n\n### Documentation (`📃 Documentation`)\n\n- Brief description with links to the new/updated docs if available.\n\n### Deprecations (`🏚️ Deprecations`)\n\n- State what is deprecated and what to use instead.\n\n### All Sections — Common Rules\n\n- Use `[ClassName](type)` for Armeria API types (classes, interfaces, annotations, methods).\n  - For classes/interfaces: `[GrpcServiceBuilder](type)`\n  - For methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always include the\n    class name, method name, and parameter types. Do NOT use backtick-only style like\n    `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - Do NOT use this syntax for JDK types (`String`, `Duration`, `CompletableFuture`), third-party\n    types, or types that are not part of Armeria's public API.\n- Issue and PR references go at the end of the entry: `#6431 #6691` (list both the issue and the PR)\n- Do NOT copy PR titles verbatim — they are often terse commit-style messages.\n- Do NOT fabricate code examples. Derive them from PR descriptions or actual source code.\n- Keep entries self-contained — a reader should understand the change without clicking the PR link.\n\n## Phase 5: Clean Up Dependencies Section\n\nThe raw script includes the full dependency update PR body, which uses a structured commit message format.\n\n1. **Strip non-user-facing dependencies**:\n   - Remove the `- Build` section and all its sub-bullets\n     (these are testImplementation, annotationProcessor, and other non-production deps).\n   - Remove **test-only** dependencies (e.g., Apache HttpClient, OkHttp, DGS) that are only\n     used in `testImplementation` / `testFixturesImplementation` and never appear in published POMs.\n   - Remove **shaded** dependencies (e.g., Guava, Bouncy Castle, JCTools) that are relocated\n     into Armeria JARs and invisible to users at runtime.\n   - Remove dependencies that are **not transitive** in any published artifact (e.g., Jsoup).\n   - When unsure, check `dependencies.toml` for the dependency's scope (`testImplementation`,\n     `shaded`, etc.) and verify whether it appears in a published module's `api` or\n     `implementation` configuration.\n2. **Format each entry**: `- LibraryName oldVersion → newVersion`\n   - Use the library's common name (e.g., `Jackson`, `Netty`, `gRPC-Java`, not the Maven artifact ID).\n   - Use `→` (unicode arrow), not `->`.\n3. **Group multi-version bumps** on one line when a library has multiple version streams:\n   `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n4. **Sort alphabetically** (A → Z).\n\n## Optional: Validate with a Site Build\n\n`cd site && npm run build` validates the MDX end to end, but requires generated files under\n`site/gen-src/` (`api-index.json`, `versions.json`, `contributors.json`):\n\n- Full generation: `./gradlew :site:generateSiteSources` (api-index needs the aggregated Javadoc,\n  which is slow).\n- Quick validation: copy `api-index.json`/`versions.json` from a previous build and generate\n  `contributors.json` with\n  `gh api --paginate repos/line/armeria/contributors | jq -s '[.[][]] | map({(.login): .avatar_url}) | add'`.\n  A stale `api-index.json` is fine for validation — unknown `[X](type)` names render as blank\n  links (`type://#`) and never fail the build, so type names must be verified against the source\n  either way.\n- `npm run build | tail` masks the exit code — capture it separately.\n\n## Phase 6: Finalize\n\n1. **Remove empty sections**: Delete any section whose only content is `- N/A`.\n2. **Remove \"Maybe ignore\"**: The `🗑 Maybe ignore` section must not appear in the final output.\n3. **Deduplicate and sort contributors**: Ensure `<ThankYou usernames={[...]} />` has\n   alphabetically sorted, deduplicated usernames. Remove bot accounts (`dependabot[bot]`,\n   `CLAassistant`). Core maintainers listed in `.github/CODEOWNERS` must always be included\n   in the Thank You section, even if they don't appear in the PR participant lists.\n4. **Ensure consistent bullet style**: Use `-` (dash) for all bullets, not `*`.\n5. **Write the final file** to `site/src/content/release-notes/<version>.mdx`.\n6. **Show a summary** to the user: list the sections, entry count per section, and any entries\n   flagged as uncertain (where PR context was insufficient to write a confident description).\n\n---\n\n## Phase 7: Lint\n\nRun the mechanical checks and fix everything they report:\n\n```\n.claude/skills/release-note/scripts/lint-release-note.sh site/src/content/release-notes/<version>.mdx\n```\n\nThis is not optional, and reading the style guide is not a substitute for running it. The rules it\nenforces are all written down elsewhere in this skill and have still been broken while the author\nhad them open — a malformed `(type)` link never fails the site build, it just renders as a blank\n`type://#` link, so nothing else catches it.\n\nThe script cannot see two things, so check them by hand:\n\n- **Identifiers copied from a PR body** — metric names, class names, proto fields. PR descriptions\n  go stale; the source does not. Grep the repo for every identifier you quote. (A real case: PR\n  #6840's body advertised `armeria.xds.lb.request`, while the code registered `lb.select`.)\n- **Whether a claim is true**, as opposed to well-formed. Read the code the entry describes.\n\n## Execution Checklist\n\n- [ ] Phase 0 — Ran `npm run release-note <version>` and verified output file exists\n- [ ] Phase 1 — Read draft, recent examples, and style guide\n- [ ] Phase 2 — Fetched PR/issue context for all referenced PRs\n- [ ] Phase 3 — Triaged all \"Maybe ignore\" entries\n- [ ] Phase 4 — Rewrote all entries per style guide\n- [ ] Phase 5 — Cleaned up dependencies section\n- [ ] Phase 6 — Removed empty sections, finalized file, reported summary\n- [ ] Phase 7 — Ran `lint-release-note.sh` and it exited 0\n\n## Common Mistakes to Avoid\n\n- **Copying PR titles as-is**: PR titles like \"Fix NPE in FooBar\" are not user-friendly.\n  Rewrite as \"Fixed a `NullPointerException` in [FooBar](type) when ...\"\n- **Fabricating code examples**: If you cannot find a clear usage pattern from the PR description\n  or source code, write a descriptive sentence instead of guessing at code.\n- **Over-linking types**: Only use `[Name](type)` for Armeria's own public API types, not for\n  JDK classes, third-party libraries, or internal classes.\n- **Including build dependencies**: The dependency update PR body contains `- Build` sub-bullets\n  for test/build-only deps. These must be stripped.\n- **Leaving `- N/A` sections**: The polished output should only contain sections with actual content.\n- **Using `*` bullets**: Standardize on `-` dashes for all bullet points.\n- **Missing `👈👈👈` callouts**: Every code example for a new feature should highlight the key line.\n","frontmatter":{"name":"release-note","description":"Generate and polish release notes for an Armeria version. Runs site/release-note.ts to collect PR data from a GitHub milestone, then rewrites the skeletal output into publication-ready MDX. Invoked as `/release-note <version>` (e.g., `/release-note 1.38.0`)."},"isInternal":false,"tokens":3076,"sizeBytes":12753},{"name":"SKILL.md","path":".claude/skills/upgrade-deps/SKILL.md","rawUrl":"https://raw.githubusercontent.com/line/armeria/HEAD/.claude/skills/upgrade-deps/SKILL.md","title":"upgrade-deps","category":"anthropic-skill","format":"markdown","content":"---\nname: upgrade-deps\ndescription: Upgrade Gradle dependencies and Gradle wrapper version\ndisable-model-invocation: true\n---\n\n# Dependency Upgrade Workflow\n\nThis skill performs a full Gradle dependency upgrade for the Armeria project step by step.\n\n## Background: Minimum Java Version Per Module\n\nArmeria is a multi-module project with different minimum Java version requirements.\nBefore upgrading a dependency, always verify that the new version supports the minimum\nJava version required by the modules that use it.\n\n| Java Version | Modules |\n|-------------|---------|\n| **8**  | `core`, `brave5`, `brave6`, `eureka`, `grpc`, `grpc-kotlin`, `graphql-protocol`, `jetty9`, `junit4`, `junit5`, `kafka`, `kotlin`, `logback`, `logback12`, `logback13`, `micrometer-context`, `oauth2`, `prometheus1`, `protobuf`, `reactor3`, `resteasy`, `retrofit2`, `rxjava2`, `rxjava3`, `sangria`, `scala*`, `spring:boot2-*`, `dropwizard2`, `thrift0.9`–`thrift0.17`, `tomcat8`, `tomcat9`, `xds`, `zookeeper3`, `saml`, `bucket4j`, `consul`, `nacos` |\n| **11** | `athenz`, `graphql`, `jetty10`, `jetty11`, `kubernetes`, `logback14`, `thrift0.18`–`thrift0.22`, `tomcat10` |\n| **17** | `ai:mcp`, `jetty12`, `resilience4j2`, `spring:boot3-*`, `spring:spring6`, `spring:boot4-*`, `spring:spring7` |\n\nWhen in doubt, check `settings.gradle` for the `java`, `java11`, or `java17` flag on each subproject.\n\n**The table above lists published modules only.** Integration-test (`:it:*`) and example (`:examples:*`)\nmodules carry their own flags in `settings.gradle` and are frequently `java17` already. A dependency\nconsumed *only* by those modules is not held back by the Java 8 baseline at all — always resolve the\nactual consumer before assuming a Java constraint applies.\n\n## Step 1: Run dependencyUpdates\n\n```\n./gradlew dependencyUpdates --no-parallel\n```\n\nThe report is saved to `build/dependencyUpdates/report.txt`.\n\n## Step 2: Parse the Report\n\nRead `build/dependencyUpdates/report.txt` to identify all outdated dependencies.\nFor each dependency, note the current version and the available latest version.\n\nIf a version looks ambiguous (unexpected format, major version bump, unusual naming),\nverify on Maven Central before proceeding:\n  https://central.sonatype.com/artifact/{groupId}/{artifactId}\n\n## Step 3: Check Skip Hints and Java Version Compatibility\n\nBefore upgrading any dependency, check `dependencies.toml` for a comment directly above\nits version entry. Many pinned versions have an explicit reason, for example:\n\n```toml\n# Don't upgrade Caffeine to 3.x that requires Java 11.\ncaffeine = \"2.9.3\"\n\n# Upgrade once https://github.com/ronmamo/reflections/issues/279 is fixed.\nreflections = \"0.9.11\"\n\n# Ensure that we use the same ZooKeeper version as what Curator depends on.\nzookeeper = \"3.9.3\"\n```\n\n### Step 3.1: Validate the comment before trusting it\n\nA hold comment is a claim about this repository, and claims go stale. **Never treat a comment as\nproof — re-derive its reason every upgrade.** For each held entry:\n\n1. Find the aliases for the version key in the `[libraries.*]` / `[plugins]` sections.\n2. Grep the build scripts for each alias accessor (`libs.foo.bar` for alias `foo-bar`):\n   `git grep -n 'libs\\.foo\\.bar' -- '*.gradle' '*.gradle.kts'`\n3. Map each hit's build file to its project, and read that project's flags in `settings.gradle`.\n4. Compare the *consumer's* Java level against the new version's requirement — **not** the library's\n   requirement in isolation. \"Library X requires Java 17\" is not a blocker when every consumer is\n   already `java17`.\n\nIf the reason no longer holds, **remove the comment and upgrade**. If it still holds, keep both.\n\nTwo comments were found stale this way (`dgs` and `graphql-kotlin`, both consumed only by `java17`\nmodules), so treat this step as mandatory rather than a spot check.\n\n### Step 3.2: Never write a hold reason you have not verified\n\nWhen you add a comment, the reason must be something you confirmed, with the evidence to hand:\n\n- **Java constraint** — confirmed against the consumer module's flag, per Step 3.1.\n- **Runtime/API breakage** — confirmed by actually building and running the affected module's tests,\n  and the comment names the concrete blocker (e.g. `# Don't upgrade graphql-kotlin to 10.x that\n  migrated to Jackson 3`), not a vague \"major upgrade deferred\".\n- **Version-sync constraint** — confirmed from the other project's POM *and* from this repo's history\n  (`git log -p -- dependencies.toml`). Do not invent a sync rule: check whether the two versions have\n  ever actually moved independently before claiming they must move together.\n\nA plausible-sounding reason that nobody verified is worse than no comment, because the next upgrade\nwill trust it.\n\nIf no comment exists, proceed with the Java version compatibility check:\n1. Find which modules use this dependency in `dependencies.toml`\n2. Determine the minimum Java version required by those modules (see table above)\n3. Check the new library version's minimum Java requirement (Maven Central / release notes)\n4. If the library's required Java > module's minimum Java → **skip the upgrade** and add a\n   comment above the version entry explaining why (e.g. `# X.Y requires Java 11`)\n5. If compatible → proceed with the upgrade\n\n## Step 4: Update dependencies.toml\n\nEdit the `[versions]` section in `dependencies.toml` to update the version strings.\nThe file is located at the root of the repository: `dependencies.toml`.\n\n## Step 5: Cross-check Updated Versions\n\nAfter editing `dependencies.toml`, re-read the full diff (`git diff dependencies.toml`) and\ncross-check every changed entry against the `dependencyUpdates` report:\n\n1. **No entry was accidentally skipped** — compare the report's \"The following dependencies\n   have later milestone versions\" list against your edits. If a dependency appears in the\n   report but not in the diff, either upgrade it or document why it was skipped.\n2. **Versions match the report** — confirm each new version in the diff matches the latest\n   version from the report, not a typo or intermediate version.\n3. **Pinned-version comments were respected** — verify you did not upgrade a dependency whose\n   comment says to skip it, and that you did not remove or contradict an existing comment.\n4. **Linked-version constraints are satisfied** — some dependencies must stay in sync with\n   others (e.g. ZooKeeper with Curator, Protobuf with gRPC). Read the comments above those\n   entries and verify the constraint still holds after the upgrade.\n\n## Step 6: Upgrade Gradle Wrapper\n\nCheck the latest stable Gradle release at https://gradle.org/releases/ and update\n`gradle/wrapper/gradle-wrapper.properties`:\n\n```properties\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-X.Y.Z-all.zip\n```\n\n## Step 7: Verify the Build\n\nRun the full build including tests to catch both compilation errors and runtime regressions:\n\n```\n./gradlew build --no-daemon\n```\n\nIf there are compilation errors, API breaking changes, or test failures caused by the upgrade,\nfix them before proceeding.\n\n## Step 8: Commit\n\nCreate a commit with the **exact** message format below. Follow it strictly — do not add extra\nsections, reorder bullets, or change the structure:\n\n```\nUpdate dependencies\n\n- {library-name} {old-version} -> {new-version}\n- {library-name} {old-version} -> {new-version}\n- Build\n   - {library-name} {old-version} -> {new-version}\n   - {library-name} {old-version} -> {new-version}\n```\n\nRules:\n- Each production dependency (api/implementation scope) gets one bullet: `- {name} {old} -> {new}`\n- The `- Build` bullet groups build-only dependencies (testImplementation, annotationProcessor,\n  relocated libs, non-transitive deps); nest them as sub-bullets with 3-space indent\n- Use the library's official name if one exists (e.g. `gRPC-Java`, `Jackson`, `Netty`, `Kotlin`,\n  `Reactor`, `Logback`, `Micrometer`); otherwise use the key name as-is from `dependencies.toml`\n- Sort entries **alphabetically (A → Z)** within each section\n- If no build-only deps were upgraded, omit the `- Build` section entirely\n- If any dependencies were **not upgraded** (due to Java version constraints, pinned-version\n  comments, or linked-version constraints), list them under a `- Unupdated` section with the\n  reason. This helps reviewers know which upgrades were intentionally skipped.\n\nExample:\n```\nUpdate dependencies\n\n- gRPC-Java 1.63.0 -> 1.64.0\n- Jackson 2.17.0 -> 2.18.0\n- Build\n   - checkstyle 10.14.0 -> 10.17.0\n   - ErrorProne 2.27.0 -> 2.28.0\n- Unupdated\n   - Caffeine 2.9.3 -> 3.2.0 (requires Java 11)\n   - ZooKeeper 3.9.3 (pinned to Curator's version)\n```\n","frontmatter":{"name":"upgrade-deps","description":"Upgrade Gradle dependencies and Gradle wrapper version","disable-model-invocation":true,"disableModelInvocation":true},"isInternal":false,"tokens":2176,"sizeBytes":8661},{"name":"armeria-thread-pool-guide.md","path":"tools/claude-plugins/assistant/skills/eventloop-audit/references/armeria-thread-pool-guide.md","rawUrl":"https://raw.githubusercontent.com/line/armeria/HEAD/tools/claude-plugins/assistant/skills/eventloop-audit/references/armeria-thread-pool-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"<!-- A developer-facing version of this guide is published at /docs/advanced/threading-model -->\n\n# Armeria Thread Pool Guide for Developers\n\n## Context\n\nArmeria is built on Netty's non-blocking I/O model. Misusing thread pools — especially blocking the event loop — is the most common cause of performance degradation and deadlocks. This guide provides an architectural overview, recommended configurations, and practical dos/don'ts for developers working with Armeria.\n\n---\n\n## 1. Architectural Overview\n\n### Thread Pool Roles\n\n| Pool | What it does | Thread name pattern | Default size | Daemon? |\n|------|-------------|---------------------|-------------|---------|\n| **bossGroup** | Accepts incoming TCP connections (one per server port) | `armeria-boss-{protocol}-{addr}` where addr is `*:{port}`, `{ip}:{port}`, or `unix:{path}` | **1 per port** (fixed, not configurable) | No |\n| **workerGroup** | Handles all socket I/O (reads/writes) and executes non-blocking service logic | `armeria-common-worker-*` | `2 * CPU cores` (NIO/epoll/kqueue) or `CPU cores` (io_uring) | Yes |\n| **serviceWorkerGroup** | Optional dedicated EventLoopGroup for service method execution, isolating service logic from socket I/O | User-defined | Falls back to workerGroup if not set | Yes |\n| **blockingTaskExecutor** | Runs long-running or blocking operations (DB calls, file I/O, legacy sync APIs) | `armeria-common-blocking-tasks-*` | **200** threads (inspired by Tomcat default) | Yes |\n\n### How a Request Flows Through the Pools\n\n```text\nClient request\n    |\n    v\n[bossGroup]  -- accepts TCP connection, hands channel to workerGroup\n    |\n    v\n[workerGroup]  -- reads bytes, decodes HTTP/2 frames, invokes service\n    |\n    +---> Non-blocking service: runs directly on event loop thread\n    |\n    +---> @Blocking / useBlockingTaskExecutor(true): dispatched to blockingTaskExecutor\n    |\n    +---> serviceWorkerGroup configured: service runs on dedicated event loop\n```\n\n### Key Classes and Files\n\n- `CommonPools` — global shared pools (`core/.../common/CommonPools.java`)\n- `EventLoopGroups` — factory for creating EventLoopGroups (`core/.../common/util/EventLoopGroups.java`)\n- `BlockingTaskExecutor` / `BlockingTaskExecutorBuilder` — blocking pool abstraction (`core/.../common/util/`)\n- `EventLoopThread` — custom thread extending Netty's `FastThreadLocalThread`, implements `NonBlocking` marker (`core/.../internal/common/util/EventLoopThread.java`)\n- `EventLoopCheckingFuture` — warns when `.get()`/`.join()` is called from an event loop (`core/.../common/util/EventLoopCheckingFuture.java`)\n- `DefaultAnnotatedService` — dispatches to event loop or blocking executor based on `@Blocking` (`core/.../internal/server/annotation/DefaultAnnotatedService.java`)\n- `Flags` / `DefaultFlagsProvider` — system-wide defaults and JVM flag overrides (`core/.../common/Flags.java`)\n\n### Client-Side Thread Pools\n\nThe HTTP client (`WebClient`) uses the same `CommonPools.workerGroup()` by default. An `EventLoopScheduler` distributes connections across event loops per endpoint (default: 1 event loop per endpoint, configurable).\n\nKey client config class: `ClientFactoryBuilder` (`core/.../client/ClientFactoryBuilder.java`)\n\n---\n\n## 2. `serviceWorkerGroup` vs `blockingTaskExecutor` — When to Use Which\n\nThese two are the most commonly confused. They solve **different problems** and have fundamentally different runtime characteristics.\n\n### The Core Distinction\n\n| Aspect | `serviceWorkerGroup` | `blockingTaskExecutor` |\n|--------|----------------------|------------------------|\n| **Java type** | `EventLoopGroup` (Netty event loops) | `ScheduledExecutorService` (thread pool) |\n| **Thread type** | Event loop threads (`NonBlocking`) | Regular threads (blocking allowed) |\n| **Can you block in it?** | **NO** — same rules as workerGroup | **YES** — that's its purpose |\n| **Scheduling model** | Single-threaded per loop, run-to-completion | Traditional thread pool, one thread per task |\n| **Typical size** | Small (matches CPU cores) | Large (default 200) |\n\n### How They Work at Runtime\n\n**serviceWorkerGroup** — The service method still runs on an event loop, just a *different* one from the I/O channel. In `HttpServerHandler`, Armeria checks whether the configured serviceWorkerGroup differs from the main workerGroup. If so, it calls `req.subscribeOn(serviceEventLoop)` and invokes the service on that loop. Request reading, service execution, and response emission all happen on the service event loop. Response *writing* to the socket still goes back to the channel's original event loop.\n\n```text\n[workerGroup event loop]  reads bytes from socket\n        |\n        v  (if serviceWorkerGroup != workerGroup)\n[serviceWorkerGroup event loop]  decodes request, runs service, emits response\n        |\n        v  (response writing)\n[workerGroup event loop]  writes bytes to socket\n```\n\n**blockingTaskExecutor** — The service method is submitted via `thenApplyAsync(..., ctx.blockingTaskExecutor())` to a traditional thread pool. The calling event loop is freed immediately. The blocking thread runs the service logic, and when done, the response flows back to the event loop for writing.\n\n```text\n[event loop]  reads bytes, decodes request\n        |\n        v  (thenApplyAsync)\n[blocking thread pool]  runs service method (blocking OK here)\n        |\n        v  (future completes)\n[event loop]  writes response bytes to socket\n```\n\n### Decision Guide\n\n**Use `serviceWorkerGroup` when:**\n\n- Your service does **CPU-intensive but non-blocking** work (e.g., JSON serialization of large payloads, image transformation in memory, complex computation)\n- You want to **prevent service CPU work from starving socket I/O** — if your service burns 50ms of CPU per request, that delays read/write for all other connections sharing that event loop\n- You need **isolation between services** — e.g., a high-throughput service shouldn't compete for event loop time with a latency-sensitive one\n- Your code is already fully async (returns `CompletableFuture`, uses reactive streams) — you just want it on a different event loop\n\n```java\n// Good use case: CPU-heavy serialization isolated from I/O\nServer.builder()\n      .serviceWorkerGroup(4)  // Dedicated event loops for service logic\n      .service(\"/heavy-json\", (ctx, req) -> {\n          // Non-blocking but CPU-intensive: serialize huge object\n          return HttpResponse.of(hugeObjectMapper.writeValueAsBytes(bigData));\n      })\n      .build();\n```\n\n**Use `blockingTaskExecutor` when:**\n\n- Your code calls **synchronous blocking APIs** (JDBC, file I/O, `Thread.sleep`, `future.get()`, legacy HTTP clients, `synchronized` blocks)\n- You're integrating with **libraries that block the calling thread** (most traditional Java libraries)\n- You have **no choice** but to wait for a result synchronously\n\n```java\n// Good use case: JDBC call that blocks the thread\nServer.builder()\n      .service(\"/users\", (ctx, req) ->\n          HttpResponse.of(CompletableFuture.supplyAsync(() -> {\n              // This blocks — JDBC has no async API\n              User user = jdbcTemplate.queryForObject(\"SELECT ...\", User.class);\n              return HttpResponse.of(HttpStatus.OK, MediaType.JSON, toJson(user));\n          }, ctx.blockingTaskExecutor())))\n      .build();\n```\n\n**DON'T use `serviceWorkerGroup` for blocking work** — it's still an event loop. Blocking it has the same catastrophic effect as blocking the main workerGroup. You're just blocking a *different* event loop.\n\n**DON'T use `blockingTaskExecutor` for CPU-bound async work** — it wastes a thread sitting idle during async operations and doesn't benefit from event loop optimizations (FastThreadLocal, zero-copy buffers, etc.).\n\n### When You Need Neither\n\nMost async services need neither. If your service just orchestrates async HTTP calls, reactive streams, or CompletableFuture chains, let it run on the default workerGroup.\n\n---\n\n## 3. Recommended Configuration Settings\n\n### Sizing Guidelines\n\n| Pool | Guideline |\n|------|-----------|\n| **workerGroup** | Default (`2 * CPU`) is usually fine. Increase only if profiling shows event loop saturation on I/O-heavy workloads. |\n| **blockingTaskExecutor** | Default 200 is generous. Size based on expected concurrent blocking operations. Consider: `expected_concurrent_blocking_calls * avg_blocking_duration / target_latency`. |\n| **serviceWorkerGroup** | Only configure if you need to isolate service execution from socket I/O (e.g., CPU-heavy request processing that shouldn't delay I/O). Most services don't need this. |\n\n### JVM Flags\n\n```properties\n-Dcom.linecorp.armeria.numCommonWorkers=<int>           # Override worker group size\n-Dcom.linecorp.armeria.numCommonBlockingTaskThreads=<int> # Override blocking executor size\n-Dcom.linecorp.armeria.reportBlockedEventLoop=true       # Enable event loop blocking warnings (default: true)\n```\n\n### Server Builder Example\n\n```java\nServer.builder()\n      // Custom worker group (only if default is insufficient)\n      .workerGroup(16)\n      // Custom blocking executor for this server\n      .blockingTaskExecutor(BlockingTaskExecutor.builder()\n              .numThreads(100)\n              .threadNamePrefix(\"my-app-blocking\")\n              .build(), true)\n      .service(\"/api\", myService)\n      .build();\n```\n\n### Client Factory Example\n\n```java\nClientFactory.builder()\n      .workerGroup(8)\n      // Distribute HTTP/2 connections across more event loops for high-throughput endpoints\n      .maxNumEventLoopsPerEndpoint(4)\n      .build();\n```\n\n---\n\n## 4. Dos and Don'ts\n\n### DO: Use `@Blocking` for Synchronous Service Methods\n\n```java\n// Annotated service — method level\n@Get(\"/users/{id}\")\n@Blocking\npublic User getUser(@Param int id) {\n    return database.findById(id); // Safe: runs on blocking executor\n}\n\n// Annotated service — class level (all methods become blocking)\n@Blocking\npublic class MyDatabaseService {\n    @Get(\"/items\")\n    public List<Item> list() { return db.listAll(); }\n}\n```\n\n### DO: Use `useBlockingTaskExecutor(true)` for gRPC/Thrift Services\n\n```java\n// gRPC\nServer.builder()\n      .service(GrpcService.builder()\n              .addService(new MyBlockingGrpcService())\n              .useBlockingTaskExecutor(true)\n              .build());\n\n// Thrift\nServer.builder()\n      .service(\"/thrift\", THttpService.builder()\n              .addService(new MyBlockingThriftService())\n              .useBlockingTaskExecutor(true)\n              .build());\n```\n\n### DO: Use `ctx.blockingTaskExecutor()` for Ad-Hoc Blocking Work\n\n```java\n@Get(\"/report\")\npublic CompletableFuture<HttpResponse> generateReport(ServiceRequestContext ctx) {\n    return CompletableFuture.supplyAsync(() -> {\n        byte[] pdf = slowPdfGenerator.generate(); // blocking\n        return HttpResponse.of(HttpStatus.OK, MediaType.PDF, pdf);\n    }, ctx.blockingTaskExecutor());\n}\n```\n\nThe `ctx.blockingTaskExecutor()` returns a context-aware executor — it automatically propagates `ServiceRequestContext` to the blocking thread, so logging, tracing, and `ServiceRequestContext.current()` work correctly.\n\n### DO: Stay Async When Possible\n\n```java\n// Prefer returning async types on the event loop\n@Get(\"/data\")\npublic CompletableFuture<String> getData() {\n    return asyncHttpClient.get(\"/upstream\")\n            .thenApply(response -> response.contentUtf8());\n    // Runs entirely on event loop — no blocking, no thread switch\n}\n```\n\n### DON'T: Block the Event Loop\n\n```java\n// NEVER do this on the event loop\n@Get(\"/bad\")\npublic String bad() {\n    Thread.sleep(1000);                    // Blocks event loop\n    return db.query(\"SELECT ...\");         // Blocks event loop\n    future.get();                          // Blocks event loop\n    future.join();                         // Blocks event loop\n    synchronized(lock) { ... }             // Can block event loop\n    new URL(...).openConnection();         // Blocks event loop\n}\n```\n\nArmeria will log a warning if you call `.get()` or `.join()` on an `EventLoopCheckingFuture` from an event loop thread. Treat these warnings as bugs.\n\n### DON'T: Create Unbounded Thread Pools for Blocking Work\n\n```java\n// Don't do this — use Armeria's blockingTaskExecutor instead\nExecutorService myPool = Executors.newCachedThreadPool();\nmyPool.submit(() -> { ... });\n// Problems: no context propagation, no metrics, no coordinated shutdown\n```\n\n### DON'T: Mix Blocking and Non-Blocking in the Same Service Without Annotation\n\n```java\n// BAD: one blocking method taints the whole event loop experience\npublic class MixedService {\n    @Get(\"/fast\")\n    public String fast() { return \"ok\"; }\n\n    @Get(\"/slow\")\n    public String slow() {\n        return db.query(\"...\"); // Blocks! But no @Blocking annotation\n    }\n}\n```\n\nFix: add `@Blocking` to the slow method, or use `ctx.blockingTaskExecutor()` explicitly.\n\n### DON'T: Assume `serviceWorkerGroup` is Required\n\nMost applications should NOT configure `serviceWorkerGroup`. It adds complexity and is only useful when:\n- Your service does CPU-intensive work (e.g., serialization of large payloads) that could starve socket I/O\n- You want strict isolation between I/O handling and request processing\n\nFor blocking work, use `blockingTaskExecutor` instead — that's what it's for.\n\n### DON'T: Forget Context Propagation When Switching Threads\n\n```java\n// BAD: loses ServiceRequestContext\nsomeOtherExecutor.submit(() -> {\n    ServiceRequestContext.current(); // Throws IllegalStateException!\n});\n\n// GOOD: use context-aware executor\nctx.blockingTaskExecutor().submit(() -> {\n    ServiceRequestContext.current(); // Works\n});\n\n// GOOD: if you must use a custom executor, propagate context manually\nsomeOtherExecutor.submit(ctx.makeContextAware(() -> {\n    ServiceRequestContext.current(); // Works\n}));\n```\n\n---\n\n## 5. Programmatic Usage (Without Annotations)\n\nWhen you implement `HttpService` directly, build decorators, or work with gRPC/Thrift at a lower level, you manage executor dispatch yourself.\n\n### 5.1 Direct `HttpService` with Blocking Work\n\n```java\n// Implement HttpService directly — you control everything\nHttpService myService = (ctx, req) -> {\n    CompletableFuture<HttpResponse> future = new CompletableFuture<>();\n    ctx.blockingTaskExecutor().execute(() -> {\n        try {\n            String result = legacySyncClient.call(); // blocking\n            future.complete(HttpResponse.of(HttpStatus.OK, MediaType.JSON, result));\n        } catch (Exception e) {\n            future.completeExceptionally(e);\n        }\n    });\n    return HttpResponse.of(future);\n};\n\nServer.builder().service(\"/api\", myService).build();\n```\n\n### 5.2 CompletableFuture Chaining with Explicit Executors\n\nUse `thenApplyAsync` / `thenComposeAsync` / `supplyAsync` with the right executor to control where each stage runs.\n\n```java\nHttpService myService = (ctx, req) -> {\n    // Stage 1: blocking DB call on blocking executor\n    CompletableFuture<List<Long>> ids = CompletableFuture.supplyAsync(\n            () -> {\n                assert ServiceRequestContext.current() == ctx;  // context is propagated\n                assert !ctx.eventLoop().inEventLoop();           // not on event loop\n                return jdbcTemplate.queryForList(\"SELECT id FROM items\", Long.class);\n            },\n            ctx.blockingTaskExecutor());\n\n    // Stage 2: async HTTP calls back on event loop\n    CompletableFuture<List<AggregatedHttpResponse>> details = ids.thenComposeAsync(\n            idList -> {\n                assert ctx.eventLoop().inEventLoop();  // back on event loop\n                List<CompletableFuture<AggregatedHttpResponse>> futures = idList.stream()\n                        .map(id -> backendClient.get(\"/items/\" + id).aggregate())\n                        .collect(Collectors.toList());\n                return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))\n                        .thenApply(v -> futures.stream()\n                                // join() is safe here: allOf() guarantees all futures\n                                // are already complete, so join() returns immediately.\n                                .map(CompletableFuture::join)\n                                .collect(Collectors.toList()));\n            },\n            ctx.eventLoop());\n\n    // Stage 3: assemble response (still on event loop)\n    CompletableFuture<HttpResponse> response = details.thenApply(\n            results -> HttpResponse.of(HttpStatus.OK, MediaType.JSON, toJson(results)));\n\n    return HttpResponse.of(response);\n};\n```\n\n### 5.3 Scheduling Delayed Work on the Event Loop\n\nUse `ctx.eventLoop().schedule()` for non-blocking timed operations.\n\n```java\n// gRPC: respond after a delay without blocking a thread\nServiceRequestContext.current().eventLoop().schedule(() -> {\n    responseObserver.onNext(buildReply(request.getName()));\n    responseObserver.onCompleted();\n}, 3, TimeUnit.SECONDS);\n```\n\n### 5.4 gRPC: Submit Blocking Work Manually\n\nWhen you don't want `useBlockingTaskExecutor(true)` for the entire service, submit individual calls.\n\n```java\npublic class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {\n    @Override\n    public void hello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {\n        // This method runs on the event loop by default.\n        // Submit only the blocking part:\n        ServiceRequestContext.current().blockingTaskExecutor().submit(() -> {\n            try {\n                Thread.sleep(3000); // Simulate blocking API\n            } catch (Exception ignored) {}\n            responseObserver.onNext(buildReply(request.getName()));\n            responseObserver.onCompleted();\n        });\n    }\n}\n```\n\n### 5.5 Thrift: Submit Blocking Work Manually\n\nSame pattern as gRPC.\n\n```java\nServiceRequestContext.current().blockingTaskExecutor().execute(() -> {\n    try {\n        Thread.sleep(3000); // Simulate blocking\n    } catch (Exception ignored) {}\n    resultHandler.onComplete(buildReply(request.getName()));\n});\n```\n\n### 5.6 Decorators with Executor Awareness\n\n```java\n// A decorator that logs request duration, running the delegate as-is\nHttpService decorated = myService.decorate((delegate, ctx, req) -> {\n    long start = System.nanoTime();\n    return delegate.serve(ctx, req).mapHeaders(headers -> {\n        long elapsed = System.nanoTime() - start;\n        logger.info(\"{} took {} ms\", ctx.path(), elapsed / 1_000_000);\n        return headers;\n    });\n});\n```\n\n### 5.7 Context Propagation with `makeContextAware`\n\nWhen you must use a third-party executor that Armeria doesn't control, wrap your task.\n\n```java\n// Wrapping a Runnable\nsomeExternalExecutor.submit(ctx.makeContextAware(() -> {\n    ServiceRequestContext.current(); // Works — context is mounted\n    doWork();\n}));\n\n// Wrapping a Callable\nFuture<String> result = someExternalExecutor.submit(ctx.makeContextAware(() -> {\n    return ServiceRequestContext.current().path(); // Works\n}));\n\n// Wrapping a Function (for CompletableFuture chains)\nfuture.thenApply(ctx.makeContextAware(value -> {\n    ServiceRequestContext.current(); // Works\n    return transform(value);\n}));\n```\n\n### 5.8 Reactor Integration\n\n```java\nHttpService myService = (ctx, req) -> {\n    Scheduler blockingScheduler = Schedulers.fromExecutor(ctx.blockingTaskExecutor());\n    Scheduler eventLoopScheduler = Schedulers.fromExecutor(ctx.eventLoop());\n\n    Mono<String> result = Mono.fromCallable(() -> {\n                // Runs on blocking scheduler\n                return database.query(\"SELECT ...\");\n            })\n            .subscribeOn(blockingScheduler)\n            .flatMap(dbResult ->\n                // Switch back to event loop for async HTTP call\n                Mono.fromCompletionStage(backendClient.get(\"/enrich/\" + dbResult).aggregate())\n                    .subscribeOn(eventLoopScheduler)\n                    .map(resp -> resp.contentUtf8())\n            );\n\n    return HttpResponse.of(result.map(body ->\n            HttpResponse.of(HttpStatus.OK, MediaType.JSON, body)).toFuture());\n};\n```\n\n### 5.9 RxJava Integration\n\n```java\nScheduler blockingScheduler = Schedulers.from(ctx.blockingTaskExecutor());\nScheduler eventLoopScheduler = Schedulers.from(ctx.eventLoop());\n\nSingle<String> result = Single.fromCallable(() -> database.query(\"SELECT ...\"))\n        .subscribeOn(blockingScheduler)       // blocking work here\n        .observeOn(eventLoopScheduler)        // switch to event loop\n        .flatMap(dbResult ->\n            Single.fromCompletionStage(backendClient.get(\"/enrich/\" + dbResult).aggregate())\n                  .map(AggregatedHttpResponse::contentUtf8));\n```\n\n---\n\n## Quick Reference: Which Pool to Use\n\n| Scenario | Pool | How |\n|----------|------|-----|\n| HTTP/gRPC request handling (async) | workerGroup (event loop) | Default — just return `CompletableFuture` or reactive types |\n| Database queries, file I/O, sync APIs | blockingTaskExecutor | `@Blocking`, `useBlockingTaskExecutor(true)`, or `ctx.blockingTaskExecutor()` |\n| CPU-heavy non-blocking work that shouldn't starve I/O | serviceWorkerGroup | `ServerBuilder.serviceWorkerGroup(n)` (rare) |\n| Client HTTP calls | workerGroup (event loop) | Default — `WebClient` is async |\n| Legacy synchronous library calls | blockingTaskExecutor | Wrap with `ctx.blockingTaskExecutor().submit(...)` |\n| Delayed/scheduled non-blocking work | event loop | `ctx.eventLoop().schedule(...)` |\n| Reactor blocking operations | blockingTaskExecutor | `.subscribeOn(Schedulers.fromExecutor(ctx.blockingTaskExecutor()))` |\n| Custom third-party executor | context-wrap it | `ctx.makeContextAware(runnable)` |\n","isInternal":false,"tokens":4702,"sizeBytes":21610},{"name":"detection-patterns.md","path":"tools/claude-plugins/assistant/skills/eventloop-audit/references/detection-patterns.md","rawUrl":"https://raw.githubusercontent.com/line/armeria/HEAD/tools/claude-plugins/assistant/skills/eventloop-audit/references/detection-patterns.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Detection Patterns — 7 Categories\n\nSearch the entire project (or scoped path) for each category of blocking pattern. Exclude test files (`**/test/**`, `**/tests/**`) and generated code (`**/generated/**`, `**/target/**`, `**/build/**`).\n\n## Category 1: Direct Blocking Calls\n\nThese are explicit operations that block the calling thread.\n\n**Search patterns:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `\\.join\\(\\)` | `CompletableFuture.join()` — blocks indefinitely |\n| `\\.get\\(\\s*\\)` | `Future.get()` with no args — blocks indefinitely |\n| `\\.get\\(\\s*\\d` | `Future.get(timeout, unit)` — bounded but still blocks |\n| `Thread\\.sleep\\(` | `Thread.sleep()` — deliberate blocking |\n| `\\.await\\(` | `CountDownLatch.await()`, `Condition.await()`, `CyclicBarrier.await()` |\n| `\\.acquire\\(` | `Semaphore.acquire()` — blocks until permit available |\n| `\\.lock\\(\\)` | `ReentrantLock.lock()` — blocks until lock available |\n| `\\.lockInterruptibly\\(\\)` | Same, interruptible variant |\n| `\\.take\\(\\)` | `BlockingQueue.take()` — blocks until element available |\n| `\\.put\\(` | `BlockingQueue.put()` — blocks if queue full (check context) |\n| `\\.wait\\(` | `Object.wait()` — monitor wait |\n\n**Triage for each hit:**\n1. Identify the enclosing method\n2. Trace callers to determine if reachable from an event loop entry point (from Phase 1)\n3. Check if the call is inside a known safe execution wrapper\n4. Classify severity (see Phase 3)\n\n## Category 2: Blocking I/O Operations\n\nI/O operations always block the calling thread. The question is whether they run on the event loop.\n\n**2A — File system operations:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `new FileInputStream` | File reading |\n| `new FileOutputStream` | File writing |\n| `new RandomAccessFile` | File random access |\n| `FileChannel\\.open` | File channel (blocking despite NIO package) |\n| `Files\\.(read\\|write\\|copy\\|move\\|walk\\|list\\|newInputStream\\|newOutputStream)` | NIO file operations (still blocking despite being in `java.nio`) |\n| `ProcessBuilder.*\\.start\\(\\)` | Process execution |\n| `Runtime\\..*exec\\(` | Process execution |\n\n**2B — Network and HTTP operations:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `\\.openConnection\\(\\)` | `URL.openConnection()` — blocking HTTP |\n| `\\.openStream\\(\\)` | `URL.openStream()` — blocking HTTP |\n| `HttpURLConnection` | Legacy blocking HTTP client |\n| `HttpClient.*\\.send\\(` | `java.net.http.HttpClient.send()` — synchronous (`.sendAsync()` is safe) |\n| `new Socket\\(` | TCP socket creation |\n| `new ServerSocket\\(` | Server socket binding |\n| `Pipe\\.open\\(\\)` | NIO pipe creation — blocking despite NIO package |\n\n**2C — Database operations:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `DriverManager\\.getConnection` | JDBC connection creation |\n| `javax\\.sql\\.DataSource` | JDBC data source |\n| `java\\.sql\\.\\(Connection\\|Statement\\|PreparedStatement\\|ResultSet\\)` | JDBC operations |\n\n**2D — Serialization and compression:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `ObjectInputStream\\|ObjectOutputStream` | Java serialization (blocking I/O on streams) |\n| `GZIPInputStream\\|GZIPOutputStream` | GZIP compression (blocking I/O) |\n| `ZipInputStream\\|ZipOutputStream` | ZIP operations (blocking I/O) |\n| `DeflaterInputStream\\|InflaterOutputStream` | Raw deflate/inflate (blocking I/O) |\n\n**2E — Encryption and security:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `KeyStore\\.load\\(` | Keystore loading from disk — blocking file I/O |\n| `SSLContext\\.getInstance` | SSL context initialization — can trigger I/O |\n| `TrustManagerFactory\\.init` | Trust manager init — may load certificates |\n\n## Category 3: Synchronization That Could Contend on Event Loop\n\nLocks are only a problem if they can contend (block waiting) on an event loop thread.\n\n**Search patterns:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `synchronized\\s*\\(` | Synchronized blocks |\n| `\\bsynchronized\\b[^\\n{;]*\\(` | Synchronized methods (modifier in method signature) |\n| `ReentrantLock` | Explicit locking |\n| `ReentrantReadWriteLock` | Read-write locking |\n| `StampedLock` | Stamped locking |\n\n**Triage:**\n- `synchronized` on startup/config-only paths → **SAFE**\n- `synchronized` on request-path data accessed from event loop → **MEDIUM to HIGH** depending on contention probability\n- `synchronized` on a monitor that is also locked by a thread pool thread → **HIGH** (event loop can wait for thread pool)\n\n## Category 4: CompletableFuture Continuations on Wrong Executor\n\nWhen you chain `.thenApply()` (without `Async`), the continuation runs on whichever thread completes the source future. If that's an event loop thread, the continuation is on the event loop.\n\n**Search patterns (non-async variants — potentially dangerous):**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `\\.thenApply\\(` | Synchronous continuation — inherits completing thread |\n| `\\.thenAccept\\(` | Same |\n| `\\.thenRun\\(` | Same |\n| `\\.thenCompose\\(` | Same |\n| `\\.whenComplete\\(` | Same |\n| `\\.handle\\(` | Same |\n| `\\.exceptionally\\(` | Same |\n\n**For each hit, determine:**\n1. What completes the source future? (event loop thread? thread pool thread?)\n2. Does the continuation body do blocking work?\n3. If both answers are problematic → needs `*Async` variant with explicit executor\n\n**Important — `*Async()` without executor is NOT always safe:**\n```regex\n\\.thenApplyAsync\\(\n\\.thenAcceptAsync\\(\n\\.thenComposeAsync\\(\n```\nThe single-argument `*Async()` variants (e.g., `.thenApplyAsync(fn)`) use `ForkJoinPool.commonPool()`, NOT the blocking task executor. This is problematic if:\n- The continuation does blocking I/O → blocks a common pool thread, starving other ForkJoinPool work\n- The project expects `ServiceRequestContext` to be available → it won't be on ForkJoinPool threads\n\nOnly the two-argument variants (e.g., `.thenApplyAsync(fn, executor)`) with an explicit executor are reliably safe. Verify that the second argument is a blocking executor, not just any executor.\n\n## Category 5: Thread Pool Misconfiguration\n\n**Search patterns:**\n\n| Pattern | Risk |\n|---------|------|\n| `Executors\\.newCachedThreadPool` | Unbounded — can exhaust memory under load |\n| `Executors\\.newSingleThreadExecutor` | Single thread — no parallelism, instant bottleneck |\n| `new ThreadPoolExecutor\\(` | Check queue type and max size — unbounded `LinkedBlockingQueue` is risky |\n| `new ForkJoinPool\\(` | Check initialization is thread-safe, check parallelism level |\n\n**Also verify:**\n- Is `blockingTaskExecutor` configured in `ServerBuilder`? (If not, Armeria uses its default 200-thread pool)\n- Is the blocking executor sized for the expected concurrent blocking operations?\n- Are custom executors properly shut down on server close?\n\n## Category 6: Missing Armeria Idioms\n\nThese are patterns where the project doesn't use Armeria's built-in mechanisms for handling blocking work.\n\n**Check for:**\n\n1. **gRPC services doing blocking work without `useBlockingTaskExecutor(true)`**\n   - Find gRPC handler methods (from Phase 1.4)\n   - Check if the method body contains I/O, database calls, or blocking waits\n   - If yes and `useBlockingTaskExecutor(true)` is not set → **CRITICAL**\n\n2. **Annotated services with blocking methods missing `@Blocking`**\n   - Find `@Get`/`@Post`/etc. methods (from Phase 1.3)\n   - Check if the method body contains blocking operations\n   - If yes and `@Blocking` is absent → **CRITICAL**\n\n3. **Custom `HttpService.serve()` that blocks**\n   - Find `HttpService` implementations (from Phase 1.2)\n   - Check if `serve()` body contains blocking operations without offloading to `ctx.blockingTaskExecutor()`\n   - If yes → **CRITICAL**\n\n4. **Cancel handlers that block**\n   ```text\n   Grep: setOnCancelHandler|whenRequestCancelling\n   ```\n   - Cancel handler lambdas run on the event loop\n   - Check if they call `.get()`, `.join()`, I/O, or any blocking operation → **CRITICAL**\n\n5. **Missing context propagation**\n   ```text\n   Grep: \\.submit\\(|\\.execute\\(|\\.schedule\\(\n   ```\n   - If work is submitted to a non-Armeria executor, check for `ctx.makeContextAware()` wrapping\n   - Missing context propagation means `ServiceRequestContext.current()` will fail in the submitted task → **MEDIUM**\n\n## Category 7: Transitive / Hidden Blocking\n\nThese are blocking operations that are easy to overlook.\n\n**Search patterns:**\n\n| Pattern | Risk |\n|---------|------|\n| `InetAddress\\.getByName` | DNS resolution — can block for seconds |\n| `InetAddress\\.getAllByName` | Same |\n| `Class\\.forName` | Class loading — can trigger I/O to read class files |\n| `ServiceLoader\\.load` | Classpath scanning — iterates JARs |\n| `System\\.getProperty` | Usually fast, but can block with SecurityManager |\n\n**Configuration-level checks:**\n- **Logging framework**: Does the project use Logback or Log4j? Check `logback.xml` / `log4j2.xml` for synchronous appenders (`FileAppender`, `ConsoleAppender` without `AsyncAppender` wrapper). Every `log.info()` on the event loop becomes blocking I/O if appenders are synchronous.\n  ```text\n  Grep: <appender|<AsyncAppender|AsyncLogger\n  Scope: **/logback*.xml, **/log4j2*.xml, **/logging*.xml\n  ```\n","isInternal":false,"tokens":2287,"sizeBytes":9300},{"name":"fix-templates.md","path":"tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md","rawUrl":"https://raw.githubusercontent.com/line/armeria/HEAD/tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/line/armeria/HEAD/tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md)","tokens":300,"sizeBytes":274},{"name":"SKILL.md","path":"tools/claude-plugins/assistant/skills/eventloop-audit/SKILL.md","rawUrl":"https://raw.githubusercontent.com/line/armeria/HEAD/tools/claude-plugins/assistant/skills/eventloop-audit/SKILL.md","title":"eventloop-audit","category":"anthropic-skill","format":"markdown","content":"---\nname: eventloop-audit\ndescription: Systematically audits any Armeria-based Java project for event loop blocking issues — discovers project patterns, scans for blocking operations, traces call chains, and produces a fix plan without modifying code. Use when experiencing latency spikes, connection timeouts, deadlocks, or thread starvation in Armeria services, or as a pre-release audit. Invoked as `/eventloop-audit` for the full project or `/eventloop-audit <path>` for a specific directory.\n---\n\n# Event Loop Blocking Audit for Armeria Projects\n\n## Overview\n\nA systematic, multi-phase audit that discovers how a project uses Armeria, scans for all categories of event loop blocking, traces call chains to determine thread context, and produces a structured report with fix plans. **No code is modified** — only analysis and planning.\n\nThis skill is **framework-generic** — it works on any Armeria-based Java project by discovering the project's patterns dynamically, not assuming specific class names or directory structures.\n\n## When to Use\n\n- After setting up a new Armeria-based project and want to verify thread safety\n- Before a release to audit for latent event loop blocking issues\n- When experiencing unexplained latency spikes, connection timeouts, or deadlocks in an Armeria server\n- After integrating a new library that may contain blocking calls\n- As a periodic health check on the codebase's non-blocking discipline\n\n## Thread Model Reference\n\nFor a comprehensive guide on Armeria's thread pools, executor selection, and programmatic usage patterns, see [armeria-thread-pool-guide.md](references/armeria-thread-pool-guide.md).\n\n**The Golden Rule**: If `Thread.currentThread() instanceof NonBlocking` — you **MUST NOT** block. Armeria's event loop threads implement the `NonBlocking` marker interface. Any blocking operation on these threads stalls ALL connections sharing that event loop, causing cascading latency and potential deadlocks.\n\n---\n\n## Phase 1: Discovery — Understand the Project's Armeria Usage\n\nBefore searching for problems, discover HOW the project uses Armeria. Launch an **Explore agent** (or do it yourself) with these specific searches. Record findings as they'll inform all subsequent phases.\n\n### 1.1 Find Armeria Server Configuration\n\n```text\nGrep: Server\\.builder\\(\\)|ServerBuilder\nScope: **/*.java\n```\n\nRead each match and document:\n- Worker group configuration (`workerGroup(n)`, `serviceWorkerGroup(n)`)\n- Blocking executor configuration (`blockingTaskExecutor(...)`)\n- Decorator chain (`.decorate(...)` calls)\n- Request/idle timeout settings\n- Any custom `EventLoopGroup` creation\n\n### 1.2 Find HttpService Implementations\n\n```text\nGrep: implements HttpService|extends AbstractHttpService|extends SimpleDecoratingHttpService\nScope: **/*.java\n```\n\nThese classes have `serve()` methods that run on the event loop. List all of them — they are the primary audit targets.\n\n### 1.3 Find Annotated Services\n\n```text\nGrep: @Get\\(|@Post\\(|@Put\\(|@Delete\\(|@Head\\(|@Patch\\(|@Options\\(\nScope: **/*.java\n```\n\nFor each, check if the method or class has `@Blocking`. Methods without `@Blocking` run on the event loop.\n\n### 1.4 Find gRPC Services\n\n```text\nGrep: extends \\w+ImplBase|GrpcService\\.builder\\(\\)\nScope: **/*.java\n```\n\nCheck whether `useBlockingTaskExecutor(true)` is configured in the `GrpcService.builder()`. If not, all gRPC handler methods run on the event loop.\n\n### 1.5 Find Thrift Services\n\n```text\nGrep: THttpService\\.builder\\(\\)|THttpService\\.of\\(\nScope: **/*.java\n```\n\nSame check — look for `useBlockingTaskExecutor(true)`.\n\n### 1.6 Find Decorators\n\n```text\nGrep: SimpleDecoratingHttpService|DecoratingHttpServiceFunction|\\.decorate\\(\nScope: **/*.java\n```\n\nEvery decorator's `serve()` method runs on the event loop. List them all.\n\n### 1.7 Find Custom Executors and Thread Pools\n\n```text\nGrep: blockingTaskExecutor\\(\\)|ctx\\.eventLoop\\(\\)|Executors\\.new|new ThreadPoolExecutor|new ForkJoinPool|ExecutorService\nScope: **/*.java\n```\n\nDocument all custom thread pools and how they're used. These are the \"safe zones\" where blocking is OK.\n\n### 1.8 Find Offloading Patterns\n\n```text\nGrep: supplyAsync|thenApplyAsync|thenComposeAsync|thenAcceptAsync|ctx\\.blockingTaskExecutor|makeContextAware\nScope: **/*.java\n```\n\nUnderstand the project's conventions for offloading work from the event loop. Look for:\n- **Base class methods** that submit work to a blocking executor (e.g., a `runBlocking(Supplier)` method in a base handler)\n- **Utility classes** that wrap `ctx.blockingTaskExecutor()` with additional logic (cancellation, metrics, tracing)\n- **Project-specific annotations** that trigger blocking executor dispatch (beyond Armeria's `@Blocking`)\n\nA wrapper is \"safe\" if it provably submits the lambda/callable to a non-event-loop executor before the lambda body executes. Read the wrapper's implementation to verify — don't trust method names alone.\n\n### 1.9 Build Architecture Map\n\nSynthesize findings into a concise map:\n- **Event loop entry points**: List of classes/methods where the event loop calls into application code\n- **Safe execution wrappers**: Project-specific methods that offload to blocking executors\n- **Thread pool inventory**: All thread pools, their sizing, and purpose\n- **Decorator chain**: The order in which decorators wrap services\n\n---\n\n## Phase 2: Systematic Detection — 7 Categories\n\nSearch the entire project (or scoped path) for each category of blocking pattern. Exclude test files (`**/test/**`, `**/tests/**`) and generated code (`**/generated/**`, `**/target/**`, `**/build/**`).\n\nSee [detection-patterns.md](references/detection-patterns.md) for the complete search patterns and triage guidance for all 7 categories:\n\n1. **Direct Blocking Calls** — `.join()`, `.get()`, `Thread.sleep()`, `.await()`, `.lock()`, etc.\n2. **Blocking I/O Operations** — File I/O, network, JDBC, serialization, encryption\n3. **Synchronization** — `synchronized`, `ReentrantLock`, `StampedLock` on request paths\n4. **CompletableFuture Continuations on Wrong Executor** — `.thenApply()` without `Async` where completing thread is event loop\n5. **Thread Pool Misconfiguration** — Unbounded pools, missing shutdown, undersized blocking executor\n6. **Missing Armeria Idioms** — Missing `@Blocking`, missing `useBlockingTaskExecutor(true)`, blocking cancel handlers, missing context propagation\n7. **Transitive / Hidden Blocking** — DNS resolution, class loading, synchronous logging appenders\n\n---\n\n## Phase 3: Call Chain Analysis\n\nFor each finding from Phase 2, perform call chain analysis to determine the thread context.\n\n### Analysis Steps\n\n1. **Identify enclosing method** — What method contains the potentially blocking call?\n\n2. **Find all callers** — Grep for the method name across the project:\n   ```text\n   Grep: methodName\\(\n   Scope: **/*.java\n   ```\n   When handling overloaded methods (same name, different signatures), read each call site to confirm it calls the blocking variant. Also check superclass/interface hierarchies — a `serve()` override in a subclass is still called from the event loop if the parent is an `HttpService`.\n\n3. **Trace to entry point** — Follow the call chain upward until you reach one of:\n   - An Armeria entry point (`serve()`, `@Get`/`@Post` handler, gRPC handler, decorator)\n   - A thread pool submission (`blockingTaskExecutor().execute(...)`, `supplyAsync(...)`, etc.)\n   - A framework callback (`whenRequestCancelling`, `setOnCancelHandler`)\n   - A lambda/method reference passed to an executor (`.submit(this::methodName)`)\n\n   **Handling ambiguity**: If a method is called from BOTH event loop paths AND thread pool paths, classify based on the worst case (event loop path). Note both paths in the finding.\n\n4. **Determine thread context** — At the entry point:\n   - Is there a `@Blocking` annotation? → Safe\n   - Is there `useBlockingTaskExecutor(true)`? → Safe\n   - Is it inside a `supplyAsync(..., blockingExecutor)` call? → Safe\n   - Is it inside a project-specific safe wrapper (discovered in Phase 1.8)? → Safe\n   - None of the above? → **Event loop**\n\n5. **Classify severity using this decision tree:**\n\n```text\nIs the blocking call provably inside a blocking executor or thread pool?\n├── YES → SAFE (exclude from report or list separately)\n├── NO, it's provably on the event loop:\n│   ├── Is it an I/O operation, .join(), .get() without timeout, or Thread.sleep()?\n│   │   └── YES → CRITICAL\n│   ├── Is it .get(timeout, unit) or .await(timeout, unit)?\n│   │   └── YES → HIGH (blocks event loop for the timeout duration)\n│   ├── Is it a synchronized block or lock?\n│   │   ├── Can it contend with a thread that holds the lock for >1ms?\n│   │   │   └── YES → HIGH\n│   │   └── Low contention, very fast critical section?\n│   │       └── LOW\n│   └── Is it a very fast operation (System.getProperty, in-memory map lookup)?\n│       └── LOW\n└── UNDETERMINED (e.g., CF continuation where completing thread varies):\n    ├── Could the continuation body block?\n    │   ├── YES → MEDIUM (must verify at runtime or refactor to be safe)\n    │   └── NO (continuation is pure computation) → LOW\n    └── Is the source future always completed by a thread pool?\n        └── YES → SAFE\n```\n\n**Key distinction**: MEDIUM means \"we cannot statically prove the thread context\" — it requires either runtime verification (add logging/assertion) or defensive refactoring (use `*Async` variant with explicit executor).\n\n---\n\n## Phase 4: Fix Plan Templates\n\nFor each non-SAFE finding, recommend one of the fix approaches documented in [fix-templates.md](references/fix-templates.md):\n\n| Template | When to use |\n|----------|-------------|\n| **FP-1** | Replace `.join()`/`.get()` with async chaining |\n| **FP-2** | Offload blocking work to `ctx.blockingTaskExecutor()` |\n| **FP-3** | Add `@Blocking` annotation to annotated service methods |\n| **FP-4** | Enable `useBlockingTaskExecutor(true)` for gRPC/Thrift services |\n| **FP-5** | Use `*Async` CF variant with explicit executor |\n| **FP-6** | Add timeout to unavoidable blocking |\n| **FP-7** | Make thread pool initialization thread-safe |\n| **FP-8** | Wire context propagation with `makeContextAware` |\n\n**Document the recommendation — do not apply it.**\n\n---\n\n## Phase 5: Produce the Report\n\n### Per-Finding Format\n\n```markdown\n## Finding [ID]: [Short descriptive title]\n\n**Severity**: CRITICAL / HIGH / MEDIUM / LOW\n**File**: [relative path]:[line number]\n**Thread context**: Event loop / Blocking executor / Undetermined\n**Detection category**: [1-7 with category name]\n\n### Description\n[What the code does and why it's a blocking risk on the event loop]\n\n### Call chain\n[Armeria entry point (event loop)] → [Method A] → [Method B] → [blocking call at line X]\n\n### Recommended fix\n[Reference FP-1 through FP-8, with specific application to this case:\n which method to change, what the new code should look like, which executor to use]\n\n### Risk assessment\n[What could break when applying the fix, edge cases to consider, tests to run]\n```\n\n### Summary Table\n\nAt the end of all findings, produce a summary:\n\n```markdown\n| # | Severity | File | Line | Category | Blocking Pattern | Fix |\n|---|----------|------|------|----------|-----------------|-----|\n| 1 | CRITICAL | FooService.java | 42 | 1 - Direct blocking | `.join()` | FP-1 |\n| 2 | HIGH | BarHandler.java | 88 | 2 - I/O | FileInputStream | FP-2 |\n| ... | ... | ... | ... | ... | ... | ... |\n```\n\n### Statistics\n\n```markdown\n- Total findings: [N]\n- CRITICAL: [N] | HIGH: [N] | MEDIUM: [N] | LOW: [N] | SAFE (excluded): [N]\n- By category: Cat.1: [N] | Cat.2: [N] | Cat.3: [N] | Cat.4: [N] | Cat.5: [N] | Cat.6: [N] | Cat.7: [N]\n```\n\n### Follow-Up Recommendations\n\nAlways include these recommendations regardless of findings:\n\n1. **Runtime detection**: Consider adding [BlockHound](https://github.com/reactor/BlockHound) to the test suite — it detects blocking calls on non-blocking threads at runtime\n2. **Async logging**: Verify that the logging framework uses async appenders in production to avoid blocking on `log.*()` calls\n3. **Event loop monitoring**: Enable Armeria's built-in event loop blocking detection:\n   ```text\n   -Dcom.linecorp.armeria.reportBlockedEventLoop=true\n   ```\n4. **Code review checklist**: Add \"Does this handler offload blocking work?\" to the team's code review checklist for Armeria services\n\n---\n\n## Execution Checklist\n\nFollow this checklist to ensure nothing is skipped. Check off each item as you complete it.\n\n### Phase 1: Discovery\n- [ ] 1.1 — Found and documented Armeria server configuration(s)\n- [ ] 1.2 — Listed all `HttpService` implementations\n- [ ] 1.3 — Listed all annotated services, noted `@Blocking` presence/absence\n- [ ] 1.4 — Listed all gRPC services, noted `useBlockingTaskExecutor` presence/absence\n- [ ] 1.5 — Listed all Thrift services (if any)\n- [ ] 1.6 — Listed all decorators\n- [ ] 1.7 — Documented all custom executors and thread pools\n- [ ] 1.8 — Identified project-specific offloading patterns\n- [ ] 1.9 — Built architecture map (entry points, safe wrappers, thread pools)\n\n### Phase 2: Detection\n- [ ] Category 1 — Searched for all direct blocking calls\n- [ ] Category 2 — Searched for all blocking I/O operations\n- [ ] Category 3 — Searched for synchronization on request paths\n- [ ] Category 4 — Audited CompletableFuture continuation executors\n- [ ] Category 5 — Audited thread pool configurations\n- [ ] Category 6 — Checked for missing Armeria idioms (@Blocking, useBlockingTaskExecutor, cancel handlers, context propagation)\n- [ ] Category 7 — Searched for transitive/hidden blocking (DNS, class loading, logging config)\n\n### Phase 3: Analysis\n- [ ] Traced call chains for ALL non-obvious findings\n- [ ] Classified every finding by severity\n- [ ] Excluded SAFE findings from the report (or listed them separately)\n\n### Phase 4: Fix Planning\n- [ ] Assigned a fix template (FP-1 through FP-8) to each non-SAFE finding\n- [ ] Documented specific fix details for each finding\n\n### Phase 5: Reporting\n- [ ] Produced per-finding reports\n- [ ] Produced summary table\n- [ ] Produced statistics\n- [ ] Included follow-up recommendations\n\n---\n\n## Common Mistakes to Avoid\n\n- **Stopping at grep hits** — A grep hit is not a finding. You must trace the call chain to determine thread context. Many `.join()` calls are inside thread pool code and are perfectly safe.\n- **Ignoring non-async CF continuations** — `.thenApply()` without `Async` is the most subtle source of event loop blocking. The continuation runs on whatever thread completes the source future.\n- **Missing cancel handlers** — Cancel handler lambdas always run on the event loop. They're easy to overlook but are a common source of blocking.\n- **Assuming `synchronized` is always bad** — It's only a problem if (a) it's on a request path reachable from the event loop AND (b) the lock can be contended by another thread holding it for a long time.\n- **Forgetting transitive blocking** — A clean `serve()` method that calls `helperMethod()` which calls `utilMethod()` which calls `.join()` is still blocking the event loop.\n- **Ignoring logging** — If the logging framework uses synchronous file appenders, every `log.info()` on the event loop is a blocking I/O operation. This is a configuration issue, not a code issue, but still critical.\n","frontmatter":{"name":"eventloop-audit","description":"Systematically audits any Armeria-based Java project for event loop blocking issues — discovers project patterns, scans for blocking operations, traces call chains, and produces a fix plan without modifying code. Use when experiencing latency spikes, connection timeouts, deadlocks, or thread starvation in Armeria services, or as a pre-release audit. Invoked as `/eventloop-audit` for the full project or `/eventloop-audit <path>` for a specific directory."},"isInternal":false,"tokens":3741,"sizeBytes":15536},{"name":"lint-release-note.sh","path":".claude/skills/release-note/scripts/lint-release-note.sh","rawUrl":"https://raw.githubusercontent.com/line/armeria/HEAD/.claude/skills/release-note/scripts/lint-release-note.sh","title":"Claude Directive: lint-release-note.sh","category":"claude-rule","format":"markdown","content":"#!/usr/bin/env bash\n# Mechanical checks for a release note. Prose rules only fire if someone remembers to apply them\n# document-wide; these run. Exits non-zero on any finding.\n#\n#   .claude/skills/release-note/scripts/lint-release-note.sh site/src/content/release-notes/1.41.0.mdx\n\nset -uo pipefail\n\nf=\"${1:?usage: lint-release-note.sh <release-note.mdx>}\"\n[ -f \"$f\" ] || { echo \"no such file: $f\" >&2; exit 2; }\n\nfail=0\n# check <description> <grep-output>; prints each matched line indented, preserving line breaks.\ncheck() {\n  [ -n \"$2\" ] || return 0\n  fail=1\n  printf '\\n✗ %s\\n' \"$1\"\n  printf '%s\\n' \"$2\" | sed 's/^/    /'\n}\n\n# --- type links -------------------------------------------------------------\n# A class link glued to a backticked method is not a link to that method.\nhits=$(grep -n '\\](type)`\\.' \"$f\" || true)\ncheck \"class link + backtick method (write [Foo#bar(Baz)](type) as one unit)\" \"$hits\"\n\n# api-index keys carry no space after commas; a space yields a dead type://# link.\nhits=$(grep -nE '\\[[A-Za-z0-9_.]+#[^]]*, [^]]*\\]\\(type\\)' \"$f\" || true)\ncheck \"space after comma in a method type link (silently dead)\" \"$hits\"\n\n# --- links ------------------------------------------------------------------\nhits=$(grep -n '](https://armeria.dev/' \"$f\" || true)\ncheck \"absolute armeria.dev link (use a site-relative /docs/... path)\" \"$hits\"\n\n# --- leftovers from the generator ------------------------------------------\nhits=$(grep -n '^- N/A' \"$f\" || true)\ncheck \"empty section left as '- N/A' (delete the section)\" \"$hits\"\n\nhits=$(grep -n 'Maybe ignore' \"$f\" || true)\ncheck \"'🗑 Maybe ignore' section must be triaged away\" \"$hits\"\n\nhits=$(grep -nE '^\\s*\\*\\s' \"$f\" || true)\ncheck \"'*' bullet (use '-')\" \"$hits\"\n\n# --- dependencies -----------------------------------------------------------\ndeps=$(sed -n '/^## ⛓ Dependencies/,/^## /p' \"$f\")\nhits=$(printf '%s' \"$deps\" | grep -n ' -> ' || true)\ncheck \"ASCII arrow in Dependencies (use →)\" \"$hits\"\n\n# Strip stable-release metadata only; -RC1/-M1/-SNAPSHOT identify distinct builds and must stay.\nhits=$(printf '%s' \"$deps\" | grep -nE '[0-9](\\.(Final|RELEASE|GA)|-GA|\\.v[0-9]{6,})' || true)\ncheck \"release qualifier left in a version (.Final/.RELEASE/-GA/.vYYYYMMDD)\" \"$hits\"\n\nhits=$(printf '%s' \"$deps\" | grep -n '^- Build' || true)\ncheck \"build-only dependency section leaked in\" \"$hits\"\n\n# --- formatting -------------------------------------------------------------\nhits=$(awk 'length>104 {printf \"%d: (%d chars)\\n\", FNR, length}' \"$f\" || true)\ncheck \"line longer than 104 chars\" \"$hits\"\n\nif [ $fail -eq 0 ]; then\n  echo \"✓ $(basename \"$f\"): all mechanical checks passed\"\nelse\n  printf '\\nSome checks failed. Note that a bad type link never fails the site build —\\n'\n  printf 'it renders as a blank type://# link, so this script is the only thing that catches it.\\n'\nfi\nexit $fail\n","isInternal":false,"tokens":800,"sizeBytes":2857},{"name":"marketplace.json","path":".claude-plugin/marketplace.json","rawUrl":"https://raw.githubusercontent.com/line/armeria/HEAD/.claude-plugin/marketplace.json","title":"armeria","category":"marketplace","format":"json","content":"{\n  \"name\": \"armeria\",\n  \"owner\": {\n    \"name\": \"armerian\",\n    \"email\": \"dl_armeria@linecorp.com\"\n  },\n  \"metadata\": {\n    \"description\": \"Claude plugin for the Armeria microservice framework\",\n    \"version\": \"0.0.1\"\n  },\n  \"plugins\": [\n    {\n      \"name\": \"assistant\",\n      \"source\": {\n        \"source\": \"git-subdir\",\n        \"url\": \"https://github.com/line/armeria.git\",\n        \"path\": \"tools/claude-plugins/assistant\"\n      }\n    }\n  ]\n}\n","frontmatter":{"name":"armeria","owner":{"name":"armerian","email":"dl_armeria@linecorp.com"},"metadata":{"description":"Claude plugin for the Armeria microservice framework","version":"0.0.1"},"plugins":[{"name":"assistant","source":{"source":"git-subdir","url":"https://github.com/line/armeria.git","path":"tools/claude-plugins/assistant"}}]},"isInternal":false,"tokens":139,"sizeBytes":444},{"name":"plugin.json","path":"tools/claude-plugins/assistant/.claude-plugin/plugin.json","rawUrl":"https://raw.githubusercontent.com/line/armeria/HEAD/tools/claude-plugins/assistant/.claude-plugin/plugin.json","title":"assistant","category":"plugin-manifest","format":"json","content":"{\n  \"name\": \"assistant\"\n}\n","frontmatter":{"name":"assistant"},"isInternal":false,"tokens":9,"sizeBytes":26}],"systemPromptSnippet":"<yakaai_skills repo=\"line/armeria\">\n<!-- File: .agents/skills/release-note/references/style-guide.md (Tokens: ~1964 | Category: anthropic-skill) -->\n# Release Notes Style Guide\n\nAnnotated examples from real Armeria release notes showing the exact formatting conventions.\n\n---\n\n## Major New Feature Entry\n\nBold title, description explaining value, `[Type](type)` links, code example with `👈👈👈`, PR refs.\n\n```mdx\n- **Standalone Athenz Token Validation**: You can now use [AthenzAuthorizer](type) to validate Athenz tokens\n  outside of Armeria's request pipeline. This allows you to easily integrate Athenz authorization into\n  third-party frameworks like Spring MVC or other servlet-based applications. #6607\n  ```java\n  ZtsBaseClient ztsBaseClient = ...;\n  AthenzAuthorizer authorizer =\n    AthenzAuthorizer.builder(ztsBaseClient)\n                    .policyConfig(new AthenzPolicyConfig(\"your-domain\"))\n                    .build();\n\n  // Validate tokens anywhere\n  AccessCheckStatus status =\n    authorizer.authorize(token, resource, action); // 👈👈👈\n  ```\n```\n\nKey elements:\n- `**Bold Title**:` prefix — only for significant features, not every entry\n- Description explains *what you can now do* and *why it matters*\n- `[AthenzAuthorizer](type)` — Armeria API type link\n- Code block indented under the bullet (2 spaces)\n- `// 👈👈👈` on the line the reader should focus on\n- `#6607` issue/PR references at end of description line (list both the issue and the PR)\n\n---\n\n## Minor New Feature Entry\n\nOne-liner without code example, for smaller additions.\n\n```mdx\n- **Per-Request Client TLS Configuration**: You can now specify [ClientTlsSpec](type) for each request using\n  [RequestOptions#clientTlsSpec()](type) or [ClientRequestContext#clientTlsSpec()](type). #6551\n```\n\nOr even simpler:\n\n```mdx\n- [ConnectionPoolListener](type) now supports ping-related events. #6539 #6552\n```\n\n---\n\n## Improvement Entry\n\nConcise description with type links. No code example unless it changes user-facing API.\n\n```mdx\n- [XdsBootstrap](type) now supports SDS. #6597 #6610 #6654\n- Improved warning logs by adding diagnostic context to frequent HTTP/2 connection errors. #6638\n```\n\n---\n\n## Bug Fix Entry\n\nDescribe the symptom that was fixed, not the internal root cause.\n\n```mdx\n- [GrpcMeterIdPrefixFunction](type) records `grpc-status` correctly for responses which fail with an exception. #6606 #6621\n- `X-Forwarded-For` header values with leading or trailing whitespace around comma-separated addresses\n  (e.g., `\"192.168.1.1 , 10.0.0.1\"`) are now trimmed and parsed correctly. #6615\n- Setting `pingIntervalMillis` to a value greater than 33 seconds no longer throws an exception. #6648\n  - Linux keepalive socket options (`SO_KEEPALIVE` `TCP_KEEPIDLE`, `TCP_KEEPINTVL`) are no longer set by default.\n```\n\nKey patterns:\n- \"[Thing] now [works correctly]\" or \"[Thing] no longer [does wrong thing]\"\n- Sub-bullets for related side effects or additional context\n- Use backticks for config names, error types, and header names\n\n---\n\n## Breaking Change Entry\n\nState what changed and what users must do to migrate.\n\n```mdx\n- A subclass of [AbstractEndpointSelector](type) must implement [AbstractEndpointSelector#doSelectNow(ClientRequestContext)](type)\n  instead of [AbstractEndpointSelector#selectNow(ClientRequestContext)](type), which is now a final method. #6535\n```\n\nFor non-trivial migrations, include before/after code.\n\n---\n\n## Documentation Entry\n\nBrief description with links to new/updated docs.\n\nLink to docs pages with a **site-relative path** (`/docs/...`), never an absolute `https://armeria.dev/...`\nURL — that is the convention across the docs and the newer release notes, and it keeps links working\non local builds and previews.\n\n```mdx\n- New comprehensive documentation on understanding and handling timeouts: #6592\n  - [Understanding timeout and cancellation origins](/docs/advanced/understanding-timeouts)\n  - Handling timeouts for streaming:\n    - [Client-side streaming](/docs/client/timeouts#handling-timeouts-for-streaming-responses)\n    - [Server-side streaming](/docs/server/timeouts#handling-timeouts-for-streaming-requests)\n```\n\n---\n\n## Dependencies Section\n\nFormat: `- LibraryName oldVersion → newVersion`, sorted alphabetically.\n\n```mdx\n- Athenz 1.12.31 → 1.12.33\n- BlockHound 1.0.15 → 1.0.16\n- Jackson 2.20.1 → 2.21.0\n- Kotlin 2.2.21 → 2.3.0\n- Logback 1.5.23 → 1.5.27\n- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3\n- Spring Boot 3.5.8 → 3.5.10, 4.0.1 → 4.0.2\n```\n\nRules:\n- Use `→` (unicode arrow), not `->` or `-->`\n- Use `-` dash bullets, not `*`\n- Use the library's common name, not Maven artifact IDs\n- Group multi-version bumps: `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n- Omit build-only dependencies (anything under the `- Build` section in the raw dependency PR)\n- Sort alphabetically (A → Z)\n- Strip trailing release qualifiers from version numbers — `.Final`, `.RELEASE`, `-GA`,\n  `.vYYYYMMDD` and the like. They add no information a reader acts on:\n  - `Netty 4.2.15.Final → 4.2.16.Final` becomes `Netty 4.2.15 → 4.2.16`\n  - `Javassist 3.31.0-GA → 3.32.0-GA` becomes `Javassist 3.31.0 → 3.32.0`\n  - `Jetty 9.4.55.v20240627 → 9.4.58.v20250814` becomes `Jetty 9.4.55 → 9.4.58`\n\n---\n\n## Thank You Section\n\n```mdx\n## 🙇 Thank you\n\n<ThankYou usernames={[\n  'contributor1',\n  'contributor2',\n  'contributor3'\n]} />\n```\n\nRules:\n- Usernames sorted alphabetically\n- Remove bot accounts: `dependabot[bot]`, `CLAassistant`\n- Core maintainers from `.github/CODEOWNERS` must always be included\n- One username per line, single-quoted, comma-separated\n\n---\n\n## What NOT to Include\n\n- **`- N/A` entries**: Remove sections that have no content instead of writing `- N/A`\n- **`🗑 Maybe ignore` section**: Must be fully triaged and removed\n- **Build-only dependencies**: `- Build` sub-section from dependency PRs\n- **Internal refactoring details**: Changes with no user-facing impact\n- **Site/docs dependency bumps**: npm bumps for `site/` or `docs-client/` (e.g., webpack, lodash)\n\n## Type Link Rules\n\nUse `[ClassName](type)` ONLY for Armeria public API references:\n- Classes/interfaces: `[GrpcServiceBuilder](type)`\n- Methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always use the full\n  `ClassName#methodName(ParamType)` form. Never use backtick-only style like\n  `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - **No space after commas** in the parameter list. The lookup key in `gen-src/api-index.json`\n    has none, so `[X#m(A, B)](type)` silently resolves to a dead `type://#` link while\n    `[X#m(A,B)](type)` works. The rendered text hides the difference — it shows `X.m()` either way.\n  - **Never split a method across a class link and a backtick.** `[Foo](type)` `` `.bar()` `` is\n    not a link to `bar()`; write `[Foo#bar(Baz)](type)` as one unit.\n- Annotations: `[@Blocking](type)`\n\nDo NOT use `[Name](type)` for:\n- JDK types (`String`, `Duration`, `CompletableFuture`, `List`)\n- Third-party types (`Mono`, `Flux`, `HttpServletRequest`)\n- Internal/private Armeria classes\n- Generic concepts (use plain text or backticks instead)\n- Method calls inside code blocks — code blocks use plain Java syntax, not `(type)` links\n\n<!-- File: .agents/skills/release-note/SKILL.md (Tokens: ~2911 | Category: anthropic-skill) -->\n---\nname: release-note\ndescription: Generate and polish release notes for an Armeria version. Runs site/release-note.ts to collect PR data from a GitHub milestone, then rewrites the skeletal output into publication-ready MDX. Invoked as `/release-note <version>` (e.g., `/release-note 1.38.0`).\n---\n\n# Release Note Generator\n\nGenerates skeletal release notes from a GitHub milestone using `site/release-note.ts`,\nthen rewrites them into polished, publication-ready MDX with rich descriptions, code examples,\nand proper formatting.\n\n## Prerequisites\n\n- The `gh` CLI must be authenticated with access to `line/armeria`. Verify with `gh auth status`.\n- A `GITHUB_ACCESS_TOKEN` environment variable is recommended for higher GitHub API rate limits.\n  If not set, the script falls back to anonymous access (lower rate limits).\n- Node.js and npm must be available. The `site/` directory must have dependencies installed\n  (`npm install` in `site/`).\n\n## Invocation\n\n```\n/release-note <version>\n```\n\nExample: `/release-note 1.38.0`\n\n---\n\n## Phase 0: Generate Skeletal Release Notes\n\n1. Verify the GitHub milestone exists for the given version by checking:\n   ```\n   gh api repos/line/armeria/milestones --jq '.[] | select(.title == \"<version>\") | .number'\n   ```\n2. Run the release note generation script:\n   ```\n   cd site && npm run release-note <version>\n   ```\n3. Verify the output file was created at `site/src/content/release-notes/<version>.mdx`.\n4. If the script fails (e.g., milestone not found, network error), report the error and stop.\n\n## Phase 1: Load Draft and Study Style\n\n1. Read the generated draft file at `site/src/content/release-notes/<version>.mdx`.\n2. Read 3-4 recent polished release notes (e.g., `1.36.0.mdx`, `1.37.0.mdx`) to calibrate tone and style.\n3. Read the style guide at `references/style-guide.md` for formatting rules.\n4. Extract all PR/issue references (`#NNNN`) from every line of the draft.\n\n## Phase 2: Gather PR Context from GitHub\n\nFor each unique PR number found in the draft:\n\n1. Fetch PR details:\n   ```\n   gh pr view <number> --repo line/armeria --json title,body,labels,files\n   ```\n2. Parse the PR body to extract **Motivation**, **Modifications**, and **Result** sections.\n3. Fetch PR review comments — these often contain important design decisions, caveats, and\n   scope limitations (e.g., \"this only applies to unary calls\") that are not in the PR description:\n   ```\n   gh api repos/line/armeria/pulls/<number>/comments --jq '.[].body'\n   ```\n4. Extract linked issue numbers from the body (`Closes #NNNN`, `Fixes #NNNN`, `Resolves #NNNN`).\n5. For each linked issue, fetch its context **including comments**, which often contain use cases,\n   edge cases, and design discussions that inform the release note description:\n   ```\n   gh issue view <number> --repo line/armeria --json title,body\n   gh api repos/line/armeria/issues/<number>/comments --jq '.[].body'\n   ```\n6. For PRs in the \"New features\" section that introduce significant new API:\n   - Read key changed source files from the PR's `files` list to understand method signatures.\n   - Look for usage examples in the PR description's Result section first — prefer these over\n     constructing examples from scratch.\n   - Check review comments for scope limitations, caveats, or known constraints that should be\n     mentioned in the release note (e.g., \"only supports unary methods\", \"HTTP/2 only\").\n\n**Rate limiting**: If fetching many PRs, batch requests and pause briefly between them to avoid\nGitHub API rate limits.\n\n## Phase 3: Triage \"Maybe Ignore\" Section\n\nThe script puts PRs without a recognized label into `🗑 Maybe ignore`. For each entry:\n\n1. Check the PR's labels and description.\n2. **Drop** items that are purely internal (CI config, build scripts, test infrastructure,\n   non-user-facing refactoring, site/docs dependency bumps).\n3. **Relocate** user-facing items to the correct section based on their actual impact:\n   - API additions → New features\n   - Performance or usability improvements → Improvements\n   - Bug fixes → Bug fixes\n   - Breaking API changes → Breaking changes\n4. Report triage decisions to the user so they can override if needed.\n\n## Phase 4: Rewrite Each Section\n\nRewrite every entry following the formatting rules in `references/style-guide.md`.\n\n### Ordering\n\n1. **Lead with user interest**: Place the top 3 entries that users would care about most first.\n   Prioritize broadly applicable features (core, gRPC, Kubernetes) over niche modules (Athenz, xDS).\n   Consider the size of the user base affected and how common the use case is.\n2. **Then group by module**: After the top 3, group remaining entries by module/area so that\n   related changes appear together (e.g., Athenz entries adjacent, Kubernetes entries adjacent).\n3. This applies to all sections (New features, Improvements, Bug fixes, etc.).\n\n### Key principles:\n\n### New Features (`🌟 New features`)\n\n- All new feature entries get a bold title prefix: `- **Feature Title**: Description. #NNNN`\n- **Keep descriptions concise** — most entries should fit within 3 lines of prose.\n  Only high-impact features (e.g., a brand-new module or paradigm-shifting API) warrant\n  longer descriptions.\n- Include a Java code example (5-15 lines) whenever possible. It may be omitted if there is\n  no clear usage pattern to show.\n  - Mark the most important line with `// 👈👈👈`\n  - Indent code blocks with 2 spaces under the bullet.\n\n### Improvements (`📈 Improvements`)\n\n- Concise description of what improved and why it matters.\n- Code examples only if the improvement changes how users interact with an API.\n\n### Bug Fixes (`🛠️ Bug fixes`)\n\n- Describe the symptom that was fixed, not the internal cause.\n- Format: \"[What was broken] now [works correctly]. #NNNN\"\n\n### Breaking Changes (`☢️ Breaking changes`)\n\n- State clearly what changed and what users must do to migrate.\n- Include before/after code if the migration is non-trivial.\n\n### Documentation (`📃 Documentation`)\n\n- Brief description with links to the new/updated docs if available.\n\n### Deprecations (`🏚️ Deprecations`)\n\n- State what is deprecated and what to use instead.\n\n### All Sections — Common Rules\n\n- Use `[ClassName](type)` for Armeria API types (classes, interfaces, annotations, methods).\n  - For classes/interfaces: `[GrpcServiceBuilder](type)`\n  - For methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always include the\n    class name, method name, and parameter types. Do NOT use backtick-only style like\n    `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - Do NOT use this syntax for JDK types (`String`, `Duration`, `CompletableFuture`), third-party\n    types, or types that are not part of Armeria's public API.\n- Issue and PR references go at the end of the entry: `#6431 #6691` (list both the issue and the PR)\n- Do NOT copy PR titles verbatim — they are often terse commit-style messages.\n- Do NOT fabricate code examples. Derive them from PR descriptions or actual source code.\n- Keep entries self-contained — a reader should understand the change without clicking the PR link.\n\n## Phase 5: Clean Up Dependencies Section\n\nThe raw script includes the full dependency update PR body, which uses a structured commit message format.\n\n1. **Strip build-only dependencies**: Remove the `- Build` section and all its sub-bullets\n   (these are testImplementation, annotationProcessor, and other non-production deps).\n2. **Format each entry**: `- LibraryName oldVersion → newVersion`\n   - Use the library's common name (e.g., `Jackson`, `Netty`, `gRPC-Java`, not the Maven artifact ID).\n   - Use `→` (unicode arrow), not `->`.\n3. **Group multi-version bumps** on one line when a library has multiple version streams:\n   `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n4. **Sort alphabetically** (A → Z).\n\n## Optional: Validate with a Site Build\n\n`cd site && npm run build` validates the MDX end to end, but requires generated files under\n`site/gen-src/` (`api-index.json`, `versions.json`, `contributors.json`):\n\n- Full generation: `./gradlew :site:generateSiteSources` (api-index needs the aggregated Javadoc,\n  which is slow).\n- Quick validation: copy `api-index.json`/`versions.json` from a previous build and generate\n  `contributors.json` with\n  `gh api --paginate repos/line/armeria/contributors | jq -s '[.[][]] | map({(.login): .avatar_url}) | add'`.\n  A stale `api-index.json` is fine for validation — unknown `[X](type)` names render as blank\n  links (`type://#`) and never fail the build, so type names must be verified against the source\n  either way.\n- `npm run build | tail` masks the exit code — capture it separately.\n\n## Phase 6: Finalize\n\n1. **Remove empty sections**: Delete any section whose only content is `- N/A`.\n2. **Remove \"Maybe ignore\"**: The `🗑 Maybe ignore` section must not appear in the final output.\n3. **Deduplicate and sort contributors**: Ensure `<ThankYou usernames={[...]} />` has\n   alphabetically sorted, deduplicated usernames. Remove bot accounts (`dependabot[bot]`,\n   `CLAassistant`). Core maintainers listed in `.github/CODEOWNERS` must always be included\n   in the Thank You section, even if they don't appear in the PR participant lists.\n4. **Ensure consistent bullet style**: Use `-` (dash) for all bullets, not `*`.\n5. **Write the final file** to `site/src/content/release-notes/<version>.mdx`.\n6. **Show a summary** to the user: list the sections, entry count per section, and any entries\n   flagged as uncertain (where PR context was insufficient to write a confident description).\n\n---\n\n## Phase 7: Lint\n\nRun the mechanical checks and fix everything they report:\n\n```\n.agents/skills/release-note/scripts/lint-release-note.sh site/src/content/release-notes/<version>.mdx\n```\n\nThis is not optional, and reading the style guide is not a substitute for running it. The rules it\nenforces are all written down elsewhere in this skill and have still been broken while the author\nhad them open — a malformed `(type)` link never fails the site build, it just renders as a blank\n`type://#` link, so nothing else catches it.\n\nThe script cannot see two things, so check them by hand:\n\n- **Identifiers copied from a PR body** — metric names, class names, proto fields. PR descriptions\n  go stale; the source does not. Grep the repo for every identifier you quote. (A real case: PR\n  #6840's body advertised `armeria.xds.lb.request`, while the code registered `lb.select`.)\n- **Whether a claim is true**, as opposed to well-formed. Read the code the entry describes.\n\n## Execution Checklist\n\n- [ ] Phase 0 — Ran `npm run release-note <version>` and verified output file exists\n- [ ] Phase 1 — Read draft, recent examples, and style guide\n- [ ] Phase 2 — Fetched PR/issue context for all referenced PRs\n- [ ] Phase 3 — Triaged all \"Maybe ignore\" entries\n- [ ] Phase 4 — Rewrote all entries per style guide\n- [ ] Phase 5 — Cleaned up dependencies section\n- [ ] Phase 6 — Removed empty sections, finalized file, reported summary\n- [ ] Phase 7 — Ran `lint-release-note.sh` and it exited 0\n\n## Common Mistakes to Avoid\n\n- **Copying PR titles as-is**: PR titles like \"Fix NPE in FooBar\" are not user-friendly.\n  Rewrite as \"Fixed a `NullPointerException` in [FooBar](type) when ...\"\n- **Fabricating code examples**: If you cannot find a clear usage pattern from the PR description\n  or source code, write a descriptive sentence instead of guessing at code.\n- **Over-linking types**: Only use `[Name](type)` for Armeria's own public API types, not for\n  JDK classes, third-party libraries, or internal classes.\n- **Including build dependencies**: The dependency update PR body contains `- Build` sub-bullets\n  for test/build-only deps. These must be stripped.\n- **Leaving `- N/A` sections**: The polished output should only contain sections with actual content.\n- **Using `*` bullets**: Standardize on `-` dashes for all bullet points.\n- **Missing `👈👈👈` callouts**: Every code example for a new feature should highlight the key line.\n\n<!-- File: .agents/skills/upgrade-deps/SKILL.md (Tokens: ~2176 | Category: anthropic-skill) -->\n---\nname: upgrade-deps\ndescription: Upgrade Gradle dependencies and Gradle wrapper version\ndisable-model-invocation: true\n---\n\n# Dependency Upgrade Workflow\n\nThis skill performs a full Gradle dependency upgrade for the Armeria project step by step.\n\n## Background: Minimum Java Version Per Module\n\nArmeria is a multi-module project with different minimum Java version requirements.\nBefore upgrading a dependency, always verify that the new version supports the minimum\nJava version required by the modules that use it.\n\n| Java Version | Modules |\n|-------------|---------|\n| **8**  | `core`, `brave5`, `brave6`, `eureka`, `grpc`, `grpc-kotlin`, `graphql-protocol`, `jetty9`, `junit4`, `junit5`, `kafka`, `kotlin`, `logback`, `logback12`, `logback13`, `micrometer-context`, `oauth2`, `prometheus1`, `protobuf`, `reactor3`, `resteasy`, `retrofit2`, `rxjava2`, `rxjava3`, `sangria`, `scala*`, `spring:boot2-*`, `dropwizard2`, `thrift0.9`–`thrift0.17`, `tomcat8`, `tomcat9`, `xds`, `zookeeper3`, `saml`, `bucket4j`, `consul`, `nacos` |\n| **11** | `athenz`, `graphql`, `jetty10`, `jetty11`, `kubernetes`, `logback14`, `thrift0.18`–`thrift0.22`, `tomcat10` |\n| **17** | `ai:mcp`, `jetty12`, `resilience4j2`, `spring:boot3-*`, `spring:spring6`, `spring:boot4-*`, `spring:spring7` |\n\nWhen in doubt, check `settings.gradle` for the `java`, `java11`, or `java17` flag on each subproject.\n\n**The table above lists published modules only.** Integration-test (`:it:*`) and example (`:examples:*`)\nmodules carry their own flags in `settings.gradle` and are frequently `java17` already. A dependency\nconsumed *only* by those modules is not held back by the Java 8 baseline at all — always resolve the\nactual consumer before assuming a Java constraint applies.\n\n## Step 1: Run dependencyUpdates\n\n```\n./gradlew dependencyUpdates --no-parallel\n```\n\nThe report is saved to `build/dependencyUpdates/report.txt`.\n\n## Step 2: Parse the Report\n\nRead `build/dependencyUpdates/report.txt` to identify all outdated dependencies.\nFor each dependency, note the current version and the available latest version.\n\nIf a version looks ambiguous (unexpected format, major version bump, unusual naming),\nverify on Maven Central before proceeding:\n  https://central.sonatype.com/artifact/{groupId}/{artifactId}\n\n## Step 3: Check Skip Hints and Java Version Compatibility\n\nBefore upgrading any dependency, check `dependencies.toml` for a comment directly above\nits version entry. Many pinned versions have an explicit reason, for example:\n\n```toml\n# Don't upgrade Caffeine to 3.x that requires Java 11.\ncaffeine = \"2.9.3\"\n\n# Upgrade once https://github.com/ronmamo/reflections/issues/279 is fixed.\nreflections = \"0.9.11\"\n\n# Ensure that we use the same ZooKeeper version as what Curator depends on.\nzookeeper = \"3.9.3\"\n```\n\n### Step 3.1: Validate the comment before trusting it\n\nA hold comment is a claim about this repository, and claims go stale. **Never treat a comment as\nproof — re-derive its reason every upgrade.** For each held entry:\n\n1. Find the aliases for the version key in the `[libraries.*]` / `[plugins]` sections.\n2. Grep the build scripts for each alias accessor (`libs.foo.bar` for alias `foo-bar`):\n   `git grep -n 'libs\\.foo\\.bar' -- '*.gradle' '*.gradle.kts'`\n3. Map each hit's build file to its project, and read that project's flags in `settings.gradle`.\n4. Compare the *consumer's* Java level against the new version's requirement — **not** the library's\n   requirement in isolation. \"Library X requires Java 17\" is not a blocker when every consumer is\n   already `java17`.\n\nIf the reason no longer holds, **remove the comment and upgrade**. If it still holds, keep both.\n\nTwo comments were found stale this way (`dgs` and `graphql-kotlin`, both consumed only by `java17`\nmodules), so treat this step as mandatory rather than a spot check.\n\n### Step 3.2: Never write a hold reason you have not verified\n\nWhen you add a comment, the reason must be something you confirmed, with the evidence to hand:\n\n- **Java constraint** — confirmed against the consumer module's flag, per Step 3.1.\n- **Runtime/API breakage** — confirmed by actually building and running the affected module's tests,\n  and the comment names the concrete blocker (e.g. `# Don't upgrade graphql-kotlin to 10.x that\n  migrated to Jackson 3`), not a vague \"major upgrade deferred\".\n- **Version-sync constraint** — confirmed from the other project's POM *and* from this repo's history\n  (`git log -p -- dependencies.toml`). Do not invent a sync rule: check whether the two versions have\n  ever actually moved independently before claiming they must move together.\n\nA plausible-sounding reason that nobody verified is worse than no comment, because the next upgrade\nwill trust it.\n\nIf no comment exists, proceed with the Java version compatibility check:\n1. Find which modules use this dependency in `dependencies.toml`\n2. Determine the minimum Java version required by those modules (see table above)\n3. Check the new library version's minimum Java requirement (Maven Central / release notes)\n4. If the library's required Java > module's minimum Java → **skip the upgrade** and add a\n   comment above the version entry explaining why (e.g. `# X.Y requires Java 11`)\n5. If compatible → proceed with the upgrade\n\n## Step 4: Update dependencies.toml\n\nEdit the `[versions]` section in `dependencies.toml` to update the version strings.\nThe file is located at the root of the repository: `dependencies.toml`.\n\n## Step 5: Cross-check Updated Versions\n\nAfter editing `dependencies.toml`, re-read the full diff (`git diff dependencies.toml`) and\ncross-check every changed entry against the `dependencyUpdates` report:\n\n1. **No entry was accidentally skipped** — compare the report's \"The following dependencies\n   have later milestone versions\" list against your edits. If a dependency appears in the\n   report but not in the diff, either upgrade it or document why it was skipped.\n2. **Versions match the report** — confirm each new version in the diff matches the latest\n   version from the report, not a typo or intermediate version.\n3. **Pinned-version comments were respected** — verify you did not upgrade a dependency whose\n   comment says to skip it, and that you did not remove or contradict an existing comment.\n4. **Linked-version constraints are satisfied** — some dependencies must stay in sync with\n   others (e.g. ZooKeeper with Curator, Protobuf with gRPC). Read the comments above those\n   entries and verify the constraint still holds after the upgrade.\n\n## Step 6: Upgrade Gradle Wrapper\n\nCheck the latest stable Gradle release at https://gradle.org/releases/ and update\n`gradle/wrapper/gradle-wrapper.properties`:\n\n```properties\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-X.Y.Z-all.zip\n```\n\n## Step 7: Verify the Build\n\nRun the full build including tests to catch both compilation errors and runtime regressions:\n\n```\n./gradlew build --no-daemon\n```\n\nIf there are compilation errors, API breaking changes, or test failures caused by the upgrade,\nfix them before proceeding.\n\n## Step 8: Commit\n\nCreate a commit with the **exact** message format below. Follow it strictly — do not add extra\nsections, reorder bullets, or change the structure:\n\n```\nUpdate dependencies\n\n- {library-name} {old-version} -> {new-version}\n- {library-name} {old-version} -> {new-version}\n- Build\n   - {library-name} {old-version} -> {new-version}\n   - {library-name} {old-version} -> {new-version}\n```\n\nRules:\n- Each production dependency (api/implementation scope) gets one bullet: `- {name} {old} -> {new}`\n- The `- Build` bullet groups build-only dependencies (testImplementation, annotationProcessor,\n  relocated libs, non-transitive deps); nest them as sub-bullets with 3-space indent\n- Use the library's official name if one exists (e.g. `gRPC-Java`, `Jackson`, `Netty`, `Kotlin`,\n  `Reactor`, `Logback`, `Micrometer`); otherwise use the key name as-is from `dependencies.toml`\n- Sort entries **alphabetically (A → Z)** within each section\n- If no build-only deps were upgraded, omit the `- Build` section entirely\n- If any dependencies were **not upgraded** (due to Java version constraints, pinned-version\n  comments, or linked-version constraints), list them under a `- Unupdated` section with the\n  reason. This helps reviewers know which upgrades were intentionally skipped.\n\nExample:\n```\nUpdate dependencies\n\n- gRPC-Java 1.63.0 -> 1.64.0\n- Jackson 2.17.0 -> 2.18.0\n- Build\n   - checkstyle 10.14.0 -> 10.17.0\n   - ErrorProne 2.27.0 -> 2.28.0\n- Unupdated\n   - Caffeine 2.9.3 -> 3.2.0 (requires Java 11)\n   - ZooKeeper 3.9.3 (pinned to Curator's version)\n```\n\n<!-- File: .claude/skills/release-note/references/style-guide.md (Tokens: ~2097 | Category: anthropic-skill) -->\n# Release Notes Style Guide\n\nAnnotated examples from real Armeria release notes showing the exact formatting conventions.\n\n---\n\n## Major New Feature Entry\n\nBold title, description explaining value, `[Type](type)` links, code example with `👈👈👈`, PR refs.\n\n```mdx\n- **Standalone Athenz Token Validation**: You can now use [AthenzAuthorizer](type) to validate Athenz tokens\n  outside of Armeria's request pipeline. This allows you to easily integrate Athenz authorization into\n  third-party frameworks like Spring MVC or other servlet-based applications. #6607\n  ```java\n  ZtsBaseClient ztsBaseClient = ...;\n  AthenzAuthorizer authorizer =\n    AthenzAuthorizer.builder(ztsBaseClient)\n                    .policyConfig(new AthenzPolicyConfig(\"your-domain\"))\n                    .build();\n\n  // Validate tokens anywhere\n  AccessCheckStatus status =\n    authorizer.authorize(token, resource, action); // 👈👈👈\n  ```\n```\n\nKey elements:\n- `**Bold Title**:` prefix — only for significant features, not every entry\n- Description explains *what you can now do* and *why it matters*\n- `[AthenzAuthorizer](type)` — Armeria API type link\n- Code block indented under the bullet (2 spaces)\n- `// 👈👈👈` on the line the reader should focus on\n- `#6607` issue/PR references at end of description line (list both the issue and the PR)\n\n---\n\n## Minor New Feature Entry\n\nOne-liner without code example, for smaller additions.\n\n```mdx\n- **Per-Request Client TLS Configuration**: You can now specify [ClientTlsSpec](type) for each request using\n  [RequestOptions#clientTlsSpec()](type) or [ClientRequestContext#clientTlsSpec()](type). #6551\n```\n\nOr even simpler:\n\n```mdx\n- [ConnectionPoolListener](type) now supports ping-related events. #6539 #6552\n```\n\n---\n\n## Improvement Entry\n\nConcise description with type links. No code example unless it changes user-facing API.\n\n```mdx\n- [XdsBootstrap](type) now supports SDS. #6597 #6610 #6654\n- Improved warning logs by adding diagnostic context to frequent HTTP/2 connection errors. #6638\n```\n\n---\n\n## Bug Fix Entry\n\nDescribe the symptom that was fixed, not the internal root cause.\n\n```mdx\n- [GrpcMeterIdPrefixFunction](type) records `grpc-status` correctly for responses which fail with an exception. #6606 #6621\n- `X-Forwarded-For` header values with leading or trailing whitespace around comma-separated addresses\n  (e.g., `\"192.168.1.1 , 10.0.0.1\"`) are now trimmed and parsed correctly. #6615\n- Setting `pingIntervalMillis` to a value greater than 33 seconds no longer throws an exception. #6648\n  - Linux keepalive socket options (`SO_KEEPALIVE` `TCP_KEEPIDLE`, `TCP_KEEPINTVL`) are no longer set by default.\n```\n\nKey patterns:\n- \"[Thing] now [works correctly]\" or \"[Thing] no longer [does wrong thing]\"\n- Sub-bullets for related side effects or additional context\n- Use backticks for config names, error types, and header names\n\n---\n\n## Breaking Change Entry\n\nState what changed and what users must do to migrate.\n\n```mdx\n- A subclass of [AbstractEndpointSelector](type) must implement [AbstractEndpointSelector#doSelectNow(ClientRequestContext)](type)\n  instead of [AbstractEndpointSelector#selectNow(ClientRequestContext)](type), which is now a final method. #6535\n```\n\nFor non-trivial migrations, include before/after code.\n\n---\n\n## Documentation Entry\n\nBrief description with links to new/updated docs.\n\nLink to docs pages with a **site-relative path** (`/docs/...`), never an absolute `https://armeria.dev/...`\nURL — that is the convention across the docs and the newer release notes, and it keeps links working\non local builds and previews.\n\n```mdx\n- New comprehensive documentation on understanding and handling timeouts: #6592\n  - [Understanding timeout and cancellation origins](/docs/advanced/understanding-timeouts)\n  - Handling timeouts for streaming:\n    - [Client-side streaming](/docs/client/timeouts#handling-timeouts-for-streaming-responses)\n    - [Server-side streaming](/docs/server/timeouts#handling-timeouts-for-streaming-requests)\n```\n\n---\n\n## Dependencies Section\n\nFormat: `- LibraryName oldVersion → newVersion`, sorted alphabetically.\n\n```mdx\n- Athenz 1.12.31 → 1.12.33\n- BlockHound 1.0.15 → 1.0.16\n- Jackson 2.20.1 → 2.21.0\n- Kotlin 2.2.21 → 2.3.0\n- Logback 1.5.23 → 1.5.27\n- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3\n- Spring Boot 3.5.8 → 3.5.10, 4.0.1 → 4.0.2\n```\n\nRules:\n- Use `→` (unicode arrow), not `->` or `-->`\n- Use `-` dash bullets, not `*`\n- Use the library's common name, not Maven artifact IDs\n- Group multi-version bumps: `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n- Omit build-only dependencies (anything under the `- Build` section in the raw dependency PR)\n- Omit **test-only** dependencies (e.g., Apache HttpClient, OkHttp, DGS) — only used in\n  `testImplementation` and not in published POMs\n- Omit **shaded** dependencies (e.g., Guava, Bouncy Castle, JCTools) — relocated into Armeria\n  JARs and invisible to users\n- Omit dependencies that are **not transitive** in any published artifact (e.g., Jsoup)\n- When unsure, check `dependencies.toml` for scope and whether the dep appears in a published\n  module's `api` or `implementation` configuration\n- Sort alphabetically (A → Z)\n- Strip trailing release qualifiers from version numbers — `.Final`, `.RELEASE`, `-GA`,\n  `.vYYYYMMDD` and the like. They add no information a reader acts on:\n  - `Netty 4.2.15.Final → 4.2.16.Final` becomes `Netty 4.2.15 → 4.2.16`\n  - `Javassist 3.31.0-GA → 3.32.0-GA` becomes `Javassist 3.31.0 → 3.32.0`\n  - `Jetty 9.4.55.v20240627 → 9.4.58.v20250814` becomes `Jetty 9.4.55 → 9.4.58`\n\n---\n\n## Thank You Section\n\n```mdx\n## 🙇 Thank you\n\n<ThankYou usernames={[\n  'contributor1',\n  'contributor2',\n  'contributor3'\n]} />\n```\n\nRules:\n- Usernames sorted alphabetically\n- Remove bot accounts: `dependabot[bot]`, `CLAassistant`\n- Core maintainers from `.github/CODEOWNERS` must always be included\n- One username per line, single-quoted, comma-separated\n\n---\n\n## What NOT to Include\n\n- **`- N/A` entries**: Remove sections that have no content instead of writing `- N/A`\n- **`🗑 Maybe ignore` section**: Must be fully triaged and removed\n- **Build-only dependencies**: `- Build` sub-section from dependency PRs\n- **Internal refactoring details**: Changes with no user-facing impact\n- **Site/docs dependency bumps**: npm bumps for `site/` or `docs-client/` (e.g., webpack, lodash)\n\n## Type Link Rules\n\nUse `[ClassName](type)` ONLY for Armeria public API references:\n- Classes/interfaces: `[GrpcServiceBuilder](type)`\n- Methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always use the full\n  `ClassName#methodName(ParamType)` form. Never use backtick-only style like\n  `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - **No space after commas** in the parameter list. The lookup key in `gen-src/api-index.json`\n    has none, so `[X#m(A, B)](type)` silently resolves to a dead `type://#` link while\n    `[X#m(A,B)](type)` works. The rendered text hides the difference — it shows `X.m()` either way.\n  - **Never split a method across a class link and a backtick.** `[Foo](type)` `` `.bar()` `` is\n    not a link to `bar()`; write `[Foo#bar(Baz)](type)` as one unit.\n- Annotations: `[@Blocking](type)`\n\nDo NOT use `[Name](type)` for:\n- JDK types (`String`, `Duration`, `CompletableFuture`, `List`)\n- Third-party types (`Mono`, `Flux`, `HttpServletRequest`)\n- Internal/private Armeria classes\n- Generic concepts (use plain text or backticks instead)\n- Method calls inside code blocks — code blocks use plain Java syntax, not `(type)` links\n\n<!-- File: .claude/skills/release-note/SKILL.md (Tokens: ~3076 | Category: anthropic-skill) -->\n---\nname: release-note\ndescription: Generate and polish release notes for an Armeria version. Runs site/release-note.ts to collect PR data from a GitHub milestone, then rewrites the skeletal output into publication-ready MDX. Invoked as `/release-note <version>` (e.g., `/release-note 1.38.0`).\n---\n\n# Release Note Generator\n\nGenerates skeletal release notes from a GitHub milestone using `site/release-note.ts`,\nthen rewrites them into polished, publication-ready MDX with rich descriptions, code examples,\nand proper formatting.\n\n## Prerequisites\n\n- The `gh` CLI must be authenticated with access to `line/armeria`. Verify with `gh auth status`.\n- A `GITHUB_ACCESS_TOKEN` environment variable is recommended for higher GitHub API rate limits.\n  If not set, the script falls back to anonymous access (lower rate limits).\n- Node.js and npm must be available. The `site/` directory must have dependencies installed\n  (`npm install` in `site/`).\n\n## Invocation\n\n```\n/release-note <version>\n```\n\nExample: `/release-note 1.38.0`\n\n---\n\n## Phase 0: Generate Skeletal Release Notes\n\n1. Verify the GitHub milestone exists for the given version by checking:\n   ```\n   gh api repos/line/armeria/milestones --jq '.[] | select(.title == \"<version>\") | .number'\n   ```\n2. Run the release note generation script:\n   ```\n   cd site && npm run release-note <version>\n   ```\n3. Verify the output file was created at `site/src/content/release-notes/<version>.mdx`.\n4. If the script fails (e.g., milestone not found, network error), report the error and stop.\n\n## Phase 1: Load Draft and Study Style\n\n1. Read the generated draft file at `site/src/content/release-notes/<version>.mdx`.\n2. Read 3-4 recent polished release notes (e.g., `1.36.0.mdx`, `1.37.0.mdx`) to calibrate tone and style.\n3. Read the style guide at `references/style-guide.md` for formatting rules.\n4. Extract all PR/issue references (`#NNNN`) from every line of the draft.\n\n## Phase 2: Gather PR Context from GitHub\n\nFor each unique PR number found in the draft:\n\n1. Fetch PR details:\n   ```\n   gh pr view <number> --repo line/armeria --json title,body,labels,files\n   ```\n2. Parse the PR body to extract **Motivation**, **Modifications**, and **Result** sections.\n3. Fetch PR review comments — these often contain important design decisions, caveats, and\n   scope limitations (e.g., \"this only applies to unary calls\") that are not in the PR description:\n   ```\n   gh api repos/line/armeria/pulls/<number>/comments --jq '.[].body'\n   ```\n4. Extract linked issue numbers from the body (`Closes #NNNN`, `Fixes #NNNN`, `Resolves #NNNN`).\n5. For each linked issue, fetch its context **including comments**, which often contain use cases,\n   edge cases, and design discussions that inform the release note description:\n   ```\n   gh issue view <number> --repo line/armeria --json title,body\n   gh api repos/line/armeria/issues/<number>/comments --jq '.[].body'\n   ```\n6. For PRs in the \"New features\" section that introduce significant new API:\n   - Read key changed source files from the PR's `files` list to understand method signatures.\n   - Look for usage examples in the PR description's Result section first — prefer these over\n     constructing examples from scratch.\n   - Check review comments for scope limitations, caveats, or known constraints that should be\n     mentioned in the release note (e.g., \"only supports unary methods\", \"HTTP/2 only\").\n\n**Rate limiting**: If fetching many PRs, batch requests and pause briefly between them to avoid\nGitHub API rate limits.\n\n## Phase 3: Triage \"Maybe Ignore\" Section\n\nThe script puts PRs without a recognized label into `🗑 Maybe ignore`. For each entry:\n\n1. Check the PR's labels and description.\n2. **Drop** items that are purely internal (CI config, build scripts, test infrastructure,\n   non-user-facing refactoring, site/docs dependency bumps).\n3. **Relocate** user-facing items to the correct section based on their actual impact:\n   - API additions → New features\n   - Performance or usability improvements → Improvements\n   - Bug fixes → Bug fixes\n   - Breaking API changes → Breaking changes\n4. Report triage decisions to the user so they can override if needed.\n\n## Phase 4: Rewrite Each Section\n\nRewrite every entry following the formatting rules in `references/style-guide.md`.\n\n### Ordering\n\n1. **Lead with user interest**: Place the top 3 entries that users would care about most first.\n   Prioritize broadly applicable features (core, gRPC, Kubernetes) over niche modules (Athenz, xDS).\n   Consider the size of the user base affected and how common the use case is.\n2. **Then group by module**: After the top 3, group remaining entries by module/area so that\n   related changes appear together (e.g., Athenz entries adjacent, Kubernetes entries adjacent).\n3. This applies to all sections (New features, Improvements, Bug fixes, etc.).\n\n### Key principles:\n\n### New Features (`🌟 New features`)\n\n- All new feature entries get a bold title prefix: `- **Feature Title**: Description. #NNNN`\n- **Keep descriptions concise** — most entries should fit within 3 lines of prose.\n  Only high-impact features (e.g., a brand-new module or paradigm-shifting API) warrant\n  longer descriptions.\n- Include a Java code example (5-15 lines) whenever possible. It may be omitted if there is\n  no clear usage pattern to show.\n  - Mark the most important line with `// 👈👈👈`\n  - Indent code blocks with 2 spaces under the bullet.\n\n### Improvements (`📈 Improvements`)\n\n- Concise description of what improved and why it matters.\n- Code examples only if the improvement changes how users interact with an API.\n\n### Bug Fixes (`🛠️ Bug fixes`)\n\n- Describe the symptom that was fixed, not the internal cause.\n- Format: \"[What was broken] now [works correctly]. #NNNN\"\n\n### Breaking Changes (`☢️ Breaking changes`)\n\n- State clearly what changed and what users must do to migrate.\n- Include before/after code if the migration is non-trivial.\n\n### Documentation (`📃 Documentation`)\n\n- Brief description with links to the new/updated docs if available.\n\n### Deprecations (`🏚️ Deprecations`)\n\n- State what is deprecated and what to use instead.\n\n### All Sections — Common Rules\n\n- Use `[ClassName](type)` for Armeria API types (classes, interfaces, annotations, methods).\n  - For classes/interfaces: `[GrpcServiceBuilder](type)`\n  - For methods: `[GrpcServiceBuilder#enableEnvoyHttp1Bridge(boolean)](type)` — always include the\n    class name, method name, and parameter types. Do NOT use backtick-only style like\n    `` `enableEnvoyHttp1Bridge(true)` `` for Armeria public API references in prose.\n  - Do NOT use this syntax for JDK types (`String`, `Duration`, `CompletableFuture`), third-party\n    types, or types that are not part of Armeria's public API.\n- Issue and PR references go at the end of the entry: `#6431 #6691` (list both the issue and the PR)\n- Do NOT copy PR titles verbatim — they are often terse commit-style messages.\n- Do NOT fabricate code examples. Derive them from PR descriptions or actual source code.\n- Keep entries self-contained — a reader should understand the change without clicking the PR link.\n\n## Phase 5: Clean Up Dependencies Section\n\nThe raw script includes the full dependency update PR body, which uses a structured commit message format.\n\n1. **Strip non-user-facing dependencies**:\n   - Remove the `- Build` section and all its sub-bullets\n     (these are testImplementation, annotationProcessor, and other non-production deps).\n   - Remove **test-only** dependencies (e.g., Apache HttpClient, OkHttp, DGS) that are only\n     used in `testImplementation` / `testFixturesImplementation` and never appear in published POMs.\n   - Remove **shaded** dependencies (e.g., Guava, Bouncy Castle, JCTools) that are relocated\n     into Armeria JARs and invisible to users at runtime.\n   - Remove dependencies that are **not transitive** in any published artifact (e.g., Jsoup).\n   - When unsure, check `dependencies.toml` for the dependency's scope (`testImplementation`,\n     `shaded`, etc.) and verify whether it appears in a published module's `api` or\n     `implementation` configuration.\n2. **Format each entry**: `- LibraryName oldVersion → newVersion`\n   - Use the library's common name (e.g., `Jackson`, `Netty`, `gRPC-Java`, not the Maven artifact ID).\n   - Use `→` (unicode arrow), not `->`.\n3. **Group multi-version bumps** on one line when a library has multiple version streams:\n   `- Spring 6.2.14 → 6.2.15, 7.0.2 → 7.0.3`\n4. **Sort alphabetically** (A → Z).\n\n## Optional: Validate with a Site Build\n\n`cd site && npm run build` validates the MDX end to end, but requires generated files under\n`site/gen-src/` (`api-index.json`, `versions.json`, `contributors.json`):\n\n- Full generation: `./gradlew :site:generateSiteSources` (api-index needs the aggregated Javadoc,\n  which is slow).\n- Quick validation: copy `api-index.json`/`versions.json` from a previous build and generate\n  `contributors.json` with\n  `gh api --paginate repos/line/armeria/contributors | jq -s '[.[][]] | map({(.login): .avatar_url}) | add'`.\n  A stale `api-index.json` is fine for validation — unknown `[X](type)` names render as blank\n  links (`type://#`) and never fail the build, so type names must be verified against the source\n  either way.\n- `npm run build | tail` masks the exit code — capture it separately.\n\n## Phase 6: Finalize\n\n1. **Remove empty sections**: Delete any section whose only content is `- N/A`.\n2. **Remove \"Maybe ignore\"**: The `🗑 Maybe ignore` section must not appear in the final output.\n3. **Deduplicate and sort contributors**: Ensure `<ThankYou usernames={[...]} />` has\n   alphabetically sorted, deduplicated usernames. Remove bot accounts (`dependabot[bot]`,\n   `CLAassistant`). Core maintainers listed in `.github/CODEOWNERS` must always be included\n   in the Thank You section, even if they don't appear in the PR participant lists.\n4. **Ensure consistent bullet style**: Use `-` (dash) for all bullets, not `*`.\n5. **Write the final file** to `site/src/content/release-notes/<version>.mdx`.\n6. **Show a summary** to the user: list the sections, entry count per section, and any entries\n   flagged as uncertain (where PR context was insufficient to write a confident description).\n\n---\n\n## Phase 7: Lint\n\nRun the mechanical checks and fix everything they report:\n\n```\n.claude/skills/release-note/scripts/lint-release-note.sh site/src/content/release-notes/<version>.mdx\n```\n\nThis is not optional, and reading the style guide is not a substitute for running it. The rules it\nenforces are all written down elsewhere in this skill and have still been broken while the author\nhad them open — a malformed `(type)` link never fails the site build, it just renders as a blank\n`type://#` link, so nothing else catches it.\n\nThe script cannot see two things, so check them by hand:\n\n- **Identifiers copied from a PR body** — metric names, class names, proto fields. PR descriptions\n  go stale; the source does not. Grep the repo for every identifier you quote. (A real case: PR\n  #6840's body advertised `armeria.xds.lb.request`, while the code registered `lb.select`.)\n- **Whether a claim is true**, as opposed to well-formed. Read the code the entry describes.\n\n## Execution Checklist\n\n- [ ] Phase 0 — Ran `npm run release-note <version>` and verified output file exists\n- [ ] Phase 1 — Read draft, recent examples, and style guide\n- [ ] Phase 2 — Fetched PR/issue context for all referenced PRs\n- [ ] Phase 3 — Triaged all \"Maybe ignore\" entries\n- [ ] Phase 4 — Rewrote all entries per style guide\n- [ ] Phase 5 — Cleaned up dependencies section\n- [ ] Phase 6 — Removed empty sections, finalized file, reported summary\n- [ ] Phase 7 — Ran `lint-release-note.sh` and it exited 0\n\n## Common Mistakes to Avoid\n\n- **Copying PR titles as-is**: PR titles like \"Fix NPE in FooBar\" are not user-friendly.\n  Rewrite as \"Fixed a `NullPointerException` in [FooBar](type) when ...\"\n- **Fabricating code examples**: If you cannot find a clear usage pattern from the PR description\n  or source code, write a descriptive sentence instead of guessing at code.\n- **Over-linking types**: Only use `[Name](type)` for Armeria's own public API types, not for\n  JDK classes, third-party libraries, or internal classes.\n- **Including build dependencies**: The dependency update PR body contains `- Build` sub-bullets\n  for test/build-only deps. These must be stripped.\n- **Leaving `- N/A` sections**: The polished output should only contain sections with actual content.\n- **Using `*` bullets**: Standardize on `-` dashes for all bullet points.\n- **Missing `👈👈👈` callouts**: Every code example for a new feature should highlight the key line.\n\n<!-- File: .claude/skills/upgrade-deps/SKILL.md (Tokens: ~2176 | Category: anthropic-skill) -->\n---\nname: upgrade-deps\ndescription: Upgrade Gradle dependencies and Gradle wrapper version\ndisable-model-invocation: true\n---\n\n# Dependency Upgrade Workflow\n\nThis skill performs a full Gradle dependency upgrade for the Armeria project step by step.\n\n## Background: Minimum Java Version Per Module\n\nArmeria is a multi-module project with different minimum Java version requirements.\nBefore upgrading a dependency, always verify that the new version supports the minimum\nJava version required by the modules that use it.\n\n| Java Version | Modules |\n|-------------|---------|\n| **8**  | `core`, `brave5`, `brave6`, `eureka`, `grpc`, `grpc-kotlin`, `graphql-protocol`, `jetty9`, `junit4`, `junit5`, `kafka`, `kotlin`, `logback`, `logback12`, `logback13`, `micrometer-context`, `oauth2`, `prometheus1`, `protobuf`, `reactor3`, `resteasy`, `retrofit2`, `rxjava2`, `rxjava3`, `sangria`, `scala*`, `spring:boot2-*`, `dropwizard2`, `thrift0.9`–`thrift0.17`, `tomcat8`, `tomcat9`, `xds`, `zookeeper3`, `saml`, `bucket4j`, `consul`, `nacos` |\n| **11** | `athenz`, `graphql`, `jetty10`, `jetty11`, `kubernetes`, `logback14`, `thrift0.18`–`thrift0.22`, `tomcat10` |\n| **17** | `ai:mcp`, `jetty12`, `resilience4j2`, `spring:boot3-*`, `spring:spring6`, `spring:boot4-*`, `spring:spring7` |\n\nWhen in doubt, check `settings.gradle` for the `java`, `java11`, or `java17` flag on each subproject.\n\n**The table above lists published modules only.** Integration-test (`:it:*`) and example (`:examples:*`)\nmodules carry their own flags in `settings.gradle` and are frequently `java17` already. A dependency\nconsumed *only* by those modules is not held back by the Java 8 baseline at all — always resolve the\nactual consumer before assuming a Java constraint applies.\n\n## Step 1: Run dependencyUpdates\n\n```\n./gradlew dependencyUpdates --no-parallel\n```\n\nThe report is saved to `build/dependencyUpdates/report.txt`.\n\n## Step 2: Parse the Report\n\nRead `build/dependencyUpdates/report.txt` to identify all outdated dependencies.\nFor each dependency, note the current version and the available latest version.\n\nIf a version looks ambiguous (unexpected format, major version bump, unusual naming),\nverify on Maven Central before proceeding:\n  https://central.sonatype.com/artifact/{groupId}/{artifactId}\n\n## Step 3: Check Skip Hints and Java Version Compatibility\n\nBefore upgrading any dependency, check `dependencies.toml` for a comment directly above\nits version entry. Many pinned versions have an explicit reason, for example:\n\n```toml\n# Don't upgrade Caffeine to 3.x that requires Java 11.\ncaffeine = \"2.9.3\"\n\n# Upgrade once https://github.com/ronmamo/reflections/issues/279 is fixed.\nreflections = \"0.9.11\"\n\n# Ensure that we use the same ZooKeeper version as what Curator depends on.\nzookeeper = \"3.9.3\"\n```\n\n### Step 3.1: Validate the comment before trusting it\n\nA hold comment is a claim about this repository, and claims go stale. **Never treat a comment as\nproof — re-derive its reason every upgrade.** For each held entry:\n\n1. Find the aliases for the version key in the `[libraries.*]` / `[plugins]` sections.\n2. Grep the build scripts for each alias accessor (`libs.foo.bar` for alias `foo-bar`):\n   `git grep -n 'libs\\.foo\\.bar' -- '*.gradle' '*.gradle.kts'`\n3. Map each hit's build file to its project, and read that project's flags in `settings.gradle`.\n4. Compare the *consumer's* Java level against the new version's requirement — **not** the library's\n   requirement in isolation. \"Library X requires Java 17\" is not a blocker when every consumer is\n   already `java17`.\n\nIf the reason no longer holds, **remove the comment and upgrade**. If it still holds, keep both.\n\nTwo comments were found stale this way (`dgs` and `graphql-kotlin`, both consumed only by `java17`\nmodules), so treat this step as mandatory rather than a spot check.\n\n### Step 3.2: Never write a hold reason you have not verified\n\nWhen you add a comment, the reason must be something you confirmed, with the evidence to hand:\n\n- **Java constraint** — confirmed against the consumer module's flag, per Step 3.1.\n- **Runtime/API breakage** — confirmed by actually building and running the affected module's tests,\n  and the comment names the concrete blocker (e.g. `# Don't upgrade graphql-kotlin to 10.x that\n  migrated to Jackson 3`), not a vague \"major upgrade deferred\".\n- **Version-sync constraint** — confirmed from the other project's POM *and* from this repo's history\n  (`git log -p -- dependencies.toml`). Do not invent a sync rule: check whether the two versions have\n  ever actually moved independently before claiming they must move together.\n\nA plausible-sounding reason that nobody verified is worse than no comment, because the next upgrade\nwill trust it.\n\nIf no comment exists, proceed with the Java version compatibility check:\n1. Find which modules use this dependency in `dependencies.toml`\n2. Determine the minimum Java version required by those modules (see table above)\n3. Check the new library version's minimum Java requirement (Maven Central / release notes)\n4. If the library's required Java > module's minimum Java → **skip the upgrade** and add a\n   comment above the version entry explaining why (e.g. `# X.Y requires Java 11`)\n5. If compatible → proceed with the upgrade\n\n## Step 4: Update dependencies.toml\n\nEdit the `[versions]` section in `dependencies.toml` to update the version strings.\nThe file is located at the root of the repository: `dependencies.toml`.\n\n## Step 5: Cross-check Updated Versions\n\nAfter editing `dependencies.toml`, re-read the full diff (`git diff dependencies.toml`) and\ncross-check every changed entry against the `dependencyUpdates` report:\n\n1. **No entry was accidentally skipped** — compare the report's \"The following dependencies\n   have later milestone versions\" list against your edits. If a dependency appears in the\n   report but not in the diff, either upgrade it or document why it was skipped.\n2. **Versions match the report** — confirm each new version in the diff matches the latest\n   version from the report, not a typo or intermediate version.\n3. **Pinned-version comments were respected** — verify you did not upgrade a dependency whose\n   comment says to skip it, and that you did not remove or contradict an existing comment.\n4. **Linked-version constraints are satisfied** — some dependencies must stay in sync with\n   others (e.g. ZooKeeper with Curator, Protobuf with gRPC). Read the comments above those\n   entries and verify the constraint still holds after the upgrade.\n\n## Step 6: Upgrade Gradle Wrapper\n\nCheck the latest stable Gradle release at https://gradle.org/releases/ and update\n`gradle/wrapper/gradle-wrapper.properties`:\n\n```properties\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-X.Y.Z-all.zip\n```\n\n## Step 7: Verify the Build\n\nRun the full build including tests to catch both compilation errors and runtime regressions:\n\n```\n./gradlew build --no-daemon\n```\n\nIf there are compilation errors, API breaking changes, or test failures caused by the upgrade,\nfix them before proceeding.\n\n## Step 8: Commit\n\nCreate a commit with the **exact** message format below. Follow it strictly — do not add extra\nsections, reorder bullets, or change the structure:\n\n```\nUpdate dependencies\n\n- {library-name} {old-version} -> {new-version}\n- {library-name} {old-version} -> {new-version}\n- Build\n   - {library-name} {old-version} -> {new-version}\n   - {library-name} {old-version} -> {new-version}\n```\n\nRules:\n- Each production dependency (api/implementation scope) gets one bullet: `- {name} {old} -> {new}`\n- The `- Build` bullet groups build-only dependencies (testImplementation, annotationProcessor,\n  relocated libs, non-transitive deps); nest them as sub-bullets with 3-space indent\n- Use the library's official name if one exists (e.g. `gRPC-Java`, `Jackson`, `Netty`, `Kotlin`,\n  `Reactor`, `Logback`, `Micrometer`); otherwise use the key name as-is from `dependencies.toml`\n- Sort entries **alphabetically (A → Z)** within each section\n- If no build-only deps were upgraded, omit the `- Build` section entirely\n- If any dependencies were **not upgraded** (due to Java version constraints, pinned-version\n  comments, or linked-version constraints), list them under a `- Unupdated` section with the\n  reason. This helps reviewers know which upgrades were intentionally skipped.\n\nExample:\n```\nUpdate dependencies\n\n- gRPC-Java 1.63.0 -> 1.64.0\n- Jackson 2.17.0 -> 2.18.0\n- Build\n   - checkstyle 10.14.0 -> 10.17.0\n   - ErrorProne 2.27.0 -> 2.28.0\n- Unupdated\n   - Caffeine 2.9.3 -> 3.2.0 (requires Java 11)\n   - ZooKeeper 3.9.3 (pinned to Curator's version)\n```\n\n<!-- File: tools/claude-plugins/assistant/skills/eventloop-audit/references/armeria-thread-pool-guide.md (Tokens: ~4702 | Category: anthropic-skill) -->\n<!-- A developer-facing version of this guide is published at /docs/advanced/threading-model -->\n\n# Armeria Thread Pool Guide for Developers\n\n## Context\n\nArmeria is built on Netty's non-blocking I/O model. Misusing thread pools — especially blocking the event loop — is the most common cause of performance degradation and deadlocks. This guide provides an architectural overview, recommended configurations, and practical dos/don'ts for developers working with Armeria.\n\n---\n\n## 1. Architectural Overview\n\n### Thread Pool Roles\n\n| Pool | What it does | Thread name pattern | Default size | Daemon? |\n|------|-------------|---------------------|-------------|---------|\n| **bossGroup** | Accepts incoming TCP connections (one per server port) | `armeria-boss-{protocol}-{addr}` where addr is `*:{port}`, `{ip}:{port}`, or `unix:{path}` | **1 per port** (fixed, not configurable) | No |\n| **workerGroup** | Handles all socket I/O (reads/writes) and executes non-blocking service logic | `armeria-common-worker-*` | `2 * CPU cores` (NIO/epoll/kqueue) or `CPU cores` (io_uring) | Yes |\n| **serviceWorkerGroup** | Optional dedicated EventLoopGroup for service method execution, isolating service logic from socket I/O | User-defined | Falls back to workerGroup if not set | Yes |\n| **blockingTaskExecutor** | Runs long-running or blocking operations (DB calls, file I/O, legacy sync APIs) | `armeria-common-blocking-tasks-*` | **200** threads (inspired by Tomcat default) | Yes |\n\n### How a Request Flows Through the Pools\n\n```text\nClient request\n    |\n    v\n[bossGroup]  -- accepts TCP connection, hands channel to workerGroup\n    |\n    v\n[workerGroup]  -- reads bytes, decodes HTTP/2 frames, invokes service\n    |\n    +---> Non-blocking service: runs directly on event loop thread\n    |\n    +---> @Blocking / useBlockingTaskExecutor(true): dispatched to blockingTaskExecutor\n    |\n    +---> serviceWorkerGroup configured: service runs on dedicated event loop\n```\n\n### Key Classes and Files\n\n- `CommonPools` — global shared pools (`core/.../common/CommonPools.java`)\n- `EventLoopGroups` — factory for creating EventLoopGroups (`core/.../common/util/EventLoopGroups.java`)\n- `BlockingTaskExecutor` / `BlockingTaskExecutorBuilder` — blocking pool abstraction (`core/.../common/util/`)\n- `EventLoopThread` — custom thread extending Netty's `FastThreadLocalThread`, implements `NonBlocking` marker (`core/.../internal/common/util/EventLoopThread.java`)\n- `EventLoopCheckingFuture` — warns when `.get()`/`.join()` is called from an event loop (`core/.../common/util/EventLoopCheckingFuture.java`)\n- `DefaultAnnotatedService` — dispatches to event loop or blocking executor based on `@Blocking` (`core/.../internal/server/annotation/DefaultAnnotatedService.java`)\n- `Flags` / `DefaultFlagsProvider` — system-wide defaults and JVM flag overrides (`core/.../common/Flags.java`)\n\n### Client-Side Thread Pools\n\nThe HTTP client (`WebClient`) uses the same `CommonPools.workerGroup()` by default. An `EventLoopScheduler` distributes connections across event loops per endpoint (default: 1 event loop per endpoint, configurable).\n\nKey client config class: `ClientFactoryBuilder` (`core/.../client/ClientFactoryBuilder.java`)\n\n---\n\n## 2. `serviceWorkerGroup` vs `blockingTaskExecutor` — When to Use Which\n\nThese two are the most commonly confused. They solve **different problems** and have fundamentally different runtime characteristics.\n\n### The Core Distinction\n\n| Aspect | `serviceWorkerGroup` | `blockingTaskExecutor` |\n|--------|----------------------|------------------------|\n| **Java type** | `EventLoopGroup` (Netty event loops) | `ScheduledExecutorService` (thread pool) |\n| **Thread type** | Event loop threads (`NonBlocking`) | Regular threads (blocking allowed) |\n| **Can you block in it?** | **NO** — same rules as workerGroup | **YES** — that's its purpose |\n| **Scheduling model** | Single-threaded per loop, run-to-completion | Traditional thread pool, one thread per task |\n| **Typical size** | Small (matches CPU cores) | Large (default 200) |\n\n### How They Work at Runtime\n\n**serviceWorkerGroup** — The service method still runs on an event loop, just a *different* one from the I/O channel. In `HttpServerHandler`, Armeria checks whether the configured serviceWorkerGroup differs from the main workerGroup. If so, it calls `req.subscribeOn(serviceEventLoop)` and invokes the service on that loop. Request reading, service execution, and response emission all happen on the service event loop. Response *writing* to the socket still goes back to the channel's original event loop.\n\n```text\n[workerGroup event loop]  reads bytes from socket\n        |\n        v  (if serviceWorkerGroup != workerGroup)\n[serviceWorkerGroup event loop]  decodes request, runs service, emits response\n        |\n        v  (response writing)\n[workerGroup event loop]  writes bytes to socket\n```\n\n**blockingTaskExecutor** — The service method is submitted via `thenApplyAsync(..., ctx.blockingTaskExecutor())` to a traditional thread pool. The calling event loop is freed immediately. The blocking thread runs the service logic, and when done, the response flows back to the event loop for writing.\n\n```text\n[event loop]  reads bytes, decodes request\n        |\n        v  (thenApplyAsync)\n[blocking thread pool]  runs service method (blocking OK here)\n        |\n        v  (future completes)\n[event loop]  writes response bytes to socket\n```\n\n### Decision Guide\n\n**Use `serviceWorkerGroup` when:**\n\n- Your service does **CPU-intensive but non-blocking** work (e.g., JSON serialization of large payloads, image transformation in memory, complex computation)\n- You want to **prevent service CPU work from starving socket I/O** — if your service burns 50ms of CPU per request, that delays read/write for all other connections sharing that event loop\n- You need **isolation between services** — e.g., a high-throughput service shouldn't compete for event loop time with a latency-sensitive one\n- Your code is already fully async (returns `CompletableFuture`, uses reactive streams) — you just want it on a different event loop\n\n```java\n// Good use case: CPU-heavy serialization isolated from I/O\nServer.builder()\n      .serviceWorkerGroup(4)  // Dedicated event loops for service logic\n      .service(\"/heavy-json\", (ctx, req) -> {\n          // Non-blocking but CPU-intensive: serialize huge object\n          return HttpResponse.of(hugeObjectMapper.writeValueAsBytes(bigData));\n      })\n      .build();\n```\n\n**Use `blockingTaskExecutor` when:**\n\n- Your code calls **synchronous blocking APIs** (JDBC, file I/O, `Thread.sleep`, `future.get()`, legacy HTTP clients, `synchronized` blocks)\n- You're integrating with **libraries that block the calling thread** (most traditional Java libraries)\n- You have **no choice** but to wait for a result synchronously\n\n```java\n// Good use case: JDBC call that blocks the thread\nServer.builder()\n      .service(\"/users\", (ctx, req) ->\n          HttpResponse.of(CompletableFuture.supplyAsync(() -> {\n              // This blocks — JDBC has no async API\n              User user = jdbcTemplate.queryForObject(\"SELECT ...\", User.class);\n              return HttpResponse.of(HttpStatus.OK, MediaType.JSON, toJson(user));\n          }, ctx.blockingTaskExecutor())))\n      .build();\n```\n\n**DON'T use `serviceWorkerGroup` for blocking work** — it's still an event loop. Blocking it has the same catastrophic effect as blocking the main workerGroup. You're just blocking a *different* event loop.\n\n**DON'T use `blockingTaskExecutor` for CPU-bound async work** — it wastes a thread sitting idle during async operations and doesn't benefit from event loop optimizations (FastThreadLocal, zero-copy buffers, etc.).\n\n### When You Need Neither\n\nMost async services need neither. If your service just orchestrates async HTTP calls, reactive streams, or CompletableFuture chains, let it run on the default workerGroup.\n\n---\n\n## 3. Recommended Configuration Settings\n\n### Sizing Guidelines\n\n| Pool | Guideline |\n|------|-----------|\n| **workerGroup** | Default (`2 * CPU`) is usually fine. Increase only if profiling shows event loop saturation on I/O-heavy workloads. |\n| **blockingTaskExecutor** | Default 200 is generous. Size based on expected concurrent blocking operations. Consider: `expected_concurrent_blocking_calls * avg_blocking_duration / target_latency`. |\n| **serviceWorkerGroup** | Only configure if you need to isolate service execution from socket I/O (e.g., CPU-heavy request processing that shouldn't delay I/O). Most services don't need this. |\n\n### JVM Flags\n\n```properties\n-Dcom.linecorp.armeria.numCommonWorkers=<int>           # Override worker group size\n-Dcom.linecorp.armeria.numCommonBlockingTaskThreads=<int> # Override blocking executor size\n-Dcom.linecorp.armeria.reportBlockedEventLoop=true       # Enable event loop blocking warnings (default: true)\n```\n\n### Server Builder Example\n\n```java\nServer.builder()\n      // Custom worker group (only if default is insufficient)\n      .workerGroup(16)\n      // Custom blocking executor for this server\n      .blockingTaskExecutor(BlockingTaskExecutor.builder()\n              .numThreads(100)\n              .threadNamePrefix(\"my-app-blocking\")\n              .build(), true)\n      .service(\"/api\", myService)\n      .build();\n```\n\n### Client Factory Example\n\n```java\nClientFactory.builder()\n      .workerGroup(8)\n      // Distribute HTTP/2 connections across more event loops for high-throughput endpoints\n      .maxNumEventLoopsPerEndpoint(4)\n      .build();\n```\n\n---\n\n## 4. Dos and Don'ts\n\n### DO: Use `@Blocking` for Synchronous Service Methods\n\n```java\n// Annotated service — method level\n@Get(\"/users/{id}\")\n@Blocking\npublic User getUser(@Param int id) {\n    return database.findById(id); // Safe: runs on blocking executor\n}\n\n// Annotated service — class level (all methods become blocking)\n@Blocking\npublic class MyDatabaseService {\n    @Get(\"/items\")\n    public List<Item> list() { return db.listAll(); }\n}\n```\n\n### DO: Use `useBlockingTaskExecutor(true)` for gRPC/Thrift Services\n\n```java\n// gRPC\nServer.builder()\n      .service(GrpcService.builder()\n              .addService(new MyBlockingGrpcService())\n              .useBlockingTaskExecutor(true)\n              .build());\n\n// Thrift\nServer.builder()\n      .service(\"/thrift\", THttpService.builder()\n              .addService(new MyBlockingThriftService())\n              .useBlockingTaskExecutor(true)\n              .build());\n```\n\n### DO: Use `ctx.blockingTaskExecutor()` for Ad-Hoc Blocking Work\n\n```java\n@Get(\"/report\")\npublic CompletableFuture<HttpResponse> generateReport(ServiceRequestContext ctx) {\n    return CompletableFuture.supplyAsync(() -> {\n        byte[] pdf = slowPdfGenerator.generate(); // blocking\n        return HttpResponse.of(HttpStatus.OK, MediaType.PDF, pdf);\n    }, ctx.blockingTaskExecutor());\n}\n```\n\nThe `ctx.blockingTaskExecutor()` returns a context-aware executor — it automatically propagates `ServiceRequestContext` to the blocking thread, so logging, tracing, and `ServiceRequestContext.current()` work correctly.\n\n### DO: Stay Async When Possible\n\n```java\n// Prefer returning async types on the event loop\n@Get(\"/data\")\npublic CompletableFuture<String> getData() {\n    return asyncHttpClient.get(\"/upstream\")\n            .thenApply(response -> response.contentUtf8());\n    // Runs entirely on event loop — no blocking, no thread switch\n}\n```\n\n### DON'T: Block the Event Loop\n\n```java\n// NEVER do this on the event loop\n@Get(\"/bad\")\npublic String bad() {\n    Thread.sleep(1000);                    // Blocks event loop\n    return db.query(\"SELECT ...\");         // Blocks event loop\n    future.get();                          // Blocks event loop\n    future.join();                         // Blocks event loop\n    synchronized(lock) { ... }             // Can block event loop\n    new URL(...).openConnection();         // Blocks event loop\n}\n```\n\nArmeria will log a warning if you call `.get()` or `.join()` on an `EventLoopCheckingFuture` from an event loop thread. Treat these warnings as bugs.\n\n### DON'T: Create Unbounded Thread Pools for Blocking Work\n\n```java\n// Don't do this — use Armeria's blockingTaskExecutor instead\nExecutorService myPool = Executors.newCachedThreadPool();\nmyPool.submit(() -> { ... });\n// Problems: no context propagation, no metrics, no coordinated shutdown\n```\n\n### DON'T: Mix Blocking and Non-Blocking in the Same Service Without Annotation\n\n```java\n// BAD: one blocking method taints the whole event loop experience\npublic class MixedService {\n    @Get(\"/fast\")\n    public String fast() { return \"ok\"; }\n\n    @Get(\"/slow\")\n    public String slow() {\n        return db.query(\"...\"); // Blocks! But no @Blocking annotation\n    }\n}\n```\n\nFix: add `@Blocking` to the slow method, or use `ctx.blockingTaskExecutor()` explicitly.\n\n### DON'T: Assume `serviceWorkerGroup` is Required\n\nMost applications should NOT configure `serviceWorkerGroup`. It adds complexity and is only useful when:\n- Your service does CPU-intensive work (e.g., serialization of large payloads) that could starve socket I/O\n- You want strict isolation between I/O handling and request processing\n\nFor blocking work, use `blockingTaskExecutor` instead — that's what it's for.\n\n### DON'T: Forget Context Propagation When Switching Threads\n\n```java\n// BAD: loses ServiceRequestContext\nsomeOtherExecutor.submit(() -> {\n    ServiceRequestContext.current(); // Throws IllegalStateException!\n});\n\n// GOOD: use context-aware executor\nctx.blockingTaskExecutor().submit(() -> {\n    ServiceRequestContext.current(); // Works\n});\n\n// GOOD: if you must use a custom executor, propagate context manually\nsomeOtherExecutor.submit(ctx.makeContextAware(() -> {\n    ServiceRequestContext.current(); // Works\n}));\n```\n\n---\n\n## 5. Programmatic Usage (Without Annotations)\n\nWhen you implement `HttpService` directly, build decorators, or work with gRPC/Thrift at a lower level, you manage executor dispatch yourself.\n\n### 5.1 Direct `HttpService` with Blocking Work\n\n```java\n// Implement HttpService directly — you control everything\nHttpService myService = (ctx, req) -> {\n    CompletableFuture<HttpResponse> future = new CompletableFuture<>();\n    ctx.blockingTaskExecutor().execute(() -> {\n        try {\n            String result = legacySyncClient.call(); // blocking\n            future.complete(HttpResponse.of(HttpStatus.OK, MediaType.JSON, result));\n        } catch (Exception e) {\n            future.completeExceptionally(e);\n        }\n    });\n    return HttpResponse.of(future);\n};\n\nServer.builder().service(\"/api\", myService).build();\n```\n\n### 5.2 CompletableFuture Chaining with Explicit Executors\n\nUse `thenApplyAsync` / `thenComposeAsync` / `supplyAsync` with the right executor to control where each stage runs.\n\n```java\nHttpService myService = (ctx, req) -> {\n    // Stage 1: blocking DB call on blocking executor\n    CompletableFuture<List<Long>> ids = CompletableFuture.supplyAsync(\n            () -> {\n                assert ServiceRequestContext.current() == ctx;  // context is propagated\n                assert !ctx.eventLoop().inEventLoop();           // not on event loop\n                return jdbcTemplate.queryForList(\"SELECT id FROM items\", Long.class);\n            },\n            ctx.blockingTaskExecutor());\n\n    // Stage 2: async HTTP calls back on event loop\n    CompletableFuture<List<AggregatedHttpResponse>> details = ids.thenComposeAsync(\n            idList -> {\n                assert ctx.eventLoop().inEventLoop();  // back on event loop\n                List<CompletableFuture<AggregatedHttpResponse>> futures = idList.stream()\n                        .map(id -> backendClient.get(\"/items/\" + id).aggregate())\n                        .collect(Collectors.toList());\n                return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))\n                        .thenApply(v -> futures.stream()\n                                // join() is safe here: allOf() guarantees all futures\n                                // are already complete, so join() returns immediately.\n                                .map(CompletableFuture::join)\n                                .collect(Collectors.toList()));\n            },\n            ctx.eventLoop());\n\n    // Stage 3: assemble response (still on event loop)\n    CompletableFuture<HttpResponse> response = details.thenApply(\n            results -> HttpResponse.of(HttpStatus.OK, MediaType.JSON, toJson(results)));\n\n    return HttpResponse.of(response);\n};\n```\n\n### 5.3 Scheduling Delayed Work on the Event Loop\n\nUse `ctx.eventLoop().schedule()` for non-blocking timed operations.\n\n```java\n// gRPC: respond after a delay without blocking a thread\nServiceRequestContext.current().eventLoop().schedule(() -> {\n    responseObserver.onNext(buildReply(request.getName()));\n    responseObserver.onCompleted();\n}, 3, TimeUnit.SECONDS);\n```\n\n### 5.4 gRPC: Submit Blocking Work Manually\n\nWhen you don't want `useBlockingTaskExecutor(true)` for the entire service, submit individual calls.\n\n```java\npublic class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {\n    @Override\n    public void hello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {\n        // This method runs on the event loop by default.\n        // Submit only the blocking part:\n        ServiceRequestContext.current().blockingTaskExecutor().submit(() -> {\n            try {\n                Thread.sleep(3000); // Simulate blocking API\n            } catch (Exception ignored) {}\n            responseObserver.onNext(buildReply(request.getName()));\n            responseObserver.onCompleted();\n        });\n    }\n}\n```\n\n### 5.5 Thrift: Submit Blocking Work Manually\n\nSame pattern as gRPC.\n\n```java\nServiceRequestContext.current().blockingTaskExecutor().execute(() -> {\n    try {\n        Thread.sleep(3000); // Simulate blocking\n    } catch (Exception ignored) {}\n    resultHandler.onComplete(buildReply(request.getName()));\n});\n```\n\n### 5.6 Decorators with Executor Awareness\n\n```java\n// A decorator that logs request duration, running the delegate as-is\nHttpService decorated = myService.decorate((delegate, ctx, req) -> {\n    long start = System.nanoTime();\n    return delegate.serve(ctx, req).mapHeaders(headers -> {\n        long elapsed = System.nanoTime() - start;\n        logger.info(\"{} took {} ms\", ctx.path(), elapsed / 1_000_000);\n        return headers;\n    });\n});\n```\n\n### 5.7 Context Propagation with `makeContextAware`\n\nWhen you must use a third-party executor that Armeria doesn't control, wrap your task.\n\n```java\n// Wrapping a Runnable\nsomeExternalExecutor.submit(ctx.makeContextAware(() -> {\n    ServiceRequestContext.current(); // Works — context is mounted\n    doWork();\n}));\n\n// Wrapping a Callable\nFuture<String> result = someExternalExecutor.submit(ctx.makeContextAware(() -> {\n    return ServiceRequestContext.current().path(); // Works\n}));\n\n// Wrapping a Function (for CompletableFuture chains)\nfuture.thenApply(ctx.makeContextAware(value -> {\n    ServiceRequestContext.current(); // Works\n    return transform(value);\n}));\n```\n\n### 5.8 Reactor Integration\n\n```java\nHttpService myService = (ctx, req) -> {\n    Scheduler blockingScheduler = Schedulers.fromExecutor(ctx.blockingTaskExecutor());\n    Scheduler eventLoopScheduler = Schedulers.fromExecutor(ctx.eventLoop());\n\n    Mono<String> result = Mono.fromCallable(() -> {\n                // Runs on blocking scheduler\n                return database.query(\"SELECT ...\");\n            })\n            .subscribeOn(blockingScheduler)\n            .flatMap(dbResult ->\n                // Switch back to event loop for async HTTP call\n                Mono.fromCompletionStage(backendClient.get(\"/enrich/\" + dbResult).aggregate())\n                    .subscribeOn(eventLoopScheduler)\n                    .map(resp -> resp.contentUtf8())\n            );\n\n    return HttpResponse.of(result.map(body ->\n            HttpResponse.of(HttpStatus.OK, MediaType.JSON, body)).toFuture());\n};\n```\n\n### 5.9 RxJava Integration\n\n```java\nScheduler blockingScheduler = Schedulers.from(ctx.blockingTaskExecutor());\nScheduler eventLoopScheduler = Schedulers.from(ctx.eventLoop());\n\nSingle<String> result = Single.fromCallable(() -> database.query(\"SELECT ...\"))\n        .subscribeOn(blockingScheduler)       // blocking work here\n        .observeOn(eventLoopScheduler)        // switch to event loop\n        .flatMap(dbResult ->\n            Single.fromCompletionStage(backendClient.get(\"/enrich/\" + dbResult).aggregate())\n                  .map(AggregatedHttpResponse::contentUtf8));\n```\n\n---\n\n## Quick Reference: Which Pool to Use\n\n| Scenario | Pool | How |\n|----------|------|-----|\n| HTTP/gRPC request handling (async) | workerGroup (event loop) | Default — just return `CompletableFuture` or reactive types |\n| Database queries, file I/O, sync APIs | blockingTaskExecutor | `@Blocking`, `useBlockingTaskExecutor(true)`, or `ctx.blockingTaskExecutor()` |\n| CPU-heavy non-blocking work that shouldn't starve I/O | serviceWorkerGroup | `ServerBuilder.serviceWorkerGroup(n)` (rare) |\n| Client HTTP calls | workerGroup (event loop) | Default — `WebClient` is async |\n| Legacy synchronous library calls | blockingTaskExecutor | Wrap with `ctx.blockingTaskExecutor().submit(...)` |\n| Delayed/scheduled non-blocking work | event loop | `ctx.eventLoop().schedule(...)` |\n| Reactor blocking operations | blockingTaskExecutor | `.subscribeOn(Schedulers.fromExecutor(ctx.blockingTaskExecutor()))` |\n| Custom third-party executor | context-wrap it | `ctx.makeContextAware(runnable)` |\n\n<!-- File: tools/claude-plugins/assistant/skills/eventloop-audit/references/detection-patterns.md (Tokens: ~2287 | Category: anthropic-skill) -->\n# Detection Patterns — 7 Categories\n\nSearch the entire project (or scoped path) for each category of blocking pattern. Exclude test files (`**/test/**`, `**/tests/**`) and generated code (`**/generated/**`, `**/target/**`, `**/build/**`).\n\n## Category 1: Direct Blocking Calls\n\nThese are explicit operations that block the calling thread.\n\n**Search patterns:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `\\.join\\(\\)` | `CompletableFuture.join()` — blocks indefinitely |\n| `\\.get\\(\\s*\\)` | `Future.get()` with no args — blocks indefinitely |\n| `\\.get\\(\\s*\\d` | `Future.get(timeout, unit)` — bounded but still blocks |\n| `Thread\\.sleep\\(` | `Thread.sleep()` — deliberate blocking |\n| `\\.await\\(` | `CountDownLatch.await()`, `Condition.await()`, `CyclicBarrier.await()` |\n| `\\.acquire\\(` | `Semaphore.acquire()` — blocks until permit available |\n| `\\.lock\\(\\)` | `ReentrantLock.lock()` — blocks until lock available |\n| `\\.lockInterruptibly\\(\\)` | Same, interruptible variant |\n| `\\.take\\(\\)` | `BlockingQueue.take()` — blocks until element available |\n| `\\.put\\(` | `BlockingQueue.put()` — blocks if queue full (check context) |\n| `\\.wait\\(` | `Object.wait()` — monitor wait |\n\n**Triage for each hit:**\n1. Identify the enclosing method\n2. Trace callers to determine if reachable from an event loop entry point (from Phase 1)\n3. Check if the call is inside a known safe execution wrapper\n4. Classify severity (see Phase 3)\n\n## Category 2: Blocking I/O Operations\n\nI/O operations always block the calling thread. The question is whether they run on the event loop.\n\n**2A — File system operations:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `new FileInputStream` | File reading |\n| `new FileOutputStream` | File writing |\n| `new RandomAccessFile` | File random access |\n| `FileChannel\\.open` | File channel (blocking despite NIO package) |\n| `Files\\.(read\\|write\\|copy\\|move\\|walk\\|list\\|newInputStream\\|newOutputStream)` | NIO file operations (still blocking despite being in `java.nio`) |\n| `ProcessBuilder.*\\.start\\(\\)` | Process execution |\n| `Runtime\\..*exec\\(` | Process execution |\n\n**2B — Network and HTTP operations:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `\\.openConnection\\(\\)` | `URL.openConnection()` — blocking HTTP |\n| `\\.openStream\\(\\)` | `URL.openStream()` — blocking HTTP |\n| `HttpURLConnection` | Legacy blocking HTTP client |\n| `HttpClient.*\\.send\\(` | `java.net.http.HttpClient.send()` — synchronous (`.sendAsync()` is safe) |\n| `new Socket\\(` | TCP socket creation |\n| `new ServerSocket\\(` | Server socket binding |\n| `Pipe\\.open\\(\\)` | NIO pipe creation — blocking despite NIO package |\n\n**2C — Database operations:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `DriverManager\\.getConnection` | JDBC connection creation |\n| `javax\\.sql\\.DataSource` | JDBC data source |\n| `java\\.sql\\.\\(Connection\\|Statement\\|PreparedStatement\\|ResultSet\\)` | JDBC operations |\n\n**2D — Serialization and compression:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `ObjectInputStream\\|ObjectOutputStream` | Java serialization (blocking I/O on streams) |\n| `GZIPInputStream\\|GZIPOutputStream` | GZIP compression (blocking I/O) |\n| `ZipInputStream\\|ZipOutputStream` | ZIP operations (blocking I/O) |\n| `DeflaterInputStream\\|InflaterOutputStream` | Raw deflate/inflate (blocking I/O) |\n\n**2E — Encryption and security:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `KeyStore\\.load\\(` | Keystore loading from disk — blocking file I/O |\n| `SSLContext\\.getInstance` | SSL context initialization — can trigger I/O |\n| `TrustManagerFactory\\.init` | Trust manager init — may load certificates |\n\n## Category 3: Synchronization That Could Contend on Event Loop\n\nLocks are only a problem if they can contend (block waiting) on an event loop thread.\n\n**Search patterns:**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `synchronized\\s*\\(` | Synchronized blocks |\n| `\\bsynchronized\\b[^\\n{;]*\\(` | Synchronized methods (modifier in method signature) |\n| `ReentrantLock` | Explicit locking |\n| `ReentrantReadWriteLock` | Read-write locking |\n| `StampedLock` | Stamped locking |\n\n**Triage:**\n- `synchronized` on startup/config-only paths → **SAFE**\n- `synchronized` on request-path data accessed from event loop → **MEDIUM to HIGH** depending on contention probability\n- `synchronized` on a monitor that is also locked by a thread pool thread → **HIGH** (event loop can wait for thread pool)\n\n## Category 4: CompletableFuture Continuations on Wrong Executor\n\nWhen you chain `.thenApply()` (without `Async`), the continuation runs on whichever thread completes the source future. If that's an event loop thread, the continuation is on the event loop.\n\n**Search patterns (non-async variants — potentially dangerous):**\n\n| Pattern | What it catches |\n|---------|----------------|\n| `\\.thenApply\\(` | Synchronous continuation — inherits completing thread |\n| `\\.thenAccept\\(` | Same |\n| `\\.thenRun\\(` | Same |\n| `\\.thenCompose\\(` | Same |\n| `\\.whenComplete\\(` | Same |\n| `\\.handle\\(` | Same |\n| `\\.exceptionally\\(` | Same |\n\n**For each hit, determine:**\n1. What completes the source future? (event loop thread? thread pool thread?)\n2. Does the continuation body do blocking work?\n3. If both answers are problematic → needs `*Async` variant with explicit executor\n\n**Important — `*Async()` without executor is NOT always safe:**\n```regex\n\\.thenApplyAsync\\(\n\\.thenAcceptAsync\\(\n\\.thenComposeAsync\\(\n```\nThe single-argument `*Async()` variants (e.g., `.thenApplyAsync(fn)`) use `ForkJoinPool.commonPool()`, NOT the blocking task executor. This is problematic if:\n- The continuation does blocking I/O → blocks a common pool thread, starving other ForkJoinPool work\n- The project expects `ServiceRequestContext` to be available → it won't be on ForkJoinPool threads\n\nOnly the two-argument variants (e.g., `.thenApplyAsync(fn, executor)`) with an explicit executor are reliably safe. Verify that the second argument is a blocking executor, not just any executor.\n\n## Category 5: Thread Pool Misconfiguration\n\n**Search patterns:**\n\n| Pattern | Risk |\n|---------|------|\n| `Executors\\.newCachedThreadPool` | Unbounded — can exhaust memory under load |\n| `Executors\\.newSingleThreadExecutor` | Single thread — no parallelism, instant bottleneck |\n| `new ThreadPoolExecutor\\(` | Check queue type and max size — unbounded `LinkedBlockingQueue` is risky |\n| `new ForkJoinPool\\(` | Check initialization is thread-safe, check parallelism level |\n\n**Also verify:**\n- Is `blockingTaskExecutor` configured in `ServerBuilder`? (If not, Armeria uses its default 200-thread pool)\n- Is the blocking executor sized for the expected concurrent blocking operations?\n- Are custom executors properly shut down on server close?\n\n## Category 6: Missing Armeria Idioms\n\nThese are patterns where the project doesn't use Armeria's built-in mechanisms for handling blocking work.\n\n**Check for:**\n\n1. **gRPC services doing blocking work without `useBlockingTaskExecutor(true)`**\n   - Find gRPC handler methods (from Phase 1.4)\n   - Check if the method body contains I/O, database calls, or blocking waits\n   - If yes and `useBlockingTaskExecutor(true)` is not set → **CRITICAL**\n\n2. **Annotated services with blocking methods missing `@Blocking`**\n   - Find `@Get`/`@Post`/etc. methods (from Phase 1.3)\n   - Check if the method body contains blocking operations\n   - If yes and `@Blocking` is absent → **CRITICAL**\n\n3. **Custom `HttpService.serve()` that blocks**\n   - Find `HttpService` implementations (from Phase 1.2)\n   - Check if `serve()` body contains blocking operations without offloading to `ctx.blockingTaskExecutor()`\n   - If yes → **CRITICAL**\n\n4. **Cancel handlers that block**\n   ```text\n   Grep: setOnCancelHandler|whenRequestCancelling\n   ```\n   - Cancel handler lambdas run on the event loop\n   - Check if they call `.get()`, `.join()`, I/O, or any blocking operation → **CRITICAL**\n\n5. **Missing context propagation**\n   ```text\n   Grep: \\.submit\\(|\\.execute\\(|\\.schedule\\(\n   ```\n   - If work is submitted to a non-Armeria executor, check for `ctx.makeContextAware()` wrapping\n   - Missing context propagation means `ServiceRequestContext.current()` will fail in the submitted task → **MEDIUM**\n\n## Category 7: Transitive / Hidden Blocking\n\nThese are blocking operations that are easy to overlook.\n\n**Search patterns:**\n\n| Pattern | Risk |\n|---------|------|\n| `InetAddress\\.getByName` | DNS resolution — can block for seconds |\n| `InetAddress\\.getAllByName` | Same |\n| `Class\\.forName` | Class loading — can trigger I/O to read class files |\n| `ServiceLoader\\.load` | Classpath scanning — iterates JARs |\n| `System\\.getProperty` | Usually fast, but can block with SecurityManager |\n\n**Configuration-level checks:**\n- **Logging framework**: Does the project use Logback or Log4j? Check `logback.xml` / `log4j2.xml` for synchronous appenders (`FileAppender`, `ConsoleAppender` without `AsyncAppender` wrapper). Every `log.info()` on the event loop becomes blocking I/O if appenders are synchronous.\n  ```text\n  Grep: <appender|<AsyncAppender|AsyncLogger\n  Scope: **/logback*.xml, **/log4j2*.xml, **/logging*.xml\n  ```\n\n<!-- File: tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/line/armeria/HEAD/tools/claude-plugins/assistant/skills/eventloop-audit/references/fix-templates.md)\n\n<!-- File: tools/claude-plugins/assistant/skills/eventloop-audit/SKILL.md (Tokens: ~3741 | Category: anthropic-skill) -->\n---\nname: eventloop-audit\ndescription: Systematically audits any Armeria-based Java project for event loop blocking issues — discovers project patterns, scans for blocking operations, traces call chains, and produces a fix plan without modifying code. Use when experiencing latency spikes, connection timeouts, deadlocks, or thread starvation in Armeria services, or as a pre-release audit. Invoked as `/eventloop-audit` for the full project or `/eventloop-audit <path>` for a specific directory.\n---\n\n# Event Loop Blocking Audit for Armeria Projects\n\n## Overview\n\nA systematic, multi-phase audit that discovers how a project uses Armeria, scans for all categories of event loop blocking, traces call chains to determine thread context, and produces a structured report with fix plans. **No code is modified** — only analysis and planning.\n\nThis skill is **framework-generic** — it works on any Armeria-based Java project by discovering the project's patterns dynamically, not assuming specific class names or directory structures.\n\n## When to Use\n\n- After setting up a new Armeria-based project and want to verify thread safety\n- Before a release to audit for latent event loop blocking issues\n- When experiencing unexplained latency spikes, connection timeouts, or deadlocks in an Armeria server\n- After integrating a new library that may contain blocking calls\n- As a periodic health check on the codebase's non-blocking discipline\n\n## Thread Model Reference\n\nFor a comprehensive guide on Armeria's thread pools, executor selection, and programmatic usage patterns, see [armeria-thread-pool-guide.md](references/armeria-thread-pool-guide.md).\n\n**The Golden Rule**: If `Thread.currentThread() instanceof NonBlocking` — you **MUST NOT** block. Armeria's event loop threads implement the `NonBlocking` marker interface. Any blocking operation on these threads stalls ALL connections sharing that event loop, causing cascading latency and potential deadlocks.\n\n---\n\n## Phase 1: Discovery — Understand the Project's Armeria Usage\n\nBefore searching for problems, discover HOW the project uses Armeria. Launch an **Explore agent** (or do it yourself) with these specific searches. Record findings as they'll inform all subsequent phases.\n\n### 1.1 Find Armeria Server Configuration\n\n```text\nGrep: Server\\.builder\\(\\)|ServerBuilder\nScope: **/*.java\n```\n\nRead each match and document:\n- Worker group configuration (`workerGroup(n)`, `serviceWorkerGroup(n)`)\n- Blocking executor configuration (`blockingTaskExecutor(...)`)\n- Decorator chain (`.decorate(...)` calls)\n- Request/idle timeout settings\n- Any custom `EventLoopGroup` creation\n\n### 1.2 Find HttpService Implementations\n\n```text\nGrep: implements HttpService|extends AbstractHttpService|extends SimpleDecoratingHttpService\nScope: **/*.java\n```\n\nThese classes have `serve()` methods that run on the event loop. List all of them — they are the primary audit targets.\n\n### 1.3 Find Annotated Services\n\n```text\nGrep: @Get\\(|@Post\\(|@Put\\(|@Delete\\(|@Head\\(|@Patch\\(|@Options\\(\nScope: **/*.java\n```\n\nFor each, check if the method or class has `@Blocking`. Methods without `@Blocking` run on the event loop.\n\n### 1.4 Find gRPC Services\n\n```text\nGrep: extends \\w+ImplBase|GrpcService\\.builder\\(\\)\nScope: **/*.java\n```\n\nCheck whether `useBlockingTaskExecutor(true)` is configured in the `GrpcService.builder()`. If not, all gRPC handler methods run on the event loop.\n\n### 1.5 Find Thrift Services\n\n```text\nGrep: THttpService\\.builder\\(\\)|THttpService\\.of\\(\nScope: **/*.java\n```\n\nSame check — look for `useBlockingTaskExecutor(true)`.\n\n### 1.6 Find Decorators\n\n```text\nGrep: SimpleDecoratingHttpService|DecoratingHttpServiceFunction|\\.decorate\\(\nScope: **/*.java\n```\n\nEvery decorator's `serve()` method runs on the event loop. List them all.\n\n### 1.7 Find Custom Executors and Thread Pools\n\n```text\nGrep: blockingTaskExecutor\\(\\)|ctx\\.eventLoop\\(\\)|Executors\\.new|new ThreadPoolExecutor|new ForkJoinPool|ExecutorService\nScope: **/*.java\n```\n\nDocument all custom thread pools and how they're used. These are the \"safe zones\" where blocking is OK.\n\n### 1.8 Find Offloading Patterns\n\n```text\nGrep: supplyAsync|thenApplyAsync|thenComposeAsync|thenAcceptAsync|ctx\\.blockingTaskExecutor|makeContextAware\nScope: **/*.java\n```\n\nUnderstand the project's conventions for offloading work from the event loop. Look for:\n- **Base class methods** that submit work to a blocking executor (e.g., a `runBlocking(Supplier)` method in a base handler)\n- **Utility classes** that wrap `ctx.blockingTaskExecutor()` with additional logic (cancellation, metrics, tracing)\n- **Project-specific annotations** that trigger blocking executor dispatch (beyond Armeria's `@Blocking`)\n\nA wrapper is \"safe\" if it provably submits the lambda/callable to a non-event-loop executor before the lambda body executes. Read the wrapper's implementation to verify — don't trust method names alone.\n\n### 1.9 Build Architecture Map\n\nSynthesize findings into a concise map:\n- **Event loop entry points**: List of classes/methods where the event loop calls into application code\n- **Safe execution wrappers**: Project-specific methods that offload to blocking executors\n- **Thread pool inventory**: All thread pools, their sizing, and purpose\n- **Decorator chain**: The order in which decorators wrap services\n\n---\n\n## Phase 2: Systematic Detection — 7 Categories\n\nSearch the entire project (or scoped path) for each category of blocking pattern. Exclude test files (`**/test/**`, `**/tests/**`) and generated code (`**/generated/**`, `**/target/**`, `**/build/**`).\n\nSee [detection-patterns.md](references/detection-patterns.md) for the complete search patterns and triage guidance for all 7 categories:\n\n1. **Direct Blocking Calls** — `.join()`, `.get()`, `Thread.sleep()`, `.await()`, `.lock()`, etc.\n2. **Blocking I/O Operations** — File I/O, network, JDBC, serialization, encryption\n3. **Synchronization** — `synchronized`, `ReentrantLock`, `StampedLock` on request paths\n4. **CompletableFuture Continuations on Wrong Executor** — `.thenApply()` without `Async` where completing thread is event loop\n5. **Thread Pool Misconfiguration** — Unbounded pools, missing shutdown, undersized blocking executor\n6. **Missing Armeria Idioms** — Missing `@Blocking`, missing `useBlockingTaskExecutor(true)`, blocking cancel handlers, missing context propagation\n7. **Transitive / Hidden Blocking** — DNS resolution, class loading, synchronous logging appenders\n\n---\n\n## Phase 3: Call Chain Analysis\n\nFor each finding from Phase 2, perform call chain analysis to determine the thread context.\n\n### Analysis Steps\n\n1. **Identify enclosing method** — What method contains the potentially blocking call?\n\n2. **Find all callers** — Grep for the method name across the project:\n   ```text\n   Grep: methodName\\(\n   Scope: **/*.java\n   ```\n   When handling overloaded methods (same name, different signatures), read each call site to confirm it calls the blocking variant. Also check superclass/interface hierarchies — a `serve()` override in a subclass is still called from the event loop if the parent is an `HttpService`.\n\n3. **Trace to entry point** — Follow the call chain upward until you reach one of:\n   - An Armeria entry point (`serve()`, `@Get`/`@Post` handler, gRPC handler, decorator)\n   - A thread pool submission (`blockingTaskExecutor().execute(...)`, `supplyAsync(...)`, etc.)\n   - A framework callback (`whenRequestCancelling`, `setOnCancelHandler`)\n   - A lambda/method reference passed to an executor (`.submit(this::methodName)`)\n\n   **Handling ambiguity**: If a method is called from BOTH event loop paths AND thread pool paths, classify based on the worst case (event loop path). Note both paths in the finding.\n\n4. **Determine thread context** — At the entry point:\n   - Is there a `@Blocking` annotation? → Safe\n   - Is there `useBlockingTaskExecutor(true)`? → Safe\n   - Is it inside a `supplyAsync(..., blockingExecutor)` call? → Safe\n   - Is it inside a project-specific safe wrapper (discovered in Phase 1.8)? → Safe\n   - None of the above? → **Event loop**\n\n5. **Classify severity using this decision tree:**\n\n```text\nIs the blocking call provably inside a blocking executor or thread pool?\n├── YES → SAFE (exclude from report or list separately)\n├── NO, it's provably on the event loop:\n│   ├── Is it an I/O operation, .join(), .get() without timeout, or Thread.sleep()?\n│   │   └── YES → CRITICAL\n│   ├── Is it .get(timeout, unit) or .await(timeout, unit)?\n│   │   └── YES → HIGH (blocks event loop for the timeout duration)\n│   ├── Is it a synchronized block or lock?\n│   │   ├── Can it contend with a thread that holds the lock for >1ms?\n│   │   │   └── YES → HIGH\n│   │   └── Low contention, very fast critical section?\n│   │       └── LOW\n│   └── Is it a very fast operation (System.getProperty, in-memory map lookup)?\n│       └── LOW\n└── UNDETERMINED (e.g., CF continuation where completing thread varies):\n    ├── Could the continuation body block?\n    │   ├── YES → MEDIUM (must verify at runtime or refactor to be safe)\n    │   └── NO (continuation is pure computation) → LOW\n    └── Is the source future always completed by a thread pool?\n        └── YES → SAFE\n```\n\n**Key distinction**: MEDIUM means \"we cannot statically prove the thread context\" — it requires either runtime verification (add logging/assertion) or defensive refactoring (use `*Async` variant with explicit executor).\n\n---\n\n## Phase 4: Fix Plan Templates\n\nFor each non-SAFE finding, recommend one of the fix approaches documented in [fix-templates.md](references/fix-templates.md):\n\n| Template | When to use |\n|----------|-------------|\n| **FP-1** | Replace `.join()`/`.get()` with async chaining |\n| **FP-2** | Offload blocking work to `ctx.blockingTaskExecutor()` |\n| **FP-3** | Add `@Blocking` annotation to annotated service methods |\n| **FP-4** | Enable `useBlockingTaskExecutor(true)` for gRPC/Thrift services |\n| **FP-5** | Use `*Async` CF variant with explicit executor |\n| **FP-6** | Add timeout to unavoidable blocking |\n| **FP-7** | Make thread pool initialization thread-safe |\n| **FP-8** | Wire context propagation with `makeContextAware` |\n\n**Document the recommendation — do not apply it.**\n\n---\n\n## Phase 5: Produce the Report\n\n### Per-Finding Format\n\n```markdown\n## Finding [ID]: [Short descriptive title]\n\n**Severity**: CRITICAL / HIGH / MEDIUM / LOW\n**File**: [relative path]:[line number]\n**Thread context**: Event loop / Blocking executor / Undetermined\n**Detection category**: [1-7 with category name]\n\n### Description\n[What the code does and why it's a blocking risk on the event loop]\n\n### Call chain\n[Armeria entry point (event loop)] → [Method A] → [Method B] → [blocking call at line X]\n\n### Recommended fix\n[Reference FP-1 through FP-8, with specific application to this case:\n which method to change, what the new code should look like, which executor to use]\n\n### Risk assessment\n[What could break when applying the fix, edge cases to consider, tests to run]\n```\n\n### Summary Table\n\nAt the end of all findings, produce a summary:\n\n```markdown\n| # | Severity | File | Line | Category | Blocking Pattern | Fix |\n|---|----------|------|------|----------|-----------------|-----|\n| 1 | CRITICAL | FooService.java | 42 | 1 - Direct blocking | `.join()` | FP-1 |\n| 2 | HIGH | BarHandler.java | 88 | 2 - I/O | FileInputStream | FP-2 |\n| ... | ... | ... | ... | ... | ... | ... |\n```\n\n### Statistics\n\n```markdown\n- Total findings: [N]\n- CRITICAL: [N] | HIGH: [N] | MEDIUM: [N] | LOW: [N] | SAFE (excluded): [N]\n- By category: Cat.1: [N] | Cat.2: [N] | Cat.3: [N] | Cat.4: [N] | Cat.5: [N] | Cat.6: [N] | Cat.7: [N]\n```\n\n### Follow-Up Recommendations\n\nAlways include these recommendations regardless of findings:\n\n1. **Runtime detection**: Consider adding [BlockHound](https://github.com/reactor/BlockHound) to the test suite — it detects blocking calls on non-blocking threads at runtime\n2. **Async logging**: Verify that the logging framework uses async appenders in production to avoid blocking on `log.*()` calls\n3. **Event loop monitoring**: Enable Armeria's built-in event loop blocking detection:\n   ```text\n   -Dcom.linecorp.armeria.reportBlockedEventLoop=true\n   ```\n4. **Code review checklist**: Add \"Does this handler offload blocking work?\" to the team's code review checklist for Armeria services\n\n---\n\n## Execution Checklist\n\nFollow this checklist to ensure nothing is skipped. Check off each item as you complete it.\n\n### Phase 1: Discovery\n- [ ] 1.1 — Found and documented Armeria server configuration(s)\n- [ ] 1.2 — Listed all `HttpService` implementations\n- [ ] 1.3 — Listed all annotated services, noted `@Blocking` presence/absence\n- [ ] 1.4 — Listed all gRPC services, noted `useBlockingTaskExecutor` presence/absence\n- [ ] 1.5 — Listed all Thrift services (if any)\n- [ ] 1.6 — Listed all decorators\n- [ ] 1.7 — Documented all custom executors and thread pools\n- [ ] 1.8 — Identified project-specific offloading patterns\n- [ ] 1.9 — Built architecture map (entry points, safe wrappers, thread pools)\n\n### Phase 2: Detection\n- [ ] Category 1 — Searched for all direct blocking calls\n- [ ] Category 2 — Searched for all blocking I/O operations\n- [ ] Category 3 — Searched for synchronization on request paths\n- [ ] Category 4 — Audited CompletableFuture continuation executors\n- [ ] Category 5 — Audited thread pool configurations\n- [ ] Category 6 — Checked for missing Armeria idioms (@Blocking, useBlockingTaskExecutor, cancel handlers, context propagation)\n- [ ] Category 7 — Searched for transitive/hidden blocking (DNS, class loading, logging config)\n\n### Phase 3: Analysis\n- [ ] Traced call chains for ALL non-obvious findings\n- [ ] Classified every finding by severity\n- [ ] Excluded SAFE findings from the report (or listed them separately)\n\n### Phase 4: Fix Planning\n- [ ] Assigned a fix template (FP-1 through FP-8) to each non-SAFE finding\n- [ ] Documented specific fix details for each finding\n\n### Phase 5: Reporting\n- [ ] Produced per-finding reports\n- [ ] Produced summary table\n- [ ] Produced statistics\n- [ ] Included follow-up recommendations\n\n---\n\n## Common Mistakes to Avoid\n\n- **Stopping at grep hits** — A grep hit is not a finding. You must trace the call chain to determine thread context. Many `.join()` calls are inside thread pool code and are perfectly safe.\n- **Ignoring non-async CF continuations** — `.thenApply()` without `Async` is the most subtle source of event loop blocking. The continuation runs on whatever thread completes the source future.\n- **Missing cancel handlers** — Cancel handler lambdas always run on the event loop. They're easy to overlook but are a common source of blocking.\n- **Assuming `synchronized` is always bad** — It's only a problem if (a) it's on a request path reachable from the event loop AND (b) the lock can be contended by another thread holding it for a long time.\n- **Forgetting transitive blocking** — A clean `serve()` method that calls `helperMethod()` which calls `utilMethod()` which calls `.join()` is still blocking the event loop.\n- **Ignoring logging** — If the logging framework uses synchronous file appenders, every `log.info()` on the event loop is a blocking I/O operation. This is a configuration issue, not a code issue, but still critical.\n\n<!-- File: .claude/skills/release-note/scripts/lint-release-note.sh (Tokens: ~800 | Category: claude-rule) -->\n#!/usr/bin/env bash\n# Mechanical checks for a release note. Prose rules only fire if someone remembers to apply them\n# document-wide; these run. Exits non-zero on any finding.\n#\n#   .claude/skills/release-note/scripts/lint-release-note.sh site/src/content/release-notes/1.41.0.mdx\n\nset -uo pipefail\n\nf=\"${1:?usage: lint-release-note.sh <release-note.mdx>}\"\n[ -f \"$f\" ] || { echo \"no such file: $f\" >&2; exit 2; }\n\nfail=0\n# check <description> <grep-output>; prints each matched line indented, preserving line breaks.\ncheck() {\n  [ -n \"$2\" ] || return 0\n  fail=1\n  printf '\\n✗ %s\\n' \"$1\"\n  printf '%s\\n' \"$2\" | sed 's/^/    /'\n}\n\n# --- type links -------------------------------------------------------------\n# A class link glued to a backticked method is not a link to that method.\nhits=$(grep -n '\\](type)`\\.' \"$f\" || true)\ncheck \"class link + backtick method (write [Foo#bar(Baz)](type) as one unit)\" \"$hits\"\n\n# api-index keys carry no space after commas; a space yields a dead type://# link.\nhits=$(grep -nE '\\[[A-Za-z0-9_.]+#[^]]*, [^]]*\\]\\(type\\)' \"$f\" || true)\ncheck \"space after comma in a method type link (silently dead)\" \"$hits\"\n\n# --- links ------------------------------------------------------------------\nhits=$(grep -n '](https://armeria.dev/' \"$f\" || true)\ncheck \"absolute armeria.dev link (use a site-relative /docs/... path)\" \"$hits\"\n\n# --- leftovers from the generator ------------------------------------------\nhits=$(grep -n '^- N/A' \"$f\" || true)\ncheck \"empty section left as '- N/A' (delete the section)\" \"$hits\"\n\nhits=$(grep -n 'Maybe ignore' \"$f\" || true)\ncheck \"'🗑 Maybe ignore' section must be triaged away\" \"$hits\"\n\nhits=$(grep -nE '^\\s*\\*\\s' \"$f\" || true)\ncheck \"'*' bullet (use '-')\" \"$hits\"\n\n# --- dependencies -----------------------------------------------------------\ndeps=$(sed -n '/^## ⛓ Dependencies/,/^## /p' \"$f\")\nhits=$(printf '%s' \"$deps\" | grep -n ' -> ' || true)\ncheck \"ASCII arrow in Dependencies (use →)\" \"$hits\"\n\n# Strip stable-release metadata only; -RC1/-M1/-SNAPSHOT identify distinct builds and must stay.\nhits=$(printf '%s' \"$deps\" | grep -nE '[0-9](\\.(Final|RELEASE|GA)|-GA|\\.v[0-9]{6,})' || true)\ncheck \"release qualifier left in a version (.Final/.RELEASE/-GA/.vYYYYMMDD)\" \"$hits\"\n\nhits=$(printf '%s' \"$deps\" | grep -n '^- Build' || true)\ncheck \"build-only dependency section leaked in\" \"$hits\"\n\n# --- formatting -------------------------------------------------------------\nhits=$(awk 'length>104 {printf \"%d: (%d chars)\\n\", FNR, length}' \"$f\" || true)\ncheck \"line longer than 104 chars\" \"$hits\"\n\nif [ $fail -eq 0 ]; then\n  echo \"✓ $(basename \"$f\"): all mechanical checks passed\"\nelse\n  printf '\\nSome checks failed. Note that a bad type link never fails the site build —\\n'\n  printf 'it renders as a blank type://# link, so this script is the only thing that catches it.\\n'\nfi\nexit $fail\n\n<!-- File: .claude-plugin/marketplace.json (Tokens: ~139 | Category: marketplace) -->\n{\n  \"name\": \"armeria\",\n  \"owner\": {\n    \"name\": \"armerian\",\n    \"email\": \"dl_armeria@linecorp.com\"\n  },\n  \"metadata\": {\n    \"description\": \"Claude plugin for the Armeria microservice framework\",\n    \"version\": \"0.0.1\"\n  },\n  \"plugins\": [\n    {\n      \"name\": \"assistant\",\n      \"source\": {\n        \"source\": \"git-subdir\",\n        \"url\": \"https://github.com/line/armeria.git\",\n        \"path\": \"tools/claude-plugins/assistant\"\n      }\n    }\n  ]\n}\n\n<!-- File: tools/claude-plugins/assistant/.claude-plugin/plugin.json (Tokens: ~9 | Category: plugin-manifest) -->\n{\n  \"name\": \"assistant\"\n}\n</yakaai_skills>"}