Skip to content
Merged
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
19 changes: 19 additions & 0 deletions docs/docs/configure/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@ Configuration is loaded from multiple sources, with later sources overriding ear
| `compaction` | `object` | Context compaction settings (see [Context Management](context-management.md)) |
| `experimental` | `object` | Experimental feature flags |

### Experimental Flags

These flags are under `experimental` and may change or be removed in future releases.

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `env_fingerprint_skill_selection` | `boolean` | `false` | Use environment fingerprint (dbt, airflow, databricks, SQL) to select relevant skills once per session via LLM. When enabled, the configured model runs once at session start to filter skills based on detected project environment. Results are cached per working directory. When disabled (default), all skills are shown. |
| `auto_enhance_prompt` | `boolean` | `false` | Automatically rewrite prompts with AI before sending. Uses a small model to clarify rough prompts. |

Example:

```json
{
"experimental": {
"env_fingerprint_skill_selection": true
}
}
```

## Value Substitution

Config values support dynamic substitution so you never need to hardcode secrets.
Expand Down
137 changes: 137 additions & 0 deletions packages/opencode/src/altimate/fingerprint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Filesystem } from "../../util/filesystem"
import { Glob } from "../../util/glob"
import { Log } from "../../util/log"
import { Tracer } from "../observability/tracing"
import path from "path"

const log = Log.create({ service: "fingerprint" })

export namespace Fingerprint {
export interface Result {
tags: string[]
detectedAt: number
cwd: string
}

let cached: Result | undefined

export function get(): Result | undefined {
return cached
}

export async function refresh(): Promise<Result> {
const previousCwd = cached?.cwd ?? process.cwd()
cached = undefined
return detect(previousCwd)
}

export async function detect(cwd: string, root?: string): Promise<Result> {
Copy link

Choose a reason for hiding this comment

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

Bug: The fingerprint cache is not invalidated between sessions for the same directory, leading to stale environment data being used for skill selection.
Severity: MEDIUM

Suggested Fix

The cache should be made session-aware. One approach is to call Fingerprint.refresh() at the start of each new session to force re-detection. Alternatively, the cache could be invalidated between sessions or the cache key could be modified to include a session-specific identifier.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/opencode/src/altimate/fingerprint/index.ts#L28

Potential issue: A module-level cache for environment fingerprints is keyed only by the
current working directory (`cwd`). When a user starts a new session in the same
directory, `Fingerprint.detect()` is called with the same `cwd` and returns the cached
result from the previous session. If the project's dependencies or configuration (e.g.,
adding a `databricks.yml` file) changed between sessions, the stale cache is used. This
leads to incorrect environment detection and subsequent inaccurate skill selection, as
the system will not be aware of the updated project state.

if (cached && cached.cwd === cwd) return cached

const startTime = Date.now()
const timer = log.time("detect", { cwd, root })
const tags: string[] = []

const dirs = root && root !== cwd ? [cwd, root] : [cwd]

await Promise.all(
dirs.map((dir) => detectDir(dir, tags)),
)

// Deduplicate
const unique = [...new Set(tags)]

const result: Result = {
tags: unique,
detectedAt: Date.now(),
cwd,
}

cached = result
timer.stop()
log.info("detected", { tags: unique.join(","), cwd })

Tracer.active?.logSpan({
name: "fingerprint",
startTime,
endTime: Date.now(),
input: { cwd, root },
output: { tags: unique },
})

return result
}

