Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ SonarQube Agentic Analysis (SQAA) is delivered per agent:
- **Codex** uses a `PostToolUse` hook on `apply_patch` running `sonar hook codex-post-tool-use --project <key>` for change-set DEEP analysis after each patch (`codex-post-tool-use.ts`).
- **Copilot, Cursor, and Antigravity use instructions/rules only** — Cursor's `afterFileEdit` hook is fire-and-forget (it cannot return `additionalContext` to the conversation), so a post-edit hook would be useless for SQAA. All three write the shared `buildSqaaSectionBody(projectKey)` template (`integrate/_common/instructions-templates.ts`) telling the agent to run `sonar analyze agentic --project <key>` on modified files at end-of-turn. Cursor's `sqaa-instructions` feature (`integrate/cursor/declaration.ts`) wraps it in `.cursor/rules/sonar-agentic-analysis.mdc` with `alwaysApply: true` YAML front-matter; Copilot writes `.github/instructions/sonarqube.instructions.md`; **Antigravity** writes `.agents/rules/sonar-agentic-analysis.md` with `trigger: always_on` front-matter per [Antigravity Rules](https://antigravity.google/docs/rules-workflows). Antigravity prompt-secrets uses workspace rules (`.agents/rules/sonar-prompt-secrets.md`) and global rules (`~/.gemini/GEMINI.md` snippet) as separate integrate features.

SQAA is **project-scoped and opt-in**: integrate orchestrators call `resolveSqaaSetup()` and only install when the org is entitled and a project key is known; `resolveSqaaSetup` owns the user-facing promotion / `--global` skip messaging. Claude/Cursor/Copilot/Antigravity removers key on the `# SonarQube Agentic Analysis protocol` managed marker so teardown only deletes content the CLI wrote.
SQAA is **project-scoped and opt-in**: integrate orchestrators call `resolveSqaaSetup()` and only install when the org is entitled and a project key is known; `resolveSqaaSetup` owns the user-facing promotion / `--global` skip messaging. Claude/Cursor/Copilot/Antigravity removers key on the `# SonarQube Agentic Analysis protocol` managed marker so teardown only deletes content the CLI wrote. SQAA project-key resolution is git-worktree-aware end to end: integrate records the `sonar-sqaa` extension's `projectRoot` as the repository's **main working tree** (`resolveMainWorktreeRoot`), and at runtime `resolveSqaaProjectKey` (`analyze/sqaa-auth.ts`) looks up the `agentExtensions` registry trying both the current repo root and its main-working-tree equivalent (`resolveWorktreeEquivalentPaths`). So `sonar analyze agentic` resolves the key from any worktree regardless of which worktree integrate ran in. (`projectRoot` here is an identity key only — SQAA teardown does not use it.)

### Context Augmentation

`sonar context [action] [args...]` is a passthrough to the locally-installed `sonar-context-augmentation` binary (CAG). It forwards args verbatim, propagates the child exit code, and injects context through `SONAR_CONTEXT_ORGANIZATION`, `SONAR_CONTEXT_PROJECT`, `SONAR_CONTEXT_TOKEN`, and `SONAR_CONTEXT_URL` env vars. Every CAG spawn (including the `--help` path and the integrate/post-update flows) also carries `SONAR_CONTEXT_INVOCATION_ID` — the per-CLI-process correlation id from `src/lib/invocation-id.ts`, shared with telemetry's `invocation_id` and forwarded by CAG to its daemon as the `x-sonar-invocation-id` header. The passthrough resolves project context from the recorded declarative CAG feature state for the current project rather than running full project auto-discovery. Implementation in `src/cli/commands/context/`. The binary is downloaded by `sonar integrate claude` / `sonar integrate copilot` / `sonar integrate codex` / `sonar integrate antigravity` / `sonar integrate cursor` (skip with `--skip-context`); `sonar context` itself never auto-installs and emits a clear "not installed" error pointing the user back to integrate. `--global` integrations also skip CAG setup; install it by re-running `sonar integrate <agent>` from a project directory. The `--global` skip notice (`Skipping Context Augmentation: not supported with --global. Re-run without --global from a project directory to install it there.`) is only emitted when the org is actually entitled to CAG — unentitled orgs skip silently. The CLI owns the agent skill file declaratively as a `wholeFile` resource inside a single CAG feature and renders it by calling `sonar-context-augmentation tool print-skill --invocation-prefix "sonar context" --sca-enabled=<resolved>`; the rendered file is written to `.claude/skills/sonar-context-augmentation/SKILL.md`, `.github/skills/sonar-context-augmentation/SKILL.md`, or `.agents/skills/sonar-context-augmentation/SKILL.md` depending on the agent. **Cursor** also writes to `.agents/skills/sonar-context-augmentation/SKILL.md` — the shared cross-tool skills directory it reads alongside Codex and Antigravity — rather than a Cursor-private `.cursor/skills` copy, so the three tools share one skill instead of duplicating it. Cursor loads skills on demand (distinct from Cursor's SQAA delivery, which is an always-applied `.cursor/rules/*.mdc` rule because that protocol must run every turn). That same feature also declares a `tool integrate --invocation-prefix "sonar context"` operation, but the operation is install-only: the framework passes `executionMode=install|update` into feature contexts, and CAG uses `shouldApply` so `tool integrate` runs during `sonar integrate <agent>` but not during post-update refreshes. Post-update still reinstalls the shared dependency, best-effort runs `sonar-context-augmentation tool stop --all` against the previously-installed binary before replacing it, and refreshes the skill file. Replay failures are debug-logged so they do not abort CLI startup.
`sonar context [action] [args...]` is a passthrough to the locally-installed `sonar-context-augmentation` binary (CAG). It forwards args verbatim, propagates the child exit code, and injects context through `SONAR_CONTEXT_ORGANIZATION`, `SONAR_CONTEXT_PROJECT`, `SONAR_CONTEXT_TOKEN`, and `SONAR_CONTEXT_URL` env vars. Every CAG spawn (including the `--help` path and the integrate/post-update flows) also carries `SONAR_CONTEXT_INVOCATION_ID` — the per-CLI-process correlation id from `src/lib/invocation-id.ts`, shared with telemetry's `invocation_id` and forwarded by CAG to its daemon as the `x-sonar-invocation-id` header. The passthrough resolves project context from the recorded declarative CAG feature state for the current project rather than running full project auto-discovery. This lookup is git-worktree-aware so you can `sonar integrate` once and use `sonar context` from any worktree (including ones created later, and after the integrate-time worktree is removed): integrate records a stable `attrs.repoRoot` = the repository's **main working tree** (resolved via `resolveMainWorktreeRoot` in `src/lib/project-workspace/git-worktree.ts`, which reads `git worktree list --porcelain`) while keeping `targetRoot` = the physical install dir for teardown; the passthrough matches the current directory (mapped to its main-worktree equivalent via `resolveWorktreeEquivalentPaths`) against `attrs.repoRoot ?? targetRoot`, falling back to `targetRoot` for state written by older CLI versions. When a recorded integration matches, the passthrough also sets `SONAR_CONTEXT_WORKSPACE_ROOT` to that integration's recorded integrated folder (`attrs.repoRoot ?? targetRoot` — the repository's main working tree) so CAG can locate or lazily create its own per-workspace daemon folder. Because this value comes from recorded state rather than the current directory, it is correct from any subdirectory or linked worktree and does not require git at call time (e.g. a non-git project invoked from a subfolder); it is left unset (and any inherited value dropped) when no integration matches. Implementation in `src/cli/commands/context/`. The binary is downloaded by `sonar integrate claude` / `sonar integrate copilot` / `sonar integrate codex` / `sonar integrate antigravity` / `sonar integrate cursor` (skip with `--skip-context`); `sonar context` itself never auto-installs and emits a clear "not installed" error pointing the user back to integrate. `--global` integrations also skip CAG setup; install it by re-running `sonar integrate <agent>` from a project directory. The `--global` skip notice (`Skipping Context Augmentation: not supported with --global. Re-run without --global from a project directory to install it there.`) is only emitted when the org is actually entitled to CAG — unentitled orgs skip silently. The CLI owns the agent skill file declaratively as a `wholeFile` resource inside a single CAG feature and renders it by calling `sonar-context-augmentation tool print-skill --invocation-prefix "sonar context" --sca-enabled=<resolved>`; the rendered file is written to `.claude/skills/sonar-context-augmentation/SKILL.md`, `.github/skills/sonar-context-augmentation/SKILL.md`, or `.agents/skills/sonar-context-augmentation/SKILL.md` depending on the agent. **Cursor** also writes to `.agents/skills/sonar-context-augmentation/SKILL.md` — the shared cross-tool skills directory it reads alongside Codex and Antigravity — rather than a Cursor-private `.cursor/skills` copy, so the three tools share one skill instead of duplicating it. Cursor loads skills on demand (distinct from Cursor's SQAA delivery, which is an always-applied `.cursor/rules/*.mdc` rule because that protocol must run every turn). That same feature also declares a `tool integrate --invocation-prefix "sonar context"` operation, but the operation is install-only: the framework passes `executionMode=install|update` into feature contexts, and CAG uses `shouldApply` so `tool integrate` runs during `sonar integrate <agent>` but not during post-update refreshes. Post-update still reinstalls the shared dependency, best-effort runs `sonar-context-augmentation tool stop --all` against the previously-installed binary before replacing it, and refreshes the skill file. Replay failures are debug-logged so they do not abort CLI startup.

`--help`, `-h`, and bare `sonar context` (no action) are forwarded to CAG.

Expand Down Expand Up @@ -176,3 +176,35 @@ When adding, removing, or changing commands, scripts, or project structure, upda

The docs site is generated from the CLI source — do not edit `commands.json`, `llms.txt`, or `sitemap.xml` by hand. This is done by automation post-release. The docs generator also stamps the current package version into the nav badge in `index.html` and `commands.html`, plus the JSON-LD `softwareVersion` and `meta-version` in `index.html`.
Keep a fixed `<base href="/sonarqube/cli/">` in the docs HTML so production resolves relative assets and internal links under the public mount point. This is intentionally production-oriented: Netlify deploy previews served from `/` will not work with that fixed base.

<!-- sonar:begin:sonarqube-agentic-analysis-protocol -->
# SonarQube Agentic Analysis protocol

SonarQube Agentic Analysis is the final confirmation layer at the end of every turn in which you wrote to one or more files in the workspace (create, edit, patch, format — any tool call that changed file contents on disk).

**Trigger:** the moment before you send your final reply for the turn.

When the trigger fires, you **MUST** run SonarQube Agentic Analysis on the full set of files you modified this turn — **one command, not a per-file loop**.

**Preferred:** pass every path you modified this turn in a single invocation (repeat `--file` for each path). Use paths **relative to the project root** (e.g. `src/foo.ts`). For end-of-turn analysis, **always** pass `--depth DEEP` — even when you changed only one file — so cross-file findings are included.

```bash
sonar analyze agentic --project SonarSource_sonarqube-cli --depth DEEP --file <path/to/file1> --file <path/to/file2>
```

**Fallback:** when you cannot reliably list every modified path (e.g. you did not track them, or git state is the only source of truth), use the git change set with DEEP analysis:

```bash
sonar analyze agentic --project SonarSource_sonarqube-cli --depth DEEP
```

Per-edit hooks run faster STANDARD analysis. End-of-turn analysis must always use `--depth DEEP` (including a single `--file`). Use `--depth STANDARD` only for optional faster passes — never for the mandatory end-of-turn run.

Non-negotiable rules:

1. Mid-turn edits do **not** need SonarQube Agentic Analysis — only the final on-disk state matters. Skipping SonarQube Agentic Analysis at end-of-turn is a protocol violation.
2. Run SonarQube Agentic Analysis **before** sending your final reply, marking the task done, or handing control back to the user.
3. If SonarQube Agentic Analysis reports issues on lines you touched in this turn, fix them, then re-run SonarQube Agentic Analysis on the same scope (change set or explicit file list). Repeat until clean (or only pre-existing findings on lines you did not touch remain). Pre-existing findings on untouched lines are out of scope — do not "fix" them unless the user asked.
4. If SonarQube Agentic Analysis is skipped (no SonarQube Cloud connection, or no project configured), state the skip reason to the user once and continue — do not retry.
5. Do not suppress, summarize away, or omit SonarQube Agentic Analysis findings from your reply. Surface them verbatim.
<!-- sonar:end:sonarqube-agentic-analysis-protocol -->
14 changes: 13 additions & 1 deletion src/cli/commands/_common/context-augmentation-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,21 @@ export interface ContextAugmentationEnvContext {
projectKey?: string;
serverUrl?: string;
token?: string;
/**
* The repository's main working tree (the recorded integrated folder), passed
* to CAG so it can locate (or lazily create) the per-workspace daemon folder
* consistently from any git worktree. Set only when the repository's main
* working tree has a recorded integration.
*/
workspaceDir?: string;
Comment thread
gitar-bot[bot] marked this conversation as resolved.
}

type ContextAugmentationEnvKey =
| 'SONAR_CONTEXT_ORGANIZATION'
| 'SONAR_CONTEXT_PROJECT'
| 'SONAR_CONTEXT_TOKEN'
| 'SONAR_CONTEXT_URL';
| 'SONAR_CONTEXT_URL'
| 'SONAR_CONTEXT_WORKSPACE_ROOT';

/**
* Build the env passed to sonar-context-augmentation subprocesses.
Expand All @@ -59,6 +67,7 @@ export function buildContextAugmentationEnv(
setContextEnvValue(env, 'SONAR_CONTEXT_PROJECT', context.projectKey);
setContextEnvValue(env, 'SONAR_CONTEXT_TOKEN', context.token);
setContextEnvValue(env, 'SONAR_CONTEXT_URL', context.serverUrl);
setContextEnvValue(env, 'SONAR_CONTEXT_WORKSPACE_ROOT', context.workspaceDir);

return env;
}
Expand Down Expand Up @@ -87,5 +96,8 @@ function setContextEnvValue(
case 'SONAR_CONTEXT_URL':
delete env.SONAR_CONTEXT_URL;
return;
case 'SONAR_CONTEXT_WORKSPACE_ROOT':
delete env.SONAR_CONTEXT_WORKSPACE_ROOT;
return;
}
}
44 changes: 14 additions & 30 deletions src/cli/commands/analyze/sqaa-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,9 @@

// Auth and project-key resolution for SQAA commands.

import { resolve } from 'node:path';

import type { ResolvedAuth } from '../../../lib/auth-resolver';
import logger from '../../../lib/logger';
import { spawnProcess } from '../../../lib/process';
import { resolveWorktreeRootCandidates } from '../../../lib/project-workspace/git-worktree';
import { loadState } from '../../../lib/repository/state-repository';
import { canonicalProjectRoot } from '../../../lib/state-manager';
import { blank, confirmPrompt, text, warn } from '../../../ui';
Expand Down Expand Up @@ -110,28 +108,30 @@ export function resolveCloudAuth(
* Look up the project key for the current project from the declarative
* integration state (`integrations.installed`).
*
* The SQAA hook feature is recorded per install target root (the directory
* passed to `sonar integrate claude`), so when the user runs SQAA from a
* subdirectory we resolve the git repository top-level first — otherwise
* `process.cwd()` is a non-match against the recorded root and we incorrectly
* skip with "no project configured".
*
* Falls back to `process.cwd()` when not inside a git repository so the
* single-file path still works outside git.
* The SQAA hook feature is recorded per install target root, so
* `resolveWorktreeRootCandidates` maps the current directory to its working-tree
* root(s) — the current tree, then the main tree — via a single `git worktree
* list` call. This lets SQAA resolve the key from a subdirectory and from any
* linked worktree, matching state recorded in the main checkout. Falls back to
* `process.cwd()` when not inside a git repository so the single-file path still
* works outside git.
*/
export async function resolveSqaaProjectKey(projectRoot?: string): Promise<string | null> {
try {
const root = canonicalProjectRoot(projectRoot ?? (await tryResolveRepoRoot(process.cwd())));
const state = loadState();

const claude = state.integrations.installed.find(
(integration) => integration.integrationId === CLAUDE_INTEGRATION_ID,
);
// Try the current working tree's root, then the main working tree, so a
// linked worktree still resolves state recorded in the main checkout.
const candidates = new Set(
(await resolveWorktreeRootCandidates(projectRoot ?? process.cwd())).map(canonicalProjectRoot),
);
const sqaaFeature = claude?.features.find(
(feature) =>
feature.featureId === SQAA_HOOK_FEATURE_ID &&
feature.scope === 'project' &&
canonicalProjectRoot(feature.targetRoot) === root,
candidates.has(canonicalProjectRoot(feature.targetRoot)),
);
Comment on lines +127 to 135

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: Ambiguous match when multiple worktrees register SQAA features

resolveSqaaProjectKey builds a Set of candidate roots (current worktree root + main worktree root) and then uses claude.features.find(...) to return the first project-scoped sonar-sqaa-hook feature whose targetRoot matches any candidate. If the same repo has SQAA integrated in more than one worktree (e.g. both the main checkout and a linked worktree register a feature with different projectKeys), the candidate Set can match either, and find returns whichever feature happens to appear first in the features array (installation order). This yields a non-obvious, order-dependent project key.

Impact is low: the common case is a single project-scoped SQAA feature recorded against the main checkout, and the tests cover that path. But if you want deterministic behavior favoring the current working tree over the main tree, consider preserving the candidate ordering and selecting the feature matching the earliest candidate rather than treating all candidates as an unordered Set.

Resolve the project key deterministically by preferring the current worktree root before falling back to the main tree.:

const candidates = (
  await resolveWorktreeRootCandidates(projectRoot ?? process.cwd())
).map(canonicalProjectRoot);
// Prefer the current working tree over the main tree by honoring candidate order.
let sqaaFeature;
for (const candidate of candidates) {
  sqaaFeature = claude?.features.find(
    (feature) =>
      feature.featureId === SQAA_HOOK_FEATURE_ID &&
      feature.scope === 'project' &&
      canonicalProjectRoot(feature.targetRoot) === candidate,
  );
  if (sqaaFeature) break;
}
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎


const projectKey = sqaaFeature?.attrs?.projectKey;
Expand All @@ -147,22 +147,6 @@ export async function resolveSqaaProjectKey(projectRoot?: string): Promise<strin
}
}

/**
* Resolve the git repository top-level for `cwd`, falling back to `cwd` itself
* when not inside a git repository (so non-git workflows still work).
*/
async function tryResolveRepoRoot(cwd: string): Promise<string> {
try {
const result = await spawnProcess('git', ['rev-parse', '--show-toplevel'], { cwd });
if (result.exitCode === 0) {
return resolve(result.stdout.trim());
}
} catch {
// git not installed or otherwise unavailable — fall through to cwd.
}
return cwd;
}

/**
* Warn about a large change set and ask the user to confirm.
* In non-interactive contexts (no stdin TTY — e.g. CI/agent runs), prints a
Expand Down
Loading