@kici-dev/compiler 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/cli.js +25 -7
  2. package/dist/commands/compile.d.ts +6 -0
  3. package/dist/commands/compile.js +6 -3
  4. package/dist/commands/docs.d.ts +8 -8
  5. package/dist/commands/docs.js +35 -16
  6. package/dist/commands/held-run-client.d.ts +7 -2
  7. package/dist/commands/held-run-client.js +9 -3
  8. package/dist/commands/held-run-resolve.d.ts +5 -0
  9. package/dist/commands/login.d.ts +2 -0
  10. package/dist/commands/login.js +15 -7
  11. package/dist/commands/org.js +2 -2
  12. package/dist/commands/run-hold-watch.d.ts +57 -0
  13. package/dist/commands/run-hold-watch.js +87 -0
  14. package/dist/commands/run.d.ts +23 -0
  15. package/dist/commands/run.js +150 -17
  16. package/dist/commands/test.d.ts +14 -0
  17. package/dist/commands/types.d.ts +2 -0
  18. package/dist/commands/types.js +1 -1
  19. package/dist/fixtures/describe-event.d.ts +6 -0
  20. package/dist/fixtures/describe-event.js +18 -0
  21. package/dist/fixtures/picker.d.ts +19 -0
  22. package/dist/fixtures/picker.js +64 -0
  23. package/dist/llm-context/llms-architecture.txt +1440 -0
  24. package/dist/llm-context/llms-cli.txt +2386 -0
  25. package/dist/llm-context/llms-features.txt +2389 -0
  26. package/dist/llm-context/llms-full.txt +1304 -349
  27. package/dist/llm-context/llms-getting-started.txt +519 -0
  28. package/dist/llm-context/llms-patterns.txt +1324 -0
  29. package/dist/llm-context/llms-providers.txt +805 -0
  30. package/dist/llm-context/llms-sdk.txt +3725 -0
  31. package/dist/llm-context/llms.txt +15 -1
  32. package/dist/local-executor/index.js +40 -3
  33. package/dist/local-executor/job-runner.d.ts +2 -0
  34. package/dist/local-executor/job-runner.js +37 -4
  35. package/dist/local-executor/types.d.ts +2 -0
  36. package/dist/lockfile/generator.js +46 -20
  37. package/dist/remote/config.d.ts +2 -0
  38. package/dist/remote/config.js +1 -0
  39. package/dist/remote/platform-client.d.ts +12 -1
  40. package/dist/remote/uploader.js +1 -0
  41. package/dist/templates/package-json.js +1 -1
  42. package/dist/test-runner/rule-evaluator.d.ts +1 -1
  43. package/dist/test-runner/rule-evaluator.js +2 -1
  44. package/dist/test-runner/step-context.d.ts +1 -1
  45. package/dist/test-runner/step-context.js +8 -2
  46. package/dist/types.d.ts +15 -6
  47. package/package.json +4 -4
  48. package/sbom.spdx.json +35 -35
@@ -4,6 +4,18 @@
4
4
 
5
5
  The full markdown bundle of every page indexed here is available at https://docs.kici.dev/llms-full.txt.
6
6
 
