Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
39fac46
Move the website's Voice code under the Brunch plugin folder
lunelson Oct 13, 2026
6ad1b8f
Assemble Brunch's assistant in its plugin folder instead of the demo …
lunelson Oct 13, 2026
51bb31d
Give Brunch's cards the editor through a Brunch plugin's api
lunelson Oct 13, 2026
8f67af4
Colocate Brunch agent unit tests with their source modules
lunelson Oct 13, 2026
e06eaab
Move history-retention crash helpers into Brunch integration tests
lunelson Oct 13, 2026
c1b9e0e
Move Brunch code shared with the Petrinaut website into private libra…
lunelson Oct 13, 2026
e3a955f
Test the website's pending tool rows against Brunch's production admi…
lunelson Oct 13, 2026
f54186c
Render the Ledger refusal test through Petrinaut's public entry and f…
lunelson Oct 13, 2026
00ab3a1
Check the stream-loss witness's stored place by name now that place i…
lunelson Oct 13, 2026
ce106f8
Stop giving the stock assistant Brunch's conversation id
lunelson Oct 13, 2026
029b69d
Colocate the Brunch library's unit tests with their source modules
lunelson Oct 13, 2026
7d59271
Run the website's Brunch integration tests from their own package
lunelson Oct 13, 2026
81f9df7
Hand Brunch the host's plugins so its cards always get the Brunch plugin
lunelson Oct 13, 2026
9e75727
Keep Brunch's test fixtures and snapshots out of its production image
lunelson Oct 13, 2026
3e213b2
Document the Brunch entry in the website's Plugins settings
lunelson Oct 13, 2026
9a1925d
Pin the draft card's revision check to the full document
lunelson Oct 13, 2026
8cd385b
Derive Brunch's harness and card types from the code that owns them
lunelson Oct 13, 2026
8c55850
Stop returning the admission probe's Voice evidence twice
lunelson Oct 14, 2026
c123465
Give the draft card the document handle so it alone chooses the revis…
lunelson Oct 14, 2026
ccfb84d
Declare the website modules the Brunch integration tests import
lunelson Oct 14, 2026
a80722b
Drop the website's exports map for the Brunch integration tests
lunelson Oct 14, 2026
48f0a6a
Drop the RDS IAM probe so Brunch's image ships only its build
lunelson Oct 14, 2026
93aaa82
Show a notice when Brunch's editor plugin is missing
lunelson Oct 14, 2026
9ee3d35
Forbid relative imports into another package's test folder or tests/
lunelson Oct 14, 2026
d685698
Give useBrunchAssistant the open document as one input
lunelson Oct 14, 2026
8e9c1f4
Say what keeps Brunch's draft key stable
lunelson Oct 14, 2026
6fd2700
Keep the in-editor approval test's turn open so the chat doesn't resend
lunelson Oct 15, 2026
d5bbe60
Seed the in-editor approval test with a dynamic tool call, as Brunch …
lunelson Oct 15, 2026
fdc60e9
Fix persona admission aliases and approval ordering
lunelson Oct 15, 2026
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: 4 additions & 0 deletions apps/brunch-agent/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
}
],
"patterns": [
{
"regex": "^(\\.\\./)+((apps|libs|tests|@[a-z0-9-]+)/|[^./][^/]*/(src|dist|test)/)",
"message": "Import another workspace package by its package name, not a relative path into it."
},
{
"group": [
"@hashintel/petrinaut/*",
Expand Down
11 changes: 11 additions & 0 deletions apps/brunch-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,19 @@ To try a candidate, copy `src/agents/petrinaut/` to `src/agents/petrinaut-alt-<i

Neither `test:unit` nor `test:integration` needs anything running. `test:integration` checks the built app, starting it as a server and loading it in process with scripted OpenAI responses on the default model.

The app exports its harness for the built server under `@apps/brunch-agent/testing/*` (`test/load-built-application.ts` and `test/integration/run-admission-controls.ts`). Its one consumer is `tests/petrinaut-brunch-integration`, which runs the Petrinaut website's Brunch code against the built app.

The `test:manual:*` scripts are not part of either suite and do not run in CI. `test:manual:browser` builds the website and drives it in the local macOS Chrome against the built app.

`test:manual:browser:persona` exercises the persona helper through that same browser boundary, using only synthetic model responses. It checks opening and subsequent admissions, current-turn approvals, and an unanswered approval left visible by a transport failure. Run it once for each supported endpoint:

```sh
yarn workspace @apps/brunch-agent test:manual:browser:persona
VITE_BRUNCH_CHAT_ENDPOINT=/agents/chat yarn workspace @apps/brunch-agent test:manual:browser:persona
```

The transport-failure case starts with an unanswered ordinary composer turn. It reproduces the stale-panel failure at the persona helper's entrypoint; it does not claim that a sequence made exclusively of persona turns reaches that starting state.

## Local Postgres

Set `BRUNCH_DB_KIND=postgres` explicitly to use the existing Postgres adapter and migrations locally. An unset selector defaults to SQLite outside production; `BRUNCH_DB_KIND=sqlite` also selects that lightweight path. Production always requires Postgres (selector unset or `postgres`), and rejects `sqlite`. Selector values are exact and case-sensitive; blank or unknown values fail.
Expand Down
2 changes: 1 addition & 1 deletion apps/brunch-agent/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,target=/repo/target,sharing=locked \
--mount=type=cache,target=/repo/.turbo/cache,sharing=locked \
turbo run build --filter '@apps/brunch-agent' --env-mode=loose && \
rm -rf apps/brunch-agent/test
rm -rf apps/brunch-agent/src apps/brunch-agent/test


FROM mise AS runner
Expand Down
3 changes: 2 additions & 1 deletion apps/brunch-agent/docs/task-dependencies.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"@hashintel/brunch-agent",
"@hashintel/petrinaut-core",
"@local/flue-aisdk-transport",
"@local/hash-backend-utils"
"@local/hash-backend-utils",
"@local/pi-faux-providers"
],
"tasks": {
"build": {
Expand Down
27 changes: 14 additions & 13 deletions apps/brunch-agent/evals/petrinaut/persona/browser-turn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,9 @@ export const submitPersonaBrowserTurn = async (
"",
"Refusing to overwrite an existing browser draft",
);
// Match the mounted route, including aliases and evaluation candidates.
// Only the conversation POST admits work; /abort is a control request.
const admissionPath = new RegExp(
`^${brunchRoutes.agents}/${brunchRoutes.petrinautAgent}/[^/]+$`,
"u",
);
const admissionPath = new RegExp(`^${brunchRoutes.agents}/[^/]+/[^/]+$`, "u");
const isAdmission = (response: BrowserResponse) =>
response.request().method() === "POST" &&
admissionPath.test(new URL(response.url()).pathname);
Expand All @@ -136,13 +134,7 @@ export const submitPersonaBrowserTurn = async (
};
signal?.addEventListener("abort", cancel, { once: true });
let turnSettled = false;
const allowing = allowDestructiveEdits(
page,
() => turnSettled,
options.onAllowedEdit,
);
// Raced while the turn runs, awaited once it ends.
void allowing.catch(() => {});
let allowing: Promise<void> | undefined;
try {
await composer.fill(message);
signal?.throwIfAborted();
Expand Down Expand Up @@ -197,7 +189,16 @@ export const submitPersonaBrowserTurn = async (
headers: agentOwnershipHeaders(session),
});
await options.onAdmission?.(session, admission);
if (signal?.aborted) cancel();
signal?.throwIfAborted();
// Submission revokes earlier approvals before its POST. Wait for the
// receipt and utterance log before watching for this turn's approvals.
allowing = allowDestructiveEdits(
page,
() => turnSettled,
options.onAllowedEdit,
);
// Raced while the turn runs, awaited once it ends.
void allowing.catch(() => {});
await Promise.race([
stop.waitFor({ state: "hidden", timeout: 0 }),
aborted.promise,
Expand Down Expand Up @@ -263,6 +264,6 @@ export const submitPersonaBrowserTurn = async (
} finally {
signal?.removeEventListener("abort", cancel);
turnSettled = true;
await allowing.catch(() => {});
await allowing?.catch(() => {});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import { join } from "node:path";

import { afterEach, expect, test } from "vitest";

import {
deriveProofTrace,
writeProofArtifacts,
} from "../evals/petrinaut/persona/proof-artifacts";
import { deriveProofTrace, writeProofArtifacts } from "./proof-artifacts";

import type { FlueConversationSnapshot } from "@flue/sdk";

Expand Down
6 changes: 6 additions & 0 deletions apps/brunch-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
"description": "Remote Brunch server and local Petrinaut development loop.",
"license": "AGPL-3.0",
"type": "module",
"exports": {
"./testing/admission-controls": "./test/integration/run-admission-controls.ts",
"./testing/load-built-application": "./test/load-built-application.ts"
},
"scripts": {
"build": "vite build && vite build --config vite.client.config.ts",
"build:docker": "docker buildx build --tag brunch-agent --file docker/Dockerfile ../../ --load",
Expand All @@ -23,6 +27,7 @@
"test:integration": "vitest run --config vitest.integration.config.ts",
"test:manual:browser": "VITE_BRUNCH_CHAT_ENDPOINT=/agents/petrinaut VITE_PETRINAUT_DEFAULT_ASSISTANT=brunch turbo run build --filter '@apps/brunch-agent...' --filter '@apps/petrinaut-website...' --env-mode=loose && node --experimental-strip-types test/browser-witness/petrinaut-panel.ts",
"test:manual:browser:ledger-tab": "VITE_BRUNCH_CHAT_ENDPOINT=/agents/petrinaut VITE_PETRINAUT_DEFAULT_ASSISTANT=brunch turbo run build --filter '@apps/brunch-agent...' --filter '@apps/petrinaut-website...' --env-mode=loose && node --experimental-strip-types test/browser-witness/ledger-tab.ts",
"test:manual:browser:persona": "VITE_BRUNCH_CHAT_ENDPOINT=${VITE_BRUNCH_CHAT_ENDPOINT:-/agents/petrinaut} VITE_PETRINAUT_DEFAULT_ASSISTANT=brunch turbo run build --filter '@apps/brunch-agent...' --filter '@apps/petrinaut-website...' --env-mode=loose && node --experimental-strip-types test/browser-witness/persona-turn.ts",
"test:unit": "vitest run --config vitest.config.ts",
"transcript": "node --experimental-strip-types src/server/transcript-cli.ts"
},
Expand Down Expand Up @@ -50,6 +55,7 @@
"devDependencies": {
"@earendil-works/pi-tui": "0.84.3",
"@flue/vite": "2.0.3",
"@local/pi-faux-providers": "workspace:*",
"@playwright/test": "1.58.2",
"@types/node": "22.18.13",
"@types/pg": "8.23.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import {
type PetrinautExperimentRequest,
} from "@hashintel/petrinaut-core";

import { openaiProviderWithAddedModels } from "../src/server/openai-provider.ts";
import { openrouterProviderWithAddedModels } from "../src/server/openrouter-provider.ts";
import { openaiProviderWithAddedModels } from "../../server/openai-provider.ts";
import { openrouterProviderWithAddedModels } from "../../server/openrouter-provider.ts";
import {
selectChatModelSpecifier,
selectChatThinking,
} from "../src/shared/chat-model.ts";
} from "../../shared/chat-model.ts";

const reference = readFileSync(
new URL(
"../src/agents/petrinaut/skills/modelling/references/experiment-configuration.md",
"./skills/modelling/references/experiment-configuration.md",
import.meta.url,
),
"utf8",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { brunchTools, sdcpnInitialDataSchema } from "@hashintel/brunch-agent";
import { petrinautAiTools } from "@hashintel/petrinaut-core/ai";

import {
commitToolDescription,
compileToolDescription,
} from "../src/agents/petrinaut/tools/ledger.ts";
import { queryBasisToolDescription } from "../src/agents/petrinaut/tools/query-basis.ts";
import {
assertPetrinautToolCatalogueConformance,
canonicalPetrinautToolCatalogue,
brunchToolCatalogue,
} from "../src/shared/tool-catalogue.ts";
} from "../../shared/tool-catalogue.ts";
import {
commitToolDescription,
compileToolDescription,
} from "./tools/ledger.ts";
import { queryBasisToolDescription } from "./tools/query-basis.ts";

const mounted = vi.hoisted(() => ({
initialData: undefined as unknown,
Expand Down Expand Up @@ -46,13 +46,13 @@ vi.mock("@flue/runtime", async (importOriginal) => ({
},
}));
// Only the Flue build packages skills; these tests exercise the composition around them.
vi.mock("../src/agents/petrinaut/skills/eliciting/SKILL.md", () => ({
vi.mock("./skills/eliciting/SKILL.md", () => ({
default: { name: "eliciting" },
}));
vi.mock("../src/agents/petrinaut/skills/modelling/SKILL.md", () => ({
vi.mock("./skills/modelling/SKILL.md", () => ({
default: { name: "modelling" },
}));
vi.mock("../src/agents/petrinaut/skills/petrinaut/SKILL.md", () => ({
vi.mock("./skills/petrinaut/SKILL.md", () => ({
default: { name: "petrinaut" },
}));

Expand All @@ -64,10 +64,7 @@ const bound = {
};
const systemPrompt = async () =>
(
await readFile(
new URL("../src/agents/petrinaut/prompts/system.md", import.meta.url),
"utf8",
)
await readFile(new URL("./prompts/system.md", import.meta.url), "utf8")
).trim();
beforeEach(() => {
vi.resetModules();
Expand All @@ -86,8 +83,7 @@ afterEach(() => vi.unstubAllEnvs());

test("a document binding mounts the guidance, the complete canonical catalogue, the Ledger and the draft", async () => {
mounted.initialData = bound;
const { PetrinautAgent: renderAgent } =
await import("../src/agents/petrinaut/agent.ts");
const { PetrinautAgent: renderAgent } = await import("./agent.ts");
expect(renderAgent({ id: "bound" })).toBe(await systemPrompt());
expect(mounted.contextProjections).toBe(1);
expect(mounted.skills).toEqual(["eliciting", "modelling", "petrinaut"]);
Expand Down Expand Up @@ -118,8 +114,7 @@ test("a document binding mounts the guidance, the complete canonical catalogue,
});

test("a conversation without initial data mounts the guidance and Ledger without browser tools", async () => {
const { PetrinautAgent: renderAgent } =
await import("../src/agents/petrinaut/agent.ts");
const { PetrinautAgent: renderAgent } = await import("./agent.ts");
renderAgent({ id: "unbound" });
expect(mounted.skills).toEqual(["eliciting", "modelling", "petrinaut"]);
expect(mounted.tools.sort()).toEqual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,16 @@ import {
vStatus,
} from "@hashintel/brunch-agent";

import { openaiProviderWithAddedModels } from "../../../server/openai-provider.ts";
import {
selectChatModelSpecifier,
selectChatThinking,
} from "../../../shared/chat-model.ts";
import {
commitToolDescription,
createLedgerCommitTool,
createLedgerCompileTool,
} from "../src/agents/petrinaut/tools/ledger.ts";
import { openaiProviderWithAddedModels } from "../src/server/openai-provider.ts";
import {
selectChatModelSpecifier,
selectChatThinking,
} from "../src/shared/chat-model.ts";
} from "./ledger.ts";

type Batch = LedgerAppend["entries"];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from "vitest";

import { queryBasis } from "../src/agents/petrinaut/tools/query-basis.ts";
import { queryBasis } from "./query-basis.ts";

import type { FlueConversationSnapshot } from "@flue/sdk";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";

import { afterEach, describe, expect, test } from "vitest";

import { conversationDbPath } from "../src/db-path";
import { conversationDbPath } from "./db-path";

const appDir = fileURLToPath(new URL("..", import.meta.url));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { instrument, setProvider } from "@flue/runtime";
import { Hono } from "hono";
import { beforeAll, expect, test, vi } from "vitest";

vi.mock("../src/server/telemetry-bootstrap.ts", () => ({}));
vi.mock("../src/agents/petrinaut/agent.ts", () => ({
vi.mock("./server/telemetry-bootstrap.ts", () => ({}));
vi.mock("./agents/petrinaut/agent.ts", () => ({
PetrinautAgent: { agentName: "brunch-chat-agent" },
}));
vi.mock("@flue/runtime/routing", () => ({
Expand Down Expand Up @@ -52,7 +52,7 @@ vi.mock(
},
);
beforeAll(async () => {
await import("../src/app");
await import("./app");
});

test("app registration admits the Anthropic, OpenAI and OpenRouter providers", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, test } from "vitest";

import { brunchEnv } from "@hashintel/brunch-agent";

import { loadDatabaseConfig } from "../src/server/database-config.ts";
import { loadDatabaseConfig } from "./database-config.ts";

const productionEnvironment = {
NODE_ENV: "production",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { describe, expect, test, vi } from "vitest";
import {
validatePersistedHistory,
validateUiMessageStream,
} from "../src/server/deployment-smoke-validation.ts";
} from "./deployment-smoke-validation.ts";

const streamOf = (...values: string[]): ReadableStream<Uint8Array> => {
const encoder = new TextEncoder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { join } from "node:path";

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { checkDevConfiguration } from "../src/server/dev-configuration-preflight.ts";
import {
selectChatModelSpecifier,
selectChatThinking,
} from "../src/shared/chat-model.ts";
} from "../shared/chat-model.ts";
import { checkDevConfiguration } from "./dev-configuration-preflight.ts";

const syntheticKey = "synthetic-config-fixture-not-a-real-credential";
let root: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Hono } from "hono";
import { expect, test, vi } from "vitest";

import { healthHandler } from "../src/server/health.ts";
import { healthHandler } from "./health.ts";

test("health reports liveness without consulting dependencies", async () => {
const dependency = vi.fn<() => void>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { expect, test } from "vitest";

import { brunchRoutes } from "@hashintel/brunch-agent";

import { agentOwnershipGuard } from "../src/server/http/ownership.ts";
import {
agentOwnershipHeaders,
flueConversationIdFrom,
} from "../src/shared/conversation/identity.ts";
} from "../../shared/conversation/identity.ts";
import { agentOwnershipGuard } from "./ownership.ts";

const mount = `${brunchRoutes.agents}/${brunchRoutes.petrinautAgent}`;
const app = new Hono();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { pathToFileURL } from "node:url";
import { Hono } from "hono";
import { afterAll, describe, expect, test } from "vitest";

import { assetHandler } from "../src/server/http/assets";
import { assetHandler } from "./assets";

const uiRoot = mkdtempSync(join(tmpdir(), "brunch-assets-"));
const BINARY_BYTES = Uint8Array.from({ length: 256 }, (_, i) => i);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,12 @@ import {
brunchRoutes,
} from "@hashintel/brunch-agent";

import {
createAgentCors,
parseCorsAllowedOrigins,
} from "../src/server/http/cors.ts";
import { agentOwnershipGuard } from "../src/server/http/ownership.ts";
import {
agentOwnershipHeaders,
flueConversationIdFrom,
} from "../src/shared/conversation/identity.ts";
} from "../../shared/conversation/identity.ts";
import { createAgentCors, parseCorsAllowedOrigins } from "./cors.ts";
import { agentOwnershipGuard } from "./ownership.ts";

describe("parseCorsAllowedOrigins", () => {
test("grants no cross-origin access when configuration is absent or blank", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import {
liveToolRouteSegment,
} from "@local/flue-aisdk-transport/server";

import { agentOwnershipGuard } from "../src/server/http/ownership.ts";
import {
agentOwnershipHeaders,
flueConversationIdFrom,
} from "../src/shared/conversation/identity.ts";
} from "../../shared/conversation/identity.ts";
import { agentOwnershipGuard } from "./ownership.ts";

const mount = "/agents/chat";
const identity = {
Expand Down
Loading
Loading