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

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
@@ -5335,11 +5838,25 @@ var DebugCoordinator = class {
5335
5838
  const steps = await session.finish();
5336
5839
  const flow = this.deps.store.getFlow(flowId);
5337
5840
  if (!flow) return { ok: false, error: "flow not found" };
5338
- this.deps.store.extractSecrets(flow.slug, steps);
5841
+ if (this.deps.commit.to === "caller") {
5842
+ const { scratch } = this.deps.commit;
5843
+ const before = scratch.readSecrets();
5844
+ scratch.extractSecrets(flow.slug, steps);
5845
+ const after = scratch.readSecrets();
5846
+ const captured = {};
5847
+ for (const [ref, value] of Object.entries(after)) {
5848
+ if (before[ref] !== value) captured[ref] = value;
5849
+ }
5850
+ const updated2 = { ...flow, steps, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
5851
+ this.deps.emit({ type: "debug.ended", flowId, committed: true });
5852
+ return { ok: true, committed: true, flow: updated2, secrets: captured };
5853
+ }
5854
+ const { store } = this.deps.commit;
5855
+ store.extractSecrets(flow.slug, steps);
5339
5856
  const updated = { ...flow, steps, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
5340
- this.deps.store.saveFlow(updated);
5857
+ store.saveFlow(updated);
5341
5858
  this.deps.emit({ type: "debug.ended", flowId, committed: true });
5342
- return { ok: true, committed: true, flow: { ...updated, path: this.deps.store.relPath(updated) } };
5859
+ return { ok: true, committed: true, flow: { ...updated, path: store.relPath(updated) } };
5343
5860
  } finally {
5344
5861
  this.deps.lease?.release("debug");
5345
5862
  }
@@ -5356,7 +5873,7 @@ async function verifyFlowSteps(deps, opts) {
5356
5873
  const { store } = deps;
5357
5874
  const flow = store.getFlow(opts.flowId);
5358
5875
  if (!flow) throw new FlowNotFound("flow not found");
5359
- const steps = (opts.steps ?? flow.steps).map((s) => RecordedStepSchema.parse(s));
5876
+ const steps = (opts.steps ?? flow.steps).map((s2) => RecordedStepSchema.parse(s2));
5360
5877
  if (!opts.verifyIds.length) return { results: [], error: null, failedAtStepId: null, ok: true };
5361
5878
  const baseUrl = store.baseUrlFor(flow);
5362
5879
  const claim = deps.lease?.acquire("run");
@@ -5717,16 +6234,16 @@ var EventJournal = class {
5717
6234
  return this.state ? this.state.size() : this.events.length;
5718
6235
  }
5719
6236
  };
5720
- var { DatabaseSync } = createRequire2(import.meta.url)("node:sqlite");
6237
+ var { DatabaseSync } = createRequire3(import.meta.url)("node:sqlite");
5721
6238
  var AgentState = class {
5722
6239
  constructor(file, deviceId, commandLimit = 1e5) {
5723
6240
  this.commandLimit = commandLimit;
5724
6241
  if (!file || file === ":memory:") throw new Error("a persistent agent state file is required");
5725
6242
  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);
6243
+ fs6.mkdirSync(path5.dirname(file), { recursive: true, mode: 448 });
6244
+ const fd = fs6.openSync(file, "a", 384);
6245
+ fs6.closeSync(fd);
6246
+ fs6.chmodSync(file, 384);
5730
6247
  this.db = new DatabaseSync(file);
5731
6248
  try {
5732
6249
  this.db.exec("PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;");
@@ -5770,7 +6287,7 @@ var AgentState = class {
5770
6287
  static fingerprint(command) {
5771
6288
  const canonical = (value) => {
5772
6289
  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)]));
6290
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b2]) => a.localeCompare(b2)).map(([key, v]) => [key, canonical(v)]));
5774
6291
  return value;
5775
6292
  };
5776
6293
  const { expiresAt: _expiry, issuedAt: _issued, ...work } = command;
@@ -5917,15 +6434,11 @@ var Agent = class {
5917
6434
  );
5918
6435
  }
5919
6436
  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
- }
6437
+ const project = this.opts.projects.find((p) => p.id === command.projectId) ?? {
6438
+ id: command.projectId,
6439
+ name: String(delivered?.name ?? command.projectId),
6440
+ root: ""
6441
+ };
5929
6442
  const allowed = this.opts.authorise?.(command) ?? { ok: true };
