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
4 changes: 2 additions & 2 deletions docs/docs/configure/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Agents define different AI personas with specific models, prompts, permissions,
| Agent | Description | Access Level |
|-------|------------|-------------|
| `builder` | Create and modify dbt models, SQL pipelines, and data transformations | Full read/write. SQL mutations prompt for approval. |
| `analyst` | Explore data, run SELECT queries, inspect schemas, generate insights | Read-only (enforced). SQL writes denied. Safe bash commands auto-allowed. |
| `analyst` | Answer questions about your data — explore it, run SELECT queries, inspect schemas, generate insights | Read-only (enforced). SQL writes denied. Safe bash commands auto-allowed. |
| `plan` | Plan before acting — restricted to planning files only | Minimal — no edits, no bash, no SQL |

### Builder
Expand All @@ -21,7 +21,7 @@ Full access mode. Can read/write files, run any bash command (with approval), ex

### Analyst

Truly read-only mode for safe data exploration:
The agent for asking questions about your data. Truly read-only mode for safe data exploration:

- **File access**: Read, grep, glob — no prompts
- **SQL**: SELECT queries execute freely. Write queries are denied (not prompted — blocked entirely)
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/data-engineering/agent-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ altimate runs in one of three specialized modes. Each mode has different permiss
| Mode | Access | Purpose |
|---|---|---|
| **Builder** | Read/Write | Create and modify data pipelines |
| **Analyst** | Read-only | Safe exploration and cost analysis |
| **Analyst** | Read-only | Answering questions about your data — safe exploration and cost analysis |
| **Plan** | Minimal | Planning only, no edits or execution |

## Builder
Expand Down Expand Up @@ -78,7 +78,7 @@ I'll create a staging model with proper typing, deduplication, and column naming

## Analyst

**Read-only access. Safe for production environments.**
**Read-only access. The agent for asking questions about your data and exploring it safely — use it whenever you just want answers, not changes. Safe for production environments.**

