@code-partner/codepipe-hub 0.14.1-dev.420.g57ff103d → 0.14.1-dev.423.ga49d1f61

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.
@@ -16,6 +16,15 @@ function humanSubjectOf(actor) {
16
16
  return actor.principal.delegatedBy ?? null;
17
17
  return null;
18
18
  }
19
+ function accountOf(actor) {
20
+ const subjectId = humanSubjectOf(actor);
21
+ if (subjectId === null)
22
+ return null;
23
+ return {
24
+ subjectId,
25
+ email: actor.principal.kind === "human" && actor.displayEmail !== void 0 ? actor.displayEmail : null
26
+ };
27
+ }
19
28
  function serviceOf(actor) {
20
29
  return actor.principal.kind === "service" ? actor.principal.service : null;
21
30
  }
@@ -448,7 +457,7 @@ var PLATFORM_VERSION;
448
457
  var init_version_generated = __esm({
449
458
  "../packages/shared/dist/version.generated.js"() {
450
459
  "use strict";
451
- PLATFORM_VERSION = "0.14.1-dev.420.g57ff103d";
460
+ PLATFORM_VERSION = "0.14.1-dev.423.ga49d1f61";
452
461
  }
453
462
  });
454
463
 
@@ -458,7 +467,7 @@ var init_version = __esm({
458
467
  "../packages/shared/dist/version.js"() {
459
468
  "use strict";
460
469
  init_version_generated();
461
- PROTOCOL_VERSION = 4;
470
+ PROTOCOL_VERSION = 5;
462
471
  }
463
472
  });
464
473
 
