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.
- package/LICENSE +662 -0
- package/README.md +192 -2
- package/bin/debutly.mjs +8 -0
- package/drizzle/0000_init.sql +107 -0
- package/drizzle/0001_predictions.sql +14 -0
- package/drizzle/0002_dashboard.sql +15 -0
- package/drizzle/meta/0000_snapshot.json +722 -0
- package/drizzle/meta/0001_snapshot.json +821 -0
- package/drizzle/meta/0002_snapshot.json +942 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +83 -4
- package/src/agent/describe.ts +29 -0
- package/src/agent/effect.ts +94 -0
- package/src/agent/give-up.ts +42 -0
- package/src/agent/label.ts +19 -0
- package/src/agent/loop.ts +302 -0
- package/src/agent/parse-reply.ts +127 -0
- package/src/agent/prompt.ts +85 -0
- package/src/agent/run-agent.ts +68 -0
- package/src/artifacts/db.ts +55 -0
- package/src/artifacts/s3.ts +36 -0
- package/src/artifacts/store.ts +26 -0
- package/src/bin.ts +140 -0
- package/src/browser/ax.ts +77 -0
- package/src/browser/execute.ts +401 -0
- package/src/browser/observe.ts +62 -0
- package/src/browser/reflow.ts +73 -0
- package/src/browser/samples.ts +119 -0
- package/src/browser/scan-script.ts +108 -0
- package/src/browser/settle.ts +32 -0
- package/src/browser/signals.ts +73 -0
- package/src/cancel.ts +20 -0
- package/src/cli.ts +281 -0
- package/src/config.ts +62 -0
- package/src/cost/estimate.ts +32 -0
- package/src/cost/prices.ts +32 -0
- package/src/cost/tiers.ts +27 -0
- package/src/costs.ts +96 -0
- package/src/crew/run-unit.ts +62 -0
- package/src/detect/findings.ts +649 -0
- package/src/detect/wcag.ts +38 -0
- package/src/doctor.ts +145 -0
- package/src/hear-cli.ts +137 -0
- package/src/hear.ts +309 -0
- package/src/import.ts +108 -0
- package/src/inbox/mailpit.ts +46 -0
- package/src/inbox/types.ts +10 -0
- package/src/init.ts +184 -0
- package/src/llm/anthropic.ts +41 -0
- package/src/llm/credits.ts +35 -0
- package/src/llm/factory.ts +65 -0
- package/src/llm/meter.ts +39 -0
- package/src/llm/openai-compatible.ts +90 -0
- package/src/llm/openrouter.ts +9 -0
- package/src/llm/scripted.ts +18 -0
- package/src/llm/types.ts +36 -0
- package/src/perceive.ts +150 -0
- package/src/perception/aggregate.ts +111 -0
- package/src/perception/compare.ts +77 -0
- package/src/perception/evaluate.ts +83 -0
- package/src/perception/exhibits.ts +146 -0
- package/src/perception/market-context.ts +50 -0
- package/src/perception/run-perception.ts +178 -0
- package/src/persona/generate.ts +92 -0
- package/src/persona/inclusion.ts +53 -0
- package/src/persona/language.ts +59 -0
- package/src/persona/people.ts +32 -0
- package/src/predictions.ts +33 -0
- package/src/queue/dispatch.ts +18 -0
- package/src/queue/jobs.ts +28 -0
- package/src/queue/processor.ts +42 -0
- package/src/report/fix-first.ts +121 -0
- package/src/report/launch-readiness.ts +106 -0
- package/src/report/markdown.ts +202 -0
- package/src/run.ts +141 -0
- package/src/sandbox/outbox.ts +65 -0
- package/src/store/db.ts +40 -0
- package/src/store/import-run.ts +116 -0
- package/src/store/pg-run-store.ts +163 -0
- package/src/store/predictions.ts +70 -0
- package/src/store/run-store.ts +78 -0
- package/src/store/schema.ts +193 -0
- package/src/store/url.ts +15 -0
- package/src/types.ts +201 -0
- package/src/util/chromium.ts +20 -0
- package/src/util/json.ts +7 -0
- package/src/util/key-defaults.ts +34 -0
- package/src/util/legacy-env.ts +14 -0
- package/src/util/load-env.ts +28 -0
- package/src/util/pool.ts +12 -0
- package/src/util/run-origin.ts +27 -0
- package/src/worker.ts +53 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Run sizes. Units are individuals or business crews; breadth evaluators judge pages on top.
|
|
2
|
+
export const TIERS = {
|
|
3
|
+
quick: { units: 3, employees: 0 }, // CI / pre-deploy smoke: owners only
|
|
4
|
+
standard: { units: 10, employees: 2 },
|
|
5
|
+
deep: { units: 30, employees: 3 },
|
|
6
|
+
} as const;
|
|
7
|
+
export type Tier = keyof typeof TIERS;
|
|
8
|
+
|
|
9
|
+
export interface RunSize {
|
|
10
|
+
tier: Tier;
|
|
11
|
+
units: number;
|
|
12
|
+
employees: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A tier's size, with explicitly given counts taking precedence. */
|
|
16
|
+
export function resolveRunSize(o: { tier?: string; units?: number; employees?: number }): RunSize {
|
|
17
|
+
const tier = (o.tier ?? "quick") as Tier;
|
|
18
|
+
if (!(tier in TIERS)) throw new Error(`--tier must be quick, standard or deep (got "${o.tier}")`);
|
|
19
|
+
return { tier, units: o.units ?? TIERS[tier].units, employees: o.employees ?? TIERS[tier].employees };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Agents a run will use: individuals count once; a crew is its owner plus employees. */
|
|
23
|
+
export const agentCount = (kind: "individual" | "business", size: RunSize) =>
|
|
24
|
+
kind === "individual" ? size.units : size.units * (1 + size.employees);
|
|
25
|
+
|
|
26
|
+
/** Breadth evaluators per tier: Quick ~50, Standard 200–500, Deep 1,000–5,000. */
|
|
27
|
+
export const EVALUATORS: Record<Tier, number> = { quick: 50, standard: 300, deep: 1500 };
|
package/src/costs.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Cost meter: what Debutly is spending, in one place.
|
|
2
|
+
//
|
|
3
|
+
// npm run debutly:costs -- [--repo owner/name ...]
|
|
4
|
+
//
|
|
5
|
+
// Shows the OpenRouter key's spend (today, this week, this month, limit left) and, for each app
|
|
6
|
+
// repo given, what its latest nightly agent run cost (read from the GitHub run log via `gh`).
|
|
7
|
+
import "./util/legacy-env";
|
|
8
|
+
import "./util/key-defaults";
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { parseArgs } from "node:util";
|
|
12
|
+
import { isOpenRouter } from "./llm/openrouter";
|
|
13
|
+
|
|
14
|
+
const run = promisify(execFile);
|
|
15
|
+
|
|
16
|
+
export interface KeySpend {
|
|
17
|
+
today: number;
|
|
18
|
+
week: number;
|
|
19
|
+
month: number;
|
|
20
|
+
total: number;
|
|
21
|
+
limit: number | null;
|
|
22
|
+
remaining: number | null;
|
|
23
|
+
resets: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function fetchKeySpend(baseUrl: string, apiKey: string, fetchFn: typeof fetch = fetch): Promise<KeySpend> {
|
|
27
|
+
const res = await fetchFn(`${baseUrl.replace(/\/+$/, "")}/key`, { headers: { authorization: `Bearer ${apiKey}` } });
|
|
28
|
+
if (!res.ok) throw new Error(`OpenRouter key info: HTTP ${res.status}`);
|
|
29
|
+
const { data: d } = (await res.json()) as { data: Record<string, unknown> };
|
|
30
|
+
const n = (k: string) => (typeof d[k] === "number" ? (d[k] as number) : 0);
|
|
31
|
+
return {
|
|
32
|
+
today: n("usage_daily"),
|
|
33
|
+
week: n("usage_weekly"),
|
|
34
|
+
month: n("usage_monthly"),
|
|
35
|
+
total: n("usage"),
|
|
36
|
+
limit: typeof d.limit === "number" ? d.limit : null,
|
|
37
|
+
remaining: typeof d.limit_remaining === "number" ? d.limit_remaining : null,
|
|
38
|
+
resets: typeof d.limit_reset === "string" ? d.limit_reset : null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The cost a Debutly run printed at the end ("Done: … cost $0.1234 …"), from a CI log. */
|
|
43
|
+
export function costFromLog(log: string): number | null {
|
|
44
|
+
const matches = [...log.matchAll(/Done: [^\n]*?cost \$(\d+(?:\.\d+)?)/g)];
|
|
45
|
+
return matches.length ? Number(matches[matches.length - 1][1]) : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface NightlyCost {
|
|
49
|
+
repo: string;
|
|
50
|
+
when: string;
|
|
51
|
+
costUsd: number | null;
|
|
52
|
+
url: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function latestNightly(repo: string): Promise<NightlyCost | null> {
|
|
56
|
+
const { stdout } = await run("gh", ["run", "list", "--repo", repo, "--workflow", "throwaway-env.yml", "--status", "success", "--limit", "5", "--json", "databaseId,createdAt,url,event"]);
|
|
57
|
+
const runs = JSON.parse(stdout) as { databaseId: number; createdAt: string; url: string; event: string }[];
|
|
58
|
+
for (const r of runs) {
|
|
59
|
+
const { stdout: log } = await run("gh", ["run", "view", String(r.databaseId), "--repo", repo, "--log"], { maxBuffer: 64 * 1024 * 1024 });
|
|
60
|
+
const costUsd = costFromLog(log);
|
|
61
|
+
if (costUsd !== null) return { repo, when: r.createdAt, costUsd, url: r.url };
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const usd = (x: number) => `$${x.toFixed(2)}`;
|
|
67
|
+
|
|
68
|
+
if (process.argv[1]?.endsWith("costs.ts")) {
|
|
69
|
+
const { values } = parseArgs({ options: { repo: { type: "string", multiple: true } } });
|
|
70
|
+
const { DEBUTLY_BASE_URL, DEBUTLY_API_KEY } = process.env;
|
|
71
|
+
if (DEBUTLY_BASE_URL && isOpenRouter(DEBUTLY_BASE_URL) && DEBUTLY_API_KEY) {
|
|
72
|
+
const s = await fetchKeySpend(DEBUTLY_BASE_URL, DEBUTLY_API_KEY);
|
|
73
|
+
console.log("OpenRouter key");
|
|
74
|
+
console.log(` Today ${usd(s.today)} · this week ${usd(s.week)} · this month ${usd(s.month)} · all time ${usd(s.total)}`);
|
|
75
|
+
if (s.limit !== null) console.log(` Limit ${usd(s.limit)}${s.resets ? ` (resets ${s.resets})` : ""} · left ${usd(s.remaining ?? 0)}`);
|
|
76
|
+
} else {
|
|
77
|
+
console.log("OpenRouter key: not configured (DEBUTLY_BASE_URL / DEBUTLY_API_KEY)");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const nightly: NightlyCost[] = [];
|
|
81
|
+
for (const repo of values.repo ?? []) {
|
|
82
|
+
try {
|
|
83
|
+
const n = await latestNightly(repo);
|
|
84
|
+
if (n) nightly.push(n);
|
|
85
|
+
else console.log(` ${repo}: no costed agent run found yet`);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.log(` ${repo}: could not read runs (${err instanceof Error ? err.message.split("\n")[0] : err})`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (nightly.length) {
|
|
91
|
+
console.log("\nLatest agent run per app");
|
|
92
|
+
for (const n of nightly) console.log(` ${n.repo.padEnd(40)} ${usd(n.costUsd ?? 0)} (${n.when.slice(0, 10)})`);
|
|
93
|
+
const perNight = nightly.reduce((a, n) => a + (n.costUsd ?? 0), 0);
|
|
94
|
+
console.log(` ${"Per night, all apps".padEnd(40)} ${usd(perNight)} → about ${usd(perNight * 30)} a month`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Inbox } from "../inbox/types";
|
|
2
|
+
import type { Business, CustomerUnit, Persona, Session } from "../types";
|
|
3
|
+
|
|
4
|
+
/** Runs one agent in its own browser context; `brief` joins its system prompt. */
|
|
5
|
+
export type RunAgent = (persona: Persona, goal: string, brief: string | undefined) => Promise<Session>;
|
|
6
|
+
|
|
7
|
+
export interface UnitDeps {
|
|
8
|
+
runAgent: RunAgent;
|
|
9
|
+
emailOf: (persona: Persona) => string;
|
|
10
|
+
inbox?: Inbox;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const article = (word: string) => (/^[aeiou]/i.test(word) ? "an" : "a");
|
|
14
|
+
|
|
15
|
+
const companyBrief = (b: Business) =>
|
|
16
|
+
`You work at ${b.name}, ${article(b.archetype)} ${b.archetype} of about ${b.size} people. Its budget for tools like this is $${b.monthlyBudget} a month. Company goal: ${b.goal}.`;
|
|
17
|
+
|
|
18
|
+
export function ownerGoal(goal: string, team: { name: string; role?: string; email: string }[]): string {
|
|
19
|
+
if (!team.length) return goal;
|
|
20
|
+
const who = team.map((t) => `${t.name} (${t.role ?? "Teammate"}) at ${t.email}`).join("; ");
|
|
21
|
+
return `${goal}\nOnce you are set up, invite your teammates by email so they can use it too: ${who}.`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// A teammate who lands in an empty workspace of their own has not joined anything, even if the
|
|
25
|
+
// invite page said "accepted"; that must not count as success.
|
|
26
|
+
export const employeeGoal = (role: string, owner: string, company: string) =>
|
|
27
|
+
`${owner} has invited you to use this product at ${company}. Check your email for the invite, accept it and set up your account. Then check that you are in ${company}'s shared workspace: you should see what ${owner} set up there, such as the company details or the items they added. If you only see an empty workspace of your own, joining did not work: report that as a failure, not success. If you are in, do one task a ${role} would typically do here. Finish when you have done that or when you would give up.`;
|
|
28
|
+
|
|
29
|
+
const tag = (s: Session, unitId: string, p: Persona): Session => ({ ...s, unitId, role: p.role, inclusion: p.inclusion, language: p.language });
|
|
30
|
+
const notInvited = (unitId: string, p: Persona, why: string): Session =>
|
|
31
|
+
tag({ agentId: p.id, personaName: p.name, goal: "", steps: [], outcome: "not_invited", outcomeNote: why }, unitId, p);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One customer unit. An individual runs once. A business crew runs its owner first (who is told
|
|
35
|
+
* whom to invite), then, in parallel and each in its own browser, only the employees whose invite
|
|
36
|
+
* actually reached their inbox, sharing the company brief and what the owner reported.
|
|
37
|
+
*/
|
|
38
|
+
export async function runUnit(unit: CustomerUnit, goal: string, deps: UnitDeps): Promise<Session[]> {
|
|
39
|
+
if (unit.kind === "individual") return [tag(await deps.runAgent(unit.person, goal, undefined), unit.id, unit.person)];
|
|
40
|
+
|
|
41
|
+
const { business, owner, employees } = unit;
|
|
42
|
+
const brief = companyBrief(business);
|
|
43
|
+
const team = employees.map((e) => ({ name: e.name, role: e.role, email: deps.emailOf(e) }));
|
|
44
|
+
const ownerSession = tag(
|
|
45
|
+
await deps.runAgent(owner, ownerGoal(goal, team), `${brief}\nYou run ${business.name} and decide which plan and how many seats to buy.`),
|
|
46
|
+
unit.id,
|
|
47
|
+
owner,
|
|
48
|
+
);
|
|
49
|
+
if (!employees.length) return [ownerSession];
|
|
50
|
+
const { inbox } = deps;
|
|
51
|
+
if (!inbox) return [ownerSession, ...employees.map((e) => notInvited(unit.id, e, "No test inbox, so teammates cannot receive invites."))];
|
|
52
|
+
|
|
53
|
+
const shared = `${brief}\n${owner.name} (the owner) reported: "${ownerSession.outcomeNote}"`;
|
|
54
|
+
const rest = await Promise.all(
|
|
55
|
+
employees.map(async (e) => {
|
|
56
|
+
const mail = await inbox.latest(deps.emailOf(e)).catch(() => []);
|
|
57
|
+
if (!mail.length) return notInvited(unit.id, e, `No invite email arrived after ${owner.name} finished.`);
|
|
58
|
+
return tag(await deps.runAgent(e, employeeGoal(e.role ?? "teammate", owner.name, business.name), shared), unit.id, e);
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
return [ownerSession, ...rest];
|
|
62
|
+
}
|