@kici-dev/agent 0.1.21 → 0.1.23

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.
@@ -8,9 +8,9 @@ import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:pa
8
8
  import { $ } from "zx";
9
9
  import { createLogger, deriveSharedSecret, initZx, normalizeLineEndings, sha256, sha256File, toErrorMessage } from "@kici-dev/shared";
10
10
  import { CacheOutcome, CacheStepType, CheckMode, CheckStepOutcome, ExecutionJobStatus, ExecutionStepStatus, TimeoutReason } from "@kici-dev/engine";
11
- import { buildKiciApi, createStepSecrets, evaluateRules, isDynamicJobFn, normalizeCacheSpecs, normalizeRequireApproval, provenanceSubjectIsPath, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
11
+ import { buildKiciApi, buildNeedsContext, createStepSecrets, evaluateRules, isDynamicJobFn, normalizeApproval, normalizeCacheSpecs, provenanceSubjectIsPath, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
12
12
  import { OIDC_TOKEN_REQUEST_METHOD } from "@kici-dev/engine/protocol/messages/oidc-token-relay";
13
- import { sha256File as sha256File$1 } from "@kici-dev/core";
13
+ import { computeBackoffDelay, sha256File as sha256File$1 } from "@kici-dev/core";
14
14
  import { calculateJwkThumbprint, decodeJwt, exportJWK, generateKeyPair } from "jose";
15
15
  import { IN_TOTO_PAYLOAD_TYPE, KICI_PROVENANCE_AUDIENCE, KICI_PROVENANCE_BUNDLE_MEDIA_TYPE } from "@kici-dev/engine/provenance/bundle";
16
16
  import { IN_TOTO_STATEMENT_TYPE, KICI_WORKFLOW_BUILD_TYPE, SLSA_PROVENANCE_PREDICATE_TYPE } from "@kici-dev/engine/provenance/schema";
@@ -1106,17 +1106,28 @@ initZx();
1106
1106
  * @param event - Event payload from the dispatch message
1107
1107
  * @param changedFiles - List of files changed in this event
1108
1108
  * @param env - Merged environment variables
1109
+ * @param dispatchInputs - Operator dispatch inputs (`ctx.dispatchInputs`)
1110
+ * @param fanout - Fan-out position (`ctx.fanout`); undefined on a non-fan-out job
1109
1111
  */
1110
- function createRuleContext(event, changedFiles = [], env = {}) {
1112
+ function createRuleContext(event, changedFiles = [], env = {}, dispatchInputs = {}, fanout) {
1111
1113
  return {
1112
1114
  event,
1113
1115
  changedFiles,
1114
1116
  env,
1117
+ dispatchInputs,
1118
+ ...fanout && { fanout },
1115
1119
  $
1116
1120
  };
1117
1121
  }
1118
1122
  //#endregion
1119
1123
  //#region src/execution/sandbox/step-loop.ts
1124
+ /** Result of a rejected drift gate: the run was declined by a reviewer. */
1125
+ var DriftGateRejectedError = class extends Error {
1126
+ constructor(reason) {
1127
+ super(reason ? `approval rejected: ${reason}` : "approval rejected");
1128
+ this.name = "DriftGateRejectedError";
1129
+ }
1130
+ };
1120
1131
  /**
1121
1132
  * Run one step honoring the run-level {@link CheckMode}, reusing the
1122
1133
  * `runIdempotentStep` primitive for checked steps (never hand-rolled branching).
@@ -1124,10 +1135,13 @@ function createRuleContext(event, changedFiles = [], env = {}) {
1124
1135
  * - Plain step (no `check`): in apply mode, runs as today; in any check mode it
1125
1136
  * is skipped with `no_check` (a side-effecting step can't be safely previewed).
1126
1137
  * - Checked step: adapted into an `IdempotentStep` and driven by the primitive
1127
- * with `dryRun` set in check mode (so `apply`/`run` never fires) and `yes: true`
1128
- * (v0 has no mid-run confirm). On drift the summary is emitted as a log line.
1138
+ * with `dryRun` set in check mode (so `apply`/`run` never fires). On drift the
1139
+ * summary is emitted as a log line. A `approval: { when: 'drift' }` step in
1140
+ * apply mode passes a `confirm` callback that round-trips a payload-bearing
1141
+ * step-approval; on reject the gate throws (fail-stop). Any other apply-mode
1142
+ * step uses `yes: true`.
1129
1143
  */
1130
- async function runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn) {
1144
+ async function runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn, opts) {
1131
1145
  if (!step.check) {
1132
1146
  if (checkMode !== CheckMode.enum.apply) return {
1133
1147
  checkOutcome: CheckStepOutcome.enum.no_check,
@@ -1139,15 +1153,37 @@ async function runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn) {
1139
1153
  outputs: await step.run(ctx)
1140
1154
  };
1141
1155
  }
1142
- const res = await runIdempotentStep({
1156
+ let lastDrift = null;
1157
+ const adapted = {
1143
1158
  name: step.name,
1144
- check: () => step.check(ctx),
1159
+ check: async () => {
1160
+ lastDrift = await step.check(ctx);
1161
+ return lastDrift;
1162
+ },
1145
1163
  summarize: step.summarize,
1146
1164
  apply: (drift) => step.run(ctx, drift),
1147
1165
  whenInSync: step.whenInSync ? () => step.whenInSync(ctx) : void 0
1148
- }, {
1166
+ };
1167
+ const driftGate = step.approval !== void 0 && normalizeApproval(step.approval).when === "drift" && checkMode === CheckMode.enum.apply && opts.awaitStepApprovalWithPayload !== void 0;
1168
+ const res = await runIdempotentStep(adapted, {
1149
1169
  dryRun: checkMode !== CheckMode.enum.apply,
1150
- yes: true,
1170
+ ...driftGate ? { confirm: async () => {
1171
+ const norm = normalizeApproval(step.approval);
1172
+ const summaryMarkdown = step.summarize(lastDrift);
1173
+ const resolution = await opts.awaitStepApprovalWithPayload({
1174
+ stepIndex,
1175
+ stepName: step.name,
1176
+ clauses: norm.clauses,
1177
+ reason: norm.reason ?? `Approval required for drift in '${step.name}'`,
1178
+ ...norm.timeoutSeconds !== void 0 && { timeoutSeconds: norm.timeoutSeconds },
1179
+ payload: {
1180
+ summaryMarkdown,
1181
+ drift: lastDrift
1182
+ }
1183
+ });
1184
+ if (resolution.outcome === "approved") return true;
1185
+ throw new DriftGateRejectedError(resolution.outcome === "expired" ? "approval expired" : resolution.reason);
1186
+ } } : { yes: true },
1151
1187
  log: (line) => sendFn({
1152
1188
  type: "log.line",
1153
1189
  stepIndex,
@@ -1170,7 +1206,7 @@ async function runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn) {
1170
1206
  *
1171
1207
  * Timeout pattern using Promise.race + AbortController, with IPC status reporting.
1172
1208
  */
1173
- async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, outputsMap, getSecretsAccessLog, getSecretMountRecords, jobDeadlineSignal, checkMode = CheckMode.enum.apply) {
1209
+ async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, outputsMap, getSecretsAccessLog, getSecretMountRecords, jobDeadlineSignal, checkMode, opts) {
1174
1210
  sendFn({
1175
1211
  type: "step.start",
1176
1212
  stepIndex,
@@ -1181,7 +1217,7 @@ async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, output
1181
1217
  const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
1182
1218
  try {
1183
1219
  const phase = await Promise.race([
1184
- runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn),
1220
+ runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn, opts),
1185
1221
  new Promise((_, reject) => {
1186
1222
  abortController.signal.addEventListener("abort", () => {
1187
1223
  reject(/* @__PURE__ */ new Error(`Step '${step.name}' timed out after ${timeoutMs}ms`));
@@ -1284,7 +1320,7 @@ function extractSignal(error) {
1284
1320
  */
1285
1321
  async function evaluateStepRulesAndMaybeSkip(step, stepIndex, opts) {
1286
1322
  if (!step.rules || step.rules.length === 0) return null;
1287
- const ruleCtx = createRuleContext(opts.event, [], opts.env);
1323
+ const ruleCtx = createRuleContext(opts.event, [], opts.env, opts.dispatchInputs ?? {}, opts.fanout);
1288
1324
  const ruleResult = await evaluateRules(step.rules, ruleCtx, step.name);
1289
1325
  if (ruleResult.allPassed) return null;
1290
1326
  opts.sendIpc({
@@ -1311,14 +1347,17 @@ async function evaluateStepRulesAndMaybeSkip(step, stepIndex, opts) {
1311
1347
  };
1312
1348
  }
1313
1349
  /**
1314
- * Manual approval gate for a step. When the step declares `requireApproval`
1315
- * and the harness wired `awaitStepApproval`, block until the orchestrator
1316
- * resolves the hold. Returns a failed `StepIterationOutcome` (breaking the
1317
- * loop) on reject/expired; returns null when approved or when no gate applies.
1350
+ * Pre-step manual approval gate. When the step declares `approval` with
1351
+ * `when: 'always'` and the harness wired `awaitStepApproval`, block until the
1352
+ * orchestrator resolves the hold. A `when: 'drift'` gate is NOT handled here —
1353
+ * it fires mid-execution inside `runStepWithCheckMode` once `check()` returns
1354
+ * drift. Returns a failed `StepIterationOutcome` (breaking the loop) on
1355
+ * reject/expired; returns null when approved or when no gate applies.
1318
1356
  */
1319
1357
  async function maybeGateStepApproval(step, stepIndex, opts) {
1320
- if (step.requireApproval === void 0 || !opts.awaitStepApproval) return null;
1321
- const normalized = normalizeRequireApproval(step.requireApproval);
1358
+ if (step.approval === void 0 || !opts.awaitStepApproval) return null;
1359
+ const normalized = normalizeApproval(step.approval);
1360
+ if (normalized.when === "drift") return null;
1322
1361
  opts.sendIpc({
1323
1362
  type: "log.line",
1324
1363
  stepIndex,
@@ -1407,6 +1446,39 @@ async function runObserverHook(args) {
1407
1446
  * `ctx.secrets.mountFile` tmpdir, any env vars set via `exposeFile`) is
1408
1447
  * removed even when the step throws, times out, or rule-skips.
1409
1448
  */
1449
+ /**
1450
+ * Run a step through its retry policy. Each call to `executeStepInLoop` is one
1451
+ * attempt: it sets up its own per-attempt timeout from `step.timeout` and returns
1452
+ * a `SandboxStepResult` (it never throws — a failed attempt is reported as a
1453
+ * `failed` status with an `error`). A failed attempt is retried while attempts
1454
+ * remain AND `retryIf(reconstructedError)` is true; backoff sleeps between
1455
+ * attempts. The retry loop runs to completion BEFORE the caller applies
1456
+ * `continueOnError` to the final outcome.
1457
+ */
1458
+ async function runStepWithRetry(step, stepIndex, ctx, timeoutMs, opts) {
1459
+ const retry = step.retry;
1460
+ const max = retry?.maxAttempts ?? 1;
1461
+ let result;
1462
+ for (let n = 1; n <= max; n++) {
1463
+ result = await executeStepInLoop(step, stepIndex, ctx, timeoutMs, opts.sendIpc, opts.outputsMap, opts.getSecretsAccessLog, opts.getSecretMountRecords, opts.jobDeadlineSignal, opts.checkMode ?? CheckMode.enum.apply, opts);
1464
+ if (result.status !== ExecutionStepStatus.enum.failed) return result;
1465
+ const err = new Error(result.error?.message ?? `Step '${step.name}' failed`);
1466
+ if (!(n < max && (retry?.retryIf?.(err) ?? true))) break;
1467
+ const delay = computeBackoffDelay(n, {
1468
+ maxAttempts: max,
1469
+ delayMs: retry.delayMs,
1470
+ backoff: retry.backoff,
1471
+ maxDelayMs: retry.maxDelayMs
1472
+ });
1473
+ opts.sendIpc({
1474
+ type: "log.line",
1475
+ stepIndex,
1476
+ line: `[kici] Step '${step.name}' attempt ${n}/${max} failed: ${err.message}; retrying in ${delay}ms`
1477
+ });
1478
+ await new Promise((r) => setTimeout(r, delay));
1479
+ }
1480
+ return result;
1481
+ }
1410
1482
  async function runStepIteration(step, stepIndex, opts) {
1411
1483
  const skippedResult = await evaluateStepRulesAndMaybeSkip(step, stepIndex, opts);
1412
1484
  if (skippedResult) {
@@ -1434,7 +1506,7 @@ async function runStepIteration(step, stepIndex, opts) {
1434
1506
  const timeoutMs = step.timeout ?? opts.defaultTimeoutMs;
1435
1507
  let result;
1436
1508
  try {
1437
- result = await executeStepInLoop(step, stepIndex, ctx, timeoutMs, opts.sendIpc, opts.outputsMap, opts.getSecretsAccessLog, opts.getSecretMountRecords, opts.jobDeadlineSignal, opts.checkMode ?? CheckMode.enum.apply);
1509
+ result = await runStepWithRetry(step, stepIndex, ctx, timeoutMs, opts);
1438
1510
  } finally {
1439
1511
  await opts.afterStepApplyEnvFiles?.();
1440
1512
  }
@@ -3076,8 +3148,8 @@ function logSubprocessStreams(e, tokens) {
3076
3148
  * no Rolldown step at runtime. `@kici-dev/sdk` and host-repo deps resolve via
3077
3149
  * Node's normal ESM lookup against `.kici/node_modules/`.
3078
3150
  */
3079
- const AGENT_SDK_VERSION = "0.1.21";
3080
- const AGENT_SDK_BUNDLE_HASH = "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
3151
+ const AGENT_SDK_VERSION = "0.1.23";
3152
+ const AGENT_SDK_BUNDLE_HASH = "3a83610d2d122b9f9b0f924b225a47050d0b35f00a8f3256a3e15f3ff3bf7ddc";
3081
3153
  /**
3082
3154
  * Register the `@kici-dev/core/ts-loader-hook` oxc-transform ESM loader hook so
3083
3155
  * subsequent dynamic `import()` calls for `.ts` / `.tsx` files transform on the
@@ -3439,7 +3511,7 @@ async function applyOverlay(config) {
3439
3511
  */
3440
3512
  init_download();
3441
3513
  init_dep_restore();
3442
- const AGENT_VERSION = "0.1.21";
3514
+ const AGENT_VERSION = "0.1.23";
3443
3515
  process.on("uncaughtException", (err) => {
3444
3516
  process.stderr.write(`[workflow-runner] UNCAUGHT EXCEPTION: ${err.message}\n`);
3445
3517
  if (err.stack) process.stderr.write(`[workflow-runner] Stack: ${err.stack}\n`);
@@ -3832,35 +3904,49 @@ function waitForApprovalResolution(requestId) {
3832
3904
  });
3833
3905
  }
3834
3906
  /**
3835
- * Build the `awaitStepApproval` callback the step loop uses to block on a
3836
- * `requireApproval` step. Sends an `approval.request` IPC (relayed by the agent
3837
- * over the WS as a `step.approval-request`) and awaits the matching
3838
- * `approval.resolved`. A relay error is treated as a fail-closed reject.
3907
+ * Send a step-approval `approval.request` IPC (relayed by the agent over the WS
3908
+ * as a `step.approval-request`), await the matching `approval.resolved`, and map
3909
+ * it onto a `StepApprovalResolution`. A relay error is treated as a fail-closed
3910
+ * reject. Shared by the `when: 'always'` and `when: 'drift'` callbacks; the
3911
+ * latter carries a drift `payload`.
3839
3912
  */
3840
- function buildAwaitStepApproval() {
3841
- return async (req) => {
3842
- const requestId = randomUUID();
3843
- sendMessage({
3844
- type: "approval.request",
3845
- requestId,
3846
- stepIndex: req.stepIndex,
3847
- stepName: req.stepName,
3848
- clauses: req.clauses,
3849
- reason: req.reason,
3850
- ...req.timeoutSeconds !== void 0 && { timeoutSeconds: req.timeoutSeconds }
3851
- });
3852
- const resolution = await waitForApprovalResolution(requestId);
3853
- if (resolution.error) return {
3854
- outcome: "rejected",
3855
- reason: resolution.error
3856
- };
3857
- return {
3858
- outcome: resolution.outcome ?? "rejected",
3859
- ...resolution.reason !== void 0 && { reason: resolution.reason }
3860
- };
3913
+ async function requestStepApproval(req) {
3914
+ const requestId = randomUUID();
3915
+ sendMessage({
3916
+ type: "approval.request",
3917
+ requestId,
3918
+ stepIndex: req.stepIndex,
3919
+ stepName: req.stepName,
3920
+ clauses: req.clauses,
3921
+ reason: req.reason,
3922
+ ...req.timeoutSeconds !== void 0 && { timeoutSeconds: req.timeoutSeconds },
3923
+ ...req.payload !== void 0 && { payload: req.payload }
3924
+ });
3925
+ const resolution = await waitForApprovalResolution(requestId);
3926
+ if (resolution.error) return {
3927
+ outcome: "rejected",
3928
+ reason: resolution.error
3929
+ };
3930
+ return {
3931
+ outcome: resolution.outcome ?? "rejected",
3932
+ ...resolution.reason !== void 0 && { reason: resolution.reason }
3861
3933
  };
3862
3934
  }
3863
3935
  /**
3936
+ * Build the `awaitStepApproval` callback the step loop uses to block on an
3937
+ * `approval` step (`when: 'always'`).
3938
+ */
3939
+ function buildAwaitStepApproval() {
3940
+ return (req) => requestStepApproval(req);
3941
+ }
3942
+ /**
3943
+ * Build the `awaitStepApprovalWithPayload` callback the step loop uses to block
3944
+ * on a `when: 'drift'` step mid-execution, carrying the computed drift payload.
3945
+ */
3946
+ function buildAwaitStepApprovalWithPayload() {
3947
+ return (req) => requestStepApproval(req);
3948
+ }
3949
+ /**
3864
3950
  * Build the {@link CacheTransport} the sandbox-side cache engine uses to reach
3865
3951
  * the orchestrator. Each method sends a `cache.request` IPC (relayed by the
3866
3952
  * agent over the WS as a `cache.user.*` message) and awaits the matching
@@ -4093,6 +4179,75 @@ function createIpcLogger(stepIndex, stepName, sendFn) {
4093
4179
  }
4094
4180
  };
4095
4181
  }
4182
+ /** A lock needs entry maps to a base name (single/matrix/host) or a group name. */
4183
+ function needBaseName(need) {
4184
+ if (typeof need === "string") return {
4185
+ kind: "job",
4186
+ key: need
4187
+ };
4188
+ if (need && typeof need === "object") {
4189
+ if ("group" in need) return {
4190
+ kind: "group",
4191
+ key: need.group
4192
+ };
4193
+ if ("name" in need) return {
4194
+ kind: "job",
4195
+ key: need.name
4196
+ };
4197
+ }
4198
+ return null;
4199
+ }
4200
+ /**
4201
+ * Build `ctx.needs` for a job's steps from the dispatch envelope. Reconstructs
4202
+ * an {@link UpstreamSnapshot} from `upstreamJobOutputs` (flat per single job;
4203
+ * `byMatrix` / `byHost` envelopes per fan-out) + `upstreamJobStatuses` (keyed by
4204
+ * each upstream job/child name), then resolves the job's declared needs into the
4205
+ * `{ result, status }` / ordered-array shape via the shared SDK builder. Returns
4206
+ * undefined when the job declares no needs.
4207
+ */
4208
+ function buildStepNeedsContext(declaredNeeds, upstreamJobOutputs, upstreamJobStatuses) {
4209
+ if (!declaredNeeds || declaredNeeds.length === 0) return void 0;
4210
+ const statuses = upstreamJobStatuses ?? {};
4211
+ const jobs = {};
4212
+ const groups = {};
4213
+ const snapStatuses = {};
4214
+ const resolvedNeeds = [];
4215
+ for (const need of declaredNeeds) {
4216
+ const base = needBaseName(need);
4217
+ if (!base) continue;
4218
+ const childNames = Object.keys(statuses).filter((n) => n.startsWith(`${base.key} (`));
4219
+ if (base.kind === "group" || childNames.length > 0) {
4220
+ groups[base.key] = [...childNames].sort();
4221
+ const envelope = upstreamJobOutputs?.[base.key];
4222
+ const bySuffix = envelopeChildOutputs(envelope);
4223
+ for (const child of childNames) {
4224
+ jobs[child] = bySuffix[child.slice(base.key.length + 2, -1)] ?? {};
4225
+ snapStatuses[child] = statuses[child];
4226
+ }
4227
+ resolvedNeeds.push({ group: base.key });
4228
+ } else {
4229
+ jobs[base.key] = upstreamJobOutputs?.[base.key] ?? {};
4230
+ if (statuses[base.key]) snapStatuses[base.key] = statuses[base.key];
4231
+ resolvedNeeds.push(base.key);
4232
+ }
4233
+ }
4234
+ return buildNeedsContext({
4235
+ jobs,
4236
+ groups,
4237
+ statuses: snapStatuses
4238
+ }, resolvedNeeds);
4239
+ }
4240
+ /**
4241
+ * Extract per-child output records from a fan-out outputs envelope, keyed by the
4242
+ * combination suffix (matrix `byMatrix`) or hostname (`runsOnAll` `byHost`).
4243
+ * Returns an empty map for a non-envelope value.
4244
+ */
4245
+ function envelopeChildOutputs(envelope) {
4246
+ if (!envelope) return {};
4247
+ const byMatrix = envelope.byMatrix;
4248
+ const byHost = envelope.byHost;
4249
+ return byMatrix ?? byHost ?? {};
4250
+ }
4096
4251
  /**
4097
4252
  * Build StepSecrets from the job execution request, wired with the per-step
4098
4253
  * file-mount host (used by `ctx.secrets.mountFile` / `exposeFile`).
@@ -4288,7 +4443,31 @@ function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedS
4288
4443
  ...request.provider && { provider: request.provider },
4289
4444
  ...request.matrixValues && { matrix: request.matrixValues },
4290
4445
  ...request.host && { host: request.host },
4291
- ...request.agent && { agent: request.agent }
4446
+ ...request.agent && { agent: request.agent },
4447
+ ...(() => {
4448
+ const fanout = deriveFanout(request);
4449
+ return fanout ? { fanout } : {};
4450
+ })(),
4451
+ dispatchInputs: request.dispatchInputs ?? {},
4452
+ ...(() => {
4453
+ const needs = buildStepNeedsContext(request.jobNeeds, request.upstreamJobOutputs, request.upstreamJobStatuses);
4454
+ return needs ? { needs } : {};
4455
+ })()
4456
+ };
4457
+ }
4458
+ /**
4459
+ * Derive the fan-out position (`ctx.fanout`) from a dispatch request. Returns
4460
+ * `undefined` for a non-fan-out job (no `fanoutTotal`), so `ctx.fanout` is only
4461
+ * set for `runsOnAll` host children and matrix combinations.
4462
+ */
4463
+ function deriveFanout(request) {
4464
+ if (request.fanoutTotal === void 0) return void 0;
4465
+ const index = request.fanoutIndex ?? 0;
4466
+ return {
4467
+ index,
4468
+ total: request.fanoutTotal,
4469
+ first: index === 0,
4470
+ last: index === request.fanoutTotal - 1
4292
4471
  };
4293
4472
  }
4294
4473
  /** Raw provider webhook body for ctx.rawPayload — nested in the envelope. */
@@ -4904,7 +5083,7 @@ function buildOutputInfrastructure(request, refMap) {
4904
5083
  */
4905
5084
  async function maybeSkipJobOnRules(job, request, normalizedSteps) {
4906
5085
  if (!job?.rules || job.rules.length === 0) return false;
4907
- const ruleCtx = createRuleContext(request.event ?? {}, [], process.env);
5086
+ const ruleCtx = createRuleContext(request.event ?? {}, [], process.env, request.dispatchInputs ?? {}, deriveFanout(request));
4908
5087
  if ((await evaluateRules(job.rules, ruleCtx, request.jobName)).allPassed) return false;
4909
5088
  const skippedResults = normalizedSteps.map((s, i) => ({
4910
5089
  name: s.name,
@@ -5159,6 +5338,8 @@ async function main() {
5159
5338
  outputsMap,
5160
5339
  event: request.event ?? {},
5161
5340
  env: process.env,
5341
+ dispatchInputs: request.dispatchInputs ?? {},
5342
+ fanout: deriveFanout(request),
5162
5343
  jobHooks,
5163
5344
  cachePhaseDeps,
5164
5345
  isAborted: () => aborted,
@@ -5180,7 +5361,8 @@ async function main() {
5180
5361
  },
5181
5362
  beforeStepEnvFiles: stepEnvHooks.beforeStepEnvFiles,
5182
5363
  afterStepApplyEnvFiles: stepEnvHooks.afterStepApplyEnvFiles,
5183
- awaitStepApproval: buildAwaitStepApproval()
5364
+ awaitStepApproval: buildAwaitStepApproval(),
5365
+ awaitStepApprovalWithPayload: buildAwaitStepApprovalWithPayload()
5184
5366
  });
5185
5367
  jobDeadline.clear();
5186
5368
  await maybeSaveJobCache(jobCacheSpecs, jobCacheRestore, cachePhaseDeps, !aborted && loopResult.status === ExecutionStepStatus.enum.success);
@@ -5266,6 +5448,6 @@ main().catch((error) => {
5266
5448
  setTimeout(() => process.exit(1), 100);
5267
5449
  });
5268
5450
  //#endregion
5269
- export { createSandboxStepContext, rawPayloadFromEvent };
5451
+ export { buildStepNeedsContext, createSandboxStepContext, deriveFanout, rawPayloadFromEvent };
5270
5452
 
5271
5453
  //# sourceMappingURL=workflow-runner.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kici-dev/agent",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "Customer-deployable agent for the KiCI CI/CD stack. Connects to an orchestrator, clones the workflow repo, executes steps, and streams logs back.",
5
5
  "keywords": [
6
6
  "ci",
@@ -64,10 +64,10 @@
64
64
  "yaml": "^2.9.0",
65
65
  "zod": "^4.4.3",
66
66
  "zx": "^8.8.5",
67
- "@kici-dev/core": "0.1.21",
68
- "@kici-dev/engine": "0.1.21",
69
- "@kici-dev/sdk": "0.1.21",
70
- "@kici-dev/shared": "0.1.21"
67
+ "@kici-dev/core": "0.1.23",
68
+ "@kici-dev/engine": "0.1.23",
69
+ "@kici-dev/sdk": "0.1.23",
70
+ "@kici-dev/shared": "0.1.23"
71
71
  },
72
72
  "kici": {
73
73
  "metrics": {