@kici-dev/compiler 0.1.22 → 0.1.24

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 (61) hide show
  1. package/dist/cli.js +34 -10
  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/index.d.ts +4 -2
  7. package/dist/commands/index.js +3 -2
  8. package/dist/commands/init.js +2 -2
  9. package/dist/commands/org.js +2 -2
  10. package/dist/commands/pat.d.ts +27 -0
  11. package/dist/commands/pat.js +76 -0
  12. package/dist/commands/preview.d.ts +88 -0
  13. package/dist/commands/{test.js → preview.js} +15 -14
  14. package/dist/commands/run.d.ts +27 -2
  15. package/dist/commands/run.js +117 -18
  16. package/dist/commands/test.d.ts +4 -0
  17. package/dist/commands/types.d.ts +2 -0
  18. package/dist/commands/types.js +1 -1
  19. package/dist/commands/verify-attestation.d.ts +4 -1
  20. package/dist/commands/verify-attestation.js +26 -10
  21. package/dist/fixtures/describe-event.d.ts +6 -0
  22. package/dist/fixtures/describe-event.js +18 -0
  23. package/dist/fixtures/picker.d.ts +19 -0
  24. package/dist/fixtures/picker.js +64 -0
  25. package/dist/generators/secrets-dts.js +2 -0
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +2 -2
  28. package/dist/llm-context/llms-architecture.txt +1440 -0
  29. package/dist/llm-context/llms-cli.txt +2509 -0
  30. package/dist/llm-context/llms-features.txt +2491 -0
  31. package/dist/llm-context/llms-full.txt +1364 -361
  32. package/dist/llm-context/llms-getting-started.txt +519 -0
  33. package/dist/llm-context/llms-patterns.txt +1324 -0
  34. package/dist/llm-context/llms-providers.txt +805 -0
  35. package/dist/llm-context/llms-sdk.txt +3844 -0
  36. package/dist/llm-context/llms.txt +16 -1
  37. package/dist/local-executor/index.js +42 -4
  38. package/dist/local-executor/job-runner.d.ts +2 -0
  39. package/dist/local-executor/job-runner.js +38 -6
  40. package/dist/local-executor/types.d.ts +2 -0
  41. package/dist/lockfile/generator.d.ts +10 -2
  42. package/dist/lockfile/generator.js +112 -49
  43. package/dist/remote/history.d.ts +1 -1
  44. package/dist/remote/history.js +1 -1
  45. package/dist/remote/local-repo-identity.d.ts +32 -0
  46. package/dist/remote/local-repo-identity.js +74 -0
  47. package/dist/remote/platform-client.d.ts +6 -0
  48. package/dist/remote/prod-defaults.d.ts +8 -0
  49. package/dist/remote/prod-defaults.js +9 -1
  50. package/dist/remote/uploader.js +1 -0
  51. package/dist/templates/agents-md.d.ts +1 -1
  52. package/dist/templates/agents-md.js +2 -2
  53. package/dist/templates/package-json.js +1 -1
  54. package/dist/test-runner/rule-evaluator.d.ts +1 -1
  55. package/dist/test-runner/rule-evaluator.js +2 -1
  56. package/dist/test-runner/step-context.d.ts +1 -1
  57. package/dist/test-runner/step-context.js +8 -2
  58. package/dist/types.d.ts +38 -7
  59. package/dist/types.js +5 -1
  60. package/package.json +4 -7
  61. 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) (190 KB) — Authoring API: workflow/job/step factories, triggers, rules, matrix, runtime, caching
