@kici-dev/compiler 0.1.19 → 0.1.21

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/cli.d.ts CHANGED
@@ -8,4 +8,18 @@ import { Command } from 'commander';
8
8
  export declare function buildProgram(): Command;
9
9
  /** Build the program and parse argv — the bin-shim entry point. */
10
10
  export declare function runCli(argv?: string[]): void;
11
+ /**
12
+ * Decide whether this module is the process entry point, tolerating a
13
+ * symlinked `argv[1]`. A `node_modules/.bin/kici` entry is a symlink, and when
14
+ * it points at this compiled `cli.js` (the compiler package declares a `kici`
15
+ * bin), `process.argv[1]` is the symlink path while `import.meta.url` is the
16
+ * real file. A plain `resolve()` comparison sees two different paths and never
17
+ * matches, silently skipping `runCli()` — so `kici compile` (and every other
18
+ * subcommand) becomes a no-op when invoked through the bin symlink.
19
+ * Dereference both sides with `realpathSync` so a symlinked invocation is
20
+ * correctly recognised as the entry point. Falls back to a plain `resolve()`
21
+ * comparison when `argv[1]` doesn't resolve to a real file (e.g. a virtual
22
+ * entry point), preserving the previous behaviour for that edge case.
23
+ */
24
+ export declare function isMainEntryPoint(argv1: string | undefined, importMetaUrl: string): boolean;
11
25
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js CHANGED
@@ -3,10 +3,11 @@ import "./chunk-BTugEXQM.js";
3
3
  import { shouldSuppressBanner } from "./cli-banner.js";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { resolve } from "node:path";
6
+ import { realpathSync } from "node:fs";
6
7
  import { Argument, Command, Option } from "commander";
7
8
  import pc from "picocolors";
8
9
  //#region src/cli.ts
9
- const version = "0.1.19";
10
+ const version = "0.1.21";
10
11
  /**
11
12
  * Build the kici Commander program with every command registered. Exported so
12
13
  * the surface registry can walk the real command tree without parsing argv (no
@@ -43,7 +44,7 @@ function buildProgram() {
43
44
  await fixtureCommand(event, options);
44
45
  });
45
46
  const runCommand = program.command("run").description("Execute workflows locally or remotely");
46
- runCommand.command("local").argument("[event]", "Event type (e.g., push, pr:open, schedule) — optional with --pick").description("Execute workflows locally without orchestrator infrastructure").option("-p, --pick", "Interactively pick a workflow and trigger to simulate", false).option("--workflow <name>", "Run only the specified workflow").option("--job <name>", "Run only the specified job (and its dependencies)").option("--branch <name>", "Override detected git branch").option("--sha <hash>", "Override detected git SHA").option("--payload <path>", "Path to explicit event payload JSON file").option("--concurrency <n>", "Max parallel jobs (default: CPU cores)", parseInt).option("--keep-going", "Continue after job failure", false).option("--container", "Use Podman container isolation", false).option("--env <KEY=VALUE>", "Environment variable override (repeatable)", (val, prev) => [...prev, val], []).option("--quiet", "Suppress streaming output", false).option("--json", "Output structured JSON result", false).option("--junit <path>", "Output JUnit XML result").option("--files <path>", "Override changed file paths (repeatable, default: git diff)", (val, prev) => [...prev, val], []).option("--debug", "Verbose internals", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--in-place", "Run against the real working directory instead of an isolated tmp checkout", false).option("--keep", "Always retain the isolated tmp checkout (default: keep only on failure)", false).action(async (event, options) => {
47
+ runCommand.command("local").argument("[event]", "Event type (e.g., push, pr:open, schedule) — optional with --pick").description("Execute workflows locally without orchestrator infrastructure").option("-p, --pick", "Interactively pick a workflow and trigger to simulate", false).option("--workflow <name>", "Run only the specified workflow").option("--job <name>", "Run only the specified job (and its dependencies)").option("--branch <name>", "Override detected git branch").option("--sha <hash>", "Override detected git SHA").option("--payload <path>", "Path to explicit event payload JSON file").option("--concurrency <n>", "Max parallel jobs (default: CPU cores)", parseInt).option("--keep-going", "Continue after job failure", false).option("--container", "Use Podman container isolation", false).option("--env <KEY=VALUE>", "Environment variable override (repeatable)", (val, prev) => [...prev, val], []).option("--quiet", "Suppress streaming output", false).option("--json", "Output structured JSON result", false).option("--junit <path>", "Output JUnit XML result").option("--files <path>", "Override changed file paths (repeatable, default: git diff)", (val, prev) => [...prev, val], []).option("--debug", "Verbose internals", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--in-place", "Run against the real working directory instead of an isolated tmp checkout", false).option("--keep", "Always retain the isolated tmp checkout (default: keep only on failure)", false).option("--check", "Run in check mode: report drift, change nothing", false).option("--fail-on-drift", "In check mode, exit non-zero if any step reports drift", false).action(async (event, options) => {
47
48
  if (options.pick && options.workflow) {
48
49
  console.error("Error: --pick is mutually exclusive with --workflow.");
49
50
  process.exit(2);
@@ -53,8 +54,20 @@ function buildProgram() {
53
54
  process.exit(2);
54
55
  }
55
56
  const { runLocalCommand } = await import("./commands/index.js");
57
+ const { resolveCheckMode } = await import("./commands/check-mode.js");
58
+ let checkMode;
59
+ try {
60
+ checkMode = resolveCheckMode({
61
+ check: options.check,
62
+ failOnDrift: options.failOnDrift
63
+ });
64
+ } catch (err) {
65
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
66
+ process.exit(2);
67
+ }
56
68
  const success = await runLocalCommand({
57
69
  event,
70
+ checkMode,
58
71
  pick: options.pick,
59
72
  workflow: options.workflow,
60
73
  job: options.job,
@@ -76,10 +89,22 @@ function buildProgram() {
76
89
  });
77
90
  process.exit(success ? 0 : 1);
78
91
  });
79
- runCommand.command("remote").argument("[fixture]", "Fixture name or glob pattern (omit to list available)").description("Execute fixtures remotely via orchestrator").option("--workflow <name>", "Run a specific workflow directly (bypass triggers)").option("--all", "Run all available fixtures", false).option("--parallel", "Run matching fixtures concurrently", false).option("--no-wait", "Fire and forget (print runIds, don't stream)").option("--quiet", "Suppress output except final result", false).option("--json", "Output structured JSON result", false).option("--junit <path>", "Output JUnit XML result").option("--history", "Show recent run history", false).option("--routing-key <key>", "Override routing key for this run").option("--org <id>", "Target organization (overrides the active org)").option("--orchestrator <name>", "Target orchestrator cluster (overrides the per-org default)").option("--debug", "Verbose internals", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--context <ctx.key=value>", "Inject a namespaced context secret, uploaded encrypted to the orchestrator (repeatable)", (val, prev) => [...prev, val], []).option("--env <KEY=VALUE>", "Provide a per-run secret (repeatable); uploaded encrypted to the orchestrator", (val, prev) => [...prev, val], []).action(async (fixture, options) => {
92
+ runCommand.command("remote").argument("[fixture]", "Fixture name or glob pattern (omit to list available)").description("Execute fixtures remotely via orchestrator").option("--workflow <name>", "Run a specific workflow directly (bypass triggers)").option("--all", "Run all available fixtures", false).option("--parallel", "Run matching fixtures concurrently", false).option("--no-wait", "Fire and forget (print runIds, don't stream)").option("--quiet", "Suppress output except final result", false).option("--json", "Output structured JSON result", false).option("--junit <path>", "Output JUnit XML result").option("--history", "Show recent run history", false).option("--routing-key <key>", "Override routing key for this run").option("--org <id>", "Target organization (overrides the active org)").option("--orchestrator <name>", "Target orchestrator cluster (overrides the per-org default)").option("--debug", "Verbose internals", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--context <ctx.key=value>", "Inject a namespaced context secret, uploaded encrypted to the orchestrator (repeatable)", (val, prev) => [...prev, val], []).option("--env <KEY=VALUE>", "Provide a per-run secret (repeatable); uploaded encrypted to the orchestrator", (val, prev) => [...prev, val], []).option("--check", "Run in check mode: report drift, change nothing", false).option("--fail-on-drift", "In check mode, exit non-zero if any step reports drift", false).action(async (fixture, options) => {
80
93
  const { runRemoteCommand } = await import("./commands/index.js");
94
+ const { resolveCheckMode } = await import("./commands/check-mode.js");
95
+ let checkMode;
96
+ try {
97
+ checkMode = resolveCheckMode({
98
+ check: options.check,
99
+ failOnDrift: options.failOnDrift
100
+ });
101
+ } catch (err) {
102
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
103
+ process.exit(2);
104
+ }
81
105
  const success = await runRemoteCommand(fixture, {
82
106
  ...options,
107
+ checkMode,
83
108
  envFlags: options.env
84
109
  });
85
110
  process.exit(success ? 0 : 1);
@@ -263,8 +288,30 @@ Environment variables:
263
288
  function runCli(argv = process.argv) {
264
289
  buildProgram().parse(argv);
265
290
  }
266
- if (resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) runCli();
291
+ /**
292
+ * Decide whether this module is the process entry point, tolerating a
293
+ * symlinked `argv[1]`. A `node_modules/.bin/kici` entry is a symlink, and when
294
+ * it points at this compiled `cli.js` (the compiler package declares a `kici`
295
+ * bin), `process.argv[1]` is the symlink path while `import.meta.url` is the
296
+ * real file. A plain `resolve()` comparison sees two different paths and never
297
+ * matches, silently skipping `runCli()` — so `kici compile` (and every other
298
+ * subcommand) becomes a no-op when invoked through the bin symlink.
299
+ * Dereference both sides with `realpathSync` so a symlinked invocation is
300
+ * correctly recognised as the entry point. Falls back to a plain `resolve()`
301
+ * comparison when `argv[1]` doesn't resolve to a real file (e.g. a virtual
302
+ * entry point), preserving the previous behaviour for that edge case.
303
+ */
304
+ function isMainEntryPoint(argv1, importMetaUrl) {
305
+ if (!argv1) return false;
306
+ const modulePath = fileURLToPath(importMetaUrl);
307
+ try {
308
+ return realpathSync(argv1) === realpathSync(modulePath);
309
+ } catch {
310
+ return resolve(argv1) === resolve(modulePath);
311
+ }
312
+ }
313
+ if (isMainEntryPoint(process.argv[1], import.meta.url)) runCli();
267
314
  //#endregion