async function detectDir(dir: string, tags: string[]): Promise<void> {
// Data-engineering detections only
const [
hasDbtProject,
hasProfilesYml,
hasSqlfluff,
hasDbtPackagesYml,
hasAirflowCfg,
hasDagsDir,
hasDatabricksYml,
] = await Promise.all([
Filesystem.exists(path.join(dir, "dbt_project.yml")),
Filesystem.exists(path.join(dir, "profiles.yml")),
Filesystem.exists(path.join(dir, ".sqlfluff")),
Filesystem.exists(path.join(dir, "dbt_packages.yml")),
Filesystem.exists(path.join(dir, "airflow.cfg")),
Filesystem.isDir(path.join(dir, "dags")),
Filesystem.exists(path.join(dir, "databricks.yml")),
])

// dbt detection
if (hasDbtProject) {
tags.push("dbt", "data-engineering")
}

// dbt packages
if (hasDbtPackagesYml) {
tags.push("dbt-packages")
}

// profiles.yml - extract adapter type
if (hasProfilesYml) {
try {
const content = await Filesystem.readText(path.join(dir, "profiles.yml"))
const adapterMatch = content.match(
/type:\s*(snowflake|bigquery|redshift|databricks|postgres|mysql|sqlite|duckdb|trino|spark|clickhouse)/i,
)
if (adapterMatch) {
tags.push(adapterMatch[1]!.toLowerCase())
}
} catch (e) {
log.debug("profiles.yml unreadable", { dir, error: e })
}
}

// SQL - check for .sqlfluff or any .sql files
if (hasSqlfluff) {
tags.push("sql")
} else {
try {
const sqlFiles = await Glob.scan("*.sql", {
cwd: dir,
include: "file",
})
if (sqlFiles.length > 0) {
Comment on lines +114 to +119
Copy link

Choose a reason for hiding this comment

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

Bug: The glob pattern *.sql only scans the root directory for SQL files, failing to detect them in subdirectories, which is standard for dbt projects.
Severity: MEDIUM

Suggested Fix

To correctly identify SQL files in a typical dbt project structure, update the glob pattern in fingerprint/index.ts from *.sql to **/*.sql. This will enable a recursive search through all subdirectories.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/opencode/src/altimate/fingerprint/index.ts#L114-L119

Potential issue: The SQL detection logic uses `Glob.scan("*.sql", ...)` to identify SQL
files for project fingerprinting. This pattern does not recursively search
subdirectories. Since dbt projects typically organize SQL models within subdirectories
like `models/`, the detection will fail for these standard project structures. This
results in an incomplete project fingerprint, as the `sql` tag will be missing.
Consequently, the LLM skill selector receives inaccurate environment information,
leading to suboptimal skill selection. The failure is silent due to an exception
handler.

tags.push("sql")
}
} catch (e) {
log.debug("sql glob scan failed", { dir, error: e })
}
}

// Airflow
if (hasAirflowCfg || hasDagsDir) {
tags.push("airflow")
}

// Databricks
if (hasDatabricksYml) {
tags.push("databricks")
}
}
}
40 changes: 39 additions & 1 deletion packages/opencode/src/altimate/observability/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface TraceSpan {
spanId: string
parentSpanId: string | null
name: string
kind: "session" | "generation" | "tool" | "text"
kind: "session" | "generation" | "tool" | "text" | "span"
startTime: number
endTime?: number
status: "ok" | "error"
Expand Down Expand Up @@ -246,6 +246,11 @@ interface TracerOptions {
}

export class Tracer {
// Global active tracer — set when a session starts, cleared on end.
private static _active: Tracer | null = null
static get active(): Tracer | null { return Tracer._active }
static setActive(tracer: Tracer | null) { Tracer._active = tracer }

private traceId: string
private sessionId: string | undefined
private rootSpanId: string | undefined
Expand Down Expand Up @@ -561,6 +566,39 @@ export class Tracer {
if (part.text != null) this.generationText.push(String(part.text))
}

/**
* Log a custom span (e.g., fingerprint detection, skill selection).
* Used for internal operations that aren't LLM generations or tool calls.
*/
logSpan(span: {
name: string
startTime: number
endTime: number
status?: "ok" | "error"
input?: unknown
output?: unknown
attributes?: Record<string, unknown>
}) {
if (!this.rootSpanId) return
try {
this.spans.push({
spanId: randomUUIDv7(),
parentSpanId: this.rootSpanId,
name: span.name,
kind: "span",
startTime: span.startTime,
endTime: span.endTime,
status: span.status ?? "ok",
input: span.input,
output: span.output,
attributes: span.attributes,
})
this.snapshot()
} catch {
// best-effort
}
}

/**
* Build a TraceFile snapshot of the current state (in-progress or complete).
* Used for incremental writes and live viewing.
Expand Down
Loading
Loading