@kici-dev/agent 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -27,7 +27,8 @@ import { gcStaleTmpDirs } from "@kici-dev/core/tmp-gc";
27
27
  import fsPromises, { access, lstat, mkdir, readFile, readdir, realpath, unlink, writeFile } from "node:fs/promises";
28
28
  import { makeTempDir } from "@kici-dev/core/tmp";
29
29
  import Docker from "dockerode";
30
- import { buildKiciApi, buildNeedsContext, isDynamicGroupRef, isDynamicJobFn, isParallelGroup, isStaticArray, isStaticObject, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
30
+ import { buildKiciApi, buildNeedsContext, createFilterContext, isDynamicGroupRef, isDynamicJobFn, isParallelGroup, isStaticArray, isStaticObject, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
31
+ import { $ } from "zx";
31
32
  import { c, x } from "tar";
32
33
  import https from "node:https";
33
34
  import http from "node:http";
@@ -324,7 +325,7 @@ var LogBuffer = class extends RingBuffer {
324
325
  };
325
326
  //#endregion
326
327
  //#region src/ws/orchestrator-client.ts
327
- const logger$12 = createLogger({ prefix: "orchestrator-client" });
328
+ const logger$13 = createLogger({ prefix: "orchestrator-client" });
328
329
  /**
329
330
  * How long to wait for `artifacts.upload.complete.ack` before failing the step.
330
331
  *
@@ -484,7 +485,7 @@ var OrchestratorClient = class OrchestratorClient {
484
485
  */
485
486
  connect() {
486
487
  if (this._state !== "disconnected") {
487
- logger$12.warn("connect() called while not disconnected", { state: this._state });
488
+ logger$13.warn("connect() called while not disconnected", { state: this._state });
488
489
  return;
489
490
  }
490
491
  this.intentionalDisconnect = false;
@@ -1048,7 +1049,7 @@ var OrchestratorClient = class OrchestratorClient {
1048
1049
  }
1049
1050
  });
1050
1051
  } catch (err) {
1051
- logger$12.error("Failed to create WebSocket", { error: toErrorMessage(err) });
1052
+ logger$13.error("Failed to create WebSocket", { error: toErrorMessage(err) });
1052
1053
  this._state = "disconnected";
1053
1054
  this.scheduleReconnect();
1054
1055
  return;
@@ -1056,7 +1057,7 @@ var OrchestratorClient = class OrchestratorClient {
1056
1057
  this.ws.on("open", () => {
1057
1058
  if (this.token) {
1058
1059
  this._state = "authenticating";
1059
- logger$12.info("Connected to orchestrator, sending auth.request", {
1060
+ logger$13.info("Connected to orchestrator, sending auth.request", {
1060
1061
  url: this.url,
1061
1062
  agentId: this.agentId
1062
1063
  });
@@ -1067,7 +1068,7 @@ var OrchestratorClient = class OrchestratorClient {
1067
1068
  }));
1068
1069
  } else {
1069
1070
  this._state = "registering";
1070
- logger$12.info("Connected to orchestrator, sending agent.register (no token)", {
1071
+ logger$13.info("Connected to orchestrator, sending agent.register (no token)", {
1071
1072
  url: this.url,
1072
1073
  agentId: this.agentId
1073
1074
  });
@@ -1078,12 +1079,12 @@ var OrchestratorClient = class OrchestratorClient {
1078
1079
  this.handleMessage(data);
1079
1080
  });
1080
1081
  this.ws.on("close", (code, reason) => {
1081
- logger$12.info("Orchestrator connection closed", {
1082
+ logger$13.info("Orchestrator connection closed", {
1082
1083
  code,
1083
1084
  reason: reason.toString()
1084
1085
  });
1085
1086
  if (code === WS_CLOSE_AGENT_AUTH_FAILED) {
1086
- logger$12.error("Orchestrator closed with auth-failed code -- token is invalid or revoked. NOT retrying.", {
1087
+ logger$13.error("Orchestrator closed with auth-failed code -- token is invalid or revoked. NOT retrying.", {
1087
1088
  code,
1088
1089
  reason: reason.toString()
1089
1090
  });
@@ -1120,7 +1121,7 @@ var OrchestratorClient = class OrchestratorClient {
1120
1121
  if (!this.intentionalDisconnect) this.scheduleReconnect();
1121
1122
  });
1122
1123
  this.ws.on("error", (err) => {
1123
- logger$12.error(`Orchestrator WebSocket error: ${err.message}`);
1124
+ logger$13.error(`Orchestrator WebSocket error: ${err.message}`);
1124
1125
  if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.close();
1125
1126
  });
1126
1127
  }
@@ -1228,7 +1229,7 @@ var OrchestratorClient = class OrchestratorClient {
1228
1229
  this.pendingUserArtifactRequests.set(record.messageId, record.pending);
1229
1230
  this.resendableCompletes.set(record.messageId, record);
1230
1231
  this.sendDirect(record.frame);
1231
- logger$12.info("Re-sent artifact upload-complete after reconnect", {
1232
+ logger$13.info("Re-sent artifact upload-complete after reconnect", {
1232
1233
  messageId: record.messageId,
1233
1234
  attempt: record.attempts
1234
1235
  });
@@ -1255,7 +1256,7 @@ var OrchestratorClient = class OrchestratorClient {
1255
1256
  try {
1256
1257
  raw = JSON.parse(data.toString());
1257
1258
  } catch {
1258
- logger$12.warn("Malformed JSON received from orchestrator");
1259
+ logger$13.warn("Malformed JSON received from orchestrator");
1259
1260
  return;
1260
1261
  }
1261
1262
  const rawMsg = raw;
@@ -1314,13 +1315,13 @@ var OrchestratorClient = class OrchestratorClient {
1314
1315
  switch (msg.type) {
1315
1316
  case "auth.success":
1316
1317
  if (this._state === "authenticating") {
1317
- logger$12.info("Authentication successful, sending agent.register", { connectionId: msg.connectionId });
1318
+ logger$13.info("Authentication successful, sending agent.register", { connectionId: msg.connectionId });
1318
1319
  this._state = "registering";
1319
1320
  this.sendAgentRegister();
1320
1321
  }
1321
1322
  break;
1322
1323
  case "auth.failure":
1323
- logger$12.error("Authentication FAILED -- token is invalid or expired. NOT retrying.", { reason: msg.reason });
1324
+ logger$13.error("Authentication FAILED -- token is invalid or expired. NOT retrying.", { reason: msg.reason });
1324
1325
  this.authFailed = true;
1325
1326
  this.intentionalDisconnect = true;
1326
1327
  if (this.ws) {
@@ -1330,7 +1331,7 @@ var OrchestratorClient = class OrchestratorClient {
1330
1331
  this._state = "disconnected";
1331
1332
  break;
1332
1333
  case "register.ack":
1333
- logger$12.info("Registration acknowledged by orchestrator", {
1334
+ logger$13.info("Registration acknowledged by orchestrator", {
1334
1335
  agentId: msg.agentId,
1335
1336
  labels: msg.labels,
1336
1337
  scalerManaged: msg.scalerManaged,
@@ -1347,14 +1348,14 @@ var OrchestratorClient = class OrchestratorClient {
1347
1348
  this.sendConfigAck(msg.agentId);
1348
1349
  break;
1349
1350
  case "job.dispatch":
1350
- logger$12.info("Job dispatch received", {
1351
+ logger$13.info("Job dispatch received", {
1351
1352
  runId: msg.runId,
1352
1353
  jobId: msg.jobId
1353
1354
  });
1354
1355
  this.onJobDispatch(msg);
1355
1356
  break;
1356
1357
  case "job.cancel":
1357
- logger$12.info("Job cancel received", {
1358
+ logger$13.info("Job cancel received", {
1358
1359
  runId: msg.runId,
1359
1360
  jobId: msg.jobId,
1360
1361
  reason: msg.reason
@@ -1362,7 +1363,7 @@ var OrchestratorClient = class OrchestratorClient {
1362
1363
  this.onJobCancel(msg);
1363
1364
  break;
1364
1365
  case "job.concurrency.ack": {
1365
- logger$12.info("Concurrency ack received", {
1366
+ logger$13.info("Concurrency ack received", {
1366
1367
  requestId: msg.requestId,
1367
1368
  action: msg.action
1368
1369
  });
@@ -1377,7 +1378,7 @@ var OrchestratorClient = class OrchestratorClient {
1377
1378
  break;
1378
1379
  }
1379
1380
  case "step.approval-resolved": {
1380
- logger$12.info("Step approval resolved", {
1381
+ logger$13.info("Step approval resolved", {
1381
1382
  requestId: msg.requestId,
1382
1383
  runId: msg.runId,
1383
1384
  jobId: msg.jobId,
@@ -1397,7 +1398,7 @@ var OrchestratorClient = class OrchestratorClient {
1397
1398
  break;
1398
1399
  }
1399
1400
  case "fleet.logs.request":
1400
- logger$12.info("Fleet log collection requested", {
1401
+ logger$13.info("Fleet log collection requested", {
1401
1402
  requestId: msg.requestId,
1402
1403
  logWindowHours: msg.logWindowHours
1403
1404
  });
@@ -1407,7 +1408,7 @@ var OrchestratorClient = class OrchestratorClient {
1407
1408
  return;
1408
1409
  }
1409
1410
  if (heartbeatSchema.safeParse(raw).success) return;
1410
- logger$12.warn("Invalid message from orchestrator", { errors: parsed.error.issues });
1411
+ logger$13.warn("Invalid message from orchestrator", { errors: parsed.error.issues });
1411
1412
  }
1412
1413
  flushBuffer() {
1413
1414
  const events = this.eventBuffer.flush();
@@ -1421,11 +1422,11 @@ var OrchestratorClient = class OrchestratorClient {
1421
1422
  }
1422
1423
  this.disconnectedAt = null;
1423
1424
  if (events.length > 0) {
1424
- logger$12.info("Flushing event buffer", { count: events.length });
1425
+ logger$13.info("Flushing event buffer", { count: events.length });
1425
1426
  for (const msg of events) this.sendDirect(msg);
1426
1427
  }
1427
1428
  if (logLines.length > 0) {
1428
- logger$12.info("Flushing log buffer", { count: logLines.length });
1429
+ logger$13.info("Flushing log buffer", { count: logLines.length });
1429
1430
  for (let i = 0; i < logLines.length; i += OrchestratorClient.LOG_BATCH_SIZE) {
1430
1431
  const batch = logLines.slice(i, i + OrchestratorClient.LOG_BATCH_SIZE);
1431
1432
  this.sendAgentLogMessage(batch);
@@ -1478,14 +1479,14 @@ var OrchestratorClient = class OrchestratorClient {
1478
1479
  */
1479
1480
  blockMmdsAccess() {
1480
1481
  if (process.getuid?.() !== 0) {
1481
- logger$12.info("MMDS iptables block skipped (non-root) — network isolation handled by orchestrator");
1482
+ logger$13.info("MMDS iptables block skipped (non-root) — network isolation handled by orchestrator");
1482
1483
  return;
1483
1484
  }
1484
1485
  try {
1485
1486
  execSync("iptables -A OUTPUT -d 169.254.169.254 -j DROP", { timeout: 5e3 });
1486
- logger$12.info("MMDS access blocked via iptables");
1487
+ logger$13.info("MMDS access blocked via iptables");
1487
1488
  } catch (err) {
1488
- logger$12.warn("Failed to block MMDS access via iptables", { error: toErrorMessage(err) });
1489
+ logger$13.warn("Failed to block MMDS access via iptables", { error: toErrorMessage(err) });
1489
1490
  }
1490
1491
  }
1491
1492
  /**
@@ -1499,7 +1500,7 @@ var OrchestratorClient = class OrchestratorClient {
1499
1500
  messageId: `config-ack-${agentId}-${Date.now()}`,
1500
1501
  agentId
1501
1502
  }));
1502
- logger$12.info("Config ACK sent to orchestrator", { agentId });
1503
+ logger$13.info("Config ACK sent to orchestrator", { agentId });
1503
1504
  }
1504
1505
  }
1505
1506
  startHeartbeat() {
@@ -1565,12 +1566,12 @@ var OrchestratorClient = class OrchestratorClient {
1565
1566
  scheduleReconnect() {
1566
1567
  this.cancelReconnect();
1567
1568
  if (this.authFailed) {
1568
- logger$12.error("Not reconnecting: authentication permanently failed");
1569
+ logger$13.error("Not reconnecting: authentication permanently failed");
1569
1570
  return;
1570
1571
  }
1571
1572
  const delay = this.getReconnectDelay();
1572
1573
  this.reconnectAttempts++;
1573
- logger$12.info("Scheduling reconnect", {
1574
+ logger$13.info("Scheduling reconnect", {
1574
1575
  attempt: this.reconnectAttempts,
1575
1576
  delayMs: Math.round(delay)
1576
1577
  });
@@ -1655,14 +1656,14 @@ var init_console_capture = __esmMin((() => {
1655
1656
  init_console_capture();
1656
1657
  function safe(name, fallback = "unknown") {
1657
1658
  switch (name) {
1658
- case "version": return "0.4.0";
1659
- case "buildCommit": return "ffd489464";
1660
- case "sdkVersion": return "0.4.0";
1661
- case "sdkBundleHash": return "fcd0a836aa1cb92debd93e0254450614d123b3748511aa4148d956fd72a7c29f";
1662
- case "sharedVersion": return "0.4.0";
1663
- case "sharedBundleHash": return "f16fa528a8a9ef1bed8df369f0e88efafca8a9954a749005299fdbcdb93fa188";
1664
- case "engineVersion": return "0.4.0";
1665
- case "engineBundleHash": return "31e2d5c327284e1cac908d7f85cf6d878bbf87ea0da93ed09c6205ada6935f89";
1659
+ case "version": return "0.5.0";
1660
+ case "buildCommit": return "cb51c7d1e";
1661
+ case "sdkVersion": return "0.5.0";
1662
+ case "sdkBundleHash": return "5b85e7cffa4a08e39329ff80448a2744af8e5b5f9840ec3801fad45ed11a3de9";
1663
+ case "sharedVersion": return "0.5.0";
1664
+ case "sharedBundleHash": return "b6b6d1818d1fe648150422daf073dafc18b037404cacf82e0ec03b4a2863e4c4";
1665
+ case "engineVersion": return "0.5.0";
1666
+ case "engineBundleHash": return "35d174ce6f4748abffcad194b335b508cade5d1c8af16d9cd3bdcb744f38717e";
1666
1667
  default: return fallback;
1667
1668
  }
1668
1669
  }
@@ -1947,7 +1948,7 @@ async function gcStaleAgentTmpDirs(base = kiciTmpBase()) {
1947
1948
  * spawn fails and the caller clears the orchestrator's reboot-pending flag and
1948
1949
  * surfaces the error — the deadline sweep is the backstop.
1949
1950
  */
1950
- const logger$11 = createLogger({ prefix: "reboot" });
1951
+ const logger$12 = createLogger({ prefix: "reboot" });
1951
1952
  /** The OS reboot primitive for a Node platform string. */
1952
1953
  function rebootCommandFor(platform) {
1953
1954
  switch (platform) {
@@ -1985,7 +1986,7 @@ function issueReboot(platform = process.platform) {
1985
1986
  stdio: "ignore"
1986
1987
  });
1987
1988
  child.on("error", (err) => {
1988
- logger$11.error("Reboot command failed to spawn", {
1989
+ logger$12.error("Reboot command failed to spawn", {
1989
1990
  cmd,
1990
1991
  args,
1991
1992
  error: String(err)
@@ -1993,14 +1994,14 @@ function issueReboot(platform = process.platform) {
1993
1994
  reject(err);
1994
1995
  });
1995
1996
  child.on("exit", (code) => {
1996
- if (code !== 0 && code !== null) logger$11.warn("Reboot command exited non-zero (privilege denied?)", {
1997
+ if (code !== 0 && code !== null) logger$12.warn("Reboot command exited non-zero (privilege denied?)", {
1997
1998
  cmd,
1998
1999
  args,
1999
2000
  code
2000
2001
  });
2001
2002
  });
2002
2003
  child.unref();
2003
- logger$11.info("Issued host reboot", {
2004
+ logger$12.info("Issued host reboot", {
2004
2005
  cmd,
2005
2006
  args
2006
2007
  });
@@ -2259,6 +2260,229 @@ var init_git_clone = __esmMin((() => {
2259
2260
  init_ssh_auth();
2260
2261
  }));
2261
2262
  //#endregion
2263
+ //#region src/checkout/changed-files.ts
2264
+ /** Build the auth context for the fetches, mirroring git-clone.ts's auth. */
2265
+ async function buildAuthCtx(auth) {
2266
+ if (!auth) return { args: [] };
2267
+ if (auth.kind === "basic") {
2268
+ const user = auth.user ?? "x-access-token";
2269
+ return { args: ["-c", `http.extraHeader=Authorization: Basic ${Buffer.from(`${user}:${auth.secret}`).toString("base64")}`] };
2270
+ }
2271
+ const sshSetup = await setupSshAuth({
2272
+ privateKey: auth.secret,
2273
+ hostKeyPolicy: auth.sshHostKeyPolicy,
2274
+ knownHosts: auth.sshKnownHostsPem
2275
+ });
2276
+ return {
2277
+ args: [],
2278
+ env: { GIT_SSH_COMMAND: sshSetup.gitSshCommand },
2279
+ cleanup: () => sshSetup.cleanup()
2280
+ };
2281
+ }
2282
+ function git(workDir, args, ctx) {
2283
+ return execFileSync("git", [
2284
+ ...ctx.args,
2285
+ ...BASE_GIT_ARGS,
2286
+ "-C",
2287
+ workDir,
2288
+ ...args
2289
+ ], {
2290
+ encoding: "utf8",
2291
+ stdio: [
2292
+ "ignore",
2293
+ "pipe",
2294
+ "pipe"
2295
+ ],
2296
+ ...ctx.env && { env: {
2297
+ ...process.env,
2298
+ ...ctx.env
2299
+ } }
2300
+ });
2301
+ }
2302
+ function tryGit(workDir, args, ctx) {
2303
+ try {
2304
+ git(workDir, args, ctx);
2305
+ return true;
2306
+ } catch {
2307
+ return false;
2308
+ }
2309
+ }
2310
+ function parseNameOnly(out) {
2311
+ return out.split("\n").map((s) => s.replace(/\r$/, "")).filter((s) => s.length > 0);
2312
+ }
2313
+ /** Ensure `commitish` exists locally; fetch / deepen (bounded) if not. */
2314
+ function ensureCommit(workDir, commitish, ctx) {
2315
+ if (tryGit(workDir, [
2316
+ "cat-file",
2317
+ "-e",
2318
+ `${commitish}^{commit}`
2319
+ ], ctx)) return true;
2320
+ if (tryGit(workDir, [
2321
+ "fetch",
2322
+ "--depth",
2323
+ "1",
2324
+ "origin",
2325
+ commitish
2326
+ ], ctx)) {
2327
+ if (tryGit(workDir, [
2328
+ "cat-file",
2329
+ "-e",
2330
+ `${commitish}^{commit}`
2331
+ ], ctx)) return true;
2332
+ }
2333
+ for (let i = 0; i < MAX_DEEPEN; i++) {
2334
+ if (!tryGit(workDir, [
2335
+ "fetch",
2336
+ `--deepen=${DEEPEN_STEP}`,
2337
+ "origin"
2338
+ ], ctx)) break;
2339
+ if (tryGit(workDir, [
2340
+ "cat-file",
2341
+ "-e",
2342
+ `${commitish}^{commit}`
2343
+ ], ctx)) return true;
2344
+ }
2345
+ return false;
2346
+ }
2347
+ function pushDiff(workDir, before, ctx) {
2348
+ const isZero = !before || ZERO_SHA.test(before);
2349
+ const baseRef = isZero ? EMPTY_TREE_SHA : before;
2350
+ if (!isZero && !ensureCommit(workDir, before, ctx)) return {
2351
+ files: [],
2352
+ status: "unavailable"
2353
+ };
2354
+ return {
2355
+ files: parseNameOnly(git(workDir, [
2356
+ "diff",
2357
+ "--name-only",
2358
+ baseRef,
2359
+ "HEAD"
2360
+ ], ctx)),
2361
+ status: "fetched"
2362
+ };
2363
+ }
2364
+ function prDiff(workDir, base, ctx) {
2365
+ const candidates = [
2366
+ base,
2367
+ `origin/${base}`,
2368
+ "FETCH_HEAD"
2369
+ ];
2370
+ const resolveBase = () => candidates.find((c) => tryGit(workDir, [
2371
+ "rev-parse",
2372
+ "--verify",
2373
+ `${c}^{commit}`
2374
+ ], ctx));
2375
+ let baseRef = resolveBase();
2376
+ if (!baseRef) {
2377
+ if (!ensureCommit(workDir, base, ctx)) return {
2378
+ files: [],
2379
+ status: "unavailable"
2380
+ };
2381
+ baseRef = resolveBase();
2382
+ }
2383
+ if (!baseRef) return {
2384
+ files: [],
2385
+ status: "unavailable"
2386
+ };
2387
+ for (let i = 0; i <= MAX_DEEPEN; i++) {
2388
+ if (tryGit(workDir, [
2389
+ "merge-base",
2390
+ baseRef,
2391
+ "HEAD"
2392
+ ], ctx)) return {
2393
+ files: parseNameOnly(git(workDir, [
2394
+ "diff",
2395
+ "--name-only",
2396
+ `${baseRef}...HEAD`
2397
+ ], ctx)),
2398
+ status: "fetched"
2399
+ };
2400
+ if (!tryGit(workDir, [
2401
+ "fetch",
2402
+ `--deepen=${DEEPEN_STEP}`,
2403
+ "origin"
2404
+ ], ctx)) break;
2405
+ }
2406
+ return {
2407
+ files: [],
2408
+ status: "unavailable"
2409
+ };
2410
+ }
2411
+ /**
2412
+ * Compute the changed-files list from the agent's local clone (HEAD is the
2413
+ * checked-out head commit). Ground truth for job/step rule evaluation. `auth`
2414
+ * (the same credentials used for the clone) authenticates the deepen / fetch
2415
+ * calls so a private remote resolves. Returns `unavailable` for diff-less
2416
+ * events (schedule/tag/manual) or any git failure — never throws.
2417
+ */
2418
+ async function computeChangedFiles(workDir, event, auth) {
2419
+ let ctx;
2420
+ try {
2421
+ if (event.type !== "push" && event.type !== "pull_request") return {
2422
+ files: [],
2423
+ status: "unavailable"
2424
+ };
2425
+ ctx = await buildAuthCtx(auth);
2426
+ if (event.type === "push") return pushDiff(workDir, event.payload?.before ?? "", ctx);
2427
+ const base = event.baseBranch ?? event.targetBranch;
2428
+ if (!base) return {
2429
+ files: [],
2430
+ status: "unavailable"
2431
+ };
2432
+ return prDiff(workDir, base, ctx);
2433
+ } catch {
2434
+ return {
2435
+ files: [],
2436
+ status: "unavailable"
2437
+ };
2438
+ } finally {
2439
+ if (ctx?.cleanup) await ctx.cleanup().catch(() => {});
2440
+ }
2441
+ }
2442
+ var EMPTY_TREE_SHA, ZERO_SHA, MAX_DEEPEN, DEEPEN_STEP, BASE_GIT_ARGS;
2443
+ var init_changed_files = __esmMin((() => {
2444
+ init_ssh_auth();
2445
+ EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
2446
+ ZERO_SHA = /^0+$/;
2447
+ MAX_DEEPEN = 4;
2448
+ DEEPEN_STEP = 50;
2449
+ BASE_GIT_ARGS = [
2450
+ "-c",
2451
+ "safe.directory=*",
2452
+ "-c",
2453
+ "core.quotePath=false"
2454
+ ];
2455
+ }));
2456
+ //#endregion
2457
+ //#region src/execution/generator-context.ts
2458
+ /**
2459
+ * Build the context handed to a `DynamicJobFn`.
2460
+ *
2461
+ * Optional members are spread conditionally rather than assigned `undefined`,
2462
+ * so an absent `needs` / repo pair leaves no key behind — a present-but-
2463
+ * undefined key reads as "declared" to a generator and serializes differently
2464
+ * between the two evaluations.
2465
+ */
2466
+ function buildGeneratorContext(input) {
2467
+ const { workflowName, event, env, repos, needs, $, log, kici } = input;
2468
+ return {
2469
+ $,
2470
+ ctx: {
2471
+ workflow: { name: workflowName },
2472
+ event,
2473
+ ...needs && { needs }
2474
+ },
2475
+ log,
2476
+ env,
2477
+ kici,
2478
+ ...repos && {
2479
+ sourceRepo: repos.sourceRepo,
2480
+ workflowRepo: repos.workflowRepo
2481
+ }
2482
+ };
2483
+ }
2484
+ var init_generator_context = __esmMin((() => {}));
2485
+ //#endregion
2262
2486
  //#region src/execution/workflow-loader.ts
2263
2487
  /**
2264
2488
  * Workflow module loading: transforms `.ts` workflow files on import via the
@@ -2429,7 +2653,7 @@ function extractSteps(workflow, jobName) {
2429
2653
  * A sibling mismatch logs a warning; a missing target job throws a clear
2430
2654
  * determinism error.
2431
2655
  */
2432
- async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event, env, apiTransport, expectedJobNames, upstreamSnapshot, declaredNeeds) {
2656
+ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event, env, apiTransport, expectedJobNames, upstreamSnapshot, declaredNeeds, repos) {
2433
2657
  const dynamicFn = extractDynamicJobFn(workflow, dynamicIndex);
2434
2658
  const { $ } = await import("zx");
2435
2659
  const { createLogger } = await import("@kici-dev/shared");
@@ -2437,17 +2661,16 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
2437
2661
  const log = createLogger({ prefix: `dynamic-job-fn:${workflow.name}` });
2438
2662
  const kici = buildKiciApi(apiTransport ?? (() => Promise.reject(/* @__PURE__ */ new Error("Agent API not available during re-evaluation"))));
2439
2663
  const needs = upstreamSnapshot ? buildNeedsContext(upstreamSnapshot, declaredNeeds ?? []) : void 0;
2440
- const generatedJobs = await dynamicFn({
2664
+ const generatedJobs = await dynamicFn(buildGeneratorContext({
2665
+ workflowName: workflow.name,
2666
+ event,
2667
+ env,
2668
+ ...repos && { repos },
2669
+ ...needs && { needs },
2441
2670
  $,
2442
- ctx: {
2443
- workflow: { name: workflow.name },
2444
- event,
2445
- ...needs && { needs }
2446
- },
2447
2671
  log,
2448
- env,
2449
2672
  kici
2450
- });
2673
+ }));
2451
2674
  const actualNames = generatedJobs.map((j) => j.name);
2452
2675
  let droppedJobs = [];
2453
2676
  if (expectedJobNames) {
@@ -2470,8 +2693,9 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
2470
2693
  }
2471
2694
  var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
2472
2695
  var init_workflow_loader = __esmMin((() => {
2473
- AGENT_SDK_VERSION = "0.4.0";
2474
- AGENT_SDK_BUNDLE_HASH = "fcd0a836aa1cb92debd93e0254450614d123b3748511aa4148d956fd72a7c29f";
2696
+ init_generator_context();
2697
+ AGENT_SDK_VERSION = "0.5.0";
2698
+ AGENT_SDK_BUNDLE_HASH = "5b85e7cffa4a08e39329ff80448a2744af8e5b5f9840ec3801fad45ed11a3de9";
2475
2699
  hookRegistered = false;
2476
2700
  }));
2477
2701
  //#endregion
@@ -2492,7 +2716,7 @@ var init_workflow_loader = __esmMin((() => {
2492
2716
  async function packKiciSource(workDir) {
2493
2717
  const kiciDir = join(workDir, ".kici");
2494
2718
  if (!existsSync(kiciDir)) throw new Error(`.kici/ not found at ${kiciDir}`);
2495
- logger$10.info("Packing .kici/ source tarball", { dir: workDir });
2719
+ logger$11.info("Packing .kici/ source tarball", { dir: workDir });
2496
2720
  const startTime = Date.now();
2497
2721
  const stream = c({
2498
2722
  gzip: true,
@@ -2506,7 +2730,7 @@ async function packKiciSource(workDir) {
2506
2730
  const hash = sha256(tarball);
2507
2731
  const sizeKB = (tarball.length / 1024).toFixed(2);
2508
2732
  const durationMs = Date.now() - startTime;
2509
- logger$10.info(".kici/ source packed", {
2733
+ logger$11.info(".kici/ source packed", {
2510
2734
  sizeKB,
2511
2735
  hash: hash.slice(0, 12),
2512
2736
  durationMs
@@ -2516,9 +2740,9 @@ async function packKiciSource(workDir) {
2516
2740
  hash
2517
2741
  };
2518
2742
  }
2519
- var logger$10;
2743
+ var logger$11;
2520
2744
  var init_source_packer = __esmMin((() => {
2521
- logger$10 = createLogger({ prefix: "source-packer" });
2745
+ logger$11 = createLogger({ prefix: "source-packer" });
2522
2746
  }));
2523
2747
  //#endregion
2524
2748
  //#region src/execution/dep-restore.ts
@@ -2605,6 +2829,52 @@ async function extractIntoScratch(url, kiciDir, attempt) {
2605
2829
  };
2606
2830
  }
2607
2831
  /**
2832
+ * Append `SCRATCH_DIR_GIT_EXCLUDE_GLOB` to `${repoWorkDir}/.git/info/exclude`
2833
+ * so any in-flight or orphaned dep-restore scratch dirs are invisible to
2834
+ * `git status` / `git add` inside the customer's cloned working tree.
2835
+ *
2836
+ * Why `.git/info/exclude` and not `.gitignore`:
2837
+ * - `.gitignore` lives in the customer's repo and is committed; we MUST NOT
2838
+ * modify it. Doing so would surface the rule in their PRs and create a
2839
+ * diff customers never asked for.
2840
+ * - `.git/info/exclude` is per-clone, on-disk only, and exactly the git
2841
+ * mechanism for "ignore these patterns in THIS working tree". Git creates
2842
+ * an empty (template-commented) file on `git init` / `git clone`, so it
2843
+ * already exists by the time we're called.
2844
+ *
2845
+ * Why this lives next to `extractIntoScratch`:
2846
+ * - The exclude glob is tied 1:1 to the scratch dir naming convention. If
2847
+ * the prefix ever changes, the rule must change too. Defining both in the
2848
+ * same file means a rename touches one place, not two.
2849
+ *
2850
+ * Best-effort: if the exclude file is missing (e.g. caller sandbox blocked
2851
+ * `git clone` and the dir layout differs) we log and continue — failing the
2852
+ * job over a missing git ignore wiring would be worse than the cosmetic
2853
+ * issue we're solving.
2854
+ *
2855
+ * Idempotent: callers may invoke this multiple times (dual-clone path, retry
2856
+ * after partial setup). We skip the append if the glob is already present.
2857
+ *
2858
+ * @param repoWorkDir - The git working tree root (the dir that contains
2859
+ * `.git/`). For normal workflows this is the agent's job workDir; for
2860
+ * global workflows it is the workflow repo dir (whose `.kici/` carries
2861
+ * the scratch dirs).
2862
+ */
2863
+ async function excludeScratchFromGit(repoWorkDir) {
2864
+ const excludePath = join(repoWorkDir, ".git", "info", "exclude");
2865
+ try {
2866
+ const existing = await fsPromises.readFile(excludePath, "utf-8").catch(() => "");
2867
+ if (existing.split("\n").some((line) => line.trim() === SCRATCH_DIR_GIT_EXCLUDE_GLOB)) return;
2868
+ const suffix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
2869
+ await fsPromises.appendFile(excludePath, `${suffix}# kici: hide dep-restore scratch dirs from customer git status\n${SCRATCH_DIR_GIT_EXCLUDE_GLOB}\n`);
2870
+ } catch (err) {
2871
+ logger$10.warn("Failed to register scratch dir glob in .git/info/exclude", {
2872
+ excludePath,
2873
+ error: err instanceof Error ? err.message : String(err)
2874
+ });
2875
+ }
2876
+ }
2877
+ /**
2608
2878
  * Rewrite localhost URLs to use the orchestrator host.
2609
2879
  *
2610
2880
  * The orchestrator rewrites file:// cache URLs to http://localhost:PORT/...
@@ -2659,7 +2929,7 @@ async function cleanupScratch(scratchDir) {
2659
2929
  force: true
2660
2930
  });
2661
2931
  } catch (cleanupErr) {
2662
- logger$9.warn("Scratch dir cleanup failed (orphan left behind)", {
2932
+ logger$10.warn("Scratch dir cleanup failed (orphan left behind)", {
2663
2933
  scratchDir,
2664
2934
  error: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)
2665
2935
  });
@@ -2684,7 +2954,7 @@ async function cleanupScratch(scratchDir) {
2684
2954
  */
2685
2955
  async function restoreDeps(workDir, depsUrl, depsHash) {
2686
2956
  depsUrl = resolveOrchestratorUrl(depsUrl);
2687
- logger$9.info("Downloading dependency tarball", { url: depsUrl });
2957
+ logger$10.info("Downloading dependency tarball", { url: depsUrl });
2688
2958
  const kiciDir = join(workDir, ".kici");
2689
2959
  if (depsUrl.startsWith("file://")) {
2690
2960
  const localPath = fileURLToPath(depsUrl);
@@ -2698,7 +2968,7 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
2698
2968
  await moveScratchIntoRepo(scratchDir, workDir);
2699
2969
  await cleanupScratch(scratchDir);
2700
2970
  const sizeMB = (data.length / (1024 * 1024)).toFixed(2);
2701
- logger$9.info("Dependencies restored from cache (file)", {
2971
+ logger$10.info("Dependencies restored from cache (file)", {
2702
2972
  sizeMB,
2703
2973
  targetDir: workDir
2704
2974
  });
@@ -2707,7 +2977,7 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
2707
2977
  if (!depsUrl.startsWith("http://") && !depsUrl.startsWith("https://")) throw new Error(`Unsupported deps URL scheme: ${depsUrl}`);
2708
2978
  let lastError;
2709
2979
  for (let attempt = 0; attempt <= 2; attempt++) {
2710
- if (attempt > 0) logger$9.warn("Retrying dep tarball download", {
2980
+ if (attempt > 0) logger$10.warn("Retrying dep tarball download", {
2711
2981
  attempt,
2712
2982
  url: depsUrl
2713
2983
  });
@@ -2716,11 +2986,11 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
2716
2986
  if (depsHash && hash !== depsHash) throw new Error(`Dep tarball hash mismatch: expected ${depsHash}, got ${hash}`);
2717
2987
  await moveScratchIntoRepo(scratchDir, workDir);
2718
2988
  await cleanupScratch(scratchDir);
2719
- logger$9.info("Dependencies restored from cache (stream)", { targetDir: workDir });
2989
+ logger$10.info("Dependencies restored from cache (stream)", { targetDir: workDir });
2720
2990
  return;
2721
2991
  } catch (err) {
2722
2992
  lastError = err instanceof Error ? err : new Error(String(err));
2723
- logger$9.warn("Dep tarball download failed", {
2993
+ logger$10.warn("Dep tarball download failed", {
2724
2994
  attempt,
2725
2995
  error: lastError.message
2726
2996
  });
@@ -2728,12 +2998,12 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
2728
2998
  }
2729
2999
  throw new Error(`Dep tarball download failed after 3 attempts: ${lastError?.message}`);
2730
3000
  }
2731
- var logger$9, DOWNLOAD_TIMEOUT_MS$1, SCRATCH_DIR_BASENAME_PREFIX;
3001
+ var logger$10, DOWNLOAD_TIMEOUT_MS$1, SCRATCH_DIR_BASENAME_PREFIX, SCRATCH_DIR_GIT_EXCLUDE_GLOB;
2732
3002
  var init_dep_restore = __esmMin((() => {
2733
- logger$9 = createLogger({ prefix: "dep-restore" });
3003
+ logger$10 = createLogger({ prefix: "dep-restore" });
2734
3004
  DOWNLOAD_TIMEOUT_MS$1 = 300 * 1e3;
2735
3005
  SCRATCH_DIR_BASENAME_PREFIX = ".dep-restore-scratch-";
2736
- `${SCRATCH_DIR_BASENAME_PREFIX}`;
3006
+ SCRATCH_DIR_GIT_EXCLUDE_GLOB = `.kici/${SCRATCH_DIR_BASENAME_PREFIX}*`;
2737
3007
  }));
2738
3008
  //#endregion
2739
3009
  //#region src/execution/download.ts
@@ -2744,6 +3014,20 @@ var init_dep_restore = __esmMin((() => {
2744
3014
  * dep-restore.ts and workflow-loader.ts.
2745
3015
  */
2746
3016
  /**
3017
+ * Whether a failed upload attempt is worth repeating.
3018
+ *
3019
+ * A transport failure (connection refused, reset, DNS) never reached a
3020
+ * responder, and 5xx / 429 are the object-storage overload signals AWS
3021
+ * documents as retry-with-backoff (S3 answers `SlowDown` with 503). Every other
3022
+ * status is a decision the server will repeat: a 403 from an expired or
3023
+ * malformed signature, a 400 from a malformed request. Retrying those burns the
3024
+ * ceiling without a chance of success and delays the real error.
3025
+ */
3026
+ function isRetryableUploadFailure(err) {
3027
+ if (!(err instanceof PresignedUploadHttpError)) return true;
3028
+ return err.statusCode >= 500 || err.statusCode === 429;
3029
+ }
3030
+ /**
2747
3031
  * Download content from an HTTP/HTTPS URL.
2748
3032
  *
2749
3033
  * Includes a 5-minute timeout to prevent the agent from hanging indefinitely
@@ -2767,21 +3051,10 @@ function downloadUrl(url) {
2767
3051
  }).on("error", reject);
2768
3052
  });
2769
3053
  }
2770
- /**
2771
- * Upload a buffer to a pre-signed S3 URL via HTTP PUT.
2772
- *
2773
- * Used for direct-to-S3 uploads of bundles and dep tarballs. Localhost /
2774
- * 127.0.0.1 URLs are rewritten via `resolveOrchestratorUrl` so the
2775
- * filesystem cache backend's signed URLs work from container agents that
2776
- * can't reach the orchestrator's host loopback directly.
2777
- *
2778
- * @param url - The pre-signed URL to upload to
2779
- * @param data - The buffer to upload
2780
- */
2781
- function uploadToPresignedUrl(url, data) {
3054
+ /** One PUT of the whole buffer. Rejects with {@link PresignedUploadHttpError} on a non-2xx. */
3055
+ function putOnce(resolvedUrl, data, timeoutMs) {
2782
3056
  return new Promise((resolve, reject) => {
2783
- const resolved = resolveOrchestratorUrl(url);
2784
- const parsed = new URL(resolved);
3057
+ const parsed = new URL(resolvedUrl);
2785
3058
  const req = (parsed.protocol === "https:" ? https : http).request({
2786
3059
  hostname: parsed.hostname,
2787
3060
  port: parsed.port,
@@ -2790,7 +3063,7 @@ function uploadToPresignedUrl(url, data) {
2790
3063
  headers: { "Content-Length": data.length }
2791
3064
  }, (res) => {
2792
3065
  if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
2793
- reject(/* @__PURE__ */ new Error(`HTTP ${res.statusCode} uploading to pre-signed URL`));
3066
+ reject(new PresignedUploadHttpError(res.statusCode));
2794
3067
  res.resume();
2795
3068
  return;
2796
3069
  }
@@ -2798,14 +3071,80 @@ function uploadToPresignedUrl(url, data) {
2798
3071
  res.on("end", () => resolve());
2799
3072
  res.on("error", reject);
2800
3073
  });
3074
+ req.setTimeout(timeoutMs, () => {
3075
+ req.destroy(/* @__PURE__ */ new Error(`Pre-signed upload timed out after ${timeoutMs}ms`));
3076
+ });
2801
3077
  req.on("error", reject);
2802
3078
  req.end(data);
2803
3079
  });
2804
3080
  }
2805
- var DOWNLOAD_TIMEOUT_MS;
3081
+ /**
3082
+ * Upload a buffer to a pre-signed S3 URL via HTTP PUT, retrying a transient
3083
+ * failure.
3084
+ *
3085
+ * Used for direct-to-S3 uploads of bundles and dep tarballs. Localhost /
3086
+ * 127.0.0.1 URLs are rewritten via `resolveOrchestratorUrl` so the
3087
+ * filesystem cache backend's signed URLs work from container agents that
3088
+ * can't reach the orchestrator's host loopback directly.
3089
+ *
3090
+ * **Why retrying is safe here.** A pre-signed PUT writes one whole object at a
3091
+ * single key: there is no multipart session, no append, and no
3092
+ * server-generated identity, so a repeat attempt writes the same bytes to the
3093
+ * same key and the last write wins. S3 also only makes an object visible once
3094
+ * the body has been received in full, so an attempt that died mid-body left
3095
+ * nothing behind. A retry therefore cannot double-write or produce a torn
3096
+ * object — which is why every AWS SDK retries PUTs by default.
3097
+ *
3098
+ * Only a failure that can plausibly differ next time is repeated — see
3099
+ * {@link isRetryableUploadFailure}.
3100
+ *
3101
+ * @param url - The pre-signed URL to upload to
3102
+ * @param data - The buffer to upload
3103
+ * @param opts.baseDelayMs - Backoff before the first retry (doubles thereafter)
3104
+ * @param opts.timeoutMs - Per-attempt socket-inactivity timeout (see
3105
+ * {@link UPLOAD_TIMEOUT_MS}); an override exists so a test can drive the
3106
+ * stall path without waiting out the production budget.
3107
+ */
3108
+ async function uploadToPresignedUrl(url, data, opts) {
3109
+ const resolved = resolveOrchestratorUrl(url);
3110
+ const baseDelayMs = opts?.baseDelayMs ?? UPLOAD_RETRY_BASE_DELAY_MS;
3111
+ const timeoutMs = opts?.timeoutMs ?? UPLOAD_TIMEOUT_MS;
3112
+ let lastError;
3113
+ for (let attempt = 0; attempt <= 2; attempt++) {
3114
+ if (attempt > 0) {
3115
+ const delayMs = baseDelayMs * 2 ** (attempt - 1);
3116
+ logger$9.warn("Retrying pre-signed upload", {
3117
+ attempt,
3118
+ delayMs,
3119
+ error: lastError?.message
3120
+ });
3121
+ await new Promise((r) => setTimeout(r, delayMs));
3122
+ }
3123
+ try {
3124
+ await putOnce(resolved, data, timeoutMs);
3125
+ return;
3126
+ } catch (err) {
3127
+ lastError = err instanceof Error ? err : new Error(String(err));
3128
+ if (!isRetryableUploadFailure(lastError)) throw lastError;
3129
+ }
3130
+ }
3131
+ throw new Error(`Pre-signed upload failed after 3 attempts: ${lastError?.message}`);
3132
+ }
3133
+ var logger$9, DOWNLOAD_TIMEOUT_MS, UPLOAD_TIMEOUT_MS, UPLOAD_RETRY_BASE_DELAY_MS, PresignedUploadHttpError;
2806
3134
  var init_download = __esmMin((() => {
2807
3135
  init_dep_restore();
3136
+ logger$9 = createLogger({ prefix: "agent:download" });
2808
3137
  DOWNLOAD_TIMEOUT_MS = 300 * 1e3;
3138
+ UPLOAD_TIMEOUT_MS = 300 * 1e3;
3139
+ UPLOAD_RETRY_BASE_DELAY_MS = 500;
3140
+ PresignedUploadHttpError = class extends Error {
3141
+ statusCode;
3142
+ constructor(statusCode) {
3143
+ super(`HTTP ${statusCode} uploading to pre-signed URL`);
3144
+ this.statusCode = statusCode;
3145
+ this.name = "PresignedUploadHttpError";
3146
+ }
3147
+ };
2809
3148
  }));
2810
3149
  //#endregion
2811
3150
  //#region src/execution/source-restore.ts
@@ -2889,6 +3228,44 @@ var init_timeout_util = __esmMin((() => {}));
2889
3228
  //#endregion
2890
3229
  //#region src/execution/init-runner.ts
2891
3230
  /**
3231
+ * Run a workflow's `filter` and report whether the workflow applies.
3232
+ *
3233
+ * Shared by both agent-side evaluation sites for a same-repo workflow: the init
3234
+ * job that gates each static job's dispatch, and the dynamic-eval job that gates
3235
+ * whether a generator runs at all. Both must reach the same verdict from the same
3236
+ * inputs, so neither builds the context itself.
3237
+ *
3238
+ * The context is built through `createFilterContext` rather than as an object
3239
+ * literal: the factory installs `changedFiles` as a throwing getter, so a filter
3240
+ * that reads the diff on an event that has none fails loudly instead of seeing an
3241
+ * empty list. A `false` verdict dispatches none of the workflow's own jobs, so a
3242
+ * silently-empty diff would suppress it on a mistake. On this same-repo path the
3243
+ * verdict is at least recoverable — the run row exists, carrying the `__init__*`
3244
+ * jobs, and this evaluation's own step log records the verdict; it is the
3245
+ * organization-wide path, which runs elsewhere, that leaves nothing behind.
3246
+ *
3247
+ * A throwing filter propagates: the evaluating job fails, which surfaces as a
3248
+ * failed run. "Could not decide" is never treated as "do not run" — that would
3249
+ * be a false green, the same reasoning `buildJobRuleCompletion` applies to a rule
3250
+ * whose `check()` threw.
3251
+ */
3252
+ async function evaluateWorkflowFilter(workflow, event, input, timeoutMs) {
3253
+ if (typeof workflow.filter !== "function") throw new Error(`Workflow '${workflow.name}' is recorded as declaring a filter, but its module exports none — the lock file is out of date. Run 'kici compile' and commit the result.`);
3254
+ if (!input) throw new Error(`Workflow '${workflow.name}' declares a filter but the evaluating job supplied no filter context (source tree / changed files) to evaluate it against.`);
3255
+ const filterFn = workflow.filter;
3256
+ const ctx = createFilterContext({
3257
+ sourceRepo: input.sourceRepo,
3258
+ workflowRepo: input.workflowRepo,
3259
+ event,
3260
+ changedFiles: input.changedFiles,
3261
+ changedFilesStatus: input.changedFilesStatus,
3262
+ ...input.env && { env: input.env },
3263
+ ...input.$ && { $: input.$ }
3264
+ });
3265
+ const verdict = await withTimeout(() => filterFn(ctx), timeoutMs, `filter for workflow '${workflow.name}'`);
3266
+ return Boolean(verdict);
3267
+ }
3268
+ /**
2892
3269
  * Find a static job by name in a workflow's jobs array.
2893
3270
  * Skips dynamic job functions (factories).
2894
3271
  */
@@ -2907,15 +3284,25 @@ function findJobByName(workflow, jobName) {
2907
3284
  * -: If a dynamic function returns undefined/null, the field is left undefined.
2908
3285
  * -: Each dynamic function call is wrapped in a timeout (default 60s).
2909
3286
  *
3287
+ * A workflow-level `filter` is evaluated FIRST when `flags.hasFilter` is set. A
3288
+ * `false` verdict returns immediately: no job of that workflow will be
3289
+ * dispatched, so evaluating this one's dynamic fields would run customer code
3290
+ * whose result nothing can consume.
3291
+ *
2910
3292
  * @param workflow - The extracted Workflow object
2911
3293
  * @param jobName - Name of the job whose dynamic fields to evaluate
2912
3294
  * @param event - Normalized event envelope — same shape every dynamic-function call site receives.
2913
3295
  * @param flags - Which fields are dynamic and need evaluation
2914
3296
  * @param timeoutMs - Timeout per dynamic function call (default 60_000ms)
3297
+ * @param filterInput - Source tree + diff the workflow's `filter` reads. Required when `flags.hasFilter`.
2915
3298
  */
2916
- async function evaluateDynamicFields(workflow, jobName, event, flags, timeoutMs = 6e4) {
2917
- const job = findJobByName(workflow, jobName);
3299
+ async function evaluateDynamicFields(workflow, jobName, event, flags, timeoutMs = 6e4, filterInput) {
2918
3300
  const result = {};
3301
+ if (flags.hasFilter) {
3302
+ result.filterPassed = await evaluateWorkflowFilter(workflow, event, filterInput, timeoutMs);
3303
+ if (!result.filterPassed) return result;
3304
+ }
3305
+ const job = findJobByName(workflow, jobName);
2919
3306
  if (flags.dynamicMatrix && typeof job.matrix === "function") {
2920
3307
  const matrixContext = {
2921
3308
  $: (await import("zx")).$,
@@ -4012,6 +4399,231 @@ var init_dynamic_job_serializer = __esmMin((() => {
4012
4399
  }
4013
4400
  };
4014
4401
  DYNAMIC_FIELD_TIMEOUT_MS = 6e4;
4402
+ }));
4403
+ //#endregion
4404
+ //#region src/execution/global-workflow-env.ts
4405
+ /**
4406
+ * Derive an `owner/repo` identifier from a clone URL, stripping the trailing
4407
+ * `.git` and any `http(s)://host/` prefix.
4408
+ */
4409
+ function repoIdentifierFromUrl(repoUrl) {
4410
+ return repoUrl.replace(/\.git$/, "").replace(/^https?:\/\/[^/]+\//, "");
4411
+ }
4412
+ /**
4413
+ * Inject the seven global-workflow env keys and return a restorer that puts
4414
+ * `process.env` back exactly as it was — each key reset to its prior value, or
4415
+ * deleted if it had none.
4416
+ *
4417
+ * **The restorer is mandatory for any caller in a long-lived process.** The
4418
+ * sandbox may ignore it: it runs one job per forked child, which exits. The
4419
+ * pre-dispatch global eval round may NOT: it runs in the agent process, which
4420
+ * serves many dispatches from one `JobRunner`. Leaving the keys set there is
4421
+ * this module's own hazard running backwards — a later NON-global
4422
+ * `DynamicJobFn` evaluation builds its generator context with
4423
+ * `env: process.env` still carrying `KICI_IS_GLOBAL_WORKFLOW=true` and a
4424
+ * `KICI_SOURCE_REPO_PATH` pointing at a deleted work directory, while that
4425
+ * job's own sandbox re-evaluation sees neither (`buildSanitizedEnv` scrubs the
4426
+ * whole `KICI_*` namespace on the trusted profile, and the default profile is
4427
+ * allowlist-only). That is the same two-worlds determinism failure, injected
4428
+ * into an unrelated job.
4429
+ *
4430
+ * `RepoInfo.ref` / `.sha` are optional, so an evaluation with no checkout
4431
+ * metadata writes an empty string rather than leaving the key unset — matching
4432
+ * how `KICI_WORKFLOW_REPO` already handles a missing identifier. Assigning
4433
+ * `undefined` to a `process.env` key would stringify to `"undefined"`, which is
4434
+ * worse than either.
4435
+ */
4436
+ function applyGlobalWorkflowEnv(repos) {
4437
+ const prior = GLOBAL_WORKFLOW_ENV_KEYS.map((key) => [key, process.env[key]]);
4438
+ process.env.KICI_IS_GLOBAL_WORKFLOW = "true";
4439
+ process.env.KICI_WORKFLOW_REPO_PATH = repos.workflowRepo.path;
4440
+ process.env.KICI_SOURCE_REPO_PATH = repos.sourceRepo.path;
4441
+ process.env.KICI_SOURCE_REPO = repos.sourceRepo.identifier;
4442
+ process.env.KICI_SOURCE_BRANCH = repos.sourceRepo.ref ?? "";
4443
+ process.env.KICI_SOURCE_SHA = repos.sourceRepo.sha ?? "";
4444
+ process.env.KICI_WORKFLOW_REPO = repos.workflowRepo.identifier;
4445
+ return () => {
4446
+ for (const [key, value] of prior) if (value === void 0) delete process.env[key];
4447
+ else process.env[key] = value;
4448
+ };
4449
+ }
4450
+ var GLOBAL_WORKFLOW_ENV_KEYS;
4451
+ var init_global_workflow_env = __esmMin((() => {
4452
+ GLOBAL_WORKFLOW_ENV_KEYS = [
4453
+ "KICI_IS_GLOBAL_WORKFLOW",
4454
+ "KICI_WORKFLOW_REPO_PATH",
4455
+ "KICI_SOURCE_REPO_PATH",
4456
+ "KICI_SOURCE_REPO",
4457
+ "KICI_SOURCE_BRANCH",
4458
+ "KICI_SOURCE_SHA",
4459
+ "KICI_WORKFLOW_REPO"
4460
+ ];
4461
+ }));
4462
+ //#endregion
4463
+ //#region src/execution/global-eval-runner.ts
4464
+ function buildRoundState(args) {
4465
+ const loadModule = args.loadModule ?? (async (sourceFile) => (await loadWorkflowSource(args.workflowDir, sourceFile)).module);
4466
+ return {
4467
+ args,
4468
+ $: args.$ ?? $,
4469
+ log: args.log ?? NOOP_LOG,
4470
+ kici: args.kici ?? buildKiciApi(() => Promise.reject(/* @__PURE__ */ new Error("Agent API not available"))),
4471
+ loadModule,
4472
+ moduleCache: /* @__PURE__ */ new Map()
4473
+ };
4474
+ }
4475
+ /** Load a workflow, caching per source file so a shared module is imported once. */
4476
+ async function loadWorkflowCached(shared, sourceFile, workflowName) {
4477
+ let pending = shared.moduleCache.get(sourceFile);
4478
+ if (!pending) {
4479
+ pending = shared.loadModule(sourceFile);
4480
+ shared.moduleCache.set(sourceFile, pending);
4481
+ }
4482
+ return extractWorkflow(await pending, workflowName);
4483
+ }
4484
+ /**
4485
+ * Run every `DynamicJobFn` the workflow declares and serialize the result.
4486
+ *
4487
+ * The generator context is built through `buildGeneratorContext` with the same
4488
+ * repo pair the sandbox re-evaluation gets, so the two calls a generator
4489
+ * receives cannot drift apart. Returns `undefined` when the workflow declares
4490
+ * no generators, which keeps the `jobs` key off the wire entirely.
4491
+ */
4492
+ async function generateDynamicJobs(workflow, shared) {
4493
+ const { args } = shared;
4494
+ const generators = workflow.jobs.filter(isDynamicJobFn);
4495
+ if (generators.length === 0) return void 0;
4496
+ const serializerCtx = {
4497
+ event: args.event,
4498
+ $: shared.$,
4499
+ log: shared.log,
4500
+ env: process.env,
4501
+ workflowName: workflow.name
4502
+ };
4503
+ const jobs = [];
4504
+ for (const generator of generators) {
4505
+ const generated = await generator(buildGeneratorContext({
4506
+ workflowName: workflow.name,
4507
+ event: args.event,
4508
+ env: process.env,
4509
+ repos: args.repos,
4510
+ $: shared.$,
4511
+ log: shared.log,
4512
+ kici: shared.kici
4513
+ }));
4514
+ jobs.push(...await serializeJobsToLock(generated, serializerCtx));
4515
+ }
4516
+ return jobs;
4517
+ }
4518
+ /**
4519
+ * Evaluate one candidate to a verdict: run its `filter` if it declares one,
4520
+ * then its generators if it survives.
4521
+ */
4522
+ async function evaluateCandidateInner(candidate, shared) {
4523
+ const { args } = shared;
4524
+ const workflow = await loadWorkflowCached(shared, candidate.sourceFile, candidate.workflowName);
4525
+ if (candidate.hasFilter) {
4526
+ if (typeof workflow.filter !== "function") throw new Error(`Workflow '${candidate.workflowName}' is recorded as declaring a filter, but its module exports none — the lock file is out of date. Run 'kici compile' and commit the result.`);
4527
+ const filterCtx = createFilterContext({
4528
+ sourceRepo: args.repos.sourceRepo,
4529
+ workflowRepo: args.repos.workflowRepo,
4530
+ event: args.event,
4531
+ changedFiles: args.changedFiles,
4532
+ changedFilesStatus: args.changedFilesStatus,
4533
+ env: process.env,
4534
+ $: shared.$
4535
+ });
4536
+ if (!await workflow.filter(filterCtx)) return {
4537
+ workflowName: candidate.workflowName,
4538
+ run: false
4539
+ };
4540
+ }
4541
+ const jobs = await generateDynamicJobs(workflow, shared);
4542
+ return {
4543
+ workflowName: candidate.workflowName,
4544
+ run: true,
4545
+ ...jobs && { jobs }
4546
+ };
4547
+ }
4548
+ /**
4549
+ * Evaluate one candidate, never throwing. A failure — a throwing filter, a
4550
+ * broken generator, a blown per-candidate budget — becomes an indeterminate
4551
+ * verdict so the round's other candidates still get real answers.
4552
+ */
4553
+ async function evaluateCandidate(candidate, shared) {
4554
+ try {
4555
+ return await withTimeout(() => evaluateCandidateInner(candidate, shared), shared.args.candidateTimeoutMs, `global workflow '${candidate.workflowName}'`);
4556
+ } catch (error) {
4557
+ return {
4558
+ workflowName: candidate.workflowName,
4559
+ run: false,
4560
+ indeterminate: true,
4561
+ reason: error instanceof Error ? error.message : String(error)
4562
+ };
4563
+ }
4564
+ }
4565
+ /**
4566
+ * Evaluate candidates one at a time — they share one checkout and one working
4567
+ * directory, so a parallel `$` would race on cwd.
4568
+ *
4569
+ * Stops before starting a candidate once the round deadline has passed or the
4570
+ * caller aborted. That check is what keeps the sequential guarantee meaningful
4571
+ * past a timeout: `withTimeout` races rather than cancels, so without it the
4572
+ * loop would keep launching every remaining candidate — each up to
4573
+ * `candidateTimeoutMs` — into a work directory the job has already reported on
4574
+ * and whose cleanup has already deleted. With it, at most one candidate is ever
4575
+ * in flight past the deadline.
4576
+ *
4577
+ * Results are appended to the caller's array as they land, so a round that
4578
+ * blows its own budget can still report the verdicts it did establish rather
4579
+ * than discarding the work.
4580
+ */
4581
+ async function evaluateAllCandidates(shared, into, deadline) {
4582
+ for (const candidate of shared.args.candidates) {
4583
+ if (shared.args.signal?.aborted || Date.now() >= deadline) return;
4584
+ into.push(await evaluateCandidate(candidate, shared));
4585
+ }
4586
+ }
4587
+ /**
4588
+ * Run one global eval round and return every candidate's verdict, in candidate
4589
+ * order. Never throws: a round that exceeds `roundTimeoutMs` reports whatever
4590
+ * it established and marks the rest indeterminate.
4591
+ */
4592
+ async function runGlobalEvalRound(args) {
4593
+ const restoreEnv = applyGlobalWorkflowEnv(args.repos);
4594
+ const shared = buildRoundState(args);
4595
+ const settled = [];
4596
+ let stopReason;
4597
+ try {
4598
+ await withTimeout(() => evaluateAllCandidates(shared, settled, Date.now() + args.roundTimeoutMs), args.roundTimeoutMs, `global eval round (${args.candidates.length} candidate(s))`);
4599
+ } catch (error) {
4600
+ stopReason = error instanceof Error ? error.message : String(error);
4601
+ } finally {
4602
+ restoreEnv();
4603
+ }
4604
+ const candidates = [...settled];
4605
+ const reason = stopReason ?? (args.signal?.aborted ? "global eval round was cancelled before this candidate was evaluated" : "global eval round deadline reached before this candidate was evaluated");
4606
+ for (const candidate of args.candidates.slice(candidates.length)) candidates.push({
4607
+ workflowName: candidate.workflowName,
4608
+ run: false,
4609
+ indeterminate: true,
4610
+ reason
4611
+ });
4612
+ return { candidates };
4613
+ }
4614
+ var NOOP_LOG;
4615
+ var init_global_eval_runner = __esmMin((() => {
4616
+ init_timeout_util();
4617
+ init_generator_context();
4618
+ init_global_workflow_env();
4619
+ init_workflow_loader();
4620
+ init_dynamic_job_serializer();
4621
+ NOOP_LOG = {
4622
+ info: () => {},
4623
+ warn: () => {},
4624
+ error: () => {},
4625
+ debug: () => {}
4626
+ };
4015
4627
  })), DEFAULT_MAX_LOG_SIZE_BYTES, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_FLUSH_LINE_THRESHOLD, PAUSE_SAFETY_TIMEOUT_MS, LogStreamer;
4016
4628
  var init_log_streamer = __esmMin((() => {
4017
4629
  init_prometheus();
@@ -6964,6 +7576,9 @@ var init_sandbox = __esmMin((() => {
6964
7576
  var job_runner_exports = /* @__PURE__ */ __exportAll({
6965
7577
  JobRunner: () => JobRunner$1,
6966
7578
  buildEvalNeedsContext: () => buildEvalNeedsContext,
7579
+ buildEvalShell: () => buildEvalShell,
7580
+ buildInitFilterInput: () => buildInitFilterInput,
7581
+ ensureFilterSourceDir: () => ensureFilterSourceDir,
6967
7582
  resolveJobWorkDir: () => resolveJobWorkDir,
6968
7583
  resolveRunnerBundlePath: () => resolveRunnerBundlePath
6969
7584
  });
@@ -6979,6 +7594,192 @@ async function fileExists(p) {
6979
7594
  }
6980
7595
  }
6981
7596
  /**
7597
+ * Build the source / workflow repo pair the round hands to every filter and
7598
+ * generator. Mirrors the sandbox's own `setupGlobalWorkflowEnv` construction so
7599
+ * a generator's two evaluations see the same identifiers, refs, and shas — only
7600
+ * the absolute paths differ, and those are never compared.
7601
+ */
7602
+ function buildRoundRepos(dispatch, config, workflowDir, sourceDir) {
7603
+ return {
7604
+ workflowRepo: {
7605
+ identifier: config.workflowRepoIdentifier ?? repoIdentifierFromUrl(config.workflowRepoUrl),
7606
+ path: workflowDir,
7607
+ ref: config.workflowRef,
7608
+ sha: config.workflowSha
7609
+ },
7610
+ sourceRepo: {
7611
+ identifier: repoIdentifierFromUrl(dispatch.repoUrl),
7612
+ path: sourceDir,
7613
+ ref: dispatch.ref,
7614
+ sha: dispatch.sha
7615
+ }
7616
+ };
7617
+ }
7618
+ /**
7619
+ * Resolve the changed-files list a `filter` reads — for a global eval round and
7620
+ * for a filter-bearing init job alike.
7621
+ *
7622
+ * Ground truth is the agent's own source clone; an already-`fetched` list from
7623
+ * the orchestrator is a free fast-path. A diff-less event (schedule / tag /
7624
+ * manual) resolves to `unavailable`, which makes `ctx.changedFiles` throw
7625
+ * rather than read as an empty diff — a `filter` returning false produces no
7626
+ * run at all, so a silently-empty diff would suppress the workflow with no
7627
+ * artifact anywhere to inspect.
7628
+ */
7629
+ async function resolveEvalChangedFiles(dispatch, event, sourceDir) {
7630
+ const ev = event;
7631
+ if (ev.changedFilesStatus === "fetched") return {
7632
+ files: ev.changedFiles ?? [],
7633
+ status: "fetched"
7634
+ };
7635
+ return computeChangedFiles(sourceDir, event, dispatch.sourceAuth ?? dispatch.workflowAuth ?? (dispatch.token ? {
7636
+ kind: "basic",
7637
+ user: "x-access-token",
7638
+ secret: dispatch.token
7639
+ } : void 0));
7640
+ }
7641
+ /**
7642
+ * Materialize the source tree a non-global workflow's `filter` reads through
7643
+ * `ctx.sourceRepo.path`.
7644
+ *
7645
+ * An init or dynamic-eval job normally restores only `.kici/` from the cached
7646
+ * source tarball — enough to import the workflow module, but a directory with no
7647
+ * repo in it. A filter that reads a file or shells out against that path would
7648
+ * get a confidently wrong answer, and `changedFiles` could not be computed at
7649
+ * all, so a filter-bearing job clones the source repo into a sibling directory.
7650
+ *
7651
+ * When no tarball was attached the job already cloned the whole repo into
7652
+ * `workDir`, and that clone is reused rather than duplicated — including the
7653
+ * local working-tree case, where there is no repo url and `workDir` IS the tree.
7654
+ *
7655
+ * A tarball with no repo url is the one combination that cannot be honoured:
7656
+ * `workDir` holds `.kici/` alone and there is nothing to clone from. Returning it
7657
+ * would hand the filter a directory in which every path test answers "absent" —
7658
+ * the exact silent lie this function exists to prevent — so it throws instead.
7659
+ */
7660
+ async function ensureFilterSourceDir(dispatch, workDir) {
7661
+ if (!dispatch.sourceTarUrl) return workDir;
7662
+ if (!dispatch.repoUrl) throw new Error("Workflow declares a filter, but this job restored its source from the cache with no repo url to clone from — the filter would see an empty tree. Re-run with a source repository configured, or remove the filter.");
7663
+ const sourceDir = join(workDir, FILTER_SOURCE_DIRNAME);
7664
+ const sourceAuth = dispatch.sourceAuth;
7665
+ await gitClone({
7666
+ repoUrl: dispatch.repoUrl,
7667
+ ref: dispatch.ref,
7668
+ sha: dispatch.sha,
7669
+ workDir: sourceDir,
7670
+ gitAuth: sourceAuth,
7671
+ token: sourceAuth ? void 0 : dispatch.token
7672
+ });
7673
+ return sourceDir;
7674
+ }
7675
+ /**
7676
+ * Restore deps, materialize the workflow source, and install `.kici/`
7677
+ * dependencies for a dynamic-eval job — everything that has to exist before its
7678
+ * workflow module can be imported.
7679
+ *
7680
+ * The init handler performs the same three steps against its own log wording and
7681
+ * keeps its own copy: sharing one helper would have to either move that
7682
+ * handler's `logger.info` call site (which Loki keys off) or parameterize it,
7683
+ * and neither is worth it for twenty lines.
7684
+ */
7685
+ async function materializeEvalWorkspace(dispatch, workDir, log) {
7686
+ if (dispatch.depsUrl) {
7687
+ log("Restoring dependencies from cache");
7688
+ await restoreDeps(workDir, dispatch.depsUrl, dispatch.depsHash);
7689
+ log("Dependencies restored");
7690
+ }
7691
+ if (dispatch.sourceTarUrl) {
7692
+ log("Restoring workflow source from cached tarball");
7693
+ await restoreSource(workDir, dispatch.sourceTarUrl);
7694
+ } else {
7695
+ log(`Cloning ${dispatch.repoUrl} ref=${dispatch.ref}`);
7696
+ const cloneStart = Date.now();
7697
+ await gitClone({
7698
+ repoUrl: dispatch.repoUrl,
7699
+ ref: dispatch.ref,
7700
+ sha: dispatch.sha,
7701
+ workDir,
7702
+ gitAuth: dispatch.sourceAuth,
7703
+ token: dispatch.sourceAuth ? void 0 : dispatch.token
7704
+ });
7705
+ cloneDurationSeconds.record((Date.now() - cloneStart) / 1e3);
7706
+ }
7707
+ const kiciDir = join(workDir, ".kici");
7708
+ if (!dispatch.depsUrl && await fileExists(join(kiciDir, "package.json"))) {
7709
+ log("Installing dependencies locally");
7710
+ await installDeps(kiciDir, {
7711
+ npmRegistries: dispatch.npmRegistries,
7712
+ installEnvSecrets: dispatch.installEnvSecrets,
7713
+ jobIdShort: dispatch.jobId.slice(0, 8)
7714
+ });
7715
+ }
7716
+ }
7717
+ /**
7718
+ * Build the context a non-global workflow's `filter` is evaluated against.
7719
+ *
7720
+ * `sourceRepo` and `workflowRepo` are the same repo — that is what "non-global"
7721
+ * means — so both carry the same identifier, path, ref, and sha. The zx shell is
7722
+ * rooted at the source tree and streams into the evaluating step's log, matching
7723
+ * what the global eval round hands its own filters.
7724
+ *
7725
+ * They are two distinct objects all the same. Being the same repo is a fact
7726
+ * about their VALUES, not a licence to hand the author one object under two
7727
+ * names: a filter that mutated `ctx.sourceRepo` would silently see
7728
+ * `ctx.workflowRepo` change with it, which happens on no other path.
7729
+ */
7730
+ async function buildInitFilterInput(dispatch, event, workDir, emit) {
7731
+ const sourceDir = await ensureFilterSourceDir(dispatch, workDir);
7732
+ const diff = await resolveEvalChangedFiles(dispatch, event, sourceDir);
7733
+ const repo = {
7734
+ identifier: repoIdentifierFromUrl(dispatch.repoUrl),
7735
+ path: sourceDir,
7736
+ ref: dispatch.ref,
7737
+ sha: dispatch.sha
7738
+ };
7739
+ return {
7740
+ sourceRepo: repo,
7741
+ workflowRepo: { ...repo },
7742
+ changedFiles: diff.files,
7743
+ changedFilesStatus: diff.status,
7744
+ env: process.env,
7745
+ $: await buildEvalShell(sourceDir, emit)
7746
+ };
7747
+ }
7748
+ /**
7749
+ * Build the per-invocation zx `$` a global eval round hands to filters and
7750
+ * generators, so a `await $\`…\`` inside one is visible in the eval step's log.
7751
+ *
7752
+ * **`env` is the LIVE `process.env` reference, never a spread.** A spread is a
7753
+ * snapshot taken when the shell is built, which is before the round applies the
7754
+ * seven `KICI_*` keys — so a filter that shells out (`$\`printenv
7755
+ * KICI_SOURCE_REPO_PATH\``, or any subprocess inheriting env) would see nothing
7756
+ * here while the sandbox re-evaluation's ambient `$` resolves `process.env`
7757
+ * after `setupGlobalWorkflowEnv` has run and does see them. That is the same
7758
+ * two-worlds determinism failure the cwd choice below exists to prevent, one
7759
+ * layer down. Passing the live reference reproduces the ambient `$`'s own
7760
+ * behaviour, which is what the sandbox uses.
7761
+ *
7762
+ * `verbose: true` + `makeStreamingZxLog` honors a per-call `quiet: true`, so a
7763
+ * decrypted secret never leaks into the log.
7764
+ *
7765
+ * `emit` is a callback rather than the `LogStreamer` itself so the caller can
7766
+ * route it through its own closed-guard: `LogStreamer.destroy()` sets no closed
7767
+ * flag and `addLine` buffers unconditionally, so a subprocess line arriving
7768
+ * after the step was reported would otherwise emit a `log.chunk` for a terminal
7769
+ * step. That is the likeliest path for it — an orphaned candidate is usually
7770
+ * orphaned *because* it is waiting on a subprocess.
7771
+ */
7772
+ async function buildEvalShell(cwd, emit) {
7773
+ const { $: zx$ } = await import("zx");
7774
+ return zx$({
7775
+ cwd,
7776
+ env: process.env,
7777
+ verbose: true,
7778
+ quiet: false,
7779
+ log: makeStreamingZxLog(emit)
7780
+ });
7781
+ }
7782
+ /**
6982
7783
  * Resolve the absolute path to the compiled workflow-runner.js entry point.
6983
7784
  *
6984
7785
  * The runner is a separate rolldown entry point. Its location depends on the
@@ -7062,9 +7863,10 @@ async function resolveJobWorkDir(inPlace, repoUrl) {
7062
7863
  inPlace: false
7063
7864
  };
7064
7865
  }
7065
- var logger$2, JobRunner$1;
7866
+ var logger$2, DEFAULT_GLOBAL_EVAL_ROUND_TIMEOUT_MS, DEFAULT_GLOBAL_EVAL_CANDIDATE_TIMEOUT_MS, FILTER_SOURCE_DIRNAME, JobRunner$1;
7066
7867
  var init_job_runner = __esmMin((() => {
7067
7868
  init_git_clone();
7869
+ init_changed_files();
7068
7870
  init_workflow_loader();
7069
7871
  init_source_packer();
7070
7872
  init_source_restore();
@@ -7076,6 +7878,9 @@ var init_job_runner = __esmMin((() => {
7076
7878
  init_timeout_util();
7077
7879
  init_streaming_zx_log();
7078
7880
  init_dynamic_job_serializer();
7881
+ init_generator_context();
7882
+ init_global_workflow_env();
7883
+ init_global_eval_runner();
7079
7884
  init_log_streamer();
7080
7885
  init_overlay_applier();
7081
7886
  init_dep_installer();
@@ -7085,6 +7890,9 @@ var init_job_runner = __esmMin((() => {
7085
7890
  init_sandbox();
7086
7891
  init_prometheus();
7087
7892
  logger$2 = createLogger({ prefix: "job-runner" });
7893
+ DEFAULT_GLOBAL_EVAL_ROUND_TIMEOUT_MS = 12e4;
7894
+ DEFAULT_GLOBAL_EVAL_CANDIDATE_TIMEOUT_MS = 2e4;
7895
+ FILTER_SOURCE_DIRNAME = "__kici_filter_source__";
7088
7896
  JobRunner$1 = class {
7089
7897
  send;
7090
7898
  config;
@@ -7180,6 +7988,10 @@ var init_job_runner = __esmMin((() => {
7180
7988
  await this.handleInitJob(dispatch, workDir, abortController);
7181
7989
  return true;
7182
7990
  }
7991
+ if (jobConfig.globalEvalRound === true) {
7992
+ await this.handleGlobalEvalRound(dispatch, workDir, abortController);
7993
+ return true;
7994
+ }
7183
7995
  if (jobConfig.dynamicJobFn === true) {
7184
7996
  await this.handleDynamicJobFn(dispatch, workDir, abortController);
7185
7997
  return true;
@@ -7595,6 +8407,53 @@ var init_job_runner = __esmMin((() => {
7595
8407
  }
7596
8408
  }
7597
8409
  /**
8410
+ * Materialize the init job's workflow source into `workDir`. A test run ships
8411
+ * its full working tree as an encrypted overlay tarball (`fullRepo`) rather
8412
+ * than a git repo, so skip the clone and let the overlay populate the
8413
+ * workspace — the same handling the normal execution-job path uses. Otherwise
8414
+ * restore from the cached tarball if present, else clone. In every case, apply
8415
+ * an attached overlay tarball afterward (test runs with uncommitted changes;
8416
+ * for a fullRepo run this is what actually populates the workspace, so the
8417
+ * init job resolves a dynamic context against the real source tree instead of
8418
+ * an empty directory).
8419
+ */
8420
+ async materializeInitJobSource(dispatch, workDir, initLog) {
8421
+ const initJobConfig = dispatch.jobConfig;
8422
+ if (initJobConfig.fullRepo) {
8423
+ initLog("Test run: materializing workspace from overlay (no clone)");
8424
+ await fsPromises.mkdir(workDir, { recursive: true });
8425
+ } else if (dispatch.sourceTarUrl) {
8426
+ initLog("Restoring workflow source from cached tarball");
8427
+ await restoreSource(workDir, dispatch.sourceTarUrl);
8428
+ } else {
8429
+ initLog(`Cloning ${dispatch.repoUrl} (ref: ${dispatch.ref})`);
8430
+ const cloneStart = Date.now();
8431
+ await gitClone({
8432
+ repoUrl: dispatch.repoUrl,
8433
+ ref: dispatch.ref,
8434
+ sha: dispatch.sha,
8435
+ workDir,
8436
+ gitAuth: dispatch.sourceAuth,
8437
+ token: dispatch.sourceAuth ? void 0 : dispatch.token
8438
+ });
8439
+ cloneDurationSeconds.record((Date.now() - cloneStart) / 1e3);
8440
+ }
8441
+ if (initJobConfig.tarballUrl && initJobConfig.cliPublicKey && initJobConfig.orchestratorPrivateKey) {
8442
+ initLog("Applying overlay tarball for test run");
8443
+ const overlayResult = await applyOverlay({
8444
+ tarballUrl: initJobConfig.tarballUrl,
8445
+ cliPublicKey: initJobConfig.cliPublicKey,
8446
+ orchestratorPrivateKey: initJobConfig.orchestratorPrivateKey,
8447
+ repoDir: workDir
8448
+ });
8449
+ logger$2.info("Init job: overlay applied", {
8450
+ jobId: dispatch.jobId,
8451
+ filesApplied: overlayResult.filesApplied,
8452
+ filesDeleted: overlayResult.filesDeleted
8453
+ });
8454
+ }
8455
+ }
8456
+ /**
7598
8457
  * Phase 2 of build: install dependencies locally if needed for the build,
7599
8458
  * and (when the orchestrator has flagged the dep cache as stale) pack
7600
8459
  * `.kici/node_modules/` into a tarball and upload it to the deps cache.
@@ -7670,6 +8529,169 @@ var init_job_runner = __esmMin((() => {
7670
8529
  });
7671
8530
  }
7672
8531
  /**
8532
+ * Clone both repos for a global eval round and materialize the workflow
8533
+ * repo's dependencies, mirroring the sandbox's own dual-clone: the workflow
8534
+ * repo under `<workDir>/workflow`, the source repo under `<workDir>/source`.
8535
+ *
8536
+ * `.kici/` lives in the WORKFLOW repo for a global workflow, so deps and the
8537
+ * scratch-dir git exclude both apply to that checkout, never the source one.
8538
+ */
8539
+ async checkoutForGlobalEvalRound(dispatch, config, workflowDir, sourceDir, log) {
8540
+ const workflowAuth = dispatch.workflowAuth ?? dispatch.sourceAuth;
8541
+ const sourceAuth = dispatch.sourceAuth ?? dispatch.workflowAuth;
8542
+ await fsPromises.mkdir(workflowDir, { recursive: true });
8543
+ await fsPromises.mkdir(sourceDir, { recursive: true });
8544
+ log(`Cloning workflow repo ${config.workflowRepoUrl} (ref: ${config.workflowRef ?? ""})`);
8545
+ const cloneStart = Date.now();
8546
+ await gitClone({
8547
+ repoUrl: config.workflowRepoUrl,
8548
+ ref: config.workflowRef ?? "",
8549
+ sha: config.workflowSha ?? "",
8550
+ workDir: workflowDir,
8551
+ gitAuth: workflowAuth,
8552
+ token: workflowAuth ? void 0 : dispatch.token
8553
+ });
8554
+ await excludeScratchFromGit(workflowDir);
8555
+ log(`Cloning source repo ${dispatch.repoUrl} (ref: ${dispatch.ref})`);
8556
+ await gitClone({
8557
+ repoUrl: dispatch.repoUrl,
8558
+ ref: dispatch.ref,
8559
+ sha: dispatch.sha,
8560
+ workDir: sourceDir,
8561
+ gitAuth: sourceAuth,
8562
+ token: sourceAuth ? void 0 : dispatch.token
8563
+ });
8564
+ cloneDurationSeconds.record((Date.now() - cloneStart) / 1e3);
8565
+ if (dispatch.depsUrl) {
8566
+ log("Restoring dependencies from cache");
8567
+ await restoreDeps(workflowDir, dispatch.depsUrl, dispatch.depsHash);
8568
+ }
8569
+ if (dispatch.sourceTarUrl) {
8570
+ log("Restoring workflow source from cached tarball");
8571
+ await restoreSource(workflowDir, dispatch.sourceTarUrl);
8572
+ }
8573
+ const kiciDir = join(workflowDir, ".kici");
8574
+ if (!dispatch.depsUrl && await fileExists(join(kiciDir, "package.json"))) {
8575
+ log("Installing dependencies locally");
8576
+ await installDeps(kiciDir, {
8577
+ npmRegistries: dispatch.npmRegistries,
8578
+ installEnvSecrets: dispatch.installEnvSecrets,
8579
+ jobIdShort: dispatch.jobId.slice(0, 8)
8580
+ });
8581
+ }
8582
+ }
8583
+ /**
8584
+ * Handle a pre-run global eval round.
8585
+ *
8586
+ * The round runs once per (event × workflow repo) BEFORE any run row exists:
8587
+ * it checks out the workflow repo and the source repo, then runs each
8588
+ * candidate global workflow's `filter` and — for a survivor — its
8589
+ * `DynamicJobFn`s, so the orchestrator learns which workflows apply to this
8590
+ * source repo and which jobs each one generates.
8591
+ *
8592
+ * A candidate that fails is reported indeterminate inside the result, not as
8593
+ * a job failure: the round carries several unrelated org-wide workflows, and
8594
+ * one broken filter must not suppress the rest. The job itself fails only
8595
+ * when the checkout or the round machinery breaks.
8596
+ */
8597
+ async handleGlobalEvalRound(dispatch, workDir, abortController) {
8598
+ const { runId, jobId, jobConfig } = dispatch;
8599
+ const config = jobConfig;
8600
+ const workflowDir = join(workDir, "workflow");
8601
+ const sourceDir = join(workDir, "source");
8602
+ logger$2.info("Starting global eval round", {
8603
+ jobId,
8604
+ candidateCount: config.candidates.length,
8605
+ workflowRepoIdentifier: config.workflowRepoIdentifier
8606
+ });
8607
+ this.sendJobStatus(dispatch, ExecutionJobStatus.enum.running);
8608
+ const evalStreamer = this.createStepStreamer(dispatch, 0);
8609
+ let streamerClosed = false;
8610
+ const evalLog = (msg) => {
8611
+ if (!streamerClosed) evalStreamer.addLine(msg);
8612
+ };
8613
+ const evalStreamLine = (line, stream) => {
8614
+ if (!streamerClosed) evalStreamer.addLine(line, stream);
8615
+ };
8616
+ const closeStreamer = async () => {
8617
+ await evalStreamer.flush();
8618
+ evalStreamer.destroy();
8619
+ streamerClosed = true;
8620
+ };
8621
+ const evalSink = { addLine: (line) => evalLog(line) };
8622
+ this.sendStepStatus(dispatch, 0, "global-eval", ExecutionStepStatus.enum.running);
8623
+ const heartbeatTimer = setInterval(() => {
8624
+ this.send({
8625
+ type: "job.heartbeat",
8626
+ runId,
8627
+ jobId,
8628
+ timestamp: Date.now()
8629
+ });
8630
+ }, this.config.jobHeartbeatIntervalMs);
8631
+ try {
8632
+ if (abortController.signal.aborted) {
8633
+ await closeStreamer();
8634
+ this.sendStepStatus(dispatch, 0, "global-eval", ExecutionStepStatus.enum.skipped, void 0, evalStreamer.getTotalBytes());
8635
+ this.sendJobStatus(dispatch, ExecutionJobStatus.enum.cancelled);
8636
+ return;
8637
+ }
8638
+ await this.checkoutForGlobalEvalRound(dispatch, config, workflowDir, sourceDir, evalLog);
8639
+ const diff = await resolveEvalChangedFiles(dispatch, config.event, sourceDir);
8640
+ const evalShell = await buildEvalShell(workDir, evalStreamLine);
8641
+ const roundResult = await runCaptured(evalSink, () => runGlobalEvalRound({
8642
+ workflowDir,
8643
+ sourceDir,
8644
+ repos: buildRoundRepos(dispatch, config, workflowDir, sourceDir),
8645
+ candidates: config.candidates,
8646
+ event: config.event,
8647
+ changedFiles: diff.files,
8648
+ changedFilesStatus: diff.status,
8649
+ roundTimeoutMs: config.roundTimeoutMs ?? DEFAULT_GLOBAL_EVAL_ROUND_TIMEOUT_MS,
8650
+ candidateTimeoutMs: config.candidateTimeoutMs ?? DEFAULT_GLOBAL_EVAL_CANDIDATE_TIMEOUT_MS,
8651
+ signal: abortController.signal,
8652
+ $: evalShell,
8653
+ log: {
8654
+ info: (msg) => evalLog(msg),
8655
+ warn: (msg) => evalLog(`WARN: ${msg}`),
8656
+ error: (msg) => evalLog(`ERROR: ${msg}`),
8657
+ debug: (msg) => evalLog(`DEBUG: ${msg}`)
8658
+ },
8659
+ kici: buildKiciApi(this._sendApiRequest ? withBootstrapInterception((method, params) => this._sendApiRequest(method, params ?? {})) : () => Promise.reject(/* @__PURE__ */ new Error("Agent API not available")))
8660
+ }));
8661
+ const running = roundResult.candidates.filter((c) => c.run).length;
8662
+ const indeterminate = roundResult.candidates.filter((c) => c.indeterminate).length;
8663
+ logger$2.info("Global eval round completed", {
8664
+ jobId,
8665
+ candidateCount: roundResult.candidates.length,
8666
+ running,
8667
+ indeterminate
8668
+ });
8669
+ evalLog(`Global eval round completed: ${running} of ${roundResult.candidates.length} workflow(s) apply` + (indeterminate > 0 ? ` (${indeterminate} indeterminate)` : ""));
8670
+ for (const candidate of roundResult.candidates) if (candidate.indeterminate) evalLog(` ${candidate.workflowName}: indeterminate — ${candidate.reason ?? "unknown"}`);
8671
+ await closeStreamer();
8672
+ this.sendStepStatus(dispatch, 0, "global-eval", ExecutionStepStatus.enum.success, void 0, evalStreamer.getTotalBytes());
8673
+ this.sendJobStatus(dispatch, ExecutionJobStatus.enum.success, {
8674
+ globalEvalResult: roundResult,
8675
+ globalEvalComplete: true
8676
+ });
8677
+ } catch (err) {
8678
+ const errorMsg = toErrorMessage(err);
8679
+ logger$2.error("Global eval round failed", {
8680
+ jobId,
8681
+ error: errorMsg
8682
+ });
8683
+ evalLog(`Error: ${errorMsg}`);
8684
+ await closeStreamer();
8685
+ this.sendStepStatus(dispatch, 0, "global-eval", ExecutionStepStatus.enum.failed, { error: errorMsg }, evalStreamer.getTotalBytes());
8686
+ this.sendJobStatus(dispatch, ExecutionJobStatus.enum.failed, {
8687
+ error: errorMsg,
8688
+ globalEvalFailed: true
8689
+ });
8690
+ } finally {
8691
+ clearInterval(heartbeatTimer);
8692
+ }
8693
+ }
8694
+ /**
7673
8695
  * Handle an init-only job.
7674
8696
  *
7675
8697
  * Init jobs evaluate dynamic functions (environment, env, concurrencyGroup)
@@ -7691,8 +8713,20 @@ var init_job_runner = __esmMin((() => {
7691
8713
  });
7692
8714
  this.sendJobStatus(dispatch, ExecutionJobStatus.enum.running);
7693
8715
  const initStreamer = this.createStepStreamer(dispatch, 0);
7694
- const initLog = (msg) => initStreamer.addLine(msg);
7695
- const initSink = { addLine: (line) => initStreamer.addLine(line) };
8716
+ let streamerClosed = false;
8717
+ const initLog = (msg) => {
8718
+ if (!streamerClosed) initStreamer.addLine(msg);
8719
+ };
8720
+ const initStreamLine = (line, stream) => {
8721
+ if (!streamerClosed) initStreamer.addLine(line, stream);
8722
+ };
8723
+ const closeStreamer = async () => {
8724
+ if (streamerClosed) return;
8725
+ streamerClosed = true;
8726
+ await initStreamer.flush();
8727
+ initStreamer.destroy();
8728
+ };
8729
+ const initSink = { addLine: (line) => initLog(line) };
7696
8730
  this.sendStepStatus(dispatch, 0, "init", ExecutionStepStatus.enum.running);
7697
8731
  const heartbeatTimer = setInterval(() => {
7698
8732
  this.send({
@@ -7704,8 +8738,7 @@ var init_job_runner = __esmMin((() => {
7704
8738
  }, this.config.jobHeartbeatIntervalMs);
7705
8739
  try {
7706
8740
  if (abortController.signal.aborted) {
7707
- await initStreamer.flush();
7708
- initStreamer.destroy();
8741
+ await closeStreamer();
7709
8742
  this.sendStepStatus(dispatch, 0, "init", ExecutionStepStatus.enum.skipped, void 0, initStreamer.getTotalBytes());
7710
8743
  this.sendJobStatus(dispatch, ExecutionJobStatus.enum.cancelled);
7711
8744
  return;
@@ -7714,22 +8747,7 @@ var init_job_runner = __esmMin((() => {
7714
8747
  initLog("Restoring dependencies from cache");
7715
8748
  await restoreDeps(workDir, dispatch.depsUrl, dispatch.depsHash);
7716
8749
  }
7717
- if (dispatch.sourceTarUrl) {
7718
- initLog("Restoring workflow source from cached tarball");
7719
- await restoreSource(workDir, dispatch.sourceTarUrl);
7720
- } else {
7721
- initLog(`Cloning ${dispatch.repoUrl} (ref: ${dispatch.ref})`);
7722
- const cloneStart = Date.now();
7723
- await gitClone({
7724
- repoUrl: dispatch.repoUrl,
7725
- ref: dispatch.ref,
7726
- sha: dispatch.sha,
7727
- workDir,
7728
- gitAuth: dispatch.sourceAuth,
7729
- token: dispatch.sourceAuth ? void 0 : dispatch.token
7730
- });
7731
- cloneDurationSeconds.record((Date.now() - cloneStart) / 1e3);
7732
- }
8750
+ await this.materializeInitJobSource(dispatch, workDir, initLog);
7733
8751
  const kiciDir = join(workDir, ".kici");
7734
8752
  const hasPackage = await fileExists(join(kiciDir, "package.json"));
7735
8753
  logger$2.info("Init job: checking deps", {
@@ -7745,26 +8763,29 @@ var init_job_runner = __esmMin((() => {
7745
8763
  jobIdShort: dispatch.jobId.slice(0, 8)
7746
8764
  });
7747
8765
  }
8766
+ const filterInput = config.hasFilter ? await buildInitFilterInput(dispatch, config.event, workDir, initStreamLine) : void 0;
7748
8767
  const initResult = await runCaptured(initSink, async () => {
7749
8768
  const { module } = await loadWorkflowSource(workDir, config.source, config.contentHash, config.resolvedHashFiles);
7750
8769
  const workflow = extractWorkflow(module, config.workflowName);
7751
- initLog(`Evaluating dynamic fields for job '${config.targetJobName}' (env=${config.dynamicEnv} context=${config.dynamicContext} concurrencyGroup=${config.dynamicConcurrencyGroup} matrix=${config.dynamicMatrix ?? false})`);
8770
+ initLog(`Evaluating dynamic fields for job '${config.targetJobName}' (env=${config.dynamicEnv} context=${config.dynamicContext} concurrencyGroup=${config.dynamicConcurrencyGroup} matrix=${config.dynamicMatrix ?? false} filter=${config.hasFilter ?? false})`);
7752
8771
  return evaluateDynamicFields(workflow, config.targetJobName, config.event, {
7753
8772
  dynamicContext: config.dynamicContext,
7754
8773
  dynamicEnv: config.dynamicEnv,
7755
8774
  dynamicConcurrencyGroup: config.dynamicConcurrencyGroup,
7756
- dynamicMatrix: config.dynamicMatrix ?? false
7757
- }, config.timeoutMs);
8775
+ dynamicMatrix: config.dynamicMatrix ?? false,
8776
+ hasFilter: config.hasFilter ?? false
8777
+ }, config.timeoutMs, filterInput);
7758
8778
  });
7759
8779
  logger$2.info("Init job completed successfully", {
7760
8780
  jobId,
7761
8781
  hasContext: initResult.contextNames !== void 0,
7762
8782
  hasEnv: initResult.env !== void 0,
7763
- hasConcurrencyGroup: initResult.concurrencyGroup !== void 0
8783
+ hasConcurrencyGroup: initResult.concurrencyGroup !== void 0,
8784
+ filterPassed: initResult.filterPassed
7764
8785
  });
8786
+ if (initResult.filterPassed === false) initLog(`Workflow filter returned false — '${config.workflowName}' does not apply to this event, so no job is dispatched`);
7765
8787
  initLog("Init completed successfully");
7766
- await initStreamer.flush();
7767
- initStreamer.destroy();
8788
+ await closeStreamer();
7768
8789
  this.sendStepStatus(dispatch, 0, "init", ExecutionStepStatus.enum.success, void 0, initStreamer.getTotalBytes());
7769
8790
  this.sendJobStatus(dispatch, ExecutionJobStatus.enum.success, {
7770
8791
  initResult,
@@ -7777,8 +8798,7 @@ var init_job_runner = __esmMin((() => {
7777
8798
  error: errorMsg
7778
8799
  });
7779
8800
  initLog(`Error: ${errorMsg}`);
7780
- await initStreamer.flush();
7781
- initStreamer.destroy();
8801
+ await closeStreamer();
7782
8802
  this.sendStepStatus(dispatch, 0, "init", ExecutionStepStatus.enum.failed, { error: errorMsg }, initStreamer.getTotalBytes());
7783
8803
  this.sendJobStatus(dispatch, ExecutionJobStatus.enum.failed, {
7784
8804
  error: errorMsg,
@@ -7822,36 +8842,7 @@ var init_job_runner = __esmMin((() => {
7822
8842
  this.sendJobStatus(dispatch, ExecutionJobStatus.enum.cancelled);
7823
8843
  return;
7824
8844
  }
7825
- if (dispatch.depsUrl) {
7826
- evalLog("Restoring dependencies from cache");
7827
- await restoreDeps(workDir, dispatch.depsUrl, dispatch.depsHash);
7828
- evalLog("Dependencies restored");
7829
- }
7830
- if (dispatch.sourceTarUrl) {
7831
- evalLog("Restoring workflow source from cached tarball");
7832
- await restoreSource(workDir, dispatch.sourceTarUrl);
7833
- } else {
7834
- evalLog(`Cloning ${dispatch.repoUrl} ref=${dispatch.ref}`);
7835
- const cloneStart = Date.now();
7836
- await gitClone({
7837
- repoUrl: dispatch.repoUrl,
7838
- ref: dispatch.ref,
7839
- sha: dispatch.sha,
7840
- workDir,
7841
- gitAuth: dispatch.sourceAuth,
7842
- token: dispatch.sourceAuth ? void 0 : dispatch.token
7843
- });
7844
- cloneDurationSeconds.record((Date.now() - cloneStart) / 1e3);
7845
- }
7846
- const kiciDir = join(workDir, ".kici");
7847
- if (!dispatch.depsUrl && await fileExists(join(kiciDir, "package.json"))) {
7848
- evalLog("Installing dependencies locally");
7849
- await installDeps(kiciDir, {
7850
- npmRegistries: dispatch.npmRegistries,
7851
- installEnvSecrets: dispatch.installEnvSecrets,
7852
- jobIdShort: dispatch.jobId.slice(0, 8)
7853
- });
7854
- }
8845
+ await materializeEvalWorkspace(dispatch, workDir, evalLog);
7855
8846
  const { $: zx$ } = await import("zx");
7856
8847
  const scopedDollar = zx$({
7857
8848
  cwd: workDir,
@@ -7861,6 +8852,7 @@ var init_job_runner = __esmMin((() => {
7861
8852
  log: makeStreamingZxLog((line, stream) => evalStreamer.addLine(line, stream))
7862
8853
  });
7863
8854
  const evalSink = { addLine: (line) => evalStreamer.addLine(line) };
8855
+ const filterInput = config.hasFilter ? await buildInitFilterInput(dispatch, config.event, workDir, (line, stream) => evalStreamer.addLine(line, stream)) : void 0;
7864
8856
  const dynamicJobLogger = {
7865
8857
  info: (msg, ..._args) => evalLog(msg),
7866
8858
  warn: (msg, ..._args) => evalLog(`WARN: ${msg}`),
@@ -7872,20 +8864,25 @@ var init_job_runner = __esmMin((() => {
7872
8864
  const { module } = await loadWorkflowSource(workDir, config.source.file, config.contentHash, config.resolvedHashFiles);
7873
8865
  evalLog("Workflow loaded");
7874
8866
  const { extractDynamicJobFn } = await Promise.resolve().then(() => (init_workflow_loader(), workflow_loader_exports));
7875
- const dynamicFn = extractDynamicJobFn(extractWorkflow(module, config.workflowName), config.source.index);
8867
+ const workflow = extractWorkflow(module, config.workflowName);
8868
+ if (config.hasFilter) {
8869
+ if (!await evaluateWorkflowFilter(workflow, config.event, filterInput, timeoutMs)) {
8870
+ evalLog(`Workflow filter returned false — '${config.workflowName}' does not apply to this event, so its generator is not run and no jobs are generated`);
8871
+ return [];
8872
+ }
8873
+ }
8874
+ const dynamicFn = extractDynamicJobFn(workflow, config.source.index);
7876
8875
  evalLog(`Evaluating DynamicJobFn (index ${config.source.index}, timeout ${timeoutMs}ms)`);
7877
8876
  const needs = buildEvalNeedsContext(config);
7878
- const context = {
8877
+ const context = buildGeneratorContext({
8878
+ workflowName: config.workflowName,
8879
+ event: config.event,
8880
+ env: process.env,
8881
+ ...needs && { needs },
7879
8882
  $: scopedDollar,
7880
- ctx: {
7881
- workflow: { name: config.workflowName },
7882
- event: config.event,
7883
- ...needs && { needs }
7884
- },
7885
8883
  log: dynamicJobLogger,
7886
- env: process.env,
7887
8884
  kici
7888
- };
8885
+ });
7889
8886
  return serializeJobsToLock(await withTimeout(() => dynamicFn(context), timeoutMs, `DynamicJobFn index ${config.source.index} in workflow '${config.workflowName}'`), {
7890
8887
  event: config.event,
7891
8888
  $: scopedDollar,
@@ -8111,14 +9108,14 @@ var init_job_runner = __esmMin((() => {
8111
9108
  */
8112
9109
  init_console_capture();
8113
9110
  init_npm_resolver();
8114
- const AGENT_VERSION = "0.4.0";
8115
- const BUILD_COMMIT = "ffd489464";
8116
- const SDK_VERSION = "0.4.0";
8117
- const SDK_BUNDLE_HASH = "fcd0a836aa1cb92debd93e0254450614d123b3748511aa4148d956fd72a7c29f";
8118
- const SHARED_VERSION = "0.4.0";
8119
- const SHARED_BUNDLE_HASH = "f16fa528a8a9ef1bed8df369f0e88efafca8a9954a749005299fdbcdb93fa188";
8120
- const ENGINE_VERSION = "0.4.0";
8121
- const ENGINE_BUNDLE_HASH = "31e2d5c327284e1cac908d7f85cf6d878bbf87ea0da93ed09c6205ada6935f89";
9111
+ const AGENT_VERSION = "0.5.0";
9112
+ const BUILD_COMMIT = "cb51c7d1e";
9113
+ const SDK_VERSION = "0.5.0";
9114
+ const SDK_BUNDLE_HASH = "5b85e7cffa4a08e39329ff80448a2744af8e5b5f9840ec3801fad45ed11a3de9";
9115
+ const SHARED_VERSION = "0.5.0";
9116
+ const SHARED_BUNDLE_HASH = "b6b6d1818d1fe648150422daf073dafc18b037404cacf82e0ec03b4a2863e4c4";
9117
+ const ENGINE_VERSION = "0.5.0";
9118
+ const ENGINE_BUNDLE_HASH = "35d174ce6f4748abffcad194b335b508cade5d1c8af16d9cd3bdcb744f38717e";
8122
9119
  initTelemetry({
8123
9120
  serviceName: "kici-agent",
8124
9121
  otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT