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
17 changes: 0 additions & 17 deletions packages/opencode/src/altimate/prompts/builder.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,6 @@ When creating dbt models:
- Update schema.yml files alongside model changes
- Run `lineage_check` to verify column-level data flow


## Pre-Execution Protocol

Before executing ANY SQL via sql_execute, follow this mandatory sequence:

1. **Analyze first**: Run `sql_analyze` on the query. Check for HIGH severity anti-patterns.
- If HIGH severity issues found (SELECT *, cartesian products, missing WHERE on DELETE/UPDATE, full table scans on large tables): FIX THEM before executing. Show the user what you found and the fixed query.
- If MEDIUM severity issues found: mention them and proceed unless the user asks to fix.

2. **Validate syntax**: Run `altimate_core_validate` to catch syntax errors and schema issues BEFORE hitting the warehouse.

3. **Execute**: Only after steps 1-2 pass, run `sql_execute`.

This sequence is NOT optional. Skipping it means the user pays for avoidable mistakes. You are the customer's cost advocate — every credit saved is trust earned. If the user explicitly requests skipping the protocol, note the risk and proceed.

For trivial queries (e.g., `SELECT 1`, `SHOW TABLES`), use judgment — skip the full sequence but still validate syntax.

## dbt Verification Workflow

After ANY dbt operation (build, run, test, model creation/modification):
Expand Down
257 changes: 257 additions & 0 deletions packages/opencode/src/session/pre-execution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
// Fork-only module — owns the PRE-EXECUTION PROTOCOL SCOPING CONTRACT.
//
// The protocol below used to sit statically in `altimate/prompts/builder.txt`.
// builder is a PRIMARY agent, so a static section there governs every builder
// surface at once: dbt authoring, interactive chat, and headless
// question-answering runs. A pre-registered paired ablation (540 trials on a
// public data-question benchmark, one binary across both arms) measured what
// the section costs on the question-answering surface:
//
// macro Pass@1 control 0.6667 → treatment 0.6807
// delta +0.0140, query-blocked permutation p = 0.7358,
// cluster-bootstrap 95% CI [-0.0400, +0.0674] → no score effect
// wall clock 440.9s → 319.4s (-27.6%)
// model turns -27.7%, generation time -32.2%
// `altimate_core_validate` + `sql_analyze` calls 2,805 → 0
// `sql_execute` calls +49% (the freed budget went into real querying)
//
// The 2,805 → 0 is the one number directly attributable to this text: the
// ritual is prompt-ordered, and deleting the order deletes it completely. The
// latency win is NOT attributable to this section alone — that treatment arm
// bundled five coupled changes and the experiment declined to attribute.
//
// So this module SCOPES rather than deletes. The measurement covers exactly
// one cell — headless question-answering in a workspace with no dbt project —
// and that is the only cell where the protocol is dropped. dbt work and
// interactive chat, where a pre-execution discipline may genuinely earn its
// place, are unmeasured and keep it. Anything that cannot be classified
// confidently keeps it too: the cost of keeping it is latency on one workload,
// the cost of wrongly dropping it is unmeasured.
//
// Directive text lives here (not at the call site) so any wording-change review
// covers ONE file, mirroring session/termination.ts.

import fs from "fs/promises"
import path from "path"
import { Log } from "../util/log"

const log = Log.create({ service: "pre-execution-scope" })

/** Files that mark a directory as a dbt project root. */
const PROJECT_FILES = ["dbt_project.yml", "dbt_project.yaml"] as const

/**
* Subdirectories never considered candidates for a nested dbt project, mirroring
* `findDbtProjectRoot`'s skip list so a fixture project shipped inside
* `node_modules/foo/` or a compiled artifact in `target/` is not mistaken for
* the user's real project.
*/
const SKIP_DIRS = new Set(["node_modules", "target"])

/**
* The `errno` code of a filesystem rejection, when it carries one.
*
* `ENOENT` and `ENOTDIR` are real answers — nothing is there. Every other code,
* and an error carrying no code at all, means the question went unanswered.
*/
function errnoCode(err: unknown): string | undefined {
if (typeof err !== "object" || err === null || !("code" in err)) return undefined
const code = err.code
return typeof code === "string" ? code : undefined
}