7
+ ## Bundles
8
+
9
+ Each bundle below is a self-contained markdown file for one authoring task. Fetch only the one your task needs instead of the full bundle:
10
+
11
+ - [getting-started](https://docs.kici.dev/llms-getting-started.txt) (25 KB) — Install the SDK, write your first workflow, compile and test locally
12
+ - [patterns](https://docs.kici.dev/llms-patterns.txt) (49 KB) — Copy-paste workflow recipes: triggers, conditionals, matrix, scheduling, integrations
13
+ - [sdk](https://docs.kici.dev/llms-sdk.txt) (185 KB) — Authoring API: workflow/job/step factories, triggers, rules, matrix, runtime, caching
14
+ - [cli](https://docs.kici.dev/llms-cli.txt) (117 KB) — Running the CLI: compile, test, run local/remote, auth, hooks, lock-file drift
15
+ - [features](https://docs.kici.dev/llms-features.txt) (116 KB) — Workflow features: concurrency, environments, secrets, approvals, provenance, events
16
+ - [providers](https://docs.kici.dev/llms-providers.txt) (36 KB) — Connecting sources: GitHub App, universal-git (Forgejo/Gitea/GitLab), local file://
17
+ - [architecture](https://docs.kici.dev/llms-architecture.txt) (88 KB) — How the runtime works: three-tier relay model, data flows, configuration
18
+
7
19
  ## Getting started
8
20
 
9
21
  - [User guide](https://docs.kici.dev/user/): Writing and testing CI/CD workflows in TypeScript
@@ -14,6 +26,7 @@ The full markdown bundle of every page indexed here is available at https://docs
14
26
 
15
27
  - [Basic workflow patterns](https://docs.kici.dev/user/patterns/basic/): Basic CI, PR-only / push-only filters, multiple triggers, manual-only workflows
16
28
  - [Conditionals & matrix patterns](https://docs.kici.dev/user/patterns/conditionals-matrix/): Conditional execution with rules, matrix builds (static + dynamic), dynamic job generation
29
+ - [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
30
  - [Integration patterns](https://docs.kici.dev/user/patterns/integrations/): Workflow chaining, generic webhooks, Stripe, self-hosted git forges, plain GitHub repos
18
31
  - [Pattern reference](https://docs.kici.dev/user/patterns/reference/): Step context, examples repository, GitHub check run output — cross-cutting reference for all patterns
19
32
  - [Scheduling & event patterns](https://docs.kici.dev/user/patterns/scheduling-and-events/): Nightly cron, workflow-complete-triggered deploys, custom event chaining
@@ -44,7 +57,7 @@ The full markdown bundle of every page indexed here is available at https://docs
44
57
  ## Workflow features
45
58
 
46
59
  - [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
60
+ - [Approval gates](https://docs.kici.dev/user/approvals/): Pause a workflow for human sign-off at step, job, or workflow granularity with approval
48
61
  - [Concurrency groups](https://docs.kici.dev/user/concurrency/): Control parallel execution with auto-cancel and queue modes
49
62
  - [Dashboard](https://docs.kici.dev/user/dashboard/): Web UI for monitoring workflow runs, managing sources, secrets, and organization settings.
50
63
  - [Dynamic values](https://docs.kici.dev/user/dynamic-values/)
@@ -65,5 +78,6 @@ The full markdown bundle of every page indexed here is available at https://docs
65
78
 
66
79
  ## Architecture overview
67
80
 
81
+ - [Configuration architecture](https://docs.kici.dev/architecture/configuration/): Config resolution chain, DB schema, encryption, hot-reload, cluster sync
68
82
  - [Data flows](https://docs.kici.dev/architecture/data-flows/): End-to-end data flows through the KiCI three-tier architecture
69
83
  - [Architecture overview](https://docs.kici.dev/architecture/overview/): Three-tier relay model, package structure, and component responsibilities
@@ -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 { CheckMode, CheckStepOutcome, matchAllWorkflows } from "@kici-dev/engine";
18
+ import { CheckMode, CheckStepOutcome, coerceDispatchInputs, matchAllWorkflows, parseInputPairs } from "@kici-dev/engine";
19
19
  import os from "node:os";
20
20
  //#region src/local-executor/index.ts
21
21
  /**
@@ -136,8 +136,37 @@ async function runOneMatchedWorkflow(workflow, ctx) {
136
136
  * The actual DAG-execution body for one workflow. Split out so
137
137
  * {@link runOneMatchedWorkflow} can wrap it in a lock acquire/release.
138
138
  */
139
+ /**
140
+ * Resolve the dispatch-input descriptor declared on a workflow's `dispatch()`
141
+ * trigger(s) from the in-memory trigger objects, merged across triggers.
142
+ * Returns undefined when none declared.
143
+ */
144
+ function dispatchInputsDescriptorForWorkflow(workflow) {
145
+ const merged = {};
146
+ let found = false;
147
+ for (const trigger of transformTriggers(workflow.on)) if (trigger._type === "dispatch" && trigger.inputs) {
148
+ Object.assign(merged, trigger.inputs);
149
+ found = true;
150
+ }
151
+ return found ? merged : void 0;
152
+ }
153
+ /**
154
+ * Coerce + default the operator's `--input` pairs against the workflow's
155
+ * dispatch descriptor for a local run. The local executor is authoritative for
156
+ * `run local` (no orchestrator), so defaults are applied here exactly once.
157
+ * Returns the resolved values, or throws when input is invalid.
158
+ */
159
+ function resolveLocalDispatchInputs(workflow, options) {
160
+ const descriptor = dispatchInputsDescriptorForWorkflow(workflow);
161
+ const raw = parseInputPairs(options.inputs ?? []);
162
+ if (!descriptor) return raw;
163
+ const r = coerceDispatchInputs(raw, descriptor);
164
+ if ("error" in r) throw r.error;
165
+ return r.values;
166
+ }
139
167
  async function runWorkflowBody(workflow, ctx, options, secrets, kiciDir, concurrency, failFast) {
140
168
  const { event } = ctx;
169
+ const dispatchInputs = resolveLocalDispatchInputs(workflow, options);
141
170
  const resolvedJobs = await resolveJobs(workflow, event);
142
171
  let dagNodes = resolvedJobs.map((r) => ({
143
172
  name: r.expandedName,
@@ -170,7 +199,8 @@ async function runWorkflowBody(workflow, ctx, options, secrets, kiciDir, concurr
170
199
  execDir: ctx.execDir,
171
200
  jobOutputsMap,
172
201
  signal,
173
- checkMode: options.checkMode
202
+ checkMode: options.checkMode,
203
+ dispatchInputs
174
204
  });
175
205
  },
176
206
  isSuccess: (result) => result.status === "success" || result.status === "skipped"
@@ -223,7 +253,8 @@ async function executeLocal(options) {
223
253
  if (!await compileCommand({
224
254
  kiciDir,
225
255
  check: false,
226
- verbose: options.debug ?? false
256
+ verbose: options.debug ?? false,
257
+ quiet: Boolean(options.json || options.quiet)
227
258
  })) {
228
259
  process.exitCode = 2;
229
260
  return false;
@@ -268,6 +299,12 @@ async function executeLocal(options) {
268
299
  return true;
269
300
  }
270
301
  }
302
+ for (const workflow of matchedWorkflows) try {
303
+ resolveLocalDispatchInputs(workflow, options);
304
+ } catch (err) {
305
+ logger.error(pc.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
306
+ process.exit(2);
307
+ }
271
308
  const isQuiet = Boolean(options.quiet || options.json);
272
309
  const repoRoot = path.dirname(kiciDir);
273
310
  let materialized = null;
@@ -27,6 +27,8 @@ export interface JobExecutionContext {
27
27
  signal: AbortSignal;
28
28
  /** Run mode for idempotent steps. Defaults to `apply` when unset. */
29
29
  checkMode?: CheckMode;
30
+ /** Resolved (coerced + defaulted) workflow-dispatch inputs for `ctx.dispatchInputs`. */
31
+ dispatchInputs?: Readonly<Record<string, string | number | boolean | null>>;
30
32
  }
31
33
  /**
32
34
  * Resolve all jobs in a workflow, expanding matrix jobs and evaluating dynamic jobs.
@@ -8,6 +8,7 @@ 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 { computeBackoffDelay } from "@kici-dev/core";
11
12
  import { CheckMode, CheckStepOutcome, formatMatrixSuffix } from "@kici-dev/engine";
12
13
  import { runIdempotentStep } from "@kici-dev/core/idempotency";
13
14
  //#region src/local-executor/job-runner.ts
@@ -91,6 +92,33 @@ async function runLocalStepWithCheckMode(step, ctx, checkMode) {
91
92
  };
92
93
  }
93
94
  /**
95
+ * Run a local step through its retry policy. An attempt is one full
96
+ * `runLocalStepWithCheckMode` call (which throws on failure). A thrown attempt
97
+ * is retried while attempts remain AND `retryIf(err)` is true; backoff sleeps
98
+ * between attempts. Mirrors the agent step loop so `kici run local` retries
99
+ * identically to a remote run.
100
+ */
101
+ async function runLocalStepWithRetry(step, ctx, checkMode, log) {
102
+ const retry = step.retry;
103
+ const max = retry?.maxAttempts ?? 1;
104
+ let lastErr;
105
+ for (let n = 1; n <= max; n++) try {
106
+ return await runLocalStepWithCheckMode(step, ctx, checkMode);
107
+ } catch (err) {
108
+ lastErr = err;
109
+ if (!(n < max && (retry?.retryIf?.(err) ?? true))) break;
110
+ const delay = computeBackoffDelay(n, {
111
+ maxAttempts: max,
112
+ delayMs: retry.delayMs,
113
+ backoff: retry.backoff,
114
+ maxDelayMs: retry.maxDelayMs
115
+ });
116
+ log(`Step '${step.name}' attempt ${n}/${max} failed: ${err instanceof Error ? err.message : String(err)}; retrying in ${delay}ms`);
117
+ await new Promise((r) => setTimeout(r, delay));
118
+ }
119
+ throw lastErr;
120
+ }
121
+ /**
94
122
  * Maintain the base-name `{ byMatrix, merged }` envelope as each matrix child
95
123
  * completes. `merged` is rebuilt last-write-wins in suffix order so the result
96
124
  * is deterministic regardless of child completion order — matching the remote
@@ -145,7 +173,12 @@ async function resolveJobs(workflow, event) {
145
173
  query: () => Promise.resolve([]),
146
174
  get: () => Promise.resolve(null)
147
175
  },
148
- oidc: { token: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.oidc.token() is not available during local execution")) }
176
+ oidc: { token: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.oidc.token() is not available during local execution")) },
177
+ host: { requestReboot: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.host.requestReboot() is not available during local execution")) },
178
+ bootstrap: {
179
+ ensureInitRunner: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.bootstrap.ensureInitRunner() is not available during local execution")),
180
+ preBootSend: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.bootstrap.preBootSend() is not available during local execution"))
181
+ }
149
182
  }
150
183
  });
151
184
  for (const genJob of generatedJobs) {
@@ -256,7 +289,7 @@ async function executeResolvedJobInner(resolvedJob, context, startTime) {
256
289
  const sdkSetters = await resolveSdkSetters(context.kiciDir);
257
290
  let ruleResults;
258
291
  if (job.rules && job.rules.length > 0) {
259
- const ruleContext = createRuleContext(context.event, context.event.changedFiles);
292
+ const ruleContext = createRuleContext(context.event, context.event.changedFiles, context.dispatchInputs ?? {});
260
293
  const ruleEval = await evaluateRulesWithFormatting(job.rules, ruleContext, expandedName);
261
294
  ruleResults = ruleEval.results;
262
295
  if (!ruleEval.allPassed) return {
@@ -281,7 +314,7 @@ async function executeResolvedJobInner(resolvedJob, context, startTime) {
281
314
  const stepCtx = createStepContext({ name: context.workflowName }, {
282
315
  name: expandedName,
283
316
  runsOn: localRunsOnString(job.runsOn)
284
- }, repoRoot, void 0, hasMatrix ? matrixValues : void 0, context.secrets, void 0, context.event.payload, context.event.provider);
317
+ }, repoRoot, void 0, hasMatrix ? matrixValues : void 0, context.secrets, void 0, context.event.payload, context.event.provider, context.dispatchInputs ?? {});
285
318
  const stepResults = [];
286
319
  let stepCounter = 0;
287
320
  for (const stepOrFn of job.steps) {
@@ -312,7 +345,7 @@ async function executeResolvedJobInner(resolvedJob, context, startTime) {
312
345
  const stepStart = Date.now();
313
346
  try {
314
347
  const checkMode = context.checkMode ?? CheckMode.enum.apply;
315
- const phase = await runLocalStepWithCheckMode(normalizedStep, stepCtx, checkMode);
348
+ const phase = await runLocalStepWithRetry(normalizedStep, stepCtx, checkMode, (line) => formatter.logJobLine(expandedName, line));
316
349
  const outputs = phase.outputs;
317
350
  const stepDuration = Date.now() - stepStart;
318
351
  formatter.logStepComplete(expandedName, normalizedStep.name, stepDuration);
@@ -27,6 +27,8 @@ export interface RunLocalOptions {
27
27
  container?: boolean;
28
28
  /** --env KEY=VALUE overrides (repeatable) */
29
29
  env?: string[];
30
+ /** --input KEY=VALUE typed workflow-dispatch inputs (repeatable) */
31
+ inputs?: string[];
30
32
  /** --quiet: minimal output */
31
33
  quiet?: boolean;
32
34
  /** --json: JSON output format */
@@ -5,11 +5,11 @@ 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";
12
- import { normalizeRunsOnAllToMatchers, normalizeRunsOnToMatchers } from "@kici-dev/engine/labels/compile";
11
+ import { extractInputsDescriptorMap, resolveWhenToRunOn, validateResourceRequest } from "@kici-dev/engine";
12
+ import { normalizeRunsOnAllToMatchers, normalizeRunsOnToMatchers, runsOnPickFromInput } from "@kici-dev/engine/labels/compile";
13
13
  import { execSync } from "node:child_process";
14
14
  //#region src/lockfile/generator.ts
15
15
  /**
@@ -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
  /**
@@ -234,7 +234,8 @@ function toLockDispatch(t) {
234
234
  return {
235
235
  _type: "dispatch",
236
236
  types: t.types,
237
- ...reposField(t)
237
+ ...reposField(t),
238
+ ...t.inputs && { inputs: extractInputsDescriptorMap(t.inputs) }
238
239
  };
239
240
  }
240
241
  function toLockCreate(t) {
@@ -458,7 +459,8 @@ function normalizeRunsOnForLock(runsOn, jobName) {
458
459
  const { include, exclude } = normalizeRunsOnToMatchers(runsOn, `job '${jobName}' runsOn`);
459
460
  return {
460
461
  runsOn: include,
461
- ...exclude.length > 0 ? { excludeLabels: exclude } : {}
462
+ ...exclude.length > 0 ? { excludeLabels: exclude } : {},
463
+ runsOnPick: runsOnPickFromInput(runsOn)
462
464
  };
463
465
  }
464
466
  /**
@@ -540,6 +542,7 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
540
542
  ...job.runsOn !== void 0 ? normalizeRunsOnForLock(job.runsOn, job.name) : {},
541
543
  ...job.runsOnAll !== void 0 && { runsOnAll: normalizeRunsOnAllToMatchers(job.runsOnAll, `job '${job.name}' runsOnAll`) },
542
544
  ...job.onUnreachable !== void 0 && { onUnreachable: job.onUnreachable },
545
+ ...job.includeUninitialized !== void 0 && { includeUninitialized: job.includeUninitialized },
543
546
  ...job.maxParallel !== void 0 && { maxParallel: job.maxParallel },
544
547
  ...job.failFast !== void 0 && { failFast: job.failFast },
545
548
  ...resolveNeedsForLock(job.needs, uuidToName),
@@ -565,12 +568,13 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
565
568
  ...job.timeout !== void 0 && { timeout: job.timeout },
566
569
  ...job.resources !== void 0 && { resources: job.resources },
567
570
  ...job.init !== void 0 && { init: job.init },
568
- ...job.requireApproval !== void 0 && { approval: toLockApproval(job.requireApproval) }
571
+ ...job.approval !== void 0 && { approval: (assertNonStepApprovalScope(job.approval, "job"), toLockApproval(job.approval)) }
569
572
  };
570
573
  }
571
574
  /**
572
575
  * Resolve needs to lock file format.
573
- * Handles strings, Job objects, DynamicGroupRef, and object forms with ifFailed policy.
576
+ * Handles strings, Job objects, DynamicGroupRef, and object forms with a `when`
577
+ * run condition (normalized to a `runOn` status-set via the engine helper).
574
578
  * Uses the UUID-to-renamed-name mapping so that references to id-less jobs
575
579
  * resolve to their lock file names (job-N) instead of the original UUIDs.
576
580
  */
@@ -582,26 +586,26 @@ function resolveNeedsForLock(needs, uuidToName) {
582
586
  else if (isDynamicGroupRef(need)) {
583
587
  resolvedNeeds.push({
584
588
  group: need.group,
585
- ifFailed: need.ifFailed ?? "skip"
589
+ runOn: resolveWhenToRunOn(need.when)
586
590
  });
587
591
  groups.push(need.group);
592
+ } else if ("_tag" in need && need._tag === "Job") {
593
+ const name = need.name;
594
+ resolvedNeeds.push(uuidToName?.get(name) ?? name);
588
595
  } else if ("group" in need && typeof need.group === "string") {
589
596
  const g = need;
590
597
  resolvedNeeds.push({
591
598
  group: g.group,
592
- ifFailed: g.ifFailed ?? "skip"
599
+ runOn: resolveWhenToRunOn(g.when)
593
600
  });
594
601
  groups.push(g.group);
595
- } else if ("ifFailed" in need && "name" in need) {
602
+ } else {
596
603
  const n = need;
597
604
  const name = uuidToName?.get(n.name) ?? n.name;
598
605
  resolvedNeeds.push({
599
606
  name,
600
- ifFailed: n.ifFailed
607
+ runOn: resolveWhenToRunOn(n.when)
601
608
  });
602
- } else {
603
- const name = need.name;
604
- resolvedNeeds.push(uuidToName?.get(name) ?? name);
605
609
  }
606
610
  return {
607
611
  needs: resolvedNeeds,
@@ -613,15 +617,31 @@ function resolveNeedsForLock(needs, uuidToName) {
613
617
  * Assigns counter-based IDs to unnamed steps (bare functions and id-less steps).
614
618
  * Counter only increments for unnamed entries; named steps keep their names.
615
619
  */
616
- /** Map an SDK `requireApproval` to the normalized lock `approval` block. */
617
- function toLockApproval(r) {
618
- const n = normalizeRequireApproval(r);
620
+ /** Map an SDK `approval` to the normalized lock `approval` block. */
621
+ function toLockApproval(c) {
622
+ const n = normalizeApproval(c);
619
623
  return {
620
624
  clauses: n.clauses,
621
625
  ...n.reason !== void 0 && { reason: n.reason },
622
- ...n.timeoutSeconds !== void 0 && { timeoutSeconds: n.timeoutSeconds }
626
+ ...n.timeoutSeconds !== void 0 && { timeoutSeconds: n.timeoutSeconds },
627
+ when: n.when
623
628
  };
624
629
  }
630
+ /**
631
+ * Validate an approval config at job/workflow scope: `when: 'drift'` is a
632
+ * step-scope-only gate (it fires between a step's check and run), so it is a
633
+ * compile error anywhere else.
634
+ */
635
+ function assertNonStepApprovalScope(c, scope) {
636
+ if (normalizeApproval(c).when === "drift") throw new Error(`approval.when "drift" is only valid on steps (found at ${scope} scope)`);
637
+ }
638
+ /**
639
+ * Validate a step's approval config: `when: 'drift'` fires between the step's
640
+ * check and run, so it requires a `check` facet. A compile error otherwise.
641
+ */
642
+ function assertStepApprovalCheckFacet(step) {
643
+ 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`);
644
+ }
625
645
  function transformSteps(steps, gitRoot) {
626
646
  let stepCounter = 0;
627
647
  return steps.map((stepOrFn) => {
@@ -638,6 +658,12 @@ function transformSteps(steps, gitRoot) {
638
658
  hasOutputs: !!step.outputs && Object.keys(step.outputs).length > 0,
639
659
  ...step.continueOnError !== void 0 && { continueOnError: step.continueOnError },
640
660
  ...step.timeout !== void 0 && { timeout: step.timeout },
661
+ ...step.retry !== void 0 && { retry: {
662
+ maxAttempts: step.retry.maxAttempts,
663
+ delayMs: step.retry.delayMs,
664
+ backoff: step.retry.backoff,
665
+ maxDelayMs: step.retry.maxDelayMs
666
+ } },
641
667
  ...step.cache !== void 0 && { cache: normalizeCacheSpecs(step.cache) },
642
668
  ...step._sourceLocation && { sourceLocation: {
643
669
  file: makeRelativePath(step._sourceLocation.file, gitRoot),
@@ -652,7 +678,7 @@ function transformSteps(steps, gitRoot) {
652
678
  ...step.cleanup !== void 0 && { hasCleanup: true },
653
679
  ...step.check !== void 0 && { hasCheck: true },
654
680
  ...step.whenInSync !== void 0 && { hasWhenInSync: true },
655
- ...step.requireApproval !== void 0 && { approval: toLockApproval(step.requireApproval) }
681
+ ...step.approval !== void 0 && { approval: (assertStepApprovalCheckFacet(step), toLockApproval(step.approval)) }
656
682
  };
657
683
  });
658
684
  }
@@ -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,7 +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
+ import type { CheckMode, HostTargetSelector } from '@kici-dev/engine';
14
14
  export declare class AuthenticationError extends Error {
15
15
  constructor(message?: string);
16
16
  }
@@ -77,6 +77,17 @@ export interface PlatformTriggerInput {
77
77
  * agent step loop in the requested mode. Omitted means `apply`.
78
78
  */
79
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;
85
+ /**
86
+ * Raw operator-supplied `kici run --input KEY=VALUE` pairs (not defaulted /
87
+ * coerced). The Platform relays them verbatim; the orchestrator validates,
88
+ * coerces, and applies defaults authoritatively against the lock descriptor.
89
+ */
90
+ dispatchInputs?: Record<string, string>;
80
91
  }
81
92
  export interface PlatformTriggerResponse {
82
93
  runId: string;
@@ -232,6 +232,7 @@ function getSizeWarning(compressedSize) {
232
232
  */
233
233
  async function uploadTarball(opts) {
234
234
  const { tarballPath, signedUrl, orchestratorPublicKey, onProgress } = opts;
235
+ if (!signedUrl) throw new Error("The orchestrator did not return an upload URL, so the overlay cannot be uploaded. This usually means the orchestrator has no object storage configured for remote runs. Ask your orchestrator operator to enable cache storage (KICI_STORAGE_TYPE=s3 or filesystem).");
235
236
  const { encryptedPath, cliPublicKey } = await encryptTarball(tarballPath, orchestratorPublicKey);
236
237
  const encryptedData = await fs.readFile(encryptedPath);
237
238
  const encryptedSize = encryptedData.length;
@@ -1,6 +1,6 @@
1
1
  import "../chunk-BTugEXQM.js";
2
2
  //#region src/templates/package-json.ts
3
- const sdkVersion = "0.1.21";
3
+ const sdkVersion = "0.1.23";
4
4
  /**
5
5
  * Generate package.json content for .kici/ directory
6
6
  *
@@ -3,7 +3,7 @@ import type { RuleEvaluationResult } from '@kici-dev/sdk';
3
3
  /**
4
4
  * Create RuleContext for rule evaluation.
5
5
  */
6
- export declare function createRuleContext(event: EventPayload, changedFiles?: string[]): RuleContext;
6
+ export declare function createRuleContext(event: EventPayload, changedFiles?: string[], dispatchInputs?: Readonly<Record<string, string | number | boolean | null>>): RuleContext;
7
7
  /**
8
8
  * Evaluate rules with formatting output.
9
9
  * Wraps the SDK's evaluateRules() with a callback that logs each rule result.
@@ -8,11 +8,12 @@ initZx();
8
8
  /**
9
9
  * Create RuleContext for rule evaluation.
10
10
  */
11
- function createRuleContext(event, changedFiles = []) {
11
+ function createRuleContext(event, changedFiles = [], dispatchInputs = {}) {
12
12
  return {
13
13
  event,
14
14
  changedFiles,
15
15
  env: { ...process.env },
16
+ dispatchInputs,
16
17
  $
17
18
  };
18
19
  }
@@ -11,5 +11,5 @@ import type { StepContext, WorkflowInfo, JobInfo, MatrixValues } from '@kici-dev
11
11
  export declare function createStepContext(workflowInfo: WorkflowInfo, jobInfo: JobInfo, repoRoot: string, inputs?: Record<string, unknown>, matrix?: MatrixValues, testSecrets?: {
12
12
  flat: Record<string, string>;
13
13
  contexts: Record<string, Record<string, string>>;
14
- }, environment?: string, rawPayload?: Record<string, unknown>, provider?: string): StepContext;
14
+ }, environment?: string, rawPayload?: Record<string, unknown>, provider?: string, dispatchInputs?: Readonly<Record<string, string | number | boolean | null>>): StepContext;
15
15
  //# sourceMappingURL=step-context.d.ts.map
@@ -42,7 +42,7 @@ function createTestLogger(jobName) {
42
42
  * `ctx.$` would inherit `process.cwd()` — i.e. wherever the user invoked
43
43
  * `kici` — which silently breaks any step that uses relative paths.
44
44
  */
45
- function createStepContext(workflowInfo, jobInfo, repoRoot, inputs = {}, matrix, testSecrets, environment, rawPayload, provider) {
45
+ function createStepContext(workflowInfo, jobInfo, repoRoot, inputs = {}, matrix, testSecrets, environment, rawPayload, provider, dispatchInputs = {}) {
46
46
  const flat = testSecrets?.flat ?? {};
47
47
  const namespacedSecrets = testSecrets?.contexts ?? {};
48
48
  const mergedFlat = { ...flat };
@@ -110,6 +110,7 @@ function createStepContext(workflowInfo, jobInfo, repoRoot, inputs = {}, matrix,
110
110
  process.env.PATH = updated;
111
111
  },
112
112
  inputs,
113
+ dispatchInputs,
113
114
  workflow: workflowInfo,
114
115
  job: jobInfo,
115
116
  matrix,
@@ -137,7 +138,12 @@ function createStepContext(workflowInfo, jobInfo, repoRoot, inputs = {}, matrix,
137
138
  query: () => Promise.resolve([]),
138
139
  get: () => Promise.resolve(null)
139
140
  },
140
- oidc: { token: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.oidc.token() is not available in the local test runner")) }
141
+ oidc: { token: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.oidc.token() is not available in the local test runner")) },
142
+ host: { requestReboot: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.host.requestReboot() is not available in the local test runner")) },
143
+ bootstrap: {
144
+ ensureInitRunner: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.bootstrap.ensureInitRunner() is not available in the local test runner")),
145
+ preBootSend: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.bootstrap.preBootSend() is not available in the local test runner"))
146
+ }
141
147
  },
142
148
  cache: {
143
149
  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, InputsDescriptorMap } 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: 21;
31
+ export declare const SCHEMA_VERSION: 26;
27
32
  /**
28
33
  * Source file reference with meaningful path.
29
34
  * Format: file is relative path from git root, export uses hash syntax.
@@ -122,6 +127,8 @@ export interface LockDispatchTrigger {
122
127
  readonly _type: 'dispatch';
123
128
  readonly types: readonly string[];
124
129
  readonly repos?: readonly LockBranchPattern[];
130
+ /** Typed dispatch-input descriptors (from `dispatch({ inputs })`). */
131
+ readonly inputs?: InputsDescriptorMap;
125
132
  }
126
133
  /**
127
134
  * Create trigger in lock file.
@@ -367,15 +374,15 @@ export declare function isLockInlineValue(value: unknown): value is LockInlineVa
367
374
  * (e.g., `kici:role:builder`, `kici:role:init-runner`) are injected by the orchestrator
368
375
  * for internal job types (build/init) and are not user-settable.
369
376
  */
370
- /** Needs entry with per-edge failure policy (mirrors engine NeedsEntry). */
377
+ /** Needs entry with per-edge run-on status-set (mirrors engine NeedsEntry). */
371
378
  export interface LockNeedsEntry {
372
379
  readonly name: string;
373
- readonly ifFailed: 'skip' | 'run';
380
+ readonly runOn: ExecutionJobStatus[];
374
381
  }
375
382
  /** Needs group entry for dynamic group dependencies (mirrors engine NeedsGroupEntry). */
376
383
  export interface LockNeedsGroupEntry {
377
384
  readonly group: string;
378
- readonly ifFailed: 'skip' | 'run';
385
+ readonly runOn: ExecutionJobStatus[];
379
386
  }
380
387
  export interface LockJob {
381
388
  readonly _type: 'static';
@@ -387,6 +394,8 @@ export interface LockJob {
387
394
  readonly runsOnAll?: RunsOnAllPredicate;
388
395
  /** Failure policy for unreachable durable hosts in a runsOnAll fan-out. */
389
396
  readonly onUnreachable?: OnUnreachableMode;
397
+ /** Widen runsOnAll to declared-but-un-agented hosts (init-runner bring-up per fresh box). */
398
+ readonly includeUninitialized?: boolean;
390
399
  /** Fan-out concurrency width (sliding window; 1 = serial). Applies to matrix and runsOnAll. */
391
400
  readonly maxParallel?: number;
392
401
  /** Halt the fan-out on first child failure, skipping the held remainder. Default false. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kici-dev/compiler",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
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.21",
67
- "@kici-dev/engine": "0.1.21"
66
+ "@kici-dev/core": "0.1.23",
67
+ "@kici-dev/engine": "0.1.23"
68
68
  },
69
69
  "peerDependencies": {
70
- "@kici-dev/sdk": "0.1.21"
70
+ "@kici-dev/sdk": "0.1.23"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@types/proper-lockfile": "^4.1.4"