5930
6443
  if (!allowed.ok) return this.refuse(command.id, allowed.reason);
5931
6444
  if (this.closed) return this.refuse(command.id, "storage_unavailable", "the agent is closed");
@@ -6218,11 +6731,16 @@ var scratchRoots = /* @__PURE__ */ new Map();
6218
6731
  function scratchRoot(projectId) {
6219
6732
  let root = scratchRoots.get(projectId);
6220
6733
  if (!root) {
6221
- root = fs23.mkdtempSync(path23.join(os2.tmpdir(), `flowriki-record-${projectId.replace(/[^\w-]/g, "")}-`));
6734
+ root = fs23.mkdtempSync(path24.join(os2.tmpdir(), `flowriki-record-${projectId.replace(/[^\w-]/g, "")}-`));
6222
6735
  scratchRoots.set(projectId, root);
6223
6736
  }
6224
6737
  return root;
6225
6738
  }
6739
+ var NO_CHECKOUT = {
6740
+ ok: false,
6741
+ reason: "failed",
6742
+ detail: "this machine has no checkout of that project \u2014 clone it and start the agent with that folder"
6743
+ };
6226
6744
  function buildHandlers(deps) {
6227
6745
  const { storeFor, storage = null, onActivity, lease, recorder } = deps;
6228
6746
  const debugging = deps.debugger;
@@ -6230,6 +6748,7 @@ function buildHandlers(deps) {
6230
6748
  const cancels = /* @__PURE__ */ new Map();
6231
6749
  return {
6232
6750
  "project.list": async (command, project) => {
6751
+ if (!project.root) return NO_CHECKOUT;
6233
6752
  const store = storeFor(project.root);
6234
6753
  const full = command.args["documents"] === true;
6235
6754
  return {
@@ -6254,6 +6773,7 @@ function buildHandlers(deps) {
6254
6773
  };
6255
6774
  },
6256
6775
  "flow.get": async (command, project) => {
6776
+ if (!project.root) return NO_CHECKOUT;
6257
6777
  const store = storeFor(project.root);
6258
6778
  const flow = store.getFlow(String(command.args["flowId"] ?? ""));
6259
6779
  if (!flow) return { ok: false, reason: "failed", detail: "no such flow in this project" };
@@ -6265,7 +6785,7 @@ function buildHandlers(deps) {
6265
6785
  // has to be able to say "this flow cannot run until you set the
6266
6786
  // password" without the password ever leaving this machine.
6267
6787
  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)
6788
+ boundSecrets: flow.steps.filter((s2) => s2.secret && s2.secretRef).map((s2) => s2.secretRef).filter((ref) => store.readSecrets()[ref] !== void 0)
6269
6789
  }
6270
6790
  };
6271
6791
  },
@@ -6288,6 +6808,7 @@ function buildHandlers(deps) {
6288
6808
  return { ok: true, result: { ...await storage.runStats(Number(command.args["days"] ?? 7)) } };
6289
6809
  },
6290
6810
  "flow.save": async (command, project) => {
6811
+ if (!project.root) return NO_CHECKOUT;
6291
6812
  if (command.flowRevision === void 0) {
6292
6813
  return { ok: false, reason: "stale_revision", detail: "supply the revision being edited; new flows require create:true and revision 0" };
6293
6814
  }
@@ -6317,6 +6838,7 @@ function buildHandlers(deps) {
6317
6838
  // keyboard. The file stays in the machine's own git repository — the
6318
6839
  // service never gets a copy to diverge from.
6319
6840
  "flow.update": async (command, project) => {
6841
+ if (!project.root) return NO_CHECKOUT;
6320
6842
  if (command.flowRevision === void 0) {
6321
6843
  return { ok: false, reason: "stale_revision", detail: "this edit needs the revision it was opened from \u2014 reload the flow" };
6322
6844
  }
@@ -6354,6 +6876,7 @@ function buildHandlers(deps) {
6354
6876
  // rather than the command, the service persists no command at all, and
6355
6877
  // neither the result below nor any event carries the value back.
6356
6878
  "secret.set": async (command, project) => {
6879
+ if (!project.root) return NO_CHECKOUT;
6357
6880
  const store = storeFor(project.root);
6358
6881
  const ref = String(command.args["ref"] ?? "");
6359
6882
  const value = command.args["value"];
@@ -6373,6 +6896,7 @@ function buildHandlers(deps) {
6373
6896
  // from the CLI or the local app — which, for anybody who had only ever
6374
6897
  // used the hosted service, was none.
6375
6898
  "feature.create": async (command, project) => {
6899
+ if (!project.root) return NO_CHECKOUT;
6376
6900
  const store = storeFor(project.root);
6377
6901
  const name = String(command.args["name"] ?? "").trim();
6378
6902
  if (!name) return { ok: false, reason: "failed", detail: "a feature name is required" };
@@ -6385,6 +6909,7 @@ function buildHandlers(deps) {
6385
6909
  }
6386
6910
  },
6387
6911
  "flow.delete": async (command, project) => {
6912
+ if (!project.root) return NO_CHECKOUT;
6388
6913
  const store = storeFor(project.root);
6389
6914
  const flowId = String(command.args["flowId"] ?? "");
6390
6915
  if (!store.getFlow(flowId)) return { ok: true, result: { deleted: false } };
@@ -6395,6 +6920,7 @@ function buildHandlers(deps) {
6395
6920
  // command because the whole point is to check an edit nobody has saved —
6396
6921
  // saving is gated on this passing.
6397
6922
  "flow.verify": async (command, project, emit) => {
6923
+ if (!project.root) return NO_CHECKOUT;
6398
6924
  if (!deps.verify) {
6399
6925
  return { ok: false, reason: "failed", detail: "this agent cannot verify steps \u2014 no browser is configured" };
6400
6926
  }
@@ -6423,13 +6949,23 @@ function buildHandlers(deps) {
6423
6949
  // The browser opens on this machine and the panel lives inside that window,
6424
6950
  // so whoever is at the keyboard sees the same state the dashboard does.
6425
6951
  "debug.start": async (command, project) => {
6952
+ const carried = command.args["flow"];
6953
+ if (!carried && !project.root) return NO_CHECKOUT;
6426
6954
  if (!debugging) {
6427
6955
  return { ok: false, reason: "failed", detail: "this agent cannot debug \u2014 no browser is configured" };
6428
6956
  }
6429
6957
  try {
6430
6958
  const started = await debugging.start(String(command.args["flowId"] ?? ""), {
6431
6959
  ...command.args["breakpoints"] ? { breakpoints: command.args["breakpoints"] } : {},
6432
- ...command.args["watch"] === void 0 ? {} : { watch: Boolean(command.args["watch"]) }
6960
+ ...command.args["watch"] === void 0 ? {} : { watch: Boolean(command.args["watch"]) },
6961
+ ...carried ? {
6962
+ delivered: {
6963
+ flow: carried,
6964
+ secrets: command.args["secrets"] ?? {},
6965
+ config: command.args["config"] ?? {},
6966
+ scratchRoot: scratchRoot(project.id)
6967
+ }
6968
+ } : {}
6433
6969
  });
6434
6970
  onActivity?.({ kind: "debugging", projectId: project.id, detail: started.flowId });
6435
6971
  return { ok: true, result: started };
@@ -6471,6 +7007,7 @@ function buildHandlers(deps) {
6471
7007
  }
6472
7008
  },
6473
7009
  "config.save": async (command, project) => {
7010
+ if (!project.root) return NO_CHECKOUT;
6474
7011
  const store = storeFor(project.root);
6475
7012
  try {
6476
7013
  store.writeConfig(command.args["config"]);
@@ -6483,6 +7020,7 @@ function buildHandlers(deps) {
6483
7020
  // committed, and the answer is what lets the service tell "not set up yet"
6484
7021
  // apart from "set up, but you are in the wrong clone".
6485
7022
  "project.status": async (_command, project) => {
7023
+ if (!project.root) return NO_CHECKOUT;
6486
7024
  if (!deps.binding) {
6487
7025
  return { ok: false, reason: "failed", detail: "this agent cannot inspect a repository" };
6488
7026
  }
@@ -6491,6 +7029,7 @@ function buildHandlers(deps) {
6491
7029
  // Write the marker, commit it, push it. Everything here happens with the
6492
7030
  // person's own git credentials, on their own machine.
6493
7031
  "project.claim": async (command, project) => {
7032
+ if (!project.root) return NO_CHECKOUT;
6494
7033
  if (!deps.binding) {
6495
7034
  return { ok: false, reason: "failed", detail: "this agent cannot write to a repository" };
6496
7035
  }
@@ -6555,7 +7094,10 @@ function buildHandlers(deps) {
6555
7094
  runId = String(event.runId ?? "");
6556
7095
  if (runId) cancels.set(runId, cancels.get(command.id));
6557
7096
  }
6558
- emit(`run.${event.type}`, event);
7097
+ emit(`run.${event.type}`, {
7098
+ ...event,
7099
+ projectId: project.id
7100
+ });
6559
7101
  }
6560
7102
  });
6561
7103
  return {
@@ -6688,7 +7230,7 @@ function buildHandlers(deps) {
6688
7230
  const artifact = run.artifacts.find((a) => a.id === artifactId);
6689
7231
  if (!artifact) return { ok: false, reason: "failed", detail: "no such artifact on that run" };
6690
7232
  const dir = run.artifactsDir ?? runArtifactsDir(runId, run.projectId);
6691
- const file = path23.join(dir, artifact.relPath);
7233
+ const file = path24.join(dir, artifact.relPath);
6692
7234
  if (!isInside(dir, file) || !fs23.existsSync(file) || !isInside(fs23.realpathSync(dir), fs23.realpathSync(file))) {
6693
7235
  return { ok: false, reason: "failed", detail: "the artifact file is not on this machine" };
6694
7236
  }
@@ -6734,8 +7276,10 @@ function createAgentRuntime(options) {
6734
7276
  const debuggerFor = (projectRoot) => {
6735
7277
  let existing = debuggers.get(projectRoot);
6736
7278
  if (!existing) {
7279
+ const store = new Store(projectRoot);
6737
7280
  existing = new DebugCoordinator({
6738
- store: new Store(projectRoot),
7281
+ store,
7282
+ commit: { to: "disk", store },
6739
7283
  emit: (event) => {
6740
7284
  journal?.append({ name: `debug.${event.type.replace(/^debug\./, "")}`, data: event });
6741
7285
  }
@@ -6744,10 +7288,24 @@ function createAgentRuntime(options) {
6744
7288
  }
6745
7289
  return existing;
6746
7290
  };
7291
+ const deliveredDebugger = (delivered) => new DebugCoordinator({
7292
+ store: deliveredRun({
7293
+ flow: delivered.flow,
7294
+ secrets: delivered.secrets,
7295
+ config: delivered.config
7296
+ }),
7297
+ // Handed back rather than written, and redacted on the way out through
7298
+ // a scratch store — somewhere for extraction to put a password typed
7299
+ // during the session, so it does not travel inside the document.
7300
+ commit: { to: "caller", scratch: new Store(delivered.scratchRoot) },
7301
+ emit: (event) => {
7302
+ journal?.append({ name: `debug.${event.type.replace(/^debug\./, "")}`, data: event });
7303
+ }
7304
+ });
6747
7305
  const debugControl = {
6748
7306
  start: async (flowId, opts) => {
6749
7307
  if (debuggingIn?.active()) throw new Error("a debug session is already running on this machine");
6750
- const coordinator = debuggerFor(options.projects[0].root);
7308
+ const coordinator = opts.delivered ? deliveredDebugger(opts.delivered) : debuggerFor(options.projects[0].root);
6751
7309
  const started = await coordinator.start(flowId, opts);
6752
7310
  debuggingIn = coordinator;
6753
7311
  return started;
@@ -6847,15 +7405,15 @@ function createAgentRuntime(options) {
6847
7405
  }
6848
7406
 
6849
7407
  // src/engine.ts
6850
- import fs9 from "fs";
7408
+ import fs10 from "fs";
6851
7409
  import path7 from "path";
6852
7410
 
6853
7411
  // src/credentials.ts
6854
7412
  import crypto3 from "crypto";
6855
- import fs7 from "fs";
7413
+ import fs8 from "fs";
6856
7414
 
6857
7415
  // src/paths.ts
6858
- import fs6 from "fs";
7416
+ import fs7 from "fs";
6859
7417
  import os3 from "os";
6860
7418
  import path6 from "path";
6861
7419
  function dataDir(env = process.env) {
@@ -6873,10 +7431,10 @@ function dataDir(env = process.env) {
6873
7431
  }
6874
7432
  function ensureDataDir(env = process.env) {
6875
7433
  const dir = dataDir(env);
6876
- fs6.mkdirSync(dir, { recursive: true, mode: 448 });
7434
+ fs7.mkdirSync(dir, { recursive: true, mode: 448 });
6877
7435
  if (process.platform !== "win32") {
6878
7436
  try {
6879
- fs6.chmodSync(dir, 448);
7437
+ fs7.chmodSync(dir, 448);
6880
7438
  } catch {
6881
7439
  }
6882
7440
  }
@@ -6889,7 +7447,7 @@ var ledgerPath = (deviceId, env = process.env) => path6.join(dataDir(env), `ledg
6889
7447
  // src/credentials.ts
6890
7448
  function readFile(env = process.env) {
6891
7449
  try {
6892
- const raw = JSON.parse(fs7.readFileSync(credentialsPath(env), "utf8"));
7450
+ const raw = JSON.parse(fs8.readFileSync(credentialsPath(env), "utf8"));
6893
7451
  if (Array.isArray(raw.connections)) return { version: 3, connections: raw.connections };
6894
7452
  const older = Array.isArray(raw.devices) ? raw.devices : raw.deviceId ? [raw] : [];
6895
7453
  const connections = older.filter((d) => d.deviceId && d.secret).map((d) => ({
@@ -6919,8 +7477,8 @@ function saveConnection(connection, env = process.env) {
6919
7477
  connections: [...readFile(env).connections.filter((c) => c.service !== connection.service), connection]
6920
7478
  };
6921
7479
  const temporary = `${file}.${process.pid}.tmp`;
6922
- fs7.writeFileSync(temporary, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
6923
- fs7.renameSync(temporary, file);
7480
+ fs8.writeFileSync(temporary, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
7481
+ fs8.renameSync(temporary, file);
6924
7482
  }
6925
7483
  function forgetConnection(service, env = process.env) {
6926
7484
  const before = readFile(env).connections;
@@ -6929,8 +7487,8 @@ function forgetConnection(service, env = process.env) {
6929
7487
  ensureDataDir(env);
6930
7488
  const file = credentialsPath(env);
6931
7489
  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);
7490
+ fs8.writeFileSync(temporary, JSON.stringify({ version: 3, connections: after }, null, 2) + "\n", { mode: 384 });
7491
+ fs8.renameSync(temporary, file);
6934
7492
  return true;
6935
7493
  }
6936
7494
  var fingerprint = (secret) => crypto3.createHash("sha256").update(secret).digest("hex").slice(0, 12);
@@ -6957,7 +7515,7 @@ function resolveService(explicit, env = process.env) {
6957
7515
  }
6958
7516
 
6959
7517
  // src/lock.ts
6960
- import fs8 from "fs";
7518
+ import fs9 from "fs";
6961
7519
  import os4 from "os";
6962
7520
  function takeLock(env = process.env) {
6963
7521
  ensureDataDir(env);
@@ -6965,20 +7523,20 @@ function takeLock(env = process.env) {
6965
7523
  const mine = { pid: process.pid, since: (/* @__PURE__ */ new Date()).toISOString(), host: os4.hostname() };
6966
7524
  for (let attempt = 0; attempt < 2; attempt += 1) {
6967
7525
  try {
6968
- fs8.writeFileSync(file, JSON.stringify(mine), { flag: "wx", mode: 384 });
7526
+ fs9.writeFileSync(file, JSON.stringify(mine), { flag: "wx", mode: 384 });
6969
7527
  return { ok: true, lock: { release: () => releaseLock(file, mine.pid) } };
6970
7528
  } catch {
6971
7529
  const held = readLock(file);
6972
7530
  if (!held) {
6973
7531
  try {
6974
- fs8.rmSync(file, { force: true });
7532
+ fs9.rmSync(file, { force: true });
6975
7533
  } catch {
6976
7534
  }
6977
7535
  continue;
6978
7536
  }
6979
7537
  if (held.host === mine.host && !alive(held.pid)) {
6980
7538
  try {
6981
- fs8.rmSync(file, { force: true });
7539
+ fs9.rmSync(file, { force: true });
6982
7540
  } catch {
6983
7541
  }
6984
7542
  continue;
@@ -6990,7 +7548,7 @@ function takeLock(env = process.env) {
6990
7548
  }
6991
7549
  function readLock(file) {
6992
7550
  try {
6993
- const held = JSON.parse(fs8.readFileSync(file, "utf8"));
7551
+ const held = JSON.parse(fs9.readFileSync(file, "utf8"));
6994
7552
  return typeof held.pid === "number" ? { pid: held.pid, since: held.since ?? "", host: held.host ?? "" } : null;
6995
7553
  } catch {
6996
7554
  return null;
@@ -7009,7 +7567,7 @@ function releaseLock(file, pid) {
7009
7567
  const held = readLock(file);
7010
7568
  if (held?.pid === pid) {
7011
7569
  try {
7012
- fs8.rmSync(file, { force: true });
7570
+ fs9.rmSync(file, { force: true });
7013
7571
  } catch {
7014
7572
  }
7015
7573
  }
@@ -7038,7 +7596,7 @@ function openBrowser(url, env = process.env) {
7038
7596
  }
7039
7597
 
7040
7598
  // src/engine.ts
7041
- var AGENT_VERSION = "0.1.0-beta.6";
7599
+ var AGENT_VERSION = "0.1.0-beta.8";
7042
7600
  async function connect(options = {}) {
7043
7601
  let service;
7044
7602
  try {
@@ -7122,7 +7680,7 @@ function resolveCheckouts(dirs) {
7122
7680
  const skipped = [];
7123
7681
  for (const dir of dirs) {
7124
7682
  const root = path7.resolve(dir);
7125
- if (!fs9.existsSync(root)) {
7683
+ if (!fs10.existsSync(root)) {
7126
7684
  skipped.push({ root, why: "no such folder" });
7127
7685
  continue;
7128
7686
  }
@@ -7186,11 +7744,21 @@ async function start(options = {}) {
7186
7744
  if (projects.length !== (connection.checkouts ?? []).length || options.projects?.length) {
7187
7745
  saveConnection({ ...connection, checkouts: projects.map((p) => p.root) });
7188
7746
  }
7747
+ const storage = new SqliteStorage();
7748
+ try {
7749
+ await storage.init();
7750
+ } catch (error) {
7751
+ options.onProgress?.({
7752
+ kind: "warning",
7753
+ message: `run history is unavailable on this machine \u2014 ${error.message}`
7754
+ });
7755
+ }
7189
7756
  const agent = createAgentRuntime({
7190
7757
  deviceId: connection.deviceId,
7191
7758
  agentVersion: AGENT_VERSION,
7192
7759
  stateFile: ledgerPath(connection.deviceId),
7193
7760
  projects,
7761
+ storage,
7194
7762
  onEvent: (event) => options.onProgress?.({ kind: event.name, message: "", detail: event.data })
7195
7763
  });
7196
7764
  const link = new AgentLink({
@@ -7221,7 +7789,7 @@ async function prepareBrowsers() {
7221
7789
  }
7222
7790
  let cli;
7223
7791
  try {
7224
- cli = createRequire3(import.meta.url).resolve("playwright/cli.js");
7792
+ cli = createRequire4(import.meta.url).resolve("playwright/cli.js");
7225
7793
  } catch {
7226
7794
  return {
7227
7795
  ok: false,
@@ -7259,7 +7827,7 @@ function status(serviceArg) {
7259
7827
  ...connection ? { deviceId: connection.deviceId, machineName: connection.machineName, connectedAt: connection.connectedAt } : {},
7260
7828
  checkouts: [
7261
7829
  ...projects.map((p) => ({ root: p.root, project: p.name })),
7262
- ...skipped.map((s) => ({ root: s.root, project: null, why: s.why }))
7830
+ ...skipped.map((s2) => ({ root: s2.root, project: null, why: s2.why }))
7263
7831
  ],
7264
7832
  running
7265
7833
  };
@@ -7307,7 +7875,7 @@ async function doctor(serviceArg) {
7307
7875
  function inspectPlaywright() {
7308
7876
  try {
7309
7877
  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;
7878
+ const chromiumInstalled = fs10.existsSync(browsersPath) ? fs10.readdirSync(browsersPath).some((d) => d.startsWith("chromium")) : false;
7311
7879
  return { version: null, browsersPath, chromiumInstalled };
7312
7880
  } catch {
7313
7881
  return { version: null, browsersPath: null, chromiumInstalled: null };