/** True when a rejection means "nothing is there", rather than "could not tell". */
function meansAbsent(err: unknown): boolean {
const code = errnoCode(err)
return code === "ENOENT" || code === "ENOTDIR"
}

/**
* How a workspace classifies for the purpose of this gate.
*
* `unknown` is a real, load-bearing state, not a placeholder: it is what we
* report when the filesystem could not answer the question, and it keeps the
* protocol. The existing `findDbtProjectRoot` helper cannot serve this gate
* directly because it collapses "no project here" and "could not look here"
* into the same `null`, and this gate turns a directive off on the difference.
*/
export type WorkspaceShape = "dbt" | "non-dbt" | "unknown"

/**
* The mandatory pre-execution sequence, verbatim as it shipped in
* `builder.txt`. Prompt-visible text — changes need extra review.
*
* Kept byte-identical to the previous static section so that in every case
* where the gate injects it, the resolved prompt is unchanged from before.
*/
export const PRE_EXECUTION_PROTOCOL = [
"## Pre-Execution Protocol",
"",
"Before executing ANY SQL via sql_execute, follow this mandatory sequence:",
"",
"1. **Analyze first**: Run `sql_analyze` on the query. Check for HIGH severity anti-patterns.",
" - If HIGH severity issues found (SELECT *, cartesian products, missing WHERE on DELETE/UPDATE, full table scans on large tables): FIX THEM before executing. Show the user what you found and the fixed query.",
" - If MEDIUM severity issues found: mention them and proceed unless the user asks to fix.",
"",
"2. **Validate syntax**: Run `altimate_core_validate` to catch syntax errors and schema issues BEFORE hitting the warehouse.",
"",
"3. **Execute**: Only after steps 1-2 pass, run `sql_execute`.",
"",
"This sequence is NOT optional. Skipping it means the user pays for avoidable mistakes. You are the customer's cost advocate — every credit saved is trust earned. If the user explicitly requests skipping the protocol, note the risk and proceed.",
"",
"For trivial queries (e.g., `SELECT 1`, `SHOW TABLES`), use judgment — skip the full sequence but still validate syntax.",
].join("\n")

/**
* Does `dir` itself contain a dbt project file?
*
* Returns `undefined` — not `false` — when the filesystem could not answer.
* ENOENT and ENOTDIR are real answers ("nothing there"); anything else (EACCES,
* EIO, a transient network mount failure) is not, and must not be read as
* "no dbt project here".
*/
async function hasProjectFile(dir: string): Promise<boolean | undefined> {
let sawUnknown = false
for (const name of PROJECT_FILES) {
try {
if ((await fs.stat(path.join(dir, name))).isFile()) return true
} catch (err) {
if (meansAbsent(err)) continue
log.warn("project-file probe failed", { dir, name, code: errnoCode(err) })
sawUnknown = true
}
}
return sawUnknown ? undefined : false
}

