@flowriki/agent 0.1.0-beta.6 → 0.1.0-beta.7

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.
@@ -2,22 +2,22 @@
2
2
 
3
3
  // src/engine.ts
4
4
  import os5 from "os";
5
- import { createRequire as createRequire3 } from "module";
5
+ import { createRequire as createRequire4 } from "module";
6
6
  import { spawnSync } from "child_process";
7
7
 
8
8
  // ../agent-core/dist/index.js
9
9
  import { z as z4 } from "zod";
10
- import fs5 from "fs";
10
+ import fs6 from "fs";
11
11
  import path5 from "path";
12
12
  import crypto22 from "crypto";
13
- import { createRequire as createRequire2 } from "module";
13
+ import { createRequire as createRequire3 } from "module";
14
14
  import { WebSocket, WebSocketServer } from "ws";
15
15
  import fs23 from "fs";
16
16
  import os2 from "os";
17
- import path23 from "path";
17
+ import path24 from "path";
18
18
 
19
19
  // ../orchestrator/dist/index.js
20
- import fs4 from "fs";
20
+ import fs5 from "fs";
21
21
  import path4 from "path";
22
22
 
23
23
  // ../protocol/dist/chunk-ROEX7TWK.js
@@ -287,17 +287,17 @@ var FlowDocumentSchema = z.object({
287
287
  revision: z.number().int().nonnegative().default(0)
288
288
  }).strict().superRefine((flow, ctx) => {
289
289
  const ids = /* @__PURE__ */ new Set();
290
- flow.steps.forEach((s, i) => {
291
- if (ids.has(s.id)) ctx.addIssue({ code: "custom", path: ["steps", i, "id"], message: "Step IDs must be unique" });
292
- ids.add(s.id);
290
+ flow.steps.forEach((s2, i) => {
291
+ if (ids.has(s2.id)) ctx.addIssue({ code: "custom", path: ["steps", i, "id"], message: "Step IDs must be unique" });
292
+ ids.add(s2.id);
293
293
  });
294
294
  });
295
- var surfaceOf = (s) => JSON.stringify([s["pageId"] ?? "", s["frame"] ?? []]);
296
- var containerOf = (s) => JSON.stringify([targetOfStep(s), s["legacySelector"] ?? ""]);
297
- function targetOfStep(s) {
298
- const t = { role: s["role"] ?? "", name: s["name"] ?? "", scope: s["scope"] ?? "" };
295
+ var surfaceOf = (s2) => JSON.stringify([s2["pageId"] ?? "", s2["frame"] ?? []]);
296
+ var containerOf = (s2) => JSON.stringify([targetOfStep(s2), s2["legacySelector"] ?? ""]);
297
+ function targetOfStep(s2) {
298
+ const t = { role: s2["role"] ?? "", name: s2["name"] ?? "", scope: s2["scope"] ?? "" };
299
299
  for (const key of ["scopeRole", "frame", "pageId", "nameFrom", "locator", "nth"]) {
300
- if (s[key] !== void 0) t[key] = s[key];
300
+ if (s2[key] !== void 0) t[key] = s2[key];
301
301
  }
302
302
  return t;
303
303
  }
@@ -305,12 +305,12 @@ function migrateScrollPrerequisites(doc) {
305
305
  const steps = Array.isArray(doc.steps) ? doc.steps : [];
306
306
  const out = [];
307
307
  const pending = [];
308
- const isScroll = (s) => s["type"] === "scroll" && s["scroll"] !== void 0 && s["scroll"] !== null;
309
- const hasChecks = (s) => Array.isArray(s["proposals"]) && s["proposals"].length > 0;
310
- const carry = (s) => {
311
- const pos = s["scroll"] ?? {};
312
- const prerequisite = { x: Number(pos.x ?? 0), y: Number(pos.y ?? 0), container: targetOfStep(s) };
313
- if (typeof s["legacySelector"] === "string" && s["legacySelector"]) prerequisite["legacySelector"] = s["legacySelector"];
308
+ const isScroll = (s2) => s2["type"] === "scroll" && s2["scroll"] !== void 0 && s2["scroll"] !== null;
309
+ const hasChecks = (s2) => Array.isArray(s2["proposals"]) && s2["proposals"].length > 0;
310
+ const carry = (s2) => {
311
+ const pos = s2["scroll"] ?? {};
312
+ const prerequisite = { x: Number(pos.x ?? 0), y: Number(pos.y ?? 0), container: targetOfStep(s2) };
313
+ if (typeof s2["legacySelector"] === "string" && s2["legacySelector"]) prerequisite["legacySelector"] = s2["legacySelector"];
314
314
  return prerequisite;
315
315
  };
316
316
  for (const step of steps) {
@@ -784,7 +784,7 @@ function describeAssertion(a, subject) {
784
784
  // ../orchestrator/dist/index.js
785
785
  import { execFileSync } from "child_process";
786
786
  import fs22 from "fs";
787
- import path22 from "path";
787
+ import path23 from "path";
788
788
 
789
789
  // ../runner/dist/index.js
790
790
  import { chromium, firefox, webkit } from "playwright";
@@ -807,7 +807,7 @@ function installRecorder() {
807
807
  if (d.__flInstalled) return;
808
808
  d.__flInstalled = true;
809
809
  w.__flInstalled = true;
810
- const clean = (s) => (s || "").replace(/\s+/g, " ").trim();
810
+ const clean = (s2) => (s2 || "").replace(/\s+/g, " ").trim();
811
811
  const FRAME_CHAIN = (() => {
812
812
  const chain = [];
813
813
  try {
@@ -887,7 +887,7 @@ function installRecorder() {
887
887
  return (first && first.tagName ? first : e.target) ?? null;
888
888
  }
889
889
  w.__flSignature = () => {
890
- const texts = (sel) => Array.from(document.querySelectorAll(sel)).filter(readable).map((n) => clean(n.textContent)).filter(Boolean);
890
+ const texts = (sel) => Array.from(document.querySelectorAll(sel)).filter(readable).map((n2) => clean(n2.textContent)).filter(Boolean);
891
891
  return {
892
892
  url: location.href,
893
893
  title: document.title,
@@ -896,15 +896,15 @@ function installRecorder() {
896
896
  fields: Array.from(document.querySelectorAll("input, select, textarea")).filter(readable).map(accName).filter(Boolean),
897
897
  texts: (() => {
898
898
  const out = [];
899
- for (const n of Array.from(document.querySelectorAll("body *"))) {
899
+ for (const n2 of Array.from(document.querySelectorAll("body *"))) {
900
900
  if (out.length >= 200) break;
901
901
  const t = clean(
902
- Array.from(n.childNodes).filter((c) => c.nodeType === 3).map((c) => c.textContent).join(" ")
902
+ Array.from(n2.childNodes).filter((c) => c.nodeType === 3).map((c) => c.textContent).join(" ")
903
903
  );
904
904
  if (!t || t.length > 120) continue;
905
- if (!readable(n)) continue;
906
- const tag = (n.tagName || "").toLowerCase();
907
- out.push({ key: `${tag}${n.id ? "#" + n.id : ""}|${t}`, text: t });
905
+ if (!readable(n2)) continue;
906
+ const tag = (n2.tagName || "").toLowerCase();
907
+ out.push({ key: `${tag}${n2.id ? "#" + n2.id : ""}|${t}`, text: t });
908
908
  }
909
909
  return out;
910
910
  })()
@@ -1894,7 +1894,7 @@ function installDebugPanel() {
1894
1894
  back: "Back",
1895
1895
  forward: "Forward"
1896
1896
  })[t] || t;
1897
- const esc = (s) => String(s).replace(
1897
+ const esc = (s2) => String(s2).replace(
1898
1898
  /[&<>"]/g,
1899
1899
  (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]
1900
1900
  );
@@ -2320,7 +2320,7 @@ async function locateAll(page, step, opts = {}) {
2320
2320
  if (step.scope && !plan.overridden) {
2321
2321
  const roles = step.scopeRole ? [step.scopeRole] : [...SCOPE_ROLES];
2322
2322
  const containers = roles.map((role) => root.getByRole(role, { name: step.scope, exact: !step.scopeRole, includeHidden: opts.includeHidden }));
2323
- root = containers.reduce((a, b) => a.or(b));
2323
+ root = containers.reduce((a, b2) => a.or(b2));
2324
2324
  }
2325
2325
  if (plan.viaAdjacentLabel) return root.getByText(plan.arg, { exact: true }).locator("xpath=..").getByRole(step.role || "textbox", { includeHidden: opts.includeHidden });
2326
2326
  let loc;
@@ -2719,7 +2719,7 @@ async function startRecordingSession(opts) {
2719
2719
  void ensureInstalled(p);
2720
2720
  p.on("domcontentloaded", () => void ensureInstalled(p));
2721
2721
  p.on("download", (download) => {
2722
- const step = [...steps].reverse().find((s) => (s.pageId ?? "main") === pageIds.get(p));
2722
+ const step = [...steps].reverse().find((s2) => (s2.pageId ?? "main") === pageIds.get(p));
2723
2723
  if (step) {
2724
2724
  (step.downloads ??= []).push(download.suggestedFilename());
2725
2725
  onStep?.(step);
@@ -2727,7 +2727,7 @@ async function startRecordingSession(opts) {
2727
2727
  }
2728
2728
  });
2729
2729
  p.on("dialog", (dialog) => {
2730
- const step = [...steps].reverse().find((s) => (s.pageId ?? "main") === pageIds.get(p));
2730
+ const step = [...steps].reverse().find((s2) => (s2.pageId ?? "main") === pageIds.get(p));
2731
2731
  if (!step) {
2732
2732
  void dialog.dismiss();
2733
2733
  record({ type: "unsupported", name: "Dialog before first action", warning: "Record an explicit navigation before a page-load dialog", pageId: pageIds.get(p) }, null);
@@ -2746,7 +2746,7 @@ async function startRecordingSession(opts) {
2746
2746
  pageIds.set(p, id);
2747
2747
  watchPage(p);
2748
2748
  void p.opener().then((opener) => {
2749
- const step = [...steps].reverse().find((s) => (s.pageId ?? "main") === pageIds.get(opener));
2749
+ const step = [...steps].reverse().find((s2) => (s2.pageId ?? "main") === pageIds.get(opener));
2750
2750
  if (step) {
2751
2751
  step.opensPage = id;
2752
2752
  onStep?.(step);
@@ -3154,8 +3154,8 @@ async function checkAssertion(page, kind, value, timeoutMs = 3e3, assertion) {
3154
3154
  reading = { outcome: "value", value: kind === "url" ? page.url() : await page.title() };
3155
3155
  } else if (kind === "count") {
3156
3156
  try {
3157
- const n = await (await locateAll(page, asStep(assertion.target), { allowRoleOnly: true })).count();
3158
- reading = { outcome: "value", value: String(n) };
3157
+ const n2 = await (await locateAll(page, asStep(assertion.target), { allowRoleOnly: true })).count();
3158
+ reading = { outcome: "value", value: String(n2) };
3159
3159
  } catch (err) {
3160
3160
  reading = { outcome: "invalid", detail: firstLine2(err) };
3161
3161
  }
@@ -3225,7 +3225,7 @@ async function applyAction(loc, step, value, ctx = {}) {
3225
3225
  case "upload": {
3226
3226
  const name = String(value ?? "");
3227
3227
  const names = step.values ?? (name ? [name] : []);
3228
- const files = names.map((n) => ctx.resolveFile?.(n));
3228
+ const files = names.map((n2) => ctx.resolveFile?.(n2));
3229
3229
  const file = files.every(Boolean);
3230
3230
  if (!file) {
3231
3231
  throw Object.assign(
@@ -3271,7 +3271,7 @@ async function replayFlow(opts) {
3271
3271
  opts.environment,
3272
3272
  headless,
3273
3273
  slowMo,
3274
- { policy: opts.artifacts, dir: evidenceDir, hasSecrets: steps.some((s) => s.secret) }
3274
+ { policy: opts.artifacts, dir: evidenceDir, hasSecrets: steps.some((s2) => s2.secret) }
3275
3275
  );
3276
3276
  const page = await context.newPage();
3277
3277
  const pages = new ExecutionPages(context, page);
@@ -3639,7 +3639,7 @@ async function startDebugSession(opts) {
3639
3639
  onDisconnect,
3640
3640
  onEndRequested
3641
3641
  } = opts;
3642
- const steps = opts.steps.map((s) => ({ ...s }));
3642
+ const steps = opts.steps.map((s2) => ({ ...s2 }));
3643
3643
  const breakpoints = new Set(opts.breakpoints ?? []);
3644
3644
  const results = {};
3645
3645
  let status2 = "idle";
@@ -3661,7 +3661,7 @@ async function startDebugSession(opts) {
3661
3661
  status: status2,
3662
3662
  cursor,
3663
3663
  breakpoints: [...breakpoints],
3664
- steps: steps.map((s) => ({ ...s })),
3664
+ steps: steps.map((s2) => ({ ...s2 })),
3665
3665
  results: { ...results },
3666
3666
  failure,
3667
3667
  recording,
@@ -3669,15 +3669,15 @@ async function startDebugSession(opts) {
3669
3669
  mode: "debug"
3670
3670
  });
3671
3671
  const publish = () => {
3672
- const s = snapshot();
3673
- onState?.(s);
3674
- void paint(s);
3675
- return s;
3672
+ const s2 = snapshot();
3673
+ onState?.(s2);
3674
+ void paint(s2);
3675
+ return s2;
3676
3676
  };
3677
- async function paint(s) {
3677
+ async function paint(s2) {
3678
3678
  const safe2 = {
3679
- ...s,
3680
- steps: s.steps.map((st) => ({
3679
+ ...s2,
3680
+ steps: s2.steps.map((st) => ({
3681
3681
  id: st.id,
3682
3682
  type: st.type,
3683
3683
  name: st.name,
@@ -3720,7 +3720,7 @@ async function startDebugSession(opts) {
3720
3720
  legacySelector: payload.legacySelector ?? "",
3721
3721
  proposals: []
3722
3722
  };
3723
- const at = cursor ? steps.findIndex((s) => s.id === cursor) : steps.length;
3723
+ const at = cursor ? steps.findIndex((s2) => s2.id === cursor) : steps.length;
3724
3724
  const insertAt = at === -1 ? steps.length : at;
3725
3725
  if (["fill", "select", "setRange"].includes(step.type)) {
3726
3726
  const prev = steps[insertAt - 1];
@@ -3810,7 +3810,7 @@ async function startDebugSession(opts) {
3810
3810
  break;
3811
3811
  }
3812
3812
  }
3813
- const indexOf = (id) => id ? steps.findIndex((s) => s.id === id) : -1;
3813
+ const indexOf = (id) => id ? steps.findIndex((s2) => s2.id === id) : -1;
3814
3814
  function advanceFrom(i) {
3815
3815
  let next = i + 1;
3816
3816
  while (next < steps.length && applied.has(steps[next].id)) next += 1;
@@ -3969,7 +3969,7 @@ async function startDebugSession(opts) {
3969
3969
  return publish();
3970
3970
  },
3971
3971
  deleteStep(stepId) {
3972
- const i = steps.findIndex((s) => s.id === stepId);
3972
+ const i = steps.findIndex((s2) => s2.id === stepId);
3973
3973
  if (i === -1) return publish();
3974
3974
  if (cursor === stepId) cursor = steps[i + 1]?.id ?? null;
3975
3975
  if (failure?.stepId === stepId) failure = null;
@@ -3990,7 +3990,7 @@ async function startDebugSession(opts) {
3990
3990
  status2 = "finished";
3991
3991
  await browser.close().catch(() => {
3992
3992
  });
3993
- return steps.map((s) => ({ ...s }));
3993
+ return steps.map((s2) => ({ ...s2 }));
3994
3994
  },
3995
3995
  async abort() {
3996
3996
  closed = true;
@@ -4003,6 +4003,9 @@ async function startDebugSession(opts) {
4003
4003
  }
4004
4004
 
4005
4005
  // ../rundb/dist/index.js
4006
+ import fs4 from "fs";
4007
+ import { createRequire as createRequire2 } from "module";
4008
+ import path22 from "path";
4006
4009
  import os from "os";
4007
4010
  import path3 from "path";
4008
4011
  var MIGRATIONS2 = [
@@ -4150,9 +4153,46 @@ var MIGRATIONS2 = [
4150
4153
  `
4151
4154
  ];
4152
4155
  var SCHEMA_VERSION = MIGRATIONS2.length;
4156
+ var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
4157
+ var TIME_LEN = 10;
4158
+ var RANDOM_LEN = 16;
4159
+ var lastTime = -1;
4160
+ var lastRandom = [];
4161
+ function encodeTime(ms) {
4162
+ let out = "";
4163
+ for (let i = TIME_LEN - 1; i >= 0; i--) {
4164
+ out = CROCKFORD[ms % 32] + out;
4165
+ ms = Math.floor(ms / 32);
4166
+ }
4167
+ return out;
4168
+ }
4169
+ function bumpRandom() {
4170
+ for (let i = RANDOM_LEN - 1; i >= 0; i--) {
4171
+ if (lastRandom[i] < 31) {
4172
+ lastRandom[i]++;
4173
+ return;
4174
+ }
4175
+ lastRandom[i] = 0;
4176
+ }
4177
+ }
4178
+ function ulid(now = Date.now()) {
4179
+ if (now === lastTime) {
4180
+ bumpRandom();
4181
+ } else {
4182
+ lastTime = now;
4183
+ lastRandom = Array.from(crypto.getRandomValues(new Uint8Array(RANDOM_LEN)), (b2) => b2 % 32);
4184
+ }
4185
+ return encodeTime(now) + lastRandom.map((n2) => CROCKFORD[n2]).join("");
4186
+ }
4187
+ function rowId(prefix) {
4188
+ return `${prefix}_${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
4189
+ }
4153
4190
  function flowrikiHome() {
4154
4191
  return process.env.FLOWRIKI_HOME || path3.join(os.homedir(), ".flowriki");
4155
4192
  }
4193
+ function defaultDbPath() {
4194
+ return path3.join(flowrikiHome(), "flowriki.db");
4195
+ }
4156
4196
  function runArtifactsDir(runId, projectId = "default") {
4157
4197
  return path3.join(flowrikiHome(), "projects", safe(projectId), "runs", safe(runId));
4158
4198
  }
@@ -4167,6 +4207,469 @@ function isInside(root, candidate) {
4167
4207
  const target = path3.resolve(candidate);
4168
4208
  return target === base || target.startsWith(base + path3.sep);
4169
4209
  }
4210
+ var b = (v) => v ? 1 : 0;
4211
+ var s = (v) => v === void 0 || v === null ? null : String(v);
4212
+ var n = (v) => v === void 0 || v === null ? null : Number(v);
4213
+ var RUN_COLUMNS = `r.*,
4214
+ (SELECT COALESCE(SUM(steps_run), 0) FROM run_task t WHERE t.run_id = r.id) AS steps_run,
4215
+ (SELECT COALESCE(SUM(steps_total), 0) FROM run_task t WHERE t.run_id = r.id) AS steps_total`;
4216
+ var SqliteStorage = class {
4217
+ constructor(file = defaultDbPath()) {
4218
+ this.file = file;
4219
+ }
4220
+ file;
4221
+ db = null;
4222
+ async init() {
4223
+ if (this.db) return;
4224
+ let DatabaseSync2;
4225
+ try {
4226
+ ({ DatabaseSync: DatabaseSync2 } = createRequire2(import.meta.url)("node:sqlite"));
4227
+ } catch (err) {
4228
+ throw new Error(
4229
+ `run history needs node:sqlite, which this Node (${process.version}) does not provide \u2014 Node 22.13 or newer is required (${err.message})`
4230
+ );
4231
+ }
4232
+ if (this.file !== ":memory:") fs4.mkdirSync(path22.dirname(this.file), { recursive: true });
4233
+ const db = new DatabaseSync2(this.file);
4234
+ if (this.file !== ":memory:") db.exec("PRAGMA journal_mode = WAL");
4235
+ db.exec("PRAGMA busy_timeout = 5000");
4236
+ db.exec("PRAGMA foreign_keys = ON");
4237
+ const current = Number(db.prepare("PRAGMA user_version").get().user_version ?? 0);
4238
+ if (current > SCHEMA_VERSION) {
4239
+ throw new Error(
4240
+ `${this.file} was written by a newer Flowriki (schema v${current}, this build understands v${SCHEMA_VERSION})`
4241
+ );
4242
+ }
4243
+ for (let v = current; v < MIGRATIONS2.length; v++) {
4244
+ db.exec("BEGIN");
4245
+ try {
4246
+ db.exec(MIGRATIONS2[v]);
4247
+ db.exec(`PRAGMA user_version = ${v + 1}`);
4248
+ db.exec("COMMIT");
4249
+ } catch (err) {
4250
+ db.exec("ROLLBACK");
4251
+ throw err;
4252
+ }
4253
+ }
4254
+ this.db = db;
4255
+ }
4256
+ async close() {
4257
+ this.db?.close();
4258
+ this.db = null;
4259
+ }
4260
+ get handle() {
4261
+ if (!this.db) throw new Error("storage used before init()");
4262
+ return this.db;
4263
+ }
4264
+ tx(fn) {
4265
+ const db = this.handle;
4266
+ db.exec("BEGIN IMMEDIATE");
4267
+ try {
4268
+ const out = fn();
4269
+ db.exec("COMMIT");
4270
+ return out;
4271
+ } catch (err) {
4272
+ db.exec("ROLLBACK");
4273
+ throw err;
4274
+ }
4275
+ }
4276
+ // ---- runs ----------------------------------------------------------------
4277
+ async createRun(input) {
4278
+ const id = ulid();
4279
+ const queuedAt = (/* @__PURE__ */ new Date()).toISOString();
4280
+ this.handle.prepare(
4281
+ `INSERT INTO run (id, project_id, kind, trigger, status, flow_id, flow_name,
4282
+ git_sha, git_branch, git_dirty, env, base_url, concurrency, queued_at, flow_snapshot)
4283
+ VALUES (?,?,?,?,'queued',?,?,?,?,?,?,?,?,?,?)`
4284
+ ).run(
4285
+ id,
4286
+ input.projectId ?? "default",
4287
+ input.kind,
4288
+ input.trigger,
4289
+ input.flowId,
4290
+ input.flowName,
4291
+ input.git?.sha ?? null,
4292
+ input.git?.branch ?? null,
4293
+ b(input.git?.dirty),
4294
+ input.env ?? null,
4295
+ input.baseUrl ?? null,
4296
+ input.concurrency ?? null,
4297
+ queuedAt,
4298
+ input.flowSnapshot === void 0 ? null : JSON.stringify(input.flowSnapshot)
4299
+ );
4300
+ return await this.getRunSummary(id);
4301
+ }
4302
+ async startRun(runId) {
4303
+ this.handle.prepare("UPDATE run SET status='running', started_at=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), runId);
4304
+ }
4305
+ async finishRun(runId, input) {
4306
+ const row = this.handle.prepare("SELECT started_at, queued_at FROM run WHERE id=?").get(runId);
4307
+ if (!row) return;
4308
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
4309
+ const from = Date.parse(String(row.started_at ?? row.queued_at));
4310
+ this.handle.prepare(
4311
+ `UPDATE run SET status=?, finished_at=?, duration_ms=?, error_message=?, artifacts_dir=?
4312
+ WHERE id=?`
4313
+ ).run(
4314
+ input.status,
4315
+ finishedAt,
4316
+ Math.max(0, Date.parse(finishedAt) - from),
4317
+ input.errorMessage ?? null,
4318
+ input.artifactsDir ?? null,
4319
+ runId
4320
+ );
4321
+ }
4322
+ // ---- tasks and steps -----------------------------------------------------
4323
+ async createTask(runId, input) {
4324
+ const id = rowId("tsk");
4325
+ return this.tx(() => {
4326
+ const ordinal = input.ordinal ?? Number(
4327
+ this.handle.prepare("SELECT COUNT(*) AS c FROM run_task WHERE run_id=?").get(runId).c
4328
+ );
4329
+ this.handle.prepare(
4330
+ `INSERT INTO run_task (id, run_id, ordinal, case_name, row_id, capability_id,
4331
+ capability, provider, attempt, max_attempts, status, worker_id, queued_at, started_at)
4332
+ VALUES (?,?,?,?,?,?,?,?,?,?,'running',?,?,?)`
4333
+ ).run(
4334
+ id,
4335
+ runId,
4336
+ ordinal,
4337
+ input.caseName,
4338
+ input.rowId ?? null,
4339
+ input.capability.id,
4340
+ JSON.stringify(input.capability),
4341
+ input.capability.provider ?? "local",
4342
+ input.attempt ?? 1,
4343
+ input.maxAttempts ?? 1,
4344
+ input.workerId ?? null,
4345
+ (/* @__PURE__ */ new Date()).toISOString(),
4346
+ (/* @__PURE__ */ new Date()).toISOString()
4347
+ );
4348
+ this.handle.prepare("UPDATE run SET tasks_total = tasks_total + 1 WHERE id=?").run(runId);
4349
+ return id;
4350
+ });
4351
+ }
4352
+ async finishTask(taskId, input) {
4353
+ this.tx(() => {
4354
+ const row = this.handle.prepare("SELECT run_id, started_at, queued_at FROM run_task WHERE id=?").get(taskId);
4355
+ if (!row) return;
4356
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
4357
+ const from = Date.parse(String(row.started_at ?? row.queued_at ?? finishedAt));
4358
+ this.handle.prepare(
4359
+ `UPDATE run_task SET status=?, detail=?, session_id=?, session_url=?,
4360
+ steps_run=COALESCE(?, steps_run), steps_total=COALESCE(?, steps_total),
4361
+ finished_at=?, duration_ms=? WHERE id=?`
4362
+ ).run(
4363
+ input.status,
4364
+ input.detail ?? null,
4365
+ input.sessionId ?? null,
4366
+ input.sessionUrl ?? null,
4367
+ input.stepsRun ?? null,
4368
+ input.stepsTotal ?? null,
4369
+ finishedAt,
4370
+ Math.max(0, Date.parse(finishedAt) - from),
4371
+ taskId
4372
+ );
4373
+ const column = input.status === "passed" ? "tasks_passed" : input.status === "failed" || input.status === "errored" ? "tasks_failed" : null;
4374
+ if (column) {
4375
+ this.handle.prepare(`UPDATE run SET ${column} = ${column} + 1 WHERE id=?`).run(String(row.run_id));
4376
+ }
4377
+ });
4378
+ }
4379
+ async recordStep(taskId, input) {
4380
+ const id = rowId("stp");
4381
+ return this.tx(() => {
4382
+ this.handle.prepare(
4383
+ `INSERT INTO run_step (id, run_task_id, ordinal, step_id, action, target_role,
4384
+ target_name, target_scope, status, failure_kind, detail, duration_ms,
4385
+ page_url, page_title, value, secret)
4386
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
4387
+ ).run(
4388
+ id,
4389
+ taskId,
4390
+ input.ordinal,
4391
+ input.stepId,
4392
+ input.action,
4393
+ input.targetRole ?? null,
4394
+ input.targetName ?? null,
4395
+ input.targetScope ?? null,
4396
+ input.status,
4397
+ input.failureKind ?? null,
4398
+ input.detail ?? null,
4399
+ input.durationMs ?? null,
4400
+ input.pageUrl ?? null,
4401
+ input.pageTitle ?? null,
4402
+ // Never the plaintext. A secret arrives here already withheld.
4403
+ input.secret ? null : input.value ?? null,
4404
+ b(input.secret ?? false)
4405
+ );
4406
+ const stmt = this.handle.prepare(
4407
+ `INSERT INTO run_check (id, run_step_id, assertion_id, kind, expected, actual, ok, soft, detail)
4408
+ VALUES (?,?,?,?,?,?,?,?,?)`
4409
+ );
4410
+ for (const c of input.checks ?? []) {
4411
+ stmt.run(
4412
+ rowId("chk"),
4413
+ id,
4414
+ c.assertionId ?? null,
4415
+ c.kind,
4416
+ c.expected ?? null,
4417
+ c.actual ?? null,
4418
+ b(c.ok),
4419
+ b(c.soft ?? false),
4420
+ c.detail ?? null
4421
+ );
4422
+ }
4423
+ return id;
4424
+ });
4425
+ }
4426
+ async addArtifact(runId, input) {
4427
+ const artifact = {
4428
+ id: rowId("art"),
4429
+ runId,
4430
+ runStepId: input.runStepId ?? null,
4431
+ kind: input.kind,
4432
+ relPath: input.relPath,
4433
+ bytes: input.bytes ?? null,
4434
+ masked: input.masked ?? false,
4435
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
4436
+ };
4437
+ this.handle.prepare(
4438
+ `INSERT INTO artifact (id, run_id, run_step_id, kind, rel_path, bytes, masked, created_at)
4439
+ VALUES (?,?,?,?,?,?,?,?)`
4440
+ ).run(
4441
+ artifact.id,
4442
+ artifact.runId,
4443
+ artifact.runStepId,
4444
+ artifact.kind,
4445
+ artifact.relPath,
4446
+ artifact.bytes,
4447
+ b(artifact.masked),
4448
+ artifact.createdAt
4449
+ );
4450
+ return artifact;
4451
+ }
4452
+ // ---- events --------------------------------------------------------------
4453
+ async appendEvent(runId, type, payload) {
4454
+ return this.tx(() => {
4455
+ const seq = Number(
4456
+ this.handle.prepare("SELECT COALESCE(MAX(seq), 0) AS m FROM run_event WHERE run_id=?").get(runId).m
4457
+ ) + 1;
4458
+ this.handle.prepare("INSERT INTO run_event (run_id, seq, at, type, payload) VALUES (?,?,?,?,?)").run(runId, seq, (/* @__PURE__ */ new Date()).toISOString(), type, JSON.stringify(payload ?? null));
4459
+ return seq;
4460
+ });
4461
+ }
4462
+ async listEvents(runId, since = 0) {
4463
+ return this.handle.prepare("SELECT seq, at, type, payload FROM run_event WHERE run_id=? AND seq>? ORDER BY seq").all(runId, since).map((r) => ({
4464
+ seq: Number(r.seq),
4465
+ at: String(r.at),
4466
+ type: String(r.type),
4467
+ payload: JSON.parse(String(r.payload))
4468
+ }));
4469
+ }
4470
+ // ---- reads ---------------------------------------------------------------
4471
+ async getRunSummary(runId) {
4472
+ const row = this.handle.prepare(`SELECT ${RUN_COLUMNS} FROM run r WHERE r.id=?`).get(runId);
4473
+ return row ? toSummary(row) : null;
4474
+ }
4475
+ async listRuns(query = {}) {
4476
+ const limit = Math.min(Math.max(query.limit ?? 25, 1), 200);
4477
+ const where = [];
4478
+ const params = [];
4479
+ const restrict = (clause, value) => {
4480
+ where.push(clause);
4481
+ params.push(value);
4482
+ };
4483
+ if (query.projectId) restrict("r.project_id=?", query.projectId);
4484
+ if (query.flowId) restrict("r.flow_id=?", query.flowId);
4485
+ if (query.status) restrict("r.status=?", query.status);
4486
+ if (query.branch) restrict("r.git_branch=?", query.branch);
4487
+ if (query.cursor) restrict("r.id<?", query.cursor);
4488
+ const rows = this.handle.prepare(
4489
+ `SELECT ${RUN_COLUMNS} FROM run r
4490
+ ${where.length ? `WHERE ${where.join(" AND ")}` : ""}
4491
+ ORDER BY r.id DESC LIMIT ?`
4492
+ ).all(...params, limit + 1);
4493
+ const page = rows.slice(0, limit);
4494
+ return {
4495
+ runs: page.map(toSummary),
4496
+ nextCursor: rows.length > limit ? String(page[page.length - 1].id) : null
4497
+ };
4498
+ }
4499
+ async getRun(runId) {
4500
+ const run = this.handle.prepare(`SELECT ${RUN_COLUMNS} FROM run r WHERE r.id=?`).get(runId);
4501
+ if (!run) return null;
4502
+ const tasks = this.handle.prepare("SELECT * FROM run_task WHERE run_id=? ORDER BY ordinal").all(runId);
4503
+ const steps = this.handle.prepare(
4504
+ `SELECT s.* FROM run_step s JOIN run_task t ON t.id = s.run_task_id
4505
+ WHERE t.run_id=? ORDER BY t.ordinal, s.ordinal`
4506
+ ).all(runId);
4507
+ const artifacts = this.handle.prepare("SELECT * FROM artifact WHERE run_id=? ORDER BY created_at").all(runId);
4508
+ const checks = this.handle.prepare(
4509
+ `SELECT c.* FROM run_check c
4510
+ JOIN run_step s ON s.id = c.run_step_id
4511
+ JOIN run_task t ON t.id = s.run_task_id
4512
+ WHERE t.run_id=?`
4513
+ ).all(runId);
4514
+ const checksByStep = /* @__PURE__ */ new Map();
4515
+ for (const c of checks) {
4516
+ const list = checksByStep.get(String(c.run_step_id)) ?? [];
4517
+ list.push({
4518
+ id: String(c.id),
4519
+ assertionId: s(c.assertion_id),
4520
+ kind: String(c.kind),
4521
+ expected: s(c.expected),
4522
+ actual: s(c.actual),
4523
+ ok: Number(c.ok) === 1,
4524
+ soft: Number(c.soft) === 1,
4525
+ detail: s(c.detail)
4526
+ });
4527
+ checksByStep.set(String(c.run_step_id), list);
4528
+ }
4529
+ const stepsByTask = /* @__PURE__ */ new Map();
4530
+ for (const st of steps) {
4531
+ const list = stepsByTask.get(String(st.run_task_id)) ?? [];
4532
+ list.push({
4533
+ id: String(st.id),
4534
+ ordinal: Number(st.ordinal),
4535
+ stepId: String(st.step_id),
4536
+ action: String(st.action),
4537
+ targetRole: s(st.target_role),
4538
+ targetName: s(st.target_name),
4539
+ targetScope: s(st.target_scope),
4540
+ value: s(st.value),
4541
+ secret: Number(st.secret) === 1,
4542
+ status: String(st.status),
4543
+ failureKind: s(st.failure_kind),
4544
+ detail: s(st.detail),
4545
+ durationMs: n(st.duration_ms),
4546
+ pageUrl: s(st.page_url),
4547
+ pageTitle: s(st.page_title),
4548
+ checks: checksByStep.get(String(st.id)) ?? []
4549
+ });
4550
+ stepsByTask.set(String(st.run_task_id), list);
4551
+ }
4552
+ return {
4553
+ ...toSummary(run),
4554
+ errorMessage: s(run.error_message),
4555
+ artifactsDir: s(run.artifacts_dir),
4556
+ artifacts: artifacts.map(
4557
+ (a) => ({
4558
+ id: String(a.id),
4559
+ runId: String(a.run_id),
4560
+ runStepId: s(a.run_step_id),
4561
+ kind: String(a.kind),
4562
+ relPath: String(a.rel_path),
4563
+ bytes: n(a.bytes),
4564
+ masked: Number(a.masked) === 1,
4565
+ createdAt: String(a.created_at)
4566
+ })
4567
+ ),
4568
+ tasks: tasks.map(
4569
+ (t) => ({
4570
+ id: String(t.id),
4571
+ ordinal: Number(t.ordinal),
4572
+ caseName: String(t.case_name),
4573
+ rowId: s(t.row_id),
4574
+ capability: JSON.parse(String(t.capability)),
4575
+ provider: String(t.provider),
4576
+ attempt: Number(t.attempt),
4577
+ maxAttempts: Number(t.max_attempts),
4578
+ status: String(t.status),
4579
+ workerId: s(t.worker_id),
4580
+ sessionId: s(t.session_id),
4581
+ sessionUrl: s(t.session_url),
4582
+ detail: s(t.detail),
4583
+ stepsRun: Number(t.steps_run),
4584
+ stepsTotal: Number(t.steps_total),
4585
+ durationMs: n(t.duration_ms),
4586
+ steps: stepsByTask.get(String(t.id)) ?? []
4587
+ })
4588
+ )
4589
+ };
4590
+ }
4591
+ async deleteRun(runId) {
4592
+ this.handle.prepare("DELETE FROM run WHERE id=?").run(runId);
4593
+ }
4594
+ async pruneRuns(keep) {
4595
+ if (keep < 0) throw new Error("keep must not be negative");
4596
+ return this.tx(() => {
4597
+ const doomed = this.handle.prepare(
4598
+ `SELECT id FROM (
4599
+ SELECT id, ROW_NUMBER() OVER (
4600
+ PARTITION BY COALESCE(flow_id, '') ORDER BY id DESC
4601
+ ) AS rn FROM run
4602
+ ) WHERE rn > ?`
4603
+ ).all(keep).map((r) => String(r.id));
4604
+ const stmt = this.handle.prepare("DELETE FROM run WHERE id=?");
4605
+ for (const id of doomed) stmt.run(id);
4606
+ return doomed;
4607
+ });
4608
+ }
4609
+ async runStats(days, projectId = "default") {
4610
+ const since = new Date(Date.now() - days * 864e5).toISOString();
4611
+ const row = this.handle.prepare(
4612
+ `SELECT COUNT(*) AS total,
4613
+ SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END) AS passed,
4614
+ SUM(CASE WHEN status IN ('failed', 'errored') THEN 1 ELSE 0 END) AS failed
4615
+ FROM run
4616
+ WHERE project_id = ? AND queued_at >= ? AND finished_at IS NOT NULL
4617
+ -- A flakiness check repeats one flow on purpose and reports how
4618
+ -- often it passed. Counting a deliberate 98/100 as a failing run
4619
+ -- would make measuring reliability look like losing it.
4620
+ AND kind <> 'flake'`
4621
+ ).get(projectId, since);
4622
+ return {
4623
+ total: Number(row.total ?? 0),
4624
+ passed: Number(row.passed ?? 0),
4625
+ failed: Number(row.failed ?? 0)
4626
+ };
4627
+ }
4628
+ async stepHistory(stepId, limit = 20) {
4629
+ return this.handle.prepare(
4630
+ `SELECT r.id AS run_id, COALESCE(r.started_at, r.queued_at) AS at, s.status,
4631
+ s.failure_kind, s.detail, s.duration_ms, r.git_sha
4632
+ FROM run_step s
4633
+ JOIN run_task t ON t.id = s.run_task_id
4634
+ JOIN run r ON r.id = t.run_id
4635
+ WHERE s.step_id = ?
4636
+ ORDER BY r.id DESC LIMIT ?`
4637
+ ).all(stepId, limit).map((r) => ({
4638
+ runId: String(r.run_id),
4639
+ at: String(r.at),
4640
+ status: String(r.status),
4641
+ failureKind: s(r.failure_kind),
4642
+ detail: s(r.detail),
4643
+ durationMs: n(r.duration_ms),
4644
+ gitSha: s(r.git_sha)
4645
+ })).reverse();
4646
+ }
4647
+ };
4648
+ function toSummary(row) {
4649
+ return {
4650
+ id: String(row.id),
4651
+ projectId: String(row.project_id),
4652
+ kind: String(row.kind),
4653
+ trigger: String(row.trigger),
4654
+ status: String(row.status),
4655
+ flowId: s(row.flow_id),
4656
+ flowName: s(row.flow_name),
4657
+ gitSha: s(row.git_sha),
4658
+ gitBranch: s(row.git_branch),
4659
+ gitDirty: Number(row.git_dirty) === 1,
4660
+ env: s(row.env),
4661
+ concurrency: n(row.concurrency),
4662
+ tasksTotal: Number(row.tasks_total),
4663
+ tasksPassed: Number(row.tasks_passed),
4664
+ tasksFailed: Number(row.tasks_failed),
4665
+ stepsRun: Number(row.steps_run ?? 0),
4666
+ stepsTotal: Number(row.steps_total ?? 0),
4667
+ queuedAt: String(row.queued_at),
4668
+ startedAt: s(row.started_at),
4669
+ finishedAt: s(row.finished_at),
4670
+ durationMs: n(row.duration_ms)
4671
+ };
4672
+ }
4170
4673
 
4171
4674
  // ../orchestrator/dist/index.js
4172
4675
  import crypto2 from "crypto";
@@ -4189,17 +4692,17 @@ var FlowConflict = class extends Error {
4189
4692
  };
4190
4693
  function writeFileAtomic(file, contents) {
4191
4694
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
4192
- const handle = fs4.openSync(tmp, "w");
4695
+ const handle = fs5.openSync(tmp, "w");
4193
4696
  try {
4194
- fs4.writeFileSync(handle, contents);
4195
- fs4.fsyncSync(handle);
4697
+ fs5.writeFileSync(handle, contents);
4698
+ fs5.fsyncSync(handle);
4196
4699
  } finally {
4197
- fs4.closeSync(handle);
4700
+ fs5.closeSync(handle);
4198
4701
  }
4199
4702
  try {
4200
- fs4.renameSync(tmp, file);
4703
+ fs5.renameSync(tmp, file);
4201
4704
  } catch (err) {
4202
- fs4.rmSync(tmp, { force: true });
4705
+ fs5.rmSync(tmp, { force: true });
4203
4706
  throw err;
4204
4707
  }
4205
4708
  }
@@ -4213,9 +4716,9 @@ var FlowNotRunnable = class extends Error {
4213
4716
  var DEFAULT_FLAKINESS_REPEAT = 10;
4214
4717
  var MAX_FLAKINESS_REPEAT = 500;
4215
4718
  function clampRepeat(value) {
4216
- const n = Math.floor(Number(value));
4217
- if (!Number.isFinite(n) || n < 2) return DEFAULT_FLAKINESS_REPEAT;
4218
- return Math.min(n, MAX_FLAKINESS_REPEAT);
4719
+ const n2 = Math.floor(Number(value));
4720
+ if (!Number.isFinite(n2) || n2 < 2) return DEFAULT_FLAKINESS_REPEAT;
4721
+ return Math.min(n2, MAX_FLAKINESS_REPEAT);
4219
4722
  }
4220
4723
  function uniqueRef(flowSlug, name, taken) {
4221
4724
  const base = `${flowSlug}_${slugify(name) || "secret"}`;
@@ -4223,8 +4726,8 @@ function uniqueRef(flowSlug, name, taken) {
4223
4726
  taken.add(base);
4224
4727
  return base;
4225
4728
  }
4226
- for (let n = 2; ; n++) {
4227
- const ref = `${base}_${n}`;
4729
+ for (let n2 = 2; ; n2++) {
4730
+ const ref = `${base}_${n2}`;
4228
4731
  if (!taken.has(ref)) {
4229
4732
  taken.add(ref);
4230
4733
  return ref;
@@ -4246,14 +4749,14 @@ var Store = class {
4246
4749
  return slug;
4247
4750
  }
4248
4751
  listFeatures() {
4249
- if (!fs4.existsSync(this.featuresDir)) return [];
4752
+ if (!fs5.existsSync(this.featuresDir)) return [];
4250
4753
  const out = [];
4251
- for (const entry of fs4.readdirSync(this.featuresDir, { withFileTypes: true })) {
4754
+ for (const entry of fs5.readdirSync(this.featuresDir, { withFileTypes: true })) {
4252
4755
  if (!entry.isDirectory()) continue;
4253
4756
  const file = path4.join(this.featuresDir, entry.name, "feature.json");
4254
- if (!fs4.existsSync(file)) continue;
4757
+ if (!fs5.existsSync(file)) continue;
4255
4758
  try {
4256
- out.push(FeatureSchema.parse(JSON.parse(fs4.readFileSync(file, "utf8"))));
4759
+ out.push(FeatureSchema.parse(JSON.parse(fs5.readFileSync(file, "utf8"))));
4257
4760
  } catch (err) {
4258
4761
  this.featureProblems.push({
4259
4762
  path: path4.relative(this.root, file),
@@ -4262,14 +4765,14 @@ var Store = class {
4262
4765
  });
4263
4766
  }
4264
4767
  }
4265
- return out.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
4768
+ return out.sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
4266
4769
  }
4267
4770
  featureProblems = [];
4268
4771
  createFeature(name, description = "") {
4269
4772
  const slug = this.assertSlug(slugify(name));
4270
4773
  const dir = path4.join(this.featuresDir, slug);
4271
- if (fs4.existsSync(dir)) throw new Error(`a feature named "${name}" already exists`);
4272
- fs4.mkdirSync(dir, { recursive: true });
4774
+ if (fs5.existsSync(dir)) throw new Error(`a feature named "${name}" already exists`);
4775
+ fs5.mkdirSync(dir, { recursive: true });
4273
4776
  const doc = {
4274
4777
  schemaVersion: 1,
4275
4778
  id: `f_${slug}`,
@@ -4278,11 +4781,11 @@ var Store = class {
4278
4781
  description: description.trim(),
4279
4782
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
4280
4783
  };
4281
- fs4.writeFileSync(path4.join(dir, "feature.json"), JSON.stringify(doc, null, 2) + "\n");
4784
+ fs5.writeFileSync(path4.join(dir, "feature.json"), JSON.stringify(doc, null, 2) + "\n");
4282
4785
  return doc;
4283
4786
  }
4284
4787
  deleteFeature(slug) {
4285
- fs4.rmSync(path4.join(this.featuresDir, this.assertSlug(slug)), { recursive: true, force: true });
4788
+ fs5.rmSync(path4.join(this.featuresDir, this.assertSlug(slug)), { recursive: true, force: true });
4286
4789
  }
4287
4790
  /** Every flow that could be read.
4288
4791
  *
@@ -4297,22 +4800,22 @@ var Store = class {
4297
4800
  this.problemsFound = [];
4298
4801
  for (const slug of features) {
4299
4802
  const dir = path4.join(this.featuresDir, slug);
4300
- if (!fs4.existsSync(dir)) continue;
4301
- for (const file of fs4.readdirSync(dir)) {
4803
+ if (!fs5.existsSync(dir)) continue;
4804
+ for (const file of fs5.readdirSync(dir)) {
4302
4805
  if (!file.endsWith(".flow.json")) continue;
4303
4806
  const full = path4.join(dir, file);
4304
4807
  try {
4305
- out.push(parseFlowDocument(JSON.parse(fs4.readFileSync(full, "utf8"))));
4808
+ out.push(parseFlowDocument(JSON.parse(fs5.readFileSync(full, "utf8"))));
4306
4809
  } catch (err) {
4307
4810
  this.problemsFound.push({
4308
4811
  path: path4.relative(this.root, full),
4309
4812
  error: String(err instanceof Error ? err.message : err).split("\n")[0].trim(),
4310
- recoverable: fs4.existsSync(this.recoveryPathFor(full))
4813
+ recoverable: fs5.existsSync(this.recoveryPathFor(full))
4311
4814
  });
4312
4815
  }
4313
4816
  }
4314
4817
  }
4315
- return out.sort((a, b) => a.name.localeCompare(b.name));
4818
+ return out.sort((a, b2) => a.name.localeCompare(b2.name));
4316
4819
  }
4317
4820
  problemsFound = [];
4318
4821
  /** Documents that could not be read on the last listing, and whether a
@@ -4337,8 +4840,8 @@ var Store = class {
4337
4840
  recoverFlowFile(relativePath) {
4338
4841
  const file = path4.join(this.root, relativePath);
4339
4842
  const backup = this.recoveryPathFor(file);
4340
- if (!fs4.existsSync(backup)) return false;
4341
- fs4.copyFileSync(backup, file);
4843
+ if (!fs5.existsSync(backup)) return false;
4844
+ fs5.copyFileSync(backup, file);
4342
4845
  return true;
4343
4846
  }
4344
4847
  /** Write a flow, atomically, keeping the previous contents recoverable.
@@ -4356,10 +4859,10 @@ var Store = class {
4356
4859
  const doc = parseFlowDocument(input);
4357
4860
  const featureSlug = this.assertSlug(doc.featureId.replace(/^f_/, ""));
4358
4861
  const dir = path4.join(this.featuresDir, featureSlug);
4359
- fs4.mkdirSync(dir, { recursive: true });
4862
+ fs5.mkdirSync(dir, { recursive: true });
4360
4863
  const file = path4.join(dir, `${this.assertSlug(doc.slug)}.flow.json`);
4361
4864
  const destination = this.readFlowFile(file);
4362
- if (fs4.existsSync(file) && (!destination || destination.id !== doc.id)) {
4865
+ if (fs5.existsSync(file) && (!destination || destination.id !== doc.id)) {
4363
4866
  throw new FlowConflict("the destination contains another flow or an unreadable document \u2014 choose another name or recover it first", destination?.revision ?? 0);
4364
4867
  }
4365
4868
  const current = this.getFlow(doc.id) ?? destination;
@@ -4377,20 +4880,20 @@ var Store = class {
4377
4880
  }
4378
4881
  doc.revision = (current?.revision ?? doc.revision ?? 0) + 1;
4379
4882
  const previousFile = current ? path4.join(this.root, this.relPath(current)) : file;
4380
- if (fs4.existsSync(previousFile)) {
4883
+ if (fs5.existsSync(previousFile)) {
4381
4884
  const backup = this.recoveryPathFor(previousFile);
4382
- fs4.mkdirSync(path4.dirname(backup), { recursive: true });
4383
- fs4.copyFileSync(previousFile, backup);
4885
+ fs5.mkdirSync(path4.dirname(backup), { recursive: true });
4886
+ fs5.copyFileSync(previousFile, backup);
4384
4887
  this.ensureRecoveryIgnored();
4385
4888
  }
4386
4889
  writeFileAtomic(file, JSON.stringify(doc, null, 2) + "\n");
4387
- if (previousFile !== file) fs4.rmSync(previousFile);
4890
+ if (previousFile !== file) fs5.rmSync(previousFile);
4388
4891
  return path4.relative(this.root, file);
4389
4892
  }
4390
4893
  /** Read one flow file, or null when it is missing or unreadable. */
4391
4894
  readFlowFile(file) {
4392
4895
  try {
4393
- return parseFlowDocument(JSON.parse(fs4.readFileSync(file, "utf8")));
4896
+ return parseFlowDocument(JSON.parse(fs5.readFileSync(file, "utf8")));
4394
4897
  } catch {
4395
4898
  return null;
4396
4899
  }
@@ -4427,7 +4930,7 @@ var Store = class {
4427
4930
  deleteFlowDocument(doc) {
4428
4931
  const featureSlug = this.assertSlug(doc.featureId.replace(/^f_/, ""));
4429
4932
  this.assertSlug(doc.slug);
4430
- fs4.rmSync(path4.join(this.featuresDir, featureSlug, `${doc.slug}.flow.json`), { force: true });
4933
+ fs5.rmSync(path4.join(this.featuresDir, featureSlug, `${doc.slug}.flow.json`), { force: true });
4431
4934
  }
4432
4935
  relPath(doc) {
4433
4936
  return path4.join(".flowriki", "features", doc.featureId.replace(/^f_/, ""), `${doc.slug}.flow.json`);
@@ -4460,7 +4963,7 @@ var Store = class {
4460
4963
  }
4461
4964
  readConfig() {
4462
4965
  try {
4463
- const raw = JSON.parse(fs4.readFileSync(this.configPath, "utf8"));
4966
+ const raw = JSON.parse(fs5.readFileSync(this.configPath, "utf8"));
4464
4967
  return {
4465
4968
  // Normalised on write, so anything here is already a URL. Read back
4466
4969
  // leniently anyway: this file is git-tracked and routinely arrives
@@ -4489,8 +4992,8 @@ var Store = class {
4489
4992
  artifacts: ArtifactPolicySchema.parse(input.artifacts ?? existing.artifacts),
4490
4993
  flakinessRepeat: clampRepeat(input.flakinessRepeat ?? existing.flakinessRepeat)
4491
4994
  };
4492
- fs4.mkdirSync(path4.dirname(this.configPath), { recursive: true });
4493
- fs4.writeFileSync(this.configPath, JSON.stringify(config, null, 2) + "\n");
4995
+ fs5.mkdirSync(path4.dirname(this.configPath), { recursive: true });
4996
+ fs5.writeFileSync(this.configPath, JSON.stringify(config, null, 2) + "\n");
4494
4997
  return config;
4495
4998
  }
4496
4999
  /** Where a flow actually starts.
@@ -4518,7 +5021,7 @@ var Store = class {
4518
5021
  }
4519
5022
  readSecrets() {
4520
5023
  try {
4521
- return JSON.parse(fs4.readFileSync(this.secretsPath, "utf8"));
5024
+ return JSON.parse(fs5.readFileSync(this.secretsPath, "utf8"));
4522
5025
  } catch {
4523
5026
  return {};
4524
5027
  }
@@ -4564,8 +5067,8 @@ var Store = class {
4564
5067
  changed = true;
4565
5068
  }
4566
5069
  if (changed) {
4567
- fs4.mkdirSync(path4.dirname(this.secretsPath), { recursive: true });
4568
- fs4.writeFileSync(this.secretsPath, JSON.stringify(secrets, null, 2) + "\n", { mode: 384 });
5070
+ fs5.mkdirSync(path4.dirname(this.secretsPath), { recursive: true });
5071
+ fs5.writeFileSync(this.secretsPath, JSON.stringify(secrets, null, 2) + "\n", { mode: 384 });
4569
5072
  this.ensureSecretsIgnored();
4570
5073
  }
4571
5074
  return steps;
@@ -4585,7 +5088,7 @@ var Store = class {
4585
5088
  const first = name;
4586
5089
  if (!first || first.includes("/") || first.includes("\\") || first.includes("..")) return void 0;
4587
5090
  const file = path4.join(this.filesDir, first);
4588
- return fs4.existsSync(file) ? file : void 0;
5091
+ return fs5.existsSync(file) ? file : void 0;
4589
5092
  }
4590
5093
  /** The secret references a flow needs that this machine does not hold.
4591
5094
  *
@@ -4612,8 +5115,8 @@ var Store = class {
4612
5115
  if (!/^[a-z0-9][a-z0-9_-]*$/.test(ref)) throw new Error(`invalid secret name: ${ref}`);
4613
5116
  const secrets = this.readSecrets();
4614
5117
  secrets[ref] = value;
4615
- fs4.mkdirSync(path4.dirname(this.secretsPath), { recursive: true });
4616
- fs4.writeFileSync(this.secretsPath, JSON.stringify(secrets, null, 2) + "\n", { mode: 384 });
5118
+ fs5.mkdirSync(path4.dirname(this.secretsPath), { recursive: true });
5119
+ fs5.writeFileSync(this.secretsPath, JSON.stringify(secrets, null, 2) + "\n", { mode: 384 });
4617
5120
  this.ensureSecretsIgnored();
4618
5121
  }
4619
5122
  /** Move plaintext passwords out of flow documents recorded before secrets
@@ -4636,7 +5139,7 @@ var Store = class {
4636
5139
  const healed = [];
4637
5140
  for (const flow of this.listFlows()) {
4638
5141
  let changed = false;
4639
- const taken = new Set(flow.steps.flatMap((s) => s.secretRef ? [s.secretRef] : []));
5142
+ const taken = new Set(flow.steps.flatMap((s2) => s2.secretRef ? [s2.secretRef] : []));
4640
5143
  for (const step of flow.steps) {
4641
5144
  if (!step.secret || step.secretRef || !step.value) continue;
4642
5145
  if (isSecretRefName(step.value)) continue;
@@ -4653,8 +5156,8 @@ var Store = class {
4653
5156
  }
4654
5157
  }
4655
5158
  if (healed.length) {
4656
- fs4.mkdirSync(path4.dirname(this.secretsPath), { recursive: true });
4657
- fs4.writeFileSync(this.secretsPath, JSON.stringify(secrets, null, 2) + "\n", { mode: 384 });
5159
+ fs5.mkdirSync(path4.dirname(this.secretsPath), { recursive: true });
5160
+ fs5.writeFileSync(this.secretsPath, JSON.stringify(secrets, null, 2) + "\n", { mode: 384 });
4658
5161
  this.ensureSecretsIgnored();
4659
5162
  }
4660
5163
  return healed;
@@ -4671,10 +5174,10 @@ var Store = class {
4671
5174
  /** Add one entry to .flowriki/.gitignore if it is not already there. */
4672
5175
  ensureIgnored(line, why) {
4673
5176
  const ignore = path4.join(this.root, ".flowriki", ".gitignore");
4674
- const existing = fs4.existsSync(ignore) ? fs4.readFileSync(ignore, "utf8") : "";
5177
+ const existing = fs5.existsSync(ignore) ? fs5.readFileSync(ignore, "utf8") : "";
4675
5178
  if (existing.split("\n").includes(line)) return;
4676
- fs4.mkdirSync(path4.dirname(ignore), { recursive: true });
4677
- fs4.writeFileSync(
5179
+ fs5.mkdirSync(path4.dirname(ignore), { recursive: true });
5180
+ fs5.writeFileSync(
4678
5181
  ignore,
4679
5182
  `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}# ${why}
4680
5183
  ${line}
@@ -4752,12 +5255,12 @@ async function executeFlow(opts) {
4752
5255
  stepsTotal: flow.steps.length
4753
5256
  });
4754
5257
  const runDir = storage && run ? runArtifactsDir(run.id, run.projectId) : void 0;
4755
- const byId = new Map(flow.steps.map((s) => [s.id, s]));
5258
+ const byId = new Map(flow.steps.map((s2) => [s2.id, s2]));
4756
5259
  const attempts = [];
4757
5260
  const perStep = /* @__PURE__ */ new Map();
4758
5261
  for (let attempt = 1; attempt <= repeat; attempt++) {
4759
5262
  if (opts.signal?.aborted) break;
4760
- const evidenceDir = runDir && repeat > 1 ? path22.join(runDir, `attempt-${String(attempt).padStart(3, "0")}`) : runDir;
5263
+ const evidenceDir = runDir && repeat > 1 ? path23.join(runDir, `attempt-${String(attempt).padStart(3, "0")}`) : runDir;
4761
5264
  const relativeTo = (file) => repeat > 1 ? `attempt-${String(attempt).padStart(3, "0")}/${file}` : file;
4762
5265
  const taskId = storage && run ? await storage.createTask(run.id, {
4763
5266
  caseName: repeat > 1 ? `${flow.name} \xB7 attempt ${attempt}` : flow.name,
@@ -4914,15 +5417,15 @@ var percentile = (sorted, p) => sorted.length === 0 ? 0 : sorted[Math.min(sorted
4914
5417
  function report(attempts, perStep) {
4915
5418
  const passed = attempts.filter((a) => a.status === "passed").length;
4916
5419
  const failed = attempts.length - passed;
4917
- const steps = [...perStep].map(([stepId, s]) => {
4918
- const sorted = [...s.durations].sort((a, b) => a - b);
5420
+ const steps = [...perStep].map(([stepId, s2]) => {
5421
+ const sorted = [...s2.durations].sort((a, b2) => a - b2);
4919
5422
  return {
4920
5423
  stepId,
4921
- name: s.name,
4922
- type: s.type,
4923
- ran: s.ran,
4924
- failed: s.failed,
4925
- reasons: [...s.reasons],
5424
+ name: s2.name,
5425
+ type: s2.type,
5426
+ ran: s2.ran,
5427
+ failed: s2.failed,
5428
+ reasons: [...s2.reasons],
4926
5429
  durationMs: { p50: percentile(sorted, 50), p99: percentile(sorted, 99), max: sorted[sorted.length - 1] ?? 0 }
4927
5430
  };
4928
5431
  });
@@ -4932,14 +5435,14 @@ function report(attempts, perStep) {
4932
5435
  failed,
4933
5436
  // How much a single green result is worth. 0 = every repetition agreed.
4934
5437
  flakeRate: attempts.length ? Math.min(passed, failed) / attempts.length * 2 : 0,
4935
- unstableSteps: steps.filter((s) => s.failed > 0).sort((a, b) => b.failed - a.failed),
5438
+ unstableSteps: steps.filter((s2) => s2.failed > 0).sort((a, b2) => b2.failed - a.failed),
4936
5439
  steps
4937
5440
  };
4938
5441
  }
4939
5442
  function sizeOf(dir, file) {
4940
5443
  if (!dir) return null;
4941
5444
  try {
4942
- return fs22.statSync(path22.join(dir, file)).size;
5445
+ return fs22.statSync(path23.join(dir, file)).size;
4943
5446
  } catch {
4944
5447
  return null;
4945
5448
  }
@@ -4992,7 +5495,7 @@ function redactPanel(state) {
4992
5495
  if (!state || typeof state !== "object") return state;
4993
5496
  const panel = state;
4994
5497
  if (!Array.isArray(panel.steps)) return state;
4995
- return { ...panel, steps: panel.steps.map((s) => redact(s)) };
5498
+ return { ...panel, steps: panel.steps.map((s2) => redact(s2)) };
4996
5499
  }
4997
5500
  var START_GRACE_MS = 3e3;
4998
5501
  var RecordingCoordinator = class {
@@ -5123,7 +5626,7 @@ var RecordingCoordinator = class {
5123
5626
  * resolves and attaches assertions — so this is an upsert, not an append. */
5124
5627
  upsert(step) {
5125
5628
  if (!this.draft) return;
5126
- const i = this.draft.steps.findIndex((s) => s.id === step.id);
5629
+ const i = this.draft.steps.findIndex((s2) => s2.id === step.id);
5127
5630
  if (i === -1) this.draft.steps.push(step);
5128
5631
  else this.draft.steps[i] = step;
5129
5632
  this.deps.emit({ type: "recording.step", step: redact(step) });
@@ -5290,32 +5793,32 @@ var DebugCoordinator = class {
5290
5793
  async command(command, args = {}) {
5291
5794
  if (!this.current) throw new Error("no debug session is running");
5292
5795
  if (!COMMANDS.has(command)) throw new UnknownDebugCommand(`unknown command: ${command}`);
5293
- const s = this.current.session;
5796
+ const s2 = this.current.session;
5294
5797
  switch (command) {
5295
5798
  case "play":
5296
- return s.play();
5799
+ return s2.play();
5297
5800
  case "pause":
5298
- return s.pause();
5801
+ return s2.pause();
5299
5802
  case "stepOver":
5300
- return s.stepOver();
5803
+ return s2.stepOver();
5301
5804
  case "retry":
5302
- return s.retry();
5805
+ return s2.retry();
5303
5806
  case "skip":
5304
- return s.skip();
5807
+ return s2.skip();
5305
5808
  case "runTo":
5306
- return s.runTo(String(args.stepId));
5809
+ return s2.runTo(String(args.stepId));
5307
5810
  case "toggleBreakpoint":
5308
- return s.toggleBreakpoint(String(args.stepId));
5811
+ return s2.toggleBreakpoint(String(args.stepId));
5309
5812
  case "deleteStep":
5310
- return s.deleteStep(String(args.stepId));
5813
+ return s2.deleteStep(String(args.stepId));
5311
5814
  case "updateStep":
5312
- return s.updateStep(String(args.stepId), args.patch ?? {});
5815
+ return s2.updateStep(String(args.stepId), args.patch ?? {});
5313
5816
  case "record":
5314
- return s.setRecording(Boolean(args.on));
5817
+ return s2.setRecording(Boolean(args.on));
5315
5818
  case "moveStep":
5316
- return s.moveStep(String(args.stepId), Number(args.toIndex));
5819
+ return s2.moveStep(String(args.stepId), Number(args.toIndex));
5317
5820
  case "replay":
5318
- return s.replay();
5821
+ return s2.replay();
5319
5822
  }
5320
5823
  }
5321
5824
  /** Shared by every door — the local route, the hosted command, and the
@@ -5356,7 +5859,7 @@ async function verifyFlowSteps(deps, opts) {
5356
5859
  const { store } = deps;
5357
5860
  const flow = store.getFlow(opts.flowId);
5358
5861
  if (!flow) throw new FlowNotFound("flow not found");
5359
- const steps = (opts.steps ?? flow.steps).map((s) => RecordedStepSchema.parse(s));
5862
+ const steps = (opts.steps ?? flow.steps).map((s2) => RecordedStepSchema.parse(s2));
5360
5863
  if (!opts.verifyIds.length) return { results: [], error: null, failedAtStepId: null, ok: true };
5361
5864
  const baseUrl = store.baseUrlFor(flow);
5362
5865
  const claim = deps.lease?.acquire("run");
@@ -5717,16 +6220,16 @@ var EventJournal = class {
5717
6220
  return this.state ? this.state.size() : this.events.length;
5718
6221
  }
5719
6222
  };
5720
- var { DatabaseSync } = createRequire2(import.meta.url)("node:sqlite");
6223
+ var { DatabaseSync } = createRequire3(import.meta.url)("node:sqlite");
5721
6224
  var AgentState = class {
5722
6225
  constructor(file, deviceId, commandLimit = 1e5) {
5723
6226
  this.commandLimit = commandLimit;
5724
6227
  if (!file || file === ":memory:") throw new Error("a persistent agent state file is required");
5725
6228
  if (!Number.isInteger(commandLimit) || commandLimit < 1) throw new Error("invalid command capacity");
5726
- fs5.mkdirSync(path5.dirname(file), { recursive: true, mode: 448 });
5727
- const fd = fs5.openSync(file, "a", 384);
5728
- fs5.closeSync(fd);
5729
- fs5.chmodSync(file, 384);
6229
+ fs6.mkdirSync(path5.dirname(file), { recursive: true, mode: 448 });
6230
+ const fd = fs6.openSync(file, "a", 384);
6231
+ fs6.closeSync(fd);
6232
+ fs6.chmodSync(file, 384);
5730
6233
  this.db = new DatabaseSync(file);
5731
6234
  try {
5732
6235
  this.db.exec("PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;");
@@ -5770,7 +6273,7 @@ var AgentState = class {
5770
6273
  static fingerprint(command) {
5771
6274
  const canonical = (value) => {
5772
6275
  if (Array.isArray(value)) return value.map(canonical);
5773
- if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, v]) => [key, canonical(v)]));
6276
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b2]) => a.localeCompare(b2)).map(([key, v]) => [key, canonical(v)]));
5774
6277
  return value;
5775
6278
  };
5776
6279
  const { expiresAt: _expiry, issuedAt: _issued, ...work } = command;
@@ -5917,15 +6420,11 @@ var Agent = class {
5917
6420
  );
5918
6421
  }
5919
6422
  const delivered = command.args["flow"];
5920
- const produces = command.name.startsWith("record.") || command.name === "run.cancel";
5921
- const project = this.opts.projects.find((p) => p.id === command.projectId) ?? (delivered || produces ? { id: command.projectId, name: String(delivered?.name ?? command.projectId), root: "" } : void 0);
5922
- if (!project) {
5923
- return this.refuse(
5924
- command.id,
5925
- "unknown_project",
5926
- "this device has no project mapped under that name"
5927
- );
5928
- }
6423
+ const project = this.opts.projects.find((p) => p.id === command.projectId) ?? {
6424
+ id: command.projectId,
6425
+ name: String(delivered?.name ?? command.projectId),
6426
+ root: ""
6427
+ };
5929
6428
  const allowed = this.opts.authorise?.(command) ?? { ok: true };
5930
6429
  if (!allowed.ok) return this.refuse(command.id, allowed.reason);
5931
6430
  if (this.closed) return this.refuse(command.id, "storage_unavailable", "the agent is closed");
@@ -6218,11 +6717,16 @@ var scratchRoots = /* @__PURE__ */ new Map();
6218
6717
  function scratchRoot(projectId) {
6219
6718
  let root = scratchRoots.get(projectId);
6220
6719
  if (!root) {
6221
- root = fs23.mkdtempSync(path23.join(os2.tmpdir(), `flowriki-record-${projectId.replace(/[^\w-]/g, "")}-`));
6720
+ root = fs23.mkdtempSync(path24.join(os2.tmpdir(), `flowriki-record-${projectId.replace(/[^\w-]/g, "")}-`));
6222
6721
  scratchRoots.set(projectId, root);
6223
6722
  }
6224
6723
  return root;
6225
6724
  }
6725
+ var NO_CHECKOUT = {
6726
+ ok: false,
6727
+ reason: "failed",
6728
+ detail: "this machine has no checkout of that project \u2014 clone it and start the agent with that folder"
6729
+ };
6226
6730
  function buildHandlers(deps) {
6227
6731
  const { storeFor, storage = null, onActivity, lease, recorder } = deps;
6228
6732
  const debugging = deps.debugger;
@@ -6230,6 +6734,7 @@ function buildHandlers(deps) {
6230
6734
  const cancels = /* @__PURE__ */ new Map();
6231
6735
  return {
6232
6736
  "project.list": async (command, project) => {
6737
+ if (!project.root) return NO_CHECKOUT;
6233
6738
  const store = storeFor(project.root);
6234
6739
  const full = command.args["documents"] === true;
6235
6740
  return {
@@ -6254,6 +6759,7 @@ function buildHandlers(deps) {
6254
6759
  };
6255
6760
  },
6256
6761
  "flow.get": async (command, project) => {
6762
+ if (!project.root) return NO_CHECKOUT;
6257
6763
  const store = storeFor(project.root);
6258
6764
  const flow = store.getFlow(String(command.args["flowId"] ?? ""));
6259
6765
  if (!flow) return { ok: false, reason: "failed", detail: "no such flow in this project" };
@@ -6265,7 +6771,7 @@ function buildHandlers(deps) {
6265
6771
  // has to be able to say "this flow cannot run until you set the
6266
6772
  // password" without the password ever leaving this machine.
6267
6773
  unresolvedSecrets: store.unresolvedSecrets(flow),
6268
- boundSecrets: flow.steps.filter((s) => s.secret && s.secretRef).map((s) => s.secretRef).filter((ref) => store.readSecrets()[ref] !== void 0)
6774
+ boundSecrets: flow.steps.filter((s2) => s2.secret && s2.secretRef).map((s2) => s2.secretRef).filter((ref) => store.readSecrets()[ref] !== void 0)
6269
6775
  }
6270
6776
  };
6271
6777
  },
@@ -6288,6 +6794,7 @@ function buildHandlers(deps) {
6288
6794
  return { ok: true, result: { ...await storage.runStats(Number(command.args["days"] ?? 7)) } };
6289
6795
  },
6290
6796
  "flow.save": async (command, project) => {
6797
+ if (!project.root) return NO_CHECKOUT;
6291
6798
  if (command.flowRevision === void 0) {
6292
6799
  return { ok: false, reason: "stale_revision", detail: "supply the revision being edited; new flows require create:true and revision 0" };
6293
6800
  }
@@ -6317,6 +6824,7 @@ function buildHandlers(deps) {
6317
6824
  // keyboard. The file stays in the machine's own git repository — the
6318
6825
  // service never gets a copy to diverge from.
6319
6826
  "flow.update": async (command, project) => {
6827
+ if (!project.root) return NO_CHECKOUT;
6320
6828
  if (command.flowRevision === void 0) {
6321
6829
  return { ok: false, reason: "stale_revision", detail: "this edit needs the revision it was opened from \u2014 reload the flow" };
6322
6830
  }
@@ -6354,6 +6862,7 @@ function buildHandlers(deps) {
6354
6862
  // rather than the command, the service persists no command at all, and
6355
6863
  // neither the result below nor any event carries the value back.
6356
6864
  "secret.set": async (command, project) => {
6865
+ if (!project.root) return NO_CHECKOUT;
6357
6866
  const store = storeFor(project.root);
6358
6867
  const ref = String(command.args["ref"] ?? "");
6359
6868
  const value = command.args["value"];
@@ -6373,6 +6882,7 @@ function buildHandlers(deps) {
6373
6882
  // from the CLI or the local app — which, for anybody who had only ever
6374
6883
  // used the hosted service, was none.
6375
6884
  "feature.create": async (command, project) => {
6885
+ if (!project.root) return NO_CHECKOUT;
6376
6886
  const store = storeFor(project.root);
6377
6887
  const name = String(command.args["name"] ?? "").trim();
6378
6888
  if (!name) return { ok: false, reason: "failed", detail: "a feature name is required" };
@@ -6385,6 +6895,7 @@ function buildHandlers(deps) {
6385
6895
  }
6386
6896
  },
6387
6897
  "flow.delete": async (command, project) => {
6898
+ if (!project.root) return NO_CHECKOUT;
6388
6899
  const store = storeFor(project.root);
6389
6900
  const flowId = String(command.args["flowId"] ?? "");
6390
6901
  if (!store.getFlow(flowId)) return { ok: true, result: { deleted: false } };
@@ -6395,6 +6906,7 @@ function buildHandlers(deps) {
6395
6906
  // command because the whole point is to check an edit nobody has saved —
6396
6907
  // saving is gated on this passing.
6397
6908
  "flow.verify": async (command, project, emit) => {
6909
+ if (!project.root) return NO_CHECKOUT;
6398
6910
  if (!deps.verify) {
6399
6911
  return { ok: false, reason: "failed", detail: "this agent cannot verify steps \u2014 no browser is configured" };
6400
6912
  }
@@ -6423,6 +6935,7 @@ function buildHandlers(deps) {
6423
6935
  // The browser opens on this machine and the panel lives inside that window,
6424
6936
  // so whoever is at the keyboard sees the same state the dashboard does.
6425
6937
  "debug.start": async (command, project) => {
6938
+ if (!project.root) return NO_CHECKOUT;
6426
6939
  if (!debugging) {
6427
6940
  return { ok: false, reason: "failed", detail: "this agent cannot debug \u2014 no browser is configured" };
6428
6941
  }
@@ -6471,6 +6984,7 @@ function buildHandlers(deps) {
6471
6984
  }
6472
6985
  },
6473
6986
  "config.save": async (command, project) => {
6987
+ if (!project.root) return NO_CHECKOUT;
6474
6988
  const store = storeFor(project.root);
6475
6989
  try {
6476
6990
  store.writeConfig(command.args["config"]);
@@ -6483,6 +6997,7 @@ function buildHandlers(deps) {
6483
6997
  // committed, and the answer is what lets the service tell "not set up yet"
6484
6998
  // apart from "set up, but you are in the wrong clone".
6485
6999
  "project.status": async (_command, project) => {
7000
+ if (!project.root) return NO_CHECKOUT;
6486
7001
  if (!deps.binding) {
6487
7002
  return { ok: false, reason: "failed", detail: "this agent cannot inspect a repository" };
6488
7003
  }
@@ -6491,6 +7006,7 @@ function buildHandlers(deps) {
6491
7006
  // Write the marker, commit it, push it. Everything here happens with the
6492
7007
  // person's own git credentials, on their own machine.
6493
7008
  "project.claim": async (command, project) => {
7009
+ if (!project.root) return NO_CHECKOUT;
6494
7010
  if (!deps.binding) {
6495
7011
  return { ok: false, reason: "failed", detail: "this agent cannot write to a repository" };
6496
7012
  }
@@ -6555,7 +7071,10 @@ function buildHandlers(deps) {
6555
7071
  runId = String(event.runId ?? "");
6556
7072
  if (runId) cancels.set(runId, cancels.get(command.id));
6557
7073
  }
6558
- emit(`run.${event.type}`, event);
7074
+ emit(`run.${event.type}`, {
7075
+ ...event,
7076
+ projectId: project.id
7077
+ });
6559
7078
  }
6560
7079
  });
6561
7080
  return {
@@ -6688,7 +7207,7 @@ function buildHandlers(deps) {
6688
7207
  const artifact = run.artifacts.find((a) => a.id === artifactId);
6689
7208
  if (!artifact) return { ok: false, reason: "failed", detail: "no such artifact on that run" };
6690
7209
  const dir = run.artifactsDir ?? runArtifactsDir(runId, run.projectId);
6691
- const file = path23.join(dir, artifact.relPath);
7210
+ const file = path24.join(dir, artifact.relPath);
6692
7211
  if (!isInside(dir, file) || !fs23.existsSync(file) || !isInside(fs23.realpathSync(dir), fs23.realpathSync(file))) {
6693
7212
  return { ok: false, reason: "failed", detail: "the artifact file is not on this machine" };
6694
7213
  }
@@ -6847,15 +7366,15 @@ function createAgentRuntime(options) {
6847
7366
  }
6848
7367
 
6849
7368
  // src/engine.ts
6850
- import fs9 from "fs";
7369
+ import fs10 from "fs";
6851
7370
  import path7 from "path";
6852
7371
 
6853
7372
  // src/credentials.ts
6854
7373
  import crypto3 from "crypto";
6855
- import fs7 from "fs";
7374
+ import fs8 from "fs";
6856
7375
 
6857
7376
  // src/paths.ts
6858
- import fs6 from "fs";
7377
+ import fs7 from "fs";
6859
7378
  import os3 from "os";
6860
7379
  import path6 from "path";
6861
7380
  function dataDir(env = process.env) {
@@ -6873,10 +7392,10 @@ function dataDir(env = process.env) {
6873
7392
  }
6874
7393
  function ensureDataDir(env = process.env) {
6875
7394
  const dir = dataDir(env);
6876
- fs6.mkdirSync(dir, { recursive: true, mode: 448 });
7395
+ fs7.mkdirSync(dir, { recursive: true, mode: 448 });
6877
7396
  if (process.platform !== "win32") {
6878
7397
  try {
6879
- fs6.chmodSync(dir, 448);
7398
+ fs7.chmodSync(dir, 448);
6880
7399
  } catch {
6881
7400
  }
6882
7401
  }
@@ -6889,7 +7408,7 @@ var ledgerPath = (deviceId, env = process.env) => path6.join(dataDir(env), `ledg
6889
7408
  // src/credentials.ts
6890
7409
  function readFile(env = process.env) {
6891
7410
  try {
6892
- const raw = JSON.parse(fs7.readFileSync(credentialsPath(env), "utf8"));
7411
+ const raw = JSON.parse(fs8.readFileSync(credentialsPath(env), "utf8"));
6893
7412
  if (Array.isArray(raw.connections)) return { version: 3, connections: raw.connections };
6894
7413
  const older = Array.isArray(raw.devices) ? raw.devices : raw.deviceId ? [raw] : [];
6895
7414
  const connections = older.filter((d) => d.deviceId && d.secret).map((d) => ({
@@ -6919,8 +7438,8 @@ function saveConnection(connection, env = process.env) {
6919
7438
  connections: [...readFile(env).connections.filter((c) => c.service !== connection.service), connection]
6920
7439
  };
6921
7440
  const temporary = `${file}.${process.pid}.tmp`;
6922
- fs7.writeFileSync(temporary, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
6923
- fs7.renameSync(temporary, file);
7441
+ fs8.writeFileSync(temporary, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
7442
+ fs8.renameSync(temporary, file);
6924
7443
  }
6925
7444
  function forgetConnection(service, env = process.env) {
6926
7445
  const before = readFile(env).connections;
@@ -6929,8 +7448,8 @@ function forgetConnection(service, env = process.env) {
6929
7448
  ensureDataDir(env);
6930
7449
  const file = credentialsPath(env);
6931
7450
  const temporary = `${file}.${process.pid}.tmp`;
6932
- fs7.writeFileSync(temporary, JSON.stringify({ version: 3, connections: after }, null, 2) + "\n", { mode: 384 });
6933
- fs7.renameSync(temporary, file);
7451
+ fs8.writeFileSync(temporary, JSON.stringify({ version: 3, connections: after }, null, 2) + "\n", { mode: 384 });
7452
+ fs8.renameSync(temporary, file);
6934
7453
  return true;
6935
7454
  }
6936
7455
  var fingerprint = (secret) => crypto3.createHash("sha256").update(secret).digest("hex").slice(0, 12);
@@ -6957,7 +7476,7 @@ function resolveService(explicit, env = process.env) {
6957
7476
  }
6958
7477
 
6959
7478
  // src/lock.ts
6960
- import fs8 from "fs";
7479
+ import fs9 from "fs";
6961
7480
  import os4 from "os";
6962
7481
  function takeLock(env = process.env) {
6963
7482
  ensureDataDir(env);
@@ -6965,20 +7484,20 @@ function takeLock(env = process.env) {
6965
7484
  const mine = { pid: process.pid, since: (/* @__PURE__ */ new Date()).toISOString(), host: os4.hostname() };
6966
7485
  for (let attempt = 0; attempt < 2; attempt += 1) {
6967
7486
  try {
6968
- fs8.writeFileSync(file, JSON.stringify(mine), { flag: "wx", mode: 384 });
7487
+ fs9.writeFileSync(file, JSON.stringify(mine), { flag: "wx", mode: 384 });
6969
7488
  return { ok: true, lock: { release: () => releaseLock(file, mine.pid) } };
6970
7489
  } catch {
6971
7490
  const held = readLock(file);
6972
7491
  if (!held) {
6973
7492
  try {
6974
- fs8.rmSync(file, { force: true });
7493
+ fs9.rmSync(file, { force: true });
6975
7494
  } catch {
6976
7495
  }
6977
7496
  continue;
6978
7497
  }
6979
7498
  if (held.host === mine.host && !alive(held.pid)) {
6980
7499
  try {
6981
- fs8.rmSync(file, { force: true });
7500
+ fs9.rmSync(file, { force: true });
6982
7501
  } catch {
6983
7502
  }
6984
7503
  continue;
@@ -6990,7 +7509,7 @@ function takeLock(env = process.env) {
6990
7509
  }
6991
7510
  function readLock(file) {
6992
7511
  try {
6993
- const held = JSON.parse(fs8.readFileSync(file, "utf8"));
7512
+ const held = JSON.parse(fs9.readFileSync(file, "utf8"));
6994
7513
  return typeof held.pid === "number" ? { pid: held.pid, since: held.since ?? "", host: held.host ?? "" } : null;
6995
7514
  } catch {
6996
7515
  return null;
@@ -7009,7 +7528,7 @@ function releaseLock(file, pid) {
7009
7528
  const held = readLock(file);
7010
7529
  if (held?.pid === pid) {
7011
7530
  try {
7012
- fs8.rmSync(file, { force: true });
7531
+ fs9.rmSync(file, { force: true });
7013
7532
  } catch {
7014
7533
  }
7015
7534
  }
@@ -7038,7 +7557,7 @@ function openBrowser(url, env = process.env) {
7038
7557
  }
7039
7558
 
7040
7559
  // src/engine.ts
7041
- var AGENT_VERSION = "0.1.0-beta.6";
7560
+ var AGENT_VERSION = "0.1.0-beta.7";
7042
7561
  async function connect(options = {}) {
7043
7562
  let service;
7044
7563
  try {
@@ -7122,7 +7641,7 @@ function resolveCheckouts(dirs) {
7122
7641
  const skipped = [];
7123
7642
  for (const dir of dirs) {
7124
7643
  const root = path7.resolve(dir);
7125
- if (!fs9.existsSync(root)) {
7644
+ if (!fs10.existsSync(root)) {
7126
7645
  skipped.push({ root, why: "no such folder" });
7127
7646
  continue;
7128
7647
  }
@@ -7186,11 +7705,21 @@ async function start(options = {}) {
7186
7705
  if (projects.length !== (connection.checkouts ?? []).length || options.projects?.length) {
7187
7706
  saveConnection({ ...connection, checkouts: projects.map((p) => p.root) });
7188
7707
  }
7708
+ const storage = new SqliteStorage();
7709
+ try {
7710
+ await storage.init();
7711
+ } catch (error) {
7712
+ options.onProgress?.({
7713
+ kind: "warning",
7714
+ message: `run history is unavailable on this machine \u2014 ${error.message}`
7715
+ });
7716
+ }
7189
7717
  const agent = createAgentRuntime({
7190
7718
  deviceId: connection.deviceId,
7191
7719
  agentVersion: AGENT_VERSION,
7192
7720
  stateFile: ledgerPath(connection.deviceId),
7193
7721
  projects,
7722
+ storage,
7194
7723
  onEvent: (event) => options.onProgress?.({ kind: event.name, message: "", detail: event.data })
7195
7724
  });
7196
7725
  const link = new AgentLink({
@@ -7221,7 +7750,7 @@ async function prepareBrowsers() {
7221
7750
  }
7222
7751
  let cli;
7223
7752
  try {
7224
- cli = createRequire3(import.meta.url).resolve("playwright/cli.js");
7753
+ cli = createRequire4(import.meta.url).resolve("playwright/cli.js");
7225
7754
  } catch {
7226
7755
  return {
7227
7756
  ok: false,
@@ -7259,7 +7788,7 @@ function status(serviceArg) {
7259
7788
  ...connection ? { deviceId: connection.deviceId, machineName: connection.machineName, connectedAt: connection.connectedAt } : {},
7260
7789
  checkouts: [
7261
7790
  ...projects.map((p) => ({ root: p.root, project: p.name })),
7262
- ...skipped.map((s) => ({ root: s.root, project: null, why: s.why }))
7791
+ ...skipped.map((s2) => ({ root: s2.root, project: null, why: s2.why }))
7263
7792
  ],
7264
7793
  running
7265
7794
  };
@@ -7307,7 +7836,7 @@ async function doctor(serviceArg) {
7307
7836
  function inspectPlaywright() {
7308
7837
  try {
7309
7838
  const browsersPath = process.env["PLAYWRIGHT_BROWSERS_PATH"] ?? (process.platform === "win32" ? path7.join(os5.homedir(), "AppData", "Local", "ms-playwright") : process.platform === "darwin" ? path7.join(os5.homedir(), "Library", "Caches", "ms-playwright") : path7.join(os5.homedir(), ".cache", "ms-playwright"));
7310
- const chromiumInstalled = fs9.existsSync(browsersPath) ? fs9.readdirSync(browsersPath).some((d) => d.startsWith("chromium")) : false;
7839
+ const chromiumInstalled = fs10.existsSync(browsersPath) ? fs10.readdirSync(browsersPath).some((d) => d.startsWith("chromium")) : false;
7311
7840
  return { version: null, browsersPath, chromiumInstalled };
7312
7841
  } catch {
7313
7842
  return { version: null, browsersPath: null, chromiumInstalled: null };