268
- export { buildProgram, runCli };
315
+ export { buildProgram, isMainEntryPoint, runCli };
269
316
 
270
317
  //# sourceMappingURL=cli.js.map
@@ -0,0 +1,19 @@
1
+ import { CheckMode } from '@kici-dev/engine';
2
+ /** The two run-mode flags shared by `kici run local` and `kici run remote`. */
3
+ export interface CheckModeFlags {
4
+ /** --check: report drift, change nothing. */
5
+ check?: boolean;
6
+ /** --fail-on-drift: in check mode, exit non-zero if any step reports drift. */
7
+ failOnDrift?: boolean;
8
+ }
9
+ /**
10
+ * Resolve the run {@link CheckMode} from the `--check` / `--fail-on-drift` flags.
11
+ *
12
+ * - no flags -> `apply` (the unchanged default: converge).
13
+ * - `--check` -> `check` (report-only, changes nothing).
14
+ * - `--check --fail-on-drift` -> `check-fail-on-drift` (fails the run on drift).
15
+ *
16
+ * `--fail-on-drift` without `--check` is an error — it only modifies check mode.
17
+ */
18
+ export declare function resolveCheckMode(flags: CheckModeFlags): CheckMode;
19
+ //# sourceMappingURL=check-mode.d.ts.map
@@ -0,0 +1,21 @@
1
+ import "../chunk-BTugEXQM.js";
2
+ import { CheckMode } from "@kici-dev/engine";
3
+ //#region src/commands/check-mode.ts
4
+ /**
5
+ * Resolve the run {@link CheckMode} from the `--check` / `--fail-on-drift` flags.
6
+ *
7
+ * - no flags -> `apply` (the unchanged default: converge).
8
+ * - `--check` -> `check` (report-only, changes nothing).
9
+ * - `--check --fail-on-drift` -> `check-fail-on-drift` (fails the run on drift).
10
+ *
11
+ * `--fail-on-drift` without `--check` is an error — it only modifies check mode.
12
+ */
13
+ function resolveCheckMode(flags) {
14
+ if (flags.failOnDrift && !flags.check) throw new Error("--fail-on-drift requires --check");
15
+ if (flags.check) return flags.failOnDrift ? CheckMode.enum["check-fail-on-drift"] : CheckMode.enum.check;
16
+ return CheckMode.enum.apply;
17
+ }
18
+ //#endregion
19
+ export { resolveCheckMode };
20
+
21
+ //# sourceMappingURL=check-mode.js.map
@@ -8,8 +8,8 @@ import "../validation/index.js";
8
8
  import { computeLockfileHash, detectGitRoot, generateLockFile, serializeLockFile } from "../lockfile/generator.js";
9
9
  import "../lockfile/index.js";
10
10
  import path from "node:path";
11
- import pc from "picocolors";
12
11
  import { existsSync } from "node:fs";
12
+ import pc from "picocolors";
13
13
  import fs from "node:fs/promises";
14
14
  import { logger, toErrorMessage } from "@kici-dev/core";
15
15
  import { PackageManager, detectPackageManagerSync } from "@kici-dev/core/package-manager";
@@ -294,7 +294,8 @@ async function runSingleFixture(fixture, ctx, options, config, history) {
294
294
  },
295
295
  workflowName: opts.workflowName,
296
296
  inlineLockFile: overlay.inlineLockFile,
297
- fullRepo: true
297
+ fullRepo: true,
298
+ ...options.checkMode && { checkMode: options.checkMode }
298
299
  });
299
300
  if (triggerResult.status === "rejected") {
300
301
  if (!options.quiet) logger.info(pc.red(`Rejected: ${triggerResult.reason ?? "unknown reason"}`));
@@ -481,7 +482,8 @@ async function runDirectWorkflow(workflowName, options) {
481
482
  },
482
483
  workflowName,
483
484
  inlineLockFile: overlay.inlineLockFile,
484
- fullRepo: true
485
+ fullRepo: true,
486
+ ...options.checkMode && { checkMode: options.checkMode }
485
487
  });
486
488
  if (!options.quiet) logger.info(pc.green(`Run started: ${triggerResult.runId}`));
487
489
  if (options.wait === false) return triggerResult.status === "accepted";
@@ -1,4 +1,5 @@
1
1
  import { type PayloadOptions } from '../test-runner/payload-builder.js';
2
+ import { type CheckMode } from '@kici-dev/engine';
2
3
  /** Options for the kici test command (dry-run trigger preview) */