/**
* Classify a workspace by the presence of a dbt project.
*
* `dbt` requires an actual `dbt_project.yml` (or `.yaml`) FILE at, above, or
* one level below a candidate directory:
*
* - **at** the candidate,
* - **above** it, every ancestor up to the filesystem root. A session is
* routinely started inside `models/`, and on a non-git project the worktree
* candidate is the same directory, so nothing else would find the project.
* The walk is deliberately unbounded: a depth limit would have to report
* "I stopped early" as `unknown` to stay honest, which on any deep tree
* turns the gate off entirely. Two `stat` calls per level, in run mode
* only, is not worth that.
* - **one level below** it, which is how benchmark and monorepo layouts nest
* a project (the same rule, and the same skip list, as
* `findDbtProjectRoot`).
*
* A project found in an unrelated ancestor is a false positive that KEEPS the
* protocol, which is the safe direction.
*
* `non-dbt` requires at least one candidate the scan examined COMPLETELY — its
* symlinks resolved, every ancestor probe answered up to the root, and its own
* children enumerated and probed — with no project found. If no candidate
* managed that, the answer is `unknown`, which keeps the protocol. One complete
* answer is enough: the ancestor walk from that candidate already covers the
* worktree above it, so a partner candidate that could not be read has nothing
* left to contribute.
*/
export async function classifyWorkspace(candidates: (string | undefined)[]): Promise<WorkspaceShape> {
const dirs = [...new Set(candidates.filter((d): d is string => !!d))]
let sawCompleteAnswer = false

for (const dir of dirs) {
let complete = true

// Resolve symlinks first. `path.resolve` is lexical, so a symlinked cwd
// (`/tmp/ws` -> `/repo/models`) would walk `/tmp` and `/` and never see the
// project the session is actually inside.
let start: string
try {
start = await fs.realpath(dir)
} catch (err) {
if (!meansAbsent(err)) log.warn("candidate realpath failed", { dir, code: errnoCode(err) })
continue
}

// At the candidate, then upwards to the filesystem root.
let current = start
for (;;) {
const found = await hasProjectFile(current)
if (found === true) return "dbt"
if (found === undefined) complete = false
const parent = path.dirname(current)
if (parent === current) break
current = parent
}

// One level below. The filesystem root is a legitimate stop rather than a
// directory to enumerate — a non-git project sets worktree to it, and
// scanning its children is meaningless and can be slow or permission-denied
// — so the root on its own never yields a complete answer.
if (start === path.parse(start).root) {
complete = false

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: A filesystem-root candidate forces unknown, so headless non-dbt runs in non-git workspaces never drop the protocol

classifyWorkspace no longer filters the filesystem root out of dirs (the previous version excluded it via d !== path.parse(d).root). A non-git project sets Instance.worktree to / (see packages/opencode/src/storage/storage.ts:94), so directories is [cwd, "/"]. For the / candidate this branch marks complete = false, which sets sawIncomplete = true, and the final return sawCompleteAnswer && !sawIncomplete ? "non-dbt" : "unknown" then returns "unknown" even though the real cwd candidate was examined completely. Result: the protocol is always kept for non-git headless runs — the exact surface this PR targets — contradicting the docstring, which says root should force unknown only when it is the only candidate. Skip the root candidate rather than marking it incomplete.

Suggested change
complete = false
continue

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

} else {
let entries
try {
entries = await fs.readdir(start, { withFileTypes: true })
} catch (err) {
if (!meansAbsent(err)) log.warn("workspace enumeration failed", { dir: start, code: errnoCode(err) })
entries = undefined
}
if (entries === undefined) {
complete = false
} else {
// Probe everything that is not plainly a regular file. Filtering on
// `isDirectory()` would silently skip symlinked directories and any
// entry whose type the filesystem did not report, and a skipped entry
// is an unexamined one. `hasProjectFile` on a non-directory just gets
// ENOTDIR, which is a real "nothing there".
const children = entries
.filter((e) => !e.isFile() && !e.name.startsWith(".") && !SKIP_DIRS.has(e.name))
// Deterministic order: fs.readdir's order varies across filesystems.
.sort((a, b) => a.name.localeCompare(b.name))
for (const child of children) {
const found = await hasProjectFile(path.join(start, child.name))
if (found === true) return "dbt"
if (found === undefined) complete = false
}
}
}

if (complete) sawCompleteAnswer = true
}

return sawCompleteAnswer ? "non-dbt" : "unknown"
}

