diff --git a/.opencode/command/dbt-health-config.md b/.opencode/command/dbt-health-config.md
new file mode 100644
index 000000000..bccb45eb9
--- /dev/null
+++ b/.opencode/command/dbt-health-config.md
@@ -0,0 +1,25 @@
+---
+description: infer a dbt health-check config from the project's own conventions and write .altimate/dbt-health.yml
+agent: build
+subtask: true
+---
+
+Generate a dbt health-check configuration by inferring it from a dbt project's **existing conventions** (deterministic — no guessing).
+
+## Target project
+
+Use `$ARGUMENTS` as the dbt project directory if provided. Otherwise, locate the dbt project in the current worktree by finding the directory that contains a `dbt_project.yml` (run `project_scan` if you need to discover it). If there are **multiple** dbt projects, do this for **each** one — every project gets its own `.altimate/dbt-health.yml`.
+
+## Steps
+
+1. For each target project directory, call the **`dbt_health_config`** tool:
+ `dbt_health_config({ project_dir: "
" })`
+ This deterministically infers the config from the project (modal tags / meta keys / tests, configured schemas & databases, naming contracts, distribution-ratcheted thresholds) and writes it to `/.altimate/dbt-health.yml`.
+
+2. Then run **`dbt_project_health`** on the same directory:
+ `dbt_project_health({ project_dir: "" })`
+ It auto-discovers the freshly written `.altimate/dbt-health.yml`, so the report now reflects the inferred rules.
+
+3. Summarize for the user: the path(s) written, which config-driven checks were activated (tags, meta keys, tests, naming contracts, parent schema/database whitelists, thresholds), and the resulting finding counts by severity. Tell them the file is committed with the project and can be hand-edited — re-running this command regenerates it deterministically.
+
+Do not fabricate values; everything comes from the two tools above.
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 173bd0e1b..d45dc1261 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -78,7 +78,7 @@
"@ai-sdk/togetherai": "2.0.41",
"@ai-sdk/vercel": "2.0.39",
"@ai-sdk/xai": "3.0.82",
- "@altimateai/altimate-core": "0.5.1",
+ "@altimateai/altimate-core": "0.8.0",
"@altimateai/drivers": "workspace:*",
"@aws-sdk/credential-providers": "3.1057.0",
"@clack/prompts": "1.0.0-alpha.1",
diff --git a/packages/opencode/src/altimate/index.ts b/packages/opencode/src/altimate/index.ts
index f6dae26fc..d6894bdfe 100644
--- a/packages/opencode/src/altimate/index.ts
+++ b/packages/opencode/src/altimate/index.ts
@@ -40,6 +40,8 @@ export * from "./tools/altimate-core-validate"
export * from "./tools/dbt-lineage"
export * from "./tools/dbt-manifest"
export * from "./tools/dbt-profiles"
+export * from "./tools/dbt-project-health"
+export * from "./tools/dbt-health-config"
export * from "./tools/finops-analyze-credits"
export * from "./tools/finops-expensive-queries"
diff --git a/packages/opencode/src/altimate/native/altimate-core.ts b/packages/opencode/src/altimate/native/altimate-core.ts
index 137e28d84..65e073cfc 100644
--- a/packages/opencode/src/altimate/native/altimate-core.ts
+++ b/packages/opencode/src/altimate/native/altimate-core.ts
@@ -267,6 +267,23 @@ export function registerAll(): void {
return fail(e)
}
})
+ // dbt project health checks — reads the dbt project files directly (no manifest required).
+ register("altimate_core.dbt_project_health", async (params) => {
+ try {
+ const json = core.dbtProjectHealth(params.project_dir, params.config_json, params.catalog_path)
+ return ok(true, { findings: JSON.parse(json) })
+ } catch (e) {
+ return fail(e)
+ }
+ })
+ // Deterministically infer a dbt health-check config (YAML) from a project's conventions.
+ register("altimate_core.dbt_health_infer_config", async (params) => {
+ try {
+ return ok(true, { config: core.dbtHealthInferConfig(params.project_dir) })
+ } catch (e) {
+ return fail(e)
+ }
+ })
// altimate_change end
// 7. altimate_core.fix
diff --git a/packages/opencode/src/altimate/native/types.ts b/packages/opencode/src/altimate/native/types.ts
index ad90c7631..1176bafe5 100644
--- a/packages/opencode/src/altimate/native/types.ts
+++ b/packages/opencode/src/altimate/native/types.ts
@@ -1244,6 +1244,14 @@ export const BridgeMethods = {
params: { base_sql: string; head_sql: string }
result: AltimateCoreResult
},
+ "altimate_core.dbt_project_health": {} as {
+ params: { project_dir: string; config_json?: string; catalog_path?: string }
+ result: AltimateCoreResult
+ },
+ "altimate_core.dbt_health_infer_config": {} as {
+ params: { project_dir: string }
+ result: AltimateCoreResult
+ },
// altimate_change end
// --- altimate-core Phase 1 (P0) ---
"altimate_core.fix": {} as { params: AltimateCoreFixParams; result: AltimateCoreResult },
diff --git a/packages/opencode/src/altimate/tools/dbt-health-config.ts b/packages/opencode/src/altimate/tools/dbt-health-config.ts
new file mode 100644
index 000000000..139fed334
--- /dev/null
+++ b/packages/opencode/src/altimate/tools/dbt-health-config.ts
@@ -0,0 +1,55 @@
+import { mkdir } from "node:fs/promises"
+import { dirname } from "node:path"
+import z from "zod"
+import { Tool } from "../../tool/tool"
+import { Dispatcher } from "../native"
+
+/** Relative path (from the dbt project root) where the inferred config is written. */
+export const DBT_HEALTH_CONFIG_RELPATH = ".altimate/dbt-health.yml"
+
+export const DbtHealthConfigTool = Tool.define("dbt_health_config", {
+ description:
+ "Deterministically infer a dbt health-check config from a dbt project's EXISTING conventions (modal tags/meta keys/tests, configured schemas & databases, naming contracts, distribution-ratcheted thresholds) and write it to /.altimate/dbt-health.yml. This activates the config-driven governance checks (which are no-ops until configured). Per-project: each dbt project gets its own config file. Reproducible — no LLM, same project always yields the same file.",
+ parameters: z.object({
+ project_dir: z.string().describe("Path to the dbt project root (the directory containing dbt_project.yml)"),
+ write: z
+ .boolean()
+ .optional()
+ .describe("Write the config to /.altimate/dbt-health.yml (default true). If false, only return the YAML."),
+ }),
+ async execute(args, _ctx) {
+ const result = await Dispatcher.call("altimate_core.dbt_health_infer_config", {
+ project_dir: args.project_dir,
+ })
+
+ if (!result.success) {
+ const error = result.error ?? "unknown error"
+ return {
+ title: "dbt health config: ERROR",
+ metadata: { error },
+ output: `Failed to infer dbt health config: ${error}`,
+ }
+ }
+
+ const yaml = String((result.data as { config?: unknown }).config ?? "")
+ const outPath = `${args.project_dir.replace(/\/+$/, "")}/${DBT_HEALTH_CONFIG_RELPATH}`
+
+ let written = false
+ if (args.write !== false) {
+ await mkdir(dirname(outPath), { recursive: true })
+ await Bun.write(outPath, yaml)
+ written = true
+ }
+
+ const checkCount = (yaml.match(/^ {2}\S/gm) ?? []).length
+ return {
+ title: written
+ ? `dbt health config written (${checkCount} checks tuned) → ${outPath}`
+ : `dbt health config inferred (${checkCount} checks tuned)`,
+ metadata: { path: outPath, written, project_dir: args.project_dir },
+ output: written
+ ? `Wrote inferred config to ${outPath}:\n\n${yaml}`
+ : `Inferred config (not written):\n\n${yaml}`,
+ }
+ },
+})
diff --git a/packages/opencode/src/altimate/tools/dbt-project-health.ts b/packages/opencode/src/altimate/tools/dbt-project-health.ts
new file mode 100644
index 000000000..dab9e6e49
--- /dev/null
+++ b/packages/opencode/src/altimate/tools/dbt-project-health.ts
@@ -0,0 +1,100 @@
+import z from "zod"
+import { Tool } from "../../tool/tool"
+import { Dispatcher } from "../native"
+
+/** A single finding returned by `altimate_core.dbt_project_health` (Rust `HealthFinding`). */
+interface HealthFinding {
+ code: string
+ alias: string
+ resource_type: "model" | "source" | "exposure" | "macro" | "project"
+ unique_id?: string
+ file?: string
+ severity: "error" | "warning" | "info"
+ message: string
+ recommendation?: string
+ reason_to_flag?: string
+ metadata?: unknown
+}
+
+const SEVERITY_ORDER: Record = { error: 0, warning: 1, info: 2 }
+
+export const DbtProjectHealthTool = Tool.define("dbt_project_health", {
+ description:
+ "Run dbt project health checks (governance, modelling, documentation, tests, sources) directly against a dbt project's files — no compiled manifest.json required. Reads .sql models (via the dbt Jinja AST parser), schema.yml properties and dbt_project.yml. Optionally uses a catalog.json for column-level checks; otherwise columns are inferred from the SQL parser.",
+ parameters: z.object({
+ project_dir: z.string().describe("Path to the dbt project root (the directory containing dbt_project.yml)"),
+ config_path: z
+ .string()
+ .optional()
+ .describe("Optional path to a JSON health-check config (disabled checks, severity overrides, options)"),
+ catalog_path: z
+ .string()
+ .optional()
+ .describe("Optional path to a dbt catalog.json to power column-aware checks"),
+ }),
+ async execute(args, _ctx) {
+ // Explicit config_path wins; otherwise auto-discover a per-project inferred config
+ // at /.altimate/dbt-health.{yml,yaml,json} (YAML or JSON both parse).
+ let config_json: string | undefined
+ const candidates = args.config_path
+ ? [args.config_path]
+ : [
+ `${args.project_dir.replace(/\/+$/, "")}/.altimate/dbt-health.yml`,
+ `${args.project_dir.replace(/\/+$/, "")}/.altimate/dbt-health.yaml`,
+ `${args.project_dir.replace(/\/+$/, "")}/.altimate/dbt-health.json`,
+ ]
+ for (const candidate of candidates) {
+ const file = Bun.file(candidate)
+ if (await file.exists()) {
+ config_json = await file.text()
+ break
+ }
+ }
+
+ const result = await Dispatcher.call("altimate_core.dbt_project_health", {
+ project_dir: args.project_dir,
+ config_json,
+ catalog_path: args.catalog_path,
+ })
+
+ if (!result.success) {
+ const error = result.error ?? "unknown error"
+ return {
+ title: "dbt health: ERROR",
+ metadata: { error },
+ output: `Failed to run dbt health checks: ${error}`,
+ }
+ }
+
+ const findings = ((result.data.findings as HealthFinding[] | undefined) ?? []).slice()
+ findings.sort(
+ (a, b) => (SEVERITY_ORDER[a.severity] ?? 3) - (SEVERITY_ORDER[b.severity] ?? 3) || a.code.localeCompare(b.code),
+ )
+
+ const counts = { error: 0, warning: 0, info: 0 } as Record
+ for (const f of findings) counts[f.severity] = (counts[f.severity] ?? 0) + 1
+
+ return {
+ title: `dbt health: ${findings.length} finding(s) — ${counts.error} error, ${counts.warning} warning, ${counts.info} info`,
+ metadata: {
+ finding_count: findings.length,
+ error_count: counts.error,
+ warning_count: counts.warning,
+ info_count: counts.info,
+ },
+ output: formatFindings(findings),
+ }
+ },
+})
+
+function formatFindings(findings: HealthFinding[]): string {
+ if (findings.length === 0) return "✓ No dbt health issues found."
+ const lines: string[] = []
+ for (const f of findings) {
+ const where = f.file ? ` (${f.file})` : f.unique_id ? ` (${f.unique_id})` : ""
+ lines.push(`[${f.severity.toUpperCase()}] ${f.code} ${f.alias}${where}`)
+ lines.push(` ${f.message}`)
+ if (f.recommendation) lines.push(` → ${f.recommendation}`)
+ }
+ return lines.join("\n")
+}
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 570bbd03c..349246582 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -69,6 +69,8 @@ import { WarehouseDiscoverTool } from "../altimate/tools/warehouse-discover"
import { McpDiscoverTool } from "../altimate/tools/mcp-discover"
import { DbtManifestTool } from "../altimate/tools/dbt-manifest"
+import { DbtProjectHealthTool } from "../altimate/tools/dbt-project-health"
+import { DbtHealthConfigTool } from "../altimate/tools/dbt-health-config"
// altimate_change start - import dbt unit test generation tool
import { DbtUnitTestGenTool } from "../altimate/tools/dbt-unit-test-gen"
// altimate_change end
@@ -405,6 +407,8 @@ export namespace ToolRegistry {
DbtUnitTestGenTool,
// altimate_change end
DbtProfilesTool,
+ DbtProjectHealthTool,
+ DbtHealthConfigTool,
DbtLineageTool,
SchemaIndexTool,
SchemaSearchTool,