debutly 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +662 -0
  2. package/README.md +192 -2
  3. package/bin/debutly.mjs +8 -0
  4. package/drizzle/0000_init.sql +107 -0
  5. package/drizzle/0001_predictions.sql +14 -0
  6. package/drizzle/0002_dashboard.sql +15 -0
  7. package/drizzle/meta/0000_snapshot.json +722 -0
  8. package/drizzle/meta/0001_snapshot.json +821 -0
  9. package/drizzle/meta/0002_snapshot.json +942 -0
  10. package/drizzle/meta/_journal.json +27 -0
  11. package/package.json +83 -4
  12. package/src/agent/describe.ts +29 -0
  13. package/src/agent/effect.ts +94 -0
  14. package/src/agent/give-up.ts +42 -0
  15. package/src/agent/label.ts +19 -0
  16. package/src/agent/loop.ts +302 -0
  17. package/src/agent/parse-reply.ts +127 -0
  18. package/src/agent/prompt.ts +85 -0
  19. package/src/agent/run-agent.ts +68 -0
  20. package/src/artifacts/db.ts +55 -0
  21. package/src/artifacts/s3.ts +36 -0
  22. package/src/artifacts/store.ts +26 -0
  23. package/src/bin.ts +140 -0
  24. package/src/browser/ax.ts +77 -0
  25. package/src/browser/execute.ts +401 -0
  26. package/src/browser/observe.ts +62 -0
  27. package/src/browser/reflow.ts +73 -0
  28. package/src/browser/samples.ts +119 -0
  29. package/src/browser/scan-script.ts +108 -0
  30. package/src/browser/settle.ts +32 -0
  31. package/src/browser/signals.ts +73 -0
  32. package/src/cancel.ts +20 -0
  33. package/src/cli.ts +281 -0
  34. package/src/config.ts +62 -0
  35. package/src/cost/estimate.ts +32 -0
  36. package/src/cost/prices.ts +32 -0
  37. package/src/cost/tiers.ts +27 -0
  38. package/src/costs.ts +96 -0
  39. package/src/crew/run-unit.ts +62 -0
  40. package/src/detect/findings.ts +649 -0
  41. package/src/detect/wcag.ts +38 -0
  42. package/src/doctor.ts +145 -0
  43. package/src/hear-cli.ts +137 -0
  44. package/src/hear.ts +309 -0
  45. package/src/import.ts +108 -0
  46. package/src/inbox/mailpit.ts +46 -0
  47. package/src/inbox/types.ts +10 -0
  48. package/src/init.ts +184 -0
  49. package/src/llm/anthropic.ts +41 -0
  50. package/src/llm/credits.ts +35 -0
  51. package/src/llm/factory.ts +65 -0
  52. package/src/llm/meter.ts +39 -0
  53. package/src/llm/openai-compatible.ts +90 -0
  54. package/src/llm/openrouter.ts +9 -0
  55. package/src/llm/scripted.ts +18 -0
  56. package/src/llm/types.ts +36 -0
  57. package/src/perceive.ts +150 -0
  58. package/src/perception/aggregate.ts +111 -0
  59. package/src/perception/compare.ts +77 -0
  60. package/src/perception/evaluate.ts +83 -0
  61. package/src/perception/exhibits.ts +146 -0
  62. package/src/perception/market-context.ts +50 -0
  63. package/src/perception/run-perception.ts +178 -0
  64. package/src/persona/generate.ts +92 -0
  65. package/src/persona/inclusion.ts +53 -0
  66. package/src/persona/language.ts +59 -0
  67. package/src/persona/people.ts +32 -0
  68. package/src/predictions.ts +33 -0
  69. package/src/queue/dispatch.ts +18 -0
  70. package/src/queue/jobs.ts +28 -0
  71. package/src/queue/processor.ts +42 -0
  72. package/src/report/fix-first.ts +121 -0
  73. package/src/report/launch-readiness.ts +106 -0
  74. package/src/report/markdown.ts +202 -0
  75. package/src/run.ts +141 -0
  76. package/src/sandbox/outbox.ts +65 -0
  77. package/src/store/db.ts +40 -0
  78. package/src/store/import-run.ts +116 -0
  79. package/src/store/pg-run-store.ts +163 -0
  80. package/src/store/predictions.ts +70 -0
  81. package/src/store/run-store.ts +78 -0
  82. package/src/store/schema.ts +193 -0
  83. package/src/store/url.ts +15 -0
  84. package/src/types.ts +201 -0
  85. package/src/util/chromium.ts +20 -0
  86. package/src/util/json.ts +7 -0
  87. package/src/util/key-defaults.ts +34 -0
  88. package/src/util/legacy-env.ts +14 -0
  89. package/src/util/load-env.ts +28 -0
  90. package/src/util/pool.ts +12 -0
  91. package/src/util/run-origin.ts +27 -0
  92. package/src/worker.ts +53 -0
  93. package/tsconfig.json +13 -0