/**
* The sole gate for injecting the pre-execution protocol into a prompt.
*
* Returns the protocol text to inject, or `undefined` to drop it. The ONLY
* dropping case is the one the ablation measured:
*
* run mode (headless / CI, the `run` CLI) AND
* the builder agent (the only prompt that ever carried the section) AND
* a workspace confidently classified as having no dbt project.
*
* Everything else keeps it, including `unknown`. Note the asymmetry is
* deliberate: run mode is not itself a task-shape signal, it is the surface the
* evidence covers. Widening this to interactive chat needs its own measurement.
*
* Classification is only performed when the cheap conditions already hold, so
* an interactive session pays no filesystem cost for this gate.
*/
export async function preExecutionInstruction(input: {
runMode: boolean
agent: string
/** Candidate directories to classify — typically the cwd and the worktree root. */
directories: (string | undefined)[]
}): Promise<string | undefined> {
// Only builder ever carried this section; analyst and reviewer never did.
if (input.agent !== "builder") return undefined

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 Key the gate on the builder's registry identity

When agent.builder.name is customized, the loader retains the agent under the builder registry key but replaces Info.name (src/agent/agent.ts:541), and prompt.ts passes that mutable name here. The renamed native builder therefore fails this check in every run mode, so the protocol removed from builder.txt is never restored even for interactive or dbt sessions; conversely, a custom agent named builder receives instructions it never previously had. Pass the selected registry key or another stable native-agent identity to the gate instead of using the configurable display name.

Useful? React with 👍 / 👎.

if (!input.runMode) return PRE_EXECUTION_PROTOCOL

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: Only inject PRE_EXECUTION_PROTOCOL when the resolved builder prompt is the built-in prompt; this branch adds it to custom builder prompts and can duplicate intentional instructions.

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

<comment>Only inject `PRE_EXECUTION_PROTOCOL` when the resolved builder prompt is the built-in prompt; this branch adds it to custom builder prompts and can duplicate intentional instructions.</comment>

<file context>
@@ -0,0 +1,140 @@
+}): Promise<string | undefined> {
+  // Only builder ever carried this section; analyst and reviewer never did.
+  if (input.agent !== "builder") return undefined
+  if (!input.runMode) return PRE_EXECUTION_PROTOCOL
+  const shape = await classifyWorkspace(input.directories)
+  if (shape !== "non-dbt") return PRE_EXECUTION_PROTOCOL
</file context>

const shape = await classifyWorkspace(input.directories)

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 Preserve custom builder prompt semantics

When a user overrides agent.builder.prompt or supplies .altimate-code/agents/builder.md, the agent loader replaces the built-in prompt (src/agent/agent.ts:508-534), so previously the protocol was absent or user-authored. Keying this injection only on the agent name now unconditionally appends the stock protocol in interactive and dbt sessions, potentially duplicating a custom protocol or overriding an intentional customization; inject it only when the applicable prompt previously contained the moved section.

Useful? React with 👍 / 👎.

if (shape !== "non-dbt") return PRE_EXECUTION_PROTOCOL
log.info("pre-execution protocol scoped out", { agent: input.agent, shape })
return undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Gate protocol removal on task intent, not non-dbt alone; this return drops the analyze/validate sequence for every headless builder request in a non-dbt workspace.

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

<comment>Gate protocol removal on task intent, not `non-dbt` alone; this return drops the analyze/validate sequence for every headless builder request in a non-dbt workspace.</comment>

<file context>
@@ -0,0 +1,140 @@
+  const shape = await classifyWorkspace(input.directories)
+  if (shape !== "non-dbt") return PRE_EXECUTION_PROTOCOL
+  log.info("pre-execution protocol scoped out", { agent: input.agent, shape })
+  return undefined
+}
+
</file context>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove the remaining mandatory validation directives

When this returns undefined for a headless non-dbt question, the builder still receives builder.txt:42-44, which says to always run sql_analyze when writing SQL and to run altimate_core_validate before warehouse execution. Those are the same two ritual calls this gate is intended to eliminate, so the stock prompt continues ordering them even though the named protocol section is absent and the claimed latency/tool-call reduction may not materialize; scope or rewrite these duplicate directives alongside the protocol.

Useful? React with 👍 / 👎.

}
Comment on lines +253 to +255

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate on question-answering intent, not just workspace layout

A non-dbt workspace does not imply a question-answering task: for example, run can start in an empty directory with a builder request to modify production tables or author a standalone SQL pipeline. This branch removes the analyze-and-validate protocol for every such headless builder run even though the cited measurement covered only question answering, so unmeasured and potentially destructive SQL workflows lose the safety checks; the dropping condition needs an actual task-intent signal rather than treating non-dbt as equivalent to the benchmark workload.

Useful? React with 👍 / 👎.