```bash
altimate --agent analyst
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ altimate offers specialized agent modes for different workflows:

| What do you want to do? | Use this agent mode |
|---|---|
| Analyzing data without risk of changes | **Analyst** for read-only queries, cost analysis, data profiling. SQL writes are blocked entirely. |
| Asking questions about your data, or analyzing it without risk of changes | **Analyst** for read-only queries, cost analysis, data profiling. SQL writes are blocked entirely. |
| Building or generating dbt models | **Builder** for model scaffolding, SQL generation, ref() wiring. SQL writes prompt for approval. |
| Planning before acting | **Plan** for outlining an approach before switching to builder to execute it |

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ altimate ships with specialized agent modes, each with its own tool permissions:
| Mode | Access | Use when you want to... |
| ----------- | ---------- | ------------------------------------------------------------------------------ |
| **Builder** | Read/Write | Create and modify SQL, dbt models, pipelines. SQL writes prompt for approval. |
| **Analyst** | Read-only | Explore production data safely, run cost analysis. SQL writes denied entirely. |
| **Analyst** | Read-only | Ask questions about your data, explore production data safely, run cost analysis. SQL writes denied entirely. |
| **Plan** | Minimal | Plan an approach before switching to builder to execute it |

Switch modes in the TUI:
Expand Down
68 changes: 29 additions & 39 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import PROMPT_TITLE from "./prompt/title.txt"
// PromptProfiles.PROMPT_BUILDER is assembled from core + pack fragments (byte-identical
// to the former builder.txt — see profiles.ts and test/altimate/prompt-profiles.test.ts)
import { PromptProfiles } from "../altimate/prompts/profiles"
import { Flag } from "@/flag/flag"
import PROMPT_ANALYST from "../altimate/prompts/analyst.txt"
import PROMPT_REVIEWER from "../altimate/prompts/reviewer.txt"
import { Log } from "../util/log"
// altimate_change end
import { Permission } from "@/permission"
import { mergeDeep, pipe, sortBy, values } from "remeda"
Expand Down Expand Up @@ -222,6 +222,12 @@ export const layer = Layer.effect(
const userWithSafety = Permission.merge(user, safetyDenials)
// altimate_change end

// altimate_change start — one-time warning state for the removed data-qa
// default-agent migration (see defaultInfo() below)
const log = Log.create({ service: "agent" })
let warnedRemovedDataQaDefault = false
// altimate_change end

const agents: Record<string, Info> = {
// altimate_change start - 3 modes: builder, analyst, plan (replaces upstream single "build" agent)
builder: {
Expand Down Expand Up @@ -318,43 +324,6 @@ export const layer = Layer.effect(
mode: "primary",
native: true,
},
// Opt-in data-qa profile (workload-adaptive harness PR 1): the invariant
// core + skills catalogue + teammate training — omits the Pre-Execution
// Protocol (sql-guard) pack and the build-oriented packs (dbt-ops,
// dbt-verify, dbt-workflow, pitfalls, self-review, finish). Ships the
// same DEFAULT permission ruleset as builder; per-agent config
// overrides apply per agent, as for every agent. Registered on any of
// three explicit opt-ins: ALTIMATE_DATA_QA_PROFILE=1/true, an
// `agent: {"data-qa": {...}}` entry in config (which then overlays the
// native profile via the standard merge below), or `default_agent:
// "data-qa"` (naming it as the default is itself an explicit
// selection — without this arm, defaultInfo() would throw "default
// agent \"data-qa\" not found" instead of registering it). Nothing
// selects it implicitly otherwise; the default agent stays builder.
...(Flag.truthyEnv("ALTIMATE_DATA_QA_PROFILE") ||
Comment thread
anandgupta42 marked this conversation as resolved.
cfg.agent?.["data-qa"] != null ||
cfg.default_agent === "data-qa"
? {
"data-qa": {
name: "data-qa",
description:
"Opt-in data Q&A profile: builder toolset with a slimmer prompt (no dbt build protocols).",
prompt: PromptProfiles.PROMPT_DATA_QA,
options: {},
permission: Permission.merge(
defaults,
Permission.fromConfig({
question: "allow",
plan_enter: "allow",
sql_execute_write: "ask",
}),
userWithSafety,
),
mode: "primary",
native: true,
} satisfies Info,
}
: {}),
// reviewer agent: dbt PR review verdict engine
Comment thread
anandgupta42 marked this conversation as resolved.
Comment thread
anandgupta42 marked this conversation as resolved.
reviewer: {
name: "reviewer",
Expand Down Expand Up @@ -630,7 +599,28 @@ export const layer = Layer.effect(
const defaultInfo = Effect.fnUntraced(function* () {
const c = yield* config.get()
if (c.default_agent) {
const agent = agents[c.default_agent]
// altimate_change start — migrate the removed data-qa default to analyst
let agent = agents[c.default_agent]
// #1217 let `default_agent: "data-qa"` alone (no matching `agent.data-qa`
// config entry) opt into the native data-qa profile. That profile is now
// removed, so a persisted `default_agent: "data-qa"` would otherwise throw
// here and strand every call site that resolves the default agent
// (session/prompt.ts, the session HTTP routes, ACP) — upgrading users could
// no longer start an ordinary default-agent session. Fall back to `analyst`,
// the documented agent for read-only data questions, with a one-time warning
// instead of a hard failure. A user who separately defines their own
// `agent.data-qa` config entry is unaffected — `agents[c.default_agent]`
// already resolves to that legitimate custom agent above.
if (!agent && c.default_agent === "data-qa") {
if (!warnedRemovedDataQaDefault) {
warnedRemovedDataQaDefault = true
log.warn(
'the "data-qa" agent was removed; defaulting to "analyst" for read-only data questions — set default_agent to override',

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: The advertised one-time migration warning never reaches default TUI users. log.warn goes through the Log shim (src/altimate/util/log.ts), whose shouldLog requires printEnabled() — it only writes when OPENCODE_PRINT_LOGS/ALTIMATE_PRINT_LOGS is set, and that is deliberately OFF for the in-process TUI server. So an upgrading user whose persisted default_agent: "data-qa" is silently redirected to analyst will not see the notice that the PR description promises ("Fall back to analyst ... with a one-time warning"). Consider surfacing the migration through a channel that reaches interactive users (e.g. a session/notification) rather than the print-gated stderr log.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/agent/agent.ts, line 618:

<comment>The advertised one-time migration warning never reaches default TUI users. `log.warn` goes through the `Log` shim (`src/altimate/util/log.ts`), whose `shouldLog` requires `printEnabled()` — it only writes when `OPENCODE_PRINT_LOGS`/`ALTIMATE_PRINT_LOGS` is set, and that is deliberately OFF for the in-process TUI server. So an upgrading user whose persisted `default_agent: "data-qa"` is silently redirected to `analyst` will not see the notice that the PR description promises ("Fall back to `analyst` ... with a one-time warning"). Consider surfacing the migration through a channel that reaches interactive users (e.g. a session/notification) rather than the print-gated stderr log.</comment>

<file context>
@@ -592,7 +599,28 @@ export const layer = Layer.effect(
+              if (!warnedRemovedDataQaDefault) {
+                warnedRemovedDataQaDefault = true
+                log.warn(
+                  'the "data-qa" agent was removed; defaulting to "analyst" for read-only data questions — set default_agent to override',
+                )
+              }
</file context>

)
Comment on lines +617 to +619

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 Badge Surface the migration warning in normal runs

When a user upgrades with default_agent: "data-qa", this warning is sent through the fork-local Log shim, whose output is disabled unless ALTIMATE_PRINT_LOGS or OPENCODE_PRINT_LOGS is explicitly enabled (src/altimate/util/log.ts). Normal CLI and TUI users therefore receive no warning that their configured writable agent was silently replaced by the read-only analyst, and the configuration remains stale, so the silent substitution repeats on later starts. Emit this through a user-visible diagnostic channel or persistently migrate the setting.

Useful? React with 👍 / 👎.

}
agent = agents["analyst"]
}
// altimate_change end
if (!agent) throw new Error(`default agent "${c.default_agent}" not found`)
if (agent.mode === "subagent") throw new Error(`default agent "${c.default_agent}" is a subagent`)
if (agent.hidden === true) throw new Error(`default agent "${c.default_agent}" is hidden`)
Expand Down
13 changes: 0 additions & 13 deletions packages/opencode/src/altimate/prompts/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,24 +60,11 @@ export const BUILDER_PROFILE: readonly FragmentName[] = [
"finish",
]

/**
* Opt-in data-qa profile: the invariant core + skills catalogue + teammate
* training. Relative to builder it omits the Pre-Execution Protocol
* (sql-guard) pack and the build-oriented packs: dbt-ops, dbt-verify,
* dbt-workflow, pitfalls, self-review, finish. Basis: an internal 540-trial
* paired prompt ablation on a public benchmark found removing these on data-QA
* workloads had no score effect (permutation p=0.74) and cut wall clock 27.6%.
* Nothing selects this profile automatically — see `agent.ts`
* (ALTIMATE_DATA_QA_PROFILE gate).
*/
export const DATA_QA_PROFILE: readonly FragmentName[] = ["core", "legacy-skills-catalogue", "core-training"]

export function assemble(profile: readonly FragmentName[]): string {
return profile.map((name) => FRAGMENTS[name]).join("")
}

export const PROMPT_BUILDER = assemble(BUILDER_PROFILE)
export const PROMPT_DATA_QA = assemble(DATA_QA_PROFILE)

export * as PromptProfiles from "./profiles"
// altimate_change end
15 changes: 6 additions & 9 deletions packages/opencode/src/session/termination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export function explicitDoneStop(input: {
}

/**
* Run-mode completion instruction for builder and builder-derived agents.
* Run-mode completion instruction for builder.
*
* This wording lived in `builder.txt`, but builder is a PRIMARY agent, so a
* static instruction there also governs interactive chat — where nothing
Expand All @@ -183,10 +183,9 @@ export function explicitDoneStop(input: {
* is only consumed by the run-mode accounting path.
*
* Injected only in run mode, and only for the agents named in
* COMPLETION_CONTRACT_AGENTS below (builder, plus the opt-in data-qa profile
* — see that set for why). For builder alone this is byte-identical to the
* previous run-mode behaviour, when builder was the only prompt carrying it.
* Prompt-visible text — changes need extra review.
* COMPLETION_CONTRACT_AGENTS below (builder). Byte-identical to builder's
* original run-mode behaviour, from when this text was still static in
* `builder.txt`. Prompt-visible text — changes need extra review.
*/
export const RUN_MODE_COMPLETION_INSTRUCTION =
"**Signal completion explicitly**: only after every requirement above is satisfied, end your final " +
Expand All @@ -195,11 +194,9 @@ export const RUN_MODE_COMPLETION_INSTRUCTION =

/**
* Agents that receive the run-mode completion-token contract. builder is the
* historical carrier; data-qa is the builder-derived opt-in profile (its
* headless runs need a termination contract without inheriting the dbt
* finish-build ritual, which lives in the prompt packs it omits).
* historical (and currently sole) carrier.
*/
const COMPLETION_CONTRACT_AGENTS = new Set(["builder", "data-qa"])
const COMPLETION_CONTRACT_AGENTS = new Set(["builder"])

/** The sole gate for injecting the completion-token contract into a prompt. */
export function completionInstruction(input: { runMode: boolean; agent: string }): string | undefined {
Expand Down
85 changes: 85 additions & 0 deletions packages/opencode/test/agent/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,42 @@ it.instance(
},
)

it.instance(
// Regression for the data-qa agent removal: #1217 shipped `data-qa` as a
// config-registerable native profile, so an upgrading user's config may
// still carry a leftover `agent: {"data-qa": {...}}` block after this
// profile was dropped. That entry now takes the exact same path as any
// other user-defined agent name not registered natively (see "custom agent
// from config creates new agent" above) — it resolves to a plain generic
// agent (mode "all", native: false, no `data-qa`-specific prompt), it does
// not crash, and it does not resurrect the removed profile's permissions.
"a leftover config `agent.data-qa` entry resolves as a harmless generic custom agent, not a crash",
() =>
Effect.gen(function* () {
const dataQa = yield* load((svc) => svc.get("data-qa"))
expect(dataQa).toBeDefined()
expect(dataQa?.native).toBe(false)
expect(dataQa?.mode).toBe("all")
expect(dataQa?.description).toBe("leftover config from an old data-qa opt-in")
// No native data-qa prompt exists anymore to resurrect.
expect(dataQa?.prompt).toBeUndefined()
// The rest of the registry is unaffected.
const builder = yield* load((svc) => svc.get("builder"))
expect(builder?.native).toBe(true)
const fallback = yield* load((svc) => svc.defaultAgent())
expect(fallback).toBe("builder")
}),
{
config: {
agent: {
"data-qa": {
description: "leftover config from an old data-qa opt-in",
},
},
},
},
)

it.instance(
"agent disable removes agent from list",
() =>
Expand Down Expand Up @@ -775,6 +811,55 @@ it.instance(
},
)

it.instance(
// Regression for the data-qa agent removal: #1217 let `default_agent:
// "data-qa"` alone (no `agent.data-qa` config entry) opt into the native
// data-qa profile. An upgrading user's persisted config can still set
// this. Unlike a generic typo'd/never-valid default_agent (which still
// throws — see the test above), this specific removed name must degrade
// to a working session instead of stranding the user: it resolves to
// `analyst`, the documented replacement for read-only data questions.
"defaultAgent migrates a persisted default_agent: \"data-qa\" (no matching agent entry) to analyst instead of throwing",
() =>
Effect.gen(function* () {
const agent = yield* load((svc) => svc.defaultAgent())
expect(agent).toBe("analyst")
const info = yield* load((svc) => svc.defaultInfo())
expect(info?.name).toBe("analyst")
expect(info?.native).toBe(true)
}),
{
config: {
default_agent: "data-qa",
},
},
)

it.instance(
// A user who ALSO defines their own `agent.data-qa` config entry gets
// that legitimate custom agent as their default — the migration above
// only kicks in when no agent actually resolves for the name.
"defaultAgent respects an explicit agent.data-qa config entry over the analyst migration",
() =>
Effect.gen(function* () {
const agent = yield* load((svc) => svc.defaultAgent())
expect(agent).toBe("data-qa")
const info = yield* load((svc) => svc.defaultInfo())
expect(info?.native).toBe(false)
expect(info?.description).toBe("my own data-qa agent")
}),
{
config: {
default_agent: "data-qa",
agent: {
"data-qa": {
description: "my own data-qa agent",
},
},
},
},
)

it.instance(
"defaultAgent returns plan when build is disabled and default_agent not set",
() =>
Expand Down
Loading
Loading