@code-partner/codepipe-hub 0.14.1-dev.419.g21c1b62a → 0.14.1-dev.422.gf90f06c4

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.419.g21c1b62a";
460
+ PLATFORM_VERSION = "0.14.1-dev.422.gf90f06c4";
452
461
  }
453
462
  });
454
463
 
@@ -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,16 @@ 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();
4941
4986
  }
4942
4987
  }
4988
+ function accountActorOf(actor) {
4989
+ const account = accountOf(actor);
4990
+ if (account === null)
4991
+ throw errors.forbidden({ details: { reason: "human_only" } });
4992
+ return account;
4993
+ }
4943
4994
  async function audited(actor, entry, run) {
4944
4995
  const policy = { decision: "not_evaluated" };
4945
4996
  const common = {
@@ -5428,6 +5479,107 @@ function createCommandOperations(deps) {
5428
5479
  return { value: outcome.result, replayed: outcome.replayed };
5429
5480
  });
5430
5481
  },
5482
+ async createProject(actor, command) {
5483
+ const { project } = command;
5484
+ const projectId = project.projectId;
5485
+ if (!PROJECT_ID_RE.test(projectId)) {
5486
+ throw errors.invalidInput({
5487
+ message: "A project id is lowercase letters, digits and dashes, starts with a letter, 2\u201331 characters.",
5488
+ details: { field: "projectId" }
5489
+ });
5490
+ }
5491
+ for (const repo of project.repos) {
5492
+ if (repo.defaultBranch !== void 0 && !isValidBranchName(repo.defaultBranch)) {
5493
+ throw errors.invalidInput({ message: "That is not a valid git branch name.", details: { field: "repos.defaultBranch", repo: repo.name } });
5494
+ }
5495
+ }
5496
+ const key = asIdempotencyKey(command.idempotencyKey);
5497
+ const scope = { subjectId: actor.principal.subjectId, operationId: "project.create", projectId, key };
5498
+ return await audited(actor, { operationId: "project.create", aggregateType: "project", aggregateId: projectId, projectId, data: { name: project.name, tracker: project.trackerProvider } }, async (policy) => {
5499
+ await require2(actor, "account.manage", { type: "account" }, policy);
5500
+ const run = await runIdempotent(idempotency, scope, { project }, async () => unwrap(await write2.createProject({ project, owner: accountActorOf(actor) })), now());
5501
+ return { value: run.result, replayed: run.replayed };
5502
+ });
5503
+ },
5504
+ async provisionOnFarm(actor, command) {
5505
+ const { farmId, projectId } = command;
5506
+ const key = asIdempotencyKey(command.idempotencyKey);
5507
+ const scope = { subjectId: actor.principal.subjectId, operationId: "farm.provision", projectId, key };
5508
+ return await audited(actor, { operationId: "farm.provision", aggregateType: "project", aggregateId: projectId, projectId, data: { farmId } }, async (policy) => {
5509
+ await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
5510
+ const run = await runIdempotent(idempotency, scope, { farmId, projectId }, async () => {
5511
+ const row = unwrap(await write2.provisionOnFarm({ farmId, projectId, sealedBundle: command.sealedBundle, actor: accountActorOf(actor) }));
5512
+ return { projectId, farmId, ok: row.ok, daemonStatus: row.daemonStatus, error: row.error };
5513
+ }, now());
5514
+ return { value: run.result, replayed: run.replayed };
5515
+ });
5516
+ },
5517
+ async provisionGhRunners(actor, command) {
5518
+ const { runnerId, projectId } = command;
5519
+ const key = asIdempotencyKey(command.idempotencyKey);
5520
+ const scope = { subjectId: actor.principal.subjectId, operationId: "runner.provisionGhRunners", projectId, key };
5521
+ return await audited(actor, { operationId: "runner.provisionGhRunners", aggregateType: "project", aggregateId: projectId, projectId, data: { runnerId } }, async (policy) => {
5522
+ await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
5523
+ const run = await runIdempotent(idempotency, scope, { runnerId, projectId }, async () => {
5524
+ const row = unwrap(await write2.provisionGhRunners({ runnerId, projectId, sealedBundle: command.sealedBundle, actor: accountActorOf(actor) }));
5525
+ return {
5526
+ projectId,
5527
+ runnerId,
5528
+ ok: row.ok,
5529
+ runners: row.runners.map((r) => ({ owner: r.owner, repo: r.repo, status: r.status, error: r.error, labels: r.labels })),
5530
+ error: row.error
5531
+ };
5532
+ }, now());
5533
+ return { value: run.result, replayed: run.replayed };
5534
+ });
5535
+ },
5536
+ async teardownGhRunners(actor, command) {
5537
+ const { runnerId, projectId } = command;
5538
+ const key = asIdempotencyKey(command.idempotencyKey);
5539
+ const scope = { subjectId: actor.principal.subjectId, operationId: "runner.teardownGhRunners", projectId, key };
5540
+ const repo = command.repo ?? null;
5541
+ return await audited(actor, {
5542
+ operationId: "runner.teardownGhRunners",
5543
+ aggregateType: "project",
5544
+ aggregateId: projectId,
5545
+ projectId,
5546
+ data: { runnerId, ...repo === null ? {} : { repo: `${repo.owner}/${repo.repo}` } }
5547
+ }, async (policy) => {
5548
+ await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
5549
+ const run = await runIdempotent(idempotency, scope, { runnerId, projectId, repo }, async () => {
5550
+ const row = unwrap(await write2.teardownGhRunners({
5551
+ runnerId,
5552
+ projectId,
5553
+ sealedBundle: command.sealedBundle ?? null,
5554
+ repo,
5555
+ actor: accountActorOf(actor)
5556
+ }));
5557
+ return { projectId, runnerId, ok: row.ok, error: row.error };
5558
+ }, now());
5559
+ return { value: run.result, replayed: run.replayed };
5560
+ });
5561
+ },
5562
+ async requestProjectIndex(actor, command) {
5563
+ const { projectId, kind } = command;
5564
+ const key = asIdempotencyKey(command.idempotencyKey);
5565
+ const scope = { subjectId: actor.principal.subjectId, operationId: "project.index", projectId, key };
5566
+ return await audited(actor, { operationId: "project.index", aggregateType: "project", aggregateId: projectId, projectId, data: { kind } }, async (policy) => {
5567
+ await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
5568
+ const run = await runIdempotent(idempotency, scope, { kind }, async () => {
5569
+ const { jobId } = unwrap(await write2.enqueueProjectIndex({ projectId, kind }));
5570
+ return { jobId, kind };
5571
+ }, now());
5572
+ return { value: run.result, replayed: run.replayed };
5573
+ });
5574
+ },
5575
+ async reportInitStatus(actor, command) {
5576
+ const { projectId, phase } = command;
5577
+ return await audited(actor, { operationId: "project.reportInitStatus", aggregateType: "project", aggregateId: projectId, projectId, data: { phase, failed: command.error !== void 0 } }, async (policy) => {
5578
+ await require2(actor, "project.manage", { type: "project", id: projectId, projectId }, policy);
5579
+ const { recorded } = unwrap(await write2.reportInitStatus({ projectId, phase, error: command.error ?? null }));
5580
+ return { value: { recorded }, replayed: false };
5581
+ });
5582
+ },
5431
5583
  async setRepoDefaultBranch(actor, command) {
5432
5584
  const { projectId, name } = command;
5433
5585
  const revision = asRevision(command.revision);
@@ -8623,14 +8775,14 @@ function buildQueueDag(tasks, isSatisfiedOutside = () => false) {
8623
8775
  function stableTopoOrder(currentOrder, dag) {
8624
8776
  const sortable = currentOrder.filter((k) => dag.before.has(k) && !dag.cycleKeys.has(k));
8625
8777
  const index = new Map(sortable.map((k, i) => [k, i]));
8626
- const pending4 = /* @__PURE__ */ new Map();
8778
+ const pending5 = /* @__PURE__ */ new Map();
8627
8779
  const dependents = /* @__PURE__ */ new Map();
8628
8780
  for (const key of sortable) {
8629
8781
  const preds = [...dag.before.get(key) ?? []].filter(
8630
8782
  (p) => index.has(p)
8631
8783
  // cycle keys / out-of-order keys don't gate the sort
8632
8784
  );
8633
- pending4.set(key, preds.length);
8785
+ pending5.set(key, preds.length);
8634
8786
  for (const p of preds) {
8635
8787
  const d = dependents.get(p) ?? [];
8636
8788
  d.push(key);
@@ -8638,7 +8790,7 @@ function stableTopoOrder(currentOrder, dag) {
8638
8790
  }
8639
8791
  }
8640
8792
  const ready = /* @__PURE__ */ new Set();
8641
- for (const [key, deg] of pending4) if (deg === 0) ready.add(key);
8793
+ for (const [key, deg] of pending5) if (deg === 0) ready.add(key);
8642
8794
  const out = [];
8643
8795
  while (ready.size > 0) {
8644
8796
  let best = null;
@@ -8648,8 +8800,8 @@ function stableTopoOrder(currentOrder, dag) {
8648
8800
  ready.delete(best);
8649
8801
  out.push(best);
8650
8802
  for (const dep of dependents.get(best) ?? []) {
8651
- const deg = pending4.get(dep) - 1;
8652
- pending4.set(dep, deg);
8803
+ const deg = pending5.get(dep) - 1;
8804
+ pending5.set(dep, deg);
8653
8805
  if (deg === 0) ready.add(dep);
8654
8806
  }
8655
8807
  }
@@ -10331,8 +10483,8 @@ async function saveDeliveries(db, taskId2, sourceJobId, deliveries) {
10331
10483
  if (deliveries.length === 0) return;
10332
10484
  const now = Date.now();
10333
10485
  for (const d of deliveries) {
10334
- const pending4 = pendingRelayRevision(d);
10335
- const unpublished = pending4 !== null;
10486
+ const pending5 = pendingRelayRevision(d);
10487
+ const unpublished = pending5 !== null;
10336
10488
  await db.run(
10337
10489
  `INSERT INTO task_deliveries
10338
10490
  (task_id, repo_name, delivery_ref, delivery_sha, base_sha, changed_files_json, source_job_id, created_at,
@@ -10355,7 +10507,7 @@ async function saveDeliveries(db, taskId2, sourceJobId, deliveries) {
10355
10507
  JSON.stringify(d.changedFiles ?? []),
10356
10508
  sourceJobId,
10357
10509
  now,
10358
- pending4 && pending4 !== "unusable" ? JSON.stringify(pending4) : null,
10510
+ pending5 && pending5 !== "unusable" ? JSON.stringify(pending5) : null,
10359
10511
  unpublished ? null : now
10360
10512
  );
10361
10513
  }
@@ -10886,6 +11038,9 @@ async function getInitStatus(db, projectId) {
10886
11038
  async function write(db, projectId, status) {
10887
11039
  await db.run(`UPDATE projects SET init_status_json = ? WHERE id = ?`, JSON.stringify(status), projectId);
10888
11040
  }
11041
+ async function beginInitTracking(db, projectId, phase) {
11042
+ await write(db, projectId, { phase, updatedAt: Date.now() });
11043
+ }
10889
11044
  async function advanceInitPhase(db, projectId, phase) {
10890
11045
  const cur = await getInitStatus(db, projectId);
10891
11046
  if (!cur) return false;
@@ -11151,8 +11306,94 @@ var init_sandbox2 = __esm({
11151
11306
  }
11152
11307
  });
11153
11308
 
11154
- // src/farm/pool-spec.ts
11309
+ // src/routes/farm.ts
11155
11310
  import { nanoid as nanoid11 } from "nanoid";
11311
+ function viewerInAssignments(viewer, assignments) {
11312
+ return assignments.some(
11313
+ (a) => viewer.userId !== null && a.userId === viewer.userId || a.email !== null && a.email === viewer.email
11314
+ );
11315
+ }
11316
+ async function viewerOwnsFarm(db, viewer, farmId) {
11317
+ const row = await db.get(
11318
+ `SELECT 1 FROM farm_assignments
11319
+ WHERE farm_id = ? AND (user_id = ? OR email = ?) LIMIT 1`,
11320
+ farmId,
11321
+ viewer.userId,
11322
+ viewer.email
11323
+ );
11324
+ return !!row;
11325
+ }
11326
+ async function listFarms(db, viewer) {
11327
+ const online = new Set(listConnectedFarmIds());
11328
+ const rows = await db.all(`SELECT f.id, f.name, f.public_key as publicKey,
11329
+ f.key_fingerprint as keyFingerprint, f.last_seen_at as lastSeenAt,
11330
+ a.user_id as aUserId, a.email as aEmail
11331
+ FROM farms f LEFT JOIN farm_assignments a ON a.farm_id = f.id
11332
+ ORDER BY f.registered_at ASC`);
11333
+ const byId = /* @__PURE__ */ new Map();
11334
+ for (const r of rows) {
11335
+ let farm = byId.get(r.id);
11336
+ if (!farm) {
11337
+ farm = {
11338
+ farmId: r.id,
11339
+ name: r.name,
11340
+ publicKey: r.publicKey,
11341
+ keyFingerprint: r.keyFingerprint,
11342
+ online: online.has(r.id),
11343
+ lastSeenAt: r.lastSeenAt,
11344
+ assignments: []
11345
+ };
11346
+ byId.set(r.id, farm);
11347
+ }
11348
+ if (r.aUserId !== null || r.aEmail !== null) {
11349
+ farm.assignments.push({ userId: r.aUserId, email: r.aEmail });
11350
+ }
11351
+ }
11352
+ return [...byId.values()].filter((f) => !viewer || viewer.isSuperAdmin || viewerInAssignments(viewer, f.assignments)).map(({ assignments: _assignments, ...f }) => f);
11353
+ }
11354
+ var init_farm2 = __esm({
11355
+ "src/routes/farm.ts"() {
11356
+ "use strict";
11357
+ init_sealedbox();
11358
+ init_abuse_throttle();
11359
+ init_ws();
11360
+ }
11361
+ });
11362
+
11363
+ // src/runner-reg-tokens.ts
11364
+ import { nanoid as nanoid12 } from "nanoid";
11365
+ var init_runner_reg_tokens = __esm({
11366
+ "src/runner-reg-tokens.ts"() {
11367
+ "use strict";
11368
+ }
11369
+ });
11370
+
11371
+ // src/routes/runner.ts
11372
+ import { nanoid as nanoid13 } from "nanoid";
11373
+ async function viewerOwnsRunner(db, viewer, runnerId) {
11374
+ const row = await db.get(
11375
+ `SELECT 1 FROM runners WHERE id = ? AND (owner_user_id = ? OR owner_email = ?) LIMIT 1`,
11376
+ runnerId,
11377
+ viewer.userId,
11378
+ viewer.email
11379
+ );
11380
+ return !!row;
11381
+ }
11382
+ var init_runner2 = __esm({
11383
+ "src/routes/runner.ts"() {
11384
+ "use strict";
11385
+ init_sealedbox();
11386
+ init_abuse_throttle();
11387
+ init_cli_auth();
11388
+ init_auth();
11389
+ init_users();
11390
+ init_runner_reg_tokens();
11391
+ init_ws();
11392
+ }
11393
+ });
11394
+
11395
+ // src/farm/pool-spec.ts
11396
+ import { nanoid as nanoid14 } from "nanoid";
11156
11397
  async function computeFarmPoolSpec(db, farmId) {
11157
11398
  const rows = await db.all(
11158
11399
  `SELECT fa.user_id as userId, fa.max_sandboxes as quota,
@@ -11184,7 +11425,7 @@ async function computeFarmPoolSpec(db, farmId) {
11184
11425
  return spec;
11185
11426
  }
11186
11427
  function buildSyncWorkerPoolMessage(accounts) {
11187
- return { type: "sync_worker_pool", requestId: nanoid11(16), accounts };
11428
+ return { type: "sync_worker_pool", requestId: nanoid14(16), accounts };
11188
11429
  }
11189
11430
  var init_pool_spec = __esm({
11190
11431
  "src/farm/pool-spec.ts"() {
@@ -11192,127 +11433,6 @@ var init_pool_spec = __esm({
11192
11433
  }
11193
11434
  });
11194
11435
 
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
11436
  // src/routes/ws.ts
11317
11437
  function sendToDeliveryFollowers(projectId, message) {
11318
11438
  const payload = JSON.stringify(message);
@@ -11356,6 +11476,13 @@ function sendToFarm(farmId, message) {
11356
11476
  function listConnectedFarmIds() {
11357
11477
  return [...farmConnections.keys()];
11358
11478
  }
11479
+ function sendToRunner(runnerId, message) {
11480
+ const conn = runnerConnections.get(runnerId);
11481
+ if (!conn) return false;
11482
+ if (conn.socket.readyState !== conn.socket.OPEN) return false;
11483
+ conn.socket.send(JSON.stringify(message));
11484
+ return true;
11485
+ }
11359
11486
  function getProjectCliOrigin(projectId) {
11360
11487
  return projectCliConnections.get(projectId)?.origin ?? null;
11361
11488
  }
@@ -11598,7 +11725,7 @@ async function getPresence(db, viewer) {
11598
11725
  })()
11599
11726
  };
11600
11727
  }
11601
- var agentLoginProviders, deliveryFollowers, projectCliConnections, projectLastSeenAt, projectLastDuplicateRegisterAt, sandboxConnections, farmConnections, cliMessageHandlers, cliRegisterHandlers;
11728
+ var agentLoginProviders, deliveryFollowers, projectCliConnections, projectLastSeenAt, projectLastDuplicateRegisterAt, sandboxConnections, farmConnections, runnerConnections, cliMessageHandlers, cliRegisterHandlers;
11602
11729
  var init_ws = __esm({
11603
11730
  "src/routes/ws.ts"() {
11604
11731
  "use strict";
@@ -11624,6 +11751,7 @@ var init_ws = __esm({
11624
11751
  projectLastDuplicateRegisterAt = /* @__PURE__ */ new Map();
11625
11752
  sandboxConnections = /* @__PURE__ */ new Map();
11626
11753
  farmConnections = /* @__PURE__ */ new Map();
11754
+ runnerConnections = /* @__PURE__ */ new Map();
11627
11755
  cliMessageHandlers = /* @__PURE__ */ new Set();
11628
11756
  cliRegisterHandlers = /* @__PURE__ */ new Set();
11629
11757
  }
@@ -11751,7 +11879,7 @@ var init_policy = __esm({
11751
11879
  });
11752
11880
 
11753
11881
  // src/apply/merge-branch.ts
11754
- import { nanoid as nanoid17 } from "nanoid";
11882
+ import { nanoid as nanoid15 } from "nanoid";
11755
11883
  async function enqueueMergeBranch(db, taskId2, log) {
11756
11884
  if (await hasInflightMergeBranch(db, taskId2)) {
11757
11885
  log.info({ taskId: taskId2 }, "merge_branch ignored \u2014 chain already in flight");
@@ -11768,7 +11896,7 @@ async function enqueueMergeBranch(db, taskId2, log) {
11768
11896
  const now = Date.now();
11769
11897
  const ids = [];
11770
11898
  for (const t of targets) {
11771
- const id = `wa_${nanoid17(12)}`;
11899
+ const id = `wa_${nanoid15(12)}`;
11772
11900
  const action = {
11773
11901
  type: "merge_branch",
11774
11902
  taskId: taskId2,
@@ -11849,7 +11977,7 @@ var init_merge_branch = __esm({
11849
11977
  });
11850
11978
 
11851
11979
  // src/apply/publish-delivery.ts
11852
- import { nanoid as nanoid18 } from "nanoid";
11980
+ import { nanoid as nanoid16 } from "nanoid";
11853
11981
  async function enqueuePublishDeliveries(db, taskId2, log) {
11854
11982
  const unpublished = await unpublishedDeliveries(db, taskId2);
11855
11983
  const unusable = unpublished.filter((p) => p.revision === null);
@@ -11860,8 +11988,8 @@ async function enqueuePublishDeliveries(db, taskId2, log) {
11860
11988
  };
11861
11989
  }
11862
11990
  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 };
11991
+ const pending5 = unpublished.filter((p) => inFlight2.get(p.repoName) !== p.revision?.sha);
11992
+ if (pending5.length === 0) return { queued: 0 };
11865
11993
  const projectId = taskId2.split(":", 2)[0] ?? "";
11866
11994
  const contextRepoCloneUrl = await contextRepoCloneUrlOf(db, projectId);
11867
11995
  if (!contextRepoCloneUrl) {
@@ -11870,8 +11998,8 @@ async function enqueuePublishDeliveries(db, taskId2, log) {
11870
11998
  }
11871
11999
  const now = Date.now();
11872
12000
  const ids = [];
11873
- for (const p of pending4) {
11874
- const id = `wa_${nanoid18(12)}`;
12001
+ for (const p of pending5) {
12002
+ const id = `wa_${nanoid16(12)}`;
11875
12003
  const action = {
11876
12004
  type: "publish_delivery",
11877
12005
  taskId: taskId2,
@@ -13132,7 +13260,7 @@ var init_settings = __esm({
13132
13260
  });
13133
13261
 
13134
13262
  // src/jobs/queue.ts
13135
- import { nanoid as nanoid19 } from "nanoid";
13263
+ import { nanoid as nanoid17 } from "nanoid";
13136
13264
  async function heldByTrancheDeferral(db) {
13137
13265
  const rows = await db.all(
13138
13266
  `SELECT t.id AS id FROM tasks t
@@ -13204,7 +13332,7 @@ async function restampQueuedJobs(db, projectId) {
13204
13332
  return restamped;
13205
13333
  }
13206
13334
  async function createJob(db, input) {
13207
- const id = `job_${nanoid19(16)}`;
13335
+ const id = `job_${nanoid17(16)}`;
13208
13336
  const now = Date.now();
13209
13337
  const projectRepos = await loadProjectRepoRefs(db, input.projectId);
13210
13338
  const metadata = {
@@ -13405,7 +13533,7 @@ async function claimNextJob(db, sandboxId, capabilities, lane) {
13405
13533
  data: e.data
13406
13534
  });
13407
13535
  },
13408
- newExecutionId: () => `exe_${nanoid19(16)}`,
13536
+ newExecutionId: () => `exe_${nanoid17(16)}`,
13409
13537
  // DEV-49/DEV-221: stack the worktree on the predecessor's branch so the
13410
13538
  // agent (and the resulting meta/bases.json baseSha) start from the chain
13411
13539
  // tip, not the repo default. Applies to analyze/estimate/implement — a
@@ -13671,7 +13799,7 @@ var init_legacy_cred_bundles = __esm({
13671
13799
  });
13672
13800
 
13673
13801
  // src/db/schema.ts
13674
- import { nanoid as nanoid20 } from "nanoid";
13802
+ import { nanoid as nanoid18 } from "nanoid";
13675
13803
  async function ensureColumn(db, table, column, ddl) {
13676
13804
  const row = await db.get(
13677
13805
  `SELECT 1 FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,
@@ -13980,7 +14108,7 @@ async function initSchema(db) {
13980
14108
  await ensureColumn(db, "projects", "init_status_json", "init_status_json TEXT");
13981
14109
  await ensureColumn(db, "users", "id", "id TEXT");
13982
14110
  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);
14111
+ await db.run(`UPDATE users SET id = ? WHERE email = ?`, `usr_${nanoid18(12)}`, row.email);
13984
14112
  }
13985
14113
  await db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_users_id ON users(id)`);
13986
14114
  await ensureColumn(db, "projects", "owner_user_id", "owner_user_id TEXT");
@@ -14064,7 +14192,7 @@ async function initSchema(db) {
14064
14192
  await db.run(
14065
14193
  `INSERT INTO context_outbox (id, project_id, ordinal, files_json, message, state, created_at)
14066
14194
  VALUES (?, ?, ?, ?, 'migration: legacy display diffs', 'pending', ?)`,
14067
- `ctx_${nanoid20(16)}`,
14195
+ `ctx_${nanoid18(16)}`,
14068
14196
  projectId,
14069
14197
  next.next,
14070
14198
  JSON.stringify(files),
@@ -15898,27 +16026,27 @@ var init_delivery_decision = __esm({
15898
16026
  });
15899
16027
 
15900
16028
  // src/projects/context-read.ts
15901
- import { nanoid as nanoid21 } from "nanoid";
16029
+ import { nanoid as nanoid19 } from "nanoid";
15902
16030
  function requestContextFile(projectId, path, timeoutMs = DEFAULT_TIMEOUT_MS, send = sendToProjectCli) {
15903
16031
  const action = { type: "read_context_file", projectId, path };
15904
- const actionId2 = `rdctx_${nanoid21(16)}`;
16032
+ const actionId2 = `rdctx_${nanoid19(16)}`;
15905
16033
  if (!send(projectId, { type: "dispatch_action", actionId: actionId2, action })) {
15906
16034
  return Promise.reject(new Error("cli_offline"));
15907
16035
  }
15908
16036
  return new Promise((resolve2, reject) => {
15909
16037
  const timer = setTimeout(() => {
15910
- pending2.delete(actionId2);
16038
+ pending.delete(actionId2);
15911
16039
  reject(new Error("cli_timeout"));
15912
16040
  }, timeoutMs);
15913
- pending2.set(actionId2, { resolve: resolve2, reject, timer });
16041
+ pending.set(actionId2, { resolve: resolve2, reject, timer });
15914
16042
  });
15915
16043
  }
15916
- var pending2, DEFAULT_TIMEOUT_MS;
16044
+ var pending, DEFAULT_TIMEOUT_MS;
15917
16045
  var init_context_read = __esm({
15918
16046
  "src/projects/context-read.ts"() {
15919
16047
  "use strict";
15920
16048
  init_ws();
15921
- pending2 = /* @__PURE__ */ new Map();
16049
+ pending = /* @__PURE__ */ new Map();
15922
16050
  DEFAULT_TIMEOUT_MS = 2e4;
15923
16051
  }
15924
16052
  });
@@ -15963,14 +16091,14 @@ async function recordVerdicts(db, taskId2, verdicts) {
15963
16091
  return current;
15964
16092
  }
15965
16093
  function decidePlan(plan, verdicts) {
15966
- const pending4 = plan.items.filter((i) => !(i.id in verdicts)).map((i) => i.id);
16094
+ const pending5 = plan.items.filter((i) => !(i.id in verdicts)).map((i) => i.id);
15967
16095
  const list = plan.items.map((i) => verdicts[i.id]).filter((v) => !!v);
15968
- const approved = pending4.length === 0 && list.every((v) => v.approved);
16096
+ const approved = pending5.length === 0 && list.every((v) => v.approved);
15969
16097
  return {
15970
- complete: pending4.length === 0,
15971
- pending: pending4,
16098
+ complete: pending5.length === 0,
16099
+ pending: pending5,
15972
16100
  approved,
15973
- rework: pending4.length === 0 ? compilePlanRework(plan, list) : ""
16101
+ rework: pending5.length === 0 ? compilePlanRework(plan, list) : ""
15974
16102
  };
15975
16103
  }
15976
16104
  function describeDecision(d) {
@@ -16108,7 +16236,7 @@ var init_project_access = __esm({
16108
16236
  });
16109
16237
 
16110
16238
  // src/projects/regimen-write.ts
16111
- import { nanoid as nanoid22 } from "nanoid";
16239
+ import { nanoid as nanoid20 } from "nanoid";
16112
16240
  function dispatchSetYouTrackConfig(db, projectId, payload, log) {
16113
16241
  const action = {
16114
16242
  type: "set_youtrack_config",
@@ -16116,7 +16244,7 @@ function dispatchSetYouTrackConfig(db, projectId, payload, log) {
16116
16244
  ...payload,
16117
16245
  commitMessage: `chore(youtrack): update YT config (dashboard)`
16118
16246
  };
16119
- const actionId2 = `ytcfg_${nanoid22(16)}`;
16247
+ const actionId2 = `ytcfg_${nanoid20(16)}`;
16120
16248
  const sent = sendToProjectCli(projectId, { type: "dispatch_action", actionId: actionId2, action });
16121
16249
  if (!sent) {
16122
16250
  log.warn({ projectId, actionId: actionId2 }, "dispatchSetYouTrackConfig: CLI not connected");
@@ -16132,7 +16260,7 @@ function dispatchSetRepoDefaultBranch(db, projectId, repoName, defaultBranch, lo
16132
16260
  repoName,
16133
16261
  defaultBranch
16134
16262
  };
16135
- const actionId2 = `repocfg_${nanoid22(16)}`;
16263
+ const actionId2 = `repocfg_${nanoid20(16)}`;
16136
16264
  const sent = sendToProjectCli(projectId, { type: "dispatch_action", actionId: actionId2, action });
16137
16265
  if (!sent) {
16138
16266
  log.warn({ projectId, actionId: actionId2 }, "dispatchSetRepoDefaultBranch: CLI not connected");
@@ -16152,7 +16280,7 @@ function dispatchSetProjectSetting(db, projectId, edits, log, opts) {
16152
16280
  edits,
16153
16281
  commitMessage: opts?.commitMessage ?? "chore(settings): update project settings (dashboard)"
16154
16282
  };
16155
- const actionId2 = `${prefix}${nanoid22(16)}`;
16283
+ const actionId2 = `${prefix}${nanoid20(16)}`;
16156
16284
  if (prefix === "setg_") rememberSettingsDispatch(actionId2, edits);
16157
16285
  const sent = sendToProjectCli(projectId, { type: "dispatch_action", actionId: actionId2, action });
16158
16286
  if (!sent) {
@@ -16164,7 +16292,7 @@ function dispatchSetProjectSetting(db, projectId, edits, log, opts) {
16164
16292
  }
16165
16293
  function dispatchSetBudgetSource(db, projectId, source, log) {
16166
16294
  const action = { type: "set_budget_source", projectId, source };
16167
- const actionId2 = `bsrc_${nanoid22(16)}`;
16295
+ const actionId2 = `bsrc_${nanoid20(16)}`;
16168
16296
  const sent = sendToProjectCli(projectId, { type: "dispatch_action", actionId: actionId2, action });
16169
16297
  if (!sent) log.warn({ projectId, actionId: actionId2 }, "dispatchSetBudgetSource: CLI not connected");
16170
16298
  else log.info({ projectId, actionId: actionId2, kind: source.kind }, "dispatchSetBudgetSource: sent to CLI");
@@ -16186,7 +16314,7 @@ function dispatchEditTrackerIssue(db, projectId, fields, log) {
16186
16314
  ...fields.title !== void 0 ? { title: fields.title } : {},
16187
16315
  ...fields.body !== void 0 ? { body: fields.body } : {}
16188
16316
  };
16189
- const actionId2 = `tedit_${nanoid22(16)}`;
16317
+ const actionId2 = `tedit_${nanoid20(16)}`;
16190
16318
  const sent = sendToProjectCli(projectId, { type: "dispatch_action", actionId: actionId2, action });
16191
16319
  if (!sent) {
16192
16320
  log.warn({ projectId, actionId: actionId2, issueKey: fields.issueKey }, "dispatchEditTrackerIssue: CLI not connected");
@@ -16206,21 +16334,21 @@ var init_regimen_write = __esm({
16206
16334
  });
16207
16335
 
16208
16336
  // src/tasks/context-outbox.ts
16209
- import { nanoid as nanoid23 } from "nanoid";
16337
+ import { nanoid as nanoid21 } from "nanoid";
16210
16338
  async function nextOrdinal(db, projectId) {
16211
16339
  const row = await db.get(`SELECT COALESCE(MAX(ordinal), 0) AS max FROM context_outbox WHERE project_id = ?`, projectId);
16212
16340
  return row.max + 1;
16213
16341
  }
16214
16342
  async function enqueueContextWrite(db, projectId, files, message, log) {
16215
16343
  if (files.length === 0) return;
16216
- const id = `ctx_${nanoid23(16)}`;
16344
+ const id = `ctx_${nanoid21(16)}`;
16217
16345
  await db.run(`INSERT INTO context_outbox (id, project_id, ordinal, files_json, message, state, created_at)
16218
16346
  VALUES (?, ?, ?, ?, ?, 'pending', ?)`, id, projectId, await nextOrdinal(db, projectId), JSON.stringify(files), message, Date.now());
16219
16347
  await dispatchNextContextWrite(db, projectId, log);
16220
16348
  }
16221
16349
  async function enqueueAdrPromote(db, projectId, taskKey, slugs, log, source = "task") {
16222
16350
  if (slugs.length === 0) return;
16223
- const id = `ctx_${nanoid23(16)}`;
16351
+ const id = `ctx_${nanoid21(16)}`;
16224
16352
  await db.run(`INSERT INTO context_outbox (id, project_id, ordinal, files_json, message, state, kind, meta_json, created_at)
16225
16353
  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
16354
  await dispatchNextContextWrite(db, projectId, log);
@@ -16625,10 +16753,10 @@ __export(cluster_journal_exports, {
16625
16753
  clusterJournal: () => clusterJournal,
16626
16754
  recordClusterEvent: () => recordClusterEvent
16627
16755
  });
16628
- import { nanoid as nanoid24 } from "nanoid";
16756
+ import { nanoid as nanoid22 } from "nanoid";
16629
16757
  async function recordClusterEvent(db, projectId, anchorKey, kind, detail) {
16630
16758
  await engineStore(db).clusters.appendEvent(
16631
- `ce_${nanoid24(12)}`,
16759
+ `ce_${nanoid22(12)}`,
16632
16760
  projectId,
16633
16761
  anchorKey,
16634
16762
  kind,
@@ -16696,7 +16824,7 @@ __export(cluster_recovery_exports, {
16696
16824
  reconcileClusterRecoveries: () => reconcileClusterRecoveries,
16697
16825
  startClusterRecovery: () => startClusterRecovery
16698
16826
  });
16699
- import { nanoid as nanoid25 } from "nanoid";
16827
+ import { nanoid as nanoid23 } from "nanoid";
16700
16828
  async function startClusterRecovery(db, task, instruction, log, opts = {}) {
16701
16829
  const anchorKey = await clusterAnchorFor(db, task);
16702
16830
  const cluster = await getCluster(db, task.projectId, anchorKey);
@@ -17054,7 +17182,7 @@ async function dropCluster(db, task, cluster, reason, log, opts = {}) {
17054
17182
  if (carrier) {
17055
17183
  const now = Date.now();
17056
17184
  for (const t of await loadCreatePRTargets(db, carrier)) {
17057
- const id = `wa_${nanoid25(12)}`;
17185
+ const id = `wa_${nanoid23(12)}`;
17058
17186
  const action = {
17059
17187
  type: "close_pr",
17060
17188
  taskId: carrier,
@@ -17173,7 +17301,7 @@ __export(tasks_exports, {
17173
17301
  queueTrackerStatusMove: () => queueTrackerStatusMove,
17174
17302
  tasksRoutes: () => tasksRoutes
17175
17303
  });
17176
- import { nanoid as nanoid26 } from "nanoid";
17304
+ import { nanoid as nanoid24 } from "nanoid";
17177
17305
  async function queueTrackerStatusMove(db, log, taskId2, projectId, newStatus, manual = false) {
17178
17306
  const projectRow = await db.get(`SELECT automation_level as automationLevel FROM projects WHERE id = ?`, projectId);
17179
17307
  const writes = await loadYouTrackWrites(
@@ -17188,7 +17316,7 @@ async function queueTrackerStatusMove(db, log, taskId2, projectId, newStatus, ma
17188
17316
  newStatus,
17189
17317
  ...manual ? { manual: true } : {}
17190
17318
  };
17191
- const statusActionId = `wa_${nanoid26(12)}`;
17319
+ const statusActionId = `wa_${nanoid24(12)}`;
17192
17320
  await engineStore(db).actions.appendPlan(
17193
17321
  taskId2,
17194
17322
  [{ id: statusActionId, kind: "tracker_status", payloadJson: JSON.stringify(action) }],
@@ -18037,7 +18165,7 @@ async function tasksRoutes(app, deps) {
18037
18165
  }
18038
18166
  await ingestComment(db, {
18039
18167
  taskId: taskId2,
18040
- trackerCommentId: `${DASHBOARD_PLACEHOLDER_PREFIX}${nanoid26(8)}`,
18168
+ trackerCommentId: `${DASHBOARD_PLACEHOLDER_PREFIX}${nanoid24(8)}`,
18041
18169
  author: session.email,
18042
18170
  body: req.body.comment,
18043
18171
  origin: "dashboard",
@@ -18195,7 +18323,7 @@ async function tasksRoutes(app, deps) {
18195
18323
  if (!task) return reply.status(404).send({ error: "task_not_found" });
18196
18324
  const ingest = await ingestComment(db, {
18197
18325
  taskId: taskId2,
18198
- trackerCommentId: `${DASHBOARD_PLACEHOLDER_PREFIX}${nanoid26(8)}`,
18326
+ trackerCommentId: `${DASHBOARD_PLACEHOLDER_PREFIX}${nanoid24(8)}`,
18199
18327
  author: session.email,
18200
18328
  body: req.body.body,
18201
18329
  origin: "dashboard",
@@ -18264,7 +18392,7 @@ ${req.body.body}
18264
18392
  // comments.jsonl; YouTrack ignores it (the token owner authors).
18265
18393
  author: session.email
18266
18394
  };
18267
- mirrorActionId = `wa_${nanoid26(12)}`;
18395
+ mirrorActionId = `wa_${nanoid24(12)}`;
18268
18396
  await engineStore(db).actions.appendPlan(
18269
18397
  taskId2,
18270
18398
  [{ id: mirrorActionId, kind: "tracker_comment", payloadJson: JSON.stringify(action) }],
@@ -18455,7 +18583,7 @@ __export(observer_exports, {
18455
18583
  stampTranche: () => stampTranche,
18456
18584
  startStagedBatch: () => startStagedBatch
18457
18585
  });
18458
- import { nanoid as nanoid27 } from "nanoid";
18586
+ import { nanoid as nanoid25 } from "nanoid";
18459
18587
  async function loadContextRepo2(db, projectId) {
18460
18588
  const row = await db.get(
18461
18589
  `SELECT context_repo_json AS json FROM projects WHERE id = ?`,
@@ -18500,7 +18628,7 @@ async function enqueueObserverJob(db, batch, log, opts = {}) {
18500
18628
  return spec.jobId;
18501
18629
  }
18502
18630
  async function stampTranche(db, taskIds) {
18503
- const trancheId = `tr_${nanoid27(12)}`;
18631
+ const trancheId = `tr_${nanoid25(12)}`;
18504
18632
  for (const id of taskIds) {
18505
18633
  await engineStore(db).tasks.stampTrancheMembership(id, trancheId);
18506
18634
  }
@@ -18926,7 +19054,7 @@ var init_queue_view = __esm({
18926
19054
  });
18927
19055
 
18928
19056
  // src/tasks/cluster-order.ts
18929
- import { nanoid as nanoid28 } from "nanoid";
19057
+ import { nanoid as nanoid26 } from "nanoid";
18930
19058
  function chainLess2(a, b) {
18931
19059
  const aq = a.queuePosition ?? Number.MAX_SAFE_INTEGER;
18932
19060
  const bq = b.queuePosition ?? Number.MAX_SAFE_INTEGER;
@@ -19032,7 +19160,7 @@ function dispatchProposedLinks(db, log, projectId, anchorKey, addLinks, memberKe
19032
19160
  links.push({ sourceIssueKey: source, rel: "depends_on", targetIssueKey: target });
19033
19161
  }
19034
19162
  if (links.length === 0) return;
19035
- const actionId2 = `clnk_${nanoid28(16)}`;
19163
+ const actionId2 = `clnk_${nanoid26(16)}`;
19036
19164
  const action = { type: "apply_tracker_links", draftId: actionId2, projectId, links };
19037
19165
  const sent = sendToProjectCli(projectId, { type: "dispatch_action", actionId: actionId2, action });
19038
19166
  if (sent) {
@@ -21020,7 +21148,7 @@ __export(cluster_finalize_exports, {
21020
21148
  maybeFinalizeCluster: () => maybeFinalizeCluster,
21021
21149
  recordClusterPrCreated: () => recordClusterPrCreated
21022
21150
  });
21023
- import { nanoid as nanoid29 } from "nanoid";
21151
+ import { nanoid as nanoid27 } from "nanoid";
21024
21152
  async function isFullAutomation2(db, projectId) {
21025
21153
  const row = await db.get(`SELECT automation_level as automationLevel FROM projects WHERE id = ?`, projectId);
21026
21154
  return row?.automationLevel === "full";
@@ -21041,7 +21169,7 @@ async function loadMergePolicy2(db, projectId) {
21041
21169
  };
21042
21170
  }
21043
21171
  async function enqueueAndDispatch(db, taskId2, action, log) {
21044
- const id = `wa_${nanoid29(12)}`;
21172
+ const id = `wa_${nanoid27(12)}`;
21045
21173
  await engineStore(db).actions.appendPlan(
21046
21174
  taskId2,
21047
21175
  [{ id, kind: action.type, payloadJson: JSON.stringify(action) }],
@@ -21771,7 +21899,7 @@ __export(tracking_commands_exports, {
21771
21899
  handleUpdatePr: () => handleUpdatePr,
21772
21900
  loadCreatePRTargets: () => loadCreatePRTargets
21773
21901
  });
21774
- import { nanoid as nanoid30 } from "nanoid";
21902
+ import { nanoid as nanoid28 } from "nanoid";
21775
21903
  async function loadCreatePRTargets(db, taskId2) {
21776
21904
  const rows = await db.all(`SELECT payload_json AS payloadJson, result_json AS resultJson
21777
21905
  FROM write_actions
@@ -21871,7 +21999,7 @@ async function handleApprove(db, task, log) {
21871
21999
  const now = Date.now();
21872
22000
  const ids = [];
21873
22001
  for (const t of targets) {
21874
- const id = `wa_${nanoid30(12)}`;
22002
+ const id = `wa_${nanoid28(12)}`;
21875
22003
  const action = {
21876
22004
  type: "merge_pr",
21877
22005
  taskId: chainTaskId,
@@ -22059,7 +22187,7 @@ async function handleClusterReject(db, task, comment, log) {
22059
22187
  if (carrier) {
22060
22188
  const now = Date.now();
22061
22189
  for (const t of await loadCreatePRTargets(db, carrier)) {
22062
- const id = `wa_${nanoid30(12)}`;
22190
+ const id = `wa_${nanoid28(12)}`;
22063
22191
  const action = {
22064
22192
  type: "close_pr",
22065
22193
  taskId: carrier,
@@ -22084,7 +22212,7 @@ async function handleClusterReject(db, task, comment, log) {
22084
22212
  if (deliveredRepos.size > 0) {
22085
22213
  const now = Date.now();
22086
22214
  for (const repoName of deliveredRepos) {
22087
- const id = `wa_${nanoid30(12)}`;
22215
+ const id = `wa_${nanoid28(12)}`;
22088
22216
  const action = {
22089
22217
  type: "rewrite_cluster_branch",
22090
22218
  taskId: owner.id,
@@ -24645,25 +24773,25 @@ async function requestGrantsFrom(spec, recipient, bindingVersion, log, timeoutMs
24645
24773
  const requestId2 = msg.requestId;
24646
24774
  return await new Promise((resolve2) => {
24647
24775
  const timer = setTimeout(() => {
24648
- pending3.delete(requestId2);
24776
+ pending2.delete(requestId2);
24649
24777
  log.warn({ projectId: spec.projectId, jobId: spec.jobId, sandboxId: recipient.sandboxId }, "credential grants: CLI did not answer in time");
24650
24778
  resolve2(null);
24651
24779
  }, timeoutMs);
24652
- pending3.set(requestId2, {
24780
+ pending2.set(requestId2, {
24653
24781
  projectId: spec.projectId,
24654
24782
  jobId: spec.jobId,
24655
24783
  sandboxId: recipient.sandboxId,
24656
24784
  holder,
24657
24785
  resolve: (answer) => {
24658
24786
  clearTimeout(timer);
24659
- pending3.delete(requestId2);
24787
+ pending2.delete(requestId2);
24660
24788
  resolve2(answer);
24661
24789
  }
24662
24790
  });
24663
24791
  const sent = holder.kind === "provider" ? sendToAgentLoginProvider(holder.ownerKey, holder.providerId, msg) : sendToProjectCli(spec.projectId, msg);
24664
24792
  if (!sent) {
24665
24793
  clearTimeout(timer);
24666
- pending3.delete(requestId2);
24794
+ pending2.delete(requestId2);
24667
24795
  resolve2(null);
24668
24796
  }
24669
24797
  });
@@ -24775,7 +24903,7 @@ async function sweepExpiredGrants(db, now = Date.now()) {
24775
24903
  const { changes } = await db.run(`DELETE FROM credential_grants WHERE not_after <= ?`, now);
24776
24904
  return changes;
24777
24905
  }
24778
- var GRANT_REQUEST_TIMEOUT_MS, pending3, LATE_ANSWER_TTL_MS, lateAnswers;
24906
+ var GRANT_REQUEST_TIMEOUT_MS, pending2, LATE_ANSWER_TTL_MS, lateAnswers;
24779
24907
  var init_credential_grants = __esm({
24780
24908
  "src/tracker/credential-grants.ts"() {
24781
24909
  "use strict";
@@ -24786,7 +24914,7 @@ var init_credential_grants = __esm({
24786
24914
  init_ws();
24787
24915
  init_sandbox_keys();
24788
24916
  GRANT_REQUEST_TIMEOUT_MS = 6e3;
24789
- pending3 = /* @__PURE__ */ new Map();
24917
+ pending2 = /* @__PURE__ */ new Map();
24790
24918
  LATE_ANSWER_TTL_MS = 6e4;
24791
24919
  lateAnswers = /* @__PURE__ */ new Map();
24792
24920
  }
@@ -24840,7 +24968,7 @@ init_regimen_config();
24840
24968
  // src/projects/cred-updates.ts
24841
24969
  init_dist();
24842
24970
  init_ws();
24843
- import { nanoid as nanoid31 } from "nanoid";
24971
+ import { nanoid as nanoid29 } from "nanoid";
24844
24972
  async function latestCredUpdates(db, projectId) {
24845
24973
  const rows = await db.all(
24846
24974
  `SELECT id, kind, state, detail, updated_at as updatedAt
@@ -24865,6 +24993,7 @@ async function latestCredUpdates(db, projectId) {
24865
24993
 
24866
24994
  // src/application/read-model.ts
24867
24995
  init_ws();
24996
+ init_farm2();
24868
24997
  init_queue_view();
24869
24998
  init_state_machine();
24870
24999
  init_delivery_decision();
@@ -25076,7 +25205,7 @@ function configProposalRow(stored) {
25076
25205
  }
25077
25206
 
25078
25207
  // src/config-proposals/state-machine.ts
25079
- import { nanoid as nanoid32 } from "nanoid";
25208
+ import { nanoid as nanoid30 } from "nanoid";
25080
25209
  function rowToProposal(raw) {
25081
25210
  return {
25082
25211
  id: raw["id"],
@@ -25101,7 +25230,7 @@ var PROPOSAL_COLUMNS = `id, project_id as projectId, user_input as userInput, st
25101
25230
  created_at as createdAt, updated_at as updatedAt`;
25102
25231
  var PROPOSAL_SELECT = `SELECT ${PROPOSAL_COLUMNS} FROM config_proposals`;
25103
25232
  async function createProposal(db, projectId, userInput) {
25104
- const id = `cfp_${nanoid32(16)}`;
25233
+ const id = `cfp_${nanoid30(16)}`;
25105
25234
  const now = Date.now();
25106
25235
  await db.run(
25107
25236
  `INSERT INTO config_proposals
@@ -25803,7 +25932,7 @@ async function projectDetailOf(db, projectId, raw) {
25803
25932
  actionErrors: getProjectActions(projectId)?.actionErrors ?? []
25804
25933
  };
25805
25934
  }
25806
- function createReadModel(db) {
25935
+ function createReadModel(db, options = {}) {
25807
25936
  const SUBMISSION_COLUMNS = `${DRAFT_COLUMNS},
25808
25937
  (SELECT COUNT(*) FROM draft_tasks c WHERE c.parent_draft_id = draft_tasks.id) as childCount,
25809
25938
  EXISTS (SELECT 1 FROM tasks t
@@ -26495,6 +26624,35 @@ function createReadModel(db) {
26495
26624
  const p = await getPresence(db, { email: viewer.email ?? "", userId: viewer.subjectId });
26496
26625
  return presenceRowOf(p, new Set(viewer.projectIds));
26497
26626
  },
26627
+ // --- the account's fleet (DEV-927) ---
26628
+ async listFarms(viewer) {
26629
+ const isOperator = viewer.email !== null && (options.isOperator?.(viewer.email) ?? false);
26630
+ return await listFarms(db, { email: viewer.email ?? "", userId: viewer.subjectId, isSuperAdmin: isOperator });
26631
+ },
26632
+ async listProjectDeliverables(projectId) {
26633
+ const rows = await db.all(
26634
+ `SELECT id, tracker_issue_key AS "taskKey", title, phase, updated_at AS "updatedAt"
26635
+ FROM tasks
26636
+ WHERE project_id = ? AND (archived IS NULL OR archived = 0)
26637
+ AND phase IN ('TRACKING', 'AWAITING_CLUSTER', 'DONE', 'NEEDS_MANUAL')
26638
+ ORDER BY CASE WHEN phase IN ('TRACKING', 'AWAITING_CLUSTER') THEN 0 ELSE 1 END,
26639
+ updated_at DESC
26640
+ LIMIT 50`,
26641
+ projectId
26642
+ );
26643
+ const out = [];
26644
+ for (const r of rows) {
26645
+ const byRepo = await loadTaskBranchesByRepo(db, r.id);
26646
+ const repos = Object.entries(byRepo).map(([repoName, b]) => ({
26647
+ repoName,
26648
+ branchName: b.branchName,
26649
+ baseRef: b.baseRef
26650
+ }));
26651
+ if (repos.length === 0) continue;
26652
+ out.push({ taskKey: r.taskKey, title: r.title, phase: r.phase, updatedAt: Number(r.updatedAt), repos });
26653
+ }
26654
+ return out;
26655
+ },
26498
26656
  async readNotificationSettings(subjectId) {
26499
26657
  const user = await db.get(`SELECT 1 as hit FROM users WHERE id = ?`, subjectId);
26500
26658
  if (!user) return null;
@@ -26635,7 +26793,7 @@ function createAuthorizer(db, config) {
26635
26793
  // src/jobs/apply-result.ts
26636
26794
  init_apply_overrides();
26637
26795
  init_publish_delivery();
26638
- import { nanoid as nanoid33 } from "nanoid";
26796
+ import { nanoid as nanoid31 } from "nanoid";
26639
26797
 
26640
26798
  // src/drafts/result-handler.ts
26641
26799
  init_dist();
@@ -27251,7 +27409,7 @@ async function applyJobResult(fx, job, result) {
27251
27409
  size_bytes = excluded.size_bytes,
27252
27410
  source_job_id = excluded.source_job_id,
27253
27411
  created_at = excluded.created_at`,
27254
- `art_${nanoid33(16)}`,
27412
+ `art_${nanoid31(16)}`,
27255
27413
  job.taskId,
27256
27414
  a.path,
27257
27415
  encoding,
@@ -28018,7 +28176,7 @@ init_state_machine2();
28018
28176
  // src/drafts/publish.ts
28019
28177
  init_ws();
28020
28178
  init_state_machine2();
28021
- import { nanoid as nanoid34 } from "nanoid";
28179
+ import { nanoid as nanoid32 } from "nanoid";
28022
28180
  function firstNonBlank(...vals) {
28023
28181
  for (const v of vals) {
28024
28182
  if (v != null && v.trim() !== "") return v;
@@ -28026,7 +28184,7 @@ function firstNonBlank(...vals) {
28026
28184
  return "";
28027
28185
  }
28028
28186
  async function dispatchPublish(db, draft, targetStatus, log) {
28029
- const publishActionId = `dpub_${nanoid34(16)}`;
28187
+ const publishActionId = `dpub_${nanoid32(16)}`;
28030
28188
  const title = firstNonBlank(draft.editedTitle, draft.generatedTitle);
28031
28189
  const body = firstNonBlank(draft.editedBody, draft.generatedBody);
28032
28190
  const project = await db.get(`SELECT youtrack_project_key as youtrackProjectKey FROM projects WHERE id = ?`, draft.projectId);
@@ -28172,7 +28330,7 @@ async function maybeAdvancePackage(db, parentDraftId, log) {
28172
28330
  log.info({ draftId: parentDraftId }, "package published (no links) \u2192 DONE");
28173
28331
  return;
28174
28332
  }
28175
- const linkActionId = `dpkg_${nanoid34(16)}`;
28333
+ const linkActionId = `dpkg_${nanoid32(16)}`;
28176
28334
  const action = {
28177
28335
  type: "apply_tracker_links",
28178
28336
  draftId: parentDraftId,
@@ -28252,7 +28410,83 @@ async function discardDraft(db, draft, log) {
28252
28410
  init_dist2();
28253
28411
  init_regimen_write();
28254
28412
  init_ws();
28255
- init_provision();
28413
+
28414
+ // src/farm/provision.ts
28415
+ init_ws();
28416
+ init_pool_spec();
28417
+ import { nanoid as nanoid33 } from "nanoid";
28418
+ var pending3 = /* @__PURE__ */ new Map();
28419
+ function awaitResult(requestId2, send, timeoutMs) {
28420
+ if (!send()) return Promise.reject(new Error("farm_offline"));
28421
+ return new Promise((resolve2, reject) => {
28422
+ const timer = setTimeout(() => {
28423
+ pending3.delete(requestId2);
28424
+ reject(new Error("farm_timeout"));
28425
+ }, timeoutMs);
28426
+ pending3.set(requestId2, { resolve: resolve2, reject, timer });
28427
+ });
28428
+ }
28429
+ function requestProvision(farmId, projectId, sealedBundle, timeoutMs) {
28430
+ const requestId2 = nanoid33(16);
28431
+ return awaitResult(
28432
+ requestId2,
28433
+ () => sendToFarm(farmId, { type: "provision_project", requestId: requestId2, projectId, sealedBundle }),
28434
+ timeoutMs
28435
+ );
28436
+ }
28437
+ function requestDelete(farmId, projectId, timeoutMs) {
28438
+ const requestId2 = nanoid33(16);
28439
+ return awaitResult(
28440
+ requestId2,
28441
+ () => sendToFarm(farmId, { type: "delete_project", requestId: requestId2, projectId }),
28442
+ timeoutMs
28443
+ );
28444
+ }
28445
+ async function requestFarmPoolSync(db, farmId) {
28446
+ const accounts = await computeFarmPoolSpec(db, farmId);
28447
+ sendToFarm(farmId, buildSyncWorkerPoolMessage(accounts));
28448
+ }
28449
+
28450
+ // src/runner/provision.ts
28451
+ init_ws();
28452
+ import { nanoid as nanoid34 } from "nanoid";
28453
+ var pending4 = /* @__PURE__ */ new Map();
28454
+ function awaitResult2(requestId2, send, timeoutMs) {
28455
+ if (!send()) return Promise.reject(new Error("runner_offline"));
28456
+ return new Promise((resolve2, reject) => {
28457
+ const timer = setTimeout(() => {
28458
+ pending4.delete(requestId2);
28459
+ reject(new Error("runner_timeout"));
28460
+ }, timeoutMs);
28461
+ pending4.set(requestId2, { resolve: resolve2, reject, timer });
28462
+ });
28463
+ }
28464
+ function requestGhRunnersProvision(runnerId, projectId, sealedBundle, timeoutMs) {
28465
+ const requestId2 = nanoid34(16);
28466
+ return awaitResult2(
28467
+ requestId2,
28468
+ () => sendToRunner(runnerId, { type: "provision_gh_runners", requestId: requestId2, projectId, sealedBundle }),
28469
+ timeoutMs
28470
+ );
28471
+ }
28472
+ function requestGhRunnersDelete(runnerId, projectId, sealedBundle, timeoutMs) {
28473
+ const requestId2 = nanoid34(16);
28474
+ return awaitResult2(
28475
+ requestId2,
28476
+ () => sendToRunner(runnerId, {
28477
+ type: "delete_gh_runners",
28478
+ requestId: requestId2,
28479
+ projectId,
28480
+ ...sealedBundle ? { sealedBundle } : {}
28481
+ }),
28482
+ timeoutMs
28483
+ );
28484
+ }
28485
+
28486
+ // src/application/write-model.ts
28487
+ init_farm2();
28488
+ init_runner2();
28489
+ init_init_status();
28256
28490
 
28257
28491
  // src/projects/forget.ts
28258
28492
  init_legacy_cred_bundles();
@@ -28296,8 +28530,57 @@ async function deleteProjectData(db, projectId) {
28296
28530
  });
28297
28531
  }
28298
28532
 
28533
+ // src/runner/bindings.ts
28534
+ function parseGhRunnerBindings(json) {
28535
+ if (!json) return [];
28536
+ try {
28537
+ const parsed = JSON.parse(json);
28538
+ return Array.isArray(parsed.bindings) ? parsed.bindings : [];
28539
+ } catch {
28540
+ return [];
28541
+ }
28542
+ }
28543
+ function serialize(bindings) {
28544
+ return JSON.stringify({ bindings });
28545
+ }
28546
+ async function upsertGhRunnerBinding(db, projectId, binding) {
28547
+ await db.tx(async (tx) => {
28548
+ const row = await tx.get(
28549
+ `SELECT gh_runner_bindings_json as bindingsJson FROM projects WHERE id = ?`,
28550
+ projectId
28551
+ );
28552
+ if (!row) return;
28553
+ const bindings = parseGhRunnerBindings(row.bindingsJson).filter(
28554
+ (b) => !(b.owner === binding.owner && b.repo === binding.repo)
28555
+ );
28556
+ bindings.push(binding);
28557
+ await tx.run(
28558
+ `UPDATE projects SET gh_runner_bindings_json = ? WHERE id = ?`,
28559
+ serialize(bindings),
28560
+ projectId
28561
+ );
28562
+ });
28563
+ }
28564
+ async function removeGhRunnerBinding(db, projectId, owner, repo) {
28565
+ return db.tx(async (tx) => {
28566
+ const row = await tx.get(
28567
+ `SELECT gh_runner_bindings_json as bindingsJson FROM projects WHERE id = ?`,
28568
+ projectId
28569
+ );
28570
+ if (!row) return false;
28571
+ const bindings = parseGhRunnerBindings(row.bindingsJson);
28572
+ const kept = bindings.filter((b) => !(b.owner === owner && b.repo === repo));
28573
+ if (kept.length === bindings.length) return false;
28574
+ await tx.run(
28575
+ `UPDATE projects SET gh_runner_bindings_json = ? WHERE id = ?`,
28576
+ serialize(kept),
28577
+ projectId
28578
+ );
28579
+ return true;
28580
+ });
28581
+ }
28582
+
28299
28583
  // src/application/write-model.ts
28300
- init_bindings();
28301
28584
  init_tracking_commands();
28302
28585
 
28303
28586
  // src/config-proposals/apply.ts
@@ -28373,10 +28656,26 @@ function conflict(reason) {
28373
28656
  function stale() {
28374
28657
  return { ok: false, refusal: "stale_revision" };
28375
28658
  }
28659
+ function notFound() {
28660
+ return { ok: false, refusal: "not_found" };
28661
+ }
28662
+ var FARM_PROVISION_TIMEOUT_MS = 6e4;
28663
+ var GH_RUNNER_RELAY_TIMEOUT_MS = 12e4;
28664
+ function nodeUnavailable(capability, err) {
28665
+ const reason = err instanceof Error ? err.message : `${capability}_error`;
28666
+ const what = capability === "farm" ? "The farm" : "The runner host";
28667
+ return {
28668
+ ok: false,
28669
+ refusal: "capability_unavailable",
28670
+ capability,
28671
+ 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.`,
28672
+ retryable: true
28673
+ };
28674
+ }
28376
28675
  function taskState(row) {
28377
28676
  return { ...row, revision: String(row.updatedAt) };
28378
28677
  }
28379
- function createWriteModel(db, log) {
28678
+ function createWriteModel(db, log, hooks = {}) {
28380
28679
  async function readTaskRow(projectId, key) {
28381
28680
  const row = await db.get(
28382
28681
  `SELECT project_id as projectId, tracker_issue_key as key, phase,
@@ -28451,6 +28750,13 @@ function createWriteModel(db, log) {
28451
28750
  if (sandboxId !== null) sendToSandbox(sandboxId, { type: "pause_job", jobId: stored.sourceJobId });
28452
28751
  await cancelActiveJobs(db, `config::${stored.id}`);
28453
28752
  }
28753
+ function isOperator(actor) {
28754
+ return actor.email !== null && (hooks.isOperator?.(actor.email) ?? false);
28755
+ }
28756
+ async function ghRunnerHost(runnerId, actor) {
28757
+ 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);
28758
+ return owns ? ok(true) : notFound();
28759
+ }
28454
28760
  return {
28455
28761
  async readTask(projectId, taskKey) {
28456
28762
  const row = await readTaskRow(projectId, taskKey);
@@ -28922,6 +29228,143 @@ function createWriteModel(db, log) {
28922
29228
  * daemon behind it. The Context Repo remote is not touched: what the
28923
29229
  * pipeline wrote about the work outlives the record of the project.
28924
29230
  */
29231
+ // --- registration and the account's fleet (DEV-927) ---
29232
+ async createProject({ project, owner }) {
29233
+ const trackerProvider = project.trackerProvider;
29234
+ const inserted = await db.tx(async (tx) => {
29235
+ const existing = await tx.get(`SELECT id FROM projects WHERE id = ?`, project.projectId);
29236
+ if (existing) return conflict("project_already_exists");
29237
+ const now = Date.now();
29238
+ await tx.run(
29239
+ `INSERT INTO projects (id, name, automation_level, youtrack_project_key, tracker_provider,
29240
+ repos_json, owner_email, owner_user_id, language_json,
29241
+ provisioning_status, context_repo_json, provisioned_at, provisioning_error, created_at)
29242
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'provisioned', ?, ?, NULL, ?)`,
29243
+ project.projectId,
29244
+ project.name,
29245
+ project.automationLevel,
29246
+ project.trackerProjectKey,
29247
+ trackerProvider,
29248
+ JSON.stringify(
29249
+ project.repos.map((r) => ({
29250
+ name: r.name,
29251
+ host: r.host,
29252
+ owner: r.owner,
29253
+ repoName: r.repoName,
29254
+ ...r.defaultBranch ? { defaultBranch: r.defaultBranch } : {},
29255
+ ...r.mountAs ? { mountAs: r.mountAs } : {},
29256
+ ...r.mirror ? { mirror: { host: r.mirror.host, owner: r.mirror.owner, repoName: r.mirror.repoName } } : {}
29257
+ }))
29258
+ ),
29259
+ owner.email,
29260
+ owner.subjectId,
29261
+ project.language === null ? null : JSON.stringify(project.language),
29262
+ JSON.stringify(project.contextRepo),
29263
+ now,
29264
+ now
29265
+ );
29266
+ return ok({ projectId: project.projectId });
29267
+ });
29268
+ if (!inserted.ok) return inserted;
29269
+ await beginInitTracking(db, project.projectId, "registered");
29270
+ log.info({ projectId: project.projectId, contextRepo: project.contextRepo }, "project registered, Context Repo provisioned by the client");
29271
+ hooks.projectRegistered?.({
29272
+ projectId: project.projectId,
29273
+ name: project.name,
29274
+ automationLevel: project.automationLevel,
29275
+ ownerEmail: owner.email,
29276
+ repoCount: project.repos.length
29277
+ });
29278
+ return inserted;
29279
+ },
29280
+ async provisionOnFarm({ farmId, projectId, sealedBundle, actor }) {
29281
+ 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);
29282
+ if (!usable) return notFound();
29283
+ await beginInitTracking(db, projectId, "farm_provisioning");
29284
+ let result;
29285
+ try {
29286
+ result = await requestProvision(farmId, projectId, sealedBundle, FARM_PROVISION_TIMEOUT_MS);
29287
+ } catch (err) {
29288
+ const reason = err instanceof Error ? err.message : "provision_failed";
29289
+ await recordInitError(
29290
+ db,
29291
+ projectId,
29292
+ reason === "farm_offline" ? "the hosting farm is offline" : reason === "farm_timeout" ? "the hosting farm did not respond in time" : `farm provisioning failed: ${reason}`
29293
+ );
29294
+ return nodeUnavailable("farm", err);
29295
+ }
29296
+ if (result.ok) {
29297
+ await db.run(`UPDATE projects SET farm_id = ? WHERE id = ?`, farmId, projectId);
29298
+ await advanceInitPhase(db, projectId, "daemon_starting");
29299
+ requestFarmPoolSync(db, farmId).catch(
29300
+ (err) => log.warn({ farmId, err }, "farm pool sync after provision failed")
29301
+ );
29302
+ } else {
29303
+ await recordInitError(db, projectId, `farm provisioning failed: ${result.error ?? "unknown error"}`);
29304
+ }
29305
+ return ok({ ok: result.ok, daemonStatus: result.daemonStatus ?? null, error: result.error ?? null });
29306
+ },
29307
+ async provisionGhRunners({ runnerId, projectId, sealedBundle, actor }) {
29308
+ const guard = await ghRunnerHost(runnerId, actor);
29309
+ if (!guard.ok) return guard;
29310
+ let result;
29311
+ try {
29312
+ result = await requestGhRunnersProvision(runnerId, projectId, sealedBundle, GH_RUNNER_RELAY_TIMEOUT_MS);
29313
+ } catch (err) {
29314
+ return nodeUnavailable("runner", err);
29315
+ }
29316
+ const runners = result.runners ?? [];
29317
+ if (result.ok) {
29318
+ const connectedAt = Date.now();
29319
+ for (const r of runners) {
29320
+ await upsertGhRunnerBinding(db, projectId, {
29321
+ owner: r.owner,
29322
+ repo: r.repo,
29323
+ runnerId,
29324
+ connectedAt,
29325
+ status: r.status,
29326
+ ...r.error ? { error: r.error } : {},
29327
+ ...r.labels ? { labels: r.labels } : {}
29328
+ });
29329
+ }
29330
+ }
29331
+ return ok({
29332
+ ok: result.ok,
29333
+ runners: runners.map((r) => ({ owner: r.owner, repo: r.repo, status: r.status, error: r.error ?? null, labels: r.labels ?? [] })),
29334
+ error: result.error ?? null
29335
+ });
29336
+ },
29337
+ async teardownGhRunners({ runnerId, projectId, sealedBundle, repo, actor }) {
29338
+ const guard = await ghRunnerHost(runnerId, actor);
29339
+ if (!guard.ok) return guard;
29340
+ let result;
29341
+ try {
29342
+ result = await requestGhRunnersDelete(runnerId, projectId, sealedBundle ?? void 0, GH_RUNNER_RELAY_TIMEOUT_MS);
29343
+ } catch (err) {
29344
+ return nodeUnavailable("runner", err);
29345
+ }
29346
+ if (result.ok && repo !== null) {
29347
+ await removeGhRunnerBinding(db, projectId, repo.owner, repo.repo);
29348
+ }
29349
+ return ok({ ok: result.ok, error: result.error ?? null });
29350
+ },
29351
+ async enqueueProjectIndex({ projectId, kind }) {
29352
+ const p = await db.get(`SELECT id, context_repo_json as contextRepoJson FROM projects WHERE id = ?`, projectId);
29353
+ if (!p) return notFound();
29354
+ if (!p.contextRepoJson) return conflict("context_repo_not_provisioned");
29355
+ const jobId = kind === "backfill" ? await enqueueBackfillJob(db, projectId) : await enqueueIndexJob(db, projectId);
29356
+ await advanceInitPhase(db, projectId, "indexing");
29357
+ log.info({ projectId, jobId, kind }, kind === "backfill" ? "backfill job enqueued" : "index job enqueued");
29358
+ return ok({ jobId });
29359
+ },
29360
+ async reportInitStatus({ projectId, phase, error }) {
29361
+ const recorded = await advanceInitPhase(db, projectId, phase);
29362
+ if (error !== null) {
29363
+ const cur = await getInitStatus(db, projectId);
29364
+ if (cur && cur.phase === phase) await recordInitError(db, projectId, error);
29365
+ }
29366
+ return ok({ recorded });
29367
+ },
28925
29368
  async deleteProject({ projectId }) {
28926
29369
  const row = await db.get(`SELECT farm_id as farmId FROM projects WHERE id = ?`, projectId);
28927
29370
  if (!row) return conflict("project_not_found");
@@ -29289,9 +29732,9 @@ function createDirectPipeline(deps) {
29289
29732
  const materialization = {
29290
29733
  async next(projectId) {
29291
29734
  for (; ; ) {
29292
- const pending4 = await nextPendingProjectAction(db, projectId);
29293
- if (!pending4) return null;
29294
- const prepared = await prepareDispatch(db, pending4.id, log);
29735
+ const pending5 = await nextPendingProjectAction(db, projectId);
29736
+ if (!pending5) return null;
29737
+ const prepared = await prepareDispatch(db, pending5.id, log);
29295
29738
  if (prepared.kind === "settled") continue;
29296
29739
  if (prepared.kind !== "ready") return null;
29297
29740
  await markDispatched(db, prepared.actionId);
@@ -29498,7 +29941,7 @@ async function compose(db, config, log, issueGrants) {
29498
29941
  const notify2 = { db, config, channels: [new EmailChannel(mailer)], log };
29499
29942
  setNotifyDeps(notify2);
29500
29943
  const authorizer = createAuthorizer(db, config);
29501
- const reads = createReadOperations(createReadModel(db), authorizer);
29944
+ const reads = createReadOperations(createReadModel(db, { isOperator: (email) => isSuperUser(config, email) }), authorizer);
29502
29945
  const pipeline = createDirectPipeline({
29503
29946
  db,
29504
29947
  log,