export * as SessionPreExecution from "./pre-execution"
20 changes: 20 additions & 0 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { type Tool as AITool, tool, jsonSchema, type ToolCallOptions, asSchema }
import { SessionCompaction } from "./compaction"
import { NudgeArbiter } from "./nudge"
import { SessionTermination } from "./termination"
// altimate_change — task-shape-scoped pre-execution protocol (see pre-execution.ts)
import { SessionPreExecution } from "./pre-execution"
import { Instance } from "../project/instance"
import { Bus } from "../bus"
import { ProviderTransform } from "../provider/transform"
Expand Down Expand Up @@ -1456,6 +1458,24 @@ export namespace SessionPrompt {
...(await InstructionPrompt.system()),
...hoistedReminders,
]
// altimate_change start — task-shape-scoped pre-execution protocol. Same
// shape as the completion instruction below and for the same reason: the
// text used to sit in builder.txt, and builder is a PRIMARY agent, so it
// reached every builder surface. A paired ablation measured it as pure
// overhead on headless question-answering (2,805 ritual tool calls → 0, no
// score effect) but says nothing about dbt work or interactive chat, so
// those keep it. See session/pre-execution.ts for the gate and the numbers.
//
// Injected BEFORE the completion instruction, which says to signal DONE
// only once "every requirement above" is satisfied. A mandatory protocol
// pushed after it would not be one of those requirements.
const preExecutionInstruction = await SessionPreExecution.preExecutionInstruction({

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: The dbt classification re-runs on every loop() step

system is rebuilt each iteration of the while (true) loop, and this call is not guarded by step === 1, so every step in a headless builder run re-executes classifyWorkspace — a readdir of the worktree root plus one stat per top-level subdirectory. The result is deterministic within a single loop() invocation (same agent, same directories), so it could be computed once and reused, mirroring the step === 1 guard already applied to the trace span just below.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

runMode: Flag.ALTIMATE_RUN_MODE,
Comment on lines +1471 to +1473

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 Avoid rescanning the workspace on every model step

In a headless builder session, this call runs inside SessionPrompt.loop's per-generation while loop, so every tool-call continuation repeats realpath, all ancestor probes, readdir, and sequential project-file stats for each child. On large monorepos or network-mounted workspaces, a multi-step run can therefore incur the full metadata-scan latency many times, undermining the latency reduction this gate targets; cache the classification for the turn or invalidate it only after relevant filesystem changes.

Useful? React with 👍 / 👎.

agent: agent.name,
directories: [Instance.directory, Instance.worktree],

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 gate runs on the hot path: preExecutionInstruction is awaited on every prompt build (every model turn in loop()). For run-mode builder sessions in a non-dbt workspace — the exact workload this PR is trying to make faster — each turn now performs a fresh findDbtProjectRoot, i.e. synchronous statSync (via Filesystem.isDir) plus fs.readdir/fs.stat on the candidate directories, with no caching across steps. The classification is constant for a session, so it is computed repeatedly on every step, adding per-turn filesystem I/O that partially offsets the latency the change targets.

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

<comment>The gate runs on the hot path: `preExecutionInstruction` is awaited on every prompt build (every model turn in `loop()`). For run-mode builder sessions in a non-dbt workspace — the exact workload this PR is trying to make faster — each turn now performs a fresh `findDbtProjectRoot`, i.e. synchronous `statSync` (via Filesystem.isDir) plus `fs.readdir`/`fs.stat` on the candidate directories, with no caching across steps. The classification is constant for a session, so it is computed repeatedly on every step, adding per-turn filesystem I/O that partially offsets the latency the change targets.</comment>

<file context>
@@ -1468,6 +1470,20 @@ export namespace SessionPrompt {
+      const preExecutionInstruction = await SessionPreExecution.preExecutionInstruction({
+        runMode: Flag.ALTIMATE_RUN_MODE,
+        agent: agent.name,
+        directories: [Instance.directory, Instance.worktree],
+      })
+      if (preExecutionInstruction) system.push(preExecutionInstruction)
</file context>

})
if (preExecutionInstruction) system.push(preExecutionInstruction)

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 claim that every kept case leaves 'the resolved prompt unchanged from before' holds only for the text content, not its position. Previously the protocol was a static section inside builder.txt, which becomes agent.prompt and is emitted as the leading system part in llm/request.ts / llm.ts. Now the same text is pushed at the end of the session system array (after hoistedReminders, alongside the completion instruction). Instruction ordering in a system prompt is behaviorally relevant for LLM adherence, so every builder surface that 'keeps' it—interactive chat and run-mode dbt workspaces—actually sees the protocol repositioned relative to the rest of the prompt, not an unchanged prompt. Worth confirming the intended order and, if unchanged behavior is required, inserting at the corresponding position.

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

<comment>The claim that every kept case leaves 'the resolved prompt unchanged from before' holds only for the text content, not its position. Previously the protocol was a static section inside builder.txt, which becomes `agent.prompt` and is emitted as the leading system part in llm/request.ts / llm.ts. Now the same text is pushed at the end of the session `system` array (after hoistedReminders, alongside the completion instruction). Instruction ordering in a system prompt is behaviorally relevant for LLM adherence, so every builder surface that 'keeps' it—interactive chat and run-mode dbt workspaces—actually sees the protocol repositioned relative to the rest of the prompt, not an unchanged prompt. Worth confirming the intended order and, if unchanged behavior is required, inserting at the corresponding position.</comment>

<file context>
@@ -1468,6 +1470,20 @@ export namespace SessionPrompt {
+        agent: agent.name,
+        directories: [Instance.directory, Instance.worktree],
+      })
+      if (preExecutionInstruction) system.push(preExecutionInstruction)
+      // altimate_change end
       const format = lastUser.format ?? { type: "text" }