@@ -1227,7 +1236,7 @@ function parseRepoMirror(raw) {
1227
1236
  function isCredKind(value) {
1228
1237
  return typeof value === "string" && CRED_KINDS.includes(value);
1229
1238
  }
1230
- var DEFAULT_BRANCH_PATTERN, BRANCH_SLUG_MAX_LENGTH, CYRILLIC_TRANSLIT, CRED_KINDS;
1239
+ var DEFAULT_BRANCH_PATTERN, BRANCH_SLUG_MAX_LENGTH, CYRILLIC_TRANSLIT, CRED_KINDS, PROJECT_ID_RE;
1231
1240
  var init_project = __esm({
1232
1241
  "../packages/shared/dist/project.js"() {
1233
1242
  "use strict";
@@ -1269,6 +1278,7 @@ var init_project = __esm({
1269
1278
  \u044F: "ya"
1270
1279
  };
1271
1280
  CRED_KINDS = ["github", "gitlab", "bitbucket", "youtrack", "contextWrite"];
1281
+ PROJECT_ID_RE = /^[a-z][a-z0-9-]{1,30}$/;
1272
1282
  }
1273
1283
  });
1274
1284
 
@@ -4231,16 +4241,40 @@ function createReadOperations(port, authorizer) {
4231
4241
  return { projectId, items: rows, revision: valueRevision(rows) };
4232
4242
  }
4233
4243
  function projectVisibilityOf(actor) {
4234
- const subjectId = humanSubjectOf(actor);
4235
- if (subjectId === null)
4244
+ const account = accountOf(actor);
4245
+ if (account === null)
4236
4246
  return null;
4237
4247
  return {
4238
- subjectId,
4239
- ...actor.principal.kind === "human" && actor.displayEmail !== void 0 ? { email: actor.displayEmail } : {},
4248
+ subjectId: account.subjectId,
4249
+ ...account.email === null ? {} : { email: account.email },
4240
4250
  ...actor.projectScope === void 0 ? {} : { projectScope: actor.projectScope },
4241
4251
  includeUnowned: actor.authMethod !== "cli_token"
4242
4252
  };
4243
4253
  }
4254
+ function farmsView(rows) {
4255
+ return {
4256
+ items: rows.map((r) => ({
4257
+ id: r.farmId,
4258
+ name: r.name,
4259
+ publicKey: r.publicKey,
4260
+ keyFingerprint: r.keyFingerprint,
4261
+ online: r.online,
4262
+ lastSeenAt: toOptionalTimestamp(r.lastSeenAt)
4263
+ }))
4264
+ };
4265
+ }
4266
+ function projectDeliverablesView(projectId, rows) {
4267
+ return {
4268
+ projectId,
4269
+ items: rows.map((r) => ({
4270
+ taskKey: r.taskKey,
4271
+ title: r.title,
4272
+ phase: r.phase,
4273
+ updatedAt: toTimestamp(r.updatedAt),
4274
+ repos: r.repos.map((x) => ({ repoName: x.repoName, branchName: x.branchName, baseRef: x.baseRef }))
4275
+ }))
4276
+ };
4277
+ }
4244
4278
  async function visibleProjects(actor) {
4245
4279
  const visibility = projectVisibilityOf(actor);
4246
4280
  return visibility === null ? [] : await port.visibleProjectIds(visibility);
@@ -4709,6 +4743,15 @@ function createReadOperations(port, authorizer) {
4709
4743
  const rows = await port.listRepos(query.projectId);
4710
4744
  return reposView(query.projectId, rows);
4711
4745
  },
4746
+ async listProjectDeliverables(actor, query) {
4747
+ await require2(actor, "delivery.read", { type: "deliverable", projectId: query.projectId });
4748
+ return projectDeliverablesView(query.projectId, await port.listProjectDeliverables(query.projectId));
4749
+ },
4750
+ async listFarms(actor) {
4751
+ await require2(actor, "account.read", { type: "account" });
4752
+ const account = accountOf(actor);
4753
+ return farmsView(account === null ? [] : await port.listFarms(account));
4754
+ },
4712
4755
  async listConfigProposals(actor, query) {
4713
4756
  await require2(actor, "project.read", { type: "project", id: query.projectId, projectId: query.projectId });
4714
4757
  const limit = normalizeLimit(query.limit);
@@ -4938,8 +4981,18 @@ function createCommandOperations(deps) {
4938
4981
  });
4939
4982
  case "unprocessable":
4940
4983
  throw errors.unprocessable({ details: { reason: outcome.reason } });
4984
+ case "not_found":
4985
+ throw errors.notFound();
4986
+ case "rate_limited":
4987
+ throw errors.rateLimited();
4941
4988
  }
4942
4989
  }
4990
+ function accountActorOf(actor) {
4991
+ const account = accountOf(actor);
4992
+ if (account === null)
4993
+ throw errors.forbidden({ details: { reason: "human_only" } });
4994
+ return account;
4995
+ }
4943
4996
  async function audited(actor, entry, run) {
4944
4997
  const policy = { decision: "not_evaluated" };
4945
4998
  const common = {
@@ -5428,6 +5481,115 @@ function createCommandOperations(deps) {
5428
5481
  return { value: outcome.result, replayed: outcome.replayed };
5429
5482
  });
5430
5483
  },
5484
+ async createProject(actor, command) {
5485
+ const { project } = command;
5486
+ const projectId = project.projectId;
5487
+ if (!PROJECT_ID_RE.test(projectId)) {
5488
+ throw errors.invalidInput({
5489
+ message: "A project id is lowercase letters, digits and dashes, starts with a letter, 2\u201331 characters.",
5490
+ details: { field: "projectId" }
5491
+ });
5492
+ }
5493
+ for (const repo of project.repos) {
5494
+ if (repo.defaultBranch !== void 0 && !isValidBranchName(repo.defaultBranch)) {
5495
+ throw errors.invalidInput({ message: "That is not a valid git branch name.", details: { field: "repos.defaultBranch", repo: repo.name } });
5496
+ }
5497
+ }
5498
+ const key = asIdempotencyKey(command.idempotencyKey);
5499
+ const scope = { subjectId: actor.principal.subjectId, operationId: "project.create", projectId, key };
5500
+ return await audited(actor, { operationId: "project.create", aggregateType: "project", aggregateId: projectId, projectId, data: { name: project.name, tracker: project.trackerProvider } }, async (policy) => {
5501
+ await require2(actor, "account.manage", { type: "account" }, policy);
5502
+ const run = await runIdempotent(idempotency, scope, { project }, async () => unwrap(await write2.createProject({ project, owner: accountActorOf(actor) })), now());
5503
+ return { value: run.result, replayed: run.replayed };
5504
+ });
5505
+ },
5506
+ async provisionOnFarm(actor, command) {
5507
+ const { farmId, projectId } = command;
5508
+ const key = asIdempotencyKey(command.idempotencyKey);
5509
+ const scope = { subjectId: actor.principal.subjectId, operationId: "farm.provision", projectId, key };
5510
+ return await audited(actor, { operationId: "farm.provision", aggregateType: "project", aggregateId: projectId, projectId, data: { farmId } }, async (policy) => {
5511
+ await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
5512
+ const run = await runIdempotent(idempotency, scope, { farmId, projectId }, async () => {
5513
+ const row = unwrap(await write2.provisionOnFarm({ farmId, projectId, sealedBundle: command.sealedBundle, actor: accountActorOf(actor) }));
5514
+ return { projectId, farmId, ok: row.ok, daemonStatus: row.daemonStatus, error: row.error };
5515
+ }, now());
5516
+ return { value: run.result, replayed: run.replayed };
5517
+ });
5518
+ },
5519
+ async provisionGhRunners(actor, command) {
5520
+ const { runnerId, projectId } = command;
5521
+ const key = asIdempotencyKey(command.idempotencyKey);
5522
+ const scope = { subjectId: actor.principal.subjectId, operationId: "runner.provisionGhRunners", projectId, key };
5523
+ return await audited(actor, { operationId: "runner.provisionGhRunners", aggregateType: "project", aggregateId: projectId, projectId, data: { runnerId } }, async (policy) => {
5524
+ await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
5525
+ const run = await runIdempotent(idempotency, scope, { runnerId, projectId }, async () => {
5526
+ const row = unwrap(await write2.provisionGhRunners({ runnerId, projectId, sealedBundle: command.sealedBundle, actor: accountActorOf(actor) }));
5527
+ return {
5528
+ projectId,
5529
+ runnerId,
5530
+ ok: row.ok,
5531
+ runners: row.runners.map((r) => ({ owner: r.owner, repo: r.repo, status: r.status, error: r.error, labels: r.labels })),
5532
+ error: row.error
5533
+ };
5534
+ }, now());
5535
+ return { value: run.result, replayed: run.replayed };
5536
+ });
5537
+ },
5538
+ async teardownGhRunners(actor, command) {
5539
+ const { runnerId, projectId } = command;
5540
+ const key = asIdempotencyKey(command.idempotencyKey);
5541
+ const scope = { subjectId: actor.principal.subjectId, operationId: "runner.teardownGhRunners", projectId, key };
5542
+ const repo = command.repo ?? null;
5543
+ return await audited(actor, {
5544
+ operationId: "runner.teardownGhRunners",
5545
+ aggregateType: "project",
5546
+ aggregateId: projectId,
5547
+ projectId,
5548
+ data: { runnerId, ...repo === null ? {} : { repo: `${repo.owner}/${repo.repo}` } }
5549
+ }, async (policy) => {
5550
+ await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
5551
+ const run = await runIdempotent(idempotency, scope, { runnerId, projectId, repo }, async () => {
5552
+ const row = unwrap(await write2.teardownGhRunners({
5553
+ runnerId,
5554
+ projectId,
5555
+ sealedBundle: command.sealedBundle ?? null,
5556
+ repo,
5557
+ actor: accountActorOf(actor)
5558
+ }));
5559
+ return { projectId, runnerId, ok: row.ok, error: row.error };
5560
+ }, now());
5561
+ return { value: run.result, replayed: run.replayed };
5562
+ });
5563
+ },
5564
+ async requestProjectIndex(actor, command) {
5565
+ const { projectId, kind } = command;
5566
+ const key = asIdempotencyKey(command.idempotencyKey);
5567
+ const scope = { subjectId: actor.principal.subjectId, operationId: "project.index", projectId, key };
5568
+ return await audited(actor, { operationId: "project.index", aggregateType: "project", aggregateId: projectId, projectId, data: { kind } }, async (policy) => {
5569
+ await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
5570
+ const run = await runIdempotent(idempotency, scope, { kind }, async () => {
5571
+ const { jobId } = unwrap(await write2.enqueueProjectIndex({ projectId, kind }));
5572
+ return { jobId, kind };
5573
+ }, now());
5574
+ return { value: run.result, replayed: run.replayed };
5575
+ });
5576
+ },
5577
+ async issueWorkerRegistrationToken(actor) {
5578
+ const subjectId = actor.principal.subjectId;
5579
+ return await audited(actor, { operationId: "worker.issueRegistrationToken", aggregateType: "account", aggregateId: subjectId, projectId: null }, async (policy) => {
5580
+ await require2(actor, "account.manage", { type: "account" }, policy);
5581
+ const issued = unwrap(await write2.issueWorkerRegistrationToken({ owner: accountActorOf(actor) }));
5582
+ return { value: { secret: issued.secret, expiresAt: toTimestamp(issued.expiresAt) }, replayed: false };
5583
+ });
5584
+ },
5585
+ async reportInitStatus(actor, command) {
5586
+ const { projectId, phase } = command;
5587
+ return await audited(actor, { operationId: "project.reportInitStatus", aggregateType: "project", aggregateId: projectId, projectId, data: { phase, failed: command.error !== void 0 } }, async (policy) => {
5588
+ await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
5589
+ const { recorded } = unwrap(await write2.reportInitStatus({ projectId, phase, error: command.error ?? null }));
5590
+ return { value: { recorded }, replayed: false };
5591
+ });
5592
+ },
5431
5593
  async setRepoDefaultBranch(actor, command) {
5432
5594
  const { projectId, name } = command;
5433
5595
  const revision = asRevision(command.revision);
@@ -8623,14 +8785,14 @@ function buildQueueDag(tasks, isSatisfiedOutside = () => false) {
8623
8785
  function stableTopoOrder(currentOrder, dag) {
8624
8786
  const sortable = currentOrder.filter((k) => dag.before.has(k) && !dag.cycleKeys.has(k));
8625
8787
  const index = new Map(sortable.map((k, i) => [k, i]));
8626
- const pending4 = /* @__PURE__ */ new Map();
8788
+ const pending5 = /* @__PURE__ */ new Map();
8627
8789
  const dependents = /* @__PURE__ */ new Map();
8628
8790
  for (const key of sortable) {
8629
8791
  const preds = [...dag.before.get(key) ?? []].filter(
8630
8792
  (p) => index.has(p)
8631
8793
  // cycle keys / out-of-order keys don't gate the sort
8632
8794
  );
8633
- pending4.set(key, preds.length);
8795
+ pending5.set(key, preds.length);
8634
8796
  for (const p of preds) {
8635
8797
  const d = dependents.get(p) ?? [];
8636
8798
  d.push(key);
@@ -8638,7 +8800,7 @@ function stableTopoOrder(currentOrder, dag) {
8638
8800
  }
8639
8801
  }
8640
8802
  const ready = /* @__PURE__ */ new Set();
8641
- for (const [key, deg] of pending4) if (deg === 0) ready.add(key);
8803
+ for (const [key, deg] of pending5) if (deg === 0) ready.add(key);
8642
8804
  const out = [];
8643
8805
  while (ready.size > 0) {
8644
8806
  let best = null;
@@ -8648,8 +8810,8 @@ function stableTopoOrder(currentOrder, dag) {
8648
8810
  ready.delete(best);
8649
8811
  out.push(best);
8650
8812
  for (const dep of dependents.get(best) ?? []) {
8651
- const deg = pending4.get(dep) - 1;
8652
- pending4.set(dep, deg);
8813
+ const deg = pending5.get(dep) - 1;
8814
+ pending5.set(dep, deg);
8653
8815
  if (deg === 0) ready.add(dep);
8654
8816
  }
8655
8817
  }
@@ -10331,8 +10493,8 @@ async function saveDeliveries(db, taskId2, sourceJobId, deliveries) {
10331
10493
  if (deliveries.length === 0) return;
10332
10494
  const now = Date.now();
10333
10495
  for (const d of deliveries) {
10334
- const pending4 = pendingRelayRevision(d);
10335
- const unpublished = pending4 !== null;
10496
+ const pending5 = pendingRelayRevision(d);
10497
+ const unpublished = pending5 !== null;
10336
10498
  await db.run(
10337
10499
  `INSERT INTO task_deliveries
10338
10500
  (task_id, repo_name, delivery_ref, delivery_sha, base_sha, changed_files_json, source_job_id, created_at,
@@ -10355,7 +10517,7 @@ async function saveDeliveries(db, taskId2, sourceJobId, deliveries) {
10355
10517
  JSON.stringify(d.changedFiles ?? []),
10356
10518
  sourceJobId,
10357
10519
  now,
10358
- pending4 && pending4 !== "unusable" ? JSON.stringify(pending4) : null,
10520
+ pending5 && pending5 !== "unusable" ? JSON.stringify(pending5) : null,
10359
10521
  unpublished ? null : now
10360
10522
  );
10361
10523
  }
@@ -10820,7 +10982,20 @@ var init_auth = __esm({
10820
10982
  });
10821
10983
 
10822
10984
  // src/sandbox-reg-tokens.ts
10985
+ import { createHash as createHash2 } from "node:crypto";
10823
10986
  import { nanoid as nanoid7 } from "nanoid";
10987
+ function hashSandboxRegToken(secret) {
10988
+ return createHash2("sha256").update(secret, "utf8").digest("hex");
10989
+ }
10990
+ async function issueEphemeralSandboxRegToken(db, email, ttlMs = EPHEMERAL_REG_TOKEN_TTL_MS) {
10991
+ const normalized = email.trim().toLowerCase();
10992
+ const secret = `sbxreg_${nanoid7(48)}`;
10993
+ const now = Date.now();
10994
+ const expiresAt = now + ttlMs;
10995
+ await db.run(`INSERT INTO sandbox_reg_tokens (token_hash, email, user_id, created_at, expires_at)
10996
+ VALUES (?, ?, (SELECT id FROM users WHERE email = ?), ?, ?)`, hashSandboxRegToken(secret), normalized, normalized, now, expiresAt);
10997
+ return { secret, expiresAt };
10998
+ }
10824
10999
  var EPHEMERAL_REG_TOKEN_TTL_MS;
10825
11000
  var init_sandbox_reg_tokens = __esm({
10826
11001
  "src/sandbox-reg-tokens.ts"() {
@@ -10886,6 +11061,9 @@ async function getInitStatus(db, projectId) {
10886
11061
  async function write(db, projectId, status) {
10887
11062
  await db.run(`UPDATE projects SET init_status_json = ? WHERE id = ?`, JSON.stringify(status), projectId);
10888
11063
  }
11064
+ async function beginInitTracking(db, projectId, phase) {
11065
+ await write(db, projectId, { phase, updatedAt: Date.now() });
11066
+ }
10889
11067
  async function advanceInitPhase(db, projectId, phase) {
10890
11068
  const cur = await getInitStatus(db, projectId);
10891
11069
  if (!cur) return false;
@@ -11113,6 +11291,25 @@ var init_state_machine2 = __esm({
11113
11291
  });
11114
11292
 
11115
11293
  // src/abuse-throttle.ts
11294
+ function createAbuseThrottle(opts) {
11295
+ const hits = /* @__PURE__ */ new Map();
11296
+ return {
11297
+ take(key) {
11298
+ const now = Date.now();
11299
+ const entry = hits.get(key);
11300
+ if (!entry || entry.resetAt <= now) {
11301
+ hits.set(key, { count: 1, resetAt: now + opts.windowMs });
11302
+ return true;
11303
+ }
11304
+ if (entry.count >= opts.max) return false;
11305
+ entry.count += 1;
11306
+ return true;
11307
+ },
11308
+ reset() {
11309
+ hits.clear();
11310
+ }
11311
+ };
11312
+ }
11116
11313
  var init_abuse_throttle = __esm({
11117
11314
  "src/abuse-throttle.ts"() {
11118
11315
  "use strict";
@@ -11143,16 +11340,104 @@ var init_sandbox2 = __esm({
11143
11340
  init_sealedbox();
11144
11341
  init_abuse_throttle();
11145
11342
  init_auth();
11146
- init_cli_auth();
11147
11343
  init_sandbox_reg_tokens();
11148
11344
  init_users();
11149
11345
  init_agent_auth_probe();
11150
11346
  init_ws();
11347
+ init_route_registry();
11151
11348
  }
11152
11349
  });
11153
11350
 
11154
- // src/farm/pool-spec.ts
11351
+ // src/routes/farm.ts
11155
11352
  import { nanoid as nanoid11 } from "nanoid";
11353
+ function viewerInAssignments(viewer, assignments) {
11354
+ return assignments.some(
11355
+ (a) => viewer.userId !== null && a.userId === viewer.userId || a.email !== null && a.email === viewer.email
11356
+ );
11357
+ }
11358
+ async function viewerOwnsFarm(db, viewer, farmId) {
11359
+ const row = await db.get(
11360
+ `SELECT 1 FROM farm_assignments
11361
+ WHERE farm_id = ? AND (user_id = ? OR email = ?) LIMIT 1`,
11362
+ farmId,
11363
+ viewer.userId,
11364
+ viewer.email
11365
+ );
11366
+ return !!row;
11367
+ }
11368
+ async function listFarms(db, viewer) {
11369
+ const online = new Set(listConnectedFarmIds());
11370
+ const rows = await db.all(`SELECT f.id, f.name, f.public_key as publicKey,
11371
+ f.key_fingerprint as keyFingerprint, f.last_seen_at as lastSeenAt,
11372
+ a.user_id as aUserId, a.email as aEmail
11373
+ FROM farms f LEFT JOIN farm_assignments a ON a.farm_id = f.id
11374
+ ORDER BY f.registered_at ASC`);
11375
+ const byId = /* @__PURE__ */ new Map();
11376
+ for (const r of rows) {
11377
+ let farm = byId.get(r.id);
11378
+ if (!farm) {
11379
+ farm = {
11380
+ farmId: r.id,
11381
+ name: r.name,
11382
+ publicKey: r.publicKey,
11383
+ keyFingerprint: r.keyFingerprint,
11384
+ online: online.has(r.id),
11385
+ lastSeenAt: r.lastSeenAt,
11386
+ assignments: []
11387
+ };
11388
+ byId.set(r.id, farm);
11389
+ }
11390
+ if (r.aUserId !== null || r.aEmail !== null) {
11391
+ farm.assignments.push({ userId: r.aUserId, email: r.aEmail });
11392
+ }
11393
+ }
11394
+ return [...byId.values()].filter((f) => !viewer || viewer.isSuperAdmin || viewerInAssignments(viewer, f.assignments)).map(({ assignments: _assignments, ...f }) => f);
11395
+ }
11396
+ var init_farm2 = __esm({
11397
+ "src/routes/farm.ts"() {
11398
+ "use strict";
11399
+ init_sealedbox();
11400
+ init_abuse_throttle();
11401
+ init_ws();
11402
+ init_route_registry();
11403
+ }
11404
+ });
11405
+
11406
+ // src/runner-reg-tokens.ts
11407
+ import { nanoid as nanoid12 } from "nanoid";
11408
+ var init_runner_reg_tokens = __esm({
11409
+ "src/runner-reg-tokens.ts"() {
11410
+ "use strict";
11411
+ }
11412
+ });
11413
+
11414
+ // src/routes/runner.ts
11415
+ import { nanoid as nanoid13 } from "nanoid";
11416
+ async function viewerOwnsRunner(db, viewer, runnerId) {
11417
+ const row = await db.get(
11418
+ `SELECT 1 FROM runners WHERE id = ? AND (owner_user_id = ? OR owner_email = ?) LIMIT 1`,
11419
+ runnerId,
11420
+ viewer.userId,
11421
+ viewer.email
11422
+ );
11423
+ return !!row;
11424
+ }
11425
+ var init_runner2 = __esm({
11426
+ "src/routes/runner.ts"() {
11427
+ "use strict";
11428
+ init_sealedbox();
11429
+ init_abuse_throttle();
11430
+ init_cli_auth();
11431
+ init_auth();
11432
+ init_users();
11433
+ init_runner_reg_tokens();
11434
+ init_ws();
11435
+ init_route_registry();
11436
+ }
11437
+ });
11438
+
11439
+ // src/farm/pool-spec.ts
11440
+ import { nanoid as nanoid14 } from "nanoid";
11156
11441
  async function computeFarmPoolSpec(db, farmId) {
11157
11442
  const rows = await db.all(
11158
11443
  `SELECT fa.user_id as userId, fa.max_sandboxes as quota,
@@ -11184,7 +11469,7 @@ async function computeFarmPoolSpec(db, farmId) {
11184
11469
  return spec;
11185
11470
  }
11186
11471
  function buildSyncWorkerPoolMessage(accounts) {
11187
- return { type: "sync_worker_pool", requestId: nanoid11(16), accounts };
11472
+ return { type: "sync_worker_pool", requestId: nanoid14(16), accounts };
11188
11473
  }
11189
11474
  var init_pool_spec = __esm({
11190
11475
  "src/farm/pool-spec.ts"() {
@@ -11192,127 +11477,6 @@ var init_pool_spec = __esm({
11192
11477
  }
11193
11478
  });
11194
11479
 
11195
- // src/farm/provision.ts
11196
- import { nanoid as nanoid12 } from "nanoid";
11197
- function awaitResult(requestId2, send, timeoutMs) {
11198
- if (!send()) return Promise.reject(new Error("farm_offline"));
11199
- return new Promise((resolve2, reject) => {
11200
- const timer = setTimeout(() => {
11201
- pending.delete(requestId2);
11202
- reject(new Error("farm_timeout"));
11203
- }, timeoutMs);
11204
- pending.set(requestId2, { resolve: resolve2, reject, timer });
11205
- });
11206
- }
11207
- function requestDelete(farmId, projectId, timeoutMs) {
11208
- const requestId2 = nanoid12(16);
11209
- return awaitResult(
11210
- requestId2,
11211
- () => sendToFarm(farmId, { type: "delete_project", requestId: requestId2, projectId }),
11212
- timeoutMs
11213
- );
11214
- }
11215
- async function requestFarmPoolSync(db, farmId) {
11216
- const accounts = await computeFarmPoolSpec(db, farmId);
11217
- sendToFarm(farmId, buildSyncWorkerPoolMessage(accounts));
11218
- }
11219
- var pending;
11220
- var init_provision = __esm({
11221
- "src/farm/provision.ts"() {
11222
- "use strict";
11223
- init_ws();
11224
- init_pool_spec();
11225
- pending = /* @__PURE__ */ new Map();
11226
- }
11227
- });
11228
-
11229
- // src/routes/farm.ts
11230
- import { nanoid as nanoid13 } from "nanoid";
11231
- var init_farm2 = __esm({
11232
- "src/routes/farm.ts"() {
11233
- "use strict";
11234
- init_sealedbox();
11235
- init_abuse_throttle();
11236
- init_cli_auth();
11237
- init_auth();
11238
- init_users();
11239
- init_init_status();
11240
- init_ws();
11241
- init_provision();
11242
- }
11243
- });
11244
-
11245
- // src/runner-reg-tokens.ts
11246
- import { nanoid as nanoid14 } from "nanoid";
11247
- var init_runner_reg_tokens = __esm({
11248
- "src/runner-reg-tokens.ts"() {
11249
- "use strict";
11250
- }
11251
- });
11252
-
11253
- // src/runner/provision.ts
11254
- import { nanoid as nanoid15 } from "nanoid";
11255
- var init_provision2 = __esm({
11256
- "src/runner/provision.ts"() {
11257
- "use strict";
11258
- init_ws();
11259
- }
11260
- });
11261
-
11262
- // src/runner/bindings.ts
11263
- function parseGhRunnerBindings(json) {
11264
- if (!json) return [];
11265
- try {
11266
- const parsed = JSON.parse(json);
11267
- return Array.isArray(parsed.bindings) ? parsed.bindings : [];
11268
- } catch {
11269
- return [];
11270
- }
11271
- }
11272
- function serialize(bindings) {
11273
- return JSON.stringify({ bindings });
11274
- }
11275
- async function removeGhRunnerBinding(db, projectId, owner, repo) {
11276
- return db.tx(async (tx) => {
11277
- const row = await tx.get(
11278
- `SELECT gh_runner_bindings_json as bindingsJson FROM projects WHERE id = ?`,
11279
- projectId
11280
- );
11281
- if (!row) return false;
11282
- const bindings = parseGhRunnerBindings(row.bindingsJson);
11283
- const kept = bindings.filter((b) => !(b.owner === owner && b.repo === repo));
11284
- if (kept.length === bindings.length) return false;
11285
- await tx.run(
11286
- `UPDATE projects SET gh_runner_bindings_json = ? WHERE id = ?`,
11287
- serialize(kept),
11288
- projectId
11289
- );
11290
- return true;
11291
- });
11292
- }
11293
- var init_bindings = __esm({
11294
- "src/runner/bindings.ts"() {
11295
- "use strict";
11296
- }
11297
- });
11298
-
11299
- // src/routes/runner.ts
11300
- import { nanoid as nanoid16 } from "nanoid";
11301
- var init_runner2 = __esm({
11302
- "src/routes/runner.ts"() {
11303
- "use strict";
11304
- init_sealedbox();
11305
- init_abuse_throttle();
11306
- init_cli_auth();
11307
- init_auth();
11308
- init_users();
11309
- init_runner_reg_tokens();
11310
- init_ws();
11311
- init_provision2();
11312
- init_bindings();
11313
- }
11314
- });
11315
-
11316
11480
  // src/routes/ws.ts
11317
11481
  function sendToDeliveryFollowers(projectId, message) {
11318
11482
  const payload = JSON.stringify(message);
@@ -11356,6 +11520,13 @@ function sendToFarm(farmId, message) {
11356
11520
  function listConnectedFarmIds() {
11357
11521
  return [...farmConnections.keys()];
11358
11522
  }
11523
+ function sendToRunner(runnerId, message) {
11524
+ const conn = runnerConnections.get(runnerId);
11525
+ if (!conn) return false;
11526
+ if (conn.socket.readyState !== conn.socket.OPEN) return false;
11527
+ conn.socket.send(JSON.stringify(message));
11528
+ return true;
11529
+ }
11359
11530
  function getProjectCliOrigin(projectId) {
11360
11531
  return projectCliConnections.get(projectId)?.origin ?? null;
11361
11532
  }
@@ -11598,7 +11769,7 @@ async function getPresence(db, viewer) {
11598
11769
  })()
11599
11770
  };
11600
11771
  }
11601
- var agentLoginProviders, deliveryFollowers, projectCliConnections, projectLastSeenAt, projectLastDuplicateRegisterAt, sandboxConnections, farmConnections, cliMessageHandlers, cliRegisterHandlers;
11772
+ var agentLoginProviders, deliveryFollowers, projectCliConnections, projectLastSeenAt, projectLastDuplicateRegisterAt, sandboxConnections, farmConnections, runnerConnections, cliMessageHandlers, cliRegisterHandlers;
11602
11773
  var init_ws = __esm({
11603
11774
  "src/routes/ws.ts"() {
11604
11775
  "use strict";
@@ -11624,6 +11795,7 @@ var init_ws = __esm({
11624
11795
  projectLastDuplicateRegisterAt = /* @__PURE__ */ new Map();
11625
11796
  sandboxConnections = /* @__PURE__ */ new Map();
11626
11797
  farmConnections = /* @__PURE__ */ new Map();
11798
+ runnerConnections = /* @__PURE__ */ new Map();
11627
11799
  cliMessageHandlers = /* @__PURE__ */ new Set();
11628
11800
  cliRegisterHandlers = /* @__PURE__ */ new Set();
11629
11801
  }
@@ -11751,7 +11923,7 @@ var init_policy = __esm({
11751
11923
  });
11752
11924
 
11753
11925
  // src/apply/merge-branch.ts
11754
- import { nanoid as nanoid17 } from "nanoid";
11926
+ import { nanoid as nanoid15 } from "nanoid";
11755
11927
  async function enqueueMergeBranch(db, taskId2, log) {
11756
11928
  if (await hasInflightMergeBranch(db, taskId2)) {
11757
11929
  log.info({ taskId: taskId2 }, "merge_branch ignored \u2014 chain already in flight");
@@ -11768,7 +11940,7 @@ async function enqueueMergeBranch(db, taskId2, log) {
11768
11940
  const now = Date.now();
11769
11941
  const ids = [];
11770
11942
  for (const t of targets) {
11771
- const id = `wa_${nanoid17(12)}`;
11943
+ const id = `wa_${nanoid15(12)}`;
11772
11944
  const action = {
11773
11945
  type: "merge_branch",
11774
11946
  taskId: taskId2,
@@ -11849,7 +12021,7 @@ var init_merge_branch = __esm({
11849
12021
  });
11850
12022
 
11851
12023
  // src/apply/publish-delivery.ts
11852
- import { nanoid as nanoid18 } from "nanoid";
12024
+ import { nanoid as nanoid16 } from "nanoid";
11853
12025
  async function enqueuePublishDeliveries(db, taskId2, log) {
11854
12026
  const unpublished = await unpublishedDeliveries(db, taskId2);
11855
12027
  const unusable = unpublished.filter((p) => p.revision === null);
@@ -11860,8 +12032,8 @@ async function enqueuePublishDeliveries(db, taskId2, log) {
11860
12032
  };
11861
12033
  }
11862
12034
  const inFlight2 = await inFlightPublications(db, taskId2);
11863
- const pending4 = unpublished.filter((p) => inFlight2.get(p.repoName) !== p.revision?.sha);
11864
- if (pending4.length === 0) return { queued: 0 };
12035
+ const pending5 = unpublished.filter((p) => inFlight2.get(p.repoName) !== p.revision?.sha);
12036
+ if (pending5.length === 0) return { queued: 0 };
11865
12037
  const projectId = taskId2.split(":", 2)[0] ?? "";
11866
12038
  const contextRepoCloneUrl = await contextRepoCloneUrlOf(db, projectId);
11867
12039
  if (!contextRepoCloneUrl) {
@@ -11870,8 +12042,8 @@ async function enqueuePublishDeliveries(db, taskId2, log) {
11870
12042
  }
11871
12043
  const now = Date.now();
11872
12044
  const ids = [];
11873
- for (const p of pending4) {
11874
- const id = `wa_${nanoid18(12)}`;
12045
+ for (const p of pending5) {
12046
+ const id = `wa_${nanoid16(12)}`;
11875
12047
  const action = {
11876
12048
  type: "publish_delivery",
11877
12049
  taskId: taskId2,
@@ -12239,10 +12411,10 @@ __export(regimen_config_exports, {
12239
12411
  loadWorkflowSettings: () => loadWorkflowSettings,
12240
12412
  loadYouTrackWrites: () => loadYouTrackWrites
12241
12413
  });
12242
- import { createHash as createHash2 } from "node:crypto";
12414
+ import { createHash as createHash3 } from "node:crypto";
12243
12415
  import { parse as parseYaml2 } from "yaml";
12244
12416
  function contentSha2(content) {
12245
- return createHash2("sha1").update(content, "utf-8").digest("hex");
12417
+ return createHash3("sha1").update(content, "utf-8").digest("hex");
12246
12418
  }
12247
12419
  async function applyRegimenContent(db, projectId, content, log, preParsed) {
12248
12420
  const previous = cache3.get(projectId) ?? null;
@@ -13132,7 +13304,7 @@ var init_settings = __esm({
13132
13304
  });
13133
13305
 
13134
13306
  // src/jobs/queue.ts
13135
- import { nanoid as nanoid19 } from "nanoid";
13307
+ import { nanoid as nanoid17 } from "nanoid";
13136
13308
  async function heldByTrancheDeferral(db) {
13137
13309
  const rows = await db.all(
13138
13310
  `SELECT t.id AS id FROM tasks t
@@ -13204,7 +13376,7 @@ async function restampQueuedJobs(db, projectId) {
13204
13376
  return restamped;
13205
13377
  }
13206
13378
  async function createJob(db, input) {
13207
- const id = `job_${nanoid19(16)}`;
13379
+ const id = `job_${nanoid17(16)}`;
13208
13380
  const now = Date.now();
13209
13381
  const projectRepos = await loadProjectRepoRefs(db, input.projectId);
13210
13382
  const metadata = {
@@ -13405,7 +13577,7 @@ async function claimNextJob(db, sandboxId, capabilities, lane) {
13405
13577
  data: e.data
13406
13578
  });
13407
13579
  },
13408
- newExecutionId: () => `exe_${nanoid19(16)}`,
13580
+ newExecutionId: () => `exe_${nanoid17(16)}`,
13409
13581
  // DEV-49/DEV-221: stack the worktree on the predecessor's branch so the
13410
13582
  // agent (and the resulting meta/bases.json baseSha) start from the chain
13411
13583
  // tip, not the repo default. Applies to analyze/estimate/implement — a
@@ -13671,7 +13843,7 @@ var init_legacy_cred_bundles = __esm({
13671
13843
  });
13672
13844
 
13673
13845
  // src/db/schema.ts
13674
- import { nanoid as nanoid20 } from "nanoid";
13846
+ import { nanoid as nanoid18 } from "nanoid";
13675
13847
  async function ensureColumn(db, table, column, ddl) {
13676
13848
  const row = await db.get(
13677
13849
  `SELECT 1 FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,
@@ -13980,7 +14152,7 @@ async function initSchema(db) {
13980
14152
  await ensureColumn(db, "projects", "init_status_json", "init_status_json TEXT");
13981
14153
  await ensureColumn(db, "users", "id", "id TEXT");
13982
14154
  for (const row of await db.all(`SELECT email FROM users WHERE id IS NULL`)) {
13983
- await db.run(`UPDATE users SET id = ? WHERE email = ?`, `usr_${nanoid20(12)}`, row.email);
14155
+ await db.run(`UPDATE users SET id = ? WHERE email = ?`, `usr_${nanoid18(12)}`, row.email);
13984
14156
  }
13985
14157
  await db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_users_id ON users(id)`);
13986
14158
  await ensureColumn(db, "projects", "owner_user_id", "owner_user_id TEXT");
@@ -14064,7 +14236,7 @@ async function initSchema(db) {
14064
14236
  await db.run(
14065
14237
  `INSERT INTO context_outbox (id, project_id, ordinal, files_json, message, state, created_at)
14066
14238
  VALUES (?, ?, ?, ?, 'migration: legacy display diffs', 'pending', ?)`,
14067
- `ctx_${nanoid20(16)}`,
14239
+ `ctx_${nanoid18(16)}`,
14068
14240
  projectId,
14069
14241
  next.next,
14070
14242
  JSON.stringify(files),
@@ -15898,27 +16070,27 @@ var init_delivery_decision = __esm({
15898
16070
  });
15899
16071
 
15900
16072
  // src/projects/context-read.ts
15901
- import { nanoid as nanoid21 } from "nanoid";
16073
+ import { nanoid as nanoid19 } from "nanoid";
15902
16074
  function requestContextFile(projectId, path, timeoutMs = DEFAULT_TIMEOUT_MS, send = sendToProjectCli) {
15903
16075
  const action = { type: "read_context_file", projectId, path };
15904
- const actionId2 = `rdctx_${nanoid21(16)}`;
16076
+ const actionId2 = `rdctx_${nanoid19(16)}`;
15905
16077
  if (!send(projectId, { type: "dispatch_action", actionId: actionId2, action })) {
15906
16078
  return Promise.reject(new Error("cli_offline"));
15907
16079
  }
15908
16080
  return new Promise((resolve2, reject) => {
15909
16081
  const timer = setTimeout(() => {
15910
- pending2.delete(actionId2);
16082
+ pending.delete(actionId2);
15911
16083
  reject(new Error("cli_timeout"));
15912
16084
  }, timeoutMs);
15913
- pending2.set(actionId2, { resolve: resolve2, reject, timer });
16085
+ pending.set(actionId2, { resolve: resolve2, reject, timer });
15914
16086
  });
15915
16087
  }
15916
- var pending2, DEFAULT_TIMEOUT_MS;
16088
+ var pending, DEFAULT_TIMEOUT_MS;
15917
16089
  var init_context_read = __esm({
15918
16090
  "src/projects/context-read.ts"() {
15919
16091
  "use strict";
15920
16092
  init_ws();
15921
- pending2 = /* @__PURE__ */ new Map();
16093
+ pending = /* @__PURE__ */ new Map();
15922
16094
  DEFAULT_TIMEOUT_MS = 2e4;
15923
16095
  }
15924
16096
  });
@@ -15963,14 +16135,14 @@ async function recordVerdicts(db, taskId2, verdicts) {
15963
16135
  return current;
15964
16136
  }
15965
16137
  function decidePlan(plan, verdicts) {
15966
- const pending4 = plan.items.filter((i) => !(i.id in verdicts)).map((i) => i.id);
16138
+ const pending5 = plan.items.filter((i) => !(i.id in verdicts)).map((i) => i.id);
15967
16139
  const list = plan.items.map((i) => verdicts[i.id]).filter((v) => !!v);
15968
- const approved = pending4.length === 0 && list.every((v) => v.approved);
16140
+ const approved = pending5.length === 0 && list.every((v) => v.approved);
15969
16141
  return {
15970
- complete: pending4.length === 0,
15971
- pending: pending4,
16142
+ complete: pending5.length === 0,
16143
+ pending: pending5,
15972
16144
  approved,
15973
- rework: pending4.length === 0 ? compilePlanRework(plan, list) : ""
16145
+ rework: pending5.length === 0 ? compilePlanRework(plan, list) : ""
15974
16146
  };
15975
16147
  }
15976
16148
  function describeDecision(d) {
@@ -16108,7 +16280,7 @@ var init_project_access = __esm({
16108
16280
  });
16109
16281
 
16110
16282
  // src/projects/regimen-write.ts
16111
- import { nanoid as nanoid22 } from "nanoid";
16283
+ import { nanoid as nanoid20 } from "nanoid";
16112
16284
  function dispatchSetYouTrackConfig(db, projectId, payload, log) {
16113
16285
  const action = {
16114
16286
  type: "set_youtrack_config",
@@ -16116,7 +16288,7 @@ function dispatchSetYouTrackConfig(db, projectId, payload, log) {
16116
16288
  ...payload,
16117
16289
  commitMessage: `chore(youtrack): update YT config (dashboard)`
16118
16290
  };
16119
- const actionId2 = `ytcfg_${nanoid22(16)}`;
16291
+ const actionId2 = `ytcfg_${nanoid20(16)}`;
16120
16292
  const sent = sendToProjectCli(projectId, { type: "dispatch_action", actionId: actionId2, action });
16121
16293
  if (!sent) {
16122
16294
  log.warn({ projectId, actionId: actionId2 }, "dispatchSetYouTrackConfig: CLI not connected");
@@ -16132,7 +16304,7 @@ function dispatchSetRepoDefaultBranch(db, projectId, repoName, defaultBranch, lo
16132
16304
  repoName,
16133
16305
  defaultBranch
16134
16306
  };
16135
- const actionId2 = `repocfg_${nanoid22(16)}`;
16307
+ const actionId2 = `repocfg_${nanoid20(16)}`;
16136
16308
  const sent = sendToProjectCli(projectId, { type: "dispatch_action", actionId: actionId2, action });
16137
16309
  if (!sent) {
16138
16310
  log.warn({ projectId, actionId: actionId2 }, "dispatchSetRepoDefaultBranch: CLI not connected");
@@ -16152,7 +16324,7 @@ function dispatchSetProjectSetting(db, projectId, edits, log, opts) {
16152
16324
  edits,
16153
16325
  commitMessage: opts?.commitMessage ?? "chore(settings): update project settings (dashboard)"
16154
16326
  };
16155
- const actionId2 = `${prefix}${nanoid22(16)}`;
16327
+ const actionId2 = `${prefix}${nanoid20(16)}`;
16156
16328
  if (prefix === "setg_") rememberSettingsDispatch(actionId2, edits);
16157
16329
  const sent = sendToProjectCli(projectId, { type: "dispatch_action", actionId: actionId2, action });
16158
16330
  if (!sent) {
@@ -16164,7 +16336,7 @@ function dispatchSetProjectSetting(db, projectId, edits, log, opts) {
16164
16336
  }
16165
16337
  function dispatchSetBudgetSource(db, projectId, source, log) {
16166
16338
  const action = { type: "set_budget_source", projectId, source };
16167
- const actionId2 = `bsrc_${nanoid22(16)}`;
16339
+ const actionId2 = `bsrc_${nanoid20(16)}`;
16168
16340
  const sent = sendToProjectCli(projectId, { type: "dispatch_action", actionId: actionId2, action });
16169
16341
  if (!sent) log.warn({ projectId, actionId: actionId2 }, "dispatchSetBudgetSource: CLI not connected");
16170
16342
  else log.info({ projectId, actionId: actionId2, kind: source.kind }, "dispatchSetBudgetSource: sent to CLI");
@@ -16186,7 +16358,7 @@ function dispatchEditTrackerIssue(db, projectId, fields, log) {
16186
16358
  ...fields.title !== void 0 ? { title: fields.title } : {},
16187
16359
  ...fields.body !== void 0 ? { body: fields.body } : {}
16188
16360
  };
16189
- const actionId2 = `tedit_${nanoid22(16)}`;
16361
+ const actionId2 = `tedit_${nanoid20(16)}`;
16190
16362
  const sent = sendToProjectCli(projectId, { type: "dispatch_action", actionId: actionId2, action });
16191
16363
  if (!sent) {
16192
16364
  log.warn({ projectId, actionId: actionId2, issueKey: fields.issueKey }, "dispatchEditTrackerIssue: CLI not connected");
@@ -16206,21 +16378,21 @@ var init_regimen_write = __esm({
16206
16378
  });
16207
16379
 
16208
16380
  // src/tasks/context-outbox.ts
16209
- import { nanoid as nanoid23 } from "nanoid";
16381
+ import { nanoid as nanoid21 } from "nanoid";
16210
16382
  async function nextOrdinal(db, projectId) {
16211
16383
  const row = await db.get(`SELECT COALESCE(MAX(ordinal), 0) AS max FROM context_outbox WHERE project_id = ?`, projectId);
16212
16384
  return row.max + 1;
16213
16385
  }
16214
16386
  async function enqueueContextWrite(db, projectId, files, message, log) {
16215
16387
  if (files.length === 0) return;
16216
- const id = `ctx_${nanoid23(16)}`;
16388
+ const id = `ctx_${nanoid21(16)}`;
16217
16389
  await db.run(`INSERT INTO context_outbox (id, project_id, ordinal, files_json, message, state, created_at)
16218
16390
  VALUES (?, ?, ?, ?, ?, 'pending', ?)`, id, projectId, await nextOrdinal(db, projectId), JSON.stringify(files), message, Date.now());
16219
16391
  await dispatchNextContextWrite(db, projectId, log);
16220
16392
  }
16221
16393
  async function enqueueAdrPromote(db, projectId, taskKey, slugs, log, source = "task") {
16222
16394
  if (slugs.length === 0) return;
16223
- const id = `ctx_${nanoid23(16)}`;
16395
+ const id = `ctx_${nanoid21(16)}`;
16224
16396
  await db.run(`INSERT INTO context_outbox (id, project_id, ordinal, files_json, message, state, kind, meta_json, created_at)
16225
16397
  VALUES (?, ?, ?, ?, ?, 'pending', 'adr_promote', ?, ?)`, id, projectId, await nextOrdinal(db, projectId), JSON.stringify(slugs), `adr(${taskKey}): promote ${slugs.join(", ")}`, JSON.stringify({ taskKey, source }), Date.now());
16226
16398
  await dispatchNextContextWrite(db, projectId, log);
@@ -16625,10 +16797,10 @@ __export(cluster_journal_exports, {
16625
16797
  clusterJournal: () => clusterJournal,
16626
16798
  recordClusterEvent: () => recordClusterEvent
16627
16799
  });
16628
- import { nanoid as nanoid24 } from "nanoid";
16800
+ import { nanoid as nanoid22 } from "nanoid";
16629
16801
  async function recordClusterEvent(db, projectId, anchorKey, kind, detail) {
16630
16802
  await engineStore(db).clusters.appendEvent(
16631
- `ce_${nanoid24(12)}`,
16803
+ `ce_${nanoid22(12)}`,
16632
16804
  projectId,
16633
16805
  anchorKey,
16634
16806
  kind,
@@ -16696,7 +16868,7 @@ __export(cluster_recovery_exports, {
16696
16868
  reconcileClusterRecoveries: () => reconcileClusterRecoveries,
16697
16869
  startClusterRecovery: () => startClusterRecovery
16698
16870
  });
16699
- import { nanoid as nanoid25 } from "nanoid";
16871
+ import { nanoid as nanoid23 } from "nanoid";
16700
16872
  async function startClusterRecovery(db, task, instruction, log, opts = {}) {
16701
16873
  const anchorKey = await clusterAnchorFor(db, task);
16702
16874
  const cluster = await getCluster(db, task.projectId, anchorKey);
@@ -17054,7 +17226,7 @@ async function dropCluster(db, task, cluster, reason, log, opts = {}) {
17054
17226
  if (carrier) {
17055
17227
  const now = Date.now();
17056
17228
  for (const t of await loadCreatePRTargets(db, carrier)) {
17057
- const id = `wa_${nanoid25(12)}`;
17229
+ const id = `wa_${nanoid23(12)}`;
17058
17230
  const action = {
17059
17231
  type: "close_pr",
17060
17232
  taskId: carrier,
@@ -17173,7 +17345,7 @@ __export(tasks_exports, {
17173
17345
  queueTrackerStatusMove: () => queueTrackerStatusMove,
17174
17346
  tasksRoutes: () => tasksRoutes
17175
17347
  });
17176
- import { nanoid as nanoid26 } from "nanoid";
17348
+ import { nanoid as nanoid24 } from "nanoid";
17177
17349
  async function queueTrackerStatusMove(db, log, taskId2, projectId, newStatus, manual = false) {
17178
17350
  const projectRow = await db.get(`SELECT automation_level as automationLevel FROM projects WHERE id = ?`, projectId);
17179
17351
  const writes = await loadYouTrackWrites(
@@ -17188,7 +17360,7 @@ async function queueTrackerStatusMove(db, log, taskId2, projectId, newStatus, ma
17188
17360
  newStatus,
17189
17361
  ...manual ? { manual: true } : {}
17190
17362
  };
17191
- const statusActionId = `wa_${nanoid26(12)}`;
17363
+ const statusActionId = `wa_${nanoid24(12)}`;
17192
17364
  await engineStore(db).actions.appendPlan(
17193
17365
  taskId2,
17194
17366
  [{ id: statusActionId, kind: "tracker_status", payloadJson: JSON.stringify(action) }],
@@ -18037,7 +18209,7 @@ async function tasksRoutes(app, deps) {
18037
18209
  }
18038
18210
  await ingestComment(db, {
18039
18211
  taskId: taskId2,
18040
- trackerCommentId: `${DASHBOARD_PLACEHOLDER_PREFIX}${nanoid26(8)}`,
18212
+ trackerCommentId: `${DASHBOARD_PLACEHOLDER_PREFIX}${nanoid24(8)}`,
18041
18213
  author: session.email,
18042
18214
  body: req.body.comment,
18043
18215
  origin: "dashboard",
@@ -18195,7 +18367,7 @@ async function tasksRoutes(app, deps) {
18195
18367
  if (!task) return reply.status(404).send({ error: "task_not_found" });
18196
18368
  const ingest = await ingestComment(db, {
18197
18369
  taskId: taskId2,
18198
- trackerCommentId: `${DASHBOARD_PLACEHOLDER_PREFIX}${nanoid26(8)}`,
18370
+ trackerCommentId: `${DASHBOARD_PLACEHOLDER_PREFIX}${nanoid24(8)}`,
18199
18371
  author: session.email,
18200
18372
  body: req.body.body,
18201
18373
  origin: "dashboard",
@@ -18264,7 +18436,7 @@ ${req.body.body}
18264
18436
  // comments.jsonl; YouTrack ignores it (the token owner authors).
18265
18437
  author: session.email
18266
18438
  };
18267
- mirrorActionId = `wa_${nanoid26(12)}`;
18439
+ mirrorActionId = `wa_${nanoid24(12)}`;
18268
18440
  await engineStore(db).actions.appendPlan(
18269
18441
  taskId2,
18270
18442
  [{ id: mirrorActionId, kind: "tracker_comment", payloadJson: JSON.stringify(action) }],
@@ -18455,7 +18627,7 @@ __export(observer_exports, {
18455
18627
  stampTranche: () => stampTranche,
18456
18628
  startStagedBatch: () => startStagedBatch
18457
18629
  });
18458
- import { nanoid as nanoid27 } from "nanoid";
18630
+ import { nanoid as nanoid25 } from "nanoid";
18459
18631
  async function loadContextRepo2(db, projectId) {
18460
18632
  const row = await db.get(
18461
18633
  `SELECT context_repo_json AS json FROM projects WHERE id = ?`,
@@ -18500,7 +18672,7 @@ async function enqueueObserverJob(db, batch, log, opts = {}) {
18500
18672
  return spec.jobId;
18501
18673
  }
18502
18674
  async function stampTranche(db, taskIds) {
18503
- const trancheId = `tr_${nanoid27(12)}`;
18675
+ const trancheId = `tr_${nanoid25(12)}`;
18504
18676
  for (const id of taskIds) {
18505
18677
  await engineStore(db).tasks.stampTrancheMembership(id, trancheId);
18506
18678
  }
@@ -18926,7 +19098,7 @@ var init_queue_view = __esm({
18926
19098
  });
18927
19099
 
18928
19100
  // src/tasks/cluster-order.ts
18929
- import { nanoid as nanoid28 } from "nanoid";
19101
+ import { nanoid as nanoid26 } from "nanoid";
18930
19102
  function chainLess2(a, b) {
18931
19103
  const aq = a.queuePosition ?? Number.MAX_SAFE_INTEGER;
18932
19104
  const bq = b.queuePosition ?? Number.MAX_SAFE_INTEGER;
@@ -19032,7 +19204,7 @@ function dispatchProposedLinks(db, log, projectId, anchorKey, addLinks, memberKe
19032
19204
  links.push({ sourceIssueKey: source, rel: "depends_on", targetIssueKey: target });
19033
19205
  }
19034
19206
  if (links.length === 0) return;
19035
- const actionId2 = `clnk_${nanoid28(16)}`;
19207
+ const actionId2 = `clnk_${nanoid26(16)}`;
19036
19208
  const action = { type: "apply_tracker_links", draftId: actionId2, projectId, links };
19037
19209
  const sent = sendToProjectCli(projectId, { type: "dispatch_action", actionId: actionId2, action });
19038
19210
  if (sent) {
@@ -19058,9 +19230,9 @@ var init_cluster_order = __esm({
19058
19230
  });
19059
19231
 
19060
19232
  // src/prompts/prompt-cache.ts
19061
- import { createHash as createHash3 } from "node:crypto";
19233
+ import { createHash as createHash4 } from "node:crypto";
19062
19234
  function contentSha3(content) {
19063
- return createHash3("sha1").update(content, "utf-8").digest("hex");
19235
+ return createHash4("sha1").update(content, "utf-8").digest("hex");
19064
19236
  }
19065
19237
  function getProjectPromptTemplate(projectId, role) {
19066
19238
  return cache4.get(projectId)?.get(role)?.content ?? null;
@@ -21020,7 +21192,7 @@ __export(cluster_finalize_exports, {
21020
21192
  maybeFinalizeCluster: () => maybeFinalizeCluster,
21021
21193
  recordClusterPrCreated: () => recordClusterPrCreated
21022
21194
  });
21023
- import { nanoid as nanoid29 } from "nanoid";
21195
+ import { nanoid as nanoid27 } from "nanoid";
21024
21196
  async function isFullAutomation2(db, projectId) {
21025
21197
  const row = await db.get(`SELECT automation_level as automationLevel FROM projects WHERE id = ?`, projectId);
21026
21198
  return row?.automationLevel === "full";
@@ -21041,7 +21213,7 @@ async function loadMergePolicy2(db, projectId) {
21041
21213
  };
21042
21214
  }
21043
21215
  async function enqueueAndDispatch(db, taskId2, action, log) {
21044
- const id = `wa_${nanoid29(12)}`;
21216
+ const id = `wa_${nanoid27(12)}`;
21045
21217
  await engineStore(db).actions.appendPlan(
21046
21218
  taskId2,
21047
21219
  [{ id, kind: action.type, payloadJson: JSON.stringify(action) }],
@@ -21771,7 +21943,7 @@ __export(tracking_commands_exports, {
21771
21943
  handleUpdatePr: () => handleUpdatePr,
21772
21944
  loadCreatePRTargets: () => loadCreatePRTargets
21773
21945
  });
21774
- import { nanoid as nanoid30 } from "nanoid";
21946
+ import { nanoid as nanoid28 } from "nanoid";
21775
21947
  async function loadCreatePRTargets(db, taskId2) {
21776
21948
  const rows = await db.all(`SELECT payload_json AS payloadJson, result_json AS resultJson
21777
21949
  FROM write_actions
@@ -21871,7 +22043,7 @@ async function handleApprove(db, task, log) {
21871
22043
  const now = Date.now();
21872
22044
  const ids = [];
21873
22045
  for (const t of targets) {
21874
- const id = `wa_${nanoid30(12)}`;
22046
+ const id = `wa_${nanoid28(12)}`;
21875
22047
  const action = {
21876
22048
  type: "merge_pr",
21877
22049
  taskId: chainTaskId,
@@ -22059,7 +22231,7 @@ async function handleClusterReject(db, task, comment, log) {
22059
22231
  if (carrier) {
22060
22232
  const now = Date.now();
22061
22233
  for (const t of await loadCreatePRTargets(db, carrier)) {
22062
- const id = `wa_${nanoid30(12)}`;
22234
+ const id = `wa_${nanoid28(12)}`;
22063
22235
  const action = {
22064
22236
  type: "close_pr",
22065
22237
  taskId: carrier,
@@ -22084,7 +22256,7 @@ async function handleClusterReject(db, task, comment, log) {
22084
22256
  if (deliveredRepos.size > 0) {
22085
22257
  const now = Date.now();
22086
22258
  for (const repoName of deliveredRepos) {
22087
- const id = `wa_${nanoid30(12)}`;
22259
+ const id = `wa_${nanoid28(12)}`;
22088
22260
  const action = {
22089
22261
  type: "rewrite_cluster_branch",
22090
22262
  taskId: owner.id,
@@ -22658,7 +22830,7 @@ __export(lifecycle_exports, {
22658
22830
  taskRunsDirectImplement: () => taskRunsDirectImplement,
22659
22831
  triggerRework: () => triggerRework
22660
22832
  });
22661
- import { createHash as createHash4 } from "node:crypto";
22833
+ import { createHash as createHash5 } from "node:crypto";
22662
22834
  async function reconcileStuckTasks(db, log) {
22663
22835
  let recovered = 0;
22664
22836
  for (const task of await listTasks(db)) {
@@ -23253,7 +23425,7 @@ async function maybeAwaitingAnswer(db, task, log, blockingQuestions = []) {
23253
23425
  if (blocking.length === 0) return false;
23254
23426
  const now = Date.now();
23255
23427
  for (let i = 0; i < blocking.length; i++) {
23256
- const digest2 = createHash4("sha256").update(blocking[i]).digest("hex").slice(0, 12);
23428
+ const digest2 = createHash5("sha256").update(blocking[i]).digest("hex").slice(0, 12);
23257
23429
  await ingestComment(db, {
23258
23430
  taskId: task.id,
23259
23431
  trackerCommentId: `agent:${task.phase.toLowerCase()}:${i}:${digest2}`,
@@ -24645,25 +24817,25 @@ async function requestGrantsFrom(spec, recipient, bindingVersion, log, timeoutMs
24645
24817
  const requestId2 = msg.requestId;
24646
24818
  return await new Promise((resolve2) => {
24647
24819
  const timer = setTimeout(() => {
24648
- pending3.delete(requestId2);
24820
+ pending2.delete(requestId2);
24649
24821
  log.warn({ projectId: spec.projectId, jobId: spec.jobId, sandboxId: recipient.sandboxId }, "credential grants: CLI did not answer in time");
24650
24822
  resolve2(null);
24651
24823
  }, timeoutMs);
24652
- pending3.set(requestId2, {
24824
+ pending2.set(requestId2, {
24653
24825
  projectId: spec.projectId,
24654
24826
  jobId: spec.jobId,
24655
24827
  sandboxId: recipient.sandboxId,
24656
24828
  holder,
24657
24829
  resolve: (answer) => {
24658
24830
  clearTimeout(timer);
24659
- pending3.delete(requestId2);
24831
+ pending2.delete(requestId2);
24660
24832
  resolve2(answer);
24661
24833
  }
24662
24834
  });
24663
24835
  const sent = holder.kind === "provider" ? sendToAgentLoginProvider(holder.ownerKey, holder.providerId, msg) : sendToProjectCli(spec.projectId, msg);
24664
24836
  if (!sent) {
24665
24837
  clearTimeout(timer);
24666
- pending3.delete(requestId2);
24838
+ pending2.delete(requestId2);
24667
24839
  resolve2(null);
24668
24840
  }
24669
24841
  });
@@ -24775,7 +24947,7 @@ async function sweepExpiredGrants(db, now = Date.now()) {
24775
24947
  const { changes } = await db.run(`DELETE FROM credential_grants WHERE not_after <= ?`, now);
24776
24948
  return changes;
24777
24949
  }
24778
- var GRANT_REQUEST_TIMEOUT_MS, pending3, LATE_ANSWER_TTL_MS, lateAnswers;
24950
+ var GRANT_REQUEST_TIMEOUT_MS, pending2, LATE_ANSWER_TTL_MS, lateAnswers;
24779
24951
  var init_credential_grants = __esm({
24780
24952
  "src/tracker/credential-grants.ts"() {
24781
24953
  "use strict";
@@ -24786,7 +24958,7 @@ var init_credential_grants = __esm({
24786
24958
  init_ws();
24787
24959
  init_sandbox_keys();
24788
24960
  GRANT_REQUEST_TIMEOUT_MS = 6e3;
24789
- pending3 = /* @__PURE__ */ new Map();
24961
+ pending2 = /* @__PURE__ */ new Map();
24790
24962
  LATE_ANSWER_TTL_MS = 6e4;
24791
24963
  lateAnswers = /* @__PURE__ */ new Map();
24792
24964
  }
@@ -24840,7 +25012,7 @@ init_regimen_config();
24840
25012
  // src/projects/cred-updates.ts
24841
25013
  init_dist();
24842
25014
  init_ws();
24843
- import { nanoid as nanoid31 } from "nanoid";
25015
+ import { nanoid as nanoid29 } from "nanoid";
24844
25016
  async function latestCredUpdates(db, projectId) {
24845
25017
  const rows = await db.all(
24846
25018
  `SELECT id, kind, state, detail, updated_at as updatedAt
@@ -24865,6 +25037,7 @@ async function latestCredUpdates(db, projectId) {
24865
25037
 
24866
25038
  // src/application/read-model.ts
24867
25039
  init_ws();
25040
+ init_farm2();
24868
25041
  init_queue_view();
24869
25042
  init_state_machine();
24870
25043
  init_delivery_decision();
@@ -25076,7 +25249,7 @@ function configProposalRow(stored) {
25076
25249
  }
25077
25250
 
25078
25251
  // src/config-proposals/state-machine.ts
25079
- import { nanoid as nanoid32 } from "nanoid";
25252
+ import { nanoid as nanoid30 } from "nanoid";
25080
25253
  function rowToProposal(raw) {
25081
25254
  return {
25082
25255
  id: raw["id"],
@@ -25101,7 +25274,7 @@ var PROPOSAL_COLUMNS = `id, project_id as projectId, user_input as userInput, st
25101
25274
  created_at as createdAt, updated_at as updatedAt`;
25102
25275
  var PROPOSAL_SELECT = `SELECT ${PROPOSAL_COLUMNS} FROM config_proposals`;
25103
25276
  async function createProposal(db, projectId, userInput) {
25104
- const id = `cfp_${nanoid32(16)}`;
25277
+ const id = `cfp_${nanoid30(16)}`;
25105
25278
  const now = Date.now();
25106
25279
  await db.run(
25107
25280
  `INSERT INTO config_proposals
@@ -25803,7 +25976,7 @@ async function projectDetailOf(db, projectId, raw) {
25803
25976
  actionErrors: getProjectActions(projectId)?.actionErrors ?? []
25804
25977
  };
25805
25978
  }
25806
- function createReadModel(db) {
25979
+ function createReadModel(db, options = {}) {
25807
25980
  const SUBMISSION_COLUMNS = `${DRAFT_COLUMNS},
25808
25981
  (SELECT COUNT(*) FROM draft_tasks c WHERE c.parent_draft_id = draft_tasks.id) as childCount,
25809
25982
  EXISTS (SELECT 1 FROM tasks t
@@ -26495,6 +26668,35 @@ function createReadModel(db) {
26495
26668
  const p = await getPresence(db, { email: viewer.email ?? "", userId: viewer.subjectId });
26496
26669
  return presenceRowOf(p, new Set(viewer.projectIds));
26497
26670
  },
26671
+ // --- the account's fleet (DEV-927) ---
26672
+ async listFarms(viewer) {
26673
+ const isOperator = viewer.email !== null && (options.isOperator?.(viewer.email) ?? false);
26674
+ return await listFarms(db, { email: viewer.email ?? "", userId: viewer.subjectId, isSuperAdmin: isOperator });
26675
+ },
26676
+ async listProjectDeliverables(projectId) {
26677
+ const rows = await db.all(
26678
+ `SELECT id, tracker_issue_key AS "taskKey", title, phase, updated_at AS "updatedAt"
26679
+ FROM tasks
26680
+ WHERE project_id = ? AND (archived IS NULL OR archived = 0)
26681
+ AND phase IN ('TRACKING', 'AWAITING_CLUSTER', 'DONE', 'NEEDS_MANUAL')
26682
+ ORDER BY CASE WHEN phase IN ('TRACKING', 'AWAITING_CLUSTER') THEN 0 ELSE 1 END,
26683
+ updated_at DESC
26684
+ LIMIT 50`,
26685
+ projectId
26686
+ );
26687
+ const out = [];
26688
+ for (const r of rows) {
26689
+ const byRepo = await loadTaskBranchesByRepo(db, r.id);
26690
+ const repos = Object.entries(byRepo).map(([repoName, b]) => ({
26691
+ repoName,
26692
+ branchName: b.branchName,
26693
+ baseRef: b.baseRef
26694
+ }));
26695
+ if (repos.length === 0) continue;
26696
+ out.push({ taskKey: r.taskKey, title: r.title, phase: r.phase, updatedAt: Number(r.updatedAt), repos });
26697
+ }
26698
+ return out;
26699
+ },
26498
26700
  async readNotificationSettings(subjectId) {
26499
26701
  const user = await db.get(`SELECT 1 as hit FROM users WHERE id = ?`, subjectId);
26500
26702
  if (!user) return null;
@@ -26635,7 +26837,7 @@ function createAuthorizer(db, config) {
26635
26837
  // src/jobs/apply-result.ts
26636
26838
  init_apply_overrides();
26637
26839
  init_publish_delivery();
26638
- import { nanoid as nanoid33 } from "nanoid";
26840
+ import { nanoid as nanoid31 } from "nanoid";
26639
26841
 
26640
26842
  // src/drafts/result-handler.ts
26641
26843
  init_dist();
@@ -27251,7 +27453,7 @@ async function applyJobResult(fx, job, result) {
27251
27453
  size_bytes = excluded.size_bytes,
27252
27454
  source_job_id = excluded.source_job_id,
27253
27455
  created_at = excluded.created_at`,
27254
- `art_${nanoid33(16)}`,
27456
+ `art_${nanoid31(16)}`,
27255
27457
  job.taskId,
27256
27458
  a.path,
27257
27459
  encoding,
@@ -28018,7 +28220,7 @@ init_state_machine2();
28018
28220
  // src/drafts/publish.ts
28019
28221
  init_ws();
28020
28222
  init_state_machine2();
28021
- import { nanoid as nanoid34 } from "nanoid";
28223
+ import { nanoid as nanoid32 } from "nanoid";
28022
28224
  function firstNonBlank(...vals) {
28023
28225
  for (const v of vals) {
28024
28226
  if (v != null && v.trim() !== "") return v;
@@ -28026,7 +28228,7 @@ function firstNonBlank(...vals) {
28026
28228
  return "";
28027
28229
  }
28028
28230
  async function dispatchPublish(db, draft, targetStatus, log) {
28029
- const publishActionId = `dpub_${nanoid34(16)}`;
28231
+ const publishActionId = `dpub_${nanoid32(16)}`;
28030
28232
  const title = firstNonBlank(draft.editedTitle, draft.generatedTitle);
28031
28233
  const body = firstNonBlank(draft.editedBody, draft.generatedBody);
28032
28234
  const project = await db.get(`SELECT youtrack_project_key as youtrackProjectKey FROM projects WHERE id = ?`, draft.projectId);
@@ -28172,7 +28374,7 @@ async function maybeAdvancePackage(db, parentDraftId, log) {
28172
28374
  log.info({ draftId: parentDraftId }, "package published (no links) \u2192 DONE");
28173
28375
  return;
28174
28376
  }
28175
- const linkActionId = `dpkg_${nanoid34(16)}`;
28377
+ const linkActionId = `dpkg_${nanoid32(16)}`;
28176
28378
  const action = {
28177
28379
  type: "apply_tracker_links",
28178
28380
  draftId: parentDraftId,
@@ -28252,7 +28454,85 @@ async function discardDraft(db, draft, log) {
28252
28454
  init_dist2();
28253
28455
  init_regimen_write();
28254
28456
  init_ws();
28255
- init_provision();
28457
+
28458
+ // src/farm/provision.ts
28459
+ init_ws();
28460
+ init_pool_spec();
28461
+ import { nanoid as nanoid33 } from "nanoid";
28462
+ var pending3 = /* @__PURE__ */ new Map();
28463
+ function awaitResult(requestId2, send, timeoutMs) {
28464
+ if (!send()) return Promise.reject(new Error("farm_offline"));
28465
+ return new Promise((resolve2, reject) => {
28466
+ const timer = setTimeout(() => {
28467
+ pending3.delete(requestId2);
28468
+ reject(new Error("farm_timeout"));
28469
+ }, timeoutMs);
28470
+ pending3.set(requestId2, { resolve: resolve2, reject, timer });
28471
+ });
28472
+ }
28473
+ function requestProvision(farmId, projectId, sealedBundle, timeoutMs) {
28474
+ const requestId2 = nanoid33(16);
28475
+ return awaitResult(
28476
+ requestId2,
28477
+ () => sendToFarm(farmId, { type: "provision_project", requestId: requestId2, projectId, sealedBundle }),
28478
+ timeoutMs
28479
+ );
28480
+ }
28481
+ function requestDelete(farmId, projectId, timeoutMs) {
28482
+ const requestId2 = nanoid33(16);
28483
+ return awaitResult(
28484
+ requestId2,
28485
+ () => sendToFarm(farmId, { type: "delete_project", requestId: requestId2, projectId }),
28486
+ timeoutMs
28487
+ );
28488
+ }
28489
+ async function requestFarmPoolSync(db, farmId) {
28490
+ const accounts = await computeFarmPoolSpec(db, farmId);
28491
+ sendToFarm(farmId, buildSyncWorkerPoolMessage(accounts));
28492
+ }
28493
+
28494
+ // src/runner/provision.ts
28495
+ init_ws();
28496
+ import { nanoid as nanoid34 } from "nanoid";
28497
+ var pending4 = /* @__PURE__ */ new Map();
28498
+ function awaitResult2(requestId2, send, timeoutMs) {
28499
+ if (!send()) return Promise.reject(new Error("runner_offline"));
28500
+ return new Promise((resolve2, reject) => {
28501
+ const timer = setTimeout(() => {
28502
+ pending4.delete(requestId2);
28503
+ reject(new Error("runner_timeout"));
28504
+ }, timeoutMs);
28505
+ pending4.set(requestId2, { resolve: resolve2, reject, timer });
28506
+ });
28507
+ }
28508
+ function requestGhRunnersProvision(runnerId, projectId, sealedBundle, timeoutMs) {
28509
+ const requestId2 = nanoid34(16);
28510
+ return awaitResult2(
28511
+ requestId2,
28512
+ () => sendToRunner(runnerId, { type: "provision_gh_runners", requestId: requestId2, projectId, sealedBundle }),
28513
+ timeoutMs
28514
+ );
28515
+ }
28516
+ function requestGhRunnersDelete(runnerId, projectId, sealedBundle, timeoutMs) {
28517
+ const requestId2 = nanoid34(16);
28518
+ return awaitResult2(
28519
+ requestId2,
28520
+ () => sendToRunner(runnerId, {
28521
+ type: "delete_gh_runners",
28522
+ requestId: requestId2,
28523
+ projectId,
28524
+ ...sealedBundle ? { sealedBundle } : {}
28525
+ }),
28526
+ timeoutMs
28527
+ );
28528
+ }
28529
+
28530
+ // src/application/write-model.ts
28531
+ init_farm2();
28532
+ init_runner2();
28533
+ init_init_status();
28534
+ init_sandbox_reg_tokens();
28535
+ init_abuse_throttle();
28256
28536
 
28257
28537
  // src/projects/forget.ts
28258
28538
  init_legacy_cred_bundles();
@@ -28296,8 +28576,57 @@ async function deleteProjectData(db, projectId) {
28296
28576
  });
28297
28577
  }
28298
28578
 
28579
+ // src/runner/bindings.ts
28580
+ function parseGhRunnerBindings(json) {
28581
+ if (!json) return [];
28582
+ try {
28583
+ const parsed = JSON.parse(json);
28584
+ return Array.isArray(parsed.bindings) ? parsed.bindings : [];
28585
+ } catch {
28586
+ return [];
28587
+ }
28588
+ }
28589
+ function serialize(bindings) {
28590
+ return JSON.stringify({ bindings });
28591
+ }
28592
+ async function upsertGhRunnerBinding(db, projectId, binding) {
28593
+ await db.tx(async (tx) => {
28594
+ const row = await tx.get(
28595
+ `SELECT gh_runner_bindings_json as bindingsJson FROM projects WHERE id = ?`,
28596
+ projectId
28597
+ );
28598
+ if (!row) return;
28599
+ const bindings = parseGhRunnerBindings(row.bindingsJson).filter(
28600
+ (b) => !(b.owner === binding.owner && b.repo === binding.repo)
28601
+ );
28602
+ bindings.push(binding);
28603
+ await tx.run(
28604
+ `UPDATE projects SET gh_runner_bindings_json = ? WHERE id = ?`,
28605
+ serialize(bindings),
28606
+ projectId
28607
+ );
28608
+ });
28609
+ }
28610
+ async function removeGhRunnerBinding(db, projectId, owner, repo) {
28611
+ return db.tx(async (tx) => {
28612
+ const row = await tx.get(
28613
+ `SELECT gh_runner_bindings_json as bindingsJson FROM projects WHERE id = ?`,
28614
+ projectId
28615
+ );
28616
+ if (!row) return false;
28617
+ const bindings = parseGhRunnerBindings(row.bindingsJson);
28618
+ const kept = bindings.filter((b) => !(b.owner === owner && b.repo === repo));
28619
+ if (kept.length === bindings.length) return false;
28620
+ await tx.run(
28621
+ `UPDATE projects SET gh_runner_bindings_json = ? WHERE id = ?`,
28622
+ serialize(kept),
28623
+ projectId
28624
+ );
28625
+ return true;
28626
+ });
28627
+ }
28628
+
28299
28629
  // src/application/write-model.ts
28300
- init_bindings();
28301
28630
  init_tracking_commands();
28302
28631
 
28303
28632
  // src/config-proposals/apply.ts
@@ -28373,10 +28702,27 @@ function conflict(reason) {
28373
28702
  function stale() {
28374
28703
  return { ok: false, refusal: "stale_revision" };
28375
28704
  }
28705
+ function notFound() {
28706
+ return { ok: false, refusal: "not_found" };
28707
+ }
28708
+ var FARM_PROVISION_TIMEOUT_MS = 6e4;
28709
+ var GH_RUNNER_RELAY_TIMEOUT_MS = 12e4;
28710
+ function nodeUnavailable(capability, err) {
28711
+ const reason = err instanceof Error ? err.message : `${capability}_error`;
28712
+ const what = capability === "farm" ? "The farm" : "The runner host";
28713
+ return {
28714
+ ok: false,
28715
+ refusal: "capability_unavailable",
28716
+ capability,
28717
+ message: reason === `${capability}_timeout` ? `${what} did not answer in time. Try again once it is responsive.` : reason === `${capability}_offline` ? `${what} is offline. Start it and try again.` : `${what} could not be reached for this. Try again in a moment.`,
28718
+ retryable: true
28719
+ };
28720
+ }
28376
28721
  function taskState(row) {
28377
28722
  return { ...row, revision: String(row.updatedAt) };
28378
28723
  }
28379
- function createWriteModel(db, log) {
28724
+ var WORKER_REG_TOKEN_MINTS = createAbuseThrottle({ windowMs: 60 * 6e4, max: 30 });
28725
+ function createWriteModel(db, log, hooks = {}) {
28380
28726
  async function readTaskRow(projectId, key) {
28381
28727
  const row = await db.get(
28382
28728
  `SELECT project_id as projectId, tracker_issue_key as key, phase,
@@ -28451,6 +28797,13 @@ function createWriteModel(db, log) {
28451
28797
  if (sandboxId !== null) sendToSandbox(sandboxId, { type: "pause_job", jobId: stored.sourceJobId });
28452
28798
  await cancelActiveJobs(db, `config::${stored.id}`);
28453
28799
  }
28800
+ function isOperator(actor) {
28801
+ return actor.email !== null && (hooks.isOperator?.(actor.email) ?? false);
28802
+ }
28803
+ async function ghRunnerHost(runnerId, actor) {
28804
+ const owns = isOperator(actor) ? await db.get(`SELECT id FROM runners WHERE id = ?`, runnerId) !== void 0 : await viewerOwnsRunner(db, { email: actor.email ?? "", userId: actor.subjectId }, runnerId);
28805
+ return owns ? ok(true) : notFound();
28806
+ }
28454
28807
  return {
28455
28808
  async readTask(projectId, taskKey) {
28456
28809
  const row = await readTaskRow(projectId, taskKey);
@@ -28922,6 +29275,153 @@ function createWriteModel(db, log) {
28922
29275
  * daemon behind it. The Context Repo remote is not touched: what the
28923
29276
  * pipeline wrote about the work outlives the record of the project.
28924
29277
  */
29278
+ // --- registration and the account's fleet (DEV-927) ---
29279
+ async createProject({ project, owner }) {
29280
+ const trackerProvider = project.trackerProvider;
29281
+ const inserted = await db.tx(async (tx) => {
29282
+ const existing = await tx.get(`SELECT id FROM projects WHERE id = ?`, project.projectId);
29283
+ if (existing) return conflict("project_already_exists");
29284
+ const now = Date.now();
29285
+ await tx.run(
29286
+ `INSERT INTO projects (id, name, automation_level, youtrack_project_key, tracker_provider,
29287
+ repos_json, owner_email, owner_user_id, language_json,
29288
+ provisioning_status, context_repo_json, provisioned_at, provisioning_error, created_at)
29289
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'provisioned', ?, ?, NULL, ?)`,
29290
+ project.projectId,
29291
+ project.name,
29292
+ project.automationLevel,
29293
+ project.trackerProjectKey,
29294
+ trackerProvider,
29295
+ JSON.stringify(
29296
+ project.repos.map((r) => ({
29297
+ name: r.name,
29298
+ host: r.host,
29299
+ owner: r.owner,
29300
+ repoName: r.repoName,
29301
+ ...r.defaultBranch ? { defaultBranch: r.defaultBranch } : {},
29302
+ ...r.mountAs ? { mountAs: r.mountAs } : {},
29303
+ ...r.mirror ? { mirror: { host: r.mirror.host, owner: r.mirror.owner, repoName: r.mirror.repoName } } : {}
29304
+ }))
29305
+ ),
29306
+ owner.email,
29307
+ owner.subjectId,
29308
+ project.language === null ? null : JSON.stringify(project.language),
29309
+ JSON.stringify(project.contextRepo),
29310
+ now,
29311
+ now
29312
+ );
29313
+ return ok({ projectId: project.projectId });
29314
+ });
29315
+ if (!inserted.ok) return inserted;
29316
+ await beginInitTracking(db, project.projectId, "registered");
29317
+ log.info({ projectId: project.projectId, contextRepo: project.contextRepo }, "project registered, Context Repo provisioned by the client");
29318
+ hooks.projectRegistered?.({
29319
+ projectId: project.projectId,
29320
+ name: project.name,
29321
+ automationLevel: project.automationLevel,
29322
+ ownerEmail: owner.email,
29323
+ repoCount: project.repos.length
29324
+ });
29325
+ return inserted;
29326
+ },
29327
+ async provisionOnFarm({ farmId, projectId, sealedBundle, actor }) {
29328
+ const usable = isOperator(actor) ? await db.get(`SELECT id FROM farms WHERE id = ?`, farmId) !== void 0 : await viewerOwnsFarm(db, { email: actor.email ?? "", userId: actor.subjectId }, farmId);
29329
+ if (!usable) return notFound();
29330
+ await beginInitTracking(db, projectId, "farm_provisioning");
29331
+ let result;
29332
+ try {
29333
+ result = await requestProvision(farmId, projectId, sealedBundle, FARM_PROVISION_TIMEOUT_MS);
29334
+ } catch (err) {
29335
+ const reason = err instanceof Error ? err.message : "provision_failed";
29336
+ await recordInitError(
29337
+ db,
29338
+ projectId,
29339
+ reason === "farm_offline" ? "the hosting farm is offline" : reason === "farm_timeout" ? "the hosting farm did not respond in time" : `farm provisioning failed: ${reason}`
29340
+ );
29341
+ return nodeUnavailable("farm", err);
29342
+ }
29343
+ if (result.ok) {
29344
+ await db.run(`UPDATE projects SET farm_id = ? WHERE id = ?`, farmId, projectId);
29345
+ await advanceInitPhase(db, projectId, "daemon_starting");
29346
+ requestFarmPoolSync(db, farmId).catch(
29347
+ (err) => log.warn({ farmId, err }, "farm pool sync after provision failed")
29348
+ );
29349
+ } else {
29350
+ await recordInitError(db, projectId, `farm provisioning failed: ${result.error ?? "unknown error"}`);
29351
+ }
29352
+ return ok({ ok: result.ok, daemonStatus: result.daemonStatus ?? null, error: result.error ?? null });
29353
+ },
29354
+ async provisionGhRunners({ runnerId, projectId, sealedBundle, actor }) {
29355
+ const guard = await ghRunnerHost(runnerId, actor);
29356
+ if (!guard.ok) return guard;
29357
+ let result;
29358
+ try {
29359
+ result = await requestGhRunnersProvision(runnerId, projectId, sealedBundle, GH_RUNNER_RELAY_TIMEOUT_MS);
29360
+ } catch (err) {
29361
+ return nodeUnavailable("runner", err);
29362
+ }
29363
+ const runners = result.runners ?? [];
29364
+ if (result.ok) {
29365
+ const connectedAt = Date.now();
29366
+ for (const r of runners) {
29367
+ await upsertGhRunnerBinding(db, projectId, {
29368
+ owner: r.owner,
29369
+ repo: r.repo,
29370
+ runnerId,
29371
+ connectedAt,
29372
+ status: r.status,
29373
+ ...r.error ? { error: r.error } : {},
29374
+ ...r.labels ? { labels: r.labels } : {}
29375
+ });
29376
+ }
29377
+ }
29378
+ return ok({
29379
+ ok: result.ok,
29380
+ runners: runners.map((r) => ({ owner: r.owner, repo: r.repo, status: r.status, error: r.error ?? null, labels: r.labels ?? [] })),
29381
+ error: result.error ?? null
29382
+ });
29383
+ },
29384
+ async teardownGhRunners({ runnerId, projectId, sealedBundle, repo, actor }) {
29385
+ const guard = await ghRunnerHost(runnerId, actor);
29386
+ if (!guard.ok) return guard;
29387
+ let result;
29388
+ try {
29389
+ result = await requestGhRunnersDelete(runnerId, projectId, sealedBundle ?? void 0, GH_RUNNER_RELAY_TIMEOUT_MS);
29390
+ } catch (err) {
29391
+ return nodeUnavailable("runner", err);
29392
+ }
29393
+ if (result.ok && repo !== null) {
29394
+ await removeGhRunnerBinding(db, projectId, repo.owner, repo.repo);
29395
+ }
29396
+ return ok({ ok: result.ok, error: result.error ?? null });
29397
+ },
29398
+ async enqueueProjectIndex({ projectId, kind }) {
29399
+ const p = await db.get(`SELECT id, context_repo_json as contextRepoJson FROM projects WHERE id = ?`, projectId);
29400
+ if (!p) return notFound();
29401
+ if (!p.contextRepoJson) return conflict("context_repo_not_provisioned");
29402
+ const jobId = kind === "backfill" ? await enqueueBackfillJob(db, projectId) : await enqueueIndexJob(db, projectId);
29403
+ await advanceInitPhase(db, projectId, "indexing");
29404
+ log.info({ projectId, jobId, kind }, kind === "backfill" ? "backfill job enqueued" : "index job enqueued");
29405
+ return ok({ jobId });
29406
+ },
29407
+ async issueWorkerRegistrationToken({ owner }) {
29408
+ if (owner.email === null) return { ok: false, refusal: "unprocessable", reason: "account_has_no_email" };
29409
+ if (!WORKER_REG_TOKEN_MINTS.take(`email:${owner.email}`)) {
29410
+ log.warn({ email: owner.email }, "ephemeral sandbox reg token mint throttled");
29411
+ return { ok: false, refusal: "rate_limited" };
29412
+ }
29413
+ const issued = await issueEphemeralSandboxRegToken(db, owner.email);
29414
+ log.info({ email: owner.email }, "ephemeral sandbox reg token issued for a client");
29415
+ return ok(issued);
29416
+ },
29417
+ async reportInitStatus({ projectId, phase, error }) {
29418
+ const recorded = await advanceInitPhase(db, projectId, phase);
29419
+ if (error !== null) {
29420
+ const cur = await getInitStatus(db, projectId);
29421
+ if (cur && cur.phase === phase) await recordInitError(db, projectId, error);
29422
+ }
29423
+ return ok({ recorded });
29424
+ },
28925
29425
  async deleteProject({ projectId }) {
28926
29426
  const row = await db.get(`SELECT farm_id as farmId FROM projects WHERE id = ?`, projectId);
28927
29427
  if (!row) return conflict("project_not_found");
@@ -29289,9 +29789,9 @@ function createDirectPipeline(deps) {
29289
29789
  const materialization = {
29290
29790
  async next(projectId) {
29291
29791
  for (; ; ) {
29292
- const pending4 = await nextPendingProjectAction(db, projectId);
29293
- if (!pending4) return null;
29294
- const prepared = await prepareDispatch(db, pending4.id, log);
29792
+ const pending5 = await nextPendingProjectAction(db, projectId);
29793
+ if (!pending5) return null;
29794
+ const prepared = await prepareDispatch(db, pending5.id, log);
29295
29795
  if (prepared.kind === "settled") continue;
29296
29796
  if (prepared.kind !== "ready") return null;
29297
29797
  await markDispatched(db, prepared.actionId);
@@ -29498,7 +29998,7 @@ async function compose(db, config, log, issueGrants) {
29498
29998
  const notify2 = { db, config, channels: [new EmailChannel(mailer)], log };
29499
29999
  setNotifyDeps(notify2);
29500
30000
  const authorizer = createAuthorizer(db, config);
29501
- const reads = createReadOperations(createReadModel(db), authorizer);
30001
+ const reads = createReadOperations(createReadModel(db, { isOperator: (email) => isSuperUser(config, email) }), authorizer);
29502
30002
  const pipeline = createDirectPipeline({
29503
30003
  db,
29504
30004
  log,