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
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
This codebase will outlive you. Every shortcut you take becomes
someone else's burden. Every hack compounds into technical debt
that slows the whole team down.

You are not just writing code. You are shaping the future of this
project. The patterns you establish will be copied. The corners
you cut will be cut again.

Fight entropy. Leave the codebase better than you found it.
47 changes: 47 additions & 0 deletions src/artifact-store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { mkdtemp } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { FsArtifactStore } from './fs-artifact-store.js';
import type { RunArtifact } from './run-artifact.js';

describe('ArtifactStore port contract (fs adapter)', () => {
it('round-trips a Run Artifact through save then load', async () => {
const baseDir = await mkdtemp(join(tmpdir(), 'testbuds-'));
const store = new FsArtifactStore(baseDir);
const artifact: RunArtifact = {
runId: 'run-001',
result: { outcome: 'achieved' },
findings: [
{
type: 'dead-end',
severity: 'high',
confidence: 'high',
stepRef: 3,
evidence: {
type: 'mechanical',
stepRef: 3,
kind: 'error_state',
confidence: 'high',
},
},
],
trace: {
mechanical: [
{
type: 'mechanical',
stepRef: 3,
kind: 'error_state',
confidence: 'high',
},
],
},
metadata: { budConfig: { perceptionMode: 'hybrid' }, mode: 'persona' },
};

const ref = await store.save(artifact);
const loaded = await store.load(ref);

expect(loaded).toEqual(artifact);
});
});
11 changes: 11 additions & 0 deletions src/artifact-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { RunArtifact } from './run-artifact.js';

/**
* Port for persisting Run Artifacts. v1 adapter is the local
* filesystem; a cloud adapter is the future hosted-tier seam.
*/
export interface ArtifactStore {
/** Persists the artifact and returns a ref it can be loaded by. */
save(artifact: RunArtifact): Promise<string>;
load(ref: string): Promise<RunArtifact>;
}
22 changes: 22 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { mkdtemp } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { main } from './cli.js';
import { FsArtifactStore } from './fs-artifact-store.js';

describe('CLI', () => {
it('runs the scripted demo Run end-to-end and prints the saved artifact path', async () => {
const baseDir = await mkdtemp(join(tmpdir(), 'testbuds-'));
const lines: string[] = [];

await main({ baseDir, out: (line) => lines.push(line) });

expect(lines).toHaveLength(1);
const ref = lines[0];
expect(ref.startsWith(baseDir)).toBe(true);
const artifact = await new FsArtifactStore(baseDir).load(ref);
expect(artifact.result.outcome).toBe('achieved');
expect(artifact.findings.length).toBeGreaterThan(0);
});
});
31 changes: 31 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { pathToFileURL } from 'node:url';
import { demoInvocation, demoScript } from './demo-run.js';
import { FakeInteractionAdapter } from './fake-interaction-adapter.js';
import { FsArtifactStore } from './fs-artifact-store.js';
import { run } from './run-orchestrator.js';

export interface CliOptions {
baseDir?: string;
out?: (line: string) => void;
}

/** Runs the scripted demo Run end-to-end and prints the artifact path. */
export async function main({
baseDir = '.testbuds/runs',
out = console.log,
}: CliOptions = {}): Promise<void> {
const adapter = new FakeInteractionAdapter(demoScript);
const store = new FsArtifactStore(baseDir);
const { ref } = await run(demoInvocation, { adapter, store });
out(ref);
}

if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}
98 changes: 98 additions & 0 deletions src/demo-run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { ScriptedRun } from './fake-interaction-adapter.js';
import type { RunInvocation } from './interaction-contract.js';