14
+ - [cli](https://docs.kici.dev/llms-cli.txt) (124 KB) — Running the CLI: compile, test, run local/remote, auth, hooks, lock-file drift
15
+ - [features](https://docs.kici.dev/llms-features.txt) (124 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) (87 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
@@ -25,6 +37,7 @@ The full markdown bundle of every page indexed here is available at https://docs
25
37
  - [SDK reference: core](https://docs.kici.dev/user/sdk/core/): Factory functions (workflow, job, step) and authoring patterns: needs, output chaining, dynamic groups
26
38
  - [Event payload reference](https://docs.kici.dev/user/sdk/event-payloads/): Generated schema of the normalized event envelope passed to rules and dynamic functions.
27
39
  - [SDK reference: idempotent](https://docs.kici.dev/user/sdk/idempotent/): Idempotent helpers for declarative check / apply patterns inside workflow steps
40
+ - [SDK reference: parallel](https://docs.kici.dev/user/sdk/parallel/): Run independent steps concurrently within a single job, each as its own observable step
28
41
  - [SDK reference: rules, matrix, dynamic jobs](https://docs.kici.dev/user/sdk/rules-matrix-dynamic/): rule(), skip(), matrix builds (static + dynamic), and dynamicJob / dynamicGroup
29
42
  - [SDK reference: runsOnAll host fan-out](https://docs.kici.dev/user/sdk/runs-on-all/): Fan one job out to every matching connected host, one pinned execution per host
30
43
  - [SDK reference: runtime](https://docs.kici.dev/user/sdk/runtime/): Types index, StepContext, secrets, fixtures
@@ -35,8 +48,9 @@ The full markdown bundle of every page indexed here is available at https://docs
35
48
 
36
49
  ## CLI and authoring
37
50
 
51
+ - [Drive KiCI from your coding agent](https://docs.kici.dev/user/ai-agents/): Point a coding agent at KiCI's MCP server and let it trigger, read, and re-run your CI under your own identity.
38
52
  - [CLI authentication](https://docs.kici.dev/user/cli-auth/): Authenticate the KiCI CLI with browser OAuth, device flow, or API key paste
39
- - [CLI reference](https://docs.kici.dev/user/cli-reference/): All CLI commands: compile, run (local/remote), orchestrators, test, login, logout, org, diagnostics, runs (list/show/logs/rerun/cancel), secrets, types, fixture, init, hook, endpoints, workflows, docs, admin
53
+ - [CLI reference](https://docs.kici.dev/user/cli-reference/): All CLI commands: compile, run (local/remote), orchestrators, preview, login, logout, org, diagnostics, runs (list/show/logs/rerun/cancel), secrets, types, fixture, init, hook, endpoints, workflows, docs, admin
40
54
  - [Lifecycle hooks](https://docs.kici.dev/user/hooks/): SDK hook API for cancel, cleanup, success, failure, and step-level callbacks
41
55
  - [Lock file and workflow drift](https://docs.kici.dev/user/lock-file-and-drift/): Keep the lock file in sync with your workflow source and avoid drift
42
56
  - [Testing guide](https://docs.kici.dev/user/testing-guide/): Running remote test fixtures with kici run remote
@@ -66,5 +80,6 @@ The full markdown bundle of every page indexed here is available at https://docs
66
80
 
67
81
  ## Architecture overview
68
82
 
83
+ - [Configuration architecture](https://docs.kici.dev/architecture/configuration/): Config resolution chain, DB schema, encryption, hot-reload, cluster sync
69
84
  - [Data flows](https://docs.kici.dev/architecture/data-flows/): End-to-end data flows through the KiCI three-tier architecture
70
85
  - [Architecture overview](https://docs.kici.dev/architecture/overview/): Three-tier relay model, package structure, and component responsibilities
@@ -14,8 +14,9 @@ import { ConcurrencyKeyEvaluationError, acquireWorkflowLock } from "./workflow-l
14
14
  import path from "node:path";
15
15
  import pc from "picocolors";
16
16
  import { writeFile } from "node:fs/promises";
17
+ import { flattenStepInputs } from "@kici-dev/sdk";
17
18
  import { logger } from "@kici-dev/core";
18
- import { CheckMode, CheckStepOutcome, matchAllWorkflows } from "@kici-dev/engine";
19
+ import { CheckMode, CheckStepOutcome, coerceDispatchInputs, matchAllWorkflows, parseInputPairs } from "@kici-dev/engine";
19
20
  import os from "node:os";
20
21
  //#region src/local-executor/index.ts
21
22
  /**
@@ -55,7 +56,7 @@ function workflowsToLockFormat(workflows) {
55
56
  name: j.name,
56
57
  runsOn: j.runsOn,
57
58
  needs: j.needs?.map((n) => typeof n === "string" ? n : n.name) ?? [],
58
- steps: j.steps.map((s) => {
59
+ steps: flattenStepInputs(j.steps).map((s) => {
59
60
  if (typeof s === "function") return {
60
61
  name: "",
61
62
  hasOutputs: false
@@ -136,8 +137,37 @@ async function runOneMatchedWorkflow(workflow, ctx) {
136
137
  * The actual DAG-execution body for one workflow. Split out so
137
138
  * {@link runOneMatchedWorkflow} can wrap it in a lock acquire/release.
138
139
  */
140
+ /**
141
+ * Resolve the dispatch-input descriptor declared on a workflow's `dispatch()`
142
+ * trigger(s) from the in-memory trigger objects, merged across triggers.
143
+ * Returns undefined when none declared.
144
+ */
145
+ function dispatchInputsDescriptorForWorkflow(workflow) {
146
+ const merged = {};
147
+ let found = false;
148
+ for (const trigger of transformTriggers(workflow.on)) if (trigger._type === "dispatch" && trigger.inputs) {
149
+ Object.assign(merged, trigger.inputs);
150
+ found = true;
151
+ }
152
+ return found ? merged : void 0;
153
+ }
154
+ /**
155
+ * Coerce + default the operator's `--input` pairs against the workflow's
156
+ * dispatch descriptor for a local run. The local executor is authoritative for
157
+ * `run local` (no orchestrator), so defaults are applied here exactly once.
158
+ * Returns the resolved values, or throws when input is invalid.
159
+ */
160
+ function resolveLocalDispatchInputs(workflow, options) {
161
+ const descriptor = dispatchInputsDescriptorForWorkflow(workflow);
162
+ const raw = parseInputPairs(options.inputs ?? []);
163
+ if (!descriptor) return raw;
164
+ const r = coerceDispatchInputs(raw, descriptor);
165
+ if ("error" in r) throw r.error;
166
+ return r.values;
167
+ }
139
168
  async function runWorkflowBody(workflow, ctx, options, secrets, kiciDir, concurrency, failFast) {
140
169
  const { event } = ctx;
170
+ const dispatchInputs = resolveLocalDispatchInputs(workflow, options);
141
171
  const resolvedJobs = await resolveJobs(workflow, event);
142
172
  let dagNodes = resolvedJobs.map((r) => ({
143
173
  name: r.expandedName,
@@ -170,7 +200,8 @@ async function runWorkflowBody(workflow, ctx, options, secrets, kiciDir, concurr
170
200
  execDir: ctx.execDir,
171
201
  jobOutputsMap,
172
202
  signal,
173
- checkMode: options.checkMode
203
+ checkMode: options.checkMode,
204
+ dispatchInputs
174
205
  });
175
206
  },
176
207
  isSuccess: (result) => result.status === "success" || result.status === "skipped"
@@ -223,7 +254,8 @@ async function executeLocal(options) {
223
254
  if (!await compileCommand({
224
255
  kiciDir,
225
256
  check: false,
226
- verbose: options.debug ?? false
257
+ verbose: options.debug ?? false,
258
+ quiet: Boolean(options.json || options.quiet)
227
259
  })) {
228
260
  process.exitCode = 2;
229
261
  return false;
@@ -268,6 +300,12 @@ async function executeLocal(options) {
268
300
  return true;
269
301
  }
270
302
  }
303
+ for (const workflow of matchedWorkflows) try {
304
+ resolveLocalDispatchInputs(workflow, options);
305
+ } catch (err) {
306
+ logger.error(pc.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
307
+ process.exit(2);
308
+ }
271
309
  const isQuiet = Boolean(options.quiet || options.json);
272
310
  const repoRoot = path.dirname(kiciDir);
273
311
  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.
@@ -7,7 +7,8 @@ import { localRunsOnString } from "./runs-on-display.js";
7
7
  import { toEventPayload } from "./to-event-payload.js";
8
8
  import { pathToFileURL } from "node:url";
9
9
  import path from "node:path";
10
- import { applyIncludeExclude, expandMatrix, isDynamicJobFn, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
10
+ import { applyIncludeExclude, expandMatrix, flattenStepInputs, 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
@@ -146,7 +174,11 @@ async function resolveJobs(workflow, event) {
146
174
  get: () => Promise.resolve(null)
147
175
  },
148
176
  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")) }
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
+ }
150
182
  }
151
183
  });
152
184
  for (const genJob of generatedJobs) {
@@ -257,7 +289,7 @@ async function executeResolvedJobInner(resolvedJob, context, startTime) {
257
289
  const sdkSetters = await resolveSdkSetters(context.kiciDir);
258
290
  let ruleResults;
259
291
  if (job.rules && job.rules.length > 0) {
260
- const ruleContext = createRuleContext(context.event, context.event.changedFiles);
292
+ const ruleContext = createRuleContext(context.event, context.event.changedFiles, context.dispatchInputs ?? {});
261
293
  const ruleEval = await evaluateRulesWithFormatting(job.rules, ruleContext, expandedName);
262
294
  ruleResults = ruleEval.results;
263
295
  if (!ruleEval.allPassed) return {
@@ -282,10 +314,10 @@ async function executeResolvedJobInner(resolvedJob, context, startTime) {
282
314
  const stepCtx = createStepContext({ name: context.workflowName }, {
283
315
  name: expandedName,
284
316
  runsOn: localRunsOnString(job.runsOn)
285
- }, 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 ?? {}, context.signal);
286
318
  const stepResults = [];
287
319
  let stepCounter = 0;
288
- for (const stepOrFn of job.steps) {
320
+ for (const stepOrFn of flattenStepInputs(job.steps)) {
289
321
  if (context.signal.aborted) return {
290
322
  name: expandedName,
291
323
  status: "cancelled",
@@ -313,7 +345,7 @@ async function executeResolvedJobInner(resolvedJob, context, startTime) {
313
345
  const stepStart = Date.now();
314
346
  try {
315
347
  const checkMode = context.checkMode ?? CheckMode.enum.apply;
316
- const phase = await runLocalStepWithCheckMode(normalizedStep, stepCtx, checkMode);
348
+ const phase = await runLocalStepWithRetry(normalizedStep, stepCtx, checkMode, (line) => formatter.logJobLine(expandedName, line));
317
349
  const outputs = phase.outputs;
318
350
  const stepDuration = Date.now() - stepStart;
319
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 */
@@ -1,5 +1,5 @@
1
- import type { TriggerConfig } from '@kici-dev/sdk';
2
- import { type LockFile, type LockTrigger, type WorkflowWithSource } from '../types.js';
1
+ import type { StepInput, TriggerConfig } from '@kici-dev/sdk';
2
+ import { type LockFile, type LockTrigger, type LockStepEntry, type WorkflowWithSource } from '../types.js';
3
3
  /**
4
4
  * Detect git repository root by running `git rev-parse --show-toplevel`.
5
5
  * Falls back to cwd if not in a git repo.
@@ -40,6 +40,14 @@ export declare function generateLockFile(workflowsWithSource: WorkflowWithSource
40
40
  * Exported for reuse in the test runner's in-memory lock format conversion.
41
41
  */
42
42
  export declare function transformTriggers(triggers?: TriggerConfig[]): readonly LockTrigger[];
43
+ /**
44
+ * Transform a job's `steps` array into lock-file entries. Sequential steps and
45
+ * parallel-group children share one flat `step-N` counter (anonymous steps are
46
+ * numbered across the whole flattened sequence, parallel children inline) so the
47
+ * compiler's naming matches the agent's `extractAndNormalizeSteps` enumeration —
48
+ * the flat-stepIndex invariant.
49
+ */
50
+ export declare function transformSteps(steps: readonly StepInput[], gitRoot: string): readonly LockStepEntry[];
43
51
  /**
44
52
  * Serialize lock file to JSON string.
45
53
  *
@@ -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, normalizeApproval, normalizeCacheSpecs } from "@kici-dev/sdk";
8
+ import { getDynamicJobGroup, getDynamicJobNeeds, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isParallelGroup, 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 { resolveWhenToRunOn, validateResourceRequest } from "@kici-dev/engine";
12
- import { normalizeRunsOnAllToMatchers, normalizeRunsOnToMatchers } from "@kici-dev/engine/labels/compile";
11
+ import { assertScheduleInputsSatisfiable, 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
  /**
@@ -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) {
@@ -344,11 +345,14 @@ function toLockGenericWebhook(t) {
344
345
  };
345
346
  }
346
347
  function toLockSchedule(t) {
348
+ const inputs = t.inputs ? extractInputsDescriptorMap(t.inputs) : void 0;
349
+ if (inputs) assertScheduleInputsSatisfiable(inputs);
347
350
  return {
348
351
  _type: "schedule",
349
352
  cronExpression: t.cron,
350
353
  timezone: t.timezone,
351
- ...t.description && { description: t.description }
354
+ ...t.description && { description: t.description },
355
+ ...inputs && { inputs }
352
356
  };
353
357
  }
354
358
  function toLockLifecycle(t) {
@@ -458,7 +462,8 @@ function normalizeRunsOnForLock(runsOn, jobName) {
458
462
  const { include, exclude } = normalizeRunsOnToMatchers(runsOn, `job '${jobName}' runsOn`);
459
463
  return {
460
464
  runsOn: include,
461
- ...exclude.length > 0 ? { excludeLabels: exclude } : {}
465
+ ...exclude.length > 0 ? { excludeLabels: exclude } : {},
466
+ runsOnPick: runsOnPickFromInput(runsOn)
462
467
  };
463
468
  }
464
469
  /**
@@ -480,6 +485,34 @@ function validateRunsOn(runsOn, jobName) {
480
485
  if (overlap.length > 0) throw new Error(`Job "${jobName}": labels and exclude overlap on [${overlap.join(", ")}]. A label cannot be both required and excluded.`);
481
486
  }
482
487
  /**
488
+ * Transform one environment reference (static name or function) into a lock
489
+ * `{ value, dynamic }` entry. A function element is analyzed for purity: a pure
490
+ * function becomes an inline expression resolvable at two-phase eval; an impure
491
+ * one carries only the `dynamic` flag (the agent runs an init job to resolve it).
492
+ */
493
+ function transformEnvironmentRef(ref, jobName) {
494
+ if (typeof ref === "function") {
495
+ const fnSource = ref.toString();
496
+ const purity = analyzePurity(fnSource);
497
+ if (purity.pure) return {
498
+ value: {
499
+ _type: "inline",
500
+ expression: fnSource
501
+ },
502
+ dynamic: true
503
+ };
504
+ console.warn(`[kici] Job "${jobName}": environment function is not pure (${purity.reason}). An init job will be required, adding ~5-10s delay.`);
505
+ return {
506
+ value: "",
507
+ dynamic: true
508
+ };
509
+ }
510
+ return {
511
+ value: ref,
512
+ dynamic: false
513
+ };
514
+ }
515
+ /**
483
516
  * Transform a static job to lock file format.
484
517
  */
485
518
  function transformJob(job, configPath, index, gitRoot, uuidToName) {
@@ -490,18 +523,8 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
490
523
  if (!(job.matrix !== void 0 || job.runsOnAll !== void 0) && (job.maxParallel !== void 0 || job.failFast !== void 0)) console.warn(`[kici] job '${job.name}': maxParallel/failFast are ignored without matrix or runsOnAll (no fan-out to bound)`);
491
524
  if (job.runsOn !== void 0) validateRunsOn(job.runsOn, job.name);
492
525
  const environmentFields = {};
493
- if (job.environment !== void 0) {
494
- if (typeof job.environment === "function") {
495
- const fnSource = job.environment.toString();
496
- const purity = analyzePurity(fnSource);
497
- environmentFields.dynamicEnvironment = true;
498
- if (purity.pure) environmentFields.environment = {
499
- _type: "inline",
500
- expression: fnSource
501
- };
502
- else console.warn(`[kici] Job "${job.name}": environment function is not pure (${purity.reason}). An init job will be required, adding ~5-10s delay.`);
503
- } else if (typeof job.environment === "string") environmentFields.environment = job.environment;
504
- }
526
+ const envRefs = job.environments ?? (job.environment !== void 0 ? [job.environment] : void 0);
527
+ if (envRefs !== void 0 && envRefs.length > 0) environmentFields.environments = envRefs.map((ref) => transformEnvironmentRef(ref, job.name));
505
528
  const envFields = {};
506
529
  if (job.env !== void 0) {
507
530
  if (typeof job.env === "function") {
@@ -540,6 +563,7 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
540
563
  ...job.runsOn !== void 0 ? normalizeRunsOnForLock(job.runsOn, job.name) : {},
541
564
  ...job.runsOnAll !== void 0 && { runsOnAll: normalizeRunsOnAllToMatchers(job.runsOnAll, `job '${job.name}' runsOnAll`) },
542
565
  ...job.onUnreachable !== void 0 && { onUnreachable: job.onUnreachable },
566
+ ...job.includeUninitialized !== void 0 && { includeUninitialized: job.includeUninitialized },
543
567
  ...job.maxParallel !== void 0 && { maxParallel: job.maxParallel },
544
568
  ...job.failFast !== void 0 && { failFast: job.failFast },
545
569
  ...resolveNeedsForLock(job.needs, uuidToName),
@@ -639,39 +663,78 @@ function assertNonStepApprovalScope(c, scope) {
639
663
  function assertStepApprovalCheckFacet(step) {
640
664
  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
665
  }
666
+ /**
667
+ * Transform a job's `steps` array into lock-file entries. Sequential steps and
668
+ * parallel-group children share one flat `step-N` counter (anonymous steps are
669
+ * numbered across the whole flattened sequence, parallel children inline) so the
670
+ * compiler's naming matches the agent's `extractAndNormalizeSteps` enumeration —
671
+ * the flat-stepIndex invariant.
672
+ */
642
673
  function transformSteps(steps, gitRoot) {
643
- let stepCounter = 0;
644
- return steps.map((stepOrFn) => {
645
- if (typeof stepOrFn === "function") {
646
- stepCounter++;
647
- return {
648
- name: `step-${stepCounter}`,
649
- hasOutputs: false
650
- };
651
- }
652
- const step = stepOrFn;
674
+ const counter = { n: 0 };
675
+ let groupOrdinal = 0;
676
+ return steps.map((entry) => {
677
+ if (isParallelGroup(entry)) return transformParallelGroup(entry, gitRoot, counter, groupOrdinal++);
678
+ return transformSequentialStep(entry, gitRoot, counter);
679
+ });
680
+ }
681
+ /** Validate and transform a `ParallelGroup` into a `LockParallelStep`. */
682
+ function transformParallelGroup(group, gitRoot, counter, groupOrdinal) {
683
+ if (group.steps.length === 0) throw new Error("job step: empty parallel group not allowed");
684
+ const seen = /* @__PURE__ */ new Set();
685
+ const children = group.steps.map((child) => {
686
+ if (isParallelGroup(child)) throw new Error("job step: nested parallel groups are not supported");
687
+ const lockChild = transformSequentialStep(child, gitRoot, counter);
688
+ if (seen.has(lockChild.name)) throw new Error(`job step: duplicate step name '${lockChild.name}' in parallel group`);
689
+ seen.add(lockChild.name);
690
+ return lockChild;
691
+ });
692
+ if (children.length === 1) console.warn(`[kici] parallel group with a single step ('${children[0].name}') runs identically to a sequential step`);
693
+ return {
694
+ kind: "parallel",
695
+ name: group.name ?? `parallel-${groupOrdinal}`,
696
+ failFast: group.failFast,
697
+ ...group.maxParallel !== void 0 && { maxParallel: group.maxParallel },
698
+ children
699
+ };
700
+ }
701
+ /** Transform a single sequential step (or bare function) into a `LockStep`. */
702
+ function transformSequentialStep(stepOrFn, gitRoot, counter) {
703
+ if (typeof stepOrFn === "function") {
704
+ counter.n++;
653
705
  return {
654
- name: step.name || `step-${++stepCounter}`,
655
- hasOutputs: !!step.outputs && Object.keys(step.outputs).length > 0,
656
- ...step.continueOnError !== void 0 && { continueOnError: step.continueOnError },
657
- ...step.timeout !== void 0 && { timeout: step.timeout },
658
- ...step.cache !== void 0 && { cache: normalizeCacheSpecs(step.cache) },
659
- ...step._sourceLocation && { sourceLocation: {
660
- file: makeRelativePath(step._sourceLocation.file, gitRoot),
661
- line: step._sourceLocation.line,
662
- column: step._sourceLocation.column
663
- } },
664
- ...step.rules && step.rules.length > 0 && {
665
- hasRules: true,
666
- rules: transformRules(step.rules, makeRelativePath(step._sourceLocation?.file ?? "", gitRoot))
667
- },
668
- ...step.onCancel !== void 0 && { hasOnCancel: true },
669
- ...step.cleanup !== void 0 && { hasCleanup: true },
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)) }
706
+ name: `step-${counter.n}`,
707
+ hasOutputs: false
673
708
  };
674
- });
709
+ }
710
+ const step = stepOrFn;
711
+ return {
712
+ name: step.name || `step-${++counter.n}`,
713
+ hasOutputs: !!step.outputs && Object.keys(step.outputs).length > 0,
714
+ ...step.continueOnError !== void 0 && { continueOnError: step.continueOnError },
715
+ ...step.timeout !== void 0 && { timeout: step.timeout },
716
+ ...step.retry !== void 0 && { retry: {
717
+ maxAttempts: step.retry.maxAttempts,
718
+ delayMs: step.retry.delayMs,
719
+ backoff: step.retry.backoff,
720
+ maxDelayMs: step.retry.maxDelayMs
721
+ } },
722
+ ...step.cache !== void 0 && { cache: normalizeCacheSpecs(step.cache) },
723
+ ...step._sourceLocation && { sourceLocation: {
724
+ file: makeRelativePath(step._sourceLocation.file, gitRoot),
725
+ line: step._sourceLocation.line,
726
+ column: step._sourceLocation.column
727
+ } },
728
+ ...step.rules && step.rules.length > 0 && {
729
+ hasRules: true,
730
+ rules: transformRules(step.rules, makeRelativePath(step._sourceLocation?.file ?? "", gitRoot))
731
+ },
732
+ ...step.onCancel !== void 0 && { hasOnCancel: true },
733
+ ...step.cleanup !== void 0 && { hasCleanup: true },
734
+ ...step.check !== void 0 && { hasCheck: true },
735
+ ...step.whenInSync !== void 0 && { hasWhenInSync: true },
736
+ ...step.approval !== void 0 && { approval: (assertStepApprovalCheckFacet(step), toLockApproval(step.approval)) }
737
+ };
675
738
  }
676
739
  /**
677
740
  * Strip the `?t=...` query suffix added by cache-busting `import()` calls.
@@ -749,6 +812,6 @@ function serializeLockFile(lockFile, pretty = true) {
749
812
  return JSON.stringify(lockFile, null, pretty ? 2 : void 0);
750
813
  }
751
814
  //#endregion
752
- export { computeLockfileHash, detectGitRoot, generateLockFile, serializeLockFile, transformTriggers };
815
+ export { computeLockfileHash, detectGitRoot, generateLockFile, serializeLockFile, transformSteps, transformTriggers };
753
816
 
754
817
  //# sourceMappingURL=generator.js.map
@@ -2,7 +2,7 @@
2
2
  * Local run history manager.
3
3
  *
4
4
  * Persists test run history in ~/.kici/history/runs.json so developers
5
- * can review past runs (kici test --history) and inspect specific runs
5
+ * can review past runs (kici run remote --history) and inspect specific runs
6
6
  * (kici status <run-id>).
7
7
  */
8
8
  /** Status of a test run */
@@ -11,7 +11,7 @@ import picomatch from "picomatch";
11
11
  * Local run history manager.
12
12
  *
13
13
  * Persists test run history in ~/.kici/history/runs.json so developers
14
- * can review past runs (kici test --history) and inspect specific runs
14
+ * can review past runs (kici run remote --history) and inspect specific runs
15
15
  * (kici status <run-id>).
16
16
  */
17
17
  /** Maximum number of entries to keep in history */
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ /** Git hosts whose origin URL we can turn into a real `owner/repo` + web link. */
3
+ export declare const RecognizedProvider: z.ZodEnum<{
4
+ github: "github";
5
+ gitlab: "gitlab";
6
+ bitbucket: "bitbucket";
7
+ }>;
8
+ export type RecognizedProvider = z.infer<typeof RecognizedProvider>;
9
+ /** The literal provider value stamped when no recognized git origin exists. */
10
+ export declare const LOCAL_PROVIDER = "local";
11
+ /**
12
+ * Parse a git remote URL into `{ provider, owner/repo }` for a recognized host.
13
+ * Handles scp-style ssh (`git@host:owner/repo.git`), `ssh://`, and `https://`
14
+ * forms; strips a trailing `.git`. Returns null for any unrecognized host or
15
+ * unparseable input.
16
+ */
17
+ export declare function parseGitOrigin(url: string): {
18
+ provider: RecognizedProvider;
19
+ repoIdentifier: string;
20
+ } | null;
21
+ /** Read the `origin` remote URL of the working tree at `repoRoot`, or null. */
22
+ export declare function detectGitOrigin(repoRoot: string): string | null;
23
+ /**
24
+ * Build the repo identity stamped on a `kici run remote` run. A recognized git
25
+ * origin yields the real `owner/repo` + provider so the dashboard can link to
26
+ * it; otherwise a synthetic `local/<basename>` identity with provider `local`.
27
+ */
28
+ export declare function buildLocalRepoIdentity(repoRoot: string): {
29
+ repoIdentifier: string;
30
+ provider: string;
31
+ };
32
+ //# sourceMappingURL=local-repo-identity.d.ts.map
@@ -0,0 +1,74 @@
1
+ import "../chunk-BTugEXQM.js";
2
+ import path from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import { z } from "zod";
5
+ //#region src/remote/local-repo-identity.ts
6
+ /** Git hosts whose origin URL we can turn into a real `owner/repo` + web link. */
7
+ const RecognizedProvider = z.enum([
8
+ "github",
9
+ "gitlab",
10
+ "bitbucket"
11
+ ]);
12
+ /** Host → provider map. The keys are the canonical public hostnames. */
13
+ const HOST_PROVIDER = {
14
+ "github.com": "github",
15
+ "gitlab.com": "gitlab",
16
+ "bitbucket.org": "bitbucket"
17
+ };
18
+ /** The literal provider value stamped when no recognized git origin exists. */
19
+ const LOCAL_PROVIDER = "local";
20
+ /**
21
+ * Parse a git remote URL into `{ provider, owner/repo }` for a recognized host.
22
+ * Handles scp-style ssh (`git@host:owner/repo.git`), `ssh://`, and `https://`
23
+ * forms; strips a trailing `.git`. Returns null for any unrecognized host or
24
+ * unparseable input.
25
+ */
26
+ function parseGitOrigin(url) {
27
+ const trimmed = url.trim();
28
+ const scp = /^[^@]+@([^:]+):(.+?)(?:\.git)?$/.exec(trimmed);
29
+ const urlStyle = /^[a-z]+:\/\/(?:[^@/]+@)?([^/]+)\/(.+?)(?:\.git)?$/.exec(trimmed);
30
+ const match = scp ?? urlStyle;
31
+ if (!match) return null;
32
+ const provider = HOST_PROVIDER[match[1].toLowerCase()];
33
+ if (!provider) return null;
34
+ const repoIdentifier = match[2].replace(/^\/+|\/+$/g, "");
35
+ if (!repoIdentifier.includes("/")) return null;
36
+ return {
37
+ provider,
38
+ repoIdentifier
39
+ };
40
+ }
41
+ /** Read the `origin` remote URL of the working tree at `repoRoot`, or null. */
42
+ function detectGitOrigin(repoRoot) {
43
+ try {
44
+ return execSync("git remote get-url origin", {
45
+ cwd: repoRoot,
46
+ encoding: "utf-8",
47
+ stdio: [
48
+ "pipe",
49
+ "pipe",
50
+ "pipe"
51
+ ]
52
+ }).trim();
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ /**
58
+ * Build the repo identity stamped on a `kici run remote` run. A recognized git
59
+ * origin yields the real `owner/repo` + provider so the dashboard can link to
60
+ * it; otherwise a synthetic `local/<basename>` identity with provider `local`.
61
+ */
62
+ function buildLocalRepoIdentity(repoRoot) {
63
+ const origin = detectGitOrigin(repoRoot);
64
+ const parsed = origin ? parseGitOrigin(origin) : null;
65
+ if (parsed) return parsed;
66
+ return {
67
+ repoIdentifier: `local/${path.basename(repoRoot)}`,
68
+ provider: LOCAL_PROVIDER
69
+ };
70
+ }
71
+ //#endregion
72
+ export { LOCAL_PROVIDER, RecognizedProvider, buildLocalRepoIdentity, detectGitOrigin, parseGitOrigin };
73
+
74
+ //# sourceMappingURL=local-repo-identity.js.map
@@ -82,6 +82,12 @@ export interface PlatformTriggerInput {
82
82
  * the orchestrator, which intersects each runsOnAll roster with it.
83
83
  */
84
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>;
85
91
  }
86
92
  export interface PlatformTriggerResponse {
87
93
  runId: string;
@@ -15,4 +15,12 @@ export declare const PROD_PLATFORM_URL = "https://api.kici.dev";
15
15
  export declare const PROD_OIDC_ISSUER = "https://auth.kici.dev/realms/kici-internal";
16
16
  /** Public OIDC client id registered for the `kici` CLI. */
17
17
  export declare const PROD_OIDC_CLIENT_ID = "kici-cli";
18
+ /**
19
+ * Provenance trust root for `kici verify-attestation` when `--trust-root` is
20
+ * omitted: the hosted KiCI Platform's provenance issuer. The verifier appends
21
+ * `/.well-known/openid-configuration` and pins the token issuer to it. Pass
22
+ * `--trust-root` to verify against a different environment (e.g. staging) or an
23
+ * offline `{ issuer, jwks }` file.
24
+ */
25
+ export declare const PROD_PROVENANCE_ISSUER = "https://api.kici.dev";
18
26
  //# sourceMappingURL=prod-defaults.d.ts.map