</file context>

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 Keep repository instructions after the injected protocol

When a workspace's AGENTS.md or configured instructions relax or replace this SQL workflow (for example because the dialect is unsupported by altimate_core_validate), this append reverses the previous ordering: the protocol formerly lived in agent.prompt, which LLM.stream places before InstructionPrompt.system(), but it now follows those repository instructions and presents a later mandatory conflicting directive. This changes behavior even for the stock builder prompt; insert the protocol before the instruction-file entries so their established precedence is preserved.

Useful? React with 👍 / 👎.

// altimate_change end
// altimate_change start — run-mode-only completion instruction. This text
// used to sit in builder.txt, but builder is a PRIMARY agent, so it also
// reached interactive chat, where nothing interprets or strips the token
Expand Down
24 changes: 20 additions & 4 deletions packages/opencode/test/altimate/sql-validation-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
* 4. altimate_core_check composite pipeline works end-to-end
* 5. sql.analyze composite pipeline works end-to-end
* 6. Pre-execution protocol tools are callable (sql_analyze → altimate_core_validate → sql_execute)
* (the protocol TEXT itself is gated in session/pre-execution.ts, tested there)
* 7. sql-classify correctly gates sql_execute
* 8. Analyst and builder agent permissions are consistent with their prompts
*/

import { describe, expect, test, beforeAll, afterAll } from "bun:test"
import fs from "fs"
import path from "path"
import { SessionPreExecution } from "../../src/session/pre-execution"
import * as Dispatcher from "../../src/altimate/native/dispatcher"
import { registerAll } from "../../src/altimate/native/altimate-core"
import { registerAllSql } from "../../src/altimate/native/sql/register"
Expand Down Expand Up @@ -95,22 +97,36 @@ describe("Tool name consistency in prompts", () => {
})
})

test("builder prompt contains pre-execution protocol with correct tool names", async () => {
test("builder prompt names the pre-execution tools", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const builder = await Agent.get("builder")
expect(builder).toBeDefined()
// Pre-Execution Protocol references:
expect(builder!.prompt).toContain("sql_analyze")
expect(builder!.prompt).toContain("altimate_core_validate")
expect(builder!.prompt).toContain("sql_execute")
// The protocol section itself
expect(builder!.prompt).toContain("Pre-Execution Protocol")
},
})
})

// The protocol section itself is no longer static in builder.txt — it is
// injected per-session by the gate in session/pre-execution.ts, which drops it
// only for headless builder runs in a workspace with no dbt project. The
// wording is unchanged; see test/session/pre-execution.test.ts for the gate.
test("the pre-execution protocol still reaches an interactive builder", async () => {
await using tmp = await tmpdir()
const instruction = await SessionPreExecution.preExecutionInstruction({
runMode: false,
agent: "builder",
directories: [tmp.path],
})
expect(instruction).toContain("Pre-Execution Protocol")
expect(instruction).toContain("sql_analyze")
expect(instruction).toContain("altimate_core_validate")
expect(instruction).toContain("sql_execute")
})
})

// ---------------------------------------------------------------------------
Expand Down
Loading
Loading