Skip to content
Open
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.8.5] - Unreleased

### Fixed

- **The `github/review` composite action can be downloaded by GitHub Actions again.** The release tree contained three VS Code image symlinks whose removed targets caused GitHub's action downloader to reject the entire archive before the review step started. The images are now self-contained files and a release-critical test prevents dangling links from returning.
- **A valid dbt manifest is no longer mislabeled as a lint-only run.** Manifest availability is now checked independently from changed-model lookup, so new models and other valid manifests receive the correct full-run status.

### Added

- **Direct GitHub onboarding and a live dbt review demo.** The GitHub App installer now opens GitHub's repository-selection screen directly, and the README/docs link to the public `dbt-pr-review-demo` pull requests.

## [0.8.4] - 2026-06-05

A trace-durability patch. Open `/traces` mid-session and you'd see a rich waterfall — then the moment the agent finished its turn the view collapsed to a single "system-prompt" span, the Summary tab's *"What was asked"* showed *"No prompt recorded"*, and the Chat tab dropped every user turn but the last. The data was genuinely gone from disk, not just hidden in the viewer. This release stops the on-disk trace from being overwritten after each turn and makes the file authoritative across worker restarts. A five-persona pre-release review drove a follow-up wording fix so a reconstructed trace isn't misread as a failed run.
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ into CI pipelines and orchestration DAGs. Precision data tooling for any LLM.
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
[![Slack](https://img.shields.io/badge/Slack-Join%20Community-4A154B?logo=slack)](https://altimate.studio/join-agentic-data-engineering-slack)
[![Docs](https://img.shields.io/badge/docs-docs.altimate.sh-blue)](https://docs.altimate.sh)
[![Install GitHub App](https://img.shields.io/badge/GitHub-Install%20App-24292f?logo=github)](https://github.com/apps/altimate-code-agent/installations/new)
[![Live dbt review](https://img.shields.io/badge/dbt-Live%20PR%20Demo-ff694b?logo=dbt)](https://github.com/AltimateAI/dbt-pr-review-demo/pulls)

</div>

Expand All @@ -36,6 +38,12 @@ curl -fsSL https://www.altimate.sh/install | bash

The curl install drops a single self-contained binary named `altimate`. The npm install exposes both `altimate` and `altimate-code` on PATH; the curl install only exposes `altimate`. Alpine Linux (musl) and Windows on ARM64 are not currently supported by the standalone binary — use `apk add gcompat` on Alpine, or use WSL on Windows-on-ARM.

For GitHub, [install the Altimate Code App](https://github.com/apps/altimate-code-agent/installations/new)
to select repositories for interactive agent tasks. Automatic dbt pull-request
reviews use the deterministic GitHub Action documented below; see the
[public demo PRs](https://github.com/AltimateAI/dbt-pr-review-demo/pulls) before
installing it in your own repository.

Then — in order:

**Step 1: Configure your LLM provider** (required before anything works):
Expand Down
13 changes: 11 additions & 2 deletions docs/docs/usage/dbt-pr-review.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# dbt PR Review

[**See live review PRs**](https://github.com/AltimateAI/dbt-pr-review-demo/pulls)
·
[**Install the GitHub App**](https://github.com/apps/altimate-code-agent/installations/new)

The public demo is a zero-secret DuckDB project with open PRs for broken joins,
removed tests, PII exposure, `SELECT *`, unsafe incremental models, and a safe
refactor. The GitHub App handles interactive repository tasks; the automatic
review on every pull request is installed with the Action below.

AI code review specialized for dbt/SQL. `dbt-pr-review` produces a single,
**signed** verdict on a pull request — `APPROVE`, `COMMENT`, or `REQUEST_CHANGES`
— where every **blocking** finding is backed by a deterministic engine call, not
Expand Down Expand Up @@ -172,7 +181,7 @@ jobs:
with: { fetch-depth: 0 }
# Produce target/manifest.json for the full verdict (adapter-specific).
- run: pip install dbt-core dbt-bigquery && dbt deps && dbt compile
- uses: AltimateAI/altimate-code/github/review@v1
- uses: AltimateAI/altimate-code/github/review@v0.8.5
with:
mode: comment # `gate` to block merges
manifest_path: target/manifest.json
Expand Down Expand Up @@ -270,7 +279,7 @@ In GitHub Actions, supply the connection from a secret — both sides of the dif
run against the **same** warehouse (base-compiled vs head-compiled SQL):

```yaml
- uses: AltimateAI/altimate-code/github/review@v1
- uses: AltimateAI/altimate-code/github/review@v0.8.5
with:
mode: comment
manifest_path: target/manifest.json
Expand Down
2 changes: 1 addition & 1 deletion github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ This will walk you through installing the GitHub app, creating the workflow, and

### Manual Setup

1. Install the GitHub app https://github.com/apps/altimate-code-agent. Make sure it is installed on the target repository.
1. [Install the GitHub app](https://github.com/apps/altimate-code-agent/installations/new) and select the target repository.
2. Add the following workflow file to `.github/workflows/altimate-code.yml` in your repo. Set the appropriate `model` and required API keys in `env`.

```yml
Expand Down
16 changes: 14 additions & 2 deletions github/review/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,20 @@ runs:
- name: Get altimate-code version
id: version
shell: bash
env:
ACTION_REF: ${{ github.action_ref }}
run: |
VERSION=$(curl -sf https://api.github.com/repos/AltimateAI/altimate-code/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
VERSION="${VERSION#v}" # the installer's --version expects no leading 'v'
set -euo pipefail
if [[ "${ACTION_REF:-}" =~ ^v?([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
VERSION="${BASH_REMATCH[1]}"
else
VERSION=$(
curl -sf --connect-timeout 5 --max-time 15 \
https://api.github.com/repos/AltimateAI/altimate-code/releases/latest \
| grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4 || true
)
VERSION="${VERSION#v}" # the installer's --version expects no leading 'v'
fi
echo "version=${VERSION:-latest}" >> $GITHUB_OUTPUT

- name: Cache altimate-code
Expand Down Expand Up @@ -107,6 +118,7 @@ runs:
echo "::error::altimate_api_key is set but altimate_instance is empty — provide the tenant/instance name."
exit 1
fi
umask 077
mkdir -p "$HOME/.altimate"
jq -nc \
--arg url "${IN_ALT_URL:-https://api.myaltimate.com}" \
Expand Down
2 changes: 1 addition & 1 deletion github/review/examples/altimate-ingestion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
- name: altimate dbt PR review
# Pin to the altimate-code release that ships `altimate review`
# (the first release cut after the dbt-pr-review feature merges).
uses: AltimateAI/altimate-code/github/review@v0.8.0
uses: AltimateAI/altimate-code/github/review@v0.8.5
with:
mode: comment # start non-blocking; switch to `gate` once trusted
manifest_path: target/manifest.json
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/altimate/review/orchestrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export interface CheckResult {

/** High-level engine surface the orchestrator depends on. */
export interface ReviewRunner {
/** True when the configured dbt manifest loaded, independent of model lookup. */
manifestAvailable?(): Promise<boolean>
impact(model: string): Promise<ImpactResult>
grade(sql: string, dialect: string): Promise<GradeResult>
check(sql: string, dialect: string, baseSql?: string): Promise<CheckResult>
Expand Down Expand Up @@ -1002,6 +1004,9 @@ export async function runReview(input: OrchestrateInput): Promise<VerdictEnvelop
const modelFiles = reviewable.filter((f) => f.kind === "model_sql" || f.kind === "python_model")
const ctxByPath = new Map<string, ModelContext>()
let anyManifest = false
if (input.runner.manifestAvailable) {
anyManifest = await input.runner.manifestAvailable().catch(() => false)
}
await Promise.all(
modelFiles.map(async (file) => {
const model = modelNameFromPath(file.path)
Expand Down
10 changes: 9 additions & 1 deletion packages/opencode/src/altimate/review/runner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Dispatcher } from "../native"
import { parseManifest } from "../native/dbt/manifest"
import type { CheckResult, EquivalenceResult, GradeResult, ImpactResult, ReviewRunner } from "./orchestrate"
import { buildReviewSchemaContext, type SchemaContext } from "./schema-context"

Expand Down Expand Up @@ -116,7 +117,10 @@ export function createDispatcherRunner(opts: DispatcherRunnerOptions): ReviewRun
if (!manifestPromise) {
manifestPromise = (async () => {
try {
const res = await Dispatcher.call("dbt.manifest", { path: opts.manifestPath })
// Manifest parsing is pure TypeScript. Keep it independent from the
// native dispatcher registration path so a core-loading failure
// cannot incorrectly downgrade a valid dbt run to lint-only.
const res = await parseManifest({ path: opts.manifestPath })
const models = new Map<string, ManifestModel>()
const byName = new Map<string, ManifestModel>()
const children = new Map<string, string[]>()
Expand Down Expand Up @@ -239,6 +243,10 @@ export function createDispatcherRunner(opts: DispatcherRunnerOptions): ReviewRun
}

return {
async manifestAvailable(): Promise<boolean> {
return (await loadManifest()).ok
},

async impact(model: string): Promise<ImpactResult> {
const mf = await loadManifest()
if (!mf.ok) {
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/cli/cmd/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ type IssueQueryResponse = {
const AGENT_USERNAME = "altimate-code-agent[bot]"
const AGENT_REACTION = "eyes"
const WORKFLOW_FILE = ".github/workflows/altimate-code.yml"
export const GITHUB_APP_INSTALL_URL = "https://github.com/apps/altimate-code-agent/installations/new"
// altimate_change end

// Event categories for routing
Expand Down Expand Up @@ -333,7 +334,7 @@ export const GithubInstallCommand = cmd({

// Open browser
// altimate_change start — upstream_fix: GitHub App slug is altimate-code-agent
const url = "https://github.com/apps/altimate-code-agent"
const url = GITHUB_APP_INSTALL_URL
// altimate_change end
const command =
process.platform === "darwin"
Expand Down
39 changes: 39 additions & 0 deletions packages/opencode/test/altimate/review-runner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, test } from "bun:test"
import { writeFileSync } from "node:fs"
import path from "node:path"
import { createDispatcherRunner } from "../../src/altimate/review/runner"
import { tmpdir } from "../fixture/fixture"

describe("review manifest loading", () => {
test("loads a valid manifest without initializing the native dispatcher", async () => {
await using tmp = await tmpdir()
const manifestPath = path.join(tmp.path, "manifest.json")
writeFileSync(
manifestPath,
JSON.stringify({
metadata: { adapter_type: "duckdb" },
nodes: {
"model.demo.orders": {
resource_type: "model",
name: "orders",
original_file_path: "models/orders.sql",
config: { materialized: "table" },
depends_on: { nodes: [] },
columns: {},
},
},
sources: {},
}),
)

const runner = createDispatcherRunner({ manifestPath })
expect(await runner.manifestAvailable?.()).toBe(true)
expect(await runner.impact("orders")).toEqual({
hasManifest: true,
severity: "SAFE",
directCount: 0,
transitiveCount: 0,
testCount: 0,
})
})
})
46 changes: 45 additions & 1 deletion packages/opencode/test/altimate/review.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1079,7 +1079,7 @@ describe("orchestrate", () => {
const runner: ReviewRunner = {
...fakeRunner({}),
async impact() {
return { hasManifest: true, severity: "SAFE", directCount: 0, transitiveCount: 0, testCount: 0 }
return { hasManifest: false, severity: "UNKNOWN", directCount: 0, transitiveCount: 0, testCount: 0 }
},
async equivalence() {
return { decided: true, equivalent: false, differences: ["filter changed"], confidence: "high" }
Expand Down Expand Up @@ -1229,6 +1229,50 @@ describe("orchestrate", () => {
expect(["APPROVE", "COMMENT"]).toContain(env.verdict)
})

test("loaded manifest is not marked lint-only when a changed model is absent from it", async () => {
const files: ChangedFile[] = [{ path: "models/staging/new_model.sql", status: "added", diff: "+select 1\n" }]
const runner: ReviewRunner = {
...fakeRunner({}),
async manifestAvailable() {
return true
},
async impact() {
return { hasManifest: false, severity: "UNKNOWN", directCount: 0, transitiveCount: 0, testCount: 0 }
},
Comment thread
anandgupta42 marked this conversation as resolved.
}
const env = await runReview({
changedFiles: files,
config: { ...DEFAULT_REVIEW_CONFIG, reviewers: ["sql_quality"] },
rubric: DEFAULT_RUBRIC,
mode: "comment",
runner,
getContent: content("select 1 as value"),
})
expect(env.summary.degraded).toBe(false)
})

test("manifest availability errors degrade safely instead of aborting the review", async () => {
const files: ChangedFile[] = [{ path: "models/staging/stg_x.sql", status: "modified", diff: "+select 1\n" }]
const runner: ReviewRunner = {
...fakeRunner({}),
async manifestAvailable() {
throw new Error("manifest unreadable")
},
async impact() {
return { hasManifest: false, severity: "UNKNOWN", directCount: 0, transitiveCount: 0, testCount: 0 }
},
}
const env = await runReview({
changedFiles: files,
config: { ...DEFAULT_REVIEW_CONFIG, reviewers: ["sql_quality"] },
rubric: DEFAULT_RUBRIC,
mode: "comment",
runner,
getContent: content("select 1 as value"),
})
expect(env.summary.degraded).toBe(true)
})

test("renderSummary + inlineComments produce marker + structured output", async () => {
const env = buildEnvelope({
findings: [
Expand Down
6 changes: 5 additions & 1 deletion packages/opencode/test/cli/github-action.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { test, expect, describe } from "bun:test"
import { extractResponseText, formatPromptTooLargeError } from "../../src/cli/cmd/github"
import { extractResponseText, formatPromptTooLargeError, GITHUB_APP_INSTALL_URL } from "../../src/cli/cmd/github"
import type { MessageV2 } from "../../src/session/message-v2"
import { SessionID, MessageID, PartID } from "../../src/session/schema"

test("GitHub App install URL opens the repository-selection flow", () => {
expect(GITHUB_APP_INSTALL_URL).toBe("https://github.com/apps/altimate-code-agent/installations/new")
})

// Helper to create minimal valid parts
function createTextPart(text: string): MessageV2.Part {
return {
Expand Down
17 changes: 17 additions & 0 deletions packages/opencode/test/install/repository-symlinks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, test } from "bun:test"
import { existsSync, lstatSync, statSync } from "node:fs"
import path from "node:path"

const root = path.resolve(import.meta.dir, "../../../..")
const vscodeImages = ["button-dark.svg", "button-light.svg", "icon.png"]

describe("release archive assets", () => {
test("VS Code images are self-contained regular files", () => {
for (const name of vscodeImages) {
const asset = path.join(root, "sdks/vscode/images", name)
expect(existsSync(asset)).toBe(true)
expect(lstatSync(asset).isSymbolicLink()).toBe(false)
expect(statSync(asset).size).toBeGreaterThan(0)
}
})
})
Loading
Loading