3
4
  export interface TestOptions extends PayloadOptions {
4
5
  /** Filter to specific workflow */
@@ -38,6 +39,12 @@ export interface RemoteRunOptions extends TestOptions {
38
39
  org?: string;
39
40
  /** Target orchestrator cluster name (overrides the per-org default). */
40
41
  orchestrator?: string;
42
+ /**
43
+ * Run mode resolved from --check / --fail-on-drift, threaded onto the dispatch
44
+ * payload so the orchestrator runs the agent step loop in the requested mode.
45
+ * Defaults to `apply`.
46
+ */
47
+ checkMode?: CheckMode;
41
48
  }
42
49
  /** Result of a single remote fixture run */
43
50
  export interface RemoteRunResult {
@@ -2108,6 +2108,28 @@ const build = step('build', {
2108
2108
 
2109
2109
  **StepRunFn type:** `(ctx: StepContext) => Promise<void>`
2110
2110
 
2111
+ **With a check facet (idempotent step):**
2112
+
2113
+ Add a `check` function to describe _desired state_ instead of a fixed action. When
2114
+ `check` is present, `run` becomes the _apply_ function and receives the drift value
2115
+ `check` returned; `summarize` (required) renders that drift for logs and the
2116
+ dashboard; `whenInSync` optionally produces the step's outputs when already in sync.
2117
+
2118
+ ```typescript
2119
+ const configureNginx = step('configure-nginx', {
2120
+ check: async (ctx) => ((await inSync(ctx)) ? null : { want: DESIRED }),
2121
+ summarize: (drift) => `would rewrite nginx.conf (${drift.want.length} bytes)`,
2122
+ run: async (ctx, drift) => {
2123
+ await writeConfig(drift.want);
2124
+ return { reloaded: true };
2125
+ },
2126
+ whenInSync: async () => ({ reloaded: false }),
2127
+ });
2128
+ ```
2129
+
2130
+ A checked step can run in apply mode (converge) or `--check` preview mode (report
2131
+ drift, change nothing). See [Idempotent steps and check mode](../idempotent-steps.md).
2132
+
2111
2133
  ### Per-job resources
2112
2134
 
2113
2135
  `options.resources` declares the CPU and memory the job needs. The orchestrator's auto-scaler uses these numbers to:
@@ -4021,6 +4043,70 @@ const publish = job('publish', {
4021
4043
  - The step never holds platform credentials — the request is relayed through the orchestrator, which mints the token on the step's behalf.
4022
4044
  - Only available inside a running job step; calling it outside one (for example, during local execution) rejects with a clear error.
4023
4045
 
4046
+ ### ctx.kici.inventory.query(selector?) / .get(agentId)
4047
+
4048
+ Query the **host inventory** — the roster of agents in the caller's orchestrator cluster — from inside a workflow. Each host is a `HostInventoryEntry`:
4049
+
4050
+ ```typescript
4051
+ interface HostInventoryEntry {
4052
+ agentId: string;
4053
+ labels: string[]; // flat-string grouping/tags dimension
4054
+ properties: Record<string, string | number | boolean>; // typed host-vars dimension
4055
+ hostname: string | null;
4056
+ platform: string | null;
4057
+ arch: string | null;
4058
+ lifecycleClass: 'static' | 'ephemeral';
4059
+ status: 'ready' | 'unreachable' | 'stale';
4060
+ lastSeen: string; // ISO timestamp
4061
+ }
4062
+ ```
4063
+
4064
+ Two dimensions describe a host. **Labels** are flat strings used for grouping and targeting (the same labels `runsOn` / `runsOnAll` match). **Properties** are typed host-vars (`string | number | boolean`) — the place for facts like `region`, `cores`, or `gpu`. A host reports its own properties via the agent's `KICI_PROPERTIES` config, and an operator can pre-declare them with `kici-admin host declare --prop key=value`; the two are shallow-merged (agent-reported keys win).
4065
+
4066
+ ```typescript
4067
+ // All hosts:
4068
+ const all = await ctx.kici.inventory.query();
4069
+
4070
+ // Server-side label filter (OR-of-AND include groups, plus exclude):
4071
+ const dbHosts = await ctx.kici.inventory.query({
4072
+ include: [[{ kind: 'exact', value: 'role:db' }]],
4073
+ });
4074
+
4075
+ // Property filtering is client-side — plain JS in the workflow:
4076
+ const euDbHosts = dbHosts.filter((h) => h.properties.region === 'eu');
4077
+
4078
+ // One host by id:
4079
+ const host = await ctx.kici.inventory.get('box-1'); // HostInventoryEntry | null
4080
+ ```
4081
+
4082
+ **The label selector is applied server-side** (reusing the same glob/regex matchers as `runsOnAll`). **Property filtering is client-side** — you filter the returned array in plain JavaScript, so there is no query DSL to learn.
4083
+
4084
+ **Headline use — dynamic-job fan-out.** A dynamic-job generator can query the inventory and return one job per matching host, fanning a workflow out across a fleet:
4085
+
4086
+ ```typescript
4087
+ const migrate = job('migrate', async (ctx) => {
4088
+ const hosts = await ctx.kici.inventory.query({
4089
+ include: [[{ kind: 'exact', value: 'role:db' }]],
4090
+ });
4091
+ return hosts
4092
+ .filter((h) => h.properties.region === 'eu')
4093
+ .map((h) =>
4094
+ job(`migrate-${h.agentId}`, {
4095
+ runsOn: [h.agentId],
4096
+ run: async (c) => {
4097
+ await c.$`./migrate.sh`;
4098
+ },
4099
+ }),
4100
+ );
4101
+ });
4102
+ ```
4103
+
4104
+ A `runsOn` of a single host's `agentId` (as in `runsOn: [h.agentId]` above) **pins the job to that host**: the orchestrator routes it to that agent only, and queues it with the pin if the host is momentarily offline — the same host-pin path `runsOnAll` uses. A `runsOn` with multiple labels or a glob/regex pattern stays ordinary label routing.
4105
+
4106
+ `ctx.kici.inventory` is available to **both** steps and dynamic-job generators (unlike `ctx.kici.oidc.token`, which is job-bound — the inventory is cluster-scoped, not job-bound).
4107
+
4108
+ **Determinism caveat.** The inventory is **live**: it can change between when a dynamic-job generator first runs (at dispatch) and when it re-evaluates (at agent time). Generating jobs from `inventory.query()` therefore inherits the same non-determinism contract as `infrastructure.list()` — KiCI warns when the re-evaluated job set drifts (a sibling job name changed) and hard-errors when a targeted job vanishes. Prefer stable inputs where you can, and treat a fanned-out job set as a snapshot of the roster at generation time.
4109
+
4024
4110
  ### ctx.attestProvenance({ subject })
4025
4111
 
4026
4112
  Build, sign, and persist a build-provenance attestation for an artifact your step produced. KiCI assembles an in-toto SLSA v1.0 provenance statement whose build identity (`repository`, `ref`, `sha`, run/job ids) comes from the platform — not from the step — so it cannot be spoofed, signs it, and stores a verifiable bundle that the dashboard surfaces and the `kici verify-attestation` CLI checks.
@@ -5582,7 +5668,7 @@ kici run remote push-main --org xyz789ghi012
5582
5668
  kici run remote push-main --orchestrator us-east
5583
5669
 
5584
5670
  # Run all push-related fixtures
5585
- kici run remote push-*
5671
+ kici run remote 'push-*'
5586
5672
 
5587
5673
  # Run everything
5588
5674
  kici run remote --all
@@ -5753,7 +5839,8 @@ kici login [options]
5753
5839
  | --------------------------- | ------- | ---------------------------------------------- |
5754
5840
  | `--token <key>` | none | API key for direct authentication (legacy) |
5755
5841
  | `--device` | false | Force device authorization flow (headless/SSH) |
5756
- | `--platform-endpoint <url>` | none | Platform API base URL |
5842
+ | `--platform-endpoint <url>` | none | Platform relay URL |
5843
+ | `--routing-key <key>` | none | Routing key for webhook source identification |
5757
5844
 
5758
5845
  **Environment variables:**
5759
5846
 
@@ -6042,11 +6129,10 @@ kici approve <run-id> [options]
6042
6129
 
6043
6130
  **Options:**
6044
6131
 
6045
- | Option | Default | Description |
6046
- | ----------------- | ------- | --------------------------------------------------- |
6047
- | `--job <name>` | none | Approve a held job (omit for a workflow-level hold) |
6048
- | `--step <name>` | none | Approve a held step (used with `--job`) |
6049
- | `--reason <text>` | none | Optional note recorded with the approval |
6132
+ | Option | Default | Description |
6133
+ | ---------------- | ------- | ---------------------------------------------------- |
6134
+ | `--job <name>` | none | Approve a held job (omit for a workflow-level hold) |
6135
+ | `--step <index>` | none | Approve a held step by its index (used with `--job`) |
6050
6136
 
6051
6137
  **Examples:**
6052
6138
 
@@ -6057,8 +6143,8 @@ kici approve abc123
6057
6143
  # Approve a held job
6058
6144
  kici approve abc123 --job deploy-production
6059
6145
 
6060
- # Approve a held step
6061
- kici approve abc123 --job migrate-and-deploy --step apply-migration
6146
+ # Approve a held step (steps are addressed by index)
6147
+ kici approve abc123 --job migrate-and-deploy --step 1
6062
6148
  ```
6063
6149
 
6064
6150
  You must be eligible for at least one unsatisfied clause (a member of a named team, or a named user) and hold the `environments:write` or `ci_trust:write` permission. The command reports whether the element was released, how many clauses remain, or that it was rejected.
@@ -6079,11 +6165,11 @@ kici reject <run-id> --reason <text> [options]
6079
6165
 
6080
6166
  **Options:**
6081
6167
 
6082
- | Option | Default | Description |
6083
- | ----------------- | ------- | -------------------------------------------------- |
6084
- | `--reason <text>` | none | Required. Reason recorded with the rejection |
6085
- | `--job <name>` | none | Reject a held job (omit for a workflow-level hold) |
6086
- | `--step <name>` | none | Reject a held step (used with `--job`) |
6168
+ | Option | Default | Description |
6169
+ | ----------------- | ------- | --------------------------------------------------- |
6170
+ | `--reason <text>` | none | Required. Reason recorded with the rejection |
6171
+ | `--job <name>` | none | Reject a held job (omit for a workflow-level hold) |
6172
+ | `--step <index>` | none | Reject a held step by its index (used with `--job`) |
6087
6173
 
6088
6174
  **Examples:**
6089
6175
 
@@ -6097,15 +6183,9 @@ kici reject abc123 --job deploy-production --reason "Wrong release branch"
6097
6183
  List secret contexts available for test runs. Shows context names and key names (not values).
6098
6184
 
6099
6185
  ```bash
6100
- kici secrets list [options]
6186
+ kici secrets list
6101
6187
  ```
6102
6188
 
6103
- **Options:**
6104
-
6105
- | Option | Default | Description |
6106
- | ------------------ | ------- | ------------------------- |
6107
- | `--endpoint <url>` | none | Orchestrator URL override |
6108
-
6109
6189
  Each "context" corresponds to an environment configured on the orchestrator. The output lists every environment whose `allowLocalExecution` flag is `true` (the gate that lets CLI-initiated test runs resolve secrets through that environment), along with the secret key names reachable from the environment's bound scopes.
6110
6190
 
6111
6191
  Only key names are shown — secret values are never returned over this endpoint.
@@ -6122,10 +6202,9 @@ kici types [options]
6122
6202
 
6123
6203
  **Options:**
6124
6204
 
6125
- | Option | Default | Description |
6126
- | ------------------- | ------- | ------------------------- |
6127
- | `--kici-dir <path>` | `.kici` | Path to .kici directory |
6128
- | `--endpoint <url>` | none | Orchestrator URL override |
6205
+ | Option | Default | Description |
6206
+ | ------------------- | ------- | ----------------------- |
6207
+ | `--kici-dir <path>` | `.kici` | Path to .kici directory |
6129
6208
 
6130
6209
  **Prerequisites:** Must be authenticated via `kici login`.
6131
6210
 
@@ -6139,9 +6218,6 @@ kici types
6139
6218
 
6140
6219
  # Use custom .kici directory
6141
6220
  kici types --kici-dir packages/app/.kici
6142
-
6143
- # Override orchestrator endpoint
6144
- kici types --endpoint https://my-orchestrator.example.com
6145
6221
  ```
6146
6222
 
6147
6223
  **How it works:**
@@ -6823,7 +6899,7 @@ The lock file (`kici.lock.json`) is a JSON file with the following top-level fie
6823
6899
 
6824
6900
  | Field | Description |
6825
6901
  | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
6826
- | `schemaVersion` | Lock file schema version (currently 17). Incremented on breaking format changes. |
6902
+ | `schemaVersion` | Lock file schema version (currently 20). Incremented on breaking format changes. |
6827
6903
  | `source` | Reference to the source file and export (e.g., `{ file: “.kici/workflows/ci.ts”, export: “#default” }`). |
6828
6904
  | `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
6829
6905
  | `lockfileHash` | SHA-256 of the detected package manager's lockfile, used as the dependency cache key. The lockfile is `.kici/package-lock.json` for npm, or the repo-root `pnpm-lock.yaml` / `yarn.lock` for a pnpm/yarn workspace; the hash input is prefixed with the manager name so a manager change is a guaranteed cache miss. Omitted when no lockfile exists. |
@@ -6998,12 +7074,14 @@ kici run remote
6998
7074
  kici run remote push-main
6999
7075
 
7000
7076
  # Run all fixtures matching a glob
7001
- kici run remote push-*
7077
+ kici run remote 'push-*'
7002
7078
 
7003
7079
  # Run everything
7004
7080
  kici run remote --all
7005
7081
  ```
7006
7082
 
7083
+ The single quotes keep your shell from expanding `push-*` against local files, so the pattern reaches KiCI intact for its own fixture-glob matching.
7084
+
7007
7085
  ## Fixture reference
7008
7086
 
7009
7087
  ### Event types
@@ -7099,7 +7177,7 @@ kici run remote
7099
7177
  kici run remote push-main
7100
7178
 
7101
7179
  # Glob matching -- run all push-related fixtures
7102
- kici run remote push-*
7180
+ kici run remote 'push-*'
7103
7181
 
7104
7182
  # Run all fixtures sequentially
7105
7183
  kici run remote --all
@@ -8843,6 +8921,135 @@ Non-push triggers work too — `pr()`, `tag()`, `comment()`, `release()`, `workf
8843
8921
 
8844
8922
  ---
8845
8923
 
8924
+ ## Idempotent steps and check mode
8925
+
8926
+ Source: https://docs.kici.dev/user/idempotent-steps/
8927
+
8928
+ An **idempotent step** describes _desired state_ rather than a fixed sequence of
8929
+ commands. You give the step a `check` function that inspects the world and a
8930
+ `run` function that converges it. KiCI then executes the workflow in one of two
8931
+ modes:
8932
+
8933
+ - **Apply mode** (the default): for each step, `check()` runs first; on drift the
8934
+ step applies the change; when already in sync the step is skipped.
8935
+ - **Check mode** (`--check`): for each step, `check()` runs and KiCI reports what
8936
+ _would_ change — **without changing anything**. This is the same model as a
8937
+ dry-run plan: you see the drift before any side effect happens.
8938
+
8939
+ This turns a workflow into convergent configuration management: re-running an
8940
+ apply is safe (in-sync steps do nothing), and a check-mode run is a read-only
8941
+ preview you can gate a build on.
8942
+
8943
+ ## Authoring a checked step
8944
+
8945
+ Add a `check` facet to the existing `step()` factory. When `check` is present,
8946
+ `run` becomes the _apply_ function and receives the drift value `check`
8947
+ returned:
8948
+
8949
+ ```typescript
8950
+ import { step, z } from '@kici-dev/sdk';
8951
+
8952
+ const configureNginx = step('configure-nginx', {
8953
+ // optional schema for the drift value — gives the dashboard a typed shape
8954
+ drift: z.object({ want: z.string() }),
8955
+
8956
+ // read-only inspection; return null when already in the desired state
8957
+ check: async (ctx) => {
8958
+ const current = await ctx.$`nginx -T`;
8959
+ return current.stdout.includes(DESIRED) ? null : { want: DESIRED };
8960
+ },
8961
+
8962
+ // human-readable preview line — REQUIRED when check is set. It is the drift's
8963
+ // serializable face: it streams to the logs and persists for the dashboard.
8964
+ summarize: (drift) => `would rewrite nginx.conf (${drift.want.length} bytes)`,
8965
+
8966
+ // apply — runs only when check returned drift (apply mode); receives that drift
8967
+ run: async (ctx, drift) => {
8968
+ await writeConfig(drift.want);
8969
+ return { reloaded: true };
8970
+ },
8971
+
8972
+ // optional — runs when check returned null, to produce the step's outputs
8973
+ whenInSync: async () => ({ reloaded: false }),
8974
+ });
8975
+ ```
8976
+
8977
+ ### The facet fields
8978
+
8979
+ | Field | Required | Purpose |
8980
+ | ------------ | ---------------- | ---------------------------------------------------------------------- |
8981
+ | `check` | to opt in | Read-only inspection. Return a drift value, or `null` when in sync. |
8982
+ | `summarize` | when `check` set | Human-readable, serializable preview of the drift. Streams + persists. |
8983
+ | `run` | always | Apply function. With `check`, it receives the drift as its second arg. |
8984
+ | `whenInSync` | optional | Produces the step's outputs when `check` returned `null`. |
8985
+ | `drift` | optional | Schema that validates / shapes the drift value. |
8986
+
8987
+ `summarize` is **required** whenever `check` is declared. `run` and `whenInSync`
8988
+ both produce the same output type — one output shape per step, whichever path
8989
+ runs. Every other step facet (`cache`, `rules`, `continueOnError`, `timeout`,
8990
+ `requireApproval`, `onCancel`, `cleanup`, `outputs`) composes unchanged.
8991
+
8992
+ A plain `step()` without `check` keeps its exact current behavior — the check
8993
+ facet is fully optional.
8994
+
8995
+ ## Run modes
8996
+
8997
+ A run carries one of three modes:
8998
+
8999
+ | Mode | CLI flags | Behavior |
9000
+ | --------------------- | ------------------------- | ------------------------------------------------------------------------------------------ |
9001
+ | `apply` | (default, no flags) | Converge: drift ⇒ apply ⇒ **applied**; null ⇒ **in sync** (skipped). |
9002
+ | `check` | `--check` | Preview only: drift ⇒ **would change**; null ⇒ **in sync**. Never applies. Always exits 0. |
9003
+ | `check-fail-on-drift` | `--check --fail-on-drift` | Same as check, but the run **fails** if any step reports drift. |
9004
+
9005
+ Per-step outcomes:
9006
+
9007
+ - **applied** — drift was found and the step applied the change (apply mode).
9008
+ - **in sync** — `check` returned `null`; nothing to do.
9009
+ - **would change** — drift was found in check mode; the change was previewed, not applied.
9010
+ - **no check** — a plain step (no `check`) reached under check mode. A
9011
+ side-effecting step can't be safely previewed, so it is skipped.
9012
+
9013
+ In check mode KiCI never invokes a checked step's `run` (apply) — the preview is
9014
+ guaranteed side-effect-free.
9015
+
9016
+ ## Running in check mode
9017
+
9018
+ `--check` and `--fail-on-drift` work on both local and remote runs:
9019
+
9020
+ ```bash
9021
+ # Apply (default): converge the workflow.
9022
+ kici run local push
9023
+ kici run remote my-fixture
9024
+
9025
+ # Check: report drift, change nothing. Always exits 0.
9026
+ kici run local push --check
9027
+ kici run remote my-fixture --check
9028
+
9029
+ # Check + fail on drift: exit non-zero (2) locally, or fail the run remotely,
9030
+ # when any step reports drift. Use this as a CI gate ("fail the build if prod
9031
+ # has drifted").
9032
+ kici run local push --check --fail-on-drift
9033
+ ```
9034
+
9035
+ `--fail-on-drift` only modifies check mode — passing it without `--check` is an
9036
+ error.
9037
+
9038
+ ## Where outcomes show up
9039
+
9040
+ A check-mode run is labeled in the dashboard with a **CHECK MODE — preview**
9041
+ badge on the run header. Each step shows its outcome chip — applied / in sync /
9042
+ would change / no check — and, when drift was detected, the `summarize` line
9043
+ describing what would change. The rendering is read-only.
9044
+
9045
+ ## See also
9046
+
9047
+ - [Idempotent SDK helpers](./sdk/idempotent.md) — the `idempotent()` / `idempotentStep()` convenience wrappers, which always apply on drift inside a single step (no run-level check mode).
9048
+ - [Core SDK reference](./sdk/core.md) — the `step()`, `job()`, and `workflow()` factories the check facet extends.
9049
+ - [Lock file and drift](./lock-file-and-drift.md) — how the lock file carries step capability flags.
9050
+
9051
+ ---
9052
+
8846
9053
  ## Private npm registries
8847
9054
 
8848
9055
  Source: https://docs.kici.dev/user/private-registries/
@@ -9628,7 +9835,82 @@ forge side looks like:
9628
9835
  Use the App when you can; the `github-repo` preset is a fallback for
9629
9836
  repos where you can't install an App.
9630
9837
 
9631
- ## Create the GitHub App on GitHub's side
9838
+ ## One-click setup (recommended)
9839
+
9840
+ `kici-admin source add github --manifest` creates **and** configures the
9841
+ GitHub App for you via GitHub's App Manifest flow. KiCI builds a manifest
9842
+ with the exact permissions, events, webhook URL, and webhook secret baked
9843
+ in, so you never pick permissions, paste a URL, generate a secret, or
9844
+ download a `.pem` by hand — the App is correct by construction.
9845
+
9846
+ ```bash
9847
+ kici-admin --url http://<orchestrator-host>:4000 --token $KICI_BOOTSTRAP_ADMIN_TOKEN \
9848
+ source add github --manifest --name my-org --github-org my-org
9849
+ ```
9850
+
9851
+ `--github-org <slug>` creates the App under a GitHub **organization** (the
9852
+ `<slug>` is the org's `github.com/<slug>` URL slug, not its display name) — the
9853
+ recommended default, since org-owned Apps can be installed across the org. Drop
9854
+ the flag only when you deliberately want a personal-account App, which can be
9855
+ installed solely on repos you own. You need permission to create Apps in that
9856
+ org (be an org owner, or have the org allow member App creation).
9857
+
9858
+ What happens:
9859
+
9860
+ 1. The CLI resolves your org's webhook URL and opens GitHub with a
9861
+ pre-filled App manifest. You click **"Create GitHub App"** once — the
9862
+ only manual step.
9863
+ 2. GitHub redirects back to a localhost callback; the CLI exchanges the
9864
+ returned setup code for the App's id, private key, and webhook secret.
9865
+ **The private key is exchanged and stored only on your orchestrator
9866
+ host — it never transits the KiCI Platform.**
9867
+ 3. The CLI stores the credentials encrypted under `KICI_SECRET_KEY` and
9868
+ registers the routing key `github:<appId>`, reusing the same storage
9869
+ path as the manual flow.
9870
+ 4. It opens the App's install page so you can pick repos, then verifies
9871
+ end-to-end: it waits for the installation, mints an installation token,
9872
+ and confirms repo access before declaring success.
9873
+
9874
+ ```
9875
+ $ kici-admin source add github --manifest --name my-org --github-org my-org
9876
+ → Opening GitHub to create your App…
9877
+ → ✓ App created (id 12345), credentials captured
9878
+ → ✓ Stored on orchestrator (encrypted), registered as github:12345
9879
+ → Install the App on your repos: https://github.com/apps/my-org/installations/new
9880
+ → ✓ Installation detected (account my-org)
9881
+ → ✓ Credentials verified (3 repositories reachable)
9882
+
9883
+ GitHub App "my-org" is live.
9884
+ Webhook: https://<platform-host>/webhook/<orgId>/github
9885
+ ```
9886
+
9887
+ **Flags:**
9888
+
9889
+ | Flag | Effect |
9890
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9891
+ | `--name <name>` | The App name on GitHub (required). |
9892
+ | `--github-org <slug>` | Create the App under a GitHub organization instead of your personal account. |
9893
+ | `--no-browser` | Headless mode: the CLI prints a `kici.dev` URL to open, then reads the setup code you paste back. The page is pure client-side — it only displays the short-lived code, which is useless once the CLI exchanges it. |
9894
+
9895
+ The manifest flow always creates a **new** App on GitHub. If a source for
9896
+ that App id already exists on the orchestrator, the command refuses — use
9897
+ `source update` to rotate an existing App's credentials.
9898
+
9899
+ If any step after App creation fails (e.g. storage), the CLI prints the
9900
+ captured App id and writes the private key to a `0600` file, then tells
9901
+ you how to finish with the manual `source add github` command — so a
9902
+ created App is never orphaned.
9903
+
9904
+ Independent-mode orchestrators have no GitHub-App ingress (it is
9905
+ Platform-relayed), so the manifest flow is unavailable there; use a
9906
+ generic webhook source instead.
9907
+
9908
+ ## Manual setup (fallback)
9909
+
9910
+ When you'd rather create the App by hand — or your environment can't run
9911
+ the manifest flow — follow these steps.
9912
+
9913
+ ### Create the GitHub App on GitHub's side
9632
9914
 
9633
9915
  1. **Decide the App scope.** User-owned Apps can only be installed on
9634
9916
  repos you own; organization-owned Apps can be installed anywhere in
@@ -9704,7 +9986,7 @@ repos where you can't install an App.
9704
9986
  KiCI runs. Re-install to add repos later — this is live and
9705
9987
  revocable without redeploying the App.
9706
9988
 
9707
- ## Register the App with the orchestrator
9989
+ ### Register the App with the orchestrator
9708
9990
 
9709
9991
  With the App ID, private key `.pem`, and webhook secret in hand:
9710
9992
 
@@ -10283,7 +10565,7 @@ Source: https://docs.kici.dev/architecture/data-flows/
10283
10565
 
10284
10566
  This document describes the key data flows through the KiCI architecture: webhook delivery, job execution, developer-initiated remote runs, dependency caching, re-run and cancel, trace ID propagation, internal event routing, and generic webhook ingestion.
10285
10567
 
10286
- > **Lock file schema version:** The lock file uses schema version 17, which widens per-job init to typed presets (`mise` / `{ mise }`) and `auto` detection on top of v16's normalized approval config, v15's per-job init config, v14's declarative cache specs, v11's `LockInlineValue` for pure function inline evaluation, v10's simplified negative patterns (! prefix in repos/paths arrays), v9's global workflow repos matching, and v8's runsOn polymorphic type support.
10568
+ > **Lock file schema version:** The lock file uses schema version 21, which adds the `CheckMode` / `CheckStepOutcome` enums for check-mode step execution on top of v20's `LabelMatcher` (exact/regex) selectors for `runsOn`/`runsOnAll`/`excludeLabels`, v19's `maxParallel`/`failFast` fan-out concurrency, v18's `runsOnAll` host fan-out predicate and `onUnreachable` policy, v17's typed init presets (`mise` / `{ mise }`) and `auto` detection, v16's normalized approval config, v15's per-job init config, v14's declarative cache specs, v11's `LockInlineValue` for pure function inline evaluation, v10's simplified negative patterns (! prefix in repos/paths arrays), v9's global workflow repos matching, and v8's runsOn polymorphic type support.
10287
10569
 
10288
10570
  ## Webhook delivery flow
10289
10571
 
@@ -10489,7 +10771,7 @@ Dep cache misses alone do **not** trigger a build job. Deps are platform-specifi
10489
10771
 
10490
10772
  ### Cross-source / no-contentHash workflows
10491
10773
 
10492
- - **Lock files without `contentHash`** (schema v1) skip the source cache entirely; agents compile from source. Regenerate lock files with `kici compile` to enable caching. The current lock file schema version is 17.
10774
+ - **Lock files without `contentHash`** (schema v1) skip the source cache entirely; agents compile from source. Regenerate lock files with `kici compile` to enable caching. The current lock file schema version is 21.
10493
10775
  - **Cross-source / global-workflow dispatch** (a workflow registered against source A fired by a webhook on source B) bypasses both caches. The registration's lock file entry still carries `contentHash`, but the cross-source path always clone-and-installs — the eval temp dir doesn't ship `@kici-dev/sdk`. The execution agent still verifies `contentHash` against the cloned source for drift detection.
10494
10776
 
10495
10777
  ### Build deduplication
@@ -10528,7 +10810,7 @@ Both source and dep caches use `S3CacheStorage` as the sole backend. The `CacheS
10528
10810
 
10529
10811
  Cache keys reflect that source tarballs and deps have different platform characteristics:
10530
10812
 
10531
- - **Source:** `source/{contentHash}.tar.gz` — platform-agnostic. Raw TypeScript source is identical regardless of CPU architecture, so one entry is shared across all platforms. `contentHash` is the per-workflow hash from the lock file (`SHA-256(schemaVersion + ":" + rawSource [+ "\0" + assetDigest])`).
10813
+ - **Source:** `source/{contentHash}.tar.gz` — platform-agnostic. Raw TypeScript source is identical regardless of CPU architecture, so one entry is shared across all platforms. `contentHash` is the per-workflow hash from the lock file (`SHA-256(COMPILE_SCHEMA_VERSION + ":" + rawSource [+ "\0" + assetDigest])`, where `COMPILE_SCHEMA_VERSION = 5` and line endings are normalized to LF so the hash agrees across platforms).
10532
10814
  - **Deps:** `deps/{platform}-{arch}/{lockfileHash}.tar.gz` (e.g., `deps/linux-arm64/def456.tar.gz`) — platform-specific. Native dependencies in `node_modules` differ across architectures, so each platform/arch combination gets its own cache entry.
10533
10815
 
10534
10816
  The orchestrator derives the target platform/arch for dep cache lookups by probing `AgentRegistry.findAvailable()` with the workflow's first job's `runsOn` labels to find a representative matching agent, then using that agent's platform and arch. Falls back to `linux/x64` if no matching agents are registered.
@@ -11173,7 +11455,9 @@ This model also enables fully self-hosted deployment: all three tiers can run on
11173
11455
 
11174
11456
  ### Platform
11175
11457
 
11176
- The Platform tier is a thin webhook router. It verifies inbound webhook signatures, relays the payload to the correct orchestrator over WebSocket, and writes a delivery row to its event log. It does not process, store, or execute any customer code. Multi-tenant SaaS specifics (Stripe-driven billing, dashboard surface, identity provider integration) are documented in the internal docs.
11458
+ The Platform is KiCI's hosted, multi-tenant control plane. It provides the hosted dashboard (run listing, run detail, live log streaming, settings), identity and authentication (OIDC, personal access tokens, API keys, JWTs), multi-tenant organization / team / role-based access management, billing, and webhook ingestion -- verifying inbound signatures (HMAC-SHA256, timing-safe) and relaying payloads to the correct orchestrator over WebSocket. It aggregates execution telemetry and status forwarded by orchestrators, registers sources, and matchmakes peers for clustering.
11459
+
11460
+ The Platform never processes, stores, or executes customer code, and never sees customer secrets. It routes webhook payloads and aggregates execution status; the code itself only ever lives on the customer's orchestrator and agent tiers. In the execution path the Platform is deliberately thin -- it does not run jobs -- but functionally it is a full platform, not merely a relay. The hosted Platform is EU-sovereign.
11177
11461
 
11178
11462
  ### Orchestrator (`@kici-dev/orchestrator`)
11179
11463
 
@@ -11208,7 +11492,7 @@ The agent is the execution worker. It runs on customer infrastructure and has fu
11208
11492
 
11209
11493
  Shared business logic used by all three tiers. Single source of truth for cross-tier concerns. Has no internal `@kici-dev/*` dependencies -- only a handful of third-party libraries.
11210
11494
 
11211
- - Protocol message schemas (Zod-based, direction-specific unions including dashboard REST-over-WS, browser live streaming, test run lifecycle, observer channel, log pull, run events, peer-to-peer, cluster join, and source registration)
11495
+ - Protocol message schemas (Zod-based, direction-specific unions including dashboard REST-over-WS, browser live streaming, the test-relay control plane, log pull, run events, peer-to-peer, cluster join, and source registration)
11212
11496
  - Provider interfaces (WebhookNormalizer, LockFileFetcher, ChangedFilesFetcher, CloneTokenProvider, RepoUrlBuilder, ContributorResolver, CheckStatusPoster)
11213
11497
  - Trigger matching engine (branch, path, event evaluation)
11214
11498
  - Execution state machine (11 states, 16 events, pure functions)
@@ -52,6 +52,7 @@ The full markdown bundle of every page indexed here is available at https://docs
52
52
  - [Environments](https://docs.kici.dev/user/environments/): Configure deployment environments with variables, secrets, and protection rules
53
53
  - [Event system](https://docs.kici.dev/user/events/): How KiCI's event model works -- event types, the registration model, event matching, and circuit breaker protection
54
54
  - [Global workflows](https://docs.kici.dev/user/global-workflows/): Cross-repo workflows that run on events from any repo in the same org
55
+ - [Idempotent steps and check mode](https://docs.kici.dev/user/idempotent-steps/): Declare desired state with a step check facet, then run in apply or --check preview mode
55
56
  - [Private npm registries](https://docs.kici.dev/user/private-registries/): Authenticate `npm install` against private registries (CodeArtifact, GitHub Packages, Verdaccio, …) from a workflow's `.kici/package.json`
56
57
  - [Build provenance and attestations](https://docs.kici.dev/user/provenance/): Generate and verify signed SLSA provenance for the artifacts your workflows build
57
58
  - [Secrets](https://docs.kici.dev/user/secrets/): How to access secrets in KiCI workflow steps
@@ -15,7 +15,7 @@ import path from "node:path";
15
15
  import pc from "picocolors";
16
16
  import { writeFile } from "node:fs/promises";
17
17
  import { logger } from "@kici-dev/core";
18
- import { matchAllWorkflows } from "@kici-dev/engine";
18
+ import { CheckMode, CheckStepOutcome, matchAllWorkflows } from "@kici-dev/engine";
19
19
  import os from "node:os";
20
20
  //#region src/local-executor/index.ts
21
21
  /**
@@ -169,7 +169,8 @@ async function runWorkflowBody(workflow, ctx, options, secrets, kiciDir, concurr
169
169
  kiciDir,
170
170
  execDir: ctx.execDir,
171
171
  jobOutputsMap,
172
- signal
172
+ signal,
173
+ checkMode: options.checkMode
173
174
  });
174
175
  },
175
176
  isSuccess: (result) => result.status === "success" || result.status === "skipped"
@@ -305,6 +306,11 @@ async function executeLocal(options) {
305
306
  if (!isQuiet) logger.info(pc.green(`JUnit XML written to ${options.junit}`));
306
307
  }
307
308
  if (!isQuiet) displayLocalSummary(workflowResults);
309
+ if (options.checkMode === CheckMode.enum["check-fail-on-drift"] && hasDrift(workflowResults)) {
310
+ if (!isQuiet) logger.info(pc.yellow("Drift detected in check mode (--fail-on-drift): exiting with code 2"));
311
+ if (materialized && !options.keep) await materialized.cleanup();
312
+ process.exit(2);
313
+ }
308
314
  return allSucceeded;
309
315
  } finally {
310
316
  if (materialized) if (allSucceeded && !options.keep) await materialized.cleanup();
@@ -312,6 +318,13 @@ async function executeLocal(options) {
312
318
  }
313
319
  }
314
320
  /**
321
+ * True when any step across all workflows reported drift (`dry-run` outcome) —
322
+ * the signal `check-fail-on-drift` uses to exit non-zero.
323
+ */
324
+ function hasDrift(results) {
325
+ return results.some((wf) => wf.jobs.some((job) => (job.steps ?? []).some((s) => s.checkOutcome === CheckStepOutcome.enum["dry-run"])));
326
+ }
327
+ /**
315
328
  * Get default concurrency based on available CPU parallelism.
316
329
  */
317
330
  function getDefaultConcurrency() {
@@ -8,6 +8,7 @@
8
8
  * - Single-job step-by-step execution with rules, abort, and output chaining
9
9
  */
10
10
  import type { Workflow, OutputsMap } from '@kici-dev/sdk';
11
+ import { CheckMode } from '@kici-dev/engine';
11
12
  import type { SimulatedEvent } from '@kici-dev/engine';
12
13
  import type { ParsedSecrets } from '../test-runner/secrets-file.js';
13
14
  import type { ResolvedJob, LocalJobResult } from './types.js';
@@ -24,6 +25,8 @@ export interface JobExecutionContext {
24
25
  execDir: string;
25
26
  jobOutputsMap: OutputsMap;
26
27
  signal: AbortSignal;
28
+ /** Run mode for idempotent steps. Defaults to `apply` when unset. */
29
+ checkMode?: CheckMode;
27
30
  }
28
31
  /**
29
32
  * Resolve all jobs in a workflow, expanding matrix jobs and evaluating dynamic jobs.
@@ -8,7 +8,8 @@ import { toEventPayload } from "./to-event-payload.js";
8
8
  import { pathToFileURL } from "node:url";
9
9
  import path from "node:path";
10
10
  import { applyIncludeExclude, expandMatrix, isDynamicJobFn, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
11
- import { formatMatrixSuffix } from "@kici-dev/engine";
11
+ import { CheckMode, CheckStepOutcome, formatMatrixSuffix } from "@kici-dev/engine";
12
+ import { runIdempotentStep } from "@kici-dev/core/idempotency";
12
13
  //#region src/local-executor/job-runner.ts
13
14
  /**
14
15
  * Job resolution (matrix/dynamic) and single-job execution with step context.
@@ -51,6 +52,45 @@ function resolveNeedName(need) {
51
52
  return need.name;
52
53
  }
53
54
  /**
55
+ * Run one local step honoring the run-level {@link CheckMode}, reusing the
56
+ * `runIdempotentStep` primitive for checked steps (never hand-rolled branching).
57
+ * Mirrors the agent step loop's check phase so `kici run local --check` and a
58
+ * remote check run produce identical per-step outcomes.
59
+ */
60
+ async function runLocalStepWithCheckMode(step, ctx, checkMode) {
61
+ if (!step.check) {
62
+ if (checkMode !== CheckMode.enum.apply) return {
63
+ checkOutcome: CheckStepOutcome.enum.no_check,
64
+ status: "skipped",
65
+ outputs: void 0
66
+ };
67
+ return {
68
+ status: "success",
69
+ outputs: await step.run(ctx)
70
+ };
71
+ }
72
+ const res = await runIdempotentStep({
73
+ name: step.name,
74
+ check: () => step.check(ctx),
75
+ summarize: step.summarize,
76
+ apply: (drift) => step.run(ctx, drift),
77
+ whenInSync: step.whenInSync ? () => step.whenInSync(ctx) : void 0
78
+ }, {
79
+ dryRun: checkMode !== CheckMode.enum.apply,
80
+ yes: true,
81
+ log: (line) => ctx.log.info(line)
82
+ });
83
+ const driftSummary = res.drift != null ? step.summarize(res.drift) : void 0;
84
+ const status = res.outcome === CheckStepOutcome.enum.applied ? "success" : "skipped";
85
+ const mappedStatus = res.outcome === CheckStepOutcome.enum["dry-run"] ? "success" : status;
86
+ return {
87
+ checkOutcome: res.outcome,
88
+ status: mappedStatus,
89
+ outputs: res.result,
90
+ ...driftSummary !== void 0 && { driftSummary }
91
+ };
92
+ }
93
+ /**
54
94
  * Maintain the base-name `{ byMatrix, merged }` envelope as each matrix child
55
95
  * completes. `merged` is rebuilt last-write-wins in suffix order so the result
56
96
  * is deterministic regardless of child completion order — matching the remote
@@ -101,6 +141,10 @@ async function resolveJobs(workflow, event) {
101
141
  scalers: [],
102
142
  agents: []
103
143
  }) },
144
+ inventory: {
145
+ query: () => Promise.resolve([]),
146
+ get: () => Promise.resolve(null)
147
+ },
104
148
  oidc: { token: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.oidc.token() is not available during local execution")) }
105
149
  }
106
150
  });
@@ -267,14 +311,18 @@ async function executeResolvedJobInner(resolvedJob, context, startTime) {
267
311
  formatter.logStepStart(expandedName, normalizedStep.name);
268
312
  const stepStart = Date.now();
269
313
  try {
270
- const outputs = await normalizedStep.run(stepCtx);
314
+ const checkMode = context.checkMode ?? CheckMode.enum.apply;
315
+ const phase = await runLocalStepWithCheckMode(normalizedStep, stepCtx, checkMode);
316
+ const outputs = phase.outputs;
271
317
  const stepDuration = Date.now() - stepStart;
272
318
  formatter.logStepComplete(expandedName, normalizedStep.name, stepDuration);
273
319
  const stepResult = {
274
320
  name: normalizedStep.name,
275
- status: "success",
321
+ status: phase.status,
276
322
  durationMs: stepDuration,
277
- outputs
323
+ outputs,
324
+ ...phase.checkOutcome !== void 0 && { checkOutcome: phase.checkOutcome },
325
+ ...phase.driftSummary !== void 0 && { driftSummary: phase.driftSummary }
278
326
  };
279
327
  if (outputs != null) outputsMap.set(normalizedStep.name, outputs);
280
328
  stepResults.push(stepResult);
@@ -95,7 +95,9 @@ function formatLocalJsonResult(results) {
95
95
  name: step.name,
96
96
  status: step.status,
97
97
  durationMs: step.durationMs,
98
- ...step.error && { error: step.error.message }
98
+ ...step.error && { error: step.error.message },
99
+ ...step.checkOutcome && { checkOutcome: step.checkOutcome },
100
+ ...step.driftSummary && { driftSummary: step.driftSummary }
99
101
  })),
100
102
  ...job.ruleResults && job.ruleResults.length > 0 && { ruleResults: job.ruleResults.map((r) => ({
101
103
  label: r.label,
@@ -1,4 +1,5 @@
1
1
  import type { MatrixValues, Job } from '@kici-dev/sdk';
2
+ import type { CheckMode } from '@kici-dev/engine';
2
3
  import type { JobResult } from '../test-runner/job-executor.js';
3
4
  /**
4
5
  * All CLI flags for `kici run local`.
@@ -42,6 +43,12 @@ export interface RunLocalOptions {
42
43
  inPlace?: boolean;
43
44
  /** --keep: always retain the isolated tmp checkout (default: keep only on failure) */
44
45
  keep?: boolean;
46
+ /**
47
+ * Run mode resolved from --check / --fail-on-drift. Threads to the agent step
48
+ * loop: `apply` (default) converges, `check` previews drift, `check-fail-on-drift`
49
+ * previews drift and fails the run if any step reports drift. Defaults to `apply`.
50
+ */
51
+ checkMode?: CheckMode;
45
52
  }
46
53
  /**
47
54
  * A job after matrix expansion with resolved values.
@@ -650,6 +650,8 @@ function transformSteps(steps, gitRoot) {
650
650
  },
651
651
  ...step.onCancel !== void 0 && { hasOnCancel: true },
652
652
  ...step.cleanup !== void 0 && { hasCleanup: true },
653
+ ...step.check !== void 0 && { hasCheck: true },
654
+ ...step.whenInSync !== void 0 && { hasWhenInSync: true },
653
655
  ...step.requireApproval !== void 0 && { approval: toLockApproval(step.requireApproval) }
654
656
  };
655
657
  });
@@ -10,6 +10,7 @@
10
10
  * The overlay tarball does NOT flow through here: `initUpload` returns an
11
11
  * external presigned URL the CLI PUTs to directly (data plane).
12
12
  */
13
+ import type { CheckMode } from '@kici-dev/engine';
13
14
  export declare class AuthenticationError extends Error {
14
15
  constructor(message?: string);
15
16
  }
@@ -70,6 +71,12 @@ export interface PlatformTriggerInput {
70
71
  secrets?: Record<string, string>;
71
72
  encryptedSecrets?: string;
72
73
  encryptedSecretsKey?: string;
74
+ /**
75
+ * Run mode for the dispatched run (`apply` | `check` | `check-fail-on-drift`).
76
+ * The Platform relays it onto the dispatch event so the orchestrator runs the
77
+ * agent step loop in the requested mode. Omitted means `apply`.
78
+ */
79
+ checkMode?: CheckMode;
73
80
  }
74
81
  export interface PlatformTriggerResponse {
75
82
  runId: string;
@@ -1,6 +1,6 @@
1
1
  import "../chunk-BTugEXQM.js";
2
2
  //#region src/templates/package-json.ts
3
- const sdkVersion = "0.1.19";
3
+ const sdkVersion = "0.1.21";
4
4
  /**
5
5
  * Generate package.json content for .kici/ directory
6
6
  *
@@ -1,5 +1,6 @@
1
1
  import type { Job, Workflow } from '@kici-dev/sdk';
2
2
  import type { OutputsMap, StepRefMap } from '@kici-dev/sdk';
3
+ import type { CheckStepOutcome } from '@kici-dev/engine';
3
4
  import type { SimulatedEvent } from '@kici-dev/engine';
4
5
  import type { RuleResult } from '@kici-dev/sdk';
5
6
  import type { ParsedSecrets } from './secrets-file.js';
@@ -15,10 +16,14 @@ interface SdkOutputSetters {
15
16
  }
16
17
  export interface StepResult {
17
18
  name: string;
18
- status: 'success' | 'failure';
19
+ status: 'success' | 'failure' | 'skipped';
19
20
  durationMs: number;
20
21
  error?: Error;
21
22
  outputs?: Record<string, unknown>;
23
+ /** Idempotent per-step outcome under a non-default check mode. */
24
+ checkOutcome?: CheckStepOutcome;
25
+ /** Human-readable drift summary, present when drift was detected. */
26
+ driftSummary?: string;
22
27
  }
23
28
  export interface JobResult {
24
29
  name: string;
@@ -133,6 +133,10 @@ function createStepContext(workflowInfo, jobInfo, repoRoot, inputs = {}, matrix,
133
133
  scalers: [],
134
134
  agents: []
135
135
  }) },
136
+ inventory: {
137
+ query: () => Promise.resolve([]),
138
+ get: () => Promise.resolve(null)
139
+ },
136
140
  oidc: { token: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.oidc.token() is not available in the local test runner")) }
137
141
  },
138
142
  cache: {
package/dist/types.d.ts CHANGED
@@ -23,7 +23,7 @@ export interface LockApproval {
23
23
  readonly timeoutSeconds?: number;
24
24
  }
25
25
  /** Schema version - re-exported from engine as single source of truth */
26
- export declare const SCHEMA_VERSION: 20;
26
+ export declare const SCHEMA_VERSION: 21;
27
27
  /**
28
28
  * Source file reference with meaningful path.
29
29
  * Format: file is relative path from git root, export uses hash syntax.
@@ -334,6 +334,15 @@ export interface LockStep {
334
334
  readonly hasOnCancel?: boolean;
335
335
  /** Whether this step has a cleanup hook. */
336
336
  readonly hasCleanup?: boolean;
337
+ /**
338
+ * Whether this step declares an idempotent `check` facet. When true the
339
+ * orchestrator knows the step is check-capable and a run can be dispatched in
340
+ * check mode. The check/apply closures themselves are never serialized — the
341
+ * agent re-evaluates the real workflow TypeScript.
342
+ */
343
+ readonly hasCheck?: boolean;
344
+ /** Whether this step declares a `whenInSync` facet (produces outputs when in sync). */
345
+ readonly hasWhenInSync?: boolean;
337
346
  /** Normalized approval gate; when set the step pauses for a human approval. */
338
347
  readonly approval?: LockApproval;
339
348
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kici-dev/compiler",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Compiler and CLI for KiCI workflows. Compiles `.kici/workflows/*.ts` to a `kici.lock.json` file consumed by the orchestrator and agents, and runs workflows locally or against a remote orchestrator.",
5
5
  "keywords": [
6
6
  "ci",
@@ -63,11 +63,11 @@
63
63
  "yaml": "^2.9.0",
64
64
  "zod": "^4.4.3",
65
65
  "zx": "^8.8.5",
66
- "@kici-dev/core": "0.1.19",
67
- "@kici-dev/engine": "0.1.19"
66
+ "@kici-dev/core": "0.1.21",
67
+ "@kici-dev/engine": "0.1.21"
68
68
  },
69
69
  "peerDependencies": {
70
- "@kici-dev/sdk": "0.1.19"
70
+ "@kici-dev/sdk": "0.1.21"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@types/proper-lockfile": "^4.1.4"
package/sbom.spdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "@kici-dev/compiler@0.1.19",
6
- "documentNamespace": "https://kici.dev/sbom/%40kici-dev%2Fcompiler/0.1.19/3822508c-fd16-4b1b-a7f4-3a5038adca82",
5
+ "name": "@kici-dev/compiler@0.1.21",
6
+ "documentNamespace": "https://kici.dev/sbom/%40kici-dev%2Fcompiler/0.1.21/4162fc88-26bd-41e3-8085-1dc26bb0eb1f",
7
7
  "creationInfo": {
8
- "created": "2026-06-19T05:11:14Z",
8
+ "created": "2026-06-23T05:10:19Z",
9
9
  "creators": [
10
10
  "Tool: kici-sbom-generator"
11
11
  ]
@@ -484,7 +484,7 @@
484
484
  {
485
485
  "SPDXID": "SPDXRef-RootPackage",
486
486
  "name": "@kici-dev/compiler",
487
- "versionInfo": "0.1.19",
487
+ "versionInfo": "0.1.21",
488
488
  "downloadLocation": "NOASSERTION",
489
489
  "filesAnalyzed": false,
490
490
  "licenseConcluded": "NOASSERTION",
@@ -495,16 +495,16 @@
495
495
  {
496
496
  "referenceCategory": "PACKAGE-MANAGER",
497
497
  "referenceType": "purl",
498
- "referenceLocator": "pkg:npm/%40kici-dev/compiler@0.1.19"
498
+ "referenceLocator": "pkg:npm/%40kici-dev/compiler@0.1.21"
499
499
  }
500
500
  ],
501
501
  "description": "Compiler and CLI for KiCI workflows. Compiles `.kici/workflows/*.ts` to a `kici.lock.json` file consumed by the orchestrator and agents, and runs workflows locally or against a remote orchestrator.",
502
502
  "homepage": "https://kici.dev"
503
503
  },
504
504
  {
505
- "SPDXID": "SPDXRef-Package--kici-dev-core-0.1.19",
505
+ "SPDXID": "SPDXRef-Package--kici-dev-core-0.1.21",
506
506
  "name": "@kici-dev/core",
507
- "versionInfo": "0.1.19",
507
+ "versionInfo": "0.1.21",
508
508
  "downloadLocation": "NOASSERTION",
509
509
  "filesAnalyzed": false,
510
510
  "licenseConcluded": "NOASSERTION",
@@ -515,16 +515,16 @@
515
515
  {
516
516
  "referenceCategory": "PACKAGE-MANAGER",
517
517
  "referenceType": "purl",
518
- "referenceLocator": "pkg:npm/%40kici-dev/core@0.1.19"
518
+ "referenceLocator": "pkg:npm/%40kici-dev/core@0.1.21"
519
519
  }
520
520
  ],
521
521
  "description": "Light shared utilities for the KiCI stack (logging, errors, formatting, crypto, zx init, the TypeScript ESM loader hook). No server-side dependencies.",
522
522
  "homepage": "https://kici.dev"
523
523
  },
524
524
  {
525
- "SPDXID": "SPDXRef-Package--kici-dev-engine-0.1.19",
525
+ "SPDXID": "SPDXRef-Package--kici-dev-engine-0.1.21",
526
526
  "name": "@kici-dev/engine",
527
- "versionInfo": "0.1.19",
527
+ "versionInfo": "0.1.21",
528
528
  "downloadLocation": "NOASSERTION",
529
529
  "filesAnalyzed": false,
530
530
  "licenseConcluded": "NOASSERTION",
@@ -535,16 +535,16 @@
535
535
  {
536
536
  "referenceCategory": "PACKAGE-MANAGER",
537
537
  "referenceType": "purl",
538
- "referenceLocator": "pkg:npm/%40kici-dev/engine@0.1.19"
538
+ "referenceLocator": "pkg:npm/%40kici-dev/engine@0.1.21"
539
539
  }
540
540
  ],
541
541
  "description": "Shared business logic for the KiCI CI/CD stack: protocol, triggers, state machine, and provider interfaces used by the Platform relay, orchestrator, and compiler.",
542
542
  "homepage": "https://kici.dev"
543
543
  },
544
544
  {
545
- "SPDXID": "SPDXRef-Package--kici-dev-sdk-0.1.19",
545
+ "SPDXID": "SPDXRef-Package--kici-dev-sdk-0.1.21",
546
546
  "name": "@kici-dev/sdk",
547
- "versionInfo": "0.1.19",
547
+ "versionInfo": "0.1.21",
548
548
  "downloadLocation": "NOASSERTION",
549
549
  "filesAnalyzed": false,
550
550
  "licenseConcluded": "NOASSERTION",
@@ -555,7 +555,7 @@
555
555
  {
556
556
  "referenceCategory": "PACKAGE-MANAGER",
557
557
  "referenceType": "purl",
558
- "referenceLocator": "pkg:npm/%40kici-dev/sdk@0.1.19"
558
+ "referenceLocator": "pkg:npm/%40kici-dev/sdk@0.1.21"
559
559
  }
560
560
  ],
561
561
  "description": "TypeScript SDK for defining KiCI workflows. Import into `.kici/workflows/*.ts` to declare workflows, jobs, steps, triggers, rules, and matrix configurations.",
@@ -3222,17 +3222,17 @@
3222
3222
  },
3223
3223
  {
3224
3224
  "spdxElementId": "SPDXRef-RootPackage",
3225
- "relatedSpdxElement": "SPDXRef-Package--kici-dev-core-0.1.19",
3225
+ "relatedSpdxElement": "SPDXRef-Package--kici-dev-core-0.1.21",
3226
3226
  "relationshipType": "DEPENDS_ON"
3227
3227
  },
3228
3228
  {
3229
3229
  "spdxElementId": "SPDXRef-RootPackage",
3230
- "relatedSpdxElement": "SPDXRef-Package--kici-dev-engine-0.1.19",
3230
+ "relatedSpdxElement": "SPDXRef-Package--kici-dev-engine-0.1.21",
3231
3231
  "relationshipType": "DEPENDS_ON"
3232
3232
  },
3233
3233
  {
3234
3234
  "spdxElementId": "SPDXRef-RootPackage",
3235
- "relatedSpdxElement": "SPDXRef-Package--kici-dev-sdk-0.1.19",
3235
+ "relatedSpdxElement": "SPDXRef-Package--kici-dev-sdk-0.1.21",
3236
3236
  "relationshipType": "DEPENDS_ON"
3237
3237
  },
3238
3238
  {
@@ -3301,82 +3301,82 @@
3301
3301
  "relationshipType": "DEPENDS_ON"
3302
3302
  },
3303
3303
  {
3304
- "spdxElementId": "SPDXRef-Package--kici-dev-core-0.1.19",
3304
+ "spdxElementId": "SPDXRef-Package--kici-dev-core-0.1.21",
3305
3305
  "relatedSpdxElement": "SPDXRef-Package-oxc-transform-0.135.0",
3306
3306
  "relationshipType": "DEPENDS_ON"
3307
3307
  },
3308
3308
  {
3309
- "spdxElementId": "SPDXRef-Package--kici-dev-core-0.1.19",
3309
+ "spdxElementId": "SPDXRef-Package--kici-dev-core-0.1.21",
3310
3310
  "relatedSpdxElement": "SPDXRef-Package-picocolors-1.1.1",
3311
3311
  "relationshipType": "DEPENDS_ON"
3312
3312
  },
3313
3313
  {
3314
- "spdxElementId": "SPDXRef-Package--kici-dev-core-0.1.19",
3314
+ "spdxElementId": "SPDXRef-Package--kici-dev-core-0.1.21",
3315
3315
  "relatedSpdxElement": "SPDXRef-Package-winston-daily-rotate-file-5.0.0",
3316
3316
  "relationshipType": "DEPENDS_ON"
3317
3317
  },
3318
3318
  {
3319
- "spdxElementId": "SPDXRef-Package--kici-dev-core-0.1.19",
3319
+ "spdxElementId": "SPDXRef-Package--kici-dev-core-0.1.21",
3320
3320
  "relatedSpdxElement": "SPDXRef-Package-winston-3.19.0",
3321
3321
  "relationshipType": "DEPENDS_ON"
3322
3322
  },
3323
3323
  {
3324
- "spdxElementId": "SPDXRef-Package--kici-dev-core-0.1.19",
3324
+ "spdxElementId": "SPDXRef-Package--kici-dev-core-0.1.21",
3325
3325
  "relatedSpdxElement": "SPDXRef-Package-zod-4.4.3",
3326
3326
  "relationshipType": "DEPENDS_ON"
3327
3327
  },
3328
3328
  {
3329
- "spdxElementId": "SPDXRef-Package--kici-dev-core-0.1.19",
3329
+ "spdxElementId": "SPDXRef-Package--kici-dev-core-0.1.21",
3330
3330
  "relatedSpdxElement": "SPDXRef-Package-zx-8.8.5",
3331
3331
  "relationshipType": "DEPENDS_ON"
3332
3332
  },
3333
3333
  {
3334
- "spdxElementId": "SPDXRef-Package--kici-dev-engine-0.1.19",
3334
+ "spdxElementId": "SPDXRef-Package--kici-dev-engine-0.1.21",
3335
3335
  "relatedSpdxElement": "SPDXRef-Package-jose-6.2.3",
3336
3336
  "relationshipType": "DEPENDS_ON"
3337
3337
  },
3338
3338
  {
3339
- "spdxElementId": "SPDXRef-Package--kici-dev-engine-0.1.19",
3339
+ "spdxElementId": "SPDXRef-Package--kici-dev-engine-0.1.21",
3340
3340
  "relatedSpdxElement": "SPDXRef-Package-jsonpath-plus-10.4.0",
3341
3341
  "relationshipType": "DEPENDS_ON"
3342
3342
  },
3343
3343
  {
3344
- "spdxElementId": "SPDXRef-Package--kici-dev-engine-0.1.19",
3344
+ "spdxElementId": "SPDXRef-Package--kici-dev-engine-0.1.21",
3345
3345
  "relatedSpdxElement": "SPDXRef-Package-picomatch-4.0.4",
3346
3346
  "relationshipType": "DEPENDS_ON"
3347
3347
  },
3348
3348
  {
3349
- "spdxElementId": "SPDXRef-Package--kici-dev-engine-0.1.19",
3349
+ "spdxElementId": "SPDXRef-Package--kici-dev-engine-0.1.21",
3350
3350
  "relatedSpdxElement": "SPDXRef-Package-safe-regex-2.1.1",
3351
3351
  "relationshipType": "DEPENDS_ON"
3352
3352
  },
3353
3353
  {
3354
- "spdxElementId": "SPDXRef-Package--kici-dev-engine-0.1.19",
3354
+ "spdxElementId": "SPDXRef-Package--kici-dev-engine-0.1.21",
3355
3355
  "relatedSpdxElement": "SPDXRef-Package-zod-4.4.3",
3356
3356
  "relationshipType": "DEPENDS_ON"
3357
3357
  },
3358
3358
  {
3359
- "spdxElementId": "SPDXRef-Package--kici-dev-sdk-0.1.19",
3360
- "relatedSpdxElement": "SPDXRef-Package--kici-dev-core-0.1.19",
3359
+ "spdxElementId": "SPDXRef-Package--kici-dev-sdk-0.1.21",
3360
+ "relatedSpdxElement": "SPDXRef-Package--kici-dev-core-0.1.21",
3361
3361
  "relationshipType": "DEPENDS_ON"
3362
3362
  },
3363
3363
  {
3364
- "spdxElementId": "SPDXRef-Package--kici-dev-sdk-0.1.19",
3365
- "relatedSpdxElement": "SPDXRef-Package--kici-dev-engine-0.1.19",
3364
+ "spdxElementId": "SPDXRef-Package--kici-dev-sdk-0.1.21",
3365
+ "relatedSpdxElement": "SPDXRef-Package--kici-dev-engine-0.1.21",
3366
3366
  "relationshipType": "DEPENDS_ON"
3367
3367
  },
3368
3368
  {
3369
- "spdxElementId": "SPDXRef-Package--kici-dev-sdk-0.1.19",
3369
+ "spdxElementId": "SPDXRef-Package--kici-dev-sdk-0.1.21",
3370
3370
  "relatedSpdxElement": "SPDXRef-Package-micromatch-4.0.8",
3371
3371
  "relationshipType": "DEPENDS_ON"
3372
3372
  },
3373
3373
  {
3374
- "spdxElementId": "SPDXRef-Package--kici-dev-sdk-0.1.19",
3374
+ "spdxElementId": "SPDXRef-Package--kici-dev-sdk-0.1.21",
3375
3375
  "relatedSpdxElement": "SPDXRef-Package-zod-4.4.3",
3376
3376
  "relationshipType": "DEPENDS_ON"
3377
3377
  },
3378
3378
  {
3379
- "spdxElementId": "SPDXRef-Package--kici-dev-sdk-0.1.19",
3379
+ "spdxElementId": "SPDXRef-Package--kici-dev-sdk-0.1.21",
3380
3380
  "relatedSpdxElement": "SPDXRef-Package-zx-8.8.5",
3381
3381
  "relationshipType": "DEPENDS_ON"
3382
3382
  },