Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d0f7a7b
Keep upcoming-mission drafts at conversational fidelity.
lunelson Aug 28, 2026
fcb46f4
housekeeping and new inbox sources
lunelson Aug 28, 2026
6497b02
add brunch-rel'd image assets to the brunch-agent app
lunelson Aug 28, 2026
55718ad
Specify structurally typed elicitation runbooks
lunelson Aug 28, 2026
a6ab067
Land Mission 3 pass 1: sdcpn-modelling runbook skill, headless drive,…
lunelson Aug 28, 2026
83749d0
Close validated construction side quest
lunelson Aug 28, 2026
088693a
Fold Mission 3 review findings into upcoming-mission clusters
lunelson Aug 28, 2026
18532de
Specify elicitation-to-IR oracle design
lunelson Aug 28, 2026
f2801ac
Freeze elicitation-to-IR grader ruler v1
lunelson Aug 28, 2026
af91513
Flatten Brunch evaluation topology
lunelson Aug 28, 2026
83015b2
further docs weeding and flattening
lunelson Aug 28, 2026
5237a29
Restore prospective runbook elicitation baseline
lunelson Aug 28, 2026
afcb5c0
detailed research reports from research I
lunelson Aug 28, 2026
f8c950b
Synthesize elicitation research and reframe successor missions
lunelson Aug 31, 2026
b50ea43
Close Mission 3 with prospective baseline evidence
lunelson Aug 31, 2026
8ae05fc
Register Mission 3 substrate test entries
lunelson Aug 31, 2026
de3e566
Express Brunch test fixtures in the Turbo graph
lunelson Aug 31, 2026
450994a
Include Brunch app fixtures in CI prune
lunelson Sep 1, 2026
f4b995d
Specialize runbook IR fence recovery
lunelson Sep 1, 2026
beb21bd
Resolve runbook proof review findings
lunelson Sep 3, 2026
5717c7d
Preserve nested validation issue paths
lunelson Sep 3, 2026
174d74a
Move Brunch architecture checks to app tests
lunelson Sep 3, 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
48 changes: 15 additions & 33 deletions .github/actions/prune-repository/prune.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,11 @@
"@rust/hash-graph-types": ["@rust/hash-graph-test-data"],
}

# Extras that must not fire on a transitive or prefix match. Brunch core's
# architecture and contract tests inspect the app and shipped plugins, but a job
# whose requested scope is only a sibling or a consumer of core must not pull
# those fixtures.
REQUESTED_DEPENDENCIES: dict[str, list[str]] = {
"@hashintel/brunch-agent": [
"@apps/brunch-agent",
"@hashintel/brunch-agent-plugin-gherkin",
"@hashintel/brunch-agent-plugin-sdcpn",
],
}

# Non-workspace paths required by packages in the *requested* scope.
# `turbo prune` copies workspace directories and root manifests only.
REQUESTED_PATHS: dict[str, list[str]] = {
# The Brunch context root is deliberately not a workspace, but the
# architecture tests in packages/core read its docs, scripts, and agent
# contract files
# Core's shipped-definition and baseline tests read the non-workspace
# context root alongside their plugin task dependencies.
"@hashintel/brunch-agent": [
".config/oxlint/brunch",
"libs/@hashintel/brunch-agent/AGENTS.md",
Expand All @@ -66,9 +53,18 @@
"libs/@hashintel/brunch-agent/evaluations",
"libs/@hashintel/brunch-agent/scripts",
],
# The app's condition-5 test executes the evaluation runner as a child
# process; the context root is not a workspace and must be copied explicitly.
"@apps/brunch-agent": ["libs/@hashintel/brunch-agent/evaluations"],
# The app's tests execute evaluation runners and govern the complete Brunch
# composition. Its context root is not a workspace, so copy the docs,
# scripts, and agent contract files explicitly.
"@apps/brunch-agent": [
".config/oxlint/brunch",
"libs/@hashintel/brunch-agent/AGENTS.md",
"libs/@hashintel/brunch-agent/CONTEXT.md",
"libs/@hashintel/brunch-agent/docs",
"libs/@hashintel/brunch-agent/evaluations",
"libs/@hashintel/brunch-agent/scripts",
"libs/@hashintel/petrinaut/docs",
],
Comment thread
lunelson marked this conversation as resolved.
}

