@kici-dev/compiler 0.1.20 → 0.1.22

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.
@@ -14,6 +14,7 @@ The full markdown bundle of every page indexed here is available at https://docs
14
14
 
15
15
  - [Basic workflow patterns](https://docs.kici.dev/user/patterns/basic/): Basic CI, PR-only / push-only filters, multiple triggers, manual-only workflows
16
16
  - [Conditionals & matrix patterns](https://docs.kici.dev/user/patterns/conditionals-matrix/): Conditional execution with rules, matrix builds (static + dynamic), dynamic job generation
17
+ - [Host restart & wait-for-alive](https://docs.kici.dev/user/patterns/host-restart/): Reboot the host a workflow runs on and continue after it comes back
17
18
  - [Integration patterns](https://docs.kici.dev/user/patterns/integrations/): Workflow chaining, generic webhooks, Stripe, self-hosted git forges, plain GitHub repos
18
19
  - [Pattern reference](https://docs.kici.dev/user/patterns/reference/): Step context, examples repository, GitHub check run output — cross-cutting reference for all patterns
19
20
  - [Scheduling & event patterns](https://docs.kici.dev/user/patterns/scheduling-and-events/): Nightly cron, workflow-complete-triggered deploys, custom event chaining
@@ -44,7 +45,7 @@ The full markdown bundle of every page indexed here is available at https://docs
44
45
  ## Workflow features
45
46
 
46
47
  - [Account and sign-in](https://docs.kici.dev/user/account-and-login/): How your KiCI account relates to sign-in methods, and how to change the way you sign in.
47
- - [Approval gates](https://docs.kici.dev/user/approvals/): Pause a workflow for human sign-off at step, job, or workflow granularity with requireApproval
48
+ - [Approval gates](https://docs.kici.dev/user/approvals/): Pause a workflow for human sign-off at step, job, or workflow granularity with approval
48
49
  - [Concurrency groups](https://docs.kici.dev/user/concurrency/): Control parallel execution with auto-cancel and queue modes
49
50
  - [Dashboard](https://docs.kici.dev/user/dashboard/): Web UI for monitoring workflow runs, managing sources, secrets, and organization settings.
50
51
  - [Dynamic values](https://docs.kici.dev/user/dynamic-values/)
@@ -52,6 +53,7 @@ The full markdown bundle of every page indexed here is available at https://docs
52
53
  - [Environments](https://docs.kici.dev/user/environments/): Configure deployment environments with variables, secrets, and protection rules
53
54
  - [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
55
  - [Global workflows](https://docs.kici.dev/user/global-workflows/): Cross-repo workflows that run on events from any repo in the same org
56
+ - [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
57
  - [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
58
  - [Build provenance and attestations](https://docs.kici.dev/user/provenance/): Generate and verify signed SLSA provenance for the artifacts your workflows build
57
59
  - [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,7 +141,12 @@ async function resolveJobs(workflow, event) {
101
141
  scalers: [],
102
142
  agents: []
103
143
  }) },
104
- oidc: { token: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.oidc.token() is not available during local execution")) }
144
+ inventory: {
145
+ query: () => Promise.resolve([]),
146
+ get: () => Promise.resolve(null)
147
+ },
148
+ oidc: { token: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.oidc.token() is not available during local execution")) },
149
+ host: { requestReboot: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.host.requestReboot() is not available during local execution")) }
105
150
  }
106
151
  });
107
152
  for (const genJob of generatedJobs) {
@@ -267,14 +312,18 @@ async function executeResolvedJobInner(resolvedJob, context, startTime) {
267
312
  formatter.logStepStart(expandedName, normalizedStep.name);
268
313
  const stepStart = Date.now();
269
314
  try {
270
- const outputs = await normalizedStep.run(stepCtx);
315
+ const checkMode = context.checkMode ?? CheckMode.enum.apply;
316
+ const phase = await runLocalStepWithCheckMode(normalizedStep, stepCtx, checkMode);
317
+ const outputs = phase.outputs;
271
318
  const stepDuration = Date.now() - stepStart;
272
319
  formatter.logStepComplete(expandedName, normalizedStep.name, stepDuration);
273
320
  const stepResult = {
274
321
  name: normalizedStep.name,
275
- status: "success",
322
+ status: phase.status,
276
323
  durationMs: stepDuration,
277
- outputs
324
+ outputs,
325
+ ...phase.checkOutcome !== void 0 && { checkOutcome: phase.checkOutcome },
326
+ ...phase.driftSummary !== void 0 && { driftSummary: phase.driftSummary }
278
327
  };
279
328
  if (outputs != null) outputsMap.set(normalizedStep.name, outputs);
280
329
  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.
@@ -5,10 +5,10 @@ import { resolveHashFiles } from "./hash-files.js";
5
5
  import { analyzePurity } from "./purity-analyzer.js";
6
6
  import path from "node:path";
7
7
  import { readFileSync } from "node:fs";
8
- import { getDynamicJobGroup, getDynamicJobNeeds, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject, normalizeCacheSpecs, normalizeRequireApproval } from "@kici-dev/sdk";
8
+ import { getDynamicJobGroup, getDynamicJobNeeds, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject, normalizeApproval, normalizeCacheSpecs } from "@kici-dev/sdk";
9
9
  import { sha256 } from "@kici-dev/core";
10
10
  import { PackageManager, detectPackageManagerSync, detectYarnFlavorSync } from "@kici-dev/core/package-manager";
11
- import { validateResourceRequest } from "@kici-dev/engine";
11
+ import { resolveWhenToRunOn, validateResourceRequest } from "@kici-dev/engine";
12
12
  import { normalizeRunsOnAllToMatchers, normalizeRunsOnToMatchers } from "@kici-dev/engine/labels/compile";
13
13
  import { execSync } from "node:child_process";
14
14
  //#region src/lockfile/generator.ts
@@ -158,7 +158,7 @@ function transformWorkflow(workflow, sourceFile, exportRef, bundleSource, gitRoo
158
158
  ...workflow.concurrency.max !== void 0 && { max: workflow.concurrency.max }
159
159
  } },
160
160
  ...workflow.timeout !== void 0 && { timeout: workflow.timeout },
161
- ...workflow.requireApproval !== void 0 && { approval: toLockApproval(workflow.requireApproval) }
161
+ ...workflow.approval !== void 0 && { approval: (assertNonStepApprovalScope(workflow.approval, "workflow"), toLockApproval(workflow.approval)) }
162
162
  };
163
163
  }
164
164
  /**
@@ -565,12 +565,13 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
565
565
  ...job.timeout !== void 0 && { timeout: job.timeout },
566
566
  ...job.resources !== void 0 && { resources: job.resources },
567
567
  ...job.init !== void 0 && { init: job.init },
568
- ...job.requireApproval !== void 0 && { approval: toLockApproval(job.requireApproval) }
568
+ ...job.approval !== void 0 && { approval: (assertNonStepApprovalScope(job.approval, "job"), toLockApproval(job.approval)) }
569
569
  };
570
570
  }
571
571
  /**
572
572
  * Resolve needs to lock file format.
573
- * Handles strings, Job objects, DynamicGroupRef, and object forms with ifFailed policy.
573
+ * Handles strings, Job objects, DynamicGroupRef, and object forms with a `when`
574
+ * run condition (normalized to a `runOn` status-set via the engine helper).
574
575
  * Uses the UUID-to-renamed-name mapping so that references to id-less jobs
575
576
  * resolve to their lock file names (job-N) instead of the original UUIDs.
576
577
  */
@@ -582,26 +583,26 @@ function resolveNeedsForLock(needs, uuidToName) {
582
583
  else if (isDynamicGroupRef(need)) {
583
584
  resolvedNeeds.push({
584
585
  group: need.group,
585
- ifFailed: need.ifFailed ?? "skip"
586
+ runOn: resolveWhenToRunOn(need.when)
586
587
  });
587
588
  groups.push(need.group);
589
+ } else if ("_tag" in need && need._tag === "Job") {
590
+ const name = need.name;
591
+ resolvedNeeds.push(uuidToName?.get(name) ?? name);
588
592
  } else if ("group" in need && typeof need.group === "string") {
589
593
  const g = need;
590
594
  resolvedNeeds.push({
591
595
  group: g.group,
592
- ifFailed: g.ifFailed ?? "skip"
596
+ runOn: resolveWhenToRunOn(g.when)
593
597
  });
594
598
  groups.push(g.group);
595
- } else if ("ifFailed" in need && "name" in need) {
599
+ } else {
596
600
  const n = need;
597
601
  const name = uuidToName?.get(n.name) ?? n.name;
598
602
  resolvedNeeds.push({
599
603
  name,
600
- ifFailed: n.ifFailed
604
+ runOn: resolveWhenToRunOn(n.when)
601
605
  });
602
- } else {
603
- const name = need.name;
604
- resolvedNeeds.push(uuidToName?.get(name) ?? name);
605
606
  }
606
607
  return {
607
608
  needs: resolvedNeeds,
@@ -613,15 +614,31 @@ function resolveNeedsForLock(needs, uuidToName) {
613
614
  * Assigns counter-based IDs to unnamed steps (bare functions and id-less steps).
614
615
  * Counter only increments for unnamed entries; named steps keep their names.
615
616
  */
616
- /** Map an SDK `requireApproval` to the normalized lock `approval` block. */
617
- function toLockApproval(r) {
618
- const n = normalizeRequireApproval(r);
617
+ /** Map an SDK `approval` to the normalized lock `approval` block. */
618
+ function toLockApproval(c) {
619
+ const n = normalizeApproval(c);
619
620
  return {
620
621
  clauses: n.clauses,
621
622
  ...n.reason !== void 0 && { reason: n.reason },
622
- ...n.timeoutSeconds !== void 0 && { timeoutSeconds: n.timeoutSeconds }
623
+ ...n.timeoutSeconds !== void 0 && { timeoutSeconds: n.timeoutSeconds },
624
+ when: n.when
623
625
  };
624
626
  }
627
+ /**
628
+ * Validate an approval config at job/workflow scope: `when: 'drift'` is a
629
+ * step-scope-only gate (it fires between a step's check and run), so it is a
630
+ * compile error anywhere else.
631
+ */
632
+ function assertNonStepApprovalScope(c, scope) {
633
+ if (normalizeApproval(c).when === "drift") throw new Error(`approval.when "drift" is only valid on steps (found at ${scope} scope)`);
634
+ }
635
+ /**
636
+ * Validate a step's approval config: `when: 'drift'` fires between the step's
637
+ * check and run, so it requires a `check` facet. A compile error otherwise.
638
+ */
639
+ function assertStepApprovalCheckFacet(step) {
640
+ if (step.approval !== void 0 && normalizeApproval(step.approval).when === "drift" && step.check === void 0) throw new Error(`step '${step.name || "(unnamed)"}': approval.when "drift" requires a check facet`);
641
+ }
625
642
  function transformSteps(steps, gitRoot) {
626
643
  let stepCounter = 0;
627
644
  return steps.map((stepOrFn) => {
@@ -650,7 +667,9 @@ function transformSteps(steps, gitRoot) {
650
667
  },
651
668
  ...step.onCancel !== void 0 && { hasOnCancel: true },
652
669
  ...step.cleanup !== void 0 && { hasCleanup: true },
653
- ...step.requireApproval !== void 0 && { approval: toLockApproval(step.requireApproval) }
670
+ ...step.check !== void 0 && { hasCheck: true },
671
+ ...step.whenInSync !== void 0 && { hasWhenInSync: true },
672
+ ...step.approval !== void 0 && { approval: (assertStepApprovalCheckFacet(step), toLockApproval(step.approval)) }
654
673
  };
655
674
  });
656
675
  }
@@ -9,6 +9,8 @@ export interface GlobalConfig {
9
9
  endpoint?: string;
10
10
  /** Platform relay URL */
11
11
  platformEndpoint?: string;
12
+ /** OIDC issuer URL the PAT was minted against (provenance; from OAuth login) */
13
+ oidcIssuer?: string;
12
14
  /** Routing key for webhook source identification (e.g., 'github:42') */
13
15
  routingKey?: string;
14
16
  /** Personal access token (from OAuth login) */
@@ -14,6 +14,7 @@ function sanitizeConfig(raw) {
14
14
  if (typeof obj.token === "string") config.token = obj.token;
15
15
  if (typeof obj.endpoint === "string") config.endpoint = obj.endpoint;
16
16
  if (typeof obj.platformEndpoint === "string") config.platformEndpoint = obj.platformEndpoint;
17
+ if (typeof obj.oidcIssuer === "string") config.oidcIssuer = obj.oidcIssuer;
17
18
  if (typeof obj.routingKey === "string") config.routingKey = obj.routingKey;
18
19
  if (typeof obj.pat === "string") config.pat = obj.pat;
19
20
  if (typeof obj.patId === "string") config.patId = obj.patId;
@@ -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, HostTargetSelector } from '@kici-dev/engine';
13
14
  export declare class AuthenticationError extends Error {
14
15
  constructor(message?: string);
15
16
  }
@@ -70,6 +71,17 @@ 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;
80
+ /**
81
+ * Host narrowing from `kici run --target`. The Platform relays it verbatim to
82
+ * the orchestrator, which intersects each runsOnAll roster with it.
83
+ */
84
+ target?: HostTargetSelector;
73
85
  }
74
86
  export interface PlatformTriggerResponse {
75
87
  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.20";
3
+ const sdkVersion = "0.1.22";
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,7 +133,12 @@ function createStepContext(workflowInfo, jobInfo, repoRoot, inputs = {}, matrix,
133
133
  scalers: [],
134
134
  agents: []
135
135
  }) },
136
- oidc: { token: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.oidc.token() is not available in the local test runner")) }
136
+ inventory: {
137
+ query: () => Promise.resolve([]),
138
+ get: () => Promise.resolve(null)
139
+ },
140
+ oidc: { token: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.oidc.token() is not available in the local test runner")) },
141
+ host: { requestReboot: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.host.requestReboot() is not available in the local test runner")) }
137
142
  },
138
143
  cache: {
139
144
  restore: async () => ({ hit: false }),
package/dist/types.d.ts CHANGED
@@ -12,18 +12,23 @@
12
12
  * v15 adds per-job init config(s).
13
13
  * v17 widens per-job init to typed presets ('mise' / { mise }) and 'auto' detection.
14
14
  */
15
- import type { ResourceRequest, ApproverClause, RunsOnAllPredicate, OnUnreachableMode, LabelMatcher } from '@kici-dev/engine';
15
+ import type { ResourceRequest, ApproverClause, RunsOnAllPredicate, OnUnreachableMode, LabelMatcher, ExecutionJobStatus } from '@kici-dev/engine';
16
16
  /**
17
17
  * Normalized approval config carried in the lock file. Mirrors the engine
18
- * `LockApproval` type. Produced by the compiler from an SDK `requireApproval`.
18
+ * `LockApproval` type. Produced by the compiler from an SDK `approval` config.
19
19
  */
20
20
  export interface LockApproval {
21
21
  readonly clauses: ApproverClause[];
22
22
  readonly reason?: string;
23
23
  readonly timeoutSeconds?: number;
24
+ /**
25
+ * When the gate fires. `always` (default) gates before the element; `drift`
26
+ * gates between a step's check and run on detected drift (step scope only).
27
+ */
28
+ readonly when: 'always' | 'drift';
24
29
  }
25
30
  /** Schema version - re-exported from engine as single source of truth */
26
- export declare const SCHEMA_VERSION: 20;
31
+ export declare const SCHEMA_VERSION: 22;
27
32
  /**
28
33
  * Source file reference with meaningful path.
29
34
  * Format: file is relative path from git root, export uses hash syntax.
@@ -334,6 +339,15 @@ export interface LockStep {
334
339
  readonly hasOnCancel?: boolean;
335
340
  /** Whether this step has a cleanup hook. */
336
341
  readonly hasCleanup?: boolean;
342
+ /**
343
+ * Whether this step declares an idempotent `check` facet. When true the
344
+ * orchestrator knows the step is check-capable and a run can be dispatched in
345
+ * check mode. The check/apply closures themselves are never serialized — the
346
+ * agent re-evaluates the real workflow TypeScript.
347
+ */
348
+ readonly hasCheck?: boolean;
349
+ /** Whether this step declares a `whenInSync` facet (produces outputs when in sync). */
350
+ readonly hasWhenInSync?: boolean;
337
351
  /** Normalized approval gate; when set the step pauses for a human approval. */
338
352
  readonly approval?: LockApproval;
339
353
  }
@@ -358,15 +372,15 @@ export declare function isLockInlineValue(value: unknown): value is LockInlineVa
358
372
  * (e.g., `kici:role:builder`, `kici:role:init-runner`) are injected by the orchestrator
359
373
  * for internal job types (build/init) and are not user-settable.
360
374
  */
361
- /** Needs entry with per-edge failure policy (mirrors engine NeedsEntry). */
375
+ /** Needs entry with per-edge run-on status-set (mirrors engine NeedsEntry). */
362
376
  export interface LockNeedsEntry {
363
377
  readonly name: string;
364
- readonly ifFailed: 'skip' | 'run';
378
+ readonly runOn: ExecutionJobStatus[];
365
379
  }
366
380
  /** Needs group entry for dynamic group dependencies (mirrors engine NeedsGroupEntry). */
367
381
  export interface LockNeedsGroupEntry {
368
382
  readonly group: string;
369
- readonly ifFailed: 'skip' | 'run';
383
+ readonly runOn: ExecutionJobStatus[];
370
384
  }
371
385
  export interface LockJob {
372
386
  readonly _type: 'static';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kici-dev/compiler",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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.20",
67
- "@kici-dev/engine": "0.1.20"
66
+ "@kici-dev/core": "0.1.22",
67
+ "@kici-dev/engine": "0.1.22"
68
68
  },
69
69
  "peerDependencies": {
70
- "@kici-dev/sdk": "0.1.20"
70
+ "@kici-dev/sdk": "0.1.22"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@types/proper-lockfile": "^4.1.4"