package/src/types.ts ADDED
@@ -0,0 +1,201 @@
1
+ /** Accessibility & inclusion needs every run includes by default. */
2
+ export const INCLUSION_NEEDS = ["screen_reader", "low_vision", "slow_connection", "non_native_english"] as const;
3
+ export type InclusionNeed = (typeof INCLUSION_NEEDS)[number];
4
+
5
+ export interface Persona {
6
+ id: string;
7
+ name: string;
8
+ background: string;
9
+ goal: string;
10
+ patience: "low" | "medium" | "high";
11
+ techSavvy: "low" | "medium" | "high";
12
+ /** Job role inside a business crew, e.g. "Owner" or "Content writer". */
13
+ role?: string;
14
+ inclusion?: InclusionNeed;
15
+ /** First language, as a lang code ("ar"); English when unset. */
16
+ language?: string;
17
+ }
18
+
19
+ export interface Business {
20
+ name: string;
21
+ /** agency, SME, small business, freelancer, startup, enterprise team… */
22
+ archetype: string;
23
+ /** Role-played headcount; the crew itself is capped. */
24
+ size: number;
25
+ /** Monthly budget ceiling for tools like this, in USD. */
26
+ monthlyBudget: number;
27
+ goal: string;
28
+ }
29
+
30
+ /** Who a simulation run models: one person (B2C) or a business crew of owner + employees (B2B). */
31
+ export type CustomerUnit =
32
+ | { kind: "individual"; id: string; person: Persona }
33
+ | { kind: "business"; id: string; business: Business; owner: Persona; employees: Persona[] };
34
+
35
+ /** Keys a person uses to drive popups and forms; nothing that could close tabs or run shortcuts. */
36
+ export const PRESS_KEYS = ["Escape", "Enter", "Space", "Tab", "ArrowDown", "ArrowUp"] as const;
37
+ export type PressKey = (typeof PRESS_KEYS)[number];
38
+
39
+ export type Action =
40
+ | { type: "click"; id: number }
41
+ | { type: "fill"; id: number; text: string }
42
+ // file: which sample to attach (a photo, or a document such as a CV); by default what the field accepts.
43
+ | { type: "upload"; id: number; file?: "photo" | "document" }
44
+ | { type: "goto"; path: string }
45
+ | { type: "check_email" }
46
+ | { type: "press"; key: PressKey }
47
+ | { type: "wait" }
48
+ | { type: "done"; success: boolean; note: string };
49
+
50
+ export type Signal =
51
+ | { kind: "console_error"; message: string; url: string }
52
+ | { kind: "page_error"; message: string; url: string }
53
+ // startedAt/at (epoch ms) bracket the request, to match it against what a sandbox blocked meanwhile.
54
+ // body: the start of a server error's response, which can say why (e.g. a missing API key).
55
+ | { kind: "http_error"; status: number; requestUrl: string; url: string; startedAt?: number; at?: number; body?: string }
56
+ | { kind: "dialog"; dialogType: string; message: string; url: string }
57
+ // A page that took long to arrive. finished: false when the agent's wait ran out first.
58
+ // assets: the scripts and styles received by then, as sent, and how much of that came uncompressed.
59
+ | { kind: "slow_load"; url: string; ms: number; finished: boolean; slowNetwork: boolean; assets?: { files: number; bytes: number; uncompressedBytes: number } }
60
+ // A file the page handed over (a report, an export): the page itself often doesn't change.
61
+ | { kind: "download"; filename: string; url: string };
62
+
63
+ /** What the browser tells assistive technology about an element. hidden: left out of the tree (e.g. aria-hidden). */
64
+ export interface AccessibleName {
65
+ name: string;
66
+ role: string;
67
+ hidden: boolean;
68
+ /** What a screen reader reads after the role (aria-describedby, a tooltip); absent when there's none. */
69
+ description?: string;
70
+ }
71
+
72
+ export interface InteractiveElement {
73
+ id: number;
74
+ role: string;
75
+ name: string;
76
+ tag: string;
77
+ /** What a field holds: a native select's chosen option, or a text field's text (never a password's). */
78
+ value?: string;
79
+ /** A native select's options, by label. */
80
+ options?: string[];
81
+ /** A password field with something typed in it. */
82
+ filled?: true;
83
+ /** Shown but not editable. */
84
+ readOnly?: true;
85
+ /** A checkbox, radio button or switch: on or off. */
86
+ checked?: boolean;
87
+ /** A control that opens something (a menu, a dropdown): open or closed. */
88
+ expanded?: boolean;
89
+ /** The browser's own accessible name and role (Chromium), as a screen reader would announce them. */
90
+ ax?: AccessibleName;
91
+ /** Named only by its placeholder: no label, aria-label or aria-labelledby. */
92
+ nameFrom?: "placeholder";
93
+ /** Disabled, so Tab skips it. */
94
+ disabled?: true;
95
+ /** Out of the tab order (tabindex -1, or inside an inert part of the page). */
96
+ tabbable?: false;
97
+ /** A control with no text: the icon it shows, as its markup names it ("menu", "trash"). What a sighted person sees. */
98
+ icon?: string;
99
+ }
100
+
101
+ export interface Observation {
102
+ url: string;
103
+ title: string;
104
+ elements: InteractiveElement[];
105
+ text: string;
106
+ /** The page's lang attribute ("" when missing), its text direction, and the share of its letters that are Arabic. */
107
+ lang?: string;
108
+ dir?: string;
109
+ arabicShare?: number;
110
+ /** The page's viewport setting when it stops pinch-zoom (user-scalable=no, maximum-scale under 2), else "". */
111
+ zoomBlocked?: string;
112
+ }
113
+
114
+ export interface ActionResult {
115
+ blocked?: string;
116
+ error?: string;
117
+ /** Keyboard only: what entirely hid the control once it had focus. */
118
+ focusObscured?: string;
119
+ /** Keyboard only: Enter did nothing on the button, and Space pressed it. */
120
+ enterIgnored?: true;
121
+ }
122
+
123
+ export interface Step extends ActionResult {
124
+ index: number;
125
+ observation: Observation;
126
+ action: Action;
127
+ reasoning: string;
128
+ signals: Signal[];
129
+ screenshotPath?: string;
130
+ /** A goto to an address the agent typed itself, not a link from an email. */
131
+ guessedUrl?: boolean;
132
+ }
133
+
134
+ /** A page measured at a narrow width after the low-vision agent's run (WCAG 1.4.10 Reflow). */
135
+ export interface ReflowCheck {
136
+ url: string;
137
+ widthPx: number;
138
+ /** How much wider than the window the page is; 0 when it fits. */
139
+ overflowPx: number;
140
+ /** The element reaching furthest to the right, outside any scrolling box. */
141
+ widest: string;
142
+ /** Why the overflow may be allowed (e.g. it is a data table), or "". */
143
+ exception: string;
144
+ }
145
+
146
+ export interface Session {
147
+ agentId: string;
148
+ personaName: string;
149
+ goal: string;
150
+ steps: Step[];
151
+ outcome: "success" | "gave_up" | "budget_exhausted" | "error" | "not_invited" | "cancelled";
152
+ outcomeNote: string;
153
+ unitId?: string;
154
+ role?: string;
155
+ inclusion?: InclusionNeed;
156
+ language?: string;
157
+ /** Tokens this session spent, and their cost when the model price is known. */
158
+ usage?: { inputTokens: number; outputTokens: number; costUsd?: number };
159
+ /** The low-vision agent's pages, measured again at 320 CSS pixels wide. */
160
+ reflow?: ReflowCheck[];
161
+ }
162
+
163
+ export type Severity = "blocker" | "major" | "minor";
164
+ export type Confidence = "high" | "medium" | "low";
165
+ export type FindingKind =
166
+ | "http_error"
167
+ | "page_error"
168
+ | "console_error"
169
+ | "a11y_unlabeled"
170
+ | "stuck"
171
+ | "slow_load"
172
+ | "covered"
173
+ | "keyboard"
174
+ | "focus_obscured"
175
+ | "reflow"
176
+ | "language"
177
+ | "zoom";
178
+
179
+ export interface Finding {
180
+ signature: string;
181
+ kind: FindingKind;
182
+ severity: Severity;
183
+ title: string;
184
+ detail: string;
185
+ pagePath: string;
186
+ hitBy: string[];
187
+ visitors: number;
188
+ confidence: Confidence;
189
+ evidence: { agentId: string; stepIndex: number }[];
190
+ /**
191
+ * Why every occurrence of this failure is the test environment's doing: hosts it blocked at the
192
+ * same moment, `missing key: <what the app said>` when the app reported a key the test
193
+ * environment leaves out on purpose, and/or `made-up site: <host>` when the app couldn't reach
194
+ * the website an agent invented for its company.
195
+ */
196
+ sandboxCause?: string[];
197
+ /** WCAG 2.2 success criteria the finding relates to ("4.1.2"), for a person to check. */
198
+ wcag?: string[];
199
+ /** The control a screen-reader finding is about, so `debutly hear` can find it again. */
200
+ control?: { role: string; name?: string; problem: "unnamed" | "described" | "unreached" | "placeholder" | "keyboard" };
201
+ }
@@ -0,0 +1,20 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, join } from "node:path";
5
+ import { chromium } from "playwright";
6
+
7
+ /** Is the browser the agents use downloaded yet? */
8
+ export function chromiumInstalled(): boolean {
9
+ try {
10
+ return existsSync(chromium.executablePath());
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ /** Downloads it with Playwright's own installer (about 150 MB, once per machine). */
17
+ export function installChromium(): boolean {
18
+ const cli = join(dirname(createRequire(import.meta.url).resolve("playwright/package.json")), "cli.js");
19
+ return spawnSync(process.execPath, [cli, "install", "chromium"], { stdio: "inherit" }).status === 0;
20
+ }
@@ -0,0 +1,7 @@
1
+ /** The JSON object in a model's reply, tolerating prose or code fences around it. */
2
+ export function extractJsonObject(raw: string): unknown {
3
+ const start = raw.indexOf("{");
4
+ const end = raw.lastIndexOf("}");
5
+ if (start === -1 || end <= start) throw new Error("no JSON object in the reply");
6
+ return JSON.parse(raw.slice(start, end + 1));
7
+ }
@@ -0,0 +1,34 @@
1
+ // One key is enough. Whatever is unset is filled in from the key itself: an OpenRouter key
2
+ // (sk-or-…) gets the models that worked best in our comparisons, an Anthropic key (sk-ant-…)
3
+ // the Anthropic provider. OPENROUTER_API_KEY and ANTHROPIC_API_KEY are read when
4
+ // DEBUTLY_API_KEY isn't set, so a key already in the environment for other tools just works.
5
+ // Anything set explicitly wins. Entry points import this right after legacy-env.
6
+ import { isOpenRouter } from "../llm/openrouter";
7
+
8
+ export const OPENROUTER_DEFAULTS = {
9
+ DEBUTLY_PROVIDER: "openai-compatible",
10
+ DEBUTLY_BASE_URL: "https://openrouter.ai/api/v1",
11
+ DEBUTLY_NAV_MODEL: "z-ai/glm-5.3-flash",
12
+ DEBUTLY_JUDGE_MODEL: "deepseek/deepseek-v4-flash",
13
+ DEBUTLY_REASONING_EFFORT: "low",
14
+ } as const;
15
+
16
+ export function applyKeyDefaults(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
17
+ // A borrowed key only goes where it belongs: OpenRouter's to OpenRouter, Anthropic's to
18
+ // Anthropic, never to an address set elsewhere.
19
+ if (!env.DEBUTLY_API_KEY) {
20
+ const toOpenRouter = !env.DEBUTLY_BASE_URL || isOpenRouter(env.DEBUTLY_BASE_URL);
21
+ if (env.OPENROUTER_API_KEY && toOpenRouter && (!env.DEBUTLY_PROVIDER || env.DEBUTLY_PROVIDER === "openai-compatible")) env.DEBUTLY_API_KEY = env.OPENROUTER_API_KEY;
22
+ else if (env.ANTHROPIC_API_KEY && (!env.DEBUTLY_PROVIDER || env.DEBUTLY_PROVIDER === "anthropic")) env.DEBUTLY_API_KEY = env.ANTHROPIC_API_KEY;
23
+ }
24
+ const key = env.DEBUTLY_API_KEY ?? "";
25
+ if (!env.DEBUTLY_PROVIDER && key.startsWith("sk-ant-")) env.DEBUTLY_PROVIDER = "anthropic";
26
+ const openRouter = key.startsWith("sk-or-") && (!env.DEBUTLY_PROVIDER || env.DEBUTLY_PROVIDER === "openai-compatible") && (!env.DEBUTLY_BASE_URL || isOpenRouter(env.DEBUTLY_BASE_URL));
27
+ if (openRouter) {
28
+ // Set to empty is set: DEBUTLY_REASONING_EFFORT= turns reasoning control off.
29
+ for (const [name, value] of Object.entries(OPENROUTER_DEFAULTS)) if (env[name] === undefined) env[name] = value;
30
+ }
31
+ return env;
32
+ }
33
+
34
+ applyKeyDefaults();
@@ -0,0 +1,14 @@
1
+ // Settings are named DEBUTLY_*. Debutly was called Wind Tunnel until 2026-10-03, and the WT_*
2
+ // names from then still work: each fills in its DEBUTLY_* name when that one is unset.
3
+ // Entry points import this module first, so the copy happens before anything reads a setting.
4
+
5
+ export function adoptLegacyEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
6
+ for (const [name, value] of Object.entries(env)) {
7
+ if (!name.startsWith("WT_")) continue;
8
+ const current = `DEBUTLY_${name.slice(3)}`;
9
+ if (env[current] === undefined) env[current] = value;
10
+ }
11
+ return env;
12
+ }
13
+
14
+ adoptLegacyEnv();
@@ -0,0 +1,28 @@
1
+ // The debutly command reads a .env file in the folder it runs in, as `node --env-file=.env` does.
2
+ // Variables already set win. Imported first, before anything reads a setting.
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { parseEnv } from "node:util";
5
+
6
+ const KEYS = ["DEBUTLY_API_KEY", "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY"];
7
+ // Where the key is sent. A .env may choose that only along with the key itself: one in a repo
8
+ // you cloned must not send the key in your shell to an address of its choosing.
9
+ const ENDPOINTS = ["DEBUTLY_BASE_URL", "ANTHROPIC_BASE_URL"];
10
+
11
+ /** Adds the file's variables to `env`; returns a warning for each one left out. */
12
+ export function loadEnv(text: string, env: NodeJS.ProcessEnv = process.env): string[] {
13
+ const file = parseEnv(text);
14
+ const used = KEYS.find((k) => env[k] || file[k]);
15
+ const keyFromFile = used !== undefined && !env[used] && !!file[used];
16
+ const warnings: string[] = [];
17
+ for (const [name, value] of Object.entries(file)) {
18
+ if (env[name] !== undefined) continue;
19
+ if (ENDPOINTS.includes(name) && used && !keyFromFile) {
20
+ warnings.push(`.env here sets ${name}, but your key comes from your environment, so that address is ignored. Put the key in the same .env to use it.`);
21
+ continue;
22
+ }
23
+ env[name] = value;
24
+ }
25
+ return warnings;
26
+ }
27
+
28
+ if (existsSync(".env")) for (const warning of loadEnv(readFileSync(".env", "utf8"))) console.warn(warning);
@@ -0,0 +1,12 @@
1
+ export async function mapWithConcurrency<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]> {
2
+ const results = new Array<R>(items.length);
3
+ let next = 0;
4
+ const worker = async () => {
5
+ while (next < items.length) {
6
+ const i = next++;
7
+ results[i] = await fn(items[i], i);
8
+ }
9
+ };
10
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
11
+ return results;
12
+ }
@@ -0,0 +1,27 @@
1
+ // Which app a run tested, what started it and where its CI run lives, so the dashboard can
2
+ // group runs by app and link back. Flags win; in GitHub Actions the rest comes from its own
3
+ // variables, so a workflow needs no extra arguments.
4
+
5
+ export interface RunOrigin {
6
+ app?: string;
7
+ trigger?: string;
8
+ ciUrl?: string;
9
+ }
10
+
11
+ const TRIGGERS: Record<string, string> = {
12
+ schedule: "nightly",
13
+ pull_request: "pr",
14
+ pull_request_target: "pr",
15
+ workflow_dispatch: "manual",
16
+ push: "push",
17
+ };
18
+
19
+ export function runOrigin(flags: { app?: string; trigger?: string; ciUrl?: string }, env: Record<string, string | undefined>): RunOrigin {
20
+ const repo = env.GITHUB_REPOSITORY;
21
+ const inCi = !!(repo && env.GITHUB_RUN_ID);
22
+ return {
23
+ app: flags.app ?? (repo ? repo.split("/").pop() : undefined),
24
+ trigger: flags.trigger ?? (inCi ? (TRIGGERS[env.GITHUB_EVENT_NAME ?? ""] ?? env.GITHUB_EVENT_NAME) : "local"),
25
+ ciUrl: flags.ciUrl ?? (inCi ? `${env.GITHUB_SERVER_URL ?? "https://github.com"}/${repo}/actions/runs/${env.GITHUB_RUN_ID}` : undefined),
26
+ };
27
+ }
package/src/worker.ts ADDED
@@ -0,0 +1,53 @@
1
+ // A queue worker: runs agent sessions that runs (CLI with --queue, or later the API) enqueue.
2
+ // Scale out by starting more (docker compose up --scale worker=4).
3
+ //
4
+ // REDIS_URL=redis://… DATABASE_URL=postgres://… DEBUTLY_API_KEY=… npm run debutly:worker
5
+ //
6
+ // Optional: DEBUTLY_WORKER_CONCURRENCY (default 2), DEBUTLY_S3_* for screenshots (else ./runs/worker/<runId>).
7
+ import "./util/legacy-env";
8
+ import "./util/key-defaults";
9
+ import { join } from "node:path";
10
+ import { Worker } from "bullmq";
11
+ import { chromium } from "playwright";
12
+ import { S3ArtifactStore } from "./artifacts/s3";
13
+ import { LocalArtifactStore } from "./artifacts/store";
14
+ import { MailpitInbox } from "./inbox/mailpit";
15
+ import { createLlms } from "./llm/factory";
16
+ import { AGENT_QUEUE, redisClient, type AgentJob } from "./queue/jobs";
17
+ import { createAgentProcessor } from "./queue/processor";
18
+ import { openDb } from "./store/db";
19
+ import { PgRunStore } from "./store/pg-run-store";
20
+ import type { Session } from "./types";
21
+
22
+ const { REDIS_URL, DATABASE_URL } = process.env;
23
+ if (!REDIS_URL || !DATABASE_URL) {
24
+ console.error("The worker needs REDIS_URL and DATABASE_URL (and the DEBUTLY_* model settings).");
25
+ process.exit(1);
26
+ }
27
+
28
+ const { db, close } = await openDb(DATABASE_URL);
29
+ const browser = await chromium.launch();
30
+ const process_ = createAgentProcessor({
31
+ browser,
32
+ llm: createLlms(process.env).nav,
33
+ store: new PgRunStore(db),
34
+ artifactsFor: (runId) => S3ArtifactStore.fromEnv(process.env, runId) ?? new LocalArtifactStore(join("runs", "worker", runId)),
35
+ inboxFor: (url) => new MailpitInbox(url),
36
+ });
37
+
38
+ const worker = new Worker<AgentJob, Session>(AGENT_QUEUE, (job) => process_(job), {
39
+ connection: redisClient(REDIS_URL),
40
+ concurrency: Number(process.env.DEBUTLY_WORKER_CONCURRENCY ?? 2),
41
+ });
42
+ worker.on("completed", (job, s) => console.log(`[worker] ${job.data.persona.name} (${job.data.runId}): ${s.outcome}`));
43
+ worker.on("failed", (job, err) => console.error(`[worker] ${job?.data.persona.name ?? "?"}: ${err.message}`));
44
+ console.log(`[worker] waiting for agent sessions on ${AGENT_QUEUE}`);
45
+
46
+ const shutdown = async () => {
47
+ await worker.close();
48
+ await browser.close();
49
+ await close();
50
+ process.exit(0);
51
+ };
52
+ process.on("SIGTERM", shutdown);
53
+ process.on("SIGINT", shutdown);
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "noEmit": true,
10
+ "types": ["node"]
11
+ },
12
+ "include": ["src", "fixture", "tests", "scripts", "vitest.config.ts"]
13
+ }