@kici-dev/agent 0.1.17 → 0.1.18

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.
@@ -24,7 +24,8 @@ import { fileURLToPath, pathToFileURL } from "node:url";
24
24
  import { c, x } from "tar";
25
25
  import { execFile } from "node:child_process";
26
26
  import { promisify } from "node:util";
27
- import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, detectPackageManagerFromManifests } from "@kici-dev/shared/package-manager";
27
+ import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, YarnFlavor, detectPackageManagerFromManifests, detectYarnFlavor } from "@kici-dev/shared/package-manager";
28
+ import { parse, stringify } from "yaml";
28
29
  var __defProp = Object.defineProperty;
29
30
  var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
30
31
  var __exportAll = (all, no_symbols) => {
@@ -2175,7 +2176,7 @@ function noopResult() {
2175
2176
  };
2176
2177
  }
2177
2178
  /** Build the synthesized env-var name for registry index `i`. */
2178
- function tokenEnvName(jobIdShort, index) {
2179
+ function tokenEnvName$1(jobIdShort, index) {
2179
2180
  return `KICI_NPM_TOKEN_${jobIdShort}_${index}`;
2180
2181
  }
2181
2182
  /** Render the agent-managed block of `.npmrc` lines. */
@@ -2184,7 +2185,7 @@ function renderAgentLines(registries, jobIdShort) {
2184
2185
  const lines = [];
2185
2186
  for (let i = 0; i < registries.length; i++) {
2186
2187
  const reg = registries[i];
2187
- const envVar = tokenEnvName(jobIdShort, i);
2188
+ const envVar = tokenEnvName$1(jobIdShort, i);
2188
2189
  const authKey = reg.url.replace(/^https?:/, "");
2189
2190
  if (reg.scope) lines.push(`${reg.scope}:registry=${reg.url}`);
2190
2191
  else lines.push(`registry=${reg.url}`);
@@ -2216,7 +2217,7 @@ async function applyNpmRegistryConfig(args) {
2216
2217
  const tokenEnv = {};
2217
2218
  const tokensForRedaction = [];
2218
2219
  for (let i = 0; i < registries.length; i++) {
2219
- tokenEnv[tokenEnvName(args.jobIdShort, i)] = registries[i].token;
2220
+ tokenEnv[tokenEnvName$1(args.jobIdShort, i)] = registries[i].token;
2220
2221
  tokensForRedaction.push(registries[i].token);
2221
2222
  }
2222
2223
  for (const value of Object.values(installEnvSecrets)) if (value) tokensForRedaction.push(value);
@@ -2252,6 +2253,112 @@ function redactNpmOutput(input, tokens) {
2252
2253
  }
2253
2254
  return out;
2254
2255
  }
2256
+ //#endregion
2257
+ //#region src/execution/yarnrc-berry-config.ts
2258
+ /**
2259
+ * Apply yarn-berry registry auth + a forced `nodeLinker: node-modules` to a
2260
+ * workflow's `.kici/.yarnrc.yml` for the lifetime of one `yarn install`, then
2261
+ * restore the file on cleanup. The berry analog of `npm-registry-config.ts`:
2262
+ * berry reads `.yarnrc.yml` (not `.npmrc`), so the auth block uses berry's
2263
+ * `npmRegistryServer` / `npmScopes` / `npmAuthToken` keys with `${VAR}`
2264
+ * env-var interpolation. Token bytes never reach disk — each registry token is
2265
+ * exposed as a job-scoped env var and the on-disk value is the `${VAR}`
2266
+ * reference.
2267
+ *
2268
+ * `nodeLinker: node-modules` makes berry lay down a real `node_modules` tree
2269
+ * (no PnP `.pnp.cjs`), so the agent's packer / restore / sibling-walk /
2270
+ * workflow-loader work unchanged. `enableScripts: false` (when a private
2271
+ * registry is configured) keeps dependency lifecycle scripts from seeing the
2272
+ * synthesized token env vars — the same security model as npm/pnpm/classic
2273
+ * `--ignore-scripts`.
2274
+ *
2275
+ * Reuses the same `ApplyNpmRegistryConfigArgs` / `ApplyNpmRegistryConfigResult`
2276
+ * shapes as the npm overlay so `dep-installer` can pick either by flavor.
2277
+ */
2278
+ /** Build the synthesized env-var name for registry index `i`. */
2279
+ function tokenEnvName(jobIdShort, index) {
2280
+ return `KICI_NPM_TOKEN_${jobIdShort}_${index}`;
2281
+ }
2282
+ /** Read + parse an existing `.yarnrc.yml`, or `{}` when absent/empty. */
2283
+ async function readOriginalYarnrc(path) {
2284
+ try {
2285
+ const raw = await readFile(path, "utf8");
2286
+ return {
2287
+ raw,
2288
+ doc: parse(raw) ?? {}
2289
+ };
2290
+ } catch (err) {
2291
+ if (err.code === "ENOENT") return {
2292
+ raw: null,
2293
+ doc: {}
2294
+ };
2295
+ throw err;
2296
+ }
2297
+ }
2298
+ function buildRegistryBlock(envVar, url, alwaysAuth) {
2299
+ return {
2300
+ npmRegistryServer: url,
2301
+ npmAuthToken: `\${${envVar}}`,
2302
+ ...alwaysAuth ? { npmAlwaysAuth: true } : {}
2303
+ };
2304
+ }
2305
+ async function applyYarnrcBerryConfig(args) {
2306
+ const registries = args.npmRegistries ?? [];
2307
+ const installEnvSecrets = args.installEnvSecrets ?? {};
2308
+ const hasPrivateRegistry = registries.length > 0 || Object.keys(installEnvSecrets).length > 0;
2309
+ const yarnrcPath = join(args.kiciDir, ".yarnrc.yml");
2310
+ const { raw: original, doc } = await readOriginalYarnrc(yarnrcPath);
2311
+ const cacheFolder = await mkdtemp(join(tmpdir(), "kici-yarn-berry-cache-"));
2312
+ const merged = {
2313
+ ...doc,
2314
+ nodeLinker: "node-modules",
2315
+ enableGlobalCache: false,
2316
+ cacheFolder
2317
+ };
2318
+ const tokenEnv = {};
2319
+ const tokensForRedaction = [];
2320
+ if (hasPrivateRegistry) {
2321
+ merged.enableScripts = false;
2322
+ const npmScopes = { ...doc.npmScopes ?? {} };
2323
+ for (let i = 0; i < registries.length; i++) {
2324
+ const reg = registries[i];
2325
+ const envVar = tokenEnvName(args.jobIdShort, i);
2326
+ tokenEnv[envVar] = reg.token;
2327
+ tokensForRedaction.push(reg.token);
2328
+ const block = buildRegistryBlock(envVar, reg.url, reg.alwaysAuth);
2329
+ if (reg.scope) npmScopes[reg.scope] = block;
2330
+ else {
2331
+ merged.npmRegistryServer = reg.url;
2332
+ merged.npmAuthToken = block.npmAuthToken;
2333
+ if (reg.alwaysAuth) merged.npmAlwaysAuth = true;
2334
+ }
2335
+ }
2336
+ if (Object.keys(npmScopes).length > 0) merged.npmScopes = npmScopes;
2337
+ for (const value of Object.values(installEnvSecrets)) if (value) tokensForRedaction.push(value);
2338
+ }
2339
+ await writeFile(yarnrcPath, stringify(merged), {
2340
+ encoding: "utf8",
2341
+ mode: 384
2342
+ });
2343
+ const cleanup = async () => {
2344
+ try {
2345
+ if (original === null) await unlink(yarnrcPath).catch(() => {});
2346
+ else await writeFile(yarnrcPath, original, { encoding: "utf8" });
2347
+ } catch {}
2348
+ await rm(cacheFolder, {
2349
+ recursive: true,
2350
+ force: true
2351
+ }).catch(() => {});
2352
+ };
2353
+ return {
2354
+ extraEnv: {
2355
+ ...installEnvSecrets,
2356
+ ...tokenEnv
2357
+ },
2358
+ tokensForRedaction,
2359
+ cleanup
2360
+ };
2361
+ }
2255
2362
  const LOCAL_PROTOCOLS = [
2256
2363
  "workspace:",
2257
2364
  "file:",
@@ -2320,6 +2427,17 @@ async function fileExists$1(target) {
2320
2427
  return false;
2321
2428
  }
2322
2429
  }
2430
+ /** Whether the repo-root package.json declares a non-empty `workspaces` array. */
2431
+ async function rootHasWorkspaces(repoRoot) {
2432
+ try {
2433
+ const ws = JSON.parse(await readFile(join(repoRoot, "package.json"), "utf-8")).workspaces;
2434
+ if (Array.isArray(ws)) return ws.length > 0;
2435
+ if (ws && typeof ws === "object" && Array.isArray(ws.packages)) return ws.packages.length > 0;
2436
+ return false;
2437
+ } catch {
2438
+ return false;
2439
+ }
2440
+ }
2323
2441
  /** Resolve a `file:`/`link:`/`portal:` spec to an absolute path under kiciDir. */
2324
2442
  function resolveLocalPath(kiciDir, dep) {
2325
2443
  const rawPath = dep.spec.slice(dep.protocol.length);
@@ -2334,8 +2452,20 @@ function isInsideRepo(repoRoot, target) {
2334
2452
  * Classify each local-protocol dependency for the detected package manager and
2335
2453
  * return the ones that are unresolvable in the agent's single-clone model.
2336
2454
  */
2337
- async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
2455
+ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot, yarnFlavor) {
2338
2456
  if (packageManager === PackageManager.Npm) return [...deps];
2457
+ if (packageManager === PackageManager.Yarn && yarnFlavor === YarnFlavor.Berry) {
2458
+ const hasWorkspaces = await rootHasWorkspaces(repoRoot);
2459
+ const unresolvable = [];
2460
+ for (const dep of deps) {
2461
+ if (dep.protocol === "workspace:") {
2462
+ if (!hasWorkspaces) unresolvable.push(dep);
2463
+ continue;
2464
+ }
2465
+ if (!isInsideRepo(repoRoot, resolveLocalPath(kiciDir, dep))) unresolvable.push(dep);
2466
+ }
2467
+ return unresolvable;
2468
+ }
2339
2469
  if (packageManager === PackageManager.Yarn) {
2340
2470
  const unresolvable = [];
2341
2471
  for (const dep of deps) {
@@ -2359,10 +2489,11 @@ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
2359
2489
  return unresolvable;
2360
2490
  }
2361
2491
  /** Build the actionable error for unresolvable local-protocol dependencies. */
2362
- function formatUnresolvableDepError(offenders, packageManager) {
2492
+ function formatUnresolvableDepError(offenders, packageManager, yarnFlavor) {
2363
2493
  const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
2364
2494
  if (packageManager === PackageManager.Npm) return `These .kici/ dependencies use local-protocol specifiers npm cannot resolve from a registry: ${list}. npm has no workspace protocol — pin a published version, publish the package to your registry, or use pnpm so an in-repo workspace sibling can be resolved.`;
2365
- if (packageManager === PackageManager.Yarn) return `These .kici/ dependencies use specifiers yarn classic cannot resolve: ${list}. yarn classic has no workspace: or portal: protocol reference an in-repo sibling by a version range (yarn links matching workspace members), use pnpm, or keep file:/link: paths inside this repository. (yarn berry support is planned.)`;
2495
+ if (packageManager === PackageManager.Yarn && yarnFlavor === YarnFlavor.Berry) return `These .kici/ dependencies cannot be resolved by yarn berry from the cloned repository: ${list}. A workspace: dependency requires a "workspaces" array in the repo-root package.json, and file:/link:/portal: paths must stay inside this repository.`;
2496
+ if (packageManager === PackageManager.Yarn) return `These .kici/ dependencies use specifiers yarn classic cannot resolve: ${list}. yarn classic has no workspace: or portal: protocol — reference an in-repo sibling by a version range (yarn links matching workspace members), use pnpm, or keep file:/link: paths inside this repository. (yarn berry support requires a yarn@2+ packageManager field or a .yarnrc.yml.)`;
2366
2497
  return `These .kici/ dependencies point outside the cloned repository, which the agent never has: ${list}. A workspace: dependency requires a pnpm-workspace.yaml at the repo root, and file:/link:/portal: paths must stay inside this repository.`;
2367
2498
  }
2368
2499
  /**
@@ -2376,9 +2507,10 @@ async function assertResolvableDeps(args) {
2376
2507
  if (!pkg) return;
2377
2508
  const localDeps = findLocalProtocolDeps(pkg);
2378
2509
  if (localDeps.length === 0) return;
2379
- const offenders = await findUnresolvableDeps(localDeps, args.packageManager, args.kiciDir, args.repoRoot);
2510
+ const flavor = args.yarnFlavor ?? YarnFlavor.Classic;
2511
+ const offenders = await findUnresolvableDeps(localDeps, args.packageManager, args.kiciDir, args.repoRoot, flavor);
2380
2512
  if (offenders.length === 0) return;
2381
- throw new Error(formatUnresolvableDepError(offenders, args.packageManager));
2513
+ throw new Error(formatUnresolvableDepError(offenders, args.packageManager, flavor));
2382
2514
  }
2383
2515
  //#endregion
2384
2516
  //#region src/execution/workspace-siblings.ts
@@ -2487,10 +2619,13 @@ function isAbsoluteRel(rel) {
2487
2619
  * presence of `.kici/package.json` signals that deps should be installed. npm
2488
2620
  * is the default and ships with every Node.js install; pnpm is used when the
2489
2621
  * repo is a pnpm workspace so a `.kici/` member can resolve in-repo
2490
- * `workspace:` siblings. yarn classic (v1) is supported for registry
2491
- * dependencies and version-range workspace siblings (which it links but does
2492
- * not build, so the agent builds the in-repo closure after install). yarn
2493
- * berry (v2+) is not yet supported.
2622
+ * `workspace:` siblings. yarn is supported in both flavors: classic (v1) reads
2623
+ * `.kici/.npmrc` for registry auth and links version-range workspace siblings;
2624
+ * berry (v2+) reads a synthesized `.kici/.yarnrc.yml` for auth, runs with a
2625
+ * forced `nodeLinker: node-modules` (so the resulting tree matches classic/npm
2626
+ * and the runner's plain node resolution holds), and resolves
2627
+ * `workspace:`/`portal:` siblings. Either flavor links the sibling but does not
2628
+ * build it, so the agent builds the in-repo closure after install.
2494
2629
  *
2495
2630
  * Security: the install runs with an isolated per-invocation cache/store
2496
2631
  * directory to prevent cache poisoning across build jobs — a malicious
@@ -2516,6 +2651,15 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
2516
2651
  return await detectPackageManagerFromManifests(repoRoot) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
2517
2652
  }
2518
2653
  /**
2654
+ * Detect the yarn flavor (classic vs berry) for the cloned repo. Mirrors
2655
+ * `detectKiciPackageManager`: probe the repo root first, then `.kici/` for a
2656
+ * standalone project. Only called when the detected manager is `Yarn`.
2657
+ */
2658
+ async function detectKiciYarnFlavor(repoRoot, kiciDir) {
2659
+ if (await detectYarnFlavor(repoRoot) === YarnFlavor.Berry) return YarnFlavor.Berry;
2660
+ return detectYarnFlavor(kiciDir);
2661
+ }
2662
+ /**
2519
2663
  * Install `.kici/` dependencies inline with the repo's package manager.
2520
2664
  *
2521
2665
  * Falls back to this when the dep cache is unavailable or a download fails.
@@ -2534,19 +2678,28 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
2534
2678
  async function installDeps(kiciDir, opts = {}) {
2535
2679
  const repoRoot = opts.repoRoot ?? dirname(kiciDir);
2536
2680
  const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
2681
+ const yarnFlavor = packageManager === PackageManager.Yarn ? await detectKiciYarnFlavor(repoRoot, kiciDir) : YarnFlavor.Classic;
2537
2682
  logger$2.info("Installing deps inline", {
2538
2683
  packageManager,
2684
+ yarnFlavor,
2539
2685
  dir: kiciDir
2540
2686
  });
2541
- process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
2687
+ process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, flavor=${yarnFlavor}, cwd=${kiciDir}\n`);
2542
2688
  await assertResolvableDeps({
2543
2689
  kiciDir,
2544
2690
  repoRoot,
2545
- packageManager
2691
+ packageManager,
2692
+ yarnFlavor
2546
2693
  });
2547
2694
  const startTime = Date.now();
2548
2695
  const hasPrivateRegistry = (opts.npmRegistries?.length ?? 0) > 0 || (opts.installEnvSecrets ? Object.keys(opts.installEnvSecrets).length > 0 : false);
2549
- const registryConfig = await applyNpmRegistryConfig({
2696
+ const isBerry = packageManager === PackageManager.Yarn && yarnFlavor === YarnFlavor.Berry;
2697
+ const registryConfig = isBerry ? await applyYarnrcBerryConfig({
2698
+ kiciDir,
2699
+ npmRegistries: opts.npmRegistries,
2700
+ installEnvSecrets: opts.installEnvSecrets,
2701
+ jobIdShort: opts.jobIdShort ?? "00000000"
2702
+ }) : await applyNpmRegistryConfig({
2550
2703
  kiciDir,
2551
2704
  npmRegistries: opts.npmRegistries,
2552
2705
  installEnvSecrets: opts.installEnvSecrets,
@@ -2558,6 +2711,10 @@ async function installDeps(kiciDir, opts = {}) {
2558
2711
  hasPrivateRegistry,
2559
2712
  registryConfig
2560
2713
  });
2714
+ else if (isBerry) await runYarnBerryInstall({
2715
+ kiciDir,
2716
+ registryConfig
2717
+ });
2561
2718
  else if (packageManager === PackageManager.Yarn) await runYarnInstall({
2562
2719
  kiciDir,
2563
2720
  hasPrivateRegistry,
@@ -2577,7 +2734,7 @@ async function installDeps(kiciDir, opts = {}) {
2577
2734
  await registryConfig.cleanup();
2578
2735
  }
2579
2736
  if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
2580
- if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir);
2737
+ if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor);
2581
2738
  const durationMs = Date.now() - startTime;
2582
2739
  process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
2583
2740
  logger$2.info("Deps installed inline", {
@@ -2706,6 +2863,35 @@ async function runYarnInstall(args) {
2706
2863
  }).catch(() => {});
2707
2864
  }
2708
2865
  }
2866
+ /** Pure: argv for a berry `yarn install`. Cache + linker live in .yarnrc.yml. */
2867
+ function buildYarnBerryInstallArgs() {
2868
+ return ["install"];
2869
+ }
2870
+ /**
2871
+ * Run a berry `yarn install` from `.kici/`. The synthesized `.kici/.yarnrc.yml`
2872
+ * (applied by `applyYarnrcBerryConfig`) forces `nodeLinker: node-modules`, an
2873
+ * isolated `cacheFolder`, and — when a private registry is configured —
2874
+ * `enableScripts: false` + `npmScopes`/`npmRegistryServer` auth. corepack
2875
+ * provisions the repo-pinned berry version; `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`
2876
+ * makes that non-interactive. Not `--immutable` (resolved URLs in the lockfile
2877
+ * may point at a different registry than the synthesized config).
2878
+ */
2879
+ async function runYarnBerryInstall(args) {
2880
+ await assertYarnAvailable();
2881
+ const { nodeDir } = resolveNpm();
2882
+ const env = {
2883
+ ...envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir),
2884
+ COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
2885
+ };
2886
+ const argv = buildYarnBerryInstallArgs();
2887
+ process.stderr.write(`[dep-installer:trace] running: yarn ${argv.join(" ")} (berry)\n`);
2888
+ await execFileAsync("yarn", argv, {
2889
+ cwd: args.kiciDir,
2890
+ env,
2891
+ timeout: INSTALL_TIMEOUT_MS,
2892
+ maxBuffer: INSTALL_MAX_BUFFER
2893
+ });
2894
+ }
2709
2895
  /** Throw an actionable error when the repo needs yarn but it is not installed. */
2710
2896
  async function assertYarnAvailable() {
2711
2897
  try {
@@ -2725,7 +2911,7 @@ async function assertYarnAvailable() {
2725
2911
  * Deep cross-sibling build chains may build out of strict topological order —
2726
2912
  * real `.kici` closures are shallow.
2727
2913
  */
2728
- async function buildYarnWorkspaceClosure(repoRoot, kiciDir) {
2914
+ async function buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor) {
2729
2915
  const siblings = await collectInRepoSiblings(repoRoot, kiciDir, resolveYarnNodeModulesRoot(repoRoot, kiciDir));
2730
2916
  if (siblings.length === 0) return;
2731
2917
  const { nodeDir } = resolveNpm();
@@ -2733,15 +2919,16 @@ async function buildYarnWorkspaceClosure(repoRoot, kiciDir) {
2733
2919
  for (const rel of [...siblings].reverse()) {
2734
2920
  const sibDir = join(repoRoot, rel);
2735
2921
  if (!await siblingHasBuildScript(sibDir)) continue;
2736
- process.stderr.write(`[dep-installer:trace] building yarn sibling: yarn --cwd ${sibDir} run build\n`);
2922
+ const [argv, cwd] = yarnFlavor === YarnFlavor.Berry ? [["run", "build"], sibDir] : [[
2923
+ "--cwd",
2924
+ sibDir,
2925
+ "run",
2926
+ "build"
2927
+ ], repoRoot];
2928
+ process.stderr.write(`[dep-installer:trace] building yarn sibling (${yarnFlavor}): yarn ${argv.join(" ")} @ ${cwd}\n`);
2737
2929
  try {
2738
- await execFileAsync("yarn", [
2739
- "--cwd",
2740
- sibDir,
2741
- "run",
2742
- "build"
2743
- ], {
2744
- cwd: repoRoot,
2930
+ await execFileAsync("yarn", argv, {
2931
+ cwd,
2745
2932
  env,
2746
2933
  timeout: INSTALL_TIMEOUT_MS,
2747
2934
  maxBuffer: INSTALL_MAX_BUFFER
@@ -2826,8 +3013,8 @@ function logSubprocessStreams(e, tokens) {
2826
3013
  * no Rolldown step at runtime. `@kici-dev/sdk` and host-repo deps resolve via
2827
3014
  * Node's normal ESM lookup against `.kici/node_modules/`.
2828
3015
  */
2829
- const AGENT_SDK_VERSION = "0.1.17";
2830
- const AGENT_SDK_BUNDLE_HASH = "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
3016
+ const AGENT_SDK_VERSION = "0.1.18";
3017
+ const AGENT_SDK_BUNDLE_HASH = "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
2831
3018
  /**
2832
3019
  * Register the `@kici-dev/core/ts-loader-hook` oxc-transform ESM loader hook so
2833
3020
  * subsequent dynamic `import()` calls for `.ts` / `.tsx` files transform on the
@@ -2950,18 +3137,20 @@ function extractSteps(workflow, jobName) {
2950
3137
  * A sibling mismatch logs a warning; a missing target job throws a clear
2951
3138
  * determinism error.
2952
3139
  */
2953
- async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event, env, apiTransport, expectedJobNames) {
3140
+ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event, env, apiTransport, expectedJobNames, upstreamSnapshot, declaredNeeds) {
2954
3141
  const dynamicFn = extractDynamicJobFn(workflow, dynamicIndex);
2955
3142
  const { $ } = await import("zx");
2956
3143
  const { createLogger } = await import("@kici-dev/shared");
2957
- const { buildKiciApi } = await import("@kici-dev/sdk");
3144
+ const { buildKiciApi, buildNeedsContext } = await import("@kici-dev/sdk");
2958
3145
  const log = createLogger({ prefix: `dynamic-job-fn:${workflow.name}` });
2959
3146
  const kici = buildKiciApi(apiTransport ?? (() => Promise.reject(/* @__PURE__ */ new Error("Agent API not available during re-evaluation"))));
3147
+ const needs = upstreamSnapshot ? buildNeedsContext(upstreamSnapshot, declaredNeeds ?? []) : void 0;
2960
3148
  const generatedJobs = await dynamicFn({
2961
3149
  $,
2962
3150
  ctx: {
2963
3151
  workflow: { name: workflow.name },
2964
- event
3152
+ event,
3153
+ ...needs && { needs }
2965
3154
  },
2966
3155
  log,
2967
3156
  env,
@@ -3187,7 +3376,7 @@ async function applyOverlay(config) {
3187
3376
  */
3188
3377
  init_download();
3189
3378
  init_dep_restore();
3190
- const AGENT_VERSION = "0.1.17";
3379
+ const AGENT_VERSION = "0.1.18";
3191
3380
  process.on("uncaughtException", (err) => {
3192
3381
  process.stderr.write(`[workflow-runner] UNCAUGHT EXCEPTION: ${err.message}\n`);
3193
3382
  if (err.stack) process.stderr.write(`[workflow-runner] Stack: ${err.stack}\n`);
@@ -4034,7 +4223,9 @@ function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedS
4034
4223
  attestProvenance: buildAttestProvenanceFn(request, workDir, (o) => kici.oidc.token(o)),
4035
4224
  ...rawPayload && { rawPayload },
4036
4225
  ...request.provider && { provider: request.provider },
4037
- ...request.matrixValues && { matrix: request.matrixValues }
4226
+ ...request.matrixValues && { matrix: request.matrixValues },
4227
+ ...request.host && { host: request.host },
4228
+ ...request.agent && { agent: request.agent }
4038
4229
  };
4039
4230
  }
4040
4231
  /** Raw provider webhook body for ctx.rawPayload — nested in the envelope. */
@@ -4585,7 +4776,7 @@ async function extractAndNormalizeSteps(workflow, request, apiTransport) {
4585
4776
  let rawSteps;
4586
4777
  let driftDroppedJobs = [];
4587
4778
  if (request.dynamicSource) {
4588
- const dynamicResult = await extractStepsFromDynamicJob(workflow, request.dynamicSource.index, request.jobName, request.dynamicSource.event, process.env, apiTransport, request.dynamicSource.expectedJobNames);
4779
+ const dynamicResult = await extractStepsFromDynamicJob(workflow, request.dynamicSource.index, request.jobName, request.dynamicSource.event, process.env, apiTransport, request.dynamicSource.expectedJobNames, request.dynamicSource.upstreamSnapshot, request.dynamicSource.declaredNeeds);
4589
4780
  rawSteps = dynamicResult.steps;
4590
4781
  driftDroppedJobs = dynamicResult.droppedJobs;
4591
4782
  if (driftDroppedJobs.length > 0) trace(`Determinism drift: ${driftDroppedJobs.length} job(s) dropped: ${driftDroppedJobs.join(", ")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kici-dev/agent",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
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",
@@ -53,20 +53,21 @@
53
53
  }
54
54
  },
55
55
  "dependencies": {
56
- "@hono/node-server": "^2.0.0",
57
- "archiver": "^7.0.1",
56
+ "@hono/node-server": "^2.0.4",
57
+ "archiver": "^8.0.0",
58
58
  "dockerode": "^5.0.0",
59
- "hono": "^4.12.18",
59
+ "hono": "^4.12.25",
60
60
  "jose": "^6.1.0",
61
- "tar": "^7.5.13",
61
+ "tar": "^7.5.16",
62
62
  "winston": "^3.19.0",
63
- "ws": "^8.20.0",
64
- "zod": "^4.3.6",
63
+ "ws": "^8.21.0",
64
+ "yaml": "^2.9.0",
65
+ "zod": "^4.4.3",
65
66
  "zx": "^8.8.5",
66
- "@kici-dev/core": "0.1.17",
67
- "@kici-dev/sdk": "0.1.17",
68
- "@kici-dev/shared": "0.1.17",
69
- "@kici-dev/engine": "0.1.17"
67
+ "@kici-dev/core": "0.1.18",
68
+ "@kici-dev/sdk": "0.1.18",
69
+ "@kici-dev/engine": "0.1.18",
70
+ "@kici-dev/shared": "0.1.18"
70
71
  },
71
72
  "kici": {
72
73
  "metrics": {
@@ -75,7 +76,7 @@
75
76
  }
76
77
  },
77
78
  "devDependencies": {
78
- "@types/archiver": "^7.0.0",
79
+ "@types/archiver": "^8.0.0",
79
80
  "@types/dockerode": "^4.0.1"
80
81
  },
81
82
  "scripts": {