Visitar URL original
fix(review): an honest PR comment: real lint-only banner, AI lane status, PR intent, project_dir by RaghwendraSingh · Pull Request #1720 · AltimateAI/altimate-code · GitHub
Skip to content

fix(review): an honest PR comment: real lint-only banner, AI lane status, PR intent, project_dir - #1720

Open
RaghwendraSingh wants to merge 1 commit into
fix/review-scope-difffrom
fix/review-ci-experience
Open

RaghwendraSingh wants to merge 1 commit into
fix/review-scope-difffrom
fix/review-ci-experience

Conversation

@RaghwendraSingh

@RaghwendraSingh RaghwendraSingh commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #

Stacked on #1719; please review and merge that one first. Takes ideas from the closed #1241, not its code.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

The review comment could misstate what it checked:

  • Lint-only banner: a single undecidable finding set degraded, so every manifest-backed run said "Lint-only run — no dbt manifest". Now it shows only when no changed model resolved against a manifest. Undecidable findings, models without compiled SQL, and files outside the project each get their own line.
  • Silent AI lane: the lane returned [] on every failure, so an expired key, a timeout, or a missing model looked like "no AI findings". It now reports ok | skipped | timeout | error with a reason (credentials masked), the model, and duration. These land in a new signed ai field and in the comment. The timeout now scales with prompt size (60 s + 1 s per 2K chars, capped at 5 min) instead of a fixed 60 s. --ai-model provider/model picks the model explicitly.
  • No PR intent: prTitle/prBody existed but nothing passed them. The CLI now reads them from the GitHub pull_request event, or from GitLab's CI_MERGE_REQUEST_TITLE/_DESCRIPTION.
  • Action: new project_dir input, mapped to --cwd, for monorepos.
  • Docs: a table of what each input unlocks (fetch depth, manifest, compiled SQL, target-base/, catalog.json) and a snippet for compiling the base.
  • Telemetry: review_run gains undecided, unverified_models, ai_state, ai_duration_ms, base_source, out_of_scope_files.

Open question, deliberately not changed: --no-ai still auto-registers the free tier, which is a network call. A release test asserts that on purpose, so the decision is the maintainers'. Changing it is a one-line guard.

How did you verify your code works?

  • New test/altimate/review-ci-honesty.test.ts (13 tests):
    • banner with and without a manifest
    • AI lane: disabled, outside the tier, timeout (signature still verifies), throwing, ok with model and selector, legacy array return
    • timeout scaling
    • PR context from GitHub and GitLab env, and its forwarding to the lane
  • 1,859 tests across the review files and release gates pass. bun typecheck is clean.

Cost side effects

None. The AI lane's timeout ceiling can rise from 60 s to 5 min on very large prompts, but the number of model calls doesn't change (still at most one per review).

Screenshots / recordings

N/A (CLI).

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR
  • I checked for cost side effects (model calls, queries or cloud resources that grow with user data) and described them, or wrote "none"

🤖 Generated with Claude Code


Summary by cubic

Makes the dbt PR review comment candid about what it checked. Previously a single undecidable finding labeled any manifest-backed run as "Lint-only", and the advisory AI lane returned [] on every failure, so an expired key, timeout, or missing model looked like "no AI findings".

Review honesty

  • The lint-only banner now shows only when no changed model resolved against a manifest; undecidable findings, models without compiled SQL, and out-of-scope files each get their own line.
  • The AI lane now reports ok | skipped | timeout | error with a masked reason, model, and duration, on a new signed ai envelope field and in the comment; it never fails silently.
  • The AI timeout scales with prompt size (60 s + 1 s per 2K chars, capped at 5 min); --ai-model provider/model selects the model explicitly.
  • The CLI passes the PR title and body to the intent check, reading them from GitHub's pull_request event or GitLab's CI_MERGE_REQUEST_TITLE/_DESCRIPTION.

Inputs, docs, and telemetry

  • New project_dir input maps to --cwd for monorepos.
  • review_run telemetry gains undecided, unverified_models, ai_state, ai_duration_ms, base_source, and out_of_scope_files.
  • Docs now list what each input unlocks and how to compile the base into target-base/.

Deliberately unchanged: --no-ai still auto-registers the free tier, which is a network call; a release test asserts that, so the decision is the maintainers'. Model call count is unchanged (still at most one per review); only the timeout ceiling rises, to 5 minutes on very large prompts.

Written for commit 1fda92a. Summary will update on new commits.

View guided diff Turn on auto-fix

…tus, PR intent, `project_dir`

- Lint-only now means no changed model resolved against a manifest; an
  undecidable finding no longer flips the whole run. Undecidable findings,
  unverified models, out-of-scope files and AI-lane problems each get their
  own line in the summary.
- The advisory AI lane reports `ok | skipped | timeout | error` with a
  reason, model and duration on the signed envelope (`ai`) and in the
  comment; it never fails silently. The timeout scales with prompt size
  (60 s + 1 s per 2K chars, capped at 5 min). `--ai-model provider/model`
  selects the model explicitly.
- The CLI passes the PR title and description to the intent check (GitHub
  `pull_request` event, GitLab `CI_MERGE_REQUEST_TITLE`/`_DESCRIPTION`).
- The action gains `project_dir` (→ `--cwd`) for monorepos.
- Docs: what each input unlocks (fetch depth, manifest, compiled SQL,
  `target-base/`, `catalog.json`) and how to compile the base.
- `review_run` telemetry adds `undecided`, `unverified_models`, `ai_state`,
  `ai_duration_ms`, `base_source`, `out_of_scope_files`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2122aff6-90c4-46b2-bdb8-226046850e46

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

9 issues found across 11 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/opencode/src/altimate/telemetry/index.ts">

<violation number="1" location="packages/opencode/src/altimate/telemetry/index.ts:1606">
P3: `ai_state` is typed as `string`, but the AI lane only produces `ok | skipped | timeout | error`. Narrow the type to that union so the telemetry contract matches the envelope schema and typos are caught at compile time.</violation>
</file>

<file name="packages/opencode/test/altimate/review-ci-honesty.test.ts">

<violation number="1" location="packages/opencode/test/altimate/review-ci-honesty.test.ts:157">
P3: This test creates a temp directory and never removes it, so every run leaves a `review-event-*` directory behind. Remove it in a `finally` block (`fs.rm(dir, { recursive: true, force: true })`) or use the repo's tmpdir fixture with auto cleanup.</violation>
</file>

<file name="docs/docs/usage/dbt-pr-review.md">

<violation number="1" location="docs/docs/usage/dbt-pr-review.md:218">
P2: In a monorepo the base-compile example writes `target-base/` at the repo root and runs `dbt` from the repo root, but the reviewer reads `target-base/` relative to `project_dir`. Equivalence and column-breakage checks would silently fall back to not verified. Note that the base compile must target `<project_dir>/target-base` and run inside the project folder, or the Monorepos paragraph should say so.</violation>
</file>

<file name="packages/opencode/src/altimate/review/ai-review.ts">

<violation number="1" location="packages/opencode/src/altimate/review/ai-review.ts:66">
P2: `safeReason` still exposes credential formats that do not use the four dashed prefixes, so provider failures can disclose secrets in PR comments. Reuse the repository's credential/error redactor or mask credential-labeled values and credential-bearing URLs before truncating the reason.