TURBO_QUERY = """
Expand Down Expand Up @@ -129,20 +125,6 @@ def turbo_dependency_map() -> dict[str, frozenset[str]]:
return dep_map


def extras_for_requested(requested: Iterable[str]) -> frozenset[str]:
"""Return extras implied by the job's requested scopes only.

Exact identity: a name that is a prefix of its siblings must not match them.
"""

names = set(requested)
extras: set[str] = set()
for trigger, additions in REQUESTED_DEPENDENCIES.items():
if trigger in names:
extras.update(additions)
return frozenset(extras)


def extra_paths_for_requested(requested: Iterable[str]) -> list[str]:
"""Return non-workspace paths implied by the job's requested scopes only."""

Expand Down Expand Up @@ -297,7 +279,7 @@ def main(argv: list[str] | None = None) -> None:
}

dependencies = turbo_dependency_map()
scopes = fixpoint_expand(initial | extras_for_requested(initial), dependencies)
scopes = fixpoint_expand(initial, dependencies)
turbo_prune(scopes, dry_run=args.dry_run)
copy_extra_paths(initial, dry_run=args.dry_run)
stub_missing_members(dry_run=args.dry_run)
Expand Down
42 changes: 17 additions & 25 deletions .github/actions/prune-repository/prune_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from prune import (
expand_scopes,
extra_paths_for_requested,
extras_for_requested,
fixpoint_expand,
)

Expand All @@ -21,54 +20,47 @@


class BrunchRequestedExtras(unittest.TestCase):
def test_core_job_adds_the_app_plugins_and_context_paths(self) -> None:
expected_workspaces = frozenset({APP, PLUGIN_GHERKIN, PLUGIN_SDCPN})
self.assertEqual(extras_for_requested({CORE}), expected_workspaces)
def test_app_task_adds_the_core_plugins_and_context_paths(self) -> None:
expected_workspaces = frozenset({CORE, PLUGIN_GHERKIN, PLUGIN_SDCPN})
expanded = fixpoint_expand(
{CORE} | extras_for_requested({CORE}),
{APP},
{
CORE: frozenset(),
APP: frozenset({CORE}),
APP: expected_workspaces,
PLUGIN_GHERKIN: frozenset({CORE}),
PLUGIN_SDCPN: frozenset({CORE}),
},
)
self.assertTrue(expected_workspaces.issubset(expanded))
self.assertEqual(
extra_paths_for_requested({CORE}),
extra_paths_for_requested({APP}),
[
".config/oxlint/brunch",
"libs/@hashintel/brunch-agent/AGENTS.md",
"libs/@hashintel/brunch-agent/CONTEXT.md",
"libs/@hashintel/brunch-agent/docs",
"libs/@hashintel/brunch-agent/evaluations",
"libs/@hashintel/brunch-agent/scripts",
"libs/@hashintel/petrinaut/docs",
],
)

def test_app_job_adds_the_baseline_evaluation_paths(self) -> None:
def test_core_adds_its_non_workspace_test_fixtures(self) -> None:
self.assertEqual(
extra_paths_for_requested({APP}),
["libs/@hashintel/brunch-agent/evaluations"],
extra_paths_for_requested({CORE}),
[
".config/oxlint/brunch",
"libs/@hashintel/brunch-agent/AGENTS.md",
"libs/@hashintel/brunch-agent/CONTEXT.md",
"libs/@hashintel/brunch-agent/docs",
"libs/@hashintel/brunch-agent/evaluations",
"libs/@hashintel/brunch-agent/scripts",
],
)

def test_sibling_or_website_job_does_not_add_brunch_extras(self) -> None:
self.assertEqual(extras_for_requested({TRANSPORT}), frozenset())
self.assertEqual(extras_for_requested({WEBSITE}), frozenset())
def test_sibling_or_website_job_does_not_add_context_paths(self) -> None:
self.assertEqual(extra_paths_for_requested({TRANSPORT}), [])
self.assertEqual(extra_paths_for_requested({WEBSITE}), [])

def test_core_in_the_dependency_closure_does_not_add_the_app(self) -> None:
dependencies = {
WEBSITE: frozenset({CORE, TRANSPORT}),
TRANSPORT: frozenset(),
CORE: frozenset(),
}
expanded = fixpoint_expand({WEBSITE}, dependencies)
self.assertNotIn(APP, expanded)
self.assertEqual(extras_for_requested({WEBSITE}), frozenset())


class DarwinPrefix(unittest.TestCase):
def test_child_crate_still_triggers_the_prefix_family(self) -> None:
extras = expand_scopes({"@rust/darwin-kperf-sys"})
Expand Down
26 changes: 19 additions & 7 deletions apps/brunch-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,25 @@ yarn dev:brunch

The first step builds the Petrinaut libraries the panel imports (`dist/` and design-system
codegen). Then it starts the Brunch server at `http://127.0.0.1:4321` and the real Petrinaut
website at `http://127.0.0.1:4915`. The website proxies `/api/chat` to Brunch. The panel talks to one plain
Flue chat agent: streamed text and reasoning, one server `ping` tool, one stub
skill (`confirm-path`, activated via `activate_skill`), and the existing Petrinaut
`readPetrinautDoc` client tool. There is no elicitation loop, sweep tool, or
`brunch_ask` on this path. Capture is a harness-side pipe: an explicit settled
range of Flue history is applied into a JSON store beside the conversation
database, not by the interviewer.
website at `http://127.0.0.1:4915`. The website proxies `/api/chat` to Brunch. The panel talks to one Flue
chat agent: streamed text and reasoning, one server `ping` tool, one
modelling runbook skill (`sdcpn-modelling`, activated via `activate_skill`,
with supporting resources via `read_skill_resource`), and the existing
Petrinaut `readPetrinautDoc` client tool. There is no elicitation loop,
sweep tool, or `brunch_ask` on this path. Capture is a harness-side pipe:
an explicit settled range of Flue history is applied into a JSON store
beside the conversation database, not by the interviewer.

A headless Mission 3 drive (simulated expert, same `ChatAgent` door):

```sh
yarn workspace @apps/brunch-agent runbook:headless
```

`ANTHROPIC_API_KEY` is required. `BRUNCH_CHAT_MODEL` selects the interviewer
(default `claude-sonnet-4-5` for this script only). Artifacts write under
`libs/@hashintel/brunch-agent/docs/evidence/evaluations/vestera-runbook-headless/`
unless `BRUNCH_RUNBOOK_OUTPUT_DIR` is set.

Conversations persist in `apps/brunch-agent/.data-wipe-me/conversations.db`. `BRUNCH_DEV_DB_PATH`
overrides that local path. Capture envelopes for one Flue conversation sit beside that sqlite
Expand Down
3 changes: 3 additions & 0 deletions apps/brunch-agent/docs/task-dependencies.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
],
"test:unit": [
"@apps/brunch-agent#build",
"@hashintel/brunch-agent#build",
"@hashintel/brunch-agent-binding-flue#build",
"@hashintel/brunch-agent-plugin-gherkin#build",
"@hashintel/brunch-agent-plugin-sdcpn#build",
"@hashintel/brunch-agent-transport-aisdk#build",
"@hashintel/petrinaut-core#build"
]
Expand Down
Binary file added apps/brunch-agent/files/brunch-clipped.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/brunch-agent/files/favicon.ico
Binary file not shown.
2 changes: 2 additions & 0 deletions apps/brunch-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"lint:eslint": "oxlint --type-aware --type-check --report-unused-disable-directives-severity=error .",
"lint:tsc": "tsgo --noEmit",
"petrinaut:dev": "vite dev --config petrinaut-local.vite.config.ts",
"runbook:elicit": "vite build && node --experimental-strip-types src/runbook-elicitation-run.ts",
"runbook:headless": "vite build && node --experimental-strip-types src/runbook-headless-run.ts",
"test:unit": "vitest run --config vitest.config.ts",
"transcript": "node --experimental-strip-types src/transcript-cli.ts"
},
Expand Down
56 changes: 39 additions & 17 deletions apps/brunch-agent/src/agents/chat-agent.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,68 @@
"use agent";
/**
* One plain Flue chat agent for the Petrinaut panel throughline.
* One Flue chat agent for the Petrinaut panel throughline.
*
* Capture is a harness-side pipe, not an interviewer tool. One stub skill is
* mounted so activation can appear in Flue history.
* Capture is a harness-side pipe, not an interviewer tool. One runbook skill
* carries the modelling lifecycle and its supporting resources.
*/

import { defineSkill, useModel, useSkill, useTool } from "@flue/runtime";
import { useInitialData, useModel, useSkill, useTool } from "@flue/runtime";
import * as v from "valibot";

import sdcpnModellingSkill from "../skills/sdcpn-modelling/SKILL.md";
import {
petrinautConstructionTools,
VALIDATED_CONSTRUCTION_MODE,
} from "../tools/petrinaut-construction.ts";
import { ping } from "../tools/ping.ts";
import { readPetrinautDoc } from "../tools/read-petrinaut-doc.ts";

export const CHAT_MODEL_ID =
process.env["BRUNCH_CHAT_MODEL"] || "claude-haiku-4-5";

export const STUB_SKILL_NAME = "confirm-path";
export const RUNBOOK_SKILL_NAME = sdcpnModellingSkill.name;

export const ACTIVATE_SKILL_TOOL_NAME = "activate_skill";

const confirmPath = defineSkill({
name: STUB_SKILL_NAME,
description:
"Confirm how this assistant is mounted. Use when checking the server path or tool layout.",
instructions:
"Say that ping confirms the server tool path. Then continue helping the user.",
});
export const chatAgentInitialDataSchema = v.optional(
v.object({
mode: v.literal(VALIDATED_CONSTRUCTION_MODE),
}),
);

export type ChatAgentInitialData = v.InferOutput<
typeof chatAgentInitialDataSchema
>;

export function ChatAgent() {
const initialData = useInitialData<ChatAgentInitialData>();
useModel(`anthropic/${CHAT_MODEL_ID}`);
useSkill(confirmPath);
useSkill(sdcpnModellingSkill);
useTool(ping);
useTool(readPetrinautDoc);
return [
"You are a concise assistant inside the Petrinaut editor.",
if (initialData?.mode === VALIDATED_CONSTRUCTION_MODE) {
for (const constructionTool of petrinautConstructionTools) {
useTool(constructionTool);
}
}
const instructions = [
"You are the Brunch modelling assistant inside the Petrinaut editor.",
`Activate the \`${RUNBOOK_SKILL_NAME}\` skill before interviewing or constructing a process model.`,
"The Markdown IR is the shared workpiece of one looping lifecycle.",
"Call ping when you need to confirm the server tool path.",
`Activate the \`${STUB_SKILL_NAME}\` skill before calling ping.`,
"When the user asks how Petrinaut's UI works, call readPetrinautDoc.",
"A client-tool-result signal is JSON [{ toolCallId, toolName, output }]. Treat output as the browser's result for that call and continue helping the user.",
].join("\n");
];
if (initialData?.mode === VALIDATED_CONSTRUCTION_MODE) {
instructions.push(
"This is a construct-only headless conversation. Use only the supplied runbook IR as modelling input, do not interview, and build the net through the mounted Petrinaut tools instead of emitting net JSON.",
);
}
return instructions.join("\n");
}

/**
* Pinned, and never to be edited: conversation storage keys on this literal.
*/
ChatAgent.agentName = "brunch-chat-agent";
ChatAgent.initialData = chatAgentInitialDataSchema;
16 changes: 12 additions & 4 deletions apps/brunch-agent/src/capture-sweep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,22 @@ export interface CaptureSweepResult {
const conversationUrl = (instanceId: string): string =>
`http://brunch.local/agents/${CHAT_AGENT_ROUTE}/${instanceId}`;

const ownedTransport = (identity: ConversationIdentity): typeof fetch => {
const sourceAppTransport: typeof fetch = async (input, init) => {
const { default: app } = await import("./app.ts");
return app.fetch(input instanceof Request ? input : new Request(input, init));
};

const ownedTransport = (
identity: ConversationIdentity,
transport: typeof fetch,
): typeof fetch => {
const ownership = agentOwnershipHeaders(identity);
return async (input, init) => {
const { default: app } = await import("./app.ts");
const headers = new Headers(init?.headers);
for (const [key, value] of Object.entries(ownership)) {
headers.set(key, value);
}
return app.fetch(
return transport(
input instanceof Request
? new Request(input, { headers })
: new Request(input, { ...init, headers }),
Expand All @@ -53,14 +60,15 @@ const ownedTransport = (identity: ConversationIdentity): typeof fetch => {
export const applyCaptureSweep = async (
identity: ConversationIdentity,
userEntryIds: readonly string[],
transport: typeof fetch = sourceAppTransport,
): Promise<CaptureSweepResult> => {
const instanceId = flueConversationIdFrom(identity);
const store = createLocalCaptureStore(captureStorePath(instanceId), {
ownerKey: identity.principalKey,
});
const historyReader = createFlueHistoryReader({
resolveConversationUrl: conversationUrl,
transport: ownedTransport(identity),
transport: ownedTransport(identity, transport),
archive: store,
});
const snapshot = await historyReader.read(instanceId);
Expand Down
Loading
Loading