/**
* The canonical scripted demo Run: a Bud signs up, hits an unexplained
* validation error, recovers, and reaches the goal. Drives the CLI
* walking skeleton and is reusable as a test fixture.
*/
export const demoScript: ScriptedRun = {
events: [
{
type: 'perceived',
stepRef: 1,
elements: [
{ id: 'el-1', role: 'textfield', name: 'Email' },
{ id: 'el-2', role: 'button', name: 'Sign up' },
],
},
{
type: 'intent',
stepRef: 1,
rationale: 'The email field is the obvious starting point',
action: {
type: 'input',
targetId: 'el-1',
value: 'bud@example.com',
reason: 'Fill in email',
},
},
{
type: 'action',
stepRef: 1,
action: {
type: 'input',
targetId: 'el-1',
value: 'bud@example.com',
reason: 'Fill in email',
},
},
{ type: 'outcome', stepRef: 1, success: true },
{
type: 'action',
stepRef: 2,
action: {
type: 'point',
targetId: 'el-2',
reason: 'Submit the signup form',
},
},
{ type: 'outcome', stepRef: 2, success: false },
{
type: 'mechanical',
stepRef: 2,
kind: 'error_state',
confidence: 'high',
detail: 'Validation error shown with no explanation of what to fix',
},
{
type: 'reaction',
stepRef: 2,
utterance: 'Hmm, it says something went wrong but not what…',
expression: 'confused',
cause: 'error_state',
},
{
type: 'action',
stepRef: 3,
action: {
type: 'point',
targetId: 'el-2',
reason: 'Retry submitting the form',
},
},
{ type: 'outcome', stepRef: 3, success: true },
{
type: 'mechanical',
stepRef: 3,
kind: 'goal_achieved',
confidence: 'high',
},
{
type: 'reaction',
stepRef: 3,
utterance: 'Got there in the end!',
expression: 'happy',
cause: 'goal_achieved',
},
{ type: 'milestone', stepRef: 3, kind: 'goal_achieved' },
],
result: { outcome: 'achieved' },
};

export const demoInvocation: RunInvocation = {
budConfig: { perceptionMode: 'hybrid', stepBudget: 10 },
flowSpec: { goal: 'Sign up for an account' },
mode: 'persona',
target: { surface: 'web', entry: 'fixture://signup' },
};
85 changes: 85 additions & 0 deletions src/fake-interaction-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it } from 'vitest';
import { FakeInteractionAdapter } from './fake-interaction-adapter.js';
import type { RunEvent, RunInvocation } from './interaction-contract.js';

const invocation: RunInvocation = {
budConfig: { perceptionMode: 'hybrid' },
flowSpec: { goal: 'Sign up for an account' },
mode: 'persona',
target: { surface: 'web', entry: 'fixture://signup' },
};

describe('Fake Interaction adapter', () => {
it('advertises a Capabilities descriptor', () => {
const adapter = new FakeInteractionAdapter({
events: [],
result: { outcome: 'achieved' },
});

expect(adapter.capabilities).toEqual({
surface: 'web',
perceptionModes: ['a11y', 'vision', 'hybrid'],
inputs: ['point', 'input', 'scroll', 'key'],
canScreenshot: false,
liveView: false,
});
});

it('emits the scripted event stream and then the final run result', async () => {
const events: RunEvent[] = [
{
type: 'perceived',
stepRef: 1,
elements: [{ id: 'el-1', role: 'textfield', name: 'Email' }],
},
{
type: 'intent',
stepRef: 1,
rationale: 'The email field is the obvious starting point',
action: {
type: 'input',
targetId: 'el-1',
value: 'bud@example.com',
reason: 'Fill in email',
},
},
{
type: 'action',
stepRef: 1,
action: {
type: 'input',
targetId: 'el-1',
value: 'bud@example.com',
reason: 'Fill in email',
},
},
{ type: 'outcome', stepRef: 1, success: true },
{
type: 'mechanical',
stepRef: 1,
kind: 'goal_achieved',
confidence: 'high',
},
{
type: 'reaction',
stepRef: 1,
utterance: 'That was easy!',
expression: 'happy',
},
{ type: 'milestone', stepRef: 1, kind: 'goal_achieved' },
];
const adapter = new FakeInteractionAdapter({
events,
result: { outcome: 'achieved' },
});

const run = adapter.interact(invocation);
const received: RunEvent[] = [];
for await (const event of run.events) {
received.push(event);
}

expect(received).toEqual(events);
await expect(run.result).resolves.toEqual({ outcome: 'achieved' });
});
});
38 changes: 38 additions & 0 deletions src/fake-interaction-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {
Capabilities,
InteractionAdapter,
InteractionRun,
RunEvent,
RunInvocation,
RunResult,
} from './interaction-contract.js';
import { streamOf } from './scripted-stream.js';

/** A scripted Run: the events to emit and the result to report. */
export interface ScriptedRun {
events: RunEvent[];
result: RunResult;
}

/**
* Interaction adapter that replays a scripted Run — no browser, no LLM.
* Exercises the whole pipe downstream of the Interaction boundary.
*/
export class FakeInteractionAdapter implements InteractionAdapter {
readonly capabilities: Capabilities = {
surface: 'web',
perceptionModes: ['a11y', 'vision', 'hybrid'],
inputs: ['point', 'input', 'scroll', 'key'],
canScreenshot: false,
liveView: false,
};

constructor(private readonly script: ScriptedRun) {}

interact(_invocation: RunInvocation): InteractionRun {
return {
events: streamOf(this.script.events),
result: Promise.resolve(this.script.result),
};
}
}
Loading