(Based on your team's feedback about masking credential-stem error values.) [a5c5a10f-6a4a-4e42-8c2e-f8e01b48683d]</violation>
</file>

<file name="packages/opencode/src/altimate/review/orchestrate.ts">

<violation number="1" location="packages/opencode/src/altimate/review/orchestrate.ts:1418">
P2: Empty-scope reviews now emit an AI `skipped` status and coverage note even though there is no review scope or AI lane to skip. Guard the AI-status block on a non-empty review scope so empty reviews leave `ai` undefined.</violation>

<violation number="2" location="packages/opencode/src/altimate/review/orchestrate.ts:1444">
P2: AI callback failures bypass the redaction promised by `AiLaneStatus` and can publish raw error text in the signed envelope and PR comment. Apply the same credential-safe error formatter before storing `ai.reason`. 

(Based on your team's feedback about masking credential-bearing error text.) [173edeff-f795-42ca-9d7d-86524f63e58a]</violation>
</file>

<file name="github/review/action.yml">

<violation number="1" location="github/review/action.yml:189">
P2: A misspelled `project_dir` silently broadens a monorepo review to every changed file. Validate that the directory contains `dbt_project.yml`/`.yaml` before adding `--cwd`, or make the review fail closed when project discovery fails.</violation>
</file>

<file name="packages/opencode/src/altimate/review/format.ts">

<violation number="1" location="packages/opencode/src/altimate/review/format.ts:136">
P2: Mixed head/base compile failures are rendered with the first model’s reason for every model, which misstates review coverage. Group the entries by reason or render each model with its own reason.</violation>

<violation number="2" location="packages/opencode/src/altimate/review/format.ts:144">
P2: The PR comment does not report the AI lane’s available model and duration: successful runs show only the model, while failed or timed-out runs show only state and reason. Include model and duration in the rendered AI status.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

- name: Compile the base into target-base/
run: |
git worktree add ../base "origin/${{ github.base_ref }}"
(cd ../base && dbt deps && dbt compile --target-path "$GITHUB_WORKSPACE/target-base")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: In a monorepo the base-compile example writes target-base/ at the repo root and runs dbt from the repo root, but the reviewer reads target-base/ relative to project_dir. Equivalence and column-breakage checks would silently fall back to not verified. Note that the base compile must target <project_dir>/target-base and run inside the project folder, or the Monorepos paragraph should say so.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/docs/usage/dbt-pr-review.md, line 218:

<comment>In a monorepo the base-compile example writes `target-base/` at the repo root and runs `dbt` from the repo root, but the reviewer reads `target-base/` relative to `project_dir`. Equivalence and column-breakage checks would silently fall back to not verified. Note that the base compile must target `<project_dir>/target-base` and run inside the project folder, or the Monorepos paragraph should say so.</comment>

<file context>
@@ -199,6 +199,34 @@ jobs:
+      - name: Compile the base into target-base/
+        run: |
+          git worktree add ../base "origin/${{ github.base_ref }}"
+          (cd ../base && dbt deps && dbt compile --target-path "$GITHUB_WORKSPACE/target-base")
+```
+
</file context>

return raw
.replace(/\s+/g, " ")
.replace(/(bearer\s+)[^\s]+/gi, "$1***")
.replace(/\b(sk|pk|key|token)[-_][A-Za-z0-9_-]{8,}/gi, "***")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: safeReason still exposes credential formats that do not use the four dashed prefixes, so provider failures can disclose secrets in PR comments. Reuse the repository's credential/error redactor or mask credential-labeled values and credential-bearing URLs before truncating the reason.

(Based on your team's feedback about masking credential-stem error values.) [a5c5a10f-6a4a-4e42-8c2e-f8e01b48683d]

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/altimate/review/ai-review.ts, line 66:

<comment>`safeReason` still exposes credential formats that do not use the four dashed prefixes, so provider failures can disclose secrets in PR comments. Reuse the repository's credential/error redactor or mask credential-labeled values and credential-bearing URLs before truncating the reason.

(Based on your team's feedback about masking credential-stem error values.) [a5c5a10f-6a4a-4e42-8c2e-f8e01b48683d]</comment>

<file context>
@@ -31,6 +32,39 @@ export interface AiReviewInput {
+  return raw
+    .replace(/\s+/g, " ")
+    .replace(/(bearer\s+)[^\s]+/gi, "$1***")
+    .replace(/\b(sk|pk|key|token)[-_][A-Za-z0-9_-]{8,}/gi, "***")
+    .slice(0, 200)
 }
</file context>

// headless-without-model) — the review degrades to deterministic-only.
if (lanes.has("ai_review") && input.aiReview) {
let aiStatus: AiLaneStatus | undefined
if (!lanes.has("ai_review")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Empty-scope reviews now emit an AI skipped status and coverage note even though there is no review scope or AI lane to skip. Guard the AI-status block on a non-empty review scope so empty reviews leave ai undefined.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/altimate/review/orchestrate.ts, line 1418:

<comment>Empty-scope reviews now emit an AI `skipped` status and coverage note even though there is no review scope or AI lane to skip. Guard the AI-status block on a non-empty review scope so empty reviews leave `ai` undefined.</comment>

<file context>
@@ -1410,7 +1414,12 @@ export async function runReview(input: OrchestrateInput): Promise<VerdictEnvelop
   // headless-without-model) — the review degrades to deterministic-only.
-  if (lanes.has("ai_review") && input.aiReview) {
+  let aiStatus: AiLaneStatus | undefined
+  if (!lanes.has("ai_review")) {
+    aiStatus = { state: "skipped", reason: `not part of the ${tier} tier` }
+  } else if (!input.aiReview) {
</file context>

// A failed AI lane must never fail the review.
} catch (err) {
// A failed AI lane must never fail the review — but it must say so.
aiStatus = { state: "error", reason: String((err as Error)?.message ?? err).slice(0, 200) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: AI callback failures bypass the redaction promised by AiLaneStatus and can publish raw error text in the signed envelope and PR comment. Apply the same credential-safe error formatter before storing ai.reason.

(Based on your team's feedback about masking credential-bearing error text.) [173edeff-f795-42ca-9d7d-86524f63e58a]

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/altimate/review/orchestrate.ts, line 1444:

<comment>AI callback failures bypass the redaction promised by `AiLaneStatus` and can publish raw error text in the signed envelope and PR comment. Apply the same credential-safe error formatter before storing `ai.reason`. 

(Based on your team's feedback about masking credential-bearing error text.) [173edeff-f795-42ca-9d7d-86524f63e58a]</comment>

<file context>
@@ -1419,16 +1428,20 @@ export async function runReview(input: OrchestrateInput): Promise<VerdictEnvelop
-      // A failed AI lane must never fail the review.
+    } catch (err) {
+      // A failed AI lane must never fail the review — but it must say so.
+      aiStatus = { state: "error", reason: String((err as Error)?.message ?? err).slice(0, 200) }
     }
   }
</file context>

Comment thread github/review/action.yml
ALTIMATE_REVIEW_SIGNING_KEY: ${{ inputs.signing_key }}
run: |
args=(--mode "$IN_MODE" --manifest "$IN_MANIFEST" --severity "$IN_SEVERITY")
[[ -n "$IN_PROJECT_DIR" && "$IN_PROJECT_DIR" != "." ]] && args+=(--cwd "$IN_PROJECT_DIR")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: A misspelled project_dir silently broadens a monorepo review to every changed file. Validate that the directory contains dbt_project.yml/.yaml before adding --cwd, or make the review fail closed when project discovery fails.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At github/review/action.yml, line 189:

<comment>A misspelled `project_dir` silently broadens a monorepo review to every changed file. Validate that the directory contains `dbt_project.yml`/`.yaml` before adding `--cwd`, or make the review fail closed when project discovery fails.</comment>

<file context>
@@ -181,6 +186,7 @@ runs:
         ALTIMATE_REVIEW_SIGNING_KEY: ${{ inputs.signing_key }}
       run: |
         args=(--mode "$IN_MODE" --manifest "$IN_MANIFEST" --severity "$IN_SEVERITY")
+        [[ -n "$IN_PROJECT_DIR" && "$IN_PROJECT_DIR" != "." ]] && args+=(--cwd "$IN_PROJECT_DIR")
         [[ -n "$IN_BASE" ]] && args+=(--base "$IN_BASE")
         [[ -n "$IN_HEAD" ]] && args+=(--head "$IN_HEAD")
</file context>
Suggested change
[[ -n "$IN_PROJECT_DIR" && "$IN_PROJECT_DIR" != "." ]] && args+=(--cwd "$IN_PROJECT_DIR")
if [[ -n "$IN_PROJECT_DIR" && "$IN_PROJECT_DIR" != "." ]]; then
project_path="${GITHUB_WORKSPACE:-$PWD}/$IN_PROJECT_DIR"
if [[ ! -f "$project_path/dbt_project.yml" && ! -f "$project_path/dbt_project.yaml" ]]; then
echo "::error::project_dir must contain dbt_project.yml or dbt_project.yaml"
exit 1
fi
args+=(--cwd "$IN_PROJECT_DIR")
fi

notes.push(`${env.scope.outOfScopeFiles} changed file(s) outside the dbt project were not reviewed.`)
if (env.ai && env.ai.state !== "ok") {
const what = { skipped: "skipped", timeout: "timed out", error: "failed" }[env.ai.state]
notes.push(`Advisory AI review ${what}${env.ai.reason ? `: ${env.ai.reason}` : ""}.`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The PR comment does not report the AI lane’s available model and duration: successful runs show only the model, while failed or timed-out runs show only state and reason. Include model and duration in the rendered AI status.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/altimate/review/format.ts, line 144:

<comment>The PR comment does not report the AI lane’s available model and duration: successful runs show only the model, while failed or timed-out runs show only state and reason. Include model and duration in the rendered AI status.</comment>

<file context>
@@ -110,11 +111,41 @@ export function renderSummary(env: VerdictEnvelope): string {
+    notes.push(`${env.scope.outOfScopeFiles} changed file(s) outside the dbt project were not reviewed.`)
+  if (env.ai && env.ai.state !== "ok") {
+    const what = { skipped: "skipped", timeout: "timed out", error: "failed" }[env.ai.state]
+    notes.push(`Advisory AI review ${what}${env.ai.reason ? `: ${env.ai.reason}` : ""}.`)
+  }
+  return notes.length ? [...notes.map((n) => `> ℹ️ ${n}`), ""] : []
</file context>

const head = unverified.slice(0, 5).map((u) => `\`${u.path}\``).join(", ")
const more = unverified.length > 5 ? ` and ${unverified.length - 5} more` : ""
notes.push(
`${unverified.length} model(s) not verified by the engine — ${unverified[0].reason}: ${head}${more}. ` +

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Mixed head/base compile failures are rendered with the first model’s reason for every model, which misstates review coverage. Group the entries by reason or render each model with its own reason.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/altimate/review/format.ts, line 136:

<comment>Mixed head/base compile failures are rendered with the first model’s reason for every model, which misstates review coverage. Group the entries by reason or render each model with its own reason.</comment>

<file context>
@@ -110,11 +111,41 @@ export function renderSummary(env: VerdictEnvelope): string {
+    const head = unverified.slice(0, 5).map((u) => `\`${u.path}\``).join(", ")
+    const more = unverified.length > 5 ? ` and ${unverified.length - 5} more` : ""
+    notes.push(
+      `${unverified.length} model(s) not verified by the engine — ${unverified[0].reason}: ${head}${more}. ` +
+        "Run `dbt compile` (and compile the base into `target-base/`) for equivalence and lineage checks.",
+    )
</file context>

/** Models the engine could not verify (no compiled SQL). */
unverified_models?: number
/** Advisory AI lane outcome: ok | skipped | timeout | error. */
ai_state?: string

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: ai_state is typed as string, but the AI lane only produces ok | skipped | timeout | error. Narrow the type to that union so the telemetry contract matches the envelope schema and typos are caught at compile time.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/altimate/telemetry/index.ts, line 1606:

<comment>`ai_state` is typed as `string`, but the AI lane only produces `ok | skipped | timeout | error`. Narrow the type to that union so the telemetry contract matches the envelope schema and typos are caught at compile time.</comment>

<file context>
@@ -1594,10 +1594,20 @@ export namespace Telemetry {
+        /** Models the engine could not verify (no compiled SQL). */
+        unverified_models?: number
+        /** Advisory AI lane outcome: ok | skipped | timeout | error. */
+        ai_state?: string
+        ai_duration_ms?: number
+        /** Where the review base came from (flag, github_event, default_branch, …). */
</file context>
Suggested change
ai_state?: string
ai_state?: "ok" | "skipped" | "timeout" | "error"


describe("PR title and description reach the intent check", () => {
test("GitHub: read from the pull_request event", async () => {
const dir = await fs.mkdtemp(path.join(tmpdir(), "review-event-"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This test creates a temp directory and never removes it, so every run leaves a review-event-* directory behind. Remove it in a finally block (fs.rm(dir, { recursive: true, force: true })) or use the repo's tmpdir fixture with auto cleanup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/test/altimate/review-ci-honesty.test.ts, line 157:

<comment>This test creates a temp directory and never removes it, so every run leaves a `review-event-*` directory behind. Remove it in a `finally` block (`fs.rm(dir, { recursive: true, force: true })`) or use the repo's tmpdir fixture with auto cleanup.</comment>

<file context>
@@ -0,0 +1,188 @@
+
+describe("PR title and description reach the intent check", () => {
+  test("GitHub: read from the pull_request event", async () => {
+    const dir = await fs.mkdtemp(path.join(tmpdir(), "review-event-"))
+    const ev = path.join(dir, "event.json")
+    await fs.writeFile(ev, JSON.stringify({ pull_request: { title: "Add fct_orders", body: "## Spec\n- grain: order_id" } }))
</file context>

if (env.summary.degraded) {
lines.push(
"> ⚙️ **Lint-only run** — no dbt manifest/warehouse was available, so lineage, equivalence and",
"> ⚙️ **Lint-only run** — no changed model resolved against a dbt manifest, so lineage, equivalence and",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: The banner now asserts a cause that is only one of the two ways summary.degraded becomes true.

orchestrate.ts:1120 sets runDegraded = modelFiles.length > 0 ? !anyManifest : reviewable.length === 0, so an empty reviewable set also renders this banner. That happens for a PR whose only changed files are non-dbt (.md, .json, … are dropped by filterChangedFiles → shouldReview), and there the advice "Wire manifest_path" cannot change anything. The comment then misstates why checks were skipped, which is the class of problem this PR is fixing. Branch the wording on the actual cause, or give the empty-reviewable case its own note.

const head = unverified.slice(0, 5).map((u) => `\`${u.path}\``).join(", ")
const more = unverified.length > 5 ? ` and ${unverified.length - 5} more` : ""
notes.push(
`${unverified.length} model(s) not verified by the engine — ${unverified[0].reason}: ${head}${more}. ` +

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Only the first model's reason is reported for the whole set.

unverified entries carry two distinct reasons (orchestrate.ts:1108 head-side vs 1110 base-side), so a PR that mixes gaps gets every path labelled with unverified[0].reason. With unverified[0] a base-side entry, the line even tells the user to run dbt compile for models that were only missing target-base. Group by reason or use a neutral phrase ("no compiled SQL for the head or base").

notes.push(`${env.scope.outOfScopeFiles} changed file(s) outside the dbt project were not reviewed.`)
if (env.ai && env.ai.state !== "ok") {
const what = { skipped: "skipped", timeout: "timed out", error: "failed" }[env.ai.state]
notes.push(`Advisory AI review ${what}${env.ai.reason ? `: ${env.ai.reason}` : ""}.`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: env.ai.reason is interpolated into a > ℹ️ blockquote verbatim.

safeReason in ai-review.ts collapses whitespace, but the orchestrate catch path does not (see the comment there), so a multi-line error message breaks out of the blockquote and can inject arbitrary markdown into the posted PR comment. The footer at lines 114-115 has the same shape: env.scope.baseRef and env.ai.model are placed in single-backtick spans without the fence-length logic this file already applies to tierReasons (lines 64-74, added for exactly this reason). Reuse that escaping for these values.

if (lanes.has("ai_review") && input.aiReview) {
let aiStatus: AiLaneStatus | undefined
if (!lanes.has("ai_review")) {
aiStatus = { state: "skipped", reason: `not part of the ${tier} tier` }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: This reason can be false.

lanes is input.config.reviewers.length ? input.config.reviewers : TIER_LANES[tier] (line 1175), so the branch also fires when a user restricts lanes with reviewers: in .altimate/review.yml. A full-tier run with reviewers: ["sql_quality"] then reports "not part of the full tier" in the PR comment, which is a misstatement of the same kind the PR set out to remove. Distinguish "excluded by the reviewers config" from "not part of the tier".

// A failed AI lane must never fail the review.
} catch (err) {
// A failed AI lane must never fail the review — but it must say so.
aiStatus = { state: "error", reason: String((err as Error)?.message ?? err).slice(0, 200) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: This reason reaches the signed envelope and the PR comment unmasked.

Unlike safeReason in ai-review.ts, it is not whitespace-collapsed, not credential-masked, and only sliced to 200 chars — while AiLaneStatus.reason is documented as "never contains credentials". runAiReview does not throw today, so this is the fallback for any injected lane, but the contract should hold on every path: reuse the masking helper (or sanitize the message here) rather than embedding the raw error.

- name: Compile the base into target-base/
run: |
git worktree add ../base "origin/${{ github.base_ref }}"
(cd ../base && dbt deps && dbt compile --target-path "$GITHUB_WORKSPACE/target-base")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: This snippet does not work in the monorepo layout the paragraph below documents.

It writes target-base/ to the repository root and runs dbt compile from the worktree root. The resolver reads <dbt root>/target-base/compiled/<project>/… (compiled.ts:117, with opts.cwd = dbtRoot from run.ts:363), and dbt compile needs dbt_project.yml, so with project_dir: dbt both steps fail or land where the review never looks — equivalence and column-breakage silently stay off. Add a monorepo variant, e.g. (cd ../base/dbt && dbt deps && dbt compile --target-path "$GITHUB_WORKSPACE/dbt/target-base"), and note the path must sit next to the project's manifest.json.


describe("PR title and description reach the intent check", () => {
test("GitHub: read from the pull_request event", async () => {
const dir = await fs.mkdtemp(path.join(tmpdir(), "review-event-"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: This temp directory is never removed.

test/AGENTS.md ("Temporary Directory Fixture") asks tests to use tmpdir() / tmpdirScoped() from fixture/fixture.ts so cleanup is automatic; a raw fs.mkdtemp leaks one directory per run under the parallel suite. Use await using tmp = await tmpdir({ init: ... }) and write event.json inside tmp.path.

@kilo-code-bot

kilo-code-bot Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 7 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 6

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/altimate/review/format.ts 45 Lint-only banner asserts "no changed model resolved against a dbt manifest", but summary.degraded is also true when reviewable.length === 0 (e.g. a docs-only PR). The "wire manifest_path" advice cannot help there, so the comment misstates why checks were skipped.

SUGGESTION

File Line Issue
packages/opencode/src/altimate/review/format.ts 136 Unverified-model note uses only unverified[0].reason for every path; head-side and base-side gaps are conflated.
packages/opencode/src/altimate/review/format.ts 144 env.ai.reason is interpolated into the PR comment verbatim; footer scope.baseRef/ai.model in single-backtick spans lack the fence-length escaping already used for tierReasons.
packages/opencode/src/altimate/review/orchestrate.ts 1419 "not part of the ${tier} tier" also fires when reviewers: config excluded ai_review, so the posted reason can be false.
packages/opencode/src/altimate/review/orchestrate.ts 1444 Catch-path reason is raw (no masking, no whitespace collapse) although AiLaneStatus.reason is documented credential-free and is posted publicly.
docs/docs/usage/dbt-pr-review.md 218 Base-compile snippet writes target-base/ to the repo root and compiles from the worktree root, so it does not work in the project_dir monorepo layout this same PR documents.
packages/opencode/test/altimate/review-ci-honesty.test.ts 157 fs.mkdtemp temp dir is never cleaned up; test/AGENTS.md asks for the tmpdir() fixture.
Files Reviewed (11 files)
  • docs/docs/usage/dbt-pr-review.md - 1 issue
  • github/review/action.yml - no issues
  • packages/opencode/src/altimate/review/ai-review.ts - no issues
  • packages/opencode/src/altimate/review/format.ts - 3 issues
  • packages/opencode/src/altimate/review/orchestrate.ts - 2 issues
  • packages/opencode/src/altimate/review/run.ts - no issues
  • packages/opencode/src/altimate/review/telemetry.ts - no issues
  • packages/opencode/src/altimate/review/verdict.ts - no issues
  • packages/opencode/src/altimate/telemetry/index.ts - no issues
  • packages/opencode/src/cli/cmd/review.ts - no issues
  • packages/opencode/test/altimate/review-ci-honesty.test.ts - 1 issue

Verified on 1fda92a8: prContextFromEnv reads the right CI variables, the new ai field is covered by the signature, and the AI lane still makes at most one model call per review (no cost side effect). --cwd resolution for project_dir matches how manifest_path is resolved. Typecheck/CI gates were not re-run in this read-only review.


Reviewed by deepseek-v4.1-flash-1017 · Input: 148.1K · Output: 44.8K · Cached: 4.2M

Review guidance: REVIEW.md from base branch fix/review-scope-diff (truncated)

@dev-punia-altimate dev-punia-altimate left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Code Review — OpenCodeReview (Gemini) — 3 finding(s)

  • 3 anchored to a line (posted inline when the comment stream is on)
  • 0 without a line anchor
All findings (full text)

1. packages/opencode/src/altimate/review/run.ts (L382-L385)

[🔵 LOW] The checklist prohibits the use of nested ternary expressions. Please refactor this to avoid nesting, perhaps by computing the disabled reason in a separate variable before using it here.

Suggested change:

    cliVersion: opts.cliVersion ?? Installation.VERSION,
    aiReview: opts.noAi || config.ai === false ? undefined : runAiReview,
    aiDisabledReason: (function() {
      if (opts.noAi) return "disabled by --no-ai"
      if (config.ai === false) return "disabled by `ai: false` in .altimate/review.yml"
      return undefined
    })(),
    aiModel: opts.aiModel,

2. packages/opencode/src/altimate/review/ai-review.ts (L124-L129)

[🔴 HIGH] Moving buildUserMessage outside the try...catch block introduces the risk of unhandled exceptions. If buildUserMessage throws an error (e.g., due to an unexpected value or formatting error), it will crash the function, violating the JSDoc contract that "a review must not crash because the AI layer is unavailable".

Consider moving the message generation and timeout setup inside the try block to ensure all potential errors are caught and a proper failure status is returned.

Suggested change:

  const controller = new AbortController()
  let timeout: ReturnType<typeof setTimeout> | undefined
  let timeoutMs = 0
  let modelLabel: string | undefined
  try {
    const message = buildUserMessage({ ...input, files })
    timeoutMs = aiTimeoutMs(message.length)
    timeout = setTimeout(() => controller.abort(), timeoutMs)

3. packages/opencode/src/altimate/review/ai-review.ts (L61-L68)

[🟠 MEDIUM] While this effectively sanitizes bearer tokens and typical API key patterns, it may not cover other sensitive credentials occasionally found in network errors, such as Basic <base64> auth headers or credentials embedded within URLs (e.g., https://user:pass@host/...).

Consider expanding the regex to cover these patterns for more robust redaction.

Suggested change:

function safeReason(err: unknown): string {
  const raw = err instanceof Error ? err.message : String(err)
  return raw
    .replace(/\s+/g, " ")
    .replace(/(bearer\s+|basic\s+)[^\s]+/gi, "$1***")
    .replace(/:\/\/[^/]*@[^/]+/gi, "://***@")
    .replace(/\b(sk|pk|key|token)[-_][A-Za-z0-9_-]{8,}/gi, "***")
    .slice(0, 200)
}

Comment on lines 382 to +385
cliVersion: opts.cliVersion ?? Installation.VERSION,
aiReview: opts.noAi || config.ai === false ? undefined : runAiReview,
aiDisabledReason: opts.noAi ? "disabled by --no-ai" : config.ai === false ? "disabled by `ai: false` in .altimate/review.yml" : undefined,
aiModel: opts.aiModel,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[🔵 LOW] The checklist prohibits the use of nested ternary expressions. Please refactor this to avoid nesting, perhaps by computing the disabled reason in a separate variable before using it here.

Suggested change:

Suggested change
cliVersion: opts.cliVersion ?? Installation.VERSION,
aiReview: opts.noAi || config.ai === false ? undefined : runAiReview,
aiDisabledReason: opts.noAi ? "disabled by --no-ai" : config.ai === false ? "disabled by `ai: false` in .altimate/review.yml" : undefined,
aiModel: opts.aiModel,
cliVersion: opts.cliVersion ?? Installation.VERSION,
aiReview: opts.noAi || config.ai === false ? undefined : runAiReview,
aiDisabledReason: (function() {
if (opts.noAi) return "disabled by --no-ai"
if (config.ai === false) return "disabled by `ai: false` in .altimate/review.yml"
return undefined
})(),
aiModel: opts.aiModel,

Comment on lines +124 to 129
const message = buildUserMessage({ ...input, files })
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), AI_TIMEOUT_MS)
const timeoutMs = aiTimeoutMs(message.length)
const timeout = setTimeout(() => controller.abort(), timeoutMs)
let modelLabel: string | undefined
try {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[🔴 HIGH] Moving buildUserMessage outside the try...catch block introduces the risk of unhandled exceptions. If buildUserMessage throws an error (e.g., due to an unexpected value or formatting error), it will crash the function, violating the JSDoc contract that "a review must not crash because the AI layer is unavailable".

Consider moving the message generation and timeout setup inside the try block to ensure all potential errors are caught and a proper failure status is returned.

Suggested change:

Suggested change
const message = buildUserMessage({ ...input, files })
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), AI_TIMEOUT_MS)
const timeoutMs = aiTimeoutMs(message.length)
const timeout = setTimeout(() => controller.abort(), timeoutMs)
let modelLabel: string | undefined
try {
const controller = new AbortController()
let timeout: ReturnType<typeof setTimeout> | undefined
let timeoutMs = 0
let modelLabel: string | undefined
try {
const message = buildUserMessage({ ...input, files })
timeoutMs = aiTimeoutMs(message.length)
timeout = setTimeout(() => controller.abort(), timeoutMs)

Comment on lines +61 to 68
function safeReason(err: unknown): string {
const raw = err instanceof Error ? err.message : String(err)
return raw
.replace(/\s+/g, " ")
.replace(/(bearer\s+)[^\s]+/gi, "$1***")
.replace(/\b(sk|pk|key|token)[-_][A-Za-z0-9_-]{8,}/gi, "***")
.slice(0, 200)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[🟠 MEDIUM] While this effectively sanitizes bearer tokens and typical API key patterns, it may not cover other sensitive credentials occasionally found in network errors, such as Basic <base64> auth headers or credentials embedded within URLs (e.g., https://user:pass@host/...).

Consider expanding the regex to cover these patterns for more robust redaction.

Suggested change:

Suggested change
function safeReason(err: unknown): string {
const raw = err instanceof Error ? err.message : String(err)
return raw
.replace(/\s+/g, " ")
.replace(/(bearer\s+)[^\s]+/gi, "$1***")
.replace(/\b(sk|pk|key|token)[-_][A-Za-z0-9_-]{8,}/gi, "***")
.slice(0, 200)
}
function safeReason(err: unknown): string {
const raw = err instanceof Error ? err.message : String(err)
return raw
.replace(/\s+/g, " ")
.replace(/(bearer\s+|basic\s+)[^\s]+/gi, "$1***")
.replace(/:\/\/[^/]*@[^/]+/gi, "://***@")
.replace(/\b(sk|pk|key|token)[-_][A-Za-z0-9_-]{8,}/gi, "***")
.slice(0, 200)
}

@dev-punia-altimate

Copy link
Copy Markdown
Contributor

🤖 Code Review — OpenCodeReview (Gemini) — 3 finding(s)

  • 3 anchored to a line (posted inline when the comment stream is on)
  • 0 without a line anchor
All findings (full text)

1. packages/opencode/src/altimate/review/run.ts (L382-L385)

[🔵 LOW] The checklist prohibits the use of nested ternary expressions. Please refactor this to avoid nesting, perhaps by computing the disabled reason in a separate variable before using it here.

Suggested change:

    cliVersion: opts.cliVersion ?? Installation.VERSION,
    aiReview: opts.noAi || config.ai === false ? undefined : runAiReview,
    aiDisabledReason: (function() {
      if (opts.noAi) return "disabled by --no-ai"
      if (config.ai === false) return "disabled by `ai: false` in .altimate/review.yml"
      return undefined
    })(),
    aiModel: opts.aiModel,

2. packages/opencode/src/altimate/review/ai-review.ts (L124-L129)

[🔴 HIGH] Moving buildUserMessage outside the try...catch block introduces the risk of unhandled exceptions. If buildUserMessage throws an error (e.g., due to an unexpected value or formatting error), it will crash the function, violating the JSDoc contract that "a review must not crash because the AI layer is unavailable".

Consider moving the message generation and timeout setup inside the try block to ensure all potential errors are caught and a proper failure status is returned.

Suggested change:

  const controller = new AbortController()
  let timeout: ReturnType<typeof setTimeout> | undefined
  let timeoutMs = 0
  let modelLabel: string | undefined
  try {
    const message = buildUserMessage({ ...input, files })
    timeoutMs = aiTimeoutMs(message.length)
    timeout = setTimeout(() => controller.abort(), timeoutMs)

3. packages/opencode/src/altimate/review/ai-review.ts (L61-L68)

[🟠 MEDIUM] While this effectively sanitizes bearer tokens and typical API key patterns, it may not cover other sensitive credentials occasionally found in network errors, such as Basic <base64> auth headers or credentials embedded within URLs (e.g., https://user:pass@host/...).

Consider expanding the regex to cover these patterns for more robust redaction.

Suggested change:

function safeReason(err: unknown): string {
  const raw = err instanceof Error ? err.message : String(err)
  return raw
    .replace(/\s+/g, " ")
    .replace(/(bearer\s+|basic\s+)[^\s]+/gi, "$1***")
    .replace(/:\/\/[^/]*@[^/]+/gi, "://***@")
    .replace(/\b(sk|pk|key|token)[-_][A-Za-z0-9_-]{8,}/gi, "***")
    .slice(0, 200)
}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants